@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-XFIFFYNH.js";
4
+ } from "./chunk-HETCZVFB.js";
5
5
  import {
6
6
  exitCodeFor
7
7
  } from "./chunk-UKLSRQ5J.js";
@@ -9,4 +9,4 @@ export {
9
9
  exitCodeFor,
10
10
  runCli
11
11
  };
12
- //# sourceMappingURL=cli-OH6PCYZ3.js.map
12
+ //# sourceMappingURL=cli-LGYDXY5R.js.map
package/dist/index.cjs CHANGED
@@ -1110,6 +1110,66 @@ function addOption(options, name, value2) {
1110
1110
  else options[name] = [String(current), String(value2)];
1111
1111
  }
1112
1112
 
1113
+ // src/admin-spend.ts
1114
+ async function call(ctx, method, scope) {
1115
+ const url = `${ctx.platformUrl.replace(/\/$/, "")}/registry/platform/spend?scope=${encodeURIComponent(scope)}`;
1116
+ const response2 = await ctx.doFetch(url, {
1117
+ method,
1118
+ headers: { authorization: `Bearer ${ctx.token}` }
1119
+ });
1120
+ const body = await response2.json().catch(() => ({}));
1121
+ if (!response2.ok) {
1122
+ const detail = body.error?.message ?? body.error?.code ?? `registry returned ${response2.status}`;
1123
+ throw new Error(`spend ${method} failed: ${detail} (${response2.status})`);
1124
+ }
1125
+ return body;
1126
+ }
1127
+ var money = (value2) => `$${value2.toFixed(2)}`;
1128
+ async function spendShow(ctx, scope) {
1129
+ const view = await call(ctx, "GET", scope);
1130
+ if (ctx.json) return ctx.out.log(JSON.stringify(view, null, 2));
1131
+ ctx.out.log(`scope: ${view.scope}`);
1132
+ ctx.out.log(`day: ${view.day} (UTC)`);
1133
+ ctx.out.log(
1134
+ `spent: ${money(view.spentUsd)} of ${view.capUsd > 0 ? money(view.capUsd) : "(no cap set)"}`
1135
+ );
1136
+ ctx.out.log(`calls: ${view.calls}`);
1137
+ if (view.unpricedCalls > 0) {
1138
+ ctx.out.log(
1139
+ `unpriced: ${view.unpricedCalls} call(s) had no price, so "spent" is a floor, not the total.`
1140
+ );
1141
+ }
1142
+ if (!view.latched) return ctx.out.log("status: running");
1143
+ ctx.out.log(
1144
+ `status: HALTED \u2014 reached ${money(view.latched.capUsd)} on ${view.latched.day} (${view.latched.reason}), at ${money(view.latched.costUsd)}.`
1145
+ );
1146
+ ctx.out.log("A new day does not clear this. Resume with:");
1147
+ ctx.out.log(` odla-ai admin spend reset ${view.scope}`);
1148
+ }
1149
+ async function spendReset(ctx, scope) {
1150
+ const result = await call(ctx, "DELETE", scope);
1151
+ if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
1152
+ ctx.out.log(result.message);
1153
+ if (result.stillOverCap) {
1154
+ ctx.out.log(
1155
+ ` today: ${money(result.spentUsd)} against a ${money(result.capUsd)} cap.`
1156
+ );
1157
+ }
1158
+ }
1159
+ async function adminSpend(parsed, ctx) {
1160
+ const action2 = parsed.positionals[2];
1161
+ const scope = parsed.positionals[3] ?? stringOpt(parsed.options.scope);
1162
+ if (action2 !== "show" && action2 !== "reset") {
1163
+ throw new Error('unknown spend command. Try "odla-ai admin spend show <scope>".');
1164
+ }
1165
+ if (!scope) {
1166
+ throw new Error(
1167
+ `"admin spend ${action2}" needs a scope, e.g. odla-ai admin spend ${action2} app:my-app:<incarnation>`
1168
+ );
1169
+ }
1170
+ return action2 === "show" ? spendShow(ctx, scope) : spendReset(ctx, scope);
1171
+ }
1172
+
1113
1173
  // src/operator-context.ts
