claude-code-infinite 1.0.11 → 1.0.16

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 (136) hide show
  1. package/README.md +214 -47
  2. package/dist/cc-request.d.ts +94 -0
  3. package/dist/cc-request.d.ts.map +1 -0
  4. package/dist/cc-request.js +194 -0
  5. package/dist/cc-request.js.map +1 -0
  6. package/dist/claude-env.d.ts +30 -0
  7. package/dist/claude-env.d.ts.map +1 -0
  8. package/dist/claude-env.js +55 -0
  9. package/dist/claude-env.js.map +1 -0
  10. package/dist/cli-args.d.ts +32 -0
  11. package/dist/cli-args.d.ts.map +1 -0
  12. package/dist/cli-args.js +72 -0
  13. package/dist/cli-args.js.map +1 -0
  14. package/dist/cli-lifecycle.d.ts +17 -0
  15. package/dist/cli-lifecycle.d.ts.map +1 -0
  16. package/dist/cli-lifecycle.js +30 -0
  17. package/dist/cli-lifecycle.js.map +1 -0
  18. package/dist/cli-main.d.ts +15 -0
  19. package/dist/cli-main.d.ts.map +1 -0
  20. package/dist/cli-main.js +464 -0
  21. package/dist/cli-main.js.map +1 -0
  22. package/dist/cli.js +13 -178
  23. package/dist/cli.js.map +1 -1
  24. package/dist/config.d.ts +3 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +29 -3
  27. package/dist/config.js.map +1 -1
  28. package/dist/count-tokens.d.ts +78 -0
  29. package/dist/count-tokens.d.ts.map +1 -0
  30. package/dist/count-tokens.js +277 -0
  31. package/dist/count-tokens.js.map +1 -0
  32. package/dist/final-index.d.ts +10 -0
  33. package/dist/final-index.d.ts.map +1 -0
  34. package/dist/final-index.js +37 -0
  35. package/dist/final-index.js.map +1 -0
  36. package/dist/hooks.d.ts +239 -0
  37. package/dist/hooks.d.ts.map +1 -0
  38. package/dist/hooks.js +618 -0
  39. package/dist/hooks.js.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +8 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/memtree-fetch.d.ts +24 -0
  45. package/dist/memtree-fetch.d.ts.map +1 -0
  46. package/dist/memtree-fetch.js +121 -0
  47. package/dist/memtree-fetch.js.map +1 -0
  48. package/dist/memtree-finder.d.ts +99 -0
  49. package/dist/memtree-finder.d.ts.map +1 -0
  50. package/dist/memtree-finder.js +235 -0
  51. package/dist/memtree-finder.js.map +1 -0
  52. package/dist/memtree-links.d.ts +16 -0
  53. package/dist/memtree-links.d.ts.map +1 -0
  54. package/dist/memtree-links.js +66 -0
  55. package/dist/memtree-links.js.map +1 -0
  56. package/dist/memtree-mcp-config.d.ts +28 -0
  57. package/dist/memtree-mcp-config.d.ts.map +1 -0
  58. package/dist/memtree-mcp-config.js +121 -0
  59. package/dist/memtree-mcp-config.js.map +1 -0
  60. package/dist/memtree-mcp.d.ts +265 -0
  61. package/dist/memtree-mcp.d.ts.map +1 -0
  62. package/dist/memtree-mcp.js +667 -0
  63. package/dist/memtree-mcp.js.map +1 -0
  64. package/dist/memtree-newest.d.ts +74 -0
  65. package/dist/memtree-newest.d.ts.map +1 -0
  66. package/dist/memtree-newest.js +217 -0
  67. package/dist/memtree-newest.js.map +1 -0
  68. package/dist/memtree-tools.d.ts +105 -0
  69. package/dist/memtree-tools.d.ts.map +1 -0
  70. package/dist/memtree-tools.js +408 -0
  71. package/dist/memtree-tools.js.map +1 -0
  72. package/dist/memtree.d.ts +430 -0
  73. package/dist/memtree.d.ts.map +1 -0
  74. package/dist/memtree.js +1286 -0
  75. package/dist/memtree.js.map +1 -0
  76. package/dist/notices.d.ts +177 -0
  77. package/dist/notices.d.ts.map +1 -0
  78. package/dist/notices.js +469 -0
  79. package/dist/notices.js.map +1 -0
  80. package/dist/payment-gate.d.ts +53 -0
  81. package/dist/payment-gate.d.ts.map +1 -0
  82. package/dist/payment-gate.js +97 -0
  83. package/dist/payment-gate.js.map +1 -0
  84. package/dist/project-meta.d.ts +15 -0
  85. package/dist/project-meta.d.ts.map +1 -0
  86. package/dist/project-meta.js +80 -0
  87. package/dist/project-meta.js.map +1 -0
  88. package/dist/prompt-accounting.d.ts +12 -0
  89. package/dist/prompt-accounting.d.ts.map +1 -0
  90. package/dist/prompt-accounting.js +70 -0
  91. package/dist/prompt-accounting.js.map +1 -0
  92. package/dist/proxy.d.ts +204 -0
  93. package/dist/proxy.d.ts.map +1 -0
  94. package/dist/proxy.js +4299 -0
  95. package/dist/proxy.js.map +1 -0
  96. package/dist/reqlog.d.ts +427 -0
  97. package/dist/reqlog.d.ts.map +1 -0
  98. package/dist/reqlog.js +146 -0
  99. package/dist/reqlog.js.map +1 -0
  100. package/dist/request-sizing.d.ts +74 -0
  101. package/dist/request-sizing.d.ts.map +1 -0
  102. package/dist/request-sizing.js +121 -0
  103. package/dist/request-sizing.js.map +1 -0
  104. package/dist/resume-repair-file.d.ts +17 -0
  105. package/dist/resume-repair-file.d.ts.map +1 -0
  106. package/dist/resume-repair-file.js +100 -0
  107. package/dist/resume-repair-file.js.map +1 -0
  108. package/dist/resume-repair.d.ts +27 -0
  109. package/dist/resume-repair.d.ts.map +1 -0
  110. package/dist/resume-repair.js +198 -0
  111. package/dist/resume-repair.js.map +1 -0
  112. package/dist/route-cache.d.ts +5 -0
  113. package/dist/route-cache.d.ts.map +1 -0
  114. package/dist/route-cache.js +63 -0
  115. package/dist/route-cache.js.map +1 -0
  116. package/dist/route-fallback.d.ts +67 -0
  117. package/dist/route-fallback.d.ts.map +1 -0
  118. package/dist/route-fallback.js +126 -0
  119. package/dist/route-fallback.js.map +1 -0
  120. package/dist/route-identity.d.ts +6 -0
  121. package/dist/route-identity.d.ts.map +1 -0
  122. package/dist/route-identity.js +100 -0
  123. package/dist/route-identity.js.map +1 -0
  124. package/dist/transcript-usage.d.ts +57 -0
  125. package/dist/transcript-usage.d.ts.map +1 -0
  126. package/dist/transcript-usage.js +570 -0
  127. package/dist/transcript-usage.js.map +1 -0
  128. package/dist/turns.d.ts +77 -0
  129. package/dist/turns.d.ts.map +1 -0
  130. package/dist/turns.js +330 -0
  131. package/dist/turns.js.map +1 -0
  132. package/dist/update-check.d.ts +38 -0
  133. package/dist/update-check.d.ts.map +1 -0
  134. package/dist/update-check.js +67 -0
  135. package/dist/update-check.js.map +1 -0
  136. package/package.json +6 -2
