@aurora-foundation/obsidian-next 0.2.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 +190 -0
- package/README.md +138 -0
- package/dist/index.js +3736 -0
- package/dist/mcp/index.js +448 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3736 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import React7 from "react";
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
|
|
7
|
+
// src/ui/Root.tsx
|
|
8
|
+
import { useState as useState4, useEffect, useCallback as useCallback3 } from "react";
|
|
9
|
+
import { Box as Box7, Text as Text7, useApp, useInput as useInput3 } from "ink";
|
|
10
|
+
import TextInput from "ink-text-input";
|
|
11
|
+
|
|
12
|
+
// src/core/bus.ts
|
|
13
|
+
import { EventEmitter } from "events";
|
|
14
|
+
var EventBus = class extends EventEmitter {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.setMaxListeners(20);
|
|
18
|
+
}
|
|
19
|
+
emitAgent(e) {
|
|
20
|
+
this.emit("agent", e);
|
|
21
|
+
}
|
|
22
|
+
emitUser(e) {
|
|
23
|
+
this.emit("user", e);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var bus = new EventBus();
|
|
27
|
+
|
|
28
|
+
// src/components/AgentLine.tsx
|
|
29
|
+
import React from "react";
|
|
30
|
+
import { Box, Text } from "ink";
|
|
31
|
+
|
|
32
|
+
// src/utils/syntax.ts
|
|
33
|
+
import chalk from "chalk";
|
|
34
|
+
var KEYWORDS = {
|
|
35
|
+
typescript: ["const", "let", "var", "function", "class", "interface", "type", "import", "export", "from", "return", "if", "else", "for", "while", "switch", "case", "break", "continue", "try", "catch", "throw", "async", "await", "new", "this", "super", "extends", "implements", "static", "public", "private", "protected", "readonly", "enum", "namespace", "module", "declare", "as", "is", "in", "of", "typeof", "instanceof", "void", "null", "undefined", "true", "false"],
|
|
36
|
+
javascript: ["const", "let", "var", "function", "class", "import", "export", "from", "return", "if", "else", "for", "while", "switch", "case", "break", "continue", "try", "catch", "throw", "async", "await", "new", "this", "super", "extends", "static", "typeof", "instanceof", "void", "null", "undefined", "true", "false"],
|
|
37
|
+
python: ["def", "class", "import", "from", "return", "if", "elif", "else", "for", "while", "try", "except", "finally", "with", "as", "pass", "break", "continue", "raise", "yield", "lambda", "and", "or", "not", "in", "is", "None", "True", "False", "self", "async", "await"],
|
|
38
|
+
bash: ["if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac", "function", "return", "exit", "export", "local", "readonly", "declare", "echo", "cd", "ls", "rm", "cp", "mv", "mkdir", "cat", "grep", "sed", "awk", "npm", "npx", "node", "git"],
|
|
39
|
+
json: [],
|
|
40
|
+
default: ["const", "let", "var", "function", "class", "return", "if", "else", "for", "while", "import", "export", "true", "false", "null"]
|
|
41
|
+
};
|
|
42
|
+
var TYPES = ["string", "number", "boolean", "object", "any", "void", "never", "unknown", "Array", "Promise", "Map", "Set", "Record", "Partial", "Required", "Readonly"];
|
|
43
|
+
function highlightCodeLine(line, lang) {
|
|
44
|
+
const keywords = KEYWORDS[lang] || KEYWORDS.default;
|
|
45
|
+
if (!line.trim()) return line;
|
|
46
|
+
let result = line;
|
|
47
|
+
const commentMatch = result.match(/(\/\/.*|#.*)$/);
|
|
48
|
+
if (commentMatch) {
|
|
49
|
+
const commentStart = result.indexOf(commentMatch[0]);
|
|
50
|
+
const before = result.slice(0, commentStart);
|
|
51
|
+
const comment = chalk.gray(commentMatch[0]);
|
|
52
|
+
result = before + comment;
|
|
53
|
+
line = before;
|
|
54
|
+
}
|
|
55
|
+
result = result.replace(/(["'`])(?:(?!\1)[^\\]|\\.)*\1/g, (match) => {
|
|
56
|
+
return chalk.green(match);
|
|
57
|
+
});
|
|
58
|
+
result = result.replace(/\b(\d+\.?\d*)\b/g, (match) => {
|
|
59
|
+
return chalk.yellow(match);
|
|
60
|
+
});
|
|
61
|
+
for (const kw of keywords) {
|
|
62
|
+
const regex = new RegExp(`\\b(${kw})\\b`, "g");
|
|
63
|
+
result = result.replace(regex, chalk.magenta("$1"));
|
|
64
|
+
}
|
|
65
|
+
if (lang === "typescript" || lang === "ts") {
|
|
66
|
+
for (const type of TYPES) {
|
|
67
|
+
const regex = new RegExp(`\\b(${type})\\b`, "g");
|
|
68
|
+
result = result.replace(regex, chalk.cyan("$1"));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
result = result.replace(/\b([a-zA-Z_]\w*)\s*\(/g, (match, fn) => {
|
|
72
|
+
return chalk.blue(fn) + "(";
|
|
73
|
+
});
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function highlightCodeBlock(code, lang = "default") {
|
|
77
|
+
const normalizedLang = lang.toLowerCase().replace("shell", "bash").replace("sh", "bash").replace("ts", "typescript").replace("js", "javascript").replace("py", "python");
|
|
78
|
+
const lines = code.split("\n");
|
|
79
|
+
return lines.map((line) => highlightCodeLine(line, normalizedLang)).join("\n");
|
|
80
|
+
}
|
|
81
|
+
function parseMarkdown(content) {
|
|
82
|
+
const segments = [];
|
|
83
|
+
const lines = content.split("\n");
|
|
84
|
+
let inCodeBlock = false;
|
|
85
|
+
let codeBlockLang = "";
|
|
86
|
+
let codeBlockContent = [];
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (line.startsWith("```")) {
|
|
89
|
+
if (inCodeBlock) {
|
|
90
|
+
segments.push({
|
|
91
|
+
type: "code",
|
|
92
|
+
content: codeBlockContent.join("\n"),
|
|
93
|
+
lang: codeBlockLang
|
|
94
|
+
});
|
|
95
|
+
codeBlockContent = [];
|
|
96
|
+
inCodeBlock = false;
|
|
97
|
+
codeBlockLang = "";
|
|
98
|
+
} else {
|
|
99
|
+
inCodeBlock = true;
|
|
100
|
+
codeBlockLang = line.slice(3).trim() || "default";
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (inCodeBlock) {
|
|
105
|
+
codeBlockContent.push(line);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (line.startsWith("#")) {
|
|
109
|
+
segments.push({ type: "heading", content: line });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (line.match(/^\s*[-*]\s/) || line.match(/^\s*\d+\.\s/)) {
|
|
113
|
+
segments.push({ type: "list", content: line });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
segments.push({ type: "text", content: line });
|
|
117
|
+
}
|
|
118
|
+
if (inCodeBlock && codeBlockContent.length > 0) {
|
|
119
|
+
segments.push({
|
|
120
|
+
type: "code",
|
|
121
|
+
content: codeBlockContent.join("\n"),
|
|
122
|
+
lang: codeBlockLang
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return segments;
|
|
126
|
+
}
|
|
127
|
+
function renderMarkdown(content) {
|
|
128
|
+
const segments = parseMarkdown(content);
|
|
129
|
+
const output = [];
|
|
130
|
+
for (const seg of segments) {
|
|
131
|
+
switch (seg.type) {
|
|
132
|
+
case "code":
|
|
133
|
+
output.push(chalk.bgGray.black(" " + (seg.lang || "code") + " "));
|
|
134
|
+
const highlighted = highlightCodeBlock(seg.content, seg.lang);
|
|
135
|
+
for (const line2 of highlighted.split("\n")) {
|
|
136
|
+
output.push(" " + line2);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case "heading":
|
|
140
|
+
const level = (seg.content.match(/^#+/) || [""])[0].length;
|
|
141
|
+
const text = seg.content.replace(/^#+\s*/, "");
|
|
142
|
+
if (level === 1) {
|
|
143
|
+
output.push(chalk.bold.white(text));
|
|
144
|
+
} else if (level === 2) {
|
|
145
|
+
output.push(chalk.bold.cyan(text));
|
|
146
|
+
} else {
|
|
147
|
+
output.push(chalk.bold.gray(text));
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
case "list":
|
|
151
|
+
const listText = seg.content.replace(/^(\s*)([-*]|\d+\.)\s/, (_, indent, bullet) => {
|
|
152
|
+
return indent + chalk.cyan(bullet) + " ";
|
|
153
|
+
});
|
|
154
|
+
output.push(listText);
|
|
155
|
+
break;
|
|
156
|
+
case "text":
|
|
157
|
+
let line = seg.content;
|
|
158
|
+
line = line.replace(/`([^`]+)`/g, (_, code) => {
|
|
159
|
+
return chalk.bgGray.white(" " + code + " ");
|
|
160
|
+
});
|
|
161
|
+
line = line.replace(/\*\*([^*]+)\*\*/g, (_, text2) => {
|
|
162
|
+
return chalk.bold(text2);
|
|
163
|
+
});
|
|
164
|
+
line = line.replace(/\*([^*]+)\*/g, (_, text2) => {
|
|
165
|
+
return chalk.italic(text2);
|
|
166
|
+
});
|
|
167
|
+
output.push(line);
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
output.push(seg.content);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return output.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/components/AgentLine.tsx
|
|
177
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
178
|
+
var flareAnim = ["\xB7", "\u25AA", "\u259A", "\u2756", "\u2726", "\u2739", "\u2726", "\u25AA"];
|
|
179
|
+
var isProcessingMessage = (content) => {
|
|
180
|
+
const lower = content.toLowerCase().trim();
|
|
181
|
+
if (content.length > 100) return false;
|
|
182
|
+
return lower.endsWith("...") || lower.startsWith("[safe]") || lower.startsWith("[plan]") || lower.startsWith("[auto]") || lower === "generating plan..." || lower === "executing plan...";
|
|
183
|
+
};
|
|
184
|
+
var AgentLine = ({ content, isStreaming }) => {
|
|
185
|
+
const isProcessing = isStreaming || isProcessingMessage(content);
|
|
186
|
+
const [frame, setFrame] = React.useState(0);
|
|
187
|
+
React.useEffect(() => {
|
|
188
|
+
if (!isProcessing) return;
|
|
189
|
+
const interval = setInterval(() => {
|
|
190
|
+
setFrame((prev) => (prev + 1) % flareAnim.length);
|
|
191
|
+
}, 100);
|
|
192
|
+
return () => clearInterval(interval);
|
|
193
|
+
}, [isProcessing]);
|
|
194
|
+
const hasMarkdown = content.includes("```") || content.includes("# ") || content.match(/^\s*[-*]\s/m) || content.includes("`");
|
|
195
|
+
const renderedContent = hasMarkdown && !isProcessing ? renderMarkdown(content) : content;
|
|
196
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
197
|
+
/* @__PURE__ */ jsx(Box, { marginRight: 1, children: isProcessing ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: flareAnim[frame] }) : /* @__PURE__ */ jsx(Text, { color: "cyan", children: "*" }) }),
|
|
198
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1, flexDirection: "column", children: renderedContent.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { color: isProcessing ? "gray" : void 0, children: line }, i)) })
|
|
199
|
+
] }) });
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/components/ToolOutput.tsx
|
|
203
|
+
import { useState } from "react";
|
|
204
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
205
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
206
|
+
var MAX_VISIBLE_LINES = 8;
|
|
207
|
+
var colorizeLine = (line, isError) => {
|
|
208
|
+
if (isError) {
|
|
209
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "red", children: line });
|
|
210
|
+
}
|
|
211
|
+
if (line.match(/^\s*\d+\s*\+/) || line.startsWith("+") && !line.startsWith("+++")) {
|
|
212
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "green", children: line });
|
|
213
|
+
}
|
|
214
|
+
if (line.match(/^\s*\d+\s*-/) || line.startsWith("-") && !line.startsWith("---")) {
|
|
215
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "red", children: line });
|
|
216
|
+
}
|
|
217
|
+
if (line.startsWith("@@")) {
|
|
218
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: line });
|
|
219
|
+
}
|
|
220
|
+
if (line.startsWith(">") || line.startsWith("$")) {
|
|
221
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: line });
|
|
222
|
+
}
|
|
223
|
+
if (line.includes("success") || line.includes("Success") || line.includes("\u2713") || line.includes("Build success")) {
|
|
224
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "green", children: line });
|
|
225
|
+
}
|
|
226
|
+
if (line.includes("warning") || line.includes("Warning") || line.includes("WARN")) {
|
|
227
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: line });
|
|
228
|
+
}
|
|
229
|
+
if (line.includes("error") || line.includes("Error") || line.includes("ERR") || line.includes("failed")) {
|
|
230
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "red", children: line });
|
|
231
|
+
}
|
|
232
|
+
if (line.match(/^[a-zA-Z].*\.(ts|js|tsx|jsx|json|md|css|html)/) || line.includes("dist/") || line.includes("src/")) {
|
|
233
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: line });
|
|
234
|
+
}
|
|
235
|
+
if (line.match(/\d+(\.\d+)?\s*(KB|MB|ms|s)\b/)) {
|
|
236
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "magenta", children: line });
|
|
237
|
+
}
|
|
238
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "white", children: line });
|
|
239
|
+
};
|
|
240
|
+
var ToolOutput = ({ tool, output, isError }) => {
|
|
241
|
+
const [expanded, setExpanded] = useState(false);
|
|
242
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
243
|
+
const needsCollapse = lines.length > MAX_VISIBLE_LINES;
|
|
244
|
+
const visibleLines = expanded ? lines : lines.slice(0, MAX_VISIBLE_LINES);
|
|
245
|
+
const hiddenCount = lines.length - MAX_VISIBLE_LINES;
|
|
246
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
247
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
248
|
+
/* @__PURE__ */ jsx2(Text2, { color: isError ? "red" : "green", children: isError ? " \u2717 " : " \u23BF " }),
|
|
249
|
+
colorizeLine(visibleLines[0] || "", isError)
|
|
250
|
+
] }),
|
|
251
|
+
visibleLines.slice(1).map((line, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
252
|
+
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
253
|
+
colorizeLine(line, isError)
|
|
254
|
+
] }, i)),
|
|
255
|
+
needsCollapse && !expanded && /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
|
|
256
|
+
" ... +",
|
|
257
|
+
hiddenCount,
|
|
258
|
+
" lines"
|
|
259
|
+
] }) })
|
|
260
|
+
] });
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/components/ApprovalPrompt.tsx
|
|
264
|
+
import { useState as useState2, useCallback } from "react";
|
|
265
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
266
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
267
|
+
var ApprovalPrompt = ({
|
|
268
|
+
requestId,
|
|
269
|
+
context: context2,
|
|
270
|
+
diff,
|
|
271
|
+
onResolve
|
|
272
|
+
}) => {
|
|
273
|
+
const [resolved, setResolved] = useState2(false);
|
|
274
|
+
const handleApproval = useCallback((approved) => {
|
|
275
|
+
if (resolved) return;
|
|
276
|
+
setResolved(true);
|
|
277
|
+
bus.emitUser({
|
|
278
|
+
type: "approval_response",
|
|
279
|
+
approved,
|
|
280
|
+
requestId
|
|
281
|
+
});
|
|
282
|
+
onResolve();
|
|
283
|
+
}, [resolved, requestId, onResolve]);
|
|
284
|
+
useInput((input, key) => {
|
|
285
|
+
if (resolved) return;
|
|
286
|
+
if (input.toLowerCase() === "y" || key.return && !key.shift) {
|
|
287
|
+
handleApproval(true);
|
|
288
|
+
}
|
|
289
|
+
if (input.toLowerCase() === "n" || key.escape) {
|
|
290
|
+
handleApproval(false);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
if (resolved) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return /* @__PURE__ */ jsxs3(
|
|
297
|
+
Box3,
|
|
298
|
+
{
|
|
299
|
+
flexDirection: "column",
|
|
300
|
+
borderStyle: "round",
|
|
301
|
+
borderColor: "yellow",
|
|
302
|
+
paddingX: 1,
|
|
303
|
+
paddingY: 0,
|
|
304
|
+
marginY: 1,
|
|
305
|
+
children: [
|
|
306
|
+
/* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: "[!] Approval Required" }) }),
|
|
307
|
+
/* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "white", children: context2 }) }),
|
|
308
|
+
diff && /* @__PURE__ */ jsxs3(
|
|
309
|
+
Box3,
|
|
310
|
+
{
|
|
311
|
+
flexDirection: "column",
|
|
312
|
+
borderStyle: "single",
|
|
313
|
+
borderColor: "gray",
|
|
314
|
+
paddingX: 1,
|
|
315
|
+
marginBottom: 1,
|
|
316
|
+
children: [
|
|
317
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Preview:" }),
|
|
318
|
+
diff.split("\n").slice(0, 10).map((line, i) => /* @__PURE__ */ jsx3(
|
|
319
|
+
Text3,
|
|
320
|
+
{
|
|
321
|
+
color: line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : "gray",
|
|
322
|
+
children: line
|
|
323
|
+
},
|
|
324
|
+
i
|
|
325
|
+
)),
|
|
326
|
+
diff.split("\n").length > 10 && /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
|
|
327
|
+
"... (",
|
|
328
|
+
diff.split("\n").length - 10,
|
|
329
|
+
" more lines)"
|
|
330
|
+
] })
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
),
|
|
334
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
335
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "[Y]" }),
|
|
336
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", children: " Approve " }),
|
|
337
|
+
/* @__PURE__ */ jsx3(Text3, { color: "red", bold: true, children: "[N]" }),
|
|
338
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", children: " Deny" })
|
|
339
|
+
] })
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/components/ChoicePrompt.tsx
|
|
346
|
+
import { useState as useState3, useCallback as useCallback2 } from "react";
|
|
347
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
348
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
349
|
+
var ChoicePrompt = ({
|
|
350
|
+
question,
|
|
351
|
+
options,
|
|
352
|
+
onResolve
|
|
353
|
+
}) => {
|
|
354
|
+
const [selectedIndex, setSelectedIndex] = useState3(0);
|
|
355
|
+
const [resolved, setResolved] = useState3(false);
|
|
356
|
+
const handleSelect = useCallback2(() => {
|
|
357
|
+
if (resolved) return;
|
|
358
|
+
const selected = options[selectedIndex];
|
|
359
|
+
if (!selected) return;
|
|
360
|
+
setResolved(true);
|
|
361
|
+
bus.emitUser({
|
|
362
|
+
type: "user_choice",
|
|
363
|
+
selectionId: selected.id
|
|
364
|
+
});
|
|
365
|
+
onResolve();
|
|
366
|
+
}, [resolved, selectedIndex, options, onResolve]);
|
|
367
|
+
useInput2((input, key) => {
|
|
368
|
+
if (resolved) return;
|
|
369
|
+
if (key.upArrow) {
|
|
370
|
+
setSelectedIndex((prev) => prev > 0 ? prev - 1 : options.length - 1);
|
|
371
|
+
}
|
|
372
|
+
if (key.downArrow) {
|
|
373
|
+
setSelectedIndex((prev) => prev < options.length - 1 ? prev + 1 : 0);
|
|
374
|
+
}
|
|
375
|
+
const num = parseInt(input, 10);
|
|
376
|
+
if (num >= 1 && num <= options.length) {
|
|
377
|
+
setSelectedIndex(num - 1);
|
|
378
|
+
}
|
|
379
|
+
if (key.return) {
|
|
380
|
+
handleSelect();
|
|
381
|
+
}
|
|
382
|
+
if (key.escape) {
|
|
383
|
+
setResolved(true);
|
|
384
|
+
bus.emitUser({
|
|
385
|
+
type: "user_choice",
|
|
386
|
+
selectionId: "cancel"
|
|
387
|
+
});
|
|
388
|
+
onResolve();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
if (resolved) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return /* @__PURE__ */ jsxs4(
|
|
395
|
+
Box4,
|
|
396
|
+
{
|
|
397
|
+
flexDirection: "column",
|
|
398
|
+
borderStyle: "round",
|
|
399
|
+
borderColor: "cyan",
|
|
400
|
+
paddingX: 1,
|
|
401
|
+
paddingY: 0,
|
|
402
|
+
marginY: 1,
|
|
403
|
+
children: [
|
|
404
|
+
/* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
405
|
+
"[?] ",
|
|
406
|
+
question
|
|
407
|
+
] }) }),
|
|
408
|
+
/* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginBottom: 1, children: options.map((option, index) => {
|
|
409
|
+
const isSelected = index === selectedIndex;
|
|
410
|
+
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
411
|
+
/* @__PURE__ */ jsx4(Text4, { color: isSelected ? "cyan" : "gray", children: isSelected ? ">" : " " }),
|
|
412
|
+
/* @__PURE__ */ jsxs4(Text4, { color: isSelected ? "white" : "gray", bold: isSelected, children: [
|
|
413
|
+
"[",
|
|
414
|
+
index + 1,
|
|
415
|
+
"] ",
|
|
416
|
+
option.label
|
|
417
|
+
] })
|
|
418
|
+
] }, option.id);
|
|
419
|
+
}) }),
|
|
420
|
+
/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "Use arrows or numbers to select, Enter to confirm, Esc to cancel" }) })
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/ui/Dashboard.tsx
|
|
427
|
+
import React5 from "react";
|
|
428
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
429
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
430
|
+
var flareAnim2 = ["\xB7", "\u25AA", "\u259A", "\u2756", "\u2726", "\u2739", "\u2726", "\u25AA"];
|
|
431
|
+
var owlSprites = {
|
|
432
|
+
idle: `\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259C\u258C
|
|
433
|
+
\u2590\u2588\u2588\u2580 \u2580\u2588\u2588\u258C
|
|
434
|
+
\u2590\u2588\u2588 \u2584 \u2588\u2588\u258C
|
|
435
|
+
\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259F\u258C`,
|
|
436
|
+
blink_half: `\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259C\u258C
|
|
437
|
+
\u2590\u2588\u2588\u2584 \u2584\u2588\u2588\u258C
|
|
438
|
+
\u2590\u2588\u2588 \u2584 \u2588\u2588\u258C
|
|
439
|
+
\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259F\u258C`,
|
|
440
|
+
blink_full: `\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259C\u258C
|
|
441
|
+
\u2590\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u258C
|
|
442
|
+
\u2590\u2588\u2588\u2588\u2588\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u258C
|
|
443
|
+
\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259F\u258C`,
|
|
444
|
+
suspicious: `\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259C\u258C
|
|
445
|
+
\u2590\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u258C
|
|
446
|
+
\u2590\u2588\u2588\u2580 \u2584 \u2580\u2588\u2588\u258C
|
|
447
|
+
\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259F\u258C`,
|
|
448
|
+
look_right: `\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259C\u258C
|
|
449
|
+
\u2590\u2588\u2588\u2580 \u2590\u2588\u258C
|
|
450
|
+
\u2590\u2588\u2588 \u2584 \u2590\u2588\u258C
|
|
451
|
+
\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259F\u258C`,
|
|
452
|
+
look_left: `\u2590\u259B\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u259C\u258C
|
|
453
|
+
\u2590\u2588\u258C \u2580\u2588\u2588\u258C
|
|
454
|
+
\u2590\u2588\u258C \u2584 \u2588\u2588\u258C
|
|
455
|
+
\u2590\u2599\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u259F\u258C`
|
|
456
|
+
};
|
|
457
|
+
var Dashboard = ({
|
|
458
|
+
username = "User",
|
|
459
|
+
model = "Claude Sonnet 4.5",
|
|
460
|
+
workspace = process.cwd()
|
|
461
|
+
}) => {
|
|
462
|
+
const [flareFrame, setFlareFrame] = React5.useState(0);
|
|
463
|
+
const [owlState, setOwlState] = React5.useState("idle");
|
|
464
|
+
const [columns, setColumns] = React5.useState(process.stdout.columns);
|
|
465
|
+
React5.useEffect(() => {
|
|
466
|
+
const onResize = () => setColumns(process.stdout.columns);
|
|
467
|
+
process.stdout.on("resize", onResize);
|
|
468
|
+
return () => {
|
|
469
|
+
process.stdout.off("resize", onResize);
|
|
470
|
+
};
|
|
471
|
+
}, []);
|
|
472
|
+
React5.useEffect(() => {
|
|
473
|
+
const interval = setInterval(() => {
|
|
474
|
+
setFlareFrame((prev) => (prev + 1) % flareAnim2.length);
|
|
475
|
+
}, 100);
|
|
476
|
+
return () => clearInterval(interval);
|
|
477
|
+
}, []);
|
|
478
|
+
React5.useEffect(() => {
|
|
479
|
+
let isActive = true;
|
|
480
|
+
const loop = async () => {
|
|
481
|
+
while (isActive) {
|
|
482
|
+
const delay = Math.random() * 5e3 + 3e3;
|
|
483
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
484
|
+
if (!isActive) break;
|
|
485
|
+
setOwlState("blink_half");
|
|
486
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
487
|
+
setOwlState("blink_full");
|
|
488
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
489
|
+
setOwlState("blink_half");
|
|
490
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
491
|
+
setOwlState("idle");
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
loop();
|
|
495
|
+
return () => {
|
|
496
|
+
isActive = false;
|
|
497
|
+
};
|
|
498
|
+
}, []);
|
|
499
|
+
const showRightColumn = columns >= 100;
|
|
500
|
+
return /* @__PURE__ */ jsxs5(
|
|
501
|
+
Box5,
|
|
502
|
+
{
|
|
503
|
+
borderStyle: "round",
|
|
504
|
+
borderColor: "red",
|
|
505
|
+
flexDirection: "row",
|
|
506
|
+
paddingX: 1,
|
|
507
|
+
paddingY: 0,
|
|
508
|
+
children: [
|
|
509
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width: showRightColumn ? "60%" : "100%", paddingRight: showRightColumn ? 1 : 0, children: [
|
|
510
|
+
/* @__PURE__ */ jsx5(Box5, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "white", children: [
|
|
511
|
+
"Welcome back, ",
|
|
512
|
+
username,
|
|
513
|
+
"!"
|
|
514
|
+
] }) }),
|
|
515
|
+
/* @__PURE__ */ jsx5(Box5, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: owlSprites[owlState] }) }),
|
|
516
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", alignItems: "center", children: [
|
|
517
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
|
|
518
|
+
model,
|
|
519
|
+
" ",
|
|
520
|
+
/* @__PURE__ */ jsx5(Text5, { color: "yellow", children: flareAnim2[flareFrame] }),
|
|
521
|
+
" Obsidian Next"
|
|
522
|
+
] }),
|
|
523
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: workspace })
|
|
524
|
+
] })
|
|
525
|
+
] }),
|
|
526
|
+
showRightColumn && /* @__PURE__ */ jsx5(Box5, { borderStyle: "single", borderTop: false, borderBottom: false, borderLeft: false, borderColor: "red", marginX: 1 }),
|
|
527
|
+
showRightColumn && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width: "40%", paddingLeft: 1, children: [
|
|
528
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
529
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "Commands" }),
|
|
530
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
|
|
531
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "/help" }),
|
|
532
|
+
" Show all commands"
|
|
533
|
+
] }),
|
|
534
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
|
|
535
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "/tool" }),
|
|
536
|
+
" Execute tools"
|
|
537
|
+
] }),
|
|
538
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "white", children: [
|
|
539
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "/clear" }),
|
|
540
|
+
" Clear history"
|
|
541
|
+
] })
|
|
542
|
+
] }),
|
|
543
|
+
/* @__PURE__ */ jsx5(Box5, { borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderColor: "red", marginBottom: 1 }),
|
|
544
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
545
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "Quick Start" }),
|
|
546
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Ask me to read, edit, or" }),
|
|
547
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "run commands in your code." })
|
|
548
|
+
] })
|
|
549
|
+
] })
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/ui/CommandPopup.tsx
|
|
556
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
557
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
558
|
+
var COMMANDS = [
|
|
559
|
+
{ name: "/help", desc: "Show available commands" },
|
|
560
|
+
{ name: "/init", desc: "Initialize configuration" },
|
|
561
|
+
{ name: "/config", desc: "View/edit configuration" },
|
|
562
|
+
{ name: "/models", desc: "Select AI model" },
|
|
563
|
+
{ name: "/mode", desc: "Set mode (auto/plan/safe)" },
|
|
564
|
+
{ name: "/clear", desc: "Clear conversation" },
|
|
565
|
+
{ name: "/cost", desc: "Show session cost" },
|
|
566
|
+
{ name: "/usage", desc: "Show historical usage" },
|
|
567
|
+
{ name: "/status", desc: "Show system status" },
|
|
568
|
+
{ name: "/task", desc: "View current task" },
|
|
569
|
+
{ name: "/tool", desc: "Execute tools manually" },
|
|
570
|
+
{ name: "/sandbox", desc: "Toggle sandbox mode" },
|
|
571
|
+
{ name: "/undo", desc: "Undo file changes" },
|
|
572
|
+
{ name: "/doctor", desc: "Run diagnostics" },
|
|
573
|
+
{ name: "/settings", desc: "View/edit settings" },
|
|
574
|
+
{ name: "/exit", desc: "Exit the CLI" }
|
|
575
|
+
];
|
|
576
|
+
var CommandPopup = ({ matches, selectedIndex }) => {
|
|
577
|
+
if (matches.length === 0) return null;
|
|
578
|
+
return /* @__PURE__ */ jsx6(
|
|
579
|
+
Box6,
|
|
580
|
+
{
|
|
581
|
+
flexDirection: "column",
|
|
582
|
+
borderStyle: "round",
|
|
583
|
+
borderColor: "gray",
|
|
584
|
+
paddingX: 1,
|
|
585
|
+
marginBottom: 0,
|
|
586
|
+
width: "100%",
|
|
587
|
+
children: matches.map((cmd, i) => {
|
|
588
|
+
const isSelected = i === selectedIndex;
|
|
589
|
+
return /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
|
|
590
|
+
/* @__PURE__ */ jsxs6(Text6, { color: isSelected ? "cyan" : "red", bold: isSelected, children: [
|
|
591
|
+
isSelected ? "> " : " ",
|
|
592
|
+
cmd.name
|
|
593
|
+
] }),
|
|
594
|
+
/* @__PURE__ */ jsx6(Text6, { color: isSelected ? "white" : "gray", children: cmd.desc })
|
|
595
|
+
] }, cmd.name);
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/core/history.ts
|
|
602
|
+
import fs from "fs/promises";
|
|
603
|
+
import path from "path";
|
|
604
|
+
import os from "os";
|
|
605
|
+
var HistoryManager = class {
|
|
606
|
+
historyPath;
|
|
607
|
+
saveTimer = null;
|
|
608
|
+
constructor(customPath) {
|
|
609
|
+
this.historyPath = customPath || path.join(os.homedir(), ".obsidian", "history.json");
|
|
610
|
+
}
|
|
611
|
+
async load() {
|
|
612
|
+
try {
|
|
613
|
+
const data = await fs.readFile(this.historyPath, "utf-8");
|
|
614
|
+
const events = JSON.parse(data);
|
|
615
|
+
return Array.isArray(events) ? events : [];
|
|
616
|
+
} catch {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async save(events) {
|
|
621
|
+
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
622
|
+
this.saveTimer = setTimeout(async () => {
|
|
623
|
+
try {
|
|
624
|
+
const dir = path.dirname(this.historyPath);
|
|
625
|
+
await fs.mkdir(dir, { recursive: true });
|
|
626
|
+
await fs.writeFile(this.historyPath, JSON.stringify(events, null, 2));
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.error("Failed to save history:", error);
|
|
629
|
+
}
|
|
630
|
+
}, 500);
|
|
631
|
+
}
|
|
632
|
+
async clear() {
|
|
633
|
+
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
634
|
+
try {
|
|
635
|
+
await fs.writeFile(this.historyPath, JSON.stringify([], null, 2));
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
var history = new HistoryManager();
|
|
641
|
+
|
|
642
|
+
// src/core/usage.ts
|
|
643
|
+
import fs2 from "fs/promises";
|
|
644
|
+
import path2 from "path";
|
|
645
|
+
import os2 from "os";
|
|
646
|
+
import { z } from "zod";
|
|
647
|
+
var UsageSchema = z.object({
|
|
648
|
+
totalSessions: z.number().default(0),
|
|
649
|
+
totalRequests: z.number().default(0),
|
|
650
|
+
totalInputTokens: z.number().default(0),
|
|
651
|
+
totalOutputTokens: z.number().default(0),
|
|
652
|
+
totalCost: z.number().default(0)
|
|
653
|
+
});
|
|
654
|
+
var MODEL_PRICES = {
|
|
655
|
+
// Claude 4.5 Family (2025-2026)
|
|
656
|
+
"claude-sonnet-4-5-20250929": { input: 3, output: 15 },
|
|
657
|
+
"claude-haiku-4-5-20251001": { input: 1, output: 5 },
|
|
658
|
+
"claude-opus-4-5-20251101": { input: 5, output: 25 },
|
|
659
|
+
// Claude 3.5 Family (backward compatibility)
|
|
660
|
+
"claude-3-5-sonnet": { input: 3, output: 15 },
|
|
661
|
+
"claude-3-5-haiku": { input: 0.25, output: 1.25 },
|
|
662
|
+
"claude-3-haiku": { input: 0.25, output: 1.25 },
|
|
663
|
+
"claude-3-opus": { input: 15, output: 75 },
|
|
664
|
+
"claude-3-sonnet": { input: 3, output: 15 }
|
|
665
|
+
};
|
|
666
|
+
var UsageTracker = class {
|
|
667
|
+
usagePath;
|
|
668
|
+
stats;
|
|
669
|
+
sessionCost = 0;
|
|
670
|
+
constructor(customPath) {
|
|
671
|
+
this.usagePath = customPath || path2.join(os2.homedir(), ".obsidian", "usage.json");
|
|
672
|
+
this.stats = UsageSchema.parse({});
|
|
673
|
+
}
|
|
674
|
+
async init() {
|
|
675
|
+
try {
|
|
676
|
+
const data = await fs2.readFile(this.usagePath, "utf-8");
|
|
677
|
+
this.stats = UsageSchema.parse(JSON.parse(data));
|
|
678
|
+
} catch {
|
|
679
|
+
await this.save();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async track(model, input, output) {
|
|
683
|
+
let prices = MODEL_PRICES[model];
|
|
684
|
+
if (!prices) {
|
|
685
|
+
const key = Object.keys(MODEL_PRICES).find((k) => model.includes(k));
|
|
686
|
+
prices = key ? MODEL_PRICES[key] : { input: 0, output: 0 };
|
|
687
|
+
}
|
|
688
|
+
const cost = input / 1e6 * prices.input + output / 1e6 * prices.output;
|
|
689
|
+
this.stats.totalRequests++;
|
|
690
|
+
this.stats.totalInputTokens += input;
|
|
691
|
+
this.stats.totalOutputTokens += output;
|
|
692
|
+
this.stats.totalCost += cost;
|
|
693
|
+
this.sessionCost += cost;
|
|
694
|
+
await this.save();
|
|
695
|
+
}
|
|
696
|
+
getSessionCost() {
|
|
697
|
+
return this.sessionCost;
|
|
698
|
+
}
|
|
699
|
+
async trackSession() {
|
|
700
|
+
this.stats.totalSessions++;
|
|
701
|
+
await this.save();
|
|
702
|
+
}
|
|
703
|
+
getStats() {
|
|
704
|
+
return this.stats;
|
|
705
|
+
}
|
|
706
|
+
async save() {
|
|
707
|
+
const dir = path2.dirname(this.usagePath);
|
|
708
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
709
|
+
await fs2.writeFile(this.usagePath, JSON.stringify(this.stats, null, 2));
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
var usage = new UsageTracker();
|
|
713
|
+
|
|
714
|
+
// src/core/config.ts
|
|
715
|
+
import fs3 from "fs/promises";
|
|
716
|
+
import path3 from "path";
|
|
717
|
+
import os3 from "os";
|
|
718
|
+
import { z as z2 } from "zod";
|
|
719
|
+
import dotenv from "dotenv";
|
|
720
|
+
dotenv.config();
|
|
721
|
+
var ConfigSchema = z2.object({
|
|
722
|
+
apiKey: z2.string().optional(),
|
|
723
|
+
model: z2.string().default("claude-sonnet-4-5-20250929"),
|
|
724
|
+
workspaceRoot: z2.string().default(process.cwd()),
|
|
725
|
+
maxTokens: z2.number().default(8192),
|
|
726
|
+
language: z2.string().default("en")
|
|
727
|
+
});
|
|
728
|
+
var DEFAULT_CONFIG = {
|
|
729
|
+
model: "claude-3-5-sonnet",
|
|
730
|
+
maxTokens: 4096,
|
|
731
|
+
language: "en",
|
|
732
|
+
workspaceRoot: process.cwd()
|
|
733
|
+
};
|
|
734
|
+
var ConfigManager = class {
|
|
735
|
+
configPath;
|
|
736
|
+
cachedConfig = null;
|
|
737
|
+
constructor(customPath) {
|
|
738
|
+
this.configPath = customPath || path3.join(os3.homedir(), ".obsidian", "config.json");
|
|
739
|
+
}
|
|
740
|
+
async load() {
|
|
741
|
+
if (this.cachedConfig) return this.cachedConfig;
|
|
742
|
+
return this.reload();
|
|
743
|
+
}
|
|
744
|
+
async reload() {
|
|
745
|
+
let loadedConfig = DEFAULT_CONFIG;
|
|
746
|
+
try {
|
|
747
|
+
const data = await fs3.readFile(this.configPath, "utf-8");
|
|
748
|
+
const parsed = JSON.parse(data);
|
|
749
|
+
loadedConfig = { ...DEFAULT_CONFIG, ...parsed };
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
const envKey = process.env.ANTHROPIC_API_KEY;
|
|
753
|
+
const finalConfig = ConfigSchema.parse({
|
|
754
|
+
...loadedConfig,
|
|
755
|
+
apiKey: envKey || loadedConfig.apiKey
|
|
756
|
+
});
|
|
757
|
+
this.cachedConfig = finalConfig;
|
|
758
|
+
return finalConfig;
|
|
759
|
+
}
|
|
760
|
+
clearCache() {
|
|
761
|
+
this.cachedConfig = null;
|
|
762
|
+
}
|
|
763
|
+
async save(config2) {
|
|
764
|
+
const dir = path3.dirname(this.configPath);
|
|
765
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
766
|
+
await fs3.writeFile(this.configPath, JSON.stringify(config2, null, 2));
|
|
767
|
+
this.clearCache();
|
|
768
|
+
}
|
|
769
|
+
async exists() {
|
|
770
|
+
try {
|
|
771
|
+
await fs3.access(this.configPath);
|
|
772
|
+
return true;
|
|
773
|
+
} catch {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
getPath() {
|
|
778
|
+
return this.configPath;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
var config = new ConfigManager();
|
|
782
|
+
|
|
783
|
+
// src/core/context.ts
|
|
784
|
+
import fs5 from "fs/promises";
|
|
785
|
+
import path5 from "path";
|
|
786
|
+
|
|
787
|
+
// src/core/settings.ts
|
|
788
|
+
import fs4 from "fs/promises";
|
|
789
|
+
import path4 from "path";
|
|
790
|
+
import { z as z3 } from "zod";
|
|
791
|
+
var SETTINGS_DIR = ".obsidian";
|
|
792
|
+
var SETTINGS_FILE = "settings.json";
|
|
793
|
+
var SettingsSchema = z3.object({
|
|
794
|
+
// Execution mode
|
|
795
|
+
mode: z3.enum(["auto", "plan", "safe"]).default("safe"),
|
|
796
|
+
// Auto-accept settings
|
|
797
|
+
autoAccept: z3.object({
|
|
798
|
+
enabled: z3.boolean().default(false),
|
|
799
|
+
readOperations: z3.boolean().default(true),
|
|
800
|
+
safeCommands: z3.boolean().default(true)
|
|
801
|
+
}).default({}),
|
|
802
|
+
// Tool permissions
|
|
803
|
+
permissions: z3.object({
|
|
804
|
+
// Patterns always allowed without prompt: "tool:pattern"
|
|
805
|
+
allow: z3.array(z3.string()).default([]),
|
|
806
|
+
// Patterns always blocked
|
|
807
|
+
deny: z3.array(z3.string()).default([])
|
|
808
|
+
}).default({}),
|
|
809
|
+
// UI preferences
|
|
810
|
+
ui: z3.object({
|
|
811
|
+
syntaxHighlight: z3.boolean().default(true),
|
|
812
|
+
diffColors: z3.boolean().default(true),
|
|
813
|
+
showLineNumbers: z3.boolean().default(true)
|
|
814
|
+
}).default({})
|
|
815
|
+
});
|
|
816
|
+
var DEFAULT_SETTINGS = {
|
|
817
|
+
mode: "safe",
|
|
818
|
+
autoAccept: {
|
|
819
|
+
enabled: false,
|
|
820
|
+
readOperations: false,
|
|
821
|
+
safeCommands: false
|
|
822
|
+
},
|
|
823
|
+
permissions: {
|
|
824
|
+
allow: [],
|
|
825
|
+
// Empty - user builds their own allow list
|
|
826
|
+
deny: []
|
|
827
|
+
},
|
|
828
|
+
ui: {
|
|
829
|
+
syntaxHighlight: true,
|
|
830
|
+
diffColors: true,
|
|
831
|
+
showLineNumbers: true
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
var SettingsManager = class {
|
|
835
|
+
settingsPath;
|
|
836
|
+
cached = null;
|
|
837
|
+
constructor() {
|
|
838
|
+
this.settingsPath = path4.join(process.cwd(), SETTINGS_DIR, SETTINGS_FILE);
|
|
839
|
+
}
|
|
840
|
+
async load() {
|
|
841
|
+
if (this.cached) return this.cached;
|
|
842
|
+
return this.reload();
|
|
843
|
+
}
|
|
844
|
+
async reload() {
|
|
845
|
+
try {
|
|
846
|
+
const data = await fs4.readFile(this.settingsPath, "utf-8");
|
|
847
|
+
const parsed = JSON.parse(data);
|
|
848
|
+
this.cached = SettingsSchema.parse({ ...DEFAULT_SETTINGS, ...parsed });
|
|
849
|
+
} catch {
|
|
850
|
+
this.cached = DEFAULT_SETTINGS;
|
|
851
|
+
await this.save(DEFAULT_SETTINGS);
|
|
852
|
+
}
|
|
853
|
+
return this.cached;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Add a permission to the allow list (called when user approves a command)
|
|
857
|
+
*/
|
|
858
|
+
async addAllowedPermission(tool, command) {
|
|
859
|
+
const s = await this.load();
|
|
860
|
+
const pattern = `${tool}:${command}`;
|
|
861
|
+
if (!s.permissions.allow.includes(pattern)) {
|
|
862
|
+
s.permissions.allow.push(pattern);
|
|
863
|
+
await this.save({ permissions: s.permissions });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Add a permission to the deny list
|
|
868
|
+
*/
|
|
869
|
+
async addDeniedPermission(tool, command) {
|
|
870
|
+
const s = await this.load();
|
|
871
|
+
const pattern = `${tool}:${command}`;
|
|
872
|
+
if (!s.permissions.deny.includes(pattern)) {
|
|
873
|
+
s.permissions.deny.push(pattern);
|
|
874
|
+
await this.save({ permissions: s.permissions });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async save(newSettings) {
|
|
878
|
+
const current = await this.load();
|
|
879
|
+
const merged = { ...current, ...newSettings };
|
|
880
|
+
if (newSettings.autoAccept) {
|
|
881
|
+
merged.autoAccept = { ...current.autoAccept, ...newSettings.autoAccept };
|
|
882
|
+
}
|
|
883
|
+
if (newSettings.permissions) {
|
|
884
|
+
merged.permissions = { ...current.permissions, ...newSettings.permissions };
|
|
885
|
+
}
|
|
886
|
+
if (newSettings.ui) {
|
|
887
|
+
merged.ui = { ...current.ui, ...newSettings.ui };
|
|
888
|
+
}
|
|
889
|
+
const validated = SettingsSchema.parse(merged);
|
|
890
|
+
const dir = path4.dirname(this.settingsPath);
|
|
891
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
892
|
+
await fs4.writeFile(this.settingsPath, JSON.stringify(validated, null, 2));
|
|
893
|
+
this.cached = validated;
|
|
894
|
+
}
|
|
895
|
+
async get(key) {
|
|
896
|
+
const s = await this.load();
|
|
897
|
+
return s[key];
|
|
898
|
+
}
|
|
899
|
+
async set(key, value) {
|
|
900
|
+
await this.save({ [key]: value });
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Check if a tool:command pattern is allowed
|
|
904
|
+
*/
|
|
905
|
+
async isAllowed(tool, command) {
|
|
906
|
+
const s = await this.load();
|
|
907
|
+
const pattern = `${tool}:${command}`;
|
|
908
|
+
for (const deny of s.permissions.deny) {
|
|
909
|
+
if (this.matchPattern(pattern, deny)) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
for (const allow of s.permissions.allow) {
|
|
914
|
+
if (this.matchPattern(pattern, allow)) {
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Check if a tool:command pattern is explicitly denied
|
|
922
|
+
*/
|
|
923
|
+
async isDenied(tool, command) {
|
|
924
|
+
const s = await this.load();
|
|
925
|
+
const pattern = `${tool}:${command}`;
|
|
926
|
+
for (const deny of s.permissions.deny) {
|
|
927
|
+
if (this.matchPattern(pattern, deny)) {
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Simple glob-like pattern matching
|
|
935
|
+
* Supports * as wildcard
|
|
936
|
+
*/
|
|
937
|
+
matchPattern(value, pattern) {
|
|
938
|
+
const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
939
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
940
|
+
}
|
|
941
|
+
clearCache() {
|
|
942
|
+
this.cached = null;
|
|
943
|
+
}
|
|
944
|
+
getPath() {
|
|
945
|
+
return this.settingsPath;
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
var settings = new SettingsManager();
|
|
949
|
+
|
|
950
|
+
// src/core/context.ts
|
|
951
|
+
var CONTEXT_DIR = ".obsidian";
|
|
952
|
+
var CONTEXT_FILE = "context.json";
|
|
953
|
+
function generateSessionId() {
|
|
954
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
955
|
+
}
|
|
956
|
+
function createEmptyContext() {
|
|
957
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
958
|
+
return {
|
|
959
|
+
session_id: generateSessionId(),
|
|
960
|
+
mode: "safe",
|
|
961
|
+
current_task: null,
|
|
962
|
+
files_read: [],
|
|
963
|
+
files_modified: [],
|
|
964
|
+
working_set: [],
|
|
965
|
+
last_action: null,
|
|
966
|
+
created_at: now,
|
|
967
|
+
updated_at: now
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
var ContextManager = class {
|
|
971
|
+
ctx = createEmptyContext();
|
|
972
|
+
contextPath;
|
|
973
|
+
constructor() {
|
|
974
|
+
this.contextPath = path5.join(process.cwd(), CONTEXT_DIR, CONTEXT_FILE);
|
|
975
|
+
}
|
|
976
|
+
async init() {
|
|
977
|
+
const dir = path5.join(process.cwd(), CONTEXT_DIR);
|
|
978
|
+
try {
|
|
979
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
980
|
+
await this.load();
|
|
981
|
+
} catch {
|
|
982
|
+
this.ctx = createEmptyContext();
|
|
983
|
+
await this.save();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async load() {
|
|
987
|
+
try {
|
|
988
|
+
const data = await fs5.readFile(this.contextPath, "utf-8");
|
|
989
|
+
this.ctx = JSON.parse(data);
|
|
990
|
+
} catch {
|
|
991
|
+
this.ctx = createEmptyContext();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async save() {
|
|
995
|
+
this.ctx.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
996
|
+
await fs5.writeFile(this.contextPath, JSON.stringify(this.ctx, null, 2));
|
|
997
|
+
}
|
|
998
|
+
// Getters
|
|
999
|
+
get() {
|
|
1000
|
+
return { ...this.ctx };
|
|
1001
|
+
}
|
|
1002
|
+
getMode() {
|
|
1003
|
+
return this.ctx.mode;
|
|
1004
|
+
}
|
|
1005
|
+
async syncModeFromSettings() {
|
|
1006
|
+
const s = await settings.load();
|
|
1007
|
+
this.ctx.mode = s.mode;
|
|
1008
|
+
}
|
|
1009
|
+
getCurrentTask() {
|
|
1010
|
+
return this.ctx.current_task;
|
|
1011
|
+
}
|
|
1012
|
+
getWorkingSet() {
|
|
1013
|
+
return [...this.ctx.working_set];
|
|
1014
|
+
}
|
|
1015
|
+
// Setters
|
|
1016
|
+
async setMode(mode) {
|
|
1017
|
+
this.ctx.mode = mode;
|
|
1018
|
+
await settings.set("mode", mode);
|
|
1019
|
+
await this.save();
|
|
1020
|
+
}
|
|
1021
|
+
async setTask(task) {
|
|
1022
|
+
this.ctx.current_task = task;
|
|
1023
|
+
await this.save();
|
|
1024
|
+
}
|
|
1025
|
+
// Tracking
|
|
1026
|
+
async trackRead(filePath) {
|
|
1027
|
+
const normalized = path5.relative(process.cwd(), path5.resolve(filePath));
|
|
1028
|
+
if (!this.ctx.files_read.includes(normalized)) {
|
|
1029
|
+
this.ctx.files_read.push(normalized);
|
|
1030
|
+
}
|
|
1031
|
+
if (!this.ctx.working_set.includes(normalized)) {
|
|
1032
|
+
this.ctx.working_set.push(normalized);
|
|
1033
|
+
}
|
|
1034
|
+
await this.save();
|
|
1035
|
+
}
|
|
1036
|
+
async trackModified(filePath) {
|
|
1037
|
+
const normalized = path5.relative(process.cwd(), path5.resolve(filePath));
|
|
1038
|
+
if (!this.ctx.files_modified.includes(normalized)) {
|
|
1039
|
+
this.ctx.files_modified.push(normalized);
|
|
1040
|
+
}
|
|
1041
|
+
if (!this.ctx.working_set.includes(normalized)) {
|
|
1042
|
+
this.ctx.working_set.push(normalized);
|
|
1043
|
+
}
|
|
1044
|
+
await this.save();
|
|
1045
|
+
}
|
|
1046
|
+
async setLastAction(action) {
|
|
1047
|
+
this.ctx.last_action = action;
|
|
1048
|
+
await this.save();
|
|
1049
|
+
}
|
|
1050
|
+
// Reset
|
|
1051
|
+
async reset() {
|
|
1052
|
+
this.ctx = createEmptyContext();
|
|
1053
|
+
await this.save();
|
|
1054
|
+
}
|
|
1055
|
+
async clearWorkingSet() {
|
|
1056
|
+
this.ctx.working_set = [];
|
|
1057
|
+
await this.save();
|
|
1058
|
+
}
|
|
1059
|
+
// Summary for LLM
|
|
1060
|
+
getSummary() {
|
|
1061
|
+
const lines = [];
|
|
1062
|
+
if (this.ctx.current_task) {
|
|
1063
|
+
lines.push(`Task: ${this.ctx.current_task}`);
|
|
1064
|
+
}
|
|
1065
|
+
if (this.ctx.working_set.length > 0) {
|
|
1066
|
+
lines.push(`Working set: ${this.ctx.working_set.slice(-5).join(", ")}`);
|
|
1067
|
+
}
|
|
1068
|
+
if (this.ctx.files_modified.length > 0) {
|
|
1069
|
+
lines.push(`Modified: ${this.ctx.files_modified.slice(-3).join(", ")}`);
|
|
1070
|
+
}
|
|
1071
|
+
return lines.join("\n");
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
var context = new ContextManager();
|
|
1075
|
+
|
|
1076
|
+
// src/core/llm.ts
|
|
1077
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
1078
|
+
|
|
1079
|
+
// src/core/tools.ts
|
|
1080
|
+
import { exec as exec2 } from "child_process";
|
|
1081
|
+
import { promisify as promisify2 } from "util";
|
|
1082
|
+
import fs8 from "fs/promises";
|
|
1083
|
+
import path8 from "path";
|
|
1084
|
+
|
|
1085
|
+
// src/core/auditor.ts
|
|
1086
|
+
import path6 from "path";
|
|
1087
|
+
import fs6 from "fs/promises";
|
|
1088
|
+
var BLOCKED_PATTERNS = [
|
|
1089
|
+
"rm -rf /",
|
|
1090
|
+
"rm -fr /",
|
|
1091
|
+
":(){:|:&};:",
|
|
1092
|
+
// Fork bomb
|
|
1093
|
+
"> /dev/sda",
|
|
1094
|
+
// Disk overwrite
|
|
1095
|
+
"mkfs",
|
|
1096
|
+
"dd if=",
|
|
1097
|
+
"chmod -R 777",
|
|
1098
|
+
":(){ :|:& };:",
|
|
1099
|
+
"curl | sh",
|
|
1100
|
+
// Pipe to shell
|
|
1101
|
+
"wget | sh",
|
|
1102
|
+
"curl | bash",
|
|
1103
|
+
"wget | bash"
|
|
1104
|
+
];
|
|
1105
|
+
var APPROVAL_PATTERNS = [
|
|
1106
|
+
{ pattern: "rm -rf", reason: "Recursive delete operation" },
|
|
1107
|
+
{ pattern: "rm -r", reason: "Recursive delete operation" },
|
|
1108
|
+
{ pattern: "git push --force", reason: "Force push to remote" },
|
|
1109
|
+
{ pattern: "git reset --hard", reason: "Hard reset (loses changes)" },
|
|
1110
|
+
{ pattern: "npm publish", reason: "Publishing to npm registry" },
|
|
1111
|
+
{ pattern: "docker rm", reason: "Removing Docker containers" },
|
|
1112
|
+
{ pattern: "DROP TABLE", reason: "SQL table deletion" },
|
|
1113
|
+
{ pattern: "DROP DATABASE", reason: "SQL database deletion" },
|
|
1114
|
+
{ pattern: "truncate", reason: "Truncating data" }
|
|
1115
|
+
];
|
|
1116
|
+
var Auditor = class {
|
|
1117
|
+
workspaceRoot;
|
|
1118
|
+
constructor(root = process.cwd()) {
|
|
1119
|
+
this.workspaceRoot = path6.resolve(root);
|
|
1120
|
+
}
|
|
1121
|
+
async checkCommand(command) {
|
|
1122
|
+
const lowerCommand = command.toLowerCase();
|
|
1123
|
+
if (BLOCKED_PATTERNS.some((p) => command.includes(p))) {
|
|
1124
|
+
return {
|
|
1125
|
+
approved: false,
|
|
1126
|
+
reason: "Detected destructive command pattern",
|
|
1127
|
+
isCritical: true
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
if (await settings.isDenied("bash", command)) {
|
|
1131
|
+
return {
|
|
1132
|
+
approved: false,
|
|
1133
|
+
reason: "Command blocked by settings",
|
|
1134
|
+
isCritical: false
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
if (await settings.isAllowed("bash", command)) {
|
|
1138
|
+
return {
|
|
1139
|
+
approved: true,
|
|
1140
|
+
autoApproved: true
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
for (const { pattern, reason } of APPROVAL_PATTERNS) {
|
|
1144
|
+
if (lowerCommand.includes(pattern.toLowerCase())) {
|
|
1145
|
+
return {
|
|
1146
|
+
approved: true,
|
|
1147
|
+
requiresApproval: true,
|
|
1148
|
+
reason
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const s = await settings.load();
|
|
1153
|
+
if (s.mode === "safe") {
|
|
1154
|
+
return {
|
|
1155
|
+
approved: true,
|
|
1156
|
+
requiresApproval: true,
|
|
1157
|
+
reason: "Safe mode requires approval for all commands"
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
return { approved: true };
|
|
1161
|
+
}
|
|
1162
|
+
checkPath(filePath) {
|
|
1163
|
+
const resolved = path6.resolve(this.workspaceRoot, filePath);
|
|
1164
|
+
if (!resolved.startsWith(this.workspaceRoot)) {
|
|
1165
|
+
return { approved: false, reason: `Path outside workspace: ${filePath}`, isCritical: true };
|
|
1166
|
+
}
|
|
1167
|
+
return { approved: true };
|
|
1168
|
+
}
|
|
1169
|
+
async checkFileEdit(filePath) {
|
|
1170
|
+
const pathCheck = this.checkPath(filePath);
|
|
1171
|
+
if (!pathCheck.approved) return pathCheck;
|
|
1172
|
+
try {
|
|
1173
|
+
await fs6.access(filePath);
|
|
1174
|
+
return { approved: true };
|
|
1175
|
+
} catch {
|
|
1176
|
+
return { approved: false, reason: `File not found: ${filePath}`, isCritical: false };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
var auditor = new Auditor();
|
|
1181
|
+
|
|
1182
|
+
// src/core/sandbox.ts
|
|
1183
|
+
import { exec } from "child_process";
|
|
1184
|
+
import { promisify } from "util";
|
|
1185
|
+
import os4 from "os";
|
|
1186
|
+
var execAsync = promisify(exec);
|
|
1187
|
+
var DEFAULT_SANDBOX_CONFIG = {
|
|
1188
|
+
mode: "local",
|
|
1189
|
+
allowedDomains: [
|
|
1190
|
+
"*.github.com",
|
|
1191
|
+
"*.npmjs.org",
|
|
1192
|
+
"*.npmjs.com",
|
|
1193
|
+
"api.anthropic.com",
|
|
1194
|
+
"registry.npmjs.org"
|
|
1195
|
+
],
|
|
1196
|
+
deniedDomains: [],
|
|
1197
|
+
denyRead: [
|
|
1198
|
+
"~/.ssh",
|
|
1199
|
+
"~/.aws",
|
|
1200
|
+
"~/.config/gcloud",
|
|
1201
|
+
"~/.kube",
|
|
1202
|
+
"~/.gnupg"
|
|
1203
|
+
],
|
|
1204
|
+
allowWrite: [
|
|
1205
|
+
".",
|
|
1206
|
+
// Current workspace
|
|
1207
|
+
"/tmp"
|
|
1208
|
+
],
|
|
1209
|
+
denyWrite: [
|
|
1210
|
+
".env",
|
|
1211
|
+
".env.*",
|
|
1212
|
+
"*.key",
|
|
1213
|
+
"*.pem",
|
|
1214
|
+
".git/config"
|
|
1215
|
+
]
|
|
1216
|
+
};
|
|
1217
|
+
var SandboxExecutor = class {
|
|
1218
|
+
initialized = false;
|
|
1219
|
+
mode = "local";
|
|
1220
|
+
sandboxManager = null;
|
|
1221
|
+
config = DEFAULT_SANDBOX_CONFIG;
|
|
1222
|
+
/**
|
|
1223
|
+
* Initialize sandbox with configuration
|
|
1224
|
+
*/
|
|
1225
|
+
async initialize() {
|
|
1226
|
+
try {
|
|
1227
|
+
const cfg = await config.load();
|
|
1228
|
+
this.mode = cfg.executionMode || "local";
|
|
1229
|
+
this.config = {
|
|
1230
|
+
...DEFAULT_SANDBOX_CONFIG,
|
|
1231
|
+
...cfg.sandbox || {}
|
|
1232
|
+
};
|
|
1233
|
+
if (this.mode !== "sandbox") {
|
|
1234
|
+
this.initialized = true;
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
const { SandboxManager } = await import("@anthropic-ai/sandbox-runtime");
|
|
1239
|
+
const runtimeConfig = {
|
|
1240
|
+
network: {
|
|
1241
|
+
allowedDomains: this.config.allowedDomains,
|
|
1242
|
+
deniedDomains: this.config.deniedDomains
|
|
1243
|
+
},
|
|
1244
|
+
filesystem: {
|
|
1245
|
+
denyRead: this.config.denyRead,
|
|
1246
|
+
allowWrite: this.config.allowWrite,
|
|
1247
|
+
denyWrite: this.config.denyWrite
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
await SandboxManager.initialize(runtimeConfig);
|
|
1251
|
+
this.sandboxManager = SandboxManager;
|
|
1252
|
+
this.initialized = true;
|
|
1253
|
+
bus.emitAgent({
|
|
1254
|
+
type: "thought",
|
|
1255
|
+
content: "[SANDBOX] Initialized with OS-level isolation"
|
|
1256
|
+
});
|
|
1257
|
+
return true;
|
|
1258
|
+
} catch (importError) {
|
|
1259
|
+
bus.emitAgent({
|
|
1260
|
+
type: "error",
|
|
1261
|
+
message: `Sandbox runtime unavailable: ${importError.message}. Using local mode.`
|
|
1262
|
+
});
|
|
1263
|
+
this.mode = "local";
|
|
1264
|
+
this.initialized = true;
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
bus.emitAgent({
|
|
1269
|
+
type: "error",
|
|
1270
|
+
message: `Sandbox initialization failed: ${error.message}`
|
|
1271
|
+
});
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Wrap a command with sandbox protection
|
|
1277
|
+
*/
|
|
1278
|
+
async wrapCommand(command) {
|
|
1279
|
+
if (!this.initialized) {
|
|
1280
|
+
await this.initialize();
|
|
1281
|
+
}
|
|
1282
|
+
if (this.mode === "local") {
|
|
1283
|
+
return command;
|
|
1284
|
+
}
|
|
1285
|
+
if (this.sandboxManager) {
|
|
1286
|
+
try {
|
|
1287
|
+
return await this.sandboxManager.wrapWithSandbox(command);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return this.wrapWithNativeSandbox(command);
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Wrap command with native OS sandbox (macOS sandbox-exec or Linux firejail)
|
|
1295
|
+
*/
|
|
1296
|
+
async wrapWithNativeSandbox(command) {
|
|
1297
|
+
const platform = os4.platform();
|
|
1298
|
+
if (platform === "darwin") {
|
|
1299
|
+
const profile = `
|
|
1300
|
+
(version 1)
|
|
1301
|
+
(allow default)
|
|
1302
|
+
(deny network*)
|
|
1303
|
+
(allow network-outbound (remote tcp "*:80" "*:443"))
|
|
1304
|
+
(deny file-read* (subpath "${os4.homedir()}/.ssh"))
|
|
1305
|
+
(deny file-read* (subpath "${os4.homedir()}/.aws"))
|
|
1306
|
+
(deny file-read* (subpath "${os4.homedir()}/.gnupg"))
|
|
1307
|
+
(deny file-write* (literal "${process.cwd()}/.env"))
|
|
1308
|
+
`.trim();
|
|
1309
|
+
const escapedCommand = command.replace(/"/g, '\\"');
|
|
1310
|
+
return `sandbox-exec -p '${profile}' bash -c "${escapedCommand}"`;
|
|
1311
|
+
}
|
|
1312
|
+
if (platform === "linux") {
|
|
1313
|
+
try {
|
|
1314
|
+
await execAsync("which firejail");
|
|
1315
|
+
const escapedCommand = command.replace(/'/g, "'\\''");
|
|
1316
|
+
return `firejail --net=none --blacklist=~/.ssh --blacklist=~/.aws --blacklist=~/.gnupg bash -c '${escapedCommand}'`;
|
|
1317
|
+
} catch {
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return command;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Get current execution mode
|
|
1324
|
+
*/
|
|
1325
|
+
getMode() {
|
|
1326
|
+
return this.mode;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Set execution mode
|
|
1330
|
+
*/
|
|
1331
|
+
async setMode(mode) {
|
|
1332
|
+
this.mode = mode;
|
|
1333
|
+
if (mode === "sandbox" && !this.sandboxManager) {
|
|
1334
|
+
await this.initialize();
|
|
1335
|
+
}
|
|
1336
|
+
bus.emitAgent({
|
|
1337
|
+
type: "thought",
|
|
1338
|
+
content: `[SANDBOX] Execution mode set to: ${mode}`
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Get current sandbox configuration
|
|
1343
|
+
*/
|
|
1344
|
+
getConfig() {
|
|
1345
|
+
return { ...this.config };
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Update sandbox configuration
|
|
1349
|
+
*/
|
|
1350
|
+
updateConfig(updates) {
|
|
1351
|
+
this.config = { ...this.config, ...updates };
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Reset and cleanup sandbox resources
|
|
1355
|
+
*/
|
|
1356
|
+
async reset() {
|
|
1357
|
+
if (this.sandboxManager) {
|
|
1358
|
+
try {
|
|
1359
|
+
await this.sandboxManager.reset();
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
this.initialized = false;
|
|
1364
|
+
this.sandboxManager = null;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Check if sandbox mode is available on this system
|
|
1368
|
+
*/
|
|
1369
|
+
async isAvailable() {
|
|
1370
|
+
try {
|
|
1371
|
+
await import("@anthropic-ai/sandbox-runtime");
|
|
1372
|
+
return true;
|
|
1373
|
+
} catch {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
var sandbox = new SandboxExecutor();
|
|
1379
|
+
|
|
1380
|
+
// src/core/undo.ts
|
|
1381
|
+
import { PrismaClient } from "@prisma/client";
|
|
1382
|
+
import fs7 from "fs/promises";
|
|
1383
|
+
import path7 from "path";
|
|
1384
|
+
var UndoManager = class {
|
|
1385
|
+
prisma = null;
|
|
1386
|
+
sessionId = null;
|
|
1387
|
+
changes = [];
|
|
1388
|
+
// In-memory stack for fast access
|
|
1389
|
+
dbEnabled = false;
|
|
1390
|
+
async init(sessionId) {
|
|
1391
|
+
this.sessionId = sessionId;
|
|
1392
|
+
try {
|
|
1393
|
+
this.prisma = new PrismaClient();
|
|
1394
|
+
await this.prisma.$connect();
|
|
1395
|
+
this.dbEnabled = true;
|
|
1396
|
+
const dbChanges = await this.prisma.fileChange.findMany({
|
|
1397
|
+
where: { sessionId, undone: false },
|
|
1398
|
+
orderBy: { createdAt: "desc" },
|
|
1399
|
+
take: 50
|
|
1400
|
+
});
|
|
1401
|
+
this.changes = dbChanges.map((c) => ({
|
|
1402
|
+
id: c.id,
|
|
1403
|
+
filePath: c.filePath,
|
|
1404
|
+
operation: c.operation,
|
|
1405
|
+
beforeContent: c.beforeContent,
|
|
1406
|
+
afterContent: c.afterContent,
|
|
1407
|
+
timestamp: c.createdAt,
|
|
1408
|
+
undone: c.undone
|
|
1409
|
+
}));
|
|
1410
|
+
} catch {
|
|
1411
|
+
this.dbEnabled = false;
|
|
1412
|
+
this.changes = [];
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
async recordChange(filePath, operation, beforeContent, afterContent) {
|
|
1416
|
+
const id = `chg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1417
|
+
const change = {
|
|
1418
|
+
id,
|
|
1419
|
+
filePath: path7.relative(process.cwd(), path7.resolve(filePath)),
|
|
1420
|
+
operation,
|
|
1421
|
+
beforeContent,
|
|
1422
|
+
afterContent,
|
|
1423
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1424
|
+
undone: false
|
|
1425
|
+
};
|
|
1426
|
+
this.changes.unshift(change);
|
|
1427
|
+
if (this.dbEnabled && this.prisma && this.sessionId) {
|
|
1428
|
+
try {
|
|
1429
|
+
await this.prisma.fileChange.create({
|
|
1430
|
+
data: {
|
|
1431
|
+
id,
|
|
1432
|
+
sessionId: this.sessionId,
|
|
1433
|
+
filePath: change.filePath,
|
|
1434
|
+
operation,
|
|
1435
|
+
beforeContent,
|
|
1436
|
+
afterContent
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
} catch {
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (this.changes.length > 100) {
|
|
1443
|
+
this.changes = this.changes.slice(0, 100);
|
|
1444
|
+
}
|
|
1445
|
+
return id;
|
|
1446
|
+
}
|
|
1447
|
+
async undo(count = 1) {
|
|
1448
|
+
const toUndo = this.changes.filter((c) => !c.undone).slice(0, count);
|
|
1449
|
+
if (toUndo.length === 0) {
|
|
1450
|
+
return { success: false, message: "Nothing to undo" };
|
|
1451
|
+
}
|
|
1452
|
+
const results = [];
|
|
1453
|
+
for (const change of toUndo) {
|
|
1454
|
+
try {
|
|
1455
|
+
const fullPath = path7.resolve(process.cwd(), change.filePath);
|
|
1456
|
+
switch (change.operation) {
|
|
1457
|
+
case "create":
|
|
1458
|
+
await fs7.unlink(fullPath);
|
|
1459
|
+
results.push(`Deleted: ${change.filePath}`);
|
|
1460
|
+
break;
|
|
1461
|
+
case "edit":
|
|
1462
|
+
if (change.beforeContent !== null) {
|
|
1463
|
+
await fs7.writeFile(fullPath, change.beforeContent, "utf-8");
|
|
1464
|
+
results.push(`Restored: ${change.filePath}`);
|
|
1465
|
+
}
|
|
1466
|
+
break;
|
|
1467
|
+
case "delete":
|
|
1468
|
+
if (change.beforeContent !== null) {
|
|
1469
|
+
await fs7.mkdir(path7.dirname(fullPath), { recursive: true });
|
|
1470
|
+
await fs7.writeFile(fullPath, change.beforeContent, "utf-8");
|
|
1471
|
+
results.push(`Restored: ${change.filePath}`);
|
|
1472
|
+
}
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
change.undone = true;
|
|
1476
|
+
if (this.dbEnabled && this.prisma) {
|
|
1477
|
+
await this.prisma.fileChange.update({
|
|
1478
|
+
where: { id: change.id },
|
|
1479
|
+
data: { undone: true }
|
|
1480
|
+
}).catch(() => {
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
results.push(`Failed: ${change.filePath} - ${error.message}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
success: true,
|
|
1489
|
+
message: results.join("\n")
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
getHistory(limit = 10) {
|
|
1493
|
+
return this.changes.filter((c) => !c.undone).slice(0, limit);
|
|
1494
|
+
}
|
|
1495
|
+
async close() {
|
|
1496
|
+
if (this.prisma) {
|
|
1497
|
+
await this.prisma.$disconnect();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
var undo = new UndoManager();
|
|
1502
|
+
|
|
1503
|
+
// src/core/tools.ts
|
|
1504
|
+
var execAsync2 = promisify2(exec2);
|
|
1505
|
+
var MAX_OUTPUT_LENGTH = 1e4;
|
|
1506
|
+
var MAX_FILE_READ_LINES = 500;
|
|
1507
|
+
var IGNORED_DIRS = ["node_modules", ".git", "dist", ".next", "__pycache__", ".cache", "coverage"];
|
|
1508
|
+
var APPROVAL_TIMEOUT = 3e4;
|
|
1509
|
+
function truncateOutput(output, maxLength = MAX_OUTPUT_LENGTH) {
|
|
1510
|
+
if (output.length <= maxLength) return output;
|
|
1511
|
+
const truncated = output.slice(0, maxLength);
|
|
1512
|
+
const remaining = output.length - maxLength;
|
|
1513
|
+
return `${truncated}
|
|
1514
|
+
|
|
1515
|
+
... [TRUNCATED: ${remaining} more characters]`;
|
|
1516
|
+
}
|
|
1517
|
+
var pendingApprovals = /* @__PURE__ */ new Map();
|
|
1518
|
+
bus.on("user", (event) => {
|
|
1519
|
+
if (event.type === "approval_response") {
|
|
1520
|
+
const pending = pendingApprovals.get(event.requestId);
|
|
1521
|
+
if (pending) {
|
|
1522
|
+
clearTimeout(pending.timeout);
|
|
1523
|
+
pendingApprovals.delete(event.requestId);
|
|
1524
|
+
pending.resolve(event.approved);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
async function requestApproval(command, reason) {
|
|
1529
|
+
const requestId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1530
|
+
return new Promise((resolve) => {
|
|
1531
|
+
const timeout = setTimeout(() => {
|
|
1532
|
+
pendingApprovals.delete(requestId);
|
|
1533
|
+
bus.emitAgent({
|
|
1534
|
+
type: "error",
|
|
1535
|
+
message: "Approval request timed out. Command denied."
|
|
1536
|
+
});
|
|
1537
|
+
resolve(false);
|
|
1538
|
+
}, APPROVAL_TIMEOUT);
|
|
1539
|
+
pendingApprovals.set(requestId, { resolve, timeout });
|
|
1540
|
+
bus.emitAgent({
|
|
1541
|
+
type: "approval_request",
|
|
1542
|
+
requestId,
|
|
1543
|
+
context: `Command: ${command}
|
|
1544
|
+
Reason: ${reason}`
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
var BashTool = {
|
|
1549
|
+
name: "bash",
|
|
1550
|
+
description: "Execute shell commands in the workspace",
|
|
1551
|
+
async execute(args) {
|
|
1552
|
+
const command = args.command;
|
|
1553
|
+
if (!command) {
|
|
1554
|
+
return { success: false, error: "No command provided" };
|
|
1555
|
+
}
|
|
1556
|
+
const audit = await auditor.checkCommand(command);
|
|
1557
|
+
if (!audit.approved) {
|
|
1558
|
+
return {
|
|
1559
|
+
success: false,
|
|
1560
|
+
error: `Security violation: ${audit.reason}`
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
if (audit.requiresApproval && !audit.autoApproved) {
|
|
1564
|
+
const approved = await requestApproval(command, audit.reason || "Potentially dangerous operation");
|
|
1565
|
+
if (!approved) {
|
|
1566
|
+
await settings.addDeniedPermission("bash", command);
|
|
1567
|
+
return {
|
|
1568
|
+
success: false,
|
|
1569
|
+
error: "Command rejected by user"
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
await settings.addAllowedPermission("bash", command);
|
|
1573
|
+
}
|
|
1574
|
+
try {
|
|
1575
|
+
const execCommand = await sandbox.wrapCommand(command);
|
|
1576
|
+
const { stdout, stderr } = await execAsync2(execCommand, {
|
|
1577
|
+
cwd: process.cwd(),
|
|
1578
|
+
timeout: 3e4,
|
|
1579
|
+
// 30 second timeout
|
|
1580
|
+
maxBuffer: 1024 * 1024
|
|
1581
|
+
// 1MB buffer (reduced from 10MB)
|
|
1582
|
+
});
|
|
1583
|
+
const output = stdout || stderr || "Command executed successfully";
|
|
1584
|
+
return {
|
|
1585
|
+
success: true,
|
|
1586
|
+
output: truncateOutput(output)
|
|
1587
|
+
};
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
return {
|
|
1590
|
+
success: false,
|
|
1591
|
+
error: error.message || "Command execution failed"
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
var ReadTool = {
|
|
1597
|
+
name: "read",
|
|
1598
|
+
description: "Read file contents from the workspace",
|
|
1599
|
+
async execute(args) {
|
|
1600
|
+
const filePath = args.path;
|
|
1601
|
+
if (!filePath) {
|
|
1602
|
+
return { success: false, error: "No file path provided" };
|
|
1603
|
+
}
|
|
1604
|
+
const pathCheck = auditor.checkPath(filePath);
|
|
1605
|
+
if (!pathCheck.approved) {
|
|
1606
|
+
return {
|
|
1607
|
+
success: false,
|
|
1608
|
+
error: pathCheck.reason
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
if (IGNORED_DIRS.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`))) {
|
|
1612
|
+
return {
|
|
1613
|
+
success: false,
|
|
1614
|
+
error: `Cannot read from ignored directory. Paths containing ${IGNORED_DIRS.join(", ")} are blocked to prevent context explosion.`
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
const fullPath = path8.resolve(process.cwd(), filePath);
|
|
1619
|
+
const content = await fs8.readFile(fullPath, "utf-8");
|
|
1620
|
+
const lines = content.split("\n");
|
|
1621
|
+
const limitedLines = lines.slice(0, MAX_FILE_READ_LINES);
|
|
1622
|
+
const numbered = limitedLines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
|
|
1623
|
+
const truncationNote = lines.length > MAX_FILE_READ_LINES ? `
|
|
1624
|
+
|
|
1625
|
+
... [TRUNCATED: ${lines.length - MAX_FILE_READ_LINES} more lines. Use offset parameter to read more.]` : "";
|
|
1626
|
+
await context.trackRead(filePath);
|
|
1627
|
+
return {
|
|
1628
|
+
success: true,
|
|
1629
|
+
output: truncateOutput(`File: ${filePath} (${lines.length} lines)
|
|
1630
|
+
${"=".repeat(60)}
|
|
1631
|
+
${numbered}${truncationNote}`)
|
|
1632
|
+
};
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
return {
|
|
1635
|
+
success: false,
|
|
1636
|
+
error: `Failed to read file: ${error.message}`
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
var WriteTool = {
|
|
1642
|
+
name: "write",
|
|
1643
|
+
description: "Create new files in the workspace",
|
|
1644
|
+
async execute(args) {
|
|
1645
|
+
const filePath = args.path;
|
|
1646
|
+
const content = args.content;
|
|
1647
|
+
if (!filePath) {
|
|
1648
|
+
return { success: false, error: "No file path provided" };
|
|
1649
|
+
}
|
|
1650
|
+
if (content === void 0) {
|
|
1651
|
+
return { success: false, error: "No content provided" };
|
|
1652
|
+
}
|
|
1653
|
+
const pathCheck = auditor.checkPath(filePath);
|
|
1654
|
+
if (!pathCheck.approved) {
|
|
1655
|
+
return {
|
|
1656
|
+
success: false,
|
|
1657
|
+
error: pathCheck.reason
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
try {
|
|
1661
|
+
const fullPath = path8.resolve(process.cwd(), filePath);
|
|
1662
|
+
try {
|
|
1663
|
+
await fs8.access(fullPath);
|
|
1664
|
+
return {
|
|
1665
|
+
success: false,
|
|
1666
|
+
error: `File already exists: ${filePath}. Use 'edit' tool to modify.`
|
|
1667
|
+
};
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
await fs8.mkdir(path8.dirname(fullPath), { recursive: true });
|
|
1671
|
+
await fs8.writeFile(fullPath, content, "utf-8");
|
|
1672
|
+
await context.trackModified(filePath);
|
|
1673
|
+
await undo.recordChange(filePath, "create", null, content);
|
|
1674
|
+
return {
|
|
1675
|
+
success: true,
|
|
1676
|
+
output: `Created file: ${filePath} (${content.length} bytes)`
|
|
1677
|
+
};
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
return {
|
|
1680
|
+
success: false,
|
|
1681
|
+
error: `Failed to write file: ${error.message}`
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
var EditTool = {
|
|
1687
|
+
name: "edit",
|
|
1688
|
+
description: "Edit existing files using search and replace",
|
|
1689
|
+
async execute(args) {
|
|
1690
|
+
const filePath = args.path;
|
|
1691
|
+
const search = args.search;
|
|
1692
|
+
const replace = args.replace;
|
|
1693
|
+
if (!filePath) {
|
|
1694
|
+
return { success: false, error: "No file path provided" };
|
|
1695
|
+
}
|
|
1696
|
+
if (!search) {
|
|
1697
|
+
return { success: false, error: "No search string provided" };
|
|
1698
|
+
}
|
|
1699
|
+
if (replace === void 0) {
|
|
1700
|
+
return { success: false, error: "No replacement string provided" };
|
|
1701
|
+
}
|
|
1702
|
+
const fileCheck = await auditor.checkFileEdit(filePath);
|
|
1703
|
+
if (!fileCheck.approved) {
|
|
1704
|
+
return {
|
|
1705
|
+
success: false,
|
|
1706
|
+
error: fileCheck.reason
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
const fullPath = path8.resolve(process.cwd(), filePath);
|
|
1711
|
+
const original = await fs8.readFile(fullPath, "utf-8");
|
|
1712
|
+
if (!original.includes(search)) {
|
|
1713
|
+
return {
|
|
1714
|
+
success: false,
|
|
1715
|
+
error: `Search string not found in ${filePath}`
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
const modified = original.replace(search, replace);
|
|
1719
|
+
const diffPreview = generateDiffPreview(search, replace);
|
|
1720
|
+
await fs8.writeFile(fullPath, modified, "utf-8");
|
|
1721
|
+
const originalLines = original.split("\n").length;
|
|
1722
|
+
const modifiedLines = modified.split("\n").length;
|
|
1723
|
+
const delta = modifiedLines - originalLines;
|
|
1724
|
+
await context.trackModified(filePath);
|
|
1725
|
+
await undo.recordChange(filePath, "edit", original, modified);
|
|
1726
|
+
return {
|
|
1727
|
+
success: true,
|
|
1728
|
+
output: `Edited ${filePath}:
|
|
1729
|
+
${diffPreview}
|
|
1730
|
+
Lines: ${originalLines} -> ${modifiedLines} (${delta >= 0 ? "+" : ""}${delta})`
|
|
1731
|
+
};
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
return {
|
|
1734
|
+
success: false,
|
|
1735
|
+
error: `Failed to edit file: ${error.message}`
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
function generateDiffPreview(search, replace) {
|
|
1741
|
+
const searchLines = search.split("\n").slice(0, 5);
|
|
1742
|
+
const replaceLines = replace.split("\n").slice(0, 5);
|
|
1743
|
+
let preview = "";
|
|
1744
|
+
for (const line of searchLines) {
|
|
1745
|
+
preview += `- ${line}
|
|
1746
|
+
`;
|
|
1747
|
+
}
|
|
1748
|
+
if (search.split("\n").length > 5) {
|
|
1749
|
+
preview += `- ... (${search.split("\n").length - 5} more lines)
|
|
1750
|
+
`;
|
|
1751
|
+
}
|
|
1752
|
+
for (const line of replaceLines) {
|
|
1753
|
+
preview += `+ ${line}
|
|
1754
|
+
`;
|
|
1755
|
+
}
|
|
1756
|
+
if (replace.split("\n").length > 5) {
|
|
1757
|
+
preview += `+ ... (${replace.split("\n").length - 5} more lines)
|
|
1758
|
+
`;
|
|
1759
|
+
}
|
|
1760
|
+
return preview.trim();
|
|
1761
|
+
}
|
|
1762
|
+
var ListTool = {
|
|
1763
|
+
name: "list",
|
|
1764
|
+
description: "List files and directories in the workspace",
|
|
1765
|
+
async execute(args) {
|
|
1766
|
+
const dirPath = args.path || ".";
|
|
1767
|
+
const pathCheck = auditor.checkPath(dirPath);
|
|
1768
|
+
if (!pathCheck.approved) {
|
|
1769
|
+
return {
|
|
1770
|
+
success: false,
|
|
1771
|
+
error: pathCheck.reason
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
const fullPath = path8.resolve(process.cwd(), dirPath);
|
|
1776
|
+
const entries = await fs8.readdir(fullPath, { withFileTypes: true });
|
|
1777
|
+
const filtered = entries.filter(
|
|
1778
|
+
(entry) => !IGNORED_DIRS.includes(entry.name) && !entry.name.startsWith(".")
|
|
1779
|
+
);
|
|
1780
|
+
const formatted = filtered.map((entry) => {
|
|
1781
|
+
const prefix = entry.isDirectory() ? "[DIR]" : "[FILE]";
|
|
1782
|
+
return `${prefix} ${entry.name}`;
|
|
1783
|
+
}).join("\n");
|
|
1784
|
+
const hiddenCount = entries.length - filtered.length;
|
|
1785
|
+
const hiddenNote = hiddenCount > 0 ? `
|
|
1786
|
+
|
|
1787
|
+
(${hiddenCount} hidden: node_modules, .git, etc.)` : "";
|
|
1788
|
+
return {
|
|
1789
|
+
success: true,
|
|
1790
|
+
output: `Directory: ${dirPath}
|
|
1791
|
+
${"=".repeat(60)}
|
|
1792
|
+
${formatted}${hiddenNote}`
|
|
1793
|
+
};
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
return {
|
|
1796
|
+
success: false,
|
|
1797
|
+
error: `Failed to list directory: ${error.message}`
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
var GrepTool = {
|
|
1803
|
+
name: "grep",
|
|
1804
|
+
description: "Search for patterns in files using regex",
|
|
1805
|
+
async execute(args) {
|
|
1806
|
+
const pattern = args.pattern;
|
|
1807
|
+
const searchPath = args.path || ".";
|
|
1808
|
+
const maxResults = args.limit || 50;
|
|
1809
|
+
if (!pattern) {
|
|
1810
|
+
return { success: false, error: "No search pattern provided" };
|
|
1811
|
+
}
|
|
1812
|
+
const pathCheck = auditor.checkPath(searchPath);
|
|
1813
|
+
if (!pathCheck.approved) {
|
|
1814
|
+
return {
|
|
1815
|
+
success: false,
|
|
1816
|
+
error: pathCheck.reason
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
const fullPath = path8.resolve(process.cwd(), searchPath);
|
|
1821
|
+
const results = [];
|
|
1822
|
+
await searchDirectory(fullPath, pattern, results, maxResults);
|
|
1823
|
+
if (results.length === 0) {
|
|
1824
|
+
return {
|
|
1825
|
+
success: true,
|
|
1826
|
+
output: `No matches found for: ${pattern}`
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
return {
|
|
1830
|
+
success: true,
|
|
1831
|
+
output: truncateOutput(`Found ${results.length} matches for "${pattern}":
|
|
1832
|
+
${"=".repeat(60)}
|
|
1833
|
+
${results.join("\n")}`)
|
|
1834
|
+
};
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
return {
|
|
1837
|
+
success: false,
|
|
1838
|
+
error: `Search failed: ${error.message}`
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
async function searchDirectory(dir, pattern, results, maxResults, depth = 0) {
|
|
1844
|
+
if (results.length >= maxResults || depth > 10) return;
|
|
1845
|
+
try {
|
|
1846
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1847
|
+
const regex = new RegExp(pattern, "gi");
|
|
1848
|
+
for (const entry of entries) {
|
|
1849
|
+
if (results.length >= maxResults) break;
|
|
1850
|
+
const fullPath = path8.join(dir, entry.name);
|
|
1851
|
+
const relativePath = path8.relative(process.cwd(), fullPath);
|
|
1852
|
+
if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
if (entry.isDirectory()) {
|
|
1856
|
+
await searchDirectory(fullPath, pattern, results, maxResults, depth + 1);
|
|
1857
|
+
} else if (entry.isFile()) {
|
|
1858
|
+
const ext = path8.extname(entry.name).toLowerCase();
|
|
1859
|
+
const textExtensions = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt", ".yaml", ".yml", ".css", ".html", ".sh"];
|
|
1860
|
+
if (textExtensions.includes(ext) || ext === "") {
|
|
1861
|
+
try {
|
|
1862
|
+
const content = await fs8.readFile(fullPath, "utf-8");
|
|
1863
|
+
const lines = content.split("\n");
|
|
1864
|
+
for (let i = 0; i < lines.length && results.length < maxResults; i++) {
|
|
1865
|
+
if (regex.test(lines[i])) {
|
|
1866
|
+
results.push(`${relativePath}:${i + 1}: ${lines[i].trim().slice(0, 100)}`);
|
|
1867
|
+
}
|
|
1868
|
+
regex.lastIndex = 0;
|
|
1869
|
+
}
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
} catch {
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
var GlobTool = {
|
|
1879
|
+
name: "glob",
|
|
1880
|
+
description: "Find files matching a glob pattern (e.g., **/*.ts, src/**/*.tsx)",
|
|
1881
|
+
async execute(args) {
|
|
1882
|
+
const pattern = args.pattern;
|
|
1883
|
+
const basePath = args.path || ".";
|
|
1884
|
+
if (!pattern) {
|
|
1885
|
+
return { success: false, error: "No pattern provided" };
|
|
1886
|
+
}
|
|
1887
|
+
try {
|
|
1888
|
+
const results = [];
|
|
1889
|
+
const fullBase = path8.resolve(process.cwd(), basePath);
|
|
1890
|
+
await globSearch(fullBase, pattern, results, 100);
|
|
1891
|
+
if (results.length === 0) {
|
|
1892
|
+
return {
|
|
1893
|
+
success: true,
|
|
1894
|
+
output: `No files matching: ${pattern}`
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
return {
|
|
1898
|
+
success: true,
|
|
1899
|
+
output: truncateOutput(`Found ${results.length} files:
|
|
1900
|
+
${results.join("\n")}`)
|
|
1901
|
+
};
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
return {
|
|
1904
|
+
success: false,
|
|
1905
|
+
error: `Glob failed: ${error.message}`
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
async function globSearch(dir, pattern, results, maxResults, depth = 0) {
|
|
1911
|
+
if (results.length >= maxResults || depth > 15) return;
|
|
1912
|
+
try {
|
|
1913
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1914
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/{{GLOBSTAR}}/g, ".*");
|
|
1915
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
1916
|
+
for (const entry of entries) {
|
|
1917
|
+
if (results.length >= maxResults) break;
|
|
1918
|
+
if (entry.name.startsWith(".") || IGNORED_DIRS.includes(entry.name)) {
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
const fullPath = path8.join(dir, entry.name);
|
|
1922
|
+
const relativePath = path8.relative(process.cwd(), fullPath);
|
|
1923
|
+
if (entry.isDirectory()) {
|
|
1924
|
+
if (pattern.includes("**") || pattern.includes("/")) {
|
|
1925
|
+
await globSearch(fullPath, pattern, results, maxResults, depth + 1);
|
|
1926
|
+
}
|
|
1927
|
+
} else if (entry.isFile()) {
|
|
1928
|
+
if (regex.test(relativePath) || regex.test(entry.name)) {
|
|
1929
|
+
results.push(relativePath);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
var WebFetchTool = {
|
|
1937
|
+
name: "web_fetch",
|
|
1938
|
+
description: "Fetch content from a URL (for documentation, APIs, etc.)",
|
|
1939
|
+
async execute(args) {
|
|
1940
|
+
const url = args.url;
|
|
1941
|
+
if (!url) {
|
|
1942
|
+
return { success: false, error: "No URL provided" };
|
|
1943
|
+
}
|
|
1944
|
+
try {
|
|
1945
|
+
new URL(url);
|
|
1946
|
+
} catch {
|
|
1947
|
+
return { success: false, error: "Invalid URL format" };
|
|
1948
|
+
}
|
|
1949
|
+
const blockedDomains = ["localhost", "127.0.0.1", "0.0.0.0", "169.254"];
|
|
1950
|
+
const urlObj = new URL(url);
|
|
1951
|
+
if (blockedDomains.some((d) => urlObj.hostname.includes(d))) {
|
|
1952
|
+
return {
|
|
1953
|
+
success: false,
|
|
1954
|
+
error: "Cannot fetch from local/private addresses"
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
try {
|
|
1958
|
+
const controller = new AbortController();
|
|
1959
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
1960
|
+
const response = await fetch(url, {
|
|
1961
|
+
signal: controller.signal,
|
|
1962
|
+
headers: {
|
|
1963
|
+
"User-Agent": "Obsidian-Next/1.0 (AI Agent CLI)",
|
|
1964
|
+
"Accept": "text/html,application/json,text/plain,*/*"
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1967
|
+
clearTimeout(timeoutId);
|
|
1968
|
+
if (!response.ok) {
|
|
1969
|
+
return {
|
|
1970
|
+
success: false,
|
|
1971
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1975
|
+
let content = await response.text();
|
|
1976
|
+
if (content.length > MAX_OUTPUT_LENGTH) {
|
|
1977
|
+
content = content.slice(0, MAX_OUTPUT_LENGTH) + `
|
|
1978
|
+
|
|
1979
|
+
... [TRUNCATED: ${content.length - MAX_OUTPUT_LENGTH} more characters]`;
|
|
1980
|
+
}
|
|
1981
|
+
if (contentType.includes("text/html")) {
|
|
1982
|
+
content = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
1983
|
+
}
|
|
1984
|
+
return {
|
|
1985
|
+
success: true,
|
|
1986
|
+
output: truncateOutput(`URL: ${url}
|
|
1987
|
+
Content-Type: ${contentType}
|
|
1988
|
+
${"=".repeat(60)}
|
|
1989
|
+
${content}`)
|
|
1990
|
+
};
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
if (error.name === "AbortError") {
|
|
1993
|
+
return { success: false, error: "Request timed out after 10 seconds" };
|
|
1994
|
+
}
|
|
1995
|
+
return {
|
|
1996
|
+
success: false,
|
|
1997
|
+
error: `Fetch failed: ${error.message}`
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
var ToolRegistry = class {
|
|
2003
|
+
tools = /* @__PURE__ */ new Map();
|
|
2004
|
+
constructor() {
|
|
2005
|
+
this.register(BashTool);
|
|
2006
|
+
this.register(ReadTool);
|
|
2007
|
+
this.register(WriteTool);
|
|
2008
|
+
this.register(EditTool);
|
|
2009
|
+
this.register(ListTool);
|
|
2010
|
+
this.register(GrepTool);
|
|
2011
|
+
this.register(GlobTool);
|
|
2012
|
+
this.register(WebFetchTool);
|
|
2013
|
+
}
|
|
2014
|
+
register(tool) {
|
|
2015
|
+
this.tools.set(tool.name, tool);
|
|
2016
|
+
}
|
|
2017
|
+
has(name) {
|
|
2018
|
+
return this.tools.has(name);
|
|
2019
|
+
}
|
|
2020
|
+
get(name) {
|
|
2021
|
+
return this.tools.get(name);
|
|
2022
|
+
}
|
|
2023
|
+
list() {
|
|
2024
|
+
return Array.from(this.tools.values());
|
|
2025
|
+
}
|
|
2026
|
+
async execute(name, args) {
|
|
2027
|
+
const tool = this.tools.get(name);
|
|
2028
|
+
if (!tool) {
|
|
2029
|
+
return {
|
|
2030
|
+
success: false,
|
|
2031
|
+
error: `Unknown tool: ${name}`
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
bus.emitAgent({
|
|
2035
|
+
type: "tool_start",
|
|
2036
|
+
tool: name,
|
|
2037
|
+
args: JSON.stringify(args, null, 2)
|
|
2038
|
+
});
|
|
2039
|
+
const result = await tool.execute(args);
|
|
2040
|
+
bus.emitAgent({
|
|
2041
|
+
type: "tool_result",
|
|
2042
|
+
tool: name,
|
|
2043
|
+
output: result.success ? result.output || "Success" : result.error || "Failed",
|
|
2044
|
+
isError: !result.success
|
|
2045
|
+
});
|
|
2046
|
+
return result;
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
var tools = new ToolRegistry();
|
|
2050
|
+
|
|
2051
|
+
// src/core/llm.ts
|
|
2052
|
+
var MAX_TOOL_ITERATIONS = 10;
|
|
2053
|
+
var LLMClient = class {
|
|
2054
|
+
client = null;
|
|
2055
|
+
lastConfig = null;
|
|
2056
|
+
conversationHistory = [];
|
|
2057
|
+
toolIterations = 0;
|
|
2058
|
+
accumulatedInputTokens = 0;
|
|
2059
|
+
accumulatedOutputTokens = 0;
|
|
2060
|
+
async initialize() {
|
|
2061
|
+
const cfg = await config.load();
|
|
2062
|
+
await usage.init();
|
|
2063
|
+
if (!cfg.apiKey) {
|
|
2064
|
+
bus.emitAgent({
|
|
2065
|
+
type: "error",
|
|
2066
|
+
message: "Missing ANTHROPIC_API_KEY. Please set it in .env or via /init."
|
|
2067
|
+
});
|
|
2068
|
+
return false;
|
|
2069
|
+
}
|
|
2070
|
+
this.client = new Anthropic({
|
|
2071
|
+
apiKey: cfg.apiKey
|
|
2072
|
+
});
|
|
2073
|
+
this.lastConfig = cfg;
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
async streamChat(userMessage) {
|
|
2077
|
+
if (!this.client) {
|
|
2078
|
+
const initialized = await this.initialize();
|
|
2079
|
+
if (!initialized || !this.client) return null;
|
|
2080
|
+
}
|
|
2081
|
+
try {
|
|
2082
|
+
const modelMap = {
|
|
2083
|
+
// New 4.5 Aliases
|
|
2084
|
+
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
|
|
2085
|
+
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
|
2086
|
+
"claude-opus-4-5": "claude-opus-4-5-20251101",
|
|
2087
|
+
"ollama": "llama3"
|
|
2088
|
+
};
|
|
2089
|
+
const requestedModel = this.lastConfig?.model || "claude-sonnet-4-5-20250929";
|
|
2090
|
+
let apiModel = modelMap[requestedModel] || requestedModel;
|
|
2091
|
+
if (userMessage.trim()) {
|
|
2092
|
+
this.toolIterations = 0;
|
|
2093
|
+
this.accumulatedInputTokens = 0;
|
|
2094
|
+
this.accumulatedOutputTokens = 0;
|
|
2095
|
+
this.conversationHistory.push({
|
|
2096
|
+
role: "user",
|
|
2097
|
+
content: userMessage
|
|
2098
|
+
});
|
|
2099
|
+
} else {
|
|
2100
|
+
this.toolIterations++;
|
|
2101
|
+
if (this.toolIterations > MAX_TOOL_ITERATIONS) {
|
|
2102
|
+
bus.emitAgent({
|
|
2103
|
+
type: "error",
|
|
2104
|
+
message: `Tool iteration limit (${MAX_TOOL_ITERATIONS}) exceeded. Stopping to prevent infinite loop.`
|
|
2105
|
+
});
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const toolDefinitions = tools.list().map((tool) => ({
|
|
2110
|
+
name: tool.name,
|
|
2111
|
+
description: tool.description,
|
|
2112
|
+
input_schema: {
|
|
2113
|
+
type: "object",
|
|
2114
|
+
properties: this.getToolSchema(tool.name),
|
|
2115
|
+
required: this.getRequiredParams(tool.name)
|
|
2116
|
+
}
|
|
2117
|
+
}));
|
|
2118
|
+
const toolList = tools.list().map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
2119
|
+
const systemPrompt = `You are a CLI coding agent. You have tools. Use them.
|
|
2120
|
+
|
|
2121
|
+
Available: ${tools.list().map((t) => t.name).join(", ")}
|
|
2122
|
+
|
|
2123
|
+
- Dont explain. Just do it.
|
|
2124
|
+
- No markdown. No ** or \` or #. Plain text.
|
|
2125
|
+
- Read files before editing.
|
|
2126
|
+
- Grep to find code. Read to understand. Edit to change.
|
|
2127
|
+
- One thought, then act. No preamble.
|
|
2128
|
+
- If asked to do something, do it. Dont ask for confirmation.
|
|
2129
|
+
- Errors: fix them, dont apologize.
|
|
2130
|
+
- Never read node_modules or .git.
|
|
2131
|
+
|
|
2132
|
+
cwd: ${process.cwd()}`;
|
|
2133
|
+
const createMessage = async (model) => {
|
|
2134
|
+
return await this.client.messages.create({
|
|
2135
|
+
model,
|
|
2136
|
+
max_tokens: this.lastConfig?.maxTokens || 8192,
|
|
2137
|
+
system: systemPrompt,
|
|
2138
|
+
messages: [...this.conversationHistory],
|
|
2139
|
+
tools: toolDefinitions,
|
|
2140
|
+
stream: true
|
|
2141
|
+
});
|
|
2142
|
+
};
|
|
2143
|
+
let stream;
|
|
2144
|
+
let currentModel = apiModel;
|
|
2145
|
+
let inputTokens = 0;
|
|
2146
|
+
let outputTokens = 0;
|
|
2147
|
+
try {
|
|
2148
|
+
stream = await createMessage(apiModel);
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
const isNotFound = error.status === 404 || error.message && error.message.includes("not_found_error") || error.error && error.error.type === "not_found_error";
|
|
2151
|
+
if (isNotFound) {
|
|
2152
|
+
bus.emitAgent({
|
|
2153
|
+
type: "error",
|
|
2154
|
+
message: `Model ${apiModel} not available. Falling back to claude-haiku-4-5.`
|
|
2155
|
+
});
|
|
2156
|
+
currentModel = "claude-haiku-4-5-20251001";
|
|
2157
|
+
stream = await createMessage(currentModel);
|
|
2158
|
+
} else {
|
|
2159
|
+
throw error;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
let fullResponse = "";
|
|
2163
|
+
let buffer = "";
|
|
2164
|
+
let toolUses = [];
|
|
2165
|
+
let currentToolUse = null;
|
|
2166
|
+
for await (const chunk of stream) {
|
|
2167
|
+
if (chunk.type === "message_start" && chunk.message && chunk.message.usage) {
|
|
2168
|
+
inputTokens += chunk.message.usage.input_tokens || 0;
|
|
2169
|
+
this.accumulatedInputTokens += chunk.message.usage.input_tokens || 0;
|
|
2170
|
+
}
|
|
2171
|
+
if (chunk.type === "message_delta" && chunk.usage) {
|
|
2172
|
+
outputTokens += chunk.usage.output_tokens || 0;
|
|
2173
|
+
this.accumulatedOutputTokens += chunk.usage.output_tokens || 0;
|
|
2174
|
+
}
|
|
2175
|
+
if (chunk.type === "content_block_start" && chunk.content_block.type === "tool_use") {
|
|
2176
|
+
currentToolUse = {
|
|
2177
|
+
id: chunk.content_block.id,
|
|
2178
|
+
name: chunk.content_block.name,
|
|
2179
|
+
input: ""
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
if (chunk.type === "content_block_delta" && chunk.delta.type === "input_json_delta") {
|
|
2183
|
+
if (currentToolUse) {
|
|
2184
|
+
currentToolUse.input += chunk.delta.partial_json;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
if (chunk.type === "content_block_stop" && currentToolUse) {
|
|
2188
|
+
try {
|
|
2189
|
+
currentToolUse.input = JSON.parse(currentToolUse.input);
|
|
2190
|
+
toolUses.push(currentToolUse);
|
|
2191
|
+
currentToolUse = null;
|
|
2192
|
+
} catch (e) {
|
|
2193
|
+
bus.emitAgent({
|
|
2194
|
+
type: "error",
|
|
2195
|
+
message: `Failed to parse tool input: ${e}`
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
|
2200
|
+
const text = chunk.delta.text;
|
|
2201
|
+
fullResponse += text;
|
|
2202
|
+
buffer += text;
|
|
2203
|
+
const shouldEmit = buffer.length >= 50 || buffer.match(/[.!?]\s*$/) || buffer.match(/\n/);
|
|
2204
|
+
if (shouldEmit) {
|
|
2205
|
+
bus.emitAgent({
|
|
2206
|
+
type: "thought",
|
|
2207
|
+
content: fullResponse
|
|
2208
|
+
});
|
|
2209
|
+
buffer = "";
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
if (buffer.length > 0) {
|
|
2214
|
+
bus.emitAgent({
|
|
2215
|
+
type: "thought",
|
|
2216
|
+
content: fullResponse
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
if (toolUses.length > 0) {
|
|
2220
|
+
const toolResults = [];
|
|
2221
|
+
for (const toolUse of toolUses) {
|
|
2222
|
+
const result = await tools.execute(toolUse.name, toolUse.input);
|
|
2223
|
+
toolResults.push({
|
|
2224
|
+
type: "tool_result",
|
|
2225
|
+
tool_use_id: toolUse.id,
|
|
2226
|
+
content: result.success ? result.output || "Success" : result.error || "Failed",
|
|
2227
|
+
is_error: !result.success
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
this.conversationHistory.push({
|
|
2231
|
+
role: "assistant",
|
|
2232
|
+
content: [
|
|
2233
|
+
...fullResponse ? [{ type: "text", text: fullResponse }] : [],
|
|
2234
|
+
...toolUses.map((tu) => ({
|
|
2235
|
+
type: "tool_use",
|
|
2236
|
+
id: tu.id,
|
|
2237
|
+
name: tu.name,
|
|
2238
|
+
input: tu.input
|
|
2239
|
+
}))
|
|
2240
|
+
]
|
|
2241
|
+
});
|
|
2242
|
+
this.conversationHistory.push({
|
|
2243
|
+
role: "user",
|
|
2244
|
+
content: toolResults
|
|
2245
|
+
});
|
|
2246
|
+
return await this.streamChat("");
|
|
2247
|
+
}
|
|
2248
|
+
if (fullResponse) {
|
|
2249
|
+
this.conversationHistory.push({
|
|
2250
|
+
role: "assistant",
|
|
2251
|
+
content: fullResponse
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
await usage.track(currentModel, this.accumulatedInputTokens, this.accumulatedOutputTokens);
|
|
2255
|
+
return fullResponse;
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
bus.emitAgent({
|
|
2258
|
+
type: "error",
|
|
2259
|
+
message: `LLM Error: ${error.message}`
|
|
2260
|
+
});
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
getToolSchema(toolName) {
|
|
2265
|
+
const schemas = {
|
|
2266
|
+
bash: {
|
|
2267
|
+
command: {
|
|
2268
|
+
type: "string",
|
|
2269
|
+
description: "The shell command to execute"
|
|
2270
|
+
}
|
|
2271
|
+
},
|
|
2272
|
+
read: {
|
|
2273
|
+
path: {
|
|
2274
|
+
type: "string",
|
|
2275
|
+
description: "Path to the file to read (relative to workspace)"
|
|
2276
|
+
}
|
|
2277
|
+
},
|
|
2278
|
+
write: {
|
|
2279
|
+
path: {
|
|
2280
|
+
type: "string",
|
|
2281
|
+
description: "Path where to create the new file"
|
|
2282
|
+
},
|
|
2283
|
+
content: {
|
|
2284
|
+
type: "string",
|
|
2285
|
+
description: "Content to write to the file"
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2288
|
+
edit: {
|
|
2289
|
+
path: {
|
|
2290
|
+
type: "string",
|
|
2291
|
+
description: "Path to the file to edit"
|
|
2292
|
+
},
|
|
2293
|
+
search: {
|
|
2294
|
+
type: "string",
|
|
2295
|
+
description: "Text to search for (must match exactly)"
|
|
2296
|
+
},
|
|
2297
|
+
replace: {
|
|
2298
|
+
type: "string",
|
|
2299
|
+
description: "Text to replace with"
|
|
2300
|
+
}
|
|
2301
|
+
},
|
|
2302
|
+
list: {
|
|
2303
|
+
path: {
|
|
2304
|
+
type: "string",
|
|
2305
|
+
description: "Directory path to list (defaults to current directory)"
|
|
2306
|
+
}
|
|
2307
|
+
},
|
|
2308
|
+
grep: {
|
|
2309
|
+
pattern: {
|
|
2310
|
+
type: "string",
|
|
2311
|
+
description: "Regex pattern to search for"
|
|
2312
|
+
},
|
|
2313
|
+
path: {
|
|
2314
|
+
type: "string",
|
|
2315
|
+
description: "Directory to search in (defaults to current directory)"
|
|
2316
|
+
},
|
|
2317
|
+
limit: {
|
|
2318
|
+
type: "number",
|
|
2319
|
+
description: "Maximum number of results (default: 50)"
|
|
2320
|
+
}
|
|
2321
|
+
},
|
|
2322
|
+
glob: {
|
|
2323
|
+
pattern: {
|
|
2324
|
+
type: "string",
|
|
2325
|
+
description: "Glob pattern like **/*.ts or src/**/*.tsx"
|
|
2326
|
+
},
|
|
2327
|
+
path: {
|
|
2328
|
+
type: "string",
|
|
2329
|
+
description: "Base directory (defaults to current directory)"
|
|
2330
|
+
}
|
|
2331
|
+
},
|
|
2332
|
+
web_fetch: {
|
|
2333
|
+
url: {
|
|
2334
|
+
type: "string",
|
|
2335
|
+
description: "URL to fetch content from"
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
return schemas[toolName] || {};
|
|
2340
|
+
}
|
|
2341
|
+
getRequiredParams(toolName) {
|
|
2342
|
+
const required = {
|
|
2343
|
+
bash: ["command"],
|
|
2344
|
+
read: ["path"],
|
|
2345
|
+
write: ["path", "content"],
|
|
2346
|
+
edit: ["path", "search", "replace"],
|
|
2347
|
+
list: [],
|
|
2348
|
+
grep: ["pattern"],
|
|
2349
|
+
glob: ["pattern"],
|
|
2350
|
+
web_fetch: ["url"]
|
|
2351
|
+
};
|
|
2352
|
+
return required[toolName] || [];
|
|
2353
|
+
}
|
|
2354
|
+
clearHistory() {
|
|
2355
|
+
this.conversationHistory = [];
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
var llm = new LLMClient();
|
|
2359
|
+
|
|
2360
|
+
// src/core/tasks.ts
|
|
2361
|
+
import fs9 from "fs/promises";
|
|
2362
|
+
import path9 from "path";
|
|
2363
|
+
var TASKS_DIR = ".obsidian";
|
|
2364
|
+
var TASKS_FILE = "tasks.md";
|
|
2365
|
+
var TaskTracker = class {
|
|
2366
|
+
task = null;
|
|
2367
|
+
tasksPath;
|
|
2368
|
+
constructor() {
|
|
2369
|
+
this.tasksPath = path9.join(process.cwd(), TASKS_DIR, TASKS_FILE);
|
|
2370
|
+
}
|
|
2371
|
+
async init() {
|
|
2372
|
+
const dir = path9.join(process.cwd(), TASKS_DIR);
|
|
2373
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
2374
|
+
await this.load();
|
|
2375
|
+
}
|
|
2376
|
+
async load() {
|
|
2377
|
+
try {
|
|
2378
|
+
const content = await fs9.readFile(this.tasksPath, "utf-8");
|
|
2379
|
+
this.task = this.parse(content);
|
|
2380
|
+
} catch {
|
|
2381
|
+
this.task = null;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
parse(content) {
|
|
2385
|
+
const lines = content.split("\n");
|
|
2386
|
+
let title = "";
|
|
2387
|
+
let status = "pending";
|
|
2388
|
+
const subtasks = [];
|
|
2389
|
+
const context2 = [];
|
|
2390
|
+
for (const line of lines) {
|
|
2391
|
+
if (line.startsWith("# ")) {
|
|
2392
|
+
title = line.slice(2).trim();
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
if (line.startsWith("Status: ")) {
|
|
2396
|
+
const s = line.slice(8).trim().toLowerCase();
|
|
2397
|
+
if (["pending", "in_progress", "blocked", "done"].includes(s)) {
|
|
2398
|
+
status = s;
|
|
2399
|
+
}
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
const subtaskMatch = line.match(/^- \[([ x])\] (.+)$/);
|
|
2403
|
+
if (subtaskMatch) {
|
|
2404
|
+
subtasks.push({
|
|
2405
|
+
done: subtaskMatch[1] === "x",
|
|
2406
|
+
text: subtaskMatch[2]
|
|
2407
|
+
});
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
if (line.startsWith("- Modified: ") || line.startsWith("- Read: ")) {
|
|
2411
|
+
context2.push(line.slice(2));
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
if (!title) return null;
|
|
2415
|
+
return {
|
|
2416
|
+
id: Date.now().toString(36),
|
|
2417
|
+
title,
|
|
2418
|
+
status,
|
|
2419
|
+
subtasks,
|
|
2420
|
+
context: context2,
|
|
2421
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2422
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
serialize() {
|
|
2426
|
+
if (!this.task) return "# No active task\n";
|
|
2427
|
+
const lines = [
|
|
2428
|
+
`# ${this.task.title}`,
|
|
2429
|
+
"",
|
|
2430
|
+
`Status: ${this.task.status}`,
|
|
2431
|
+
"",
|
|
2432
|
+
"## Progress"
|
|
2433
|
+
];
|
|
2434
|
+
for (const st of this.task.subtasks) {
|
|
2435
|
+
lines.push(`- [${st.done ? "x" : " "}] ${st.text}`);
|
|
2436
|
+
}
|
|
2437
|
+
if (this.task.context.length > 0) {
|
|
2438
|
+
lines.push("", "## Context");
|
|
2439
|
+
for (const c of this.task.context) {
|
|
2440
|
+
lines.push(`- ${c}`);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
lines.push("", `Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2444
|
+
return lines.join("\n");
|
|
2445
|
+
}
|
|
2446
|
+
async save() {
|
|
2447
|
+
const content = this.serialize();
|
|
2448
|
+
await fs9.writeFile(this.tasksPath, content);
|
|
2449
|
+
}
|
|
2450
|
+
// Task management
|
|
2451
|
+
async create(title) {
|
|
2452
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2453
|
+
this.task = {
|
|
2454
|
+
id: Date.now().toString(36),
|
|
2455
|
+
title,
|
|
2456
|
+
status: "in_progress",
|
|
2457
|
+
subtasks: [],
|
|
2458
|
+
context: [],
|
|
2459
|
+
created_at: now,
|
|
2460
|
+
updated_at: now
|
|
2461
|
+
};
|
|
2462
|
+
await this.save();
|
|
2463
|
+
return this.task;
|
|
2464
|
+
}
|
|
2465
|
+
async addSubtask(text) {
|
|
2466
|
+
if (!this.task) return;
|
|
2467
|
+
this.task.subtasks.push({ text, done: false });
|
|
2468
|
+
this.task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2469
|
+
await this.save();
|
|
2470
|
+
}
|
|
2471
|
+
async completeSubtask(index) {
|
|
2472
|
+
if (!this.task || index >= this.task.subtasks.length) return;
|
|
2473
|
+
this.task.subtasks[index].done = true;
|
|
2474
|
+
this.task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2475
|
+
await this.save();
|
|
2476
|
+
}
|
|
2477
|
+
async setStatus(status) {
|
|
2478
|
+
if (!this.task) return;
|
|
2479
|
+
this.task.status = status;
|
|
2480
|
+
this.task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2481
|
+
await this.save();
|
|
2482
|
+
}
|
|
2483
|
+
async addContext(ctx) {
|
|
2484
|
+
if (!this.task) return;
|
|
2485
|
+
if (!this.task.context.includes(ctx)) {
|
|
2486
|
+
this.task.context.push(ctx);
|
|
2487
|
+
await this.save();
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
async complete() {
|
|
2491
|
+
if (!this.task) return;
|
|
2492
|
+
this.task.status = "done";
|
|
2493
|
+
for (const st of this.task.subtasks) {
|
|
2494
|
+
st.done = true;
|
|
2495
|
+
}
|
|
2496
|
+
await this.save();
|
|
2497
|
+
}
|
|
2498
|
+
async clear() {
|
|
2499
|
+
this.task = null;
|
|
2500
|
+
await this.save();
|
|
2501
|
+
}
|
|
2502
|
+
// Getters
|
|
2503
|
+
get() {
|
|
2504
|
+
return this.task ? { ...this.task } : null;
|
|
2505
|
+
}
|
|
2506
|
+
getProgress() {
|
|
2507
|
+
if (!this.task) return "No active task";
|
|
2508
|
+
const done = this.task.subtasks.filter((s) => s.done).length;
|
|
2509
|
+
const total = this.task.subtasks.length;
|
|
2510
|
+
return `${this.task.title} [${done}/${total}]`;
|
|
2511
|
+
}
|
|
2512
|
+
hasActiveTask() {
|
|
2513
|
+
return this.task !== null && this.task.status !== "done";
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
var tasks = new TaskTracker();
|
|
2517
|
+
|
|
2518
|
+
// src/core/agent.ts
|
|
2519
|
+
var Agent = class {
|
|
2520
|
+
initialized = false;
|
|
2521
|
+
pendingPlan = null;
|
|
2522
|
+
async init() {
|
|
2523
|
+
if (this.initialized) return;
|
|
2524
|
+
await context.init();
|
|
2525
|
+
await tasks.init();
|
|
2526
|
+
this.initialized = true;
|
|
2527
|
+
}
|
|
2528
|
+
async run(input) {
|
|
2529
|
+
await this.init();
|
|
2530
|
+
const mode = context.getMode();
|
|
2531
|
+
if (this.isNewTask(input)) {
|
|
2532
|
+
await tasks.create(this.extractTaskTitle(input));
|
|
2533
|
+
await context.setTask(tasks.getProgress());
|
|
2534
|
+
}
|
|
2535
|
+
bus.emitAgent({ type: "thought", content: `[${mode}] Processing...` });
|
|
2536
|
+
if (mode === "plan") {
|
|
2537
|
+
await this.runPlanMode(input);
|
|
2538
|
+
} else {
|
|
2539
|
+
await this.runDirectMode(input);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
isNewTask(input) {
|
|
2543
|
+
const actionWords = ["create", "add", "implement", "fix", "update", "refactor", "build", "make", "write"];
|
|
2544
|
+
const lower = input.toLowerCase();
|
|
2545
|
+
return input.length > 20 && actionWords.some((w) => lower.includes(w));
|
|
2546
|
+
}
|
|
2547
|
+
extractTaskTitle(input) {
|
|
2548
|
+
const firstSentence = input.split(/[.!?\n]/)[0];
|
|
2549
|
+
return firstSentence.slice(0, 50) + (firstSentence.length > 50 ? "..." : "");
|
|
2550
|
+
}
|
|
2551
|
+
async runPlanMode(input) {
|
|
2552
|
+
bus.emitAgent({ type: "thought", content: "Generating plan (read-only)..." });
|
|
2553
|
+
const planPrompt = `Analyze this request and create a plan.
|
|
2554
|
+
|
|
2555
|
+
IMPORTANT: You are in PLANNING mode. You may ONLY use read operations (read, list, grep, glob) to understand the codebase. Do NOT execute any writes or modifications yet.
|
|
2556
|
+
|
|
2557
|
+
OUTPUT a structured plan:
|
|
2558
|
+
|
|
2559
|
+
REQUEST: ${input}
|
|
2560
|
+
|
|
2561
|
+
FORMAT:
|
|
2562
|
+
TASK: <one line summary>
|
|
2563
|
+
STEPS:
|
|
2564
|
+
1. <step>
|
|
2565
|
+
2. <step>
|
|
2566
|
+
FILES_READ: <comma separated paths or "none">
|
|
2567
|
+
FILES_MODIFY: <comma separated paths or "none">
|
|
2568
|
+
APPROVAL: <yes if destructive, no otherwise>`;
|
|
2569
|
+
const planResponse = await llm.streamChat(planPrompt);
|
|
2570
|
+
if (!planResponse) {
|
|
2571
|
+
bus.emitAgent({ type: "error", message: "Failed to generate plan" });
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
const plan = this.parsePlan(planResponse);
|
|
2575
|
+
this.pendingPlan = { plan, originalInput: input };
|
|
2576
|
+
bus.emitAgent({
|
|
2577
|
+
type: "thought",
|
|
2578
|
+
content: this.formatPlan(plan)
|
|
2579
|
+
});
|
|
2580
|
+
bus.emitAgent({
|
|
2581
|
+
type: "approval_request",
|
|
2582
|
+
requestId: `plan_${Date.now()}`,
|
|
2583
|
+
context: `Execute this plan?
|
|
2584
|
+
|
|
2585
|
+
${this.formatPlan(plan)}`
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
async runDirectMode(input) {
|
|
2589
|
+
const startTime = Date.now();
|
|
2590
|
+
const ctxSummary = context.getSummary();
|
|
2591
|
+
const taskProgress = tasks.getProgress();
|
|
2592
|
+
let enhancedInput = input;
|
|
2593
|
+
if (ctxSummary || taskProgress !== "No active task") {
|
|
2594
|
+
enhancedInput = `${input}
|
|
2595
|
+
|
|
2596
|
+
[Context: ${ctxSummary}]
|
|
2597
|
+
[${taskProgress}]`;
|
|
2598
|
+
}
|
|
2599
|
+
const response = await llm.streamChat(enhancedInput);
|
|
2600
|
+
if (response) {
|
|
2601
|
+
await context.setLastAction(input.slice(0, 50));
|
|
2602
|
+
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2603
|
+
bus.emitAgent({ type: "done", summary: `Completed in ${duration}s` });
|
|
2604
|
+
} else {
|
|
2605
|
+
bus.emitAgent({ type: "error", message: "Failed to get response" });
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
async handleApprovalResponse(approved, requestId) {
|
|
2609
|
+
if (!this.pendingPlan) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (!approved) {
|
|
2613
|
+
bus.emitAgent({ type: "thought", content: "Plan rejected. Awaiting new instructions." });
|
|
2614
|
+
this.pendingPlan = null;
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
const { plan, originalInput } = this.pendingPlan;
|
|
2618
|
+
this.pendingPlan = null;
|
|
2619
|
+
await this.executePlan(plan, originalInput);
|
|
2620
|
+
}
|
|
2621
|
+
async executePlan(plan, originalInput) {
|
|
2622
|
+
const startTime = Date.now();
|
|
2623
|
+
const previousMode = context.getMode();
|
|
2624
|
+
await context.setMode("auto");
|
|
2625
|
+
bus.emitAgent({ type: "thought", content: "Executing approved plan (auto-accept enabled)..." });
|
|
2626
|
+
const executionPrompt = `Execute this plan step by step:
|
|
2627
|
+
|
|
2628
|
+
ORIGINAL REQUEST: ${originalInput}
|
|
2629
|
+
|
|
2630
|
+
PLAN:
|
|
2631
|
+
${this.formatPlan(plan)}
|
|
2632
|
+
|
|
2633
|
+
Execute each step carefully. Use available tools as needed.`;
|
|
2634
|
+
try {
|
|
2635
|
+
const response = await llm.streamChat(executionPrompt);
|
|
2636
|
+
if (response) {
|
|
2637
|
+
await context.setLastAction(`Executed: ${plan.task.slice(0, 40)}`);
|
|
2638
|
+
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2639
|
+
bus.emitAgent({ type: "done", summary: `Plan executed in ${duration}s` });
|
|
2640
|
+
} else {
|
|
2641
|
+
bus.emitAgent({ type: "error", message: "Failed to execute plan" });
|
|
2642
|
+
}
|
|
2643
|
+
} finally {
|
|
2644
|
+
await context.setMode(previousMode);
|
|
2645
|
+
bus.emitAgent({ type: "thought", content: `Mode restored to: ${previousMode}` });
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
parsePlan(response) {
|
|
2649
|
+
const lines = response.split("\n");
|
|
2650
|
+
const plan = {
|
|
2651
|
+
task: "",
|
|
2652
|
+
steps: [],
|
|
2653
|
+
files_to_read: [],
|
|
2654
|
+
files_to_modify: [],
|
|
2655
|
+
requires_approval: false
|
|
2656
|
+
};
|
|
2657
|
+
for (const line of lines) {
|
|
2658
|
+
if (line.startsWith("TASK:")) {
|
|
2659
|
+
plan.task = line.slice(5).trim();
|
|
2660
|
+
} else if (line.match(/^\d+\./)) {
|
|
2661
|
+
plan.steps.push(line.replace(/^\d+\.\s*/, "").trim());
|
|
2662
|
+
} else if (line.startsWith("FILES_READ:")) {
|
|
2663
|
+
const files = line.slice(11).trim();
|
|
2664
|
+
if (files !== "none") {
|
|
2665
|
+
plan.files_to_read = files.split(",").map((f) => f.trim());
|
|
2666
|
+
}
|
|
2667
|
+
} else if (line.startsWith("FILES_MODIFY:")) {
|
|
2668
|
+
const files = line.slice(13).trim();
|
|
2669
|
+
if (files !== "none") {
|
|
2670
|
+
plan.files_to_modify = files.split(",").map((f) => f.trim());
|
|
2671
|
+
}
|
|
2672
|
+
} else if (line.startsWith("APPROVAL:")) {
|
|
2673
|
+
plan.requires_approval = line.toLowerCase().includes("yes");
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
return plan;
|
|
2677
|
+
}
|
|
2678
|
+
formatPlan(plan) {
|
|
2679
|
+
const lines = [`Task: ${plan.task}`, "", "Steps:"];
|
|
2680
|
+
plan.steps.forEach((s, i) => lines.push(` ${i + 1}. ${s}`));
|
|
2681
|
+
if (plan.files_to_read.length > 0) {
|
|
2682
|
+
lines.push("", `Read: ${plan.files_to_read.join(", ")}`);
|
|
2683
|
+
}
|
|
2684
|
+
if (plan.files_to_modify.length > 0) {
|
|
2685
|
+
lines.push(`Modify: ${plan.files_to_modify.join(", ")}`);
|
|
2686
|
+
}
|
|
2687
|
+
return lines.join("\n");
|
|
2688
|
+
}
|
|
2689
|
+
// Mode control
|
|
2690
|
+
async setMode(mode) {
|
|
2691
|
+
await context.setMode(mode);
|
|
2692
|
+
bus.emitAgent({ type: "thought", content: `Mode: ${mode}` });
|
|
2693
|
+
}
|
|
2694
|
+
getMode() {
|
|
2695
|
+
return context.getMode();
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
var agent = new Agent();
|
|
2699
|
+
|
|
2700
|
+
// src/ui/Root.tsx
|
|
2701
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2702
|
+
var MAX_EVENTS = 50;
|
|
2703
|
+
var Root = () => {
|
|
2704
|
+
const [events, setEvents] = useState4([]);
|
|
2705
|
+
const [input, setInput] = useState4("");
|
|
2706
|
+
const [pendingPrompt, setPendingPrompt] = useState4(null);
|
|
2707
|
+
const { exit } = useApp();
|
|
2708
|
+
const [stats, setStats] = useState4({ cost: 0, model: "Loading...", mode: "safe" });
|
|
2709
|
+
const handlePromptResolve = useCallback3(() => {
|
|
2710
|
+
setPendingPrompt(null);
|
|
2711
|
+
}, []);
|
|
2712
|
+
useEffect(() => {
|
|
2713
|
+
const updateStats = async () => {
|
|
2714
|
+
const cfg = await config.load();
|
|
2715
|
+
setStats({
|
|
2716
|
+
cost: usage.getSessionCost(),
|
|
2717
|
+
model: cfg.model,
|
|
2718
|
+
mode: context.getMode()
|
|
2719
|
+
});
|
|
2720
|
+
};
|
|
2721
|
+
updateStats();
|
|
2722
|
+
const statHandler = (event) => {
|
|
2723
|
+
if (event.type === "done" || event.type === "tool_result" || event.type === "thought") {
|
|
2724
|
+
updateStats();
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
bus.on("agent", statHandler);
|
|
2728
|
+
return () => {
|
|
2729
|
+
bus.off("agent", statHandler);
|
|
2730
|
+
};
|
|
2731
|
+
}, []);
|
|
2732
|
+
useEffect(() => {
|
|
2733
|
+
history.load().then((loadedEvents) => {
|
|
2734
|
+
if (loadedEvents.length > 0) {
|
|
2735
|
+
setEvents(loadedEvents);
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
}, []);
|
|
2739
|
+
useEffect(() => {
|
|
2740
|
+
if (events.length > 0) {
|
|
2741
|
+
history.save(events);
|
|
2742
|
+
}
|
|
2743
|
+
}, [events]);
|
|
2744
|
+
useEffect(() => {
|
|
2745
|
+
const handler = (event) => {
|
|
2746
|
+
if (event.type === "clear_history") {
|
|
2747
|
+
setEvents([]);
|
|
2748
|
+
history.clear();
|
|
2749
|
+
setPendingPrompt(null);
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
if (event.type === "approval_request") {
|
|
2753
|
+
setPendingPrompt({
|
|
2754
|
+
type: "approval",
|
|
2755
|
+
requestId: event.requestId,
|
|
2756
|
+
context: event.context,
|
|
2757
|
+
diff: event.diff
|
|
2758
|
+
});
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (event.type === "choice_request") {
|
|
2762
|
+
setPendingPrompt({
|
|
2763
|
+
type: "choice",
|
|
2764
|
+
question: event.question,
|
|
2765
|
+
options: event.options
|
|
2766
|
+
});
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
setEvents((prev) => {
|
|
2770
|
+
const last = prev[prev.length - 1];
|
|
2771
|
+
if (event.type === "thought" && last && last.type === "thought") {
|
|
2772
|
+
if (last.content === event.content) return prev;
|
|
2773
|
+
const newEvents = [...prev];
|
|
2774
|
+
newEvents[newEvents.length - 1] = event;
|
|
2775
|
+
return newEvents;
|
|
2776
|
+
}
|
|
2777
|
+
return [...prev, event];
|
|
2778
|
+
});
|
|
2779
|
+
};
|
|
2780
|
+
const userHandler = (event) => {
|
|
2781
|
+
if (event.type === "user_input") {
|
|
2782
|
+
setEvents((prev) => [...prev, { type: "user_input", content: event.content }]);
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
bus.on("agent", handler);
|
|
2786
|
+
bus.on("user", userHandler);
|
|
2787
|
+
return () => {
|
|
2788
|
+
bus.off("agent", handler);
|
|
2789
|
+
bus.off("user", userHandler);
|
|
2790
|
+
};
|
|
2791
|
+
}, []);
|
|
2792
|
+
const [inputKey, setInputKey] = useState4(0);
|
|
2793
|
+
const [selectedIndex, setSelectedIndex] = useState4(0);
|
|
2794
|
+
const query = input.toLowerCase();
|
|
2795
|
+
const isCommand = input.startsWith("/");
|
|
2796
|
+
const matches = isCommand ? COMMANDS.filter((c) => c.name.startsWith(query)) : [];
|
|
2797
|
+
useEffect(() => {
|
|
2798
|
+
setSelectedIndex(0);
|
|
2799
|
+
}, [input]);
|
|
2800
|
+
const cycleMode = useCallback3(async () => {
|
|
2801
|
+
const modes = ["auto", "plan", "safe"];
|
|
2802
|
+
const currentIndex = modes.indexOf(stats.mode);
|
|
2803
|
+
const nextMode = modes[(currentIndex + 1) % modes.length];
|
|
2804
|
+
await agent.setMode(nextMode);
|
|
2805
|
+
setStats((prev) => ({ ...prev, mode: nextMode }));
|
|
2806
|
+
}, [stats.mode]);
|
|
2807
|
+
useInput3((input2, key) => {
|
|
2808
|
+
if (key.shift && key.tab) {
|
|
2809
|
+
cycleMode();
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
if (matches.length === 0) return;
|
|
2813
|
+
if (key.upArrow) {
|
|
2814
|
+
setSelectedIndex((prev) => prev > 0 ? prev - 1 : matches.length - 1);
|
|
2815
|
+
}
|
|
2816
|
+
if (key.downArrow) {
|
|
2817
|
+
setSelectedIndex((prev) => prev < matches.length - 1 ? prev + 1 : 0);
|
|
2818
|
+
}
|
|
2819
|
+
if (key.return || key.tab) {
|
|
2820
|
+
const selected = matches[selectedIndex];
|
|
2821
|
+
if (selected && input2 !== selected.name) {
|
|
2822
|
+
setInput(selected.name);
|
|
2823
|
+
setInputKey((prev) => prev + 1);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
const handleSubmit = (value) => {
|
|
2828
|
+
if (!value.trim()) return;
|
|
2829
|
+
if (matches.length > 0 && value !== matches[selectedIndex]?.name) {
|
|
2830
|
+
const selected = matches[selectedIndex];
|
|
2831
|
+
if (selected && selected.name.startsWith(value) && selected.name !== value) {
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
if (value.trim() === "/exit") {
|
|
2836
|
+
exit();
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
bus.emitUser({ type: "user_input", content: value });
|
|
2840
|
+
setInput("");
|
|
2841
|
+
};
|
|
2842
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", height: "100%", children: [
|
|
2843
|
+
/* @__PURE__ */ jsx7(Dashboard, {}),
|
|
2844
|
+
/* @__PURE__ */ jsx7(Box7, { flexDirection: "column", flexGrow: 1, marginY: 1, overflowY: "hidden", justifyContent: "flex-end", children: events.slice(-MAX_EVENTS).map((event, i) => {
|
|
2845
|
+
let content = null;
|
|
2846
|
+
if (event.type === "user_input") {
|
|
2847
|
+
content = /* @__PURE__ */ jsx7(Box7, { flexDirection: "row", paddingX: 1, marginBottom: 0, children: /* @__PURE__ */ jsxs7(Text7, { backgroundColor: "#222222", dimColor: true, children: [
|
|
2848
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " > " }),
|
|
2849
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: event.content }),
|
|
2850
|
+
/* @__PURE__ */ jsx7(Text7, { children: " " })
|
|
2851
|
+
] }) }, i);
|
|
2852
|
+
} else if (event.type === "thought") {
|
|
2853
|
+
content = /* @__PURE__ */ jsx7(AgentLine, { content: event.content }, i);
|
|
2854
|
+
} else if (event.type === "tool_start") {
|
|
2855
|
+
let argsSummary = "";
|
|
2856
|
+
try {
|
|
2857
|
+
const args = JSON.parse(event.args);
|
|
2858
|
+
const firstVal = Object.values(args)[0];
|
|
2859
|
+
if (typeof firstVal === "string") {
|
|
2860
|
+
argsSummary = firstVal.length > 60 ? firstVal.slice(0, 60) + "..." : firstVal;
|
|
2861
|
+
}
|
|
2862
|
+
} catch {
|
|
2863
|
+
}
|
|
2864
|
+
content = /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
2865
|
+
/* @__PURE__ */ jsx7(Text7, { backgroundColor: "#1a1a2e", color: "cyan", children: " \u23FA " }),
|
|
2866
|
+
/* @__PURE__ */ jsxs7(Text7, { backgroundColor: "#1a1a2e", color: "white", bold: true, children: [
|
|
2867
|
+
" ",
|
|
2868
|
+
event.tool
|
|
2869
|
+
] }),
|
|
2870
|
+
/* @__PURE__ */ jsxs7(Text7, { backgroundColor: "#1a1a2e", color: "gray", children: [
|
|
2871
|
+
"(",
|
|
2872
|
+
argsSummary,
|
|
2873
|
+
") "
|
|
2874
|
+
] })
|
|
2875
|
+
] }, i);
|
|
2876
|
+
} else if (event.type === "tool_result") {
|
|
2877
|
+
content = /* @__PURE__ */ jsx7(ToolOutput, { tool: event.tool, output: event.output, isError: event.isError }, i);
|
|
2878
|
+
} else if (event.type === "done") {
|
|
2879
|
+
content = /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
|
|
2880
|
+
"[OK] ",
|
|
2881
|
+
event.summary
|
|
2882
|
+
] }, i);
|
|
2883
|
+
} else if (event.type === "error") {
|
|
2884
|
+
content = /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
|
|
2885
|
+
"[ERR] ",
|
|
2886
|
+
event.message
|
|
2887
|
+
] }, i);
|
|
2888
|
+
} else if (event.type === "clear_history") {
|
|
2889
|
+
content = /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "[SYS] History cleared" }, i);
|
|
2890
|
+
}
|
|
2891
|
+
if (!content) return null;
|
|
2892
|
+
return /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: content }, i);
|
|
2893
|
+
}) }),
|
|
2894
|
+
pendingPrompt?.type === "approval" && /* @__PURE__ */ jsx7(
|
|
2895
|
+
ApprovalPrompt,
|
|
2896
|
+
{
|
|
2897
|
+
requestId: pendingPrompt.requestId,
|
|
2898
|
+
context: pendingPrompt.context,
|
|
2899
|
+
diff: pendingPrompt.diff,
|
|
2900
|
+
onResolve: handlePromptResolve
|
|
2901
|
+
}
|
|
2902
|
+
),
|
|
2903
|
+
pendingPrompt?.type === "choice" && /* @__PURE__ */ jsx7(
|
|
2904
|
+
ChoicePrompt,
|
|
2905
|
+
{
|
|
2906
|
+
question: pendingPrompt.question,
|
|
2907
|
+
options: pendingPrompt.options,
|
|
2908
|
+
onResolve: handlePromptResolve
|
|
2909
|
+
}
|
|
2910
|
+
),
|
|
2911
|
+
/* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
2912
|
+
/* @__PURE__ */ jsx7(
|
|
2913
|
+
CommandPopup,
|
|
2914
|
+
{
|
|
2915
|
+
matches,
|
|
2916
|
+
selectedIndex
|
|
2917
|
+
}
|
|
2918
|
+
),
|
|
2919
|
+
/* @__PURE__ */ jsxs7(Box7, { borderStyle: "round", borderColor: pendingPrompt ? "gray" : "gray", paddingX: 1, children: [
|
|
2920
|
+
/* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "> " }),
|
|
2921
|
+
/* @__PURE__ */ jsx7(
|
|
2922
|
+
TextInput,
|
|
2923
|
+
{
|
|
2924
|
+
value: input,
|
|
2925
|
+
onChange: pendingPrompt ? () => {
|
|
2926
|
+
} : setInput,
|
|
2927
|
+
onSubmit: pendingPrompt ? () => {
|
|
2928
|
+
} : handleSubmit,
|
|
2929
|
+
placeholder: pendingPrompt ? "Respond to prompt above..." : "Type a message or command..."
|
|
2930
|
+
},
|
|
2931
|
+
inputKey
|
|
2932
|
+
)
|
|
2933
|
+
] })
|
|
2934
|
+
] }),
|
|
2935
|
+
/* @__PURE__ */ jsxs7(
|
|
2936
|
+
Box7,
|
|
2937
|
+
{
|
|
2938
|
+
borderStyle: "single",
|
|
2939
|
+
borderTop: false,
|
|
2940
|
+
borderLeft: false,
|
|
2941
|
+
borderRight: false,
|
|
2942
|
+
borderColor: "gray",
|
|
2943
|
+
marginTop: 0,
|
|
2944
|
+
paddingX: 1,
|
|
2945
|
+
flexDirection: "row",
|
|
2946
|
+
justifyContent: "space-between",
|
|
2947
|
+
children: [
|
|
2948
|
+
/* @__PURE__ */ jsx7(Box7, { minWidth: 22, children: /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
2949
|
+
"[ ",
|
|
2950
|
+
/* @__PURE__ */ jsx7(Text7, { color: stats.mode === "plan" ? "yellow" : stats.mode === "auto" ? "green" : "white", children: stats.mode === "auto" ? "auto-accept ON" : stats.mode === "plan" ? "plan mode" : "default" }),
|
|
2951
|
+
" ]"
|
|
2952
|
+
] }) }),
|
|
2953
|
+
/* @__PURE__ */ jsx7(Box7, { minWidth: 20, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "[ Context: 0 files ]" }) }),
|
|
2954
|
+
/* @__PURE__ */ jsx7(Box7, { minWidth: 25, justifyContent: "center", children: /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
2955
|
+
"[ Model: ",
|
|
2956
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: stats.model }),
|
|
2957
|
+
" ]"
|
|
2958
|
+
] }) }),
|
|
2959
|
+
/* @__PURE__ */ jsx7(Box7, { minWidth: 15, justifyContent: "flex-end", children: /* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
2960
|
+
"[ Cost: ",
|
|
2961
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
|
|
2962
|
+
"$",
|
|
2963
|
+
stats.cost.toFixed(4)
|
|
2964
|
+
] }),
|
|
2965
|
+
" ]"
|
|
2966
|
+
] }) })
|
|
2967
|
+
]
|
|
2968
|
+
}
|
|
2969
|
+
)
|
|
2970
|
+
] });
|
|
2971
|
+
};
|
|
2972
|
+
|
|
2973
|
+
// src/commands/init.ts
|
|
2974
|
+
var initCommand = async (_args) => {
|
|
2975
|
+
bus.emitAgent({ type: "thought", content: "Checking configuration..." });
|
|
2976
|
+
if (await config.exists()) {
|
|
2977
|
+
bus.emitAgent({
|
|
2978
|
+
type: "tool_result",
|
|
2979
|
+
tool: "System",
|
|
2980
|
+
output: `Configuration already exists at ${config.getPath()}`
|
|
2981
|
+
});
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
bus.emitAgent({ type: "thought", content: "Creating default configuration..." });
|
|
2985
|
+
await config.save({
|
|
2986
|
+
model: "claude-3-5-sonnet",
|
|
2987
|
+
maxTokens: 8192,
|
|
2988
|
+
language: "en"
|
|
2989
|
+
});
|
|
2990
|
+
bus.emitAgent({
|
|
2991
|
+
type: "tool_result",
|
|
2992
|
+
tool: "System",
|
|
2993
|
+
output: `Initialized configuration at ${config.getPath()}`
|
|
2994
|
+
});
|
|
2995
|
+
};
|
|
2996
|
+
|
|
2997
|
+
// src/commands/clear.ts
|
|
2998
|
+
var clearCommand = async (_args) => {
|
|
2999
|
+
bus.emitAgent({
|
|
3000
|
+
type: "clear_history"
|
|
3001
|
+
});
|
|
3002
|
+
llm.clearHistory();
|
|
3003
|
+
await context.reset();
|
|
3004
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3005
|
+
bus.emitAgent({
|
|
3006
|
+
type: "done",
|
|
3007
|
+
summary: "History, conversation, and context cleared."
|
|
3008
|
+
});
|
|
3009
|
+
};
|
|
3010
|
+
|
|
3011
|
+
// src/commands/cost.ts
|
|
3012
|
+
var costCommand = async (_args) => {
|
|
3013
|
+
await usage.init();
|
|
3014
|
+
const stats = usage.getStats();
|
|
3015
|
+
bus.emitAgent({
|
|
3016
|
+
type: "tool_result",
|
|
3017
|
+
tool: "Cost Tracker",
|
|
3018
|
+
output: `Session Cost Report:
|
|
3019
|
+
Input tokens: ${stats.totalInputTokens.toLocaleString()}
|
|
3020
|
+
Output tokens: ${stats.totalOutputTokens.toLocaleString()}
|
|
3021
|
+
Total cost: $${stats.totalCost.toFixed(4)}`
|
|
3022
|
+
});
|
|
3023
|
+
};
|
|
3024
|
+
|
|
3025
|
+
// src/commands/usage.ts
|
|
3026
|
+
var usageCommand = async (_args) => {
|
|
3027
|
+
await usage.init();
|
|
3028
|
+
const stats = usage.getStats();
|
|
3029
|
+
bus.emitAgent({
|
|
3030
|
+
type: "tool_result",
|
|
3031
|
+
tool: "Usage Tracker",
|
|
3032
|
+
output: `Historical Usage Report:
|
|
3033
|
+
Total sessions: ${stats.totalSessions}
|
|
3034
|
+
Total messages: ${stats.totalRequests}
|
|
3035
|
+
Total tokens: ${(stats.totalInputTokens + stats.totalOutputTokens).toLocaleString()}
|
|
3036
|
+
Total cost: $${stats.totalCost.toFixed(4)}`
|
|
3037
|
+
});
|
|
3038
|
+
};
|
|
3039
|
+
|
|
3040
|
+
// src/commands/models.ts
|
|
3041
|
+
var modelsCommand = async (args) => {
|
|
3042
|
+
const currentConfig = await config.load();
|
|
3043
|
+
if (args.length === 0) {
|
|
3044
|
+
bus.emitAgent({
|
|
3045
|
+
type: "tool_result",
|
|
3046
|
+
tool: "Model Selector",
|
|
3047
|
+
output: `Available AI Models:
|
|
3048
|
+
1. claude-sonnet-4-5 [Current/Default]
|
|
3049
|
+
2. claude-opus-4-5 [Powerful]
|
|
3050
|
+
3. claude-haiku-4-5 [Fast/Cheap]
|
|
3051
|
+
4. ollama [Local]
|
|
3052
|
+
|
|
3053
|
+
Current model: ${currentConfig.model}
|
|
3054
|
+
|
|
3055
|
+
Usage: /models <number> to select a model`
|
|
3056
|
+
});
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
const selection = args[0];
|
|
3060
|
+
let newModel;
|
|
3061
|
+
switch (selection) {
|
|
3062
|
+
case "1":
|
|
3063
|
+
case "sonnet":
|
|
3064
|
+
case "claude-sonnet-4-5":
|
|
3065
|
+
newModel = "claude-sonnet-4-5-20250929";
|
|
3066
|
+
break;
|
|
3067
|
+
case "2":
|
|
3068
|
+
case "opus":
|
|
3069
|
+
case "claude-opus-4-5":
|
|
3070
|
+
newModel = "claude-opus-4-5-20251101";
|
|
3071
|
+
break;
|
|
3072
|
+
case "3":
|
|
3073
|
+
case "haiku":
|
|
3074
|
+
case "claude-haiku-4-5":
|
|
3075
|
+
newModel = "claude-haiku-4-5-20251001";
|
|
3076
|
+
break;
|
|
3077
|
+
case "4":
|
|
3078
|
+
case "ollama":
|
|
3079
|
+
newModel = "ollama";
|
|
3080
|
+
break;
|
|
3081
|
+
default:
|
|
3082
|
+
bus.emitAgent({
|
|
3083
|
+
type: "error",
|
|
3084
|
+
message: `Invalid model selection: ${selection}. Use /models to see available options.`
|
|
3085
|
+
});
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
await config.save({
|
|
3089
|
+
...currentConfig,
|
|
3090
|
+
model: newModel
|
|
3091
|
+
});
|
|
3092
|
+
bus.emitAgent({
|
|
3093
|
+
type: "done",
|
|
3094
|
+
summary: `Model switched to ${newModel}`
|
|
3095
|
+
});
|
|
3096
|
+
};
|
|
3097
|
+
|
|
3098
|
+
// src/commands/tool.ts
|
|
3099
|
+
var toolCommand = async (args) => {
|
|
3100
|
+
if (args.length === 0) {
|
|
3101
|
+
const toolList = tools.list();
|
|
3102
|
+
const formatted = toolList.map((t) => ` \u2022 ${t.name}: ${t.description}`).join("\n");
|
|
3103
|
+
bus.emitAgent({
|
|
3104
|
+
type: "thought",
|
|
3105
|
+
content: `Available tools:
|
|
3106
|
+
${formatted}
|
|
3107
|
+
|
|
3108
|
+
Usage: /tool <name> <json-args>`
|
|
3109
|
+
});
|
|
3110
|
+
bus.emitAgent({
|
|
3111
|
+
type: "done",
|
|
3112
|
+
summary: "Tools listed"
|
|
3113
|
+
});
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
const toolName = args[0];
|
|
3117
|
+
const jsonArgs = args.slice(1).join(" ");
|
|
3118
|
+
if (!tools.has(toolName)) {
|
|
3119
|
+
bus.emitAgent({
|
|
3120
|
+
type: "error",
|
|
3121
|
+
message: `Unknown tool: ${toolName}`
|
|
3122
|
+
});
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
try {
|
|
3126
|
+
const parsedArgs = jsonArgs ? JSON.parse(jsonArgs) : {};
|
|
3127
|
+
const result = await tools.execute(toolName, parsedArgs);
|
|
3128
|
+
bus.emitAgent({
|
|
3129
|
+
type: "done",
|
|
3130
|
+
summary: result.success ? "Tool executed successfully" : "Tool execution failed"
|
|
3131
|
+
});
|
|
3132
|
+
} catch (error) {
|
|
3133
|
+
bus.emitAgent({
|
|
3134
|
+
type: "error",
|
|
3135
|
+
message: `Failed to execute tool: ${error.message}`
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
|
|
3140
|
+
// src/commands/status.ts
|
|
3141
|
+
import os5 from "os";
|
|
3142
|
+
import path10 from "path";
|
|
3143
|
+
var statusCommand = async (_args) => {
|
|
3144
|
+
const cfg = await config.load();
|
|
3145
|
+
const stats = usage.getStats();
|
|
3146
|
+
const platform = `${os5.type()} ${os5.release()}`;
|
|
3147
|
+
const nodeVersion = process.version;
|
|
3148
|
+
const workspace = process.cwd();
|
|
3149
|
+
const workspaceName = path10.basename(workspace);
|
|
3150
|
+
const sessionCost = usage.getSessionCost();
|
|
3151
|
+
const toolList = tools.list().map((t) => t.name).join(", ");
|
|
3152
|
+
const statusLines = [
|
|
3153
|
+
"=".repeat(50),
|
|
3154
|
+
"OBSIDIAN NEXT - System Status",
|
|
3155
|
+
"=".repeat(50),
|
|
3156
|
+
"",
|
|
3157
|
+
"[System]",
|
|
3158
|
+
` Platform: ${platform}`,
|
|
3159
|
+
` Node.js: ${nodeVersion}`,
|
|
3160
|
+
` Workspace: ${workspaceName}`,
|
|
3161
|
+
` Path: ${workspace}`,
|
|
3162
|
+
"",
|
|
3163
|
+
"[Configuration]",
|
|
3164
|
+
` Model: ${cfg.model || "claude-sonnet-4-5"}`,
|
|
3165
|
+
` Max Tokens: ${cfg.maxTokens || 8192}`,
|
|
3166
|
+
` API Key: ${cfg.apiKey ? "[SET]" : "[NOT SET]"}`,
|
|
3167
|
+
"",
|
|
3168
|
+
"[Session]",
|
|
3169
|
+
` Cost: $${sessionCost.toFixed(4)}`,
|
|
3170
|
+
` Input: ${stats.totalInputTokens.toLocaleString()} tokens`,
|
|
3171
|
+
` Output: ${stats.totalOutputTokens.toLocaleString()} tokens`,
|
|
3172
|
+
"",
|
|
3173
|
+
"[Tools]",
|
|
3174
|
+
` Available: ${toolList}`,
|
|
3175
|
+
"",
|
|
3176
|
+
"=".repeat(50)
|
|
3177
|
+
];
|
|
3178
|
+
bus.emitAgent({
|
|
3179
|
+
type: "thought",
|
|
3180
|
+
content: statusLines.join("\n")
|
|
3181
|
+
});
|
|
3182
|
+
bus.emitAgent({
|
|
3183
|
+
type: "done",
|
|
3184
|
+
summary: "Status displayed"
|
|
3185
|
+
});
|
|
3186
|
+
};
|
|
3187
|
+
|
|
3188
|
+
// src/commands/sandbox.ts
|
|
3189
|
+
var sandboxCommand = async (args) => {
|
|
3190
|
+
const subcommand = args[0]?.toLowerCase();
|
|
3191
|
+
if (!subcommand) {
|
|
3192
|
+
const mode = sandbox.getMode();
|
|
3193
|
+
const available = await sandbox.isAvailable();
|
|
3194
|
+
const statusLines = [
|
|
3195
|
+
"=".repeat(50),
|
|
3196
|
+
"SANDBOX EXECUTION MODE",
|
|
3197
|
+
"=".repeat(50),
|
|
3198
|
+
"",
|
|
3199
|
+
`Current Mode: ${mode.toUpperCase()}`,
|
|
3200
|
+
`Runtime: ${available ? "Available" : "Not installed"}`,
|
|
3201
|
+
"",
|
|
3202
|
+
"Modes:",
|
|
3203
|
+
" local - Direct execution with auditor checks",
|
|
3204
|
+
" sandbox - OS-level isolation (filesystem/network)",
|
|
3205
|
+
"",
|
|
3206
|
+
"Usage:",
|
|
3207
|
+
" /sandbox local - Switch to local mode",
|
|
3208
|
+
" /sandbox sandbox - Switch to sandbox mode",
|
|
3209
|
+
" /sandbox config - Show sandbox configuration",
|
|
3210
|
+
"",
|
|
3211
|
+
"=".repeat(50)
|
|
3212
|
+
];
|
|
3213
|
+
bus.emitAgent({
|
|
3214
|
+
type: "thought",
|
|
3215
|
+
content: statusLines.join("\n")
|
|
3216
|
+
});
|
|
3217
|
+
bus.emitAgent({
|
|
3218
|
+
type: "done",
|
|
3219
|
+
summary: "Sandbox status displayed"
|
|
3220
|
+
});
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
if (subcommand === "local" || subcommand === "sandbox") {
|
|
3224
|
+
const available = await sandbox.isAvailable();
|
|
3225
|
+
if (subcommand === "sandbox" && !available) {
|
|
3226
|
+
bus.emitAgent({
|
|
3227
|
+
type: "error",
|
|
3228
|
+
message: "Sandbox runtime not available. Install with: npm install @anthropic-ai/sandbox-runtime"
|
|
3229
|
+
});
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
await sandbox.setMode(subcommand);
|
|
3233
|
+
bus.emitAgent({
|
|
3234
|
+
type: "done",
|
|
3235
|
+
summary: `Execution mode: ${subcommand.toUpperCase()}`
|
|
3236
|
+
});
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
if (subcommand === "config") {
|
|
3240
|
+
const config2 = sandbox.getConfig();
|
|
3241
|
+
const configLines = [
|
|
3242
|
+
"=".repeat(50),
|
|
3243
|
+
"SANDBOX CONFIGURATION",
|
|
3244
|
+
"=".repeat(50),
|
|
3245
|
+
"",
|
|
3246
|
+
"[Network]",
|
|
3247
|
+
` Allowed: ${config2.allowedDomains.join(", ") || "none"}`,
|
|
3248
|
+
` Denied: ${config2.deniedDomains.join(", ") || "none"}`,
|
|
3249
|
+
"",
|
|
3250
|
+
"[Filesystem]",
|
|
3251
|
+
` Deny Read: ${config2.denyRead.join(", ") || "none"}`,
|
|
3252
|
+
` Allow Write: ${config2.allowWrite.join(", ") || "none"}`,
|
|
3253
|
+
` Deny Write: ${config2.denyWrite.join(", ") || "none"}`,
|
|
3254
|
+
"",
|
|
3255
|
+
"=".repeat(50)
|
|
3256
|
+
];
|
|
3257
|
+
bus.emitAgent({
|
|
3258
|
+
type: "thought",
|
|
3259
|
+
content: configLines.join("\n")
|
|
3260
|
+
});
|
|
3261
|
+
bus.emitAgent({
|
|
3262
|
+
type: "done",
|
|
3263
|
+
summary: "Sandbox config displayed"
|
|
3264
|
+
});
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
bus.emitAgent({
|
|
3268
|
+
type: "error",
|
|
3269
|
+
message: `Unknown sandbox command: ${subcommand}. Use: local, sandbox, or config`
|
|
3270
|
+
});
|
|
3271
|
+
};
|
|
3272
|
+
|
|
3273
|
+
// src/commands/mode.ts
|
|
3274
|
+
var MODES = ["auto", "plan", "safe"];
|
|
3275
|
+
async function modeCommand(args) {
|
|
3276
|
+
const mode = args[0]?.toLowerCase();
|
|
3277
|
+
if (!mode) {
|
|
3278
|
+
const current = agent.getMode();
|
|
3279
|
+
bus.emitAgent({
|
|
3280
|
+
type: "thought",
|
|
3281
|
+
content: `Current mode: ${current}
|
|
3282
|
+
|
|
3283
|
+
Available modes:
|
|
3284
|
+
- auto: Execute without confirmation
|
|
3285
|
+
- plan: Think -> Approve -> Execute
|
|
3286
|
+
- safe: Auto reads, approve writes (default)`
|
|
3287
|
+
});
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
if (!MODES.includes(mode)) {
|
|
3291
|
+
bus.emitAgent({
|
|
3292
|
+
type: "error",
|
|
3293
|
+
message: `Invalid mode: ${mode}. Use: auto, plan, safe`
|
|
3294
|
+
});
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
await agent.setMode(mode);
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// src/commands/task.ts
|
|
3301
|
+
async function taskCommand(args) {
|
|
3302
|
+
await tasks.init();
|
|
3303
|
+
const subcommand = args[0]?.toLowerCase();
|
|
3304
|
+
if (!subcommand || subcommand === "show") {
|
|
3305
|
+
const task = tasks.get();
|
|
3306
|
+
if (!task) {
|
|
3307
|
+
bus.emitAgent({ type: "thought", content: "No active task" });
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
const lines = [
|
|
3311
|
+
`Task: ${task.title}`,
|
|
3312
|
+
`Status: ${task.status}`,
|
|
3313
|
+
"",
|
|
3314
|
+
"Progress:"
|
|
3315
|
+
];
|
|
3316
|
+
task.subtasks.forEach((st, i) => {
|
|
3317
|
+
lines.push(` ${st.done ? "[x]" : "[ ]"} ${st.text}`);
|
|
3318
|
+
});
|
|
3319
|
+
if (task.context.length > 0) {
|
|
3320
|
+
lines.push("", "Context:");
|
|
3321
|
+
task.context.forEach((c) => lines.push(` - ${c}`));
|
|
3322
|
+
}
|
|
3323
|
+
bus.emitAgent({ type: "thought", content: lines.join("\n") });
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
if (subcommand === "new") {
|
|
3327
|
+
const title = args.slice(1).join(" ");
|
|
3328
|
+
if (!title) {
|
|
3329
|
+
bus.emitAgent({ type: "error", message: "Usage: /task new <title>" });
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
await tasks.create(title);
|
|
3333
|
+
bus.emitAgent({ type: "thought", content: `Task created: ${title}` });
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
if (subcommand === "add") {
|
|
3337
|
+
const text = args.slice(1).join(" ");
|
|
3338
|
+
if (!text) {
|
|
3339
|
+
bus.emitAgent({ type: "error", message: "Usage: /task add <subtask>" });
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
await tasks.addSubtask(text);
|
|
3343
|
+
bus.emitAgent({ type: "thought", content: `Added: ${text}` });
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
if (subcommand === "done") {
|
|
3347
|
+
const index = parseInt(args[1]);
|
|
3348
|
+
if (isNaN(index)) {
|
|
3349
|
+
await tasks.complete();
|
|
3350
|
+
bus.emitAgent({ type: "thought", content: "Task completed" });
|
|
3351
|
+
} else {
|
|
3352
|
+
await tasks.completeSubtask(index - 1);
|
|
3353
|
+
bus.emitAgent({ type: "thought", content: `Subtask ${index} done` });
|
|
3354
|
+
}
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
if (subcommand === "clear") {
|
|
3358
|
+
await tasks.clear();
|
|
3359
|
+
bus.emitAgent({ type: "thought", content: "Task cleared" });
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
bus.emitAgent({
|
|
3363
|
+
type: "thought",
|
|
3364
|
+
content: `Usage:
|
|
3365
|
+
/task - Show current task
|
|
3366
|
+
/task new <t> - Create new task
|
|
3367
|
+
/task add <st> - Add subtask
|
|
3368
|
+
/task done [n] - Complete task or subtask n
|
|
3369
|
+
/task clear - Clear task`
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/commands/undo.ts
|
|
3374
|
+
async function undoCommand(args) {
|
|
3375
|
+
const count = parseInt(args[0]) || 1;
|
|
3376
|
+
const history2 = undo.getHistory(10);
|
|
3377
|
+
if (args[0] === "list" || args[0] === "history") {
|
|
3378
|
+
if (history2.length === 0) {
|
|
3379
|
+
bus.emitAgent({ type: "thought", content: "No changes to undo" });
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
const lines = ["Recent changes:", ""];
|
|
3383
|
+
history2.forEach((c, i) => {
|
|
3384
|
+
const op = c.operation === "create" ? "+" : c.operation === "delete" ? "-" : "~";
|
|
3385
|
+
lines.push(` ${i + 1}. [${op}] ${c.filePath}`);
|
|
3386
|
+
});
|
|
3387
|
+
bus.emitAgent({ type: "thought", content: lines.join("\n") });
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
const result = await undo.undo(count);
|
|
3391
|
+
if (result.success) {
|
|
3392
|
+
bus.emitAgent({ type: "thought", content: result.message });
|
|
3393
|
+
} else {
|
|
3394
|
+
bus.emitAgent({ type: "error", message: result.message });
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// src/commands/config.ts
|
|
3399
|
+
async function configCommand(args) {
|
|
3400
|
+
const subcommand = args[0];
|
|
3401
|
+
const cfg = await config.load();
|
|
3402
|
+
if (!subcommand) {
|
|
3403
|
+
const display = [
|
|
3404
|
+
"Current Configuration:",
|
|
3405
|
+
"=".repeat(40),
|
|
3406
|
+
` model: ${cfg.model}`,
|
|
3407
|
+
` maxTokens: ${cfg.maxTokens}`,
|
|
3408
|
+
` apiKey: ${cfg.apiKey ? "***" + cfg.apiKey.slice(-4) : "not set"}`,
|
|
3409
|
+
"",
|
|
3410
|
+
"Usage:",
|
|
3411
|
+
" /config set <key> <value>",
|
|
3412
|
+
" /config reset",
|
|
3413
|
+
"",
|
|
3414
|
+
"Keys: model, maxTokens"
|
|
3415
|
+
];
|
|
3416
|
+
bus.emitAgent({ type: "thought", content: display.join("\n") });
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
if (subcommand === "set") {
|
|
3420
|
+
const key = args[1];
|
|
3421
|
+
const value = args.slice(2).join(" ");
|
|
3422
|
+
if (!key || !value) {
|
|
3423
|
+
bus.emitAgent({ type: "error", message: "Usage: /config set <key> <value>" });
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
const validKeys = ["model", "maxTokens"];
|
|
3427
|
+
if (!validKeys.includes(key)) {
|
|
3428
|
+
bus.emitAgent({ type: "error", message: `Invalid key. Valid keys: ${validKeys.join(", ")}` });
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
const updates = {};
|
|
3432
|
+
if (key === "maxTokens") {
|
|
3433
|
+
const num = parseInt(value);
|
|
3434
|
+
if (isNaN(num) || num < 100 || num > 2e5) {
|
|
3435
|
+
bus.emitAgent({ type: "error", message: "maxTokens must be between 100 and 200000" });
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
updates.maxTokens = num;
|
|
3439
|
+
} else {
|
|
3440
|
+
updates[key] = value;
|
|
3441
|
+
}
|
|
3442
|
+
await config.save(updates);
|
|
3443
|
+
bus.emitAgent({ type: "thought", content: `Config updated: ${key} = ${value}` });
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
if (subcommand === "reset") {
|
|
3447
|
+
await config.save({
|
|
3448
|
+
model: "claude-sonnet-4-5-20250929",
|
|
3449
|
+
maxTokens: 8192
|
|
3450
|
+
});
|
|
3451
|
+
bus.emitAgent({ type: "thought", content: "Config reset to defaults" });
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
bus.emitAgent({ type: "error", message: `Unknown subcommand: ${subcommand}` });
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// src/commands/doctor.ts
|
|
3458
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
3459
|
+
async function doctorCommand(args) {
|
|
3460
|
+
const results = [];
|
|
3461
|
+
bus.emitAgent({ type: "thought", content: "Running diagnostics..." });
|
|
3462
|
+
try {
|
|
3463
|
+
const cfg = await config.load();
|
|
3464
|
+
if (cfg.apiKey) {
|
|
3465
|
+
results.push({
|
|
3466
|
+
name: "API Key",
|
|
3467
|
+
status: "ok",
|
|
3468
|
+
message: `Configured (***${cfg.apiKey.slice(-4)})`
|
|
3469
|
+
});
|
|
3470
|
+
} else {
|
|
3471
|
+
results.push({
|
|
3472
|
+
name: "API Key",
|
|
3473
|
+
status: "error",
|
|
3474
|
+
message: "Not set. Run /init or set ANTHROPIC_API_KEY"
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
results.push({
|
|
3478
|
+
name: "Model",
|
|
3479
|
+
status: "ok",
|
|
3480
|
+
message: cfg.model
|
|
3481
|
+
});
|
|
3482
|
+
} catch (e) {
|
|
3483
|
+
results.push({
|
|
3484
|
+
name: "Config",
|
|
3485
|
+
status: "error",
|
|
3486
|
+
message: `Failed to load: ${e}`
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
const toolList = tools.list();
|
|
3490
|
+
results.push({
|
|
3491
|
+
name: "Tools",
|
|
3492
|
+
status: "ok",
|
|
3493
|
+
message: `${toolList.length} registered: ${toolList.map((t) => t.name).join(", ")}`
|
|
3494
|
+
});
|
|
3495
|
+
const sandboxAvailable = await sandbox.isAvailable();
|
|
3496
|
+
results.push({
|
|
3497
|
+
name: "Sandbox",
|
|
3498
|
+
status: sandboxAvailable ? "ok" : "warn",
|
|
3499
|
+
message: sandboxAvailable ? "Available" : "Not available (commands run directly)"
|
|
3500
|
+
});
|
|
3501
|
+
try {
|
|
3502
|
+
const cfg = await config.load();
|
|
3503
|
+
if (cfg.apiKey) {
|
|
3504
|
+
const client = new Anthropic2({ apiKey: cfg.apiKey });
|
|
3505
|
+
results.push({
|
|
3506
|
+
name: "API Client",
|
|
3507
|
+
status: "ok",
|
|
3508
|
+
message: "Anthropic SDK initialized"
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
} catch (e) {
|
|
3512
|
+
results.push({
|
|
3513
|
+
name: "API Client",
|
|
3514
|
+
status: "error",
|
|
3515
|
+
message: e.message || "Failed to initialize"
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
const cwd = process.cwd();
|
|
3519
|
+
results.push({
|
|
3520
|
+
name: "Workspace",
|
|
3521
|
+
status: "ok",
|
|
3522
|
+
message: cwd
|
|
3523
|
+
});
|
|
3524
|
+
const nodeVersion = process.version;
|
|
3525
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
3526
|
+
results.push({
|
|
3527
|
+
name: "Node.js",
|
|
3528
|
+
status: majorVersion >= 18 ? "ok" : "warn",
|
|
3529
|
+
message: nodeVersion + (majorVersion < 18 ? " (recommend v18+)" : "")
|
|
3530
|
+
});
|
|
3531
|
+
const statusIcons = {
|
|
3532
|
+
ok: "[OK]",
|
|
3533
|
+
warn: "[!!]",
|
|
3534
|
+
error: "[X]"
|
|
3535
|
+
};
|
|
3536
|
+
const statusColors = {
|
|
3537
|
+
ok: "green",
|
|
3538
|
+
warn: "yellow",
|
|
3539
|
+
error: "red"
|
|
3540
|
+
};
|
|
3541
|
+
const output = [
|
|
3542
|
+
"System Diagnostics",
|
|
3543
|
+
"=".repeat(50),
|
|
3544
|
+
"",
|
|
3545
|
+
...results.map((r) => {
|
|
3546
|
+
const icon = statusIcons[r.status];
|
|
3547
|
+
return `${icon.padEnd(6)} ${r.name.padEnd(12)} ${r.message}`;
|
|
3548
|
+
}),
|
|
3549
|
+
"",
|
|
3550
|
+
"=".repeat(50),
|
|
3551
|
+
`Total: ${results.filter((r) => r.status === "ok").length} OK, ${results.filter((r) => r.status === "warn").length} warnings, ${results.filter((r) => r.status === "error").length} errors`
|
|
3552
|
+
];
|
|
3553
|
+
bus.emitAgent({ type: "thought", content: output.join("\n") });
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// src/commands/settings.ts
|
|
3557
|
+
var settingsCommand = async (args) => {
|
|
3558
|
+
const s = await settings.load();
|
|
3559
|
+
if (args.length === 0) {
|
|
3560
|
+
bus.emitAgent({
|
|
3561
|
+
type: "thought",
|
|
3562
|
+
content: `Settings (${settings.getPath()}):
|
|
3563
|
+
${JSON.stringify(s, null, 2)}`
|
|
3564
|
+
});
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
const [key, ...valueParts] = args;
|
|
3568
|
+
const value = valueParts.join(" ");
|
|
3569
|
+
if (!value) {
|
|
3570
|
+
const keys = key.split(".");
|
|
3571
|
+
let current = s;
|
|
3572
|
+
for (const k of keys) {
|
|
3573
|
+
if (current && typeof current === "object" && k in current) {
|
|
3574
|
+
current = current[k];
|
|
3575
|
+
} else {
|
|
3576
|
+
bus.emitAgent({
|
|
3577
|
+
type: "error",
|
|
3578
|
+
message: `Unknown setting: ${key}`
|
|
3579
|
+
});
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
bus.emitAgent({
|
|
3584
|
+
type: "thought",
|
|
3585
|
+
content: `${key} = ${JSON.stringify(current, null, 2)}`
|
|
3586
|
+
});
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
try {
|
|
3590
|
+
const keys = key.split(".");
|
|
3591
|
+
let parsed;
|
|
3592
|
+
if (value === "true") parsed = true;
|
|
3593
|
+
else if (value === "false") parsed = false;
|
|
3594
|
+
else if (!isNaN(Number(value))) parsed = Number(value);
|
|
3595
|
+
else if (value.startsWith("[") || value.startsWith("{")) parsed = JSON.parse(value);
|
|
3596
|
+
else parsed = value;
|
|
3597
|
+
if (keys.length === 1) {
|
|
3598
|
+
await settings.save({ [keys[0]]: parsed });
|
|
3599
|
+
} else if (keys.length === 2) {
|
|
3600
|
+
const current = await settings.load();
|
|
3601
|
+
const topKey = keys[0];
|
|
3602
|
+
const subKey = keys[1];
|
|
3603
|
+
if (typeof current[topKey] === "object" && current[topKey] !== null) {
|
|
3604
|
+
await settings.save({
|
|
3605
|
+
[topKey]: {
|
|
3606
|
+
...current[topKey],
|
|
3607
|
+
[subKey]: parsed
|
|
3608
|
+
}
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
const updated = await settings.reload();
|
|
3613
|
+
bus.emitAgent({
|
|
3614
|
+
type: "done",
|
|
3615
|
+
summary: `Set ${key} = ${JSON.stringify(parsed)}`
|
|
3616
|
+
});
|
|
3617
|
+
} catch (err) {
|
|
3618
|
+
bus.emitAgent({
|
|
3619
|
+
type: "error",
|
|
3620
|
+
message: `Failed to set ${key}: ${err.message}`
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
};
|
|
3624
|
+
|
|
3625
|
+
// src/core/commands.ts
|
|
3626
|
+
var CommandRegistry = class {
|
|
3627
|
+
commands = /* @__PURE__ */ new Map();
|
|
3628
|
+
constructor() {
|
|
3629
|
+
this.register("help", "Show available commands", async () => {
|
|
3630
|
+
const validCommands = Array.from(this.commands.values()).map((c) => ` /${c.name.padEnd(10)} - ${c.description}`).join("\n");
|
|
3631
|
+
bus.emitAgent({
|
|
3632
|
+
type: "thought",
|
|
3633
|
+
content: `Available Commands:
|
|
3634
|
+
${validCommands}`
|
|
3635
|
+
});
|
|
3636
|
+
});
|
|
3637
|
+
this.register("init", "Initialize configuration", initCommand);
|
|
3638
|
+
this.register("clear", "Clear conversation history", clearCommand);
|
|
3639
|
+
this.register("cost", "Show session cost", costCommand);
|
|
3640
|
+
this.register("usage", "Show historical usage", usageCommand);
|
|
3641
|
+
this.register("models", "Select AI model", modelsCommand);
|
|
3642
|
+
this.register("tool", "Execute tools manually", toolCommand);
|
|
3643
|
+
this.register("status", "Show system status", statusCommand);
|
|
3644
|
+
this.register("sandbox", "Toggle sandbox mode", sandboxCommand);
|
|
3645
|
+
this.register("mode", "Set execution mode (auto/plan/safe)", modeCommand);
|
|
3646
|
+
this.register("task", "View/manage current task", taskCommand);
|
|
3647
|
+
this.register("undo", "Undo recent file changes", undoCommand);
|
|
3648
|
+
this.register("config", "View/edit configuration", configCommand);
|
|
3649
|
+
this.register("doctor", "Run system diagnostics", doctorCommand);
|
|
3650
|
+
this.register("settings", "View/edit settings (mode, permissions, ui)", settingsCommand);
|
|
3651
|
+
}
|
|
3652
|
+
register(name, description, handler) {
|
|
3653
|
+
this.commands.set(name, { name, description, handler });
|
|
3654
|
+
}
|
|
3655
|
+
has(name) {
|
|
3656
|
+
return this.commands.has(name);
|
|
3657
|
+
}
|
|
3658
|
+
async execute(name, args) {
|
|
3659
|
+
const cmd = this.commands.get(name);
|
|
3660
|
+
if (!cmd) {
|
|
3661
|
+
bus.emitAgent({
|
|
3662
|
+
type: "error",
|
|
3663
|
+
message: `Unknown command: /${name}`
|
|
3664
|
+
});
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
try {
|
|
3668
|
+
await cmd.handler(args);
|
|
3669
|
+
} catch (error) {
|
|
3670
|
+
bus.emitAgent({
|
|
3671
|
+
type: "error",
|
|
3672
|
+
message: `Command /${name} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3673
|
+
});
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
var commands = new CommandRegistry();
|
|
3678
|
+
|
|
3679
|
+
// src/agents/supervisor.ts
|
|
3680
|
+
var Supervisor = class {
|
|
3681
|
+
constructor() {
|
|
3682
|
+
this.setupListeners();
|
|
3683
|
+
}
|
|
3684
|
+
setupListeners() {
|
|
3685
|
+
bus.on("user", async (event) => {
|
|
3686
|
+
if (event.type === "user_input") {
|
|
3687
|
+
await this.handleInput(event.content);
|
|
3688
|
+
}
|
|
3689
|
+
if (event.type === "approval_response") {
|
|
3690
|
+
await agent.handleApprovalResponse(event.approved, event.requestId);
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
async handleInput(input) {
|
|
3695
|
+
if (input.startsWith("/")) {
|
|
3696
|
+
const [cmdName, ...args] = input.slice(1).split(" ");
|
|
3697
|
+
if (commands.has(cmdName)) {
|
|
3698
|
+
await commands.execute(cmdName, args);
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
bus.emitAgent({
|
|
3702
|
+
type: "error",
|
|
3703
|
+
message: `Unknown command: /${cmdName}. Try /help`
|
|
3704
|
+
});
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
await agent.run(input);
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
var supervisor = new Supervisor();
|
|
3711
|
+
|
|
3712
|
+
// src/index.ts
|
|
3713
|
+
async function main() {
|
|
3714
|
+
process.stdout.write("\x1B[?1049h");
|
|
3715
|
+
process.stdout.write("\x1Bc");
|
|
3716
|
+
const { waitUntilExit, cleanup } = render(React7.createElement(Root), {
|
|
3717
|
+
patchConsole: false,
|
|
3718
|
+
exitOnCtrlC: true
|
|
3719
|
+
});
|
|
3720
|
+
try {
|
|
3721
|
+
await waitUntilExit();
|
|
3722
|
+
} catch (error) {
|
|
3723
|
+
console.error("Runtime Error:", error);
|
|
3724
|
+
} finally {
|
|
3725
|
+
process.stdout.write("\x1B[?1049l");
|
|
3726
|
+
process.exit(0);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (!supervisor) {
|
|
3730
|
+
console.error("Fatal: Supervisor failed to initialize");
|
|
3731
|
+
process.exit(1);
|
|
3732
|
+
}
|
|
3733
|
+
main().catch((err) => {
|
|
3734
|
+
console.error("Fatal Error:", err);
|
|
3735
|
+
process.exit(1);
|
|
3736
|
+
});
|