@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.
@@ -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
  }
@@ -538,6 +538,14 @@ async function readSecretStream(kind, stream = process6.stdin) {
538
538
  return value2;
539
539
  }
540
540
 
541
+ // src/principal-presentation.ts
542
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
543
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
544
+ const id = typeof principalId === "string" ? principalId.trim() : "";
545
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
546
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
547
+ }
548
+
541
549
  // src/admin-ai-audit.ts
542
550
  function adminAiAuditQuery(filters) {
543
551
  if (filters.limit === void 0) return "";
@@ -567,17 +575,17 @@ async function readAdminAiAudit(request2) {
567
575
  String(event.changeKind ?? ""),
568
576
  String(event.purpose ?? event.provider ?? ""),
569
577
  route2,
570
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
578
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
571
579
  ].join(" "));
572
580
  }
573
581
  }
574
582
  async function responseBody(response2) {
575
- const text = await response2.text();
576
- if (!text) return {};
583
+ const text2 = await response2.text();
584
+ if (!text2) return {};
577
585
  try {
578
- return JSON.parse(text);
586
+ return JSON.parse(text2);
579
587
  } catch {
580
- return { message: text.slice(0, 300) };
588
+ return { message: text2.slice(0, 300) };
581
589
  }
582
590
  }
583
591
  function apiError(status, body) {
@@ -644,7 +652,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
644
652
  timestamp2(event.created_at),
645
653
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
646
654
  String(event.run_id ?? ""),
647
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
655
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
648
656
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
649
657
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
650
658
  String(input + output),
@@ -665,12 +673,12 @@ function timestamp2(value2) {
665
673
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
666
674
  }
667
675
  async function responseBody2(res) {
668
- const text = await res.text();
669
- if (!text) return {};
676
+ const text2 = await res.text();
677
+ if (!text2) return {};
670
678
  try {
671
- return JSON.parse(text);
679
+ return JSON.parse(text2);
672
680
  } catch {
673
- return { message: text.slice(0, 300) };
681
+ return { message: text2.slice(0, 300) };
674
682
  }
675
683
  }
676
684
  function apiError2(action2, status, body) {
@@ -846,12 +854,12 @@ function catalogModels(body) {
846
854
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
847
855
  }
848
856
  async function responseBody3(res) {
849
- const text = await res.text();
850
- if (!text) return {};
857
+ const text2 = await res.text();
858
+ if (!text2) return {};
851
859
  try {
852
- return JSON.parse(text);
860
+ return JSON.parse(text2);
853
861
  } catch {
854
- return { message: text.slice(0, 300) };
862
+ return { message: text2.slice(0, 300) };
855
863
  }
856
864
  }
857
865
  function apiError3(action2, status, body) {
@@ -2414,7 +2422,7 @@ function compareOptionalEnvironmentState(env, desired, observed, desiredSource,
2414
2422
  }
2415
2423
 
2416
2424
  // src/config-reconcile-command.ts
2417
- import { createAppsClient as createAppsClient2 } from "@odla-ai/apps";
2425
+ import { createAppsClient as createAppsClient2, studioAppSettingsPath } from "@odla-ai/apps";
2418
2426
  import { join as join4 } from "path";
2419
2427
  async function configDiff(options) {
2420
2428
  const reconciliation = await inspectConfig(options);
@@ -2561,22 +2569,26 @@ function planNextActions(reconciliation, configPath) {
2561
2569
  }
2562
2570
  }
2563
2571
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
2564
- const { appId, platformUrl } = reconciliation.scope;
2565
2572
  next.push({
2566
2573
  code: "review_destructive",
2567
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
2574
+ command: studioSettingsUrl(reconciliation),
2568
2575
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
2569
2576
  });
2570
2577
  }
2571
2578
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
2572
2579
  next.push({
2573
2580
  code: "declare_or_accept_runtime_state",
2574
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
2581
+ command: studioSettingsUrl(reconciliation),
2575
2582
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
2576
2583
  });
2577
2584
  }
2578
2585
  return next;
2579
2586
  }
