fourmis-agents-sdk 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -198
- package/dist/agent-loop.js +0 -15
- package/dist/agents/index.js +2557 -2543
- package/dist/agents/task-manager.js +0 -15
- package/dist/agents/tools.js +2557 -2543
- package/dist/api.js +1246 -1230
- package/dist/auth/gemini-oauth.js +0 -15
- package/dist/auth/login-openai.js +0 -15
- package/dist/auth/openai-oauth.js +0 -15
- package/dist/hooks.js +0 -15
- package/dist/index.js +1246 -1230
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +3 -16
- package/dist/mcp/index.js +3 -16
- package/dist/mcp/server.js +0 -15
- package/dist/mcp/types.d.ts +2 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/memory/index.js +0 -15
- package/dist/memory/memory-handler.js +0 -15
- package/dist/permissions.js +0 -15
- package/dist/providers/anthropic.js +0 -15
- package/dist/providers/gemini.js +0 -15
- package/dist/providers/openai.d.ts +6 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +36 -21
- package/dist/providers/registry.js +35 -21
- package/dist/query.js +0 -15
- package/dist/settings.js +0 -15
- package/dist/skills/frontmatter.js +0 -15
- package/dist/skills/index.js +0 -15
- package/dist/skills/skills.js +0 -15
- package/dist/tools/ask-user-question.js +0 -15
- package/dist/tools/bash.js +0 -15
- package/dist/tools/config.js +0 -15
- package/dist/tools/edit.js +0 -15
- package/dist/tools/exit-plan-mode.js +0 -15
- package/dist/tools/glob.js +0 -15
- package/dist/tools/grep.js +0 -15
- package/dist/tools/index.js +0 -15
- package/dist/tools/mcp-resources.js +0 -15
- package/dist/tools/notebook-edit.js +0 -15
- package/dist/tools/presets.js +0 -15
- package/dist/tools/read.js +0 -15
- package/dist/tools/registry.js +0 -15
- package/dist/tools/todo-write.js +0 -15
- package/dist/tools/web-fetch.js +0 -15
- package/dist/tools/web-search.js +0 -15
- package/dist/tools/write.js +0 -15
- package/dist/types.js +0 -15
- package/dist/utils/cost.js +0 -15
- package/dist/utils/session-store.js +0 -15
- package/dist/utils/system-prompt.js +0 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
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
|
-
};
|
|
18
3
|
var __export = (target, all) => {
|
|
19
4
|
for (var name in all)
|
|
20
5
|
__defProp(target, name, {
|
|
@@ -27,6 +12,76 @@ var __export = (target, all) => {
|
|
|
27
12
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
28
13
|
var __require = import.meta.require;
|
|
29
14
|
|
|
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
|
+
|
|
30
85
|
// src/auth/openai-oauth.ts
|
|
31
86
|
var exports_openai_oauth = {};
|
|
32
87
|
__export(exports_openai_oauth, {
|
|
@@ -40,25 +95,25 @@ __export(exports_openai_oauth, {
|
|
|
40
95
|
decodeJwtPayload: () => decodeJwtPayload
|
|
41
96
|
});
|
|
42
97
|
import { randomBytes, createHash } from "crypto";
|
|
43
|
-
import { readFileSync
|
|
44
|
-
import { join
|
|
45
|
-
import { homedir
|
|
98
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
99
|
+
import { join } from "path";
|
|
100
|
+
import { homedir } from "os";
|
|
46
101
|
function getHome() {
|
|
47
|
-
return process.env.HOME ??
|
|
102
|
+
return process.env.HOME ?? homedir();
|
|
48
103
|
}
|
|
49
104
|
function tokenDir() {
|
|
50
|
-
return
|
|
105
|
+
return join(getHome(), ".fourmis");
|
|
51
106
|
}
|
|
52
107
|
function tokenPath() {
|
|
53
|
-
return
|
|
108
|
+
return join(tokenDir(), "openai-auth.json");
|
|
54
109
|
}
|
|
55
110
|
function codexFallbackPath() {
|
|
56
|
-
return
|
|
111
|
+
return join(getHome(), ".codex", "auth.json");
|
|
57
112
|
}
|
|
58
113
|
function loadTokens() {
|
|
59
114
|
for (const p of [tokenPath(), codexFallbackPath()]) {
|
|
60
115
|
try {
|
|
61
|
-
const raw =
|
|
116
|
+
const raw = readFileSync(p, "utf-8");
|
|
62
117
|
const data = JSON.parse(raw);
|
|
63
118
|
if (data.access_token && data.account_id) {
|
|
64
119
|
return data;
|
|
@@ -69,10 +124,10 @@ function loadTokens() {
|
|
|
69
124
|
}
|
|
70
125
|
function saveTokens(tokens) {
|
|
71
126
|
const dir = tokenDir();
|
|
72
|
-
if (!
|
|
73
|
-
|
|
127
|
+
if (!existsSync(dir)) {
|
|
128
|
+
mkdirSync(dir, { recursive: true });
|
|
74
129
|
}
|
|
75
|
-
|
|
130
|
+
writeFileSync(tokenPath(), JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
76
131
|
}
|
|
77
132
|
function generateCodeVerifier() {
|
|
78
133
|
return randomBytes(64).toString("base64url");
|
|
@@ -322,19 +377,19 @@ __export(exports_gemini_oauth, {
|
|
|
322
377
|
isLoggedIn: () => isLoggedIn2,
|
|
323
378
|
getValidToken: () => getValidToken2
|
|
324
379
|
});
|
|
325
|
-
import { readFileSync as
|
|
326
|
-
import { join as
|
|
327
|
-
import { homedir as
|
|
380
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
381
|
+
import { join as join2 } from "path";
|
|
382
|
+
import { homedir as homedir2 } from "os";
|
|
328
383
|
function getHome2() {
|
|
329
|
-
return process.env.HOME ??
|
|
384
|
+
return process.env.HOME ?? homedir2();
|
|
330
385
|
}
|
|
331
386
|
function tokenPath2() {
|
|
332
|
-
return
|
|
387
|
+
return join2(getHome2(), ".gemini", "oauth_creds.json");
|
|
333
388
|
}
|
|
334
389
|
function loadTokens2() {
|
|
335
390
|
const p = tokenPath2();
|
|
336
391
|
try {
|
|
337
|
-
const raw =
|
|
392
|
+
const raw = readFileSync2(p, "utf-8");
|
|
338
393
|
const data = JSON.parse(raw);
|
|
339
394
|
if (data.access_token && data.refresh_token) {
|
|
340
395
|
return data;
|
|
@@ -347,12 +402,12 @@ function loadTokensSync2() {
|
|
|
347
402
|
}
|
|
348
403
|
function saveTokens2(tokens) {
|
|
349
404
|
const p = tokenPath2();
|
|
350
|
-
const dir =
|
|
351
|
-
if (!
|
|
352
|
-
const { mkdirSync:
|
|
353
|
-
|
|
405
|
+
const dir = join2(getHome2(), ".gemini");
|
|
406
|
+
if (!existsSync2(dir)) {
|
|
407
|
+
const { mkdirSync: mkdirSync2 } = __require("fs");
|
|
408
|
+
mkdirSync2(dir, { recursive: true });
|
|
354
409
|
}
|
|
355
|
-
|
|
410
|
+
writeFileSync2(p, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
356
411
|
}
|
|
357
412
|
async function refreshAccessToken2(refreshToken) {
|
|
358
413
|
const res = await fetch(GOOGLE_TOKEN_URL, {
|
|
@@ -406,367 +461,520 @@ var init_gemini_oauth = __esm(() => {
|
|
|
406
461
|
GEMINI_CLIENT_SECRET = process.env.GEMINI_OAUTH_CLIENT_SECRET ?? ["GOCSPX", "4uHgMPm", "1o7Sk", "geV6Cu5clXFsxl"].join("-");
|
|
407
462
|
});
|
|
408
463
|
|
|
409
|
-
// src/
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
});
|
|
415
|
-
function createListMcpResourcesTool(mcpClient) {
|
|
464
|
+
// src/types.ts
|
|
465
|
+
function uuid() {
|
|
466
|
+
return crypto.randomUUID();
|
|
467
|
+
}
|
|
468
|
+
function emptyTokenUsage() {
|
|
416
469
|
return {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
}
|
|
470
|
+
inputTokens: 0,
|
|
471
|
+
outputTokens: 0,
|
|
472
|
+
cacheReadInputTokens: 0,
|
|
473
|
+
cacheCreationInputTokens: 0
|
|
443
474
|
};
|
|
444
475
|
}
|
|
445
|
-
function
|
|
476
|
+
function mergeUsage(a, b) {
|
|
446
477
|
return {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
}
|
|
478
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
479
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
480
|
+
cacheReadInputTokens: a.cacheReadInputTokens + b.cacheReadInputTokens,
|
|
481
|
+
cacheCreationInputTokens: a.cacheCreationInputTokens + b.cacheCreationInputTokens
|
|
476
482
|
};
|
|
477
483
|
}
|
|
478
484
|
|
|
479
|
-
// src/
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
485
|
+
// src/agent-loop.ts
|
|
486
|
+
function makeModelUsageEntry() {
|
|
487
|
+
return {
|
|
488
|
+
inputTokens: 0,
|
|
489
|
+
outputTokens: 0,
|
|
490
|
+
cacheReadInputTokens: 0,
|
|
491
|
+
cacheCreationInputTokens: 0,
|
|
492
|
+
totalCostUsd: 0
|
|
493
|
+
};
|
|
488
494
|
}
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
495
|
+
function makeErrorResult(params) {
|
|
496
|
+
return {
|
|
497
|
+
type: "result",
|
|
498
|
+
subtype: params.subtype,
|
|
499
|
+
duration_ms: Date.now() - params.startTime,
|
|
500
|
+
duration_api_ms: params.apiTimeMs,
|
|
501
|
+
is_error: true,
|
|
502
|
+
num_turns: params.turns,
|
|
503
|
+
stop_reason: null,
|
|
504
|
+
total_cost_usd: params.costUsd,
|
|
505
|
+
usage: params.usage,
|
|
506
|
+
modelUsage: params.modelUsage,
|
|
507
|
+
permission_denials: params.permissionDenials,
|
|
508
|
+
errors: params.errors,
|
|
509
|
+
uuid: uuid(),
|
|
510
|
+
session_id: params.sessionId
|
|
511
|
+
};
|
|
500
512
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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;
|
|
513
|
+
function extractStructuredJson(text) {
|
|
514
|
+
const trimmed = text.trim();
|
|
515
|
+
if (!trimmed) {
|
|
516
|
+
return { ok: false, error: "Empty result text; expected JSON output." };
|
|
514
517
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
message: `Tool "${toolName}" is denied by permissions config.`
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
if (this.mode === "plan") {
|
|
526
|
-
if (!SAFE_TOOLS.has(toolName)) {
|
|
527
|
-
return {
|
|
528
|
-
behavior: "deny",
|
|
529
|
-
message: `Tool "${toolName}" is not allowed in plan mode. Only read-only tools are available.`
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
return { behavior: "allow" };
|
|
533
|
-
}
|
|
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" };
|
|
542
|
-
}
|
|
543
|
-
if (matchesRule(this.allowRules, toolName, input)) {
|
|
544
|
-
return { behavior: "allow" };
|
|
545
|
-
}
|
|
546
|
-
if (SAFE_TOOLS.has(toolName)) {
|
|
547
|
-
return { behavior: "allow" };
|
|
548
|
-
}
|
|
549
|
-
if (this.mode === "acceptEdits") {
|
|
550
|
-
if (EDIT_TOOLS.has(toolName)) {
|
|
551
|
-
return { behavior: "allow" };
|
|
552
|
-
}
|
|
553
|
-
if (toolName === "Bash") {
|
|
554
|
-
const cmd = String(input?.command ?? "").trimStart();
|
|
555
|
-
if (FS_COMMANDS.some((fc) => cmd.startsWith(fc + " ") || cmd === fc)) {
|
|
556
|
-
return { behavior: "allow" };
|
|
557
|
-
}
|
|
558
|
-
}
|
|
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.`
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
return { behavior: "allow" };
|
|
518
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
519
|
+
const candidate = fenced ? fenced[1].trim() : trimmed;
|
|
520
|
+
try {
|
|
521
|
+
return { ok: true, value: JSON.parse(candidate) };
|
|
522
|
+
} catch (err) {
|
|
523
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
524
|
+
return { ok: false, error: `Invalid JSON output: ${message}` };
|
|
578
525
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
526
|
+
}
|
|
527
|
+
async function* agentLoop(prompt, options) {
|
|
528
|
+
const {
|
|
529
|
+
provider,
|
|
530
|
+
model,
|
|
531
|
+
fallbackModel,
|
|
532
|
+
modelState,
|
|
533
|
+
maxThinkingTokensState,
|
|
534
|
+
thinking,
|
|
535
|
+
effort,
|
|
536
|
+
outputFormat,
|
|
537
|
+
systemPrompt,
|
|
538
|
+
tools,
|
|
539
|
+
permissions,
|
|
540
|
+
cwd,
|
|
541
|
+
sessionId,
|
|
542
|
+
maxTurns,
|
|
543
|
+
maxBudgetUsd,
|
|
544
|
+
includePartialMessages,
|
|
545
|
+
signal,
|
|
546
|
+
env,
|
|
547
|
+
debug,
|
|
548
|
+
hooks,
|
|
549
|
+
mcpClient,
|
|
550
|
+
previousMessages,
|
|
551
|
+
sessionLogger,
|
|
552
|
+
nativeMemoryTool,
|
|
553
|
+
initMeta
|
|
554
|
+
} = options;
|
|
555
|
+
const effectiveModelState = modelState ?? { current: model };
|
|
556
|
+
const startTime = Date.now();
|
|
557
|
+
let apiTimeMs = 0;
|
|
558
|
+
let turns = 0;
|
|
559
|
+
let totalUsage = emptyTokenUsage();
|
|
560
|
+
let costUsd = 0;
|
|
561
|
+
const modelUsage = {};
|
|
562
|
+
const permissionDenials = [];
|
|
563
|
+
if (mcpClient) {
|
|
564
|
+
await mcpClient.connectAll();
|
|
565
|
+
for (const tool of mcpClient.getTools()) {
|
|
566
|
+
tools.register(tool);
|
|
614
567
|
}
|
|
568
|
+
const { createListMcpResourcesTool: createListMcpResourcesTool2, createReadMcpResourceTool: createReadMcpResourceTool2 } = await Promise.resolve().then(() => exports_mcp_resources);
|
|
569
|
+
tools.register(createListMcpResourcesTool2(mcpClient));
|
|
570
|
+
tools.register(createReadMcpResourceTool2(mcpClient));
|
|
615
571
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
572
|
+
const messages = [
|
|
573
|
+
...previousMessages ?? [],
|
|
574
|
+
{ role: "user", content: prompt }
|
|
575
|
+
];
|
|
576
|
+
if (sessionLogger) {
|
|
577
|
+
sessionLogger("user", prompt, null);
|
|
621
578
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
579
|
+
yield {
|
|
580
|
+
type: "system",
|
|
581
|
+
subtype: "init",
|
|
582
|
+
apiKeySource: "user",
|
|
583
|
+
claude_code_version: "fourmis-agent-sdk",
|
|
584
|
+
session_id: sessionId,
|
|
585
|
+
model: effectiveModelState.current,
|
|
586
|
+
tools: tools.list(),
|
|
587
|
+
cwd,
|
|
588
|
+
mcp_servers: (mcpClient?.status() ?? []).map((s) => ({ name: s.name, status: s.status })),
|
|
589
|
+
permissionMode: permissions.getMode(),
|
|
590
|
+
agents: initMeta?.agents,
|
|
591
|
+
betas: initMeta?.betas,
|
|
592
|
+
slash_commands: initMeta?.slashCommands ?? [],
|
|
593
|
+
output_style: initMeta?.outputStyle ?? "default",
|
|
594
|
+
skills: initMeta?.skills ?? [],
|
|
595
|
+
plugins: (initMeta?.plugins ?? []).map((p) => ({ name: p.path.split("/").pop() ?? p.path, path: p.path })),
|
|
596
|
+
uuid: uuid()
|
|
597
|
+
};
|
|
598
|
+
if (hooks) {
|
|
599
|
+
await hooks.fire("Setup", {
|
|
600
|
+
event: "Setup",
|
|
601
|
+
hook_event_name: "Setup",
|
|
602
|
+
trigger: "init",
|
|
603
|
+
session_id: sessionId,
|
|
604
|
+
cwd,
|
|
605
|
+
permission_mode: permissions.getMode()
|
|
606
|
+
}, undefined, { signal });
|
|
633
607
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
for (const rule of perms.allow) {
|
|
645
|
-
if (typeof rule === "string") {
|
|
646
|
-
allAllow.push(this.parseRule(rule));
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
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
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
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;
|
|
608
|
+
if (hooks) {
|
|
609
|
+
await hooks.fire("SessionStart", {
|
|
610
|
+
event: "SessionStart",
|
|
611
|
+
hook_event_name: "SessionStart",
|
|
612
|
+
session_id: sessionId,
|
|
613
|
+
source: "startup",
|
|
614
|
+
model: effectiveModelState.current,
|
|
615
|
+
cwd,
|
|
616
|
+
permission_mode: permissions.getMode()
|
|
617
|
+
}, undefined, { signal });
|
|
664
618
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
619
|
+
while (true) {
|
|
620
|
+
if (signal.aborted) {
|
|
621
|
+
yield makeErrorResult({
|
|
622
|
+
subtype: "error_during_execution",
|
|
623
|
+
errors: ["Aborted"],
|
|
624
|
+
turns,
|
|
625
|
+
costUsd,
|
|
626
|
+
sessionId,
|
|
627
|
+
startTime,
|
|
628
|
+
apiTimeMs,
|
|
629
|
+
usage: totalUsage,
|
|
630
|
+
modelUsage,
|
|
631
|
+
permissionDenials
|
|
632
|
+
});
|
|
668
633
|
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;
|
|
683
|
-
}
|
|
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;
|
|
691
|
-
}
|
|
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;
|
|
703
634
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
635
|
+
if (turns >= maxTurns) {
|
|
636
|
+
yield makeErrorResult({
|
|
637
|
+
subtype: "error_max_turns",
|
|
638
|
+
errors: [`Reached maximum turns (${maxTurns})`],
|
|
639
|
+
turns,
|
|
640
|
+
costUsd,
|
|
641
|
+
sessionId,
|
|
642
|
+
startTime,
|
|
643
|
+
apiTimeMs,
|
|
644
|
+
usage: totalUsage,
|
|
645
|
+
modelUsage,
|
|
646
|
+
permissionDenials
|
|
647
|
+
});
|
|
648
|
+
return;
|
|
718
649
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
650
|
+
if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
|
|
651
|
+
yield makeErrorResult({
|
|
652
|
+
subtype: "error_max_budget_usd",
|
|
653
|
+
errors: [],
|
|
654
|
+
turns,
|
|
655
|
+
costUsd,
|
|
656
|
+
sessionId,
|
|
657
|
+
startTime,
|
|
658
|
+
apiTimeMs,
|
|
659
|
+
usage: totalUsage,
|
|
660
|
+
modelUsage,
|
|
661
|
+
permissionDenials
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const activeModel = effectiveModelState.current;
|
|
666
|
+
const toolDefs = tools.getDefinitions();
|
|
667
|
+
const apiStart = Date.now();
|
|
668
|
+
let assistantTextParts = [];
|
|
669
|
+
const toolCalls = [];
|
|
670
|
+
let turnUsage = emptyTokenUsage();
|
|
671
|
+
let turnStopReason = null;
|
|
672
|
+
const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
|
|
673
|
+
try {
|
|
674
|
+
const chunks = provider.chat({
|
|
675
|
+
model: activeModel,
|
|
676
|
+
messages,
|
|
677
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
678
|
+
systemPrompt,
|
|
679
|
+
signal,
|
|
680
|
+
nativeTools,
|
|
681
|
+
thinkingBudget: maxThinkingTokensState?.current,
|
|
682
|
+
thinking,
|
|
683
|
+
effort,
|
|
684
|
+
outputFormat
|
|
685
|
+
});
|
|
686
|
+
for await (const chunk of chunks) {
|
|
687
|
+
switch (chunk.type) {
|
|
688
|
+
case "text_delta":
|
|
689
|
+
assistantTextParts.push(chunk.text);
|
|
690
|
+
if (includePartialMessages) {
|
|
691
|
+
yield {
|
|
692
|
+
type: "stream_event",
|
|
693
|
+
event: { type: "text_delta", text: chunk.text },
|
|
694
|
+
parent_tool_use_id: null,
|
|
695
|
+
uuid: uuid(),
|
|
696
|
+
session_id: sessionId
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
break;
|
|
700
|
+
case "thinking_delta":
|
|
701
|
+
if (includePartialMessages) {
|
|
702
|
+
yield {
|
|
703
|
+
type: "stream_event",
|
|
704
|
+
event: { type: "thinking_delta", thinking: chunk.text },
|
|
705
|
+
parent_tool_use_id: null,
|
|
706
|
+
uuid: uuid(),
|
|
707
|
+
session_id: sessionId
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
case "tool_call":
|
|
712
|
+
toolCalls.push({ id: chunk.id, name: chunk.name, input: chunk.input });
|
|
713
|
+
break;
|
|
714
|
+
case "usage":
|
|
715
|
+
turnUsage = mergeUsage(turnUsage, chunk.usage);
|
|
716
|
+
break;
|
|
717
|
+
case "done":
|
|
718
|
+
turnStopReason = chunk.stopReason ?? null;
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} catch (err) {
|
|
723
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
724
|
+
if (fallbackModel && activeModel !== fallbackModel) {
|
|
725
|
+
effectiveModelState.current = fallbackModel;
|
|
726
|
+
yield {
|
|
727
|
+
type: "system",
|
|
728
|
+
subtype: "status",
|
|
729
|
+
status: null,
|
|
730
|
+
permissionMode: permissions.getMode(),
|
|
731
|
+
uuid: uuid(),
|
|
732
|
+
session_id: sessionId
|
|
733
|
+
};
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
yield makeErrorResult({
|
|
737
|
+
subtype: "error_during_execution",
|
|
738
|
+
errors: [`API error: ${message}`],
|
|
739
|
+
turns,
|
|
740
|
+
costUsd,
|
|
741
|
+
sessionId,
|
|
742
|
+
startTime,
|
|
743
|
+
apiTimeMs,
|
|
744
|
+
usage: totalUsage,
|
|
745
|
+
modelUsage,
|
|
746
|
+
permissionDenials
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
apiTimeMs += Date.now() - apiStart;
|
|
751
|
+
turns++;
|
|
752
|
+
totalUsage = mergeUsage(totalUsage, turnUsage);
|
|
753
|
+
const turnCost = provider.calculateCost(activeModel, turnUsage);
|
|
754
|
+
costUsd += turnCost;
|
|
755
|
+
if (!modelUsage[activeModel]) {
|
|
756
|
+
modelUsage[activeModel] = makeModelUsageEntry();
|
|
757
|
+
}
|
|
758
|
+
modelUsage[activeModel].inputTokens += turnUsage.inputTokens;
|
|
759
|
+
modelUsage[activeModel].outputTokens += turnUsage.outputTokens;
|
|
760
|
+
modelUsage[activeModel].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
|
|
761
|
+
modelUsage[activeModel].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
|
|
762
|
+
modelUsage[activeModel].totalCostUsd += turnCost;
|
|
763
|
+
modelUsage[activeModel].webSearchRequests = (modelUsage[activeModel].webSearchRequests ?? 0) + (turnUsage.webSearchRequests ?? 0);
|
|
764
|
+
modelUsage[activeModel].costUSD = modelUsage[activeModel].totalCostUsd;
|
|
765
|
+
modelUsage[activeModel].contextWindow = provider.getContextWindow(activeModel);
|
|
766
|
+
const assistantText = assistantTextParts.join("");
|
|
767
|
+
const assistantContent = [];
|
|
768
|
+
if (assistantText) {
|
|
769
|
+
assistantContent.push({ type: "text", text: assistantText });
|
|
770
|
+
}
|
|
771
|
+
for (const call of toolCalls) {
|
|
772
|
+
assistantContent.push({
|
|
773
|
+
type: "tool_use",
|
|
774
|
+
id: call.id,
|
|
775
|
+
name: call.name,
|
|
776
|
+
input: call.input
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
780
|
+
if (sessionLogger) {
|
|
781
|
+
sessionLogger("assistant", assistantContent, null);
|
|
782
|
+
}
|
|
783
|
+
yield {
|
|
784
|
+
type: "assistant",
|
|
785
|
+
message: {
|
|
786
|
+
role: "assistant",
|
|
787
|
+
content: assistantContent
|
|
788
|
+
},
|
|
789
|
+
parent_tool_use_id: null,
|
|
790
|
+
uuid: uuid(),
|
|
791
|
+
session_id: sessionId
|
|
792
|
+
};
|
|
793
|
+
if (toolCalls.length === 0) {
|
|
794
|
+
let structuredOutput;
|
|
795
|
+
if (outputFormat?.type === "json_schema") {
|
|
796
|
+
const parsed = extractStructuredJson(assistantText);
|
|
797
|
+
if (!parsed.ok) {
|
|
798
|
+
yield makeErrorResult({
|
|
799
|
+
subtype: "error_max_structured_output_retries",
|
|
800
|
+
errors: [parsed.error],
|
|
801
|
+
turns,
|
|
802
|
+
costUsd,
|
|
803
|
+
sessionId,
|
|
804
|
+
startTime,
|
|
805
|
+
apiTimeMs,
|
|
806
|
+
usage: totalUsage,
|
|
807
|
+
modelUsage,
|
|
808
|
+
permissionDenials
|
|
809
|
+
});
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
structuredOutput = parsed.value;
|
|
813
|
+
}
|
|
814
|
+
if (hooks) {
|
|
815
|
+
await hooks.fire("Stop", {
|
|
816
|
+
event: "Stop",
|
|
817
|
+
hook_event_name: "Stop",
|
|
818
|
+
session_id: sessionId,
|
|
819
|
+
text: assistantText || undefined,
|
|
820
|
+
stop_reason: turnStopReason ?? undefined
|
|
821
|
+
}, undefined, {
|
|
822
|
+
signal
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (hooks) {
|
|
826
|
+
await hooks.fire("SessionEnd", {
|
|
827
|
+
event: "SessionEnd",
|
|
828
|
+
hook_event_name: "SessionEnd",
|
|
829
|
+
session_id: sessionId,
|
|
830
|
+
reason: "other"
|
|
831
|
+
}, undefined, { signal });
|
|
832
|
+
}
|
|
833
|
+
yield {
|
|
834
|
+
type: "result",
|
|
835
|
+
subtype: "success",
|
|
836
|
+
duration_ms: Date.now() - startTime,
|
|
837
|
+
duration_api_ms: apiTimeMs,
|
|
838
|
+
is_error: false,
|
|
839
|
+
num_turns: turns,
|
|
840
|
+
result: assistantText,
|
|
841
|
+
stop_reason: turnStopReason,
|
|
842
|
+
total_cost_usd: costUsd,
|
|
843
|
+
usage: totalUsage,
|
|
844
|
+
modelUsage,
|
|
845
|
+
permission_denials: permissionDenials,
|
|
846
|
+
structured_output: structuredOutput,
|
|
847
|
+
uuid: uuid(),
|
|
848
|
+
session_id: sessionId
|
|
849
|
+
};
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const toolResults = [];
|
|
853
|
+
for (const call of toolCalls) {
|
|
854
|
+
let hookDenied = false;
|
|
855
|
+
let hookUpdatedInput;
|
|
856
|
+
if (hooks) {
|
|
857
|
+
const hookResult = await hooks.fire("PreToolUse", {
|
|
858
|
+
event: "PreToolUse",
|
|
859
|
+
hook_event_name: "PreToolUse",
|
|
860
|
+
tool_name: call.name,
|
|
861
|
+
tool_input: call.input,
|
|
862
|
+
session_id: sessionId
|
|
863
|
+
}, call.id, { signal });
|
|
864
|
+
if (hookResult) {
|
|
865
|
+
if (hookResult.permissionDecision === "deny") {
|
|
866
|
+
hookDenied = true;
|
|
867
|
+
}
|
|
868
|
+
if (hookResult.updatedInput !== undefined) {
|
|
869
|
+
hookUpdatedInput = hookResult.updatedInput;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (hookDenied) {
|
|
874
|
+
const denyContent = "Denied by hook";
|
|
875
|
+
toolResults.push({
|
|
876
|
+
type: "tool_result",
|
|
877
|
+
tool_use_id: call.id,
|
|
878
|
+
content: denyContent,
|
|
879
|
+
is_error: true
|
|
880
|
+
});
|
|
881
|
+
if (hooks) {
|
|
882
|
+
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
883
|
+
}
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const inputAfterHook = hookUpdatedInput !== undefined ? hookUpdatedInput : call.input;
|
|
887
|
+
const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
|
|
888
|
+
if (permResult.behavior === "deny") {
|
|
889
|
+
const denyContent = `Permission denied: ${permResult.message}`;
|
|
890
|
+
permissionDenials.push({
|
|
891
|
+
tool_name: call.name,
|
|
892
|
+
tool_use_id: call.id,
|
|
893
|
+
tool_input: inputAfterHook ?? {}
|
|
894
|
+
});
|
|
895
|
+
toolResults.push({
|
|
896
|
+
type: "tool_result",
|
|
897
|
+
tool_use_id: call.id,
|
|
898
|
+
content: denyContent,
|
|
899
|
+
is_error: true
|
|
900
|
+
});
|
|
901
|
+
if (hooks) {
|
|
902
|
+
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
903
|
+
}
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const toolInput = permResult.behavior === "allow" && permResult.updatedInput ? permResult.updatedInput : inputAfterHook;
|
|
907
|
+
let result;
|
|
908
|
+
if (call.name === "memory" && nativeMemoryTool) {
|
|
909
|
+
try {
|
|
910
|
+
const content = await nativeMemoryTool.execute(toolInput);
|
|
911
|
+
result = { content, isError: content.startsWith("Error:") };
|
|
912
|
+
} catch (err) {
|
|
913
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
914
|
+
result = { content: `Error: ${message}`, isError: true };
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
const toolCtx = {
|
|
918
|
+
cwd,
|
|
919
|
+
signal,
|
|
920
|
+
sessionId,
|
|
921
|
+
env
|
|
922
|
+
};
|
|
923
|
+
result = await tools.execute(call.name, toolInput, toolCtx);
|
|
924
|
+
}
|
|
925
|
+
if (call.name === "ExitPlanMode") {
|
|
926
|
+
permissions.setMode("default");
|
|
927
|
+
}
|
|
928
|
+
if (debug) {
|
|
929
|
+
console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
|
|
930
|
+
}
|
|
931
|
+
if (hooks) {
|
|
932
|
+
if (result.isError) {
|
|
933
|
+
await hooks.fire("PostToolUseFailure", {
|
|
934
|
+
event: "PostToolUseFailure",
|
|
935
|
+
hook_event_name: "PostToolUseFailure",
|
|
936
|
+
tool_name: call.name,
|
|
937
|
+
tool_result: result.content,
|
|
938
|
+
tool_error: true,
|
|
939
|
+
session_id: sessionId
|
|
940
|
+
}, call.id, { signal });
|
|
941
|
+
} else {
|
|
942
|
+
const postResult = await hooks.fire("PostToolUse", {
|
|
943
|
+
event: "PostToolUse",
|
|
944
|
+
hook_event_name: "PostToolUse",
|
|
945
|
+
tool_name: call.name,
|
|
946
|
+
tool_result: result.content,
|
|
947
|
+
session_id: sessionId
|
|
948
|
+
}, call.id, { signal });
|
|
949
|
+
if (postResult?.additionalContext) {
|
|
950
|
+
result.content += `
|
|
951
|
+
${postResult.additionalContext}`;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
toolResults.push({
|
|
956
|
+
type: "tool_result",
|
|
957
|
+
tool_use_id: call.id,
|
|
958
|
+
content: result.content,
|
|
959
|
+
is_error: result.isError
|
|
960
|
+
});
|
|
730
961
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const content = readFileSync(path, "utf-8");
|
|
735
|
-
return JSON.parse(content);
|
|
736
|
-
} catch {
|
|
737
|
-
return null;
|
|
962
|
+
messages.push({ role: "user", content: toolResults });
|
|
963
|
+
if (sessionLogger) {
|
|
964
|
+
sessionLogger("user", toolResults, null);
|
|
738
965
|
}
|
|
966
|
+
yield {
|
|
967
|
+
type: "user",
|
|
968
|
+
message: {
|
|
969
|
+
role: "user",
|
|
970
|
+
content: toolResults
|
|
971
|
+
},
|
|
972
|
+
parent_tool_use_id: null,
|
|
973
|
+
isSynthetic: true,
|
|
974
|
+
uuid: uuid(),
|
|
975
|
+
session_id: sessionId
|
|
976
|
+
};
|
|
739
977
|
}
|
|
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();
|
|
754
|
-
}
|
|
755
|
-
function emptyTokenUsage() {
|
|
756
|
-
return {
|
|
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
|
|
769
|
-
};
|
|
770
978
|
}
|
|
771
979
|
|
|
772
980
|
// src/utils/cost.ts
|
|
@@ -1262,6 +1470,22 @@ var CODEX_MODELS = new Set([
|
|
|
1262
1470
|
"gpt-5-codex",
|
|
1263
1471
|
"gpt-5-codex-mini"
|
|
1264
1472
|
]);
|
|
1473
|
+
var OPENAI_MAX_TOOL_NAME = 64;
|
|
1474
|
+
function sanitizeToolName(name) {
|
|
1475
|
+
const clean = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1476
|
+
if (clean.length <= OPENAI_MAX_TOOL_NAME)
|
|
1477
|
+
return clean;
|
|
1478
|
+
const hash = simpleHash(name);
|
|
1479
|
+
return clean.slice(0, OPENAI_MAX_TOOL_NAME - 7) + "_" + hash;
|
|
1480
|
+
}
|
|
1481
|
+
function simpleHash(s) {
|
|
1482
|
+
let h = 2166136261;
|
|
1483
|
+
for (let i = 0;i < s.length; i++) {
|
|
1484
|
+
h ^= s.charCodeAt(i);
|
|
1485
|
+
h = Math.imul(h, 16777619);
|
|
1486
|
+
}
|
|
1487
|
+
return (h >>> 0).toString(16).padStart(8, "0").slice(0, 6);
|
|
1488
|
+
}
|
|
1265
1489
|
|
|
1266
1490
|
class OpenAIAdapter {
|
|
1267
1491
|
name = "openai";
|
|
@@ -1269,6 +1493,7 @@ class OpenAIAdapter {
|
|
|
1269
1493
|
codexMode;
|
|
1270
1494
|
accountId;
|
|
1271
1495
|
currentAccessToken;
|
|
1496
|
+
toolNameMap = new Map;
|
|
1272
1497
|
constructor(options) {
|
|
1273
1498
|
const key = options?.apiKey ?? process.env.OPENAI_API_KEY;
|
|
1274
1499
|
if (key) {
|
|
@@ -1301,6 +1526,15 @@ class OpenAIAdapter {
|
|
|
1301
1526
|
}
|
|
1302
1527
|
}
|
|
1303
1528
|
async* chat(request) {
|
|
1529
|
+
this.toolNameMap.clear();
|
|
1530
|
+
if (request.tools) {
|
|
1531
|
+
for (const tool of request.tools) {
|
|
1532
|
+
const sanitized = sanitizeToolName(tool.name);
|
|
1533
|
+
if (sanitized !== tool.name) {
|
|
1534
|
+
this.toolNameMap.set(sanitized, tool.name);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1304
1538
|
if (this.codexMode) {
|
|
1305
1539
|
yield* this.chatResponses(request);
|
|
1306
1540
|
} else {
|
|
@@ -1402,7 +1636,7 @@ class OpenAIAdapter {
|
|
|
1402
1636
|
yield {
|
|
1403
1637
|
type: "tool_call",
|
|
1404
1638
|
id: buf.id,
|
|
1405
|
-
name: buf.name,
|
|
1639
|
+
name: this.resolveToolName(buf.name),
|
|
1406
1640
|
input
|
|
1407
1641
|
};
|
|
1408
1642
|
}
|
|
@@ -1446,7 +1680,7 @@ class OpenAIAdapter {
|
|
|
1446
1680
|
yield {
|
|
1447
1681
|
type: "tool_call",
|
|
1448
1682
|
id: item.call_id,
|
|
1449
|
-
name: item.name,
|
|
1683
|
+
name: this.resolveToolName(item.name),
|
|
1450
1684
|
input: parsedInput
|
|
1451
1685
|
};
|
|
1452
1686
|
}
|
|
@@ -1513,7 +1747,7 @@ class OpenAIAdapter {
|
|
|
1513
1747
|
id: block.id,
|
|
1514
1748
|
type: "function",
|
|
1515
1749
|
function: {
|
|
1516
|
-
name: block.name,
|
|
1750
|
+
name: sanitizeToolName(block.name),
|
|
1517
1751
|
arguments: JSON.stringify(block.input)
|
|
1518
1752
|
}
|
|
1519
1753
|
});
|
|
@@ -1578,7 +1812,7 @@ class OpenAIAdapter {
|
|
|
1578
1812
|
result.push({
|
|
1579
1813
|
type: "function_call",
|
|
1580
1814
|
call_id: tu.id,
|
|
1581
|
-
name: tu.name,
|
|
1815
|
+
name: sanitizeToolName(tu.name),
|
|
1582
1816
|
arguments: JSON.stringify(tu.input)
|
|
1583
1817
|
});
|
|
1584
1818
|
}
|
|
@@ -1610,7 +1844,7 @@ class OpenAIAdapter {
|
|
|
1610
1844
|
return tools.map((tool) => ({
|
|
1611
1845
|
type: "function",
|
|
1612
1846
|
function: {
|
|
1613
|
-
name: tool.name,
|
|
1847
|
+
name: sanitizeToolName(tool.name),
|
|
1614
1848
|
description: tool.description,
|
|
1615
1849
|
parameters: tool.inputSchema
|
|
1616
1850
|
}
|
|
@@ -1619,12 +1853,15 @@ class OpenAIAdapter {
|
|
|
1619
1853
|
convertToolsForResponses(tools) {
|
|
1620
1854
|
return tools.map((tool) => ({
|
|
1621
1855
|
type: "function",
|
|
1622
|
-
name: tool.name,
|
|
1856
|
+
name: sanitizeToolName(tool.name),
|
|
1623
1857
|
description: tool.description,
|
|
1624
1858
|
parameters: tool.inputSchema,
|
|
1625
1859
|
strict: false
|
|
1626
1860
|
}));
|
|
1627
1861
|
}
|
|
1862
|
+
resolveToolName(name) {
|
|
1863
|
+
return this.toolNameMap.get(name) ?? name;
|
|
1864
|
+
}
|
|
1628
1865
|
mapStopReason(reason) {
|
|
1629
1866
|
switch (reason) {
|
|
1630
1867
|
case "stop":
|
|
@@ -2374,7 +2611,7 @@ function resolvePath(filePath, cwd) {
|
|
|
2374
2611
|
|
|
2375
2612
|
// src/tools/write.ts
|
|
2376
2613
|
import { mkdir } from "fs/promises";
|
|
2377
|
-
import { dirname
|
|
2614
|
+
import { dirname } from "path";
|
|
2378
2615
|
var WriteTool = {
|
|
2379
2616
|
name: "Write",
|
|
2380
2617
|
description: "Writes content to a file. Creates parent directories if needed. " + "Overwrites existing files.",
|
|
@@ -2402,7 +2639,7 @@ var WriteTool = {
|
|
|
2402
2639
|
}
|
|
2403
2640
|
const resolvedPath = file_path.startsWith("/") ? file_path : `${ctx.cwd}/${file_path}`;
|
|
2404
2641
|
try {
|
|
2405
|
-
const dir =
|
|
2642
|
+
const dir = dirname(resolvedPath);
|
|
2406
2643
|
await mkdir(dir, { recursive: true });
|
|
2407
2644
|
await Bun.write(resolvedPath, content);
|
|
2408
2645
|
const lines = content.split(`
|
|
@@ -2963,7 +3200,7 @@ var AskUserQuestionTool = {
|
|
|
2963
3200
|
|
|
2964
3201
|
// src/tools/todo-write.ts
|
|
2965
3202
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
2966
|
-
import { dirname as
|
|
3203
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
2967
3204
|
var TodoWriteTool = {
|
|
2968
3205
|
name: "TodoWrite",
|
|
2969
3206
|
description: "Write/update task todo items for the current session.",
|
|
@@ -2995,9 +3232,9 @@ var TodoWriteTool = {
|
|
|
2995
3232
|
return { content: "Error: each todo requires content and status", isError: true };
|
|
2996
3233
|
}
|
|
2997
3234
|
}
|
|
2998
|
-
const filePath =
|
|
3235
|
+
const filePath = join3(ctx.cwd, ".claude", "todos.json");
|
|
2999
3236
|
try {
|
|
3000
|
-
await mkdir2(
|
|
3237
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
3001
3238
|
const payload = {
|
|
3002
3239
|
updatedAt: new Date().toISOString(),
|
|
3003
3240
|
todos
|
|
@@ -3016,11 +3253,11 @@ var TodoWriteTool = {
|
|
|
3016
3253
|
|
|
3017
3254
|
// src/tools/config.ts
|
|
3018
3255
|
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
3019
|
-
import { join as
|
|
3256
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
3020
3257
|
function scopePath(cwd, scope) {
|
|
3021
3258
|
if (scope === "project")
|
|
3022
|
-
return
|
|
3023
|
-
return
|
|
3259
|
+
return join4(cwd, ".claude", "settings.json");
|
|
3260
|
+
return join4(cwd, ".claude", "settings.local.json");
|
|
3024
3261
|
}
|
|
3025
3262
|
function setByPath(obj, keyPath, value) {
|
|
3026
3263
|
const keys = keyPath.split(".").filter(Boolean);
|
|
@@ -3101,7 +3338,7 @@ var ConfigTool = {
|
|
|
3101
3338
|
}
|
|
3102
3339
|
setByPath(data, key, value);
|
|
3103
3340
|
try {
|
|
3104
|
-
await mkdir3(
|
|
3341
|
+
await mkdir3(dirname3(filePath), { recursive: true });
|
|
3105
3342
|
await writeFile3(filePath, JSON.stringify(data, null, 2) + `
|
|
3106
3343
|
`, "utf-8");
|
|
3107
3344
|
return { content: `Updated ${key} in ${filePath}` };
|
|
@@ -3149,904 +3386,681 @@ function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
|
|
|
3149
3386
|
const registry = new ToolRegistry;
|
|
3150
3387
|
for (const name of toolNames) {
|
|
3151
3388
|
if (disallowedTools?.includes(name))
|
|
3152
|
-
continue;
|
|
3153
|
-
const tool = ALL_TOOLS[name];
|
|
3154
|
-
if (tool) {
|
|
3155
|
-
registry.register(tool);
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
return registry;
|
|
3159
|
-
}
|
|
3160
|
-
|
|
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
|
-
`);
|
|
3167
|
-
}
|
|
3168
|
-
function extractFrontmatter(content) {
|
|
3169
|
-
const normalized = normalizeNewlines(content);
|
|
3170
|
-
if (!normalized.startsWith("---")) {
|
|
3171
|
-
return { yamlString: null, body: normalized };
|
|
3172
|
-
}
|
|
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;
|
|
3193
|
-
}
|
|
3194
|
-
|
|
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}"`);
|
|
3210
|
-
}
|
|
3211
|
-
if (name.length > MAX_NAME_LENGTH) {
|
|
3212
|
-
errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
|
3213
|
-
}
|
|
3214
|
-
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
3215
|
-
errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
|
|
3216
|
-
}
|
|
3217
|
-
if (name.startsWith("-") || name.endsWith("-")) {
|
|
3218
|
-
errors.push(`name must not start or end with a hyphen`);
|
|
3219
|
-
}
|
|
3220
|
-
if (name.includes("--")) {
|
|
3221
|
-
errors.push(`name must not contain consecutive hyphens`);
|
|
3222
|
-
}
|
|
3223
|
-
return errors;
|
|
3224
|
-
}
|
|
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})`];
|
|
3239
|
-
}
|
|
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)) {
|
|
3255
|
-
continue;
|
|
3256
|
-
}
|
|
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;
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
if (isDirectory) {
|
|
3270
|
-
const subResult = loadSkillsFromDirInternal(fullPath, source, false);
|
|
3271
|
-
skills.push(...subResult.skills);
|
|
3272
|
-
diagnostics.push(...subResult.diagnostics);
|
|
3273
|
-
continue;
|
|
3274
|
-
}
|
|
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);
|
|
3284
|
-
}
|
|
3285
|
-
diagnostics.push(...result.diagnostics);
|
|
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 };
|
|
3389
|
+
continue;
|
|
3390
|
+
const tool = ALL_TOOLS[name];
|
|
3391
|
+
if (tool) {
|
|
3392
|
+
registry.register(tool);
|
|
3312
3393
|
}
|
|
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 };
|
|
3334
3394
|
}
|
|
3395
|
+
return registry;
|
|
3335
3396
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3397
|
+
|
|
3398
|
+
// src/permissions.ts
|
|
3399
|
+
var SAFE_TOOLS = new Set(["Read", "Glob", "Grep", "WebFetch", "WebSearch"]);
|
|
3400
|
+
var EDIT_TOOLS = new Set(["Write", "Edit", "NotebookEdit", "TodoWrite", "Config"]);
|
|
3401
|
+
var FS_COMMANDS = ["mkdir", "touch", "rm", "mv", "cp"];
|
|
3402
|
+
var DELEGATE_TOOLS = new Set(["Teammate", "Task", "TaskOutput", "TaskStop"]);
|
|
3403
|
+
function normalizeRules(rules) {
|
|
3404
|
+
if (!rules)
|
|
3405
|
+
return [];
|
|
3406
|
+
return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
|
|
3345
3407
|
}
|
|
3346
|
-
function
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3408
|
+
function matchesRule(rules, toolName, input) {
|
|
3409
|
+
for (const rule of rules) {
|
|
3410
|
+
if (rule.toolName !== toolName)
|
|
3411
|
+
continue;
|
|
3412
|
+
if (!rule.ruleContent)
|
|
3413
|
+
return true;
|
|
3414
|
+
const inputStr = toolName === "Bash" ? String(input?.command ?? "") : JSON.stringify(input ?? {});
|
|
3415
|
+
if (inputStr.includes(rule.ruleContent))
|
|
3416
|
+
return true;
|
|
3417
|
+
}
|
|
3418
|
+
return false;
|
|
3349
3419
|
}
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3420
|
+
|
|
3421
|
+
class PermissionManager {
|
|
3422
|
+
mode;
|
|
3423
|
+
canUseTool;
|
|
3424
|
+
allowRules;
|
|
3425
|
+
denyRules;
|
|
3426
|
+
settingsManager;
|
|
3427
|
+
constructor(mode = "default", canUseTool, permissions, settingsManager) {
|
|
3428
|
+
this.mode = mode;
|
|
3429
|
+
this.canUseTool = canUseTool;
|
|
3430
|
+
this.allowRules = normalizeRules(permissions?.allow);
|
|
3431
|
+
this.denyRules = normalizeRules(permissions?.deny);
|
|
3432
|
+
this.settingsManager = settingsManager;
|
|
3433
|
+
}
|
|
3434
|
+
async check(toolName, input, options) {
|
|
3435
|
+
if (this.mode === "bypassPermissions") {
|
|
3436
|
+
return { behavior: "allow" };
|
|
3437
|
+
}
|
|
3438
|
+
if (matchesRule(this.denyRules, toolName, input)) {
|
|
3439
|
+
return {
|
|
3440
|
+
behavior: "deny",
|
|
3441
|
+
message: `Tool "${toolName}" is denied by permissions config.`
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
if (this.mode === "plan") {
|
|
3445
|
+
if (!SAFE_TOOLS.has(toolName)) {
|
|
3446
|
+
return {
|
|
3447
|
+
behavior: "deny",
|
|
3448
|
+
message: `Tool "${toolName}" is not allowed in plan mode. Only read-only tools are available.`
|
|
3449
|
+
};
|
|
3364
3450
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
if (
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
message: `
|
|
3372
|
-
|
|
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);
|
|
3451
|
+
return { behavior: "allow" };
|
|
3452
|
+
}
|
|
3453
|
+
if (this.mode === "delegate") {
|
|
3454
|
+
if (!DELEGATE_TOOLS.has(toolName) && !SAFE_TOOLS.has(toolName)) {
|
|
3455
|
+
return {
|
|
3456
|
+
behavior: "deny",
|
|
3457
|
+
message: `Tool "${toolName}" is not allowed in delegate mode. Only Teammate, Task, and read-only tools are available.`
|
|
3458
|
+
};
|
|
3383
3459
|
}
|
|
3460
|
+
return { behavior: "allow" };
|
|
3384
3461
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
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));
|
|
3391
|
-
}
|
|
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;
|
|
3462
|
+
if (matchesRule(this.allowRules, toolName, input)) {
|
|
3463
|
+
return { behavior: "allow" };
|
|
3397
3464
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3465
|
+
if (SAFE_TOOLS.has(toolName)) {
|
|
3466
|
+
return { behavior: "allow" };
|
|
3467
|
+
}
|
|
3468
|
+
if (this.mode === "acceptEdits") {
|
|
3469
|
+
if (EDIT_TOOLS.has(toolName)) {
|
|
3470
|
+
return { behavior: "allow" };
|
|
3471
|
+
}
|
|
3472
|
+
if (toolName === "Bash") {
|
|
3473
|
+
const cmd = String(input?.command ?? "").trimStart();
|
|
3474
|
+
if (FS_COMMANDS.some((fc) => cmd.startsWith(fc + " ") || cmd === fc)) {
|
|
3475
|
+
return { behavior: "allow" };
|
|
3408
3476
|
}
|
|
3409
|
-
} else {
|
|
3410
|
-
allDiagnostics.push({ type: "warning", message: "skill path is not a markdown file", path: resolvedPath });
|
|
3411
3477
|
}
|
|
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
3478
|
}
|
|
3479
|
+
if (this.canUseTool) {
|
|
3480
|
+
const result = await this.canUseTool(toolName, input, {
|
|
3481
|
+
...options,
|
|
3482
|
+
toolUseID: options.toolUseId,
|
|
3483
|
+
agentID: options.agentId
|
|
3484
|
+
});
|
|
3485
|
+
if (result.behavior === "allow" && result.updatedPermissions) {
|
|
3486
|
+
this.applyPermissionUpdates(result.updatedPermissions);
|
|
3487
|
+
}
|
|
3488
|
+
return result;
|
|
3489
|
+
}
|
|
3490
|
+
if (this.mode === "dontAsk") {
|
|
3491
|
+
return {
|
|
3492
|
+
behavior: "deny",
|
|
3493
|
+
message: `Tool "${toolName}" requires approval. In dontAsk mode, tools must be pre-approved via permissions config.`
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
return { behavior: "allow" };
|
|
3416
3497
|
}
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3498
|
+
applyPermissionUpdates(updates) {
|
|
3499
|
+
for (const update of updates) {
|
|
3500
|
+
if (this.settingsManager && update.destination !== "session" && update.destination !== "cliArg") {
|
|
3501
|
+
this.settingsManager.persistUpdate(update);
|
|
3502
|
+
}
|
|
3503
|
+
switch (update.type) {
|
|
3504
|
+
case "addRules":
|
|
3505
|
+
for (const rule of update.rules) {
|
|
3506
|
+
if (update.behavior === "allow") {
|
|
3507
|
+
this.allowRules.push(rule);
|
|
3508
|
+
} else if (update.behavior === "deny") {
|
|
3509
|
+
this.denyRules.push(rule);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
break;
|
|
3513
|
+
case "removeRules":
|
|
3514
|
+
for (const rule of update.rules) {
|
|
3515
|
+
if (update.behavior === "allow") {
|
|
3516
|
+
this.allowRules = this.allowRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
|
|
3517
|
+
} else if (update.behavior === "deny") {
|
|
3518
|
+
this.denyRules = this.denyRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
break;
|
|
3522
|
+
case "replaceRules":
|
|
3523
|
+
if (update.behavior === "allow") {
|
|
3524
|
+
this.allowRules = [...update.rules];
|
|
3525
|
+
} else if (update.behavior === "deny") {
|
|
3526
|
+
this.denyRules = [...update.rules];
|
|
3527
|
+
}
|
|
3528
|
+
break;
|
|
3529
|
+
case "setMode":
|
|
3530
|
+
this.mode = update.mode;
|
|
3531
|
+
break;
|
|
3532
|
+
}
|
|
3444
3533
|
}
|
|
3445
|
-
lines.push(" </skill>");
|
|
3446
3534
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3535
|
+
setMode(mode) {
|
|
3536
|
+
this.mode = mode;
|
|
3537
|
+
}
|
|
3538
|
+
getMode() {
|
|
3539
|
+
return this.mode;
|
|
3540
|
+
}
|
|
3450
3541
|
}
|
|
3451
|
-
// src/utils/system-prompt.ts
|
|
3452
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
3453
|
-
import { join as join7 } from "path";
|
|
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.
|
|
3455
|
-
|
|
3456
|
-
You are highly capable and can help users complete complex tasks that would otherwise be too difficult or time-consuming.`;
|
|
3457
|
-
var CODING_GUIDELINES = `# Coding Guidelines
|
|
3458
|
-
|
|
3459
|
-
- Read files before modifying them. Understand existing code before suggesting changes.
|
|
3460
|
-
- Make minimal, focused changes. Only modify what's directly requested or clearly necessary.
|
|
3461
|
-
- Don't over-engineer. Keep solutions simple. Don't add features, refactoring, or abstractions beyond what was asked.
|
|
3462
|
-
- Don't introduce security vulnerabilities (command injection, XSS, SQL injection, etc.).
|
|
3463
|
-
- Use the dedicated tools for file operations instead of shell commands:
|
|
3464
|
-
- Read files with the Read tool (not cat/head/tail)
|
|
3465
|
-
- Edit files with the Edit tool (not sed/awk)
|
|
3466
|
-
- Write files with the Write tool (not echo/cat heredoc)
|
|
3467
|
-
- Search files with Glob (not find/ls) and Grep (not grep/rg)
|
|
3468
|
-
- Use Bash only for system commands that truly require shell execution.`;
|
|
3469
|
-
var BASH_GUIDELINES = `# Bash Tool Guidelines
|
|
3470
|
-
|
|
3471
|
-
- Use for system commands, git operations, running scripts, and terminal tasks.
|
|
3472
|
-
- Always quote file paths with spaces.
|
|
3473
|
-
- Prefer absolute paths over cd + relative paths.
|
|
3474
|
-
- Don't run destructive commands without clear intent.
|
|
3475
|
-
- Capture output \u2014 the result is returned, not displayed interactively.
|
|
3476
|
-
- Commands timeout after 120s by default (max 600s).`;
|
|
3477
|
-
var EDIT_GUIDELINES = `# Edit Tool Guidelines
|
|
3478
|
-
|
|
3479
|
-
- The old_string must match exactly (including indentation and whitespace).
|
|
3480
|
-
- The old_string must be unique in the file, or the edit will fail. Provide more surrounding context to make it unique.
|
|
3481
|
-
- Use replace_all: true only when you want to replace every occurrence.`;
|
|
3482
|
-
var READ_GUIDELINES = `# Read Tool Guidelines
|
|
3483
|
-
|
|
3484
|
-
- Returns content with line numbers in cat -n format.
|
|
3485
|
-
- Use offset/limit for large files to read specific sections.
|
|
3486
|
-
- Lines longer than 2000 characters are truncated.`;
|
|
3487
|
-
var GLOB_GUIDELINES = `# Glob Tool Guidelines
|
|
3488
3542
|
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3543
|
+
// src/settings.ts
|
|
3544
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3545
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
3546
|
+
import { homedir as homedir3 } from "os";
|
|
3492
3547
|
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
var TOOL_SPECIFIC_GUIDELINES = {
|
|
3498
|
-
Bash: BASH_GUIDELINES,
|
|
3499
|
-
Edit: EDIT_GUIDELINES,
|
|
3500
|
-
Read: READ_GUIDELINES,
|
|
3501
|
-
Glob: GLOB_GUIDELINES,
|
|
3502
|
-
Grep: GREP_GUIDELINES
|
|
3503
|
-
};
|
|
3504
|
-
function buildSystemPrompt(context) {
|
|
3505
|
-
const sections = [CORE_IDENTITY];
|
|
3506
|
-
for (const toolName of context.tools) {
|
|
3507
|
-
const guidelines = TOOL_SPECIFIC_GUIDELINES[toolName];
|
|
3508
|
-
if (guidelines) {
|
|
3509
|
-
sections.push(guidelines);
|
|
3510
|
-
}
|
|
3548
|
+
class SettingsManager {
|
|
3549
|
+
cwd;
|
|
3550
|
+
constructor(cwd) {
|
|
3551
|
+
this.cwd = cwd;
|
|
3511
3552
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3553
|
+
loadPermissions(sources) {
|
|
3554
|
+
const allAllow = [];
|
|
3555
|
+
const allDeny = [];
|
|
3556
|
+
for (const source of sources) {
|
|
3557
|
+
const path = this.sourceToPath(source);
|
|
3558
|
+
const data = this.readJson(path);
|
|
3559
|
+
if (!data?.permissions)
|
|
3560
|
+
continue;
|
|
3561
|
+
const perms = data.permissions;
|
|
3562
|
+
if (Array.isArray(perms.allow)) {
|
|
3563
|
+
for (const rule of perms.allow) {
|
|
3564
|
+
if (typeof rule === "string") {
|
|
3565
|
+
allAllow.push(this.parseRule(rule));
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
if (Array.isArray(perms.deny)) {
|
|
3570
|
+
for (const rule of perms.deny) {
|
|
3571
|
+
if (typeof rule === "string") {
|
|
3572
|
+
allDeny.push(this.parseRule(rule));
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3521
3576
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3577
|
+
const result = {};
|
|
3578
|
+
if (allAllow.length > 0)
|
|
3579
|
+
result.allow = allAllow;
|
|
3580
|
+
if (allDeny.length > 0)
|
|
3581
|
+
result.deny = allDeny;
|
|
3582
|
+
return result;
|
|
3583
|
+
}
|
|
3584
|
+
persistUpdate(update) {
|
|
3585
|
+
const path = this.destinationToPath(update.destination);
|
|
3586
|
+
if (!path)
|
|
3587
|
+
return;
|
|
3588
|
+
const data = this.readJson(path) ?? {};
|
|
3589
|
+
if (!data.permissions)
|
|
3590
|
+
data.permissions = {};
|
|
3591
|
+
const perms = data.permissions;
|
|
3592
|
+
switch (update.type) {
|
|
3593
|
+
case "addRules": {
|
|
3594
|
+
const key = update.behavior;
|
|
3595
|
+
const existing = Array.isArray(perms[key]) ? perms[key] : [];
|
|
3596
|
+
const newRules = update.rules.map((r) => this.serializeRule(r));
|
|
3597
|
+
const set = new Set(existing);
|
|
3598
|
+
for (const rule of newRules)
|
|
3599
|
+
set.add(rule);
|
|
3600
|
+
perms[key] = [...set];
|
|
3601
|
+
break;
|
|
3602
|
+
}
|
|
3603
|
+
case "removeRules": {
|
|
3604
|
+
const key = update.behavior;
|
|
3605
|
+
if (!Array.isArray(perms[key]))
|
|
3606
|
+
break;
|
|
3607
|
+
const toRemove = new Set(update.rules.map((r) => this.serializeRule(r)));
|
|
3608
|
+
perms[key] = perms[key].filter((r) => !toRemove.has(r));
|
|
3609
|
+
break;
|
|
3610
|
+
}
|
|
3611
|
+
case "replaceRules": {
|
|
3612
|
+
const key = update.behavior;
|
|
3613
|
+
perms[key] = update.rules.map((r) => this.serializeRule(r));
|
|
3614
|
+
break;
|
|
3528
3615
|
}
|
|
3616
|
+
case "setMode": {
|
|
3617
|
+
perms.defaultMode = update.mode;
|
|
3618
|
+
break;
|
|
3619
|
+
}
|
|
3620
|
+
default:
|
|
3621
|
+
return;
|
|
3529
3622
|
}
|
|
3623
|
+
const dir = dirname4(path);
|
|
3624
|
+
if (!existsSync3(dir))
|
|
3625
|
+
mkdirSync2(dir, { recursive: true });
|
|
3626
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + `
|
|
3627
|
+
`);
|
|
3530
3628
|
}
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3629
|
+
sourceToPath(source) {
|
|
3630
|
+
switch (source) {
|
|
3631
|
+
case "user":
|
|
3632
|
+
return join5(homedir3(), ".claude", "settings.json");
|
|
3633
|
+
case "project":
|
|
3634
|
+
return join5(this.cwd, ".claude", "settings.json");
|
|
3635
|
+
case "local":
|
|
3636
|
+
return join5(this.cwd, ".claude", "settings.local.json");
|
|
3537
3637
|
}
|
|
3538
3638
|
}
|
|
3539
|
-
|
|
3540
|
-
|
|
3639
|
+
destinationToPath(destination) {
|
|
3640
|
+
switch (destination) {
|
|
3641
|
+
case "userSettings":
|
|
3642
|
+
return join5(homedir3(), ".claude", "settings.json");
|
|
3643
|
+
case "projectSettings":
|
|
3644
|
+
return join5(this.cwd, ".claude", "settings.json");
|
|
3645
|
+
case "localSettings":
|
|
3646
|
+
return join5(this.cwd, ".claude", "settings.local.json");
|
|
3647
|
+
default:
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3541
3650
|
}
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
`);
|
|
3545
|
-
}
|
|
3546
|
-
function readProjectInstructions(cwd) {
|
|
3547
|
-
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
3651
|
+
readJson(path) {
|
|
3548
3652
|
try {
|
|
3549
|
-
const content =
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3653
|
+
const content = readFileSync3(path, "utf-8");
|
|
3654
|
+
return JSON.parse(content);
|
|
3655
|
+
} catch {
|
|
3656
|
+
return null;
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
parseRule(s) {
|
|
3660
|
+
const match = s.match(/^([^(]+)\((.+)\)$/);
|
|
3661
|
+
if (match)
|
|
3662
|
+
return { toolName: match[1], ruleContent: match[2] };
|
|
3663
|
+
return { toolName: s };
|
|
3664
|
+
}
|
|
3665
|
+
serializeRule(rule) {
|
|
3666
|
+
return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
|
|
3553
3667
|
}
|
|
3554
|
-
return null;
|
|
3555
3668
|
}
|
|
3556
3669
|
|
|
3557
|
-
// src/
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
cacheCreationInputTokens: 0,
|
|
3564
|
-
totalCostUsd: 0
|
|
3565
|
-
};
|
|
3670
|
+
// src/skills/frontmatter.ts
|
|
3671
|
+
import { parse } from "yaml";
|
|
3672
|
+
function normalizeNewlines(value) {
|
|
3673
|
+
return value.replace(/\r\n/g, `
|
|
3674
|
+
`).replace(/\r/g, `
|
|
3675
|
+
`);
|
|
3566
3676
|
}
|
|
3567
|
-
function
|
|
3677
|
+
function extractFrontmatter(content) {
|
|
3678
|
+
const normalized = normalizeNewlines(content);
|
|
3679
|
+
if (!normalized.startsWith("---")) {
|
|
3680
|
+
return { yamlString: null, body: normalized };
|
|
3681
|
+
}
|
|
3682
|
+
const endIndex = normalized.indexOf(`
|
|
3683
|
+
---`, 3);
|
|
3684
|
+
if (endIndex === -1) {
|
|
3685
|
+
return { yamlString: null, body: normalized };
|
|
3686
|
+
}
|
|
3568
3687
|
return {
|
|
3569
|
-
|
|
3570
|
-
|
|
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
|
|
3688
|
+
yamlString: normalized.slice(4, endIndex),
|
|
3689
|
+
body: normalized.slice(endIndex + 4).trim()
|
|
3583
3690
|
};
|
|
3584
3691
|
}
|
|
3585
|
-
function
|
|
3586
|
-
const
|
|
3587
|
-
if (!
|
|
3588
|
-
return {
|
|
3692
|
+
function parseFrontmatter(content) {
|
|
3693
|
+
const { yamlString, body } = extractFrontmatter(content);
|
|
3694
|
+
if (!yamlString) {
|
|
3695
|
+
return { frontmatter: {}, body };
|
|
3696
|
+
}
|
|
3697
|
+
const parsed = parse(yamlString);
|
|
3698
|
+
return { frontmatter: parsed ?? {}, body };
|
|
3699
|
+
}
|
|
3700
|
+
function stripFrontmatter(content) {
|
|
3701
|
+
return parseFrontmatter(content).body;
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// src/skills/skills.ts
|
|
3705
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, realpathSync, statSync } from "fs";
|
|
3706
|
+
import { homedir as homedir4 } from "os";
|
|
3707
|
+
import { basename, dirname as dirname5, isAbsolute, join as join6, resolve } from "path";
|
|
3708
|
+
var MAX_NAME_LENGTH = 64;
|
|
3709
|
+
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
3710
|
+
var MAX_COMPATIBILITY_LENGTH = 500;
|
|
3711
|
+
var CONFIG_DIR_NAME = ".claude";
|
|
3712
|
+
function shouldIgnore(name) {
|
|
3713
|
+
return name.startsWith(".") || name === "node_modules";
|
|
3714
|
+
}
|
|
3715
|
+
function validateName(name, parentDirName) {
|
|
3716
|
+
const errors = [];
|
|
3717
|
+
if (name !== parentDirName) {
|
|
3718
|
+
errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
|
|
3589
3719
|
}
|
|
3590
|
-
|
|
3591
|
-
|
|
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}` };
|
|
3720
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
3721
|
+
errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
|
|
3597
3722
|
}
|
|
3598
|
-
|
|
3599
|
-
|
|
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));
|
|
3723
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
3724
|
+
errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
|
|
3643
3725
|
}
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
{ role: "user", content: prompt }
|
|
3647
|
-
];
|
|
3648
|
-
if (sessionLogger) {
|
|
3649
|
-
sessionLogger("user", prompt, null);
|
|
3726
|
+
if (name.startsWith("-") || name.endsWith("-")) {
|
|
3727
|
+
errors.push(`name must not start or end with a hyphen`);
|
|
3650
3728
|
}
|
|
3651
|
-
|
|
3652
|
-
|
|
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 });
|
|
3729
|
+
if (name.includes("--")) {
|
|
3730
|
+
errors.push(`name must not contain consecutive hyphens`);
|
|
3679
3731
|
}
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
permission_mode: permissions.getMode()
|
|
3689
|
-
}, undefined, { signal });
|
|
3732
|
+
return errors;
|
|
3733
|
+
}
|
|
3734
|
+
function validateDescription(description) {
|
|
3735
|
+
const errors = [];
|
|
3736
|
+
if (!description || description.trim() === "") {
|
|
3737
|
+
errors.push("description is required");
|
|
3738
|
+
} else if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
3739
|
+
errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
|
|
3690
3740
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
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;
|
|
3741
|
+
return errors;
|
|
3742
|
+
}
|
|
3743
|
+
function validateCompatibility(compatibility) {
|
|
3744
|
+
if (!compatibility)
|
|
3745
|
+
return [];
|
|
3746
|
+
if (compatibility.length > MAX_COMPATIBILITY_LENGTH) {
|
|
3747
|
+
return [`compatibility exceeds ${MAX_COMPATIBILITY_LENGTH} characters (${compatibility.length})`];
|
|
3748
|
+
}
|
|
3749
|
+
return [];
|
|
3750
|
+
}
|
|
3751
|
+
function loadSkillsFromDir(options) {
|
|
3752
|
+
return loadSkillsFromDirInternal(options.dir, options.source, true);
|
|
3753
|
+
}
|
|
3754
|
+
function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
|
|
3755
|
+
const skills = [];
|
|
3756
|
+
const diagnostics = [];
|
|
3757
|
+
if (!existsSync4(dir)) {
|
|
3758
|
+
return { skills, diagnostics };
|
|
3759
|
+
}
|
|
3760
|
+
try {
|
|
3761
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
3762
|
+
for (const entry of entries) {
|
|
3763
|
+
if (shouldIgnore(entry.name)) {
|
|
3764
|
+
continue;
|
|
3765
|
+
}
|
|
3766
|
+
const fullPath = join6(dir, entry.name);
|
|
3767
|
+
let isDirectory = entry.isDirectory();
|
|
3768
|
+
let isFile = entry.isFile();
|
|
3769
|
+
if (entry.isSymbolicLink()) {
|
|
3770
|
+
try {
|
|
3771
|
+
const stats = statSync(fullPath);
|
|
3772
|
+
isDirectory = stats.isDirectory();
|
|
3773
|
+
isFile = stats.isFile();
|
|
3774
|
+
} catch {
|
|
3775
|
+
continue;
|
|
3792
3776
|
}
|
|
3793
3777
|
}
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
yield {
|
|
3799
|
-
type: "system",
|
|
3800
|
-
subtype: "status",
|
|
3801
|
-
status: null,
|
|
3802
|
-
permissionMode: permissions.getMode(),
|
|
3803
|
-
uuid: uuid(),
|
|
3804
|
-
session_id: sessionId
|
|
3805
|
-
};
|
|
3778
|
+
if (isDirectory) {
|
|
3779
|
+
const subResult = loadSkillsFromDirInternal(fullPath, source, false);
|
|
3780
|
+
skills.push(...subResult.skills);
|
|
3781
|
+
diagnostics.push(...subResult.diagnostics);
|
|
3806
3782
|
continue;
|
|
3807
3783
|
}
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
});
|
|
3820
|
-
return;
|
|
3784
|
+
if (!isFile)
|
|
3785
|
+
continue;
|
|
3786
|
+
const isRootMd = includeRootFiles && entry.name.endsWith(".md");
|
|
3787
|
+
const isSkillMd = !includeRootFiles && entry.name === "SKILL.md";
|
|
3788
|
+
if (!isRootMd && !isSkillMd)
|
|
3789
|
+
continue;
|
|
3790
|
+
const result = loadSkillFromFile(fullPath, source);
|
|
3791
|
+
if (result.skill) {
|
|
3792
|
+
skills.push(result.skill);
|
|
3793
|
+
}
|
|
3794
|
+
diagnostics.push(...result.diagnostics);
|
|
3821
3795
|
}
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3796
|
+
} catch {}
|
|
3797
|
+
return { skills, diagnostics };
|
|
3798
|
+
}
|
|
3799
|
+
function loadSkillFromFile(filePath, source) {
|
|
3800
|
+
const diagnostics = [];
|
|
3801
|
+
try {
|
|
3802
|
+
const rawContent = readFileSync4(filePath, "utf-8");
|
|
3803
|
+
const { frontmatter } = parseFrontmatter(rawContent);
|
|
3804
|
+
const skillDir = dirname5(filePath);
|
|
3805
|
+
const parentDirName = basename(skillDir);
|
|
3806
|
+
const descErrors = validateDescription(frontmatter.description);
|
|
3807
|
+
for (const error of descErrors) {
|
|
3808
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3829
3809
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
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 });
|
|
3810
|
+
const name = frontmatter.name || parentDirName;
|
|
3811
|
+
const nameErrors = validateName(name, parentDirName);
|
|
3812
|
+
for (const error of nameErrors) {
|
|
3813
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3842
3814
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
id: call.id,
|
|
3847
|
-
name: call.name,
|
|
3848
|
-
input: call.input
|
|
3849
|
-
});
|
|
3815
|
+
const compatErrors = validateCompatibility(frontmatter.compatibility);
|
|
3816
|
+
for (const error of compatErrors) {
|
|
3817
|
+
diagnostics.push({ type: "warning", message: error, path: filePath });
|
|
3850
3818
|
}
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
sessionLogger("assistant", assistantContent, null);
|
|
3819
|
+
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
|
3820
|
+
return { skill: null, diagnostics };
|
|
3854
3821
|
}
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3822
|
+
const allowedToolsRaw = frontmatter["allowed-tools"];
|
|
3823
|
+
const allowedTools = allowedToolsRaw ? allowedToolsRaw.split(/\s+/).filter(Boolean) : undefined;
|
|
3824
|
+
return {
|
|
3825
|
+
skill: {
|
|
3826
|
+
name,
|
|
3827
|
+
description: frontmatter.description,
|
|
3828
|
+
filePath,
|
|
3829
|
+
baseDir: skillDir,
|
|
3830
|
+
source,
|
|
3831
|
+
disableModelInvocation: frontmatter["disable-model-invocation"] === true,
|
|
3832
|
+
license: frontmatter.license,
|
|
3833
|
+
compatibility: frontmatter.compatibility,
|
|
3834
|
+
metadata: frontmatter.metadata,
|
|
3835
|
+
allowedTools
|
|
3860
3836
|
},
|
|
3861
|
-
|
|
3862
|
-
uuid: uuid(),
|
|
3863
|
-
session_id: sessionId
|
|
3837
|
+
diagnostics
|
|
3864
3838
|
};
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
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;
|
|
3839
|
+
} catch (error) {
|
|
3840
|
+
const message = error instanceof Error ? error.message : "failed to parse skill file";
|
|
3841
|
+
diagnostics.push({ type: "warning", message, path: filePath });
|
|
3842
|
+
return { skill: null, diagnostics };
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
function normalizePath(input) {
|
|
3846
|
+
const trimmed = input.trim();
|
|
3847
|
+
if (trimmed === "~")
|
|
3848
|
+
return homedir4();
|
|
3849
|
+
if (trimmed.startsWith("~/"))
|
|
3850
|
+
return join6(homedir4(), trimmed.slice(2));
|
|
3851
|
+
if (trimmed.startsWith("~"))
|
|
3852
|
+
return join6(homedir4(), trimmed.slice(1));
|
|
3853
|
+
return trimmed;
|
|
3854
|
+
}
|
|
3855
|
+
function resolveSkillPath(p, cwd) {
|
|
3856
|
+
const normalized = normalizePath(p);
|
|
3857
|
+
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
|
3858
|
+
}
|
|
3859
|
+
function loadSkills(options = {}) {
|
|
3860
|
+
const { cwd = process.cwd(), skillPaths = [], includeDefaults = true } = options;
|
|
3861
|
+
const skillMap = new Map;
|
|
3862
|
+
const realPathSet = new Set;
|
|
3863
|
+
const allDiagnostics = [];
|
|
3864
|
+
const collisionDiagnostics = [];
|
|
3865
|
+
function addSkills(result) {
|
|
3866
|
+
allDiagnostics.push(...result.diagnostics);
|
|
3867
|
+
for (const skill of result.skills) {
|
|
3868
|
+
let realPath;
|
|
3869
|
+
try {
|
|
3870
|
+
realPath = realpathSync(skill.filePath);
|
|
3871
|
+
} catch {
|
|
3872
|
+
realPath = skill.filePath;
|
|
3977
3873
|
}
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3874
|
+
if (realPathSet.has(realPath))
|
|
3875
|
+
continue;
|
|
3876
|
+
const existing = skillMap.get(skill.name);
|
|
3877
|
+
if (existing) {
|
|
3878
|
+
collisionDiagnostics.push({
|
|
3879
|
+
type: "collision",
|
|
3880
|
+
message: `name "${skill.name}" collision`,
|
|
3881
|
+
path: skill.filePath,
|
|
3882
|
+
collision: {
|
|
3883
|
+
resourceType: "skill",
|
|
3884
|
+
name: skill.name,
|
|
3885
|
+
winnerPath: existing.filePath,
|
|
3886
|
+
loserPath: skill.filePath
|
|
3887
|
+
}
|
|
3888
|
+
});
|
|
3988
3889
|
} else {
|
|
3989
|
-
|
|
3990
|
-
|
|
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)`);
|
|
3890
|
+
skillMap.set(skill.name, skill);
|
|
3891
|
+
realPathSet.add(realPath);
|
|
4002
3892
|
}
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
if (includeDefaults) {
|
|
3896
|
+
const userSkillsDir = join6(homedir4(), CONFIG_DIR_NAME, "skills");
|
|
3897
|
+
const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
|
|
3898
|
+
addSkills(loadSkillsFromDirInternal(userSkillsDir, "user", true));
|
|
3899
|
+
addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", true));
|
|
3900
|
+
}
|
|
3901
|
+
for (const rawPath of skillPaths) {
|
|
3902
|
+
const resolvedPath = resolveSkillPath(rawPath, cwd);
|
|
3903
|
+
if (!existsSync4(resolvedPath)) {
|
|
3904
|
+
allDiagnostics.push({ type: "warning", message: "skill path does not exist", path: resolvedPath });
|
|
3905
|
+
continue;
|
|
3906
|
+
}
|
|
3907
|
+
try {
|
|
3908
|
+
const stats = statSync(resolvedPath);
|
|
3909
|
+
if (stats.isDirectory()) {
|
|
3910
|
+
addSkills(loadSkillsFromDirInternal(resolvedPath, "path", true));
|
|
3911
|
+
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
|
3912
|
+
const result = loadSkillFromFile(resolvedPath, "path");
|
|
3913
|
+
if (result.skill) {
|
|
3914
|
+
addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
|
|
4013
3915
|
} else {
|
|
4014
|
-
|
|
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
|
-
}
|
|
3916
|
+
allDiagnostics.push(...result.diagnostics);
|
|
4025
3917
|
}
|
|
3918
|
+
} else {
|
|
3919
|
+
allDiagnostics.push({ type: "warning", message: "skill path is not a markdown file", path: resolvedPath });
|
|
4026
3920
|
}
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
content: result.content,
|
|
4031
|
-
is_error: result.isError
|
|
4032
|
-
});
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
const message = error instanceof Error ? error.message : "failed to read skill path";
|
|
3923
|
+
allDiagnostics.push({ type: "warning", message, path: resolvedPath });
|
|
4033
3924
|
}
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
3925
|
+
}
|
|
3926
|
+
return {
|
|
3927
|
+
skills: Array.from(skillMap.values()),
|
|
3928
|
+
diagnostics: [...allDiagnostics, ...collisionDiagnostics]
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
function escapeXml(str) {
|
|
3932
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3933
|
+
}
|
|
3934
|
+
function formatSkillsForPrompt(skills) {
|
|
3935
|
+
const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
|
|
3936
|
+
if (visibleSkills.length === 0) {
|
|
3937
|
+
return "";
|
|
3938
|
+
}
|
|
3939
|
+
const lines = [
|
|
3940
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
3941
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
3942
|
+
"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.",
|
|
3943
|
+
"",
|
|
3944
|
+
"<available_skills>"
|
|
3945
|
+
];
|
|
3946
|
+
for (const skill of visibleSkills) {
|
|
3947
|
+
lines.push(" <skill>");
|
|
3948
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
3949
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
3950
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
3951
|
+
if (skill.allowedTools?.length) {
|
|
3952
|
+
lines.push(` <allowed-tools>${escapeXml(skill.allowedTools.join(" "))}</allowed-tools>`);
|
|
3953
|
+
}
|
|
3954
|
+
lines.push(" </skill>");
|
|
3955
|
+
}
|
|
3956
|
+
lines.push("</available_skills>");
|
|
3957
|
+
return lines.join(`
|
|
3958
|
+
`);
|
|
3959
|
+
}
|
|
3960
|
+
// src/utils/system-prompt.ts
|
|
3961
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3962
|
+
import { join as join7 } from "path";
|
|
3963
|
+
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.
|
|
3964
|
+
|
|
3965
|
+
You are highly capable and can help users complete complex tasks that would otherwise be too difficult or time-consuming.`;
|
|
3966
|
+
var CODING_GUIDELINES = `# Coding Guidelines
|
|
3967
|
+
|
|
3968
|
+
- Read files before modifying them. Understand existing code before suggesting changes.
|
|
3969
|
+
- Make minimal, focused changes. Only modify what's directly requested or clearly necessary.
|
|
3970
|
+
- Don't over-engineer. Keep solutions simple. Don't add features, refactoring, or abstractions beyond what was asked.
|
|
3971
|
+
- Don't introduce security vulnerabilities (command injection, XSS, SQL injection, etc.).
|
|
3972
|
+
- Use the dedicated tools for file operations instead of shell commands:
|
|
3973
|
+
- Read files with the Read tool (not cat/head/tail)
|
|
3974
|
+
- Edit files with the Edit tool (not sed/awk)
|
|
3975
|
+
- Write files with the Write tool (not echo/cat heredoc)
|
|
3976
|
+
- Search files with Glob (not find/ls) and Grep (not grep/rg)
|
|
3977
|
+
- Use Bash only for system commands that truly require shell execution.`;
|
|
3978
|
+
var BASH_GUIDELINES = `# Bash Tool Guidelines
|
|
3979
|
+
|
|
3980
|
+
- Use for system commands, git operations, running scripts, and terminal tasks.
|
|
3981
|
+
- Always quote file paths with spaces.
|
|
3982
|
+
- Prefer absolute paths over cd + relative paths.
|
|
3983
|
+
- Don't run destructive commands without clear intent.
|
|
3984
|
+
- Capture output \u2014 the result is returned, not displayed interactively.
|
|
3985
|
+
- Commands timeout after 120s by default (max 600s).`;
|
|
3986
|
+
var EDIT_GUIDELINES = `# Edit Tool Guidelines
|
|
3987
|
+
|
|
3988
|
+
- The old_string must match exactly (including indentation and whitespace).
|
|
3989
|
+
- The old_string must be unique in the file, or the edit will fail. Provide more surrounding context to make it unique.
|
|
3990
|
+
- Use replace_all: true only when you want to replace every occurrence.`;
|
|
3991
|
+
var READ_GUIDELINES = `# Read Tool Guidelines
|
|
3992
|
+
|
|
3993
|
+
- Returns content with line numbers in cat -n format.
|
|
3994
|
+
- Use offset/limit for large files to read specific sections.
|
|
3995
|
+
- Lines longer than 2000 characters are truncated.`;
|
|
3996
|
+
var GLOB_GUIDELINES = `# Glob Tool Guidelines
|
|
3997
|
+
|
|
3998
|
+
- Use glob patterns like "**/*.ts" to find files by name.
|
|
3999
|
+
- Results are sorted by modification time (most recent first).`;
|
|
4000
|
+
var GREP_GUIDELINES = `# Grep Tool Guidelines
|
|
4001
|
+
|
|
4002
|
+
- Use regex patterns to search file contents.
|
|
4003
|
+
- Default output mode is "files_with_matches" (file paths only).
|
|
4004
|
+
- Use output_mode: "content" to see matching lines with context.
|
|
4005
|
+
- Use -i for case-insensitive search.`;
|
|
4006
|
+
var TOOL_SPECIFIC_GUIDELINES = {
|
|
4007
|
+
Bash: BASH_GUIDELINES,
|
|
4008
|
+
Edit: EDIT_GUIDELINES,
|
|
4009
|
+
Read: READ_GUIDELINES,
|
|
4010
|
+
Glob: GLOB_GUIDELINES,
|
|
4011
|
+
Grep: GREP_GUIDELINES
|
|
4012
|
+
};
|
|
4013
|
+
function buildSystemPrompt(context) {
|
|
4014
|
+
const sections = [CORE_IDENTITY];
|
|
4015
|
+
for (const toolName of context.tools) {
|
|
4016
|
+
const guidelines = TOOL_SPECIFIC_GUIDELINES[toolName];
|
|
4017
|
+
if (guidelines) {
|
|
4018
|
+
sections.push(guidelines);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
sections.push(CODING_GUIDELINES);
|
|
4022
|
+
if (context.cwd) {
|
|
4023
|
+
sections.push(`# Environment
|
|
4024
|
+
|
|
4025
|
+
Working directory: ${context.cwd}`);
|
|
4026
|
+
if (context.additionalDirectories && context.additionalDirectories.length > 0) {
|
|
4027
|
+
sections.push(`Additional allowed directories:
|
|
4028
|
+
${context.additionalDirectories.map((d) => `- ${d}`).join(`
|
|
4029
|
+
`)}`);
|
|
4030
|
+
}
|
|
4031
|
+
if (context.loadProjectInstructions) {
|
|
4032
|
+
const instructions = readProjectInstructions(context.cwd);
|
|
4033
|
+
if (instructions) {
|
|
4034
|
+
sections.push(`# Project Instructions
|
|
4035
|
+
|
|
4036
|
+
${instructions}`);
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
if (context.skills && context.skills.length > 0) {
|
|
4041
|
+
const skillsPrompt = formatSkillsForPrompt(context.skills);
|
|
4042
|
+
if (skillsPrompt) {
|
|
4043
|
+
sections.push(`# Skills
|
|
4044
|
+
|
|
4045
|
+
${skillsPrompt}`);
|
|
4037
4046
|
}
|
|
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
|
-
};
|
|
4049
4047
|
}
|
|
4048
|
+
if (context.customPrompt) {
|
|
4049
|
+
sections.push(context.customPrompt);
|
|
4050
|
+
}
|
|
4051
|
+
return sections.join(`
|
|
4052
|
+
|
|
4053
|
+
`);
|
|
4054
|
+
}
|
|
4055
|
+
function readProjectInstructions(cwd) {
|
|
4056
|
+
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
4057
|
+
try {
|
|
4058
|
+
const content = readFileSync5(join7(cwd, name), "utf-8").trim();
|
|
4059
|
+
if (content)
|
|
4060
|
+
return content;
|
|
4061
|
+
} catch {}
|
|
4062
|
+
}
|
|
4063
|
+
return null;
|
|
4050
4064
|
}
|
|
4051
4065
|
|
|
4052
4066
|
// src/query.ts
|
|
@@ -4343,7 +4357,9 @@ class McpClientManager {
|
|
|
4343
4357
|
if (server.status.status !== "connected")
|
|
4344
4358
|
continue;
|
|
4345
4359
|
for (const tool of server.tools) {
|
|
4346
|
-
const
|
|
4360
|
+
const config = this.configs[serverName];
|
|
4361
|
+
const prefix = "toolPrefix" in config ? config.toolPrefix : serverName;
|
|
4362
|
+
const namespacedName = prefix === "" ? tool.name : `${prefix}__${tool.name}`;
|
|
4347
4363
|
result.push({
|
|
4348
4364
|
name: namespacedName,
|
|
4349
4365
|
description: tool.description ?? `MCP tool ${tool.name} from ${serverName}`,
|