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,34 @@
1
+ const DRIFT_SQL = `
2
+ SELECT
3
+ m1.id AS newer_id,
4
+ m2.id AS older_id,
5
+ (
6
+ SELECT COUNT(*)::int FROM unnest(m1.topics) t1
7
+ WHERE t1 = ANY(m2.topics)
8
+ ) AS shared_topics
9
+ FROM memories m1
10
+ JOIN memories m2 ON m1.created_at > m2.created_at + INTERVAL '30 days'
11
+ WHERE m1.type != 'archived' AND m2.type != 'archived'
12
+ AND (
13
+ SELECT COUNT(*) FROM unnest(m1.topics) t1
14
+ WHERE t1 = ANY(m2.topics)
15
+ ) >= 2
16
+ AND NOT EXISTS (
17
+ SELECT 1 FROM memory_edges e
18
+ WHERE (e.from_memory_id = m1.id AND e.to_memory_id = m2.id)
19
+ OR (e.from_memory_id = m2.id AND e.to_memory_id = m1.id)
20
+ )
21
+ ORDER BY shared_topics DESC, m1.created_at DESC
22
+ LIMIT $1
23
+ `;
24
+ export async function findDrift(pool, opts) {
25
+ const rows = await pool.query(DRIFT_SQL, [opts.limit]);
26
+ return rows.rows.map((r) => ({
27
+ check: 'drift',
28
+ memory_id: r.newer_id,
29
+ related_memory_id: r.older_id,
30
+ rationale: `Pair shares ${r.shared_topics} topics with >30d gap and no classifier-written edge — possible classification gap.`,
31
+ suggested_action: 'reclassify',
32
+ confidence: 0.55,
33
+ }));
34
+ }
@@ -0,0 +1,5 @@
1
+ import type { LintFinding } from '../types.js';
2
+ import type { PoolLike } from './contradictions.js';
3
+ export declare function findOrphans(pool: PoolLike, opts: {
4
+ limit: number;
5
+ }): Promise<LintFinding[]>;
@@ -0,0 +1,25 @@
1
+ const ORPHANS_SQL = `
2
+ SELECT
3
+ m.id,
4
+ m.created_at::text AS created_at
5
+ FROM memories m
6
+ WHERE m.type != 'archived'
7
+ AND m.created_at < NOW() - INTERVAL '30 days'
8
+ AND NOT EXISTS (
9
+ SELECT 1 FROM memory_edges e
10
+ WHERE (e.from_memory_id = m.id OR e.to_memory_id = m.id)
11
+ AND e.valid_until IS NULL
12
+ )
13
+ ORDER BY m.created_at ASC
14
+ LIMIT $1
15
+ `;
16
+ export async function findOrphans(pool, opts) {
17
+ const rows = await pool.query(ORPHANS_SQL, [opts.limit]);
18
+ return rows.rows.map((r) => ({
19
+ check: 'orphans',
20
+ memory_id: r.id,
21
+ rationale: `Memory has no edges in either direction since ${r.created_at}.`,
22
+ suggested_action: 'human_review',
23
+ confidence: 0.6,
24
+ }));
25
+ }
@@ -0,0 +1,6 @@
1
+ import type { LintFinding } from '../types.js';
2
+ import type { PoolLike } from './contradictions.js';
3
+ export declare function findStale(pool: PoolLike, opts: {
4
+ sinceDays: number;
5
+ limit: number;
6
+ }): Promise<LintFinding[]>;
@@ -0,0 +1,29 @@
1
+ const STALE_SQL = `
2
+ SELECT
3
+ m.id,
4
+ m.created_at::text AS created_at,
5
+ EXISTS (
6
+ SELECT 1 FROM memory_edges oe
7
+ WHERE oe.from_memory_id = m.id AND oe.valid_until IS NULL
8
+ ) AS has_outgoing
9
+ FROM memories m
10
+ WHERE m.type != 'archived'
11
+ AND m.tier != 'preferences'
12
+ AND m.created_at < NOW() - ($1 || ' days')::interval
13
+ AND NOT EXISTS (
14
+ SELECT 1 FROM memory_edges ie
15
+ WHERE ie.to_memory_id = m.id AND ie.valid_until IS NULL
16
+ )
17
+ ORDER BY m.created_at ASC
18
+ LIMIT $2
19
+ `;
20
+ export async function findStale(pool, opts) {
21
+ const rows = await pool.query(STALE_SQL, [opts.sinceDays, opts.limit]);
22
+ return rows.rows.map((r) => ({
23
+ check: 'stale',
24
+ memory_id: r.id,
25
+ rationale: `Memory created ${r.created_at} has no incoming edges${r.has_outgoing ? ' (but has outgoing references)' : ' or outgoing references'}.`,
26
+ suggested_action: 'archive',
27
+ confidence: r.has_outgoing ? 0.7 : 0.95,
28
+ }));
29
+ }
@@ -0,0 +1,5 @@
1
+ import type { LintFinding } from '../types.js';
2
+ import type { PoolLike } from './contradictions.js';
3
+ export declare function findSupersededUnflagged(pool: PoolLike, opts: {
4
+ limit: number;
5
+ }): Promise<LintFinding[]>;
@@ -0,0 +1,47 @@
1
+ const SUPER_UNFLAGGED_SQL = `
2
+ SELECT
3
+ e.id::text AS edge_id,
4
+ e.from_memory_id AS from_id,
5
+ e.to_memory_id AS to_id,
6
+ e.confidence::float AS confidence,
7
+ e.rationale,
8
+ m1.created_at::text AS from_created_at,
9
+ m2.created_at::text AS to_created_at,
10
+ (
11
+ SELECT COUNT(*)::int FROM unnest(m1.topics) t1
12
+ WHERE t1 = ANY(m2.topics)
13
+ ) AS shared_topics
14
+ FROM memory_edges e
15
+ JOIN memories m1 ON m1.id = e.from_memory_id
16
+ JOIN memories m2 ON m2.id = e.to_memory_id
17
+ WHERE e.relation = 'contradicts'
18
+ AND e.valid_until IS NULL
19
+ AND m1.created_at > m2.created_at -- from is strictly newer
20
+ AND m1.type != 'archived'
21
+ AND m2.type != 'archived'
22
+ ORDER BY (m1.created_at - m2.created_at) DESC
23
+ LIMIT $1
24
+ `;
25
+ export async function findSupersededUnflagged(pool, opts) {
26
+ const rows = await pool.query(SUPER_UNFLAGGED_SQL, [opts.limit]);
27
+ const findings = [];
28
+ for (const r of rows.rows) {
29
+ const ageGapDays = ageDays(r.from_created_at, r.to_created_at);
30
+ let confidence = 0.85;
31
+ if (r.shared_topics >= 1 && ageGapDays >= 90)
32
+ confidence = 0.92;
33
+ findings.push({
34
+ check: 'superseded_unflagged',
35
+ memory_id: r.from_id,
36
+ related_memory_id: r.to_id,
37
+ rationale: `${r.from_id} contradicts ${r.to_id} but is ${ageGapDays}d newer with ${r.shared_topics} shared topics — likely supersedes, not contradicts.`,
38
+ suggested_action: 'fix_edge_type',
39
+ confidence,
40
+ });
41
+ }
42
+ return findings;
43
+ }
44
+ function ageDays(newer, older) {
45
+ const ms = new Date(newer).getTime() - new Date(older).getTime();
46
+ return Math.round(ms / (1000 * 60 * 60 * 24));
47
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Lint orchestrator — dispatches the requested check (or all five), composes
3
+ * findings into the standard `LintResult` shape, and applies `--fix` actions
4
+ * for findings with confidence >= 0.9.
5
+ *
6
+ * SQL-only. No LLM calls (C.R5). Each check is independent and runs as its
7
+ * own bounded query.
8
+ */
9
+ import { type PoolLike } from './checks/contradictions.js';
10
+ import { type LintInput, type LintResult } from './types.js';
11
+ export declare function runLint(input: LintInput, pool: PoolLike): Promise<LintResult>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Lint orchestrator — dispatches the requested check (or all five), composes
3
+ * findings into the standard `LintResult` shape, and applies `--fix` actions
4
+ * for findings with confidence >= 0.9.
5
+ *
6
+ * SQL-only. No LLM calls (C.R5). Each check is independent and runs as its
7
+ * own bounded query.
8
+ */
9
+ import { findContradictions } from './checks/contradictions.js';
10
+ import { findStale } from './checks/stale.js';
11
+ import { findOrphans } from './checks/orphans.js';
12
+ import { findDrift } from './checks/drift.js';
13
+ import { findSupersededUnflagged } from './checks/superseded-unflagged.js';
14
+ import { ALL_CHECKS, FIX_CONFIDENCE_THRESHOLD, } from './types.js';
15
+ const DEFAULT_LIMIT = 100;
16
+ const DEFAULT_SINCE_DAYS = 90;
17
+ export async function runLint(input, pool) {
18
+ const limit = input.limit ?? DEFAULT_LIMIT;
19
+ const sinceDays = input.since_days ?? DEFAULT_SINCE_DAYS;
20
+ const checksToRun = input.check ? [input.check] : [...ALL_CHECKS];
21
+ const findings = [];
22
+ for (const check of checksToRun) {
23
+ if (check === 'contradictions') {
24
+ findings.push(...(await findContradictions(pool, { limit, memoryId: input.memory_id })));
25
+ }
26
+ else if (check === 'stale') {
27
+ findings.push(...(await findStale(pool, { sinceDays, limit })));
28
+ }
29
+ else if (check === 'orphans') {
30
+ findings.push(...(await findOrphans(pool, { limit })));
31
+ }
32
+ else if (check === 'drift') {
33
+ findings.push(...(await findDrift(pool, { limit })));
34
+ }
35
+ else if (check === 'superseded_unflagged') {
36
+ findings.push(...(await findSupersededUnflagged(pool, { limit })));
37
+ }
38
+ }
39
+ const summary = buildSummary(findings);
40
+ const result = { summary, findings };
41
+ if (input.fix) {
42
+ const fixesApplied = await applyFixes(pool, findings);
43
+ result.fixes_applied = fixesApplied;
44
+ }
45
+ return result;
46
+ }
47
+ /**
48
+ * Compose the per-check counts plus total. Always includes every check name
49
+ * even when count is 0 — keeps the response shape stable across invocations
50
+ * (C.AC2: shape doesn't drift between different `check` selections).
51
+ */
52
+ function buildSummary(findings) {
53
+ const by_check = {
54
+ contradictions: 0,
55
+ stale: 0,
56
+ orphans: 0,
57
+ drift: 0,
58
+ superseded_unflagged: 0,
59
+ };
60
+ for (const f of findings)
61
+ by_check[f.check]++;
62
+ return {
63
+ total_findings: findings.length,
64
+ by_check,
65
+ };
66
+ }
67
+ /**
68
+ * Apply auto-fixes for findings with confidence >= 0.9 (C.R3, C.AC4).
69
+ *
70
+ * Currently fixed:
71
+ * - stale: archive the memory (set type='archived')
72
+ * - superseded_unflagged: rewrite edge type from 'contradicts' to 'supersedes'
73
+ *
74
+ * Other check types return findings only — `--fix` is a no-op for them.
75
+ */
76
+ async function applyFixes(pool, findings) {
77
+ const applied = [];
78
+ for (const f of findings) {
79
+ if (f.confidence < FIX_CONFIDENCE_THRESHOLD)
80
+ continue;
81
+ if (f.check === 'stale' && f.suggested_action === 'archive') {
82
+ await pool.query(`UPDATE memories SET type = 'archived', updated_at = NOW() WHERE id = $1 AND type != 'archived'`, [f.memory_id]);
83
+ applied.push({ memory_id: f.memory_id, action: 'archive' });
84
+ }
85
+ else if (f.check === 'superseded_unflagged' &&
86
+ f.suggested_action === 'fix_edge_type' &&
87
+ f.related_memory_id) {
88
+ await pool.query(`UPDATE memory_edges
89
+ SET relation = 'supersedes', updated_at = NOW()
90
+ WHERE from_memory_id = $1 AND to_memory_id = $2 AND relation = 'contradicts' AND valid_until IS NULL`, [f.memory_id, f.related_memory_id]);
91
+ applied.push({ memory_id: f.memory_id, action: 'fix_edge_type' });
92
+ }
93
+ }
94
+ return applied;
95
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Lint surfaces structural feedback that `meditate()` could only express
3
+ * implicitly. Five SQL-driven checks against memory_edges + memories — no
4
+ * LLM calls (SPEC-044 C.R5).
5
+ */
6
+ export type CheckName = 'contradictions' | 'stale' | 'orphans' | 'drift' | 'superseded_unflagged';
7
+ /**
8
+ * Suggested actions are a closed vocabulary so callers can route findings
9
+ * without natural-language matching. Each check's findings draw from the
10
+ * subset that fits its domain.
11
+ */
12
+ export type SuggestedAction = 'archive_one' | 'add_supersedes_edge' | 'human_review' | 'archive' | 'reclassify' | 'fix_edge_type';
13
+ export interface LintFinding {
14
+ check: CheckName;
15
+ memory_id: string;
16
+ /** Optional companion memory when the finding is about a pair (contradictions, drift, superseded_unflagged). */
17
+ related_memory_id?: string;
18
+ /**
19
+ * Optional first topic of `memory_id`'s memory, surfaced so downstream
20
+ * consumers (e.g. SPEC-047 lint→compile breadcrumb, which needs `compile
21
+ * --topic=<topic>`) can route findings without a second DB query. Populated
22
+ * by `contradictions` today; other checks may follow.
23
+ */
24
+ topic?: string;
25
+ rationale: string;
26
+ suggested_action: SuggestedAction;
27
+ /** Confidence score [0..1]. `lint --fix` only acts on findings >= 0.9. */
28
+ confidence: number;
29
+ }
30
+ export interface LintSummary {
31
+ total_findings: number;
32
+ by_check: Record<CheckName, number>;
33
+ }
34
+ export interface LintInput {
35
+ check?: CheckName;
36
+ /** Used by stale check (default: 90 days). */
37
+ since_days?: number;
38
+ /** Cap findings per-check (default: 100). */
39
+ limit?: number;
40
+ /** When true, apply fixes for findings with confidence >= 0.9. */
41
+ fix?: boolean;
42
+ /**
43
+ * Optional scope filter — only applied by the `contradictions` check today.
44
+ * Restricts findings to edges where either endpoint matches this memory id.
45
+ * Added for SPEC-047 (`lint --check=contradictions --memory-id=<id>`
46
+ * breadcrumb path); other checks ignore it.
47
+ */
48
+ memory_id?: string;
49
+ }
50
+ export interface LintResult {
51
+ summary: LintSummary;
52
+ findings: LintFinding[];
53
+ /** Per-finding action records when fix=true. */
54
+ fixes_applied?: Array<{
55
+ memory_id: string;
56
+ action: SuggestedAction;
57
+ }>;
58
+ }
59
+ export declare const ALL_CHECKS: ReadonlyArray<CheckName>;
60
+ export declare const FIX_CONFIDENCE_THRESHOLD = 0.9;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Lint surfaces structural feedback that `meditate()` could only express
3
+ * implicitly. Five SQL-driven checks against memory_edges + memories — no
4
+ * LLM calls (SPEC-044 C.R5).
5
+ */
6
+ export const ALL_CHECKS = [
7
+ 'contradictions',
8
+ 'stale',
9
+ 'orphans',
10
+ 'drift',
11
+ 'superseded_unflagged',
12
+ ];
13
+ export const FIX_CONFIDENCE_THRESHOLD = 0.9;
@@ -0,0 +1,7 @@
1
+ import { type ToolName } from './breadcrumbs.js';
2
+ export declare function asMcpResponse<T extends object>(toolName: ToolName, result: T, args: unknown): {
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ };
@@ -0,0 +1,13 @@
1
+ // SPEC-047 Phase 5 — MCP response shaping.
2
+ //
3
+ // Centralises JSON-stringify + breadcrumb wrapping for every server.tool() handler.
4
+ // Lives in its own module (not src/index.ts) so test files can import asMcpResponse
5
+ // without triggering the MCP server's main() and DB init.
6
+ import { withBreadcrumbs } from './breadcrumbs.js';
7
+ export function asMcpResponse(toolName, result, args) {
8
+ const ctx = { tool: toolName, response: result, args };
9
+ const wrapped = withBreadcrumbs(result, ctx);
10
+ return {
11
+ content: [{ type: 'text', text: JSON.stringify(wrapped, null, 2) }],
12
+ };
13
+ }
@@ -0,0 +1,13 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import type { CompleteRequest, CompleteResponse, LLMProvider, LogicalModel, ProviderName } from './types.js';
3
+ export declare class AnthropicProvider implements LLMProvider {
4
+ readonly name: ProviderName;
5
+ readonly concurrencyLimit = 10;
6
+ private sdk;
7
+ constructor(opts?: {
8
+ apiKey?: string;
9
+ sdk?: Anthropic;
10
+ });
11
+ static priceForTokens(model: LogicalModel, inputTokens: number, outputTokens: number): number;
12
+ complete(req: CompleteRequest): Promise<CompleteResponse>;
13
+ }
@@ -0,0 +1,56 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ const MODEL_IDS = {
3
+ haiku: 'claude-haiku-4-5-20251001',
4
+ opus: 'claude-opus-4-7',
5
+ sonnet: 'claude-sonnet-4-6',
6
+ };
7
+ // Per-million-token list prices (USD), public list price as of 2026-05.
8
+ const PRICES = {
9
+ haiku: { input: 0.8, output: 4.0 },
10
+ opus: { input: 15.0, output: 75.0 },
11
+ sonnet: { input: 3.0, output: 15.0 },
12
+ };
13
+ const DEFAULT_MAX_TOKENS = 256;
14
+ export class AnthropicProvider {
15
+ name = 'anthropic';
16
+ concurrencyLimit = 10;
17
+ sdk;
18
+ constructor(opts = {}) {
19
+ if (opts.sdk) {
20
+ this.sdk = opts.sdk;
21
+ return;
22
+ }
23
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
24
+ if (!apiKey) {
25
+ throw new Error('ANTHROPIC_API_KEY is required to construct AnthropicProvider');
26
+ }
27
+ this.sdk = new Anthropic({ apiKey });
28
+ }
29
+ static priceForTokens(model, inputTokens, outputTokens) {
30
+ const p = PRICES[model];
31
+ return (inputTokens / 1_000_000) * p.input + (outputTokens / 1_000_000) * p.output;
32
+ }
33
+ async complete(req) {
34
+ const startedAt = Date.now();
35
+ const response = await this.sdk.messages.create({
36
+ model: MODEL_IDS[req.model],
37
+ max_tokens: req.max_tokens ?? DEFAULT_MAX_TOKENS,
38
+ system: req.system,
39
+ messages: [{ role: 'user', content: req.prompt }],
40
+ });
41
+ const text = response.content
42
+ .filter((b) => b.type === 'text')
43
+ .map((b) => b.text)
44
+ .join('');
45
+ const inputTokens = response.usage.input_tokens;
46
+ const outputTokens = response.usage.output_tokens;
47
+ return {
48
+ response: text,
49
+ cost_usd: AnthropicProvider.priceForTokens(req.model, inputTokens, outputTokens),
50
+ latency_ms: Date.now() - startedAt,
51
+ input_tokens: inputTokens,
52
+ output_tokens: outputTokens,
53
+ raw: response,
54
+ };
55
+ }
56
+ }
@@ -0,0 +1,35 @@
1
+ import { spawn } from 'node:child_process';
2
+ import type { CompleteRequest, CompleteResponse, LLMProvider, ProviderName } from './types.js';
3
+ export interface ClaudeCodeOptions {
4
+ /** Override the binary name (tests use this to point at a stub). */
5
+ binary?: string;
6
+ /** Override the spawn function (tests use this to inject a mock child process). */
7
+ spawnFn?: typeof spawn;
8
+ /** Per-call timeout in milliseconds. Default 120s. */
9
+ runTimeoutMs?: number;
10
+ }
11
+ export declare class ClaudeCodeProvider implements LLMProvider {
12
+ readonly name: ProviderName;
13
+ readonly concurrencyLimit = 2;
14
+ private readonly binary;
15
+ private readonly spawnFn;
16
+ private readonly runTimeoutMs;
17
+ constructor(opts?: ClaudeCodeOptions);
18
+ complete(req: CompleteRequest): Promise<CompleteResponse>;
19
+ private composePrompt;
20
+ }
21
+ /**
22
+ * Probe whether Claude Code is logged in. Fast (~5s timeout). Used by
23
+ * `selectProvider()` for auto-fallback (D.AC1).
24
+ */
25
+ export declare function probeClaudeCode(opts?: {
26
+ binary?: string;
27
+ spawnFn?: typeof spawn;
28
+ timeoutMs?: number;
29
+ }): Promise<boolean>;
30
+ interface ClaudeJsonEnvelope {
31
+ text: string;
32
+ raw: unknown;
33
+ }
34
+ export declare function parseClaudeJson(stdout: string): ClaudeJsonEnvelope;
35
+ export {};
@@ -0,0 +1,175 @@
1
+ import { spawn } from 'node:child_process';
2
+ /**
3
+ * Claude Code headless adapter.
4
+ *
5
+ * Spawns `claude -p <prompt> --output-format=json` and reads the JSON envelope
6
+ * from stdout. Auth is OAuth-based (Max plan); `cost_usd` is exactly 0 for
7
+ * every call (Max-covered, D.AC5 strict equality).
8
+ *
9
+ * Probe: `probeClaudeCode()` runs a minimal prompt with a 5s timeout to
10
+ * detect whether the binary is on PATH and the user is logged in.
11
+ */
12
+ // Map logical models to the model identifiers Claude Code's CLI accepts.
13
+ const MODEL_IDS = {
14
+ haiku: 'claude-haiku-4-5',
15
+ opus: 'claude-opus-4-7',
16
+ sonnet: 'claude-sonnet-4-6',
17
+ };
18
+ const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
19
+ const DEFAULT_RUN_TIMEOUT_MS = 120_000;
20
+ export class ClaudeCodeProvider {
21
+ name = 'claude-code';
22
+ // Subprocess overhead — keep this conservative. D.R6.
23
+ concurrencyLimit = 2;
24
+ binary;
25
+ spawnFn;
26
+ runTimeoutMs;
27
+ constructor(opts = {}) {
28
+ // Resolution precedence: explicit opts.binary → R2MCP_CLAUDE_BIN env →
29
+ // bare 'claude'. The env var lets packaged consumers point at an
30
+ // absolute install path (e.g., ~/.local/bin/claude) when the spawning
31
+ // process inherits a sanitized PATH (launchd jobs, systemd services).
32
+ this.binary = opts.binary ?? process.env.R2MCP_CLAUDE_BIN ?? 'claude';
33
+ this.spawnFn = opts.spawnFn ?? spawn;
34
+ this.runTimeoutMs = opts.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
35
+ }
36
+ async complete(req) {
37
+ const startedAt = Date.now();
38
+ const args = [
39
+ '-p',
40
+ this.composePrompt(req),
41
+ '--output-format=json',
42
+ '--model',
43
+ MODEL_IDS[req.model],
44
+ ];
45
+ const stdout = await runClaude(this.spawnFn, this.binary, args, this.runTimeoutMs);
46
+ const parsed = parseClaudeJson(stdout);
47
+ return {
48
+ response: parsed.text,
49
+ cost_usd: 0, // Max-covered (D.AC5 strict equality)
50
+ latency_ms: Date.now() - startedAt,
51
+ raw: parsed.raw,
52
+ };
53
+ }
54
+ composePrompt(req) {
55
+ if (!req.system)
56
+ return req.prompt;
57
+ // Claude Code headless takes a single prompt string. Inline the system
58
+ // section so the same prompt structure works across providers.
59
+ return `[SYSTEM]\n${req.system}\n[/SYSTEM]\n\n${req.prompt}`;
60
+ }
61
+ }
62
+ /**
63
+ * Probe whether Claude Code is logged in. Fast (~5s timeout). Used by
64
+ * `selectProvider()` for auto-fallback (D.AC1).
65
+ */
66
+ export async function probeClaudeCode(opts = {}) {
67
+ const binary = opts.binary ?? process.env.R2MCP_CLAUDE_BIN ?? 'claude';
68
+ const spawnFn = opts.spawnFn ?? spawn;
69
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
70
+ try {
71
+ const out = await runClaude(spawnFn, binary, ['-p', 'ok', '--output-format=json'], timeoutMs);
72
+ const parsed = parseClaudeJson(out);
73
+ return typeof parsed.text === 'string';
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ /**
80
+ * claw-8cjf.7: an ENOENT here means the claude CLI isn't on this process's
81
+ * PATH (common under launchd / MCP hosts with sanitized PATH). Name the
82
+ * escape hatch instead of surfacing a bare `spawn claude ENOENT`.
83
+ */
84
+ function wrapSpawnError(err, binary) {
85
+ if (err.code !== 'ENOENT')
86
+ return err;
87
+ return new Error(`could not spawn '${binary}' (ENOENT) — the claude CLI is not on this process's PATH. ` +
88
+ `Set R2MCP_CLAUDE_BIN to the absolute path of the claude binary ` +
89
+ `(e.g. ~/.local/bin/claude) in your environment or .mcp.json "env" block.`, { cause: err });
90
+ }
91
+ function runClaude(spawnFn, binary, args, timeoutMs) {
92
+ return new Promise((resolve, reject) => {
93
+ let child;
94
+ try {
95
+ child = spawnFn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
96
+ }
97
+ catch (err) {
98
+ reject(wrapSpawnError(err instanceof Error ? err : new Error(String(err)), binary));
99
+ return;
100
+ }
101
+ let stdout = '';
102
+ let stderr = '';
103
+ let timedOut = false;
104
+ const timer = setTimeout(() => {
105
+ timedOut = true;
106
+ try {
107
+ child.kill('SIGKILL');
108
+ }
109
+ catch {
110
+ /* ignore */
111
+ }
112
+ }, timeoutMs);
113
+ child.stdout?.on('data', (d) => {
114
+ stdout += d.toString();
115
+ });
116
+ child.stderr?.on('data', (d) => {
117
+ stderr += d.toString();
118
+ });
119
+ child.on('error', (err) => {
120
+ clearTimeout(timer);
121
+ reject(wrapSpawnError(err, binary));
122
+ });
123
+ child.on('exit', (code, signal) => {
124
+ clearTimeout(timer);
125
+ if (timedOut)
126
+ return reject(new Error(`claude timed out after ${timeoutMs}ms`));
127
+ if (code !== 0) {
128
+ return reject(new Error(`claude exited ${code} (signal=${signal}): ${stderr.slice(0, 500)}`));
129
+ }
130
+ resolve(stdout);
131
+ });
132
+ });
133
+ }
134
+ export function parseClaudeJson(stdout) {
135
+ // Claude Code's --output-format=json envelope wraps the assistant response.
136
+ // Shape varies slightly across versions; the canonical fields are
137
+ // { result: "...", ... } or { messages: [...], ... }. We accept either.
138
+ let parsed;
139
+ try {
140
+ parsed = JSON.parse(stdout);
141
+ }
142
+ catch {
143
+ throw new Error(`Claude Code output was not JSON: ${stdout.slice(0, 200)}`);
144
+ }
145
+ if (!parsed || typeof parsed !== 'object') {
146
+ throw new Error('Claude Code JSON envelope is not an object');
147
+ }
148
+ const obj = parsed;
149
+ // Preferred: top-level `result` field
150
+ if (typeof obj.result === 'string') {
151
+ return { text: obj.result, raw: parsed };
152
+ }
153
+ // Fallback: messages array, take the last assistant message text
154
+ if (Array.isArray(obj.messages)) {
155
+ const msgs = obj.messages;
156
+ for (let i = msgs.length - 1; i >= 0; i--) {
157
+ const m = msgs[i];
158
+ if (m.role !== 'assistant')
159
+ continue;
160
+ if (typeof m.content === 'string')
161
+ return { text: m.content, raw: parsed };
162
+ if (Array.isArray(m.content)) {
163
+ const text = m.content
164
+ .filter((b) => {
165
+ return !!b && typeof b === 'object' && b.type === 'text';
166
+ })
167
+ .map((b) => b.text)
168
+ .join('');
169
+ if (text)
170
+ return { text, raw: parsed };
171
+ }
172
+ }
173
+ }
174
+ throw new Error(`Claude Code JSON envelope had no readable response: ${stdout.slice(0, 200)}`);
175
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Provider-selection error surfaces. Kept in a dedicated module so consumers
3
+ * can import them without pulling in the heavy adapter modules.
4
+ */
5
+ export declare class ProviderUnavailableError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * The exact error message thrown when no provider can be auto-selected.
10
+ * D.AC4 requires that all three remediation paths be named.
11
+ */
12
+ export declare const NO_PROVIDER_AVAILABLE_MESSAGE: string;