@corenel/cli 0.4.7 → 0.4.9

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 (2) hide show
  1. package/dist/main.js +184 -5
  2. package/package.json +5 -5
package/dist/main.js CHANGED
@@ -13488,7 +13488,7 @@ var require_filters = __commonJS({
13488
13488
  function striptags(input, preserveLinebreaks) {
13489
13489
  input = normalize(input, "");
13490
13490
  var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>|<!--[\s\S]*?-->/gi;
13491
- var trimmedInput = trim(input.replace(tags, ""));
13491
+ var trimmedInput = trim2(input.replace(tags, ""));
13492
13492
  var res = "";
13493
13493
  if (preserveLinebreaks) {
13494
13494
  res = trimmedInput.replace(/^ +| +$/gm, "").replace(/ +/g, " ").replace(/(\r\n)/g, "\n").replace(/\n\n\n+/g, "\n\n");
@@ -13506,10 +13506,10 @@ var require_filters = __commonJS({
13506
13506
  return r.copySafeness(str, words.join(" "));
13507
13507
  }
13508
13508
  _exports.title = title;
13509
- function trim(str) {
13509
+ function trim2(str) {
13510
13510
  return r.copySafeness(str, str.replace(/^\s*|\s*$/g, ""));
13511
13511
  }
13512
- _exports.trim = trim;
13512
+ _exports.trim = trim2;
13513
13513
  function truncate2(input, length, killwords, end) {
13514
13514
  var orig = input;
13515
13515
  input = normalize(input, "");
@@ -22722,6 +22722,26 @@ function selfDirectionBlock(def) {
22722
22722
  return readConfigSyncRendered(SELF_DIRECTION_BLOCK, { consequence });
22723
22723
  }
22724
22724
 
22725
+ // ../crew/triggerMatch.ts
22726
+ var RUN_STATUS_VALUES = ["ok", "aborted", "budget", "stalled", "policy", "stuck", "max_turns", "error"];
22727
+ var RUN_STATUS_ALIASES = [
22728
+ ["stop", "ok"],
22729
+ ["tool_calls", "ok"],
22730
+ ["function_call", "ok"],
22731
+ ["end_turn", "ok"],
22732
+ ["stop_sequence", "ok"],
22733
+ ["tool_use", "ok"],
22734
+ ["length", "stuck"],
22735
+ ["max_tokens", "stuck"],
22736
+ ["refusal", "policy"],
22737
+ ["content_filter", "policy"],
22738
+ ["interrupted", "aborted"]
22739
+ ];
22740
+ var RUN_STATUS_BY_RAW = new Map([
22741
+ ...RUN_STATUS_VALUES.map((v) => [v, v]),
22742
+ ...RUN_STATUS_ALIASES
22743
+ ]);
22744
+
22725
22745
  // ../crew/memory.ts
22726
22746
  import { makeFactStore } from "@corenel/harness/memory/store";
22727
22747
  function agentMemoryRoot(crewRoot, agentName) {
@@ -23081,6 +23101,12 @@ function helpText(version2) {
23081
23101
  " --allow-mutations allow write/shell/other mutating tools (run/run-agent refuse them",
23082
23102
  " outright without this; interactively it skips the per-command",
23083
23103
  " prompt instead). --allow-shell still works as an alias.",
23104
+ " --max-usd <n> stop `corenel run` once it has spent this much. Needs a model the",
23105
+ " catalog prices -- refused up front rather than metering $0 and",
23106
+ " never firing",
23107
+ " --max-tokens <n> stop `corenel run` after this many tokens",
23108
+ " --max-ms <n> stop `corenel run` after this long",
23109
+ " --on-budget <what> what a passed ceiling does: stop (the default) or warn",
23084
23110
  " --no-tui plain line-by-line mode (automatic when output is piped)",
23085
23111
  " --tui force the full terminal face, and fail if it cannot start",
23086
23112
  " --resume [id] reopen the last conversation, or the one named",
@@ -23103,6 +23129,9 @@ function helpText(version2) {
23103
23129
  " unresolvable --policy, or an unknown command",
23104
23130
  " 3 the policy asked to park an unattended action, but a one-shot run",
23105
23131
  " has no one to park it with -- refused before the turn started",
23132
+ " 4 a --max-usd/--max-tokens/--max-ms ceiling stopped the run. Distinct from",
23133
+ ' 1 so the script that set the cap can tell "I capped this" apart from',
23134
+ ' "the run broke"',
23106
23135
  ""
23107
23136
  ].join("\n");
23108
23137
  }
@@ -23953,6 +23982,104 @@ function currentFaceEnv(argv) {
23953
23982
  };
23954
23983
  }
23955
23984
 
23985
+ // src/budgetFlags.ts
23986
+ var BudgetFlagError = class extends Error {
23987
+ constructor(flagName, detail) {
23988
+ super(`${flagName} ${detail}`);
23989
+ this.flagName = flagName;
23990
+ this.name = "BudgetFlagError";
23991
+ }
23992
+ };
23993
+ var CEILINGS = [
23994
+ { name: "max-usd", key: "maxUsd", unit: "an amount in dollars, e.g. --max-usd 2.50" },
23995
+ { name: "max-tokens", key: "maxTokens", unit: "a token count, e.g. --max-tokens 100000" },
23996
+ { name: "max-ms", key: "maxMs", unit: "a duration in milliseconds, e.g. --max-ms 600000" }
23997
+ ];
23998
+ function ceiling(argv, name, unit) {
23999
+ if (!hasValueFlag(argv, name)) return void 0;
24000
+ const raw = flag(argv, name, "");
24001
+ const n = Number(raw);
24002
+ if (!Number.isFinite(n)) throw new BudgetFlagError(`--${name}`, `needs ${unit} -- got "${raw}"`);
24003
+ if (n <= 0) throw new BudgetFlagError(`--${name}`, `must be greater than zero -- got "${raw}"`);
24004
+ return n;
24005
+ }
24006
+ var ACTIONS = ["stop", "warn"];
24007
+ function breachAction(argv) {
24008
+ if (!hasValueFlag(argv, "on-budget")) return void 0;
24009
+ const raw = flag(argv, "on-budget", "");
24010
+ if (raw === "ask") {
24011
+ throw new BudgetFlagError("--on-budget", 'cannot be "ask": a one-shot run has nobody to ask. Use stop or warn.');
24012
+ }
24013
+ if (raw === "degrade") {
24014
+ throw new BudgetFlagError("--on-budget", 'cannot be "degrade": `run` has no cheaper-model picker. Use stop or warn.');
24015
+ }
24016
+ if (!ACTIONS.includes(raw)) {
24017
+ throw new BudgetFlagError("--on-budget", `must be stop or warn -- got "${raw}"`);
24018
+ }
24019
+ return raw;
24020
+ }
24021
+ function budgetConstraintsFrom(argv) {
24022
+ const limits = {};
24023
+ for (const c of CEILINGS) {
24024
+ const v = ceiling(argv, c.name, c.unit);
24025
+ if (v !== void 0) limits[c.key] = v;
24026
+ }
24027
+ const action = breachAction(argv);
24028
+ const hasLimit = Object.keys(limits).length > 0;
24029
+ if (!hasLimit) {
24030
+ if (action !== void 0) {
24031
+ throw new BudgetFlagError("--on-budget", "needs a budget to act on -- add --max-usd, --max-tokens or --max-ms.");
24032
+ }
24033
+ return void 0;
24034
+ }
24035
+ return { ...limits, onBreach: action ?? "stop" };
24036
+ }
24037
+
24038
+ // src/runBudget.ts
24039
+ import { BudgetMeter as BudgetMeter2 } from "@corenel/harness/core/budget";
24040
+ import { priceUsageWith, pricedForUsd } from "@corenel/harness/core/pricing";
24041
+ var UnpriceableBudgetError = class extends Error {
24042
+ constructor(model) {
24043
+ super(`--max-usd cannot be enforced: no price is known for model "${model}", so spend would always read $0. Use --max-tokens or --max-ms instead, or run a model the catalog prices.`);
24044
+ this.model = model;
24045
+ this.name = "UnpriceableBudgetError";
24046
+ }
24047
+ };
24048
+ function makeRunBudget(deps) {
24049
+ const { constraints, rates, model, onStop, onWarn } = deps;
24050
+ if (constraints.maxUsd != null && !pricedForUsd(rates)) throw new UnpriceableBudgetError(model);
24051
+ const meter = new BudgetMeter2(constraints, deps.now ?? (() => Date.now()));
24052
+ let fired = false;
24053
+ return {
24054
+ onUsage(usage) {
24055
+ if (fired) return;
24056
+ meter.add({
24057
+ inTok: usage.promptTokens,
24058
+ outTok: usage.completionTokens,
24059
+ // `?? 0` is safe ONLY because an unenforceable usd cap was refused
24060
+ // above: with no maxUsd, cost is irrelevant to every other ceiling.
24061
+ cost: priceUsageWith(usage, rates) ?? 0
24062
+ });
24063
+ const breach = meter.breach();
24064
+ if (!breach) return;
24065
+ fired = true;
24066
+ const message = describeBreach(breach);
24067
+ if (constraints.onBreach === "warn") onWarn(message);
24068
+ else onStop(message);
24069
+ }
24070
+ };
24071
+ }
24072
+ function describeBreach(b) {
24073
+ return `budget ${b.kind} ${trim(b.spent)} > ${trim(b.limit)}`;
24074
+ }
24075
+ function trim(v) {
24076
+ if (Number.isInteger(v)) return String(v);
24077
+ return v.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
24078
+ }
24079
+
24080
+ // src/cli.ts
24081
+ import { hydratePrices } from "@corenel/harness/core/pricing";
24082
+
23956
24083
  // src/runOutcome.ts
23957
24084
  function isFailedTurn(outcome) {
23958
24085
  if (outcome.text !== "") return false;
@@ -24130,6 +24257,7 @@ async function resolvePolicy(opts) {
24130
24257
 
24131
24258
  // src/cli.ts
24132
24259
  var EXIT_POLICY_PARK_REFUSED = 3;
24260
+ var EXIT_BUDGET_EXCEEDED = 4;
24133
24261
  async function agentSetup(argv) {
24134
24262
  setGatewayBase2(await apiBase(flag(argv, "base", "")));
24135
24263
  const token = await nodeAuthToken().getToken();
@@ -24182,12 +24310,13 @@ async function agentSetup(argv) {
24182
24310
  async function run(argv) {
24183
24311
  const prompt = promptOf(argv);
24184
24312
  if (!prompt) {
24185
- process.stderr.write('usage: corenel run "<prompt>" [--model M] [--base URL] [--session ID] [--allow-mutations]\n');
24313
+ process.stderr.write('usage: corenel run "<prompt>" [--model M] [--base URL] [--session ID] [--allow-mutations]\n [--max-usd N] [--max-tokens N] [--max-ms N] [--on-budget stop|warn]\n');
24186
24314
  process.exit(2);
24187
24315
  }
24316
+ const budget = budgetConstraintsFrom(argv);
24188
24317
  const session = flag(argv, "session", "");
24189
24318
  if (session) {
24190
- if (hasAllowMutationsFlag(argv) || hasValueFlag(argv, "model") || hasValueFlag(argv, "base")) {
24319
+ if (hasAllowMutationsFlag(argv) || hasValueFlag(argv, "model") || hasValueFlag(argv, "base") || budget) {
24191
24320
  process.stderr.write("corenel: note: --session ignores every other flag on this line -- the sandboxed run uses its own defaults.\n");
24192
24321
  }
24193
24322
  const ext = findExtension("sessions");
@@ -24202,6 +24331,26 @@ async function run(argv) {
24202
24331
  }
24203
24332
  async function runOnce() {
24204
24333
  const ac = new AbortController();
24334
+ let budgetStopped = false;
24335
+ let meter = null;
24336
+ if (budget) {
24337
+ const rates = budget.maxUsd != null ? (await hydratePrices(await apiBase(flag(argv, "base", "")), nodeAuthToken().getToken)).get(model) ?? null : null;
24338
+ meter = makeRunBudget({
24339
+ constraints: budget,
24340
+ rates,
24341
+ model,
24342
+ onStop: (m) => {
24343
+ budgetStopped = true;
24344
+ process.stderr.write(`corenel: ${m} -- stopping the run.
24345
+ `);
24346
+ ac.abort();
24347
+ },
24348
+ onWarn: (m) => {
24349
+ process.stderr.write(`corenel: ${m} -- continuing, because --on-budget is warn.
24350
+ `);
24351
+ }
24352
+ });
24353
+ }
24205
24354
  const allowMutations = hasAllowMutationsFlag(argv);
24206
24355
  const unattended = policy.unattended ?? "deny";
24207
24356
  const unattendedReasons = [];
@@ -24240,10 +24389,17 @@ async function run(argv) {
24240
24389
  spawn: { tools, client, defaultModel: model, permission }
24241
24390
  }),
24242
24391
  signal: ac.signal,
24392
+ // The meter is fed from the SAME event stream, one `usage` per model call.
24243
24393
  onEvent: (e) => {
24244
24394
  if (e.type === "error") lastError = e.message;
24395
+ else if (e.type === "usage") meter?.onUsage(e.usage);
24245
24396
  }
24246
24397
  });
24398
+ if (budgetStopped) {
24399
+ process.stderr.write("corenel: the run stopped at its budget.\n");
24400
+ process.exitCode = EXIT_BUDGET_EXCEEDED;
24401
+ return;
24402
+ }
24247
24403
  if (isFailedTurn({ stopReason: result.stopReason, text: result.text, errorEmitted: lastError !== "" })) {
24248
24404
  process.stderr.write(`corenel: ${friendlyError(lastError || `the run ended without an answer (${result.stopReason})`)}
24249
24405
  `);
@@ -24263,6 +24419,12 @@ async function interactive(argv) {
24263
24419
  async function runInteractive() {
24264
24420
  const approveAll = hasAllowMutationsFlag(argv);
24265
24421
  const cwd = process.cwd();
24422
+ let modelRates = null;
24423
+ const priceBase = await apiBase(flag(argv, "base", ""));
24424
+ void hydratePrices(priceBase, nodeAuthToken().getToken).then((table) => {
24425
+ modelRates = table.get(model) ?? null;
24426
+ }).catch(() => {
24427
+ });
24266
24428
  const files = new NodeFileService(cwd);
24267
24429
  const persistence = createSessionPersistence(files);
24268
24430
  const toolDefs = tools.map((t) => ({ name: t.name, description: t.description }));
@@ -24296,6 +24458,7 @@ async function interactive(argv) {
24296
24458
  process.exitCode = await runTui({
24297
24459
  cwd,
24298
24460
  model,
24461
+ rates: () => modelRates,
24299
24462
  system: systemSource,
24300
24463
  client,
24301
24464
  tools,
@@ -24330,6 +24493,7 @@ async function interactive(argv) {
24330
24493
  process.exitCode = await runRepl({
24331
24494
  client,
24332
24495
  model,
24496
+ rates: () => modelRates,
24333
24497
  system: systemSource,
24334
24498
  tools,
24335
24499
  // The readline face attaches its own PermissionService too (repl.ts) and
@@ -24452,6 +24616,20 @@ Logged in. Token saved to ${authTokenPath()}
24452
24616
  process.exitCode = 2;
24453
24617
  return;
24454
24618
  }
24619
+ if (e instanceof BudgetFlagError) {
24620
+ process.stderr.write(`corenel: ${e.message}
24621
+
24622
+ `);
24623
+ process.stderr.write(helpText(version()));
24624
+ process.exitCode = 2;
24625
+ return;
24626
+ }
24627
+ if (e instanceof UnpriceableBudgetError) {
24628
+ process.stderr.write(`corenel: ${e.message}
24629
+ `);
24630
+ process.exitCode = 2;
24631
+ return;
24632
+ }
24455
24633
  if (e instanceof InvalidApiBaseError) {
24456
24634
  process.stderr.write(`corenel: ${e.message}
24457
24635
  `);
@@ -24483,5 +24661,6 @@ main().catch((e) => {
24483
24661
  bail.unref();
24484
24662
  });
24485
24663
  export {
24664
+ EXIT_BUDGET_EXCEEDED,
24486
24665
  EXIT_POLICY_PARK_REFUSED
24487
24666
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "type": "module",
5
5
  "description": "Corenel CLI — runs the harness in node, in-proc, no transport. The headless proof the kernel is host-agnostic. (corenel run/ask/chat/login + start --sidecar to follow.)",
6
6
  "bin": {
@@ -9,9 +9,9 @@
9
9
  "dependencies": {
10
10
  "openai": "^4.77.0",
11
11
  "ws": "^8.18.0",
12
- "@corenel/harness": "0.7.2",
13
- "@corenel/protocol": "0.7.1",
14
- "@corenel/term": "0.2.6"
12
+ "@corenel/term": "0.2.8",
13
+ "@corenel/protocol": "0.7.3",
14
+ "@corenel/harness": "0.7.4"
15
15
  },
16
16
  "devDependencies": {
17
17
  "esbuild": "^0.21.5",
@@ -20,7 +20,7 @@
20
20
  "tsx": "^4.19.2",
21
21
  "typescript": "^5.9.3",
22
22
  "@corenel/crew": "0.0.0",
23
- "@corenel/tools-node": "0.3.4"
23
+ "@corenel/tools-node": "0.3.6"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"