@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/index.cjs CHANGED
@@ -250,10 +250,10 @@ function isManagedDevVar(line) {
250
250
  const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
251
251
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
252
252
  }
253
- function writePrivateText(path, text) {
253
+ function writePrivateText(path, text2) {
254
254
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
255
255
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
256
- (0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
256
+ (0, import_node_fs.writeFileSync)(temporary, text2, { mode: 384 });
257
257
  (0, import_node_fs.chmodSync)(temporary, 384);
258
258
  (0, import_node_fs.renameSync)(temporary, path);
259
259
  }
@@ -560,6 +560,7 @@ function audienceBoundEnvToken(token, platform) {
560
560
  }
561
561
  var SCOPE_PURPOSE = {
562
562
  "platform:status:read": "read the platform fleet health and deployment snapshot",
563
+ "platform:chat:credential:write": "rotate the built-in Discussion responder credential",
563
564
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
564
565
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
565
566
  "platform:runbook:write": "add or edit odla's operational runbooks",
@@ -619,6 +620,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
619
620
  return token;
620
621
  }
621
622
 
623
+ // src/principal-presentation.ts
624
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
625
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
626
+ const id = typeof principalId === "string" ? principalId.trim() : "";
627
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
628
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
629
+ }
630
+
622
631
  // src/admin-ai-audit.ts
623
632
  function adminAiAuditQuery(filters) {
624
633
  if (filters.limit === void 0) return "";
@@ -648,17 +657,17 @@ async function readAdminAiAudit(request2) {
648
657
  String(event.changeKind ?? ""),
649
658
  String(event.purpose ?? event.provider ?? ""),
650
659
  route2,
651
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
660
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
652
661
  ].join(" "));
653
662
  }
654
663
  }
655
664
  async function responseBody(response2) {
656
- const text = await response2.text();
657
- if (!text) return {};
665
+ const text2 = await response2.text();
666
+ if (!text2) return {};
658
667
  try {
659
- return JSON.parse(text);
668
+ return JSON.parse(text2);
660
669
  } catch {
661
- return { message: text.slice(0, 300) };
670
+ return { message: text2.slice(0, 300) };
662
671
  }
663
672
  }
664
673
  function apiError(status, body) {
@@ -739,7 +748,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
739
748
  timestamp2(event.created_at),
740
749
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
741
750
  String(event.run_id ?? ""),
742
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
751
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
743
752
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
744
753
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
745
754
  String(input + output),
@@ -760,12 +769,12 @@ function timestamp2(value2) {
760
769
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
761
770
  }
762
771
  async function responseBody2(res) {
763
- const text = await res.text();
764
- if (!text) return {};
772
+ const text2 = await res.text();
773
+ if (!text2) return {};
765
774
  try {
766
- return JSON.parse(text);
775
+ return JSON.parse(text2);
767
776
  } catch {
768
- return { message: text.slice(0, 300) };
777
+ return { message: text2.slice(0, 300) };
769
778
  }
770
779
  }
771
780
  function apiError2(action2, status, body) {
@@ -941,12 +950,12 @@ function catalogModels(body) {
941
950
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
942
951
  }
943
952
  async function responseBody3(res) {
944
- const text = await res.text();
945
- if (!text) return {};
953
+ const text2 = await res.text();
954
+ if (!text2) return {};
946
955
  try {
947
- return JSON.parse(text);
956
+ return JSON.parse(text2);
948
957
  } catch {
949
- return { message: text.slice(0, 300) };
958
+ return { message: text2.slice(0, 300) };
950
959
  }
951
960
  }
952
961
  function apiError3(action2, status, body) {
@@ -1790,8 +1799,8 @@ async function appImport(options) {
1790
1799
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1791
1800
  const doFetch = options.fetch ?? fetch;
1792
1801
  const { tenant } = resolveTenant(cfg, options.env);
1793
- const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1794
- const { format, sources } = (0, import_import.parseImport)(text, options.ns);
1802
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1803
+ const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1795
1804
  if (format === "namespace-map" && options.ns) {
1796
1805
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1797
1806
  }
@@ -1882,7 +1891,10 @@ function report(options, owners, headline) {
1882
1891
  if (headline) out.log(headline);
1883
1892
  out.log(`owners (${owners.length}):`);
1884
1893
  for (const o of owners) {
1885
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
1894
+ const name = o.email?.trim() || "Unnamed member";
1895
+ out.log(
1896
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1897
+ );
1886
1898
  }
1887
1899
  }
1888
1900
  async function ownersList(options) {
@@ -2799,9 +2811,9 @@ function canonicalValue(value2) {
2799
2811
  }
2800
2812
  if (Array.isArray(value2)) return value2.map(canonicalValue);
2801
2813
  if (value2 && typeof value2 === "object") {
2802
- const record10 = value2;
2814
+ const record11 = value2;
2803
2815
  return Object.fromEntries(
2804
- Object.keys(record10).filter((key) => record10[key] !== void 0).sort().map((key) => [key, canonicalValue(record10[key])])
2816
+ Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, canonicalValue(record11[key])])
2805
2817
  );
2806
2818
  }
2807
2819
  throw new TypeError("canonical JSON rejects unsupported values");
@@ -3627,22 +3639,26 @@ function planNextActions(reconciliation, configPath) {
3627
3639
  }
3628
3640
  }
3629
3641
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3630
- const { appId, platformUrl } = reconciliation.scope;
3631
3642
  next.push({
3632
3643
  code: "review_destructive",
3633
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3644
+ command: studioSettingsUrl(reconciliation),
3634
3645
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3635
3646
  });
3636
3647
  }
3637
3648
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3638
3649
  next.push({
3639
3650
  code: "declare_or_accept_runtime_state",
3640
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3651
+ command: studioSettingsUrl(reconciliation),
3641
3652
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3642
3653
  });
3643
3654
  }
3644
3655
  return next;
3645
3656
  }
