@odla-ai/cli 0.25.23 → 0.26.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
@@ -182,10 +182,10 @@ function isManagedDevVar(line) {
182
182
  const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
183
183
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
184
184
  }
185
- function writePrivateText(path, text) {
185
+ function writePrivateText(path, text2) {
186
186
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
187
187
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
188
- (0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
188
+ (0, import_node_fs.writeFileSync)(temporary, text2, { mode: 384 });
189
189
  (0, import_node_fs.chmodSync)(temporary, 384);
190
190
  (0, import_node_fs.renameSync)(temporary, path);
191
191
  }
@@ -492,6 +492,7 @@ function audienceBoundEnvToken(token, platform) {
492
492
  }
493
493
  var SCOPE_PURPOSE = {
494
494
  "platform:status:read": "read the platform fleet health and deployment snapshot",
495
+ "platform:chat:credential:write": "rotate the built-in Discussion responder credential",
495
496
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
496
497
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
497
498
  "platform:runbook:write": "add or edit odla's operational runbooks",
@@ -551,6 +552,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
551
552
  return token;
552
553
  }
553
554
 
555
+ // src/principal-presentation.ts
556
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
557
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
558
+ const id = typeof principalId === "string" ? principalId.trim() : "";
559
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
560
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
561
+ }
562
+
554
563
  // src/admin-ai-audit.ts
555
564
  function adminAiAuditQuery(filters) {
556
565
  if (filters.limit === void 0) return "";
@@ -580,17 +589,17 @@ async function readAdminAiAudit(request2) {
580
589
  String(event.changeKind ?? ""),
581
590
  String(event.purpose ?? event.provider ?? ""),
582
591
  route2,
583
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
592
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
584
593
  ].join(" "));
585
594
  }
586
595
  }
587
596
  async function responseBody(response2) {
588
- const text = await response2.text();
589
- if (!text) return {};
597
+ const text2 = await response2.text();
598
+ if (!text2) return {};
590
599
  try {
591
- return JSON.parse(text);
600
+ return JSON.parse(text2);
592
601
  } catch {
593
- return { message: text.slice(0, 300) };
602
+ return { message: text2.slice(0, 300) };
594
603
  }
595
604
  }
596
605
  function apiError(status, body) {
@@ -671,7 +680,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
671
680
  timestamp2(event.created_at),
672
681
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
673
682
  String(event.run_id ?? ""),
674
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
683
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
675
684
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
676
685
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
677
686
  String(input + output),
@@ -692,12 +701,12 @@ function timestamp2(value2) {
692
701
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
693
702
  }
694
703
  async function responseBody2(res) {
695
- const text = await res.text();
696
- if (!text) return {};
704
+ const text2 = await res.text();
705
+ if (!text2) return {};
697
706
  try {
698
- return JSON.parse(text);
707
+ return JSON.parse(text2);
699
708
  } catch {
700
- return { message: text.slice(0, 300) };
709
+ return { message: text2.slice(0, 300) };
701
710
  }
702
711
  }
703
712
  function apiError2(action2, status, body) {
@@ -873,12 +882,12 @@ function catalogModels(body) {
873
882
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
874
883
  }
875
884
  async function responseBody3(res) {
876
- const text = await res.text();
877
- if (!text) return {};
885
+ const text2 = await res.text();
886
+ if (!text2) return {};
878
887
  try {
879
- return JSON.parse(text);
888
+ return JSON.parse(text2);
880
889
  } catch {
881
- return { message: text.slice(0, 300) };
890
+ return { message: text2.slice(0, 300) };
882
891
  }
883
892
  }
884
893
  function apiError3(action2, status, body) {
@@ -1722,8 +1731,8 @@ async function appImport(options) {
1722
1731
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1723
1732
  const doFetch = options.fetch ?? fetch;
1724
1733
  const { tenant } = resolveTenant(cfg, options.env);
1725
- const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1726
- const { format, sources } = (0, import_import.parseImport)(text, options.ns);
1734
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1735
+ const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1727
1736
  if (format === "namespace-map" && options.ns) {
1728
1737
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1729
1738
  }
@@ -1814,7 +1823,10 @@ function report(options, owners, headline) {
1814
1823
  if (headline) out.log(headline);
1815
1824
  out.log(`owners (${owners.length}):`);
1816
1825
  for (const o of owners) {
1817
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
1826
+ const name = o.email?.trim() || "Unnamed member";
1827
+ out.log(
1828
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1829
+ );
1818
1830
  }
1819
1831
  }
1820
1832
  async function ownersList(options) {
@@ -2731,9 +2743,9 @@ function canonicalValue(value2) {
2731
2743
  }
2732
2744
  if (Array.isArray(value2)) return value2.map(canonicalValue);
2733
2745
  if (value2 && typeof value2 === "object") {
2734
- const record10 = value2;
2746
+ const record11 = value2;
2735
2747
  return Object.fromEntries(
2736
- Object.keys(record10).filter((key) => record10[key] !== void 0).sort().map((key) => [key, canonicalValue(record10[key])])
2748
+ Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, canonicalValue(record11[key])])
2737
2749
  );
2738
2750
  }
2739
2751
  throw new TypeError("canonical JSON rejects unsupported values");
@@ -3559,22 +3571,26 @@ function planNextActions(reconciliation, configPath) {
3559
3571
  }
3560
3572
  }
3561
3573
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3562
- const { appId, platformUrl } = reconciliation.scope;
3563
3574
  next.push({
3564
3575
  code: "review_destructive",
3565
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3576
+ command: studioSettingsUrl(reconciliation),
3566
3577
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3567
3578
  });
3568
3579
  }
3569
3580
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3570
3581
  next.push({
3571
3582
  code: "declare_or_accept_runtime_state",
3572
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3583
+ command: studioSettingsUrl(reconciliation),
3573
3584
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3574
3585
  });
3575
3586
  }
3576
3587
  return next;
3577
3588
  }
