@junando/webhook 0.12.2 → 0.13.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.
- package/dist/{checksum-AM2soALR.cjs → checksum-Conn-Zv6.cjs} +473 -243
- package/dist/{dist-CbGq-c4z.cjs → dist-DV15lRHH.cjs} +1 -1
- package/dist/{es5-CBwA-Kuh.cjs → es5-BeSWht5a.cjs} +1 -1
- package/dist/{event-streams-cGG6y4x_.cjs → event-streams-D4zq5a68.cjs} +74 -49
- package/dist/handler.cjs +14098 -9591
- package/dist/node-CaH-Nh2c.cjs +39 -0
- package/dist/node-DdUjbr7d.cjs +2703 -0
- package/dist/{rolldown-runtime-CfAboj1T.cjs → rolldown-runtime-BDXX5GFZ.cjs} +1 -2
- package/dist/{sdk-a89HQ1X4.cjs → sdk-DKBOmAd5.cjs} +3409 -771
- package/package.json +8 -8
- package/dist/checksum-DYd9h1Z6.cjs +0 -7720
- package/dist/dist-cjs-mxzJ7CE9.cjs +0 -100
- package/dist/event-streams-x0B21mfk.cjs +0 -953
- package/dist/node-BYAxz9X-.cjs +0 -1186
|
@@ -0,0 +1,2703 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('./rolldown-runtime-BDXX5GFZ.cjs');
|
|
2
|
+
const require_sdk = require('./sdk-DKBOmAd5.cjs');
|
|
3
|
+
require('./node-CaH-Nh2c.cjs');
|
|
4
|
+
let node_crypto = require("node:crypto");
|
|
5
|
+
node_crypto = require_rolldown_runtime.__toESM(node_crypto, 1);
|
|
6
|
+
let node_fs = require("node:fs");
|
|
7
|
+
node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
|
|
8
|
+
let node_path = require("node:path");
|
|
9
|
+
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
10
|
+
let node_fs_promises = require("node:fs/promises");
|
|
11
|
+
node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises, 1);
|
|
12
|
+
let node_child_process = require("node:child_process");
|
|
13
|
+
node_child_process = require_rolldown_runtime.__toESM(node_child_process, 1);
|
|
14
|
+
let node_readline = require("node:readline");
|
|
15
|
+
node_readline = require_rolldown_runtime.__toESM(node_readline, 1);
|
|
16
|
+
let node_util = require("node:util");
|
|
17
|
+
node_util = require_rolldown_runtime.__toESM(node_util, 1);
|
|
18
|
+
let node_stream = require("node:stream");
|
|
19
|
+
node_stream = require_rolldown_runtime.__toESM(node_stream, 1);
|
|
20
|
+
|
|
21
|
+
//#region ../../node_modules/.pnpm/@anthropic-ai+sdk@0.124.0_zod@4.5.4/node_modules/@anthropic-ai/sdk/lib/transform-json-schema.mjs
|
|
22
|
+
const SUPPORTED_STRING_FORMATS = new Set([
|
|
23
|
+
"date-time",
|
|
24
|
+
"time",
|
|
25
|
+
"date",
|
|
26
|
+
"duration",
|
|
27
|
+
"email",
|
|
28
|
+
"hostname",
|
|
29
|
+
"uri",
|
|
30
|
+
"ipv4",
|
|
31
|
+
"ipv6",
|
|
32
|
+
"uuid"
|
|
33
|
+
]);
|
|
34
|
+
function deepClone(obj) {
|
|
35
|
+
return JSON.parse(JSON.stringify(obj));
|
|
36
|
+
}
|
|
37
|
+
function transformJSONSchema(jsonSchema) {
|
|
38
|
+
const workingCopy = deepClone(jsonSchema);
|
|
39
|
+
return _transformJSONSchema(workingCopy);
|
|
40
|
+
}
|
|
41
|
+
function _transformJSONSchema(jsonSchema) {
|
|
42
|
+
const strictSchema = {};
|
|
43
|
+
const ref = require_sdk.pop(jsonSchema, "$ref");
|
|
44
|
+
if (ref !== undefined) {
|
|
45
|
+
strictSchema["$ref"] = ref;
|
|
46
|
+
return strictSchema;
|
|
47
|
+
}
|
|
48
|
+
const defs = require_sdk.pop(jsonSchema, "$defs");
|
|
49
|
+
if (defs !== undefined) {
|
|
50
|
+
const strictDefs = {};
|
|
51
|
+
strictSchema["$defs"] = strictDefs;
|
|
52
|
+
for (const [name, defSchema] of Object.entries(defs)) {
|
|
53
|
+
strictDefs[name] = _transformJSONSchema(defSchema);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const type = require_sdk.pop(jsonSchema, "type");
|
|
57
|
+
const anyOf = require_sdk.pop(jsonSchema, "anyOf");
|
|
58
|
+
const oneOf = require_sdk.pop(jsonSchema, "oneOf");
|
|
59
|
+
const allOf = require_sdk.pop(jsonSchema, "allOf");
|
|
60
|
+
if (Array.isArray(anyOf)) {
|
|
61
|
+
strictSchema["anyOf"] = anyOf.map((variant) => _transformJSONSchema(variant));
|
|
62
|
+
} else if (Array.isArray(oneOf)) {
|
|
63
|
+
strictSchema["anyOf"] = oneOf.map((variant) => _transformJSONSchema(variant));
|
|
64
|
+
} else if (Array.isArray(allOf)) {
|
|
65
|
+
strictSchema["allOf"] = allOf.map((entry) => _transformJSONSchema(entry));
|
|
66
|
+
} else {
|
|
67
|
+
if (type === undefined) {
|
|
68
|
+
throw new Error("JSON schema must have a type defined if anyOf/oneOf/allOf are not used");
|
|
69
|
+
}
|
|
70
|
+
strictSchema["type"] = type;
|
|
71
|
+
}
|
|
72
|
+
const description = require_sdk.pop(jsonSchema, "description");
|
|
73
|
+
if (description !== undefined) {
|
|
74
|
+
strictSchema["description"] = description;
|
|
75
|
+
}
|
|
76
|
+
const title = require_sdk.pop(jsonSchema, "title");
|
|
77
|
+
if (title !== undefined) {
|
|
78
|
+
strictSchema["title"] = title;
|
|
79
|
+
}
|
|
80
|
+
if (type === "object") {
|
|
81
|
+
const properties = require_sdk.pop(jsonSchema, "properties") || {};
|
|
82
|
+
strictSchema["properties"] = Object.fromEntries(Object.entries(properties).map(([key, propSchema]) => [key, _transformJSONSchema(propSchema)]));
|
|
83
|
+
require_sdk.pop(jsonSchema, "additionalProperties");
|
|
84
|
+
strictSchema["additionalProperties"] = false;
|
|
85
|
+
const required = require_sdk.pop(jsonSchema, "required");
|
|
86
|
+
if (required !== undefined) {
|
|
87
|
+
strictSchema["required"] = required;
|
|
88
|
+
}
|
|
89
|
+
} else if (type === "string") {
|
|
90
|
+
const format = require_sdk.pop(jsonSchema, "format");
|
|
91
|
+
if (format !== undefined && SUPPORTED_STRING_FORMATS.has(format)) {
|
|
92
|
+
strictSchema["format"] = format;
|
|
93
|
+
} else if (format !== undefined) {
|
|
94
|
+
jsonSchema["format"] = format;
|
|
95
|
+
}
|
|
96
|
+
} else if (type === "array") {
|
|
97
|
+
const items = require_sdk.pop(jsonSchema, "items");
|
|
98
|
+
if (items !== undefined) {
|
|
99
|
+
strictSchema["items"] = _transformJSONSchema(items);
|
|
100
|
+
}
|
|
101
|
+
const minItems = require_sdk.pop(jsonSchema, "minItems");
|
|
102
|
+
if (minItems !== undefined && (minItems === 0 || minItems === 1)) {
|
|
103
|
+
strictSchema["minItems"] = minItems;
|
|
104
|
+
} else if (minItems !== undefined) {
|
|
105
|
+
jsonSchema["minItems"] = minItems;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (Object.keys(jsonSchema).length > 0) {
|
|
109
|
+
const existingDescription = strictSchema["description"];
|
|
110
|
+
strictSchema["description"] = (existingDescription ? existingDescription + "\n\n" : "") + "{" + Object.entries(jsonSchema).map(([key, value]) => `${key}: ${JSON.stringify(value)}`).join(", ") + "}";
|
|
111
|
+
}
|
|
112
|
+
return strictSchema;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region ../../node_modules/.pnpm/@anthropic-ai+sdk@0.124.0_zod@4.5.4/node_modules/@anthropic-ai/sdk/helpers/beta/json-schema.mjs
|
|
117
|
+
/**
|
|
118
|
+
* Creates a Tool with a provided JSON schema that can be passed
|
|
119
|
+
* to the `.toolRunner()` method. The schema is used to automatically validate
|
|
120
|
+
* the input arguments for the tool.
|
|
121
|
+
*/
|
|
122
|
+
function betaTool(options) {
|
|
123
|
+
if (options.inputSchema.type !== "object") {
|
|
124
|
+
throw new Error(`JSON schema for tool "${options.name}" must be an object, but got ${options.inputSchema.type}`);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
type: "custom",
|
|
128
|
+
name: options.name,
|
|
129
|
+
input_schema: options.inputSchema,
|
|
130
|
+
description: options.description,
|
|
131
|
+
run: options.run,
|
|
132
|
+
parse: (content) => content,
|
|
133
|
+
...options.close ? { close: options.close } : {}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Creates a JSON schema output format object from the given JSON schema.
|
|
138
|
+
* If this is passed to the `.parse()` method then the response message will contain a
|
|
139
|
+
* `.parsed_output` property that is the result of parsing the content with the given JSON schema.
|
|
140
|
+
*
|
|
141
|
+
*/
|
|
142
|
+
function betaJSONSchemaOutputFormat(jsonSchema, options) {
|
|
143
|
+
if (jsonSchema.type !== "object") {
|
|
144
|
+
throw new Error(`JSON schema for tool must be an object, but got ${jsonSchema.type}`);
|
|
145
|
+
}
|
|
146
|
+
const transform = options?.transform ?? true;
|
|
147
|
+
if (transform) {
|
|
148
|
+
jsonSchema = transformJSONSchema(jsonSchema);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
type: "json_schema",
|
|
152
|
+
schema: { ...jsonSchema },
|
|
153
|
+
parse: (content) => {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(content);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new require_sdk.AnthropicError(`Failed to parse structured output: ${error}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region ../../node_modules/.pnpm/@anthropic-ai+sdk@0.124.0_zod@4.5.4/node_modules/@anthropic-ai/sdk/tools/agent-toolset/fs-util.mjs
|
|
165
|
+
/**
|
|
166
|
+
* Shared, Node-only filesystem helpers for the agent toolset's file tools:
|
|
167
|
+
* path confinement (symlink-aware), an atomic write, and language-independent
|
|
168
|
+
* error messages. Kept out of `node.ts` so the tool implementations stay focused
|
|
169
|
+
* and these helpers can be reused by every file tool.
|
|
170
|
+
*/
|
|
171
|
+
const fs$1 = node_fs.promises;
|
|
172
|
+
/** Mode for directories the file tools create: owner-only under any umask, like the memory tool. */
|
|
173
|
+
const DIR_CREATE_MODE = 448;
|
|
174
|
+
/**
|
|
175
|
+
* Mode for files the file tools create: owner-only, so other local users can't
|
|
176
|
+
* read what an agent wrote. A file that already exists keeps its own mode.
|
|
177
|
+
*/
|
|
178
|
+
const FILE_CREATE_MODE = 384;
|
|
179
|
+
/** True when `p` is `root` itself or lexically contained within it. */
|
|
180
|
+
function isWithin(root, p) {
|
|
181
|
+
const rel = node_path.relative(root, p);
|
|
182
|
+
return rel === "" || !rel.startsWith(".." + node_path.sep) && rel !== ".." && !node_path.isAbsolute(rel);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The first entry of `roots` whose canonical form contains the
|
|
186
|
+
* already-canonical `target`, returned as configured; `undefined` when none
|
|
187
|
+
* does. Each root goes through {@link canonicalize} at check time, exactly like
|
|
188
|
+
* the workdir in {@link confineToRoot}, so granting access (`allowedRoots`)
|
|
189
|
+
* and refusing writes (`readOnlyRoots`) can never resolve the same entry two
|
|
190
|
+
* different ways.
|
|
191
|
+
*/
|
|
192
|
+
async function containingRoot(roots, target) {
|
|
193
|
+
for (const root of roots) {
|
|
194
|
+
if (isWithin(await canonicalize(node_path.resolve(root)), target)) return root;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
/** Matches Linux MAXSYMLINKS, the threshold at which `realpath` itself reports ELOOP. */
|
|
199
|
+
const MAX_SYMLINK_HOPS = 40;
|
|
200
|
+
/** The `code` of a Node system error, or `undefined` for anything else. */
|
|
201
|
+
function errnoCode(err) {
|
|
202
|
+
const code = err?.code;
|
|
203
|
+
return typeof code === "string" ? code : undefined;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Fully resolve `abs`: `realpath` the longest existing ancestor and re-append
|
|
207
|
+
* the rest, but never re-append a component that is itself a symlink — read the
|
|
208
|
+
* link and continue from its target instead. This handles paths being created
|
|
209
|
+
* (write/edit) without letting a symlink leaf (e.g. a dangling one pointing
|
|
210
|
+
* outside a confinement root) slip through unresolved.
|
|
211
|
+
*
|
|
212
|
+
* Returns a symlink-free path or throws an errno-carrying error (`ELOOP` for a
|
|
213
|
+
* cycle or more than {@link MAX_SYMLINK_HOPS} links, the `lstat`/`realpath`
|
|
214
|
+
* error for an unreadable component); it never returns `abs` unresolved. Only
|
|
215
|
+
* symlink hops count against the cap, so any depth of not-yet-existing
|
|
216
|
+
* directories still resolves.
|
|
217
|
+
*/
|
|
218
|
+
async function canonicalize(abs) {
|
|
219
|
+
const tail = [];
|
|
220
|
+
let prefix = abs;
|
|
221
|
+
let hops = 0;
|
|
222
|
+
for (;;) {
|
|
223
|
+
let real;
|
|
224
|
+
try {
|
|
225
|
+
real = await fs$1.realpath(prefix);
|
|
226
|
+
} catch (realpathErr) {
|
|
227
|
+
let isLink;
|
|
228
|
+
try {
|
|
229
|
+
isLink = (await fs$1.lstat(prefix)).isSymbolicLink();
|
|
230
|
+
} catch (lstatErr) {
|
|
231
|
+
const code = errnoCode(lstatErr);
|
|
232
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw lstatErr;
|
|
233
|
+
const parent = node_path.dirname(prefix);
|
|
234
|
+
if (parent === prefix) throw lstatErr;
|
|
235
|
+
tail.push(node_path.basename(prefix));
|
|
236
|
+
prefix = parent;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!isLink) throw realpathErr;
|
|
240
|
+
if (++hops > MAX_SYMLINK_HOPS) {
|
|
241
|
+
throw Object.assign(new Error("too many levels of symbolic links"), { code: "ELOOP" });
|
|
242
|
+
}
|
|
243
|
+
prefix = node_path.resolve(node_path.dirname(prefix), await fs$1.readlink(prefix));
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
return tail.length ? node_path.join(real, ...tail.reverse()) : real;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Resolve `p` against `root` and confine it to `root` or one of `allowedRoots`
|
|
251
|
+
* (absolute paths, resolved at check time exactly like `root`).
|
|
252
|
+
*
|
|
253
|
+
* Absolute and relative inputs go through the same canonicalise-then-contain
|
|
254
|
+
* check — an absolute path that lands inside a permitted root is accepted,
|
|
255
|
+
* only paths that resolve *outside* all of them are rejected. Every symlink in
|
|
256
|
+
* `p` (including the leaf, even a dangling one) is resolved before the
|
|
257
|
+
* confinement check, and the resolved path is what the caller then operates
|
|
258
|
+
* on, so a symlink inside `root` that points outside it can neither pass the
|
|
259
|
+
* check nor be followed afterwards. `..` is collapsed lexically before any
|
|
260
|
+
* symlink is followed. A path that cannot be resolved (symlink loop, unreadable
|
|
261
|
+
* component) is rejected with a `ToolError` naming `p`, never the host's
|
|
262
|
+
* absolute path.
|
|
263
|
+
*
|
|
264
|
+
* Residual TOCTOU: a component could still be swapped for a symlink between this
|
|
265
|
+
* call and the eventual `fs` operation. Closing that fully needs per-component
|
|
266
|
+
* `O_NOFOLLOW`/`openat`, which Node does not expose ergonomically; this is why a
|
|
267
|
+
* sandbox is still recommended for the toolset as a whole.
|
|
268
|
+
*/
|
|
269
|
+
async function confineToRoot(root, p, opts) {
|
|
270
|
+
const allowedRoots = opts?.allowedRoots ?? [];
|
|
271
|
+
const realRoot = await canonicalize(node_path.resolve(root));
|
|
272
|
+
let real;
|
|
273
|
+
try {
|
|
274
|
+
real = await canonicalize(node_path.resolve(realRoot, p));
|
|
275
|
+
} catch (err) {
|
|
276
|
+
throw new require_sdk.ToolError(fsErrorMessage(err, `path ${JSON.stringify(p)}`));
|
|
277
|
+
}
|
|
278
|
+
if (isWithin(realRoot, real) || await containingRoot(allowedRoots, real) !== undefined) {
|
|
279
|
+
return real;
|
|
280
|
+
}
|
|
281
|
+
const permitted = allowedRoots.length ? "the session's working directory and its other permitted directories" : "the session's working directory";
|
|
282
|
+
throw new require_sdk.ToolError(`path ${JSON.stringify(p)} is outside ${permitted}`);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Atomically write `content` to `targetPath`: write a sibling temp file, fsync
|
|
286
|
+
* it, then rename over the target. The rename is atomic on most filesystems, so
|
|
287
|
+
* a crash mid-write never leaves the target half-written.
|
|
288
|
+
*
|
|
289
|
+
* A new file is created {@link FILE_CREATE_MODE}; an existing one keeps its
|
|
290
|
+
* permission bits (the rename replaces the inode, so they are copied onto the
|
|
291
|
+
* temp file first) — an edit must not strip `+x` or sharing the owner chose.
|
|
292
|
+
*/
|
|
293
|
+
async function atomicWriteFile(targetPath, content) {
|
|
294
|
+
const dir = node_path.dirname(targetPath);
|
|
295
|
+
const tempPath = node_path.join(dir, `.tmp-${process.pid}-${node_crypto.randomUUID()}`);
|
|
296
|
+
const existingMode = await fs$1.stat(targetPath).then((st) => st.mode & 511, () => undefined);
|
|
297
|
+
let handle;
|
|
298
|
+
try {
|
|
299
|
+
handle = await fs$1.open(tempPath, "wx", 384);
|
|
300
|
+
if (existingMode !== undefined) await handle.chmod(existingMode);
|
|
301
|
+
await handle.writeFile(content, "utf-8");
|
|
302
|
+
await handle.sync();
|
|
303
|
+
await handle.close();
|
|
304
|
+
handle = undefined;
|
|
305
|
+
await fs$1.rename(tempPath, targetPath);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (handle) await handle.close().catch(() => {});
|
|
308
|
+
await fs$1.unlink(tempPath).catch(() => {});
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Map a thrown filesystem error to a consistent, language-independent message,
|
|
314
|
+
* so the model sees the same wording regardless of the runtime (Node's raw
|
|
315
|
+
* `ENOENT: no such file...` text would otherwise leak through). Codes we don't
|
|
316
|
+
* special-case render as the bare code, never Node's message, which embeds the
|
|
317
|
+
* host's absolute path.
|
|
318
|
+
*/
|
|
319
|
+
function fsErrorMessage(err, file) {
|
|
320
|
+
const code = errnoCode(err);
|
|
321
|
+
switch (code) {
|
|
322
|
+
case "ENOENT": return `${file}: no such file or directory`;
|
|
323
|
+
case "EACCES":
|
|
324
|
+
case "EPERM": return `${file}: permission denied`;
|
|
325
|
+
case "ENOTDIR": return `${file}: not a directory`;
|
|
326
|
+
case "EISDIR": return `${file}: is a directory`;
|
|
327
|
+
case "ELOOP": return `${file}: too many levels of symbolic links`;
|
|
328
|
+
case "ENAMETOOLONG": return `${file}: file name too long`;
|
|
329
|
+
case "ENOSPC": return `${file}: no space left on device`;
|
|
330
|
+
case "EMFILE":
|
|
331
|
+
case "ENFILE": return `${file}: too many open files`;
|
|
332
|
+
default: return `${file}: ${code !== undefined ? `i/o error (${code})` : "i/o error"}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region ../../node_modules/.pnpm/@anthropic-ai+sdk@0.124.0_zod@4.5.4/node_modules/@anthropic-ai/sdk/tools/agent-toolset/skills.mjs
|
|
338
|
+
/**
|
|
339
|
+
* Node-only skill plumbing for the agent toolset: downloading a session
|
|
340
|
+
* agent's skills into the workdir and extracting the archives. Kept in its own
|
|
341
|
+
* file because it is a distinct concern from the tool implementations in
|
|
342
|
+
* `node.ts` — distinct enough, and large enough, to review on its own.
|
|
343
|
+
*/
|
|
344
|
+
const fs = node_fs.promises;
|
|
345
|
+
const execFileAsync = node_util.promisify(node_child_process.execFile);
|
|
346
|
+
/**
|
|
347
|
+
* Download the session agent's skills into `{ctx.workdir}/skills/<name>/`.
|
|
348
|
+
*
|
|
349
|
+
* No-op (returns a no-op cleanup) unless `ctx.client` is set together with
|
|
350
|
+
* `ctx.session` (or the deprecated `ctx.sessionId`). Reads the resolved agent
|
|
351
|
+
* off the session and, for each skill, fetches its files via
|
|
352
|
+
* `client.beta.skills.versions.download` and extracts the archive (a zip or
|
|
353
|
+
* tar.* archive) into a directory named after the skill. A failure on one skill
|
|
354
|
+
* is logged and does not block the others. Call this before starting the
|
|
355
|
+
* session tool runner (e.g. right after the bash session / workdir is ready).
|
|
356
|
+
*
|
|
357
|
+
* Pass `ctx.session`. A session's resources cannot change while it runs, so the
|
|
358
|
+
* caller fetches it once and shares that snapshot with the memory-store
|
|
359
|
+
* download — the two can then never disagree about the attached resources.
|
|
360
|
+
*
|
|
361
|
+
* `ctx.sessionId` is deprecated: it costs an extra `sessions.retrieve` round
|
|
362
|
+
* trip on every call, and a caller that uses it for both this and the
|
|
363
|
+
* memory-store download fetches the session twice. It remains supported for
|
|
364
|
+
* callers written before `session` existed.
|
|
365
|
+
*
|
|
366
|
+
* Returns a cleanup function that removes the skill directories this call
|
|
367
|
+
* created — call it once the work item is done so downloaded skills do not
|
|
368
|
+
* accumulate in the workdir across sessions.
|
|
369
|
+
*/
|
|
370
|
+
async function setupSkills(ctx) {
|
|
371
|
+
const { client, sessionId } = ctx;
|
|
372
|
+
if (!client) return async () => {};
|
|
373
|
+
const log = require_sdk.loggerFor(client);
|
|
374
|
+
let session = ctx.session;
|
|
375
|
+
if (!session) {
|
|
376
|
+
if (sessionId === undefined) return async () => {};
|
|
377
|
+
log.warn("AgentToolContext.sessionId is deprecated and costs an extra session fetch; " + "fetch the session once and set `session` instead", { component: "agent-tool-context" });
|
|
378
|
+
session = await client.beta.sessions.retrieve(sessionId);
|
|
379
|
+
}
|
|
380
|
+
const skillsRoot = node_path.resolve(ctx.workdir, "skills");
|
|
381
|
+
const created = [];
|
|
382
|
+
for (const skill of session.agent.skills) {
|
|
383
|
+
try {
|
|
384
|
+
const version = await client.beta.skills.versions.retrieve(skill.version, { skill_id: skill.skill_id });
|
|
385
|
+
let dirname = node_path.basename(version.name.trim());
|
|
386
|
+
if (dirname === "" || dirname === "." || dirname === "..") dirname = skill.skill_id;
|
|
387
|
+
const dest = node_path.resolve(skillsRoot, dirname);
|
|
388
|
+
if (dest !== skillsRoot && !dest.startsWith(skillsRoot + node_path.sep)) {
|
|
389
|
+
log.warn("skill name escapes the skills dir; skipping", {
|
|
390
|
+
component: "agent-tool-context",
|
|
391
|
+
name: version.name
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const resp = await client.beta.skills.versions.download(version.id, { skill_id: skill.skill_id });
|
|
396
|
+
await fs.rm(dest, {
|
|
397
|
+
recursive: true,
|
|
398
|
+
force: true
|
|
399
|
+
});
|
|
400
|
+
await fs.mkdir(dest, {
|
|
401
|
+
recursive: true,
|
|
402
|
+
mode: 448
|
|
403
|
+
});
|
|
404
|
+
created.push(dest);
|
|
405
|
+
await extractSkillArchive(resp, dest);
|
|
406
|
+
log.info("downloaded skill", {
|
|
407
|
+
component: "agent-tool-context",
|
|
408
|
+
skill_id: skill.skill_id,
|
|
409
|
+
version: version.id,
|
|
410
|
+
dest
|
|
411
|
+
});
|
|
412
|
+
} catch (e) {
|
|
413
|
+
log.warn("failed to download skill", {
|
|
414
|
+
component: "agent-tool-context",
|
|
415
|
+
skill_id: skill.skill_id,
|
|
416
|
+
error: String(e)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return async () => {
|
|
421
|
+
for (const dest of created) {
|
|
422
|
+
await fs.rm(dest, {
|
|
423
|
+
recursive: true,
|
|
424
|
+
force: true
|
|
425
|
+
}).catch((e) => {
|
|
426
|
+
log.warn("failed to clean up skill", {
|
|
427
|
+
component: "agent-tool-context",
|
|
428
|
+
dest,
|
|
429
|
+
error: String(e)
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/** Reject archive members that are absolute or contain a `..` component. */
|
|
436
|
+
function assertSafeMemberNames(names) {
|
|
437
|
+
for (const raw of names) {
|
|
438
|
+
const entry = raw.trim();
|
|
439
|
+
if (!entry) continue;
|
|
440
|
+
if (node_path.isAbsolute(entry) || entry.split(/[\\/]/).includes("..")) {
|
|
441
|
+
throw new require_sdk.AnthropicError(`refusing to extract unsafe archive member: ${entry}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const INCONSISTENT_LISTING = "skill archive listing is inconsistent; refusing to extract";
|
|
446
|
+
/**
|
|
447
|
+
* Type chars (first byte of each `ls`-style line from `unzip -Z` / `tar -tvf`)
|
|
448
|
+
* that denote a regular file or directory. `zipinfo` prints `?` for entries
|
|
449
|
+
* with no Unix type bits, which `unzip` extracts as regular files; GNU tar
|
|
450
|
+
* prints `C` for contiguous files. Everything else — `l` symlink, `h`
|
|
451
|
+
* hardlink, `b`/`c` device, `p` fifo, `s` socket, unknown tar types — is a
|
|
452
|
+
* special member.
|
|
453
|
+
*/
|
|
454
|
+
const PLAIN_TYPE_CHARS = {
|
|
455
|
+
unzip: new Set([
|
|
456
|
+
"-",
|
|
457
|
+
"d",
|
|
458
|
+
"?"
|
|
459
|
+
]),
|
|
460
|
+
tar: new Set([
|
|
461
|
+
"-",
|
|
462
|
+
"d",
|
|
463
|
+
"C"
|
|
464
|
+
])
|
|
465
|
+
};
|
|
466
|
+
function listingLines(listing) {
|
|
467
|
+
const lines = listing.split("\n");
|
|
468
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
469
|
+
return lines;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* A special member is excluded by handing its listed name back to the CLI as
|
|
473
|
+
* a pattern, so the name must be byte-identical to what is stored. `tar`,
|
|
474
|
+
* `bsdtar` and `unzip` print bytes they cannot show literally as `\ooo`, `^X`
|
|
475
|
+
* or `#U` escapes, or as raw non-ASCII; any such name cannot be excluded
|
|
476
|
+
* reliably. A leading `-` would let `unzip` parse the pattern as an option.
|
|
477
|
+
*/
|
|
478
|
+
function canExcludeVerbatim(cmd, name) {
|
|
479
|
+
return /^[\x20-\x7E]+$/.test(name) && !/[\\^#]/.test(name) && !(cmd === "unzip" && name.startsWith("-"));
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Pair an archive's name listing (`unzip -Z1` / `tar -tf`) with its typed
|
|
483
|
+
* listing (`unzip -Z --h --t` / `tar -tvf`) and split the members into plain
|
|
484
|
+
* (regular file or directory) and special (everything else). Special members
|
|
485
|
+
* are excluded from extraction rather than rejected; the archive is refused
|
|
486
|
+
* only when the two listings disagree in length or a special member's name
|
|
487
|
+
* cannot be passed back to the CLI verbatim (see {@link canExcludeVerbatim}).
|
|
488
|
+
*/
|
|
489
|
+
function classifyArchiveListing(cmd, names, typed) {
|
|
490
|
+
const nameLines = listingLines(names);
|
|
491
|
+
const typedLines = listingLines(typed);
|
|
492
|
+
if (nameLines.length !== typedLines.length) throw new require_sdk.AnthropicError(INCONSISTENT_LISTING);
|
|
493
|
+
const plain = [];
|
|
494
|
+
const special = [];
|
|
495
|
+
nameLines.forEach((name, i) => {
|
|
496
|
+
if (PLAIN_TYPE_CHARS[cmd].has(typedLines[i].charAt(0))) {
|
|
497
|
+
plain.push(name);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (!canExcludeVerbatim(cmd, name)) {
|
|
501
|
+
throw new require_sdk.AnthropicError(`refusing to extract archive: cannot safely exclude member ${JSON.stringify(name)}`);
|
|
502
|
+
}
|
|
503
|
+
special.push(name);
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
plain,
|
|
507
|
+
special
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Walk `dir` with `lstat` semantics and reject anything that is not a regular
|
|
512
|
+
* file or directory. Never follows a link and never descends into anything
|
|
513
|
+
* but a real directory.
|
|
514
|
+
*/
|
|
515
|
+
async function assertOnlyPlainEntries(dir) {
|
|
516
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
517
|
+
if (entry.isDirectory()) await assertOnlyPlainEntries(node_path.join(dir, entry.name));
|
|
518
|
+
else if (!entry.isFile()) throw new require_sdk.AnthropicError(INCONSISTENT_LISTING);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Run an archive CLI (`unzip` for zip archives, `tar` for everything else),
|
|
523
|
+
* returning its stdout. Both binaries must be on `PATH`; a missing one would
|
|
524
|
+
* otherwise surface as an opaque `ENOENT` spawn failure, so it is turned into a
|
|
525
|
+
* clear, specific error naming the missing command.
|
|
526
|
+
*/
|
|
527
|
+
async function runArchiveTool(cmd, args) {
|
|
528
|
+
try {
|
|
529
|
+
const { stdout } = await execFileAsync(cmd, args);
|
|
530
|
+
return stdout;
|
|
531
|
+
} catch (e) {
|
|
532
|
+
if (errnoCode(e) === "ENOENT") {
|
|
533
|
+
throw new require_sdk.AnthropicError(`skill extraction requires the \`${cmd}\` command, but it was not found on PATH`);
|
|
534
|
+
}
|
|
535
|
+
throw e;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The single top-level directory shared by every entry in an archive listing,
|
|
540
|
+
* or `''` if entries don't all live under one common directory. Skill bundles
|
|
541
|
+
* are packaged wrapped in one directory named after the skill (e.g.
|
|
542
|
+
* `pdf/SKILL.md`, `pdf/scripts/...`); the extractor strips it so contents land
|
|
543
|
+
* directly in the skill's dir instead of a redundant nested `<skill>/<skill>/`
|
|
544
|
+
* level. A flat or multi-root archive yields `''`.
|
|
545
|
+
*/
|
|
546
|
+
function archiveTopDir(names) {
|
|
547
|
+
let top;
|
|
548
|
+
let nested = false;
|
|
549
|
+
for (const raw of names) {
|
|
550
|
+
const parts = raw.trim().split("/").filter((p) => p !== "" && p !== ".");
|
|
551
|
+
if (parts.length === 0) continue;
|
|
552
|
+
const first = parts[0];
|
|
553
|
+
if (top === undefined) top = first;
|
|
554
|
+
else if (first !== top) return "";
|
|
555
|
+
if (parts.length > 1) nested = true;
|
|
556
|
+
}
|
|
557
|
+
return top !== undefined && nested ? top : "";
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Extract a skill download (a zip or tar.* archive) into `dest`. Streams the
|
|
561
|
+
* response body straight to a temp file beside `dest` (so the whole archive is
|
|
562
|
+
* never buffered in memory — skills can contain large binaries), then shells out
|
|
563
|
+
* to `unzip`/`tar` — consistent with the rest of the toolset, which already
|
|
564
|
+
* invokes `bash` and `rg`. Both `unzip` and `tar` must be available on `PATH`; a
|
|
565
|
+
* missing binary surfaces as a clear error (see {@link runArchiveTool}). Refuses
|
|
566
|
+
* any member that would escape `dest` (zip-slip / tar-slip): skill archives
|
|
567
|
+
* come from the API, but skills can be third-party. Members that are not a
|
|
568
|
+
* regular file or directory (symlink, hardlink, device, fifo) are excluded
|
|
569
|
+
* from extraction rather than rejected; an archive whose special members
|
|
570
|
+
* cannot be excluded reliably is refused (see {@link classifyArchiveListing}).
|
|
571
|
+
* `tar` matches exclusions unanchored, so a plain member sharing a special
|
|
572
|
+
* member's name may be dropped too. The staging tree is verified to hold only
|
|
573
|
+
* regular files and directories before anything is promoted into `dest`.
|
|
574
|
+
*
|
|
575
|
+
* The skill bundle's single wrapper directory is stripped: the archive is
|
|
576
|
+
* extracted into a staging dir and the wrapper's contents are promoted into
|
|
577
|
+
* `dest`, so files land at `dest/SKILL.md` rather than a doubled
|
|
578
|
+
* `dest/<skill>/SKILL.md` (`unzip` has no `--strip-components`, so this is
|
|
579
|
+
* done uniformly by staging + promote rather than per-tool flags).
|
|
580
|
+
*/
|
|
581
|
+
async function extractSkillArchive(resp, dest) {
|
|
582
|
+
const tmp = node_path.join(dest, `.skill-archive-${process.pid}-${Date.now()}`);
|
|
583
|
+
if (!resp.body) {
|
|
584
|
+
throw new require_sdk.AnthropicError("skill download response had no body");
|
|
585
|
+
}
|
|
586
|
+
await node_stream.promises.pipeline(node_stream.Readable.fromWeb(resp.body), node_fs.createWriteStream(tmp));
|
|
587
|
+
const stage = node_path.join(node_path.dirname(dest), `.skill-stage-${process.pid}-${Date.now()}`);
|
|
588
|
+
const excludeFile = node_path.join(node_path.dirname(dest), `.skill-exclude-${process.pid}-${Date.now()}`);
|
|
589
|
+
try {
|
|
590
|
+
const head = await readHead(tmp, 4);
|
|
591
|
+
const isZip = head.length >= 4 && head[0] === 80 && head[1] === 75 && head[2] === 3 && head[3] === 4;
|
|
592
|
+
const archiveCmd = isZip ? "unzip" : "tar";
|
|
593
|
+
const names = await runArchiveTool(archiveCmd, isZip ? ["-Z1", tmp] : ["-tf", tmp]);
|
|
594
|
+
const typed = await runArchiveTool(archiveCmd, isZip ? [
|
|
595
|
+
"-Z",
|
|
596
|
+
"--h",
|
|
597
|
+
"--t",
|
|
598
|
+
tmp
|
|
599
|
+
] : ["-tvf", tmp]);
|
|
600
|
+
const { plain, special } = classifyArchiveListing(archiveCmd, names, typed);
|
|
601
|
+
assertSafeMemberNames([...plain, ...special]);
|
|
602
|
+
const top = archiveTopDir(plain);
|
|
603
|
+
await fs.mkdir(stage, {
|
|
604
|
+
recursive: true,
|
|
605
|
+
mode: 448
|
|
606
|
+
});
|
|
607
|
+
if (plain.length > 0) {
|
|
608
|
+
await runArchiveTool(archiveCmd, await extractArgs(archiveCmd, tmp, stage, special, excludeFile));
|
|
609
|
+
}
|
|
610
|
+
await assertOnlyPlainEntries(stage);
|
|
611
|
+
const srcRoot = top ? node_path.join(stage, top) : stage;
|
|
612
|
+
const entries = await fs.readdir(srcRoot).catch((e) => {
|
|
613
|
+
throw errnoCode(e) === "ENOENT" ? new require_sdk.AnthropicError(INCONSISTENT_LISTING) : e;
|
|
614
|
+
});
|
|
615
|
+
for (const entry of entries) {
|
|
616
|
+
await fs.rename(node_path.join(srcRoot, entry), node_path.join(dest, entry));
|
|
617
|
+
}
|
|
618
|
+
} finally {
|
|
619
|
+
await fs.rm(tmp, { force: true });
|
|
620
|
+
await fs.rm(excludeFile, { force: true });
|
|
621
|
+
await fs.rm(stage, {
|
|
622
|
+
recursive: true,
|
|
623
|
+
force: true
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Arguments that extract `archive` into `stage` while excluding every member
|
|
629
|
+
* in `special`. Names are glob-escaped because both CLIs treat exclusions as
|
|
630
|
+
* patterns; `tar` reads them from `excludeFile`, `unzip` takes them after
|
|
631
|
+
* `-x`, which must follow `-d` so no pattern is parsed as an option.
|
|
632
|
+
*/
|
|
633
|
+
async function extractArgs(cmd, archive, stage, special, excludeFile) {
|
|
634
|
+
const patterns = special.map((name) => name.replace(/[*?[\\]/g, "\\$&"));
|
|
635
|
+
if (cmd === "unzip") {
|
|
636
|
+
return [
|
|
637
|
+
"-oq",
|
|
638
|
+
archive,
|
|
639
|
+
"-d",
|
|
640
|
+
stage,
|
|
641
|
+
...patterns.length > 0 ? ["-x", ...patterns] : []
|
|
642
|
+
];
|
|
643
|
+
}
|
|
644
|
+
if (patterns.length === 0) return [
|
|
645
|
+
"-xf",
|
|
646
|
+
archive,
|
|
647
|
+
"-C",
|
|
648
|
+
stage
|
|
649
|
+
];
|
|
650
|
+
await fs.writeFile(excludeFile, patterns.join("\n") + "\n", {
|
|
651
|
+
flag: "wx",
|
|
652
|
+
mode: 384
|
|
653
|
+
});
|
|
654
|
+
return [
|
|
655
|
+
"-xf",
|
|
656
|
+
archive,
|
|
657
|
+
"-C",
|
|
658
|
+
stage,
|
|
659
|
+
"-X",
|
|
660
|
+
excludeFile
|
|
661
|
+
];
|
|
662
|
+
}
|
|
663
|
+
/** Read the first `n` bytes of `file`. */
|
|
664
|
+
async function readHead(file, n) {
|
|
665
|
+
const handle = await fs.open(file, "r");
|
|
666
|
+
try {
|
|
667
|
+
const buf = Buffer.alloc(n);
|
|
668
|
+
const { bytesRead } = await handle.read(buf, 0, n, 0);
|
|
669
|
+
return buf.subarray(0, bytesRead);
|
|
670
|
+
} finally {
|
|
671
|
+
await handle.close();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
//#endregion
|
|
676
|
+
//#region ../../node_modules/.pnpm/@anthropic-ai+sdk@0.124.0_zod@4.5.4/node_modules/@anthropic-ai/sdk/internal/file-store.mjs
|
|
677
|
+
/**
|
|
678
|
+
* `FileStore` — one confined folder; a relative path cannot escape it.
|
|
679
|
+
*
|
|
680
|
+
* Beta scope: symlinks are refused or skipped wherever the store meets them,
|
|
681
|
+
* but there is no hardening against a process racing the store's own
|
|
682
|
+
* syscalls; fsync durability, non-POSIX hosts, and read-size caps are out of
|
|
683
|
+
* scope.
|
|
684
|
+
*/
|
|
685
|
+
const fsp = node_fs.promises;
|
|
686
|
+
const C = node_fs.constants;
|
|
687
|
+
const OWNER_ONLY_DIR_MODE = 448;
|
|
688
|
+
const OWNER_ONLY_FILE_MODE = 384;
|
|
689
|
+
const OWNER_ONLY_EXEC_MODE = 448;
|
|
690
|
+
const O_NOFOLLOW = C.O_NOFOLLOW ?? 0;
|
|
691
|
+
const O_NONBLOCK = C.O_NONBLOCK ?? 0;
|
|
692
|
+
/** A refused operation — input the store will not act on. OS errors propagate with their `.code`. */
|
|
693
|
+
var FileStoreError = class extends Error {
|
|
694
|
+
constructor(reason, relPath) {
|
|
695
|
+
super(`path ${JSON.stringify(relPath)} ${reason}`);
|
|
696
|
+
this.name = "FileStoreError";
|
|
697
|
+
this.reason = reason;
|
|
698
|
+
this.relPath = relPath;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
FileStoreError.ESCAPES_ROOT = "escapes the store root";
|
|
702
|
+
FileStoreError.IS_A_SYMLINK = "is a symlink";
|
|
703
|
+
FileStoreError.NOT_A_FILE = "is not a regular file";
|
|
704
|
+
FileStoreError.NOT_A_DIRECTORY = "is not a directory";
|
|
705
|
+
FileStoreError.NOT_UTF8 = "is not valid utf-8";
|
|
706
|
+
FileStoreError.MOVE_DESTINATION_EXISTS = "already exists";
|
|
707
|
+
/** Resolve `root`; creates nothing — only {@link FileStore.createRoot} makes the folder. */
|
|
708
|
+
async function openFileStore(root, opts) {
|
|
709
|
+
return FileStore.open(root, opts);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* True for a path usable verbatim as a store location: absolute, with no `..`
|
|
713
|
+
* components. Paths are judged in POSIX terms — they are wire values naming
|
|
714
|
+
* locations inside a POSIX container, not host-native paths.
|
|
715
|
+
*/
|
|
716
|
+
function isPathLegal(p) {
|
|
717
|
+
return p.startsWith("/") && !p.split("/").includes("..");
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* One confined folder of regular files.
|
|
721
|
+
*
|
|
722
|
+
* Every `relPath` is relative to the root (a leading `/` also means the root)
|
|
723
|
+
* and refused with {@link FileStoreError} when it escapes. The store holds
|
|
724
|
+
* regular files only: symlinks are refused on read and skipped by listings —
|
|
725
|
+
* {@link findSymlinks} reports them. A `relPath` resolving to the root itself
|
|
726
|
+
* is banned by this interface: `put` and `get` refuse it, `move` and `remove`
|
|
727
|
+
* do nothing. A store opened with `utf8: true` refuses binary content the
|
|
728
|
+
* same way — on `put` of such bytes and on `get` of such a file. Only
|
|
729
|
+
* {@link createRoot} makes the root: writes create directories below it,
|
|
730
|
+
* never the root itself, so a root removed while the store is open stays
|
|
731
|
+
* removed and the write fails with `ENOENT`.
|
|
732
|
+
*/
|
|
733
|
+
var FileStore = class FileStore {
|
|
734
|
+
/** @internal — use {@link FileStore.open} / {@link openFileStore}. */
|
|
735
|
+
constructor(root, removedOnDispose, utf8Only = false) {
|
|
736
|
+
/** `hashtree`'s advisory cache; every hit re-validates against a fresh stat. */
|
|
737
|
+
this.hashes = new Map();
|
|
738
|
+
this.rootPath = root;
|
|
739
|
+
this.removedOnDispose = removedOnDispose;
|
|
740
|
+
this.decoder = utf8Only ? new TextDecoder("utf-8", { fatal: true }) : undefined;
|
|
741
|
+
}
|
|
742
|
+
/** Resolve `root`; creates nothing — only {@link createRoot} makes the folder. */
|
|
743
|
+
static async open(root, opts) {
|
|
744
|
+
if (!platformSupported()) {
|
|
745
|
+
throw new Error("FileStore requires O_NOFOLLOW support on this platform");
|
|
746
|
+
}
|
|
747
|
+
let removedOnDispose = false;
|
|
748
|
+
try {
|
|
749
|
+
await fsp.lstat(root);
|
|
750
|
+
} catch (e) {
|
|
751
|
+
if (e.code !== "ENOENT") throw e;
|
|
752
|
+
removedOnDispose = true;
|
|
753
|
+
}
|
|
754
|
+
return new FileStore(node_path.resolve(root), removedOnDispose, opts?.utf8 ?? false);
|
|
755
|
+
}
|
|
756
|
+
/** Create the root directory and any missing ancestors; already existing is fine. */
|
|
757
|
+
async createRoot() {
|
|
758
|
+
await makeDirAndAncestors(this.rootPath);
|
|
759
|
+
}
|
|
760
|
+
/** The resolved root, and what {@link dispose} will do to it. */
|
|
761
|
+
root() {
|
|
762
|
+
return {
|
|
763
|
+
path: this.rootPath,
|
|
764
|
+
removedOnDispose: this.removedOnDispose
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Remove the root iff `open` created it; pre-existing roots are kept.
|
|
769
|
+
*
|
|
770
|
+
* Wired to `Symbol.asyncDispose` at runtime when the host provides it, so
|
|
771
|
+
* `await using` works on engines with explicit resource management.
|
|
772
|
+
*/
|
|
773
|
+
async dispose() {
|
|
774
|
+
if (!this.removedOnDispose) return;
|
|
775
|
+
await fsp.rm(this.rootPath, {
|
|
776
|
+
recursive: true,
|
|
777
|
+
force: true
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Write `data` (`string` UTF-8 or bytes) atomically to the file at `relPath`.
|
|
782
|
+
*
|
|
783
|
+
* Missing directories below the root are created; a missing root is not —
|
|
784
|
+
* the write fails with `ENOENT`.
|
|
785
|
+
*/
|
|
786
|
+
async put(relPath, data, opts) {
|
|
787
|
+
const tail = relPath.replace(/\\/g, "/");
|
|
788
|
+
if (tail.endsWith("/") || tail.endsWith("/.") || tail === "" || tail === ".") {
|
|
789
|
+
throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath);
|
|
790
|
+
}
|
|
791
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
792
|
+
const payload = typeof data === "string" ? require_sdk.encodeUTF8(data) : data;
|
|
793
|
+
this.requireUtf8(relPath, payload);
|
|
794
|
+
await makeDirsBelowRoot(this.rootPath, node_path.dirname(dest));
|
|
795
|
+
await replaceViaTemp(dest, payload, opts?.executable ?? false);
|
|
796
|
+
}
|
|
797
|
+
/** The file's bytes; `null` when absent. */
|
|
798
|
+
async get(relPath) {
|
|
799
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
800
|
+
let handle;
|
|
801
|
+
try {
|
|
802
|
+
handle = await openRegularFile(relPath, dest);
|
|
803
|
+
} catch (e) {
|
|
804
|
+
if (e.code === "ENOENT") return null;
|
|
805
|
+
throw e;
|
|
806
|
+
}
|
|
807
|
+
let data;
|
|
808
|
+
try {
|
|
809
|
+
const buf = await handle.readFile();
|
|
810
|
+
data = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
811
|
+
} finally {
|
|
812
|
+
await handle.close();
|
|
813
|
+
}
|
|
814
|
+
this.requireUtf8(relPath, data);
|
|
815
|
+
return data;
|
|
816
|
+
}
|
|
817
|
+
/** The relative path of every file under the directory `under`. */
|
|
818
|
+
async ls(under = "/") {
|
|
819
|
+
const base = this.resolveUnderRoot(under);
|
|
820
|
+
return new Set((await filenamesInDir(this.rootPath, under, base)).map(([rel]) => rel));
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Every symlink under `under` — listings skip them and reads refuse them,
|
|
824
|
+
* so a caller that must know they exist asks here.
|
|
825
|
+
*/
|
|
826
|
+
async findSymlinks(under = "/") {
|
|
827
|
+
const base = this.resolveUnderRoot(under);
|
|
828
|
+
return symlinksInDir(this.rootPath, under, base);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* `{relPath: sha256Hex}` of every file under the directory `under`.
|
|
832
|
+
*
|
|
833
|
+
* Unchanged files — same size, mtime, and ctime since the last call —
|
|
834
|
+
* reuse their recorded hash instead of being re-read.
|
|
835
|
+
*/
|
|
836
|
+
async hashtree(under = "/") {
|
|
837
|
+
const base = this.resolveUnderRoot(under);
|
|
838
|
+
const walkStartNs = _internals.nowNs();
|
|
839
|
+
const out = Object.create(null);
|
|
840
|
+
for (const [rel, full] of await filenamesInDir(this.rootPath, under, base)) {
|
|
841
|
+
const sha = await this.hashViaCache(rel, full, walkStartNs);
|
|
842
|
+
if (sha !== null) out[rel] = sha;
|
|
843
|
+
}
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
/** One file's sha256; `null` when absent. Shares {@link hashtree}'s cache. */
|
|
847
|
+
async hashFile(relPath) {
|
|
848
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
849
|
+
let st;
|
|
850
|
+
try {
|
|
851
|
+
st = await fsp.lstat(dest, { bigint: true });
|
|
852
|
+
} catch (e) {
|
|
853
|
+
if (e.code === "ENOENT") return null;
|
|
854
|
+
throw e;
|
|
855
|
+
}
|
|
856
|
+
if (st.isSymbolicLink()) throw new FileStoreError(FileStoreError.IS_A_SYMLINK, relPath);
|
|
857
|
+
if (!st.isFile()) throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath);
|
|
858
|
+
const rel = node_path.relative(this.rootPath, dest).split(node_path.sep).join("/");
|
|
859
|
+
return this.hashViaCache(rel, dest, _internals.nowNs());
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Rename `src` to `dst`; an existing `dst` is refused. The banned store
|
|
863
|
+
* root as either end does nothing.
|
|
864
|
+
*/
|
|
865
|
+
async move(src, dst) {
|
|
866
|
+
const s = this.resolveUnderRoot(src);
|
|
867
|
+
const d = this.resolveUnderRoot(dst);
|
|
868
|
+
if (s === this.rootPath || d === this.rootPath) return;
|
|
869
|
+
const dstExists = await fsp.stat(d).then(() => true, () => false);
|
|
870
|
+
if (dstExists) throw new FileStoreError(FileStoreError.MOVE_DESTINATION_EXISTS, dst);
|
|
871
|
+
await makeDirsBelowRoot(this.rootPath, node_path.dirname(d));
|
|
872
|
+
await fsp.rename(s, d);
|
|
873
|
+
}
|
|
874
|
+
/** Delete a file or subtree; absent — and the banned store root — do nothing. */
|
|
875
|
+
async remove(relPath) {
|
|
876
|
+
const dest = this.resolveUnderRoot(relPath);
|
|
877
|
+
if (dest === this.rootPath) return;
|
|
878
|
+
let st;
|
|
879
|
+
try {
|
|
880
|
+
st = await fsp.lstat(dest, { bigint: true });
|
|
881
|
+
} catch (e) {
|
|
882
|
+
if (e.code === "ENOENT") return;
|
|
883
|
+
throw e;
|
|
884
|
+
}
|
|
885
|
+
if (st.isDirectory()) {
|
|
886
|
+
await fsp.rm(dest, {
|
|
887
|
+
recursive: true,
|
|
888
|
+
force: true
|
|
889
|
+
});
|
|
890
|
+
} else {
|
|
891
|
+
try {
|
|
892
|
+
await fsp.unlink(dest);
|
|
893
|
+
} catch (e) {
|
|
894
|
+
if (e.code !== "ENOENT") throw e;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
resolveUnderRoot(relPath) {
|
|
899
|
+
const norm = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
900
|
+
const parts = norm.split("/").filter((p) => p !== "" && p !== ".");
|
|
901
|
+
if (node_path.posix.isAbsolute(norm) || parts.includes("..")) {
|
|
902
|
+
throw new FileStoreError(FileStoreError.ESCAPES_ROOT, relPath);
|
|
903
|
+
}
|
|
904
|
+
return parts.length === 0 ? this.rootPath : node_path.join(this.rootPath, ...parts);
|
|
905
|
+
}
|
|
906
|
+
requireUtf8(relPath, data) {
|
|
907
|
+
if (!this.decoder) return;
|
|
908
|
+
try {
|
|
909
|
+
this.decoder.decode(data);
|
|
910
|
+
} catch {
|
|
911
|
+
throw new FileStoreError(FileStoreError.NOT_UTF8, relPath);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async hashViaCache(rel, full, walkStartNs) {
|
|
915
|
+
let st;
|
|
916
|
+
try {
|
|
917
|
+
st = await fsp.lstat(full, { bigint: true });
|
|
918
|
+
} catch (e) {
|
|
919
|
+
if (e.code === "ENOENT") return null;
|
|
920
|
+
throw e;
|
|
921
|
+
}
|
|
922
|
+
if (!st.isFile()) return null;
|
|
923
|
+
const cached = this.hashes.get(rel);
|
|
924
|
+
let sha;
|
|
925
|
+
if (cached !== undefined && unchangedSinceHashed(cached, st)) {
|
|
926
|
+
sha = cached.sha;
|
|
927
|
+
} else {
|
|
928
|
+
try {
|
|
929
|
+
sha = await _internals.hashFile(full);
|
|
930
|
+
} catch (e) {
|
|
931
|
+
const code = e.code;
|
|
932
|
+
if (code === "ENOENT" || e instanceof FileStoreError) return null;
|
|
933
|
+
if (code === "ELOOP" || code === "EMLINK") return null;
|
|
934
|
+
throw e;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (oldEnoughToCache(st, walkStartNs)) {
|
|
938
|
+
this.hashes.set(rel, {
|
|
939
|
+
mtimeNs: st.mtimeNs,
|
|
940
|
+
ctimeNs: st.ctimeNs,
|
|
941
|
+
size: st.size,
|
|
942
|
+
sha
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
return sha;
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
FileStore.isPathLegal = isPathLegal;
|
|
949
|
+
function platformSupported() {
|
|
950
|
+
return O_NOFOLLOW !== 0;
|
|
951
|
+
}
|
|
952
|
+
async function makeDirAndAncestors(dir) {
|
|
953
|
+
const missing = [];
|
|
954
|
+
let current = dir;
|
|
955
|
+
for (;;) {
|
|
956
|
+
try {
|
|
957
|
+
await fsp.stat(current);
|
|
958
|
+
break;
|
|
959
|
+
} catch (e) {
|
|
960
|
+
const code = e.code;
|
|
961
|
+
if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "ELOOP") throw e;
|
|
962
|
+
}
|
|
963
|
+
missing.push(current);
|
|
964
|
+
const parent = node_path.dirname(current);
|
|
965
|
+
if (parent === current) break;
|
|
966
|
+
current = parent;
|
|
967
|
+
}
|
|
968
|
+
for (const directory of missing.reverse()) {
|
|
969
|
+
try {
|
|
970
|
+
await fsp.mkdir(directory, { mode: OWNER_ONLY_DIR_MODE });
|
|
971
|
+
} catch (e) {
|
|
972
|
+
if (e.code !== "EEXIST") throw e;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function makeDirsBelowRoot(root, dir) {
|
|
977
|
+
const below = node_path.relative(root, dir);
|
|
978
|
+
if (below === "") return;
|
|
979
|
+
let current = root;
|
|
980
|
+
for (const part of below.split(node_path.sep)) {
|
|
981
|
+
current = node_path.join(current, part);
|
|
982
|
+
try {
|
|
983
|
+
await fsp.mkdir(current, { mode: OWNER_ONLY_DIR_MODE });
|
|
984
|
+
} catch (e) {
|
|
985
|
+
if (e.code !== "EEXIST") throw e;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function replaceViaTemp(dest, data, isExecutable) {
|
|
990
|
+
const mode = isExecutable ? OWNER_ONLY_EXEC_MODE : OWNER_ONLY_FILE_MODE;
|
|
991
|
+
const tmp = node_path.join(node_path.dirname(dest), `.fs-${node_crypto.randomBytes(8).toString("hex")}.tmp`);
|
|
992
|
+
let handle;
|
|
993
|
+
try {
|
|
994
|
+
handle = await fsp.open(tmp, C.O_WRONLY | C.O_CREAT | C.O_EXCL | O_NOFOLLOW, mode);
|
|
995
|
+
await handle.writeFile(data);
|
|
996
|
+
await handle.close();
|
|
997
|
+
handle = undefined;
|
|
998
|
+
await fsp.rename(tmp, dest);
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
if (handle) await handle.close().catch(() => {});
|
|
1001
|
+
await fsp.unlink(tmp).catch(() => {});
|
|
1002
|
+
throw err;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async function openRegularFile(relPath, dest) {
|
|
1006
|
+
let handle;
|
|
1007
|
+
try {
|
|
1008
|
+
handle = await fsp.open(dest, C.O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
const code = e.code;
|
|
1011
|
+
if (code === "ELOOP" || code === "EMLINK") {
|
|
1012
|
+
throw new FileStoreError(FileStoreError.IS_A_SYMLINK, relPath);
|
|
1013
|
+
}
|
|
1014
|
+
throw e;
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
const st = await handle.stat();
|
|
1018
|
+
if (!st.isFile()) throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath);
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
await handle.close().catch(() => {});
|
|
1021
|
+
throw e;
|
|
1022
|
+
}
|
|
1023
|
+
return handle;
|
|
1024
|
+
}
|
|
1025
|
+
/** sha256 of a file's contents, streamed — constant memory on any file size. */
|
|
1026
|
+
async function hashFile(full) {
|
|
1027
|
+
const digest = node_crypto.createHash("sha256");
|
|
1028
|
+
const handle = await openRegularFile(node_path.basename(full), full);
|
|
1029
|
+
const buf = new Uint8Array(1024 * 1024);
|
|
1030
|
+
try {
|
|
1031
|
+
for (;;) {
|
|
1032
|
+
const { bytesRead } = await handle.read(buf, 0, buf.length);
|
|
1033
|
+
if (bytesRead === 0) break;
|
|
1034
|
+
digest.update(buf.subarray(0, bytesRead));
|
|
1035
|
+
}
|
|
1036
|
+
} finally {
|
|
1037
|
+
await handle.close();
|
|
1038
|
+
}
|
|
1039
|
+
return digest.digest("hex");
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* `[rel, path]` for every regular file under the directory `base`; an absent
|
|
1043
|
+
* `base` is empty, a present non-directory is refused. The walk never
|
|
1044
|
+
* descends symlinked directories.
|
|
1045
|
+
*/
|
|
1046
|
+
async function filenamesInDir(root, under, base) {
|
|
1047
|
+
if (!await requireDir(under, base)) return [];
|
|
1048
|
+
const out = [];
|
|
1049
|
+
await walk$1(base, (full, entry) => {
|
|
1050
|
+
if (entry.isFile()) out.push([node_path.relative(root, full).split(node_path.sep).join("/"), full]);
|
|
1051
|
+
});
|
|
1052
|
+
out.sort();
|
|
1053
|
+
return out;
|
|
1054
|
+
}
|
|
1055
|
+
async function symlinksInDir(root, under, base) {
|
|
1056
|
+
const relOf = (full) => node_path.relative(root, full).split(node_path.sep).join("/");
|
|
1057
|
+
let st;
|
|
1058
|
+
try {
|
|
1059
|
+
st = await fsp.lstat(base, { bigint: true });
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
const code = e.code;
|
|
1062
|
+
if (code === "ENOENT" || code === "ENOTDIR") return new Set();
|
|
1063
|
+
throw e;
|
|
1064
|
+
}
|
|
1065
|
+
if (st.isSymbolicLink()) return new Set([relOf(base)]);
|
|
1066
|
+
if (!st.isDirectory()) throw new FileStoreError(FileStoreError.NOT_A_DIRECTORY, under);
|
|
1067
|
+
const out = new Set();
|
|
1068
|
+
await walk$1(base, (full, entry) => {
|
|
1069
|
+
if (entry.isSymbolicLink()) out.add(relOf(full));
|
|
1070
|
+
});
|
|
1071
|
+
return out;
|
|
1072
|
+
}
|
|
1073
|
+
/** `false` when `base` is absent, refused when present but not a directory. */
|
|
1074
|
+
async function requireDir(under, base) {
|
|
1075
|
+
let st;
|
|
1076
|
+
try {
|
|
1077
|
+
st = await fsp.lstat(base, { bigint: true });
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
const code = e.code;
|
|
1080
|
+
if (code === "ENOENT" || code === "ENOTDIR") return false;
|
|
1081
|
+
throw e;
|
|
1082
|
+
}
|
|
1083
|
+
if (!st.isDirectory()) throw new FileStoreError(FileStoreError.NOT_A_DIRECTORY, under);
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
/** Visit every entry under `base` without descending symlinked directories. */
|
|
1087
|
+
async function walk$1(base, visit) {
|
|
1088
|
+
const stack = [base];
|
|
1089
|
+
while (stack.length) {
|
|
1090
|
+
const dir = stack.pop();
|
|
1091
|
+
let entries;
|
|
1092
|
+
try {
|
|
1093
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
if (e.code === "ENOENT") continue;
|
|
1096
|
+
throw e;
|
|
1097
|
+
}
|
|
1098
|
+
for (const entry of entries) {
|
|
1099
|
+
const full = node_path.join(dir, entry.name);
|
|
1100
|
+
visit(full, entry);
|
|
1101
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) stack.push(full);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function unchangedSinceHashed(cached, st) {
|
|
1106
|
+
return st.mtimeNs === cached.mtimeNs && st.ctimeNs === cached.ctimeNs && st.size === cached.size;
|
|
1107
|
+
}
|
|
1108
|
+
function oldEnoughToCache(st, walkStartNs) {
|
|
1109
|
+
const newestNs = st.mtimeNs > st.ctimeNs ? st.mtimeNs : st.ctimeNs;
|
|
1110
|
+
return newestNs < walkStartNs - _internals.timestampTrustMarginNs;
|
|
1111
|
+
}
|
|
1112
|
+
const TIMESTAMP_TRUST_MARGIN_NS = 2000000000n;
|
|
1113
|
+
/** Test seam — the hasher, the trust margin, and the walk clock. @internal */
|
|
1114
|
+
const _internals = {
|
|
1115
|
+
hashFile,
|
|
1116
|
+
timestampTrustMarginNs: TIMESTAMP_TRUST_MARGIN_NS,
|
|
1117
|
+
nowNs: () => BigInt(Date.now()) * 1000000n
|
|
1118
|
+
};
|
|
1119
|
+
const LocalFileStore = FileStore;
|
|
1120
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
1121
|
+
if (asyncDispose) {
|
|
1122
|
+
Object.defineProperty(FileStore.prototype, asyncDispose, {
|
|
1123
|
+
value: FileStore.prototype.dispose,
|
|
1124
|
+
configurable: true,
|
|
1125
|
+
writable: true
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
//#endregion
|
|
1130
|
+
//#region ../../node_modules/.pnpm/@anthropic-ai+sdk@0.124.0_zod@4.5.4/node_modules/@anthropic-ai/sdk/tools/agent-toolset/memories.mjs
|
|
1131
|
+
/**
|
|
1132
|
+
* Session-level memory-store download and sync.
|
|
1133
|
+
*
|
|
1134
|
+
* A session may have several memory stores attached. This module resolves
|
|
1135
|
+
* where each store's folder goes on disk, opens a {@link LocalFileStore}
|
|
1136
|
+
* there, and reconciles each folder with its remote store — the merge rules
|
|
1137
|
+
* live on {@link SessionMemoryStores}.
|
|
1138
|
+
*
|
|
1139
|
+
* Node-only (it sits on the filesystem-backed FileStore); like `skills.ts`,
|
|
1140
|
+
* it is reachable through the shimmed `node.ts` entry point.
|
|
1141
|
+
*/
|
|
1142
|
+
var _SessionMemoryStores_instances;
|
|
1143
|
+
var _SessionMemoryStores_client;
|
|
1144
|
+
var _SessionMemoryStores_workdir;
|
|
1145
|
+
var _SessionMemoryStores_syncIntervalMs;
|
|
1146
|
+
var _SessionMemoryStores_syncDeletions;
|
|
1147
|
+
var _SessionMemoryStores_log;
|
|
1148
|
+
var _SessionMemoryStores_lastSyncAt;
|
|
1149
|
+
var _SessionMemoryStores_finished;
|
|
1150
|
+
var _SessionMemoryStores_stores;
|
|
1151
|
+
var _SessionMemoryStores_storeRoot;
|
|
1152
|
+
var _SessionMemoryStores_scanMarker;
|
|
1153
|
+
var _SessionMemoryStores_syncStore;
|
|
1154
|
+
var _SessionMemoryStores_flushStore;
|
|
1155
|
+
var _SessionMemoryStores_recover;
|
|
1156
|
+
var _SessionMemoryStores_stampAndPull;
|
|
1157
|
+
var _SessionMemoryStores_syncPath;
|
|
1158
|
+
var _SessionMemoryStores_removeLocal;
|
|
1159
|
+
var _SessionMemoryStores_write;
|
|
1160
|
+
var _SessionMemoryStores_pullAll;
|
|
1161
|
+
var _SessionMemoryStores_uploadAll;
|
|
1162
|
+
var _SessionMemoryStores_listMemories;
|
|
1163
|
+
var _SessionMemoryStores_upload;
|
|
1164
|
+
var _SessionMemoryStores_corroboratedDelete;
|
|
1165
|
+
var _SessionMemoryStores_deleteRemote;
|
|
1166
|
+
/**
|
|
1167
|
+
* Time bound the worker puts on each teardown pass — the final
|
|
1168
|
+
* {@link SessionMemoryStores.finish}, then {@link SessionMemoryStores.flushWrites} —
|
|
1169
|
+
* so a slow server cannot stall teardown.
|
|
1170
|
+
*/
|
|
1171
|
+
const MEMORY_FLUSH_TIMEOUT_MS = 3e4;
|
|
1172
|
+
/**
|
|
1173
|
+
* Marker file stamped into every store folder; a sync trusts the folder only
|
|
1174
|
+
* when it matches. Never itself syncs.
|
|
1175
|
+
*/
|
|
1176
|
+
const MARKER_PATH = ".anthropic-memory-store";
|
|
1177
|
+
const MARKER_VERSION = 1;
|
|
1178
|
+
function markerSha(memoryStoreId) {
|
|
1179
|
+
return node_crypto.createHash("sha256").update(`version ${MARKER_VERSION}\n${memoryStoreId}`, "utf-8").digest("hex");
|
|
1180
|
+
}
|
|
1181
|
+
/** How long a file must stay missing locally before its server delete goes out. */
|
|
1182
|
+
const DELETE_CORROBORATION_MS = 3e4;
|
|
1183
|
+
/**
|
|
1184
|
+
* Page sizes for memory listings — the API's maximum per view: `basic` pages
|
|
1185
|
+
* carry up to 100 items, `full` pages are capped by the server.
|
|
1186
|
+
*/
|
|
1187
|
+
const LIST_PAGE_SIZE = 100;
|
|
1188
|
+
const FULL_LIST_PAGE_SIZE = 20;
|
|
1189
|
+
/**
|
|
1190
|
+
* How many single-memory content fetches may be in flight at once during one
|
|
1191
|
+
* store's pull pass. A sync rarely pulls more than a handful of memories, so
|
|
1192
|
+
* a higher cap buys nothing in the common case.
|
|
1193
|
+
*/
|
|
1194
|
+
const FETCH_CONCURRENCY = 16;
|
|
1195
|
+
/**
|
|
1196
|
+
* How many uploads one store's flush keeps in flight. At ~0.3s per upload,
|
|
1197
|
+
* 32 clears the server's 2000-memories-per-store cap inside
|
|
1198
|
+
* {@link MEMORY_FLUSH_TIMEOUT_MS}.
|
|
1199
|
+
*/
|
|
1200
|
+
const UPLOAD_CONCURRENCY = 32;
|
|
1201
|
+
/**
|
|
1202
|
+
* Per-sync remote-delete cap bounds. The floor lets a small store's
|
|
1203
|
+
* deletes clear in one pass; the ceiling caps damage on large ones.
|
|
1204
|
+
*/
|
|
1205
|
+
const DELETE_CAP_FLOOR = 8;
|
|
1206
|
+
const DELETE_CAP_CEILING = 50;
|
|
1207
|
+
/**
|
|
1208
|
+
* A session's memory stores could not be mounted.
|
|
1209
|
+
*
|
|
1210
|
+
* Thrown by {@link SessionMemoryStores.download} when a store cannot be
|
|
1211
|
+
* materialised on disk, and by the environment worker when a work item for a
|
|
1212
|
+
* session that has memory stores carried no sessions token to reach them with.
|
|
1213
|
+
*/
|
|
1214
|
+
var SessionMemoryError = class extends require_sdk.AnthropicError {
|
|
1215
|
+
constructor(message, cause) {
|
|
1216
|
+
super(message);
|
|
1217
|
+
this.name = "SessionMemoryError";
|
|
1218
|
+
if (cause !== undefined) this.cause = cause;
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
/** One sync's remote-delete gate and counters. */
|
|
1222
|
+
var DeletePass = class {
|
|
1223
|
+
constructor(mode, cap, waiveWindow) {
|
|
1224
|
+
this.mode = mode;
|
|
1225
|
+
this.cap = cap;
|
|
1226
|
+
this.waiveWindow = waiveWindow;
|
|
1227
|
+
this.attempted = 0;
|
|
1228
|
+
this.capped = 0;
|
|
1229
|
+
this.suppressed = 0;
|
|
1230
|
+
}
|
|
1231
|
+
takeSlot() {
|
|
1232
|
+
if (this.attempted >= this.cap) {
|
|
1233
|
+
this.capped++;
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
this.attempted++;
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
/**
|
|
1241
|
+
* The memory stores attached to one session, materialised on disk.
|
|
1242
|
+
*
|
|
1243
|
+
* {@link SessionMemoryStores.download} opens a {@link LocalFileStore} at each
|
|
1244
|
+
* attached store's directory (its `mount_path`, or a workdir fallback — see
|
|
1245
|
+
* {@link SessionMemoryStores.download}), pulls its memories, and records each
|
|
1246
|
+
* one's `content_sha256` as the sync baseline. Each sync
|
|
1247
|
+
* ({@link SessionMemoryStores.syncIfDue} on the worker's cadence,
|
|
1248
|
+
* {@link SessionMemoryStores.finish} once at the end) reconciles disk against
|
|
1249
|
+
* server, per store and per path:
|
|
1250
|
+
*
|
|
1251
|
+
* - a memory changed only remotely is written to disk;
|
|
1252
|
+
* - a file changed only locally is uploaded — an update with a
|
|
1253
|
+
* `content_sha256` precondition, or a create for a new file;
|
|
1254
|
+
* - a file changed on both sides logs a warning and takes the server version;
|
|
1255
|
+
* - a file the server refuses (too large, invalid content) is skipped —
|
|
1256
|
+
* warned once and retried only after the file changes; other files keep
|
|
1257
|
+
* syncing;
|
|
1258
|
+
* - a file deleted locally is deleted on the server after a delay and a
|
|
1259
|
+
* re-check — never on the first sync that notices, and only up to a
|
|
1260
|
+
* per-sync cap. `syncDeletions` gates it;
|
|
1261
|
+
* - a memory deleted on the server is deleted on disk — unless the local
|
|
1262
|
+
* file holds un-pushed edits: a writable store re-creates the memory
|
|
1263
|
+
* from the file, a read-only one keeps the file unsynced;
|
|
1264
|
+
* - a store attached read-only pulls but never pushes.
|
|
1265
|
+
*
|
|
1266
|
+
* A download pulls the whole store, so it lists with content included. The
|
|
1267
|
+
* recurring syncs instead run two phases: a content-free listing (paths and
|
|
1268
|
+
* shas) drives the merge decisions, then only the memories actually being
|
|
1269
|
+
* written to disk are fetched, a bounded number at a time. A sync that finds
|
|
1270
|
+
* nothing changed moves no content at all.
|
|
1271
|
+
*
|
|
1272
|
+
* A file whose write to disk failed is never in the baseline, so its absence
|
|
1273
|
+
* reads as a failed download — it is pulled again, never deleted. A write
|
|
1274
|
+
* never re-creates a store folder that vanished mid-sync: it fails, and the
|
|
1275
|
+
* next sync's scan finds whatever is at the path by then — nothing
|
|
1276
|
+
* (re-downloaded) or someone else's files (left alone) — under the rules
|
|
1277
|
+
* below.
|
|
1278
|
+
*
|
|
1279
|
+
* A store folder that loses its {@link MARKER_PATH} marker, is emptied,
|
|
1280
|
+
* or vanishes is re-downloaded rather than treated as a mass local
|
|
1281
|
+
* delete; a folder whose marker names another store is left as found —
|
|
1282
|
+
* nothing pushed, nothing deleted.
|
|
1283
|
+
*
|
|
1284
|
+
* Two things about the store's directory make
|
|
1285
|
+
* {@link SessionMemoryStores.download} refuse the session outright, with
|
|
1286
|
+
* {@link SessionMemoryError}: a `mount_path` that is not a clean absolute
|
|
1287
|
+
* path, and a directory already sitting at that path.
|
|
1288
|
+
*
|
|
1289
|
+
* {@link SessionMemoryStores.download} throws on the first store it cannot
|
|
1290
|
+
* materialise. The syncs never throw: mid-session, one bad store or one bad
|
|
1291
|
+
* file is logged and the rest continue. Instances are not safe for concurrent
|
|
1292
|
+
* use. The worker builds one on its token-scoped sub-client (the memory
|
|
1293
|
+
* endpoints reject the environment key): `syncIfDue` after each tool call,
|
|
1294
|
+
* `finish` once at a clean end, a bounded {@link SessionMemoryStores.flushWrites}
|
|
1295
|
+
* in every teardown, `dispose` last.
|
|
1296
|
+
*/
|
|
1297
|
+
var SessionMemoryStores = class {
|
|
1298
|
+
constructor(client, opts) {
|
|
1299
|
+
_SessionMemoryStores_instances.add(this);
|
|
1300
|
+
_SessionMemoryStores_client.set(this, void 0);
|
|
1301
|
+
_SessionMemoryStores_workdir.set(this, void 0);
|
|
1302
|
+
_SessionMemoryStores_syncIntervalMs.set(this, void 0);
|
|
1303
|
+
_SessionMemoryStores_syncDeletions.set(this, void 0);
|
|
1304
|
+
_SessionMemoryStores_log.set(this, void 0);
|
|
1305
|
+
_SessionMemoryStores_lastSyncAt.set(this, void 0);
|
|
1306
|
+
_SessionMemoryStores_finished.set(this, false);
|
|
1307
|
+
_SessionMemoryStores_stores.set(this, []);
|
|
1308
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_client, client, "f");
|
|
1309
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_workdir, opts.workdir, "f");
|
|
1310
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_syncIntervalMs, opts.syncIntervalMs ?? 15e3, "f");
|
|
1311
|
+
require_sdk.checkMemorySyncInterval(require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_syncIntervalMs, "f"), "syncIntervalMs");
|
|
1312
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_syncDeletions, opts.syncDeletions ?? "enabled", "f");
|
|
1313
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_log, require_sdk.loggerFor(client), "f");
|
|
1314
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_lastSyncAt, Date.now(), "f");
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Every attached store's root directory.
|
|
1318
|
+
*
|
|
1319
|
+
* The worker lists these as the file tools' allowed roots so a store
|
|
1320
|
+
* mounted outside the workdir stays reachable.
|
|
1321
|
+
*/
|
|
1322
|
+
get roots() {
|
|
1323
|
+
return require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_stores, "f").map((s) => s.files.root().path);
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Root directories of stores attached read-only.
|
|
1327
|
+
*
|
|
1328
|
+
* The file tools consult this to refuse writes into read-only stores.
|
|
1329
|
+
*/
|
|
1330
|
+
get readOnlyRoots() {
|
|
1331
|
+
return require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_stores, "f").filter((s) => s.readOnly).map((s) => s.files.root().path);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Download every attached store's memories to disk.
|
|
1335
|
+
*
|
|
1336
|
+
* `session` arrives already fetched — one snapshot shared with the skills
|
|
1337
|
+
* download, so the two cannot disagree about the resources.
|
|
1338
|
+
*/
|
|
1339
|
+
async download(session) {
|
|
1340
|
+
for (const resource of session.resources) {
|
|
1341
|
+
if (resource.type !== "memory_store") continue;
|
|
1342
|
+
const root = require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_storeRoot).call(this, resource);
|
|
1343
|
+
let store;
|
|
1344
|
+
try {
|
|
1345
|
+
store = {
|
|
1346
|
+
memoryStoreId: resource.memory_store_id,
|
|
1347
|
+
files: await LocalFileStore.open(root, { utf8: true }),
|
|
1348
|
+
readOnly: resource.access === "read_only",
|
|
1349
|
+
baseline: new Map(),
|
|
1350
|
+
refusedShas: new Map(),
|
|
1351
|
+
pendingDeletes: new Map()
|
|
1352
|
+
};
|
|
1353
|
+
if (!store.files.root().removedOnDispose) {
|
|
1354
|
+
throw new SessionMemoryError(`something already exists at the memory store's path: ${root} ` + `(memory_store_id=${resource.memory_store_id}); ` + "it must not exist when the session starts");
|
|
1355
|
+
}
|
|
1356
|
+
try {
|
|
1357
|
+
await store.files.createRoot();
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
if (!isErrno(e)) throw e;
|
|
1360
|
+
throw new SessionMemoryError(`cannot create the memory store's folder: ${root} ` + `(memory_store_id=${resource.memory_store_id}): ${e}; ` + "the worker host must make this mount path writable", e);
|
|
1361
|
+
}
|
|
1362
|
+
await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_stampAndPull).call(this, store);
|
|
1363
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").info("downloaded memories", {
|
|
1364
|
+
count: store.baseline.size,
|
|
1365
|
+
memory_store_id: store.memoryStoreId,
|
|
1366
|
+
dest: store.files.root().path
|
|
1367
|
+
});
|
|
1368
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_stores, "f").push(store);
|
|
1369
|
+
} catch (e) {
|
|
1370
|
+
if (store) await store.files.dispose().catch(() => {});
|
|
1371
|
+
if (e instanceof SessionMemoryError) throw e;
|
|
1372
|
+
throw new SessionMemoryError(`failed to download memory store memory_store_id=${resource.memory_store_id}: ${e}`, e);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_lastSyncAt, Date.now(), "f");
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* The session's last sync — skips the delete wait, so calling it twice
|
|
1379
|
+
* would undo the protection; it throws instead.
|
|
1380
|
+
*/
|
|
1381
|
+
async finish() {
|
|
1382
|
+
if (require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_finished, "f")) {
|
|
1383
|
+
throw new require_sdk.AnthropicError("finish() was already called: it is the session's last sync and runs once");
|
|
1384
|
+
}
|
|
1385
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_finished, true, "f");
|
|
1386
|
+
await this.syncAll(true);
|
|
1387
|
+
}
|
|
1388
|
+
/** @internal — reconcile every store once; the tests' deterministic driver */
|
|
1389
|
+
async syncAll(final) {
|
|
1390
|
+
await Promise.all(require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_stores, "f").map((store) => require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_syncStore).call(this, store, final)));
|
|
1391
|
+
require_sdk.__classPrivateFieldSet(this, _SessionMemoryStores_lastSyncAt, Date.now(), "f");
|
|
1392
|
+
}
|
|
1393
|
+
/** Sync when `syncIntervalMs` has elapsed since the last one. Never throws. */
|
|
1394
|
+
async syncIfDue() {
|
|
1395
|
+
if (Date.now() - require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_lastSyncAt, "f") < require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_syncIntervalMs, "f")) return;
|
|
1396
|
+
await this.syncAll(false);
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Upload new and changed files; send no deletes and pull nothing.
|
|
1400
|
+
*
|
|
1401
|
+
* The push-only rescue pass for a session ending on an error or
|
|
1402
|
+
* cancel — best-effort, bounded by the caller: once `signal` aborts no
|
|
1403
|
+
* further upload starts, each store cut off part-way logs how many
|
|
1404
|
+
* changed files it had not finished uploading, and this resolves without
|
|
1405
|
+
* waiting for requests already in flight. Each store uploads up to
|
|
1406
|
+
* {@link UPLOAD_CONCURRENCY} files at a time. Skips read-only stores,
|
|
1407
|
+
* refused files, files the server already holds, and folders that fail
|
|
1408
|
+
* the marker check. Never throws.
|
|
1409
|
+
*/
|
|
1410
|
+
async flushWrites(signal) {
|
|
1411
|
+
await Promise.all(require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_stores, "f").map((store) => require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_flushStore).call(this, store, signal)));
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Remove every store directory that {@link SessionMemoryStores.download}
|
|
1415
|
+
* created. Pre-existing directories are left alone — that is
|
|
1416
|
+
* {@link FileStore.dispose}'s own rule. A folder that fails the marker
|
|
1417
|
+
* check is kept too — sync left it as found, so must dispose.
|
|
1418
|
+
*/
|
|
1419
|
+
async dispose() {
|
|
1420
|
+
for (const store of require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_stores, "f")) {
|
|
1421
|
+
const root = store.files.root();
|
|
1422
|
+
try {
|
|
1423
|
+
const scan = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_scanMarker).call(this, store);
|
|
1424
|
+
if (!scan.markerOk && Object.keys(scan.files).length > 0) {
|
|
1425
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn(`${scan.distrustReason}; leaving the memory store folder on disk`, {
|
|
1426
|
+
root: root.path,
|
|
1427
|
+
memory_store_id: store.memoryStoreId
|
|
1428
|
+
});
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
await store.files.dispose();
|
|
1432
|
+
} catch (e) {
|
|
1433
|
+
if (!(e instanceof FileStoreError) && !isErrno(e)) throw e;
|
|
1434
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("failed to remove the memory store folder", {
|
|
1435
|
+
root: store.files.root().path,
|
|
1436
|
+
memory_store_id: store.memoryStoreId,
|
|
1437
|
+
error: String(e)
|
|
1438
|
+
});
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (root.removedOnDispose) {
|
|
1442
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").info("removed memory store dir", {
|
|
1443
|
+
dest: root.path,
|
|
1444
|
+
memory_store_id: store.memoryStoreId
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
_SessionMemoryStores_client = new WeakMap(), _SessionMemoryStores_workdir = new WeakMap(), _SessionMemoryStores_syncIntervalMs = new WeakMap(), _SessionMemoryStores_syncDeletions = new WeakMap(), _SessionMemoryStores_log = new WeakMap(), _SessionMemoryStores_lastSyncAt = new WeakMap(), _SessionMemoryStores_finished = new WeakMap(), _SessionMemoryStores_stores = new WeakMap(), _SessionMemoryStores_instances = new WeakSet(), _SessionMemoryStores_storeRoot = function _SessionMemoryStores_storeRoot(resource) {
|
|
1451
|
+
if (resource.mount_path) {
|
|
1452
|
+
if (!isPathLegal(resource.mount_path)) {
|
|
1453
|
+
throw new SessionMemoryError(`memory store mount_path is not a clean absolute path: ${JSON.stringify(resource.mount_path)} ` + `(memory_store_id=${resource.memory_store_id})`);
|
|
1454
|
+
}
|
|
1455
|
+
return resource.mount_path;
|
|
1456
|
+
}
|
|
1457
|
+
return node_path.join(require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_workdir, "f"), "memory", resource.name || resource.memory_store_id);
|
|
1458
|
+
}, _SessionMemoryStores_scanMarker = async function _SessionMemoryStores_scanMarker(store) {
|
|
1459
|
+
const local = await store.files.hashtree();
|
|
1460
|
+
const marker = local[MARKER_PATH];
|
|
1461
|
+
delete local[MARKER_PATH];
|
|
1462
|
+
if (marker === markerSha(store.memoryStoreId)) {
|
|
1463
|
+
return {
|
|
1464
|
+
files: local,
|
|
1465
|
+
markerOk: true,
|
|
1466
|
+
distrustReason: null
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1470
|
+
files: local,
|
|
1471
|
+
markerOk: false,
|
|
1472
|
+
distrustReason: marker !== undefined ? "the marker file does not match this store" : "the marker file is gone"
|
|
1473
|
+
};
|
|
1474
|
+
}, _SessionMemoryStores_syncStore = async function _SessionMemoryStores_syncStore(store, final) {
|
|
1475
|
+
try {
|
|
1476
|
+
const scan = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_scanMarker).call(this, store);
|
|
1477
|
+
const local = scan.files;
|
|
1478
|
+
if (!scan.markerOk) {
|
|
1479
|
+
if (Object.keys(local).length > 0) {
|
|
1480
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn(`${scan.distrustReason}; leaving the memory store folder as found and not syncing`, {
|
|
1481
|
+
root: store.files.root().path,
|
|
1482
|
+
memory_store_id: store.memoryStoreId
|
|
1483
|
+
});
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_recover).call(this, store, "the folder or its marker is gone");
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (Object.keys(local).length === 0 && store.baseline.size > 1) {
|
|
1490
|
+
await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_recover).call(this, store, "every memory file is gone at once");
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const remote = new Map();
|
|
1494
|
+
for await (const [rel, item] of require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_listMemories).call(this, store.memoryStoreId)) {
|
|
1495
|
+
remote.set(rel, item);
|
|
1496
|
+
}
|
|
1497
|
+
const deletes = new DeletePass(require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_syncDeletions, "f"), Math.max(DELETE_CAP_FLOOR, Math.min(DELETE_CAP_CEILING, Math.floor(store.baseline.size / 4))), final);
|
|
1498
|
+
const pulls = [];
|
|
1499
|
+
const baseline = new Map();
|
|
1500
|
+
const paths = [...new Set([
|
|
1501
|
+
...remote.keys(),
|
|
1502
|
+
...Object.keys(local),
|
|
1503
|
+
...store.baseline.keys()
|
|
1504
|
+
])].sort();
|
|
1505
|
+
for (const rel of paths) {
|
|
1506
|
+
const remoteItem = remote.get(rel);
|
|
1507
|
+
const localSha = local[rel];
|
|
1508
|
+
const baseSha = store.baseline.get(rel);
|
|
1509
|
+
let sha;
|
|
1510
|
+
if (localSha === undefined && baseSha !== undefined && remoteItem !== undefined && remoteItem.content_sha256 === baseSha && !store.readOnly) {
|
|
1511
|
+
sha = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_corroboratedDelete).call(this, store, rel, remoteItem, baseSha, deletes);
|
|
1512
|
+
} else {
|
|
1513
|
+
sha = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_syncPath).call(this, store, rel, remoteItem, localSha, pulls);
|
|
1514
|
+
}
|
|
1515
|
+
if (sha !== undefined) baseline.set(rel, sha);
|
|
1516
|
+
}
|
|
1517
|
+
store.baseline = baseline;
|
|
1518
|
+
await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_pullAll).call(this, store, pulls);
|
|
1519
|
+
if (deletes.suppressed > 0) {
|
|
1520
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").debug("remote deletes are disabled; locally deleted memories stay on the server", {
|
|
1521
|
+
count: deletes.suppressed,
|
|
1522
|
+
memory_store_id: store.memoryStoreId
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
if (deletes.capped > 0) {
|
|
1526
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn(`delete cap reached: ${deletes.mode === "log_only" ? "would send" : "sent"} ` + `${deletes.attempted} deletes, held ${deletes.capped} for later syncs`, { memory_store_id: store.memoryStoreId });
|
|
1527
|
+
}
|
|
1528
|
+
} catch (e) {
|
|
1529
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory sync failed", {
|
|
1530
|
+
memory_store_id: store.memoryStoreId,
|
|
1531
|
+
error: String(e)
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}, _SessionMemoryStores_flushStore = async function _SessionMemoryStores_flushStore(store, signal) {
|
|
1535
|
+
const dirty = new Map();
|
|
1536
|
+
const unsent = new Set();
|
|
1537
|
+
const push = async () => {
|
|
1538
|
+
if (store.readOnly) return;
|
|
1539
|
+
const scan = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_scanMarker).call(this, store);
|
|
1540
|
+
if (!scan.markerOk) {
|
|
1541
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn(`${scan.distrustReason}; not uploading anything from the memory store folder`, {
|
|
1542
|
+
root: store.files.root().path,
|
|
1543
|
+
memory_store_id: store.memoryStoreId
|
|
1544
|
+
});
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
for (const [rel, sha] of Object.entries(scan.files)) {
|
|
1548
|
+
if (sha !== store.baseline.get(rel) && store.refusedShas.get(rel) !== sha) {
|
|
1549
|
+
dirty.set(rel, sha);
|
|
1550
|
+
unsent.add(rel);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (dirty.size === 0 || signal?.aborted) return;
|
|
1554
|
+
const remote = new Map();
|
|
1555
|
+
for await (const [rel, item] of require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_listMemories).call(this, store.memoryStoreId)) {
|
|
1556
|
+
if (signal?.aborted) return;
|
|
1557
|
+
remote.set(rel, item);
|
|
1558
|
+
}
|
|
1559
|
+
const uploads = [];
|
|
1560
|
+
for (const rel of [...dirty.keys()].sort()) {
|
|
1561
|
+
const localSha = dirty.get(rel);
|
|
1562
|
+
const baseSha = store.baseline.get(rel);
|
|
1563
|
+
const existing = remote.get(rel);
|
|
1564
|
+
if (existing !== undefined && existing.content_sha256 === localSha) {
|
|
1565
|
+
store.baseline.set(rel, existing.content_sha256);
|
|
1566
|
+
unsent.delete(rel);
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
if (existing !== undefined && existing.content_sha256 !== baseSha) {
|
|
1570
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory changed both locally and remotely; the flush leaves the remote version", {
|
|
1571
|
+
path: rel,
|
|
1572
|
+
memory_store_id: store.memoryStoreId
|
|
1573
|
+
});
|
|
1574
|
+
unsent.delete(rel);
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
uploads.push([
|
|
1578
|
+
rel,
|
|
1579
|
+
localSha,
|
|
1580
|
+
existing
|
|
1581
|
+
]);
|
|
1582
|
+
}
|
|
1583
|
+
await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_uploadAll).call(this, store, uploads, unsent, signal);
|
|
1584
|
+
};
|
|
1585
|
+
try {
|
|
1586
|
+
await settledOrAborted(push(), signal);
|
|
1587
|
+
if (signal?.aborted && unsent.size > 0) {
|
|
1588
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn(`memory flush cut off part-way; ${unsent.size} of ${dirty.size} changed files had not finished uploading`, { memory_store_id: store.memoryStoreId });
|
|
1589
|
+
}
|
|
1590
|
+
} catch (e) {
|
|
1591
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory flush failed", {
|
|
1592
|
+
memory_store_id: store.memoryStoreId,
|
|
1593
|
+
error: String(e)
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
}, _SessionMemoryStores_recover = async function _SessionMemoryStores_recover(store, reason) {
|
|
1597
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn(`${reason}; re-downloading the memory store folder instead of syncing`, {
|
|
1598
|
+
root: store.files.root().path,
|
|
1599
|
+
memory_store_id: store.memoryStoreId
|
|
1600
|
+
});
|
|
1601
|
+
await store.files.createRoot();
|
|
1602
|
+
await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_stampAndPull).call(this, store);
|
|
1603
|
+
}, _SessionMemoryStores_stampAndPull = async function _SessionMemoryStores_stampAndPull(store) {
|
|
1604
|
+
store.baseline = new Map();
|
|
1605
|
+
store.pendingDeletes.clear();
|
|
1606
|
+
await store.files.put(MARKER_PATH, `version ${MARKER_VERSION}\n${store.memoryStoreId}`);
|
|
1607
|
+
for await (const [rel, item] of require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_listMemories).call(this, store.memoryStoreId, "full")) {
|
|
1608
|
+
if (await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_write).call(this, store, rel, item.content ?? "")) {
|
|
1609
|
+
store.baseline.set(rel, item.content_sha256);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}, _SessionMemoryStores_syncPath = async function _SessionMemoryStores_syncPath(store, rel, remote, localSha, pulls) {
|
|
1613
|
+
const baseSha = store.baseline.get(rel);
|
|
1614
|
+
if (localSha !== undefined) {
|
|
1615
|
+
store.pendingDeletes.delete(rel);
|
|
1616
|
+
}
|
|
1617
|
+
if (!remote) {
|
|
1618
|
+
if (localSha === undefined) {
|
|
1619
|
+
store.pendingDeletes.delete(rel);
|
|
1620
|
+
return undefined;
|
|
1621
|
+
}
|
|
1622
|
+
if (baseSha !== undefined) {
|
|
1623
|
+
if (localSha === baseSha) {
|
|
1624
|
+
const fresh = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_removeLocal).call(this, store, rel, baseSha);
|
|
1625
|
+
if (fresh === undefined) return undefined;
|
|
1626
|
+
if (fresh === baseSha) return baseSha;
|
|
1627
|
+
localSha = fresh;
|
|
1628
|
+
}
|
|
1629
|
+
if (store.readOnly) {
|
|
1630
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory deleted remotely but edited locally; keeping the file, " + "which a read-only store cannot push", {
|
|
1631
|
+
path: rel,
|
|
1632
|
+
memory_store_id: store.memoryStoreId
|
|
1633
|
+
});
|
|
1634
|
+
} else if (store.refusedShas.get(rel) !== localSha) {
|
|
1635
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").info("memory deleted remotely but edited locally; re-creating it from the file", {
|
|
1636
|
+
path: rel,
|
|
1637
|
+
memory_store_id: store.memoryStoreId
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (store.readOnly) return undefined;
|
|
1642
|
+
if (store.refusedShas.get(rel) === localSha) return undefined;
|
|
1643
|
+
return await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_upload).call(this, store, rel, localSha, undefined);
|
|
1644
|
+
}
|
|
1645
|
+
const remoteSha = remote.content_sha256;
|
|
1646
|
+
const remoteChanged = remoteSha !== baseSha;
|
|
1647
|
+
const locallyEdited = localSha !== undefined && localSha !== baseSha && localSha !== remoteSha;
|
|
1648
|
+
const localChanged = !store.readOnly && locallyEdited;
|
|
1649
|
+
if (localSha === undefined && baseSha !== undefined) {
|
|
1650
|
+
if (remoteChanged) {
|
|
1651
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory deleted locally but changed remotely; restoring the remote version", {
|
|
1652
|
+
path: rel,
|
|
1653
|
+
memory_store_id: store.memoryStoreId
|
|
1654
|
+
});
|
|
1655
|
+
store.pendingDeletes.delete(rel);
|
|
1656
|
+
pulls.push([rel, remote]);
|
|
1657
|
+
}
|
|
1658
|
+
return baseSha;
|
|
1659
|
+
}
|
|
1660
|
+
if (remoteChanged) {
|
|
1661
|
+
if (localSha === remoteSha) return remoteSha;
|
|
1662
|
+
if (locallyEdited) {
|
|
1663
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory changed both locally and remotely; keeping the remote version", {
|
|
1664
|
+
path: rel,
|
|
1665
|
+
memory_store_id: store.memoryStoreId
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
pulls.push([rel, remote]);
|
|
1669
|
+
return baseSha;
|
|
1670
|
+
}
|
|
1671
|
+
if (localChanged) {
|
|
1672
|
+
if (store.refusedShas.get(rel) === localSha) return remoteSha;
|
|
1673
|
+
return await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_upload).call(this, store, rel, localSha, remote) ?? remoteSha;
|
|
1674
|
+
}
|
|
1675
|
+
return remoteSha;
|
|
1676
|
+
}, _SessionMemoryStores_removeLocal = async function _SessionMemoryStores_removeLocal(store, rel, expectSha) {
|
|
1677
|
+
let freshSha;
|
|
1678
|
+
try {
|
|
1679
|
+
freshSha = await store.files.hashFile(rel);
|
|
1680
|
+
} catch (e) {
|
|
1681
|
+
if (!(e instanceof FileStoreError) && !isErrno(e)) throw e;
|
|
1682
|
+
return expectSha;
|
|
1683
|
+
}
|
|
1684
|
+
if (freshSha === null) return undefined;
|
|
1685
|
+
if (freshSha !== expectSha) return freshSha;
|
|
1686
|
+
try {
|
|
1687
|
+
await store.files.remove(rel);
|
|
1688
|
+
} catch (e) {
|
|
1689
|
+
if (!(e instanceof FileStoreError) && !isErrno(e)) throw e;
|
|
1690
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("failed to remove memory deleted remotely", {
|
|
1691
|
+
path: rel,
|
|
1692
|
+
memory_store_id: store.memoryStoreId,
|
|
1693
|
+
error: String(e)
|
|
1694
|
+
});
|
|
1695
|
+
return expectSha;
|
|
1696
|
+
}
|
|
1697
|
+
return undefined;
|
|
1698
|
+
}, _SessionMemoryStores_write = async function _SessionMemoryStores_write(store, rel, content) {
|
|
1699
|
+
try {
|
|
1700
|
+
await store.files.put(rel, content);
|
|
1701
|
+
} catch (e) {
|
|
1702
|
+
if (!(e instanceof FileStoreError) && !isErrno(e)) throw e;
|
|
1703
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("failed to write memory", {
|
|
1704
|
+
path: rel,
|
|
1705
|
+
memory_store_id: store.memoryStoreId,
|
|
1706
|
+
error: String(e)
|
|
1707
|
+
});
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
return true;
|
|
1711
|
+
}, _SessionMemoryStores_pullAll = async function _SessionMemoryStores_pullAll(store, pulls) {
|
|
1712
|
+
if (pulls.length === 0) return;
|
|
1713
|
+
const pullOne = async (rel, listed) => {
|
|
1714
|
+
let item;
|
|
1715
|
+
try {
|
|
1716
|
+
item = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_client, "f").beta.memoryStores.memories.retrieve(listed.id, {
|
|
1717
|
+
memory_store_id: store.memoryStoreId,
|
|
1718
|
+
view: "full"
|
|
1719
|
+
});
|
|
1720
|
+
} catch (e) {
|
|
1721
|
+
if (require_sdk.isStatus(e, 404)) return;
|
|
1722
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("failed to fetch memory content", {
|
|
1723
|
+
path: rel,
|
|
1724
|
+
memory_store_id: store.memoryStoreId,
|
|
1725
|
+
error: String(e)
|
|
1726
|
+
});
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
if (await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_write).call(this, store, rel, item.content ?? "")) {
|
|
1730
|
+
store.baseline.set(rel, item.content_sha256);
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
const queue = pulls[Symbol.iterator]();
|
|
1734
|
+
const worker = async () => {
|
|
1735
|
+
for (const [rel, listed] of queue) await pullOne(rel, listed);
|
|
1736
|
+
};
|
|
1737
|
+
await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, pulls.length) }, worker));
|
|
1738
|
+
}, _SessionMemoryStores_uploadAll = async function _SessionMemoryStores_uploadAll(store, uploads, unsent, signal) {
|
|
1739
|
+
const queue = uploads[Symbol.iterator]();
|
|
1740
|
+
const worker = async () => {
|
|
1741
|
+
for (const [rel, localSha, existing] of queue) {
|
|
1742
|
+
if (signal?.aborted) return;
|
|
1743
|
+
const sha = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_upload).call(this, store, rel, localSha, existing);
|
|
1744
|
+
unsent.delete(rel);
|
|
1745
|
+
if (sha !== undefined) store.baseline.set(rel, sha);
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
await Promise.all(Array.from({ length: Math.min(32, uploads.length) }, worker));
|
|
1749
|
+
}, _SessionMemoryStores_listMemories = async function* _SessionMemoryStores_listMemories(memoryStoreId, view = "basic") {
|
|
1750
|
+
const limit = view === "basic" ? LIST_PAGE_SIZE : FULL_LIST_PAGE_SIZE;
|
|
1751
|
+
for await (const item of require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_client, "f").beta.memoryStores.memories.list(memoryStoreId, {
|
|
1752
|
+
view,
|
|
1753
|
+
limit
|
|
1754
|
+
})) {
|
|
1755
|
+
if (item.type !== "memory") continue;
|
|
1756
|
+
const rel = item.path.replace(/^\/+/, "");
|
|
1757
|
+
if (rel === ".anthropic-memory-store") {
|
|
1758
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("the server listed the reserved marker path; skipping", {
|
|
1759
|
+
path: item.path,
|
|
1760
|
+
memory_store_id: memoryStoreId
|
|
1761
|
+
});
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
yield [rel, item];
|
|
1765
|
+
}
|
|
1766
|
+
}, _SessionMemoryStores_upload = async function _SessionMemoryStores_upload(store, rel, localSha, existing) {
|
|
1767
|
+
try {
|
|
1768
|
+
const data = await store.files.get(rel);
|
|
1769
|
+
if (data === null) return undefined;
|
|
1770
|
+
const content = require_sdk.decodeUTF8(data);
|
|
1771
|
+
const item = existing ? await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_client, "f").beta.memoryStores.memories.update(existing.id, {
|
|
1772
|
+
memory_store_id: store.memoryStoreId,
|
|
1773
|
+
content,
|
|
1774
|
+
precondition: {
|
|
1775
|
+
type: "content_sha256",
|
|
1776
|
+
content_sha256: existing.content_sha256
|
|
1777
|
+
}
|
|
1778
|
+
}) : await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_client, "f").beta.memoryStores.memories.create(store.memoryStoreId, {
|
|
1779
|
+
path: "/" + rel,
|
|
1780
|
+
content
|
|
1781
|
+
});
|
|
1782
|
+
store.refusedShas.delete(rel);
|
|
1783
|
+
return item.content_sha256;
|
|
1784
|
+
} catch (e) {
|
|
1785
|
+
if (existing && require_sdk.isStatus(e, 404)) {
|
|
1786
|
+
return await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_upload).call(this, store, rel, localSha, undefined);
|
|
1787
|
+
}
|
|
1788
|
+
const permanent = e instanceof FileStoreError || require_sdk.isStatus(e, 400) || require_sdk.isStatus(e, 413);
|
|
1789
|
+
if (existing && require_sdk.isStatus(e, 409)) {
|
|
1790
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory changed both locally and remotely; the upload was refused and the local edit loses", {
|
|
1791
|
+
path: rel,
|
|
1792
|
+
memory_store_id: store.memoryStoreId
|
|
1793
|
+
});
|
|
1794
|
+
} else if (permanent && localSha !== undefined) {
|
|
1795
|
+
store.refusedShas.set(rel, localSha);
|
|
1796
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("the server rejected this memory file, so it stays un-synced until its content changes", {
|
|
1797
|
+
path: rel,
|
|
1798
|
+
memory_store_id: store.memoryStoreId,
|
|
1799
|
+
rejection: String(e)
|
|
1800
|
+
});
|
|
1801
|
+
} else {
|
|
1802
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("failed to upload memory", {
|
|
1803
|
+
path: rel,
|
|
1804
|
+
memory_store_id: store.memoryStoreId,
|
|
1805
|
+
error: String(e)
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
return undefined;
|
|
1809
|
+
}
|
|
1810
|
+
}, _SessionMemoryStores_corroboratedDelete = async function _SessionMemoryStores_corroboratedDelete(store, rel, remote, baseSha, deletes) {
|
|
1811
|
+
if (deletes.mode === "disabled") {
|
|
1812
|
+
deletes.suppressed++;
|
|
1813
|
+
return baseSha;
|
|
1814
|
+
}
|
|
1815
|
+
let firstAbsent = store.pendingDeletes.get(rel);
|
|
1816
|
+
if (firstAbsent === undefined) {
|
|
1817
|
+
firstAbsent = Date.now();
|
|
1818
|
+
store.pendingDeletes.set(rel, firstAbsent);
|
|
1819
|
+
}
|
|
1820
|
+
if (!deletes.waiveWindow && Date.now() - firstAbsent < 3e4) {
|
|
1821
|
+
return baseSha;
|
|
1822
|
+
}
|
|
1823
|
+
let markerOk;
|
|
1824
|
+
let stillAbsent;
|
|
1825
|
+
try {
|
|
1826
|
+
markerOk = await store.files.hashFile(MARKER_PATH) === markerSha(store.memoryStoreId);
|
|
1827
|
+
stillAbsent = await store.files.hashFile(rel) === null;
|
|
1828
|
+
} catch (e) {
|
|
1829
|
+
if (!(e instanceof FileStoreError) && !isErrno(e)) throw e;
|
|
1830
|
+
markerOk = stillAbsent = false;
|
|
1831
|
+
}
|
|
1832
|
+
if (!markerOk) return baseSha;
|
|
1833
|
+
if (!stillAbsent) {
|
|
1834
|
+
store.pendingDeletes.delete(rel);
|
|
1835
|
+
return baseSha;
|
|
1836
|
+
}
|
|
1837
|
+
if (!deletes.takeSlot()) return baseSha;
|
|
1838
|
+
if (deletes.mode === "log_only") {
|
|
1839
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").info("log-only: sync would delete this memory on the server", {
|
|
1840
|
+
path: rel,
|
|
1841
|
+
memory_store_id: store.memoryStoreId
|
|
1842
|
+
});
|
|
1843
|
+
return baseSha;
|
|
1844
|
+
}
|
|
1845
|
+
const sha = await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_instances, "m", _SessionMemoryStores_deleteRemote).call(this, store, rel, remote, baseSha);
|
|
1846
|
+
if (sha === undefined) {
|
|
1847
|
+
store.pendingDeletes.delete(rel);
|
|
1848
|
+
}
|
|
1849
|
+
return sha;
|
|
1850
|
+
}, _SessionMemoryStores_deleteRemote = async function _SessionMemoryStores_deleteRemote(store, rel, remote, baseSha) {
|
|
1851
|
+
try {
|
|
1852
|
+
await require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_client, "f").beta.memoryStores.memories.delete(remote.id, {
|
|
1853
|
+
memory_store_id: store.memoryStoreId,
|
|
1854
|
+
expected_content_sha256: baseSha
|
|
1855
|
+
});
|
|
1856
|
+
} catch (e) {
|
|
1857
|
+
if (require_sdk.isStatus(e, 404)) return undefined;
|
|
1858
|
+
if (require_sdk.isStatus(e, 409) || require_sdk.isStatus(e, 412)) {
|
|
1859
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("memory deleted locally but changed remotely; keeping the remote version", {
|
|
1860
|
+
path: rel,
|
|
1861
|
+
memory_store_id: store.memoryStoreId
|
|
1862
|
+
});
|
|
1863
|
+
} else {
|
|
1864
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").warn("failed to delete memory", {
|
|
1865
|
+
path: rel,
|
|
1866
|
+
memory_store_id: store.memoryStoreId,
|
|
1867
|
+
error: String(e)
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
return baseSha;
|
|
1871
|
+
}
|
|
1872
|
+
require_sdk.__classPrivateFieldGet(this, _SessionMemoryStores_log, "f").info("propagated local deletion", {
|
|
1873
|
+
path: rel,
|
|
1874
|
+
memory_store_id: store.memoryStoreId
|
|
1875
|
+
});
|
|
1876
|
+
return undefined;
|
|
1877
|
+
};
|
|
1878
|
+
/**
|
|
1879
|
+
* True for a thrown value shaped like a Node filesystem error. Shape-checked,
|
|
1880
|
+
* not `instanceof Error` — fs errors can come from another realm.
|
|
1881
|
+
*/
|
|
1882
|
+
function isErrno(e) {
|
|
1883
|
+
return typeof e === "object" && e !== null && typeof e.code === "string";
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Resolve when `p` settles, or as soon as `signal` aborts. A rejection from
|
|
1887
|
+
* `p` before the abort propagates; one after it is dropped.
|
|
1888
|
+
*/
|
|
1889
|
+
async function settledOrAborted(p, signal) {
|
|
1890
|
+
if (!signal) {
|
|
1891
|
+
await p;
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
let onAbort;
|
|
1895
|
+
const aborted = new Promise((resolve) => {
|
|
1896
|
+
onAbort = resolve;
|
|
1897
|
+
if (signal.aborted) resolve();
|
|
1898
|
+
});
|
|
1899
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1900
|
+
try {
|
|
1901
|
+
await Promise.race([p, aborted]);
|
|
1902
|
+
} finally {
|
|
1903
|
+
signal.removeEventListener("abort", onAbort);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
//#endregion
|
|
1908
|
+
//#region ../../node_modules/.pnpm/@anthropic-ai+sdk@0.124.0_zod@4.5.4/node_modules/@anthropic-ai/sdk/tools/agent-toolset/node.mjs
|
|
1909
|
+
/**
|
|
1910
|
+
* Node implementation of the `agent_toolset_20260401` tools — `bash`, `read`,
|
|
1911
|
+
* `write`, `edit`, `glob`, `grep` — plus the workdir/skills
|
|
1912
|
+
* {@link AgentToolContext}.
|
|
1913
|
+
*
|
|
1914
|
+
* This mirrors `@anthropic-ai/sdk/tools/memory/node`: it is the explicit,
|
|
1915
|
+
* Node-only entry point for these implementations. Importing it pulls in
|
|
1916
|
+
* `node:child_process`, `node:fs`, etc., so it is kept separate from the rest of
|
|
1917
|
+
* the SDK — depending on it is an opt-in.
|
|
1918
|
+
*
|
|
1919
|
+
* **Node 22+ is required** for this module: the `glob` tool uses the native
|
|
1920
|
+
* `fs.glob`, added in Node 22. The rest of the SDK still supports Node 18+; only
|
|
1921
|
+
* the agent toolset has this requirement.
|
|
1922
|
+
*
|
|
1923
|
+
* The result of {@link betaAgentToolset20260401} is a plain `BetaRunnableTool[]`;
|
|
1924
|
+
* hand it to any tool runner — `client.beta.messages.toolRunner({ …, tools })`
|
|
1925
|
+
* for the Messages API, or `client.beta.sessions.events.toolRunner({ …, tools })`
|
|
1926
|
+
* for a managed-agents session:
|
|
1927
|
+
*
|
|
1928
|
+
* ```ts
|
|
1929
|
+
* import { betaAgentToolset20260401 } from '@anthropic-ai/sdk/tools/agent-toolset/node';
|
|
1930
|
+
*
|
|
1931
|
+
* const tools = betaAgentToolset20260401({ workdir: '/work' });
|
|
1932
|
+
* const tools2 = betaAgentToolset20260401({ workdir: '/work' }).filter((t) => t.name !== 'bash');
|
|
1933
|
+
* ```
|
|
1934
|
+
*
|
|
1935
|
+
* Trust model: the file tools confine to `workdir` plus any `allowedRoots`
|
|
1936
|
+
* (symlink-aware) and are safe without a sandbox; `bash` is unrestricted and
|
|
1937
|
+
* should run inside one. See {@link AgentToolContext}.
|
|
1938
|
+
*/
|
|
1939
|
+
var _BashSession_instances;
|
|
1940
|
+
var _BashSession_proc;
|
|
1941
|
+
var _BashSession_buf;
|
|
1942
|
+
var _BashSession_truncated;
|
|
1943
|
+
var _BashSession_closed;
|
|
1944
|
+
var _BashSession_waiting;
|
|
1945
|
+
var _BashSession_append;
|
|
1946
|
+
var _LineRangeCollector_instances;
|
|
1947
|
+
var _LineRangeCollector_filePath;
|
|
1948
|
+
var _LineRangeCollector_startLine;
|
|
1949
|
+
var _LineRangeCollector_endLine;
|
|
1950
|
+
var _LineRangeCollector_start;
|
|
1951
|
+
var _LineRangeCollector_end;
|
|
1952
|
+
var _LineRangeCollector_limit;
|
|
1953
|
+
var _LineRangeCollector_line;
|
|
1954
|
+
var _LineRangeCollector_collected;
|
|
1955
|
+
var _LineRangeCollector_collectedBytes;
|
|
1956
|
+
var _LineRangeCollector_collect;
|
|
1957
|
+
var _LineRangeCollector_overLimitError;
|
|
1958
|
+
const BASH_OUTPUT_LIMIT = 100 * 1024;
|
|
1959
|
+
const BASH_DEFAULT_TIMEOUT_MS = 12e4;
|
|
1960
|
+
const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
1961
|
+
const READ_STREAM_CHUNK_BYTES = 64 * 1024;
|
|
1962
|
+
const NEWLINE = Buffer.from("\n");
|
|
1963
|
+
const GREP_OUTPUT_LIMIT = 100 * 1024;
|
|
1964
|
+
const GREP_MAX_LINE_LENGTH = 2e3;
|
|
1965
|
+
const GLOB_RESULT_LIMIT = 200;
|
|
1966
|
+
/**
|
|
1967
|
+
* A bash command exceeded its `timeoutMs`. Carries the timeout so a caller can
|
|
1968
|
+
* tell it apart from an abort without matching on the message text.
|
|
1969
|
+
*/
|
|
1970
|
+
var BashTimeoutError = class extends require_sdk.AnthropicError {
|
|
1971
|
+
constructor(timeoutMs) {
|
|
1972
|
+
super(`bash command timed out after ${timeoutMs}ms`);
|
|
1973
|
+
this.name = "BashTimeoutError";
|
|
1974
|
+
this.timeoutMs = timeoutMs;
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
1978
|
+
const fsGlob = node_fs_promises.glob;
|
|
1979
|
+
function resolveMaxBytes(configured) {
|
|
1980
|
+
return configured === undefined ? DEFAULT_MAX_FILE_BYTES : configured;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Throw when the deprecated {@link AgentToolContext.unrestrictedPaths} was
|
|
1984
|
+
* passed at all. Nothing else reads that property.
|
|
1985
|
+
*/
|
|
1986
|
+
function rejectUnrestrictedPaths(value) {
|
|
1987
|
+
if (value === undefined) return;
|
|
1988
|
+
throw new require_sdk.AnthropicError("The `unrestrictedPaths` option you passed to the agent toolset (AgentToolContext) is no longer " + "supported. The toolset's file tools (read, write, edit, glob, grep) are now always confined to " + "the working directory plus the directories listed in `allowedRoots`. Remove `unrestrictedPaths` " + "from your context; to let the file tools reach any other directory, add it to `allowedRoots`.");
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Returns the `agent_toolset_20260401` implementations bound to `ctx`. The
|
|
1992
|
+
* result is a plain array of `BetaRunnableTool`; filter or extend it before
|
|
1993
|
+
* handing it to a tool runner:
|
|
1994
|
+
*
|
|
1995
|
+
* ```ts
|
|
1996
|
+
* const tools = [...betaAgentToolset20260401(ctx), myCustomTool];
|
|
1997
|
+
* const tools = betaAgentToolset20260401(ctx).filter((t) => t.name !== 'grep');
|
|
1998
|
+
* ```
|
|
1999
|
+
*
|
|
2000
|
+
* Concurrency note: `client.beta.sessions.events.toolRunner` dispatches a
|
|
2001
|
+
* session's tool calls serially (the sessions API delivers one `agent.tool_use`
|
|
2002
|
+
* at a time). `client.beta.messages.toolRunner` runs a turn's `tool.run` calls
|
|
2003
|
+
* via `Promise.all`. The toolset below is safe under either model —
|
|
2004
|
+
* {@link betaBashTool} serializes its persistent shell internally and the FS
|
|
2005
|
+
* tools are independent per call — but {@link betaEditTool}/{@link betaWriteTool}
|
|
2006
|
+
* cannot synchronize concurrent writes to the *same* file across processes, so a
|
|
2007
|
+
* multi-edit turn touching one path is still subject to inherent FS lost-update
|
|
2008
|
+
* races. Custom tools that close over mutable state should do their own queueing.
|
|
2009
|
+
*/
|
|
2010
|
+
function betaAgentToolset20260401(ctx) {
|
|
2011
|
+
return [
|
|
2012
|
+
betaBashTool(ctx),
|
|
2013
|
+
betaReadTool(ctx),
|
|
2014
|
+
betaWriteTool(ctx),
|
|
2015
|
+
betaEditTool(ctx),
|
|
2016
|
+
betaGlobTool(ctx),
|
|
2017
|
+
betaGrepTool(ctx)
|
|
2018
|
+
];
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Resolve `p` against `ctx.workdir`; reject results outside `ctx.workdir` and
|
|
2022
|
+
* `ctx.allowedRoots`. Absolute and relative inputs go through the same
|
|
2023
|
+
* canonicalise-then-contain check — an absolute path that lands inside a
|
|
2024
|
+
* permitted root is accepted, only paths that resolve *outside* all of them
|
|
2025
|
+
* are rejected. Every symlink in `p` (including the leaf, even a dangling one)
|
|
2026
|
+
* is resolved before the check, and the resolved path is what the tool then
|
|
2027
|
+
* operates on, so a symlink inside the workdir that points outside it can
|
|
2028
|
+
* neither pass the check nor be followed afterwards. See the trust model on
|
|
2029
|
+
* {@link AgentToolContext}.
|
|
2030
|
+
*
|
|
2031
|
+
* Residual TOCTOU: a component could still be swapped for a symlink between this
|
|
2032
|
+
* call and the eventual `fs` operation. Closing that fully needs per-component
|
|
2033
|
+
* `O_NOFOLLOW`/`openat`, which Node does not expose ergonomically; the same
|
|
2034
|
+
* residual exposure exists in `tools/memory/node` and is why a sandbox is still
|
|
2035
|
+
* recommended for the toolset as a whole.
|
|
2036
|
+
*/
|
|
2037
|
+
async function resolvePath(ctx, p) {
|
|
2038
|
+
rejectUnrestrictedPaths(ctx.unrestrictedPaths);
|
|
2039
|
+
return confineToRoot(ctx.workdir, p, { allowedRoots: ctx.allowedRoots ?? [] });
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* The read-only root `target` falls under, or `undefined`. `target` arrives
|
|
2043
|
+
* fully canonicalized (from {@link resolvePath}), so each root is
|
|
2044
|
+
* canonicalized too — a root recorded through a symlinked workdir must still
|
|
2045
|
+
* match the resolved write target.
|
|
2046
|
+
*/
|
|
2047
|
+
function readOnlyRootFor(ctx, target) {
|
|
2048
|
+
return containingRoot(ctx.readOnlyRoots ?? [], target);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Build the environment for the spawned bash shell. The runner process holds
|
|
2052
|
+
* Anthropic credentials in `ANTHROPIC_*` env vars — the API key, the auth token,
|
|
2053
|
+
* and the per-work session token among them. `bash` runs an unrestricted shell,
|
|
2054
|
+
* so any command the agent runs could read those straight out of `process.env`;
|
|
2055
|
+
* strip the whole `ANTHROPIC_*` namespace from the child's environment.
|
|
2056
|
+
* Everything else (PATH, HOME, locale, …) is passed through unchanged.
|
|
2057
|
+
*
|
|
2058
|
+
* Passing an explicit `env` to {@link AgentToolContext} does NOT add to this
|
|
2059
|
+
* default — it FULLY REPLACES it. The provided mapping becomes the entire bash
|
|
2060
|
+
* environment verbatim; nothing here is merged in, so callers who want the
|
|
2061
|
+
* scrubbed process environment plus extras must build that mapping themselves.
|
|
2062
|
+
*/
|
|
2063
|
+
function scrubbedShellEnv() {
|
|
2064
|
+
const env = {};
|
|
2065
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
2066
|
+
if (key.startsWith("ANTHROPIC_")) continue;
|
|
2067
|
+
env[key] = value;
|
|
2068
|
+
}
|
|
2069
|
+
return env;
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* A persistent /bin/bash process. State (cwd, env, background jobs) survives
|
|
2073
|
+
* across exec() calls. Uses pipes rather than a PTY so input is never echoed.
|
|
2074
|
+
*/
|
|
2075
|
+
var BashSession = class {
|
|
2076
|
+
constructor(dir, env = scrubbedShellEnv()) {
|
|
2077
|
+
_BashSession_instances.add(this);
|
|
2078
|
+
_BashSession_proc.set(this, void 0);
|
|
2079
|
+
_BashSession_buf.set(this, "");
|
|
2080
|
+
_BashSession_truncated.set(this, false);
|
|
2081
|
+
_BashSession_closed.set(this, false);
|
|
2082
|
+
_BashSession_waiting.set(this, null);
|
|
2083
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_proc, node_child_process.spawn("/bin/bash", ["--noprofile", "--norc"], {
|
|
2084
|
+
cwd: dir,
|
|
2085
|
+
env: {
|
|
2086
|
+
...env,
|
|
2087
|
+
PS1: "",
|
|
2088
|
+
PS2: "",
|
|
2089
|
+
TERM: "dumb"
|
|
2090
|
+
},
|
|
2091
|
+
stdio: [
|
|
2092
|
+
"pipe",
|
|
2093
|
+
"pipe",
|
|
2094
|
+
"pipe"
|
|
2095
|
+
],
|
|
2096
|
+
detached: true
|
|
2097
|
+
}), "f");
|
|
2098
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stdout.setEncoding("utf8");
|
|
2099
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stderr.setEncoding("utf8");
|
|
2100
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stdout.on("data", (d) => require_sdk.__classPrivateFieldGet(this, _BashSession_instances, "m", _BashSession_append).call(this, d));
|
|
2101
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stderr.on("data", (d) => require_sdk.__classPrivateFieldGet(this, _BashSession_instances, "m", _BashSession_append).call(this, d));
|
|
2102
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").once("close", () => {
|
|
2103
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_closed, true, "f");
|
|
2104
|
+
const w = require_sdk.__classPrivateFieldGet(this, _BashSession_waiting, "f");
|
|
2105
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_waiting, null, "f");
|
|
2106
|
+
w?.resolve();
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
/** Whether the underlying shell process has exited. */
|
|
2110
|
+
get closed() {
|
|
2111
|
+
return require_sdk.__classPrivateFieldGet(this, _BashSession_closed, "f");
|
|
2112
|
+
}
|
|
2113
|
+
async exec(command, opts = {}) {
|
|
2114
|
+
if (require_sdk.__classPrivateFieldGet(this, _BashSession_closed, "f")) {
|
|
2115
|
+
throw new require_sdk.AnthropicError("bash session terminated");
|
|
2116
|
+
}
|
|
2117
|
+
const timeoutMs = opts.timeoutMs ?? BASH_DEFAULT_TIMEOUT_MS;
|
|
2118
|
+
const signal = opts.signal;
|
|
2119
|
+
signal?.throwIfAborted();
|
|
2120
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_buf, "", "f");
|
|
2121
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_truncated, false, "f");
|
|
2122
|
+
const sentinel = `__ANT_CMD_${node_crypto.randomUUID()}_DONE__`;
|
|
2123
|
+
const sentinelSplit = `${sentinel.slice(0, 8)}''${sentinel.slice(8)}`;
|
|
2124
|
+
const wrapped = `{ ${command}\n} </dev/null 2>&1; printf '\\n${sentinelSplit}%d\\n' $?\n`;
|
|
2125
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stdin.write(wrapped);
|
|
2126
|
+
if (require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").indexOf(sentinel) < 0) {
|
|
2127
|
+
const { promise: sentinelSeen, resolve } = require_sdk.promiseWithResolvers();
|
|
2128
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_waiting, {
|
|
2129
|
+
sentinel,
|
|
2130
|
+
resolve
|
|
2131
|
+
}, "f");
|
|
2132
|
+
let timer;
|
|
2133
|
+
let onAbort;
|
|
2134
|
+
try {
|
|
2135
|
+
await Promise.race([
|
|
2136
|
+
sentinelSeen,
|
|
2137
|
+
new Promise((_, reject) => {
|
|
2138
|
+
timer = setTimeout(() => reject(new BashTimeoutError(timeoutMs)), timeoutMs);
|
|
2139
|
+
}),
|
|
2140
|
+
new Promise((_, reject) => {
|
|
2141
|
+
if (!signal) return;
|
|
2142
|
+
onAbort = () => reject(signal.reason);
|
|
2143
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2144
|
+
})
|
|
2145
|
+
]);
|
|
2146
|
+
} finally {
|
|
2147
|
+
if (timer) clearTimeout(timer);
|
|
2148
|
+
if (onAbort && signal) signal.removeEventListener("abort", onAbort);
|
|
2149
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_waiting, null, "f");
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
const idx = require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").indexOf(sentinel);
|
|
2153
|
+
if (idx < 0) {
|
|
2154
|
+
throw new require_sdk.AnthropicError("bash session terminated");
|
|
2155
|
+
}
|
|
2156
|
+
const tail = require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").slice(idx + sentinel.length);
|
|
2157
|
+
const m = tail.match(/^(-?\d+)/);
|
|
2158
|
+
const exitCode = m ? parseInt(m[1], 10) : -1;
|
|
2159
|
+
let out = require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").slice(0, idx).replace(ANSI_RE, "").replace(/\n+$/, "");
|
|
2160
|
+
if (require_sdk.__classPrivateFieldGet(this, _BashSession_truncated, "f")) {
|
|
2161
|
+
out = `[output truncated]\n${out}`;
|
|
2162
|
+
}
|
|
2163
|
+
return {
|
|
2164
|
+
output: out,
|
|
2165
|
+
exitCode
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
close() {
|
|
2169
|
+
if (require_sdk.__classPrivateFieldGet(this, _BashSession_closed, "f")) return;
|
|
2170
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_closed, true, "f");
|
|
2171
|
+
const w = require_sdk.__classPrivateFieldGet(this, _BashSession_waiting, "f");
|
|
2172
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_waiting, null, "f");
|
|
2173
|
+
w?.resolve();
|
|
2174
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stdout.destroy();
|
|
2175
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stderr.destroy();
|
|
2176
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").stdin.destroy();
|
|
2177
|
+
try {
|
|
2178
|
+
process.kill(-require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").pid, "SIGKILL");
|
|
2179
|
+
} catch {
|
|
2180
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").kill("SIGKILL");
|
|
2181
|
+
}
|
|
2182
|
+
require_sdk.__classPrivateFieldGet(this, _BashSession_proc, "f").unref();
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
_BashSession_proc = new WeakMap(), _BashSession_buf = new WeakMap(), _BashSession_truncated = new WeakMap(), _BashSession_closed = new WeakMap(), _BashSession_waiting = new WeakMap(), _BashSession_instances = new WeakSet(), _BashSession_append = function _BashSession_append(d) {
|
|
2186
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_buf, require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f") + d, "f");
|
|
2187
|
+
if (require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").length > BASH_OUTPUT_LIMIT) {
|
|
2188
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_buf, require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").slice(require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").length - BASH_OUTPUT_LIMIT), "f");
|
|
2189
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_truncated, true, "f");
|
|
2190
|
+
}
|
|
2191
|
+
if (require_sdk.__classPrivateFieldGet(this, _BashSession_waiting, "f") && require_sdk.__classPrivateFieldGet(this, _BashSession_buf, "f").indexOf(require_sdk.__classPrivateFieldGet(this, _BashSession_waiting, "f").sentinel) >= 0) {
|
|
2192
|
+
const w = require_sdk.__classPrivateFieldGet(this, _BashSession_waiting, "f");
|
|
2193
|
+
require_sdk.__classPrivateFieldSet(this, _BashSession_waiting, null, "f");
|
|
2194
|
+
w.resolve();
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
function betaBashTool(ctx) {
|
|
2198
|
+
rejectUnrestrictedPaths(ctx.unrestrictedPaths);
|
|
2199
|
+
let session;
|
|
2200
|
+
let tail = Promise.resolve();
|
|
2201
|
+
return betaTool({
|
|
2202
|
+
name: "bash",
|
|
2203
|
+
description: "Run a bash command in a persistent shell. State (cwd, env vars) persists across calls.",
|
|
2204
|
+
inputSchema: {
|
|
2205
|
+
type: "object",
|
|
2206
|
+
properties: {
|
|
2207
|
+
command: {
|
|
2208
|
+
type: "string",
|
|
2209
|
+
description: "The command to run"
|
|
2210
|
+
},
|
|
2211
|
+
restart: {
|
|
2212
|
+
type: "boolean",
|
|
2213
|
+
description: "Restart the persistent shell before running"
|
|
2214
|
+
},
|
|
2215
|
+
timeout_ms: {
|
|
2216
|
+
type: "integer",
|
|
2217
|
+
description: "Per-call timeout in milliseconds"
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2221
|
+
run: async ({ command, restart, timeout_ms }, context) => {
|
|
2222
|
+
const prev = tail;
|
|
2223
|
+
const gate = require_sdk.promiseWithResolvers();
|
|
2224
|
+
tail = gate.promise;
|
|
2225
|
+
try {
|
|
2226
|
+
await prev;
|
|
2227
|
+
} catch {}
|
|
2228
|
+
try {
|
|
2229
|
+
if (restart) {
|
|
2230
|
+
session?.close();
|
|
2231
|
+
session = undefined;
|
|
2232
|
+
}
|
|
2233
|
+
if (!command) {
|
|
2234
|
+
if (restart) return "bash session restarted";
|
|
2235
|
+
throw new require_sdk.ToolError("bash: command is required");
|
|
2236
|
+
}
|
|
2237
|
+
session ?? (session = new BashSession(ctx.workdir, ctx.env));
|
|
2238
|
+
try {
|
|
2239
|
+
const { output, exitCode } = await session.exec(command, {
|
|
2240
|
+
timeoutMs: timeout_ms ?? BASH_DEFAULT_TIMEOUT_MS,
|
|
2241
|
+
signal: context?.signal
|
|
2242
|
+
});
|
|
2243
|
+
if (exitCode !== 0) throw new require_sdk.ToolError(output || `exit ${exitCode}`);
|
|
2244
|
+
return output;
|
|
2245
|
+
} catch (e) {
|
|
2246
|
+
if (e instanceof require_sdk.ToolError) throw e;
|
|
2247
|
+
session.close();
|
|
2248
|
+
session = undefined;
|
|
2249
|
+
throw new require_sdk.ToolError(`bash: ${e instanceof Error ? e.message : String(e)}`);
|
|
2250
|
+
}
|
|
2251
|
+
} finally {
|
|
2252
|
+
gate.resolve();
|
|
2253
|
+
}
|
|
2254
|
+
},
|
|
2255
|
+
close: () => {
|
|
2256
|
+
session?.close();
|
|
2257
|
+
session = undefined;
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
function betaReadTool(ctx) {
|
|
2262
|
+
rejectUnrestrictedPaths(ctx.unrestrictedPaths);
|
|
2263
|
+
return betaTool({
|
|
2264
|
+
name: "read",
|
|
2265
|
+
description: "Read a UTF-8 text file relative to the workdir.",
|
|
2266
|
+
inputSchema: {
|
|
2267
|
+
type: "object",
|
|
2268
|
+
properties: {
|
|
2269
|
+
file_path: { type: "string" },
|
|
2270
|
+
view_range: {
|
|
2271
|
+
type: "array",
|
|
2272
|
+
items: { type: "integer" },
|
|
2273
|
+
description: "[start_line, end_line] 1-indexed inclusive"
|
|
2274
|
+
}
|
|
2275
|
+
},
|
|
2276
|
+
required: ["file_path"]
|
|
2277
|
+
},
|
|
2278
|
+
run: async ({ file_path, view_range }) => {
|
|
2279
|
+
if (!file_path) throw new require_sdk.ToolError("read: file_path is required");
|
|
2280
|
+
const abs = await resolvePath(ctx, file_path);
|
|
2281
|
+
if (view_range?.length && view_range.length !== 2) {
|
|
2282
|
+
throw new require_sdk.ToolError("read: view_range must be [start_line, end_line]");
|
|
2283
|
+
}
|
|
2284
|
+
let data;
|
|
2285
|
+
try {
|
|
2286
|
+
const st = await node_fs_promises.stat(abs);
|
|
2287
|
+
if (!st.isFile()) {
|
|
2288
|
+
throw new require_sdk.ToolError(`read: ${file_path} is not a regular file`);
|
|
2289
|
+
}
|
|
2290
|
+
const limit = resolveMaxBytes(ctx.maxFileBytes);
|
|
2291
|
+
if (limit !== null && st.size > limit) {
|
|
2292
|
+
if (!view_range?.length) {
|
|
2293
|
+
throw new require_sdk.ToolError(`read: ${file_path} is ${st.size} bytes, exceeds ${limit}-byte limit. ` + "Use the view_range parameter to read specific line ranges, e.g. view_range: [1, 500].");
|
|
2294
|
+
}
|
|
2295
|
+
const [startLine, endLine] = view_range;
|
|
2296
|
+
return await readRangeStreaming(abs, file_path, startLine, endLine, limit);
|
|
2297
|
+
}
|
|
2298
|
+
data = await node_fs_promises.readFile(abs, "utf8");
|
|
2299
|
+
} catch (e) {
|
|
2300
|
+
if (e instanceof require_sdk.ToolError) throw e;
|
|
2301
|
+
throw new require_sdk.ToolError(`read: ${fsErrorMessage(e, file_path)}`);
|
|
2302
|
+
}
|
|
2303
|
+
if (!view_range?.length) return data;
|
|
2304
|
+
const [startLine, endLine] = view_range;
|
|
2305
|
+
const lines = data.split("\n");
|
|
2306
|
+
const start = Math.max(0, startLine - 1);
|
|
2307
|
+
const end = endLine > 0 ? endLine : lines.length;
|
|
2308
|
+
return lines.slice(start, end).join("\n");
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
/** Returns lines `[startLine, endLine]` of the file at `abs`, capping the selected bytes at `limit`. */
|
|
2313
|
+
async function readRangeStreaming(abs, filePath, startLine, endLine, limit) {
|
|
2314
|
+
const lines = new LineRangeCollector(filePath, startLine, endLine, limit);
|
|
2315
|
+
if (lines.rangeIsEmpty()) return "";
|
|
2316
|
+
const stream = node_fs.createReadStream(abs, { highWaterMark: READ_STREAM_CHUNK_BYTES });
|
|
2317
|
+
try {
|
|
2318
|
+
for await (const chunk of stream) {
|
|
2319
|
+
lines.collectFrom(chunk);
|
|
2320
|
+
if (lines.rangeIsCollected()) break;
|
|
2321
|
+
}
|
|
2322
|
+
} finally {
|
|
2323
|
+
stream.destroy();
|
|
2324
|
+
}
|
|
2325
|
+
return lines.text();
|
|
2326
|
+
}
|
|
2327
|
+
/** Collects the bytes of lines `[startLine, endLine]` from consecutive file chunks, capped at `limit`. */
|
|
2328
|
+
var LineRangeCollector = class {
|
|
2329
|
+
constructor(filePath, startLine, endLine, limit) {
|
|
2330
|
+
_LineRangeCollector_instances.add(this);
|
|
2331
|
+
_LineRangeCollector_filePath.set(this, void 0);
|
|
2332
|
+
_LineRangeCollector_startLine.set(this, void 0);
|
|
2333
|
+
_LineRangeCollector_endLine.set(this, void 0);
|
|
2334
|
+
_LineRangeCollector_start.set(this, void 0);
|
|
2335
|
+
_LineRangeCollector_end.set(this, void 0);
|
|
2336
|
+
_LineRangeCollector_limit.set(this, void 0);
|
|
2337
|
+
_LineRangeCollector_line.set(this, 0);
|
|
2338
|
+
_LineRangeCollector_collected.set(this, []);
|
|
2339
|
+
_LineRangeCollector_collectedBytes.set(this, 0);
|
|
2340
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_filePath, filePath, "f");
|
|
2341
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_startLine, startLine, "f");
|
|
2342
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_endLine, endLine, "f");
|
|
2343
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_start, Math.max(0, startLine - 1), "f");
|
|
2344
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_end, endLine > 0 ? endLine : Infinity, "f");
|
|
2345
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_limit, limit, "f");
|
|
2346
|
+
}
|
|
2347
|
+
rangeIsEmpty() {
|
|
2348
|
+
return require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_end, "f") <= require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_start, "f");
|
|
2349
|
+
}
|
|
2350
|
+
rangeIsCollected() {
|
|
2351
|
+
return require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_line, "f") >= require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_end, "f");
|
|
2352
|
+
}
|
|
2353
|
+
collectFrom(chunk) {
|
|
2354
|
+
var _a;
|
|
2355
|
+
let lineStart = 0;
|
|
2356
|
+
while (lineStart < chunk.length && !this.rangeIsCollected()) {
|
|
2357
|
+
const newline = chunk.indexOf(10, lineStart);
|
|
2358
|
+
const lineEnd = newline < 0 ? chunk.length : newline;
|
|
2359
|
+
if (require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_line, "f") >= require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_start, "f")) {
|
|
2360
|
+
require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_instances, "m", _LineRangeCollector_collect).call(this, chunk.subarray(lineStart, lineEnd), newline >= 0);
|
|
2361
|
+
}
|
|
2362
|
+
if (newline < 0) break;
|
|
2363
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_line, (_a = require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_line, "f"), _a++, _a), "f");
|
|
2364
|
+
lineStart = newline + 1;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
text() {
|
|
2368
|
+
return Buffer.concat(require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_collected, "f"), require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_collectedBytes, "f")).toString("utf8");
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
_LineRangeCollector_filePath = new WeakMap(), _LineRangeCollector_startLine = new WeakMap(), _LineRangeCollector_endLine = new WeakMap(), _LineRangeCollector_start = new WeakMap(), _LineRangeCollector_end = new WeakMap(), _LineRangeCollector_limit = new WeakMap(), _LineRangeCollector_line = new WeakMap(), _LineRangeCollector_collected = new WeakMap(), _LineRangeCollector_collectedBytes = new WeakMap(), _LineRangeCollector_instances = new WeakSet(), _LineRangeCollector_collect = function _LineRangeCollector_collect(lineBytes, newlineTerminated) {
|
|
2372
|
+
require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_collected, "f").push(lineBytes);
|
|
2373
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_collectedBytes, require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_collectedBytes, "f") + lineBytes.length, "f");
|
|
2374
|
+
if (newlineTerminated && require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_line, "f") + 1 < require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_end, "f")) {
|
|
2375
|
+
require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_collected, "f").push(NEWLINE);
|
|
2376
|
+
require_sdk.__classPrivateFieldSet(this, _LineRangeCollector_collectedBytes, require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_collectedBytes, "f") + NEWLINE.length, "f");
|
|
2377
|
+
}
|
|
2378
|
+
if (require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_collectedBytes, "f") > require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_limit, "f")) throw require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_instances, "m", _LineRangeCollector_overLimitError).call(this);
|
|
2379
|
+
}, _LineRangeCollector_overLimitError = function _LineRangeCollector_overLimitError() {
|
|
2380
|
+
if (require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_end, "f") - require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_start, "f") === 1) {
|
|
2381
|
+
return new require_sdk.ToolError(`read: line ${require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_start, "f") + 1} of ${require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_filePath, "f")} alone exceeds ${require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_limit, "f")}-byte limit. ` + "The read tool cannot return part of a line, so view_range cannot narrow this further.");
|
|
2382
|
+
}
|
|
2383
|
+
return new require_sdk.ToolError(`read: view_range [${require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_startLine, "f")}, ${require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_endLine, "f")}] of ${require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_filePath, "f")} exceeds ${require_sdk.__classPrivateFieldGet(this, _LineRangeCollector_limit, "f")}-byte limit. ` + "Narrow the view_range to read a smaller portion.");
|
|
2384
|
+
};
|
|
2385
|
+
function betaWriteTool(ctx) {
|
|
2386
|
+
rejectUnrestrictedPaths(ctx.unrestrictedPaths);
|
|
2387
|
+
return betaTool({
|
|
2388
|
+
name: "write",
|
|
2389
|
+
description: "Write a UTF-8 text file relative to the workdir, creating parent directories as needed.",
|
|
2390
|
+
inputSchema: {
|
|
2391
|
+
type: "object",
|
|
2392
|
+
properties: {
|
|
2393
|
+
file_path: { type: "string" },
|
|
2394
|
+
content: { type: "string" }
|
|
2395
|
+
},
|
|
2396
|
+
required: ["file_path", "content"]
|
|
2397
|
+
},
|
|
2398
|
+
run: async ({ file_path, content }) => {
|
|
2399
|
+
if (!file_path) throw new require_sdk.ToolError("write: file_path is required");
|
|
2400
|
+
const abs = await resolvePath(ctx, file_path);
|
|
2401
|
+
const ro = await readOnlyRootFor(ctx, abs);
|
|
2402
|
+
if (ro !== undefined) {
|
|
2403
|
+
throw new require_sdk.ToolError(`write: ${file_path} is inside read-only directory ${ro}`);
|
|
2404
|
+
}
|
|
2405
|
+
try {
|
|
2406
|
+
await node_fs_promises.mkdir(node_path.dirname(abs), {
|
|
2407
|
+
recursive: true,
|
|
2408
|
+
mode: 448
|
|
2409
|
+
});
|
|
2410
|
+
await atomicWriteFile(abs, content ?? "");
|
|
2411
|
+
} catch (e) {
|
|
2412
|
+
throw new require_sdk.ToolError(`write: ${fsErrorMessage(e, file_path)}`);
|
|
2413
|
+
}
|
|
2414
|
+
return `wrote ${Buffer.byteLength(content ?? "")} bytes to ${file_path}`;
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
function betaEditTool(ctx) {
|
|
2419
|
+
rejectUnrestrictedPaths(ctx.unrestrictedPaths);
|
|
2420
|
+
return betaTool({
|
|
2421
|
+
name: "edit",
|
|
2422
|
+
description: "Replace old_string with new_string in a file. old_string must be unique unless replace_all.",
|
|
2423
|
+
inputSchema: {
|
|
2424
|
+
type: "object",
|
|
2425
|
+
properties: {
|
|
2426
|
+
file_path: { type: "string" },
|
|
2427
|
+
old_string: { type: "string" },
|
|
2428
|
+
new_string: { type: "string" },
|
|
2429
|
+
replace_all: { type: "boolean" }
|
|
2430
|
+
},
|
|
2431
|
+
required: [
|
|
2432
|
+
"file_path",
|
|
2433
|
+
"old_string",
|
|
2434
|
+
"new_string"
|
|
2435
|
+
]
|
|
2436
|
+
},
|
|
2437
|
+
run: async ({ file_path, old_string, new_string, replace_all }) => {
|
|
2438
|
+
if (!file_path) throw new require_sdk.ToolError("edit: file_path is required");
|
|
2439
|
+
if (!old_string) throw new require_sdk.ToolError("edit: old_string is required");
|
|
2440
|
+
const abs = await resolvePath(ctx, file_path);
|
|
2441
|
+
const ro = await readOnlyRootFor(ctx, abs);
|
|
2442
|
+
if (ro !== undefined) {
|
|
2443
|
+
throw new require_sdk.ToolError(`edit: ${file_path} is inside read-only directory ${ro}`);
|
|
2444
|
+
}
|
|
2445
|
+
let data;
|
|
2446
|
+
try {
|
|
2447
|
+
const st = await node_fs_promises.stat(abs);
|
|
2448
|
+
if (!st.isFile()) {
|
|
2449
|
+
throw new require_sdk.ToolError(`edit: ${file_path} is not a regular file`);
|
|
2450
|
+
}
|
|
2451
|
+
const limit = resolveMaxBytes(ctx.maxFileBytes);
|
|
2452
|
+
if (limit !== null && st.size > limit) {
|
|
2453
|
+
throw new require_sdk.ToolError(`edit: ${file_path} is ${st.size} bytes, exceeds ${limit}-byte limit. ` + "The edit tool loads the whole file and cannot modify a file this large.");
|
|
2454
|
+
}
|
|
2455
|
+
data = await node_fs_promises.readFile(abs, "utf8");
|
|
2456
|
+
} catch (e) {
|
|
2457
|
+
if (e instanceof require_sdk.ToolError) throw e;
|
|
2458
|
+
throw new require_sdk.ToolError(`edit: ${fsErrorMessage(e, file_path)}`);
|
|
2459
|
+
}
|
|
2460
|
+
const count = data.split(old_string).length - 1;
|
|
2461
|
+
if (count === 0) throw new require_sdk.ToolError(`edit: old_string not found in ${file_path}`);
|
|
2462
|
+
let updated;
|
|
2463
|
+
if (replace_all) {
|
|
2464
|
+
updated = data.split(old_string).join(new_string);
|
|
2465
|
+
} else {
|
|
2466
|
+
if (count > 1) throw new require_sdk.ToolError(`edit: old_string appears ${count} times in ${file_path} (must be unique)`);
|
|
2467
|
+
updated = data.replace(old_string, () => new_string);
|
|
2468
|
+
}
|
|
2469
|
+
try {
|
|
2470
|
+
await atomicWriteFile(abs, updated);
|
|
2471
|
+
} catch (e) {
|
|
2472
|
+
throw new require_sdk.ToolError(`edit: write: ${fsErrorMessage(e, file_path)}`);
|
|
2473
|
+
}
|
|
2474
|
+
return `edited ${file_path} (${replace_all ? count : 1} replacement(s))`;
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Best-effort: stops `fs.glob` from walking out of the root via a literal or
|
|
2480
|
+
* brace-expanded `..`. The realpath post-filter in {@link betaGlobTool} is the
|
|
2481
|
+
* boundary; this only avoids the walk.
|
|
2482
|
+
*/
|
|
2483
|
+
function patternCanAscend(pattern) {
|
|
2484
|
+
return pattern.split(/[\\/{},]/).includes("..");
|
|
2485
|
+
}
|
|
2486
|
+
function betaGlobTool(ctx) {
|
|
2487
|
+
rejectUnrestrictedPaths(ctx.unrestrictedPaths);
|
|
2488
|
+
return betaTool({
|
|
2489
|
+
name: "glob",
|
|
2490
|
+
description: "Match files under the workdir against a glob pattern. Results are mtime-sorted, newest first.",
|
|
2491
|
+
inputSchema: {
|
|
2492
|
+
type: "object",
|
|
2493
|
+
properties: {
|
|
2494
|
+
pattern: { type: "string" },
|
|
2495
|
+
path: {
|
|
2496
|
+
type: "string",
|
|
2497
|
+
description: "Directory to search in. Defaults to the workdir."
|
|
2498
|
+
}
|
|
2499
|
+
},
|
|
2500
|
+
required: ["pattern"]
|
|
2501
|
+
},
|
|
2502
|
+
run: async ({ pattern, path: searchPath }) => {
|
|
2503
|
+
if (!pattern) throw new require_sdk.ToolError("glob: pattern is required");
|
|
2504
|
+
if (node_path.isAbsolute(pattern)) {
|
|
2505
|
+
throw new require_sdk.ToolError("glob: absolute pattern not permitted; pass a relative pattern (and optionally path)");
|
|
2506
|
+
}
|
|
2507
|
+
if (patternCanAscend(pattern)) {
|
|
2508
|
+
throw new require_sdk.ToolError("glob: \"..\" is not permitted in the pattern");
|
|
2509
|
+
}
|
|
2510
|
+
const root = searchPath ? await resolvePath(ctx, searchPath) : node_path.resolve(ctx.workdir);
|
|
2511
|
+
const realRoot = searchPath ? root : await canonicalize(root);
|
|
2512
|
+
const matches = [];
|
|
2513
|
+
let remaining = WALK_MAX_ENTRIES;
|
|
2514
|
+
try {
|
|
2515
|
+
for await (const entry of fsGlob(pattern, {
|
|
2516
|
+
cwd: root,
|
|
2517
|
+
withFileTypes: true,
|
|
2518
|
+
exclude: (d) => d.name === ".git" || d.name === "node_modules"
|
|
2519
|
+
})) {
|
|
2520
|
+
if (remaining-- <= 0) break;
|
|
2521
|
+
if (!entry.isFile()) continue;
|
|
2522
|
+
const full = node_path.join(entry.parentPath, entry.name);
|
|
2523
|
+
let real;
|
|
2524
|
+
try {
|
|
2525
|
+
real = await node_fs_promises.realpath(full);
|
|
2526
|
+
} catch {
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
if (!isWithin(realRoot, real)) continue;
|
|
2530
|
+
let mtime = 0;
|
|
2531
|
+
try {
|
|
2532
|
+
mtime = (await node_fs_promises.stat(full)).mtimeMs;
|
|
2533
|
+
} catch {}
|
|
2534
|
+
matches.push({
|
|
2535
|
+
path: full,
|
|
2536
|
+
mtime
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
} catch (e) {
|
|
2540
|
+
throw new require_sdk.ToolError(`glob: ${e instanceof Error ? e.message : String(e)}`);
|
|
2541
|
+
}
|
|
2542
|
+
if (matches.length === 0) return "no matches";
|
|
2543
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
2544
|
+
return matches.slice(0, GLOB_RESULT_LIMIT).map((m) => m.path).join("\n");
|
|
2545
|
+
}
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
function betaGrepTool(ctx) {
|
|
2549
|
+
rejectUnrestrictedPaths(ctx.unrestrictedPaths);
|
|
2550
|
+
return betaTool({
|
|
2551
|
+
name: "grep",
|
|
2552
|
+
description: "Search file contents for a regex. Uses ripgrep if available, otherwise a built-in walker.",
|
|
2553
|
+
inputSchema: {
|
|
2554
|
+
type: "object",
|
|
2555
|
+
properties: {
|
|
2556
|
+
pattern: { type: "string" },
|
|
2557
|
+
path: { type: "string" }
|
|
2558
|
+
},
|
|
2559
|
+
required: ["pattern"]
|
|
2560
|
+
},
|
|
2561
|
+
run: async ({ pattern, path: p }, context) => {
|
|
2562
|
+
if (!pattern) throw new require_sdk.ToolError("grep: pattern is required");
|
|
2563
|
+
let searchPath = node_path.resolve(ctx.workdir);
|
|
2564
|
+
if (p) searchPath = await resolvePath(ctx, p);
|
|
2565
|
+
const rg = await findRg();
|
|
2566
|
+
return rg ? runRipgrep(rg, pattern, searchPath, context?.signal) : runWalkGrep(pattern, searchPath, context?.signal);
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
function runRipgrep(rg, pattern, searchPath, signal) {
|
|
2571
|
+
return new Promise((resolve, reject) => {
|
|
2572
|
+
const proc = node_child_process.spawn(rg, [
|
|
2573
|
+
"-n",
|
|
2574
|
+
"--no-heading",
|
|
2575
|
+
"-e",
|
|
2576
|
+
pattern,
|
|
2577
|
+
"--",
|
|
2578
|
+
searchPath
|
|
2579
|
+
], { ...signal ? { signal } : {} });
|
|
2580
|
+
let out = "";
|
|
2581
|
+
let errOut = "";
|
|
2582
|
+
let truncated = false;
|
|
2583
|
+
proc.stdout.on("data", (d) => {
|
|
2584
|
+
if (truncated) return;
|
|
2585
|
+
out += d;
|
|
2586
|
+
if (out.length > GREP_OUTPUT_LIMIT) {
|
|
2587
|
+
truncated = true;
|
|
2588
|
+
out = out.slice(0, GREP_OUTPUT_LIMIT);
|
|
2589
|
+
proc.kill("SIGKILL");
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
proc.stderr.on("data", (d) => errOut += d);
|
|
2593
|
+
proc.on("close", (code) => {
|
|
2594
|
+
if (signal?.aborted) return reject(new require_sdk.ToolError("grep: aborted"));
|
|
2595
|
+
if (truncated) return resolve(out + `\n[output truncated at ${GREP_OUTPUT_LIMIT} bytes]`);
|
|
2596
|
+
if (code === 0) return resolve(out);
|
|
2597
|
+
if (code === 1) return resolve("no matches");
|
|
2598
|
+
reject(new require_sdk.ToolError(`grep: rg failed: ${errOut || `exit ${code}`}`));
|
|
2599
|
+
});
|
|
2600
|
+
proc.on("error", (e) => {
|
|
2601
|
+
if (signal?.aborted) return reject(new require_sdk.ToolError("grep: aborted"));
|
|
2602
|
+
reject(new require_sdk.ToolError(`grep: rg failed: ${e.message}`));
|
|
2603
|
+
});
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
async function runWalkGrep(pattern, root, signal) {
|
|
2607
|
+
let re;
|
|
2608
|
+
try {
|
|
2609
|
+
re = new RegExp(pattern);
|
|
2610
|
+
} catch (e) {
|
|
2611
|
+
throw new require_sdk.ToolError(`grep: invalid regex: ${e instanceof Error ? e.message : String(e)}`);
|
|
2612
|
+
}
|
|
2613
|
+
const hits = [];
|
|
2614
|
+
let budget = GREP_OUTPUT_LIMIT;
|
|
2615
|
+
const push = (line) => {
|
|
2616
|
+
budget -= line.length + 1;
|
|
2617
|
+
if (budget < 0) {
|
|
2618
|
+
hits.push(`[output truncated at ${GREP_OUTPUT_LIMIT} bytes]`);
|
|
2619
|
+
return false;
|
|
2620
|
+
}
|
|
2621
|
+
hits.push(line);
|
|
2622
|
+
return true;
|
|
2623
|
+
};
|
|
2624
|
+
const stat = await node_fs_promises.stat(root).catch(() => null);
|
|
2625
|
+
if (stat?.isFile()) {
|
|
2626
|
+
await grepFile(root, re, push);
|
|
2627
|
+
} else {
|
|
2628
|
+
await walk(root, "", (rel) => grepFile(node_path.join(root, rel), re, push), signal);
|
|
2629
|
+
}
|
|
2630
|
+
if (signal?.aborted) throw new require_sdk.ToolError("grep: aborted");
|
|
2631
|
+
if (hits.length === 0) return "no matches";
|
|
2632
|
+
return hits.join("\n");
|
|
2633
|
+
}
|
|
2634
|
+
async function grepFile(file, re, push) {
|
|
2635
|
+
const stream = node_fs.createReadStream(file, { encoding: "utf8" });
|
|
2636
|
+
const rl = node_readline.createInterface({
|
|
2637
|
+
input: stream,
|
|
2638
|
+
crlfDelay: Infinity
|
|
2639
|
+
});
|
|
2640
|
+
let i = 0;
|
|
2641
|
+
try {
|
|
2642
|
+
for await (const line of rl) {
|
|
2643
|
+
i++;
|
|
2644
|
+
if (line.length > GREP_MAX_LINE_LENGTH) continue;
|
|
2645
|
+
if (re.test(line) && !push(`${file}:${i}:${line}`)) return false;
|
|
2646
|
+
}
|
|
2647
|
+
} catch {} finally {
|
|
2648
|
+
stream.destroy();
|
|
2649
|
+
}
|
|
2650
|
+
return true;
|
|
2651
|
+
}
|
|
2652
|
+
const WALK_MAX_DEPTH = 40;
|
|
2653
|
+
const WALK_MAX_ENTRIES = 5e4;
|
|
2654
|
+
/**
|
|
2655
|
+
* Bounded recursive walk. `fn` may return `false` to abort. Only real
|
|
2656
|
+
* directories are descended into and only real files are handed to `fn` —
|
|
2657
|
+
* symlinks (and devices/fifos/sockets) are skipped entirely so a symlink inside
|
|
2658
|
+
* the root cannot be followed out of it.
|
|
2659
|
+
*/
|
|
2660
|
+
async function walk(root, rel, fn, signal) {
|
|
2661
|
+
let remaining = WALK_MAX_ENTRIES;
|
|
2662
|
+
async function inner(rel, depth) {
|
|
2663
|
+
if (depth > WALK_MAX_DEPTH) return true;
|
|
2664
|
+
if (signal?.aborted) return false;
|
|
2665
|
+
let entries;
|
|
2666
|
+
try {
|
|
2667
|
+
entries = await node_fs_promises.readdir(node_path.join(root, rel), { withFileTypes: true });
|
|
2668
|
+
} catch {
|
|
2669
|
+
return true;
|
|
2670
|
+
}
|
|
2671
|
+
for (const e of entries) {
|
|
2672
|
+
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
2673
|
+
if (remaining-- <= 0) return false;
|
|
2674
|
+
if (signal?.aborted) return false;
|
|
2675
|
+
const childRel = rel ? node_path.join(rel, e.name) : e.name;
|
|
2676
|
+
if (e.isDirectory()) {
|
|
2677
|
+
if (!await inner(childRel, depth + 1)) return false;
|
|
2678
|
+
} else if (e.isFile()) {
|
|
2679
|
+
if (await fn(childRel) === false) return false;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return true;
|
|
2683
|
+
}
|
|
2684
|
+
await inner(rel, 0);
|
|
2685
|
+
}
|
|
2686
|
+
async function findRg() {
|
|
2687
|
+
const dirs = (process.env["PATH"] ?? "").split(node_path.delimiter);
|
|
2688
|
+
for (const d of dirs) {
|
|
2689
|
+
const candidate = node_path.join(d, "rg");
|
|
2690
|
+
try {
|
|
2691
|
+
await node_fs_promises.access(candidate, node_fs.constants.X_OK);
|
|
2692
|
+
return candidate;
|
|
2693
|
+
} catch {}
|
|
2694
|
+
}
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
//#endregion
|
|
2699
|
+
exports.MEMORY_FLUSH_TIMEOUT_MS = MEMORY_FLUSH_TIMEOUT_MS;
|
|
2700
|
+
exports.SessionMemoryError = SessionMemoryError;
|
|
2701
|
+
exports.SessionMemoryStores = SessionMemoryStores;
|
|
2702
|
+
exports.betaAgentToolset20260401 = betaAgentToolset20260401;
|
|
2703
|
+
exports.setupSkills = setupSkills;
|