@odla-ai/cli 0.37.1 → 0.38.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/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;
@@ -2050,7 +2118,11 @@ async function resolveOperatorContext(parsed, options = {}) {
2050
2118
  const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
2051
2119
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
2052
2120
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
2053
- if (appValue) assertOperatorName(appValue, "app");
2121
+ if (appValue) {
2122
+ for (const id2 of options.allowAppList ? appValue.split(",") : [appValue]) {
2123
+ assertOperatorName(id2.trim(), "app");
2124
+ }
2125
+ }
2054
2126
  if (options.requireApp && !appValue) {
2055
2127
  throw new Error(
2056
2128
  "app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
@@ -2141,6 +2213,31 @@ var init_operator_context = __esm({
2141
2213
  async function adminCommand(parsed, deps = {}) {
2142
2214
  const area = parsed.positionals[1];
2143
2215
  const action2 = parsed.positionals[2];
2216
+ if (area === "spend") {
2217
+ assertArgs(parsed, JSON_OPTIONS, 4);
2218
+ const context2 = await resolveOperatorContext(parsed, { allowMissingConfig: true });
2219
+ const out = deps.stdout ?? console;
2220
+ const doFetch = deps.fetch ?? fetch;
2221
+ const token = await getDeveloperToken(
2222
+ context2.cfg,
2223
+ {
2224
+ configPath: context2.cfg.configPath,
2225
+ token: stringOpt(parsed.options.token),
2226
+ email: stringOpt(parsed.options.email),
2227
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2228
+ openApprovalUrl: deps.openUrl
2229
+ },
2230
+ doFetch,
2231
+ out
2232
+ );
2233
+ return adminSpend(parsed, {
2234
+ platformUrl: context2.platform.value,
2235
+ token,
2236
+ doFetch,
2237
+ json: parsed.options.json === true,
2238
+ out
2239
+ });
2240
+ }
2144
2241
  const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
2145
2242
  const credentials = action2 === "credentials";
2146
2243
  const models = action2 === "models";
@@ -2190,6 +2287,8 @@ var init_admin_command = __esm({
2190
2287
  "use strict";
2191
2288
  init_cjs_shims();
2192
2289
  init_admin_ai();
2290
+ init_admin_spend();
2291
+ init_token();
2193
2292
  init_argv();
2194
2293
  init_operator_context();
2195
2294
  CONTEXT_OPTIONS = ["platform", "config", "context", "token", "open", "email"];
@@ -2265,7 +2364,8 @@ async function fetchIdentity(platformUrl, token, doFetch) {
2265
2364
  email,
2266
2365
  admin: body.admin === true,
2267
2366
  machine,
2268
- scopes
2367
+ scopes,
2368
+ projects: Array.isArray(body.projects) ? body.projects.map(String) : null
2269
2369
  };
2270
2370
  }
2271
2371
  function credentialLabel(identity) {
@@ -2335,6 +2435,13 @@ async function whoamiCommand(parsed, deps = {}) {
2335
2435
  out.log(`credential id: ${identity.credential.id}`);
2336
2436
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
2337
2437
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
2438
+ if (identity.projects === null) {
2439
+ out.log("projects: (this registry does not report project grants)");
2440
+ } else if (identity.projects.length) {
2441
+ out.log(`projects: ${identity.projects.join(", ")}`);
2442
+ } else {
2443
+ out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2444
+ }
2338
2445
  if (!identity.admin) {
2339
2446
  if (identity.scopes.includes("platform:runbook:write")) {
2340
2447
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -6211,17 +6318,17 @@ var init_cli_project = __esm({
6211
6318
  }
6212
6319
  });
6213
6320
 
6214
- // ../harness/dist/chunk-3QP4VDQS.js
6321
+ // ../harness/dist/chunk-LNQNFGQC.js
6215
6322
  var HARNESS_PROTOCOL_VERSION;
6216
- var init_chunk_3QP4VDQS = __esm({
6217
- "../harness/dist/chunk-3QP4VDQS.js"() {
6323
+ var init_chunk_LNQNFGQC = __esm({
6324
+ "../harness/dist/chunk-LNQNFGQC.js"() {
6218
6325
  "use strict";
6219
6326
  init_cjs_shims();
6220
6327
  HARNESS_PROTOCOL_VERSION = 1;
6221
6328
  }
6222
6329
  });
6223
6330
 
6224
- // ../harness/dist/chunk-GKDKIU4P.js
6331
+ // ../harness/dist/chunk-K76I2TCQ.js
6225
6332
  function assertPinnedImage(image) {
6226
6333
  if (!DIGEST_IMAGE.test(image)) throw new TypeError("container image must be pinned by sha256 digest");
6227
6334
  }
@@ -6570,8 +6677,8 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
6570
6677
  }
6571
6678
  }
6572
6679
  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"() {
6680
+ var init_chunk_K76I2TCQ = __esm({
6681
+ "../harness/dist/chunk-K76I2TCQ.js"() {
6575
6682
  "use strict";
6576
6683
  init_cjs_shims();
6577
6684
  import_child_process = require("child_process");
@@ -7569,7 +7676,7 @@ var init_code2 = __esm({
7569
7676
  }
7570
7677
  });
7571
7678
 
7572
- // ../harness/dist/chunk-ANNX7VGK.js
7679
+ // ../harness/dist/chunk-UVGZHNLW.js
7573
7680
  async function digestStagedWorkspace(root, limits) {
7574
7681
  const files = [];
7575
7682
  const walk = async (directory) => {
@@ -7647,7 +7754,7 @@ function createCodeRuntimeControlClient(options) {
7647
7754
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
7648
7755
  }
7649
7756
  const request3 = options.fetch ?? fetch;
7650
- const call3 = async (path, body, timeoutMs = requestTimeoutMs) => {
7757
+ const call4 = async (path, body, timeoutMs = requestTimeoutMs) => {
7651
7758
  const timeout = AbortSignal.timeout(timeoutMs);
7652
7759
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
7653
7760
  let response2;
@@ -7677,17 +7784,17 @@ function createCodeRuntimeControlClient(options) {
7677
7784
  return {
7678
7785
  heartbeat: async (version, capabilities) => {
7679
7786
  validateHeartbeat(version, capabilities);
7680
- return parseSnapshot(await call3("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7787
+ return parseSnapshot(await call4("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7681
7788
  },
7682
7789
  acknowledge: async (commandId, result) => {
7683
7790
  if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
7684
- await call3(`/registry/code/runtime/commands/${commandId}/ack`, result);
7791
+ await call4(`/registry/code/runtime/commands/${commandId}/ack`, result);
7685
7792
  },
7686
7793
  source: async (sessionId) => parseSource(
7687
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7794
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7688
7795
  ),
7689
7796
  infer: async (sessionId, inference) => {
7690
- const value2 = record5(await call3(
7797
+ const value2 = record5(await call4(
7691
7798
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
7692
7799
  inference,
7693
7800
  modelRequestTimeoutMs
@@ -7698,11 +7805,11 @@ function createCodeRuntimeControlClient(options) {
7698
7805
  return value2;
7699
7806
  },
7700
7807
  review: async (sessionId, review) => parseReview(
7701
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7808
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7702
7809
  ),
7703
7810
  submitCandidate: async (sessionId, checkpointId, verification) => {
7704
7811
  if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
7705
- return parseCandidate(await call3(
7812
+ return parseCandidate(await call4(
7706
7813
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
7707
7814
  { checkpointId, verification }
7708
7815
  ));
@@ -7712,21 +7819,21 @@ function createCodeRuntimeControlClient(options) {
7712
7819
  if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
7713
7820
  throw new TypeError("invalid Code session event");
7714
7821
  }
7715
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7822
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7716
7823
  },
7717
7824
  recallMemories: async (sessionId, subjects, limit) => {
7718
- const response2 = await call3(
7825
+ const response2 = await call4(
7719
7826
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
7720
7827
  { subjects: [...subjects], limit }
7721
7828
  );
7722
7829
  return Array.isArray(response2.memories) ? response2.memories : [];
7723
7830
  },
7724
7831
  rememberMemory: async (sessionId, memory) => {
7725
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7832
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7726
7833
  },
7727
7834
  reportSessionFailure: async (sessionId, message2) => {
7728
7835
  if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
7729
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7836
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7730
7837
  }
7731
7838
  };
7732
7839
  }
@@ -8512,7 +8619,7 @@ async function materializeCommandWorkspace(input) {
8512
8619
  }
8513
8620
  function codeSkill(opts) {
8514
8621
  let seq = 0;
8515
- const call3 = async (tool, input, signal) => {
8622
+ const call4 = async (tool, input, signal) => {
8516
8623
  const startedAt = Date.now();
8517
8624
  const response2 = await opts.broker.execute(
8518
8625
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
@@ -8534,7 +8641,7 @@ function codeSkill(opts) {
8534
8641
  },
8535
8642
  additionalProperties: false
8536
8643
  },
8537
- handler: (input, ctx) => call3("sandbox.read", input, ctx.signal)
8644
+ handler: (input, ctx) => call4("sandbox.read", input, ctx.signal)
8538
8645
  };
8539
8646
  const applyPatch = {
8540
8647
  name: "odla_apply_git_diff",
@@ -8545,7 +8652,7 @@ function codeSkill(opts) {
8545
8652
  properties: { patch: { type: "string", minLength: 1, maxLength: 262144 } },
8546
8653
  additionalProperties: false
8547
8654
  },
8548
- handler: (input, ctx) => call3("sandbox.apply_patch", input, ctx.signal)
8655
+ handler: (input, ctx) => call4("sandbox.apply_patch", input, ctx.signal)
8549
8656
  };
8550
8657
  const runRecipe = {
8551
8658
  name: "odla_run_recipe",
@@ -8556,7 +8663,7 @@ function codeSkill(opts) {
8556
8663
  properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
8557
8664
  additionalProperties: false
8558
8665
  },
8559
- handler: (input, ctx) => call3("sandbox.run_recipe", input, ctx.signal)
8666
+ handler: (input, ctx) => call4("sandbox.run_recipe", input, ctx.signal)
8560
8667
  };
8561
8668
  const listFiles2 = {
8562
8669
  name: "odla_list",
@@ -8569,7 +8676,7 @@ function codeSkill(opts) {
8569
8676
  },
8570
8677
  additionalProperties: false
8571
8678
  },
8572
- handler: (input, ctx) => call3("sandbox.list", input, ctx.signal)
8679
+ handler: (input, ctx) => call4("sandbox.list", input, ctx.signal)
8573
8680
  };
8574
8681
  const searchFiles = {
8575
8682
  name: "odla_search",
@@ -8585,7 +8692,7 @@ function codeSkill(opts) {
8585
8692
  },
8586
8693
  additionalProperties: false
8587
8694
  },
8588
- handler: (input, ctx) => call3("sandbox.search", input, ctx.signal)
8695
+ handler: (input, ctx) => call4("sandbox.search", input, ctx.signal)
8589
8696
  };
8590
8697
  const graphTool = (name, tool, description, required) => ({
8591
8698
  name,
@@ -8596,7 +8703,7 @@ function codeSkill(opts) {
8596
8703
  properties: { query: { type: "string", maxLength: 512 } },
8597
8704
  additionalProperties: false
8598
8705
  },
8599
- handler: (input, ctx) => call3(tool, input, ctx.signal)
8706
+ handler: (input, ctx) => call4(tool, input, ctx.signal)
8600
8707
  });
8601
8708
  const orientation = [
8602
8709
  graphTool(
@@ -8635,9 +8742,9 @@ async function runCodeAgent(options) {
8635
8742
  lease: options.lease,
8636
8743
  workspaceDir: options.workspaceDir,
8637
8744
  surface,
8638
- onToolCall: (call3) => {
8639
- toolCalls.push(call3);
8640
- options.onToolCall?.(call3);
8745
+ onToolCall: (call4) => {
8746
+ toolCalls.push(call4);
8747
+ options.onToolCall?.(call4);
8641
8748
  }
8642
8749
  });
8643
8750
  const compaction = options.compaction === void 0 ? (0, import_ai4.keepRecentExchanges)({ whenInputTokensExceed: 12e4, keep: 3 }) : options.compaction;
@@ -8722,6 +8829,9 @@ async function handleCodeRuntimeInference(input) {
8722
8829
  call: request3.call
8723
8830
  });
8724
8831
  state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
8832
+ const { costUsd } = response2.receipt;
8833
+ if (costUsd === void 0) state2.costKnown = false;
8834
+ else state2.costUsd += costUsd;
8725
8835
  await input.event({
8726
8836
  type: "usage",
8727
8837
  provider: response2.receipt.provider,
@@ -8731,7 +8841,9 @@ async function handleCodeRuntimeInference(input) {
8731
8841
  durationMs: Date.now() - startedAt,
8732
8842
  interactionId: command.commandId,
8733
8843
  interactionTokens: state2.tokens,
8734
- interactionMaxTokens: metadata2.maxTokensPerInteraction
8844
+ interactionMaxTokens: metadata2.maxTokensPerInteraction,
8845
+ ...costUsd === void 0 ? {} : { costUsd },
8846
+ ...state2.costKnown ? { interactionCostUsd: state2.costUsd } : {}
8735
8847
  }).catch(() => void 0);
8736
8848
  return {
8737
8849
  protocolVersion: HARNESS_PROTOCOL_VERSION,
@@ -9522,10 +9634,16 @@ async function startGoalPursuit(input) {
9522
9634
  attempt: async ({ prompt }) => {
9523
9635
  const result = await input.attempt(prompt);
9524
9636
  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,
9637
+ // What the attempt actually spent, so the runner's token_budget and
9638
+ // cost_budget checks can be reached. This used to be a hardcoded 0 with
9639
+ // no cost at all, which made maxTokens and maxUsd unreachable while
9640
+ // callers reasonably read them as hard ceilings.
9641
+ //
9642
+ // costUsd is omitted rather than zeroed when any call in the attempt
9643
+ // was unpriced: the runner only enforces a cost budget while the cost
9644
+ // is known, and a zero would make it enforce against a lie.
9645
+ tokens: result.tokens ?? 0,
9646
+ ...result.costUsd === void 0 ? {} : { costUsd: result.costUsd },
9529
9647
  ...result.status === "failed" ? { error: result.error ?? "attempt failed" } : {}
9530
9648
  };
9531
9649
  },
@@ -9554,12 +9672,12 @@ async function appendCodeRuntimeEvent(control, command, event, refs) {
9554
9672
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
9555
9673
  }
9556
9674
  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"() {
9675
+ var init_chunk_UVGZHNLW = __esm({
9676
+ "../harness/dist/chunk-UVGZHNLW.js"() {
9559
9677
  "use strict";
9560
9678
  init_cjs_shims();
9561
- init_chunk_GKDKIU4P();
9562
- init_chunk_3QP4VDQS();
9679
+ init_chunk_K76I2TCQ();
9680
+ init_chunk_LNQNFGQC();
9563
9681
  import_crypto = require("crypto");
9564
9682
  import_promises5 = require("fs/promises");
9565
9683
  import_path5 = require("path");
@@ -9973,7 +10091,7 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
9973
10091
  recipeAuthorization: this.options.recipeAuthorization
9974
10092
  }, lease, metadata2.role));
9975
10093
  const startedAt = Date.now();
9976
- const interaction = { tokens: 0, noticeEmitted: false };
10094
+ const interaction = { tokens: 0, noticeEmitted: false, costUsd: 0, costKnown: true };
9977
10095
  const inference = createCodeRuntimeInference({
9978
10096
  command,
9979
10097
  metadata: metadata2,
@@ -10011,7 +10129,11 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
10011
10129
  await this.#diagnostic(command, active, detail);
10012
10130
  await this.#failure(command, active, detail);
10013
10131
  }
10014
- return result;
10132
+ return {
10133
+ ...result,
10134
+ tokens: interaction.tokens,
10135
+ ...interaction.costKnown ? { costUsd: interaction.costUsd } : {}
10136
+ };
10015
10137
  }
10016
10138
  /** Report every brokered effect as it starts and finishes. */
10017
10139
  #observed(command, active, broker) {
@@ -10070,8 +10192,8 @@ var init_node = __esm({
10070
10192
  "../harness/dist/node.js"() {
10071
10193
  "use strict";
10072
10194
  init_cjs_shims();
10073
- init_chunk_ANNX7VGK();
10074
- init_chunk_GKDKIU4P();
10195
+ init_chunk_UVGZHNLW();
10196
+ init_chunk_K76I2TCQ();
10075
10197
  MEASURED_PREMIUM = Object.freeze({
10076
10198
  /** 3 racers vs pure depth at equal budget: 21,044 / 7,936. */
10077
10199
  racePerRacer: 0.55,
@@ -11175,6 +11297,8 @@ Usage:
11175
11297
  odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
11176
11298
  odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
11177
11299
  odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
11300
+ odla-ai admin spend show <app:<id>:<incarnation>> [--context <name>] [--json]
11301
+ odla-ai admin spend reset <app:<id>:<incarnation>> [--context <name>] [--json]
11178
11302
  odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
11179
11303
  odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
11180
11304
  odla-ai security plan [--env dev] [--json]
@@ -11269,6 +11393,9 @@ Commands:
11269
11393
  human session connects one); "code grant request|list|approve|revoke" then
11270
11394
  governs unattended access: an agent may request, only a human may approve.
11271
11395
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
11396
+ "admin spend" reads one app's daily inference spend and resumes it
11397
+ after a cap halts it. The cap is per UTC day; exhausting it LATCHES,
11398
+ and a new day does not clear the latch \u2014 resuming is deliberate.
11272
11399
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
11273
11400
  pm Project management (via @odla-ai/pm): Products contain Projects;
11274
11401
  projects contain goals, kanban tasks, decisions, and bugs. Use
@@ -11276,6 +11403,12 @@ Commands:
11276
11403
  pass --app/--project explicitly. Same device-grant auth as "app".
11277
11404
  Status changes and comments post to each item's @odla-ai/chat
11278
11405
  discussion thread.
11406
+ NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
11407
+ approves its complete execution contract, so it needs pm.plan,
11408
+ which no device enrollment or handshake approval can grant. An
11409
+ agent proposes in Backlog (the default); a human owner or a
11410
+ pm.plan agent promotes. This is deliberate, not a permission
11411
+ gap \u2014 it was reported as one.
11279
11412
  bug Intent-first alias for PM bugs. "bug report" writes to
11280
11413
  odla PM; odla product defects do not belong in GitHub Issues.
11281
11414
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -14663,7 +14796,11 @@ async function revoke(parsed, deps, cfg, doFetch, out, json) {
14663
14796
  if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
14664
14797
  }
14665
14798
  async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app:device:enroll") {
14666
- const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
14799
+ const { credentials } = await resolveOperatorContext(parsed, {
14800
+ allowMissingConfig: true,
14801
+ // A device is granted the apps named in ONE approval, so --app is a list here.
14802
+ allowAppList: true
14803
+ });
14667
14804
  const scopedTokenFile = credentials.scopedTokenFile;
14668
14805
  return getScopedPlatformToken({
14669
14806
  platform: cfg.platformUrl,
@@ -14700,7 +14837,7 @@ var init_device_command = __esm({
14700
14837
  });
14701
14838
 
14702
14839
  // src/runbook-actions.ts
14703
- async function call(ctx, method, path, body) {
14840
+ async function call2(ctx, method, path, body) {
14704
14841
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
14705
14842
  method,
14706
14843
  headers: {
@@ -14722,7 +14859,7 @@ async function call(ctx, method, path, body) {
14722
14859
  throw new Error(message2);
14723
14860
  }
14724
14861
  async function bySlug(ctx, slug) {
14725
- const page2 = await call(
14862
+ const page2 = await call2(
14726
14863
  ctx,
14727
14864
  "GET",
14728
14865
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
@@ -14731,7 +14868,7 @@ async function bySlug(ctx, slug) {
14731
14868
  if (filtered) return filtered;
14732
14869
  const limit = 100;
14733
14870
  for (let offset = 0; ; offset += limit) {
14734
- const fallback = await call(
14871
+ const fallback = await call2(
14735
14872
  ctx,
14736
14873
  "GET",
14737
14874
  `/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
@@ -14751,7 +14888,7 @@ async function runbookList(ctx, all, query) {
14751
14888
  const params = new URLSearchParams();
14752
14889
  if (!all) params.set("app", ctx.appId);
14753
14890
  if (query) params.set("q", query);
14754
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14891
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14755
14892
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
14756
14893
  if (!page2.records.length) return ctx.out.log("(no runbooks)");
14757
14894
  ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
@@ -14768,7 +14905,7 @@ async function runbookGet(ctx, slug) {
14768
14905
  ctx.out.log(runbook.body);
14769
14906
  }
14770
14907
  async function runbookNew(ctx, slug, title, body, summary, requires) {
14771
- const created = await call(ctx, "POST", "/runbook", {
14908
+ const created = await call2(ctx, "POST", "/runbook", {
14772
14909
  appId: ctx.appId,
14773
14910
  input: { slug, title, body, ...summary ? { summary } : {}, ...requires ? { requires } : {} }
14774
14911
  });
@@ -14776,7 +14913,7 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
14776
14913
  }
14777
14914
  async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
14778
14915
  const runbook = await bySlug(ctx, slug);
14779
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14916
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14780
14917
  // An empty --requires clears the declaration; omitting the flag leaves
14781
14918
  // whatever is there, so an ordinary body edit never drops it.
14782
14919
  patch: {
@@ -14791,7 +14928,7 @@ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
14791
14928
  }
14792
14929
  async function runbookStatus(ctx, slug, status) {
14793
14930
  const runbook = await bySlug(ctx, slug);
14794
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14931
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14795
14932
  patch: { status }
14796
14933
  });
14797
14934
  ctx.out.log(ctx.json ? JSON.stringify(result, null, 2) : `${slug} \u2192 ${status}`);
@@ -14800,7 +14937,7 @@ async function runbookVisibility(ctx, slug, visibility) {
14800
14937
  if (visibility !== "operator" && visibility !== "admin")
14801
14938
  throw new Error(`visibility must be "operator" or "admin", got "${visibility}"`);
14802
14939
  const runbook = await bySlug(ctx, slug);
14803
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14940
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
14804
14941
  patch: { visibility }
14805
14942
  });
14806
14943
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
@@ -14810,7 +14947,7 @@ async function runbookVisibility(ctx, slug, visibility) {
14810
14947
  }
14811
14948
  async function runbookHistory(ctx, slug) {
14812
14949
  const runbook = await bySlug(ctx, slug);
14813
- const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
14950
+ const page2 = await call2(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
14814
14951
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
14815
14952
  ctx.out.log(`${slug} is at v${runbook.version}`);
14816
14953
  if (!page2.records.length) return ctx.out.log("(no earlier versions)");
@@ -14820,7 +14957,7 @@ async function runbookHistory(ctx, slug) {
14820
14957
  }
14821
14958
  async function runbookRevert(ctx, slug, version) {
14822
14959
  const runbook = await bySlug(ctx, slug);
14823
- const result = await call(
14960
+ const result = await call2(
14824
14961
  ctx,
14825
14962
  "POST",
14826
14963
  `/runbook/${encodeURIComponent(runbook.id)}/revert`,
@@ -14831,7 +14968,7 @@ async function runbookRevert(ctx, slug, version) {
14831
14968
  }
14832
14969
  async function runbookRemove(ctx, slug) {
14833
14970
  const runbook = await bySlug(ctx, slug);
14834
- await call(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
14971
+ await call2(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
14835
14972
  ctx.out.log(`removed ${slug}`);
14836
14973
  }
14837
14974
  var import_node_fs21, PLATFORM_SCOPE, stamp;
@@ -14902,7 +15039,7 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
14902
15039
  async function upsert(ctx, r, visibility) {
14903
15040
  const found = await bySlug(ctx, r.slug).catch(() => null);
14904
15041
  if (!found) {
14905
- await call(ctx, "POST", "/runbook", {
15042
+ await call2(ctx, "POST", "/runbook", {
14906
15043
  appId: ctx.appId,
14907
15044
  input: {
14908
15045
  slug: r.slug,
@@ -14921,7 +15058,7 @@ async function upsert(ctx, r, visibility) {
14921
15058
  ctx.out.log(` = ${r.slug} (already current at v${found.version})`);
14922
15059
  return "unchanged";
14923
15060
  }
14924
- const result = await call(
15061
+ const result = await call2(
14925
15062
  ctx,
14926
15063
  "PATCH",
14927
15064
  `/runbook/${encodeURIComponent(found.id)}`,
@@ -15160,7 +15297,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
15160
15297
  for (const surface of surfaces) {
15161
15298
  const params = new URLSearchParams({ q: surface.query, limit: String(limit) });
15162
15299
  if (!all) params.set("app", ctx.appId);
15163
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
15300
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
15164
15301
  const runbooks = result.outcome === "ranked" ? foldHits(result.hits) : result.candidates.map((c) => ({ ...c, version: 0, sections: [] }));
15165
15302
  out.push({ surface, runbooks });
15166
15303
  }
@@ -15256,7 +15393,7 @@ function lintRunbook(runbook, installed) {
15256
15393
  async function runbookLint(ctx, all) {
15257
15394
  const params = new URLSearchParams();
15258
15395
  if (!all) params.set("app", ctx.appId);
15259
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
15396
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
15260
15397
  const installed = { "@odla-ai/cli": cliVersion() };
15261
15398
  const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
15262
15399
  if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
@@ -15291,7 +15428,7 @@ async function runbookSearch(ctx, query, all, limit) {
15291
15428
  const params = new URLSearchParams({ q: query });
15292
15429
  if (!all) params.set("app", ctx.appId);
15293
15430
  if (limit) params.set("limit", String(limit));
15294
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
15431
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
15295
15432
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
15296
15433
  if (result.outcome === "empty-corpus") return ctx.out.log("no runbooks are available to search");
15297
15434
  if (result.outcome === "no-match") return ctx.out.log(`no runbook mentions "${query}"`);
@@ -15309,7 +15446,7 @@ async function runbookSearch(ctx, query, all, limit) {
15309
15446
  }
15310
15447
  }
15311
15448
  async function runbookAsk(ctx, question, all) {
15312
- const result = await call(ctx, "POST", "/runbook/ask", {
15449
+ const result = await call2(ctx, "POST", "/runbook/ask", {
15313
15450
  question,
15314
15451
  ...all ? {} : { app: ctx.appId }
15315
15452
  });
@@ -15339,7 +15476,7 @@ async function runbookAsk(ctx, question, all) {
15339
15476
  }
15340
15477
  async function runbookComment(ctx, slug, body) {
15341
15478
  const found = await bySlug(ctx, slug);
15342
- await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
15479
+ await call2(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
15343
15480
  ctx.out.log(`commented on ${slug} (v${found.version})`);
15344
15481
  }
15345
15482
  var JSDOC_POINTER;