3589
+ function studioSettingsUrl(reconciliation) {
3590
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
3591
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
3592
+ return `${origin}${(0, import_apps8.studioAppSettingsPath)(reconciliation.scope.appId, environment2, "environment")}`;
3593
+ }
3578
3594
  function quoteArg2(value2) {
3579
3595
  return `'${value2.replace(/'/g, `'\\''`)}'`;
3580
3596
  }
@@ -3614,15 +3630,15 @@ function readWranglerConfig(path) {
3614
3630
  return null;
3615
3631
  }
3616
3632
  }
3617
- function stripJsonComments(text) {
3633
+ function stripJsonComments(text2) {
3618
3634
  let result = "";
3619
3635
  let inString = false;
3620
- for (let i = 0; i < text.length; i++) {
3621
- const ch = text[i];
3636
+ for (let i = 0; i < text2.length; i++) {
3637
+ const ch = text2[i];
3622
3638
  if (inString) {
3623
3639
  result += ch;
3624
3640
  if (ch === "\\") {
3625
- result += text[i + 1] ?? "";
3641
+ result += text2[i + 1] ?? "";
3626
3642
  i++;
3627
3643
  } else if (ch === '"') {
3628
3644
  inString = false;
@@ -3634,14 +3650,14 @@ function stripJsonComments(text) {
3634
3650
  result += ch;
3635
3651
  continue;
3636
3652
  }
3637
- if (ch === "/" && text[i + 1] === "/") {
3638
- while (i < text.length && text[i] !== "\n") i++;
3653
+ if (ch === "/" && text2[i + 1] === "/") {
3654
+ while (i < text2.length && text2[i] !== "\n") i++;
3639
3655
  result += "\n";
3640
3656
  continue;
3641
3657
  }
3642
- if (ch === "/" && text[i + 1] === "*") {
3658
+ if (ch === "/" && text2[i + 1] === "*") {
3643
3659
  i += 2;
3644
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3660
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
3645
3661
  i++;
3646
3662
  continue;
3647
3663
  }
@@ -3658,7 +3674,14 @@ async function wranglerLoggedIn(run, cwd) {
3658
3674
  }
3659
3675
  }
3660
3676
  function wranglerPutSecret(run, opts) {
3661
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
3677
+ const args = [
3678
+ "wrangler",
3679
+ "secret",
3680
+ "put",
3681
+ opts.name,
3682
+ ...opts.env ? ["--env", opts.env] : [],
3683
+ ...opts.configPath ? ["--config", opts.configPath] : []
3684
+ ];
3662
3685
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3663
3686
  }
3664
3687
 
@@ -4036,9 +4059,9 @@ function initProject(options) {
4036
4059
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
4037
4060
  out.log("updated .gitignore for local odla credentials");
4038
4061
  }
4039
- function writeIfMissing(path, text) {
4062
+ function writeIfMissing(path, text2) {
4040
4063
  if ((0, import_node_fs13.existsSync)(path)) return;
4041
- (0, import_node_fs13.writeFileSync)(path, text);
4064
+ (0, import_node_fs13.writeFileSync)(path, text2);
4042
4065
  }
4043
4066
  function configTemplate(input) {
4044
4067
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4246,13 +4269,13 @@ async function secretsSetClerkKey(options) {
4246
4269
  body: JSON.stringify({ value: value2 })
4247
4270
  });
4248
4271
  if (!res.ok) {
4249
- const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4250
- throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
4272
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4273
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
4251
4274
  }
4252
4275
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
4253
4276
  }
4254
- function scrubValue(text, value2) {
4255
- return redactSecrets(text).split(value2).join("[value redacted]");
4277
+ function scrubValue(text2, value2) {
4278
+ return redactSecrets(text2).split(value2).join("[value redacted]");
4256
4279
  }
4257
4280
  async function resolveVaultWrite(options) {
4258
4281
  const out = options.stdout ?? console;
@@ -5080,8 +5103,8 @@ async function runContainerAttempt(options) {
5080
5103
  let stopped = false;
5081
5104
  let exited = false;
5082
5105
  child.stderr.setEncoding("utf8");
5083
- child.stderr.on("data", (text) => {
5084
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
5106
+ child.stderr.on("data", (text2) => {
5107
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
5085
5108
  });
5086
5109
  const stop = (reason) => {
5087
5110
  if (stopped || exited) return;
@@ -5239,8 +5262,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
5239
5262
  const maxFiles = options.maxFiles ?? 2e4;
5240
5263
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
5241
5264
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
5242
- const entries = inventory.flatMap((record10) => {
5243
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record10);
5265
+ const entries = inventory.flatMap((record11) => {
5266
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record11);
5244
5267
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
5245
5268
  });
5246
5269
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -5488,8 +5511,8 @@ function normalize(value2) {
5488
5511
  if (Array.isArray(value2)) return value2.map(normalize);
5489
5512
  if (value2 instanceof Uint8Array) return { $bytes: [...value2] };
5490
5513
  if (typeof value2 === "object") {
5491
- const record10 = value2;
5492
- return Object.fromEntries(Object.keys(record10).filter((key) => record10[key] !== void 0).sort().map((key) => [key, normalize(record10[key])]));
5514
+ const record11 = value2;
5515
+ return Object.fromEntries(Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, normalize(record11[key])]));
5493
5516
  }
5494
5517
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
5495
5518
  }
@@ -5913,8 +5936,8 @@ function boundedInteger(value2, spec) {
5913
5936
  }
5914
5937
  function boundedNumber(value2, spec) {
5915
5938
  if (spec.kind !== "finite_number" || typeof value2 !== "number" || !Number.isFinite(value2) || value2 < spec.minimum || value2 > spec.maximum) throw new CamelError("conversion_rejected", "Finite-number conversion rejected the structured value.");
5916
- const text = String(value2);
5917
- if (/e/i.test(text) || (text.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
5939
+ const text2 = String(value2);
5940
+ if (/e/i.test(text2) || (text2.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
5918
5941
  return value2;
5919
5942
  }
5920
5943
  function enumMember(value2, spec) {
@@ -6070,8 +6093,8 @@ function validateUnsafeSelector(path, value2, tool) {
6070
6093
  return void 0;
6071
6094
  }
6072
6095
  function looksLikeDestination(value2) {
6073
- const text = value2.trim();
6074
- return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
6096
+ const text2 = value2.trim();
6097
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
6075
6098
  }
6076
6099
 
6077
6100
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -6440,8 +6463,8 @@ function gitApply(cwd, patch2, check) {
6440
6463
  });
6441
6464
  let stderr = "";
6442
6465
  child.stderr.setEncoding("utf8");
6443
- child.stderr.on("data", (text) => {
6444
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
6466
+ child.stderr.on("data", (text2) => {
6467
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
6445
6468
  });
6446
6469
  child.once("error", reject);
6447
6470
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -7612,10 +7635,10 @@ var CodePiRuntimeEngine = class {
7612
7635
  task: lease.task,
7613
7636
  limits: this.options.limits,
7614
7637
  signal: active.abort.signal,
7615
- onStderr: (text) => this.#event(command, {
7638
+ onStderr: (text2) => this.#event(command, {
7616
7639
  type: "message",
7617
7640
  actor: "system",
7618
- body: text.slice(0, 4e3)
7641
+ body: text2.slice(0, 4e3)
7619
7642
  }, active.conversationRefs),
7620
7643
  onMessage: async (output) => {
7621
7644
  if (output.type === "inference.request") {
@@ -8497,7 +8520,8 @@ Usage:
8497
8520
  odla-ai doctor [--config odla.config.mjs]
8498
8521
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8499
8522
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
8500
- odla-ai operations <get|wait> <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8523
+ odla-ai operations get <operation-id> [--json]
8524
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8501
8525
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8502
8526
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8503
8527
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -8548,6 +8572,7 @@ Usage:
8548
8572
  odla-ai context remove <name> --yes [--json]
8549
8573
  odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
8550
8574
  odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8575
+ odla-ai platform chat-credentials rotate [--context <name>] [--email <odla-account>] [--wrangler-config <path>] [--expected-version <id>] [--json] --yes
8551
8576
  odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
8552
8577
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
8553
8578
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -8611,9 +8636,9 @@ Commands:
8611
8636
  does and what it guarantees is JSDoc, rendered per package at
8612
8637
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
8613
8638
  a question usually needs both.
8614
- whoami Report who this terminal is authenticated as, and whether it holds
8615
- platform admin. A handshake-minted device token is never admin,
8616
- however the human who approved it is configured.
8639
+ whoami Report the actual principal name, handle and kind, its manager,
8640
+ credential kind, accountable owner, and platform-admin status.
8641
+ A manager relationship never supplies authority by itself.
8617
8642
  context Explain selected config, platform, app, environment, and
8618
8643
  developer-token provenance without printing credentials or
8619
8644
  starting a device handshake. Operator work can run outside a
@@ -8733,6 +8758,58 @@ Safety:
8733
8758
  `);