2587
+ function studioSettingsUrl(reconciliation) {
2588
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
2589
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
2590
+ return `${origin}${studioAppSettingsPath(reconciliation.scope.appId, environment2, "environment")}`;
2591
+ }
2580
2592
  function quoteArg2(value2) {
2581
2593
  return `'${value2.replace(/'/g, `'\\''`)}'`;
2582
2594
  }
@@ -2616,15 +2628,15 @@ function readWranglerConfig(path) {
2616
2628
  return null;
2617
2629
  }
2618
2630
  }
2619
- function stripJsonComments(text) {
2631
+ function stripJsonComments(text2) {
2620
2632
  let result = "";
2621
2633
  let inString = false;
2622
- for (let i = 0; i < text.length; i++) {
2623
- const ch = text[i];
2634
+ for (let i = 0; i < text2.length; i++) {
2635
+ const ch = text2[i];
2624
2636
  if (inString) {
2625
2637
  result += ch;
2626
2638
  if (ch === "\\") {
2627
- result += text[i + 1] ?? "";
2639
+ result += text2[i + 1] ?? "";
2628
2640
  i++;
2629
2641
  } else if (ch === '"') {
2630
2642
  inString = false;
@@ -2636,14 +2648,14 @@ function stripJsonComments(text) {
2636
2648
  result += ch;
2637
2649
  continue;
2638
2650
  }
2639
- if (ch === "/" && text[i + 1] === "/") {
2640
- while (i < text.length && text[i] !== "\n") i++;
2651
+ if (ch === "/" && text2[i + 1] === "/") {
2652
+ while (i < text2.length && text2[i] !== "\n") i++;
2641
2653
  result += "\n";
2642
2654
  continue;
2643
2655
  }
2644
- if (ch === "/" && text[i + 1] === "*") {
2656
+ if (ch === "/" && text2[i + 1] === "*") {
2645
2657
  i += 2;
2646
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
2658
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
2647
2659
  i++;
2648
2660
  continue;
2649
2661
  }
@@ -2660,7 +2672,14 @@ async function wranglerLoggedIn(run, cwd) {
2660
2672
  }
2661
2673
  }
2662
2674
  function wranglerPutSecret(run, opts) {
2663
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
2675
+ const args = [
2676
+ "wrangler",
2677
+ "secret",
2678
+ "put",
2679
+ opts.name,
2680
+ ...opts.env ? ["--env", opts.env] : [],
2681
+ ...opts.configPath ? ["--config", opts.configPath] : []
2682
+ ];
2664
2683
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
2665
2684
  }
2666
2685
 
@@ -3019,9 +3038,9 @@ function initProject(options) {
3019
3038
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
3020
3039
  out.log("updated .gitignore for local odla credentials");
3021
3040
  }
3022
- function writeIfMissing(path, text) {
3041
+ function writeIfMissing(path, text2) {
3023
3042
  if (existsSync6(path)) return;
3024
- writeFileSync2(path, text);
3043
+ writeFileSync2(path, text2);
3025
3044
  }
3026
3045
  function configTemplate(input) {
3027
3046
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -3229,13 +3248,13 @@ async function secretsSetClerkKey(options) {
3229
3248
  body: JSON.stringify({ value: value2 })
3230
3249
  });
3231
3250
  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"}`);
3251
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
3252
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
3234
3253
  }
3235
3254
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
3236
3255
  }
