@cruxy/cli 1.2.0 → 1.3.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 (77) hide show
  1. package/dist/agent/context.js +178 -0
  2. package/dist/agent/index.js +1 -0
  3. package/dist/agent/loop.js +41 -2
  4. package/dist/agent/mode.js +103 -0
  5. package/dist/agent/prompts.js +1 -1
  6. package/dist/agent/session.js +185 -72
  7. package/dist/approval/classify.js +204 -0
  8. package/dist/approval/policy.js +41 -3
  9. package/dist/approval/prompt.js +49 -22
  10. package/dist/checkpoint/gate.js +12 -0
  11. package/dist/cli/commands/run.js +374 -227
  12. package/dist/cli/commands/usage.js +45 -45
  13. package/dist/cli/onboard.js +2 -1
  14. package/dist/cli/program.js +60 -18
  15. package/dist/cli/repl.js +67 -249
  16. package/dist/cli/session-commands.js +755 -0
  17. package/dist/cli/session-factory.js +198 -76
  18. package/dist/cli/suggest.js +77 -0
  19. package/dist/components/fuzzy.js +3 -3
  20. package/dist/components/input.js +17 -2
  21. package/dist/components/keys.js +27 -3
  22. package/dist/components/select.js +3 -3
  23. package/dist/config/project.js +53 -1
  24. package/dist/config/schema.js +49 -16
  25. package/dist/jobs/log-renderer.js +47 -0
  26. package/dist/onboarding/steps.js +13 -22
  27. package/dist/plan/approve.js +36 -24
  28. package/dist/plan/execute.js +9 -7
  29. package/dist/plan/render.js +10 -23
  30. package/dist/plan/service.js +4 -1
  31. package/dist/render/capabilities.js +30 -1
  32. package/dist/render/context-view.js +106 -0
  33. package/dist/render/diff.js +198 -12
  34. package/dist/render/index.js +31 -5
  35. package/dist/render/plain-renderer.js +38 -2
  36. package/dist/render/plan-view.js +108 -0
  37. package/dist/render/resize.js +7 -2
  38. package/dist/render/status-view.js +66 -0
  39. package/dist/render/test-view.js +89 -0
  40. package/dist/render/tty-renderer.js +40 -0
  41. package/dist/routing/index.js +1 -0
  42. package/dist/routing/router.js +13 -4
  43. package/dist/routing/session-model.js +109 -0
  44. package/dist/routing/types.js +14 -0
  45. package/dist/session/export.js +88 -0
  46. package/dist/session/index.js +20 -0
  47. package/dist/session/list.js +137 -0
  48. package/dist/session/log.js +137 -0
  49. package/dist/session/paths.js +73 -0
  50. package/dist/session/replay.js +169 -0
  51. package/dist/session/resume.js +128 -0
  52. package/dist/session/types.js +223 -0
  53. package/dist/subagent/orchestrator.js +23 -0
  54. package/dist/testing/run-tests-tool.js +8 -0
  55. package/dist/tools/registry.js +3 -3
  56. package/dist/tui/app.js +385 -0
  57. package/dist/tui/approval-overlay.js +160 -0
  58. package/dist/tui/context-gauge.js +48 -0
  59. package/dist/tui/git-status.js +63 -0
  60. package/dist/tui/index.js +10 -0
  61. package/dist/tui/layout.js +269 -0
  62. package/dist/tui/overlay.js +105 -0
  63. package/dist/tui/palette.js +73 -0
  64. package/dist/tui/panels.js +235 -0
  65. package/dist/tui/renderer.js +776 -0
  66. package/dist/tui/supports.js +20 -0
  67. package/dist/tui/tool-versions.js +129 -0
  68. package/dist/usage/collect.js +21 -3
  69. package/dist/usage/index.js +10 -2
  70. package/dist/usage/report.js +76 -0
  71. package/dist/usage/store.js +7 -1
  72. package/dist/usage/summary.js +106 -17
  73. package/dist/usage/types.js +73 -4
  74. package/dist/usage/weighted.js +77 -0
  75. package/dist/utils/git.js +50 -4
  76. package/package.json +2 -2
  77. package/dist/usage/cost.js +0 -29
