r2mcp 0.2.0

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.
Files changed (138) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +21 -0
  3. package/README.md +532 -0
  4. package/dist/breadcrumbs.d.ts +123 -0
  5. package/dist/breadcrumbs.js +135 -0
  6. package/dist/cli/classify-edges.d.ts +2 -0
  7. package/dist/cli/classify-edges.js +130 -0
  8. package/dist/cli/compile-wiki.d.ts +2 -0
  9. package/dist/cli/compile-wiki.js +173 -0
  10. package/dist/cli/dump-edges-json.d.ts +2 -0
  11. package/dist/cli/dump-edges-json.js +21 -0
  12. package/dist/cli/extract-entities.d.ts +17 -0
  13. package/dist/cli/extract-entities.js +166 -0
  14. package/dist/cli/lint-memory.d.ts +16 -0
  15. package/dist/cli/lint-memory.js +94 -0
  16. package/dist/cli/migrate.d.ts +17 -0
  17. package/dist/cli/migrate.js +146 -0
  18. package/dist/cli/setup-helpers.d.ts +7 -0
  19. package/dist/cli/setup-helpers.js +72 -0
  20. package/dist/cli/setup.d.ts +15 -0
  21. package/dist/cli/setup.js +95 -0
  22. package/dist/compiler/clustering.d.ts +29 -0
  23. package/dist/compiler/clustering.js +66 -0
  24. package/dist/compiler/frontmatter.d.ts +35 -0
  25. package/dist/compiler/frontmatter.js +168 -0
  26. package/dist/compiler/manifest.d.ts +32 -0
  27. package/dist/compiler/manifest.js +82 -0
  28. package/dist/compiler/prompts.d.ts +17 -0
  29. package/dist/compiler/prompts.js +82 -0
  30. package/dist/compiler/run.d.ts +52 -0
  31. package/dist/compiler/run.js +186 -0
  32. package/dist/compiler/tier.d.ts +10 -0
  33. package/dist/compiler/tier.js +85 -0
  34. package/dist/compiler/topic.d.ts +16 -0
  35. package/dist/compiler/topic.js +105 -0
  36. package/dist/compiler/types.d.ts +101 -0
  37. package/dist/compiler/types.js +4 -0
  38. package/dist/db.d.ts +10 -0
  39. package/dist/db.js +46 -0
  40. package/dist/edges/candidate-pairs.d.ts +24 -0
  41. package/dist/edges/candidate-pairs.js +35 -0
  42. package/dist/edges/classifier.d.ts +45 -0
  43. package/dist/edges/classifier.js +172 -0
  44. package/dist/edges/signals.d.ts +13 -0
  45. package/dist/edges/signals.js +45 -0
  46. package/dist/edges/stage1-haiku.d.ts +21 -0
  47. package/dist/edges/stage1-haiku.js +33 -0
  48. package/dist/edges/stage2-opus.d.ts +41 -0
  49. package/dist/edges/stage2-opus.js +101 -0
  50. package/dist/edges/state.d.ts +44 -0
  51. package/dist/edges/state.js +79 -0
  52. package/dist/edges/types.d.ts +20 -0
  53. package/dist/edges/types.js +1 -0
  54. package/dist/embeddings.d.ts +13 -0
  55. package/dist/embeddings.js +54 -0
  56. package/dist/entities/db.d.ts +49 -0
  57. package/dist/entities/db.js +109 -0
  58. package/dist/entities/extractor.d.ts +14 -0
  59. package/dist/entities/extractor.js +154 -0
  60. package/dist/entities/normalize.d.ts +5 -0
  61. package/dist/entities/normalize.js +7 -0
  62. package/dist/entities/prompt.d.ts +19 -0
  63. package/dist/entities/prompt.js +100 -0
  64. package/dist/entities/state.d.ts +44 -0
  65. package/dist/entities/state.js +99 -0
  66. package/dist/entities/types.d.ts +62 -0
  67. package/dist/entities/types.js +6 -0
  68. package/dist/env.d.ts +13 -0
  69. package/dist/env.js +32 -0
  70. package/dist/fingerprint.d.ts +2 -0
  71. package/dist/fingerprint.js +12 -0
  72. package/dist/graph-rebuild.d.ts +6 -0
  73. package/dist/graph-rebuild.js +20 -0
  74. package/dist/index.d.ts +4 -0
  75. package/dist/index.js +403 -0
  76. package/dist/instrumentation.d.ts +10 -0
  77. package/dist/instrumentation.js +37 -0
  78. package/dist/lint/checks/contradictions.d.ts +30 -0
  79. package/dist/lint/checks/contradictions.js +52 -0
  80. package/dist/lint/checks/drift.d.ts +5 -0
  81. package/dist/lint/checks/drift.js +34 -0
  82. package/dist/lint/checks/orphans.d.ts +5 -0
  83. package/dist/lint/checks/orphans.js +25 -0
  84. package/dist/lint/checks/stale.d.ts +6 -0
  85. package/dist/lint/checks/stale.js +29 -0
  86. package/dist/lint/checks/superseded-unflagged.d.ts +5 -0
  87. package/dist/lint/checks/superseded-unflagged.js +47 -0
  88. package/dist/lint/run.d.ts +11 -0
  89. package/dist/lint/run.js +95 -0
  90. package/dist/lint/types.d.ts +60 -0
  91. package/dist/lint/types.js +13 -0
  92. package/dist/mcp-response.d.ts +7 -0
  93. package/dist/mcp-response.js +13 -0
  94. package/dist/providers/anthropic.d.ts +13 -0
  95. package/dist/providers/anthropic.js +56 -0
  96. package/dist/providers/claude-code.d.ts +35 -0
  97. package/dist/providers/claude-code.js +175 -0
  98. package/dist/providers/errors.d.ts +12 -0
  99. package/dist/providers/errors.js +19 -0
  100. package/dist/providers/index.d.ts +30 -0
  101. package/dist/providers/index.js +71 -0
  102. package/dist/providers/openrouter.d.ts +19 -0
  103. package/dist/providers/openrouter.js +76 -0
  104. package/dist/providers/semaphore.d.ts +19 -0
  105. package/dist/providers/semaphore.js +51 -0
  106. package/dist/providers/types.d.ts +27 -0
  107. package/dist/providers/types.js +7 -0
  108. package/dist/schema.sql +116 -0
  109. package/dist/server-instructions.d.ts +9 -0
  110. package/dist/server-instructions.js +20 -0
  111. package/dist/telemetry.d.ts +39 -0
  112. package/dist/telemetry.js +130 -0
  113. package/dist/tools/classify.d.ts +44 -0
  114. package/dist/tools/classify.js +121 -0
  115. package/dist/tools/compile.d.ts +31 -0
  116. package/dist/tools/compile.js +132 -0
  117. package/dist/tools/dump-edges-sidecar.d.ts +37 -0
  118. package/dist/tools/dump-edges-sidecar.js +80 -0
  119. package/dist/tools/extract-entities.d.ts +53 -0
  120. package/dist/tools/extract-entities.js +169 -0
  121. package/dist/tools/lint.d.ts +10 -0
  122. package/dist/tools/lint.js +13 -0
  123. package/dist/tools/meditate.d.ts +25 -0
  124. package/dist/tools/meditate.js +128 -0
  125. package/dist/tools/recall.d.ts +66 -0
  126. package/dist/tools/recall.js +409 -0
  127. package/dist/tools/reject.d.ts +10 -0
  128. package/dist/tools/reject.js +24 -0
  129. package/dist/tools/remember.d.ts +26 -0
  130. package/dist/tools/remember.js +140 -0
  131. package/dist/tools/search.d.ts +30 -0
  132. package/dist/tools/search.js +69 -0
  133. package/dist/tools/spawn-cli.d.ts +14 -0
  134. package/dist/tools/spawn-cli.js +41 -0
  135. package/dist/tools/stats.d.ts +31 -0
  136. package/dist/tools/stats.js +88 -0
  137. package/package.json +86 -0
  138. package/skills/remember/SKILL.md +357 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * MCP tool handler for `classify()` — wraps the SPEC-043 edge classifier.
