@garuda-agent/garuda 0.17.0 → 0.18.1

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 (64) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +3 -2
  3. package/SECURITY.md +1 -1
  4. package/dist/{acpCommand-7EY3NHPG.js → acpCommand-JAYNMIMP.js} +19 -16
  5. package/dist/advise-HDKJGNBL.js +195 -0
  6. package/dist/brief-RWKESOPT.js +91 -0
  7. package/dist/buildCheck-HB46NWLX.js +19 -0
  8. package/dist/buildInfo-K73FFI5S.js +8 -0
  9. package/dist/chunk-2OYTNNUJ.js +216 -0
  10. package/dist/{chunk-Z66BWTMD.js → chunk-34WBDQBL.js} +135 -78
  11. package/dist/chunk-4QRTTRVK.js +88 -0
  12. package/dist/{chunk-2DYMJMRQ.js → chunk-5UDYQ5MK.js} +405 -70
  13. package/dist/{chunk-J4346IDH.js → chunk-7KBOIPUT.js} +2 -1
  14. package/dist/chunk-CWVXOSAA.js +894 -0
  15. package/dist/{chunk-FARS4R67.js → chunk-DQSCCE77.js} +18 -1
  16. package/dist/chunk-DUYLVGPY.js +821 -0
  17. package/dist/chunk-GKU62MHQ.js +5258 -0
  18. package/dist/chunk-H7LIBGZT.js +9 -0
  19. package/dist/chunk-IKPGHM54.js +334 -0
  20. package/dist/chunk-IYGJWX65.js +9 -0
  21. package/dist/{chunk-U65DQZDB.js → chunk-JMH5W5DG.js} +2 -45
  22. package/dist/chunk-L55VUSX3.js +54 -0
  23. package/dist/{chunk-B4SJY7KT.js → chunk-LMKGSYJC.js} +77 -8
  24. package/dist/{chunk-23FXBNBB.js → chunk-MNE7HHX2.js} +2 -1
  25. package/dist/chunk-NH72AELP.js +48 -0
  26. package/dist/{chunk-735BKABA.js → chunk-NLS7GMF7.js} +12 -106
  27. package/dist/{chunk-ZAE25JM2.js → chunk-QANRZW7W.js} +8 -1
  28. package/dist/{chunk-JRVWH27M.js → chunk-QLBCM2YQ.js} +7 -3
  29. package/dist/{chunk-6XHON3Q6.js → chunk-QXTHEFKP.js} +3 -3
  30. package/dist/{chunk-TJIWD7R7.js → chunk-R7RCW4BQ.js} +10 -8
  31. package/dist/chunk-TLA2PDGM.js +156 -0
  32. package/dist/{chunk-5B5HDTUR.js → chunk-TWQ7V2L6.js} +2 -2
  33. package/dist/chunk-ULAJPMY7.js +107 -0
  34. package/dist/chunk-VISKX36R.js +306 -0
  35. package/dist/{chunk-55CFEEIN.js → chunk-WK5NE6RY.js} +2 -2
  36. package/dist/chunk-Y62J7PTD.js +90 -0
  37. package/dist/chunk-YAQ6QLKB.js +40 -0
  38. package/dist/{chunk-7TH3NHCQ.js → chunk-ZKT4P2PC.js} +2 -2
  39. package/dist/cli/index.js +289 -65
  40. package/dist/config-WV2H64TF.js +23 -0
  41. package/dist/{create-W6BV75QX.js → create-OACSGJ37.js} +5 -5
  42. package/dist/evalCommand-BLJJ4G76.js +696 -0
  43. package/dist/evalTune-W53KAD4V.js +302 -0
  44. package/dist/{export-SJ3DUP6W.js → export-E677HIQS.js} +27 -4
  45. package/dist/exportHtml-BMV73YRO.js +21 -0
  46. package/dist/{inkChat-FNUBK43U.js → inkChat-EVLYDC3K.js} +263 -47
  47. package/dist/{install-TKWN5AYH.js → install-4ST5BNRG.js} +3 -3
  48. package/dist/{jobCommand-FE6XN3YD.js → jobCommand-ILOTYOX3.js} +9 -8
  49. package/dist/{launchd-3A3ONEW5.js → launchd-QG6R5RAK.js} +4 -4
  50. package/dist/{lspCommand-ZZ6R4PA2.js → lspCommand-X4FTTONR.js} +3 -3
  51. package/dist/{manager-73MPYFTP.js → manager-L25IUXHW.js} +11 -11
  52. package/dist/{nightCommand-OODJNWDT.js → nightCommand-XPWT6CQZ.js} +9 -8
  53. package/dist/receiptView-YFDHV253.js +19 -0
  54. package/dist/{remove-42WQNVPX.js → remove-STX5UO7X.js} +3 -3
  55. package/dist/{run-ML6GSPN4.js → run-W4TK52WT.js} +6 -6
  56. package/dist/{server-VEYIJHEU.js → server-ASM73QVY.js} +3 -3
  57. package/dist/{setupCommand-GBQIVYJC.js → setupCommand-OSOMFHDQ.js} +10 -9
  58. package/dist/tour-CEVMAC5S.js +18 -0
  59. package/dist/tune-VX2WQTHS.js +52 -0
  60. package/dist/uiState-YFJQ43D7.js +16 -0
  61. package/package.json +1 -1
  62. package/dist/chunk-4JSUUDFQ.js +0 -118
  63. package/dist/chunk-BSK5T2ZG.js +0 -9
  64. package/dist/evalCommand-YVQAWUZE.js +0 -2833
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BUILD_COMMIT
4
+ } from "./chunk-H7LIBGZT.js";
5
+ export {
6
+ BUILD_COMMIT
7
+ };
8
+ //# sourceMappingURL=buildInfo-K73FFI5S.js.map
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ budgetSpent,
4
+ isCostIncomplete,
5
+ pendingChildTokens
6
+ } from "./chunk-NLS7GMF7.js";
7
+ import {
8
+ totalTokens
9
+ } from "./chunk-7KBOIPUT.js";
10
+
11
+ // src/cli/report.ts
12
+ function formatTokens(n) {
13
+ if (n < 1e3) return String(n);
14
+ if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
15
+ return `${(n / 1e6).toFixed(1)}M`;
16
+ }
17
+ function formatUsd(usd, incomplete = false) {
18
+ if (usd === void 0) return "cost unknown";
19
+ const amount = usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
20
+ return incomplete ? `\u2265 ${amount} (incomplete)` : amount;
21
+ }
22
+ function budgetNote(session) {
23
+ const unreported = session.unreportedTokens;
24
+ const children = pendingChildTokens(session);
25
+ if (unreported <= 0 && children <= 0) return void 0;
26
+ const parts = [
27
+ ...unreported > 0 ? [`${formatTokens(unreported)} reserved without reported usage`] : [],
28
+ ...children > 0 ? [`${formatTokens(children)} from subagents with no report`] : []
29
+ ];
30
+ return `budget ${formatTokens(budgetSpent(session))} tokens (${parts.join(", ")})`;
31
+ }
32
+ function usageLine(result, session, runCostUsd, contextWindow, runCostIncomplete = false) {
33
+ const u = result.usage;
34
+ const input = u.inputTokens + u.cacheReadTokens + u.cacheWriteTokens;
35
+ const percent = Math.round(session.contextTokens / contextWindow * 100);
36
+ return [
37
+ `${result.stopReason} \xB7 ${result.steps} step(s)`,
38
+ `${budgetNote(session) !== void 0 ? "reported " : ""}${formatTokens(input)} in (${formatTokens(u.cacheReadTokens)} cached) / ${formatTokens(u.outputTokens)} out`,
39
+ formatUsd(runCostUsd, runCostIncomplete),
40
+ `context ${percent}% of ${formatTokens(contextWindow)}`,
41
+ `session ${formatTokens(totalTokens(session.usage))} tokens, ${formatUsd(session.costUsd, isCostIncomplete(session))}`,
42
+ ...budgetNote(session) === void 0 ? [] : [budgetNote(session)]
43
+ ].join(" \xB7 ");
44
+ }
45
+ function stopMessage(reason, limits) {
46
+ switch (reason) {
47
+ case "done":
48
+ return void 0;
49
+ case "max_steps":
50
+ return `Stopped: the run reached the step limit (${limits.maxSteps}). Use --resume to continue, or raise limits.maxSteps in .garuda/settings.json.`;
51
+ case "token_budget":
52
+ return `Stopped: the session used its token budget (${formatTokens(limits.tokenBudget)} tokens). Raise limits.tokenBudget in .garuda/settings.json to continue.`;
53
+ case "repeated_calls":
54
+ 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.";
55
+ case "max_tokens":
56
+ return "Stopped: the response reached the output token limit.";
57
+ case "refusal":
58
+ return "Stopped: the model refused the request.";
59
+ }
60
+ }
61
+ function chatStopMessage(reason, limits) {
62
+ switch (reason) {
63
+ case "max_steps":
64
+ return `\u25A0 Stopped at the step limit (${limits.maxSteps}). The task is not finished.`;
65
+ case "repeated_calls":
66
+ return "\u25A0 Stopped: the agent made the same tool call 3 times in a row, so it may be stuck. Type a hint as your next message, and it goes on from there.";
67
+ default:
68
+ return stopMessage(reason, limits);
69
+ }
70
+ }
71
+
72
+ // src/cli/receiptView.ts
73
+ function screenReaderMode(env = process.env) {
74
+ return env.GARUDA_SCREEN_READER === "1";
75
+ }
76
+ var RECEIPT_BOX_MIN_COLUMNS = 60;
77
+ var RECEIPT_MAX_WIDTH = 76;
78
+ var seconds = (ms) => `${Math.max(0, Math.round(ms / 1e3))} s`;
79
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
80
+ function cells(text) {
81
+ let n = 0;
82
+ for (const ch of text) {
83
+ const c = ch.codePointAt(0) ?? 0;
84
+ const wide = c >= 4352 && c <= 4447 || c >= 11904 && c <= 42191 || c >= 44032 && c <= 55203 || c >= 63744 && c <= 64255 || c >= 65072 && c <= 65103 || c >= 65280 && c <= 65376 || c >= 65504 && c <= 65510 || c >= 127744 && c <= 129791 || c >= 131072 && c <= 262141;
85
+ n += wide ? 2 : 1;
86
+ }
87
+ return n;
88
+ }
89
+ function cut(text, width) {
90
+ if (cells(text) <= width) return text;
91
+ let out = "";
92
+ for (const ch of text) {
93
+ if (cells(out + ch) > width - 1) break;
94
+ out += ch;
95
+ }
96
+ return `${out}\u2026`;
97
+ }
98
+ function filesParts(receipt, room) {
99
+ const files = receipt.files;
100
+ if (files === void 0) return [["undo is off: no file list", "dim"]];
101
+ if (files.length === 0) return [["no file changed", "dim"]];
102
+ const shown = [];
103
+ for (const [i, f] of files.entries()) {
104
+ const lines = f.added === void 0 ? " (binary)" : ` +${f.added} \u2212${f.removed ?? 0}`;
105
+ const item = `${f.path}${lines}`;
106
+ const rest = files.length - i - 1;
107
+ const tail = rest > 0 ? ` \xB7 ${rest} more` : "";
108
+ const next = [...shown, item].join(" \xB7 ");
109
+ if (shown.length > 0 && cells(next + tail) > room) {
110
+ shown.push(`${files.length - i} more`);
111
+ return [[shown.join(" \xB7 ")]];
112
+ }
113
+ shown.push(item);
114
+ }
115
+ return [[shown.join(" \xB7 ")]];
116
+ }
117
+ function testsParts(receipt) {
118
+ const t = receipt.tests;
119
+ if (t === void 0) return void 0;
120
+ const count = t.total === void 0 ? "" : t.passed ? ` (${plural(t.total, "test")}${t.previousTotal !== void 0 && t.previousTotal !== t.total ? `, was ${t.previousTotal}` : ""})` : (t.failed ?? 0) > 0 ? ` (${t.failed} of ${t.total})` : ` (${plural(t.total, "test")})`;
121
+ return t.passed ? [[`${t.command} `], ["\u2713 passed", "green"], [count]] : [[`${t.command} `], [t.timedOut === true ? "\u2717 timed out" : "\u2717 failed", "red"], [count]];
122
+ }
123
+ function sandboxParts(receipt) {
124
+ const c = receipt.commands;
125
+ if (c.sandboxed + c.outside === 0) return void 0;
126
+ const parts = [
127
+ [`\u26E8 ${plural(c.sandboxed, "command")} in the sandbox \xB7 `],
128
+ c.outside > 0 ? [`${c.outside} outside`, "yellow"] : [`${c.outside} outside`]
129
+ ];
130
+ return parts;
131
+ }
132
+ function rows(receipt, room) {
133
+ const out = [{ label: "Files", parts: filesParts(receipt, room) }];
134
+ const tests = testsParts(receipt);
135
+ if (tests !== void 0) out.push({ label: "Tests", parts: tests });
136
+ const sandbox = sandboxParts(receipt);
137
+ if (sandbox !== void 0) out.push({ label: "Sandbox", parts: sandbox });
138
+ const hosts = receipt.commands.blockedHosts;
139
+ if (hosts.length > 0) {
140
+ out.push({ label: "Network", parts: [[`blocked ${hosts.join(", ")}`, "yellow"]] });
141
+ }
142
+ const changed = receipt.files?.length ?? 0;
143
+ if (changed > 0)
144
+ out.push({ label: "Undo", parts: [[`/undo puts back ${plural(changed, "file")}`]] });
145
+ return out;
146
+ }
147
+ function headText(receipt) {
148
+ return `Turn ${receipt.turn} \xB7 ${receipt.stopReason} \xB7 ${plural(receipt.steps, "step")} \xB7 ${seconds(receipt.durationMs)}`;
149
+ }
150
+ function footText(receipt) {
151
+ const parts = [`context ${receipt.contextPercent}%`];
152
+ if (receipt.sessionCostUsd !== void 0) {
153
+ parts.push(
154
+ `session ${formatUsd(receipt.sessionCostUsd, receipt.sessionCostIncomplete === true)}`
155
+ );
156
+ }
157
+ const kept = receipt.keptChanges ?? 0;
158
+ if (kept > 0) {
159
+ parts.push(plural(kept, "kept change"));
160
+ if (receipt.sessionCostUsd !== void 0) {
161
+ const each = formatUsd(receipt.sessionCostUsd / kept, receipt.sessionCostIncomplete === true);
162
+ parts.push(`${each} each`);
163
+ }
164
+ }
165
+ return parts.join(" \xB7 ");
166
+ }
167
+ function receiptText(receipt, options = {}) {
168
+ const paint = options.paint ?? ((_style, text) => text);
169
+ const draw = (parts) => parts.map(([text, style]) => style === void 0 ? text : paint(style, text)).join("");
170
+ const cost = formatUsd(receipt.costUsd, receipt.costIncomplete === true);
171
+ const columns = options.columns;
172
+ if (options.screenReader === true || columns === void 0 || columns < RECEIPT_BOX_MIN_COLUMNS) {
173
+ const lines = [`${headText(receipt)} \xB7 ${cost}`];
174
+ for (const row of rows(receipt, 200)) lines.push(`${row.label}: ${draw(row.parts)}`);
175
+ lines.push(footText(receipt));
176
+ return lines.join("\n");
177
+ }
178
+ const width = Math.min(columns, RECEIPT_MAX_WIDTH);
179
+ const label = 9;
180
+ const room = width - 4 - label;
181
+ const border = (text) => paint("yellow", text);
182
+ const edge = (left, text, right, end) => {
183
+ const tail = right === "" ? "" : ` ${right} \u2500`;
184
+ const head = cut(text, width - 4 - cells(tail) - cells(end) - 1);
185
+ const fill = width - 3 - cells(head) - 1 - cells(tail) - cells(end);
186
+ return `${border(`${left}\u2500 `)}${head} ${border("\u2500".repeat(Math.max(1, fill)))}${right === "" ? "" : ` ${right} ${border("\u2500")}`}${border(end)}`;
187
+ };
188
+ const out = [edge("\u256D", headText(receipt), cost, "\u256E")];
189
+ for (const row of rows(receipt, room)) {
190
+ const plain = row.parts.map(([text]) => text).join("");
191
+ let parts = row.parts;
192
+ if (cells(plain) > room) parts = [[cut(plain, room)]];
193
+ const shown = parts.map(([text]) => text).join("");
194
+ const pad = " ".repeat(Math.max(0, room - cells(shown)));
195
+ out.push(
196
+ `${border("\u2502")} ${paint("dim", row.label.padEnd(label))}${draw(parts)}${pad} ${border("\u2502")}`
197
+ );
198
+ }
199
+ out.push(edge("\u2570", footText(receipt), "", "\u256F"));
200
+ return out.join("\n");
201
+ }
202
+
203
+ export {
204
+ formatTokens,
205
+ formatUsd,
206
+ budgetNote,
207
+ usageLine,
208
+ stopMessage,
209
+ chatStopMessage,
210
+ screenReaderMode,
211
+ RECEIPT_BOX_MIN_COLUMNS,
212
+ RECEIPT_MAX_WIDTH,
213
+ cells,
214
+ receiptText
215
+ };
216
+ //# sourceMappingURL=chunk-2OYTNNUJ.js.map
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- colorDiff
4
- } from "./chunk-4JSUUDFQ.js";
5
2
  import {
6
3
  AuditDirError,
7
4
  SPECIALIST_SPECS,
@@ -18,19 +15,35 @@ import {
18
15
  repoMapText,
19
16
  repoMapTool,
20
17
  verifyAuditDir
21
- } from "./chunk-2DYMJMRQ.js";
18
+ } from "./chunk-5UDYQ5MK.js";
19
+ import {
20
+ colorDiff
21
+ } from "./chunk-VISKX36R.js";
22
+ import {
23
+ hasReceipt
24
+ } from "./chunk-4QRTTRVK.js";
25
+ import {
26
+ budgetNote,
27
+ chatStopMessage,
28
+ formatTokens,
29
+ formatUsd,
30
+ stopMessage,
31
+ usageLine
32
+ } from "./chunk-2OYTNNUJ.js";
22
33
  import {
23
- budgetSpent,
24
34
  isCostIncomplete,
25
35
  pendingChildTokens
26
- } from "./chunk-735BKABA.js";
36
+ } from "./chunk-NLS7GMF7.js";
27
37
  import {
28
38
  WEB_SEARCH_USD,
29
39
  totalTokens
30
- } from "./chunk-J4346IDH.js";
40
+ } from "./chunk-7KBOIPUT.js";
41
+ import {
42
+ tourChoice
43
+ } from "./chunk-Y62J7PTD.js";
31
44
  import {
32
45
  BUILTIN_COMMANDS
33
- } from "./chunk-23FXBNBB.js";
46
+ } from "./chunk-MNE7HHX2.js";
34
47
  import {
35
48
  LSP_LANGUAGES
36
49
  } from "./chunk-EU7PA45O.js";
