@jcheesepkg/nanobot 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/agent/loop.d.mts +3 -0
  2. package/dist/agent/loop.d.mts.map +1 -1
  3. package/dist/agent/loop.mjs +61 -26
  4. package/dist/agent/loop.mjs.map +1 -1
  5. package/dist/agent/subagent.d.mts +2 -0
  6. package/dist/agent/subagent.d.mts.map +1 -1
  7. package/dist/agent/subagent.mjs +8 -8
  8. package/dist/agent/subagent.mjs.map +1 -1
  9. package/dist/agent/tools/filesystem.d.mts +16 -0
  10. package/dist/agent/tools/filesystem.d.mts.map +1 -1
  11. package/dist/agent/tools/filesystem.mjs +37 -1
  12. package/dist/agent/tools/filesystem.mjs.map +1 -1
  13. package/dist/channels/line.d.mts +131 -0
  14. package/dist/channels/line.d.mts.map +1 -0
  15. package/dist/channels/line.mjs +265 -0
  16. package/dist/channels/line.mjs.map +1 -0
  17. package/dist/channels/manager.d.mts.map +1 -1
  18. package/dist/channels/manager.mjs +8 -0
  19. package/dist/channels/manager.mjs.map +1 -1
  20. package/dist/cli/index.mjs +31 -21
  21. package/dist/cli/index.mjs.map +1 -1
  22. package/dist/config/schema.d.mts +454 -46
  23. package/dist/config/schema.d.mts.map +1 -1
  24. package/dist/config/schema.mjs +96 -33
  25. package/dist/config/schema.mjs.map +1 -1
  26. package/dist/gateway/server.d.mts +2 -0
  27. package/dist/gateway/server.d.mts.map +1 -1
  28. package/dist/gateway/server.mjs +17 -1
  29. package/dist/gateway/server.mjs.map +1 -1
  30. package/dist/index.d.mts +3 -1
  31. package/dist/index.d.mts.map +1 -1
  32. package/dist/index.mjs +3 -1
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/providers/base.d.mts +1 -0
  35. package/dist/providers/base.d.mts.map +1 -1
  36. package/dist/providers/openai-provider.d.mts +3 -0
  37. package/dist/providers/openai-provider.d.mts.map +1 -1
  38. package/dist/providers/openai-provider.mjs +5 -2
  39. package/dist/providers/openai-provider.mjs.map +1 -1
  40. package/dist/providers/registry.d.mts +65 -0
  41. package/dist/providers/registry.d.mts.map +1 -0
  42. package/dist/providers/registry.mjs +221 -0
  43. package/dist/providers/registry.mjs.map +1 -0
  44. package/package.json +1 -1