8734
8759
  }
8735
8760
 
8761
+ // src/discuss-principals.ts
8762
+ function mergeDiscussPrincipals(target, source) {
8763
+ Object.assign(target.authors, source.authors ?? {});
8764
+ Object.assign(target.principals, source.principals ?? {});
8765
+ }
8766
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
8767
+ function discussPrincipalLabel(principalId, authorKind, projection) {
8768
+ const profile = projection.principals?.[principalId];
8769
+ if (profile) {
8770
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
8771
+ if (profile.kind === "agent") {
8772
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
8773
+ }
8774
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
8775
+ }
8776
+ const legacyName = projection.authors?.[principalId]?.trim();
8777
+ if (legacyName) {
8778
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
8779
+ }
8780
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
8781
+ }
8782
+
8783
+ // src/discuss-read-render.ts
8784
+ function discussBodyWithRefs(post) {
8785
+ if (!post.refs || post.refs.length === 0) return post.body;
8786
+ let out = "";
8787
+ let cursor = 0;
8788
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8789
+ if (ref.start < cursor || ref.end > post.body.length) continue;
8790
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8791
+ cursor = ref.end;
8792
+ }
8793
+ return out + post.body.slice(cursor);
8794
+ }
8795
+ function renderDiscussRead(ctx, topic, posts, projection) {
8796
+ const status = topic.resolved ? "resolved" : "open";
8797
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
8798
+ for (const post of posts) {
8799
+ const who = discussPrincipalLabel(
8800
+ post.authorId,
8801
+ post.authorKind,
8802
+ projection
8803
+ );
8804
+ ctx.out.log(`
8805
+ \u2014 ${who}`);
8806
+ ctx.out.log(discussBodyWithRefs(post));
8807
+ for (const file of post.attachments ?? []) {
8808
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8809
+ }
8810
+ }
8811
+ }
8812
+
8736
8813
  // src/discuss-actions.ts
8737
8814
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8738
8815
  async function request(ctx, method, path, body) {
@@ -8751,17 +8828,6 @@ function emit(ctx, value2, human) {
8751
8828
  else human();
8752
8829
  }
8753
8830
  var state = (topic) => topic.resolved ? "resolved" : "open";
8754
- function bodyWithRefs(post) {
8755
- if (!post.refs || post.refs.length === 0) return post.body;
8756
- let out = "";
8757
- let cursor = 0;
8758
- for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8759
- if (ref.start < cursor || ref.end > post.body.length) continue;
8760
- out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8761
- cursor = ref.end;
8762
- }
8763
- return out + post.body.slice(cursor);
8764
- }
8765
8831
  function content(parsed) {
8766
8832
  const markup = stringOpt(parsed.options.markup);
8767
8833
  if (markup) return { markup };
@@ -8816,11 +8882,19 @@ async function discussRead(ctx, id, parsed) {
8816
8882
  offset: requestedOffset ?? "0"
8817
8883
  });
8818
8884
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
8819
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
8885
+ emit(
8886
+ ctx,
8887
+ page,
8888
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
8889
+ );
8820
8890
  return;
8821
8891
  }
8822
8892
  for (let scan = 0; scan < 3; scan++) {
8823
8893
  const posts = /* @__PURE__ */ new Map();
8894
+ const projection = {
8895
+ authors: {},
8896
+ principals: {}
8897
+ };
8824
8898
  let topic = null;
8825
8899
  let offset = 0;
8826
8900
  for (; ; ) {
@@ -8831,6 +8905,7 @@ async function discussRead(ctx, id, parsed) {
8831
8905
  );
8832
8906
  topic = page.topic;
8833
8907
  for (const post of page.posts) posts.set(post.id, post);
8908
+ mergeDiscussPrincipals(projection, page);
8834
8909
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
8835
8910
  if (!page.page?.hasMore) break;
8836
8911
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -8843,24 +8918,18 @@ async function discussRead(ctx, id, parsed) {
8843
8918
  );
8844
8919
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
8845
8920
  if (ordered.length === expected) {
8846
- const result = { topic, posts: ordered };
8847
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
8921
+ const result = { topic, posts: ordered, ...projection };
8922
+ emit(
8923
+ ctx,
8924
+ result,
8925
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
8926
+ );
8848
8927
  return;
8849
8928
  }
8850
8929
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
8851
8930
  }
8852
8931
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
8853
8932
  }
