audacity-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +247 -0
  3. package/dist/index.js +1805 -0
  4. package/package.json +35 -0
package/dist/index.js ADDED
@@ -0,0 +1,1805 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import path3 from "path";
5
+ import { Command } from "commander";
6
+
7
+ // src/ui.ts
8
+ var useColor = () => process.stdout.isTTY === true && !process.env.NO_COLOR;
9
+ var wrap = (code, s) => useColor() ? `${code}${s}\x1B[0m` : s;
10
+ var cyan = (s) => wrap("\x1B[36m", s);
11
+ var dim = (s) => wrap("\x1B[2m", s);
12
+ var bold = (s) => wrap("\x1B[1m", s);
13
+ var yellow = (s) => wrap("\x1B[33m", s);
14
+ var italic = (s) => wrap("\x1B[3m", s);
15
+ var underline = (s) => wrap("\x1B[4m", s);
16
+ var ART = [
17
+ " \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
18
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D",
19
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D ",
20
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D ",
21
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ",
22
+ "\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D "
23
+ ];
24
+ function frameWidth() {
25
+ const max = ART[0]?.length ?? 60;
26
+ const cols = process.stdout.columns ?? max;
27
+ return Math.max(20, Math.min(max, cols - 1));
28
+ }
29
+ function printBanner() {
30
+ console.log("");
31
+ for (const row of ART) console.log(` ${cyan(row)}`);
32
+ console.log("");
33
+ }
34
+ function hRule() {
35
+ return dim("\u2500".repeat(frameWidth()));
36
+ }
37
+ function promptString(badge = "") {
38
+ return `${badge}${cyan("\u276F")} `;
39
+ }
40
+
41
+ // src/markdown.ts
42
+ var defaultStyle = { bold, cyan, dim, italic, underline };
43
+ var FENCE = /^\s*```/;
44
+ var HEADING = /^(#{1,6})\s+(.*)$/;
45
+ var HR = /^\s*([-*_])\1{2,}\s*$/;
46
+ var BLOCKQUOTE = /^(\s*)>\s?(.*)$/;
47
+ var UL = /^(\s*)[-*+]\s+(.*)$/;
48
+ var OL = /^(\s*)(\d+)\.\s+(.*)$/;
49
+ var NUL = String.fromCharCode(0);
50
+ var RESTORE = new RegExp(`${NUL}(\\d+)${NUL}`, "g");
51
+ function inline(text, style = defaultStyle) {
52
+ const spans = [];
53
+ let out = text.replace(/`([^`]+)`/g, (_m, code) => {
54
+ spans.push(style.cyan(code));
55
+ return `${NUL}${spans.length - 1}${NUL}`;
56
+ });
57
+ out = out.replace(
58
+ /\[([^\]]+)\]\(([^)]+)\)/g,
59
+ (_m, label, url) => `${style.underline(label)}${style.dim(` (${url})`)}`
60
+ );
61
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_m, s) => style.bold(s));
62
+ out = out.replace(/__([^_]+)__/g, (_m, s) => style.bold(s));
63
+ out = out.replace(/\*([^*]+)\*/g, (_m, s) => style.italic(s));
64
+ out = out.replace(/(?<![A-Za-z0-9])_([^_]+)_(?![A-Za-z0-9])/g, (_m, s) => style.italic(s));
65
+ out = out.replace(RESTORE, (_m, i) => spans[Number(i)] ?? "");
66
+ return out;
67
+ }
68
+ function renderer(write = (s) => void process.stdout.write(s), style = defaultStyle) {
69
+ let buf = "";
70
+ let insideFence = false;
71
+ const renderLine = (line, newline) => {
72
+ let rendered;
73
+ if (FENCE.test(line)) {
74
+ insideFence = !insideFence;
75
+ return;
76
+ }
77
+ if (insideFence) {
78
+ rendered = `${style.dim("\u2502 ")}${line}`;
79
+ } else {
80
+ const heading = HEADING.exec(line);
81
+ const hr = HR.exec(line);
82
+ const quote = BLOCKQUOTE.exec(line);
83
+ const ul = UL.exec(line);
84
+ const ol = OL.exec(line);
85
+ if (heading) {
86
+ rendered = style.bold(style.cyan(heading[2] ?? ""));
87
+ } else if (hr) {
88
+ rendered = style.dim("\u2500".repeat(frameWidth()));
89
+ } else if (quote) {
90
+ rendered = style.dim(`\u258F ${inline(quote[2] ?? "", style)}`);
91
+ } else if (ul) {
92
+ rendered = `${ul[1] ?? ""}\u2022 ${inline(ul[2] ?? "", style)}`;
93
+ } else if (ol) {
94
+ rendered = `${ol[1] ?? ""}${ol[2]}. ${inline(ol[3] ?? "", style)}`;
95
+ } else {
96
+ rendered = inline(line, style);
97
+ }
98
+ }
99
+ write(newline ? `${rendered}
100
+ ` : rendered);
101
+ };
102
+ return {
103
+ write(delta) {
104
+ buf += delta;
105
+ let nl = buf.indexOf("\n");
106
+ while (nl !== -1) {
107
+ renderLine(buf.slice(0, nl), true);
108
+ buf = buf.slice(nl + 1);
109
+ nl = buf.indexOf("\n");
110
+ }
111
+ },
112
+ end() {
113
+ if (buf.length) renderLine(buf, false);
114
+ buf = "";
115
+ insideFence = false;
116
+ }
117
+ };
118
+ }
119
+ function createMarkdownStream(write = (s) => void process.stdout.write(s)) {
120
+ if (!useColor()) {
121
+ return { write: (d) => write(d), end: () => {
122
+ } };
123
+ }
124
+ return renderer(write);
125
+ }
126
+
127
+ // src/model.ts
128
+ var ModelError = class extends Error {
129
+ constructor(status, message) {
130
+ super(message);
131
+ this.status = status;
132
+ this.name = "ModelError";
133
+ }
134
+ };
135
+ function extractErrorMessage(body, status) {
136
+ try {
137
+ const parsed = JSON.parse(body);
138
+ if (parsed.error?.message) return parsed.error.message;
139
+ } catch {
140
+ }
141
+ return body.trim() || `Request failed with status ${status}`;
142
+ }
143
+ async function streamCompletion(params, cfg) {
144
+ const base = cfg.apiUrl.replace(/\/+$/, "");
145
+ const path4 = params.rag ? "/v1/agent/chat/completions" : "/v1/chat/completions";
146
+ const body = {
147
+ model: params.model,
148
+ messages: params.messages,
149
+ stream: true,
150
+ stream_options: { include_usage: true }
151
+ };
152
+ if (!params.rag && params.tools?.length) {
153
+ body.tools = params.tools;
154
+ body.tool_choice = "auto";
155
+ }
156
+ if (params.rag && params.conversationId) {
157
+ body.conversation_id = params.conversationId;
158
+ }
159
+ const headers = {
160
+ Authorization: `Bearer ${cfg.apiKey}`,
161
+ "Content-Type": "application/json",
162
+ Accept: "text/event-stream"
163
+ };
164
+ if (params.rag && params.conversationId) {
165
+ headers["X-Conversation-Id"] = params.conversationId;
166
+ }
167
+ let res;
168
+ try {
169
+ res = await fetch(`${base}${path4}`, {
170
+ method: "POST",
171
+ headers,
172
+ body: JSON.stringify(body)
173
+ });
174
+ } catch (err) {
175
+ const reason = err instanceof Error ? err.message : String(err);
176
+ throw new ModelError(0, `Could not reach ${base}: ${reason}`);
177
+ }
178
+ if (!res.ok) {
179
+ const text = await res.text().catch(() => "");
180
+ throw new ModelError(res.status, extractErrorMessage(text, res.status));
181
+ }
182
+ if (!res.body) {
183
+ throw new ModelError(502, "Upstream returned no response body.");
184
+ }
185
+ const conversationId = res.headers.get("x-conversation-id") ?? params.conversationId;
186
+ let content = "";
187
+ const toolCallsByIndex = /* @__PURE__ */ new Map();
188
+ let finishReason = null;
189
+ let usage;
190
+ const reader = res.body.getReader();
191
+ const decoder = new TextDecoder();
192
+ let buffer = "";
193
+ const handleChunk = (chunk) => {
194
+ if (chunk.usage) usage = chunk.usage;
195
+ const choice = chunk.choices?.[0];
196
+ if (!choice) return;
197
+ const delta = choice.delta ?? {};
198
+ if (typeof delta.content === "string" && delta.content.length > 0) {
199
+ content += delta.content;
200
+ params.onText(delta.content);
201
+ }
202
+ if (Array.isArray(delta.tool_calls)) {
203
+ for (const tc of delta.tool_calls) {
204
+ const idx = tc.index ?? 0;
205
+ const cur = toolCallsByIndex.get(idx) ?? { id: "", name: "", args: "" };
206
+ if (tc.id) cur.id = tc.id;
207
+ if (tc.function?.name) cur.name = tc.function.name;
208
+ if (tc.function?.arguments) cur.args += tc.function.arguments;
209
+ toolCallsByIndex.set(idx, cur);
210
+ }
211
+ }
212
+ if (choice.finish_reason) finishReason = choice.finish_reason;
213
+ };
214
+ const processLine = (line) => {
215
+ const trimmed = line.trim();
216
+ if (!trimmed.startsWith("data:")) return;
217
+ const data = trimmed.slice(5).trim();
218
+ if (!data || data === "[DONE]") return;
219
+ try {
220
+ handleChunk(JSON.parse(data));
221
+ } catch {
222
+ }
223
+ };
224
+ for (; ; ) {
225
+ const { done, value } = await reader.read();
226
+ if (done) break;
227
+ buffer += decoder.decode(value, { stream: true });
228
+ const lines = buffer.split("\n");
229
+ buffer = lines.pop() ?? "";
230
+ for (const line of lines) processLine(line);
231
+ }
232
+ if (buffer.trim()) processLine(buffer);
233
+ const toolCalls = [...toolCallsByIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, v]) => ({
234
+ id: v.id,
235
+ type: "function",
236
+ function: { name: v.name, arguments: v.args }
237
+ }));
238
+ return { content, toolCalls, finishReason, usage, conversationId: conversationId ?? void 0 };
239
+ }
240
+
241
+ // src/models.ts
242
+ var DEFAULT_MODEL = "gemini-2.5-flash";
243
+ var COMPACT_MODEL = "gemini-2.5-flash";
244
+ var AVAILABLE_MODELS = [
245
+ // Google (tool-capable, good defaults)
246
+ "gemini-2.5-flash",
247
+ "gemini-2.5-pro",
248
+ "gemini-3-flash-preview",
249
+ // Anthropic
250
+ "claude-opus-4-8",
251
+ "claude-opus-4-7",
252
+ "claude-opus-4-6",
253
+ "claude-sonnet-4-6",
254
+ "claude-haiku-4-5-20251001",
255
+ // OpenAI
256
+ "gpt-5.5",
257
+ "gpt-5.4",
258
+ "gpt-5.4-mini",
259
+ "gpt-4o",
260
+ "gpt-4o-mini",
261
+ "gpt-4-turbo",
262
+ // xAI
263
+ "grok-4-3",
264
+ "grok-4-1-fast-reasoning",
265
+ "grok-4-1-fast-non-reasoning",
266
+ "grok-3",
267
+ "grok-3-mini",
268
+ // DeepSeek
269
+ "deepseek-chat",
270
+ "deepseek-reasoner",
271
+ // Moonshot / Kimi
272
+ "moonshot-v1-8k",
273
+ "moonshot-v1-32k",
274
+ "moonshot-v1-128k",
275
+ // Mistral
276
+ "mistral-large",
277
+ "mistral-small",
278
+ // Meta Llama
279
+ "llama-4-maverick",
280
+ "llama-4-scout",
281
+ // Perplexity Sonar (note: no tool-calling support — weak for the coding agent)
282
+ "sonar-pro",
283
+ "sonar",
284
+ "sonar-reasoning-pro",
285
+ "sonar-deep-research"
286
+ ];
287
+
288
+ // src/modes.ts
289
+ var AGENT_MODES = [
290
+ { id: "normal", label: "normal", description: "approve each tool action", autoApprove: false, gate: "executing" },
291
+ {
292
+ id: "plan",
293
+ label: "plan",
294
+ description: "no project changes without approval; draft a plan",
295
+ autoApprove: false,
296
+ gate: "planning"
297
+ },
298
+ { id: "auto", label: "auto-approve", description: "run tool actions without asking", autoApprove: true, gate: "executing" }
299
+ ];
300
+ var DEFAULT_MODE_ID = "normal";
301
+ function initialModeIndex(autoApprove) {
302
+ const i = AGENT_MODES.findIndex((m) => m.autoApprove === autoApprove);
303
+ return i >= 0 ? i : 0;
304
+ }
305
+ function currentMode(session) {
306
+ return AGENT_MODES[session.modeIndex] ?? AGENT_MODES[0];
307
+ }
308
+ function setMode(session, index) {
309
+ const n = AGENT_MODES.length;
310
+ const i = (index % n + n) % n;
311
+ const mode = AGENT_MODES[i];
312
+ session.modeIndex = i;
313
+ session.autoApprove = mode.autoApprove;
314
+ return mode;
315
+ }
316
+ function cycleMode(session) {
317
+ return setMode(session, session.modeIndex + 1);
318
+ }
319
+
320
+ // src/tools.ts
321
+ import { spawn } from "child_process";
322
+ import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
323
+ import path from "path";
324
+ var MAX_OUTPUT = 3e4;
325
+ var READ_ONLY = /* @__PURE__ */ new Set(["read_file", "list_files", "grep"]);
326
+ var CONTROL = /* @__PURE__ */ new Set(["present_plan", "ask_choice"]);
327
+ function isControlTool(name) {
328
+ return CONTROL.has(name);
329
+ }
330
+ function cap(text) {
331
+ if (text.length <= MAX_OUTPUT) return text;
332
+ return `${text.slice(0, MAX_OUTPUT)}
333
+ \u2026[truncated ${text.length - MAX_OUTPUT} chars]`;
334
+ }
335
+ function safeResolve(cwd, p) {
336
+ const resolved = path.resolve(cwd, p || ".");
337
+ const rel = path.relative(cwd, resolved);
338
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
339
+ throw new Error(`Path escapes the working directory: ${p}`);
340
+ }
341
+ return resolved;
342
+ }
343
+ function str(v) {
344
+ return typeof v === "string" ? v : v == null ? "" : String(v);
345
+ }
346
+ function getToolSchemas() {
347
+ return [
348
+ {
349
+ type: "function",
350
+ function: {
351
+ name: "read_file",
352
+ description: "Read a UTF-8 text file relative to the working directory.",
353
+ parameters: {
354
+ type: "object",
355
+ properties: { path: { type: "string", description: "File path relative to cwd." } },
356
+ required: ["path"]
357
+ }
358
+ }
359
+ },
360
+ {
361
+ type: "function",
362
+ function: {
363
+ name: "list_files",
364
+ description: "List entries in a directory (directories shown with a trailing /).",
365
+ parameters: {
366
+ type: "object",
367
+ properties: { path: { type: "string", description: "Directory path relative to cwd. Defaults to '.'." } }
368
+ }
369
+ }
370
+ },
371
+ {
372
+ type: "function",
373
+ function: {
374
+ name: "grep",
375
+ description: "Search file contents for a regex pattern. Returns `path:line: text` matches.",
376
+ parameters: {
377
+ type: "object",
378
+ properties: {
379
+ pattern: { type: "string", description: "Regular expression to search for." },
380
+ path: { type: "string", description: "File or directory to search. Defaults to '.'." }
381
+ },
382
+ required: ["pattern"]
383
+ }
384
+ }
385
+ },
386
+ {
387
+ type: "function",
388
+ function: {
389
+ name: "edit_file",
390
+ description: "Replace an exact string in a file. old_str must occur exactly once or the edit fails.",
391
+ parameters: {
392
+ type: "object",
393
+ properties: {
394
+ path: { type: "string" },
395
+ old_str: { type: "string", description: "Exact text to replace (must be unique in the file)." },
396
+ new_str: { type: "string", description: "Replacement text." }
397
+ },
398
+ required: ["path", "old_str", "new_str"]
399
+ }
400
+ }
401
+ },
402
+ {
403
+ type: "function",
404
+ function: {
405
+ name: "write_file",
406
+ description: "Create or overwrite a file with the given content.",
407
+ parameters: {
408
+ type: "object",
409
+ properties: {
410
+ path: { type: "string" },
411
+ content: { type: "string" }
412
+ },
413
+ required: ["path", "content"]
414
+ }
415
+ }
416
+ },
417
+ {
418
+ type: "function",
419
+ function: {
420
+ name: "run_bash",
421
+ description: "Run a shell command in the working directory (60s timeout). Returns combined stdout+stderr.",
422
+ parameters: {
423
+ type: "object",
424
+ properties: { command: { type: "string" } },
425
+ required: ["command"]
426
+ }
427
+ }
428
+ },
429
+ {
430
+ type: "function",
431
+ function: {
432
+ name: "present_plan",
433
+ description: "Call when exploration is complete and you have a concrete plan. Ends plan mode and requests user approval before any change is made. Do not call any mutating tool before this is approved. If open_questions is non-empty, prefer resolving them with ask_choice first.",
434
+ parameters: {
435
+ type: "object",
436
+ properties: {
437
+ title: { type: "string", description: "Short imperative title, e.g. 'Add retry to IAM token refresh'." },
438
+ summary: { type: "string", description: "1-3 sentences on the approach and why." },
439
+ steps: {
440
+ type: "array",
441
+ items: { type: "string" },
442
+ description: "Ordered, concrete steps. Each references specific files/functions."
443
+ },
444
+ files_to_change: {
445
+ type: "array",
446
+ items: { type: "string" },
447
+ description: "Relative paths this plan will create or modify."
448
+ },
449
+ open_questions: {
450
+ type: "array",
451
+ items: { type: "string" },
452
+ description: "Anything still unresolved. If non-empty, prefer ask_choice first."
453
+ }
454
+ },
455
+ required: ["title", "summary", "steps"]
456
+ }
457
+ }
458
+ },
459
+ {
460
+ type: "function",
461
+ function: {
462
+ name: "ask_choice",
463
+ description: "Ask the user to choose between options when you lack the information to proceed confidently. Use this instead of guessing or asking in prose. Provide 2-5 concrete, mutually exclusive options. Two extra options ('Type something else' and 'Chat about this') are added automatically \u2014 do not include them yourself.",
464
+ parameters: {
465
+ type: "object",
466
+ properties: {
467
+ question: { type: "string", description: "One clear question." },
468
+ options: {
469
+ type: "array",
470
+ minItems: 2,
471
+ maxItems: 5,
472
+ items: {
473
+ type: "object",
474
+ properties: {
475
+ label: { type: "string", description: "Short, scannable." },
476
+ detail: { type: "string", description: "Optional one-liner shown under the label." }
477
+ },
478
+ required: ["label"]
479
+ },
480
+ description: "2-5 concrete, mutually exclusive options."
481
+ }
482
+ },
483
+ required: ["question", "options"]
484
+ }
485
+ }
486
+ }
487
+ ];
488
+ }
489
+ function describeToolCall(name, args) {
490
+ switch (name) {
491
+ case "run_bash":
492
+ return `$ ${str(args.command)}`;
493
+ case "write_file":
494
+ return `write ${str(args.path)} (${Buffer.byteLength(str(args.content))} bytes)`;
495
+ case "edit_file":
496
+ return `edit ${str(args.path)}
497
+ - ${preview(str(args.old_str))}
498
+ + ${preview(str(args.new_str))}`;
499
+ default:
500
+ return `${name}(${JSON.stringify(args)})`;
501
+ }
502
+ }
503
+ function preview(s) {
504
+ const oneLine = s.replace(/\n/g, "\u23CE");
505
+ return oneLine.length > 80 ? `${oneLine.slice(0, 80)}\u2026` : oneLine;
506
+ }
507
+ async function executeTool(name, args, cwd) {
508
+ switch (name) {
509
+ case "read_file":
510
+ return cap(await readFile(safeResolve(cwd, str(args.path)), "utf8"));
511
+ case "list_files": {
512
+ const dir = safeResolve(cwd, str(args.path) || ".");
513
+ const entries = await readdir(dir, { withFileTypes: true });
514
+ const listed = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort((a, b) => a.localeCompare(b));
515
+ return listed.length ? listed.join("\n") : "(empty directory)";
516
+ }
517
+ case "grep": {
518
+ const pattern = str(args.pattern);
519
+ const target = safeResolve(cwd, str(args.path) || ".");
520
+ const viaRg = await ripgrep(pattern, target, cwd);
521
+ return viaRg ?? await jsGrep(pattern, target, cwd);
522
+ }
523
+ case "edit_file": {
524
+ const file = safeResolve(cwd, str(args.path));
525
+ const oldStr = str(args.old_str);
526
+ const newStr = str(args.new_str);
527
+ if (!oldStr) throw new Error("old_str must not be empty");
528
+ const content = await readFile(file, "utf8");
529
+ const occurrences = content.split(oldStr).length - 1;
530
+ if (occurrences === 0) throw new Error("old_str not found in file");
531
+ if (occurrences > 1) {
532
+ throw new Error(`old_str matches ${occurrences} times; it must match exactly once`);
533
+ }
534
+ await writeFile(file, content.replace(oldStr, newStr), "utf8");
535
+ return `Edited ${path.relative(cwd, file)} (1 replacement)`;
536
+ }
537
+ case "write_file": {
538
+ const file = safeResolve(cwd, str(args.path));
539
+ const content = str(args.content);
540
+ await mkdir(path.dirname(file), { recursive: true });
541
+ await writeFile(file, content, "utf8");
542
+ return `Wrote ${Buffer.byteLength(content)} bytes to ${path.relative(cwd, file)}`;
543
+ }
544
+ case "run_bash":
545
+ return runBash(str(args.command), cwd);
546
+ default:
547
+ throw new Error(`Unknown tool: ${name}`);
548
+ }
549
+ }
550
+ function ripgrep(pattern, target, cwd) {
551
+ const rel = path.relative(cwd, target) || ".";
552
+ return new Promise((resolve) => {
553
+ const child = spawn("rg", ["--line-number", "--no-heading", "--color", "never", pattern, rel], {
554
+ cwd
555
+ });
556
+ let out = "";
557
+ child.stdout.on("data", (d) => {
558
+ out += d.toString();
559
+ });
560
+ child.on("error", () => resolve(null));
561
+ child.on("close", (code) => {
562
+ if (code === 0) resolve(cap(out) || "(no matches)");
563
+ else if (code === 1) resolve("(no matches)");
564
+ else resolve(null);
565
+ });
566
+ });
567
+ }
568
+ async function jsGrep(pattern, target, cwd) {
569
+ let regex = null;
570
+ try {
571
+ regex = new RegExp(pattern);
572
+ } catch {
573
+ regex = null;
574
+ }
575
+ const skipDirs = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", ".cache", ".turbo"]);
576
+ const matches = [];
577
+ const MAX = 300;
578
+ const scanFile = async (full) => {
579
+ let text;
580
+ try {
581
+ text = await readFile(full, "utf8");
582
+ } catch {
583
+ return;
584
+ }
585
+ if (text.includes("\0")) return;
586
+ const lines = text.split("\n");
587
+ for (let i = 0; i < lines.length && matches.length < MAX; i++) {
588
+ const line = lines[i] ?? "";
589
+ const hit = regex ? regex.test(line) : line.includes(pattern);
590
+ if (hit) matches.push(`${path.relative(cwd, full)}:${i + 1}: ${line.trim()}`);
591
+ }
592
+ };
593
+ const walk = async (dir) => {
594
+ if (matches.length >= MAX) return;
595
+ let entries;
596
+ try {
597
+ entries = await readdir(dir, { withFileTypes: true });
598
+ } catch {
599
+ return;
600
+ }
601
+ for (const e of entries) {
602
+ if (matches.length >= MAX) return;
603
+ const full = path.join(dir, e.name);
604
+ if (e.isDirectory()) {
605
+ if (!skipDirs.has(e.name)) await walk(full);
606
+ } else if (e.isFile()) {
607
+ await scanFile(full);
608
+ }
609
+ }
610
+ };
611
+ const info = await stat(target).catch(() => null);
612
+ if (info?.isFile()) await scanFile(target);
613
+ else await walk(target);
614
+ return matches.length ? cap(matches.join("\n")) : "(no matches)";
615
+ }
616
+ function runBash(command, cwd) {
617
+ if (!command.trim()) return Promise.resolve("ERROR: empty command");
618
+ return new Promise((resolve) => {
619
+ const child = spawn(command, { cwd, shell: true });
620
+ let out = "";
621
+ const onData = (d) => {
622
+ out += d.toString();
623
+ if (out.length > MAX_OUTPUT * 2) {
624
+ out = out.slice(0, MAX_OUTPUT * 2);
625
+ child.kill("SIGKILL");
626
+ }
627
+ };
628
+ child.stdout.on("data", onData);
629
+ child.stderr.on("data", onData);
630
+ const timer = setTimeout(() => {
631
+ child.kill("SIGKILL");
632
+ out += "\n[timed out after 60s]";
633
+ }, 6e4);
634
+ child.on("error", (e) => {
635
+ clearTimeout(timer);
636
+ resolve(`ERROR: ${e.message}`);
637
+ });
638
+ child.on("close", (code) => {
639
+ clearTimeout(timer);
640
+ resolve(`${cap(out)}
641
+ [exit ${code ?? "killed"}]`);
642
+ });
643
+ });
644
+ }
645
+
646
+ // src/policy.ts
647
+ function decideTool(name, session) {
648
+ if (isControlTool(name)) return { kind: "control_flow" };
649
+ const planning = currentMode(session).gate === "planning";
650
+ if (planning) {
651
+ if (READ_ONLY.has(name)) return { kind: "execute", approval: "none" };
652
+ if (name === "run_bash") return { kind: "execute", approval: "ask" };
653
+ return {
654
+ kind: "block",
655
+ message: `${name} modifies the project and is not allowed in plan mode. Explore with read_file/list_files/grep (and run_bash, which will ask first), then call present_plan to propose changes for approval.`
656
+ };
657
+ }
658
+ if (READ_ONLY.has(name)) return { kind: "execute", approval: "none" };
659
+ return { kind: "execute", approval: session.autoApprove ? "auto" : "ask" };
660
+ }
661
+
662
+ // src/agent.ts
663
+ var MAX_ITERATIONS = 50;
664
+ var MAX_MESSAGES = 100;
665
+ var KEEP_RECENT = 2;
666
+ var SUMMARY_INSTRUCTION = `Summarize the conversation so far into a compact briefing so you (the assistant) can keep working seamlessly after older turns are dropped. Capture: the user's goals, key decisions, files read or changed, important facts discovered, and any open or pending tasks. Be thorough on anything needed to continue; skip pleasantries. Output only the summary.`;
667
+ function trimContext(messages) {
668
+ if (messages.length <= MAX_MESSAGES) return;
669
+ const head = messages[0];
670
+ const system = head && head.role === "system" ? [head] : [];
671
+ const tail = messages.slice(-(MAX_MESSAGES - system.length));
672
+ while (tail.length && tail[0]?.role === "tool") tail.shift();
673
+ messages.splice(0, messages.length, ...system, ...tail);
674
+ }
675
+ function buildCompacted(messages, summary, keepRecent = KEEP_RECENT) {
676
+ const system = messages[0]?.role === "system" ? [messages[0]] : [];
677
+ const body = messages.slice(system.length);
678
+ let recent = keepRecent > 0 ? body.slice(-keepRecent) : [];
679
+ while (recent.length && recent[0]?.role === "tool") recent = recent.slice(1);
680
+ const summaryMsg = {
681
+ role: "user",
682
+ content: `[Summary of earlier conversation]
683
+ ${summary}`
684
+ };
685
+ return [...system, summaryMsg, ...recent];
686
+ }
687
+ async function compactSession(session, cfg, keepRecent = KEEP_RECENT) {
688
+ const msgs = session.messages;
689
+ const bodyLen = msgs.length - (msgs[0]?.role === "system" ? 1 : 0);
690
+ if (bodyLen <= keepRecent) return null;
691
+ const model = cfg.compactModel || COMPACT_MODEL;
692
+ const turn = await streamCompletion(
693
+ {
694
+ messages: [...msgs, { role: "user", content: SUMMARY_INSTRUCTION }],
695
+ model,
696
+ rag: false,
697
+ onText: () => {
698
+ }
699
+ },
700
+ cfg
701
+ );
702
+ const summary = turn.content.trim();
703
+ if (!summary) return null;
704
+ const before = msgs.length;
705
+ session.messages = buildCompacted(msgs, summary, keepRecent);
706
+ return { before, after: session.messages.length, summary, model };
707
+ }
708
+ function modeReminder(session) {
709
+ if (currentMode(session).gate === "planning") {
710
+ return "You are in PLAN MODE. Do not modify the project \u2014 edit_file/write_file are blocked, and run_bash requires the user's approval per command. Explore freely with read_file, list_files, and grep; resolve open questions with ask_choice. When you have a concrete plan, call present_plan \u2014 do not attempt any change before it is approved.";
711
+ }
712
+ if (session.activePlan) {
713
+ const steps = session.activePlan.steps.map((s, i) => `${i + 1}. ${s}`).join("\n");
714
+ return `APPROVED PLAN \u2014 "${session.activePlan.title}"
715
+ ${session.activePlan.summary}
716
+
717
+ ${steps}
718
+
719
+ Execute this approved plan, following the steps above.`;
720
+ }
721
+ return null;
722
+ }
723
+ function summarize(args) {
724
+ const key = args.path ?? args.command ?? args.pattern;
725
+ return key != null ? String(key) : JSON.stringify(args);
726
+ }
727
+ function parseArgs(tc) {
728
+ try {
729
+ return tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
730
+ } catch {
731
+ return null;
732
+ }
733
+ }
734
+ async function runToolCall(tc, session, approve) {
735
+ const args = parseArgs(tc);
736
+ if (args === null) return `ERROR: tool arguments were not valid JSON: ${tc.function.arguments}`;
737
+ process.stdout.write(`
738
+ \u23FA ${tc.function.name}(${summarize(args)})
739
+ `);
740
+ const decision = decideTool(tc.function.name, session);
741
+ if (decision.kind === "block") {
742
+ console.log(` \u2717 ${decision.message}`);
743
+ return decision.message;
744
+ }
745
+ if (decision.kind === "control_flow") {
746
+ return "ERROR: control-flow tool was routed to the executor.";
747
+ }
748
+ if (decision.approval === "ask") {
749
+ const ok = await approve(tc.function.name, describeToolCall(tc.function.name, args));
750
+ if (!ok) {
751
+ console.log(" \u2717 denied");
752
+ return "ERROR: the user denied permission to run this action.";
753
+ }
754
+ }
755
+ try {
756
+ const result = await executeTool(tc.function.name, args, session.cwd);
757
+ const firstLine = result.split("\n")[0] ?? "";
758
+ const more = result.length > firstLine.length ? " \u2026" : "";
759
+ console.log(` \u21B3 ${firstLine.slice(0, 200)}${more}`);
760
+ return result;
761
+ } catch (err) {
762
+ const msg = err instanceof Error ? err.message : String(err);
763
+ console.log(` \u2717 ${msg}`);
764
+ return `ERROR: ${msg}`;
765
+ }
766
+ }
767
+ async function runControlTool(tc, session, control) {
768
+ const args = parseArgs(tc);
769
+ if (args === null) {
770
+ return { toolResult: `ERROR: tool arguments were not valid JSON: ${tc.function.arguments}`, next: "continue" };
771
+ }
772
+ if (tc.function.name === "present_plan") {
773
+ return control.presentPlan(args, session);
774
+ }
775
+ return control.askChoice(args, session);
776
+ }
777
+ async function runTurn(session, cfg, approve, control) {
778
+ const tools = getToolSchemas();
779
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
780
+ trimContext(session.messages);
781
+ const reminder = modeReminder(session);
782
+ const outgoing = reminder ? [...session.messages, { role: "system", content: reminder }] : session.messages;
783
+ const md = createMarkdownStream();
784
+ let turn;
785
+ try {
786
+ turn = await streamCompletion(
787
+ {
788
+ messages: outgoing,
789
+ tools,
790
+ model: session.model,
791
+ rag: false,
792
+ onText: (delta) => md.write(delta)
793
+ },
794
+ cfg
795
+ );
796
+ } catch (err) {
797
+ if (err instanceof ModelError) {
798
+ console.error(`
799
+ [audacity] request failed (${err.status}): ${err.message}`);
800
+ if (err.status === 401) {
801
+ console.error("[audacity] run /key and enter a valid API key.");
802
+ }
803
+ return;
804
+ }
805
+ throw err;
806
+ }
807
+ md.end();
808
+ session.messages.push({
809
+ role: "assistant",
810
+ content: turn.content || null,
811
+ ...turn.toolCalls.length ? { tool_calls: turn.toolCalls } : {}
812
+ });
813
+ process.stdout.write("\n");
814
+ if (turn.finishReason !== "tool_calls" || turn.toolCalls.length === 0) {
815
+ return;
816
+ }
817
+ let stop = false;
818
+ for (const tc of turn.toolCalls) {
819
+ let content;
820
+ if (stop) {
821
+ content = "Skipped \u2014 returning to the prompt.";
822
+ } else if (isControlTool(tc.function.name)) {
823
+ const outcome = await runControlTool(tc, session, control);
824
+ content = outcome.toolResult;
825
+ if (outcome.next === "stop") stop = true;
826
+ } else {
827
+ content = await runToolCall(tc, session, approve);
828
+ }
829
+ session.messages.push({ role: "tool", tool_call_id: tc.id, content });
830
+ }
831
+ if (stop) return;
832
+ }
833
+ console.log(`
834
+ [audacity] Reached the ${MAX_ITERATIONS}-iteration limit \u2014 stopping.`);
835
+ }
836
+
837
+ // src/config.ts
838
+ import { chmod, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
839
+ import { homedir } from "os";
840
+ import { join } from "path";
841
+
842
+ // src/interactive.ts
843
+ import { emitKeypressEvents } from "readline";
844
+ import { createInterface } from "readline/promises";
845
+
846
+ // src/prompt.ts
847
+ async function askHidden(rl2, question) {
848
+ const iface = rl2;
849
+ const output = iface.output;
850
+ const original = iface._writeToOutput?.bind(rl2);
851
+ output.write(question);
852
+ let muted = true;
853
+ iface._writeToOutput = (s) => {
854
+ if (!muted) output.write(s);
855
+ };
856
+ try {
857
+ const answer = await rl2.question("");
858
+ return answer.trim();
859
+ } finally {
860
+ iface._writeToOutput = original ?? ((s) => output.write(s));
861
+ muted = false;
862
+ output.write("\n");
863
+ }
864
+ }
865
+ async function pickFromList(rl2, title, items, current) {
866
+ console.log(title);
867
+ items.forEach((item, i) => {
868
+ const marker = item === current ? " \u2190 current" : "";
869
+ console.log(` ${String(i + 1).padStart(2)}. ${item}${marker}`);
870
+ });
871
+ const answer = (await rl2.question("Number (blank to cancel): ")).trim();
872
+ if (!answer) return void 0;
873
+ const n = Number(answer);
874
+ if (!Number.isInteger(n) || n < 1 || n > items.length) {
875
+ console.log("Invalid selection.");
876
+ return void 0;
877
+ }
878
+ return items[n - 1];
879
+ }
880
+
881
+ // src/interactive.ts
882
+ var stdin = process.stdin;
883
+ var stdout = process.stdout;
884
+ var interactive = () => Boolean(stdin.isTTY && stdout.isTTY);
885
+ var ANSI_SGR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
886
+ var visibleLen = (s) => s.replace(ANSI_SGR, "").length;
887
+ var keypressReady = false;
888
+ function ensureKeypress() {
889
+ if (!keypressReady) {
890
+ emitKeypressEvents(stdin);
891
+ keypressReady = true;
892
+ }
893
+ }
894
+ function rawSession(handler) {
895
+ ensureKeypress();
896
+ return new Promise((resolve) => {
897
+ const onKey = (str2, key) => handler(key, str2, finish);
898
+ const finish = (value) => {
899
+ stdin.off("keypress", onKey);
900
+ if (stdin.isTTY) stdin.setRawMode(false);
901
+ stdin.pause();
902
+ resolve(value);
903
+ };
904
+ if (stdin.isTTY) stdin.setRawMode(true);
905
+ stdin.resume();
906
+ stdin.on("keypress", onKey);
907
+ });
908
+ }
909
+ var isPrintable = (str2, key) => Boolean(str2) && !key.ctrl && !key.meta && str2 !== "\r" && str2 !== "\n" && str2 >= " ";
910
+ function wrapText(text, firstWidth, restWidth) {
911
+ const lines = [];
912
+ let cur = "";
913
+ const cap2 = () => Math.max(1, lines.length === 0 ? firstWidth : restWidth);
914
+ for (let word of text.split(" ")) {
915
+ const sep = cur.length > 0 ? " " : "";
916
+ if (cur.length + sep.length + word.length <= cap2()) {
917
+ cur += sep + word;
918
+ continue;
919
+ }
920
+ if (cur.length > 0) {
921
+ lines.push(cur);
922
+ cur = "";
923
+ }
924
+ while (word.length > cap2()) {
925
+ lines.push(word.slice(0, cap2()));
926
+ word = word.slice(cap2());
927
+ }
928
+ cur = word;
929
+ }
930
+ lines.push(cur);
931
+ return lines;
932
+ }
933
+ function deleteWord(text) {
934
+ return text.replace(/\s+$/, "").replace(/\S+$/, "");
935
+ }
936
+ async function readPromptLine(opts) {
937
+ if (!interactive()) return fallbackLine();
938
+ const { commands } = opts;
939
+ let buf = "";
940
+ let sel = 0;
941
+ let prevContentRows = 0;
942
+ const prompt = () => {
943
+ const label = opts.modeLabel?.() ?? "";
944
+ const badge = label ? `${yellow(`\u23F5 ${label}`)} ` : "";
945
+ return promptString(badge);
946
+ };
947
+ const promptW = () => visibleLen(prompt());
948
+ const matches = () => {
949
+ if (!buf.startsWith("/") || buf.includes(" ")) return [];
950
+ const token = buf.slice(1);
951
+ return commands.filter((c) => c.name.startsWith(token));
952
+ };
953
+ const wrapBuf = () => wrapText(buf, frameWidth() - promptW(), frameWidth() - 2);
954
+ const clearBox = () => {
955
+ if (prevContentRows > 0) stdout.write(`\x1B[${prevContentRows}A`);
956
+ stdout.write("\r\x1B[0J");
957
+ };
958
+ const render = () => {
959
+ clearBox();
960
+ const p = prompt();
961
+ const lines = wrapBuf();
962
+ const rows = [hRule(), ...lines.map((seg, i) => (i === 0 ? p : " ") + seg), hRule()];
963
+ const items = matches();
964
+ if (items.length) {
965
+ if (sel >= items.length) sel = items.length - 1;
966
+ for (let i = 0; i < items.length; i++) {
967
+ const cmd = items[i];
968
+ const active = i === sel;
969
+ const marker = active ? cyan("\u203A") : " ";
970
+ const label = active ? bold(`/${cmd.name}`) : `/${cmd.name}`;
971
+ rows.push(`${marker} ${label} ${dim(cmd.desc)}`);
972
+ }
973
+ }
974
+ stdout.write(rows.join("\n"));
975
+ const below = 1 + items.length;
976
+ stdout.write(`\x1B[${below}A\r`);
977
+ const lastPrefixW = lines.length === 1 ? promptW() : 2;
978
+ const col = lastPrefixW + (lines[lines.length - 1]?.length ?? 0);
979
+ if (col > 0) stdout.write(`\x1B[${col}C`);
980
+ prevContentRows = lines.length;
981
+ };
982
+ const finishRender = () => {
983
+ clearBox();
984
+ const p = prompt();
985
+ const lines = wrapBuf();
986
+ const rows = [hRule(), ...lines.map((seg, i) => (i === 0 ? p : " ") + seg), hRule()];
987
+ stdout.write(`${rows.join("\n")}
988
+ `);
989
+ };
990
+ render();
991
+ return rawSession((key, str2, done) => {
992
+ const items = matches();
993
+ const name = key.name;
994
+ const exitBelowBox = () => {
995
+ stdout.write("\x1B[1B\r\n");
996
+ };
997
+ if (key.ctrl && name === "c") {
998
+ if (buf) {
999
+ buf = "";
1000
+ sel = 0;
1001
+ render();
1002
+ return;
1003
+ }
1004
+ exitBelowBox();
1005
+ done(null);
1006
+ return;
1007
+ }
1008
+ if (key.ctrl && name === "d") {
1009
+ if (!buf) {
1010
+ exitBelowBox();
1011
+ done(null);
1012
+ }
1013
+ return;
1014
+ }
1015
+ if (name === "return" || name === "enter") {
1016
+ if (items.length && buf.length > 1) buf = `/${items[sel].name}`;
1017
+ finishRender();
1018
+ done(buf);
1019
+ return;
1020
+ }
1021
+ if (name === "tab" && key.shift) {
1022
+ opts.onCycleMode?.();
1023
+ render();
1024
+ return;
1025
+ }
1026
+ if (name === "tab") {
1027
+ if (items.length) {
1028
+ buf = `/${items[sel].name} `;
1029
+ sel = 0;
1030
+ render();
1031
+ }
1032
+ return;
1033
+ }
1034
+ if (name === "up") {
1035
+ if (items.length) {
1036
+ sel = (sel - 1 + items.length) % items.length;
1037
+ render();
1038
+ }
1039
+ return;
1040
+ }
1041
+ if (name === "down") {
1042
+ if (items.length) {
1043
+ sel = (sel + 1) % items.length;
1044
+ render();
1045
+ }
1046
+ return;
1047
+ }
1048
+ if (key.meta && name === "backspace" || str2 === "\x1B\x7F") {
1049
+ if (buf) {
1050
+ buf = deleteWord(buf);
1051
+ sel = 0;
1052
+ render();
1053
+ }
1054
+ return;
1055
+ }
1056
+ if (name === "backspace") {
1057
+ if (buf) {
1058
+ buf = buf.slice(0, -1);
1059
+ sel = 0;
1060
+ render();
1061
+ }
1062
+ return;
1063
+ }
1064
+ if (name === "escape") {
1065
+ if (buf) {
1066
+ buf = "";
1067
+ sel = 0;
1068
+ render();
1069
+ }
1070
+ return;
1071
+ }
1072
+ if (isPrintable(str2, key)) {
1073
+ buf += str2;
1074
+ sel = 0;
1075
+ render();
1076
+ }
1077
+ });
1078
+ }
1079
+ async function selectMenu(title, items, current) {
1080
+ if (!items.length) return void 0;
1081
+ if (!interactive()) return fallbackPick(title, items, current);
1082
+ let sel = current ? items.indexOf(current) : 0;
1083
+ if (sel < 0) sel = 0;
1084
+ const rowsAvail = stdout.rows ? Math.max(3, stdout.rows - 6) : 12;
1085
+ const view = Math.min(items.length, rowsAvail, 12);
1086
+ let top = Math.min(Math.max(0, sel - Math.floor(view / 2)), Math.max(0, items.length - view));
1087
+ console.log(title);
1088
+ console.log(dim(" \u2191/\u2193 navigate \xB7 enter select \xB7 esc cancel"));
1089
+ let drawn = 0;
1090
+ const render = () => {
1091
+ if (drawn > 0) stdout.write(`\x1B[${drawn}A`);
1092
+ stdout.write("\r\x1B[0J");
1093
+ if (sel < top) top = sel;
1094
+ if (sel >= top + view) top = sel - view + 1;
1095
+ for (let i = top; i < top + view; i++) {
1096
+ const item = items[i];
1097
+ const active = i === sel;
1098
+ const prefix = active ? cyan("\u276F ") : " ";
1099
+ const label = active ? bold(item) : item;
1100
+ const marker = item === current ? dim(" (current)") : "";
1101
+ stdout.write(`${prefix}${label}${marker}
1102
+ `);
1103
+ }
1104
+ const footer = items.length > view ? dim(` ${sel + 1}/${items.length}`) : "";
1105
+ stdout.write(`${footer}
1106
+ `);
1107
+ drawn = view + 1;
1108
+ };
1109
+ const cleanup = () => {
1110
+ if (drawn > 0) stdout.write(`\x1B[${drawn + 2}A\r\x1B[0J`);
1111
+ };
1112
+ render();
1113
+ return rawSession((key, _str, done) => {
1114
+ const name = key.name;
1115
+ if (name === "up") {
1116
+ sel = (sel - 1 + items.length) % items.length;
1117
+ render();
1118
+ return;
1119
+ }
1120
+ if (name === "down") {
1121
+ sel = (sel + 1) % items.length;
1122
+ render();
1123
+ return;
1124
+ }
1125
+ if (name === "return" || name === "enter") {
1126
+ cleanup();
1127
+ done(items[sel]);
1128
+ return;
1129
+ }
1130
+ if (name === "escape" || key.ctrl && name === "c") {
1131
+ cleanup();
1132
+ done(void 0);
1133
+ }
1134
+ });
1135
+ }
1136
+ async function rawIndexMenu(header, entries, sepBefore) {
1137
+ const n = entries.length;
1138
+ let sel = 0;
1139
+ console.log("");
1140
+ console.log(header);
1141
+ console.log(dim(" \u2191/\u2193 navigate \xB7 enter select"));
1142
+ let drawn = 0;
1143
+ const render = () => {
1144
+ if (drawn > 0) stdout.write(`\x1B[${drawn}A`);
1145
+ stdout.write("\r\x1B[0J");
1146
+ let lines = 0;
1147
+ entries.forEach((e, i) => {
1148
+ if (sepBefore === i) {
1149
+ stdout.write(` ${dim("\u2500".repeat(24))}
1150
+ `);
1151
+ lines++;
1152
+ }
1153
+ const active = i === sel;
1154
+ const prefix = active ? cyan("\u276F ") : " ";
1155
+ stdout.write(`${prefix}${active ? bold(e.label) : e.label}
1156
+ `);
1157
+ lines++;
1158
+ if (e.detail) {
1159
+ stdout.write(` ${dim(e.detail)}
1160
+ `);
1161
+ lines++;
1162
+ }
1163
+ });
1164
+ drawn = lines;
1165
+ };
1166
+ const cleanup = () => {
1167
+ if (drawn > 0) stdout.write(`\x1B[${drawn + 3}A\r\x1B[0J`);
1168
+ };
1169
+ render();
1170
+ return rawSession((key, _str, done) => {
1171
+ const name = key.name;
1172
+ if (name === "up") {
1173
+ sel = (sel - 1 + n) % n;
1174
+ render();
1175
+ return;
1176
+ }
1177
+ if (name === "down") {
1178
+ sel = (sel + 1) % n;
1179
+ render();
1180
+ return;
1181
+ }
1182
+ if (name === "return" || name === "enter") {
1183
+ cleanup();
1184
+ done(sel);
1185
+ return;
1186
+ }
1187
+ if (name === "escape" || key.ctrl && name === "c") {
1188
+ cleanup();
1189
+ done(null);
1190
+ }
1191
+ });
1192
+ }
1193
+ var TYPE_LABEL = "\u270E Type something else\u2026";
1194
+ var CHAT_LABEL = "\u{1F4AC} Chat about this";
1195
+ function choiceFromIndex(options, idx) {
1196
+ if (idx >= 0 && idx < options.length) {
1197
+ const o = options[idx];
1198
+ return { kind: "option", index: idx, label: o.label, detail: o.detail };
1199
+ }
1200
+ return idx === options.length ? { kind: "free" } : { kind: "chat" };
1201
+ }
1202
+ async function pickChoice(question, options) {
1203
+ const toResult = (idx2) => choiceFromIndex(options, idx2);
1204
+ if (!interactive()) {
1205
+ const labels = [...options.map((o) => o.label), TYPE_LABEL, CHAT_LABEL];
1206
+ const chosen = await pickFromList(rl(), `? ${question}`, labels);
1207
+ const idx2 = chosen ? labels.indexOf(chosen) : labels.length - 1;
1208
+ return toResult(idx2 < 0 ? labels.length - 1 : idx2);
1209
+ }
1210
+ const entries = [
1211
+ ...options.map((o) => ({ label: o.label, detail: o.detail })),
1212
+ { label: TYPE_LABEL },
1213
+ { label: CHAT_LABEL }
1214
+ ];
1215
+ const idx = await rawIndexMenu(`? ${bold(question)}`, entries, options.length);
1216
+ return toResult(idx ?? options.length + 1);
1217
+ }
1218
+ var APPROVAL_ENTRIES = [
1219
+ { label: "Approve \u2014 auto-approve edits", detail: "run the plan to completion without asking per edit" },
1220
+ { label: "Approve \u2014 approve each edit", detail: "unblock changes, but confirm each one (y/N)" },
1221
+ { label: "Approve with comments\u2026", detail: "auto-approve and pass extra guidance to the agent" },
1222
+ { label: "Request changes\u2026", detail: "send feedback; the agent drafts a new plan" },
1223
+ { label: "Chat about this", detail: "discuss instead \u2014 return to the prompt in normal mode" }
1224
+ ];
1225
+ function approvalFromIndex(idx) {
1226
+ switch (idx) {
1227
+ case 0:
1228
+ return { kind: "approve", autoApprove: true };
1229
+ case 1:
1230
+ return { kind: "approve", autoApprove: false };
1231
+ case 2:
1232
+ return { kind: "approveWithComments" };
1233
+ case 3:
1234
+ return { kind: "requestChanges" };
1235
+ default:
1236
+ return { kind: "chat" };
1237
+ }
1238
+ }
1239
+ async function approvalPicker() {
1240
+ const toChoice = (idx2) => approvalFromIndex(idx2);
1241
+ if (!interactive()) {
1242
+ const labels = APPROVAL_ENTRIES.map((e) => e.label);
1243
+ const chosen = await pickFromList(rl(), "Review this plan:", labels);
1244
+ const idx2 = chosen ? labels.indexOf(chosen) : labels.length - 1;
1245
+ return toChoice(idx2 < 0 ? labels.length - 1 : idx2);
1246
+ }
1247
+ const idx = await rawIndexMenu(bold("Review this plan:"), APPROVAL_ENTRIES, 3);
1248
+ return toChoice(idx ?? APPROVAL_ENTRIES.length - 1);
1249
+ }
1250
+ async function confirm(question) {
1251
+ if (!interactive()) return fallbackConfirm(question);
1252
+ stdout.write(question);
1253
+ return rawSession((key, str2, done) => {
1254
+ const c = (str2 ?? "").toLowerCase();
1255
+ if (c === "y") {
1256
+ stdout.write("y\n");
1257
+ done(true);
1258
+ return;
1259
+ }
1260
+ if (c === "n" || key.name === "return" || key.name === "enter" || key.ctrl && key.name === "c") {
1261
+ stdout.write("n\n");
1262
+ done(false);
1263
+ }
1264
+ });
1265
+ }
1266
+ async function readSecret(question) {
1267
+ if (!interactive()) return fallbackSecret(question);
1268
+ stdout.write(question);
1269
+ let buf = "";
1270
+ return rawSession((key, str2, done) => {
1271
+ const name = key.name;
1272
+ if (name === "return" || name === "enter") {
1273
+ stdout.write("\n");
1274
+ done(buf.trim());
1275
+ return;
1276
+ }
1277
+ if (key.ctrl && name === "c") {
1278
+ stdout.write("\n");
1279
+ done("");
1280
+ return;
1281
+ }
1282
+ if (name === "backspace") {
1283
+ buf = buf.slice(0, -1);
1284
+ return;
1285
+ }
1286
+ if (str2 && !key.ctrl && !key.meta && str2 >= " ") buf += str2;
1287
+ });
1288
+ }
1289
+ var sharedRl = null;
1290
+ function rl() {
1291
+ if (!sharedRl) sharedRl = createInterface({ input: stdin, output: stdout });
1292
+ return sharedRl;
1293
+ }
1294
+ function fallbackLine() {
1295
+ return new Promise((resolve) => {
1296
+ const r = rl();
1297
+ let settled = false;
1298
+ const onClose = () => {
1299
+ if (!settled) {
1300
+ settled = true;
1301
+ resolve(null);
1302
+ }
1303
+ };
1304
+ r.once("close", onClose);
1305
+ r.question(promptString()).then(
1306
+ (ans) => {
1307
+ if (!settled) {
1308
+ settled = true;
1309
+ r.off("close", onClose);
1310
+ resolve(ans);
1311
+ }
1312
+ },
1313
+ () => {
1314
+ if (!settled) {
1315
+ settled = true;
1316
+ resolve(null);
1317
+ }
1318
+ }
1319
+ );
1320
+ });
1321
+ }
1322
+ function fallbackPick(title, items, current) {
1323
+ return pickFromList(rl(), title, items, current);
1324
+ }
1325
+ async function fallbackConfirm(question) {
1326
+ const a = (await rl().question(question)).trim().toLowerCase();
1327
+ return a === "y" || a === "yes";
1328
+ }
1329
+ function fallbackSecret(question) {
1330
+ return askHidden(rl(), question);
1331
+ }
1332
+
1333
+ // src/config.ts
1334
+ var PROD_URL = "https://portal.audacityinvestments.com";
1335
+ var CONFIG_DIR = join(homedir(), ".audacity");
1336
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
1337
+ async function readConfigFile() {
1338
+ try {
1339
+ const raw = await readFile2(CONFIG_PATH, "utf8");
1340
+ const parsed = JSON.parse(raw);
1341
+ return {
1342
+ apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : void 0,
1343
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
1344
+ model: typeof parsed.model === "string" ? parsed.model : void 0,
1345
+ compactModel: typeof parsed.compactModel === "string" ? parsed.compactModel : void 0
1346
+ };
1347
+ } catch {
1348
+ return {};
1349
+ }
1350
+ }
1351
+ async function saveConfig(cfg) {
1352
+ await mkdir2(CONFIG_DIR, { recursive: true });
1353
+ await writeFile2(CONFIG_PATH, `${JSON.stringify(cfg, null, 2)}
1354
+ `, "utf8");
1355
+ await chmod(CONFIG_PATH, 384);
1356
+ }
1357
+ async function loadConfig() {
1358
+ return readConfigFile();
1359
+ }
1360
+ function resolveNonInteractive(partial) {
1361
+ const apiKey = partial.apiKey;
1362
+ if (!apiKey) {
1363
+ throw new Error(
1364
+ "No API key saved. Run `audacity` once (no arguments) to enter your audacity_api_\u2026 key \u2014 it will be saved to ~/.audacity/config.json and remembered."
1365
+ );
1366
+ }
1367
+ return {
1368
+ apiUrl: partial.apiUrl || PROD_URL,
1369
+ apiKey,
1370
+ model: partial.model || DEFAULT_MODEL
1371
+ };
1372
+ }
1373
+ async function resolveInteractive(partial) {
1374
+ let { apiUrl, apiKey, model } = partial;
1375
+ let changed = false;
1376
+ if (!apiUrl) {
1377
+ apiUrl = PROD_URL;
1378
+ changed = true;
1379
+ }
1380
+ if (!apiKey) {
1381
+ apiKey = await readSecret("Paste your production audacity_api_ key: ");
1382
+ if (!apiKey.startsWith("audacity_api_")) {
1383
+ console.log("\u26A0 That doesn't look like an audacity_api_ key \u2014 using it anyway.");
1384
+ }
1385
+ changed = true;
1386
+ }
1387
+ if (!model) {
1388
+ model = await selectMenu("Pick a model:", [...AVAILABLE_MODELS], DEFAULT_MODEL) ?? DEFAULT_MODEL;
1389
+ changed = true;
1390
+ }
1391
+ const cfg = { apiUrl, apiKey, model };
1392
+ if (changed) {
1393
+ await saveConfig(cfg);
1394
+ console.log(`\u2713 Saved to ${CONFIG_PATH} (chmod 600).`);
1395
+ }
1396
+ return cfg;
1397
+ }
1398
+
1399
+ // src/plan.ts
1400
+ import { access, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1401
+ import path2 from "path";
1402
+ var PLANS_DIR = [".audacity", "plans"];
1403
+ function slugify(title) {
1404
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
1405
+ return slug || "plan";
1406
+ }
1407
+ function timestampPrefix(now) {
1408
+ const p = (n, w = 2) => String(n).padStart(w, "0");
1409
+ const date = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
1410
+ const time = `${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
1411
+ return `${date}-${time}`;
1412
+ }
1413
+ function renderBody(input) {
1414
+ const parts = [`## Summary
1415
+ ${input.summary}`];
1416
+ const steps = input.steps.map((s, i) => `${i + 1}. ${s}`).join("\n");
1417
+ parts.push(`## Steps
1418
+ ${steps}`);
1419
+ if (input.files_to_change?.length) {
1420
+ parts.push(`## Files to change
1421
+ ${input.files_to_change.map((f) => `- \`${f}\``).join("\n")}`);
1422
+ }
1423
+ if (input.open_questions?.length) {
1424
+ parts.push(`## Open questions
1425
+ ${input.open_questions.map((q) => `- ${q}`).join("\n")}`);
1426
+ }
1427
+ return parts.join("\n\n");
1428
+ }
1429
+ function renderFrontmatter(input, status, created) {
1430
+ const files = (input.files_to_change ?? []).map((f) => ` - ${f}`).join("\n");
1431
+ return [
1432
+ "---",
1433
+ `title: ${input.title}`,
1434
+ `status: ${status}`,
1435
+ `created: ${created}`,
1436
+ "approved: null",
1437
+ files ? `files:
1438
+ ${files}` : "files: []",
1439
+ "---"
1440
+ ].join("\n");
1441
+ }
1442
+ async function exists(p) {
1443
+ try {
1444
+ await access(p);
1445
+ return true;
1446
+ } catch {
1447
+ return false;
1448
+ }
1449
+ }
1450
+ async function writePlan(cwd, input, now = /* @__PURE__ */ new Date()) {
1451
+ const dir = path2.join(cwd, ...PLANS_DIR);
1452
+ const firstTime = !await exists(dir);
1453
+ await mkdir3(dir, { recursive: true });
1454
+ const file = path2.join(dir, `${timestampPrefix(now)}-${slugify(input.title)}.md`);
1455
+ const created = now.toISOString();
1456
+ const contents = `${renderFrontmatter(input, "proposed", created)}
1457
+
1458
+ ${renderBody(input)}
1459
+ `;
1460
+ await writeFile3(file, contents, "utf8");
1461
+ if (firstTime) {
1462
+ console.log(
1463
+ dim(
1464
+ " note: plans are written to .audacity/plans/ (not gitignored \u2014 useful in PRs/history). Add .audacity/ to .gitignore if you'd rather keep them local."
1465
+ )
1466
+ );
1467
+ }
1468
+ return file;
1469
+ }
1470
+ async function updatePlanStatus(file, status, now = /* @__PURE__ */ new Date()) {
1471
+ const contents = await readFile3(file, "utf8");
1472
+ let updated = contents.replace(/^status:.*$/m, `status: ${status}`);
1473
+ const approvedValue = status === "approved" ? now.toISOString() : "null";
1474
+ updated = updated.replace(/^approved:.*$/m, `approved: ${approvedValue}`);
1475
+ await writeFile3(file, updated, "utf8");
1476
+ }
1477
+
1478
+ // src/index.ts
1479
+ var modeIndexById = (id) => Math.max(0, AGENT_MODES.findIndex((m) => m.id === id));
1480
+ var SLASH_COMMANDS = [
1481
+ { name: "model", desc: "switch the model (arrow-key picker)" },
1482
+ { name: "rag", desc: "toggle RAG mode \u2014 /rag true | /rag false" },
1483
+ { name: "plan", desc: "toggle plan mode \u2014 no changes without approval; draft a plan" },
1484
+ { name: "auto", desc: "toggle auto-approve of tool actions" },
1485
+ { name: "key", desc: "paste & save a new audacity_api_ key" },
1486
+ { name: "cwd", desc: "print the working directory" },
1487
+ { name: "compact", desc: "summarize older turns, keep recent context" },
1488
+ { name: "clear", desc: "clear the conversation & screen" },
1489
+ { name: "exit", desc: "quit audacity" }
1490
+ ];
1491
+ var SYSTEM_PROMPT = `You are audacity, a command-line coding agent operating inside a user's project directory.
1492
+ You have tools to read, list, and search files, edit and write files, and run shell commands.
1493
+ Prefer reading before editing. Make minimal, correct changes. When you use edit_file, old_str must
1494
+ match exactly once \u2014 read the file first if unsure. Keep prose concise; let tool results speak.`;
1495
+ function newSession(cfg, cwd, autoApprove, plan = false) {
1496
+ const modeIndex = plan ? modeIndexById("plan") : initialModeIndex(autoApprove);
1497
+ return {
1498
+ messages: [{ role: "system", content: SYSTEM_PROMPT }],
1499
+ model: cfg.model,
1500
+ cwd,
1501
+ autoApprove: AGENT_MODES[modeIndex].autoApprove,
1502
+ modeIndex,
1503
+ ragMode: false
1504
+ };
1505
+ }
1506
+ function renderPlan(input) {
1507
+ const md = createMarkdownStream();
1508
+ md.write(`
1509
+ # ${input.title}
1510
+
1511
+ ${input.summary}
1512
+
1513
+ ## Steps
1514
+ `);
1515
+ input.steps.forEach((s, i) => md.write(`${i + 1}. ${s}
1516
+ `));
1517
+ if (input.files_to_change?.length) {
1518
+ md.write(`
1519
+ ## Files to change
1520
+ `);
1521
+ input.files_to_change.forEach((f) => md.write(`- \`${f}\`
1522
+ `));
1523
+ }
1524
+ if (input.open_questions?.length) {
1525
+ md.write(`
1526
+ ## Open questions
1527
+ `);
1528
+ input.open_questions.forEach((q) => md.write(`- ${q}
1529
+ `));
1530
+ }
1531
+ md.end();
1532
+ process.stdout.write("\n");
1533
+ }
1534
+ async function readLine() {
1535
+ const raw = await readPromptLine({ commands: [] });
1536
+ return (raw ?? "").trim();
1537
+ }
1538
+ function interactiveControls() {
1539
+ return {
1540
+ async presentPlan(input, session) {
1541
+ const path4 = await writePlan(session.cwd, input);
1542
+ session.activePlan = { path: path4, title: input.title, summary: input.summary, steps: input.steps };
1543
+ renderPlan(input);
1544
+ const choice = await approvalPicker();
1545
+ switch (choice.kind) {
1546
+ case "approve": {
1547
+ await updatePlanStatus(path4, "approved");
1548
+ setMode(session, initialModeIndex(choice.autoApprove));
1549
+ console.log(
1550
+ ` \u2713 plan approved \u2014 ${choice.autoApprove ? "auto-approving edits" : "approving each edit"}.`
1551
+ );
1552
+ return { toolResult: "Plan approved \u2014 proceed with execution.", next: "continue" };
1553
+ }
1554
+ case "approveWithComments": {
1555
+ const comment = await readLine();
1556
+ await updatePlanStatus(path4, "approved");
1557
+ setMode(session, initialModeIndex(true));
1558
+ console.log(" \u2713 plan approved (auto) with comments.");
1559
+ return {
1560
+ toolResult: `Plan approved (auto). Apply with these comments: ${comment || "(none)"}`,
1561
+ next: "continue"
1562
+ };
1563
+ }
1564
+ case "requestChanges": {
1565
+ const feedback = await readLine();
1566
+ await updatePlanStatus(path4, "rejected");
1567
+ session.activePlan = void 0;
1568
+ console.log(" \u21BB requesting changes \u2014 the agent will revise.");
1569
+ return {
1570
+ toolResult: `Plan rejected \u2014 revise per this feedback and call present_plan again: ${feedback || "(no feedback given)"}`,
1571
+ next: "continue"
1572
+ };
1573
+ }
1574
+ case "chat":
1575
+ default: {
1576
+ setMode(session, initialModeIndex(false));
1577
+ session.activePlan = void 0;
1578
+ console.log(" \u{1F4AC} switched to chat \u2014 plan left as proposed.");
1579
+ return { toolResult: "User switched to chat to discuss instead of approving.", next: "stop" };
1580
+ }
1581
+ }
1582
+ },
1583
+ async askChoice(input) {
1584
+ const options = Array.isArray(input.options) ? input.options : [];
1585
+ if (options.length < 2) {
1586
+ return { toolResult: "ERROR: ask_choice needs at least 2 options.", next: "continue" };
1587
+ }
1588
+ const result = await pickChoice(input.question, options);
1589
+ switch (result.kind) {
1590
+ case "option": {
1591
+ const detail = result.detail ? ` (${result.detail})` : "";
1592
+ console.log(` \u2192 ${result.label}`);
1593
+ return { toolResult: `Selected: "${result.label}"${detail}`, next: "continue" };
1594
+ }
1595
+ case "free": {
1596
+ const text = await readLine();
1597
+ return { toolResult: text || "(no input given)", next: "continue" };
1598
+ }
1599
+ case "chat":
1600
+ default: {
1601
+ const labels = options.map((o) => o.label).join(", ");
1602
+ return {
1603
+ toolResult: `User chose to discuss instead. Question was "${input.question}"; options were: ${labels}.`,
1604
+ next: "stop"
1605
+ };
1606
+ }
1607
+ }
1608
+ }
1609
+ };
1610
+ }
1611
+ function autoControls(yes) {
1612
+ return {
1613
+ async presentPlan(input, session) {
1614
+ const path4 = await writePlan(session.cwd, input);
1615
+ if (yes) {
1616
+ session.activePlan = { path: path4, title: input.title, summary: input.summary, steps: input.steps };
1617
+ await updatePlanStatus(path4, "approved");
1618
+ setMode(session, initialModeIndex(true));
1619
+ return { toolResult: "Plan approved (non-interactive -y) \u2014 proceed with execution.", next: "continue" };
1620
+ }
1621
+ return {
1622
+ toolResult: "Plan not approved: this is a non-interactive run. Re-run with -y to auto-approve.",
1623
+ next: "stop"
1624
+ };
1625
+ },
1626
+ async askChoice() {
1627
+ return {
1628
+ toolResult: "ERROR: cannot ask the user a question in a non-interactive run. Make a reasonable assumption and proceed, stating it explicitly.",
1629
+ next: "continue"
1630
+ };
1631
+ }
1632
+ };
1633
+ }
1634
+ function banner(session, cfg) {
1635
+ const host = (() => {
1636
+ try {
1637
+ return new URL(cfg.apiUrl).host;
1638
+ } catch {
1639
+ return cfg.apiUrl;
1640
+ }
1641
+ })();
1642
+ printBanner();
1643
+ console.log(` coding agent \xB7 model ${session.model} \xB7 ${host}`);
1644
+ console.log(` cwd: ${session.cwd}`);
1645
+ console.log(dim(" type / for commands \xB7 \u2191/\u2193 to pick \xB7 shift+tab cycles mode"));
1646
+ console.log(dim(` mode: ${currentMode(session).label} (${AGENT_MODES.map((m) => m.label).join(" \u2192 ")})`));
1647
+ console.log("");
1648
+ }
1649
+ async function handleSlash(line, session, cfg) {
1650
+ const parts = line.slice(1).split(/\s+/);
1651
+ const cmd = parts[0] ?? "";
1652
+ const arg = parts.slice(1).join(" ").trim();
1653
+ switch (cmd) {
1654
+ case "exit":
1655
+ case "quit":
1656
+ return "exit";
1657
+ case "clear":
1658
+ session.messages = [{ role: "system", content: SYSTEM_PROMPT }];
1659
+ session.conversationId = void 0;
1660
+ console.clear();
1661
+ banner(session, cfg);
1662
+ console.log("Context cleared.");
1663
+ return "continue";
1664
+ case "cwd":
1665
+ console.log(session.cwd);
1666
+ return "continue";
1667
+ case "compact": {
1668
+ const summarizer = cfg.compactModel || COMPACT_MODEL;
1669
+ process.stdout.write(dim(`Compacting conversation\u2026 (summarizer: ${summarizer})
1670
+ `));
1671
+ try {
1672
+ const res = await compactSession(session, cfg);
1673
+ if (!res) {
1674
+ console.log("Nothing to compact yet.");
1675
+ return "continue";
1676
+ }
1677
+ console.log(dim(res.summary));
1678
+ console.log(
1679
+ `
1680
+ \u2713 Compacted ${res.before} \u2192 ${res.after} messages (summary + last ${KEEP_RECENT}).`
1681
+ );
1682
+ } catch (err) {
1683
+ const msg = err instanceof Error ? err.message : String(err);
1684
+ console.log(`[audacity] compact failed: ${msg}`);
1685
+ }
1686
+ return "continue";
1687
+ }
1688
+ case "plan": {
1689
+ const target = currentMode(session).id === "plan" ? "normal" : "plan";
1690
+ const mode = setMode(session, modeIndexById(target));
1691
+ console.log(`Mode \u2192 ${mode.label}.`);
1692
+ return "continue";
1693
+ }
1694
+ case "auto": {
1695
+ const mode = setMode(session, initialModeIndex(!session.autoApprove));
1696
+ console.log(`Mode \u2192 ${mode.label}.`);
1697
+ return "continue";
1698
+ }
1699
+ case "key": {
1700
+ const key = await readSecret("Paste your audacity_api_ key: ");
1701
+ if (key) {
1702
+ cfg.apiKey = key;
1703
+ await saveConfig(cfg);
1704
+ console.log("\u2713 key updated & saved.");
1705
+ }
1706
+ return "continue";
1707
+ }
1708
+ case "model": {
1709
+ const chosen = arg || await selectMenu("Pick a model:", [...AVAILABLE_MODELS], session.model);
1710
+ if (chosen) {
1711
+ session.model = chosen;
1712
+ cfg.model = chosen;
1713
+ await saveConfig(cfg);
1714
+ console.log(`Model \u2192 ${chosen} (saved).`);
1715
+ }
1716
+ return "continue";
1717
+ }
1718
+ case "rag": {
1719
+ const v = arg.toLowerCase();
1720
+ if (v === "true" || v === "on") {
1721
+ session.ragMode = true;
1722
+ console.log(
1723
+ "RAG mode ON. Note: the agent/RAG endpoint (/v1/agent/chat/completions) arrives in Phase 2 \u2014 messages won't be sent until then. Use `/rag false` to resume the coding agent."
1724
+ );
1725
+ } else if (v === "false" || v === "off") {
1726
+ session.ragMode = false;
1727
+ console.log("RAG mode OFF \u2014 coding agent active.");
1728
+ } else {
1729
+ console.log(`RAG mode is ${session.ragMode ? "ON" : "OFF"}. Usage: /rag true | /rag false`);
1730
+ }
1731
+ return "continue";
1732
+ }
1733
+ default:
1734
+ console.log(`Unknown command: /${cmd}. Try /model /rag /auto /key /cwd /compact /clear /exit`);
1735
+ return "continue";
1736
+ }
1737
+ }
1738
+ async function runRepl(cfg, cwd, autoApprove, plan) {
1739
+ const session = newSession(cfg, cwd, autoApprove, plan);
1740
+ banner(session, cfg);
1741
+ const approve = async (_name, preview2) => {
1742
+ console.log(` \u26A0 approve:
1743
+ ${preview2.replace(/\n/g, "\n ")}`);
1744
+ return confirm(" Allow? (y/N) ");
1745
+ };
1746
+ const control = interactiveControls();
1747
+ for (; ; ) {
1748
+ const raw = await readPromptLine({
1749
+ commands: SLASH_COMMANDS,
1750
+ // Default mode renders no badge; others show their label in the prompt.
1751
+ modeLabel: () => currentMode(session).id === DEFAULT_MODE_ID ? "" : currentMode(session).label,
1752
+ onCycleMode: () => cycleMode(session)
1753
+ });
1754
+ if (raw === null) break;
1755
+ const line = raw.trim();
1756
+ if (!line) continue;
1757
+ if (line.startsWith("/")) {
1758
+ if (await handleSlash(line, session, cfg) === "exit") break;
1759
+ continue;
1760
+ }
1761
+ if (session.ragMode) {
1762
+ console.log(
1763
+ "[audacity] RAG mode (/rag true) arrives in Phase 2. Use `/rag false` to chat with the coding agent."
1764
+ );
1765
+ continue;
1766
+ }
1767
+ session.messages.push({ role: "user", content: line });
1768
+ await runTurn(session, cfg, approve, control);
1769
+ }
1770
+ }
1771
+ async function runOneShot(cfg, cwd, prompt, yes, plan) {
1772
+ const session = newSession(cfg, cwd, yes, plan);
1773
+ session.messages.push({ role: "user", content: prompt });
1774
+ const approve = async () => yes;
1775
+ await runTurn(session, cfg, approve, autoControls(yes));
1776
+ }
1777
+ var program = new Command();
1778
+ program.name("audacity").description("Audacity coding-agent CLI \u2014 talks only to your Audacity endpoints").argument("[prompt...]", "run a single prompt to completion, then exit").option("--cwd <path>", "working directory", process.cwd()).option("--model <id>", "model id (overrides config for this run)").option("-y, --yes", "auto-approve all tool actions", false).option("--plan", "start in plan mode (no changes without approval)", false).action(async (promptParts, opts) => {
1779
+ const cwd = path3.resolve(opts.cwd);
1780
+ const partial = await loadConfig();
1781
+ if (opts.model) partial.model = opts.model;
1782
+ const oneShotPrompt = promptParts.join(" ").trim();
1783
+ if (oneShotPrompt) {
1784
+ let cfg2;
1785
+ try {
1786
+ cfg2 = resolveNonInteractive(partial);
1787
+ } catch (err) {
1788
+ console.error(`[audacity] ${err.message}`);
1789
+ process.exitCode = 1;
1790
+ return;
1791
+ }
1792
+ await runOneShot(cfg2, cwd, oneShotPrompt, opts.yes, opts.plan);
1793
+ return;
1794
+ }
1795
+ let cfg;
1796
+ try {
1797
+ cfg = await resolveInteractive(partial);
1798
+ } catch (err) {
1799
+ console.error(`[audacity] ${err.message}`);
1800
+ process.exitCode = 1;
1801
+ return;
1802
+ }
1803
+ await runRepl(cfg, cwd, opts.yes, opts.plan);
1804
+ });
1805
+ await program.parseAsync(process.argv);