@odla-ai/cli 0.25.23 → 0.26.2

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
  }
@@ -619,6 +619,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
619
619
  return token;
620
620
  }
621
621
 
622
+ // src/principal-presentation.ts
623
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
624
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
625
+ const id = typeof principalId === "string" ? principalId.trim() : "";
626
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
627
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
628
+ }
629
+
622
630
  // src/admin-ai-audit.ts
623
631
  function adminAiAuditQuery(filters) {
624
632
  if (filters.limit === void 0) return "";
@@ -648,17 +656,17 @@ async function readAdminAiAudit(request2) {
648
656
  String(event.changeKind ?? ""),
649
657
  String(event.purpose ?? event.provider ?? ""),
650
658
  route2,
651
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
659
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
652
660
  ].join(" "));
653
661
  }
654
662
  }
655
663
  async function responseBody(response2) {
656
- const text = await response2.text();
657
- if (!text) return {};
664
+ const text2 = await response2.text();
665
+ if (!text2) return {};
658
666
  try {
659
- return JSON.parse(text);
667
+ return JSON.parse(text2);
660
668
  } catch {
661
- return { message: text.slice(0, 300) };
669
+ return { message: text2.slice(0, 300) };
662
670
  }
663
671
  }
664
672
  function apiError(status, body) {
@@ -739,7 +747,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
739
747
  timestamp2(event.created_at),
740
748
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
741
749
  String(event.run_id ?? ""),
742
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
750
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
743
751
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
744
752
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
745
753
  String(input + output),
@@ -760,12 +768,12 @@ function timestamp2(value2) {
760
768
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
761
769
  }
762
770
  async function responseBody2(res) {
763
- const text = await res.text();
764
- if (!text) return {};
771
+ const text2 = await res.text();
772
+ if (!text2) return {};
765
773
  try {
766
- return JSON.parse(text);
774
+ return JSON.parse(text2);
767
775
  } catch {
768
- return { message: text.slice(0, 300) };
776
+ return { message: text2.slice(0, 300) };
769
777
  }
770
778
  }
771
779
  function apiError2(action2, status, body) {
@@ -941,12 +949,12 @@ function catalogModels(body) {
941
949
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
942
950
  }
943
951
  async function responseBody3(res) {
944
- const text = await res.text();
945
- if (!text) return {};
952
+ const text2 = await res.text();
953
+ if (!text2) return {};
946
954
  try {
947
- return JSON.parse(text);
955
+ return JSON.parse(text2);
948
956
  } catch {
949
- return { message: text.slice(0, 300) };
957
+ return { message: text2.slice(0, 300) };
950
958
  }
951
959
  }
952
960
  function apiError3(action2, status, body) {
@@ -1790,8 +1798,8 @@ async function appImport(options) {
1790
1798
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1791
1799
  const doFetch = options.fetch ?? fetch;
1792
1800
  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);
1801
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1802
+ const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1795
1803
  if (format === "namespace-map" && options.ns) {
1796
1804
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1797
1805
  }
@@ -1882,7 +1890,10 @@ function report(options, owners, headline) {
1882
1890
  if (headline) out.log(headline);
1883
1891
  out.log(`owners (${owners.length}):`);
1884
1892
  for (const o of owners) {
1885
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
1893
+ const name = o.email?.trim() || "Unnamed member";
1894
+ out.log(
1895
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1896
+ );
1886
1897
  }
1887
1898
  }
1888
1899
  async function ownersList(options) {
@@ -3627,22 +3638,26 @@ function planNextActions(reconciliation, configPath) {
3627
3638
  }
3628
3639
  }
3629
3640
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3630
- const { appId, platformUrl } = reconciliation.scope;
3631
3641
  next.push({
3632
3642
  code: "review_destructive",
3633
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3643
+ command: studioSettingsUrl(reconciliation),
3634
3644
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3635
3645
  });
3636
3646
  }
3637
3647
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3638
3648
  next.push({
3639
3649
  code: "declare_or_accept_runtime_state",
3640
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3650
+ command: studioSettingsUrl(reconciliation),
3641
3651
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3642
3652
  });
3643
3653
  }
3644
3654
  return next;
3645
3655
  }
