@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.
package/dist/index.cjs CHANGED
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
42
42
  acceptedAfter: () => acceptedAfter,
43
43
  adminAi: () => adminAi,
44
+ aiModels: () => aiModels,
44
45
  calendarBookingPageUrl: () => calendarBookingPageUrl,
45
46
  calendarCalendars: () => calendarCalendars,
46
47
  calendarConnect: () => calendarConnect,
@@ -390,7 +391,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
390
391
  const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
391
392
  const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
392
393
  const cached = readJsonFile(cfg.local.tokenFile);
393
- if (!grantRequest.forceReview) {
394
+ if (!grantRequest.forceReview && !grantRequest.freshLogin) {
394
395
  if (options.token) return options.token;
395
396
  if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
396
397
  const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
@@ -407,9 +408,9 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
407
408
  }
408
409
  } else {
409
410
  if (options.token) {
410
- throw new Error("--request-grant cannot be combined with --token; remove --token so the approved replacement credential can be collected and cached");
411
+ 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");
411
412
  }
412
- out.error(`auth: requesting fresh owner review for app.manage on exact project "${cfg.app.id}"`);
413
+ 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}"`);
413
414
  }
414
415
  const ctx = {
415
416
  cfg,
@@ -422,6 +423,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
422
423
  grantIntent
423
424
  };
424
425
  const waitMs = handshakeWaitMs(options.wait);
426
+ if (grantRequest.freshLogin) clearPendingHandshake(ctx.pendingFile);
425
427
  const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
426
428
  clearPendingHandshake(ctx.pendingFile);