@@ -76,70 +89,19 @@ function focusEvent(input) {
76
89
  return void 0;
77
90
  }
78
91
 
79
- // src/cli/report.ts
80
- function formatTokens(n) {
81
- if (n < 1e3) return String(n);
82
- if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
83
- return `${(n / 1e6).toFixed(1)}M`;
84
- }
85
- function formatUsd(usd, incomplete = false) {
86
- if (usd === void 0) return "cost unknown";
87
- const amount = usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
88
- return incomplete ? `\u2265 ${amount} (incomplete)` : amount;
89
- }
90
- function budgetNote(session) {
91
- const unreported = session.unreportedTokens;
92
- const children = pendingChildTokens(session);
93
- if (unreported <= 0 && children <= 0) return void 0;
94
- const parts = [
95
- ...unreported > 0 ? [`${formatTokens(unreported)} reserved without reported usage`] : [],
96
- ...children > 0 ? [`${formatTokens(children)} from subagents with no report`] : []
97
- ];
98
- return `budget ${formatTokens(budgetSpent(session))} tokens (${parts.join(", ")})`;
99
- }
100
- function usageLine(result, session, runCostUsd, contextWindow, runCostIncomplete = false) {
101
- const u = result.usage;
102
- const input = u.inputTokens + u.cacheReadTokens + u.cacheWriteTokens;
103
- const percent = Math.round(session.contextTokens / contextWindow * 100);
104
- return [
105
- `${result.stopReason} \xB7 ${result.steps} step(s)`,
106
- `${budgetNote(session) !== void 0 ? "reported " : ""}${formatTokens(input)} in (${formatTokens(u.cacheReadTokens)} cached) / ${formatTokens(u.outputTokens)} out`,
107
- formatUsd(runCostUsd, runCostIncomplete),
108
- `context ${percent}% of ${formatTokens(contextWindow)}`,
109
- `session ${formatTokens(totalTokens(session.usage))} tokens, ${formatUsd(session.costUsd, isCostIncomplete(session))}`,
110
- ...budgetNote(session) === void 0 ? [] : [budgetNote(session)]
111
- ].join(" \xB7 ");
112
- }
113
- function stopMessage(reason, limits) {
114
- switch (reason) {
115
- case "done":
116
- return void 0;
117
- case "max_steps":
118
- return `Stopped: the run reached the step limit (${limits.maxSteps}). Use --resume to continue, or raise limits.maxSteps in .garuda/settings.json.`;
119
- case "token_budget":
120
- return `Stopped: the session used its token budget (${formatTokens(limits.tokenBudget)} tokens). Raise limits.tokenBudget in .garuda/settings.json to continue.`;
121
- case "repeated_calls":
122
- 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.";
123
- case "max_tokens":
124
- return "Stopped: the response reached the output token limit.";
125
- case "refusal":
126
- return "Stopped: the model refused the request.";
127
- }
128
- }
129
-
130
92
  // src/cli/turn.ts
