@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.
@@ -115,10 +115,10 @@ function isManagedDevVar(line) {
115
115
  const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
116
116
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
117
117
  }
118
- function writePrivateText(path, text) {
118
+ function writePrivateText(path, text2) {
119
119
  mkdirSync(dirname(path), { recursive: true });
120
120
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
121
- writeFileSync(temporary, text, { mode: 384 });
121
+ writeFileSync(temporary, text2, { mode: 384 });
122
122
  chmodSync(temporary, 384);
123
123
  renameSync(temporary, path);
124
124
  }
@@ -439,6 +439,7 @@ function audienceBoundEnvToken(token, platform) {
439
439
  }
440
440
  var SCOPE_PURPOSE = {
441
441
  "platform:status:read": "read the platform fleet health and deployment snapshot",
442
+ "platform:chat:credential:write": "rotate the built-in Discussion responder credential",
442
443
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
443
444
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
444
445
  "platform:runbook:write": "add or edit odla's operational runbooks",
@@ -538,6 +539,14 @@ async function readSecretStream(kind, stream = process6.stdin) {
538
539
  return value2;
539
540
  }
540
541
 
542
+ // src/principal-presentation.ts
543
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
544
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
545
+ const id = typeof principalId === "string" ? principalId.trim() : "";
546
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
547
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
548
+ }
549
+
541
550
  // src/admin-ai-audit.ts
542
551
  function adminAiAuditQuery(filters) {
543
552
  if (filters.limit === void 0) return "";
@@ -567,17 +576,17 @@ async function readAdminAiAudit(request2) {
567
576
  String(event.changeKind ?? ""),
568
577
  String(event.purpose ?? event.provider ?? ""),
569
578
  route2,
570
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
579
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
571
580
  ].join(" "));
572
581
  }
573
582
  }
574
583
  async function responseBody(response2) {
575
- const text = await response2.text();
576
- if (!text) return {};
584
+ const text2 = await response2.text();
585
+ if (!text2) return {};
577
586
  try {
578
- return JSON.parse(text);
587
+ return JSON.parse(text2);
579
588
  } catch {
580
- return { message: text.slice(0, 300) };
589
+ return { message: text2.slice(0, 300) };
581
590
  }
582
591
  }
583
592
  function apiError(status, body) {
@@ -644,7 +653,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
644
653
  timestamp2(event.created_at),
645
654
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
646
655
  String(event.run_id ?? ""),
647
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
656
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
648
657
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
649
658
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
650
659
  String(input + output),
@@ -665,12 +674,12 @@ function timestamp2(value2) {
665
674
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
666
675
  }
667
676
  async function responseBody2(res) {
668
- const text = await res.text();
669
- if (!text) return {};
677
+ const text2 = await res.text();
678
+ if (!text2) return {};
670
679
  try {
671
- return JSON.parse(text);
680
+ return JSON.parse(text2);
672
681
  } catch {
673
- return { message: text.slice(0, 300) };
682
+ return { message: text2.slice(0, 300) };
674
683
  }
675
684
  }
676
685
  function apiError2(action2, status, body) {
@@ -846,12 +855,12 @@ function catalogModels(body) {
846
855
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
847
856
  }
848
857
  async function responseBody3(res) {
849
- const text = await res.text();
850
- if (!text) return {};
858
+ const text2 = await res.text();
859
+ if (!text2) return {};
851
860
  try {
852
- return JSON.parse(text);
861
+ return JSON.parse(text2);
853
862
  } catch {
854
- return { message: text.slice(0, 300) };
863
+ return { message: text2.slice(0, 300) };
855
864
  }
856
865
  }
857
866
  function apiError3(action2, status, body) {
@@ -1828,9 +1837,9 @@ function canonicalValue(value2) {
1828
1837
  }
1829
1838
  if (Array.isArray(value2)) return value2.map(canonicalValue);
1830
1839
  if (value2 && typeof value2 === "object") {
1831
- const record10 = value2;
1840
+ const record11 = value2;
1832
1841
  return Object.fromEntries(
1833
- Object.keys(record10).filter((key) => record10[key] !== void 0).sort().map((key) => [key, canonicalValue(record10[key])])
1842
+ Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, canonicalValue(record11[key])])
1834
1843
  );
1835
1844
  }
1836
1845
  throw new TypeError("canonical JSON rejects unsupported values");
@@ -2414,7 +2423,7 @@ function compareOptionalEnvironmentState(env, desired, observed, desiredSource,
2414
2423
  }
2415
2424
 
2416
2425
  // src/config-reconcile-command.ts
2417
- import { createAppsClient as createAppsClient2 } from "@odla-ai/apps";
2426
+ import { createAppsClient as createAppsClient2, studioAppSettingsPath } from "@odla-ai/apps";
2418
2427
  import { join as join4 } from "path";
2419
2428
  async function configDiff(options) {
2420
2429
  const reconciliation = await inspectConfig(options);
@@ -2561,22 +2570,26 @@ function planNextActions(reconciliation, configPath) {
2561
2570
  }
2562
2571
  }
2563
2572
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
2564
- const { appId, platformUrl } = reconciliation.scope;
2565
2573
  next.push({
2566
2574
  code: "review_destructive",
2567
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
2575
+ command: studioSettingsUrl(reconciliation),
2568
2576
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
2569
2577
  });
2570
2578
  }
2571
2579
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
2572
2580
  next.push({
2573
2581
  code: "declare_or_accept_runtime_state",
2574
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
2582
+ command: studioSettingsUrl(reconciliation),
2575
2583
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
2576
2584
  });
2577
2585
  }
2578
2586
  return next;
2579
2587
  }
