@dreb/coding-agent 2.33.0 → 2.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +2 -0
  2. package/dist/core/tools/bash.d.ts.map +1 -1
  3. package/dist/core/tools/bash.js +4 -4
  4. package/dist/core/tools/bash.js.map +1 -1
  5. package/dist/core/tools/edit.d.ts.map +1 -1
  6. package/dist/core/tools/edit.js +2 -2
  7. package/dist/core/tools/edit.js.map +1 -1
  8. package/dist/core/tools/find.d.ts.map +1 -1
  9. package/dist/core/tools/find.js +2 -2
  10. package/dist/core/tools/find.js.map +1 -1
  11. package/dist/core/tools/grep.d.ts.map +1 -1
  12. package/dist/core/tools/grep.js +2 -2
  13. package/dist/core/tools/grep.js.map +1 -1
  14. package/dist/core/tools/ls.d.ts.map +1 -1
  15. package/dist/core/tools/ls.js +2 -2
  16. package/dist/core/tools/ls.js.map +1 -1
  17. package/dist/core/tools/read.d.ts.map +1 -1
  18. package/dist/core/tools/read.js +2 -2
  19. package/dist/core/tools/read.js.map +1 -1
  20. package/dist/core/tools/search.d.ts.map +1 -1
  21. package/dist/core/tools/search.js +2 -2
  22. package/dist/core/tools/search.js.map +1 -1
  23. package/dist/core/tools/skill.d.ts.map +1 -1
  24. package/dist/core/tools/skill.js +2 -2
  25. package/dist/core/tools/skill.js.map +1 -1
  26. package/dist/core/tools/subagent.d.ts.map +1 -1
  27. package/dist/core/tools/subagent.js +2 -2
  28. package/dist/core/tools/subagent.js.map +1 -1
  29. package/dist/core/tools/suggest-next.d.ts.map +1 -1
  30. package/dist/core/tools/suggest-next.js +5 -5
  31. package/dist/core/tools/suggest-next.js.map +1 -1
  32. package/dist/core/tools/tasks.d.ts.map +1 -1
  33. package/dist/core/tools/tasks.js +2 -2
  34. package/dist/core/tools/tasks.js.map +1 -1
  35. package/dist/core/tools/wait.d.ts.map +1 -1
  36. package/dist/core/tools/wait.js +2 -2
  37. package/dist/core/tools/wait.js.map +1 -1
  38. package/dist/core/tools/web.d.ts.map +1 -1
  39. package/dist/core/tools/web.js +4 -4
  40. package/dist/core/tools/web.js.map +1 -1
  41. package/dist/core/tools/write.d.ts.map +1 -1
  42. package/dist/core/tools/write.js +1 -1
  43. package/dist/core/tools/write.js.map +1 -1
  44. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  45. package/dist/modes/interactive/components/assistant-message.js +5 -10
  46. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  47. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/bash-execution.js +4 -4
  49. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  50. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  51. package/dist/modes/interactive/components/branch-summary-message.js +1 -1
  52. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  53. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  54. package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  55. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  56. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/custom-message.js +1 -1
  58. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  59. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/skill-invocation-message.js +1 -1
  61. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  62. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/tool-execution.js +2 -2
  64. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  65. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/user-message.js +1 -1
  67. package/dist/modes/interactive/components/user-message.js.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  69. package/dist/modes/interactive/interactive-mode.js +29 -29
  70. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  71. package/package.json +1 -1
@@ -64,14 +64,14 @@ export function createSuggestNextToolDefinition(onSuggest) {
64
64
  };
65
65
  },
66
66
  renderCall(args, theme, context) {
67
- const text = context.lastComponent ?? new Text("", 0, 0);
67
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
68
68
  text.setText(formatSuggestNextCall(args, theme));
69
69
  return text;
70
70
  },