3656
+ function studioSettingsUrl(reconciliation) {
3657
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
3658
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
3659
+ return `${origin}${(0, import_apps8.studioAppSettingsPath)(reconciliation.scope.appId, environment2, "environment")}`;
3660
+ }
3646
3661
  function quoteArg2(value2) {
3647
3662
  return `'${value2.replace(/'/g, `'\\''`)}'`;
3648
3663
  }
@@ -3682,15 +3697,15 @@ function readWranglerConfig(path) {
3682
3697
  return null;
3683
3698
  }
3684
3699
  }
3685
- function stripJsonComments(text) {
3700
+ function stripJsonComments(text2) {
3686
3701
  let result = "";
3687
3702
  let inString = false;
3688
- for (let i = 0; i < text.length; i++) {
3689
- const ch = text[i];
3703
+ for (let i = 0; i < text2.length; i++) {
3704
+ const ch = text2[i];
3690
3705
  if (inString) {
3691
3706
  result += ch;
3692
3707
  if (ch === "\\") {
3693
- result += text[i + 1] ?? "";
3708
+ result += text2[i + 1] ?? "";
3694
3709
  i++;
3695
3710
  } else if (ch === '"') {
3696
3711
  inString = false;
@@ -3702,14 +3717,14 @@ function stripJsonComments(text) {
3702
3717
  result += ch;
3703
3718
  continue;
3704
3719
  }
3705
- if (ch === "/" && text[i + 1] === "/") {
3706
- while (i < text.length && text[i] !== "\n") i++;
3720
+ if (ch === "/" && text2[i + 1] === "/") {
3721
+ while (i < text2.length && text2[i] !== "\n") i++;
3707
3722
  result += "\n";
3708
3723
  continue;
3709
3724
  }
3710
- if (ch === "/" && text[i + 1] === "*") {
3725
+ if (ch === "/" && text2[i + 1] === "*") {
3711
3726
  i += 2;
3712
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3727
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
3713
3728
  i++;
3714
3729
  continue;
3715
3730
  }
@@ -3726,7 +3741,14 @@ async function wranglerLoggedIn(run, cwd) {
3726
3741
  }
3727
3742
  }
3728
3743
  function wranglerPutSecret(run, opts) {
3729
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
3744
+ const args = [
3745
+ "wrangler",
3746
+ "secret",
3747
+ "put",
3748
+ opts.name,
3749
+ ...opts.env ? ["--env", opts.env] : [],
3750
+ ...opts.configPath ? ["--config", opts.configPath] : []
3751
+ ];
3730
3752
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3731
3753
  }
3732
3754
 
@@ -4104,9 +4126,9 @@ function initProject(options) {
4104
4126
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
4105
4127
  out.log("updated .gitignore for local odla credentials");
4106
4128
  }
4107
- function writeIfMissing(path, text) {
4129
+ function writeIfMissing(path, text2) {
4108
4130
  if ((0, import_node_fs13.existsSync)(path)) return;
4109
- (0, import_node_fs13.writeFileSync)(path, text);
4131
+ (0, import_node_fs13.writeFileSync)(path, text2);
4110
4132
  }
4111
4133
  function configTemplate(input) {
4112
4134
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4314,13 +4336,13 @@ async function secretsSetClerkKey(options) {
4314
4336
  body: JSON.stringify({ value: value2 })
4315
4337
  });
4316
4338
  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"}`);
4339
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4340
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
4319
4341
  }
4320
4342
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
4321
4343
  }