3657
+ function studioSettingsUrl(reconciliation) {
3658
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
3659
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
3660
+ return `${origin}${(0, import_apps8.studioAppSettingsPath)(reconciliation.scope.appId, environment2, "environment")}`;
3661
+ }
3646
3662
  function quoteArg2(value2) {
3647
3663
  return `'${value2.replace(/'/g, `'\\''`)}'`;
3648
3664
  }
@@ -3682,15 +3698,15 @@ function readWranglerConfig(path) {
3682
3698
  return null;
3683
3699
  }
3684
3700
  }
3685
- function stripJsonComments(text) {
3701
+ function stripJsonComments(text2) {
3686
3702
  let result = "";
3687
3703
  let inString = false;
3688
- for (let i = 0; i < text.length; i++) {
3689
- const ch = text[i];
3704
+ for (let i = 0; i < text2.length; i++) {
3705
+ const ch = text2[i];
3690
3706
  if (inString) {
3691
3707
  result += ch;
3692
3708
  if (ch === "\\") {
3693
- result += text[i + 1] ?? "";
3709
+ result += text2[i + 1] ?? "";
3694
3710
  i++;
3695
3711
  } else if (ch === '"') {
3696
3712
  inString = false;
@@ -3702,14 +3718,14 @@ function stripJsonComments(text) {
3702
3718
  result += ch;
3703
3719
  continue;
3704
3720
  }
3705
- if (ch === "/" && text[i + 1] === "/") {
3706
- while (i < text.length && text[i] !== "\n") i++;
3721
+ if (ch === "/" && text2[i + 1] === "/") {
3722
+ while (i < text2.length && text2[i] !== "\n") i++;
3707
3723
  result += "\n";
3708
3724
  continue;
3709
3725
  }
3710
- if (ch === "/" && text[i + 1] === "*") {
3726
+ if (ch === "/" && text2[i + 1] === "*") {
3711
3727
  i += 2;
3712
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3728
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
3713
3729
  i++;
3714
3730
  continue;
3715
3731
  }
@@ -3726,7 +3742,14 @@ async function wranglerLoggedIn(run, cwd) {
3726
3742
  }
3727
3743
  }
3728
3744
  function wranglerPutSecret(run, opts) {
3729
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
3745
+ const args = [
3746
+ "wrangler",
3747
+ "secret",
3748
+ "put",
3749
+ opts.name,
3750
+ ...opts.env ? ["--env", opts.env] : [],
3751
+ ...opts.configPath ? ["--config", opts.configPath] : []
3752
+ ];
3730
3753
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3731
3754
  }
3732
3755
 
@@ -4104,9 +4127,9 @@ function initProject(options) {
4104
4127
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
4105
4128
  out.log("updated .gitignore for local odla credentials");
4106
4129
  }
4107
- function writeIfMissing(path, text) {
4130
+ function writeIfMissing(path, text2) {
4108
4131
  if ((0, import_node_fs13.existsSync)(path)) return;
4109
- (0, import_node_fs13.writeFileSync)(path, text);
4132
+ (0, import_node_fs13.writeFileSync)(path, text2);
4110
4133
  }
4111
4134
  function configTemplate(input) {
4112
4135
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4314,13 +4337,13 @@ async function secretsSetClerkKey(options) {
4314
4337
  body: JSON.stringify({ value: value2 })
4315
4338
  });
4316
4339
  if (!res.ok) {
4317
- const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4318
- throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
4340
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4341
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
4319
4342
  }
4320
4343
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
4321
4344
  }
4322
- function scrubValue(text, value2) {
4323
- return redactSecrets(text).split(value2).join("[value redacted]");
4345
+ function scrubValue(text2, value2) {
4346
+ return redactSecrets(text2).split(value2).join("[value redacted]");
4324
4347
  }
4325
4348
  async function resolveVaultWrite(options) {
4326
4349
  const out = options.stdout ?? console;
@@ -5148,8 +5171,8 @@ async function runContainerAttempt(options) {
5148
5171
  let stopped = false;
5149
5172
  let exited = false;
5150
5173
  child.stderr.setEncoding("utf8");
5151
- child.stderr.on("data", (text) => {
5152
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
5174
+ child.stderr.on("data", (text2) => {
5175
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
5153
5176
  });
5154
5177
  const stop = (reason) => {
5155
5178
  if (stopped || exited) return;
@@ -5307,8 +5330,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
5307
5330
  const maxFiles = options.maxFiles ?? 2e4;
5308
5331
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
5309
5332
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
5310
- const entries = inventory.flatMap((record10) => {
5311
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record10);
5333
+ const entries = inventory.flatMap((record11) => {
5334
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record11);
5312
5335
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
5313
5336
  });
5314
5337
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -5556,8 +5579,8 @@ function normalize(value2) {
5556
5579
  if (Array.isArray(value2)) return value2.map(normalize);
5557
5580
  if (value2 instanceof Uint8Array) return { $bytes: [...value2] };
5558
5581
  if (typeof value2 === "object") {
5559
- const record10 = value2;
5560
- return Object.fromEntries(Object.keys(record10).filter((key) => record10[key] !== void 0).sort().map((key) => [key, normalize(record10[key])]));
5582
+ const record11 = value2;
5583
+ return Object.fromEntries(Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, normalize(record11[key])]));
5561
5584
  }
5562
5585
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
5563
5586
  }
@@ -5981,8 +6004,8 @@ function boundedInteger(value2, spec) {
5981
6004
  }
5982
6005
  function boundedNumber(value2, spec) {
5983
6006
  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.");
5984
- const text = String(value2);
5985
- 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.");
6007
+ const text2 = String(value2);
6008
+ 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.");
5986
6009
  return value2;
5987
6010
  }
5988
6011
  function enumMember(value2, spec) {
@@ -6138,8 +6161,8 @@ function validateUnsafeSelector(path, value2, tool) {
6138
6161
  return void 0;
6139
6162
  }
6140
6163
  function looksLikeDestination(value2) {
6141
- const text = value2.trim();
6142
- return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
6164
+ const text2 = value2.trim();
6165
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
6143
6166
  }
6144
6167
 
6145
6168
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -6508,8 +6531,8 @@ function gitApply(cwd, patch2, check) {
6508
6531
  });
6509
6532
  let stderr = "";
6510
6533
  child.stderr.setEncoding("utf8");
6511
- child.stderr.on("data", (text) => {
6512
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
6534
+ child.stderr.on("data", (text2) => {
6535
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
6513
6536
  });
6514
6537
  child.once("error", reject);
6515
6538
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -7680,10 +7703,10 @@ var CodePiRuntimeEngine = class {
7680
7703
  task: lease.task,
7681
7704
  limits: this.options.limits,
7682
7705
  signal: active.abort.signal,
7683
- onStderr: (text) => this.#event(command, {
7706
+ onStderr: (text2) => this.#event(command, {
7684
7707
  type: "message",
7685
7708
  actor: "system",
7686
- body: text.slice(0, 4e3)
7709
+ body: text2.slice(0, 4e3)
7687
7710
  }, active.conversationRefs),
7688
7711
  onMessage: async (output) => {
7689
7712
  if (output.type === "inference.request") {
@@ -8565,7 +8588,8 @@ Usage:
8565
8588
  odla-ai doctor [--config odla.config.mjs]
8566
8589
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8567
8590
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
8568
- odla-ai operations <get|wait> <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8591
+ odla-ai operations get <operation-id> [--json]
8592
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8569
8593
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8570
8594
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8571
8595
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -8616,6 +8640,7 @@ Usage:
8616
8640
  odla-ai context remove <name> --yes [--json]
8617
8641
  odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
8618
8642
  odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8643
+ odla-ai platform chat-credentials rotate [--context <name>] [--email <odla-account>] [--wrangler-config <path>] [--expected-version <id>] [--json] --yes
8619
8644
  odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
8620
8645
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
8621
8646
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -8679,9 +8704,9 @@ Commands:
8679
8704
  does and what it guarantees is JSDoc, rendered per package at
8680
8705
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
8681
8706
  a question usually needs both.
8682
- whoami Report who this terminal is authenticated as, and whether it holds
8683
- platform admin. A handshake-minted device token is never admin,
8684
- however the human who approved it is configured.
8707
+ whoami Report the actual principal name, handle and kind, its manager,
8708
+ credential kind, accountable owner, and platform-admin status.
8709
+ A manager relationship never supplies authority by itself.
8685
8710
  context Explain selected config, platform, app, environment, and
8686
8711
  developer-token provenance without printing credentials or
8687
8712
  starting a device handshake. Operator work can run outside a
@@ -8801,6 +8826,58 @@ Safety:
8801
8826
  `);
