@odla-ai/cli 0.27.15 → 0.28.0

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.
@@ -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, {
@@ -458,8 +459,15 @@ function stillPending(pending, email) {
458
459
  }
459
460
  function handshakeEmail(value2, cached) {
460
461
  const email = (value2 ?? process5.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
462
+ if (/@users\.noreply\.github\.com$/i.test(email)) {
463
+ throw new Error(
464
+ `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
465
+ );
466
+ }
461
467
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
462
- throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
468
+ throw new Error(
469
+ "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"
470
+ );
463
471
  }
464
472
  return email;
465
473
  }
@@ -1627,6 +1635,204 @@ async function adminCommand(parsed, deps = {}) {
1627
1635
  });
1628
1636
  }
1629
1637
 
1638
+ // src/auth-command.ts
1639
+ import process11 from "process";
1640
+
1641
+ // src/whoami-command.ts
1642
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
1643
+ function principalKind(value2, machine) {
1644
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
1645
+ }
1646
+ function credentialKind(value2, machine, scopes) {
1647
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
1648
+ if (machine) return "machine";
1649
+ if (scopes.length) return "device";
1650
+ return "unknown";
1651
+ }
1652
+ function managerOf(value2) {
1653
+ if (!value2 || typeof value2 !== "object") return null;
1654
+ const row = value2;
1655
+ const principalId = text(row.principalId);
1656
+ if (!principalId) return null;
1657
+ return {
1658
+ principalId,
1659
+ displayName: text(row.displayName) ?? "Unnamed member",
1660
+ handle: text(row.handle) ?? ""
1661
+ };
1662
+ }
1663
+ function unnamedPrincipal(kind) {
1664
+ if (kind === "agent") return "Unnamed agent";
1665
+ if (kind === "service") return "Unnamed service";
1666
+ return "Unnamed member";
1667
+ }
1668
+ async function fetchIdentity(platformUrl, token, doFetch) {
1669
+ const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
1670
+ headers: { authorization: `Bearer ${token}` }
1671
+ });
1672
+ if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
1673
+ const body = await res.json();
1674
+ const developerId = text(body.developerId) ?? "";
1675
+ const machine = body.machine === true;
1676
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
1677
+ const principalId = text(body.principalId) ?? developerId;
1678
+ const email = text(body.email);
1679
+ const kind = principalKind(body.principalKind, machine);
1680
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
1681
+ const handle = text(body.handle) ?? "";
1682
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
1683
+ return {
1684
+ developerId,
1685
+ principalId,
1686
+ principalKind: kind,
1687
+ displayName,
1688
+ handle,
1689
+ manager: managerOf(body.manager),
1690
+ credential: {
1691
+ id: text(credential2.id),
1692
+ kind: credentialKind(credential2.kind, machine, scopes)
1693
+ },
1694
+ email,
1695
+ admin: body.admin === true,
1696
+ machine,
1697
+ scopes
1698
+ };
1699
+ }
1700
+ function credentialLabel(identity) {
1701
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
1702
+ if (identity.credential.kind === "device")
1703
+ return identity.scopes.length ? "device (scoped)" : "device";
1704
+ if (identity.credential.kind === "clerk") return "clerk";
1705
+ return "unknown (legacy server)";
1706
+ }
1707
+ function namedPrincipal(identity) {
1708
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
1709
+ }
1710
+ function namedManager(manager) {
1711
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
1712
+ }
1713
+ function accountableOwner(identity) {
1714
+ if (identity.principalKind === "agent" && identity.manager) {
1715
+ return namedManager(identity.manager);
1716
+ }
1717
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
1718
+ return namedPrincipal(identity);
1719
+ }
1720
+ return identity.email ?? "Unnamed member";
1721
+ }
1722
+ async function whoamiCommand(parsed, deps = {}) {
1723
+ assertArgs(
1724
+ parsed,
1725
+ ["config", "context", "platform", "token", "email", "json", "open"],
1726
+ 1
1727
+ );
1728
+ const out = deps.stdout ?? console;
1729
+ const doFetch = deps.fetch ?? fetch;
1730
+ const { cfg } = await resolveOperatorContext(parsed, {
1731
+ allowMissingConfig: true
1732
+ });
1733
+ const token = await getDeveloperToken(
1734
+ cfg,
1735
+ {
1736
+ configPath: cfg.configPath,
1737
+ token: stringOpt(parsed.options.token),
1738
+ email: stringOpt(parsed.options.email),
1739
+ // As above: never suppress the browser for an ordinary sign-in.
1740
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1741
+ openApprovalUrl: deps.openUrl
1742
+ },
1743
+ doFetch,
1744
+ out
1745
+ );
1746
+ const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
1747
+ if (parsed.options.json === true) {
1748
+ out.log(JSON.stringify(identity, null, 2));
1749
+ return;
1750
+ }
1751
+ out.log(`platform: ${cfg.platformUrl}`);
1752
+ out.log(`principal: ${namedPrincipal(identity)}`);
1753
+ out.log(`principal id: ${identity.principalId}`);
1754
+ out.log(`kind: ${identity.principalKind}`);
1755
+ if (identity.principalKind === "agent")
1756
+ out.log(
1757
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
1758
+ );
1759
+ out.log(`owner: ${accountableOwner(identity)}`);
1760
+ out.log(`owner id: ${identity.developerId}`);
1761
+ out.log(`email: ${identity.email ?? "(none)"}`);
1762
+ out.log(`credential: ${credentialLabel(identity)}`);
1763
+ if (identity.credential.id)
1764
+ out.log(`credential id: ${identity.credential.id}`);
1765
+ out.log(`admin: ${identity.admin ? "yes" : "no"}`);
1766
+ if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
1767
+ if (!identity.admin) {
1768
+ if (identity.scopes.includes("platform:runbook:write")) {
1769
+ out.log("\nThis exact scope can read and edit all platform runbook content.");
1770
+ out.log("The device token is not ambient admin and cannot change visibility.");
1771
+ } else {
1772
+ out.log("\nYou can read operator-visible platform runbooks but not edit them.");
1773
+ out.log("Use a platform-admin-approved runbook write request to edit.");
1774
+ }
1775
+ }
1776
+ }
1777
+
1778
+ // src/auth-command.ts
1779
+ async function authCommand(parsed, deps = {}) {
1780
+ assertArgs(parsed, [
1781
+ "config",
1782
+ "context",
1783
+ "platform",
1784
+ "app",
1785
+ "email",
1786
+ "open",
1787
+ "wait",
1788
+ "json"
1789
+ ], 2);
1790
+ const action2 = parsed.positionals[1] ?? "login";
1791
+ if (action2 !== "login") {
1792
+ throw new Error(`unknown auth action "${action2}". Try "odla-ai auth login --app <id> --email <odla-account>".`);
1793
+ }
1794
+ const context = await resolveOperatorContext(parsed, {
1795
+ allowMissingConfig: true,
1796
+ requireApp: true
1797
+ });
1798
+ const { cfg } = context;
1799
+ const out = deps.stdout ?? console;
1800
+ const doFetch = deps.fetch ?? fetch;
1801
+ const email = stringOpt(parsed.options.email) ?? process11.env.ODLA_USER_EMAIL?.trim();
1802
+ if (!email) {
1803
+ throw new Error(
1804
+ "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
1805
+ );
1806
+ }
1807
+ const token = await getDeveloperToken(
1808
+ cfg,
1809
+ {
1810
+ configPath: cfg.configPath,
1811
+ email,
1812
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1813
+ wait: numberOpt(parsed.options.wait, "--wait"),
1814
+ openApprovalUrl: deps.openUrl
1815
+ },
1816
+ doFetch,
1817
+ out,
1818
+ { freshLogin: true }
1819
+ );
1820
+ const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
1821
+ if (parsed.options.json === true) {
1822
+ out.log(JSON.stringify({
1823
+ principalId: identity.principalId,
1824
+ displayName: identity.displayName,
1825
+ handle: identity.handle,
1826
+ email: identity.email,
1827
+ appId: cfg.app.id
1828
+ }, null, 2));
1829
+ return;
1830
+ }
1831
+ const handle = identity.handle ? ` (@${identity.handle})` : "";
1832
+ out.log(`Authorized ${identity.displayName}${handle} for ${cfg.app.id}.`);
1833
+ out.log(`odla account: ${identity.email ?? "not returned"}`);
1834
+ }
1835
+
1630
1836
  // src/tenant.ts
1631
1837
  import { tenantIdFor } from "@odla-ai/apps";
1632
1838
  function resolveEnv(cfg, requested) {
@@ -2577,6 +2783,63 @@ function printGroup(out, heading, items) {
2577
2783
  out.log("");
2578
2784
  }
2579
2785
 
2786
+ // src/ai-models.ts
2787
+ import { DEFAULT_CATALOG } from "@odla-ai/ai";
2788
+ async function aiModels(options = {}) {
2789
+ const cfg = await loadProjectConfig(options.configPath ?? "odla.config.mjs");
2790
+ const env = options.env ?? cfg.envs[0] ?? "dev";
2791
+ if (!cfg.envs.includes(env)) throw new Error(`ai models env "${env}" is not declared in config envs`);
2792
+ const url = new URL(`/registry/apps/${encodeURIComponent(cfg.app.id)}/public-config`, cfg.platformUrl);
2793
+ url.searchParams.set("env", env);
2794
+ const response2 = await (options.fetch ?? fetch)(url);
2795
+ if (!response2.ok) throw new Error(`read app AI models failed (${response2.status}): ${await safeText4(response2)}`);
2796
+ const body = await response2.json();
2797
+ if (!body.ai) throw new Error(`ai is not configured for ${cfg.app.id}/${env}`);
2798
+ const mode = body.ai.mode === "hosted" ? "hosted" : "byok";
2799
+ const defaultModel = typeof body.ai.model === "string" ? body.ai.model : void 0;
2800
+ let models;
2801
+ if (mode === "hosted") {
2802
+ if (body.ai.enabled !== true) throw new Error(`hosted ai is disabled for ${cfg.app.id}/${env}`);
2803
+ if (!Array.isArray(body.ai.models) || !body.ai.models.every(isModelSpec)) {
2804
+ throw new Error("platform returned an invalid hosted AI model catalog");
2805
+ }
2806
+ models = body.ai.models;
2807
+ } else {
2808
+ const provider = typeof body.ai.provider === "string" ? body.ai.provider : cfg.ai?.provider;
2809
+ if (!provider) throw new Error(`BYOK ai has no provider for ${cfg.app.id}/${env}`);
2810
+ models = Object.values(DEFAULT_CATALOG).filter((model) => model.provider === provider);
2811
+ }
2812
+ if (options.provider) models = models.filter((model) => model.provider === options.provider);
2813
+ models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
2814
+ const out = options.stdout ?? console;
2815
+ if (options.json) {
2816
+ out.log(JSON.stringify({ appId: cfg.app.id, env, mode, defaultModel: defaultModel ?? null, models }, null, 2));
2817
+ return;
2818
+ }
2819
+ out.log("provider model default capabilities");
2820
+ for (const model of models) {
2821
+ out.log([model.provider, model.id, model.id === defaultModel ? "yes" : "", capabilityList(model)].join(" "));
2822
+ }
2823
+ }
2824
+ function capabilityList(model) {
2825
+ return [
2826
+ model.capabilities.imageIn ? "image" : "",
2827
+ model.capabilities.audioIn ? "audio" : "",
2828
+ model.capabilities.documentIn ? "document" : "",
2829
+ model.capabilities.toolUse ? "tools" : "",
2830
+ model.capabilities.thinking || model.capabilities.effort ? "reasoning" : "",
2831
+ model.capabilities.webSearch || model.superpowers?.webSearch ? "web-search" : ""
2832
+ ].filter(Boolean).join(",");
2833
+ }
2834
+ function isModelSpec(value2) {
2835
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return false;
2836
+ const model = value2;
2837
+ return typeof model.id === "string" && typeof model.nativeId === "string" && (model.provider === "anthropic" || model.provider === "openai" || model.provider === "google") && Boolean(model.capabilities) && typeof model.capabilities === "object";
2838
+ }
2839
+ async function safeText4(response2) {
2840
+ return (await response2.text().catch(() => "request failed")).slice(0, 300);
2841
+ }
2842
+
2580
2843
  // src/config-operation-command.ts
2581
2844
  import {
2582
2845
  AppsError,
@@ -2768,7 +3031,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2768
3031
  });
2769
3032
  if (res.ok || res.status === 404) return;
2770
3033
  if (res.status === 403) {
2771
- const detail = await safeText4(res);
3034
+ const detail = await safeText5(res);
2772
3035
  const code = errorCode(detail);
2773
3036
  if (code === "human_session_required") {
2774
3037
  throw new Error(
@@ -2784,7 +3047,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2784
3047
  `${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; run "odla-ai provision --request-grant --email <odla-account>" to open a fresh owner review. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
2785
3048
  );
2786
3049
  }
2787
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
3050
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
2788
3051
  }
2789
3052
  function errorCode(text2) {
2790
3053
  try {
@@ -2800,7 +3063,7 @@ async function postJson(doFetch, url, bearer, body) {
2800
3063
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
2801
3064
  body: JSON.stringify(body)
2802
3065
  });
2803
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
3066
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
2804
3067
  }
2805
3068
  function normalizeClerkConfig(value2) {
2806
3069
  if (!value2) return null;
@@ -2813,7 +3076,7 @@ function normalizeClerkConfig(value2) {
2813
3076
  const publishableKey = envValue(cfg.publishableKey);
2814
3077
  return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
2815
3078
  }
2816
- async function safeText4(res) {
3079
+ async function safeText5(res) {
2817
3080
  try {
2818
3081
  return redactSecrets((await res.text()).slice(0, 500));
2819
3082
  } catch {
@@ -4215,6 +4478,14 @@ and atomically claim a refined Ready task. Record decisions when you make them
4215
4478
  and file bugs when you notice them. The conventions and the full command set are
4216
4479
  in \`.agents/skills/odla/references/pm.md\`.
4217
4480
 
4481
+ Use the human's signed-in odla account email for device authorization; never
4482
+ infer it from git config, commit metadata, or GitHub. If authorization is not
4483
+ already active, run
4484
+ \`npx @odla-ai/cli auth login --app <appId> --email <odla-account>\` and open
4485
+ the exact Studio URL it prints. Never file an odla project or product defect in
4486
+ GitHub Issues: run \`npx @odla-ai/cli bug report --app <appId> ...\` so the bug
4487
+ lands in odla PM with the rest of the project's goals, tasks, and decisions.
4488
+
4218
4489
  The setup runbooks and their references are installed in this repository, pinned
4219
4490
  to this CLI version. Use them as your setup context.
4220
4491
 
@@ -4583,7 +4854,7 @@ async function getJson(doFetch, url, bearer) {
4583
4854
  const res = await doFetch(url, {
4584
4855
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
4585
4856
  });
4586
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4857
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText6(res)}`);
4587
4858
  return res.json();
4588
4859
  }
4589
4860
  async function postJson2(doFetch, url, bearer, body) {
@@ -4592,7 +4863,7 @@ async function postJson2(doFetch, url, bearer, body) {
4592
4863
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
4593
4864
  body: JSON.stringify(body)
4594
4865
  });
4595
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4866
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText6(res)}`);
4596
4867
  return res.json();
4597
4868
  }
4598
4869
  function publicConfigUrl(platformUrl, appId, env) {
@@ -4600,7 +4871,7 @@ function publicConfigUrl(platformUrl, appId, env) {
4600
4871
  url.searchParams.set("env", env);
4601
4872
  return url.toString();
4602
4873
  }
4603
- async function safeText5(res) {
4874
+ async function safeText6(res) {
4604
4875
  try {
4605
4876
  return redactSecrets((await res.text()).slice(0, 500));
4606
4877
  } catch {
@@ -4656,6 +4927,20 @@ async function secretsCommand(parsed, deps) {
4656
4927
  });
4657
4928
  }
4658
4929
  async function projectCommand(command, parsed, deps) {
4930
+ if (command === "ai") {
4931
+ const sub = parsed.positionals[1];
4932
+ if (sub !== "models") throw new Error(`unknown ai subcommand "${sub ?? ""}". Try "odla-ai ai models --env dev".`);
4933
+ assertArgs(parsed, ["config", "env", "provider", "json"], 2);
4934
+ await aiModels({
4935
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
4936
+ env: stringOpt(parsed.options.env),
4937
+ provider: stringOpt(parsed.options.provider),
4938
+ json: parsed.options.json === true,
4939
+ fetch: deps.fetch,
4940
+ stdout: deps.stdout
4941
+ });
4942
+ return true;
4943
+ }
4659
4944
  if (command === "config") {
4660
4945
  const sub = parsed.positionals[1];
4661
4946
  if (sub !== "diff" && sub !== "plan" && sub !== "apply") {
@@ -8266,13 +8551,13 @@ async function codeCommand(parsed, dependencies) {
8266
8551
  }
8267
8552
 
8268
8553
  // src/operator-credentials.ts
8269
- import process11 from "process";
8554
+ import process12 from "process";
8270
8555
  function developerTokenStatus(context, parsed, now = Date.now()) {
8271
8556
  const cached = readJsonFile(context.cfg.local.tokenFile);
8272
8557
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8273
8558
  const source = clean3(
8274
8559
  stringOpt(parsed.options.token)
8275
- ) ? "flag" : clean3(process11.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8560
+ ) ? "flag" : clean3(process12.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8276
8561
  return {
8277
8562
  source,
8278
8563
  cacheFile: context.cfg.local.tokenFile,
@@ -8429,9 +8714,11 @@ Start here:
8429
8714
  step it made wrong.
8430
8715
 
8431
8716
  Usage:
8717
+ odla-ai auth login --app <id> --email <odla-account> [--platform https://odla.ai] [--no-open] [--wait <seconds>] [--json]
8432
8718
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8433
8719
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
8434
8720
  odla-ai doctor [--config odla.config.mjs]
8721
+ odla-ai ai models [--config odla.config.mjs] [--env dev] [--provider <id>] [--json]
8435
8722
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8436
8723
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
8437
8724
  odla-ai operations get <operation-id> [--json]
@@ -8464,6 +8751,7 @@ Usage:
8464
8751
  odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
8465
8752
  odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8466
8753
  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]
8754
+ odla-ai bug report --app <id> --title <t> (--description <text>|--body <text>) [--severity <s>] [--json]
8467
8755
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
8468
8756
  odla-ai pm <goal|task|decision|bug> ref <id> [--json]
8469
8757
  odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
@@ -8543,6 +8831,9 @@ function printHelp(output = console) {
8543
8831
  output.log(`odla-ai
8544
8832
  ${USAGE_SECTION}
8545
8833
  Commands:
8834
+ auth Start a fresh, exact-project agent authorization in the browser.
8835
+ The email is the signed-in odla account, never git or GitHub
8836
+ identity. The approval screen confirms the agent name first.
8546
8837
  agent Inspect durable agent wakeups and explicitly requeue a
8547
8838
  dead-lettered job; JSON output is stable for remote operators.
8548
8839
  runbook odla's operational procedures, stored in the database and read at
@@ -8610,6 +8901,8 @@ Commands:
8610
8901
  "app". Entities: goal (alias conformance), task (alias kanban),
8611
8902
  decision, bug. Status changes and comments post to each item's
8612
8903
  @odla-ai/chat discussion thread.
8904
+ bug Intent-first alias for PM bugs. "bug report" writes to
8905
+ odla PM; odla product defects do not belong in GitHub Issues.
8613
8906
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
8614
8907
  one group per project, topics with replies, @-mentions of people,
8615
8908
  agents, PM items, and projects. Built for unattended use \u2014 post a
@@ -8667,8 +8960,12 @@ Safety:
8667
8960
  wait for approval, and re-run to collect.
8668
8961
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8669
8962
  The email is a non-secret identity hint: never provide a password or session
8670
- token. The matching account must already exist, be signed in, explicitly
8963
+ token. It is the email shown by the signed-in odla account \u2014 never infer it
8964
+ from git config, a commit author, or GitHub. The matching account must already exist, be signed in, explicitly
8671
8965
  review the exact code, and finish any current request before claiming another.
8966
+ Use "auth login --app <id> --email <odla-account>" when an outside agent needs
8967
+ a deliberate fresh request; it ignores cached credentials and opens the same
8968
+ focused authorization sequence used by every first-time command.
8672
8969
  If provision reports that the current agent principal has no live app.manage
8673
8970
  grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
8674
8971
  the local cache, prints and opens a fresh exact-project owner-review URL, then
@@ -9162,7 +9459,8 @@ var ALLOWED = [
9162
9459
  "jsonl",
9163
9460
  "mutation-id",
9164
9461
  "platform",
9165
- "context"
9462
+ "context",
9463
+ "open"
9166
9464
  ];
9167
9465
  function requireId(id, action2) {
9168
9466
  if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
@@ -9181,7 +9479,8 @@ async function buildContext(parsed, deps) {
9181
9479
  configPath: cfg.configPath,
9182
9480
  token: stringOpt(parsed.options.token),
9183
9481
  email: stringOpt(parsed.options.email),
9184
- open: false
9482
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9483
+ openApprovalUrl: deps.openUrl
9185
9484
  },
9186
9485
  doFetch,
9187
9486
  out
@@ -9756,7 +10055,7 @@ var ALIASES = {
9756
10055
  decision: "decision",
9757
10056
  bug: "bug"
9758
10057
  };
9759
- var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context"];
10058
+ var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context", "open"];
9760
10059
  var ACTION_OPTIONS = {
9761
10060
  list: ["app", "q", "limit", "offset"],
9762
10061
  add: ["app", "title", "mutation-id"],
@@ -9820,7 +10119,13 @@ async function buildContext2(parsed, deps) {
9820
10119
  const out = deps.stdout ?? console;
9821
10120
  const token = await getDeveloperToken(
9822
10121
  cfg,
9823
- { configPath: cfg.configPath, token: stringOpt(parsed.options.token), email: stringOpt(parsed.options.email), open: false },
10122
+ {
10123
+ configPath: cfg.configPath,
10124
+ token: stringOpt(parsed.options.token),
10125
+ email: stringOpt(parsed.options.email),
10126
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10127
+ openApprovalUrl: deps.openUrl
10128
+ },
9824
10129
  doFetch,
9825
10130
  out
9826
10131
  );
@@ -10284,7 +10589,8 @@ async function o11yCommand(parsed, deps = {}) {
10284
10589
  "json",
10285
10590
  "app",
10286
10591
  "env",
10287
- "minutes"
10592
+ "minutes",
10593
+ "open"
10288
10594
  ],
10289
10595
  2
10290
10596
  );
@@ -10311,7 +10617,8 @@ async function o11yCommand(parsed, deps = {}) {
10311
10617
  configPath: cfg.configPath,
10312
10618
  token: stringOpt(parsed.options.token),
10313
10619
  email: stringOpt(parsed.options.email),
10314
- open: false
10620
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10621
+ openApprovalUrl: deps.openUrl
10315
10622
  },
10316
10623
  doFetch,
10317
10624
  out
@@ -10422,7 +10729,7 @@ async function read2(url, headers, doFetch) {
10422
10729
  // src/provision.ts
10423
10730
  import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor5 } from "@odla-ai/apps";
10424
10731
  import { putSecret as putSecret2 } from "@odla-ai/ai";
10425
- import process12 from "process";
10732
+ import process13 from "process";
10426
10733
 
10427
10734
  // src/integration-provision.ts
10428
10735
  import { uuidv7 } from "@odla-ai/db";
@@ -10535,14 +10842,14 @@ async function mintDbKey(opts, tenantId) {
10535
10842
  appId: tenantId
10536
10843
  })
10537
10844
  });
10538
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
10845
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText7(created)}`);
10539
10846
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
10540
10847
  method: "POST",
10541
10848
  headers,
10542
10849
  body: "{}"
10543
10850
  });
10544
10851
  }
10545
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
10852
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText7(res)}`);
10546
10853
  const body = await res.json();
10547
10854
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
10548
10855
  return body.key;
@@ -10558,12 +10865,12 @@ async function issueO11yToken(opts) {
10558
10865
  `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
10559
10866
  );
10560
10867
  }
10561
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
10868
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
10562
10869
  const body = await res.json();
10563
10870
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
10564
10871
  return body.token;
10565
10872
  }
10566
- async function safeText6(res) {
10873
+ async function safeText7(res) {
10567
10874
  try {
10568
10875
  return redactSecrets((await res.text()).slice(0, 500));
10569
10876
  } catch {
@@ -10742,7 +11049,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10742
11049
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10743
11050
  }
10744
11051
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10745
- const key = process12.env[cfg.ai.keyEnv];
11052
+ const key = process13.env[cfg.ai.keyEnv];
10746
11053
  if (key) {
10747
11054
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10748
11055
  await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10784,7 +11091,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10784
11091
 
10785
11092
  // src/record.ts
10786
11093
  import { appendFileSync } from "fs";
10787
- import process13 from "process";
11094
+ import process14 from "process";
10788
11095
 
10789
11096
  // src/surface.ts
10790
11097
  var PM_ACTIONS = {
@@ -10818,6 +11125,7 @@ var PM_ENTITIES = {
10818
11125
  };
10819
11126
  var COMMAND_SURFACE = {
10820
11127
  agent: { jobs: {}, retry: {} },
11128
+ ai: { models: {} },
10821
11129
  admin: {
10822
11130
  ai: {
10823
11131
  show: {},
@@ -10840,7 +11148,9 @@ var COMMAND_SURFACE = {
10840
11148
  promote: {},
10841
11149
  owners: { list: {}, add: {}, remove: {} }
10842
11150
  },
11151
+ auth: { login: {} },
10843
11152
  brand: { design: { unpack: {} } },
11153
+ bug: { create: {}, list: {}, report: {} },
10844
11154
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
10845
11155
  capabilities: {},
10846
11156
  code: { connect: {} },
@@ -10955,7 +11265,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
10955
11265
 
10956
11266
  // src/record.ts
10957
11267
  function recordInvocation(parsed) {
10958
- const file = process13.env.ODLA_CLI_RECORD;
11268
+ const file = process14.env.ODLA_CLI_RECORD;
10959
11269
  if (!file) return;
10960
11270
  try {
10961
11271
  const entry = {
@@ -11578,9 +11888,9 @@ import { spawnSync } from "child_process";
11578
11888
  import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
11579
11889
  import { tmpdir as tmpdir5 } from "os";
11580
11890
  import { join as join15 } from "path";
11581
- import process14 from "process";
11891
+ import process15 from "process";
11582
11892
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11583
- function resolveEditor(env = process14.env) {
11893
+ function resolveEditor(env = process15.env) {
11584
11894
  for (const name of EDITOR_ENV) {
11585
11895
  const value2 = env[name];
11586
11896
  if (value2 && value2.trim()) return value2.trim();
@@ -11594,8 +11904,8 @@ function defaultRun(command, path) {
11594
11904
  return result.status ?? 0;
11595
11905
  }
11596
11906
  function editText(initial, slug, deps = {}) {
11597
- const env = deps.env ?? process14.env;
11598
- const interactive = deps.interactive ?? (() => Boolean(process14.stdin.isTTY));
11907
+ const env = deps.env ?? process15.env;
11908
+ const interactive = deps.interactive ?? (() => Boolean(process15.stdin.isTTY));
11599
11909
  const editor = resolveEditor(env);
11600
11910
  if (!editor)
11601
11911
  throw new Error(
@@ -11631,142 +11941,6 @@ async function editRunbook(ctx, slug, deps = {}) {
11631
11941
  return body === null ? null : { body, expectedVersion: found.version };
11632
11942
  }
11633
11943
 
11634
- // src/whoami-command.ts
11635
- var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11636
- function principalKind(value2, machine) {
11637
- return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11638
- }
11639
- function credentialKind(value2, machine, scopes) {
11640
- if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11641
- if (machine) return "machine";
11642
- if (scopes.length) return "device";
11643
- return "unknown";
11644
- }
11645
- function managerOf(value2) {
11646
- if (!value2 || typeof value2 !== "object") return null;
11647
- const row = value2;
11648
- const principalId = text(row.principalId);
11649
- if (!principalId) return null;
11650
- return {
11651
- principalId,
11652
- displayName: text(row.displayName) ?? "Unnamed member",
11653
- handle: text(row.handle) ?? ""
11654
- };
11655
- }
11656
- function unnamedPrincipal(kind) {
11657
- if (kind === "agent") return "Unnamed agent";
11658
- if (kind === "service") return "Unnamed service";
11659
- return "Unnamed member";
11660
- }
11661
- async function fetchIdentity(platformUrl, token, doFetch) {
11662
- const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11663
- headers: { authorization: `Bearer ${token}` }
11664
- });
11665
- if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11666
- const body = await res.json();
11667
- const developerId = text(body.developerId) ?? "";
11668
- const machine = body.machine === true;
11669
- const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11670
- const principalId = text(body.principalId) ?? developerId;
11671
- const email = text(body.email);
11672
- const kind = principalKind(body.principalKind, machine);
11673
- const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11674
- const handle = text(body.handle) ?? "";
11675
- const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11676
- return {
11677
- developerId,
11678
- principalId,
11679
- principalKind: kind,
11680
- displayName,
11681
- handle,
11682
- manager: managerOf(body.manager),
11683
- credential: {
11684
- id: text(credential2.id),
11685
- kind: credentialKind(credential2.kind, machine, scopes)
11686
- },
11687
- email,
11688
- admin: body.admin === true,
11689
- machine,
11690
- scopes
11691
- };
11692
- }
11693
- function credentialLabel(identity) {
11694
- if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11695
- if (identity.credential.kind === "device")
11696
- return identity.scopes.length ? "device (scoped)" : "device";
11697
- if (identity.credential.kind === "clerk") return "clerk";
11698
- return "unknown (legacy server)";
11699
- }
11700
- function namedPrincipal(identity) {
11701
- return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11702
- }
11703
- function namedManager(manager) {
11704
- return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11705
- }
11706
- function accountableOwner(identity) {
11707
- if (identity.principalKind === "agent" && identity.manager) {
11708
- return namedManager(identity.manager);
11709
- }
11710
- if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11711
- return namedPrincipal(identity);
11712
- }
11713
- return identity.email ?? "Unnamed member";
11714
- }
11715
- async function whoamiCommand(parsed, deps = {}) {
11716
- assertArgs(
11717
- parsed,
11718
- ["config", "context", "platform", "token", "email", "json"],
11719
- 1
11720
- );
11721
- const out = deps.stdout ?? console;
11722
- const doFetch = deps.fetch ?? fetch;
11723
- const { cfg } = await resolveOperatorContext(parsed, {
11724
- allowMissingConfig: true
11725
- });
11726
- const token = await getDeveloperToken(
11727
- cfg,
11728
- {
11729
- configPath: cfg.configPath,
11730
- token: stringOpt(parsed.options.token),
11731
- email: stringOpt(parsed.options.email),
11732
- // As above: never suppress the browser for an ordinary sign-in.
11733
- open: void 0
11734
- },
11735
- doFetch,
11736
- out
11737
- );
11738
- const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
11739
- if (parsed.options.json === true) {
11740
- out.log(JSON.stringify(identity, null, 2));
11741
- return;
11742
- }
11743
- out.log(`platform: ${cfg.platformUrl}`);
11744
- out.log(`principal: ${namedPrincipal(identity)}`);
11745
- out.log(`principal id: ${identity.principalId}`);
11746
- out.log(`kind: ${identity.principalKind}`);
11747
- if (identity.principalKind === "agent")
11748
- out.log(
11749
- `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11750
- );
11751
- out.log(`owner: ${accountableOwner(identity)}`);
11752
- out.log(`owner id: ${identity.developerId}`);
11753
- out.log(`email: ${identity.email ?? "(none)"}`);
11754
- out.log(`credential: ${credentialLabel(identity)}`);
11755
- if (identity.credential.id)
11756
- out.log(`credential id: ${identity.credential.id}`);
11757
- out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11758
- if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11759
- if (!identity.admin) {
11760
- if (identity.scopes.includes("platform:runbook:write")) {
11761
- out.log("\nThis exact scope can read and edit all platform runbook content.");
11762
- out.log("The device token is not ambient admin and cannot change visibility.");
11763
- } else {
11764
- out.log("\nYou can read operator-visible platform runbooks but not edit them.");
11765
- out.log("Use a platform-admin-approved runbook write request to edit.");
11766
- }
11767
- }
11768
- }
11769
-
11770
11944
  // src/runbook-command.ts
