2026-05-10 23:12:26 +02:00
import { join } from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
import YAML from 'yaml' ;
import { localConnectionInfoFromConfig } from '../connections/index.js' ;
2026-05-10 23:51:24 +02:00
import type { KtxEmbeddingPort , KtxFileStorePort , KtxFileWriteResult } from '../core/index.js' ;
import { type KtxLogger , noopLogger , SessionWorktreeService } from '../core/index.js' ;
import type { KtxSemanticLayerComputePort } from '../daemon/index.js' ;
2026-05-21 10:38:23 +02:00
import type { KtxEmbeddingProvider } from '@ktx/llm' ;
2026-05-16 12:06:34 +02:00
import {
createLocalKtxLlmRuntimeFromConfig ,
2026-05-21 10:38:23 +02:00
KtxIngestEmbeddingPortAdapter ,
2026-05-16 12:06:34 +02:00
RuntimeAgentRunner ,
type AgentRunnerPort ,
type KtxLlmRuntimePort ,
type KtxRuntimeToolSet ,
} from '../llm/index.js' ;
2026-05-10 23:51:24 +02:00
import type { KtxLocalProject } from '../project/index.js' ;
2026-05-10 23:12:26 +02:00
import { PromptService } from '../prompts/index.js' ;
import { SkillsRegistryService } from '../skills/index.js' ;
import {
2026-05-10 23:51:24 +02:00
type KtxConnectionInfo ,
type KtxQueryResult ,
2026-05-10 23:12:26 +02:00
SemanticLayerService ,
type SemanticLayerSource ,
type SlConnectionCatalogPort ,
SlDiscoverTool ,
SlEditSourceTool ,
type SlPythonPort ,
SlReadSourceTool ,
SlRollbackTool ,
SlSearchService ,
type SlSourcesIndexPort ,
SlValidateTool ,
type SlValidationDeps ,
type SlValidatorPort ,
SlWriteSourceTool ,
SqliteSlSourcesIndex ,
sourceDefinitionSchema ,
sourceOverlaySchema ,
} from '../sl/index.js' ;
import { BaseTool , type GitAuthorResolverPort , type ToolContext } from '../tools/index.js' ;
import {
type KnowledgeEventPort ,
type KnowledgeIndexPort ,
2026-05-13 13:43:23 +02:00
type KnowledgeIndexPageListing ,
2026-05-10 23:12:26 +02:00
KnowledgeWikiService ,
searchLocalKnowledgePages ,
WikiListTagsTool ,
WikiReadTool ,
WikiRemoveTool ,
WikiSearchTool ,
WikiWriteTool ,
} from '../wiki/index.js' ;
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 ) ;
}