8802
8827
  }
8803
8828
 
8829
+ // src/discuss-principals.ts
8830
+ function mergeDiscussPrincipals(target, source) {
8831
+ Object.assign(target.authors, source.authors ?? {});
8832
+ Object.assign(target.principals, source.principals ?? {});
8833
+ }
8834
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
8835
+ function discussPrincipalLabel(principalId, authorKind, projection) {
8836
+ const profile = projection.principals?.[principalId];
8837
+ if (profile) {
8838
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
8839
+ if (profile.kind === "agent") {
8840
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
8841
+ }
8842
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
8843
+ }
8844
+ const legacyName = projection.authors?.[principalId]?.trim();
8845
+ if (legacyName) {
8846
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
8847
+ }
8848
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
8849
+ }
8850
+
8851
+ // src/discuss-read-render.ts
8852
+ function discussBodyWithRefs(post) {
8853
+ if (!post.refs || post.refs.length === 0) return post.body;
8854
+ let out = "";
8855
+ let cursor = 0;
8856
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8857
+ if (ref.start < cursor || ref.end > post.body.length) continue;
8858
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8859
+ cursor = ref.end;
8860
+ }
8861
+ return out + post.body.slice(cursor);
8862
+ }
8863
+ function renderDiscussRead(ctx, topic, posts, projection) {
8864
+ const status = topic.resolved ? "resolved" : "open";
8865
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
8866
+ for (const post of posts) {
8867
+ const who = discussPrincipalLabel(
8868
+ post.authorId,
8869
+ post.authorKind,
8870
+ projection
8871
+ );
8872
+ ctx.out.log(`
8873
+ \u2014 ${who}`);
8874
+ ctx.out.log(discussBodyWithRefs(post));
8875
+ for (const file of post.attachments ?? []) {
8876
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8877
+ }
8878
+ }
8879
+ }
8880
+
8804
8881
  // src/discuss-actions.ts
8805
8882
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8806
8883
  async function request(ctx, method, path, body) {
@@ -8819,17 +8896,6 @@ function emit(ctx, value2, human) {
8819
8896
  else human();
8820
8897
  }
8821
8898
  var state = (topic) => topic.resolved ? "resolved" : "open";
8822
- function bodyWithRefs(post) {
8823
- if (!post.refs || post.refs.length === 0) return post.body;
8824
- let out = "";
8825
- let cursor = 0;
8826
- for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8827
- if (ref.start < cursor || ref.end > post.body.length) continue;
8828
- out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8829
- cursor = ref.end;
8830
- }
8831
- return out + post.body.slice(cursor);
8832
- }
8833
8899
  function content(parsed) {
8834
8900
  const markup = stringOpt(parsed.options.markup);
8835
8901
  if (markup) return { markup };
@@ -8884,11 +8950,19 @@ async function discussRead(ctx, id, parsed) {
8884
8950
  offset: requestedOffset ?? "0"
8885
8951
  });
8886
8952
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
8887
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
8953
+ emit(
8954
+ ctx,
8955
+ page,
8956
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
8957
+ );
8888
8958
  return;
8889
8959
  }
8890
8960
  for (let scan = 0; scan < 3; scan++) {
8891
8961
  const posts = /* @__PURE__ */ new Map();
8962
+ const projection = {
8963
+ authors: {},
8964
+ principals: {}
8965
+ };
8892
8966
  let topic = null;
8893
8967
  let offset = 0;
8894
8968
  for (; ; ) {
@@ -8899,6 +8973,7 @@ async function discussRead(ctx, id, parsed) {
8899
8973
  );
8900
8974
  topic = page.topic;
8901
8975
  for (const post of page.posts) posts.set(post.id, post);
8976
+ mergeDiscussPrincipals(projection, page);
8902
8977
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
8903
8978
  if (!page.page?.hasMore) break;
8904
8979
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -8911,24 +8986,18 @@ async function discussRead(ctx, id, parsed) {
8911
8986
  );
8912
8987
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
8913
8988
  if (ordered.length === expected) {
8914
- const result = { topic, posts: ordered };
8915
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
8989
+ const result = { topic, posts: ordered, ...projection };
8990
+ emit(
8991
+ ctx,
8992
+ result,
8993
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
8994
+ );
8916
8995
  return;
8917
8996
  }
8918
8997
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
8919
8998
  }
8920
8999
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
8921
9000
  }
