@askexenow/exe-os 0.8.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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/bin/backfill-responses.js +1912 -0
- package/dist/bin/backfill-vectors.js +1642 -0
- package/dist/bin/cleanup-stale-review-tasks.js +1339 -0
- package/dist/bin/cli.js +18800 -0
- package/dist/bin/exe-agent.js +1858 -0
- package/dist/bin/exe-assign.js +1957 -0
- package/dist/bin/exe-boot.js +6460 -0
- package/dist/bin/exe-call.js +197 -0
- package/dist/bin/exe-cloud.js +850 -0
- package/dist/bin/exe-dispatch.js +1146 -0
- package/dist/bin/exe-doctor.js +1657 -0
- package/dist/bin/exe-export-behaviors.js +1494 -0
- package/dist/bin/exe-forget.js +1627 -0
- package/dist/bin/exe-gateway.js +7732 -0
- package/dist/bin/exe-healthcheck.js +207 -0
- package/dist/bin/exe-heartbeat.js +1647 -0
- package/dist/bin/exe-kill.js +1479 -0
- package/dist/bin/exe-launch-agent.js +1704 -0
- package/dist/bin/exe-link.js +192 -0
- package/dist/bin/exe-new-employee.js +852 -0
- package/dist/bin/exe-pending-messages.js +1446 -0
- package/dist/bin/exe-pending-notifications.js +1321 -0
- package/dist/bin/exe-pending-reviews.js +1468 -0
- package/dist/bin/exe-repo-drift.js +95 -0
- package/dist/bin/exe-review.js +1590 -0
- package/dist/bin/exe-search.js +2651 -0
- package/dist/bin/exe-session-cleanup.js +3173 -0
- package/dist/bin/exe-settings.js +354 -0
- package/dist/bin/exe-status.js +1532 -0
- package/dist/bin/exe-team.js +1324 -0
- package/dist/bin/git-sweep.js +2185 -0
- package/dist/bin/graph-backfill.js +1968 -0
- package/dist/bin/graph-export.js +1604 -0
- package/dist/bin/install.js +656 -0
- package/dist/bin/list-providers.js +140 -0
- package/dist/bin/scan-tasks.js +1820 -0
- package/dist/bin/setup.js +951 -0
- package/dist/bin/shard-migrate.js +1494 -0
- package/dist/bin/update.js +95 -0
- package/dist/bin/wiki-sync.js +1514 -0
- package/dist/gateway/index.js +8848 -0
- package/dist/hooks/bug-report-worker.js +2743 -0
- package/dist/hooks/commit-complete.js +2108 -0
- package/dist/hooks/error-recall.js +2861 -0
- package/dist/hooks/exe-heartbeat-hook.js +232 -0
- package/dist/hooks/ingest-worker.js +4793 -0
- package/dist/hooks/ingest.js +684 -0
- package/dist/hooks/instructions-loaded.js +1880 -0
- package/dist/hooks/notification.js +1726 -0
- package/dist/hooks/post-compact.js +1751 -0
- package/dist/hooks/pre-compact.js +1746 -0
- package/dist/hooks/pre-tool-use.js +2191 -0
- package/dist/hooks/prompt-ingest-worker.js +2126 -0
- package/dist/hooks/prompt-submit.js +4693 -0
- package/dist/hooks/response-ingest-worker.js +1936 -0
- package/dist/hooks/session-end.js +1752 -0
- package/dist/hooks/session-start.js +2795 -0
- package/dist/hooks/stop.js +1835 -0
- package/dist/hooks/subagent-stop.js +1726 -0
- package/dist/hooks/summary-worker.js +2661 -0
- package/dist/index.js +11834 -0
- package/dist/lib/cloud-sync.js +495 -0
- package/dist/lib/config.js +222 -0
- package/dist/lib/consolidation.js +476 -0
- package/dist/lib/crypto.js +51 -0
- package/dist/lib/database.js +730 -0
- package/dist/lib/device-registry.js +900 -0
- package/dist/lib/embedder.js +632 -0
- package/dist/lib/employee-templates.js +543 -0
- package/dist/lib/employees.js +177 -0
- package/dist/lib/error-detector.js +156 -0
- package/dist/lib/exe-daemon-client.js +451 -0
- package/dist/lib/exe-daemon.js +8285 -0
- package/dist/lib/file-grep.js +199 -0
- package/dist/lib/hybrid-search.js +1819 -0
- package/dist/lib/identity-templates.js +320 -0
- package/dist/lib/identity.js +223 -0
- package/dist/lib/keychain.js +145 -0
- package/dist/lib/license.js +377 -0
- package/dist/lib/messaging.js +1376 -0
- package/dist/lib/reminders.js +63 -0
- package/dist/lib/schedules.js +1396 -0
- package/dist/lib/session-registry.js +52 -0
- package/dist/lib/skill-learning.js +477 -0
- package/dist/lib/status-brief.js +235 -0
- package/dist/lib/store.js +1551 -0
- package/dist/lib/task-router.js +62 -0
- package/dist/lib/tasks.js +2456 -0
- package/dist/lib/tmux-routing.js +2836 -0
- package/dist/lib/tmux-status.js +261 -0
- package/dist/lib/tmux-transport.js +83 -0
- package/dist/lib/transport.js +128 -0
- package/dist/lib/ws-auth.js +19 -0
- package/dist/lib/ws-client.js +160 -0
- package/dist/mcp/server.js +10538 -0
- package/dist/mcp/tools/complete-reminder.js +67 -0
- package/dist/mcp/tools/create-reminder.js +52 -0
- package/dist/mcp/tools/create-task.js +1853 -0
- package/dist/mcp/tools/deactivate-behavior.js +263 -0
- package/dist/mcp/tools/list-reminders.js +62 -0
- package/dist/mcp/tools/list-tasks.js +463 -0
- package/dist/mcp/tools/send-message.js +1382 -0
- package/dist/mcp/tools/update-task.js +1692 -0
- package/dist/runtime/index.js +6809 -0
- package/dist/tui/App.js +17479 -0
- package/package.json +104 -0
- package/src/commands/exe/assign.md +17 -0
- package/src/commands/exe/build-adv.md +381 -0
- package/src/commands/exe/call.md +133 -0
- package/src/commands/exe/cloud.md +17 -0
- package/src/commands/exe/employee-heartbeat.md +44 -0
- package/src/commands/exe/forget.md +15 -0
- package/src/commands/exe/heartbeat.md +92 -0
- package/src/commands/exe/intercom.md +81 -0
- package/src/commands/exe/kill.md +34 -0
- package/src/commands/exe/launch.md +52 -0
- package/src/commands/exe/link.md +17 -0
- package/src/commands/exe/logs.md +22 -0
- package/src/commands/exe/new-employee.md +12 -0
- package/src/commands/exe/review.md +14 -0
- package/src/commands/exe/schedule.md +108 -0
- package/src/commands/exe/search.md +13 -0
- package/src/commands/exe/sessions.md +25 -0
- package/src/commands/exe/settings.md +13 -0
- package/src/commands/exe/setup.md +171 -0
- package/src/commands/exe/status.md +15 -0
- package/src/commands/exe/team.md +11 -0
- package/src/commands/exe/update.md +11 -0
- package/src/commands/exe.md +181 -0
|
@@ -0,0 +1,1858 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/exe-agent.ts
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import path7 from "path";
|
|
7
|
+
import os2 from "os";
|
|
8
|
+
|
|
9
|
+
// src/runtime/external-agent-mode.ts
|
|
10
|
+
var HARD_BLOCKED_TOOLS = /* @__PURE__ */ new Set([
|
|
11
|
+
"Bash",
|
|
12
|
+
"Write",
|
|
13
|
+
"Edit",
|
|
14
|
+
"Glob",
|
|
15
|
+
"Grep",
|
|
16
|
+
"Read",
|
|
17
|
+
"Agent",
|
|
18
|
+
"NotebookEdit"
|
|
19
|
+
]);
|
|
20
|
+
var BLOCKED_TOOL_PATTERNS = [
|
|
21
|
+
/^mcp__.*git/i,
|
|
22
|
+
/^mcp__.*exec/i,
|
|
23
|
+
/^mcp__.*shell/i,
|
|
24
|
+
/^mcp__.*process/i,
|
|
25
|
+
/^mcp__.*file/i
|
|
26
|
+
];
|
|
27
|
+
function checkExternalAgentPermission(toolName, config) {
|
|
28
|
+
if (HARD_BLOCKED_TOOLS.has(toolName)) {
|
|
29
|
+
return `DENIED: "${toolName}" is hard-blocked for customer-facing agents`;
|
|
30
|
+
}
|
|
31
|
+
for (const pattern of BLOCKED_TOOL_PATTERNS) {
|
|
32
|
+
if (pattern.test(toolName)) {
|
|
33
|
+
return `DENIED: "${toolName}" matches blocked pattern for customer-facing agents`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!config.allowedTools.includes(toolName)) {
|
|
37
|
+
return `DENIED: "${toolName}" is not in the external agent tool whitelist`;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/runtime/permission-presets.ts
|
|
43
|
+
var CMO_PRESET = {
|
|
44
|
+
defaultMode: "deny",
|
|
45
|
+
rules: [
|
|
46
|
+
{ tool: "Read", decision: "allow" },
|
|
47
|
+
{ tool: "Glob", decision: "allow" },
|
|
48
|
+
{ tool: "Grep", decision: "allow" },
|
|
49
|
+
{ tool: "Write", pattern: "exe/output/*", decision: "allow" },
|
|
50
|
+
{ tool: "mcp__exe-os__*", decision: "allow" },
|
|
51
|
+
{ tool: "mcp__exe-mem__*", decision: "allow" }
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
var CONTENT_SPECIALIST_PRESET = {
|
|
55
|
+
...CMO_PRESET
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/runtime/permissions.ts
|
|
59
|
+
function checkPermission(toolName, permissions, externalAgentConfig) {
|
|
60
|
+
if (externalAgentConfig) {
|
|
61
|
+
const denyReason = checkExternalAgentPermission(toolName, externalAgentConfig);
|
|
62
|
+
if (denyReason) {
|
|
63
|
+
return { allowed: false, mode: "deny", reason: denyReason };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const mode = permissions.overrides[toolName] ?? permissions.defaultMode;
|
|
67
|
+
if (mode === "auto-approve") {
|
|
68
|
+
return { allowed: true, mode };
|
|
69
|
+
}
|
|
70
|
+
if (mode === "deny") {
|
|
71
|
+
return { allowed: false, mode, reason: `Tool "${toolName}" is denied by permission policy` };
|
|
72
|
+
}
|
|
73
|
+
return { allowed: false, mode, reason: `Tool "${toolName}" requires approval` };
|
|
74
|
+
}
|
|
75
|
+
var EMPLOYEE_PERMISSIONS = {
|
|
76
|
+
defaultMode: "auto-approve",
|
|
77
|
+
overrides: {}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/runtime/zod-to-schema.ts
|
|
81
|
+
function zodToJsonSchema(schema) {
|
|
82
|
+
if ("toJSONSchema" in schema && typeof schema.toJSONSchema === "function") {
|
|
83
|
+
return schema.toJSONSchema();
|
|
84
|
+
}
|
|
85
|
+
const def = schema._def;
|
|
86
|
+
if (!def) {
|
|
87
|
+
return { type: "object", properties: {} };
|
|
88
|
+
}
|
|
89
|
+
return extractSchema(def);
|
|
90
|
+
}
|
|
91
|
+
function extractSchema(def) {
|
|
92
|
+
const typeName = def.typeName;
|
|
93
|
+
switch (typeName) {
|
|
94
|
+
case "ZodObject": {
|
|
95
|
+
const shape = def.shape;
|
|
96
|
+
const properties = {};
|
|
97
|
+
const required = [];
|
|
98
|
+
if (shape) {
|
|
99
|
+
const resolved = typeof shape === "function" ? shape() : shape;
|
|
100
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
101
|
+
const fieldDef = value._def ?? value;
|
|
102
|
+
const isOptional = fieldDef.typeName === "ZodOptional";
|
|
103
|
+
if (isOptional) {
|
|
104
|
+
properties[key] = extractSchema(
|
|
105
|
+
fieldDef.innerType?._def ?? {}
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
properties[key] = extractSchema(fieldDef);
|
|
109
|
+
required.push(key);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const result = { type: "object", properties };
|
|
114
|
+
if (required.length > 0) result.required = required;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
case "ZodString":
|
|
118
|
+
return { type: "string", ...def.description ? { description: String(def.description) } : {} };
|
|
119
|
+
case "ZodNumber":
|
|
120
|
+
return { type: "number", ...def.description ? { description: String(def.description) } : {} };
|
|
121
|
+
case "ZodBoolean":
|
|
122
|
+
return { type: "boolean", ...def.description ? { description: String(def.description) } : {} };
|
|
123
|
+
case "ZodArray":
|
|
124
|
+
return {
|
|
125
|
+
type: "array",
|
|
126
|
+
items: extractSchema(
|
|
127
|
+
def.type?._def ?? {}
|
|
128
|
+
)
|
|
129
|
+
};
|
|
130
|
+
case "ZodEnum":
|
|
131
|
+
return { type: "string", enum: def.values };
|
|
132
|
+
case "ZodOptional":
|
|
133
|
+
return extractSchema(
|
|
134
|
+
def.innerType?._def ?? {}
|
|
135
|
+
);
|
|
136
|
+
case "ZodDefault":
|
|
137
|
+
return extractSchema(
|
|
138
|
+
def.innerType?._def ?? {}
|
|
139
|
+
);
|
|
140
|
+
default:
|
|
141
|
+
return { type: "string" };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/runtime/tool-registry.ts
|
|
146
|
+
var ToolRegistry = class {
|
|
147
|
+
tools = /* @__PURE__ */ new Map();
|
|
148
|
+
/** Register a tool */
|
|
149
|
+
register(tool) {
|
|
150
|
+
this.tools.set(tool.name, tool);
|
|
151
|
+
}
|
|
152
|
+
/** Look up a tool by name */
|
|
153
|
+
get(name) {
|
|
154
|
+
return this.tools.get(name);
|
|
155
|
+
}
|
|
156
|
+
/** List all registered tools */
|
|
157
|
+
list() {
|
|
158
|
+
return [...this.tools.values()];
|
|
159
|
+
}
|
|
160
|
+
/** List tool names */
|
|
161
|
+
names() {
|
|
162
|
+
return [...this.tools.keys()];
|
|
163
|
+
}
|
|
164
|
+
/** Convert all tools to LLM-ready NormalizedTool format */
|
|
165
|
+
toNormalizedTools() {
|
|
166
|
+
return this.list().map((tool) => ({
|
|
167
|
+
name: tool.name,
|
|
168
|
+
description: tool.description,
|
|
169
|
+
inputSchema: zodToJsonSchema(tool.inputSchema)
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
/** Get only tools allowed by the given permission set */
|
|
173
|
+
listAllowed(permissions) {
|
|
174
|
+
return this.list().filter(
|
|
175
|
+
(tool) => checkPermission(tool.name, permissions).allowed
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
/** Convert allowed tools to NormalizedTool format */
|
|
179
|
+
toAllowedNormalizedTools(permissions) {
|
|
180
|
+
return this.listAllowed(permissions).map((tool) => ({
|
|
181
|
+
name: tool.name,
|
|
182
|
+
description: tool.description,
|
|
183
|
+
inputSchema: zodToJsonSchema(tool.inputSchema)
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function partitionTools(blocks, registry) {
|
|
188
|
+
const concurrent = [];
|
|
189
|
+
const sequential = [];
|
|
190
|
+
let hitWrite = false;
|
|
191
|
+
for (const block of blocks) {
|
|
192
|
+
if (block.type !== "tool_use") continue;
|
|
193
|
+
const tool = registry.get(block.name);
|
|
194
|
+
if (!hitWrite && tool?.isReadOnly) {
|
|
195
|
+
concurrent.push(block);
|
|
196
|
+
} else {
|
|
197
|
+
hitWrite = true;
|
|
198
|
+
sequential.push(block);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { concurrent, sequential };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/runtime/compact.ts
|
|
205
|
+
var KEEP_RECENT_MESSAGES = 10;
|
|
206
|
+
function estimateTokens(messages) {
|
|
207
|
+
let chars = 0;
|
|
208
|
+
for (const msg of messages) {
|
|
209
|
+
if (typeof msg.content === "string") {
|
|
210
|
+
chars += msg.content.length;
|
|
211
|
+
} else {
|
|
212
|
+
for (const block of msg.content) {
|
|
213
|
+
if (block.type === "text") chars += block.text.length;
|
|
214
|
+
else if (block.type === "tool_use") chars += JSON.stringify(block.input).length + block.name.length;
|
|
215
|
+
else if (block.type === "tool_result") chars += block.content.length;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return Math.ceil(chars / 4);
|
|
220
|
+
}
|
|
221
|
+
function stripMediaBlocks(messages) {
|
|
222
|
+
return messages.map((msg) => {
|
|
223
|
+
if (typeof msg.content === "string") return msg;
|
|
224
|
+
const filtered = msg.content.filter(
|
|
225
|
+
(block) => block.type !== "tool_use" || !isMediaTool(block.name)
|
|
226
|
+
);
|
|
227
|
+
if (filtered.length === 0) {
|
|
228
|
+
return { ...msg, content: "[media content removed for compaction]" };
|
|
229
|
+
}
|
|
230
|
+
return { ...msg, content: filtered };
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function isMediaTool(name) {
|
|
234
|
+
return ["upload_image", "screenshot", "render_image"].includes(name);
|
|
235
|
+
}
|
|
236
|
+
async function compactMessages(messages, provider, summaryModel = "claude-haiku-4-5-20251001") {
|
|
237
|
+
if (messages.length <= KEEP_RECENT_MESSAGES) {
|
|
238
|
+
return { messages, removedCount: 0 };
|
|
239
|
+
}
|
|
240
|
+
const toSummarize = messages.slice(0, -KEEP_RECENT_MESSAGES);
|
|
241
|
+
const toKeep = messages.slice(-KEEP_RECENT_MESSAGES);
|
|
242
|
+
const stripped = stripMediaBlocks(toSummarize);
|
|
243
|
+
const serialized = stripped.map((m) => {
|
|
244
|
+
const content = typeof m.content === "string" ? m.content : m.content.map((b) => {
|
|
245
|
+
if (b.type === "text") return b.text;
|
|
246
|
+
if (b.type === "tool_use") return `[tool: ${b.name}(${JSON.stringify(b.input).slice(0, 200)})]`;
|
|
247
|
+
if (b.type === "tool_result") return `[result: ${b.content.slice(0, 200)}]`;
|
|
248
|
+
return "";
|
|
249
|
+
}).join("\n");
|
|
250
|
+
return `${m.role}: ${content}`;
|
|
251
|
+
}).join("\n\n");
|
|
252
|
+
try {
|
|
253
|
+
const summary = await provider.createMessage({
|
|
254
|
+
model: summaryModel,
|
|
255
|
+
system: "Summarize this conversation concisely. Preserve: decisions made, files modified, current task status, blockers, key findings. Be specific about file paths and tool results.",
|
|
256
|
+
messages: [{ role: "user", content: serialized.slice(0, 5e4) }],
|
|
257
|
+
// Cap input
|
|
258
|
+
maxTokens: 2e3
|
|
259
|
+
});
|
|
260
|
+
const summaryText = summary.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
261
|
+
return {
|
|
262
|
+
messages: [
|
|
263
|
+
{ role: "user", content: `[Previous conversation summary]
|
|
264
|
+
${summaryText}` },
|
|
265
|
+
{ role: "assistant", content: "Understood. I have context from the summary. Continuing." },
|
|
266
|
+
...toKeep
|
|
267
|
+
],
|
|
268
|
+
removedCount: toSummarize.length
|
|
269
|
+
};
|
|
270
|
+
} catch {
|
|
271
|
+
return {
|
|
272
|
+
messages: [
|
|
273
|
+
{ role: "user", content: "[Earlier conversation history was compacted due to context limits]" },
|
|
274
|
+
{ role: "assistant", content: "Understood. Continuing with recent context." },
|
|
275
|
+
...toKeep
|
|
276
|
+
],
|
|
277
|
+
removedCount: toSummarize.length
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/runtime/context-manager.ts
|
|
283
|
+
var MODEL_CONTEXT_LIMITS = {
|
|
284
|
+
// Anthropic
|
|
285
|
+
"claude-opus-4": 2e5,
|
|
286
|
+
"claude-sonnet-4": 2e5,
|
|
287
|
+
"claude-haiku-4": 2e5,
|
|
288
|
+
// Extended context variants
|
|
289
|
+
"claude-opus-4-6[1m]": 1e6,
|
|
290
|
+
"claude-sonnet-4-6[1m]": 1e6,
|
|
291
|
+
// OpenAI
|
|
292
|
+
"gpt-4o": 128e3,
|
|
293
|
+
"gpt-4-turbo": 128e3,
|
|
294
|
+
"gpt-4": 8192,
|
|
295
|
+
"o3": 2e5,
|
|
296
|
+
"o4-mini": 2e5,
|
|
297
|
+
// Google
|
|
298
|
+
"gemini-2.0": 1e6,
|
|
299
|
+
"gemini-2.5-pro": 1e6,
|
|
300
|
+
"gemini-2.5-flash": 1e6,
|
|
301
|
+
// Local / small models
|
|
302
|
+
"llama": 8192,
|
|
303
|
+
"mistral": 32768,
|
|
304
|
+
"qwen": 32768,
|
|
305
|
+
"deepseek": 64e3
|
|
306
|
+
};
|
|
307
|
+
var DEFAULT_CONTEXT_LIMIT = 2e5;
|
|
308
|
+
var PRESSURE_THRESHOLDS = [50, 70, 90];
|
|
309
|
+
function getContextLimit(model) {
|
|
310
|
+
if (model in MODEL_CONTEXT_LIMITS) {
|
|
311
|
+
return MODEL_CONTEXT_LIMITS[model];
|
|
312
|
+
}
|
|
313
|
+
let bestMatch = "";
|
|
314
|
+
let bestLimit = DEFAULT_CONTEXT_LIMIT;
|
|
315
|
+
for (const [prefix, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
316
|
+
if (model.startsWith(prefix) && prefix.length > bestMatch.length) {
|
|
317
|
+
bestMatch = prefix;
|
|
318
|
+
bestLimit = limit;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return bestLimit;
|
|
322
|
+
}
|
|
323
|
+
function createContextManager(model, hooks) {
|
|
324
|
+
return new ContextManager(model, hooks);
|
|
325
|
+
}
|
|
326
|
+
var ContextManager = class {
|
|
327
|
+
state;
|
|
328
|
+
hooks;
|
|
329
|
+
constructor(model, hooks) {
|
|
330
|
+
this.hooks = hooks;
|
|
331
|
+
this.state = {
|
|
332
|
+
inputTokens: 0,
|
|
333
|
+
outputTokens: 0,
|
|
334
|
+
estimatedTokens: 0,
|
|
335
|
+
maxTokens: getContextLimit(model),
|
|
336
|
+
thresholdsEmitted: /* @__PURE__ */ new Set(),
|
|
337
|
+
model
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** Get current context state (read-only snapshot) */
|
|
341
|
+
getState() {
|
|
342
|
+
return { ...this.state, thresholdsEmitted: new Set(this.state.thresholdsEmitted) };
|
|
343
|
+
}
|
|
344
|
+
/** Get current token usage (best available: API data or estimate) */
|
|
345
|
+
get usedTokens() {
|
|
346
|
+
if (this.state.inputTokens > 0) {
|
|
347
|
+
return this.state.inputTokens;
|
|
348
|
+
}
|
|
349
|
+
return this.state.estimatedTokens;
|
|
350
|
+
}
|
|
351
|
+
/** Get percent of context used */
|
|
352
|
+
get percentUsed() {
|
|
353
|
+
return Math.round(this.usedTokens / this.state.maxTokens * 100);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Update with API usage data (from LLM response).
|
|
357
|
+
* This is the most accurate source of token counts.
|
|
358
|
+
*/
|
|
359
|
+
updateFromApiUsage(inputTokens, outputTokens) {
|
|
360
|
+
this.state.inputTokens = inputTokens;
|
|
361
|
+
this.state.outputTokens = outputTokens;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Update with estimated tokens from message content.
|
|
365
|
+
* Used as fallback when API doesn't report cache-aware usage.
|
|
366
|
+
*/
|
|
367
|
+
updateFromMessages(messages) {
|
|
368
|
+
this.state.estimatedTokens = estimateTokens(messages);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Check context pressure and emit threshold events.
|
|
372
|
+
* Should be called after each turn.
|
|
373
|
+
*/
|
|
374
|
+
async checkPressure() {
|
|
375
|
+
const percent = this.percentUsed;
|
|
376
|
+
for (const threshold of PRESSURE_THRESHOLDS) {
|
|
377
|
+
if (percent >= threshold && !this.state.thresholdsEmitted.has(threshold)) {
|
|
378
|
+
this.state.thresholdsEmitted.add(threshold);
|
|
379
|
+
const event = {
|
|
380
|
+
threshold,
|
|
381
|
+
usedTokens: this.usedTokens,
|
|
382
|
+
maxTokens: this.state.maxTokens,
|
|
383
|
+
percentUsed: percent
|
|
384
|
+
};
|
|
385
|
+
if (this.hooks.onContextPressure) {
|
|
386
|
+
try {
|
|
387
|
+
await this.hooks.onContextPressure(event);
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Should compaction run? (85% threshold, matching compact.ts) */
|
|
395
|
+
shouldCompact() {
|
|
396
|
+
return this.usedTokens > this.state.maxTokens * 0.85;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Pre-compaction: fire hooks and extract critical context to protect.
|
|
400
|
+
* Returns strings that must survive compaction.
|
|
401
|
+
*/
|
|
402
|
+
async preCompact(messages) {
|
|
403
|
+
const protectedContext = [];
|
|
404
|
+
const memoriesToExtract = [];
|
|
405
|
+
if (this.hooks.onCompact) {
|
|
406
|
+
try {
|
|
407
|
+
await this.hooks.onCompact(messages.length, 0);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const DECISION_PATTERNS = /\b(decided|chose|selected|fixed|resolved|implemented|created|deleted|changed|updated|configured)\b/i;
|
|
412
|
+
const FILE_PATTERNS = /(?:(?:src|lib|bin|tests?)\/[\w/.-]+\.(?:ts|js|json|md))/g;
|
|
413
|
+
for (const msg of messages) {
|
|
414
|
+
const text = typeof msg.content === "string" ? msg.content : msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
415
|
+
if (text.length > 50 && DECISION_PATTERNS.test(text)) {
|
|
416
|
+
const firstSentence = text.split(/[.\n]/).find((s) => DECISION_PATTERNS.test(s));
|
|
417
|
+
if (firstSentence) {
|
|
418
|
+
memoriesToExtract.push(firstSentence.trim().slice(0, 200));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const files = text.match(FILE_PATTERNS);
|
|
422
|
+
if (files && files.length > 0) {
|
|
423
|
+
protectedContext.push(`Files referenced: ${[...new Set(files)].join(", ")}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { protectedContext, memoriesToExtract };
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Post-compaction: fire hooks and verify state.
|
|
430
|
+
*/
|
|
431
|
+
async postCompact(removedCount, summaryLength) {
|
|
432
|
+
this.state.estimatedTokens = 0;
|
|
433
|
+
const currentPercent = this.percentUsed;
|
|
434
|
+
for (const threshold of PRESSURE_THRESHOLDS) {
|
|
435
|
+
if (currentPercent < threshold) {
|
|
436
|
+
this.state.thresholdsEmitted.delete(threshold);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (this.hooks.onPostCompact) {
|
|
440
|
+
try {
|
|
441
|
+
await this.hooks.onPostCompact(removedCount, `Compacted ${removedCount} messages into ${summaryLength} chars`);
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Switch model mid-session (e.g., failover).
|
|
448
|
+
* Recalculates context limit.
|
|
449
|
+
*/
|
|
450
|
+
switchModel(newModel) {
|
|
451
|
+
this.state.model = newModel;
|
|
452
|
+
this.state.maxTokens = getContextLimit(newModel);
|
|
453
|
+
this.state.thresholdsEmitted.clear();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/runtime/denial-tracking.ts
|
|
458
|
+
var DENIAL_LIMITS = {
|
|
459
|
+
maxConsecutive: 3,
|
|
460
|
+
maxTotal: 10
|
|
461
|
+
};
|
|
462
|
+
function createDenialTrackingState() {
|
|
463
|
+
return {
|
|
464
|
+
consecutiveDenials: 0,
|
|
465
|
+
totalDenials: 0
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function recordDenial(state) {
|
|
469
|
+
return {
|
|
470
|
+
consecutiveDenials: state.consecutiveDenials + 1,
|
|
471
|
+
totalDenials: state.totalDenials + 1
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function recordSuccess(state) {
|
|
475
|
+
if (state.consecutiveDenials === 0) return state;
|
|
476
|
+
return {
|
|
477
|
+
...state,
|
|
478
|
+
consecutiveDenials: 0
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function shouldFallbackToAsk(state) {
|
|
482
|
+
return state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive || state.totalDenials >= DENIAL_LIMITS.maxTotal;
|
|
483
|
+
}
|
|
484
|
+
function shouldWarnDenials(state) {
|
|
485
|
+
return state.consecutiveDenials === DENIAL_LIMITS.maxConsecutive;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/runtime/agent-loop.ts
|
|
489
|
+
async function* agentLoop(userMessage, history, config) {
|
|
490
|
+
const messages = [
|
|
491
|
+
...history,
|
|
492
|
+
{ role: "user", content: userMessage }
|
|
493
|
+
];
|
|
494
|
+
const totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
495
|
+
const abortController = config.abortController ?? new AbortController();
|
|
496
|
+
const context = {
|
|
497
|
+
cwd: config.cwd,
|
|
498
|
+
agentId: config.agentId,
|
|
499
|
+
abortSignal: abortController.signal,
|
|
500
|
+
sessionState: /* @__PURE__ */ new Map()
|
|
501
|
+
};
|
|
502
|
+
const allowedTools = config.tools.toAllowedNormalizedTools(config.permissions);
|
|
503
|
+
const contextManager = createContextManager(config.model, config.hooks);
|
|
504
|
+
let denialState = createDenialTrackingState();
|
|
505
|
+
let turns = 0;
|
|
506
|
+
while (turns < config.maxTurns) {
|
|
507
|
+
turns++;
|
|
508
|
+
if (abortController.signal.aborted) {
|
|
509
|
+
yield { type: "aborted", reason: "Agent stopped by user" };
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
if (config.maxTokenBudget && totalUsage.inputTokens + totalUsage.outputTokens >= config.maxTokenBudget) {
|
|
513
|
+
yield { type: "error", error: new Error("Token budget exceeded") };
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
let response;
|
|
517
|
+
try {
|
|
518
|
+
response = await config.provider.createMessage({
|
|
519
|
+
model: config.model,
|
|
520
|
+
system: config.systemPrompt,
|
|
521
|
+
messages,
|
|
522
|
+
tools: allowedTools.length > 0 ? allowedTools : void 0,
|
|
523
|
+
maxTokens: 4096
|
|
524
|
+
});
|
|
525
|
+
} catch (err) {
|
|
526
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
527
|
+
yield { type: "error", error };
|
|
528
|
+
if (config.hooks.onError) await config.hooks.onError(error, context);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
totalUsage.inputTokens += response.usage.inputTokens;
|
|
532
|
+
totalUsage.outputTokens += response.usage.outputTokens;
|
|
533
|
+
contextManager.updateFromApiUsage(response.usage.inputTokens, response.usage.outputTokens);
|
|
534
|
+
contextManager.updateFromMessages(messages);
|
|
535
|
+
await contextManager.checkPressure();
|
|
536
|
+
if (contextManager.shouldCompact() && messages.length > 12) {
|
|
537
|
+
const { memoriesToExtract } = await contextManager.preCompact(
|
|
538
|
+
messages.slice(0, -10)
|
|
539
|
+
);
|
|
540
|
+
if (memoriesToExtract.length > 0 && config.hooks.onNotification) {
|
|
541
|
+
await config.hooks.onNotification(
|
|
542
|
+
`Pre-compaction: extracted ${memoriesToExtract.length} key decisions to memory`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
const compacted = await compactMessages(messages, config.provider);
|
|
546
|
+
const removedCount = compacted.removedCount;
|
|
547
|
+
if (removedCount > 0) {
|
|
548
|
+
messages.length = 0;
|
|
549
|
+
messages.push(...compacted.messages);
|
|
550
|
+
await contextManager.postCompact(removedCount, messages[0]?.content?.toString().length ?? 0);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
for (const block of response.content) {
|
|
554
|
+
if (block.type === "text" && block.text) {
|
|
555
|
+
yield { type: "text", text: block.text };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
messages.push({ role: "assistant", content: response.content });
|
|
559
|
+
if (response.stopReason === "end_turn" || response.stopReason === "max_tokens") {
|
|
560
|
+
yield { type: "turn_complete", turn: turns, usage: response.usage };
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
const toolUseBlocks = response.content.filter(
|
|
564
|
+
(b) => b.type === "tool_use"
|
|
565
|
+
);
|
|
566
|
+
if (toolUseBlocks.length === 0) {
|
|
567
|
+
yield { type: "turn_complete", turn: turns, usage: response.usage };
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
const { concurrent, sequential } = partitionTools(toolUseBlocks, config.tools);
|
|
571
|
+
const toolResults = [];
|
|
572
|
+
if (concurrent.length > 0) {
|
|
573
|
+
const concurrentResults = await Promise.all(
|
|
574
|
+
concurrent.map(
|
|
575
|
+
(block) => executeToolBlock(
|
|
576
|
+
block,
|
|
577
|
+
config,
|
|
578
|
+
context,
|
|
579
|
+
denialState
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
);
|
|
583
|
+
for (let i = 0; i < concurrent.length; i++) {
|
|
584
|
+
const block = concurrent[i];
|
|
585
|
+
const execResult = concurrentResults[i];
|
|
586
|
+
denialState = execResult.denialState;
|
|
587
|
+
yield execResult.event;
|
|
588
|
+
if (execResult.permissionEvent) yield execResult.permissionEvent;
|
|
589
|
+
yield { type: "tool_result", name: block.name, id: block.id, result: execResult.result };
|
|
590
|
+
toolResults.push({
|
|
591
|
+
type: "tool_result",
|
|
592
|
+
tool_use_id: block.id,
|
|
593
|
+
content: execResult.result.content,
|
|
594
|
+
is_error: execResult.result.isError
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
for (const block of sequential) {
|
|
599
|
+
const typedBlock = block;
|
|
600
|
+
const execResult = await executeToolBlock(typedBlock, config, context, denialState);
|
|
601
|
+
denialState = execResult.denialState;
|
|
602
|
+
yield execResult.event;
|
|
603
|
+
if (execResult.permissionEvent) yield execResult.permissionEvent;
|
|
604
|
+
yield { type: "tool_result", name: typedBlock.name, id: typedBlock.id, result: execResult.result };
|
|
605
|
+
toolResults.push({
|
|
606
|
+
type: "tool_result",
|
|
607
|
+
tool_use_id: typedBlock.id,
|
|
608
|
+
content: execResult.result.content,
|
|
609
|
+
is_error: execResult.result.isError
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
messages.push({ role: "user", content: toolResults });
|
|
613
|
+
yield { type: "turn_complete", turn: turns, usage: response.usage };
|
|
614
|
+
}
|
|
615
|
+
yield { type: "done", totalUsage, turns };
|
|
616
|
+
}
|
|
617
|
+
async function executeToolBlock(block, config, context, denialState) {
|
|
618
|
+
const startEvent = { type: "tool_use_start", name: block.name, id: block.id };
|
|
619
|
+
let permissionEvent;
|
|
620
|
+
const tool = config.tools.get(block.name);
|
|
621
|
+
if (!tool) {
|
|
622
|
+
return {
|
|
623
|
+
result: { content: `Unknown tool: "${block.name}"`, isError: true },
|
|
624
|
+
event: startEvent,
|
|
625
|
+
denialState
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (!tool.isReadOnly) {
|
|
629
|
+
if (tool.checkPermissions) {
|
|
630
|
+
try {
|
|
631
|
+
const toolPerm = await tool.checkPermissions(block.input, context);
|
|
632
|
+
if (toolPerm.behavior === "deny") {
|
|
633
|
+
denialState = recordDenial(denialState);
|
|
634
|
+
if (shouldWarnDenials(denialState)) {
|
|
635
|
+
process.stderr.write(
|
|
636
|
+
`[agent] WARNING: ${denialState.consecutiveDenials} consecutive tool denials
|
|
637
|
+
`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
result: {
|
|
642
|
+
content: `Permission denied${toolPerm.bypassImmune ? " (safety check)" : ""}: ${toolPerm.reason ?? "blocked by tool policy"}`,
|
|
643
|
+
isError: true
|
|
644
|
+
},
|
|
645
|
+
event: { type: "tool_denied", name: block.name, id: block.id, reason: toolPerm.reason ?? "denied" },
|
|
646
|
+
denialState
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
denialState = recordDenial(denialState);
|
|
651
|
+
return {
|
|
652
|
+
result: { content: "Permission check failed (fail-closed)", isError: true },
|
|
653
|
+
event: { type: "tool_denied", name: block.name, id: block.id, reason: "permission check error (fail-closed)" },
|
|
654
|
+
denialState
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const effectivePermissions = shouldFallbackToAsk(denialState) ? { ...config.permissions, defaultMode: "ask" } : config.permissions;
|
|
659
|
+
const perm = checkPermission(block.name, effectivePermissions);
|
|
660
|
+
if (!perm.allowed) {
|
|
661
|
+
if (perm.mode === "ask" && config.permissionHandler) {
|
|
662
|
+
const inp = block.input;
|
|
663
|
+
const filePath = inp.file_path ?? inp.filePath;
|
|
664
|
+
const description = tool.description;
|
|
665
|
+
const permReq = { name: block.name, id: block.id, description, filePath };
|
|
666
|
+
const decision = await config.permissionHandler(permReq);
|
|
667
|
+
permissionEvent = { type: "permission_request", name: block.name, id: block.id, description, filePath };
|
|
668
|
+
if (decision === "allow") {
|
|
669
|
+
} else {
|
|
670
|
+
denialState = recordDenial(denialState);
|
|
671
|
+
return {
|
|
672
|
+
result: { content: `Permission denied by user`, isError: true },
|
|
673
|
+
event: { type: "tool_denied", name: block.name, id: block.id, reason: "denied by user" },
|
|
674
|
+
denialState,
|
|
675
|
+
permissionEvent: { type: "permission_request", name: block.name, id: block.id, description, filePath }
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
denialState = recordDenial(denialState);
|
|
680
|
+
return {
|
|
681
|
+
result: { content: `Permission denied: ${perm.reason}`, isError: true },
|
|
682
|
+
event: { type: "tool_denied", name: block.name, id: block.id, reason: perm.reason ?? "denied" },
|
|
683
|
+
denialState
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (context.abortSignal.aborted) {
|
|
689
|
+
return {
|
|
690
|
+
result: { content: "Interrupted by user", isError: true },
|
|
691
|
+
event: { type: "aborted", reason: "Agent stopped by user" },
|
|
692
|
+
denialState
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (config.hooks.beforeToolUse) {
|
|
696
|
+
const intercepted = await config.hooks.beforeToolUse(block, context);
|
|
697
|
+
if (intercepted) {
|
|
698
|
+
return { result: intercepted, event: startEvent, denialState };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
let result;
|
|
702
|
+
try {
|
|
703
|
+
result = await tool.call(block.input, context);
|
|
704
|
+
denialState = recordSuccess(denialState);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
result = {
|
|
707
|
+
content: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
|
|
708
|
+
isError: true
|
|
709
|
+
};
|
|
710
|
+
if (config.hooks.onError) {
|
|
711
|
+
await config.hooks.onError(
|
|
712
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
713
|
+
context
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (config.hooks.afterToolUse) {
|
|
718
|
+
await config.hooks.afterToolUse(block, result, context);
|
|
719
|
+
}
|
|
720
|
+
return { result, event: startEvent, denialState, permissionEvent };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/runtime/hooks.ts
|
|
724
|
+
function createDefaultHooks() {
|
|
725
|
+
return {};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/runtime/stream.ts
|
|
729
|
+
function createTerminalRenderer() {
|
|
730
|
+
return {
|
|
731
|
+
onText(text) {
|
|
732
|
+
process.stdout.write(text);
|
|
733
|
+
},
|
|
734
|
+
onToolStart(name, _id) {
|
|
735
|
+
process.stderr.write(`
|
|
736
|
+
[tool] ${name}...
|
|
737
|
+
`);
|
|
738
|
+
},
|
|
739
|
+
onToolResult(name, result, isError) {
|
|
740
|
+
if (isError) {
|
|
741
|
+
process.stderr.write(`[tool] ${name} ERROR: ${result.slice(0, 200)}
|
|
742
|
+
`);
|
|
743
|
+
} else {
|
|
744
|
+
const preview = result.length > 200 ? result.slice(0, 200) + "..." : result;
|
|
745
|
+
process.stderr.write(`[tool] ${name} \u2192 ${preview}
|
|
746
|
+
`);
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
onToolDenied(name, reason) {
|
|
750
|
+
process.stderr.write(`[tool] ${name} DENIED: ${reason}
|
|
751
|
+
`);
|
|
752
|
+
},
|
|
753
|
+
onTurnComplete(_turn, _inputTokens, _outputTokens) {
|
|
754
|
+
},
|
|
755
|
+
onAborted(reason) {
|
|
756
|
+
process.stderr.write(`
|
|
757
|
+
[aborted] ${reason}
|
|
758
|
+
`);
|
|
759
|
+
},
|
|
760
|
+
onError(error) {
|
|
761
|
+
process.stderr.write(`
|
|
762
|
+
[error] ${error.message}
|
|
763
|
+
`);
|
|
764
|
+
},
|
|
765
|
+
onDone(inputTokens, outputTokens, turns) {
|
|
766
|
+
process.stderr.write(
|
|
767
|
+
`
|
|
768
|
+
[done] ${turns} turns, ${inputTokens + outputTokens} tokens
|
|
769
|
+
`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
async function renderAgentEvents(events, renderer) {
|
|
775
|
+
for await (const event of events) {
|
|
776
|
+
switch (event.type) {
|
|
777
|
+
case "text":
|
|
778
|
+
renderer.onText(event.text);
|
|
779
|
+
break;
|
|
780
|
+
case "tool_use_start":
|
|
781
|
+
renderer.onToolStart(event.name, event.id);
|
|
782
|
+
break;
|
|
783
|
+
case "tool_result":
|
|
784
|
+
renderer.onToolResult(event.name, event.result.content, event.result.isError ?? false);
|
|
785
|
+
break;
|
|
786
|
+
case "tool_denied":
|
|
787
|
+
renderer.onToolDenied(event.name, event.reason);
|
|
788
|
+
break;
|
|
789
|
+
case "turn_complete":
|
|
790
|
+
renderer.onTurnComplete(event.turn, event.usage.inputTokens, event.usage.outputTokens);
|
|
791
|
+
break;
|
|
792
|
+
case "aborted":
|
|
793
|
+
renderer.onAborted(event.reason);
|
|
794
|
+
break;
|
|
795
|
+
case "error":
|
|
796
|
+
renderer.onError(event.error);
|
|
797
|
+
break;
|
|
798
|
+
case "done":
|
|
799
|
+
renderer.onDone(event.totalUsage.inputTokens, event.totalUsage.outputTokens, event.turns);
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/gateway/providers/anthropic.ts
|
|
806
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
807
|
+
var AnthropicProvider = class {
|
|
808
|
+
name;
|
|
809
|
+
client;
|
|
810
|
+
defaultModel;
|
|
811
|
+
constructor(name, config) {
|
|
812
|
+
this.name = name;
|
|
813
|
+
this.defaultModel = config.defaultModel ?? "claude-sonnet-4-20250514";
|
|
814
|
+
this.client = new Anthropic({
|
|
815
|
+
apiKey: config.apiKey,
|
|
816
|
+
baseURL: config.baseUrl || void 0
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
async createMessage(params) {
|
|
820
|
+
const response = await this.client.messages.create({
|
|
821
|
+
model: params.model || this.defaultModel,
|
|
822
|
+
max_tokens: params.maxTokens,
|
|
823
|
+
system: params.system,
|
|
824
|
+
messages: params.messages.map((m) => this.toAnthropicMessage(m)),
|
|
825
|
+
tools: params.tools?.map((t) => ({
|
|
826
|
+
name: t.name,
|
|
827
|
+
description: t.description,
|
|
828
|
+
input_schema: t.inputSchema
|
|
829
|
+
}))
|
|
830
|
+
});
|
|
831
|
+
return this.normalizeResponse(response);
|
|
832
|
+
}
|
|
833
|
+
async healthCheck() {
|
|
834
|
+
const start = Date.now();
|
|
835
|
+
try {
|
|
836
|
+
await this.client.messages.create({
|
|
837
|
+
model: this.defaultModel,
|
|
838
|
+
max_tokens: 1,
|
|
839
|
+
messages: [{ role: "user", content: "hi" }]
|
|
840
|
+
});
|
|
841
|
+
return { available: true, latencyMs: Date.now() - start };
|
|
842
|
+
} catch {
|
|
843
|
+
return { available: false, latencyMs: Date.now() - start };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
toAnthropicMessage(msg) {
|
|
847
|
+
if (typeof msg.content === "string") {
|
|
848
|
+
return { role: msg.role, content: msg.content };
|
|
849
|
+
}
|
|
850
|
+
const blocks = msg.content.map((block) => {
|
|
851
|
+
if (block.type === "text") {
|
|
852
|
+
return { type: "text", text: block.text };
|
|
853
|
+
}
|
|
854
|
+
if (block.type === "tool_use") {
|
|
855
|
+
return {
|
|
856
|
+
type: "tool_use",
|
|
857
|
+
id: block.id,
|
|
858
|
+
name: block.name,
|
|
859
|
+
input: block.input
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (block.type === "tool_result") {
|
|
863
|
+
return {
|
|
864
|
+
type: "tool_result",
|
|
865
|
+
tool_use_id: block.tool_use_id,
|
|
866
|
+
content: block.content,
|
|
867
|
+
is_error: block.is_error
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
return { type: "text", text: "" };
|
|
871
|
+
});
|
|
872
|
+
return { role: msg.role, content: blocks };
|
|
873
|
+
}
|
|
874
|
+
normalizeResponse(response) {
|
|
875
|
+
const content = response.content.map((block) => {
|
|
876
|
+
if (block.type === "text") {
|
|
877
|
+
return { type: "text", text: block.text };
|
|
878
|
+
}
|
|
879
|
+
if (block.type === "tool_use") {
|
|
880
|
+
return {
|
|
881
|
+
type: "tool_use",
|
|
882
|
+
id: block.id,
|
|
883
|
+
name: block.name,
|
|
884
|
+
input: block.input
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
return { type: "text", text: "" };
|
|
888
|
+
});
|
|
889
|
+
const stopReason = response.stop_reason === "tool_use" ? "tool_use" : response.stop_reason === "max_tokens" ? "max_tokens" : "end_turn";
|
|
890
|
+
return {
|
|
891
|
+
content,
|
|
892
|
+
stopReason,
|
|
893
|
+
usage: {
|
|
894
|
+
inputTokens: response.usage.input_tokens,
|
|
895
|
+
outputTokens: response.usage.output_tokens
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// src/gateway/providers/openai-compat.ts
|
|
902
|
+
import OpenAI from "openai";
|
|
903
|
+
import { randomUUID } from "crypto";
|
|
904
|
+
var OpenAICompatProvider = class {
|
|
905
|
+
name;
|
|
906
|
+
client;
|
|
907
|
+
defaultModel;
|
|
908
|
+
constructor(name, config) {
|
|
909
|
+
this.name = name;
|
|
910
|
+
this.defaultModel = config.defaultModel ?? "gpt-4o";
|
|
911
|
+
this.client = new OpenAI({
|
|
912
|
+
apiKey: config.apiKey,
|
|
913
|
+
baseURL: config.baseUrl
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
async createMessage(params) {
|
|
917
|
+
const messages = [
|
|
918
|
+
{ role: "system", content: params.system },
|
|
919
|
+
...params.messages.flatMap((m) => this.toOpenAIMessages(m))
|
|
920
|
+
];
|
|
921
|
+
const tools = params.tools?.map(
|
|
922
|
+
(t) => ({
|
|
923
|
+
type: "function",
|
|
924
|
+
function: {
|
|
925
|
+
name: t.name,
|
|
926
|
+
description: t.description,
|
|
927
|
+
parameters: t.inputSchema
|
|
928
|
+
}
|
|
929
|
+
})
|
|
930
|
+
);
|
|
931
|
+
const response = await this.client.chat.completions.create({
|
|
932
|
+
model: params.model || this.defaultModel,
|
|
933
|
+
max_tokens: params.maxTokens,
|
|
934
|
+
messages,
|
|
935
|
+
tools: tools?.length ? tools : void 0
|
|
936
|
+
});
|
|
937
|
+
return this.normalizeResponse(response);
|
|
938
|
+
}
|
|
939
|
+
async healthCheck() {
|
|
940
|
+
const start = Date.now();
|
|
941
|
+
try {
|
|
942
|
+
await this.client.chat.completions.create({
|
|
943
|
+
model: this.defaultModel,
|
|
944
|
+
max_tokens: 1,
|
|
945
|
+
messages: [{ role: "user", content: "hi" }]
|
|
946
|
+
});
|
|
947
|
+
return { available: true, latencyMs: Date.now() - start };
|
|
948
|
+
} catch {
|
|
949
|
+
return { available: false, latencyMs: Date.now() - start };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
toOpenAIMessages(msg) {
|
|
953
|
+
if (typeof msg.content === "string") {
|
|
954
|
+
return [{ role: msg.role, content: msg.content }];
|
|
955
|
+
}
|
|
956
|
+
if (msg.role === "assistant") {
|
|
957
|
+
const textParts = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
958
|
+
const toolCalls = msg.content.filter((b) => b.type === "tool_use").map((b) => ({
|
|
959
|
+
id: b.id,
|
|
960
|
+
type: "function",
|
|
961
|
+
function: {
|
|
962
|
+
name: b.name,
|
|
963
|
+
arguments: typeof b.input === "string" ? b.input : JSON.stringify(b.input)
|
|
964
|
+
}
|
|
965
|
+
}));
|
|
966
|
+
if (toolCalls.length > 0) {
|
|
967
|
+
return [{
|
|
968
|
+
role: "assistant",
|
|
969
|
+
content: textParts || null,
|
|
970
|
+
tool_calls: toolCalls
|
|
971
|
+
}];
|
|
972
|
+
}
|
|
973
|
+
return [{ role: "assistant", content: textParts }];
|
|
974
|
+
}
|
|
975
|
+
if (msg.role === "user") {
|
|
976
|
+
const toolResults = msg.content.filter(
|
|
977
|
+
(b) => b.type === "tool_result"
|
|
978
|
+
);
|
|
979
|
+
if (toolResults.length > 0) {
|
|
980
|
+
return toolResults.map((r) => ({
|
|
981
|
+
role: "tool",
|
|
982
|
+
tool_call_id: r.tool_use_id,
|
|
983
|
+
content: r.content
|
|
984
|
+
}));
|
|
985
|
+
}
|
|
986
|
+
const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
987
|
+
return [{ role: "user", content: text }];
|
|
988
|
+
}
|
|
989
|
+
return [{ role: msg.role, content: "" }];
|
|
990
|
+
}
|
|
991
|
+
normalizeResponse(response) {
|
|
992
|
+
const choice = response.choices[0];
|
|
993
|
+
if (!choice) {
|
|
994
|
+
return {
|
|
995
|
+
content: [{ type: "text", text: "" }],
|
|
996
|
+
stopReason: "end_turn",
|
|
997
|
+
usage: { inputTokens: 0, outputTokens: 0 }
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
const content = [];
|
|
1001
|
+
if (choice.message.content) {
|
|
1002
|
+
content.push({ type: "text", text: choice.message.content });
|
|
1003
|
+
}
|
|
1004
|
+
if (choice.message.tool_calls) {
|
|
1005
|
+
for (const call of choice.message.tool_calls) {
|
|
1006
|
+
const fn = call.function;
|
|
1007
|
+
if (!fn) continue;
|
|
1008
|
+
let input;
|
|
1009
|
+
try {
|
|
1010
|
+
input = JSON.parse(fn.arguments);
|
|
1011
|
+
} catch {
|
|
1012
|
+
input = fn.arguments;
|
|
1013
|
+
}
|
|
1014
|
+
content.push({
|
|
1015
|
+
type: "tool_use",
|
|
1016
|
+
id: call.id ?? randomUUID(),
|
|
1017
|
+
name: fn.name,
|
|
1018
|
+
input
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (content.length === 0) {
|
|
1023
|
+
content.push({ type: "text", text: "" });
|
|
1024
|
+
}
|
|
1025
|
+
const stopReason = choice.finish_reason === "tool_calls" ? "tool_use" : choice.finish_reason === "length" ? "max_tokens" : "end_turn";
|
|
1026
|
+
return {
|
|
1027
|
+
content,
|
|
1028
|
+
stopReason,
|
|
1029
|
+
usage: {
|
|
1030
|
+
inputTokens: response.usage?.prompt_tokens ?? 0,
|
|
1031
|
+
outputTokens: response.usage?.completion_tokens ?? 0
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// src/gateway/providers/ollama.ts
|
|
1038
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1039
|
+
var OllamaProvider = class {
|
|
1040
|
+
name;
|
|
1041
|
+
host;
|
|
1042
|
+
defaultModel;
|
|
1043
|
+
constructor(name, config = {}) {
|
|
1044
|
+
this.name = name;
|
|
1045
|
+
this.host = (config.host ?? "http://localhost:11434").replace(/\/+$/, "");
|
|
1046
|
+
this.defaultModel = config.defaultModel ?? "qwen3:14b";
|
|
1047
|
+
}
|
|
1048
|
+
async createMessage(params) {
|
|
1049
|
+
const messages = [
|
|
1050
|
+
{ role: "system", content: params.system }
|
|
1051
|
+
];
|
|
1052
|
+
for (const msg of params.messages) {
|
|
1053
|
+
if (typeof msg.content === "string") {
|
|
1054
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
1055
|
+
} else {
|
|
1056
|
+
const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
1057
|
+
messages.push({ role: msg.role, content: text });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const tools = params.tools?.map((t) => ({
|
|
1061
|
+
type: "function",
|
|
1062
|
+
function: {
|
|
1063
|
+
name: t.name,
|
|
1064
|
+
description: t.description,
|
|
1065
|
+
parameters: t.inputSchema
|
|
1066
|
+
}
|
|
1067
|
+
}));
|
|
1068
|
+
const body = {
|
|
1069
|
+
model: params.model || this.defaultModel,
|
|
1070
|
+
messages,
|
|
1071
|
+
stream: false,
|
|
1072
|
+
options: { num_predict: params.maxTokens }
|
|
1073
|
+
};
|
|
1074
|
+
if (tools?.length) body.tools = tools;
|
|
1075
|
+
const res = await fetch(`${this.host}/api/chat`, {
|
|
1076
|
+
method: "POST",
|
|
1077
|
+
headers: { "Content-Type": "application/json" },
|
|
1078
|
+
body: JSON.stringify(body)
|
|
1079
|
+
});
|
|
1080
|
+
if (!res.ok) {
|
|
1081
|
+
throw new Error(`Ollama API error: ${res.status} ${await res.text()}`);
|
|
1082
|
+
}
|
|
1083
|
+
const data = await res.json();
|
|
1084
|
+
return this.normalizeResponse(data);
|
|
1085
|
+
}
|
|
1086
|
+
async healthCheck() {
|
|
1087
|
+
const start = Date.now();
|
|
1088
|
+
try {
|
|
1089
|
+
const res = await fetch(`${this.host}/api/tags`, {
|
|
1090
|
+
signal: AbortSignal.timeout(5e3)
|
|
1091
|
+
});
|
|
1092
|
+
return { available: res.ok, latencyMs: Date.now() - start };
|
|
1093
|
+
} catch {
|
|
1094
|
+
return { available: false };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
normalizeResponse(data) {
|
|
1098
|
+
const content = [];
|
|
1099
|
+
if (data.message.content) {
|
|
1100
|
+
content.push({ type: "text", text: data.message.content });
|
|
1101
|
+
}
|
|
1102
|
+
if (data.message.tool_calls) {
|
|
1103
|
+
for (const call of data.message.tool_calls) {
|
|
1104
|
+
content.push({
|
|
1105
|
+
type: "tool_use",
|
|
1106
|
+
id: randomUUID2(),
|
|
1107
|
+
name: call.function.name,
|
|
1108
|
+
input: call.function.arguments
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (content.length === 0) {
|
|
1113
|
+
content.push({ type: "text", text: "" });
|
|
1114
|
+
}
|
|
1115
|
+
const hasToolUse = content.some((b) => b.type === "tool_use");
|
|
1116
|
+
return {
|
|
1117
|
+
content,
|
|
1118
|
+
stopReason: hasToolUse ? "tool_use" : "end_turn",
|
|
1119
|
+
usage: {
|
|
1120
|
+
inputTokens: data.prompt_eval_count ?? 0,
|
|
1121
|
+
outputTokens: data.eval_count ?? 0
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
// src/runtime/tools/bash.ts
|
|
1128
|
+
import { spawn } from "child_process";
|
|
1129
|
+
import { z } from "zod";
|
|
1130
|
+
|
|
1131
|
+
// src/runtime/dangerous-patterns.ts
|
|
1132
|
+
var DANGEROUS_PATTERNS = [
|
|
1133
|
+
// Destructive file operations
|
|
1134
|
+
{
|
|
1135
|
+
regex: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|(-[a-zA-Z]*f[a-zA-Z]*r))\s+[/~*]/,
|
|
1136
|
+
severity: "critical",
|
|
1137
|
+
reason: "Recursive force delete of root, home, or wildcard path"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
regex: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|(-[a-zA-Z]*f[a-zA-Z]*r))\s+\./,
|
|
1141
|
+
severity: "warning",
|
|
1142
|
+
reason: "Recursive force delete of relative path"
|
|
1143
|
+
},
|
|
1144
|
+
// Destructive git operations
|
|
1145
|
+
{
|
|
1146
|
+
regex: /\bgit\s+push\s+--force\b/,
|
|
1147
|
+
severity: "warning",
|
|
1148
|
+
reason: "Force push can overwrite remote history"
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
regex: /\bgit\s+reset\s+--hard\b/,
|
|
1152
|
+
severity: "warning",
|
|
1153
|
+
reason: "Hard reset discards all uncommitted changes"
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
regex: /\bgit\s+clean\s+-[a-zA-Z]*f/,
|
|
1157
|
+
severity: "warning",
|
|
1158
|
+
reason: "Git clean force removes untracked files"
|
|
1159
|
+
},
|
|
1160
|
+
// Shell injection / expansion in paths
|
|
1161
|
+
{
|
|
1162
|
+
regex: /\$\(.*\)/,
|
|
1163
|
+
severity: "warning",
|
|
1164
|
+
reason: "Command substitution in shell command"
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
regex: /`[^`]+`/,
|
|
1168
|
+
severity: "warning",
|
|
1169
|
+
reason: "Backtick command substitution"
|
|
1170
|
+
},
|
|
1171
|
+
// Remote code execution
|
|
1172
|
+
{
|
|
1173
|
+
regex: /\bcurl\b.*\|\s*(bash|sh|zsh)\b/,
|
|
1174
|
+
severity: "critical",
|
|
1175
|
+
reason: "Remote code execution via curl pipe to shell"
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
regex: /\bwget\b.*\|\s*(bash|sh|zsh)\b/,
|
|
1179
|
+
severity: "critical",
|
|
1180
|
+
reason: "Remote code execution via wget pipe to shell"
|
|
1181
|
+
},
|
|
1182
|
+
// Privilege escalation
|
|
1183
|
+
{
|
|
1184
|
+
regex: /\bsudo\b/,
|
|
1185
|
+
severity: "warning",
|
|
1186
|
+
reason: "Privilege escalation via sudo"
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
regex: /\bchmod\s+777\b/,
|
|
1190
|
+
severity: "warning",
|
|
1191
|
+
reason: "Setting world-writable permissions"
|
|
1192
|
+
},
|
|
1193
|
+
// Dangerous disk operations
|
|
1194
|
+
{
|
|
1195
|
+
regex: /\bdd\s+.*of=\/dev\//,
|
|
1196
|
+
severity: "critical",
|
|
1197
|
+
reason: "Direct disk write can destroy data"
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
regex: /\bmkfs\b/,
|
|
1201
|
+
severity: "critical",
|
|
1202
|
+
reason: "Filesystem creation destroys existing data"
|
|
1203
|
+
},
|
|
1204
|
+
// Process/system manipulation
|
|
1205
|
+
{
|
|
1206
|
+
regex: /\bkillall\b/,
|
|
1207
|
+
severity: "warning",
|
|
1208
|
+
reason: "Killing all processes by name"
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
regex: /\bkill\s+-9\b/,
|
|
1212
|
+
severity: "warning",
|
|
1213
|
+
reason: "Force kill signal"
|
|
1214
|
+
}
|
|
1215
|
+
];
|
|
1216
|
+
function checkDangerousPatterns(command) {
|
|
1217
|
+
const matched = [];
|
|
1218
|
+
for (const rule of DANGEROUS_PATTERNS) {
|
|
1219
|
+
if (rule.regex.test(command)) {
|
|
1220
|
+
matched.push({
|
|
1221
|
+
pattern: rule.regex.source,
|
|
1222
|
+
severity: rule.severity,
|
|
1223
|
+
reason: rule.reason
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
dangerous: matched.length > 0,
|
|
1229
|
+
patterns: matched
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
function hasCriticalPattern(result) {
|
|
1233
|
+
return result.patterns.some((p) => p.severity === "critical");
|
|
1234
|
+
}
|
|
1235
|
+
function formatDangerousPatterns(patterns) {
|
|
1236
|
+
return patterns.map((p) => `[${p.severity.toUpperCase()}] ${p.reason}`).join("\n");
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/runtime/tools/bash.ts
|
|
1240
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
1241
|
+
var inputSchema = z.object({
|
|
1242
|
+
command: z.string(),
|
|
1243
|
+
timeout: z.number().optional()
|
|
1244
|
+
});
|
|
1245
|
+
var BashTool = {
|
|
1246
|
+
name: "bash",
|
|
1247
|
+
description: "Execute a shell command",
|
|
1248
|
+
inputSchema,
|
|
1249
|
+
isReadOnly: false,
|
|
1250
|
+
async checkPermissions(input) {
|
|
1251
|
+
const result = checkDangerousPatterns(input.command);
|
|
1252
|
+
if (hasCriticalPattern(result)) {
|
|
1253
|
+
return {
|
|
1254
|
+
behavior: "deny",
|
|
1255
|
+
reason: formatDangerousPatterns(result.patterns),
|
|
1256
|
+
bypassImmune: true
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
return { behavior: "allow" };
|
|
1260
|
+
},
|
|
1261
|
+
async call(input, context) {
|
|
1262
|
+
const timeout = input.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
1263
|
+
return new Promise((resolve) => {
|
|
1264
|
+
const child = spawn("bash", ["-c", input.command], {
|
|
1265
|
+
cwd: context.cwd,
|
|
1266
|
+
timeout,
|
|
1267
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1268
|
+
env: { ...process.env }
|
|
1269
|
+
});
|
|
1270
|
+
const stdoutChunks = [];
|
|
1271
|
+
const stderrChunks = [];
|
|
1272
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
1273
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
1274
|
+
const onAbort = () => {
|
|
1275
|
+
child.kill("SIGTERM");
|
|
1276
|
+
setTimeout(() => {
|
|
1277
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
1278
|
+
}, 1e3);
|
|
1279
|
+
};
|
|
1280
|
+
context.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1281
|
+
child.on("close", (code) => {
|
|
1282
|
+
context.abortSignal.removeEventListener("abort", onAbort);
|
|
1283
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
1284
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
1285
|
+
if (context.abortSignal.aborted) {
|
|
1286
|
+
resolve({ content: "Command killed: agent aborted", isError: true });
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (code !== 0) {
|
|
1290
|
+
const output = stderr || stdout || `Exit code ${code}`;
|
|
1291
|
+
resolve({ content: output, isError: true });
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
resolve({ content: stdout || "(no output)" });
|
|
1295
|
+
});
|
|
1296
|
+
child.on("error", (err) => {
|
|
1297
|
+
context.abortSignal.removeEventListener("abort", onAbort);
|
|
1298
|
+
resolve({ content: `Spawn error: ${err.message}`, isError: true });
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
// src/runtime/tools/file-read.ts
|
|
1305
|
+
import fs from "fs/promises";
|
|
1306
|
+
import path2 from "path";
|
|
1307
|
+
import { z as z2 } from "zod";
|
|
1308
|
+
|
|
1309
|
+
// src/runtime/safety-checks.ts
|
|
1310
|
+
import path from "path";
|
|
1311
|
+
import os from "os";
|
|
1312
|
+
var HOME = os.homedir();
|
|
1313
|
+
var BYPASS_IMMUNE_PATTERNS = [
|
|
1314
|
+
{
|
|
1315
|
+
pattern: /\/\.git\/hooks\//,
|
|
1316
|
+
reason: "Git hooks can execute arbitrary code"
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
pattern: /\/\.git\/config$/,
|
|
1320
|
+
reason: "Git config can set hooks and command execution"
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
pattern: (p) => p.startsWith(path.join(HOME, ".claude")),
|
|
1324
|
+
reason: "Claude configuration files are protected"
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
pattern: (p) => p.startsWith(path.join(HOME, ".exe-os")),
|
|
1328
|
+
reason: "exe-os configuration files are protected"
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
pattern: /\/\.env($|\.)/,
|
|
1332
|
+
reason: "Environment files may contain secrets"
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
pattern: /\/(credentials|secrets|tokens)\.(json|yaml|yml|toml|ini|conf)$/i,
|
|
1336
|
+
reason: "Credential files are protected"
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
pattern: /(\.pem|\.key|\.p12|\.pfx|\.jks|id_rsa|id_ed25519)$/,
|
|
1340
|
+
reason: "Private key files are protected"
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
pattern: (p) => {
|
|
1344
|
+
const name = path.basename(p);
|
|
1345
|
+
return [".bashrc", ".zshrc", ".profile", ".bash_profile", ".zprofile", ".zshenv"].includes(name);
|
|
1346
|
+
},
|
|
1347
|
+
reason: "Shell configuration files can execute arbitrary code on login"
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
pattern: (p) => p.startsWith("/etc/"),
|
|
1351
|
+
reason: "System configuration directory is protected"
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
pattern: (p) => p.startsWith("/usr/") && !p.startsWith("/usr/local/"),
|
|
1355
|
+
reason: "System binary directory is protected"
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
pattern: (p) => p === "/" || p === HOME,
|
|
1359
|
+
reason: "Root and home directory direct modification is protected"
|
|
1360
|
+
}
|
|
1361
|
+
];
|
|
1362
|
+
function checkPathSafety(filePath) {
|
|
1363
|
+
const resolved = path.resolve(filePath);
|
|
1364
|
+
for (const { pattern, reason } of BYPASS_IMMUNE_PATTERNS) {
|
|
1365
|
+
const matches = typeof pattern === "function" ? pattern(resolved) : pattern.test(resolved);
|
|
1366
|
+
if (matches) {
|
|
1367
|
+
return { safe: false, reason, bypassImmune: true };
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return { safe: true, bypassImmune: true };
|
|
1371
|
+
}
|
|
1372
|
+
function checkReadPathSafety(filePath) {
|
|
1373
|
+
const resolved = path.resolve(filePath);
|
|
1374
|
+
const credPatterns = BYPASS_IMMUNE_PATTERNS.filter(
|
|
1375
|
+
(p) => typeof p.pattern !== "function" && (p.reason.includes("secrets") || p.reason.includes("Private key") || p.reason.includes("Credential"))
|
|
1376
|
+
);
|
|
1377
|
+
for (const { pattern, reason } of credPatterns) {
|
|
1378
|
+
if (typeof pattern !== "function" && pattern.test(resolved)) {
|
|
1379
|
+
return { safe: false, reason, bypassImmune: true };
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return { safe: true, bypassImmune: true };
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/runtime/tools/file-read.ts
|
|
1386
|
+
var inputSchema2 = z2.object({
|
|
1387
|
+
file_path: z2.string(),
|
|
1388
|
+
offset: z2.number().optional(),
|
|
1389
|
+
limit: z2.number().optional()
|
|
1390
|
+
});
|
|
1391
|
+
var DEFAULT_LIMIT = 2e3;
|
|
1392
|
+
var FileReadTool = {
|
|
1393
|
+
name: "file_read",
|
|
1394
|
+
description: "Read a file from disk",
|
|
1395
|
+
inputSchema: inputSchema2,
|
|
1396
|
+
isReadOnly: true,
|
|
1397
|
+
async checkPermissions(input) {
|
|
1398
|
+
const safety = checkReadPathSafety(input.file_path);
|
|
1399
|
+
if (!safety.safe) {
|
|
1400
|
+
return {
|
|
1401
|
+
behavior: "deny",
|
|
1402
|
+
reason: safety.reason ?? "Read blocked by safety check",
|
|
1403
|
+
bypassImmune: true
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
return { behavior: "allow" };
|
|
1407
|
+
},
|
|
1408
|
+
async call(input, context) {
|
|
1409
|
+
const filePath = path2.isAbsolute(input.file_path) ? input.file_path : path2.resolve(context.cwd, input.file_path);
|
|
1410
|
+
let stat;
|
|
1411
|
+
try {
|
|
1412
|
+
stat = await fs.stat(filePath);
|
|
1413
|
+
} catch {
|
|
1414
|
+
return { content: `File not found: ${filePath}`, isError: true };
|
|
1415
|
+
}
|
|
1416
|
+
if (stat.size > 0) {
|
|
1417
|
+
const sample = Buffer.alloc(512);
|
|
1418
|
+
const fh = await fs.open(filePath, "r");
|
|
1419
|
+
try {
|
|
1420
|
+
const { bytesRead } = await fh.read(sample, 0, 512, 0);
|
|
1421
|
+
const slice = sample.subarray(0, bytesRead);
|
|
1422
|
+
if (isBinary(slice)) {
|
|
1423
|
+
return {
|
|
1424
|
+
content: `Binary file (${stat.size} bytes): ${filePath}`
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
} finally {
|
|
1428
|
+
await fh.close();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
1432
|
+
const allLines = raw.split("\n");
|
|
1433
|
+
const offset = input.offset ?? 0;
|
|
1434
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
1435
|
+
const lines = allLines.slice(offset, offset + limit);
|
|
1436
|
+
const numbered = lines.map((line, i) => `${String(offset + i + 1).padStart(6)} ${line}`).join("\n");
|
|
1437
|
+
return { content: numbered };
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
function isBinary(buf) {
|
|
1441
|
+
for (let i = 0; i < buf.length; i++) {
|
|
1442
|
+
if (buf[i] === 0) return true;
|
|
1443
|
+
}
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/runtime/tools/file-edit.ts
|
|
1448
|
+
import fs2 from "fs/promises";
|
|
1449
|
+
import path3 from "path";
|
|
1450
|
+
import { z as z3 } from "zod";
|
|
1451
|
+
var inputSchema3 = z3.object({
|
|
1452
|
+
file_path: z3.string(),
|
|
1453
|
+
old_string: z3.string(),
|
|
1454
|
+
new_string: z3.string(),
|
|
1455
|
+
replace_all: z3.boolean().optional()
|
|
1456
|
+
});
|
|
1457
|
+
var FileEditTool = {
|
|
1458
|
+
name: "file_edit",
|
|
1459
|
+
description: "Edit a file by replacing a string",
|
|
1460
|
+
inputSchema: inputSchema3,
|
|
1461
|
+
isReadOnly: false,
|
|
1462
|
+
async checkPermissions(input) {
|
|
1463
|
+
const safety = checkPathSafety(input.file_path);
|
|
1464
|
+
if (!safety.safe) {
|
|
1465
|
+
return {
|
|
1466
|
+
behavior: "deny",
|
|
1467
|
+
reason: safety.reason ?? "Edit blocked by safety check",
|
|
1468
|
+
bypassImmune: true
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
return { behavior: "allow" };
|
|
1472
|
+
},
|
|
1473
|
+
async call(input, context) {
|
|
1474
|
+
const filePath = path3.isAbsolute(input.file_path) ? input.file_path : path3.resolve(context.cwd, input.file_path);
|
|
1475
|
+
let content;
|
|
1476
|
+
try {
|
|
1477
|
+
content = await fs2.readFile(filePath, "utf-8");
|
|
1478
|
+
} catch {
|
|
1479
|
+
return { content: `File not found: ${filePath}`, isError: true };
|
|
1480
|
+
}
|
|
1481
|
+
const occurrences = countOccurrences(content, input.old_string);
|
|
1482
|
+
if (occurrences === 0) {
|
|
1483
|
+
return {
|
|
1484
|
+
content: `old_string not found in ${filePath}`,
|
|
1485
|
+
isError: true
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
if (occurrences > 1 && !input.replace_all) {
|
|
1489
|
+
return {
|
|
1490
|
+
content: `old_string appears ${occurrences} times in ${filePath}. Use replace_all: true to replace all, or provide more context to make it unique.`,
|
|
1491
|
+
isError: true
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
let newContent;
|
|
1495
|
+
if (input.replace_all) {
|
|
1496
|
+
newContent = content.split(input.old_string).join(input.new_string);
|
|
1497
|
+
} else {
|
|
1498
|
+
const idx = content.indexOf(input.old_string);
|
|
1499
|
+
newContent = content.slice(0, idx) + input.new_string + content.slice(idx + input.old_string.length);
|
|
1500
|
+
}
|
|
1501
|
+
await fs2.writeFile(filePath, newContent, "utf-8");
|
|
1502
|
+
const replaced = input.replace_all ? occurrences : 1;
|
|
1503
|
+
return {
|
|
1504
|
+
content: `Replaced ${replaced} occurrence(s) in ${filePath}`,
|
|
1505
|
+
sideEffects: { filesModified: [filePath] }
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
function countOccurrences(haystack, needle) {
|
|
1510
|
+
let count = 0;
|
|
1511
|
+
let pos = 0;
|
|
1512
|
+
while (true) {
|
|
1513
|
+
const idx = haystack.indexOf(needle, pos);
|
|
1514
|
+
if (idx === -1) break;
|
|
1515
|
+
count++;
|
|
1516
|
+
pos = idx + needle.length;
|
|
1517
|
+
}
|
|
1518
|
+
return count;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/runtime/tools/file-write.ts
|
|
1522
|
+
import fs3 from "fs/promises";
|
|
1523
|
+
import path4 from "path";
|
|
1524
|
+
import { z as z4 } from "zod";
|
|
1525
|
+
var inputSchema4 = z4.object({
|
|
1526
|
+
file_path: z4.string(),
|
|
1527
|
+
content: z4.string()
|
|
1528
|
+
});
|
|
1529
|
+
var FileWriteTool = {
|
|
1530
|
+
name: "file_write",
|
|
1531
|
+
description: "Write content to a file (creates or overwrites)",
|
|
1532
|
+
inputSchema: inputSchema4,
|
|
1533
|
+
isReadOnly: false,
|
|
1534
|
+
async checkPermissions(input) {
|
|
1535
|
+
const safety = checkPathSafety(input.file_path);
|
|
1536
|
+
if (!safety.safe) {
|
|
1537
|
+
return {
|
|
1538
|
+
behavior: "deny",
|
|
1539
|
+
reason: safety.reason ?? "Write blocked by safety check",
|
|
1540
|
+
bypassImmune: true
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
return { behavior: "allow" };
|
|
1544
|
+
},
|
|
1545
|
+
async call(input, context) {
|
|
1546
|
+
const filePath = path4.isAbsolute(input.file_path) ? input.file_path : path4.resolve(context.cwd, input.file_path);
|
|
1547
|
+
const dir = path4.dirname(filePath);
|
|
1548
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
1549
|
+
await fs3.writeFile(filePath, input.content, "utf-8");
|
|
1550
|
+
return {
|
|
1551
|
+
content: `Wrote ${input.content.length} bytes to ${filePath}`,
|
|
1552
|
+
sideEffects: { filesModified: [filePath] }
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
// src/runtime/tools/glob.ts
|
|
1558
|
+
import fs4 from "fs/promises";
|
|
1559
|
+
import path5 from "path";
|
|
1560
|
+
import { z as z5 } from "zod";
|
|
1561
|
+
var inputSchema5 = z5.object({
|
|
1562
|
+
pattern: z5.string(),
|
|
1563
|
+
path: z5.string().optional()
|
|
1564
|
+
});
|
|
1565
|
+
var GlobTool = {
|
|
1566
|
+
name: "glob",
|
|
1567
|
+
description: "Find files matching a glob pattern",
|
|
1568
|
+
inputSchema: inputSchema5,
|
|
1569
|
+
isReadOnly: true,
|
|
1570
|
+
async call(input, context) {
|
|
1571
|
+
const baseDir = input.path ? path5.isAbsolute(input.path) ? input.path : path5.resolve(context.cwd, input.path) : context.cwd;
|
|
1572
|
+
try {
|
|
1573
|
+
const entries = await walkDir(baseDir);
|
|
1574
|
+
const matched = entries.filter(
|
|
1575
|
+
(e) => simpleGlobMatch(path5.relative(baseDir, e.path), input.pattern)
|
|
1576
|
+
);
|
|
1577
|
+
matched.sort((a, b) => b.mtime - a.mtime);
|
|
1578
|
+
if (matched.length === 0) {
|
|
1579
|
+
return { content: "No files matched." };
|
|
1580
|
+
}
|
|
1581
|
+
const lines = matched.map((e) => e.path);
|
|
1582
|
+
return { content: lines.join("\n") };
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
return {
|
|
1585
|
+
content: `Glob error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1586
|
+
isError: true
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
async function walkDir(dir, maxDepth = 10) {
|
|
1592
|
+
const results = [];
|
|
1593
|
+
async function walk(current, depth) {
|
|
1594
|
+
if (depth > maxDepth) return;
|
|
1595
|
+
let entries;
|
|
1596
|
+
try {
|
|
1597
|
+
entries = await fs4.readdir(current, { withFileTypes: true });
|
|
1598
|
+
} catch {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
for (const entry of entries) {
|
|
1602
|
+
if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git")) {
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const fullPath = path5.join(current, entry.name);
|
|
1606
|
+
if (entry.isDirectory()) {
|
|
1607
|
+
await walk(fullPath, depth + 1);
|
|
1608
|
+
} else {
|
|
1609
|
+
try {
|
|
1610
|
+
const stat = await fs4.stat(fullPath);
|
|
1611
|
+
results.push({ path: fullPath, mtime: stat.mtimeMs });
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
await walk(dir, 0);
|
|
1618
|
+
return results;
|
|
1619
|
+
}
|
|
1620
|
+
function simpleGlobMatch(filePath, pattern) {
|
|
1621
|
+
let regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
1622
|
+
regex = `^${regex}$`;
|
|
1623
|
+
return new RegExp(regex).test(filePath);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/runtime/tools/grep.ts
|
|
1627
|
+
import { spawn as spawn2 } from "child_process";
|
|
1628
|
+
import fs5 from "fs/promises";
|
|
1629
|
+
import path6 from "path";
|
|
1630
|
+
import { z as z6 } from "zod";
|
|
1631
|
+
var inputSchema6 = z6.object({
|
|
1632
|
+
pattern: z6.string(),
|
|
1633
|
+
path: z6.string().optional(),
|
|
1634
|
+
glob: z6.string().optional(),
|
|
1635
|
+
include: z6.string().optional()
|
|
1636
|
+
});
|
|
1637
|
+
var GrepTool = {
|
|
1638
|
+
name: "grep",
|
|
1639
|
+
description: "Search file contents using regex",
|
|
1640
|
+
inputSchema: inputSchema6,
|
|
1641
|
+
isReadOnly: true,
|
|
1642
|
+
async call(input, context) {
|
|
1643
|
+
const searchPath = input.path ? path6.isAbsolute(input.path) ? input.path : path6.resolve(context.cwd, input.path) : context.cwd;
|
|
1644
|
+
try {
|
|
1645
|
+
const result = await runRipgrep(input, searchPath, context);
|
|
1646
|
+
return result;
|
|
1647
|
+
} catch {
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
return await nodeGrep(input, searchPath);
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
return {
|
|
1653
|
+
content: `Grep error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1654
|
+
isError: true
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
function runRipgrep(input, searchPath, context) {
|
|
1660
|
+
return new Promise((resolve, reject) => {
|
|
1661
|
+
const args = ["--files-with-matches", "--no-heading"];
|
|
1662
|
+
if (input.glob) {
|
|
1663
|
+
args.push("--glob", input.glob);
|
|
1664
|
+
}
|
|
1665
|
+
if (input.include) {
|
|
1666
|
+
args.push("--glob", input.include);
|
|
1667
|
+
}
|
|
1668
|
+
args.push(input.pattern, searchPath);
|
|
1669
|
+
const child = spawn2("rg", args, {
|
|
1670
|
+
cwd: searchPath,
|
|
1671
|
+
timeout: 3e4,
|
|
1672
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1673
|
+
});
|
|
1674
|
+
const chunks = [];
|
|
1675
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
1676
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
1677
|
+
context.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1678
|
+
child.on("close", (code) => {
|
|
1679
|
+
context.abortSignal.removeEventListener("abort", onAbort);
|
|
1680
|
+
const output = Buffer.concat(chunks).toString("utf-8").trim();
|
|
1681
|
+
if (code === 1 && !output) {
|
|
1682
|
+
resolve({ content: "No matches found." });
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (code === 2) {
|
|
1686
|
+
reject(new Error("ripgrep error"));
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
resolve({ content: output || "No matches found." });
|
|
1690
|
+
});
|
|
1691
|
+
child.on("error", () => reject(new Error("rg not found")));
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
async function nodeGrep(input, searchPath) {
|
|
1695
|
+
const regex = new RegExp(input.pattern);
|
|
1696
|
+
const matches = [];
|
|
1697
|
+
async function walk(dir) {
|
|
1698
|
+
let entries;
|
|
1699
|
+
try {
|
|
1700
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1701
|
+
} catch {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
for (const entry of entries) {
|
|
1705
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
1706
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1707
|
+
if (entry.isDirectory()) {
|
|
1708
|
+
await walk(fullPath);
|
|
1709
|
+
} else {
|
|
1710
|
+
if (input.glob || input.include) {
|
|
1711
|
+
const filter = input.glob ?? input.include;
|
|
1712
|
+
if (!simpleExtMatch(entry.name, filter)) continue;
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
const content = await fs5.readFile(fullPath, "utf-8");
|
|
1716
|
+
if (regex.test(content)) {
|
|
1717
|
+
matches.push(fullPath);
|
|
1718
|
+
}
|
|
1719
|
+
} catch {
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
await walk(searchPath);
|
|
1725
|
+
if (matches.length === 0) {
|
|
1726
|
+
return { content: "No matches found." };
|
|
1727
|
+
}
|
|
1728
|
+
return { content: matches.join("\n") };
|
|
1729
|
+
}
|
|
1730
|
+
function simpleExtMatch(filename, pattern) {
|
|
1731
|
+
if (pattern.startsWith("*.")) {
|
|
1732
|
+
return filename.endsWith(pattern.slice(1));
|
|
1733
|
+
}
|
|
1734
|
+
return filename.includes(pattern.replace(/\*/g, ""));
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/bin/exe-agent.ts
|
|
1738
|
+
function parseArgs(argv) {
|
|
1739
|
+
const args = { employee: "" };
|
|
1740
|
+
for (let i = 2; i < argv.length; i++) {
|
|
1741
|
+
const arg = argv[i];
|
|
1742
|
+
if (arg === "--employee" && argv[i + 1]) args.employee = argv[++i];
|
|
1743
|
+
else if (arg === "--model" && argv[i + 1]) args.model = argv[++i];
|
|
1744
|
+
else if (arg === "--provider" && argv[i + 1]) args.provider = argv[++i];
|
|
1745
|
+
else if (arg === "--api-key" && argv[i + 1]) args.apiKey = argv[++i];
|
|
1746
|
+
else if (arg === "--base-url" && argv[i + 1]) args.baseUrl = argv[++i];
|
|
1747
|
+
else if (!arg.startsWith("-") && !args.employee) args.employee = arg;
|
|
1748
|
+
}
|
|
1749
|
+
return args;
|
|
1750
|
+
}
|
|
1751
|
+
function loadEmployee(name) {
|
|
1752
|
+
try {
|
|
1753
|
+
const rosterPath = path7.join(os2.homedir(), ".exe-os", "exe-employees.json");
|
|
1754
|
+
const roster = JSON.parse(readFileSync(rosterPath, "utf8"));
|
|
1755
|
+
return roster.find((e) => e.name === name) ?? null;
|
|
1756
|
+
} catch {
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function createProvider(args) {
|
|
1761
|
+
const providerType = args.provider ?? "anthropic";
|
|
1762
|
+
const apiKey = args.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
|
|
1763
|
+
switch (providerType) {
|
|
1764
|
+
case "anthropic":
|
|
1765
|
+
return new AnthropicProvider("anthropic", {
|
|
1766
|
+
apiKey,
|
|
1767
|
+
baseUrl: args.baseUrl,
|
|
1768
|
+
defaultModel: args.model
|
|
1769
|
+
});
|
|
1770
|
+
case "openai-compat":
|
|
1771
|
+
case "openrouter":
|
|
1772
|
+
return new OpenAICompatProvider(providerType, {
|
|
1773
|
+
apiKey: args.apiKey ?? process.env.OPENROUTER_API_KEY ?? "",
|
|
1774
|
+
baseUrl: args.baseUrl ?? "https://openrouter.ai/api/v1",
|
|
1775
|
+
defaultModel: args.model
|
|
1776
|
+
});
|
|
1777
|
+
case "ollama":
|
|
1778
|
+
return new OllamaProvider("ollama", {
|
|
1779
|
+
host: args.baseUrl ?? "http://localhost:11434",
|
|
1780
|
+
defaultModel: args.model
|
|
1781
|
+
});
|
|
1782
|
+
default:
|
|
1783
|
+
throw new Error(`Unknown provider: ${providerType}`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
function registerBuiltinTools(registry) {
|
|
1787
|
+
registry.register(BashTool);
|
|
1788
|
+
registry.register(FileReadTool);
|
|
1789
|
+
registry.register(FileEditTool);
|
|
1790
|
+
registry.register(FileWriteTool);
|
|
1791
|
+
registry.register(GlobTool);
|
|
1792
|
+
registry.register(GrepTool);
|
|
1793
|
+
}
|
|
1794
|
+
async function main() {
|
|
1795
|
+
const args = parseArgs(process.argv);
|
|
1796
|
+
if (!args.employee) {
|
|
1797
|
+
console.error("Usage: exe-agent --employee <name> [--model <model>] [--provider <provider>]");
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|
|
1800
|
+
const employee = loadEmployee(args.employee);
|
|
1801
|
+
if (!employee) {
|
|
1802
|
+
console.error(`Employee "${args.employee}" not found in roster.`);
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
const provider = createProvider(args);
|
|
1806
|
+
const model = args.model ?? "claude-sonnet-4-20250514";
|
|
1807
|
+
const registry = new ToolRegistry();
|
|
1808
|
+
registerBuiltinTools(registry);
|
|
1809
|
+
const abortController = new AbortController();
|
|
1810
|
+
const shutdown = () => {
|
|
1811
|
+
process.stderr.write("\n[exe-agent] Shutting down...\n");
|
|
1812
|
+
abortController.abort();
|
|
1813
|
+
};
|
|
1814
|
+
process.on("SIGINT", shutdown);
|
|
1815
|
+
process.on("SIGTERM", shutdown);
|
|
1816
|
+
const config = {
|
|
1817
|
+
provider,
|
|
1818
|
+
model,
|
|
1819
|
+
systemPrompt: employee.systemPrompt,
|
|
1820
|
+
tools: registry,
|
|
1821
|
+
hooks: createDefaultHooks(),
|
|
1822
|
+
permissions: EMPLOYEE_PERMISSIONS,
|
|
1823
|
+
maxTurns: 100,
|
|
1824
|
+
cwd: process.cwd(),
|
|
1825
|
+
agentId: args.employee,
|
|
1826
|
+
abortController
|
|
1827
|
+
};
|
|
1828
|
+
const renderer = createTerminalRenderer();
|
|
1829
|
+
const history = [];
|
|
1830
|
+
console.error(`[exe-agent] ${args.employee} (${employee.role}) online \u2014 ${provider.name}/${model}`);
|
|
1831
|
+
console.error(`[exe-agent] Tools: ${registry.names().join(", ")}`);
|
|
1832
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, prompt: "> " });
|
|
1833
|
+
rl.prompt();
|
|
1834
|
+
rl.on("line", async (line) => {
|
|
1835
|
+
const input = line.trim();
|
|
1836
|
+
if (!input) {
|
|
1837
|
+
rl.prompt();
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
if (input === "/quit" || input === "/exit") {
|
|
1841
|
+
rl.close();
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
const events = agentLoop(input, history, config);
|
|
1845
|
+
await renderAgentEvents(events, renderer);
|
|
1846
|
+
history.push({ role: "user", content: input });
|
|
1847
|
+
process.stderr.write("\n");
|
|
1848
|
+
rl.prompt();
|
|
1849
|
+
});
|
|
1850
|
+
rl.on("close", () => {
|
|
1851
|
+
console.error("[exe-agent] Session ended.");
|
|
1852
|
+
process.exit(0);
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
main().catch((err) => {
|
|
1856
|
+
console.error(`[exe-agent] Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
});
|