@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-OWTIDSL5.js";
4
+ } from "./chunk-DKOJBWRJ.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-QR44X5IB.js.map
12
+ //# sourceMappingURL=cli-DZKEPGBZ.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");
@@ -1889,6 +1949,31 @@ var SET_OPTIONS = [
1889
1949
  async function adminCommand(parsed, deps = {}) {
1890
1950
  const area = parsed.positionals[1];
1891
1951
  const action2 = parsed.positionals[2];
1952
+ if (area === "spend") {
1953
+ assertArgs(parsed, JSON_OPTIONS, 4);
1954
+ const context2 = await resolveOperatorContext(parsed, { allowMissingConfig: true });
1955
+ const out = deps.stdout ?? console;
1956
+ const doFetch = deps.fetch ?? fetch;
1957
+ const token = await getDeveloperToken(
1958
+ context2.cfg,
1959
+ {
1960
+ configPath: context2.cfg.configPath,
1961
+ token: stringOpt(parsed.options.token),
1962
+ email: stringOpt(parsed.options.email),
1963
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1964
+ openApprovalUrl: deps.openUrl
1965
+ },
1966
+ doFetch,
1967
+ out
1968
+ );
1969
+ return adminSpend(parsed, {
1970
+ platformUrl: context2.platform.value,
1971
+ token,
1972
+ doFetch,
1973
+ json: parsed.options.json === true,
1974
+ out
1975
+ });
1976
+ }
1892
1977
  const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
1893
1978
  const credentials = action2 === "credentials";
1894
1979
  const models = action2 === "models";
@@ -1992,7 +2077,8 @@ async function fetchIdentity(platformUrl, token, doFetch) {
1992
2077
  email,
1993
2078
  admin: body.admin === true,
1994
2079
  machine,
1995
- scopes
2080
+ scopes,
2081
+ projects: Array.isArray(body.projects) ? body.projects.map(String) : null
1996
2082
  };
1997
2083
  }
1998
2084
  function credentialLabel(identity) {
@@ -2062,6 +2148,13 @@ async function whoamiCommand(parsed, deps = {}) {
2062
2148
  out.log(`credential id: ${identity.credential.id}`);
2063
2149
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
2064
2150
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
2151
+ if (identity.projects === null) {
2152
+ out.log("projects: (this registry does not report project grants)");
2153
+ } else if (identity.projects.length) {
2154
+ out.log(`projects: ${identity.projects.join(", ")}`);
2155
+ } else {
2156
+ out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2157
+ }
2065
2158
  if (!identity.admin) {
2066
2159
  if (identity.scopes.includes("platform:runbook:write")) {
2067
2160
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -5607,10 +5700,10 @@ var import_node_fs16 = require("fs");
5607
5700
  var import_node_os4 = require("os");
5608
5701
  var import_node_path15 = require("path");
5609
5702
 
5610
- // ../harness/dist/chunk-3QP4VDQS.js
5703
+ // ../harness/dist/chunk-LNQNFGQC.js
5611
5704
  var HARNESS_PROTOCOL_VERSION = 1;
5612
5705
 
5613
- // ../harness/dist/chunk-GKDKIU4P.js
5706
+ // ../harness/dist/chunk-K76I2TCQ.js
5614
5707
  var import_child_process = require("child_process");
5615
5708
  var import_fs = require("fs");
5616
5709
  var import_promises2 = require("fs/promises");
@@ -5983,7 +6076,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
5983
6076
  }
5984
6077
  }
5985
6078
 
5986
- // ../harness/dist/chunk-ANNX7VGK.js
6079
+ // ../harness/dist/chunk-UVGZHNLW.js
5987
6080
  var import_crypto = require("crypto");
5988
6081
  var import_promises5 = require("fs/promises");
5989
6082
  var import_path5 = require("path");
@@ -6320,7 +6413,7 @@ function validateSnapshot(snapshot, limits) {
6320
6413
  }
6321
6414
  }
6322
6415
 
6323
- // ../harness/dist/chunk-ANNX7VGK.js
6416
+ // ../harness/dist/chunk-UVGZHNLW.js
6324
6417
  var import_child_process4 = require("child_process");
6325
6418
  var import_promises6 = require("fs/promises");
6326
6419
  var import_path6 = require("path");
@@ -6622,7 +6715,7 @@ function looksLikeDestination(value2) {
6622
6715
  return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text3) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text3);
6623
6716
  }
