@rulvar/cli 1.24.0 → 1.25.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/cli.js +4 -2
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/dist/{io-CL4wldEQ.js → io-CAXfPWq7.js} +73 -27
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as runCli, t as processIo } from "./io-
|
|
2
|
+
import { r as runCli, t as processIo } from "./io-CAXfPWq7.js";
|
|
3
|
+
import { sanitizeTerminalText } from "@rulvar/core";
|
|
3
4
|
import { inspect } from "node:util";
|
|
4
5
|
//#region src/cli.ts
|
|
5
6
|
/**
|
|
@@ -11,7 +12,8 @@ runCli(process.argv.slice(2), {
|
|
|
11
12
|
}).then((code) => {
|
|
12
13
|
process.exitCode = code;
|
|
13
14
|
}, (thrown) => {
|
|
14
|
-
|
|
15
|
+
const rendered = inspect(thrown).split("\n").map(sanitizeTerminalText).join("\n");
|
|
16
|
+
process.stderr.write(`${rendered}\n`);
|
|
15
17
|
process.exitCode = 1;
|
|
16
18
|
});
|
|
17
19
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -142,7 +142,13 @@ declare function driveRun(options: {
|
|
|
142
142
|
io: CliIo; /** Original run arguments: not journaled in v1, the host re-supplies them. */
|
|
143
143
|
args?: unknown;
|
|
144
144
|
}): Promise<RunOutcome<unknown>>;
|
|
145
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Renders the settled outcome; returns the process exit code. Error
|
|
147
|
+
* messages, suspension keys, model refs, and phase names originate from
|
|
148
|
+
* providers, tools, and workflow authors, so each is sanitized before
|
|
149
|
+
* it reaches a terminal line, matching the TUI renderer (v1.24.1 review
|
|
150
|
+
* P2-1). Values print as JSON, which escapes control bytes on its own.
|
|
151
|
+
*/
|
|
146
152
|
declare function reportOutcome(outcome: RunOutcome<unknown>, io: CliIo): number;
|
|
147
153
|
//#endregion
|
|
148
154
|
//#region src/server.d.ts
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-
|
|
1
|
+
import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-CAXfPWq7.js";
|
|
2
2
|
import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHeldError, Replayer, RulvarError, buildDeriverRegistry, costReportFromJournal, maskSecrets, normalizeEntry, scanJournalCompatibility, validateSchemaSpec } from "@rulvar/core";
|
|
3
3
|
//#region src/server.ts
|
|
4
4
|
/**
|
|
@@ -234,6 +234,13 @@ function attachProgress(handle, io) {
|
|
|
234
234
|
}
|
|
235
235
|
//#endregion
|
|
236
236
|
//#region src/drive.ts
|
|
237
|
+
/**
|
|
238
|
+
* The run/suspend/resolve/resume loop shared by `rulvar run` and
|
|
239
|
+
* `rulvar resume` (the CLI performs interactive resolution of suspended
|
|
240
|
+
* approvals and external inputs). Prompts read
|
|
241
|
+
* one line per pending suspension; EOF leaves the run suspended with a
|
|
242
|
+
* notice, never an error.
|
|
243
|
+
*/
|
|
237
244
|
const APPROVAL_PREFIX = "approval:";
|
|
238
245
|
/** Parses an approval answer; undefined = unusable input. */
|
|
239
246
|
function approvalDecision(answer) {
|
|
@@ -259,31 +266,32 @@ function approvalDecision(answer) {
|
|
|
259
266
|
async function resolvePending(handle, pending, io) {
|
|
260
267
|
let applied = 0;
|
|
261
268
|
for (const item of pending) {
|
|
269
|
+
const keyRef = sanitizeTerminalText(item.key);
|
|
262
270
|
if (item.key.startsWith(APPROVAL_PREFIX)) {
|
|
263
|
-
const answer = await io.prompt(`approve '${item.prompt ?? item.key}'? [allow/deny]`);
|
|
271
|
+
const answer = await io.prompt(`approve '${sanitizeTerminalText(item.prompt ?? item.key)}'? [allow/deny]`);
|
|
264
272
|
if (answer === void 0) return applied;
|
|
265
273
|
const decision = approvalDecision(answer);
|
|
266
274
|
if (decision === void 0) {
|
|
267
|
-
io.err(`unrecognized answer '${answer.trim()}'; leaving ${
|
|
275
|
+
io.err(`unrecognized answer '${sanitizeTerminalText(answer.trim())}'; leaving ${keyRef} suspended`);
|
|
268
276
|
continue;
|
|
269
277
|
}
|
|
270
278
|
const outcome = await handle.resolveExternal(item.key, decision);
|
|
271
|
-
io.err(`approval ${
|
|
279
|
+
io.err(`approval ${keyRef}: ${decision.decision} (${outcome.applied ? "applied" : sanitizeTerminalText(outcome.reason)})`);
|
|
272
280
|
if (outcome.applied) applied += 1;
|
|
273
281
|
continue;
|
|
274
282
|
}
|
|
275
|
-
const label = item.prompt === void 0 ?
|
|
283
|
+
const label = item.prompt === void 0 ? keyRef : `${keyRef} (${sanitizeTerminalText(item.prompt)})`;
|
|
276
284
|
const answer = await io.prompt(`value for external '${label}' as JSON:`);
|
|
277
285
|
if (answer === void 0) return applied;
|
|
278
286
|
let value;
|
|
279
287
|
try {
|
|
280
288
|
value = JSON.parse(answer);
|
|
281
289
|
} catch {
|
|
282
|
-
io.err(`not valid JSON; leaving '${
|
|
290
|
+
io.err(`not valid JSON; leaving '${keyRef}' suspended`);
|
|
283
291
|
continue;
|
|
284
292
|
}
|
|
285
293
|
const outcome = await handle.resolveExternal(item.key, value);
|
|
286
|
-
io.err(`external '${
|
|
294
|
+
io.err(`external '${keyRef}': ${outcome.applied ? "applied" : sanitizeTerminalText(outcome.reason)}`);
|
|
287
295
|
if (outcome.applied) applied += 1;
|
|
288
296
|
}
|
|
289
297
|
return applied;
|
|
@@ -320,29 +328,35 @@ async function reportDryRun(handle, io) {
|
|
|
320
328
|
io.err(` hits: ${preview.hits} misses: ${preview.misses} reruns: ${preview.reruns} skipped: ${preview.skipped}`);
|
|
321
329
|
io.err(preview.orphaned.length === 0 ? " orphaned effect roots: none" : ` orphaned effect roots (entryRefs): ${preview.orphaned.join(", ")}`);
|
|
322
330
|
if (preview.invalidResolutions.length === 0) io.err(" invalid resolutions: none");
|
|
323
|
-
else for (const invalid of preview.invalidResolutions) io.err(` invalid resolution at seq ${invalid.seq}: ${invalid.detail}`);
|
|
331
|
+
else for (const invalid of preview.invalidResolutions) io.err(` invalid resolution at seq ${invalid.seq}: ${sanitizeTerminalText(invalid.detail)}`);
|
|
324
332
|
if (outcome.error?.code === "journal_miss") {
|
|
325
|
-
io.err(` stopped at the first would-be-live call: ${outcome.error.message}`);
|
|
333
|
+
io.err(` stopped at the first would-be-live call: ${sanitizeTerminalText(outcome.error.message)}`);
|
|
326
334
|
io.err(" a real resume would perform new paid work from this point");
|
|
327
335
|
return 0;
|
|
328
336
|
}
|
|
329
337
|
io.err(` would settle: ${outcome.status}`);
|
|
330
|
-
if (outcome.error !== void 0) io.err(` error: ${outcome.error.message}`);
|
|
331
|
-
for (const pending of outcome.pending) io.err(` pending: ${pending.key} (entry ${pending.entryRef})`);
|
|
338
|
+
if (outcome.error !== void 0) io.err(` error: ${sanitizeTerminalText(outcome.error.message)}`);
|
|
339
|
+
for (const pending of outcome.pending) io.err(` pending: ${sanitizeTerminalText(pending.key)} (entry ${pending.entryRef})`);
|
|
332
340
|
if (outcome.value !== void 0) io.out(JSON.stringify(outcome.value, null, 2));
|
|
333
341
|
return 0;
|
|
334
342
|
}
|
|
335
|
-
/**
|
|
343
|
+
/**
|
|
344
|
+
* Renders the settled outcome; returns the process exit code. Error
|
|
345
|
+
* messages, suspension keys, model refs, and phase names originate from
|
|
346
|
+
* providers, tools, and workflow authors, so each is sanitized before
|
|
347
|
+
* it reaches a terminal line, matching the TUI renderer (v1.24.1 review
|
|
348
|
+
* P2-1). Values print as JSON, which escapes control bytes on its own.
|
|
349
|
+
*/
|
|
336
350
|
function reportOutcome(outcome, io) {
|
|
337
351
|
io.err(`status: ${outcome.status}`);
|
|
338
352
|
if (outcome.value !== void 0) io.out(JSON.stringify(outcome.value, null, 2));
|
|
339
|
-
if (outcome.error !== void 0) io.err(`error: ${outcome.error.message}`);
|
|
353
|
+
if (outcome.error !== void 0) io.err(`error: ${sanitizeTerminalText(outcome.error.message)}`);
|
|
340
354
|
if (outcome.dropped.length > 0) io.err(`dropped: ${outcome.dropped.length} item(s)`);
|
|
341
|
-
for (const pending of outcome.pending) io.err(`pending: ${pending.key} (entry ${pending.entryRef})`);
|
|
355
|
+
for (const pending of outcome.pending) io.err(`pending: ${sanitizeTerminalText(pending.key)} (entry ${pending.entryRef})`);
|
|
342
356
|
io.err(`cost: $${outcome.cost.totalUsd.toFixed(4)}`);
|
|
343
|
-
for (const [model, usd] of Object.entries(outcome.cost.byModel)) io.err(` by model ${model}: $${usd.toFixed(4)}`);
|
|
344
|
-
for (const [phase, usd] of Object.entries(outcome.cost.byPhase)) if (phase !== "") io.err(` by phase ${phase}: $${usd.toFixed(4)}`);
|
|
345
|
-
if (outcome.cost.unpriced.length > 0) io.err(`unpriced models: ${outcome.cost.unpriced.map((u) => u.model).join(", ")}`);
|
|
357
|
+
for (const [model, usd] of Object.entries(outcome.cost.byModel)) io.err(` by model ${sanitizeTerminalText(model)}: $${usd.toFixed(4)}`);
|
|
358
|
+
for (const [phase, usd] of Object.entries(outcome.cost.byPhase)) if (phase !== "") io.err(` by phase ${sanitizeTerminalText(phase)}: $${usd.toFixed(4)}`);
|
|
359
|
+
if (outcome.cost.unpriced.length > 0) io.err(`unpriced models: ${outcome.cost.unpriced.map((u) => sanitizeTerminalText(u.model)).join(", ")}`);
|
|
346
360
|
switch (outcome.status) {
|
|
347
361
|
case "ok":
|
|
348
362
|
case "suspended": return 0;
|
|
@@ -641,14 +655,39 @@ async function loadCompanion(loading, specifier, command, missingMessage) {
|
|
|
641
655
|
throw new Error(`${command}: ${specifier} is installed but failed to load; the cause below is a defect in the installed package or its dependencies, not a missing install`, { cause: error });
|
|
642
656
|
}
|
|
643
657
|
}
|
|
644
|
-
/**
|
|
658
|
+
/**
|
|
659
|
+
* Parses --args JSON into workflow arguments; undefined when absent.
|
|
660
|
+
*
|
|
661
|
+
* CLI args must be representable in canonical JCS, i.e. finite JSON. A
|
|
662
|
+
* numeric literal that overflows JavaScript's finite range parses to
|
|
663
|
+
* Infinity, which `hashRunArgs` cannot canonicalize, so genesis would
|
|
664
|
+
* record `argsProvided` WITHOUT a hash and the resume gate would soften
|
|
665
|
+
* to an unverifiable warning that lets changed args through (v1.24.0
|
|
666
|
+
* review P2-1). A CLI value always arrives as JSON text, so it can
|
|
667
|
+
* always be canonicalized; reject the non-finite case here, before any
|
|
668
|
+
* config, store, or adapter loads, instead of letting it defeat the gate
|
|
669
|
+
* later. In-process hosts keep the wider engine contract (functions,
|
|
670
|
+
* BigInt, cycles record presence without a hash); the CLI does not need
|
|
671
|
+
* it.
|
|
672
|
+
*
|
|
673
|
+
* Diagnostics name the failure class and the way out but never echo the
|
|
674
|
+
* value: workflow args may carry private data, and stderr routinely
|
|
675
|
+
* lands in CI logs (v1.24.1 review P2-1).
|
|
676
|
+
*/
|
|
645
677
|
function parseArgsJson(raw) {
|
|
646
678
|
if (raw === void 0) return;
|
|
679
|
+
let parsed;
|
|
680
|
+
try {
|
|
681
|
+
parsed = JSON.parse(raw);
|
|
682
|
+
} catch {
|
|
683
|
+
throw new ConfigError("--args is not valid JSON; check the JSON syntax and shell quoting (the value is withheld from diagnostics: workflow args may carry private data)");
|
|
684
|
+
}
|
|
647
685
|
try {
|
|
648
|
-
|
|
686
|
+
hashRunArgs(parsed);
|
|
649
687
|
} catch {
|
|
650
|
-
throw new ConfigError(
|
|
688
|
+
throw new ConfigError("--args is not representable as canonical JSON: a numeric value overflows JavaScript's finite range (e.g. 1e400 parses to Infinity). Supply finite JSON so the run's args binding can be hashed and later verified on resume (the value is withheld from diagnostics: workflow args may carry private data)");
|
|
651
689
|
}
|
|
690
|
+
return parsed;
|
|
652
691
|
}
|
|
653
692
|
async function runCommand(argv, context) {
|
|
654
693
|
const parsed = parseCommand(GRAMMAR.run, argv);
|
|
@@ -690,31 +729,38 @@ async function runCommand(argv, context) {
|
|
|
690
729
|
*/
|
|
691
730
|
function enforceArgsBinding(input) {
|
|
692
731
|
const { meta, argsGiven, args, allowChange, io } = input;
|
|
732
|
+
const runRef = sanitizeTerminalText(meta.runId);
|
|
693
733
|
if (meta.argsProvided === void 0) {
|
|
694
734
|
if (!argsGiven && !allowChange) throw new ConfigError(`run '${meta.runId}' predates the args binding (rulvar < 1.24.0), so the CLI cannot tell whether it was started with --args, and resuming without them silently changes the logical run if any were used at start. Re-supply the original --args, or acknowledge explicitly with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
|
|
695
|
-
if (argsGiven) io.err(`warning: run '${
|
|
735
|
+
if (argsGiven) io.err(`warning: run '${runRef}' predates the args binding; the supplied --args cannot be verified against the original invocation`);
|
|
696
736
|
return;
|
|
697
737
|
}
|
|
698
738
|
if (meta.argsProvided) {
|
|
699
739
|
if (!argsGiven) {
|
|
700
740
|
if (!allowChange) throw new ConfigError(`run '${meta.runId}' was started WITH args, but this resume supplies none; the workflow would see undefined and every args-dependent call would become new paid work instead of a replay. Re-supply the original --args, or force the change with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
|
|
701
|
-
io.err(`warning: resuming '${
|
|
741
|
+
io.err(`warning: resuming '${runRef}' without its genesis args (--allow-args-change)`);
|
|
702
742
|
return;
|
|
703
743
|
}
|
|
704
744
|
if (meta.argsHash === void 0) {
|
|
705
|
-
|
|
745
|
+
if (!allowChange) throw new ConfigError(`run '${meta.runId}' started WITH args but recorded no verifiable hash (the genesis args were not JCS-serializable), so the CLI cannot confirm the supplied --args match the original; resuming risks silently changing the logical run and re-paying every args-dependent call. Force deliberately with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
|
|
746
|
+
io.err(`warning: run '${runRef}' recorded args presence but no hash (genesis args not JCS-serializable); the supplied --args cannot be verified (--allow-args-change)`);
|
|
706
747
|
return;
|
|
707
748
|
}
|
|
708
|
-
|
|
749
|
+
let supplied;
|
|
750
|
+
try {
|
|
751
|
+
supplied = hashRunArgs(args);
|
|
752
|
+
} catch {
|
|
753
|
+
throw new ConfigError(`--args cannot be canonicalized to compare against run '${meta.runId}' (a numeric value overflows the finite range, or the value is otherwise not canonical JSON); supply finite JSON, or force the resume with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
|
|
754
|
+
}
|
|
709
755
|
if (supplied !== meta.argsHash) {
|
|
710
756
|
if (!allowChange) throw new ConfigError(`--args does not match the args run '${meta.runId}' was started with (recorded hash ${meta.argsHash.slice(0, 12)}, supplied ${supplied?.slice(0, 12) ?? "none"}); changed args silently change the logical run and re-pay every args-dependent call. Force deliberately with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
|
|
711
|
-
io.err(`warning: resuming '${
|
|
757
|
+
io.err(`warning: resuming '${runRef}' with changed args (--allow-args-change)`);
|
|
712
758
|
}
|
|
713
759
|
return;
|
|
714
760
|
}
|
|
715
761
|
if (argsGiven) {
|
|
716
762
|
if (!allowChange) throw new ConfigError(`run '${meta.runId}' was started WITHOUT args, but this resume supplies some; added args silently change the logical run. Drop --args, or force the change with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
|
|
717
|
-
io.err(`warning: resuming no-args run '${
|
|
763
|
+
io.err(`warning: resuming no-args run '${runRef}' with args (--allow-args-change)`);
|
|
718
764
|
}
|
|
719
765
|
}
|
|
720
766
|
async function resumeCommand(argv, context) {
|
|
@@ -845,7 +891,7 @@ async function planCommand(argv, context) {
|
|
|
845
891
|
});
|
|
846
892
|
const planned = await plannerModule.plan(assembled.engine, goal, planningBudgetUsd === void 0 ? void 0 : { run: { budgetUsd: planningBudgetUsd } });
|
|
847
893
|
context.io.err(`plan: accepted with ${String(planned.lint.length)} advisory diagnostic(s)`);
|
|
848
|
-
for (const diagnostic of planned.lint) context.io.err(` ${diagnostic.ruleId}: ${diagnostic.message}`);
|
|
894
|
+
for (const diagnostic of planned.lint) context.io.err(` ${diagnostic.ruleId}: ${sanitizeTerminalText(diagnostic.message)}`);
|
|
849
895
|
if (dryRun) {
|
|
850
896
|
context.io.out(planned.source);
|
|
851
897
|
return 0;
|
|
@@ -1312,7 +1358,7 @@ async function runCli(argv, options) {
|
|
|
1312
1358
|
}
|
|
1313
1359
|
} catch (thrown) {
|
|
1314
1360
|
if (thrown instanceof ConfigError) {
|
|
1315
|
-
options.io.err(`error: ${thrown.message}`);
|
|
1361
|
+
options.io.err(`error: ${sanitizeTerminalText(thrown.message)}`);
|
|
1316
1362
|
return 1;
|
|
1317
1363
|
}
|
|
1318
1364
|
throw thrown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Rulvar shell: run/resume/runs/inspect/plan/kb commands, TUI progress, createServer, createWorker, OTel exporter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,17 +22,17 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rulvar/core": "1.
|
|
25
|
+
"@rulvar/core": "1.25.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.20.0",
|
|
29
29
|
"tsdown": "^0.22.3",
|
|
30
30
|
"typescript": "~6.0.3",
|
|
31
|
-
"@rulvar/
|
|
32
|
-
"@rulvar/
|
|
33
|
-
"@rulvar/
|
|
34
|
-
"@rulvar/
|
|
35
|
-
"@rulvar/
|
|
31
|
+
"@rulvar/planner": "1.25.0",
|
|
32
|
+
"@rulvar/testing": "1.25.0",
|
|
33
|
+
"@rulvar/plan": "1.25.0",
|
|
34
|
+
"@rulvar/store-sqlite": "1.25.0",
|
|
35
|
+
"@rulvar/evals": "1.25.0"
|
|
36
36
|
},
|
|
37
37
|
"bin": {
|
|
38
38
|
"rulvar": "./dist/cli.js"
|