@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/bin.cjs CHANGED
@@ -182,10 +182,10 @@ function isManagedDevVar(line) {
182
182
  const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
183
183
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
184
184
  }
185
- function writePrivateText(path, text) {
185
+ function writePrivateText(path, text2) {
186
186
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
187
187
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
188
- (0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
188
+ (0, import_node_fs.writeFileSync)(temporary, text2, { mode: 384 });
189
189
  (0, import_node_fs.chmodSync)(temporary, 384);
190
190
  (0, import_node_fs.renameSync)(temporary, path);
191
191
  }
@@ -551,6 +551,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
551
551
  return token;
552
552
  }
553
553
 
554
+ // src/principal-presentation.ts
555
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
556
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
557
+ const id = typeof principalId === "string" ? principalId.trim() : "";
558
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
559
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
560
+ }
561
+
554
562
  // src/admin-ai-audit.ts
555
563
  function adminAiAuditQuery(filters) {
556
564
  if (filters.limit === void 0) return "";
@@ -580,17 +588,17 @@ async function readAdminAiAudit(request2) {
580
588
  String(event.changeKind ?? ""),
581
589
  String(event.purpose ?? event.provider ?? ""),
582
590
  route2,
583
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
591
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
584
592
  ].join(" "));
585
593
  }
586
594
  }
587
595
  async function responseBody(response2) {
588
- const text = await response2.text();
589
- if (!text) return {};
596
+ const text2 = await response2.text();
597
+ if (!text2) return {};
590
598
  try {
591
- return JSON.parse(text);
599
+ return JSON.parse(text2);
592
600
  } catch {
593
- return { message: text.slice(0, 300) };
601
+ return { message: text2.slice(0, 300) };
594
602
  }
595
603
  }
