claude-code-infinite 1.0.33 → 1.0.35
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.
- package/README.md +60 -2
- package/dist/cli-main.js +24 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/cli.js +23 -10
- package/dist/cli.js.map +1 -1
- package/dist/hooks.d.ts +20 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +34 -4
- package/dist/hooks.js.map +1 -1
- package/dist/host-session.d.ts +12 -0
- package/dist/host-session.d.ts.map +1 -0
- package/dist/host-session.js +12 -0
- package/dist/host-session.js.map +1 -0
- package/dist/image-refs.d.ts +24 -2
- package/dist/image-refs.d.ts.map +1 -1
- package/dist/image-refs.js +67 -20
- package/dist/image-refs.js.map +1 -1
- package/dist/index-wait-413.d.ts +26 -0
- package/dist/index-wait-413.d.ts.map +1 -0
- package/dist/index-wait-413.js +113 -0
- package/dist/index-wait-413.js.map +1 -0
- package/dist/memtree-finder.d.ts +70 -6
- package/dist/memtree-finder.d.ts.map +1 -1
- package/dist/memtree-finder.js +150 -20
- package/dist/memtree-finder.js.map +1 -1
- package/dist/memtree-handover.d.ts +13 -0
- package/dist/memtree-handover.d.ts.map +1 -0
- package/dist/memtree-handover.js +231 -0
- package/dist/memtree-handover.js.map +1 -0
- package/dist/memtree-mcp-config.d.ts +2 -2
- package/dist/memtree-mcp-config.d.ts.map +1 -1
- package/dist/memtree-mcp-config.js +8 -5
- package/dist/memtree-mcp-config.js.map +1 -1
- package/dist/memtree-mcp.d.ts +182 -2
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +357 -38
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree-newest.d.ts +31 -5
- package/dist/memtree-newest.d.ts.map +1 -1
- package/dist/memtree-newest.js +65 -13
- package/dist/memtree-newest.js.map +1 -1
- package/dist/memtree-times.d.ts +33 -0
- package/dist/memtree-times.d.ts.map +1 -1
- package/dist/memtree-times.js +77 -0
- package/dist/memtree-times.js.map +1 -1
- package/dist/memtree-tools.d.ts +19 -5
- package/dist/memtree-tools.d.ts.map +1 -1
- package/dist/memtree-tools.js +97 -23
- package/dist/memtree-tools.js.map +1 -1
- package/dist/memtree.d.ts +33 -0
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +92 -4
- package/dist/memtree.js.map +1 -1
- package/dist/network-failure.d.ts +28 -0
- package/dist/network-failure.d.ts.map +1 -0
- package/dist/network-failure.js +67 -0
- package/dist/network-failure.js.map +1 -0
- package/dist/prefix-store.d.ts +4 -0
- package/dist/prefix-store.d.ts.map +1 -1
- package/dist/prefix-store.js +8 -4
- package/dist/prefix-store.js.map +1 -1
- package/dist/project-trees.d.ts +15 -7
- package/dist/project-trees.d.ts.map +1 -1
- package/dist/project-trees.js +20 -9
- package/dist/project-trees.js.map +1 -1
- package/dist/proxy.d.ts +85 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +552 -44
- package/dist/proxy.js.map +1 -1
- package/dist/refusal-reason.d.ts +69 -0
- package/dist/refusal-reason.d.ts.map +1 -0
- package/dist/refusal-reason.js +131 -0
- package/dist/refusal-reason.js.map +1 -0
- package/dist/reqlog.d.ts +43 -0
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/resume-guidance.d.ts +9 -3
- package/dist/resume-guidance.d.ts.map +1 -1
- package/dist/resume-guidance.js +9 -3
- package/dist/resume-guidance.js.map +1 -1
- package/dist/route-fallback.d.ts +79 -11
- package/dist/route-fallback.d.ts.map +1 -1
- package/dist/route-fallback.js +162 -38
- package/dist/route-fallback.js.map +1 -1
- package/dist/route-identity.d.ts +6 -2
- package/dist/route-identity.d.ts.map +1 -1
- package/dist/route-identity.js +209 -16
- package/dist/route-identity.js.map +1 -1
- package/dist/slow-upload.d.ts +40 -0
- package/dist/slow-upload.d.ts.map +1 -0
- package/dist/slow-upload.js +69 -0
- package/dist/slow-upload.js.map +1 -0
- package/dist/status-link.d.ts +18 -0
- package/dist/status-link.d.ts.map +1 -0
- package/dist/status-link.js +103 -0
- package/dist/status-link.js.map +1 -0
- package/dist/statusline.d.ts +51 -0
- package/dist/statusline.d.ts.map +1 -0
- package/dist/statusline.js +379 -0
- package/dist/statusline.js.map +1 -0
- package/dist/subagent-hint.d.ts +28 -0
- package/dist/subagent-hint.d.ts.map +1 -1
- package/dist/subagent-hint.js +37 -0
- package/dist/subagent-hint.js.map +1 -1
- package/package.json +1 -1
package/dist/memtree-mcp.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* tree the current request was compressed against. The session id is the
|
|
19
19
|
* `CLAUDE_CODE_SESSION_ID` Claude Code sets for MCP servers. Reads of this
|
|
20
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
|
|
22
|
-
* restart this server
|
|
21
|
+
* and the page; missing or mismatched identity fails closed. Claude Code does not
|
|
22
|
+
* restart this server when its process switches sessions, so each tool call first
|
|
23
|
+
* asks the proxy which session that process is on (`refreshHostSession`). The page JSON
|
|
23
24
|
* is fetched through the proxy's key-free `/memtree/<id>.json` relay and cached
|
|
24
25
|
* once complete.
|
|
25
26
|
*
|
|
@@ -32,11 +33,14 @@
|
|
|
32
33
|
* and falls back to searching the page JSON locally on a server without that
|
|
33
34
|
* endpoint.
|
|
34
35
|
*/
|
|
36
|
+
import { HOST_TOKEN_ENV, HOST_TOKEN_HEADER } from "./host-session.js";
|
|
35
37
|
import fs from "node:fs";
|
|
36
38
|
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";
|
|
39
|
+
import { FINDER_SEARCH_DEFAULT_LIMIT, FINDER_SEARCH_MAX_LIMIT, formatSearchResults, searchMode, formatSessions, RECENT_SEARCH_WINDOWS_DAYS, recentSearchArgs, recentSearchFailedNote, recentSearchNote, searchQuery, SESSIONS_DEFAULT_LIMIT, SESSIONS_MAX_LIMIT, sessionsQuery, } from "./memtree-finder.js";
|
|
38
40
|
import { formatLines, formatNode, formatSearch, formatSearchHits, formatTail, MemtreeIndex, parseNodeAddress, serverSearchHits, serverTailHits, READ_LINES_MAX_CHARS, READ_LINES_MAX_LINES, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT, ToolInputError, } from "./memtree-tools.js";
|
|
39
41
|
import { CLIENT_VERSION } from "./memtree.js";
|
|
42
|
+
import { formatHandover, formatUserMessages, HANDOVER_DEFAULT_DAYS, HANDOVER_MAX_DAYS, handoverQuery, USER_MESSAGES_MAX_LIMIT, userMessagesQuery, } from "./memtree-handover.js";
|
|
43
|
+
import { formatTime } from "./memtree-times.js";
|
|
40
44
|
export const MEMTREE_MCP_SERVER_NAME = "memtree";
|
|
41
45
|
/**
|
|
42
46
|
* The version of the ccc whose proxy this server talks to, set in the config
|
|
@@ -54,17 +58,71 @@ export function versionMismatchNote(proxyVersion, ownVersion = CLIENT_VERSION) {
|
|
|
54
58
|
const safe = proxyVersion.replace(/[^0-9A-Za-z.+-]/g, "").slice(0, 40);
|
|
55
59
|
return `(These MemTree tools are ccc ${ownVersion} but this session runs ccc ${safe}: ccc was updated during the session. Restart ccc to use one version.)`;
|
|
56
60
|
}
|
|
61
|
+
/** How often a tool call may re-read the installed package.json (StaleToolsNotice). */
|
|
62
|
+
const STALE_CHECK_INTERVAL_MS = 60_000;
|
|
63
|
+
/**
|
|
64
|
+
* Says once per session, after a tool's answer, that ccc was updated on disk since
|
|
65
|
+
* this server started: Claude Code keeps an MCP server for the whole session, so
|
|
66
|
+
* the tools stay the version it loaded (an older one may lack message times) until
|
|
67
|
+
* ccc restarts. Silent when the versions match or package.json is unreadable.
|
|
68
|
+
*/
|
|
69
|
+
export class StaleToolsNotice {
|
|
70
|
+
readInstalled;
|
|
71
|
+
own;
|
|
72
|
+
now;
|
|
73
|
+
startedAt;
|
|
74
|
+
said = false;
|
|
75
|
+
checkedAt = -Infinity;
|
|
76
|
+
constructor(readInstalled = installedVersion, own = CLIENT_VERSION, now = Date.now, startedAt = new Date()) {
|
|
77
|
+
this.readInstalled = readInstalled;
|
|
78
|
+
this.own = own;
|
|
79
|
+
this.now = now;
|
|
80
|
+
this.startedAt = startedAt;
|
|
81
|
+
}
|
|
82
|
+
next() {
|
|
83
|
+
if (this.said || this.now() - this.checkedAt < STALE_CHECK_INTERVAL_MS)
|
|
84
|
+
return undefined;
|
|
85
|
+
this.checkedAt = this.now();
|
|
86
|
+
const installed = this.readInstalled();
|
|
87
|
+
if (!installed || installed === this.own)
|
|
88
|
+
return undefined;
|
|
89
|
+
this.said = true;
|
|
90
|
+
return staleToolsNote(installed, this.own, this.startedAt);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function staleToolsNote(installed, own, startedAt) {
|
|
94
|
+
const safe = installed.replace(/[^0-9A-Za-z.+-]/g, "").slice(0, 40);
|
|
95
|
+
const loaded = formatTime(startedAt.toISOString());
|
|
96
|
+
return `(These MemTree tools are ccc ${own}${loaded ? `, loaded ${loaded}` : ""}; ccc ${safe} is installed: restart ccc to use its newer MemTree tools.)`;
|
|
97
|
+
}
|
|
98
|
+
/** The version in this install's package.json now (ccc may have been updated since it started). */
|
|
99
|
+
export function installedVersion() {
|
|
100
|
+
try {
|
|
101
|
+
const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
102
|
+
return typeof version === "string" ? version : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
57
108
|
/** Sent to MemTree as `x-memtree-tools` when this server is configured. */
|
|
58
109
|
export const MEMTREE_TOOL_NAMES = [
|
|
59
110
|
"search",
|
|
60
111
|
"read_node",
|
|
61
112
|
"read_lines",
|
|
62
113
|
"list",
|
|
114
|
+
"user_messages",
|
|
115
|
+
"handover",
|
|
63
116
|
];
|
|
64
117
|
/** `tree` value naming this session's own current tree. */
|
|
65
118
|
export const CURRENT_TREE = "current";
|
|
66
119
|
const LATEST_PROTOCOL_VERSION = "2025-06-18";
|
|
67
120
|
const FETCH_TIMEOUT_MS = 60_000;
|
|
121
|
+
/**
|
|
122
|
+
* A handover pack's first build runs a model call on the server (up to 90 s, plus loading
|
|
123
|
+
* the tree): wait longer for it. Past this, the server still finishes and caches the pack.
|
|
124
|
+
*/
|
|
125
|
+
const HANDOVER_TIMEOUT_MS = 150_000;
|
|
68
126
|
const PREFIX_RETRY_INTERVAL_MS = 45_000;
|
|
69
127
|
/**
|
|
70
128
|
* The MCP server's instructions, which Claude Code places in the system prompt
|
|
@@ -92,16 +150,20 @@ export const MEMTREE_MCP_INSTRUCTIONS = "This session runs in Claude Code Infini
|
|
|
92
150
|
"and failed), search MemTree before re-deriving it from the code or guessing. The code shows what exists; the " +
|
|
93
151
|
"memory shows what was decided and why. Read the exact lines (read_lines) when precision matters.\n\n" +
|
|
94
152
|
"search with \"tree\": \"current\" looks in this session's MemTree; without tree it searches all of the user's " +
|
|
95
|
-
"sessions, and list shows those " +
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
153
|
+
"ccc sessions on any machine (not Codex or plain claude sessions), and list shows those sessions by time and " +
|
|
154
|
+
"project. Text mode (default) matches exact words, ids and paths; vector mode matches meaning. A search costs " +
|
|
155
|
+
"$0.0005 either way: its first page is charged even when cached; cursor continuation is free, even if " +
|
|
156
|
+
"re-embedded, as is a text search of one tree. 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: " +
|
|
99
157
|
"read_node {\"node\": address} opens it with its children, and the path's addresses lead to its parent and " +
|
|
100
158
|
"siblings; read_lines {\"node\": address} reads a leaf's exact lines. Cursors page over live results, not a " +
|
|
101
159
|
"frozen snapshot. A tree trails its conversation: the newest messages (often how a session ended) are in no tree " +
|
|
102
160
|
"yet. A text search of one tree also matches them (listed after the tree's hits), list shows how each session " +
|
|
103
161
|
"ended, and read_lines {\"tail\": true} reads them (with tree for another session's page; large tool calls are " +
|
|
104
|
-
"shortened, \"full\": true reads them whole)
|
|
162
|
+
"shortened, \"full\": true reads them whole).\n\n" +
|
|
163
|
+
"To take over another session's work, call handover with its session id first; user_messages lists only the " +
|
|
164
|
+
"person's own messages over a time range.\n\n" +
|
|
165
|
+
"A hand-off or kickoff prompt you write for another agent or machine names this session's id and MemTree link " +
|
|
166
|
+
"(list marks this session), so that agent can read this work with list, search and read_node.";
|
|
105
167
|
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.";
|
|
106
168
|
/**
|
|
107
169
|
* Claude Code's per-tool key (2.1.296): the tool is in the prompt from the
|
|
@@ -123,13 +185,14 @@ export const MEMTREE_TOOLS = [
|
|
|
123
185
|
name: "search",
|
|
124
186
|
description: "Find what this conversation doesn't show: exact details from earlier in this session that the memory only " +
|
|
125
187
|
"summarizes, or earlier work from the user's other sessions. Search one tree (tree, \"current\" for this session) " +
|
|
126
|
-
"or, without tree, all of the user's sessions
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
188
|
+
"or, without tree, all of the user's ccc sessions on any machine (not Codex or plain claude sessions). " +
|
|
189
|
+
"Use it before re-deriving or guessing something from earlier work. " +
|
|
190
|
+
"mode \"text\" (default) matches words, ids, paths and errors (in one tree: node summaries and transcript " +
|
|
191
|
+
"lines); mode \"vector\" matches meaning, ranked per embedding model. A search costs $0.0005 either way: its " +
|
|
192
|
+
"first page is charged even when cached; cursor continuation and a text search of one tree are free. Cursors page over live results, not a frozen snapshot. " +
|
|
130
193
|
"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. " +
|
|
131
194
|
"A text search of one tree also lists matching newest messages no tree covers yet (read them with read_lines tail). " +
|
|
132
|
-
"Results show when the messages were written
|
|
195
|
+
"Results show when the messages were written, each matched line its own message's time where recorded. " +
|
|
133
196
|
// Live on every request, unlike the server instructions and Claude Code's
|
|
134
197
|
// deferred-tools list, which a resumed session keeps from its first day.
|
|
135
198
|
MEMTREE_LOADED_NOTE,
|
|
@@ -138,7 +201,7 @@ export const MEMTREE_TOOLS = [
|
|
|
138
201
|
properties: {
|
|
139
202
|
query: { type: "string", description: "Distinctive words (text; \"quoted phrase\", or, -word across sessions) or a question (vector)." },
|
|
140
203
|
tree: { type: "string", description: `Optional. ${TREE_DESC} Omit to search all sessions.` },
|
|
141
|
-
mode: { type: "string", enum: ["text", "vector"], description: "text (default, exact words
|
|
204
|
+
mode: { type: "string", enum: ["text", "vector"], description: "text (default, exact words) or vector (meaning); first page charged, except text in one tree." },
|
|
142
205
|
project: { type: "string", description: "Without tree: only sessions in this working directory or git repository (owner/repo, or just repo)." },
|
|
143
206
|
since: { type: "string", description: "Without tree: only requests at or after this ISO 8601 time or date." },
|
|
144
207
|
until: { type: "string", description: "Without tree: only requests before this ISO 8601 time or date." },
|
|
@@ -152,7 +215,7 @@ export const MEMTREE_TOOLS = [
|
|
|
152
215
|
name: "read_node",
|
|
153
216
|
description: "Read one MemTree node: its summary, its path from the root (ancestor summaries), its children (id, summary, leaf or branch) and, " +
|
|
154
217
|
"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. " +
|
|
155
|
-
"Results show when the messages were written (
|
|
218
|
+
"Results show when the messages were written (a leaf: its own messages, where recorded).",
|
|
156
219
|
inputSchema: {
|
|
157
220
|
type: "object",
|
|
158
221
|
properties: {
|
|
@@ -169,7 +232,7 @@ export const MEMTREE_TOOLS = [
|
|
|
169
232
|
"With tail: true, read the messages after the tree that no tree covers yet (the end of a session usually is there): " +
|
|
170
233
|
"the newest ones, or start/end message positions; add tree for another session's page. Tool calls and results over " +
|
|
171
234
|
"2,000 characters or 40 lines come shortened to their first and last lines; add full: true to read them whole. " +
|
|
172
|
-
"Results show when the messages were written
|
|
235
|
+
"Results show when the messages were written: each message header its own time, where recorded.",
|
|
173
236
|
inputSchema: {
|
|
174
237
|
type: "object",
|
|
175
238
|
properties: {
|
|
@@ -185,7 +248,8 @@ export const MEMTREE_TOOLS = [
|
|
|
185
248
|
},
|
|
186
249
|
{
|
|
187
250
|
name: "list",
|
|
188
|
-
description: "List the user's own MemTree sessions
|
|
251
|
+
description: "List the user's own MemTree sessions, most recently active first: every ccc session on any machine using this " +
|
|
252
|
+
"account (this one marked; Codex and plain claude sessions are not included). Each: title (the tree's root summary), " +
|
|
189
253
|
"first message, how it ended (last user message, the tools after it and the last reply, with the time), times, " +
|
|
190
254
|
"project (directory, git repo, branch, commit), models, request count and the latest tree's reference. " +
|
|
191
255
|
"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>, ...}.",
|
|
@@ -201,18 +265,61 @@ export const MEMTREE_TOOLS = [
|
|
|
201
265
|
},
|
|
202
266
|
},
|
|
203
267
|
},
|
|
268
|
+
{
|
|
269
|
+
name: "user_messages",
|
|
270
|
+
description: "The person's own messages in a session over a time range: what they typed, commands they ran, words sent while " +
|
|
271
|
+
"the agent worked, and board messages. Tool results, reminders, prompts the agent scheduled and other agents' " +
|
|
272
|
+
"messages are left out. Use it to learn what the user asked for, decided and corrected. Covers the tree and its " +
|
|
273
|
+
"newest messages no tree covers yet. Returns the newest page, oldest first; cursor lists older ones. Each " +
|
|
274
|
+
"message says where read_lines shows its context. Results show when each message was written.",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
tree: { type: "string", description: `${TREE_DESC} A session id works too. Omit for this session.` },
|
|
279
|
+
since: { type: "string", description: "Only messages at or after this ISO 8601 time or date (UTC unless an offset is given)." },
|
|
280
|
+
until: { type: "string", description: "Only messages before this ISO 8601 time or date." },
|
|
281
|
+
query: { type: "string", description: "Only messages holding every one of these words." },
|
|
282
|
+
cursor: { type: "string", description: "next cursor from a previous call: older messages (same other arguments)." },
|
|
283
|
+
limit: { type: "number", description: `Messages per page (default 50, at most ${USER_MESSAGES_MAX_LIMIT}).` },
|
|
284
|
+
full: { type: "boolean", description: "Long messages and pastes whole (default: shortened)." },
|
|
285
|
+
context: { type: "boolean", description: "Also the agent's last words before each message (what it answered)." },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "handover",
|
|
291
|
+
description: "A handover pack for taking over or continuing a session's work: the person's messages over its last days (each " +
|
|
292
|
+
"with the agent's words it answered), how the session ended, and a model's reading of their goals, standing " +
|
|
293
|
+
"instructions, decisions with reasons and open questions, each citing its messages. Call it first when you take " +
|
|
294
|
+
"over another session (tree: its session id or tree). The model's lists can be wrong: check one against the " +
|
|
295
|
+
"messages it cites before acting on it. A first call for a session can take up to a minute.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
tree: { type: "string", description: `${TREE_DESC} A session id works too. Omit for this session.` },
|
|
300
|
+
days: { type: "number", description: `Days before the session's newest message to cover (default ${HANDOVER_DEFAULT_DAYS}, at most ${HANDOVER_MAX_DAYS}).` },
|
|
301
|
+
since: { type: "string", description: "Instead of days: messages at or after this ISO 8601 time or date." },
|
|
302
|
+
until: { type: "string", description: "Only messages before this ISO 8601 time or date." },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
204
306
|
].map((tool) => ({ ...tool, _meta: MEMTREE_TOOL_META }));
|
|
205
307
|
/** A request id in either spelling (UUID or the short leading-hex form). */
|
|
206
308
|
const TREE_ID_RE = /^[A-Za-z0-9-]{1,64}$/;
|
|
309
|
+
/** Pages of `list` results a session-id-as-tree lookup reads at most. */
|
|
310
|
+
const SESSION_TREE_MAX_PAGES = 3;
|
|
311
|
+
/** How long a session-id-as-tree answer is reused (its newest tree can change). */
|
|
312
|
+
const SESSION_TREE_TTL_MS = 60_000;
|
|
313
|
+
const SESSION_TREES_CACHED = 64;
|
|
207
314
|
/** Other sessions' pages kept in memory at once (each can be several MB). */
|
|
208
315
|
const OTHER_TREES_CACHED = 4;
|
|
209
316
|
/** GET JSON from the loopback proxy; never throws. */
|
|
210
|
-
async function proxyGetJson(fetchImpl, url, headers = {}) {
|
|
317
|
+
async function proxyGetJson(fetchImpl, url, headers = {}, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
211
318
|
let response;
|
|
212
319
|
try {
|
|
213
320
|
response = await fetchImpl(url, {
|
|
214
321
|
headers: { accept: "application/json", ...headers },
|
|
215
|
-
signal: AbortSignal.timeout(
|
|
322
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
216
323
|
});
|
|
217
324
|
}
|
|
218
325
|
catch (err) {
|
|
@@ -238,6 +345,41 @@ async function proxyGetJson(fetchImpl, url, headers = {}) {
|
|
|
238
345
|
return { ok: false, status: response.status, error: "response was not JSON" };
|
|
239
346
|
}
|
|
240
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Sent with each tool's proxy reads: which tool (`page` is a whole tree's `.json`) and
|
|
350
|
+
* the calling session, which the proxy passes on so the server's one content-free log
|
|
351
|
+
* line per call can say which tool read what (polychat memtree_tool_log.py). Not to be
|
|
352
|
+
* confused with `x-memtree-tools`, the list of configured tools sent on compress calls.
|
|
353
|
+
*/
|
|
354
|
+
export const MEMTREE_TOOL_HEADER = "x-memtree-tool";
|
|
355
|
+
export function toolHeaders(tool, deps) {
|
|
356
|
+
const headers = { [MEMTREE_TOOL_HEADER]: tool };
|
|
357
|
+
if (deps.sessionId)
|
|
358
|
+
headers["x-claude-code-session-id"] = deps.sessionId;
|
|
359
|
+
return headers;
|
|
360
|
+
}
|
|
361
|
+
/** How long a tool call waits to learn its process's session before using its own id. */
|
|
362
|
+
const HOST_SESSION_TIMEOUT_MS = 2_000;
|
|
363
|
+
/**
|
|
364
|
+
* Point `deps.sessionId` at the session this server's Claude Code process is on now.
|
|
365
|
+
* Claude Code starts the server with the session id it had at launch and does not
|
|
366
|
+
* restart it when the process moves to another session (a resume through the picker,
|
|
367
|
+
* /resume, /clear), so that id can name a session with no page ("no MemTree yet",
|
|
368
|
+
* 2026-11-09). The proxy of the ccc that launched this Claude Code knows the process's
|
|
369
|
+
* session from its SessionStart hook and answers only that process (`hostPid`: this
|
|
370
|
+
* server's parent, with the token from ccc's MCP config). Without a token (an older
|
|
371
|
+
* ccc's config: its proxy has no such route) nothing is asked; any other answer
|
|
372
|
+
* (another process, no session start seen) leaves the id as it is.
|
|
373
|
+
*/
|
|
374
|
+
export async function refreshHostSession(deps, hostPid = process.ppid) {
|
|
375
|
+
const base = deps.proxyUrl?.replace(/\/$/, "");
|
|
376
|
+
if (!base || !deps.hostToken)
|
|
377
|
+
return;
|
|
378
|
+
const answer = await proxyGetJson(deps.fetch ?? fetch, `${base}/memtree/host-session?host_pid=${hostPid}`, { [HOST_TOKEN_HEADER]: deps.hostToken }, HOST_SESSION_TIMEOUT_MS);
|
|
379
|
+
const sessionId = answer.ok ? answer.body?.session_id : undefined;
|
|
380
|
+
if (typeof sessionId === "string" && sessionId)
|
|
381
|
+
deps.sessionId = sessionId;
|
|
382
|
+
}
|
|
241
383
|
function proxyBase(deps) {
|
|
242
384
|
const base = deps.proxyUrl?.replace(/\/$/, "");
|
|
243
385
|
if (!base) {
|
|
@@ -260,6 +402,8 @@ function checkTreeId(tree) {
|
|
|
260
402
|
export class CurrentTree {
|
|
261
403
|
deps;
|
|
262
404
|
cached;
|
|
405
|
+
/** The session `cached` was read for: a page is never reused across sessions. */
|
|
406
|
+
cachedSession;
|
|
263
407
|
/** Temporary prefix pages: when each may be fetched again. */
|
|
264
408
|
prefixRetryAt = new Map();
|
|
265
409
|
/** The last fetched temporary prefix per page, served during its cooldown. */
|
|
@@ -267,6 +411,8 @@ export class CurrentTree {
|
|
|
267
411
|
inFlight;
|
|
268
412
|
/** Other trees, least recently used first. */
|
|
269
413
|
others = new Map();
|
|
414
|
+
/** Pages whose per-message times were incomplete: when to refetch them (timesDue). */
|
|
415
|
+
timesRetryAt = new WeakMap();
|
|
270
416
|
/** Set once the server answered 404 to a tree search the page JSON could answer. */
|
|
271
417
|
serverSearchMissing = false;
|
|
272
418
|
fetchImpl;
|
|
@@ -307,14 +453,14 @@ export class CurrentTree {
|
|
|
307
453
|
if (limit !== undefined && Number.isFinite(limit))
|
|
308
454
|
params.set("limit", String(limit));
|
|
309
455
|
const base = proxyBase(this.deps);
|
|
310
|
-
const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/search?${params}
|
|
456
|
+
const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/search?${params}`, toolHeaders("search", this.deps));
|
|
311
457
|
const parsed = answer.ok ? serverSearchHits(answer.body) : undefined;
|
|
312
458
|
if (parsed) {
|
|
313
459
|
const session = answer.body.session_id;
|
|
314
460
|
if (!other && session !== undefined)
|
|
315
461
|
this.checkSession(session, this.requireSessionId());
|
|
316
462
|
if (other || session !== undefined) {
|
|
317
|
-
return formatSearchHits(parsed.hits, query, parsed.terms, other, serverTailHits(answer.body));
|
|
463
|
+
return formatSearchHits(parsed.hits, query, parsed.terms, other, serverTailHits(answer.body), parsed.treeTimes);
|
|
318
464
|
}
|
|
319
465
|
// Older servers omit identity: search only the session-validated page.
|
|
320
466
|
}
|
|
@@ -344,7 +490,7 @@ export class CurrentTree {
|
|
|
344
490
|
params.set("full", "true");
|
|
345
491
|
const query = params.toString() ? `?${params}` : "";
|
|
346
492
|
const base = proxyBase(this.deps);
|
|
347
|
-
const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/messages${query}
|
|
493
|
+
const answer = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}/messages${query}`, toolHeaders("read_lines", this.deps));
|
|
348
494
|
if (answer.status === 404) {
|
|
349
495
|
return "No un-indexed messages after this tree: everything the server has recorded is in the tree (or the server predates tails).";
|
|
350
496
|
}
|
|
@@ -357,20 +503,70 @@ export class CurrentTree {
|
|
|
357
503
|
this.checkSession(answer.body.session_id, this.requireSessionId());
|
|
358
504
|
return formatTail(answer.body, other);
|
|
359
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* `/memtree/<id>/<route>`, never cached (the newest messages grow). This session's
|
|
508
|
+
* tree must be this session's (as tail); a 404 without the server's own detail means
|
|
509
|
+
* the server or this ccc's proxy predates the route.
|
|
510
|
+
*/
|
|
511
|
+
async serverRead(route, tree, params) {
|
|
512
|
+
const other = checkTreeId(tree);
|
|
513
|
+
const id = other ?? (await this.currentId());
|
|
514
|
+
const query = params.toString() ? `?${params}` : "";
|
|
515
|
+
const answer = await proxyGetJson(this.fetchImpl, `${proxyBase(this.deps)}/memtree/${encodeURIComponent(id)}/${route}${query}`, toolHeaders(route, this.deps), route === "handover" ? HANDOVER_TIMEOUT_MS : FETCH_TIMEOUT_MS);
|
|
516
|
+
if (answer.status === 202)
|
|
517
|
+
throw new ToolInputError("That MemTree is still being built; try again in a minute.");
|
|
518
|
+
if (answer.status === 0 && route === "handover" && /timeout|abort/i.test(answer.error ?? "")) {
|
|
519
|
+
throw new ToolInputError("handover: the pack is still being prepared on the server; call handover again in a minute.");
|
|
520
|
+
}
|
|
521
|
+
if (answer.status === 404 && /^HTTP 404 Not Found$/.test(answer.error ?? "")) {
|
|
522
|
+
throw new ToolInputError(`${route}: this MemTree server (or this ccc) does not have it yet; update ccc or try later.`);
|
|
523
|
+
}
|
|
524
|
+
if (!answer.ok)
|
|
525
|
+
throw new ToolInputError(`${route}: ${(answer.error ?? "").replace(/^HTTP (400|404) /, "")}`);
|
|
526
|
+
if (!other)
|
|
527
|
+
this.checkSession(answer.body.session_id, this.requireSessionId());
|
|
528
|
+
return answer.body;
|
|
529
|
+
}
|
|
360
530
|
async loadCurrent() {
|
|
361
531
|
const id = await this.currentId();
|
|
362
532
|
const sessionId = this.requireSessionId();
|
|
363
|
-
if (this.cached?.id === id)
|
|
533
|
+
if (this.cached?.id === id && !this.timesDue(this.cached.index))
|
|
364
534
|
return this.cached.index;
|
|
365
|
-
const
|
|
535
|
+
const stale = this.cached?.id === id ? this.cached.index : undefined;
|
|
536
|
+
const index = await this.refreshed(stale, () => this.loadPage(id, "this session", sessionId));
|
|
366
537
|
if (!isTemporaryPrefix(index, id))
|
|
367
538
|
this.cached = { id, index };
|
|
368
539
|
return index;
|
|
369
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Whether a cached page's per-message times were incomplete (`line_times_incomplete`:
|
|
543
|
+
* the server's times lookup was slow) and are due a refetch. The page itself is
|
|
544
|
+
* readable meanwhile, so it stays cached; only its times are retried.
|
|
545
|
+
*/
|
|
546
|
+
timesDue(index) {
|
|
547
|
+
const at = this.timesRetryAt.get(index);
|
|
548
|
+
return at !== undefined && this.now() >= at;
|
|
549
|
+
}
|
|
550
|
+
/** `load()`, or the readable `stale` copy when that fails (its times retried later). */
|
|
551
|
+
async refreshed(stale, load) {
|
|
552
|
+
if (!stale)
|
|
553
|
+
return load();
|
|
554
|
+
try {
|
|
555
|
+
return await load();
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
this.timesRetryAt.set(stale, this.now() + PREFIX_RETRY_INTERVAL_MS);
|
|
559
|
+
return stale;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
370
562
|
requireSessionId() {
|
|
371
563
|
const sessionId = this.deps.sessionId;
|
|
372
564
|
if (!sessionId)
|
|
373
565
|
throw new ToolInputError("MemTree unavailable: the calling session id is missing");
|
|
566
|
+
if (this.cachedSession !== sessionId) {
|
|
567
|
+
this.cached = undefined; // the process moved to another session (refreshHostSession)
|
|
568
|
+
this.cachedSession = sessionId;
|
|
569
|
+
}
|
|
374
570
|
return sessionId;
|
|
375
571
|
}
|
|
376
572
|
/** The proxy's current page for the calling session; fails closed on any other session. */
|
|
@@ -407,12 +603,13 @@ export class CurrentTree {
|
|
|
407
603
|
}
|
|
408
604
|
async getOther(id) {
|
|
409
605
|
const hit = this.others.get(id);
|
|
410
|
-
if (hit) {
|
|
606
|
+
if (hit && !this.timesDue(hit)) {
|
|
411
607
|
this.others.delete(id);
|
|
412
608
|
this.others.set(id, hit);
|
|
413
609
|
return hit;
|
|
414
610
|
}
|
|
415
|
-
const index = await this.loadPage(id, `tree ${id}`);
|
|
611
|
+
const index = await this.refreshed(hit, () => this.loadPage(id, `tree ${id}`));
|
|
612
|
+
this.others.delete(id);
|
|
416
613
|
if (!isTemporaryPrefix(index, id))
|
|
417
614
|
this.others.set(id, index);
|
|
418
615
|
while (this.others.size > OTHER_TREES_CACHED) {
|
|
@@ -445,7 +642,7 @@ export class CurrentTree {
|
|
|
445
642
|
if (retryAt !== undefined)
|
|
446
643
|
this.prefixRetryAt.set(id, now + PREFIX_RETRY_INTERVAL_MS);
|
|
447
644
|
const base = proxyBase(this.deps);
|
|
448
|
-
const page = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}.json
|
|
645
|
+
const page = await proxyGetJson(this.fetchImpl, `${base}/memtree/${encodeURIComponent(id)}.json`, toolHeaders("page", this.deps));
|
|
449
646
|
if (!page.ok)
|
|
450
647
|
throw new ToolInputError(`MemTree unavailable (page ${id}: ${page.error})`);
|
|
451
648
|
const json = page.body;
|
|
@@ -457,6 +654,9 @@ export class CurrentTree {
|
|
|
457
654
|
throw new ToolInputError("MemTree unavailable: page belongs to a different or unknown session");
|
|
458
655
|
}
|
|
459
656
|
const index = new MemtreeIndex(json);
|
|
657
|
+
// Readable now; its incomplete per-message times are refetched after the cooldown.
|
|
658
|
+
if (json.line_times_incomplete === true)
|
|
659
|
+
this.timesRetryAt.set(index, this.now() + PREFIX_RETRY_INTERVAL_MS);
|
|
460
660
|
if (isTemporaryPrefix(index, id)) {
|
|
461
661
|
this.prefixRetryAt.set(id, this.now() + PREFIX_RETRY_INTERVAL_MS);
|
|
462
662
|
this.prefixes.delete(id);
|
|
@@ -501,20 +701,83 @@ export class SessionFinder {
|
|
|
501
701
|
}
|
|
502
702
|
async listSessions(args) {
|
|
503
703
|
const body = await this.get(`/memtree/sessions${sessionsQuery(args)}`, "list");
|
|
504
|
-
return formatSessions(body, args);
|
|
704
|
+
return formatSessions(body, args, this.deps.sessionId);
|
|
705
|
+
}
|
|
706
|
+
sessionTrees = new Map();
|
|
707
|
+
/** sessionTree, remembered for SESSION_TREE_TTL_MS (hits and misses: legacy ids repeat). */
|
|
708
|
+
async sessionTree(sessionId) {
|
|
709
|
+
const known = this.sessionTrees.get(sessionId);
|
|
710
|
+
if (known && this.now() - known.at < SESSION_TREE_TTL_MS)
|
|
711
|
+
return known.ref;
|
|
712
|
+
const ref = await this.lookupSessionTree(sessionId);
|
|
713
|
+
this.sessionTrees.set(sessionId, { ref, at: this.now() });
|
|
714
|
+
while (this.sessionTrees.size > SESSION_TREES_CACHED) {
|
|
715
|
+
this.sessionTrees.delete(this.sessionTrees.keys().next().value);
|
|
716
|
+
}
|
|
717
|
+
return ref;
|
|
718
|
+
}
|
|
719
|
+
async lookupSessionTree(sessionId) {
|
|
720
|
+
// `q` also matches titles and first messages, so sessions quoting the id (a hand-off)
|
|
721
|
+
// can fill the first page: follow the cursor, a few pages at most.
|
|
722
|
+
let cursor;
|
|
723
|
+
for (let page = 0; page < SESSION_TREE_MAX_PAGES; page++) {
|
|
724
|
+
const query = sessionsQuery({ q: sessionId, limit: SESSIONS_MAX_LIMIT, cursor });
|
|
725
|
+
const answer = await this.fetch(`/memtree/sessions${query}`, "list");
|
|
726
|
+
if (!answer.ok)
|
|
727
|
+
return undefined;
|
|
728
|
+
const body = answer.body;
|
|
729
|
+
if (!Array.isArray(body?.sessions))
|
|
730
|
+
return undefined;
|
|
731
|
+
const item = body.sessions.find((s) => s?.session_id === sessionId);
|
|
732
|
+
if (item) {
|
|
733
|
+
const ref = item.latest_tree?.ref ?? item.latest_tree?.request_id;
|
|
734
|
+
return typeof ref === "string" && TREE_ID_RE.test(ref) && ref !== sessionId ? ref : undefined;
|
|
735
|
+
}
|
|
736
|
+
if (typeof body.next_cursor !== "string" || !body.next_cursor || body.next_cursor === cursor) {
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
cursor = body.next_cursor;
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
505
742
|
}
|
|
506
743
|
async searchSessions(args, tree) {
|
|
507
|
-
const
|
|
744
|
+
const first = await this.fetch(`/memtree/search${searchQuery(args, tree)}`, "search");
|
|
745
|
+
let answer = first;
|
|
746
|
+
let narrowed;
|
|
747
|
+
const tried = [];
|
|
748
|
+
for (const days of RECENT_SEARCH_WINDOWS_DAYS) {
|
|
749
|
+
const recent = recentSearchArgs(args, tree, answer.status, this.now(), days);
|
|
750
|
+
if (!recent)
|
|
751
|
+
break;
|
|
752
|
+
narrowed = { args: recent, days };
|
|
753
|
+
tried.push(days);
|
|
754
|
+
answer = await this.fetch(`/memtree/search${searchQuery(recent, tree)}`, "search");
|
|
755
|
+
}
|
|
756
|
+
// No window answered: the first refusal says how to narrow the search, plus what ccc
|
|
757
|
+
// tried and, when the last try failed otherwise, how.
|
|
758
|
+
const body = this.bodyOf(answer.ok ? answer : narrowed
|
|
759
|
+
? { ...first, error: `${first.error}${recentSearchFailedNote(tried, answer)}` } : first, "search");
|
|
508
760
|
if (tree && (!body || typeof body !== "object" || body.tree !== tree)) {
|
|
509
761
|
throw new ToolInputError("search: server did not confirm the requested tree; update the MemTree server before scoped search");
|
|
510
762
|
}
|
|
511
|
-
|
|
763
|
+
const text = formatSearchResults(body, {
|
|
764
|
+
...(narrowed?.args ?? args), tree,
|
|
765
|
+
});
|
|
766
|
+
if (!narrowed)
|
|
767
|
+
return text;
|
|
768
|
+
const pages = typeof body?.next_cursor === "string";
|
|
769
|
+
return `${recentSearchNote(narrowed.args.since, narrowed.days, pages)}\n\n${text}`;
|
|
770
|
+
}
|
|
771
|
+
now() {
|
|
772
|
+
return this.deps.now?.() ?? Date.now();
|
|
512
773
|
}
|
|
513
774
|
async get(pathAndQuery, tool) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
775
|
+
return this.bodyOf(await this.fetch(pathAndQuery, tool), tool);
|
|
776
|
+
}
|
|
777
|
+
fetch(pathAndQuery, tool) {
|
|
778
|
+
return proxyGetJson(this.fetchImpl, `${proxyBase(this.deps)}${pathAndQuery}`, toolHeaders(tool, this.deps));
|
|
779
|
+
}
|
|
780
|
+
bodyOf(answer, tool) {
|
|
518
781
|
if (answer.ok)
|
|
519
782
|
return answer.body;
|
|
520
783
|
if (answer.status === 404) {
|
|
@@ -529,7 +792,9 @@ export class SessionFinder {
|
|
|
529
792
|
*/
|
|
530
793
|
export async function handleMcpMessage(input, tree, finder,
|
|
531
794
|
/** versionMismatchNote: appended to failed tool calls. */
|
|
532
|
-
versionNote
|
|
795
|
+
versionNote,
|
|
796
|
+
/** A note to append once, to whichever tool answer comes first (StaleToolsNotice). */
|
|
797
|
+
notice) {
|
|
533
798
|
if (!isRpcRequest(input)) {
|
|
534
799
|
return { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request" } };
|
|
535
800
|
}
|
|
@@ -558,16 +823,20 @@ versionNote) {
|
|
|
558
823
|
case "tools/call": {
|
|
559
824
|
const name = message.params?.name;
|
|
560
825
|
const args = (message.params?.arguments ?? {});
|
|
826
|
+
const withNotice = (text) => {
|
|
827
|
+
const note = notice?.next();
|
|
828
|
+
return note ? `${text}\n\n${note}` : text;
|
|
829
|
+
};
|
|
561
830
|
try {
|
|
562
831
|
const text = await callTool(String(name), args, tree, finder);
|
|
563
|
-
return reply({ content: [{ type: "text", text }] });
|
|
832
|
+
return reply({ content: [{ type: "text", text: withNotice(text) }] });
|
|
564
833
|
}
|
|
565
834
|
catch (err) {
|
|
566
835
|
if (err instanceof UnknownToolError)
|
|
567
836
|
return fail(-32602, err.message);
|
|
568
837
|
const message = err instanceof Error ? err.message : String(err);
|
|
569
838
|
const text = versionNote ? `${message}\n${versionNote}` : message;
|
|
570
|
-
return reply({ content: [{ type: "text", text }], isError: true });
|
|
839
|
+
return reply({ content: [{ type: "text", text: withNotice(text) }], isError: true });
|
|
571
840
|
}
|
|
572
841
|
}
|
|
573
842
|
default:
|
|
@@ -589,6 +858,35 @@ function isRpcRequest(value) {
|
|
|
589
858
|
class UnknownToolError extends Error {
|
|
590
859
|
}
|
|
591
860
|
async function callTool(name, args, tree, finder) {
|
|
861
|
+
const session = await sessionAsTree(args.tree, finder);
|
|
862
|
+
if (session) {
|
|
863
|
+
const text = await dispatchTool(name, { ...args, tree: session.ref }, tree, finder);
|
|
864
|
+
return `(${session.id} is a session id: this reads its newest tree, ${session.ref}.)\n\n${text}`;
|
|
865
|
+
}
|
|
866
|
+
return dispatchTool(name, args, tree, finder);
|
|
867
|
+
}
|
|
868
|
+
/** A bare UUID: a session id, or a legacy request id (a ref ends in -v<N>-own/served). */
|
|
869
|
+
const BARE_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
870
|
+
/**
|
|
871
|
+
* A `tree` that is a session id, resolved to that session's newest tree. Hand-offs name
|
|
872
|
+
* session ids (the instructions ask them to), and receiving agents pass them as `tree`:
|
|
873
|
+
* the page lookup then fails with 404, or a tail reads nothing (xsession benchmark,
|
|
874
|
+
* 2026-11-06). Undefined for anything else, including a legacy request id.
|
|
875
|
+
*/
|
|
876
|
+
async function sessionAsTree(value, finder) {
|
|
877
|
+
if (typeof value !== "string" || !BARE_UUID_RE.test(value.trim()) || !finder?.sessionTree) {
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
const id = value.trim();
|
|
881
|
+
try {
|
|
882
|
+
const ref = await finder.sessionTree(id);
|
|
883
|
+
return ref ? { id, ref } : undefined;
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function dispatchTool(name, args, tree, finder) {
|
|
592
890
|
switch (name) {
|
|
593
891
|
case "search":
|
|
594
892
|
return searchTool(args, tree, finder);
|
|
@@ -602,6 +900,9 @@ async function callTool(name, args, tree, finder) {
|
|
|
602
900
|
if (!finder)
|
|
603
901
|
throw new ToolInputError("list is not available here");
|
|
604
902
|
return finder.listSessions(args);
|
|
903
|
+
case "user_messages":
|
|
904
|
+
case "handover":
|
|
905
|
+
return treeRouteTool(name, args, tree);
|
|
605
906
|
default:
|
|
606
907
|
throw new UnknownToolError(`Unknown tool: ${name}`);
|
|
607
908
|
}
|
|
@@ -642,6 +943,16 @@ function nodeTarget(args, field) {
|
|
|
642
943
|
function treeArgument(tree) {
|
|
643
944
|
return tree === CURRENT_TREE ? undefined : checkTreeId(tree);
|
|
644
945
|
}
|
|
946
|
+
/** `user_messages` / `handover`: the server's answer for the tree, formatted. */
|
|
947
|
+
async function treeRouteTool(name, args, tree) {
|
|
948
|
+
if (!tree.serverRead)
|
|
949
|
+
throw new ToolInputError(`${name} is not available here`);
|
|
950
|
+
const target = treeArgument(args.tree);
|
|
951
|
+
if (name === "user_messages") {
|
|
952
|
+
return formatUserMessages(await tree.serverRead(name, target, userMessagesQuery(args)), args, target);
|
|
953
|
+
}
|
|
954
|
+
return formatHandover(await tree.serverRead(name, target, handoverQuery(args)), target);
|
|
955
|
+
}
|
|
645
956
|
/** read_lines by leaf address (its range unless block/start/end are given), by range, or the tail. */
|
|
646
957
|
async function readLinesTool(args, tree) {
|
|
647
958
|
if (args.tail === true) {
|
|
@@ -679,10 +990,12 @@ export function runMemtreeMcpServer(env = process.env) {
|
|
|
679
990
|
const deps = {
|
|
680
991
|
proxyUrl: env.CCC_MEMTREE_PROXY || env.ANTHROPIC_BASE_URL,
|
|
681
992
|
sessionId: env.CLAUDE_CODE_SESSION_ID || undefined,
|
|
993
|
+
hostToken: env[HOST_TOKEN_ENV] || undefined,
|
|
682
994
|
};
|
|
683
995
|
const tree = new CurrentTree(deps);
|
|
684
996
|
const finder = new SessionFinder(deps);
|
|
685
997
|
const versionNote = versionMismatchNote(env[PROXY_VERSION_ENV]);
|
|
998
|
+
const notice = new StaleToolsNotice();
|
|
686
999
|
if (versionNote)
|
|
687
1000
|
process.stderr.write(`memtree MCP: ${versionNote}\n`);
|
|
688
1001
|
let buffer = "";
|
|
@@ -707,7 +1020,10 @@ export function runMemtreeMcpServer(env = process.env) {
|
|
|
707
1020
|
continue;
|
|
708
1021
|
}
|
|
709
1022
|
queue = queue.then(async () => {
|
|
710
|
-
|
|
1023
|
+
// Calls run one at a time, so each reads the session its process is on now.
|
|
1024
|
+
if (isToolCall(message))
|
|
1025
|
+
await refreshHostSession(deps);
|
|
1026
|
+
const response = await handleMcpMessage(message, tree, finder, versionNote, notice).catch((err) => ({
|
|
711
1027
|
jsonrpc: "2.0",
|
|
712
1028
|
id: isRpcRequest(message) ? message.id ?? null : null,
|
|
713
1029
|
error: { code: -32603, message: String(err) },
|
|
@@ -721,6 +1037,9 @@ export function runMemtreeMcpServer(env = process.env) {
|
|
|
721
1037
|
void queue.then(() => process.exit(0));
|
|
722
1038
|
});
|
|
723
1039
|
}
|
|
1040
|
+
function isToolCall(message) {
|
|
1041
|
+
return isRpcRequest(message) && message.method === "tools/call";
|
|
1042
|
+
}
|
|
724
1043
|
function isMainModule() {
|
|
725
1044
|
try {
|
|
726
1045
|
const self = fs.realpathSync(fileURLToPath(import.meta.url));
|