4322
- function scrubValue(text, value2) {
4323
- return redactSecrets(text).split(value2).join("[value redacted]");
4344
+ function scrubValue(text2, value2) {
4345
+ return redactSecrets(text2).split(value2).join("[value redacted]");
4324
4346
  }
4325
4347
  async function resolveVaultWrite(options) {
4326
4348
  const out = options.stdout ?? console;
@@ -5148,8 +5170,8 @@ async function runContainerAttempt(options) {
5148
5170
  let stopped = false;
5149
5171
  let exited = false;
5150
5172
  child.stderr.setEncoding("utf8");
5151
- child.stderr.on("data", (text) => {
5152
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
5173
+ child.stderr.on("data", (text2) => {
5174
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
5153
5175
  });
5154
5176
  const stop = (reason) => {
5155
5177
  if (stopped || exited) return;
@@ -5981,8 +6003,8 @@ function boundedInteger(value2, spec) {
5981
6003
  }
5982
6004
  function boundedNumber(value2, spec) {
5983
6005
  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.");
6006
+ const text2 = String(value2);
6007
+ 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
6008
  return value2;
5987
6009
  }
5988
6010
  function enumMember(value2, spec) {
@@ -6138,8 +6160,8 @@ function validateUnsafeSelector(path, value2, tool) {
6138
6160
  return void 0;
6139
6161
  }
6140
6162
  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);
6163
+ const text2 = value2.trim();
6164
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
6143
6165
  }
6144
6166
 
6145
6167
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -6508,8 +6530,8 @@ function gitApply(cwd, patch2, check) {
6508
6530
  });
6509
6531
  let stderr = "";
6510
6532
  child.stderr.setEncoding("utf8");
6511
- child.stderr.on("data", (text) => {
6512
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
6533
+ child.stderr.on("data", (text2) => {
6534
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
6513
6535
  });
6514
6536
  child.once("error", reject);
6515
6537
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -7680,10 +7702,10 @@ var CodePiRuntimeEngine = class {
7680
7702
  task: lease.task,
7681
7703
  limits: this.options.limits,
7682
7704
  signal: active.abort.signal,
7683
- onStderr: (text) => this.#event(command, {
7705
+ onStderr: (text2) => this.#event(command, {
7684
7706
  type: "message",
7685
7707
  actor: "system",
7686
- body: text.slice(0, 4e3)
7708
+ body: text2.slice(0, 4e3)
7687
7709
  }, active.conversationRefs),
7688
7710
  onMessage: async (output) => {
7689
7711
  if (output.type === "inference.request") {
@@ -8565,7 +8587,8 @@ Usage:
8565
8587
  odla-ai doctor [--config odla.config.mjs]
8566
8588
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8567
8589
  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]
8590
+ odla-ai operations get <operation-id> [--json]
8591
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8569
8592
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8570
8593
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8571
8594
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -8679,9 +8702,9 @@ Commands:
8679
8702
  does and what it guarantees is JSDoc, rendered per package at
8680
8703
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
8681
8704
  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.
8705
+ whoami Report the actual principal name, handle and kind, its manager,
8706
+ credential kind, accountable owner, and platform-admin status.
8707
+ A manager relationship never supplies authority by itself.
8685
8708
  context Explain selected config, platform, app, environment, and
8686
8709
  developer-token provenance without printing credentials or
8687
8710
  starting a device handshake. Operator work can run outside a
@@ -8801,6 +8824,58 @@ Safety:
8801
8824
  `);
8802
8825
  }
8803
8826
 
8827
+ // src/discuss-principals.ts
8828
+ function mergeDiscussPrincipals(target, source) {
8829
+ Object.assign(target.authors, source.authors ?? {});
8830
+ Object.assign(target.principals, source.principals ?? {});
8831
+ }
8832
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
8833
+ function discussPrincipalLabel(principalId, authorKind, projection) {
8834
+ const profile = projection.principals?.[principalId];
8835
+ if (profile) {
8836
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
8837
+ if (profile.kind === "agent") {
8838
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
8839
+ }
8840
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
8841
+ }
8842
+ const legacyName = projection.authors?.[principalId]?.trim();
8843
+ if (legacyName) {
8844
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
8845
+ }
8846
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
8847
+ }
8848
+
8849
+ // src/discuss-read-render.ts
8850
+ function discussBodyWithRefs(post) {
8851
+ if (!post.refs || post.refs.length === 0) return post.body;
8852
+ let out = "";
8853
+ let cursor = 0;
8854
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8855
+ if (ref.start < cursor || ref.end > post.body.length) continue;
8856
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8857
+ cursor = ref.end;
8858
+ }
8859
+ return out + post.body.slice(cursor);
8860
+ }
8861
+ function renderDiscussRead(ctx, topic, posts, projection) {
8862
+ const status = topic.resolved ? "resolved" : "open";
8863
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
8864
+ for (const post of posts) {
8865
+ const who = discussPrincipalLabel(
8866
+ post.authorId,
8867
+ post.authorKind,
8868
+ projection
8869
+ );
8870
+ ctx.out.log(`
8871
+ \u2014 ${who}`);
8872
+ ctx.out.log(discussBodyWithRefs(post));
8873
+ for (const file of post.attachments ?? []) {
8874
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8875
+ }
8876
+ }
8877
+ }
8878
+
8804
8879
  // src/discuss-actions.ts
8805
8880
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8806
8881
  async function request(ctx, method, path, body) {
@@ -8819,17 +8894,6 @@ function emit(ctx, value2, human) {
8819
8894
  else human();
8820
8895
  }
8821
8896
  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
8897
  function content(parsed) {
8834
8898
  const markup = stringOpt(parsed.options.markup);
8835
8899
  if (markup) return { markup };
@@ -8884,11 +8948,19 @@ async function discussRead(ctx, id, parsed) {
8884
8948
  offset: requestedOffset ?? "0"
8885
8949
  });
8886
8950
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
8887
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
8951
+ emit(
8952
+ ctx,
8953
+ page,
8954
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
8955
+ );
8888
8956
  return;
8889
8957
  }
8890
8958
  for (let scan = 0; scan < 3; scan++) {
8891
8959
  const posts = /* @__PURE__ */ new Map();
8960
+ const projection = {
8961
+ authors: {},
8962
+ principals: {}
8963
+ };
8892
8964
  let topic = null;
8893
8965
  let offset = 0;
8894
8966
  for (; ; ) {
@@ -8899,6 +8971,7 @@ async function discussRead(ctx, id, parsed) {
8899
8971
  );
8900
8972
  topic = page.topic;
8901
8973
  for (const post of page.posts) posts.set(post.id, post);
8974
+ mergeDiscussPrincipals(projection, page);
8902
8975
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
8903
8976
  if (!page.page?.hasMore) break;
8904
8977
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -8911,24 +8984,18 @@ async function discussRead(ctx, id, parsed) {
8911
8984
  );
8912
8985
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
8913
8986
  if (ordered.length === expected) {
8914
- const result = { topic, posts: ordered };
8915
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
8987
+ const result = { topic, posts: ordered, ...projection };
8988
+ emit(
8989
+ ctx,
8990
+ result,
8991
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
8992
+ );
8916
8993
  return;
8917
8994
  }
8918
8995
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
8919
8996
  }
8920
8997
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
8921
8998
  }
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
8999
  async function discussPost(ctx, parsed) {
8933
9000
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8934
9001
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -9153,6 +9220,8 @@ async function discussWatch(ctx, topicId, parsed) {
9153
9220
  found: true,
9154
9221
  cursor,
9155
9222
  events: matching,
9223
+ ...page.authors ? { authors: page.authors } : {},
9224
+ ...page.principals ? { principals: page.principals } : {},
9156
9225
  ...posts && posts.length > 0 ? { posts } : {},
9157
9226
  ...topics && topics.length > 0 ? { topics } : {}
9158
9227
  });
@@ -9185,7 +9254,11 @@ function report2(ctx, parsed, result) {
9185
9254
  ctx.out.log(JSON.stringify(result, null, 2));
9186
9255
  } else if (parsed.options.jsonl !== true && result.found) {
9187
9256
  for (const post of result.posts ?? []) {
9188
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9257
+ const who = discussPrincipalLabel(
9258
+ post.authorId,
9259
+ post.authorKind,
9260
+ result
9261
+ );
9189
9262
  ctx.out.log(`\u2014 ${who}
9190
9263
  ${post.body}`);
9191
9264
  }
@@ -9330,8 +9403,8 @@ function collectFields(parsed, allowClear) {
9330
9403
  if (allowClear) out[spec.key] = null;
9331
9404
  continue;
9332
9405
  }
9333
- const text = stringOpt(value2);
9334
- out[spec.key] = spec.num ? Number(text) : text;
9406
+ const text2 = stringOpt(value2);
9407
+ out[spec.key] = spec.num ? Number(text2) : text2;
9335
9408
  }
9336
9409
  return out;
9337
9410
  }
@@ -9661,17 +9734,23 @@ function age(input) {
9661
9734
 
9662
9735
  // src/platform-command.ts
9663
9736
  async function platformCommand(parsed, deps = {}) {
9737
+ const action2 = parsed.positionals[1];
9738
+ if (action2 === "status") {
9739
+ return platformStatus(parsed, deps);
9740
+ }
9741
+ throw new Error(
9742
+ `unknown platform action "${[
9743
+ action2,
9744
+ parsed.positionals[2]
9745
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
9746
+ );
9747
+ }
9748
+ async function platformStatus(parsed, deps) {
9664
9749
  assertArgs(
9665
9750
  parsed,
9666
9751
  ["config", "context", "platform", "token", "email", "open", "json"],
9667
9752
  2
9668
9753
  );
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
9754
  const context = await resolveOperatorContext(parsed, {
9676
9755
  allowMissingConfig: true
9677
9756
  });
@@ -10116,14 +10195,14 @@ function statusMinutes(value2) {
10116
10195
  }
10117
10196
  async function read2(url, headers, doFetch) {
10118
10197
  const response2 = await doFetch(url, { headers });
10119
- const text = await response2.text();
10198
+ const text2 = await response2.text();
10120
10199
  let body = {};
10121
- if (text) {
10200
+ if (text2) {
10122
10201
  try {
10123
- const value2 = JSON.parse(text);
10202
+ const value2 = JSON.parse(text2);
10124
10203
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
10125
10204
  } catch {
10126
- body = { message: text.slice(0, 300) };
10205
+ body = { message: text2.slice(0, 300) };
10127
10206
  }
10128
10207
  }
10129
10208
  return { httpStatus: response2.status, body };
@@ -10548,7 +10627,9 @@ var COMMAND_SURFACE = {
10548
10627
  init: {},
10549
10628
  o11y: { status: {} },
10550
10629
  operations: { get: {}, wait: {} },
10551
- platform: { status: {} },
10630
+ platform: {
10631
+ status: {}
10632
+ },
10552
10633
  pm: {
10553
10634
  ...PM_ENTITIES,
10554
10635
  handoff: {}
@@ -10766,12 +10847,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10766
10847
  });
10767
10848
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10768
10849
  }
10769
- async function runbookEdit(ctx, slug, body, note, requires) {
10850
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10770
10851
  const runbook = await bySlug(ctx, slug);
10771
10852
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10772
10853
  // An empty --requires clears the declaration; omitting the flag leaves
10773
10854
  // whatever is there, so an ordinary body edit never drops it.
10774
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
10855
+ patch: {
10856
+ body,
10857
+ expectedVersion: expectedVersion ?? runbook.version,
10858
+ ...note ? { note } : {},
10859
+ ...requires === void 0 ? {} : { requires: requires || null }
10860
+ }
10775
10861
  });
10776
10862
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10777
10863
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -10825,12 +10911,12 @@ async function runbookRemove(ctx, slug) {
10825
10911
  // src/runbook-import.ts
10826
10912
  var import_node_fs18 = require("fs");
10827
10913
  var import_node_path15 = require("path");
10828
- function parseRunbook(text, slug) {
10829
- let rest = text;
10914
+ function parseRunbook(text2, slug) {
10915
+ let rest = text2;
10830
10916
  const meta = {};
10831
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
10917
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
10832
10918
  if (fm) {
10833
- rest = text.slice(fm[0].length);
10919
+ rest = text2.slice(fm[0].length);
10834
10920
  for (const line of fm[1].split(/\r?\n/)) {
10835
10921
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
10836
10922
  if (!pair) continue;
@@ -10911,6 +10997,7 @@ async function upsert(ctx, r, visibility) {
10911
10997
  patch: {
10912
10998
  title: r.title,
10913
10999
  body: r.body,
11000
+ expectedVersion: found.version,
10914
11001
  ...r.summary ? { summary: r.summary } : {},
10915
11002
  ...r.tags ? { tags: r.tags } : {},
10916
11003
  note: `imported from ${r.file}`
@@ -11351,27 +11438,90 @@ async function editRunbook(ctx, slug, deps = {}) {
11351
11438
  const found = page.records[0];
11352
11439
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11353
11440
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11354
- return editText(found.body, slug, deps);
11441
+ const body = await editText(found.body, slug, deps);
11442
+ return body === null ? null : { body, expectedVersion: found.version };
11355
11443
  }
11356
11444
 
11357
11445
  // src/whoami-command.ts
11446
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11447
+ function principalKind(value2, machine) {
11448
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11449
+ }
11450
+ function credentialKind(value2, machine, scopes) {
11451
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11452
+ if (machine) return "machine";
11453
+ if (scopes.length) return "device";
11454
+ return "unknown";
11455
+ }
11456
+ function managerOf(value2) {
11457
+ if (!value2 || typeof value2 !== "object") return null;
11458
+ const row = value2;
11459
+ const principalId = text(row.principalId);
11460
+ if (!principalId) return null;
11461
+ return {
11462
+ principalId,
11463
+ displayName: text(row.displayName) ?? "Unnamed member",
11464
+ handle: text(row.handle) ?? ""
11465
+ };
11466
+ }
11467
+ function unnamedPrincipal(kind) {
11468
+ if (kind === "agent") return "Unnamed agent";
11469
+ if (kind === "service") return "Unnamed service";
11470
+ return "Unnamed member";
11471
+ }
11358
11472
  async function fetchIdentity(platformUrl, token, doFetch) {
11359
11473
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11360
11474
  headers: { authorization: `Bearer ${token}` }
11361
11475
  });
11362
11476
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11363
11477
  const body = await res.json();
11478
+ const developerId = text(body.developerId) ?? "";
11479
+ const machine = body.machine === true;
11480
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11481
+ const principalId = text(body.principalId) ?? developerId;
11482
+ const email = text(body.email);
11483
+ const kind = principalKind(body.principalKind, machine);
11484
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11485
+ const handle = text(body.handle) ?? "";
11486
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11364
11487
  return {
11365
- developerId: String(body.developerId ?? ""),
11366
- email: body.email ?? null,
11488
+ developerId,
11489
+ principalId,
11490
+ principalKind: kind,
11491
+ displayName,
11492
+ handle,
11493
+ manager: managerOf(body.manager),
11494
+ credential: {
11495
+ id: text(credential2.id),
11496
+ kind: credentialKind(credential2.kind, machine, scopes)
11497
+ },
11498
+ email,
11367
11499
  admin: body.admin === true,
11368
- machine: body.machine === true,
11369
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11500
+ machine,
11501
+ scopes
11370
11502
  };
11371
11503
  }
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";
11504
+ function credentialLabel(identity) {
11505
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11506
+ if (identity.credential.kind === "device")
11507
+ return identity.scopes.length ? "device (scoped)" : "device";
11508
+ if (identity.credential.kind === "clerk") return "clerk";
11509
+ return "unknown (legacy server)";
11510
+ }
11511
+ function namedPrincipal(identity) {
11512
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11513
+ }
11514
+ function namedManager(manager) {
11515
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11516
+ }
11517
+ function accountableOwner(identity) {
11518
+ if (identity.principalKind === "agent" && identity.manager) {
11519
+ return namedManager(identity.manager);
11520
+ }
11521
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11522
+ return namedPrincipal(identity);
11523
+ }
11524
+ return identity.email ?? "Unnamed member";
11375
11525
  }
11376
11526
  async function whoamiCommand(parsed, deps = {}) {
11377
11527
  assertArgs(
@@ -11402,9 +11552,19 @@ async function whoamiCommand(parsed, deps = {}) {
11402
11552
  return;
11403
11553
  }
11404
11554
  out.log(`platform: ${cfg.platformUrl}`);
11405
- out.log(`developer: ${identity.developerId}`);
11555
+ out.log(`principal: ${namedPrincipal(identity)}`);
11556
+ out.log(`principal id: ${identity.principalId}`);
11557
+ out.log(`kind: ${identity.principalKind}`);
11558
+ if (identity.principalKind === "agent")
11559
+ out.log(
11560
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11561
+ );
11562
+ out.log(`owner: ${accountableOwner(identity)}`);
11563
+ out.log(`owner id: ${identity.developerId}`);
11406
11564
  out.log(`email: ${identity.email ?? "(none)"}`);
11407
- out.log(`credential: ${credentialKind(identity)}`);
11565
+ out.log(`credential: ${credentialLabel(identity)}`);
11566
+ if (identity.credential.id)
11567
+ out.log(`credential id: ${identity.credential.id}`);
11408
11568
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11409
11569
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11410
11570
  if (!identity.admin) {
@@ -11553,14 +11713,16 @@ async function runbookCommand(parsed, deps = {}) {
11553
11713
  const name = requireSlug(slug, "edit");
11554
11714
  const file = stringOpt(parsed.options.file);
11555
11715
  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`);
11716
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11717
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
11718
+ const body = typeof edited === "string" ? edited : edited.body;
11558
11719
  return runbookEdit(
11559
11720
  ctx,
11560
11721
  name,
11561
11722
  body,
11562
11723
  stringOpt(parsed.options.note),
11563
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
11724
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
11725
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11564
11726
  );
11565
11727
  }
11566
11728
  case "import": {