596
604
  function apiError(status, body) {
@@ -671,7 +679,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
671
679
  timestamp2(event.created_at),
672
680
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
673
681
  String(event.run_id ?? ""),
674
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
682
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
675
683
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
676
684
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
677
685
  String(input + output),
@@ -692,12 +700,12 @@ function timestamp2(value2) {
692
700
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
693
701
  }
694
702
  async function responseBody2(res) {
695
- const text = await res.text();
696
- if (!text) return {};
703
+ const text2 = await res.text();
704
+ if (!text2) return {};
697
705
  try {
698
- return JSON.parse(text);
706
+ return JSON.parse(text2);
699
707
  } catch {
700
- return { message: text.slice(0, 300) };
708
+ return { message: text2.slice(0, 300) };
701
709
  }
702
710
  }
703
711
  function apiError2(action2, status, body) {
@@ -873,12 +881,12 @@ function catalogModels(body) {
873
881
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
874
882
  }
875
883
  async function responseBody3(res) {
876
- const text = await res.text();
877
- if (!text) return {};
884
+ const text2 = await res.text();
885
+ if (!text2) return {};
878
886
  try {
879
- return JSON.parse(text);
887
+ return JSON.parse(text2);
880
888
  } catch {
881
- return { message: text.slice(0, 300) };
889
+ return { message: text2.slice(0, 300) };
882
890
  }
883
891
  }
884
892
  function apiError3(action2, status, body) {
@@ -1722,8 +1730,8 @@ async function appImport(options) {
1722
1730
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1723
1731
  const doFetch = options.fetch ?? fetch;
1724
1732
  const { tenant } = resolveTenant(cfg, options.env);
1725
- const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1726
- const { format, sources } = (0, import_import.parseImport)(text, options.ns);
1733
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1734
+ const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1727
1735
  if (format === "namespace-map" && options.ns) {
1728
1736
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1729
1737
  }
@@ -1814,7 +1822,10 @@ function report(options, owners, headline) {
1814
1822
  if (headline) out.log(headline);
1815
1823
  out.log(`owners (${owners.length}):`);
1816
1824
  for (const o of owners) {
1817
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
1825
+ const name = o.email?.trim() || "Unnamed member";
1826
+ out.log(
1827
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1828
+ );
1818
1829
  }
1819
1830
  }
1820
1831
  async function ownersList(options) {
@@ -3559,22 +3570,26 @@ function planNextActions(reconciliation, configPath) {
3559
3570
  }
3560
3571
  }
3561
3572
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3562
- const { appId, platformUrl } = reconciliation.scope;
3563
3573
  next.push({
3564
3574
  code: "review_destructive",
3565
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3575
+ command: studioSettingsUrl(reconciliation),
3566
3576
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3567
3577
  });
3568
3578
  }
3569
3579
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3570
3580
  next.push({
3571
3581
  code: "declare_or_accept_runtime_state",
3572
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3582
+ command: studioSettingsUrl(reconciliation),
3573
3583
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3574
3584
  });
3575
3585
  }
3576
3586
  return next;
3577
3587
  }
3588
+ function studioSettingsUrl(reconciliation) {
3589
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
3590
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
3591
+ return `${origin}${(0, import_apps8.studioAppSettingsPath)(reconciliation.scope.appId, environment2, "environment")}`;
3592
+ }
3578
3593
  function quoteArg2(value2) {
3579
3594
  return `'${value2.replace(/'/g, `'\\''`)}'`;
3580
3595
  }
@@ -3614,15 +3629,15 @@ function readWranglerConfig(path) {
3614
3629
  return null;
3615
3630
  }
3616
3631
  }
3617
- function stripJsonComments(text) {
3632
+ function stripJsonComments(text2) {
3618
3633
  let result = "";
3619
3634
  let inString = false;
3620
- for (let i = 0; i < text.length; i++) {
3621
- const ch = text[i];
3635
+ for (let i = 0; i < text2.length; i++) {
3636
+ const ch = text2[i];
3622
3637
  if (inString) {
3623
3638
  result += ch;
3624
3639
  if (ch === "\\") {
3625
- result += text[i + 1] ?? "";
3640
+ result += text2[i + 1] ?? "";
3626
3641
  i++;
3627
3642
  } else if (ch === '"') {
3628
3643
  inString = false;
@@ -3634,14 +3649,14 @@ function stripJsonComments(text) {
3634
3649
  result += ch;
3635
3650
  continue;
3636
3651
  }
3637
- if (ch === "/" && text[i + 1] === "/") {
3638
- while (i < text.length && text[i] !== "\n") i++;
3652
+ if (ch === "/" && text2[i + 1] === "/") {
3653
+ while (i < text2.length && text2[i] !== "\n") i++;
3639
3654
  result += "\n";
3640
3655
  continue;
3641
3656
  }
3642
- if (ch === "/" && text[i + 1] === "*") {
3657
+ if (ch === "/" && text2[i + 1] === "*") {
3643
3658
  i += 2;
3644
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3659
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
3645
3660
  i++;
3646
3661
  continue;
3647
3662
  }
@@ -3658,7 +3673,14 @@ async function wranglerLoggedIn(run, cwd) {
3658
3673
  }
3659
3674
  }
3660
3675
  function wranglerPutSecret(run, opts) {
3661
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
3676
+ const args = [
3677
+ "wrangler",
3678
+ "secret",
3679
+ "put",
3680
+ opts.name,
3681
+ ...opts.env ? ["--env", opts.env] : [],
3682
+ ...opts.configPath ? ["--config", opts.configPath] : []
3683
+ ];
3662
3684
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3663
3685
  }
3664
3686
 
@@ -4036,9 +4058,9 @@ function initProject(options) {
4036
4058
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
4037
4059
  out.log("updated .gitignore for local odla credentials");
4038
4060
  }
4039
- function writeIfMissing(path, text) {
4061
+ function writeIfMissing(path, text2) {
4040
4062
  if ((0, import_node_fs13.existsSync)(path)) return;
4041
- (0, import_node_fs13.writeFileSync)(path, text);
4063
+ (0, import_node_fs13.writeFileSync)(path, text2);
4042
4064
  }
4043
4065
  function configTemplate(input) {
4044
4066
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4246,13 +4268,13 @@ async function secretsSetClerkKey(options) {
4246
4268
  body: JSON.stringify({ value: value2 })
4247
4269
  });
4248
4270
  if (!res.ok) {
4249
- const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4250
- throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
4271
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4272
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
4251
4273
  }
4252
4274
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
4253
4275
  }
4254
- function scrubValue(text, value2) {
4255
- return redactSecrets(text).split(value2).join("[value redacted]");
4276
+ function scrubValue(text2, value2) {
4277
+ return redactSecrets(text2).split(value2).join("[value redacted]");
4256
4278
  }
4257
4279
  async function resolveVaultWrite(options) {
4258
4280
  const out = options.stdout ?? console;
@@ -5080,8 +5102,8 @@ async function runContainerAttempt(options) {
5080
5102
  let stopped = false;
5081
5103
  let exited = false;
5082
5104
  child.stderr.setEncoding("utf8");
5083
- child.stderr.on("data", (text) => {
5084
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
5105
+ child.stderr.on("data", (text2) => {
5106
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
5085
5107
  });
5086
5108
  const stop = (reason) => {
5087
5109
  if (stopped || exited) return;
@@ -5913,8 +5935,8 @@ function boundedInteger(value2, spec) {
5913
5935
  }
5914
5936
  function boundedNumber(value2, spec) {
5915
5937
  if (spec.kind !== "finite_number" || typeof value2 !== "number" || !Number.isFinite(value2) || value2 < spec.minimum || value2 > spec.maximum) throw new CamelError("conversion_rejected", "Finite-number conversion rejected the structured value.");
5916
- const text = String(value2);
5917
- if (/e/i.test(text) || (text.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
5938
+ const text2 = String(value2);
5939
+ if (/e/i.test(text2) || (text2.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
5918
5940
  return value2;
5919
5941
  }
5920
5942
  function enumMember(value2, spec) {
@@ -6070,8 +6092,8 @@ function validateUnsafeSelector(path, value2, tool) {
6070
6092
  return void 0;
6071
6093
  }
6072
6094
  function looksLikeDestination(value2) {
6073
- const text = value2.trim();
6074
- return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
6095
+ const text2 = value2.trim();
6096
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
6075
6097
  }
6076
6098
 
6077
6099
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -6440,8 +6462,8 @@ function gitApply(cwd, patch2, check) {
6440
6462
  });
6441
6463
  let stderr = "";
6442
6464
  child.stderr.setEncoding("utf8");
6443
- child.stderr.on("data", (text) => {
6444
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
6465
+ child.stderr.on("data", (text2) => {
6466
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
6445
6467
  });
6446
6468
  child.once("error", reject);
6447
6469
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -7612,10 +7634,10 @@ var CodePiRuntimeEngine = class {
7612
7634
  task: lease.task,
7613
7635
  limits: this.options.limits,
7614
7636
  signal: active.abort.signal,
7615
- onStderr: (text) => this.#event(command, {
7637
+ onStderr: (text2) => this.#event(command, {
7616
7638
  type: "message",
7617
7639
  actor: "system",
7618
- body: text.slice(0, 4e3)
7640
+ body: text2.slice(0, 4e3)
7619
7641
  }, active.conversationRefs),
7620
7642
  onMessage: async (output) => {
7621
7643
  if (output.type === "inference.request") {
@@ -8497,7 +8519,8 @@ Usage:
8497
8519
  odla-ai doctor [--config odla.config.mjs]
8498
8520
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8499
8521
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
8500
- odla-ai operations <get|wait> <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8522
+ odla-ai operations get <operation-id> [--json]
8523
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8501
8524
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8502
8525
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8503
8526
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -8611,9 +8634,9 @@ Commands:
8611
8634
  does and what it guarantees is JSDoc, rendered per package at
8612
8635
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
8613
8636
  a question usually needs both.
8614
- whoami Report who this terminal is authenticated as, and whether it holds
8615
- platform admin. A handshake-minted device token is never admin,
8616
- however the human who approved it is configured.
8637
+ whoami Report the actual principal name, handle and kind, its manager,
8638
+ credential kind, accountable owner, and platform-admin status.
8639
+ A manager relationship never supplies authority by itself.
8617
8640
  context Explain selected config, platform, app, environment, and
8618
8641
  developer-token provenance without printing credentials or
8619
8642
  starting a device handshake. Operator work can run outside a
@@ -8733,6 +8756,58 @@ Safety:
8733
8756
  `);
8734
8757
  }
