@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.
- package/dist/agent/loop.d.mts +3 -0
- package/dist/agent/loop.d.mts.map +1 -1
- package/dist/agent/loop.mjs +61 -26
- package/dist/agent/loop.mjs.map +1 -1
- package/dist/agent/subagent.d.mts +2 -0
- package/dist/agent/subagent.d.mts.map +1 -1
- package/dist/agent/subagent.mjs +8 -8
- package/dist/agent/subagent.mjs.map +1 -1
- package/dist/agent/tools/filesystem.d.mts +16 -0
- package/dist/agent/tools/filesystem.d.mts.map +1 -1
- package/dist/agent/tools/filesystem.mjs +37 -1
- package/dist/agent/tools/filesystem.mjs.map +1 -1
- package/dist/channels/line.d.mts +131 -0
- package/dist/channels/line.d.mts.map +1 -0
- package/dist/channels/line.mjs +265 -0
- package/dist/channels/line.mjs.map +1 -0
- package/dist/channels/manager.d.mts.map +1 -1
- package/dist/channels/manager.mjs +8 -0
- package/dist/channels/manager.mjs.map +1 -1
- package/dist/cli/index.mjs +31 -21
- package/dist/cli/index.mjs.map +1 -1
- package/dist/config/schema.d.mts +454 -46
- package/dist/config/schema.d.mts.map +1 -1
- package/dist/config/schema.mjs +96 -33
- package/dist/config/schema.mjs.map +1 -1
- package/dist/gateway/server.d.mts +2 -0
- package/dist/gateway/server.d.mts.map +1 -1
- package/dist/gateway/server.mjs +17 -1
- package/dist/gateway/server.mjs.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/dist/providers/base.d.mts +1 -0
- package/dist/providers/base.d.mts.map +1 -1
- package/dist/providers/openai-provider.d.mts +3 -0
- package/dist/providers/openai-provider.d.mts.map +1 -1
- package/dist/providers/openai-provider.mjs +5 -2
- package/dist/providers/openai-provider.mjs.map +1 -1
- package/dist/providers/registry.d.mts +65 -0
- package/dist/providers/registry.d.mts.map +1 -0
- package/dist/providers/registry.mjs +221 -0
- package/dist/providers/registry.mjs.map +1 -0
- package/package.json +1 -1
package/dist/agent/loop.d.mts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/agent/loop.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
68
|
-
registerIfEnabled(new
|
|
69
|
-
registerIfEnabled(new
|
|
70
|
-
registerIfEnabled(new
|
|
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
|
|
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
|
-
|
|
90
|
-
const response = await this.processMessage(msg);
|
|
91
|
+
this.processMessage(msg).then(async (response) => {
|
|
91
92
|
if (response) await this.bus.publishOutbound(response);
|
|
92
|
-
}
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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 };
|
package/dist/agent/loop.mjs.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/agent/subagent.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
50
|
-
tools.register(new
|
|
51
|
-
tools.register(new
|
|
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.
|
|
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
|
|
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":";;;;
|
|
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":";;;;;;AAYA,IAAa,eAAb,cAAkC,KAAK;CACrC,AAAS,OAAO;CAChB,AAAS,cAAc;CACvB,AAAS,aAAa;EACpB,MAAM;EACN,YAAY;GACV,MAAM;IAAE,MAAM;IAAU,aAAa;IAAqB;GAC1D,UAAU;IACR,MAAM;IACN,aAAa;IACb,SAAS;IACV;GACF;EACD,UAAU,CAAC,OAAO;EACnB;CAED,MAAM,QAAQ,MAAgD;EAC5D,MAAM,WAAW,OAAO,KAAK,KAAK;EAClC,MAAM,WAAW,KAAK,WAAW,OAAO,KAAK,SAAS,GAAG;
|
|
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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"}
|