@luisarg/memory-auto 0.1.1 → 0.1.2

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.
@@ -1 +1 @@
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"}
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;EA+Cc,KAAA,EAAA,MAAK;;;;;cAvDR,QAAQ,OAAO;iBAuDZ,KAAA,MAAW,iBAAiB"}
package/dist/index.js CHANGED
@@ -286,13 +286,15 @@ async function extractEntriesWithLlm(ctx, config, project, transcript, contextFi
286
286
  }
287
287
  function connectMcp(memoryPath, serverDir) {
288
288
  return new Promise((resolve, reject) => {
289
- const child = spawn("uv", [
289
+ const launcher = join(serverDir, "launcher.mjs");
290
+ const [command, args] = existsSync(launcher) ? [process.execPath, [launcher]] : ["uv", [
290
291
  "run",
291
292
  "--directory",
292
293
  serverDir,
293
294
  "python",
294
295
  "server.py"
295
- ], {
296
+ ]];
297
+ const child = spawn(command, args, {
296
298
  env: {
297
299
  ...process.env,
298
300
  MEMORY_PATH: memoryPath,
@@ -385,9 +387,9 @@ function connectMcp(memoryPath, serverDir) {
385
387
  }).then(() => {
386
388
  if (closed) throw new Error("memory-vault-server closed during handshake");
387
389
  resolve({
388
- callTool: (name$1, args, timeoutMs = MCP_CALL_TIMEOUT_MS) => send("tools/call", {
390
+ callTool: (name$1, args$1, timeoutMs = MCP_CALL_TIMEOUT_MS) => send("tools/call", {
389
391
  name: name$1,
390
- arguments: args
392
+ arguments: args$1
391
393
  }, timeoutMs).then((result) => {
392
394
  if (result?.isError) {
393
395
  const text = Array.isArray(result.content) ? result.content.map((c) => c?.text ?? "").join("") : JSON.stringify(result);
@@ -526,6 +528,16 @@ function ensure(target, bundled, key) {
526
528
  cpSync(bundled, target, { recursive: true });
527
529
  return true;
528
530
  }
531
+ /** Copy one bundled file into `target` when missing (upgrades add files 0.1.1 → 0.1.2). */
532
+ function ensureFile(target, bundled, file) {
533
+ const dest = join(target, file);
534
+ if (existsSync(dest)) return false;
535
+ const src = join(bundled, file);
536
+ if (!existsSync(src)) return false;
537
+ mkdirSync(target, { recursive: true });
538
+ cpSync(src, dest);
539
+ return true;
540
+ }
529
541
  function apply(ctx, config) {
530
542
  if (!config.enabled) {
531
543
  console.log("[memory-auto] disabled via config");
@@ -535,6 +547,8 @@ function apply(ctx, config) {
535
547
  const serverDir = resolveUnderHome(config.serverDir, "memory-vault-server");
536
548
  if (ensure(serverDir, join(packageRoot, "server"), "server.py")) console.log(`[memory-auto] installed memory-vault-server -> ${serverDir}`);
537
549
  if (ensure(memoryPath, join(packageRoot, "vault"), "type-registry.yaml")) console.log(`[memory-auto] installed vault starter -> ${memoryPath}`);
550
+ const bundledServer = join(packageRoot, "server");
551
+ for (const file of ["launcher.mjs", "requirements.txt"]) if (ensureFile(serverDir, bundledServer, file)) console.log(`[memory-auto] installed ${file} -> ${serverDir}`);
538
552
  const digestConfig = {
539
553
  memoryPath,
540
554
  serverDir,
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 { 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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"}
1
+ {"version":3,"file":"index.js","names":["sysParts: string[]","chunks: string[]","valid: ValidEntry[]","name","messages: Message[]","options: GenerateOptions","child: ChildProcess","msg: any","args","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 { existsSync } from 'node:fs'\nimport { join } from 'node:path'\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 its launcher: uv, pip venv fallback). */\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 // Single decision point: the server bundle's launcher.mjs picks `uv run`\n // or a pip-managed .venv. Legacy hand-made server dirs without a launcher\n // keep the old direct `uv run` spawn.\n const launcher = join(serverDir, 'launcher.mjs')\n const [command, args] = existsSync(launcher)\n ? [process.execPath, [launcher]]\n : ['uv', ['run', '--directory', serverDir, 'python', 'server.py']]\n const child: ChildProcess = spawn(\n command,\n args,\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/** Copy one bundled file into `target` when missing (upgrades add files 0.1.1 → 0.1.2). */\nfunction ensureFile(target: string, bundled: string, file: string): boolean {\n const dest = join(target, file)\n if (existsSync(dest)) return false\n const src = join(bundled, file)\n if (!existsSync(src)) return false\n mkdirSync(target, { recursive: true })\n cpSync(src, dest)\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 // launcher.mjs runs the server via uv or the pip-venv fallback; upgrades of\n // existing installs (server.py already present) still need the new files.\n const bundledServer = join(packageRoot, 'server')\n for (const file of ['launcher.mjs', 'requirements.txt']) {\n if (ensureFile(serverDir, bundledServer, file)) {\n console.log(`[memory-auto] installed ${file} -> ${serverDir}`)\n }\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,6 +1,6 @@
1
1
  {
2
2
  "name": "@luisarg/memory-auto",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Auto-captura memoria DSH (idle, commit, compaction, dispose)",
6
6
  "repository": {
@@ -0,0 +1,69 @@
1
+ // memory-vault-server launcher — run `server.py` with uv when uv is on PATH,
2
+ // otherwise bootstrap a pip venv (.venv + requirements.txt) and run with that
3
+ // python. Used by both DSH launch points (memory-mcp cordis patch, memory-auto
4
+ // digest) so the fallback decision lives in exactly one place.
5
+ //
6
+ // stdout is the MCP channel of the spawned server — never print to it here.
7
+ // All progress/errors go to stderr.
8
+ //
9
+ // ponytail: fallback triggers only when the `uv` binary is absent, not when
10
+ // `uv run` fails (e.g. offline with an empty cache). If that ever matters,
11
+ // upgrade path: on uv run exit != 0, retry through the pip branch below.
12
+
13
+ import { spawn, spawnSync } from 'node:child_process'
14
+ import { existsSync } from 'node:fs'
15
+ import { dirname, join } from 'node:path'
16
+ import { fileURLToPath, pathToFileURL } from 'node:url'
17
+
18
+ const DIR = dirname(fileURLToPath(import.meta.url))
19
+
20
+ /** Pure decision: which command runs the server, given uv presence. */
21
+ export function resolveRunner(hasUv, dir = DIR, platform = process.platform) {
22
+ if (hasUv) return { command: 'uv', args: ['run', '--directory', dir, 'python', 'server.py'] }
23
+ const pyBin = join(dir, platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python')
24
+ return { command: pyBin, args: ['server.py'] }
25
+ }
26
+
27
+ export function hasUv() {
28
+ return spawnSync('uv', ['--version'], { stdio: 'ignore' }).status === 0
29
+ }
30
+
31
+ /** Ensure a runnable venv with the server's deps installed (pip branch only). */
32
+ export function ensurePipEnv(dir = DIR, platform = process.platform) {
33
+ const pyBin = join(dir, platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python')
34
+ const venvCmd = platform === 'win32' ? 'py' : 'python3'
35
+ if (!existsSync(pyBin)) {
36
+ console.error('[memory-vault-server] uv not found — creating fallback venv with ' + venvCmd)
37
+ // stdout is the MCP channel: keep install output off it.
38
+ const created = spawnSync(venvCmd, ['-m', 'venv', join(dir, '.venv')], { stdio: ['ignore', 'ignore', 'inherit'] })
39
+ if (created.status !== 0) {
40
+ console.error(`[memory-vault-server] venv creation failed (exit ${created.status}) — install uv or fix ${venvCmd}`)
41
+ process.exit(created.status ?? 1)
42
+ }
43
+ }
44
+ const depsOk = spawnSync(pyBin, ['-c', 'import mcp, yaml'], { stdio: 'ignore' }).status === 0
45
+ if (!depsOk) {
46
+ console.error('[memory-vault-server] fallback venv missing deps — pip install -r requirements.txt')
47
+ const pip = spawnSync(pyBin, ['-m', 'pip', 'install', '--quiet', '-r', join(dir, 'requirements.txt')], { stdio: ['ignore', 'ignore', 'inherit'] })
48
+ if (pip.status !== 0) {
49
+ console.error(`[memory-vault-server] pip install failed (exit ${pip.status}) — install uv, fix the network, or run the pip install manually`)
50
+ process.exit(pip.status ?? 1)
51
+ }
52
+ }
53
+ return pyBin
54
+ }
55
+
56
+ function main() {
57
+ const withUv = hasUv()
58
+ const { command, args } = resolveRunner(withUv)
59
+ if (!withUv) ensurePipEnv()
60
+ const child = spawn(command, args, { cwd: DIR, env: process.env, stdio: 'inherit' })
61
+ for (const sig of ['SIGTERM', 'SIGINT']) process.on(sig, () => child.kill(sig))
62
+ child.on('error', (err) => {
63
+ console.error(`[memory-vault-server] spawn failed (${command}): ${err.message}`)
64
+ process.exit(1)
65
+ })
66
+ child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
67
+ }
68
+
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
@@ -0,0 +1,4 @@
1
+ # Mirror of the dependencies in pyproject.toml — keep in sync by hand.
2
+ # Used only by launcher.mjs' pip fallback on hosts without uv.
3
+ mcp>=1.2.0
4
+ PyYAML>=6.0