71
71
  renderResult(result, _options, theme, context) {
72
72
  const details = result.details;
73
73
  if (!details) {
74
- const text = context.lastComponent ?? new Text("", 0, 0);
74
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
75
75
  const content = result.content?.[0];
76
76
  const msg = content?.type === "text" && content.text ? content.text : "";
77
77
  text.setText(theme.fg("toolOutput", msg));
@@ -80,11 +80,11 @@ export function createSuggestNextToolDefinition(onSuggest) {
80
80
  if (details.summary) {
81
81
  const container = context.lastComponent ?? new Container();
82
82
  container.clear();
83
- container.addChild(new Markdown(details.summary, 0, 0, getMarkdownTheme()));
84
- container.addChild(new Text(theme.fg("toolOutput", `→ ${details.suggestion}`), 0, 0));
83
+ container.addChild(new Markdown(details.summary, 0, 0, getMarkdownTheme(), undefined, true));
84
+ container.addChild(new Text(theme.fg("toolOutput", `→ ${details.suggestion}`), 0, 0, undefined, true));
85
85
  return container;
86
86
  }
87
- const text = context.lastComponent ?? new Text("", 0, 0);
87
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
88
88
  text.setText(theme.fg("toolOutput", `→ ${details.suggestion}`));
89
89
  return text;
90
90
  },
@@ -1 +1 @@
1
- {"version":3,"file":"suggest-next.js","sourceRoot":"","sources":["../../../src/core/tools/suggest-next.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Suggest next command tool.\n *\n * Allows the agent to suggest a command the user might want to run next.\n * The suggestion is shown as ghost text in the editor prompt (Tab to accept).\n */\n\nimport { Container, Markdown, Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { getMarkdownTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\nexport interface SuggestNextDetails {\n\tsuggestion: string;\n\tsummary?: string;\n}\n\nexport type SuggestNextCallback = (suggestion: string) => void;\n\n// ============================================================================\n// Schema\n\nconst suggestNextSchema = Type.Object({\n\tcommand: Type.String({\n\t\tdescription: \"The suggested command for the user to run next (e.g. /skill:mach6-push, /compact, npm run build)\",\n\t}),\n\tsummary: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Brief markdown summary of the work done this turn. Displayed to the user as the final message before the suggestion.\",\n\t\t}),\n\t),\n});\n\nexport type SuggestNextInput = Static<typeof suggestNextSchema>;\n\n// ============================================================================\n// Render helpers\n\nfunction formatSuggestNextCall(args: { command?: string } | undefined, theme: any): string {\n\tconst cmd = args?.command ?? \"\";\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"suggest_next\"))} ${theme.fg(\"accent\", cmd)}`;\n}\n\n// ============================================================================\n// Tool definition factory\n\nexport function createSuggestNextToolDefinition(\n\tonSuggest: SuggestNextCallback,\n): ToolDefinition<typeof suggestNextSchema, SuggestNextDetails | undefined> {\n\treturn {\n\t\tname: \"suggest_next\",\n\t\tlabel: \"suggest_next\",\n\t\tdescription:\n\t\t\t\"Suggest a command for the user to run next. Shows as ghost text in the prompt that the user can Tab-accept.\",\n\n\t\tparameters: suggestNextSchema,\n\n\t\tpromptSnippet: \"Suggest a next command (shown as ghost text the user can Tab-accept)\",\n\n\t\tpromptGuidelines: [\n\t\t\t\"Call suggest_next at the end of your turn when there's a clear next action the user might want\",\n\t\t\t\"Suggest a command the user can run: /skill:name args, /compact, npm run build, etc.\",\n\t\t\t\"Only suggest one command — pick the most likely next step\",\n\t\t\t\"Don't suggest if the conversation is open-ended with no obvious next action\",\n\t\t\t\"Include a brief summary of work done in the `summary` parameter — this is your last chance to communicate before the turn ends\",\n\t\t\t\"Calling this tool ends your turn automatically — do not call wait afterwards\",\n\t\t],\n\n\t\tasync execute(_toolCallId, { command: rawCommand, summary }: SuggestNextInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\t// Strip control characters (newlines, tabs, etc.) that would corrupt TUI rendering\n\t\t\tconst command = rawCommand?.replace(/[\\x00-\\x1f\\x7f]/g, \"\").trim();\n\t\t\tif (!command) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: \"Error: command is empty\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t\tendTurn: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tonSuggest(command);\n\n\t\t\t// Convert literal \\n sequences to actual newlines (LLMs emit these in XML tool calls),\n\t\t\t// then strip control characters (preserve only newlines for markdown)\n\t\t\tconst sanitizedSummary =\n\t\t\t\tsummary\n\t\t\t\t\t?.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t\t.replace(/[\\x00-\\x09\\x0b-\\x1f\\x7f]/g, \"\")\n\t\t\t\t\t.trim() || undefined;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: `Suggestion registered: ${command}` }],\n\t\t\t\tdetails: { suggestion: command, summary: sanitizedSummary },\n\t\t\t\tendTurn: true,\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSuggestNextCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst details = (result as any).details as SuggestNextDetails | undefined;\n\t\t\tif (!details) {\n\t\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t\tconst content = result.content?.[0];\n\t\t\t\tconst msg = content?.type === \"text\" && content.text ? content.text : \"\";\n\t\t\t\ttext.setText(theme.fg(\"toolOutput\", msg));\n\t\t\t\treturn text;\n\t\t\t}\n\n\t\t\tif (details.summary) {\n\t\t\t\tconst container = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcontainer.clear();\n\t\t\t\tcontainer.addChild(new Markdown(details.summary, 0, 0, getMarkdownTheme()));\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"toolOutput\", `→ ${details.suggestion}`), 0, 0));\n\t\t\t\treturn container;\n\t\t\t}\n\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(theme.fg(\"toolOutput\", `→ ${details.suggestion}`));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"suggest-next.js","sourceRoot":"","sources":["../../../src/core/tools/suggest-next.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Suggest next command tool.\n *\n * Allows the agent to suggest a command the user might want to run next.\n * The suggestion is shown as ghost text in the editor prompt (Tab to accept).\n */\n\nimport { Container, Markdown, Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { getMarkdownTheme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\nexport interface SuggestNextDetails {\n\tsuggestion: string;\n\tsummary?: string;\n}\n\nexport type SuggestNextCallback = (suggestion: string) => void;\n\n// ============================================================================\n// Schema\n\nconst suggestNextSchema = Type.Object({\n\tcommand: Type.String({\n\t\tdescription: \"The suggested command for the user to run next (e.g. /skill:mach6-push, /compact, npm run build)\",\n\t}),\n\tsummary: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Brief markdown summary of the work done this turn. Displayed to the user as the final message before the suggestion.\",\n\t\t}),\n\t),\n});\n\nexport type SuggestNextInput = Static<typeof suggestNextSchema>;\n\n// ============================================================================\n// Render helpers\n\nfunction formatSuggestNextCall(args: { command?: string } | undefined, theme: any): string {\n\tconst cmd = args?.command ?? \"\";\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"suggest_next\"))} ${theme.fg(\"accent\", cmd)}`;\n}\n\n// ============================================================================\n// Tool definition factory\n\nexport function createSuggestNextToolDefinition(\n\tonSuggest: SuggestNextCallback,\n): ToolDefinition<typeof suggestNextSchema, SuggestNextDetails | undefined> {\n\treturn {\n\t\tname: \"suggest_next\",\n\t\tlabel: \"suggest_next\",\n\t\tdescription:\n\t\t\t\"Suggest a command for the user to run next. Shows as ghost text in the prompt that the user can Tab-accept.\",\n\n\t\tparameters: suggestNextSchema,\n\n\t\tpromptSnippet: \"Suggest a next command (shown as ghost text the user can Tab-accept)\",\n\n\t\tpromptGuidelines: [\n\t\t\t\"Call suggest_next at the end of your turn when there's a clear next action the user might want\",\n\t\t\t\"Suggest a command the user can run: /skill:name args, /compact, npm run build, etc.\",\n\t\t\t\"Only suggest one command — pick the most likely next step\",\n\t\t\t\"Don't suggest if the conversation is open-ended with no obvious next action\",\n\t\t\t\"Include a brief summary of work done in the `summary` parameter — this is your last chance to communicate before the turn ends\",\n\t\t\t\"Calling this tool ends your turn automatically — do not call wait afterwards\",\n\t\t],\n\n\t\tasync execute(_toolCallId, { command: rawCommand, summary }: SuggestNextInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\t// Strip control characters (newlines, tabs, etc.) that would corrupt TUI rendering\n\t\t\tconst command = rawCommand?.replace(/[\\x00-\\x1f\\x7f]/g, \"\").trim();\n\t\t\tif (!command) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\" as const, text: \"Error: command is empty\" }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t\tendTurn: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tonSuggest(command);\n\n\t\t\t// Convert literal \\n sequences to actual newlines (LLMs emit these in XML tool calls),\n\t\t\t// then strip control characters (preserve only newlines for markdown)\n\t\t\tconst sanitizedSummary =\n\t\t\t\tsummary\n\t\t\t\t\t?.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t\t.replace(/[\\x00-\\x09\\x0b-\\x1f\\x7f]/g, \"\")\n\t\t\t\t\t.trim() || undefined;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: `Suggestion registered: ${command}` }],\n\t\t\t\tdetails: { suggestion: command, summary: sanitizedSummary },\n\t\t\t\tendTurn: true,\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatSuggestNextCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst details = (result as any).details as SuggestNextDetails | undefined;\n\t\t\tif (!details) {\n\t\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\t\tconst content = result.content?.[0];\n\t\t\t\tconst msg = content?.type === \"text\" && content.text ? content.text : \"\";\n\t\t\t\ttext.setText(theme.fg(\"toolOutput\", msg));\n\t\t\t\treturn text;\n\t\t\t}\n\n\t\t\tif (details.summary) {\n\t\t\t\tconst container = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcontainer.clear();\n\t\t\t\tcontainer.addChild(new Markdown(details.summary, 0, 0, getMarkdownTheme(), undefined, true));\n\t\t\t\tcontainer.addChild(new Text(theme.fg(\"toolOutput\", `→ ${details.suggestion}`), 0, 0, undefined, true));\n\t\t\t\treturn container;\n\t\t\t}\n\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(theme.fg(\"toolOutput\", `→ ${details.suggestion}`));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../../src/core/tools/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,gBAAgB,CAAC;AAa7E,QAAA,MAAM,iBAAiB;;;;;;EAKrB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAuC9D,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,mBAAmB,GAC3B,cAAc,CAAC,OAAO,iBAAiB,EAAE,gBAAgB,GAAG,SAAS,CAAC,CAwExE","sourcesContent":["/**\n * Session task tracking tool.\n *\n * Full-replacement model: agent sends the complete task list each call.\n * At most one task can be in_progress at a time.\n * Tasks are session-local (in-memory, lost on session end).\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"completed\";\n\nexport interface SessionTask {\n\tid: string;\n\ttitle: string;\n\tstatus: TaskStatus;\n}\n\nexport interface TasksToolDetails {\n\ttaskCount: number;\n\tcompleted: number;\n\tinProgress: string | undefined;\n}\n\nexport type TasksUpdateCallback = (tasks: SessionTask[]) => TasksToolDetails;\n\n// ============================================================================\n// Schema\n\nconst taskSchema = Type.Object({\n\tid: Type.String({ description: 'Stable identifier (e.g. \"1\", \"setup\", \"fix-auth\")' }),\n\ttitle: Type.String({ description: \"Brief action-oriented title\" }),\n\tstatus: Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\t\tdescription: \"Task status: pending, in_progress, or completed\",\n\t}),\n});\n\nconst tasksUpdateSchema = Type.Object({\n\ttasks: Type.Array(taskSchema, {\n\t\tdescription:\n\t\t\t\"Complete current task list. This is a full replacement — send ALL tasks each time, not just changes.\",\n\t}),\n});\n\nexport type TasksToolInput = Static<typeof tasksUpdateSchema>;\n\n// ============================================================================\n// Render helpers\n\nfunction formatTasksCall(\n\targs: { tasks?: Array<{ id?: string; title?: string; status?: string }> } | undefined,\n\ttheme: any,\n): string {\n\tconst count = args?.tasks?.length ?? 0;\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"tasks_update\"))} ${theme.fg(\"accent\", `${count} task${count !== 1 ? \"s\" : \"\"}`)}`;\n}\n\nfunction formatTasksResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: TasksToolDetails;\n\t},\n\ttheme: any,\n): string {\n\tconst details = result.details;\n\tif (!details || details.taskCount === undefined) {\n\t\t// Fallback to raw text content (e.g. error messages, partial results)\n\t\tconst text = result.content?.[0];\n\t\treturn text?.type === \"text\" && text.text ? theme.fg(\"toolOutput\", text.text) : \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tparts.push(`${details.taskCount} task${details.taskCount !== 1 ? \"s\" : \"\"}`);\n\tparts.push(`${details.completed ?? 0} completed`);\n\tif (details.inProgress) {\n\t\tparts.push(`current: ${details.inProgress}`);\n\t}\n\treturn theme.fg(\"toolOutput\", parts.join(\", \"));\n}\n\n// ============================================================================\n// Tool definition factory\n\nexport function createTasksToolDefinition(\n\tonUpdate: TasksUpdateCallback,\n): ToolDefinition<typeof tasksUpdateSchema, TasksToolDetails | undefined> {\n\treturn {\n\t\tname: \"tasks_update\",\n\t\tlabel: \"tasks_update\",\n\t\tdescription:\n\t\t\t\"Create or update the session task list. Send the complete current task list each time — this is a full replacement, not a patch. Use this to track progress on multi-step work.\",\n\n\t\tparameters: tasksUpdateSchema,\n\n\t\tpromptSnippet: \"Create or update session task list for multi-step work\",\n\n\t\tpromptGuidelines: [\n\t\t\t\"For work requiring 3 or more steps, use tasks_update to organize your plan and show progress\",\n\t\t\t\"Send the complete task list each time (full replacement, not a patch)\",\n\t\t\t\"At most one task can be in_progress at a time\",\n\t\t\t\"Keep task lists concise: typically 3-10 items. Never exceed 20 tasks.\",\n\t\t\t'Use short, action-oriented titles (e.g. \"Read existing tests\", \"Fix auth handler\")',\n\t\t],\n\n\t\tasync execute(_toolCallId, { tasks }: TasksToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\t// Validate: max 20 tasks\n\t\t\tif (tasks.length > 20) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: Task list too long (${tasks.length} tasks). Maximum is 20. Break work into higher-level steps.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Validate: at most one in_progress\n\t\t\tconst inProgressTasks = tasks.filter((t) => t.status === \"in_progress\");\n\t\t\tif (inProgressTasks.length > 1) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: At most one task can be in_progress at a time. Found ${inProgressTasks.length}: ${inProgressTasks.map((t) => t.id).join(\", \")}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst details = onUpdate(tasks);\n\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `Tasks updated: ${details.taskCount} total, ${details.completed} completed${details.inProgress ? `, in progress: ${details.inProgress}` : \"\"}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails,\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatTasksCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatTasksResult(result as any, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../../src/core/tools/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,gBAAgB,CAAC;AAa7E,QAAA,MAAM,iBAAiB;;;;;;EAKrB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAuC9D,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,mBAAmB,GAC3B,cAAc,CAAC,OAAO,iBAAiB,EAAE,gBAAgB,GAAG,SAAS,CAAC,CAwExE","sourcesContent":["/**\n * Session task tracking tool.\n *\n * Full-replacement model: agent sends the complete task list each call.\n * At most one task can be in_progress at a time.\n * Tasks are session-local (in-memory, lost on session end).\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"completed\";\n\nexport interface SessionTask {\n\tid: string;\n\ttitle: string;\n\tstatus: TaskStatus;\n}\n\nexport interface TasksToolDetails {\n\ttaskCount: number;\n\tcompleted: number;\n\tinProgress: string | undefined;\n}\n\nexport type TasksUpdateCallback = (tasks: SessionTask[]) => TasksToolDetails;\n\n// ============================================================================\n// Schema\n\nconst taskSchema = Type.Object({\n\tid: Type.String({ description: 'Stable identifier (e.g. \"1\", \"setup\", \"fix-auth\")' }),\n\ttitle: Type.String({ description: \"Brief action-oriented title\" }),\n\tstatus: Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\t\tdescription: \"Task status: pending, in_progress, or completed\",\n\t}),\n});\n\nconst tasksUpdateSchema = Type.Object({\n\ttasks: Type.Array(taskSchema, {\n\t\tdescription:\n\t\t\t\"Complete current task list. This is a full replacement — send ALL tasks each time, not just changes.\",\n\t}),\n});\n\nexport type TasksToolInput = Static<typeof tasksUpdateSchema>;\n\n// ============================================================================\n// Render helpers\n\nfunction formatTasksCall(\n\targs: { tasks?: Array<{ id?: string; title?: string; status?: string }> } | undefined,\n\ttheme: any,\n): string {\n\tconst count = args?.tasks?.length ?? 0;\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"tasks_update\"))} ${theme.fg(\"accent\", `${count} task${count !== 1 ? \"s\" : \"\"}`)}`;\n}\n\nfunction formatTasksResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: TasksToolDetails;\n\t},\n\ttheme: any,\n): string {\n\tconst details = result.details;\n\tif (!details || details.taskCount === undefined) {\n\t\t// Fallback to raw text content (e.g. error messages, partial results)\n\t\tconst text = result.content?.[0];\n\t\treturn text?.type === \"text\" && text.text ? theme.fg(\"toolOutput\", text.text) : \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tparts.push(`${details.taskCount} task${details.taskCount !== 1 ? \"s\" : \"\"}`);\n\tparts.push(`${details.completed ?? 0} completed`);\n\tif (details.inProgress) {\n\t\tparts.push(`current: ${details.inProgress}`);\n\t}\n\treturn theme.fg(\"toolOutput\", parts.join(\", \"));\n}\n\n// ============================================================================\n// Tool definition factory\n\nexport function createTasksToolDefinition(\n\tonUpdate: TasksUpdateCallback,\n): ToolDefinition<typeof tasksUpdateSchema, TasksToolDetails | undefined> {\n\treturn {\n\t\tname: \"tasks_update\",\n\t\tlabel: \"tasks_update\",\n\t\tdescription:\n\t\t\t\"Create or update the session task list. Send the complete current task list each time — this is a full replacement, not a patch. Use this to track progress on multi-step work.\",\n\n\t\tparameters: tasksUpdateSchema,\n\n\t\tpromptSnippet: \"Create or update session task list for multi-step work\",\n\n\t\tpromptGuidelines: [\n\t\t\t\"For work requiring 3 or more steps, use tasks_update to organize your plan and show progress\",\n\t\t\t\"Send the complete task list each time (full replacement, not a patch)\",\n\t\t\t\"At most one task can be in_progress at a time\",\n\t\t\t\"Keep task lists concise: typically 3-10 items. Never exceed 20 tasks.\",\n\t\t\t'Use short, action-oriented titles (e.g. \"Read existing tests\", \"Fix auth handler\")',\n\t\t],\n\n\t\tasync execute(_toolCallId, { tasks }: TasksToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\t// Validate: max 20 tasks\n\t\t\tif (tasks.length > 20) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: Task list too long (${tasks.length} tasks). Maximum is 20. Break work into higher-level steps.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Validate: at most one in_progress\n\t\t\tconst inProgressTasks = tasks.filter((t) => t.status === \"in_progress\");\n\t\t\tif (inProgressTasks.length > 1) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: At most one task can be in_progress at a time. Found ${inProgressTasks.length}: ${inProgressTasks.map((t) => t.id).join(\", \")}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst details = onUpdate(tasks);\n\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `Tasks updated: ${details.taskCount} total, ${details.completed} completed${details.inProgress ? `, in progress: ${details.inProgress}` : \"\"}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails,\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatTasksCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatTasksResult(result as any, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n"]}
@@ -96,12 +96,12 @@ export function createTasksToolDefinition(onUpdate) {
96
96
  };
97
97
  },
98
98
  renderCall(args, theme, context) {
99
- const text = context.lastComponent ?? new Text("", 0, 0);
99
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
100
100
  text.setText(formatTasksCall(args, theme));
101
101
  return text;
102
102
  },
103
103
  renderResult(result, _options, theme, context) {
104
- const text = context.lastComponent ?? new Text("", 0, 0);
104
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
105
105
  text.setText(formatTasksResult(result, theme));
106
106
  return text;
107
107
  },
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../../src/core/tools/tasks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session task tracking tool.\n *\n * Full-replacement model: agent sends the complete task list each call.\n * At most one task can be in_progress at a time.\n * Tasks are session-local (in-memory, lost on session end).\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"completed\";\n\nexport interface SessionTask {\n\tid: string;\n\ttitle: string;\n\tstatus: TaskStatus;\n}\n\nexport interface TasksToolDetails {\n\ttaskCount: number;\n\tcompleted: number;\n\tinProgress: string | undefined;\n}\n\nexport type TasksUpdateCallback = (tasks: SessionTask[]) => TasksToolDetails;\n\n// ============================================================================\n// Schema\n\nconst taskSchema = Type.Object({\n\tid: Type.String({ description: 'Stable identifier (e.g. \"1\", \"setup\", \"fix-auth\")' }),\n\ttitle: Type.String({ description: \"Brief action-oriented title\" }),\n\tstatus: Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\t\tdescription: \"Task status: pending, in_progress, or completed\",\n\t}),\n});\n\nconst tasksUpdateSchema = Type.Object({\n\ttasks: Type.Array(taskSchema, {\n\t\tdescription:\n\t\t\t\"Complete current task list. This is a full replacement — send ALL tasks each time, not just changes.\",\n\t}),\n});\n\nexport type TasksToolInput = Static<typeof tasksUpdateSchema>;\n\n// ============================================================================\n// Render helpers\n\nfunction formatTasksCall(\n\targs: { tasks?: Array<{ id?: string; title?: string; status?: string }> } | undefined,\n\ttheme: any,\n): string {\n\tconst count = args?.tasks?.length ?? 0;\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"tasks_update\"))} ${theme.fg(\"accent\", `${count} task${count !== 1 ? \"s\" : \"\"}`)}`;\n}\n\nfunction formatTasksResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: TasksToolDetails;\n\t},\n\ttheme: any,\n): string {\n\tconst details = result.details;\n\tif (!details || details.taskCount === undefined) {\n\t\t// Fallback to raw text content (e.g. error messages, partial results)\n\t\tconst text = result.content?.[0];\n\t\treturn text?.type === \"text\" && text.text ? theme.fg(\"toolOutput\", text.text) : \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tparts.push(`${details.taskCount} task${details.taskCount !== 1 ? \"s\" : \"\"}`);\n\tparts.push(`${details.completed ?? 0} completed`);\n\tif (details.inProgress) {\n\t\tparts.push(`current: ${details.inProgress}`);\n\t}\n\treturn theme.fg(\"toolOutput\", parts.join(\", \"));\n}\n\n// ============================================================================\n// Tool definition factory\n\nexport function createTasksToolDefinition(\n\tonUpdate: TasksUpdateCallback,\n): ToolDefinition<typeof tasksUpdateSchema, TasksToolDetails | undefined> {\n\treturn {\n\t\tname: \"tasks_update\",\n\t\tlabel: \"tasks_update\",\n\t\tdescription:\n\t\t\t\"Create or update the session task list. Send the complete current task list each time — this is a full replacement, not a patch. Use this to track progress on multi-step work.\",\n\n\t\tparameters: tasksUpdateSchema,\n\n\t\tpromptSnippet: \"Create or update session task list for multi-step work\",\n\n\t\tpromptGuidelines: [\n\t\t\t\"For work requiring 3 or more steps, use tasks_update to organize your plan and show progress\",\n\t\t\t\"Send the complete task list each time (full replacement, not a patch)\",\n\t\t\t\"At most one task can be in_progress at a time\",\n\t\t\t\"Keep task lists concise: typically 3-10 items. Never exceed 20 tasks.\",\n\t\t\t'Use short, action-oriented titles (e.g. \"Read existing tests\", \"Fix auth handler\")',\n\t\t],\n\n\t\tasync execute(_toolCallId, { tasks }: TasksToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\t// Validate: max 20 tasks\n\t\t\tif (tasks.length > 20) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: Task list too long (${tasks.length} tasks). Maximum is 20. Break work into higher-level steps.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Validate: at most one in_progress\n\t\t\tconst inProgressTasks = tasks.filter((t) => t.status === \"in_progress\");\n\t\t\tif (inProgressTasks.length > 1) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: At most one task can be in_progress at a time. Found ${inProgressTasks.length}: ${inProgressTasks.map((t) => t.id).join(\", \")}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst details = onUpdate(tasks);\n\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `Tasks updated: ${details.taskCount} total, ${details.completed} completed${details.inProgress ? `, in progress: ${details.inProgress}` : \"\"}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails,\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatTasksCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatTasksResult(result as any, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../../src/core/tools/tasks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session task tracking tool.\n *\n * Full-replacement model: agent sends the complete task list each call.\n * At most one task can be in_progress at a time.\n * Tasks are session-local (in-memory, lost on session end).\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"completed\";\n\nexport interface SessionTask {\n\tid: string;\n\ttitle: string;\n\tstatus: TaskStatus;\n}\n\nexport interface TasksToolDetails {\n\ttaskCount: number;\n\tcompleted: number;\n\tinProgress: string | undefined;\n}\n\nexport type TasksUpdateCallback = (tasks: SessionTask[]) => TasksToolDetails;\n\n// ============================================================================\n// Schema\n\nconst taskSchema = Type.Object({\n\tid: Type.String({ description: 'Stable identifier (e.g. \"1\", \"setup\", \"fix-auth\")' }),\n\ttitle: Type.String({ description: \"Brief action-oriented title\" }),\n\tstatus: Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\t\tdescription: \"Task status: pending, in_progress, or completed\",\n\t}),\n});\n\nconst tasksUpdateSchema = Type.Object({\n\ttasks: Type.Array(taskSchema, {\n\t\tdescription:\n\t\t\t\"Complete current task list. This is a full replacement — send ALL tasks each time, not just changes.\",\n\t}),\n});\n\nexport type TasksToolInput = Static<typeof tasksUpdateSchema>;\n\n// ============================================================================\n// Render helpers\n\nfunction formatTasksCall(\n\targs: { tasks?: Array<{ id?: string; title?: string; status?: string }> } | undefined,\n\ttheme: any,\n): string {\n\tconst count = args?.tasks?.length ?? 0;\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"tasks_update\"))} ${theme.fg(\"accent\", `${count} task${count !== 1 ? \"s\" : \"\"}`)}`;\n}\n\nfunction formatTasksResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: TasksToolDetails;\n\t},\n\ttheme: any,\n): string {\n\tconst details = result.details;\n\tif (!details || details.taskCount === undefined) {\n\t\t// Fallback to raw text content (e.g. error messages, partial results)\n\t\tconst text = result.content?.[0];\n\t\treturn text?.type === \"text\" && text.text ? theme.fg(\"toolOutput\", text.text) : \"\";\n\t}\n\n\tconst parts: string[] = [];\n\tparts.push(`${details.taskCount} task${details.taskCount !== 1 ? \"s\" : \"\"}`);\n\tparts.push(`${details.completed ?? 0} completed`);\n\tif (details.inProgress) {\n\t\tparts.push(`current: ${details.inProgress}`);\n\t}\n\treturn theme.fg(\"toolOutput\", parts.join(\", \"));\n}\n\n// ============================================================================\n// Tool definition factory\n\nexport function createTasksToolDefinition(\n\tonUpdate: TasksUpdateCallback,\n): ToolDefinition<typeof tasksUpdateSchema, TasksToolDetails | undefined> {\n\treturn {\n\t\tname: \"tasks_update\",\n\t\tlabel: \"tasks_update\",\n\t\tdescription:\n\t\t\t\"Create or update the session task list. Send the complete current task list each time — this is a full replacement, not a patch. Use this to track progress on multi-step work.\",\n\n\t\tparameters: tasksUpdateSchema,\n\n\t\tpromptSnippet: \"Create or update session task list for multi-step work\",\n\n\t\tpromptGuidelines: [\n\t\t\t\"For work requiring 3 or more steps, use tasks_update to organize your plan and show progress\",\n\t\t\t\"Send the complete task list each time (full replacement, not a patch)\",\n\t\t\t\"At most one task can be in_progress at a time\",\n\t\t\t\"Keep task lists concise: typically 3-10 items. Never exceed 20 tasks.\",\n\t\t\t'Use short, action-oriented titles (e.g. \"Read existing tests\", \"Fix auth handler\")',\n\t\t],\n\n\t\tasync execute(_toolCallId, { tasks }: TasksToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\t// Validate: max 20 tasks\n\t\t\tif (tasks.length > 20) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: Task list too long (${tasks.length} tasks). Maximum is 20. Break work into higher-level steps.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Validate: at most one in_progress\n\t\t\tconst inProgressTasks = tasks.filter((t) => t.status === \"in_progress\");\n\t\t\tif (inProgressTasks.length > 1) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Error: At most one task can be in_progress at a time. Found ${inProgressTasks.length}: ${inProgressTasks.map((t) => t.id).join(\", \")}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst details = onUpdate(tasks);\n\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `Tasks updated: ${details.taskCount} total, ${details.completed} completed${details.inProgress ? `, in progress: ${details.inProgress}` : \"\"}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails,\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatTasksCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatTasksResult(result as any, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../../../src/core/tools/wait.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D,gHAAgH;AAChH,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,aAAa,EAAE,SAAS,aAAa,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC/B,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,MAAM,SAAS,aAAa,EAAE,CAAC;CAClD;AAKD,QAAA,MAAM,UAAU;;EAMd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAKtD,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAAE,KAAK,EAAE,GAAG,GAAG,MAAM,CAMxF;AAED,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,eAAe,CAAA;CAAE,EACtF,KAAK,EAAE,GAAG,GACR,MAAM,CAOR;AAKD,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAsC7F,CAAC;AAOF,wBAAgB,wBAAwB,CACvC,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAiBhE","sourcesContent":["/**\n * Wait tool — explicit no-op for models that don't know how to end a turn.\n *\n * Some models (notably Kimi) invent useless `bash` calls to simulate waiting.\n * This tool gives them a sanctioned no-op action that cleanly ends the turn.\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\n/** Minimal shape of a running background agent (avoids importing from subagent.ts to prevent circular deps). */\nexport interface WaitAgentInfo {\n\tagentId: string;\n\tagentType: string;\n\ttaskSummary: string;\n}\n\nexport interface WaitToolDetails {\n\treason: string | undefined;\n\trunningAgents: readonly WaitAgentInfo[];\n}\n\nexport interface WaitToolOptions {\n\t/** Returns currently running background agents. Injected by the factory to avoid circular imports. */\n\tgetRunningAgents?: () => readonly WaitAgentInfo[];\n}\n\n// ============================================================================\n// Schema\n\nconst waitSchema = Type.Object({\n\treason: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"Optional reason for waiting (e.g. 'background subagent still running')\",\n\t\t}),\n\t),\n});\n\nexport type WaitToolInput = Static<typeof waitSchema>;\n\n// ============================================================================\n// Render helpers\n\nexport function formatWaitCall(args: { reason?: string } | undefined, theme: any): string {\n\tconst reason = args?.reason;\n\tif (reason) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"wait\"))} ${theme.fg(\"muted\", reason)}`;\n\t}\n\treturn theme.fg(\"toolTitle\", theme.bold(\"wait\"));\n}\n\nexport function formatWaitResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WaitToolDetails },\n\ttheme: any,\n): string {\n\tconst agents = result.details?.runningAgents ?? [];\n\tif (agents.length > 0) {\n\t\tconst agentList = agents.map((a) => `${a.agentId.slice(0, 12)} ${a.agentType}`).join(\", \");\n\t\treturn theme.fg(\"muted\", `→ doing nothing (waiting on: ${agentList})`);\n\t}\n\treturn theme.fg(\"muted\", \"→ doing nothing — no subagents running\");\n}\n\n// ============================================================================\n// Tool definition (singleton — no cwd or callback dependencies)\n\nexport const waitToolDefinition: ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> = {\n\tname: \"wait\",\n\tlabel: \"wait\",\n\tdescription:\n\t\t\"Do nothing and end your turn. Use this when you are explicitly told to wait, or when background subagents are running and you have no other work to do.\",\n\n\tparameters: waitSchema,\n\n\tpromptSnippet: \"Do nothing and end your turn (use when waiting for background work to complete)\",\n\n\tpromptGuidelines: [\n\t\t\"Use `wait` only when explicitly told to wait, or when background subagents are still running and you have no other work to do\",\n\t\t\"Do NOT use `wait` as a general-purpose delay or sleep — it returns immediately\",\n\t\t\"If you need to wait for something, call `wait` once — do not loop or call it repeatedly\",\n\t],\n\n\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\tconst reason = params.reason?.trim() || undefined;\n\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\tdetails: { reason, runningAgents: [] as WaitAgentInfo[] },\n\t\t\tendTurn: true,\n\t\t};\n\t},\n\n\trenderCall(args, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\ttext.setText(formatWaitCall(args, theme));\n\t\treturn text;\n\t},\n\n\trenderResult(result, _options, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\ttext.setText(formatWaitResult(result as any, theme));\n\t\treturn text;\n\t},\n};\n\n// ============================================================================\n// Factory — accepts optional getRunningAgents callback to show background agent\n// status in the result. The callback is injected here (rather than imported from\n// subagent.ts) to avoid a circular dependency through model-resolver → args → index.\n\nexport function createWaitToolDefinition(\n\toptions?: WaitToolOptions,\n): ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> {\n\tif (!options?.getRunningAgents) return waitToolDefinition;\n\n\treturn {\n\t\t...waitToolDefinition,\n\t\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\tconst reason = params.reason?.trim() || undefined;\n\t\t\tconst runningAgents = options.getRunningAgents!();\n\t\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: { reason, runningAgents },\n\t\t\t\tendTurn: true,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"wait.d.ts","sourceRoot":"","sources":["../../../src/core/tools/wait.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D,gHAAgH;AAChH,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,aAAa,EAAE,SAAS,aAAa,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC/B,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,MAAM,SAAS,aAAa,EAAE,CAAC;CAClD;AAKD,QAAA,MAAM,UAAU;;EAMd,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAKtD,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAAE,KAAK,EAAE,GAAG,GAAG,MAAM,CAMxF;AAED,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,eAAe,CAAA;CAAE,EACtF,KAAK,EAAE,GAAG,GACR,MAAM,CAOR;AAKD,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAsC7F,CAAC;AAOF,wBAAgB,wBAAwB,CACvC,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAiBhE","sourcesContent":["/**\n * Wait tool — explicit no-op for models that don't know how to end a turn.\n *\n * Some models (notably Kimi) invent useless `bash` calls to simulate waiting.\n * This tool gives them a sanctioned no-op action that cleanly ends the turn.\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\n/** Minimal shape of a running background agent (avoids importing from subagent.ts to prevent circular deps). */\nexport interface WaitAgentInfo {\n\tagentId: string;\n\tagentType: string;\n\ttaskSummary: string;\n}\n\nexport interface WaitToolDetails {\n\treason: string | undefined;\n\trunningAgents: readonly WaitAgentInfo[];\n}\n\nexport interface WaitToolOptions {\n\t/** Returns currently running background agents. Injected by the factory to avoid circular imports. */\n\tgetRunningAgents?: () => readonly WaitAgentInfo[];\n}\n\n// ============================================================================\n// Schema\n\nconst waitSchema = Type.Object({\n\treason: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"Optional reason for waiting (e.g. 'background subagent still running')\",\n\t\t}),\n\t),\n});\n\nexport type WaitToolInput = Static<typeof waitSchema>;\n\n// ============================================================================\n// Render helpers\n\nexport function formatWaitCall(args: { reason?: string } | undefined, theme: any): string {\n\tconst reason = args?.reason;\n\tif (reason) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"wait\"))} ${theme.fg(\"muted\", reason)}`;\n\t}\n\treturn theme.fg(\"toolTitle\", theme.bold(\"wait\"));\n}\n\nexport function formatWaitResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WaitToolDetails },\n\ttheme: any,\n): string {\n\tconst agents = result.details?.runningAgents ?? [];\n\tif (agents.length > 0) {\n\t\tconst agentList = agents.map((a) => `${a.agentId.slice(0, 12)} ${a.agentType}`).join(\", \");\n\t\treturn theme.fg(\"muted\", `→ doing nothing (waiting on: ${agentList})`);\n\t}\n\treturn theme.fg(\"muted\", \"→ doing nothing — no subagents running\");\n}\n\n// ============================================================================\n// Tool definition (singleton — no cwd or callback dependencies)\n\nexport const waitToolDefinition: ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> = {\n\tname: \"wait\",\n\tlabel: \"wait\",\n\tdescription:\n\t\t\"Do nothing and end your turn. Use this when you are explicitly told to wait, or when background subagents are running and you have no other work to do.\",\n\n\tparameters: waitSchema,\n\n\tpromptSnippet: \"Do nothing and end your turn (use when waiting for background work to complete)\",\n\n\tpromptGuidelines: [\n\t\t\"Use `wait` only when explicitly told to wait, or when background subagents are still running and you have no other work to do\",\n\t\t\"Do NOT use `wait` as a general-purpose delay or sleep — it returns immediately\",\n\t\t\"If you need to wait for something, call `wait` once — do not loop or call it repeatedly\",\n\t],\n\n\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\tconst reason = params.reason?.trim() || undefined;\n\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\tdetails: { reason, runningAgents: [] as WaitAgentInfo[] },\n\t\t\tendTurn: true,\n\t\t};\n\t},\n\n\trenderCall(args, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\ttext.setText(formatWaitCall(args, theme));\n\t\treturn text;\n\t},\n\n\trenderResult(result, _options, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\ttext.setText(formatWaitResult(result as any, theme));\n\t\treturn text;\n\t},\n};\n\n// ============================================================================\n// Factory — accepts optional getRunningAgents callback to show background agent\n// status in the result. The callback is injected here (rather than imported from\n// subagent.ts) to avoid a circular dependency through model-resolver → args → index.\n\nexport function createWaitToolDefinition(\n\toptions?: WaitToolOptions,\n): ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> {\n\tif (!options?.getRunningAgents) return waitToolDefinition;\n\n\treturn {\n\t\t...waitToolDefinition,\n\t\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\tconst reason = params.reason?.trim() || undefined;\n\t\t\tconst runningAgents = options.getRunningAgents!();\n\t\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: { reason, runningAgents },\n\t\t\t\tendTurn: true,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -53,12 +53,12 @@ export const waitToolDefinition = {
53
53
  };
54
54
  },
55
55
  renderCall(args, theme, context) {
56
- const text = context.lastComponent ?? new Text("", 0, 0);
56
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
57
57
  text.setText(formatWaitCall(args, theme));
58
58
  return text;
59
59
  },
60
60
  renderResult(result, _options, theme, context) {
61
- const text = context.lastComponent ?? new Text("", 0, 0);
61
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
62
62
  text.setText(formatWaitResult(result, theme));
63
63
  return text;
64
64
  },
@@ -1 +1 @@
1
- {"version":3,"file":"wait.js","sourceRoot":"","sources":["../../../src/core/tools/wait.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wait tool — explicit no-op for models that don't know how to end a turn.\n *\n * Some models (notably Kimi) invent useless `bash` calls to simulate waiting.\n * This tool gives them a sanctioned no-op action that cleanly ends the turn.\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\n/** Minimal shape of a running background agent (avoids importing from subagent.ts to prevent circular deps). */\nexport interface WaitAgentInfo {\n\tagentId: string;\n\tagentType: string;\n\ttaskSummary: string;\n}\n\nexport interface WaitToolDetails {\n\treason: string | undefined;\n\trunningAgents: readonly WaitAgentInfo[];\n}\n\nexport interface WaitToolOptions {\n\t/** Returns currently running background agents. Injected by the factory to avoid circular imports. */\n\tgetRunningAgents?: () => readonly WaitAgentInfo[];\n}\n\n// ============================================================================\n// Schema\n\nconst waitSchema = Type.Object({\n\treason: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"Optional reason for waiting (e.g. 'background subagent still running')\",\n\t\t}),\n\t),\n});\n\nexport type WaitToolInput = Static<typeof waitSchema>;\n\n// ============================================================================\n// Render helpers\n\nexport function formatWaitCall(args: { reason?: string } | undefined, theme: any): string {\n\tconst reason = args?.reason;\n\tif (reason) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"wait\"))} ${theme.fg(\"muted\", reason)}`;\n\t}\n\treturn theme.fg(\"toolTitle\", theme.bold(\"wait\"));\n}\n\nexport function formatWaitResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WaitToolDetails },\n\ttheme: any,\n): string {\n\tconst agents = result.details?.runningAgents ?? [];\n\tif (agents.length > 0) {\n\t\tconst agentList = agents.map((a) => `${a.agentId.slice(0, 12)} ${a.agentType}`).join(\", \");\n\t\treturn theme.fg(\"muted\", `→ doing nothing (waiting on: ${agentList})`);\n\t}\n\treturn theme.fg(\"muted\", \"→ doing nothing — no subagents running\");\n}\n\n// ============================================================================\n// Tool definition (singleton — no cwd or callback dependencies)\n\nexport const waitToolDefinition: ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> = {\n\tname: \"wait\",\n\tlabel: \"wait\",\n\tdescription:\n\t\t\"Do nothing and end your turn. Use this when you are explicitly told to wait, or when background subagents are running and you have no other work to do.\",\n\n\tparameters: waitSchema,\n\n\tpromptSnippet: \"Do nothing and end your turn (use when waiting for background work to complete)\",\n\n\tpromptGuidelines: [\n\t\t\"Use `wait` only when explicitly told to wait, or when background subagents are still running and you have no other work to do\",\n\t\t\"Do NOT use `wait` as a general-purpose delay or sleep — it returns immediately\",\n\t\t\"If you need to wait for something, call `wait` once — do not loop or call it repeatedly\",\n\t],\n\n\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\tconst reason = params.reason?.trim() || undefined;\n\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\tdetails: { reason, runningAgents: [] as WaitAgentInfo[] },\n\t\t\tendTurn: true,\n\t\t};\n\t},\n\n\trenderCall(args, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\ttext.setText(formatWaitCall(args, theme));\n\t\treturn text;\n\t},\n\n\trenderResult(result, _options, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\ttext.setText(formatWaitResult(result as any, theme));\n\t\treturn text;\n\t},\n};\n\n// ============================================================================\n// Factory — accepts optional getRunningAgents callback to show background agent\n// status in the result. The callback is injected here (rather than imported from\n// subagent.ts) to avoid a circular dependency through model-resolver → args → index.\n\nexport function createWaitToolDefinition(\n\toptions?: WaitToolOptions,\n): ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> {\n\tif (!options?.getRunningAgents) return waitToolDefinition;\n\n\treturn {\n\t\t...waitToolDefinition,\n\t\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\tconst reason = params.reason?.trim() || undefined;\n\t\t\tconst runningAgents = options.getRunningAgents!();\n\t\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: { reason, runningAgents },\n\t\t\t\tendTurn: true,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"wait.js","sourceRoot":"","sources":["../../../src/core/tools/wait.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wait tool — explicit no-op for models that don't know how to end a turn.\n *\n * Some models (notably Kimi) invent useless `bash` calls to simulate waiting.\n * This tool gives them a sanctioned no-op action that cleanly ends the turn.\n */\n\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\n\n// ============================================================================\n// Types\n\n/** Minimal shape of a running background agent (avoids importing from subagent.ts to prevent circular deps). */\nexport interface WaitAgentInfo {\n\tagentId: string;\n\tagentType: string;\n\ttaskSummary: string;\n}\n\nexport interface WaitToolDetails {\n\treason: string | undefined;\n\trunningAgents: readonly WaitAgentInfo[];\n}\n\nexport interface WaitToolOptions {\n\t/** Returns currently running background agents. Injected by the factory to avoid circular imports. */\n\tgetRunningAgents?: () => readonly WaitAgentInfo[];\n}\n\n// ============================================================================\n// Schema\n\nconst waitSchema = Type.Object({\n\treason: Type.Optional(\n\t\tType.String({\n\t\t\tdescription: \"Optional reason for waiting (e.g. 'background subagent still running')\",\n\t\t}),\n\t),\n});\n\nexport type WaitToolInput = Static<typeof waitSchema>;\n\n// ============================================================================\n// Render helpers\n\nexport function formatWaitCall(args: { reason?: string } | undefined, theme: any): string {\n\tconst reason = args?.reason;\n\tif (reason) {\n\t\treturn `${theme.fg(\"toolTitle\", theme.bold(\"wait\"))} ${theme.fg(\"muted\", reason)}`;\n\t}\n\treturn theme.fg(\"toolTitle\", theme.bold(\"wait\"));\n}\n\nexport function formatWaitResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: WaitToolDetails },\n\ttheme: any,\n): string {\n\tconst agents = result.details?.runningAgents ?? [];\n\tif (agents.length > 0) {\n\t\tconst agentList = agents.map((a) => `${a.agentId.slice(0, 12)} ${a.agentType}`).join(\", \");\n\t\treturn theme.fg(\"muted\", `→ doing nothing (waiting on: ${agentList})`);\n\t}\n\treturn theme.fg(\"muted\", \"→ doing nothing — no subagents running\");\n}\n\n// ============================================================================\n// Tool definition (singleton — no cwd or callback dependencies)\n\nexport const waitToolDefinition: ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> = {\n\tname: \"wait\",\n\tlabel: \"wait\",\n\tdescription:\n\t\t\"Do nothing and end your turn. Use this when you are explicitly told to wait, or when background subagents are running and you have no other work to do.\",\n\n\tparameters: waitSchema,\n\n\tpromptSnippet: \"Do nothing and end your turn (use when waiting for background work to complete)\",\n\n\tpromptGuidelines: [\n\t\t\"Use `wait` only when explicitly told to wait, or when background subagents are still running and you have no other work to do\",\n\t\t\"Do NOT use `wait` as a general-purpose delay or sleep — it returns immediately\",\n\t\t\"If you need to wait for something, call `wait` once — do not loop or call it repeatedly\",\n\t],\n\n\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\tconst reason = params.reason?.trim() || undefined;\n\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\treturn {\n\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\tdetails: { reason, runningAgents: [] as WaitAgentInfo[] },\n\t\t\tendTurn: true,\n\t\t};\n\t},\n\n\trenderCall(args, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\ttext.setText(formatWaitCall(args, theme));\n\t\treturn text;\n\t},\n\n\trenderResult(result, _options, theme, context) {\n\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\ttext.setText(formatWaitResult(result as any, theme));\n\t\treturn text;\n\t},\n};\n\n// ============================================================================\n// Factory — accepts optional getRunningAgents callback to show background agent\n// status in the result. The callback is injected here (rather than imported from\n// subagent.ts) to avoid a circular dependency through model-resolver → args → index.\n\nexport function createWaitToolDefinition(\n\toptions?: WaitToolOptions,\n): ToolDefinition<typeof waitSchema, WaitToolDetails | undefined> {\n\tif (!options?.getRunningAgents) return waitToolDefinition;\n\n\treturn {\n\t\t...waitToolDefinition,\n\t\tasync execute(_toolCallId, params: WaitToolInput, _signal?, _onUpdate?, _ctx?) {\n\t\t\tconst reason = params.reason?.trim() || undefined;\n\t\t\tconst runningAgents = options.getRunningAgents!();\n\t\t\tconst text = reason ? `Waiting: ${reason}` : \"Waiting…\";\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: { reason, runningAgents },\n\t\t\t\tendTurn: true,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAItF,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAgLnG,QAAA,MAAM,eAAe;;EAEnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,UAAU,YAAY;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB;AAuFD,MAAM,WAAW,eAAe;IAC/B,OAAO,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAkCD,wBAAgB,eAAe,IAAI,eAAe,CAyCjD;AAMD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAa1E;AAuCD,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA2C1E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,eAAe,CAAC,CAElF;AAED,eAAO,MAAM,uBAAuB;;0CAA+C,CAAC;AACpF,eAAO,MAAM,aAAa;;QAAqC,CAAC;AAMhE,QAAA,MAAM,cAAc;;EAElB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B;AA0CD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAiCjE;AAED,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CA4HxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,cAAc,CAAC,CAEhF;AAED,eAAO,MAAM,sBAAsB;;yCAA8C,CAAC;AAClF,eAAO,MAAM,YAAY;;QAAoC,CAAC","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { CONFIG_DIR_NAME } from \"../../config.js\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { log } from \"../logger.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\nimport { WebSearchQueue } from \"./web-search-queue.js\";\n\n// ---------------------------------------------------------------------------\n// Shared: HTTP fetching and HTML extraction\n// ---------------------------------------------------------------------------\n\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_CONTENT_LENGTH = 100_000;\nconst CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes\n\nconst fetchCache = new Map<string, { content: WebFetchResult; timestamp: number }>();\n\ninterface WebFetchResult {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\tfetchedAt: string;\n}\n\nfunction stripHtmlToText(html: string): string {\n\tlet text = html;\n\t// Remove script/style/nav/footer blocks entirely\n\ttext = text.replace(/<(script|style|nav|footer|header|aside|iframe|noscript)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, \"\");\n\t// Convert block elements to newlines\n\ttext = text.replace(/<\\/(p|div|li|tr|h[1-6]|blockquote|pre|section|article)>/gi, \"\\n\");\n\ttext = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n\t// Convert links to text with URL\n\ttext = text.replace(/<a\\b[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$2 ($1)\");\n\t// Convert headings to markdown-style\n\ttext = text.replace(/<h([1-6])\\b[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_match, level, content) => {\n\t\treturn `\\n${\"#\".repeat(Number(level))} ${content.trim()}\\n`;\n\t});\n\t// Convert list items\n\ttext = text.replace(/<li\\b[^>]*>/gi, \"\\n- \");\n\t// Strip all remaining tags\n\ttext = text.replace(/<[^>]+>/g, \"\");\n\t// Decode common HTML entities\n\ttext = text.replace(/&amp;/g, \"&\");\n\ttext = text.replace(/&lt;/g, \"<\");\n\ttext = text.replace(/&gt;/g, \">\");\n\ttext = text.replace(/&quot;/g, '\"');\n\ttext = text.replace(/&#39;/g, \"'\");\n\ttext = text.replace(/&nbsp;/g, \" \");\n\t// Collapse whitespace\n\ttext = text.replace(/[ \\t]+/g, \" \");\n\ttext = text.replace(/\\n{3,}/g, \"\\n\\n\");\n\treturn text.trim();\n}\n\nfunction extractTitle(html: string): string {\n\tconst match = html.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i);\n\treturn match ? match[1].trim().replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : \"\";\n}\n\nconst FETCH_HEADERS = {\n\t\"User-Agent\": \"dreb/1.0 (web fetch tool)\",\n\tAccept: \"text/html,application/xhtml+xml,text/plain,application/pdf\",\n};\n\n// Block fetches to private/internal networks to prevent SSRF\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"0.0.0.0\"]);\n\nfunction isPrivateHost(hostname: string): boolean {\n\tif (BLOCKED_HOSTNAMES.has(hostname)) return true;\n\t// IPv4 private ranges\n\tconst ipv4Match = hostname.match(/^(\\d+)\\.(\\d+)\\.\\d+\\.\\d+$/);\n\tif (ipv4Match) {\n\t\tconst [, first, second] = ipv4Match.map(Number);\n\t\tif (first === 10) return true; // 10.0.0.0/8\n\t\tif (first === 172 && second >= 16 && second <= 31) return true; // 172.16.0.0/12\n\t\tif (first === 192 && second === 168) return true; // 192.168.0.0/16\n\t\tif (first === 169 && second === 254) return true; // link-local 169.254.0.0/16\n\t}\n\t// IPv6 loopback, link-local, and ULA (fc00::/7)\n\tif (hostname.startsWith(\"[\")) {\n\t\tconst lh = hostname.toLowerCase();\n\t\tif (lh.includes(\"::1\") || lh.startsWith(\"[fe80:\") || lh.startsWith(\"[fc\") || lh.startsWith(\"[fd\")) return true;\n\t}\n\treturn false;\n}\n\nfunction buildResponse(response: Response): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst ct = response.headers.get(\"content-type\") || \"\";\n\tif (ct.includes(\"application/pdf\")) {\n\t\treturn response.arrayBuffer().then((buf) => ({ body: Buffer.from(buf), contentType: ct }));\n\t}\n\treturn response.text().then((text) => ({ body: text, contentType: ct }));\n}\n\nasync function httpFetch(url: string): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst originalHost = new URL(url).hostname;\n\tif (isPrivateHost(originalHost)) {\n\t\tthrow new Error(`Blocked: ${originalHost} is a private/internal address`);\n\t}\n\n\t// Manual redirect loop to enforce same-host on every hop\n\tlet currentUrl = url;\n\tconst maxRedirects = 10;\n\tfor (let i = 0; i <= maxRedirects; i++) {\n\t\tconst response = await fetch(currentUrl, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: FETCH_HEADERS,\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t\t});\n\n\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) {\n\t\t\t\tthrow new Error(`HTTP ${response.status}: redirect with no Location header`);\n\t\t\t}\n\t\t\tconst redirectUrl = new URL(location, currentUrl);\n\t\t\t// Block private IPs before revealing them in cross-host messages\n\t\t\tif (isPrivateHost(redirectUrl.hostname)) {\n\t\t\t\tthrow new Error(`Blocked: redirect to private/internal address`);\n\t\t\t}\n\t\t\tif (redirectUrl.hostname !== originalHost) {\n\t\t\t\treturn {\n\t\t\t\t\tbody: `Cross-host redirect detected.\\nOriginal: ${url}\\nRedirects to: ${redirectUrl.href}\\n\\nThe redirect target is on a different host. Fetch the new URL directly if you want to follow it.`,\n\t\t\t\t\tcontentType: \"text/plain\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tcurrentUrl = redirectUrl.href;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst errorBody = await response.text();\n\t\t\tthrow new Error(`HTTP ${response.status}: ${errorBody.slice(0, 200)}`);\n\t\t}\n\n\t\treturn buildResponse(response);\n\t}\n\tthrow new Error(`Too many redirects (${maxRedirects})`);\n}\n\n// -- PDF text extraction (basic) ---------------------------------------------\n\nfunction extractPdfText(buffer: Buffer): string {\n\t// Minimal PDF text extraction — only works on uncompressed PDFs with literal\n\t// string objects in BT/ET text blocks. Most production PDFs use FlateDecode\n\t// compression and will fall through to the failure message.\n\t// latin1 preserves raw byte values 0-255 as code points for safe regex matching.\n\tconst raw = buffer.toString(\"latin1\");\n\tconst textChunks: string[] = [];\n\n\tconst btEtRegex = /BT\\s([\\s\\S]*?)ET/g;\n\tfor (const match of raw.matchAll(btEtRegex)) {\n\t\tconst block = match[1];\n\t\tconst strRegex = /\\(([^)]*)\\)/g;\n\t\tfor (const strMatch of block.matchAll(strRegex)) {\n\t\t\tconst decoded = strMatch[1]\n\t\t\t\t.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t.replace(/\\\\r/g, \"\\r\")\n\t\t\t\t.replace(/\\\\t/g, \"\\t\")\n\t\t\t\t.replace(/\\\\\\(/g, \"(\")\n\t\t\t\t.replace(/\\\\\\)/g, \")\")\n\t\t\t\t.replace(/\\\\\\\\/g, \"\\\\\");\n\t\t\tif (decoded.trim()) {\n\t\t\t\ttextChunks.push(decoded);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (textChunks.length === 0) {\n\t\treturn \"[PDF text extraction failed — the PDF may use embedded fonts or image-based content that requires OCR]\";\n\t}\n\n\treturn textChunks.join(\" \").replace(/\\s+/g, \" \").trim();\n}\n\n// ---------------------------------------------------------------------------\n// web_search tool\n// ---------------------------------------------------------------------------\n\nconst webSearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n});\n\nexport type WebSearchToolInput = Static<typeof webSearchSchema>;\n\nexport interface WebSearchToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface SearchResult {\n\ttitle: string;\n\turl: string;\n\tsnippet: string;\n}\n\nasync function searchDuckDuckGo(query: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\t\"User-Agent\": \"dreb/1.0 (web search tool)\",\n\t\t\tAccept: \"text/html\",\n\t\t},\n\t\tredirect: \"follow\",\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`DuckDuckGo search failed: HTTP ${response.status}`);\n\t}\n\tconst html = await response.text();\n\tconst results: SearchResult[] = [];\n\n\t// Parse DuckDuckGo HTML results — split on result block class\n\tconst resultBlocks = html.split(/class=\"result results_links/);\n\tfor (const block of resultBlocks.slice(1, 11)) {\n\t\tconst titleMatch = block.match(/class=\"result__a\"[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/);\n\t\tconst snippetMatch = block.match(/class=\"result__snippet\"[^>]*>([\\s\\S]*?)<\\/(?:a|td|div)/);\n\n\t\tif (titleMatch) {\n\t\t\tlet url = titleMatch[1];\n\t\t\t// DDG wraps URLs in a redirect — extract the actual URL\n\t\t\tconst uddgMatch = url.match(/uddg=([^&]*)/);\n\t\t\tif (uddgMatch) {\n\t\t\t\turl = decodeURIComponent(uddgMatch[1]);\n\t\t\t}\n\t\t\tconst title = titleMatch[2].replace(/<[^>]+>/g, \"\").trim();\n\t\t\tconst snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, \"\").trim() : \"\";\n\t\t\tif (title && url) {\n\t\t\t\tresults.push({ title, url, snippet });\n\t\t\t}\n\t\t}\n\t}\n\tif (results.length === 0 && html.length > 1000) {\n\t\t// Got a substantial response but parsed 0 results — DDG HTML structure may have changed\n\t\tlog.warn(\"Warning: DDG returned HTML but 0 results were parsed. The HTML structure may have changed.\");\n\t}\n\treturn results;\n}\n\nasync function searchSearXNG(query: string, baseUrl: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`${baseUrl}/search?q=${encodedQuery}&format=json`, {\n\t\tmethod: \"GET\",\n\t\theaders: { Accept: \"application/json\" },\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`SearXNG search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as { results?: Array<{ title: string; url: string; content?: string }> };\n\treturn (data.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.content || \"\",\n\t}));\n}\n\nasync function searchBrave(query: string, apiKey: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"X-Subscription-Token\": apiKey,\n\t\t},\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Brave search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as {\n\t\tweb?: { results?: Array<{ title: string; url: string; description?: string }> };\n\t};\n\treturn (data.web?.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.description || \"\",\n\t}));\n}\n\nexport interface WebSearchConfig {\n\tbackend?: \"ddg\" | \"searxng\" | \"brave\";\n\tsearxngUrl?: string;\n\tbraveApiKey?: string;\n\trateLimitMs?: number;\n}\n\ninterface DrebConfig {\n\tsearch?: {\n\t\tbackend?: string;\n\t\tsearxng_url?: string;\n\t\tbrave_api_key?: string;\n\t\trate_limit_ms?: number;\n\t};\n}\n\nconst VALID_BACKENDS = [\"ddg\", \"searxng\", \"brave\"] as const;\n\nfunction loadDrebConfig(): DrebConfig {\n\t// Config file precedence: project-local > home directory. First valid file wins.\n\tconst candidates = [\n\t\tjoin(process.cwd(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(process.cwd(), \".dreb\", \"config.json\"),\n\t\tjoin(homedir(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(homedir(), \".dreb\", \"config.json\"),\n\t];\n\tfor (const configPath of candidates) {\n\t\tif (existsSync(configPath)) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(readFileSync(configPath, \"utf-8\")) as DrebConfig;\n\t\t\t} catch (err) {\n\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\tlog.warn(`Warning: failed to parse config at ${configPath}: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nexport function getSearchConfig(): WebSearchConfig {\n\tconst fileConfig = loadDrebConfig();\n\t// Environment variables override config file\n\tconst rawBackend = process.env.DREB_SEARCH_BACKEND || fileConfig.search?.backend;\n\tlet backend: WebSearchConfig[\"backend\"] = \"ddg\";\n\tif (rawBackend) {\n\t\tif ((VALID_BACKENDS as readonly string[]).includes(rawBackend)) {\n\t\t\tbackend = rawBackend as WebSearchConfig[\"backend\"];\n\t\t} else {\n\t\t\tlog.warn(\n\t\t\t\t`Warning: unrecognized search backend \"${rawBackend}\", falling back to ddg. Valid: ${VALID_BACKENDS.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst rateLimitEnv = process.env.DREB_WEB_SEARCH_RATE_LIMIT_MS;\n\tlet rateLimitMs = 10_000;\n\tif (rateLimitEnv) {\n\t\tconst parsed = parseInt(rateLimitEnv, 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tlog.warn(`Warning: invalid DREB_WEB_SEARCH_RATE_LIMIT_MS \"${rateLimitEnv}\", using default`);\n\t\t}\n\t} else if (fileConfig.search?.rate_limit_ms !== undefined) {\n\t\tconst parsed = parseInt(String(fileConfig.search.rate_limit_ms), 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tlog.warn(\n\t\t\t\t`Warning: invalid search.rate_limit_ms in config file \"${fileConfig.search.rate_limit_ms}\", using default`,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn {\n\t\tbackend,\n\t\tsearxngUrl: process.env.DREB_SEARXNG_URL || fileConfig.search?.searxng_url || \"http://localhost:8888\",\n\t\tbraveApiKey: process.env.DREB_BRAVE_API_KEY || fileConfig.search?.brave_api_key,\n\t\trateLimitMs,\n\t};\n}\n\nfunction getSearchQueue(): WebSearchQueue {\n\treturn new WebSearchQueue({ rateLimitMs: getSearchConfig().rateLimitMs });\n}\n\nexport async function executeSearch(query: string): Promise<SearchResult[]> {\n\treturn getSearchQueue().enqueue(async () => {\n\t\tconst config = getSearchConfig();\n\t\tswitch (config.backend) {\n\t\t\tcase \"searxng\":\n\t\t\t\treturn searchSearXNG(query, config.searxngUrl!);\n\t\t\tcase \"brave\":\n\t\t\t\tif (!config.braveApiKey) throw new Error(\"DREB_BRAVE_API_KEY not set\");\n\t\t\t\treturn searchBrave(query, config.braveApiKey);\n\t\t\tdefault:\n\t\t\t\treturn searchDuckDuckGo(query);\n\t\t}\n\t});\n}\n\nfunction formatSearchCall(\n\targs: { query: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst invalidArg = invalidArgText(theme);\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"web_search\")) +\n\t\t\" \" +\n\t\t(query === null ? invalidArg : theme.fg(\"accent\", `\"${query}\"`))\n\t);\n}\n\nfunction formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebSearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webSearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web. Returns titles, URLs, and snippets. Configure backend via DREB_SEARCH_BACKEND env var (ddg, searxng, brave).\",\n\t\tpromptSnippet: \"Search the web for information\",\n\t\tparameters: webSearchSchema,\n\t\tasync execute(_toolCallId, { query }: { query: string }) {\n\t\t\tlet results: SearchResult[];\n\t\t\ttry {\n\t\t\t\tresults = await executeSearch(query);\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Search failed for \"${query}\": ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `No results found for: ${query}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst formatted = results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join(\"\\n\\n\");\n\t\t\tconst output = `Search results for: ${query}\\n\\n${formatted}`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string): AgentTool<typeof webSearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd));\n}\n\nexport const webSearchToolDefinition = createWebSearchToolDefinition(process.cwd());\nexport const webSearchTool = createWebSearchTool(process.cwd());\n\n// ---------------------------------------------------------------------------\n// web_fetch tool\n// ---------------------------------------------------------------------------\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch\" }),\n});\n\nexport type WebFetchToolInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchToolDetails {\n\ttruncation?: TruncationResult;\n\ttruncatedContent?: boolean;\n}\n\nfunction formatFetchCall(\n\targs: { url: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst url = str(args?.url);\n\tconst invalidArg = invalidArgText(theme);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"web_fetch\"))} ${url === null ? invalidArg : theme.fg(\"accent\", url || \"\")}`;\n}\n\nfunction formatFetchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebFetchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 30;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst details = result.details;\n\tif (details?.truncatedContent || details?.truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (details.truncatedContent) warnings.push(`~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB content limit`);\n\t\tif (details.truncation?.truncated) warnings.push(`${formatSize(DEFAULT_MAX_BYTES)} output limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\n/**\n * Detect GitHub URLs and return guidance to use the `gh` CLI or a local clone\n * instead of fetching. Fetching GitHub web pages is unreliable: `/blob/` URLs\n * return rendered HTML chrome instead of file content, private repos 404 (no\n * auth), and `api.github.com` needs a specific Accept header. The `gh` CLI is\n * authenticated and purpose-built; cloning to /tmp gives direct file access.\n *\n * Returns guidance text for GitHub hosts, or `null` for any other host.\n */\nexport function getGitHubFetchGuidance(parsed: URL): string | null {\n\tconst host = parsed.hostname.toLowerCase().replace(/^www\\./, \"\");\n\tconst isGitHubHost =\n\t\thost === \"github.com\" ||\n\t\thost === \"gist.github.com\" ||\n\t\thost === \"api.github.com\" ||\n\t\thost === \"raw.githubusercontent.com\" ||\n\t\thost.endsWith(\".githubusercontent.com\");\n\tif (!isGitHubHost) {\n\t\treturn null;\n\t}\n\n\tconst path = parsed.pathname;\n\tlet hint: string;\n\tif (host === \"api.github.com\") {\n\t\thint = `Use the \\`gh api\\` command instead, e.g. \\`gh api ${path.replace(/^\\//, \"\")}\\`.`;\n\t} else if (\n\t\thost === \"raw.githubusercontent.com\" ||\n\t\thost.endsWith(\".githubusercontent.com\") ||\n\t\t/\\/(blob|raw)\\//.test(path)\n\t) {\n\t\thint =\n\t\t\t\"To read file contents, clone the repo to /tmp (`git clone <repo> /tmp/<name>`) and read the file locally, or use `gh api` to fetch the raw content.\";\n\t} else if (/\\/pull\\//.test(path)) {\n\t\thint = \"Use `gh pr view <number>` (add `--comments`) or `gh pr diff <number>` instead.\";\n\t} else if (/\\/issues\\//.test(path)) {\n\t\thint = \"Use `gh issue view <number>` (add `--comments`) instead.\";\n\t} else {\n\t\thint =\n\t\t\t\"Use the `gh` CLI (e.g. `gh repo view`, `gh api`, `gh pr view`, `gh issue view`) or clone the repo to /tmp and read files locally.\";\n\t}\n\n\treturn `web_fetch does not work reliably on GitHub URLs (private repos 404, /blob/ pages return HTML chrome, not file content). ${hint}`;\n}\n\nexport function createWebFetchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webFetchSchema, WebFetchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: `Fetch a URL and return its text content. Extracts readable text from HTML pages. Supports PDF text extraction. Content truncated to ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB. Results cached for 15 minutes. GitHub URLs are redirected to guidance for the \\`gh\\` CLI or a local clone instead of being fetched.`,\n\t\tpromptSnippet: \"Fetch a URL and extract its text content\",\n\t\tparameters: webFetchSchema,\n\t\tasync execute(_toolCallId, { url }: { url: string }) {\n\t\t\t// Validate URL\n\t\t\tlet parsed: URL;\n\t\t\ttry {\n\t\t\t\tparsed = new URL(url);\n\t\t\t} catch {\n\t\t\t\t// URL constructor threw — input is not a valid URL\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Invalid URL: ${url}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!parsed.protocol.startsWith(\"http\")) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported protocol: ${parsed.protocol}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Redirect GitHub URLs to gh/clone guidance — fetching them is unreliable\n\t\t\tconst githubGuidance = getGitHubFetchGuidance(parsed);\n\t\t\tif (githubGuidance) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: githubGuidance }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check cache (15-minute TTL, evict stale entries)\n\t\t\tconst cached = fetchCache.get(url);\n\t\t\tif (cached) {\n\t\t\t\tif (Date.now() - cached.timestamp < CACHE_TTL_MS) {\n\t\t\t\t\tconst r = cached.content;\n\t\t\t\t\tconst output = `${r.title}\\n${r.url}\\nFetched: ${r.fetchedAt} (cached)\\n\\n${r.content}`;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tfetchCache.delete(url);\n\t\t\t}\n\n\t\t\t// Fetch (with same-host redirect enforcement)\n\t\t\tlet body: string | Buffer;\n\t\t\tlet contentType: string;\n\t\t\ttry {\n\t\t\t\tconst result = await httpFetch(url);\n\t\t\t\tbody = result.body;\n\t\t\t\tcontentType = result.contentType;\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Failed to fetch ${url}: ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Extract content based on content type\n\t\t\tlet text: string;\n\t\t\tlet title: string;\n\t\t\tconst details: WebFetchToolDetails = {};\n\t\t\tconst fetchedAt = new Date().toISOString();\n\n\t\t\tif (contentType.includes(\"application/pdf\")) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = extractPdfText(body as Buffer);\n\t\t\t} else if (contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml\")) {\n\t\t\t\tconst htmlBody = body as string;\n\t\t\t\ttitle = extractTitle(htmlBody) || url;\n\t\t\t\ttext = stripHtmlToText(htmlBody);\n\t\t\t} else if (\n\t\t\t\tcontentType.includes(\"text/plain\") ||\n\t\t\t\tcontentType.includes(\"application/json\") ||\n\t\t\t\tcontentType.includes(\"text/xml\") ||\n\t\t\t\tcontentType.includes(\"application/xml\")\n\t\t\t) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = body as string;\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported content type: ${contentType}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Truncate to prevent context overflow (~100K characters)\n\t\t\tif (text.length > MAX_CONTENT_LENGTH) {\n\t\t\t\ttext = `${text.slice(0, MAX_CONTENT_LENGTH)}\\n\\n[Content truncated at ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB]`;\n\t\t\t\tdetails.truncatedContent = true;\n\t\t\t}\n\n\t\t\tconst fetchResult: WebFetchResult = { url, title, content: text, fetchedAt };\n\t\t\tfetchCache.set(url, { content: fetchResult, timestamp: Date.now() });\n\n\t\t\tconst output = `${title}\\n${url}\\nFetched: ${fetchedAt}\\n\\n${text}`;\n\t\t\tconst truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails.truncation = truncation;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: truncation.content }],\n\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatFetchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string): AgentTool<typeof webFetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd));\n}\n\nexport const webFetchToolDefinition = createWebFetchToolDefinition(process.cwd());\nexport const webFetchTool = createWebFetchTool(process.cwd());\n"]}
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../../src/core/tools/web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAItF,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAgLnG,QAAA,MAAM,eAAe;;EAEnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,UAAU,YAAY;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB;AAuFD,MAAM,WAAW,eAAe;IAC/B,OAAO,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAkCD,wBAAgB,eAAe,IAAI,eAAe,CAyCjD;AAMD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAa1E;AAuCD,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA2C1E;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,eAAe,CAAC,CAElF;AAED,eAAO,MAAM,uBAAuB;;0CAA+C,CAAC;AACpF,eAAO,MAAM,aAAa;;QAAqC,CAAC;AAMhE,QAAA,MAAM,cAAc;;EAElB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC3B;AA0CD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAiCjE;AAED,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,MAAM,GACV,cAAc,CAAC,OAAO,cAAc,EAAE,mBAAmB,GAAG,SAAS,CAAC,CA4HxE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,cAAc,CAAC,CAEhF;AAED,eAAO,MAAM,sBAAsB;;yCAA8C,CAAC;AAClF,eAAO,MAAM,YAAY;;QAAoC,CAAC","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { CONFIG_DIR_NAME } from \"../../config.js\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { log } from \"../logger.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\nimport { WebSearchQueue } from \"./web-search-queue.js\";\n\n// ---------------------------------------------------------------------------\n// Shared: HTTP fetching and HTML extraction\n// ---------------------------------------------------------------------------\n\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_CONTENT_LENGTH = 100_000;\nconst CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes\n\nconst fetchCache = new Map<string, { content: WebFetchResult; timestamp: number }>();\n\ninterface WebFetchResult {\n\turl: string;\n\ttitle: string;\n\tcontent: string;\n\tfetchedAt: string;\n}\n\nfunction stripHtmlToText(html: string): string {\n\tlet text = html;\n\t// Remove script/style/nav/footer blocks entirely\n\ttext = text.replace(/<(script|style|nav|footer|header|aside|iframe|noscript)\\b[^>]*>[\\s\\S]*?<\\/\\1>/gi, \"\");\n\t// Convert block elements to newlines\n\ttext = text.replace(/<\\/(p|div|li|tr|h[1-6]|blockquote|pre|section|article)>/gi, \"\\n\");\n\ttext = text.replace(/<(br|hr)\\s*\\/?>/gi, \"\\n\");\n\t// Convert links to text with URL\n\ttext = text.replace(/<a\\b[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, \"$2 ($1)\");\n\t// Convert headings to markdown-style\n\ttext = text.replace(/<h([1-6])\\b[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_match, level, content) => {\n\t\treturn `\\n${\"#\".repeat(Number(level))} ${content.trim()}\\n`;\n\t});\n\t// Convert list items\n\ttext = text.replace(/<li\\b[^>]*>/gi, \"\\n- \");\n\t// Strip all remaining tags\n\ttext = text.replace(/<[^>]+>/g, \"\");\n\t// Decode common HTML entities\n\ttext = text.replace(/&amp;/g, \"&\");\n\ttext = text.replace(/&lt;/g, \"<\");\n\ttext = text.replace(/&gt;/g, \">\");\n\ttext = text.replace(/&quot;/g, '\"');\n\ttext = text.replace(/&#39;/g, \"'\");\n\ttext = text.replace(/&nbsp;/g, \" \");\n\t// Collapse whitespace\n\ttext = text.replace(/[ \\t]+/g, \" \");\n\ttext = text.replace(/\\n{3,}/g, \"\\n\\n\");\n\treturn text.trim();\n}\n\nfunction extractTitle(html: string): string {\n\tconst match = html.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i);\n\treturn match ? match[1].trim().replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : \"\";\n}\n\nconst FETCH_HEADERS = {\n\t\"User-Agent\": \"dreb/1.0 (web fetch tool)\",\n\tAccept: \"text/html,application/xhtml+xml,text/plain,application/pdf\",\n};\n\n// Block fetches to private/internal networks to prevent SSRF\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"0.0.0.0\"]);\n\nfunction isPrivateHost(hostname: string): boolean {\n\tif (BLOCKED_HOSTNAMES.has(hostname)) return true;\n\t// IPv4 private ranges\n\tconst ipv4Match = hostname.match(/^(\\d+)\\.(\\d+)\\.\\d+\\.\\d+$/);\n\tif (ipv4Match) {\n\t\tconst [, first, second] = ipv4Match.map(Number);\n\t\tif (first === 10) return true; // 10.0.0.0/8\n\t\tif (first === 172 && second >= 16 && second <= 31) return true; // 172.16.0.0/12\n\t\tif (first === 192 && second === 168) return true; // 192.168.0.0/16\n\t\tif (first === 169 && second === 254) return true; // link-local 169.254.0.0/16\n\t}\n\t// IPv6 loopback, link-local, and ULA (fc00::/7)\n\tif (hostname.startsWith(\"[\")) {\n\t\tconst lh = hostname.toLowerCase();\n\t\tif (lh.includes(\"::1\") || lh.startsWith(\"[fe80:\") || lh.startsWith(\"[fc\") || lh.startsWith(\"[fd\")) return true;\n\t}\n\treturn false;\n}\n\nfunction buildResponse(response: Response): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst ct = response.headers.get(\"content-type\") || \"\";\n\tif (ct.includes(\"application/pdf\")) {\n\t\treturn response.arrayBuffer().then((buf) => ({ body: Buffer.from(buf), contentType: ct }));\n\t}\n\treturn response.text().then((text) => ({ body: text, contentType: ct }));\n}\n\nasync function httpFetch(url: string): Promise<{ body: string | Buffer; contentType: string }> {\n\tconst originalHost = new URL(url).hostname;\n\tif (isPrivateHost(originalHost)) {\n\t\tthrow new Error(`Blocked: ${originalHost} is a private/internal address`);\n\t}\n\n\t// Manual redirect loop to enforce same-host on every hop\n\tlet currentUrl = url;\n\tconst maxRedirects = 10;\n\tfor (let i = 0; i <= maxRedirects; i++) {\n\t\tconst response = await fetch(currentUrl, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: FETCH_HEADERS,\n\t\t\tredirect: \"manual\",\n\t\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t\t});\n\n\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) {\n\t\t\t\tthrow new Error(`HTTP ${response.status}: redirect with no Location header`);\n\t\t\t}\n\t\t\tconst redirectUrl = new URL(location, currentUrl);\n\t\t\t// Block private IPs before revealing them in cross-host messages\n\t\t\tif (isPrivateHost(redirectUrl.hostname)) {\n\t\t\t\tthrow new Error(`Blocked: redirect to private/internal address`);\n\t\t\t}\n\t\t\tif (redirectUrl.hostname !== originalHost) {\n\t\t\t\treturn {\n\t\t\t\t\tbody: `Cross-host redirect detected.\\nOriginal: ${url}\\nRedirects to: ${redirectUrl.href}\\n\\nThe redirect target is on a different host. Fetch the new URL directly if you want to follow it.`,\n\t\t\t\t\tcontentType: \"text/plain\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tcurrentUrl = redirectUrl.href;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst errorBody = await response.text();\n\t\t\tthrow new Error(`HTTP ${response.status}: ${errorBody.slice(0, 200)}`);\n\t\t}\n\n\t\treturn buildResponse(response);\n\t}\n\tthrow new Error(`Too many redirects (${maxRedirects})`);\n}\n\n// -- PDF text extraction (basic) ---------------------------------------------\n\nfunction extractPdfText(buffer: Buffer): string {\n\t// Minimal PDF text extraction — only works on uncompressed PDFs with literal\n\t// string objects in BT/ET text blocks. Most production PDFs use FlateDecode\n\t// compression and will fall through to the failure message.\n\t// latin1 preserves raw byte values 0-255 as code points for safe regex matching.\n\tconst raw = buffer.toString(\"latin1\");\n\tconst textChunks: string[] = [];\n\n\tconst btEtRegex = /BT\\s([\\s\\S]*?)ET/g;\n\tfor (const match of raw.matchAll(btEtRegex)) {\n\t\tconst block = match[1];\n\t\tconst strRegex = /\\(([^)]*)\\)/g;\n\t\tfor (const strMatch of block.matchAll(strRegex)) {\n\t\t\tconst decoded = strMatch[1]\n\t\t\t\t.replace(/\\\\n/g, \"\\n\")\n\t\t\t\t.replace(/\\\\r/g, \"\\r\")\n\t\t\t\t.replace(/\\\\t/g, \"\\t\")\n\t\t\t\t.replace(/\\\\\\(/g, \"(\")\n\t\t\t\t.replace(/\\\\\\)/g, \")\")\n\t\t\t\t.replace(/\\\\\\\\/g, \"\\\\\");\n\t\t\tif (decoded.trim()) {\n\t\t\t\ttextChunks.push(decoded);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (textChunks.length === 0) {\n\t\treturn \"[PDF text extraction failed — the PDF may use embedded fonts or image-based content that requires OCR]\";\n\t}\n\n\treturn textChunks.join(\" \").replace(/\\s+/g, \" \").trim();\n}\n\n// ---------------------------------------------------------------------------\n// web_search tool\n// ---------------------------------------------------------------------------\n\nconst webSearchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query\" }),\n});\n\nexport type WebSearchToolInput = Static<typeof webSearchSchema>;\n\nexport interface WebSearchToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface SearchResult {\n\ttitle: string;\n\turl: string;\n\tsnippet: string;\n}\n\nasync function searchDuckDuckGo(query: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://html.duckduckgo.com/html/?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\t\"User-Agent\": \"dreb/1.0 (web search tool)\",\n\t\t\tAccept: \"text/html\",\n\t\t},\n\t\tredirect: \"follow\",\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`DuckDuckGo search failed: HTTP ${response.status}`);\n\t}\n\tconst html = await response.text();\n\tconst results: SearchResult[] = [];\n\n\t// Parse DuckDuckGo HTML results — split on result block class\n\tconst resultBlocks = html.split(/class=\"result results_links/);\n\tfor (const block of resultBlocks.slice(1, 11)) {\n\t\tconst titleMatch = block.match(/class=\"result__a\"[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/);\n\t\tconst snippetMatch = block.match(/class=\"result__snippet\"[^>]*>([\\s\\S]*?)<\\/(?:a|td|div)/);\n\n\t\tif (titleMatch) {\n\t\t\tlet url = titleMatch[1];\n\t\t\t// DDG wraps URLs in a redirect — extract the actual URL\n\t\t\tconst uddgMatch = url.match(/uddg=([^&]*)/);\n\t\t\tif (uddgMatch) {\n\t\t\t\turl = decodeURIComponent(uddgMatch[1]);\n\t\t\t}\n\t\t\tconst title = titleMatch[2].replace(/<[^>]+>/g, \"\").trim();\n\t\t\tconst snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, \"\").trim() : \"\";\n\t\t\tif (title && url) {\n\t\t\t\tresults.push({ title, url, snippet });\n\t\t\t}\n\t\t}\n\t}\n\tif (results.length === 0 && html.length > 1000) {\n\t\t// Got a substantial response but parsed 0 results — DDG HTML structure may have changed\n\t\tlog.warn(\"Warning: DDG returned HTML but 0 results were parsed. The HTML structure may have changed.\");\n\t}\n\treturn results;\n}\n\nasync function searchSearXNG(query: string, baseUrl: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`${baseUrl}/search?q=${encodedQuery}&format=json`, {\n\t\tmethod: \"GET\",\n\t\theaders: { Accept: \"application/json\" },\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`SearXNG search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as { results?: Array<{ title: string; url: string; content?: string }> };\n\treturn (data.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.content || \"\",\n\t}));\n}\n\nasync function searchBrave(query: string, apiKey: string): Promise<SearchResult[]> {\n\tconst encodedQuery = encodeURIComponent(query);\n\tconst response = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodedQuery}`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"X-Subscription-Token\": apiKey,\n\t\t},\n\t\tsignal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Brave search failed: HTTP ${response.status}`);\n\t}\n\tconst data = (await response.json()) as {\n\t\tweb?: { results?: Array<{ title: string; url: string; description?: string }> };\n\t};\n\treturn (data.web?.results || []).slice(0, 10).map((r) => ({\n\t\ttitle: r.title,\n\t\turl: r.url,\n\t\tsnippet: r.description || \"\",\n\t}));\n}\n\nexport interface WebSearchConfig {\n\tbackend?: \"ddg\" | \"searxng\" | \"brave\";\n\tsearxngUrl?: string;\n\tbraveApiKey?: string;\n\trateLimitMs?: number;\n}\n\ninterface DrebConfig {\n\tsearch?: {\n\t\tbackend?: string;\n\t\tsearxng_url?: string;\n\t\tbrave_api_key?: string;\n\t\trate_limit_ms?: number;\n\t};\n}\n\nconst VALID_BACKENDS = [\"ddg\", \"searxng\", \"brave\"] as const;\n\nfunction loadDrebConfig(): DrebConfig {\n\t// Config file precedence: project-local > home directory. First valid file wins.\n\tconst candidates = [\n\t\tjoin(process.cwd(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(process.cwd(), \".dreb\", \"config.json\"),\n\t\tjoin(homedir(), CONFIG_DIR_NAME, \"config.json\"),\n\t\tjoin(homedir(), \".dreb\", \"config.json\"),\n\t];\n\tfor (const configPath of candidates) {\n\t\tif (existsSync(configPath)) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(readFileSync(configPath, \"utf-8\")) as DrebConfig;\n\t\t\t} catch (err) {\n\t\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\t\tlog.warn(`Warning: failed to parse config at ${configPath}: ${msg}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn {};\n}\n\nexport function getSearchConfig(): WebSearchConfig {\n\tconst fileConfig = loadDrebConfig();\n\t// Environment variables override config file\n\tconst rawBackend = process.env.DREB_SEARCH_BACKEND || fileConfig.search?.backend;\n\tlet backend: WebSearchConfig[\"backend\"] = \"ddg\";\n\tif (rawBackend) {\n\t\tif ((VALID_BACKENDS as readonly string[]).includes(rawBackend)) {\n\t\t\tbackend = rawBackend as WebSearchConfig[\"backend\"];\n\t\t} else {\n\t\t\tlog.warn(\n\t\t\t\t`Warning: unrecognized search backend \"${rawBackend}\", falling back to ddg. Valid: ${VALID_BACKENDS.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst rateLimitEnv = process.env.DREB_WEB_SEARCH_RATE_LIMIT_MS;\n\tlet rateLimitMs = 10_000;\n\tif (rateLimitEnv) {\n\t\tconst parsed = parseInt(rateLimitEnv, 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tlog.warn(`Warning: invalid DREB_WEB_SEARCH_RATE_LIMIT_MS \"${rateLimitEnv}\", using default`);\n\t\t}\n\t} else if (fileConfig.search?.rate_limit_ms !== undefined) {\n\t\tconst parsed = parseInt(String(fileConfig.search.rate_limit_ms), 10);\n\t\tif (!Number.isNaN(parsed) && parsed >= 0) {\n\t\t\trateLimitMs = parsed;\n\t\t} else {\n\t\t\tlog.warn(\n\t\t\t\t`Warning: invalid search.rate_limit_ms in config file \"${fileConfig.search.rate_limit_ms}\", using default`,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn {\n\t\tbackend,\n\t\tsearxngUrl: process.env.DREB_SEARXNG_URL || fileConfig.search?.searxng_url || \"http://localhost:8888\",\n\t\tbraveApiKey: process.env.DREB_BRAVE_API_KEY || fileConfig.search?.brave_api_key,\n\t\trateLimitMs,\n\t};\n}\n\nfunction getSearchQueue(): WebSearchQueue {\n\treturn new WebSearchQueue({ rateLimitMs: getSearchConfig().rateLimitMs });\n}\n\nexport async function executeSearch(query: string): Promise<SearchResult[]> {\n\treturn getSearchQueue().enqueue(async () => {\n\t\tconst config = getSearchConfig();\n\t\tswitch (config.backend) {\n\t\t\tcase \"searxng\":\n\t\t\t\treturn searchSearXNG(query, config.searxngUrl!);\n\t\t\tcase \"brave\":\n\t\t\t\tif (!config.braveApiKey) throw new Error(\"DREB_BRAVE_API_KEY not set\");\n\t\t\t\treturn searchBrave(query, config.braveApiKey);\n\t\t\tdefault:\n\t\t\t\treturn searchDuckDuckGo(query);\n\t\t}\n\t});\n}\n\nfunction formatSearchCall(\n\targs: { query: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst invalidArg = invalidArgText(theme);\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(\"web_search\")) +\n\t\t\" \" +\n\t\t(query === null ? invalidArg : theme.fg(\"accent\", `\"${query}\"`))\n\t);\n}\n\nfunction formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebSearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 15;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webSearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web. Returns titles, URLs, and snippets. Configure backend via DREB_SEARCH_BACKEND env var (ddg, searxng, brave).\",\n\t\tpromptSnippet: \"Search the web for information\",\n\t\tparameters: webSearchSchema,\n\t\tasync execute(_toolCallId, { query }: { query: string }) {\n\t\t\tlet results: SearchResult[];\n\t\t\ttry {\n\t\t\t\tresults = await executeSearch(query);\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Search failed for \"${query}\": ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `No results found for: ${query}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst formatted = results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join(\"\\n\\n\");\n\t\t\tconst output = `Search results for: ${query}\\n\\n${formatted}`;\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebSearchTool(cwd: string): AgentTool<typeof webSearchSchema> {\n\treturn wrapToolDefinition(createWebSearchToolDefinition(cwd));\n}\n\nexport const webSearchToolDefinition = createWebSearchToolDefinition(process.cwd());\nexport const webSearchTool = createWebSearchTool(process.cwd());\n\n// ---------------------------------------------------------------------------\n// web_fetch tool\n// ---------------------------------------------------------------------------\n\nconst webFetchSchema = Type.Object({\n\turl: Type.String({ description: \"The URL to fetch\" }),\n});\n\nexport type WebFetchToolInput = Static<typeof webFetchSchema>;\n\nexport interface WebFetchToolDetails {\n\ttruncation?: TruncationResult;\n\ttruncatedContent?: boolean;\n}\n\nfunction formatFetchCall(\n\targs: { url: string } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst url = str(args?.url);\n\tconst invalidArg = invalidArgText(theme);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"web_fetch\"))} ${url === null ? invalidArg : theme.fg(\"accent\", url || \"\")}`;\n}\n\nfunction formatFetchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: WebFetchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 30;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\tconst details = result.details;\n\tif (details?.truncatedContent || details?.truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (details.truncatedContent) warnings.push(`~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB content limit`);\n\t\tif (details.truncation?.truncated) warnings.push(`${formatSize(DEFAULT_MAX_BYTES)} output limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\n/**\n * Detect GitHub URLs and return guidance to use the `gh` CLI or a local clone\n * instead of fetching. Fetching GitHub web pages is unreliable: `/blob/` URLs\n * return rendered HTML chrome instead of file content, private repos 404 (no\n * auth), and `api.github.com` needs a specific Accept header. The `gh` CLI is\n * authenticated and purpose-built; cloning to /tmp gives direct file access.\n *\n * Returns guidance text for GitHub hosts, or `null` for any other host.\n */\nexport function getGitHubFetchGuidance(parsed: URL): string | null {\n\tconst host = parsed.hostname.toLowerCase().replace(/^www\\./, \"\");\n\tconst isGitHubHost =\n\t\thost === \"github.com\" ||\n\t\thost === \"gist.github.com\" ||\n\t\thost === \"api.github.com\" ||\n\t\thost === \"raw.githubusercontent.com\" ||\n\t\thost.endsWith(\".githubusercontent.com\");\n\tif (!isGitHubHost) {\n\t\treturn null;\n\t}\n\n\tconst path = parsed.pathname;\n\tlet hint: string;\n\tif (host === \"api.github.com\") {\n\t\thint = `Use the \\`gh api\\` command instead, e.g. \\`gh api ${path.replace(/^\\//, \"\")}\\`.`;\n\t} else if (\n\t\thost === \"raw.githubusercontent.com\" ||\n\t\thost.endsWith(\".githubusercontent.com\") ||\n\t\t/\\/(blob|raw)\\//.test(path)\n\t) {\n\t\thint =\n\t\t\t\"To read file contents, clone the repo to /tmp (`git clone <repo> /tmp/<name>`) and read the file locally, or use `gh api` to fetch the raw content.\";\n\t} else if (/\\/pull\\//.test(path)) {\n\t\thint = \"Use `gh pr view <number>` (add `--comments`) or `gh pr diff <number>` instead.\";\n\t} else if (/\\/issues\\//.test(path)) {\n\t\thint = \"Use `gh issue view <number>` (add `--comments`) instead.\";\n\t} else {\n\t\thint =\n\t\t\t\"Use the `gh` CLI (e.g. `gh repo view`, `gh api`, `gh pr view`, `gh issue view`) or clone the repo to /tmp and read files locally.\";\n\t}\n\n\treturn `web_fetch does not work reliably on GitHub URLs (private repos 404, /blob/ pages return HTML chrome, not file content). ${hint}`;\n}\n\nexport function createWebFetchToolDefinition(\n\t_cwd: string,\n): ToolDefinition<typeof webFetchSchema, WebFetchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_fetch\",\n\t\tlabel: \"web_fetch\",\n\t\tdescription: `Fetch a URL and return its text content. Extracts readable text from HTML pages. Supports PDF text extraction. Content truncated to ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB. Results cached for 15 minutes. GitHub URLs are redirected to guidance for the \\`gh\\` CLI or a local clone instead of being fetched.`,\n\t\tpromptSnippet: \"Fetch a URL and extract its text content\",\n\t\tparameters: webFetchSchema,\n\t\tasync execute(_toolCallId, { url }: { url: string }) {\n\t\t\t// Validate URL\n\t\t\tlet parsed: URL;\n\t\t\ttry {\n\t\t\t\tparsed = new URL(url);\n\t\t\t} catch {\n\t\t\t\t// URL constructor threw — input is not a valid URL\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Invalid URL: ${url}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!parsed.protocol.startsWith(\"http\")) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported protocol: ${parsed.protocol}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Redirect GitHub URLs to gh/clone guidance — fetching them is unreliable\n\t\t\tconst githubGuidance = getGitHubFetchGuidance(parsed);\n\t\t\tif (githubGuidance) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: githubGuidance }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Check cache (15-minute TTL, evict stale entries)\n\t\t\tconst cached = fetchCache.get(url);\n\t\t\tif (cached) {\n\t\t\t\tif (Date.now() - cached.timestamp < CACHE_TTL_MS) {\n\t\t\t\t\tconst r = cached.content;\n\t\t\t\t\tconst output = `${r.title}\\n${r.url}\\nFetched: ${r.fetchedAt} (cached)\\n\\n${r.content}`;\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tfetchCache.delete(url);\n\t\t\t}\n\n\t\t\t// Fetch (with same-host redirect enforcement)\n\t\t\tlet body: string | Buffer;\n\t\t\tlet contentType: string;\n\t\t\ttry {\n\t\t\t\tconst result = await httpFetch(url);\n\t\t\t\tbody = result.body;\n\t\t\t\tcontentType = result.contentType;\n\t\t\t} catch (error) {\n\t\t\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Failed to fetch ${url}: ${msg}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Extract content based on content type\n\t\t\tlet text: string;\n\t\t\tlet title: string;\n\t\t\tconst details: WebFetchToolDetails = {};\n\t\t\tconst fetchedAt = new Date().toISOString();\n\n\t\t\tif (contentType.includes(\"application/pdf\")) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = extractPdfText(body as Buffer);\n\t\t\t} else if (contentType.includes(\"text/html\") || contentType.includes(\"application/xhtml\")) {\n\t\t\t\tconst htmlBody = body as string;\n\t\t\t\ttitle = extractTitle(htmlBody) || url;\n\t\t\t\ttext = stripHtmlToText(htmlBody);\n\t\t\t} else if (\n\t\t\t\tcontentType.includes(\"text/plain\") ||\n\t\t\t\tcontentType.includes(\"application/json\") ||\n\t\t\t\tcontentType.includes(\"text/xml\") ||\n\t\t\t\tcontentType.includes(\"application/xml\")\n\t\t\t) {\n\t\t\t\ttitle = url;\n\t\t\t\ttext = body as string;\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Unsupported content type: ${contentType}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Truncate to prevent context overflow (~100K characters)\n\t\t\tif (text.length > MAX_CONTENT_LENGTH) {\n\t\t\t\ttext = `${text.slice(0, MAX_CONTENT_LENGTH)}\\n\\n[Content truncated at ~${Math.round(MAX_CONTENT_LENGTH / 1024)}KB]`;\n\t\t\t\tdetails.truncatedContent = true;\n\t\t\t}\n\n\t\t\tconst fetchResult: WebFetchResult = { url, title, content: text, fetchedAt };\n\t\t\tfetchCache.set(url, { content: fetchResult, timestamp: Date.now() });\n\n\t\t\tconst output = `${title}\\n${url}\\nFetched: ${fetchedAt}\\n\\n${text}`;\n\t\t\tconst truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails.truncation = truncation;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: truncation.content }],\n\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatFetchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0, undefined, true);\n\t\t\ttext.setText(formatFetchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWebFetchTool(cwd: string): AgentTool<typeof webFetchSchema> {\n\treturn wrapToolDefinition(createWebFetchToolDefinition(cwd));\n}\n\nexport const webFetchToolDefinition = createWebFetchToolDefinition(process.cwd());\nexport const webFetchTool = createWebFetchTool(process.cwd());\n"]}
@@ -378,12 +378,12 @@ export function createWebSearchToolDefinition(_cwd) {
378
378
  };
379
379
  },
380
380
  renderCall(args, theme, context) {
381
- const text = context.lastComponent ?? new Text("", 0, 0);
381
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
382
382
  text.setText(formatSearchCall(args, theme));
383
383
  return text;
384
384
  },
385
385
  renderResult(result, options, theme, context) {
386
- const text = context.lastComponent ?? new Text("", 0, 0);
386
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
387
387
  text.setText(formatSearchResult(result, options, theme, context.showImages));
388
388
  return text;
389
389
  },
@@ -578,12 +578,12 @@ export function createWebFetchToolDefinition(_cwd) {
578
578
  };
579
579
  },
580
580
  renderCall(args, theme, context) {
581
- const text = context.lastComponent ?? new Text("", 0, 0);
581
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
582
582
  text.setText(formatFetchCall(args, theme));
583
583
  return text;
584
584
  },
585
585
  renderResult(result, options, theme, context) {
586
- const text = context.lastComponent ?? new Text("", 0, 0);
586
+ const text = context.lastComponent ?? new Text("", 0, 0, undefined, true);
587
587
  text.setText(formatFetchResult(result, options, theme, context.showImages));
588
588
  return text;
589
589
  },