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