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,123 @@
1
+ /**
2
+ * All r2mcp MCP tools that participate in the breadcrumb contract (R2).
3
+ * Adding a new tool requires adding it here AND a BreadcrumbContext variant below.
4
+ */
5
+ export type ToolName = 'remember' | 'recall' | 'search' | 'stats' | 'meditate' | 'reject' | 'compile' | 'lint' | 'classify' | 'dump_edges_sidecar' | 'extract_entities';
6
+ /**
7
+ * A single next-step recommendation. All three fields are required and must be non-empty.
8
+ */
9
+ export interface Breadcrumb {
10
+ name: string;
11
+ usage: string;
12
+ why: string;
13
+ }
14
+ /**
15
+ * Per-tool context shape — discriminated union. Each variant carries the tool name plus
16
+ * whatever args/response shape the mappers need to inspect for that tool.
17
+ *
18
+ * Tools NOT in the R4 mapping table (search, stats, meditate, reject, compile, classify,
19
+ * dump_edges_sidecar) use `NoSignalContext` — they always return next_tools: [] for Phase 5.
20
+ */
21
+ export type BreadcrumbContext = {
22
+ tool: 'recall';
23
+ response: RecallResponse;
24
+ args: RecallArgs;
25
+ } | {
26
+ tool: 'lint';
27
+ response: LintResponse;
28
+ args: LintArgs;
29
+ } | {
30
+ tool: 'remember';
31
+ response: RememberResponse;
32
+ args: RememberArgs;
33
+ } | {
34
+ tool: 'extract_entities';
35
+ response: ExtractEntitiesResponse;
36
+ args: ExtractEntitiesArgs;
37
+ } | {
38
+ tool: 'search' | 'stats' | 'meditate' | 'reject' | 'compile' | 'classify' | 'dump_edges_sidecar';
39
+ response: unknown;
40
+ args: unknown;
41
+ };
42
+ /**
43
+ * Minimal structural shape of a single recall result. Mirrors
44
+ * `RecallResult` in src/tools/recall.ts — the only fields the breadcrumb
45
+ * logic reads. The per-result `signals` field claimed by earlier drafts
46
+ * of this file does NOT exist on the real response; signals are top-level.
47
+ */
48
+ export interface RecallResultItem {
49
+ id: string;
50
+ content: string;
51
+ }
52
+ /**
53
+ * Recall response signal — flat shape from src/edges/types.ts `RecallSignal`.
54
+ * `from_id` is the memory in the recall results whose edge produced the
55
+ * signal; `to_id` is the partner on the other end.
56
+ */
57
+ export interface RecallSignal {
58
+ kind: 'contradicts' | 'superseded_by';
59
+ from_id: string;
60
+ to_id: string;
61
+ }
62
+ export interface RecallResponse {
63
+ results: RecallResultItem[];
64
+ total_results: number;
65
+ search_mode: string;
66
+ tiers_searched: string[];
67
+ query: string;
68
+ /** Top-level signals array per SPEC-044 recall response shape. */
69
+ signals?: RecallSignal[];
70
+ }
71
+ export interface RecallArgs {
72
+ query?: string;
73
+ entity?: string;
74
+ }
75
+ export interface LintFinding {
76
+ check: string;
77
+ memory_id: string;
78
+ topic?: string;
79
+ detail?: string;
80
+ }
81
+ export interface LintResponse {
82
+ findings: LintFinding[];
83
+ total_findings: number;
84
+ }
85
+ export interface LintArgs {
86
+ check?: string;
87
+ }
88
+ /**
89
+ * Mirrors `RememberResult` in src/tools/remember.ts. The remember handler
90
+ * sets `id` (no `memory_id`); the breadcrumb mapper reads `id` only.
91
+ */
92
+ export interface RememberResponse {
93
+ operation: string;
94
+ id?: string;
95
+ message?: string;
96
+ }
97
+ export interface RememberArgs {
98
+ tier?: string;
99
+ content?: string;
100
+ }
101
+ export interface ExtractedEntitySummary {
102
+ canonical_name: string;
103
+ type: string;
104
+ confidence?: number;
105
+ }
106
+ export interface ExtractEntitiesResponse {
107
+ entities_created: number;
108
+ entities_updated: number;
109
+ new_entities?: ExtractedEntitySummary[];
110
+ total_cost_usd?: number;
111
+ }
112
+ export interface ExtractEntitiesArgs {
113
+ since_days?: number;
114
+ full?: boolean;
115
+ }
116
+ export declare class InvalidBreadcrumbError extends Error {
117
+ constructor(field: string, value: unknown);
118
+ }
119
+ export declare function assertBreadcrumb(b: unknown): asserts b is Breadcrumb;
120
+ export declare const MAX_BREADCRUMBS = 3;
121
+ export declare function withBreadcrumbs<T extends object>(response: T, context: BreadcrumbContext): T & {
122
+ next_tools: Breadcrumb[];
123
+ };
@@ -0,0 +1,135 @@
1
+ // src/breadcrumbs.ts
2
+ // SPEC-047 Phase 5 — Tool-response breadcrumbs (Tao of Mac: "servers plan, models walk").
3
+ // Pure, no I/O, no DB, no LLM. All decisions are inspection of (response, context).
4
+ export class InvalidBreadcrumbError extends Error {
5
+ constructor(field, value) {
6
+ super(`Invalid breadcrumb: field "${field}" must be a non-empty string; got ${JSON.stringify(value)}`);
7
+ this.name = 'InvalidBreadcrumbError';
8
+ }
9
+ }
10
+ export function assertBreadcrumb(b) {
11
+ if (!b || typeof b !== 'object') {
12
+ throw new InvalidBreadcrumbError('breadcrumb', b);
13
+ }
14
+ const obj = b;
15
+ for (const field of ['name', 'usage', 'why']) {
16
+ const v = obj[field];
17
+ if (typeof v !== 'string' || v.length === 0) {
18
+ throw new InvalidBreadcrumbError(field, v);
19
+ }
20
+ }
21
+ }
22
+ export const MAX_BREADCRUMBS = 3;
23
+ // Per-tool mappers.
24
+ function mapRecall(ctx) {
25
+ // Production shape (SPEC-044): RecallResponse.signals is a TOP-LEVEL flat
26
+ // array of {kind, from_id, to_id, ...}. The earlier nested per-result
27
+ // `r.signals.contradictions[]` shape claimed by this mapper never existed
28
+ // on the real response — see claw-sup7.
29
+ const seen = new Set();
30
+ const breadcrumbs = [];
31
+ for (const s of ctx.response.signals ?? []) {
32
+ if (s.kind !== 'contradicts')
33
+ continue;
34
+ if (seen.has(s.from_id))
35
+ continue;
36
+ seen.add(s.from_id);
37
+ breadcrumbs.push({
38
+ name: 'lint',
39
+ usage: `lint --check=contradictions --memory-id=${s.from_id}`,
40
+ why: 'recall flagged a contradiction on this memory; lint diagnoses it',
41
+ });
42
+ }
43
+ return breadcrumbs;
44
+ }
45
+ function mapLint(ctx) {
46
+ // R6 truncation ranking: by descending contradiction count per topic; alphabetical for ties.
47
+ const counts = new Map();
48
+ for (const f of ctx.response.findings) {
49
+ if (f.check !== 'contradictions')
50
+ continue;
51
+ if (!f.topic)
52
+ continue;
53
+ counts.set(f.topic, (counts.get(f.topic) ?? 0) + 1);
54
+ }
55
+ const ordered = Array.from(counts.entries()).sort((a, b) => {
56
+ if (b[1] !== a[1])
57
+ return b[1] - a[1]; // descending count
58
+ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; // alpha tiebreak
59
+ });
60
+ return ordered.map(([topic]) => ({
61
+ name: 'compile',
62
+ usage: `compile --topic=${topic}`,
63
+ why: 'Contradictions on this topic; recompile the wiki view',
64
+ }));
65
+ }
66
+ const QUERY_SNIPPET_MAX = 80;
67
+ function mapRemember(ctx) {
68
+ // Production shape: RememberResult exposes `id` only (no `memory_id`).
69
+ // The breadcrumb fires for ADD/UPDATE/REJECTION (which set id) and skips
70
+ // NOOP / unresolved ARCHIVE (which leave id undefined). claw-sup7.
71
+ if (!ctx.response.id)
72
+ return [];
73
+ const content = ctx.args.content ?? '';
74
+ if (!content.trim())
75
+ return [];
76
+ const snippet = content.slice(0, QUERY_SNIPPET_MAX).replace(/\s+/g, ' ').trim();
77
+ const tier = ctx.args.tier;
78
+ const usage = tier
79
+ ? `recall --query=${JSON.stringify(snippet)} --tier=${tier}`
80
+ : `recall --query=${JSON.stringify(snippet)}`;
81
+ return [
82
+ {
83
+ name: 'recall',
84
+ usage,
85
+ why: 'Verify the memory was indexed correctly',
86
+ },
87
+ ];
88
+ }
89
+ function mapExtractEntities(ctx) {
90
+ // R4 trigger: entities_created > 0
91
+ if ((ctx.response.entities_created ?? 0) <= 0)
92
+ return [];
93
+ const entities = ctx.response.new_entities ?? [];
94
+ if (entities.length === 0)
95
+ return [];
96
+ // Open Question 3 resolution: pick by highest confidence; alphabetical tiebreak.
97
+ const ranked = [...entities].sort((a, b) => {
98
+ const ca = a.confidence ?? 0;
99
+ const cb = b.confidence ?? 0;
100
+ if (cb !== ca)
101
+ return cb - ca;
102
+ return a.canonical_name < b.canonical_name ? -1 : a.canonical_name > b.canonical_name ? 1 : 0;
103
+ });
104
+ const top = ranked[0];
105
+ return [
106
+ {
107
+ name: 'recall',
108
+ usage: `recall --entity=${top.canonical_name}`,
109
+ why: 'Confirm the newly extracted entity links to expected memories',
110
+ },
111
+ ];
112
+ }
113
+ function dispatchMapper(ctx) {
114
+ switch (ctx.tool) {
115
+ case 'recall':
116
+ return mapRecall(ctx);
117
+ case 'lint':
118
+ return mapLint(ctx);
119
+ case 'remember':
120
+ return mapRemember(ctx);
121
+ case 'extract_entities':
122
+ return mapExtractEntities(ctx);
123
+ // No-signal tools always return empty for Phase 5 — see R4.
124
+ default:
125
+ return [];
126
+ }
127
+ }
128
+ export function withBreadcrumbs(response, context) {
129
+ const candidates = dispatchMapper(context);
130
+ // Validate each candidate before truncation — bad breadcrumbs throw at construction.
131
+ for (const b of candidates)
132
+ assertBreadcrumb(b);
133
+ const next_tools = candidates.slice(0, MAX_BREADCRUMBS);
134
+ return { ...response, next_tools };
135
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env tsx
2
+ import '../instrumentation.js';
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env tsx
2
+ // OTel instrumentation MUST be imported first — before any other module —
3
+ // so OTEL_TRACEPARENT propagation has an SDK to attach the parent context to
4
+ // (claw-1ejd). Without this import, propagation.extract() is a no-op.
5
+ import '../instrumentation.js';
6
+ /**
7
+ * SPEC-043 / SPEC-044 edge classifier CLI.
8
+ *
9
+ * Usage:
10
+ * npm run edges:classify -- [--since=DURATION] [--max-cost=USD] [--dry-run] [--resume=<run_id>] [--provider=<name>]
11
+ *
12
+ * Provider selection precedence (D.R3):
13
+ * 1. --provider=claude-code|anthropic|openrouter
14
+ * 2. R2MCP_CLASSIFIER_PROVIDER env var
15
+ * 3. Auto-fallback (claude-code → anthropic → openrouter)
16
+ *
17
+ * Examples:
18
+ * npm run edges:classify -- --dry-run # estimate only
19
+ * npm run edges:classify -- --max-cost=5.00 # full run, $5 cap
20
+ * npm run edges:classify -- --since=7d --max-cost=1.00 # incremental
21
+ * npm run edges:classify -- --resume=abc123 # continue prior run
22
+ * npm run edges:classify -- --provider=claude-code # force Max-covered
23
+ *
24
+ * Exit codes:
25
+ * 0 — completed normally OR hit cap gracefully OR dry-run completed
26
+ * 1 — fatal error (DB unreachable, no provider configured, etc.)
27
+ */
28
+ import { randomUUID } from 'node:crypto';
29
+ import { resolve } from 'node:path';
30
+ import { initDb, getPool, closeDb } from '../db.js';
31
+ import { findCandidatePairs } from '../edges/candidate-pairs.js';
32
+ import { stage1HaikuFilter } from '../edges/stage1-haiku.js';
33
+ import { stage2OpusClassify } from '../edges/stage2-opus.js';
34
+ import { StateStore, RunSummaryWriter } from '../edges/state.js';
35
+ import { runClassifier } from '../edges/classifier.js';
36
+ import { withToolSpan } from '../telemetry.js';
37
+ import { selectProvider, isProviderName, ProviderUnavailableError, } from '../providers/index.js';
38
+ import { loadEnvFile } from '../env.js';
39
+ // Load .env from project root — launchd-spawned subprocesses don't inherit
40
+ // shell env, so OTEL_ENABLED + DB URL + provider keys must be loaded here
41
+ // (claw-1ejd; mirrors src/index.ts).
42
+ loadEnvFile(resolve(process.env.PROJECT_ROOT || process.cwd(), '.env'));
43
+ const CLASSIFIER_VERSION = 'edges-v1-2026-05-03';
44
+ function parseArgs(argv) {
45
+ const out = {
46
+ maxCostUsd: Number(process.env.R2MCP_EDGE_MAX_USD ?? '1.00'),
47
+ dryRun: false,
48
+ };
49
+ for (const arg of argv) {
50
+ if (arg === '--dry-run')
51
+ out.dryRun = true;
52
+ else if (arg.startsWith('--max-cost='))
53
+ out.maxCostUsd = Number(arg.split('=')[1]);
54
+ else if (arg.startsWith('--resume='))
55
+ out.resumeRunId = arg.split('=')[1];
56
+ else if (arg.startsWith('--since=')) {
57
+ const raw = arg.split('=')[1];
58
+ const m = raw.match(/^(\d+)d$/);
59
+ if (!m)
60
+ throw new Error(`--since must be Nd (e.g. 7d), got ${raw}`);
61
+ out.sinceDays = Number(m[1]);
62
+ }
63
+ else if (arg.startsWith('--provider=')) {
64
+ const raw = arg.split('=')[1];
65
+ if (!isProviderName(raw)) {
66
+ throw new Error(`--provider must be one of claude-code|anthropic|openrouter, got ${raw}`);
67
+ }
68
+ out.providerFlag = raw;
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ async function main() {
74
+ const args = parseArgs(process.argv.slice(2));
75
+ const runId = args.resumeRunId ?? randomUUID();
76
+ const dataDir = resolve(process.env.R2MCP_EDGE_DATA_DIR ?? 'data');
77
+ const state = new StateStore(resolve(dataDir, 'edges-state.jsonl'), resolve(dataDir, 'edges-state.last-run'));
78
+ const summaryWriter = new RunSummaryWriter(resolve(dataDir, 'edges-state.runs'));
79
+ await initDb();
80
+ const pool = getPool();
81
+ // Lazy-create the provider only for non-dry-run paths. Dry-run is read-only
82
+ // and never calls into a provider, so don't fail if none is configured.
83
+ const provider = args.dryRun ? null : await selectProvider({ flag: args.providerFlag });
84
+ const summary = await withToolSpan('classify_edges', {
85
+ run_id: runId,
86
+ dry_run: args.dryRun,
87
+ max_cost_usd: args.maxCostUsd,
88
+ provider: provider?.name ?? 'dry-run',
89
+ }, () => runClassifier({ runId, maxCostUsd: args.maxCostUsd, dryRun: args.dryRun, sinceDays: args.sinceDays }, {
90
+ classifierVersion: CLASSIFIER_VERSION,
91
+ state,
92
+ summaryWriter,
93
+ concurrencyLimit: provider?.concurrencyLimit ?? 1,
94
+ providerName: provider?.name,
95
+ findCandidatePairs: (opts) => findCandidatePairs(pool, opts),
96
+ fetchMemoryById: async (id) => {
97
+ const r = await pool.query('SELECT id, content, type FROM memories WHERE id = $1', [id]);
98
+ return r.rows[0] ?? null;
99
+ },
100
+ stage1Filter: (pair) => stage1HaikuFilter(provider, pair),
101
+ stage2Classify: (pair) => stage2OpusClassify(provider, pair),
102
+ insertEdge: async (fromId, toId, relation, confidence, rationale, version) => {
103
+ const res = await pool.query(`INSERT INTO memory_edges (from_memory_id, to_memory_id, relation, confidence, rationale, classifier_version)
104
+ VALUES ($1, $2, $3, $4, $5, $6)
105
+ ON CONFLICT (from_memory_id, to_memory_id, relation) DO UPDATE
106
+ SET confidence = EXCLUDED.confidence,
107
+ rationale = EXCLUDED.rationale,
108
+ updated_at = NOW()
109
+ RETURNING id`, [fromId, toId, relation, confidence, rationale, version]);
110
+ return res.rows[0].id;
111
+ },
112
+ estimateCost: async (pairs) => {
113
+ const stage1 = pairs.length * 0.0005;
114
+ const stage2 = pairs.length * 0.2 * 0.018;
115
+ return stage1 + stage2;
116
+ },
117
+ }));
118
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
119
+ await closeDb();
120
+ process.exit(0);
121
+ }
122
+ main().catch((err) => {
123
+ if (err instanceof ProviderUnavailableError) {
124
+ process.stderr.write(`${err.message}\n`);
125
+ }
126
+ else {
127
+ process.stderr.write(`ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
128
+ }
129
+ process.exit(1);
130
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env tsx
2
+ import '../instrumentation.js';
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env tsx
2
+ // OTel instrumentation MUST be imported first — before any other module —
3
+ // so OTEL_TRACEPARENT propagation has an SDK to attach the parent context to
4
+ // (claw-1ejd). Without this import, propagation.extract() is a no-op.
5
+ import '../instrumentation.js';
6
+ /**
7
+ * SPEC-044 Section B — wiki compile CLI driver.
8
+ *
9
+ * Usage:
10
+ * npm run compile-wiki -- [--tier=<name> | --all | --topic=<name>] [--dry-run] [--max-cost=USD] [--provider=<name>]
11
+ *
12
+ * Provider selection: same as classify-edges (claude-code → anthropic →
13
+ * openrouter auto-fallback, --provider= overrides).
14
+ *
15
+ * Output goes to `<projectRoot>/memory/compiled/`. The host project gitignores
16
+ * that path because it is regenerable.
17
+ *
18
+ * Exit codes:
19
+ * 0 — completed normally OR hit cap gracefully
20
+ * 1 — fatal error (DB, no provider, validation failure, etc.)
21
+ */
22
+ import { randomUUID } from 'node:crypto';
23
+ import { execSync } from 'node:child_process';
24
+ import { resolve } from 'node:path';
25
+ import { initDb, getPool, closeDb } from '../db.js';
26
+ import { selectProvider, isProviderName, ProviderUnavailableError, } from '../providers/index.js';
27
+ import { runCompile } from '../compiler/run.js';
28
+ import { withToolSpan } from '../telemetry.js';
29
+ import { loadEnvFile } from '../env.js';
30
+ const VALID_TIERS = ['preferences', 'project-context', 'conversations'];
31
+ function isTier(s) {
32
+ return VALID_TIERS.includes(s);
33
+ }
34
+ function parseArgs(argv) {
35
+ const out = {
36
+ all: false,
37
+ dryRun: false,
38
+ maxCostUsd: Number(process.env.R2MCP_COMPILE_MAX_USD ?? '1.00'),
39
+ };
40
+ for (const arg of argv) {
41
+ if (arg === '--dry-run')
42
+ out.dryRun = true;
43
+ else if (arg === '--all')
44
+ out.all = true;
45
+ else if (arg.startsWith('--tier=')) {
46
+ const t = arg.split('=')[1];
47
+ if (!isTier(t))
48
+ throw new Error(`--tier must be one of ${VALID_TIERS.join('|')}, got ${t}`);
49
+ out.tier = t;
50
+ }
51
+ else if (arg.startsWith('--topic=')) {
52
+ out.topic = arg.split('=')[1];
53
+ }
54
+ else if (arg.startsWith('--max-cost=')) {
55
+ out.maxCostUsd = Number(arg.split('=')[1]);
56
+ }
57
+ else if (arg.startsWith('--provider=')) {
58
+ const raw = arg.split('=')[1];
59
+ if (!isProviderName(raw)) {
60
+ throw new Error(`--provider must be claude-code|anthropic|openrouter, got ${raw}`);
61
+ }
62
+ out.providerFlag = raw;
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ async function loadMemoriesFromDb(scope) {
68
+ const pool = getPool();
69
+ // Load memories — filter at the SQL layer when scope is narrow.
70
+ let where = `type != 'archived'`;
71
+ const params = [];
72
+ if (scope.tier) {
73
+ params.push(scope.tier);
74
+ where += ` AND tier = $${params.length}`;
75
+ }
76
+ if (scope.topic) {
77
+ params.push(scope.topic);
78
+ where += ` AND $${params.length} = ANY(topics)`;
79
+ }
80
+ const rows = await pool.query(`SELECT id, tier, type, content, topics, people, created_at::text AS created_at
81
+ FROM memories WHERE ${where}
82
+ ORDER BY id`, params);
83
+ const memories = rows.rows.map((r) => ({
84
+ id: r.id,
85
+ tier: r.tier,
86
+ type: r.type,
87
+ content: r.content,
88
+ topics: r.topics ?? [],
89
+ people: r.people ?? [],
90
+ created_at: r.created_at,
91
+ }));
92
+ // Attach inbound edges for prose framing (B.AC7). Only fetch edges that
93
+ // touch memories we already have — keeps the query bounded.
94
+ const ids = memories.map((m) => m.id);
95
+ if (ids.length > 0) {
96
+ const edgeRows = await pool.query(`SELECT from_memory_id, to_memory_id, relation, rationale, confidence::float
97
+ FROM memory_edges
98
+ WHERE from_memory_id = ANY($1) OR to_memory_id = ANY($1)`, [ids]);
99
+ const byId = new Map(memories.map((m) => [m.id, m]));
100
+ for (const e of edgeRows.rows) {
101
+ const m = byId.get(e.from_memory_id) ?? byId.get(e.to_memory_id);
102
+ if (!m)
103
+ continue;
104
+ m.edges = m.edges ?? [];
105
+ m.edges.push({
106
+ from_memory_id: e.from_memory_id,
107
+ to_memory_id: e.to_memory_id,
108
+ relation: e.relation,
109
+ rationale: e.rationale,
110
+ confidence: e.confidence,
111
+ });
112
+ }
113
+ }
114
+ return memories;
115
+ }
116
+ function gitSha(cwd) {
117
+ try {
118
+ const out = execSync('git rev-parse HEAD', { cwd, stdio: ['ignore', 'pipe', 'ignore'] });
119
+ return out.toString().trim() || null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ // Load .env from project root — launchd-spawned subprocesses don't inherit
126
+ // shell env, so OTEL_ENABLED + DB URL + provider keys must be loaded here
127
+ // (claw-1ejd; mirrors src/index.ts).
128
+ loadEnvFile(resolve(process.env.PROJECT_ROOT || process.cwd(), '.env'));
129
+ async function main() {
130
+ const args = parseArgs(process.argv.slice(2));
131
+ if (!args.tier && !args.all && !args.topic) {
132
+ throw new Error('Specify exactly one of --tier=<name> | --all | --topic=<name>');
133
+ }
134
+ const projectRoot = process.env.PROJECT_ROOT || process.cwd();
135
+ const compiledDir = resolve(projectRoot, 'memory/compiled');
136
+ const runId = randomUUID();
137
+ const startedAt = new Date().toISOString();
138
+ const sourceGitSha = gitSha(projectRoot);
139
+ await initDb();
140
+ const provider = await selectProvider({ flag: args.providerFlag });
141
+ const summary = await withToolSpan('compile_wiki', {
142
+ run_id: runId,
143
+ dry_run: args.dryRun,
144
+ max_cost_usd: args.maxCostUsd,
145
+ provider: provider.name,
146
+ scope: args.tier ?? args.topic ?? 'all',
147
+ }, () => runCompile({
148
+ tier: args.tier,
149
+ all: args.all,
150
+ topic: args.topic,
151
+ dryRun: args.dryRun,
152
+ maxCostUsd: args.maxCostUsd,
153
+ runId,
154
+ startedAt,
155
+ sourceGitSha,
156
+ compiledDir,
157
+ }, {
158
+ provider,
159
+ loadMemories: () => loadMemoriesFromDb(args),
160
+ }));
161
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
162
+ await closeDb();
163
+ process.exit(0);
164
+ }
165
+ main().catch((err) => {
166
+ if (err instanceof ProviderUnavailableError) {
167
+ process.stderr.write(`${err.message}\n`);
168
+ }
169
+ else {
170
+ process.stderr.write(`ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
171
+ }
172
+ process.exit(1);
173
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env tsx
2
+ export {};
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Phase 0b sidecar CLI — dump memory_edges + memories to JSON.
4
+ *
5
+ * Production callers should use the MCP tool `dump_edges_sidecar` instead.
6
+ * This script remains as the dev workflow entry point:
7
+ * tsx scripts/dump-edges-json.ts --out-dir=memory/compiled
8
+ */
9
+ import { resolve } from 'node:path';
10
+ import { dumpEdgesJson } from '../tools/dump-edges-sidecar.js';
11
+ const DEFAULT_DIR = 'memory/compiled';
12
+ async function main() {
13
+ const dirArg = process.argv.find((a) => a.startsWith('--out-dir='));
14
+ const outDir = resolve(dirArg ? dirArg.split('=')[1] : DEFAULT_DIR);
15
+ const result = await dumpEdgesJson(outDir);
16
+ console.log(`Wrote ${result.edges_count} edges + ${result.memories_count} memories to ${result.out_dir}/`);
17
+ }
18
+ main().catch((e) => {
19
+ console.error(e);
20
+ process.exit(1);
21
+ });
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env tsx
2
+ import '../instrumentation.js';
3
+ import { type ProviderName } from '../providers/index.js';
4
+ export interface CliArgs {
5
+ sinceDays?: number;
6
+ maxCostUsd: number;
7
+ providerFlag?: ProviderName;
8
+ resume?: string;
9
+ full: boolean;
10
+ dataDir: string;
11
+ contextTopN: number;
12
+ }
13
+ export declare class UsageError extends Error {
14
+ readonly exitCode: number;
15
+ constructor(message: string, exitCode?: number);
16
+ }
17
+ export declare function parseArgs(argv: string[]): CliArgs;