@luisarg/memory-auto 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/package.json +35 -7
- package/server/__init__.py +1 -0
- package/server/cli.py +27 -0
- package/server/pyproject.toml +14 -0
- package/server/registry.py +141 -0
- package/server/server.py +399 -0
- package/server/store.py +827 -0
- package/server/uv.lock +783 -0
- package/vault/README.md +38 -0
- package/vault/tag-vocabulary.json +175 -0
- package/vault/templates/context.md +20 -0
- package/vault/templates/convention.md +13 -0
- package/vault/templates/decision.md +23 -0
- package/vault/templates/fact.md +14 -0
- package/vault/templates/idea.md +28 -0
- package/vault/templates/learning.md +23 -0
- package/vault/templates/profile.md +13 -0
- package/vault/templates/source.md +21 -0
- package/vault/type-registry.yaml +75 -0
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts"],"sourcesContent":[],"mappings":";;;;cAkBa,IAAA;UAEI,MAAA;EAFJ,UAAI,EAAA,MAAA;EAEA,SAAM,EAAA,MAAA;EAUV,QAAA,EAQX,MAAA;EAoCc,KAAA,EAAA,MAAK;;;;;cA5CR,QAAQ,OAAO;iBA4CZ,KAAA,MAAW,iBAAiB"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { cpSync, existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { basename, isAbsolute, join } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import Schema from "@deepseek-ai/schemastery";
|
|
5
6
|
import { readFile } from "node:fs/promises";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
@@ -516,6 +517,15 @@ function resolveUnderHome(value, fallbackSegment) {
|
|
|
516
517
|
if (v.length === 0) return join(dshHome(), fallbackSegment);
|
|
517
518
|
return isAbsolute(v) ? v : join(dshHome(), v);
|
|
518
519
|
}
|
|
520
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
521
|
+
/** Copy the bundled dir into `target` when `key` is missing there. */
|
|
522
|
+
function ensure(target, bundled, key) {
|
|
523
|
+
if (existsSync(join(target, key))) return false;
|
|
524
|
+
if (!existsSync(bundled)) return false;
|
|
525
|
+
mkdirSync(target, { recursive: true });
|
|
526
|
+
cpSync(bundled, target, { recursive: true });
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
519
529
|
function apply(ctx, config) {
|
|
520
530
|
if (!config.enabled) {
|
|
521
531
|
console.log("[memory-auto] disabled via config");
|
|
@@ -523,6 +533,8 @@ function apply(ctx, config) {
|
|
|
523
533
|
}
|
|
524
534
|
const memoryPath = resolveUnderHome(config.memoryPath, "memory-vault");
|
|
525
535
|
const serverDir = resolveUnderHome(config.serverDir, "memory-vault-server");
|
|
536
|
+
if (ensure(serverDir, join(packageRoot, "server"), "server.py")) console.log(`[memory-auto] installed memory-vault-server -> ${serverDir}`);
|
|
537
|
+
if (ensure(memoryPath, join(packageRoot, "vault"), "type-registry.yaml")) console.log(`[memory-auto] installed vault starter -> ${memoryPath}`);
|
|
526
538
|
const digestConfig = {
|
|
527
539
|
memoryPath,
|
|
528
540
|
serverDir,
|
|
@@ -531,8 +543,8 @@ function apply(ctx, config) {
|
|
|
531
543
|
maxTokens: config.maxTokens,
|
|
532
544
|
minTranscriptChars: config.minTranscriptChars
|
|
533
545
|
};
|
|
534
|
-
if (!existsSync(serverDir)) console.warn(`[memory-auto] vault server not found at ${serverDir} — the digest cannot write.
|
|
535
|
-
if (!existsSync(memoryPath)) console.warn(`[memory-auto] vault not found at ${memoryPath} —
|
|
546
|
+
if (!existsSync(join(serverDir, "server.py"))) console.warn(`[memory-auto] vault server not found at ${serverDir} and not bundled — the digest cannot write. Set DSH_MEMORY_SERVER_DIR (or run \`node scripts/bundle-assets.mjs\` in a checkout).`);
|
|
547
|
+
if (!existsSync(join(memoryPath, "type-registry.yaml"))) console.warn(`[memory-auto] vault starter not found at ${memoryPath} — searches will fail until it exists.`);
|
|
536
548
|
const states = /* @__PURE__ */ new Map();
|
|
537
549
|
const activities = /* @__PURE__ */ new Map();
|
|
538
550
|
const queued = /* @__PURE__ */ new Map();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["sysParts: string[]","chunks: string[]","valid: ValidEntry[]","name","messages: Message[]","options: GenerateOptions","child: ChildProcess","msg: any","entries: ValidEntry[]","client: McpClient","Config: Schema<Config>","digestConfig: DigestConfig","sid: string | undefined"],"sources":["../src/pure.ts","../src/digest.ts","../src/plugin.ts"],"sourcesContent":["/**\n * DSH memory plugin (adapted from the OpenCode memory plugin).\n *\n * Implements session lifecycle hooks:\n * - session.created: project name resolution only (no memory auto-injection)\n * - session.idle: auto-capture gate (skip if no activity or already delivered)\n * - tool.execute.after: detect `git commit*` and queue a checkpoint\n * - tui.prompt.append: deliver queued checkpoint on next user message\n * - experimental.session.compacting: pre-compaction capture (always fires)\n * - session.end: invoke post-session digest\n * - /brain search|recall|profile: opt-in reads via MCP (2s health check)\n * - /checkpoint: manual structured review\n * - OpenCode version guard: warn on < 1.17.10, disable gracefully\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\n// ── Constants ────────────────────────────────────────────────────────────\n\nexport const MIN_OPENCODE_VERSION = \"1.17.10\";\nexport const MCP_UNREACHABLE =\n \"> ⚠️ Memory server unreachable — search cannot be completed.\";\nconst CHECKPOINT_MARKER = \"[memory-checkpoint]\";\n\n// ── Version guard ────────────────────────────────────────────────────────\n\n/** Compare two \"x.y.z\" semver strings. Returns negative/0/positive. */\nfunction compareSemver(a: string, b: string): number {\n const [a1, a2, a3] = a.split(\".\").map((n) => parseInt(n, 10) || 0);\n const [b1, b2, b3] = b.split(\".\").map((n) => parseInt(n, 10) || 0);\n if (a1 !== b1) return a1 - b1;\n if (a2 !== b2) return a2 - b2;\n return (a3 || 0) - (b3 || 0);\n}\n\nexport function isSupportedVersion(version: string): boolean {\n return compareSemver(version, MIN_OPENCODE_VERSION) >= 0;\n}\n\n// ── Project name resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the project name from a working directory.\n * Priority:\n * 1. OpenSpec presence: if `openspec/` exists, use the basename.\n * 2. package.json -> name\n * 3. pyproject.toml -> [project] -> name\n * 4. README.md: first 5 lines, heading pattern `# <ProjectName>`\n * 5. Fallback: basename of working directory\n */\nexport async function resolveProjectName(cwd: string): Promise<string> {\n if (existsSync(join(cwd, \"openspec\"))) {\n return basename(cwd);\n }\n // package.json\n const pkgPath = join(cwd, \"package.json\");\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(await readFile(pkgPath, \"utf-8\"));\n if (typeof pkg.name === \"string\" && pkg.name.trim()) {\n return pkg.name.trim();\n }\n } catch {\n // ignore parse errors; try next strategy\n }\n }\n // pyproject.toml (minimal regex parse)\n const pyprojectPath = join(cwd, \"pyproject.toml\");\n if (existsSync(pyprojectPath)) {\n try {\n const text = await readFile(pyprojectPath, \"utf-8\");\n const m = text.match(/\\[project\\][^\\[]*?name\\s*=\\s*[\"']([^\"']+)[\"']/);\n if (m) return m[1];\n } catch {\n // ignore\n }\n }\n // README.md heading\n const readmePath = join(cwd, \"README.md\");\n if (existsSync(readmePath)) {\n try {\n const text = await readFile(readmePath, \"utf-8\");\n const head = text.split(\"\\n\").slice(0, 5);\n for (const line of head) {\n const m = line.match(/^#\\s+(.+)$/);\n if (m) return m[1].trim();\n }\n } catch {\n // ignore\n }\n }\n return basename(cwd);\n}\n\n// ── Checkpoint state ─────────────────────────────────────────────────────\n\nexport interface SessionState {\n project: string;\n hasActivity: boolean;\n checkpointDelivered: boolean;\n queuedCheckpoint: string | null;\n}\n\nexport function createSessionState(project: string): SessionState {\n return {\n project,\n hasActivity: false,\n checkpointDelivered: false,\n queuedCheckpoint: null,\n };\n}\n\n/**\n * Build the checkpoint prompt body. Pure function — exported for testing.\n */\nexport function buildCheckpointPrompt(state: SessionState, activitySummary: string): string {\n return [\n `${CHECKPOINT_MARKER} End-of-session memory capture for project \\`${state.project}\\`.`,\n \"\",\n \"Tracked activity:\",\n activitySummary.trim() || \"(none recorded)\",\n \"\",\n \"Write OKF entries for any notable decisions, facts, or learnings using the `store_*` MCP tools.\",\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n/**\n * Decide whether to deliver a checkpoint on `session.idle`.\n * Returns the prompt to deliver, or null to skip.\n */\nexport function idleCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n if (state.checkpointDelivered) return null;\n state.checkpointDelivered = true;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n/**\n * Decide whether to fire on `experimental.session.compacting`.\n * Per spec: always fires when activity exists, even if checkpoint was delivered.\n */\nexport function compactingCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n// ── Git commit detection ─────────────────────────────────────────────────\n\nconst GIT_COMMIT_PATTERN = /git\\s+commit\\b/;\n\nexport function isGitCommit(command: string): boolean {\n return GIT_COMMIT_PATTERN.test(command);\n}\n\nexport function buildCommitCheckpointPrompt(state: SessionState): string {\n return [\n `${CHECKPOINT_MARKER} Memory capture after \\`git commit\\` in project \\`${state.project}\\`.`,\n \"\",\n \"Review the staged/committed changes and write OKF entries for any notable decisions, facts, or learnings.\",\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n// ── In-process session digest (ctx.llm) ──────────────────────────────────\n\n/**\n * Entry types the vault's MCP server can store via `store_*` tools. The\n * extraction prompt is restricted to these so every produced entry has a\n * write path (no `idea`/`context`/`source` — those have no store tool).\n */\nexport const EXTRACTABLE_TYPES = ['decision', 'fact', 'learning', 'convention'] as const\nexport type ExtractableType = (typeof EXTRACTABLE_TYPES)[number]\n\n/** One validated OKF entry ready to be stored through the vault server. */\nexport interface ValidEntry {\n entry_type: ExtractableType\n content: string\n description: string\n tags: string[]\n confidence: number\n openspec_change_id: string | null\n}\n\n/** Optional vault context files to embed in the extraction prompt. */\nexport interface DigestContextFiles {\n criticalFacts?: string\n claude?: string\n}\n\n/**\n * Build the system + user messages for the in-process extraction call.\n * The system part instructs the model to return a JSON array restricted to\n * EXTRACTABLE_TYPES; the user part carries the transcript.\n */\nexport function buildExtractionPrompt(\n project: string,\n transcript: string,\n contextFiles: DigestContextFiles = {},\n): { system: string; user: string } {\n const sysParts: string[] = [\n 'You are an assistant that extracts durable knowledge from a session transcript.',\n ]\n if (contextFiles.criticalFacts?.trim()) {\n sysParts.push(`Always-loaded context: CRITICAL_FACTS.md\\n${contextFiles.criticalFacts.trim()}`)\n }\n if (contextFiles.claude?.trim()) {\n sysParts.push(`Always-loaded context: _CLAUDE.md\\n${contextFiles.claude.trim()}`)\n }\n sysParts.push(\n `For the transcript of project \\`${project}\\`, identify:`,\n '- **Decisions**: architectural or design choices that were made.',\n '- **Facts**: stable, verifiable statements about the project (versions, conventions, constraints).',\n '- **Learnings**: non-obvious lessons, debugging insights, or solutions found.',\n '- **Conventions**: style rules, naming patterns, coding conventions agreed.',\n '',\n 'Return a JSON array. Each element must have exactly:',\n ' - \"entry_type\": one of \"decision\" | \"fact\" | \"learning\" | \"convention\"',\n ' - \"content\": a single-paragraph statement (no headings, no lists)',\n ' - \"description\": a one-sentence summary of `content` (queryable)',\n ' - \"tags\": an array of lowercase-kebab tags (never empty if possible, at least 1 like architecture/python/testing)',\n ' - \"confidence\": a number 0.0-1.0',\n ' - \"openspec_change_id\": (optional) the change slug if the transcript names it',\n '',\n 'Rules:',\n '- Skip trivial exchanges (greetings, \"ok\", \"thanks\", or anything with no project knowledge).',\n '- Prefer fewer, higher-signal entries over many weak ones.',\n '- Do not include anything not present in the transcript.',\n '',\n 'Return only the JSON array. No prose, no markdown fences.',\n )\n return {\n system: sysParts.join('\\n'),\n user: `Project: ${project}\\n\\nTranscript:\\n---\\n${transcript}\\n---`,\n }\n}\n\n/**\n * Split an oversized transcript into overlapping chunks (mirrors the legacy\n * digest script: 25k chars per chunk, 1k overlap, at most `cap` chunks).\n */\nexport function chunkTranscript(text: string, maxLen = 50_000, chunkSize = 25_000, overlap = 1_000, cap = 3): string[] {\n if (text.length <= maxLen) return [text]\n const chunks: string[] = []\n let i = 0\n while (i < text.length) {\n chunks.push(text.slice(i, i + chunkSize))\n i += chunkSize - overlap\n if (chunks.length >= cap) break\n }\n return chunks\n}\n\n/** Best-effort repair of common LLM JSON output; returns parsed value or null. */\nexport function repairJson(text: string): unknown {\n const s = text.trim()\n // Strip code fences: ```json ... ```\n const fenced = s.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '').trim()\n // Trailing commas: `,]` / `,}` -> `]` / `}`\n const noTrailing = fenced.replace(/,(\\s*[\\]}])/g, '$1')\n const candidates = [s, fenced, noTrailing]\n // Single-quote to double-quote conversion only when no double quotes exist.\n if (noTrailing.includes(\"'\") && !noTrailing.includes('\"')) {\n candidates.push(convertSingleQuotes(noTrailing))\n }\n for (const c of candidates) {\n try {\n return JSON.parse(c)\n } catch {\n // try the next candidate\n }\n }\n return null\n}\n\nfunction convertSingleQuotes(text: string): string {\n let out = ''\n let inString = false\n let i = 0\n while (i < text.length) {\n const ch = text[i]\n if (inString && ch === '\\\\') {\n if (i + 1 < text.length && text[i + 1] === \"'\") {\n out += \"'\"\n i += 2\n continue\n }\n out += ch\n if (i + 1 < text.length) {\n out += text[i + 1]\n i += 2\n continue\n }\n i += 1\n continue\n }\n if (ch === \"'\") {\n inString = !inString\n out += '\"'\n i += 1\n continue\n }\n out += ch\n i += 1\n }\n return out\n}\n\n/**\n * Validate a parsed extraction payload into OKF entries. Unknown types,\n * empty content and malformed values are dropped; tags and confidence are\n * normalized. Returns the valid entries (possibly empty).\n */\nexport function validateEntries(payload: unknown): ValidEntry[] {\n if (!Array.isArray(payload)) return []\n const valid: ValidEntry[] = []\n for (const item of payload) {\n if (typeof item !== 'object' || item === null) continue\n const raw = item as Record<string, unknown>\n const entryType = raw.entry_type\n if (typeof entryType !== 'string' || !(EXTRACTABLE_TYPES as readonly string[]).includes(entryType)) continue\n const content = typeof raw.content === 'string' ? raw.content.trim() : ''\n if (!content) continue\n const tags = Array.isArray(raw.tags) ? raw.tags.filter((t): t is string => typeof t === 'string' && t.length > 0) : []\n let confidence = 1\n if (typeof raw.confidence === 'number' && Number.isFinite(raw.confidence)) {\n confidence = Math.max(0, Math.min(1, raw.confidence))\n }\n const description = typeof raw.description === 'string' ? raw.description.trim() : ''\n const changeId = typeof raw.openspec_change_id === 'string' && raw.openspec_change_id ? raw.openspec_change_id : null\n valid.push({\n entry_type: entryType as ExtractableType,\n content,\n description,\n tags,\n confidence,\n openspec_change_id: changeId,\n })\n }\n return valid\n}\n\n// The full hook wiring is exposed for testing; the actual OpenCode integration\n// is done in `register.ts` (the entry point the OpenCode runtime loads via\n// package.json \"main\"). This module intentionally has no default export:\n// opencode 1.18.x only loads plugin modules with a single export.\nexport const __testing = {\n createSessionState,\n isSupportedVersion,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n};\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport readline from 'node:readline'\nimport { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm'\nimport type { FinishReason, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'\nimport type { Context } from '@deepseek-ai/cordis'\nimport {\n buildExtractionPrompt,\n chunkTranscript,\n repairJson,\n validateEntries,\n type ValidEntry,\n} from './pure.js'\n\n// Post-session digest runner: in-process LLM extraction via the harness's own\n// `ctx.llm` service (no external CLI, no credentials of its own), followed by\n// OKF writes through the vault's MCP server over stdio (`store_*` tools keep\n// the Markdown source of truth and the SQLite FTS5 index in sync).\n\nconst MIN_DIGEST_TRANSCRIPT_CHARS = 200\nconst LLM_RETRIES = 3\nconst LLM_RETRY_DELAYS_MS = [2_000, 5_000]\nconst MCP_CALL_TIMEOUT_MS = 60_000\n\nfunction log(msg: string) {\n console.log(`[memory-auto] ${msg}`)\n}\n\nexport function transcriptOfDSM(events: any[]): string {\n // DSH SessionEvent -> transcript\n return (events ?? [])\n .map((ev: any) => {\n const t = ev?.type ?? ''\n const d = ev?.data ?? ev\n if (t === 'user/message' || t === 'user_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : JSON.stringify(d).slice(0, 500)\n return `## user\\n${text}`\n }\n if (t === 'assistant/message' || t === 'assistant_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : ''\n return text ? `## assistant\\n${text}` : null\n }\n if (t === 'tool/call' || t === 'tool_call') {\n const name = d.tool ?? d.name ?? 'tool'\n const args = d.args ?? d.arguments ?? {}\n return `## tool_call ${name}\\n${JSON.stringify(args).slice(0, 1000)}`\n }\n if (t === 'tool/result' || t === 'tool_result') {\n const out = typeof d.output === 'string' ? d.output : JSON.stringify(d).slice(0, 1000)\n return `## tool_result\\n${out}`\n }\n if (t.startsWith('compaction')) return `## ${t}\\n${JSON.stringify(d).slice(0, 500)}`\n return null\n })\n .filter((x): x is string => Boolean(x))\n .join('\\n\\n')\n}\n\n/** Translate a terminal stream finish into a thrown error, mirroring dsh's own summarizers. */\nfunction finishError(finish: FinishReason): Error | undefined {\n switch (finish.kind) {\n case 'error':\n case 'aborted': {\n const error = new Error(finish.failure.message) as Error & { code?: string }\n error.code = finish.failure.code\n return error\n }\n default:\n return undefined\n }\n}\n\nexport interface DigestConfig {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n}\n\n/**\n * Run one extraction call through the harness's `ctx.llm` service: build the\n * prompt, stream, assemble, repair and validate the JSON entries.\n */\nexport async function extractEntriesWithLlm(\n ctx: Context,\n config: DigestConfig,\n project: string,\n transcript: string,\n contextFiles?: { criticalFacts?: string; claude?: string },\n signal?: AbortSignal,\n): Promise<ValidEntry[]> {\n const { system, user } = buildExtractionPrompt(project, transcript, contextFiles)\n const assembler = new BlockAssembler()\n const messages: Message[] = [\n createUserMessage({\n content: [{ type: 'text', text: user }],\n source: { kind: 'plugin', plugin: 'memory-auto' },\n }),\n ]\n const options: GenerateOptions = {\n provider: config.provider,\n model: config.model,\n messages,\n system,\n maxTokens: config.maxTokens,\n ...(signal === undefined ? {} : { signal }),\n }\n for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk)\n const error = finishError(assembler.finish)\n if (error !== undefined) throw error\n const text = assembler\n .blocks()\n .filter((b) => b.type === 'text')\n .map((b) => b.text)\n .join('')\n const parsed = repairJson(text)\n return validateEntries(parsed)\n}\n\n/** Minimal MCP stdio client for the vault server (spawned via `uv run`). */\nexport interface McpClient {\n callTool(name: string, args: Record<string, unknown>, timeoutMs?: number): Promise<unknown>\n close(): Promise<void>\n}\n\nexport function connectMcp(memoryPath: string, serverDir: string): Promise<McpClient> {\n return new Promise((resolve, reject) => {\n const child: ChildProcess = spawn(\n 'uv',\n ['run', '--directory', serverDir, 'python', 'server.py'],\n {\n env: { ...process.env, MEMORY_PATH: memoryPath, UV_CACHE_DIR: process.env.UV_CACHE_DIR ?? '/tmp/uv-cache' },\n stdio: ['pipe', 'pipe', 'inherit'],\n },\n )\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }>()\n let nextId = 1\n let closed = false\n\n const failAll = (err: Error) => {\n for (const [, p] of pending) {\n clearTimeout(p.timer)\n p.reject(err)\n }\n pending.clear()\n }\n\n child.on('error', (err) => {\n closed = true\n failAll(new Error(`memory-vault-server spawn failed: ${err.message}`))\n reject(err)\n })\n child.on('exit', (code) => {\n if (closed) return\n closed = true\n failAll(new Error(`memory-vault-server exited unexpectedly (code ${code})`))\n reject(new Error(`memory-vault-server exited before initialize (code ${code})`))\n })\n\n const rl = readline.createInterface({ input: child.stdout!, crlfDelay: Infinity })\n rl.on('line', (line) => {\n let msg: any\n try {\n msg = JSON.parse(line)\n } catch {\n return\n }\n if (typeof msg?.id === 'number') {\n const p = pending.get(msg.id)\n if (!p) return\n pending.delete(msg.id)\n clearTimeout(p.timer)\n if (msg.error) p.reject(new Error(`MCP error: ${msg.error.message ?? JSON.stringify(msg.error)}`))\n else p.resolve(msg.result)\n }\n })\n\n const send = (method: string, params: unknown, timeoutMs: number = MCP_CALL_TIMEOUT_MS): Promise<unknown> =>\n new Promise((res, rej) => {\n if (closed || !child.stdin?.writable) {\n rej(new Error('memory-vault-server is not running'))\n return\n }\n const id = nextId++\n const timer = setTimeout(() => {\n pending.delete(id)\n rej(new Error(`MCP call ${method} timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n pending.set(id, { resolve: res, reject: rej, timer })\n child.stdin!.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\\n')\n })\n\n const notify = (method: string, params: unknown) => {\n if (!closed && child.stdin?.writable) {\n child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\\n')\n }\n }\n\n // initialize handshake (notifications are fire-and-forget: no id, no reply)\n void send('initialize', {\n protocolVersion: '2025-06-18',\n capabilities: {},\n clientInfo: { name: 'memory-auto', version: '0.1.0' },\n })\n .then(() => {\n notify('notifications/initialized', {})\n })\n .then(() => {\n if (closed) throw new Error('memory-vault-server closed during handshake')\n resolve({\n callTool: (name, args, timeoutMs = MCP_CALL_TIMEOUT_MS) =>\n send('tools/call', { name, arguments: args }, timeoutMs).then((result: any) => {\n if (result?.isError) {\n const text = Array.isArray(result.content) ? result.content.map((c: any) => c?.text ?? '').join('') : JSON.stringify(result)\n throw new Error(`tool ${name} failed: ${text}`)\n }\n return result\n }),\n close: async () => {\n if (closed) return\n closed = true\n for (const [, p] of pending) clearTimeout(p.timer)\n pending.clear()\n if (child.exitCode !== null) return\n child.kill()\n await new Promise((r) => child.once('exit', r))\n },\n })\n })\n .catch((err) => {\n closed = true\n child.kill()\n reject(err)\n })\n })\n}\n\n/**\n * Write validated OKF entries through the vault server's `store_*` tools,\n * which upsert both the Markdown file and the SQLite FTS5 index.\n */\nexport async function writeEntries(client: McpClient, project: string, entries: ValidEntry[]): Promise<{ upserted: number; failed: number }> {\n let upserted = 0\n let failed = 0\n for (const e of entries) {\n try {\n await client.callTool(`store_${e.entry_type}`, {\n project,\n content: e.content,\n ...(e.description ? { description: e.description } : {}),\n tags: e.tags,\n confidence: e.confidence,\n ...(e.openspec_change_id ? { openspec_change_id: e.openspec_change_id } : {}),\n })\n upserted += 1\n } catch (err) {\n failed += 1\n console.warn(`[memory-auto] store_${e.entry_type} failed:`, err instanceof Error ? err.message : err)\n }\n }\n return { upserted, failed }\n}\n\n/**\n * Full post-session digest: transcript -> ctx.llm extraction (with retries) ->\n * OKF writes via the vault MCP server. Never throws; logs the outcome.\n */\nexport async function digestSessionDSM(\n ctx: Context,\n config: DigestConfig,\n sessionId: string,\n directory: string,\n project: string,\n events: any[],\n signal?: AbortSignal,\n): Promise<void> {\n const header = `## context\\nproject: ${project}\\ndirectory: ${directory}\\n`\n const transcript = (header + transcriptOfDSM(events)).trim()\n if (!transcript) {\n log(`digest skip ${sessionId}: empty`)\n return\n }\n if (transcript.length < (config.minTranscriptChars ?? MIN_DIGEST_TRANSCRIPT_CHARS)) {\n log(`digest skip ${sessionId}: too short ${transcript.length}`)\n return\n }\n\n const chunks = chunkTranscript(transcript)\n const entries: ValidEntry[] = []\n for (const chunk of chunks) {\n let attempt = 0\n for (;;) {\n try {\n const got = await extractEntriesWithLlm(ctx, config, project, chunk, undefined, signal)\n entries.push(...got)\n break\n } catch (err) {\n attempt += 1\n if (attempt >= LLM_RETRIES || signal?.aborted) {\n console.warn(`[memory-auto] digest extraction failed after ${attempt} attempt(s):`, err instanceof Error ? err.message : err)\n break\n }\n const delay = LLM_RETRY_DELAYS_MS[attempt - 1] ?? 5_000\n log(`digest extraction retry ${attempt}/${LLM_RETRIES} in ${delay}ms`)\n await new Promise((r) => setTimeout(r, delay))\n }\n }\n if (signal?.aborted) break\n }\n\n if (entries.length === 0) {\n log(`digest ${sessionId}: no entries extracted`)\n return\n }\n\n let client: McpClient\n try {\n client = await connectMcp(config.memoryPath, config.serverDir)\n } catch (err) {\n console.warn(`[memory-auto] digest ${sessionId}: cannot reach vault server:`, err instanceof Error ? err.message : err)\n return\n }\n try {\n const { upserted, failed } = await writeEntries(client, project, entries)\n log(`digest ${sessionId}: ${upserted} upserted, ${failed} failed (${entries.length} extracted)`)\n } finally {\n await client.close().catch(() => {})\n }\n}\n","import { existsSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { isAbsolute, join } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\nimport {\n createSessionState,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n type SessionState,\n} from './pure.js'\nimport { digestSessionDSM, type DigestConfig } from './digest.js'\n\nexport const name = 'memory-auto'\n\nexport interface Config {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n enabled: boolean\n}\n\nexport const Config: Schema<Config> = Schema.object({\n memoryPath: Schema.string().default(process.env.DSH_MEMORY_PATH ?? ''),\n serverDir: Schema.string().default(process.env.DSH_MEMORY_SERVER_DIR ?? ''),\n provider: Schema.string().default('deepseek-official'),\n model: Schema.string().default('deepseek-v4-flash'),\n maxTokens: Schema.number().default(2048),\n minTranscriptChars: Schema.number().default(200),\n enabled: Schema.boolean().default(true),\n})\n\n/** Requires the harness LLM service: extraction runs in-process via ctx.llm. */\nexport const inject = ['llm']\n\n/**\n * Resolve the harness home the same way the harness does (`$DSH_HOME`, or\n * `~/.dsh`). Paths must never depend on the launch cwd: DSH does not chdir.\n */\nfunction dshHome(): string {\n const env = process.env.DSH_HOME?.trim()\n return env && env.length > 0 ? env : join(homedir(), '.dsh')\n}\n\n/** Absolute paths stay; empty/relative values resolve under the harness home. */\nfunction resolveUnderHome(value: string, fallbackSegment: string): string {\n const v = value.trim()\n if (v.length === 0) return join(dshHome(), fallbackSegment)\n return isAbsolute(v) ? v : join(dshHome(), v)\n}\n\n// DSH session shape minimal\ntype DSHEvt = any\ntype DSHSession = { id: string; events: DSHEvt[]; cwd?: string }\n\nexport function apply(ctx: Context, config: Config) {\n if (!config.enabled) {\n console.log('[memory-auto] disabled via config')\n return\n }\n\n const memoryPath = resolveUnderHome(config.memoryPath, 'memory-vault')\n const serverDir = resolveUnderHome(config.serverDir, 'memory-vault-server')\n const digestConfig: DigestConfig = {\n memoryPath,\n serverDir,\n provider: config.provider,\n model: config.model,\n maxTokens: config.maxTokens,\n minTranscriptChars: config.minTranscriptChars,\n }\n\n if (!existsSync(serverDir)) {\n console.warn(\n `[memory-auto] vault server not found at ${serverDir} — the digest cannot write. ` +\n 'Install memory-vault-server there, or set DSH_MEMORY_SERVER_DIR / serverDir.',\n )\n }\n if (!existsSync(memoryPath)) {\n console.warn(`[memory-auto] vault not found at ${memoryPath} — it will be created on first use.`)\n }\n\n const states = new Map<string, SessionState>()\n const activities = new Map<string, string[]>()\n const queued = new Map<string, string>()\n const sessionDirs = new Map<string, string>()\n const projectCache = new Map<string, string>()\n\n const ACTIVITY_MAX_LINES = 20\n const ACTIVITY_MAX_CHARS = 80\n\n const track = (sessionId: string, line: string) => {\n const st = states.get(sessionId)\n if (!st) return\n st.hasActivity = true\n const list = activities.get(sessionId) ?? []\n if (!list.includes(line)) {\n list.push(line)\n if (list.length > ACTIVITY_MAX_LINES) list.shift()\n activities.set(sessionId, list)\n }\n }\n\n const summary = (sid: string) => (activities.get(sid) ?? []).join('\\n')\n\n const projectFor = async (dir: string): Promise<string> => {\n const cached = projectCache.get(dir)\n if (cached) return cached\n const p = await resolveProjectName(dir || process.cwd())\n projectCache.set(dir, p)\n return p\n }\n\n // session/created -> init state\n ctx.on('session/created', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n const dir = (session as any)?.cwd ?? (session as any)?.directory ?? ''\n if (!sid) return\n const proj = await projectFor(dir)\n if (!states.has(sid)) {\n states.set(sid, createSessionState(proj))\n activities.set(sid, [])\n }\n sessionDirs.set(sid, dir)\n console.log(`[memory-auto] session created ${sid} project=${proj}`)\n })\n\n // session/disposed -> digest\n ctx.on('session/disposed', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n if (!st.hasActivity) {\n console.log(`[memory-auto] digest skip ${sid}: no activity`)\n return\n }\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const evts: DSHEvt[] = (session as any)?.events ?? []\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, evts)\n })\n\n // agent/status idle -> in-session capture gate\n ctx.on('agent/status', async (payload: any) => {\n const agent = payload?.agent\n const status = payload?.status ?? payload?.agentStatus\n if (status !== 'idle') return\n const sid: string | undefined = agent?.sessionId ?? payload?.sessionId ?? agent?.id\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n const text = idleCheckpoint(st, summary(sid))\n if (text) {\n console.log(`[memory-auto] idle checkpoint digest for ${sid}`)\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n }\n })\n\n // session/event -> git commit detect + compaction start\n ctx.on('session/event', async (session: DSHSession, event: DSHEvt) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId ?? (event as any)?.sessionId\n if (!sid) return\n const t = event?.type ?? ''\n const d = event?.data ?? {}\n\n // compaction/start -> checkpoint injection (delivered on the next pre-step)\n if (t === 'compaction/start') {\n const st = states.get(sid)\n if (!st) return\n const txt = compactingCheckpoint(st, summary(sid))\n if (txt) {\n console.log(`[memory-auto] compaction checkpoint queued for ${sid}`)\n queued.set(sid, txt)\n }\n return\n }\n\n // tool/call -> git commit detection + activity track\n if (t === 'tool/call' || t === 'tool_call') {\n const cmd = d?.args?.command ?? d?.command ?? ''\n if (typeof cmd === 'string' && isGitCommit(cmd)) {\n const st = states.get(sid)\n if (st) {\n st.hasActivity = true\n queued.set(sid, buildCommitCheckpointPrompt(st))\n console.log(`[memory-auto] git commit queued for ${sid}`)\n }\n return\n }\n if (typeof cmd === 'string') {\n track(sid, `bash: ${cmd.trim().slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n return\n }\n if (t === 'tool/result') {\n // ignore\n return\n }\n if (t === 'user/message' || t === 'assistant/message') {\n // track activity\n track(sid, `${t}: ${(d?.text ?? '').slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n })\n\n // agent/pre-step Waterfall -> deliver queued checkpoint\n ctx.on('agent/pre-step', async (payload: any, next: any) => {\n const sid: string | undefined = payload?.agent?.sessionId ?? payload?.sessionId\n if (sid) {\n const q = queued.get(sid)\n if (q) {\n queued.delete(sid)\n // Best effort: append the checkpoint as user context\n if (Array.isArray(payload?.context)) payload.context.push(q)\n else if (Array.isArray(payload?.messages)) payload.messages.push({ role: 'user', content: q })\n else console.log(`[memory-auto] deliver queued checkpoint for ${sid}`)\n }\n }\n return next()\n })\n\n // dispose -> batch digest remaining sessions\n ctx.effect(() => {\n return () => {\n console.log(`[memory-auto] dispose batch ${sessionDirs.size} sessions`)\n // fire-and-forget digest for each remaining session\n for (const [sid, dir] of sessionDirs) {\n const st = states.get(sid)\n if (!st?.hasActivity) continue\n projectFor(dir).then((proj) => {\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n void digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n })\n }\n }\n })\n\n console.log(`[memory-auto] active memoryPath=${memoryPath} serverDir=${serverDir} llm=${config.provider}/${config.model}`)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["sysParts: string[]","chunks: string[]","valid: ValidEntry[]","name","messages: Message[]","options: GenerateOptions","child: ChildProcess","msg: any","entries: ValidEntry[]","client: McpClient","Config: Schema<Config>","digestConfig: DigestConfig","sid: string | undefined"],"sources":["../src/pure.ts","../src/digest.ts","../src/plugin.ts"],"sourcesContent":["/**\n * DSH memory plugin (adapted from the OpenCode memory plugin).\n *\n * Implements session lifecycle hooks:\n * - session.created: project name resolution only (no memory auto-injection)\n * - session.idle: auto-capture gate (skip if no activity or already delivered)\n * - tool.execute.after: detect `git commit*` and queue a checkpoint\n * - tui.prompt.append: deliver queued checkpoint on next user message\n * - experimental.session.compacting: pre-compaction capture (always fires)\n * - session.end: invoke post-session digest\n * - /brain search|recall|profile: opt-in reads via MCP (2s health check)\n * - /checkpoint: manual structured review\n * - OpenCode version guard: warn on < 1.17.10, disable gracefully\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\n// ── Constants ────────────────────────────────────────────────────────────\n\nexport const MIN_OPENCODE_VERSION = \"1.17.10\";\nexport const MCP_UNREACHABLE =\n \"> ⚠️ Memory server unreachable — search cannot be completed.\";\nconst CHECKPOINT_MARKER = \"[memory-checkpoint]\";\n\n// ── Version guard ────────────────────────────────────────────────────────\n\n/** Compare two \"x.y.z\" semver strings. Returns negative/0/positive. */\nfunction compareSemver(a: string, b: string): number {\n const [a1, a2, a3] = a.split(\".\").map((n) => parseInt(n, 10) || 0);\n const [b1, b2, b3] = b.split(\".\").map((n) => parseInt(n, 10) || 0);\n if (a1 !== b1) return a1 - b1;\n if (a2 !== b2) return a2 - b2;\n return (a3 || 0) - (b3 || 0);\n}\n\nexport function isSupportedVersion(version: string): boolean {\n return compareSemver(version, MIN_OPENCODE_VERSION) >= 0;\n}\n\n// ── Project name resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the project name from a working directory.\n * Priority:\n * 1. OpenSpec presence: if `openspec/` exists, use the basename.\n * 2. package.json -> name\n * 3. pyproject.toml -> [project] -> name\n * 4. README.md: first 5 lines, heading pattern `# <ProjectName>`\n * 5. Fallback: basename of working directory\n */\nexport async function resolveProjectName(cwd: string): Promise<string> {\n if (existsSync(join(cwd, \"openspec\"))) {\n return basename(cwd);\n }\n // package.json\n const pkgPath = join(cwd, \"package.json\");\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(await readFile(pkgPath, \"utf-8\"));\n if (typeof pkg.name === \"string\" && pkg.name.trim()) {\n return pkg.name.trim();\n }\n } catch {\n // ignore parse errors; try next strategy\n }\n }\n // pyproject.toml (minimal regex parse)\n const pyprojectPath = join(cwd, \"pyproject.toml\");\n if (existsSync(pyprojectPath)) {\n try {\n const text = await readFile(pyprojectPath, \"utf-8\");\n const m = text.match(/\\[project\\][^\\[]*?name\\s*=\\s*[\"']([^\"']+)[\"']/);\n if (m) return m[1];\n } catch {\n // ignore\n }\n }\n // README.md heading\n const readmePath = join(cwd, \"README.md\");\n if (existsSync(readmePath)) {\n try {\n const text = await readFile(readmePath, \"utf-8\");\n const head = text.split(\"\\n\").slice(0, 5);\n for (const line of head) {\n const m = line.match(/^#\\s+(.+)$/);\n if (m) return m[1].trim();\n }\n } catch {\n // ignore\n }\n }\n return basename(cwd);\n}\n\n// ── Checkpoint state ─────────────────────────────────────────────────────\n\nexport interface SessionState {\n project: string;\n hasActivity: boolean;\n checkpointDelivered: boolean;\n queuedCheckpoint: string | null;\n}\n\nexport function createSessionState(project: string): SessionState {\n return {\n project,\n hasActivity: false,\n checkpointDelivered: false,\n queuedCheckpoint: null,\n };\n}\n\n/**\n * Build the checkpoint prompt body. Pure function — exported for testing.\n */\nexport function buildCheckpointPrompt(state: SessionState, activitySummary: string): string {\n return [\n `${CHECKPOINT_MARKER} End-of-session memory capture for project \\`${state.project}\\`.`,\n \"\",\n \"Tracked activity:\",\n activitySummary.trim() || \"(none recorded)\",\n \"\",\n \"Write OKF entries for any notable decisions, facts, or learnings using the `store_*` MCP tools.\",\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n/**\n * Decide whether to deliver a checkpoint on `session.idle`.\n * Returns the prompt to deliver, or null to skip.\n */\nexport function idleCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n if (state.checkpointDelivered) return null;\n state.checkpointDelivered = true;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n/**\n * Decide whether to fire on `experimental.session.compacting`.\n * Per spec: always fires when activity exists, even if checkpoint was delivered.\n */\nexport function compactingCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n// ── Git commit detection ─────────────────────────────────────────────────\n\nconst GIT_COMMIT_PATTERN = /git\\s+commit\\b/;\n\nexport function isGitCommit(command: string): boolean {\n return GIT_COMMIT_PATTERN.test(command);\n}\n\nexport function buildCommitCheckpointPrompt(state: SessionState): string {\n return [\n `${CHECKPOINT_MARKER} Memory capture after \\`git commit\\` in project \\`${state.project}\\`.`,\n \"\",\n \"Review the staged/committed changes and write OKF entries for any notable decisions, facts, or learnings.\",\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n// ── In-process session digest (ctx.llm) ──────────────────────────────────\n\n/**\n * Entry types the vault's MCP server can store via `store_*` tools. The\n * extraction prompt is restricted to these so every produced entry has a\n * write path (no `idea`/`context`/`source` — those have no store tool).\n */\nexport const EXTRACTABLE_TYPES = ['decision', 'fact', 'learning', 'convention'] as const\nexport type ExtractableType = (typeof EXTRACTABLE_TYPES)[number]\n\n/** One validated OKF entry ready to be stored through the vault server. */\nexport interface ValidEntry {\n entry_type: ExtractableType\n content: string\n description: string\n tags: string[]\n confidence: number\n openspec_change_id: string | null\n}\n\n/** Optional vault context files to embed in the extraction prompt. */\nexport interface DigestContextFiles {\n criticalFacts?: string\n claude?: string\n}\n\n/**\n * Build the system + user messages for the in-process extraction call.\n * The system part instructs the model to return a JSON array restricted to\n * EXTRACTABLE_TYPES; the user part carries the transcript.\n */\nexport function buildExtractionPrompt(\n project: string,\n transcript: string,\n contextFiles: DigestContextFiles = {},\n): { system: string; user: string } {\n const sysParts: string[] = [\n 'You are an assistant that extracts durable knowledge from a session transcript.',\n ]\n if (contextFiles.criticalFacts?.trim()) {\n sysParts.push(`Always-loaded context: CRITICAL_FACTS.md\\n${contextFiles.criticalFacts.trim()}`)\n }\n if (contextFiles.claude?.trim()) {\n sysParts.push(`Always-loaded context: _CLAUDE.md\\n${contextFiles.claude.trim()}`)\n }\n sysParts.push(\n `For the transcript of project \\`${project}\\`, identify:`,\n '- **Decisions**: architectural or design choices that were made.',\n '- **Facts**: stable, verifiable statements about the project (versions, conventions, constraints).',\n '- **Learnings**: non-obvious lessons, debugging insights, or solutions found.',\n '- **Conventions**: style rules, naming patterns, coding conventions agreed.',\n '',\n 'Return a JSON array. Each element must have exactly:',\n ' - \"entry_type\": one of \"decision\" | \"fact\" | \"learning\" | \"convention\"',\n ' - \"content\": a single-paragraph statement (no headings, no lists)',\n ' - \"description\": a one-sentence summary of `content` (queryable)',\n ' - \"tags\": an array of lowercase-kebab tags (never empty if possible, at least 1 like architecture/python/testing)',\n ' - \"confidence\": a number 0.0-1.0',\n ' - \"openspec_change_id\": (optional) the change slug if the transcript names it',\n '',\n 'Rules:',\n '- Skip trivial exchanges (greetings, \"ok\", \"thanks\", or anything with no project knowledge).',\n '- Prefer fewer, higher-signal entries over many weak ones.',\n '- Do not include anything not present in the transcript.',\n '',\n 'Return only the JSON array. No prose, no markdown fences.',\n )\n return {\n system: sysParts.join('\\n'),\n user: `Project: ${project}\\n\\nTranscript:\\n---\\n${transcript}\\n---`,\n }\n}\n\n/**\n * Split an oversized transcript into overlapping chunks (mirrors the legacy\n * digest script: 25k chars per chunk, 1k overlap, at most `cap` chunks).\n */\nexport function chunkTranscript(text: string, maxLen = 50_000, chunkSize = 25_000, overlap = 1_000, cap = 3): string[] {\n if (text.length <= maxLen) return [text]\n const chunks: string[] = []\n let i = 0\n while (i < text.length) {\n chunks.push(text.slice(i, i + chunkSize))\n i += chunkSize - overlap\n if (chunks.length >= cap) break\n }\n return chunks\n}\n\n/** Best-effort repair of common LLM JSON output; returns parsed value or null. */\nexport function repairJson(text: string): unknown {\n const s = text.trim()\n // Strip code fences: ```json ... ```\n const fenced = s.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '').trim()\n // Trailing commas: `,]` / `,}` -> `]` / `}`\n const noTrailing = fenced.replace(/,(\\s*[\\]}])/g, '$1')\n const candidates = [s, fenced, noTrailing]\n // Single-quote to double-quote conversion only when no double quotes exist.\n if (noTrailing.includes(\"'\") && !noTrailing.includes('\"')) {\n candidates.push(convertSingleQuotes(noTrailing))\n }\n for (const c of candidates) {\n try {\n return JSON.parse(c)\n } catch {\n // try the next candidate\n }\n }\n return null\n}\n\nfunction convertSingleQuotes(text: string): string {\n let out = ''\n let inString = false\n let i = 0\n while (i < text.length) {\n const ch = text[i]\n if (inString && ch === '\\\\') {\n if (i + 1 < text.length && text[i + 1] === \"'\") {\n out += \"'\"\n i += 2\n continue\n }\n out += ch\n if (i + 1 < text.length) {\n out += text[i + 1]\n i += 2\n continue\n }\n i += 1\n continue\n }\n if (ch === \"'\") {\n inString = !inString\n out += '\"'\n i += 1\n continue\n }\n out += ch\n i += 1\n }\n return out\n}\n\n/**\n * Validate a parsed extraction payload into OKF entries. Unknown types,\n * empty content and malformed values are dropped; tags and confidence are\n * normalized. Returns the valid entries (possibly empty).\n */\nexport function validateEntries(payload: unknown): ValidEntry[] {\n if (!Array.isArray(payload)) return []\n const valid: ValidEntry[] = []\n for (const item of payload) {\n if (typeof item !== 'object' || item === null) continue\n const raw = item as Record<string, unknown>\n const entryType = raw.entry_type\n if (typeof entryType !== 'string' || !(EXTRACTABLE_TYPES as readonly string[]).includes(entryType)) continue\n const content = typeof raw.content === 'string' ? raw.content.trim() : ''\n if (!content) continue\n const tags = Array.isArray(raw.tags) ? raw.tags.filter((t): t is string => typeof t === 'string' && t.length > 0) : []\n let confidence = 1\n if (typeof raw.confidence === 'number' && Number.isFinite(raw.confidence)) {\n confidence = Math.max(0, Math.min(1, raw.confidence))\n }\n const description = typeof raw.description === 'string' ? raw.description.trim() : ''\n const changeId = typeof raw.openspec_change_id === 'string' && raw.openspec_change_id ? raw.openspec_change_id : null\n valid.push({\n entry_type: entryType as ExtractableType,\n content,\n description,\n tags,\n confidence,\n openspec_change_id: changeId,\n })\n }\n return valid\n}\n\n// The full hook wiring is exposed for testing; the actual OpenCode integration\n// is done in `register.ts` (the entry point the OpenCode runtime loads via\n// package.json \"main\"). This module intentionally has no default export:\n// opencode 1.18.x only loads plugin modules with a single export.\nexport const __testing = {\n createSessionState,\n isSupportedVersion,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n};\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport readline from 'node:readline'\nimport { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm'\nimport type { FinishReason, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'\nimport type { Context } from '@deepseek-ai/cordis'\nimport {\n buildExtractionPrompt,\n chunkTranscript,\n repairJson,\n validateEntries,\n type ValidEntry,\n} from './pure.js'\n\n// Post-session digest runner: in-process LLM extraction via the harness's own\n// `ctx.llm` service (no external CLI, no credentials of its own), followed by\n// OKF writes through the vault's MCP server over stdio (`store_*` tools keep\n// the Markdown source of truth and the SQLite FTS5 index in sync).\n\nconst MIN_DIGEST_TRANSCRIPT_CHARS = 200\nconst LLM_RETRIES = 3\nconst LLM_RETRY_DELAYS_MS = [2_000, 5_000]\nconst MCP_CALL_TIMEOUT_MS = 60_000\n\nfunction log(msg: string) {\n console.log(`[memory-auto] ${msg}`)\n}\n\nexport function transcriptOfDSM(events: any[]): string {\n // DSH SessionEvent -> transcript\n return (events ?? [])\n .map((ev: any) => {\n const t = ev?.type ?? ''\n const d = ev?.data ?? ev\n if (t === 'user/message' || t === 'user_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : JSON.stringify(d).slice(0, 500)\n return `## user\\n${text}`\n }\n if (t === 'assistant/message' || t === 'assistant_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : ''\n return text ? `## assistant\\n${text}` : null\n }\n if (t === 'tool/call' || t === 'tool_call') {\n const name = d.tool ?? d.name ?? 'tool'\n const args = d.args ?? d.arguments ?? {}\n return `## tool_call ${name}\\n${JSON.stringify(args).slice(0, 1000)}`\n }\n if (t === 'tool/result' || t === 'tool_result') {\n const out = typeof d.output === 'string' ? d.output : JSON.stringify(d).slice(0, 1000)\n return `## tool_result\\n${out}`\n }\n if (t.startsWith('compaction')) return `## ${t}\\n${JSON.stringify(d).slice(0, 500)}`\n return null\n })\n .filter((x): x is string => Boolean(x))\n .join('\\n\\n')\n}\n\n/** Translate a terminal stream finish into a thrown error, mirroring dsh's own summarizers. */\nfunction finishError(finish: FinishReason): Error | undefined {\n switch (finish.kind) {\n case 'error':\n case 'aborted': {\n const error = new Error(finish.failure.message) as Error & { code?: string }\n error.code = finish.failure.code\n return error\n }\n default:\n return undefined\n }\n}\n\nexport interface DigestConfig {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n}\n\n/**\n * Run one extraction call through the harness's `ctx.llm` service: build the\n * prompt, stream, assemble, repair and validate the JSON entries.\n */\nexport async function extractEntriesWithLlm(\n ctx: Context,\n config: DigestConfig,\n project: string,\n transcript: string,\n contextFiles?: { criticalFacts?: string; claude?: string },\n signal?: AbortSignal,\n): Promise<ValidEntry[]> {\n const { system, user } = buildExtractionPrompt(project, transcript, contextFiles)\n const assembler = new BlockAssembler()\n const messages: Message[] = [\n createUserMessage({\n content: [{ type: 'text', text: user }],\n source: { kind: 'plugin', plugin: 'memory-auto' },\n }),\n ]\n const options: GenerateOptions = {\n provider: config.provider,\n model: config.model,\n messages,\n system,\n maxTokens: config.maxTokens,\n ...(signal === undefined ? {} : { signal }),\n }\n for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk)\n const error = finishError(assembler.finish)\n if (error !== undefined) throw error\n const text = assembler\n .blocks()\n .filter((b) => b.type === 'text')\n .map((b) => b.text)\n .join('')\n const parsed = repairJson(text)\n return validateEntries(parsed)\n}\n\n/** Minimal MCP stdio client for the vault server (spawned via `uv run`). */\nexport interface McpClient {\n callTool(name: string, args: Record<string, unknown>, timeoutMs?: number): Promise<unknown>\n close(): Promise<void>\n}\n\nexport function connectMcp(memoryPath: string, serverDir: string): Promise<McpClient> {\n return new Promise((resolve, reject) => {\n const child: ChildProcess = spawn(\n 'uv',\n ['run', '--directory', serverDir, 'python', 'server.py'],\n {\n env: { ...process.env, MEMORY_PATH: memoryPath, UV_CACHE_DIR: process.env.UV_CACHE_DIR ?? '/tmp/uv-cache' },\n stdio: ['pipe', 'pipe', 'inherit'],\n },\n )\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }>()\n let nextId = 1\n let closed = false\n\n const failAll = (err: Error) => {\n for (const [, p] of pending) {\n clearTimeout(p.timer)\n p.reject(err)\n }\n pending.clear()\n }\n\n child.on('error', (err) => {\n closed = true\n failAll(new Error(`memory-vault-server spawn failed: ${err.message}`))\n reject(err)\n })\n child.on('exit', (code) => {\n if (closed) return\n closed = true\n failAll(new Error(`memory-vault-server exited unexpectedly (code ${code})`))\n reject(new Error(`memory-vault-server exited before initialize (code ${code})`))\n })\n\n const rl = readline.createInterface({ input: child.stdout!, crlfDelay: Infinity })\n rl.on('line', (line) => {\n let msg: any\n try {\n msg = JSON.parse(line)\n } catch {\n return\n }\n if (typeof msg?.id === 'number') {\n const p = pending.get(msg.id)\n if (!p) return\n pending.delete(msg.id)\n clearTimeout(p.timer)\n if (msg.error) p.reject(new Error(`MCP error: ${msg.error.message ?? JSON.stringify(msg.error)}`))\n else p.resolve(msg.result)\n }\n })\n\n const send = (method: string, params: unknown, timeoutMs: number = MCP_CALL_TIMEOUT_MS): Promise<unknown> =>\n new Promise((res, rej) => {\n if (closed || !child.stdin?.writable) {\n rej(new Error('memory-vault-server is not running'))\n return\n }\n const id = nextId++\n const timer = setTimeout(() => {\n pending.delete(id)\n rej(new Error(`MCP call ${method} timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n pending.set(id, { resolve: res, reject: rej, timer })\n child.stdin!.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\\n')\n })\n\n const notify = (method: string, params: unknown) => {\n if (!closed && child.stdin?.writable) {\n child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\\n')\n }\n }\n\n // initialize handshake (notifications are fire-and-forget: no id, no reply)\n void send('initialize', {\n protocolVersion: '2025-06-18',\n capabilities: {},\n clientInfo: { name: 'memory-auto', version: '0.1.0' },\n })\n .then(() => {\n notify('notifications/initialized', {})\n })\n .then(() => {\n if (closed) throw new Error('memory-vault-server closed during handshake')\n resolve({\n callTool: (name, args, timeoutMs = MCP_CALL_TIMEOUT_MS) =>\n send('tools/call', { name, arguments: args }, timeoutMs).then((result: any) => {\n if (result?.isError) {\n const text = Array.isArray(result.content) ? result.content.map((c: any) => c?.text ?? '').join('') : JSON.stringify(result)\n throw new Error(`tool ${name} failed: ${text}`)\n }\n return result\n }),\n close: async () => {\n if (closed) return\n closed = true\n for (const [, p] of pending) clearTimeout(p.timer)\n pending.clear()\n if (child.exitCode !== null) return\n child.kill()\n await new Promise((r) => child.once('exit', r))\n },\n })\n })\n .catch((err) => {\n closed = true\n child.kill()\n reject(err)\n })\n })\n}\n\n/**\n * Write validated OKF entries through the vault server's `store_*` tools,\n * which upsert both the Markdown file and the SQLite FTS5 index.\n */\nexport async function writeEntries(client: McpClient, project: string, entries: ValidEntry[]): Promise<{ upserted: number; failed: number }> {\n let upserted = 0\n let failed = 0\n for (const e of entries) {\n try {\n await client.callTool(`store_${e.entry_type}`, {\n project,\n content: e.content,\n ...(e.description ? { description: e.description } : {}),\n tags: e.tags,\n confidence: e.confidence,\n ...(e.openspec_change_id ? { openspec_change_id: e.openspec_change_id } : {}),\n })\n upserted += 1\n } catch (err) {\n failed += 1\n console.warn(`[memory-auto] store_${e.entry_type} failed:`, err instanceof Error ? err.message : err)\n }\n }\n return { upserted, failed }\n}\n\n/**\n * Full post-session digest: transcript -> ctx.llm extraction (with retries) ->\n * OKF writes via the vault MCP server. Never throws; logs the outcome.\n */\nexport async function digestSessionDSM(\n ctx: Context,\n config: DigestConfig,\n sessionId: string,\n directory: string,\n project: string,\n events: any[],\n signal?: AbortSignal,\n): Promise<void> {\n const header = `## context\\nproject: ${project}\\ndirectory: ${directory}\\n`\n const transcript = (header + transcriptOfDSM(events)).trim()\n if (!transcript) {\n log(`digest skip ${sessionId}: empty`)\n return\n }\n if (transcript.length < (config.minTranscriptChars ?? MIN_DIGEST_TRANSCRIPT_CHARS)) {\n log(`digest skip ${sessionId}: too short ${transcript.length}`)\n return\n }\n\n const chunks = chunkTranscript(transcript)\n const entries: ValidEntry[] = []\n for (const chunk of chunks) {\n let attempt = 0\n for (;;) {\n try {\n const got = await extractEntriesWithLlm(ctx, config, project, chunk, undefined, signal)\n entries.push(...got)\n break\n } catch (err) {\n attempt += 1\n if (attempt >= LLM_RETRIES || signal?.aborted) {\n console.warn(`[memory-auto] digest extraction failed after ${attempt} attempt(s):`, err instanceof Error ? err.message : err)\n break\n }\n const delay = LLM_RETRY_DELAYS_MS[attempt - 1] ?? 5_000\n log(`digest extraction retry ${attempt}/${LLM_RETRIES} in ${delay}ms`)\n await new Promise((r) => setTimeout(r, delay))\n }\n }\n if (signal?.aborted) break\n }\n\n if (entries.length === 0) {\n log(`digest ${sessionId}: no entries extracted`)\n return\n }\n\n let client: McpClient\n try {\n client = await connectMcp(config.memoryPath, config.serverDir)\n } catch (err) {\n console.warn(`[memory-auto] digest ${sessionId}: cannot reach vault server:`, err instanceof Error ? err.message : err)\n return\n }\n try {\n const { upserted, failed } = await writeEntries(client, project, entries)\n log(`digest ${sessionId}: ${upserted} upserted, ${failed} failed (${entries.length} extracted)`)\n } finally {\n await client.close().catch(() => {})\n }\n}\n","import { cpSync, existsSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\nimport {\n createSessionState,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n type SessionState,\n} from './pure.js'\nimport { digestSessionDSM, type DigestConfig } from './digest.js'\n\nexport const name = 'memory-auto'\n\nexport interface Config {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n enabled: boolean\n}\n\nexport const Config: Schema<Config> = Schema.object({\n memoryPath: Schema.string().default(process.env.DSH_MEMORY_PATH ?? ''),\n serverDir: Schema.string().default(process.env.DSH_MEMORY_SERVER_DIR ?? ''),\n provider: Schema.string().default('deepseek-official'),\n model: Schema.string().default('deepseek-v4-flash'),\n maxTokens: Schema.number().default(2048),\n minTranscriptChars: Schema.number().default(200),\n enabled: Schema.boolean().default(true),\n})\n\n/** Requires the harness LLM service: extraction runs in-process via ctx.llm. */\nexport const inject = ['llm']\n\n/**\n * Resolve the harness home the same way the harness does (`$DSH_HOME`, or\n * `~/.dsh`). Paths must never depend on the launch cwd: DSH does not chdir.\n */\nfunction dshHome(): string {\n const env = process.env.DSH_HOME?.trim()\n return env && env.length > 0 ? env : join(homedir(), '.dsh')\n}\n\n/** Absolute paths stay; empty/relative values resolve under the harness home. */\nfunction resolveUnderHome(value: string, fallbackSegment: string): string {\n const v = value.trim()\n if (v.length === 0) return join(dshHome(), fallbackSegment)\n return isAbsolute(v) ? v : join(dshHome(), v)\n}\n\nconst packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))\n\n/** Copy the bundled dir into `target` when `key` is missing there. */\nfunction ensure(target: string, bundled: string, key: string): boolean {\n if (existsSync(join(target, key))) return false\n if (!existsSync(bundled)) return false\n mkdirSync(target, { recursive: true })\n cpSync(bundled, target, { recursive: true })\n return true\n}\n\n// DSH session shape minimal\ntype DSHEvt = any\ntype DSHSession = { id: string; events: DSHEvt[]; cwd?: string }\n\nexport function apply(ctx: Context, config: Config) {\n if (!config.enabled) {\n console.log('[memory-auto] disabled via config')\n return\n }\n\n const memoryPath = resolveUnderHome(config.memoryPath, 'memory-vault')\n const serverDir = resolveUnderHome(config.serverDir, 'memory-vault-server')\n\n // Self-contained install: first boot copies the bundled server and vault\n // starter under the harness home when they are missing.\n if (ensure(serverDir, join(packageRoot, 'server'), 'server.py')) {\n console.log(`[memory-auto] installed memory-vault-server -> ${serverDir}`)\n }\n if (ensure(memoryPath, join(packageRoot, 'vault'), 'type-registry.yaml')) {\n console.log(`[memory-auto] installed vault starter -> ${memoryPath}`)\n }\n\n const digestConfig: DigestConfig = {\n memoryPath,\n serverDir,\n provider: config.provider,\n model: config.model,\n maxTokens: config.maxTokens,\n minTranscriptChars: config.minTranscriptChars,\n }\n\n if (!existsSync(join(serverDir, 'server.py'))) {\n console.warn(\n `[memory-auto] vault server not found at ${serverDir} and not bundled — the digest cannot write. ` +\n 'Set DSH_MEMORY_SERVER_DIR (or run `node scripts/bundle-assets.mjs` in a checkout).',\n )\n }\n if (!existsSync(join(memoryPath, 'type-registry.yaml'))) {\n console.warn(`[memory-auto] vault starter not found at ${memoryPath} — searches will fail until it exists.`)\n }\n\n const states = new Map<string, SessionState>()\n const activities = new Map<string, string[]>()\n const queued = new Map<string, string>()\n const sessionDirs = new Map<string, string>()\n const projectCache = new Map<string, string>()\n\n const ACTIVITY_MAX_LINES = 20\n const ACTIVITY_MAX_CHARS = 80\n\n const track = (sessionId: string, line: string) => {\n const st = states.get(sessionId)\n if (!st) return\n st.hasActivity = true\n const list = activities.get(sessionId) ?? []\n if (!list.includes(line)) {\n list.push(line)\n if (list.length > ACTIVITY_MAX_LINES) list.shift()\n activities.set(sessionId, list)\n }\n }\n\n const summary = (sid: string) => (activities.get(sid) ?? []).join('\\n')\n\n const projectFor = async (dir: string): Promise<string> => {\n const cached = projectCache.get(dir)\n if (cached) return cached\n const p = await resolveProjectName(dir || process.cwd())\n projectCache.set(dir, p)\n return p\n }\n\n // session/created -> init state\n ctx.on('session/created', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n const dir = (session as any)?.cwd ?? (session as any)?.directory ?? ''\n if (!sid) return\n const proj = await projectFor(dir)\n if (!states.has(sid)) {\n states.set(sid, createSessionState(proj))\n activities.set(sid, [])\n }\n sessionDirs.set(sid, dir)\n console.log(`[memory-auto] session created ${sid} project=${proj}`)\n })\n\n // session/disposed -> digest\n ctx.on('session/disposed', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n if (!st.hasActivity) {\n console.log(`[memory-auto] digest skip ${sid}: no activity`)\n return\n }\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const evts: DSHEvt[] = (session as any)?.events ?? []\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, evts)\n })\n\n // agent/status idle -> in-session capture gate\n ctx.on('agent/status', async (payload: any) => {\n const agent = payload?.agent\n const status = payload?.status ?? payload?.agentStatus\n if (status !== 'idle') return\n const sid: string | undefined = agent?.sessionId ?? payload?.sessionId ?? agent?.id\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n const text = idleCheckpoint(st, summary(sid))\n if (text) {\n console.log(`[memory-auto] idle checkpoint digest for ${sid}`)\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n }\n })\n\n // session/event -> git commit detect + compaction start\n ctx.on('session/event', async (session: DSHSession, event: DSHEvt) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId ?? (event as any)?.sessionId\n if (!sid) return\n const t = event?.type ?? ''\n const d = event?.data ?? {}\n\n // compaction/start -> checkpoint injection (delivered on the next pre-step)\n if (t === 'compaction/start') {\n const st = states.get(sid)\n if (!st) return\n const txt = compactingCheckpoint(st, summary(sid))\n if (txt) {\n console.log(`[memory-auto] compaction checkpoint queued for ${sid}`)\n queued.set(sid, txt)\n }\n return\n }\n\n // tool/call -> git commit detection + activity track\n if (t === 'tool/call' || t === 'tool_call') {\n const cmd = d?.args?.command ?? d?.command ?? ''\n if (typeof cmd === 'string' && isGitCommit(cmd)) {\n const st = states.get(sid)\n if (st) {\n st.hasActivity = true\n queued.set(sid, buildCommitCheckpointPrompt(st))\n console.log(`[memory-auto] git commit queued for ${sid}`)\n }\n return\n }\n if (typeof cmd === 'string') {\n track(sid, `bash: ${cmd.trim().slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n return\n }\n if (t === 'tool/result') {\n // ignore\n return\n }\n if (t === 'user/message' || t === 'assistant/message') {\n // track activity\n track(sid, `${t}: ${(d?.text ?? '').slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n })\n\n // agent/pre-step Waterfall -> deliver queued checkpoint\n ctx.on('agent/pre-step', async (payload: any, next: any) => {\n const sid: string | undefined = payload?.agent?.sessionId ?? payload?.sessionId\n if (sid) {\n const q = queued.get(sid)\n if (q) {\n queued.delete(sid)\n // Best effort: append the checkpoint as user context\n if (Array.isArray(payload?.context)) payload.context.push(q)\n else if (Array.isArray(payload?.messages)) payload.messages.push({ role: 'user', content: q })\n else console.log(`[memory-auto] deliver queued checkpoint for ${sid}`)\n }\n }\n return next()\n })\n\n // dispose -> batch digest remaining sessions\n ctx.effect(() => {\n return () => {\n console.log(`[memory-auto] dispose batch ${sessionDirs.size} sessions`)\n // fire-and-forget digest for each remaining session\n for (const [sid, dir] of sessionDirs) {\n const st = states.get(sid)\n if (!st?.hasActivity) continue\n projectFor(dir).then((proj) => {\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n void digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n })\n }\n }\n })\n\n console.log(`[memory-auto] active memoryPath=${memoryPath} serverDir=${serverDir} llm=${config.provider}/${config.model}`)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luisarg/memory-auto",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Auto-captura memoria DSH (idle, commit, compaction, dispose)",
|
|
6
|
-
"repository": {
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Luisarg03/dsh-memory-vault.git"
|
|
9
|
+
},
|
|
7
10
|
"main": "dist/index.js",
|
|
8
11
|
"types": "dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"cordis.patch.yml",
|
|
21
|
+
"server/",
|
|
22
|
+
"vault/"
|
|
23
|
+
],
|
|
24
|
+
"dsh": {
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "./cordis.patch.yml"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsdown",
|
|
31
|
+
"dev": "tsdown --watch",
|
|
32
|
+
"test": "vitest run --passWithNoTests",
|
|
33
|
+
"prepare": "tsdown && node ../../scripts/bundle-assets.mjs",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
},
|
|
13
36
|
"dependencies": {
|
|
14
37
|
"@deepseek-ai/cordis": "4.0.1",
|
|
15
38
|
"@deepseek-ai/schemastery": "3.18.1"
|
|
@@ -17,5 +40,10 @@
|
|
|
17
40
|
"peerDependencies": {
|
|
18
41
|
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0"
|
|
19
42
|
},
|
|
20
|
-
"devDependencies": {
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.9.2",
|
|
45
|
+
"tsdown": "^0.15.6",
|
|
46
|
+
"vitest": "^3.2.7",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.7"
|
|
48
|
+
}
|
|
21
49
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Memory store: SQLite + OKF Markdown for the DSH memory vault."""
|
package/server/cli.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Memory server CLI helpers (memory path resolution + storage validation)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_memory_path() -> Path:
|
|
11
|
+
"""Return MEMORY_PATH from env, or default to `<project>/memory-vault/`."""
|
|
12
|
+
env = os.environ.get("MEMORY_PATH")
|
|
13
|
+
if env:
|
|
14
|
+
return Path(env)
|
|
15
|
+
return Path(__file__).resolve().parent.parent / "memory-vault"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_storage_path(path: str) -> None:
|
|
19
|
+
"""Exit 1 with a clear message if `path` is missing or not a directory."""
|
|
20
|
+
p = Path(path)
|
|
21
|
+
if not p.exists():
|
|
22
|
+
print(f"ERROR: Memory storage path does not exist: {path}", file=sys.stderr)
|
|
23
|
+
print(f" Run: mkdir -p {path}", file=sys.stderr)
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
if not p.is_dir():
|
|
26
|
+
print(f"ERROR: Memory storage path is not a directory: {path}", file=sys.stderr)
|
|
27
|
+
sys.exit(1)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dsh-memory-vault-server"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server (SQLite FTS5 + markdown OKF) for dsh-memory-vault"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"mcp>=1.2.0",
|
|
8
|
+
"PyYAML>=6.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
# Virtual project: uv installs dependencies and runs scripts without
|
|
12
|
+
# packaging this directory itself (no build backend needed).
|
|
13
|
+
[tool.uv]
|
|
14
|
+
package = false
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Type registry loader — reads memory/type-registry.yaml and provides
|
|
2
|
+
derived maps for store, server, and digest_session modules.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RegistryValidationError(Exception):
|
|
13
|
+
"""Raised when the type registry fails validation."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_REQUIRED_KEYS = ("name", "directory", "singular", "tool", "template", "extraction_hint")
|
|
17
|
+
_RESERVED_DIRECTORIES = {"index.md", "log.md", ".obsidian"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validate_registry(types: list[dict[str, Any]]) -> None:
|
|
21
|
+
"""Validate a parsed registry. Raises RegistryValidationError on failure."""
|
|
22
|
+
# Required keys
|
|
23
|
+
for t in types:
|
|
24
|
+
for key in _REQUIRED_KEYS:
|
|
25
|
+
if key not in t:
|
|
26
|
+
raise RegistryValidationError(
|
|
27
|
+
f"missing required key {key!r} in type {t.get('name', '<unnamed>')}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Duplicate name
|
|
31
|
+
seen_names: set[str] = set()
|
|
32
|
+
for t in types:
|
|
33
|
+
n: str = t["name"]
|
|
34
|
+
if n in seen_names:
|
|
35
|
+
raise RegistryValidationError(f"duplicate name: {n!r}")
|
|
36
|
+
seen_names.add(n)
|
|
37
|
+
|
|
38
|
+
# Duplicate directory across project-attached types
|
|
39
|
+
seen_project_dirs: set[str] = set()
|
|
40
|
+
for t in types:
|
|
41
|
+
if t.get("project_attached", True) is True:
|
|
42
|
+
d: str = t["directory"]
|
|
43
|
+
if d in seen_project_dirs:
|
|
44
|
+
raise RegistryValidationError(
|
|
45
|
+
f"duplicate directory for project-attached types: {d!r}"
|
|
46
|
+
)
|
|
47
|
+
seen_project_dirs.add(d)
|
|
48
|
+
|
|
49
|
+
# Reserved directory collision
|
|
50
|
+
for t in types:
|
|
51
|
+
d = t.get("directory", "")
|
|
52
|
+
if d in _RESERVED_DIRECTORIES:
|
|
53
|
+
raise RegistryValidationError(
|
|
54
|
+
f"directory {d!r} collides with reserved path"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Empty extraction_hint on project-attached types
|
|
58
|
+
for t in types:
|
|
59
|
+
if t.get("project_attached", True) is True:
|
|
60
|
+
hint = t.get("extraction_hint")
|
|
61
|
+
if hint is None or (isinstance(hint, str) and hint == ""):
|
|
62
|
+
raise RegistryValidationError(
|
|
63
|
+
f"empty extraction_hint on project-attached type {t.get('name')!r}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@lru_cache(maxsize=1)
|
|
68
|
+
def load_type_registry(bundle_root: Path) -> list[dict[str, Any]]:
|
|
69
|
+
"""Load and validate the type registry from bundle_root/type-registry.yaml.
|
|
70
|
+
|
|
71
|
+
Returns the list of type dicts. Raises RegistryValidationError on
|
|
72
|
+
validation failure. Uses @lru_cache so the file is read only once.
|
|
73
|
+
"""
|
|
74
|
+
import yaml # lazy import: only needed when loading the registry
|
|
75
|
+
|
|
76
|
+
registry_path = bundle_root / "type-registry.yaml"
|
|
77
|
+
if not registry_path.is_file():
|
|
78
|
+
raise RegistryValidationError(
|
|
79
|
+
f"registry file not found: {registry_path}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
raw = registry_path.read_text(encoding="utf-8")
|
|
83
|
+
try:
|
|
84
|
+
data = yaml.safe_load(raw)
|
|
85
|
+
except yaml.YAMLError as exc:
|
|
86
|
+
raise RegistryValidationError(f"YAML parse error: {exc}") from exc
|
|
87
|
+
|
|
88
|
+
if not isinstance(data, dict) or "types" not in data:
|
|
89
|
+
raise RegistryValidationError(
|
|
90
|
+
"registry must contain a top-level 'types' key"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
types = data["types"]
|
|
94
|
+
if not isinstance(types, list) or len(types) == 0:
|
|
95
|
+
raise RegistryValidationError("'types' must be a non-empty list")
|
|
96
|
+
|
|
97
|
+
_validate_registry(types)
|
|
98
|
+
return types
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_type_maps(
|
|
102
|
+
types: list[dict[str, Any]],
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
"""Derive the standard maps from a registry type list.
|
|
105
|
+
|
|
106
|
+
Returns a dict with keys:
|
|
107
|
+
valid_types: set of singular names
|
|
108
|
+
type_dir_map: singular -> directory
|
|
109
|
+
type_label_map: directory -> name (label)
|
|
110
|
+
dir_to_singular: directory -> singular
|
|
111
|
+
type_order: list of directory names in registry order (project-attached only)
|
|
112
|
+
extraction_types: list of dicts for types with non-null extraction_hint
|
|
113
|
+
"""
|
|
114
|
+
valid_types: set[str] = set()
|
|
115
|
+
type_dir_map: dict[str, str] = {}
|
|
116
|
+
type_label_map: dict[str, str] = {}
|
|
117
|
+
dir_to_singular: dict[str, str] = {}
|
|
118
|
+
type_order: list[str] = []
|
|
119
|
+
extraction_types: list[dict[str, Any]] = []
|
|
120
|
+
|
|
121
|
+
for t in types:
|
|
122
|
+
singular = t["singular"]
|
|
123
|
+
directory = t["directory"]
|
|
124
|
+
name = t["name"]
|
|
125
|
+
valid_types.add(singular)
|
|
126
|
+
type_dir_map[singular] = directory
|
|
127
|
+
type_label_map[directory] = name
|
|
128
|
+
dir_to_singular[directory] = singular
|
|
129
|
+
if t.get("project_attached", True):
|
|
130
|
+
type_order.append(directory)
|
|
131
|
+
if t.get("extraction_hint") is not None:
|
|
132
|
+
extraction_types.append(t)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"valid_types": valid_types,
|
|
136
|
+
"type_dir_map": type_dir_map,
|
|
137
|
+
"type_label_map": type_label_map,
|
|
138
|
+
"dir_to_singular": dir_to_singular,
|
|
139
|
+
"type_order": type_order,
|
|
140
|
+
"extraction_types": extraction_types,
|
|
141
|
+
}
|