8922
- function renderRead(ctx, topic, posts) {
8923
- ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
8924
- for (const post of posts) {
8925
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8926
- ctx.out.log(`
8927
- \u2014 ${who}`);
8928
- ctx.out.log(bodyWithRefs(post));
8929
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8930
- }
8931
- }
8932
9001
  async function discussPost(ctx, parsed) {
8933
9002
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8934
9003
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -9153,6 +9222,8 @@ async function discussWatch(ctx, topicId, parsed) {
9153
9222
  found: true,
9154
9223
  cursor,
9155
9224
  events: matching,
9225
+ ...page.authors ? { authors: page.authors } : {},
9226
+ ...page.principals ? { principals: page.principals } : {},
9156
9227
  ...posts && posts.length > 0 ? { posts } : {},
9157
9228
  ...topics && topics.length > 0 ? { topics } : {}
9158
9229
  });
@@ -9185,7 +9256,11 @@ function report2(ctx, parsed, result) {
9185
9256
  ctx.out.log(JSON.stringify(result, null, 2));
9186
9257
  } else if (parsed.options.jsonl !== true && result.found) {
9187
9258
  for (const post of result.posts ?? []) {
9188
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9259
+ const who = discussPrincipalLabel(
9260
+ post.authorId,
9261
+ post.authorKind,
9262
+ result
9263
+ );
9189
9264
  ctx.out.log(`\u2014 ${who}
9190
9265
  ${post.body}`);
9191
9266
  }
@@ -9330,8 +9405,8 @@ function collectFields(parsed, allowClear) {
9330
9405
  if (allowClear) out[spec.key] = null;
9331
9406
  continue;
9332
9407
  }
9333
- const text = stringOpt(value2);
9334
- out[spec.key] = spec.num ? Number(text) : text;
9408
+ const text2 = stringOpt(value2);
9409
+ out[spec.key] = spec.num ? Number(text2) : text2;
9335
9410
  }
9336
9411
  return out;
9337
9412
  }
@@ -9398,8 +9473,8 @@ async function pmAdd(ctx, entity, parsed) {
9398
9473
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
9399
9474
  }
9400
9475
  async function pmGet(ctx, entity, id) {
9401
- const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9402
- emit2(ctx, record10, () => printRecord(ctx, entity, record10));
9476
+ const { record: record11 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9477
+ emit2(ctx, record11, () => printRecord(ctx, entity, record11));
9403
9478
  }
9404
9479
  async function pmSet(ctx, entity, id, parsed) {
9405
9480
  const patch2 = collectEntityFields(entity, parsed, true);
@@ -9444,9 +9519,9 @@ async function pmHandoff(ctx, parsed) {
9444
9519
  ]);
9445
9520
  const handoff = {
9446
9521
  appId,
9447
- unmetGoals: goals.filter((record10) => record10.status !== "met"),
9448
- activeTasks: tasks.filter((record10) => record10.column !== "done"),
9449
- openBugs: bugs.filter((record10) => record10.status !== "fixed" && record10.status !== "wontfix")
9522
+ unmetGoals: goals.filter((record11) => record11.status !== "met"),
9523
+ activeTasks: tasks.filter((record11) => record11.column !== "done"),
9524
+ openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
9450
9525
  };
9451
9526
  const result = {
9452
9527
  ...handoff,
@@ -9465,10 +9540,10 @@ async function pmHandoff(ctx, parsed) {
9465
9540
  ]) {
9466
9541
  ctx.out.log(`${label}:`);
9467
9542
  if (!records.length) ctx.out.log("- (none)");
9468
- else for (const record10 of records) printRecord(
9543
+ else for (const record11 of records) printRecord(
9469
9544
  ctx,
9470
9545
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
9471
- record10
9546
+ record11
9472
9547
  );
9473
9548
  }
9474
9549
  });
@@ -9659,19 +9734,180 @@ function age(input) {
9659
9734
  return `${Math.round(input / 6e4)}m`;
9660
9735
  }
9661
9736
 