2588
+ function studioSettingsUrl(reconciliation) {
2589
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
2590
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
2591
+ return `${origin}${studioAppSettingsPath(reconciliation.scope.appId, environment2, "environment")}`;
2592
+ }
2580
2593
  function quoteArg2(value2) {
2581
2594
  return `'${value2.replace(/'/g, `'\\''`)}'`;
2582
2595
  }
@@ -2616,15 +2629,15 @@ function readWranglerConfig(path) {
2616
2629
  return null;
2617
2630
  }
2618
2631
  }
2619
- function stripJsonComments(text) {
2632
+ function stripJsonComments(text2) {
2620
2633
  let result = "";
2621
2634
  let inString = false;
2622
- for (let i = 0; i < text.length; i++) {
2623
- const ch = text[i];
2635
+ for (let i = 0; i < text2.length; i++) {
2636
+ const ch = text2[i];
2624
2637
  if (inString) {
2625
2638
  result += ch;
2626
2639
  if (ch === "\\") {
2627
- result += text[i + 1] ?? "";
2640
+ result += text2[i + 1] ?? "";
2628
2641
  i++;
2629
2642
  } else if (ch === '"') {
2630
2643
  inString = false;
@@ -2636,14 +2649,14 @@ function stripJsonComments(text) {
2636
2649
  result += ch;
2637
2650
  continue;
2638
2651
  }
2639
- if (ch === "/" && text[i + 1] === "/") {
2640
- while (i < text.length && text[i] !== "\n") i++;
2652
+ if (ch === "/" && text2[i + 1] === "/") {
2653
+ while (i < text2.length && text2[i] !== "\n") i++;
2641
2654
  result += "\n";
2642
2655
  continue;
2643
2656
  }
2644
- if (ch === "/" && text[i + 1] === "*") {
2657
+ if (ch === "/" && text2[i + 1] === "*") {
2645
2658
  i += 2;
2646
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
2659
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
2647
2660
  i++;
2648
2661
  continue;
2649
2662
  }
@@ -2660,7 +2673,14 @@ async function wranglerLoggedIn(run, cwd) {
2660
2673
  }
2661
2674
  }
2662
2675
  function wranglerPutSecret(run, opts) {
2663
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
2676
+ const args = [
2677
+ "wrangler",
2678
+ "secret",
2679
+ "put",
2680
+ opts.name,
2681
+ ...opts.env ? ["--env", opts.env] : [],
2682
+ ...opts.configPath ? ["--config", opts.configPath] : []
2683
+ ];
2664
2684
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
2665
2685
  }
2666
2686
 
@@ -3019,9 +3039,9 @@ function initProject(options) {
3019
3039
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
3020
3040
  out.log("updated .gitignore for local odla credentials");
3021
3041
  }
3022
- function writeIfMissing(path, text) {
3042
+ function writeIfMissing(path, text2) {
3023
3043
  if (existsSync6(path)) return;
3024
- writeFileSync2(path, text);
3044
+ writeFileSync2(path, text2);
3025
3045
  }
3026
3046
  function configTemplate(input) {
3027
3047
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -3229,13 +3249,13 @@ async function secretsSetClerkKey(options) {
3229
3249
  body: JSON.stringify({ value: value2 })
3230
3250
  });
3231
3251
  if (!res.ok) {
3232
- const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
3233
- throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
3252
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
3253
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
3234
3254
  }
3235
3255
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
3236
3256
  }
3237
- function scrubValue(text, value2) {
3238
- return redactSecrets(text).split(value2).join("[value redacted]");
3257
+ function scrubValue(text2, value2) {
3258
+ return redactSecrets(text2).split(value2).join("[value redacted]");
3239
3259
  }
3240
3260
  async function resolveVaultWrite(options) {
3241
3261
  const out = options.stdout ?? console;
@@ -4218,8 +4238,8 @@ async function runContainerAttempt(options) {
4218
4238
  let stopped = false;
4219
4239
  let exited = false;
4220
4240
  child.stderr.setEncoding("utf8");
4221
- child.stderr.on("data", (text) => {
4222
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
4241
+ child.stderr.on("data", (text2) => {
4242
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
4223
4243
  });
4224
4244
  const stop = (reason) => {
4225
4245
  if (stopped || exited) return;
@@ -4377,8 +4397,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
4377
4397
  const maxFiles = options.maxFiles ?? 2e4;
4378
4398
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
4379
4399
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
4380
- const entries = inventory.flatMap((record10) => {
4381
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record10);
4400
+ const entries = inventory.flatMap((record11) => {
4401
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record11);
4382
4402
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
4383
4403
  });
4384
4404
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -4626,8 +4646,8 @@ function normalize(value2) {
4626
4646
  if (Array.isArray(value2)) return value2.map(normalize);
4627
4647
  if (value2 instanceof Uint8Array) return { $bytes: [...value2] };
4628
4648
  if (typeof value2 === "object") {
4629
- const record10 = value2;
4630
- return Object.fromEntries(Object.keys(record10).filter((key) => record10[key] !== void 0).sort().map((key) => [key, normalize(record10[key])]));
4649
+ const record11 = value2;
4650
+ return Object.fromEntries(Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, normalize(record11[key])]));
4631
4651
  }
4632
4652
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
4633
4653
  }
@@ -5051,8 +5071,8 @@ function boundedInteger(value2, spec) {
5051
5071
  }
5052
5072
  function boundedNumber(value2, spec) {
5053
5073
  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.");
5054
- const text = String(value2);
5055
- 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.");
5074
+ const text2 = String(value2);
5075
+ 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.");
5056
5076
  return value2;
5057
5077
  }
5058
5078
  function enumMember(value2, spec) {
@@ -5208,8 +5228,8 @@ function validateUnsafeSelector(path, value2, tool) {
5208
5228
  return void 0;
5209
5229
  }
5210
5230
  function looksLikeDestination(value2) {
5211
- const text = value2.trim();
5212
- return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
5231
+ const text2 = value2.trim();
5232
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
5213
5233
  }
5214
5234
 
5215
5235
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -5578,8 +5598,8 @@ function gitApply(cwd, patch2, check) {
5578
5598
  });
5579
5599
  let stderr = "";
5580
5600
  child.stderr.setEncoding("utf8");
5581
- child.stderr.on("data", (text) => {
5582
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
5601
+ child.stderr.on("data", (text2) => {
5602
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
5583
5603
  });
5584
5604
  child.once("error", reject);
5585
5605
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -6750,10 +6770,10 @@ var CodePiRuntimeEngine = class {
6750
6770
  task: lease.task,
6751
6771
  limits: this.options.limits,
6752
6772
  signal: active.abort.signal,
6753
- onStderr: (text) => this.#event(command, {
6773
+ onStderr: (text2) => this.#event(command, {
6754
6774
  type: "message",
6755
6775
  actor: "system",
6756
- body: text.slice(0, 4e3)
6776
+ body: text2.slice(0, 4e3)
6757
6777
  }, active.conversationRefs),
6758
6778
  onMessage: async (output) => {
6759
6779
  if (output.type === "inference.request") {
@@ -7516,7 +7536,10 @@ var COMMAND_SURFACE = {
7516
7536
  init: {},
7517
7537
  o11y: { status: {} },
7518
7538
  operations: { get: {}, wait: {} },
7519
- platform: { status: {} },
7539
+ platform: {
7540
+ status: {},
7541
+ "chat-credentials": { rotate: {} }
7542
+ },
7520
7543
  pm: {
7521
7544
  ...PM_ENTITIES,
7522
7545
  handoff: {}
@@ -8413,8 +8436,8 @@ async function appImport(options) {
8413
8436
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
8414
8437
  const doFetch = options.fetch ?? fetch;
8415
8438
  const { tenant } = resolveTenant(cfg, options.env);
8416
- const text = options.file === "-" ? (options.readStdin ?? (() => readFileSync9(0, "utf8")))() : readFileSync9(options.file, "utf8");
8417
- const { format, sources } = parseImport(text, options.ns);
8439
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => readFileSync9(0, "utf8")))() : readFileSync9(options.file, "utf8");
8440
+ const { format, sources } = parseImport(text2, options.ns);
8418
8441
  if (format === "namespace-map" && options.ns) {
8419
8442
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
8420
8443
  }
@@ -8505,7 +8528,10 @@ function report(options, owners, headline) {
8505
8528
  if (headline) out.log(headline);
8506
8529
  out.log(`owners (${owners.length}):`);
8507
8530
  for (const o of owners) {
8508
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
8531
+ const name = o.email?.trim() || "Unnamed member";
8532
+ out.log(
8533
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
8534
+ );
8509
8535
  }
8510
8536
  }
8511
8537
  async function ownersList(options) {
@@ -9251,7 +9277,8 @@ Usage:
9251
9277
  odla-ai doctor [--config odla.config.mjs]
9252
9278
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
9253
9279
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
9254
- odla-ai operations <get|wait> <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
9280
+ odla-ai operations get <operation-id> [--json]
9281
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
9255
9282
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
9256
9283
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
9257
9284
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -9302,6 +9329,7 @@ Usage:
9302
9329
  odla-ai context remove <name> --yes [--json]
9303
9330
  odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
9304
9331
  odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
9332
+ odla-ai platform chat-credentials rotate [--context <name>] [--email <odla-account>] [--wrangler-config <path>] [--expected-version <id>] [--json] --yes
9305
9333
  odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
9306
9334
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
9307
9335
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -9365,9 +9393,9 @@ Commands:
9365
9393
  does and what it guarantees is JSDoc, rendered per package at
9366
9394
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
9367
9395
  a question usually needs both.
9368
- whoami Report who this terminal is authenticated as, and whether it holds
9369
- platform admin. A handshake-minted device token is never admin,
9370
- however the human who approved it is configured.
9396
+ whoami Report the actual principal name, handle and kind, its manager,
9397
+ credential kind, accountable owner, and platform-admin status.
9398
+ A manager relationship never supplies authority by itself.
9371
9399
  context Explain selected config, platform, app, environment, and
9372
9400
  developer-token provenance without printing credentials or
9373
9401
  starting a device handshake. Operator work can run outside a
@@ -9487,6 +9515,58 @@ Safety:
9487
9515
  `);