8735
8758
 
8759
+ // src/discuss-principals.ts
8760
+ function mergeDiscussPrincipals(target, source) {
8761
+ Object.assign(target.authors, source.authors ?? {});
8762
+ Object.assign(target.principals, source.principals ?? {});
8763
+ }
8764
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
8765
+ function discussPrincipalLabel(principalId, authorKind, projection) {
8766
+ const profile = projection.principals?.[principalId];
8767
+ if (profile) {
8768
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
8769
+ if (profile.kind === "agent") {
8770
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
8771
+ }
8772
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
8773
+ }
8774
+ const legacyName = projection.authors?.[principalId]?.trim();
8775
+ if (legacyName) {
8776
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
8777
+ }
8778
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
8779
+ }
8780
+
8781
+ // src/discuss-read-render.ts
8782
+ function discussBodyWithRefs(post) {
8783
+ if (!post.refs || post.refs.length === 0) return post.body;
8784
+ let out = "";
8785
+ let cursor = 0;
8786
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8787
+ if (ref.start < cursor || ref.end > post.body.length) continue;
8788
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8789
+ cursor = ref.end;
8790
+ }
8791
+ return out + post.body.slice(cursor);
8792
+ }
8793
+ function renderDiscussRead(ctx, topic, posts, projection) {
8794
+ const status = topic.resolved ? "resolved" : "open";
8795
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
8796
+ for (const post of posts) {
8797
+ const who = discussPrincipalLabel(
8798
+ post.authorId,
8799
+ post.authorKind,
8800
+ projection
8801
+ );
8802
+ ctx.out.log(`
8803
+ \u2014 ${who}`);
8804
+ ctx.out.log(discussBodyWithRefs(post));
8805
+ for (const file of post.attachments ?? []) {
8806
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8807
+ }
8808
+ }
8809
+ }
8810
+
8736
8811
  // src/discuss-actions.ts
