@cruxy/cli 1.2.1 → 1.4.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.
- package/dist/agent/context.js +178 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/loop.js +20 -1
- package/dist/agent/mode.js +103 -0
- package/dist/agent/prompts.js +1 -1
- package/dist/agent/session.js +171 -69
- package/dist/agent/status.js +56 -0
- package/dist/approval/classify.js +204 -0
- package/dist/approval/policy.js +41 -3
- package/dist/approval/prompt.js +49 -22
- package/dist/checkpoint/gate.js +12 -0
- package/dist/cli/commands/run.js +401 -227
- package/dist/cli/commands/usage.js +45 -45
- package/dist/cli/onboard.js +2 -1
- package/dist/cli/program.js +60 -18
- package/dist/cli/repl.js +67 -249
- package/dist/cli/session-commands.js +717 -0
- package/dist/cli/session-factory.js +198 -76
- package/dist/cli/suggest.js +77 -0
- package/dist/components/fuzzy.js +3 -3
- package/dist/components/input.js +17 -2
- package/dist/components/keys.js +65 -3
- package/dist/components/select.js +3 -3
- package/dist/config/effective.js +225 -0
- package/dist/config/index.js +1 -0
- package/dist/config/manager.js +50 -20
- package/dist/config/project.js +53 -1
- package/dist/config/schema.js +49 -16
- package/dist/jobs/log-renderer.js +47 -0
- package/dist/onboarding/steps.js +13 -22
- package/dist/plan/approve.js +36 -24
- package/dist/plan/execute.js +9 -7
- package/dist/plan/render.js +10 -23
- package/dist/plan/service.js +4 -1
- package/dist/render/capabilities.js +30 -1
- package/dist/render/context-view.js +106 -0
- package/dist/render/diff.js +204 -12
- package/dist/render/index.js +31 -5
- package/dist/render/plain-renderer.js +38 -2
- package/dist/render/plan-view.js +108 -0
- package/dist/render/resize.js +7 -2
- package/dist/render/status-view.js +66 -0
- package/dist/render/test-view.js +89 -0
- package/dist/render/tty-renderer.js +40 -0
- package/dist/routing/index.js +1 -0
- package/dist/routing/router.js +13 -4
- package/dist/routing/session-model.js +109 -0
- package/dist/routing/types.js +14 -0
- package/dist/session/export.js +88 -0
- package/dist/session/index.js +20 -0
- package/dist/session/list.js +137 -0
- package/dist/session/log.js +137 -0
- package/dist/session/paths.js +73 -0
- package/dist/session/replay.js +169 -0
- package/dist/session/resume.js +128 -0
- package/dist/session/types.js +223 -0
- package/dist/subagent/orchestrator.js +23 -0
- package/dist/testing/run-tests-tool.js +8 -0
- package/dist/tools/registry.js +3 -3
- package/dist/tui/app.js +508 -0
- package/dist/tui/approval-overlay.js +160 -0
- package/dist/tui/context-gauge.js +48 -0
- package/dist/tui/git-status.js +108 -0
- package/dist/tui/git-view.js +121 -0
- package/dist/tui/index.js +15 -0
- package/dist/tui/layout.js +314 -0
- package/dist/tui/overlay.js +105 -0
- package/dist/tui/overview.js +49 -0
- package/dist/tui/palette.js +73 -0
- package/dist/tui/panels.js +235 -0
- package/dist/tui/renderer.js +1121 -0
- package/dist/tui/settings-view.js +282 -0
- package/dist/tui/supports.js +20 -0
- package/dist/tui/tasks-view.js +215 -0
- package/dist/tui/tool-versions.js +129 -0
- package/dist/tui/views.js +66 -0
- package/dist/usage/collect.js +6 -6
- package/dist/usage/index.js +10 -2
- package/dist/usage/report.js +76 -0
- package/dist/usage/summary.js +106 -17
- package/dist/usage/types.js +5 -2
- package/dist/usage/weighted.js +77 -0
- package/dist/utils/git.js +163 -4
- package/package.json +1 -1
- package/dist/usage/cost.js +0 -29
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,108 @@ 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
|
+
/** Porcelain XY code → the status this reports. */
|
|
45
|
+
function statusFromCode(x, y) {
|
|
46
|
+
if (x === "?" || y === "?")
|
|
47
|
+
return "untracked";
|
|
48
|
+
// A `U` on either side, or the AA/DD pairs, are unmerged paths.
|
|
49
|
+
if (x === "U" ||
|
|
50
|
+
y === "U" ||
|
|
51
|
+
(x === "A" && y === "A") ||
|
|
52
|
+
(x === "D" && y === "D")) {
|
|
53
|
+
return "conflicted";
|
|
54
|
+
}
|
|
55
|
+
if (x === "R")
|
|
56
|
+
return "renamed";
|
|
57
|
+
if (x === "A")
|
|
58
|
+
return "added";
|
|
59
|
+
if (x === "D" || y === "D")
|
|
60
|
+
return "deleted";
|
|
61
|
+
return "modified";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse `git status --porcelain` into one entry per path.
|
|
65
|
+
*
|
|
66
|
+
* The format is `XY <path>`, with a rename written `R old -> new`. Renames
|
|
67
|
+
* take the NEW path because that is where the content lives now and what a
|
|
68
|
+
* follow-up diff or edit would name; the old one is not shown, which is a real
|
|
69
|
+
* (small) loss of information accepted to keep one row per change.
|
|
70
|
+
*
|
|
71
|
+
* Paths containing spaces are handled because the split is positional (columns
|
|
72
|
+
* 0-1 are the code, 3 onward is the path) rather than whitespace-delimited.
|
|
73
|
+
* Quoted paths — git quotes non-ASCII unless `core.quotePath=false` — are left
|
|
74
|
+
* exactly as git wrote them rather than half-unescaped into something that
|
|
75
|
+
* looks like a real path but is not.
|
|
76
|
+
*/
|
|
77
|
+
export function parsePorcelain(text) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const line of text.split("\n")) {
|
|
80
|
+
if (line.trim() === "")
|
|
81
|
+
continue;
|
|
82
|
+
const x = line[0] ?? " ";
|
|
83
|
+
const y = line[1] ?? " ";
|
|
84
|
+
let path = line.slice(3);
|
|
85
|
+
const status = statusFromCode(x, y);
|
|
86
|
+
if (status === "renamed") {
|
|
87
|
+
const arrow = path.indexOf(" -> ");
|
|
88
|
+
if (arrow >= 0)
|
|
89
|
+
path = path.slice(arrow + 4);
|
|
90
|
+
}
|
|
91
|
+
out.push({ path, status, added: null, removed: null, approximate: false });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse `git diff --numstat` into per-path line counts.
|
|
97
|
+
*
|
|
98
|
+
* A binary file is reported as `-\t-\t<path>`, which becomes `null` on both
|
|
99
|
+
* sides — the honest answer, and the one {@link GitFileChange} already models.
|
|
100
|
+
*/
|
|
101
|
+
export function parseNumstat(text) {
|
|
102
|
+
const out = new Map();
|
|
103
|
+
for (const line of text.split("\n")) {
|
|
104
|
+
if (line.trim() === "")
|
|
105
|
+
continue;
|
|
106
|
+
const [addedRaw, removedRaw, ...rest] = line.split("\t");
|
|
107
|
+
const path = rest.join("\t");
|
|
108
|
+
if (path === "")
|
|
109
|
+
continue;
|
|
110
|
+
out.set(path, {
|
|
111
|
+
added: addedRaw === "-" ? null : Number(addedRaw),
|
|
112
|
+
removed: removedRaw === "-" ? null : Number(removedRaw),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** Shared shaping so the sync and async paths cannot disagree. */
|
|
118
|
+
function toGitInfo(branch, status) {
|
|
119
|
+
const changed = countChanges(status);
|
|
120
|
+
return { branch: branch.trim(), dirty: changed > 0, changed };
|
|
121
|
+
}
|
|
20
122
|
/**
|
|
21
123
|
* Branch name plus the raw `git status --porcelain` text for `cwd`, or `null`
|
|
22
124
|
* when it isn't a git repository (or git is unavailable). Backs the `git_status`
|
|
@@ -32,12 +134,69 @@ export function getGitStatus(cwd) {
|
|
|
32
134
|
return { branch: branch.trim(), status };
|
|
33
135
|
}
|
|
34
136
|
/**
|
|
35
|
-
* Compact git context for the system prompt: current branch
|
|
36
|
-
*
|
|
137
|
+
* Compact git context for the system prompt: current branch, whether the working
|
|
138
|
+
* tree has uncommitted changes, and how many paths changed. `null` when not a
|
|
139
|
+
* repo / git missing.
|
|
140
|
+
*
|
|
141
|
+
* The change count costs no extra subprocess — it is derived from the porcelain
|
|
142
|
+
* output {@link getGitStatus} already fetched.
|
|
37
143
|
*/
|
|
38
144
|
export function getGitInfo(cwd) {
|
|
39
145
|
const info = getGitStatus(cwd);
|
|
40
146
|
if (info === null)
|
|
41
147
|
return null;
|
|
42
|
-
return
|
|
148
|
+
return toGitInfo(info.branch, info.status);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* {@link getGitInfo} without blocking the event loop — what the TUI rail uses.
|
|
152
|
+
* Identical result, identical `null` semantics.
|
|
153
|
+
*/
|
|
154
|
+
export async function getGitInfoAsync(cwd) {
|
|
155
|
+
const branch = await runGitAsync(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
156
|
+
if (branch === null)
|
|
157
|
+
return null;
|
|
158
|
+
const status = await runGitAsync(["status", "--porcelain"], cwd);
|
|
159
|
+
if (status === null)
|
|
160
|
+
return null;
|
|
161
|
+
return toGitInfo(branch, status);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* {@link getGitInfoAsync} plus the per-path detail the Git view needs (P7
|
|
165
|
+
* track 4): the same branch and count, and a `files` list.
|
|
166
|
+
*
|
|
167
|
+
* A SEPARATE ENTRY POINT rather than an option on the old one, because the
|
|
168
|
+
* extra `--numstat` is a third subprocess and the rail must not start paying
|
|
169
|
+
* for a list it has one line to show. Consumers pick the probe that matches
|
|
170
|
+
* what they render, and `files` being absent on the cheap one is what makes
|
|
171
|
+
* that choice visible in the type.
|
|
172
|
+
*
|
|
173
|
+
* The numstat is diffed against HEAD so staged and unstaged work both count —
|
|
174
|
+
* the question a Git view answers is "what has this session done to my files",
|
|
175
|
+
* and whether a change happens to be staged is not part of it. Untracked files
|
|
176
|
+
* appear in the porcelain but in no diff, so their counts stay `null` rather
|
|
177
|
+
* than being invented from a file read.
|
|
178
|
+
*/
|
|
179
|
+
export async function getGitTreeAsync(cwd) {
|
|
180
|
+
const branch = await runGitAsync(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
181
|
+
if (branch === null)
|
|
182
|
+
return null;
|
|
183
|
+
const status = await runGitAsync(["status", "--porcelain"], cwd);
|
|
184
|
+
if (status === null)
|
|
185
|
+
return null;
|
|
186
|
+
const info = toGitInfo(branch, status);
|
|
187
|
+
const files = parsePorcelain(status);
|
|
188
|
+
// Best-effort: a repo with no commits yet has no HEAD to diff against, and
|
|
189
|
+
// that must degrade to "counts unknown", not to "not a repo".
|
|
190
|
+
const numstat = await runGitAsync(["diff", "--numstat", "HEAD"], cwd);
|
|
191
|
+
if (numstat !== null) {
|
|
192
|
+
const counts = parseNumstat(numstat);
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
const c = counts.get(file.path);
|
|
195
|
+
if (c) {
|
|
196
|
+
file.added = c.added;
|
|
197
|
+
file.removed = c.removed;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ...info, files };
|
|
43
202
|
}
|
package/package.json
CHANGED
package/dist/usage/cost.js
DELETED
|
@@ -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
|
-
}
|