@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/bin.cjs CHANGED
@@ -43,7 +43,9 @@ function approvalLines(prompt) {
43
43
  lines.push("");
44
44
  lines.push(` ${prompt.approvalUrl}`);
45
45
  lines.push("");
46
- lines.push(" A signed-in odla admin opens that URL, checks the code matches, and approves.");
46
+ lines.push(
47
+ ` ${prompt.approver ?? "The matching signed-in odla account"} opens that URL, checks the code matches, and approves.`
48
+ );
47
49
  lines.push(" Opening the link without approving does nothing; this command waits until they do.");
48
50
  if (prompt.browserAttempted) {
49
51
  lines.push(" A browser launch was attempted, but it is best-effort and may have shown no tab.");
@@ -490,6 +492,7 @@ function audienceBoundEnvToken(token, platform) {
490
492
  }
491
493
  var SCOPE_PURPOSE = {
492
494
  "platform:status:read": "read the platform fleet health and deployment snapshot",
495
+ "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
493
496
  "platform:runbook:write": "add or edit odla's operational runbooks",
494
497
  "platform:ai:policy:write": "change System AI model routing",
495
498
  "platform:ai:policy:read": "read System AI model routing",
@@ -526,6 +529,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
526
529
  approvalUrl,
527
530
  minutesLeft: Math.floor(expiresIn / 60),
528
531
  purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
532
+ approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
529
533
  browserAttempted: browser.open,
530
534
  browserSkipped: browser.reason
531
535
  });
@@ -695,10 +699,10 @@ async function responseBody2(res) {
695
699
  return { message: text.slice(0, 300) };
696
700
  }
697
701
  }