427
429
  writePrivateJson(cfg.local.tokenFile, {
@@ -544,8 +546,15 @@ function stillPending(pending, email) {
544
546
  }
545
547
  function handshakeEmail(value2, cached) {
546
548
  const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
549
+ if (/@users\.noreply\.github\.com$/i.test(email)) {
550
+ throw new Error(
551
+ `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
552
+ );
553
+ }
547
554
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
548
- throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
555
+ throw new Error(
556
+ "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"
557
+ );
549
558
  }
550
559
  return email;
551
560
  }
@@ -1713,6 +1722,204 @@ async function adminCommand(parsed, deps = {}) {
1713
1722
  });
1714
1723
  }
1715
1724
 
1725
+ // src/auth-command.ts
1726
+ var import_node_process10 = __toESM(require("process"), 1);
1727
+
1728
+ // src/whoami-command.ts
1729
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
1730
+ function principalKind(value2, machine) {
1731
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
1732
+ }
1733
+ function credentialKind(value2, machine, scopes) {
1734
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
1735
+ if (machine) return "machine";
1736
+ if (scopes.length) return "device";
1737
+ return "unknown";
1738
+ }
1739
+ function managerOf(value2) {
1740
+ if (!value2 || typeof value2 !== "object") return null;
1741
+ const row = value2;
1742
+ const principalId = text(row.principalId);
1743
+ if (!principalId) return null;
1744
+ return {
1745
+ principalId,
1746
+ displayName: text(row.displayName) ?? "Unnamed member",
1747
+ handle: text(row.handle) ?? ""
1748
+ };
1749
+ }
1750
+ function unnamedPrincipal(kind) {
1751
+ if (kind === "agent") return "Unnamed agent";
1752
+ if (kind === "service") return "Unnamed service";
1753
+ return "Unnamed member";
1754
+ }
1755
+ async function fetchIdentity(platformUrl, token, doFetch) {
1756
+ const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
1757
+ headers: { authorization: `Bearer ${token}` }
1758
+ });
1759
+ if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
1760
+ const body = await res.json();
1761
+ const developerId = text(body.developerId) ?? "";
1762
+ const machine = body.machine === true;
1763
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
1764
+ const principalId = text(body.principalId) ?? developerId;
1765
+ const email = text(body.email);
1766
+ const kind = principalKind(body.principalKind, machine);
1767
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
1768
+ const handle = text(body.handle) ?? "";
1769
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
1770
+ return {
1771
+ developerId,
1772
+ principalId,
1773
+ principalKind: kind,
1774
+ displayName,
1775
+ handle,
1776
+ manager: managerOf(body.manager),
1777
+ credential: {
1778
+ id: text(credential2.id),
1779
+ kind: credentialKind(credential2.kind, machine, scopes)
1780
+ },
1781
+ email,
1782
+ admin: body.admin === true,
1783
+ machine,
1784
+ scopes
1785
+ };
1786
+ }
1787
+ function credentialLabel(identity) {
1788
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
1789
+ if (identity.credential.kind === "device")
1790
+ return identity.scopes.length ? "device (scoped)" : "device";
1791
+ if (identity.credential.kind === "clerk") return "clerk";
1792
+ return "unknown (legacy server)";
1793
+ }
1794
+ function namedPrincipal(identity) {
1795
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
1796
+ }
1797
+ function namedManager(manager) {
1798
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
1799
+ }
1800
+ function accountableOwner(identity) {
1801
+ if (identity.principalKind === "agent" && identity.manager) {
1802
+ return namedManager(identity.manager);
1803
+ }
1804
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
1805
+ return namedPrincipal(identity);
1806
+ }
1807
+ return identity.email ?? "Unnamed member";
1808
+ }
1809
+ async function whoamiCommand(parsed, deps = {}) {
1810
+ assertArgs(
1811
+ parsed,
1812
+ ["config", "context", "platform", "token", "email", "json", "open"],
1813
+ 1
1814
+ );
1815
+ const out = deps.stdout ?? console;
1816
+ const doFetch = deps.fetch ?? fetch;
1817
+ const { cfg } = await resolveOperatorContext(parsed, {
1818
+ allowMissingConfig: true
1819
+ });
1820
+ const token = await getDeveloperToken(
1821
+ cfg,
1822
+ {
1823
+ configPath: cfg.configPath,
1824
+ token: stringOpt(parsed.options.token),
1825
+ email: stringOpt(parsed.options.email),
1826
+ // As above: never suppress the browser for an ordinary sign-in.
1827
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1828
+ openApprovalUrl: deps.openUrl
1829
+ },
1830
+ doFetch,
1831
+ out
1832
+ );
1833
+ const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
1834
+ if (parsed.options.json === true) {
1835
+ out.log(JSON.stringify(identity, null, 2));
1836
+ return;
1837
+ }
1838
+ out.log(`platform: ${cfg.platformUrl}`);
1839
+ out.log(`principal: ${namedPrincipal(identity)}`);
1840
+ out.log(`principal id: ${identity.principalId}`);
1841
+ out.log(`kind: ${identity.principalKind}`);
1842
+ if (identity.principalKind === "agent")
1843
+ out.log(
1844
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
1845
+ );
1846
+ out.log(`owner: ${accountableOwner(identity)}`);
1847
+ out.log(`owner id: ${identity.developerId}`);
1848
+ out.log(`email: ${identity.email ?? "(none)"}`);
1849
+ out.log(`credential: ${credentialLabel(identity)}`);
1850
+ if (identity.credential.id)
1851
+ out.log(`credential id: ${identity.credential.id}`);
1852
+ out.log(`admin: ${identity.admin ? "yes" : "no"}`);
1853
+ if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
1854
+ if (!identity.admin) {
1855
+ if (identity.scopes.includes("platform:runbook:write")) {
1856
+ out.log("\nThis exact scope can read and edit all platform runbook content.");
1857
+ out.log("The device token is not ambient admin and cannot change visibility.");
1858
+ } else {
1859
+ out.log("\nYou can read operator-visible platform runbooks but not edit them.");
1860
+ out.log("Use a platform-admin-approved runbook write request to edit.");
1861
+ }
1862
+ }
1863
+ }
1864
+
1865
+ // src/auth-command.ts
1866
+ async function authCommand(parsed, deps = {}) {
1867
+ assertArgs(parsed, [
1868
+ "config",
1869
+ "context",
1870
+ "platform",
1871
+ "app",
1872
+ "email",
1873
+ "open",
1874
+ "wait",
1875
+ "json"
1876
+ ], 2);
1877
+ const action2 = parsed.positionals[1] ?? "login";
1878
+ if (action2 !== "login") {
1879
+ throw new Error(`unknown auth action "${action2}". Try "odla-ai auth login --app <id> --email <odla-account>".`);
1880
+ }
1881
+ const context = await resolveOperatorContext(parsed, {
1882
+ allowMissingConfig: true,
1883
+ requireApp: true
1884
+ });
1885
+ const { cfg } = context;
1886
+ const out = deps.stdout ?? console;
1887
+ const doFetch = deps.fetch ?? fetch;
1888
+ const email = stringOpt(parsed.options.email) ?? import_node_process10.default.env.ODLA_USER_EMAIL?.trim();
1889
+ if (!email) {
1890
+ throw new Error(
1891
+ "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
1892
+ );
1893
+ }
1894
+ const token = await getDeveloperToken(
1895
+ cfg,
1896
+ {
1897
+ configPath: cfg.configPath,
1898
+ email,
1899
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
1900
+ wait: numberOpt(parsed.options.wait, "--wait"),
1901
+ openApprovalUrl: deps.openUrl
1902
+ },
1903
+ doFetch,
1904
+ out,
1905
+ { freshLogin: true }
1906
+ );
1907
+ const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
1908
+ if (parsed.options.json === true) {
1909
+ out.log(JSON.stringify({
1910
+ principalId: identity.principalId,
1911
+ displayName: identity.displayName,
1912
+ handle: identity.handle,
1913
+ email: identity.email,
1914
+ appId: cfg.app.id
1915
+ }, null, 2));
1916
+ return;
1917
+ }
1918
+ const handle = identity.handle ? ` (@${identity.handle})` : "";
1919
+ out.log(`Authorized ${identity.displayName}${handle} for ${cfg.app.id}.`);
1920
+ out.log(`odla account: ${identity.email ?? "not returned"}`);
1921
+ }
1922
+
1716
1923
  // src/tenant.ts
1717
1924
  var import_apps2 = require("@odla-ai/apps");
1718
1925
  function resolveEnv(cfg, requested) {
@@ -2704,6 +2911,63 @@ function printGroup(out, heading, items) {
2704
2911
  out.log("");
2705
2912
  }
2706
2913
 
2914
+ // src/ai-models.ts
2915
+ var import_ai = require("@odla-ai/ai");
2916
+ async function aiModels(options = {}) {
2917
+ const cfg = await loadProjectConfig(options.configPath ?? "odla.config.mjs");
2918
+ const env = options.env ?? cfg.envs[0] ?? "dev";
2919
+ if (!cfg.envs.includes(env)) throw new Error(`ai models env "${env}" is not declared in config envs`);
2920
+ const url = new URL(`/registry/apps/${encodeURIComponent(cfg.app.id)}/public-config`, cfg.platformUrl);
2921
+ url.searchParams.set("env", env);
2922
+ const response2 = await (options.fetch ?? fetch)(url);
2923
+ if (!response2.ok) throw new Error(`read app AI models failed (${response2.status}): ${await safeText4(response2)}`);
2924
+ const body = await response2.json();
2925
+ if (!body.ai) throw new Error(`ai is not configured for ${cfg.app.id}/${env}`);
2926
+ const mode = body.ai.mode === "hosted" ? "hosted" : "byok";
2927
+ const defaultModel = typeof body.ai.model === "string" ? body.ai.model : void 0;
2928
+ let models;
2929
+ if (mode === "hosted") {
2930
+ if (body.ai.enabled !== true) throw new Error(`hosted ai is disabled for ${cfg.app.id}/${env}`);
2931
+ if (!Array.isArray(body.ai.models) || !body.ai.models.every(isModelSpec)) {
2932
+ throw new Error("platform returned an invalid hosted AI model catalog");
2933
+ }
2934
+ models = body.ai.models;
2935
+ } else {
2936
+ const provider = typeof body.ai.provider === "string" ? body.ai.provider : cfg.ai?.provider;
2937
+ if (!provider) throw new Error(`BYOK ai has no provider for ${cfg.app.id}/${env}`);
2938
+ models = Object.values(import_ai.DEFAULT_CATALOG).filter((model) => model.provider === provider);
2939
+ }
2940
+ if (options.provider) models = models.filter((model) => model.provider === options.provider);
2941
+ models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
2942
+ const out = options.stdout ?? console;
2943
+ if (options.json) {
2944
+ out.log(JSON.stringify({ appId: cfg.app.id, env, mode, defaultModel: defaultModel ?? null, models }, null, 2));
2945
+ return;
2946
+ }
2947
+ out.log("provider model default capabilities");
2948
+ for (const model of models) {
2949
+ out.log([model.provider, model.id, model.id === defaultModel ? "yes" : "", capabilityList(model)].join(" "));
2950
+ }
2951
+ }
2952
+ function capabilityList(model) {
2953
+ return [
2954
+ model.capabilities.imageIn ? "image" : "",
2955
+ model.capabilities.audioIn ? "audio" : "",
2956
+ model.capabilities.documentIn ? "document" : "",
2957
+ model.capabilities.toolUse ? "tools" : "",
2958
+ model.capabilities.thinking || model.capabilities.effort ? "reasoning" : "",
2959
+ model.capabilities.webSearch || model.superpowers?.webSearch ? "web-search" : ""
2960
+ ].filter(Boolean).join(",");
2961
+ }
2962
+ function isModelSpec(value2) {
2963
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return false;
2964
+ const model = value2;
2965
+ 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";
2966
+ }
2967
+ async function safeText4(response2) {
2968
+ return (await response2.text().catch(() => "request failed")).slice(0, 300);
2969
+ }
2970
+
2707
2971
  // src/config-operation-command.ts
2708
2972
  var import_apps6 = require("@odla-ai/apps");
2709
2973
  var import_node_path8 = require("path");
@@ -2884,10 +3148,10 @@ function record2(value2) {
2884
3148
  var import_apps5 = require("@odla-ai/apps");
2885
3149
 
2886
3150
  // src/provision-helpers.ts
2887
- var import_ai = require("@odla-ai/ai");
3151
+ var import_ai2 = require("@odla-ai/ai");
2888
3152
  var import_apps4 = require("@odla-ai/apps");
2889
3153
  function defaultSecretName(provider) {
2890
- const names = import_ai.DEFAULT_SECRET_NAMES;
3154
+ const names = import_ai2.DEFAULT_SECRET_NAMES;
2891
3155
  return names[provider] ?? `${provider}_api_key`;
2892
3156
  }
2893
3157
  async function assertTenantAdminAccess(doFetch, cfg, env, token) {
@@ -2897,7 +3161,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2897
3161
  });
2898
3162
  if (res.ok || res.status === 404) return;
2899
3163
  if (res.status === 403) {
2900
- const detail = await safeText4(res);
3164
+ const detail = await safeText5(res);
2901
3165
  const code = errorCode(detail);
2902
3166
  if (code === "human_session_required") {
2903
3167
  throw new Error(
@@ -2913,7 +3177,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2913
3177
  `${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`
2914
3178
  );
2915
3179
  }
2916
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
3180
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
2917
3181
  }
2918
3182
  function errorCode(text2) {
2919
3183
  try {
@@ -2929,7 +3193,7 @@ async function postJson(doFetch, url, bearer, body) {
2929
3193
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
2930
3194
  body: JSON.stringify(body)
2931
3195
  });
2932
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
3196
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
2933
3197
  }
2934
3198
  function normalizeClerkConfig(value2) {
2935
3199
  if (!value2) return null;
@@ -2942,7 +3206,7 @@ function normalizeClerkConfig(value2) {
2942
3206
  const publishableKey = envValue(cfg.publishableKey);
2943
3207
  return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
2944
3208
  }
2945
- async function safeText4(res) {
3209
+ async function safeText5(res) {
2946
3210
  try {
2947
3211
  return redactSecrets((await res.text()).slice(0, 500));
2948
3212
  } catch {
@@ -4263,7 +4527,7 @@ function assertWranglerConfig(cfg) {
4263
4527
  }
4264
4528
 
4265
4529
  // src/secrets-set.ts
4266
- var import_ai2 = require("@odla-ai/ai");
4530
+ var import_ai3 = require("@odla-ai/ai");
4267
4531
  var import_apps10 = require("@odla-ai/apps");
4268
4532
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
4269
4533
  async function secretsSet(options) {
@@ -4277,7 +4541,7 @@ async function secretsSet(options) {
4277
4541
  optionalProjectCapabilities: ["app.manage"]
4278
4542
  });
4279
4543
  try {
4280
- await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
4544
+ await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
4281
4545
  } catch (err) {
4282
4546
  throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value2));
4283
4547
  }
@@ -4344,6 +4608,14 @@ and atomically claim a refined Ready task. Record decisions when you make them
4344
4608
  and file bugs when you notice them. The conventions and the full command set are
4345
4609
  in \`.agents/skills/odla/references/pm.md\`.
4346
4610
 
4611
+ Use the human's signed-in odla account email for device authorization; never
4612
+ infer it from git config, commit metadata, or GitHub. If authorization is not
4613
+ already active, run
4614
+ \`npx @odla-ai/cli auth login --app <appId> --email <odla-account>\` and open
4615
+ the exact Studio URL it prints. Never file an odla project or product defect in
4616
+ GitHub Issues: run \`npx @odla-ai/cli bug report --app <appId> ...\` so the bug
4617
+ lands in odla PM with the rest of the project's goals, tasks, and decisions.
4618
+
4347
4619
  The setup runbooks and their references are installed in this repository, pinned
4348
4620
  to this CLI version. Use them as your setup context.
4349
4621
 
@@ -4712,7 +4984,7 @@ async function getJson(doFetch, url, bearer) {
4712
4984
  const res = await doFetch(url, {
4713
4985
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
4714
4986
  });
4715
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4987
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText6(res)}`);
4716
4988
  return res.json();
4717
4989
  }
4718
4990
  async function postJson2(doFetch, url, bearer, body) {
@@ -4721,7 +4993,7 @@ async function postJson2(doFetch, url, bearer, body) {
4721
4993
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
4722
4994
  body: JSON.stringify(body)
4723
4995
  });
4724
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
4996
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText6(res)}`);
4725
4997
  return res.json();
4726
4998
  }
4727
4999
  function publicConfigUrl(platformUrl, appId, env) {
@@ -4729,7 +5001,7 @@ function publicConfigUrl(platformUrl, appId, env) {
4729
5001
  url.searchParams.set("env", env);
4730
5002
  return url.toString();
4731
5003
  }
4732
- async function safeText5(res) {
5004
+ async function safeText6(res) {
4733
5005
  try {
4734
5006
  return redactSecrets((await res.text()).slice(0, 500));
4735
5007
  } catch {
@@ -4785,6 +5057,20 @@ async function secretsCommand(parsed, deps) {
4785
5057
  });
4786
5058
  }
4787
5059
  async function projectCommand(command, parsed, deps) {
5060
+ if (command === "ai") {
5061
+ const sub = parsed.positionals[1];
5062
+ if (sub !== "models") throw new Error(`unknown ai subcommand "${sub ?? ""}". Try "odla-ai ai models --env dev".`);
5063
+ assertArgs(parsed, ["config", "env", "provider", "json"], 2);
5064
+ await aiModels({
5065
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
5066
+ env: stringOpt(parsed.options.env),
5067
+ provider: stringOpt(parsed.options.provider),
5068
+ json: parsed.options.json === true,
5069
+ fetch: deps.fetch,
5070
+ stdout: deps.stdout
5071
+ });
5072
+ return true;
5073
+ }
4788
5074
  if (command === "config") {
4789
5075
  const sub = parsed.positionals[1];
4790
5076
  if (sub !== "diff" && sub !== "plan" && sub !== "apply") {
@@ -8395,13 +8681,13 @@ async function codeCommand(parsed, dependencies) {
8395
8681
  }
8396
8682
 
8397
8683
  // src/operator-credentials.ts
8398
- var import_node_process10 = __toESM(require("process"), 1);
8684
+ var import_node_process11 = __toESM(require("process"), 1);
8399
8685
  function developerTokenStatus(context, parsed, now = Date.now()) {
8400
8686
  const cached = readJsonFile(context.cfg.local.tokenFile);
8401
8687
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8402
8688
  const source = clean3(
8403
8689
  stringOpt(parsed.options.token)
8404
- ) ? "flag" : clean3(import_node_process10.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8690
+ ) ? "flag" : clean3(import_node_process11.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8405
8691
  return {
8406
8692
  source,
8407
8693
  cacheFile: context.cfg.local.tokenFile,
@@ -8558,9 +8844,11 @@ Start here:
8558
8844
  step it made wrong.
8559
8845
 
8560
8846
  Usage:
8847
+ odla-ai auth login --app <id> --email <odla-account> [--platform https://odla.ai] [--no-open] [--wait <seconds>] [--json]
8561
8848
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8562
8849
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
8563
8850
  odla-ai doctor [--config odla.config.mjs]
8851
+ odla-ai ai models [--config odla.config.mjs] [--env dev] [--provider <id>] [--json]
8564
8852
  odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8565
8853
  odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
8566
8854
  odla-ai operations get <operation-id> [--json]
@@ -8593,6 +8881,7 @@ Usage:
8593
8881
  odla-ai pm task release <id> --expected-revision <n> [--mutation-id <id>] [--json]
8594
8882
  odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8595
8883
  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]
8884
+ odla-ai bug report --app <id> --title <t> (--description <text>|--body <text>) [--severity <s>] [--json]
8596
8885
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
8597
8886
  odla-ai pm <goal|task|decision|bug> ref <id> [--json]
8598
8887
  odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
@@ -8672,6 +8961,9 @@ function printHelp(output = console) {
8672
8961
  output.log(`odla-ai
8673
8962
  ${USAGE_SECTION}
8674
8963
  Commands:
8964
+ auth Start a fresh, exact-project agent authorization in the browser.
8965
+ The email is the signed-in odla account, never git or GitHub
8966
+ identity. The approval screen confirms the agent name first.
8675
8967
  agent Inspect durable agent wakeups and explicitly requeue a
8676
8968
  dead-lettered job; JSON output is stable for remote operators.
8677
8969
  runbook odla's operational procedures, stored in the database and read at
@@ -8739,6 +9031,8 @@ Commands:
8739
9031
  "app". Entities: goal (alias conformance), task (alias kanban),
8740
9032
  decision, bug. Status changes and comments post to each item's
8741
9033
  @odla-ai/chat discussion thread.
9034
+ bug Intent-first alias for PM bugs. "bug report" writes to
9035
+ odla PM; odla product defects do not belong in GitHub Issues.
8742
9036
  discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
8743
9037
  one group per project, topics with replies, @-mentions of people,
8744
9038
  agents, PM items, and projects. Built for unattended use \u2014 post a
@@ -8796,8 +9090,12 @@ Safety:
8796
9090
  wait for approval, and re-run to collect.
8797
9091
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8798
9092
  The email is a non-secret identity hint: never provide a password or session
8799
- token. The matching account must already exist, be signed in, explicitly
9093
+ token. It is the email shown by the signed-in odla account \u2014 never infer it
9094
+ from git config, a commit author, or GitHub. The matching account must already exist, be signed in, explicitly
8800
9095
  review the exact code, and finish any current request before claiming another.
9096
+ Use "auth login --app <id> --email <odla-account>" when an outside agent needs
9097
+ a deliberate fresh request; it ignores cached credentials and opens the same
9098
+ focused authorization sequence used by every first-time command.
8801
9099
  If provision reports that the current agent principal has no live app.manage
8802
9100
  grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
8803
9101
  the local cache, prints and opens a fresh exact-project owner-review URL, then
@@ -9291,7 +9589,8 @@ var ALLOWED = [
9291
9589
  "jsonl",
9292
9590
  "mutation-id",
9293
9591
  "platform",
9294
- "context"
9592
+ "context",
9593
+ "open"
9295
9594
  ];
9296
9595
  function requireId(id, action2) {
9297
9596
  if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
@@ -9310,7 +9609,8 @@ async function buildContext(parsed, deps) {
9310
9609
  configPath: cfg.configPath,
9311
9610
  token: stringOpt(parsed.options.token),
9312
9611
  email: stringOpt(parsed.options.email),
9313
- open: false
9612
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9613
+ openApprovalUrl: deps.openUrl
9314
9614
  },
9315
9615
  doFetch,
9316
9616
  out
@@ -9885,7 +10185,7 @@ var ALIASES = {
9885
10185
  decision: "decision",
9886
10186
  bug: "bug"
9887
10187
  };
9888
- var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context"];
10188
+ var COMMON_OPTIONS = ["config", "token", "email", "json", "platform", "context", "open"];
9889
10189
  var ACTION_OPTIONS = {
9890
10190
  list: ["app", "q", "limit", "offset"],
9891
10191
  add: ["app", "title", "mutation-id"],
@@ -9949,7 +10249,13 @@ async function buildContext2(parsed, deps) {
9949
10249
  const out = deps.stdout ?? console;
9950
10250
  const token = await getDeveloperToken(
9951
10251
  cfg,
9952
- { configPath: cfg.configPath, token: stringOpt(parsed.options.token), email: stringOpt(parsed.options.email), open: false },
10252
+ {
10253
+ configPath: cfg.configPath,
10254
+ token: stringOpt(parsed.options.token),
10255
+ email: stringOpt(parsed.options.email),
10256
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10257
+ openApprovalUrl: deps.openUrl
10258
+ },
9953
10259
  doFetch,
9954
10260
  out
9955
10261
  );
@@ -10413,7 +10719,8 @@ async function o11yCommand(parsed, deps = {}) {
10413
10719
  "json",
10414
10720
  "app",
10415
10721
  "env",
10416
- "minutes"
10722
+ "minutes",
10723
+ "open"
10417
10724
  ],
10418
10725
  2
10419
10726
  );
@@ -10440,7 +10747,8 @@ async function o11yCommand(parsed, deps = {}) {
10440
10747
  configPath: cfg.configPath,
10441
10748
  token: stringOpt(parsed.options.token),
10442
10749
  email: stringOpt(parsed.options.email),
10443
- open: false
10750
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10751
+ openApprovalUrl: deps.openUrl
10444
10752
  },
10445
10753
  doFetch,
10446
10754
  out
@@ -10550,8 +10858,8 @@ async function read2(url, headers, doFetch) {
10550
10858
 
10551
10859
  // src/provision.ts
10552
10860
  var import_apps12 = require("@odla-ai/apps");
10553
- var import_ai3 = require("@odla-ai/ai");
10554
- var import_node_process11 = __toESM(require("process"), 1);
10861
+ var import_ai4 = require("@odla-ai/ai");
10862
+ var import_node_process12 = __toESM(require("process"), 1);
10555
10863
 
10556
10864
  // src/integration-provision.ts
10557
10865
  var import_db3 = require("@odla-ai/db");
@@ -10664,14 +10972,14 @@ async function mintDbKey(opts, tenantId) {
10664
10972
  appId: tenantId
10665
10973
  })
10666
10974
  });
10667
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
10975
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText7(created)}`);
10668
10976
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
10669
10977
  method: "POST",
10670
10978
  headers,
10671
10979
  body: "{}"
10672
10980
  });
10673
10981
  }
10674
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
10982
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText7(res)}`);
10675
10983
  const body = await res.json();
10676
10984
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
10677
10985
  return body.key;
@@ -10687,12 +10995,12 @@ async function issueO11yToken(opts) {
10687
10995
  `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`
10688
10996
  );
10689
10997
  }