8737
8812
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8738
8813
  async function request(ctx, method, path, body) {
@@ -8751,17 +8826,6 @@ function emit(ctx, value2, human) {
8751
8826
  else human();
8752
8827
  }
8753
8828
  var state = (topic) => topic.resolved ? "resolved" : "open";
8754
- function bodyWithRefs(post) {
8755
- if (!post.refs || post.refs.length === 0) return post.body;
8756
- let out = "";
8757
- let cursor = 0;
8758
- for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8759
- if (ref.start < cursor || ref.end > post.body.length) continue;
8760
- out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8761
- cursor = ref.end;
8762
- }
8763
- return out + post.body.slice(cursor);
8764
- }
8765
8829
  function content(parsed) {
8766
8830
  const markup = stringOpt(parsed.options.markup);
8767
8831
  if (markup) return { markup };
@@ -8816,11 +8880,19 @@ async function discussRead(ctx, id, parsed) {
8816
8880
  offset: requestedOffset ?? "0"
8817
8881
  });
8818
8882
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
8819
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
8883
+ emit(
8884
+ ctx,
8885
+ page,
8886
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
8887
+ );
8820
8888
  return;
8821
8889
  }
8822
8890
  for (let scan = 0; scan < 3; scan++) {
8823
8891
  const posts = /* @__PURE__ */ new Map();
8892
+ const projection = {
8893
+ authors: {},
8894
+ principals: {}
8895
+ };
8824
8896
  let topic = null;
8825
8897
  let offset = 0;
8826
8898
  for (; ; ) {
@@ -8831,6 +8903,7 @@ async function discussRead(ctx, id, parsed) {
8831
8903
  );
8832
8904
  topic = page.topic;
8833
8905
  for (const post of page.posts) posts.set(post.id, post);
8906
+ mergeDiscussPrincipals(projection, page);
8834
8907
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
8835
8908
  if (!page.page?.hasMore) break;
8836
8909
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -8843,24 +8916,18 @@ async function discussRead(ctx, id, parsed) {
8843
8916
  );
8844
8917
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
8845
8918
  if (ordered.length === expected) {
8846
- const result = { topic, posts: ordered };
8847
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
8919
+ const result = { topic, posts: ordered, ...projection };
8920
+ emit(
8921
+ ctx,
8922
+ result,
8923
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
8924
+ );
8848
8925
  return;
8849
8926
  }
8850
8927
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
8851
8928
  }
8852
8929
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
8853
8930
  }
