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.
Files changed (54) hide show
  1. package/README.md +126 -198
  2. package/dist/agent-loop.js +0 -15
  3. package/dist/agents/index.js +2557 -2543
  4. package/dist/agents/task-manager.js +0 -15
  5. package/dist/agents/tools.js +2557 -2543
  6. package/dist/api.js +1246 -1230
  7. package/dist/auth/gemini-oauth.js +0 -15
  8. package/dist/auth/login-openai.js +0 -15
  9. package/dist/auth/openai-oauth.js +0 -15
  10. package/dist/hooks.js +0 -15
  11. package/dist/index.js +1246 -1230
  12. package/dist/mcp/client.d.ts +1 -1
  13. package/dist/mcp/client.d.ts.map +1 -1
  14. package/dist/mcp/client.js +3 -16
  15. package/dist/mcp/index.js +3 -16
  16. package/dist/mcp/server.js +0 -15
  17. package/dist/mcp/types.d.ts +2 -0
  18. package/dist/mcp/types.d.ts.map +1 -1
  19. package/dist/memory/index.js +0 -15
  20. package/dist/memory/memory-handler.js +0 -15
  21. package/dist/permissions.js +0 -15
  22. package/dist/providers/anthropic.js +0 -15
  23. package/dist/providers/gemini.js +0 -15
  24. package/dist/providers/openai.d.ts +6 -0
  25. package/dist/providers/openai.d.ts.map +1 -1
  26. package/dist/providers/openai.js +36 -21
  27. package/dist/providers/registry.js +35 -21
  28. package/dist/query.js +0 -15
  29. package/dist/settings.js +0 -15
  30. package/dist/skills/frontmatter.js +0 -15
  31. package/dist/skills/index.js +0 -15
  32. package/dist/skills/skills.js +0 -15
  33. package/dist/tools/ask-user-question.js +0 -15
  34. package/dist/tools/bash.js +0 -15
  35. package/dist/tools/config.js +0 -15
  36. package/dist/tools/edit.js +0 -15
  37. package/dist/tools/exit-plan-mode.js +0 -15
  38. package/dist/tools/glob.js +0 -15
  39. package/dist/tools/grep.js +0 -15
  40. package/dist/tools/index.js +0 -15
  41. package/dist/tools/mcp-resources.js +0 -15
  42. package/dist/tools/notebook-edit.js +0 -15
  43. package/dist/tools/presets.js +0 -15
  44. package/dist/tools/read.js +0 -15
  45. package/dist/tools/registry.js +0 -15
  46. package/dist/tools/todo-write.js +0 -15
  47. package/dist/tools/web-fetch.js +0 -15
  48. package/dist/tools/web-search.js +0 -15
  49. package/dist/tools/write.js +0 -15
  50. package/dist/types.js +0 -15
  51. package/dist/utils/cost.js +0 -15
  52. package/dist/utils/session-store.js +0 -15
  53. package/dist/utils/system-prompt.js +0 -15
  54. 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 as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
44
- import { join as join2 } from "path";
45
- import { homedir as homedir2 } from "os";
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 ?? homedir2();
102
+ return process.env.HOME ?? homedir();
48
103
  }
49
104
  function tokenDir() {
50
- return join2(getHome(), ".fourmis");
105
+ return join(getHome(), ".fourmis");
51
106
  }
52
107
  function tokenPath() {
53
- return join2(tokenDir(), "openai-auth.json");
108
+ return join(tokenDir(), "openai-auth.json");
54
109
  }