9488
9516
  }
9489
9517
 
9518
+ // src/discuss-principals.ts
9519
+ function mergeDiscussPrincipals(target, source) {
9520
+ Object.assign(target.authors, source.authors ?? {});
9521
+ Object.assign(target.principals, source.principals ?? {});
9522
+ }
9523
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
9524
+ function discussPrincipalLabel(principalId, authorKind, projection) {
9525
+ const profile = projection.principals?.[principalId];
9526
+ if (profile) {
9527
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
9528
+ if (profile.kind === "agent") {
9529
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
9530
+ }
9531
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
9532
+ }
9533
+ const legacyName = projection.authors?.[principalId]?.trim();
9534
+ if (legacyName) {
9535
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
9536
+ }
9537
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
9538
+ }
9539
+
9540
+ // src/discuss-read-render.ts
9541
+ function discussBodyWithRefs(post) {
9542
+ if (!post.refs || post.refs.length === 0) return post.body;
9543
+ let out = "";
9544
+ let cursor = 0;
9545
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
9546
+ if (ref.start < cursor || ref.end > post.body.length) continue;
9547
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
9548
+ cursor = ref.end;
9549
+ }
9550
+ return out + post.body.slice(cursor);
9551
+ }
9552
+ function renderDiscussRead(ctx, topic, posts, projection) {
9553
+ const status = topic.resolved ? "resolved" : "open";
9554
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
9555
+ for (const post of posts) {
9556
+ const who = discussPrincipalLabel(
9557
+ post.authorId,
9558
+ post.authorKind,
9559
+ projection
9560
+ );
9561
+ ctx.out.log(`
9562
+ \u2014 ${who}`);
9563
+ ctx.out.log(discussBodyWithRefs(post));
9564
+ for (const file of post.attachments ?? []) {
9565
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
9566
+ }
9567
+ }
9568
+ }
9569
+
9490
9570
  // src/discuss-actions.ts
9491
9571
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
9492
9572
  async function request(ctx, method, path, body) {
@@ -9505,17 +9585,6 @@ function emit(ctx, value2, human) {
9505
9585
  else human();
9506
9586
  }
9507
9587
  var state = (topic) => topic.resolved ? "resolved" : "open";
9508
- function bodyWithRefs(post) {
9509
- if (!post.refs || post.refs.length === 0) return post.body;
9510
- let out = "";
9511
- let cursor = 0;
9512
- for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
9513
- if (ref.start < cursor || ref.end > post.body.length) continue;
9514
- out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
9515
- cursor = ref.end;
9516
- }
9517
- return out + post.body.slice(cursor);
9518
- }
9519
9588
  function content(parsed) {
9520
9589
  const markup = stringOpt(parsed.options.markup);
9521
9590
  if (markup) return { markup };
@@ -9570,11 +9639,19 @@ async function discussRead(ctx, id, parsed) {
9570
9639
  offset: requestedOffset ?? "0"
9571
9640
  });
