@garuda-agent/garuda 0.0.1 → 0.15.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 (66) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +4 -0
  4. package/README.md +1097 -0
  5. package/SECURITY.md +32 -0
  6. package/dist/acpCommand-FZ2P5JFO.js +153 -0
  7. package/dist/anthropic-NQDBCHJU.js +327 -0
  8. package/dist/chunk-23FXBNBB.js +46 -0
  9. package/dist/chunk-2S62ZNTC.js +320 -0
  10. package/dist/chunk-2TAXSFWN.js +49 -0
  11. package/dist/chunk-36GQIVGT.js +526 -0
  12. package/dist/chunk-3CEPHS4C.js +231 -0
  13. package/dist/chunk-3NJNVZTK.js +123 -0
  14. package/dist/chunk-42DSA4EA.js +116 -0
  15. package/dist/chunk-4JSUUDFQ.js +118 -0
  16. package/dist/chunk-4P26OKTP.js +100 -0
  17. package/dist/chunk-5DEWP3YX.js +139 -0
  18. package/dist/chunk-A2QAIRQL.js +45 -0
  19. package/dist/chunk-ATW7BNIG.js +31 -0
  20. package/dist/chunk-B25VL4RU.js +242 -0
  21. package/dist/chunk-EPKWDCV4.js +9 -0
  22. package/dist/chunk-EU7PA45O.js +324 -0
  23. package/dist/chunk-FARS4R67.js +206 -0
  24. package/dist/chunk-G2AX7MGS.js +36 -0
  25. package/dist/chunk-HRGX53VC.js +173 -0
  26. package/dist/chunk-HUIK3WXO.js +234 -0
  27. package/dist/chunk-INRZR772.js +73 -0
  28. package/dist/chunk-K3SIUOAG.js +138 -0
  29. package/dist/chunk-K7KK7U44.js +9358 -0
  30. package/dist/chunk-KKB5W47S.js +231 -0
  31. package/dist/chunk-N4I6YS3E.js +73 -0
  32. package/dist/chunk-NXJ4XLB5.js +19 -0
  33. package/dist/chunk-OUCV3FCT.js +205 -0
  34. package/dist/chunk-PQNT34LY.js +1247 -0
  35. package/dist/chunk-PZS3V6P3.js +46 -0
  36. package/dist/chunk-S77XDSD4.js +157 -0
  37. package/dist/chunk-SI2QJYCR.js +1341 -0
  38. package/dist/chunk-WMOLXTBU.js +61 -0
  39. package/dist/chunk-YFQJXRIJ.js +193 -0
  40. package/dist/chunk-YQTGMCU6.js +111 -0
  41. package/dist/chunk-ZAE25JM2.js +158 -0
  42. package/dist/cli/index.js +1203 -0
  43. package/dist/create-N4I7TIZE.js +223 -0
  44. package/dist/deadline-F5RJJ2QW.js +86 -0
  45. package/dist/detect-LARDSLLZ.js +10 -0
  46. package/dist/evalCommand-3SSFMZQD.js +2798 -0
  47. package/dist/export-HN3CT7AV.js +158 -0
  48. package/dist/inkChat-34BTFG2K.js +1193 -0
  49. package/dist/install-VRPBZY5H.js +17 -0
  50. package/dist/job-3NYDMP2M.js +31 -0
  51. package/dist/jobCommand-XJ5GULS5.js +30 -0
  52. package/dist/launchd-QVHQ3VNN.js +40 -0
  53. package/dist/lspCommand-XNTBHXLA.js +99 -0
  54. package/dist/manager-4PMSXT47.js +14 -0
  55. package/dist/manager-CSI76AWF.js +847 -0
  56. package/dist/nightCommand-CR6ZHYGP.js +247 -0
  57. package/dist/oauth-W6OK6XXO.js +23 -0
  58. package/dist/openaiCompatible-CJWVJP5M.js +501 -0
  59. package/dist/policy-EJPLEEXP.js +31 -0
  60. package/dist/proxy-XJAHCZB2.js +192 -0
  61. package/dist/remove-LE7Q7K46.js +59 -0
  62. package/dist/run-ML6GSPN4.js +888 -0
  63. package/dist/server-R3GNIC33.js +676 -0
  64. package/dist/servers-6YDGWOZ6.js +24 -0
  65. package/dist/text-J2GD647J.js +11 -0
  66. package/package.json +84 -3