55
110
  function codexFallbackPath() {
56
- return join2(getHome(), ".codex", "auth.json");
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 = readFileSync2(p, "utf-8");
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 (!existsSync2(dir)) {
73
- mkdirSync2(dir, { recursive: true });
127
+ if (!existsSync(dir)) {
128
+ mkdirSync(dir, { recursive: true });
74
129
  }
75
- writeFileSync2(tokenPath(), JSON.stringify(tokens, null, 2), { mode: 384 });
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 readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
326
- import { join as join3 } from "path";
327
- import { homedir as homedir3 } from "os";
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 ?? homedir3();
384
+ return process.env.HOME ?? homedir2();
330
385
  }
331
386
  function tokenPath2() {
332
- return join3(getHome2(), ".gemini", "oauth_creds.json");
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 = readFileSync3(p, "utf-8");
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 = join3(getHome2(), ".gemini");
351
- if (!existsSync3(dir)) {
352
- const { mkdirSync: mkdirSync3 } = __require("fs");
353
- mkdirSync3(dir, { recursive: true });
405
+ const dir = join2(getHome2(), ".gemini");
406
+ if (!existsSync2(dir)) {
407
+ const { mkdirSync: mkdirSync2 } = __require("fs");
408
+ mkdirSync2(dir, { recursive: true });
354
409
  }
355
- writeFileSync3(p, JSON.stringify(tokens, null, 2), { mode: 384 });
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/tools/mcp-resources.ts
410
- var exports_mcp_resources = {};
411
- __export(exports_mcp_resources, {
412
- createReadMcpResourceTool: () => createReadMcpResourceTool,
413
- createListMcpResourcesTool: () => createListMcpResourcesTool
414
- });
415
- function createListMcpResourcesTool(mcpClient) {
464
+ // src/types.ts
465
+ function uuid() {
466
+ return crypto.randomUUID();
467
+ }
468
+ function emptyTokenUsage() {
416
469
  return {
417
- name: "mcp__list_resources",
418
- description: "List available resources from MCP servers.",
419
- inputSchema: {
420
- type: "object",
421
- properties: {
422
- server: {
423
- type: "string",
424
- description: "Optional server name to filter by. If omitted, lists resources from all servers."
425
- }
426
- }
427
- },
428
- async execute(input) {
429
- const { server } = input ?? {};
430
- try {
431
- const resources = await mcpClient.listResources(server);
432
- if (resources.length === 0) {
433
- return { content: "No resources available." };
434
- }
435
- const lines = resources.map((r) => `[${r.server}] ${r.uri} - ${r.name}${r.description ? `: ${r.description}` : ""}`);
436
- return { content: lines.join(`
437
- `) };
438
- } catch (err) {
439
- const message = err instanceof Error ? err.message : String(err);
440
- return { content: `Error listing resources: ${message}`, isError: true };
441
- }
442
- }
470
+ inputTokens: 0,
471
+ outputTokens: 0,
472
+ cacheReadInputTokens: 0,
473
+ cacheCreationInputTokens: 0
443
474
  };
444
475
  }
445
- function createReadMcpResourceTool(mcpClient) {
476
+ function mergeUsage(a, b) {
446
477
  return {
447
- name: "mcp__read_resource",
448
- description: "Read a specific resource from an MCP server by URI.",
449
- inputSchema: {
450
- type: "object",
451
- properties: {
452
- server: {
453
- type: "string",
454
- description: "The MCP server name that hosts the resource."
455
- },
456
- uri: {
457
- type: "string",
458
- description: "The resource URI to read."
459
- }
460
- },
461
- required: ["server", "uri"]
462
- },
463
- async execute(input) {
464
- const { server, uri } = input;
465
- if (!server || !uri) {
466
- return { content: "Both 'server' and 'uri' are required.", isError: true };
467
- }
468
- try {
469
- const content = await mcpClient.readResource(server, uri);
470
- return { content };
471
- } catch (err) {
472
- const message = err instanceof Error ? err.message : String(err);
473
- return { content: `Error reading resource: ${message}`, isError: true };
474
- }
475
- }
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/permissions.ts
480
- var SAFE_TOOLS = new Set(["Read", "Glob", "Grep", "WebFetch", "WebSearch"]);
481
- var EDIT_TOOLS = new Set(["Write", "Edit", "NotebookEdit", "TodoWrite", "Config"]);
482
- var FS_COMMANDS = ["mkdir", "touch", "rm", "mv", "cp"];
483
- var DELEGATE_TOOLS = new Set(["Teammate", "Task", "TaskOutput", "TaskStop"]);
484
- function normalizeRules(rules) {
485
- if (!rules)
486
- return [];
487
- return rules.map((r) => typeof r === "string" ? { toolName: r } : r);
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 matchesRule(rules, toolName, input) {
490
- for (const rule of rules) {
491
- if (rule.toolName !== toolName)
492
- continue;
493
- if (!rule.ruleContent)
494
- return true;
495
- const inputStr = toolName === "Bash" ? String(input?.command ?? "") : JSON.stringify(input ?? {});
496
- if (inputStr.includes(rule.ruleContent))
497
- return true;
498
- }
499
- return false;
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
- class PermissionManager {
503
- mode;
504
- canUseTool;
505
- allowRules;
506
- denyRules;
507
- settingsManager;
508
- constructor(mode = "default", canUseTool, permissions, settingsManager) {
509
- this.mode = mode;
510
- this.canUseTool = canUseTool;
511
- this.allowRules = normalizeRules(permissions?.allow);
512
- this.denyRules = normalizeRules(permissions?.deny);
513
- this.settingsManager = settingsManager;
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
- async check(toolName, input, options) {
516
- if (this.mode === "bypassPermissions") {
517
- return { behavior: "allow" };
518
- }
519
- if (matchesRule(this.denyRules, toolName, input)) {
520
- return {
521
- behavior: "deny",
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
- applyPermissionUpdates(updates) {
580
- for (const update of updates) {
581
- if (this.settingsManager && update.destination !== "session" && update.destination !== "cliArg") {
582
- this.settingsManager.persistUpdate(update);
583
- }
584
- switch (update.type) {
585
- case "addRules":
586
- for (const rule of update.rules) {
587
- if (update.behavior === "allow") {
588
- this.allowRules.push(rule);
589
- } else if (update.behavior === "deny") {
590
- this.denyRules.push(rule);
591
- }
592
- }
593
- break;
594
- case "removeRules":
595
- for (const rule of update.rules) {
596
- if (update.behavior === "allow") {
597
- this.allowRules = this.allowRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
598
- } else if (update.behavior === "deny") {
599
- this.denyRules = this.denyRules.filter((r) => !(r.toolName === rule.toolName && r.ruleContent === rule.ruleContent));
600
- }
601
- }
602
- break;
603
- case "replaceRules":
604
- if (update.behavior === "allow") {
605
- this.allowRules = [...update.rules];
606
- } else if (update.behavior === "deny") {
607
- this.denyRules = [...update.rules];
608
- }
609
- break;
610
- case "setMode":
611
- this.mode = update.mode;
612
- break;
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
- setMode(mode) {
617
- this.mode = mode;
618
- }
619
- getMode() {
620
- return this.mode;
572
+ const messages = [
573
+ ...previousMessages ?? [],
574
+ { role: "user", content: prompt }
575
+ ];
576
+ if (sessionLogger) {
577
+ sessionLogger("user", prompt, null);
621
578
  }
622
- }
623
-
624
- // src/settings.ts
625
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
626
- import { dirname, join } from "path";
627
- import { homedir } from "os";
628
-
629
- class SettingsManager {
630
- cwd;
631
- constructor(cwd) {
632
- this.cwd = cwd;
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
- loadPermissions(sources) {
635
- const allAllow = [];
636
- const allDeny = [];
637
- for (const source of sources) {
638
- const path = this.sourceToPath(source);
639
- const data = this.readJson(path);
640
- if (!data?.permissions)
641
- continue;
642
- const perms = data.permissions;
643
- if (Array.isArray(perms.allow)) {
644
- for (const rule of perms.allow) {
645
- if (typeof rule === "string") {
646
- allAllow.push(this.parseRule(rule));
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
- persistUpdate(update) {
666
- const path = this.destinationToPath(update.destination);
667
- if (!path)
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
- const dir = dirname(path);
705
- if (!existsSync(dir))
706
- mkdirSync(dir, { recursive: true });
707
- writeFileSync(path, JSON.stringify(data, null, 2) + `
708
- `);
709
- }
710
- sourceToPath(source) {
711
- switch (source) {
712
- case "user":
713
- return join(homedir(), ".claude", "settings.json");
714
- case "project":
715
- return join(this.cwd, ".claude", "settings.json");
716
- case "local":
717
- return join(this.cwd, ".claude", "settings.local.json");
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
- destinationToPath(destination) {
721
- switch (destination) {
722
- case "userSettings":
723
- return join(homedir(), ".claude", "settings.json");
724
- case "projectSettings":
725
- return join(this.cwd, ".claude", "settings.json");
726
- case "localSettings":
727
- return join(this.cwd, ".claude", "settings.local.json");
728
- default:
729
- return null;
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
- readJson(path) {
733
- try {
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 as dirname2 } from "path";
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 = dirname2(resolvedPath);
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 dirname3, join as join4 } from "path";
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 = join4(ctx.cwd, ".claude", "todos.json");
3235
+ const filePath = join3(ctx.cwd, ".claude", "todos.json");
2999
3236
  try {
3000
- await mkdir2(dirname3(filePath), { recursive: true });
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 join5, dirname as dirname4 } from "path";
3256
+ import { join as join4, dirname as dirname3 } from "path";
3020
3257
  function scopePath(cwd, scope) {
3021
3258
  if (scope === "project")
3022
- return join5(cwd, ".claude", "settings.json");
3023
- return join5(cwd, ".claude", "settings.local.json");
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(dirname4(filePath), { recursive: true });
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
- function normalizePath(input) {
3337
- const trimmed = input.trim();
3338
- if (trimmed === "~")
3339
- return homedir4();
3340
- if (trimmed.startsWith("~/"))
3341
- return join6(homedir4(), trimmed.slice(2));
3342
- if (trimmed.startsWith("~"))
3343
- return join6(homedir4(), trimmed.slice(1));
3344
- return trimmed;
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 resolveSkillPath(p, cwd) {
3347
- const normalized = normalizePath(p);
3348
- return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
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
- function loadSkills(options = {}) {
3351
- const { cwd = process.cwd(), skillPaths = [], includeDefaults = true } = options;
3352
- const skillMap = new Map;
3353
- const realPathSet = new Set;
3354
- const allDiagnostics = [];
3355
- const collisionDiagnostics = [];
3356
- function addSkills(result) {
3357
- allDiagnostics.push(...result.diagnostics);
3358
- for (const skill of result.skills) {
3359
- let realPath;
3360
- try {
3361
- realPath = realpathSync(skill.filePath);
3362
- } catch {
3363
- realPath = skill.filePath;
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
- if (realPathSet.has(realPath))
3366
- continue;
3367
- const existing = skillMap.get(skill.name);
3368
- if (existing) {
3369
- collisionDiagnostics.push({
3370
- type: "collision",
3371
- message: `name "${skill.name}" collision`,
3372
- path: skill.filePath,
3373
- collision: {
3374
- resourceType: "skill",
3375
- name: skill.name,
3376
- winnerPath: existing.filePath,
3377
- loserPath: skill.filePath
3378
- }
3379
- });
3380
- } else {
3381
- skillMap.set(skill.name, skill);
3382
- realPathSet.add(realPath);
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
- if (includeDefaults) {
3387
- const userSkillsDir = join6(homedir4(), CONFIG_DIR_NAME, "skills");
3388
- const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
3389
- addSkills(loadSkillsFromDirInternal(userSkillsDir, "user", true));
3390
- addSkills(loadSkillsFromDirInternal(projectSkillsDir, "project", true));
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
- try {
3399
- const stats = statSync(resolvedPath);
3400
- if (stats.isDirectory()) {
3401
- addSkills(loadSkillsFromDirInternal(resolvedPath, "path", true));
3402
- } else if (stats.isFile() && resolvedPath.endsWith(".md")) {
3403
- const result = loadSkillFromFile(resolvedPath, "path");
3404
- if (result.skill) {
3405
- addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
3406
- } else {
3407
- allDiagnostics.push(...result.diagnostics);
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
- return {
3418
- skills: Array.from(skillMap.values()),
3419
- diagnostics: [...allDiagnostics, ...collisionDiagnostics]
3420
- };
3421
- }
3422
- function escapeXml(str) {
3423
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3424
- }
3425
- function formatSkillsForPrompt(skills) {
3426
- const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
3427
- if (visibleSkills.length === 0) {
3428
- return "";
3429
- }
3430
- const lines = [
3431
- "The following skills provide specialized instructions for specific tasks.",
3432
- "Use the read tool to load a skill's file when the task matches its description.",
3433
- "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
3434
- "",
3435
- "<available_skills>"
3436
- ];
3437
- for (const skill of visibleSkills) {
3438
- lines.push(" <skill>");
3439
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
3440
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
3441
- lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
3442
- if (skill.allowedTools?.length) {
3443
- lines.push(` <allowed-tools>${escapeXml(skill.allowedTools.join(" "))}</allowed-tools>`);
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
- lines.push("</available_skills>");
3448
- return lines.join(`
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
- - Use glob patterns like "**/*.ts" to find files by name.
3490
- - Results are sorted by modification time (most recent first).`;
3491
- var GREP_GUIDELINES = `# Grep Tool Guidelines
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
- - Use regex patterns to search file contents.
3494
- - Default output mode is "files_with_matches" (file paths only).
3495
- - Use output_mode: "content" to see matching lines with context.
3496
- - Use -i for case-insensitive search.`;
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
- sections.push(CODING_GUIDELINES);
3513
- if (context.cwd) {
3514
- sections.push(`# Environment
3515
-
3516
- Working directory: ${context.cwd}`);
3517
- if (context.additionalDirectories && context.additionalDirectories.length > 0) {
3518
- sections.push(`Additional allowed directories:
3519
- ${context.additionalDirectories.map((d) => `- ${d}`).join(`
3520
- `)}`);
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
- if (context.loadProjectInstructions) {
3523
- const instructions = readProjectInstructions(context.cwd);
3524
- if (instructions) {
3525
- sections.push(`# Project Instructions
3526
-
3527
- ${instructions}`);
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
- if (context.skills && context.skills.length > 0) {
3532
- const skillsPrompt = formatSkillsForPrompt(context.skills);
3533
- if (skillsPrompt) {
3534
- sections.push(`# Skills
3535
-
3536
- ${skillsPrompt}`);
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
- if (context.customPrompt) {
3540
- sections.push(context.customPrompt);
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
- return sections.join(`
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 = readFileSync5(join7(cwd, name), "utf-8").trim();
3550
- if (content)
3551
- return content;
3552
- } catch {}
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/agent-loop.ts
3558
- function makeModelUsageEntry() {
3559
- return {
3560
- inputTokens: 0,
3561
- outputTokens: 0,
3562
- cacheReadInputTokens: 0,
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 makeErrorResult(params) {
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
- type: "result",
3570
- subtype: params.subtype,
3571
- duration_ms: Date.now() - params.startTime,
3572
- duration_api_ms: params.apiTimeMs,
3573
- is_error: true,
3574
- num_turns: params.turns,
3575
- stop_reason: null,
3576
- total_cost_usd: params.costUsd,
3577
- usage: params.usage,
3578
- modelUsage: params.modelUsage,
3579
- permission_denials: params.permissionDenials,
3580
- errors: params.errors,
3581
- uuid: uuid(),
3582
- session_id: params.sessionId
3688
+ yamlString: normalized.slice(4, endIndex),
3689
+ body: normalized.slice(endIndex + 4).trim()
3583
3690
  };
3584
3691
  }
3585
- function extractStructuredJson(text) {
3586
- const trimmed = text.trim();
3587
- if (!trimmed) {
3588
- return { ok: false, error: "Empty result text; expected JSON output." };
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
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
3591
- const candidate = fenced ? fenced[1].trim() : trimmed;
3592
- try {
3593
- return { ok: true, value: JSON.parse(candidate) };
3594
- } catch (err) {
3595
- const message = err instanceof Error ? err.message : String(err);
3596
- return { ok: false, error: `Invalid JSON output: ${message}` };
3720
+ if (name.length > MAX_NAME_LENGTH) {
3721
+ errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
3597
3722
  }
3598
- }
3599
- async function* agentLoop(prompt, options) {
3600
- const {
3601
- provider,
3602
- model,
3603
- fallbackModel,
3604
- modelState,
3605
- maxThinkingTokensState,
3606
- thinking,
3607
- effort,
3608
- outputFormat,
3609
- systemPrompt,
3610
- tools,
3611
- permissions,
3612
- cwd,
3613
- sessionId,
3614
- maxTurns,
3615
- maxBudgetUsd,
3616
- includePartialMessages,
3617
- signal,
3618
- env,
3619
- debug,
3620
- hooks,
3621
- mcpClient,
3622
- previousMessages,
3623
- sessionLogger,
3624
- nativeMemoryTool,
3625
- initMeta
3626
- } = options;
3627
- const effectiveModelState = modelState ?? { current: model };
3628
- const startTime = Date.now();
3629
- let apiTimeMs = 0;
3630
- let turns = 0;
3631
- let totalUsage = emptyTokenUsage();
3632
- let costUsd = 0;
3633
- const modelUsage = {};
3634
- const permissionDenials = [];
3635
- if (mcpClient) {
3636
- await mcpClient.connectAll();
3637
- for (const tool of mcpClient.getTools()) {
3638
- tools.register(tool);
3639
- }
3640
- const { createListMcpResourcesTool: createListMcpResourcesTool2, createReadMcpResourceTool: createReadMcpResourceTool2 } = await Promise.resolve().then(() => exports_mcp_resources);
3641
- tools.register(createListMcpResourcesTool2(mcpClient));
3642
- tools.register(createReadMcpResourceTool2(mcpClient));
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
- const messages = [
3645
- ...previousMessages ?? [],
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
- yield {
3652
- type: "system",
3653
- subtype: "init",
3654
- apiKeySource: "user",
3655
- claude_code_version: "fourmis-agent-sdk",
3656
- session_id: sessionId,
3657
- model: effectiveModelState.current,
3658
- tools: tools.list(),
3659
- cwd,
3660
- mcp_servers: (mcpClient?.status() ?? []).map((s) => ({ name: s.name, status: s.status })),
3661
- permissionMode: permissions.getMode(),
3662
- agents: initMeta?.agents,
3663
- betas: initMeta?.betas,
3664
- slash_commands: initMeta?.slashCommands ?? [],
3665
- output_style: initMeta?.outputStyle ?? "default",
3666
- skills: initMeta?.skills ?? [],
3667
- plugins: (initMeta?.plugins ?? []).map((p) => ({ name: p.path.split("/").pop() ?? p.path, path: p.path })),
3668
- uuid: uuid()
3669
- };
3670
- if (hooks) {
3671
- await hooks.fire("Setup", {
3672
- event: "Setup",
3673
- hook_event_name: "Setup",
3674
- trigger: "init",
3675
- session_id: sessionId,
3676
- cwd,
3677
- permission_mode: permissions.getMode()
3678
- }, undefined, { signal });
3729
+ if (name.includes("--")) {
3730
+ errors.push(`name must not contain consecutive hyphens`);
3679
3731
  }
3680
- if (hooks) {
3681
- await hooks.fire("SessionStart", {
3682
- event: "SessionStart",
3683
- hook_event_name: "SessionStart",
3684
- session_id: sessionId,
3685
- source: "startup",
3686
- model: effectiveModelState.current,
3687
- cwd,
3688
- permission_mode: permissions.getMode()
3689
- }, undefined, { signal });
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
- while (true) {
3692
- if (signal.aborted) {
3693
- yield makeErrorResult({
3694
- subtype: "error_during_execution",
3695
- errors: ["Aborted"],
3696
- turns,
3697
- costUsd,
3698
- sessionId,
3699
- startTime,
3700
- apiTimeMs,
3701
- usage: totalUsage,
3702
- modelUsage,
3703
- permissionDenials
3704
- });
3705
- return;
3706
- }
3707
- if (turns >= maxTurns) {
3708
- yield makeErrorResult({
3709
- subtype: "error_max_turns",
3710
- errors: [`Reached maximum turns (${maxTurns})`],
3711
- turns,
3712
- costUsd,
3713
- sessionId,
3714
- startTime,
3715
- apiTimeMs,
3716
- usage: totalUsage,
3717
- modelUsage,
3718
- permissionDenials
3719
- });
3720
- return;
3721
- }
3722
- if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
3723
- yield makeErrorResult({
3724
- subtype: "error_max_budget_usd",
3725
- errors: [],
3726
- turns,
3727
- costUsd,
3728
- sessionId,
3729
- startTime,
3730
- apiTimeMs,
3731
- usage: totalUsage,
3732
- modelUsage,
3733
- permissionDenials
3734
- });
3735
- return;
3736
- }
3737
- const activeModel = effectiveModelState.current;
3738
- const toolDefs = tools.getDefinitions();
3739
- const apiStart = Date.now();
3740
- let assistantTextParts = [];
3741
- const toolCalls = [];
3742
- let turnUsage = emptyTokenUsage();
3743
- let turnStopReason = null;
3744
- const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
3745
- try {
3746
- const chunks = provider.chat({
3747
- model: activeModel,
3748
- messages,
3749
- tools: toolDefs.length > 0 ? toolDefs : undefined,
3750
- systemPrompt,
3751
- signal,
3752
- nativeTools,
3753
- thinkingBudget: maxThinkingTokensState?.current,
3754
- thinking,
3755
- effort,
3756
- outputFormat
3757
- });
3758
- for await (const chunk of chunks) {
3759
- switch (chunk.type) {
3760
- case "text_delta":
3761
- assistantTextParts.push(chunk.text);
3762
- if (includePartialMessages) {
3763
- yield {
3764
- type: "stream_event",
3765
- event: { type: "text_delta", text: chunk.text },
3766
- parent_tool_use_id: null,
3767
- uuid: uuid(),
3768
- session_id: sessionId
3769
- };
3770
- }
3771
- break;
3772
- case "thinking_delta":
3773
- if (includePartialMessages) {
3774
- yield {
3775
- type: "stream_event",
3776
- event: { type: "thinking_delta", thinking: chunk.text },
3777
- parent_tool_use_id: null,
3778
- uuid: uuid(),
3779
- session_id: sessionId
3780
- };
3781
- }
3782
- break;
3783
- case "tool_call":
3784
- toolCalls.push({ id: chunk.id, name: chunk.name, input: chunk.input });
3785
- break;
3786
- case "usage":
3787
- turnUsage = mergeUsage(turnUsage, chunk.usage);
3788
- break;
3789
- case "done":
3790
- turnStopReason = chunk.stopReason ?? null;
3791
- break;
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
- } catch (err) {
3795
- const message = err instanceof Error ? err.message : String(err);
3796
- if (fallbackModel && activeModel !== fallbackModel) {
3797
- effectiveModelState.current = fallbackModel;
3798
- yield {
3799
- type: "system",
3800
- subtype: "status",
3801
- status: null,
3802
- permissionMode: permissions.getMode(),
3803
- uuid: uuid(),
3804
- session_id: sessionId
3805
- };
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
- yield makeErrorResult({
3809
- subtype: "error_during_execution",
3810
- errors: [`API error: ${message}`],
3811
- turns,
3812
- costUsd,
3813
- sessionId,
3814
- startTime,
3815
- apiTimeMs,
3816
- usage: totalUsage,
3817
- modelUsage,
3818
- permissionDenials
3819
- });
3820
- return;
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
- apiTimeMs += Date.now() - apiStart;
3823
- turns++;
3824
- totalUsage = mergeUsage(totalUsage, turnUsage);
3825
- const turnCost = provider.calculateCost(activeModel, turnUsage);
3826
- costUsd += turnCost;
3827
- if (!modelUsage[activeModel]) {
3828
- modelUsage[activeModel] = makeModelUsageEntry();
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
- modelUsage[activeModel].inputTokens += turnUsage.inputTokens;
3831
- modelUsage[activeModel].outputTokens += turnUsage.outputTokens;
3832
- modelUsage[activeModel].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
3833
- modelUsage[activeModel].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
3834
- modelUsage[activeModel].totalCostUsd += turnCost;
3835
- modelUsage[activeModel].webSearchRequests = (modelUsage[activeModel].webSearchRequests ?? 0) + (turnUsage.webSearchRequests ?? 0);
3836
- modelUsage[activeModel].costUSD = modelUsage[activeModel].totalCostUsd;
3837
- modelUsage[activeModel].contextWindow = provider.getContextWindow(activeModel);
3838
- const assistantText = assistantTextParts.join("");
3839
- const assistantContent = [];
3840
- if (assistantText) {
3841
- assistantContent.push({ type: "text", text: assistantText });
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
- for (const call of toolCalls) {
3844
- assistantContent.push({
3845
- type: "tool_use",
3846
- id: call.id,
3847
- name: call.name,
3848
- input: call.input
3849
- });
3815
+ const compatErrors = validateCompatibility(frontmatter.compatibility);
3816
+ for (const error of compatErrors) {
3817
+ diagnostics.push({ type: "warning", message: error, path: filePath });
3850
3818
  }
3851
- messages.push({ role: "assistant", content: assistantContent });
3852
- if (sessionLogger) {
3853
- sessionLogger("assistant", assistantContent, null);
3819
+ if (!frontmatter.description || frontmatter.description.trim() === "") {
3820
+ return { skill: null, diagnostics };
3854
3821
  }
3855
- yield {
3856
- type: "assistant",
3857
- message: {
3858
- role: "assistant",
3859
- content: assistantContent
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
- parent_tool_use_id: null,
3862
- uuid: uuid(),
3863
- session_id: sessionId
3837
+ diagnostics
3864
3838
  };
3865
- if (toolCalls.length === 0) {
3866
- let structuredOutput;
3867
- if (outputFormat?.type === "json_schema") {
3868
- const parsed = extractStructuredJson(assistantText);
3869
- if (!parsed.ok) {
3870
- yield makeErrorResult({
3871
- subtype: "error_max_structured_output_retries",
3872
- errors: [parsed.error],
3873
- turns,
3874
- costUsd,
3875
- sessionId,
3876
- startTime,
3877
- apiTimeMs,
3878
- usage: totalUsage,
3879
- modelUsage,
3880
- permissionDenials
3881
- });
3882
- return;
3883
- }
3884
- structuredOutput = parsed.value;
3885
- }
3886
- if (hooks) {
3887
- await hooks.fire("Stop", {
3888
- event: "Stop",
3889
- hook_event_name: "Stop",
3890
- session_id: sessionId,
3891
- text: assistantText || undefined,
3892
- stop_reason: turnStopReason ?? undefined
3893
- }, undefined, {
3894
- signal
3895
- });
3896
- }
3897
- if (hooks) {
3898
- await hooks.fire("SessionEnd", {
3899
- event: "SessionEnd",
3900
- hook_event_name: "SessionEnd",
3901
- session_id: sessionId,
3902
- reason: "other"
3903
- }, undefined, { signal });
3904
- }
3905
- yield {
3906
- type: "result",
3907
- subtype: "success",
3908
- duration_ms: Date.now() - startTime,
3909
- duration_api_ms: apiTimeMs,
3910
- is_error: false,
3911
- num_turns: turns,
3912
- result: assistantText,
3913
- stop_reason: turnStopReason,
3914
- total_cost_usd: costUsd,
3915
- usage: totalUsage,
3916
- modelUsage,
3917
- permission_denials: permissionDenials,
3918
- structured_output: structuredOutput,
3919
- uuid: uuid(),
3920
- session_id: sessionId
3921
- };
3922
- return;
3923
- }
3924
- const toolResults = [];
3925
- for (const call of toolCalls) {
3926
- let hookDenied = false;
3927
- let hookUpdatedInput;
3928
- if (hooks) {
3929
- const hookResult = await hooks.fire("PreToolUse", {
3930
- event: "PreToolUse",
3931
- hook_event_name: "PreToolUse",
3932
- tool_name: call.name,
3933
- tool_input: call.input,
3934
- session_id: sessionId
3935
- }, call.id, { signal });
3936
- if (hookResult) {
3937
- if (hookResult.permissionDecision === "deny") {
3938
- hookDenied = true;
3939
- }
3940
- if (hookResult.updatedInput !== undefined) {
3941
- hookUpdatedInput = hookResult.updatedInput;
3942
- }
3943
- }
3944
- }
3945
- if (hookDenied) {
3946
- const denyContent = "Denied by hook";
3947
- toolResults.push({
3948
- type: "tool_result",
3949
- tool_use_id: call.id,
3950
- content: denyContent,
3951
- is_error: true
3952
- });
3953
- if (hooks) {
3954
- await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
3955
- }
3956
- continue;
3957
- }
3958
- const inputAfterHook = hookUpdatedInput !== undefined ? hookUpdatedInput : call.input;
3959
- const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
3960
- if (permResult.behavior === "deny") {
3961
- const denyContent = `Permission denied: ${permResult.message}`;
3962
- permissionDenials.push({
3963
- tool_name: call.name,
3964
- tool_use_id: call.id,
3965
- tool_input: inputAfterHook ?? {}
3966
- });
3967
- toolResults.push({
3968
- type: "tool_result",
3969
- tool_use_id: call.id,
3970
- content: denyContent,
3971
- is_error: true
3972
- });
3973
- if (hooks) {
3974
- await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
3975
- }
3976
- continue;
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
- const toolInput = permResult.behavior === "allow" && permResult.updatedInput ? permResult.updatedInput : inputAfterHook;
3979
- let result;
3980
- if (call.name === "memory" && nativeMemoryTool) {
3981
- try {
3982
- const content = await nativeMemoryTool.execute(toolInput);
3983
- result = { content, isError: content.startsWith("Error:") };
3984
- } catch (err) {
3985
- const message = err instanceof Error ? err.message : String(err);
3986
- result = { content: `Error: ${message}`, isError: true };
3987
- }
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
- const toolCtx = {
3990
- cwd,
3991
- signal,
3992
- sessionId,
3993
- env
3994
- };
3995
- result = await tools.execute(call.name, toolInput, toolCtx);
3996
- }
3997
- if (call.name === "ExitPlanMode") {
3998
- permissions.setMode("default");
3999
- }
4000
- if (debug) {
4001
- console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
3890
+ skillMap.set(skill.name, skill);
3891
+ realPathSet.add(realPath);
4002
3892
  }
4003
- if (hooks) {
4004
- if (result.isError) {
4005
- await hooks.fire("PostToolUseFailure", {
4006
- event: "PostToolUseFailure",
4007
- hook_event_name: "PostToolUseFailure",
4008
- tool_name: call.name,
4009
- tool_result: result.content,
4010
- tool_error: true,
4011
- session_id: sessionId
4012
- }, call.id, { signal });
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
- const postResult = await hooks.fire("PostToolUse", {
4015
- event: "PostToolUse",
4016
- hook_event_name: "PostToolUse",
4017
- tool_name: call.name,
4018
- tool_result: result.content,
4019
- session_id: sessionId
4020
- }, call.id, { signal });
4021
- if (postResult?.additionalContext) {
4022
- result.content += `
4023
- ${postResult.additionalContext}`;
4024
- }
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
- toolResults.push({
4028
- type: "tool_result",
4029
- tool_use_id: call.id,
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
- messages.push({ role: "user", content: toolResults });
4035
- if (sessionLogger) {
4036
- sessionLogger("user", toolResults, null);
3925
+ }
3926
+ return {
3927
+ skills: Array.from(skillMap.values()),
3928
+ diagnostics: [...allDiagnostics, ...collisionDiagnostics]
3929
+ };
3930
+ }
3931
+ function escapeXml(str) {
3932
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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 namespacedName = `mcp__${serverName}__${tool.name}`;
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}`,