8854
- function renderRead(ctx, topic, posts) {
8855
- ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
8856
- for (const post of posts) {
8857
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8858
- ctx.out.log(`
8859
- \u2014 ${who}`);
8860
- ctx.out.log(bodyWithRefs(post));
8861
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8862
- }
8863
- }
8864
8931
  async function discussPost(ctx, parsed) {
8865
8932
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8866
8933
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -9085,6 +9152,8 @@ async function discussWatch(ctx, topicId, parsed) {
9085
9152
  found: true,
9086
9153
  cursor,
9087
9154
  events: matching,
9155
+ ...page.authors ? { authors: page.authors } : {},
9156
+ ...page.principals ? { principals: page.principals } : {},
9088
9157
  ...posts && posts.length > 0 ? { posts } : {},
9089
9158
  ...topics && topics.length > 0 ? { topics } : {}
9090
9159
  });
@@ -9117,7 +9186,11 @@ function report2(ctx, parsed, result) {
9117
9186
  ctx.out.log(JSON.stringify(result, null, 2));
9118
9187
  } else if (parsed.options.jsonl !== true && result.found) {
9119
9188
  for (const post of result.posts ?? []) {
9120
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9189
+ const who = discussPrincipalLabel(
9190
+ post.authorId,
9191
+ post.authorKind,
9192
+ result
9193
+ );
9121
9194
  ctx.out.log(`\u2014 ${who}
9122
9195
  ${post.body}`);
9123
9196
  }
@@ -9262,8 +9335,8 @@ function collectFields(parsed, allowClear) {
9262
9335
  if (allowClear) out[spec.key] = null;
9263
9336
  continue;
9264
9337
  }
9265
- const text = stringOpt(value2);
9266
- out[spec.key] = spec.num ? Number(text) : text;
9338
+ const text2 = stringOpt(value2);
9339
+ out[spec.key] = spec.num ? Number(text2) : text2;
9267
9340
  }
9268
9341
  return out;
9269
9342
  }