3237
- function scrubValue(text, value2) {
3238
- return redactSecrets(text).split(value2).join("[value redacted]");
3256
+ function scrubValue(text2, value2) {
3257
+ return redactSecrets(text2).split(value2).join("[value redacted]");
3239
3258
  }
3240
3259
  async function resolveVaultWrite(options) {
3241
3260
  const out = options.stdout ?? console;
@@ -4218,8 +4237,8 @@ async function runContainerAttempt(options) {
4218
4237
  let stopped = false;
4219
4238
  let exited = false;
4220
4239
  child.stderr.setEncoding("utf8");
4221
- child.stderr.on("data", (text) => {
4222
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
4240
+ child.stderr.on("data", (text2) => {
4241
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
4223
4242
  });
4224
4243
  const stop = (reason) => {
4225
4244
  if (stopped || exited) return;
@@ -5051,8 +5070,8 @@ function boundedInteger(value2, spec) {
5051
5070
  }
5052
5071
  function boundedNumber(value2, spec) {
5053
5072
  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.");
5073
+ const text2 = String(value2);
5074
+ 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
5075
  return value2;
5057
5076
  }
5058
5077
  function enumMember(value2, spec) {
@@ -5208,8 +5227,8 @@ function validateUnsafeSelector(path, value2, tool) {
5208
5227
  return void 0;
5209
5228
  }
5210
5229
  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);
5230
+ const text2 = value2.trim();
5231
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
5213
5232
  }
5214
5233
 
5215
5234
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -5578,8 +5597,8 @@ function gitApply(cwd, patch2, check) {
5578
5597
  });
5579
5598
  let stderr = "";
5580
5599
  child.stderr.setEncoding("utf8");
5581
- child.stderr.on("data", (text) => {
5582
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
5600
+ child.stderr.on("data", (text2) => {
5601
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
5583
5602
  });
5584
5603
  child.once("error", reject);
5585
5604
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -6750,10 +6769,10 @@ var CodePiRuntimeEngine = class {
6750
6769
  task: lease.task,
6751
6770
  limits: this.options.limits,
6752
6771
  signal: active.abort.signal,
6753
- onStderr: (text) => this.#event(command, {
6772
+ onStderr: (text2) => this.#event(command, {
6754
6773
  type: "message",
6755
6774
  actor: "system",
6756
- body: text.slice(0, 4e3)
6775
+ body: text2.slice(0, 4e3)
6757
6776
  }, active.conversationRefs),
6758
6777
  onMessage: async (output) => {
6759
6778
  if (output.type === "inference.request") {
@@ -7516,7 +7535,9 @@ var COMMAND_SURFACE = {
7516
7535
  init: {},
7517
7536
  o11y: { status: {} },
7518
7537
  operations: { get: {}, wait: {} },
7519
- platform: { status: {} },
7538
+ platform: {
7539
+ status: {}
7540
+ },
7520
7541
  pm: {
7521
7542
  ...PM_ENTITIES,
7522
7543
  handoff: {}
@@ -8413,8 +8434,8 @@ async function appImport(options) {
8413
8434
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
8414
8435
  const doFetch = options.fetch ?? fetch;
8415
8436
  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);
8437
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => readFileSync9(0, "utf8")))() : readFileSync9(options.file, "utf8");
8438
+ const { format, sources } = parseImport(text2, options.ns);
8418
8439
  if (format === "namespace-map" && options.ns) {
8419
8440
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
8420
8441
  }
@@ -8505,7 +8526,10 @@ function report(options, owners, headline) {
8505
8526
  if (headline) out.log(headline);
8506
8527
  out.log(`owners (${owners.length}):`);
8507
8528
  for (const o of owners) {
8508
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
8529
+ const name = o.email?.trim() || "Unnamed member";
8530
+ out.log(
8531
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
8532
+ );
8509
8533
  }
8510
8534
  }
8511
8535
  async function ownersList(options) {
@@ -9251,7 +9275,8 @@ Usage:
9251
9275
  odla-ai doctor [--config odla.config.mjs]
9252
9276
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
9253
9277
  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]
9278
+ odla-ai operations get <operation-id> [--json]
9279
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
9255
9280
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
9256
9281
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
9257
9282
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -9365,9 +9390,9 @@ Commands:
9365
9390
  does and what it guarantees is JSDoc, rendered per package at
9366
9391
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
9367
9392
  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.
9393
+ whoami Report the actual principal name, handle and kind, its manager,
9394
+ credential kind, accountable owner, and platform-admin status.
9395
+ A manager relationship never supplies authority by itself.
9371
9396
  context Explain selected config, platform, app, environment, and
9372
9397
  developer-token provenance without printing credentials or
9373
9398
  starting a device handshake. Operator work can run outside a
@@ -9487,6 +9512,58 @@ Safety:
9487
9512
  `);
9488
9513
  }
9489
9514
 
9515
+ // src/discuss-principals.ts
9516
+ function mergeDiscussPrincipals(target, source) {
9517
+ Object.assign(target.authors, source.authors ?? {});
9518
+ Object.assign(target.principals, source.principals ?? {});
9519
+ }
9520
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
9521
+ function discussPrincipalLabel(principalId, authorKind, projection) {
9522
+ const profile = projection.principals?.[principalId];
9523
+ if (profile) {
9524
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
9525
+ if (profile.kind === "agent") {
9526
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
9527
+ }
9528
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
9529
+ }
9530
+ const legacyName = projection.authors?.[principalId]?.trim();
9531
+ if (legacyName) {
9532
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
9533
+ }
9534
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
9535
+ }
9536
+
9537
+ // src/discuss-read-render.ts
9538
+ function discussBodyWithRefs(post) {
9539
+ if (!post.refs || post.refs.length === 0) return post.body;
9540
+ let out = "";
9541
+ let cursor = 0;
9542
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
9543
+ if (ref.start < cursor || ref.end > post.body.length) continue;
9544
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
9545
+ cursor = ref.end;
9546
+ }
9547
+ return out + post.body.slice(cursor);
9548
+ }
9549
+ function renderDiscussRead(ctx, topic, posts, projection) {
9550
+ const status = topic.resolved ? "resolved" : "open";
9551
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
9552
+ for (const post of posts) {
9553
+ const who = discussPrincipalLabel(
9554
+ post.authorId,
9555
+ post.authorKind,
9556
+ projection
9557
+ );
9558
+ ctx.out.log(`
9559
+ \u2014 ${who}`);
9560
+ ctx.out.log(discussBodyWithRefs(post));
9561
+ for (const file of post.attachments ?? []) {
9562
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
9563
+ }
9564
+ }
9565
+ }
9566
+
9490
9567
  // src/discuss-actions.ts
9491
9568
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
9492
9569
  async function request(ctx, method, path, body) {
@@ -9505,17 +9582,6 @@ function emit(ctx, value2, human) {
9505
9582
  else human();
9506
9583
  }
9507
9584
  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
9585
  function content(parsed) {
9520
9586
  const markup = stringOpt(parsed.options.markup);
9521
9587
  if (markup) return { markup };
@@ -9570,11 +9636,19 @@ async function discussRead(ctx, id, parsed) {
9570
9636
  offset: requestedOffset ?? "0"
9571
9637
  });
9572
9638
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
9573
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
9639
+ emit(
9640
+ ctx,
9641
+ page,
9642
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
9643
+ );
9574
9644
  return;
9575
9645
  }
9576
9646
  for (let scan = 0; scan < 3; scan++) {
9577
9647
  const posts = /* @__PURE__ */ new Map();
9648
+ const projection = {
9649
+ authors: {},
9650
+ principals: {}
9651
+ };
9578
9652
  let topic = null;
9579
9653
  let offset = 0;
9580
9654
  for (; ; ) {
@@ -9585,6 +9659,7 @@ async function discussRead(ctx, id, parsed) {
9585
9659
  );
9586
9660
  topic = page.topic;
9587
9661
  for (const post of page.posts) posts.set(post.id, post);
9662
+ mergeDiscussPrincipals(projection, page);
9588
9663
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
9589
9664
  if (!page.page?.hasMore) break;
9590
9665
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -9597,24 +9672,18 @@ async function discussRead(ctx, id, parsed) {
9597
9672
  );
9598
9673
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
9599
9674
  if (ordered.length === expected) {
9600
- const result = { topic, posts: ordered };
9601
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
9675
+ const result = { topic, posts: ordered, ...projection };
9676
+ emit(
9677
+ ctx,
9678
+ result,
9679
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
9680
+ );
9602
9681
  return;
9603
9682
  }
9604
9683
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
9605
9684
  }
9606
9685
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
9607
9686
  }
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
9687
  async function discussPost(ctx, parsed) {
9619
9688
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
9620
9689
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -9839,6 +9908,8 @@ async function discussWatch(ctx, topicId, parsed) {
9839
9908
  found: true,
9840
9909
  cursor,
9841
9910
  events: matching,
9911
+ ...page.authors ? { authors: page.authors } : {},
9912
+ ...page.principals ? { principals: page.principals } : {},
9842
9913
  ...posts && posts.length > 0 ? { posts } : {},
9843
9914
  ...topics && topics.length > 0 ? { topics } : {}
9844
9915
  });
@@ -9871,7 +9942,11 @@ function report2(ctx, parsed, result) {
9871
9942
  ctx.out.log(JSON.stringify(result, null, 2));
9872
9943
  } else if (parsed.options.jsonl !== true && result.found) {
9873
9944
  for (const post of result.posts ?? []) {
9874
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9945
+ const who = discussPrincipalLabel(
9946
+ post.authorId,
9947
+ post.authorKind,
9948
+ result
9949
+ );
9875
9950
  ctx.out.log(`\u2014 ${who}
9876
9951
  ${post.body}`);
9877
9952
  }
@@ -10016,8 +10091,8 @@ function collectFields(parsed, allowClear) {
10016
10091
  if (allowClear) out[spec.key] = null;
10017
10092
  continue;
10018
10093
  }
10019
- const text = stringOpt(value2);
10020
- out[spec.key] = spec.num ? Number(text) : text;
10094
+ const text2 = stringOpt(value2);
10095
+ out[spec.key] = spec.num ? Number(text2) : text2;
10021
10096
  }
10022
10097
  return out;
10023
10098
  }
@@ -10347,17 +10422,23 @@ function age(input) {
10347
10422
 
10348
10423
  // src/platform-command.ts
10349
10424
  async function platformCommand(parsed, deps = {}) {
10425
+ const action2 = parsed.positionals[1];
10426
+ if (action2 === "status") {
10427
+ return platformStatus(parsed, deps);
10428
+ }
10429
+ throw new Error(
10430
+ `unknown platform action "${[
10431
+ action2,
10432
+ parsed.positionals[2]
10433
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
10434
+ );
10435
+ }
10436
+ async function platformStatus(parsed, deps) {
10350
10437
  assertArgs(
10351
10438
  parsed,
10352
10439
  ["config", "context", "platform", "token", "email", "open", "json"],
10353
10440
  2
10354
10441
  );
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
10442
  const context = await resolveOperatorContext(parsed, {
10362
10443
  allowMissingConfig: true
10363
10444
  });
@@ -10802,14 +10883,14 @@ function statusMinutes(value2) {
10802
10883
  }
10803
10884
  async function read2(url, headers, doFetch) {
10804
10885
  const response2 = await doFetch(url, { headers });
10805
- const text = await response2.text();
10886
+ const text2 = await response2.text();
10806
10887
  let body = {};
10807
- if (text) {
10888
+ if (text2) {
10808
10889
  try {
10809
- const value2 = JSON.parse(text);
10890
+ const value2 = JSON.parse(text2);
10810
10891
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
10811
10892
  } catch {
10812
- body = { message: text.slice(0, 300) };
10893
+ body = { message: text2.slice(0, 300) };
10813
10894
  }
10814
10895
  }
10815
10896
  return { httpStatus: response2.status, body };
@@ -10949,12 +11030,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10949
11030
  });
10950
11031
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10951
11032
  }
10952
- async function runbookEdit(ctx, slug, body, note, requires) {
11033
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10953
11034
  const runbook = await bySlug(ctx, slug);
10954
11035
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10955
11036
  // An empty --requires clears the declaration; omitting the flag leaves
10956
11037
  // whatever is there, so an ordinary body edit never drops it.
10957
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
11038
+ patch: {
11039
+ body,
11040
+ expectedVersion: expectedVersion ?? runbook.version,
11041
+ ...note ? { note } : {},
11042
+ ...requires === void 0 ? {} : { requires: requires || null }
11043
+ }
10958
11044
  });
10959
11045
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10960
11046
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -11008,12 +11094,12 @@ async function runbookRemove(ctx, slug) {
11008
11094
  // src/runbook-import.ts
11009
11095
  import { readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
11010
11096
  import { basename as basename2, join as join13 } from "path";
11011
- function parseRunbook(text, slug) {
11012
- let rest = text;
11097
+ function parseRunbook(text2, slug) {
11098
+ let rest = text2;
11013
11099
  const meta = {};
11014
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
11100
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
11015
11101
  if (fm) {
11016
- rest = text.slice(fm[0].length);
11102
+ rest = text2.slice(fm[0].length);
11017
11103
  for (const line of fm[1].split(/\r?\n/)) {
11018
11104
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
11019
11105
  if (!pair) continue;
@@ -11094,6 +11180,7 @@ async function upsert(ctx, r, visibility) {
11094
11180
  patch: {
11095
11181
  title: r.title,
11096
11182
  body: r.body,
11183
+ expectedVersion: found.version,
11097
11184
  ...r.summary ? { summary: r.summary } : {},
11098
11185
  ...r.tags ? { tags: r.tags } : {},
11099
11186
  note: `imported from ${r.file}`
@@ -11534,27 +11621,90 @@ async function editRunbook(ctx, slug, deps = {}) {
11534
11621
  const found = page.records[0];
11535
11622
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
11536
11623
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
11537
- return editText(found.body, slug, deps);
11624
+ const body = await editText(found.body, slug, deps);
11625
+ return body === null ? null : { body, expectedVersion: found.version };
11538
11626
  }
11539
11627
 
11540
11628
  // src/whoami-command.ts
11629
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11630
+ function principalKind(value2, machine) {
11631
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11632
+ }
11633
+ function credentialKind(value2, machine, scopes) {
11634
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11635
+ if (machine) return "machine";
11636
+ if (scopes.length) return "device";
11637
+ return "unknown";
11638
+ }
11639
+ function managerOf(value2) {
11640
+ if (!value2 || typeof value2 !== "object") return null;
11641
+ const row = value2;
11642
+ const principalId = text(row.principalId);
11643
+ if (!principalId) return null;
11644
+ return {
11645
+ principalId,
11646
+ displayName: text(row.displayName) ?? "Unnamed member",
11647
+ handle: text(row.handle) ?? ""
11648
+ };
11649
+ }
11650
+ function unnamedPrincipal(kind) {
11651
+ if (kind === "agent") return "Unnamed agent";
11652
+ if (kind === "service") return "Unnamed service";
11653
+ return "Unnamed member";
11654
+ }
11541
11655
  async function fetchIdentity(platformUrl, token, doFetch) {
11542
11656
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11543
11657
  headers: { authorization: `Bearer ${token}` }
11544
11658
  });
11545
11659
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11546
11660
  const body = await res.json();
11661
+ const developerId = text(body.developerId) ?? "";
11662
+ const machine = body.machine === true;
11663
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11664
+ const principalId = text(body.principalId) ?? developerId;
11665
+ const email = text(body.email);
11666
+ const kind = principalKind(body.principalKind, machine);
11667
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11668
+ const handle = text(body.handle) ?? "";
11669
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11547
11670
  return {
11548
- developerId: String(body.developerId ?? ""),
11549
- email: body.email ?? null,
11671
+ developerId,
11672
+ principalId,
11673
+ principalKind: kind,
11674
+ displayName,
11675
+ handle,
11676
+ manager: managerOf(body.manager),
11677
+ credential: {
11678
+ id: text(credential2.id),
11679
+ kind: credentialKind(credential2.kind, machine, scopes)
11680
+ },
11681
+ email,
11550
11682
  admin: body.admin === true,
11551
- machine: body.machine === true,
11552
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11683
+ machine,
11684
+ scopes
11553
11685
  };
11554
11686
  }
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";
11687
+ function credentialLabel(identity) {
11688
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11689
+ if (identity.credential.kind === "device")
11690
+ return identity.scopes.length ? "device (scoped)" : "device";
11691
+ if (identity.credential.kind === "clerk") return "clerk";
11692
+ return "unknown (legacy server)";
11693
+ }
11694
+ function namedPrincipal(identity) {
11695
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11696
+ }
11697
+ function namedManager(manager) {
11698
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11699
+ }
11700
+ function accountableOwner(identity) {
11701
+ if (identity.principalKind === "agent" && identity.manager) {
11702
+ return namedManager(identity.manager);
11703
+ }
11704
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11705
+ return namedPrincipal(identity);
11706
+ }
11707
+ return identity.email ?? "Unnamed member";
11558
11708
  }
11559
11709
  async function whoamiCommand(parsed, deps = {}) {
11560
11710
  assertArgs(
@@ -11585,9 +11735,19 @@ async function whoamiCommand(parsed, deps = {}) {
11585
11735
  return;
11586
11736
  }
11587
11737
  out.log(`platform: ${cfg.platformUrl}`);
11588
- out.log(`developer: ${identity.developerId}`);
11738
+ out.log(`principal: ${namedPrincipal(identity)}`);
11739
+ out.log(`principal id: ${identity.principalId}`);
11740
+ out.log(`kind: ${identity.principalKind}`);
11741
+ if (identity.principalKind === "agent")
11742
+ out.log(
11743
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11744
+ );
11745
+ out.log(`owner: ${accountableOwner(identity)}`);
11746
+ out.log(`owner id: ${identity.developerId}`);
11589
11747
  out.log(`email: ${identity.email ?? "(none)"}`);
11590
- out.log(`credential: ${credentialKind(identity)}`);
11748
+ out.log(`credential: ${credentialLabel(identity)}`);
11749
+ if (identity.credential.id)
11750
+ out.log(`credential id: ${identity.credential.id}`);
11591
11751
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11592
11752
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11593
11753
  if (!identity.admin) {
@@ -11736,14 +11896,16 @@ async function runbookCommand(parsed, deps = {}) {
11736
11896
  const name = requireSlug(slug, "edit");
11737
11897
  const file = stringOpt(parsed.options.file);
11738
11898
  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`);
11899
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11900
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
11901
+ const body = typeof edited === "string" ? edited : edited.body;
11741
11902
  return runbookEdit(
11742
11903
  ctx,
11743
11904
  name,
11744
11905
  body,
11745
11906
  stringOpt(parsed.options.note),
11746
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
11907
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
11908
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11747
11909
  );
11748
11910
  }
11749
11911
  case "import": {
@@ -12414,4 +12576,4 @@ export {
12414
12576
  exitCodeFor,
12415
12577
  runCli
12416
12578
  };
12417
- //# sourceMappingURL=chunk-JJ42NV7X.js.map
12579
+ //# sourceMappingURL=chunk-MBZXWPZP.js.map