3
+ *
4
+ * Per the SPEC-044 invariant that the MCP server itself makes no LLM
5
+ * calls, this handler spawns the standalone classify-edges driver as a
6
+ * subprocess. The actual provider calls happen in that subprocess.
7
+ *
8
+ * The subprocess command is resolved via resolveCliCommand so the tool
9
+ * works for both dev (tsx + .ts) and prod (node + dist/.js) consumers.
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ export interface ClassifyToolInput {
13
+ /** Filter candidate pairs to memories updated in the last N days. */
14
+ since_days?: number;
15
+ /** Per-run cost cap in USD. Default: $1.00 (R2MCP_EDGE_MAX_USD env). */
16
+ max_cost_usd?: number;
17
+ /** Estimate-only mode — no edges written, no LLM calls beyond Stage 1 sampling. */
18
+ dry_run?: boolean;
19
+ /** Resume a prior run_id; pairs already terminal in that run are not re-classified. */
20
+ resume_run_id?: string;
21
+ /** Force a specific provider for this run. */
22
+ provider?: 'claude-code' | 'anthropic' | 'openrouter';
23
+ }
24
+ export interface ClassifySummary {
25
+ run_id: string;
26
+ started_at: string;
27
+ ended_at: string;
28
+ candidate_pairs: number;
29
+ stage1_total: number;
30
+ stage1_pass: number;
31
+ stage1_skip: number;
32
+ stage2_total: number;
33
+ stage2_classified: number;
34
+ edges_written: number;
35
+ total_cost_usd: number;
36
+ hit_cost_cap: boolean;
37
+ error?: string;
38
+ }
39
+ export interface ClassifyToolDeps {
40
+ spawnFn?: typeof spawn;
41
+ cwd?: string;
42
+ runTimeoutMs?: number;
43
+ }
44
+ export declare function classify(input: ClassifyToolInput, deps?: ClassifyToolDeps): Promise<ClassifySummary>;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * MCP tool handler for `classify()` — wraps the SPEC-043 edge classifier.
3
+ *
4
+ * Per the SPEC-044 invariant that the MCP server itself makes no LLM
5
+ * calls, this handler spawns the standalone classify-edges driver as a
6
+ * subprocess. The actual provider calls happen in that subprocess.
7
+ *
8
+ * The subprocess command is resolved via resolveCliCommand so the tool
9
+ * works for both dev (tsx + .ts) and prod (node + dist/.js) consumers.
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ import { resolveCliCommand } from './spawn-cli.js';
13
+ export async function classify(input, deps = {}) {
14
+ const { bin, args: cliArgs } = resolveCliCommand('classify-edges');
15
+ const flags = buildArgs(input);
16
+ const cwd = deps.cwd ?? process.cwd();
17
+ const spawnFn = deps.spawnFn ?? spawn;
18
+ const timeoutMs = deps.runTimeoutMs ?? 30 * 60_000;
19
+ const stdout = await runSubprocess(spawnFn, cwd, [bin, ...cliArgs, ...flags], timeoutMs);
20
+ return parseSummary(stdout);
21
+ }
22
+ function buildArgs(input) {
23
+ const flags = [];
24
+ if (typeof input.since_days === 'number')
25
+ flags.push(`--since=${input.since_days}d`);
26
+ if (typeof input.max_cost_usd === 'number')
27
+ flags.push(`--max-cost=${input.max_cost_usd}`);
28
+ if (input.dry_run)
29
+ flags.push('--dry-run');
30
+ if (input.resume_run_id)
31
+ flags.push(`--resume=${input.resume_run_id}`);
32
+ if (input.provider)
33
+ flags.push(`--provider=${input.provider}`);
34
+ return flags;
35
+ }
36
+ function runSubprocess(spawnFn, cwd, args, timeoutMs) {
37
+ return new Promise((resolveP, rejectP) => {
38
+ const [bin, ...rest] = args;
39
+ const child = spawnFn(bin, rest, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
40
+ let stdout = '';
41
+ let stderr = '';
42
+ let settled = false;
43
+ const settle = (fn) => {
44
+ if (settled)
45
+ return;
46
+ settled = true;
47
+ fn();
48
+ };
49
+ const timer = setTimeout(() => {
50
+ try {
51
+ child.kill('SIGKILL');
52
+ }
53
+ catch {
54
+ /* ignore */
55
+ }
56
+ settle(() => rejectP(new Error(`classify-edges timed out after ${timeoutMs}ms`)));
57
+ }, timeoutMs);
58
+ child.stdout?.on('data', (d) => {
59
+ stdout += d.toString();
60
+ });
61
+ child.stderr?.on('data', (d) => {
62
+ stderr += d.toString();
63
+ });
64
+ child.on('error', (err) => {
65
+ clearTimeout(timer);
66
+ settle(() => rejectP(err));
67
+ });
68
+ child.on('exit', (code) => {
69
+ clearTimeout(timer);
70
+ if (code !== 0) {
71
+ return settle(() => rejectP(new Error(`classify-edges exited ${code}: ${stderr.slice(-500)}`)));
72
+ }
73
+ settle(() => resolveP(stdout));
74
+ });
75
+ });
76
+ }
77
+ function parseSummary(stdout) {
78
+ // The CLI prints the JSON summary as its final stdout output. Walk back
79
+ // from the last '}' matching braces to find the start. Same pattern as
80
+ // compile.ts — robust against earlier JSON-shaped output.
81
+ const trimmed = stdout.trimEnd();
82
+ const lastBrace = trimmed.lastIndexOf('}');
83
+ if (lastBrace === -1) {
84
+ throw new Error(`classify-edges produced no parseable summary; output was:\n${stdout.slice(-500)}`);
85
+ }
86
+ let depth = 0;
87
+ let start = -1;
88
+ let inString = false;
89
+ let escapeNext = false;
90
+ for (let i = lastBrace; i >= 0; i--) {
91
+ const c = trimmed[i];
92
+ if (escapeNext) {
93
+ escapeNext = false;
94
+ continue;
95
+ }
96
+ if (c === '\\' && inString) {
97
+ escapeNext = true;
98
+ continue;
99
+ }
100
+ if (c === '"' && !escapeNext) {
101
+ inString = !inString;
102
+ continue;
103
+ }
104
+ if (inString)
105
+ continue;
106
+ if (c === '}')
107
+ depth++;
108
+ else if (c === '{') {
109
+ depth--;
110
+ if (depth === 0) {
111
+ start = i;
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ if (start === -1) {
117
+ throw new Error(`classify-edges produced no parseable summary; output was:\n${stdout.slice(-500)}`);
118
+ }
119
+ const json = trimmed.slice(start, lastBrace + 1);
120
+ return JSON.parse(json);
121
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * MCP tool handler for `compile()`.
3
+ *
4
+ * Per the SPEC-044 constraint that the MCP server itself makes no LLM calls,
5
+ * this handler spawns the standalone `compile-wiki` driver as a subprocess
6
+ * and returns the JSON summary it prints to stdout. The actual provider
7
+ * calls happen in that subprocess, not in the server process.
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ import type { CompileSummary } from '../compiler/types.js';
11
+ export interface CompileToolInput {
12
+ tier?: 'preferences' | 'project-context' | 'conversations';
13
+ all?: boolean;
14
+ topic?: string;
15
+ dry_run?: boolean;
16
+ /** Override the cost cap for this invocation. */
17
+ max_cost_usd?: number;
18
+ /** Force a specific provider for this run. */
19
+ provider?: 'claude-code' | 'anthropic' | 'openrouter';
20
+ }
21
+ export interface CompileToolDeps {
22
+ /** Override the spawn function (tests inject a mock). */
23
+ spawnFn?: typeof spawn;
24
+ /** Working directory for the subprocess. Defaults to `process.cwd()`. */
25
+ cwd?: string;
26
+ /** Maximum subprocess runtime. Default 30 minutes. */
27
+ runTimeoutMs?: number;
28
+ }
29
+ export declare function compile(input: CompileToolInput, deps?: CompileToolDeps): Promise<CompileSummary>;
30
+ /** Resolved path to the compile-wiki CLI script — used in production wiring. */
31
+ export declare function compileCliPath(projectRoot: string): string;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * MCP tool handler for `compile()`.
3
+ *
4
+ * Per the SPEC-044 constraint that the MCP server itself makes no LLM calls,
5
+ * this handler spawns the standalone `compile-wiki` driver as a subprocess
6
+ * and returns the JSON summary it prints to stdout. The actual provider
7
+ * calls happen in that subprocess, not in the server process.
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ import { resolve } from 'node:path';
11
+ import { resolveCliCommand } from './spawn-cli.js';
12
+ export async function compile(input, deps = {}) {
13
+ validateInput(input);
14
+ const { bin, args: cliArgs } = resolveCliCommand('compile-wiki');
15
+ const flags = buildArgs(input);
16
+ const cwd = deps.cwd ?? process.cwd();
17
+ const spawnFn = deps.spawnFn ?? spawn;
18
+ const timeoutMs = deps.runTimeoutMs ?? 30 * 60_000;
19
+ const stdout = await runSubprocess(spawnFn, cwd, [bin, ...cliArgs, ...flags], timeoutMs);
20
+ return parseSummary(stdout);
21
+ }
22
+ function validateInput(input) {
23
+ const modes = [input.tier, input.all, input.topic].filter(Boolean).length;
24
+ if (modes !== 1) {
25
+ throw new Error('compile() requires exactly one of: tier, all, topic');
26
+ }
27
+ }
28
+ function buildArgs(input) {
29
+ const flags = [];
30
+ if (input.tier)
31
+ flags.push(`--tier=${input.tier}`);
32
+ if (input.all)
33
+ flags.push('--all');
34
+ if (input.topic)
35
+ flags.push(`--topic=${input.topic}`);
36
+ if (input.dry_run)
37
+ flags.push('--dry-run');
38
+ if (typeof input.max_cost_usd === 'number')
39
+ flags.push(`--max-cost=${input.max_cost_usd}`);
40
+ if (input.provider)
41
+ flags.push(`--provider=${input.provider}`);
42
+ return flags;
43
+ }
44
+ function runSubprocess(spawnFn, cwd, args, timeoutMs) {
45
+ return new Promise((resolveP, rejectP) => {
46
+ const [bin, ...rest] = args;
47
+ const child = spawnFn(bin, rest, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
48
+ let stdout = '';
49
+ let stderr = '';
50
+ let timedOut = false;
51
+ const timer = setTimeout(() => {
52
+ timedOut = true;
53
+ try {
54
+ child.kill('SIGKILL');
55
+ }
56
+ catch {
57
+ /* ignore */
58
+ }
59
+ }, timeoutMs);
60
+ child.stdout?.on('data', (d) => {
61
+ stdout += d.toString();
62
+ });
63
+ child.stderr?.on('data', (d) => {
64
+ stderr += d.toString();
65
+ });
66
+ child.on('error', (err) => {
67
+ clearTimeout(timer);
68
+ rejectP(err);
69
+ });
70
+ child.on('exit', (code) => {
71
+ clearTimeout(timer);
72
+ if (timedOut)
73
+ return rejectP(new Error(`compile-wiki timed out after ${timeoutMs}ms`));
74
+ if (code !== 0) {
75
+ return rejectP(new Error(`compile-wiki exited ${code}: ${stderr.slice(-500)}`));
76
+ }
77
+ resolveP(stdout);
78
+ });
79
+ });
80
+ }
81
+ function parseSummary(stdout) {
82
+ // The CLI prints the JSON summary as its final stdout output. Walk the
83
+ // string from the end backwards looking for a trailing `}`, then match
84
+ // braces to find the start of the JSON block. This is robust against the
85
+ // subprocess emitting earlier JSON-shaped output (dry-run previews,
86
+ // intermediate logging) — only the FINAL balanced object is parsed.
87
+ const trimmed = stdout.trimEnd();
88
+ const lastBrace = trimmed.lastIndexOf('}');
89
+ if (lastBrace === -1) {
90
+ throw new Error(`compile-wiki produced no parseable summary; output was:\n${stdout.slice(-500)}`);
91
+ }
92
+ // Walk back, balancing braces (ignoring those inside strings).
93
+ let depth = 0;
94
+ let start = -1;
95
+ let inString = false;
96
+ let escapeNext = false;
97
+ for (let i = lastBrace; i >= 0; i--) {
98
+ const c = trimmed[i];
99
+ if (escapeNext) {
100
+ escapeNext = false;
101
+ continue;
102
+ }
103
+ if (c === '\\' && inString) {
104
+ escapeNext = true;
105
+ continue;
106
+ }
107
+ if (c === '"' && !escapeNext) {
108
+ inString = !inString;
109
+ continue;
110
+ }
111
+ if (inString)
112
+ continue;
113
+ if (c === '}')
114
+ depth++;
115
+ else if (c === '{') {
116
+ depth--;
117
+ if (depth === 0) {
118
+ start = i;
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ if (start === -1) {
124
+ throw new Error(`compile-wiki produced no parseable summary; output was:\n${stdout.slice(-500)}`);
125
+ }
126
+ const json = trimmed.slice(start, lastBrace + 1);
127
+ return JSON.parse(json);
128
+ }
129
+ /** Resolved path to the compile-wiki CLI script — used in production wiring. */
130
+ export function compileCliPath(projectRoot) {
131
+ return resolve(projectRoot, 'scripts/compile-wiki.ts');
132
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * SPEC-045: in-process sidecar JSON exporter for memory_edges + memories.
3
+ *
4
+ * Exposed in two ways:
5
+ * 1. As an MCP tool (handler in this same file — see dumpEdgesSidecarTool).
6
+ * 2. As a CLI script (scripts/dump-edges-json.ts) — thin wrapper over dumpEdgesJson().
7
+ *
8
+ * Both paths run in-process against pgvector — no subprocess, no LLM call.
9
+ * Output: <out_dir>/edges.json and <out_dir>/memories.json. The caller
10
+ * supplies out_dir; the MCP server makes no assumption about a fixed path.
11
+ */
12
+ import pg from 'pg';
13
+ export interface DumpEdgesInput {
14
+ /** Absolute path to the directory where edges.json + memories.json land. Required. */
15
+ out_dir: string;
16
+ }
17
+ export interface DumpEdgesOutput {
18
+ memories_count: number;
19
+ edges_count: number;
20
+ out_dir: string;
21
+ }
22
+ /**
23
+ * Pure function — takes a connected pg.Client and an out dir, writes the
24
+ * two JSON files, returns counts. Connection lifecycle is the caller's job
25
+ * so tests can inject a test client.
26
+ */
27
+ export declare function dumpEdgesJsonWithClient(client: pg.Client, outDir: string): Promise<DumpEdgesOutput>;
28
+ /**
29
+ * Convenience wrapper that opens its own connection from R2MCP_DATABASE_URL.
30
+ * Used by the CLI script and the MCP tool handler.
31
+ */
32
+ export declare function dumpEdgesJson(outDir: string): Promise<DumpEdgesOutput>;
33
+ /**
34
+ * MCP tool wrapper. The server calls this with the validated input.
35
+ * Returns the structured output as a JSON-serialized text content block.
36
+ */
37
+ export declare function dumpEdgesSidecarTool(input: DumpEdgesInput): Promise<DumpEdgesOutput>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * SPEC-045: in-process sidecar JSON exporter for memory_edges + memories.
3
+ *
4
+ * Exposed in two ways:
5
+ * 1. As an MCP tool (handler in this same file — see dumpEdgesSidecarTool).
6
+ * 2. As a CLI script (scripts/dump-edges-json.ts) — thin wrapper over dumpEdgesJson().
7
+ *
8
+ * Both paths run in-process against pgvector — no subprocess, no LLM call.
9
+ * Output: <out_dir>/edges.json and <out_dir>/memories.json. The caller
10
+ * supplies out_dir; the MCP server makes no assumption about a fixed path.
11
+ */
12
+ import pg from 'pg';
13
+ import { mkdir, writeFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ /**
16
+ * Pure function — takes a connected pg.Client and an out dir, writes the
17
+ * two JSON files, returns counts. Connection lifecycle is the caller's job
18
+ * so tests can inject a test client.
19
+ */
20
+ export async function dumpEdgesJsonWithClient(client, outDir) {
21
+ const edges = await client.query(`
22
+ SELECT id, from_memory_id, to_memory_id, relation, confidence, rationale,
23
+ classifier_version, valid_from, valid_until, created_at, updated_at
24
+ FROM memory_edges
25
+ WHERE valid_until IS NULL
26
+ ORDER BY created_at DESC
27
+ `);
28
+ const memories = await client.query(`
29
+ SELECT id, content, tier, type, section, topics, people, date, fingerprint,
30
+ source_file, source_line, created_at, updated_at
31
+ FROM memories
32
+ WHERE type != 'archived'
33
+ ORDER BY created_at DESC
34
+ `);
35
+ await mkdir(outDir, { recursive: true });
36
+ const generatedAt = new Date().toISOString();
37
+ await writeFile(join(outDir, 'edges.json'), JSON.stringify({
38
+ generated_at: generatedAt,
39
+ memory_count: memories.rows.length,
40
+ edge_count: edges.rows.length,
41
+ edges: edges.rows,
42
+ }, null, 2), 'utf-8');
43
+ await writeFile(join(outDir, 'memories.json'), JSON.stringify({
44
+ generated_at: generatedAt,
45
+ memory_count: memories.rows.length,
46
+ memories: memories.rows,
47
+ }, null, 2), 'utf-8');
48
+ return {
49
+ memories_count: memories.rows.length,
50
+ edges_count: edges.rows.length,
51
+ out_dir: outDir,
52
+ };
53
+ }
54
+ /**
55
+ * Convenience wrapper that opens its own connection from R2MCP_DATABASE_URL.
56
+ * Used by the CLI script and the MCP tool handler.
57
+ */
58
+ export async function dumpEdgesJson(outDir) {
59
+ const url = process.env.R2MCP_DATABASE_URL;
60
+ if (!url)
61
+ throw new Error('R2MCP_DATABASE_URL not set');
62
+ const client = new pg.Client({ connectionString: url });
63
+ await client.connect();
64
+ try {
65
+ return await dumpEdgesJsonWithClient(client, outDir);
66
+ }
67
+ finally {
68
+ await client.end();
69
+ }
70
+ }
71
+ /**
72
+ * MCP tool wrapper. The server calls this with the validated input.
73
+ * Returns the structured output as a JSON-serialized text content block.
74
+ */
75
+ export async function dumpEdgesSidecarTool(input) {
76
+ if (!input.out_dir) {
77
+ throw new Error('dump_edges_sidecar requires out_dir');
78
+ }
79
+ return dumpEdgesJson(input.out_dir);
80
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * MCP tool handler for `extract_entities()` — SPEC-046 Task 8.
3
+ *
4
+ * Per the SPEC-044/045 invariant that the MCP server itself makes no LLM
5
+ * calls, this handler spawns the standalone extract-entities driver as a
6
+ * subprocess. The actual provider calls happen in that subprocess.
7
+ *
8
+ * The subprocess command is resolved via resolveCliCommand so the tool
9
+ * works for both dev (tsx + .ts) and prod (node + dist/.js) consumers.
10
+ *
11
+ * Pattern mirrors src/tools/classify.ts intentionally — same dependency
12
+ * injection seams (spawnFn, cwd, runTimeoutMs), same settled-flag timeout
13
+ * shape, same JSON parse strategy.
14
+ */
15
+ import { spawn } from 'node:child_process';
16
+ import type { RunSummary } from '../entities/types.js';
17
+ /**
18
+ * Build a W3C `traceparent` string from the currently active span.
19
+ * Returns undefined when there's no active span (OTel SDK not initialized
20
+ * or this call is outside withToolSpan). Exported for testability.
21
+ *
22
+ * Format: `00-<trace_id:32hex>-<span_id:16hex>-<flags:2hex>` (version 00,
23
+ * sampled flag taken from the span's traceFlags). See
24
+ * https://www.w3.org/TR/trace-context/#traceparent-header.
25
+ *
26
+ * Subprocess receivers should set this on a `propagation.extract()` carrier
27
+ * keyed by `traceparent` (lowercase) to rebuild the context — see
28
+ * scripts/extract-entities.ts startup.
29
+ */
30
+ export declare function currentTraceparent(): string | undefined;
31
+ export interface ExtractEntitiesInput {
32
+ /** Filter candidate memories to those updated in the last N days. */
33
+ since_days?: number;
34
+ /** Per-run cost cap in USD. Default: $1.00 (R2MCP_ENTITY_MAX_USD env). */
35
+ max_cost_usd?: number;
36
+ /** Force a specific provider for this run. */
37
+ provider?: 'claude-code' | 'anthropic' | 'openrouter';
38
+ /** Resume a prior run_id; memories already terminal in that run are skipped. */
39
+ resume?: string;
40
+ /** Backfill mode — process all memories regardless of recency. */
41
+ full?: boolean;
42
+ /** Top-N existing entities to include in extraction context. */
43
+ context_top_n?: number;
44
+ }
45
+ export interface ExtractEntitiesToolDeps {
46
+ /** Override the spawn function (tests inject a mock). */
47
+ spawnFn?: typeof spawn;
48
+ /** Working directory for the subprocess. Defaults to `process.cwd()`. */
49
+ cwd?: string;
50
+ /** Maximum subprocess runtime. Default 30 minutes. */
51
+ runTimeoutMs?: number;
52
+ }
53
+ export declare function extractEntitiesTool(input: ExtractEntitiesInput, deps?: ExtractEntitiesToolDeps): Promise<RunSummary>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * MCP tool handler for `extract_entities()` — SPEC-046 Task 8.
3
+ *
4
+ * Per the SPEC-044/045 invariant that the MCP server itself makes no LLM
5
+ * calls, this handler spawns the standalone extract-entities driver as a
6
+ * subprocess. The actual provider calls happen in that subprocess.
7
+ *
8
+ * The subprocess command is resolved via resolveCliCommand so the tool
9
+ * works for both dev (tsx + .ts) and prod (node + dist/.js) consumers.
10
+ *
11
+ * Pattern mirrors src/tools/classify.ts intentionally — same dependency
12
+ * injection seams (spawnFn, cwd, runTimeoutMs), same settled-flag timeout
13
+ * shape, same JSON parse strategy.
14
+ */
15
+ import { spawn } from 'node:child_process';
16
+ import { trace } from '@opentelemetry/api';
17
+ import { resolveCliCommand } from './spawn-cli.js';
18
+ /**
19
+ * Build a W3C `traceparent` string from the currently active span.
20
+ * Returns undefined when there's no active span (OTel SDK not initialized
21
+ * or this call is outside withToolSpan). Exported for testability.
22
+ *
23
+ * Format: `00-<trace_id:32hex>-<span_id:16hex>-<flags:2hex>` (version 00,
24
+ * sampled flag taken from the span's traceFlags). See
25
+ * https://www.w3.org/TR/trace-context/#traceparent-header.
26
+ *
27
+ * Subprocess receivers should set this on a `propagation.extract()` carrier
28
+ * keyed by `traceparent` (lowercase) to rebuild the context — see
29
+ * scripts/extract-entities.ts startup.
30
+ */
31
+ export function currentTraceparent() {
32
+ const span = trace.getActiveSpan();
33
+ if (!span)
34
+ return undefined;
35
+ const ctx = span.spanContext();
36
+ if (!ctx || !ctx.traceId || !ctx.spanId)
37
+ return undefined;
38
+ const flags = (ctx.traceFlags & 0xff).toString(16).padStart(2, '0');
39
+ return `00-${ctx.traceId}-${ctx.spanId}-${flags}`;
40
+ }
41
+ export async function extractEntitiesTool(input, deps = {}) {
42
+ validateInput(input);
43
+ const { bin, args: cliArgs } = resolveCliCommand('extract-entities');
44
+ const flags = buildArgs(input);
45
+ const cwd = deps.cwd ?? process.cwd();
46
+ const spawnFn = deps.spawnFn ?? spawn;
47
+ const timeoutMs = deps.runTimeoutMs ?? 30 * 60_000;
48
+ const traceparent = currentTraceparent();
49
+ const stdout = await runSubprocess(spawnFn, cwd, [bin, ...cliArgs, ...flags], timeoutMs, traceparent);
50
+ return parseSummary(stdout);
51
+ }
52
+ function validateInput(input) {
53
+ if (input.full && input.since_days !== undefined) {
54
+ throw new Error('extract_entities: --full and --since-days are mutually exclusive');
55
+ }
56
+ }
57
+ function buildArgs(input) {
58
+ const flags = [];
59
+ if (typeof input.since_days === 'number')
60
+ flags.push(`--since-days=${input.since_days}`);
61
+ if (typeof input.max_cost_usd === 'number')
62
+ flags.push(`--max-cost=${input.max_cost_usd}`);
63
+ if (input.provider)
64
+ flags.push(`--provider=${input.provider}`);
65
+ if (input.resume)
66
+ flags.push(`--resume=${input.resume}`);
67
+ if (input.full)
68
+ flags.push('--full');
69
+ if (typeof input.context_top_n === 'number')
70
+ flags.push(`--context-top-n=${input.context_top_n}`);
71
+ return flags;
72
+ }
73
+ function runSubprocess(spawnFn, cwd, args, timeoutMs, traceparent) {
74
+ return new Promise((resolveP, rejectP) => {
75
+ const [bin, ...rest] = args;
76
+ // claw-2jbo finding 6: propagate the parent OTel span context to the
77
+ // child via the standard W3C `traceparent` env var. The script entry
78
+ // point reads OTEL_TRACEPARENT at startup (see scripts/extract-entities.ts)
79
+ // and uses `propagation.extract()` to make the child's spans children of
80
+ // the parent. When traceparent is undefined (no active span / SDK off),
81
+ // we simply inherit process.env unchanged.
82
+ const childEnv = traceparent ? { ...process.env, OTEL_TRACEPARENT: traceparent } : process.env;
83
+ const child = spawnFn(bin, rest, {
84
+ cwd,
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ env: childEnv,
87
+ });
88
+ let stdout = '';
89
+ let stderr = '';
90
+ let settled = false;
91
+ const settle = (fn) => {
92
+ if (settled)
93
+ return;
94
+ settled = true;
95
+ fn();
96
+ };
97
+ const timer = setTimeout(() => {
98
+ try {
99
+ child.kill('SIGKILL');
100
+ }
101
+ catch {
102
+ /* ignore */
103
+ }
104
+ settle(() => rejectP(new Error(`extract-entities timed out after ${timeoutMs}ms`)));
105
+ }, timeoutMs);
106
+ child.stdout?.on('data', (d) => {
107
+ stdout += d.toString();
108
+ });
109
+ child.stderr?.on('data', (d) => {
110
+ stderr += d.toString();
111
+ });
112
+ child.on('error', (err) => {
113
+ clearTimeout(timer);
114
+ settle(() => rejectP(err));
115
+ });
116
+ child.on('exit', (code) => {
117
+ clearTimeout(timer);
118
+ if (code !== 0) {
119
+ return settle(() => rejectP(new Error(`extract-entities exited ${code}: ${stderr.slice(-500)}`)));
120
+ }
121
+ settle(() => resolveP(stdout));
122
+ });
123
+ });
124
+ }
125
+ function parseSummary(stdout) {
126
+ // The CLI prints the JSON summary as its final stdout output. Walk back
127
+ // from the last '}' matching braces to find the start. Same pattern as
128
+ // classify.ts / compile.ts — robust against earlier JSON-shaped output.
129
+ const trimmed = stdout.trimEnd();
130
+ const lastBrace = trimmed.lastIndexOf('}');
131
+ if (lastBrace === -1) {
132
+ throw new Error(`extract-entities produced no parseable summary; output was:\n${stdout.slice(-500)}`);
133
+ }
134
+ let depth = 0;
135
+ let start = -1;
136
+ let inString = false;
137
+ let escapeNext = false;
138
+ for (let i = lastBrace; i >= 0; i--) {
139
+ const c = trimmed[i];
140
+ if (escapeNext) {
141
+ escapeNext = false;
142
+ continue;
143
+ }
144
+ if (c === '\\' && inString) {
145
+ escapeNext = true;
146
+ continue;
147
+ }
148
+ if (c === '"' && !escapeNext) {
149
+ inString = !inString;
150
+ continue;
151
+ }
152
+ if (inString)
153
+ continue;
154
+ if (c === '}')
155
+ depth++;
156
+ else if (c === '{') {
157
+ depth--;
158
+ if (depth === 0) {
159
+ start = i;
160
+ break;
161
+ }
162
+ }
163
+ }
164
+ if (start === -1) {
165
+ throw new Error(`extract-entities produced no parseable summary; output was:\n${stdout.slice(-500)}`);
166
+ }
167
+ const json = trimmed.slice(start, lastBrace + 1);
168
+ return JSON.parse(json);
169
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * MCP tool handler for `lint()`.
3
+ *
4
+ * Lint is SQL-only — no LLM calls (C.R5) — so unlike `compile()` it runs
5
+ * directly in the MCP server process against the existing pgvector pool.
6
+ * No subprocess delegation needed.
7
+ */
8
+ import type { LintInput, LintResult } from '../lint/types.js';
9
+ export type { LintInput, LintResult } from '../lint/types.js';
10
+ export declare function lint(input: LintInput): Promise<LintResult>;