9572
9641
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9573
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
9642
+ emit(
9643
+ ctx,
9644
+ page,
9645
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
9646
+ );
9574
9647
  return;
9575
9648
  }
9576
9649
  for (let scan = 0; scan < 3; scan++) {
9577
9650
  const posts = /* @__PURE__ */ new Map();
9651
+ const projection = {
9652
+ authors: {},
9653
+ principals: {}
9654
+ };
9578
9655
  let topic = null;
9579
9656
  let offset = 0;
9580
9657
  for (; ; ) {
@@ -9585,6 +9662,7 @@ async function discussRead(ctx, id, parsed) {
9585
9662
  );
9586
9663
  topic = page.topic;
9587
9664
  for (const post of page.posts) posts.set(post.id, post);
9665
+ mergeDiscussPrincipals(projection, page);
9588
9666
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
9589
9667
  if (!page.page?.hasMore) break;
9590
9668
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -9597,24 +9675,18 @@ async function discussRead(ctx, id, parsed) {
9597
9675
  );
9598
9676
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
9599
9677
  if (ordered.length === expected) {
9600
- const result = { topic, posts: ordered };
9601
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
9678
+ const result = { topic, posts: ordered, ...projection };
9679
+ emit(
9680
+ ctx,
9681
+ result,
9682
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
9683
+ );
9602
9684
  return;
9603
9685
  }
9604
9686
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
9605
9687
  }
9606
9688
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
9607
9689
  }
9608
- function renderRead(ctx, topic, posts) {
9609
- ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
9610
- for (const post of posts) {
9611
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9612
- ctx.out.log(`
9613
- \u2014 ${who}`);
9614
- ctx.out.log(bodyWithRefs(post));
9615
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
9616
- }
9617
- }
9618
9690
  async function discussPost(ctx, parsed) {
9619
9691
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9620
9692
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -9839,6 +9911,8 @@ async function discussWatch(ctx, topicId, parsed) {
9839
9911
  found: true,
9840
9912
  cursor,
9841
9913
  events: matching,
9914
+ ...page.authors ? { authors: page.authors } : {},
9915
+ ...page.principals ? { principals: page.principals } : {},
9842
9916
  ...posts && posts.length > 0 ? { posts } : {},
9843
9917
  ...topics && topics.length > 0 ? { topics } : {}
9844
9918
  });
@@ -9871,7 +9945,11 @@ function report2(ctx, parsed, result) {
9871
9945
  ctx.out.log(JSON.stringify(result, null, 2));
9872
9946
  } else if (parsed.options.jsonl !== true && result.found) {
9873
9947
  for (const post of result.posts ?? []) {
9874
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9948
+ const who = discussPrincipalLabel(
9949
+ post.authorId,
9950
+ post.authorKind,
9951
+ result
9952
+ );
9875
9953
  ctx.out.log(`\u2014 ${who}
9876
9954
  ${post.body}`);
9877
9955
  }
@@ -10016,8 +10094,8 @@ function collectFields(parsed, allowClear) {
10016
10094
  if (allowClear) out[spec.key] = null;
10017
10095
  continue;
10018
10096
  }
10019
- const text = stringOpt(value2);
10020
- out[spec.key] = spec.num ? Number(text) : text;
10097
+ const text2 = stringOpt(value2);
10098
+ out[spec.key] = spec.num ? Number(text2) : text2;
10021
10099
  }
10022
10100
  return out;
10023
10101
  }
@@ -10084,8 +10162,8 @@ async function pmAdd(ctx, entity, parsed) {
10084
10162
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
10085
10163
  }
10086
10164
  async function pmGet(ctx, entity, id) {
10087
- const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
10088
- emit2(ctx, record10, () => printRecord(ctx, entity, record10));
10165
+ const { record: record11 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
10166
+ emit2(ctx, record11, () => printRecord(ctx, entity, record11));
10089
10167
  }
10090
10168
  async function pmSet(ctx, entity, id, parsed) {
10091
10169
  const patch2 = collectEntityFields(entity, parsed, true);
@@ -10130,9 +10208,9 @@ async function pmHandoff(ctx, parsed) {
10130
10208
  ]);
10131
10209
  const handoff = {
10132
10210
  appId,
10133
- unmetGoals: goals.filter((record10) => record10.status !== "met"),
10134
- activeTasks: tasks.filter((record10) => record10.column !== "done"),
10135
- openBugs: bugs.filter((record10) => record10.status !== "fixed" && record10.status !== "wontfix")
10211
+ unmetGoals: goals.filter((record11) => record11.status !== "met"),
10212
+ activeTasks: tasks.filter((record11) => record11.column !== "done"),
10213
+ openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
10136
10214
  };
10137
10215
  const result = {
10138
10216
  ...handoff,
@@ -10151,10 +10229,10 @@ async function pmHandoff(ctx, parsed) {
10151
10229
  ]) {
10152
10230
  ctx.out.log(`${label}:`);
10153
10231
  if (!records.length) ctx.out.log("- (none)");
10154
- else for (const record10 of records) printRecord(
10232
+ else for (const record11 of records) printRecord(
10155
10233
  ctx,
10156
10234
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
10157
- record10
10235
+ record11
10158
10236
  );
10159
10237
  }
10160
10238
  });
@@ -10345,19 +10423,180 @@ function age(input) {
10345
10423
  return `${Math.round(input / 6e4)}m`;
10346
10424
  }
10347
10425
 