11771
11945
  var ALLOWED2 = [
11772
11946
  "config",
@@ -12658,6 +12832,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12658
12832
  await whoamiCommand(parsed, runtime);
12659
12833
  return;
12660
12834
  }
12835
+ if (command === "auth") {
12836
+ await authCommand(parsed, runtime);
12837
+ return;
12838
+ }
12661
12839
  if (command === "context") {
12662
12840
  await contextCommand(parsed, runtime);
12663
12841
  return;
@@ -12698,6 +12876,15 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12698
12876
  await pmCommand(parsed, runtime);
12699
12877
  return;
12700
12878
  }
12879
+ if (command === "bug") {
12880
+ const action2 = parsed.positionals[1] ?? "list";
12881
+ const canonical = action2 === "report" || action2 === "create" ? "add" : action2;
12882
+ await pmCommand({
12883
+ ...parsed,
12884
+ positionals: ["pm", "bug", canonical, ...parsed.positionals.slice(2)]
12885
+ }, runtime);
12886
+ return;
12887
+ }
12701
12888
  if (command === "discuss") {
12702
12889
  await discussCommand(parsed, runtime);
12703
12890
  return;
@@ -12793,6 +12980,7 @@ export {
12793
12980
  calendarDisconnect,
12794
12981
  CAPABILITIES,
12795
12982
  printCapabilities,
12983
+ aiModels,
12796
12984
  ConfigOperationCommandError,
12797
12985
  desiredRegistryState,
12798
12986
  configApply,
@@ -12837,4 +13025,4 @@ export {
12837
13025
  isTerminalHostedSecurityStatus,
12838
13026
  runCli
12839
13027
  };
12840
- //# sourceMappingURL=chunk-MR5QXX3B.js.map
13028
+ //# sourceMappingURL=chunk-HCNESRIA.js.map