131
- async function runTurnInTerminal(runtime, approver, renderer, prompt, exitNow, watcher) {
93
+ async function runTurnInTerminal(runtime, approver, renderer, prompt, exitNow, watcher, options = {}) {
132
94
  const started = performance.now();
133
95
  watcher?.turnStarted();
134
96
  let outcome;
135
97
  try {
136
- outcome = await runTurn(runtime, approver, renderer, prompt, exitNow);
98
+ outcome = await runTurn(runtime, approver, renderer, prompt, exitNow, options);
137
99
  return outcome;
138
100
  } finally {
139
101
  watcher?.turnEnded(outcome, performance.now() - started);
140
102
  }
141
103
  }
142
- async function runTurn(runtime, approver, renderer, prompt, exitNow) {
104
+ async function runTurn(runtime, approver, renderer, prompt, exitNow, options) {
143
105
  const controller = new AbortController();
144
106
  const stop = () => {
145
107
  if (controller.signal.aborted) exitNow();
@@ -158,12 +120,20 @@ async function runTurn(runtime, approver, renderer, prompt, exitNow) {
158
120
  if (session !== void 0) {
159
121
  const runCost = session.costUsd === void 0 ? void 0 : session.costUsd - costBefore;
160
122
  const runIncomplete = session.unreportedTokens > unreportedBefore || pendingChildTokens(session) > pendingBefore || session.costIncomplete && !incompleteBefore;
161
- renderer.info(
162
- `
123
+ const receipt = runtime.lastReceipt;
124
+ if (receipt !== void 0 && hasReceipt(receipt) && renderer.receipt !== void 0) {
125
+ renderer.receipt(receipt);
126
+ } else {
127
+ renderer.info(
128
+ `
163
129
  [${usageLine(result, session, runCost, runtime.limits.contextWindow, runIncomplete)}]`
164
- );
130
+ );
131
+ }
165
132
  }
166
- const message = stopMessage(result.stopReason, runtime.limits);
133
+ const message = (options.chat === true ? chatStopMessage : stopMessage)(
134
+ result.stopReason,
135
+ runtime.limits
136
+ );
167
137
  if (message !== void 0) renderer.warn(message);
168
138
  return { kind: "done", result };
169
139
  } catch (error) {
@@ -208,6 +178,34 @@ function modeText(runtime) {
208
178
  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.";
209
179
  }
210
180
 
181
+ // src/cli/chat/rewind.ts
182
+ function ago(at, now) {
183
+ if (at === void 0) return "";
184
+ const minutes = Math.floor(Math.max(0, now - at) / 6e4);
185
+ if (minutes < 1) return "<1 min";
186
+ if (minutes < 60) return `${minutes} min`;
187
+ const hours = Math.floor(minutes / 60);
188
+ return hours < 48 ? `${hours} h` : `${Math.floor(hours / 24)} d`;
189
+ }
190
+ function rewindList(points, now = Date.now()) {
191
+ if (points.length === 0) return "There is no turn to rewind to yet.";
192
+ const width = Math.min(40, Math.max(...points.map((p) => p.prompt.length + 2)));
193
+ const rows = points.map((p, i) => {
194
+ const prompt = `"${p.prompt}"`;
195
+ const shown = prompt.length > width ? `${prompt.slice(0, width - 2)}\u2026"` : prompt;
196
+ const files = p.changedFiles === void 0 ? "" : p.changedFiles === 0 ? "no files" : `${p.changedFiles} file${p.changedFiles === 1 ? "" : "s"}`;
197
+ const cost = p.costUsd === void 0 ? "" : formatUsd(p.costUsd);
198
+ const note = p.conversation ? "" : " (before a compaction: files only)";
199
+ return ` ${String(i + 1).padStart(3)} ${shown.padEnd(width)} ${files.padEnd(8)} ${cost.padEnd(8)} ${ago(p.at, now)}${note}`.trimEnd();
200
+ }).reverse();
201
+ return [
202
+ "Rewind to the start of a turn (newest first):",
203
+ ...rows,
204
+ "/rewind <n> goes back to the start of turn n (/redo brings the turns back).",
205
+ "/rewind fork <n> starts a new session from there; this one stays as it is."
206
+ ].join("\n");
207
+ }
208
+
211
209
  // src/cli/chat/commands.ts
212
210
  var HELP = [
213
211
  "Type a task and press Enter. @path attaches a file or folder, !command runs a command, Tab completes.",
@@ -217,7 +215,7 @@ var HELP = [
217
215
  " /session the session id and file",
218
216
  " /sessions this project's sessions; /sessions <n|id> continues one; rename, delete",
219
217
  " /models the models; /models <n|id|opus|sonnet|haiku> switches for this chat",
220
- " /export write this conversation as Markdown; /export <file>",
218
+ " /export write this conversation as Markdown; /export <file>; /export --html [file] as one page",
221
219
  " /compact summarise the older turns now; /compact <what to keep>",
222
220
  " /diff file changes in this session; /diff last (last turn); /diff [last] <path>",
223
221
  " /schedule make the last plan a job that runs later: /schedule [HH:MM], then garuda run <id>",
@@ -240,6 +238,7 @@ var HELP = [
240
238
  " /build build mode: change files and run commands (the default); /build <task> runs it",
241
239
  " /undo take back the last turn: its file changes and its messages",
242
240
  " /redo bring back the last undone turn",
241
+ " /rewind the turns of this session; /rewind <n> goes back to turn n, /rewind fork <n> forks",
243
242
  " /init set up this folder: AGENTS.md, and files from other agents (Claude Code, OpenCode \u2026)",
244
243
  " /editor write the next prompt in $VISUAL or $EDITOR (also Ctrl-G)",
245
244
  " /details show or hide the result line under each tool call; /details on|off",
@@ -362,6 +361,18 @@ async function runCommand(text, { runtime, renderer, sessionPath, output, signal
362
361
  await expertsCommand(runtime, renderer);
363
362
  } else if (command === "/audit") {
364
363
  await auditCommand(runtime, renderer, text.slice(command.length).trim());
364
+ } else if (command === "/rewind") {
365
+ const args = text.slice(command.length).trim().split(/\s+/).filter((a) => a !== "");
366
+ const signal2 = new AbortController().signal;
367
+ const fork = args[0] === "fork";
368
+ const number = fork ? args[1] : args[0];
369
+ if (number === void 0 && !fork) renderer.info(rewindList(runtime.rewindPoints));
370
+ else if (number === void 0 || !/^\d+$/.test(number) || args.length > (fork ? 2 : 1)) {
371
+ renderer.info("Use: /rewind, /rewind <n> or /rewind fork <n>.");
372
+ } else {
373
+ const turn = Number(number);
374
+ renderer.info(await (fork ? runtime.fork(turn, signal2) : runtime.rewind(turn, signal2)));
375
+ }
365
376
  } else if (command === "/undo" || command === "/redo") {
366
377
  const signal2 = new AbortController().signal;
367
378
  renderer.info(await (command === "/undo" ? runtime.undo(signal2) : runtime.redo(signal2)));
@@ -484,10 +495,10 @@ async function diffCommand(runtime, renderer, arg, output) {
484
495
  `Changes${where} ${since} (${result.files.length} file${result.files.length === 1 ? "" : "s"}, +${plus} \u2212${minus}). Changes you made yourself count too.`,
485
496
  ...rows,
486
497
  "",
487
- colorDiff(lines.slice(0, DIFF_LINES).join("\n")),
498
+ colorDiff(lines.slice(0, DIFF_LINES).join("\n"), process.stdout.columns),
488
499
  ...cut ? [`\u2026 ${lines.length - DIFF_LINES} more lines. ${more}`] : []
489
500
  ].join("\n"),
490
- cut ? { title: `diff ${since}${where}`, text: colorDiff(result.patch) } : void 0
501
+ cut ? { title: `diff ${since}${where}`, text: colorDiff(result.patch, process.stdout.columns) } : void 0
491
502
  );
492
503
  }
493
504
  async function jobsCommand(runtime, renderer, id) {
@@ -513,7 +524,7 @@ async function jobsCommand(runtime, renderer, id) {
513
524
  return;
514
525
  }
515
526
  const jobs = await listJobs2(runtime.root);
516
- const { defaultAgentEnv, nightAgentTime } = await import("./launchd-3A3ONEW5.js");
527
+ const { defaultAgentEnv, nightAgentTime } = await import("./launchd-QG6R5RAK.js");
517
528
  const night = nightAgentTime(runtime.root, defaultAgentEnv());
518
529
  const nightLine = night === void 0 ? [] : [
519
530
  `Night shift: launchd starts the queue at ${night.toTimeString().slice(0, 5)} on ${night.toDateString()} (/jobs cancel night removes it).`
@@ -551,8 +562,11 @@ async function exportCommand(runtime, renderer, file) {
551
562
  renderer.info("There is no conversation to export yet.");
552
563
  return;
553
564
  }
554
- const { sessionMarkdown, writeExport } = await import("./export-SJ3DUP6W.js");
555
- const result = await writeExport(runtime.root, id, sessionMarkdown(records, id), file);
565
+ const { sessionMarkdown, writeExport } = await import("./export-E677HIQS.js");
566
+ const html = /^--html(\s|$)/.test(file);
567
+ const name = html ? file.slice("--html".length).trim() : file;
568
+ const text = html ? (await import("./exportHtml-BMV73YRO.js")).sessionHtml(records, id) : sessionMarkdown(records, id);
569
+ const result = await writeExport(runtime.root, id, text, name, html ? ".html" : ".md");
556
570
  if ("problem" in result) renderer.warn(result.problem);
557
571
  else
558
572
  renderer.info(
@@ -896,6 +910,7 @@ var RUN_AT_ONCE = /* @__PURE__ */ new Set([
896
910
  "build",
897
911
  "undo",
898
912
  "redo",
913
+ "rewind",
899
914
  "init",
900
915
  "editor",
901
916
  "details",
@@ -1097,8 +1112,30 @@ function rootFiles(root, now = Date.now) {
1097
1112
  };
1098
1113
  }
1099
1114
 
1115
+ // src/cli/chat/stop.ts
1116
+ var CONTINUE_PROMPT = "Go on with the task from where you stopped. You reached the step limit; the conversation above has your work so far.";
1117
+ var SUMMARY_PROMPT = "Stop the work here and do not call tools. Say what is done, what is left, and the next step.";
1118
+ async function stopHandoff(approver, maxSteps, isolation, signal = new AbortController().signal) {
1119
+ const request = {
1120
+ tool: "step_limit",
1121
+ target: { kind: "input", json: "{}" },
1122
+ preview: `The turn used its ${maxSteps} steps. The next turn gets ${maxSteps} more.`,
1123
+ isolation,
1124
+ title: "The task is not finished.",
1125
+ question: "Go on?",
1126
+ labels: {
1127
+ once: `Go on for ${maxSteps} more steps`,
1128
+ session: "Summarise what is done and what is left",
1129
+ deny: "Stop here"
1130
+ }
1131
+ };
1132
+ const choice = await approver.ask(request, signal);
1133
+ return choice === "once" ? "more" : choice === "session" ? "summary" : "stop";
1134
+ }
1135
+
1100
1136
  // src/cli/chat/controller.ts
1101
- async function runChat(runtime, store, sessionPath, exitNow, notifier) {
1137
+ async function runChat(runtime, store, sessionPath, exitNow, notifier, tour) {
1138
+ let tourTasks = tour;
1102
1139
  store.onToggleMode = () => {
1103
1140
  runtime.setMode(runtime.mode === "plan" ? "build" : "plan");
1104
1141
  return statusOf(runtime);
@@ -1117,7 +1154,8 @@ async function runChat(runtime, store, sessionPath, exitNow, notifier) {
1117
1154
  void args.refresh();
1118
1155
  const input = await store.nextInput();
1119
1156
  if (input === void 0) return;
1120
- const text = input.trim();
1157
+ const text = tourChoice(input.trim(), tourTasks);
1158
+ tourTasks = void 0;
1121
1159
  let prompt = text;
1122
1160
  if (text.startsWith("!") && text.length > 1) {
1123
1161
  await runShell(runtime, store, text.slice(1).trim());
@@ -1167,10 +1205,20 @@ async function runChat(runtime, store, sessionPath, exitNow, notifier) {
1167
1205
  store.begin(display, prompt === display);
1168
1206
  let outcome;
1169
1207
  try {
1170
- outcome = await runTurnInTerminal(runtime, store, store, prompt, exitNow, notifier);
1208
+ outcome = await runTurnInTerminal(runtime, store, store, prompt, exitNow, notifier, {
1209
+ chat: true
1210
+ });
1171
1211
  } finally {
1172
1212
  store.end(statusOf(runtime));
1173
1213
  }
1214
+ if (outcome.kind === "done" && outcome.result.stopReason === "max_steps") {
1215
+ const next2 = await stopHandoff(store, runtime.limits.maxSteps, runtime.executor.isolation);
1216
+ store.refreshStatus(statusOf(runtime));
1217
+ if (next2 === "stop") break;
1218
+ prompt = next2 === "more" ? CONTINUE_PROMPT : SUMMARY_PROMPT;
1219
+ display = prompt;
1220
+ continue;
1221
+ }
1174
1222
  if (!planning || outcome.kind !== "done" || outcome.result.stopReason !== "done") break;
1175
1223
  const next = await planHandoff(runtime, store);
1176
1224
  store.refreshStatus(statusOf(runtime));
@@ -1252,6 +1300,14 @@ var CommandArgs = class {
1252
1300
  return (first === "cancel" || first === "delete") && second === void 0 ? this.jobs : [];
1253
1301
  case "diff":
1254
1302
  return first === void 0 ? words("last") : [];
1303
+ case "rewind": {
1304
+ const turns = this.runtime.rewindPoints.map((p, i) => ({
1305
+ value: String(i + 1),
1306
+ hint: p.prompt
1307
+ }));
1308
+ if (first === void 0) return [...words("fork"), ...turns];
1309
+ return first === "fork" && second === void 0 ? turns : [];
1310
+ }
1255
1311
  case "mcp":
1256
1312
  if (first === void 0) return words("logout");
1257
1313
  return first === "logout" && second === void 0 ? this.runtime.mcpStatus().map((s) => ({ value: s.name })) : [];
@@ -1278,8 +1334,6 @@ export {
1278
1334
  reportsFocus,
1279
1335
  FocusTracker,
1280
1336
  focusEvent,
1281
- formatTokens,
1282
- stopMessage,
1283
1337
  BUILD_PROMPT,
1284
1338
  planHandoff,
1285
1339
  runCommand,
@@ -1288,9 +1342,12 @@ export {
1288
1342
  fuzzyFiles,
1289
1343
  rootFiles,
1290
1344
  runTurnInTerminal,
1345
+ CONTINUE_PROMPT,
1346
+ SUMMARY_PROMPT,
1347
+ stopHandoff,
1291
1348
  runChat,
1292
1349
  commandNames,
1293
1350
  statusOf,
1294
1351
  CommandArgs
1295
1352
  };
1296
- //# sourceMappingURL=chunk-Z66BWTMD.js.map
1353
+ //# sourceMappingURL=chunk-34WBDQBL.js.map
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/app/receipt.ts
4
+ function hasReceipt(receipt) {
5
+ return (receipt.files?.length ?? 0) > 0 || receipt.tests !== void 0 || receipt.commands.outside > 0 || receipt.commands.blockedHosts.length > 0;
6
+ }
7
+ var RUNNERS = /(^|[\s;&|(/])(vitest|jest|pytest|py\.test|mvn|mvnw|gradle|gradlew|go\s+test|node\s+--test|(npm|pnpm|yarn|bun)\s+(run\s+)?test|cargo\s+test|tox|nox)\b/;
8
+ function isTestCommand(command, projectTest) {
9
+ const text = command.trim();
10
+ if (projectTest !== void 0 && projectTest !== "" && text.includes(projectTest)) return true;
11
+ if (/(^|[\s;&|])(mvn|mvnw|gradle|gradlew)\b/.test(text) && !/\b(test|verify|check)\b/.test(text)) {
12
+ return false;
13
+ }
14
+ return RUNNERS.test(text);
15
+ }
16
+ function parseTestCount(output) {
17
+ const num = (s) => s === void 0 ? 0 : Number(s);
18
+ const vitest = /^\s*Tests\s+(.*\b(?:passed|failed|skipped)\b.*?)\s+\((\d+)\)\s*$/m.exec(output);
19
+ if (vitest !== null) {
20
+ return { total: num(vitest[2]), failed: num(/(\d+) failed/.exec(vitest[1] ?? "")?.[1]) };
21
+ }
22
+ const jest = /^Tests:\s+(?:(\d+) failed, )?.*?(\d+) total\s*$/m.exec(output);
23
+ if (jest !== null) return { total: num(jest[2]), failed: num(jest[1]) };
24
+ const nodeTotal = /^(?:#|ℹ) tests (\d+)\s*$/m.exec(output);
25
+ if (nodeTotal !== null) {
26
+ return { total: num(nodeTotal[1]), failed: num(/^(?:#|ℹ) fail (\d+)\s*$/m.exec(output)?.[1]) };
27
+ }
28
+ const pytest = /^=+ (.*\b(?:passed|failed)\b.*) in [\d.]+s(?: \([^)]*\))? =+\s*$/m.exec(output);
29
+ if (pytest !== null) {
30
+ const part = (word) => num(new RegExp(`(\\d+) ${word}\\b`).exec(pytest[1] ?? "")?.[1]);
31
+ const failed = part("failed") + part("errors?");
32
+ return { total: part("passed") + failed, failed };
33
+ }
34
+ const maven = [...output.matchAll(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/g)].at(-1);
35
+ if (maven !== void 0) {
36
+ return { total: num(maven[1]), failed: num(maven[2]) + num(maven[3]) };
37
+ }
38
+ const passes = output.match(/^\s*--- PASS: /gm)?.length ?? 0;
39
+ const fails = output.match(/^\s*--- FAIL: /gm)?.length ?? 0;
40
+ if (passes + fails > 0) return { total: passes + fails, failed: fails };
41
+ return void 0;
42
+ }
43
+ var ReceiptCollector = class {
44
+ constructor(projectTest) {
45
+ this.projectTest = projectTest;
46
+ }
47
+ projectTest;
48
+ calls = /* @__PURE__ */ new Map();
49
+ sandboxed = 0;
50
+ outside = 0;
51
+ blockedHosts = /* @__PURE__ */ new Set();
52
+ tests;
53
+ onEvent(event) {
54
+ if (event.type === "tool_call") {
55
+ const command = event.call.input.command;
56
+ if (typeof command === "string") this.calls.set(event.call.id, command);
57
+ return;
58
+ }
59
+ if (event.type !== "tool_result" || event.outcome.command === void 0) return;
60
+ this.add(this.calls.get(event.call.id) ?? "", event.outcome.command, event.outcome.content);
61
+ }
62
+ /** One command's facts; `output` is the text that the model got (for the test count). */
63
+ add(command, facts, output) {
64
+ if (facts.sandboxed) this.sandboxed++;
65
+ else this.outside++;
66
+ for (const host of facts.blockedHosts) this.blockedHosts.add(host);
67
+ if (!isTestCommand(command, this.projectTest)) return;
68
+ const count = parseTestCount(output);
69
+ const passed = facts.exitCode === 0 && !facts.timedOut;
70
+ this.tests = {
71
+ command: shortCommand(command),
72
+ passed,
73
+ ...facts.timedOut ? { timedOut: true } : {},
74
+ ...count === void 0 ? {} : { total: count.total, failed: count.failed }
75
+ };
76
+ }
77
+ };
78
+ function shortCommand(command) {
79
+ const line = command.trim().split("\n")[0] ?? "";
80
+ return line.length > 40 ? `${line.slice(0, 39)}\u2026` : line;
81
+ }
82
+
83
+ export {
84
+ hasReceipt,
85
+ parseTestCount,
86
+ ReceiptCollector
87
+ };
88
+ //# sourceMappingURL=chunk-4QRTTRVK.js.map