@odla-ai/cli 0.37.0 → 0.38.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/bin.cjs CHANGED
@@ -1322,6 +1322,74 @@ var init_argv = __esm({
1322
1322
  }
1323
1323
  });
1324
1324
 
1325
+ // src/admin-spend.ts
1326
+ async function call(ctx, method, scope) {
1327
+ const url = `${ctx.platformUrl.replace(/\/$/, "")}/registry/platform/spend?scope=${encodeURIComponent(scope)}`;
1328
+ const response2 = await ctx.doFetch(url, {
1329
+ method,
1330
+ headers: { authorization: `Bearer ${ctx.token}` }
1331
+ });
1332
+ const body = await response2.json().catch(() => ({}));
1333
+ if (!response2.ok) {
1334
+ const detail = body.error?.message ?? body.error?.code ?? `registry returned ${response2.status}`;
1335
+ throw new Error(`spend ${method} failed: ${detail} (${response2.status})`);
1336
+ }
1337
+ return body;
1338
+ }
1339
+ async function spendShow(ctx, scope) {
1340
+ const view = await call(ctx, "GET", scope);
1341
+ if (ctx.json) return ctx.out.log(JSON.stringify(view, null, 2));
1342
+ ctx.out.log(`scope: ${view.scope}`);
1343
+ ctx.out.log(`day: ${view.day} (UTC)`);
1344
+ ctx.out.log(
1345
+ `spent: ${money(view.spentUsd)} of ${view.capUsd > 0 ? money(view.capUsd) : "(no cap set)"}`
1346
+ );
1347
+ ctx.out.log(`calls: ${view.calls}`);
1348
+ if (view.unpricedCalls > 0) {
1349
+ ctx.out.log(
1350
+ `unpriced: ${view.unpricedCalls} call(s) had no price, so "spent" is a floor, not the total.`
1351
+ );
1352
+ }
1353
+ if (!view.latched) return ctx.out.log("status: running");
1354
+ ctx.out.log(
1355
+ `status: HALTED \u2014 reached ${money(view.latched.capUsd)} on ${view.latched.day} (${view.latched.reason}), at ${money(view.latched.costUsd)}.`
1356
+ );
1357
+ ctx.out.log("A new day does not clear this. Resume with:");
1358
+ ctx.out.log(` odla-ai admin spend reset ${view.scope}`);
1359
+ }
1360
+ async function spendReset(ctx, scope) {
1361
+ const result = await call(ctx, "DELETE", scope);
1362
+ if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
1363
+ ctx.out.log(result.message);
1364
+ if (result.stillOverCap) {
1365
+ ctx.out.log(
1366
+ ` today: ${money(result.spentUsd)} against a ${money(result.capUsd)} cap.`
1367
+ );
1368
+ }
1369
+ }
1370
+ async function adminSpend(parsed, ctx) {
1371
+ const action2 = parsed.positionals[2];
1372
+ const scope = parsed.positionals[3] ?? stringOpt(parsed.options.scope);
1373
+ if (action2 !== "show" && action2 !== "reset") {
1374
+ throw new Error('unknown spend command. Try "odla-ai admin spend show <scope>".');
1375
+ }
1376
+ if (!scope) {
1377
+ throw new Error(
1378
+ `"admin spend ${action2}" needs a scope, e.g. odla-ai admin spend ${action2} app:my-app:<incarnation>`
1379
+ );
1380
+ }
1381
+ return action2 === "show" ? spendShow(ctx, scope) : spendReset(ctx, scope);
1382
+ }
1383
+ var money;
1384
+ var init_admin_spend = __esm({
1385
+ "src/admin-spend.ts"() {
1386
+ "use strict";
1387
+ init_cjs_shims();
1388
+ init_argv();
1389
+ money = (value2) => `$${value2.toFixed(2)}`;
1390
+ }
1391
+ });
1392
+
1325
1393
  // src/ai-config-validation.ts