10426
+ // src/platform-chat-credential-command.ts
10427
+ import process12 from "process";
10428
+ async function rotatePlatformChatCredential(parsed, deps) {
10429
+ assertArgs(
10430
+ parsed,
10431
+ [
10432
+ "config",
10433
+ "context",
10434
+ "platform",
10435
+ "token",
10436
+ "email",
10437
+ "open",
10438
+ "json",
10439
+ "yes",
10440
+ "wrangler-config",
10441
+ "expected-version"
10442
+ ],
10443
+ 3
10444
+ );
10445
+ if (parsed.options.yes !== true) {
10446
+ throw new Error(
10447
+ "platform chat-credentials rotate changes the production chat secret; pass --yes"
10448
+ );
10449
+ }
10450
+ const context = await resolveOperatorContext(parsed, {
10451
+ allowMissingConfig: true
10452
+ });
10453
+ const platform = context.platform.value;
10454
+ const doFetch = deps.fetch ?? fetch;
10455
+ const out = deps.stdout ?? console;
10456
+ const run = deps.runner ?? defaultRunner;
10457
+ const cwd = process12.cwd();
10458
+ const wranglerConfig = stringOpt(parsed.options["wrangler-config"]) ?? "packages/chat-agent/wrangler.jsonc";
10459
+ if (!await wranglerLoggedIn(run, cwd)) {
10460
+ throw new Error(
10461
+ 'Wrangler is not authenticated; run "npx wrangler login" and retry'
10462
+ );
10463
+ }
10464
+ const priorHealth = await doFetch(`${platform}/health/services/chat`);
10465
+ const priorVersion = priorHealth.headers.get("x-odla-worker-version-id");
10466
+ await priorHealth.body?.cancel().catch(() => {
10467
+ });
10468
+ if (!priorVersion) {
10469
+ throw new Error(
10470
+ "chat health did not identify its current Worker version; refusing to rotate"
10471
+ );
10472
+ }
10473
+ const expectedVersion = stringOpt(parsed.options["expected-version"]);
10474
+ if (expectedVersion && priorVersion !== expectedVersion) {
10475
+ throw new Error(
10476
+ `chat health answered from version ${priorVersion}; expected ${expectedVersion}`
10477
+ );
10478
+ }
10479
+ const token = await resolveAdminPlatformToken({
10480
+ platform,
10481
+ scope: "platform:chat:credential:write",
10482
+ token: stringOpt(parsed.options.token),
10483
+ tokenFile: context.credentials.scopedTokenFile,
10484
+ email: stringOpt(parsed.options.email),
10485
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10486
+ fetch: doFetch,
10487
+ stdout: out,
10488
+ openApprovalUrl: deps.openUrl,
10489
+ label: "odla CLI (rotate built-in Discussion responder)"
10490
+ });
10491
+ const mintedResponse = await doFetch(
10492
+ `${platform}/registry/platform/chat-credentials/rotate`,
10493
+ {
10494
+ method: "POST",
10495
+ headers: { authorization: `Bearer ${token}` }
10496
+ }
10497
+ );
10498
+ const minted = await mintedResponse.json().catch(() => null);
10499
+ if (!mintedResponse.ok) {
10500
+ throw new Error(
10501
+ `mint Discussion credential failed (HTTP ${mintedResponse.status}): ${apiMessage(minted)}`
10502
+ );
10503
+ }
10504
+ if (!isPlatformChatCredential(minted)) {
10505
+ throw new Error(
10506
+ "platform returned an invalid odla.platform-chat-credential/v1 envelope"
10507
+ );
10508
+ }
10509
+ const put = await wranglerPutSecret(run, {
10510
+ name: "ODLA_BOT_TOKENS",
10511
+ value: JSON.stringify({
10512
+ [minted.appId]: { [minted.principalId]: minted.key }
10513
+ }),
10514
+ configPath: wranglerConfig,
10515
+ cwd
10516
+ });
10517
+ if (put.code !== 0) {
10518
+ throw new Error(
10519
+ "Wrangler could not replace ODLA_BOT_TOKENS; the new scoped key was not activated"
10520
+ );
10521
+ }
10522
+ const version = await waitForRotatedChatHealth({
10523
+ fetch: doFetch,
10524
+ platform,
10525
+ priorVersion,
10526
+ wait: deps.pollWait
10527
+ });
10528
+ const result = {
10529
+ schemaVersion: "odla.platform-chat-credential-rotation/v1",
10530
+ appId: minted.appId,
10531
+ appIncarnation: minted.appIncarnation,
10532
+ principalId: minted.principalId,
10533
+ health: { ok: true, service: "odla-chat-agent", version }
10534
+ };
10535
+ if (parsed.options.json === true) {
10536
+ out.log(JSON.stringify(result, null, 2));
10537
+ } else {
10538
+ out.log(
10539
+ `rotated ${result.appId} ${result.principalId} ${result.health.version}`
10540
+ );
10541
+ }
10542
+ }
10543
+ async function waitForRotatedChatHealth(opts) {
10544
+ const wait2 = opts.wait ?? ((milliseconds) => new Promise((resolve12) => setTimeout(resolve12, milliseconds)));
10545
+ let lastStatus = 0;
10546
+ let lastVersion = null;
10547
+ let lastError = "private_service_unready";
10548
+ for (let attempt = 0; attempt < 60; attempt++) {
10549
+ const response2 = await opts.fetch(
10550
+ `${opts.platform}/health/services/chat`
10551
+ );
10552
+ const body = await response2.json().catch(() => null);
10553
+ const version = response2.headers.get("x-odla-worker-version-id");
10554
+ if (response2.ok && record7(body) && body.ok === true && body.service === "odla-chat-agent" && version && version !== opts.priorVersion) {
10555
+ return version;
10556
+ }
10557
+ lastStatus = response2.status;
10558
+ lastVersion = version;
10559
+ lastError = record7(body) && typeof body.error === "string" ? body.error : "private_service_unready";
10560
+ if (attempt < 59) await wait2(5e3);
10561
+ }
10562
+ throw new Error(
10563
+ `chat health did not converge on the rotated Worker deployment (HTTP ${lastStatus}, version ${lastVersion ?? "unknown"}, ${lastError})`
10564
+ );
10565
+ }
10566
+ function isPlatformChatCredential(value2) {
10567
+ 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_");
10568
+ }
10569
+ function apiMessage(value2) {
10570
+ if (!record7(value2)) return "request failed";
10571
+ const error = record7(value2.error) ? value2.error : value2;
10572
+ return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
10573
+ }
10574
+ function record7(value2) {
10575
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
10576
+ }
10577
+
10348
10578
  // src/platform-command.ts
