2026-05-10 23:12:26 +02:00
import { join } from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
import YAML from 'yaml' ;
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
import { localConnectionInfoFromConfig } from '../../context/connections/local-warehouse-descriptor.js' ;
import type { KtxEmbeddingPort } from '../../context/core/embedding.js' ;
import type { KtxFileStorePort , KtxFileWriteResult } from '../../context/core/file-store.js' ;
import { type KtxLogger , noopLogger } from '../../context/core/config.js' ;
import { SessionWorktreeService } from '../../context/core/session-worktree.service.js' ;
import type { KtxSemanticLayerComputePort } from '../../context/daemon/semantic-layer-compute.js' ;
import { KtxIngestEmbeddingPortAdapter } from '../../context/llm/embedding-port.js' ;
import { createLocalKtxLlmRuntimeFromConfig } from '../../context/llm/local-config.js' ;
import { RuntimeAgentRunner , type AgentRunnerPort , type KtxLlmRuntimePort , type KtxRuntimeToolSet } from '../../context/llm/runtime-port.js' ;
import type { KtxEmbeddingProvider } from '../../llm/types.js' ;
import type { KtxLocalProject } from '../../context/project/project.js' ;
import { PromptService } from '../../context/prompts/prompt.service.js' ;
import { SkillsRegistryService } from '../../context/skills/skills-registry.service.js' ;
import type { KtxConnectionInfo , KtxQueryResult , SlConnectionCatalogPort , SlPythonPort , SlSourcesIndexPort } from '../../context/sl/ports.js' ;
import { SemanticLayerService } from '../../context/sl/semantic-layer.service.js' ;
import type { SemanticLayerSource } from '../../context/sl/types.js' ;
import { SlDiscoverTool } from '../../context/sl/tools/sl-discover.tool.js' ;
import { SlEditSourceTool } from '../../context/sl/tools/sl-edit-source.tool.js' ;
import { SlReadSourceTool } from '../../context/sl/tools/sl-read-source.tool.js' ;
import { SlRollbackTool } from '../../context/sl/tools/sl-rollback.tool.js' ;
import { SlSearchService } from '../../context/sl/sl-search.service.js' ;
import { SlValidateTool } from '../../context/sl/tools/sl-validate.tool.js' ;
import type { SlValidationDeps } from '../../context/sl/tools/sl-warehouse-validation.js' ;
import type { SlValidatorPort } from '../../context/sl/sl-validator.port.js' ;
import { SlWriteSourceTool } from '../../context/sl/tools/sl-write-source.tool.js' ;
import { SqliteSlSourcesIndex } from '../../context/sl/sqlite-sl-sources-index.js' ;
import { sourceDefinitionSchema , sourceOverlaySchema } from '../../context/sl/schemas.js' ;
import { BaseTool , type ToolContext } from '../../context/tools/base-tool.js' ;
import type { GitAuthorResolverPort } from '../../context/tools/authors.js' ;
import type { KnowledgeEventPort , KnowledgeIndexPort , KnowledgeIndexPageListing } from '../../context/wiki/ports.js' ;
import { KnowledgeWikiService } from '../../context/wiki/knowledge-wiki.service.js' ;
import { searchLocalKnowledgePages } from '../../context/wiki/local-knowledge.js' ;
import { WikiListTagsTool } from '../../context/wiki/tools/wiki-list-tags.tool.js' ;
import { WikiReadTool } from '../../context/wiki/tools/wiki-read.tool.js' ;
import { WikiRemoveTool } from '../../context/wiki/tools/wiki-remove.tool.js' ;
import { WikiSearchTool } from '../../context/wiki/tools/wiki-search.tool.js' ;
import { WikiWriteTool } from '../../context/wiki/tools/wiki-write.tool.js' ;
2026-05-10 23:12:26 +02:00
import { LocalMemoryRunStore } from './local-memory-runs.js' ;
import { MemoryAgentService } from './memory-agent.service.js' ;
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
import { MemoryIngestService } from './memory-runs.js' ;
2026-05-10 23:12:26 +02:00
import type {
MemoryConnectionPort ,
MemoryFileStorePort ,
MemoryKnowledgeSlRefsPort ,
MemorySlSourceReconcilerPort ,
MemoryToolSetLike ,
MemoryToolsetFactoryPort ,
} from './types.js' ;
const promptsDir = fileURLToPath ( new URL ( '../../prompts' , import . meta . url ) ) ;
const skillsDir = fileURLToPath ( new URL ( '../../skills' , import . meta . url ) ) ;
2026-05-10 23:51:24 +02:00
const LOCAL_AUTHOR = { name : 'KTX Local' , email : 'local@ktx.local' } ;
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
const LOCAL_SHAPE_WARNING = 'Local memory ingest validates semantic-layer YAML shape only.' ;
2026-05-10 23:12:26 +02:00
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
export interface CreateLocalProjectMemoryIngestOptions {
2026-05-16 12:06:34 +02:00
llmRuntime? : KtxLlmRuntimePort ;
agentRunner? : AgentRunnerPort ;
2026-05-10 23:12:26 +02:00
memoryModel? : string ;
2026-05-10 23:51:24 +02:00
semanticLayerCompute? : KtxSemanticLayerComputePort ;
queryExecutor ? : { execute ( input : { connectionId : string ; sql : string ; maxRows? : number } ) : Promise < KtxQueryResult > } ;
2026-05-10 23:12:26 +02:00
runIdFactory ? : ( ) = > string ;
2026-05-10 23:51:24 +02:00
logger? : KtxLogger ;
2026-05-21 10:38:23 +02:00
embeddingProvider? : KtxEmbeddingProvider | null ;
2026-05-10 23:12:26 +02:00
}
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
export function createLocalProjectMemoryIngest (
2026-05-10 23:51:24 +02:00
project : KtxLocalProject ,
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
options : CreateLocalProjectMemoryIngestOptions = { } ,
) : MemoryIngestService {
2026-05-10 23:12:26 +02:00
const logger = options . logger ? ? noopLogger ;
const rootFileStore = new LocalMemoryFileStore ( project . fileStore ) ;
2026-05-21 10:38:23 +02:00
const embedding = options . embeddingProvider
? new KtxIngestEmbeddingPortAdapter ( options . embeddingProvider )
: new NoopEmbeddingPort ( ) ;
if ( ! options . embeddingProvider && project . config . ingest . embeddings . backend !== 'none' ) {
// Memory-agent search (SlSearch, wiki) embeds against this port. With Noop the
// configured backend is silently inert — the agent will see empty vectors and
// rank results against zeros. Surface that so the caller knows to plumb the
// resolved embedding provider through.
logger . warn (
` [memory-ingest] embeddings backend " ${ project . config . ingest . embeddings . backend } " is configured but no embedding provider was passed; semantic search will fall back to a no-op embedding port. ` ,
) ;
}
2026-05-10 23:12:26 +02:00
const knowledgeIndex = new LocalKnowledgeIndex ( project ) ;
const knowledgeEvents = new NoopKnowledgeEventPort ( ) ;
const knowledgeSlRefs = new NoopKnowledgeSlRefsPort ( ) ;
const connections = new LocalMemoryConnections ( project , options . queryExecutor ) ;
const slPython = new LocalSlPythonPort ( options . semanticLayerCompute ) ;
const semanticLayerService = new SemanticLayerService ( rootFileStore , connections , slPython , logger ) ;
2026-05-10 23:51:24 +02:00
const slSourcesRepository = new SqliteSlSourcesIndex ( { dbPath : join ( project . projectDir , '.ktx' , 'db.sqlite' ) } ) ;
2026-05-10 23:12:26 +02:00
const slSearchService = new SlSearchService ( embedding , slSourcesRepository , logger ) ;
const wikiService = new KnowledgeWikiService ( rootFileStore , embedding , knowledgeIndex , project . git , logger ) ;
const authorResolver = new LocalAuthorResolver ( ) ;
2026-05-16 12:06:34 +02:00
const llmRuntime =
options . llmRuntime ? ? createLocalKtxLlmRuntimeFromConfig ( project . config . llm , { projectDir : project.projectDir } ) ;
2026-05-10 23:12:26 +02:00
const toolsetFactory = new LocalMemoryToolsetFactory ( {
project ,
embedding ,
wikiService ,
knowledgeIndex ,
knowledgeEvents ,
semanticLayerService ,
slSearchService ,
authorResolver ,
slSourcesRepository ,
connections ,
} ) ;
const agentRunner =
options . agentRunner ? ?
2026-05-16 12:06:34 +02:00
new RuntimeAgentRunner ( requireLlmRuntime ( llmRuntime ) ) ;
2026-05-10 23:12:26 +02:00
const memoryAgent = new MemoryAgentService ( {
settings : {
knowledge : { userScopedKnowledgeEnabled : false } ,
slValidation : { probeRowCount : 0 } ,
llm : { memoryIngestionModel : project.config.llm.models.default ? ? 'local-memory-model' } ,
} ,
promptService : new PromptService ( { promptsDir , partials : [ ] } ) ,
skillsRegistry : new SkillsRegistryService ( { skillsDir } ) ,
wikiService ,
knowledgeIndex ,
knowledgeSlRefs ,
semanticLayerService ,
slSearchService ,
connections ,
rootFileStore ,
gitService : project.git ,
lockingService : new LocalMemoryLock ( ) ,
slSourcesRepository ,
sessionWorktreeService : new SessionWorktreeService ( {
coreConfig : project.coreConfig ,
gitService : project.git ,
configService : rootFileStore ,
} ) ,
semanticLayerSourceReconciler : new NoopSemanticLayerSourceReconciler ( ) ,
agentRunner ,
slValidator : new LocalShapeOnlySlValidator ( ) ,
toolsetFactory ,
logger ,
} ) ;
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
return new MemoryIngestService ( {
2026-05-10 23:12:26 +02:00
memoryAgent ,
runs : new LocalMemoryRunStore ( { projectDir : project.projectDir , idFactory : options.runIdFactory } ) ,
} ) ;
}
2026-05-16 12:06:34 +02:00
function requireLlmRuntime ( runtime : KtxLlmRuntimePort | null | undefined ) : KtxLlmRuntimePort {
if ( ! runtime ) {
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
throw new Error ( 'createLocalProjectMemoryIngest requires llm.provider.backend or an injected agentRunner' ) ;
2026-05-10 23:12:26 +02:00
}
2026-05-16 12:06:34 +02:00
return runtime ;
2026-05-10 23:12:26 +02:00
}
class LocalMemoryFileStore implements MemoryFileStorePort {
2026-05-10 23:51:24 +02:00
constructor ( private readonly fileStore : MemoryFileStorePort | KtxFileStorePort ) { }
2026-05-10 23:12:26 +02:00
forWorktree ( workdir : string ) : LocalMemoryFileStore {
2026-05-10 23:51:24 +02:00
return new LocalMemoryFileStore ( this . fileStore . forWorktree ( workdir ) as KtxFileStorePort ) ;
2026-05-10 23:12:26 +02:00
}
2026-05-10 23:51:24 +02:00
writeFile ( . . . args : Parameters < KtxFileStorePort [ 'writeFile' ] > ) : Promise < KtxFileWriteResult > {
2026-05-10 23:12:26 +02:00
return this . fileStore . writeFile ( . . . args ) ;
}
2026-05-10 23:51:24 +02:00
readFile ( . . . args : Parameters < KtxFileStorePort [ 'readFile' ] > ) {
2026-05-10 23:12:26 +02:00
return this . fileStore . readFile ( . . . args ) ;
}
2026-05-10 23:51:24 +02:00
deleteFile ( . . . args : Parameters < KtxFileStorePort [ 'deleteFile' ] > ) {
2026-05-10 23:12:26 +02:00
return this . fileStore . deleteFile ( . . . args ) ;
}
2026-05-10 23:51:24 +02:00
listFiles ( . . . args : Parameters < KtxFileStorePort [ 'listFiles' ] > ) {
2026-05-10 23:12:26 +02:00
return this . fileStore . listFiles ( . . . args ) ;
}
2026-05-10 23:51:24 +02:00
getFileHistory ( . . . args : Parameters < KtxFileStorePort [ 'getFileHistory' ] > ) {
2026-05-10 23:12:26 +02:00
return this . fileStore . getFileHistory ( . . . args ) ;
}
async enqueueCommitMessageJobForExternalCommit ( ) : Promise < void > { }
}
2026-05-10 23:51:24 +02:00
class NoopEmbeddingPort implements KtxEmbeddingPort {
2026-05-10 23:12:26 +02:00
readonly maxBatchSize = 64 ;
async computeEmbedding ( ) : Promise < number [ ] > {
return [ ] ;
}
async computeEmbeddingsBulk ( texts : string [ ] ) : Promise < number [ ] [ ] > {
return texts . map ( ( ) = > [ ] ) ;
}
}
class LocalKnowledgeIndex implements KnowledgeIndexPort {
2026-05-10 23:51:24 +02:00
constructor ( private readonly project : KtxLocalProject ) { }
2026-05-10 23:12:26 +02:00
async upsertPage ( ) : Promise < void > { }
async applyDiffTransactional ( ) : Promise < void > { }
async getExistingSearchTexts ( ) : Promise < Map < string , { searchText : string ; hasEmbedding : boolean } > > {
return new Map ( ) ;
}
2026-05-20 01:36:54 +02:00
async deleteStale ( ) : Promise < number > {
return 0 ;
}
2026-05-10 23:12:26 +02:00
2026-05-20 01:36:54 +02:00
async deleteByScope ( ) : Promise < number > {
return 0 ;
}
2026-05-10 23:12:26 +02:00
2026-05-20 01:36:54 +02:00
async deleteByKey ( ) : Promise < number > {
return 0 ;
}
2026-05-10 23:12:26 +02:00
async findPageByKey ( scope : string , scopeId : string | null , pageKey : string ) {
const path = this . pagePath ( scope , scopeId , pageKey ) ;
try {
await this . project . fileStore . readFile ( path ) ;
return { page_key : pageKey } ;
} catch {
return null ;
}
}
async listPagesForUser ( userId : string ) {
2026-05-13 13:43:23 +02:00
const pages : KnowledgeIndexPageListing [ ] = [ ] ;
2026-05-10 23:12:26 +02:00
for ( const scope of [
2026-05-13 16:05:58 +02:00
{ scope : 'GLOBAL' , scopeId : null , dir : 'wiki/global' } ,
{ scope : 'USER' , scopeId : userId , dir : ` wiki/user/ ${ userId } ` } ,
2026-05-10 23:12:26 +02:00
] ) {
const listed = await this . project . fileStore . listFiles ( scope . dir , true ) ;
for ( const file of listed . files . filter ( ( entry ) = > entry . endsWith ( '.md' ) ) ) {
const pageKey = file . replace ( /\.md$/ , '' ) ;
const raw = await this . project . fileStore . readFile ( ` ${ scope . dir } / ${ file } ` ) ;
const parsed = parseWiki ( raw . content ) ;
pages . push ( {
page_key : pageKey ,
summary : parsed.summary ,
scope : scope.scope ,
scope_id : scope.scopeId ,
2026-05-13 13:43:23 +02:00
tags : parseWikiTags ( raw . content ) ,
2026-05-10 23:12:26 +02:00
} ) ;
}
}
return pages . sort ( ( a , b ) = > a . page_key . localeCompare ( b . page_key ) ) ;
}
async getUserPageCount ( userId : string ) : Promise < number > {
return ( await this . listPagesForUser ( userId ) ) . filter ( ( page ) = > page . scope === 'USER' ) . length ;
}
async incrementUsageCount ( ) : Promise < void > { }
async searchRRF ( _userId : string , _embedding : number [ ] | null , queryText : string , limit : number ) {
const pages = await this . listPagesForUser ( _userId ) ;
return pages
. map ( ( page ) = > ( {
pageKey : page.page_key ,
summary : page.summary ,
rrfScore : scoreText ( ` ${ page . page_key } ${ page . summary } ` , queryText ) ,
} ) )
. filter ( ( page ) = > page . rrfScore > 0 )
. sort ( ( a , b ) = > b . rrfScore - a . rrfScore || a . pageKey . localeCompare ( b . pageKey ) )
. slice ( 0 , limit ) ;
}
private pagePath ( scope : string , scopeId : string | null , pageKey : string ) : string {
2026-05-13 16:05:58 +02:00
return scope === 'GLOBAL' ? ` wiki/global/ ${ pageKey } .md ` : ` wiki/user/ ${ scopeId } / ${ pageKey } .md ` ;
2026-05-10 23:12:26 +02:00
}
}
class NoopKnowledgeEventPort implements KnowledgeEventPort {
async createEvent ( ) : Promise < void > { }
}
class NoopKnowledgeSlRefsPort implements MemoryKnowledgeSlRefsPort {
async syncFromWiki ( ) : Promise < { inserted : number ; deleted : number } > {
return { inserted : 0 , deleted : 0 } ;
}
}
class LocalMemoryConnections implements MemoryConnectionPort , SlConnectionCatalogPort {
constructor (
2026-05-10 23:51:24 +02:00
private readonly project : KtxLocalProject ,
2026-05-10 23:12:26 +02:00
private readonly queryExecutor ? : {
2026-05-10 23:51:24 +02:00
execute ( input : { connectionId : string ; sql : string ; maxRows? : number } ) : Promise < KtxQueryResult > ;
2026-05-10 23:12:26 +02:00
} ,
) { }
2026-05-10 23:51:24 +02:00
async listEnabledConnections ( ids : string [ ] ) : Promise < KtxConnectionInfo [ ] > {
2026-05-10 23:12:26 +02:00
return ids
. map ( ( id ) = > localConnectionInfoFromConfig ( id , this . project . config . connections [ id ] ) )
2026-05-10 23:51:24 +02:00
. filter ( ( connection ) : connection is KtxConnectionInfo = > connection !== null ) ;
2026-05-10 23:12:26 +02:00
}
2026-05-10 23:51:24 +02:00
async getConnectionById ( connectionId : string ) : Promise < KtxConnectionInfo > {
2026-05-10 23:12:26 +02:00
const connection = localConnectionInfoFromConfig ( connectionId , this . project . config . connections [ connectionId ] ) ;
if ( ! connection ) {
throw new Error ( ` Connection not found: ${ connectionId } ` ) ;
}
return connection ;
}
2026-05-10 23:51:24 +02:00
async executeQuery ( connectionId : string , sql : string ) : Promise < KtxQueryResult > {
2026-05-10 23:12:26 +02:00
if ( ! this . queryExecutor ) {
throw new Error ( 'Local memory capture has no query executor configured' ) ;
}
return this . queryExecutor . execute ( { connectionId , sql } ) ;
}
}
class LocalSlPythonPort implements SlPythonPort {
2026-05-10 23:51:24 +02:00
constructor ( private readonly compute? : KtxSemanticLayerComputePort ) { }
2026-05-10 23:12:26 +02:00
async validateSources ( input : Parameters < SlPythonPort [ 'validateSources' ] > [ 0 ] ) {
if ( ! this . compute ) {
return {
data : {
errors : [ ] ,
warnings : [ LOCAL_SHAPE_WARNING ] ,
per_source_warnings : { } ,
} ,
} ;
}
const result = await this . compute . validateSources ( {
sources : input.sources ,
dialect : input.dialect ,
recentlyTouched : input.recently_touched ,
} ) ;
return {
data : {
errors : result.errors ,
warnings : result.warnings ,
per_source_warnings : result.perSourceWarnings ,
} ,
} ;
}
async query ( input : Parameters < SlPythonPort [ 'query' ] > [ 0 ] ) {
if ( ! this . compute ) {
return { error : 'Local memory capture has no semantic compute adapter configured' } ;
}
const result = await this . compute . query ( {
sources : input.sources ,
dialect : input.dialect ,
query : input.query ,
} ) ;
return { data : { sql : result.sql , plan : result.plan } } ;
}
}
class LocalAuthorResolver implements GitAuthorResolverPort {
async resolve() {
return LOCAL_AUTHOR ;
}
}
class LocalMemoryLock {
async withLock < T > ( _key : 'config:repo' , fn : ( ) = > Promise < T > ) : Promise < T > {
return fn ( ) ;
}
}
class NoopSemanticLayerSourceReconciler implements MemorySlSourceReconcilerPort {
async upsertRow ( ) : Promise < void > { }
}
class LocalShapeOnlySlValidator implements SlValidatorPort < SlValidationDeps > {
async validateSingleSource ( deps : SlValidationDeps , connectionId : string , sourceName : string ) {
try {
const file = await deps . semanticLayerService . readSourceFile ( connectionId , sourceName ) ;
const parsed = YAML . parse ( file . content ) as SemanticLayerSource ;
const isOverlay = parsed . table == null && parsed . sql == null ;
const result = ( isOverlay ? sourceOverlaySchema : sourceDefinitionSchema ) . safeParse ( parsed ) ;
return result . success
? { errors : [ ] , warnings : [ LOCAL_SHAPE_WARNING ] }
: {
errors : result.error.issues.map (
( issue ) = > ` ${ sourceName } : ${ issue . path . join ( '.' ) || 'source' } ${ issue . message } ` ,
) ,
warnings : [ ] ,
} ;
} catch ( error ) {
return { errors : [ ` ${ sourceName } : ${ error instanceof Error ? error.message : String ( error ) } ` ] , warnings : [ ] } ;
}
}
}
class LocalMemoryToolSet implements MemoryToolSetLike {
constructor ( private readonly tools : BaseTool [ ] ) { }
2026-05-16 12:06:34 +02:00
toRuntimeTools ( context : ToolContext ) : KtxRuntimeToolSet {
return Object . fromEntries ( this . tools . map ( ( tool ) = > [ tool . name , tool . toRuntimeTool ( context ) ] ) ) ;
2026-05-10 23:12:26 +02:00
}
}
class LocalMemoryToolsetFactory implements MemoryToolsetFactoryPort {
private readonly wikiTools : BaseTool [ ] ;
private readonly slTools : BaseTool [ ] ;
constructor ( deps : {
2026-05-10 23:51:24 +02:00
project : KtxLocalProject ;
embedding : KtxEmbeddingPort ;
2026-05-10 23:12:26 +02:00
wikiService : KnowledgeWikiService ;
knowledgeIndex : KnowledgeIndexPort ;
knowledgeEvents : KnowledgeEventPort ;
semanticLayerService : SemanticLayerService ;
slSearchService : SlSearchService ;
authorResolver : GitAuthorResolverPort ;
slSourcesRepository : SlSourcesIndexPort ;
connections : SlConnectionCatalogPort ;
} ) {
const slDeps = {
semanticLayerService : deps.semanticLayerService ,
slSearchService : deps.slSearchService ,
authorResolver : deps.authorResolver ,
} ;
this . wikiTools = [
new WikiReadTool ( deps . wikiService , deps . knowledgeIndex ) ,
new WikiSearchTool ( {
search : async ( input ) = > {
const results = await searchLocalKnowledgePages ( deps . project , {
userId : input.userId ,
query : input.query ,
limit : input.limit ,
embeddingService : deps.embedding ,
} ) ;
return {
results : results.slice ( 0 , input . limit ) . map ( ( result ) = > ( {
key : result.key ,
path : result.path ,
summary : result.summary ,
score : result.score ,
matchReasons : result.matchReasons ,
lanes : result.lanes ,
} ) ) ,
totalFound : results.length ,
} ;
} ,
} ) ,
2026-05-13 13:43:23 +02:00
new WikiListTagsTool ( deps . knowledgeIndex ) ,
2026-05-10 23:12:26 +02:00
new WikiWriteTool ( deps . wikiService , deps . knowledgeIndex , deps . knowledgeEvents ) ,
new WikiRemoveTool ( deps . wikiService , deps . knowledgeIndex , deps . knowledgeEvents ) ,
] ;
this . slTools = [
new SlDiscoverTool ( slDeps , { maxSources : 25 , minRrfScore : 0 , maxDetailedSources : 5 } ) ,
new SlEditSourceTool ( slDeps ) ,
new SlReadSourceTool ( slDeps ) ,
new SlWriteSourceTool ( slDeps ) ,
new SlValidateTool ( slDeps ) ,
new SlRollbackTool ( deps . slSourcesRepository , deps . connections , 0 ) ,
] ;
}
createIngestWuToolset ( ) : MemoryToolSetLike {
return new LocalMemoryToolSet ( [ . . . this . wikiTools , . . . this . slTools ] ) ;
}
createToolset ( ) : MemoryToolSetLike {
return new LocalMemoryToolSet ( this . wikiTools ) ;
}
}
function parseWiki ( raw : string ) : { summary : string ; content : string } {
const match = raw . match ( /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/ ) ;
if ( ! match ) {
return { summary : '' , content : raw.trim ( ) } ;
}
const frontmatter = ( YAML . parse ( match [ 1 ] ) ? ? { } ) as Record < string , unknown > ;
return {
summary : typeof frontmatter . summary === 'string' ? frontmatter . summary : '' ,
content : match [ 2 ] . trim ( ) ,
} ;
}
2026-05-13 13:43:23 +02:00
function parseWikiTags ( raw : string ) : string [ ] {
const match = raw . match ( /^---\n([\s\S]*?)\n---\n?/ ) ;
if ( ! match ) {
return [ ] ;
}
const frontmatter = ( YAML . parse ( match [ 1 ] ) ? ? { } ) as Record < string , unknown > ;
return Array . isArray ( frontmatter . tags )
? frontmatter . tags . filter ( ( tag ) : tag is string = > typeof tag === 'string' )
: [ ] ;
}
2026-05-10 23:12:26 +02:00
function scoreText ( text : string , query : string ) : number {
const normalized = query . toLowerCase ( ) . trim ( ) ;
if ( ! normalized ) {
return 0 ;
}
const haystack = text . toLowerCase ( ) ;
if ( haystack . includes ( normalized ) ) {
return 1 ;
}
const words = normalized . split ( /\s+/ ) . filter ( Boolean ) ;
return words . filter ( ( word ) = > haystack . includes ( word ) ) . length / Math . max ( words . length , 1 ) ;
}