fourmis-agents-sdk 0.3.1 → 0.4.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/agent-loop.d.ts +21 -3
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +294 -90
- package/dist/agents/index.js +2798 -1857
- package/dist/agents/task-manager.js +15 -0
- package/dist/agents/tools.d.ts.map +1 -1
- package/dist/agents/tools.js +2798 -1857
- package/dist/agents/types.d.ts +4 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/api.d.ts +8 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2140 -923
- package/dist/auth/gemini-oauth.js +15 -0
- package/dist/auth/login-openai.js +15 -0
- package/dist/auth/openai-oauth.js +15 -0
- package/dist/hooks.d.ts +19 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +42 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2148 -924
- package/dist/mcp/client.d.ts +7 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +146 -12
- package/dist/mcp/index.js +146 -12
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/types.d.ts +19 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/memory/index.js +15 -0
- package/dist/memory/memory-handler.js +15 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +22 -3
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +56 -2
- package/dist/providers/gemini.js +15 -0
- package/dist/providers/openai.js +15 -0
- package/dist/providers/registry.js +56 -2
- package/dist/providers/types.d.ts +4 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/query.d.ts +21 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +84 -1
- package/dist/settings.js +15 -0
- package/dist/skills/frontmatter.js +15 -0
- package/dist/skills/index.js +38 -1
- package/dist/skills/skills.d.ts +16 -0
- package/dist/skills/skills.d.ts.map +1 -1
- package/dist/skills/skills.js +38 -1
- package/dist/tools/ask-user-question.d.ts +7 -0
- package/dist/tools/ask-user-question.d.ts.map +1 -0
- package/dist/tools/ask-user-question.js +63 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +62 -2
- package/dist/tools/config.d.ts +7 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +129 -0
- package/dist/tools/edit.js +15 -0
- package/dist/tools/exit-plan-mode.d.ts +7 -0
- package/dist/tools/exit-plan-mode.d.ts.map +1 -0
- package/dist/tools/exit-plan-mode.js +49 -0
- package/dist/tools/glob.js +15 -0
- package/dist/tools/grep.js +15 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +521 -9
- package/dist/tools/mcp-resources.js +15 -0
- package/dist/tools/notebook-edit.d.ts +7 -0
- package/dist/tools/notebook-edit.d.ts.map +1 -0
- package/dist/tools/notebook-edit.js +98 -0
- package/dist/tools/presets.d.ts +2 -1
- package/dist/tools/presets.d.ts.map +1 -1
- package/dist/tools/presets.js +37 -4
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +27 -1
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +25 -0
- package/dist/tools/todo-write.d.ts +7 -0
- package/dist/tools/todo-write.d.ts.map +1 -0
- package/dist/tools/todo-write.js +84 -0
- package/dist/tools/web-fetch.d.ts +6 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +100 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +93 -0
- package/dist/tools/write.js +15 -0
- package/dist/types.d.ts +344 -42
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -0
- package/dist/utils/cost.js +15 -0
- package/dist/utils/session-store.d.ts +1 -1
- package/dist/utils/session-store.d.ts.map +1 -1
- package/dist/utils/session-store.js +64 -2
- package/dist/utils/system-prompt.d.ts +2 -0
- package/dist/utils/system-prompt.d.ts.map +1 -1
- package/dist/utils/system-prompt.js +48 -4
- package/package.json +3 -2
package/dist/api.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
2
4
|
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
3
18
|
var __export = (target, all) => {
|
|
4
19
|
for (var name in all)
|
|
5
20
|
__defProp(target, name, {
|
|
@@ -12,76 +27,6 @@ var __export = (target, all) => {
|
|
|
12
27
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
13
28
|
var __require = import.meta.require;
|
|
14
29
|
|
|
15
|
-
// src/tools/mcp-resources.ts
|
|
16
|
-
var exports_mcp_resources = {};
|
|
17
|
-
__export(exports_mcp_resources, {
|
|
18
|
-
createReadMcpResourceTool: () => createReadMcpResourceTool,
|
|
19
|
-
createListMcpResourcesTool: () => createListMcpResourcesTool
|
|
20
|
-
});
|
|
21
|
-
function createListMcpResourcesTool(mcpClient) {
|
|
22
|
-
return {
|
|
23
|
-
name: "mcp__list_resources",
|
|
24
|
-
description: "List available resources from MCP servers.",
|
|
25
|
-
inputSchema: {
|
|
26
|
-
type: "object",
|
|
27
|
-
properties: {
|
|
28
|
-
server: {
|
|
29
|
-
type: "string",
|
|
30
|
-
description: "Optional server name to filter by. If omitted, lists resources from all servers."
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
async execute(input) {
|
|
35
|
-
const { server } = input ?? {};
|
|
36
|
-
try {
|
|
37
|
-
const resources = await mcpClient.listResources(server);
|
|
38
|
-
if (resources.length === 0) {
|
|
39
|
-
return { content: "No resources available." };
|
|
40
|
-
}
|
|
41
|
-
const lines = resources.map((r) => `[${r.server}] ${r.uri} - ${r.name}${r.description ? `: ${r.description}` : ""}`);
|
|
42
|
-
return { content: lines.join(`
|
|
43
|
-
`) };
|
|
44
|
-
} catch (err) {
|
|
45
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
-
return { content: `Error listing resources: ${message}`, isError: true };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function createReadMcpResourceTool(mcpClient) {
|
|
52
|
-
return {
|
|
53
|
-
name: "mcp__read_resource",
|
|
54
|
-
description: "Read a specific resource from an MCP server by URI.",
|
|
55
|
-
inputSchema: {
|
|
56
|
-
type: "object",
|
|
57
|
-
properties: {
|
|
58
|
-
server: {
|
|
59
|
-
type: "string",
|
|
60
|
-
description: "The MCP server name that hosts the resource."
|
|
61
|
-
},
|
|
62
|
-
uri: {
|
|
63
|
-
type: "string",
|
|
64
|
-
description: "The resource URI to read."
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
required: ["server", "uri"]
|
|
68
|
-
},
|
|
69
|
-
async execute(input) {
|
|
70
|
-
const { server, uri } = input;
|
|
71
|
-
if (!server || !uri) {
|
|
72
|
-
return { content: "Both 'server' and 'uri' are required.", isError: true };
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
const content = await mcpClient.readResource(server, uri);
|
|
76
|
-
return { content };
|
|
77
|
-
} catch (err) {
|
|
78
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
-
return { content: `Error reading resource: ${message}`, isError: true };
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
30
|
// src/auth/openai-oauth.ts
|
|
86
31
|
var exports_openai_oauth = {};
|
|
87
32
|
__export(exports_openai_oauth, {
|
|
@@ -95,25 +40,25 @@ __export(exports_openai_oauth, {
|
|
|
95
40
|
decodeJwtPayload: () => decodeJwtPayload
|
|
96
41
|
});
|
|
97
42
|
import { randomBytes, createHash } from "crypto";
|
|
98
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
99
|
-
import { join } from "path";
|
|
100
|
-
import { homedir } from "os";
|
|
43
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
44
|
+
import { join as join2 } from "path";
|
|
45
|
+
import { homedir as homedir2 } from "os";
|
|
101
46
|
function getHome() {
|
|
102
|
-
return process.env.HOME ??
|
|
47
|
+
return process.env.HOME ?? homedir2();
|
|
103
48
|
}
|
|
104
49
|
function tokenDir() {
|
|
105
|
-
return
|
|
50
|
+
return join2(getHome(), ".fourmis");
|
|
106
51
|
}
|
|
107
52
|
function tokenPath() {
|
|
108
|
-
return
|
|
53
|
+
return join2(tokenDir(), "openai-auth.json");
|
|
109
54
|
}
|
|
110
55
|
function codexFallbackPath() {
|
|
111
|
-
return
|
|
56
|
+
return join2(getHome(), ".codex", "auth.json");
|
|
112
57
|
}
|
|
113
58
|
function loadTokens() {
|
|
114
59
|
for (const p of [tokenPath(), codexFallbackPath()]) {
|
|
115
60
|
try {
|
|
116
|
-
const raw =
|
|
61
|
+
const raw = readFileSync2(p, "utf-8");
|
|
117
62
|
const data = JSON.parse(raw);
|
|
118
63
|
if (data.access_token && data.account_id) {
|
|
119
64
|
return data;
|
|
@@ -124,10 +69,10 @@ function loadTokens() {
|
|
|
124
69
|
}
|
|
125
70
|
function saveTokens(tokens) {
|
|
126
71
|
const dir = tokenDir();
|
|
127
|
-
if (!
|
|
128
|
-
|
|
72
|
+
if (!existsSync2(dir)) {
|
|
73
|
+
mkdirSync2(dir, { recursive: true });
|
|
129
74
|
}
|
|
130
|
-
|
|
75
|
+
writeFileSync2(tokenPath(), JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
131
76
|
}
|
|
132
77
|
function generateCodeVerifier() {
|
|
133
78
|
return randomBytes(64).toString("base64url");
|
|
@@ -377,19 +322,19 @@ __export(exports_gemini_oauth, {
|
|
|
377
322
|
isLoggedIn: () => isLoggedIn2,
|
|
378
323
|
getValidToken: () => getValidToken2
|
|
379
324
|
});
|
|
380
|
-
import { readFileSync as
|
|
381
|
-
import { join as
|
|
382
|
-
import { homedir as
|
|
325
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
|
|
326
|
+
import { join as join3 } from "path";
|
|
327
|
+
import { homedir as homedir3 } from "os";
|
|
383
328
|
function getHome2() {
|
|
384
|
-
return process.env.HOME ??
|
|
329
|
+
return process.env.HOME ?? homedir3();
|
|
385
330
|
}
|
|
386
331
|
function tokenPath2() {
|
|
387
|
-
return
|
|
332
|
+
return join3(getHome2(), ".gemini", "oauth_creds.json");
|
|
388
333
|
}
|
|
389
334
|
function loadTokens2() {
|
|
390
335
|
const p = tokenPath2();
|
|
391
336
|
try {
|
|
392
|
-
const raw =
|
|
337
|
+
const raw = readFileSync3(p, "utf-8");
|
|
393
338
|
const data = JSON.parse(raw);
|
|
394
339
|
if (data.access_token && data.refresh_token) {
|
|
395
340
|
return data;
|
|
@@ -402,12 +347,12 @@ function loadTokensSync2() {
|
|
|
402
347
|
}
|
|
403
348
|
function saveTokens2(tokens) {
|
|
404
349
|
const p = tokenPath2();
|
|
405
|
-
const dir =
|
|
406
|
-
if (!
|
|
407
|
-
const { mkdirSync:
|
|
408
|
-
|
|
350
|
+
const dir = join3(getHome2(), ".gemini");
|
|
351
|
+
if (!existsSync3(dir)) {
|
|
352
|
+
const { mkdirSync: mkdirSync3 } = __require("fs");
|
|
353
|
+
mkdirSync3(dir, { recursive: true });
|
|
409
354
|
}
|
|
410
|
-
|
|
355
|
+
writeFileSync3(p, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
411
356
|
}
|
|
412
357
|
async function refreshAccessToken2(refreshToken) {
|
|
413
358
|
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
@@ -461,330 +406,366 @@ var init_gemini_oauth = __esm(() => {
|
|
|
461
406
|
GEMINI_CLIENT_SECRET = process.env.GEMINI_OAUTH_CLIENT_SECRET ?? ["GOCSPX", "4uHgMPm", "1o7Sk", "geV6Cu5clXFsxl"].join("-");
|
|
462
407
|
});
|
|
463
408
|
|
|
464
|
-
// src/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
409
|
+
// src/tools/mcp-resources.ts
|
|
410
|
+
var exports_mcp_resources = {};
|
|
411
|
+
__export(exports_mcp_resources, {
|
|
412
|
+
createReadMcpResourceTool: () => createReadMcpResourceTool,
|
|
413
|
+
createListMcpResourcesTool: () => createListMcpResourcesTool
|
|
414
|
+
});
|
|
415
|
+
function createListMcpResourcesTool(mcpClient) {
|
|
469
416
|
return {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
417
|
+
name: "mcp__list_resources",
|
|
418
|
+
description: "List available resources from MCP servers.",
|
|
419
|
+
inputSchema: {
|
|
420
|
+
type: "object",
|
|
421
|
+
properties: {
|
|
422
|
+
server: {
|
|
423
|
+
type: "string",
|
|
424
|
+
description: "Optional server name to filter by. If omitted, lists resources from all servers."
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
async execute(input) {
|
|
429
|
+
const { server } = input ?? {};
|
|
430
|
+
try {
|
|
431
|
+
const resources = await mcpClient.listResources(server);
|
|
432
|
+
if (resources.length === 0) {
|
|
433
|
+
return { content: "No resources available." };
|
|
434
|
+
}
|
|
435
|
+
const lines = resources.map((r) => `[${r.server}] ${r.uri} - ${r.name}${r.description ? `: ${r.description}` : ""}`);
|
|
436
|
+
return { content: lines.join(`
|
|
437
|
+
`) };
|
|
438
|
+
} catch (err) {
|
|
439
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
+
return { content: `Error listing resources: ${message}`, isError: true };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
474
443
|
};
|
|
475
444
|
}
|
|
476
|
-
function
|
|
445
|
+
function createReadMcpResourceTool(mcpClient) {
|
|
477
446
|
return {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
447
|
+
name: "mcp__read_resource",
|
|
448
|
+
description: "Read a specific resource from an MCP server by URI.",
|
|
449
|
+
inputSchema: {
|
|
450
|
+
type: "object",
|
|
451
|
+
properties: {
|
|
452
|
+
server: {
|
|
453
|
+
type: "string",
|
|
454
|
+
description: "The MCP server name that hosts the resource."
|
|
455
|
+
},
|
|
456
|
+
uri: {
|
|
457
|
+
type: "string",
|
|
458
|
+
description: "The resource URI to read."
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
required: ["server", "uri"]
|
|
462
|
+
},
|
|
463
|
+
async execute(input) {
|
|
464
|
+
const { server, uri } = input;
|
|
465
|
+
if (!server || !uri) {
|
|
466
|
+
return { content: "Both 'server' and 'uri' are required.", isError: true };
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
const content = await mcpClient.readResource(server, uri);
|
|
470
|
+
return { content };
|
|
471
|
+
} catch (err) {
|
|
472
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
473
|
+
return { content: `Error reading resource: ${message}`, isError: true };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
482
476
|
};
|
|
483
477
|
}
|
|
484
478
|
|
|
485
|
-
// src/
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
sessionLogger,
|
|
505
|
-
nativeMemoryTool
|
|
506
|
-
} = options;
|
|
507
|
-
const startTime = Date.now();
|
|
508
|
-
let apiTimeMs = 0;
|
|
509
|
-
let turns = 0;
|
|
510
|
-
let totalUsage = emptyTokenUsage();
|
|
511
|
-
let costUsd = 0;
|
|
512
|
-
const modelUsage = {};
|
|
513
|
-
if (mcpClient) {
|
|
514
|
-
await mcpClient.connectAll();
|
|
515
|
-
for (const tool of mcpClient.getTools()) {
|
|
516
|
-
tools.register(tool);
|
|
517
|
-
}
|
|
518
|
-
const { createListMcpResourcesTool: createListMcpResourcesTool2, createReadMcpResourceTool: createReadMcpResourceTool2 } = await Promise.resolve().then(() => exports_mcp_resources);
|
|
519
|
-
tools.register(createListMcpResourcesTool2(mcpClient));
|
|
520
|
-
tools.register(createReadMcpResourceTool2(mcpClient));
|
|
521
|
-
}
|
|
522
|
-
const messages = [
|
|
523
|
-
...previousMessages ?? [],
|
|
524
|
-
{ role: "user", content: prompt }
|
|
525
|
-
];
|
|
526
|
-
if (sessionLogger) {
|
|
527
|
-
sessionLogger("user", prompt, null);
|
|
479
|
+
// src/permissions.ts
|
|
480
|
+
var SAFE_TOOLS = new Set(["Read", "Glob", "Grep", "WebFetch", "WebSearch"]);
|
|
481
|
+
var EDIT_TOOLS = new Set(["Write", "Edit", "NotebookEdit", "TodoWrite", "Config"]);
|
|
482
|
+
var FS_COMMANDS = ["mkdir", "touch", "rm", "mv", "cp"];
|
|
483
|
+
var DELEGATE_TOOLS = new Set(["Teammate", "Task", "TaskOutput", "TaskStop"]);
|
|
484
|
+
function normalizeRules(rules) {
|
|
485
|
+
if (!rules)
|
|
486
|
+
return [];
|
|
487
|
+
return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
|
|
488
|
+
}
|
|
489
|
+
function matchesRule(rules, toolName, input) {
|
|
490
|
+
for (const rule of rules) {
|
|
491
|
+
if (rule.toolName !== toolName)
|
|
492
|
+
continue;
|
|
493
|
+
if (!rule.ruleContent)
|
|
494
|
+
return true;
|
|
495
|
+
const inputStr = toolName === "Bash" ? String(input?.command ?? "") : JSON.stringify(input ?? {});
|
|
496
|
+
if (inputStr.includes(rule.ruleContent))
|
|
497
|
+
return true;
|
|
528
498
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
class PermissionManager {
|
|
503
|
+
mode;
|
|
504
|
+
canUseTool;
|
|
505
|
+
allowRules;
|
|
506
|
+
denyRules;
|
|
507
|
+
settingsManager;
|
|
508
|
+
constructor(mode = "default", canUseTool, permissions, settingsManager) {
|
|
509
|
+
this.mode = mode;
|
|
510
|
+
this.canUseTool = canUseTool;
|
|
511
|
+
this.allowRules = normalizeRules(permissions?.allow);
|
|
512
|
+
this.denyRules = normalizeRules(permissions?.deny);
|
|
513
|
+
this.settingsManager = settingsManager;
|
|
540
514
|
}
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
return;
|
|
515
|
+
async check(toolName, input, options) {
|
|
516
|
+
if (this.mode === "bypassPermissions") {
|
|
517
|
+
return { behavior: "allow" };
|
|
545
518
|
}
|
|
546
|
-
if (
|
|
547
|
-
|
|
548
|
-
|
|
519
|
+
if (matchesRule(this.denyRules, toolName, input)) {
|
|
520
|
+
return {
|
|
521
|
+
behavior: "deny",
|
|
522
|
+
message: `Tool "${toolName}" is denied by permissions config.`
|
|
523
|
+
};
|
|
549
524
|
}
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
|
|
525
|
+
if (this.mode === "plan") {
|
|
526
|
+
if (!SAFE_TOOLS.has(toolName)) {
|
|
527
|
+
return {
|
|
528
|
+
behavior: "deny",
|
|
529
|
+
message: `Tool "${toolName}" is not allowed in plan mode. Only read-only tools are available.`
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return { behavior: "allow" };
|
|
553
533
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
try {
|
|
561
|
-
const chunks = provider.chat({
|
|
562
|
-
model,
|
|
563
|
-
messages,
|
|
564
|
-
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
565
|
-
systemPrompt,
|
|
566
|
-
signal,
|
|
567
|
-
nativeTools
|
|
568
|
-
});
|
|
569
|
-
for await (const chunk of chunks) {
|
|
570
|
-
switch (chunk.type) {
|
|
571
|
-
case "text_delta":
|
|
572
|
-
assistantTextParts.push(chunk.text);
|
|
573
|
-
if (includeStreamEvents) {
|
|
574
|
-
yield { type: "stream", subtype: "text_delta", text: chunk.text, uuid: uuid() };
|
|
575
|
-
}
|
|
576
|
-
break;
|
|
577
|
-
case "thinking_delta":
|
|
578
|
-
if (includeStreamEvents) {
|
|
579
|
-
yield { type: "stream", subtype: "thinking_delta", text: chunk.text, uuid: uuid() };
|
|
580
|
-
}
|
|
581
|
-
break;
|
|
582
|
-
case "tool_call":
|
|
583
|
-
toolCalls.push({ id: chunk.id, name: chunk.name, input: chunk.input });
|
|
584
|
-
break;
|
|
585
|
-
case "usage":
|
|
586
|
-
turnUsage = mergeUsage(turnUsage, chunk.usage);
|
|
587
|
-
break;
|
|
588
|
-
case "done":
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
534
|
+
if (this.mode === "delegate") {
|
|
535
|
+
if (!DELEGATE_TOOLS.has(toolName) && !SAFE_TOOLS.has(toolName)) {
|
|
536
|
+
return {
|
|
537
|
+
behavior: "deny",
|
|
538
|
+
message: `Tool "${toolName}" is not allowed in delegate mode. Only Teammate, Task, and read-only tools are available.`
|
|
539
|
+
};
|
|
591
540
|
}
|
|
592
|
-
|
|
593
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
594
|
-
yield makeError("error_execution", [`API error: ${message}`], turns, costUsd, sessionId, startTime);
|
|
595
|
-
return;
|
|
541
|
+
return { behavior: "allow" };
|
|
596
542
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
totalUsage = mergeUsage(totalUsage, turnUsage);
|
|
600
|
-
const turnCost = provider.calculateCost(model, turnUsage);
|
|
601
|
-
costUsd += turnCost;
|
|
602
|
-
if (!modelUsage[model]) {
|
|
603
|
-
modelUsage[model] = {
|
|
604
|
-
inputTokens: 0,
|
|
605
|
-
outputTokens: 0,
|
|
606
|
-
cacheReadInputTokens: 0,
|
|
607
|
-
cacheCreationInputTokens: 0,
|
|
608
|
-
totalCostUsd: 0
|
|
609
|
-
};
|
|
543
|
+
if (matchesRule(this.allowRules, toolName, input)) {
|
|
544
|
+
return { behavior: "allow" };
|
|
610
545
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
modelUsage[model].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
|
|
614
|
-
modelUsage[model].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
|
|
615
|
-
modelUsage[model].totalCostUsd += turnCost;
|
|
616
|
-
const assistantText = assistantTextParts.join("");
|
|
617
|
-
const assistantContent = [];
|
|
618
|
-
if (assistantText) {
|
|
619
|
-
assistantContent.push({ type: "text", text: assistantText });
|
|
546
|
+
if (SAFE_TOOLS.has(toolName)) {
|
|
547
|
+
return { behavior: "allow" };
|
|
620
548
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
input
|
|
627
|
-
|
|
549
|
+
if (this.mode === "acceptEdits") {
|
|
550
|
+
if (EDIT_TOOLS.has(toolName)) {
|
|
551
|
+
return { behavior: "allow" };
|
|
552
|
+
}
|
|
553
|
+
if (toolName === "Bash") {
|
|
554
|
+
const cmd = String(input?.command ?? "").trimStart();
|
|
555
|
+
if (FS_COMMANDS.some((fc) => cmd.startsWith(fc + " ") || cmd === fc)) {
|
|
556
|
+
return { behavior: "allow" };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
628
559
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
560
|
+
if (this.canUseTool) {
|
|
561
|
+
const result = await this.canUseTool(toolName, input, {
|
|
562
|
+
...options,
|
|
563
|
+
toolUseID: options.toolUseId,
|
|
564
|
+
agentID: options.agentId
|
|
565
|
+
});
|
|
566
|
+
if (result.behavior === "allow" && result.updatedPermissions) {
|
|
567
|
+
this.applyPermissionUpdates(result.updatedPermissions);
|
|
568
|
+
}
|
|
569
|
+
return result;
|
|
632
570
|
}
|
|
633
|
-
if (
|
|
634
|
-
|
|
571
|
+
if (this.mode === "dontAsk") {
|
|
572
|
+
return {
|
|
573
|
+
behavior: "deny",
|
|
574
|
+
message: `Tool "${toolName}" requires approval. In dontAsk mode, tools must be pre-approved via permissions config.`
|
|
575
|
+
};
|
|
635
576
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
577
|
+
return { behavior: "allow" };
|
|
578
|
+
}
|
|
579
|
+
applyPermissionUpdates(updates) {
|
|
580
|
+
for (const update of updates) {
|
|
581
|
+
if (this.settingsManager && update.destination !== "session" && update.destination !== "cliArg") {
|
|
582
|
+
this.settingsManager.persistUpdate(update);
|
|
639
583
|
}
|
|
640
|
-
|
|
641
|
-
|
|
584
|
+
switch (update.type) {
|
|
585
|
+
case "addRules":
|
|
586
|
+
for (const rule of update.rules) {
|
|
587
|
+
if (update.behavior === "allow") {
|
|
588
|
+
this.allowRules.push(rule);
|
|
589
|
+
} else if (update.behavior === "deny") {
|
|
590
|
+
this.denyRules.push(rule);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
case "removeRules":
|
|
595
|
+
for (const rule of update.rules) {
|
|
596
|
+
if (update.behavior === "allow") {
|
|
597
|
+
this.allowRules = this.allowRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
|
|
598
|
+
} else if (update.behavior === "deny") {
|
|
599
|
+
this.denyRules = this.denyRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
break;
|
|
603
|
+
case "replaceRules":
|
|
604
|
+
if (update.behavior === "allow") {
|
|
605
|
+
this.allowRules = [...update.rules];
|
|
606
|
+
} else if (update.behavior === "deny") {
|
|
607
|
+
this.denyRules = [...update.rules];
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
case "setMode":
|
|
611
|
+
this.mode = update.mode;
|
|
612
|
+
break;
|
|
642
613
|
}
|
|
643
|
-
yield {
|
|
644
|
-
type: "result",
|
|
645
|
-
subtype: "success",
|
|
646
|
-
text: assistantText || null,
|
|
647
|
-
turns,
|
|
648
|
-
costUsd,
|
|
649
|
-
durationMs: Date.now() - startTime,
|
|
650
|
-
durationApiMs: apiTimeMs,
|
|
651
|
-
sessionId,
|
|
652
|
-
usage: totalUsage,
|
|
653
|
-
modelUsage,
|
|
654
|
-
uuid: uuid()
|
|
655
|
-
};
|
|
656
|
-
return;
|
|
657
614
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
615
|
+
}
|
|
616
|
+
setMode(mode) {
|
|
617
|
+
this.mode = mode;
|
|
618
|
+
}
|
|
619
|
+
getMode() {
|
|
620
|
+
return this.mode;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/settings.ts
|
|
625
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
626
|
+
import { dirname, join } from "path";
|
|
627
|
+
import { homedir } from "os";
|
|
628
|
+
|
|
629
|
+
class SettingsManager {
|
|
630
|
+
cwd;
|
|
631
|
+
constructor(cwd) {
|
|
632
|
+
this.cwd = cwd;
|
|
633
|
+
}
|
|
634
|
+
loadPermissions(sources) {
|
|
635
|
+
const allAllow = [];
|
|
636
|
+
const allDeny = [];
|
|
637
|
+
for (const source of sources) {
|
|
638
|
+
const path = this.sourceToPath(source);
|
|
639
|
+
const data = this.readJson(path);
|
|
640
|
+
if (!data?.permissions)
|
|
641
|
+
continue;
|
|
642
|
+
const perms = data.permissions;
|
|
643
|
+
if (Array.isArray(perms.allow)) {
|
|
644
|
+
for (const rule of perms.allow) {
|
|
645
|
+
if (typeof rule === "string") {
|
|
646
|
+
allAllow.push(this.parseRule(rule));
|
|
670
647
|
}
|
|
671
648
|
}
|
|
672
649
|
}
|
|
673
|
-
if (
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
name: call.name,
|
|
679
|
-
content: denyContent,
|
|
680
|
-
isError: true,
|
|
681
|
-
uuid: uuid()
|
|
682
|
-
};
|
|
683
|
-
toolResults.push({
|
|
684
|
-
type: "tool_result",
|
|
685
|
-
tool_use_id: call.id,
|
|
686
|
-
content: denyContent,
|
|
687
|
-
is_error: true
|
|
688
|
-
});
|
|
689
|
-
if (hooks) {
|
|
690
|
-
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
650
|
+
if (Array.isArray(perms.deny)) {
|
|
651
|
+
for (const rule of perms.deny) {
|
|
652
|
+
if (typeof rule === "string") {
|
|
653
|
+
allDeny.push(this.parseRule(rule));
|
|
654
|
+
}
|
|
691
655
|
}
|
|
692
|
-
continue;
|
|
693
656
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
657
|
+
}
|
|
658
|
+
const result = {};
|
|
659
|
+
if (allAllow.length > 0)
|
|
660
|
+
result.allow = allAllow;
|
|
661
|
+
if (allDeny.length > 0)
|
|
662
|
+
result.deny = allDeny;
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
persistUpdate(update) {
|
|
666
|
+
const path = this.destinationToPath(update.destination);
|
|
667
|
+
if (!path)
|
|
668
|
+
return;
|
|
669
|
+
const data = this.readJson(path) ?? {};
|
|
670
|
+
if (!data.permissions)
|
|
671
|
+
data.permissions = {};
|
|
672
|
+
const perms = data.permissions;
|
|
673
|
+
switch (update.type) {
|
|
674
|
+
case "addRules": {
|
|
675
|
+
const key = update.behavior;
|
|
676
|
+
const existing = Array.isArray(perms[key]) ? perms[key] : [];
|
|
677
|
+
const newRules = update.rules.map((r) => this.serializeRule(r));
|
|
678
|
+
const set = new Set(existing);
|
|
679
|
+
for (const rule of newRules)
|
|
680
|
+
set.add(rule);
|
|
681
|
+
perms[key] = [...set];
|
|
682
|
+
break;
|
|
716
683
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
};
|
|
725
|
-
let result;
|
|
726
|
-
if (call.name === "memory" && nativeMemoryTool) {
|
|
727
|
-
try {
|
|
728
|
-
const content = await nativeMemoryTool.execute(toolInput);
|
|
729
|
-
result = { content, isError: content.startsWith("Error:") };
|
|
730
|
-
} catch (err) {
|
|
731
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
732
|
-
result = { content: `Error: ${message}`, isError: true };
|
|
733
|
-
}
|
|
734
|
-
} else {
|
|
735
|
-
const toolCtx = {
|
|
736
|
-
cwd,
|
|
737
|
-
signal,
|
|
738
|
-
sessionId,
|
|
739
|
-
env
|
|
740
|
-
};
|
|
741
|
-
result = await tools.execute(call.name, toolInput, toolCtx);
|
|
684
|
+
case "removeRules": {
|
|
685
|
+
const key = update.behavior;
|
|
686
|
+
if (!Array.isArray(perms[key]))
|
|
687
|
+
break;
|
|
688
|
+
const toRemove = new Set(update.rules.map((r) => this.serializeRule(r)));
|
|
689
|
+
perms[key] = perms[key].filter((r) => !toRemove.has(r));
|
|
690
|
+
break;
|
|
742
691
|
}
|
|
743
|
-
|
|
744
|
-
|
|
692
|
+
case "replaceRules": {
|
|
693
|
+
const key = update.behavior;
|
|
694
|
+
perms[key] = update.rules.map((r) => this.serializeRule(r));
|
|
695
|
+
break;
|
|
745
696
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
} else {
|
|
750
|
-
const postResult = await hooks.fire("PostToolUse", { event: "PostToolUse", tool_name: call.name, tool_result: result.content, session_id: sessionId }, call.id, { signal });
|
|
751
|
-
if (postResult?.additionalContext) {
|
|
752
|
-
result.content += `
|
|
753
|
-
${postResult.additionalContext}`;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
697
|
+
case "setMode": {
|
|
698
|
+
perms.defaultMode = update.mode;
|
|
699
|
+
break;
|
|
756
700
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
id: call.id,
|
|
760
|
-
name: call.name,
|
|
761
|
-
content: result.content,
|
|
762
|
-
isError: result.isError,
|
|
763
|
-
uuid: uuid()
|
|
764
|
-
};
|
|
765
|
-
toolResults.push({
|
|
766
|
-
type: "tool_result",
|
|
767
|
-
tool_use_id: call.id,
|
|
768
|
-
content: result.content,
|
|
769
|
-
is_error: result.isError
|
|
770
|
-
});
|
|
701
|
+
default:
|
|
702
|
+
return;
|
|
771
703
|
}
|
|
772
|
-
|
|
773
|
-
if (
|
|
774
|
-
|
|
704
|
+
const dir = dirname(path);
|
|
705
|
+
if (!existsSync(dir))
|
|
706
|
+
mkdirSync(dir, { recursive: true });
|
|
707
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
708
|
+
`);
|
|
709
|
+
}
|
|
710
|
+
sourceToPath(source) {
|
|
711
|
+
switch (source) {
|
|
712
|
+
case "user":
|
|
713
|
+
return join(homedir(), ".claude", "settings.json");
|
|
714
|
+
case "project":
|
|
715
|
+
return join(this.cwd, ".claude", "settings.json");
|
|
716
|
+
case "local":
|
|
717
|
+
return join(this.cwd, ".claude", "settings.local.json");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
destinationToPath(destination) {
|
|
721
|
+
switch (destination) {
|
|
722
|
+
case "userSettings":
|
|
723
|
+
return join(homedir(), ".claude", "settings.json");
|
|
724
|
+
case "projectSettings":
|
|
725
|
+
return join(this.cwd, ".claude", "settings.json");
|
|
726
|
+
case "localSettings":
|
|
727
|
+
return join(this.cwd, ".claude", "settings.local.json");
|
|
728
|
+
default:
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
readJson(path) {
|
|
733
|
+
try {
|
|
734
|
+
const content = readFileSync(path, "utf-8");
|
|
735
|
+
return JSON.parse(content);
|
|
736
|
+
} catch {
|
|
737
|
+
return null;
|
|
775
738
|
}
|
|
776
739
|
}
|
|
740
|
+
parseRule(s) {
|
|
741
|
+
const match = s.match(/^([^(]+)\((.+)\)$/);
|
|
742
|
+
if (match)
|
|
743
|
+
return { toolName: match[1], ruleContent: match[2] };
|
|
744
|
+
return { toolName: s };
|
|
745
|
+
}
|
|
746
|
+
serializeRule(rule) {
|
|
747
|
+
return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/types.ts
|
|
752
|
+
function uuid() {
|
|
753
|
+
return crypto.randomUUID();
|
|
777
754
|
}
|
|
778
|
-
function
|
|
755
|
+
function emptyTokenUsage() {
|
|
779
756
|
return {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
757
|
+
inputTokens: 0,
|
|
758
|
+
outputTokens: 0,
|
|
759
|
+
cacheReadInputTokens: 0,
|
|
760
|
+
cacheCreationInputTokens: 0
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function mergeUsage(a, b) {
|
|
764
|
+
return {
|
|
765
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
766
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
767
|
+
cacheReadInputTokens: a.cacheReadInputTokens + b.cacheReadInputTokens,
|
|
768
|
+
cacheCreationInputTokens: a.cacheCreationInputTokens + b.cacheCreationInputTokens
|
|
788
769
|
};
|
|
789
770
|
}
|
|
790
771
|
|
|
@@ -1043,6 +1024,44 @@ class AnthropicAdapter {
|
|
|
1043
1024
|
max_tokens: maxTokens,
|
|
1044
1025
|
stream: true
|
|
1045
1026
|
};
|
|
1027
|
+
if (request.thinking) {
|
|
1028
|
+
switch (request.thinking.type) {
|
|
1029
|
+
case "adaptive":
|
|
1030
|
+
params.thinking = { type: "adaptive" };
|
|
1031
|
+
break;
|
|
1032
|
+
case "disabled":
|
|
1033
|
+
params.thinking = { type: "disabled" };
|
|
1034
|
+
break;
|
|
1035
|
+
case "enabled":
|
|
1036
|
+
params.thinking = {
|
|
1037
|
+
type: "enabled",
|
|
1038
|
+
budget_tokens: request.thinking.budgetTokens
|
|
1039
|
+
};
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
} else if (request.thinkingBudget !== undefined) {
|
|
1043
|
+
if (request.thinkingBudget <= 0) {
|
|
1044
|
+
params.thinking = { type: "disabled" };
|
|
1045
|
+
} else {
|
|
1046
|
+
params.thinking = {
|
|
1047
|
+
type: "enabled",
|
|
1048
|
+
budget_tokens: Math.max(1024, request.thinkingBudget)
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const outputConfig = {};
|
|
1053
|
+
if (request.effort) {
|
|
1054
|
+
outputConfig.effort = request.effort;
|
|
1055
|
+
}
|
|
1056
|
+
if (request.outputFormat?.type === "json_schema") {
|
|
1057
|
+
outputConfig.format = {
|
|
1058
|
+
type: "json_schema",
|
|
1059
|
+
schema: request.outputFormat.schema
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
if (Object.keys(outputConfig).length > 0) {
|
|
1063
|
+
params.output_config = outputConfig;
|
|
1064
|
+
}
|
|
1046
1065
|
if (this.oauthMode) {
|
|
1047
1066
|
const systemBlocks = [
|
|
1048
1067
|
{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }
|
|
@@ -1167,11 +1186,12 @@ class AnthropicAdapter {
|
|
|
1167
1186
|
switch (feature) {
|
|
1168
1187
|
case "streaming":
|
|
1169
1188
|
case "tool_calling":
|
|
1170
|
-
case "image_input":
|
|
1171
|
-
case "pdf_input":
|
|
1172
1189
|
case "thinking":
|
|
1173
1190
|
case "structured_output":
|
|
1174
1191
|
return true;
|
|
1192
|
+
case "image_input":
|
|
1193
|
+
case "pdf_input":
|
|
1194
|
+
return false;
|
|
1175
1195
|
default:
|
|
1176
1196
|
return false;
|
|
1177
1197
|
}
|
|
@@ -2086,6 +2106,16 @@ class ToolRegistry {
|
|
|
2086
2106
|
register(tool) {
|
|
2087
2107
|
this.tools.set(tool.name, tool);
|
|
2088
2108
|
}
|
|
2109
|
+
unregister(name) {
|
|
2110
|
+
this.tools.delete(name);
|
|
2111
|
+
}
|
|
2112
|
+
clearByPrefix(prefix) {
|
|
2113
|
+
for (const name of this.tools.keys()) {
|
|
2114
|
+
if (name.startsWith(prefix)) {
|
|
2115
|
+
this.tools.delete(name);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2089
2119
|
get(name) {
|
|
2090
2120
|
return this.tools.get(name);
|
|
2091
2121
|
}
|
|
@@ -2119,16 +2149,34 @@ class ToolRegistry {
|
|
|
2119
2149
|
// src/tools/presets.ts
|
|
2120
2150
|
var PRESETS = {
|
|
2121
2151
|
coding: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
|
|
2152
|
+
claude_code: [
|
|
2153
|
+
"Bash",
|
|
2154
|
+
"Read",
|
|
2155
|
+
"Write",
|
|
2156
|
+
"Edit",
|
|
2157
|
+
"Glob",
|
|
2158
|
+
"Grep",
|
|
2159
|
+
"NotebookEdit",
|
|
2160
|
+
"WebFetch",
|
|
2161
|
+
"WebSearch",
|
|
2162
|
+
"TodoWrite",
|
|
2163
|
+
"Config",
|
|
2164
|
+
"AskUserQuestion",
|
|
2165
|
+
"ExitPlanMode"
|
|
2166
|
+
],
|
|
2122
2167
|
readonly: ["Read", "Glob", "Grep"],
|
|
2123
2168
|
minimal: ["Read", "Write", "Edit", "Glob", "Grep"]
|
|
2124
2169
|
};
|
|
2125
2170
|
function resolveToolNames(tools) {
|
|
2126
2171
|
if (!tools)
|
|
2127
|
-
return PRESETS.
|
|
2128
|
-
if (
|
|
2129
|
-
return
|
|
2172
|
+
return PRESETS.claude_code;
|
|
2173
|
+
if (Array.isArray(tools)) {
|
|
2174
|
+
return tools;
|
|
2175
|
+
}
|
|
2176
|
+
if (tools.type === "preset") {
|
|
2177
|
+
return PRESETS[tools.preset] ?? PRESETS.claude_code;
|
|
2130
2178
|
}
|
|
2131
|
-
|
|
2179
|
+
throw new Error("Invalid tools option. Expected string[] or { type: 'preset', preset: 'claude_code' }.");
|
|
2132
2180
|
}
|
|
2133
2181
|
|
|
2134
2182
|
// src/tools/bash.ts
|
|
@@ -2152,17 +2200,58 @@ var BashTool = {
|
|
|
2152
2200
|
timeout: {
|
|
2153
2201
|
type: "number",
|
|
2154
2202
|
description: "Timeout in milliseconds (max 600000)"
|
|
2203
|
+
},
|
|
2204
|
+
run_in_background: {
|
|
2205
|
+
type: "boolean",
|
|
2206
|
+
description: "Run command asynchronously and return immediately."
|
|
2207
|
+
},
|
|
2208
|
+
dangerouslyDisableSandbox: {
|
|
2209
|
+
type: "boolean",
|
|
2210
|
+
description: "If true, explicitly request unsandboxed execution."
|
|
2211
|
+
},
|
|
2212
|
+
_simulatedSedEdit: {
|
|
2213
|
+
type: "object",
|
|
2214
|
+
properties: {
|
|
2215
|
+
filePath: { type: "string" },
|
|
2216
|
+
newContent: { type: "string" }
|
|
2217
|
+
},
|
|
2218
|
+
description: "Internal field for precomputed edit previews."
|
|
2155
2219
|
}
|
|
2156
2220
|
},
|
|
2157
2221
|
required: ["command"]
|
|
2158
2222
|
},
|
|
2159
2223
|
async execute(input, ctx) {
|
|
2160
|
-
const {
|
|
2224
|
+
const {
|
|
2225
|
+
command,
|
|
2226
|
+
timeout: timeoutMs,
|
|
2227
|
+
run_in_background,
|
|
2228
|
+
description,
|
|
2229
|
+
dangerouslyDisableSandbox,
|
|
2230
|
+
_simulatedSedEdit
|
|
2231
|
+
} = input;
|
|
2161
2232
|
if (!command || typeof command !== "string") {
|
|
2162
2233
|
return { content: "Error: command is required", isError: true };
|
|
2163
2234
|
}
|
|
2164
2235
|
const timeout = Math.min(timeoutMs ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
2165
2236
|
try {
|
|
2237
|
+
if (run_in_background) {
|
|
2238
|
+
const proc2 = Bun.spawn(["bash", "-c", command], {
|
|
2239
|
+
cwd: ctx.cwd,
|
|
2240
|
+
stdout: "ignore",
|
|
2241
|
+
stderr: "ignore",
|
|
2242
|
+
stdin: "ignore",
|
|
2243
|
+
env: { ...process.env, ...ctx.env }
|
|
2244
|
+
});
|
|
2245
|
+
return {
|
|
2246
|
+
content: `Background command started (pid ${proc2.pid ?? "unknown"}).`,
|
|
2247
|
+
metadata: {
|
|
2248
|
+
pid: proc2.pid ?? null,
|
|
2249
|
+
run_in_background: true,
|
|
2250
|
+
dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
|
|
2251
|
+
hasSimulatedSedEdit: !!_simulatedSedEdit
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2166
2255
|
const proc = Bun.spawn(["bash", "-c", command], {
|
|
2167
2256
|
cwd: ctx.cwd,
|
|
2168
2257
|
stdout: "pipe",
|
|
@@ -2194,7 +2283,11 @@ var BashTool = {
|
|
|
2194
2283
|
return {
|
|
2195
2284
|
content: output,
|
|
2196
2285
|
isError: exitCode !== 0 ? true : undefined,
|
|
2197
|
-
metadata: {
|
|
2286
|
+
metadata: {
|
|
2287
|
+
exitCode,
|
|
2288
|
+
dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
|
|
2289
|
+
hasSimulatedSedEdit: !!_simulatedSedEdit
|
|
2290
|
+
}
|
|
2198
2291
|
};
|
|
2199
2292
|
} catch (err) {
|
|
2200
2293
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2223,12 +2316,17 @@ var ReadTool = {
|
|
|
2223
2316
|
limit: {
|
|
2224
2317
|
type: "number",
|
|
2225
2318
|
description: "Number of lines to read"
|
|
2319
|
+
},
|
|
2320
|
+
pages: {
|
|
2321
|
+
type: "array",
|
|
2322
|
+
items: { type: "number" },
|
|
2323
|
+
description: "Optional PDF page numbers (1-based)."
|
|
2226
2324
|
}
|
|
2227
2325
|
},
|
|
2228
2326
|
required: ["file_path"]
|
|
2229
2327
|
},
|
|
2230
2328
|
async execute(input, ctx) {
|
|
2231
|
-
const { file_path, offset, limit } = input;
|
|
2329
|
+
const { file_path, offset, limit, pages } = input;
|
|
2232
2330
|
if (!file_path) {
|
|
2233
2331
|
return { content: "Error: file_path is required", isError: true };
|
|
2234
2332
|
}
|
|
@@ -2239,6 +2337,12 @@ var ReadTool = {
|
|
|
2239
2337
|
if (!exists) {
|
|
2240
2338
|
return { content: `Error: File not found: ${resolvedPath}`, isError: true };
|
|
2241
2339
|
}
|
|
2340
|
+
if (Array.isArray(pages) && pages.length > 0 && resolvedPath.toLowerCase().endsWith(".pdf")) {
|
|
2341
|
+
return {
|
|
2342
|
+
content: "Error: PDF page extraction is not implemented in this runtime.",
|
|
2343
|
+
isError: true
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2242
2346
|
const text = await file.text();
|
|
2243
2347
|
const lines = text.split(`
|
|
2244
2348
|
`);
|
|
@@ -2270,7 +2374,7 @@ function resolvePath(filePath, cwd) {
|
|
|
2270
2374
|
|
|
2271
2375
|
// src/tools/write.ts
|
|
2272
2376
|
import { mkdir } from "fs/promises";
|
|
2273
|
-
import { dirname } from "path";
|
|
2377
|
+
import { dirname as dirname2 } from "path";
|
|
2274
2378
|
var WriteTool = {
|
|
2275
2379
|
name: "Write",
|
|
2276
2380
|
description: "Writes content to a file. Creates parent directories if needed. " + "Overwrites existing files.",
|
|
@@ -2298,7 +2402,7 @@ var WriteTool = {
|
|
|
2298
2402
|
}
|
|
2299
2403
|
const resolvedPath = file_path.startsWith("/") ? file_path : `${ctx.cwd}/${file_path}`;
|
|
2300
2404
|
try {
|
|
2301
|
-
const dir =
|
|
2405
|
+
const dir = dirname2(resolvedPath);
|
|
2302
2406
|
await mkdir(dir, { recursive: true });
|
|
2303
2407
|
await Bun.write(resolvedPath, content);
|
|
2304
2408
|
const lines = content.split(`
|
|
@@ -2593,327 +2697,465 @@ async function runJsGrep(opts, searchPath, mode, ctx) {
|
|
|
2593
2697
|
totalCount += matchedLines.length;
|
|
2594
2698
|
const relativePath = filePath.startsWith(ctx.cwd) ? filePath.slice(ctx.cwd.length + 1) : filePath;
|
|
2595
2699
|
if (mode === "files_with_matches") {
|
|
2596
|
-
results.push(relativePath);
|
|
2597
|
-
} else if (mode === "count") {
|
|
2598
|
-
results.push(`${relativePath}:${matchedLines.length}`);
|
|
2599
|
-
} else {
|
|
2600
|
-
for (const { num, line } of matchedLines) {
|
|
2601
|
-
results.push(`${relativePath}:${num}:${line}`);
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
} catch {}
|
|
2605
|
-
if (opts.head_limit && results.length >= opts.head_limit) {
|
|
2606
|
-
break;
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
let output = results.join(`
|
|
2610
|
-
`);
|
|
2611
|
-
if (opts.head_limit) {
|
|
2612
|
-
const entries = output.split(`
|
|
2613
|
-
`).slice(0, opts.head_limit);
|
|
2614
|
-
output = entries.join(`
|
|
2615
|
-
`);
|
|
2616
|
-
}
|
|
2617
|
-
return { content: output || "No matches found." };
|
|
2618
|
-
}
|
|
2619
|
-
async function collectFiles(dir, globPattern) {
|
|
2620
|
-
const { Glob: Glob2 } = await Promise.resolve(globalThis.Bun);
|
|
2621
|
-
const pattern = globPattern ?? "**/*";
|
|
2622
|
-
const glob = new Glob2(pattern);
|
|
2623
|
-
const files = [];
|
|
2624
|
-
for await (const path of glob.scan({ cwd: dir, dot: false, onlyFiles: true })) {
|
|
2625
|
-
files.push(`${dir}/${path}`);
|
|
2626
|
-
}
|
|
2627
|
-
return files;
|
|
2628
|
-
}
|
|
2629
|
-
// src/tools/index.ts
|
|
2630
|
-
var ALL_TOOLS = {
|
|
2631
|
-
Bash: BashTool,
|
|
2632
|
-
Read: ReadTool,
|
|
2633
|
-
Write: WriteTool,
|
|
2634
|
-
Edit: EditTool,
|
|
2635
|
-
Glob: GlobTool,
|
|
2636
|
-
Grep: GrepTool
|
|
2637
|
-
};
|
|
2638
|
-
function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
|
|
2639
|
-
const registry = new ToolRegistry;
|
|
2640
|
-
for (const name of toolNames) {
|
|
2641
|
-
if (disallowedTools?.includes(name))
|
|
2642
|
-
continue;
|
|
2643
|
-
const tool = ALL_TOOLS[name];
|
|
2644
|
-
if (tool) {
|
|
2645
|
-
registry.register(tool);
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
return registry;
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
// src/permissions.ts
|
|
2652
|
-
var SAFE_TOOLS = new Set(["Read", "Glob", "Grep"]);
|
|
2653
|
-
var EDIT_TOOLS = new Set(["Write", "Edit"]);
|
|
2654
|
-
var FS_COMMANDS = ["mkdir", "touch", "rm", "mv", "cp"];
|
|
2655
|
-
var DELEGATE_TOOLS = new Set(["Teammate", "Task", "TaskOutput", "TaskStop"]);
|
|
2656
|
-
function normalizeRules(rules) {
|
|
2657
|
-
if (!rules)
|
|
2658
|
-
return [];
|
|
2659
|
-
return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
|
|
2660
|
-
}
|
|
2661
|
-
function matchesRule(rules, toolName, input) {
|
|
2662
|
-
for (const rule of rules) {
|
|
2663
|
-
if (rule.toolName !== toolName)
|
|
2664
|
-
continue;
|
|
2665
|
-
if (!rule.ruleContent)
|
|
2666
|
-
return true;
|
|
2667
|
-
const inputStr = toolName === "Bash" ? String(input?.command ?? "") : JSON.stringify(input ?? {});
|
|
2668
|
-
if (inputStr.includes(rule.ruleContent))
|
|
2669
|
-
return true;
|
|
2670
|
-
}
|
|
2671
|
-
return false;
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
class PermissionManager {
|
|
2675
|
-
mode;
|
|
2676
|
-
canUseTool;
|
|
2677
|
-
allowRules;
|
|
2678
|
-
denyRules;
|
|
2679
|
-
settingsManager;
|
|
2680
|
-
constructor(mode = "default", canUseTool, permissions, settingsManager) {
|
|
2681
|
-
this.mode = mode;
|
|
2682
|
-
this.canUseTool = canUseTool;
|
|
2683
|
-
this.allowRules = normalizeRules(permissions?.allow);
|
|
2684
|
-
this.denyRules = normalizeRules(permissions?.deny);
|
|
2685
|
-
this.settingsManager = settingsManager;
|
|
2686
|
-
}
|
|
2687
|
-
async check(toolName, input, options) {
|
|
2688
|
-
if (this.mode === "bypassPermissions") {
|
|
2689
|
-
return { behavior: "allow" };
|
|
2690
|
-
}
|
|
2691
|
-
if (matchesRule(this.denyRules, toolName, input)) {
|
|
2692
|
-
return {
|
|
2693
|
-
behavior: "deny",
|
|
2694
|
-
message: `Tool "${toolName}" is denied by permissions config.`
|
|
2695
|
-
};
|
|
2696
|
-
}
|
|
2697
|
-
if (this.mode === "plan") {
|
|
2698
|
-
if (!SAFE_TOOLS.has(toolName)) {
|
|
2699
|
-
return {
|
|
2700
|
-
behavior: "deny",
|
|
2701
|
-
message: `Tool "${toolName}" is not allowed in plan mode. Only read-only tools are available.`
|
|
2702
|
-
};
|
|
2703
|
-
}
|
|
2704
|
-
return { behavior: "allow" };
|
|
2705
|
-
}
|
|
2706
|
-
if (this.mode === "delegate") {
|
|
2707
|
-
if (!DELEGATE_TOOLS.has(toolName) && !SAFE_TOOLS.has(toolName)) {
|
|
2708
|
-
return {
|
|
2709
|
-
behavior: "deny",
|
|
2710
|
-
message: `Tool "${toolName}" is not allowed in delegate mode. Only Teammate, Task, and read-only tools are available.`
|
|
2711
|
-
};
|
|
2712
|
-
}
|
|
2713
|
-
return { behavior: "allow" };
|
|
2714
|
-
}
|
|
2715
|
-
if (matchesRule(this.allowRules, toolName, input)) {
|
|
2716
|
-
return { behavior: "allow" };
|
|
2717
|
-
}
|
|
2718
|
-
if (SAFE_TOOLS.has(toolName)) {
|
|
2719
|
-
return { behavior: "allow" };
|
|
2720
|
-
}
|
|
2721
|
-
if (this.mode === "acceptEdits") {
|
|
2722
|
-
if (EDIT_TOOLS.has(toolName)) {
|
|
2723
|
-
return { behavior: "allow" };
|
|
2724
|
-
}
|
|
2725
|
-
if (toolName === "Bash") {
|
|
2726
|
-
const cmd = String(input?.command ?? "").trimStart();
|
|
2727
|
-
if (FS_COMMANDS.some((fc) => cmd.startsWith(fc + " ") || cmd === fc)) {
|
|
2728
|
-
return { behavior: "allow" };
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
if (this.canUseTool) {
|
|
2733
|
-
const result = await this.canUseTool(toolName, input, options);
|
|
2734
|
-
if (result.behavior === "allow" && result.updatedPermissions) {
|
|
2735
|
-
this.applyPermissionUpdates(result.updatedPermissions);
|
|
2736
|
-
}
|
|
2737
|
-
return result;
|
|
2738
|
-
}
|
|
2739
|
-
if (this.mode === "dontAsk") {
|
|
2740
|
-
return {
|
|
2741
|
-
behavior: "deny",
|
|
2742
|
-
message: `Tool "${toolName}" requires approval. In dontAsk mode, tools must be pre-approved via permissions config.`
|
|
2743
|
-
};
|
|
2744
|
-
}
|
|
2745
|
-
return { behavior: "allow" };
|
|
2746
|
-
}
|
|
2747
|
-
applyPermissionUpdates(updates) {
|
|
2748
|
-
for (const update of updates) {
|
|
2749
|
-
if (this.settingsManager && update.destination !== "session" && update.destination !== "cliArg") {
|
|
2750
|
-
this.settingsManager.persistUpdate(update);
|
|
2751
|
-
}
|
|
2752
|
-
switch (update.type) {
|
|
2753
|
-
case "addRules":
|
|
2754
|
-
for (const rule of update.rules) {
|
|
2755
|
-
if (update.behavior === "allow") {
|
|
2756
|
-
this.allowRules.push(rule);
|
|
2757
|
-
} else if (update.behavior === "deny") {
|
|
2758
|
-
this.denyRules.push(rule);
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
break;
|
|
2762
|
-
case "removeRules":
|
|
2763
|
-
for (const rule of update.rules) {
|
|
2764
|
-
if (update.behavior === "allow") {
|
|
2765
|
-
this.allowRules = this.allowRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
|
|
2766
|
-
} else if (update.behavior === "deny") {
|
|
2767
|
-
this.denyRules = this.denyRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
break;
|
|
2771
|
-
case "replaceRules":
|
|
2772
|
-
if (update.behavior === "allow") {
|
|
2773
|
-
this.allowRules = [...update.rules];
|
|
2774
|
-
} else if (update.behavior === "deny") {
|
|
2775
|
-
this.denyRules = [...update.rules];
|
|
2776
|
-
}
|
|
2777
|
-
break;
|
|
2778
|
-
case "setMode":
|
|
2779
|
-
this.mode = update.mode;
|
|
2780
|
-
break;
|
|
2700
|
+
results.push(relativePath);
|
|
2701
|
+
} else if (mode === "count") {
|
|
2702
|
+
results.push(`${relativePath}:${matchedLines.length}`);
|
|
2703
|
+
} else {
|
|
2704
|
+
for (const { num, line } of matchedLines) {
|
|
2705
|
+
results.push(`${relativePath}:${num}:${line}`);
|
|
2706
|
+
}
|
|
2781
2707
|
}
|
|
2708
|
+
} catch {}
|
|
2709
|
+
if (opts.head_limit && results.length >= opts.head_limit) {
|
|
2710
|
+
break;
|
|
2782
2711
|
}
|
|
2783
2712
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2713
|
+
let output = results.join(`
|
|
2714
|
+
`);
|
|
2715
|
+
if (opts.head_limit) {
|
|
2716
|
+
const entries = output.split(`
|
|
2717
|
+
`).slice(0, opts.head_limit);
|
|
2718
|
+
output = entries.join(`
|
|
2719
|
+
`);
|
|
2786
2720
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2721
|
+
return { content: output || "No matches found." };
|
|
2722
|
+
}
|
|
2723
|
+
async function collectFiles(dir, globPattern) {
|
|
2724
|
+
const { Glob: Glob2 } = await Promise.resolve(globalThis.Bun);
|
|
2725
|
+
const pattern = globPattern ?? "**/*";
|
|
2726
|
+
const glob = new Glob2(pattern);
|
|
2727
|
+
const files = [];
|
|
2728
|
+
for await (const path of glob.scan({ cwd: dir, dot: false, onlyFiles: true })) {
|
|
2729
|
+
files.push(`${dir}/${path}`);
|
|
2789
2730
|
}
|
|
2731
|
+
return files;
|
|
2790
2732
|
}
|
|
2791
2733
|
|
|
2792
|
-
// src/
|
|
2793
|
-
import {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2734
|
+
// src/tools/notebook-edit.ts
|
|
2735
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2736
|
+
function toSourceLines(text) {
|
|
2737
|
+
const lines = text.split(`
|
|
2738
|
+
`);
|
|
2739
|
+
return lines.map((line, idx) => idx < lines.length - 1 ? `${line}
|
|
2740
|
+
` : line);
|
|
2741
|
+
}
|
|
2742
|
+
var NotebookEditTool = {
|
|
2743
|
+
name: "NotebookEdit",
|
|
2744
|
+
description: "Edit a specific Jupyter notebook cell by id or index.",
|
|
2745
|
+
inputSchema: {
|
|
2746
|
+
type: "object",
|
|
2747
|
+
properties: {
|
|
2748
|
+
notebook_path: { type: "string", description: "Path to .ipynb file." },
|
|
2749
|
+
cell_id: { type: "string", description: "Cell id to edit." },
|
|
2750
|
+
cell_index: { type: "number", description: "Cell index to edit if id is not provided." },
|
|
2751
|
+
new_source: { type: "string", description: "New cell source content." }
|
|
2752
|
+
},
|
|
2753
|
+
required: ["notebook_path", "new_source"]
|
|
2754
|
+
},
|
|
2755
|
+
async execute(input, ctx) {
|
|
2756
|
+
const {
|
|
2757
|
+
notebook_path,
|
|
2758
|
+
cell_id,
|
|
2759
|
+
cell_index,
|
|
2760
|
+
new_source
|
|
2761
|
+
} = input ?? {};
|
|
2762
|
+
if (!notebook_path)
|
|
2763
|
+
return { content: "Error: notebook_path is required", isError: true };
|
|
2764
|
+
if (new_source === undefined)
|
|
2765
|
+
return { content: "Error: new_source is required", isError: true };
|
|
2766
|
+
const filePath = notebook_path.startsWith("/") ? notebook_path : `${ctx.cwd}/${notebook_path}`;
|
|
2767
|
+
try {
|
|
2768
|
+
const raw = await readFile(filePath, "utf-8");
|
|
2769
|
+
const notebook = JSON.parse(raw);
|
|
2770
|
+
if (!Array.isArray(notebook.cells)) {
|
|
2771
|
+
return { content: "Error: notebook has no cells array", isError: true };
|
|
2772
|
+
}
|
|
2773
|
+
let targetIndex = -1;
|
|
2774
|
+
if (cell_id) {
|
|
2775
|
+
targetIndex = notebook.cells.findIndex((c) => c.id === cell_id);
|
|
2776
|
+
} else if (typeof cell_index === "number") {
|
|
2777
|
+
targetIndex = cell_index;
|
|
2778
|
+
} else {
|
|
2779
|
+
targetIndex = 0;
|
|
2780
|
+
}
|
|
2781
|
+
if (targetIndex < 0 || targetIndex >= notebook.cells.length) {
|
|
2782
|
+
return {
|
|
2783
|
+
content: `Error: cell not found (id=${cell_id ?? "n/a"}, index=${String(cell_index ?? "n/a")})`,
|
|
2784
|
+
isError: true
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
const cell = notebook.cells[targetIndex];
|
|
2788
|
+
cell.source = toSourceLines(new_source);
|
|
2789
|
+
await writeFile(filePath, JSON.stringify(notebook, null, 2) + `
|
|
2790
|
+
`, "utf-8");
|
|
2791
|
+
return {
|
|
2792
|
+
content: `Updated notebook cell ${targetIndex} in ${filePath}`
|
|
2793
|
+
};
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2796
|
+
return { content: `Error editing notebook: ${message}`, isError: true };
|
|
2797
|
+
}
|
|
2801
2798
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2801
|
+
// src/tools/web-fetch.ts
|
|
2802
|
+
var DEFAULT_TIMEOUT_MS = 20000;
|
|
2803
|
+
var MAX_OUTPUT = 80000;
|
|
2804
|
+
var WebFetchTool = {
|
|
2805
|
+
name: "WebFetch",
|
|
2806
|
+
description: "Fetches a URL and returns response text.",
|
|
2807
|
+
inputSchema: {
|
|
2808
|
+
type: "object",
|
|
2809
|
+
properties: {
|
|
2810
|
+
url: {
|
|
2811
|
+
type: "string",
|
|
2812
|
+
description: "The URL to fetch."
|
|
2813
|
+
},
|
|
2814
|
+
prompt: {
|
|
2815
|
+
type: "string",
|
|
2816
|
+
description: "Optional fetch intent/instructions."
|
|
2817
|
+
},
|
|
2818
|
+
timeout_ms: {
|
|
2819
|
+
type: "number",
|
|
2820
|
+
description: "Timeout in milliseconds (default 20000)."
|
|
2821
|
+
},
|
|
2822
|
+
max_length: {
|
|
2823
|
+
type: "number",
|
|
2824
|
+
description: "Maximum output length (default 80000)."
|
|
2817
2825
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2826
|
+
},
|
|
2827
|
+
required: ["url"]
|
|
2828
|
+
},
|
|
2829
|
+
async execute(input) {
|
|
2830
|
+
const { url, timeout_ms, max_length } = input ?? {};
|
|
2831
|
+
if (!url)
|
|
2832
|
+
return { content: "Error: url is required", isError: true };
|
|
2833
|
+
const timeout = Math.max(1000, timeout_ms ?? DEFAULT_TIMEOUT_MS);
|
|
2834
|
+
const outLimit = Math.max(1000, max_length ?? MAX_OUTPUT);
|
|
2835
|
+
const controller = new AbortController;
|
|
2836
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
2837
|
+
try {
|
|
2838
|
+
const res = await fetch(url, {
|
|
2839
|
+
method: "GET",
|
|
2840
|
+
signal: controller.signal,
|
|
2841
|
+
headers: {
|
|
2842
|
+
"user-agent": "fourmis-agent-sdk/1.0"
|
|
2823
2843
|
}
|
|
2844
|
+
});
|
|
2845
|
+
const contentType = res.headers.get("content-type") ?? "unknown";
|
|
2846
|
+
let body = await res.text();
|
|
2847
|
+
if (body.length > outLimit) {
|
|
2848
|
+
body = body.slice(0, outLimit) + `
|
|
2849
|
+
... (truncated)`;
|
|
2824
2850
|
}
|
|
2851
|
+
return {
|
|
2852
|
+
content: [
|
|
2853
|
+
`Status: ${res.status} ${res.statusText}`,
|
|
2854
|
+
`Content-Type: ${contentType}`,
|
|
2855
|
+
"",
|
|
2856
|
+
body
|
|
2857
|
+
].join(`
|
|
2858
|
+
`),
|
|
2859
|
+
isError: res.ok ? undefined : true
|
|
2860
|
+
};
|
|
2861
|
+
} catch (err) {
|
|
2862
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2863
|
+
return { content: `Error fetching URL: ${message}`, isError: true };
|
|
2864
|
+
} finally {
|
|
2865
|
+
clearTimeout(timer);
|
|
2825
2866
|
}
|
|
2826
|
-
const result = {};
|
|
2827
|
-
if (allAllow.length > 0)
|
|
2828
|
-
result.allow = allAllow;
|
|
2829
|
-
if (allDeny.length > 0)
|
|
2830
|
-
result.deny = allDeny;
|
|
2831
|
-
return result;
|
|
2832
2867
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2868
|
+
};
|
|
2869
|
+
|
|
2870
|
+
// src/tools/web-search.ts
|
|
2871
|
+
var SEARCH_ENDPOINT = "https://duckduckgo.com/html/";
|
|
2872
|
+
function stripTags(input) {
|
|
2873
|
+
return input.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").trim();
|
|
2874
|
+
}
|
|
2875
|
+
var WebSearchTool = {
|
|
2876
|
+
name: "WebSearch",
|
|
2877
|
+
description: "Searches the web and returns top result links.",
|
|
2878
|
+
inputSchema: {
|
|
2879
|
+
type: "object",
|
|
2880
|
+
properties: {
|
|
2881
|
+
query: {
|
|
2882
|
+
type: "string",
|
|
2883
|
+
description: "Search query."
|
|
2884
|
+
},
|
|
2885
|
+
max_results: {
|
|
2886
|
+
type: "number",
|
|
2887
|
+
description: "Maximum results to return (default 5)."
|
|
2851
2888
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2889
|
+
},
|
|
2890
|
+
required: ["query"]
|
|
2891
|
+
},
|
|
2892
|
+
async execute(input) {
|
|
2893
|
+
const { query, max_results } = input ?? {};
|
|
2894
|
+
if (!query) {
|
|
2895
|
+
return { content: "Error: query is required", isError: true };
|
|
2896
|
+
}
|
|
2897
|
+
const limit = Math.max(1, Math.min(20, max_results ?? 5));
|
|
2898
|
+
try {
|
|
2899
|
+
const url = `${SEARCH_ENDPOINT}?q=${encodeURIComponent(query)}`;
|
|
2900
|
+
const res = await fetch(url, {
|
|
2901
|
+
headers: {
|
|
2902
|
+
"user-agent": "fourmis-agent-sdk/1.0"
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
if (!res.ok) {
|
|
2906
|
+
return {
|
|
2907
|
+
content: `Error searching web: ${res.status} ${res.statusText}`,
|
|
2908
|
+
isError: true
|
|
2909
|
+
};
|
|
2859
2910
|
}
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2911
|
+
const html = await res.text();
|
|
2912
|
+
const matches = [...html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
|
|
2913
|
+
if (matches.length === 0) {
|
|
2914
|
+
return { content: "No search results found." };
|
|
2864
2915
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2916
|
+
const lines = [];
|
|
2917
|
+
for (let i = 0;i < Math.min(limit, matches.length); i++) {
|
|
2918
|
+
const href = stripTags(matches[i][1]);
|
|
2919
|
+
const title = stripTags(matches[i][2]);
|
|
2920
|
+
lines.push(`${i + 1}. ${title}
|
|
2921
|
+
${href}`);
|
|
2868
2922
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2923
|
+
return { content: lines.join(`
|
|
2924
|
+
`) };
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2927
|
+
return { content: `Error searching web: ${message}`, isError: true };
|
|
2871
2928
|
}
|
|
2872
|
-
const dir = dirname2(path);
|
|
2873
|
-
if (!existsSync3(dir))
|
|
2874
|
-
mkdirSync2(dir, { recursive: true });
|
|
2875
|
-
writeFileSync3(path, JSON.stringify(data, null, 2) + `
|
|
2876
|
-
`);
|
|
2877
2929
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2930
|
+
};
|
|
2931
|
+
|
|
2932
|
+
// src/tools/ask-user-question.ts
|
|
2933
|
+
var AskUserQuestionTool = {
|
|
2934
|
+
name: "AskUserQuestion",
|
|
2935
|
+
description: "Ask the user a clarifying question and wait for their response.",
|
|
2936
|
+
inputSchema: {
|
|
2937
|
+
type: "object",
|
|
2938
|
+
properties: {
|
|
2939
|
+
question: {
|
|
2940
|
+
type: "string",
|
|
2941
|
+
description: "Question to ask the user."
|
|
2942
|
+
},
|
|
2943
|
+
options: {
|
|
2944
|
+
type: "array",
|
|
2945
|
+
items: { type: "string" },
|
|
2946
|
+
description: "Optional fixed choices."
|
|
2947
|
+
}
|
|
2948
|
+
},
|
|
2949
|
+
required: ["question"]
|
|
2950
|
+
},
|
|
2951
|
+
async execute(input) {
|
|
2952
|
+
const { question, options } = input ?? {};
|
|
2953
|
+
if (!question) {
|
|
2954
|
+
return { content: "Error: question is required", isError: true };
|
|
2886
2955
|
}
|
|
2956
|
+
const choices = Array.isArray(options) && options.length > 0 ? ` Choices: ${options.join(" | ")}` : "";
|
|
2957
|
+
return {
|
|
2958
|
+
content: `User interaction is not available in this runtime. Unanswered question: ${question}.${choices}`,
|
|
2959
|
+
isError: true
|
|
2960
|
+
};
|
|
2887
2961
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2962
|
+
};
|
|
2963
|
+
|
|
2964
|
+
// src/tools/todo-write.ts
|
|
2965
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
2966
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
2967
|
+
var TodoWriteTool = {
|
|
2968
|
+
name: "TodoWrite",
|
|
2969
|
+
description: "Write/update task todo items for the current session.",
|
|
2970
|
+
inputSchema: {
|
|
2971
|
+
type: "object",
|
|
2972
|
+
properties: {
|
|
2973
|
+
todos: {
|
|
2974
|
+
type: "array",
|
|
2975
|
+
items: {
|
|
2976
|
+
type: "object",
|
|
2977
|
+
properties: {
|
|
2978
|
+
content: { type: "string" },
|
|
2979
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed"] },
|
|
2980
|
+
activeForm: { type: "string" }
|
|
2981
|
+
},
|
|
2982
|
+
required: ["content", "status"]
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
},
|
|
2986
|
+
required: ["todos"]
|
|
2987
|
+
},
|
|
2988
|
+
async execute(input, ctx) {
|
|
2989
|
+
const { todos } = input ?? {};
|
|
2990
|
+
if (!Array.isArray(todos)) {
|
|
2991
|
+
return { content: "Error: todos must be an array", isError: true };
|
|
2992
|
+
}
|
|
2993
|
+
for (const todo of todos) {
|
|
2994
|
+
if (!todo?.content || !todo?.status) {
|
|
2995
|
+
return { content: "Error: each todo requires content and status", isError: true };
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
const filePath = join4(ctx.cwd, ".claude", "todos.json");
|
|
2999
|
+
try {
|
|
3000
|
+
await mkdir2(dirname3(filePath), { recursive: true });
|
|
3001
|
+
const payload = {
|
|
3002
|
+
updatedAt: new Date().toISOString(),
|
|
3003
|
+
todos
|
|
3004
|
+
};
|
|
3005
|
+
await writeFile2(filePath, JSON.stringify(payload, null, 2) + `
|
|
3006
|
+
`, "utf-8");
|
|
3007
|
+
return {
|
|
3008
|
+
content: `Saved ${todos.length} todo item(s) to ${filePath}`
|
|
3009
|
+
};
|
|
3010
|
+
} catch (err) {
|
|
3011
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3012
|
+
return { content: `Error writing todos: ${message}`, isError: true };
|
|
2898
3013
|
}
|
|
2899
3014
|
}
|
|
2900
|
-
|
|
3015
|
+
};
|
|
3016
|
+
|
|
3017
|
+
// src/tools/config.ts
|
|
3018
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
3019
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
3020
|
+
function scopePath(cwd, scope) {
|
|
3021
|
+
if (scope === "project")
|
|
3022
|
+
return join5(cwd, ".claude", "settings.json");
|
|
3023
|
+
return join5(cwd, ".claude", "settings.local.json");
|
|
3024
|
+
}
|
|
3025
|
+
function setByPath(obj, keyPath, value) {
|
|
3026
|
+
const keys = keyPath.split(".").filter(Boolean);
|
|
3027
|
+
if (keys.length === 0)
|
|
3028
|
+
return;
|
|
3029
|
+
let current = obj;
|
|
3030
|
+
for (let i = 0;i < keys.length - 1; i++) {
|
|
3031
|
+
const key = keys[i];
|
|
3032
|
+
const next = current[key];
|
|
3033
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
3034
|
+
current[key] = {};
|
|
3035
|
+
}
|
|
3036
|
+
current = current[key];
|
|
3037
|
+
}
|
|
3038
|
+
current[keys[keys.length - 1]] = value;
|
|
3039
|
+
}
|
|
3040
|
+
function getByPath(obj, keyPath) {
|
|
3041
|
+
const keys = keyPath.split(".").filter(Boolean);
|
|
3042
|
+
let current = obj;
|
|
3043
|
+
for (const key of keys) {
|
|
3044
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
3045
|
+
return;
|
|
3046
|
+
current = current[key];
|
|
3047
|
+
}
|
|
3048
|
+
return current;
|
|
3049
|
+
}
|
|
3050
|
+
var ConfigTool = {
|
|
3051
|
+
name: "Config",
|
|
3052
|
+
description: "Read or update .claude settings values.",
|
|
3053
|
+
inputSchema: {
|
|
3054
|
+
type: "object",
|
|
3055
|
+
properties: {
|
|
3056
|
+
action: {
|
|
3057
|
+
type: "string",
|
|
3058
|
+
enum: ["get", "set", "list"]
|
|
3059
|
+
},
|
|
3060
|
+
key: {
|
|
3061
|
+
type: "string",
|
|
3062
|
+
description: "Dot-path key (for get/set)."
|
|
3063
|
+
},
|
|
3064
|
+
value: {
|
|
3065
|
+
description: "Value for set action."
|
|
3066
|
+
},
|
|
3067
|
+
scope: {
|
|
3068
|
+
type: "string",
|
|
3069
|
+
enum: ["local", "project"]
|
|
3070
|
+
}
|
|
3071
|
+
},
|
|
3072
|
+
required: ["action"]
|
|
3073
|
+
},
|
|
3074
|
+
async execute(input, ctx) {
|
|
3075
|
+
const {
|
|
3076
|
+
action,
|
|
3077
|
+
key,
|
|
3078
|
+
value,
|
|
3079
|
+
scope = "local"
|
|
3080
|
+
} = input ?? {};
|
|
3081
|
+
if (!action) {
|
|
3082
|
+
return { content: "Error: action is required", isError: true };
|
|
3083
|
+
}
|
|
3084
|
+
const filePath = scopePath(ctx.cwd, scope);
|
|
3085
|
+
let data = {};
|
|
2901
3086
|
try {
|
|
2902
|
-
const
|
|
2903
|
-
|
|
3087
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
3088
|
+
data = JSON.parse(raw);
|
|
2904
3089
|
} catch {
|
|
2905
|
-
|
|
3090
|
+
data = {};
|
|
3091
|
+
}
|
|
3092
|
+
if (action === "list") {
|
|
3093
|
+
return { content: JSON.stringify(data, null, 2) };
|
|
3094
|
+
}
|
|
3095
|
+
if (!key) {
|
|
3096
|
+
return { content: "Error: key is required for get/set", isError: true };
|
|
3097
|
+
}
|
|
3098
|
+
if (action === "get") {
|
|
3099
|
+
const out = getByPath(data, key);
|
|
3100
|
+
return { content: out === undefined ? "undefined" : JSON.stringify(out, null, 2) };
|
|
3101
|
+
}
|
|
3102
|
+
setByPath(data, key, value);
|
|
3103
|
+
try {
|
|
3104
|
+
await mkdir3(dirname4(filePath), { recursive: true });
|
|
3105
|
+
await writeFile3(filePath, JSON.stringify(data, null, 2) + `
|
|
3106
|
+
`, "utf-8");
|
|
3107
|
+
return { content: `Updated ${key} in ${filePath}` };
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3110
|
+
return { content: `Error writing config: ${message}`, isError: true };
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
|
|
3115
|
+
// src/tools/exit-plan-mode.ts
|
|
3116
|
+
var ExitPlanModeTool = {
|
|
3117
|
+
name: "ExitPlanMode",
|
|
3118
|
+
description: "Exit plan mode and resume normal execution permissions.",
|
|
3119
|
+
inputSchema: {
|
|
3120
|
+
type: "object",
|
|
3121
|
+
properties: {}
|
|
3122
|
+
},
|
|
3123
|
+
async execute() {
|
|
3124
|
+
return {
|
|
3125
|
+
content: "Exiting plan mode.",
|
|
3126
|
+
metadata: {
|
|
3127
|
+
setPermissionMode: "default"
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
// src/tools/index.ts
|
|
3133
|
+
var ALL_TOOLS = {
|
|
3134
|
+
Bash: BashTool,
|
|
3135
|
+
Read: ReadTool,
|
|
3136
|
+
Write: WriteTool,
|
|
3137
|
+
Edit: EditTool,
|
|
3138
|
+
Glob: GlobTool,
|
|
3139
|
+
Grep: GrepTool,
|
|
3140
|
+
NotebookEdit: NotebookEditTool,
|
|
3141
|
+
WebFetch: WebFetchTool,
|
|
3142
|
+
WebSearch: WebSearchTool,
|
|
3143
|
+
AskUserQuestion: AskUserQuestionTool,
|
|
3144
|
+
TodoWrite: TodoWriteTool,
|
|
3145
|
+
Config: ConfigTool,
|
|
3146
|
+
ExitPlanMode: ExitPlanModeTool
|
|
3147
|
+
};
|
|
3148
|
+
function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
|
|
3149
|
+
const registry = new ToolRegistry;
|
|
3150
|
+
for (const name of toolNames) {
|
|
3151
|
+
if (disallowedTools?.includes(name))
|
|
3152
|
+
continue;
|
|
3153
|
+
const tool = ALL_TOOLS[name];
|
|
3154
|
+
if (tool) {
|
|
3155
|
+
registry.register(tool);
|
|
2906
3156
|
}
|
|
2907
3157
|
}
|
|
2908
|
-
|
|
2909
|
-
const match = s.match(/^([^(]+)\((.+)\)$/);
|
|
2910
|
-
if (match)
|
|
2911
|
-
return { toolName: match[1], ruleContent: match[2] };
|
|
2912
|
-
return { toolName: s };
|
|
2913
|
-
}
|
|
2914
|
-
serializeRule(rule) {
|
|
2915
|
-
return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
|
|
2916
|
-
}
|
|
3158
|
+
return registry;
|
|
2917
3159
|
}
|
|
2918
3160
|
|
|
2919
3161
|
// src/skills/frontmatter.ts
|
|
@@ -2953,9 +3195,10 @@ function stripFrontmatter(content) {
|
|
|
2953
3195
|
// src/skills/skills.ts
|
|
2954
3196
|
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, realpathSync, statSync } from "fs";
|
|
2955
3197
|
import { homedir as homedir4 } from "os";
|
|
2956
|
-
import { basename, dirname as
|
|
3198
|
+
import { basename, dirname as dirname5, isAbsolute, join as join6, resolve } from "path";
|
|
2957
3199
|
var MAX_NAME_LENGTH = 64;
|
|
2958
3200
|
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
3201
|
+
var MAX_COMPATIBILITY_LENGTH = 500;
|
|
2959
3202
|
var CONFIG_DIR_NAME = ".claude";
|
|
2960
3203
|
function shouldIgnore(name) {
|
|
2961
3204
|
return name.startsWith(".") || name === "node_modules";
|
|
@@ -2988,6 +3231,14 @@ function validateDescription(description) {
|
|
|
2988
3231
|
}
|
|
2989
3232
|
return errors;
|
|
2990
3233
|
}
|
|
3234
|
+
function validateCompatibility(compatibility) {
|
|
3235
|
+
if (!compatibility)
|
|
3236
|
+
return [];
|
|
3237
|
+
if (compatibility.length > MAX_COMPATIBILITY_LENGTH) {
|
|
3238
|
+
return [`compatibility exceeds ${MAX_COMPATIBILITY_LENGTH} characters (${compatibility.length})`];
|
|
3239
|
+
}
|
|
3240
|
+
return [];
|
|
3241
|
+
}
|
|
2991
3242
|
function loadSkillsFromDir(options) {
|
|
2992
3243
|
return loadSkillsFromDirInternal(options.dir, options.source, true);
|
|
2993
3244
|
}
|
|
@@ -3003,7 +3254,7 @@ function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
|
|
|
3003
3254
|
if (shouldIgnore(entry.name)) {
|
|
3004
3255
|
continue;
|
|
3005
3256
|
}
|
|
3006
|
-
const fullPath =
|
|
3257
|
+
const fullPath = join6(dir, entry.name);
|
|
3007
3258
|
let isDirectory = entry.isDirectory();
|
|
3008
3259
|
let isFile = entry.isFile();
|
|
3009
3260
|
if (entry.isSymbolicLink()) {
|
|
@@ -3041,7 +3292,7 @@ function loadSkillFromFile(filePath, source) {
|
|
|
3041
3292
|
try {
|
|
3042
3293
|
const rawContent = readFileSync4(filePath, "utf-8");
|
|
3043
3294
|
const { frontmatter } = parseFrontmatter(rawContent);
|
|
3044
|
-
const skillDir =
|
|
3295
|
+
const skillDir = dirname5(filePath);
|
|
3045
3296
|
const parentDirName = basename(skillDir);
|
|
3046
3297
|
const descErrors = validateDescription(frontmatter.description);
|
|
3047
3298
|
for (const error of descErrors) {
|
|
@@ -3052,9 +3303,15 @@ function loadSkillFromFile(filePath, source) {
|
|
|
3052
3303
|
for (const error of nameErrors) {
|
|
3053
3304
|
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3054
3305
|
}
|
|
3306
|
+
const compatErrors = validateCompatibility(frontmatter.compatibility);
|
|
3307
|
+
for (const error of compatErrors) {
|
|
3308
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3309
|
+
}
|
|
3055
3310
|
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
|
3056
3311
|
return { skill: null, diagnostics };
|
|
3057
3312
|
}
|
|
3313
|
+
const allowedToolsRaw = frontmatter["allowed-tools"];
|
|
3314
|
+
const allowedTools = allowedToolsRaw ? allowedToolsRaw.split(/\s+/).filter(Boolean) : undefined;
|
|
3058
3315
|
return {
|
|
3059
3316
|
skill: {
|
|
3060
3317
|
name,
|
|
@@ -3062,7 +3319,11 @@ function loadSkillFromFile(filePath, source) {
|
|
|
3062
3319
|
filePath,
|
|
3063
3320
|
baseDir: skillDir,
|
|
3064
3321
|
source,
|
|
3065
|
-
disableModelInvocation: frontmatter["disable-model-invocation"] === true
|
|
3322
|
+
disableModelInvocation: frontmatter["disable-model-invocation"] === true,
|
|
3323
|
+
license: frontmatter.license,
|
|
3324
|
+
compatibility: frontmatter.compatibility,
|
|
3325
|
+
metadata: frontmatter.metadata,
|
|
3326
|
+
allowedTools
|
|
3066
3327
|
},
|
|
3067
3328
|
diagnostics
|
|
3068
3329
|
};
|
|
@@ -3077,9 +3338,9 @@ function normalizePath(input) {
|
|
|
3077
3338
|
if (trimmed === "~")
|
|
3078
3339
|
return homedir4();
|
|
3079
3340
|
if (trimmed.startsWith("~/"))
|
|
3080
|
-
return
|
|
3341
|
+
return join6(homedir4(), trimmed.slice(2));
|
|
3081
3342
|
if (trimmed.startsWith("~"))
|
|
3082
|
-
return
|
|
3343
|
+
return join6(homedir4(), trimmed.slice(1));
|
|
3083
3344
|
return trimmed;
|
|
3084
3345
|
}
|
|
3085
3346
|
function resolveSkillPath(p, cwd) {
|
|
@@ -3123,7 +3384,7 @@ function loadSkills(options = {}) {
|
|
|
3123
3384
|
}
|
|
3124
3385
|
}
|
|
3125
3386
|
if (includeDefaults) {
|
|
3126
|
-
const userSkillsDir =
|
|
3387
|
+
const userSkillsDir = join6(homedir4(), CONFIG_DIR_NAME, "skills");
|
|
3127
3388
|
const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
|
|
3128
3389
|
addSkills(loadSkillsFromDirInternal(userSkillsDir, "user", true));
|
|
3129
3390
|
addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", true));
|
|
@@ -3178,6 +3439,9 @@ function formatSkillsForPrompt(skills) {
|
|
|
3178
3439
|
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
3179
3440
|
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
3180
3441
|
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
3442
|
+
if (skill.allowedTools?.length) {
|
|
3443
|
+
lines.push(` <allowed-tools>${escapeXml(skill.allowedTools.join(" "))}</allowed-tools>`);
|
|
3444
|
+
}
|
|
3181
3445
|
lines.push(" </skill>");
|
|
3182
3446
|
}
|
|
3183
3447
|
lines.push("</available_skills>");
|
|
@@ -3186,7 +3450,7 @@ function formatSkillsForPrompt(skills) {
|
|
|
3186
3450
|
}
|
|
3187
3451
|
// src/utils/system-prompt.ts
|
|
3188
3452
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3189
|
-
import { join as
|
|
3453
|
+
import { join as join7 } from "path";
|
|
3190
3454
|
var CORE_IDENTITY = `You are an AI coding agent. You help users with software engineering tasks by reading, writing, and modifying code. You have access to tools that let you interact with the filesystem and execute commands.
|
|
3191
3455
|
|
|
3192
3456
|
You are highly capable and can help users complete complex tasks that would otherwise be too difficult or time-consuming.`;
|
|
@@ -3250,41 +3514,546 @@ function buildSystemPrompt(context) {
|
|
|
3250
3514
|
sections.push(`# Environment
|
|
3251
3515
|
|
|
3252
3516
|
Working directory: ${context.cwd}`);
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3517
|
+
if (context.additionalDirectories && context.additionalDirectories.length > 0) {
|
|
3518
|
+
sections.push(`Additional allowed directories:
|
|
3519
|
+
${context.additionalDirectories.map((d) => `- ${d}`).join(`
|
|
3520
|
+
`)}`);
|
|
3521
|
+
}
|
|
3522
|
+
if (context.loadProjectInstructions) {
|
|
3523
|
+
const instructions = readProjectInstructions(context.cwd);
|
|
3524
|
+
if (instructions) {
|
|
3525
|
+
sections.push(`# Project Instructions
|
|
3256
3526
|
|
|
3257
3527
|
${instructions}`);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
if (context.skills && context.skills.length > 0) {
|
|
3532
|
+
const skillsPrompt = formatSkillsForPrompt(context.skills);
|
|
3533
|
+
if (skillsPrompt) {
|
|
3534
|
+
sections.push(`# Skills
|
|
3535
|
+
|
|
3536
|
+
${skillsPrompt}`);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
if (context.customPrompt) {
|
|
3540
|
+
sections.push(context.customPrompt);
|
|
3541
|
+
}
|
|
3542
|
+
return sections.join(`
|
|
3543
|
+
|
|
3544
|
+
`);
|
|
3545
|
+
}
|
|
3546
|
+
function readProjectInstructions(cwd) {
|
|
3547
|
+
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
3548
|
+
try {
|
|
3549
|
+
const content = readFileSync5(join7(cwd, name), "utf-8").trim();
|
|
3550
|
+
if (content)
|
|
3551
|
+
return content;
|
|
3552
|
+
} catch {}
|
|
3553
|
+
}
|
|
3554
|
+
return null;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// src/agent-loop.ts
|
|
3558
|
+
function makeModelUsageEntry() {
|
|
3559
|
+
return {
|
|
3560
|
+
inputTokens: 0,
|
|
3561
|
+
outputTokens: 0,
|
|
3562
|
+
cacheReadInputTokens: 0,
|
|
3563
|
+
cacheCreationInputTokens: 0,
|
|
3564
|
+
totalCostUsd: 0
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
function makeErrorResult(params) {
|
|
3568
|
+
return {
|
|
3569
|
+
type: "result",
|
|
3570
|
+
subtype: params.subtype,
|
|
3571
|
+
duration_ms: Date.now() - params.startTime,
|
|
3572
|
+
duration_api_ms: params.apiTimeMs,
|
|
3573
|
+
is_error: true,
|
|
3574
|
+
num_turns: params.turns,
|
|
3575
|
+
stop_reason: null,
|
|
3576
|
+
total_cost_usd: params.costUsd,
|
|
3577
|
+
usage: params.usage,
|
|
3578
|
+
modelUsage: params.modelUsage,
|
|
3579
|
+
permission_denials: params.permissionDenials,
|
|
3580
|
+
errors: params.errors,
|
|
3581
|
+
uuid: uuid(),
|
|
3582
|
+
session_id: params.sessionId
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
function extractStructuredJson(text) {
|
|
3586
|
+
const trimmed = text.trim();
|
|
3587
|
+
if (!trimmed) {
|
|
3588
|
+
return { ok: false, error: "Empty result text; expected JSON output." };
|
|
3589
|
+
}
|
|
3590
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
3591
|
+
const candidate = fenced ? fenced[1].trim() : trimmed;
|
|
3592
|
+
try {
|
|
3593
|
+
return { ok: true, value: JSON.parse(candidate) };
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3596
|
+
return { ok: false, error: `Invalid JSON output: ${message}` };
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
async function* agentLoop(prompt, options) {
|
|
3600
|
+
const {
|
|
3601
|
+
provider,
|
|
3602
|
+
model,
|
|
3603
|
+
fallbackModel,
|
|
3604
|
+
modelState,
|
|
3605
|
+
maxThinkingTokensState,
|
|
3606
|
+
thinking,
|
|
3607
|
+
effort,
|
|
3608
|
+
outputFormat,
|
|
3609
|
+
systemPrompt,
|
|
3610
|
+
tools,
|
|
3611
|
+
permissions,
|
|
3612
|
+
cwd,
|
|
3613
|
+
sessionId,
|
|
3614
|
+
maxTurns,
|
|
3615
|
+
maxBudgetUsd,
|
|
3616
|
+
includePartialMessages,
|
|
3617
|
+
signal,
|
|
3618
|
+
env,
|
|
3619
|
+
debug,
|
|
3620
|
+
hooks,
|
|
3621
|
+
mcpClient,
|
|
3622
|
+
previousMessages,
|
|
3623
|
+
sessionLogger,
|
|
3624
|
+
nativeMemoryTool,
|
|
3625
|
+
initMeta
|
|
3626
|
+
} = options;
|
|
3627
|
+
const effectiveModelState = modelState ?? { current: model };
|
|
3628
|
+
const startTime = Date.now();
|
|
3629
|
+
let apiTimeMs = 0;
|
|
3630
|
+
let turns = 0;
|
|
3631
|
+
let totalUsage = emptyTokenUsage();
|
|
3632
|
+
let costUsd = 0;
|
|
3633
|
+
const modelUsage = {};
|
|
3634
|
+
const permissionDenials = [];
|
|
3635
|
+
if (mcpClient) {
|
|
3636
|
+
await mcpClient.connectAll();
|
|
3637
|
+
for (const tool of mcpClient.getTools()) {
|
|
3638
|
+
tools.register(tool);
|
|
3639
|
+
}
|
|
3640
|
+
const { createListMcpResourcesTool: createListMcpResourcesTool2, createReadMcpResourceTool: createReadMcpResourceTool2 } = await Promise.resolve().then(() => exports_mcp_resources);
|
|
3641
|
+
tools.register(createListMcpResourcesTool2(mcpClient));
|
|
3642
|
+
tools.register(createReadMcpResourceTool2(mcpClient));
|
|
3643
|
+
}
|
|
3644
|
+
const messages = [
|
|
3645
|
+
...previousMessages ?? [],
|
|
3646
|
+
{ role: "user", content: prompt }
|
|
3647
|
+
];
|
|
3648
|
+
if (sessionLogger) {
|
|
3649
|
+
sessionLogger("user", prompt, null);
|
|
3650
|
+
}
|
|
3651
|
+
yield {
|
|
3652
|
+
type: "system",
|
|
3653
|
+
subtype: "init",
|
|
3654
|
+
apiKeySource: "user",
|
|
3655
|
+
claude_code_version: "fourmis-agent-sdk",
|
|
3656
|
+
session_id: sessionId,
|
|
3657
|
+
model: effectiveModelState.current,
|
|
3658
|
+
tools: tools.list(),
|
|
3659
|
+
cwd,
|
|
3660
|
+
mcp_servers: (mcpClient?.status() ?? []).map((s) => ({ name: s.name, status: s.status })),
|
|
3661
|
+
permissionMode: permissions.getMode(),
|
|
3662
|
+
agents: initMeta?.agents,
|
|
3663
|
+
betas: initMeta?.betas,
|
|
3664
|
+
slash_commands: initMeta?.slashCommands ?? [],
|
|
3665
|
+
output_style: initMeta?.outputStyle ?? "default",
|
|
3666
|
+
skills: initMeta?.skills ?? [],
|
|
3667
|
+
plugins: (initMeta?.plugins ?? []).map((p) => ({ name: p.path.split("/").pop() ?? p.path, path: p.path })),
|
|
3668
|
+
uuid: uuid()
|
|
3669
|
+
};
|
|
3670
|
+
if (hooks) {
|
|
3671
|
+
await hooks.fire("Setup", {
|
|
3672
|
+
event: "Setup",
|
|
3673
|
+
hook_event_name: "Setup",
|
|
3674
|
+
trigger: "init",
|
|
3675
|
+
session_id: sessionId,
|
|
3676
|
+
cwd,
|
|
3677
|
+
permission_mode: permissions.getMode()
|
|
3678
|
+
}, undefined, { signal });
|
|
3679
|
+
}
|
|
3680
|
+
if (hooks) {
|
|
3681
|
+
await hooks.fire("SessionStart", {
|
|
3682
|
+
event: "SessionStart",
|
|
3683
|
+
hook_event_name: "SessionStart",
|
|
3684
|
+
session_id: sessionId,
|
|
3685
|
+
source: "startup",
|
|
3686
|
+
model: effectiveModelState.current,
|
|
3687
|
+
cwd,
|
|
3688
|
+
permission_mode: permissions.getMode()
|
|
3689
|
+
}, undefined, { signal });
|
|
3690
|
+
}
|
|
3691
|
+
while (true) {
|
|
3692
|
+
if (signal.aborted) {
|
|
3693
|
+
yield makeErrorResult({
|
|
3694
|
+
subtype: "error_during_execution",
|
|
3695
|
+
errors: ["Aborted"],
|
|
3696
|
+
turns,
|
|
3697
|
+
costUsd,
|
|
3698
|
+
sessionId,
|
|
3699
|
+
startTime,
|
|
3700
|
+
apiTimeMs,
|
|
3701
|
+
usage: totalUsage,
|
|
3702
|
+
modelUsage,
|
|
3703
|
+
permissionDenials
|
|
3704
|
+
});
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
if (turns >= maxTurns) {
|
|
3708
|
+
yield makeErrorResult({
|
|
3709
|
+
subtype: "error_max_turns",
|
|
3710
|
+
errors: [`Reached maximum turns (${maxTurns})`],
|
|
3711
|
+
turns,
|
|
3712
|
+
costUsd,
|
|
3713
|
+
sessionId,
|
|
3714
|
+
startTime,
|
|
3715
|
+
apiTimeMs,
|
|
3716
|
+
usage: totalUsage,
|
|
3717
|
+
modelUsage,
|
|
3718
|
+
permissionDenials
|
|
3719
|
+
});
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
|
|
3723
|
+
yield makeErrorResult({
|
|
3724
|
+
subtype: "error_max_budget_usd",
|
|
3725
|
+
errors: [],
|
|
3726
|
+
turns,
|
|
3727
|
+
costUsd,
|
|
3728
|
+
sessionId,
|
|
3729
|
+
startTime,
|
|
3730
|
+
apiTimeMs,
|
|
3731
|
+
usage: totalUsage,
|
|
3732
|
+
modelUsage,
|
|
3733
|
+
permissionDenials
|
|
3734
|
+
});
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
const activeModel = effectiveModelState.current;
|
|
3738
|
+
const toolDefs = tools.getDefinitions();
|
|
3739
|
+
const apiStart = Date.now();
|
|
3740
|
+
let assistantTextParts = [];
|
|
3741
|
+
const toolCalls = [];
|
|
3742
|
+
let turnUsage = emptyTokenUsage();
|
|
3743
|
+
let turnStopReason = null;
|
|
3744
|
+
const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
|
|
3745
|
+
try {
|
|
3746
|
+
const chunks = provider.chat({
|
|
3747
|
+
model: activeModel,
|
|
3748
|
+
messages,
|
|
3749
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
3750
|
+
systemPrompt,
|
|
3751
|
+
signal,
|
|
3752
|
+
nativeTools,
|
|
3753
|
+
thinkingBudget: maxThinkingTokensState?.current,
|
|
3754
|
+
thinking,
|
|
3755
|
+
effort,
|
|
3756
|
+
outputFormat
|
|
3757
|
+
});
|
|
3758
|
+
for await (const chunk of chunks) {
|
|
3759
|
+
switch (chunk.type) {
|
|
3760
|
+
case "text_delta":
|
|
3761
|
+
assistantTextParts.push(chunk.text);
|
|
3762
|
+
if (includePartialMessages) {
|
|
3763
|
+
yield {
|
|
3764
|
+
type: "stream_event",
|
|
3765
|
+
event: { type: "text_delta", text: chunk.text },
|
|
3766
|
+
parent_tool_use_id: null,
|
|
3767
|
+
uuid: uuid(),
|
|
3768
|
+
session_id: sessionId
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
break;
|
|
3772
|
+
case "thinking_delta":
|
|
3773
|
+
if (includePartialMessages) {
|
|
3774
|
+
yield {
|
|
3775
|
+
type: "stream_event",
|
|
3776
|
+
event: { type: "thinking_delta", thinking: chunk.text },
|
|
3777
|
+
parent_tool_use_id: null,
|
|
3778
|
+
uuid: uuid(),
|
|
3779
|
+
session_id: sessionId
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
break;
|
|
3783
|
+
case "tool_call":
|
|
3784
|
+
toolCalls.push({ id: chunk.id, name: chunk.name, input: chunk.input });
|
|
3785
|
+
break;
|
|
3786
|
+
case "usage":
|
|
3787
|
+
turnUsage = mergeUsage(turnUsage, chunk.usage);
|
|
3788
|
+
break;
|
|
3789
|
+
case "done":
|
|
3790
|
+
turnStopReason = chunk.stopReason ?? null;
|
|
3791
|
+
break;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
} catch (err) {
|
|
3795
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3796
|
+
if (fallbackModel && activeModel !== fallbackModel) {
|
|
3797
|
+
effectiveModelState.current = fallbackModel;
|
|
3798
|
+
yield {
|
|
3799
|
+
type: "system",
|
|
3800
|
+
subtype: "status",
|
|
3801
|
+
status: null,
|
|
3802
|
+
permissionMode: permissions.getMode(),
|
|
3803
|
+
uuid: uuid(),
|
|
3804
|
+
session_id: sessionId
|
|
3805
|
+
};
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
yield makeErrorResult({
|
|
3809
|
+
subtype: "error_during_execution",
|
|
3810
|
+
errors: [`API error: ${message}`],
|
|
3811
|
+
turns,
|
|
3812
|
+
costUsd,
|
|
3813
|
+
sessionId,
|
|
3814
|
+
startTime,
|
|
3815
|
+
apiTimeMs,
|
|
3816
|
+
usage: totalUsage,
|
|
3817
|
+
modelUsage,
|
|
3818
|
+
permissionDenials
|
|
3819
|
+
});
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
apiTimeMs += Date.now() - apiStart;
|
|
3823
|
+
turns++;
|
|
3824
|
+
totalUsage = mergeUsage(totalUsage, turnUsage);
|
|
3825
|
+
const turnCost = provider.calculateCost(activeModel, turnUsage);
|
|
3826
|
+
costUsd += turnCost;
|
|
3827
|
+
if (!modelUsage[activeModel]) {
|
|
3828
|
+
modelUsage[activeModel] = makeModelUsageEntry();
|
|
3829
|
+
}
|
|
3830
|
+
modelUsage[activeModel].inputTokens += turnUsage.inputTokens;
|
|
3831
|
+
modelUsage[activeModel].outputTokens += turnUsage.outputTokens;
|
|
3832
|
+
modelUsage[activeModel].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
|
|
3833
|
+
modelUsage[activeModel].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
|
|
3834
|
+
modelUsage[activeModel].totalCostUsd += turnCost;
|
|
3835
|
+
modelUsage[activeModel].webSearchRequests = (modelUsage[activeModel].webSearchRequests ?? 0) + (turnUsage.webSearchRequests ?? 0);
|
|
3836
|
+
modelUsage[activeModel].costUSD = modelUsage[activeModel].totalCostUsd;
|
|
3837
|
+
modelUsage[activeModel].contextWindow = provider.getContextWindow(activeModel);
|
|
3838
|
+
const assistantText = assistantTextParts.join("");
|
|
3839
|
+
const assistantContent = [];
|
|
3840
|
+
if (assistantText) {
|
|
3841
|
+
assistantContent.push({ type: "text", text: assistantText });
|
|
3842
|
+
}
|
|
3843
|
+
for (const call of toolCalls) {
|
|
3844
|
+
assistantContent.push({
|
|
3845
|
+
type: "tool_use",
|
|
3846
|
+
id: call.id,
|
|
3847
|
+
name: call.name,
|
|
3848
|
+
input: call.input
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
3852
|
+
if (sessionLogger) {
|
|
3853
|
+
sessionLogger("assistant", assistantContent, null);
|
|
3854
|
+
}
|
|
3855
|
+
yield {
|
|
3856
|
+
type: "assistant",
|
|
3857
|
+
message: {
|
|
3858
|
+
role: "assistant",
|
|
3859
|
+
content: assistantContent
|
|
3860
|
+
},
|
|
3861
|
+
parent_tool_use_id: null,
|
|
3862
|
+
uuid: uuid(),
|
|
3863
|
+
session_id: sessionId
|
|
3864
|
+
};
|
|
3865
|
+
if (toolCalls.length === 0) {
|
|
3866
|
+
let structuredOutput;
|
|
3867
|
+
if (outputFormat?.type === "json_schema") {
|
|
3868
|
+
const parsed = extractStructuredJson(assistantText);
|
|
3869
|
+
if (!parsed.ok) {
|
|
3870
|
+
yield makeErrorResult({
|
|
3871
|
+
subtype: "error_max_structured_output_retries",
|
|
3872
|
+
errors: [parsed.error],
|
|
3873
|
+
turns,
|
|
3874
|
+
costUsd,
|
|
3875
|
+
sessionId,
|
|
3876
|
+
startTime,
|
|
3877
|
+
apiTimeMs,
|
|
3878
|
+
usage: totalUsage,
|
|
3879
|
+
modelUsage,
|
|
3880
|
+
permissionDenials
|
|
3881
|
+
});
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
structuredOutput = parsed.value;
|
|
3885
|
+
}
|
|
3886
|
+
if (hooks) {
|
|
3887
|
+
await hooks.fire("Stop", {
|
|
3888
|
+
event: "Stop",
|
|
3889
|
+
hook_event_name: "Stop",
|
|
3890
|
+
session_id: sessionId,
|
|
3891
|
+
text: assistantText || undefined,
|
|
3892
|
+
stop_reason: turnStopReason ?? undefined
|
|
3893
|
+
}, undefined, {
|
|
3894
|
+
signal
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
if (hooks) {
|
|
3898
|
+
await hooks.fire("SessionEnd", {
|
|
3899
|
+
event: "SessionEnd",
|
|
3900
|
+
hook_event_name: "SessionEnd",
|
|
3901
|
+
session_id: sessionId,
|
|
3902
|
+
reason: "other"
|
|
3903
|
+
}, undefined, { signal });
|
|
3904
|
+
}
|
|
3905
|
+
yield {
|
|
3906
|
+
type: "result",
|
|
3907
|
+
subtype: "success",
|
|
3908
|
+
duration_ms: Date.now() - startTime,
|
|
3909
|
+
duration_api_ms: apiTimeMs,
|
|
3910
|
+
is_error: false,
|
|
3911
|
+
num_turns: turns,
|
|
3912
|
+
result: assistantText,
|
|
3913
|
+
stop_reason: turnStopReason,
|
|
3914
|
+
total_cost_usd: costUsd,
|
|
3915
|
+
usage: totalUsage,
|
|
3916
|
+
modelUsage,
|
|
3917
|
+
permission_denials: permissionDenials,
|
|
3918
|
+
structured_output: structuredOutput,
|
|
3919
|
+
uuid: uuid(),
|
|
3920
|
+
session_id: sessionId
|
|
3921
|
+
};
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
const toolResults = [];
|
|
3925
|
+
for (const call of toolCalls) {
|
|
3926
|
+
let hookDenied = false;
|
|
3927
|
+
let hookUpdatedInput;
|
|
3928
|
+
if (hooks) {
|
|
3929
|
+
const hookResult = await hooks.fire("PreToolUse", {
|
|
3930
|
+
event: "PreToolUse",
|
|
3931
|
+
hook_event_name: "PreToolUse",
|
|
3932
|
+
tool_name: call.name,
|
|
3933
|
+
tool_input: call.input,
|
|
3934
|
+
session_id: sessionId
|
|
3935
|
+
}, call.id, { signal });
|
|
3936
|
+
if (hookResult) {
|
|
3937
|
+
if (hookResult.permissionDecision === "deny") {
|
|
3938
|
+
hookDenied = true;
|
|
3939
|
+
}
|
|
3940
|
+
if (hookResult.updatedInput !== undefined) {
|
|
3941
|
+
hookUpdatedInput = hookResult.updatedInput;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
if (hookDenied) {
|
|
3946
|
+
const denyContent = "Denied by hook";
|
|
3947
|
+
toolResults.push({
|
|
3948
|
+
type: "tool_result",
|
|
3949
|
+
tool_use_id: call.id,
|
|
3950
|
+
content: denyContent,
|
|
3951
|
+
is_error: true
|
|
3952
|
+
});
|
|
3953
|
+
if (hooks) {
|
|
3954
|
+
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
3955
|
+
}
|
|
3956
|
+
continue;
|
|
3957
|
+
}
|
|
3958
|
+
const inputAfterHook = hookUpdatedInput !== undefined ? hookUpdatedInput : call.input;
|
|
3959
|
+
const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
|
|
3960
|
+
if (permResult.behavior === "deny") {
|
|
3961
|
+
const denyContent = `Permission denied: ${permResult.message}`;
|
|
3962
|
+
permissionDenials.push({
|
|
3963
|
+
tool_name: call.name,
|
|
3964
|
+
tool_use_id: call.id,
|
|
3965
|
+
tool_input: inputAfterHook ?? {}
|
|
3966
|
+
});
|
|
3967
|
+
toolResults.push({
|
|
3968
|
+
type: "tool_result",
|
|
3969
|
+
tool_use_id: call.id,
|
|
3970
|
+
content: denyContent,
|
|
3971
|
+
is_error: true
|
|
3972
|
+
});
|
|
3973
|
+
if (hooks) {
|
|
3974
|
+
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
3975
|
+
}
|
|
3976
|
+
continue;
|
|
3977
|
+
}
|
|
3978
|
+
const toolInput = permResult.behavior === "allow" && permResult.updatedInput ? permResult.updatedInput : inputAfterHook;
|
|
3979
|
+
let result;
|
|
3980
|
+
if (call.name === "memory" && nativeMemoryTool) {
|
|
3981
|
+
try {
|
|
3982
|
+
const content = await nativeMemoryTool.execute(toolInput);
|
|
3983
|
+
result = { content, isError: content.startsWith("Error:") };
|
|
3984
|
+
} catch (err) {
|
|
3985
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3986
|
+
result = { content: `Error: ${message}`, isError: true };
|
|
3987
|
+
}
|
|
3988
|
+
} else {
|
|
3989
|
+
const toolCtx = {
|
|
3990
|
+
cwd,
|
|
3991
|
+
signal,
|
|
3992
|
+
sessionId,
|
|
3993
|
+
env
|
|
3994
|
+
};
|
|
3995
|
+
result = await tools.execute(call.name, toolInput, toolCtx);
|
|
3996
|
+
}
|
|
3997
|
+
if (call.name === "ExitPlanMode") {
|
|
3998
|
+
permissions.setMode("default");
|
|
3999
|
+
}
|
|
4000
|
+
if (debug) {
|
|
4001
|
+
console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
|
|
4002
|
+
}
|
|
4003
|
+
if (hooks) {
|
|
4004
|
+
if (result.isError) {
|
|
4005
|
+
await hooks.fire("PostToolUseFailure", {
|
|
4006
|
+
event: "PostToolUseFailure",
|
|
4007
|
+
hook_event_name: "PostToolUseFailure",
|
|
4008
|
+
tool_name: call.name,
|
|
4009
|
+
tool_result: result.content,
|
|
4010
|
+
tool_error: true,
|
|
4011
|
+
session_id: sessionId
|
|
4012
|
+
}, call.id, { signal });
|
|
4013
|
+
} else {
|
|
4014
|
+
const postResult = await hooks.fire("PostToolUse", {
|
|
4015
|
+
event: "PostToolUse",
|
|
4016
|
+
hook_event_name: "PostToolUse",
|
|
4017
|
+
tool_name: call.name,
|
|
4018
|
+
tool_result: result.content,
|
|
4019
|
+
session_id: sessionId
|
|
4020
|
+
}, call.id, { signal });
|
|
4021
|
+
if (postResult?.additionalContext) {
|
|
4022
|
+
result.content += `
|
|
4023
|
+
${postResult.additionalContext}`;
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
toolResults.push({
|
|
4028
|
+
type: "tool_result",
|
|
4029
|
+
tool_use_id: call.id,
|
|
4030
|
+
content: result.content,
|
|
4031
|
+
is_error: result.isError
|
|
4032
|
+
});
|
|
3258
4033
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
if (skillsPrompt) {
|
|
3263
|
-
sections.push(`# Skills
|
|
3264
|
-
|
|
3265
|
-
${skillsPrompt}`);
|
|
4034
|
+
messages.push({ role: "user", content: toolResults });
|
|
4035
|
+
if (sessionLogger) {
|
|
4036
|
+
sessionLogger("user", toolResults, null);
|
|
3266
4037
|
}
|
|
4038
|
+
yield {
|
|
4039
|
+
type: "user",
|
|
4040
|
+
message: {
|
|
4041
|
+
role: "user",
|
|
4042
|
+
content: toolResults
|
|
4043
|
+
},
|
|
4044
|
+
parent_tool_use_id: null,
|
|
4045
|
+
isSynthetic: true,
|
|
4046
|
+
uuid: uuid(),
|
|
4047
|
+
session_id: sessionId
|
|
4048
|
+
};
|
|
3267
4049
|
}
|
|
3268
|
-
if (context.customPrompt) {
|
|
3269
|
-
sections.push(context.customPrompt);
|
|
3270
|
-
}
|
|
3271
|
-
return sections.join(`
|
|
3272
|
-
|
|
3273
|
-
`);
|
|
3274
|
-
}
|
|
3275
|
-
function readProjectInstructions(cwd) {
|
|
3276
|
-
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
3277
|
-
try {
|
|
3278
|
-
const content = readFileSync5(join5(cwd, name), "utf-8").trim();
|
|
3279
|
-
if (content)
|
|
3280
|
-
return content;
|
|
3281
|
-
} catch {}
|
|
3282
|
-
}
|
|
3283
|
-
return null;
|
|
3284
4050
|
}
|
|
3285
4051
|
|
|
3286
4052
|
// src/query.ts
|
|
3287
|
-
function
|
|
4053
|
+
function unsupported(methodName) {
|
|
4054
|
+
return Promise.reject(new Error(`Query.${methodName} is not supported in this runtime yet.`));
|
|
4055
|
+
}
|
|
4056
|
+
function createQuery(generator, abortController, controls) {
|
|
3288
4057
|
const query = {
|
|
3289
4058
|
next: generator.next.bind(generator),
|
|
3290
4059
|
return: generator.return.bind(generator),
|
|
@@ -3295,6 +4064,71 @@ function createQuery(generator, abortController) {
|
|
|
3295
4064
|
async interrupt() {
|
|
3296
4065
|
abortController.abort();
|
|
3297
4066
|
},
|
|
4067
|
+
async setPermissionMode(mode) {
|
|
4068
|
+
if (!controls?.setPermissionMode)
|
|
4069
|
+
return unsupported("setPermissionMode");
|
|
4070
|
+
await controls.setPermissionMode(mode);
|
|
4071
|
+
},
|
|
4072
|
+
async setModel(model) {
|
|
4073
|
+
if (!controls?.setModel)
|
|
4074
|
+
return unsupported("setModel");
|
|
4075
|
+
await controls.setModel(model);
|
|
4076
|
+
},
|
|
4077
|
+
async setMaxThinkingTokens(maxThinkingTokens) {
|
|
4078
|
+
if (!controls?.setMaxThinkingTokens)
|
|
4079
|
+
return unsupported("setMaxThinkingTokens");
|
|
4080
|
+
await controls.setMaxThinkingTokens(maxThinkingTokens);
|
|
4081
|
+
},
|
|
4082
|
+
async initializationResult() {
|
|
4083
|
+
if (!controls?.initializationResult)
|
|
4084
|
+
return unsupported("initializationResult");
|
|
4085
|
+
return controls.initializationResult();
|
|
4086
|
+
},
|
|
4087
|
+
async supportedCommands() {
|
|
4088
|
+
if (!controls?.supportedCommands)
|
|
4089
|
+
return unsupported("supportedCommands");
|
|
4090
|
+
return controls.supportedCommands();
|
|
4091
|
+
},
|
|
4092
|
+
async supportedModels() {
|
|
4093
|
+
if (!controls?.supportedModels)
|
|
4094
|
+
return unsupported("supportedModels");
|
|
4095
|
+
return controls.supportedModels();
|
|
4096
|
+
},
|
|
4097
|
+
async mcpServerStatus() {
|
|
4098
|
+
if (!controls?.mcpServerStatus)
|
|
4099
|
+
return unsupported("mcpServerStatus");
|
|
4100
|
+
return controls.mcpServerStatus();
|
|
4101
|
+
},
|
|
4102
|
+
async accountInfo() {
|
|
4103
|
+
if (!controls?.accountInfo)
|
|
4104
|
+
return unsupported("accountInfo");
|
|
4105
|
+
return controls.accountInfo();
|
|
4106
|
+
},
|
|
4107
|
+
async rewindFiles(userMessageId, options) {
|
|
4108
|
+
if (!controls?.rewindFiles)
|
|
4109
|
+
return unsupported("rewindFiles");
|
|
4110
|
+
return controls.rewindFiles(userMessageId, options);
|
|
4111
|
+
},
|
|
4112
|
+
async reconnectMcpServer(serverName) {
|
|
4113
|
+
if (!controls?.reconnectMcpServer)
|
|
4114
|
+
return unsupported("reconnectMcpServer");
|
|
4115
|
+
await controls.reconnectMcpServer(serverName);
|
|
4116
|
+
},
|
|
4117
|
+
async toggleMcpServer(serverName, enabled) {
|
|
4118
|
+
if (!controls?.toggleMcpServer)
|
|
4119
|
+
return unsupported("toggleMcpServer");
|
|
4120
|
+
await controls.toggleMcpServer(serverName, enabled);
|
|
4121
|
+
},
|
|
4122
|
+
async setMcpServers(servers) {
|
|
4123
|
+
if (!controls?.setMcpServers)
|
|
4124
|
+
return unsupported("setMcpServers");
|
|
4125
|
+
return controls.setMcpServers(servers);
|
|
4126
|
+
},
|
|
4127
|
+
async streamInput(stream) {
|
|
4128
|
+
if (!controls?.streamInput)
|
|
4129
|
+
return unsupported("streamInput");
|
|
4130
|
+
await controls.streamInput(stream);
|
|
4131
|
+
},
|
|
3298
4132
|
close() {
|
|
3299
4133
|
abortController.abort();
|
|
3300
4134
|
generator.return(undefined);
|
|
@@ -3316,7 +4150,10 @@ var HOOK_EVENTS = [
|
|
|
3316
4150
|
"SubagentStart",
|
|
3317
4151
|
"SubagentStop",
|
|
3318
4152
|
"PreCompact",
|
|
3319
|
-
"PermissionRequest"
|
|
4153
|
+
"PermissionRequest",
|
|
4154
|
+
"Setup",
|
|
4155
|
+
"TeammateIdle",
|
|
4156
|
+
"TaskCompleted"
|
|
3320
4157
|
];
|
|
3321
4158
|
var TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
|
|
3322
4159
|
|
|
@@ -3348,9 +4185,31 @@ class HookManager {
|
|
|
3348
4185
|
}
|
|
3349
4186
|
}
|
|
3350
4187
|
for (const callback of entry.hooks) {
|
|
3351
|
-
const
|
|
4188
|
+
const timeoutMs = typeof entry.timeout === "number" && entry.timeout > 0 ? Math.floor(entry.timeout * 1000) : null;
|
|
4189
|
+
const invoke = callback(input, toolUseId, { signal });
|
|
4190
|
+
const result = timeoutMs ? await Promise.race([
|
|
4191
|
+
invoke,
|
|
4192
|
+
new Promise((resolve2) => {
|
|
4193
|
+
setTimeout(() => resolve2({}), timeoutMs);
|
|
4194
|
+
})
|
|
4195
|
+
]) : await invoke;
|
|
3352
4196
|
if (result) {
|
|
3353
4197
|
hasOutput = true;
|
|
4198
|
+
if (result.continue === false && !result.permissionDecision) {
|
|
4199
|
+
merged.permissionDecision = "deny";
|
|
4200
|
+
}
|
|
4201
|
+
if (result.decision) {
|
|
4202
|
+
if (result.decision.behavior === "deny") {
|
|
4203
|
+
merged.permissionDecision = "deny";
|
|
4204
|
+
} else {
|
|
4205
|
+
if (!merged.permissionDecision) {
|
|
4206
|
+
merged.permissionDecision = "allow";
|
|
4207
|
+
}
|
|
4208
|
+
if (result.decision.updatedInput !== undefined) {
|
|
4209
|
+
merged.updatedInput = result.decision.updatedInput;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
3354
4213
|
if (result.permissionDecision) {
|
|
3355
4214
|
if (!merged.permissionDecision || result.permissionDecision === "deny") {
|
|
3356
4215
|
merged.permissionDecision = result.permissionDecision;
|
|
@@ -3380,14 +4239,29 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
|
3380
4239
|
class McpClientManager {
|
|
3381
4240
|
configs;
|
|
3382
4241
|
servers = new Map;
|
|
4242
|
+
disabled = new Set;
|
|
3383
4243
|
constructor(configs) {
|
|
3384
|
-
this.configs = configs;
|
|
4244
|
+
this.configs = { ...configs };
|
|
3385
4245
|
}
|
|
3386
4246
|
async connectAll() {
|
|
3387
4247
|
const entries = Object.entries(this.configs);
|
|
3388
4248
|
await Promise.all(entries.map(([name, config]) => this.connectOne(name, config)));
|
|
3389
4249
|
}
|
|
3390
4250
|
async connectOne(name, config) {
|
|
4251
|
+
if (this.disabled.has(name)) {
|
|
4252
|
+
this.servers.set(name, {
|
|
4253
|
+
name,
|
|
4254
|
+
client: null,
|
|
4255
|
+
tools: [],
|
|
4256
|
+
status: {
|
|
4257
|
+
name,
|
|
4258
|
+
status: "disabled",
|
|
4259
|
+
config: this.toStatusConfig(config),
|
|
4260
|
+
scope: config.type === "sdk" ? "sdk" : "session"
|
|
4261
|
+
}
|
|
4262
|
+
});
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
3391
4265
|
try {
|
|
3392
4266
|
const client = new Client({ name: `fourmis-${name}`, version: "1.0.0" });
|
|
3393
4267
|
if (config.type === "sdk") {
|
|
@@ -3421,13 +4295,25 @@ class McpClientManager {
|
|
|
3421
4295
|
const tools = (toolsResult.tools ?? []).map((t) => ({
|
|
3422
4296
|
name: t.name,
|
|
3423
4297
|
description: t.description,
|
|
3424
|
-
inputSchema: t.inputSchema
|
|
4298
|
+
inputSchema: t.inputSchema,
|
|
4299
|
+
annotations: t.annotations ? {
|
|
4300
|
+
readOnly: t.annotations.readOnly,
|
|
4301
|
+
destructive: t.annotations.destructive,
|
|
4302
|
+
openWorld: t.annotations.openWorld
|
|
4303
|
+
} : undefined
|
|
3425
4304
|
}));
|
|
3426
4305
|
this.servers.set(name, {
|
|
3427
4306
|
name,
|
|
3428
4307
|
client,
|
|
3429
4308
|
tools,
|
|
3430
|
-
status: {
|
|
4309
|
+
status: {
|
|
4310
|
+
name,
|
|
4311
|
+
status: "connected",
|
|
4312
|
+
tools,
|
|
4313
|
+
serverInfo: { name, version: "1.0.0" },
|
|
4314
|
+
config: this.toStatusConfig(config),
|
|
4315
|
+
scope: config.type === "sdk" ? "sdk" : "session"
|
|
4316
|
+
}
|
|
3431
4317
|
});
|
|
3432
4318
|
} catch (err) {
|
|
3433
4319
|
const error = err instanceof Error ? err.message : String(err);
|
|
@@ -3435,10 +4321,22 @@ class McpClientManager {
|
|
|
3435
4321
|
name,
|
|
3436
4322
|
client: null,
|
|
3437
4323
|
tools: [],
|
|
3438
|
-
status: {
|
|
4324
|
+
status: {
|
|
4325
|
+
name,
|
|
4326
|
+
status: "failed",
|
|
4327
|
+
error,
|
|
4328
|
+
config: this.toStatusConfig(config),
|
|
4329
|
+
scope: config.type === "sdk" ? "sdk" : "session"
|
|
4330
|
+
}
|
|
3439
4331
|
});
|
|
3440
4332
|
}
|
|
3441
4333
|
}
|
|
4334
|
+
toStatusConfig(config) {
|
|
4335
|
+
if (config.type === "sdk") {
|
|
4336
|
+
return { type: "sdk", name: config.name };
|
|
4337
|
+
}
|
|
4338
|
+
return config;
|
|
4339
|
+
}
|
|
3442
4340
|
getTools() {
|
|
3443
4341
|
const result = [];
|
|
3444
4342
|
for (const [serverName, server] of this.servers) {
|
|
@@ -3478,64 +4376,294 @@ class McpClientManager {
|
|
|
3478
4376
|
return { content: `MCP tool error: ${message}`, isError: true };
|
|
3479
4377
|
}
|
|
3480
4378
|
}
|
|
3481
|
-
async listResources(serverName) {
|
|
3482
|
-
const result = [];
|
|
3483
|
-
const serversToQuery = serverName ? [this.servers.get(serverName)].filter(Boolean) : [...this.servers.values()];
|
|
3484
|
-
for (const server of serversToQuery) {
|
|
3485
|
-
if (server.status.status !== "connected")
|
|
4379
|
+
async listResources(serverName) {
|
|
4380
|
+
const result = [];
|
|
4381
|
+
const serversToQuery = serverName ? [this.servers.get(serverName)].filter(Boolean) : [...this.servers.values()];
|
|
4382
|
+
for (const server of serversToQuery) {
|
|
4383
|
+
if (server.status.status !== "connected")
|
|
4384
|
+
continue;
|
|
4385
|
+
try {
|
|
4386
|
+
const resources = await server.client.listResources();
|
|
4387
|
+
for (const r of resources.resources ?? []) {
|
|
4388
|
+
result.push({
|
|
4389
|
+
uri: r.uri,
|
|
4390
|
+
name: r.name,
|
|
4391
|
+
description: r.description,
|
|
4392
|
+
mimeType: r.mimeType,
|
|
4393
|
+
server: server.name
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
} catch {}
|
|
4397
|
+
}
|
|
4398
|
+
return result;
|
|
4399
|
+
}
|
|
4400
|
+
async readResource(serverName, uri) {
|
|
4401
|
+
const server = this.servers.get(serverName);
|
|
4402
|
+
if (!server || server.status.status !== "connected") {
|
|
4403
|
+
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
4404
|
+
}
|
|
4405
|
+
const result = await server.client.readResource({ uri });
|
|
4406
|
+
const contents = result.contents ?? [];
|
|
4407
|
+
return contents.map((c) => {
|
|
4408
|
+
if ("text" in c)
|
|
4409
|
+
return c.text;
|
|
4410
|
+
if ("blob" in c)
|
|
4411
|
+
return `[binary data: ${c.mimeType ?? "unknown"}]`;
|
|
4412
|
+
return "";
|
|
4413
|
+
}).join("");
|
|
4414
|
+
}
|
|
4415
|
+
status() {
|
|
4416
|
+
const result = [];
|
|
4417
|
+
for (const [name, config] of Object.entries(this.configs)) {
|
|
4418
|
+
const server = this.servers.get(name);
|
|
4419
|
+
if (server) {
|
|
4420
|
+
result.push(server.status);
|
|
4421
|
+
} else if (this.disabled.has(name)) {
|
|
4422
|
+
result.push({
|
|
4423
|
+
name,
|
|
4424
|
+
status: "disabled",
|
|
4425
|
+
config: this.toStatusConfig(config),
|
|
4426
|
+
scope: config.type === "sdk" ? "sdk" : "session"
|
|
4427
|
+
});
|
|
4428
|
+
} else {
|
|
4429
|
+
result.push({
|
|
4430
|
+
name,
|
|
4431
|
+
status: "pending",
|
|
4432
|
+
config: this.toStatusConfig(config),
|
|
4433
|
+
scope: config.type === "sdk" ? "sdk" : "session"
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
return result;
|
|
4438
|
+
}
|
|
4439
|
+
async reconnectServer(serverName) {
|
|
4440
|
+
const config = this.configs[serverName];
|
|
4441
|
+
if (!config) {
|
|
4442
|
+
throw new Error(`MCP server "${serverName}" is not configured`);
|
|
4443
|
+
}
|
|
4444
|
+
await this.closeOne(serverName);
|
|
4445
|
+
await this.connectOne(serverName, config);
|
|
4446
|
+
const status = this.servers.get(serverName)?.status;
|
|
4447
|
+
if (!status || status.status !== "connected") {
|
|
4448
|
+
throw new Error(status?.error ?? `Failed to reconnect MCP server "${serverName}"`);
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
async toggleServer(serverName, enabled) {
|
|
4452
|
+
const config = this.configs[serverName];
|
|
4453
|
+
if (!config) {
|
|
4454
|
+
throw new Error(`MCP server "${serverName}" is not configured`);
|
|
4455
|
+
}
|
|
4456
|
+
if (!enabled) {
|
|
4457
|
+
this.disabled.add(serverName);
|
|
4458
|
+
await this.closeOne(serverName);
|
|
4459
|
+
this.servers.set(serverName, {
|
|
4460
|
+
name: serverName,
|
|
4461
|
+
client: null,
|
|
4462
|
+
tools: [],
|
|
4463
|
+
status: {
|
|
4464
|
+
name: serverName,
|
|
4465
|
+
status: "disabled",
|
|
4466
|
+
config: this.toStatusConfig(config),
|
|
4467
|
+
scope: config.type === "sdk" ? "sdk" : "session"
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
this.disabled.delete(serverName);
|
|
4473
|
+
await this.reconnectServer(serverName);
|
|
4474
|
+
}
|
|
4475
|
+
async setServers(servers) {
|
|
4476
|
+
const prevNames = new Set(Object.keys(this.configs));
|
|
4477
|
+
const nextNames = new Set(Object.keys(servers));
|
|
4478
|
+
const added = [...nextNames].filter((n) => !prevNames.has(n));
|
|
4479
|
+
const removed = [...prevNames].filter((n) => !nextNames.has(n));
|
|
4480
|
+
const errors = {};
|
|
4481
|
+
for (const name of removed) {
|
|
4482
|
+
await this.closeOne(name);
|
|
4483
|
+
delete this.configs[name];
|
|
4484
|
+
this.disabled.delete(name);
|
|
4485
|
+
this.servers.delete(name);
|
|
4486
|
+
}
|
|
4487
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
4488
|
+
const prev = this.configs[name];
|
|
4489
|
+
this.configs[name] = config;
|
|
4490
|
+
if (this.disabled.has(name))
|
|
4491
|
+
continue;
|
|
4492
|
+
if (!prev || JSON.stringify(this.toStatusConfig(prev)) !== JSON.stringify(this.toStatusConfig(config))) {
|
|
4493
|
+
await this.closeOne(name);
|
|
4494
|
+
await this.connectOne(name, config);
|
|
4495
|
+
}
|
|
4496
|
+
const status = this.servers.get(name)?.status;
|
|
4497
|
+
if (!status || status.status === "failed") {
|
|
4498
|
+
errors[name] = status?.error ?? "Failed to connect";
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
return { added, removed, errors };
|
|
4502
|
+
}
|
|
4503
|
+
async closeOne(serverName) {
|
|
4504
|
+
const existing = this.servers.get(serverName);
|
|
4505
|
+
if (existing?.client) {
|
|
4506
|
+
try {
|
|
4507
|
+
await existing.client.close();
|
|
4508
|
+
} catch {}
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
async closeAll() {
|
|
4512
|
+
for (const [name] of this.servers) {
|
|
4513
|
+
await this.closeOne(name);
|
|
4514
|
+
}
|
|
4515
|
+
this.servers.clear();
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
// src/utils/session-store.ts
|
|
4520
|
+
import { readFileSync as readFileSync6, appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
4521
|
+
import { join as join8 } from "path";
|
|
4522
|
+
import { homedir as homedir5 } from "os";
|
|
4523
|
+
function safeStringify(value) {
|
|
4524
|
+
try {
|
|
4525
|
+
return JSON.stringify(value);
|
|
4526
|
+
} catch {
|
|
4527
|
+
return String(value);
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
function sanitizeCwd(cwd) {
|
|
4531
|
+
return cwd.replace(/[/.]/g, "-");
|
|
4532
|
+
}
|
|
4533
|
+
function sessionsDir(cwd) {
|
|
4534
|
+
return join8(homedir5(), ".claude", "projects", sanitizeCwd(cwd));
|
|
4535
|
+
}
|
|
4536
|
+
function ensureDir(dir) {
|
|
4537
|
+
mkdirSync3(dir, { recursive: true });
|
|
4538
|
+
}
|
|
4539
|
+
function logMessage(dir, sessionId, entry) {
|
|
4540
|
+
ensureDir(dir);
|
|
4541
|
+
const filePath = join8(dir, `${sessionId}.jsonl`);
|
|
4542
|
+
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
4543
|
+
`);
|
|
4544
|
+
}
|
|
4545
|
+
function createSessionLogger(cwd, sessionId, model) {
|
|
4546
|
+
const dir = sessionsDir(cwd);
|
|
4547
|
+
let lastUuid = null;
|
|
4548
|
+
return (role, content, parentUuid) => {
|
|
4549
|
+
const entryUuid = uuid();
|
|
4550
|
+
let normalizedContent = content;
|
|
4551
|
+
if (role === "user" && typeof content === "string") {
|
|
4552
|
+
normalizedContent = [{ type: "text", text: content }];
|
|
4553
|
+
}
|
|
4554
|
+
const entry = {
|
|
4555
|
+
type: role,
|
|
4556
|
+
uuid: entryUuid,
|
|
4557
|
+
parentUuid: parentUuid ?? lastUuid,
|
|
4558
|
+
sessionId,
|
|
4559
|
+
timestamp: new Date().toISOString(),
|
|
4560
|
+
cwd,
|
|
4561
|
+
isSidechain: false,
|
|
4562
|
+
userType: "external",
|
|
4563
|
+
message: {
|
|
4564
|
+
role,
|
|
4565
|
+
content: normalizedContent,
|
|
4566
|
+
...role === "assistant" && model ? { model } : {}
|
|
4567
|
+
},
|
|
4568
|
+
...role === "user" ? { permissionMode: "default" } : {}
|
|
4569
|
+
};
|
|
4570
|
+
logMessage(dir, sessionId, entry);
|
|
4571
|
+
lastUuid = entryUuid;
|
|
4572
|
+
return entryUuid;
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
function findLatestSession(cwd) {
|
|
4576
|
+
const dir = sessionsDir(cwd);
|
|
4577
|
+
try {
|
|
4578
|
+
const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
|
|
4579
|
+
const filePath = join8(dir, f);
|
|
4580
|
+
try {
|
|
4581
|
+
return { name: f, mtime: statSync2(filePath).mtimeMs };
|
|
4582
|
+
} catch {
|
|
4583
|
+
return null;
|
|
4584
|
+
}
|
|
4585
|
+
}).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
|
|
4586
|
+
if (files.length === 0)
|
|
4587
|
+
return null;
|
|
4588
|
+
return files[0].name.replace(/\.jsonl$/, "");
|
|
4589
|
+
} catch {
|
|
4590
|
+
return null;
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
function loadSessionMessages(cwd, sessionId, resumeSessionAt) {
|
|
4594
|
+
const dir = sessionsDir(cwd);
|
|
4595
|
+
const filePath = join8(dir, `${sessionId}.jsonl`);
|
|
4596
|
+
let lines;
|
|
4597
|
+
try {
|
|
4598
|
+
lines = readFileSync6(filePath, "utf-8").trim().split(`
|
|
4599
|
+
`).filter(Boolean);
|
|
4600
|
+
} catch {
|
|
4601
|
+
return [];
|
|
4602
|
+
}
|
|
4603
|
+
const messages = [];
|
|
4604
|
+
let reachedResumePoint = false;
|
|
4605
|
+
for (const line of lines) {
|
|
4606
|
+
if (resumeSessionAt && reachedResumePoint)
|
|
4607
|
+
break;
|
|
4608
|
+
try {
|
|
4609
|
+
const entry = JSON.parse(line);
|
|
4610
|
+
if (entry.type !== "user" && entry.type !== "assistant")
|
|
3486
4611
|
continue;
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
4612
|
+
if (entry.isMeta === true)
|
|
4613
|
+
continue;
|
|
4614
|
+
const message = entry.message;
|
|
4615
|
+
if (!message)
|
|
4616
|
+
continue;
|
|
4617
|
+
const role = entry.type === "user" ? "user" : "assistant";
|
|
4618
|
+
let content;
|
|
4619
|
+
if (typeof message.content === "string") {
|
|
4620
|
+
content = message.content;
|
|
4621
|
+
} else if (Array.isArray(message.content)) {
|
|
4622
|
+
const normalizedBlocks = [];
|
|
4623
|
+
for (const c of message.content) {
|
|
4624
|
+
if (!c || typeof c !== "object")
|
|
4625
|
+
continue;
|
|
4626
|
+
const block = c;
|
|
4627
|
+
if (typeof block.type !== "string")
|
|
4628
|
+
continue;
|
|
4629
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
4630
|
+
normalizedBlocks.push({ type: "text", text: block.text });
|
|
4631
|
+
continue;
|
|
4632
|
+
}
|
|
4633
|
+
if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
4634
|
+
normalizedBlocks.push({
|
|
4635
|
+
type: "tool_use",
|
|
4636
|
+
id: block.id,
|
|
4637
|
+
name: block.name,
|
|
4638
|
+
input: block.input ?? {}
|
|
4639
|
+
});
|
|
4640
|
+
continue;
|
|
4641
|
+
}
|
|
4642
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
4643
|
+
normalizedBlocks.push({
|
|
4644
|
+
type: "tool_result",
|
|
4645
|
+
tool_use_id: block.tool_use_id,
|
|
4646
|
+
content: typeof block.content === "string" ? block.content : safeStringify(block.content),
|
|
4647
|
+
is_error: typeof block.is_error === "boolean" ? block.is_error : undefined
|
|
4648
|
+
});
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
normalizedBlocks.push({
|
|
4652
|
+
type: "text",
|
|
4653
|
+
text: `[session:${block.type}] ${safeStringify(block)}`
|
|
3496
4654
|
});
|
|
3497
4655
|
}
|
|
3498
|
-
|
|
3499
|
-
}
|
|
3500
|
-
return result;
|
|
3501
|
-
}
|
|
3502
|
-
async readResource(serverName, uri) {
|
|
3503
|
-
const server = this.servers.get(serverName);
|
|
3504
|
-
if (!server || server.status.status !== "connected") {
|
|
3505
|
-
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
3506
|
-
}
|
|
3507
|
-
const result = await server.client.readResource({ uri });
|
|
3508
|
-
const contents = result.contents ?? [];
|
|
3509
|
-
return contents.map((c) => {
|
|
3510
|
-
if ("text" in c)
|
|
3511
|
-
return c.text;
|
|
3512
|
-
if ("blob" in c)
|
|
3513
|
-
return `[binary data: ${c.mimeType ?? "unknown"}]`;
|
|
3514
|
-
return "";
|
|
3515
|
-
}).join("");
|
|
3516
|
-
}
|
|
3517
|
-
status() {
|
|
3518
|
-
const result = [];
|
|
3519
|
-
for (const [name] of Object.entries(this.configs)) {
|
|
3520
|
-
const server = this.servers.get(name);
|
|
3521
|
-
if (server) {
|
|
3522
|
-
result.push(server.status);
|
|
4656
|
+
content = normalizedBlocks;
|
|
3523
4657
|
} else {
|
|
3524
|
-
|
|
4658
|
+
continue;
|
|
3525
4659
|
}
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
async closeAll() {
|
|
3530
|
-
for (const [, server] of this.servers) {
|
|
3531
|
-
if (server.client) {
|
|
3532
|
-
try {
|
|
3533
|
-
await server.client.close();
|
|
3534
|
-
} catch {}
|
|
4660
|
+
messages.push({ role, content });
|
|
4661
|
+
if (resumeSessionAt && entry.uuid === resumeSessionAt) {
|
|
4662
|
+
reachedResumePoint = true;
|
|
3535
4663
|
}
|
|
3536
|
-
}
|
|
3537
|
-
this.servers.clear();
|
|
4664
|
+
} catch {}
|
|
3538
4665
|
}
|
|
4666
|
+
return messages;
|
|
3539
4667
|
}
|
|
3540
4668
|
|
|
3541
4669
|
// src/agents/tools.ts
|
|
@@ -3558,6 +4686,15 @@ function createTaskTool(ctx) {
|
|
|
3558
4686
|
type: "string",
|
|
3559
4687
|
description: "The type of agent to use. Must match a registered agent definition."
|
|
3560
4688
|
},
|
|
4689
|
+
model: {
|
|
4690
|
+
type: "string",
|
|
4691
|
+
enum: ["sonnet", "opus", "haiku"],
|
|
4692
|
+
description: "Optional model family hint for this subagent."
|
|
4693
|
+
},
|
|
4694
|
+
resume: {
|
|
4695
|
+
type: "string",
|
|
4696
|
+
description: "Optional session ID to resume this subagent from."
|
|
4697
|
+
},
|
|
3561
4698
|
run_in_background: {
|
|
3562
4699
|
type: "boolean",
|
|
3563
4700
|
description: "If true, run the task in the background and return a task ID."
|
|
@@ -3565,16 +4702,35 @@ function createTaskTool(ctx) {
|
|
|
3565
4702
|
max_turns: {
|
|
3566
4703
|
type: "number",
|
|
3567
4704
|
description: "Maximum number of turns for the subagent."
|
|
4705
|
+
},
|
|
4706
|
+
name: {
|
|
4707
|
+
type: "string",
|
|
4708
|
+
description: "Optional display name for the spawned subagent."
|
|
4709
|
+
},
|
|
4710
|
+
team_name: {
|
|
4711
|
+
type: "string",
|
|
4712
|
+
description: "Optional team name context for this subagent."
|
|
4713
|
+
},
|
|
4714
|
+
mode: {
|
|
4715
|
+
type: "string",
|
|
4716
|
+
enum: ["acceptEdits", "bypassPermissions", "default", "delegate", "dontAsk", "plan"],
|
|
4717
|
+
description: "Permission mode hint for the spawned subagent."
|
|
3568
4718
|
}
|
|
3569
4719
|
},
|
|
3570
|
-
required: ["prompt", "subagent_type"]
|
|
4720
|
+
required: ["description", "prompt", "subagent_type"]
|
|
3571
4721
|
},
|
|
3572
4722
|
async execute(input, toolCtx) {
|
|
3573
4723
|
const {
|
|
4724
|
+
description,
|
|
3574
4725
|
prompt,
|
|
3575
4726
|
subagent_type,
|
|
4727
|
+
model: requestedModel,
|
|
4728
|
+
resume,
|
|
3576
4729
|
run_in_background,
|
|
3577
|
-
max_turns
|
|
4730
|
+
max_turns,
|
|
4731
|
+
name,
|
|
4732
|
+
team_name,
|
|
4733
|
+
mode
|
|
3578
4734
|
} = input;
|
|
3579
4735
|
const agentDef = ctx.agents[subagent_type];
|
|
3580
4736
|
if (!agentDef) {
|
|
@@ -3588,28 +4744,53 @@ function createTaskTool(ctx) {
|
|
|
3588
4744
|
await ctx.parentHooks.fire("SubagentStart", { event: "SubagentStart", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
|
|
3589
4745
|
}
|
|
3590
4746
|
const provider = agentDef.provider ? getProvider(agentDef.provider) : ctx.parentProvider;
|
|
3591
|
-
const
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
|
-
}
|
|
4747
|
+
const modelAliases = {
|
|
4748
|
+
sonnet: "claude-sonnet-4-5-20250929",
|
|
4749
|
+
opus: "claude-opus-4-5-20251101",
|
|
4750
|
+
haiku: "claude-haiku-4-5-20251001"
|
|
4751
|
+
};
|
|
4752
|
+
const model = requestedModel ? modelAliases[requestedModel] : agentDef.model ?? ctx.parentModel;
|
|
4753
|
+
const baseTools = agentDef.tools ?? resolveToolNames({ type: "preset", preset: "claude_code" });
|
|
4754
|
+
const subTools = buildToolRegistry(baseTools, undefined, agentDef.disallowedTools);
|
|
3598
4755
|
const maxTurns = max_turns ?? agentDef.maxTurns ?? 10;
|
|
3599
|
-
const sessionId = uuid();
|
|
4756
|
+
const sessionId = resume ?? uuid();
|
|
4757
|
+
const previousMessages = resume ? loadSessionMessages(ctx.parentCwd, resume) : undefined;
|
|
3600
4758
|
const abortController = new AbortController;
|
|
3601
4759
|
if (toolCtx.signal) {
|
|
3602
4760
|
toolCtx.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
3603
4761
|
}
|
|
3604
|
-
const
|
|
4762
|
+
const systemPromptParts = [
|
|
4763
|
+
agentDef.prompt,
|
|
4764
|
+
`You are a subagent of type "${subagent_type}". ${agentDef.description}`,
|
|
4765
|
+
`Task summary: ${description}`
|
|
4766
|
+
];
|
|
4767
|
+
if (agentDef.criticalSystemReminder_EXPERIMENTAL) {
|
|
4768
|
+
systemPromptParts.push(`Critical reminder: ${agentDef.criticalSystemReminder_EXPERIMENTAL}`);
|
|
4769
|
+
}
|
|
4770
|
+
if (agentDef.skills && agentDef.skills.length > 0) {
|
|
4771
|
+
systemPromptParts.push(`Available skills:
|
|
4772
|
+
${agentDef.skills.map((s) => `- ${s}`).join(`
|
|
4773
|
+
`)}`);
|
|
4774
|
+
}
|
|
4775
|
+
if (name) {
|
|
4776
|
+
systemPromptParts.push(`Subagent name: ${name}`);
|
|
4777
|
+
}
|
|
4778
|
+
if (team_name) {
|
|
4779
|
+
systemPromptParts.push(`Team context: ${team_name}`);
|
|
4780
|
+
}
|
|
4781
|
+
if (mode) {
|
|
4782
|
+
systemPromptParts.push(`Permission mode hint: ${mode}`);
|
|
4783
|
+
}
|
|
4784
|
+
const systemPrompt = systemPromptParts.join(`
|
|
3605
4785
|
|
|
3606
|
-
|
|
4786
|
+
`);
|
|
3607
4787
|
const runAgent = async () => {
|
|
3608
4788
|
const messages = [];
|
|
3609
4789
|
let resultText = "";
|
|
3610
4790
|
for await (const msg of agentLoop(prompt, {
|
|
3611
4791
|
provider,
|
|
3612
4792
|
model,
|
|
4793
|
+
modelState: { current: model },
|
|
3613
4794
|
systemPrompt,
|
|
3614
4795
|
tools: subTools,
|
|
3615
4796
|
permissions: ctx.parentPermissions,
|
|
@@ -3617,18 +4798,23 @@ You are a subagent of type "${subagent_type}". ${agentDef.description}`;
|
|
|
3617
4798
|
sessionId,
|
|
3618
4799
|
maxTurns,
|
|
3619
4800
|
maxBudgetUsd: 5,
|
|
3620
|
-
|
|
4801
|
+
includePartialMessages: false,
|
|
3621
4802
|
signal: abortController.signal,
|
|
3622
4803
|
env: ctx.parentEnv,
|
|
3623
4804
|
debug: ctx.parentDebug,
|
|
3624
|
-
hooks: ctx.parentHooks
|
|
4805
|
+
hooks: ctx.parentHooks,
|
|
4806
|
+
previousMessages
|
|
3625
4807
|
})) {
|
|
3626
4808
|
messages.push(msg);
|
|
3627
|
-
if (msg.type === "
|
|
3628
|
-
|
|
4809
|
+
if (msg.type === "assistant") {
|
|
4810
|
+
for (const block of msg.message.content) {
|
|
4811
|
+
if (block.type === "text") {
|
|
4812
|
+
resultText += block.text;
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
3629
4815
|
}
|
|
3630
4816
|
if (msg.type === "result" && msg.subtype === "success") {
|
|
3631
|
-
resultText = msg.
|
|
4817
|
+
resultText = msg.result || resultText;
|
|
3632
4818
|
}
|
|
3633
4819
|
}
|
|
3634
4820
|
return resultText || "Subagent completed with no text output.";
|
|
@@ -3787,112 +4973,9 @@ class TaskManager {
|
|
|
3787
4973
|
}
|
|
3788
4974
|
}
|
|
3789
4975
|
|
|
3790
|
-
// src/utils/session-store.ts
|
|
3791
|
-
import { readFileSync as readFileSync6, appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3792
|
-
import { join as join6 } from "path";
|
|
3793
|
-
import { homedir as homedir5 } from "os";
|
|
3794
|
-
function sanitizeCwd(cwd) {
|
|
3795
|
-
return cwd.replace(/[/.]/g, "-");
|
|
3796
|
-
}
|
|
3797
|
-
function sessionsDir(cwd) {
|
|
3798
|
-
return join6(homedir5(), ".claude", "projects", sanitizeCwd(cwd));
|
|
3799
|
-
}
|
|
3800
|
-
function ensureDir(dir) {
|
|
3801
|
-
mkdirSync3(dir, { recursive: true });
|
|
3802
|
-
}
|
|
3803
|
-
function logMessage(dir, sessionId, entry) {
|
|
3804
|
-
ensureDir(dir);
|
|
3805
|
-
const filePath = join6(dir, `${sessionId}.jsonl`);
|
|
3806
|
-
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
3807
|
-
`);
|
|
3808
|
-
}
|
|
3809
|
-
function createSessionLogger(cwd, sessionId, model) {
|
|
3810
|
-
const dir = sessionsDir(cwd);
|
|
3811
|
-
let lastUuid = null;
|
|
3812
|
-
return (role, content, parentUuid) => {
|
|
3813
|
-
const entryUuid = uuid();
|
|
3814
|
-
let normalizedContent = content;
|
|
3815
|
-
if (role === "user" && typeof content === "string") {
|
|
3816
|
-
normalizedContent = [{ type: "text", text: content }];
|
|
3817
|
-
}
|
|
3818
|
-
const entry = {
|
|
3819
|
-
type: role,
|
|
3820
|
-
uuid: entryUuid,
|
|
3821
|
-
parentUuid: parentUuid ?? lastUuid,
|
|
3822
|
-
sessionId,
|
|
3823
|
-
timestamp: new Date().toISOString(),
|
|
3824
|
-
cwd,
|
|
3825
|
-
isSidechain: false,
|
|
3826
|
-
userType: "external",
|
|
3827
|
-
message: {
|
|
3828
|
-
role,
|
|
3829
|
-
content: normalizedContent,
|
|
3830
|
-
...role === "assistant" && model ? { model } : {}
|
|
3831
|
-
},
|
|
3832
|
-
...role === "user" ? { permissionMode: "default" } : {}
|
|
3833
|
-
};
|
|
3834
|
-
logMessage(dir, sessionId, entry);
|
|
3835
|
-
lastUuid = entryUuid;
|
|
3836
|
-
return entryUuid;
|
|
3837
|
-
};
|
|
3838
|
-
}
|
|
3839
|
-
function findLatestSession(cwd) {
|
|
3840
|
-
const dir = sessionsDir(cwd);
|
|
3841
|
-
try {
|
|
3842
|
-
const files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
|
|
3843
|
-
const filePath = join6(dir, f);
|
|
3844
|
-
try {
|
|
3845
|
-
return { name: f, mtime: statSync2(filePath).mtimeMs };
|
|
3846
|
-
} catch {
|
|
3847
|
-
return null;
|
|
3848
|
-
}
|
|
3849
|
-
}).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
|
|
3850
|
-
if (files.length === 0)
|
|
3851
|
-
return null;
|
|
3852
|
-
return files[0].name.replace(/\.jsonl$/, "");
|
|
3853
|
-
} catch {
|
|
3854
|
-
return null;
|
|
3855
|
-
}
|
|
3856
|
-
}
|
|
3857
|
-
function loadSessionMessages(cwd, sessionId) {
|
|
3858
|
-
const dir = sessionsDir(cwd);
|
|
3859
|
-
const filePath = join6(dir, `${sessionId}.jsonl`);
|
|
3860
|
-
let lines;
|
|
3861
|
-
try {
|
|
3862
|
-
lines = readFileSync6(filePath, "utf-8").trim().split(`
|
|
3863
|
-
`).filter(Boolean);
|
|
3864
|
-
} catch {
|
|
3865
|
-
return [];
|
|
3866
|
-
}
|
|
3867
|
-
const messages = [];
|
|
3868
|
-
for (const line of lines) {
|
|
3869
|
-
try {
|
|
3870
|
-
const entry = JSON.parse(line);
|
|
3871
|
-
if (entry.type !== "user" && entry.type !== "assistant")
|
|
3872
|
-
continue;
|
|
3873
|
-
if (entry.isMeta === true)
|
|
3874
|
-
continue;
|
|
3875
|
-
const message = entry.message;
|
|
3876
|
-
if (!message)
|
|
3877
|
-
continue;
|
|
3878
|
-
const role = entry.type === "user" ? "user" : "assistant";
|
|
3879
|
-
let content;
|
|
3880
|
-
if (typeof message.content === "string") {
|
|
3881
|
-
content = message.content;
|
|
3882
|
-
} else if (Array.isArray(message.content)) {
|
|
3883
|
-
content = message.content.filter((c) => c.type === "text" || c.type === "tool_use" || c.type === "tool_result").map((c) => c);
|
|
3884
|
-
} else {
|
|
3885
|
-
continue;
|
|
3886
|
-
}
|
|
3887
|
-
messages.push({ role, content });
|
|
3888
|
-
} catch {}
|
|
3889
|
-
}
|
|
3890
|
-
return messages;
|
|
3891
|
-
}
|
|
3892
|
-
|
|
3893
4976
|
// src/memory/memory-handler.ts
|
|
3894
|
-
import { readdir, stat, readFile, writeFile, rm, rename, mkdir as
|
|
3895
|
-
import { join as
|
|
4977
|
+
import { readdir, stat, readFile as readFile3, writeFile as writeFile4, rm, rename, mkdir as mkdir4 } from "fs/promises";
|
|
4978
|
+
import { join as join9, resolve as resolve2, relative as relative2 } from "path";
|
|
3896
4979
|
import { existsSync as existsSync5 } from "fs";
|
|
3897
4980
|
function createMemoryHandler(memoryDir) {
|
|
3898
4981
|
const absMemoryDir = resolve2(memoryDir);
|
|
@@ -3938,7 +5021,7 @@ function createMemoryHandler(memoryDir) {
|
|
|
3938
5021
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3939
5022
|
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
3940
5023
|
continue;
|
|
3941
|
-
const entryPath =
|
|
5024
|
+
const entryPath = join9(dirPath, entry.name);
|
|
3942
5025
|
const entryStat = await stat(entryPath);
|
|
3943
5026
|
lines.push(`${formatSize(entryStat.size)} ${toLogicalPath(entryPath)}`);
|
|
3944
5027
|
if (entry.isDirectory()) {
|
|
@@ -3981,7 +5064,7 @@ function createMemoryHandler(memoryDir) {
|
|
|
3981
5064
|
${lines.join(`
|
|
3982
5065
|
`)}`;
|
|
3983
5066
|
}
|
|
3984
|
-
const content = await
|
|
5067
|
+
const content = await readFile3(absPath, "utf-8");
|
|
3985
5068
|
const formatted = formatFileContent(content, cmd.view_range);
|
|
3986
5069
|
return `Here's the content of ${cmd.path} with line numbers:
|
|
3987
5070
|
${formatted}`;
|
|
@@ -3992,8 +5075,8 @@ ${formatted}`;
|
|
|
3992
5075
|
return `Error: File ${cmd.path} already exists`;
|
|
3993
5076
|
}
|
|
3994
5077
|
const parentDir = resolve2(absPath, "..");
|
|
3995
|
-
await
|
|
3996
|
-
await
|
|
5078
|
+
await mkdir4(parentDir, { recursive: true });
|
|
5079
|
+
await writeFile4(absPath, cmd.file_text, "utf-8");
|
|
3997
5080
|
return `File created successfully at: ${cmd.path}`;
|
|
3998
5081
|
}
|
|
3999
5082
|
async function handleStrReplace(cmd) {
|
|
@@ -4005,7 +5088,7 @@ ${formatted}`;
|
|
|
4005
5088
|
if (s.isDirectory()) {
|
|
4006
5089
|
return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
|
|
4007
5090
|
}
|
|
4008
|
-
const content = await
|
|
5091
|
+
const content = await readFile3(absPath, "utf-8");
|
|
4009
5092
|
const lines = content.split(`
|
|
4010
5093
|
`);
|
|
4011
5094
|
const matchingLines = [];
|
|
@@ -4028,7 +5111,7 @@ ${formatted}`;
|
|
|
4028
5111
|
return `No replacement was performed. Multiple occurrences of old_str \`${cmd.old_str}\` in lines: ${matchingLines.join(", ")}. Please ensure it is unique`;
|
|
4029
5112
|
}
|
|
4030
5113
|
const newContent = content.replace(cmd.old_str, cmd.new_str);
|
|
4031
|
-
await
|
|
5114
|
+
await writeFile4(absPath, newContent, "utf-8");
|
|
4032
5115
|
const newLines = newContent.split(`
|
|
4033
5116
|
`);
|
|
4034
5117
|
const replaceLine = matchingLines[0];
|
|
@@ -4048,7 +5131,7 @@ ${snippet}`;
|
|
|
4048
5131
|
if (s.isDirectory()) {
|
|
4049
5132
|
return `Error: The path ${cmd.path} does not exist`;
|
|
4050
5133
|
}
|
|
4051
|
-
const content = await
|
|
5134
|
+
const content = await readFile3(absPath, "utf-8");
|
|
4052
5135
|
const lines = content.split(`
|
|
4053
5136
|
`);
|
|
4054
5137
|
if (cmd.insert_line < 0 || cmd.insert_line > lines.length) {
|
|
@@ -4057,7 +5140,7 @@ ${snippet}`;
|
|
|
4057
5140
|
const insertLines = cmd.insert_text.split(`
|
|
4058
5141
|
`);
|
|
4059
5142
|
lines.splice(cmd.insert_line, 0, ...insertLines);
|
|
4060
|
-
await
|
|
5143
|
+
await writeFile4(absPath, lines.join(`
|
|
4061
5144
|
`), "utf-8");
|
|
4062
5145
|
return `The file ${cmd.path} has been edited.`;
|
|
4063
5146
|
}
|
|
@@ -4079,13 +5162,13 @@ ${snippet}`;
|
|
|
4079
5162
|
return `Error: The destination ${cmd.new_path} already exists`;
|
|
4080
5163
|
}
|
|
4081
5164
|
const parentDir = resolve2(newAbs, "..");
|
|
4082
|
-
await
|
|
5165
|
+
await mkdir4(parentDir, { recursive: true });
|
|
4083
5166
|
await rename(oldAbs, newAbs);
|
|
4084
5167
|
return `Successfully renamed ${cmd.old_path} to ${cmd.new_path}`;
|
|
4085
5168
|
}
|
|
4086
5169
|
async function execute(cmd) {
|
|
4087
5170
|
if (!existsSync5(absMemoryDir)) {
|
|
4088
|
-
await
|
|
5171
|
+
await mkdir4(absMemoryDir, { recursive: true });
|
|
4089
5172
|
}
|
|
4090
5173
|
switch (cmd.command) {
|
|
4091
5174
|
case "view":
|
|
@@ -4190,19 +5273,41 @@ function createMemoryTool(config) {
|
|
|
4190
5273
|
}
|
|
4191
5274
|
};
|
|
4192
5275
|
}
|
|
4193
|
-
|
|
4194
5276
|
// src/api.ts
|
|
4195
5277
|
var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
|
|
4196
5278
|
var DEFAULT_MAX_TURNS = 10;
|
|
4197
5279
|
var DEFAULT_MAX_BUDGET_USD = 5;
|
|
4198
5280
|
function query(params) {
|
|
4199
|
-
const {
|
|
5281
|
+
const { options = {} } = params;
|
|
5282
|
+
const prompt = params.prompt;
|
|
5283
|
+
if (typeof prompt !== "string") {
|
|
5284
|
+
throw new Error("query({ prompt: AsyncIterable }) is not supported in single-prompt mode yet. Use Query.streamInput() in a streaming session.");
|
|
5285
|
+
}
|
|
4200
5286
|
const providerName = options.provider ?? "anthropic";
|
|
4201
5287
|
const provider = getProvider(providerName, {
|
|
4202
5288
|
apiKey: options.apiKey,
|
|
4203
5289
|
baseUrl: options.baseUrl
|
|
4204
5290
|
});
|
|
4205
5291
|
const model = options.model ?? DEFAULT_MODEL;
|
|
5292
|
+
const fallbackModel = options.fallbackModel;
|
|
5293
|
+
const modelState = { current: model };
|
|
5294
|
+
const resolvedThinkingBudget = (() => {
|
|
5295
|
+
if (options.thinking) {
|
|
5296
|
+
switch (options.thinking.type) {
|
|
5297
|
+
case "disabled":
|
|
5298
|
+
return 0;
|
|
5299
|
+
case "enabled":
|
|
5300
|
+
return options.thinking.budgetTokens;
|
|
5301
|
+
case "adaptive":
|
|
5302
|
+
return;
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
return options.maxThinkingTokens;
|
|
5306
|
+
})();
|
|
5307
|
+
const maxThinkingTokensState = { current: resolvedThinkingBudget };
|
|
5308
|
+
if (options.permissionMode === "bypassPermissions" && options.allowDangerouslySkipPermissions !== true) {
|
|
5309
|
+
throw new Error('permissionMode "bypassPermissions" requires allowDangerouslySkipPermissions: true');
|
|
5310
|
+
}
|
|
4206
5311
|
const toolNames = resolveToolNames(options.tools);
|
|
4207
5312
|
const registry = buildToolRegistry(toolNames, undefined, options.disallowedTools);
|
|
4208
5313
|
let skills = [];
|
|
@@ -4219,12 +5324,22 @@ function query(params) {
|
|
|
4219
5324
|
}
|
|
4220
5325
|
}
|
|
4221
5326
|
}
|
|
4222
|
-
const systemPrompt =
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
5327
|
+
const systemPrompt = (() => {
|
|
5328
|
+
if (typeof options.systemPrompt === "string") {
|
|
5329
|
+
return options.systemPrompt;
|
|
5330
|
+
}
|
|
5331
|
+
const appendedPrompt = typeof options.systemPrompt === "object" ? [options.systemPrompt.append, options.appendSystemPrompt].filter(Boolean).join(`
|
|
5332
|
+
|
|
5333
|
+
`) : options.appendSystemPrompt;
|
|
5334
|
+
return buildSystemPrompt({
|
|
5335
|
+
tools: registry.list(),
|
|
5336
|
+
cwd: options.cwd,
|
|
5337
|
+
additionalDirectories: options.additionalDirectories,
|
|
5338
|
+
loadProjectInstructions: Array.isArray(options.settingSources) && options.settingSources.includes("project"),
|
|
5339
|
+
customPrompt: appendedPrompt,
|
|
5340
|
+
skills
|
|
5341
|
+
});
|
|
5342
|
+
})();
|
|
4228
5343
|
let settingsManager;
|
|
4229
5344
|
let mergedPermissions = options.permissions;
|
|
4230
5345
|
if (options.settingSources && options.settingSources.length > 0) {
|
|
@@ -4270,7 +5385,7 @@ function query(params) {
|
|
|
4270
5385
|
}
|
|
4271
5386
|
}
|
|
4272
5387
|
} else if (options.resume) {
|
|
4273
|
-
previousMessages = loadSessionMessages(cwd, options.resume);
|
|
5388
|
+
previousMessages = loadSessionMessages(cwd, options.resume, options.resumeSessionAt);
|
|
4274
5389
|
if (options.forkSession) {
|
|
4275
5390
|
sessionId = uuid();
|
|
4276
5391
|
} else {
|
|
@@ -4279,18 +5394,28 @@ function query(params) {
|
|
|
4279
5394
|
}
|
|
4280
5395
|
const persistSession = options.persistSession !== false;
|
|
4281
5396
|
const sessionLogger = persistSession ? createSessionLogger(cwd, sessionId, model) : undefined;
|
|
4282
|
-
const abortController = new AbortController;
|
|
5397
|
+
const abortController = options.abortController ?? new AbortController;
|
|
4283
5398
|
if (options.signal) {
|
|
4284
5399
|
options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
4285
5400
|
}
|
|
4286
5401
|
const hookManager = options.hooks ? new HookManager(options.hooks) : undefined;
|
|
4287
5402
|
const mcpClient = options.mcpServers && Object.keys(options.mcpServers).length > 0 ? new McpClientManager(options.mcpServers) : undefined;
|
|
5403
|
+
const syncMcpTools = () => {
|
|
5404
|
+
if (!mcpClient)
|
|
5405
|
+
return;
|
|
5406
|
+
registry.clearByPrefix("mcp__");
|
|
5407
|
+
for (const tool of mcpClient.getTools()) {
|
|
5408
|
+
registry.register(tool);
|
|
5409
|
+
}
|
|
5410
|
+
registry.register(createListMcpResourcesTool(mcpClient));
|
|
5411
|
+
registry.register(createReadMcpResourceTool(mcpClient));
|
|
5412
|
+
};
|
|
4288
5413
|
if (options.agents && Object.keys(options.agents).length > 0) {
|
|
4289
5414
|
const taskManager = new TaskManager;
|
|
4290
5415
|
const agentCtx = {
|
|
4291
5416
|
agents: options.agents,
|
|
4292
5417
|
parentProvider: provider,
|
|
4293
|
-
parentModel:
|
|
5418
|
+
parentModel: modelState.current,
|
|
4294
5419
|
parentPermissions: permissions,
|
|
4295
5420
|
parentHooks: hookManager,
|
|
4296
5421
|
parentCwd: cwd,
|
|
@@ -4314,6 +5439,12 @@ function query(params) {
|
|
|
4314
5439
|
const generator = agentLoop(prompt, {
|
|
4315
5440
|
provider,
|
|
4316
5441
|
model,
|
|
5442
|
+
modelState,
|
|
5443
|
+
maxThinkingTokensState,
|
|
5444
|
+
fallbackModel,
|
|
5445
|
+
thinking: options.thinking,
|
|
5446
|
+
effort: options.effort,
|
|
5447
|
+
outputFormat: options.outputFormat,
|
|
4317
5448
|
systemPrompt,
|
|
4318
5449
|
tools: registry,
|
|
4319
5450
|
permissions,
|
|
@@ -4321,7 +5452,7 @@ function query(params) {
|
|
|
4321
5452
|
sessionId,
|
|
4322
5453
|
maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
4323
5454
|
maxBudgetUsd: options.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
|
|
4324
|
-
|
|
5455
|
+
includePartialMessages: options.includePartialMessages ?? false,
|
|
4325
5456
|
signal: abortController.signal,
|
|
4326
5457
|
env: options.env,
|
|
4327
5458
|
debug: options.debug,
|
|
@@ -4329,9 +5460,95 @@ function query(params) {
|
|
|
4329
5460
|
mcpClient,
|
|
4330
5461
|
previousMessages,
|
|
4331
5462
|
sessionLogger,
|
|
4332
|
-
nativeMemoryTool
|
|
5463
|
+
nativeMemoryTool,
|
|
5464
|
+
initMeta: {
|
|
5465
|
+
betas: options.betas,
|
|
5466
|
+
outputStyle: "default",
|
|
5467
|
+
slashCommands: skills.map((s) => `/${s.name}`),
|
|
5468
|
+
skills: skills.map((s) => s.name),
|
|
5469
|
+
plugins: options.plugins,
|
|
5470
|
+
agents: options.agents ? Object.keys(options.agents) : undefined
|
|
5471
|
+
}
|
|
4333
5472
|
});
|
|
4334
|
-
|
|
5473
|
+
const controls = {
|
|
5474
|
+
async setPermissionMode(mode) {
|
|
5475
|
+
permissions.setMode(mode);
|
|
5476
|
+
},
|
|
5477
|
+
async setModel(nextModel) {
|
|
5478
|
+
modelState.current = nextModel ?? model;
|
|
5479
|
+
},
|
|
5480
|
+
async setMaxThinkingTokens(maxThinkingTokens) {
|
|
5481
|
+
maxThinkingTokensState.current = maxThinkingTokens ?? undefined;
|
|
5482
|
+
},
|
|
5483
|
+
async initializationResult() {
|
|
5484
|
+
const models = provider.listModels ? await provider.listModels() : [];
|
|
5485
|
+
return {
|
|
5486
|
+
commands: skills.map((s) => ({
|
|
5487
|
+
name: s.name,
|
|
5488
|
+
description: s.description,
|
|
5489
|
+
argumentHint: ""
|
|
5490
|
+
})),
|
|
5491
|
+
output_style: "default",
|
|
5492
|
+
available_output_styles: ["default"],
|
|
5493
|
+
models,
|
|
5494
|
+
account: {}
|
|
5495
|
+
};
|
|
5496
|
+
},
|
|
5497
|
+
async supportedCommands() {
|
|
5498
|
+
return skills.map((s) => ({
|
|
5499
|
+
name: s.name,
|
|
5500
|
+
description: s.description,
|
|
5501
|
+
argumentHint: ""
|
|
5502
|
+
}));
|
|
5503
|
+
},
|
|
5504
|
+
async supportedModels() {
|
|
5505
|
+
return provider.listModels ? await provider.listModels() : [];
|
|
5506
|
+
},
|
|
5507
|
+
async mcpServerStatus() {
|
|
5508
|
+
return mcpClient ? mcpClient.status() : [];
|
|
5509
|
+
},
|
|
5510
|
+
async accountInfo() {
|
|
5511
|
+
return {
|
|
5512
|
+
tokenSource: options.apiKey ? "api-key" : "runtime",
|
|
5513
|
+
apiKeySource: options.apiKey ? "explicit" : "env_or_oauth"
|
|
5514
|
+
};
|
|
5515
|
+
},
|
|
5516
|
+
async rewindFiles(_userMessageId, _options) {
|
|
5517
|
+
if (!options.enableFileCheckpointing) {
|
|
5518
|
+
return {
|
|
5519
|
+
canRewind: false,
|
|
5520
|
+
error: "File checkpointing is disabled. Set enableFileCheckpointing: true."
|
|
5521
|
+
};
|
|
5522
|
+
}
|
|
5523
|
+
return {
|
|
5524
|
+
canRewind: false,
|
|
5525
|
+
error: "File checkpoint rewind is not implemented in fourmis-agent-sdk yet."
|
|
5526
|
+
};
|
|
5527
|
+
},
|
|
5528
|
+
async reconnectMcpServer(serverName) {
|
|
5529
|
+
if (!mcpClient)
|
|
5530
|
+
throw new Error("No MCP servers are configured for this query.");
|
|
5531
|
+
await mcpClient.reconnectServer(serverName);
|
|
5532
|
+
syncMcpTools();
|
|
5533
|
+
},
|
|
5534
|
+
async toggleMcpServer(serverName, enabled) {
|
|
5535
|
+
if (!mcpClient)
|
|
5536
|
+
throw new Error("No MCP servers are configured for this query.");
|
|
5537
|
+
await mcpClient.toggleServer(serverName, enabled);
|
|
5538
|
+
syncMcpTools();
|
|
5539
|
+
},
|
|
5540
|
+
async setMcpServers(servers) {
|
|
5541
|
+
if (!mcpClient)
|
|
5542
|
+
throw new Error("No MCP client is available for this query.");
|
|
5543
|
+
const result = await mcpClient.setServers(servers);
|
|
5544
|
+
syncMcpTools();
|
|
5545
|
+
return result;
|
|
5546
|
+
},
|
|
5547
|
+
async streamInput() {
|
|
5548
|
+
throw new Error("Query.streamInput is not implemented for single-prompt query mode.");
|
|
5549
|
+
}
|
|
5550
|
+
};
|
|
5551
|
+
return createQuery(generator, abortController, controls);
|
|
4335
5552
|
}
|
|
4336
5553
|
export {
|
|
4337
5554
|
query
|