8854
- function renderRead(ctx, topic, posts) {
8855
- ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
8856
- for (const post of posts) {
8857
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8858
- ctx.out.log(`
8859
- \u2014 ${who}`);
8860
- ctx.out.log(bodyWithRefs(post));
8861
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8862
- }
8863
- }
8864
8933
  async function discussPost(ctx, parsed) {
8865
8934
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8866
8935
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -9085,6 +9154,8 @@ async function discussWatch(ctx, topicId, parsed) {
9085
9154
  found: true,
9086
9155
  cursor,
9087
9156
  events: matching,
9157
+ ...page.authors ? { authors: page.authors } : {},
9158
+ ...page.principals ? { principals: page.principals } : {},
9088
9159
  ...posts && posts.length > 0 ? { posts } : {},
9089
9160
  ...topics && topics.length > 0 ? { topics } : {}
9090
9161
  });
@@ -9117,7 +9188,11 @@ function report2(ctx, parsed, result) {
9117
9188
  ctx.out.log(JSON.stringify(result, null, 2));
9118
9189
  } else if (parsed.options.jsonl !== true && result.found) {
9119
9190
  for (const post of result.posts ?? []) {
9120
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9191
+ const who = discussPrincipalLabel(
9192
+ post.authorId,
9193
+ post.authorKind,
9194
+ result
9195
+ );
9121
9196
  ctx.out.log(`\u2014 ${who}
9122
9197
  ${post.body}`);
9123
9198
  }
@@ -9262,8 +9337,8 @@ function collectFields(parsed, allowClear) {
9262
9337
  if (allowClear) out[spec.key] = null;
9263
9338
  continue;
9264
9339
  }
9265
- const text = stringOpt(value2);
9266
- out[spec.key] = spec.num ? Number(text) : text;
9340
+ const text2 = stringOpt(value2);
9341
+ out[spec.key] = spec.num ? Number(text2) : text2;
9267
9342
  }
9268
9343
  return out;
9269
9344
  }
@@ -9330,8 +9405,8 @@ async function pmAdd(ctx, entity, parsed) {
9330
9405
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
9331
9406
  }
9332
9407
  async function pmGet(ctx, entity, id) {
9333
- const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9334
- emit2(ctx, record10, () => printRecord(ctx, entity, record10));
9408
+ const { record: record11 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9409
+ emit2(ctx, record11, () => printRecord(ctx, entity, record11));
9335
9410
  }
9336
9411
  async function pmSet(ctx, entity, id, parsed) {
9337
9412
  const patch2 = collectEntityFields(entity, parsed, true);
@@ -9376,9 +9451,9 @@ async function pmHandoff(ctx, parsed) {
9376
9451
  ]);
9377
9452
  const handoff = {
9378
9453
  appId,
9379
- unmetGoals: goals.filter((record10) => record10.status !== "met"),
9380
- activeTasks: tasks.filter((record10) => record10.column !== "done"),
9381
- openBugs: bugs.filter((record10) => record10.status !== "fixed" && record10.status !== "wontfix")
9454
+ unmetGoals: goals.filter((record11) => record11.status !== "met"),
9455
+ activeTasks: tasks.filter((record11) => record11.column !== "done"),
9456
+ openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
9382
9457
  };
9383
9458
  const result = {
9384
9459
  ...handoff,
@@ -9397,10 +9472,10 @@ async function pmHandoff(ctx, parsed) {
9397
9472
  ]) {
9398
9473
  ctx.out.log(`${label}:`);
9399
9474
  if (!records.length) ctx.out.log("- (none)");
9400
- else for (const record10 of records) printRecord(
9475
+ else for (const record11 of records) printRecord(
9401
9476
  ctx,
9402
9477
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
9403
- record10
9478
+ record11
9404
9479
  );
9405
9480
  }
9406
9481
  });
@@ -9591,19 +9666,180 @@ function age(input) {
9591
9666
  return `${Math.round(input / 6e4)}m`;
9592
9667
  }
9593
9668
 