9737
+ // src/platform-chat-credential-command.ts
9738
+ var import_node_process10 = __toESM(require("process"), 1);
9739
+ async function rotatePlatformChatCredential(parsed, deps) {
9740
+ assertArgs(
9741
+ parsed,
9742
+ [
9743
+ "config",
9744
+ "context",
9745
+ "platform",
9746
+ "token",
9747
+ "email",
9748
+ "open",
9749
+ "json",
9750
+ "yes",
9751
+ "wrangler-config",
9752
+ "expected-version"
9753
+ ],
9754
+ 3
9755
+ );
9756
+ if (parsed.options.yes !== true) {
9757
+ throw new Error(
9758
+ "platform chat-credentials rotate changes the production chat secret; pass --yes"
9759
+ );
9760
+ }
9761
+ const context = await resolveOperatorContext(parsed, {
9762
+ allowMissingConfig: true
9763
+ });
9764
+ const platform = context.platform.value;
9765
+ const doFetch = deps.fetch ?? fetch;
9766
+ const out = deps.stdout ?? console;
9767
+ const run = deps.runner ?? defaultRunner;
9768
+ const cwd = import_node_process10.default.cwd();
9769
+ const wranglerConfig = stringOpt(parsed.options["wrangler-config"]) ?? "packages/chat-agent/wrangler.jsonc";
9770
+ if (!await wranglerLoggedIn(run, cwd)) {
9771
+ throw new Error(
9772
+ 'Wrangler is not authenticated; run "npx wrangler login" and retry'
9773
+ );
9774
+ }
9775
+ const priorHealth = await doFetch(`${platform}/health/services/chat`);
9776
+ const priorVersion = priorHealth.headers.get("x-odla-worker-version-id");
9777
+ await priorHealth.body?.cancel().catch(() => {
9778
+ });
9779
+ if (!priorVersion) {
9780
+ throw new Error(
9781
+ "chat health did not identify its current Worker version; refusing to rotate"
9782
+ );
9783
+ }
9784
+ const expectedVersion = stringOpt(parsed.options["expected-version"]);
9785
+ if (expectedVersion && priorVersion !== expectedVersion) {
9786
+ throw new Error(
9787
+ `chat health answered from version ${priorVersion}; expected ${expectedVersion}`
9788
+ );
9789
+ }
9790
+ const token = await resolveAdminPlatformToken({
9791
+ platform,
9792
+ scope: "platform:chat:credential:write",
9793
+ token: stringOpt(parsed.options.token),
9794
+ tokenFile: context.credentials.scopedTokenFile,
9795
+ email: stringOpt(parsed.options.email),
9796
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9797
+ fetch: doFetch,
9798
+ stdout: out,
9799
+ openApprovalUrl: deps.openUrl,
9800
+ label: "odla CLI (rotate built-in Discussion responder)"
9801
+ });
9802
+ const mintedResponse = await doFetch(
9803
+ `${platform}/registry/platform/chat-credentials/rotate`,
9804
+ {
9805
+ method: "POST",
9806
+ headers: { authorization: `Bearer ${token}` }
9807
+ }
9808
+ );
9809
+ const minted = await mintedResponse.json().catch(() => null);
9810
+ if (!mintedResponse.ok) {
9811
+ throw new Error(
9812
+ `mint Discussion credential failed (HTTP ${mintedResponse.status}): ${apiMessage(minted)}`
9813
+ );
9814
+ }
9815
+ if (!isPlatformChatCredential(minted)) {
9816
+ throw new Error(
9817
+ "platform returned an invalid odla.platform-chat-credential/v1 envelope"
9818
+ );
9819
+ }
9820
+ const put = await wranglerPutSecret(run, {
9821
+ name: "ODLA_BOT_TOKENS",
9822
+ value: JSON.stringify({
9823
+ [minted.appId]: { [minted.principalId]: minted.key }
9824
+ }),
9825
+ configPath: wranglerConfig,
9826
+ cwd
9827
+ });
9828
+ if (put.code !== 0) {
9829
+ throw new Error(
9830
+ "Wrangler could not replace ODLA_BOT_TOKENS; the new scoped key was not activated"
9831
+ );
9832
+ }
9833
+ const version = await waitForRotatedChatHealth({
9834
+ fetch: doFetch,
9835
+ platform,
9836
+ priorVersion,
9837
+ wait: deps.pollWait
9838
+ });
9839
+ const result = {
9840
+ schemaVersion: "odla.platform-chat-credential-rotation/v1",
9841
+ appId: minted.appId,
9842
+ appIncarnation: minted.appIncarnation,
9843
+ principalId: minted.principalId,
9844
+ health: { ok: true, service: "odla-chat-agent", version }
9845
+ };
9846
+ if (parsed.options.json === true) {
9847
+ out.log(JSON.stringify(result, null, 2));
9848
+ } else {
9849
+ out.log(
9850
+ `rotated ${result.appId} ${result.principalId} ${result.health.version}`
9851
+ );
9852
+ }
9853
+ }
9854
+ async function waitForRotatedChatHealth(opts) {
9855
+ const wait2 = opts.wait ?? ((milliseconds) => new Promise((resolve12) => setTimeout(resolve12, milliseconds)));
9856
+ let lastStatus = 0;
9857
+ let lastVersion = null;
9858
+ let lastError = "private_service_unready";
9859
+ for (let attempt = 0; attempt < 60; attempt++) {
9860
+ const response2 = await opts.fetch(
9861
+ `${opts.platform}/health/services/chat`
9862
+ );
9863
+ const body = await response2.json().catch(() => null);
9864
+ const version = response2.headers.get("x-odla-worker-version-id");
9865
+ if (response2.ok && record7(body) && body.ok === true && body.service === "odla-chat-agent" && version && version !== opts.priorVersion) {
9866
+ return version;
9867
+ }
9868
+ lastStatus = response2.status;
9869
+ lastVersion = version;
9870
+ lastError = record7(body) && typeof body.error === "string" ? body.error : "private_service_unready";
9871
+ if (attempt < 59) await wait2(5e3);
9872
+ }
9873
+ throw new Error(
9874
+ `chat health did not converge on the rotated Worker deployment (HTTP ${lastStatus}, version ${lastVersion ?? "unknown"}, ${lastError})`
9875
+ );
9876
+ }
9877
+ function isPlatformChatCredential(value2) {
9878
+ 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_");
9879
+ }
9880
+ function apiMessage(value2) {
9881
+ if (!record7(value2)) return "request failed";
9882
+ const error = record7(value2.error) ? value2.error : value2;
9883
+ return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9884
+ }
9885
+ function record7(value2) {
9886
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9887
+ }
9888
+
9662
9889
  // src/platform-command.ts
9663
9890
  async function platformCommand(parsed, deps = {}) {
9891
+ const action2 = parsed.positionals[1];
9892
+ if (action2 === "status") {
9893
+ return platformStatus(parsed, deps);
9894
+ }
9895
+ if (action2 === "chat-credentials" && parsed.positionals[2] === "rotate") {
9896
+ return rotatePlatformChatCredential(parsed, deps);
9897
+ }
9898
+ throw new Error(
9899
+ `unknown platform action "${[
9900
+ action2,
9901
+ parsed.positionals[2]
9902
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
9903
+ );
9904
+ }
9905
+ async function platformStatus(parsed, deps) {
9664
9906
  assertArgs(
9665
9907
  parsed,
9666
9908
  ["config", "context", "platform", "token", "email", "open", "json"],
9667
9909
  2
9668
9910
  );
9669
- const action2 = parsed.positionals[1];
9670
- if (action2 !== "status") {
9671
- throw new Error(
9672
- `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
9673
- );
9674
- }
9675
9911
  const context = await resolveOperatorContext(parsed, {
9676
9912
  allowMissingConfig: true
9677
9913
  });
