@odla-ai/cli 0.27.17 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -174,7 +174,7 @@ function absoluteEntryPath(entryPath) {
174
174
 
175
175
  // src/bin.ts
176
176
  var argv = process.argv.slice(2);
177
- requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-DCSVQAZ6.js")).runCli()).catch((err) => {
177
+ requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-2WZIFXA6.js")).runCli()).catch((err) => {
178
178
  console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
179
179
  process.exitCode = exitCodeFor(err);
180
180
  });
@@ -304,7 +304,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
304
304
  const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
305
305
  const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
306
306
  const cached = readJsonFile(cfg.local.tokenFile);
307
- if (!grantRequest.forceReview) {
307
+ if (!grantRequest.forceReview && !grantRequest.freshLogin) {
308
308
  if (options.token) return options.token;
309
309
  if (process5.env.ODLA_DEV_TOKEN) {
310
310
  const declared = process5.env.ODLA_DEV_TOKEN_AUDIENCE;
@@ -321,9 +321,9 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
321
321
  }
322
322
  } else {
323
323
  if (options.token) {
324
- throw new Error("--request-grant cannot be combined with --token; remove --token so the approved replacement credential can be collected and cached");
324
+ throw new Error(grantRequest.forceReview ? "--request-grant cannot be combined with --token; remove --token so the approved replacement credential can be collected and cached" : "a fresh authorization cannot be combined with --token; remove --token so the approved replacement credential can be collected and cached");
325
325
  }
326
- out.error(`auth: requesting fresh owner review for app.manage on exact project "${cfg.app.id}"`);
326
+ out.error(grantRequest.forceReview ? `auth: requesting fresh owner review for app.manage on exact project "${cfg.app.id}"` : `auth: requesting a fresh agent sign-in for exact project "${cfg.app.id}"`);
327
327
  }
328
328
  const ctx = {
329
329
  cfg,
@@ -336,6 +336,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
336
336
  grantIntent
337
337
  };
338
338
  const waitMs = handshakeWaitMs(options.wait);
339
+ if (grantRequest.freshLogin) clearPendingHandshake(ctx.pendingFile);
339
340
  const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
340
341
  clearPendingHandshake(ctx.pendingFile);
341
342
  writePrivateJson(cfg.local.tokenFile, {
@@ -393,11 +394,12 @@ async function resumePendingHandshake(ctx, waitMs) {
393
394
  async function freshHandshake(ctx, waitMs) {
394
395
  let started;
395
396
  let stopReminder;
397
+ const provisioning = ctx.grantIntent.optionalProjectCapabilities.includes("app.manage");
396
398
  try {
397
399
  return await requestToken({
398
400
  endpoint: ctx.cfg.platformUrl,
399
401
  email: ctx.email,
400
- label: `${ctx.cfg.app.id} provisioner`,
402
+ label: `${ctx.cfg.app.id} ${provisioning ? "provisioner" : "agent"}`,
401
403
  agentHandle: projectAgentHandle(ctx.cfg.app.id),
402
404
  projectIds: [ctx.cfg.app.id],
403
405
  optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities,
@@ -458,8 +460,15 @@ function stillPending(pending, email) {
458
460
  }
459
461
  function handshakeEmail(value2, cached) {
460
462
  const email = (value2 ?? process5.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
463
+ if (/@users\.noreply\.github\.com$/i.test(email)) {
464
+ throw new Error(
465
+ `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
466
+ );
467
+ }
461
468
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
462
- throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
469
+ throw new Error(
470
+ "a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL; use the signed-in email shown in odla Studio, not git or GitHub identity"
471
+ );
463
472
  }
464
473
  return email;
465
474
  }
@@ -1627,6 +1636,204 @@ async function adminCommand(parsed, deps = {}) {
1627
1636
  });
1628
1637
  }
1629
1638
 
1639
+ // src/auth-command.ts
1640
+ import process11 from "process";
1641
+
1642
+ // src/whoami-command.ts
1643
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
1644
+ function principalKind(value2, machine) {
1645
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
1646
+ }
1647
+ function credentialKind(value2, machine, scopes) {
1648
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
1649
+ if (machine) return "machine";
1650
+ if (scopes.length) return "device";
1651
+ return "unknown";
1652
+ }
1653
+ function managerOf(value2) {
1654
+ if (!value2 || typeof value2 !== "object") return null;
1655
+ const row = value2;
1656
+ const principalId = text(row.principalId);
1657
+ if (!principalId) return null;
1658
+ return {
1659
+ principalId,
1660
+ displayName: text(row.displayName) ?? "Unnamed member",
1661
+ handle: text(row.handle) ?? ""
1662
+ };
1663
+ }
1664
+ function unnamedPrincipal(kind) {
1665
+ if (kind === "agent") return "Unnamed agent";
1666
+ if (kind === "service") return "Unnamed service";
1667
+ return "Unnamed member";
1668
+ }
1669
+ async function fetchIdentity(platformUrl, token, doFetch) {
1670
+ const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
1671
+ headers: { authorization: `Bearer ${token}` }
1672
+ });
1673
+ if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
1674
+ const body = await res.json();
1675
+ const developerId = text(body.developerId) ?? "";
1676
+ const machine = body.machine === true;
1677
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
1678
+ const principalId = text(body.principalId) ?? developerId;
1679
+ const email = text(body.email);
1680
+ const kind = principalKind(body.principalKind, machine);
1681
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
1682
+ const handle = text(body.handle) ?? "";
1683
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
1684
+ return {
1685
+ developerId,
1686
+ principalId,
1687
+ principalKind: kind,
1688
+ displayName,
1689
+ handle,
1690
+ manager: managerOf(body.manager),
1691
+ credential: {
1692
+ id: text(credential2.id),
1693
+ kind: credentialKind(credential2.kind, machine, scopes)
1694
+ },
1695
+ email,
1696
+ admin: body.admin === true,
1697
+ machine,
1698
+ scopes
1699
+ };
1700
+ }
1701
+ function credentialLabel(identity) {
1702
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
1703
+ if (identity.credential.kind === "device")
1704
+ return identity.scopes.length ? "device (scoped)" : "device";
1705
+ if (identity.credential.kind === "clerk") return "clerk";
1706
+ return "unknown (legacy server)";
1707
+ }
1708
+ function namedPrincipal(identity) {
1709
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
1710
+ }
1711
+ function namedManager(manager) {
1712
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
1713
+ }
1714
+ function accountableOwner(identity) {
1715
+ if (identity.principalKind === "agent" && identity.manager) {
1716
+ return namedManager(identity.manager);
1717
+ }
1718
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
1719
+ return namedPrincipal(identity);
1720
+ }
1721
+ return identity.email ?? "Unnamed member";
1722
+ }
1723
+ async function whoamiCommand(parsed, deps = {}) {
1724
+ assertArgs(
1725
+ parsed,
1726
+ ["config", "context", "platform", "token", "email", "json", "open"],
1727
+ 1
1728
+ );
1729
+ const out = deps.stdout ?? console;
1730
+ const doFetch = deps.fetch ?? fetch;
1731
+ const { cfg } = await resolveOperatorContext(parsed, {
1732
+ allowMissingConfig: true
1733
+ });
1734
+ const token = await getDeveloperToken(
1735
+ cfg,
1736
+ {
1737
+ configPath: cfg.configPath,
1738
+ token: stringOpt(parsed.options.token),
1739
+ email: stringOpt(parsed.options.email),
1740
+ // As above: never suppress the browser for an ordinary sign-in.
1741
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1742
+ openApprovalUrl: deps.openUrl
1743
+ },
1744
+ doFetch,
1745
+ out
1746
+ );
1747
+ const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
1748
+ if (parsed.options.json === true) {
1749
+ out.log(JSON.stringify(identity, null, 2));
1750
+ return;
1751
+ }
1752
+ out.log(`platform: ${cfg.platformUrl}`);
1753
+ out.log(`principal: ${namedPrincipal(identity)}`);
1754
+ out.log(`principal id: ${identity.principalId}`);
1755
+ out.log(`kind: ${identity.principalKind}`);
1756
+ if (identity.principalKind === "agent")
1757
+ out.log(
1758
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
1759
+ );
1760
+ out.log(`owner: ${accountableOwner(identity)}`);
1761
+ out.log(`owner id: ${identity.developerId}`);
1762
+ out.log(`email: ${identity.email ?? "(none)"}`);
1763
+ out.log(`credential: ${credentialLabel(identity)}`);
1764
+ if (identity.credential.id)
1765
+ out.log(`credential id: ${identity.credential.id}`);
1766
+ out.log(`admin: ${identity.admin ? "yes" : "no"}`);
1767
+ if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
1768
+ if (!identity.admin) {
1769
+ if (identity.scopes.includes("platform:runbook:write")) {
1770
+ out.log("\nThis exact scope can read and edit all platform runbook content.");
1771
+ out.log("The device token is not ambient admin and cannot change visibility.");
1772
+ } else {
1773
+ out.log("\nYou can read operator-visible platform runbooks but not edit them.");
1774
+ out.log("Use a platform-admin-approved runbook write request to edit.");
1775
+ }
1776
+ }
1777
+ }
1778
+
1779
+ // src/auth-command.ts
1780
+ async function authCommand(parsed, deps = {}) {
1781
+ assertArgs(parsed, [
1782
+ "config",
1783
+ "context",
1784
+ "platform",
1785
+ "app",
1786
+ "email",
1787
+ "open",
1788
+ "wait",
1789
+ "json"
1790
+ ], 2);
1791
+ const action2 = parsed.positionals[1] ?? "login";
1792
+ if (action2 !== "login") {
1793
+ throw new Error(`unknown auth action "${action2}". Try "odla-ai auth login --app <id> --email <odla-account>".`);
1794
+ }
1795
+ const context = await resolveOperatorContext(parsed, {
1796
+ allowMissingConfig: true,
1797
+ requireApp: true
1798
+ });
1799
+ const { cfg } = context;
1800
+ const out = deps.stdout ?? console;
1801
+ const doFetch = deps.fetch ?? fetch;
1802
+ const email = stringOpt(parsed.options.email) ?? process11.env.ODLA_USER_EMAIL?.trim();
1803
+ if (!email) {
1804
+ throw new Error(
1805
+ "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
1806
+ );
1807
+ }
1808
+ const token = await getDeveloperToken(
1809
+ cfg,
1810
+ {
1811
+ configPath: cfg.configPath,
1812
+ email,
1813
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1814
+ wait: numberOpt(parsed.options.wait, "--wait"),
1815
+ openApprovalUrl: deps.openUrl
1816
+ },
1817
+ doFetch,
1818
+ out,
1819
+ { freshLogin: true }
1820
+ );
1821
+ const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
1822
+ if (parsed.options.json === true) {
1823
+ out.log(JSON.stringify({
1824
+ principalId: identity.principalId,
1825
+ displayName: identity.displayName,
1826
+ handle: identity.handle,
1827
+ email: identity.email,
1828
+ appId: cfg.app.id
1829
+ }, null, 2));
1830
+ return;
1831
+ }
1832
+ const handle = identity.handle ? ` (@${identity.handle})` : "";
1833
+ out.log(`Authorized ${identity.displayName}${handle} for ${cfg.app.id}.`);
1834
+ out.log(`odla account: ${identity.email ?? "not returned"}`);
1835
+ }
1836
+
1630
1837
  // src/tenant.ts
1631
1838
  import { tenantIdFor } from "@odla-ai/apps";
1632
1839
  function resolveEnv(cfg, requested) {
@@ -4272,6 +4479,14 @@ and atomically claim a refined Ready task. Record decisions when you make them
4272
4479
  and file bugs when you notice them. The conventions and the full command set are
4273
4480
  in \`.agents/skills/odla/references/pm.md\`.
4274
4481
 
4482
+ Use the human's signed-in odla account email for device authorization; never
4483
+ infer it from git config, commit metadata, or GitHub. If authorization is not
4484
+ already active, run
4485
+ \`npx @odla-ai/cli auth login --app <appId> --email <odla-account>\` and open
4486
+ the exact Studio URL it prints. Never file an odla project or product defect in
4487
+ GitHub Issues: run \`npx @odla-ai/cli bug report --app <appId> ...\` so the bug
4488
+ lands in odla PM with the rest of the project's goals, tasks, and decisions.
4489
+
4275
4490
  The setup runbooks and their references are installed in this repository, pinned
4276
4491
  to this CLI version. Use them as your setup context.
4277
4492
 
@@ -8337,13 +8552,13 @@ async function codeCommand(parsed, dependencies) {
8337
8552
  }
8338
8553
 
8339
8554
  // src/operator-credentials.ts
8340
- import process11 from "process";
8555
+ import process12 from "process";
8341
8556
  function developerTokenStatus(context, parsed, now = Date.now()) {
8342
8557
  const cached = readJsonFile(context.cfg.local.tokenFile);
8343
8558
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8344
8559
  const source = clean3(
8345
8560
  stringOpt(parsed.options.token)
8346
- ) ? "flag" : clean3(process11.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8561
+ ) ? "flag" : clean3(process12.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8347
8562
  return {
8348
8563
  source,
8349
8564
  cacheFile: context.cfg.local.tokenFile,
@@ -8500,6 +8715,7 @@ Start here:
8500
8715
  step it made wrong.
8501
8716
 
8502
8717
  Usage:
8718
+ odla-ai auth login --app <id> --email <odla-account> [--platform https://odla.ai] [--no-open] [--wait <seconds>] [--json]
8503
8719
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8504
8720
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
8505
8721
  odla-ai doctor [--config odla.config.mjs]
@@ -8536,6 +8752,7 @@ Usage:
8536
8752
  odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
8537
8753
  odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8538
8754
  odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
8755
+ odla-ai bug report --app <id> --title <t> (--description <text>|--body <text>) [--severity <s>] [--json]
8539
8756
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
8540
8757
  odla-ai pm <goal|task|decision|bug> ref <id> [--json]
8541
8758
  odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
@@ -8615,6 +8832,9 @@ function printHelp(output = console) {
8615
8832
  output.log(`odla-ai
8616
8833
  ${USAGE_SECTION}
8617
8834
  Commands:
8835
+ auth Start a fresh, exact-project agent authorization in the browser.
8836
+ The email is the signed-in odla account, never git or GitHub
8837
+ identity. The approval screen confirms the agent name first.
8618
8838
  agent Inspect durable agent wakeups and explicitly requeue a
8619
8839
  dead-lettered job; JSON output is stable for remote operators.
8620
8840
  runbook odla's operational procedures, stored in the database and read at
@@ -8682,6 +8902,8 @@ Commands:
8682
8902
  "app". Entities: goal (alias conformance), task (alias kanban),
8683
8903
  decision, bug. Status changes and comments post to each item's
8684
8904
  @odla-ai/chat discussion thread.
8905
+ bug Intent-first alias for PM bugs. "bug report" writes to
8906
+ odla PM; odla product defects do not belong in GitHub Issues.
8685
8907
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
8686
8908
  one group per project, topics with replies, @-mentions of people,
8687
8909
  agents, PM items, and projects. Built for unattended use \u2014 post a
@@ -8739,8 +8961,12 @@ Safety:
8739
8961
  wait for approval, and re-run to collect.
8740
8962
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8741
8963
  The email is a non-secret identity hint: never provide a password or session
8742
- token. The matching account must already exist, be signed in, explicitly
8964
+ token. It is the email shown by the signed-in odla account \u2014 never infer it
8965
+ from git config, a commit author, or GitHub. The matching account must already exist, be signed in, explicitly
8743
8966
  review the exact code, and finish any current request before claiming another.
8967
+ Use "auth login --app <id> --email <odla-account>" when an outside agent needs
8968
+ a deliberate fresh request; it ignores cached credentials and opens the same
8969
+ focused authorization sequence used by every first-time command.
8744
8970
  If provision reports that the current agent principal has no live app.manage
8745
8971
  grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
8746
8972
  the local cache, prints and opens a fresh exact-project owner-review URL, then
@@ -9234,7 +9460,8 @@ var ALLOWED = [
9234
9460
  "jsonl",
9235
9461
  "mutation-id",
9236
9462
  "platform",
9237
- "context"
9463
+ "context",
9464
+ "open"
9238
9465
  ];
9239
9466
  function requireId(id, action2) {
9240
9467
  if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
@@ -9253,7 +9480,8 @@ async function buildContext(parsed, deps) {
9253
9480
  configPath: cfg.configPath,
9254
9481
  token: stringOpt(parsed.options.token),
9255
9482
  email: stringOpt(parsed.options.email),
9256
- open: false
9483
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9484
+ openApprovalUrl: deps.openUrl
9257
9485
  },
9258
9486
  doFetch,
9259
9487
  out
@@ -9828,7 +10056,7 @@ var ALIASES = {
9828
10056
  decision: "decision",
9829
10057
  bug: "bug"
9830
10058
  };
9831
- var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context"];
10059
+ var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context", "open"];
9832
10060
  var ACTION_OPTIONS = {
9833
10061
  list: ["app", "q", "limit", "offset"],
9834
10062
  add: ["app", "title", "mutation-id"],
@@ -9892,7 +10120,13 @@ async function buildContext2(parsed, deps) {
9892
10120
  const out = deps.stdout ?? console;
9893
10121
  const token = await getDeveloperToken(
9894
10122
  cfg,
9895
- { configPath: cfg.configPath, token: stringOpt(parsed.options.token), email: stringOpt(parsed.options.email), open: false },
10123
+ {
10124
+ configPath: cfg.configPath,
10125
+ token: stringOpt(parsed.options.token),
10126
+ email: stringOpt(parsed.options.email),
10127
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10128
+ openApprovalUrl: deps.openUrl
10129
+ },
9896
10130
  doFetch,
9897
10131
  out
9898
10132
  );
@@ -10356,7 +10590,8 @@ async function o11yCommand(parsed, deps = {}) {
10356
10590
  "json",
10357
10591
  "app",
10358
10592
  "env",
10359
- "minutes"
10593
+ "minutes",
10594
+ "open"
10360
10595
  ],
10361
10596
  2
10362
10597
  );
@@ -10383,7 +10618,8 @@ async function o11yCommand(parsed, deps = {}) {
10383
10618
  configPath: cfg.configPath,
10384
10619
  token: stringOpt(parsed.options.token),
10385
10620
  email: stringOpt(parsed.options.email),
10386
- open: false
10621
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10622
+ openApprovalUrl: deps.openUrl
10387
10623
  },
10388
10624
  doFetch,
10389
10625
  out
@@ -10494,7 +10730,7 @@ async function read2(url, headers, doFetch) {
10494
10730
  // src/provision.ts
10495
10731
  import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor5 } from "@odla-ai/apps";
10496
10732
  import { putSecret as putSecret2 } from "@odla-ai/ai";
10497
- import process12 from "process";
10733
+ import process13 from "process";
10498
10734
 
10499
10735
  // src/integration-provision.ts
10500
10736
  import { uuidv7 } from "@odla-ai/db";
@@ -10814,7 +11050,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10814
11050
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10815
11051
  }
10816
11052
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10817
- const key = process12.env[cfg.ai.keyEnv];
11053
+ const key = process13.env[cfg.ai.keyEnv];
10818
11054
  if (key) {
10819
11055
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10820
11056
  await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10856,7 +11092,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10856
11092
 
10857
11093
  // src/record.ts
10858
11094
  import { appendFileSync } from "fs";
10859
- import process13 from "process";
11095
+ import process14 from "process";
10860
11096
 
10861
11097
  // src/surface.ts
10862
11098
  var PM_ACTIONS = {
@@ -10913,7 +11149,9 @@ var COMMAND_SURFACE = {
10913
11149
  promote: {},
10914
11150
  owners: { list: {}, add: {}, remove: {} }
10915
11151
  },
11152
+ auth: { login: {} },
10916
11153
  brand: { design: { unpack: {} } },
11154
+ bug: { create: {}, list: {}, report: {} },
10917
11155
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
10918
11156
  capabilities: {},
10919
11157
  code: { connect: {} },
@@ -11028,7 +11266,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
11028
11266
 
11029
11267
  // src/record.ts
11030
11268
  function recordInvocation(parsed) {
11031
- const file = process13.env.ODLA_CLI_RECORD;
11269
+ const file = process14.env.ODLA_CLI_RECORD;
11032
11270
  if (!file) return;
11033
11271
  try {
11034
11272
  const entry = {
@@ -11651,9 +11889,9 @@ import { spawnSync } from "child_process";
11651
11889
  import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
11652
11890
  import { tmpdir as tmpdir5 } from "os";
11653
11891
  import { join as join15 } from "path";
11654
- import process14 from "process";
11892
+ import process15 from "process";
11655
11893
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11656
- function resolveEditor(env = process14.env) {
11894
+ function resolveEditor(env = process15.env) {
11657
11895
  for (const name of EDITOR_ENV) {
11658
11896
  const value2 = env[name];
11659
11897
  if (value2 && value2.trim()) return value2.trim();
@@ -11667,8 +11905,8 @@ function defaultRun(command, path) {
11667
11905
  return result.status ?? 0;
11668
11906
  }
11669
11907
  function editText(initial, slug, deps = {}) {
11670
- const env = deps.env ?? process14.env;
11671
- const interactive = deps.interactive ?? (() => Boolean(process14.stdin.isTTY));
11908
+ const env = deps.env ?? process15.env;
11909
+ const interactive = deps.interactive ?? (() => Boolean(process15.stdin.isTTY));
11672
11910
  const editor = resolveEditor(env);
11673
11911
  if (!editor)
11674
11912
  throw new Error(
@@ -11704,142 +11942,6 @@ async function editRunbook(ctx, slug, deps = {}) {
11704
11942
  return body === null ? null : { body, expectedVersion: found.version };
11705
11943
  }
11706
11944
 
11707
- // src/whoami-command.ts
11708
- var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11709
- function principalKind(value2, machine) {
11710
- return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11711
- }
11712
- function credentialKind(value2, machine, scopes) {
11713
- if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11714
- if (machine) return "machine";
11715
- if (scopes.length) return "device";
11716
- return "unknown";
11717
- }
11718
- function managerOf(value2) {
11719
- if (!value2 || typeof value2 !== "object") return null;
11720
- const row = value2;
11721
- const principalId = text(row.principalId);
11722
- if (!principalId) return null;
11723
- return {
11724
- principalId,
11725
- displayName: text(row.displayName) ?? "Unnamed member",
11726
- handle: text(row.handle) ?? ""
11727
- };
11728
- }
11729
- function unnamedPrincipal(kind) {
11730
- if (kind === "agent") return "Unnamed agent";
11731
- if (kind === "service") return "Unnamed service";
11732
- return "Unnamed member";
11733
- }
11734
- async function fetchIdentity(platformUrl, token, doFetch) {
11735
- const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11736
- headers: { authorization: `Bearer ${token}` }
11737
- });
11738
- if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11739
- const body = await res.json();
11740
- const developerId = text(body.developerId) ?? "";
11741
- const machine = body.machine === true;
11742
- const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11743
- const principalId = text(body.principalId) ?? developerId;
11744
- const email = text(body.email);
11745
- const kind = principalKind(body.principalKind, machine);
11746
- const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11747
- const handle = text(body.handle) ?? "";
11748
- const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11749
- return {
11750
- developerId,
11751
- principalId,
11752
- principalKind: kind,
11753
- displayName,
11754
- handle,
11755
- manager: managerOf(body.manager),
11756
- credential: {
11757
- id: text(credential2.id),
11758
- kind: credentialKind(credential2.kind, machine, scopes)
11759
- },
11760
- email,
11761
- admin: body.admin === true,
11762
- machine,
11763
- scopes
11764
- };
11765
- }
11766
- function credentialLabel(identity) {
11767
- if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11768
- if (identity.credential.kind === "device")
11769
- return identity.scopes.length ? "device (scoped)" : "device";
11770
- if (identity.credential.kind === "clerk") return "clerk";
11771
- return "unknown (legacy server)";
11772
- }
11773
- function namedPrincipal(identity) {
11774
- return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11775
- }
11776
- function namedManager(manager) {
11777
- return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11778
- }
11779
- function accountableOwner(identity) {
11780
- if (identity.principalKind === "agent" && identity.manager) {
11781
- return namedManager(identity.manager);
11782
- }
11783
- if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11784
- return namedPrincipal(identity);
11785
- }
11786
- return identity.email ?? "Unnamed member";
11787
- }
11788
- async function whoamiCommand(parsed, deps = {}) {
11789
- assertArgs(
11790
- parsed,
11791
- ["config", "context", "platform", "token", "email", "json"],
11792
- 1
11793
- );
11794
- const out = deps.stdout ?? console;
11795
- const doFetch = deps.fetch ?? fetch;
11796
- const { cfg } = await resolveOperatorContext(parsed, {
11797
- allowMissingConfig: true
11798
- });
11799
- const token = await getDeveloperToken(
11800
- cfg,
11801
- {
11802
- configPath: cfg.configPath,
11803
- token: stringOpt(parsed.options.token),
11804
- email: stringOpt(parsed.options.email),
11805
- // As above: never suppress the browser for an ordinary sign-in.
11806
- open: void 0
11807
- },
11808
- doFetch,
11809
- out
11810
- );
11811
- const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
11812
- if (parsed.options.json === true) {
11813
- out.log(JSON.stringify(identity, null, 2));
11814
- return;
11815
- }
11816
- out.log(`platform: ${cfg.platformUrl}`);
11817
- out.log(`principal: ${namedPrincipal(identity)}`);
11818
- out.log(`principal id: ${identity.principalId}`);
11819
- out.log(`kind: ${identity.principalKind}`);
11820
- if (identity.principalKind === "agent")
11821
- out.log(
11822
- `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11823
- );
11824
- out.log(`owner: ${accountableOwner(identity)}`);
11825
- out.log(`owner id: ${identity.developerId}`);
11826
- out.log(`email: ${identity.email ?? "(none)"}`);
11827
- out.log(`credential: ${credentialLabel(identity)}`);
11828
- if (identity.credential.id)
11829
- out.log(`credential id: ${identity.credential.id}`);
11830
- out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11831
- if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11832
- if (!identity.admin) {
11833
- if (identity.scopes.includes("platform:runbook:write")) {
11834
- out.log("\nThis exact scope can read and edit all platform runbook content.");
11835
- out.log("The device token is not ambient admin and cannot change visibility.");
11836
- } else {
11837
- out.log("\nYou can read operator-visible platform runbooks but not edit them.");
11838
- out.log("Use a platform-admin-approved runbook write request to edit.");
11839
- }
11840
- }
11841
- }
11842
-
11843
11945
  // src/runbook-command.ts
11844
11946
  var ALLOWED2 = [
11845
11947
  "config",
@@ -12731,6 +12833,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12731
12833
  await whoamiCommand(parsed, runtime);
12732
12834
  return;
12733
12835
  }
12836
+ if (command === "auth") {
12837
+ await authCommand(parsed, runtime);
12838
+ return;
12839
+ }
12734
12840
  if (command === "context") {
12735
12841
  await contextCommand(parsed, runtime);
12736
12842
  return;
@@ -12771,6 +12877,15 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12771
12877
  await pmCommand(parsed, runtime);
12772
12878
  return;
12773
12879
  }
12880
+ if (command === "bug") {
12881
+ const action2 = parsed.positionals[1] ?? "list";
12882
+ const canonical = action2 === "report" || action2 === "create" ? "add" : action2;
12883
+ await pmCommand({
12884
+ ...parsed,
12885
+ positionals: ["pm", "bug", canonical, ...parsed.positionals.slice(2)]
12886
+ }, runtime);
12887
+ return;
12888
+ }
12774
12889
  if (command === "discuss") {
12775
12890
  await discussCommand(parsed, runtime);
12776
12891
  return;
@@ -12911,4 +13026,4 @@ export {
12911
13026
  isTerminalHostedSecurityStatus,
12912
13027
  runCli
12913
13028
  };
12914
- //# sourceMappingURL=chunk-3WVVHH3Y.js.map
13029
+ //# sourceMappingURL=chunk-STJCWZJU.js.map