9669
+ // src/platform-chat-credential-command.ts
9670
+ var import_node_process10 = __toESM(require("process"), 1);
9671
+ async function rotatePlatformChatCredential(parsed, deps) {
9672
+ assertArgs(
9673
+ parsed,
9674
+ [
9675
+ "config",
9676
+ "context",
9677
+ "platform",
9678
+ "token",
9679
+ "email",
9680
+ "open",
9681
+ "json",
9682
+ "yes",
9683
+ "wrangler-config",
9684
+ "expected-version"
9685
+ ],
9686
+ 3
9687
+ );
9688
+ if (parsed.options.yes !== true) {
9689
+ throw new Error(
9690
+ "platform chat-credentials rotate changes the production chat secret; pass --yes"
9691
+ );
9692
+ }
9693
+ const context = await resolveOperatorContext(parsed, {
9694
+ allowMissingConfig: true
9695
+ });
9696
+ const platform = context.platform.value;
9697
+ const doFetch = deps.fetch ?? fetch;
9698
+ const out = deps.stdout ?? console;
9699
+ const run = deps.runner ?? defaultRunner;
9700
+ const cwd = import_node_process10.default.cwd();
9701
+ const wranglerConfig = stringOpt(parsed.options["wrangler-config"]) ?? "packages/chat-agent/wrangler.jsonc";
9702
+ if (!await wranglerLoggedIn(run, cwd)) {
9703
+ throw new Error(
9704
+ 'Wrangler is not authenticated; run "npx wrangler login" and retry'
9705
+ );
9706
+ }
9707
+ const priorHealth = await doFetch(`${platform}/health/services/chat`);
9708
+ const priorVersion = priorHealth.headers.get("x-odla-worker-version-id");
9709
+ await priorHealth.body?.cancel().catch(() => {
9710
+ });
9711
+ if (!priorVersion) {
9712
+ throw new Error(
9713
+ "chat health did not identify its current Worker version; refusing to rotate"
9714
+ );
9715
+ }
9716
+ const expectedVersion = stringOpt(parsed.options["expected-version"]);
9717
+ if (expectedVersion && priorVersion !== expectedVersion) {
9718
+ throw new Error(
9719
+ `chat health answered from version ${priorVersion}; expected ${expectedVersion}`
9720
+ );
9721
+ }
9722
+ const token = await resolveAdminPlatformToken({
9723
+ platform,
9724
+ scope: "platform:chat:credential:write",
9725
+ token: stringOpt(parsed.options.token),
9726
+ tokenFile: context.credentials.scopedTokenFile,
9727
+ email: stringOpt(parsed.options.email),
9728
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9729
+ fetch: doFetch,
9730
+ stdout: out,
9731
+ openApprovalUrl: deps.openUrl,
9732
+ label: "odla CLI (rotate built-in Discussion responder)"
9733
+ });
9734
+ const mintedResponse = await doFetch(
9735
+ `${platform}/registry/platform/chat-credentials/rotate`,
9736
+ {
9737
+ method: "POST",
9738
+ headers: { authorization: `Bearer ${token}` }
9739
+ }
9740
+ );
9741
+ const minted = await mintedResponse.json().catch(() => null);
9742
+ if (!mintedResponse.ok) {
9743
+ throw new Error(
9744
+ `mint Discussion credential failed (HTTP ${mintedResponse.status}): ${apiMessage(minted)}`
9745
+ );
9746
+ }
9747
+ if (!isPlatformChatCredential(minted)) {
9748
+ throw new Error(
9749
+ "platform returned an invalid odla.platform-chat-credential/v1 envelope"
9750
+ );
9751
+ }
9752
+ const put = await wranglerPutSecret(run, {
9753
+ name: "ODLA_BOT_TOKENS",
9754
+ value: JSON.stringify({
9755
+ [minted.appId]: { [minted.principalId]: minted.key }
9756
+ }),
9757
+ configPath: wranglerConfig,
9758
+ cwd
9759
+ });
9760
+ if (put.code !== 0) {
9761
+ throw new Error(
9762
+ "Wrangler could not replace ODLA_BOT_TOKENS; the new scoped key was not activated"
9763
+ );
9764
+ }
9765
+ const version = await waitForRotatedChatHealth({
9766
+ fetch: doFetch,
9767
+ platform,
9768
+ priorVersion,
9769
+ wait: deps.pollWait
9770
+ });
9771
+ const result = {
9772
+ schemaVersion: "odla.platform-chat-credential-rotation/v1",
9773
+ appId: minted.appId,
9774
+ appIncarnation: minted.appIncarnation,
9775
+ principalId: minted.principalId,
9776
+ health: { ok: true, service: "odla-chat-agent", version }
9777
+ };
9778
+ if (parsed.options.json === true) {
9779
+ out.log(JSON.stringify(result, null, 2));
9780
+ } else {
9781
+ out.log(
9782
+ `rotated ${result.appId} ${result.principalId} ${result.health.version}`
9783
+ );
9784
+ }
9785
+ }
9786
+ async function waitForRotatedChatHealth(opts) {
9787
+ const wait2 = opts.wait ?? ((milliseconds) => new Promise((resolve12) => setTimeout(resolve12, milliseconds)));
9788
+ let lastStatus = 0;
9789
+ let lastVersion = null;
9790
+ let lastError = "private_service_unready";
9791
+ for (let attempt = 0; attempt < 60; attempt++) {
9792
+ const response2 = await opts.fetch(
9793
+ `${opts.platform}/health/services/chat`
9794
+ );
9795
+ const body = await response2.json().catch(() => null);
9796
+ const version = response2.headers.get("x-odla-worker-version-id");
9797
+ if (response2.ok && record7(body) && body.ok === true && body.service === "odla-chat-agent" && version && version !== opts.priorVersion) {
9798
+ return version;
9799
+ }
9800
+ lastStatus = response2.status;
9801
+ lastVersion = version;
9802
+ lastError = record7(body) && typeof body.error === "string" ? body.error : "private_service_unready";
9803
+ if (attempt < 59) await wait2(5e3);
9804
+ }
9805
+ throw new Error(
9806
+ `chat health did not converge on the rotated Worker deployment (HTTP ${lastStatus}, version ${lastVersion ?? "unknown"}, ${lastError})`
9807
+ );
9808
+ }
9809
+ function isPlatformChatCredential(value2) {
9810
+ return record7(value2) && value2.schemaVersion === "odla.platform-chat-credential/v1" && value2.appId === "odla-pm" && typeof value2.appIncarnation === "string" && value2.appIncarnation.length > 0 && value2.principalId === "agent_odla" && typeof value2.key === "string" && value2.key.startsWith("odla_sk_");
9811
+ }
9812
+ function apiMessage(value2) {
9813
+ if (!record7(value2)) return "request failed";
9814
+ const error = record7(value2.error) ? value2.error : value2;
9815
+ return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9816
+ }
9817
+ function record7(value2) {
9818
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9819
+ }
9820
+
9594
9821
  // src/platform-command.ts
9595
9822
  async function platformCommand(parsed, deps = {}) {
9823
+ const action2 = parsed.positionals[1];
9824
+ if (action2 === "status") {
9825
+ return platformStatus(parsed, deps);
9826
+ }
9827
+ if (action2 === "chat-credentials" && parsed.positionals[2] === "rotate") {
9828
+ return rotatePlatformChatCredential(parsed, deps);
9829
+ }
9830
+ throw new Error(
9831
+ `unknown platform action "${[
9832
+ action2,
9833
+ parsed.positionals[2]
9834
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
9835
+ );
9836
+ }
9837
+ async function platformStatus(parsed, deps) {
9596
9838
  assertArgs(
9597
9839
  parsed,
9598
9840
  ["config", "context", "platform", "token", "email", "open", "json"],
9599
9841
  2
9600
9842
  );
9601
- const action2 = parsed.positionals[1];
9602
- if (action2 !== "status") {
9603
- throw new Error(
9604
- `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
9605
- );
9606
- }
9607
9843
  const context = await resolveOperatorContext(parsed, {
9608
9844
  allowMissingConfig: true
9609
9845
  });