1326
1394
  function validateAiConfig(cfg, path) {
1327
1395
  if (cfg.ai === void 0) return;
@@ -2141,6 +2209,31 @@ var init_operator_context = __esm({
2141
2209
  async function adminCommand(parsed, deps = {}) {
2142
2210
  const area = parsed.positionals[1];
2143
2211
  const action2 = parsed.positionals[2];
2212
+ if (area === "spend") {
2213
+ assertArgs(parsed, JSON_OPTIONS, 4);
2214
+ const context2 = await resolveOperatorContext(parsed, { allowMissingConfig: true });
2215
+ const out = deps.stdout ?? console;
2216
+ const doFetch = deps.fetch ?? fetch;
2217
+ const token = await getDeveloperToken(
2218
+ context2.cfg,
2219
+ {
2220
+ configPath: context2.cfg.configPath,
2221
+ token: stringOpt(parsed.options.token),
2222
+ email: stringOpt(parsed.options.email),
2223
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2224
+ openApprovalUrl: deps.openUrl
2225
+ },
2226
+ doFetch,
2227
+ out
2228
+ );
2229
+ return adminSpend(parsed, {
2230
+ platformUrl: context2.platform.value,
2231
+ token,
2232
+ doFetch,
2233
+ json: parsed.options.json === true,
2234
+ out
2235
+ });
2236
+ }
2144
2237
  const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
2145
2238
  const credentials = action2 === "credentials";
2146
2239
  const models = action2 === "models";
@@ -2190,6 +2283,8 @@ var init_admin_command = __esm({
2190
2283
  "use strict";
2191
2284
  init_cjs_shims();
2192
2285
  init_admin_ai();
2286
+ init_admin_spend();
2287
+ init_token();
2193
2288
  init_argv();
2194
2289
  init_operator_context();
2195
2290
  CONTEXT_OPTIONS = ["platform", "config", "context", "token", "open", "email"];
@@ -2265,7 +2360,8 @@ async function fetchIdentity(platformUrl, token, doFetch) {
2265
2360
  email,
2266
2361
  admin: body.admin === true,
2267
2362
  machine,
2268
- scopes
2363
+ scopes,
2364
+ projects: Array.isArray(body.projects) ? body.projects.map(String) : null
2269
2365
  };
2270
2366
  }
2271
2367
  function credentialLabel(identity) {
@@ -2335,6 +2431,13 @@ async function whoamiCommand(parsed, deps = {}) {
2335
2431
  out.log(`credential id: ${identity.credential.id}`);
2336
2432
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
2337
2433
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
2434
+ if (identity.projects === null) {
2435
+ out.log("projects: (this registry does not report project grants)");
2436
+ } else if (identity.projects.length) {
2437
+ out.log(`projects: ${identity.projects.join(", ")}`);
2438
+ } else {
2439
+ out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2440
+ }
2338
2441
  if (!identity.admin) {
2339
2442
  if (identity.scopes.includes("platform:runbook:write")) {
2340
2443
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -6211,17 +6314,17 @@ var init_cli_project = __esm({
6211
6314
  }
6212
6315
  });
6213
6316
 
6214
- // ../harness/dist/chunk-3QP4VDQS.js
6317
+ // ../harness/dist/chunk-LNQNFGQC.js
6215
6318
  var HARNESS_PROTOCOL_VERSION;
6216
- var init_chunk_3QP4VDQS = __esm({
6217
- "../harness/dist/chunk-3QP4VDQS.js"() {
6319
+ var init_chunk_LNQNFGQC = __esm({
6320
+ "../harness/dist/chunk-LNQNFGQC.js"() {
6218
6321
  "use strict";
6219
6322
  init_cjs_shims();
6220
6323
  HARNESS_PROTOCOL_VERSION = 1;
6221
6324
  }
6222
6325
  });
6223
6326
 
6224
- // ../harness/dist/chunk-GKDKIU4P.js
6327
+ // ../harness/dist/chunk-K76I2TCQ.js
6225
6328
  function assertPinnedImage(image) {
6226
6329
  if (!DIGEST_IMAGE.test(image)) throw new TypeError("container image must be pinned by sha256 digest");
6227
6330
  }
@@ -6570,8 +6673,8 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
6570
6673
  }
6571
6674
  }
6572
6675
  var import_child_process, import_fs, import_promises2, import_path, import_process, import_promises3, import_os, import_path2, import_child_process2, import_path3, import_promises4, import_os2, import_path4, import_child_process3, DIGEST_IMAGE, SKIP_WORKSPACE_DIRS, SECRET_WORKSPACE_FILE;
6573
- var init_chunk_GKDKIU4P = __esm({
6574
- "../harness/dist/chunk-GKDKIU4P.js"() {
6676
+ var init_chunk_K76I2TCQ = __esm({
6677
+ "../harness/dist/chunk-K76I2TCQ.js"() {
6575
6678
  "use strict";
6576
6679
  init_cjs_shims();
6577
6680
  import_child_process = require("child_process");
@@ -7569,7 +7672,7 @@ var init_code2 = __esm({
7569
7672
  }
7570
7673
  });
7571
7674
 
7572
- // ../harness/dist/chunk-ANNX7VGK.js
7675
+ // ../harness/dist/chunk-UVGZHNLW.js
7573
7676
  async function digestStagedWorkspace(root, limits) {
7574
7677
  const files = [];
7575
7678
  const walk = async (directory) => {
@@ -7647,7 +7750,7 @@ function createCodeRuntimeControlClient(options) {
7647
7750
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
7648
7751
  }
7649
7752
  const request3 = options.fetch ?? fetch;
7650
- const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
7753
+ const call4 = async (path, body, timeoutMs = requestTimeoutMs) => {
7651
7754
  const timeout = AbortSignal.timeout(timeoutMs);
7652
7755
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
7653
7756
  let response2;
@@ -7677,17 +7780,17 @@ function createCodeRuntimeControlClient(options) {
7677
7780
  return {
7678
7781
  heartbeat: async (version, capabilities) => {
7679
7782
  validateHeartbeat(version, capabilities);
7680
- return parseSnapshot(await call2("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7783
+ return parseSnapshot(await call4("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7681
7784
  },
7682
7785
  acknowledge: async (commandId, result) => {
7683
7786
  if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
7684
- await call2(`/registry/code/runtime/commands/${commandId}/ack`, result);
7787
+ await call4(`/registry/code/runtime/commands/${commandId}/ack`, result);
7685
7788
  },
7686
7789
  source: async (sessionId) => parseSource(
7687
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7790
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7688
7791
  ),
7689
7792
  infer: async (sessionId, inference) => {
7690
- const value2 = record5(await call2(
7793
+ const value2 = record5(await call4(
7691
7794
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
7692
7795
  inference,
7693
7796
  modelRequestTimeoutMs
@@ -7698,11 +7801,11 @@ function createCodeRuntimeControlClient(options) {
7698
7801
  return value2;
7699
7802
  },
7700
7803
  review: async (sessionId, review) => parseReview(
7701
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7804
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7702
7805
  ),
7703
7806
  submitCandidate: async (sessionId, checkpointId, verification) => {
7704
7807
  if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
7705
- return parseCandidate(await call2(
7808
+ return parseCandidate(await call4(
7706
7809
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
7707
7810
  { checkpointId, verification }
7708
7811
  ));
@@ -7712,21 +7815,21 @@ function createCodeRuntimeControlClient(options) {
7712
7815
  if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
7713
7816
  throw new TypeError("invalid Code session event");
7714
7817
  }
7715
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7818
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7716
7819
  },
7717
7820
  recallMemories: async (sessionId, subjects, limit) => {
7718
- const response2 = await call2(
7821
+ const response2 = await call4(
7719
7822
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
7720
7823
  { subjects: [...subjects], limit }
7721
7824
  );
7722
7825
  return Array.isArray(response2.memories) ? response2.memories : [];
7723
7826
  },
7724
7827
  rememberMemory: async (sessionId, memory) => {
7725
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7828
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7726
7829
  },
7727
7830
  reportSessionFailure: async (sessionId, message2) => {
7728
7831
  if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
7729
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7832
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7730
7833
  }
7731
7834
  };
7732
7835
  }
@@ -8512,7 +8615,7 @@ async function materializeCommandWorkspace(input) {
8512
8615
  }
8513
8616
  function codeSkill(opts) {
8514
8617
  let seq = 0;
8515
- const call2 = async (tool, input, signal) => {
8618
+ const call4 = async (tool, input, signal) => {
8516
8619
  const startedAt = Date.now();
8517
8620
  const response2 = await opts.broker.execute(
8518
8621
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
@@ -8534,7 +8637,7 @@ function codeSkill(opts) {
8534
8637
  },
8535
8638
  additionalProperties: false
8536
8639
  },
8537
- handler: (input, ctx) => call2("sandbox.read", input, ctx.signal)
8640
+ handler: (input, ctx) => call4("sandbox.read", input, ctx.signal)
8538
8641
  };
8539
8642
  const applyPatch = {
8540
8643
  name: "odla_apply_git_diff",
@@ -8545,7 +8648,7 @@ function codeSkill(opts) {
8545
8648
  properties: { patch: { type: "string", minLength: 1, maxLength: 262144 } },
8546
8649
  additionalProperties: false
8547
8650
  },
8548
- handler: (input, ctx) => call2("sandbox.apply_patch", input, ctx.signal)
8651
+ handler: (input, ctx) => call4("sandbox.apply_patch", input, ctx.signal)
8549
8652
  };
8550
8653
  const runRecipe = {
8551
8654
  name: "odla_run_recipe",
@@ -8556,7 +8659,7 @@ function codeSkill(opts) {
8556
8659
  properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
8557
8660
  additionalProperties: false
8558
8661
  },
8559
- handler: (input, ctx) => call2("sandbox.run_recipe", input, ctx.signal)
8662
+ handler: (input, ctx) => call4("sandbox.run_recipe", input, ctx.signal)
8560
8663
  };
8561
8664
  const listFiles2 = {
8562
8665
  name: "odla_list",
@@ -8569,7 +8672,7 @@ function codeSkill(opts) {
8569
8672
  },
8570
8673
  additionalProperties: false
8571
8674
  },
8572
- handler: (input, ctx) => call2("sandbox.list", input, ctx.signal)
8675
+ handler: (input, ctx) => call4("sandbox.list", input, ctx.signal)
8573
8676
  };
8574
8677
  const searchFiles = {
8575
8678
  name: "odla_search",
@@ -8585,7 +8688,7 @@ function codeSkill(opts) {
8585
8688
  },
8586
8689
  additionalProperties: false
8587
8690
  },
8588
- handler: (input, ctx) => call2("sandbox.search", input, ctx.signal)
8691
+ handler: (input, ctx) => call4("sandbox.search", input, ctx.signal)
8589
8692
  };
8590
8693
  const graphTool = (name, tool, description, required) => ({
8591
8694
  name,
@@ -8596,7 +8699,7 @@ function codeSkill(opts) {
8596
8699
  properties: { query: { type: "string", maxLength: 512 } },
8597
8700
  additionalProperties: false
8598
8701
  },
8599
- handler: (input, ctx) => call2(tool, input, ctx.signal)
8702
+ handler: (input, ctx) => call4(tool, input, ctx.signal)
8600
8703
  });
8601
8704
  const orientation = [
8602
8705
  graphTool(
@@ -8635,9 +8738,9 @@ async function runCodeAgent(options) {
8635
8738
  lease: options.lease,
8636
8739
  workspaceDir: options.workspaceDir,
8637
8740
  surface,
8638
- onToolCall: (call2) => {
8639
- toolCalls.push(call2);
8640
- options.onToolCall?.(call2);
8741
+ onToolCall: (call4) => {
8742
+ toolCalls.push(call4);
8743
+ options.onToolCall?.(call4);
8641
8744
  }
8642
8745
  });
8643
8746
  const compaction = options.compaction === void 0 ? (0, import_ai4.keepRecentExchanges)({ whenInputTokensExceed: 12e4, keep: 3 }) : options.compaction;
@@ -8722,6 +8825,9 @@ async function handleCodeRuntimeInference(input) {
8722
8825
  call: request3.call
8723
8826
  });
8724
8827
  state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
8828
+ const { costUsd } = response2.receipt;
8829
+ if (costUsd === void 0) state2.costKnown = false;
8830
+ else state2.costUsd += costUsd;
8725
8831
  await input.event({
8726
8832
  type: "usage",
8727
8833
  provider: response2.receipt.provider,
@@ -8731,7 +8837,9 @@ async function handleCodeRuntimeInference(input) {
8731
8837
  durationMs: Date.now() - startedAt,
8732
8838
  interactionId: command.commandId,
8733
8839
  interactionTokens: state2.tokens,
8734
- interactionMaxTokens: metadata2.maxTokensPerInteraction
8840
+ interactionMaxTokens: metadata2.maxTokensPerInteraction,
8841
+ ...costUsd === void 0 ? {} : { costUsd },
8842
+ ...state2.costKnown ? { interactionCostUsd: state2.costUsd } : {}
8735
8843
  }).catch(() => void 0);
8736
8844
  return {
8737
8845
  protocolVersion: HARNESS_PROTOCOL_VERSION,
@@ -9522,10 +9630,16 @@ async function startGoalPursuit(input) {
9522
9630
  attempt: async ({ prompt }) => {
9523
9631
  const result = await input.attempt(prompt);
9524
9632
  return {
9525
- // The runtime charges tokens through the control plane's own
9526
- // per-interaction reservation, so the goal budget bounds ATTEMPTS here
9527
- // and the token ceiling is enforced where the credential lives.
9528
- tokens: 0,
9633
+ // What the attempt actually spent, so the runner's token_budget and
9634
+ // cost_budget checks can be reached. This used to be a hardcoded 0 with
9635
+ // no cost at all, which made maxTokens and maxUsd unreachable while
9636
+ // callers reasonably read them as hard ceilings.
9637
+ //
9638
+ // costUsd is omitted rather than zeroed when any call in the attempt
9639
+ // was unpriced: the runner only enforces a cost budget while the cost
9640
+ // is known, and a zero would make it enforce against a lie.
9641
+ tokens: result.tokens ?? 0,
9642
+ ...result.costUsd === void 0 ? {} : { costUsd: result.costUsd },
9529
9643
  ...result.status === "failed" ? { error: result.error ?? "attempt failed" } : {}
9530
9644
  };
9531
9645
  },
@@ -9554,12 +9668,12 @@ async function appendCodeRuntimeEvent(control, command, event, refs) {
9554
9668
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
9555
9669
  }
9556
9670
  var import_crypto, import_promises5, import_path5, import_child_process4, import_promises6, import_path6, import_child_process5, import_process2, import_crypto2, import_crypto3, import_fs2, import_promises7, import_path7, import_promises8, import_os3, import_path8, import_ai4, import_promises9, import_path9, import_promises10, import_promises11, import_path10, import_crypto4, CODE_RUNTIME_PROTOCOL_VERSION, CodeRuntimeReconciler, CodeRuntimeControlError, record5, invalid, RESERVED, SECRET, PATH, FORBIDDEN, ARTIFACT_PATH, PRIVATE_ARTIFACT_PART, SHA3, DIGEST3, ID3, RULE, DEFAULT_PREFIXES, DEFAULT_SUFFIXES, message, CodeRuntimeCheckpointManager, record22, SOURCE_LIMITS, RESERVED2, SECRET2, V1_SYSTEM_PROMPT, V2_SYSTEM_PROMPT, V3_SYSTEM_PROMPT, SYSTEM_PROMPT_FOR, DEFAULT_MAX_FILES, DEFAULT_MAX_RESULTS, DEFAULT_MAX_FILE_BYTES, DESTINATIONS, READ, LIST, SEARCH, GRAPH, PATCH, RECIPE, cache, shortId, GRAPH_TOOLS, MAX_MEMORY_BODY, POSITIVE, digestRuntimeValue, runtimeErrorMessage, CodePiRuntimeEngine;
9557
- var init_chunk_ANNX7VGK = __esm({
9558
- "../harness/dist/chunk-ANNX7VGK.js"() {
9671
+ var init_chunk_UVGZHNLW = __esm({
9672
+ "../harness/dist/chunk-UVGZHNLW.js"() {
9559
9673
  "use strict";
9560
9674
  init_cjs_shims();
9561
- init_chunk_GKDKIU4P();
9562
- init_chunk_3QP4VDQS();
9675
+ init_chunk_K76I2TCQ();
9676
+ init_chunk_LNQNFGQC();
9563
9677
  import_crypto = require("crypto");
9564
9678
  import_promises5 = require("fs/promises");
9565
9679
  import_path5 = require("path");
@@ -9973,7 +10087,7 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
9973
10087
  recipeAuthorization: this.options.recipeAuthorization
9974
10088
  }, lease, metadata2.role));
9975
10089
  const startedAt = Date.now();
9976
- const interaction = { tokens: 0, noticeEmitted: false };
10090
+ const interaction = { tokens: 0, noticeEmitted: false, costUsd: 0, costKnown: true };
9977
10091
  const inference = createCodeRuntimeInference({
9978
10092
  command,
9979
10093
  metadata: metadata2,
@@ -10011,7 +10125,11 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
10011
10125
  await this.#diagnostic(command, active, detail);
10012
10126
  await this.#failure(command, active, detail);
10013
10127
  }
10014
- return result;
10128
+ return {
10129
+ ...result,
10130
+ tokens: interaction.tokens,
10131
+ ...interaction.costKnown ? { costUsd: interaction.costUsd } : {}
10132
+ };
10015
10133
  }
10016
10134
  /** Report every brokered effect as it starts and finishes. */
10017
10135
  #observed(command, active, broker) {
@@ -10070,8 +10188,8 @@ var init_node = __esm({
10070
10188
  "../harness/dist/node.js"() {
10071
10189
  "use strict";
10072
10190
  init_cjs_shims();
10073
- init_chunk_ANNX7VGK();
10074
- init_chunk_GKDKIU4P();
10191
+ init_chunk_UVGZHNLW();
10192
+ init_chunk_K76I2TCQ();
10075
10193
  MEASURED_PREMIUM = Object.freeze({
10076
10194
  /** 3 racers vs pure depth at equal budget: 21,044 / 7,936. */
10077
10195
  racePerRacer: 0.55,
@@ -11175,6 +11293,8 @@ Usage:
11175
11293
  odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
11176
11294
  odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
11177
11295
  odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
11296
+ odla-ai admin spend show <app:<id>:<incarnation>> [--context <name>] [--json]
11297
+ odla-ai admin spend reset <app:<id>:<incarnation>> [--context <name>] [--json]
11178
11298
  odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
11179
11299
  odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
11180
11300
  odla-ai security plan [--env dev] [--json]
@@ -11269,6 +11389,9 @@ Commands:
11269
11389
  human session connects one); "code grant request|list|approve|revoke" then
11270
11390
  governs unattended access: an agent may request, only a human may approve.
11271
11391
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
11392
+ "admin spend" reads one app's daily inference spend and resumes it
11393
+ after a cap halts it. The cap is per UTC day; exhausting it LATCHES,
11394
+ and a new day does not clear the latch \u2014 resuming is deliberate.
11272
11395
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
11273
11396
  pm Project management (via @odla-ai/pm): Products contain Projects;
11274
11397
  projects contain goals, kanban tasks, decisions, and bugs. Use
@@ -11276,6 +11399,12 @@ Commands:
11276
11399
  pass --app/--project explicitly. Same device-grant auth as "app".
11277
11400
  Status changes and comments post to each item's @odla-ai/chat
11278
11401
  discussion thread.
11402
+ NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
11403
+ approves its complete execution contract, so it needs pm.plan,
11404
+ which no device enrollment or handshake approval can grant. An
11405
+ agent proposes in Backlog (the default); a human owner or a
11406
+ pm.plan agent promotes. This is deliberate, not a permission
11407
+ gap \u2014 it was reported as one.
11279
11408
  bug Intent-first alias for PM bugs. "bug report" writes to
11280
11409
  odla PM; odla product defects do not belong in GitHub Issues.
11281
11410
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -13866,9 +13995,15 @@ async function provisionEnvCredentials(opts) {
13866
13995
  if (o11yToken) {
13867
13996
  opts.stdout.log(`${opts.env}: reusing local o11y ingest token`);
13868
13997
  } else {
13869
- o11yToken = await issueO11yToken(opts);
13870
- credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
13871
- opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
13998
+ o11yToken = await issueO11yToken(opts) ?? void 0;
13999
+ if (o11yToken) {
14000
+ credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
14001
+ opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
14002
+ } else {
14003
+ opts.stdout.log(
14004
+ `${opts.env}: o11y token already exists and its shown-once value is not on this machine \u2014 leaving it alone (a co-owner provisioned this env). Nothing was rotated, and this env's running Worker keeps its token. To replace it deliberately \u2014 which invalidates the token on whatever is deployed \u2014 run "odla-ai provision --rotate-o11y-token --push-secrets".`
14005
+ );
14006
+ }
13872
14007
  }
13873
14008
  }
13874
14009
  return save(opts, credentials, tenantId, {
@@ -13922,11 +14057,7 @@ async function issueO11yToken(opts) {
13922
14057
  `${opts.cfg.platformUrl}/o11y/${encodeURIComponent(opts.cfg.app.id)}/token${suffix}?env=${encodeURIComponent(opts.env)}`,
13923
14058
  { method: "POST", headers: { authorization: `Bearer ${opts.developerToken}` } }
13924
14059
  );
13925
- if (res.status === 409 && !opts.rotateO11y) {
13926
- throw new Error(
13927
- `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --push-secrets" to install a separate runtime credential without rotating siblings`
13928
- );
13929
- }
14060
+ if (res.status === 409 && !opts.rotateO11y) return null;
13930
14061
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
13931
14062
  const body = await res.json();
13932
14063
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
@@ -14698,7 +14829,7 @@ var init_device_command = __esm({
14698
14829
  });
14699
14830
 
14700
14831
  // src/runbook-actions.ts
14701
- async function call(ctx, method, path, body) {
14832
+ async function call2(ctx, method, path, body) {
14702
14833
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
14703
14834
  method,
14704
14835
  headers: {
@@ -14720,7 +14851,7 @@ async function call(ctx, method, path, body) {
14720
14851
  throw new Error(message2);
14721
14852
  }
14722
14853
  async function bySlug(ctx, slug) {
14723
- const page2 = await call(
14854
+ const page2 = await call2(
14724
14855
  ctx,
14725
14856
  "GET",
14726
14857
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
@@ -14729,7 +14860,7 @@ async function bySlug(ctx, slug) {
14729
14860
  if (filtered) return filtered;
14730
14861
  const limit = 100;
14731
14862
  for (let offset = 0; ; offset += limit) {
14732
- const fallback = await call(
14863
+ const fallback = await call2(
14733
14864
  ctx,
14734
14865
  "GET",
14735
14866
  `/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
@@ -14749,7 +14880,7 @@ async function runbookList(ctx, all, query) {
14749
14880
  const params = new URLSearchParams();
14750
14881
  if (!all) params.set("app", ctx.appId);
14751
14882
  if (query) params.set("q", query);
14752
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14883
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14753
14884
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
14754
14885
  if (!page2.records.length) return ctx.out.log("(no runbooks)");
14755
14886
  ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
@@ -14766,7 +14897,7 @@ async function runbookGet(ctx, slug) {
14766
14897
  ctx.out.log(runbook.body);
14767
14898
  }
14768
14899
  async function runbookNew(ctx, slug, title, body, summary, requires) {
14769
- const created = await call(ctx, "POST", "/runbook", {
14900
+ const created = await call2(ctx, "POST", "/runbook", {
14770
14901
  appId: ctx.appId,
14771
14902
  input: { slug, title, body, ...summary ? { summary } : {}, ...requires ? { requires } : {} }
14772
14903
  });
@@ -14774,7 +14905,7 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
14774
14905
  }
14775
14906
  async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
14776
14907
  const runbook = await bySlug(ctx, slug);
14777
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14908
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14778
14909
  // An empty --requires clears the declaration; omitting the flag leaves
14779
14910
  // whatever is there, so an ordinary body edit never drops it.
14780
14911
  patch: {
@@ -14789,7 +14920,7 @@ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
14789
14920
  }
14790
14921
  async function runbookStatus(ctx, slug, status) {
14791
14922
  const runbook = await bySlug(ctx, slug);
14792
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14923
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14793
14924
  patch: { status }
14794
14925
  });
14795
14926
  ctx.out.log(ctx.json ? JSON.stringify(result, null, 2) : `${slug} \u2192 ${status}`);
@@ -14798,7 +14929,7 @@ async function runbookVisibility(ctx, slug, visibility) {
14798
14929
  if (visibility !== "operator" && visibility !== "admin")
14799
14930
  throw new Error(`visibility must be "operator" or "admin", got "${visibility}"`);
14800
14931
  const runbook = await bySlug(ctx, slug);
14801
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14932
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14802
14933
  patch: { visibility }
14803
14934
  });
14804
14935
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
@@ -14808,7 +14939,7 @@ async function runbookVisibility(ctx, slug, visibility) {
14808
14939
  }
14809
14940
  async function runbookHistory(ctx, slug) {
14810
14941
  const runbook = await bySlug(ctx, slug);
14811
- const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
14942
+ const page2 = await call2(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
14812
14943
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
14813
14944
  ctx.out.log(`${slug} is at v${runbook.version}`);
14814
14945
  if (!page2.records.length) return ctx.out.log("(no earlier versions)");
@@ -14818,7 +14949,7 @@ async function runbookHistory(ctx, slug) {
14818
14949
  }
14819
14950
  async function runbookRevert(ctx, slug, version) {
14820
14951
  const runbook = await bySlug(ctx, slug);
14821
- const result = await call(
14952
+ const result = await call2(
14822
14953
  ctx,
14823
14954
  "POST",
14824
14955
  `/runbook/${encodeURIComponent(runbook.id)}/revert`,
@@ -14829,7 +14960,7 @@ async function runbookRevert(ctx, slug, version) {
14829
14960
  }
14830
14961
  async function runbookRemove(ctx, slug) {
14831
14962
  const runbook = await bySlug(ctx, slug);
14832
- await call(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
14963
+ await call2(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
14833
14964
  ctx.out.log(`removed ${slug}`);
14834
14965
  }
14835
14966
  var import_node_fs21, PLATFORM_SCOPE, stamp;
@@ -14898,14 +15029,9 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
14898
15029
  );
14899
15030
  }
14900
15031
  async function upsert(ctx, r, visibility) {
14901
- const page2 = await call(
14902
- ctx,
14903
- "GET",
14904
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
14905
- );
14906
- const found = page2.records[0];
15032
+ const found = await bySlug(ctx, r.slug).catch(() => null);
14907
15033
  if (!found) {
14908
- await call(ctx, "POST", "/runbook", {
15034
+ await call2(ctx, "POST", "/runbook", {
14909
15035
  appId: ctx.appId,
14910
15036
  input: {
14911
15037
  slug: r.slug,
@@ -14924,7 +15050,7 @@ async function upsert(ctx, r, visibility) {
14924
15050
  ctx.out.log(` = ${r.slug} (already current at v${found.version})`);
14925
15051
  return "unchanged";
14926
15052
  }
14927
- const result = await call(
15053
+ const result = await call2(
14928
15054
  ctx,
14929
15055
  "PATCH",
14930
15056
  `/runbook/${encodeURIComponent(found.id)}`,
@@ -15163,7 +15289,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
15163
15289
  for (const surface of surfaces) {
15164
15290
  const params = new URLSearchParams({ q: surface.query, limit: String(limit) });
15165
15291
  if (!all) params.set("app", ctx.appId);
15166
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
15292
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
15167
15293
  const runbooks = result.outcome === "ranked" ? foldHits(result.hits) : result.candidates.map((c) => ({ ...c, version: 0, sections: [] }));
15168
15294
  out.push({ surface, runbooks });
15169
15295
  }
@@ -15259,7 +15385,7 @@ function lintRunbook(runbook, installed) {
15259
15385
  async function runbookLint(ctx, all) {
15260
15386
  const params = new URLSearchParams();
15261
15387
  if (!all) params.set("app", ctx.appId);
15262
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
15388
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
15263
15389
  const installed = { "@odla-ai/cli": cliVersion() };
15264
15390
  const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
15265
15391
  if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
@@ -15294,7 +15420,7 @@ async function runbookSearch(ctx, query, all, limit) {
15294
15420
  const params = new URLSearchParams({ q: query });
15295
15421
  if (!all) params.set("app", ctx.appId);
15296
15422
  if (limit) params.set("limit", String(limit));
15297
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
15423
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
15298
15424
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
15299
15425
  if (result.outcome === "empty-corpus") return ctx.out.log("no runbooks are available to search");
15300
15426
  if (result.outcome === "no-match") return ctx.out.log(`no runbook mentions "${query}"`);
@@ -15312,7 +15438,7 @@ async function runbookSearch(ctx, query, all, limit) {
15312
15438
  }
15313
15439
  }
15314
15440
  async function runbookAsk(ctx, question, all) {
15315
- const result = await call(ctx, "POST", "/runbook/ask", {
15441
+ const result = await call2(ctx, "POST", "/runbook/ask", {
15316
15442
  question,
15317
15443
  ...all ? {} : { app: ctx.appId }
15318
15444
  });
@@ -15341,14 +15467,8 @@ async function runbookAsk(ctx, question, all) {
15341
15467
  ctx.out.log(JSDOC_POINTER);
15342
15468
  }
15343
15469
  async function runbookComment(ctx, slug, body) {
15344
- const page2 = await call(
15345
- ctx,
15346
- "GET",
15347
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
15348
- );
15349
- const found = page2.records[0];
15350
- if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
15351
- await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
15470
+ const found = await bySlug(ctx, slug);
15471
+ await call2(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
15352
15472
  ctx.out.log(`commented on ${slug} (v${found.version})`);
15353
15473
  }
15354
15474
  var JSDOC_POINTER;
@@ -15414,13 +15534,7 @@ var init_runbook_editor = __esm({
15414
15534
 
15415
15535
  // src/runbook-edit-flow.ts
15416
15536
  async function editRunbook(ctx, slug, deps = {}) {
15417
- const page2 = await call(
15418
- ctx,
15419
- "GET",
15420
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
15421
- );
15422
- const found = page2.records[0];
15423
- if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
15537
+ const found = await bySlug(ctx, slug);
15424
15538
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
15425
15539
  const body = await editText(found.body, slug, deps);
15426
15540
  return body === null ? null : { body, expectedVersion: found.version };