1114
1174
  var import_node_fs8 = require("fs");
1115
1175
  var import_node_path7 = require("path");
@@ -1798,7 +1858,11 @@ async function resolveOperatorContext(parsed, options = {}) {
1798
1858
  const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
1799
1859
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1800
1860
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1801
- if (appValue) assertOperatorName(appValue, "app");
1861
+ if (appValue) {
1862
+ for (const id2 of options.allowAppList ? appValue.split(",") : [appValue]) {
1863
+ assertOperatorName(id2.trim(), "app");
1864
+ }
1865
+ }
1802
1866
  if (options.requireApp && !appValue) {
1803
1867
  throw new Error(
1804
1868
  "app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
@@ -1889,6 +1953,31 @@ var SET_OPTIONS = [
1889
1953
  async function adminCommand(parsed, deps = {}) {
1890
1954
  const area = parsed.positionals[1];
1891
1955
  const action2 = parsed.positionals[2];
1956
+ if (area === "spend") {
1957
+ assertArgs(parsed, JSON_OPTIONS, 4);
1958
+ const context2 = await resolveOperatorContext(parsed, { allowMissingConfig: true });
1959
+ const out = deps.stdout ?? console;
1960
+ const doFetch = deps.fetch ?? fetch;
1961
+ const token = await getDeveloperToken(
1962
+ context2.cfg,
1963
+ {
1964
+ configPath: context2.cfg.configPath,
1965
+ token: stringOpt(parsed.options.token),
1966
+ email: stringOpt(parsed.options.email),
1967
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1968
+ openApprovalUrl: deps.openUrl
1969
+ },
1970
+ doFetch,
1971
+ out
1972
+ );
1973
+ return adminSpend(parsed, {
1974
+ platformUrl: context2.platform.value,
1975
+ token,
1976
+ doFetch,
1977
+ json: parsed.options.json === true,
1978
+ out
1979
+ });
1980
+ }
1892
1981
  const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
1893
1982
  const credentials = action2 === "credentials";
1894
1983
  const models = action2 === "models";
@@ -1992,7 +2081,8 @@ async function fetchIdentity(platformUrl, token, doFetch) {
1992
2081
  email,
1993
2082
  admin: body.admin === true,
1994
2083
  machine,
1995
- scopes
2084
+ scopes,
2085
+ projects: Array.isArray(body.projects) ? body.projects.map(String) : null
1996
2086
  };
1997
2087
  }
1998
2088
  function credentialLabel(identity) {
@@ -2062,6 +2152,13 @@ async function whoamiCommand(parsed, deps = {}) {
2062
2152
  out.log(`credential id: ${identity.credential.id}`);
2063
2153
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
2064
2154
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
2155
+ if (identity.projects === null) {
2156
+ out.log("projects: (this registry does not report project grants)");
2157
+ } else if (identity.projects.length) {
2158
+ out.log(`projects: ${identity.projects.join(", ")}`);
2159
+ } else {
2160
+ out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2161
+ }
2065
2162
  if (!identity.admin) {
2066
2163
  if (identity.scopes.includes("platform:runbook:write")) {
2067
2164
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -5607,10 +5704,10 @@ var import_node_fs16 = require("fs");
5607
5704
  var import_node_os4 = require("os");
5608
5705
  var import_node_path15 = require("path");
5609
5706
 
5610
- // ../harness/dist/chunk-3QP4VDQS.js
5707
+ // ../harness/dist/chunk-LNQNFGQC.js
5611
5708
  var HARNESS_PROTOCOL_VERSION = 1;
5612
5709
 
5613
- // ../harness/dist/chunk-GKDKIU4P.js
5710
+ // ../harness/dist/chunk-K76I2TCQ.js
5614
5711
  var import_child_process = require("child_process");
5615
5712
  var import_fs = require("fs");
5616
5713
  var import_promises2 = require("fs/promises");
@@ -5983,7 +6080,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
5983
6080
  }
5984
6081
  }
5985
6082
 
5986
- // ../harness/dist/chunk-ANNX7VGK.js
6083
+ // ../harness/dist/chunk-UVGZHNLW.js
5987
6084
  var import_crypto = require("crypto");
5988
6085
  var import_promises5 = require("fs/promises");
5989
6086
  var import_path5 = require("path");
@@ -6320,7 +6417,7 @@ function validateSnapshot(snapshot, limits) {
6320
6417
  }
6321
6418
  }
6322
6419
 
6323
- // ../harness/dist/chunk-ANNX7VGK.js
6420
+ // ../harness/dist/chunk-UVGZHNLW.js
6324
6421
  var import_child_process4 = require("child_process");
6325
6422
  var import_promises6 = require("fs/promises");
6326
6423
  var import_path6 = require("path");
@@ -6622,7 +6719,7 @@ function looksLikeDestination(value2) {
6622
6719
  return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text3) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text3);
6623
6720
  }
6624
6721
 
6625
- // ../harness/dist/chunk-ANNX7VGK.js
6722
+ // ../harness/dist/chunk-UVGZHNLW.js
6626
6723
  var import_promises10 = require("fs/promises");
6627
6724
  var import_promises11 = require("fs/promises");
6628
6725
  var import_path10 = require("path");
@@ -6890,7 +6987,7 @@ async function buildCodeGraph(input) {
6890
6987
  return builder.build();
6891
6988
  }
6892
6989
 
6893
- // ../harness/dist/chunk-ANNX7VGK.js
6990
+ // ../harness/dist/chunk-UVGZHNLW.js
6894
6991
  var import_crypto4 = require("crypto");
6895
6992
  async function digestStagedWorkspace(root, limits) {
6896
6993
  const files = [];
@@ -7010,7 +7107,7 @@ function createCodeRuntimeControlClient(options) {
7010
7107
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
7011
7108
  }
7012
7109
  const request3 = options.fetch ?? fetch;
7013
- const call3 = async (path, body, timeoutMs = requestTimeoutMs) => {
7110
+ const call4 = async (path, body, timeoutMs = requestTimeoutMs) => {
7014
7111
  const timeout = AbortSignal.timeout(timeoutMs);
7015
7112
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
7016
7113
  let response2;
@@ -7040,17 +7137,17 @@ function createCodeRuntimeControlClient(options) {
7040
7137
  return {
7041
7138
  heartbeat: async (version, capabilities) => {
7042
7139
  validateHeartbeat(version, capabilities);
7043
- return parseSnapshot(await call3("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7140
+ return parseSnapshot(await call4("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7044
7141
  },
7045
7142
  acknowledge: async (commandId, result) => {
7046
7143
  if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
7047
- await call3(`/registry/code/runtime/commands/${commandId}/ack`, result);
7144
+ await call4(`/registry/code/runtime/commands/${commandId}/ack`, result);
7048
7145
  },
7049
7146
  source: async (sessionId) => parseSource(
7050
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7147
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7051
7148
  ),
7052
7149
  infer: async (sessionId, inference) => {
7053
- const value2 = record5(await call3(
7150
+ const value2 = record5(await call4(
7054
7151
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
7055
7152
  inference,
7056
7153
  modelRequestTimeoutMs
@@ -7061,11 +7158,11 @@ function createCodeRuntimeControlClient(options) {
7061
7158
  return value2;
7062
7159
  },
7063
7160
  review: async (sessionId, review) => parseReview(
7064
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7161
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7065
7162
  ),
7066
7163
  submitCandidate: async (sessionId, checkpointId, verification) => {
7067
7164
  if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
7068
- return parseCandidate(await call3(
7165
+ return parseCandidate(await call4(
7069
7166
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
7070
7167
  { checkpointId, verification }
7071
7168
  ));
@@ -7075,21 +7172,21 @@ function createCodeRuntimeControlClient(options) {
7075
7172
  if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
7076
7173
  throw new TypeError("invalid Code session event");
7077
7174
  }
7078
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7175
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7079
7176
  },
7080
7177
  recallMemories: async (sessionId, subjects, limit) => {
7081
- const response2 = await call3(
7178
+ const response2 = await call4(
7082
7179
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
7083
7180
  { subjects: [...subjects], limit }
7084
7181
  );
7085
7182
  return Array.isArray(response2.memories) ? response2.memories : [];
7086
7183
  },
7087
7184
  rememberMemory: async (sessionId, memory) => {
7088
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7185
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7089
7186
  },
7090
7187
  reportSessionFailure: async (sessionId, message2) => {
7091
7188
  if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
7092
- await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7189
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7093
7190
  }
7094
7191
  };
7095
7192
  }
@@ -7978,7 +8075,7 @@ var SYSTEM_PROMPT_FOR = {
7978
8075
  };
7979
8076
  function codeSkill(opts) {
7980
8077
  let seq = 0;
7981
- const call3 = async (tool, input, signal) => {
8078
+ const call4 = async (tool, input, signal) => {
7982
8079
  const startedAt = Date.now();
7983
8080
  const response2 = await opts.broker.execute(
7984
8081
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
@@ -8000,7 +8097,7 @@ function codeSkill(opts) {
8000
8097
  },
8001
8098
  additionalProperties: false
8002
8099
  },
8003
- handler: (input, ctx) => call3("sandbox.read", input, ctx.signal)
8100
+ handler: (input, ctx) => call4("sandbox.read", input, ctx.signal)
8004
8101
  };
8005
8102
  const applyPatch = {
8006
8103
  name: "odla_apply_git_diff",
@@ -8011,7 +8108,7 @@ function codeSkill(opts) {
8011
8108
  properties: { patch: { type: "string", minLength: 1, maxLength: 262144 } },
8012
8109
  additionalProperties: false
8013
8110
  },
8014
- handler: (input, ctx) => call3("sandbox.apply_patch", input, ctx.signal)
8111
+ handler: (input, ctx) => call4("sandbox.apply_patch", input, ctx.signal)
8015
8112
  };
8016
8113
  const runRecipe = {
8017
8114
  name: "odla_run_recipe",
@@ -8022,7 +8119,7 @@ function codeSkill(opts) {
8022
8119
  properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
8023
8120
  additionalProperties: false
8024
8121
  },
8025
- handler: (input, ctx) => call3("sandbox.run_recipe", input, ctx.signal)
8122
+ handler: (input, ctx) => call4("sandbox.run_recipe", input, ctx.signal)
8026
8123
  };
8027
8124
  const listFiles2 = {
8028
8125
  name: "odla_list",
@@ -8035,7 +8132,7 @@ function codeSkill(opts) {
8035
8132
  },
8036
8133
  additionalProperties: false
8037
8134
  },
8038
- handler: (input, ctx) => call3("sandbox.list", input, ctx.signal)
8135
+ handler: (input, ctx) => call4("sandbox.list", input, ctx.signal)
8039
8136
  };
8040
8137
  const searchFiles = {
8041
8138
  name: "odla_search",
@@ -8051,7 +8148,7 @@ function codeSkill(opts) {
8051
8148
  },
8052
8149
  additionalProperties: false
8053
8150
  },
8054
- handler: (input, ctx) => call3("sandbox.search", input, ctx.signal)
8151
+ handler: (input, ctx) => call4("sandbox.search", input, ctx.signal)
8055
8152
  };
8056
8153
  const graphTool = (name, tool, description, required) => ({
8057
8154
  name,
@@ -8062,7 +8159,7 @@ function codeSkill(opts) {
8062
8159
  properties: { query: { type: "string", maxLength: 512 } },
8063
8160
  additionalProperties: false
8064
8161
  },
8065
- handler: (input, ctx) => call3(tool, input, ctx.signal)
8162
+ handler: (input, ctx) => call4(tool, input, ctx.signal)
8066
8163
  });
8067
8164
  const orientation = [
8068
8165
  graphTool(
@@ -8101,9 +8198,9 @@ async function runCodeAgent(options) {
8101
8198
  lease: options.lease,
8102
8199
  workspaceDir: options.workspaceDir,
8103
8200
  surface,
8104
- onToolCall: (call3) => {
8105
- toolCalls.push(call3);
8106
- options.onToolCall?.(call3);
8201
+ onToolCall: (call4) => {
8202
+ toolCalls.push(call4);
8203
+ options.onToolCall?.(call4);
8107
8204
  }
8108
8205
  });
8109
8206
  const compaction = options.compaction === void 0 ? (0, import_ai4.keepRecentExchanges)({ whenInputTokensExceed: 12e4, keep: 3 }) : options.compaction;
@@ -8188,6 +8285,9 @@ async function handleCodeRuntimeInference(input) {
8188
8285
  call: request3.call
8189
8286
  });
8190
8287
  state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
8288
+ const { costUsd } = response2.receipt;
8289
+ if (costUsd === void 0) state2.costKnown = false;
8290
+ else state2.costUsd += costUsd;
8191
8291
  await input.event({
8192
8292
  type: "usage",
8193
8293
  provider: response2.receipt.provider,
@@ -8197,7 +8297,9 @@ async function handleCodeRuntimeInference(input) {
8197
8297
  durationMs: Date.now() - startedAt,
8198
8298
  interactionId: command.commandId,
8199
8299
  interactionTokens: state2.tokens,
8200
- interactionMaxTokens: metadata2.maxTokensPerInteraction
8300
+ interactionMaxTokens: metadata2.maxTokensPerInteraction,
8301
+ ...costUsd === void 0 ? {} : { costUsd },
8302
+ ...state2.costKnown ? { interactionCostUsd: state2.costUsd } : {}
8201
8303
  }).catch(() => void 0);
8202
8304
  return {
8203
8305
  protocolVersion: HARNESS_PROTOCOL_VERSION,
@@ -9041,10 +9143,16 @@ async function startGoalPursuit(input) {
9041
9143
  attempt: async ({ prompt }) => {
9042
9144
  const result = await input.attempt(prompt);
9043
9145
  return {
9044
- // The runtime charges tokens through the control plane's own
9045
- // per-interaction reservation, so the goal budget bounds ATTEMPTS here
9046
- // and the token ceiling is enforced where the credential lives.
9047
- tokens: 0,
9146
+ // What the attempt actually spent, so the runner's token_budget and
9147
+ // cost_budget checks can be reached. This used to be a hardcoded 0 with
9148
+ // no cost at all, which made maxTokens and maxUsd unreachable while
9149
+ // callers reasonably read them as hard ceilings.
9150
+ //
9151
+ // costUsd is omitted rather than zeroed when any call in the attempt
9152
+ // was unpriced: the runner only enforces a cost budget while the cost
9153
+ // is known, and a zero would make it enforce against a lie.
9154
+ tokens: result.tokens ?? 0,
9155
+ ...result.costUsd === void 0 ? {} : { costUsd: result.costUsd },
9048
9156
  ...result.status === "failed" ? { error: result.error ?? "attempt failed" } : {}
9049
9157
  };
9050
9158
  },
@@ -9258,7 +9366,7 @@ var CodePiRuntimeEngine = class {
9258
9366
  recipeAuthorization: this.options.recipeAuthorization
9259
9367
  }, lease, metadata2.role));
9260
9368
  const startedAt = Date.now();
9261
- const interaction = { tokens: 0, noticeEmitted: false };
9369
+ const interaction = { tokens: 0, noticeEmitted: false, costUsd: 0, costKnown: true };
9262
9370
  const inference = createCodeRuntimeInference({
9263
9371
  command,
9264
9372
  metadata: metadata2,
@@ -9296,7 +9404,11 @@ var CodePiRuntimeEngine = class {
9296
9404
  await this.#diagnostic(command, active, detail);
9297
9405
  await this.#failure(command, active, detail);
9298
9406
  }
9299
- return result;
9407
+ return {
9408
+ ...result,
9409
+ tokens: interaction.tokens,
9410
+ ...interaction.costKnown ? { costUsd: interaction.costUsd } : {}
9411
+ };
9300
9412
  }
9301
9413
  /** Report every brokered effect as it starts and finishes. */
9302
9414
  #observed(command, active, broker) {
@@ -10405,6 +10517,8 @@ Usage:
10405
10517
  odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
10406
10518
  odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
10407
10519
  odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
10520
+ odla-ai admin spend show <app:<id>:<incarnation>> [--context <name>] [--json]
10521
+ odla-ai admin spend reset <app:<id>:<incarnation>> [--context <name>] [--json]
10408
10522
  odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
10409
10523
  odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
10410
10524
  odla-ai security plan [--env dev] [--json]
@@ -10497,6 +10611,9 @@ Commands:
10497
10611
  human session connects one); "code grant request|list|approve|revoke" then
10498
10612
  governs unattended access: an agent may request, only a human may approve.
10499
10613
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
10614
+ "admin spend" reads one app's daily inference spend and resumes it
10615
+ after a cap halts it. The cap is per UTC day; exhausting it LATCHES,
10616
+ and a new day does not clear the latch \u2014 resuming is deliberate.
10500
10617
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10501
10618
  pm Project management (via @odla-ai/pm): Products contain Projects;
10502
10619
  projects contain goals, kanban tasks, decisions, and bugs. Use
@@ -10504,6 +10621,12 @@ Commands:
10504
10621
  pass --app/--project explicitly. Same device-grant auth as "app".
10505
10622
  Status changes and comments post to each item's @odla-ai/chat
10506
10623
  discussion thread.
10624
+ NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
10625
+ approves its complete execution contract, so it needs pm.plan,
10626
+ which no device enrollment or handshake approval can grant. An
10627
+ agent proposes in Backlog (the default); a human owner or a
10628
+ pm.plan agent promotes. This is deliberate, not a permission
10629
+ gap \u2014 it was reported as one.
10507
10630
  bug Intent-first alias for PM bugs. "bug report" writes to
10508
10631
  odla PM; odla product defects do not belong in GitHub Issues.
10509
10632
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -13618,7 +13741,11 @@ async function revoke(parsed, deps, cfg, doFetch, out, json) {
13618
13741
  if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
13619
13742
  }
13620
13743
  async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app:device:enroll") {
13621
- const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
13744
+ const { credentials } = await resolveOperatorContext(parsed, {
13745
+ allowMissingConfig: true,
13746
+ // A device is granted the apps named in ONE approval, so --app is a list here.
13747
+ allowAppList: true
13748
+ });
13622
13749
  const scopedTokenFile = credentials.scopedTokenFile;
13623
13750
  return getScopedPlatformToken({
13624
13751
  platform: cfg.platformUrl,
@@ -13690,7 +13817,7 @@ function describeUnmet(slug, unmet) {
13690
13817
 
13691
13818
  // src/runbook-actions.ts
13692
13819
  var PLATFORM_SCOPE = "$platform";
13693
- async function call(ctx, method, path, body) {
13820
+ async function call2(ctx, method, path, body) {
13694
13821
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
13695
13822
  method,
13696
13823
  headers: {
@@ -13712,7 +13839,7 @@ async function call(ctx, method, path, body) {
13712
13839
  throw new Error(message2);
13713
13840
  }
13714
13841
  async function bySlug(ctx, slug) {
13715
- const page2 = await call(
13842
+ const page2 = await call2(
13716
13843
  ctx,
13717
13844
  "GET",
13718
13845
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
@@ -13721,7 +13848,7 @@ async function bySlug(ctx, slug) {
13721
13848
  if (filtered) return filtered;
13722
13849
  const limit = 100;
13723
13850
  for (let offset = 0; ; offset += limit) {
13724
- const fallback = await call(
13851
+ const fallback = await call2(
13725
13852
  ctx,
13726
13853
  "GET",
13727
13854
  `/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
@@ -13742,7 +13869,7 @@ async function runbookList(ctx, all, query) {
13742
13869
  const params = new URLSearchParams();
13743
13870
  if (!all) params.set("app", ctx.appId);
13744
13871
  if (query) params.set("q", query);
13745
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
13872
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
13746
13873
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
13747
13874
  if (!page2.records.length) return ctx.out.log("(no runbooks)");
13748
13875
  ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
@@ -13759,7 +13886,7 @@ async function runbookGet(ctx, slug) {
13759
13886
  ctx.out.log(runbook.body);
13760
13887
  }
13761
13888
  async function runbookNew(ctx, slug, title, body, summary, requires) {
13762
- const created = await call(ctx, "POST", "/runbook", {
13889
+ const created = await call2(ctx, "POST", "/runbook", {
13763
13890
  appId: ctx.appId,
13764
13891
  input: { slug, title, body, ...summary ? { summary } : {}, ...requires ? { requires } : {} }
13765
13892
  });
@@ -13767,7 +13894,7 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
13767
13894
  }
13768
13895
  async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
13769
13896
  const runbook = await bySlug(ctx, slug);
13770
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13897
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13771
13898
  // An empty --requires clears the declaration; omitting the flag leaves
13772
13899
  // whatever is there, so an ordinary body edit never drops it.
13773
13900
  patch: {
@@ -13782,7 +13909,7 @@ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
13782
13909
  }
13783
13910
  async function runbookStatus(ctx, slug, status) {
13784
13911
  const runbook = await bySlug(ctx, slug);
13785
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13912
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13786
13913
  patch: { status }
13787
13914
  });
13788
13915
  ctx.out.log(ctx.json ? JSON.stringify(result, null, 2) : `${slug} \u2192 ${status}`);
@@ -13791,7 +13918,7 @@ async function runbookVisibility(ctx, slug, visibility) {
13791
13918
  if (visibility !== "operator" && visibility !== "admin")
13792
13919
  throw new Error(`visibility must be "operator" or "admin", got "${visibility}"`);
13793
13920
  const runbook = await bySlug(ctx, slug);
13794
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13921
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13795
13922
  patch: { visibility }
13796
13923
  });
13797
13924
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
@@ -13801,7 +13928,7 @@ async function runbookVisibility(ctx, slug, visibility) {
13801
13928
  }
13802
13929
  async function runbookHistory(ctx, slug) {
13803
13930
  const runbook = await bySlug(ctx, slug);
13804
- const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
13931
+ const page2 = await call2(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
13805
13932
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
13806
13933
  ctx.out.log(`${slug} is at v${runbook.version}`);
13807
13934
  if (!page2.records.length) return ctx.out.log("(no earlier versions)");
@@ -13811,7 +13938,7 @@ async function runbookHistory(ctx, slug) {
13811
13938
  }
13812
13939
  async function runbookRevert(ctx, slug, version) {
13813
13940
  const runbook = await bySlug(ctx, slug);
13814
- const result = await call(
13941
+ const result = await call2(
13815
13942
  ctx,
13816
13943
  "POST",
13817
13944
  `/runbook/${encodeURIComponent(runbook.id)}/revert`,
@@ -13822,7 +13949,7 @@ async function runbookRevert(ctx, slug, version) {
13822
13949
  }
13823
13950
  async function runbookRemove(ctx, slug) {
13824
13951
  const runbook = await bySlug(ctx, slug);
13825
- await call(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
13952
+ await call2(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
13826
13953
  ctx.out.log(`removed ${slug}`);
13827
13954
  }
13828
13955
 
@@ -13883,7 +14010,7 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
13883
14010
  async function upsert(ctx, r, visibility) {
13884
14011
  const found = await bySlug(ctx, r.slug).catch(() => null);
13885
14012
  if (!found) {
13886
- await call(ctx, "POST", "/runbook", {
14013
+ await call2(ctx, "POST", "/runbook", {
13887
14014
  appId: ctx.appId,
13888
14015
  input: {
13889
14016
  slug: r.slug,
@@ -13902,7 +14029,7 @@ async function upsert(ctx, r, visibility) {
13902
14029
  ctx.out.log(` = ${r.slug} (already current at v${found.version})`);
13903
14030
  return "unchanged";
13904
14031
  }
13905
- const result = await call(
14032
+ const result = await call2(
13906
14033
  ctx,
13907
14034
  "PATCH",
13908
14035
  `/runbook/${encodeURIComponent(found.id)}`,
@@ -14130,7 +14257,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
14130
14257
  for (const surface of surfaces) {
14131
14258
  const params = new URLSearchParams({ q: surface.query, limit: String(limit) });
14132
14259
  if (!all) params.set("app", ctx.appId);
14133
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
14260
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
14134
14261
  const runbooks = result.outcome === "ranked" ? foldHits(result.hits) : result.candidates.map((c) => ({ ...c, version: 0, sections: [] }));
14135
14262
  out.push({ surface, runbooks });
14136
14263
  }
@@ -14213,7 +14340,7 @@ function lintRunbook(runbook, installed) {
14213
14340
  async function runbookLint(ctx, all) {
14214
14341
  const params = new URLSearchParams();
14215
14342
  if (!all) params.set("app", ctx.appId);
14216
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14343
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14217
14344
  const installed = { "@odla-ai/cli": cliVersion() };
14218
14345
  const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
14219
14346
  if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
@@ -14238,7 +14365,7 @@ async function runbookSearch(ctx, query, all, limit) {
14238
14365
  const params = new URLSearchParams({ q: query });
14239
14366
  if (!all) params.set("app", ctx.appId);
14240
14367
  if (limit) params.set("limit", String(limit));
14241
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
14368
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
14242
14369
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
14243
14370
  if (result.outcome === "empty-corpus") return ctx.out.log("no runbooks are available to search");
14244
14371
  if (result.outcome === "no-match") return ctx.out.log(`no runbook mentions "${query}"`);
@@ -14257,7 +14384,7 @@ async function runbookSearch(ctx, query, all, limit) {
14257
14384
  }
14258
14385
  var JSDOC_POINTER = "Runbooks are procedure. For what an export does \u2014 arguments, defaults, errors \u2014 read its JSDoc: the installed package's .d.ts, or its page under https://odla.ai/docs.";
14259
14386
  async function runbookAsk(ctx, question, all) {
14260
- const result = await call(ctx, "POST", "/runbook/ask", {
14387
+ const result = await call2(ctx, "POST", "/runbook/ask", {
14261
14388
  question,
14262
14389
  ...all ? {} : { app: ctx.appId }
14263
14390
  });
@@ -14287,7 +14414,7 @@ async function runbookAsk(ctx, question, all) {
14287
14414
  }
14288
14415
  async function runbookComment(ctx, slug, body) {
14289
14416
  const found = await bySlug(ctx, slug);
14290
- await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
14417
+ await call2(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
14291
14418
  ctx.out.log(`commented on ${slug} (v${found.version})`);
14292
14419
  }
14293
14420