@@ -0,0 +1,1247 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ colorDiff
4
+ } from "./chunk-4JSUUDFQ.js";
5
+ import {
6
+ SPECIALIST_SPECS,
7
+ THINKING_WORDS,
8
+ callersText,
9
+ findCallersTool,
10
+ findReferencesTool,
11
+ findSymbolText,
12
+ findSymbolTool,
13
+ impactAnalysisText,
14
+ impactAnalysisTool,
15
+ modelFacts,
16
+ referencesText,
17
+ repoMapText,
18
+ repoMapTool,
19
+ verifyAuditFile
20
+ } from "./chunk-K7KK7U44.js";
21
+ import {
22
+ WEB_SEARCH_USD,
23
+ totalTokens
24
+ } from "./chunk-HRGX53VC.js";
25
+ import {
26
+ BUILTIN_COMMANDS
27
+ } from "./chunk-23FXBNBB.js";
28
+ import {
29
+ LSP_LANGUAGES
30
+ } from "./chunk-EU7PA45O.js";
31
+ import {
32
+ listJobs
33
+ } from "./chunk-KKB5W47S.js";
34
+
35
+ // src/cli/errors.ts
36
+ function describeError(error) {
37
+ const parts = [];
38
+ let current = error;
39
+ for (let depth = 0; current !== void 0 && current !== null && depth < 5; depth++) {
40
+ if (current instanceof Error) {
41
+ const code = current.code;
42
+ parts.push(typeof code === "string" ? `${current.message} (${code})` : current.message);
43
+ current = current.cause;
44
+ } else {
45
+ parts.push(String(current));
46
+ break;
47
+ }
48
+ }
49
+ return parts.join("\n caused by: ");
50
+ }
51
+
52
+ // src/cli/focus.ts
53
+ var FOCUS_REPORTING_ON = "\x1B[?1004h";
54
+ var FOCUS_REPORTING_OFF = "\x1B[?1004l";
55
+ var REPORTING_TERMINALS = /* @__PURE__ */ new Set(["iTerm.app", "ghostty", "WezTerm", "vscode"]);
56
+ function reportsFocus(env = process.env) {
57
+ const multiplexer = env.TMUX !== void 0 || (env.TERM ?? "").startsWith("screen");
58
+ return !multiplexer && REPORTING_TERMINALS.has(env.TERM_PROGRAM ?? "");
59
+ }
60
+ var FocusTracker = class {
61
+ focused = true;
62
+ update(focused) {
63
+ this.focused = focused;
64
+ }
65
+ };
66
+ function focusEvent(input) {
67
+ const code = input.startsWith("\x1B") ? input.slice(1) : input;
68
+ if (code === "[I") return true;
69
+ if (code === "[O") return false;
70
+ return void 0;
71
+ }
72
+
73
+ // src/cli/report.ts
74
+ function formatTokens(n) {
75
+ if (n < 1e3) return String(n);
76
+ if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
77
+ return `${(n / 1e6).toFixed(1)}M`;
78
+ }
79
+ function formatUsd(usd) {
80
+ if (usd === void 0) return "cost unknown";
81
+ return usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
82
+ }
83
+ function usageLine(result, session, runCostUsd, contextWindow) {
84
+ const u = result.usage;
85
+ const input = u.inputTokens + u.cacheReadTokens + u.cacheWriteTokens;
86
+ const percent = Math.round(session.contextTokens / contextWindow * 100);
87
+ return [
88
+ `${result.stopReason} \xB7 ${result.steps} step(s)`,
89
+ `${formatTokens(input)} in (${formatTokens(u.cacheReadTokens)} cached) / ${formatTokens(u.outputTokens)} out`,
90
+ formatUsd(runCostUsd),
91
+ `context ${percent}% of ${formatTokens(contextWindow)}`,
92
+ `session ${formatTokens(totalTokens(session.usage))} tokens, ${formatUsd(session.costUsd)}`
93
+ ].join(" \xB7 ");
94
+ }
95
+ function stopMessage(reason, limits) {
96
+ switch (reason) {
97
+ case "done":
98
+ return void 0;
99
+ case "max_steps":
100
+ return `Stopped: the run reached the step limit (${limits.maxSteps}). Use --resume to continue, or raise limits.maxSteps in .garuda/settings.json.`;
101
+ case "token_budget":
102
+ return `Stopped: the session used its token budget (${formatTokens(limits.tokenBudget)} tokens). Raise limits.tokenBudget in .garuda/settings.json to continue.`;
103
+ case "repeated_calls":
104
+ return "Stopped: the agent made the same tool call 3 times in a row, so it may be stuck. Use --resume with a hint to continue.";
105
+ case "max_tokens":
106
+ return "Stopped: the response reached the output token limit.";
107
+ case "refusal":
108
+ return "Stopped: the model refused the request.";
109
+ }
110
+ }
111
+
112
+ // src/cli/turn.ts
113
+ async function runTurnInTerminal(runtime, approver, renderer, prompt, exitNow, watcher) {
114
+ const started = performance.now();
115
+ watcher?.turnStarted();
116
+ let outcome;
117
+ try {
118
+ outcome = await runTurn(runtime, approver, renderer, prompt, exitNow);
119
+ return outcome;
120
+ } finally {
121
+ watcher?.turnEnded(outcome, performance.now() - started);
122
+ }
123
+ }
124
+ async function runTurn(runtime, approver, renderer, prompt, exitNow) {
125
+ const controller = new AbortController();
126
+ const stop = () => {
127
+ if (controller.signal.aborted) exitNow();
128
+ renderer.warn("\nStopping\u2026 (press Ctrl-C again to exit Garuda)");
129
+ controller.abort();
130
+ };
131
+ process.on("SIGINT", stop);
132
+ approver.onInterrupt = stop;
133
+ const costBefore = runtime.session?.costUsd ?? 0;
134
+ try {
135
+ const result = await runtime.runTurn(prompt, controller.signal);
136
+ const session = runtime.session;
137
+ if (session !== void 0) {
138
+ const runCost = session.costUsd === void 0 ? void 0 : session.costUsd - costBefore;
139
+ renderer.info(`
140
+ [${usageLine(result, session, runCost, runtime.limits.contextWindow)}]`);
141
+ }
142
+ const message = stopMessage(result.stopReason, runtime.limits);
143
+ if (message !== void 0) renderer.warn(message);
144
+ return { kind: "done", result };
145
+ } catch (error) {
146
+ if (controller.signal.aborted) {
147
+ runtime.recordStop("interrupted");
148
+ renderer.warn("Turn stopped.");
149
+ return { kind: "interrupted" };
150
+ }
151
+ runtime.recordStop("error");
152
+ const message = describeError(error);
153
+ renderer.error(`Error: ${message}`);
154
+ return { kind: "error", message };
155
+ } finally {
156
+ process.off("SIGINT", stop);
157
+ approver.onInterrupt = () => {
158
+ };
159
+ }
160
+ }
161
+
162
+ // src/cli/chat/plan.ts
163
+ var BUILD_PROMPT = "Carry out the plan above.";
164
+ async function planHandoff(runtime, approver, signal = new AbortController().signal) {
165
+ const request = {
166
+ tool: "plan",
167
+ target: { kind: "input", json: "{}" },
168
+ preview: "Build mode can change files and run commands, with the usual approvals and sandbox. To build it later, unattended, choose No and type /schedule.",
169
+ isolation: runtime.executor.isolation,
170
+ title: "The plan is ready.",
171
+ question: "Build this plan?",
172
+ labels: {
173
+ once: "Yes, build it now",
174
+ session: "Switch to build mode; I will type the task",
175
+ deny: "No, keep planning"
176
+ }
177
+ };
178
+ const choice = await approver.ask(request, signal);
179
+ if (choice === "deny") return "stay";
180
+ runtime.setMode("build");
181
+ return choice === "once" ? "now" : "later";
182
+ }
183
+ function modeText(runtime) {
184
+ return runtime.mode === "plan" ? "Plan mode: the agent reads and plans. It cannot change files; commands run read-only in the sandbox. /build (or Shift+Tab) switches back." : "Build mode: the agent can change files and run commands, with the usual approvals.";
185
+ }
186
+
187
+ // src/cli/chat/commands.ts
188
+ var HELP = [
189
+ "Type a task and press Enter. @path attaches a file or folder, !command runs a command, Tab completes.",
190
+ "Commands:",
191
+ " /help show this help",
192
+ " /usage tokens and cost of this session",
193
+ " /session the session id and file",
194
+ " /sessions this project's sessions; /sessions <n|id> continues one; rename, delete",
195
+ " /models the models; /models <n|id|opus|sonnet|haiku> switches for this chat",
196
+ " /export write this conversation as Markdown; /export <file>",
197
+ " /compact summarise the older turns now; /compact <what to keep>",
198
+ " /diff file changes in this session; /diff last (last turn); /diff [last] <path>",
199
+ " /schedule make the last plan a job that runs later: /schedule [HH:MM], then garuda run <id>",
200
+ " /jobs the scheduled jobs of this project; /jobs <id> shows one; /jobs cancel|delete <id>; /jobs cancel night",
201
+ " /where X where symbol X is defined (code index, no model call)",
202
+ " /defs X alias of /where X (definitions of X)",
203
+ " /refs X every use of symbol X (code index, no model call)",
204
+ " /callers X who calls symbol X (code index, no model call)",
205
+ " /impact X blast radius and affected tests for file or symbol X",
206
+ " /map [dir] what each code file exports and imports",
207
+ " /mcp MCP servers: state, sandbox, network and tool count; /mcp logout <server>",
208
+ " /hooks the active hooks",
209
+ " /lsp language servers for diagnostics; /lsp install <typescript|python|java>",
210
+ " /languages registered language plugins and indexed file counts",
211
+ " /experts MoE language specialist subagents and their active status",
212
+ " /audit team security policy and recent audit trail; /audit [n|denials|stats|verify]",
213
+ " /commands your custom commands and skills (~/.garuda, .garuda, .claude)",
214
+ " /agents your custom agents and their tools (~/.garuda, .garuda, .claude)",
215
+ " /plan plan mode: read and plan, change nothing (Shift+Tab toggles); /plan <task> plans it",
216
+ " /build build mode: change files and run commands (the default); /build <task> runs it",
217
+ " /undo take back the last turn: its file changes and its messages",
218
+ " /redo bring back the last undone turn",
219
+ " /init set up this folder: AGENTS.md, and files from other agents (Claude Code, OpenCode \u2026)",
220
+ " /editor write the next prompt in $VISUAL or $EDITOR (also Ctrl-G)",
221
+ " /details show or hide the result line under each tool call; /details on|off",
222
+ " /thinking Claude's thinking: /thinking low|medium|high|xhigh|max|default, show|hide, on|off",
223
+ " /search web search: /search shows it; /search claude|provider|off (this session); /search default <one> (saved)",
224
+ " /new start a new session (the old one stays on disk)",
225
+ " /exit leave (or press Ctrl-D, or Ctrl-C twice)"
226
+ ].join("\n");
227
+ async function runCommand(text, { runtime, renderer, sessionPath, output, signal }) {
228
+ const command = text.split(/\s+/)[0];
229
+ if (command === "/exit" || command === "/quit") return "exit";
230
+ if (command === "/help") renderer.info(helpText(runtime));
231
+ else if (command === "/commands") renderer.info(commandsText(runtime));
232
+ else if (command === "/agents") renderer.info(agentsText(runtime));
233
+ else if (command === "/plan" || command === "/build") {
234
+ runtime.setMode(command === "/plan" ? "plan" : "build");
235
+ renderer.info(modeText(runtime));
236
+ const task = text.slice(command.length).trim();
237
+ if (task !== "") return { prompt: task };
238
+ } else if (command === "/thinking") {
239
+ const word = text.slice(command.length).trim();
240
+ if (word === "") renderer.info(runtime.thinkingStatus());
241
+ else {
242
+ const result = runtime.setThinking(word);
243
+ if (result.ok) renderer.info(result.text);
244
+ else renderer.warn(result.text);
245
+ }
246
+ } else if (command === "/search") {
247
+ const words = text.slice(command.length).trim().split(/\s+/).filter(Boolean);
248
+ const save = words[0] === "default";
249
+ const use = save ? words[1] : words[0];
250
+ if (use === void 0 && !save) renderer.info(runtime.searchStatus());
251
+ else if (use === "claude" || use === "provider" || use === "off") {
252
+ const result = await runtime.setSearch(use, save);
253
+ if (result.ok) renderer.info(result.text);
254
+ else renderer.warn(result.text);
255
+ } else
256
+ renderer.warn(
257
+ "Use: /search, /search claude|provider|off, /search default claude|provider|off."
258
+ );
259
+ } else if (command === "/details") {
260
+ detailsCommand(renderer, text.slice(command.length).trim());
261
+ } else if (command === "/usage") renderer.info(usageSummary(runtime));
262
+ else if (command === "/session") {
263
+ const id = runtime.session?.id;
264
+ if (id === void 0) {
265
+ renderer.info("No session yet.");
266
+ } else {
267
+ const lines = [`Session ${id}`, sessionPath(id)];
268
+ const sub = runtime.subagentsSummary();
269
+ if (sub.length > 0) lines.push(`Subagents: ${sub.join(" \xB7 ")} (see /experts)`);
270
+ const ag = runtime.agentsSummary();
271
+ if (ag.length > 0) lines.push(`Agents: ${ag.join(" \xB7 ")} (see /agents)`);
272
+ const sk = runtime.skillsSummary();
273
+ if (sk.length > 0) lines.push(`Skills: ${sk.join(" \xB7 ")} (see /commands)`);
274
+ const tl = runtime.toolsSummary();
275
+ if (tl.length > 0) lines.push(`Tools: ${tl.join(" \xB7 ")}`);
276
+ renderer.info(lines.join("\n"));
277
+ }
278
+ } else if (command === "/sessions") {
279
+ await sessionsCommand(
280
+ runtime,
281
+ renderer,
282
+ text.slice(command.length).trim(),
283
+ signal ?? new AbortController().signal
284
+ );
285
+ } else if (command === "/models") {
286
+ const arg = text.slice(command.length).trim();
287
+ if (arg === "") renderer.info(modelsText(runtime));
288
+ else {
289
+ const result = await runtime.setModel(arg);
290
+ if (result.ok) renderer.info(result.text);
291
+ else renderer.warn(result.text);
292
+ }
293
+ } else if (command === "/compact") {
294
+ await compactCommand(
295
+ runtime,
296
+ renderer,
297
+ text.slice(command.length).trim(),
298
+ signal ?? new AbortController().signal
299
+ );
300
+ } else if (command === "/diff") {
301
+ await diffCommand(runtime, renderer, text.slice(command.length).trim(), output);
302
+ } else if (command === "/schedule") {
303
+ const at = text.slice(command.length).trim();
304
+ const result = await runtime.scheduleJob(
305
+ at === "" ? void 0 : at,
306
+ new AbortController().signal
307
+ );
308
+ if (result.ok) renderer.info(result.text);
309
+ else renderer.warn(result.text);
310
+ } else if (command === "/jobs") {
311
+ await jobsCommand(runtime, renderer, text.slice(command.length).trim());
312
+ } else if (command === "/export") {
313
+ await exportCommand(runtime, renderer, text.slice(command.length).trim());
314
+ } else if (command === "/where" || command === "/defs" || command === "/refs" || command === "/callers" || command === "/impact" || command === "/map") {
315
+ await lookup(runtime, renderer, command, text.slice(command.length).trim());
316
+ } else if (command === "/hooks") {
317
+ const lines = runtime.hookLines();
318
+ renderer.info(
319
+ lines.length === 0 ? "No active hooks. They load with the first task. Configure them in ~/.garuda/hooks.json or .garuda/hooks.json." : lines.join("\n")
320
+ );
321
+ } else if (command === "/mcp") {
322
+ const [verb, name] = text.slice(command.length).trim().split(/\s+/);
323
+ if (verb === "logout" && name !== void 0 && name !== "") {
324
+ const removed = await runtime.mcpLogout(name);
325
+ renderer.info(
326
+ removed > 0 ? `Signed out of MCP server "${name}": its tokens are gone. The next session asks you to sign in again.` : `There are no tokens for MCP server "${name}".`
327
+ );
328
+ } else if (verb !== void 0 && verb !== "") {
329
+ renderer.warn("Use: /mcp, or /mcp logout <server>.");
330
+ } else {
331
+ renderer.info(mcpSummary(runtime));
332
+ }
333
+ } else if (command === "/lsp") {
334
+ await lspCommand(runtime, renderer, text.slice(command.length).trim());
335
+ } else if (command === "/languages") {
336
+ await languagesCommand(runtime, renderer);
337
+ } else if (command === "/experts") {
338
+ await expertsCommand(runtime, renderer);
339
+ } else if (command === "/audit") {
340
+ await auditCommand(runtime, renderer, text.slice(command.length).trim());
341
+ } else if (command === "/undo" || command === "/redo") {
342
+ const signal2 = new AbortController().signal;
343
+ renderer.info(await (command === "/undo" ? runtime.undo(signal2) : runtime.redo(signal2)));
344
+ } else if (command === "/init") {
345
+ const { report, prompt } = await runtime.init(new AbortController().signal);
346
+ if (report.length > 0) renderer.info(report.join("\n"));
347
+ return { prompt };
348
+ } else if (command === "/editor") {
349
+ renderer.info("The external editor works in the full chat: Ctrl-G or /editor.");
350
+ } else if (command === "/new") {
351
+ runtime.newSession();
352
+ renderer.info("The next task starts a new session.");
353
+ } else {
354
+ const resolved = await runtime.resolveCommand(text, new AbortController().signal);
355
+ if (resolved.kind === "prompt") return { prompt: resolved.prompt };
356
+ if (resolved.kind === "denied") renderer.info(resolved.message);
357
+ else renderer.warn(`Unknown command ${command}. Type /help.`);
358
+ }
359
+ return "done";
360
+ }
361
+ async function sessionsCommand(runtime, renderer, arg, signal) {
362
+ const [verb = "", ref = "", ...rest] = arg.split(/\s+/);
363
+ if (verb === "rename" || verb === "delete") {
364
+ const use = `Use: /sessions ${verb} <number or id>${verb === "rename" ? " <title>" : ""}.`;
365
+ if (ref === "" || verb === "rename" && rest.length === 0 || verb === "delete" && rest.length > 0) {
366
+ renderer.warn(use);
367
+ return;
368
+ }
369
+ const result = verb === "rename" ? await runtime.renameSession(ref, rest.join(" ")) : await runtime.deleteSession(ref, signal);
370
+ if (result.ok) renderer.info(result.text);
371
+ else renderer.warn(result.text);
372
+ return;
373
+ }
374
+ if (arg !== "") {
375
+ const result = await runtime.switchSession(arg);
376
+ if (result.ok) renderer.info(result.text);
377
+ else renderer.warn(result.text);
378
+ return;
379
+ }
380
+ const { sessions, total } = await runtime.listSessions();
381
+ if (sessions.length === 0) {
382
+ renderer.info("There are no sessions in this project yet.");
383
+ return;
384
+ }
385
+ const open = runtime.session?.id;
386
+ const width = String(sessions.length).length;
387
+ const rows = sessions.flatMap((s, i) => {
388
+ const facts = [
389
+ s.id,
390
+ `${s.turns} turn${s.turns === 1 ? "" : "s"}`,
391
+ ...s.costUsd === void 0 || s.costUsd === 0 ? [] : [`$${s.costUsd.toFixed(2)}`],
392
+ ...s.model === void 0 ? [] : [s.model]
393
+ ].join(" \xB7 ");
394
+ return [
395
+ ` ${String(i + 1).padStart(width)}. ${localTime(s.updated)} ${s.title === "" ? "(no prompt)" : s.title}${s.id === open ? " (open)" : ""}`,
396
+ ` ${"".padStart(width)} ${facts}`
397
+ ];
398
+ });
399
+ renderer.info(
400
+ [
401
+ "Sessions in this project, newest first:",
402
+ ...rows,
403
+ ...total > sessions.length ? [` \u2026 ${total - sessions.length} older`] : [],
404
+ "Continue one with /sessions <number or id>. /new starts a new one.",
405
+ "/sessions rename <number or id> <title> \xB7 /sessions delete <number or id>"
406
+ ].join("\n")
407
+ );
408
+ }
409
+ function localTime(d) {
410
+ const pad = (n) => String(n).padStart(2, "0");
411
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
412
+ }
413
+ function modelsText(runtime) {
414
+ const list = runtime.modelList();
415
+ const width = Math.max(...list.map((m) => m.spec.length));
416
+ const numbers = String(list.length).length;
417
+ return [
418
+ "Models (\u25CF the current one):",
419
+ ...list.map((m, i) => {
420
+ const mark = m.spec === runtime.modelId ? "\u25CF" : " ";
421
+ return ` ${mark} ${String(i + 1).padStart(numbers)} ${m.spec.padEnd(width + 2)}${modelFacts(m.info.contextWindow, m.info.price)}`;
422
+ }),
423
+ "Switch with /models <number, id or opus|sonnet|haiku|fable>. It is for this chat only.",
424
+ "Add other models (Ollama, OpenRouter \u2026) in ~/.garuda/models.json."
425
+ ].join("\n");
426
+ }
427
+ var DIFF_LINES = 300;
428
+ async function diffCommand(runtime, renderer, arg, output) {
429
+ const words = arg === "" ? [] : arg.split(/\s+/);
430
+ const scope = words[0] === "last" ? "last" : "session";
431
+ const path = (scope === "last" ? words.slice(1) : words).join(" ");
432
+ const result = await runtime.diff(
433
+ scope,
434
+ path === "" ? void 0 : path,
435
+ new AbortController().signal
436
+ );
437
+ if ("problem" in result) {
438
+ renderer.warn(result.problem);
439
+ return;
440
+ }
441
+ const since = scope === "last" ? "since the start of the last turn" : "since the first turn of this session";
442
+ const where = result.path === void 0 ? "" : ` in ${result.path}`;
443
+ if (result.files.length === 0) {
444
+ renderer.info(`No file changed${where} ${since}.`);
445
+ return;
446
+ }
447
+ const plus = result.files.reduce((n, f) => n + (f.added ?? 0), 0);
448
+ const minus = result.files.reduce((n, f) => n + (f.removed ?? 0), 0);
449
+ const width = Math.max(...result.files.map((f) => f.path.length));
450
+ const mark = { added: "A", modified: "M", deleted: "D" };
451
+ const rows = result.files.map((f) => {
452
+ const counts = f.added === void 0 ? "binary" : [f.added > 0 ? `+${f.added}` : "", f.removed ? `\u2212${f.removed}` : ""].filter(Boolean).join(" ");
453
+ return ` ${mark[f.status]} ${f.path.padEnd(width + 2)}${counts}`;
454
+ });
455
+ const lines = result.patch.replace(/\n$/, "").split("\n");
456
+ const cut = lines.length > DIFF_LINES;
457
+ const more = output === void 0 ? "/diff <path> shows one file." : "Ctrl-O shows all; /diff <path> shows one file.";
458
+ (output ?? ((t) => renderer.info(t)))(
459
+ [
460
+ `Changes${where} ${since} (${result.files.length} file${result.files.length === 1 ? "" : "s"}, +${plus} \u2212${minus}). Changes you made yourself count too.`,
461
+ ...rows,
462
+ "",
463
+ colorDiff(lines.slice(0, DIFF_LINES).join("\n")),
464
+ ...cut ? [`\u2026 ${lines.length - DIFF_LINES} more lines. ${more}`] : []
465
+ ].join("\n"),
466
+ cut ? { title: `diff ${since}${where}`, text: colorDiff(result.patch) } : void 0
467
+ );
468
+ }
469
+ async function jobsCommand(runtime, renderer, id) {
470
+ const { listJobs: listJobs2, loadJob, JOBS_DIR } = await import("./job-3NYDMP2M.js");
471
+ const cancel = /^cancel\s+(\S+)$/.exec(id);
472
+ if (cancel !== null) {
473
+ renderer.info(await runtime.cancelJob(cancel[1]));
474
+ return;
475
+ }
476
+ const remove = /^delete\s+(\S+)$/.exec(id);
477
+ if (remove !== null) {
478
+ renderer.info(await runtime.deleteJob(remove[1], new AbortController().signal));
479
+ return;
480
+ }
481
+ if (id !== "") {
482
+ try {
483
+ const job = await loadJob(runtime.root, id);
484
+ const { jobReport } = await import("./text-J2GD647J.js");
485
+ renderer.info(jobReport(job));
486
+ } catch (error) {
487
+ renderer.warn(error.message);
488
+ }
489
+ return;
490
+ }
491
+ const jobs = await listJobs2(runtime.root);
492
+ const { defaultAgentEnv, nightAgentTime } = await import("./launchd-QVHQ3VNN.js");
493
+ const night = nightAgentTime(runtime.root, defaultAgentEnv());
494
+ const nightLine = night === void 0 ? [] : [
495
+ `Night shift: launchd starts the queue at ${night.toTimeString().slice(0, 5)} on ${night.toDateString()} (/jobs cancel night removes it).`
496
+ ];
497
+ if (jobs.length === 0) {
498
+ renderer.info("No jobs in this project. Make a plan (/plan), then /schedule.");
499
+ return;
500
+ }
501
+ renderer.info(
502
+ [
503
+ ...nightLine,
504
+ `Jobs (${JOBS_DIR}), newest first:`,
505
+ ...jobs.map((j) => {
506
+ const r = j.result;
507
+ const facts = [
508
+ j.status,
509
+ ...j.status === "scheduled" && j.queue === true && j.launchd === void 0 ? ["in the night queue"] : [],
510
+ ...j.at === void 0 || j.status !== "scheduled" ? [] : [`at ${j.at}`],
511
+ ...r?.proof === void 0 ? [] : [r.proof.verdict === "ready" ? "ready to merge" : "needs a look"],
512
+ ...r === void 0 ? [] : [`${r.files.length} file(s)`],
513
+ ...r?.costUsd === void 0 ? [] : [`$${r.costUsd.toFixed(2)}`],
514
+ ...r === void 0 || r.denied.length === 0 ? [] : [`${r.denied.length} denied`]
515
+ ].join(" \xB7 ");
516
+ return ` ${j.id} ${j.title}
517
+ ${facts}`;
518
+ }),
519
+ "Details: /jobs <id>. Run one: garuda run <id> [--at HH:MM]. Run the queue: garuda night [--at HH:MM]."
520
+ ].join("\n")
521
+ );
522
+ }
523
+ async function exportCommand(runtime, renderer, file) {
524
+ const records = await runtime.sessionRecords();
525
+ const id = runtime.session?.id;
526
+ if (records === void 0 || id === void 0) {
527
+ renderer.info("There is no conversation to export yet.");
528
+ return;
529
+ }
530
+ const { sessionMarkdown, writeExport } = await import("./export-HN3CT7AV.js");
531
+ const result = await writeExport(runtime.root, id, sessionMarkdown(records, id), file);
532
+ if ("problem" in result) renderer.warn(result.problem);
533
+ else
534
+ renderer.info(
535
+ `Wrote the conversation to ${result.path}. Secrets are redacted as in the session file.`
536
+ );
537
+ }
538
+ async function lspCommand(runtime, renderer, arg) {
539
+ if (arg === "") {
540
+ renderer.info(await runtime.lspStatus());
541
+ return;
542
+ }
543
+ const [verb, language] = arg.split(/\s+/);
544
+ const lang = LSP_LANGUAGES.find((l) => l === language);
545
+ if (verb !== "install" || lang === void 0) {
546
+ renderer.warn(`Use: /lsp, or /lsp install <${LSP_LANGUAGES.join("|")}>.`);
547
+ return;
548
+ }
549
+ renderer.info(`Installing the ${lang} language server (download outside the sandbox) \u2026`);
550
+ const result = await runtime.installLsp(lang, new AbortController().signal);
551
+ if (!result.ok) {
552
+ renderer.warn(`The install failed.
553
+ $ ${result.command}
554
+ ${result.output ?? ""}`);
555
+ return;
556
+ }
557
+ renderer.info(
558
+ runtime.lspEnabled ? `Installed into ${result.dir}. The next edit of a ${lang} file uses it.` : `Installed into ${result.dir}. Diagnostics are off: start with --lsp, or set "lsp": { "enabled": true } in .garuda/settings.json.`
559
+ );
560
+ }
561
+ function helpText(runtime) {
562
+ return runtime.commands.length + runtime.skills.length === 0 ? HELP : `${HELP}
563
+
564
+ ${commandsText(runtime)}`;
565
+ }
566
+ function commandsText(runtime) {
567
+ const skills = runtime.skills;
568
+ if (runtime.commands.length + skills.length === 0) {
569
+ return [
570
+ "No custom commands and no skills.",
571
+ "Commands: Markdown files in ~/.garuda/commands/ or .garuda/commands/.",
572
+ "Skills: <name>/SKILL.md folders in ~/.garuda/skills/, .garuda/skills/ (or .claude/skills/)."
573
+ ].join("\n");
574
+ }
575
+ const rows = (list) => list.map((c) => ({
576
+ usage: `/${c.name}${c.argumentHint === void 0 ? "" : ` ${c.argumentHint}`}`,
577
+ about: `${c.about}${c.project ? " (project)" : ""}`
578
+ }));
579
+ const commandRows = rows(
580
+ runtime.commands.map((c) => ({
581
+ name: c.name,
582
+ ...c.argumentHint === void 0 ? {} : { argumentHint: c.argumentHint },
583
+ about: c.description ?? "",
584
+ project: c.source === "project"
585
+ }))
586
+ );
587
+ const skillRows = skills.map((s) => ({
588
+ usage: s.userInvocable ? `/${s.name}${s.argumentHint === void 0 ? "" : ` ${s.argumentHint}`}` : s.name,
589
+ about: `${s.description.length > 80 ? `${s.description.slice(0, 79)}\u2026` : s.description} (${s.shown}${s.modelInvocable ? "" : ", you only"}${s.userInvocable ? "" : ", model only"})`
590
+ }));
591
+ const width = Math.max(...[...commandRows, ...skillRows].map((r) => r.usage.length));
592
+ const table = (title, list) => list.length === 0 ? [] : [title, ...list.map((r) => ` ${r.usage.padEnd(width + 2)}${r.about}`.trimEnd())];
593
+ return [...table("Custom commands:", commandRows), ...table("Skills:", skillRows)].join("\n");
594
+ }
595
+ function agentsText(runtime) {
596
+ const agents = runtime.agents;
597
+ if (agents.length === 0) {
598
+ return "No custom agents. Add Markdown files to ~/.garuda/agents/ or .garuda/agents/ (or .claude/agents/).";
599
+ }
600
+ const width = Math.max(...agents.map((a) => a.name.length));
601
+ return [
602
+ "Custom agents (the model hands tasks to them with the agent tool):",
603
+ ...agents.map((a) => {
604
+ const about = a.description.length > 80 ? `${a.description.slice(0, 79)}\u2026` : a.description;
605
+ const tools = a.tools === void 0 ? "read-only tools" : a.tools.join(", ");
606
+ const model = a.model === void 0 ? "" : ` \xB7 model ${a.model}`;
607
+ return ` ${a.name.padEnd(width + 2)}${about}
608
+ ${"".padEnd(width + 2)}${tools}${model} \xB7 ${a.shown}`;
609
+ })
610
+ ].join("\n");
611
+ }
612
+ async function lookup(runtime, renderer, command, arg) {
613
+ if (command !== "/map" && arg === "") {
614
+ renderer.warn(`Usage: ${command} <symbol or file name>`);
615
+ return;
616
+ }
617
+ const context = { knowledge: runtime.knowledge };
618
+ try {
619
+ let text;
620
+ if (command === "/where" || command === "/defs") {
621
+ text = findSymbolText(await findSymbolTool.run({ name: arg, exact: true }, context));
622
+ } else if (command === "/refs") {
623
+ text = referencesText(await findReferencesTool.run({ name: arg }, context));
624
+ } else if (command === "/callers") {
625
+ text = callersText(await findCallersTool.run({ name: arg }, context));
626
+ } else if (command === "/impact") {
627
+ text = impactAnalysisText(await impactAnalysisTool.run({ target: arg }, context));
628
+ } else {
629
+ text = repoMapText(await repoMapTool.run({ path: arg }, context));
630
+ }
631
+ renderer.info(text);
632
+ } catch (error) {
633
+ renderer.error(error.message);
634
+ }
635
+ }
636
+ async function languagesCommand(runtime, renderer) {
637
+ const statuses = await runtime.knowledge.languageStatuses();
638
+ if (statuses.length === 0) {
639
+ renderer.info("No language plugins registered.");
640
+ return;
641
+ }
642
+ const lines = ["Language plugins:"];
643
+ for (const s of statuses) {
644
+ const extStr = s.extensions.join(", ");
645
+ const stateStr = s.active ? `${s.indexedFiles} file(s) indexed` : "standby (no matching files)";
646
+ const sourceBadge = s.source === "built-in" ? "built-in" : `${s.source} plugin`;
647
+ lines.push(` \u2022 ${s.id} (${sourceBadge}): ${extStr} \u2014 ${stateStr}`);
648
+ }
649
+ const warnings = runtime.knowledge.getWarnings();
650
+ if (warnings.length > 0) {
651
+ lines.push("");
652
+ lines.push("Warnings:");
653
+ for (const w of warnings) {
654
+ lines.push(` \u26A0 ${w}`);
655
+ }
656
+ }
657
+ renderer.info(lines.join("\n"));
658
+ }
659
+ async function expertsCommand(runtime, renderer) {
660
+ const statuses = await runtime.knowledge.languageStatuses();
661
+ const lines = ["Mixture-of-Experts (MoE) Language Specialists:"];
662
+ const specialistEntries = Object.values(SPECIALIST_SPECS);
663
+ for (const spec of specialistEntries) {
664
+ const status = statuses.find((s) => s.id === spec.id);
665
+ const active = status?.active ?? false;
666
+ const count = status?.indexedFiles ?? 0;
667
+ const badge = active ? `${count} file(s) active` : "standby";
668
+ lines.push(` \u2022 ${spec.name} (${spec.id}): ${badge}`);
669
+ lines.push(` test command: ${spec.testCommand}`);
670
+ }
671
+ renderer.info(lines.join("\n"));
672
+ }
673
+ async function auditCommand(runtime, renderer, arg) {
674
+ const policy = runtime.teamPolicy;
675
+ const lines = [];
676
+ if (policy === void 0) {
677
+ lines.push("Team Policy: none (no managed policy file and no ~/.garuda/policy.json)");
678
+ } else {
679
+ const from = runtime.teamPolicySources;
680
+ lines.push(
681
+ `Team Policy: active (${from.length === 0 ? "given by the caller" : from.join(", ")})`
682
+ );
683
+ if (policy.requireSandbox === true) {
684
+ lines.push(" \u2022 Require Sandbox: enabled (all bash commands must run sandboxed)");
685
+ }
686
+ if (policy.disallowedCommands && policy.disallowedCommands.length > 0) {
687
+ const sample = policy.disallowedCommands.slice(0, 3).join(", ");
688
+ const extra = policy.disallowedCommands.length > 3 ? "\u2026" : "";
689
+ lines.push(
690
+ ` \u2022 Disallowed Commands: ${policy.disallowedCommands.length} pattern(s) (${sample}${extra})`
691
+ );
692
+ }
693
+ if (policy.denyPaths && policy.denyPaths.length > 0) {
694
+ const sample = policy.denyPaths.slice(0, 3).join(", ");
695
+ const extra = policy.denyPaths.length > 3 ? "\u2026" : "";
696
+ lines.push(` \u2022 Deny Paths: ${policy.denyPaths.length} pattern(s) (${sample}${extra})`);
697
+ }
698
+ if (policy.allowedModels && policy.allowedModels.length > 0) {
699
+ lines.push(` \u2022 Allowed Models: ${policy.allowedModels.join(", ")}`);
700
+ }
701
+ if (policy.network?.blockedHosts && policy.network.blockedHosts.length > 0) {
702
+ lines.push(` \u2022 Blocked Hosts: ${policy.network.blockedHosts.join(", ")}`);
703
+ }
704
+ if (policy.network?.strictAllowlist === true) {
705
+ lines.push(" \u2022 Network: strict allowlist enforced (no interactive exceptions)");
706
+ }
707
+ }
708
+ if (arg === "verify") {
709
+ const files = await runtime.audit.files();
710
+ lines.push("", `Audit files: ${runtime.audit.dir}`);
711
+ if (files.length === 0) lines.push(" none yet");
712
+ let broken = 0;
713
+ for (const file of files) {
714
+ const result = await verifyAuditFile(file);
715
+ const name = file.slice(runtime.audit.dir.length + 1);
716
+ if (result.ok) {
717
+ lines.push(` ok ${name} (${result.lines} lines)`);
718
+ } else {
719
+ broken++;
720
+ lines.push(` BROKEN ${name}: line ${result.line}: ${result.why}`);
721
+ }
722
+ }
723
+ if (broken > 0) lines.push(`${broken} file(s) were changed after Garuda wrote them.`);
724
+ lines.push("This check cannot see lines cut from the end of a file, or a removed file.");
725
+ renderer.info(lines.join("\n"));
726
+ return;
727
+ }
728
+ if (arg === "stats") {
729
+ const stats = await runtime.audit.getStats();
730
+ lines.push("");
731
+ lines.push("Audit Statistics:");
732
+ lines.push(` Total Events: ${stats.total}`);
733
+ lines.push(` Allowed: ${stats.allowed}`);
734
+ lines.push(` Denied / Blocked: ${stats.denied}`);
735
+ lines.push(` Policy Blocks: ${stats.policyBlocked}`);
736
+ lines.push(` Critical Risk: ${stats.criticalCount}`);
737
+ renderer.info(lines.join("\n"));
738
+ return;
739
+ }
740
+ const isDenials = arg === "denials";
741
+ const limit = /^\d+$/.test(arg) ? Number.parseInt(arg, 10) : 10;
742
+ const events = await runtime.audit.readEvents({
743
+ limit,
744
+ ...isDenials ? { denialsOnly: true } : {}
745
+ });
746
+ lines.push("");
747
+ if (events.length === 0) {
748
+ lines.push(
749
+ isDenials ? "No denied or blocked audit events found." : "No audit events recorded yet."
750
+ );
751
+ } else {
752
+ lines.push(
753
+ isDenials ? `Recent Security Denials / Policy Blocks (last ${events.length}):` : `Recent Audit Events (last ${events.length}):`
754
+ );
755
+ for (const e of events) {
756
+ const time = e.timestamp.slice(11, 19);
757
+ const badge = e.decision === "deny_policy" ? "BLOCK" : !e.allowed ? "DENY " : e.isError ? "ERROR" : "ALLOW";
758
+ const targetStr = e.target ? ` \xB7 ${e.target}` : "";
759
+ const reasonStr = e.reason ? ` (${e.reason})` : "";
760
+ const durStr = e.durationMs !== void 0 ? ` [${e.durationMs}ms]` : "";
761
+ lines.push(` [${time}] ${badge} (${e.risk}) ${e.tool}${targetStr}${durStr}${reasonStr}`);
762
+ }
763
+ }
764
+ renderer.info(lines.join("\n"));
765
+ }
766
+ function usageSummary(runtime) {
767
+ const session = runtime.session;
768
+ if (session === void 0) return "No session yet.";
769
+ const u = session.usage;
770
+ const cost = session.costUsd === void 0 ? "cost unknown" : `$${session.costUsd.toFixed(4)}`;
771
+ return [
772
+ `Session ${session.id}: ${formatTokens(totalTokens(u))} tokens, ${cost}`,
773
+ ` input ${formatTokens(u.inputTokens)}, cache read ${formatTokens(u.cacheReadTokens)}, cache write ${formatTokens(u.cacheWriteTokens)}, output ${formatTokens(u.outputTokens)}`,
774
+ ` context ${formatTokens(session.contextTokens)} of ${formatTokens(runtime.limits.contextWindow)}`,
775
+ ...u.webSearches === void 0 ? [] : [
776
+ ` Claude web searches: ${u.webSearches} ($${(u.webSearches * WEB_SEARCH_USD).toFixed(2)})`
777
+ ]
778
+ ].join("\n");
779
+ }
780
+ function mcpSummary(runtime) {
781
+ const servers = runtime.mcpStatus();
782
+ if (servers.length === 0) {
783
+ return "No MCP servers are running. They start with the first task. Configure them in ~/.garuda/mcp.json or .garuda/mcp.json.";
784
+ }
785
+ return servers.map((s) => {
786
+ const note = s.message === void 0 ? "" : ` (${s.message})`;
787
+ if (s.transport === "http") {
788
+ const auth = s.signedIn ? "signed in" : "no sign-in";
789
+ return `${s.name} [${s.source}] ${s.state}${note} \xB7 ${s.tools} tool(s) \xB7 remote ${s.url ?? ""} \xB7 ${auth}`;
790
+ }
791
+ const box = s.sandboxed ? "sandbox" : "NO sandbox";
792
+ const net = s.network ? "network" : "no network";
793
+ return `${s.name} [${s.source}] ${s.state}${note} \xB7 ${s.tools} tool(s) \xB7 ${box} \xB7 ${net}`;
794
+ }).join("\n");
795
+ }
796
+ async function compactCommand(runtime, renderer, focus, signal) {
797
+ if (runtime.session === void 0) {
798
+ renderer.info("No session yet: there is nothing to compact.");
799
+ return;
800
+ }
801
+ let result;
802
+ try {
803
+ result = await runtime.compact(focus, signal);
804
+ } catch (error) {
805
+ renderer.warn(
806
+ signal.aborted ? "Compaction stopped. The conversation did not change." : `Compaction failed, and the conversation did not change: ${error.message}`
807
+ );
808
+ return;
809
+ }
810
+ if (result === void 0) {
811
+ renderer.info(
812
+ "Too little to compact: the last 4 steps always stay in full. Auto-compaction still runs when the context gets full."
813
+ );
814
+ return;
815
+ }
816
+ const cost = result.costUsd === void 0 ? "" : ` \xB7 $${result.costUsd.toFixed(4)}`;
817
+ renderer.info(
818
+ `Context compacted: ${formatTokens(result.beforeTokens)} \u2192 about ${formatTokens(result.afterTokens)} tokens${cost}. The last 4 steps stay in full; /usage shows the session totals.`
819
+ );
820
+ }
821
+ function detailsCommand(renderer, arg) {
822
+ if (renderer.details === void 0) {
823
+ renderer.info("This view always shows the details.");
824
+ return;
825
+ }
826
+ if (arg !== "" && arg !== "on" && arg !== "off") {
827
+ renderer.warn("Use: /details, /details on or /details off.");
828
+ return;
829
+ }
830
+ const on = arg === "" ? !renderer.details : arg === "on";
831
+ renderer.details = on;
832
+ renderer.info(
833
+ on ? "Details on: each tool call shows its result line." : "Details off: tool calls show one line; a failed call still shows its result. In the full chat, Ctrl-O shows the last output in full. /details turns them on again."
834
+ );
835
+ }
836
+ var RUN_AT_ONCE = /* @__PURE__ */ new Set([
837
+ "help",
838
+ "usage",
839
+ "session",
840
+ "hooks",
841
+ "languages",
842
+ "experts",
843
+ "audit",
844
+ "commands",
845
+ "agents",
846
+ "plan",
847
+ "build",
848
+ "undo",
849
+ "redo",
850
+ "init",
851
+ "editor",
852
+ "details",
853
+ "new",
854
+ "exit"
855
+ ]);
856
+ function paletteEntries(runtime) {
857
+ const builtins = [...HELP.matchAll(/^ {2}\/([a-z]+)(?: (?:X|\[dir\]))?\s+(.+)$/gm)].map((m) => ({
858
+ name: m[1] ?? "",
859
+ hint: m[2] ?? "",
860
+ runs: RUN_AT_ONCE.has(m[1] ?? "")
861
+ }));
862
+ const custom = runtime.commands.map((c) => ({
863
+ name: c.name,
864
+ hint: c.description ?? "custom command",
865
+ runs: false
866
+ }));
867
+ const skills = runtime.skills.filter((s) => s.userInvocable).map((s) => ({ name: s.name, hint: `skill: ${s.description}`, runs: false }));
868
+ return [...builtins, ...custom, ...skills];
869
+ }
870
+
871
+ // src/cli/chat/complete.ts
872
+ import { readdirSync, statSync } from "fs";
873
+ import { join, relative, resolve, sep } from "path";
874
+ var MAX_CANDIDATES = 30;
875
+ function complete(text, cursor, sources) {
876
+ const before = text.slice(0, cursor);
877
+ const start = before.search(/\S*$/);
878
+ const word = before.slice(start);
879
+ if (word.startsWith("/") && start === 0) {
880
+ return apply(text, cursor, start, "/", word.slice(1), sources.commands, " ");
881
+ }
882
+ if (word.startsWith("@")) {
883
+ const path = word.slice(1);
884
+ const cut = path.lastIndexOf("/") + 1;
885
+ const folder = path.slice(0, cut);
886
+ const prefix = path.slice(cut);
887
+ const entries = sources.list(folder, prefix.startsWith("."));
888
+ const exact = apply(text, cursor, start, `@${folder}`, prefix, entries, "");
889
+ if (exact !== void 0 || sources.files === void 0 || path === "") return exact;
890
+ const found = fuzzyFiles(path, sources.files(), MAX_FUZZY);
891
+ if (found.length === 0) return void 0;
892
+ if (found.length === 1) {
893
+ const insert = `@${found[0]}`;
894
+ return {
895
+ text: text.slice(0, start) + insert + text.slice(cursor),
896
+ cursor: start + insert.length,
897
+ candidates: []
898
+ };
899
+ }
900
+ return { text, cursor, candidates: found.map((f) => `@${f}`), lines: true };
901
+ }
902
+ if (before.startsWith("/") && start > 0 && sources.args !== void 0) {
903
+ const [head = "", ...rest] = before.slice(0, start).trim().split(/\s+/);
904
+ const choices = sources.args(head.slice(1), rest);
905
+ if (choices.length === 0) return void 0;
906
+ const hints = new Map(choices.map((c) => [c.value, c.hint]));
907
+ const result = apply(
908
+ text,
909
+ cursor,
910
+ start,
911
+ "",
912
+ word,
913
+ choices.map((c) => c.value),
914
+ " "
915
+ );
916
+ if (result === void 0 || result.candidates.length === 0 || hints.size === 0) return result;
917
+ if (![...hints.values()].some((h) => h !== void 0 && h !== "")) return result;
918
+ return {
919
+ ...result,
920
+ lines: true,
921
+ candidates: result.candidates.map((c) => {
922
+ const hint = hints.get(c);
923
+ return hint === void 0 || hint === "" ? c : `${c} ${hint}`;
924
+ })
925
+ };
926
+ }
927
+ return void 0;
928
+ }
929
+ function apply(text, cursor, start, lead, prefix, options, after) {
930
+ const matches = [...new Set(options.filter((o) => o.startsWith(prefix)))].sort();
931
+ if (matches.length === 0) return void 0;
932
+ const common = commonStart(matches);
933
+ const done = matches.length === 1 && !common.endsWith("/");
934
+ const insert = `${lead}${common}${done ? after : ""}`;
935
+ const next = text.slice(0, start) + insert + text.slice(cursor);
936
+ return {
937
+ text: next,
938
+ cursor: start + insert.length,
939
+ candidates: matches.length === 1 ? [] : [
940
+ ...matches.slice(0, MAX_CANDIDATES).map((m) => `${lead}${m}`),
941
+ ...matches.length > MAX_CANDIDATES ? [`\u2026 ${matches.length - MAX_CANDIDATES} more`] : []
942
+ ]
943
+ };
944
+ }
945
+ function commonStart(words) {
946
+ let out = words[0] ?? "";
947
+ for (const w of words) {
948
+ let i = 0;
949
+ while (i < out.length && i < w.length && out[i] === w[i]) i++;
950
+ out = out.slice(0, i);
951
+ }
952
+ return out;
953
+ }
954
+ function rootLister(root) {
955
+ return (folder, withHidden) => {
956
+ const dir = resolve(root, folder === "" ? "." : folder);
957
+ if (dir !== root && !dir.startsWith(`${root}${sep}`)) return [];
958
+ try {
959
+ return readdirSync(dir).filter((n) => n !== ".git" && n !== "node_modules" && (withHidden || !n.startsWith("."))).slice(0, 2e3).map((n) => {
960
+ try {
961
+ return statSync(join(dir, n)).isDirectory() ? `${n}/` : n;
962
+ } catch {
963
+ return n;
964
+ }
965
+ });
966
+ } catch {
967
+ return [];
968
+ }
969
+ };
970
+ }
971
+ var MAX_FUZZY = 10;
972
+ function fuzzyFiles(query, files, max) {
973
+ const q = query.toLowerCase();
974
+ const scored = [];
975
+ for (const file of files) {
976
+ const score = fuzzyScore(q, file);
977
+ if (score !== void 0) scored.push({ file, score });
978
+ }
979
+ return scored.sort(
980
+ (a, b) => b.score - a.score || a.file.length - b.file.length || (a.file < b.file ? -1 : 1)
981
+ ).slice(0, max).map((s) => s.file);
982
+ }
983
+ function fuzzyScore(query, file) {
984
+ const path = file.toLowerCase();
985
+ const nameStart = path.lastIndexOf("/") + 1;
986
+ const inName = match(query, path, nameStart);
987
+ if (inName !== void 0) {
988
+ const startsName = path[nameStart] === query[0] ? 8 : 0;
989
+ return inName.score + 20 + startsName - (path.length - nameStart) / 10;
990
+ }
991
+ return match(query, path, 0)?.score;
992
+ }
993
+ function match(query, path, from) {
994
+ let score = 0;
995
+ let at = from;
996
+ let last = -1;
997
+ for (const ch of query) {
998
+ const i = path.indexOf(ch, at);
999
+ if (i === -1) return void 0;
1000
+ score += 1;
1001
+ if (last >= 0) score += i === last + 1 ? 5 : -Math.min(i - last - 1, 10);
1002
+ const prev = path[i - 1];
1003
+ if (i === 0 || prev === "/" || prev === "-" || prev === "_" || prev === ".") score += 3;
1004
+ last = i;
1005
+ at = i + 1;
1006
+ }
1007
+ return { score };
1008
+ }
1009
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1010
+ ".git",
1011
+ "node_modules",
1012
+ ".garuda",
1013
+ "dist",
1014
+ "build",
1015
+ "target",
1016
+ ".venv",
1017
+ "venv",
1018
+ "__pycache__"
1019
+ ]);
1020
+ var MAX_FILES = 2e4;
1021
+ var FILES_TTL_MS = 1e4;
1022
+ function rootFiles(root, now = Date.now) {
1023
+ let cache;
1024
+ return () => {
1025
+ if (cache !== void 0 && now() - cache.at < FILES_TTL_MS) return cache.files;
1026
+ const files = [];
1027
+ const walk = (dir) => {
1028
+ let entries;
1029
+ try {
1030
+ entries = readdirSync(dir, { withFileTypes: true });
1031
+ } catch {
1032
+ return;
1033
+ }
1034
+ for (const entry of entries) {
1035
+ if (files.length >= MAX_FILES) return;
1036
+ if (entry.name.startsWith(".")) continue;
1037
+ const full = join(dir, entry.name);
1038
+ if (entry.isDirectory()) {
1039
+ if (!SKIP_DIRS.has(entry.name)) walk(full);
1040
+ } else if (entry.isFile()) {
1041
+ files.push(relative(root, full).split(sep).join("/"));
1042
+ }
1043
+ }
1044
+ };
1045
+ walk(root);
1046
+ cache = { at: now(), files };
1047
+ return files;
1048
+ };
1049
+ }
1050
+
1051
+ // src/cli/chat/controller.ts
1052
+ async function runChat(runtime, store, sessionPath, exitNow, notifier) {
1053
+ store.onToggleMode = () => {
1054
+ runtime.setMode(runtime.mode === "plan" ? "build" : "plan");
1055
+ return statusOf(runtime);
1056
+ };
1057
+ const list = rootLister(runtime.root);
1058
+ const args = new CommandArgs(runtime);
1059
+ const files = rootFiles(runtime.root);
1060
+ store.paletteSource = () => paletteEntries(runtime);
1061
+ store.completer = (text, cursor) => complete(text, cursor, {
1062
+ commands: commandNames(runtime),
1063
+ list,
1064
+ args: (command, before) => args.choices(command, before),
1065
+ files
1066
+ });
1067
+ for (; ; ) {
1068
+ void args.refresh();
1069
+ const input = await store.nextInput();
1070
+ if (input === void 0) return;
1071
+ const text = input.trim();
1072
+ let prompt = text;
1073
+ if (text.startsWith("!") && text.length > 1) {
1074
+ await runShell(runtime, store, text.slice(1).trim());
1075
+ continue;
1076
+ }
1077
+ if (text === "/editor") {
1078
+ store.openEditor();
1079
+ continue;
1080
+ }
1081
+ if (text.startsWith("/")) {
1082
+ const slow = /^\/compact(\s|$)/.test(text);
1083
+ const controller = new AbortController();
1084
+ if (slow) {
1085
+ store.begin(text);
1086
+ store.onInterrupt = () => controller.abort();
1087
+ } else store.echo(text);
1088
+ let result;
1089
+ try {
1090
+ result = await runCommand(text, {
1091
+ runtime,
1092
+ renderer: store,
1093
+ sessionPath,
1094
+ output: (shown, full) => {
1095
+ store.print(shown);
1096
+ if (full !== void 0) store.keepOutput(full.title, full.text);
1097
+ },
1098
+ signal: controller.signal
1099
+ });
1100
+ } catch (error) {
1101
+ result = "done";
1102
+ if (!controller.signal.aborted) store.error(describeError(error));
1103
+ } finally {
1104
+ if (slow) {
1105
+ store.onInterrupt = () => {
1106
+ };
1107
+ store.end(statusOf(runtime));
1108
+ }
1109
+ }
1110
+ store.refreshStatus(statusOf(runtime));
1111
+ if (result === "exit") return;
1112
+ if (result === "done") continue;
1113
+ prompt = result.prompt;
1114
+ }
1115
+ let display = text;
1116
+ for (; ; ) {
1117
+ const planning = runtime.mode === "plan";
1118
+ store.begin(display, prompt === display);
1119
+ let outcome;
1120
+ try {
1121
+ outcome = await runTurnInTerminal(runtime, store, store, prompt, exitNow, notifier);
1122
+ } finally {
1123
+ store.end(statusOf(runtime));
1124
+ }
1125
+ if (!planning || outcome.kind !== "done" || outcome.result.stopReason !== "done") break;
1126
+ const next = await planHandoff(runtime, store);
1127
+ store.refreshStatus(statusOf(runtime));
1128
+ if (next !== "now") break;
1129
+ prompt = BUILD_PROMPT;
1130
+ display = BUILD_PROMPT;
1131
+ }
1132
+ }
1133
+ }
1134
+ function commandNames(runtime) {
1135
+ return [
1136
+ ...BUILTIN_COMMANDS,
1137
+ ...runtime.commands.map((c) => c.name),
1138
+ ...runtime.skills.filter((s) => s.userInvocable).map((s) => s.name)
1139
+ ];
1140
+ }
1141
+ async function runShell(runtime, store, command) {
1142
+ const controller = new AbortController();
1143
+ store.begin(`!${command}`);
1144
+ store.onInterrupt = () => controller.abort();
1145
+ try {
1146
+ const result = await runtime.runUserCommand(command, controller.signal);
1147
+ store.print(result.text);
1148
+ store.info("The output goes to the model with your next message.");
1149
+ } catch (error) {
1150
+ store.warn(controller.signal.aborted ? "Command stopped." : error.message);
1151
+ } finally {
1152
+ store.onInterrupt = () => {
1153
+ };
1154
+ store.end(statusOf(runtime));
1155
+ }
1156
+ }
1157
+ function statusOf(runtime) {
1158
+ const session = runtime.session;
1159
+ const status = {
1160
+ model: runtime.modelId,
1161
+ sandbox: runtime.executor.isolation === "none" ? "no sandbox" : `sandbox ${runtime.executor.name}`,
1162
+ ...runtime.mode === "plan" ? { mode: "plan" } : {}
1163
+ };
1164
+ if (session !== void 0) {
1165
+ status.contextPercent = Math.round(
1166
+ session.contextTokens / runtime.limits.contextWindow * 100
1167
+ );
1168
+ if (session.costUsd !== void 0) status.costUsd = session.costUsd;
1169
+ }
1170
+ return status;
1171
+ }
1172
+ var CommandArgs = class {
1173
+ constructor(runtime) {
1174
+ this.runtime = runtime;
1175
+ }
1176
+ runtime;
1177
+ sessions = [];
1178
+ jobs = [];
1179
+ async refresh() {
1180
+ try {
1181
+ const { sessions } = await this.runtime.listSessions();
1182
+ this.sessions = sessions.map((s) => ({ value: s.id, hint: s.title }));
1183
+ const jobs = await listJobs(this.runtime.root);
1184
+ this.jobs = jobs.map((j) => ({ value: j.id, hint: `${j.status} \xB7 ${j.title}` }));
1185
+ } catch {
1186
+ }
1187
+ }
1188
+ choices(command, before) {
1189
+ const words = (...w) => w.map((value) => ({ value }));
1190
+ const [first, second] = before;
1191
+ switch (command) {
1192
+ case "models":
1193
+ if (first !== void 0) return [];
1194
+ return [
1195
+ ...words("opus", "sonnet", "haiku", "fable"),
1196
+ ...this.runtime.modelList().map((m) => ({ value: m.spec }))
1197
+ ];
1198
+ case "sessions":
1199
+ if (first === void 0) return [...words("rename", "delete"), ...this.sessions];
1200
+ return (first === "rename" || first === "delete") && second === void 0 ? this.sessions : [];
1201
+ case "jobs":
1202
+ if (first === void 0) return [...words("cancel", "delete"), ...this.jobs];
1203
+ return (first === "cancel" || first === "delete") && second === void 0 ? this.jobs : [];
1204
+ case "diff":
1205
+ return first === void 0 ? words("last") : [];
1206
+ case "mcp":
1207
+ if (first === void 0) return words("logout");
1208
+ return first === "logout" && second === void 0 ? this.runtime.mcpStatus().map((s) => ({ value: s.name })) : [];
1209
+ case "details":
1210
+ return first === void 0 ? words("on", "off") : [];
1211
+ case "thinking":
1212
+ return first === void 0 ? words(...THINKING_WORDS) : [];
1213
+ case "search":
1214
+ if (first === void 0) return words("claude", "provider", "off", "default");
1215
+ return first === "default" && second === void 0 ? words("claude", "provider", "off") : [];
1216
+ case "lsp":
1217
+ if (first === void 0) return words("install");
1218
+ return first === "install" && second === void 0 ? words(...LSP_LANGUAGES) : [];
1219
+ default:
1220
+ return [];
1221
+ }
1222
+ }
1223
+ };
1224
+
1225
+ export {
1226
+ describeError,
1227
+ FOCUS_REPORTING_ON,
1228
+ FOCUS_REPORTING_OFF,
1229
+ reportsFocus,
1230
+ FocusTracker,
1231
+ focusEvent,
1232
+ formatTokens,
1233
+ stopMessage,
1234
+ BUILD_PROMPT,
1235
+ planHandoff,
1236
+ runCommand,
1237
+ complete,
1238
+ rootLister,
1239
+ fuzzyFiles,
1240
+ rootFiles,
1241
+ runTurnInTerminal,
1242
+ runChat,
1243
+ commandNames,
1244
+ statusOf,
1245
+ CommandArgs
1246
+ };
1247
+ //# sourceMappingURL=chunk-PQNT34LY.js.map