@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
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Weighted tokens (C.22) — the unit the cruxy gateway actually meters a
3
+ * subscriber in.
4
+ *
5
+ * A subscriber is not billed per token and not billed in dollars: the gateway
6
+ * counts `(billable_input + output) × multiplier` per request and checks that
7
+ * against two windows. This module computes THAT number, locally, from figures
8
+ * already on disk — so `cruxy usage` can state consumption in the same unit the
9
+ * meter uses instead of a currency nobody is charged in.
10
+ *
11
+ * WHAT THIS IS NOT: it is not headroom, and it must never be rendered as such.
12
+ * A cap needs the user's plan, and how much of that plan every OTHER surface
13
+ * (web chat, desktop, phone) has already spent — neither of which is on this
14
+ * machine. All this module can honestly say is "this is what the runs in this
15
+ * CLI's own store weigh". Anything phrased as remaining/left/available would be
16
+ * a number this process cannot possibly know.
17
+ *
18
+ * No network: the multipliers are a compiled-in constant, not a fetched one.
19
+ */
20
+ /**
21
+ * Weighted-token multipliers, keyed by tier — mirroring `multipliers` in the
22
+ * gateway's `internal/budget/config.go`, which is the source of truth.
23
+ *
24
+ * MIRRORED, THEREFORE DRIFTABLE. Nothing checks these against the server at
25
+ * runtime (that would be a network call this module is forbidden to make), so a
26
+ * gateway-side reweighting silently makes an old CLI's arithmetic stale. That
27
+ * is the accepted cost of computing offline, and it is why every rendering of
28
+ * this figure is framed as what THIS CLI reckons it used — never as an
29
+ * authoritative balance. If they drift, the server is right and this is wrong.
30
+ *
31
+ * Only the chat tiers are listed. The gateway also weighs its embed/guard/rerank
32
+ * models at 0.1×, but the CLI's chat path cannot produce a request on one, so
33
+ * including them would model traffic that never reaches this store.
34
+ */
35
+ export const TIER_MULTIPLIERS = {
36
+ mira: 1.0,
37
+ vaani: 3.25,
38
+ kavi: 3.8,
39
+ };
40
+ /** The multiplier for a tier, or `undefined` when the tier isn't a known one. */
41
+ export function multiplierForTier(tier) {
42
+ return TIER_MULTIPLIERS[tier];
43
+ }
44
+ /**
45
+ * Weighted tokens for one request's figures, or `undefined` when the request
46
+ * cannot be weighed honestly.
47
+ *
48
+ * BOTH terms are required, and this is the whole discipline of the function.
49
+ * The meter's formula has two addends; substituting 0 for an unknown one does
50
+ * not produce an approximate answer, it produces a confidently understated one —
51
+ * and understating consumption is the direction that flatters, so it is exactly
52
+ * the error that would go unnoticed. `costFor` could price one known side
53
+ * because the raw token counts stayed visible beside it; here the weighted
54
+ * figure IS the deliverable, so a partial one is worse than none. Callers count
55
+ * the `undefined`s and surface them instead.
56
+ *
57
+ * Three ways a request is unweighable, all of them real and all treated alike:
58
+ * - no tier, or a tier this build doesn't know (a non-cruxy provider — those
59
+ * tokens never touched the weighted pool, so weighing them would be fiction);
60
+ * - no `billable_input_tokens` (a gateway too old to send it). Note that
61
+ * `inputTokens` is NOT a fallback: it includes cache-read tokens, which the
62
+ * meter excludes, so it would overstate every cached request — the precise
63
+ * inverse of the caching win the field exists to show;
64
+ * - no output count.
65
+ *
66
+ * A reported `0` on either side is a real measurement and weighs in as 0.
67
+ */
68
+ export function weightedFor(tier, billableInputTokens, outputTokens) {
69
+ if (tier === undefined)
70
+ return undefined;
71
+ const multiplier = multiplierForTier(tier);
72
+ if (multiplier === undefined)
73
+ return undefined;
74
+ if (billableInputTokens === undefined || outputTokens === undefined)
75
+ return undefined;
76
+ return (billableInputTokens + outputTokens) * multiplier;
77
+ }
package/dist/utils/git.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { execFile, spawnSync } from "node:child_process";
2
2
  /** Hard ceiling on a git invocation; a hung git must never stall startup. */
3
3
  const GIT_TIMEOUT_MS = 5000;