10349
10579
  async function platformCommand(parsed, deps = {}) {
10580
+ const action2 = parsed.positionals[1];
10581
+ if (action2 === "status") {
10582
+ return platformStatus(parsed, deps);
10583
+ }
10584
+ if (action2 === "chat-credentials" && parsed.positionals[2] === "rotate") {
10585
+ return rotatePlatformChatCredential(parsed, deps);
10586
+ }
10587
+ throw new Error(
10588
+ `unknown platform action "${[
10589
+ action2,
10590
+ parsed.positionals[2]
10591
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
10592
+ );
10593
+ }
10594
+ async function platformStatus(parsed, deps) {
10350
10595
  assertArgs(
10351
10596
  parsed,
10352
10597
  ["config", "context", "platform", "token", "email", "open", "json"],
10353
10598
  2
10354
10599
  );
10355
- const action2 = parsed.positionals[1];
10356
- if (action2 !== "status") {
10357
- throw new Error(
10358
- `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
10359
- );
10360
- }
10361
10600
  const context = await resolveOperatorContext(parsed, {
10362
10601
  allowMissingConfig: true
10363
10602
  });
@@ -10382,7 +10621,7 @@ async function platformCommand(parsed, deps = {}) {
10382
10621
  const body = await response2.json().catch(() => null);
10383
10622
  if (!response2.ok) {
10384
10623
  throw new Error(
10385
- `read platform status failed (HTTP ${response2.status}): ${apiMessage(body)}`
10624
+ `read platform status failed (HTTP ${response2.status}): ${apiMessage2(body)}`
10386
10625
  );
10387
10626
  }
10388
10627
  if (!isPlatformStatus(body)) {
@@ -10395,17 +10634,17 @@ async function platformCommand(parsed, deps = {}) {
10395
10634
  }
10396
10635
  }
10397
10636
  function isPlatformStatus(value2) {
10398
- if (!record7(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
10399
- if (!record7(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
10400
- if (!record7(value2.catalog) || !record7(value2.summary)) return false;
10637
+ if (!record8(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
10638
+ if (!record8(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
10639
+ if (!record8(value2.catalog) || !record8(value2.summary)) return false;
10401
10640
  return Array.isArray(value2.services) && Array.isArray(value2.nextActions);
10402
10641
  }
10403
- function apiMessage(value2) {
10404
- if (!record7(value2)) return "request failed";
10405
- const error = record7(value2.error) ? value2.error : value2;
10642
+ function apiMessage2(value2) {
10643
+ if (!record8(value2)) return "request failed";
10644
+ const error = record8(value2.error) ? value2.error : value2;
10406
10645
  return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
10407
10646
  }
10408
- function record7(value2) {
10647
+ function record8(value2) {
10409
10648
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
10410
10649
  }
10411
10650
 
@@ -10446,7 +10685,7 @@ function statusVerdict(reads) {
10446
10685
  severity: "degraded"
10447
10686
  });
10448
10687
  }
10449
- const performance = record8(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
10688
+ const performance = record9(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
10450
10689
  if (performance?.status === "unavailable") {
10451
10690
  reasons.push({
10452
10691
  source: "liveSync",
@@ -10527,7 +10766,7 @@ function statusVerdict(reads) {
10527
10766
  reasons
10528
10767
  };
10529
10768
  }
10530
- function record8(value2) {
10769
+ function record9(value2) {
10531
10770
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
10532
10771
  }
10533
10772
  function numeric2(value2) {
@@ -10555,7 +10794,7 @@ function printO11yStatus(status, out) {
10555
10794
  out.log(
10556
10795
  `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
10557
10796
  );
10558
- const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record9) : [];
10797
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record10) : [];
10559
10798
  const requests = routes.reduce(
10560
10799
  (total, row) => total + numeric3(row.requests),
10561
10800
  0
@@ -10567,39 +10806,39 @@ function printO11yStatus(status, out) {
10567
10806
  out.log(
10568
10807
  `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
10569
10808
  );
10570
- const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record9) : [];
10809
+ const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record10) : [];
10571
10810
  out.log(
10572
10811
  `application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
10573
10812
  (row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
10574
10813
  ).join(", ") : "none observed"}`
10575
10814
  );
10576
10815
  out.log(liveSyncLine(status.liveSync));
10577
- const canaryDurations = record9(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
10816
+ const canaryDurations = record10(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
10578
10817
  out.log(
10579
10818
  `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
10580
10819
  );
10581
- const collectorIngest = record9(status.collector.body.ingest) ? status.collector.body.ingest : {};
10582
- const collectorStorage = record9(collectorIngest.storage) ? collectorIngest.storage : {};
10820
+ const collectorIngest = record10(status.collector.body.ingest) ? status.collector.body.ingest : {};
10821
+ const collectorStorage = record10(collectorIngest.storage) ? collectorIngest.storage : {};
10583
10822
  out.log(
10584
10823
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
10585
10824
  );
10586
- const providerMetrics = record9(status.provider.body.metrics) ? status.provider.body.metrics : {};
10587
- const providerCapacity = record9(status.provider.body.capacity) ? status.provider.body.capacity : {};
10588
- const workerMemory = record9(providerCapacity.memory) ? providerCapacity.memory : {};
10825
+ const providerMetrics = record10(status.provider.body.metrics) ? status.provider.body.metrics : {};
10826
+ const providerCapacity = record10(status.provider.body.capacity) ? status.provider.body.capacity : {};
10827
+ const workerMemory = record10(providerCapacity.memory) ? providerCapacity.memory : {};
10589
10828
  out.log(
10590
10829
  `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`
10591
10830
  );
10592
10831
  for (const line of providerCapacityLines(status.providerCapacity)) {
10593
10832
  out.log(line);
10594
10833
  }
10595
- const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
10596
- const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
10597
- const coverageBudget = record9(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
10834
+ const coverage = record10(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
10835
+ const coverageCounts = record10(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
10836
+ const coverageBudget = record10(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
10598
10837
  out.log(
10599
10838
  `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`
10600
10839
  );
10601
10840
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
10602
- const providerFreshness = record9(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
10841
+ const providerFreshness = record10(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
10603
10842
  out.log(
10604
10843
  `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
10605
10844
  );
@@ -10608,17 +10847,17 @@ function printO11yStatus(status, out) {
10608
10847
  );
10609
10848
  }
10610
10849
  function providerCapacityLines(read3) {
10611
- const resources = record9(read3.body.resources) ? read3.body.resources : {};
10612
- const durableObjects = record9(resources.durableObjects) ? resources.durableObjects : {};
10613
- const periodic = record9(durableObjects.periodic) ? durableObjects.periodic : {};
10614
- const storage = record9(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
10615
- const d1 = record9(resources.d1) ? resources.d1 : {};
10616
- const d1Activity = record9(d1.activity) ? d1.activity : {};
10617
- const d1Storage = record9(d1.storage) ? d1.storage : {};
10618
- const d1Latency = record9(d1Activity.latency) ? d1Activity.latency : {};
10619
- const r2 = record9(resources.r2) ? resources.r2 : {};
10620
- const r2Operations = record9(r2.operations) ? r2.operations : {};
10621
- const r2Storage = record9(r2.storage) ? r2.storage : {};
10850
+ const resources = record10(read3.body.resources) ? read3.body.resources : {};
10851
+ const durableObjects = record10(resources.durableObjects) ? resources.durableObjects : {};
10852
+ const periodic = record10(durableObjects.periodic) ? durableObjects.periodic : {};
10853
+ const storage = record10(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
10854
+ const d1 = record10(resources.d1) ? resources.d1 : {};
10855
+ const d1Activity = record10(d1.activity) ? d1.activity : {};
10856
+ const d1Storage = record10(d1.storage) ? d1.storage : {};
10857
+ const d1Latency = record10(d1Activity.latency) ? d1Activity.latency : {};
10858
+ const r2 = record10(resources.r2) ? resources.r2 : {};
10859
+ const r2Operations = record10(r2.operations) ? r2.operations : {};
10860
+ const r2Storage = record10(r2.storage) ? r2.storage : {};
10622
10861
  const status = String(
10623
10862
  read3.body.status ?? read3.body.error ?? "unavailable"
10624
10863
  );
@@ -10629,11 +10868,11 @@ function providerCapacityLines(read3) {
10629
10868
  ];
10630
10869
  }
10631
10870
  function liveSyncLine(read3) {
10632
- const performance = record9(read3.body.performance) ? read3.body.performance : {};
10633
- const commitToSend = record9(performance.commitToSend) ? performance.commitToSend : {};
10871
+ const performance = record10(read3.body.performance) ? read3.body.performance : {};
10872
+ const commitToSend = record10(performance.commitToSend) ? performance.commitToSend : {};
10634
10873
  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`;
10635
10874
  }
10636
- function record9(value2) {
10875
+ function record10(value2) {
10637
10876
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
10638
10877
  }
10639
10878
  function numeric3(value2) {
@@ -10802,14 +11041,14 @@ function statusMinutes(value2) {
10802
11041
  }
10803
11042
  async function read2(url, headers, doFetch) {
10804
11043
  const response2 = await doFetch(url, { headers });
10805
- const text = await response2.text();
11044
+ const text2 = await response2.text();
10806
11045
  let body = {};
10807
- if (text) {
11046
+ if (text2) {
10808
11047
  try {
10809
- const value2 = JSON.parse(text);
11048
+ const value2 = JSON.parse(text2);
10810
11049
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
10811
11050
  } catch {
10812
- body = { message: text.slice(0, 300) };
11051
+ body = { message: text2.slice(0, 300) };
10813
11052
  }
10814
11053
  }
10815
11054
  return { httpStatus: response2.status, body };
@@ -10817,9 +11056,9 @@ async function read2(url, headers, doFetch) {
10817
11056
 
10818
11057
  // src/record.ts
10819
11058
  import { appendFileSync } from "fs";
10820
- import process12 from "process";
11059
+ import process13 from "process";
10821
11060
  function recordInvocation(parsed) {
10822
- const file = process12.env.ODLA_CLI_RECORD;
11061
+ const file = process13.env.ODLA_CLI_RECORD;
10823
11062
  if (!file) return;
10824
11063
  try {
10825
11064
  const entry = {
@@ -10949,12 +11188,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10949
11188
  });
10950
11189
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10951
11190
  }
10952
- async function runbookEdit(ctx, slug, body, note, requires) {
11191
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10953
11192
  const runbook = await bySlug(ctx, slug);
10954
11193
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10955
11194
  // An empty --requires clears the declaration; omitting the flag leaves
10956
11195
  // whatever is there, so an ordinary body edit never drops it.
10957
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
11196
+ patch: {
11197
+ body,
11198
+ expectedVersion: expectedVersion ?? runbook.version,
11199
+ ...note ? { note } : {},
11200
+ ...requires === void 0 ? {} : { requires: requires || null }
11201
+ }
10958
11202
  });
10959
11203
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10960
11204
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -11008,12 +11252,12 @@ async function runbookRemove(ctx, slug) {
11008
11252
  // src/runbook-import.ts
11009
11253
  import { readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
11010
11254
  import { basename as basename2, join as join13 } from "path";
11011
- function parseRunbook(text, slug) {
11012
- let rest = text;
11255
+ function parseRunbook(text2, slug) {
11256
+ let rest = text2;
11013
11257
  const meta = {};
11014
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
11258
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
11015
11259
  if (fm) {
11016
- rest = text.slice(fm[0].length);
11260
+ rest = text2.slice(fm[0].length);
11017
11261
  for (const line of fm[1].split(/\r?\n/)) {
11018
11262
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
11019
11263
  if (!pair) continue;
@@ -11094,6 +11338,7 @@ async function upsert(ctx, r, visibility) {
11094
11338
  patch: {
11095
11339
  title: r.title,
11096
11340
  body: r.body,
11341
+ expectedVersion: found.version,
11097
11342
  ...r.summary ? { summary: r.summary } : {},
11098
11343
  ...r.tags ? { tags: r.tags } : {},
11099
11344
  note: `imported from ${r.file}`
@@ -11485,9 +11730,9 @@ import { spawnSync } from "child_process";
11485
11730
  import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
11486
11731
  import { tmpdir as tmpdir4 } from "os";
11487
11732
  import { join as join15 } from "path";
11488
- import process13 from "process";
11733
+ import process14 from "process";
11489
11734
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11490
- function resolveEditor(env = process13.env) {
11735
+ function resolveEditor(env = process14.env) {
11491
11736
  for (const name of EDITOR_ENV) {
11492
11737
  const value2 = env[name];
11493
11738
  if (value2 && value2.trim()) return value2.trim();
@@ -11501,8 +11746,8 @@ function defaultRun(command, path) {
11501
11746
  return result.status ?? 0;
11502
11747
  }
11503
11748
  function editText(initial, slug, deps = {}) {
11504
- const env = deps.env ?? process13.env;
11505
- const interactive = deps.interactive ?? (() => Boolean(process13.stdin.isTTY));
11749
+ const env = deps.env ?? process14.env;
11750
+ const interactive = deps.interactive ?? (() => Boolean(process14.stdin.isTTY));
11506
11751
  const editor = resolveEditor(env);
11507
11752
  if (!editor)
11508
11753
  throw new Error(
@@ -11534,27 +11779,90 @@ async function editRunbook(ctx, slug, deps = {}) {
11534
11779
  const found = page.records[0];
11535
11780
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11536
11781
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11537
- return editText(found.body, slug, deps);
11782
+ const body = await editText(found.body, slug, deps);
11783
+ return body === null ? null : { body, expectedVersion: found.version };
11538
11784
  }
11539
11785
 
11540
11786
  // src/whoami-command.ts
11787
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11788
+ function principalKind(value2, machine) {
11789
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11790
+ }
11791
+ function credentialKind(value2, machine, scopes) {
11792
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11793
+ if (machine) return "machine";
11794
+ if (scopes.length) return "device";
11795
+ return "unknown";
11796
+ }
11797
+ function managerOf(value2) {
11798
+ if (!value2 || typeof value2 !== "object") return null;
11799
+ const row = value2;
11800
+ const principalId = text(row.principalId);
11801
+ if (!principalId) return null;
11802
+ return {
11803
+ principalId,
11804
+ displayName: text(row.displayName) ?? "Unnamed member",
11805
+ handle: text(row.handle) ?? ""
11806
+ };
11807
+ }
11808
+ function unnamedPrincipal(kind) {
11809
+ if (kind === "agent") return "Unnamed agent";
11810
+ if (kind === "service") return "Unnamed service";
11811
+ return "Unnamed member";
11812
+ }
11541
11813
  async function fetchIdentity(platformUrl, token, doFetch) {
11542
11814
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11543
11815
  headers: { authorization: `Bearer ${token}` }
11544
11816
  });
11545
11817
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11546
11818
  const body = await res.json();
11819
+ const developerId = text(body.developerId) ?? "";
11820
+ const machine = body.machine === true;
11821
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11822
+ const principalId = text(body.principalId) ?? developerId;
11823
+ const email = text(body.email);
11824
+ const kind = principalKind(body.principalKind, machine);
11825
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11826
+ const handle = text(body.handle) ?? "";
11827
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11547
11828
  return {
11548
- developerId: String(body.developerId ?? ""),
11549
- email: body.email ?? null,
11829
+ developerId,
11830
+ principalId,
11831
+ principalKind: kind,
11832
+ displayName,
11833
+ handle,
11834
+ manager: managerOf(body.manager),
11835
+ credential: {
11836
+ id: text(credential2.id),
11837
+ kind: credentialKind(credential2.kind, machine, scopes)
11838
+ },
11839
+ email,
11550
11840
  admin: body.admin === true,
11551
- machine: body.machine === true,
11552
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11841
+ machine,
11842
+ scopes
11553
11843
  };
11554
11844
  }
11555
- function credentialKind(identity) {
11556
- if (identity.machine) return "machine (platform admin secret)";
11557
- return identity.scopes.length ? "device token (scoped)" : "device token or session";
11845
+ function credentialLabel(identity) {
11846
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11847
+ if (identity.credential.kind === "device")
11848
+ return identity.scopes.length ? "device (scoped)" : "device";
11849
+ if (identity.credential.kind === "clerk") return "clerk";
11850
+ return "unknown (legacy server)";
11851
+ }
11852
+ function namedPrincipal(identity) {
11853
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11854
+ }
11855
+ function namedManager(manager) {
11856
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11857
+ }
11858
+ function accountableOwner(identity) {
11859
+ if (identity.principalKind === "agent" && identity.manager) {
11860
+ return namedManager(identity.manager);
11861
+ }
11862
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11863
+ return namedPrincipal(identity);
11864
+ }
11865
+ return identity.email ?? "Unnamed member";
11558
11866
  }
11559
11867
  async function whoamiCommand(parsed, deps = {}) {
11560
11868
  assertArgs(
@@ -11585,9 +11893,19 @@ async function whoamiCommand(parsed, deps = {}) {
11585
11893
  return;
11586
11894
  }
11587
11895
  out.log(`platform: ${cfg.platformUrl}`);
11588
- out.log(`developer: ${identity.developerId}`);
11896
+ out.log(`principal: ${namedPrincipal(identity)}`);
11897
+ out.log(`principal id: ${identity.principalId}`);
11898
+ out.log(`kind: ${identity.principalKind}`);
11899
+ if (identity.principalKind === "agent")
11900
+ out.log(
11901
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11902
+ );
11903
+ out.log(`owner: ${accountableOwner(identity)}`);
11904
+ out.log(`owner id: ${identity.developerId}`);
11589
11905
  out.log(`email: ${identity.email ?? "(none)"}`);
11590
- out.log(`credential: ${credentialKind(identity)}`);
11906
+ out.log(`credential: ${credentialLabel(identity)}`);
11907
+ if (identity.credential.id)
11908
+ out.log(`credential id: ${identity.credential.id}`);
11591
11909
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11592
11910
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11593
11911
  if (!identity.admin) {
@@ -11736,14 +12054,16 @@ async function runbookCommand(parsed, deps = {}) {
11736
12054
  const name = requireSlug(slug, "edit");
11737
12055
  const file = stringOpt(parsed.options.file);
11738
12056
  const inline = stringOpt(parsed.options.body);
11739
- const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11740
- if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
12057
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
12058
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
12059
+ const body = typeof edited === "string" ? edited : edited.body;
11741
12060
  return runbookEdit(
11742
12061
  ctx,
11743
12062
  name,
11744
12063
  body,
11745
12064
  stringOpt(parsed.options.note),
11746
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
12065
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
12066
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11747
12067
  );
11748
12068
  }
11749
12069
  case "import": {
@@ -12414,4 +12734,4 @@ export {
12414
12734
  exitCodeFor,
12415
12735
  runCli
12416
12736
  };
12417
- //# sourceMappingURL=chunk-JJ42NV7X.js.map
12737
+ //# sourceMappingURL=chunk-ZAZBUURZ.js.map