10690
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
10998
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
10691
10999
  const body = await res.json();
10692
11000
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
10693
11001
  return body.token;
10694
11002
  }
10695
- async function safeText6(res) {
11003
+ async function safeText7(res) {
10696
11004
  try {
10697
11005
  return redactSecrets((await res.text()).slice(0, 500));
10698
11006
  } catch {
@@ -10871,10 +11179,10 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10871
11179
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10872
11180
  }
10873
11181
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10874
- const key = import_node_process11.default.env[cfg.ai.keyEnv];
11182
+ const key = import_node_process12.default.env[cfg.ai.keyEnv];
10875
11183
  if (key) {
10876
11184
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10877
- await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
11185
+ await (0, import_ai4.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
10878
11186
  out.log(`${env}: ${cfg.ai.provider} key stored in vault (${secretName})`);
10879
11187
  } else {
10880
11188
  out.log(`${env}: ${cfg.ai.keyEnv} not set; skipped provider key storage`);
@@ -10913,7 +11221,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10913
11221
 
10914
11222
  // src/record.ts
10915
11223
  var import_node_fs15 = require("fs");
10916
- var import_node_process12 = __toESM(require("process"), 1);
11224
+ var import_node_process13 = __toESM(require("process"), 1);
10917
11225
 
10918
11226
  // src/surface.ts
10919
11227
  var PM_ACTIONS = {
@@ -10947,6 +11255,7 @@ var PM_ENTITIES = {
10947
11255
  };
10948
11256
  var COMMAND_SURFACE = {
10949
11257
  agent: { jobs: {}, retry: {} },
11258
+ ai: { models: {} },
10950
11259
  admin: {
10951
11260
  ai: {
10952
11261
  show: {},
@@ -10969,7 +11278,9 @@ var COMMAND_SURFACE = {
10969
11278
  promote: {},
10970
11279
  owners: { list: {}, add: {}, remove: {} }
10971
11280
  },
11281
+ auth: { login: {} },
10972
11282
  brand: { design: { unpack: {} } },
11283
+ bug: { create: {}, list: {}, report: {} },
10973
11284
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
10974
11285
  capabilities: {},
10975
11286
  code: { connect: {} },
@@ -11084,7 +11395,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
11084
11395
 
11085
11396
  // src/record.ts
11086
11397
  function recordInvocation(parsed) {
11087
- const file = import_node_process12.default.env.ODLA_CLI_RECORD;
11398
+ const file = import_node_process13.default.env.ODLA_CLI_RECORD;
11088
11399
  if (!file) return;
11089
11400
  try {
11090
11401
  const entry = {
@@ -11756,9 +12067,9 @@ var import_node_child_process8 = require("child_process");
11756
12067
  var import_node_fs19 = require("fs");
11757
12068
  var import_node_os5 = require("os");
11758
12069
  var import_node_path18 = require("path");
11759
- var import_node_process13 = __toESM(require("process"), 1);
12070
+ var import_node_process14 = __toESM(require("process"), 1);
11760
12071
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11761
- function resolveEditor(env = import_node_process13.default.env) {
12072
+ function resolveEditor(env = import_node_process14.default.env) {
11762
12073
  for (const name of EDITOR_ENV) {
11763
12074
  const value2 = env[name];
11764
12075
  if (value2 && value2.trim()) return value2.trim();
@@ -11772,8 +12083,8 @@ function defaultRun(command, path) {
11772
12083
  return result.status ?? 0;
11773
12084
  }
11774
12085
  function editText(initial, slug, deps = {}) {
11775
- const env = deps.env ?? import_node_process13.default.env;
11776
- const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
12086
+ const env = deps.env ?? import_node_process14.default.env;
12087
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process14.default.stdin.isTTY));
11777
12088
  const editor = resolveEditor(env);
11778
12089
  if (!editor)
11779
12090
  throw new Error(
@@ -11809,142 +12120,6 @@ async function editRunbook(ctx, slug, deps = {}) {
11809
12120
  return body === null ? null : { body, expectedVersion: found.version };
11810
12121
  }
11811
12122
 
11812
- // src/whoami-command.ts
11813
- var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11814
- function principalKind(value2, machine) {
11815
- return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11816
- }
11817
- function credentialKind(value2, machine, scopes) {
11818
- if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11819
- if (machine) return "machine";
11820
- if (scopes.length) return "device";
11821
- return "unknown";
11822
- }
11823
- function managerOf(value2) {
11824
- if (!value2 || typeof value2 !== "object") return null;
11825
- const row = value2;
11826
- const principalId = text(row.principalId);
11827
- if (!principalId) return null;
11828
- return {
11829
- principalId,
11830
- displayName: text(row.displayName) ?? "Unnamed member",
11831
- handle: text(row.handle) ?? ""
11832
- };
11833
- }
11834
- function unnamedPrincipal(kind) {
11835
- if (kind === "agent") return "Unnamed agent";
11836
- if (kind === "service") return "Unnamed service";
11837
- return "Unnamed member";
11838
- }
11839
- async function fetchIdentity(platformUrl, token, doFetch) {
11840
- const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
11841
- headers: { authorization: `Bearer ${token}` }
11842
- });
11843
- if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
11844
- const body = await res.json();
11845
- const developerId = text(body.developerId) ?? "";
11846
- const machine = body.machine === true;
11847
- const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11848
- const principalId = text(body.principalId) ?? developerId;
11849
- const email = text(body.email);
11850
- const kind = principalKind(body.principalKind, machine);
11851
- const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11852
- const handle = text(body.handle) ?? "";
11853
- const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
11854
- return {
11855
- developerId,
11856
- principalId,
11857
- principalKind: kind,
11858
- displayName,
11859
- handle,
11860
- manager: managerOf(body.manager),
11861
- credential: {
11862
- id: text(credential2.id),
11863
- kind: credentialKind(credential2.kind, machine, scopes)
11864
- },
11865
- email,
11866
- admin: body.admin === true,
11867
- machine,
11868
- scopes
11869
- };
11870
- }
11871
- function credentialLabel(identity) {
11872
- if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11873
- if (identity.credential.kind === "device")
11874
- return identity.scopes.length ? "device (scoped)" : "device";
11875
- if (identity.credential.kind === "clerk") return "clerk";
11876
- return "unknown (legacy server)";
11877
- }
11878
- function namedPrincipal(identity) {
11879
- return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11880
- }
11881
- function namedManager(manager) {
11882
- return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11883
- }
11884
- function accountableOwner(identity) {
11885
- if (identity.principalKind === "agent" && identity.manager) {
11886
- return namedManager(identity.manager);
11887
- }
11888
- if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11889
- return namedPrincipal(identity);
11890
- }
11891
- return identity.email ?? "Unnamed member";
11892
- }
11893
- async function whoamiCommand(parsed, deps = {}) {
11894
- assertArgs(
11895
- parsed,
11896
- ["config", "context", "platform", "token", "email", "json"],
11897
- 1
11898
- );
11899
- const out = deps.stdout ?? console;
11900
- const doFetch = deps.fetch ?? fetch;
11901
- const { cfg } = await resolveOperatorContext(parsed, {
11902
- allowMissingConfig: true
11903
- });
11904
- const token = await getDeveloperToken(
11905
- cfg,
11906
- {
11907
- configPath: cfg.configPath,
11908
- token: stringOpt(parsed.options.token),
11909
- email: stringOpt(parsed.options.email),
11910
- // As above: never suppress the browser for an ordinary sign-in.
11911
- open: void 0
11912
- },
11913
- doFetch,
11914
- out
11915
- );
11916
- const identity = await fetchIdentity(cfg.platformUrl, token, doFetch);
11917
- if (parsed.options.json === true) {
11918
- out.log(JSON.stringify(identity, null, 2));
11919
- return;
11920
- }
11921
- out.log(`platform: ${cfg.platformUrl}`);
11922
- out.log(`principal: ${namedPrincipal(identity)}`);
11923
- out.log(`principal id: ${identity.principalId}`);
11924
- out.log(`kind: ${identity.principalKind}`);
11925
- if (identity.principalKind === "agent")
11926
- out.log(
11927
- `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11928
- );
11929
- out.log(`owner: ${accountableOwner(identity)}`);
11930
- out.log(`owner id: ${identity.developerId}`);
11931
- out.log(`email: ${identity.email ?? "(none)"}`);
11932
- out.log(`credential: ${credentialLabel(identity)}`);
11933
- if (identity.credential.id)
11934
- out.log(`credential id: ${identity.credential.id}`);
11935
- out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11936
- if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11937
- if (!identity.admin) {
11938
- if (identity.scopes.includes("platform:runbook:write")) {
11939
- out.log("\nThis exact scope can read and edit all platform runbook content.");
11940
- out.log("The device token is not ambient admin and cannot change visibility.");
11941
- } else {
11942
- out.log("\nYou can read operator-visible platform runbooks but not edit them.");
11943
- out.log("Use a platform-admin-approved runbook write request to edit.");
11944
- }
11945
- }
11946
- }
11947
-
11948
12123
  // src/runbook-command.ts
11949
12124
  var ALLOWED2 = [
11950
12125
  "config",
@@ -12840,6 +13015,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12840
13015
  await whoamiCommand(parsed, runtime);
12841
13016
  return;
12842
13017
  }
13018
+ if (command === "auth") {
13019
+ await authCommand(parsed, runtime);
13020
+ return;
13021
+ }
12843
13022
  if (command === "context") {
12844
13023
  await contextCommand(parsed, runtime);
12845
13024
  return;
@@ -12880,6 +13059,15 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12880
13059
  await pmCommand(parsed, runtime);
12881
13060
  return;
12882
13061
  }
13062
+ if (command === "bug") {
13063
+ const action2 = parsed.positionals[1] ?? "list";
13064
+ const canonical = action2 === "report" || action2 === "create" ? "add" : action2;
13065
+ await pmCommand({
13066
+ ...parsed,
13067
+ positionals: ["pm", "bug", canonical, ...parsed.positionals.slice(2)]
13068
+ }, runtime);
13069
+ return;
13070
+ }
12883
13071
  if (command === "discuss") {
12884
13072
  await discussCommand(parsed, runtime);
12885
13073
  return;
@@ -12973,6 +13161,7 @@ async function calendarCommand(parsed, dependencies) {
12973
13161
  SYSTEM_AI_PURPOSES,
12974
13162
  acceptedAfter,
12975
13163
  adminAi,
13164
+ aiModels,
12976
13165
  calendarBookingPageUrl,
12977
13166
  calendarCalendars,
12978
13167
  calendarConnect,