@cruxy/cli 1.10.0 → 1.11.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.
- package/README.md +13 -1
- package/dist/agent/context.js +6 -0
- package/dist/agent/session.js +11 -0
- package/dist/cli/command-catalog.js +5 -1
- package/dist/cli/commands/config.js +18 -3
- package/dist/cli/commands/logs.js +149 -0
- package/dist/cli/commands/pr.js +1 -10
- package/dist/cli/commands/run.js +6 -3
- package/dist/cli/commands/sessions.js +27 -2
- package/dist/cli/onboard.js +0 -9
- package/dist/cli/program.js +15 -1
- package/dist/cli/session-commands.js +49 -2
- package/dist/components/input.js +18 -1
- package/dist/components/keys.js +66 -3
- package/dist/config/manager.js +91 -11
- package/dist/config/schema.js +61 -12
- package/dist/errors/constructors.js +35 -2
- package/dist/errors/types.js +6 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/log-renderer.js +10 -5
- package/dist/jobs/log-store.js +505 -0
- package/dist/jobs/manager.js +69 -1
- package/dist/render/context-view.js +12 -3
- package/dist/render/index.js +2 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/router.js +34 -14
- package/dist/routing/types.js +0 -2
- package/dist/session/prune.js +83 -23
- package/dist/subagent/orchestrator.js +16 -3
- package/dist/tui/app.js +30 -7
- package/dist/tui/approval-overlay.js +4 -1
- package/dist/tui/layout.js +25 -1
- package/dist/tui/panels.js +44 -6
- package/dist/tui/renderer.js +187 -14
- package/dist/tui/supports.js +15 -0
- package/dist/usage/types.js +25 -0
- package/dist/utils/logger.js +52 -6
- package/package.json +1 -1
package/dist/components/keys.js
CHANGED
|
@@ -26,6 +26,22 @@ const CBT = 0x5a;
|
|
|
26
26
|
* not rejected, so these are matched on the leading number instead.
|
|
27
27
|
*/
|
|
28
28
|
const TILDE = 0x7e;
|
|
29
|
+
/**
|
|
30
|
+
* The two mouse encodings a terminal can answer `?1000h` with.
|
|
31
|
+
*
|
|
32
|
+
* SGR (`?1006h`, the one the TUI asks for) sends `ESC [ < Cb ; Cx ; Cy M` for
|
|
33
|
+
* a press and `… m` for a release, all in printable ASCII. A terminal that does
|
|
34
|
+
* not know SGR falls back to X10: `ESC [ M` followed by THREE RAW BYTES
|
|
35
|
+
* (button+32, column+32, row+32). The X10 form has to be recognised even though
|
|
36
|
+
* it is never requested, because the alternative is worse than an unmapped key:
|
|
37
|
+
* the generic CSI rule below would stop at the `M`, and the three bytes after
|
|
38
|
+
* it — printable, by construction — would land in the input line as text.
|
|
39
|
+
*/
|
|
40
|
+
const SGR_MOUSE_INTRO = 0x3c; // <
|
|
41
|
+
const X10_MOUSE_FINAL = 0x4d; // M
|
|
42
|
+
/** Bit 6 of the button code marks a wheel event; bit 0 says which way. */
|
|
43
|
+
const MOUSE_WHEEL_FLAG = 64;
|
|
44
|
+
const X10_MOUSE_PAYLOAD = 3;
|
|
29
45
|
/** Leading `~`-sequence parameter → key. `ESC [ 5 ~` / `ESC [ 6 ~`. */
|
|
30
46
|
const TILDES = {
|
|
31
47
|
5: "page-up",
|
|
@@ -43,9 +59,10 @@ const ARROWS = {
|
|
|
43
59
|
* keys per chunk; all are returned in order.
|
|
44
60
|
*
|
|
45
61
|
* Escape handling is deliberately simple: `ESC [ A..D` decodes to an arrow,
|
|
46
|
-
* `ESC [ … Z` decodes to Shift+Tab,
|
|
47
|
-
*
|
|
48
|
-
*
|
|
62
|
+
* `ESC [ … Z` decodes to Shift+Tab, a mouse report (SGR `ESC [ < … M`, or X10
|
|
63
|
+
* `ESC [ M` + 3 bytes) decodes to a wheel notch or to nothing, any other CSI
|
|
64
|
+
* sequence (`ESC [ …final`) is swallowed whole (unmapped keys must not leak
|
|
65
|
+
* garbage chars into a query), and a lone ESC decodes to `escape`. Terminals send arrow sequences atomically in practice; a sequence
|
|
49
66
|
* split across chunks degrades to `escape` + literal chars, which is safe
|
|
50
67
|
* (escape cancels).
|
|
51
68
|
*/
|
|
@@ -56,12 +73,44 @@ export function decodeKeys(chunk) {
|
|
|
56
73
|
const byte = buf[i];
|
|
57
74
|
if (byte === ESC) {
|
|
58
75
|
if (buf[i + 1] === 0x5b /* [ */) {
|
|
76
|
+
// X10 mouse: `ESC [ M` plus three raw payload bytes. Consumed as one
|
|
77
|
+
// unit BEFORE the generic CSI walk, which would otherwise treat `M` as
|
|
78
|
+
// the final byte and hand the payload to the printable branch.
|
|
79
|
+
if (buf[i + 2] === X10_MOUSE_FINAL) {
|
|
80
|
+
const end = i + 3 + X10_MOUSE_PAYLOAD;
|
|
81
|
+
if (end <= buf.length) {
|
|
82
|
+
const wheel = mouseWheel(buf[i + 3] - 32);
|
|
83
|
+
if (wheel)
|
|
84
|
+
keys.push({ kind: wheel });
|
|
85
|
+
i = end;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Truncated at the chunk boundary: the same degradation as any other
|
|
89
|
+
// partial CSI — escape, and drop the tail.
|
|
90
|
+
keys.push({ kind: "escape" });
|
|
91
|
+
i = buf.length;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
59
94
|
// CSI: consume parameter/intermediate bytes (0x20–0x3f) up to the
|
|
60
95
|
// final byte (0x40–0x7e); map arrows, swallow everything else.
|
|
61
96
|
let j = i + 2;
|
|
62
97
|
while (j < buf.length && buf[j] >= 0x20 && buf[j] <= 0x3f)
|
|
63
98
|
j++;
|
|
64
99
|
if (j < buf.length) {
|
|
100
|
+
if (buf[i + 2] === SGR_MOUSE_INTRO) {
|
|
101
|
+
// SGR mouse: `ESC [ < Cb ; Cx ; Cy M|m`. Only the button code is
|
|
102
|
+
// read — the wheel has no position — and a press is the only edge
|
|
103
|
+
// a wheel has, so `m` (release) never maps to anything.
|
|
104
|
+
let cb = 0;
|
|
105
|
+
for (let k = i + 3; k < j && buf[k] >= 0x30 && buf[k] <= 0x39; k++) {
|
|
106
|
+
cb = cb * 10 + (buf[k] - 0x30);
|
|
107
|
+
}
|
|
108
|
+
const wheel = buf[j] === X10_MOUSE_FINAL ? mouseWheel(cb) : null;
|
|
109
|
+
if (wheel)
|
|
110
|
+
keys.push({ kind: wheel });
|
|
111
|
+
i = j + 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
65
114
|
if (buf[j] === CBT) {
|
|
66
115
|
// Shift+Tab, in BOTH its forms: bare `ESC [ Z`, and the
|
|
67
116
|
// parameterized `ESC [ 1 ; 2 Z` some terminals send when a modifier
|
|
@@ -165,3 +214,17 @@ export function decodeKeys(chunk) {
|
|
|
165
214
|
}
|
|
166
215
|
return keys;
|
|
167
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* A mouse button code → the wheel direction it encodes, or null for any other
|
|
219
|
+
* mouse event. Modifier bits (shift 4, meta 8, ctrl 16) are masked off so a
|
|
220
|
+
* wheel with a modifier held still scrolls; the low two bits pick the direction.
|
|
221
|
+
*/
|
|
222
|
+
function mouseWheel(button) {
|
|
223
|
+
if ((button & MOUSE_WHEEL_FLAG) === 0)
|
|
224
|
+
return null;
|
|
225
|
+
return (button & 3) === 0
|
|
226
|
+
? "wheel-up"
|
|
227
|
+
: (button & 3) === 1
|
|
228
|
+
? "wheel-down"
|
|
229
|
+
: null;
|
|
230
|
+
}
|
package/dist/config/manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { configInvalid, configParse, mcpProjectHeaders, } from "../errors/index.js";
|
|
4
|
-
import { CruxyConfigSchema } from "./schema.js";
|
|
3
|
+
import { configInvalid, configNotFound, configParse, mcpProjectHeaders, } from "../errors/index.js";
|
|
4
|
+
import { CruxyConfigSchema, } from "./schema.js";
|
|
5
5
|
import { globalConfigPath, findProjectConfig } from "./paths.js";
|
|
6
6
|
import { readCredential } from "./credentials.js";
|
|
7
7
|
function isPlainObject(v) {
|
|
@@ -90,6 +90,39 @@ function envOverrides() {
|
|
|
90
90
|
}
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The `--config <path>` this process was invoked with, or null.
|
|
95
|
+
*
|
|
96
|
+
* PROCESS-WIDE RATHER THAN THREADED, because the flag is a property of the
|
|
97
|
+
* INVOCATION: every read of the config in one run must resolve the same file,
|
|
98
|
+
* and there are ~20 `loadConfig()` call sites. Two of them cannot take an
|
|
99
|
+
* argument at all — the credential-expiry probe in `src/index.ts` runs from the
|
|
100
|
+
* error boundary, after the command that would have carried the path has
|
|
101
|
+
* already thrown, and the Settings view's `reload` is a zero-argument callback
|
|
102
|
+
* held for a whole session. Threading would wire the sites that are easy to
|
|
103
|
+
* reach and leave those two reading a *different* config than the command they
|
|
104
|
+
* belong to, which is the same silent divergence this flag was filed for
|
|
105
|
+
* (#289).
|
|
106
|
+
*
|
|
107
|
+
* Set once from the `preAction` hook that already applies the other global
|
|
108
|
+
* options (`cli/program.ts`), alongside `logger.setLevel`. An explicit
|
|
109
|
+
* {@link LoadOptions.configPath} still wins, so a caller that names a file is
|
|
110
|
+
* unaffected.
|
|
111
|
+
*/
|
|
112
|
+
let cliConfigPath = null;
|
|
113
|
+
/**
|
|
114
|
+
* Record the `--config <path>` for this process. Returns the previous value so
|
|
115
|
+
* a test can restore it; pass null to clear.
|
|
116
|
+
*/
|
|
117
|
+
export function setCliConfigPath(path) {
|
|
118
|
+
const previous = cliConfigPath;
|
|
119
|
+
cliConfigPath = path;
|
|
120
|
+
return previous;
|
|
121
|
+
}
|
|
122
|
+
/** The `--config <path>` in effect for this process, or null. */
|
|
123
|
+
export function cliConfigPathInEffect() {
|
|
124
|
+
return cliConfigPath;
|
|
125
|
+
}
|
|
93
126
|
/**
|
|
94
127
|
* Resolution order (later wins):
|
|
95
128
|
* schema defaults -> global file -> project file (or explicit) -> env vars
|
|
@@ -113,12 +146,21 @@ export function loadConfig(opts = {}) {
|
|
|
113
146
|
merged = deepMerge(merged, layers.global);
|
|
114
147
|
sources.global = gPath;
|
|
115
148
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
149
|
+
// `--config` (recorded process-wide) unless this caller named a file itself.
|
|
150
|
+
const configPath = opts.configPath ?? cliConfigPath ?? undefined;
|
|
151
|
+
if (configPath) {
|
|
152
|
+
// A named file that is not there is a MISTAKE, never a fall-through to
|
|
153
|
+
// discovery or defaults: silently running the default config for a path
|
|
154
|
+
// someone typed is precisely the failure #289 is about, wearing the other
|
|
155
|
+
// hat. The global and project layers are `existsSync`-guarded above because
|
|
156
|
+
// absence there is the normal case; here absence is the error.
|
|
157
|
+
if (!existsSync(configPath))
|
|
158
|
+
throw configNotFound(configPath);
|
|
159
|
+
const obj = readJsonFile(configPath);
|
|
160
|
+
rejectProjectScopeHeaders(obj, configPath);
|
|
119
161
|
layers.explicit = obj;
|
|
120
162
|
merged = deepMerge(merged, obj);
|
|
121
|
-
sources.explicit =
|
|
163
|
+
sources.explicit = configPath;
|
|
122
164
|
}
|
|
123
165
|
else {
|
|
124
166
|
const pPath = findProjectConfig(opts.cwd);
|
|
@@ -137,10 +179,28 @@ export function loadConfig(opts = {}) {
|
|
|
137
179
|
const issues = result.error.issues
|
|
138
180
|
.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
139
181
|
.join("\n");
|
|
140
|
-
throw configInvalid(issues, sources
|
|
182
|
+
throw configInvalid(issues, configSourceFile(sources) ?? undefined);
|
|
141
183
|
}
|
|
142
184
|
return { config: result.data, sources, layers };
|
|
143
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The file to NAME for the config as a whole — the nearest winning source, in
|
|
188
|
+
* the precedence order the merge itself used.
|
|
189
|
+
*
|
|
190
|
+
* `explicit` COMES FIRST, and that is the whole point: an explicit file
|
|
191
|
+
* REPLACES project discovery, so when `--config` is in play `sources.project`
|
|
192
|
+
* is null and a `project ?? global` fallback silently names the *global* file.
|
|
193
|
+
* A user fixing the field an error reports would then be editing a file that
|
|
194
|
+
* contributed nothing to it (#289).
|
|
195
|
+
*
|
|
196
|
+
* This is the whole-config counterpart to `originFile` in `effective.ts`, which
|
|
197
|
+
* answers the same question per key. It lives here, next to the `sources` it
|
|
198
|
+
* reads, so `loadConfig`'s own error path can use it without importing
|
|
199
|
+
* `effective.ts` (which imports this module).
|
|
200
|
+
*/
|
|
201
|
+
export function configSourceFile(sources) {
|
|
202
|
+
return sources.explicit ?? sources.project ?? sources.global ?? null;
|
|
203
|
+
}
|
|
144
204
|
/** Resolve a dot-path (e.g. "model.temperature") against a config object. */
|
|
145
205
|
export function getPath(obj, path) {
|
|
146
206
|
return path
|
|
@@ -205,13 +265,33 @@ export function resolveApiKey(provider) {
|
|
|
205
265
|
function envApiKey(provider) {
|
|
206
266
|
return process.env[apiKeyEnvVar(provider)];
|
|
207
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Provider id → the environment variable its API key is read from.
|
|
270
|
+
*
|
|
271
|
+
* A TOTAL RECORD OVER `Provider`, not a switch with a default, because that is
|
|
272
|
+
* what makes the mapping self-maintaining: adding a provider to
|
|
273
|
+
* {@link ProviderSchema} makes this object a compile error until its env var is
|
|
274
|
+
* named. A `default:` arm silently answers `CRUXY_API_KEY` for a provider
|
|
275
|
+
* nobody has thought about yet, which is the failure mode #285 is named for —
|
|
276
|
+
* the copy that drifts is always the one telling the user where to put their
|
|
277
|
+
* key.
|
|
278
|
+
*/
|
|
279
|
+
const API_KEY_ENV_VAR = {
|
|
280
|
+
cruxy: "CRUXY_API_KEY",
|
|
281
|
+
};
|
|
208
282
|
/**
|
|
209
283
|
* The environment variable a provider's API key is read from — the NAME only,
|
|
210
284
|
* never the value, so a surface can say where a key would come from without
|
|
211
|
-
* ever holding one.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
285
|
+
* ever holding one.
|
|
286
|
+
*
|
|
287
|
+
* THE one definition. It was exported to prevent restatement and said so at the
|
|
288
|
+
* site, and two copies were written anyway (`cli/onboard.ts`,
|
|
289
|
+
* `cli/commands/pr.ts`), both carrying an `"openai"` branch that #284 had made
|
|
290
|
+
* unreachable — invisible because all three took `string`. The parameter is
|
|
291
|
+
* {@link Provider} now: an id the config layer would refuse no longer type-checks
|
|
292
|
+
* here, so the next such branch is a build failure rather than dead code that
|
|
293
|
+
* reads as live (#285).
|
|
214
294
|
*/
|
|
215
295
|
export function apiKeyEnvVar(provider) {
|
|
216
|
-
return provider
|
|
296
|
+
return API_KEY_ENV_VAR[provider];
|
|
217
297
|
}
|
package/dist/config/schema.js
CHANGED
|
@@ -133,7 +133,14 @@ export const ContextConfigSchema = z
|
|
|
133
133
|
* sees — the system prompt and every tool's JSON schema — added to the
|
|
134
134
|
* measured history before the threshold test so the trigger reflects the
|
|
135
135
|
* real request size, not just the visible messages. Roughly the size of
|
|
136
|
-
* the built system prompt plus the default tool catalogue today
|
|
136
|
+
* the built system prompt plus the default tool catalogue today (about
|
|
137
|
+
* 3.7k measured for the prompt and 14 tool schemas).
|
|
138
|
+
*
|
|
139
|
+
* An ALLOWANCE, not a measurement — the context panel and `/context` say
|
|
140
|
+
* so. It is the same number on every turn, so it does not track the parts
|
|
141
|
+
* of a request that vary: CRUXY.md, recalled memory, LSP context, web
|
|
142
|
+
* results and MCP tool schemas all make the real request larger without
|
|
143
|
+
* moving it. Raise it if a project carries a lot of those.
|
|
137
144
|
*/
|
|
138
145
|
reserveTokens: z.number().int().nonnegative().default(4500),
|
|
139
146
|
/** Most-recent messages always kept verbatim (a floor; the cut rounds up to
|
|
@@ -391,13 +398,47 @@ export const JobsConfigSchema = z
|
|
|
391
398
|
*/
|
|
392
399
|
maxJobs: z.number().int().positive().default(5),
|
|
393
400
|
/**
|
|
394
|
-
* How many of a job's most-recent log lines are retained in its
|
|
395
|
-
*
|
|
396
|
-
*
|
|
401
|
+
* How many of a job's most-recent log lines are retained in its IN-MEMORY
|
|
402
|
+
* ring buffer, which is what `/logs <id>` and the Tasks view read while the
|
|
403
|
+
* session is alive. Bounded so a chatty job can't grow memory without
|
|
404
|
+
* limit; older lines roll off oldest-first and `/logs` says how many.
|
|
405
|
+
* Default 1000.
|
|
406
|
+
*
|
|
407
|
+
* This is no longer the only copy: {@link logFileLines} bounds the
|
|
408
|
+
* persisted one, which `cruxy logs <id>` reads back after the fact.
|
|
397
409
|
*/
|
|
398
410
|
logBufferLines: z.number().int().positive().default(1000),
|
|
411
|
+
/**
|
|
412
|
+
* How many lines of a job's output are written to its file under
|
|
413
|
+
* `~/.cruxy/projects/<project>/subagents/` (#172 item 1). Past this the log
|
|
414
|
+
* records one honest `truncated` marker and stops; the job runs on.
|
|
415
|
+
*
|
|
416
|
+
* WHY A SECOND, LARGER BOUND rather than reusing {@link logBufferLines}.
|
|
417
|
+
* The file exists precisely to remove the ring buffer's drop-on-overflow,
|
|
418
|
+
* so a file bounded at the buffer's size would persist the same truncated
|
|
419
|
+
* tail and buy nothing — which is why this is FLOORED at `logBufferLines`
|
|
420
|
+
* rather than allowed to sink below it (see the refinement below).
|
|
421
|
+
*
|
|
422
|
+
* WHY IT IS BOUNDED AT ALL. `sessions.retention` bounds how many session-
|
|
423
|
+
* shaped things the tree keeps; nothing bounds how big ONE of them gets,
|
|
424
|
+
* and a background job is the first writer here that can produce unbounded
|
|
425
|
+
* output unattended — it runs a full agent loop off screen, and a job stuck
|
|
426
|
+
* in a tool-call loop emits lines for as long as its budget lasts with
|
|
427
|
+
* nobody watching. Streaming that to disk uncapped is the second unbounded
|
|
428
|
+
* writer under `~/.cruxy` that #257 exists to prevent.
|
|
429
|
+
*
|
|
430
|
+
* 20000 is 20x the in-memory buffer: roughly 2 MB for a worst-case job, and
|
|
431
|
+
* the sweep drops a `done` job's log at the next start, so the steady-state
|
|
432
|
+
* cost of the default is near zero.
|
|
433
|
+
*/
|
|
434
|
+
logFileLines: z.number().int().positive().default(20000),
|
|
399
435
|
})
|
|
400
|
-
.strict()
|
|
436
|
+
.strict()
|
|
437
|
+
.refine((j) => j.logFileLines >= j.logBufferLines, {
|
|
438
|
+
message: "jobs.logFileLines must be at least jobs.logBufferLines — the persisted log " +
|
|
439
|
+
"exists to remove the ring buffer's drop-on-overflow, and a file that kept " +
|
|
440
|
+
"fewer lines than memory would make persisting strictly worse than not persisting",
|
|
441
|
+
});
|
|
401
442
|
/**
|
|
402
443
|
* Sandbox / container execution (C.16): defense-in-depth beneath the U.3 gate.
|
|
403
444
|
* When enabled, `run_command` and `run_tests` execute inside an isolated,
|
|
@@ -461,17 +502,25 @@ export const HooksConfigSchema = z
|
|
|
461
502
|
/**
|
|
462
503
|
* Multi-model routing (C.30): map declared task classes to tiers so mechanical
|
|
463
504
|
* work runs on a cheap tier and hard reasoning on a strong one. Fully opt-in —
|
|
464
|
-
* with an empty `map` and no `default
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
505
|
+
* with an empty `map` and no `default` the whole table is inert and nothing
|
|
506
|
+
* about a session changes. Only tier names appear here; upstream model names
|
|
507
|
+
* never do (U.8). Keys are the fixed {@link TASK_CLASSES}, so a mistyped class
|
|
508
|
+
* is rejected at config load.
|
|
509
|
+
*
|
|
510
|
+
* A PARTIAL TABLE STAYS PARTIAL. Writing `map` without `default` routes exactly
|
|
511
|
+
* the classes named and leaves every other one to the gateway (`auto`) — it does
|
|
512
|
+
* not pin them to some tier chosen on your behalf. The exception is a
|
|
513
|
+
* `model.model` that names a tier: that is a session-wide model choice, and the
|
|
514
|
+
* unnamed classes inherit it rather than being handed back to the gateway.
|
|
468
515
|
*/
|
|
469
516
|
export const RoutingConfigSchema = z
|
|
470
517
|
.object({
|
|
471
|
-
/** Tier for any task class not in `map`. Unset → the tier
|
|
472
|
-
*
|
|
518
|
+
/** Tier for any task class not in `map`. Unset → the tier `model.model`
|
|
519
|
+
* names, if it names one; otherwise the class is not routed here at all and
|
|
520
|
+
* goes out as `auto` for the gateway to route. */
|
|
473
521
|
default: z.enum(MODEL_TIERS).optional(),
|
|
474
|
-
/** Per-task-class tier overrides; anything omitted takes `default
|
|
522
|
+
/** Per-task-class tier overrides; anything omitted takes `default`, or
|
|
523
|
+
* `auto` when there is no `default` to take. */
|
|
475
524
|
map: z.record(z.enum(TASK_CLASSES), z.enum(MODEL_TIERS)).default({}),
|
|
476
525
|
})
|
|
477
526
|
.strict();
|
|
@@ -109,14 +109,47 @@ export function configParse(path, underlying) {
|
|
|
109
109
|
meta: { path },
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* `--config <path>` named a file that does not exist.
|
|
114
|
+
*
|
|
115
|
+
* Loud by construction: the alternative is falling back to discovery or to
|
|
116
|
+
* defaults, which produces exactly the behaviour the user would have got
|
|
117
|
+
* without the flag, and so is indistinguishable from success (#289). Raised
|
|
118
|
+
* only for an EXPLICIT path — a missing global or project config is ordinary.
|
|
119
|
+
*/
|
|
120
|
+
export function configNotFound(path) {
|
|
121
|
+
return new CruxyError({
|
|
122
|
+
code: ErrorCode.ConfigNotFound,
|
|
123
|
+
title: `config file not found: ${path}`,
|
|
124
|
+
cause: "`--config` named a file that does not exist",
|
|
125
|
+
nextSteps: [
|
|
126
|
+
"check the path (it is resolved as given, relative to the current directory)",
|
|
127
|
+
"omit `--config` to use the discovered project config, or `~/.cruxy/config.json`",
|
|
128
|
+
],
|
|
129
|
+
meta: { path },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
112
132
|
export function configInvalid(issues, path) {
|
|
113
133
|
return new CruxyError({
|
|
114
134
|
code: ErrorCode.ConfigInvalid,
|
|
115
|
-
|
|
135
|
+
// NAME THE FILE, as `configParse` above already does. The path was carried
|
|
136
|
+
// in `meta` alone, and the terminal formatter renders title/cause/steps/code
|
|
137
|
+
// — not meta — so the caller's choice of which file to blame reached nobody
|
|
138
|
+
// and "correct the reported field(s)" left the user to guess between the
|
|
139
|
+
// global, project and explicit configs (#289).
|
|
140
|
+
//
|
|
141
|
+
// It is the HIGHEST-PRECEDENCE file in effect, not necessarily the one
|
|
142
|
+
// holding the bad key: the config is a merge, and a resolved value cannot
|
|
143
|
+
// be traced to a layer from here. Hence "start with" and the pointer to
|
|
144
|
+
// `config path` — an honest lead, not a claim about which line to edit.
|
|
145
|
+
title: path
|
|
146
|
+
? `the configuration is invalid (${path})`
|
|
147
|
+
: "the configuration is invalid",
|
|
116
148
|
cause: issues,
|
|
117
149
|
nextSteps: [
|
|
118
150
|
"correct the reported field(s)",
|
|
119
|
-
|
|
151
|
+
...(path ? [`start with ${path}, the last file merged`] : []),
|
|
152
|
+
"see valid keys with `cruxy config list`, or the files in effect with `cruxy config path`",
|
|
120
153
|
],
|
|
121
154
|
meta: path ? { path } : undefined,
|
|
122
155
|
});
|
package/dist/errors/types.js
CHANGED
|
@@ -29,6 +29,11 @@ export const ErrorCode = {
|
|
|
29
29
|
// config (exit 3)
|
|
30
30
|
ConfigParse: "CRUXY_E_CONFIG_PARSE",
|
|
31
31
|
ConfigInvalid: "CRUXY_E_CONFIG_INVALID",
|
|
32
|
+
/** A config file named with `--config` does not exist. DISTINCT from
|
|
33
|
+
* {@link ConfigParse}: nothing was malformed, the file simply is not there,
|
|
34
|
+
* and the advice is to check the path rather than the JSON. Only an EXPLICIT
|
|
35
|
+
* path can raise it — a missing global or project file is the normal case. */
|
|
36
|
+
ConfigNotFound: "CRUXY_E_CONFIG_NOT_FOUND",
|
|
32
37
|
// auth (exit 4)
|
|
33
38
|
AuthMissingKey: "CRUXY_E_AUTH_MISSING_KEY",
|
|
34
39
|
AuthInvalid: "CRUXY_E_AUTH_INVALID",
|
|
@@ -311,6 +316,7 @@ const EXIT_CODES = {
|
|
|
311
316
|
[ErrorCode.RoutingTierUnavailable]: 2,
|
|
312
317
|
[ErrorCode.ConfigParse]: 3,
|
|
313
318
|
[ErrorCode.ConfigInvalid]: 3,
|
|
319
|
+
[ErrorCode.ConfigNotFound]: 3,
|
|
314
320
|
[ErrorCode.AuthMissingKey]: 4,
|
|
315
321
|
[ErrorCode.AuthInvalid]: 4,
|
|
316
322
|
[ErrorCode.AuthExpired]: 4,
|
package/dist/jobs/index.js
CHANGED
|
@@ -18,9 +18,14 @@ const OFFSCREEN_CAPS = {
|
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* A {@link StreamRenderer} for a background job (C.28) that captures activity into
|
|
21
|
-
* the job's log
|
|
21
|
+
* the job's log sink and writes NOTHING to any terminal — a job runs
|
|
22
22
|
* non-interactively, off screen, and its foreground session owns the terminal.
|
|
23
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* What was captured here is read back from two different copies: the in-session
|
|
25
|
+
* `/logs <id>` and the Tasks view read the ring buffer while the session lives,
|
|
26
|
+
* and `cruxy logs <id>` reads the persisted file afterwards (#172 item 1).
|
|
27
|
+
* Everything this renderer emits goes through the one `JobManager.log` sink, so
|
|
28
|
+
* both copies see exactly the same lines.
|
|
24
29
|
*
|
|
25
30
|
* Assistant text is accumulated and flushed a line at a time on `endSegment`;
|
|
26
31
|
* committed chrome notes and tool-call completions are captured verbatim. The
|
|
@@ -94,9 +99,9 @@ export class JobLogRenderer {
|
|
|
94
99
|
this.planSteps = steps.map((s) => ({ ...s }));
|
|
95
100
|
}
|
|
96
101
|
/**
|
|
97
|
-
* A job's test run is exactly the kind of outcome
|
|
98
|
-
*
|
|
99
|
-
*
|
|
102
|
+
* A job's test run is exactly the kind of outcome a job log exists to show.
|
|
103
|
+
* Plain text, no theme glyphs, matching this log's `[ok]`/`[fail]` style —
|
|
104
|
+
* and no count this renderer was not given.
|
|
100
105
|
*/
|
|
101
106
|
testResult(report) {
|
|
102
107
|
const counted = report.total !== undefined
|