fourmis-agents-sdk 0.3.0 → 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 +2394 -886
- 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 +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2407 -887
- 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.d.ts +15 -0
- package/dist/skills/frontmatter.d.ts.map +1 -0
- package/dist/skills/frontmatter.js +66 -0
- package/dist/skills/index.d.ts +8 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +326 -0
- package/dist/skills/skills.d.ts +94 -0
- package/dist/skills/skills.d.ts.map +1 -0
- package/dist/skills/skills.js +324 -0
- 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 +360 -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 +4 -0
- package/dist/utils/system-prompt.d.ts.map +1 -1
- package/dist/utils/system-prompt.js +326 -6
- package/package.json +4 -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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
let assistantTextParts = [];
|
|
557
|
-
let toolCalls = [];
|
|
558
|
-
let turnUsage = emptyTokenUsage();
|
|
559
|
-
const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
|
|
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
|
-
}
|
|
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
|
+
};
|
|
591
531
|
}
|
|
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;
|
|
596
|
-
}
|
|
597
|
-
apiTimeMs += Date.now() - apiStart;
|
|
598
|
-
turns++;
|
|
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
|
-
};
|
|
610
|
-
}
|
|
611
|
-
modelUsage[model].inputTokens += turnUsage.inputTokens;
|
|
612
|
-
modelUsage[model].outputTokens += turnUsage.outputTokens;
|
|
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 });
|
|
532
|
+
return { behavior: "allow" };
|
|
620
533
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
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
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return { behavior: "allow" };
|
|
628
542
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
sessionLogger("assistant", assistantContent, null);
|
|
543
|
+
if (matchesRule(this.allowRules, toolName, input)) {
|
|
544
|
+
return { behavior: "allow" };
|
|
632
545
|
}
|
|
633
|
-
if (
|
|
634
|
-
|
|
546
|
+
if (SAFE_TOOLS.has(toolName)) {
|
|
547
|
+
return { behavior: "allow" };
|
|
635
548
|
}
|
|
636
|
-
if (
|
|
637
|
-
if (
|
|
638
|
-
|
|
549
|
+
if (this.mode === "acceptEdits") {
|
|
550
|
+
if (EDIT_TOOLS.has(toolName)) {
|
|
551
|
+
return { behavior: "allow" };
|
|
639
552
|
}
|
|
640
|
-
if (
|
|
641
|
-
|
|
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
|
+
}
|
|
642
558
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
559
|
+
}
|
|
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;
|
|
570
|
+
}
|
|
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.`
|
|
655
575
|
};
|
|
656
|
-
return;
|
|
657
576
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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);
|
|
583
|
+
}
|
|
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
|
+
}
|
|
667
592
|
}
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
}
|
|
670
601
|
}
|
|
671
|
-
|
|
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;
|
|
672
613
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
614
|
+
}
|
|
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)
|
|
692
641
|
continue;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
type: "tool_result",
|
|
700
|
-
id: call.id,
|
|
701
|
-
name: call.name,
|
|
702
|
-
content: denyContent,
|
|
703
|
-
isError: true,
|
|
704
|
-
uuid: uuid()
|
|
705
|
-
};
|
|
706
|
-
toolResults.push({
|
|
707
|
-
type: "tool_result",
|
|
708
|
-
tool_use_id: call.id,
|
|
709
|
-
content: denyContent,
|
|
710
|
-
is_error: true
|
|
711
|
-
});
|
|
712
|
-
if (hooks) {
|
|
713
|
-
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
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));
|
|
647
|
+
}
|
|
714
648
|
}
|
|
715
|
-
continue;
|
|
716
649
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
input: toolInput,
|
|
723
|
-
uuid: uuid()
|
|
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 };
|
|
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
|
+
}
|
|
733
655
|
}
|
|
734
|
-
} else {
|
|
735
|
-
const toolCtx = {
|
|
736
|
-
cwd,
|
|
737
|
-
signal,
|
|
738
|
-
sessionId,
|
|
739
|
-
env
|
|
740
|
-
};
|
|
741
|
-
result = await tools.execute(call.name, toolInput, toolCtx);
|
|
742
656
|
}
|
|
743
|
-
|
|
744
|
-
|
|
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;
|
|
745
683
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
${postResult.additionalContext}`;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
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;
|
|
756
691
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
content: result.content,
|
|
769
|
-
is_error: result.isError
|
|
770
|
-
});
|
|
692
|
+
case "replaceRules": {
|
|
693
|
+
const key = update.behavior;
|
|
694
|
+
perms[key] = update.rules.map((r) => this.serializeRule(r));
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case "setMode": {
|
|
698
|
+
perms.defaultMode = update.mode;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
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(`
|
|
@@ -2626,6 +2730,405 @@ async function collectFiles(dir, globPattern) {
|
|
|
2626
2730
|
}
|
|
2627
2731
|
return files;
|
|
2628
2732
|
}
|
|
2733
|
+
|
|
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
|
+
}
|
|
2798
|
+
}
|
|
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)."
|
|
2825
|
+
}
|
|
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"
|
|
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)`;
|
|
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);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
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)."
|
|
2888
|
+
}
|
|
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
|
+
};
|
|
2910
|
+
}
|
|
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." };
|
|
2915
|
+
}
|
|
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}`);
|
|
2922
|
+
}
|
|
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 };
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
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 };
|
|
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
|
+
};
|
|
2961
|
+
}
|
|
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 };
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
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 = {};
|
|
3086
|
+
try {
|
|
3087
|
+
const raw = await readFile2(filePath, "utf-8");
|
|
3088
|
+
data = JSON.parse(raw);
|
|
3089
|
+
} catch {
|
|
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
|
+
};
|
|
2629
3132
|
// src/tools/index.ts
|
|
2630
3133
|
var ALL_TOOLS = {
|
|
2631
3134
|
Bash: BashTool,
|
|
@@ -2633,7 +3136,14 @@ var ALL_TOOLS = {
|
|
|
2633
3136
|
Write: WriteTool,
|
|
2634
3137
|
Edit: EditTool,
|
|
2635
3138
|
Glob: GlobTool,
|
|
2636
|
-
Grep: GrepTool
|
|
3139
|
+
Grep: GrepTool,
|
|
3140
|
+
NotebookEdit: NotebookEditTool,
|
|
3141
|
+
WebFetch: WebFetchTool,
|
|
3142
|
+
WebSearch: WebSearchTool,
|
|
3143
|
+
AskUserQuestion: AskUserQuestionTool,
|
|
3144
|
+
TodoWrite: TodoWriteTool,
|
|
3145
|
+
Config: ConfigTool,
|
|
3146
|
+
ExitPlanMode: ExitPlanModeTool
|
|
2637
3147
|
};
|
|
2638
3148
|
function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
|
|
2639
3149
|
const registry = new ToolRegistry;
|
|
@@ -2648,277 +3158,299 @@ function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
|
|
|
2648
3158
|
return registry;
|
|
2649
3159
|
}
|
|
2650
3160
|
|
|
2651
|
-
// src/
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
if (!rules)
|
|
2658
|
-
return [];
|
|
2659
|
-
return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
|
|
3161
|
+
// src/skills/frontmatter.ts
|
|
3162
|
+
import { parse } from "yaml";
|
|
3163
|
+
function normalizeNewlines(value) {
|
|
3164
|
+
return value.replace(/\r\n/g, `
|
|
3165
|
+
`).replace(/\r/g, `
|
|
3166
|
+
`);
|
|
2660
3167
|
}
|
|
2661
|
-
function
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
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;
|
|
3168
|
+
function extractFrontmatter(content) {
|
|
3169
|
+
const normalized = normalizeNewlines(content);
|
|
3170
|
+
if (!normalized.startsWith("---")) {
|
|
3171
|
+
return { yamlString: null, body: normalized };
|
|
2670
3172
|
}
|
|
2671
|
-
|
|
3173
|
+
const endIndex = normalized.indexOf(`
|
|
3174
|
+
---`, 3);
|
|
3175
|
+
if (endIndex === -1) {
|
|
3176
|
+
return { yamlString: null, body: normalized };
|
|
3177
|
+
}
|
|
3178
|
+
return {
|
|
3179
|
+
yamlString: normalized.slice(4, endIndex),
|
|
3180
|
+
body: normalized.slice(endIndex + 4).trim()
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
function parseFrontmatter(content) {
|
|
3184
|
+
const { yamlString, body } = extractFrontmatter(content);
|
|
3185
|
+
if (!yamlString) {
|
|
3186
|
+
return { frontmatter: {}, body };
|
|
3187
|
+
}
|
|
3188
|
+
const parsed = parse(yamlString);
|
|
3189
|
+
return { frontmatter: parsed ?? {}, body };
|
|
3190
|
+
}
|
|
3191
|
+
function stripFrontmatter(content) {
|
|
3192
|
+
return parseFrontmatter(content).body;
|
|
2672
3193
|
}
|
|
2673
3194
|
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
3195
|
+
// src/skills/skills.ts
|
|
3196
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, realpathSync, statSync } from "fs";
|
|
3197
|
+
import { homedir as homedir4 } from "os";
|
|
3198
|
+
import { basename, dirname as dirname5, isAbsolute, join as join6, resolve } from "path";
|
|
3199
|
+
var MAX_NAME_LENGTH = 64;
|
|
3200
|
+
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
3201
|
+
var MAX_COMPATIBILITY_LENGTH = 500;
|
|
3202
|
+
var CONFIG_DIR_NAME = ".claude";
|
|
3203
|
+
function shouldIgnore(name) {
|
|
3204
|
+
return name.startsWith(".") || name === "node_modules";
|
|
3205
|
+
}
|
|
3206
|
+
function validateName(name, parentDirName) {
|
|
3207
|
+
const errors = [];
|
|
3208
|
+
if (name !== parentDirName) {
|
|
3209
|
+
errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
|
|
2686
3210
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
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" };
|
|
3211
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
3212
|
+
errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
|
2746
3213
|
}
|
|
2747
|
-
|
|
2748
|
-
|
|
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;
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
3214
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
3215
|
+
errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
|
|
2783
3216
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
3217
|
+
if (name.startsWith("-") || name.endsWith("-")) {
|
|
3218
|
+
errors.push(`name must not start or end with a hyphen`);
|
|
2786
3219
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
3220
|
+
if (name.includes("--")) {
|
|
3221
|
+
errors.push(`name must not contain consecutive hyphens`);
|
|
2789
3222
|
}
|
|
3223
|
+
return errors;
|
|
2790
3224
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
3225
|
+
function validateDescription(description) {
|
|
3226
|
+
const errors = [];
|
|
3227
|
+
if (!description || description.trim() === "") {
|
|
3228
|
+
errors.push("description is required");
|
|
3229
|
+
} else if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
3230
|
+
errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
|
|
3231
|
+
}
|
|
3232
|
+
return errors;
|
|
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})`];
|
|
2801
3239
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
3240
|
+
return [];
|
|
3241
|
+
}
|
|
3242
|
+
function loadSkillsFromDir(options) {
|
|
3243
|
+
return loadSkillsFromDirInternal(options.dir, options.source, true);
|
|
3244
|
+
}
|
|
3245
|
+
function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
|
|
3246
|
+
const skills = [];
|
|
3247
|
+
const diagnostics = [];
|
|
3248
|
+
if (!existsSync4(dir)) {
|
|
3249
|
+
return { skills, diagnostics };
|
|
3250
|
+
}
|
|
3251
|
+
try {
|
|
3252
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
3253
|
+
for (const entry of entries) {
|
|
3254
|
+
if (shouldIgnore(entry.name)) {
|
|
2809
3255
|
continue;
|
|
2810
|
-
const perms = data.permissions;
|
|
2811
|
-
if (Array.isArray(perms.allow)) {
|
|
2812
|
-
for (const rule of perms.allow) {
|
|
2813
|
-
if (typeof rule === "string") {
|
|
2814
|
-
allAllow.push(this.parseRule(rule));
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
3256
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
3257
|
+
const fullPath = join6(dir, entry.name);
|
|
3258
|
+
let isDirectory = entry.isDirectory();
|
|
3259
|
+
let isFile = entry.isFile();
|
|
3260
|
+
if (entry.isSymbolicLink()) {
|
|
3261
|
+
try {
|
|
3262
|
+
const stats = statSync(fullPath);
|
|
3263
|
+
isDirectory = stats.isDirectory();
|
|
3264
|
+
isFile = stats.isFile();
|
|
3265
|
+
} catch {
|
|
3266
|
+
continue;
|
|
2823
3267
|
}
|
|
2824
3268
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
result.deny = allDeny;
|
|
2831
|
-
return result;
|
|
2832
|
-
}
|
|
2833
|
-
persistUpdate(update) {
|
|
2834
|
-
const path = this.destinationToPath(update.destination);
|
|
2835
|
-
if (!path)
|
|
2836
|
-
return;
|
|
2837
|
-
const data = this.readJson(path) ?? {};
|
|
2838
|
-
if (!data.permissions)
|
|
2839
|
-
data.permissions = {};
|
|
2840
|
-
const perms = data.permissions;
|
|
2841
|
-
switch (update.type) {
|
|
2842
|
-
case "addRules": {
|
|
2843
|
-
const key = update.behavior;
|
|
2844
|
-
const existing = Array.isArray(perms[key]) ? perms[key] : [];
|
|
2845
|
-
const newRules = update.rules.map((r) => this.serializeRule(r));
|
|
2846
|
-
const set = new Set(existing);
|
|
2847
|
-
for (const rule of newRules)
|
|
2848
|
-
set.add(rule);
|
|
2849
|
-
perms[key] = [...set];
|
|
2850
|
-
break;
|
|
2851
|
-
}
|
|
2852
|
-
case "removeRules": {
|
|
2853
|
-
const key = update.behavior;
|
|
2854
|
-
if (!Array.isArray(perms[key]))
|
|
2855
|
-
break;
|
|
2856
|
-
const toRemove = new Set(update.rules.map((r) => this.serializeRule(r)));
|
|
2857
|
-
perms[key] = perms[key].filter((r) => !toRemove.has(r));
|
|
2858
|
-
break;
|
|
2859
|
-
}
|
|
2860
|
-
case "replaceRules": {
|
|
2861
|
-
const key = update.behavior;
|
|
2862
|
-
perms[key] = update.rules.map((r) => this.serializeRule(r));
|
|
2863
|
-
break;
|
|
3269
|
+
if (isDirectory) {
|
|
3270
|
+
const subResult = loadSkillsFromDirInternal(fullPath, source, false);
|
|
3271
|
+
skills.push(...subResult.skills);
|
|
3272
|
+
diagnostics.push(...subResult.diagnostics);
|
|
3273
|
+
continue;
|
|
2864
3274
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3275
|
+
if (!isFile)
|
|
3276
|
+
continue;
|
|
3277
|
+
const isRootMd = includeRootFiles && entry.name.endsWith(".md");
|
|
3278
|
+
const isSkillMd = !includeRootFiles && entry.name === "SKILL.md";
|
|
3279
|
+
if (!isRootMd && !isSkillMd)
|
|
3280
|
+
continue;
|
|
3281
|
+
const result = loadSkillFromFile(fullPath, source);
|
|
3282
|
+
if (result.skill) {
|
|
3283
|
+
skills.push(result.skill);
|
|
2868
3284
|
}
|
|
2869
|
-
|
|
2870
|
-
return;
|
|
2871
|
-
}
|
|
2872
|
-
const dir = dirname2(path);
|
|
2873
|
-
if (!existsSync3(dir))
|
|
2874
|
-
mkdirSync2(dir, { recursive: true });
|
|
2875
|
-
writeFileSync3(path, JSON.stringify(data, null, 2) + `
|
|
2876
|
-
`);
|
|
2877
|
-
}
|
|
2878
|
-
sourceToPath(source) {
|
|
2879
|
-
switch (source) {
|
|
2880
|
-
case "user":
|
|
2881
|
-
return join3(homedir3(), ".claude", "settings.json");
|
|
2882
|
-
case "project":
|
|
2883
|
-
return join3(this.cwd, ".claude", "settings.json");
|
|
2884
|
-
case "local":
|
|
2885
|
-
return join3(this.cwd, ".claude", "settings.local.json");
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
destinationToPath(destination) {
|
|
2889
|
-
switch (destination) {
|
|
2890
|
-
case "userSettings":
|
|
2891
|
-
return join3(homedir3(), ".claude", "settings.json");
|
|
2892
|
-
case "projectSettings":
|
|
2893
|
-
return join3(this.cwd, ".claude", "settings.json");
|
|
2894
|
-
case "localSettings":
|
|
2895
|
-
return join3(this.cwd, ".claude", "settings.local.json");
|
|
2896
|
-
default:
|
|
2897
|
-
return null;
|
|
3285
|
+
diagnostics.push(...result.diagnostics);
|
|
2898
3286
|
}
|
|
3287
|
+
} catch {}
|
|
3288
|
+
return { skills, diagnostics };
|
|
3289
|
+
}
|
|
3290
|
+
function loadSkillFromFile(filePath, source) {
|
|
3291
|
+
const diagnostics = [];
|
|
3292
|
+
try {
|
|
3293
|
+
const rawContent = readFileSync4(filePath, "utf-8");
|
|
3294
|
+
const { frontmatter } = parseFrontmatter(rawContent);
|
|
3295
|
+
const skillDir = dirname5(filePath);
|
|
3296
|
+
const parentDirName = basename(skillDir);
|
|
3297
|
+
const descErrors = validateDescription(frontmatter.description);
|
|
3298
|
+
for (const error of descErrors) {
|
|
3299
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3300
|
+
}
|
|
3301
|
+
const name = frontmatter.name || parentDirName;
|
|
3302
|
+
const nameErrors = validateName(name, parentDirName);
|
|
3303
|
+
for (const error of nameErrors) {
|
|
3304
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3305
|
+
}
|
|
3306
|
+
const compatErrors = validateCompatibility(frontmatter.compatibility);
|
|
3307
|
+
for (const error of compatErrors) {
|
|
3308
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3309
|
+
}
|
|
3310
|
+
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
|
3311
|
+
return { skill: null, diagnostics };
|
|
3312
|
+
}
|
|
3313
|
+
const allowedToolsRaw = frontmatter["allowed-tools"];
|
|
3314
|
+
const allowedTools = allowedToolsRaw ? allowedToolsRaw.split(/\s+/).filter(Boolean) : undefined;
|
|
3315
|
+
return {
|
|
3316
|
+
skill: {
|
|
3317
|
+
name,
|
|
3318
|
+
description: frontmatter.description,
|
|
3319
|
+
filePath,
|
|
3320
|
+
baseDir: skillDir,
|
|
3321
|
+
source,
|
|
3322
|
+
disableModelInvocation: frontmatter["disable-model-invocation"] === true,
|
|
3323
|
+
license: frontmatter.license,
|
|
3324
|
+
compatibility: frontmatter.compatibility,
|
|
3325
|
+
metadata: frontmatter.metadata,
|
|
3326
|
+
allowedTools
|
|
3327
|
+
},
|
|
3328
|
+
diagnostics
|
|
3329
|
+
};
|
|
3330
|
+
} catch (error) {
|
|
3331
|
+
const message = error instanceof Error ? error.message : "failed to parse skill file";
|
|
3332
|
+
diagnostics.push({ type: "warning", message, path: filePath });
|
|
3333
|
+
return { skill: null, diagnostics };
|
|
2899
3334
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3335
|
+
}
|
|
3336
|
+
function normalizePath(input) {
|
|
3337
|
+
const trimmed = input.trim();
|
|
3338
|
+
if (trimmed === "~")
|
|
3339
|
+
return homedir4();
|
|
3340
|
+
if (trimmed.startsWith("~/"))
|
|
3341
|
+
return join6(homedir4(), trimmed.slice(2));
|
|
3342
|
+
if (trimmed.startsWith("~"))
|
|
3343
|
+
return join6(homedir4(), trimmed.slice(1));
|
|
3344
|
+
return trimmed;
|
|
3345
|
+
}
|
|
3346
|
+
function resolveSkillPath(p, cwd) {
|
|
3347
|
+
const normalized = normalizePath(p);
|
|
3348
|
+
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
|
3349
|
+
}
|
|
3350
|
+
function loadSkills(options = {}) {
|
|
3351
|
+
const { cwd = process.cwd(), skillPaths = [], includeDefaults = true } = options;
|
|
3352
|
+
const skillMap = new Map;
|
|
3353
|
+
const realPathSet = new Set;
|
|
3354
|
+
const allDiagnostics = [];
|
|
3355
|
+
const collisionDiagnostics = [];
|
|
3356
|
+
function addSkills(result) {
|
|
3357
|
+
allDiagnostics.push(...result.diagnostics);
|
|
3358
|
+
for (const skill of result.skills) {
|
|
3359
|
+
let realPath;
|
|
3360
|
+
try {
|
|
3361
|
+
realPath = realpathSync(skill.filePath);
|
|
3362
|
+
} catch {
|
|
3363
|
+
realPath = skill.filePath;
|
|
3364
|
+
}
|
|
3365
|
+
if (realPathSet.has(realPath))
|
|
3366
|
+
continue;
|
|
3367
|
+
const existing = skillMap.get(skill.name);
|
|
3368
|
+
if (existing) {
|
|
3369
|
+
collisionDiagnostics.push({
|
|
3370
|
+
type: "collision",
|
|
3371
|
+
message: `name "${skill.name}" collision`,
|
|
3372
|
+
path: skill.filePath,
|
|
3373
|
+
collision: {
|
|
3374
|
+
resourceType: "skill",
|
|
3375
|
+
name: skill.name,
|
|
3376
|
+
winnerPath: existing.filePath,
|
|
3377
|
+
loserPath: skill.filePath
|
|
3378
|
+
}
|
|
3379
|
+
});
|
|
3380
|
+
} else {
|
|
3381
|
+
skillMap.set(skill.name, skill);
|
|
3382
|
+
realPathSet.add(realPath);
|
|
3383
|
+
}
|
|
2906
3384
|
}
|
|
2907
3385
|
}
|
|
2908
|
-
|
|
2909
|
-
const
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3386
|
+
if (includeDefaults) {
|
|
3387
|
+
const userSkillsDir = join6(homedir4(), CONFIG_DIR_NAME, "skills");
|
|
3388
|
+
const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
|
|
3389
|
+
addSkills(loadSkillsFromDirInternal(userSkillsDir, "user", true));
|
|
3390
|
+
addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", true));
|
|
2913
3391
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
3392
|
+
for (const rawPath of skillPaths) {
|
|
3393
|
+
const resolvedPath = resolveSkillPath(rawPath, cwd);
|
|
3394
|
+
if (!existsSync4(resolvedPath)) {
|
|
3395
|
+
allDiagnostics.push({ type: "warning", message: "skill path does not exist", path: resolvedPath });
|
|
3396
|
+
continue;
|
|
3397
|
+
}
|
|
3398
|
+
try {
|
|
3399
|
+
const stats = statSync(resolvedPath);
|
|
3400
|
+
if (stats.isDirectory()) {
|
|
3401
|
+
addSkills(loadSkillsFromDirInternal(resolvedPath, "path", true));
|
|
3402
|
+
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
|
3403
|
+
const result = loadSkillFromFile(resolvedPath, "path");
|
|
3404
|
+
if (result.skill) {
|
|
3405
|
+
addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
|
|
3406
|
+
} else {
|
|
3407
|
+
allDiagnostics.push(...result.diagnostics);
|
|
3408
|
+
}
|
|
3409
|
+
} else {
|
|
3410
|
+
allDiagnostics.push({ type: "warning", message: "skill path is not a markdown file", path: resolvedPath });
|
|
3411
|
+
}
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
const message = error instanceof Error ? error.message : "failed to read skill path";
|
|
3414
|
+
allDiagnostics.push({ type: "warning", message, path: resolvedPath });
|
|
3415
|
+
}
|
|
2916
3416
|
}
|
|
3417
|
+
return {
|
|
3418
|
+
skills: Array.from(skillMap.values()),
|
|
3419
|
+
diagnostics: [...allDiagnostics, ...collisionDiagnostics]
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
function escapeXml(str) {
|
|
3423
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3424
|
+
}
|
|
3425
|
+
function formatSkillsForPrompt(skills) {
|
|
3426
|
+
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
|
|
3427
|
+
if (visibleSkills.length === 0) {
|
|
3428
|
+
return "";
|
|
3429
|
+
}
|
|
3430
|
+
const lines = [
|
|
3431
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
3432
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
3433
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
3434
|
+
"",
|
|
3435
|
+
"<available_skills>"
|
|
3436
|
+
];
|
|
3437
|
+
for (const skill of visibleSkills) {
|
|
3438
|
+
lines.push(" <skill>");
|
|
3439
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
3440
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
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
|
+
}
|
|
3445
|
+
lines.push(" </skill>");
|
|
3446
|
+
}
|
|
3447
|
+
lines.push("</available_skills>");
|
|
3448
|
+
return lines.join(`
|
|
3449
|
+
`);
|
|
2917
3450
|
}
|
|
2918
|
-
|
|
2919
3451
|
// src/utils/system-prompt.ts
|
|
2920
|
-
import { readFileSync as
|
|
2921
|
-
import { join as
|
|
3452
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3453
|
+
import { join as join7 } from "path";
|
|
2922
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.
|
|
2923
3455
|
|
|
2924
3456
|
You are highly capable and can help users complete complex tasks that would otherwise be too difficult or time-consuming.`;
|
|
@@ -2976,39 +3508,552 @@ function buildSystemPrompt(context) {
|
|
|
2976
3508
|
if (guidelines) {
|
|
2977
3509
|
sections.push(guidelines);
|
|
2978
3510
|
}
|
|
2979
|
-
}
|
|
2980
|
-
sections.push(CODING_GUIDELINES);
|
|
2981
|
-
if (context.cwd) {
|
|
2982
|
-
sections.push(`# Environment
|
|
2983
|
-
|
|
2984
|
-
Working directory: ${context.cwd}`);
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3511
|
+
}
|
|
3512
|
+
sections.push(CODING_GUIDELINES);
|
|
3513
|
+
if (context.cwd) {
|
|
3514
|
+
sections.push(`# Environment
|
|
3515
|
+
|
|
3516
|
+
Working directory: ${context.cwd}`);
|
|
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
|
|
3526
|
+
|
|
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
|
+
});
|
|
4033
|
+
}
|
|
4034
|
+
messages.push({ role: "user", content: toolResults });
|
|
4035
|
+
if (sessionLogger) {
|
|
4036
|
+
sessionLogger("user", toolResults, null);
|
|
2990
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
|
+
};
|
|
2991
4049
|
}
|
|
2992
|
-
if (context.customPrompt) {
|
|
2993
|
-
sections.push(context.customPrompt);
|
|
2994
|
-
}
|
|
2995
|
-
return sections.join(`
|
|
2996
|
-
|
|
2997
|
-
`);
|
|
2998
|
-
}
|
|
2999
|
-
function readProjectInstructions(cwd) {
|
|
3000
|
-
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
3001
|
-
try {
|
|
3002
|
-
const content = readFileSync4(join4(cwd, name), "utf-8").trim();
|
|
3003
|
-
if (content)
|
|
3004
|
-
return content;
|
|
3005
|
-
} catch {}
|
|
3006
|
-
}
|
|
3007
|
-
return null;
|
|
3008
4050
|
}
|
|
3009
4051
|
|
|
3010
4052
|
// src/query.ts
|
|
3011
|
-
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) {
|
|
3012
4057
|
const query = {
|
|
3013
4058
|
next: generator.next.bind(generator),
|
|
3014
4059
|
return: generator.return.bind(generator),
|
|
@@ -3019,6 +4064,71 @@ function createQuery(generator, abortController) {
|
|
|
3019
4064
|
async interrupt() {
|
|
3020
4065
|
abortController.abort();
|
|
3021
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
|
+
},
|
|
3022
4132
|
close() {
|
|
3023
4133
|
abortController.abort();
|
|
3024
4134
|
generator.return(undefined);
|
|
@@ -3040,7 +4150,10 @@ var HOOK_EVENTS = [
|
|
|
3040
4150
|
"SubagentStart",
|
|
3041
4151
|
"SubagentStop",
|
|
3042
4152
|
"PreCompact",
|
|
3043
|
-
"PermissionRequest"
|
|
4153
|
+
"PermissionRequest",
|
|
4154
|
+
"Setup",
|
|
4155
|
+
"TeammateIdle",
|
|
4156
|
+
"TaskCompleted"
|
|
3044
4157
|
];
|
|
3045
4158
|
var TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
|
|
3046
4159
|
|
|
@@ -3072,9 +4185,31 @@ class HookManager {
|
|
|
3072
4185
|
}
|
|
3073
4186
|
}
|
|
3074
4187
|
for (const callback of entry.hooks) {
|
|
3075
|
-
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;
|
|
3076
4196
|
if (result) {
|
|
3077
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
|
+
}
|
|
3078
4213
|
if (result.permissionDecision) {
|
|
3079
4214
|
if (!merged.permissionDecision || result.permissionDecision === "deny") {
|
|
3080
4215
|
merged.permissionDecision = result.permissionDecision;
|
|
@@ -3104,14 +4239,29 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
|
3104
4239
|
class McpClientManager {
|
|
3105
4240
|
configs;
|
|
3106
4241
|
servers = new Map;
|
|
4242
|
+
disabled = new Set;
|
|
3107
4243
|
constructor(configs) {
|
|
3108
|
-
this.configs = configs;
|
|
4244
|
+
this.configs = { ...configs };
|
|
3109
4245
|
}
|
|
3110
4246
|
async connectAll() {
|
|
3111
4247
|
const entries = Object.entries(this.configs);
|
|
3112
4248
|
await Promise.all(entries.map(([name, config]) => this.connectOne(name, config)));
|
|
3113
4249
|
}
|
|
3114
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
|
+
}
|
|
3115
4265
|
try {
|
|
3116
4266
|
const client = new Client({ name: `fourmis-${name}`, version: "1.0.0" });
|
|
3117
4267
|
if (config.type === "sdk") {
|
|
@@ -3145,13 +4295,25 @@ class McpClientManager {
|
|
|
3145
4295
|
const tools = (toolsResult.tools ?? []).map((t) => ({
|
|
3146
4296
|
name: t.name,
|
|
3147
4297
|
description: t.description,
|
|
3148
|
-
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
|
|
3149
4304
|
}));
|
|
3150
4305
|
this.servers.set(name, {
|
|
3151
4306
|
name,
|
|
3152
4307
|
client,
|
|
3153
4308
|
tools,
|
|
3154
|
-
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
|
+
}
|
|
3155
4317
|
});
|
|
3156
4318
|
} catch (err) {
|
|
3157
4319
|
const error = err instanceof Error ? err.message : String(err);
|
|
@@ -3159,10 +4321,22 @@ class McpClientManager {
|
|
|
3159
4321
|
name,
|
|
3160
4322
|
client: null,
|
|
3161
4323
|
tools: [],
|
|
3162
|
-
status: {
|
|
4324
|
+
status: {
|
|
4325
|
+
name,
|
|
4326
|
+
status: "failed",
|
|
4327
|
+
error,
|
|
4328
|
+
config: this.toStatusConfig(config),
|
|
4329
|
+
scope: config.type === "sdk" ? "sdk" : "session"
|
|
4330
|
+
}
|
|
3163
4331
|
});
|
|
3164
4332
|
}
|
|
3165
4333
|
}
|
|
4334
|
+
toStatusConfig(config) {
|
|
4335
|
+
if (config.type === "sdk") {
|
|
4336
|
+
return { type: "sdk", name: config.name };
|
|
4337
|
+
}
|
|
4338
|
+
return config;
|
|
4339
|
+
}
|
|
3166
4340
|
getTools() {
|
|
3167
4341
|
const result = [];
|
|
3168
4342
|
for (const [serverName, server] of this.servers) {
|
|
@@ -3202,64 +4376,294 @@ class McpClientManager {
|
|
|
3202
4376
|
return { content: `MCP tool error: ${message}`, isError: true };
|
|
3203
4377
|
}
|
|
3204
4378
|
}
|
|
3205
|
-
async listResources(serverName) {
|
|
3206
|
-
const result = [];
|
|
3207
|
-
const serversToQuery = serverName ? [this.servers.get(serverName)].filter(Boolean) : [...this.servers.values()];
|
|
3208
|
-
for (const server of serversToQuery) {
|
|
3209
|
-
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")
|
|
3210
4611
|
continue;
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
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)}`
|
|
3220
4654
|
});
|
|
3221
4655
|
}
|
|
3222
|
-
|
|
3223
|
-
}
|
|
3224
|
-
return result;
|
|
3225
|
-
}
|
|
3226
|
-
async readResource(serverName, uri) {
|
|
3227
|
-
const server = this.servers.get(serverName);
|
|
3228
|
-
if (!server || server.status.status !== "connected") {
|
|
3229
|
-
throw new Error(`MCP server "${serverName}" is not connected`);
|
|
3230
|
-
}
|
|
3231
|
-
const result = await server.client.readResource({ uri });
|
|
3232
|
-
const contents = result.contents ?? [];
|
|
3233
|
-
return contents.map((c) => {
|
|
3234
|
-
if ("text" in c)
|
|
3235
|
-
return c.text;
|
|
3236
|
-
if ("blob" in c)
|
|
3237
|
-
return `[binary data: ${c.mimeType ?? "unknown"}]`;
|
|
3238
|
-
return "";
|
|
3239
|
-
}).join("");
|
|
3240
|
-
}
|
|
3241
|
-
status() {
|
|
3242
|
-
const result = [];
|
|
3243
|
-
for (const [name] of Object.entries(this.configs)) {
|
|
3244
|
-
const server = this.servers.get(name);
|
|
3245
|
-
if (server) {
|
|
3246
|
-
result.push(server.status);
|
|
4656
|
+
content = normalizedBlocks;
|
|
3247
4657
|
} else {
|
|
3248
|
-
|
|
4658
|
+
continue;
|
|
3249
4659
|
}
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
async closeAll() {
|
|
3254
|
-
for (const [, server] of this.servers) {
|
|
3255
|
-
if (server.client) {
|
|
3256
|
-
try {
|
|
3257
|
-
await server.client.close();
|
|
3258
|
-
} catch {}
|
|
4660
|
+
messages.push({ role, content });
|
|
4661
|
+
if (resumeSessionAt && entry.uuid === resumeSessionAt) {
|
|
4662
|
+
reachedResumePoint = true;
|
|
3259
4663
|
}
|
|
3260
|
-
}
|
|
3261
|
-
this.servers.clear();
|
|
4664
|
+
} catch {}
|
|
3262
4665
|
}
|
|
4666
|
+
return messages;
|
|
3263
4667
|
}
|
|
3264
4668
|
|
|
3265
4669
|
// src/agents/tools.ts
|
|
@@ -3282,6 +4686,15 @@ function createTaskTool(ctx) {
|
|
|
3282
4686
|
type: "string",
|
|
3283
4687
|
description: "The type of agent to use. Must match a registered agent definition."
|
|
3284
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
|
+
},
|
|
3285
4698
|
run_in_background: {
|
|
3286
4699
|
type: "boolean",
|
|
3287
4700
|
description: "If true, run the task in the background and return a task ID."
|
|
@@ -3289,16 +4702,35 @@ function createTaskTool(ctx) {
|
|
|
3289
4702
|
max_turns: {
|
|
3290
4703
|
type: "number",
|
|
3291
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."
|
|
3292
4718
|
}
|
|
3293
4719
|
},
|
|
3294
|
-
required: ["prompt", "subagent_type"]
|
|
4720
|
+
required: ["description", "prompt", "subagent_type"]
|
|
3295
4721
|
},
|
|
3296
4722
|
async execute(input, toolCtx) {
|
|
3297
4723
|
const {
|
|
4724
|
+
description,
|
|
3298
4725
|
prompt,
|
|
3299
4726
|
subagent_type,
|
|
4727
|
+
model: requestedModel,
|
|
4728
|
+
resume,
|
|
3300
4729
|
run_in_background,
|
|
3301
|
-
max_turns
|
|
4730
|
+
max_turns,
|
|
4731
|
+
name,
|
|
4732
|
+
team_name,
|
|
4733
|
+
mode
|
|
3302
4734
|
} = input;
|
|
3303
4735
|
const agentDef = ctx.agents[subagent_type];
|
|
3304
4736
|
if (!agentDef) {
|
|
@@ -3312,28 +4744,53 @@ function createTaskTool(ctx) {
|
|
|
3312
4744
|
await ctx.parentHooks.fire("SubagentStart", { event: "SubagentStart", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
|
|
3313
4745
|
}
|
|
3314
4746
|
const provider = agentDef.provider ? getProvider(agentDef.provider) : ctx.parentProvider;
|
|
3315
|
-
const
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
}
|
|
3320
|
-
|
|
3321
|
-
}
|
|
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);
|
|
3322
4755
|
const maxTurns = max_turns ?? agentDef.maxTurns ?? 10;
|
|
3323
|
-
const sessionId = uuid();
|
|
4756
|
+
const sessionId = resume ?? uuid();
|
|
4757
|
+
const previousMessages = resume ? loadSessionMessages(ctx.parentCwd, resume) : undefined;
|
|
3324
4758
|
const abortController = new AbortController;
|
|
3325
4759
|
if (toolCtx.signal) {
|
|
3326
4760
|
toolCtx.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
3327
4761
|
}
|
|
3328
|
-
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(`
|
|
3329
4785
|
|
|
3330
|
-
|
|
4786
|
+
`);
|
|
3331
4787
|
const runAgent = async () => {
|
|
3332
4788
|
const messages = [];
|
|
3333
4789
|
let resultText = "";
|
|
3334
4790
|
for await (const msg of agentLoop(prompt, {
|
|
3335
4791
|
provider,
|
|
3336
4792
|
model,
|
|
4793
|
+
modelState: { current: model },
|
|
3337
4794
|
systemPrompt,
|
|
3338
4795
|
tools: subTools,
|
|
3339
4796
|
permissions: ctx.parentPermissions,
|
|
@@ -3341,18 +4798,23 @@ You are a subagent of type "${subagent_type}". ${agentDef.description}`;
|
|
|
3341
4798
|
sessionId,
|
|
3342
4799
|
maxTurns,
|
|
3343
4800
|
maxBudgetUsd: 5,
|
|
3344
|
-
|
|
4801
|
+
includePartialMessages: false,
|
|
3345
4802
|
signal: abortController.signal,
|
|
3346
4803
|
env: ctx.parentEnv,
|
|
3347
4804
|
debug: ctx.parentDebug,
|
|
3348
|
-
hooks: ctx.parentHooks
|
|
4805
|
+
hooks: ctx.parentHooks,
|
|
4806
|
+
previousMessages
|
|
3349
4807
|
})) {
|
|
3350
4808
|
messages.push(msg);
|
|
3351
|
-
if (msg.type === "
|
|
3352
|
-
|
|
4809
|
+
if (msg.type === "assistant") {
|
|
4810
|
+
for (const block of msg.message.content) {
|
|
4811
|
+
if (block.type === "text") {
|
|
4812
|
+
resultText += block.text;
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
3353
4815
|
}
|
|
3354
4816
|
if (msg.type === "result" && msg.subtype === "success") {
|
|
3355
|
-
resultText = msg.
|
|
4817
|
+
resultText = msg.result || resultText;
|
|
3356
4818
|
}
|
|
3357
4819
|
}
|
|
3358
4820
|
return resultText || "Subagent completed with no text output.";
|
|
@@ -3490,7 +4952,7 @@ class TaskManager {
|
|
|
3490
4952
|
if (!block) {
|
|
3491
4953
|
return `Task "${id}" is still running.`;
|
|
3492
4954
|
}
|
|
3493
|
-
const timeoutPromise = new Promise((
|
|
4955
|
+
const timeoutPromise = new Promise((resolve2) => setTimeout(resolve2, timeoutMs));
|
|
3494
4956
|
await Promise.race([task.promise, timeoutPromise]);
|
|
3495
4957
|
if (task.status === "running") {
|
|
3496
4958
|
return `Task "${id}" is still running (timed out after ${timeoutMs}ms).`;
|
|
@@ -3511,115 +4973,12 @@ class TaskManager {
|
|
|
3511
4973
|
}
|
|
3512
4974
|
}
|
|
3513
4975
|
|
|
3514
|
-
// src/utils/session-store.ts
|
|
3515
|
-
import { readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync3, readdirSync, statSync } from "fs";
|
|
3516
|
-
import { join as join5 } from "path";
|
|
3517
|
-
import { homedir as homedir4 } from "os";
|
|
3518
|
-
function sanitizeCwd(cwd) {
|
|
3519
|
-
return cwd.replace(/[/.]/g, "-");
|
|
3520
|
-
}
|
|
3521
|
-
function sessionsDir(cwd) {
|
|
3522
|
-
return join5(homedir4(), ".claude", "projects", sanitizeCwd(cwd));
|
|
3523
|
-
}
|
|
3524
|
-
function ensureDir(dir) {
|
|
3525
|
-
mkdirSync3(dir, { recursive: true });
|
|
3526
|
-
}
|
|
3527
|
-
function logMessage(dir, sessionId, entry) {
|
|
3528
|
-
ensureDir(dir);
|
|
3529
|
-
const filePath = join5(dir, `${sessionId}.jsonl`);
|
|
3530
|
-
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
3531
|
-
`);
|
|
3532
|
-
}
|
|
3533
|
-
function createSessionLogger(cwd, sessionId, model) {
|
|
3534
|
-
const dir = sessionsDir(cwd);
|
|
3535
|
-
let lastUuid = null;
|
|
3536
|
-
return (role, content, parentUuid) => {
|
|
3537
|
-
const entryUuid = uuid();
|
|
3538
|
-
let normalizedContent = content;
|
|
3539
|
-
if (role === "user" && typeof content === "string") {
|
|
3540
|
-
normalizedContent = [{ type: "text", text: content }];
|
|
3541
|
-
}
|
|
3542
|
-
const entry = {
|
|
3543
|
-
type: role,
|
|
3544
|
-
uuid: entryUuid,
|
|
3545
|
-
parentUuid: parentUuid ?? lastUuid,
|
|
3546
|
-
sessionId,
|
|
3547
|
-
timestamp: new Date().toISOString(),
|
|
3548
|
-
cwd,
|
|
3549
|
-
isSidechain: false,
|
|
3550
|
-
userType: "external",
|
|
3551
|
-
message: {
|
|
3552
|
-
role,
|
|
3553
|
-
content: normalizedContent,
|
|
3554
|
-
...role === "assistant" && model ? { model } : {}
|
|
3555
|
-
},
|
|
3556
|
-
...role === "user" ? { permissionMode: "default" } : {}
|
|
3557
|
-
};
|
|
3558
|
-
logMessage(dir, sessionId, entry);
|
|
3559
|
-
lastUuid = entryUuid;
|
|
3560
|
-
return entryUuid;
|
|
3561
|
-
};
|
|
3562
|
-
}
|
|
3563
|
-
function findLatestSession(cwd) {
|
|
3564
|
-
const dir = sessionsDir(cwd);
|
|
3565
|
-
try {
|
|
3566
|
-
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
|
|
3567
|
-
const filePath = join5(dir, f);
|
|
3568
|
-
try {
|
|
3569
|
-
return { name: f, mtime: statSync(filePath).mtimeMs };
|
|
3570
|
-
} catch {
|
|
3571
|
-
return null;
|
|
3572
|
-
}
|
|
3573
|
-
}).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
|
|
3574
|
-
if (files.length === 0)
|
|
3575
|
-
return null;
|
|
3576
|
-
return files[0].name.replace(/\.jsonl$/, "");
|
|
3577
|
-
} catch {
|
|
3578
|
-
return null;
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
function loadSessionMessages(cwd, sessionId) {
|
|
3582
|
-
const dir = sessionsDir(cwd);
|
|
3583
|
-
const filePath = join5(dir, `${sessionId}.jsonl`);
|
|
3584
|
-
let lines;
|
|
3585
|
-
try {
|
|
3586
|
-
lines = readFileSync5(filePath, "utf-8").trim().split(`
|
|
3587
|
-
`).filter(Boolean);
|
|
3588
|
-
} catch {
|
|
3589
|
-
return [];
|
|
3590
|
-
}
|
|
3591
|
-
const messages = [];
|
|
3592
|
-
for (const line of lines) {
|
|
3593
|
-
try {
|
|
3594
|
-
const entry = JSON.parse(line);
|
|
3595
|
-
if (entry.type !== "user" && entry.type !== "assistant")
|
|
3596
|
-
continue;
|
|
3597
|
-
if (entry.isMeta === true)
|
|
3598
|
-
continue;
|
|
3599
|
-
const message = entry.message;
|
|
3600
|
-
if (!message)
|
|
3601
|
-
continue;
|
|
3602
|
-
const role = entry.type === "user" ? "user" : "assistant";
|
|
3603
|
-
let content;
|
|
3604
|
-
if (typeof message.content === "string") {
|
|
3605
|
-
content = message.content;
|
|
3606
|
-
} else if (Array.isArray(message.content)) {
|
|
3607
|
-
content = message.content.filter((c) => c.type === "text" || c.type === "tool_use" || c.type === "tool_result").map((c) => c);
|
|
3608
|
-
} else {
|
|
3609
|
-
continue;
|
|
3610
|
-
}
|
|
3611
|
-
messages.push({ role, content });
|
|
3612
|
-
} catch {}
|
|
3613
|
-
}
|
|
3614
|
-
return messages;
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
4976
|
// src/memory/memory-handler.ts
|
|
3618
|
-
import { readdir, stat, readFile, writeFile, rm, rename, mkdir as
|
|
3619
|
-
import { join as
|
|
3620
|
-
import { existsSync 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";
|
|
4979
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3621
4980
|
function createMemoryHandler(memoryDir) {
|
|
3622
|
-
const absMemoryDir =
|
|
4981
|
+
const absMemoryDir = resolve2(memoryDir);
|
|
3623
4982
|
function resolvePath2(logicalPath) {
|
|
3624
4983
|
let cleaned = logicalPath;
|
|
3625
4984
|
if (cleaned.startsWith("/memories")) {
|
|
@@ -3631,15 +4990,15 @@ function createMemoryHandler(memoryDir) {
|
|
|
3631
4990
|
if (cleaned.includes("..") || cleaned.includes("%2e") || cleaned.includes("%2E")) {
|
|
3632
4991
|
throw new Error(`Path traversal detected: ${logicalPath}`);
|
|
3633
4992
|
}
|
|
3634
|
-
const absPath = cleaned === "" ? absMemoryDir :
|
|
3635
|
-
const rel =
|
|
3636
|
-
if (rel.startsWith("..") ||
|
|
4993
|
+
const absPath = cleaned === "" ? absMemoryDir : resolve2(absMemoryDir, cleaned);
|
|
4994
|
+
const rel = relative2(absMemoryDir, absPath);
|
|
4995
|
+
if (rel.startsWith("..") || resolve2(absPath) !== absPath && !absPath.startsWith(absMemoryDir)) {
|
|
3637
4996
|
throw new Error(`Path traversal detected: ${logicalPath}`);
|
|
3638
4997
|
}
|
|
3639
4998
|
return absPath;
|
|
3640
4999
|
}
|
|
3641
5000
|
function toLogicalPath(absPath) {
|
|
3642
|
-
const rel =
|
|
5001
|
+
const rel = relative2(absMemoryDir, absPath);
|
|
3643
5002
|
return rel === "" ? "/memories" : `/memories/${rel}`;
|
|
3644
5003
|
}
|
|
3645
5004
|
function formatSize(bytes) {
|
|
@@ -3662,7 +5021,7 @@ function createMemoryHandler(memoryDir) {
|
|
|
3662
5021
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3663
5022
|
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
3664
5023
|
continue;
|
|
3665
|
-
const entryPath =
|
|
5024
|
+
const entryPath = join9(dirPath, entry.name);
|
|
3666
5025
|
const entryStat = await stat(entryPath);
|
|
3667
5026
|
lines.push(`${formatSize(entryStat.size)} ${toLogicalPath(entryPath)}`);
|
|
3668
5027
|
if (entry.isDirectory()) {
|
|
@@ -3695,7 +5054,7 @@ function createMemoryHandler(memoryDir) {
|
|
|
3695
5054
|
}
|
|
3696
5055
|
async function handleView(cmd) {
|
|
3697
5056
|
const absPath = resolvePath2(cmd.path);
|
|
3698
|
-
if (!
|
|
5057
|
+
if (!existsSync5(absPath)) {
|
|
3699
5058
|
return `The path ${cmd.path} does not exist. Please provide a valid path.`;
|
|
3700
5059
|
}
|
|
3701
5060
|
const s = await stat(absPath);
|
|
@@ -3705,31 +5064,31 @@ function createMemoryHandler(memoryDir) {
|
|
|
3705
5064
|
${lines.join(`
|
|
3706
5065
|
`)}`;
|
|
3707
5066
|
}
|
|
3708
|
-
const content = await
|
|
5067
|
+
const content = await readFile3(absPath, "utf-8");
|
|
3709
5068
|
const formatted = formatFileContent(content, cmd.view_range);
|
|
3710
5069
|
return `Here's the content of ${cmd.path} with line numbers:
|
|
3711
5070
|
${formatted}`;
|
|
3712
5071
|
}
|
|
3713
5072
|
async function handleCreate(cmd) {
|
|
3714
5073
|
const absPath = resolvePath2(cmd.path);
|
|
3715
|
-
if (
|
|
5074
|
+
if (existsSync5(absPath)) {
|
|
3716
5075
|
return `Error: File ${cmd.path} already exists`;
|
|
3717
5076
|
}
|
|
3718
|
-
const parentDir =
|
|
3719
|
-
await
|
|
3720
|
-
await
|
|
5077
|
+
const parentDir = resolve2(absPath, "..");
|
|
5078
|
+
await mkdir4(parentDir, { recursive: true });
|
|
5079
|
+
await writeFile4(absPath, cmd.file_text, "utf-8");
|
|
3721
5080
|
return `File created successfully at: ${cmd.path}`;
|
|
3722
5081
|
}
|
|
3723
5082
|
async function handleStrReplace(cmd) {
|
|
3724
5083
|
const absPath = resolvePath2(cmd.path);
|
|
3725
|
-
if (!
|
|
5084
|
+
if (!existsSync5(absPath)) {
|
|
3726
5085
|
return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
|
|
3727
5086
|
}
|
|
3728
5087
|
const s = await stat(absPath);
|
|
3729
5088
|
if (s.isDirectory()) {
|
|
3730
5089
|
return `Error: The path ${cmd.path} does not exist. Please provide a valid path.`;
|
|
3731
5090
|
}
|
|
3732
|
-
const content = await
|
|
5091
|
+
const content = await readFile3(absPath, "utf-8");
|
|
3733
5092
|
const lines = content.split(`
|
|
3734
5093
|
`);
|
|
3735
5094
|
const matchingLines = [];
|
|
@@ -3752,7 +5111,7 @@ ${formatted}`;
|
|
|
3752
5111
|
return `No replacement was performed. Multiple occurrences of old_str \`${cmd.old_str}\` in lines: ${matchingLines.join(", ")}. Please ensure it is unique`;
|
|
3753
5112
|
}
|
|
3754
5113
|
const newContent = content.replace(cmd.old_str, cmd.new_str);
|
|
3755
|
-
await
|
|
5114
|
+
await writeFile4(absPath, newContent, "utf-8");
|
|
3756
5115
|
const newLines = newContent.split(`
|
|
3757
5116
|
`);
|
|
3758
5117
|
const replaceLine = matchingLines[0];
|
|
@@ -3765,14 +5124,14 @@ ${snippet}`;
|
|
|
3765
5124
|
}
|
|
3766
5125
|
async function handleInsert(cmd) {
|
|
3767
5126
|
const absPath = resolvePath2(cmd.path);
|
|
3768
|
-
if (!
|
|
5127
|
+
if (!existsSync5(absPath)) {
|
|
3769
5128
|
return `Error: The path ${cmd.path} does not exist`;
|
|
3770
5129
|
}
|
|
3771
5130
|
const s = await stat(absPath);
|
|
3772
5131
|
if (s.isDirectory()) {
|
|
3773
5132
|
return `Error: The path ${cmd.path} does not exist`;
|
|
3774
5133
|
}
|
|
3775
|
-
const content = await
|
|
5134
|
+
const content = await readFile3(absPath, "utf-8");
|
|
3776
5135
|
const lines = content.split(`
|
|
3777
5136
|
`);
|
|
3778
5137
|
if (cmd.insert_line < 0 || cmd.insert_line > lines.length) {
|
|
@@ -3781,13 +5140,13 @@ ${snippet}`;
|
|
|
3781
5140
|
const insertLines = cmd.insert_text.split(`
|
|
3782
5141
|
`);
|
|
3783
5142
|
lines.splice(cmd.insert_line, 0, ...insertLines);
|
|
3784
|
-
await
|
|
5143
|
+
await writeFile4(absPath, lines.join(`
|
|
3785
5144
|
`), "utf-8");
|
|
3786
5145
|
return `The file ${cmd.path} has been edited.`;
|
|
3787
5146
|
}
|
|
3788
5147
|
async function handleDelete(cmd) {
|
|
3789
5148
|
const absPath = resolvePath2(cmd.path);
|
|
3790
|
-
if (!
|
|
5149
|
+
if (!existsSync5(absPath)) {
|
|
3791
5150
|
return `Error: The path ${cmd.path} does not exist`;
|
|
3792
5151
|
}
|
|
3793
5152
|
await rm(absPath, { recursive: true, force: true });
|
|
@@ -3796,20 +5155,20 @@ ${snippet}`;
|
|
|
3796
5155
|
async function handleRename(cmd) {
|
|
3797
5156
|
const oldAbs = resolvePath2(cmd.old_path);
|
|
3798
5157
|
const newAbs = resolvePath2(cmd.new_path);
|
|
3799
|
-
if (!
|
|
5158
|
+
if (!existsSync5(oldAbs)) {
|
|
3800
5159
|
return `Error: The path ${cmd.old_path} does not exist`;
|
|
3801
5160
|
}
|
|
3802
|
-
if (
|
|
5161
|
+
if (existsSync5(newAbs)) {
|
|
3803
5162
|
return `Error: The destination ${cmd.new_path} already exists`;
|
|
3804
5163
|
}
|
|
3805
|
-
const parentDir =
|
|
3806
|
-
await
|
|
5164
|
+
const parentDir = resolve2(newAbs, "..");
|
|
5165
|
+
await mkdir4(parentDir, { recursive: true });
|
|
3807
5166
|
await rename(oldAbs, newAbs);
|
|
3808
5167
|
return `Successfully renamed ${cmd.old_path} to ${cmd.new_path}`;
|
|
3809
5168
|
}
|
|
3810
5169
|
async function execute(cmd) {
|
|
3811
|
-
if (!
|
|
3812
|
-
await
|
|
5170
|
+
if (!existsSync5(absMemoryDir)) {
|
|
5171
|
+
await mkdir4(absMemoryDir, { recursive: true });
|
|
3813
5172
|
}
|
|
3814
5173
|
switch (cmd.command) {
|
|
3815
5174
|
case "view":
|
|
@@ -3914,26 +5273,73 @@ function createMemoryTool(config) {
|
|
|
3914
5273
|
}
|
|
3915
5274
|
};
|
|
3916
5275
|
}
|
|
3917
|
-
|
|
3918
5276
|
// src/api.ts
|
|
3919
5277
|
var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
|
|
3920
5278
|
var DEFAULT_MAX_TURNS = 10;
|
|
3921
5279
|
var DEFAULT_MAX_BUDGET_USD = 5;
|
|
3922
5280
|
function query(params) {
|
|
3923
|
-
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
|
+
}
|
|
3924
5286
|
const providerName = options.provider ?? "anthropic";
|
|
3925
5287
|
const provider = getProvider(providerName, {
|
|
3926
5288
|
apiKey: options.apiKey,
|
|
3927
5289
|
baseUrl: options.baseUrl
|
|
3928
5290
|
});
|
|
3929
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
|
+
}
|
|
3930
5311
|
const toolNames = resolveToolNames(options.tools);
|
|
3931
5312
|
const registry = buildToolRegistry(toolNames, undefined, options.disallowedTools);
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
5313
|
+
let skills = [];
|
|
5314
|
+
{
|
|
5315
|
+
const skillsResult = loadSkills({
|
|
5316
|
+
cwd: options.cwd,
|
|
5317
|
+
skillPaths: options.skillPaths,
|
|
5318
|
+
includeDefaults: options.includeDefaultSkills !== false
|
|
5319
|
+
});
|
|
5320
|
+
skills = skillsResult.skills;
|
|
5321
|
+
if (options.debug && skillsResult.diagnostics.length > 0) {
|
|
5322
|
+
for (const d of skillsResult.diagnostics) {
|
|
5323
|
+
console.warn(`[skills] ${d.type}: ${d.message} (${d.path})`);
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
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
|
+
})();
|
|
3937
5343
|
let settingsManager;
|
|
3938
5344
|
let mergedPermissions = options.permissions;
|
|
3939
5345
|
if (options.settingSources && options.settingSources.length > 0) {
|
|
@@ -3979,7 +5385,7 @@ function query(params) {
|
|
|
3979
5385
|
}
|
|
3980
5386
|
}
|
|
3981
5387
|
} else if (options.resume) {
|
|
3982
|
-
previousMessages = loadSessionMessages(cwd, options.resume);
|
|
5388
|
+
previousMessages = loadSessionMessages(cwd, options.resume, options.resumeSessionAt);
|
|
3983
5389
|
if (options.forkSession) {
|
|
3984
5390
|
sessionId = uuid();
|
|
3985
5391
|
} else {
|
|
@@ -3988,18 +5394,28 @@ function query(params) {
|
|
|
3988
5394
|
}
|
|
3989
5395
|
const persistSession = options.persistSession !== false;
|
|
3990
5396
|
const sessionLogger = persistSession ? createSessionLogger(cwd, sessionId, model) : undefined;
|
|
3991
|
-
const abortController = new AbortController;
|
|
5397
|
+
const abortController = options.abortController ?? new AbortController;
|
|
3992
5398
|
if (options.signal) {
|
|
3993
5399
|
options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
3994
5400
|
}
|
|
3995
5401
|
const hookManager = options.hooks ? new HookManager(options.hooks) : undefined;
|
|
3996
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
|
+
};
|
|
3997
5413
|
if (options.agents && Object.keys(options.agents).length > 0) {
|
|
3998
5414
|
const taskManager = new TaskManager;
|
|
3999
5415
|
const agentCtx = {
|
|
4000
5416
|
agents: options.agents,
|
|
4001
5417
|
parentProvider: provider,
|
|
4002
|
-
parentModel:
|
|
5418
|
+
parentModel: modelState.current,
|
|
4003
5419
|
parentPermissions: permissions,
|
|
4004
5420
|
parentHooks: hookManager,
|
|
4005
5421
|
parentCwd: cwd,
|
|
@@ -4023,6 +5439,12 @@ function query(params) {
|
|
|
4023
5439
|
const generator = agentLoop(prompt, {
|
|
4024
5440
|
provider,
|
|
4025
5441
|
model,
|
|
5442
|
+
modelState,
|
|
5443
|
+
maxThinkingTokensState,
|
|
5444
|
+
fallbackModel,
|
|
5445
|
+
thinking: options.thinking,
|
|
5446
|
+
effort: options.effort,
|
|
5447
|
+
outputFormat: options.outputFormat,
|
|
4026
5448
|
systemPrompt,
|
|
4027
5449
|
tools: registry,
|
|
4028
5450
|
permissions,
|
|
@@ -4030,7 +5452,7 @@ function query(params) {
|
|
|
4030
5452
|
sessionId,
|
|
4031
5453
|
maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
4032
5454
|
maxBudgetUsd: options.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
|
|
4033
|
-
|
|
5455
|
+
includePartialMessages: options.includePartialMessages ?? false,
|
|
4034
5456
|
signal: abortController.signal,
|
|
4035
5457
|
env: options.env,
|
|
4036
5458
|
debug: options.debug,
|
|
@@ -4038,9 +5460,95 @@ function query(params) {
|
|
|
4038
5460
|
mcpClient,
|
|
4039
5461
|
previousMessages,
|
|
4040
5462
|
sessionLogger,
|
|
4041
|
-
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
|
+
}
|
|
4042
5472
|
});
|
|
4043
|
-
|
|
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);
|
|
4044
5552
|
}
|
|
4045
5553
|
export {
|
|
4046
5554
|
query
|