mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-16 08:25:14 +02:00
* 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
133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import type { MemoryAction, MemoryAgentInput, MemoryAgentResult, MemoryAgentService } from './index.js';
|
|
|
|
export type MemoryRunStatus = 'running' | 'done' | 'error';
|
|
|
|
export interface MemoryRunRecord {
|
|
id: string;
|
|
status: MemoryRunStatus;
|
|
stage: string;
|
|
inputHash: string;
|
|
chatId: string | null;
|
|
outputSummary: MemoryAgentResult | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface MemoryRunStorePort {
|
|
createRunning(args: { inputHash: string; chatId?: string | null }): Promise<{ id: string }>;
|
|
markRunning(id: string, stage: string): Promise<void>;
|
|
markDone(id: string, outputSummary: MemoryAgentResult): Promise<void>;
|
|
markError(id: string, error: string): Promise<void>;
|
|
findById(id: string): Promise<MemoryRunRecord | null>;
|
|
}
|
|
|
|
export interface MemoryIngestServiceDeps {
|
|
memoryAgent: Pick<MemoryAgentService, 'ingest'>;
|
|
runs: MemoryRunStorePort;
|
|
}
|
|
|
|
export interface MemoryIngestStartResult {
|
|
runId: string;
|
|
}
|
|
|
|
export interface MemoryIngestStatus {
|
|
runId: string;
|
|
status: MemoryRunStatus;
|
|
stage: string;
|
|
done: boolean;
|
|
captured: {
|
|
wiki: string[];
|
|
sl: string[];
|
|
xrefs: string[];
|
|
};
|
|
error: string | null;
|
|
commitHash: string | null;
|
|
skillsLoaded: string[];
|
|
signalDetected: boolean;
|
|
}
|
|
|
|
function inputHash(input: MemoryAgentInput): string {
|
|
const stableInput = JSON.stringify({
|
|
userMessage: input.userMessage,
|
|
assistantMessage: input.assistantMessage ?? '',
|
|
connectionId: input.connectionId ?? null,
|
|
});
|
|
return createHash('sha256').update(stableInput).digest('hex');
|
|
}
|
|
|
|
function capturedKeys(actions: MemoryAction[]): MemoryIngestStatus['captured'] {
|
|
const wiki = new Set<string>();
|
|
const sl = new Set<string>();
|
|
const xrefs = new Set<string>();
|
|
|
|
for (const action of actions) {
|
|
if (action.target === 'wiki') {
|
|
wiki.add(action.key);
|
|
} else {
|
|
sl.add(action.key);
|
|
}
|
|
if (action.detail.toLowerCase().includes('xref') || action.detail.toLowerCase().includes('cross-ref')) {
|
|
xrefs.add(action.key);
|
|
}
|
|
}
|
|
|
|
return {
|
|
wiki: [...wiki].sort(),
|
|
sl: [...sl].sort(),
|
|
xrefs: [...xrefs].sort(),
|
|
};
|
|
}
|
|
|
|
export class MemoryIngestService {
|
|
private readonly inFlight = new Map<string, Promise<void>>();
|
|
|
|
constructor(private readonly deps: MemoryIngestServiceDeps) {}
|
|
|
|
async ingest(input: MemoryAgentInput): Promise<MemoryIngestStartResult> {
|
|
const row = await this.deps.runs.createRunning({
|
|
inputHash: inputHash(input),
|
|
chatId: input.chatId,
|
|
});
|
|
|
|
await this.deps.runs.markRunning(row.id, 'ingesting');
|
|
|
|
const run = this.runIngest(row.id, input);
|
|
this.inFlight.set(row.id, run);
|
|
run.finally(() => this.inFlight.delete(row.id)).catch(() => undefined);
|
|
|
|
return { runId: row.id };
|
|
}
|
|
|
|
async waitForRun(runId: string): Promise<void> {
|
|
await this.inFlight.get(runId);
|
|
}
|
|
|
|
private async runIngest(runId: string, input: MemoryAgentInput): Promise<void> {
|
|
try {
|
|
const outputSummary = await this.deps.memoryAgent.ingest(input);
|
|
await this.deps.runs.markDone(runId, outputSummary);
|
|
} catch (error) {
|
|
await this.deps.runs.markError(runId, error instanceof Error ? error.message : String(error));
|
|
}
|
|
}
|
|
|
|
async status(runId: string): Promise<MemoryIngestStatus | null> {
|
|
const row = await this.deps.runs.findById(runId);
|
|
if (!row) {
|
|
return null;
|
|
}
|
|
|
|
const output = row.outputSummary;
|
|
return {
|
|
runId: row.id,
|
|
status: row.status,
|
|
stage: row.stage,
|
|
done: row.status !== 'running',
|
|
captured: output ? capturedKeys(output.actions) : { wiki: [], sl: [], xrefs: [] },
|
|
error: row.error,
|
|
commitHash: output?.commitHash ?? null,
|
|
skillsLoaded: output?.skillsLoaded ?? [],
|
|
signalDetected: output?.signalDetected ?? false,
|
|
};
|
|
}
|
|
}
|