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,166 @@
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-046 entity extractor CLI.
8
+ *
9
+ * Usage:
10
+ * npm run entities:extract -- [--since-days=N | --full] [--max-cost=USD]
11
+ * [--provider=<name>] [--resume=<run_id>]
12
+ * [--data-dir=PATH] [--context-top-n=N]
13
+ *
14
+ * Provider selection precedence (mirrors SPEC-043):
15
+ * 1. --provider=claude-code|anthropic|openrouter
16
+ * 2. R2MCP_CLASSIFIER_PROVIDER env var
17
+ * 3. Auto-fallback (claude-code → anthropic → openrouter)
18
+ *
19
+ * Examples:
20
+ * npm run entities:extract -- --since-days=1 --max-cost=0.05
21
+ * npm run entities:extract -- --full --max-cost=1.00
22
+ * npm run entities:extract -- --resume=abc123
23
+ *
24
+ * Exit codes:
25
+ * 0 — completed normally (including cap-reached and provider errors that
26
+ * finalize via run summary)
27
+ * 1 — fatal error (DB unreachable, no provider configured, parse error in args, etc.)
28
+ * 2 — invalid CLI argument combination
29
+ */
30
+ import { resolve } from 'node:path';
31
+ import { context, propagation } from '@opentelemetry/api';
32
+ import { initDb, getPool, closeDb } from '../db.js';
33
+ import { runExtractor } from '../entities/extractor.js';
34
+ import { selectProvider, isProviderName, ProviderUnavailableError, } from '../providers/index.js';
35
+ import { loadEnvFile } from '../env.js';
36
+ // Load .env from project root — launchd-spawned subprocesses don't inherit
37
+ // shell env, so OTEL_ENABLED + DB URL + provider keys must be loaded here
38
+ // (claw-1ejd; mirrors src/index.ts).
39
+ loadEnvFile(resolve(process.env.PROJECT_ROOT || process.cwd(), '.env'));
40
+ /**
41
+ * claw-2jbo finding 6: if the MCP wrapper passed an OTEL_TRACEPARENT env
42
+ * var (W3C traceparent format), reconstruct the parent OTel context so any
43
+ * spans/auto-instrumentation in this subprocess chain under the wrapper's
44
+ * `memory.extract_entities` span. No-op when the env var is absent or no
45
+ * propagator is registered (OTel SDK not initialized).
46
+ */
47
+ function parentContextFromEnv() {
48
+ const traceparent = process.env.OTEL_TRACEPARENT;
49
+ if (!traceparent)
50
+ return undefined;
51
+ // Standard W3C TextMap carrier: lowercase `traceparent` (and optional
52
+ // `tracestate`) keys. propagation.extract is a no-op without a propagator,
53
+ // which is fine — the env var is still set, future SDK init will see it.
54
+ const carrier = { traceparent };
55
+ if (process.env.OTEL_TRACESTATE)
56
+ carrier.tracestate = process.env.OTEL_TRACESTATE;
57
+ return propagation.extract(context.active(), carrier);
58
+ }
59
+ // SPEC-046 R6: argv-parse errors must produce a non-zero exit with a clear
60
+ // message naming the offending flag(s). UsageError carries the exit code so
61
+ // main() can preserve the historical exit-2 contract while parseArgs stays
62
+ // pure & unit-testable.
63
+ export class UsageError extends Error {
64
+ exitCode;
65
+ constructor(message, exitCode = 2) {
66
+ super(message);
67
+ this.name = 'UsageError';
68
+ this.exitCode = exitCode;
69
+ }
70
+ }
71
+ export function parseArgs(argv) {
72
+ const args = {
73
+ maxCostUsd: Number(process.env.R2MCP_ENTITY_MAX_USD ?? '1.00'),
74
+ dataDir: process.env.R2MCP_ENTITY_DATA_DIR ?? 'data',
75
+ contextTopN: Number(process.env.R2MCP_ENTITY_CONTEXT_TOP_N ?? '100'),
76
+ full: false,
77
+ };
78
+ for (const a of argv) {
79
+ if (a === '--full') {
80
+ args.full = true;
81
+ }
82
+ else if (a.startsWith('--since-days=')) {
83
+ args.sinceDays = Number(a.split('=')[1]);
84
+ }
85
+ else if (a.startsWith('--max-cost=')) {
86
+ args.maxCostUsd = Number(a.split('=')[1]);
87
+ }
88
+ else if (a.startsWith('--provider=')) {
89
+ const raw = a.split('=')[1];
90
+ if (!isProviderName(raw)) {
91
+ throw new UsageError(`--provider must be one of claude-code|anthropic|openrouter, got ${raw}`);
92
+ }
93
+ args.providerFlag = raw;
94
+ }
95
+ else if (a.startsWith('--resume=')) {
96
+ args.resume = a.split('=')[1];
97
+ }
98
+ else if (a.startsWith('--data-dir=')) {
99
+ args.dataDir = a.split('=')[1];
100
+ }
101
+ else if (a.startsWith('--context-top-n=')) {
102
+ args.contextTopN = Number(a.split('=')[1]);
103
+ }
104
+ }
105
+ if (args.full && args.sinceDays !== undefined) {
106
+ throw new UsageError('Error: --full and --since-days are mutually exclusive');
107
+ }
108
+ return args;
109
+ }
110
+ async function main() {
111
+ const args = parseArgs(process.argv.slice(2));
112
+ await initDb();
113
+ const pool = getPool();
114
+ const provider = await selectProvider({ flag: args.providerFlag });
115
+ try {
116
+ const summary = await runExtractor({
117
+ client: pool,
118
+ provider,
119
+ dataDir: args.dataDir,
120
+ maxCostUsd: args.maxCostUsd,
121
+ contextTopN: args.contextTopN,
122
+ sinceDays: args.sinceDays,
123
+ full: args.full,
124
+ resumeFrom: args.resume,
125
+ });
126
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
127
+ }
128
+ finally {
129
+ await closeDb();
130
+ }
131
+ }
132
+ // Only run main() when invoked as the script entry point. Importing parseArgs
133
+ // (e.g. from tests) must not boot the extractor.
134
+ const isDirectInvocation = (() => {
135
+ if (!process.argv[1])
136
+ return false;
137
+ try {
138
+ return import.meta.url === new URL(`file://${process.argv[1]}`).href;
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ })();
144
+ if (isDirectInvocation) {
145
+ // Wrap main() in the extracted parent context so any future OTel spans
146
+ // in this process chain under the MCP wrapper's parent span (claw-2jbo
147
+ // finding 6). When parentCtx is undefined we just run main() directly.
148
+ const parentCtx = parentContextFromEnv();
149
+ const runner = () => main().catch((err) => {
150
+ if (err instanceof UsageError) {
151
+ process.stderr.write(`${err.message}\n`);
152
+ process.exit(err.exitCode);
153
+ }
154
+ if (err instanceof ProviderUnavailableError) {
155
+ process.stderr.write(`${err.message}\n`);
156
+ }
157
+ else {
158
+ process.stderr.write(`ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
159
+ }
160
+ process.exit(1);
161
+ });
162
+ if (parentCtx)
163
+ context.with(parentCtx, runner);
164
+ else
165
+ runner();
166
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * SPEC-044 Section C — lint CLI driver.
4
+ *
5
+ * Usage:
6
+ * npm run lint -- [--check=<name>] [--since=Nd] [--limit=N] [--fix]
7
+ *
8
+ * Output: human-readable terminal report grouped by check type, with a
9
+ * trailing JSON summary line for programmatic consumption (mirrors the
10
+ * pattern of classify-edges and compile-wiki).
11
+ *
12
+ * Exit codes:
13
+ * 0 — completed normally (regardless of finding count)
14
+ * 1 — fatal error (DB unreachable, malformed input)
15
+ */
16
+ export {};
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * SPEC-044 Section C — lint CLI driver.
4
+ *
5
+ * Usage:
6
+ * npm run lint -- [--check=<name>] [--since=Nd] [--limit=N] [--fix]
7
+ *
8
+ * Output: human-readable terminal report grouped by check type, with a
9
+ * trailing JSON summary line for programmatic consumption (mirrors the
10
+ * pattern of classify-edges and compile-wiki).
11
+ *
12
+ * Exit codes:
13
+ * 0 — completed normally (regardless of finding count)
14
+ * 1 — fatal error (DB unreachable, malformed input)
15
+ */
16
+ import { resolve } from 'node:path';
17
+ import { initDb, getPool, closeDb } from '../db.js';
18
+ import { loadEnvFile } from '../env.js';
19
+ import { runLint } from '../lint/run.js';
20
+ import { ALL_CHECKS } from '../lint/types.js';
21
+ // Load .env from project root — mirrors src/index.ts (claw-8cjf.2; this CLI
22
+ // previously loaded no environment at all and silently hit the default URL).
23
+ loadEnvFile(resolve(process.env.PROJECT_ROOT || process.cwd(), '.env'));
24
+ function isCheck(s) {
25
+ return ALL_CHECKS.includes(s);
26
+ }
27
+ function parseArgs(argv) {
28
+ const out = {};
29
+ for (const arg of argv) {
30
+ if (arg === '--fix')
31
+ out.fix = true;
32
+ else if (arg.startsWith('--check=')) {
33
+ const c = arg.split('=')[1];
34
+ if (!isCheck(c))
35
+ throw new Error(`--check must be one of ${ALL_CHECKS.join('|')}, got ${c}`);
36
+ out.check = c;
37
+ }
38
+ else if (arg.startsWith('--since=')) {
39
+ const raw = arg.split('=')[1];
40
+ const m = raw.match(/^(\d+)d$/);
41
+ if (!m)
42
+ throw new Error(`--since must be Nd (e.g. 30d), got ${raw}`);
43
+ out.since_days = Number(m[1]);
44
+ }
45
+ else if (arg.startsWith('--limit=')) {
46
+ out.limit = Number(arg.split('=')[1]);
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ function renderReport(result) {
52
+ const lines = [];
53
+ lines.push('Lint Report');
54
+ lines.push('===========');
55
+ lines.push(`Total findings: ${result.summary.total_findings}`);
56
+ for (const [check, count] of Object.entries(result.summary.by_check)) {
57
+ lines.push(` ${check}: ${count}`);
58
+ }
59
+ // Per-check sections (C.AC3)
60
+ for (const checkName of ALL_CHECKS) {
61
+ const subset = result.findings.filter((f) => f.check === checkName);
62
+ if (subset.length === 0)
63
+ continue;
64
+ lines.push('');
65
+ lines.push(`## ${checkName}`);
66
+ for (const f of subset) {
67
+ lines.push(` - ${f.memory_id}${f.related_memory_id ? ` ↔ ${f.related_memory_id}` : ''}`);
68
+ lines.push(` action: ${f.suggested_action} (confidence ${f.confidence.toFixed(2)})`);
69
+ lines.push(` ${f.rationale}`);
70
+ }
71
+ }
72
+ if (result.fixes_applied && result.fixes_applied.length > 0) {
73
+ lines.push('');
74
+ lines.push(`Fixes applied (${result.fixes_applied.length}):`);
75
+ for (const fx of result.fixes_applied) {
76
+ lines.push(` - ${fx.memory_id}: ${fx.action}`);
77
+ }
78
+ }
79
+ return lines.join('\n');
80
+ }
81
+ async function main() {
82
+ const args = parseArgs(process.argv.slice(2));
83
+ await initDb();
84
+ const result = await runLint(args, getPool());
85
+ process.stdout.write(renderReport(result) + '\n');
86
+ process.stdout.write('---\n');
87
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
88
+ await closeDb();
89
+ process.exit(0);
90
+ }
91
+ main().catch((err) => {
92
+ process.stderr.write(`ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
93
+ process.exit(1);
94
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Migration script: imports tiered markdown memory files into PostgreSQL.
3
+ *
4
+ * Usage:
5
+ * R2MCP_DATABASE_URL=postgresql://localhost:5432/r2mcp npx tsx scripts/migrate.ts <memory-dir>
6
+ *
7
+ * Where <memory-dir> contains preferences.md, project-context.md, conversations.md
8
+ *
9
+ * Idempotent — re-running produces zero new rows thanks to SHA-256 fingerprint dedup.
10
+ */
11
+ import type { Tier, MemoryType, MemoryMetadata } from '../tools/remember.js';
12
+ export interface ParsedEntry {
13
+ content: string;
14
+ tier: Tier;
15
+ metadata: MemoryMetadata;
16
+ }
17
+ export declare function parseMarkdownEntries(content: string, tier: Tier, defaultType: MemoryType): ParsedEntry[];
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Migration script: imports tiered markdown memory files into PostgreSQL.
3
+ *
4
+ * Usage:
5
+ * R2MCP_DATABASE_URL=postgresql://localhost:5432/r2mcp npx tsx scripts/migrate.ts <memory-dir>
6
+ *
7
+ * Where <memory-dir> contains preferences.md, project-context.md, conversations.md
8
+ *
9
+ * Idempotent — re-running produces zero new rows thanks to SHA-256 fingerprint dedup.
10
+ */
11
+ import { readFileSync } from 'node:fs';
12
+ import { join, resolve } from 'node:path';
13
+ import { initDb, closeDb } from '../db.js';
14
+ import { loadEnvFile } from '../env.js';
15
+ import { remember } from '../tools/remember.js';
16
+ const TIER_FILES = [
17
+ { file: 'preferences.md', tier: 'preferences', defaultType: 'preference' },
18
+ { file: 'project-context.md', tier: 'project-context', defaultType: 'context' },
19
+ { file: 'conversations.md', tier: 'conversations', defaultType: 'relationship' },
20
+ ];
21
+ function parseInlineMetadata(text) {
22
+ const meta = {};
23
+ const commentRegex = /<!--\s*(.*?)\s*-->/gs;
24
+ let match;
25
+ while ((match = commentRegex.exec(text)) !== null) {
26
+ const body = match[1];
27
+ const typeMatch = body.match(/type:(\S+)/);
28
+ if (typeMatch)
29
+ meta.type = typeMatch[1];
30
+ const topicsMatch = body.match(/topics:(\S+)/);
31
+ if (topicsMatch)
32
+ meta.topics = topicsMatch[1].split(',').filter(Boolean);
33
+ const peopleMatch = body.match(/people:(\S+)/);
34
+ if (peopleMatch)
35
+ meta.people = peopleMatch[1].split(',').filter(Boolean);
36
+ }
37
+ return meta;
38
+ }
39
+ export function parseMarkdownEntries(content, tier, defaultType) {
40
+ const lines = content.split('\n');
41
+ const entries = [];
42
+ let currentSection;
43
+ let currentEntry = null;
44
+ function flushEntry() {
45
+ if (!currentEntry || currentEntry.length === 0)
46
+ return;
47
+ const raw = currentEntry.join('\n').trim();
48
+ if (!raw)
49
+ return;
50
+ const inlineMeta = parseInlineMetadata(raw);
51
+ entries.push({
52
+ content: raw,
53
+ tier,
54
+ metadata: {
55
+ type: inlineMeta.type || defaultType,
56
+ section: currentSection,
57
+ ...(inlineMeta.topics && { topics: inlineMeta.topics }),
58
+ ...(inlineMeta.people && { people: inlineMeta.people }),
59
+ },
60
+ });
61
+ }
62
+ for (const line of lines) {
63
+ if (/^# /.test(line))
64
+ continue;
65
+ if (/^## /.test(line)) {
66
+ flushEntry();
67
+ currentEntry = null;
68
+ currentSection = line.replace(/^##\s*/, '').trim();
69
+ continue;
70
+ }
71
+ if (/^- /.test(line)) {
72
+ flushEntry();
73
+ currentEntry = [line.slice(2)];
74
+ continue;
75
+ }
76
+ if (currentEntry !== null && /^\s+/.test(line)) {
77
+ currentEntry.push(line);
78
+ continue;
79
+ }
80
+ if (line.trim() === '')
81
+ continue;
82
+ }
83
+ flushEntry();
84
+ return entries;
85
+ }
86
+ async function migrate(memoryDir) {
87
+ await initDb();
88
+ let totalAdded = 0, totalDedup = 0, totalErrors = 0;
89
+ for (const { file, tier, defaultType } of TIER_FILES) {
90
+ const filePath = join(memoryDir, file);
91
+ let raw;
92
+ try {
93
+ raw = readFileSync(filePath, 'utf-8');
94
+ }
95
+ catch (err) {
96
+ console.error(` ⚠️ Could not read ${filePath}: ${err.message}`);
97
+ totalErrors++;
98
+ continue;
99
+ }
100
+ const entries = parseMarkdownEntries(raw, tier, defaultType);
101
+ console.log(`Migrating ${file} → tier: ${tier} (${entries.length} entries)`);
102
+ for (let i = 0; i < entries.length; i++) {
103
+ const entry = entries[i];
104
+ const preview = entry.content.slice(0, 60).replace(/\n/g, ' ');
105
+ try {
106
+ const result = await remember({
107
+ operation: 'ADD',
108
+ tier: entry.tier,
109
+ content: entry.content,
110
+ metadata: entry.metadata,
111
+ });
112
+ if (result.dedup) {
113
+ console.log(` [${i + 1}/${entries.length}] Dedup: "${preview}..."`);
114
+ totalDedup++;
115
+ }
116
+ else {
117
+ console.log(` [${i + 1}/${entries.length}] Added: "${preview}..."`);
118
+ totalAdded++;
119
+ }
120
+ }
121
+ catch (err) {
122
+ console.error(` [${i + 1}/${entries.length}] Error: "${preview}..." — ${err.message}`);
123
+ totalErrors++;
124
+ }
125
+ }
126
+ }
127
+ console.log(`\nMigration complete: ${totalAdded} added, ${totalDedup} deduplicated, ${totalErrors} errors`);
128
+ await closeDb();
129
+ }
130
+ const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.dirname || '.', 'migrate.ts');
131
+ if (isMain) {
132
+ // Load .env only on the CLI path — tests import this module, and a
133
+ // module-level load would leak the consumer's R2MCP_DATABASE_URL into the
134
+ // test process (the test-isolation guard caught exactly that, claw-8cjf.2).
135
+ loadEnvFile(resolve(process.env.PROJECT_ROOT || process.cwd(), '.env'));
136
+ const memoryDir = process.argv[2] ? resolve(process.argv[2]) : null;
137
+ if (!memoryDir) {
138
+ console.error('Usage: tsx scripts/migrate.ts <memory-dir>');
139
+ console.error(' <memory-dir> must contain preferences.md, project-context.md, conversations.md');
140
+ process.exit(1);
141
+ }
142
+ migrate(memoryDir).catch((err) => {
143
+ console.error('Migration failed:', err);
144
+ process.exit(1);
145
+ });
146
+ }
@@ -0,0 +1,7 @@
1
+ export declare function redactDatabaseUrl(url: string): string;
2
+ export declare function validateDatabaseUrl(url: string): void;
3
+ export type SetupErrorClassification = {
4
+ cause: string;
5
+ fix: string;
6
+ };
7
+ export declare function classifySetupError(err: Error, redactedUrl: string): SetupErrorClassification;
@@ -0,0 +1,72 @@
1
+ export function redactDatabaseUrl(url) {
2
+ return url.replace(/:[^:/@]+@/, ':***@');
3
+ }
4
+ export function validateDatabaseUrl(url) {
5
+ let parsed;
6
+ try {
7
+ parsed = new URL(url);
8
+ }
9
+ catch {
10
+ return;
11
+ }
12
+ if (parsed.port === '6543') {
13
+ throw new Error(`Transaction-pooler URL detected (port 6543).\n` +
14
+ `Schema setup needs a session-capable connection — the transaction pooler does not\n` +
15
+ `support prepared statements or DDL. Use the Session pooler (port 5432) instead:\n` +
16
+ `\n` +
17
+ ` Supabase Dashboard → Connect (top of page) → Session pooler\n` +
18
+ `\n` +
19
+ `It looks like: postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgres\n` +
20
+ `(A Direct connection also works if your network has IPv6 or the IPv4 add-on.)`);
21
+ }
22
+ }
23
+ export function classifySetupError(err, redactedUrl) {
24
+ const msg = err.message.toLowerCase();
25
+ const code = err.code;
26
+ if (code === 'ECONNREFUSED' ||
27
+ msg.includes('connect econnrefused') ||
28
+ msg.includes('connection refused')) {
29
+ return {
30
+ cause: `Connection refused — could not reach ${redactedUrl}`,
31
+ fix: 'Check that PostgreSQL is running (Docker: docker compose up -d) and that the host/port in DATABASE_URL are correct',
32
+ };
33
+ }
34
+ if (msg.includes('password authentication failed') ||
35
+ msg.includes('authentication failed') ||
36
+ (msg.includes('role') && msg.includes('does not exist'))) {
37
+ return {
38
+ cause: `Authentication failed for ${redactedUrl}`,
39
+ fix: 'Verify R2MCP_DATABASE_URL credentials in .env — username, password, and database name must match your PostgreSQL instance',
40
+ };
41
+ }
42
+ if (msg.includes('extension "vector" is not available') ||
43
+ msg.includes('could not open extension control file') ||
44
+ msg.includes('pgvector')) {
45
+ return {
46
+ cause: `pgvector extension not available at ${redactedUrl}`,
47
+ fix: 'Enable pgvector: Supabase Dashboard → Database → Extensions → search "vector" → Enable. For Docker, use pgvector/pgvector:pg17 image.',
48
+ };
49
+ }
50
+ if (code === 'ENETUNREACH' || msg.includes('enetunreach')) {
51
+ return {
52
+ cause: `Network unreachable (IPv6) — could not reach ${redactedUrl}`,
53
+ fix: 'The Supabase Direct connection resolves to an IPv6 address, and this network appears to be IPv4-only ' +
54
+ '(IPv4 for direct connections is a paid add-on). Use the Session pooler instead: in the Supabase dashboard ' +
55
+ 'click "Connect" and copy the Session pooler string (port 5432, host like aws-0-<region>.pooler.supabase.com, ' +
56
+ 'username postgres.<project-ref>). It is IPv4-compatible on every tier and supports schema setup.',
57
+ };
58
+ }
59
+ if (code === 'ETIMEDOUT' ||
60
+ code === 'ENOTFOUND' ||
61
+ msg.includes('connect etimedout') ||
62
+ msg.includes('getaddrinfo')) {
63
+ return {
64
+ cause: `Could not reach ${redactedUrl}`,
65
+ fix: 'Check that the host in DATABASE_URL is correct. For Supabase: verify your IP is allowed under Project Settings → Networking, and that the project ref in the URL matches your project.',
66
+ };
67
+ }
68
+ return {
69
+ cause: `Setup failed: ${err.message}`,
70
+ fix: 'Check R2MCP_DATABASE_URL in .env and ensure the database is accessible',
71
+ };
72
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * r2mcp setup script — idempotent schema provisioner.
4
+ *
5
+ * Usage: npm run setup
6
+ *
7
+ * What it does:
8
+ * 1. Connects to R2MCP_DATABASE_URL
9
+ * 2. Enables the pgvector extension
10
+ * 3. Runs schema.sql (CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS)
11
+ * 4. Verifies the schema by running a quick sanity query
12
+ *
13
+ * Safe to re-run: all DDL uses IF NOT EXISTS / IF EXISTS. No data is modified.
14
+ */
15
+ export {};
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * r2mcp setup script — idempotent schema provisioner.
4
+ *
5
+ * Usage: npm run setup
6
+ *
7
+ * What it does:
8
+ * 1. Connects to R2MCP_DATABASE_URL
9
+ * 2. Enables the pgvector extension
10
+ * 3. Runs schema.sql (CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS)
11
+ * 4. Verifies the schema by running a quick sanity query
12
+ *
13
+ * Safe to re-run: all DDL uses IF NOT EXISTS / IF EXISTS. No data is modified.
14
+ */
15
+ import pg from 'pg';
16
+ import pgvector from 'pgvector/pg';
17
+ import { readFileSync } from 'node:fs';
18
+ import { resolve, dirname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { validateDatabaseUrl, classifySetupError, redactDatabaseUrl } from './setup-helpers.js';
21
+ import { loadEnvFile } from '../env.js';
22
+ import { MISSING_DATABASE_URL_MESSAGE } from '../db.js';
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ // Load .env from project root
25
+ loadEnvFile(resolve(__dirname, '..', '..', '.env'));
26
+ // claw-8cjf.2: fail fast instead of defaulting to a credential-less localhost
27
+ // URL that matches neither the Docker compose setup nor any hosted option.
28
+ if (!process.env.R2MCP_DATABASE_URL) {
29
+ console.error(`\n❌ ${MISSING_DATABASE_URL_MESSAGE}`);
30
+ process.exit(1);
31
+ }
32
+ const R2MCP_DATABASE_URL = process.env.R2MCP_DATABASE_URL;
33
+ async function setup() {
34
+ const redactedUrl = redactDatabaseUrl(R2MCP_DATABASE_URL);
35
+ try {
36
+ validateDatabaseUrl(R2MCP_DATABASE_URL);
37
+ }
38
+ catch (validationErr) {
39
+ console.error(`\n❌ Invalid database URL: ${redactedUrl}`);
40
+ console.error(validationErr.message);
41
+ process.exit(1);
42
+ }
43
+ console.log('r2mcp setup — provisioning database schema...');
44
+ console.log(`Connecting to: ${redactedUrl}`);
45
+ const pool = new pg.Pool({ connectionString: R2MCP_DATABASE_URL });
46
+ const client = await pool.connect();
47
+ try {
48
+ // 1. Enable pgvector
49
+ console.log('→ Enabling pgvector extension...');
50
+ await client.query('CREATE EXTENSION IF NOT EXISTS vector');
51
+ console.log(' ✓ pgvector enabled');
52
+ // 2. Register pgvector types
53
+ await pgvector.registerTypes(client);
54
+ // 3. Run schema.sql
55
+ const schemaPath = resolve(__dirname, '..', 'schema.sql');
56
+ const schema = readFileSync(schemaPath, 'utf-8');
57
+ console.log('→ Applying schema.sql...');
58
+ await client.query(schema);
59
+ console.log(' ✓ Schema applied (idempotent)');
60
+ // 4. Sanity check
61
+ const result = await client.query(`
62
+ SELECT
63
+ (SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'memories')::int AS table_exists,
64
+ (SELECT COUNT(*) FROM pg_indexes WHERE tablename = 'memories')::int AS index_count
65
+ `);
66
+ const { table_exists, index_count } = result.rows[0];
67
+ if (table_exists !== 1) {
68
+ throw new Error('memories table was not created — schema.sql may have failed silently');
69
+ }
70
+ console.log(` ✓ memories table present, ${index_count} indexes`);
71
+ console.log('\n✅ r2mcp setup complete!');
72
+ console.log('\nNext steps:');
73
+ console.log(' 1. Add to .mcp.json:');
74
+ console.log(' "memory": {');
75
+ console.log(' "command": "node",');
76
+ console.log(' "args": ["<path-to-r2mcp>/dist/index.js"],');
77
+ console.log(' "env": { "R2MCP_DATABASE_URL": "<your-url>", "R2MCP_OPENROUTER_API_KEY": "<your-key>" }');
78
+ console.log(' }');
79
+ console.log(' 2. Build: npm run build');
80
+ console.log(' 3. Restart Claude Code to pick up the new MCP server');
81
+ }
82
+ finally {
83
+ client.release();
84
+ await pool.end();
85
+ }
86
+ }
87
+ setup().catch((err) => {
88
+ const redactedUrl = redactDatabaseUrl(R2MCP_DATABASE_URL);
89
+ // pg library always throws Error instances; cast is safe
90
+ const { cause, fix } = classifySetupError(err, redactedUrl);
91
+ console.error('\n❌ Setup failed');
92
+ console.error(`\nCause: ${cause}`);
93
+ console.error(`Fix: ${fix}`);
94
+ process.exit(1);
95
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Deterministic memory grouping — used by both tier and topic compile to
3
+ * keep section structure stable across runs (B.R5 / B.AC3).
4
+ *
5
+ * Clustering is computed from input only — the LLM never sees an
6
+ * unsorted/unstable list, so its outputs cite the same memory IDs and
7
+ * surface the same headers run-to-run.
8
+ */
9
+ import type { MemoryForCompile } from './types.js';
10
+ export interface MemoryCluster {
11
+ topic: string;
12
+ topic_slug: string;
13
+ memories: MemoryForCompile[];
14
+ }
15
+ /**
16
+ * Group memories by their first-listed topic, then sort:
17
+ * - clusters by topic name (alphabetical)
18
+ * - memories within each cluster by `id` (alphabetical, stable)
19
+ *
20
+ * Memories with no topics are grouped under "uncategorized".
21
+ */
22
+ export declare function clusterByTopic(memories: MemoryForCompile[]): MemoryCluster[];
23
+ /**
24
+ * For topic-mode compile: filter to memories tagged with `topic`, sort by
25
+ * `created_at` ascending (so Timeline section flows naturally), tiebreak by id.
26
+ */
27
+ export declare function memoriesForTopic(memories: MemoryForCompile[], topic: string): MemoryForCompile[];
28
+ export declare function topicToSlug(topic: string): string;
29
+ export declare function topicTitle(topic: string): string;