@@ -9593,17 +9666,23 @@ function age(input) {
9593
9666
 
9594
9667
  // src/platform-command.ts
9595
9668
  async function platformCommand(parsed, deps = {}) {
9669
+ const action2 = parsed.positionals[1];
9670
+ if (action2 === "status") {
9671
+ return platformStatus(parsed, deps);
9672
+ }
9673
+ throw new Error(
9674
+ `unknown platform action "${[
9675
+ action2,
9676
+ parsed.positionals[2]
9677
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
9678
+ );
9679
+ }
9680
+ async function platformStatus(parsed, deps) {
9596
9681
  assertArgs(
9597
9682
  parsed,
9598
9683
  ["config", "context", "platform", "token", "email", "open", "json"],
9599
9684
  2
9600
9685
  );
9601
- const action2 = parsed.positionals[1];
9602
- if (action2 !== "status") {
9603
- throw new Error(
9604
- `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
9605
- );
9606
- }
9607
9686
  const context = await resolveOperatorContext(parsed, {
9608
9687
  allowMissingConfig: true
9609
9688
  });
@@ -10048,14 +10127,14 @@ function statusMinutes(value2) {
10048
10127
  }
10049
10128
  async function read2(url, headers, doFetch) {
10050
10129
  const response2 = await doFetch(url, { headers });
10051
- const text = await response2.text();
10130
+ const text2 = await response2.text();
10052
10131
  let body = {};
10053
- if (text) {
10132
+ if (text2) {
10054
10133
  try {
10055
- const value2 = JSON.parse(text);
10134
+ const value2 = JSON.parse(text2);
10056
10135
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
10057
10136
  } catch {
10058
- body = { message: text.slice(0, 300) };
10137
+ body = { message: text2.slice(0, 300) };
10059
10138
  }
10060
10139
  }
10061
10140
  return { httpStatus: response2.status, body };
@@ -10480,7 +10559,9 @@ var COMMAND_SURFACE = {
10480
10559
  init: {},
10481
10560
  o11y: { status: {} },
10482
10561
  operations: { get: {}, wait: {} },
10483
- platform: { status: {} },
10562
+ platform: {
10563
+ status: {}
10564
+ },
10484
10565
  pm: {
10485
10566
  ...PM_ENTITIES,
10486
10567
  handoff: {}
@@ -10689,12 +10770,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10689
10770
  });
10690
10771
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10691
10772
  }
10692
- async function runbookEdit(ctx, slug, body, note, requires) {
10773
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10693
10774
  const runbook = await bySlug(ctx, slug);
10694
10775
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10695
10776
  // An empty --requires clears the declaration; omitting the flag leaves
10696
10777
  // whatever is there, so an ordinary body edit never drops it.
10697
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
10778
+ patch: {
10779
+ body,
10780
+ expectedVersion: expectedVersion ?? runbook.version,
10781
+ ...note ? { note } : {},
10782
+ ...requires === void 0 ? {} : { requires: requires || null }
10783
+ }
10698
10784
  });
10699
10785
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10700
10786
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -10748,12 +10834,12 @@ async function runbookRemove(ctx, slug) {
10748
10834
  // src/runbook-import.ts
10749
10835
  var import_node_fs18 = require("fs");
10750
10836
  var import_node_path15 = require("path");
10751
- function parseRunbook(text, slug) {
10752
- let rest = text;
10837
+ function parseRunbook(text2, slug) {
10838
+ let rest = text2;
10753
10839
  const meta = {};
10754
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
10840
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
10755
10841
  if (fm) {
10756
- rest = text.slice(fm[0].length);
10842
+ rest = text2.slice(fm[0].length);
10757
10843
  for (const line of fm[1].split(/\r?\n/)) {
10758
10844
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
10759
10845
  if (!pair) continue;
@@ -10834,6 +10920,7 @@ async function upsert(ctx, r, visibility) {
10834
10920
  patch: {
10835
10921
  title: r.title,
10836
10922
  body: r.body,
10923
+ expectedVersion: found.version,
10837
10924
  ...r.summary ? { summary: r.summary } : {},
10838
10925
  ...r.tags ? { tags: r.tags } : {},
10839
10926
  note: `imported from ${r.file}`
@@ -11274,27 +11361,90 @@ async function editRunbook(ctx, slug, deps = {}) {
11274
11361
  const found = page.records[0];
11275
11362
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11276
11363
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11277
- return editText(found.body, slug, deps);
11364
+ const body = await editText(found.body, slug, deps);
11365
+ return body === null ? null : { body, expectedVersion: found.version };
11278
11366
  }
11279
11367
 
11280
11368
  // src/whoami-command.ts
11369
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11370
+ function principalKind(value2, machine) {
11371
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11372
+ }
11373
+ function credentialKind(value2, machine, scopes) {
11374
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11375
+ if (machine) return "machine";
11376
+ if (scopes.length) return "device";
11377
+ return "unknown";
11378
+ }
11379
+ function managerOf(value2) {
11380
+ if (!value2 || typeof value2 !== "object") return null;
11381
+ const row = value2;
11382
+ const principalId = text(row.principalId);
11383
+ if (!principalId) return null;
11384
+ return {
11385
+ principalId,
11386
+ displayName: text(row.displayName) ?? "Unnamed member",
11387
+ handle: text(row.handle) ?? ""
11388
+ };
11389
+ }
11390
+ function unnamedPrincipal(kind) {
11391
+ if (kind === "agent") return "Unnamed agent";
11392
+ if (kind === "service") return "Unnamed service";
11393
+ return "Unnamed member";
11394
+ }
11281
11395
  async function fetchIdentity(platformUrl, token, doFetch) {
11282
11396
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11283
11397
  headers: { authorization: `Bearer ${token}` }
11284
11398
  });
11285
11399
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11286
11400
  const body = await res.json();
11401
+ const developerId = text(body.developerId) ?? "";
11402
+ const machine = body.machine === true;
11403
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11404
+ const principalId = text(body.principalId) ?? developerId;
11405
+ const email = text(body.email);
11406
+ const kind = principalKind(body.principalKind, machine);
11407
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11408
+ const handle = text(body.handle) ?? "";
11409
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11287
11410
  return {
11288
- developerId: String(body.developerId ?? ""),
11289
- email: body.email ?? null,
11411
+ developerId,
11412
+ principalId,
11413
+ principalKind: kind,
11414
+ displayName,
11415
+ handle,
11416
+ manager: managerOf(body.manager),
11417
+ credential: {
11418
+ id: text(credential2.id),
11419
+ kind: credentialKind(credential2.kind, machine, scopes)
11420
+ },
11421
+ email,
11290
11422
  admin: body.admin === true,
11291
- machine: body.machine === true,
11292
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11423
+ machine,
11424
+ scopes
11293
11425
  };
11294
11426
  }
11295
- function credentialKind(identity) {
11296
- if (identity.machine) return "machine (platform admin secret)";
11297
- return identity.scopes.length ? "device token (scoped)" : "device token or session";
11427
+ function credentialLabel(identity) {
11428
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11429
+ if (identity.credential.kind === "device")
11430
+ return identity.scopes.length ? "device (scoped)" : "device";
11431
+ if (identity.credential.kind === "clerk") return "clerk";
11432
+ return "unknown (legacy server)";
11433
+ }
11434
+ function namedPrincipal(identity) {
11435
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11436
+ }
11437
+ function namedManager(manager) {
11438
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11439
+ }
11440
+ function accountableOwner(identity) {
11441
+ if (identity.principalKind === "agent" && identity.manager) {
11442
+ return namedManager(identity.manager);
11443
+ }
11444
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11445
+ return namedPrincipal(identity);
11446
+ }
11447
+ return identity.email ?? "Unnamed member";
11298
11448
  }
11299
11449
  async function whoamiCommand(parsed, deps = {}) {
11300
11450
  assertArgs(
@@ -11325,9 +11475,19 @@ async function whoamiCommand(parsed, deps = {}) {
11325
11475
  return;
11326
11476
  }
11327
11477
  out.log(`platform: ${cfg.platformUrl}`);
11328
- out.log(`developer: ${identity.developerId}`);
11478
+ out.log(`principal: ${namedPrincipal(identity)}`);
11479
+ out.log(`principal id: ${identity.principalId}`);
11480
+ out.log(`kind: ${identity.principalKind}`);
11481
+ if (identity.principalKind === "agent")
11482
+ out.log(
11483
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11484
+ );
11485
+ out.log(`owner: ${accountableOwner(identity)}`);
11486
+ out.log(`owner id: ${identity.developerId}`);
11329
11487
  out.log(`email: ${identity.email ?? "(none)"}`);
11330
- out.log(`credential: ${credentialKind(identity)}`);
11488
+ out.log(`credential: ${credentialLabel(identity)}`);
11489
+ if (identity.credential.id)
11490
+ out.log(`credential id: ${identity.credential.id}`);
11331
11491
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11332
11492
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11333
11493
  if (!identity.admin) {
@@ -11476,14 +11636,16 @@ async function runbookCommand(parsed, deps = {}) {
11476
11636
  const name = requireSlug(slug, "edit");
11477
11637
  const file = stringOpt(parsed.options.file);
11478
11638
  const inline = stringOpt(parsed.options.body);
11479
- const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11480
- if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
11639
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11640
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
11641
+ const body = typeof edited === "string" ? edited : edited.body;
11481
11642
  return runbookEdit(
11482
11643
  ctx,
11483
11644
  name,
11484
11645
  body,
11485
11646
  stringOpt(parsed.options.note),
11486
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
11647
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
11648
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11487
11649
  );
11488
11650
  }
11489
11651
  case "import": {