698
- function apiError2(action, status, body) {
702
+ function apiError2(action2, status, body) {
699
703
  const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
700
704
  const message2 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
701
- return `${action} failed (${status}): ${message2}`;
705
+ return `${action2} failed (${status}): ${message2}`;
702
706
  }
703
707
  function isRecord2(value2) {
704
708
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
@@ -876,10 +880,10 @@ async function responseBody3(res) {
876
880
  return { message: text.slice(0, 300) };
877
881
  }
878
882
  }
879
- function apiError3(action, status, body) {
883
+ function apiError3(action2, status, body) {
880
884
  const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
881
885
  const message2 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
882
- return `${action} failed (${status}): ${message2}`;
886
+ return `${action2} failed (${status}): ${message2}`;
883
887
  }
884
888
  function isRecord3(value2) {
885
889
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
@@ -1503,21 +1507,21 @@ var SET_OPTIONS = [
1503
1507
  ];
1504
1508
  async function adminCommand(parsed, deps = {}) {
1505
1509
  const area = parsed.positionals[1];
1506
- const action = parsed.positionals[2];
1507
- const credentialSet = action === "credential" && parsed.positionals[3] === "set";
1508
- const credentials = action === "credentials";
1509
- const models = action === "models";
1510
- const usage = action === "usage";
1511
- const audit = action === "audit";
1512
- if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
1510
+ const action2 = parsed.positionals[2];
1511
+ const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
1512
+ const credentials = action2 === "credentials";
1513
+ const models = action2 === "models";
1514
+ const usage = action2 === "usage";
1515
+ const audit = action2 === "audit";
1516
+ if (area !== "ai" || action2 !== "show" && action2 !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
1513
1517
  throw new Error('unknown admin command. Try "odla-ai admin ai show".');
1514
1518
  }
1515
- 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;
1516
- assertArgs(parsed, allowed, credentialSet ? 5 : action === "set" ? 4 : 3);
1519
+ 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;
1520
+ assertArgs(parsed, allowed, credentialSet ? 5 : action2 === "set" ? 4 : 3);
1517
1521
  const context = await resolveOperatorContext(parsed, { allowMissingConfig: true });
1518
1522
  await adminAi({
1519
- action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
1520
- purpose: action === "set" ? parsed.positionals[3] : void 0,
1523
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action2,
1524
+ purpose: action2 === "set" ? parsed.positionals[3] : void 0,
1521
1525
  provider: stringOpt(parsed.options.provider),
1522
1526
  model: stringOpt(parsed.options.model),
1523
1527
  enabled: boolOpt(parsed.options.enabled),
@@ -1574,12 +1578,12 @@ function bothTenants(cfg) {
1574
1578
 
1575
1579
  // src/agent-command.ts
1576
1580
  async function agentCommand(parsed, deps = {}) {
1577
- const action = parsed.positionals[1];
1578
- if (action !== "jobs" && action !== "retry") {
1579
- throw new Error(`unknown agent action "${action ?? ""}". Try "odla-ai agent jobs --json".`);
1581
+ const action2 = parsed.positionals[1];
1582
+ if (action2 !== "jobs" && action2 !== "retry") {
1583
+ throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
1580
1584
  }
1581
- assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action === "jobs" ? 2 : 3);
1582
- if (action === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
1585
+ assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
1586
+ if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
1583
1587
  throw new Error('--state and --limit are supported only by "agent jobs"');
1584
1588
  }
1585
1589
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
@@ -1599,7 +1603,7 @@ async function agentCommand(parsed, deps = {}) {
1599
1603
  );
1600
1604
  const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
1601
1605
  const headers = { authorization: `Bearer ${credential2}` };
1602
- if (action === "retry") {
1606
+ if (action2 === "retry") {
1603
1607
  const id = parsed.positionals[2];
1604
1608
  const res2 = await doFetch(`${base}/${encodeURIComponent(id)}/retry`, { method: "POST", headers });
1605
1609
  const body2 = await readJson(res2);
@@ -2035,7 +2039,7 @@ async function copyFiles(cfg, token, route2, doFetch, out) {
2035
2039
  }
2036
2040
 
2037
2041
  // src/app-lifecycle.ts
2038
- async function lifecycleCall(action, options) {
2042
+ async function lifecycleCall(action2, options) {
2039
2043
  const cfg = await loadProjectConfig(options.configPath);
2040
2044
  const out = options.stdout ?? console;
2041
2045
  const doFetch = options.fetch ?? fetch;
@@ -2045,13 +2049,13 @@ async function lifecycleCall(action, options) {
2045
2049
  doFetch,
2046
2050
  out
2047
2051
  );
2048
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action}`, {
2052
+ const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
2049
2053
  method: "POST",
2050
2054
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
2051
2055
  });
2052
2056
  const body = await res.json().catch(() => ({}));
2053
2057
  if (!res.ok || !body.ok) {
2054
- throw new Error(`${action} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
2058
+ throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
2055
2059
  }
2056
2060
  return body;
2057
2061
  }
@@ -2604,8 +2608,8 @@ function pollTimeout(value2 = 10 * 6e4) {
2604
2608
  if (!Number.isSafeInteger(value2) || value2 < 1e3 || value2 > 60 * 6e4) throw new Error("calendar poll timeout must be 1000-3600000ms");
2605
2609
  return value2;
2606
2610
  }
2607
- function productionConsent(env, yes, action) {
2608
- if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action} for "${env}" without --yes`);
2611
+ function productionConsent(env, yes, action2) {
2612
+ if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action2} for "${env}" without --yes`);
2609
2613
  }
2610
2614
  function printStatus(status, json, out) {
2611
2615
  if (json) {
@@ -2635,6 +2639,7 @@ var CAPABILITIES = {
2635
2639
  "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",
2636
2640
  "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",
2637
2641
  "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",
2642
+ "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",
2638
2643
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
2639
2644
  "run app-attributed hosted security discovery and independent validation without provider keys",
2640
2645
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -2681,15 +2686,542 @@ function printGroup(out, heading, items) {
2681
2686
  out.log("");
2682
2687
  }
2683
2688
 
2689
+ // src/config-reconcile-command.ts
2690
+ var import_apps6 = require("@odla-ai/apps");
2691
+ var import_node_path7 = require("path");
2692
+
2693
+ // src/config-reconcile-digest.ts
2694
+ var import_node_crypto = require("crypto");
2695
+ function canonicalJson(value2) {
2696
+ return JSON.stringify(canonicalValue(value2));
2697
+ }
2698
+ function configDigest(value2) {
2699
+ return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2700
+ }
2701
+ function canonicalValue(value2) {
2702
+ if (Array.isArray(value2)) return value2.map(canonicalValue);
2703
+ if (value2 && typeof value2 === "object") {
2704
+ return Object.fromEntries(
2705
+ Object.entries(value2).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
2706
+ );
2707
+ }
2708
+ return value2;
2709
+ }
2710
+
2711
+ // src/config-reconcile-desired.ts
2712
+ var import_apps4 = require("@odla-ai/apps");
2713
+
2714
+ // src/provision-helpers.ts
2715
+ var import_ai = require("@odla-ai/ai");
2716
+ var import_apps3 = require("@odla-ai/apps");
2717
+ function defaultSecretName(provider) {
2718
+ const names = import_ai.DEFAULT_SECRET_NAMES;
2719
+ return names[provider] ?? `${provider}_api_key`;
2720
+ }
2721
+ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2722
+ const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
2723
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
2724
+ headers: { authorization: `Bearer ${token}` }
2725
+ });
2726
+ if (res.ok || res.status === 404) return;
2727
+ if (res.status === 403) {
2728
+ throw new Error(
2729
+ `${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`
2730
+ );
2731
+ }
2732
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
2733
+ }
2734
+ async function postJson(doFetch, url, bearer, body) {
2735
+ const res = await doFetch(url, {
2736
+ method: "POST",
2737
+ headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
2738
+ body: JSON.stringify(body)
2739
+ });
2740
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
2741
+ }
2742
+ function normalizeClerkConfig(value2) {
2743
+ if (!value2) return null;
2744
+ if (typeof value2 === "string") {
2745
+ const publishableKey2 = envValue(value2);
2746
+ return publishableKey2 ? { publishableKey: publishableKey2 } : null;
2747
+ }
2748
+ if (typeof value2 !== "object") return null;
2749
+ const cfg = value2;
2750
+ const publishableKey = envValue(cfg.publishableKey);
2751
+ return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
2752
+ }
2753
+ async function safeText3(res) {
2754
+ try {
2755
+ return redactSecrets((await res.text()).slice(0, 500));
2756
+ } catch {
2757
+ return "";
2758
+ }
2759
+ }
2760
+
2761
+ // src/config-reconcile-desired.ts
2762
+ function desiredRegistryState(cfg) {
2763
+ const environments = {};
2764
+ const services = (0, import_apps4.orderAppServices)(cfg.services);
2765
+ for (const env of cfg.envs) {
2766
+ const desiredServices = {};
2767
+ for (const service of services) {
2768
+ desiredServices[service] = {
2769
+ enabled: true,
2770
+ ...managedServiceConfig(cfg, env, service)
2771
+ };
2772
+ }
2773
+ const authoredAuth = cfg.auth?.clerk && Object.hasOwn(cfg.auth.clerk, env) ? cfg.auth.clerk[env] : void 0;
2774
+ const auth = normalizeClerkConfig(authoredAuth);
2775
+ if (authoredAuth && !auth) {
2776
+ throw new Error(`auth.clerk.${env} could not resolve its publishable key`);
2777
+ }
2778
+ const linkManaged = !!cfg.links && Object.hasOwn(cfg.links, env);
2779
+ environments[env] = {
2780
+ services: desiredServices,
2781
+ ...auth ? { auth } : {},
2782
+ ...linkManaged ? { link: cfg.links?.[env] ?? null } : {}
2783
+ };
2784
+ }
2785
+ return {
2786
+ appId: cfg.app.id,
2787
+ name: cfg.app.name,
2788
+ environments
2789
+ };
2790
+ }
2791
+ function managedServiceConfig(cfg, env, service) {
2792
+ if (service === "ai" && cfg.ai?.provider) {
2793
+ return {
2794
+ config: {
2795
+ provider: cfg.ai.provider,
2796
+ ...cfg.ai.model ? { model: cfg.ai.model } : {}
2797
+ }
2798
+ };
2799
+ }
2800
+ if (service === "calendar") return { config: calendarServiceConfig(cfg, env) };
2801
+ return {};
2802
+ }
2803
+
2804
+ // src/config-reconcile.ts
2805
+ var import_apps5 = require("@odla-ai/apps");
2806
+
2807
+ // src/config-reconcile-values.ts
2808
+ function difference(path, desired, observed, status, reason, desiredSource, observedSource, env, service) {
2809
+ return { path, env, service, status, desired, observed, desiredSource, observedSource, reason };
2810
+ }
2811
+ function action(kind, path, before, after, risk, applySupport, reason) {
2812
+ return { kind, path, before, after, risk, requiresApproval: false, applySupport, reason };
2813
+ }
2814
+ function scopedAction(kind, path, env, service, before, after, reason, destructive = false) {
2815
+ const production = env === "prod" || env === "production";
2816
+ return {
2817
+ ...action(
2818
+ kind,
2819
+ path,
2820
+ before,
2821
+ after,
2822
+ destructive ? "high" : production ? "medium" : "low",
2823
+ destructive ? "studio" : "provision",
2824
+ reason
2825
+ ),
2826
+ env,
2827
+ ...service ? { service } : {},
2828
+ requiresApproval: production || destructive
2829
+ };
2830
+ }
2831
+ function observedProjection(app) {
2832
+ return {
2833
+ appId: app.appId,
2834
+ name: app.name,
2835
+ archivedAt: app.archivedAt,
2836
+ environments: Object.fromEntries(
2837
+ Object.entries(app.environments).sort(([left], [right]) => left.localeCompare(right)).map(([env, services]) => [
2838
+ env,
2839
+ Object.fromEntries(Object.entries(services).sort(([left], [right]) => left.localeCompare(right)))
2840
+ ])
2841
+ ),
2842
+ auth: app.auth,
2843
+ links: app.links
2844
+ };
2845
+ }
2846
+ function projectConfig(value2, keys) {
2847
+ return Object.fromEntries(keys.filter((key) => value2[key] !== void 0).map((key) => [key, value2[key]]));
2848
+ }
2849
+ function same(left, right) {
2850
+ return canonicalJson(left) === canonicalJson(right);
2851
+ }
2852
+ function actionId(actionValue) {
2853
+ return `action-${configDigest(actionValue).slice("sha256:".length, "sha256:".length + 16)}`;
2854
+ }
2855
+ function quoteArg(value2) {
2856
+ return `'${value2.replace(/'/g, `'\\''`)}'`;
2857
+ }
2858
+
2859
+ // src/config-reconcile.ts
2860
+ var COVERAGE = {
2861
+ included: [
2862
+ "app display name",
2863
+ "environment service enablement",
2864
+ "AI provider/model and calendar booking service configuration",
2865
+ "auth configuration when explicitly declared",
2866
+ "deployment link when explicitly declared"
2867
+ ],
2868
+ excluded: [
2869
+ { path: "db.schema", reason: "owned by the database data plane" },
2870
+ { path: "db.rules", reason: "owned by the database data plane" },
2871
+ { path: "integrations.seeds", reason: "guarded data-plane writes are not Registry state" },
2872
+ { path: "credentials", reason: "shown-once credentials are intentionally local" },
2873
+ { path: "secrets", reason: "vault and Worker secrets are write-only" },
2874
+ { path: "calendar.connection", reason: "OAuth connection state is provider-owned" },
2875
+ { path: "service.config.opaque", reason: "controller-assigned fields remain service-owned" }
2876
+ ]
2877
+ };
2878
+ function reconcileConfig(input) {
2879
+ const desiredSource = { kind: "project_config", location: input.configPath };
2880
+ const observedSource = {
2881
+ kind: "registry",
2882
+ location: `${input.platformUrl}/registry/apps/${encodeURIComponent(input.desired.appId)}`
2883
+ };
2884
+ const differences = [];
2885
+ const actions = [];
2886
+ const add = (difference2, action2) => {
2887
+ differences.push(difference2);
2888
+ if (action2) actions.push({ ...action2, id: actionId(action2) });
2889
+ };
2890
+ compareApp(input.desired, input.observed, desiredSource, observedSource, add);
2891
+ compareEnvironments(input.desired, input.observed, desiredSource, observedSource, add);
2892
+ const desiredRevision = configDigest(input.desired);
2893
+ const observedRevision = input.observed ? configDigest(observedProjection(input.observed)) : null;
2894
+ const different = differences.filter((entry) => entry.status === "different").length;
2895
+ const unmanaged = differences.length - different;
2896
+ return {
2897
+ generatedAt: input.generatedAt,
2898
+ scope: {
2899
+ appId: input.desired.appId,
2900
+ platformUrl: input.platformUrl,
2901
+ environments: Object.keys(input.desired.environments).sort()
2902
+ },
2903
+ sources: { desired: desiredSource, observed: observedSource },
2904
+ desiredRevision,
2905
+ observedRevision,
2906
+ status: different ? "different" : unmanaged ? "unmanaged" : "in_sync",
2907
+ summary: {
2908
+ differences: different,
2909
+ unmanaged,
2910
+ actions: actions.length,
2911
+ productionActions: actions.filter((action2) => action2.env === "prod" || action2.env === "production").length,
2912
+ highRiskActions: actions.filter((action2) => action2.risk === "high").length
2913
+ },
2914
+ coverage: COVERAGE,
2915
+ differences,
2916
+ actions
2917
+ };
2918
+ }
2919
+ function compareApp(desired, observed, desiredSource, observedSource, add) {
2920
+ if (!observed) {
2921
+ const path = "app";
2922
+ add(
2923
+ difference(path, desired, null, "different", "the app is absent from Registry", desiredSource, observedSource),
2924
+ action(
2925
+ "create_app",
2926
+ path,
2927
+ null,
2928
+ { appId: desired.appId, name: desired.name },
2929
+ "low",
2930
+ "provision",
2931
+ "create the repository-declared app"
2932
+ )
2933
+ );
2934
+ return;
2935
+ }
2936
+ if (observed.name !== desired.name) {
2937
+ const path = "app.name";
2938
+ add(
2939
+ difference(path, desired.name, observed.name, "different", "the checked-in display name differs", desiredSource, observedSource),
2940
+ {
2941
+ ...action("rename_app", path, observed.name, desired.name, "low", "command", "make Registry match the checked-in display name"),
2942
+ command: `odla-ai app rename ${quoteArg(desired.name)} --config ${quoteArg(desiredSource.location)}`
2943
+ }
2944
+ );
2945
+ }
2946
+ }
2947
+ function compareEnvironments(desired, observed, desiredSource, observedSource, add) {
2948
+ const envs = /* @__PURE__ */ new Set([
2949
+ ...Object.keys(desired.environments),
2950
+ ...Object.keys(observed?.environments ?? {}).filter((env) => Object.values(observed?.environments[env] ?? {}).some((service) => service.enabled))
2951
+ ]);
2952
+ for (const env of [...envs].sort()) {
2953
+ compareServices(env, desired, observed, desiredSource, observedSource, add);
2954
+ compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add);
2955
+ }
2956
+ }
2957
+ function compareServices(env, desired, observed, desiredSource, observedSource, add) {
2958
+ const wanted = desired.environments[env]?.services ?? {};
2959
+ const live = observed?.environments[env] ?? {};
2960
+ const knownOrder = (0, import_apps5.orderAppServices)((0, import_apps5.appServiceIds)());
2961
+ const services = [.../* @__PURE__ */ new Set([...knownOrder, ...Object.keys(wanted), ...Object.keys(live)])];
2962
+ for (const service of services) {
2963
+ const next = wanted[service];
2964
+ const current = live[service];
2965
+ const path = `environments.${env}.services.${service}`;
2966
+ if (next?.enabled && !current?.enabled) {
2967
+ add(
2968
+ difference(path, next, current ?? null, "different", "the repository enables a service Registry does not", desiredSource, observedSource, env, service),
2969
+ scopedAction("enable_service", path, env, service, current ?? null, next, "enable the repository-declared service")
2970
+ );
2971
+ continue;
2972
+ }
2973
+ if (next?.enabled && current?.enabled && next.config) {
2974
+ const projected = projectConfig(current.config, Object.keys(next.config));
2975
+ if (!same(next.config, projected)) {
2976
+ add(
2977
+ difference(`${path}.config`, next.config, projected, "different", "repository-managed service settings differ", desiredSource, observedSource, env, service),
2978
+ scopedAction(
2979
+ "configure_service",
2980
+ `${path}.config`,
2981
+ env,
2982
+ service,
2983
+ projected,
2984
+ next.config,
2985
+ "apply the repository-managed service settings"
2986
+ )
2987
+ );
2988
+ }
2989
+ }
2990
+ }
2991
+ for (const service of [...services].reverse()) {
2992
+ const next = wanted[service];
2993
+ const current = live[service];
2994
+ if (next?.enabled || !current?.enabled) continue;
2995
+ const path = `environments.${env}.services.${service}`;
2996
+ add(
2997
+ difference(path, null, { enabled: true }, "different", "Registry enables a service absent from repository intent", desiredSource, observedSource, env, service),
2998
+ scopedAction(
2999
+ "disable_service",
3000
+ path,
3001
+ env,
3002
+ service,
3003
+ { enabled: true },
3004
+ { enabled: false },
3005
+ "disablement is destructive to availability and requires an explicit Studio review",
3006
+ true
3007
+ )
3008
+ );
3009
+ }
3010
+ }
3011
+ function compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add) {
3012
+ const wanted = desired.environments[env];
3013
+ const liveAuth = observed?.auth[env] ?? null;
3014
+ if (wanted?.auth) {
3015
+ const projected = liveAuth ? projectConfig(liveAuth, Object.keys(wanted.auth)) : null;
3016
+ if (!same(wanted.auth, projected)) {
3017
+ const path = `environments.${env}.auth`;
3018
+ add(
3019
+ difference(path, wanted.auth, projected, "different", "explicit repository auth settings differ", desiredSource, observedSource, env),
3020
+ scopedAction("set_auth", path, env, void 0, projected, wanted.auth, "apply explicit repository auth settings")
3021
+ );
3022
+ }
3023
+ } else if (liveAuth) {
3024
+ add(difference(
3025
+ `environments.${env}.auth`,
3026
+ null,
3027
+ projectConfig(liveAuth, ["publishableKey", "audience", "mode"]),
3028
+ "unmanaged",
3029
+ "live auth exists but this project config does not manage it",
3030
+ desiredSource,
3031
+ observedSource,
3032
+ env
3033
+ ));
3034
+ }
3035
+ const managesLink = wanted && Object.hasOwn(wanted, "link");
3036
+ const liveLink = observed?.links[env] ?? null;
3037
+ if (managesLink && wanted.link !== liveLink) {
3038
+ const path = `environments.${env}.link`;
3039
+ add(
3040
+ difference(path, wanted.link ?? null, liveLink, "different", "explicit repository deployment link differs", desiredSource, observedSource, env),
3041
+ scopedAction("set_link", path, env, void 0, liveLink, wanted.link ?? null, "apply the repository deployment link")
3042
+ );
3043
+ } else if (!managesLink && liveLink) {
3044
+ add(difference(
3045
+ `environments.${env}.link`,
3046
+ null,
3047
+ liveLink,
3048
+ "unmanaged",
3049
+ "a live deployment link exists but this project config does not manage it",
3050
+ desiredSource,
3051
+ observedSource,
3052
+ env
3053
+ ));
3054
+ }
3055
+ }
3056
+
3057
+ // src/config-reconcile-command.ts
3058
+ async function configDiff(options) {
3059
+ const reconciliation = await inspectConfig(options);
3060
+ const { actions: _actions, ...read3 } = reconciliation;
3061
+ const document2 = {
3062
+ schemaVersion: "odla.config-diff/v1",
3063
+ ...read3,
3064
+ nextActions: diffNextActions(reconciliation, options.configPath)
3065
+ };
3066
+ printDiff(document2, options);
3067
+ return document2;
3068
+ }
3069
+ async function configPlan(options) {
3070
+ const reconciliation = await inspectConfig(options);
3071
+ const planDigest = configDigest({
3072
+ schemaVersion: "odla.config-plan/v1",
3073
+ desiredRevision: reconciliation.desiredRevision,
3074
+ observedRevision: reconciliation.observedRevision,
3075
+ actions: reconciliation.actions
3076
+ });
3077
+ const document2 = {
3078
+ schemaVersion: "odla.config-plan/v1",
3079
+ ...reconciliation,
3080
+ planDigest,
3081
+ apply: {
3082
+ supported: false,
3083
+ reason: "conditional, resumable config apply is not available yet; use the exact reviewed commands below"
3084
+ },
3085
+ nextActions: planNextActions(reconciliation, options.configPath)
3086
+ };
3087
+ printPlan2(document2, options);
3088
+ return document2;
3089
+ }
3090
+ async function inspectConfig(options) {
3091
+ const out = options.stdout ?? console;
3092
+ const cfg = await loadProjectConfig(options.configPath);
3093
+ const doFetch = options.fetch ?? fetch;
3094
+ const token = await resolveAdminPlatformToken({
3095
+ platform: cfg.platformUrl,
3096
+ scope: "app:config:read",
3097
+ token: options.token,
3098
+ tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3099
+ rootDir: cfg.rootDir,
3100
+ email: options.email,
3101
+ open: options.open,
3102
+ fetch: doFetch,
3103
+ stdout: out,
3104
+ openApprovalUrl: options.openApprovalUrl,
3105
+ label: `odla CLI (${cfg.app.id} config read)`
3106
+ });
3107
+ const client = (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3108
+ const observed = await client.resolveApp(cfg.app.id);
3109
+ return reconcileConfig({
3110
+ desired: desiredRegistryState(cfg),
3111
+ observed,
3112
+ configPath: cfg.configPath,
3113
+ platformUrl: cfg.platformUrl,
3114
+ generatedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
3115
+ });
3116
+ }
3117
+ function printDiff(document2, options) {
3118
+ const out = options.stdout ?? console;
3119
+ if (options.json) {
3120
+ out.log(JSON.stringify(document2, null, 2));
3121
+ return;
3122
+ }
3123
+ printHeader(out, "diff", document2);
3124
+ printDifferences(out, document2);
3125
+ printNext(out, document2.nextActions);
3126
+ }
3127
+ function printPlan2(document2, options) {
3128
+ const out = options.stdout ?? console;
3129
+ if (options.json) {
3130
+ out.log(JSON.stringify(document2, null, 2));
3131
+ return;
3132
+ }
3133
+ printHeader(out, "plan", document2);
3134
+ for (const entry of document2.actions) {
3135
+ const scope = [entry.env, entry.service].filter(Boolean).join("/");
3136
+ out.log(
3137
+ ` ${entry.id} ${entry.kind}${scope ? ` (${scope})` : ""} ${entry.risk}${entry.requiresApproval ? ", approval required" : ""}`
3138
+ );
3139
+ out.log(` ${entry.reason}`);
3140
+ }
3141
+ if (!document2.actions.length) out.log(" no managed changes");
3142
+ out.log(`plan digest: ${document2.planDigest}`);
3143
+ out.log(`apply: unsupported \u2014 ${document2.apply.reason}`);
3144
+ printNext(out, document2.nextActions);
3145
+ }
3146
+ function printHeader(out, kind, document2) {
3147
+ out.log(`config ${kind}: ${document2.scope.appId} \u2014 ${document2.status}`);
3148
+ out.log(`platform: ${document2.scope.platformUrl}`);
3149
+ out.log(`desired: ${document2.desiredRevision}`);
3150
+ out.log(`observed: ${document2.observedRevision ?? "absent"}`);
3151
+ out.log(
3152
+ `summary: ${document2.summary.differences} different, ${document2.summary.unmanaged} unmanaged, ${document2.summary.actions} planned actions`
3153
+ );
3154
+ }
3155
+ function printDifferences(out, document2) {
3156
+ for (const entry of document2.differences) {
3157
+ out.log(` ${entry.status} ${entry.path} ${entry.reason}`);
3158
+ }
3159
+ if (!document2.differences.length) out.log(" managed Registry configuration is in sync");
3160
+ }
3161
+ function printNext(out, next) {
3162
+ if (!next.length) return;
3163
+ out.log("next:");
3164
+ for (const item of next) out.log(` ${item.command}
3165
+ ${item.description}`);
3166
+ }
3167
+ function diffNextActions(reconciliation, configPath) {
3168
+ if (reconciliation.status === "in_sync") {
3169
+ return [{
3170
+ code: "verify_runtime",
3171
+ command: `odla-ai smoke --config ${quoteArg2(configPath)}`,
3172
+ description: "Registry intent matches; verify the service-owned data planes separately."
3173
+ }];
3174
+ }
3175
+ return [{
3176
+ code: "freeze_plan",
3177
+ command: `odla-ai config plan --config ${quoteArg2(configPath)} --json`,
3178
+ description: "Freeze the current desired and observed revisions into a reviewable action plan."
3179
+ }];
3180
+ }
3181
+ function planNextActions(reconciliation, configPath) {
3182
+ const next = [];
3183
+ if (reconciliation.actions.some((action2) => action2.applySupport === "provision")) {
3184
+ next.push({
3185
+ code: "review_provision",
3186
+ command: `odla-ai provision --config ${quoteArg2(configPath)} --dry-run`,
3187
+ description: "Review the existing provisioner's service/auth/link work before applying it."
3188
+ });
3189
+ }
3190
+ for (const action2 of reconciliation.actions.filter((entry) => entry.command)) {
3191
+ if (!next.some((entry) => entry.command === action2.command)) {
3192
+ next.push({ code: action2.kind, command: action2.command, description: action2.reason });
3193
+ }
3194
+ }
3195
+ if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3196
+ const { appId, platformUrl } = reconciliation.scope;
3197
+ next.push({
3198
+ code: "review_destructive",
3199
+ command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3200
+ description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3201
+ });
3202
+ }
3203
+ if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3204
+ next.push({
3205
+ code: "declare_or_accept_runtime_state",
3206
+ command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3207
+ description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3208
+ });
3209
+ }
3210
+ return next;
3211
+ }
3212
+ function quoteArg2(value2) {
3213
+ return `'${value2.replace(/'/g, `'\\''`)}'`;
3214
+ }
3215
+
2684
3216
  // src/doctor-checks.ts
2685
3217
  var import_node_child_process3 = require("child_process");
2686
3218
  var import_node_fs10 = require("fs");
2687
- var import_node_path8 = require("path");
3219
+ var import_node_path9 = require("path");
2688
3220
 
2689
3221
  // src/wrangler.ts
2690
3222
  var import_node_child_process2 = require("child_process");
2691
3223
  var import_node_fs9 = require("fs");
2692
- var import_node_path7 = require("path");
3224
+ var import_node_path8 = require("path");
2693
3225
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
2694
3226
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
2695
3227
  let stdout = "";
@@ -2703,7 +3235,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
2703
3235
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
2704
3236
  function findWranglerConfig(rootDir) {
2705
3237
  for (const name of WRANGLER_CONFIG_FILES) {
2706
- const path = (0, import_node_path7.join)(rootDir, name);
3238
+ const path = (0, import_node_path8.join)(rootDir, name);
2707
3239
  if ((0, import_node_fs9.existsSync)(path)) return path;
2708
3240
  }
2709
3241
  return null;
@@ -2769,11 +3301,11 @@ function lintRules(rules, entities, publicRead) {
2769
3301
  const warnings = [];
2770
3302
  if (!rules) return warnings;
2771
3303
  for (const [ns, actions] of Object.entries(rules)) {
2772
- for (const [action, expr] of Object.entries(actions)) {
3304
+ for (const [action2, expr] of Object.entries(actions)) {
2773
3305
  if (typeof expr !== "string" || expr.trim() !== "true") continue;
2774
- if (action === "view" && publicRead.includes(ns)) continue;
3306
+ if (action2 === "view" && publicRead.includes(ns)) continue;
2775
3307
  warnings.push(
2776
- 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`
3308
+ 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`
2777
3309
  );
2778
3310
  }
2779
3311
  }
@@ -2809,10 +3341,10 @@ function wranglerWarnings(rootDir) {
2809
3341
  for (const { label, block } of blocks) {
2810
3342
  const assets = block.assets;
2811
3343
  if (assets?.directory) {
2812
- const dir = (0, import_node_path8.resolve)(rootDir, assets.directory);
2813
- if (dir === (0, import_node_path8.resolve)(rootDir)) {
3344
+ const dir = (0, import_node_path9.resolve)(rootDir, assets.directory);
3345
+ if (dir === (0, import_node_path9.resolve)(rootDir)) {
2814
3346
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
2815
- } else if ((0, import_node_fs10.existsSync)((0, import_node_path8.join)(dir, "node_modules"))) {
3347
+ } else if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(dir, "node_modules"))) {
2816
3348
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
2817
3349
  }
2818
3350
  }
@@ -2847,7 +3379,7 @@ function o11yProjectWarnings(rootDir) {
2847
3379
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
2848
3380
  return warnings;
2849
3381
  }
2850
- const main = typeof config.main === "string" ? (0, import_node_path8.resolve)(rootDir, config.main) : null;
3382
+ const main = typeof config.main === "string" ? (0, import_node_path9.resolve)(rootDir, config.main) : null;
2851
3383
  if (!main || !(0, import_node_fs10.existsSync)(main)) {
2852
3384
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
2853
3385
  } else {
@@ -2877,7 +3409,7 @@ function calendarProjectWarnings(rootDir) {
2877
3409
  }
2878
3410
  function readPackageJson(rootDir) {
2879
3411
  try {
2880
- return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(rootDir, "package.json"), "utf8"));
3412
+ return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path9.join)(rootDir, "package.json"), "utf8"));
2881
3413
  } catch {
2882
3414
  return null;
2883
3415
  }
@@ -2933,8 +3465,8 @@ function integrationWarnings(integrations, schema, rules) {
2933
3465
  warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
2934
3466
  continue;
2935
3467
  }
2936
- for (const action of ["view", "create", "update", "delete"]) {
2937
- if (rule[action] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action}" is missing`);
3468
+ for (const action2 of ["view", "create", "update", "delete"]) {
3469
+ if (rule[action2] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action2}" is missing`);
2938
3470
  }
2939
3471
  }
2940
3472
  for (const seed of integration.seeds ?? []) {
@@ -3105,12 +3637,12 @@ function harnessOption(value2, flag) {
3105
3637
 
3106
3638
  // src/init.ts
3107
3639
  var import_node_fs11 = require("fs");
3108
- var import_node_path9 = require("path");
3109
- var import_apps3 = require("@odla-ai/apps");
3640
+ var import_node_path10 = require("path");
3641
+ var import_apps7 = require("@odla-ai/apps");
3110
3642
  function initProject(options) {
3111
3643
  const out = options.stdout ?? console;
3112
- const rootDir = (0, import_node_path9.resolve)(options.rootDir ?? process.cwd());
3113
- const configPath = (0, import_node_path9.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
3644
+ const rootDir = (0, import_node_path10.resolve)(options.rootDir ?? process.cwd());
3645
+ const configPath = (0, import_node_path10.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
3114
3646
  if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
3115
3647
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
3116
3648
  }
@@ -3120,19 +3652,19 @@ function initProject(options) {
3120
3652
  const envs = options.envs?.length ? options.envs : ["dev"];
3121
3653
  const services = options.services?.length ? options.services : ["db", "ai"];
3122
3654
  for (const service of services) {
3123
- const definition = (0, import_apps3.appServiceDefinition)(service);
3124
- if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps3.appServiceIds)().join(", ")})`);
3655
+ const definition = (0, import_apps7.appServiceDefinition)(service);
3656
+ if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps7.appServiceIds)().join(", ")})`);
3125
3657
  for (const dependency of definition.requires) {
3126
3658
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
3127
3659
  }
3128
3660
  }
3129
3661
  const aiProvider = options.aiProvider ?? "anthropic";
3130
- (0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(configPath), { recursive: true });
3131
- (0, import_node_fs11.mkdirSync)((0, import_node_path9.resolve)(rootDir, "src/odla"), { recursive: true });
3132
- (0, import_node_fs11.mkdirSync)((0, import_node_path9.resolve)(rootDir, ".odla"), { recursive: true });
3662
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true });
3663
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, "src/odla"), { recursive: true });
3664
+ (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, ".odla"), { recursive: true });
3133
3665
  (0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
3134
- writeIfMissing((0, import_node_path9.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
3135
- writeIfMissing((0, import_node_path9.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
3666
+ writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
3667
+ writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
3136
3668
  ensureGitignore(rootDir);
3137
3669
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
3138
3670
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
@@ -3314,8 +3846,8 @@ function assertWranglerConfig(cfg) {
3314
3846
  }
3315
3847
 
3316
3848
  // src/secrets-set.ts
3317
- var import_ai = require("@odla-ai/ai");
3318
- var import_apps4 = require("@odla-ai/apps");
3849
+ var import_ai2 = require("@odla-ai/ai");
3850
+ var import_apps8 = require("@odla-ai/apps");
3319
3851
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
3320
3852
  async function secretsSet(options) {
3321
3853
  const name = (options.name ?? "").trim();
@@ -3326,7 +3858,7 @@ async function secretsSet(options) {
3326
3858
  const { cfg, tenantId, value: value2, doFetch, out } = await resolveVaultWrite(options);
3327
3859
  const token = await getDeveloperToken(cfg, options, doFetch, out);
3328
3860
  try {
3329
- await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
3861
+ await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
3330
3862
  } catch (err) {
3331
3863
  throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value2));
3332
3864
  }
@@ -3367,13 +3899,13 @@ async function resolveVaultWrite(options) {
3367
3899
  throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
3368
3900
  }
3369
3901
  const value2 = await secretInputValue(options, "secret");
3370
- return { cfg, tenantId: (0, import_apps4.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
3902
+ return { cfg, tenantId: (0, import_apps8.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
3371
3903
  }
3372
3904
 
3373
3905
  // src/skill.ts
3374
3906
  var import_node_fs12 = require("fs");
3375
3907
  var import_node_os2 = require("os");
3376
- var import_node_path10 = require("path");
3908
+ var import_node_path11 = require("path");
3377
3909
  var import_node_url2 = require("url");
3378
3910
 
3379
3911
  // src/skill-adapters.ts
@@ -3452,8 +3984,8 @@ function installSkill(options = {}) {
3452
3984
  const files = listFiles(sourceDir);
3453
3985
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
3454
3986
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
3455
- const root = (0, import_node_path10.resolve)(options.dir ?? process.cwd());
3456
- const home = (0, import_node_path10.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
3987
+ const root = (0, import_node_path11.resolve)(options.dir ?? process.cwd());
3988
+ const home = (0, import_node_path11.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
3457
3989
  const plans = /* @__PURE__ */ new Map();
3458
3990
  const targets = /* @__PURE__ */ new Map();
3459
3991
  const rememberTarget = (harness, target) => {
@@ -3467,48 +3999,48 @@ function installSkill(options = {}) {
3467
3999
  plans.set(target, { target, content: content2, boundary, managedMerge });
3468
4000
  };
3469
4001
  const planSkillTree = (targetDir2, boundary = root) => {
3470
- 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);
4002
+ 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);
3471
4003
  };
3472
4004
  let targetDir;
3473
4005
  if (options.global) {
3474
- const claudeRoot = (0, import_node_path10.join)(home, ".claude", "skills");
3475
- const codexRoot = (0, import_node_path10.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path10.join)(home, ".codex"), "skills");
4006
+ const claudeRoot = (0, import_node_path11.join)(home, ".claude", "skills");
4007
+ const codexRoot = (0, import_node_path11.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path11.join)(home, ".codex"), "skills");
3476
4008
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
3477
4009
  for (const harness of harnesses) {
3478
4010
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
3479
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path10.dirname)((0, import_node_path10.dirname)(codexRoot)));
4011
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path11.dirname)((0, import_node_path11.dirname)(codexRoot)));
3480
4012
  rememberTarget(harness, skillRoot);
3481
4013
  }
3482
4014
  } else {
3483
- const sharedRoot = (0, import_node_path10.join)(root, ".agents", "skills");
4015
+ const sharedRoot = (0, import_node_path11.join)(root, ".agents", "skills");
3484
4016
  planSkillTree(sharedRoot);
3485
- const claudeRoot = (0, import_node_path10.join)(root, ".claude", "skills");
4017
+ const claudeRoot = (0, import_node_path11.join)(root, ".claude", "skills");
3486
4018
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
3487
4019
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
3488
4020
  if (harnesses.includes("claude")) {
3489
4021
  for (const skill of skillNames(files)) {
3490
- const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
3491
- plan((0, import_node_path10.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4022
+ const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, skill, "SKILL.md"), "utf8");
4023
+ plan((0, import_node_path11.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
3492
4024
  }
3493
4025
  rememberTarget("claude", claudeRoot);
3494
4026
  }
3495
4027
  if (harnesses.includes("cursor")) {
3496
- const cursorRule = (0, import_node_path10.join)(root, ".cursor", "rules", "odla.mdc");
4028
+ const cursorRule = (0, import_node_path11.join)(root, ".cursor", "rules", "odla.mdc");
3497
4029
  plan(cursorRule, CURSOR_RULE);
3498
4030
  rememberTarget("cursor", cursorRule);
3499
4031
  }
3500
4032
  if (harnesses.includes("agents")) {
3501
- const agentsFile = (0, import_node_path10.join)(root, "AGENTS.md");
4033
+ const agentsFile = (0, import_node_path11.join)(root, "AGENTS.md");
3502
4034
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
3503
4035
  rememberTarget("agents", agentsFile);
3504
4036
  }
3505
4037
  if (harnesses.includes("copilot")) {
3506
- const copilotFile = (0, import_node_path10.join)(root, ".github", "copilot-instructions.md");
4038
+ const copilotFile = (0, import_node_path11.join)(root, ".github", "copilot-instructions.md");
3507
4039
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
3508
4040
  rememberTarget("copilot", copilotFile);
3509
4041
  }
3510
4042
  if (harnesses.includes("gemini")) {
3511
- const geminiFile = (0, import_node_path10.join)(root, "GEMINI.md");
4043
+ const geminiFile = (0, import_node_path11.join)(root, "GEMINI.md");
3512
4044
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
3513
4045
  rememberTarget("gemini", geminiFile);
3514
4046
  }
@@ -3544,7 +4076,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
3544
4076
  }
3545
4077
  for (const file of plans.values()) {
3546
4078
  if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
3547
- (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
4079
+ (0, import_node_fs12.mkdirSync)((0, import_node_path11.dirname)(file.target), { recursive: true });
3548
4080
  (0, import_node_fs12.writeFileSync)(file.target, file.content);
3549
4081
  }
3550
4082
  }
@@ -3564,7 +4096,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
3564
4096
  };
3565
4097
  }
3566
4098
  function pathsUnder(root, paths) {
3567
- 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();
4099
+ 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();
3568
4100
  }
3569
4101
  function normalizeHarnesses(values, global) {
3570
4102
  const requested = values?.length ? values : ["claude"];
@@ -3609,13 +4141,13 @@ function managedFileContent(path, block, force, boundary) {
3609
4141
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
3610
4142
  }
3611
4143
  function symlinkedComponent(boundary, target) {
3612
- const rel = (0, import_node_path10.relative)(boundary, target);
3613
- if (rel === ".." || rel.startsWith(`..${import_node_path10.sep}`) || (0, import_node_path10.isAbsolute)(rel)) {
4144
+ const rel = (0, import_node_path11.relative)(boundary, target);
4145
+ if (rel === ".." || rel.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(rel)) {
3614
4146
  throw new Error(`agent setup target escapes its install root: ${target}`);
3615
4147
  }
3616
4148
  let current = boundary;
3617
- for (const part of rel.split(import_node_path10.sep).filter(Boolean)) {
3618
- current = (0, import_node_path10.join)(current, part);
4149
+ for (const part of rel.split(import_node_path11.sep).filter(Boolean)) {
4150
+ current = (0, import_node_path11.join)(current, part);
3619
4151
  try {
3620
4152
  if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
3621
4153
  } catch (error) {
@@ -3632,9 +4164,9 @@ function listFiles(dir) {
3632
4164
  const results = [];
3633
4165
  const walk = (current) => {
3634
4166
  for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
3635
- const path = (0, import_node_path10.join)(current, entry.name);
4167
+ const path = (0, import_node_path11.join)(current, entry.name);
3636
4168
  if (entry.isDirectory()) walk(path);
3637
- else results.push((0, import_node_path10.relative)(dir, path));
4169
+ else results.push((0, import_node_path11.relative)(dir, path));
3638
4170
  }
3639
4171
  };
3640
4172
  walk(dir);
@@ -3710,7 +4242,7 @@ async function smoke(options) {
3710
4242
  out.log(` schema: ${liveEntities.length} entities`);
3711
4243
  const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
3712
4244
  if (aggregateEntity) {
3713
- const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
4245
+ const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
3714
4246
  ns: aggregateEntity,
3715
4247
  aggregate: { count: true }
3716
4248
  });
@@ -3754,16 +4286,16 @@ async function getJson(doFetch, url, bearer) {
3754
4286
  const res = await doFetch(url, {
3755
4287
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
3756
4288
  });
3757
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
4289
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3758
4290
  return res.json();
3759
4291
  }
3760
- async function postJson(doFetch, url, bearer, body) {
4292
+ async function postJson2(doFetch, url, bearer, body) {
3761
4293
  const res = await doFetch(url, {
3762
4294
  method: "POST",
3763
4295
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
3764
4296
  body: JSON.stringify(body)
3765
4297
  });
3766
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
4298
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
3767
4299
  return res.json();
3768
4300
  }
3769
4301
  function publicConfigUrl(platformUrl, appId, env) {
@@ -3771,7 +4303,7 @@ function publicConfigUrl(platformUrl, appId, env) {
3771
4303
  url.searchParams.set("env", env);
3772
4304
  return url.toString();
3773
4305
  }
3774
- async function safeText3(res) {
4306
+ async function safeText4(res) {
3775
4307
  try {
3776
4308
  return redactSecrets((await res.text()).slice(0, 500));
3777
4309
  } catch {
@@ -3827,6 +4359,26 @@ async function secretsCommand(parsed, deps) {
3827
4359
  });
3828
4360
  }
3829
4361
  async function projectCommand(command, parsed, deps) {
4362
+ if (command === "config") {
4363
+ const sub = parsed.positionals[1];
4364
+ if (sub !== "diff" && sub !== "plan") {
4365
+ throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
4366
+ }
4367
+ assertArgs(parsed, ["config", "token", "email", "open", "json"], 2);
4368
+ const options = {
4369
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
4370
+ token: stringOpt(parsed.options.token),
4371
+ email: stringOpt(parsed.options.email),
4372
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
4373
+ json: parsed.options.json === true,
4374
+ fetch: deps.fetch,
4375
+ openApprovalUrl: deps.openUrl,
4376
+ stdout: deps.stdout
4377
+ };
4378
+ if (sub === "diff") await configDiff(options);
4379
+ else await configPlan(options);
4380
+ return true;
4381
+ }
3830
4382
  if (command === "init") {
3831
4383
  assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
3832
4384
  initProject({
@@ -3887,7 +4439,7 @@ async function projectCommand(command, parsed, deps) {
3887
4439
  // src/code-connect.ts
3888
4440
  var import_node_fs14 = require("fs");
3889
4441
  var import_node_os4 = require("os");
3890
- var import_node_path12 = require("path");
4442
+ var import_node_path13 = require("path");
3891
4443
 
3892
4444
  // ../harness/dist/chunk-QTUEF2HZ.js
3893
4445
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -4505,7 +5057,7 @@ var CamelError = class extends Error {
4505
5057
  };
4506
5058
 
4507
5059
  // ../camel/dist/chunk-L5DYU2E2.js
4508
- function canonicalJson(value2) {
5060
+ function canonicalJson2(value2) {
4509
5061
  return JSON.stringify(normalize(value2));
4510
5062
  }
4511
5063
  async function sha256Hex(value2) {
@@ -4517,7 +5069,7 @@ function utf8Length(value2) {
4517
5069
  if (typeof value2 === "string") return new TextEncoder().encode(value2).byteLength;
4518
5070
  if (value2 instanceof Uint8Array) return value2.byteLength;
4519
5071
  try {
4520
- return new TextEncoder().encode(canonicalJson(value2)).byteLength;
5072
+ return new TextEncoder().encode(canonicalJson2(value2)).byteLength;
4521
5073
  } catch {
4522
5074
  throw new CamelError("conversion_rejected", "Unsafe input could not be canonically measured.");
4523
5075
  }
@@ -4643,7 +5195,7 @@ async function digestCodeVerificationReceipt(fields) {
4643
5195
  changedTestsRequireReview: fields.changedTestsRequireReview,
4644
5196
  outcome: fields.outcome
4645
5197
  };
4646
- return `sha256:${await sha256Hex(canonicalJson(canonical))}`;
5198
+ return `sha256:${await sha256Hex(canonicalJson2(canonical))}`;
4647
5199
  }
4648
5200
  function validate(fields) {
4649
5201
  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) {
@@ -4681,9 +5233,9 @@ async function createCodePortableCheckpoint(input) {
4681
5233
  const state2 = normalizeState(input.state);
4682
5234
  validateBaseAndPatch(input.baseCommitSha, input.patch);
4683
5235
  const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
4684
- const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
5236
+ const stateDigest = `sha256:${await sha256Hex(canonicalJson2(state2))}`;
4685
5237
  const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
4686
- const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
5238
+ const checkpointDigest = `sha256:${await sha256Hex(canonicalJson2(fields))}`;
4687
5239
  return freeze({ ...fields, patch: input.patch, checkpointDigest });
4688
5240
  }
4689
5241
  async function verifyCodePortableCheckpoint(value2) {
@@ -4801,7 +5353,7 @@ async function digestCodeRepositorySnapshot(snapshot, limits = DEFAULT_LIMITS) {
4801
5353
  commitSha: snapshot.commitSha,
4802
5354
  files: [...snapshot.files].map((file) => ({ path: file.path, content: file.content })).sort((left, right) => left.path.localeCompare(right.path))
4803
5355
  };
4804
- return `sha256:${await sha256Hex(canonicalJson(normalized))}`;
5356
+ return `sha256:${await sha256Hex(canonicalJson2(normalized))}`;
4805
5357
  }
4806
5358
  function validateSnapshot(snapshot, limits) {
4807
5359
  if (!REPOSITORY.test(snapshot.repository) || !SHA4.test(snapshot.commitSha)) {
@@ -4843,10 +5395,10 @@ var import_path9 = require("path");
4843
5395
 
4844
5396
  // ../camel/dist/chunk-LAXU2AVK.js
4845
5397
  function conversionPolicyDigest(policy) {
4846
- return sha256Hex(canonicalJson(policy));
5398
+ return sha256Hex(canonicalJson2(policy));
4847
5399
  }
4848
5400
  function registeredIdRegistryDigest(values) {
4849
- return sha256Hex(canonicalJson(values));
5401
+ return sha256Hex(canonicalJson2(values));
4850
5402
  }
4851
5403
  async function createConversionRegistry(config) {
4852
5404
  const policies = /* @__PURE__ */ new Map();
@@ -5047,10 +5599,10 @@ function createEffectPolicy(config = {}) {
5047
5599
  return Object.freeze({ evaluate: (input) => evaluate(input, registries, approvalEffects) });
5048
5600
  }
5049
5601
  function destinationRegistryDigest(values) {
5050
- return sha256Hex(canonicalJson([...values].sort()));
5602
+ return sha256Hex(canonicalJson2([...values].sort()));
5051
5603
  }
5052
5604
  async function evaluate(input, registries, approvals) {
5053
- const actionDigest = await sha256Hex(canonicalJson(input));
5605
+ const actionDigest = await sha256Hex(canonicalJson2(input));
5054
5606
  const decisionId = `decision_${actionDigest.slice(0, 24)}`;
5055
5607
  const deny = (reasonCode) => ({ outcome: "deny", decisionId, reasonCode });
5056
5608
  if (!input.planId || !input.tool.name || !input.tool.policyId || !Number.isSafeInteger(input.tool.version)) return deny("invalid_descriptor");
@@ -6771,268 +7323,19 @@ var CodePiRuntimeEngine = class {
6771
7323
  }
6772
7324
  };
6773
7325
 
6774
- // src/help.ts
7326
+ // src/version.ts
6775
7327
  var import_node_fs13 = require("fs");
6776
7328
  function cliVersion() {
6777
7329
  const pkg = JSON.parse((0, import_node_fs13.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
6778
7330
  return pkg.version ?? "unknown";
6779
7331
  }
6780
- function printHelp(output = console) {
6781
- output.log(`odla-ai
6782
7332
 
6783
- Start here:
6784
- odla-ai runbook ask "<question>" The current procedure, from odla's own
6785
- runbooks. Ask BEFORE searching the web or
6786
- working from memory: runbooks are edited
6787
- live, so your training data is out of date
6788
- and this is not.
6789
- odla-ai runbook impact After a change: which runbooks describe the
6790
- code you just touched, so you can fix any
6791
- step it made wrong.
6792
-
6793
- Usage:
6794
- odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
6795
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
6796
- odla-ai doctor [--config odla.config.mjs]
6797
- odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
6798
- odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
6799
- odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
6800
- odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
6801
- odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
6802
- odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
6803
- odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
6804
- odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
6805
- odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
6806
- odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
6807
- odla-ai app promote [--dry-run] [--json] --yes
6808
- odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
6809
- odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
6810
- odla-ai app owners add <email> [--email <odla-account>] [--json]
6811
- odla-ai app owners remove <email> [--email <odla-account>] [--json]
6812
- odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6813
- odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6814
- odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6815
- odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6816
- odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
6817
- 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]
6818
- odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
6819
- 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]
6820
- odla-ai pm <goal|task|decision|bug> get <id> [--json]
6821
- odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
6822
- 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]
6823
- odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
6824
- 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]
6825
- odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
6826
- odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
6827
- odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
6828
- odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6829
- odla-ai pm <goal|task|decision|bug> rm <id>
6830
- odla-ai pm handoff --app <id> [--json]
6831
- odla-ai discuss groups [--json]
6832
- odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6833
- odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
6834
- odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
6835
- odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6836
- odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6837
- odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6838
- odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
6839
- odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6840
- odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6841
- odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
6842
- odla-ai context list [--json]
6843
- odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
6844
- odla-ai context remove <name> --yes [--json]
6845
- odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
6846
- odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
6847
- odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
6848
- odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6849
- odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6850
- odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
6851
- odla-ai runbook lint [--app <id>] [--all] [--json]
6852
- odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
6853
- odla-ai runbook comment <slug> --body "..." [--app <id>]
6854
- odla-ai runbook get <slug> [--app <id>]
6855
- odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
6856
- odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
6857
- odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
6858
- odla-ai runbook publish <slug> [--app <id>]
6859
- odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
6860
- odla-ai runbook archive <slug> [--app <id>]
6861
- odla-ai runbook history <slug> [--app <id>] [--json]
6862
- odla-ai runbook revert <slug> --version <n> [--app <id>]
6863
- odla-ai runbook rm <slug> [--app <id>]
6864
- odla-ai capabilities [--json]
6865
- odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
6866
- odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
6867
- odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
6868
- odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
6869
- [--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
6870
- odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
6871
- --validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
6872
- odla-ai admin ai credentials [--context <name>] [--json]
6873
- odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
6874
- odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
6875
- odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
6876
- odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
6877
- odla-ai security github disconnect --source <id> [--env dev] [--yes]
6878
- odla-ai security plan [--env dev] [--json]
6879
- odla-ai security sources [--env dev] [--json]
6880
- odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
6881
- odla-ai security status <job-id> [--follow] [--json]
6882
- odla-ai security report <job-id> [--json]
6883
- odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
6884
- odla-ai security run [target] --self --ack-redacted-source
6885
- odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
6886
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
6887
- odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
6888
- odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
6889
- odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
6890
- odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
6891
- odla-ai version
6892
-
6893
- Commands:
6894
- agent Inspect durable agent wakeups and explicitly requeue a
6895
- dead-lettered job; JSON output is stable for remote operators.
6896
- runbook odla's operational procedures, stored in the database and read at
6897
- the moment they are followed. "ask" gives a written, cited answer;
6898
- "search" the passages behind it; "get" the whole document.
6899
- "impact" diffs your working tree against a base ref and names the
6900
- runbooks that describe what you changed \u2014 run it after touching a
6901
- package's exported API or JSDoc, or a Studio surface. "lint"
6902
- checks the corpus the other way: every odla-ai command the
6903
- runbooks name is held against this CLI's real command surface,
6904
- and against the minimum versions each runbook declares. A wrong step
6905
- is fixed with "edit", which takes effect immediately: a runbook is
6906
- a row, not a release. Runbooks describe PROCEDURE; what an export
6907
- does and what it guarantees is JSDoc, rendered per package at
6908
- https://odla.ai/docs and shipped in the installed .d.ts. Answering
6909
- a question usually needs both.
6910
- whoami Report who this terminal is authenticated as, and whether it holds
6911
- platform admin. A handshake-minted device token is never admin,
6912
- however the human who approved it is configured.
6913
- context Explain selected config, platform, app, environment, and
6914
- developer-token provenance without printing credentials or
6915
- starting a device handshake. Operator work can run outside a
6916
- project checkout with explicit flags or ODLA_* environment
6917
- variables.
6918
- setup Install offline odla runbooks for common coding-agent harnesses.
6919
- init Create a generic odla.config.mjs plus starter schema/rules files.
6920
- doctor Validate and summarize the project config without network calls.
6921
- calendar Inspect, connect, or disconnect the live Google booking connection.
6922
- app Archive (suspend, data retained), restore, export, import, or
6923
- manage the co-owners of the app. Archiving takes every
6924
- environment's data plane down until restored; permanent deletion
6925
- has NO CLI \u2014 it requires a signed-in owner in Studio, and no
6926
- machine or agent credential can purge. "app export" downloads a
6927
- portable gzipped-JSONL snapshot of one database (--fresh takes a
6928
- new snapshot first) \u2014 your data is always yours to take.
6929
- "app import" upserts rows back in from JSON, JSONL, or a
6930
- {namespace: rows} map (the shape a query returns); it writes only
6931
- with --yes, so the bare command is a dry run, and it refuses to
6932
- guess an id mode that would duplicate rows on a re-run.
6933
- "app refresh-sandbox" replaces the sandbox with a copy of live
6934
- (the routine dev loop); "app go-live" copies the sandbox into an
6935
- EMPTY live database, once, at launch; "app promote" pushes only
6936
- schema, rules and gates up afterwards, leaving live's rows alone.
6937
- Each prints its plan and writes nothing without --yes, so the
6938
- bare command is the dry run. Clerk config, triggers, allowlists,
6939
- secrets and API keys never travel; identity rows stay behind
6940
- unless --include-identity.
6941
- "app rename <name>" changes only the display name humans read;
6942
- the app id is permanent (tenants, URLs and keys embed it), so a
6943
- rename never re-provisions anything or invalidates a credential.
6944
- "app owners add <email>" grants a signed-up odla member the SAME
6945
- full access as you; they then run their own "provision" to mint
6946
- their own credentials for the shared db (the live one included)
6947
- \u2014 no secret is ever copied between people.
6948
- capabilities Show what the CLI automates vs agent edits and human checkpoints.
6949
- code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
6950
- admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
6951
- security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
6952
- pm Project management (via @odla-ai/pm) shared across the apps you
6953
- co-own: conformance goals, kanban tasks, decisions, and bugs. With
6954
- no --app, "list" spans every co-owned project (the cross-project
6955
- view); with --app it scopes to one. Same device-grant auth as
6956
- "app". Entities: goal (alias conformance), task (alias kanban),
6957
- decision, bug. Status changes and comments post to each item's
6958
- @odla-ai/chat discussion thread.
6959
- discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
6960
- one group per project, topics with replies, @-mentions of people,
6961
- agents, PM items, and projects. Built for unattended use \u2014 post a
6962
- question, then "watch" it until someone answers. "watch" exits 75
6963
- (not 1) when it times out with nothing new, so a script can tell
6964
- "no answer yet" from a failure and just rerun. Use "who" to look
6965
- up the exact @[Label](kind/id) markup for a mention.
6966
- o11y Read one stable status envelope for application RED, current
6967
- live-sync load/freshness, the protected commit-to-visible
6968
- canary, collector ingest/scheduler trust, Cloudflare-owned
6969
- runtime metrics, and a machine verdict.
6970
- --json keeps auth progress on stderr for unattended agents.
6971
- platform Read canonical fleet health, releases, provider load/freshness,
6972
- explicit unknowns, and next actions through a read-only grant.
6973
- provision Register services, compose integrations, persist credentials, optionally push secrets.
6974
- smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6975
- skill Same installer; --agent accepts all, claude, codex, cursor,
6976
- copilot, gemini, or agents (repeatable or comma-separated).
6977
- secrets Push configured db/o11y secrets into the Worker via wrangler
6978
- stdin; set stores a tenant-vault secret and set-clerk-key the
6979
- reserved Clerk secret key, write-only from stdin or an env var.
6980
- version Print the CLI version.
6981
-
6982
- Safety:
6983
- Every app has two databases on odla.ai: a sandbox (env "dev", tenant
6984
- <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
6985
- production odla.ai \u2014 there is no separate odla to point at. New projects get
6986
- the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
6987
- provision the live database; use --dry-run first to inspect the resolved plan.
6988
- Provision caches the approved developer token and service credentials under
6989
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
6990
- preflights Wrangler before any shown-once issuance or destructive rotation.
6991
- Projectless PM, Discussions, o11y, runbook, and identity commands use
6992
- --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
6993
- ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
6994
- select it explicitly with --context or ODLA_CONTEXT. Each named context gets
6995
- isolated developer and scoped-token caches. Override their locations with
6996
- ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
6997
- the metadata file. Flags and specific ODLA_* scope variables beat a selected
6998
- context, which beats project config. There is no ambient current context.
6999
- "context show" reports only provenance and cache state and never authenticates.
7000
- Provision opens the approval page in your browser automatically whenever the
7001
- machine can show one, including agent-driven runs; only CI, SSH, and
7002
- display-less hosts skip it. Use --open to force or --no-open to suppress.
7003
- Browser launch is best-effort: the printed approval URL is authoritative and
7004
- agents must relay it to the human verbatim. A started handshake is persisted
7005
- under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
7006
- the same code. Outside an interactive terminal the wait is capped (90s by
7007
- default, --wait <seconds> to change); a still-pending handshake then exits
7008
- with code 75: relay the URL, wait for approval, and re-run to collect.
7009
- A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
7010
- The email is a non-secret identity hint: never provide a password or session
7011
- token. The matching account must already exist, be signed in, explicitly
7012
- review the exact code, and finish any current request before claiming another.
7013
- Run Code from a GitHub checkout already connected to an app in Studio; an
7014
- odla.config.mjs may select the app explicitly but is not required. Code host
7015
- approval and credential hashes live in odla-ai/db. The host
7016
- credential is never written under .odla/; it exists only in the foreground
7017
- "code connect" process and is rotated by the next approved connection.
7018
- Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
7019
- OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
7020
- GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
7021
- for a PAT or provider key. GitHub read access is separate from the explicit
7022
- --ack-redacted-source consent and the exact --plan-digest printed by security
7023
- plan are required before bounded snippets reach System AI.
7024
- Run security plan first to inspect the admin-selected providers, models,
7025
- per-route bounds, credential readiness, retention, no-execution boundary,
7026
- and digest that binds consent to that exact plan.
7027
- `);
7028
- }
7029
-
7030
- // src/security-hosted-github.ts
7031
- var import_node_child_process4 = require("child_process");
7032
- var import_node_util2 = require("util");
7333
+ // src/security-hosted-github.ts
7334
+ var import_node_child_process4 = require("child_process");
7335
+ var import_node_util2 = require("util");
7033
7336
 
7034
7337
  // src/security-hosted-request.ts
7035
- async function requestHostedSecurityJson(options, path, init, action) {
7338
+ async function requestHostedSecurityJson(options, path, init, action2) {
7036
7339
  const platform = platformAudience(options.platform);
7037
7340
  const token = hostedSecurityCredential(options.token);
7038
7341
  const requestInit = {
@@ -7052,7 +7355,7 @@ async function requestHostedSecurityJson(options, path, init, action) {
7052
7355
  if (!response2.ok) {
7053
7356
  const code = optionalHostedText(body.error?.code, "error code", 128);
7054
7357
  const message2 = optionalHostedText(body.error?.message, "error message", 300);
7055
- throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
7358
+ throw new Error(`${action2} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
7056
7359
  }
7057
7360
  return body;
7058
7361
  }
@@ -7243,7 +7546,7 @@ async function defaultReadOrigin(cwd) {
7243
7546
 
7244
7547
  // src/code-local-source.ts
7245
7548
  var import_node_child_process5 = require("child_process");
7246
- var import_node_crypto = require("crypto");
7549
+ var import_node_crypto2 = require("crypto");
7247
7550
  var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
7248
7551
  async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
7249
7552
  const headCommitSha = await readHead(cwd);
@@ -7294,15 +7597,15 @@ async function readGitHead(cwd) {
7294
7597
  return value2;
7295
7598
  }
7296
7599
  function digestText(value2) {
7297
- return `sha256:${(0, import_node_crypto.createHash)("sha256").update(value2).digest("hex")}`;
7600
+ return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(value2).digest("hex")}`;
7298
7601
  }
7299
7602
 
7300
7603
  // src/code-images.ts
7301
7604
  var import_node_child_process6 = require("child_process");
7302
- var import_node_crypto2 = require("crypto");
7605
+ var import_node_crypto3 = require("crypto");
7303
7606
  var import_promises10 = require("fs/promises");
7304
7607
  var import_node_os3 = require("os");
7305
- var import_node_path11 = require("path");
7608
+ var import_node_path12 = require("path");
7306
7609
  var import_node_url3 = require("url");
7307
7610
 
7308
7611
  // src/code-runtime-config.ts
@@ -7385,13 +7688,13 @@ async function embeddedPiImageName() {
7385
7688
  const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
7386
7689
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
7387
7690
  });
7388
- return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto2.createHash)("sha256").update(bundle).digest("hex")}`;
7691
+ return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
7389
7692
  }
7390
7693
  async function buildEmbeddedPiImage(engine, image, run) {
7391
- const context = await (0, import_promises10.mkdtemp)((0, import_node_path11.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
7694
+ const context = await (0, import_promises10.mkdtemp)((0, import_node_path12.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
7392
7695
  try {
7393
- await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path11.join)(context, "pi-agent.js"));
7394
- await (0, import_promises10.writeFile)((0, import_node_path11.join)(context, "Dockerfile"), [
7696
+ await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path12.join)(context, "pi-agent.js"));
7697
+ await (0, import_promises10.writeFile)((0, import_node_path12.join)(context, "Dockerfile"), [
7395
7698
  `FROM ${CODE_NODE_IMAGE}`,
7396
7699
  "COPY pi-agent.js /opt/odla/pi-agent.js",
7397
7700
  "WORKDIR /workspace",
@@ -7407,7 +7710,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
7407
7710
  // src/code-connect.ts
7408
7711
  async function codeConnect(options) {
7409
7712
  const cwd = options.cwd ?? process.cwd();
7410
- const configPath = (0, import_node_path12.resolve)(cwd, options.configPath);
7713
+ const configPath = (0, import_node_path13.resolve)(cwd, options.configPath);
7411
7714
  const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
7412
7715
  const requestedAppId = options.appId?.trim();
7413
7716
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
@@ -7575,9 +7878,9 @@ function parseConnection(value2, appId, appEnv) {
7575
7878
  }
7576
7879
  return root;
7577
7880
  }
7578
- function apiFailure(action, status, value2) {
7881
+ function apiFailure(action2, status, value2) {
7579
7882
  const message2 = record4(record4(value2)?.error)?.message;
7580
- return `${action} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
7883
+ return `${action2} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
7581
7884
  }
7582
7885
  function record4(value2) {
7583
7886
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
@@ -7644,136 +7947,391 @@ function clean3(value2) {
7644
7947
  return normalized || void 0;
7645
7948
  }
7646
7949
 
7647
- // src/context-command.ts
7648
- async function contextCommand(parsed, deps = {}) {
7649
- assertArgs(
7650
- parsed,
7651
- [
7652
- "config",
7653
- "context",
7654
- "platform",
7655
- "app",
7656
- "env",
7657
- "token",
7658
- "json",
7659
- "yes"
7660
- ],
7661
- 3
7662
- );
7663
- const action = parsed.positionals[1];
7664
- const out = deps.stdout ?? console;
7665
- if (action === "list") {
7666
- const profiles = listOperatorProfiles();
7667
- if (parsed.options.json === true) {
7668
- out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
7669
- return;
7670
- }
7671
- if (profiles.length === 0) {
7672
- out.log("No named operator contexts.");
7673
- return;
7674
- }
7675
- out.log("name platform app environment");
7676
- for (const profile of profiles) {
7677
- out.log(
7678
- `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
7679
- );
7680
- }
7681
- return;
7682
- }
7683
- if (action === "save") {
7684
- if (parsed.options.token !== void 0) {
7685
- throw new Error(
7686
- "context save does not accept --token; contexts store scope metadata only"
7687
- );
7688
- }
7689
- const name = requireName(parsed);
7690
- const context2 = await resolveOperatorContext(parsed, {
7691
- allowMissingConfig: true
7692
- });
7693
- const profile = {
7694
- platform: context2.platform.value,
7695
- ...context2.app.value ? { app: context2.app.value } : {},
7696
- ...context2.environment.value ? { environment: context2.environment.value } : {}
7697
- };
7698
- saveOperatorProfile(name, profile, context2.profile.file);
7699
- if (parsed.options.json === true) {
7700
- out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
7701
- } else {
7702
- out.log(`saved operator context "${name}" (${profile.platform})`);
7703
- }
7704
- return;
7705
- }
7706
- if (action === "remove") {
7707
- const name = requireName(parsed);
7708
- if (parsed.options.yes !== true) {
7709
- throw new Error(`context remove "${name}" requires --yes`);
7710
- }
7711
- const removed = removeOperatorProfile(name);
7712
- const result2 = {
7713
- schemaVersion: 1,
7714
- name,
7715
- removed,
7716
- credentialCachesRemoved: false,
7717
- grantsRevoked: false
7718
- };
7719
- if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
7720
- else {
7721
- const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
7722
- out.log(`${status}; credential caches were not deleted or revoked`);
7723
- }
7724
- return;
7725
- }
7726
- if (action !== "show") {
7727
- throw new Error(
7728
- `unknown context action "${action ?? ""}". Try show|list|save|remove.`
7729
- );
7730
- }
7731
- const context = await resolveOperatorContext(parsed, {
7732
- allowMissingConfig: true,
7733
- defaultEnvironment: "prod"
7734
- });
7735
- const token = developerTokenStatus(context, parsed);
7736
- const result = {
7737
- schemaVersion: 1,
7738
- profile: context.profile,
7739
- config: context.config,
7740
- platform: context.platform,
7741
- app: context.app,
7742
- environment: context.environment,
7743
- authentication: {
7744
- developerToken: token,
7745
- scopedTokenCacheFile: context.credentials.scopedTokenFile,
7746
- handshakeStarted: false
7747
- }
7748
- };
7749
- if (parsed.options.json === true) {
7750
- out.log(JSON.stringify(result, null, 2));
7751
- return;
7752
- }
7753
- out.log(`config: ${context.config.status} (${context.config.path})`);
7754
- out.log(
7755
- `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
7756
- );
7757
- out.log(
7758
- `platform: ${context.platform.value} (${context.platform.source})`
7759
- );
7760
- out.log(
7761
- `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
7762
- );
7763
- out.log(
7764
- `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
7765
- );
7766
- out.log(
7767
- `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
7768
- );
7769
- out.log("handshake: not started");
7770
- }
7771
- function requireName(parsed) {
7772
- const name = parsed.positionals[2]?.trim();
7773
- if (!name) {
7774
- throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
7775
- }
7776
- return name;
7950
+ // src/context-command.ts
7951
+ async function contextCommand(parsed, deps = {}) {
7952
+ assertArgs(
7953
+ parsed,
7954
+ [
7955
+ "config",
7956
+ "context",
7957
+ "platform",
7958
+ "app",
7959
+ "env",
7960
+ "token",
7961
+ "json",
7962
+ "yes"
7963
+ ],
7964
+ 3
7965
+ );
7966
+ const action2 = parsed.positionals[1];
7967
+ const out = deps.stdout ?? console;
7968
+ if (action2 === "list") {
7969
+ const profiles = listOperatorProfiles();
7970
+ if (parsed.options.json === true) {
7971
+ out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
7972
+ return;
7973
+ }
7974
+ if (profiles.length === 0) {
7975
+ out.log("No named operator contexts.");
7976
+ return;
7977
+ }
7978
+ out.log("name platform app environment");
7979
+ for (const profile of profiles) {
7980
+ out.log(
7981
+ `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
7982
+ );
7983
+ }
7984
+ return;
7985
+ }
7986
+ if (action2 === "save") {
7987
+ if (parsed.options.token !== void 0) {
7988
+ throw new Error(
7989
+ "context save does not accept --token; contexts store scope metadata only"
7990
+ );
7991
+ }
7992
+ const name = requireName(parsed);
7993
+ const context2 = await resolveOperatorContext(parsed, {
7994
+ allowMissingConfig: true
7995
+ });
7996
+ const profile = {
7997
+ platform: context2.platform.value,
7998
+ ...context2.app.value ? { app: context2.app.value } : {},
7999
+ ...context2.environment.value ? { environment: context2.environment.value } : {}
8000
+ };
8001
+ saveOperatorProfile(name, profile, context2.profile.file);
8002
+ if (parsed.options.json === true) {
8003
+ out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
8004
+ } else {
8005
+ out.log(`saved operator context "${name}" (${profile.platform})`);
8006
+ }
8007
+ return;
8008
+ }
8009
+ if (action2 === "remove") {
8010
+ const name = requireName(parsed);
8011
+ if (parsed.options.yes !== true) {
8012
+ throw new Error(`context remove "${name}" requires --yes`);
8013
+ }
8014
+ const removed = removeOperatorProfile(name);
8015
+ const result2 = {
8016
+ schemaVersion: 1,
8017
+ name,
8018
+ removed,
8019
+ credentialCachesRemoved: false,
8020
+ grantsRevoked: false
8021
+ };
8022
+ if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
8023
+ else {
8024
+ const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
8025
+ out.log(`${status}; credential caches were not deleted or revoked`);
8026
+ }
8027
+ return;
8028
+ }
8029
+ if (action2 !== "show") {
8030
+ throw new Error(
8031
+ `unknown context action "${action2 ?? ""}". Try show|list|save|remove.`
8032
+ );
8033
+ }
8034
+ const context = await resolveOperatorContext(parsed, {
8035
+ allowMissingConfig: true,
8036
+ defaultEnvironment: "prod"
8037
+ });
8038
+ const token = developerTokenStatus(context, parsed);
8039
+ const result = {
8040
+ schemaVersion: 1,
8041
+ profile: context.profile,
8042
+ config: context.config,
8043
+ platform: context.platform,
8044
+ app: context.app,
8045
+ environment: context.environment,
8046
+ authentication: {
8047
+ developerToken: token,
8048
+ scopedTokenCacheFile: context.credentials.scopedTokenFile,
8049
+ handshakeStarted: false
8050
+ }
8051
+ };
8052
+ if (parsed.options.json === true) {
8053
+ out.log(JSON.stringify(result, null, 2));
8054
+ return;
8055
+ }
8056
+ out.log(`config: ${context.config.status} (${context.config.path})`);
8057
+ out.log(
8058
+ `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
8059
+ );
8060
+ out.log(
8061
+ `platform: ${context.platform.value} (${context.platform.source})`
8062
+ );
8063
+ out.log(
8064
+ `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
8065
+ );
8066
+ out.log(
8067
+ `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
8068
+ );
8069
+ out.log(
8070
+ `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
8071
+ );
8072
+ out.log("handshake: not started");
8073
+ }
8074
+ function requireName(parsed) {
8075
+ const name = parsed.positionals[2]?.trim();
8076
+ if (!name) {
8077
+ throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
8078
+ }
8079
+ return name;
8080
+ }
8081
+
8082
+ // src/help.ts
8083
+ function printHelp(output = console) {
8084
+ output.log(`odla-ai
8085
+
8086
+ Start here:
8087
+ odla-ai runbook ask "<question>" The current procedure, from odla's own
8088
+ runbooks. Ask BEFORE searching the web or
8089
+ working from memory: runbooks are edited
8090
+ live, so your training data is out of date
8091
+ and this is not.
8092
+ odla-ai runbook impact After a change: which runbooks describe the
8093
+ code you just touched, so you can fix any
8094
+ step it made wrong.
8095
+
8096
+ Usage:
8097
+ odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8098
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
8099
+ odla-ai doctor [--config odla.config.mjs]
8100
+ odla-ai config diff [--config odla.config.mjs] [--email <odla-account>] [--json]
8101
+ odla-ai config plan [--config odla.config.mjs] [--email <odla-account>] [--json]
8102
+ odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8103
+ odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8104
+ odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
8105
+ odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
8106
+ odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
8107
+ odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
8108
+ odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
8109
+ odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
8110
+ odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
8111
+ odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
8112
+ odla-ai app promote [--dry-run] [--json] --yes
8113
+ odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
8114
+ odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
8115
+ odla-ai app owners add <email> [--email <odla-account>] [--json]
8116
+ odla-ai app owners remove <email> [--email <odla-account>] [--json]
8117
+ odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8118
+ odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8119
+ odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8120
+ odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8121
+ odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
8122
+ 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]
8123
+ odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8124
+ 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]
8125
+ odla-ai pm <goal|task|decision|bug> get <id> [--json]
8126
+ odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
8127
+ 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]
8128
+ odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
8129
+ 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]
8130
+ odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
8131
+ odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
8132
+ odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
8133
+ odla-ai pm <goal|task|decision|bug> comments <id> [--json]
8134
+ odla-ai pm <goal|task|decision|bug> rm <id>
8135
+ odla-ai pm handoff --app <id> [--json]
8136
+ odla-ai discuss groups [--json]
8137
+ odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
8138
+ odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
8139
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
8140
+ odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
8141
+ odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
8142
+ odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
8143
+ odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
8144
+ odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
8145
+ odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
8146
+ odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
8147
+ odla-ai context list [--json]
8148
+ odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
8149
+ odla-ai context remove <name> --yes [--json]
8150
+ odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
8151
+ odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8152
+ odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
8153
+ odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
8154
+ odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
8155
+ odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
8156
+ odla-ai runbook lint [--app <id>] [--all] [--json]
8157
+ odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
8158
+ odla-ai runbook comment <slug> --body "..." [--app <id>]
8159
+ odla-ai runbook get <slug> [--app <id>]
8160
+ odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
8161
+ odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
8162
+ odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
8163
+ odla-ai runbook publish <slug> [--app <id>]
8164
+ odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
8165
+ odla-ai runbook archive <slug> [--app <id>]
8166
+ odla-ai runbook history <slug> [--app <id>] [--json]
8167
+ odla-ai runbook revert <slug> --version <n> [--app <id>]
8168
+ odla-ai runbook rm <slug> [--app <id>]
8169
+ odla-ai capabilities [--json]
8170
+ odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
8171
+ odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8172
+ odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
8173
+ odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
8174
+ [--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
8175
+ odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
8176
+ --validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
8177
+ odla-ai admin ai credentials [--context <name>] [--json]
8178
+ odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
8179
+ odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
8180
+ odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
8181
+ odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
8182
+ odla-ai security github disconnect --source <id> [--env dev] [--yes]
8183
+ odla-ai security plan [--env dev] [--json]
8184
+ odla-ai security sources [--env dev] [--json]
8185
+ odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
8186
+ odla-ai security status <job-id> [--follow] [--json]
8187
+ odla-ai security report <job-id> [--json]
8188
+ odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8189
+ odla-ai security run [target] --self --ack-redacted-source
8190
+ odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8191
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8192
+ odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8193
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
8194
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
8195
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
8196
+ odla-ai version
8197
+
8198
+ Commands:
8199
+ agent Inspect durable agent wakeups and explicitly requeue a
8200
+ dead-lettered job; JSON output is stable for remote operators.
8201
+ runbook odla's operational procedures, stored in the database and read at
8202
+ the moment they are followed. "ask" gives a written, cited answer;
8203
+ "search" the passages behind it; "get" the whole document.
8204
+ "impact" diffs your working tree against a base ref and names the
8205
+ runbooks that describe what you changed \u2014 run it after touching a
8206
+ package's exported API or JSDoc, or a Studio surface. "lint"
8207
+ checks the corpus the other way: every odla-ai command the
8208
+ runbooks name is held against this CLI's real command surface,
8209
+ and against the minimum versions each runbook declares. A wrong step
8210
+ is fixed with "edit", which takes effect immediately: a runbook is
8211
+ a row, not a release. Runbooks describe PROCEDURE; what an export
8212
+ does and what it guarantees is JSDoc, rendered per package at
8213
+ https://odla.ai/docs and shipped in the installed .d.ts. Answering
8214
+ a question usually needs both.
8215
+ whoami Report who this terminal is authenticated as, and whether it holds
8216
+ platform admin. A handshake-minted device token is never admin,
8217
+ however the human who approved it is configured.
8218
+ context Explain selected config, platform, app, environment, and
8219
+ developer-token provenance without printing credentials or
8220
+ starting a device handshake. Operator work can run outside a
8221
+ project checkout with explicit flags or ODLA_* environment
8222
+ variables.
8223
+ setup Install offline odla runbooks for common coding-agent harnesses.
8224
+ init Create a generic odla.config.mjs plus starter schema/rules files.
8225
+ doctor Validate and summarize the project config without network calls.
8226
+ config Read-only diff and revision-bound plan for checked-in Registry
8227
+ intent; runtime-owned fields and excluded coverage stay explicit.
8228
+ calendar Inspect, connect, or disconnect the live Google booking connection.
8229
+ app Archive (suspend, data retained), restore, export, import, or
8230
+ manage the co-owners of the app. Archiving takes every
8231
+ environment's data plane down until restored; permanent deletion
8232
+ has NO CLI \u2014 it requires a signed-in owner in Studio, and no
8233
+ machine or agent credential can purge. "app export" downloads a
8234
+ portable gzipped-JSONL snapshot of one database (--fresh takes a
8235
+ new snapshot first) \u2014 your data is always yours to take.
8236
+ "app import" upserts rows back in from JSON, JSONL, or a
8237
+ {namespace: rows} map (the shape a query returns); it writes only
8238
+ with --yes, so the bare command is a dry run, and it refuses to
8239
+ guess an id mode that would duplicate rows on a re-run.
8240
+ "app refresh-sandbox" replaces the sandbox with a copy of live
8241
+ (the routine dev loop); "app go-live" copies the sandbox into an
8242
+ EMPTY live database, once, at launch; "app promote" pushes only
8243
+ schema, rules and gates up afterwards, leaving live's rows alone.
8244
+ Each prints its plan and writes nothing without --yes, so the
8245
+ bare command is the dry run. Clerk config, triggers, allowlists,
8246
+ secrets and API keys never travel; identity rows stay behind
8247
+ unless --include-identity.
8248
+ "app rename <name>" changes only the display name humans read;
8249
+ the app id is permanent (tenants, URLs and keys embed it), so a
8250
+ rename never re-provisions anything or invalidates a credential.
8251
+ "app owners add <email>" grants a signed-up odla member the SAME
8252
+ full access as you; they then run their own "provision" to mint
8253
+ their own credentials for the shared db (the live one included)
8254
+ \u2014 no secret is ever copied between people.
8255
+ capabilities Show what the CLI automates vs agent edits and human checkpoints.
8256
+ code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
8257
+ admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
8258
+ security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
8259
+ pm Project management (via @odla-ai/pm) shared across the apps you
8260
+ co-own: conformance goals, kanban tasks, decisions, and bugs. With
8261
+ no --app, "list" spans every co-owned project (the cross-project
8262
+ view); with --app it scopes to one. Same device-grant auth as
8263
+ "app". Entities: goal (alias conformance), task (alias kanban),
8264
+ decision, bug. Status changes and comments post to each item's
8265
+ @odla-ai/chat discussion thread.
8266
+ discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
8267
+ one group per project, topics with replies, @-mentions of people,
8268
+ agents, PM items, and projects. Built for unattended use \u2014 post a
8269
+ question, then "watch" it until someone answers. "watch" exits 75
8270
+ (not 1) when it times out with nothing new, so a script can tell
8271
+ "no answer yet" from a failure and just rerun. Use "who" to look
8272
+ up the exact @[Label](kind/id) markup for a mention.
8273
+ o11y Read one stable status envelope for application RED, current
8274
+ live-sync load/freshness, the protected commit-to-visible
8275
+ canary, collector ingest/scheduler trust, Cloudflare-owned
8276
+ runtime metrics, and a machine verdict.
8277
+ --json keeps auth progress on stderr for unattended agents.
8278
+ platform Read canonical fleet health, releases, provider load/freshness,
8279
+ explicit unknowns, and next actions through a read-only grant.
8280
+ provision Register services, compose integrations, persist credentials, optionally push secrets.
8281
+ smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
8282
+ skill Same installer; --agent accepts all, claude, codex, cursor,
8283
+ copilot, gemini, or agents (repeatable or comma-separated).
8284
+ secrets Push configured db/o11y secrets into the Worker via wrangler
8285
+ stdin; set stores a tenant-vault secret and set-clerk-key the
8286
+ reserved Clerk secret key, write-only from stdin or an env var.
8287
+ version Print the CLI version.
8288
+
8289
+ Safety:
8290
+ Every app has two databases on odla.ai: a sandbox (env "dev", tenant
8291
+ <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
8292
+ production odla.ai \u2014 there is no separate odla to point at. New projects get
8293
+ the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
8294
+ provision the live database; use --dry-run first to inspect the resolved plan.
8295
+ Provision caches the approved developer token and service credentials under
8296
+ .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
8297
+ preflights Wrangler before any shown-once issuance or destructive rotation.
8298
+ Projectless PM, Discussions, o11y, runbook, and identity commands use
8299
+ --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
8300
+ ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
8301
+ select it explicitly with --context or ODLA_CONTEXT. Each named context gets
8302
+ isolated developer and scoped-token caches. Override their locations with
8303
+ ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
8304
+ the metadata file. Flags and specific ODLA_* scope variables beat a selected
8305
+ context, which beats project config. There is no ambient current context.
8306
+ "context show" reports only provenance and cache state and never authenticates.
8307
+ Provision opens the approval page in your browser automatically whenever the
8308
+ machine can show one, including agent-driven runs; only CI, SSH, and
8309
+ display-less hosts skip it. Use --open to force or --no-open to suppress.
8310
+ Browser launch is best-effort: the printed approval URL is authoritative and
8311
+ agents must relay it to the human verbatim. A started handshake is persisted
8312
+ under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
8313
+ the same code. Outside an interactive terminal the wait is capped (90s by
8314
+ default, --wait <seconds> to change); a still-pending handshake then exits
8315
+ with code 75: relay the URL, wait for approval, and re-run to collect.
8316
+ A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8317
+ The email is a non-secret identity hint: never provide a password or session
8318
+ token. The matching account must already exist, be signed in, explicitly
8319
+ review the exact code, and finish any current request before claiming another.
8320
+ Run Code from a GitHub checkout already connected to an app in Studio; an
8321
+ odla.config.mjs may select the app explicitly but is not required. Code host
8322
+ approval and credential hashes live in odla-ai/db. The host
8323
+ credential is never written under .odla/; it exists only in the foreground
8324
+ "code connect" process and is rotated by the next approved connection.
8325
+ Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
8326
+ OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
8327
+ GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
8328
+ for a PAT or provider key. GitHub read access is separate from the explicit
8329
+ --ack-redacted-source consent and the exact --plan-digest printed by security
8330
+ plan are required before bounded snippets reach System AI.
8331
+ Run security plan first to inspect the admin-selected providers, models,
8332
+ per-route bounds, credential readiness, retention, no-execution boundary,
8333
+ and digest that binds consent to that exact plan.
8334
+ `);
7777
8335
  }
7778
8336
 
7779
8337
  // src/discuss-actions.ts
@@ -8197,8 +8755,8 @@ var ALLOWED = [
8197
8755
  "platform",
8198
8756
  "context"
8199
8757
  ];
8200
- function requireId(id, action) {
8201
- if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
8758
+ function requireId(id, action2) {
8759
+ if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
8202
8760
  return id;
8203
8761
  }
8204
8762
  async function buildContext(parsed, deps) {
@@ -8232,11 +8790,11 @@ async function buildContext(parsed, deps) {
8232
8790
  }
8233
8791
  async function discussCommand(parsed, deps = {}) {
8234
8792
  assertArgs(parsed, ALLOWED, 3);
8235
- const action = parsed.positionals[1];
8793
+ const action2 = parsed.positionals[1];
8236
8794
  const id = parsed.positionals[2];
8237
- if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
8795
+ if (!action2) throw new Error('"discuss" needs an action. Run "odla-ai help".');
8238
8796
  const ctx = await buildContext(parsed, deps);
8239
- switch (action) {
8797
+ switch (action2) {
8240
8798
  case "groups":
8241
8799
  return discussGroups(ctx);
8242
8800
  case "list":
@@ -8258,7 +8816,7 @@ async function discussCommand(parsed, deps = {}) {
8258
8816
  return;
8259
8817
  }
8260
8818
  default:
8261
- throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
8819
+ throw new Error(`unknown discuss action "${action2}". Run "odla-ai help".`);
8262
8820
  }
8263
8821
  }
8264
8822
 
@@ -8519,18 +9077,18 @@ var ENTITY_OPTIONS = {
8519
9077
  done: ["decision"]
8520
9078
  }
8521
9079
  };
8522
- function canonicalAction(action) {
8523
- if (action === "create") return "add";
8524
- if (action === "update" || action === "status" || action === "move") return "set";
8525
- if (action === "delete") return "rm";
8526
- return action in ACTION_OPTIONS ? action : null;
8527
- }
8528
- function allowedOptions(entity, action) {
8529
- const entityOptions = action === "list" || action === "add" || action === "set" || action === "done" ? ENTITY_OPTIONS[entity][action] : [];
8530
- return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action], ...entityOptions];
8531
- }
8532
- function requireId2(id, action) {
8533
- if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
9080
+ function canonicalAction(action2) {
9081
+ if (action2 === "create") return "add";
9082
+ if (action2 === "update" || action2 === "status" || action2 === "move") return "set";
9083
+ if (action2 === "delete") return "rm";
9084
+ return action2 in ACTION_OPTIONS ? action2 : null;
9085
+ }
9086
+ function allowedOptions(entity, action2) {
9087
+ const entityOptions = action2 === "list" || action2 === "add" || action2 === "set" || action2 === "done" ? ENTITY_OPTIONS[entity][action2] : [];
9088
+ return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action2], ...entityOptions];
9089
+ }
9090
+ function requireId2(id, action2) {
9091
+ if (!id) throw new Error(`"pm ... ${action2}" needs an item id`);
8534
9092
  return id;
8535
9093
  }
8536
9094
  async function buildContext2(parsed, deps) {
@@ -8566,28 +9124,28 @@ async function pmCommand(parsed, deps = {}) {
8566
9124
  const entity = ALIASES[word];
8567
9125
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
8568
9126
  const requestedAction = parsed.positionals[2] ?? "list";
8569
- const action = canonicalAction(requestedAction);
8570
- if (!action) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
8571
- assertArgs(parsed, allowedOptions(entity, action), 4);
9127
+ const action2 = canonicalAction(requestedAction);
9128
+ if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
9129
+ assertArgs(parsed, allowedOptions(entity, action2), 4);
8572
9130
  const ctx = await buildContext2(parsed, deps);
8573
9131
  const id = parsed.positionals[3];
8574
- switch (action) {
9132
+ switch (action2) {
8575
9133
  case "list":
8576
9134
  return pmList(ctx, entity, parsed);
8577
9135
  case "add":
8578
9136
  return pmAdd(ctx, entity, parsed);
8579
9137
  case "get":
8580
- return pmGet(ctx, entity, requireId2(id, action));
9138
+ return pmGet(ctx, entity, requireId2(id, action2));
8581
9139
  case "set":
8582
- return pmSet(ctx, entity, requireId2(id, action), parsed);
9140
+ return pmSet(ctx, entity, requireId2(id, action2), parsed);
8583
9141
  case "done":
8584
- return pmDone(ctx, entity, requireId2(id, action), parsed);
9142
+ return pmDone(ctx, entity, requireId2(id, action2), parsed);
8585
9143
  case "comment":
8586
- return pmComment(ctx, entity, requireId2(id, action), parsed);
9144
+ return pmComment(ctx, entity, requireId2(id, action2), parsed);
8587
9145
  case "comments":
8588
- return pmComments(ctx, entity, requireId2(id, action));
9146
+ return pmComments(ctx, entity, requireId2(id, action2));
8589
9147
  case "rm":
8590
- return pmRemove(ctx, entity, requireId2(id, action));
9148
+ return pmRemove(ctx, entity, requireId2(id, action2));
8591
9149
  }
8592
9150
  }
8593
9151
 
@@ -8620,8 +9178,8 @@ function printPlatformStatus(status, out) {
8620
9178
  if (status.verdict.reasons.length) {
8621
9179
  out.log(`reasons ${status.verdict.reasons.join(", ")}`);
8622
9180
  }
8623
- for (const action of status.nextActions) {
8624
- out.log(`next ${action.service} ${action.code} ${action.message}`);
9181
+ for (const action2 of status.nextActions) {
9182
+ out.log(`next ${action2.service} ${action2.code} ${action2.message}`);
8625
9183
  }
8626
9184
  }
8627
9185
  function value(input) {
@@ -8641,10 +9199,10 @@ async function platformCommand(parsed, deps = {}) {
8641
9199
  ["config", "context", "platform", "token", "email", "open", "json"],
8642
9200
  2
8643
9201
  );
8644
- const action = parsed.positionals[1];
8645
- if (action !== "status") {
9202
+ const action2 = parsed.positionals[1];
9203
+ if (action2 !== "status") {
8646
9204
  throw new Error(
8647
- `unknown platform action "${action ?? ""}". Try "odla-ai platform status --json".`
9205
+ `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
8648
9206
  );
8649
9207
  }
8650
9208
  const context = await resolveOperatorContext(parsed, {
@@ -8973,10 +9531,10 @@ async function o11yCommand(parsed, deps = {}) {
8973
9531
  ],
8974
9532
  2
8975
9533
  );
8976
- const action = parsed.positionals[1];
8977
- if (action !== "status") {
9534
+ const action2 = parsed.positionals[1];
9535
+ if (action2 !== "status") {
8978
9536
  throw new Error(
8979
- `unknown o11y action "${action ?? ""}". Try "odla-ai o11y status --json".`
9537
+ `unknown o11y action "${action2 ?? ""}". Try "odla-ai o11y status --json".`
8980
9538
  );
8981
9539
  }
8982
9540
  const minutes = statusMinutes(
@@ -9105,7 +9663,7 @@ async function read2(url, headers, doFetch) {
9105
9663
  }
9106
9664
 
9107
9665
  // src/provision.ts
9108
- var import_apps7 = require("@odla-ai/apps");
9666
+ var import_apps10 = require("@odla-ai/apps");
9109
9667
  var import_ai3 = require("@odla-ai/ai");
9110
9668
  var import_node_process10 = __toESM(require("process"), 1);
9111
9669
 
@@ -9115,7 +9673,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
9115
9673
  const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
9116
9674
  for (const integration of integrations) {
9117
9675
  for (const seed of integration.seeds ?? []) {
9118
- const payload = await postJson2(doFetch, `${base}/query`, dbKey, {
9676
+ const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
9119
9677
  query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
9120
9678
  });
9121
9679
  const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
@@ -9126,7 +9684,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
9126
9684
  out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
9127
9685
  continue;
9128
9686
  }
9129
- await postJson2(doFetch, `${base}/transact`, dbKey, {
9687
+ await postJson3(doFetch, `${base}/transact`, dbKey, {
9130
9688
  mutationId: `integration:${integration.id}:seed:${seed.id}`,
9131
9689
  ops: [{
9132
9690
  t: "update",
@@ -9141,7 +9699,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
9141
9699
  }
9142
9700
  }
9143
9701
  }
9144
- async function postJson2(doFetch, url, bearer, body) {
9702
+ async function postJson3(doFetch, url, bearer, body) {
9145
9703
  const res = await doFetch(url, {
9146
9704
  method: "POST",
9147
9705
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
@@ -9162,9 +9720,9 @@ async function responseText(res) {
9162
9720
  }
9163
9721
 
9164
9722
  // src/provision-credentials.ts
9165
- var import_apps5 = require("@odla-ai/apps");
9723
+ var import_apps9 = require("@odla-ai/apps");
9166
9724
  async function provisionEnvCredentials(opts) {
9167
- const tenantId = (0, import_apps5.tenantIdFor)(opts.cfg.app.id, opts.env);
9725
+ const tenantId = (0, import_apps9.tenantIdFor)(opts.cfg.app.id, opts.env);
9168
9726
  const prior = opts.credentials?.envs[opts.env];
9169
9727
  let credentials = opts.credentials;
9170
9728
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -9220,14 +9778,14 @@ async function mintDbKey(opts, tenantId) {
9220
9778
  appId: tenantId
9221
9779
  })
9222
9780
  });
9223
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText4(created)}`);
9781
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
9224
9782
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
9225
9783
  method: "POST",
9226
9784
  headers,
9227
9785
  body: "{}"
9228
9786
  });
9229
9787
  }
9230
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
9788
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
9231
9789
  const body = await res.json();
9232
9790
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
9233
9791
  return body.key;
@@ -9243,58 +9801,11 @@ async function issueO11yToken(opts) {
9243
9801
  `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`
9244
9802
  );
9245
9803
  }
9246
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText4(res)}`);
9804
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
9247
9805
  const body = await res.json();
9248
9806
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
9249
9807
  return body.token;
9250
9808
  }
9251
- async function safeText4(res) {
9252
- try {
9253
- return redactSecrets((await res.text()).slice(0, 500));
9254
- } catch {
9255
- return "";
9256
- }
9257
- }
9258
-
9259
- // src/provision-helpers.ts
9260
- var import_ai2 = require("@odla-ai/ai");
9261
- var import_apps6 = require("@odla-ai/apps");
9262
- function defaultSecretName(provider) {
9263
- const names = import_ai2.DEFAULT_SECRET_NAMES;
9264
- return names[provider] ?? `${provider}_api_key`;
9265
- }
9266
- async function assertTenantAdminAccess(doFetch, cfg, env, token) {
9267
- const tenantId = (0, import_apps6.tenantIdFor)(cfg.app.id, env);
9268
- const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
9269
- headers: { authorization: `Bearer ${token}` }
9270
- });
9271
- if (res.ok || res.status === 404) return;
9272
- if (res.status === 403) {
9273
- throw new Error(
9274
- `${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`
9275
- );
9276
- }
9277
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
9278
- }
9279
- async function postJson3(doFetch, url, bearer, body) {
9280
- const res = await doFetch(url, {
9281
- method: "POST",
9282
- headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
9283
- body: JSON.stringify(body)
9284
- });
9285
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
9286
- }
9287
- function normalizeClerkConfig(value2) {
9288
- if (!value2) return null;
9289
- if (typeof value2 === "string") {
9290
- const publishableKey2 = envValue(value2);
9291
- return publishableKey2 ? { publishableKey: publishableKey2 } : null;
9292
- }
9293
- if (typeof value2 !== "object") return null;
9294
- const cfg = value2;
9295
- const publishableKey = envValue(cfg.publishableKey);
9296
- return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
9297
- }
9298
9809
  async function safeText5(res) {
9299
9810
  try {
9300
9811
  return redactSecrets((await res.text()).slice(0, 500));
@@ -9373,7 +9884,7 @@ async function provision(options) {
9373
9884
  }
9374
9885
  const doFetch = options.fetch ?? fetch;
9375
9886
  const token = await getDeveloperToken(cfg, options, doFetch, out);
9376
- const apps = (0, import_apps7.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
9887
+ const apps = (0, import_apps10.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
9377
9888
  const existing = await apps.resolveApp(cfg.app.id);
9378
9889
  if (existing) {
9379
9890
  out.log(`app: ${cfg.app.id} already exists`);
@@ -9384,7 +9895,7 @@ async function provision(options) {
9384
9895
  for (const env of cfg.envs) {
9385
9896
  await assertTenantAdminAccess(doFetch, cfg, env, token);
9386
9897
  }
9387
- const serviceOrder = (0, import_apps7.orderAppServices)(cfg.services);
9898
+ const serviceOrder = (0, import_apps10.orderAppServices)(cfg.services);
9388
9899
  for (const env of cfg.envs) {
9389
9900
  for (const service of serviceOrder) {
9390
9901
  if (service === "ai") {
@@ -9417,7 +9928,7 @@ async function provision(options) {
9417
9928
  }
9418
9929
  }
9419
9930
  for (const env of cfg.envs) {
9420
- const tenantId = (0, import_apps7.tenantIdFor)(cfg.app.id, env);
9931
+ const tenantId = (0, import_apps10.tenantIdFor)(cfg.app.id, env);
9421
9932
  credentials = await provisionEnvCredentials({
9422
9933
  cfg,
9423
9934
  env,
@@ -9450,11 +9961,11 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
9450
9961
  }
9451
9962
  }
9452
9963
  if (schema && dbKey) {
9453
- await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
9964
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
9454
9965
  out.log(`${env}: schema pushed`);
9455
9966
  }
9456
9967
  if (rules) {
9457
- await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
9968
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
9458
9969
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
9459
9970
  }
9460
9971
  if (dbKey) {
@@ -9551,6 +10062,7 @@ var COMMAND_SURFACE = {
9551
10062
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
9552
10063
  capabilities: {},
9553
10064
  code: { connect: {} },
10065
+ config: { diff: {}, plan: {} },
9554
10066
  context: { show: {}, list: {}, save: {}, remove: {} },
9555
10067
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
9556
10068
  discuss: {
@@ -9835,7 +10347,7 @@ async function runbookRemove(ctx, slug) {
9835
10347
 
9836
10348
  // src/runbook-import.ts
9837
10349
  var import_node_fs17 = require("fs");
9838
- var import_node_path13 = require("path");
10350
+ var import_node_path14 = require("path");
9839
10351
  function parseRunbook(text, slug) {
9840
10352
  let rest = text;
9841
10353
  const meta = {};
@@ -9864,8 +10376,8 @@ function readRunbookDir(dir) {
9864
10376
  const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
9865
10377
  if (!files.length) throw new Error(`no .md files in ${dir}`);
9866
10378
  return files.map((file) => {
9867
- const slug = (0, import_node_path13.basename)(file, ".md");
9868
- const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path13.join)(dir, file), "utf8"), slug);
10379
+ const slug = (0, import_node_path14.basename)(file, ".md");
10380
+ const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path14.join)(dir, file), "utf8"), slug);
9869
10381
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
9870
10382
  });
9871
10383
  }
@@ -9938,7 +10450,7 @@ async function upsert(ctx, r, visibility) {
9938
10450
  // src/runbook-impact.ts
9939
10451
  var import_node_child_process7 = require("child_process");
9940
10452
  var import_node_fs18 = require("fs");
9941
- var import_node_path14 = require("path");
10453
+ var import_node_path15 = require("path");
9942
10454
 
9943
10455
  // src/runbook-impact-scan.ts
9944
10456
  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$]*)/;
@@ -10107,7 +10619,7 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
10107
10619
  }
10108
10620
  function manifestLabeller(root) {
10109
10621
  return (workspace) => {
10110
- const manifest = (0, import_node_path14.join)(root, workspace, "package.json");
10622
+ const manifest = (0, import_node_path15.join)(root, workspace, "package.json");
10111
10623
  if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
10112
10624
  try {
10113
10625
  const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
@@ -10177,7 +10689,7 @@ function report3(ctx, impacts) {
10177
10689
  async function runbookImpact(ctx, options, deps = {}) {
10178
10690
  const cwd = deps.cwd ?? process.cwd();
10179
10691
  const runGit = deps.runGit ?? gitRunner(cwd);
10180
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path14.join)(cwd, path), "utf8"));
10692
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path15.join)(cwd, path), "utf8"));
10181
10693
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
10182
10694
  if (!surfaces.length) {
10183
10695
  return ctx.out.log(
@@ -10312,7 +10824,7 @@ async function runbookComment(ctx, slug, body) {
10312
10824
  var import_node_child_process8 = require("child_process");
10313
10825
  var import_node_fs19 = require("fs");
10314
10826
  var import_node_os5 = require("os");
10315
- var import_node_path15 = require("path");
10827
+ var import_node_path16 = require("path");
10316
10828
  var import_node_process12 = __toESM(require("process"), 1);
10317
10829
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
10318
10830
  function resolveEditor(env = import_node_process12.default.env) {
@@ -10338,8 +10850,8 @@ function editText(initial, slug, deps = {}) {
10338
10850
  );
10339
10851
  if (!interactive())
10340
10852
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
10341
- const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10342
- const file = (0, import_node_path15.join)(dir, `${slug}.md`);
10853
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10854
+ const file = (0, import_node_path16.join)(dir, `${slug}.md`);
10343
10855
  try {
10344
10856
  (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
10345
10857
  const code = defaultRunOrInjected(deps)(editor, file);
@@ -10447,12 +10959,12 @@ var ALLOWED2 = [
10447
10959
  "platform",
10448
10960
  "context"
10449
10961
  ];
10450
- function requireSlug(slug, action) {
10451
- if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
10962
+ function requireSlug(slug, action2) {
10963
+ if (!slug) throw new Error(`"runbook ${action2}" needs a slug, e.g. "odla-ai runbook ${action2} release"`);
10452
10964
  return slug;
10453
10965
  }
10454
10966
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
10455
- async function buildContext3(parsed, deps, action) {
10967
+ async function buildContext3(parsed, deps, action2) {
10456
10968
  const appIdOption = stringOpt(parsed.options.app);
10457
10969
  const context = await resolveOperatorContext(parsed, {
10458
10970
  allowMissingConfig: true
@@ -10462,7 +10974,7 @@ async function buildContext3(parsed, deps, action) {
10462
10974
  const out = deps.stdout ?? console;
10463
10975
  const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
10464
10976
  const dryRun = parsed.options["dry-run"] === true;
10465
- if (action === "import" && dryRun) {
10977
+ if (action2 === "import" && dryRun) {
10466
10978
  return {
10467
10979
  platformUrl: cfg.platformUrl,
10468
10980
  token: "",
@@ -10472,12 +10984,12 @@ async function buildContext3(parsed, deps, action) {
10472
10984
  appId
10473
10985
  };
10474
10986
  }
10475
- const needsCapability = WRITES.has(action) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
10987
+ const needsCapability = WRITES.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
10476
10988
  const token = needsCapability ? await getScopedPlatformToken({
10477
10989
  platform: cfg.platformUrl,
10478
10990
  scope: "platform:runbook:write",
10479
10991
  email: stringOpt(parsed.options.email),
10480
- label: `odla CLI (runbook ${action})`,
10992
+ label: `odla CLI (runbook ${action2})`,
10481
10993
  fetch: doFetch,
10482
10994
  stdout: out,
10483
10995
  openApprovalUrl: deps.openUrl,
@@ -10512,11 +11024,11 @@ async function buildContext3(parsed, deps, action) {
10512
11024
  };
10513
11025
  }
10514
11026
  async function runbookCommand(parsed, deps = {}) {
10515
- const action = parsed.positionals[1] ?? "list";
10516
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
10517
- const ctx = await buildContext3(parsed, deps, action);
11027
+ const action2 = parsed.positionals[1] ?? "list";
11028
+ assertArgs(parsed, ALLOWED2, action2 === "ask" || action2 === "search" ? 64 : 4);
11029
+ const ctx = await buildContext3(parsed, deps, action2);
10518
11030
  const slug = parsed.positionals[2];
10519
- switch (action) {
11031
+ switch (action2) {
10520
11032
  case "list":
10521
11033
  return runbookList(ctx, parsed.options.all === true, stringOpt(parsed.options.q));
10522
11034
  case "ask": {
@@ -10601,7 +11113,7 @@ async function runbookCommand(parsed, deps = {}) {
10601
11113
  case "rm":
10602
11114
  return runbookRemove(ctx, requireSlug(slug, "rm"));
10603
11115
  default:
10604
- throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
11116
+ throw new Error(`unknown runbook action "${action2}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
10605
11117
  }
10606
11118
  }
10607
11119
 
@@ -10759,7 +11271,7 @@ function hostedSeverity(value2, flag) {
10759
11271
  var import_security2 = require("@odla-ai/security");
10760
11272
 
10761
11273
  // src/security.ts
10762
- var import_node_path16 = require("path");
11274
+ var import_node_path17 = require("path");
10763
11275
  var import_security = require("@odla-ai/security");
10764
11276
  var import_node3 = require("@odla-ai/security/node");
10765
11277
  async function runHostedSecurity(options) {
@@ -10771,9 +11283,9 @@ async function runHostedSecurity(options) {
10771
11283
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
10772
11284
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
10773
11285
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
10774
- const target = (0, import_node_path16.resolve)(options.target ?? cfg?.rootDir ?? ".");
10775
- const output = (0, import_node_path16.resolve)(options.out ?? (0, import_node_path16.resolve)(target, ".odla/security/hosted"));
10776
- const outputRelative = (0, import_node_path16.relative)(target, output).split(import_node_path16.sep).join("/");
11286
+ const target = (0, import_node_path17.resolve)(options.target ?? cfg?.rootDir ?? ".");
11287
+ const output = (0, import_node_path17.resolve)(options.out ?? (0, import_node_path17.resolve)(target, ".odla/security/hosted"));
11288
+ const outputRelative = (0, import_node_path17.relative)(target, output).split(import_node_path17.sep).join("/");
10777
11289
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
10778
11290
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
10779
11291
  const tokenRequest = {
@@ -10785,7 +11297,7 @@ async function runHostedSecurity(options) {
10785
11297
  };
10786
11298
  const token = await injectedToken(options, tokenRequest);
10787
11299
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
10788
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path16.isAbsolute)(outputRelative) ? [outputRelative] : []
11300
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path17.isAbsolute)(outputRelative) ? [outputRelative] : []
10789
11301
  });
10790
11302
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
10791
11303
  platform,
@@ -10803,7 +11315,7 @@ async function runHostedSecurity(options) {
10803
11315
  });
10804
11316
  const harness = (0, import_security.createSecurityHarness)({
10805
11317
  profile,
10806
- store: new import_node3.FileRunStore((0, import_node_path16.resolve)(output, "state")),
11318
+ store: new import_node3.FileRunStore((0, import_node_path17.resolve)(output, "state")),
10807
11319
  discoveryReasoner: hosted.discoveryReasoner,
10808
11320
  validationReasoner: hosted.validationReasoner,
10809
11321
  policy: {
@@ -10827,7 +11339,7 @@ async function runHostedSecurity(options) {
10827
11339
  function selectEnv(requested, declared, configPath, rootDir) {
10828
11340
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
10829
11341
  if (!env || !declared.includes(env)) {
10830
- const shown = (0, import_node_path16.relative)(rootDir, configPath) || configPath;
11342
+ const shown = (0, import_node_path17.relative)(rootDir, configPath) || configPath;
10831
11343
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
10832
11344
  }
10833
11345
  return env;
@@ -10856,7 +11368,7 @@ function printSummary(out, appId, env, run, report4, output) {
10856
11368
  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}`);
10857
11369
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
10858
11370
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
10859
- out.log(` report: ${(0, import_node_path16.resolve)(output, "REPORT.md")}`);
11371
+ out.log(` report: ${(0, import_node_path17.resolve)(output, "REPORT.md")}`);
10860
11372
  }
10861
11373
  function formatBudget(usage) {
10862
11374
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -11222,8 +11734,8 @@ async function securityCommand(parsed, dependencies) {
11222
11734
  else await runLocalSecurityCommand(parsed, dependencies);
11223
11735
  }
11224
11736
  async function githubSecurityCommand(parsed, dependencies) {
11225
- const action = parsed.positionals[2];
11226
- if (action === "disconnect") {
11737
+ const action2 = parsed.positionals[2];
11738
+ if (action2 === "disconnect") {
11227
11739
  assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
11228
11740
  const context2 = await hostedSecurityContext(parsed, dependencies);
11229
11741
  const sourceId = requiredString(parsed.options.source, "--source");
@@ -11238,7 +11750,7 @@ async function githubSecurityCommand(parsed, dependencies) {
11238
11750
  context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
11239
11751
  return;
11240
11752
  }
11241
- if (action !== "connect") {
11753
+ if (action2 !== "connect") {
11242
11754
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
11243
11755
  }
11244
11756
  assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);