@@ -30,6 +30,8 @@ declare class AgentLoop {
30
30
  readonly tools: ToolRegistry;
31
31
  readonly subagents: SubagentManager;
32
32
  private _running;
33
+ /** In-flight AbortControllers keyed by session key. */
34
+ private inflight;
33
35
  constructor(params: {
34
36
  bus: MessageBus;
35
37
  provider: LLMProvider;
@@ -39,6 +41,7 @@ declare class AgentLoop {
39
41
  maxIterations?: number;
40
42
  braveApiKey?: string;
41
43
  execConfig?: ExecToolConfig;
44
+ restrictToWorkspace?: boolean;
42
45
  cronService?: CronService;
43
46
  toolsEnabled?: string[];
44
47
  toolsDisabled?: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.mts","names":[],"sources":["../../src/agent/loop.ts"],"mappings":";;;;;;;;;;;;;AAmCA;;;;;;;cAAa,SAAA;EAAA,QACH,GAAA;EAAA,QACA,QAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,SAAA;EAAA,QACA,aAAA;EAAA,SAEC,OAAA,EAAS,cAAA;EAAA,SACT,QAAA,EAAU,cAAA;EAAA,SACV,KAAA,EAAO,YAAA;EAAA,SACP,SAAA,EAAW,eAAA;EAAA,QAEZ,QAAA;cAEI,MAAA;IACV,GAAA,EAAK,UAAA;IACL,QAAA,EAAU,WAAA;IACV,SAAA;IACA,KAAA;IACA,SAAA;IACA,aAAA;IACA,WAAA;IACA,UAAA,GAAa,cAAA;IACb,WAAA,GAAc,WAAA;IACd,YAAA;IACA,aAAA;IACA,WAAA,GAAc,IAAA;EAAA;EAAA,QAoCR,oBAAA;EA9CN;EA4GI,GAAA,CAAA,GAAO,OAAA;EA3GX;EAyIF,IAAA,CAAA;EAvIE;EAAA,QA6IY,cAAA;EAAA,QA4CA,oBAAA;EAAA,QA4CA,YAAA;EAAA,QAoDN,kBAAA;EArRN;EAuSI,aAAA,CACJ,OAAA,UACA,UAAA,WACA,OAAA,WACA,MAAA,YACC,OAAA;AAAA"}
1
+ {"version":3,"file":"loop.d.mts","names":[],"sources":["../../src/agent/loop.ts"],"mappings":";;;;;;;;;;;;;AAmCA;;;;;;;cAAa,SAAA;EAAA,QACH,GAAA;EAAA,QACA,QAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,SAAA;EAAA,QACA,aAAA;EAAA,SAEC,OAAA,EAAS,cAAA;EAAA,SACT,QAAA,EAAU,cAAA;EAAA,SACV,KAAA,EAAO,YAAA;EAAA,SACP,SAAA,EAAW,eAAA;EAAA,QAEZ,QAAA;EATA;EAAA,QAYA,QAAA;cAEI,MAAA;IACV,GAAA,EAAK,UAAA;IACL,QAAA,EAAU,WAAA;IACV,SAAA;IACA,KAAA;IACA,SAAA;IACA,aAAA;IACA,WAAA;IACA,UAAA,GAAa,cAAA;IACb,mBAAA;IACA,WAAA,GAAc,WAAA;IACd,YAAA;IACA,aAAA;IACA,WAAA,GAAc,IAAA;EAAA;EAAA,QAoCR,oBAAA;EA9CN;EA8GI,GAAA,CAAA,GAAO,OAAA;EA5GX;EA6IF,IAAA,CAAA;EA3IE;EAAA,QAiJY,cAAA;EAAA,QA6EA,oBAAA;EAAA,QA4CA,YAAA;EAAA,QAwDN,kBAAA;EA/TQ;EAiVV,aAAA,CACJ,OAAA,UACA,UAAA,WACA,OAAA,WACA,MAAA,YACC,OAAA;AAAA"}
@@ -32,6 +32,8 @@ var AgentLoop = class {
32
32
  tools;
33
33
  subagents;
34
34
  _running = false;
35
+ /** In-flight AbortControllers keyed by session key. */
36
+ inflight = /* @__PURE__ */ new Map();
35
37
  constructor(params) {
36
38
  this.bus = params.bus;
37
39
  this.provider = params.provider;
@@ -39,10 +41,8 @@ var AgentLoop = class {
39
41
  this.model = params.model ?? params.provider.getDefaultModel();
40
42
  this.maxTokens = params.maxTokens ?? 8192;
41
43
  this.maxIterations = params.maxIterations ?? 20;
42
- const execConfig = params.execConfig ?? {
43
- timeout: 60,
44
- restrictToWorkspace: false
45
- };
44
+ const execConfig = params.execConfig ?? { timeout: 60 };
45
+ const restrictToWorkspace = params.restrictToWorkspace ?? false;
46
46
  this.context = new ContextBuilder(params.workspace);
47
47
  this.sessions = new SessionManager(params.workspace);
48
48
  this.tools = new ToolRegistry();
@@ -52,11 +52,12 @@ var AgentLoop = class {
52
52
  bus: params.bus,
53
53
  model: this.model,
54
54
  braveApiKey: params.braveApiKey,
55
- execConfig
55
+ execConfig,
56
+ restrictToWorkspace
56
57
  });
57
- this.registerDefaultTools(execConfig, params.braveApiKey, params.cronService, params.toolsEnabled, params.toolsDisabled, params.customTools);
58
+ this.registerDefaultTools(execConfig, restrictToWorkspace, params.braveApiKey, params.cronService, params.toolsEnabled, params.toolsDisabled, params.customTools);
58
59
  }
59
- registerDefaultTools(execConfig, braveApiKey, cronService, toolsEnabled, toolsDisabled, customTools) {
60
+ registerDefaultTools(execConfig, restrictToWorkspace, braveApiKey, cronService, toolsEnabled, toolsDisabled, customTools) {
60
61
  const enabled = new Set(toolsEnabled ?? []);
61
62
  const disabled = new Set(toolsDisabled ?? []);
62
63
  const hasAllowlist = enabled.size > 0;
@@ -64,14 +65,15 @@ var AgentLoop = class {
64
65
  const registerIfEnabled = (tool) => {
65
66
  if (shouldRegister(tool.name)) this.tools.register(tool);
66
67
  };
67
- registerIfEnabled(new ReadFileTool());
68
- registerIfEnabled(new WriteFileTool());
69
- registerIfEnabled(new EditFileTool());
70
- registerIfEnabled(new ListDirTool());
68
+ const allowedDir = restrictToWorkspace ? this.workspace : void 0;
69
+ registerIfEnabled(new ReadFileTool({ allowedDir }));
70
+ registerIfEnabled(new WriteFileTool({ allowedDir }));
71
+ registerIfEnabled(new EditFileTool({ allowedDir }));
72
+ registerIfEnabled(new ListDirTool({ allowedDir }));
71
73
  registerIfEnabled(new ExecTool({
72
74
  workingDir: this.workspace,
73
75
  timeout: execConfig.timeout,
74
- restrictToWorkspace: execConfig.restrictToWorkspace
76
+ restrictToWorkspace
75
77
  }));
76
78
  registerIfEnabled(new WebSearchTool({ apiKey: braveApiKey }));
77
79
  registerIfEnabled(new WebFetchTool());
@@ -86,17 +88,17 @@ var AgentLoop = class {
86
88
  console.log("Agent loop started");
87
89
  while (this._running) try {
88
90
  const msg = await this.bus.consumeInboundTimeout(1e3);
89
- try {
90
- const response = await this.processMessage(msg);
91
+ this.processMessage(msg).then(async (response) => {
91
92
  if (response) await this.bus.publishOutbound(response);
92
- } catch (err) {
93
+ }).catch(async (err) => {
94
+ if (isAbortError(err)) return;
93
95
  console.error("Error processing message:", err);
94
96
  await this.bus.publishOutbound(createOutboundMessage({
95
97
  channel: msg.channel,
96
98
  chatId: msg.chatId,
97
99
  content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : err}`
98
100
  }));
99
- }
101
+ });
100
102
  } catch {}
101
103
  }
102
104
  /** Stop the agent loop. */
@@ -109,6 +111,13 @@ var AgentLoop = class {
109
111
  if (msg.channel === "system") return this.processSystemMessage(msg);
110
112
  console.log(`Processing message from ${msg.channel}:${msg.senderId}`);
111
113
  const sessionKey = `${msg.channel}:${msg.chatId}`;
114
+ const existing = this.inflight.get(sessionKey);
115
+ if (existing) {
116
+ console.log(`Aborting in-flight request for ${sessionKey}`);
117
+ existing.abort();
118
+ }
119
+ const controller = new AbortController();
120
+ this.inflight.set(sessionKey, controller);
112
121
  const session = this.sessions.getOrCreate(sessionKey);
113
122
  this.updateToolContexts(msg.channel, msg.chatId);
114
123
  const messages = this.context.buildMessages({
@@ -119,14 +128,29 @@ var AgentLoop = class {
119
128
  chatId: msg.chatId
120
129
  });
121
130
  const newMsgStart = 1 + session.getHistory().length;
122
- const finalContent = await this.runAgentLoop(messages);
123
- session.addTurnMessages(messages.slice(newMsgStart));
124
- this.sessions.save(session);
125
- return createOutboundMessage({
126
- channel: msg.channel,
127
- chatId: msg.chatId,
128
- content: finalContent
129
- });
131
+ try {
132
+ const finalContent = await this.runAgentLoop(messages, controller.signal);
133
+ session.addTurnMessages(messages.slice(newMsgStart));
134
+ this.sessions.save(session);
135
+ return createOutboundMessage({
136
+ channel: msg.channel,
137
+ chatId: msg.chatId,
138
+ content: finalContent
139
+ });
140
+ } catch (err) {
141
+ if (isAbortError(err)) {
142
+ const userMessages = messages.slice(newMsgStart).filter((m) => m.role === "user");
143
+ if (userMessages.length > 0) {
144
+ session.addTurnMessages(userMessages);
145
+ this.sessions.save(session);
146
+ }
147
+ console.log(`Request aborted for ${sessionKey}, user message saved to history`);
148
+ return null;
149
+ }
150
+ throw err;
151
+ } finally {
152
+ if (this.inflight.get(sessionKey) === controller) this.inflight.delete(sessionKey);
153
+ }
130
154
  }
131
155
  async processSystemMessage(msg) {
132
156
  console.log(`Processing system message from ${msg.senderId}`);
@@ -159,14 +183,15 @@ var AgentLoop = class {
159
183
  content: finalContent
160
184
  });
161
185
  }
162
- async runAgentLoop(messages) {
186
+ async runAgentLoop(messages, signal) {
163
187
  let finalContent = null;
164
188
  for (let i = 0; i < this.maxIterations; i++) {
165
189
  const response = await this.provider.chat({
166
190
  messages,
167
191
  tools: this.tools.getDefinitions(),
168
192
  model: this.model,
169
- maxTokens: this.maxTokens
193
+ maxTokens: this.maxTokens,
194
+ signal
170
195
  });
171
196
  if (response.hasToolCalls) {
172
197
  const toolCallDicts = response.toolCalls.map((tc) => ({
@@ -224,6 +249,16 @@ var AgentLoop = class {
224
249
  return finalContent;
225
250
  }
226
251
  };
252
+ /** Check if an error is an abort/cancellation error. */
253
+ function isAbortError(err) {
254
+ if (err instanceof DOMException && err.name === "AbortError") return true;
255
+ if (err instanceof Error) {
256
+ if (err.name === "AbortError") return true;
257
+ if (err.name === "APIUserAbortError") return true;
258
+ if (err.message.includes("abort")) return true;
259
+ }
260
+ return false;
261
+ }
227
262
 
228
263
  //#endregion
229
264
  export { AgentLoop };
@@ -1 +1 @@
1
- {"version":3,"file":"loop.mjs","names":[],"sources":["../../src/agent/loop.ts"],"sourcesContent":["import type { LLMProvider, ChatMessage } from \"../providers/base.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport type {\n InboundMessage,\n OutboundMessage,\n} from \"../bus/events.js\";\nimport { createOutboundMessage } from \"../bus/events.js\";\nimport { ContextBuilder } from \"./context.js\";\nimport { ToolRegistry } from \"./tools/registry.js\";\nimport {\n ReadFileTool,\n WriteFileTool,\n EditFileTool,\n ListDirTool,\n} from \"./tools/filesystem.js\";\nimport { ExecTool } from \"./tools/shell.js\";\nimport { WebSearchTool, WebFetchTool } from \"./tools/web.js\";\nimport { MessageTool } from \"./tools/message.js\";\nimport { SpawnTool } from \"./tools/spawn.js\";\nimport { CronTool } from \"./tools/cron.js\";\nimport { SubagentManager } from \"./subagent.js\";\nimport { SessionManager } from \"../session/manager.js\";\nimport type { ExecToolConfig } from \"../config/schema.js\";\nimport type { Tool } from \"./tools/base.js\";\nimport type { CronService } from \"../cron/service.js\";\n\n/**\n * The agent loop: core processing engine.\n *\n * 1. Receives messages from the bus\n * 2. Builds context with history, memory, skills\n * 3. Calls the LLM\n * 4. Executes tool calls\n * 5. Sends responses back\n */\nexport class AgentLoop {\n private bus: MessageBus;\n private provider: LLMProvider;\n private workspace: string;\n private model: string;\n private maxTokens: number;\n private maxIterations: number;\n\n readonly context: ContextBuilder;\n readonly sessions: SessionManager;\n readonly tools: ToolRegistry;\n readonly subagents: SubagentManager;\n\n private _running = false;\n\n constructor(params: {\n bus: MessageBus;\n provider: LLMProvider;\n workspace: string;\n model?: string;\n maxTokens?: number;\n maxIterations?: number;\n braveApiKey?: string;\n execConfig?: ExecToolConfig;\n cronService?: CronService;\n toolsEnabled?: string[];\n toolsDisabled?: string[];\n customTools?: Tool[];\n }) {\n this.bus = params.bus;\n this.provider = params.provider;\n this.workspace = params.workspace;\n this.model = params.model ?? params.provider.getDefaultModel();\n this.maxTokens = params.maxTokens ?? 8192;\n this.maxIterations = params.maxIterations ?? 20;\n\n const execConfig = params.execConfig ?? {\n timeout: 60,\n restrictToWorkspace: false,\n };\n\n this.context = new ContextBuilder(params.workspace);\n this.sessions = new SessionManager(params.workspace);\n this.tools = new ToolRegistry();\n this.subagents = new SubagentManager({\n provider: params.provider,\n workspace: params.workspace,\n bus: params.bus,\n model: this.model,\n braveApiKey: params.braveApiKey,\n execConfig,\n });\n\n this.registerDefaultTools(\n execConfig,\n params.braveApiKey,\n params.cronService,\n params.toolsEnabled,\n params.toolsDisabled,\n params.customTools,\n );\n }\n\n private registerDefaultTools(\n execConfig: ExecToolConfig,\n braveApiKey?: string,\n cronService?: CronService,\n toolsEnabled?: string[],\n toolsDisabled?: string[],\n customTools?: Tool[],\n ): void {\n const enabled = new Set(toolsEnabled ?? []);\n const disabled = new Set(toolsDisabled ?? []);\n const hasAllowlist = enabled.size > 0;\n const shouldRegister = (name: string): boolean =>\n (hasAllowlist ? enabled.has(name) : true) && !disabled.has(name);\n\n const registerIfEnabled = (tool: Tool): void => {\n if (shouldRegister(tool.name)) {\n this.tools.register(tool);\n }\n };\n\n // File tools\n registerIfEnabled(new ReadFileTool());\n registerIfEnabled(new WriteFileTool());\n registerIfEnabled(new EditFileTool());\n registerIfEnabled(new ListDirTool());\n\n // Shell tool\n registerIfEnabled(\n new ExecTool({\n workingDir: this.workspace,\n timeout: execConfig.timeout,\n restrictToWorkspace: execConfig.restrictToWorkspace,\n }),\n );\n\n // Web tools\n registerIfEnabled(new WebSearchTool({ apiKey: braveApiKey }));\n registerIfEnabled(new WebFetchTool());\n\n // Message tool\n const messageTool = new MessageTool({\n sendCallback: (msg) => this.bus.publishOutbound(msg),\n });\n registerIfEnabled(messageTool);\n\n // Spawn tool\n const spawnTool = new SpawnTool(this.subagents);\n registerIfEnabled(spawnTool);\n\n // Cron tool\n if (cronService) {\n registerIfEnabled(new CronTool(cronService));\n }\n\n if (customTools && customTools.length > 0) {\n for (const tool of customTools) {\n registerIfEnabled(tool);\n }\n }\n }\n\n /** Run the agent loop, processing messages from the bus. */\n async run(): Promise<void> {\n this._running = true;\n console.log(\"Agent loop started\");\n\n while (this._running) {\n try {\n const msg = await this.bus.consumeInboundTimeout(1000);\n\n try {\n const response = await this.processMessage(msg);\n if (response) {\n await this.bus.publishOutbound(response);\n }\n } catch (err) {\n console.error(\"Error processing message:\", err);\n await this.bus.publishOutbound(\n createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : err}`,\n }),\n );\n }\n } catch {\n // timeout, continue\n }\n }\n }\n\n /** Stop the agent loop. */\n stop(): void {\n this._running = false;\n console.log(\"Agent loop stopping\");\n }\n\n /** Process a single inbound message. */\n private async processMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n // Handle system messages (subagent announces)\n if (msg.channel === \"system\") {\n return this.processSystemMessage(msg);\n }\n\n console.log(`Processing message from ${msg.channel}:${msg.senderId}`);\n\n const sessionKey = `${msg.channel}:${msg.chatId}`;\n const session = this.sessions.getOrCreate(sessionKey);\n\n // Update tool contexts\n this.updateToolContexts(msg.channel, msg.chatId);\n\n // Build initial messages\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n media: msg.media.length > 0 ? msg.media : undefined,\n channel: msg.channel,\n chatId: msg.chatId,\n });\n\n // The messages array is: [system, ...history, currentUser]\n // We want to save from the current user message onward (skip system + old history).\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen; // 1 for system prompt\n\n // Agent loop (mutates messages by appending assistant/tool messages)\n const finalContent = await this.runAgentLoop(messages);\n\n // Save the new messages from this turn (user + all agent loop messages)\n session.addTurnMessages(messages.slice(newMsgStart));\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: finalContent,\n });\n }\n\n private async processSystemMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n console.log(`Processing system message from ${msg.senderId}`);\n\n let originChannel: string;\n let originChatId: string;\n\n if (msg.chatId.includes(\":\")) {\n const [ch, id] = msg.chatId.split(\":\", 2);\n originChannel = ch;\n originChatId = id;\n } else {\n originChannel = \"cli\";\n originChatId = msg.chatId;\n }\n\n const sessionKey = `${originChannel}:${originChatId}`;\n const session = this.sessions.getOrCreate(sessionKey);\n\n this.updateToolContexts(originChannel, originChatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n channel: originChannel,\n chatId: originChatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const finalContent = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart));\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: originChannel,\n chatId: originChatId,\n content: finalContent,\n });\n }\n\n private async runAgentLoop(messages: ChatMessage[]): Promise<string> {\n let finalContent: string | null = null;\n\n for (let i = 0; i < this.maxIterations; i++) {\n const response = await this.provider.chat({\n messages,\n tools: this.tools.getDefinitions(),\n model: this.model,\n maxTokens: this.maxTokens,\n });\n\n if (response.hasToolCalls) {\n const toolCallDicts = response.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: JSON.stringify(tc.arguments),\n },\n }));\n\n this.context.addAssistantMessage(\n messages,\n response.content,\n toolCallDicts,\n );\n\n for (const tc of response.toolCalls) {\n console.log(\n `Executing tool: ${tc.name} with arguments: ${JSON.stringify(tc.arguments)}`,\n );\n const result = await this.tools.execute(tc.name, tc.arguments);\n this.context.addToolResult(messages, tc.id, tc.name, result);\n }\n } else {\n finalContent = response.content;\n // Push the final assistant message so it gets persisted with the turn\n messages.push({ role: \"assistant\", content: finalContent ?? \"\" });\n break;\n }\n }\n\n finalContent ??= \"I've completed processing but have no response to give.\";\n\n // If we exhausted iterations without a non-tool-call response, still persist the final text\n if (messages[messages.length - 1]?.role !== \"assistant\" || messages[messages.length - 1]?.content !== finalContent) {\n messages.push({ role: \"assistant\", content: finalContent });\n }\n\n return finalContent;\n }\n\n private updateToolContexts(channel: string, chatId: string): void {\n const messageTool = this.tools.get(\"message\");\n if (messageTool instanceof MessageTool) {\n messageTool.setContext(channel, chatId);\n }\n\n const spawnTool = this.tools.get(\"spawn\");\n if (spawnTool instanceof SpawnTool) {\n spawnTool.setContext(channel, chatId);\n }\n\n const cronTool = this.tools.get(\"cron\");\n if (cronTool instanceof CronTool) {\n cronTool.setContext(channel, chatId);\n }\n }\n\n /** Process a message directly (for CLI or cron usage). */\n async processDirect(\n content: string,\n sessionKey = \"cli:direct\",\n channel = \"cli\",\n chatId = \"direct\",\n ): Promise<string> {\n // Use inline version of processMessage for direct calls\n const session = this.sessions.getOrCreate(sessionKey);\n this.updateToolContexts(channel, chatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: content,\n channel,\n chatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const finalContent = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart));\n this.sessions.save(session);\n\n return finalContent;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"loop.mjs","names":[],"sources":["../../src/agent/loop.ts"],"sourcesContent":["import type { LLMProvider, ChatMessage } from \"../providers/base.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport type {\n InboundMessage,\n OutboundMessage,\n} from \"../bus/events.js\";\nimport { createOutboundMessage } from \"../bus/events.js\";\nimport { ContextBuilder } from \"./context.js\";\nimport { ToolRegistry } from \"./tools/registry.js\";\nimport {\n ReadFileTool,\n WriteFileTool,\n EditFileTool,\n ListDirTool,\n} from \"./tools/filesystem.js\";\nimport { ExecTool } from \"./tools/shell.js\";\nimport { WebSearchTool, WebFetchTool } from \"./tools/web.js\";\nimport { MessageTool } from \"./tools/message.js\";\nimport { SpawnTool } from \"./tools/spawn.js\";\nimport { CronTool } from \"./tools/cron.js\";\nimport { SubagentManager } from \"./subagent.js\";\nimport { SessionManager } from \"../session/manager.js\";\nimport type { ExecToolConfig } from \"../config/schema.js\";\nimport type { Tool } from \"./tools/base.js\";\nimport type { CronService } from \"../cron/service.js\";\n\n/**\n * The agent loop: core processing engine.\n *\n * 1. Receives messages from the bus\n * 2. Builds context with history, memory, skills\n * 3. Calls the LLM\n * 4. Executes tool calls\n * 5. Sends responses back\n */\nexport class AgentLoop {\n private bus: MessageBus;\n private provider: LLMProvider;\n private workspace: string;\n private model: string;\n private maxTokens: number;\n private maxIterations: number;\n\n readonly context: ContextBuilder;\n readonly sessions: SessionManager;\n readonly tools: ToolRegistry;\n readonly subagents: SubagentManager;\n\n private _running = false;\n\n /** In-flight AbortControllers keyed by session key. */\n private inflight = new Map<string, AbortController>();\n\n constructor(params: {\n bus: MessageBus;\n provider: LLMProvider;\n workspace: string;\n model?: string;\n maxTokens?: number;\n maxIterations?: number;\n braveApiKey?: string;\n execConfig?: ExecToolConfig;\n restrictToWorkspace?: boolean;\n cronService?: CronService;\n toolsEnabled?: string[];\n toolsDisabled?: string[];\n customTools?: Tool[];\n }) {\n this.bus = params.bus;\n this.provider = params.provider;\n this.workspace = params.workspace;\n this.model = params.model ?? params.provider.getDefaultModel();\n this.maxTokens = params.maxTokens ?? 8192;\n this.maxIterations = params.maxIterations ?? 20;\n\n const execConfig = params.execConfig ?? { timeout: 60 };\n const restrictToWorkspace = params.restrictToWorkspace ?? false;\n\n this.context = new ContextBuilder(params.workspace);\n this.sessions = new SessionManager(params.workspace);\n this.tools = new ToolRegistry();\n this.subagents = new SubagentManager({\n provider: params.provider,\n workspace: params.workspace,\n bus: params.bus,\n model: this.model,\n braveApiKey: params.braveApiKey,\n execConfig,\n restrictToWorkspace,\n });\n\n this.registerDefaultTools(\n execConfig,\n restrictToWorkspace,\n params.braveApiKey,\n params.cronService,\n params.toolsEnabled,\n params.toolsDisabled,\n params.customTools,\n );\n }\n\n private registerDefaultTools(\n execConfig: ExecToolConfig,\n restrictToWorkspace: boolean,\n braveApiKey?: string,\n cronService?: CronService,\n toolsEnabled?: string[],\n toolsDisabled?: string[],\n customTools?: Tool[],\n ): void {\n const enabled = new Set(toolsEnabled ?? []);\n const disabled = new Set(toolsDisabled ?? []);\n const hasAllowlist = enabled.size > 0;\n const shouldRegister = (name: string): boolean =>\n (hasAllowlist ? enabled.has(name) : true) && !disabled.has(name);\n\n const registerIfEnabled = (tool: Tool): void => {\n if (shouldRegister(tool.name)) {\n this.tools.register(tool);\n }\n };\n\n // File tools — pass allowedDir when restrictToWorkspace is enabled\n const allowedDir = restrictToWorkspace ? this.workspace : undefined;\n registerIfEnabled(new ReadFileTool({ allowedDir }));\n registerIfEnabled(new WriteFileTool({ allowedDir }));\n registerIfEnabled(new EditFileTool({ allowedDir }));\n registerIfEnabled(new ListDirTool({ allowedDir }));\n\n // Shell tool\n registerIfEnabled(\n new ExecTool({\n workingDir: this.workspace,\n timeout: execConfig.timeout,\n restrictToWorkspace,\n }),\n );\n\n // Web tools\n registerIfEnabled(new WebSearchTool({ apiKey: braveApiKey }));\n registerIfEnabled(new WebFetchTool());\n\n // Message tool\n const messageTool = new MessageTool({\n sendCallback: (msg) => this.bus.publishOutbound(msg),\n });\n registerIfEnabled(messageTool);\n\n // Spawn tool\n const spawnTool = new SpawnTool(this.subagents);\n registerIfEnabled(spawnTool);\n\n // Cron tool\n if (cronService) {\n registerIfEnabled(new CronTool(cronService));\n }\n\n if (customTools && customTools.length > 0) {\n for (const tool of customTools) {\n registerIfEnabled(tool);\n }\n }\n }\n\n /** Run the agent loop, processing messages from the bus. */\n async run(): Promise<void> {\n this._running = true;\n console.log(\"Agent loop started\");\n\n while (this._running) {\n try {\n const msg = await this.bus.consumeInboundTimeout(1000);\n\n // Process concurrently so new messages can abort in-flight ones\n this.processMessage(msg)\n .then(async (response) => {\n if (response) {\n await this.bus.publishOutbound(response);\n }\n })\n .catch(async (err) => {\n if (isAbortError(err)) return; // Already handled\n console.error(\"Error processing message:\", err);\n await this.bus.publishOutbound(\n createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: `Sorry, I encountered an error: ${err instanceof Error ? err.message : err}`,\n }),\n );\n });\n } catch {\n // timeout, continue\n }\n }\n }\n\n /** Stop the agent loop. */\n stop(): void {\n this._running = false;\n console.log(\"Agent loop stopping\");\n }\n\n /** Process a single inbound message. */\n private async processMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n // Handle system messages (subagent announces)\n if (msg.channel === \"system\") {\n return this.processSystemMessage(msg);\n }\n\n console.log(`Processing message from ${msg.channel}:${msg.senderId}`);\n\n const sessionKey = `${msg.channel}:${msg.chatId}`;\n\n // Abort any in-flight request for this session\n const existing = this.inflight.get(sessionKey);\n if (existing) {\n console.log(`Aborting in-flight request for ${sessionKey}`);\n existing.abort();\n }\n\n // Create a new AbortController for this request\n const controller = new AbortController();\n this.inflight.set(sessionKey, controller);\n\n const session = this.sessions.getOrCreate(sessionKey);\n\n // Update tool contexts\n this.updateToolContexts(msg.channel, msg.chatId);\n\n // Build initial messages\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n media: msg.media.length > 0 ? msg.media : undefined,\n channel: msg.channel,\n chatId: msg.chatId,\n });\n\n // The messages array is: [system, ...history, currentUser]\n // We want to save from the current user message onward (skip system + old history).\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen; // 1 for system prompt\n\n try {\n // Agent loop (mutates messages by appending assistant/tool messages)\n const finalContent = await this.runAgentLoop(messages, controller.signal);\n\n // Save the new messages from this turn (user + all agent loop messages)\n session.addTurnMessages(messages.slice(newMsgStart));\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: msg.channel,\n chatId: msg.chatId,\n content: finalContent,\n });\n } catch (err) {\n if (isAbortError(err)) {\n // Request was aborted because a new message arrived.\n // Save the user message to history so the next request has context,\n // but don't save any assistant response.\n const userMessages = messages.slice(newMsgStart).filter((m) => m.role === \"user\");\n if (userMessages.length > 0) {\n session.addTurnMessages(userMessages);\n this.sessions.save(session);\n }\n console.log(`Request aborted for ${sessionKey}, user message saved to history`);\n return null; // No response -- the new message will handle it\n }\n throw err; // Re-throw non-abort errors\n } finally {\n // Clean up if this is still our controller\n if (this.inflight.get(sessionKey) === controller) {\n this.inflight.delete(sessionKey);\n }\n }\n }\n\n private async processSystemMessage(\n msg: InboundMessage,\n ): Promise<OutboundMessage | null> {\n console.log(`Processing system message from ${msg.senderId}`);\n\n let originChannel: string;\n let originChatId: string;\n\n if (msg.chatId.includes(\":\")) {\n const [ch, id] = msg.chatId.split(\":\", 2);\n originChannel = ch;\n originChatId = id;\n } else {\n originChannel = \"cli\";\n originChatId = msg.chatId;\n }\n\n const sessionKey = `${originChannel}:${originChatId}`;\n const session = this.sessions.getOrCreate(sessionKey);\n\n this.updateToolContexts(originChannel, originChatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: msg.content,\n channel: originChannel,\n chatId: originChatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const finalContent = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart));\n this.sessions.save(session);\n\n return createOutboundMessage({\n channel: originChannel,\n chatId: originChatId,\n content: finalContent,\n });\n }\n\n private async runAgentLoop(\n messages: ChatMessage[],\n signal?: AbortSignal,\n ): Promise<string> {\n let finalContent: string | null = null;\n\n for (let i = 0; i < this.maxIterations; i++) {\n const response = await this.provider.chat({\n messages,\n tools: this.tools.getDefinitions(),\n model: this.model,\n maxTokens: this.maxTokens,\n signal,\n });\n\n if (response.hasToolCalls) {\n const toolCallDicts = response.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: JSON.stringify(tc.arguments),\n },\n }));\n\n this.context.addAssistantMessage(\n messages,\n response.content,\n toolCallDicts,\n );\n\n for (const tc of response.toolCalls) {\n console.log(\n `Executing tool: ${tc.name} with arguments: ${JSON.stringify(tc.arguments)}`,\n );\n const result = await this.tools.execute(tc.name, tc.arguments);\n this.context.addToolResult(messages, tc.id, tc.name, result);\n }\n } else {\n finalContent = response.content;\n // Push the final assistant message so it gets persisted with the turn\n messages.push({ role: \"assistant\", content: finalContent ?? \"\" });\n break;\n }\n }\n\n finalContent ??= \"I've completed processing but have no response to give.\";\n\n // If we exhausted iterations without a non-tool-call response, still persist the final text\n if (messages[messages.length - 1]?.role !== \"assistant\" || messages[messages.length - 1]?.content !== finalContent) {\n messages.push({ role: \"assistant\", content: finalContent });\n }\n\n return finalContent;\n }\n\n private updateToolContexts(channel: string, chatId: string): void {\n const messageTool = this.tools.get(\"message\");\n if (messageTool instanceof MessageTool) {\n messageTool.setContext(channel, chatId);\n }\n\n const spawnTool = this.tools.get(\"spawn\");\n if (spawnTool instanceof SpawnTool) {\n spawnTool.setContext(channel, chatId);\n }\n\n const cronTool = this.tools.get(\"cron\");\n if (cronTool instanceof CronTool) {\n cronTool.setContext(channel, chatId);\n }\n }\n\n /** Process a message directly (for CLI or cron usage). */\n async processDirect(\n content: string,\n sessionKey = \"cli:direct\",\n channel = \"cli\",\n chatId = \"direct\",\n ): Promise<string> {\n // Use inline version of processMessage for direct calls\n const session = this.sessions.getOrCreate(sessionKey);\n this.updateToolContexts(channel, chatId);\n\n const messages = this.context.buildMessages({\n history: session.getHistory(),\n currentMessage: content,\n channel,\n chatId,\n });\n\n const savedHistoryLen = session.getHistory().length;\n const newMsgStart = 1 + savedHistoryLen;\n\n const finalContent = await this.runAgentLoop(messages);\n\n session.addTurnMessages(messages.slice(newMsgStart));\n this.sessions.save(session);\n\n return finalContent;\n }\n}\n\n/** Check if an error is an abort/cancellation error. */\nfunction isAbortError(err: unknown): boolean {\n if (err instanceof DOMException && err.name === \"AbortError\") return true;\n if (err instanceof Error) {\n if (err.name === \"AbortError\") return true;\n // OpenAI SDK wraps abort as APIUserAbortError\n if (err.name === \"APIUserAbortError\") return true;\n if (err.message.includes(\"abort\")) return true;\n }\n return false;\n}\n"],"mappings":"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"}
@@ -13,6 +13,7 @@ declare class SubagentManager {
13
13
  private model;
14
14
  private braveApiKey?;
15
15
  private execConfig;
16
+ private restrictToWorkspace;
16
17
  private runningTasks;
17
18
  constructor(params: {
18
19
  provider: LLMProvider;
@@ -21,6 +22,7 @@ declare class SubagentManager {
21
22
  model?: string;
22
23
  braveApiKey?: string;
23
24
  execConfig?: ExecToolConfig;
25
+ restrictToWorkspace?: boolean;
24
26
  });
25
27
  /** Spawn a subagent to execute a task in the background. */
26
28
  spawn(params: {
@@ -1 +1 @@
1
- {"version":3,"file":"subagent.d.mts","names":[],"sources":["../../src/agent/subagent.ts"],"mappings":";;;;;;;AAaA;cAAa,eAAA;EAAA,QACH,QAAA;EAAA,QACA,SAAA;EAAA,QACA,GAAA;EAAA,QACA,KAAA;EAAA,QACA,WAAA;EAAA,QACA,UAAA;EAAA,QACA,YAAA;cAEI,MAAA;IACV,QAAA,EAAU,WAAA;IACV,SAAA;IACA,GAAA,EAAK,UAAA;IACL,KAAA;IACA,WAAA;IACA,UAAA,GAAa,cAAA;EAAA;EALb;EAgBI,KAAA,CAAM,MAAA;IACV,IAAA;IACA,KAAA;IACA,aAAA;IACA,YAAA;EAAA,IACE,OAAA;EAAA,QAwBU,WAAA;EAAA,QA2FA,cAAA;EAAA,QA8BN,mBAAA;EAAA,IA+BJ,YAAA,CAAA;AAAA"}
1
+ {"version":3,"file":"subagent.d.mts","names":[],"sources":["../../src/agent/subagent.ts"],"mappings":";;;;;;;AAaA;cAAa,eAAA;EAAA,QACH,QAAA;EAAA,QACA,SAAA;EAAA,QACA,GAAA;EAAA,QACA,KAAA;EAAA,QACA,WAAA;EAAA,QACA,UAAA;EAAA,QACA,mBAAA;EAAA,QACA,YAAA;cAEI,MAAA;IACV,QAAA,EAAU,WAAA;IACV,SAAA;IACA,GAAA,EAAK,UAAA;IACL,KAAA;IACA,WAAA;IACA,UAAA,GAAa,cAAA;IACb,mBAAA;EAAA;EANU;EAkBN,KAAA,CAAM,MAAA;IACV,IAAA;IACA,KAAA;IACA,aAAA;IACA,YAAA;EAAA,IACE,OAAA;EAAA,QAwBU,WAAA;EAAA,QA4FA,cAAA;EAAA,QA8BN,mBAAA;EAAA,IA+BJ,YAAA,CAAA;AAAA"}
@@ -16,6 +16,7 @@ var SubagentManager = class {
16
16
  model;
17
17
  braveApiKey;
18
18
  execConfig;
19
+ restrictToWorkspace;
19
20
  runningTasks = /* @__PURE__ */ new Map();
20
21
  constructor(params) {
21
22
  this.provider = params.provider;
@@ -23,10 +24,8 @@ var SubagentManager = class {
23
24
  this.bus = params.bus;
24
25
  this.model = params.model ?? params.provider.getDefaultModel();
25
26
  this.braveApiKey = params.braveApiKey;
26
- this.execConfig = params.execConfig ?? {
27
- timeout: 60,
28
- restrictToWorkspace: false
29
- };
27
+ this.execConfig = params.execConfig ?? { timeout: 60 };
28
+ this.restrictToWorkspace = params.restrictToWorkspace ?? false;
30
29
  }
31
30
  /** Spawn a subagent to execute a task in the background. */
32
31
  async spawn(params) {
@@ -46,13 +45,14 @@ var SubagentManager = class {
46
45
  console.log(`Subagent [${taskId}] starting task: ${label}`);
47
46
  try {
48
47
  const tools = new ToolRegistry();
49
- tools.register(new ReadFileTool());
50
- tools.register(new WriteFileTool());
51
- tools.register(new ListDirTool());
48
+ const allowedDir = this.restrictToWorkspace ? this.workspace : void 0;
49
+ tools.register(new ReadFileTool({ allowedDir }));
50
+ tools.register(new WriteFileTool({ allowedDir }));
51
+ tools.register(new ListDirTool({ allowedDir }));
52
52
  tools.register(new ExecTool({
53
53
  workingDir: this.workspace,
54
54
  timeout: this.execConfig.timeout,
55
- restrictToWorkspace: this.execConfig.restrictToWorkspace
55
+ restrictToWorkspace: this.restrictToWorkspace
56
56
  }));
57
57
  tools.register(new WebSearchTool({ apiKey: this.braveApiKey }));
58
58
  tools.register(new WebFetchTool());
@@ -1 +1 @@
1
- {"version":3,"file":"subagent.mjs","names":[],"sources":["../../src/agent/subagent.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { LLMProvider, ChatMessage } from \"../providers/base.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { createInboundMessage } from \"../bus/events.js\";\nimport { ToolRegistry } from \"./tools/registry.js\";\nimport { ReadFileTool, WriteFileTool, ListDirTool } from \"./tools/filesystem.js\";\nimport { ExecTool } from \"./tools/shell.js\";\nimport { WebSearchTool, WebFetchTool } from \"./tools/web.js\";\nimport type { ExecToolConfig } from \"../config/schema.js\";\n\n/**\n * Manages background subagent execution.\n */\nexport class SubagentManager {\n private provider: LLMProvider;\n private workspace: string;\n private bus: MessageBus;\n private model: string;\n private braveApiKey?: string;\n private execConfig: ExecToolConfig;\n private runningTasks = new Map<string, AbortController>();\n\n constructor(params: {\n provider: LLMProvider;\n workspace: string;\n bus: MessageBus;\n model?: string;\n braveApiKey?: string;\n execConfig?: ExecToolConfig;\n }) {\n this.provider = params.provider;\n this.workspace = params.workspace;\n this.bus = params.bus;\n this.model = params.model ?? params.provider.getDefaultModel();\n this.braveApiKey = params.braveApiKey;\n this.execConfig = params.execConfig ?? { timeout: 60, restrictToWorkspace: false };\n }\n\n /** Spawn a subagent to execute a task in the background. */\n async spawn(params: {\n task: string;\n label?: string;\n originChannel?: string;\n originChatId?: string;\n }): Promise<string> {\n const taskId = randomUUID().slice(0, 8);\n const displayLabel =\n params.label ??\n (params.task.length > 30\n ? params.task.slice(0, 30) + \"...\"\n : params.task);\n\n const origin = {\n channel: params.originChannel ?? \"cli\",\n chatId: params.originChatId ?? \"direct\",\n };\n\n const controller = new AbortController();\n this.runningTasks.set(taskId, controller);\n\n // Run in background (don't await)\n this.runSubagent(taskId, params.task, displayLabel, origin)\n .finally(() => this.runningTasks.delete(taskId));\n\n console.log(`Spawned subagent [${taskId}]: ${displayLabel}`);\n return `Subagent [${displayLabel}] started (id: ${taskId}). I'll notify you when it completes.`;\n }\n\n private async runSubagent(\n taskId: string,\n task: string,\n label: string,\n origin: { channel: string; chatId: string },\n ): Promise<void> {\n console.log(`Subagent [${taskId}] starting task: ${label}`);\n\n try {\n // Build subagent tools (no message, no spawn)\n const tools = new ToolRegistry();\n tools.register(new ReadFileTool());\n tools.register(new WriteFileTool());\n tools.register(new ListDirTool());\n tools.register(\n new ExecTool({\n workingDir: this.workspace,\n timeout: this.execConfig.timeout,\n restrictToWorkspace: this.execConfig.restrictToWorkspace,\n }),\n );\n tools.register(new WebSearchTool({ apiKey: this.braveApiKey }));\n tools.register(new WebFetchTool());\n\n const systemPrompt = this.buildSubagentPrompt(task);\n const messages: ChatMessage[] = [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: task },\n ];\n\n const maxIterations = 15;\n let finalResult: string | null = null;\n\n for (let i = 0; i < maxIterations; i++) {\n const response = await this.provider.chat({\n messages,\n tools: tools.getDefinitions(),\n model: this.model,\n });\n\n if (response.hasToolCalls) {\n const toolCallDicts = response.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: JSON.stringify(tc.arguments),\n },\n }));\n\n messages.push({\n role: \"assistant\",\n content: response.content ?? \"\",\n tool_calls: toolCallDicts,\n });\n\n for (const tc of response.toolCalls) {\n console.log(\n `Subagent [${taskId}] executing: ${tc.name}`,\n );\n const result = await tools.execute(tc.name, tc.arguments);\n messages.push({\n role: \"tool\",\n tool_call_id: tc.id,\n name: tc.name,\n content: result,\n });\n }\n } else {\n finalResult = response.content;\n break;\n }\n }\n\n finalResult ??= \"Task completed but no final response was generated.\";\n console.log(`Subagent [${taskId}] completed successfully`);\n await this.announceResult(taskId, label, task, finalResult, origin, \"ok\");\n } catch (err) {\n const errorMsg = `Error: ${err instanceof Error ? err.message : err}`;\n console.error(`Subagent [${taskId}] failed:`, err);\n await this.announceResult(\n taskId,\n label,\n task,\n errorMsg,\n origin,\n \"error\",\n );\n }\n }\n\n private async announceResult(\n taskId: string,\n label: string,\n task: string,\n result: string,\n origin: { channel: string; chatId: string },\n status: string,\n ): Promise<void> {\n const statusText =\n status === \"ok\" ? \"completed successfully\" : \"failed\";\n\n const content = `[Subagent '${label}' ${statusText}]\n\nTask: ${task}\n\nResult:\n${result}\n\nSummarize this naturally for the user. Keep it brief (1-2 sentences). Do not mention technical details like \"subagent\" or task IDs.`;\n\n const msg = createInboundMessage({\n channel: \"system\",\n senderId: \"subagent\",\n chatId: `${origin.channel}:${origin.chatId}`,\n content,\n });\n\n await this.bus.publishInbound(msg);\n }\n\n private buildSubagentPrompt(task: string): string {\n return `# Subagent\n\nYou are a subagent spawned by the main agent to complete a specific task.\n\n## Your Task\n${task}\n\n## Rules\n1. Stay focused - complete only the assigned task, nothing else\n2. Your final response will be reported back to the main agent\n3. Do not initiate conversations or take on side tasks\n4. Be concise but informative in your findings\n\n## What You Can Do\n- Read and write files in the workspace\n- Execute shell commands\n- Search the web and fetch web pages\n- Complete the task thoroughly\n\n## What You Cannot Do\n- Send messages directly to users (no message tool available)\n- Spawn other subagents\n- Access the main agent's conversation history\n\n## Workspace\nYour workspace is at: ${this.workspace}\n\nWhen you have completed the task, provide a clear summary of your findings or actions.`;\n }\n\n get runningCount(): number {\n return this.runningTasks.size;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subagent.mjs","names":[],"sources":["../../src/agent/subagent.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { LLMProvider, ChatMessage } from \"../providers/base.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { createInboundMessage } from \"../bus/events.js\";\nimport { ToolRegistry } from \"./tools/registry.js\";\nimport { ReadFileTool, WriteFileTool, ListDirTool } from \"./tools/filesystem.js\";\nimport { ExecTool } from \"./tools/shell.js\";\nimport { WebSearchTool, WebFetchTool } from \"./tools/web.js\";\nimport type { ExecToolConfig } from \"../config/schema.js\";\n\n/**\n * Manages background subagent execution.\n */\nexport class SubagentManager {\n private provider: LLMProvider;\n private workspace: string;\n private bus: MessageBus;\n private model: string;\n private braveApiKey?: string;\n private execConfig: ExecToolConfig;\n private restrictToWorkspace: boolean;\n private runningTasks = new Map<string, AbortController>();\n\n constructor(params: {\n provider: LLMProvider;\n workspace: string;\n bus: MessageBus;\n model?: string;\n braveApiKey?: string;\n execConfig?: ExecToolConfig;\n restrictToWorkspace?: boolean;\n }) {\n this.provider = params.provider;\n this.workspace = params.workspace;\n this.bus = params.bus;\n this.model = params.model ?? params.provider.getDefaultModel();\n this.braveApiKey = params.braveApiKey;\n this.execConfig = params.execConfig ?? { timeout: 60 };\n this.restrictToWorkspace = params.restrictToWorkspace ?? false;\n }\n\n /** Spawn a subagent to execute a task in the background. */\n async spawn(params: {\n task: string;\n label?: string;\n originChannel?: string;\n originChatId?: string;\n }): Promise<string> {\n const taskId = randomUUID().slice(0, 8);\n const displayLabel =\n params.label ??\n (params.task.length > 30\n ? params.task.slice(0, 30) + \"...\"\n : params.task);\n\n const origin = {\n channel: params.originChannel ?? \"cli\",\n chatId: params.originChatId ?? \"direct\",\n };\n\n const controller = new AbortController();\n this.runningTasks.set(taskId, controller);\n\n // Run in background (don't await)\n this.runSubagent(taskId, params.task, displayLabel, origin)\n .finally(() => this.runningTasks.delete(taskId));\n\n console.log(`Spawned subagent [${taskId}]: ${displayLabel}`);\n return `Subagent [${displayLabel}] started (id: ${taskId}). I'll notify you when it completes.`;\n }\n\n private async runSubagent(\n taskId: string,\n task: string,\n label: string,\n origin: { channel: string; chatId: string },\n ): Promise<void> {\n console.log(`Subagent [${taskId}] starting task: ${label}`);\n\n try {\n // Build subagent tools (no message, no spawn)\n const tools = new ToolRegistry();\n const allowedDir = this.restrictToWorkspace ? this.workspace : undefined;\n tools.register(new ReadFileTool({ allowedDir }));\n tools.register(new WriteFileTool({ allowedDir }));\n tools.register(new ListDirTool({ allowedDir }));\n tools.register(\n new ExecTool({\n workingDir: this.workspace,\n timeout: this.execConfig.timeout,\n restrictToWorkspace: this.restrictToWorkspace,\n }),\n );\n tools.register(new WebSearchTool({ apiKey: this.braveApiKey }));\n tools.register(new WebFetchTool());\n\n const systemPrompt = this.buildSubagentPrompt(task);\n const messages: ChatMessage[] = [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: task },\n ];\n\n const maxIterations = 15;\n let finalResult: string | null = null;\n\n for (let i = 0; i < maxIterations; i++) {\n const response = await this.provider.chat({\n messages,\n tools: tools.getDefinitions(),\n model: this.model,\n });\n\n if (response.hasToolCalls) {\n const toolCallDicts = response.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: JSON.stringify(tc.arguments),\n },\n }));\n\n messages.push({\n role: \"assistant\",\n content: response.content ?? \"\",\n tool_calls: toolCallDicts,\n });\n\n for (const tc of response.toolCalls) {\n console.log(\n `Subagent [${taskId}] executing: ${tc.name}`,\n );\n const result = await tools.execute(tc.name, tc.arguments);\n messages.push({\n role: \"tool\",\n tool_call_id: tc.id,\n name: tc.name,\n content: result,\n });\n }\n } else {\n finalResult = response.content;\n break;\n }\n }\n\n finalResult ??= \"Task completed but no final response was generated.\";\n console.log(`Subagent [${taskId}] completed successfully`);\n await this.announceResult(taskId, label, task, finalResult, origin, \"ok\");\n } catch (err) {\n const errorMsg = `Error: ${err instanceof Error ? err.message : err}`;\n console.error(`Subagent [${taskId}] failed:`, err);\n await this.announceResult(\n taskId,\n label,\n task,\n errorMsg,\n origin,\n \"error\",\n );\n }\n }\n\n private async announceResult(\n taskId: string,\n label: string,\n task: string,\n result: string,\n origin: { channel: string; chatId: string },\n status: string,\n ): Promise<void> {\n const statusText =\n status === \"ok\" ? \"completed successfully\" : \"failed\";\n\n const content = `[Subagent '${label}' ${statusText}]\n\nTask: ${task}\n\nResult:\n${result}\n\nSummarize this naturally for the user. Keep it brief (1-2 sentences). Do not mention technical details like \"subagent\" or task IDs.`;\n\n const msg = createInboundMessage({\n channel: \"system\",\n senderId: \"subagent\",\n chatId: `${origin.channel}:${origin.chatId}`,\n content,\n });\n\n await this.bus.publishInbound(msg);\n }\n\n private buildSubagentPrompt(task: string): string {\n return `# Subagent\n\nYou are a subagent spawned by the main agent to complete a specific task.\n\n## Your Task\n${task}\n\n## Rules\n1. Stay focused - complete only the assigned task, nothing else\n2. Your final response will be reported back to the main agent\n3. Do not initiate conversations or take on side tasks\n4. Be concise but informative in your findings\n\n## What You Can Do\n- Read and write files in the workspace\n- Execute shell commands\n- Search the web and fetch web pages\n- Complete the task thoroughly\n\n## What You Cannot Do\n- Send messages directly to users (no message tool available)\n- Spawn other subagents\n- Access the main agent's conversation history\n\n## Workspace\nYour workspace is at: ${this.workspace}\n\nWhen you have completed the task, provide a clear summary of your findings or actions.`;\n }\n\n get runningCount(): number {\n return this.runningTasks.size;\n }\n}\n"],"mappings":"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"}
@@ -20,6 +20,10 @@ declare class ReadFileTool extends Tool {
20
20
  };
21
21
  required: string[];
22
22
  };
23
+ private allowedDir?;
24
+ constructor(params?: {
25
+ allowedDir?: string;
26
+ });
23
27
  execute(args: Record<string, unknown>): Promise<string>;
24
28
  }
25
29
  /** Write content to a file. */
@@ -40,6 +44,10 @@ declare class WriteFileTool extends Tool {
40
44
  };
41
45
  required: string[];
42
46
  };
47
+ private allowedDir?;
48
+ constructor(params?: {
49
+ allowedDir?: string;
50
+ });
43
51
  execute(args: Record<string, unknown>): Promise<string>;
44
52
  }
45
53
  /** Edit a file by replacing text. */
@@ -64,6 +72,10 @@ declare class EditFileTool extends Tool {
64
72
  };
65
73
  required: string[];
66
74
  };
75
+ private allowedDir?;
76
+ constructor(params?: {
77
+ allowedDir?: string;
78
+ });
67
79
  execute(args: Record<string, unknown>): Promise<string>;
68
80
  }
69
81
  /** List directory contents. */
@@ -80,6 +92,10 @@ declare class ListDirTool extends Tool {
80
92
  };
81
93
  required: string[];
82
94
  };
95
+ private allowedDir?;
96
+ constructor(params?: {
97
+ allowedDir?: string;
98
+ });
83
99
  execute(args: Record<string, unknown>): Promise<string>;
84
100
  }
85
101
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem.d.mts","names":[],"sources":["../../../src/agent/tools/filesystem.ts"],"mappings":";;;;cAYa,YAAA,SAAqB,IAAA;EAAA,SACvB,IAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;;;;;;;;;;;;;;;EAaH,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA;;cAoBnC,aAAA,SAAsB,IAAA;EAAA,SACxB,IAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;;;;;;;;;;;;;;EASH,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA;;cAkBnC,YAAA,SAAqB,IAAA;EAAA,SACvB,IAAA;EAAA,SACA,WAAA;EAAA,SAEA,UAAA;;;;;;;;;;;;;;;;;;EAUH,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA;;cAuBnC,WAAA,SAAoB,IAAA;EAAA,SACtB,IAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;;;;;;;;;;EAQH,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA"}
1
+ {"version":3,"file":"filesystem.d.mts","names":[],"sources":["../../../src/agent/tools/filesystem.ts"],"mappings":";;;;cAuBa,YAAA,SAAqB,IAAA;EAAA,SACvB,IAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;;;;;;;;;;;;;;;UAaD,UAAA;cAEI,MAAA;IAAW,UAAA;EAAA;EAKjB,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA;;cAuBnC,aAAA,SAAsB,IAAA;EAAA,SACxB,IAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;;;;;;;;;;;;;;UASD,UAAA;cAEI,MAAA;IAAW,UAAA;EAAA;EAKjB,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA;;cAqBnC,YAAA,SAAqB,IAAA;EAAA,SACvB,IAAA;EAAA,SACA,WAAA;EAAA,SAEA,UAAA;;;;;;;;;;;;;;;;;;UAUD,UAAA;cAEI,MAAA;IAAW,UAAA;EAAA;EAKjB,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA;;cA0BnC,WAAA,SAAoB,IAAA;EAAA,SACtB,IAAA;EAAA,SACA,WAAA;EAAA,SACA,UAAA;;;;;;;;;;UAQD,UAAA;cAEI,MAAA;IAAW,UAAA;EAAA;EAKjB,OAAA,CAAQ,IAAA,EAAM,MAAA,oBAA0B,OAAA;AAAA"}
@@ -1,8 +1,16 @@
1
1
  import { Tool } from "./base.mjs";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
 
5
5
  //#region src/agent/tools/filesystem.ts
6
+ /** Check if a resolved path is within the allowed directory. */
7
+ function checkAllowedDir(filePath, allowedDir) {
8
+ if (!allowedDir) return null;
9
+ const resolved = resolve(filePath);
10
+ const allowed = resolve(allowedDir);
11
+ if (!resolved.startsWith(allowed + "/") && resolved !== allowed) return `Error: Access denied — path must be within ${allowedDir}`;
12
+ return null;
13
+ }
6
14
  /** Read a file's contents. */
7
15
  var ReadFileTool = class extends Tool {
8
16
  name = "read_file";
@@ -22,9 +30,16 @@ var ReadFileTool = class extends Tool {
22
30
  },
23
31
  required: ["path"]
24
32
  };
33
+ allowedDir;
34
+ constructor(params) {
35
+ super();
36
+ this.allowedDir = params?.allowedDir;
37
+ }
25
38
  async execute(args) {
26
39
  const filePath = String(args.path);
27
40
  const maxChars = args.maxChars ? Number(args.maxChars) : void 0;
41
+ const denied = checkAllowedDir(filePath, this.allowedDir);
42
+ if (denied) return denied;
28
43
  try {
29
44
  if (!existsSync(filePath)) return `Error: File not found: ${filePath}`;
30
45
  let content = readFileSync(filePath, "utf-8");
@@ -53,9 +68,16 @@ var WriteFileTool = class extends Tool {
53
68
  },
54
69
  required: ["path", "content"]
55
70
  };
71
+ allowedDir;
72
+ constructor(params) {
73
+ super();
74
+ this.allowedDir = params?.allowedDir;
75
+ }
56
76
  async execute(args) {
57
77
  const filePath = String(args.path);
58
78
  const content = String(args.content);
79
+ const denied = checkAllowedDir(filePath, this.allowedDir);
80
+ if (denied) return denied;
59
81
  try {
60
82
  const dir = dirname(filePath);
61
83
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -92,10 +114,17 @@ var EditFileTool = class extends Tool {
92
114
  "new_text"
93
115
  ]
94
116
  };
117
+ allowedDir;
118
+ constructor(params) {
119
+ super();
120
+ this.allowedDir = params?.allowedDir;
121
+ }
95
122
  async execute(args) {
96
123
  const filePath = String(args.path);
97
124
  const oldText = String(args.old_text);
98
125
  const newText = String(args.new_text);
126
+ const denied = checkAllowedDir(filePath, this.allowedDir);
127
+ if (denied) return denied;
99
128
  try {
100
129
  if (!existsSync(filePath)) return `Error: File not found: ${filePath}`;
101
130
  const content = readFileSync(filePath, "utf-8");
@@ -119,8 +148,15 @@ var ListDirTool = class extends Tool {
119
148
  } },
120
149
  required: ["path"]
121
150
  };
151
+ allowedDir;
152
+ constructor(params) {
153
+ super();
154
+ this.allowedDir = params?.allowedDir;
155
+ }
122
156
  async execute(args) {
123
157
  const dirPath = String(args.path);
158
+ const denied = checkAllowedDir(dirPath, this.allowedDir);
159
+ if (denied) return denied;
124
160
  try {
125
161
  if (!existsSync(dirPath)) return `Error: Directory not found: ${dirPath}`;
126
162
  const entries = readdirSync(dirPath);
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem.mjs","names":[],"sources":["../../../src/agent/tools/filesystem.ts"],"sourcesContent":["import {\n readFileSync,\n writeFileSync,\n readdirSync,\n existsSync,\n statSync,\n mkdirSync,\n} from \"node:fs\";\nimport { join, dirname, resolve } from \"node:path\";\nimport { Tool } from \"./base.js\";\n\n/** Read a file's contents. */\nexport class ReadFileTool extends Tool {\n readonly name = \"read_file\";\n readonly description = \"Read the contents of a file.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"File path to read\" },\n maxChars: {\n type: \"integer\",\n description: \"Max characters to return\",\n minimum: 1,\n },\n },\n required: [\"path\"],\n };\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const filePath = String(args.path);\n const maxChars = args.maxChars ? Number(args.maxChars) : undefined;\n\n try {\n if (!existsSync(filePath)) {\n return `Error: File not found: ${filePath}`;\n }\n let content = readFileSync(filePath, \"utf-8\");\n if (maxChars && content.length > maxChars) {\n content = content.slice(0, maxChars) + \"\\n... (truncated)\";\n }\n return content;\n } catch (err) {\n return `Error reading file: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** Write content to a file. */\nexport class WriteFileTool extends Tool {\n readonly name = \"write_file\";\n readonly description = \"Write content to a file. Creates parent directories if needed.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"File path to write\" },\n content: { type: \"string\", description: \"Content to write\" },\n },\n required: [\"path\", \"content\"],\n };\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const filePath = String(args.path);\n const content = String(args.content);\n\n try {\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, content, \"utf-8\");\n return `Written ${content.length} chars to ${filePath}`;\n } catch (err) {\n return `Error writing file: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** Edit a file by replacing text. */\nexport class EditFileTool extends Tool {\n readonly name = \"edit_file\";\n readonly description =\n \"Edit a file by replacing the first occurrence of old_text with new_text. Provide enough context in old_text to uniquely identify the target.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"File path to edit\" },\n old_text: { type: \"string\", description: \"Text to find and replace\" },\n new_text: { type: \"string\", description: \"Replacement text\" },\n },\n required: [\"path\", \"old_text\", \"new_text\"],\n };\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const filePath = String(args.path);\n const oldText = String(args.old_text);\n const newText = String(args.new_text);\n\n try {\n if (!existsSync(filePath)) {\n return `Error: File not found: ${filePath}`;\n }\n const content = readFileSync(filePath, \"utf-8\");\n if (!content.includes(oldText)) {\n return `Error: old_text not found in ${filePath}`;\n }\n const newContent = content.replace(oldText, newText);\n writeFileSync(filePath, newContent, \"utf-8\");\n return `Edited ${filePath}: replaced ${oldText.length} chars`;\n } catch (err) {\n return `Error editing file: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** List directory contents. */\nexport class ListDirTool extends Tool {\n readonly name = \"list_dir\";\n readonly description = \"List files and directories at the given path.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"Directory path to list\" },\n },\n required: [\"path\"],\n };\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const dirPath = String(args.path);\n\n try {\n if (!existsSync(dirPath)) {\n return `Error: Directory not found: ${dirPath}`;\n }\n const entries = readdirSync(dirPath);\n const lines: string[] = [];\n for (const entry of entries) {\n try {\n const fullPath = join(dirPath, entry);\n const stat = statSync(fullPath);\n const type = stat.isDirectory() ? \"dir\" : \"file\";\n const size = stat.isDirectory() ? \"\" : ` (${stat.size}b)`;\n lines.push(`${type}\\t${entry}${size}`);\n } catch {\n lines.push(`?\\t${entry}`);\n }\n }\n return lines.length > 0 ? lines.join(\"\\n\") : \"(empty directory)\";\n } catch (err) {\n return `Error listing directory: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"filesystem.mjs","names":[],"sources":["../../../src/agent/tools/filesystem.ts"],"sourcesContent":["import {\n readFileSync,\n writeFileSync,\n readdirSync,\n existsSync,\n statSync,\n mkdirSync,\n} from \"node:fs\";\nimport { join, dirname, resolve } from \"node:path\";\nimport { Tool } from \"./base.js\";\n\n/** Check if a resolved path is within the allowed directory. */\nfunction checkAllowedDir(filePath: string, allowedDir?: string): string | null {\n if (!allowedDir) return null;\n const resolved = resolve(filePath);\n const allowed = resolve(allowedDir);\n if (!resolved.startsWith(allowed + \"/\") && resolved !== allowed) {\n return `Error: Access denied — path must be within ${allowedDir}`;\n }\n return null;\n}\n\n/** Read a file's contents. */\nexport class ReadFileTool extends Tool {\n readonly name = \"read_file\";\n readonly description = \"Read the contents of a file.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"File path to read\" },\n maxChars: {\n type: \"integer\",\n description: \"Max characters to return\",\n minimum: 1,\n },\n },\n required: [\"path\"],\n };\n\n private allowedDir?: string;\n\n constructor(params?: { allowedDir?: string }) {\n super();\n this.allowedDir = params?.allowedDir;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const filePath = String(args.path);\n const maxChars = args.maxChars ? Number(args.maxChars) : undefined;\n\n const denied = checkAllowedDir(filePath, this.allowedDir);\n if (denied) return denied;\n\n try {\n if (!existsSync(filePath)) {\n return `Error: File not found: ${filePath}`;\n }\n let content = readFileSync(filePath, \"utf-8\");\n if (maxChars && content.length > maxChars) {\n content = content.slice(0, maxChars) + \"\\n... (truncated)\";\n }\n return content;\n } catch (err) {\n return `Error reading file: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** Write content to a file. */\nexport class WriteFileTool extends Tool {\n readonly name = \"write_file\";\n readonly description = \"Write content to a file. Creates parent directories if needed.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"File path to write\" },\n content: { type: \"string\", description: \"Content to write\" },\n },\n required: [\"path\", \"content\"],\n };\n\n private allowedDir?: string;\n\n constructor(params?: { allowedDir?: string }) {\n super();\n this.allowedDir = params?.allowedDir;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const filePath = String(args.path);\n const content = String(args.content);\n\n const denied = checkAllowedDir(filePath, this.allowedDir);\n if (denied) return denied;\n\n try {\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, content, \"utf-8\");\n return `Written ${content.length} chars to ${filePath}`;\n } catch (err) {\n return `Error writing file: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** Edit a file by replacing text. */\nexport class EditFileTool extends Tool {\n readonly name = \"edit_file\";\n readonly description =\n \"Edit a file by replacing the first occurrence of old_text with new_text. Provide enough context in old_text to uniquely identify the target.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"File path to edit\" },\n old_text: { type: \"string\", description: \"Text to find and replace\" },\n new_text: { type: \"string\", description: \"Replacement text\" },\n },\n required: [\"path\", \"old_text\", \"new_text\"],\n };\n\n private allowedDir?: string;\n\n constructor(params?: { allowedDir?: string }) {\n super();\n this.allowedDir = params?.allowedDir;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const filePath = String(args.path);\n const oldText = String(args.old_text);\n const newText = String(args.new_text);\n\n const denied = checkAllowedDir(filePath, this.allowedDir);\n if (denied) return denied;\n\n try {\n if (!existsSync(filePath)) {\n return `Error: File not found: ${filePath}`;\n }\n const content = readFileSync(filePath, \"utf-8\");\n if (!content.includes(oldText)) {\n return `Error: old_text not found in ${filePath}`;\n }\n const newContent = content.replace(oldText, newText);\n writeFileSync(filePath, newContent, \"utf-8\");\n return `Edited ${filePath}: replaced ${oldText.length} chars`;\n } catch (err) {\n return `Error editing file: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n\n/** List directory contents. */\nexport class ListDirTool extends Tool {\n readonly name = \"list_dir\";\n readonly description = \"List files and directories at the given path.\";\n readonly parameters = {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"Directory path to list\" },\n },\n required: [\"path\"],\n };\n\n private allowedDir?: string;\n\n constructor(params?: { allowedDir?: string }) {\n super();\n this.allowedDir = params?.allowedDir;\n }\n\n async execute(args: Record<string, unknown>): Promise<string> {\n const dirPath = String(args.path);\n\n const denied = checkAllowedDir(dirPath, this.allowedDir);\n if (denied) return denied;\n\n try {\n if (!existsSync(dirPath)) {\n return `Error: Directory not found: ${dirPath}`;\n }\n const entries = readdirSync(dirPath);\n const lines: string[] = [];\n for (const entry of entries) {\n try {\n const fullPath = join(dirPath, entry);\n const stat = statSync(fullPath);\n const type = stat.isDirectory() ? \"dir\" : \"file\";\n const size = stat.isDirectory() ? \"\" : ` (${stat.size}b)`;\n lines.push(`${type}\\t${entry}${size}`);\n } catch {\n lines.push(`?\\t${entry}`);\n }\n }\n return lines.length > 0 ? lines.join(\"\\n\") : \"(empty directory)\";\n } catch (err) {\n return `Error listing directory: ${err instanceof Error ? err.message : err}`;\n }\n }\n}\n"],"mappings":"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"}