6624
6717
 
6625
- // ../harness/dist/chunk-ANNX7VGK.js
6718
+ // ../harness/dist/chunk-UVGZHNLW.js
6626
6719
  var import_promises10 = require("fs/promises");
6627
6720
  var import_promises11 = require("fs/promises");
6628
6721
  var import_path10 = require("path");
@@ -6890,7 +6983,7 @@ async function buildCodeGraph(input) {
6890
6983
  return builder.build();
6891
6984
  }
6892
6985
 
6893
- // ../harness/dist/chunk-ANNX7VGK.js
6986
+ // ../harness/dist/chunk-UVGZHNLW.js
6894
6987
  var import_crypto4 = require("crypto");
6895
6988
  async function digestStagedWorkspace(root, limits) {
6896
6989
  const files = [];
@@ -7010,7 +7103,7 @@ function createCodeRuntimeControlClient(options) {
7010
7103
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
7011
7104
  }
7012
7105
  const request3 = options.fetch ?? fetch;
7013
- const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
7106
+ const call4 = async (path, body, timeoutMs = requestTimeoutMs) => {
7014
7107
  const timeout = AbortSignal.timeout(timeoutMs);
7015
7108
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
7016
7109
  let response2;
@@ -7040,17 +7133,17 @@ function createCodeRuntimeControlClient(options) {
7040
7133
  return {
7041
7134
  heartbeat: async (version, capabilities) => {
7042
7135
  validateHeartbeat(version, capabilities);
7043
- return parseSnapshot(await call2("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7136
+ return parseSnapshot(await call4("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7044
7137
  },
7045
7138
  acknowledge: async (commandId, result) => {
7046
7139
  if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
7047
- await call2(`/registry/code/runtime/commands/${commandId}/ack`, result);
7140
+ await call4(`/registry/code/runtime/commands/${commandId}/ack`, result);
7048
7141
  },
7049
7142
  source: async (sessionId) => parseSource(
7050
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7143
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7051
7144
  ),
7052
7145
  infer: async (sessionId, inference) => {
7053
- const value2 = record5(await call2(
7146
+ const value2 = record5(await call4(
7054
7147
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
7055
7148
  inference,
7056
7149
  modelRequestTimeoutMs
@@ -7061,11 +7154,11 @@ function createCodeRuntimeControlClient(options) {
7061
7154
  return value2;
7062
7155
  },
7063
7156
  review: async (sessionId, review) => parseReview(
7064
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7157
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7065
7158
  ),
7066
7159
  submitCandidate: async (sessionId, checkpointId, verification) => {
7067
7160
  if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
7068
- return parseCandidate(await call2(
7161
+ return parseCandidate(await call4(
7069
7162
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
7070
7163
  { checkpointId, verification }
7071
7164
  ));
@@ -7075,21 +7168,21 @@ function createCodeRuntimeControlClient(options) {
7075
7168
  if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
7076
7169
  throw new TypeError("invalid Code session event");
7077
7170
  }
7078
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7171
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7079
7172
  },
7080
7173
  recallMemories: async (sessionId, subjects, limit) => {
7081
- const response2 = await call2(
7174
+ const response2 = await call4(
7082
7175
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
7083
7176
  { subjects: [...subjects], limit }
7084
7177
  );
7085
7178
  return Array.isArray(response2.memories) ? response2.memories : [];
7086
7179
  },
7087
7180
  rememberMemory: async (sessionId, memory) => {
7088
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7181
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7089
7182
  },
7090
7183
  reportSessionFailure: async (sessionId, message2) => {
7091
7184
  if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
7092
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7185
+ await call4(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7093
7186
  }
7094
7187
  };
7095
7188
  }
@@ -7978,7 +8071,7 @@ var SYSTEM_PROMPT_FOR = {
7978
8071
  };
7979
8072
  function codeSkill(opts) {
7980
8073
  let seq = 0;
7981
- const call2 = async (tool, input, signal) => {
8074
+ const call4 = async (tool, input, signal) => {
7982
8075
  const startedAt = Date.now();
7983
8076
  const response2 = await opts.broker.execute(
7984
8077
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
@@ -8000,7 +8093,7 @@ function codeSkill(opts) {
8000
8093
  },
8001
8094
  additionalProperties: false
8002
8095
  },
8003
- handler: (input, ctx) => call2("sandbox.read", input, ctx.signal)
8096
+ handler: (input, ctx) => call4("sandbox.read", input, ctx.signal)
8004
8097
  };
8005
8098
  const applyPatch = {
8006
8099
  name: "odla_apply_git_diff",
@@ -8011,7 +8104,7 @@ function codeSkill(opts) {
8011
8104
  properties: { patch: { type: "string", minLength: 1, maxLength: 262144 } },
8012
8105
  additionalProperties: false
8013
8106
  },
8014
- handler: (input, ctx) => call2("sandbox.apply_patch", input, ctx.signal)
8107
+ handler: (input, ctx) => call4("sandbox.apply_patch", input, ctx.signal)
8015
8108
  };
8016
8109
  const runRecipe = {
8017
8110
  name: "odla_run_recipe",
@@ -8022,7 +8115,7 @@ function codeSkill(opts) {
8022
8115
  properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
8023
8116
  additionalProperties: false
8024
8117
  },
8025
- handler: (input, ctx) => call2("sandbox.run_recipe", input, ctx.signal)
8118
+ handler: (input, ctx) => call4("sandbox.run_recipe", input, ctx.signal)
8026
8119
  };
8027
8120
  const listFiles2 = {
8028
8121
  name: "odla_list",
@@ -8035,7 +8128,7 @@ function codeSkill(opts) {
8035
8128
  },
8036
8129
  additionalProperties: false
8037
8130
  },
8038
- handler: (input, ctx) => call2("sandbox.list", input, ctx.signal)
8131
+ handler: (input, ctx) => call4("sandbox.list", input, ctx.signal)
8039
8132
  };
8040
8133
  const searchFiles = {
8041
8134
  name: "odla_search",
@@ -8051,7 +8144,7 @@ function codeSkill(opts) {
8051
8144
  },
8052
8145
  additionalProperties: false
8053
8146
  },
8054
- handler: (input, ctx) => call2("sandbox.search", input, ctx.signal)
8147
+ handler: (input, ctx) => call4("sandbox.search", input, ctx.signal)
8055
8148
  };
8056
8149
  const graphTool = (name, tool, description, required) => ({
8057
8150
  name,
@@ -8062,7 +8155,7 @@ function codeSkill(opts) {
8062
8155
  properties: { query: { type: "string", maxLength: 512 } },
8063
8156
  additionalProperties: false
8064
8157
  },
8065
- handler: (input, ctx) => call2(tool, input, ctx.signal)
8158
+ handler: (input, ctx) => call4(tool, input, ctx.signal)
8066
8159
  });
8067
8160
  const orientation = [
8068
8161
  graphTool(
@@ -8101,9 +8194,9 @@ async function runCodeAgent(options) {
8101
8194
  lease: options.lease,
8102
8195
  workspaceDir: options.workspaceDir,
8103
8196
  surface,
8104
- onToolCall: (call2) => {
8105
- toolCalls.push(call2);
8106
- options.onToolCall?.(call2);
8197
+ onToolCall: (call4) => {
8198
+ toolCalls.push(call4);
8199
+ options.onToolCall?.(call4);
8107
8200
  }
8108
8201
  });
8109
8202
  const compaction = options.compaction === void 0 ? (0, import_ai4.keepRecentExchanges)({ whenInputTokensExceed: 12e4, keep: 3 }) : options.compaction;
@@ -8188,6 +8281,9 @@ async function handleCodeRuntimeInference(input) {
8188
8281
  call: request3.call
8189
8282
  });
8190
8283
  state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
8284
+ const { costUsd } = response2.receipt;
8285
+ if (costUsd === void 0) state2.costKnown = false;
8286
+ else state2.costUsd += costUsd;
8191
8287
  await input.event({
8192
8288
  type: "usage",
8193
8289
  provider: response2.receipt.provider,
@@ -8197,7 +8293,9 @@ async function handleCodeRuntimeInference(input) {
8197
8293
  durationMs: Date.now() - startedAt,
8198
8294
  interactionId: command.commandId,
8199
8295
  interactionTokens: state2.tokens,
8200
- interactionMaxTokens: metadata2.maxTokensPerInteraction
8296
+ interactionMaxTokens: metadata2.maxTokensPerInteraction,
8297
+ ...costUsd === void 0 ? {} : { costUsd },
8298
+ ...state2.costKnown ? { interactionCostUsd: state2.costUsd } : {}
8201
8299
  }).catch(() => void 0);
8202
8300
  return {
8203
8301
  protocolVersion: HARNESS_PROTOCOL_VERSION,
@@ -9041,10 +9139,16 @@ async function startGoalPursuit(input) {
9041
9139
  attempt: async ({ prompt }) => {
9042
9140
  const result = await input.attempt(prompt);
9043
9141
  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,
9142
+ // What the attempt actually spent, so the runner's token_budget and
9143
+ // cost_budget checks can be reached. This used to be a hardcoded 0 with
9144
+ // no cost at all, which made maxTokens and maxUsd unreachable while
9145
+ // callers reasonably read them as hard ceilings.
9146
+ //
9147
+ // costUsd is omitted rather than zeroed when any call in the attempt
9148
+ // was unpriced: the runner only enforces a cost budget while the cost
9149
+ // is known, and a zero would make it enforce against a lie.
9150
+ tokens: result.tokens ?? 0,
9151
+ ...result.costUsd === void 0 ? {} : { costUsd: result.costUsd },
9048
9152
  ...result.status === "failed" ? { error: result.error ?? "attempt failed" } : {}
9049
9153
  };
9050
9154
  },
@@ -9258,7 +9362,7 @@ var CodePiRuntimeEngine = class {
9258
9362
  recipeAuthorization: this.options.recipeAuthorization
9259
9363
  }, lease, metadata2.role));
9260
9364
  const startedAt = Date.now();
9261
- const interaction = { tokens: 0, noticeEmitted: false };
9365
+ const interaction = { tokens: 0, noticeEmitted: false, costUsd: 0, costKnown: true };
9262
9366
  const inference = createCodeRuntimeInference({
9263
9367
  command,
9264
9368
  metadata: metadata2,
@@ -9296,7 +9400,11 @@ var CodePiRuntimeEngine = class {
9296
9400
  await this.#diagnostic(command, active, detail);
9297
9401
  await this.#failure(command, active, detail);
9298
9402
  }
9299
- return result;
9403
+ return {
9404
+ ...result,
9405
+ tokens: interaction.tokens,
9406
+ ...interaction.costKnown ? { costUsd: interaction.costUsd } : {}
9407
+ };
9300
9408
  }
9301
9409
  /** Report every brokered effect as it starts and finishes. */
9302
9410
  #observed(command, active, broker) {
@@ -10405,6 +10513,8 @@ Usage:
10405
10513
  odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
10406
10514
  odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
10407
10515
  odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
10516
+ odla-ai admin spend show <app:<id>:<incarnation>> [--context <name>] [--json]
10517
+ odla-ai admin spend reset <app:<id>:<incarnation>> [--context <name>] [--json]
10408
10518
  odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
10409
10519
  odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
10410
10520
  odla-ai security plan [--env dev] [--json]
@@ -10497,6 +10607,9 @@ Commands:
10497
10607
  human session connects one); "code grant request|list|approve|revoke" then
10498
10608
  governs unattended access: an agent may request, only a human may approve.
10499
10609
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
10610
+ "admin spend" reads one app's daily inference spend and resumes it
10611
+ after a cap halts it. The cap is per UTC day; exhausting it LATCHES,
10612
+ and a new day does not clear the latch \u2014 resuming is deliberate.
10500
10613
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10501
10614
  pm Project management (via @odla-ai/pm): Products contain Projects;
10502
10615
  projects contain goals, kanban tasks, decisions, and bugs. Use
@@ -10504,6 +10617,12 @@ Commands:
10504
10617
  pass --app/--project explicitly. Same device-grant auth as "app".
10505
10618
  Status changes and comments post to each item's @odla-ai/chat
10506
10619
  discussion thread.
10620
+ NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
10621
+ approves its complete execution contract, so it needs pm.plan,
10622
+ which no device enrollment or handshake approval can grant. An
10623
+ agent proposes in Backlog (the default); a human owner or a
10624
+ pm.plan agent promotes. This is deliberate, not a permission
10625
+ gap \u2014 it was reported as one.
10507
10626
  bug Intent-first alias for PM bugs. "bug report" writes to
10508
10627
  odla PM; odla product defects do not belong in GitHub Issues.
10509
10628
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
@@ -12884,9 +13003,15 @@ async function provisionEnvCredentials(opts) {
12884
13003
  if (o11yToken) {
12885
13004
  opts.stdout.log(`${opts.env}: reusing local o11y ingest token`);
12886
13005
  } else {
12887
- o11yToken = await issueO11yToken(opts);
12888
- credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
12889
- opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
13006
+ o11yToken = await issueO11yToken(opts) ?? void 0;
13007
+ if (o11yToken) {
13008
+ credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
13009
+ opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
13010
+ } else {
13011
+ opts.stdout.log(
13012
+ `${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".`
13013
+ );
13014
+ }
12890
13015
  }
12891
13016
  }
12892
13017
  return save(opts, credentials, tenantId, {
@@ -12940,11 +13065,7 @@ async function issueO11yToken(opts) {
12940
13065
  `${opts.cfg.platformUrl}/o11y/${encodeURIComponent(opts.cfg.app.id)}/token${suffix}?env=${encodeURIComponent(opts.env)}`,
12941
13066
  { method: "POST", headers: { authorization: `Bearer ${opts.developerToken}` } }
12942
13067
  );
12943
- if (res.status === 409 && !opts.rotateO11y) {
12944
- throw new Error(
12945
- `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`
12946
- );
12947
- }
13068
+ if (res.status === 409 && !opts.rotateO11y) return null;
12948
13069
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
12949
13070
  const body = await res.json();
12950
13071
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
@@ -13688,7 +13809,7 @@ function describeUnmet(slug, unmet) {
13688
13809
 
13689
13810
  // src/runbook-actions.ts
13690
13811
  var PLATFORM_SCOPE = "$platform";
13691
- async function call(ctx, method, path, body) {
13812
+ async function call2(ctx, method, path, body) {
13692
13813
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
13693
13814
  method,
13694
13815
  headers: {
@@ -13710,7 +13831,7 @@ async function call(ctx, method, path, body) {
13710
13831
  throw new Error(message2);
13711
13832
  }
13712
13833
  async function bySlug(ctx, slug) {
13713
- const page2 = await call(
13834
+ const page2 = await call2(
13714
13835
  ctx,
13715
13836
  "GET",
13716
13837
  `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
@@ -13719,7 +13840,7 @@ async function bySlug(ctx, slug) {
13719
13840
  if (filtered) return filtered;
13720
13841
  const limit = 100;
13721
13842
  for (let offset = 0; ; offset += limit) {
13722
- const fallback = await call(
13843
+ const fallback = await call2(
13723
13844
  ctx,
13724
13845
  "GET",
13725
13846
  `/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
@@ -13740,7 +13861,7 @@ async function runbookList(ctx, all, query) {
13740
13861
  const params = new URLSearchParams();
13741
13862
  if (!all) params.set("app", ctx.appId);
13742
13863
  if (query) params.set("q", query);
13743
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
13864
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
13744
13865
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
13745
13866
  if (!page2.records.length) return ctx.out.log("(no runbooks)");
13746
13867
  ctx.out.log(["SLUG", "STATUS", "V", "SCOPE", "UPDATED", "TITLE"].join(" "));
@@ -13757,7 +13878,7 @@ async function runbookGet(ctx, slug) {
13757
13878
  ctx.out.log(runbook.body);
13758
13879
  }
13759
13880
  async function runbookNew(ctx, slug, title, body, summary, requires) {
13760
- const created = await call(ctx, "POST", "/runbook", {
13881
+ const created = await call2(ctx, "POST", "/runbook", {
13761
13882
  appId: ctx.appId,
13762
13883
  input: { slug, title, body, ...summary ? { summary } : {}, ...requires ? { requires } : {} }
13763
13884
  });
@@ -13765,7 +13886,7 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
13765
13886
  }
13766
13887
  async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
13767
13888
  const runbook = await bySlug(ctx, slug);
13768
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13889
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13769
13890
  // An empty --requires clears the declaration; omitting the flag leaves
13770
13891
  // whatever is there, so an ordinary body edit never drops it.
13771
13892
  patch: {
@@ -13780,7 +13901,7 @@ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
13780
13901
  }
13781
13902
  async function runbookStatus(ctx, slug, status) {
13782
13903
  const runbook = await bySlug(ctx, slug);
13783
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13904
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13784
13905
  patch: { status }
13785
13906
  });
13786
13907
  ctx.out.log(ctx.json ? JSON.stringify(result, null, 2) : `${slug} \u2192 ${status}`);
@@ -13789,7 +13910,7 @@ async function runbookVisibility(ctx, slug, visibility) {
13789
13910
  if (visibility !== "operator" && visibility !== "admin")
13790
13911
  throw new Error(`visibility must be "operator" or "admin", got "${visibility}"`);
13791
13912
  const runbook = await bySlug(ctx, slug);
13792
- const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13913
+ const result = await call2(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
13793
13914
  patch: { visibility }
13794
13915
  });
13795
13916
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
@@ -13799,7 +13920,7 @@ async function runbookVisibility(ctx, slug, visibility) {
13799
13920
  }
13800
13921
  async function runbookHistory(ctx, slug) {
13801
13922
  const runbook = await bySlug(ctx, slug);
13802
- const page2 = await call(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
13923
+ const page2 = await call2(ctx, "GET", `/runbook/${encodeURIComponent(runbook.id)}/revisions`);
13803
13924
  if (ctx.json) return ctx.out.log(JSON.stringify(page2, null, 2));
13804
13925
  ctx.out.log(`${slug} is at v${runbook.version}`);
13805
13926
  if (!page2.records.length) return ctx.out.log("(no earlier versions)");
@@ -13809,7 +13930,7 @@ async function runbookHistory(ctx, slug) {
13809
13930
  }
13810
13931
  async function runbookRevert(ctx, slug, version) {
13811
13932
  const runbook = await bySlug(ctx, slug);
13812
- const result = await call(
13933
+ const result = await call2(
13813
13934
  ctx,
13814
13935
  "POST",
13815
13936
  `/runbook/${encodeURIComponent(runbook.id)}/revert`,
@@ -13820,7 +13941,7 @@ async function runbookRevert(ctx, slug, version) {
13820
13941
  }
13821
13942
  async function runbookRemove(ctx, slug) {
13822
13943
  const runbook = await bySlug(ctx, slug);
13823
- await call(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
13944
+ await call2(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
13824
13945
  ctx.out.log(`removed ${slug}`);
13825
13946
  }
13826
13947
 
@@ -13879,14 +14000,9 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
13879
14000
  );
13880
14001
  }
13881
14002
  async function upsert(ctx, r, visibility) {
13882
- const page2 = await call(
13883
- ctx,
13884
- "GET",
13885
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
13886
- );
13887
- const found = page2.records[0];
14003
+ const found = await bySlug(ctx, r.slug).catch(() => null);
13888
14004
  if (!found) {
13889
- await call(ctx, "POST", "/runbook", {
14005
+ await call2(ctx, "POST", "/runbook", {
13890
14006
  appId: ctx.appId,
13891
14007
  input: {
13892
14008
  slug: r.slug,
@@ -13905,7 +14021,7 @@ async function upsert(ctx, r, visibility) {
13905
14021
  ctx.out.log(` = ${r.slug} (already current at v${found.version})`);
13906
14022
  return "unchanged";
13907
14023
  }
13908
- const result = await call(
14024
+ const result = await call2(
13909
14025
  ctx,
13910
14026
  "PATCH",
13911
14027
  `/runbook/${encodeURIComponent(found.id)}`,
@@ -14133,7 +14249,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
14133
14249
  for (const surface of surfaces) {
14134
14250
  const params = new URLSearchParams({ q: surface.query, limit: String(limit) });
14135
14251
  if (!all) params.set("app", ctx.appId);
14136
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
14252
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
14137
14253
  const runbooks = result.outcome === "ranked" ? foldHits(result.hits) : result.candidates.map((c) => ({ ...c, version: 0, sections: [] }));
14138
14254
  out.push({ surface, runbooks });
14139
14255
  }
@@ -14216,7 +14332,7 @@ function lintRunbook(runbook, installed) {
14216
14332
  async function runbookLint(ctx, all) {
14217
14333
  const params = new URLSearchParams();
14218
14334
  if (!all) params.set("app", ctx.appId);
14219
- const page2 = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14335
+ const page2 = await call2(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
14220
14336
  const installed = { "@odla-ai/cli": cliVersion() };
14221
14337
  const findings = page2.records.flatMap((runbook) => lintRunbook(runbook, installed));
14222
14338
  if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page2.records.length, findings }, null, 2));
@@ -14241,7 +14357,7 @@ async function runbookSearch(ctx, query, all, limit) {
14241
14357
  const params = new URLSearchParams({ q: query });
14242
14358
  if (!all) params.set("app", ctx.appId);
14243
14359
  if (limit) params.set("limit", String(limit));
14244
- const result = await call(ctx, "GET", `/runbook/search?${params}`);
14360
+ const result = await call2(ctx, "GET", `/runbook/search?${params}`);
14245
14361
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
14246
14362
  if (result.outcome === "empty-corpus") return ctx.out.log("no runbooks are available to search");
14247
14363
  if (result.outcome === "no-match") return ctx.out.log(`no runbook mentions "${query}"`);
@@ -14260,7 +14376,7 @@ async function runbookSearch(ctx, query, all, limit) {
14260
14376
  }
14261
14377
  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.";
14262
14378
  async function runbookAsk(ctx, question, all) {
14263
- const result = await call(ctx, "POST", "/runbook/ask", {
14379
+ const result = await call2(ctx, "POST", "/runbook/ask", {
14264
14380
  question,
14265
14381
  ...all ? {} : { app: ctx.appId }
14266
14382
  });
@@ -14289,14 +14405,8 @@ async function runbookAsk(ctx, question, all) {
14289
14405
  ctx.out.log(JSDOC_POINTER);
14290
14406
  }
14291
14407
  async function runbookComment(ctx, slug, body) {
14292
- const page2 = await call(
14293
- ctx,
14294
- "GET",
14295
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
14296
- );
14297
- const found = page2.records[0];
14298
- if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
14299
- await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
14408
+ const found = await bySlug(ctx, slug);
14409
+ await call2(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
14300
14410
  ctx.out.log(`commented on ${slug} (v${found.version})`);
14301
14411
  }
14302
14412
 
@@ -14346,13 +14456,7 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
14346
14456
 
14347
14457
  // src/runbook-edit-flow.ts
14348
14458
  async function editRunbook(ctx, slug, deps = {}) {
14349
- const page2 = await call(
14350
- ctx,
14351
- "GET",
14352
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
14353
- );
14354
- const found = page2.records[0];
14355
- if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
14459
+ const found = await bySlug(ctx, slug);
14356
14460
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
14357
14461
  const body = await editText(found.body, slug, deps);
14358
14462
  return body === null ? null : { body, expectedVersion: found.version };