@@ -9696,7 +9932,7 @@ async function platformCommand(parsed, deps = {}) {
9696
9932
  const body = await response2.json().catch(() => null);
9697
9933
  if (!response2.ok) {
9698
9934
  throw new Error(
9699
- `read platform status failed (HTTP ${response2.status}): ${apiMessage(body)}`
9935
+ `read platform status failed (HTTP ${response2.status}): ${apiMessage2(body)}`
9700
9936
  );
9701
9937
  }
9702
9938
  if (!isPlatformStatus(body)) {
@@ -9709,17 +9945,17 @@ async function platformCommand(parsed, deps = {}) {
9709
9945
  }
9710
9946
  }
9711
9947
  function isPlatformStatus(value2) {
9712
- if (!record7(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9713
- if (!record7(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9714
- if (!record7(value2.catalog) || !record7(value2.summary)) return false;
9948
+ if (!record8(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9949
+ if (!record8(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9950
+ if (!record8(value2.catalog) || !record8(value2.summary)) return false;
9715
9951
  return Array.isArray(value2.services) && Array.isArray(value2.nextActions);
9716
9952
  }
9717
- function apiMessage(value2) {
9718
- if (!record7(value2)) return "request failed";
9719
- const error = record7(value2.error) ? value2.error : value2;
9953
+ function apiMessage2(value2) {
9954
+ if (!record8(value2)) return "request failed";
9955
+ const error = record8(value2.error) ? value2.error : value2;
9720
9956
  return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9721
9957
  }
9722
- function record7(value2) {
9958
+ function record8(value2) {
9723
9959
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9724
9960
  }
9725
9961
 
@@ -9760,7 +9996,7 @@ function statusVerdict(reads) {
9760
9996
  severity: "degraded"
9761
9997
  });
9762
9998
  }
9763
- const performance = record8(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9999
+ const performance = record9(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9764
10000
  if (performance?.status === "unavailable") {
9765
10001
  reasons.push({
9766
10002
  source: "liveSync",
@@ -9841,7 +10077,7 @@ function statusVerdict(reads) {
9841
10077
  reasons
9842
10078
  };
9843
10079
  }
9844
- function record8(value2) {
10080
+ function record9(value2) {
9845
10081
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9846
10082
  }
9847
10083
  function numeric2(value2) {
@@ -9869,7 +10105,7 @@ function printO11yStatus(status, out) {
9869
10105
  out.log(
9870
10106
  `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
9871
10107
  );
9872
- const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record9) : [];
10108
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record10) : [];
9873
10109
  const requests = routes.reduce(
9874
10110
  (total, row) => total + numeric3(row.requests),
9875
10111
  0
@@ -9881,39 +10117,39 @@ function printO11yStatus(status, out) {
9881
10117
  out.log(
9882
10118
  `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
9883
10119
  );
9884
- const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record9) : [];
10120
+ const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record10) : [];
9885
10121
  out.log(
9886
10122
  `application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
9887
10123
  (row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
9888
10124
  ).join(", ") : "none observed"}`
9889
10125
  );
9890
10126
  out.log(liveSyncLine(status.liveSync));
9891
- const canaryDurations = record9(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
10127
+ const canaryDurations = record10(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
9892
10128
  out.log(
9893
10129
  `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
9894
10130
  );
9895
- const collectorIngest = record9(status.collector.body.ingest) ? status.collector.body.ingest : {};
9896
- const collectorStorage = record9(collectorIngest.storage) ? collectorIngest.storage : {};
10131
+ const collectorIngest = record10(status.collector.body.ingest) ? status.collector.body.ingest : {};
10132
+ const collectorStorage = record10(collectorIngest.storage) ? collectorIngest.storage : {};
9897
10133
  out.log(
9898
10134
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9899
10135
  );
9900
- const providerMetrics = record9(status.provider.body.metrics) ? status.provider.body.metrics : {};
9901
- const providerCapacity = record9(status.provider.body.capacity) ? status.provider.body.capacity : {};
9902
- const workerMemory = record9(providerCapacity.memory) ? providerCapacity.memory : {};
10136
+ const providerMetrics = record10(status.provider.body.metrics) ? status.provider.body.metrics : {};
10137
+ const providerCapacity = record10(status.provider.body.capacity) ? status.provider.body.capacity : {};
10138
+ const workerMemory = record10(providerCapacity.memory) ? providerCapacity.memory : {};
9903
10139
  out.log(
9904
10140
  `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`
9905
10141
  );
9906
10142
  for (const line of providerCapacityLines(status.providerCapacity)) {
9907
10143
  out.log(line);
9908
10144
  }
9909
- const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
9910
- const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
9911
- const coverageBudget = record9(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
10145
+ const coverage = record10(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
10146
+ const coverageCounts = record10(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
10147
+ const coverageBudget = record10(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
9912
10148
  out.log(
9913
10149
  `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`
9914
10150
  );
9915
10151
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9916
- const providerFreshness = record9(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
10152
+ const providerFreshness = record10(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
9917
10153
  out.log(
9918
10154
  `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
9919
10155
  );
@@ -9922,17 +10158,17 @@ function printO11yStatus(status, out) {
9922
10158
  );
9923
10159
  }
9924
10160
  function providerCapacityLines(read3) {
9925
- const resources = record9(read3.body.resources) ? read3.body.resources : {};
9926
- const durableObjects = record9(resources.durableObjects) ? resources.durableObjects : {};
9927
- const periodic = record9(durableObjects.periodic) ? durableObjects.periodic : {};
9928
- const storage = record9(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
9929
- const d1 = record9(resources.d1) ? resources.d1 : {};
9930
- const d1Activity = record9(d1.activity) ? d1.activity : {};
9931
- const d1Storage = record9(d1.storage) ? d1.storage : {};
9932
- const d1Latency = record9(d1Activity.latency) ? d1Activity.latency : {};
9933
- const r2 = record9(resources.r2) ? resources.r2 : {};
9934
- const r2Operations = record9(r2.operations) ? r2.operations : {};
9935
- const r2Storage = record9(r2.storage) ? r2.storage : {};
10161
+ const resources = record10(read3.body.resources) ? read3.body.resources : {};
10162
+ const durableObjects = record10(resources.durableObjects) ? resources.durableObjects : {};
10163
+ const periodic = record10(durableObjects.periodic) ? durableObjects.periodic : {};
10164
+ const storage = record10(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
10165
+ const d1 = record10(resources.d1) ? resources.d1 : {};
10166
+ const d1Activity = record10(d1.activity) ? d1.activity : {};
10167
+ const d1Storage = record10(d1.storage) ? d1.storage : {};
10168
+ const d1Latency = record10(d1Activity.latency) ? d1Activity.latency : {};
10169
+ const r2 = record10(resources.r2) ? resources.r2 : {};
10170
+ const r2Operations = record10(r2.operations) ? r2.operations : {};
10171
+ const r2Storage = record10(r2.storage) ? r2.storage : {};
9936
10172
  const status = String(
9937
10173
  read3.body.status ?? read3.body.error ?? "unavailable"
9938
10174
  );
@@ -9943,11 +10179,11 @@ function providerCapacityLines(read3) {
9943
10179
  ];
9944
10180
  }
9945
10181
  function liveSyncLine(read3) {
9946
- const performance = record9(read3.body.performance) ? read3.body.performance : {};
9947
- const commitToSend = record9(performance.commitToSend) ? performance.commitToSend : {};
10182
+ const performance = record10(read3.body.performance) ? read3.body.performance : {};
10183
+ const commitToSend = record10(performance.commitToSend) ? performance.commitToSend : {};
9948
10184
  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`;
9949
10185
  }
9950
- function record9(value2) {
10186
+ function record10(value2) {
9951
10187
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9952
10188
  }
9953
10189
  function numeric3(value2) {
@@ -10116,14 +10352,14 @@ function statusMinutes(value2) {
10116
10352
  }
10117
10353
  async function read2(url, headers, doFetch) {
10118
10354
  const response2 = await doFetch(url, { headers });
10119
- const text = await response2.text();
10355
+ const text2 = await response2.text();
10120
10356
  let body = {};
10121
- if (text) {
10357
+ if (text2) {
10122
10358
  try {
10123
- const value2 = JSON.parse(text);
10359
+ const value2 = JSON.parse(text2);
10124
10360
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
10125
10361
  } catch {
10126
- body = { message: text.slice(0, 300) };
10362
+ body = { message: text2.slice(0, 300) };
10127
10363
  }
10128
10364
  }
10129
10365
  return { httpStatus: response2.status, body };
@@ -10132,7 +10368,7 @@ async function read2(url, headers, doFetch) {
10132
10368
  // src/provision.ts
10133
10369
  var import_apps12 = require("@odla-ai/apps");
10134
10370
  var import_ai3 = require("@odla-ai/ai");
10135
- var import_node_process10 = __toESM(require("process"), 1);
10371
+ var import_node_process11 = __toESM(require("process"), 1);
10136
10372
 
10137
10373
  // src/integration-provision.ts
10138
10374
  var import_db3 = require("@odla-ai/db");
@@ -10439,7 +10675,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10439
10675
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10440
10676
  }
10441
10677
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10442
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10678
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
10443
10679
  if (key) {
10444
10680
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10445
10681
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10481,7 +10717,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10481
10717
 
10482
10718
  // src/record.ts
10483
10719
  var import_node_fs16 = require("fs");
10484
- var import_node_process11 = __toESM(require("process"), 1);
10720
+ var import_node_process12 = __toESM(require("process"), 1);
10485
10721
 
10486
10722
  // src/surface.ts
10487
10723
  var PM_ACTIONS = {
@@ -10548,7 +10784,10 @@ var COMMAND_SURFACE = {
10548
10784
  init: {},
10549
10785
  o11y: { status: {} },
10550
10786
  operations: { get: {}, wait: {} },
10551
- platform: { status: {} },
10787
+ platform: {
10788
+ status: {},
10789
+ "chat-credentials": { rotate: {} }
10790
+ },
10552
10791
  pm: {
10553
10792
  ...PM_ENTITIES,
10554
10793
  handoff: {}
@@ -10636,7 +10875,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
10636
10875
 
10637
10876
  // src/record.ts
10638
10877
  function recordInvocation(parsed) {
10639
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10878
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10640
10879
  if (!file) return;
10641
10880
  try {
10642
10881
  const entry = {
@@ -10766,12 +11005,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10766
11005
  });
10767
11006
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10768
11007
  }
10769
- async function runbookEdit(ctx, slug, body, note, requires) {
11008
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10770
11009
  const runbook = await bySlug(ctx, slug);
10771
11010
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10772
11011
  // An empty --requires clears the declaration; omitting the flag leaves
10773
11012
  // whatever is there, so an ordinary body edit never drops it.
10774
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
11013
+ patch: {
11014
+ body,
11015
+ expectedVersion: expectedVersion ?? runbook.version,
11016
+ ...note ? { note } : {},
11017
+ ...requires === void 0 ? {} : { requires: requires || null }
11018
+ }
10775
11019
  });
10776
11020
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10777
11021
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -10825,12 +11069,12 @@ async function runbookRemove(ctx, slug) {
10825
11069
  // src/runbook-import.ts
10826
11070
  var import_node_fs18 = require("fs");
10827
11071
  var import_node_path15 = require("path");
10828
- function parseRunbook(text, slug) {
10829
- let rest = text;
11072
+ function parseRunbook(text2, slug) {
11073
+ let rest = text2;
10830
11074
  const meta = {};
10831
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
11075
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
10832
11076
  if (fm) {
10833
- rest = text.slice(fm[0].length);
11077
+ rest = text2.slice(fm[0].length);
10834
11078
  for (const line of fm[1].split(/\r?\n/)) {
10835
11079
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
10836
11080
  if (!pair) continue;
@@ -10911,6 +11155,7 @@ async function upsert(ctx, r, visibility) {
10911
11155
  patch: {
10912
11156
  title: r.title,
10913
11157
  body: r.body,
11158
+ expectedVersion: found.version,
10914
11159
  ...r.summary ? { summary: r.summary } : {},
10915
11160
  ...r.tags ? { tags: r.tags } : {},
10916
11161
  note: `imported from ${r.file}`
@@ -11302,9 +11547,9 @@ var import_node_child_process8 = require("child_process");
11302
11547
  var import_node_fs20 = require("fs");
11303
11548
  var import_node_os5 = require("os");
11304
11549
  var import_node_path17 = require("path");
11305
- var import_node_process12 = __toESM(require("process"), 1);
11550
+ var import_node_process13 = __toESM(require("process"), 1);
11306
11551
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11307
- function resolveEditor(env = import_node_process12.default.env) {
11552
+ function resolveEditor(env = import_node_process13.default.env) {
11308
11553
  for (const name of EDITOR_ENV) {
11309
11554
  const value2 = env[name];
11310
11555
  if (value2 && value2.trim()) return value2.trim();
@@ -11318,8 +11563,8 @@ function defaultRun(command, path) {
11318
11563
  return result.status ?? 0;
11319
11564
  }
11320
11565
  function editText(initial, slug, deps = {}) {
11321
- const env = deps.env ?? import_node_process12.default.env;
11322
- const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
11566
+ const env = deps.env ?? import_node_process13.default.env;
11567
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
11323
11568
  const editor = resolveEditor(env);
11324
11569
  if (!editor)
11325
11570
  throw new Error(
@@ -11351,27 +11596,90 @@ async function editRunbook(ctx, slug, deps = {}) {
11351
11596
  const found = page.records[0];
11352
11597
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11353
11598
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11354
- return editText(found.body, slug, deps);
11599
+ const body = await editText(found.body, slug, deps);
11600
+ return body === null ? null : { body, expectedVersion: found.version };
11355
11601
  }
11356
11602
 
11357
11603
  // src/whoami-command.ts
11604
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11605
+ function principalKind(value2, machine) {
11606
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11607
+ }
11608
+ function credentialKind(value2, machine, scopes) {
11609
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11610
+ if (machine) return "machine";
11611
+ if (scopes.length) return "device";
11612
+ return "unknown";
11613
+ }
11614
+ function managerOf(value2) {
11615
+ if (!value2 || typeof value2 !== "object") return null;
11616
+ const row = value2;
11617
+ const principalId = text(row.principalId);
11618
+ if (!principalId) return null;
11619
+ return {
11620
+ principalId,
11621
+ displayName: text(row.displayName) ?? "Unnamed member",
11622
+ handle: text(row.handle) ?? ""
11623
+ };
11624
+ }
11625
+ function unnamedPrincipal(kind) {
11626
+ if (kind === "agent") return "Unnamed agent";
11627
+ if (kind === "service") return "Unnamed service";
11628
+ return "Unnamed member";
11629
+ }
11358
11630
  async function fetchIdentity(platformUrl, token, doFetch) {
11359
11631
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11360
11632
  headers: { authorization: `Bearer ${token}` }
11361
11633
  });
11362
11634
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11363
11635
  const body = await res.json();
11636
+ const developerId = text(body.developerId) ?? "";
11637
+ const machine = body.machine === true;
11638
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11639
+ const principalId = text(body.principalId) ?? developerId;
11640
+ const email = text(body.email);
11641
+ const kind = principalKind(body.principalKind, machine);
11642
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11643
+ const handle = text(body.handle) ?? "";
11644
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11364
11645
  return {
11365
- developerId: String(body.developerId ?? ""),
11366
- email: body.email ?? null,
11646
+ developerId,
11647
+ principalId,
11648
+ principalKind: kind,
11649
+ displayName,
11650
+ handle,
11651
+ manager: managerOf(body.manager),
11652
+ credential: {
11653
+ id: text(credential2.id),
11654
+ kind: credentialKind(credential2.kind, machine, scopes)
11655
+ },
11656
+ email,
11367
11657
  admin: body.admin === true,
11368
- machine: body.machine === true,
11369
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11658
+ machine,
11659
+ scopes
11370
11660
  };
11371
11661
  }
11372
- function credentialKind(identity) {
11373
- if (identity.machine) return "machine (platform admin secret)";
11374
- return identity.scopes.length ? "device token (scoped)" : "device token or session";
11662
+ function credentialLabel(identity) {
11663
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11664
+ if (identity.credential.kind === "device")
11665
+ return identity.scopes.length ? "device (scoped)" : "device";
11666
+ if (identity.credential.kind === "clerk") return "clerk";
11667
+ return "unknown (legacy server)";
11668
+ }
11669
+ function namedPrincipal(identity) {
11670
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11671
+ }
11672
+ function namedManager(manager) {
11673
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11674
+ }
11675
+ function accountableOwner(identity) {
11676
+ if (identity.principalKind === "agent" && identity.manager) {
11677
+ return namedManager(identity.manager);
11678
+ }
11679
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11680
+ return namedPrincipal(identity);
11681
+ }
11682
+ return identity.email ?? "Unnamed member";
11375
11683
  }
11376
11684
  async function whoamiCommand(parsed, deps = {}) {
11377
11685
  assertArgs(
@@ -11402,9 +11710,19 @@ async function whoamiCommand(parsed, deps = {}) {
11402
11710
  return;
11403
11711
  }
11404
11712
  out.log(`platform: ${cfg.platformUrl}`);
11405
- out.log(`developer: ${identity.developerId}`);
11713
+ out.log(`principal: ${namedPrincipal(identity)}`);
11714
+ out.log(`principal id: ${identity.principalId}`);
11715
+ out.log(`kind: ${identity.principalKind}`);
11716
+ if (identity.principalKind === "agent")
11717
+ out.log(
11718
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11719
+ );
11720
+ out.log(`owner: ${accountableOwner(identity)}`);
11721
+ out.log(`owner id: ${identity.developerId}`);
11406
11722
  out.log(`email: ${identity.email ?? "(none)"}`);
11407
- out.log(`credential: ${credentialKind(identity)}`);
11723
+ out.log(`credential: ${credentialLabel(identity)}`);
11724
+ if (identity.credential.id)
11725
+ out.log(`credential id: ${identity.credential.id}`);
11408
11726
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11409
11727
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11410
11728
  if (!identity.admin) {
@@ -11553,14 +11871,16 @@ async function runbookCommand(parsed, deps = {}) {
11553
11871
  const name = requireSlug(slug, "edit");
11554
11872
  const file = stringOpt(parsed.options.file);
11555
11873
  const inline = stringOpt(parsed.options.body);
11556
- const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11557
- if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
11874
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11875
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
11876
+ const body = typeof edited === "string" ? edited : edited.body;
11558
11877
  return runbookEdit(
11559
11878
  ctx,
11560
11879
  name,
11561
11880
  body,
11562
11881
  stringOpt(parsed.options.note),
11563
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
11882
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
11883
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11564
11884
  );
11565
11885
  }
11566
11886
  case "import": {