@odla-ai/cli 0.25.21 → 0.25.22

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
@@ -47,8 +47,11 @@ __export(index_exports, {
47
47
  calendarServiceConfig: () => calendarServiceConfig,
48
48
  calendarStatus: () => calendarStatus,
49
49
  codeConnect: () => codeConnect,
50
+ configDiff: () => configDiff,
51
+ configPlan: () => configPlan,
50
52
  connectGitHubSecuritySource: () => connectGitHubSecuritySource,
51
53
  describeProblem: () => describeProblem,
54
+ desiredRegistryState: () => desiredRegistryState,
52
55
  disconnectGitHubSecuritySource: () => disconnectGitHubSecuritySource,
53
56
  doctor: () => doctor,
54
57
  exitCodeFor: () => exitCodeFor,
@@ -68,6 +71,7 @@ __export(index_exports, {
68
71
  prepareCodeImages: () => prepareCodeImages,
69
72
  printCapabilities: () => printCapabilities,
70
73
  provision: () => provision,
74
+ reconcileConfig: () => reconcileConfig,
71
75
  redactSecrets: () => redactSecrets,
72
76
  repositoryFromGitRemote: () => repositoryFromGitRemote,
73
77
  runCli: () => runCli,
@@ -103,7 +107,9 @@ function approvalLines(prompt) {
103
107
  lines.push("");
104
108
  lines.push(` ${prompt.approvalUrl}`);
105
109
  lines.push("");
106
- lines.push(" A signed-in odla admin opens that URL, checks the code matches, and approves.");
110
+ lines.push(
111
+ ` ${prompt.approver ?? "The matching signed-in odla account"} opens that URL, checks the code matches, and approves.`
112
+ );
107
113
  lines.push(" Opening the link without approving does nothing; this command waits until they do.");
108
114
  if (prompt.browserAttempted) {
109
115
  lines.push(" A browser launch was attempted, but it is best-effort and may have shown no tab.");
@@ -550,6 +556,7 @@ function audienceBoundEnvToken(token, platform) {
550
556
  }
551
557
  var SCOPE_PURPOSE = {
552
558
  "platform:status:read": "read the platform fleet health and deployment snapshot",
559
+ "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
553
560
  "platform:runbook:write": "add or edit odla's operational runbooks",
554
561
  "platform:ai:policy:write": "change System AI model routing",
555
562
  "platform:ai:policy:read": "read System AI model routing",
@@ -586,6 +593,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
586
593
  approvalUrl,
587
594
  minutesLeft: Math.floor(expiresIn / 60),
588
595
  purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
596
+ approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
589
597
  browserAttempted: browser.open,
590
598
  browserSkipped: browser.reason
591
599
  });
@@ -755,10 +763,10 @@ async function responseBody2(res) {
755
763
  return { message: text.slice(0, 300) };
756
764
  }
757
765
  }
758
- function apiError2(action, status, body) {
766
+ function apiError2(action2, status, body) {
759
767
  const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
760
768
  const message2 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
761
- return `${action} failed (${status}): ${message2}`;
769
+ return `${action2} failed (${status}): ${message2}`;
762
770
  }
763
771
  function isRecord2(value2) {
764
772
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
@@ -936,10 +944,10 @@ async function responseBody3(res) {
936
944
  return { message: text.slice(0, 300) };
937
945
  }
938
946
  }
939
- function apiError3(action, status, body) {
947
+ function apiError3(action2, status, body) {
940
948
  const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
941
949
  const message2 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
942
- return `${action} failed (${status}): ${message2}`;
950
+ return `${action2} failed (${status}): ${message2}`;
943
951
  }
944
952
  function isRecord3(value2) {
945
953
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
@@ -1563,21 +1571,21 @@ var SET_OPTIONS = [
1563
1571
  ];
1564
1572
  async function adminCommand(parsed, deps = {}) {
1565
1573
  const area = parsed.positionals[1];
1566
- const action = parsed.positionals[2];
1567
- const credentialSet = action === "credential" && parsed.positionals[3] === "set";
1568
- const credentials = action === "credentials";
1569
- const models = action === "models";
1570
- const usage = action === "usage";
1571
- const audit = action === "audit";
1572
- if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
1574
+ const action2 = parsed.positionals[2];
1575
+ const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
1576
+ const credentials = action2 === "credentials";
1577
+ const models = action2 === "models";
1578
+ const usage = action2 === "usage";
1579
+ const audit = action2 === "audit";
1580
+ if (area !== "ai" || action2 !== "show" && action2 !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
1573
1581
  throw new Error('unknown admin command. Try "odla-ai admin ai show".');
1574
1582
  }
1575
- const allowed = credentialSet ? [...CONTEXT_OPTIONS, "from-env", "stdin"] : action === "set" ? SET_OPTIONS : models ? [...JSON_OPTIONS, "provider"] : usage ? [...JSON_OPTIONS, "app-id", "env", "run-id", "limit"] : audit ? [...JSON_OPTIONS, "limit"] : JSON_OPTIONS;
1576
- assertArgs(parsed, allowed, credentialSet ? 5 : action === "set" ? 4 : 3);
1583
+ const allowed = credentialSet ? [...CONTEXT_OPTIONS, "from-env", "stdin"] : action2 === "set" ? SET_OPTIONS : models ? [...JSON_OPTIONS, "provider"] : usage ? [...JSON_OPTIONS, "app-id", "env", "run-id", "limit"] : audit ? [...JSON_OPTIONS, "limit"] : JSON_OPTIONS;
1584
+ assertArgs(parsed, allowed, credentialSet ? 5 : action2 === "set" ? 4 : 3);
1577
1585
  const context = await resolveOperatorContext(parsed, { allowMissingConfig: true });
1578
1586
  await adminAi({
1579
- action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
1580
- purpose: action === "set" ? parsed.positionals[3] : void 0,
1587
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action2,
1588
+ purpose: action2 === "set" ? parsed.positionals[3] : void 0,
1581
1589
  provider: stringOpt(parsed.options.provider),
1582
1590
  model: stringOpt(parsed.options.model),
1583
1591
  enabled: boolOpt(parsed.options.enabled),
@@ -1634,12 +1642,12 @@ function bothTenants(cfg) {
1634
1642
 
1635
1643
  // src/agent-command.ts
1636
1644
  async function agentCommand(parsed, deps = {}) {
1637
- const action = parsed.positionals[1];
1638
- if (action !== "jobs" && action !== "retry") {
1639
- throw new Error(`unknown agent action "${action ?? ""}". Try "odla-ai agent jobs --json".`);
1645
+ const action2 = parsed.positionals[1];
1646
+ if (action2 !== "jobs" && action2 !== "retry") {
1647
+ throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
1640
1648
  }
1641
- assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action === "jobs" ? 2 : 3);
1642
- if (action === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
1649
+ assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
1650
+ if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
1643
1651
  throw new Error('--state and --limit are supported only by "agent jobs"');
1644
1652
  }
1645
1653
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
@@ -1659,7 +1667,7 @@ async function agentCommand(parsed, deps = {}) {
1659
1667
  );
1660
1668
  const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
1661
1669
  const headers = { authorization: `Bearer ${credential2}` };
1662
- if (action === "retry") {
1670
+ if (action2 === "retry") {
1663
1671
  const id = parsed.positionals[2];
1664
1672
  const res2 = await doFetch(`${base}/${encodeURIComponent(id)}/retry`, { method: "POST", headers });
1665
1673
  const body2 = await readJson(res2);
@@ -2095,7 +2103,7 @@ async function copyFiles(cfg, token, route2, doFetch, out) {
2095
2103
  }
2096
2104
 
2097
2105
  // src/app-lifecycle.ts
2098
- async function lifecycleCall(action, options) {
2106
+ async function lifecycleCall(action2, options) {
2099
2107
  const cfg = await loadProjectConfig(options.configPath);
2100
2108
  const out = options.stdout ?? console;
2101
2109
  const doFetch = options.fetch ?? fetch;
@@ -2105,13 +2113,13 @@ async function lifecycleCall(action, options) {
2105
2113
  doFetch,
2106
2114
  out
2107
2115
  );
2108
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action}`, {
2116
+ const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
2109
2117
  method: "POST",
2110
2118
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
2111
2119
  });
2112
2120
  const body = await res.json().catch(() => ({}));
2113
2121
  if (!res.ok || !body.ok) {
2114
- throw new Error(`${action} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
2122
+ throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
2115
2123
  }
2116
2124
  return body;
2117
2125
  }
@@ -2664,8 +2672,8 @@ function pollTimeout(value2 = 10 * 6e4) {
2664
2672
  if (!Number.isSafeInteger(value2) || value2 < 1e3 || value2 > 60 * 6e4) throw new Error("calendar poll timeout must be 1000-3600000ms");
2665
2673
  return value2;
2666
2674
  }
2667
- function productionConsent(env, yes, action) {
2668
- if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action} for "${env}" without --yes`);
2675
+ function productionConsent(env, yes, action2) {
2676
+ if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action2} for "${env}" without --yes`);
2669
2677
  }
2670
2678
  function printStatus(status, json, out) {
2671
2679
  if (json) {
@@ -2695,6 +2703,7 @@ var CAPABILITIES = {
2695
2703
  "save and explicitly select non-secret named operator contexts with isolated credential caches; resolve and explain platform, app, environment, and credential provenance without authenticating; then run PM, Discussions, o11y, runbook, and identity operations outside a project checkout",
2696
2704
  "read one versioned o11y status envelope spanning application RED, exact Worker versions and Cloudflare colos observed in traffic, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, account-scoped Durable Object, D1, and R2 evidence under odla-db, and a bounded machine verdict",
2697
2705
  "read one canonical platform fleet snapshot over private service bindings, including release identities, probe latency, Cloudflare load/runtime freshness, explicit unknowns, and stable next actions through a read-only capability",
2706
+ "compare one project's checked-in Registry intent with live owner-visible state and freeze a secret-free plan digest through an exact app:config:read capability",
2698
2707
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
2699
2708
  "run app-attributed hosted security discovery and independent validation without provider keys",
2700
2709
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -2741,15 +2750,542 @@ function printGroup(out, heading, items) {
2741
2750
  out.log("");
2742
2751
  }
2743
2752
 
2753
+ // src/config-reconcile-command.ts
2754
+ var import_apps6 = require("@odla-ai/apps");
2755
+ var import_node_path7 = require("path");
2756
+
2757
+ // src/config-reconcile-digest.ts
2758
+ var import_node_crypto = require("crypto");
2759
+ function canonicalJson(value2) {
2760
+ return JSON.stringify(canonicalValue(value2));
2761
+ }
2762
+ function configDigest(value2) {
2763
+ return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2764
+ }
2765
+ function canonicalValue(value2) {
2766
+ if (Array.isArray(value2)) return value2.map(canonicalValue);
2767
+ if (value2 && typeof value2 === "object") {
2768
+ return Object.fromEntries(
2769
+ Object.entries(value2).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
2770
+ );
2771
+ }
2772
+ return value2;
2773
+ }
2774
+
2775
+ // src/config-reconcile-desired.ts
2776
+ var import_apps4 = require("@odla-ai/apps");
2777
+
2778
+ // src/provision-helpers.ts
2779
+ var import_ai = require("@odla-ai/ai");
2780
+ var import_apps3 = require("@odla-ai/apps");
2781
+ function defaultSecretName(provider) {
2782
+ const names = import_ai.DEFAULT_SECRET_NAMES;
2783
+ return names[provider] ?? `${provider}_api_key`;
2784
+ }
2785
+ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2786
+ const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
2787
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
2788
+ headers: { authorization: `Bearer ${token}` }
2789
+ });
2790
+ if (res.ok || res.status === 404) return;
2791
+ if (res.status === 403) {
2792
+ throw new Error(
2793
+ `${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
2794
+ );
2795
+ }
2796
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
2797
+ }
2798
+ async function postJson(doFetch, url, bearer, body) {
2799
+ const res = await doFetch(url, {
2800
+ method: "POST",
2801
+ headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
2802
+ body: JSON.stringify(body)
2803
+ });
2804
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
2805
+ }
2806
+ function normalizeClerkConfig(value2) {
2807
+ if (!value2) return null;
2808
+ if (typeof value2 === "string") {
2809
+ const publishableKey2 = envValue(value2);
2810
+ return publishableKey2 ? { publishableKey: publishableKey2 } : null;
2811
+ }
2812
+ if (typeof value2 !== "object") return null;
2813
+ const cfg = value2;
2814
+ const publishableKey = envValue(cfg.publishableKey);
2815
+ return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
2816
+ }
2817
+ async function safeText3(res) {
2818
+ try {
2819
+ return redactSecrets((await res.text()).slice(0, 500));
2820
+ } catch {
2821
+ return "";
2822
+ }
2823
+ }
2824
+
2825
+ // src/config-reconcile-desired.ts
2826
+ function desiredRegistryState(cfg) {
2827
+ const environments = {};
2828
+ const services = (0, import_apps4.orderAppServices)(cfg.services);
2829
+ for (const env of cfg.envs) {
2830
+ const desiredServices = {};
2831
+ for (const service of services) {
2832
+ desiredServices[service] = {
2833
+ enabled: true,
2834
+ ...managedServiceConfig(cfg, env, service)
2835
+ };
2836
+ }
2837
+ const authoredAuth = cfg.auth?.clerk && Object.hasOwn(cfg.auth.clerk, env) ? cfg.auth.clerk[env] : void 0;
2838
+ const auth = normalizeClerkConfig(authoredAuth);
2839
+ if (authoredAuth && !auth) {
2840
+ throw new Error(`auth.clerk.${env} could not resolve its publishable key`);
2841
+ }
2842
+ const linkManaged = !!cfg.links && Object.hasOwn(cfg.links, env);
2843
+ environments[env] = {
2844
+ services: desiredServices,
2845
+ ...auth ? { auth } : {},
2846
+ ...linkManaged ? { link: cfg.links?.[env] ?? null } : {}
2847
+ };
2848
+ }
2849
+ return {
2850
+ appId: cfg.app.id,
2851
+ name: cfg.app.name,
2852
+ environments
2853
+ };
2854
+ }
2855
+ function managedServiceConfig(cfg, env, service) {
2856
+ if (service === "ai" && cfg.ai?.provider) {
2857
+ return {
2858
+ config: {
2859
+ provider: cfg.ai.provider,
2860
+ ...cfg.ai.model ? { model: cfg.ai.model } : {}
2861
+ }
2862
+ };
2863
+ }
2864
+ if (service === "calendar") return { config: calendarServiceConfig(cfg, env) };
2865
+ return {};
2866
+ }
2867
+
2868
+ // src/config-reconcile.ts
2869
+ var import_apps5 = require("@odla-ai/apps");
2870
+
2871
+ // src/config-reconcile-values.ts
2872
+ function difference(path, desired, observed, status, reason, desiredSource, observedSource, env, service) {
2873
+ return { path, env, service, status, desired, observed, desiredSource, observedSource, reason };
2874
+ }
2875
+ function action(kind, path, before, after, risk, applySupport, reason) {
2876
+ return { kind, path, before, after, risk, requiresApproval: false, applySupport, reason };
2877
+ }
2878
+ function scopedAction(kind, path, env, service, before, after, reason, destructive = false) {
2879
+ const production = env === "prod" || env === "production";
2880
+ return {
2881
+ ...action(
2882
+ kind,
2883
+ path,
2884
+ before,
2885
+ after,
2886
+ destructive ? "high" : production ? "medium" : "low",
2887
+ destructive ? "studio" : "provision",
2888
+ reason
2889
+ ),
2890
+ env,
2891
+ ...service ? { service } : {},
2892
+ requiresApproval: production || destructive
2893
+ };
2894
+ }
2895
+ function observedProjection(app) {
2896
+ return {
2897
+ appId: app.appId,
2898
+ name: app.name,
2899
+ archivedAt: app.archivedAt,
2900
+ environments: Object.fromEntries(
2901
+ Object.entries(app.environments).sort(([left], [right]) => left.localeCompare(right)).map(([env, services]) => [
2902
+ env,
2903
+ Object.fromEntries(Object.entries(services).sort(([left], [right]) => left.localeCompare(right)))
2904
+ ])
2905
+ ),
2906
+ auth: app.auth,
2907
+ links: app.links
2908
+ };
2909
+ }
2910
+ function projectConfig(value2, keys) {
2911
+ return Object.fromEntries(keys.filter((key) => value2[key] !== void 0).map((key) => [key, value2[key]]));
2912
+ }
2913
+ function same(left, right) {
2914
+ return canonicalJson(left) === canonicalJson(right);
2915
+ }
2916
+ function actionId(actionValue) {
2917
+ return `action-${configDigest(actionValue).slice("sha256:".length, "sha256:".length + 16)}`;
2918
+ }
2919
+ function quoteArg(value2) {
2920
+ return `'${value2.replace(/'/g, `'\\''`)}'`;
2921
+ }
2922
+
2923
+ // src/config-reconcile.ts
2924
+ var COVERAGE = {
2925
+ included: [
2926
+ "app display name",
2927
+ "environment service enablement",
2928
+ "AI provider/model and calendar booking service configuration",
2929
+ "auth configuration when explicitly declared",
2930
+ "deployment link when explicitly declared"
2931
+ ],
2932
+ excluded: [
2933
+ { path: "db.schema", reason: "owned by the database data plane" },
2934
+ { path: "db.rules", reason: "owned by the database data plane" },
2935
+ { path: "integrations.seeds", reason: "guarded data-plane writes are not Registry state" },
2936
+ { path: "credentials", reason: "shown-once credentials are intentionally local" },
2937
+ { path: "secrets", reason: "vault and Worker secrets are write-only" },
2938
+ { path: "calendar.connection", reason: "OAuth connection state is provider-owned" },
2939
+ { path: "service.config.opaque", reason: "controller-assigned fields remain service-owned" }
2940
+ ]
2941
+ };
2942
+ function reconcileConfig(input) {
2943
+ const desiredSource = { kind: "project_config", location: input.configPath };
2944
+ const observedSource = {
2945
+ kind: "registry",
2946
+ location: `${input.platformUrl}/registry/apps/${encodeURIComponent(input.desired.appId)}`
2947
+ };
2948
+ const differences = [];
2949
+ const actions = [];
2950
+ const add = (difference2, action2) => {
2951
+ differences.push(difference2);
2952
+ if (action2) actions.push({ ...action2, id: actionId(action2) });
2953
+ };
2954
+ compareApp(input.desired, input.observed, desiredSource, observedSource, add);
2955
+ compareEnvironments(input.desired, input.observed, desiredSource, observedSource, add);
2956
+ const desiredRevision = configDigest(input.desired);
2957
+ const observedRevision = input.observed ? configDigest(observedProjection(input.observed)) : null;
2958
+ const different = differences.filter((entry) => entry.status === "different").length;
2959
+ const unmanaged = differences.length - different;
2960
+ return {
2961
+ generatedAt: input.generatedAt,
2962
+ scope: {
2963
+ appId: input.desired.appId,
2964
+ platformUrl: input.platformUrl,
2965
+ environments: Object.keys(input.desired.environments).sort()
2966
+ },
2967
+ sources: { desired: desiredSource, observed: observedSource },
2968
+ desiredRevision,
2969
+ observedRevision,
2970
+ status: different ? "different" : unmanaged ? "unmanaged" : "in_sync",
2971
+ summary: {
2972
+ differences: different,
2973
+ unmanaged,
2974
+ actions: actions.length,
2975
+ productionActions: actions.filter((action2) => action2.env === "prod" || action2.env === "production").length,
2976
+ highRiskActions: actions.filter((action2) => action2.risk === "high").length
2977
+ },
2978
+ coverage: COVERAGE,
2979
+ differences,
2980
+ actions
2981
+ };
2982
+ }
2983
+ function compareApp(desired, observed, desiredSource, observedSource, add) {
2984
+ if (!observed) {
2985
+ const path = "app";
2986
+ add(
2987
+ difference(path, desired, null, "different", "the app is absent from Registry", desiredSource, observedSource),
2988
+ action(
2989
+ "create_app",
2990
+ path,
2991
+ null,
2992
+ { appId: desired.appId, name: desired.name },
2993
+ "low",
2994
+ "provision",
2995
+ "create the repository-declared app"
2996
+ )
2997
+ );
2998
+ return;
2999
+ }
3000
+ if (observed.name !== desired.name) {
3001
+ const path = "app.name";
3002
+ add(
3003
+ difference(path, desired.name, observed.name, "different", "the checked-in display name differs", desiredSource, observedSource),
3004
+ {
3005
+ ...action("rename_app", path, observed.name, desired.name, "low", "command", "make Registry match the checked-in display name"),
3006
+ command: `odla-ai app rename ${quoteArg(desired.name)} --config ${quoteArg(desiredSource.location)}`
3007
+ }
3008
+ );
3009
+ }
3010
+ }
3011
+ function compareEnvironments(desired, observed, desiredSource, observedSource, add) {
3012
+ const envs = /* @__PURE__ */ new Set([
3013
+ ...Object.keys(desired.environments),
3014
+ ...Object.keys(observed?.environments ?? {}).filter((env) => Object.values(observed?.environments[env] ?? {}).some((service) => service.enabled))
3015
+ ]);
3016
+ for (const env of [...envs].sort()) {
3017
+ compareServices(env, desired, observed, desiredSource, observedSource, add);
3018
+ compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add);
3019
+ }
3020
+ }
3021
+ function compareServices(env, desired, observed, desiredSource, observedSource, add) {
3022
+ const wanted = desired.environments[env]?.services ?? {};
3023
+ const live = observed?.environments[env] ?? {};
3024
+ const knownOrder = (0, import_apps5.orderAppServices)((0, import_apps5.appServiceIds)());
3025
+ const services = [.../* @__PURE__ */ new Set([...knownOrder, ...Object.keys(wanted), ...Object.keys(live)])];
3026
+ for (const service of services) {
3027
+ const next = wanted[service];
3028
+ const current = live[service];
3029
+ const path = `environments.${env}.services.${service}`;
3030
+ if (next?.enabled && !current?.enabled) {
3031
+ add(
3032
+ difference(path, next, current ?? null, "different", "the repository enables a service Registry does not", desiredSource, observedSource, env, service),
3033
+ scopedAction("enable_service", path, env, service, current ?? null, next, "enable the repository-declared service")
3034
+ );
3035
+ continue;
3036
+ }
3037
+ if (next?.enabled && current?.enabled && next.config) {
3038
+ const projected = projectConfig(current.config, Object.keys(next.config));
3039
+ if (!same(next.config, projected)) {
3040
+ add(
3041
+ difference(`${path}.config`, next.config, projected, "different", "repository-managed service settings differ", desiredSource, observedSource, env, service),
3042
+ scopedAction(
3043
+ "configure_service",
3044
+ `${path}.config`,
3045
+ env,
3046
+ service,
3047
+ projected,
3048
+ next.config,
3049
+ "apply the repository-managed service settings"
3050
+ )
3051
+ );
3052
+ }
3053
+ }
3054
+ }
3055
+ for (const service of [...services].reverse()) {
3056
+ const next = wanted[service];
3057
+ const current = live[service];
3058
+ if (next?.enabled || !current?.enabled) continue;
3059
+ const path = `environments.${env}.services.${service}`;
3060
+ add(
3061
+ difference(path, null, { enabled: true }, "different", "Registry enables a service absent from repository intent", desiredSource, observedSource, env, service),
3062
+ scopedAction(
3063
+ "disable_service",
3064
+ path,
3065
+ env,
3066
+ service,
3067
+ { enabled: true },
3068
+ { enabled: false },
3069
+ "disablement is destructive to availability and requires an explicit Studio review",
3070
+ true
3071
+ )
3072
+ );
3073
+ }
3074
+ }
3075
+ function compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add) {
3076
+ const wanted = desired.environments[env];
3077
+ const liveAuth = observed?.auth[env] ?? null;
3078
+ if (wanted?.auth) {
3079
+ const projected = liveAuth ? projectConfig(liveAuth, Object.keys(wanted.auth)) : null;
3080
+ if (!same(wanted.auth, projected)) {
3081
+ const path = `environments.${env}.auth`;
3082
+ add(
3083
+ difference(path, wanted.auth, projected, "different", "explicit repository auth settings differ", desiredSource, observedSource, env),
3084
+ scopedAction("set_auth", path, env, void 0, projected, wanted.auth, "apply explicit repository auth settings")
3085
+ );
3086
+ }
3087
+ } else if (liveAuth) {
3088
+ add(difference(
3089
+ `environments.${env}.auth`,
3090
+ null,
3091
+ projectConfig(liveAuth, ["publishableKey", "audience", "mode"]),
3092
+ "unmanaged",
3093
+ "live auth exists but this project config does not manage it",
3094
+ desiredSource,
3095
+ observedSource,
3096
+ env
3097
+ ));
3098
+ }
3099
+ const managesLink = wanted && Object.hasOwn(wanted, "link");
3100
+ const liveLink = observed?.links[env] ?? null;
3101
+ if (managesLink && wanted.link !== liveLink) {
3102
+ const path = `environments.${env}.link`;
3103
+ add(
3104
+ difference(path, wanted.link ?? null, liveLink, "different", "explicit repository deployment link differs", desiredSource, observedSource, env),
3105
+ scopedAction("set_link", path, env, void 0, liveLink, wanted.link ?? null, "apply the repository deployment link")
3106
+ );
3107
+ } else if (!managesLink && liveLink) {
3108
+ add(difference(
3109
+ `environments.${env}.link`,
3110
+ null,
3111
+ liveLink,
3112
+ "unmanaged",
3113
+ "a live deployment link exists but this project config does not manage it",
3114
+ desiredSource,
3115
+ observedSource,
3116
+ env
3117
+ ));
3118
+ }
3119
+ }
3120
+
3121
+ // src/config-reconcile-command.ts
3122
+ async function configDiff(options) {
3123
+ const reconciliation = await inspectConfig(options);
3124
+ const { actions: _actions, ...read3 } = reconciliation;
3125
+ const document2 = {
3126
+ schemaVersion: "odla.config-diff/v1",
3127
+ ...read3,
3128
+ nextActions: diffNextActions(reconciliation, options.configPath)
3129
+ };
3130
+ printDiff(document2, options);
3131
+ return document2;
3132
+ }
3133
+ async function configPlan(options) {
3134
+ const reconciliation = await inspectConfig(options);
3135
+ const planDigest = configDigest({
3136
+ schemaVersion: "odla.config-plan/v1",
3137
+ desiredRevision: reconciliation.desiredRevision,
3138
+ observedRevision: reconciliation.observedRevision,
3139
+ actions: reconciliation.actions
3140
+ });
3141
+ const document2 = {
3142
+ schemaVersion: "odla.config-plan/v1",
3143
+ ...reconciliation,
3144
+ planDigest,
3145
+ apply: {
3146
+ supported: false,
3147
+ reason: "conditional, resumable config apply is not available yet; use the exact reviewed commands below"
3148
+ },
3149
+ nextActions: planNextActions(reconciliation, options.configPath)
3150
+ };
3151
+ printPlan2(document2, options);
3152
+ return document2;
3153
+ }
3154
+ async function inspectConfig(options) {
3155
+ const out = options.stdout ?? console;
3156
+ const cfg = await loadProjectConfig(options.configPath);
3157
+ const doFetch = options.fetch ?? fetch;
3158
+ const token = await resolveAdminPlatformToken({
3159
+ platform: cfg.platformUrl,
3160
+ scope: "app:config:read",
3161
+ token: options.token,
3162
+ tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3163
+ rootDir: cfg.rootDir,
3164
+ email: options.email,
3165
+ open: options.open,
3166
+ fetch: doFetch,
3167
+ stdout: out,
3168
+ openApprovalUrl: options.openApprovalUrl,
3169
+ label: `odla CLI (${cfg.app.id} config read)`
3170
+ });
3171
+ const client = (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3172
+ const observed = await client.resolveApp(cfg.app.id);
3173
+ return reconcileConfig({
3174
+ desired: desiredRegistryState(cfg),
3175
+ observed,
3176
+ configPath: cfg.configPath,
3177
+ platformUrl: cfg.platformUrl,
3178
+ generatedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
3179
+ });
3180
+ }
3181
+ function printDiff(document2, options) {
3182
+ const out = options.stdout ?? console;
3183
+ if (options.json) {
3184
+ out.log(JSON.stringify(document2, null, 2));
3185
+ return;
3186
+ }
3187
+ printHeader(out, "diff", document2);
3188
+ printDifferences(out, document2);
3189
+ printNext(out, document2.nextActions);
3190
+ }
3191
+ function printPlan2(document2, options) {
3192
+ const out = options.stdout ?? console;
3193
+ if (options.json) {
3194
+ out.log(JSON.stringify(document2, null, 2));
3195
+ return;
3196
+ }
3197
+ printHeader(out, "plan", document2);
3198
+ for (const entry of document2.actions) {
3199
+ const scope = [entry.env, entry.service].filter(Boolean).join("/");
3200
+ out.log(
3201
+ ` ${entry.id} ${entry.kind}${scope ? ` (${scope})` : ""} ${entry.risk}${entry.requiresApproval ? ", approval required" : ""}`
3202
+ );
3203
+ out.log(` ${entry.reason}`);
3204
+ }
3205
+ if (!document2.actions.length) out.log(" no managed changes");
3206
+ out.log(`plan digest: ${document2.planDigest}`);
3207
+ out.log(`apply: unsupported \u2014 ${document2.apply.reason}`);
3208
+ printNext(out, document2.nextActions);
3209
+ }
3210
+ function printHeader(out, kind, document2) {
3211
+ out.log(`config ${kind}: ${document2.scope.appId} \u2014 ${document2.status}`);
3212
+ out.log(`platform: ${document2.scope.platformUrl}`);
3213
+ out.log(`desired: ${document2.desiredRevision}`);
3214
+ out.log(`observed: ${document2.observedRevision ?? "absent"}`);
3215
+ out.log(
3216
+ `summary: ${document2.summary.differences} different, ${document2.summary.unmanaged} unmanaged, ${document2.summary.actions} planned actions`
3217
+ );
3218
+ }
3219
+ function printDifferences(out, document2) {
3220
+ for (const entry of document2.differences) {
3221
+ out.log(` ${entry.status} ${entry.path} ${entry.reason}`);
3222
+ }
3223
+ if (!document2.differences.length) out.log(" managed Registry configuration is in sync");
3224
+ }
3225
+ function printNext(out, next) {
3226
+ if (!next.length) return;
3227
+ out.log("next:");
3228
+ for (const item of next) out.log(` ${item.command}
3229
+ ${item.description}`);
3230
+ }
3231
+ function diffNextActions(reconciliation, configPath) {
3232
+ if (reconciliation.status === "in_sync") {
3233
+ return [{
3234
+ code: "verify_runtime",
3235
+ command: `odla-ai smoke --config ${quoteArg2(configPath)}`,
3236
+ description: "Registry intent matches; verify the service-owned data planes separately."
3237
+ }];
3238
+ }
3239
+ return [{
3240
+ code: "freeze_plan",
3241
+ command: `odla-ai config plan --config ${quoteArg2(configPath)} --json`,
3242
+ description: "Freeze the current desired and observed revisions into a reviewable action plan."
3243
+ }];
3244
+ }
3245
+ function planNextActions(reconciliation, configPath) {
3246
+ const next = [];
3247
+ if (reconciliation.actions.some((action2) => action2.applySupport === "provision")) {
3248
+ next.push({
3249
+ code: "review_provision",
3250
+ command: `odla-ai provision --config ${quoteArg2(configPath)} --dry-run`,
3251
+ description: "Review the existing provisioner's service/auth/link work before applying it."
3252
+ });
3253
+ }
3254
+ for (const action2 of reconciliation.actions.filter((entry) => entry.command)) {
3255
+ if (!next.some((entry) => entry.command === action2.command)) {
3256
+ next.push({ code: action2.kind, command: action2.command, description: action2.reason });
3257
+ }
3258
+ }
3259
+ if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3260
+ const { appId, platformUrl } = reconciliation.scope;
3261
+ next.push({
3262
+ code: "review_destructive",
3263
+ command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3264
+ description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3265
+ });
3266
+ }
3267
+ if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3268
+ next.push({
3269
+ code: "declare_or_accept_runtime_state",
3270
+ command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3271
+ description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3272
+ });
3273
+ }
3274
+ return next;
3275
+ }
3276
+ function quoteArg2(value2) {
3277
+ return `'${value2.replace(/'/g, `'\\''`)}'`;
3278
+ }
3279
+
2744
3280
  // src/doctor-checks.ts
2745
3281
  var import_node_child_process3 = require("child_process");
2746
3282
  var import_node_fs10 = require("fs");
2747
- var import_node_path8 = require("path");
3283
+ var import_node_path9 = require("path");
2748
3284
 
2749
3285
  // src/wrangler.ts
2750
3286
  var import_node_child_process2 = require("child_process");
2751
3287
  var import_node_fs9 = require("fs");
2752
- var import_node_path7 = require("path");
3288
+ var import_node_path8 = require("path");
2753
3289
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
2754
3290
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
2755
3291
  let stdout = "";
@@ -2763,7 +3299,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
2763
3299
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
2764
3300
  function findWranglerConfig(rootDir) {
2765
3301
  for (const name of WRANGLER_CONFIG_FILES) {
2766
- const path = (0, import_node_path7.join)(rootDir, name);
3302
+ const path = (0, import_node_path8.join)(rootDir, name);
2767
3303
  if ((0, import_node_fs9.existsSync)(path)) return path;
2768
3304
  }
2769
3305
  return null;
@@ -2829,11 +3365,11 @@ function lintRules(rules, entities, publicRead) {
2829
3365
  const warnings = [];
2830
3366
  if (!rules) return warnings;
2831
3367
  for (const [ns, actions] of Object.entries(rules)) {
2832
- for (const [action, expr] of Object.entries(actions)) {
3368
+ for (const [action2, expr] of Object.entries(actions)) {
2833
3369
  if (typeof expr !== "string" || expr.trim() !== "true") continue;
2834
- if (action === "view" && publicRead.includes(ns)) continue;
3370
+ if (action2 === "view" && publicRead.includes(ns)) continue;
2835
3371
  warnings.push(
2836
- action === "view" ? `rules.${ns}.view is "true" \u2014 public read; add "${ns}" to db.publicRead if intended` : `rules.${ns}.${action} is "true" \u2014 any client can ${action} ${ns} rows`
3372
+ action2 === "view" ? `rules.${ns}.view is "true" \u2014 public read; add "${ns}" to db.publicRead if intended` : `rules.${ns}.${action2} is "true" \u2014 any client can ${action2} ${ns} rows`
2837
3373
  );
2838
3374
  }
2839
3375
  }
@@ -2869,10 +3405,10 @@ function wranglerWarnings(rootDir) {
2869
3405
  for (const { label, block } of blocks) {
2870
3406
  const assets = block.assets;
2871
3407
  if (assets?.directory) {
2872
- const dir = (0, import_node_path8.resolve)(rootDir, assets.directory);
2873
- if (dir === (0, import_node_path8.resolve)(rootDir)) {
3408
+ const dir = (0, import_node_path9.resolve)(rootDir, assets.directory);
3409
+ if (dir === (0, import_node_path9.resolve)(rootDir)) {
2874
3410
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
2875
- } else if ((0, import_node_fs10.existsSync)((0, import_node_path8.join)(dir, "node_modules"))) {
3411
+ } else if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(dir, "node_modules"))) {
2876
3412
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
2877
3413
  }
2878
3414
  }
@@ -2907,7 +3443,7 @@ function o11yProjectWarnings(rootDir) {
2907
3443
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
2908
3444
  return warnings;
2909
3445
  }
2910
- const main = typeof config.main === "string" ? (0, import_node_path8.resolve)(rootDir, config.main) : null;
3446
+ const main = typeof config.main === "string" ? (0, import_node_path9.resolve)(rootDir, config.main) : null;
2911
3447
  if (!main || !(0, import_node_fs10.existsSync)(main)) {
2912
3448
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
2913
3449
  } else {
@@ -2937,7 +3473,7 @@ function calendarProjectWarnings(rootDir) {
2937
3473
  }
2938
3474
  function readPackageJson(rootDir) {
2939
3475
  try {
2940
- return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(rootDir, "package.json"), "utf8"));
3476
+ return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path9.join)(rootDir, "package.json"), "utf8"));
2941
3477
  } catch {
2942
3478
  return null;
2943
3479
  }
@@ -2993,8 +3529,8 @@ function integrationWarnings(integrations, schema, rules) {
2993
3529
  warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
2994
3530
  continue;
2995
3531
  }
2996
- for (const action of ["view", "create", "update", "delete"]) {
2997
- if (rule[action] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action}" is missing`);
3532
+ for (const action2 of ["view", "create", "update", "delete"]) {
3533
+ if (rule[action2] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action2}" is missing`);
2998
3534
  }
2999
3535
  }
3000
3536
  for (const seed of integration.seeds ?? []) {
@@ -3165,12 +3701,12 @@ function harnessOption(value2, flag) {
3165
3701
 
3166
3702
  // src/init.ts
3167
3703
  var import_node_fs11 = require("fs");
3168
- var import_node_path9 = require("path");
3169
- var import_apps3 = require("@odla-ai/apps");
3704
+ var import_node_path10 = require("path");
3705
+ var import_apps7 = require("@odla-ai/apps");
3170
3706
  function initProject(options) {
3171
3707
  const out = options.stdout ?? console;
3172
- const rootDir = (0, import_node_path9.resolve)(options.rootDir ?? process.cwd());
3173
- const configPath = (0, import_node_path9.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
3708
+ const rootDir = (0, import_node_path10.resolve)(options.rootDir ?? process.cwd());
3709
+ const configPath = (0, import_node_path10.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
3174
3710
  if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
3175
3711
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
3176
3712
  }
@@ -3180,19 +3716,19 @@ function initProject(options) {
3180
3716
  const envs = options.envs?.length ? options.envs : ["dev"];
3181
3717
  const services = options.services?.length ? options.services : ["db", "ai"];
3182
3718
  for (const service of services) {
3183
- const definition = (0, import_apps3.appServiceDefinition)(service);
3184
- if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps3.appServiceIds)().join(", ")})`);
3719
+ const definition = (0, import_apps7.appServiceDefinition)(service);
3720
+ if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps7.appServiceIds)().join(", ")})`);
3185
3721
  for (const dependency of definition.requires) {
3186
3722
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
3187
3723
  }
3188
3724
  }
3189
3725
  const aiProvider = options.aiProvider ?? "anthropic";
3190
- (0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(configPath), { recursive: true });
3191
- (0, import_node_fs11.mkdirSync)((0, import_node_path9.resolve)(rootDir, "src/odla"), { recursive: true });
3192
- (0, import_node_fs11.mkdirSync)((0, import_node_path9.resolve)(rootDir, ".odla"), { recursive: true });
3726
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true });
3727
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, "src/odla"), { recursive: true });
3728
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, ".odla"), { recursive: true });
3193
3729
  (0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
3194
- writeIfMissing((0, import_node_path9.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
3195
- writeIfMissing((0, import_node_path9.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
3730
+ writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
3731
+ writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
3196
3732
  ensureGitignore(rootDir);
3197
3733
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
3198
3734
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
@@ -3374,8 +3910,8 @@ function assertWranglerConfig(cfg) {
3374
3910
  }
3375
3911
 
3376
3912
  // src/secrets-set.ts
3377
- var import_ai = require("@odla-ai/ai");
3378
- var import_apps4 = require("@odla-ai/apps");
3913
+ var import_ai2 = require("@odla-ai/ai");
3914
+ var import_apps8 = require("@odla-ai/apps");
3379
3915
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
3380
3916
  async function secretsSet(options) {
3381
3917
  const name = (options.name ?? "").trim();
@@ -3386,7 +3922,7 @@ async function secretsSet(options) {
3386
3922
  const { cfg, tenantId, value: value2, doFetch, out } = await resolveVaultWrite(options);
3387
3923
  const token = await getDeveloperToken(cfg, options, doFetch, out);
3388
3924
  try {
3389
- await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
3925
+ await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
3390
3926
  } catch (err) {
3391
3927
  throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value2));
3392
3928
  }
@@ -3427,13 +3963,13 @@ async function resolveVaultWrite(options) {
3427
3963
  throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
3428
3964
  }
3429
3965
  const value2 = await secretInputValue(options, "secret");
3430
- return { cfg, tenantId: (0, import_apps4.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
3966
+ return { cfg, tenantId: (0, import_apps8.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
3431
3967
  }
3432
3968
 
3433
3969
  // src/skill.ts
3434
3970
  var import_node_fs12 = require("fs");
3435
3971
  var import_node_os2 = require("os");
3436
- var import_node_path10 = require("path");
3972
+ var import_node_path11 = require("path");
3437
3973
  var import_node_url2 = require("url");
3438
3974
 
3439
3975
  // src/skill-adapters.ts
@@ -3512,8 +4048,8 @@ function installSkill(options = {}) {
3512
4048
  const files = listFiles(sourceDir);
3513
4049
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
3514
4050
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
3515
- const root = (0, import_node_path10.resolve)(options.dir ?? process.cwd());
3516
- const home = (0, import_node_path10.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
4051
+ const root = (0, import_node_path11.resolve)(options.dir ?? process.cwd());
4052
+ const home = (0, import_node_path11.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
3517
4053
  const plans = /* @__PURE__ */ new Map();
3518
4054
  const targets = /* @__PURE__ */ new Map();
3519
4055
  const rememberTarget = (harness, target) => {
@@ -3527,48 +4063,48 @@ function installSkill(options = {}) {
3527
4063
  plans.set(target, { target, content: content2, boundary, managedMerge });
3528
4064
  };
3529
4065
  const planSkillTree = (targetDir2, boundary = root) => {
3530
- for (const rel of files) plan((0, import_node_path10.join)(targetDir2, rel), (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, rel), "utf8"), false, boundary);
4066
+ for (const rel of files) plan((0, import_node_path11.join)(targetDir2, rel), (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, rel), "utf8"), false, boundary);
3531
4067
  };
3532
4068
  let targetDir;
3533
4069
  if (options.global) {
3534
- const claudeRoot = (0, import_node_path10.join)(home, ".claude", "skills");
3535
- const codexRoot = (0, import_node_path10.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path10.join)(home, ".codex"), "skills");
4070
+ const claudeRoot = (0, import_node_path11.join)(home, ".claude", "skills");
4071
+ const codexRoot = (0, import_node_path11.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path11.join)(home, ".codex"), "skills");
3536
4072
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
3537
4073
  for (const harness of harnesses) {
3538
4074
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
3539
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path10.dirname)((0, import_node_path10.dirname)(codexRoot)));
4075
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path11.dirname)((0, import_node_path11.dirname)(codexRoot)));
3540
4076
  rememberTarget(harness, skillRoot);
3541
4077
  }
3542
4078
  } else {
3543
- const sharedRoot = (0, import_node_path10.join)(root, ".agents", "skills");
4079
+ const sharedRoot = (0, import_node_path11.join)(root, ".agents", "skills");
3544
4080
  planSkillTree(sharedRoot);
3545
- const claudeRoot = (0, import_node_path10.join)(root, ".claude", "skills");
4081
+ const claudeRoot = (0, import_node_path11.join)(root, ".claude", "skills");
3546
4082
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
3547
4083
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
3548
4084
  if (harnesses.includes("claude")) {
3549
4085
  for (const skill of skillNames(files)) {
3550
- const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
3551
- plan((0, import_node_path10.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4086
+ const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, skill, "SKILL.md"), "utf8");
4087
+ plan((0, import_node_path11.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
3552
4088
  }
3553
4089
  rememberTarget("claude", claudeRoot);
3554
4090
  }
3555
4091
  if (harnesses.includes("cursor")) {
3556
- const cursorRule = (0, import_node_path10.join)(root, ".cursor", "rules", "odla.mdc");
4092
+ const cursorRule = (0, import_node_path11.join)(root, ".cursor", "rules", "odla.mdc");
3557
4093
  plan(cursorRule, CURSOR_RULE);
3558
4094
  rememberTarget("cursor", cursorRule);
3559
4095
  }
3560
4096
  if (harnesses.includes("agents")) {
3561
- const agentsFile = (0, import_node_path10.join)(root, "AGENTS.md");
4097
+ const agentsFile = (0, import_node_path11.join)(root, "AGENTS.md");
3562
4098
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
3563
4099
  rememberTarget("agents", agentsFile);
3564
4100
  }
3565
4101
  if (harnesses.includes("copilot")) {
3566
- const copilotFile = (0, import_node_path10.join)(root, ".github", "copilot-instructions.md");
4102
+ const copilotFile = (0, import_node_path11.join)(root, ".github", "copilot-instructions.md");
3567
4103
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
3568
4104
  rememberTarget("copilot", copilotFile);
3569
4105
  }
3570
4106
  if (harnesses.includes("gemini")) {
3571
- const geminiFile = (0, import_node_path10.join)(root, "GEMINI.md");
4107
+ const geminiFile = (0, import_node_path11.join)(root, "GEMINI.md");
3572
4108
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
3573
4109
  rememberTarget("gemini", geminiFile);
3574
4110
  }
@@ -3604,7 +4140,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
3604
4140
  }
3605
4141
  for (const file of plans.values()) {
3606
4142
  if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
3607
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
4143
+ (0, import_node_fs12.mkdirSync)((0, import_node_path11.dirname)(file.target), { recursive: true });
3608
4144
  (0, import_node_fs12.writeFileSync)(file.target, file.content);
3609
4145
  }
3610
4146
  }
@@ -3624,7 +4160,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
3624
4160
  };
3625
4161
  }
3626
4162
  function pathsUnder(root, paths) {
3627
- return [...paths].map((path) => (0, import_node_path10.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path10.sep}`) && !(0, import_node_path10.isAbsolute)(path)).sort();
4163
+ return [...paths].map((path) => (0, import_node_path11.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path11.sep}`) && !(0, import_node_path11.isAbsolute)(path)).sort();
3628
4164
  }
3629
4165
  function normalizeHarnesses(values, global) {
3630
4166
  const requested = values?.length ? values : ["claude"];
@@ -3669,13 +4205,13 @@ function managedFileContent(path, block, force, boundary) {
3669
4205
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
3670
4206
  }
3671
4207
  function symlinkedComponent(boundary, target) {
3672
- const rel = (0, import_node_path10.relative)(boundary, target);
3673
- if (rel === ".." || rel.startsWith(`..${import_node_path10.sep}`) || (0, import_node_path10.isAbsolute)(rel)) {
4208
+ const rel = (0, import_node_path11.relative)(boundary, target);
4209
+ if (rel === ".." || rel.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(rel)) {
3674
4210
  throw new Error(`agent setup target escapes its install root: ${target}`);
3675
4211
  }
3676
4212
  let current = boundary;
3677
- for (const part of rel.split(import_node_path10.sep).filter(Boolean)) {
3678
- current = (0, import_node_path10.join)(current, part);
4213
+ for (const part of rel.split(import_node_path11.sep).filter(Boolean)) {
4214
+ current = (0, import_node_path11.join)(current, part);
3679
4215
  try {
3680
4216
  if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
3681
4217
  } catch (error) {
@@ -3692,9 +4228,9 @@ function listFiles(dir) {
3692
4228
  const results = [];
3693
4229
  const walk = (current) => {
3694
4230
  for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
3695
- const path = (0, import_node_path10.join)(current, entry.name);
4231
+ const path = (0, import_node_path11.join)(current, entry.name);
3696
4232
  if (entry.isDirectory()) walk(path);
3697
- else results.push((0, import_node_path10.relative)(dir, path));
4233
+ else results.push((0, import_node_path11.relative)(dir, path));
3698
4234
  }
3699
4235
  };
3700
4236
  walk(dir);
@@ -3770,7 +4306,7 @@ async function smoke(options) {
3770
4306
  out.log(` schema: ${liveEntities.length} entities`);
3771
4307
  const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
3772
4308
  if (aggregateEntity) {
3773
- const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
4309
+ const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
3774
4310
  ns: aggregateEntity,
3775
4311
  aggregate: { count: true }
3776
4312
  });
@@ -3814,16 +4350,16 @@ async function getJson(doFetch, url, bearer) {
3814
4350
  const res = await doFetch(url, {
3815
4351
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
3816
4352
  });
3817
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
4353
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3818
4354
  return res.json();
3819
4355
  }
3820
- async function postJson(doFetch, url, bearer, body) {
4356
+ async function postJson2(doFetch, url, bearer, body) {
3821
4357
  const res = await doFetch(url, {
3822
4358
  method: "POST",
3823
4359
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
3824
4360
  body: JSON.stringify(body)
3825
4361
  });
3826
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
4362
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3827
4363
  return res.json();
3828
4364
  }
3829
4365
  function publicConfigUrl(platformUrl, appId, env) {
@@ -3831,7 +4367,7 @@ function publicConfigUrl(platformUrl, appId, env) {
3831
4367
  url.searchParams.set("env", env);
3832
4368
  return url.toString();
3833
4369
  }
3834
- async function safeText3(res) {
4370
+ async function safeText4(res) {
3835
4371
  try {
3836
4372
  return redactSecrets((await res.text()).slice(0, 500));
3837
4373
  } catch {
@@ -3887,6 +4423,26 @@ async function secretsCommand(parsed, deps) {
3887
4423
  });
3888
4424
  }
3889
4425
  async function projectCommand(command, parsed, deps) {
4426
+ if (command === "config") {
4427
+ const sub = parsed.positionals[1];
4428
+ if (sub !== "diff" && sub !== "plan") {
4429
+ throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
4430
+ }
4431
+ assertArgs(parsed, ["config", "token", "email", "open", "json"], 2);
4432
+ const options = {
4433
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
4434
+ token: stringOpt(parsed.options.token),
4435
+ email: stringOpt(parsed.options.email),
4436
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
4437
+ json: parsed.options.json === true,
4438
+ fetch: deps.fetch,
4439
+ openApprovalUrl: deps.openUrl,
4440
+ stdout: deps.stdout
4441
+ };
4442
+ if (sub === "diff") await configDiff(options);
4443
+ else await configPlan(options);
4444
+ return true;
4445
+ }
3890
4446
  if (command === "init") {
3891
4447
  assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
3892
4448
  initProject({
@@ -3947,7 +4503,7 @@ async function projectCommand(command, parsed, deps) {
3947
4503
  // src/code-connect.ts
3948
4504
  var import_node_fs14 = require("fs");
3949
4505
  var import_node_os4 = require("os");
3950
- var import_node_path12 = require("path");
4506
+ var import_node_path13 = require("path");
3951
4507
 
3952
4508
  // ../harness/dist/chunk-QTUEF2HZ.js
3953
4509
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -4565,7 +5121,7 @@ var CamelError = class extends Error {
4565
5121
  };
4566
5122
 
4567
5123
  // ../camel/dist/chunk-L5DYU2E2.js
4568
- function canonicalJson(value2) {
5124
+ function canonicalJson2(value2) {
4569
5125
  return JSON.stringify(normalize(value2));
4570
5126
  }
4571
5127
  async function sha256Hex(value2) {
@@ -4577,7 +5133,7 @@ function utf8Length(value2) {
4577
5133
  if (typeof value2 === "string") return new TextEncoder().encode(value2).byteLength;
4578
5134
  if (value2 instanceof Uint8Array) return value2.byteLength;
4579
5135
  try {
4580
- return new TextEncoder().encode(canonicalJson(value2)).byteLength;
5136
+ return new TextEncoder().encode(canonicalJson2(value2)).byteLength;
4581
5137
  } catch {
4582
5138
  throw new CamelError("conversion_rejected", "Unsafe input could not be canonically measured.");
4583
5139
  }
@@ -4703,7 +5259,7 @@ async function digestCodeVerificationReceipt(fields) {
4703
5259
  changedTestsRequireReview: fields.changedTestsRequireReview,
4704
5260
  outcome: fields.outcome
4705
5261
  };
4706
- return `sha256:${await sha256Hex(canonicalJson(canonical))}`;
5262
+ return `sha256:${await sha256Hex(canonicalJson2(canonical))}`;
4707
5263
  }
4708
5264
  function validate(fields) {
4709
5265
  if (fields.schemaVersion !== 1 || typeof fields.verificationId !== "string" || !ID.test(fields.verificationId) || typeof fields.trustedBaseCommitSha !== "string" || !SHA.test(fields.trustedBaseCommitSha) || !DIGEST.test(fields.trustedBaseDigest) || !DIGEST.test(fields.patchDigest) || !DIGEST.test(fields.candidateDigest) || !DIGEST.test(fields.sourceDigest) || !DIGEST.test(fields.policyDigest) || !DIGEST.test(fields.changedTestSetDigest) || !Number.isSafeInteger(fields.changedTestCount) || fields.changedTestCount < 0 || fields.changedTestCount > 1e4 || fields.changedTestsRequireReview !== fields.changedTestCount > 0 || fields.recipes.length < 1 || fields.recipes.length > 64) {
@@ -4741,9 +5297,9 @@ async function createCodePortableCheckpoint(input) {
4741
5297
  const state2 = normalizeState(input.state);
4742
5298
  validateBaseAndPatch(input.baseCommitSha, input.patch);
4743
5299
  const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
4744
- const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
5300
+ const stateDigest = `sha256:${await sha256Hex(canonicalJson2(state2))}`;
4745
5301
  const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
4746
- const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
5302
+ const checkpointDigest = `sha256:${await sha256Hex(canonicalJson2(fields))}`;
4747
5303
  return freeze({ ...fields, patch: input.patch, checkpointDigest });
4748
5304
  }
4749
5305
  async function verifyCodePortableCheckpoint(value2) {
@@ -4861,7 +5417,7 @@ async function digestCodeRepositorySnapshot(snapshot, limits = DEFAULT_LIMITS) {
4861
5417
  commitSha: snapshot.commitSha,
4862
5418
  files: [...snapshot.files].map((file) => ({ path: file.path, content: file.content })).sort((left, right) => left.path.localeCompare(right.path))
4863
5419
  };
4864
- return `sha256:${await sha256Hex(canonicalJson(normalized))}`;
5420
+ return `sha256:${await sha256Hex(canonicalJson2(normalized))}`;
4865
5421
  }
4866
5422
  function validateSnapshot(snapshot, limits) {
4867
5423
  if (!REPOSITORY.test(snapshot.repository) || !SHA4.test(snapshot.commitSha)) {
@@ -4903,10 +5459,10 @@ var import_path9 = require("path");
4903
5459
 
4904
5460
  // ../camel/dist/chunk-LAXU2AVK.js
4905
5461
  function conversionPolicyDigest(policy) {
4906
- return sha256Hex(canonicalJson(policy));
5462
+ return sha256Hex(canonicalJson2(policy));
4907
5463
  }
4908
5464
  function registeredIdRegistryDigest(values) {
4909
- return sha256Hex(canonicalJson(values));
5465
+ return sha256Hex(canonicalJson2(values));
4910
5466
  }
4911
5467
  async function createConversionRegistry(config) {
4912
5468
  const policies = /* @__PURE__ */ new Map();
@@ -5107,10 +5663,10 @@ function createEffectPolicy(config = {}) {
5107
5663
  return Object.freeze({ evaluate: (input) => evaluate(input, registries, approvalEffects) });
5108
5664
  }
5109
5665
  function destinationRegistryDigest(values) {
5110
- return sha256Hex(canonicalJson([...values].sort()));
5666
+ return sha256Hex(canonicalJson2([...values].sort()));
5111
5667
  }
5112
5668
  async function evaluate(input, registries, approvals) {
5113
- const actionDigest = await sha256Hex(canonicalJson(input));
5669
+ const actionDigest = await sha256Hex(canonicalJson2(input));
5114
5670
  const decisionId = `decision_${actionDigest.slice(0, 24)}`;
5115
5671
  const deny = (reasonCode) => ({ outcome: "deny", decisionId, reasonCode });
5116
5672
  if (!input.planId || !input.tool.name || !input.tool.policyId || !Number.isSafeInteger(input.tool.version)) return deny("invalid_descriptor");
@@ -6831,268 +7387,19 @@ var CodePiRuntimeEngine = class {
6831
7387
  }
6832
7388
  };
6833
7389
 
6834
- // src/help.ts
7390
+ // src/version.ts
6835
7391
  var import_node_fs13 = require("fs");
6836
7392
  function cliVersion() {
6837
7393
  const pkg = JSON.parse((0, import_node_fs13.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
6838
7394
  return pkg.version ?? "unknown";
6839
7395
  }
6840
- function printHelp(output = console) {
6841
- output.log(`odla-ai
6842
7396
 
6843
- Start here:
6844
- odla-ai runbook ask "<question>" The current procedure, from odla's own
6845
- runbooks. Ask BEFORE searching the web or
6846
- working from memory: runbooks are edited
6847
- live, so your training data is out of date
6848
- and this is not.
6849
- odla-ai runbook impact After a change: which runbooks describe the
6850
- code you just touched, so you can fix any
6851
- step it made wrong.
6852
-
6853
- Usage:
6854
- odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
6855
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
6856
- odla-ai doctor [--config odla.config.mjs]
6857
- odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
6858
- odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
6859
- odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
6860
- odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
6861
- odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
6862
- odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
6863
- odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
6864
- odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
6865
- odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
6866
- odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
6867
- odla-ai app promote [--dry-run] [--json] --yes
6868
- odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
6869
- odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
6870
- odla-ai app owners add <email> [--email <odla-account>] [--json]
6871
- odla-ai app owners remove <email> [--email <odla-account>] [--json]
6872
- odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6873
- odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6874
- odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6875
- odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6876
- odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
6877
- odla-ai pm task add --app <id> --title <t> [--column <c>] [--goal <id>] [--assignee <id>] [--description <text>|--body <text>] [--due <epoch-ms>] [--mutation-id <id>] [--json]
6878
- odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
6879
- 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]
6880
- odla-ai pm <goal|task|decision|bug> get <id> [--json]
6881
- odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
6882
- odla-ai pm task set <id> [--title <t>|--column <c>|--rank <n>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--description <text>|--body <text>|--due <epoch-ms>|--no-due] [--mutation-id <id>] [--json]
6883
- odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
6884
- odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
6885
- odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
6886
- odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
6887
- odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
6888
- odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6889
- odla-ai pm <goal|task|decision|bug> rm <id>
6890
- odla-ai pm handoff --app <id> [--json]
6891
- odla-ai discuss groups [--json]
6892
- odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6893
- odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
6894
- odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
6895
- odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6896
- odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6897
- odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6898
- odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
6899
- odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6900
- odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6901
- odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
6902
- odla-ai context list [--json]
6903
- odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
6904
- odla-ai context remove <name> --yes [--json]
6905
- odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
6906
- odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
6907
- odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
6908
- odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6909
- odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6910
- odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
6911
- odla-ai runbook lint [--app <id>] [--all] [--json]
6912
- odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
6913
- odla-ai runbook comment <slug> --body "..." [--app <id>]
6914
- odla-ai runbook get <slug> [--app <id>]
6915
- odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
6916
- odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
6917
- odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
6918
- odla-ai runbook publish <slug> [--app <id>]
6919
- odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
6920
- odla-ai runbook archive <slug> [--app <id>]
6921
- odla-ai runbook history <slug> [--app <id>] [--json]
6922
- odla-ai runbook revert <slug> --version <n> [--app <id>]
6923
- odla-ai runbook rm <slug> [--app <id>]
6924
- odla-ai capabilities [--json]
6925
- odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
6926
- odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
6927
- odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
6928
- odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
6929
- [--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
6930
- odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
6931
- --validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
6932
- odla-ai admin ai credentials [--context <name>] [--json]
6933
- odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
6934
- odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
6935
- odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
6936
- odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
6937
- odla-ai security github disconnect --source <id> [--env dev] [--yes]
6938
- odla-ai security plan [--env dev] [--json]
6939
- odla-ai security sources [--env dev] [--json]
6940
- odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
6941
- odla-ai security status <job-id> [--follow] [--json]
6942
- odla-ai security report <job-id> [--json]
6943
- odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
6944
- odla-ai security run [target] --self --ack-redacted-source
6945
- odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
6946
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
6947
- odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
6948
- odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
6949
- odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
6950
- odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
6951
- odla-ai version
6952
-
6953
- Commands:
6954
- agent Inspect durable agent wakeups and explicitly requeue a
6955
- dead-lettered job; JSON output is stable for remote operators.
6956
- runbook odla's operational procedures, stored in the database and read at
6957
- the moment they are followed. "ask" gives a written, cited answer;
6958
- "search" the passages behind it; "get" the whole document.
6959
- "impact" diffs your working tree against a base ref and names the
6960
- runbooks that describe what you changed \u2014 run it after touching a
6961
- package's exported API or JSDoc, or a Studio surface. "lint"
6962
- checks the corpus the other way: every odla-ai command the
6963
- runbooks name is held against this CLI's real command surface,
6964
- and against the minimum versions each runbook declares. A wrong step
6965
- is fixed with "edit", which takes effect immediately: a runbook is
6966
- a row, not a release. Runbooks describe PROCEDURE; what an export
6967
- does and what it guarantees is JSDoc, rendered per package at
6968
- https://odla.ai/docs and shipped in the installed .d.ts. Answering
6969
- a question usually needs both.
6970
- whoami Report who this terminal is authenticated as, and whether it holds
6971
- platform admin. A handshake-minted device token is never admin,
6972
- however the human who approved it is configured.
6973
- context Explain selected config, platform, app, environment, and
6974
- developer-token provenance without printing credentials or
6975
- starting a device handshake. Operator work can run outside a
6976
- project checkout with explicit flags or ODLA_* environment
6977
- variables.
6978
- setup Install offline odla runbooks for common coding-agent harnesses.
6979
- init Create a generic odla.config.mjs plus starter schema/rules files.
6980
- doctor Validate and summarize the project config without network calls.
6981
- calendar Inspect, connect, or disconnect the live Google booking connection.
6982
- app Archive (suspend, data retained), restore, export, import, or
6983
- manage the co-owners of the app. Archiving takes every
6984
- environment's data plane down until restored; permanent deletion
6985
- has NO CLI \u2014 it requires a signed-in owner in Studio, and no
6986
- machine or agent credential can purge. "app export" downloads a
6987
- portable gzipped-JSONL snapshot of one database (--fresh takes a
6988
- new snapshot first) \u2014 your data is always yours to take.
6989
- "app import" upserts rows back in from JSON, JSONL, or a
6990
- {namespace: rows} map (the shape a query returns); it writes only
6991
- with --yes, so the bare command is a dry run, and it refuses to
6992
- guess an id mode that would duplicate rows on a re-run.
6993
- "app refresh-sandbox" replaces the sandbox with a copy of live
6994
- (the routine dev loop); "app go-live" copies the sandbox into an
6995
- EMPTY live database, once, at launch; "app promote" pushes only
6996
- schema, rules and gates up afterwards, leaving live's rows alone.
6997
- Each prints its plan and writes nothing without --yes, so the
6998
- bare command is the dry run. Clerk config, triggers, allowlists,
6999
- secrets and API keys never travel; identity rows stay behind
7000
- unless --include-identity.
7001
- "app rename <name>" changes only the display name humans read;
7002
- the app id is permanent (tenants, URLs and keys embed it), so a
7003
- rename never re-provisions anything or invalidates a credential.
7004
- "app owners add <email>" grants a signed-up odla member the SAME
7005
- full access as you; they then run their own "provision" to mint
7006
- their own credentials for the shared db (the live one included)
7007
- \u2014 no secret is ever copied between people.
7008
- capabilities Show what the CLI automates vs agent edits and human checkpoints.
7009
- code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
7010
- admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
7011
- security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
7012
- pm Project management (via @odla-ai/pm) shared across the apps you
7013
- co-own: conformance goals, kanban tasks, decisions, and bugs. With
7014
- no --app, "list" spans every co-owned project (the cross-project
7015
- view); with --app it scopes to one. Same device-grant auth as
7016
- "app". Entities: goal (alias conformance), task (alias kanban),
7017
- decision, bug. Status changes and comments post to each item's
7018
- @odla-ai/chat discussion thread.
7019
- discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
7020
- one group per project, topics with replies, @-mentions of people,
7021
- agents, PM items, and projects. Built for unattended use \u2014 post a
7022
- question, then "watch" it until someone answers. "watch" exits 75
7023
- (not 1) when it times out with nothing new, so a script can tell
7024
- "no answer yet" from a failure and just rerun. Use "who" to look
7025
- up the exact @[Label](kind/id) markup for a mention.
7026
- o11y Read one stable status envelope for application RED, current
7027
- live-sync load/freshness, the protected commit-to-visible
7028
- canary, collector ingest/scheduler trust, Cloudflare-owned
7029
- runtime metrics, and a machine verdict.
7030
- --json keeps auth progress on stderr for unattended agents.
7031
- platform Read canonical fleet health, releases, provider load/freshness,
7032
- explicit unknowns, and next actions through a read-only grant.
7033
- provision Register services, compose integrations, persist credentials, optionally push secrets.
7034
- smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
7035
- skill Same installer; --agent accepts all, claude, codex, cursor,
7036
- copilot, gemini, or agents (repeatable or comma-separated).
7037
- secrets Push configured db/o11y secrets into the Worker via wrangler
7038
- stdin; set stores a tenant-vault secret and set-clerk-key the
7039
- reserved Clerk secret key, write-only from stdin or an env var.
7040
- version Print the CLI version.
7041
-
7042
- Safety:
7043
- Every app has two databases on odla.ai: a sandbox (env "dev", tenant
7044
- <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
7045
- production odla.ai \u2014 there is no separate odla to point at. New projects get
7046
- the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
7047
- provision the live database; use --dry-run first to inspect the resolved plan.
7048
- Provision caches the approved developer token and service credentials under
7049
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
7050
- preflights Wrangler before any shown-once issuance or destructive rotation.
7051
- Projectless PM, Discussions, o11y, runbook, and identity commands use
7052
- --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
7053
- ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
7054
- select it explicitly with --context or ODLA_CONTEXT. Each named context gets
7055
- isolated developer and scoped-token caches. Override their locations with
7056
- ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
7057
- the metadata file. Flags and specific ODLA_* scope variables beat a selected
7058
- context, which beats project config. There is no ambient current context.
7059
- "context show" reports only provenance and cache state and never authenticates.
7060
- Provision opens the approval page in your browser automatically whenever the
7061
- machine can show one, including agent-driven runs; only CI, SSH, and
7062
- display-less hosts skip it. Use --open to force or --no-open to suppress.
7063
- Browser launch is best-effort: the printed approval URL is authoritative and
7064
- agents must relay it to the human verbatim. A started handshake is persisted
7065
- under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
7066
- the same code. Outside an interactive terminal the wait is capped (90s by
7067
- default, --wait <seconds> to change); a still-pending handshake then exits
7068
- with code 75: relay the URL, wait for approval, and re-run to collect.
7069
- A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
7070
- The email is a non-secret identity hint: never provide a password or session
7071
- token. The matching account must already exist, be signed in, explicitly
7072
- review the exact code, and finish any current request before claiming another.
7073
- Run Code from a GitHub checkout already connected to an app in Studio; an
7074
- odla.config.mjs may select the app explicitly but is not required. Code host
7075
- approval and credential hashes live in odla-ai/db. The host
7076
- credential is never written under .odla/; it exists only in the foreground
7077
- "code connect" process and is rotated by the next approved connection.
7078
- Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
7079
- OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
7080
- GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
7081
- for a PAT or provider key. GitHub read access is separate from the explicit
7082
- --ack-redacted-source consent and the exact --plan-digest printed by security
7083
- plan are required before bounded snippets reach System AI.
7084
- Run security plan first to inspect the admin-selected providers, models,
7085
- per-route bounds, credential readiness, retention, no-execution boundary,
7086
- and digest that binds consent to that exact plan.
7087
- `);
7088
- }
7089
-
7090
- // src/security-hosted-github.ts
7091
- var import_node_child_process4 = require("child_process");
7092
- var import_node_util2 = require("util");
7397
+ // src/security-hosted-github.ts
7398
+ var import_node_child_process4 = require("child_process");
7399
+ var import_node_util2 = require("util");
7093
7400
 
7094
7401
  // src/security-hosted-request.ts
7095
- async function requestHostedSecurityJson(options, path, init, action) {
7402
+ async function requestHostedSecurityJson(options, path, init, action2) {
7096
7403
  const platform = platformAudience(options.platform);
7097
7404
  const token = hostedSecurityCredential(options.token);
7098
7405
  const requestInit = {
@@ -7112,7 +7419,7 @@ async function requestHostedSecurityJson(options, path, init, action) {
7112
7419
  if (!response2.ok) {
7113
7420
  const code = optionalHostedText(body.error?.code, "error code", 128);
7114
7421
  const message2 = optionalHostedText(body.error?.message, "error message", 300);
7115
- throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
7422
+ throw new Error(`${action2} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
7116
7423
  }
7117
7424
  return body;
7118
7425
  }
@@ -7303,7 +7610,7 @@ async function defaultReadOrigin(cwd) {
7303
7610
 
7304
7611
  // src/code-local-source.ts
7305
7612
  var import_node_child_process5 = require("child_process");
7306
- var import_node_crypto = require("crypto");
7613
+ var import_node_crypto2 = require("crypto");
7307
7614
  var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
7308
7615
  async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
7309
7616
  const headCommitSha = await readHead(cwd);
@@ -7354,15 +7661,15 @@ async function readGitHead(cwd) {
7354
7661
  return value2;
7355
7662
  }
7356
7663
  function digestText(value2) {
7357
- return `sha256:${(0, import_node_crypto.createHash)("sha256").update(value2).digest("hex")}`;
7664
+ return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(value2).digest("hex")}`;
7358
7665
  }
7359
7666
 
7360
7667
  // src/code-images.ts
7361
7668
  var import_node_child_process6 = require("child_process");
7362
- var import_node_crypto2 = require("crypto");
7669
+ var import_node_crypto3 = require("crypto");
7363
7670
  var import_promises10 = require("fs/promises");
7364
7671
  var import_node_os3 = require("os");
7365
- var import_node_path11 = require("path");
7672
+ var import_node_path12 = require("path");
7366
7673
  var import_node_url3 = require("url");
7367
7674
 
7368
7675
  // src/code-runtime-config.ts
@@ -7445,13 +7752,13 @@ async function embeddedPiImageName() {
7445
7752
  const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
7446
7753
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
7447
7754
  });
7448
- return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto2.createHash)("sha256").update(bundle).digest("hex")}`;
7755
+ return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
7449
7756
  }
7450
7757
  async function buildEmbeddedPiImage(engine, image, run) {
7451
- const context = await (0, import_promises10.mkdtemp)((0, import_node_path11.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
7758
+ const context = await (0, import_promises10.mkdtemp)((0, import_node_path12.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
7452
7759
  try {
7453
- await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path11.join)(context, "pi-agent.js"));
7454
- await (0, import_promises10.writeFile)((0, import_node_path11.join)(context, "Dockerfile"), [
7760
+ await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path12.join)(context, "pi-agent.js"));
7761
+ await (0, import_promises10.writeFile)((0, import_node_path12.join)(context, "Dockerfile"), [
7455
7762
  `FROM ${CODE_NODE_IMAGE}`,
7456
7763
  "COPY pi-agent.js /opt/odla/pi-agent.js",
7457
7764
  "WORKDIR /workspace",
@@ -7467,7 +7774,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
7467
7774
  // src/code-connect.ts
7468
7775
  async function codeConnect(options) {
7469
7776
  const cwd = options.cwd ?? process.cwd();
7470
- const configPath = (0, import_node_path12.resolve)(cwd, options.configPath);
7777
+ const configPath = (0, import_node_path13.resolve)(cwd, options.configPath);
7471
7778
  const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
7472
7779
  const requestedAppId = options.appId?.trim();
7473
7780
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
@@ -7635,9 +7942,9 @@ function parseConnection(value2, appId, appEnv) {
7635
7942
  }
7636
7943
  return root;
7637
7944
  }
7638
- function apiFailure(action, status, value2) {
7945
+ function apiFailure(action2, status, value2) {
7639
7946
  const message2 = record4(record4(value2)?.error)?.message;
7640
- return `${action} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
7947
+ return `${action2} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
7641
7948
  }
7642
7949
  function record4(value2) {
7643
7950
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
@@ -7704,136 +8011,391 @@ function clean3(value2) {
7704
8011
  return normalized || void 0;
7705
8012
  }
7706
8013
 
7707
- // src/context-command.ts
7708
- async function contextCommand(parsed, deps = {}) {
7709
- assertArgs(
7710
- parsed,
7711
- [
7712
- "config",
7713
- "context",
7714
- "platform",
7715
- "app",
7716
- "env",
7717
- "token",
7718
- "json",
7719
- "yes"
7720
- ],
7721
- 3
7722
- );
7723
- const action = parsed.positionals[1];
7724
- const out = deps.stdout ?? console;
7725
- if (action === "list") {
7726
- const profiles = listOperatorProfiles();
7727
- if (parsed.options.json === true) {
7728
- out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
7729
- return;
7730
- }
7731
- if (profiles.length === 0) {
7732
- out.log("No named operator contexts.");
7733
- return;
7734
- }
7735
- out.log("name platform app environment");
7736
- for (const profile of profiles) {
7737
- out.log(
7738
- `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
7739
- );
7740
- }
7741
- return;
7742
- }
7743
- if (action === "save") {
7744
- if (parsed.options.token !== void 0) {
7745
- throw new Error(
7746
- "context save does not accept --token; contexts store scope metadata only"
7747
- );
7748
- }
7749
- const name = requireName(parsed);
7750
- const context2 = await resolveOperatorContext(parsed, {
7751
- allowMissingConfig: true
7752
- });
7753
- const profile = {
7754
- platform: context2.platform.value,
7755
- ...context2.app.value ? { app: context2.app.value } : {},
7756
- ...context2.environment.value ? { environment: context2.environment.value } : {}
7757
- };
7758
- saveOperatorProfile(name, profile, context2.profile.file);
7759
- if (parsed.options.json === true) {
7760
- out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
7761
- } else {
7762
- out.log(`saved operator context "${name}" (${profile.platform})`);
7763
- }
7764
- return;
7765
- }
7766
- if (action === "remove") {
7767
- const name = requireName(parsed);
7768
- if (parsed.options.yes !== true) {
7769
- throw new Error(`context remove "${name}" requires --yes`);
7770
- }
7771
- const removed = removeOperatorProfile(name);
7772
- const result2 = {
7773
- schemaVersion: 1,
7774
- name,
7775
- removed,
7776
- credentialCachesRemoved: false,
7777
- grantsRevoked: false
7778
- };
7779
- if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
7780
- else {
7781
- const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
7782
- out.log(`${status}; credential caches were not deleted or revoked`);
7783
- }
7784
- return;
7785
- }
7786
- if (action !== "show") {
7787
- throw new Error(
7788
- `unknown context action "${action ?? ""}". Try show|list|save|remove.`
7789
- );
7790
- }
7791
- const context = await resolveOperatorContext(parsed, {
7792
- allowMissingConfig: true,
7793
- defaultEnvironment: "prod"
7794
- });
7795
- const token = developerTokenStatus(context, parsed);
7796
- const result = {
7797
- schemaVersion: 1,
7798
- profile: context.profile,
7799
- config: context.config,
7800
- platform: context.platform,
7801
- app: context.app,
7802
- environment: context.environment,
7803
- authentication: {
7804
- developerToken: token,
7805
- scopedTokenCacheFile: context.credentials.scopedTokenFile,
7806
- handshakeStarted: false
7807
- }
7808
- };
7809
- if (parsed.options.json === true) {
7810
- out.log(JSON.stringify(result, null, 2));
7811
- return;
7812
- }
7813
- out.log(`config: ${context.config.status} (${context.config.path})`);
7814
- out.log(
7815
- `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
7816
- );
7817
- out.log(
7818
- `platform: ${context.platform.value} (${context.platform.source})`
7819
- );
7820
- out.log(
7821
- `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
7822
- );
7823
- out.log(
7824
- `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
7825
- );
7826
- out.log(
7827
- `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
7828
- );
7829
- out.log("handshake: not started");
7830
- }
7831
- function requireName(parsed) {
7832
- const name = parsed.positionals[2]?.trim();
7833
- if (!name) {
7834
- throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
7835
- }
7836
- return name;
8014
+ // src/context-command.ts
8015
+ async function contextCommand(parsed, deps = {}) {
8016
+ assertArgs(
8017
+ parsed,
8018
+ [
8019
+ "config",
8020
+ "context",
8021
+ "platform",
8022
+ "app",
8023
+ "env",
8024
+ "token",
8025
+ "json",
8026
+ "yes"
8027
+ ],
8028
+ 3
8029
+ );
8030
+ const action2 = parsed.positionals[1];
8031
+ const out = deps.stdout ?? console;
8032
+ if (action2 === "list") {
8033
+ const profiles = listOperatorProfiles();
8034
+ if (parsed.options.json === true) {
8035
+ out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
8036
+ return;
8037
+ }
8038
+ if (profiles.length === 0) {
8039
+ out.log("No named operator contexts.");
8040
+ return;
8041
+ }
8042
+ out.log("name platform app environment");
8043
+ for (const profile of profiles) {
8044
+ out.log(
8045
+ `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
8046
+ );
8047
+ }
8048
+ return;
8049
+ }
8050
+ if (action2 === "save") {
8051
+ if (parsed.options.token !== void 0) {
8052
+ throw new Error(
8053
+ "context save does not accept --token; contexts store scope metadata only"
8054
+ );
8055
+ }
8056
+ const name = requireName(parsed);
8057
+ const context2 = await resolveOperatorContext(parsed, {
8058
+ allowMissingConfig: true
8059
+ });
8060
+ const profile = {
8061
+ platform: context2.platform.value,
8062
+ ...context2.app.value ? { app: context2.app.value } : {},
8063
+ ...context2.environment.value ? { environment: context2.environment.value } : {}
8064
+ };
8065
+ saveOperatorProfile(name, profile, context2.profile.file);
8066
+ if (parsed.options.json === true) {
8067
+ out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
8068
+ } else {
8069
+ out.log(`saved operator context "${name}" (${profile.platform})`);
8070
+ }
8071
+ return;
8072
+ }
8073
+ if (action2 === "remove") {
8074
+ const name = requireName(parsed);
8075
+ if (parsed.options.yes !== true) {
8076
+ throw new Error(`context remove "${name}" requires --yes`);
8077
+ }
8078
+ const removed = removeOperatorProfile(name);
8079
+ const result2 = {
8080
+ schemaVersion: 1,
8081
+ name,
8082
+ removed,
8083
+ credentialCachesRemoved: false,
8084
+ grantsRevoked: false
8085
+ };
8086
+ if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
8087
+ else {
8088
+ const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
8089
+ out.log(`${status}; credential caches were not deleted or revoked`);
8090
+ }
8091
+ return;
8092
+ }
8093
+ if (action2 !== "show") {
8094
+ throw new Error(
8095
+ `unknown context action "${action2 ?? ""}". Try show|list|save|remove.`
8096
+ );
8097
+ }
8098
+ const context = await resolveOperatorContext(parsed, {
8099
+ allowMissingConfig: true,
8100
+ defaultEnvironment: "prod"
8101
+ });
8102
+ const token = developerTokenStatus(context, parsed);
8103
+ const result = {
8104
+ schemaVersion: 1,
8105
+ profile: context.profile,
8106
+ config: context.config,
8107
+ platform: context.platform,
8108
+ app: context.app,
8109
+ environment: context.environment,
8110
+ authentication: {
8111
+ developerToken: token,
8112
+ scopedTokenCacheFile: context.credentials.scopedTokenFile,
8113
+ handshakeStarted: false
8114
+ }
8115
+ };
8116
+ if (parsed.options.json === true) {
8117
+ out.log(JSON.stringify(result, null, 2));
8118
+ return;
8119
+ }
8120
+ out.log(`config: ${context.config.status} (${context.config.path})`);
8121
+ out.log(
8122
+ `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
8123
+ );
8124
+ out.log(
8125
+ `platform: ${context.platform.value} (${context.platform.source})`
8126
+ );
8127
+ out.log(
8128
+ `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
8129
+ );
8130
+ out.log(
8131
+ `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
8132
+ );
8133
+ out.log(
8134
+ `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
8135
+ );
8136
+ out.log("handshake: not started");
8137
+ }
8138
+ function requireName(parsed) {
8139
+ const name = parsed.positionals[2]?.trim();
8140
+ if (!name) {
8141
+ throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
8142
+ }
8143
+ return name;
8144
+ }
8145
+
8146
+ // src/help.ts
8147
+ function printHelp(output = console) {
8148
+ output.log(`odla-ai
8149
+
8150
+ Start here:
8151
+ odla-ai runbook ask "<question>" The current procedure, from odla's own
8152
+ runbooks. Ask BEFORE searching the web or
8153
+ working from memory: runbooks are edited
8154
+ live, so your training data is out of date
8155
+ and this is not.
8156
+ odla-ai runbook impact After a change: which runbooks describe the
8157
+ code you just touched, so you can fix any
8158
+ step it made wrong.
8159
+
8160
+ Usage:
8161
+ odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8162
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
8163
+ odla-ai doctor [--config odla.config.mjs]
8164
+ odla-ai config diff [--config odla.config.mjs] [--email <odla-account>] [--json]
8165
+ odla-ai config plan [--config odla.config.mjs] [--email <odla-account>] [--json]
8166
+ odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8167
+ odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8168
+ odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
8169
+ odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
8170
+ odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
8171
+ odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
8172
+ odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
8173
+ odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
8174
+ odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
8175
+ odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
8176
+ odla-ai app promote [--dry-run] [--json] --yes
8177
+ odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
8178
+ odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
8179
+ odla-ai app owners add <email> [--email <odla-account>] [--json]
8180
+ odla-ai app owners remove <email> [--email <odla-account>] [--json]
8181
+ odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8182
+ odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8183
+ odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8184
+ odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8185
+ odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
8186
+ odla-ai pm task add --app <id> --title <t> [--column <c>] [--goal <id>] [--assignee <id>] [--description <text>|--body <text>] [--due <epoch-ms>] [--mutation-id <id>] [--json]
8187
+ odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8188
+ 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]
8189
+ odla-ai pm <goal|task|decision|bug> get <id> [--json]
8190
+ odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
8191
+ odla-ai pm task set <id> [--title <t>|--column <c>|--rank <n>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--description <text>|--body <text>|--due <epoch-ms>|--no-due] [--mutation-id <id>] [--json]
8192
+ odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
8193
+ odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
8194
+ odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
8195
+ odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
8196
+ odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
8197
+ odla-ai pm <goal|task|decision|bug> comments <id> [--json]
8198
+ odla-ai pm <goal|task|decision|bug> rm <id>
8199
+ odla-ai pm handoff --app <id> [--json]
8200
+ odla-ai discuss groups [--json]
8201
+ odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
8202
+ odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
8203
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
8204
+ odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
8205
+ odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
8206
+ odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
8207
+ odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
8208
+ odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
8209
+ odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
8210
+ odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
8211
+ odla-ai context list [--json]
8212
+ odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
8213
+ odla-ai context remove <name> --yes [--json]
8214
+ odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
8215
+ odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8216
+ odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
8217
+ odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
8218
+ odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
8219
+ odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
8220
+ odla-ai runbook lint [--app <id>] [--all] [--json]
8221
+ odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
8222
+ odla-ai runbook comment <slug> --body "..." [--app <id>]
8223
+ odla-ai runbook get <slug> [--app <id>]
8224
+ odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
8225
+ odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
8226
+ odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
8227
+ odla-ai runbook publish <slug> [--app <id>]
8228
+ odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
8229
+ odla-ai runbook archive <slug> [--app <id>]
8230
+ odla-ai runbook history <slug> [--app <id>] [--json]
8231
+ odla-ai runbook revert <slug> --version <n> [--app <id>]
8232
+ odla-ai runbook rm <slug> [--app <id>]
8233
+ odla-ai capabilities [--json]
8234
+ odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
8235
+ odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8236
+ odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
8237
+ odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
8238
+ [--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
8239
+ odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
8240
+ --validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
8241
+ odla-ai admin ai credentials [--context <name>] [--json]
8242
+ odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
8243
+ odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
8244
+ odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
8245
+ odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
8246
+ odla-ai security github disconnect --source <id> [--env dev] [--yes]
8247
+ odla-ai security plan [--env dev] [--json]
8248
+ odla-ai security sources [--env dev] [--json]
8249
+ odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
8250
+ odla-ai security status <job-id> [--follow] [--json]
8251
+ odla-ai security report <job-id> [--json]
8252
+ odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8253
+ odla-ai security run [target] --self --ack-redacted-source
8254
+ odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8255
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8256
+ odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8257
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
8258
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
8259
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
8260
+ odla-ai version
8261
+
8262
+ Commands:
8263
+ agent Inspect durable agent wakeups and explicitly requeue a
8264
+ dead-lettered job; JSON output is stable for remote operators.
8265
+ runbook odla's operational procedures, stored in the database and read at
8266
+ the moment they are followed. "ask" gives a written, cited answer;
8267
+ "search" the passages behind it; "get" the whole document.
8268
+ "impact" diffs your working tree against a base ref and names the
8269
+ runbooks that describe what you changed \u2014 run it after touching a
8270
+ package's exported API or JSDoc, or a Studio surface. "lint"
8271
+ checks the corpus the other way: every odla-ai command the
8272
+ runbooks name is held against this CLI's real command surface,
8273
+ and against the minimum versions each runbook declares. A wrong step
8274
+ is fixed with "edit", which takes effect immediately: a runbook is
8275
+ a row, not a release. Runbooks describe PROCEDURE; what an export
8276
+ does and what it guarantees is JSDoc, rendered per package at
8277
+ https://odla.ai/docs and shipped in the installed .d.ts. Answering
8278
+ a question usually needs both.
8279
+ whoami Report who this terminal is authenticated as, and whether it holds
8280
+ platform admin. A handshake-minted device token is never admin,
8281
+ however the human who approved it is configured.
8282
+ context Explain selected config, platform, app, environment, and
8283
+ developer-token provenance without printing credentials or
8284
+ starting a device handshake. Operator work can run outside a
8285
+ project checkout with explicit flags or ODLA_* environment
8286
+ variables.
8287
+ setup Install offline odla runbooks for common coding-agent harnesses.
8288
+ init Create a generic odla.config.mjs plus starter schema/rules files.
8289
+ doctor Validate and summarize the project config without network calls.
8290
+ config Read-only diff and revision-bound plan for checked-in Registry
8291
+ intent; runtime-owned fields and excluded coverage stay explicit.
8292
+ calendar Inspect, connect, or disconnect the live Google booking connection.
8293
+ app Archive (suspend, data retained), restore, export, import, or
8294
+ manage the co-owners of the app. Archiving takes every
8295
+ environment's data plane down until restored; permanent deletion
8296
+ has NO CLI \u2014 it requires a signed-in owner in Studio, and no
8297
+ machine or agent credential can purge. "app export" downloads a
8298
+ portable gzipped-JSONL snapshot of one database (--fresh takes a
8299
+ new snapshot first) \u2014 your data is always yours to take.
8300
+ "app import" upserts rows back in from JSON, JSONL, or a
8301
+ {namespace: rows} map (the shape a query returns); it writes only
8302
+ with --yes, so the bare command is a dry run, and it refuses to
8303
+ guess an id mode that would duplicate rows on a re-run.
8304
+ "app refresh-sandbox" replaces the sandbox with a copy of live
8305
+ (the routine dev loop); "app go-live" copies the sandbox into an
8306
+ EMPTY live database, once, at launch; "app promote" pushes only
8307
+ schema, rules and gates up afterwards, leaving live's rows alone.
8308
+ Each prints its plan and writes nothing without --yes, so the
8309
+ bare command is the dry run. Clerk config, triggers, allowlists,
8310
+ secrets and API keys never travel; identity rows stay behind
8311
+ unless --include-identity.
8312
+ "app rename <name>" changes only the display name humans read;
8313
+ the app id is permanent (tenants, URLs and keys embed it), so a
8314
+ rename never re-provisions anything or invalidates a credential.
8315
+ "app owners add <email>" grants a signed-up odla member the SAME
8316
+ full access as you; they then run their own "provision" to mint
8317
+ their own credentials for the shared db (the live one included)
8318
+ \u2014 no secret is ever copied between people.
8319
+ capabilities Show what the CLI automates vs agent edits and human checkpoints.
8320
+ code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
8321
+ admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
8322
+ security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
8323
+ pm Project management (via @odla-ai/pm) shared across the apps you
8324
+ co-own: conformance goals, kanban tasks, decisions, and bugs. With
8325
+ no --app, "list" spans every co-owned project (the cross-project
8326
+ view); with --app it scopes to one. Same device-grant auth as
8327
+ "app". Entities: goal (alias conformance), task (alias kanban),
8328
+ decision, bug. Status changes and comments post to each item's
8329
+ @odla-ai/chat discussion thread.
8330
+ discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
8331
+ one group per project, topics with replies, @-mentions of people,
8332
+ agents, PM items, and projects. Built for unattended use \u2014 post a
8333
+ question, then "watch" it until someone answers. "watch" exits 75
8334
+ (not 1) when it times out with nothing new, so a script can tell
8335
+ "no answer yet" from a failure and just rerun. Use "who" to look
8336
+ up the exact @[Label](kind/id) markup for a mention.
8337
+ o11y Read one stable status envelope for application RED, current
8338
+ live-sync load/freshness, the protected commit-to-visible
8339
+ canary, collector ingest/scheduler trust, Cloudflare-owned
8340
+ runtime metrics, and a machine verdict.
8341
+ --json keeps auth progress on stderr for unattended agents.
8342
+ platform Read canonical fleet health, releases, provider load/freshness,
8343
+ explicit unknowns, and next actions through a read-only grant.
8344
+ provision Register services, compose integrations, persist credentials, optionally push secrets.
8345
+ smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
8346
+ skill Same installer; --agent accepts all, claude, codex, cursor,
8347
+ copilot, gemini, or agents (repeatable or comma-separated).
8348
+ secrets Push configured db/o11y secrets into the Worker via wrangler
8349
+ stdin; set stores a tenant-vault secret and set-clerk-key the
8350
+ reserved Clerk secret key, write-only from stdin or an env var.
8351
+ version Print the CLI version.
8352
+
8353
+ Safety:
8354
+ Every app has two databases on odla.ai: a sandbox (env "dev", tenant
8355
+ <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
8356
+ production odla.ai \u2014 there is no separate odla to point at. New projects get
8357
+ the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
8358
+ provision the live database; use --dry-run first to inspect the resolved plan.
8359
+ Provision caches the approved developer token and service credentials under
8360
+ .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
8361
+ preflights Wrangler before any shown-once issuance or destructive rotation.
8362
+ Projectless PM, Discussions, o11y, runbook, and identity commands use
8363
+ --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
8364
+ ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
8365
+ select it explicitly with --context or ODLA_CONTEXT. Each named context gets
8366
+ isolated developer and scoped-token caches. Override their locations with
8367
+ ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
8368
+ the metadata file. Flags and specific ODLA_* scope variables beat a selected
8369
+ context, which beats project config. There is no ambient current context.
8370
+ "context show" reports only provenance and cache state and never authenticates.
8371
+ Provision opens the approval page in your browser automatically whenever the
8372
+ machine can show one, including agent-driven runs; only CI, SSH, and
8373
+ display-less hosts skip it. Use --open to force or --no-open to suppress.
8374
+ Browser launch is best-effort: the printed approval URL is authoritative and
8375
+ agents must relay it to the human verbatim. A started handshake is persisted
8376
+ under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
8377
+ the same code. Outside an interactive terminal the wait is capped (90s by
8378
+ default, --wait <seconds> to change); a still-pending handshake then exits
8379
+ with code 75: relay the URL, wait for approval, and re-run to collect.
8380
+ A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8381
+ The email is a non-secret identity hint: never provide a password or session
8382
+ token. The matching account must already exist, be signed in, explicitly
8383
+ review the exact code, and finish any current request before claiming another.
8384
+ Run Code from a GitHub checkout already connected to an app in Studio; an
8385
+ odla.config.mjs may select the app explicitly but is not required. Code host
8386
+ approval and credential hashes live in odla-ai/db. The host
8387
+ credential is never written under .odla/; it exists only in the foreground
8388
+ "code connect" process and is rotated by the next approved connection.
8389
+ Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
8390
+ OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
8391
+ GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
8392
+ for a PAT or provider key. GitHub read access is separate from the explicit
8393
+ --ack-redacted-source consent and the exact --plan-digest printed by security
8394
+ plan are required before bounded snippets reach System AI.
8395
+ Run security plan first to inspect the admin-selected providers, models,
8396
+ per-route bounds, credential readiness, retention, no-execution boundary,
8397
+ and digest that binds consent to that exact plan.
8398
+ `);
7837
8399
  }
7838
8400
 
7839
8401
  // src/discuss-actions.ts
@@ -8257,8 +8819,8 @@ var ALLOWED = [
8257
8819
  "platform",
8258
8820
  "context"
8259
8821
  ];
8260
- function requireId(id, action) {
8261
- if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
8822
+ function requireId(id, action2) {
8823
+ if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
8262
8824
  return id;
8263
8825
  }
8264
8826
  async function buildContext(parsed, deps) {
@@ -8292,11 +8854,11 @@ async function buildContext(parsed, deps) {
8292
8854
  }
8293
8855
  async function discussCommand(parsed, deps = {}) {
8294
8856
  assertArgs(parsed, ALLOWED, 3);
8295
- const action = parsed.positionals[1];
8857
+ const action2 = parsed.positionals[1];
8296
8858
  const id = parsed.positionals[2];
8297
- if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
8859
+ if (!action2) throw new Error('"discuss" needs an action. Run "odla-ai help".');
8298
8860
  const ctx = await buildContext(parsed, deps);
8299
- switch (action) {
8861
+ switch (action2) {
8300
8862
  case "groups":
8301
8863
  return discussGroups(ctx);
8302
8864
  case "list":
@@ -8318,7 +8880,7 @@ async function discussCommand(parsed, deps = {}) {
8318
8880
  return;
8319
8881
  }
8320
8882
  default:
8321
- throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
8883
+ throw new Error(`unknown discuss action "${action2}". Run "odla-ai help".`);
8322
8884
  }
8323
8885
  }
8324
8886
 
@@ -8579,18 +9141,18 @@ var ENTITY_OPTIONS = {
8579
9141
  done: ["decision"]
8580
9142
  }
8581
9143
  };
8582
- function canonicalAction(action) {
8583
- if (action === "create") return "add";
8584
- if (action === "update" || action === "status" || action === "move") return "set";
8585
- if (action === "delete") return "rm";
8586
- return action in ACTION_OPTIONS ? action : null;
8587
- }
8588
- function allowedOptions(entity, action) {
8589
- const entityOptions = action === "list" || action === "add" || action === "set" || action === "done" ? ENTITY_OPTIONS[entity][action] : [];
8590
- return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action], ...entityOptions];
8591
- }
8592
- function requireId2(id, action) {
8593
- if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
9144
+ function canonicalAction(action2) {
9145
+ if (action2 === "create") return "add";
9146
+ if (action2 === "update" || action2 === "status" || action2 === "move") return "set";
9147
+ if (action2 === "delete") return "rm";
9148
+ return action2 in ACTION_OPTIONS ? action2 : null;
9149
+ }
9150
+ function allowedOptions(entity, action2) {
9151
+ const entityOptions = action2 === "list" || action2 === "add" || action2 === "set" || action2 === "done" ? ENTITY_OPTIONS[entity][action2] : [];
9152
+ return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action2], ...entityOptions];
9153
+ }
9154
+ function requireId2(id, action2) {
9155
+ if (!id) throw new Error(`"pm ... ${action2}" needs an item id`);
8594
9156
  return id;
8595
9157
  }
8596
9158
  async function buildContext2(parsed, deps) {
@@ -8626,28 +9188,28 @@ async function pmCommand(parsed, deps = {}) {
8626
9188
  const entity = ALIASES[word];
8627
9189
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
8628
9190
  const requestedAction = parsed.positionals[2] ?? "list";
8629
- const action = canonicalAction(requestedAction);
8630
- if (!action) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
8631
- assertArgs(parsed, allowedOptions(entity, action), 4);
9191
+ const action2 = canonicalAction(requestedAction);
9192
+ if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
9193
+ assertArgs(parsed, allowedOptions(entity, action2), 4);
8632
9194
  const ctx = await buildContext2(parsed, deps);
8633
9195
  const id = parsed.positionals[3];
8634
- switch (action) {
9196
+ switch (action2) {
8635
9197
  case "list":
8636
9198
  return pmList(ctx, entity, parsed);
8637
9199
  case "add":
8638
9200
  return pmAdd(ctx, entity, parsed);
8639
9201
  case "get":
8640
- return pmGet(ctx, entity, requireId2(id, action));
9202
+ return pmGet(ctx, entity, requireId2(id, action2));
8641
9203
  case "set":
8642
- return pmSet(ctx, entity, requireId2(id, action), parsed);
9204
+ return pmSet(ctx, entity, requireId2(id, action2), parsed);
8643
9205
  case "done":
8644
- return pmDone(ctx, entity, requireId2(id, action), parsed);
9206
+ return pmDone(ctx, entity, requireId2(id, action2), parsed);
8645
9207
  case "comment":
8646
- return pmComment(ctx, entity, requireId2(id, action), parsed);
9208
+ return pmComment(ctx, entity, requireId2(id, action2), parsed);
8647
9209
  case "comments":
8648
- return pmComments(ctx, entity, requireId2(id, action));
9210
+ return pmComments(ctx, entity, requireId2(id, action2));
8649
9211
  case "rm":
8650
- return pmRemove(ctx, entity, requireId2(id, action));
9212
+ return pmRemove(ctx, entity, requireId2(id, action2));
8651
9213
  }
8652
9214
  }
8653
9215
 
@@ -8680,8 +9242,8 @@ function printPlatformStatus(status, out) {
8680
9242
  if (status.verdict.reasons.length) {
8681
9243
  out.log(`reasons ${status.verdict.reasons.join(", ")}`);
8682
9244
  }
8683
- for (const action of status.nextActions) {
8684
- out.log(`next ${action.service} ${action.code} ${action.message}`);
9245
+ for (const action2 of status.nextActions) {
9246
+ out.log(`next ${action2.service} ${action2.code} ${action2.message}`);
8685
9247
  }
8686
9248
  }
8687
9249
  function value(input) {
@@ -8701,10 +9263,10 @@ async function platformCommand(parsed, deps = {}) {
8701
9263
  ["config", "context", "platform", "token", "email", "open", "json"],
8702
9264
  2
8703
9265
  );
8704
- const action = parsed.positionals[1];
8705
- if (action !== "status") {
9266
+ const action2 = parsed.positionals[1];
9267
+ if (action2 !== "status") {
8706
9268
  throw new Error(
8707
- `unknown platform action "${action ?? ""}". Try "odla-ai platform status --json".`
9269
+ `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
8708
9270
  );
8709
9271
  }
8710
9272
  const context = await resolveOperatorContext(parsed, {
@@ -9033,10 +9595,10 @@ async function o11yCommand(parsed, deps = {}) {
9033
9595
  ],
9034
9596
  2
9035
9597
  );
9036
- const action = parsed.positionals[1];
9037
- if (action !== "status") {
9598
+ const action2 = parsed.positionals[1];
9599
+ if (action2 !== "status") {
9038
9600
  throw new Error(
9039
- `unknown o11y action "${action ?? ""}". Try "odla-ai o11y status --json".`
9601
+ `unknown o11y action "${action2 ?? ""}". Try "odla-ai o11y status --json".`
9040
9602
  );
9041
9603
  }
9042
9604
  const minutes = statusMinutes(
@@ -9165,7 +9727,7 @@ async function read2(url, headers, doFetch) {
9165
9727
  }
9166
9728
 
9167
9729
  // src/provision.ts
9168
- var import_apps7 = require("@odla-ai/apps");
9730
+ var import_apps10 = require("@odla-ai/apps");
9169
9731
  var import_ai3 = require("@odla-ai/ai");
9170
9732
  var import_node_process10 = __toESM(require("process"), 1);
9171
9733
 
@@ -9175,7 +9737,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
9175
9737
  const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
9176
9738
  for (const integration of integrations) {
9177
9739
  for (const seed of integration.seeds ?? []) {
9178
- const payload = await postJson2(doFetch, `${base}/query`, dbKey, {
9740
+ const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
9179
9741
  query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
9180
9742
  });
9181
9743
  const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
@@ -9186,7 +9748,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
9186
9748
  out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
9187
9749
  continue;
9188
9750
  }
9189
- await postJson2(doFetch, `${base}/transact`, dbKey, {
9751
+ await postJson3(doFetch, `${base}/transact`, dbKey, {
9190
9752
  mutationId: `integration:${integration.id}:seed:${seed.id}`,
9191
9753
  ops: [{
9192
9754
  t: "update",
@@ -9201,7 +9763,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
9201
9763
  }
9202
9764
  }
9203
9765
  }
9204
- async function postJson2(doFetch, url, bearer, body) {
9766
+ async function postJson3(doFetch, url, bearer, body) {
9205
9767
  const res = await doFetch(url, {
9206
9768
  method: "POST",
9207
9769
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
@@ -9222,9 +9784,9 @@ async function responseText(res) {
9222
9784
  }
9223
9785
 
9224
9786
  // src/provision-credentials.ts
9225
- var import_apps5 = require("@odla-ai/apps");
9787
+ var import_apps9 = require("@odla-ai/apps");
9226
9788
  async function provisionEnvCredentials(opts) {
9227
- const tenantId = (0, import_apps5.tenantIdFor)(opts.cfg.app.id, opts.env);
9789
+ const tenantId = (0, import_apps9.tenantIdFor)(opts.cfg.app.id, opts.env);
9228
9790
  const prior = opts.credentials?.envs[opts.env];
9229
9791
  let credentials = opts.credentials;
9230
9792
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -9280,14 +9842,14 @@ async function mintDbKey(opts, tenantId) {
9280
9842
  appId: tenantId
9281
9843
  })
9282
9844
  });
9283
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText4(created)}`);
9845
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
9284
9846
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
9285
9847
  method: "POST",
9286
9848
  headers,
9287
9849
  body: "{}"
9288
9850
  });
9289
9851
  }
9290
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
9852
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
9291
9853
  const body = await res.json();
9292
9854
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
9293
9855
  return body.key;
@@ -9303,58 +9865,11 @@ async function issueO11yToken(opts) {
9303
9865
  `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`
9304
9866
  );
9305
9867
  }
9306
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText4(res)}`);
9868
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
9307
9869
  const body = await res.json();
9308
9870
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
9309
9871
  return body.token;
9310
9872
  }
9311
- async function safeText4(res) {
9312
- try {
9313
- return redactSecrets((await res.text()).slice(0, 500));
9314
- } catch {
9315
- return "";
9316
- }
9317
- }
9318
-
9319
- // src/provision-helpers.ts
9320
- var import_ai2 = require("@odla-ai/ai");
9321
- var import_apps6 = require("@odla-ai/apps");
9322
- function defaultSecretName(provider) {
9323
- const names = import_ai2.DEFAULT_SECRET_NAMES;
9324
- return names[provider] ?? `${provider}_api_key`;
9325
- }
9326
- async function assertTenantAdminAccess(doFetch, cfg, env, token) {
9327
- const tenantId = (0, import_apps6.tenantIdFor)(cfg.app.id, env);
9328
- const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
9329
- headers: { authorization: `Bearer ${token}` }
9330
- });
9331
- if (res.ok || res.status === 404) return;
9332
- if (res.status === 403) {
9333
- throw new Error(
9334
- `${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
9335
- );
9336
- }
9337
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
9338
- }
9339
- async function postJson3(doFetch, url, bearer, body) {
9340
- const res = await doFetch(url, {
9341
- method: "POST",
9342
- headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
9343
- body: JSON.stringify(body)
9344
- });
9345
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
9346
- }
9347
- function normalizeClerkConfig(value2) {
9348
- if (!value2) return null;
9349
- if (typeof value2 === "string") {
9350
- const publishableKey2 = envValue(value2);
9351
- return publishableKey2 ? { publishableKey: publishableKey2 } : null;
9352
- }
9353
- if (typeof value2 !== "object") return null;
9354
- const cfg = value2;
9355
- const publishableKey = envValue(cfg.publishableKey);
9356
- return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
9357
- }
9358
9873
  async function safeText5(res) {
9359
9874
  try {
9360
9875
  return redactSecrets((await res.text()).slice(0, 500));
@@ -9433,7 +9948,7 @@ async function provision(options) {
9433
9948
  }
9434
9949
  const doFetch = options.fetch ?? fetch;
9435
9950
  const token = await getDeveloperToken(cfg, options, doFetch, out);
9436
- const apps = (0, import_apps7.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
9951
+ const apps = (0, import_apps10.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
9437
9952
  const existing = await apps.resolveApp(cfg.app.id);
9438
9953
  if (existing) {
9439
9954
  out.log(`app: ${cfg.app.id} already exists`);
@@ -9444,7 +9959,7 @@ async function provision(options) {
9444
9959
  for (const env of cfg.envs) {
9445
9960
  await assertTenantAdminAccess(doFetch, cfg, env, token);
9446
9961
  }
9447
- const serviceOrder = (0, import_apps7.orderAppServices)(cfg.services);
9962
+ const serviceOrder = (0, import_apps10.orderAppServices)(cfg.services);
9448
9963
  for (const env of cfg.envs) {
9449
9964
  for (const service of serviceOrder) {
9450
9965
  if (service === "ai") {
@@ -9477,7 +9992,7 @@ async function provision(options) {
9477
9992
  }
9478
9993
  }
9479
9994
  for (const env of cfg.envs) {
9480
- const tenantId = (0, import_apps7.tenantIdFor)(cfg.app.id, env);
9995
+ const tenantId = (0, import_apps10.tenantIdFor)(cfg.app.id, env);
9481
9996
  credentials = await provisionEnvCredentials({
9482
9997
  cfg,
9483
9998
  env,
@@ -9510,11 +10025,11 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
9510
10025
  }
9511
10026
  }
9512
10027
  if (schema && dbKey) {
9513
- await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
10028
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
9514
10029
  out.log(`${env}: schema pushed`);
9515
10030
  }
9516
10031
  if (rules) {
9517
- await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
10032
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
9518
10033
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
9519
10034
  }
9520
10035
  if (dbKey) {
@@ -9611,6 +10126,7 @@ var COMMAND_SURFACE = {
9611
10126
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
9612
10127
  capabilities: {},
9613
10128
  code: { connect: {} },
10129
+ config: { diff: {}, plan: {} },
9614
10130
  context: { show: {}, list: {}, save: {}, remove: {} },
9615
10131
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
9616
10132
  discuss: {
@@ -9904,7 +10420,7 @@ async function runbookRemove(ctx, slug) {
9904
10420
 
9905
10421
  // src/runbook-import.ts
9906
10422
  var import_node_fs17 = require("fs");
9907
- var import_node_path13 = require("path");
10423
+ var import_node_path14 = require("path");
9908
10424
  function parseRunbook(text, slug) {
9909
10425
  let rest = text;
9910
10426
  const meta = {};
@@ -9933,8 +10449,8 @@ function readRunbookDir(dir) {
9933
10449
  const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
9934
10450
  if (!files.length) throw new Error(`no .md files in ${dir}`);
9935
10451
  return files.map((file) => {
9936
- const slug = (0, import_node_path13.basename)(file, ".md");
9937
- const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path13.join)(dir, file), "utf8"), slug);
10452
+ const slug = (0, import_node_path14.basename)(file, ".md");
10453
+ const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path14.join)(dir, file), "utf8"), slug);
9938
10454
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
9939
10455
  });
9940
10456
  }
@@ -10007,7 +10523,7 @@ async function upsert(ctx, r, visibility) {
10007
10523
  // src/runbook-impact.ts
10008
10524
  var import_node_child_process7 = require("child_process");
10009
10525
  var import_node_fs18 = require("fs");
10010
- var import_node_path14 = require("path");
10526
+ var import_node_path15 = require("path");
10011
10527
 
10012
10528
  // src/runbook-impact-scan.ts
10013
10529
  var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
@@ -10176,7 +10692,7 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
10176
10692
  }
10177
10693
  function manifestLabeller(root) {
10178
10694
  return (workspace) => {
10179
- const manifest = (0, import_node_path14.join)(root, workspace, "package.json");
10695
+ const manifest = (0, import_node_path15.join)(root, workspace, "package.json");
10180
10696
  if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
10181
10697
  try {
10182
10698
  const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
@@ -10246,7 +10762,7 @@ function report3(ctx, impacts) {
10246
10762
  async function runbookImpact(ctx, options, deps = {}) {
10247
10763
  const cwd = deps.cwd ?? process.cwd();
10248
10764
  const runGit = deps.runGit ?? gitRunner(cwd);
10249
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path14.join)(cwd, path), "utf8"));
10765
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path15.join)(cwd, path), "utf8"));
10250
10766
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
10251
10767
  if (!surfaces.length) {
10252
10768
  return ctx.out.log(
@@ -10381,7 +10897,7 @@ async function runbookComment(ctx, slug, body) {
10381
10897
  var import_node_child_process8 = require("child_process");
10382
10898
  var import_node_fs19 = require("fs");
10383
10899
  var import_node_os5 = require("os");
10384
- var import_node_path15 = require("path");
10900
+ var import_node_path16 = require("path");
10385
10901
  var import_node_process12 = __toESM(require("process"), 1);
10386
10902
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
10387
10903
  function resolveEditor(env = import_node_process12.default.env) {
@@ -10407,8 +10923,8 @@ function editText(initial, slug, deps = {}) {
10407
10923
  );
10408
10924
  if (!interactive())
10409
10925
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
10410
- const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10411
- const file = (0, import_node_path15.join)(dir, `${slug}.md`);
10926
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10927
+ const file = (0, import_node_path16.join)(dir, `${slug}.md`);
10412
10928
  try {
10413
10929
  (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
10414
10930
  const code = defaultRunOrInjected(deps)(editor, file);
@@ -10516,12 +11032,12 @@ var ALLOWED2 = [
10516
11032
  "platform",
10517
11033
  "context"
10518
11034
  ];
10519
- function requireSlug(slug, action) {
10520
- if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
11035
+ function requireSlug(slug, action2) {
11036
+ if (!slug) throw new Error(`"runbook ${action2}" needs a slug, e.g. "odla-ai runbook ${action2} release"`);
10521
11037
  return slug;
10522
11038
  }
10523
11039
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
10524
- async function buildContext3(parsed, deps, action) {
11040
+ async function buildContext3(parsed, deps, action2) {
10525
11041
  const appIdOption = stringOpt(parsed.options.app);
10526
11042
  const context = await resolveOperatorContext(parsed, {
10527
11043
  allowMissingConfig: true
@@ -10531,7 +11047,7 @@ async function buildContext3(parsed, deps, action) {
10531
11047
  const out = deps.stdout ?? console;
10532
11048
  const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
10533
11049
  const dryRun = parsed.options["dry-run"] === true;
10534
- if (action === "import" && dryRun) {
11050
+ if (action2 === "import" && dryRun) {
10535
11051
  return {
10536
11052
  platformUrl: cfg.platformUrl,
10537
11053
  token: "",
@@ -10541,12 +11057,12 @@ async function buildContext3(parsed, deps, action) {
10541
11057
  appId
10542
11058
  };
10543
11059
  }
10544
- const needsCapability = WRITES.has(action) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
11060
+ const needsCapability = WRITES.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
10545
11061
  const token = needsCapability ? await getScopedPlatformToken({
10546
11062
  platform: cfg.platformUrl,
10547
11063
  scope: "platform:runbook:write",
10548
11064
  email: stringOpt(parsed.options.email),
10549
- label: `odla CLI (runbook ${action})`,
11065
+ label: `odla CLI (runbook ${action2})`,
10550
11066
  fetch: doFetch,
10551
11067
  stdout: out,
10552
11068
  openApprovalUrl: deps.openUrl,
@@ -10581,11 +11097,11 @@ async function buildContext3(parsed, deps, action) {
10581
11097
  };
10582
11098
  }
10583
11099
  async function runbookCommand(parsed, deps = {}) {
10584
- const action = parsed.positionals[1] ?? "list";
10585
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
10586
- const ctx = await buildContext3(parsed, deps, action);
11100
+ const action2 = parsed.positionals[1] ?? "list";
11101
+ assertArgs(parsed, ALLOWED2, action2 === "ask" || action2 === "search" ? 64 : 4);
11102
+ const ctx = await buildContext3(parsed, deps, action2);
10587
11103
  const slug = parsed.positionals[2];
10588
- switch (action) {
11104
+ switch (action2) {
10589
11105
  case "list":
10590
11106
  return runbookList(ctx, parsed.options.all === true, stringOpt(parsed.options.q));
10591
11107
  case "ask": {
@@ -10670,7 +11186,7 @@ async function runbookCommand(parsed, deps = {}) {
10670
11186
  case "rm":
10671
11187
  return runbookRemove(ctx, requireSlug(slug, "rm"));
10672
11188
  default:
10673
- throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
11189
+ throw new Error(`unknown runbook action "${action2}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
10674
11190
  }
10675
11191
  }
10676
11192
 
@@ -10828,7 +11344,7 @@ function hostedSeverity(value2, flag) {
10828
11344
  var import_security2 = require("@odla-ai/security");
10829
11345
 
10830
11346
  // src/security.ts
10831
- var import_node_path16 = require("path");
11347
+ var import_node_path17 = require("path");
10832
11348
  var import_security = require("@odla-ai/security");
10833
11349
  var import_node3 = require("@odla-ai/security/node");
10834
11350
  async function runHostedSecurity(options) {
@@ -10840,9 +11356,9 @@ async function runHostedSecurity(options) {
10840
11356
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
10841
11357
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
10842
11358
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
10843
- const target = (0, import_node_path16.resolve)(options.target ?? cfg?.rootDir ?? ".");
10844
- const output = (0, import_node_path16.resolve)(options.out ?? (0, import_node_path16.resolve)(target, ".odla/security/hosted"));
10845
- const outputRelative = (0, import_node_path16.relative)(target, output).split(import_node_path16.sep).join("/");
11359
+ const target = (0, import_node_path17.resolve)(options.target ?? cfg?.rootDir ?? ".");
11360
+ const output = (0, import_node_path17.resolve)(options.out ?? (0, import_node_path17.resolve)(target, ".odla/security/hosted"));
11361
+ const outputRelative = (0, import_node_path17.relative)(target, output).split(import_node_path17.sep).join("/");
10846
11362
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
10847
11363
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
10848
11364
  const tokenRequest = {
@@ -10854,7 +11370,7 @@ async function runHostedSecurity(options) {
10854
11370
  };
10855
11371
  const token = await injectedToken(options, tokenRequest);
10856
11372
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
10857
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path16.isAbsolute)(outputRelative) ? [outputRelative] : []
11373
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path17.isAbsolute)(outputRelative) ? [outputRelative] : []
10858
11374
  });
10859
11375
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
10860
11376
  platform,
@@ -10872,7 +11388,7 @@ async function runHostedSecurity(options) {
10872
11388
  });
10873
11389
  const harness = (0, import_security.createSecurityHarness)({
10874
11390
  profile,
10875
- store: new import_node3.FileRunStore((0, import_node_path16.resolve)(output, "state")),
11391
+ store: new import_node3.FileRunStore((0, import_node_path17.resolve)(output, "state")),
10876
11392
  discoveryReasoner: hosted.discoveryReasoner,
10877
11393
  validationReasoner: hosted.validationReasoner,
10878
11394
  policy: {
@@ -10896,7 +11412,7 @@ async function runHostedSecurity(options) {
10896
11412
  function selectEnv(requested, declared, configPath, rootDir) {
10897
11413
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
10898
11414
  if (!env || !declared.includes(env)) {
10899
- const shown = (0, import_node_path16.relative)(rootDir, configPath) || configPath;
11415
+ const shown = (0, import_node_path17.relative)(rootDir, configPath) || configPath;
10900
11416
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
10901
11417
  }
10902
11418
  return env;
@@ -10925,7 +11441,7 @@ function printSummary(out, appId, env, run, report4, output) {
10925
11441
  out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
10926
11442
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
10927
11443
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
10928
- out.log(` report: ${(0, import_node_path16.resolve)(output, "REPORT.md")}`);
11444
+ out.log(` report: ${(0, import_node_path17.resolve)(output, "REPORT.md")}`);
10929
11445
  }
10930
11446
  function formatBudget(usage) {
10931
11447
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -11302,8 +11818,8 @@ async function securityCommand(parsed, dependencies) {
11302
11818
  else await runLocalSecurityCommand(parsed, dependencies);
11303
11819
  }
11304
11820
  async function githubSecurityCommand(parsed, dependencies) {
11305
- const action = parsed.positionals[2];
11306
- if (action === "disconnect") {
11821
+ const action2 = parsed.positionals[2];
11822
+ if (action2 === "disconnect") {
11307
11823
  assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
11308
11824
  const context2 = await hostedSecurityContext(parsed, dependencies);
11309
11825
  const sourceId = requiredString(parsed.options.source, "--source");
@@ -11318,7 +11834,7 @@ async function githubSecurityCommand(parsed, dependencies) {
11318
11834
  context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
11319
11835
  return;
11320
11836
  }
11321
- if (action !== "connect") {
11837
+ if (action2 !== "connect") {
11322
11838
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
11323
11839
  }
11324
11840
  assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
@@ -11538,8 +12054,11 @@ async function calendarCommand(parsed, dependencies) {
11538
12054
  calendarServiceConfig,
11539
12055
  calendarStatus,
11540
12056
  codeConnect,
12057
+ configDiff,
12058
+ configPlan,
11541
12059
  connectGitHubSecuritySource,
11542
12060
  describeProblem,
12061
+ desiredRegistryState,
11543
12062
  disconnectGitHubSecuritySource,
11544
12063
  doctor,
11545
12064
  exitCodeFor,
@@ -11559,6 +12078,7 @@ async function calendarCommand(parsed, dependencies) {
11559
12078
  prepareCodeImages,
11560
12079
  printCapabilities,
11561
12080
  provision,
12081
+ reconcileConfig,
11562
12082
  redactSecrets,
11563
12083
  repositoryFromGitRemote,
11564
12084
  runCli,