@buildaharness/aielia 0.3.1 → 0.3.2
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/README.md +30 -7
- package/dist/agent-loop.d.ts +8 -8
- package/dist/ask-clarification-service.d.ts +1 -1
- package/dist/ask-mode-flag.d.ts +1 -1
- package/dist/assistant-session.d.ts +1 -1
- package/dist/assistant-types.d.ts +3 -3
- package/dist/assistant.d.ts +12 -12
- package/dist/batch-research-fixtures.d.ts +1 -1
- package/dist/benchmark-injected-failure.d.ts +1 -1
- package/dist/bin-CqkqT6KK.js +2107 -0
- package/dist/bin-CqkqT6KK.js.map +1 -0
- package/dist/bin-sea-bootstrap.d.ts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/claude-cli-llm-client.d.ts +2 -2
- package/dist/cli-args.d.ts +20 -0
- package/dist/cli-icons.d.ts +28 -0
- package/dist/{cli-session-D0uLdT2A.js → cli-session-BiX2DOni.js} +550 -121
- package/dist/cli-session-BiX2DOni.js.map +1 -0
- package/dist/cli.d.ts +31 -1
- package/dist/cli.js +1 -1691
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -6
- package/dist/contradiction-checker.d.ts +1 -1
- package/dist/diff-format.d.ts +16 -0
- package/dist/execution-mode.d.ts +1 -1
- package/dist/fact-extraction.d.ts +4 -4
- package/dist/failure-mode-matcher.d.ts +1 -1
- package/dist/file-tools-mcp-server.mjs +2 -2
- package/dist/file-tools.d.ts +10 -2
- package/dist/harness-bridge.d.ts +4 -4
- package/dist/index.js +68 -68
- package/dist/index.js.map +1 -1
- package/dist/ink-select-prompt.d.ts +21 -0
- package/dist/markdown-line.d.ts +42 -0
- package/dist/mcp-server-asset.d.ts +24 -0
- package/dist/memory-service.d.ts +2 -2
- package/dist/network-containment.d.ts +1 -1
- package/dist/node-display-names.d.ts +1 -1
- package/dist/one-loop-flag.d.ts +1 -1
- package/dist/plan-drafting-service.d.ts +1 -1
- package/dist/plan-drafting.d.ts +4 -4
- package/dist/plan-mode-flag.d.ts +1 -1
- package/dist/plan-sketch-service.d.ts +1 -1
- package/dist/plan-sketch.d.ts +1 -1
- package/dist/plan-store.d.ts +2 -2
- package/dist/plan-templates/index.d.ts +1 -1
- package/dist/plan-verification.d.ts +1 -1
- package/dist/scripted-llm-client.d.ts +1 -1
- package/dist/sea-cache.d.ts +9 -0
- package/dist/self-update.d.ts +101 -0
- package/dist/shell-tools.d.ts +1 -1
- package/dist/spike.js.map +1 -1
- package/dist/tool-control-plane.d.ts +1 -1
- package/dist/tool-policy.d.ts +1 -1
- package/dist/tool-step.d.ts +10 -0
- package/dist/trace-events.d.ts +5 -5
- package/dist/trust-tagging.d.ts +1 -1
- package/dist/{tui-app-DxLPSGh2.js → tui-app-4_TrzESz.js} +176 -20
- package/dist/tui-app-4_TrzESz.js.map +1 -0
- package/dist/tui-app.d.ts +34 -9
- package/dist/tui-mode-flag.d.ts +1 -1
- package/dist/turn-intent-classifier.d.ts +5 -5
- package/dist/turn-policy.d.ts +1 -1
- package/dist/update-check-flag.d.ts +11 -0
- package/dist/version.d.ts +6 -0
- package/package.json +7 -3
- package/dist/cli-session-D0uLdT2A.js.map +0 -1
- package/dist/tui-app-DxLPSGh2.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,1693 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
-
import { createInterface } from "node:readline";
|
|
6
|
-
import { rename, stat, realpath, readdir, mkdir, unlink, writeFile, readFile } from "node:fs/promises";
|
|
7
|
-
import { realpathSync } from "node:fs";
|
|
8
|
-
import { tmpdir, homedir } from "node:os";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
-
import { OPENROUTER_DEFAULT_MODEL, OPENAI_DEFAULT_MODEL, ANTHROPIC_DEFAULT_MODEL, InMemoryReminderStore, FileSystemAdapter, FileSystemExperienceStore, LLMClient, OpenAICompatibleLLMClient, OPENROUTER_EXTRA_HEADERS, OPENROUTER_BASE_URL, OPENAI_BASE_URL, AnthropicLLMClient } from "@buildaharness/runtime";
|
|
12
|
-
import { w as buildClaudePrompt, aD as stripJsonCodeFence, a as ALREADY_STAGED_ACTION_TOOL, aC as stagedActionInput, aq as parseClaudeCliOutput, aE as stripMcpToolPrefix, U as envOverridesFromProcessEnv, av as resolveConfig, aA as shouldLaunchTuiApp, aH as validateConfig, c as ConfigValidationError, m as PersonalAssistant, a9 as formatSpendCapStatus, v as braveSearch, M as createSmtpSender, K as createResendSender, y as classifyError, am as nodeToLayer, L as LAYER_DISPLAY_NAME, al as nodeDisplayName, V as estimateCostUsd, $ as formatConfigListing, ad as isConfigKey, C as CONFIG_KEYS, E as ENV_VAR_FOR_CONFIG_KEY, ar as parseConfigValue, d as ConfigValueParseError, a1 as formatDoctorReport, a0 as formatCostSummary, a8 as formatSearchResults, ac as formatUndoLogListing, N as defaultExportFilename, ab as formatTranscriptMarkdown, aa as formatStatus, a3 as formatHelp, j as LAYER_ORDER, x as buildWhyChain, k as LAYER_SHORT_CODE, O as defaultMemoryExportFilename, a4 as formatMemoryExport, a5 as formatMemoryPendingOutcome, a6 as formatMemorySummary } from "./cli-session-D0uLdT2A.js";
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
|
-
import { createServer, connect } from "node:net";
|
|
15
|
-
import { randomBytes } from "node:crypto";
|
|
16
|
-
function isEnoent$1(err) {
|
|
17
|
-
return (err == null ? void 0 : err.code) === "ENOENT";
|
|
18
|
-
}
|
|
19
|
-
function createNodeFsBackend() {
|
|
20
|
-
return {
|
|
21
|
-
async readTextFile(path) {
|
|
22
|
-
try {
|
|
23
|
-
return await readFile(path, "utf-8");
|
|
24
|
-
} catch (err) {
|
|
25
|
-
if (isEnoent$1(err)) return void 0;
|
|
26
|
-
throw err;
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
async writeTextFile(path, contents) {
|
|
30
|
-
await writeFile(path, contents, "utf-8");
|
|
31
|
-
},
|
|
32
|
-
async removeFile(path) {
|
|
33
|
-
try {
|
|
34
|
-
await unlink(path);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
if (!isEnoent$1(err)) throw err;
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
async mkdir(path) {
|
|
40
|
-
await mkdir(path, { recursive: true });
|
|
41
|
-
},
|
|
42
|
-
async readDir(path) {
|
|
43
|
-
try {
|
|
44
|
-
return await readdir(path);
|
|
45
|
-
} catch (err) {
|
|
46
|
-
if (isEnoent$1(err)) return [];
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
async realpath(path) {
|
|
51
|
-
return realpath(path);
|
|
52
|
-
},
|
|
53
|
-
async stat(path) {
|
|
54
|
-
try {
|
|
55
|
-
const info = await stat(path);
|
|
56
|
-
return { isDirectory: info.isDirectory(), size: info.size };
|
|
57
|
-
} catch (err) {
|
|
58
|
-
if (isEnoent$1(err)) return void 0;
|
|
59
|
-
throw err;
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
async rename(from, to) {
|
|
63
|
-
await rename(from, to);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
const EMPTY_MCP_CONFIG = JSON.stringify({ mcpServers: {} });
|
|
68
|
-
function invokeClaude(claudePath, args) {
|
|
69
|
-
return new Promise((resolvePromise, reject) => {
|
|
70
|
-
const proc = spawn(claudePath, args, { cwd: tmpdir(), stdio: ["ignore", "pipe", "pipe"] });
|
|
71
|
-
let stdout = "";
|
|
72
|
-
let stderr = "";
|
|
73
|
-
proc.stdout.on("data", (chunk) => {
|
|
74
|
-
stdout += chunk;
|
|
75
|
-
});
|
|
76
|
-
proc.stderr.on("data", (chunk) => {
|
|
77
|
-
stderr += chunk;
|
|
78
|
-
});
|
|
79
|
-
proc.on("error", reject);
|
|
80
|
-
proc.on("close", (code) => {
|
|
81
|
-
if (code !== 0) {
|
|
82
|
-
reject(new Error(stderr.trim() || `claude exited with code ${code}`));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
resolvePromise(parseClaudeCliOutput(stdout));
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
function invokeClaudeStreaming(claudePath, args, onToolStep) {
|
|
90
|
-
return new Promise((resolvePromise, reject) => {
|
|
91
|
-
const proc = spawn(claudePath, args, { cwd: tmpdir(), stdio: ["ignore", "pipe", "pipe"] });
|
|
92
|
-
let buffer = "";
|
|
93
|
-
let stderr = "";
|
|
94
|
-
let finalResultLine;
|
|
95
|
-
proc.stdout.on("data", (chunk) => {
|
|
96
|
-
var _a;
|
|
97
|
-
buffer += chunk.toString("utf-8");
|
|
98
|
-
let newlineIndex;
|
|
99
|
-
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
100
|
-
const line = buffer.slice(0, newlineIndex);
|
|
101
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
102
|
-
if (!line.trim()) continue;
|
|
103
|
-
let event;
|
|
104
|
-
try {
|
|
105
|
-
event = JSON.parse(line);
|
|
106
|
-
} catch {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (event.type === "assistant" && onToolStep) {
|
|
110
|
-
for (const block of ((_a = event.message) == null ? void 0 : _a.content) ?? []) {
|
|
111
|
-
if (block.type !== "tool_use") continue;
|
|
112
|
-
const toolUse = block;
|
|
113
|
-
onToolStep({ tool: stripMcpToolPrefix(toolUse.name), input: toolUse.input ?? {} });
|
|
114
|
-
}
|
|
115
|
-
} else if (event.type === "result") {
|
|
116
|
-
finalResultLine = line;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
proc.stderr.on("data", (chunk) => {
|
|
121
|
-
stderr += chunk;
|
|
122
|
-
});
|
|
123
|
-
proc.on("error", reject);
|
|
124
|
-
proc.on("close", (code) => {
|
|
125
|
-
if (code !== 0) {
|
|
126
|
-
reject(new Error(stderr.trim() || `claude exited with code ${code}`));
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
resolvePromise(parseClaudeCliOutput(finalResultLine ?? ""));
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
function startToolGateServer(onToolProposal) {
|
|
134
|
-
return new Promise((resolvePromise, reject) => {
|
|
135
|
-
const server = createServer((socket) => {
|
|
136
|
-
let buffer = "";
|
|
137
|
-
socket.on("data", (chunk) => {
|
|
138
|
-
buffer += chunk.toString("utf-8");
|
|
139
|
-
let newlineIndex;
|
|
140
|
-
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
141
|
-
const line = buffer.slice(0, newlineIndex);
|
|
142
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
143
|
-
if (!line.trim()) continue;
|
|
144
|
-
void (async (requestLine) => {
|
|
145
|
-
let request;
|
|
146
|
-
try {
|
|
147
|
-
request = JSON.parse(requestLine);
|
|
148
|
-
} catch {
|
|
149
|
-
socket.write(`${JSON.stringify({ decision: "allow" })}
|
|
150
|
-
`);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
let decision;
|
|
154
|
-
try {
|
|
155
|
-
decision = onToolProposal && typeof request.tool === "string" ? await onToolProposal(request.tool, request.input ?? {}) : { decision: "allow" };
|
|
156
|
-
} catch (err) {
|
|
157
|
-
console.error("claude-cli tool gate: onToolProposal threw — failing open:", err);
|
|
158
|
-
decision = { decision: "allow" };
|
|
159
|
-
}
|
|
160
|
-
socket.write(`${JSON.stringify(decision)}
|
|
161
|
-
`);
|
|
162
|
-
})(line);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
server.on("error", reject);
|
|
167
|
-
server.listen(0, "127.0.0.1", () => {
|
|
168
|
-
const address = server.address();
|
|
169
|
-
if (address === null || typeof address === "string") {
|
|
170
|
-
reject(new Error("tool gate server failed to bind to a loopback TCP port"));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
resolvePromise({ server, port: address.port });
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
class ClaudeCliLLMClient {
|
|
178
|
-
constructor(options = {}) {
|
|
179
|
-
__publicField(this, "claudePath");
|
|
180
|
-
__publicField(this, "fileTools");
|
|
181
|
-
__publicField(this, "remindersFile");
|
|
182
|
-
__publicField(this, "shellTools");
|
|
183
|
-
__publicField(this, "webTools");
|
|
184
|
-
__publicField(this, "actionTools");
|
|
185
|
-
__publicField(this, "model");
|
|
186
|
-
/**
|
|
187
|
-
* The concrete model id the last `claude -p` call actually reported running against (read from
|
|
188
|
-
* the response's own `model` field, not the CLI alias). `undefined` until a call resolves one.
|
|
189
|
-
* Plan A1 records this into the benchmark report so a published table can name the model.
|
|
190
|
-
*/
|
|
191
|
-
__publicField(this, "resolvedModelId");
|
|
192
|
-
this.claudePath = options.claudePath ?? process.env.CLAUDE_PATH ?? "claude";
|
|
193
|
-
this.fileTools = options.fileTools;
|
|
194
|
-
this.remindersFile = options.remindersFile;
|
|
195
|
-
this.shellTools = options.shellTools;
|
|
196
|
-
this.webTools = options.webTools;
|
|
197
|
-
this.actionTools = options.actionTools;
|
|
198
|
-
this.model = options.model;
|
|
199
|
-
}
|
|
200
|
-
async *callChat(messages, options = {}) {
|
|
201
|
-
yield await this.callChatSync(messages, options);
|
|
202
|
-
}
|
|
203
|
-
async callChatSync(messages, options = {}) {
|
|
204
|
-
var _a;
|
|
205
|
-
const { systemPrompt, prompt } = buildClaudePrompt(messages);
|
|
206
|
-
const args = [
|
|
207
|
-
"--print",
|
|
208
|
-
"--output-format",
|
|
209
|
-
"json",
|
|
210
|
-
"--tools",
|
|
211
|
-
"",
|
|
212
|
-
"--no-session-persistence",
|
|
213
|
-
"--system-prompt",
|
|
214
|
-
systemPrompt,
|
|
215
|
-
"--mcp-config",
|
|
216
|
-
EMPTY_MCP_CONFIG,
|
|
217
|
-
"--strict-mcp-config"
|
|
218
|
-
// ignore any ambient project/user MCP config — see EMPTY_MCP_CONFIG's doc comment
|
|
219
|
-
];
|
|
220
|
-
const model = options.model ?? this.model;
|
|
221
|
-
if (model) args.push("--model", model);
|
|
222
|
-
args.push(prompt);
|
|
223
|
-
const { reply, usage, model: resolved } = await invokeClaude(this.claudePath, args);
|
|
224
|
-
if (resolved) this.resolvedModelId = resolved;
|
|
225
|
-
if (usage) (_a = options.onUsage) == null ? void 0 : _a.call(options, usage);
|
|
226
|
-
return reply;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* When `tools` is non-empty and `fileTools`/`shellTools` was configured, wires the
|
|
230
|
-
* file-tools MCP server into a single `claude -p` call and lets Claude Code's own
|
|
231
|
-
* agentic loop call read_file/list_directory/write_file/fetch_url/create_reminder/
|
|
232
|
-
* list_reminders/run_shell_command autonomously — we don't get to intercept each
|
|
233
|
-
* call the way the proxy backend's manual tool loop does (see
|
|
234
|
-
* plans/personal_assistant_file_tools_plan.html, T6). fetch_url is always
|
|
235
|
-
* registered on that server; create_reminder/list_reminders only when
|
|
236
|
-
* `remindersFile` is set; run_shell_command only when `shellTools` is set;
|
|
237
|
-
* web_search only when `webTools` is set (Brave Search — see the `webTools`
|
|
238
|
-
* option). Three
|
|
239
|
-
* possible outcomes: a final text reply (no tool call this backend needs to
|
|
240
|
-
* surface), a write or shell command staged by the MCP server mid-call (surfaced
|
|
241
|
-
* as a synthetic `__staged_action` tool call so assistant.ts's tool loop treats it
|
|
242
|
-
* the same as a manually staged action without staging it a second time), or —
|
|
243
|
-
* for fetch_url/create_reminder/list_reminders — the tool's result text folded
|
|
244
|
-
* directly into Claude Code's own reply, since (unlike the proxy backend's
|
|
245
|
-
* `executeToolCall`) there's no outer loop here to intercept each call and
|
|
246
|
-
* re-apply trust-tagging itself; the MCP server tags fetch_url's result before
|
|
247
|
-
* Claude Code ever sees it instead (see file-tools-mcp-server.mjs).
|
|
248
|
-
*/
|
|
249
|
-
async callChatStructured(messages, tools, options = {}) {
|
|
250
|
-
var _a, _b, _c, _d, _e, _f;
|
|
251
|
-
if (!tools || tools.length === 0) {
|
|
252
|
-
const content = await this.callChatSync(messages, options);
|
|
253
|
-
return { content: options.structuredOutput ? stripJsonCodeFence(content) : content };
|
|
254
|
-
}
|
|
255
|
-
if (!this.fileTools && !this.shellTools && !this.webTools && !this.actionTools) {
|
|
256
|
-
throw new Error(
|
|
257
|
-
"ClaudeCliLLMClient does not support tool calls unless constructed with fileTools, shellTools, webTools, or actionTools configured"
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
const { systemPrompt, prompt } = buildClaudePrompt(messages);
|
|
261
|
-
const workspaceRoot = ((_a = this.fileTools) == null ? void 0 : _a.workspaceRoot) ?? ((_b = this.shellTools) == null ? void 0 : _b.workspaceRoot) ?? ((_c = this.actionTools) == null ? void 0 : _c.workspaceRoot) ?? tmpdir();
|
|
262
|
-
const lastUserMessage = ((_d = [...messages].reverse().find((m) => m.role === "user")) == null ? void 0 : _d.content) ?? "";
|
|
263
|
-
const mcpServerFileName = "file-tools-mcp-server.mjs";
|
|
264
|
-
const mcpServerPath = fileURLToPath(new URL(mcpServerFileName, import.meta.url));
|
|
265
|
-
const gate = await startToolGateServer(options.onToolProposal);
|
|
266
|
-
try {
|
|
267
|
-
const mcpConfig = JSON.stringify({
|
|
268
|
-
mcpServers: {
|
|
269
|
-
"file-tools": {
|
|
270
|
-
command: "node",
|
|
271
|
-
args: [mcpServerPath],
|
|
272
|
-
env: {
|
|
273
|
-
WORKSPACE_ROOT: workspaceRoot,
|
|
274
|
-
TOOL_GATE_PORT: String(gate.port),
|
|
275
|
-
...this.remindersFile ? { REMINDERS_FILE: this.remindersFile, CURRENT_USER_MESSAGE: lastUserMessage } : {},
|
|
276
|
-
...this.shellTools ? { ENABLE_SHELL_TOOLS: "1" } : {},
|
|
277
|
-
...((_e = this.webTools) == null ? void 0 : _e.braveApiKey) ? { BRAVE_SEARCH_API_KEY: this.webTools.braveApiKey } : {},
|
|
278
|
-
...this.actionTools ? { ENABLE_EMAIL_TOOL: "1" } : {}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
const args = [
|
|
284
|
-
"--print",
|
|
285
|
-
"--output-format",
|
|
286
|
-
"stream-json",
|
|
287
|
-
// streamed (not the single-object 'json') so tool_use events can be reported live via onToolStep — see invokeClaudeStreaming
|
|
288
|
-
"--verbose",
|
|
289
|
-
// required by --print when --output-format is stream-json
|
|
290
|
-
"--tools",
|
|
291
|
-
"",
|
|
292
|
-
// still disable Claude Code's own built-in Read/Write/Bash tools — Bash is never added, regardless of shellTools
|
|
293
|
-
"--no-session-persistence",
|
|
294
|
-
"--system-prompt",
|
|
295
|
-
systemPrompt,
|
|
296
|
-
"--mcp-config",
|
|
297
|
-
mcpConfig,
|
|
298
|
-
"--strict-mcp-config",
|
|
299
|
-
// ignore any ambient project .mcp.json — the tool surface must be exactly this plan's tools
|
|
300
|
-
"--dangerously-skip-permissions"
|
|
301
|
-
// headless -p mode has no way to answer an interactive tool-permission prompt
|
|
302
|
-
];
|
|
303
|
-
const model = options.model ?? this.model;
|
|
304
|
-
if (model) args.push("--model", model);
|
|
305
|
-
args.push(prompt);
|
|
306
|
-
const callStartedAt = Date.now();
|
|
307
|
-
const { reply, usage, model: resolved } = await invokeClaudeStreaming(this.claudePath, args, options.onToolStep);
|
|
308
|
-
if (resolved) this.resolvedModelId = resolved;
|
|
309
|
-
if (usage) (_f = options.onUsage) == null ? void 0 : _f.call(options, usage);
|
|
310
|
-
const staged = await this.findPendingActionStagedSince(workspaceRoot, callStartedAt);
|
|
311
|
-
if (staged) {
|
|
312
|
-
return {
|
|
313
|
-
content: "",
|
|
314
|
-
toolCalls: [{ id: `cli-staged-${staged.id}`, name: ALREADY_STAGED_ACTION_TOOL, input: stagedActionInput(staged) }]
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
return { content: reply };
|
|
318
|
-
} finally {
|
|
319
|
-
gate.server.close();
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/** Diffs .pending-actions/ against the call's start time to detect a write or shell command the MCP server staged during this subprocess call. */
|
|
323
|
-
/**
|
|
324
|
-
* When the MCP server staged more than one action this call (e.g. "run X AND write Y" —
|
|
325
|
-
* see file-tools-mcp-server.mjs's stagePendingAction doc comment), every one of them lands
|
|
326
|
-
* in `.pending-actions/` with an mtime after `startTimeMs`, in `readdir`-arbitrary order —
|
|
327
|
-
* not necessarily staging order. Must return the *chain head* (earliest `stagedAt`)
|
|
328
|
-
* specifically, since resolvePendingAction (assistant.ts) walks `nextPendingActionId`
|
|
329
|
-
* forward from whichever record it's handed; returning a later link here would surface the
|
|
330
|
-
* chain out of order and orphan the earlier, still-unresolved action.
|
|
331
|
-
*/
|
|
332
|
-
async findPendingActionStagedSince(workspaceRoot, startTimeMs) {
|
|
333
|
-
const dir = `${workspaceRoot}/.pending-actions`;
|
|
334
|
-
let names;
|
|
335
|
-
try {
|
|
336
|
-
names = await readdir(dir);
|
|
337
|
-
} catch {
|
|
338
|
-
return void 0;
|
|
339
|
-
}
|
|
340
|
-
let earliest;
|
|
341
|
-
for (const name of names) {
|
|
342
|
-
if (!name.endsWith(".json")) continue;
|
|
343
|
-
const filePath = `${dir}/${name}`;
|
|
344
|
-
const stats = await stat(filePath);
|
|
345
|
-
if (stats.mtimeMs < startTimeMs - 1e3) continue;
|
|
346
|
-
const record = JSON.parse(await readFile(filePath, "utf-8"));
|
|
347
|
-
if (!earliest || record.stagedAt < earliest.stagedAt) earliest = record;
|
|
348
|
-
}
|
|
349
|
-
return earliest;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
const MAX_HEADER_BYTES = 16384;
|
|
353
|
-
function hostAllowed(host, allowlist) {
|
|
354
|
-
const normalized = host.toLowerCase();
|
|
355
|
-
return allowlist.some((entry) => {
|
|
356
|
-
const allowed = entry.toLowerCase();
|
|
357
|
-
return normalized === allowed || normalized.endsWith(`.${allowed}`);
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
function parseConnectTarget(requestLine) {
|
|
361
|
-
const match = /^CONNECT\s+([^:\s]+):(\d+)\s+HTTP/i.exec(requestLine);
|
|
362
|
-
return match ? { host: match[1], port: Number(match[2]) } : null;
|
|
363
|
-
}
|
|
364
|
-
function parsePlainHttpTarget(requestLine, headerText) {
|
|
365
|
-
const absoluteUri = /^[A-Z]+\s+https?:\/\/([^/:\s]+)(?::(\d+))?/i.exec(requestLine);
|
|
366
|
-
if (absoluteUri) return { host: absoluteUri[1], port: Number(absoluteUri[2] ?? 80) };
|
|
367
|
-
const hostHeader = /^host:\s*([^\s:]+)(?::(\d+))?/im.exec(headerText);
|
|
368
|
-
return hostHeader ? { host: hostHeader[1], port: Number(hostHeader[2] ?? 80) } : null;
|
|
369
|
-
}
|
|
370
|
-
function handleConnection(clientSocket, allowlist) {
|
|
371
|
-
let buffered = Buffer.alloc(0);
|
|
372
|
-
const onData = (chunk) => {
|
|
373
|
-
buffered = Buffer.concat([buffered, chunk]);
|
|
374
|
-
const headerEnd = buffered.indexOf("\r\n\r\n");
|
|
375
|
-
if (headerEnd === -1) {
|
|
376
|
-
if (buffered.length > MAX_HEADER_BYTES) clientSocket.destroy();
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
clientSocket.removeListener("data", onData);
|
|
380
|
-
const headerText = buffered.slice(0, headerEnd).toString("utf-8");
|
|
381
|
-
const requestLine = headerText.split("\r\n")[0] ?? "";
|
|
382
|
-
const isConnect = /^CONNECT\s/i.test(requestLine);
|
|
383
|
-
const target = isConnect ? parseConnectTarget(requestLine) : parsePlainHttpTarget(requestLine, headerText);
|
|
384
|
-
if (!target || !hostAllowed(target.host, allowlist)) {
|
|
385
|
-
clientSocket.end("HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n");
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const upstream = connect(target.port, target.host, () => {
|
|
389
|
-
if (isConnect) {
|
|
390
|
-
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
391
|
-
const tunnelBytes = buffered.slice(headerEnd + 4);
|
|
392
|
-
if (tunnelBytes.length > 0) upstream.write(tunnelBytes);
|
|
393
|
-
} else {
|
|
394
|
-
upstream.write(buffered);
|
|
395
|
-
}
|
|
396
|
-
clientSocket.pipe(upstream);
|
|
397
|
-
upstream.pipe(clientSocket);
|
|
398
|
-
});
|
|
399
|
-
upstream.on("error", () => clientSocket.destroy());
|
|
400
|
-
};
|
|
401
|
-
clientSocket.on("data", onData);
|
|
402
|
-
clientSocket.on("error", () => clientSocket.destroy());
|
|
403
|
-
}
|
|
404
|
-
function startProxyServer(allowlist) {
|
|
405
|
-
return new Promise((resolveStart, rejectStart) => {
|
|
406
|
-
const server = createServer((socket) => handleConnection(socket, allowlist));
|
|
407
|
-
server.on("error", rejectStart);
|
|
408
|
-
server.listen(0, "127.0.0.1", () => {
|
|
409
|
-
const address = server.address();
|
|
410
|
-
if (address === null || typeof address === "string") {
|
|
411
|
-
rejectStart(new Error("Failed to bind network containment proxy to a loopback port"));
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
resolveStart({
|
|
415
|
-
port: address.port,
|
|
416
|
-
close: () => new Promise((resolveClose) => server.close(() => resolveClose()))
|
|
417
|
-
});
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
const proxyCache = /* @__PURE__ */ new Map();
|
|
422
|
-
function getNetworkContainmentProxy(allowlist) {
|
|
423
|
-
const key = JSON.stringify([...allowlist].map((host) => host.toLowerCase()).sort());
|
|
424
|
-
let proxy = proxyCache.get(key);
|
|
425
|
-
if (!proxy) {
|
|
426
|
-
proxy = startProxyServer(allowlist);
|
|
427
|
-
proxyCache.set(key, proxy);
|
|
428
|
-
}
|
|
429
|
-
return proxy;
|
|
430
|
-
}
|
|
431
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
432
|
-
const DEFAULT_MAX_OUTPUT_BYTES = 2e4;
|
|
433
|
-
const ALLOWED_ENV_VARS = ["PATH", "HOME", "LANG"];
|
|
434
|
-
function allowlistedEnv() {
|
|
435
|
-
const env = {};
|
|
436
|
-
for (const key of ALLOWED_ENV_VARS) {
|
|
437
|
-
const value = process.env[key];
|
|
438
|
-
if (value !== void 0) env[key] = value;
|
|
439
|
-
}
|
|
440
|
-
return env;
|
|
441
|
-
}
|
|
442
|
-
async function networkContainmentEnv(networkAllowlist) {
|
|
443
|
-
const proxy = await getNetworkContainmentProxy(networkAllowlist);
|
|
444
|
-
const proxyUrl = `http://127.0.0.1:${proxy.port}`;
|
|
445
|
-
return {
|
|
446
|
-
HTTP_PROXY: proxyUrl,
|
|
447
|
-
http_proxy: proxyUrl,
|
|
448
|
-
HTTPS_PROXY: proxyUrl,
|
|
449
|
-
https_proxy: proxyUrl
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
function truncateOutput(text, maxBytes) {
|
|
453
|
-
const encoded = new TextEncoder().encode(text);
|
|
454
|
-
if (encoded.length <= maxBytes) return text;
|
|
455
|
-
const truncated = new TextDecoder("utf-8", { fatal: false }).decode(encoded.slice(0, maxBytes));
|
|
456
|
-
return `${truncated}
|
|
457
|
-
… (truncated)`;
|
|
458
|
-
}
|
|
459
|
-
const runApprovedShellCommand = async (command, cwd, options = {}) => {
|
|
460
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
461
|
-
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
462
|
-
const containmentEnv = await networkContainmentEnv(options.networkAllowlist ?? []);
|
|
463
|
-
return new Promise((resolvePromise, reject) => {
|
|
464
|
-
var _a, _b;
|
|
465
|
-
const proc = spawn(command, {
|
|
466
|
-
shell: true,
|
|
467
|
-
cwd,
|
|
468
|
-
env: { ...allowlistedEnv(), ...containmentEnv },
|
|
469
|
-
detached: true,
|
|
470
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
471
|
-
});
|
|
472
|
-
let output = "";
|
|
473
|
-
let timedOut = false;
|
|
474
|
-
let settled = false;
|
|
475
|
-
const timer = setTimeout(() => {
|
|
476
|
-
timedOut = true;
|
|
477
|
-
try {
|
|
478
|
-
if (proc.pid) process.kill(-proc.pid, "SIGKILL");
|
|
479
|
-
else proc.kill("SIGKILL");
|
|
480
|
-
} catch {
|
|
481
|
-
proc.kill("SIGKILL");
|
|
482
|
-
}
|
|
483
|
-
}, timeoutMs);
|
|
484
|
-
(_a = proc.stdout) == null ? void 0 : _a.on("data", (chunk) => {
|
|
485
|
-
output += chunk.toString("utf-8");
|
|
486
|
-
});
|
|
487
|
-
(_b = proc.stderr) == null ? void 0 : _b.on("data", (chunk) => {
|
|
488
|
-
output += chunk.toString("utf-8");
|
|
489
|
-
});
|
|
490
|
-
proc.on("error", (err) => {
|
|
491
|
-
if (settled) return;
|
|
492
|
-
settled = true;
|
|
493
|
-
clearTimeout(timer);
|
|
494
|
-
reject(err);
|
|
495
|
-
});
|
|
496
|
-
proc.on("close", (exitCode) => {
|
|
497
|
-
if (settled) return;
|
|
498
|
-
settled = true;
|
|
499
|
-
clearTimeout(timer);
|
|
500
|
-
resolvePromise({
|
|
501
|
-
output: truncateOutput(output, maxOutputBytes),
|
|
502
|
-
exitCode: timedOut ? null : exitCode,
|
|
503
|
-
timedOut
|
|
504
|
-
});
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
};
|
|
508
|
-
function isEnoent(err) {
|
|
509
|
-
return (err == null ? void 0 : err.code) === "ENOENT";
|
|
510
|
-
}
|
|
511
|
-
class NodeConfigStore {
|
|
512
|
-
constructor(path) {
|
|
513
|
-
__publicField(this, "queue", Promise.resolve());
|
|
514
|
-
this.path = path;
|
|
515
|
-
}
|
|
516
|
-
async load() {
|
|
517
|
-
let raw;
|
|
518
|
-
try {
|
|
519
|
-
raw = await readFile(this.path, "utf-8");
|
|
520
|
-
} catch (err) {
|
|
521
|
-
if (isEnoent(err)) return {};
|
|
522
|
-
throw err;
|
|
523
|
-
}
|
|
524
|
-
try {
|
|
525
|
-
return JSON.parse(raw);
|
|
526
|
-
} catch {
|
|
527
|
-
console.error(`Warning: ${this.path} is not valid JSON — ignoring it and falling back to defaults.`);
|
|
528
|
-
return {};
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
save(patch) {
|
|
532
|
-
const task = this.queue.then(() => this.writePatch(patch));
|
|
533
|
-
this.queue = task.catch(() => {
|
|
534
|
-
});
|
|
535
|
-
return task;
|
|
536
|
-
}
|
|
537
|
-
async writePatch(patch) {
|
|
538
|
-
const existing = await this.load();
|
|
539
|
-
const merged = { ...existing, ...patch };
|
|
540
|
-
await mkdir(dirname(this.path), { recursive: true });
|
|
541
|
-
const tempPath = `${this.path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
542
|
-
await writeFile(tempPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
543
|
-
await rename(tempPath, this.path);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
const DOCTOR_CHECK_TIMEOUT_MS = 3e3;
|
|
547
|
-
async function checkProxyHealth(proxyUrl) {
|
|
548
|
-
const label = `proxy reachable (${proxyUrl}/health)`;
|
|
549
|
-
const controller = new AbortController();
|
|
550
|
-
const timeout = setTimeout(() => controller.abort(), DOCTOR_CHECK_TIMEOUT_MS);
|
|
551
|
-
try {
|
|
552
|
-
const res = await fetch(`${proxyUrl}/health`, { signal: controller.signal });
|
|
553
|
-
if (!res.ok) return { label, ok: false, detail: `HTTP ${res.status}` };
|
|
554
|
-
const body = await res.json().catch(() => void 0);
|
|
555
|
-
return (body == null ? void 0 : body.status) === "ok" ? { label, ok: true } : { label, ok: false, detail: "unexpected response body" };
|
|
556
|
-
} catch (err) {
|
|
557
|
-
const timedOut = err instanceof Error && err.name === "AbortError";
|
|
558
|
-
return { label, ok: false, detail: timedOut ? "timed out" : err instanceof Error ? err.message : String(err) };
|
|
559
|
-
} finally {
|
|
560
|
-
clearTimeout(timeout);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
function checkClaudeCli(claudePath) {
|
|
564
|
-
const label = `claude binary (${claudePath})`;
|
|
565
|
-
return new Promise((resolvePromise) => {
|
|
566
|
-
let settled = false;
|
|
567
|
-
const proc = spawn(claudePath, ["--version"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
568
|
-
const timeout = setTimeout(() => {
|
|
569
|
-
if (settled) return;
|
|
570
|
-
settled = true;
|
|
571
|
-
proc.kill();
|
|
572
|
-
resolvePromise({ label, ok: false, detail: "timed out" });
|
|
573
|
-
}, DOCTOR_CHECK_TIMEOUT_MS);
|
|
574
|
-
proc.on("error", () => {
|
|
575
|
-
if (settled) return;
|
|
576
|
-
settled = true;
|
|
577
|
-
clearTimeout(timeout);
|
|
578
|
-
resolvePromise({ label, ok: false, detail: "not found" });
|
|
579
|
-
});
|
|
580
|
-
proc.on("close", (code) => {
|
|
581
|
-
if (settled) return;
|
|
582
|
-
settled = true;
|
|
583
|
-
clearTimeout(timeout);
|
|
584
|
-
resolvePromise({ label, ok: code === 0, detail: code === 0 ? void 0 : `exited with code ${code}` });
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
async function checkWorkspaceRoot(workspaceRoot) {
|
|
589
|
-
const label = `workspace root exists (${workspaceRoot})`;
|
|
590
|
-
try {
|
|
591
|
-
const info = await stat(workspaceRoot);
|
|
592
|
-
return info.isDirectory() ? { label, ok: true } : { label, ok: false, detail: "not a directory" };
|
|
593
|
-
} catch {
|
|
594
|
-
return { label, ok: false, detail: "not found" };
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
async function checkDataDirWritable(backend, dataDir) {
|
|
598
|
-
const label = `data dir writable (${dataDir})`;
|
|
599
|
-
const probePath = join(dataDir, ".doctor-check");
|
|
600
|
-
try {
|
|
601
|
-
await backend.mkdir(dataDir);
|
|
602
|
-
await backend.writeTextFile(probePath, "ok");
|
|
603
|
-
await backend.removeFile(probePath);
|
|
604
|
-
return { label, ok: true };
|
|
605
|
-
} catch (err) {
|
|
606
|
-
return { label, ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
function resolveNonInteractiveApprovalMode(env) {
|
|
610
|
-
const raw = env.ASSISTANT_NON_INTERACTIVE_APPROVAL;
|
|
611
|
-
if (raw === void 0) return void 0;
|
|
612
|
-
if (raw === "decline" || raw === "require-tty") return raw;
|
|
613
|
-
console.error(`[warning] ASSISTANT_NON_INTERACTIVE_APPROVAL="${raw}" is not "decline" or "require-tty" — ignoring.`);
|
|
614
|
-
return void 0;
|
|
615
|
-
}
|
|
616
|
-
function alreadyConfigured(deps) {
|
|
617
|
-
const { persisted, overriddenKeys } = deps;
|
|
618
|
-
if (persisted.llmBackend !== void 0) return true;
|
|
619
|
-
if (persisted.apiKey !== void 0 || persisted.authToken !== void 0) return true;
|
|
620
|
-
for (const key of ["llmBackend", "apiKey", "authToken", "proxyUrl"]) {
|
|
621
|
-
if (overriddenKeys.has(key)) return true;
|
|
622
|
-
}
|
|
623
|
-
return false;
|
|
624
|
-
}
|
|
625
|
-
const PROVIDER_CHOICES = {
|
|
626
|
-
"1": { backend: "anthropic", label: "Anthropic (Claude) — needs an sk-ant-… key" },
|
|
627
|
-
"2": { backend: "openai", label: "OpenAI — needs an sk-… key" },
|
|
628
|
-
"3": { backend: "openrouter", label: "OpenRouter — needs an sk-or-… key" }
|
|
629
|
-
};
|
|
630
|
-
async function maybeRunFirstRunSetup(deps) {
|
|
631
|
-
const { configStore, persisted, isInteractive, ask, detectClaudeCli, log } = deps;
|
|
632
|
-
if (alreadyConfigured(deps)) return persisted;
|
|
633
|
-
if (!isInteractive) return persisted;
|
|
634
|
-
log("");
|
|
635
|
-
log("Welcome to Aielia — this looks like your first run. Let's pick how to reach a model.");
|
|
636
|
-
log("(You can change any of this later with /config, or skip with Ctrl+C.)");
|
|
637
|
-
log("");
|
|
638
|
-
const patch = {};
|
|
639
|
-
if (await detectClaudeCli()) {
|
|
640
|
-
const useClaude = await ask(
|
|
641
|
-
"Found an authenticated `claude` CLI on your PATH. Use it? No API key needed. (Y/n) "
|
|
642
|
-
);
|
|
643
|
-
if (useClaude === "" || useClaude.toLowerCase().startsWith("y")) {
|
|
644
|
-
patch.llmBackend = "claude-cli";
|
|
645
|
-
await configStore.save(patch);
|
|
646
|
-
log("");
|
|
647
|
-
log('✓ Using the claude-cli backend. Try "what time zone is Tokyo in?", then');
|
|
648
|
-
log(' "send an email to my boss saying I quit" to see the approval gate.');
|
|
649
|
-
log("");
|
|
650
|
-
return { ...persisted, ...patch };
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
log("Pick a provider to use with your own API key:");
|
|
654
|
-
for (const [key2, { label }] of Object.entries(PROVIDER_CHOICES)) log(` ${key2}) ${label}`);
|
|
655
|
-
const choice = (await ask("Provider [1-3, or Enter to skip]: ")).trim();
|
|
656
|
-
const picked = PROVIDER_CHOICES[choice];
|
|
657
|
-
if (!picked) {
|
|
658
|
-
log("");
|
|
659
|
-
log('Skipped. The assistant will start on the "proxy" backend (needs @buildaharness/proxy');
|
|
660
|
-
log("on :8787). Run /config set llmBackend <anthropic|openai|openrouter> and");
|
|
661
|
-
log("/config set apiKey <key> when you are ready, or set ASSISTANT_LLM_BACKEND + ASSISTANT_API_KEY.");
|
|
662
|
-
log("");
|
|
663
|
-
return persisted;
|
|
664
|
-
}
|
|
665
|
-
const key = (await ask(`Paste your ${picked.backend} API key: `)).trim();
|
|
666
|
-
if (!key) {
|
|
667
|
-
log("");
|
|
668
|
-
log("No key entered — skipping. Set it later with /config set apiKey <key>.");
|
|
669
|
-
log("");
|
|
670
|
-
return persisted;
|
|
671
|
-
}
|
|
672
|
-
patch.llmBackend = picked.backend;
|
|
673
|
-
patch.apiKey = key;
|
|
674
|
-
await configStore.save(patch);
|
|
675
|
-
log("");
|
|
676
|
-
log(`✓ Saved. Using ${picked.backend}. The key is stored in plain text in your config file`);
|
|
677
|
-
log(" (~/.buildaharness/personal-assistant/config.json) — the same trust boundary as a .env.");
|
|
678
|
-
log("");
|
|
679
|
-
return { ...persisted, ...patch };
|
|
680
|
-
}
|
|
681
|
-
const defaultDataDir = join(homedir(), ".buildaharness", "personal-assistant");
|
|
682
|
-
const defaultConfigStore = new NodeConfigStore(join(defaultDataDir, "config.json"));
|
|
683
|
-
const defaultRemindersFile = join(defaultDataDir, "reminders", "reminders.json");
|
|
684
|
-
const defaultEnvOverrides = envOverridesFromProcessEnv(process.env);
|
|
685
|
-
const defaultNonInteractiveApprovalMode = resolveNonInteractiveApprovalMode(process.env);
|
|
686
|
-
const defaultBackend = createNodeFsBackend();
|
|
687
|
-
function buildEmailSender(config) {
|
|
688
|
-
if (!config.enableEmail || !config.emailFrom) return void 0;
|
|
689
|
-
if (config.emailProvider === "smtp" && config.smtpHost && config.smtpPort) {
|
|
690
|
-
return createSmtpSender({
|
|
691
|
-
host: config.smtpHost,
|
|
692
|
-
port: config.smtpPort,
|
|
693
|
-
auth: config.smtpUser && config.smtpPass ? { user: config.smtpUser, pass: config.smtpPass } : void 0,
|
|
694
|
-
from: config.emailFrom
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
if (config.emailProvider === "resend" && config.resendApiKey) {
|
|
698
|
-
return createResendSender({ apiKey: config.resendApiKey, from: config.emailFrom });
|
|
699
|
-
}
|
|
700
|
-
return void 0;
|
|
701
|
-
}
|
|
702
|
-
function buildLlmClient(config, workspaceRoot, remindersFile) {
|
|
703
|
-
switch (config.llmBackend) {
|
|
704
|
-
case "claude-cli":
|
|
705
|
-
return new ClaudeCliLLMClient({
|
|
706
|
-
fileTools: { workspaceRoot },
|
|
707
|
-
remindersFile,
|
|
708
|
-
shellTools: config.enableShell ? { workspaceRoot } : void 0,
|
|
709
|
-
webTools: config.enableWeb ? { braveApiKey: config.braveApiKey } : void 0,
|
|
710
|
-
actionTools: config.enableEmail ? { workspaceRoot } : void 0
|
|
711
|
-
});
|
|
712
|
-
case "anthropic":
|
|
713
|
-
return new AnthropicLLMClient({ apiKey: config.apiKey ?? "" });
|
|
714
|
-
case "openai":
|
|
715
|
-
return new OpenAICompatibleLLMClient({ apiKey: config.apiKey ?? "", baseUrl: OPENAI_BASE_URL, defaultModel: OPENAI_DEFAULT_MODEL });
|
|
716
|
-
case "openrouter":
|
|
717
|
-
return new OpenAICompatibleLLMClient({
|
|
718
|
-
apiKey: config.apiKey ?? "",
|
|
719
|
-
baseUrl: OPENROUTER_BASE_URL,
|
|
720
|
-
defaultModel: OPENROUTER_DEFAULT_MODEL,
|
|
721
|
-
extraHeaders: OPENROUTER_EXTRA_HEADERS
|
|
722
|
-
});
|
|
723
|
-
case "proxy":
|
|
724
|
-
return new LLMClient({ proxyUrl: config.proxyUrl, authToken: config.authToken });
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
async function buildAssistant(config, { backend, dataDir, remindersFile }) {
|
|
728
|
-
const workspaceRoot = config.workspaceRoot ?? process.cwd();
|
|
729
|
-
const search = (query) => braveSearch(query, config.braveApiKey);
|
|
730
|
-
const llmClient = buildLlmClient(config, workspaceRoot, remindersFile);
|
|
731
|
-
return PersonalAssistant.create({
|
|
732
|
-
llmClient,
|
|
733
|
-
model: config.model,
|
|
734
|
-
// config.activeProject (set via /project <name>) overrides the default of "the workspace
|
|
735
|
-
// I'm running in" — see PersonalAssistantOptions.activeProject's doc comment.
|
|
736
|
-
activeProject: config.activeProject || workspaceRoot,
|
|
737
|
-
memory: new FileSystemAdapter({ backend, baseDir: dataDir, namespace: "transcripts" }),
|
|
738
|
-
experienceStore: await FileSystemExperienceStore.create({ backend, baseDir: dataDir, namespace: "experience" }),
|
|
739
|
-
checkpointStore: new FileSystemAdapter({ backend, baseDir: dataDir, namespace: "checkpoints" }),
|
|
740
|
-
reminderStore: new InMemoryReminderStore(new FileSystemAdapter({ backend, baseDir: dataDir, namespace: "reminders" })),
|
|
741
|
-
fileTools: { backend, workspaceRoot },
|
|
742
|
-
webTools: config.enableWeb ? { search } : void 0,
|
|
743
|
-
// executeCommand is the real child_process.spawn-based implementation (shell-executor.ts) —
|
|
744
|
-
// wired in here, not inside assistant.ts, so the browser build never needs node:child_process.
|
|
745
|
-
shellTools: config.enableShell ? { backend, workspaceRoot, timeoutMs: config.shellTimeoutMs, networkAllowlist: config.shellNetworkAllowlist, executeCommand: runApprovedShellCommand } : void 0,
|
|
746
|
-
// The Resend/SMTP transport is wired in here, not inside assistant.ts, so the browser build
|
|
747
|
-
// never needs nodemailer or a mail server — same split as shellTools' executeCommand.
|
|
748
|
-
actionTools: (() => {
|
|
749
|
-
const sendEmail = buildEmailSender(config);
|
|
750
|
-
return sendEmail ? { backend, workspaceRoot, sendEmail } : void 0;
|
|
751
|
-
})(),
|
|
752
|
-
dangerouslySkipPermissions: config.dangerouslySkipPermissions,
|
|
753
|
-
spendCap: config.sessionCostLimitUsd !== void 0 || config.sessionCallLimit !== void 0 ? { sessionCostLimitUsd: config.sessionCostLimitUsd, sessionCallLimit: config.sessionCallLimit } : void 0,
|
|
754
|
-
// Resolved through the shared config seam (cli-config.ts's envOverridesFromProcessEnv reads
|
|
755
|
-
// ASSISTANT_ONE_LOOP into config.oneLoopMode; /config set oneLoopMode persists it) so the CLI,
|
|
756
|
-
// chat-ui, and the Tauri desktop build all decide this the same way. Undefined here means
|
|
757
|
-
// PersonalAssistant falls back to DEFAULT_ONE_LOOP_MODE.
|
|
758
|
-
oneLoopMode: config.oneLoopMode,
|
|
759
|
-
// P11 of plans/ask_question_and_plan_mode_plan.html — resolved through the same shared config
|
|
760
|
-
// seam as oneLoopMode above (ASSISTANT_PLAN_MODE / `/config set planMode`). Undefined here
|
|
761
|
-
// means PersonalAssistant falls back to DEFAULT_PLAN_MODE ('legacy').
|
|
762
|
-
planMode: config.planMode
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
async function runCli(options = {}) {
|
|
766
|
-
const dataDir = options.dataDir ?? defaultDataDir;
|
|
767
|
-
const configStore = options.configStore ?? defaultConfigStore;
|
|
768
|
-
const backend = options.backend ?? defaultBackend;
|
|
769
|
-
const remindersFile = options.remindersFile ?? defaultRemindersFile;
|
|
770
|
-
const envOverrides = options.envOverrides ?? defaultEnvOverrides;
|
|
771
|
-
const nonInteractiveApprovalMode = options.nonInteractiveApprovalMode ?? defaultNonInteractiveApprovalMode;
|
|
772
|
-
const inputStream = options.input ?? process.stdin;
|
|
773
|
-
const outputStream = options.output ?? process.stdout;
|
|
774
|
-
let persisted = await configStore.load();
|
|
775
|
-
let { config, overriddenKeys } = resolveConfig(persisted, envOverrides);
|
|
776
|
-
if (!options.skipFirstRunSetup && !options.assistant) {
|
|
777
|
-
const isInteractive = inputStream.isTTY === true;
|
|
778
|
-
let setupRl;
|
|
779
|
-
const firstRunAsk = options.firstRunAsk ?? ((question) => {
|
|
780
|
-
setupRl ?? (setupRl = createInterface({ input: inputStream, output: outputStream }));
|
|
781
|
-
return new Promise((resolvePrompt) => {
|
|
782
|
-
setupRl.question(question, (answer) => resolvePrompt(answer.trim()));
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
try {
|
|
786
|
-
persisted = await maybeRunFirstRunSetup({
|
|
787
|
-
configStore,
|
|
788
|
-
persisted,
|
|
789
|
-
overriddenKeys,
|
|
790
|
-
isInteractive,
|
|
791
|
-
ask: firstRunAsk,
|
|
792
|
-
detectClaudeCli: options.detectClaudeCli ?? (async () => (await checkClaudeCli(process.env.CLAUDE_PATH ?? "claude")).ok),
|
|
793
|
-
log: (line) => outputStream.write(`${line}
|
|
794
|
-
`)
|
|
795
|
-
});
|
|
796
|
-
} finally {
|
|
797
|
-
setupRl == null ? void 0 : setupRl.close();
|
|
798
|
-
}
|
|
799
|
-
({ config, overriddenKeys } = resolveConfig(persisted, envOverrides));
|
|
800
|
-
}
|
|
801
|
-
try {
|
|
802
|
-
validateConfig({}, config);
|
|
803
|
-
} catch (err) {
|
|
804
|
-
if (!(err instanceof ConfigValidationError)) throw err;
|
|
805
|
-
console.error(err.message);
|
|
806
|
-
process.exit(1);
|
|
807
|
-
}
|
|
808
|
-
if (nonInteractiveApprovalMode === "require-tty" && !process.stdin.isTTY) {
|
|
809
|
-
console.error(
|
|
810
|
-
`ASSISTANT_NON_INTERACTIVE_APPROVAL=require-tty is set, but stdin is not a real TTY (piped/scripted input). Refusing to start rather than fail confusingly at the first approval prompt — see README.md's "Non-interactive / scripted use" section for the alternative (ASSISTANT_NON_INTERACTIVE_APPROVAL=decline).`
|
|
811
|
-
);
|
|
812
|
-
process.exit(1);
|
|
813
|
-
}
|
|
814
|
-
let assistant = options.assistant ?? await buildAssistant(config, { dataDir, backend, remindersFile });
|
|
815
|
-
const rl = createInterface({ input: options.input ?? process.stdin, output: options.output ?? process.stdout, prompt: "you> " });
|
|
816
|
-
rl.pause();
|
|
817
|
-
rl.on("SIGINT", () => {
|
|
818
|
-
console.log("\nExiting.");
|
|
819
|
-
process.exit(0);
|
|
820
|
-
});
|
|
821
|
-
const backendDisplayModel = {
|
|
822
|
-
proxy: ANTHROPIC_DEFAULT_MODEL,
|
|
823
|
-
"claude-cli": "(your Claude Code default)",
|
|
824
|
-
anthropic: ANTHROPIC_DEFAULT_MODEL,
|
|
825
|
-
openai: OPENAI_DEFAULT_MODEL,
|
|
826
|
-
openrouter: OPENROUTER_DEFAULT_MODEL
|
|
827
|
-
};
|
|
828
|
-
console.log(`backend: ${config.llmBackend} (${config.model ?? backendDisplayModel[config.llmBackend]})`);
|
|
829
|
-
const enabledCapabilities = [];
|
|
830
|
-
if (config.enableWeb) enabledCapabilities.push("web search/fetch (brave)");
|
|
831
|
-
if (config.enableShell) enabledCapabilities.push(`shell commands (${config.dangerouslySkipPermissions ? "NOT approval-gated" : "approval-gated"})`);
|
|
832
|
-
const capabilitySuffix = enabledCapabilities.length > 0 ? ` — enabled: ${enabledCapabilities.join(", ")}` : "";
|
|
833
|
-
const dangerBanner = config.dangerouslySkipPermissions ? "\n⚠ dangerouslySkipPermissions is ON — every approval prompt (risky messages, file writes, shell commands) is skipped automatically.\n" : "";
|
|
834
|
-
const nonInteractiveBanner = nonInteractiveApprovalMode === "decline" ? "\nASSISTANT_NON_INTERACTIVE_APPROVAL=decline is set — every approval prompt auto-declines.\n" : "";
|
|
835
|
-
const startupUndoLogEntries = await assistant.listUndoLogEntries();
|
|
836
|
-
const undoableFromBefore = startupUndoLogEntries.filter((e) => e.undoable).length;
|
|
837
|
-
const undoBanner = undoableFromBefore > 0 ? `
|
|
838
|
-
${undoableFromBefore} action${undoableFromBefore === 1 ? "" : "s"} from earlier sessions ${undoableFromBefore === 1 ? "is" : "are"} still revertible — see /undo-action.
|
|
839
|
-
` : "";
|
|
840
|
-
console.log(`Aielia — your personal assistant on the 11-layer harness, one turn at a time. Ctrl+C to exit.${capabilitySuffix}
|
|
841
|
-
${dangerBanner}${nonInteractiveBanner}${undoBanner}`);
|
|
842
|
-
console.log("Type /help to see all commands, /config to view settings.\n");
|
|
843
|
-
rl.prompt();
|
|
844
|
-
let lastTrace;
|
|
845
|
-
let lastSources;
|
|
846
|
-
let lastPlanStatus;
|
|
847
|
-
let lastTurnUsage;
|
|
848
|
-
let lastTurnToolSteps = [];
|
|
849
|
-
let lastNoTraceReason;
|
|
850
|
-
function withCostEstimate(usage) {
|
|
851
|
-
if (config.llmBackend === "claude-cli" || usage.costUsd !== void 0) return usage;
|
|
852
|
-
const estimated = estimateCostUsd(config.model ?? ANTHROPIC_DEFAULT_MODEL, usage);
|
|
853
|
-
return estimated !== void 0 ? { ...usage, costUsd: estimated } : usage;
|
|
854
|
-
}
|
|
855
|
-
const PLAN_TASK_STATUS_ICON = {
|
|
856
|
-
PENDING: "○",
|
|
857
|
-
RUNNING: "▶",
|
|
858
|
-
COMPLETE: "✓",
|
|
859
|
-
FAILED: "✗",
|
|
860
|
-
BLOCKED: "✗",
|
|
861
|
-
HUMAN_REQUIRED: "~"
|
|
862
|
-
};
|
|
863
|
-
function printPlan() {
|
|
864
|
-
if (!lastPlanStatus) {
|
|
865
|
-
console.log("\nNo active plan for this session.\n");
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
console.log(`
|
|
869
|
-
Plan: ${lastPlanStatus.templateName ?? "custom plan"} (${lastPlanStatus.completionPct.toFixed(1)}% complete)`);
|
|
870
|
-
for (const task of lastPlanStatus.tasks) {
|
|
871
|
-
console.log(` ${PLAN_TASK_STATUS_ICON[task.status] ?? "?"} [${task.status}] ${task.id} — ${task.description}`);
|
|
872
|
-
}
|
|
873
|
-
console.log(`
|
|
874
|
-
Success criteria: ${lastPlanStatus.successCriteria}
|
|
875
|
-
`);
|
|
876
|
-
}
|
|
877
|
-
async function handlePlanSketch(args) {
|
|
878
|
-
const request = args.join(" ").trim();
|
|
879
|
-
if (!request) {
|
|
880
|
-
console.log("\nUsage: /plan sketch <request>\n");
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
console.log("\nSketching a plan (read-only grounding only, nothing staged)...\n");
|
|
884
|
-
const result = await assistant.sketchPlan("cli", request);
|
|
885
|
-
console.log(`${result.reply ?? "(no reply)"}
|
|
886
|
-
`);
|
|
887
|
-
if (result.usage) lastTurnUsage = withCostEstimate(result.usage);
|
|
888
|
-
}
|
|
889
|
-
async function handlePlan(args) {
|
|
890
|
-
if (args[0] === "sketch") {
|
|
891
|
-
await handlePlanSketch(args.slice(1));
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
printPlan();
|
|
895
|
-
}
|
|
896
|
-
function verificationHealthLabel({ strength, feasibility }) {
|
|
897
|
-
const confidence = Math.min(strength, feasibility);
|
|
898
|
-
if (confidence >= 0.7) return "High confidence";
|
|
899
|
-
if (confidence >= 0.4) return "Reasonably confident";
|
|
900
|
-
return "Worth double-checking";
|
|
901
|
-
}
|
|
902
|
-
function batchBudgetSummaryLine(batchBudget) {
|
|
903
|
-
const found = batchBudget.perItemOutcomes.filter((o) => o.status === "found").length;
|
|
904
|
-
const notFound = batchBudget.perItemOutcomes.filter((o) => o.status === "not_found").length;
|
|
905
|
-
const truncated = batchBudget.perItemOutcomes.filter((o) => o.status === "truncated_while_productive").length;
|
|
906
|
-
return `Batch: ${batchBudget.itemCount} items — ${found} found, ${notFound} not found, ${truncated} truncated (${batchBudget.totalCallsUsed} calls used, projected ~${Math.ceil(batchBudget.projectedTotal)})`;
|
|
907
|
-
}
|
|
908
|
-
function printWhy() {
|
|
909
|
-
if (!lastTrace) {
|
|
910
|
-
console.log(`
|
|
911
|
-
${lastNoTraceReason ?? "No harness trace for the last turn (nothing to explain yet, or it took the fast path)."}
|
|
912
|
-
`);
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
console.log(`
|
|
916
|
-
${verificationHealthLabel(lastTrace.verificationHealth)}`);
|
|
917
|
-
const chain = buildWhyChain(lastTrace.layerActivity);
|
|
918
|
-
if (chain.length > 0) {
|
|
919
|
-
console.log(" " + chain.map((item) => `${LAYER_SHORT_CODE[item.layer]} (${item.reason})`).join(" > "));
|
|
920
|
-
}
|
|
921
|
-
if (lastTrace.batchBudget) {
|
|
922
|
-
console.log(" " + batchBudgetSummaryLine(lastTrace.batchBudget));
|
|
923
|
-
}
|
|
924
|
-
console.log("");
|
|
925
|
-
}
|
|
926
|
-
function printLayers() {
|
|
927
|
-
if (!lastTrace) {
|
|
928
|
-
console.log(`
|
|
929
|
-
${lastNoTraceReason ?? "No harness trace for the last turn (nothing to explain yet, or it took the fast path)."}
|
|
930
|
-
`);
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
console.log("");
|
|
934
|
-
const byLayer = new Map(lastTrace.layerActivity.map((e) => [e.layer, e]));
|
|
935
|
-
for (const layer of LAYER_ORDER) {
|
|
936
|
-
const e = byLayer.get(layer);
|
|
937
|
-
const mark = (e == null ? void 0 : e.fired) ? "✓" : "·";
|
|
938
|
-
const reason = (e == null ? void 0 : e.reason) ?? "not evaluated this turn";
|
|
939
|
-
console.log(` [${mark}] ${LAYER_DISPLAY_NAME[layer].padEnd(22)} ${reason}`);
|
|
940
|
-
}
|
|
941
|
-
if (lastTrace.batchBudget) {
|
|
942
|
-
console.log("");
|
|
943
|
-
console.log(` ${batchBudgetSummaryLine(lastTrace.batchBudget)}`);
|
|
944
|
-
const STATUS_MARK = {
|
|
945
|
-
found: "✓",
|
|
946
|
-
not_found: "✗",
|
|
947
|
-
truncated_while_productive: "~"
|
|
948
|
-
};
|
|
949
|
-
for (const outcome of lastTrace.batchBudget.perItemOutcomes) {
|
|
950
|
-
console.log(` [${STATUS_MARK[outcome.status]}] ${outcome.item.padEnd(30)} ${outcome.status} (${outcome.callsUsed} calls)`);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
console.log("");
|
|
954
|
-
}
|
|
955
|
-
const SOURCE_TOOL_LABEL = {
|
|
956
|
-
read_file: "Read",
|
|
957
|
-
list_directory: "Listed",
|
|
958
|
-
web_search: "Searched",
|
|
959
|
-
fetch_url: "Fetched"
|
|
960
|
-
};
|
|
961
|
-
function printSources() {
|
|
962
|
-
if (!lastSources || lastSources.length === 0) {
|
|
963
|
-
console.log("\nNo sources for the last turn (it used no file/web tool calls).\n");
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
console.log("");
|
|
967
|
-
for (const source of lastSources) {
|
|
968
|
-
console.log(` - ${SOURCE_TOOL_LABEL[source.tool]} ${source.path}`);
|
|
969
|
-
}
|
|
970
|
-
console.log("");
|
|
971
|
-
}
|
|
972
|
-
function printHelp() {
|
|
973
|
-
console.log(`
|
|
974
|
-
${formatHelp()}
|
|
975
|
-
`);
|
|
976
|
-
}
|
|
977
|
-
async function handleClear() {
|
|
978
|
-
await assistant.clearSession("cli");
|
|
979
|
-
lastTrace = void 0;
|
|
980
|
-
lastSources = void 0;
|
|
981
|
-
lastPlanStatus = void 0;
|
|
982
|
-
lastTurnUsage = void 0;
|
|
983
|
-
lastNoTraceReason = void 0;
|
|
984
|
-
console.log("\n✓ Started a fresh conversation.\n");
|
|
985
|
-
}
|
|
986
|
-
async function handleCheckpoint(args) {
|
|
987
|
-
if (args[0] === "clear") {
|
|
988
|
-
const result = await assistant.clearCheckpoint("cli");
|
|
989
|
-
console.log(
|
|
990
|
-
result.cleared ? `
|
|
991
|
-
✓ Cleared the stuck checkpoint (was at step ${result.stepsUsed}, node "${result.currentNode}"). Conversation history is untouched — your next message starts a fresh harness run.
|
|
992
|
-
` : "\nNo checkpoint to clear — the last turn either completed normally or there was nothing in progress.\n"
|
|
993
|
-
);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
const status = await assistant.getCheckpointStatus("cli");
|
|
997
|
-
if (!status.present) {
|
|
998
|
-
console.log("\nNo checkpoint present — the last turn completed normally or there was nothing in progress.\n");
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
const attemptsNote = status.failedResumeAttempts > 0 ? ` — failed to resume ${status.failedResumeAttempts} time${status.failedResumeAttempts === 1 ? "" : "s"} in a row so far` : "";
|
|
1002
|
-
console.log(
|
|
1003
|
-
`
|
|
1004
|
-
A checkpoint is present: step ${status.stepsUsed}, last node "${status.currentNode}"${attemptsNote}.
|
|
1005
|
-
Your next message will try to resume it automatically. Run /checkpoint clear to discard it and start fresh instead.
|
|
1006
|
-
`
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
async function printStatus() {
|
|
1010
|
-
const transcript = await assistant.getTranscript("cli");
|
|
1011
|
-
const undoLogEntries = await assistant.listUndoLogEntries();
|
|
1012
|
-
const spendCapLine = await spendCapStatusLine();
|
|
1013
|
-
console.log(
|
|
1014
|
-
`
|
|
1015
|
-
${formatStatus({ config, overriddenKeys, transcriptLength: transcript.length, planActive: lastPlanStatus !== void 0, undoLogEntries, spendCapLine })}
|
|
1016
|
-
`
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
async function spendCapStatusLine() {
|
|
1020
|
-
if (config.sessionCostLimitUsd === void 0 && config.sessionCallLimit === void 0) return void 0;
|
|
1021
|
-
const state = await assistant.getSpendState("cli");
|
|
1022
|
-
return formatSpendCapStatus(state, { sessionCostLimitUsd: config.sessionCostLimitUsd, sessionCallLimit: config.sessionCallLimit });
|
|
1023
|
-
}
|
|
1024
|
-
async function handleExport(args) {
|
|
1025
|
-
const transcript = await assistant.getTranscript("cli");
|
|
1026
|
-
if (transcript.length === 0) {
|
|
1027
|
-
console.log("\nNothing to export yet.\n");
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
const filename = resolve(process.cwd(), args[0] ?? defaultExportFilename());
|
|
1031
|
-
try {
|
|
1032
|
-
await writeFile(filename, formatTranscriptMarkdown(transcript), "utf-8");
|
|
1033
|
-
console.log(`
|
|
1034
|
-
✓ Exported ${transcript.length} message${transcript.length === 1 ? "" : "s"} to ${filename}
|
|
1035
|
-
`);
|
|
1036
|
-
} catch (err) {
|
|
1037
|
-
const { message } = classifyError(err);
|
|
1038
|
-
console.log(`
|
|
1039
|
-
[error] ${message}
|
|
1040
|
-
`);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
async function handleUndo() {
|
|
1044
|
-
const transcriptBefore = await assistant.getTranscript("cli");
|
|
1045
|
-
if (transcriptBefore.length === 0) {
|
|
1046
|
-
console.log("\nNothing to undo.\n");
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
const wasPendingApproval = transcriptBefore[transcriptBefore.length - 1].role !== "assistant";
|
|
1050
|
-
const undoneReminders = lastTurnToolSteps.filter((s) => s.tool === "create_reminder");
|
|
1051
|
-
const result = await assistant.undoLastTurn("cli");
|
|
1052
|
-
if (!result.undone) {
|
|
1053
|
-
console.log("\nNothing to undo.\n");
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
lastTrace = void 0;
|
|
1057
|
-
lastSources = void 0;
|
|
1058
|
-
lastPlanStatus = void 0;
|
|
1059
|
-
lastTurnToolSteps = [];
|
|
1060
|
-
const caveat = undoneReminders.length > 0 ? ` Note: ${undoneReminders.length === 1 ? "a reminder" : `${undoneReminders.length} reminders`} created in that exchange (${undoneReminders.map((s) => `"${s.input.text}"`).join(", ")}) ${undoneReminders.length === 1 ? "is" : "are"} still active — /undo only removes chat history, not that side effect.` : "";
|
|
1061
|
-
console.log(
|
|
1062
|
-
`
|
|
1063
|
-
✓ Removed ${wasPendingApproval ? "the pending message awaiting approval" : "the last exchange (1 user message, 1 assistant reply)"}.${caveat}
|
|
1064
|
-
`
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
async function handleUndoAction(args) {
|
|
1068
|
-
if (args.length === 0) {
|
|
1069
|
-
const entries = await assistant.listUndoLogEntries();
|
|
1070
|
-
console.log(`
|
|
1071
|
-
${formatUndoLogListing(entries)}
|
|
1072
|
-
`);
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
const staged = await assistant.stageUndoAction(args[0]);
|
|
1076
|
-
if (staged.status === "error") {
|
|
1077
|
-
console.log(`
|
|
1078
|
-
✗ ${staged.message}
|
|
1079
|
-
`);
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
console.log(`
|
|
1083
|
-
[needs approval — revert] ${staged.reason}`);
|
|
1084
|
-
const confirmed = await askYesNo("Apply this revert? (y/N) ");
|
|
1085
|
-
lastTrace = void 0;
|
|
1086
|
-
lastNoTraceReason = `No harness trace — the last turn was a staged revert that was ${confirmed ? "approved" : "declined"} before the harness ran.`;
|
|
1087
|
-
await handleTurn("", confirmed, staged.pendingActionId);
|
|
1088
|
-
}
|
|
1089
|
-
async function printMemory() {
|
|
1090
|
-
const summary = await assistant.getMemorySummary("cli");
|
|
1091
|
-
console.log(`
|
|
1092
|
-
${formatMemorySummary(summary)}
|
|
1093
|
-
`);
|
|
1094
|
-
}
|
|
1095
|
-
async function handleMemoryExport(args) {
|
|
1096
|
-
const data = await assistant.exportMemory("cli");
|
|
1097
|
-
const filename = resolve(process.cwd(), args[0] ?? defaultMemoryExportFilename());
|
|
1098
|
-
try {
|
|
1099
|
-
await writeFile(filename, formatMemoryExport(data), "utf-8");
|
|
1100
|
-
console.log(`
|
|
1101
|
-
✓ Exported learned memory to ${filename}
|
|
1102
|
-
`);
|
|
1103
|
-
} catch (err) {
|
|
1104
|
-
const { message } = classifyError(err);
|
|
1105
|
-
console.log(`
|
|
1106
|
-
[error] ${message}
|
|
1107
|
-
`);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
async function handleMemoryPending(action, args) {
|
|
1111
|
-
const selector = args[0];
|
|
1112
|
-
if (!selector) {
|
|
1113
|
-
console.log(`
|
|
1114
|
-
Usage: /memory ${action} <n> or /memory ${action} <category>
|
|
1115
|
-
`);
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
const outcome = action === "confirm" ? await assistant.confirmPendingFact(selector) : await assistant.rejectPendingFact(selector);
|
|
1119
|
-
console.log(`
|
|
1120
|
-
${formatMemoryPendingOutcome(action === "confirm" ? "confirmed" : "rejected", outcome)}
|
|
1121
|
-
`);
|
|
1122
|
-
}
|
|
1123
|
-
async function handleMemoryForget(args) {
|
|
1124
|
-
const selector = args[0];
|
|
1125
|
-
if (!selector) {
|
|
1126
|
-
console.log("\nUsage: /memory forget <n>\n");
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
const outcome = await assistant.forgetFact(selector, "cli");
|
|
1130
|
-
console.log(`
|
|
1131
|
-
${formatMemoryPendingOutcome("forgotten", outcome)}
|
|
1132
|
-
`);
|
|
1133
|
-
}
|
|
1134
|
-
async function handleMemory(args) {
|
|
1135
|
-
if (args[0] === "export") {
|
|
1136
|
-
await handleMemoryExport(args.slice(1));
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
if (args[0] === "confirm" || args[0] === "reject") {
|
|
1140
|
-
await handleMemoryPending(args[0], args.slice(1));
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
if (args[0] === "forget") {
|
|
1144
|
-
await handleMemoryForget(args.slice(1));
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
await printMemory();
|
|
1148
|
-
}
|
|
1149
|
-
async function handleSearch(args) {
|
|
1150
|
-
const query = args.join(" ");
|
|
1151
|
-
if (!query.trim()) {
|
|
1152
|
-
console.log("\nUsage: /search <query>\n");
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
const hits = await assistant.searchTranscript(query);
|
|
1156
|
-
console.log(`
|
|
1157
|
-
${formatSearchResults(hits, query)}
|
|
1158
|
-
`);
|
|
1159
|
-
}
|
|
1160
|
-
async function printCost() {
|
|
1161
|
-
const spendCapLine = await spendCapStatusLine();
|
|
1162
|
-
const spendState = await assistant.getSpendState("cli");
|
|
1163
|
-
const session = { inputTokens: spendState.cumulativeInputTokens, outputTokens: spendState.cumulativeOutputTokens, costUsd: spendState.cumulativeCostUsd };
|
|
1164
|
-
console.log(`
|
|
1165
|
-
${formatCostSummary({ lastTurn: lastTurnUsage, session, backend: config.llmBackend, spendCapLine })}
|
|
1166
|
-
`);
|
|
1167
|
-
}
|
|
1168
|
-
async function handleDoctor() {
|
|
1169
|
-
const workspaceRoot = config.workspaceRoot ?? process.cwd();
|
|
1170
|
-
const checks = await Promise.all([
|
|
1171
|
-
// Backend-specific checks only run for the backend actually in use — skipped, not
|
|
1172
|
-
// reported as failing, for the one that's inactive.
|
|
1173
|
-
...config.llmBackend === "proxy" ? [checkProxyHealth(config.proxyUrl)] : [],
|
|
1174
|
-
...config.llmBackend === "claude-cli" ? [checkClaudeCli(process.env.CLAUDE_PATH ?? "claude")] : [],
|
|
1175
|
-
checkWorkspaceRoot(workspaceRoot),
|
|
1176
|
-
checkDataDirWritable(backend, dataDir),
|
|
1177
|
-
// Informational only (always ok:true) — surfaces the active backend/flags so a user has
|
|
1178
|
-
// one command to confirm config, rather than none (see file's own config summary gap).
|
|
1179
|
-
{ label: `llmBackend: ${config.llmBackend}`, ok: true },
|
|
1180
|
-
{ label: `enableShell: ${config.enableShell ?? false}, enableWeb: ${config.enableWeb ?? false}`, ok: true },
|
|
1181
|
-
// Provider API keys are stored plaintext in config.json, not an OS keychain — confirming
|
|
1182
|
-
// that explicitly here rather than leaving it unstated (no keychain integration exists).
|
|
1183
|
-
{ label: "provider keys stored: plaintext in config.json (no OS keychain integration)", ok: true }
|
|
1184
|
-
]);
|
|
1185
|
-
console.log(`
|
|
1186
|
-
${formatDoctorReport(checks)}
|
|
1187
|
-
`);
|
|
1188
|
-
}
|
|
1189
|
-
let lastProgressLineLength = 0;
|
|
1190
|
-
function writeProgress(progress) {
|
|
1191
|
-
const layer = nodeToLayer(progress.currentNode);
|
|
1192
|
-
const label = layer ? LAYER_DISPLAY_NAME[layer] : nodeDisplayName(progress.currentNode);
|
|
1193
|
-
const prefix = progress.planPosition ? `[${progress.planPosition.templateName ?? "custom plan"} — step ${progress.planPosition.stepIndex}/${progress.planPosition.stepCount} (${progress.planPosition.completionPct.toFixed(0)}%)]` : `[step ${progress.stepsUsed}/${progress.maxSteps}]`;
|
|
1194
|
-
const line = `${prefix}${label ? ` ${label}…` : ""}`;
|
|
1195
|
-
process.stdout.write(`\r${line.padEnd(lastProgressLineLength)}`);
|
|
1196
|
-
lastProgressLineLength = line.length;
|
|
1197
|
-
}
|
|
1198
|
-
function clearProgress() {
|
|
1199
|
-
if (lastProgressLineLength === 0) return;
|
|
1200
|
-
process.stdout.write(`\r${" ".repeat(lastProgressLineLength)}\r`);
|
|
1201
|
-
lastProgressLineLength = 0;
|
|
1202
|
-
}
|
|
1203
|
-
function writeToolStep(step) {
|
|
1204
|
-
clearProgress();
|
|
1205
|
-
console.log(` ⚙ ${step.summary}`);
|
|
1206
|
-
lastTurnToolSteps.push(step);
|
|
1207
|
-
}
|
|
1208
|
-
function emptyClarificationDraft() {
|
|
1209
|
-
return { selectedLabels: [], editText: "", freeText: "" };
|
|
1210
|
-
}
|
|
1211
|
-
function orderedAskOptions(question) {
|
|
1212
|
-
const options2 = question.options ?? [];
|
|
1213
|
-
return options2.map((option, index) => ({ option, index })).sort((a, b) => Number(b.option.recommended ?? false) - Number(a.option.recommended ?? false) || a.index - b.index).map(({ option }) => option);
|
|
1214
|
-
}
|
|
1215
|
-
function clarificationDraftToAnswer(questionId, draft) {
|
|
1216
|
-
if (draft.selectedLabels.length > 0) {
|
|
1217
|
-
return draft.editText.trim() ? { questionId, kind: "selected_with_edit", selectedLabels: draft.selectedLabels, editText: draft.editText.trim() } : { questionId, kind: "selected", selectedLabels: draft.selectedLabels };
|
|
1218
|
-
}
|
|
1219
|
-
if (draft.freeText.trim()) {
|
|
1220
|
-
return { questionId, kind: "free_text", freeText: draft.freeText.trim() };
|
|
1221
|
-
}
|
|
1222
|
-
return null;
|
|
1223
|
-
}
|
|
1224
|
-
function printClarificationQuestion(question, draft, index, total) {
|
|
1225
|
-
const options2 = orderedAskOptions(question);
|
|
1226
|
-
const allowFreeText = question.allowFreeText !== false;
|
|
1227
|
-
console.log("");
|
|
1228
|
-
if (question.header) console.log(`[${question.header}]`);
|
|
1229
|
-
if (total > 1) console.log(`Question ${index + 1} of ${total}`);
|
|
1230
|
-
console.log(question.question);
|
|
1231
|
-
options2.forEach((option, i) => {
|
|
1232
|
-
const n = i + 1;
|
|
1233
|
-
const picked = draft.selectedLabels.includes(option.label) ? "✓" : " ";
|
|
1234
|
-
const recommended = option.recommended ? " (recommended)" : "";
|
|
1235
|
-
console.log(` [${picked}] ${n}) ${option.label}${recommended}`);
|
|
1236
|
-
if (option.description) console.log(` ${option.description}`);
|
|
1237
|
-
if (option.preview) console.log(` preview: ${option.preview}`);
|
|
1238
|
-
});
|
|
1239
|
-
if (draft.editText) console.log(` note: ${draft.editText}`);
|
|
1240
|
-
if (draft.freeText) console.log(` other: ${draft.freeText}`);
|
|
1241
|
-
const commands2 = [
|
|
1242
|
-
options2.length > 0 ? question.allowMultiple ? "<n>/t<n> toggle option" : "<n> select option" : void 0,
|
|
1243
|
-
options2.length > 0 ? "e<n> add a note to a selected option" : void 0,
|
|
1244
|
-
allowFreeText ? "o other (free text)" : void 0,
|
|
1245
|
-
total > 1 ? "b back, n next" : void 0,
|
|
1246
|
-
"s submit"
|
|
1247
|
-
].filter((c) => c !== void 0);
|
|
1248
|
-
console.log(` (${commands2.join(" | ")})`);
|
|
1249
|
-
}
|
|
1250
|
-
async function handleClarification(result) {
|
|
1251
|
-
const questions = result.questions ?? [];
|
|
1252
|
-
const pendingClarificationId = result.pendingClarificationId;
|
|
1253
|
-
if (questions.length === 0 || !pendingClarificationId) {
|
|
1254
|
-
lastTrace = void 0;
|
|
1255
|
-
lastNoTraceReason = `No harness trace — the last turn needed clarification (${result.reason ?? "no further detail"}) but had no answerable questions.`;
|
|
1256
|
-
console.log(`
|
|
1257
|
-
[needs clarification] ${result.reason ?? "This request needs clarification."}
|
|
1258
|
-
`);
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
if (nonInteractiveApprovalMode === "decline") {
|
|
1262
|
-
console.log(`
|
|
1263
|
-
[non-interactive mode: auto-declining — ASSISTANT_NON_INTERACTIVE_APPROVAL=decline]`);
|
|
1264
|
-
console.log(`
|
|
1265
|
-
[needs clarification] ${result.reason ?? "This request needs clarification."}`);
|
|
1266
|
-
for (const q of questions) console.log(` - ${q.question}`);
|
|
1267
|
-
console.log("");
|
|
1268
|
-
lastTrace = void 0;
|
|
1269
|
-
lastNoTraceReason = "No harness trace — the last turn needed clarification and was auto-declined (non-interactive mode) before the harness resumed.";
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
console.log(`
|
|
1273
|
-
[needs clarification] ${questions.length} question${questions.length === 1 ? "" : "s"} to answer.`);
|
|
1274
|
-
const drafts = new Map(questions.map((q) => [q.id, emptyClarificationDraft()]));
|
|
1275
|
-
let index = 0;
|
|
1276
|
-
while (true) {
|
|
1277
|
-
const question = questions[index];
|
|
1278
|
-
const draft = drafts.get(question.id);
|
|
1279
|
-
printClarificationQuestion(question, draft, index, questions.length);
|
|
1280
|
-
const token = (await askLine("clarify> ")).trim();
|
|
1281
|
-
const lower = token.toLowerCase();
|
|
1282
|
-
if (lower === "s" || lower === "submit") {
|
|
1283
|
-
const answers = questions.map((q) => clarificationDraftToAnswer(q.id, drafts.get(q.id)));
|
|
1284
|
-
const missing = questions.filter((q, i) => answers[i] === null);
|
|
1285
|
-
if (missing.length > 0) {
|
|
1286
|
-
console.log(`
|
|
1287
|
-
Still need an answer for: ${missing.map((q) => q.question).join("; ")}
|
|
1288
|
-
`);
|
|
1289
|
-
continue;
|
|
1290
|
-
}
|
|
1291
|
-
const response = { answers };
|
|
1292
|
-
await handleTurn("", false, void 0, pendingClarificationId, response);
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
if (lower === "b" && questions.length > 1) {
|
|
1296
|
-
index = Math.max(0, index - 1);
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
if (lower === "n" && questions.length > 1) {
|
|
1300
|
-
index = Math.min(questions.length - 1, index + 1);
|
|
1301
|
-
continue;
|
|
1302
|
-
}
|
|
1303
|
-
if (lower === "o") {
|
|
1304
|
-
if (question.allowFreeText === false) {
|
|
1305
|
-
console.log("\nThis question does not accept a free-text answer.\n");
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
const text = await askLine("Other — type your own answer: ");
|
|
1309
|
-
if (!text.trim()) {
|
|
1310
|
-
console.log("\nEmpty answer ignored.\n");
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
drafts.set(question.id, { selectedLabels: [], editText: "", freeText: text.trim() });
|
|
1314
|
-
continue;
|
|
1315
|
-
}
|
|
1316
|
-
const editMatch = /^e(\d+)$/i.exec(token);
|
|
1317
|
-
if (editMatch) {
|
|
1318
|
-
const options2 = orderedAskOptions(question);
|
|
1319
|
-
const option = options2[Number(editMatch[1]) - 1];
|
|
1320
|
-
if (!option) {
|
|
1321
|
-
console.log(`
|
|
1322
|
-
No option ${editMatch[1]}.
|
|
1323
|
-
`);
|
|
1324
|
-
continue;
|
|
1325
|
-
}
|
|
1326
|
-
if (!draft.selectedLabels.includes(option.label)) {
|
|
1327
|
-
console.log(`
|
|
1328
|
-
Select option ${editMatch[1]} first, then add a note with e${editMatch[1]}.
|
|
1329
|
-
`);
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
const note = await askLine("Note: ");
|
|
1333
|
-
drafts.set(question.id, { ...draft, editText: note.trim() });
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
const toggleMatch = /^t(\d+)$/i.exec(token) ?? /^(\d+)$/.exec(token);
|
|
1337
|
-
if (toggleMatch) {
|
|
1338
|
-
const options2 = orderedAskOptions(question);
|
|
1339
|
-
const option = options2[Number(toggleMatch[1]) - 1];
|
|
1340
|
-
if (!option) {
|
|
1341
|
-
console.log(`
|
|
1342
|
-
No option ${toggleMatch[1]}.
|
|
1343
|
-
`);
|
|
1344
|
-
continue;
|
|
1345
|
-
}
|
|
1346
|
-
if (question.allowMultiple) {
|
|
1347
|
-
const already = draft.selectedLabels.includes(option.label);
|
|
1348
|
-
drafts.set(question.id, {
|
|
1349
|
-
...draft,
|
|
1350
|
-
selectedLabels: already ? draft.selectedLabels.filter((l) => l !== option.label) : [...draft.selectedLabels, option.label],
|
|
1351
|
-
freeText: ""
|
|
1352
|
-
});
|
|
1353
|
-
} else {
|
|
1354
|
-
drafts.set(question.id, { ...draft, selectedLabels: [option.label], freeText: "" });
|
|
1355
|
-
}
|
|
1356
|
-
continue;
|
|
1357
|
-
}
|
|
1358
|
-
console.log(`
|
|
1359
|
-
Unrecognized input "${token}".
|
|
1360
|
-
`);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
async function handleTurn(message, approved = false, pendingActionId, pendingClarificationId, clarificationAnswer) {
|
|
1364
|
-
lastTurnToolSteps = [];
|
|
1365
|
-
let streamedAnyTokens = false;
|
|
1366
|
-
function writeToken(token) {
|
|
1367
|
-
if (!streamedAnyTokens) {
|
|
1368
|
-
process.stdout.write("\nAielia> ");
|
|
1369
|
-
streamedAnyTokens = true;
|
|
1370
|
-
}
|
|
1371
|
-
process.stdout.write(token);
|
|
1372
|
-
}
|
|
1373
|
-
try {
|
|
1374
|
-
const result = await assistant.turn(message, {
|
|
1375
|
-
sessionId: "cli",
|
|
1376
|
-
approved,
|
|
1377
|
-
pendingActionId,
|
|
1378
|
-
pendingClarificationId,
|
|
1379
|
-
clarificationAnswer,
|
|
1380
|
-
// Gated on streamedAnyTokens: onProgress keeps firing for layers (Memory,
|
|
1381
|
-
// Verification) that run after the LLM call, i.e. after writeToken has already put
|
|
1382
|
-
// the reply on the current line with no trailing newline. writeProgress's \r-based
|
|
1383
|
-
// overwrite doesn't know that — on a real TTY it wrote the "[step N/5] ..." text
|
|
1384
|
-
// over the tail of the just-streamed reply, and the following clearProgress() then
|
|
1385
|
-
// blanked that same line, so the end of the assistant's answer visibly vanished
|
|
1386
|
-
// right after it finished streaming. Once streaming has started this turn, further
|
|
1387
|
-
// progress has nothing safe to overwrite, so it's dropped instead.
|
|
1388
|
-
onProgress: (progress) => {
|
|
1389
|
-
if (!streamedAnyTokens) writeProgress(progress);
|
|
1390
|
-
},
|
|
1391
|
-
onToken: writeToken,
|
|
1392
|
-
onToolStep: writeToolStep
|
|
1393
|
-
});
|
|
1394
|
-
if (!streamedAnyTokens) clearProgress();
|
|
1395
|
-
if (result.status === "needs_approval" && result.pendingActionId) {
|
|
1396
|
-
const kindLabel = result.pendingActionKind === "shell" ? "shell command" : result.pendingActionKind === "email" ? "send email" : result.pendingActionKind === "batch" ? "batch research" : "write";
|
|
1397
|
-
const promptText = result.pendingActionKind === "shell" ? "Run this command? (y/N) " : result.pendingActionKind === "email" ? "Send this email? (y/N) " : result.pendingActionKind === "batch" ? "Continue? (y/N) " : "Apply this write? (y/N) ";
|
|
1398
|
-
console.log(`
|
|
1399
|
-
[needs approval — ${kindLabel}] ${result.reason}`);
|
|
1400
|
-
const confirmed = await askYesNo(promptText);
|
|
1401
|
-
lastTrace = void 0;
|
|
1402
|
-
lastNoTraceReason = `No harness trace — the last turn was a staged ${kindLabel} that was ${confirmed ? "approved" : "declined"} before the harness ran.`;
|
|
1403
|
-
await handleTurn(message, confirmed, result.pendingActionId);
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
if (result.status === "needs_approval") {
|
|
1407
|
-
console.log(`
|
|
1408
|
-
[needs approval — ${result.riskLevel}] ${result.reason}`);
|
|
1409
|
-
console.log(` "${message}"`);
|
|
1410
|
-
const confirmed = await askYesNo("Proceed? (y/N) ");
|
|
1411
|
-
if (confirmed) {
|
|
1412
|
-
await handleTurn(message, true);
|
|
1413
|
-
} else {
|
|
1414
|
-
lastTrace = void 0;
|
|
1415
|
-
lastNoTraceReason = "No harness trace — the last turn was blocked on an approval gate and declined before the harness ran.";
|
|
1416
|
-
await assistant.recordDeclinedRequest("cli", message, result.reason ?? "This request needed approval.");
|
|
1417
|
-
console.log("Cancelled.\n");
|
|
1418
|
-
}
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
if (result.status === "needs_clarification") {
|
|
1422
|
-
await handleClarification(result);
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
if (result.status === "escalated") {
|
|
1426
|
-
lastTrace = void 0;
|
|
1427
|
-
lastNoTraceReason = `No harness trace — the last turn escalated (${result.reason}) before completing.`;
|
|
1428
|
-
console.log(`
|
|
1429
|
-
[escalated] ${result.reason}
|
|
1430
|
-
`);
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
if (!pendingActionId) {
|
|
1434
|
-
lastTrace = result.harnessSkipped ? void 0 : result.trace;
|
|
1435
|
-
lastNoTraceReason = result.harnessSkipped ? "No harness trace — the last turn was a simple, self-contained question answered directly without activating the harness (fast path)." : void 0;
|
|
1436
|
-
}
|
|
1437
|
-
lastSources = result.sources;
|
|
1438
|
-
if (!pendingActionId && !result.harnessSkipped) lastPlanStatus = result.planStatus;
|
|
1439
|
-
lastTurnUsage = result.usage ? withCostEstimate(result.usage) : void 0;
|
|
1440
|
-
const riskSuffix = result.riskLevel && result.riskLevel !== "LOW" ? ` [risk: ${result.riskLevel}]` : "";
|
|
1441
|
-
const sourcesHint = result.sources && result.sources.length > 0 ? ` (${result.sources.length} source${result.sources.length > 1 ? "s" : ""} — /sources)` : "";
|
|
1442
|
-
const planHint = result.planStatus ? ` (plan: ${result.planStatus.completionPct.toFixed(0)}% — /plan)` : "";
|
|
1443
|
-
const contradictionNotice = result.contradictionNotice ? `
|
|
1444
|
-
|
|
1445
|
-
${result.contradictionNotice}` : "";
|
|
1446
|
-
if (streamedAnyTokens) {
|
|
1447
|
-
const pausedNoteText = result.pausedNote ? `
|
|
1448
|
-
|
|
1449
|
-
${result.pausedNote}` : "";
|
|
1450
|
-
process.stdout.write(`${pausedNoteText}${riskSuffix}${sourcesHint}${planHint}${contradictionNotice}
|
|
1451
|
-
|
|
1452
|
-
`);
|
|
1453
|
-
} else {
|
|
1454
|
-
console.log(`
|
|
1455
|
-
Aielia>${riskSuffix} ${result.reply}${sourcesHint}${planHint}${contradictionNotice}
|
|
1456
|
-
`);
|
|
1457
|
-
}
|
|
1458
|
-
} catch (err) {
|
|
1459
|
-
clearProgress();
|
|
1460
|
-
const { message: errorMessage, retryable } = classifyError(err, config.llmBackend);
|
|
1461
|
-
console.log(`
|
|
1462
|
-
[error] ${errorMessage}${retryable ? " Type the message again to retry." : ""}
|
|
1463
|
-
`);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
function askYesNo(question) {
|
|
1467
|
-
if (options.askYesNo) return options.askYesNo(question);
|
|
1468
|
-
if (nonInteractiveApprovalMode === "decline") {
|
|
1469
|
-
console.log(`
|
|
1470
|
-
[non-interactive mode: auto-declining — ASSISTANT_NON_INTERACTIVE_APPROVAL=decline]`);
|
|
1471
|
-
return Promise.resolve(false);
|
|
1472
|
-
}
|
|
1473
|
-
return new Promise((resolve2) => {
|
|
1474
|
-
try {
|
|
1475
|
-
rl.question(question, (answer) => resolve2(answer.trim().toLowerCase().startsWith("y")));
|
|
1476
|
-
} catch {
|
|
1477
|
-
console.log(`
|
|
1478
|
-
[could not read a response — treating as declined]`);
|
|
1479
|
-
resolve2(false);
|
|
1480
|
-
}
|
|
1481
|
-
});
|
|
1482
|
-
}
|
|
1483
|
-
function askLine(question) {
|
|
1484
|
-
if (options.askLine) return options.askLine(question);
|
|
1485
|
-
return new Promise((resolve2) => {
|
|
1486
|
-
try {
|
|
1487
|
-
rl.question(question, (answer) => resolve2(answer.trim()));
|
|
1488
|
-
} catch {
|
|
1489
|
-
console.log(`
|
|
1490
|
-
[could not read a response — treating as blank]`);
|
|
1491
|
-
resolve2("");
|
|
1492
|
-
}
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
async function reloadAssistant() {
|
|
1496
|
-
const nextPersisted = await configStore.load();
|
|
1497
|
-
({ config, overriddenKeys } = resolveConfig(nextPersisted, envOverrides));
|
|
1498
|
-
assistant = options.assistant ?? await buildAssistant(config, { dataDir, backend, remindersFile });
|
|
1499
|
-
}
|
|
1500
|
-
async function handleConfigCommand(args) {
|
|
1501
|
-
if (args.length === 0) {
|
|
1502
|
-
console.log(`
|
|
1503
|
-
${formatConfigListing(config, overriddenKeys)}
|
|
1504
|
-
`);
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
if (args[0] === "set") {
|
|
1508
|
-
const key = args[1];
|
|
1509
|
-
const raw = args.slice(2).join(" ");
|
|
1510
|
-
if (!key || !isConfigKey(key)) {
|
|
1511
|
-
console.log(`
|
|
1512
|
-
✗ Unknown config key "${key ?? ""}". Known keys: ${CONFIG_KEYS.join(", ")}
|
|
1513
|
-
`);
|
|
1514
|
-
return;
|
|
1515
|
-
}
|
|
1516
|
-
if (!raw) {
|
|
1517
|
-
console.log(`
|
|
1518
|
-
Usage: /config set ${key} <value>
|
|
1519
|
-
`);
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
if (overriddenKeys.has(key)) {
|
|
1523
|
-
console.log(`
|
|
1524
|
-
✗ "${key}" is pinned by ${ENV_VAR_FOR_CONFIG_KEY[key]} — unset that env var to change it here.
|
|
1525
|
-
`);
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
let value;
|
|
1529
|
-
try {
|
|
1530
|
-
value = parseConfigValue(key, raw);
|
|
1531
|
-
} catch (err) {
|
|
1532
|
-
if (!(err instanceof ConfigValueParseError)) throw err;
|
|
1533
|
-
console.log(`
|
|
1534
|
-
✗ ${err.message}
|
|
1535
|
-
`);
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
const patch = { [key]: value };
|
|
1539
|
-
try {
|
|
1540
|
-
validateConfig(patch, config);
|
|
1541
|
-
} catch (err) {
|
|
1542
|
-
if (!(err instanceof ConfigValidationError)) throw err;
|
|
1543
|
-
console.log(`
|
|
1544
|
-
✗ ${err.message}
|
|
1545
|
-
`);
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
await configStore.save(patch);
|
|
1549
|
-
await reloadAssistant();
|
|
1550
|
-
console.log(`
|
|
1551
|
-
✓ ${key} updated (took effect immediately, no restart needed)
|
|
1552
|
-
`);
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
if (args[0] === "reset") {
|
|
1556
|
-
const key = args[1];
|
|
1557
|
-
if (key && !isConfigKey(key)) {
|
|
1558
|
-
console.log(`
|
|
1559
|
-
✗ Unknown config key "${key}". Known keys: ${CONFIG_KEYS.join(", ")}
|
|
1560
|
-
`);
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
const target = key ? `"${key}"` : "ALL settings";
|
|
1564
|
-
const confirmed = await askYesNo(`
|
|
1565
|
-
Reset ${target} to default? This takes effect immediately. (y/N) `);
|
|
1566
|
-
if (!confirmed) {
|
|
1567
|
-
console.log("\nCancelled — nothing was reset.\n");
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
const clearPatch = key ? { [key]: void 0 } : Object.fromEntries(CONFIG_KEYS.map((k) => [k, void 0]));
|
|
1571
|
-
await configStore.save(clearPatch);
|
|
1572
|
-
await reloadAssistant();
|
|
1573
|
-
console.log(`
|
|
1574
|
-
✓ Reset ${key ?? "all settings"} to default
|
|
1575
|
-
`);
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
console.log("\nUsage: /config | /config set <key> <value> | /config reset [key]\n");
|
|
1579
|
-
}
|
|
1580
|
-
async function handleModel(args) {
|
|
1581
|
-
if (args.length === 0) {
|
|
1582
|
-
console.log(`
|
|
1583
|
-
${config.model ?? "(using each backend's default)"}
|
|
1584
|
-
`);
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
await handleConfigCommand(["set", "model", args.join(" ")]);
|
|
1588
|
-
}
|
|
1589
|
-
async function handleProject(args) {
|
|
1590
|
-
if (args.length === 0) {
|
|
1591
|
-
console.log(`
|
|
1592
|
-
${assistant.getActiveProject() || "(none)"}
|
|
1593
|
-
`);
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
if (args[0] === "clear") {
|
|
1597
|
-
await handleConfigCommand(["reset", "activeProject"]);
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
await handleConfigCommand(["set", "activeProject", args.join(" ")]);
|
|
1601
|
-
}
|
|
1602
|
-
const commands = {
|
|
1603
|
-
"/why": () => printWhy(),
|
|
1604
|
-
"/layers": () => printLayers(),
|
|
1605
|
-
"/sources": () => printSources(),
|
|
1606
|
-
"/plan": (args) => handlePlan(args),
|
|
1607
|
-
"/help": () => printHelp(),
|
|
1608
|
-
"/clear": () => handleClear(),
|
|
1609
|
-
"/new": () => handleClear(),
|
|
1610
|
-
"/status": () => printStatus(),
|
|
1611
|
-
"/export": (args) => handleExport(args),
|
|
1612
|
-
"/undo": () => handleUndo(),
|
|
1613
|
-
"/undo-action": (args) => handleUndoAction(args),
|
|
1614
|
-
"/memory": (args) => handleMemory(args),
|
|
1615
|
-
"/search": (args) => handleSearch(args),
|
|
1616
|
-
"/model": (args) => handleModel(args),
|
|
1617
|
-
"/project": (args) => handleProject(args),
|
|
1618
|
-
"/cost": () => printCost(),
|
|
1619
|
-
"/doctor": () => handleDoctor(),
|
|
1620
|
-
"/config": (args) => handleConfigCommand(args),
|
|
1621
|
-
"/checkpoint": (args) => handleCheckpoint(args)
|
|
1622
|
-
};
|
|
1623
|
-
async function dispatchOne(message) {
|
|
1624
|
-
const [token, ...args] = message.split(/\s+/);
|
|
1625
|
-
const handler = commands[token];
|
|
1626
|
-
if (handler) {
|
|
1627
|
-
await handler(args);
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
await handleTurn(message);
|
|
1631
|
-
}
|
|
1632
|
-
let dispatchQueue = Promise.resolve();
|
|
1633
|
-
function enqueue(message) {
|
|
1634
|
-
const result = dispatchQueue.then(() => dispatchOne(message)).catch((err) => {
|
|
1635
|
-
console.error("[unexpected error]", err);
|
|
1636
|
-
});
|
|
1637
|
-
dispatchQueue = result;
|
|
1638
|
-
return result;
|
|
1639
|
-
}
|
|
1640
|
-
rl.on("line", (line) => {
|
|
1641
|
-
const message = line.trim();
|
|
1642
|
-
if (!message) {
|
|
1643
|
-
rl.prompt();
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
void enqueue(message).finally(() => rl.prompt());
|
|
1647
|
-
});
|
|
1648
|
-
rl.resume();
|
|
1649
|
-
return {
|
|
1650
|
-
dispatchLine: async (line) => {
|
|
1651
|
-
const message = line.trim();
|
|
1652
|
-
if (!message) return;
|
|
1653
|
-
await enqueue(message);
|
|
1654
|
-
},
|
|
1655
|
-
close: () => rl.close(),
|
|
1656
|
-
getStatusIndicators: async () => {
|
|
1657
|
-
const indicators = [];
|
|
1658
|
-
indicators.push(`Workspace: ${config.workspaceRoot ?? process.cwd()}`);
|
|
1659
|
-
if (config.dangerouslySkipPermissions) indicators.push("⚠ Permissions: auto-approved (dangerouslySkipPermissions)");
|
|
1660
|
-
if (lastPlanStatus !== void 0) indicators.push("Plan mode: active");
|
|
1661
|
-
const spendCapLine = await spendCapStatusLine();
|
|
1662
|
-
if (spendCapLine) indicators.push(spendCapLine);
|
|
1663
|
-
return indicators;
|
|
1664
|
-
}
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
1667
|
-
async function main() {
|
|
1668
|
-
const persisted = await defaultConfigStore.load();
|
|
1669
|
-
const { config } = resolveConfig(persisted, defaultEnvOverrides);
|
|
1670
|
-
if (shouldLaunchTuiApp(config.tuiMode ?? "disabled", Boolean(process.stdout.isTTY), Boolean(process.stdin.isTTY))) {
|
|
1671
|
-
const { runTuiApp } = await import("./tui-app-DxLPSGh2.js");
|
|
1672
|
-
await runTuiApp();
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
await runCli();
|
|
1676
|
-
}
|
|
1677
|
-
function entryArgMatchesModule(entryArg, moduleHref, resolveReal = realpathSync) {
|
|
1678
|
-
if (entryArg === void 0) return false;
|
|
1679
|
-
try {
|
|
1680
|
-
return moduleHref === pathToFileURL(resolveReal(entryArg)).href;
|
|
1681
|
-
} catch {
|
|
1682
|
-
return false;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
function isEntryModule() {
|
|
1686
|
-
return entryArgMatchesModule(process.argv[1], import.meta.url);
|
|
1687
|
-
}
|
|
1688
|
-
if (isEntryModule()) void main();
|
|
1689
|
-
export {
|
|
1690
|
-
entryArgMatchesModule,
|
|
1691
|
-
runCli
|
|
1692
|
-
};
|
|
2
|
+
import "./bin-CqkqT6KK.js";
|
|
1693
3
|
//# sourceMappingURL=cli.js.map
|