@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 CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { r as runCli, t as processIo } from "./io-CL4wldEQ.js";
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
- process.stderr.write(`${inspect(thrown)}\n`);
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
- /** Renders the settled outcome; returns the process exit code. */
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-CL4wldEQ.js";
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 ${item.key} suspended`);
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 ${item.key}: ${decision.decision} (${outcome.applied ? "applied" : outcome.reason})`);
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 ? item.key : `${item.key} (${item.prompt})`;
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 '${item.key}' suspended`);
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 '${item.key}': ${outcome.applied ? "applied" : outcome.reason}`);
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
- /** Renders the settled outcome; returns the process exit code. */
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
- /** Parses --args JSON into workflow arguments; undefined when absent. */
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
- return JSON.parse(raw);
686
+ hashRunArgs(parsed);
649
687
  } catch {
650
- throw new ConfigError(`--args is not valid JSON: ${raw}`);
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 '${meta.runId}' predates the args binding; the supplied --args cannot be verified against the original invocation`);
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 '${meta.runId}' without its genesis args (--allow-args-change)`);
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
- io.err(`warning: run '${meta.runId}' recorded args presence but no hash (the genesis args were not JCS-serializable); the supplied --args cannot be verified`);
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
- const supplied = hashRunArgs(args);
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 '${meta.runId}' with changed args (--allow-args-change)`);
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 '${meta.runId}' with args (--allow-args-change)`);
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.24.0",
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.24.0"
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/testing": "1.24.0",
32
- "@rulvar/store-sqlite": "1.24.0",
33
- "@rulvar/planner": "1.24.0",
34
- "@rulvar/evals": "1.24.0",
35
- "@rulvar/plan": "1.24.0"
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"