4
4
  /**
@@ -17,6 +17,35 @@ function runGit(args, cwd) {
17
17
  }
18
18
  return res.stdout;
19
19
  }
20
+ /**
21
+ * {@link runGit} without blocking the event loop. Same contract in every other
22
+ * respect: `null` on any failure, never throws, same timeout.
23
+ *
24
+ * This exists because `spawnSync` is measurably expensive — around 45ms warm for
25
+ * the branch+status pair — and the TUI needs this data while a model response is
26
+ * streaming. Blocking the loop for 45ms mid-stream stutters the paint; blocking
27
+ * it from the paint path itself would do so on every frame.
28
+ */
29
+ function runGitAsync(args, cwd) {
30
+ return new Promise((resolve) => {
31
+ execFile("git", args, { cwd, encoding: "utf8", timeout: GIT_TIMEOUT_MS, windowsHide: true }, (err, stdout) => resolve(err ? null : stdout));
32
+ });
33
+ }
34
+ /**
35
+ * Number of changed paths in `git status --porcelain` output.
36
+ *
37
+ * One line is one path, INCLUDING a rename (`R old -> new`), which is one
38
+ * change and not two. Blank lines are ignored so a trailing newline — always
39
+ * present on non-empty output — cannot inflate the count by one.
40
+ */
41
+ export function countChanges(status) {
42
+ return status.split("\n").filter((line) => line.trim() !== "").length;
43
+ }
44
+ /** Shared shaping so the sync and async paths cannot disagree. */
45
+ function toGitInfo(branch, status) {
46
+ const changed = countChanges(status);
47
+ return { branch: branch.trim(), dirty: changed > 0, changed };
48
+ }
20
49
  /**
21
50
  * Branch name plus the raw `git status --porcelain` text for `cwd`, or `null`
22
51
  * when it isn't a git repository (or git is unavailable). Backs the `git_status`
@@ -32,12 +61,29 @@ export function getGitStatus(cwd) {
32
61
  return { branch: branch.trim(), status };
33
62
  }
34
63
  /**
35
- * Compact git context for the system prompt: current branch and whether the
36
- * working tree has uncommitted changes. `null` when not a repo / git missing.
64
+ * Compact git context for the system prompt: current branch, whether the working
65
+ * tree has uncommitted changes, and how many paths changed. `null` when not a
66
+ * repo / git missing.
67
+ *
68
+ * The change count costs no extra subprocess — it is derived from the porcelain
69
+ * output {@link getGitStatus} already fetched.
37
70
  */
38
71
  export function getGitInfo(cwd) {
39
72
  const info = getGitStatus(cwd);
40
73
  if (info === null)
41
74
  return null;
42
- return { branch: info.branch, dirty: info.status.trim().length > 0 };
75
+ return toGitInfo(info.branch, info.status);
76
+ }
77
+ /**
78
+ * {@link getGitInfo} without blocking the event loop — what the TUI rail uses.
79
+ * Identical result, identical `null` semantics.
80
+ */
81
+ export async function getGitInfoAsync(cwd) {
82
+ const branch = await runGitAsync(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
83
+ if (branch === null)
84
+ return null;
85
+ const status = await runGitAsync(["status", "--porcelain"], cwd);
86
+ if (status === null)
87
+ return null;
88
+ return toGitInfo(branch, status);
43
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "undici": "^6.21.0",
37
37
  "zod": "^3.23.8",
38
38
  "zod-to-json-schema": "^3.23.5",
39
- "@cruxy/sdk": "0.4.0"
39
+ "@cruxy/sdk": "0.4.1"
40
40
  },
41
41
  "optionalDependencies": {
42
42
  "better-sqlite3": "^12.11.1"
@@ -1,29 +0,0 @@
1
- /**
2
- * Token → cost mapping (C.22). The whole discipline lives in one rule: a cost is
3
- * produced ONLY when the tier has a configured price AND at least one token side
4
- * is known. Otherwise the result is `undefined` — cost is omitted, tokens are
5
- * still shown, and NO dollar figure is ever fabricated. Prices are per MILLION
6
- * tokens (see {@link TierPrice}). Keyed by tier only (U.8 gag). No network.
7
- */
8
- /** The configured price for a tier, or `undefined` when the tier is unpriced. */
9
- export function priceForTier(tier, prices) {
10
- return prices[tier];
11
- }
12
- /**
13
- * Compute the cost of a tier's usage, or `undefined` when it cannot be stated
14
- * honestly:
15
- * - no configured price for the tier → `undefined` (cost omitted).
16
- * - both token counts unknown → `undefined` (nothing real to price).
17
- * A known side is priced; an unknown side contributes nothing (never a
18
- * fabricated 0-token charge). tokens/1e6 × price, summed.
19
- */
20
- export function costFor(tier, inputTokens, outputTokens, prices) {
21
- const price = priceForTier(tier, prices);
22
- if (!price)
23
- return undefined;
24
- if (inputTokens === undefined && outputTokens === undefined)
25
- return undefined;
26
- const inCost = inputTokens !== undefined ? (inputTokens / 1_000_000) * price.input : 0;
27
- const outCost = outputTokens !== undefined ? (outputTokens / 1_000_000) * price.output : 0;
28
- return inCost + outCost;
29
- }