@@ -75,20 +75,58 @@ export function scopeCovers(scope, request) {
75
75
  export class InteractivePolicy {
76
76
  allowlist;
77
77
  io;
78
- constructor(allowlist, io) {
78
+ autoApprove;
79
+ constructor(allowlist, io,
80
+ /**
81
+ * Whether the session is in an auto-approving mode (P5 track 3).
82
+ *
83
+ * A THUNK, not a boolean: the mode is runtime state the user can change at
84
+ * any point, including between two actions of the same turn. Reading it
85
+ * once at construction would let a session that has been switched back to
86
+ * `manual` keep auto-approving for the rest of its life — a stale copy of
87
+ * exactly the fact this must never be wrong about.
88
+ *
89
+ * Omitted → never auto-approves, which is every non-interactive caller.
90
+ */
91
+ autoApprove = () => false) {
79
92
  this.allowlist = allowlist;
80
93
  this.io = io;
94
+ this.autoApprove = autoApprove;
81
95
  }
82
96
  async decide(request) {
97
+ // A grant the allowlist answers shows the user nothing (P3) — left
98
+ // unflagged so a caller can render the change instead.
83
99
  if (this.allowlist.allows(request))
84
100
  return { allow: true };
101
+ // Auto-approve: allowed without a prompt, and deliberately NOT flagged as
102
+ // `prompted` — so `previewSilentApprovals` renders the diff. An unattended
103
+ // mode that showed nothing would let a session's whole body of work land
104
+ // invisibly, which is a different failure from the one it is meant to save
105
+ // the user: skipping the QUESTION is the point, skipping the RECORD is not.
106
+ //
107
+ // THE CEILING (P5 track 3). Auto-approve suppresses the prompt for
108
+ // reversible actions ONLY. An action the run's checkpoint cannot restore —
109
+ // a delete outside the workspace, a force-push, a history rewrite, an MCP
110
+ // call, a rollback, anything unprovable — falls through to the prompt even
111
+ // here, and even unattended.
112
+ //
113
+ // This is the line the product position rests on. "Only change what was
114
+ // asked, no silent deletions" is not a claim a mode may switch off:
115
+ // suppressing prompts is a convenience, and suppressing the last gate before
116
+ // irreversible damage is a different thing wearing the same word. Note the
117
+ // gate is `request.irreversible`, NOT `request.tier` — the tier is an
118
+ // unprovability signal (every shell command is `destructive`, `ls`
119
+ // included), so gating on it would make the mode either useless or
120
+ // dishonest. See `ApprovalRequest.irreversible`.
121
+ if (this.autoApprove() && !request.irreversible)
122
+ return { allow: true };
85
123
  const choice = await promptForApproval(request, this.io);
86
124
  switch (choice.kind) {
87
125
  case "once":
88
- return { allow: true };
126
+ return { allow: true, prompted: true };
89
127
  case "session":
90
128
  this.allowlist.grant(request);
91
- return { allow: true };
129
+ return { allow: true, prompted: true };
92
130
  case "reject":
93
131
  return choice.reason
94
132
  ? {
@@ -10,29 +10,39 @@ import { themeForColor } from "../theme/index.js";
10
10
  * is a reject.
11
11
  */
12
12
  export async function promptForApproval(request, io) {
13
- io.write(render(request, io.color, io.columns ?? resolveColumns()));
14
- const key = (await io.readKey()).toLowerCase();
15
- io.write("\n");
16
- switch (key) {
17
- case "y":
18
- return { kind: "once" };
19
- case "a":
20
- return { kind: "session" };
21
- case "n": {
22
- io.write(" reason (optional, sent to the agent): ");
23
- const reason = (await io.readLine()).trim();
24
- return reason ? { kind: "reject", reason } : { kind: "reject" };
25
- }
26
- case "t": {
27
- io.write(" what should the agent do instead? ");
28
- const instruction = (await io.readLine()).trim();
29
- return instruction
30
- ? { kind: "instruct", instruction }
31
- : { kind: "reject" };
13
+ // The terminal is about to belong to this prompt: yield the live region
14
+ // BEFORE the question lands, or it would be painted into a region the next
15
+ // repaint erases (U.2/U.4). `finally` gives it back on every path, including
16
+ // the default-deny ones.
17
+ io.beginPrompt?.();
18
+ try {
19
+ io.write(render(request, io.color, io.columns ?? resolveColumns()));
20
+ const key = (await io.readKey()).toLowerCase();
21
+ io.write("\n");
22
+ switch (key) {
23
+ case "y":
24
+ return { kind: "once" };
25
+ case "a":
26
+ return { kind: "session" };
27
+ case "n": {
28
+ io.write(" reason (optional, sent to the agent): ");
29
+ const reason = (await io.readLine()).trim();
30
+ return reason ? { kind: "reject", reason } : { kind: "reject" };
31
+ }
32
+ case "t": {
33
+ io.write(" what should the agent do instead? ");
34
+ const instruction = (await io.readLine()).trim();
35
+ return instruction
36
+ ? { kind: "instruct", instruction }
37
+ : { kind: "reject" };
38
+ }
39
+ default:
40
+ // n/a key, empty, EOF, Ctrl-C → default-deny.
41
+ return { kind: "reject" };
32
42
  }
33
- default:
34
- // n/a key, empty, EOF, Ctrl-C → default-deny.
35
- return { kind: "reject" };
43
+ }
44
+ finally {
45
+ io.endPrompt?.();
36
46
  }
37
47
  }
38
48
  /** Render the full prompt block: header, detail (diff or command+cwd), choices. */
@@ -48,10 +58,27 @@ export function render(request, color, columns = resolveColumns()) {
48
58
  const label = tierLabel(request.tier, t);
49
59
  const lines = [];
50
60
  lines.push(...header(request.summary, mark, label, t, columns));
61
+ lines.push(...whyAsking(request.irreversible, t, columns));
51
62
  lines.push(detail(request, t, columns));
52
63
  lines.push(choices(request.scope, t));
53
64
  return lines.filter((l) => l !== "").join("\n") + " ";
54
65
  }
66
+ /**
67
+ * The one-clause reason the checkpoint cannot restore this action, when there is
68
+ * one. Rendered for every mode, not just the auto ones — the prompt has no way
69
+ * to know the session's mode, and a user in `manual` benefits from the same
70
+ * sentence.
71
+ *
72
+ * It earns its line in auto-approve, where an unexplained prompt is actively
73
+ * misleading: the user asked not to be asked, so the only useful thing to say is
74
+ * why this one is different. Reflowed, never truncated — a half-sentence about
75
+ * irreversibility is worse than none.
76
+ */
77
+ function whyAsking(irreversible, t, width) {
78
+ if (!irreversible)
79
+ return [];
80
+ return reflow(`no undo: ${irreversible}`, Math.max(1, width - 2)).map((l) => t.muted(` ${l}`));
81
+ }
55
82
  /**
56
83
  * The header, width-aware (U.12). Wide: the one inline line `! cruxy wants to
57
84
  * <summary> (destructive)`. Narrow: the risk (mark + tier label) stands on its
@@ -81,6 +81,18 @@ export class CheckpointGate {
81
81
  });
82
82
  await writeSet(this.primaryRoot, this.set);
83
83
  }
84
+ /**
85
+ * The current run's id, or undefined before the first {@link beginRun}.
86
+ *
87
+ * This is the ONE per-turn id in the CLI, and the session log (P2) borrows it
88
+ * rather than minting a second: `cruxy rollback <id>` reverts a turn's
89
+ * mutations and the session log records that turn's messages, so if the two
90
+ * used different ids the undo unit would mean two different things depending
91
+ * on which surface you asked. Exposed read-only — only `beginRun` sets it.
92
+ */
93
+ currentRunId() {
94
+ return this.runId ?? undefined;
95
+ }
84
96
  /** Root names that got a per-root service this process (inspection/tests). */
85
97
  get touchedRoots() {
86
98
  return [...this.services.keys()];