@@ -9628,7 +9864,7 @@ async function platformCommand(parsed, deps = {}) {
9628
9864
  const body = await response2.json().catch(() => null);
9629
9865
  if (!response2.ok) {
9630
9866
  throw new Error(
9631
- `read platform status failed (HTTP ${response2.status}): ${apiMessage(body)}`
9867
+ `read platform status failed (HTTP ${response2.status}): ${apiMessage2(body)}`
9632
9868
  );
9633
9869
  }
9634
9870
  if (!isPlatformStatus(body)) {
@@ -9641,17 +9877,17 @@ async function platformCommand(parsed, deps = {}) {
9641
9877
  }
9642
9878
  }
9643
9879
  function isPlatformStatus(value2) {
9644
- if (!record7(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9645
- if (!record7(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9646
- if (!record7(value2.catalog) || !record7(value2.summary)) return false;
9880
+ if (!record8(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9881
+ if (!record8(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9882
+ if (!record8(value2.catalog) || !record8(value2.summary)) return false;
9647
9883
  return Array.isArray(value2.services) && Array.isArray(value2.nextActions);
9648
9884
  }
9649
- function apiMessage(value2) {
9650
- if (!record7(value2)) return "request failed";
9651
- const error = record7(value2.error) ? value2.error : value2;
9885
+ function apiMessage2(value2) {
9886
+ if (!record8(value2)) return "request failed";
9887
+ const error = record8(value2.error) ? value2.error : value2;
9652
9888
  return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9653
9889
  }
9654
- function record7(value2) {
9890
+ function record8(value2) {
9655
9891
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9656
9892
  }
9657
9893
 
@@ -9692,7 +9928,7 @@ function statusVerdict(reads) {
9692
9928
  severity: "degraded"
9693
9929
  });
9694
9930
  }
9695
- const performance = record8(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9931
+ const performance = record9(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9696
9932
  if (performance?.status === "unavailable") {
9697
9933
  reasons.push({
9698
9934
  source: "liveSync",
@@ -9773,7 +10009,7 @@ function statusVerdict(reads) {
9773
10009
  reasons
9774
10010
  };
9775
10011
  }
9776
- function record8(value2) {
10012
+ function record9(value2) {
9777
10013
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9778
10014
  }
9779
10015
  function numeric2(value2) {
@@ -9801,7 +10037,7 @@ function printO11yStatus(status, out) {
9801
10037
  out.log(
9802
10038
  `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
9803
10039
  );
9804
- const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record9) : [];
10040
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record10) : [];
9805
10041
  const requests = routes.reduce(
9806
10042
  (total, row) => total + numeric3(row.requests),
9807
10043
  0
@@ -9813,39 +10049,39 @@ function printO11yStatus(status, out) {
9813
10049
  out.log(
9814
10050
  `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
9815
10051
  );
9816
- const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record9) : [];
10052
+ const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record10) : [];
9817
10053
  out.log(
9818
10054
  `application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
9819
10055
  (row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
9820
10056
  ).join(", ") : "none observed"}`
9821
10057
  );
9822
10058
  out.log(liveSyncLine(status.liveSync));
9823
- const canaryDurations = record9(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
10059
+ const canaryDurations = record10(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
9824
10060
  out.log(
9825
10061
  `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
9826
10062
  );
9827
- const collectorIngest = record9(status.collector.body.ingest) ? status.collector.body.ingest : {};
9828
- const collectorStorage = record9(collectorIngest.storage) ? collectorIngest.storage : {};
10063
+ const collectorIngest = record10(status.collector.body.ingest) ? status.collector.body.ingest : {};
10064
+ const collectorStorage = record10(collectorIngest.storage) ? collectorIngest.storage : {};
9829
10065
  out.log(
9830
10066
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9831
10067
  );
9832
- const providerMetrics = record9(status.provider.body.metrics) ? status.provider.body.metrics : {};
9833
- const providerCapacity = record9(status.provider.body.capacity) ? status.provider.body.capacity : {};
9834
- const workerMemory = record9(providerCapacity.memory) ? providerCapacity.memory : {};
10068
+ const providerMetrics = record10(status.provider.body.metrics) ? status.provider.body.metrics : {};
10069
+ const providerCapacity = record10(status.provider.body.capacity) ? status.provider.body.capacity : {};
10070
+ const workerMemory = record10(providerCapacity.memory) ? providerCapacity.memory : {};
9835
10071
  out.log(
9836
10072
  `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
9837
10073
  );
9838
10074
  for (const line of providerCapacityLines(status.providerCapacity)) {
9839
10075
  out.log(line);
9840
10076
  }
9841
- const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
9842
- const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
9843
- const coverageBudget = record9(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
10077
+ const coverage = record10(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
10078
+ const coverageCounts = record10(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
10079
+ const coverageBudget = record10(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
9844
10080
  out.log(
9845
10081
  `request-coverage ${status.providerReconciliation.httpStatus} ${String(status.providerReconciliation.body.status ?? status.providerReconciliation.body.error ?? "unavailable")} ${optionalPercent(coverage.applicationCoverage)} application/provider ${numeric3(coverageCounts.applicationRequests)}/${numeric3(coverageCounts.providerRequests)} requests \xB1${optionalPercent(coverageBudget.maxRelativeError)} budget`
9846
10082
  );
9847
10083
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9848
- const providerFreshness = record9(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
10084
+ const providerFreshness = record10(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
9849
10085
  out.log(
9850
10086
  `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
9851
10087
  );
@@ -9854,17 +10090,17 @@ function printO11yStatus(status, out) {
9854
10090
  );
9855
10091
  }
9856
10092
  function providerCapacityLines(read3) {
9857
- const resources = record9(read3.body.resources) ? read3.body.resources : {};
9858
- const durableObjects = record9(resources.durableObjects) ? resources.durableObjects : {};
9859
- const periodic = record9(durableObjects.periodic) ? durableObjects.periodic : {};
9860
- const storage = record9(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
9861
- const d1 = record9(resources.d1) ? resources.d1 : {};
9862
- const d1Activity = record9(d1.activity) ? d1.activity : {};
9863
- const d1Storage = record9(d1.storage) ? d1.storage : {};
9864
- const d1Latency = record9(d1Activity.latency) ? d1Activity.latency : {};
9865
- const r2 = record9(resources.r2) ? resources.r2 : {};
9866
- const r2Operations = record9(r2.operations) ? r2.operations : {};
9867
- const r2Storage = record9(r2.storage) ? r2.storage : {};
10093
+ const resources = record10(read3.body.resources) ? read3.body.resources : {};
10094
+ const durableObjects = record10(resources.durableObjects) ? resources.durableObjects : {};
10095
+ const periodic = record10(durableObjects.periodic) ? durableObjects.periodic : {};
10096
+ const storage = record10(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
10097
+ const d1 = record10(resources.d1) ? resources.d1 : {};
10098
+ const d1Activity = record10(d1.activity) ? d1.activity : {};
10099
+ const d1Storage = record10(d1.storage) ? d1.storage : {};
10100
+ const d1Latency = record10(d1Activity.latency) ? d1Activity.latency : {};
10101
+ const r2 = record10(resources.r2) ? resources.r2 : {};
10102
+ const r2Operations = record10(r2.operations) ? r2.operations : {};
10103
+ const r2Storage = record10(r2.storage) ? r2.storage : {};
9868
10104
  const status = String(
9869
10105
  read3.body.status ?? read3.body.error ?? "unavailable"
9870
10106
  );
@@ -9875,11 +10111,11 @@ function providerCapacityLines(read3) {
9875
10111
  ];
9876
10112
  }
9877
10113
  function liveSyncLine(read3) {
9878
- const performance = record9(read3.body.performance) ? read3.body.performance : {};
9879
- const commitToSend = record9(performance.commitToSend) ? performance.commitToSend : {};
10114
+ const performance = record10(read3.body.performance) ? read3.body.performance : {};
10115
+ const commitToSend = record10(performance.commitToSend) ? performance.commitToSend : {};
9880
10116
  return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
9881
10117
  }
9882
- function record9(value2) {
10118
+ function record10(value2) {
9883
10119
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9884
10120
  }
9885
10121
  function numeric3(value2) {
@@ -10048,14 +10284,14 @@ function statusMinutes(value2) {
10048
10284
  }
10049
10285
  async function read2(url, headers, doFetch) {
10050
10286
  const response2 = await doFetch(url, { headers });
10051
- const text = await response2.text();
10287
+ const text2 = await response2.text();
10052
10288
  let body = {};
10053
- if (text) {
10289
+ if (text2) {
10054
10290
  try {
10055
- const value2 = JSON.parse(text);
10291
+ const value2 = JSON.parse(text2);
10056
10292
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
10057
10293
  } catch {
10058
- body = { message: text.slice(0, 300) };
10294
+ body = { message: text2.slice(0, 300) };
10059
10295
  }
10060
10296
  }
10061
10297
  return { httpStatus: response2.status, body };
@@ -10064,7 +10300,7 @@ async function read2(url, headers, doFetch) {
10064
10300
  // src/provision.ts
10065
10301
  var import_apps12 = require("@odla-ai/apps");
10066
10302
  var import_ai3 = require("@odla-ai/ai");
10067
- var import_node_process10 = __toESM(require("process"), 1);
10303
+ var import_node_process11 = __toESM(require("process"), 1);
10068
10304
 
10069
10305
  // src/integration-provision.ts
10070
10306
  var import_db3 = require("@odla-ai/db");
@@ -10371,7 +10607,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10371
10607
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10372
10608
  }
10373
10609
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10374
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10610
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
10375
10611
  if (key) {
10376
10612
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10377
10613
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10413,7 +10649,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10413
10649
 
10414
10650
  // src/record.ts
10415
10651
  var import_node_fs16 = require("fs");
10416
- var import_node_process11 = __toESM(require("process"), 1);
10652
+ var import_node_process12 = __toESM(require("process"), 1);
10417
10653
 
10418
10654
  // src/surface.ts
10419
10655
  var PM_ACTIONS = {
@@ -10480,7 +10716,10 @@ var COMMAND_SURFACE = {
10480
10716
  init: {},
10481
10717
  o11y: { status: {} },
10482
10718
  operations: { get: {}, wait: {} },
10483
- platform: { status: {} },
10719
+ platform: {
10720
+ status: {},
10721
+ "chat-credentials": { rotate: {} }
10722
+ },
10484
10723
  pm: {
10485
10724
  ...PM_ENTITIES,
10486
10725
  handoff: {}
@@ -10559,7 +10798,7 @@ function invocationPath(words2) {
10559
10798
 
10560
10799
  // src/record.ts
10561
10800
  function recordInvocation(parsed) {
10562
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10801
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10563
10802
  if (!file) return;
10564
10803
  try {
10565
10804
  const entry = {
@@ -10689,12 +10928,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10689
10928
  });
10690
10929
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10691
10930
  }
10692
- async function runbookEdit(ctx, slug, body, note, requires) {
10931
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10693
10932
  const runbook = await bySlug(ctx, slug);
10694
10933
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10695
10934
  // An empty --requires clears the declaration; omitting the flag leaves
10696
10935
  // whatever is there, so an ordinary body edit never drops it.
10697
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
10936
+ patch: {
10937
+ body,
10938
+ expectedVersion: expectedVersion ?? runbook.version,
10939
+ ...note ? { note } : {},
10940
+ ...requires === void 0 ? {} : { requires: requires || null }
10941
+ }
10698
10942
  });
10699
10943
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10700
10944
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -10748,12 +10992,12 @@ async function runbookRemove(ctx, slug) {
10748
10992
  // src/runbook-import.ts
10749
10993
  var import_node_fs18 = require("fs");
10750
10994
  var import_node_path15 = require("path");
10751
- function parseRunbook(text, slug) {
10752
- let rest = text;
10995
+ function parseRunbook(text2, slug) {
10996
+ let rest = text2;
10753
10997
  const meta = {};
10754
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
10998
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
10755
10999
  if (fm) {
10756
- rest = text.slice(fm[0].length);
11000
+ rest = text2.slice(fm[0].length);
10757
11001
  for (const line of fm[1].split(/\r?\n/)) {
10758
11002
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
10759
11003
  if (!pair) continue;
@@ -10834,6 +11078,7 @@ async function upsert(ctx, r, visibility) {
10834
11078
  patch: {
10835
11079
  title: r.title,
10836
11080
  body: r.body,
11081
+ expectedVersion: found.version,
10837
11082
  ...r.summary ? { summary: r.summary } : {},
10838
11083
  ...r.tags ? { tags: r.tags } : {},
10839
11084
  note: `imported from ${r.file}`
@@ -11225,9 +11470,9 @@ var import_node_child_process8 = require("child_process");
11225
11470
  var import_node_fs20 = require("fs");
11226
11471
  var import_node_os5 = require("os");
11227
11472
  var import_node_path17 = require("path");
11228
- var import_node_process12 = __toESM(require("process"), 1);
11473
+ var import_node_process13 = __toESM(require("process"), 1);
11229
11474
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11230
- function resolveEditor(env = import_node_process12.default.env) {
11475
+ function resolveEditor(env = import_node_process13.default.env) {
11231
11476
  for (const name of EDITOR_ENV) {
11232
11477
  const value2 = env[name];
11233
11478
  if (value2 && value2.trim()) return value2.trim();
@@ -11241,8 +11486,8 @@ function defaultRun(command, path) {
11241
11486
  return result.status ?? 0;
11242
11487
  }
11243
11488
  function editText(initial, slug, deps = {}) {
11244
- const env = deps.env ?? import_node_process12.default.env;
11245
- const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
11489
+ const env = deps.env ?? import_node_process13.default.env;
11490
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
11246
11491
  const editor = resolveEditor(env);
11247
11492
  if (!editor)
11248
11493
  throw new Error(
@@ -11274,27 +11519,90 @@ async function editRunbook(ctx, slug, deps = {}) {
11274
11519
  const found = page.records[0];
11275
11520
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11276
11521
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11277
- return editText(found.body, slug, deps);
11522
+ const body = await editText(found.body, slug, deps);
11523
+ return body === null ? null : { body, expectedVersion: found.version };
11278
11524
  }
11279
11525
 
11280
11526
  // src/whoami-command.ts
11527
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11528
+ function principalKind(value2, machine) {
11529
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11530
+ }
11531
+ function credentialKind(value2, machine, scopes) {
11532
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11533
+ if (machine) return "machine";
11534
+ if (scopes.length) return "device";
11535
+ return "unknown";
11536
+ }
11537
+ function managerOf(value2) {
11538
+ if (!value2 || typeof value2 !== "object") return null;
11539
+ const row = value2;
11540
+ const principalId = text(row.principalId);
11541
+ if (!principalId) return null;
11542
+ return {
11543
+ principalId,
11544
+ displayName: text(row.displayName) ?? "Unnamed member",
11545
+ handle: text(row.handle) ?? ""
11546
+ };
11547
+ }
11548
+ function unnamedPrincipal(kind) {
11549
+ if (kind === "agent") return "Unnamed agent";
11550
+ if (kind === "service") return "Unnamed service";
11551
+ return "Unnamed member";
11552
+ }
11281
11553
  async function fetchIdentity(platformUrl, token, doFetch) {
11282
11554
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11283
11555
  headers: { authorization: `Bearer ${token}` }
11284
11556
  });
11285
11557
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11286
11558
  const body = await res.json();
11559
+ const developerId = text(body.developerId) ?? "";
11560
+ const machine = body.machine === true;
11561
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11562
+ const principalId = text(body.principalId) ?? developerId;
11563
+ const email = text(body.email);
11564
+ const kind = principalKind(body.principalKind, machine);
11565
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11566
+ const handle = text(body.handle) ?? "";
11567
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11287
11568
  return {
11288
- developerId: String(body.developerId ?? ""),
11289
- email: body.email ?? null,
11569
+ developerId,
11570
+ principalId,
11571
+ principalKind: kind,
11572
+ displayName,
11573
+ handle,
11574
+ manager: managerOf(body.manager),
11575
+ credential: {
11576
+ id: text(credential2.id),
11577
+ kind: credentialKind(credential2.kind, machine, scopes)
11578
+ },
11579
+ email,
11290
11580
  admin: body.admin === true,
11291
- machine: body.machine === true,
11292
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11581
+ machine,
11582
+ scopes
11293
11583
  };
11294
11584
  }
11295
- function credentialKind(identity) {
11296
- if (identity.machine) return "machine (platform admin secret)";
11297
- return identity.scopes.length ? "device token (scoped)" : "device token or session";
11585
+ function credentialLabel(identity) {
11586
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11587
+ if (identity.credential.kind === "device")
11588
+ return identity.scopes.length ? "device (scoped)" : "device";
11589
+ if (identity.credential.kind === "clerk") return "clerk";
11590
+ return "unknown (legacy server)";
11591
+ }
11592
+ function namedPrincipal(identity) {
11593
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11594
+ }
11595
+ function namedManager(manager) {
11596
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11597
+ }
11598
+ function accountableOwner(identity) {
11599
+ if (identity.principalKind === "agent" && identity.manager) {
11600
+ return namedManager(identity.manager);
11601
+ }
11602
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11603
+ return namedPrincipal(identity);
11604
+ }
11605
+ return identity.email ?? "Unnamed member";
11298
11606
  }
11299
11607
  async function whoamiCommand(parsed, deps = {}) {
11300
11608
  assertArgs(
@@ -11325,9 +11633,19 @@ async function whoamiCommand(parsed, deps = {}) {
11325
11633
  return;
11326
11634
  }
11327
11635
  out.log(`platform: ${cfg.platformUrl}`);
11328
- out.log(`developer: ${identity.developerId}`);
11636
+ out.log(`principal: ${namedPrincipal(identity)}`);
11637
+ out.log(`principal id: ${identity.principalId}`);
11638
+ out.log(`kind: ${identity.principalKind}`);
11639
+ if (identity.principalKind === "agent")
11640
+ out.log(
11641
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11642
+ );
11643
+ out.log(`owner: ${accountableOwner(identity)}`);
11644
+ out.log(`owner id: ${identity.developerId}`);
11329
11645
  out.log(`email: ${identity.email ?? "(none)"}`);
11330
- out.log(`credential: ${credentialKind(identity)}`);
11646
+ out.log(`credential: ${credentialLabel(identity)}`);
11647
+ if (identity.credential.id)
11648
+ out.log(`credential id: ${identity.credential.id}`);
11331
11649
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11332
11650
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11333
11651
  if (!identity.admin) {
@@ -11476,14 +11794,16 @@ async function runbookCommand(parsed, deps = {}) {
11476
11794
  const name = requireSlug(slug, "edit");
11477
11795
  const file = stringOpt(parsed.options.file);
11478
11796
  const inline = stringOpt(parsed.options.body);
11479
- const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11480
- if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
11797
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11798
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
11799
+ const body = typeof edited === "string" ? edited : edited.body;
11481
11800
  return runbookEdit(
11482
11801
  ctx,
11483
11802
  name,
11484
11803
  body,
11485
11804
  stringOpt(parsed.options.note),
11486
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
11805
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
11806
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11487
11807
  );
11488
11808
  }
11489
11809
  case "import": {