@@ -0,0 +1,667 @@
1
+ /**
2
+ * `memtree` MCP server: lets the agent search and read its own session's
3
+ * MemTree and the user's other sessions (tools `search`, `read_node`,
4
+ * `read_lines`, `list`, which Claude Code exposes as `mcp__memtree__*`).
5
+ * `search` covers all of the user's sessions, or one tree with `tree` ("current"
6
+ * for this session's), in text (default) or vector mode; every hit names its
7
+ * node as an address `<tree>#<node>` with its path from the root, which
8
+ * `read_node` / `read_lines` take as `node`.
9
+ *
10
+ * A minimal JSON-RPC 2.0 server over stdio (newline-delimited messages, the
11
+ * MCP stdio transport); no SDK dependency. Started by Claude Code from the
12
+ * `--mcp-config` ccc passes (interactive sessions) or one the caller passes
13
+ * (`ccc memtree-mcp`, e.g. the memory recall probe).
14
+ *
15
+ * Which tree: the loopback proxy of the ccc that launched this Claude Code
16
+ * (`CCC_MEMTREE_PROXY`, else the inherited `ANTHROPIC_BASE_URL`) answers
17
+ * `GET /memtree/current?session=<id>` with the newest page it served — the
18
+ * tree the current request was compressed against. The session id is the
19
+ * `CLAUDE_CODE_SESSION_ID` Claude Code sets for MCP servers. Reads of this
20
+ * session's current tree require that exact session on both the proxy's pointer
21
+ * and the page; missing or mismatched identity fails closed, and the host must
22
+ * restart this server with the current id when switching sessions. The page JSON
23
+ * is fetched through the proxy's key-free `/memtree/<id>.json` relay and cached
24
+ * once complete.
25
+ *
26
+ * Other trees (the owner's other sessions, by reference from list or search)
27
+ * are read the same way without the session check: they are
28
+ * named explicitly, and the server authorizes them by the user's key.
29
+ *
30
+ * `search` asks the server first (`/memtree/<id>/search`, the Step 6 term
31
+ * search, so a long session's page JSON is not downloaded just to search it)
32
+ * and falls back to searching the page JSON locally on a server without that
33
+ * endpoint.
34
+ */
35
+ import fs from "node:fs";
36
+ import { fileURLToPath } from "node:url";
37
+ import { FINDER_SEARCH_DEFAULT_LIMIT, FINDER_SEARCH_MAX_LIMIT, formatSearchResults, searchMode, formatSessions, searchQuery, SESSIONS_DEFAULT_LIMIT, SESSIONS_MAX_LIMIT, sessionsQuery, } from "./memtree-finder.js";
38
+ import { formatLines, formatNode, formatSearch, formatSearchHits, formatTail, MemtreeIndex, parseNodeAddress, serverSearchHits, READ_LINES_MAX_CHARS, READ_LINES_MAX_LINES, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT, ToolInputError, } from "./memtree-tools.js";
39
+ import { CLIENT_VERSION } from "./memtree.js";
40
+ export const MEMTREE_MCP_SERVER_NAME = "memtree";
41
+ /** Sent to MemTree as `x-memtree-tools` when this server is configured. */
42
+ export const MEMTREE_TOOL_NAMES = [
43
+ "search",
44
+ "read_node",
45
+ "read_lines",
46
+ "list",
47
+ ];
48
+ /** `tree` value naming this session's own current tree. */
49
+ export const CURRENT_TREE = "current";
50
+ const LATEST_PROTOCOL_VERSION = "2025-06-18";
51
+ const FETCH_TIMEOUT_MS = 60_000;
52
+ const PREFIX_RETRY_INTERVAL_MS = 45_000;
53
+ /**
54
+ * The MCP server's instructions, which Claude Code places in the system prompt
55
+ * from the first request: the one place that explains the agent's situation
56
+ * before any memory message exists. Constant, so the prompt cache holds.
57
+ */
58
+ export const MEMTREE_MCP_INSTRUCTIONS = "This session runs in Claude Code Infinite, which lets a conversation continue past the model's context window. " +
59
+ "MemTree indexes the whole conversation as it goes: a tree of summaries from a one-line overview at the root down to " +
60
+ "leaves, each pointing at the exact transcript lines it summarizes. Indexing runs in the background and trails the " +
61
+ "newest messages slightly.\n\n" +
62
+ "Until the conversation reaches its size budget you see every message verbatim, and these tools are only a way to " +
63
+ "find things in a long history. Once it reaches the budget, the earlier conversation is replaced by a single memory " +
64
+ "message: that tree, expanded where it looked relevant to the latest request and collapsed elsewhere. The user's own " +
65
+ "messages shown there, and everything after the memory message, stay word for word; the rest of the earlier " +
66
+ "conversation survives only as summaries, which drop exact values, instructions, reasoning and dead ends. The detail " +
67
+ "is not gone: these tools read it back.\n\n" +
68
+ "So once you see that memory message: when your work depends on something from earlier in the session (what the " +
69
+ "user asked for or ruled out, decisions and why they were made, findings, ids and paths, commands, what was tried " +
70
+ "and failed), search MemTree before re-deriving it from the code or guessing. The code shows what exists; the " +
71
+ "memory shows what was decided and why. Read the exact lines (read_lines) when precision matters.\n\n" +
72
+ "search with \"tree\": \"current\" looks in this session's MemTree; without tree it searches all of the user's " +
73
+ "sessions (use that when the user refers to earlier work that is not in this session), and list shows those " +
74
+ "sessions by time and project. Text mode (default, free) matches exact words, ids and paths; vector mode matches " +
75
+ "meaning: its first page is charged even when the query embedding is cached; cursor continuation is free, " +
76
+ "including when the embedding must be regenerated. Hits may include an address (<tree>#<node>) and its path from the root. If the address is unavailable, read_lines with the hit’s tree.ref and range reads its exact lines. With an address: " +
77
+ "read_node {\"node\": address} opens it with its children, and the path's addresses lead to its parent and " +
78
+ "siblings; read_lines {\"node\": address} reads a leaf's exact lines. Cursors page over live results, not a " +
79
+ "frozen snapshot. A tree trails its conversation: the newest messages (often how a session ended) are in no tree " +
80
+ "yet, and search does not see them; read_lines {\"tail\": true} reads them verbatim (with tree for another " +
81
+ "session's page).";
82
+ const TREE_DESC = "A tree reference from list or search (keep its style and own/served suffix; legacy request ids work), or \"current\" for this session's tree.";
83
+ export const MEMTREE_TOOLS = [
84
+ {
85
+ name: "search",
86
+ description: "Search MemTree for exact details or for meaning: one tree (tree, \"current\" for this session) or, without tree, " +
87
+ "all of the user's sessions. Use it before re-deriving or guessing something from earlier work. " +
88
+ "mode \"text\" (default, free) matches words, ids, paths and errors (in one tree: node summaries and transcript " +
89
+ "lines); mode \"vector\" matches meaning, ranked per embedding model; its first page is charged even when the " +
90
+ "query embedding is cached, and cursor continuation is free. Cursors page over live results, not a frozen snapshot. " +
91
+ "Hits include a snippet and range; their node address (<tree>#<node>) and path may be unavailable. Without an address, use read_lines with the hit’s tree.ref and range.",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ query: { type: "string", description: "Distinctive words (text; \"quoted phrase\", or, -word across sessions) or a question (vector)." },
96
+ tree: { type: "string", description: `Optional. ${TREE_DESC} Omit to search all sessions.` },
97
+ mode: { type: "string", enum: ["text", "vector"], description: "text (default, exact words, free) or vector (meaning, first page charged)." },
98
+ project: { type: "string", description: "Without tree: only sessions in this working directory or git repository (owner/repo, or just repo)." },
99
+ since: { type: "string", description: "Without tree: only requests at or after this ISO 8601 time or date." },
100
+ until: { type: "string", description: "Without tree: only requests before this ISO 8601 time or date." },
101
+ cursor: { type: "string", description: "next cursor from a previous call, to get the next page (same other arguments)." },
102
+ limit: { type: "number", description: `Hits per page (default ${FINDER_SEARCH_DEFAULT_LIMIT}, at most ${FINDER_SEARCH_MAX_LIMIT}; per model for vector).` },
103
+ },
104
+ required: ["query"],
105
+ },
106
+ },
107
+ {
108
+ name: "read_node",
109
+ description: "Read one MemTree node: its summary, its path from the root (ancestor summaries), its children (id, summary, leaf or branch) and, " +
110
+ "for a leaf, the transcript line range it covers. Give node (an address from search) or id (0 is the root) with an optional tree.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ node: { type: "string", description: "A node address <tree>#<id> from search." },
115
+ id: { type: "number", description: "Node id (0 is the root), in tree or this session's tree." },
116
+ tree: { type: "string", description: `With id: ${TREE_DESC} Omit for this session.` },
117
+ },
118
+ },
119
+ },
120
+ {
121
+ name: "read_lines",
122
+ description: "Read exact transcript lines: a leaf's node address (its whole range), or block and 1-based inclusive start/end lines. " +
123
+ `At most ${READ_LINES_MAX_LINES} lines / ${READ_LINES_MAX_CHARS} characters per call; the reply says where to continue when capped. ` +
124
+ "With tail: true, read the messages after the tree that no tree covers yet (the end of a session usually is there, " +
125
+ "and search does not see it): the newest ones, or start/end message positions; add tree for another session's page.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ node: { type: "string", description: "A leaf's address <tree>#<id> from search; block/start/end then default to its range." },
130
+ block: { type: "number", description: "Block index (a leaf's first range number)." },
131
+ start: { type: "number", description: "First line, 1-based." },
132
+ end: { type: "number", description: "Last line, inclusive." },
133
+ tree: { type: "string", description: `With block/start/end or tail: ${TREE_DESC} Omit for this session.` },
134
+ tail: { type: "boolean", description: "Read the un-indexed messages after the tree instead of lines; start/end are then message positions." },
135
+ },
136
+ },
137
+ },
138
+ {
139
+ name: "list",
140
+ description: "List the user's own MemTree sessions (this one and others), most recently active first: title (the tree's root summary), " +
141
+ "first message, times, project (directory, git repo, branch, commit), models, request count and the latest tree's reference. " +
142
+ "Filter by time, project or words in the title. Free. Open a session's tree with read_node {\"tree\": <ref>, \"id\": 0} or search {\"tree\": <ref>, ...}.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ since: { type: "string", description: "Only activity at or after this ISO 8601 time or date (UTC unless an offset is given), e.g. 2026-09-01." },
147
+ until: { type: "string", description: "Only activity before this ISO 8601 time or date." },
148
+ project: { type: "string", description: "Working directory name or git repository (owner/repo, or just repo), case-insensitive." },
149
+ q: { type: "string", description: "Keep sessions whose title, first message or project contains this text." },
150
+ cursor: { type: "string", description: "next cursor from a previous call, to get the next page (same other arguments)." },
151
+ limit: { type: "number", description: `Sessions per page (default ${SESSIONS_DEFAULT_LIMIT}, at most ${SESSIONS_MAX_LIMIT}).` },
152
+ },
153
+ },
154
+ },
155
+ ];
156
+ /** A request id in either spelling (UUID or the short leading-hex form). */
157
+ const TREE_ID_RE = /^[A-Za-z0-9-]{1,64}$/;
158
+ /** Other sessions' pages kept in memory at once (each can be several MB). */
159
+ const OTHER_TREES_CACHED = 4;
160
+ /** GET JSON from the loopback proxy; never throws. */
161
+ async function proxyGetJson(fetchImpl, url, headers = {}) {
162
+ let response;
163
+ try {
164
+ response = await fetchImpl(url, {
165
+ headers: { accept: "application/json", ...headers },
166
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
167
+ });
168
+ }
169
+ catch (err) {
170
+ return { ok: false, status: 0, error: String(err) };
171
+ }
172
+ const text = await response.text().catch(() => "");
173
+ if (!response.ok) {
174
+ let detail = text.slice(0, 300);
175
+ try {
176
+ const parsed = JSON.parse(text);
177
+ if (typeof parsed?.detail === "string")
178
+ detail = parsed.detail;
179
+ }
180
+ catch {
181
+ // not JSON: keep the raw text
182
+ }
183
+ return { ok: false, status: response.status, error: `HTTP ${response.status} ${detail}` };
184
+ }
185
+ try {
186
+ return { ok: true, status: response.status, body: JSON.parse(text) };
187
+ }
188
+ catch {
189
+ return { ok: false, status: response.status, error: "response was not JSON" };
190
+ }
191
+ }
192
+ function proxyBase(deps) {
193
+ const base = deps.proxyUrl?.replace(/\/$/, "");
194
+ if (!base) {
195
+ throw new ToolInputError("MemTree tools need a running ccc session: neither CCC_MEMTREE_PROXY nor ANTHROPIC_BASE_URL is set.");
196
+ }
197
+ return base;
198
+ }
199
+ function checkTreeId(tree) {
200
+ if (tree === undefined || tree === null || tree === "")
201
+ return undefined;
202
+ if (typeof tree !== "string" || !TREE_ID_RE.test(tree.trim())) {
203
+ throw new ToolInputError("tree must be a tree reference from list or search, or \"current\"");
204
+ }
205
+ return tree.trim();
206
+ }
207
+ /**
208
+ * Resolves, fetches and caches the current page, and other sessions' pages
209
+ * by request id.
210
+ */
211
+ export class CurrentTree {
212
+ deps;
213
+ cached;
214
+ /** Temporary prefix pages: when each may be fetched again. */
215
+ prefixRetryAt = new Map();
216
+ /** The last fetched temporary prefix per page, served during its cooldown. */
217
+ prefixes = new Map();
218
+ inFlight;
219
+ /** Other trees, least recently used first. */
220
+ others = new Map();
221
+ /** Set once the server answered 404 to a tree search the page JSON could answer. */
222
+ serverSearchMissing = false;
223
+ fetchImpl;
224
+ now;
225
+ constructor(deps) {
226
+ this.deps = deps;
227
+ this.fetchImpl = deps.fetch ?? globalThis.fetch;
228
+ this.now = deps.now ?? Date.now;
229
+ }
230
+ get(tree) {
231
+ let other;
232
+ try {
233
+ other = checkTreeId(tree);
234
+ }
235
+ catch (err) {
236
+ return Promise.reject(err);
237
+ }
238
+ if (other)
239
+ return this.getOther(other);
240
+ if (this.inFlight)
241
+ return this.inFlight;
242
+ const pending = this.loadCurrent();
243
+ this.inFlight = pending;
244
+ return pending.finally(() => {
245
+ if (this.inFlight === pending)
246
+ this.inFlight = undefined;
247
+ });
248
+ }
249
+ /**
250
+ * The server's term search over the tree when it has the endpoint (no page
251
+ * download), else the same search over the page JSON here.
252
+ */
253
+ async search(query, limit, tree) {
254
+ const other = checkTreeId(tree);
255
+ if (!this.serverSearchMissing) {
256
+ const id = other ?? (await this.currentId());
257
+ const params = new URLSearchParams({ q: query });
258
+ if (limit !== undefined && Number.isFinite(limit))
259
+ params.set("limit", String(limit));
260
+ const base = proxyBase(this.deps);
261
+ const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/search?${params}`);
262
+ const parsed = answer.ok ? serverSearchHits(answer.body) : undefined;
263
+ if (parsed) {
264
+ const session = answer.body.session_id;
265
+ if (!other && session !== undefined)
266
+ this.checkSession(session, this.requireSessionId());
267
+ if (other || session !== undefined)
268
+ return formatSearchHits(parsed.hits, query, parsed.terms, other);
269
+ // Older servers omit identity: search only the session-validated page.
270
+ }
271
+ if (answer.status === 202) {
272
+ throw new ToolInputError("That MemTree is still being built; try again in a minute.");
273
+ }
274
+ const result = formatSearch(await this.get(other), query, limit, other);
275
+ if (answer.status === 404 || answer.status === 405)
276
+ this.serverSearchMissing = true;
277
+ return result;
278
+ }
279
+ return formatSearch(await this.get(other), query, limit, other);
280
+ }
281
+ /**
282
+ * The server's un-indexed tail after the tree (`/messages`), never cached:
283
+ * it grows with the conversation. With no range, the newest messages.
284
+ */
285
+ async tail(tree, start, end) {
286
+ const other = checkTreeId(tree);
287
+ const id = other ?? (await this.currentId());
288
+ const params = new URLSearchParams();
289
+ if (start !== undefined)
290
+ params.set("start", String(start));
291
+ if (end !== undefined)
292
+ params.set("end", String(end));
293
+ const query = params.toString() ? `?${params}` : "";
294
+ const base = proxyBase(this.deps);
295
+ const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/messages${query}`);
296
+ if (answer.status === 404) {
297
+ return "No un-indexed messages after this tree: everything the server has recorded is in the tree (or the server predates tails).";
298
+ }
299
+ if (answer.status === 416) {
300
+ throw new ToolInputError(`read_lines: ${(answer.error ?? "").replace(/^HTTP 416 /, "")}`);
301
+ }
302
+ if (!answer.ok)
303
+ throw new ToolInputError(`MemTree tail unavailable (page ${id}: ${answer.error})`);
304
+ if (!other)
305
+ this.checkSession(answer.body.session_id, this.requireSessionId());
306
+ return formatTail(answer.body, other);
307
+ }
308
+ async loadCurrent() {
309
+ const id = await this.currentId();
310
+ const sessionId = this.requireSessionId();
311
+ if (this.cached?.id === id)
312
+ return this.cached.index;
313
+ const index = await this.loadPage(id, "this session", sessionId);
314
+ if (!isTemporaryPrefix(index, id))
315
+ this.cached = { id, index };
316
+ return index;
317
+ }
318
+ requireSessionId() {
319
+ const sessionId = this.deps.sessionId;
320
+ if (!sessionId)
321
+ throw new ToolInputError("MemTree unavailable: the calling session id is missing");
322
+ return sessionId;
323
+ }
324
+ /** The proxy's current page for the calling session; fails closed on any other session. */
325
+ async currentId(validatePage = false) {
326
+ const base = proxyBase(this.deps);
327
+ const sessionId = this.requireSessionId();
328
+ const query = `?session=${encodeURIComponent(sessionId)}`;
329
+ const pointer = await proxyGetJson(this.fetchImpl, `${base}/memtree/current${query}`);
330
+ if (pointer.status === 404) {
331
+ throw new ToolInputError("This session has no MemTree yet: nothing has been indexed or compressed so far, so everything is still in your context.");
332
+ }
333
+ if (!pointer.ok)
334
+ throw new ToolInputError(`MemTree unavailable (current page: ${pointer.error})`);
335
+ const current = pointer.body;
336
+ if (typeof current?.id !== "string" || !current.id) {
337
+ throw new ToolInputError("MemTree unavailable: the proxy named no current page");
338
+ }
339
+ if (current.session_id !== sessionId) {
340
+ throw new ToolInputError("MemTree unavailable: current page belongs to a different or unknown session");
341
+ }
342
+ if (validatePage) {
343
+ const index = this.cached?.id === current.id ? this.cached.index
344
+ : await this.loadPage(current.id, "this session", sessionId);
345
+ this.checkSession(index.page.session_id, sessionId);
346
+ if (!isTemporaryPrefix(index, current.id))
347
+ this.cached = { id: current.id, index };
348
+ }
349
+ return current.id;
350
+ }
351
+ checkSession(actual, expected) {
352
+ if (actual !== expected) {
353
+ throw new ToolInputError("MemTree unavailable: page belongs to a different or unknown session");
354
+ }
355
+ }
356
+ async getOther(id) {
357
+ const hit = this.others.get(id);
358
+ if (hit) {
359
+ this.others.delete(id);
360
+ this.others.set(id, hit);
361
+ return hit;
362
+ }
363
+ const index = await this.loadPage(id, `tree ${id}`);
364
+ if (!isTemporaryPrefix(index, id))
365
+ this.others.set(id, index);
366
+ while (this.others.size > OTHER_TREES_CACHED) {
367
+ this.others.delete(this.others.keys().next().value);
368
+ }
369
+ return index;
370
+ }
371
+ /**
372
+ * Fetch one page. ``sessionId`` (the current tree only) must match the page's
373
+ * session. A temporary prefix (the newest completed tree for an earlier part
374
+ * of the conversation, shown until the request's own tree is built) is not
375
+ * cached for good: it is refetched at most every PREFIX_RETRY_INTERVAL_MS, and
376
+ * the last copy is served in between, so successive tools neither hammer the
377
+ * page endpoint nor get an error for a tree they could read.
378
+ */
379
+ async loadPage(id, label, sessionId) {
380
+ const now = this.now();
381
+ const retryAt = this.prefixRetryAt.get(id);
382
+ if (retryAt !== undefined && retryAt > now) {
383
+ const stored = this.prefixes.get(id);
384
+ if (stored) {
385
+ if (sessionId !== undefined)
386
+ this.checkSession(stored.page.session_id, sessionId);
387
+ return stored;
388
+ }
389
+ throw this.prefixRetryError(retryAt - now);
390
+ }
391
+ // If a previous prefix refresh failed, keep the same cooldown after that
392
+ // failure so successive tools cannot hammer the page endpoint.
393
+ if (retryAt !== undefined)
394
+ this.prefixRetryAt.set(id, now + PREFIX_RETRY_INTERVAL_MS);
395
+ const base = proxyBase(this.deps);
396
+ const page = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}.json`);
397
+ if (!page.ok)
398
+ throw new ToolInputError(`MemTree unavailable (page ${id}: ${page.error})`);
399
+ const json = page.body;
400
+ if (!Array.isArray(json?.nodes) || json.nodes.length === 0) {
401
+ // Still building (or an empty tree): not cached, so the next call retries.
402
+ throw new ToolInputError(`The MemTree for ${label} is still being built${json?.status ? ` (${json.status})` : ""}; try again in a minute.`);
403
+ }
404
+ if (sessionId !== undefined && json.session_id !== sessionId) {
405
+ throw new ToolInputError("MemTree unavailable: page belongs to a different or unknown session");
406
+ }
407
+ const index = new MemtreeIndex(json);
408
+ if (isTemporaryPrefix(index, id)) {
409
+ this.prefixRetryAt.set(id, this.now() + PREFIX_RETRY_INTERVAL_MS);
410
+ this.prefixes.delete(id);
411
+ this.prefixes.set(id, index);
412
+ // Each page can be several MB: keep only the most recent few.
413
+ while (this.prefixes.size > OTHER_TREES_CACHED) {
414
+ this.prefixes.delete(this.prefixes.keys().next().value);
415
+ }
416
+ // Keep the map small if a long-lived MCP process sees many page ids.
417
+ if (this.prefixRetryAt.size > 64) {
418
+ const oldest = this.prefixRetryAt.keys().next().value;
419
+ if (oldest !== undefined)
420
+ this.prefixRetryAt.delete(oldest);
421
+ }
422
+ return index;
423
+ }
424
+ this.prefixRetryAt.delete(id);
425
+ this.prefixes.delete(id);
426
+ return index;
427
+ }
428
+ prefixRetryError(remainingMs) {
429
+ const seconds = Math.ceil(remainingMs / 1000);
430
+ return new ToolInputError(`This MemTree page is a temporary prefix while newer messages are indexing; try again in ${seconds} second${seconds === 1 ? "" : "s"}.`);
431
+ }
432
+ }
433
+ /** A served prefix that may later switch to the request's own tree (a pinned ref never does). */
434
+ function isTemporaryPrefix(index, id) {
435
+ return index.page.served_prefix === true && index.page.ref !== id;
436
+ }
437
+ export class SessionFinder {
438
+ deps;
439
+ fetchImpl;
440
+ constructor(deps) {
441
+ this.deps = deps;
442
+ this.fetchImpl = deps.fetch ?? globalThis.fetch;
443
+ }
444
+ async listSessions(args) {
445
+ const body = await this.get(`/memtree/sessions${sessionsQuery(args)}`, "list");
446
+ return formatSessions(body, args);
447
+ }
448
+ async searchSessions(args, tree) {
449
+ const body = await this.get(`/memtree/search${searchQuery(args, tree)}`, "search");
450
+ if (tree && (!body || typeof body !== "object" || body.tree !== tree)) {
451
+ throw new ToolInputError("search: server did not confirm the requested tree; update the MemTree server before scoped search");
452
+ }
453
+ return formatSearchResults(body, { ...args, tree });
454
+ }
455
+ async get(pathAndQuery, tool) {
456
+ const headers = {};
457
+ if (this.deps.sessionId)
458
+ headers["x-claude-code-session-id"] = this.deps.sessionId;
459
+ const answer = await proxyGetJson(this.fetchImpl, `${proxyBase(this.deps)}${pathAndQuery}`, headers);
460
+ if (answer.ok)
461
+ return answer.body;
462
+ if (answer.status === 404) {
463
+ throw new ToolInputError(`${tool}: this MemTree server does not support finding sessions yet (${answer.error}).`);
464
+ }
465
+ throw new ToolInputError(`${tool} failed: ${answer.error}`);
466
+ }
467
+ }
468
+ /**
469
+ * One JSON-RPC message in, the response out (undefined for notifications).
470
+ * Exposed for tests; the stdio loop below is only framing.
471
+ */
472
+ export async function handleMcpMessage(input, tree, finder) {
473
+ if (!isRpcRequest(input)) {
474
+ return { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request" } };
475
+ }
476
+ const message = input;
477
+ const isRequest = message.id !== undefined && message.id !== null;
478
+ const reply = (result) => ({ jsonrpc: "2.0", id: message.id, result });
479
+ const fail = (code, text) => ({
480
+ jsonrpc: "2.0",
481
+ id: message.id ?? null,
482
+ error: { code, message: text },
483
+ });
484
+ switch (message.method) {
485
+ case "initialize": {
486
+ const requested = message.params?.protocolVersion;
487
+ return reply({
488
+ protocolVersion: typeof requested === "string" ? requested : LATEST_PROTOCOL_VERSION,
489
+ capabilities: { tools: {} },
490
+ serverInfo: { name: MEMTREE_MCP_SERVER_NAME, version: CLIENT_VERSION },
491
+ instructions: MEMTREE_MCP_INSTRUCTIONS,
492
+ });
493
+ }
494
+ case "ping":
495
+ return isRequest ? reply({}) : undefined;
496
+ case "tools/list":
497
+ return reply({ tools: MEMTREE_TOOLS });
498
+ case "tools/call": {
499
+ const name = message.params?.name;
500
+ const args = (message.params?.arguments ?? {});
501
+ try {
502
+ const text = await callTool(String(name), args, tree, finder);
503
+ return reply({ content: [{ type: "text", text }] });
504
+ }
505
+ catch (err) {
506
+ if (err instanceof UnknownToolError)
507
+ return fail(-32602, err.message);
508
+ const text = err instanceof Error ? err.message : String(err);
509
+ return reply({ content: [{ type: "text", text }], isError: true });
510
+ }
511
+ }
512
+ default:
513
+ if (!isRequest)
514
+ return undefined; // notifications/initialized, cancelled, …
515
+ return fail(-32601, `Method not found: ${message.method}`);
516
+ }
517
+ }
518
+ function isRpcRequest(value) {
519
+ if (!value || typeof value !== "object" || Array.isArray(value))
520
+ return false;
521
+ const m = value;
522
+ return typeof m.method === "string" &&
523
+ (m.jsonrpc === undefined || m.jsonrpc === "2.0") &&
524
+ (m.id === undefined || m.id === null || typeof m.id === "string" ||
525
+ (typeof m.id === "number" && Number.isFinite(m.id))) &&
526
+ (m.params === undefined || (!!m.params && typeof m.params === "object" && !Array.isArray(m.params)));
527
+ }
528
+ class UnknownToolError extends Error {
529
+ }
530
+ async function callTool(name, args, tree, finder) {
531
+ switch (name) {
532
+ case "search":
533
+ return searchTool(args, tree, finder);
534
+ case "read_node": {
535
+ const target = nodeTarget(args, "id");
536
+ return formatNode(await tree.get(target.tree), target.id, target.tree);
537
+ }
538
+ case "read_lines":
539
+ return readLinesTool(args, tree);
540
+ case "list":
541
+ if (!finder)
542
+ throw new ToolInputError("list is not available here");
543
+ return finder.listSessions(args);
544
+ default:
545
+ throw new UnknownToolError(`Unknown tool: ${name}`);
546
+ }
547
+ }
548
+ /**
549
+ * `search`: a text search of one tree uses the per-tree term search (summaries
550
+ * and lines); every other combination is the server's search, across sessions
551
+ * or scoped to the tree.
552
+ */
553
+ async function searchTool(args, tree, finder) {
554
+ if (typeof args.query !== "string" || !args.query.trim()) {
555
+ throw new ToolInputError("search: query must be a non-empty string");
556
+ }
557
+ const mode = searchMode(args.mode);
558
+ const current = args.tree === CURRENT_TREE;
559
+ const named = current ? undefined : checkTreeId(args.tree);
560
+ if (mode === "text" && (current || named)) {
561
+ const limit = args.limit === undefined ? undefined : Number(args.limit);
562
+ if (tree.search)
563
+ return tree.search(args.query, limit, named);
564
+ return formatSearch(await tree.get(named), args.query, limit, named);
565
+ }
566
+ if (!finder)
567
+ throw new ToolInputError("search across sessions is not available here");
568
+ if (current && !tree.currentId)
569
+ throw new ToolInputError("this session's tree is not available here");
570
+ const scope = current ? await tree.currentId(true) : named;
571
+ return finder.searchSessions(args, scope);
572
+ }
573
+ /** `{tree, id}` from a node address, or from tree (optional) and the numeric field. */
574
+ function nodeTarget(args, field) {
575
+ if (args.node !== undefined && args.node !== null && args.node !== "") {
576
+ const { tree, id } = parseNodeAddress(args.node);
577
+ return { tree: checkTreeId(tree), id };
578
+ }
579
+ return { tree: treeArgument(args.tree), id: Number(args[field]) };
580
+ }
581
+ function treeArgument(tree) {
582
+ return tree === CURRENT_TREE ? undefined : checkTreeId(tree);
583
+ }
584
+ /** read_lines by leaf address (its range unless block/start/end are given), by range, or the tail. */
585
+ async function readLinesTool(args, tree) {
586
+ if (args.tail === true) {
587
+ if (!tree.tail)
588
+ throw new ToolInputError("read_lines: the un-indexed tail is not available here");
589
+ const position = (v) => (v === undefined || v === null ? undefined : Number(v));
590
+ const [start, end] = [position(args.start), position(args.end)];
591
+ if ([start, end].some((v) => v !== undefined && !Number.isInteger(v))) {
592
+ throw new ToolInputError("read_lines: with tail, start and end are message positions (integers)");
593
+ }
594
+ return tree.tail(treeArgument(args.tree), start, end);
595
+ }
596
+ if (args.node === undefined || args.node === null || args.node === "") {
597
+ const target = treeArgument(args.tree);
598
+ return formatLines(await tree.get(target), Number(args.block), Number(args.start), Number(args.end), target);
599
+ }
600
+ const { tree: ref, id } = nodeTarget(args, "id");
601
+ const index = await tree.get(ref);
602
+ const node = index.nodes.get(id);
603
+ if (!node)
604
+ throw new ToolInputError(`read_lines: no node ${id} in tree ${ref}`);
605
+ if (!index.isLeaf(node)) {
606
+ throw new ToolInputError(`read_lines: node ${ref}#${id} is a branch; read_node {"node": "${ref}#${id}"} lists the leaves under it`);
607
+ }
608
+ const [block, start, end] = node.l;
609
+ const pick = (v, fallback) => (v === undefined || v === null ? fallback : Number(v));
610
+ return formatLines(index, pick(args.block, block), pick(args.start, start), pick(args.end, end), ref);
611
+ }
612
+ /** Serve MCP over this process's stdin/stdout until stdin closes. */
613
+ export function runMemtreeMcpServer(env = process.env) {
614
+ const deps = {
615
+ proxyUrl: env.CCC_MEMTREE_PROXY || env.ANTHROPIC_BASE_URL,
616
+ sessionId: env.CLAUDE_CODE_SESSION_ID || undefined,
617
+ };
618
+ const tree = new CurrentTree(deps);
619
+ const finder = new SessionFinder(deps);
620
+ let buffer = "";
621
+ // Replies go out in arrival order even though tool calls are async.
622
+ let queue = Promise.resolve();
623
+ const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
624
+ process.stdin.setEncoding("utf-8");
625
+ process.stdin.on("data", (chunk) => {
626
+ buffer += chunk;
627
+ let newline;
628
+ while ((newline = buffer.indexOf("\n")) !== -1) {
629
+ const line = buffer.slice(0, newline).trim();
630
+ buffer = buffer.slice(newline + 1);
631
+ if (!line)
632
+ continue;
633
+ let message;
634
+ try {
635
+ message = JSON.parse(line);
636
+ }
637
+ catch {
638
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
639
+ continue;
640
+ }
641
+ queue = queue.then(async () => {
642
+ const response = await handleMcpMessage(message, tree, finder).catch((err) => ({
643
+ jsonrpc: "2.0",
644
+ id: isRpcRequest(message) ? message.id ?? null : null,
645
+ error: { code: -32603, message: String(err) },
646
+ }));
647
+ if (response)
648
+ send(response);
649
+ });
650
+ }
651
+ });
652
+ process.stdin.on("end", () => {
653
+ void queue.then(() => process.exit(0));
654
+ });
655
+ }
656
+ function isMainModule() {
657
+ try {
658
+ const self = fs.realpathSync(fileURLToPath(import.meta.url));
659
+ return !!process.argv[1] && fs.realpathSync(process.argv[1]) === self;
660
+ }
661
+ catch {
662
+ return false;
663
+ }
664
+ }
665
+ if (isMainModule())
666
+ runMemtreeMcpServer();
667
+ //# sourceMappingURL=memtree-mcp.js.map