@odla-ai/cli 0.25.12 → 0.25.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -304,7 +304,7 @@ var import_node_process2 = __toESM(require("process"), 1);
304
304
  async function openUrl(url, options = {}) {
305
305
  const command = openerFor(options.platform ?? import_node_process2.default.platform);
306
306
  const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
307
- await new Promise((resolve11, reject) => {
307
+ await new Promise((resolve12, reject) => {
308
308
  const child = doSpawn(command.cmd, [...command.args, url], {
309
309
  stdio: "ignore",
310
310
  detached: true
@@ -312,7 +312,7 @@ async function openUrl(url, options = {}) {
312
312
  child.once("error", reject);
313
313
  child.once("spawn", () => {
314
314
  child.unref();
315
- resolve11();
315
+ resolve12();
316
316
  });
317
317
  });
318
318
  }
@@ -2246,8 +2246,8 @@ function credential(value) {
2246
2246
  // src/calendar-poll.ts
2247
2247
  async function waitForCalendarPoll(milliseconds, signal) {
2248
2248
  if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
2249
- await new Promise((resolve11, reject) => {
2250
- const timer = setTimeout(resolve11, milliseconds);
2249
+ await new Promise((resolve12, reject) => {
2250
+ const timer = setTimeout(resolve12, milliseconds);
2251
2251
  signal?.addEventListener("abort", () => {
2252
2252
  clearTimeout(timer);
2253
2253
  reject(signal.reason ?? new Error("calendar connection aborted"));
@@ -2448,7 +2448,8 @@ var CAPABILITIES = {
2448
2448
  "compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
2449
2449
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
2450
2450
  "validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
2451
- "read one versioned o11y status envelope spanning application RED, exact Worker versions observed in traffic, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, and a bounded machine verdict",
2451
+ "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",
2452
+ "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",
2452
2453
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
2453
2454
  "run app-attributed hosted security discovery and independent validation without provider keys",
2454
2455
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -5476,21 +5477,21 @@ async function verifyCodeCandidate(input) {
5476
5477
  const recipes = [];
5477
5478
  const logs = [];
5478
5479
  for (const recipe2 of policy.recipes) {
5479
- const clean = await stageWorkspace(staged.workspaceDir, limits);
5480
+ const clean4 = await stageWorkspace(staged.workspaceDir, limits);
5480
5481
  try {
5481
- if (await digestStagedWorkspace(clean.workspaceDir, limits) !== sourceDigest) {
5482
+ if (await digestStagedWorkspace(clean4.workspaceDir, limits) !== sourceDigest) {
5482
5483
  throw new TypeError("clean verifier source changed before execution");
5483
5484
  }
5484
5485
  const result = checkedResult(await input.recipeExecutor.run({
5485
- workspaceDir: clean.workspaceDir,
5486
+ workspaceDir: clean4.workspaceDir,
5486
5487
  recipe: recipe2,
5487
5488
  signal: input.signal
5488
5489
  }), recipe2.maxOutputBytes);
5489
- const artifacts = await inspectArtifacts(clean.workspaceDir, recipe2);
5490
+ const artifacts = await inspectArtifacts(clean4.workspaceDir, recipe2);
5490
5491
  recipes.push(recipeReceipt(recipe2, result, artifacts));
5491
5492
  logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
5492
5493
  } finally {
5493
- await clean.cleanup();
5494
+ await clean4.cleanup();
5494
5495
  }
5495
5496
  }
5496
5497
  const fields = {
@@ -6631,8 +6632,12 @@ Usage:
6631
6632
  odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
6632
6633
  odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6633
6634
  odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6634
- odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
6635
- odla-ai whoami [--json]
6635
+ odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
6636
+ odla-ai context list [--json]
6637
+ odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
6638
+ odla-ai context remove <name> --yes [--json]
6639
+ odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
6640
+ odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
6636
6641
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6637
6642
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6638
6643
  odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
@@ -6697,6 +6702,11 @@ Commands:
6697
6702
  whoami Report who this terminal is authenticated as, and whether it holds
6698
6703
  platform admin. A handshake-minted device token is never admin,
6699
6704
  however the human who approved it is configured.
6705
+ context Explain selected config, platform, app, environment, and
6706
+ developer-token provenance without printing credentials or
6707
+ starting a device handshake. Operator work can run outside a
6708
+ project checkout with explicit flags or ODLA_* environment
6709
+ variables.
6700
6710
  setup Install offline odla runbooks for common coding-agent harnesses.
6701
6711
  init Create a generic odla.config.mjs plus starter schema/rules files.
6702
6712
  doctor Validate and summarize the project config without network calls.
@@ -6768,6 +6778,15 @@ Safety:
6768
6778
  Provision caches the approved developer token and service credentials under
6769
6779
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
6770
6780
  preflights Wrangler before any shown-once issuance or destructive rotation.
6781
+ Projectless PM, Discussions, o11y, runbook, and identity commands use
6782
+ --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
6783
+ ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
6784
+ select it explicitly with --context or ODLA_CONTEXT. Each named context gets
6785
+ isolated developer and scoped-token caches. Override their locations with
6786
+ ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
6787
+ the metadata file. Flags and specific ODLA_* scope variables beat a selected
6788
+ context, which beats project config. There is no ambient current context.
6789
+ "context show" reports only provenance and cache state and never authenticates.
6771
6790
  Provision opens the approval page in your browser automatically whenever the
6772
6791
  machine can show one, including agent-driven runs; only CI, SSH, and
6773
6792
  display-less hosts skip it. Use --open to force or --no-open to suppress.
@@ -6885,8 +6904,8 @@ function hostedPollTimeout(value = 10 * 6e4) {
6885
6904
  }
6886
6905
  async function waitForHostedPoll(milliseconds, signal) {
6887
6906
  if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
6888
- await new Promise((resolve11, reject) => {
6889
- const timer = setTimeout(resolve11, milliseconds);
6907
+ await new Promise((resolve12, reject) => {
6908
+ const timer = setTimeout(resolve12, milliseconds);
6890
6909
  signal?.addEventListener("abort", () => {
6891
6910
  clearTimeout(timer);
6892
6911
  reject(signal.reason ?? new DOMException("aborted", "AbortError"));
@@ -7396,6 +7415,384 @@ async function codeCommand(parsed, dependencies) {
7396
7415
  });
7397
7416
  }
7398
7417
 
7418
+ // src/operator-credentials.ts
7419
+ var import_node_process7 = __toESM(require("process"), 1);
7420
+ function developerTokenStatus(context, parsed, now = Date.now()) {
7421
+ const cached = readJsonFile(context.cfg.local.tokenFile);
7422
+ const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
7423
+ const source = clean(
7424
+ stringOpt(parsed.options.token)
7425
+ ) ? "flag" : clean(import_node_process7.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
7426
+ return {
7427
+ source,
7428
+ cacheFile: context.cfg.local.tokenFile,
7429
+ cacheStatus
7430
+ };
7431
+ }
7432
+ function clean(value) {
7433
+ const normalized = value?.trim();
7434
+ return normalized || void 0;
7435
+ }
7436
+
7437
+ // src/operator-context.ts
7438
+ var import_node_fs14 = require("fs");
7439
+ var import_node_path12 = require("path");
7440
+ var import_node_process9 = __toESM(require("process"), 1);
7441
+
7442
+ // src/operator-profiles.ts
7443
+ var import_node_fs13 = require("fs");
7444
+ var import_node_os4 = require("os");
7445
+ var import_node_path11 = require("path");
7446
+ var import_node_process8 = __toESM(require("process"), 1);
7447
+ function operatorProfileFile() {
7448
+ return (0, import_node_path11.resolve)(
7449
+ clean2(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".odla", "contexts.json")
7450
+ );
7451
+ }
7452
+ function resolveOperatorProfile(parsed) {
7453
+ const fromFlag = clean2(stringOpt(parsed.options.context));
7454
+ const fromEnvironment = clean2(import_node_process8.default.env.ODLA_CONTEXT);
7455
+ const name = fromFlag ?? fromEnvironment ?? null;
7456
+ const file = operatorProfileFile();
7457
+ if (!name) {
7458
+ return { name: null, source: "unresolved", file, value: null };
7459
+ }
7460
+ assertOperatorName(name, "context");
7461
+ const profiles = readOperatorProfiles(file);
7462
+ const value = Object.hasOwn(profiles, name) ? profiles[name] : void 0;
7463
+ if (!value) {
7464
+ throw new Error(
7465
+ `operator context "${name}" not found in ${file}; run "odla-ai context list" or save it first`
7466
+ );
7467
+ }
7468
+ return {
7469
+ name,
7470
+ source: fromFlag ? "flag" : "environment",
7471
+ file,
7472
+ value
7473
+ };
7474
+ }
7475
+ function listOperatorProfiles(file = operatorProfileFile()) {
7476
+ return Object.entries(readOperatorProfiles(file)).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({ name, ...profile }));
7477
+ }
7478
+ function saveOperatorProfile(name, profile, file = operatorProfileFile()) {
7479
+ assertOperatorName(name, "context");
7480
+ const normalized = validateProfile(profile, `operator context "${name}"`);
7481
+ const profiles = readOperatorProfiles(file);
7482
+ profiles[name] = normalized;
7483
+ writePrivateJson(file, { schemaVersion: 1, profiles });
7484
+ }
7485
+ function removeOperatorProfile(name, file = operatorProfileFile()) {
7486
+ assertOperatorName(name, "context");
7487
+ const profiles = readOperatorProfiles(file);
7488
+ if (!Object.hasOwn(profiles, name)) return false;
7489
+ delete profiles[name];
7490
+ writePrivateJson(file, { schemaVersion: 1, profiles });
7491
+ return true;
7492
+ }
7493
+ function operatorCredentialFiles(selection) {
7494
+ const base = selection.name ? (0, import_node_path11.join)((0, import_node_path11.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".odla");
7495
+ return {
7496
+ developer: (0, import_node_path11.join)(base, "dev-token.json"),
7497
+ scoped: (0, import_node_path11.join)(base, "admin-token.local.json")
7498
+ };
7499
+ }
7500
+ function assertOperatorName(value, label) {
7501
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
7502
+ throw new Error(
7503
+ `${label} must contain lowercase letters, numbers, and hyphens`
7504
+ );
7505
+ }
7506
+ }
7507
+ function readOperatorProfiles(file) {
7508
+ if (!(0, import_node_fs13.existsSync)(file)) return emptyProfiles();
7509
+ let raw;
7510
+ try {
7511
+ raw = JSON.parse((0, import_node_fs13.readFileSync)(file, "utf8"));
7512
+ } catch {
7513
+ throw new Error(`operator context file ${file} is not valid JSON`);
7514
+ }
7515
+ if (!raw || typeof raw !== "object" || raw.schemaVersion !== 1 || !raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles)) {
7516
+ throw new Error(`operator context file ${file} has an invalid shape`);
7517
+ }
7518
+ const unknown = Object.keys(raw).filter(
7519
+ (key) => !["schemaVersion", "profiles"].includes(key)
7520
+ );
7521
+ if (unknown.length > 0) {
7522
+ throw new Error(
7523
+ `operator context file ${file} contains unsupported field(s): ${unknown.sort().join(", ")}`
7524
+ );
7525
+ }
7526
+ const profiles = emptyProfiles();
7527
+ for (const [name, value] of Object.entries(
7528
+ raw.profiles
7529
+ )) {
7530
+ assertOperatorName(name, "context");
7531
+ profiles[name] = validateProfile(value, `operator context "${name}"`);
7532
+ }
7533
+ return profiles;
7534
+ }
7535
+ function emptyProfiles() {
7536
+ return /* @__PURE__ */ Object.create(null);
7537
+ }
7538
+ function validateProfile(value, label) {
7539
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7540
+ throw new Error(`${label} has an invalid shape`);
7541
+ }
7542
+ const unknown = Object.keys(value).filter(
7543
+ (key) => !["platform", "app", "environment"].includes(key)
7544
+ );
7545
+ if (unknown.length > 0) {
7546
+ throw new Error(
7547
+ `${label} contains unsupported field(s): ${unknown.sort().join(", ")}; contexts store scope metadata only, never credentials`
7548
+ );
7549
+ }
7550
+ const candidate = value;
7551
+ if (typeof candidate.platform !== "string") {
7552
+ throw new Error(`${label} needs an absolute platform URL`);
7553
+ }
7554
+ const platform = platformAudience(candidate.platform);
7555
+ const app = clean2(candidate.app);
7556
+ const environment2 = clean2(candidate.environment);
7557
+ if (app) assertOperatorName(app, "app");
7558
+ if (environment2) assertOperatorName(environment2, "environment");
7559
+ return {
7560
+ platform,
7561
+ ...app ? { app } : {},
7562
+ ...environment2 ? { environment: environment2 } : {}
7563
+ };
7564
+ }
7565
+ function clean2(value) {
7566
+ const normalized = value?.trim();
7567
+ return normalized || void 0;
7568
+ }
7569
+
7570
+ // src/operator-context.ts
7571
+ var DEFAULT_PLATFORM2 = "https://odla.ai";
7572
+ async function resolveOperatorContext(parsed, options = {}) {
7573
+ const profile = resolveOperatorProfile(parsed);
7574
+ const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
7575
+ const configPath = (0, import_node_path12.resolve)(configArgument);
7576
+ const explicitConfig = parsed.options.config !== void 0;
7577
+ const hasConfig = (0, import_node_fs14.existsSync)(configPath);
7578
+ if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
7579
+ await loadProjectConfig(configArgument);
7580
+ }
7581
+ const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
7582
+ const platformFlag = clean3(stringOpt(parsed.options.platform));
7583
+ const platformEnvironment = clean3(import_node_process9.default.env.ODLA_PLATFORM_URL);
7584
+ const platformValue = platformAudience(
7585
+ platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
7586
+ );
7587
+ const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
7588
+ const appFlag = clean3(stringOpt(parsed.options.app));
7589
+ const appEnvironment = clean3(import_node_process9.default.env.ODLA_APP_ID);
7590
+ const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
7591
+ const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
7592
+ if (appValue) assertOperatorName(appValue, "app");
7593
+ if (options.requireApp && !appValue) {
7594
+ throw new Error(
7595
+ "app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
7596
+ );
7597
+ }
7598
+ const envFlag = clean3(stringOpt(parsed.options.env));
7599
+ const envEnvironment = clean3(import_node_process9.default.env.ODLA_ENV);
7600
+ const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
7601
+ const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
7602
+ if (environmentValue) {
7603
+ assertOperatorName(environmentValue, "environment");
7604
+ }
7605
+ const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
7606
+ const profileCredentials = operatorCredentialFiles(profile);
7607
+ const tokenFile = clean3(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path12.resolve)(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
7608
+ const scopedTokenFile = clean3(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path12.resolve)(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path12.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
7609
+ const cfg = loaded ? {
7610
+ ...loaded,
7611
+ platformUrl: platformValue,
7612
+ app: appValue ? {
7613
+ id: appValue,
7614
+ name: appValue === loaded.app.id ? loaded.app.name : appValue
7615
+ } : loaded.app,
7616
+ local: { ...loaded.local, tokenFile }
7617
+ } : {
7618
+ configPath,
7619
+ rootDir,
7620
+ platformUrl: platformValue,
7621
+ dbEndpoint: platformValue,
7622
+ app: {
7623
+ id: appValue ?? "operator",
7624
+ name: appValue ?? "Operator"
7625
+ },
7626
+ envs: environmentValue ? [environmentValue] : [],
7627
+ services: [],
7628
+ local: {
7629
+ tokenFile,
7630
+ credentialsFile: (0, import_node_path12.join)(rootDir, ".odla", "credentials.local.json"),
7631
+ devVarsFile: (0, import_node_path12.join)(rootDir, ".dev.vars"),
7632
+ gitignore: true
7633
+ }
7634
+ };
7635
+ return {
7636
+ cfg,
7637
+ profile: {
7638
+ name: profile.name,
7639
+ source: profile.source,
7640
+ file: profile.file
7641
+ },
7642
+ config: {
7643
+ path: configPath,
7644
+ status: loaded ? "loaded" : "absent",
7645
+ explicit: explicitConfig
7646
+ },
7647
+ platform: { value: platformValue, source: platformSource },
7648
+ app: { value: appValue, source: appSource },
7649
+ environment: {
7650
+ value: environmentValue,
7651
+ source: environmentSource
7652
+ },
7653
+ credentials: {
7654
+ developerTokenFile: tokenFile,
7655
+ scopedTokenFile
7656
+ }
7657
+ };
7658
+ }
7659
+ function clean3(value) {
7660
+ const normalized = value?.trim();
7661
+ return normalized || void 0;
7662
+ }
7663
+
7664
+ // src/context-command.ts
7665
+ async function contextCommand(parsed, deps = {}) {
7666
+ assertArgs(
7667
+ parsed,
7668
+ [
7669
+ "config",
7670
+ "context",
7671
+ "platform",
7672
+ "app",
7673
+ "env",
7674
+ "token",
7675
+ "json",
7676
+ "yes"
7677
+ ],
7678
+ 3
7679
+ );
7680
+ const action = parsed.positionals[1];
7681
+ const out = deps.stdout ?? console;
7682
+ if (action === "list") {
7683
+ const profiles = listOperatorProfiles();
7684
+ if (parsed.options.json === true) {
7685
+ out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
7686
+ return;
7687
+ }
7688
+ if (profiles.length === 0) {
7689
+ out.log("No named operator contexts.");
7690
+ return;
7691
+ }
7692
+ out.log("name platform app environment");
7693
+ for (const profile of profiles) {
7694
+ out.log(
7695
+ `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
7696
+ );
7697
+ }
7698
+ return;
7699
+ }
7700
+ if (action === "save") {
7701
+ if (parsed.options.token !== void 0) {
7702
+ throw new Error(
7703
+ "context save does not accept --token; contexts store scope metadata only"
7704
+ );
7705
+ }
7706
+ const name = requireName(parsed);
7707
+ const context2 = await resolveOperatorContext(parsed, {
7708
+ allowMissingConfig: true
7709
+ });
7710
+ const profile = {
7711
+ platform: context2.platform.value,
7712
+ ...context2.app.value ? { app: context2.app.value } : {},
7713
+ ...context2.environment.value ? { environment: context2.environment.value } : {}
7714
+ };
7715
+ saveOperatorProfile(name, profile, context2.profile.file);
7716
+ if (parsed.options.json === true) {
7717
+ out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
7718
+ } else {
7719
+ out.log(`saved operator context "${name}" (${profile.platform})`);
7720
+ }
7721
+ return;
7722
+ }
7723
+ if (action === "remove") {
7724
+ const name = requireName(parsed);
7725
+ if (parsed.options.yes !== true) {
7726
+ throw new Error(`context remove "${name}" requires --yes`);
7727
+ }
7728
+ const removed = removeOperatorProfile(name);
7729
+ const result2 = {
7730
+ schemaVersion: 1,
7731
+ name,
7732
+ removed,
7733
+ credentialCachesRemoved: false,
7734
+ grantsRevoked: false
7735
+ };
7736
+ if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
7737
+ else {
7738
+ const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
7739
+ out.log(`${status}; credential caches were not deleted or revoked`);
7740
+ }
7741
+ return;
7742
+ }
7743
+ if (action !== "show") {
7744
+ throw new Error(
7745
+ `unknown context action "${action ?? ""}". Try show|list|save|remove.`
7746
+ );
7747
+ }
7748
+ const context = await resolveOperatorContext(parsed, {
7749
+ allowMissingConfig: true,
7750
+ defaultEnvironment: "prod"
7751
+ });
7752
+ const token = developerTokenStatus(context, parsed);
7753
+ const result = {
7754
+ schemaVersion: 1,
7755
+ profile: context.profile,
7756
+ config: context.config,
7757
+ platform: context.platform,
7758
+ app: context.app,
7759
+ environment: context.environment,
7760
+ authentication: {
7761
+ developerToken: token,
7762
+ scopedTokenCacheFile: context.credentials.scopedTokenFile,
7763
+ handshakeStarted: false
7764
+ }
7765
+ };
7766
+ if (parsed.options.json === true) {
7767
+ out.log(JSON.stringify(result, null, 2));
7768
+ return;
7769
+ }
7770
+ out.log(`config: ${context.config.status} (${context.config.path})`);
7771
+ out.log(
7772
+ `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
7773
+ );
7774
+ out.log(
7775
+ `platform: ${context.platform.value} (${context.platform.source})`
7776
+ );
7777
+ out.log(
7778
+ `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
7779
+ );
7780
+ out.log(
7781
+ `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
7782
+ );
7783
+ out.log(
7784
+ `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
7785
+ );
7786
+ out.log("handshake: not started");
7787
+ }
7788
+ function requireName(parsed) {
7789
+ const name = parsed.positionals[2]?.trim();
7790
+ if (!name) {
7791
+ throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
7792
+ }
7793
+ return name;
7794
+ }
7795
+
7399
7796
  // src/discuss-actions.ts
7400
7797
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
7401
7798
  async function request(ctx, method, path, body) {
@@ -7447,8 +7844,9 @@ async function discussGroups(ctx) {
7447
7844
  }
7448
7845
  async function discussList(ctx, parsed) {
7449
7846
  const query = new URLSearchParams();
7847
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7848
+ if (app) query.set("app", app);
7450
7849
  for (const [flag, param] of [
7451
- ["app", "app"],
7452
7850
  ["q", "q"],
7453
7851
  ["state", "state"],
7454
7852
  ["limit", "limit"],
@@ -7524,7 +7922,7 @@ function renderRead(ctx, topic, posts) {
7524
7922
  }
7525
7923
  }
7526
7924
  async function discussPost(ctx, parsed) {
7527
- const appId = stringOpt(parsed.options.app);
7925
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
7528
7926
  if (!appId) throw new Error("discuss post needs --app <appId>");
7529
7927
  const subject = stringOpt(parsed.options.subject);
7530
7928
  if (!subject) throw new Error('discuss post needs --subject "\u2026"');
@@ -7556,7 +7954,7 @@ async function discussResolve(ctx, id, resolved, parsed) {
7556
7954
  }
7557
7955
  async function discussWho(ctx, parsed) {
7558
7956
  const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
7559
- const app = stringOpt(parsed.options.app);
7957
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7560
7958
  if (app) query.set("app", app);
7561
7959
  const kinds = stringOpt(parsed.options.kinds);
7562
7960
  if (kinds) query.set("kinds", kinds);
@@ -7660,14 +8058,14 @@ function jsonl(ctx, parsed, value) {
7660
8058
  }
7661
8059
  async function discussWatch(ctx, topicId, parsed) {
7662
8060
  if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
7663
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
8061
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
7664
8062
  const now = ctx.now ?? Date.now;
7665
8063
  const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7666
8064
  const timeoutSeconds = numberOpt2(parsed, "timeout");
7667
8065
  const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
7668
8066
  const by = stringOpt(parsed.options.by);
7669
8067
  const self = stringOpt(parsed.options.self);
7670
- const app = stringOpt(parsed.options.app);
8068
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7671
8069
  let cursor = stringOpt(parsed.options.cursor);
7672
8070
  let firstSuccess = true;
7673
8071
  let consecutiveFailures = 0;
@@ -7812,14 +8210,19 @@ var ALLOWED = [
7812
8210
  "timeout",
7813
8211
  "cursor",
7814
8212
  "jsonl",
7815
- "mutation-id"
8213
+ "mutation-id",
8214
+ "platform",
8215
+ "context"
7816
8216
  ];
7817
8217
  function requireId(id, action) {
7818
8218
  if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
7819
8219
  return id;
7820
8220
  }
7821
8221
  async function buildContext(parsed, deps) {
7822
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8222
+ const context = await resolveOperatorContext(parsed, {
8223
+ allowMissingConfig: true
8224
+ });
8225
+ const { cfg } = context;
7823
8226
  const doFetch = deps.fetch ?? fetch;
7824
8227
  const out = deps.stdout ?? console;
7825
8228
  const token = await getDeveloperToken(
@@ -7833,7 +8236,16 @@ async function buildContext(parsed, deps) {
7833
8236
  doFetch,
7834
8237
  out
7835
8238
  );
7836
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
8239
+ return {
8240
+ platformUrl: cfg.platformUrl,
8241
+ token,
8242
+ doFetch,
8243
+ out,
8244
+ json: parsed.options.json === true,
8245
+ // Preserve Discussions' existing cross-project default for config-backed
8246
+ // use while allowing remote agents to scope via ODLA_APP_ID or a profile.
8247
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
8248
+ };
7837
8249
  }
7838
8250
  async function discussCommand(parsed, deps = {}) {
7839
8251
  assertArgs(parsed, ALLOWED, 3);
@@ -7945,7 +8357,7 @@ async function pmList(ctx, entity, parsed) {
7945
8357
  assignee: "assigneeId",
7946
8358
  decision: "decisionId"
7947
8359
  };
7948
- const app = stringOpt(parsed.options.app);
8360
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7949
8361
  if (app) q.set("app", app);
7950
8362
  for (const [flag, param] of Object.entries(filters)) {
7951
8363
  const v = stringOpt(parsed.options[flag]);
@@ -7964,7 +8376,7 @@ async function pmList(ctx, entity, parsed) {
7964
8376
  });
7965
8377
  }
7966
8378
  async function pmAdd(ctx, entity, parsed) {
7967
- const appId = stringOpt(parsed.options.app);
8379
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
7968
8380
  if (!appId) throw new Error("pm add needs --app <appId>");
7969
8381
  const input = collectEntityFields(entity, parsed, false);
7970
8382
  if (!input.title) throw new Error("pm add needs --title <title>");
@@ -8015,7 +8427,7 @@ async function allRecords(ctx, entity, appId) {
8015
8427
  }
8016
8428
  }
8017
8429
  async function pmHandoff(ctx, parsed) {
8018
- const appId = stringOpt(parsed.options.app);
8430
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8019
8431
  if (!appId) throw new Error("pm handoff needs --app <appId>");
8020
8432
  const [goals, tasks, bugs] = await Promise.all([
8021
8433
  allRecords(ctx, "goal", appId),
@@ -8110,14 +8522,19 @@ var ALLOWED2 = [
8110
8522
  "limit",
8111
8523
  "offset",
8112
8524
  "q",
8113
- "mutation-id"
8525
+ "mutation-id",
8526
+ "platform",
8527
+ "context"
8114
8528
  ];
8115
8529
  function requireId2(id, action) {
8116
8530
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
8117
8531
  return id;
8118
8532
  }
8119
8533
  async function buildContext2(parsed, deps) {
8120
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8534
+ const context = await resolveOperatorContext(parsed, {
8535
+ allowMissingConfig: true
8536
+ });
8537
+ const { cfg } = context;
8121
8538
  const doFetch = deps.fetch ?? fetch;
8122
8539
  const out = deps.stdout ?? console;
8123
8540
  const token = await getDeveloperToken(
@@ -8126,7 +8543,16 @@ async function buildContext2(parsed, deps) {
8126
8543
  doFetch,
8127
8544
  out
8128
8545
  );
8129
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
8546
+ return {
8547
+ platformUrl: cfg.platformUrl,
8548
+ token,
8549
+ doFetch,
8550
+ out,
8551
+ json: parsed.options.json === true,
8552
+ // Preserve PM's existing cross-project default when a config is present;
8553
+ // only an explicit remote-agent environment or profile selects an app.
8554
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
8555
+ };
8130
8556
  }
8131
8557
  async function pmCommand(parsed, deps = {}) {
8132
8558
  const word = parsed.positionals[1] ?? "";
@@ -8175,6 +8601,20 @@ function statusVerdict(reads) {
8175
8601
  sourceHttp(reasons, "canary", reads.canary);
8176
8602
  sourceHttp(reasons, "collector", reads.collector);
8177
8603
  sourceHttp(reasons, "provider", reads.provider);
8604
+ if (reads.providerCapacity) {
8605
+ sourceHttp(
8606
+ reasons,
8607
+ "provider",
8608
+ reads.providerCapacity,
8609
+ "provider_capacity_"
8610
+ );
8611
+ }
8612
+ sourceHttp(
8613
+ reasons,
8614
+ "provider",
8615
+ reads.providerReconciliation,
8616
+ "provider_reconciliation_"
8617
+ );
8178
8618
  sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
8179
8619
  const liveStatus = String(reads.liveSync.body.status ?? "");
8180
8620
  if (liveStatus === "partial") {
@@ -8247,6 +8687,17 @@ function statusVerdict(reads) {
8247
8687
  severity: "degraded"
8248
8688
  });
8249
8689
  }
8690
+ const reconciliationStatus = String(
8691
+ reads.providerReconciliation.body.status ?? ""
8692
+ );
8693
+ if (reads.providerReconciliation.httpStatus >= 200 && reads.providerReconciliation.httpStatus < 300 && reconciliationStatus && reconciliationStatus !== "within_budget") {
8694
+ const reconciliationReason = reads.providerReconciliation.body.reason;
8695
+ reasons.push({
8696
+ source: "provider",
8697
+ code: reconciliationStatus === "outside_budget" ? typeof reconciliationReason === "string" && reconciliationReason ? reconciliationReason : "request_coverage_outside_budget" : `request_reconciliation_${reconciliationStatus}`,
8698
+ severity: "degraded"
8699
+ });
8700
+ }
8250
8701
  const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
8251
8702
  if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
8252
8703
  reasons.push({
@@ -8317,8 +8768,19 @@ function printO11yStatus(status, out) {
8317
8768
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
8318
8769
  );
8319
8770
  const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
8771
+ const providerCapacity = record6(status.provider.body.capacity) ? status.provider.body.capacity : {};
8772
+ const workerMemory = record6(providerCapacity.memory) ? providerCapacity.memory : {};
8773
+ out.log(
8774
+ `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
8775
+ );
8776
+ for (const line of providerCapacityLines(status.providerCapacity)) {
8777
+ out.log(line);
8778
+ }
8779
+ const coverage = record6(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
8780
+ const coverageCounts = record6(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
8781
+ const coverageBudget = record6(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
8320
8782
  out.log(
8321
- `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
8783
+ `request-coverage ${status.providerReconciliation.httpStatus} ${String(status.providerReconciliation.body.status ?? status.providerReconciliation.body.error ?? "unavailable")} ${optionalPercent(coverage.applicationCoverage)} application/provider ${numeric3(coverageCounts.applicationRequests)}/${numeric3(coverageCounts.providerRequests)} requests \xB1${optionalPercent(coverageBudget.maxRelativeError)} budget`
8322
8784
  );
8323
8785
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
8324
8786
  const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
@@ -8329,6 +8791,27 @@ function printO11yStatus(status, out) {
8329
8791
  `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8330
8792
  );
8331
8793
  }
8794
+ function providerCapacityLines(read3) {
8795
+ const resources = record6(read3.body.resources) ? read3.body.resources : {};
8796
+ const durableObjects = record6(resources.durableObjects) ? resources.durableObjects : {};
8797
+ const periodic = record6(durableObjects.periodic) ? durableObjects.periodic : {};
8798
+ const storage = record6(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
8799
+ const d1 = record6(resources.d1) ? resources.d1 : {};
8800
+ const d1Activity = record6(d1.activity) ? d1.activity : {};
8801
+ const d1Storage = record6(d1.storage) ? d1.storage : {};
8802
+ const d1Latency = record6(d1Activity.latency) ? d1Activity.latency : {};
8803
+ const r2 = record6(resources.r2) ? resources.r2 : {};
8804
+ const r2Operations = record6(r2.operations) ? r2.operations : {};
8805
+ const r2Storage = record6(r2.storage) ? r2.storage : {};
8806
+ const status = String(
8807
+ read3.body.status ?? read3.body.error ?? "unavailable"
8808
+ );
8809
+ return [
8810
+ `cloudflare-capacity ${read3.httpStatus} ${status} ${optionalBytes(storage.storedBytes)} SQLite DO storage ${optionalCount(periodic.activeWebsocketConnectionsMax, "active socket max")} ${optionalAgeSeconds(storage.ageSeconds)} storage evidence age`,
8811
+ `cloudflare-d1 ${read3.httpStatus} ${status} ${optionalBytes(d1Storage.databaseSizeBytes)} storage ${numeric3(d1Activity.readQueries) + numeric3(d1Activity.writeQueries)} queries ${numeric3(d1Activity.rowsRead)}/${numeric3(d1Activity.rowsWritten)} rows read/written ${optionalNumeric(d1Latency.worstDatabaseHourP99Ms)} worst database-hour p99 ${optionalAgeSeconds(d1Activity.ageSeconds)} activity evidence age`,
8812
+ `cloudflare-r2 ${read3.httpStatus} ${status} ${optionalBytes(r2Storage.storedBytes)} storage ${numeric3(r2Storage.objectCount)} objects ${numeric3(r2Operations.requests)} operations ${numeric3(r2Operations.userErrorRequests) + numeric3(r2Operations.internalErrorRequests)} provider errors ${optionalAgeSeconds(r2Operations.ageSeconds)} operation evidence age`
8813
+ ];
8814
+ }
8332
8815
  function liveSyncLine(read3) {
8333
8816
  const performance = record6(read3.body.performance) ? read3.body.performance : {};
8334
8817
  const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
@@ -8347,12 +8830,42 @@ function optionalAge(value) {
8347
8830
  if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
8348
8831
  return `${Math.max(0, Math.round(value / 1e3))}s`;
8349
8832
  }
8833
+ function optionalPercent(value) {
8834
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value * 100)}%` : "\u2014";
8835
+ }
8836
+ function optionalBytes(value) {
8837
+ if (typeof value !== "number" || !Number.isFinite(value)) return "\u2014";
8838
+ if (value >= 1024 * 1024 * 1024) {
8839
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
8840
+ }
8841
+ if (value >= 1024 * 1024) {
8842
+ return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
8843
+ }
8844
+ if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
8845
+ return `${Math.round(value)} B`;
8846
+ }
8847
+ function optionalCount(value, unit) {
8848
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ${unit}` : `\u2014 ${unit}`;
8849
+ }
8850
+ function optionalAgeSeconds(value) {
8851
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.max(0, Math.round(value))}s` : "unknown";
8852
+ }
8350
8853
 
8351
8854
  // src/o11y-command.ts
8352
8855
  async function o11yCommand(parsed, deps = {}) {
8353
8856
  assertArgs(
8354
8857
  parsed,
8355
- ["config", "token", "email", "json", "app", "env", "minutes"],
8858
+ [
8859
+ "config",
8860
+ "context",
8861
+ "platform",
8862
+ "token",
8863
+ "email",
8864
+ "json",
8865
+ "app",
8866
+ "env",
8867
+ "minutes"
8868
+ ],
8356
8869
  2
8357
8870
  );
8358
8871
  const action = parsed.positionals[1];
@@ -8364,9 +8877,12 @@ async function o11yCommand(parsed, deps = {}) {
8364
8877
  const minutes = statusMinutes(
8365
8878
  numberOpt(parsed.options.minutes, "--minutes") ?? 60
8366
8879
  );
8367
- const cfg = await loadProjectConfig(
8368
- stringOpt(parsed.options.config) ?? "odla.config.mjs"
8369
- );
8880
+ const context = await resolveOperatorContext(parsed, {
8881
+ allowMissingConfig: true,
8882
+ defaultEnvironment: "prod",
8883
+ requireApp: true
8884
+ });
8885
+ const cfg = context.cfg;
8370
8886
  const doFetch = deps.fetch ?? fetch;
8371
8887
  const out = deps.stdout ?? console;
8372
8888
  const token = await getDeveloperToken(
@@ -8380,8 +8896,8 @@ async function o11yCommand(parsed, deps = {}) {
8380
8896
  doFetch,
8381
8897
  out
8382
8898
  );
8383
- const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
8384
- const env = stringOpt(parsed.options.env)?.trim() || "prod";
8899
+ const appId = context.app.value;
8900
+ const env = context.environment.value;
8385
8901
  const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
8386
8902
  const query = new URLSearchParams({ env, minutes: String(minutes) });
8387
8903
  const headers = { authorization: `Bearer ${token}` };
@@ -8398,6 +8914,8 @@ async function o11yCommand(parsed, deps = {}) {
8398
8914
  canary,
8399
8915
  collector,
8400
8916
  provider,
8917
+ providerCapacity,
8918
+ providerReconciliation,
8401
8919
  providerHistory
8402
8920
  ] = await Promise.all([
8403
8921
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
@@ -8414,6 +8932,19 @@ async function o11yCommand(parsed, deps = {}) {
8414
8932
  ),
8415
8933
  read2(`${base}/self-health?${query}`, headers, doFetch),
8416
8934
  read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
8935
+ appId === "odla-db" ? read2(`${base}/provider/cloudflare/capacity`, headers, doFetch) : Promise.resolve({
8936
+ httpStatus: 204,
8937
+ body: {
8938
+ status: "not_applicable",
8939
+ scope: "cloudflare_account",
8940
+ reason: "account-scoped Durable Object capacity is exposed only through odla-db"
8941
+ }
8942
+ }),
8943
+ read2(
8944
+ `${base}/provider/cloudflare/reconciliation?${query}`,
8945
+ headers,
8946
+ doFetch
8947
+ ),
8417
8948
  read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
8418
8949
  ]);
8419
8950
  const verdict = statusVerdict({
@@ -8422,10 +8953,12 @@ async function o11yCommand(parsed, deps = {}) {
8422
8953
  canary,
8423
8954
  collector,
8424
8955
  provider,
8956
+ providerCapacity,
8957
+ providerReconciliation,
8425
8958
  providerHistory
8426
8959
  });
8427
8960
  const status = {
8428
- schemaVersion: 6,
8961
+ schemaVersion: 9,
8429
8962
  scope: { appId, env, minutes },
8430
8963
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
8431
8964
  verdict,
@@ -8435,6 +8968,8 @@ async function o11yCommand(parsed, deps = {}) {
8435
8968
  canary,
8436
8969
  collector,
8437
8970
  provider,
8971
+ providerCapacity,
8972
+ providerReconciliation,
8438
8973
  providerHistory
8439
8974
  };
8440
8975
  if (parsed.options.json === true) {
@@ -8467,7 +9002,7 @@ async function read2(url, headers, doFetch) {
8467
9002
  // src/provision.ts
8468
9003
  var import_apps5 = require("@odla-ai/apps");
8469
9004
  var import_ai3 = require("@odla-ai/ai");
8470
- var import_node_process7 = __toESM(require("process"), 1);
9005
+ var import_node_process10 = __toESM(require("process"), 1);
8471
9006
 
8472
9007
  // src/integration-provision.ts
8473
9008
  var import_db3 = require("@odla-ai/db");
@@ -8826,7 +9361,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
8826
9361
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
8827
9362
  }
8828
9363
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
8829
- const key = import_node_process7.default.env[cfg.ai.keyEnv];
9364
+ const key = import_node_process10.default.env[cfg.ai.keyEnv];
8830
9365
  if (key) {
8831
9366
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
8832
9367
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -8867,8 +9402,8 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
8867
9402
  }
8868
9403
 
8869
9404
  // src/record.ts
8870
- var import_node_fs13 = require("fs");
8871
- var import_node_process8 = __toESM(require("process"), 1);
9405
+ var import_node_fs15 = require("fs");
9406
+ var import_node_process11 = __toESM(require("process"), 1);
8872
9407
 
8873
9408
  // src/surface.ts
8874
9409
  var PM_ACTIONS = {
@@ -8916,6 +9451,7 @@ var COMMAND_SURFACE = {
8916
9451
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
8917
9452
  capabilities: {},
8918
9453
  code: { connect: {} },
9454
+ context: { show: {}, list: {}, save: {}, remove: {} },
8919
9455
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
8920
9456
  discuss: {
8921
9457
  groups: {},
@@ -9019,7 +9555,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
9019
9555
 
9020
9556
  // src/record.ts
9021
9557
  function recordInvocation(parsed) {
9022
- const file = import_node_process8.default.env.ODLA_CLI_RECORD;
9558
+ const file = import_node_process11.default.env.ODLA_CLI_RECORD;
9023
9559
  if (!file) return;
9024
9560
  try {
9025
9561
  const entry = {
@@ -9027,19 +9563,14 @@ function recordInvocation(parsed) {
9027
9563
  options: Object.keys(parsed.options).sort()
9028
9564
  };
9029
9565
  if (!entry.path.length) return;
9030
- (0, import_node_fs13.appendFileSync)(file, `${JSON.stringify(entry)}
9566
+ (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
9031
9567
  `);
9032
9568
  } catch {
9033
9569
  }
9034
9570
  }
9035
9571
 
9036
- // src/runbook-command.ts
9037
- var import_node_fs18 = require("fs");
9038
- var import_node_path14 = require("path");
9039
- var import_node_process10 = __toESM(require("process"), 1);
9040
-
9041
9572
  // src/runbook-actions.ts
9042
- var import_node_fs14 = require("fs");
9573
+ var import_node_fs16 = require("fs");
9043
9574
 
9044
9575
  // src/runbook-requires.ts
9045
9576
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -9124,7 +9655,7 @@ async function bySlug(ctx, slug) {
9124
9655
  function readBody(file, inline) {
9125
9656
  if (inline !== void 0) return inline;
9126
9657
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
9127
- return (0, import_node_fs14.readFileSync)(file === "-" ? 0 : file, "utf8");
9658
+ return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
9128
9659
  }
9129
9660
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
9130
9661
  async function runbookList(ctx, all, query) {
@@ -9211,8 +9742,8 @@ async function runbookRemove(ctx, slug) {
9211
9742
  }
9212
9743
 
9213
9744
  // src/runbook-import.ts
9214
- var import_node_fs15 = require("fs");
9215
- var import_node_path11 = require("path");
9745
+ var import_node_fs17 = require("fs");
9746
+ var import_node_path13 = require("path");
9216
9747
  function parseRunbook(text, slug) {
9217
9748
  let rest = text;
9218
9749
  const meta = {};
@@ -9237,12 +9768,12 @@ function parseRunbook(text, slug) {
9237
9768
  };
9238
9769
  }
9239
9770
  function readRunbookDir(dir) {
9240
- if (!(0, import_node_fs15.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
9241
- const files = (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
9771
+ if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
9772
+ const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
9242
9773
  if (!files.length) throw new Error(`no .md files in ${dir}`);
9243
9774
  return files.map((file) => {
9244
- const slug = (0, import_node_path11.basename)(file, ".md");
9245
- const parsed = parseRunbook((0, import_node_fs15.readFileSync)((0, import_node_path11.join)(dir, file), "utf8"), slug);
9775
+ const slug = (0, import_node_path13.basename)(file, ".md");
9776
+ const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path13.join)(dir, file), "utf8"), slug);
9246
9777
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
9247
9778
  });
9248
9779
  }
@@ -9314,8 +9845,8 @@ async function upsert(ctx, r, visibility) {
9314
9845
 
9315
9846
  // src/runbook-impact.ts
9316
9847
  var import_node_child_process7 = require("child_process");
9317
- var import_node_fs16 = require("fs");
9318
- var import_node_path12 = require("path");
9848
+ var import_node_fs18 = require("fs");
9849
+ var import_node_path14 = require("path");
9319
9850
 
9320
9851
  // src/runbook-impact-scan.ts
9321
9852
  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$]*)/;
@@ -9484,10 +10015,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
9484
10015
  }
9485
10016
  function manifestLabeller(root) {
9486
10017
  return (workspace) => {
9487
- const manifest = (0, import_node_path12.join)(root, workspace, "package.json");
9488
- if (!(0, import_node_fs16.existsSync)(manifest)) return void 0;
10018
+ const manifest = (0, import_node_path14.join)(root, workspace, "package.json");
10019
+ if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
9489
10020
  try {
9490
- const name = JSON.parse((0, import_node_fs16.readFileSync)(manifest, "utf8")).name;
10021
+ const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
9491
10022
  return typeof name === "string" ? name : void 0;
9492
10023
  } catch {
9493
10024
  return void 0;
@@ -9554,7 +10085,7 @@ function report3(ctx, impacts) {
9554
10085
  async function runbookImpact(ctx, options, deps = {}) {
9555
10086
  const cwd = deps.cwd ?? process.cwd();
9556
10087
  const runGit = deps.runGit ?? gitRunner(cwd);
9557
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs16.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
10088
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path14.join)(cwd, path), "utf8"));
9558
10089
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
9559
10090
  if (!surfaces.length) {
9560
10091
  return ctx.out.log(
@@ -9687,12 +10218,12 @@ async function runbookComment(ctx, slug, body) {
9687
10218
 
9688
10219
  // src/runbook-editor.ts
9689
10220
  var import_node_child_process8 = require("child_process");
9690
- var import_node_fs17 = require("fs");
9691
- var import_node_os4 = require("os");
9692
- var import_node_path13 = require("path");
9693
- var import_node_process9 = __toESM(require("process"), 1);
10221
+ var import_node_fs19 = require("fs");
10222
+ var import_node_os5 = require("os");
10223
+ var import_node_path15 = require("path");
10224
+ var import_node_process12 = __toESM(require("process"), 1);
9694
10225
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
9695
- function resolveEditor(env = import_node_process9.default.env) {
10226
+ function resolveEditor(env = import_node_process12.default.env) {
9696
10227
  for (const name of EDITOR_ENV) {
9697
10228
  const value = env[name];
9698
10229
  if (value && value.trim()) return value.trim();
@@ -9706,8 +10237,8 @@ function defaultRun(command, path) {
9706
10237
  return result.status ?? 0;
9707
10238
  }
9708
10239
  function editText(initial, slug, deps = {}) {
9709
- const env = deps.env ?? import_node_process9.default.env;
9710
- const interactive = deps.interactive ?? (() => Boolean(import_node_process9.default.stdin.isTTY));
10240
+ const env = deps.env ?? import_node_process12.default.env;
10241
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
9711
10242
  const editor = resolveEditor(env);
9712
10243
  if (!editor)
9713
10244
  throw new Error(
@@ -9715,16 +10246,16 @@ function editText(initial, slug, deps = {}) {
9715
10246
  );
9716
10247
  if (!interactive())
9717
10248
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
9718
- const dir = (0, import_node_fs17.mkdtempSync)((0, import_node_path13.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
9719
- const file = (0, import_node_path13.join)(dir, `${slug}.md`);
10249
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10250
+ const file = (0, import_node_path15.join)(dir, `${slug}.md`);
9720
10251
  try {
9721
- (0, import_node_fs17.writeFileSync)(file, initial, { mode: 384 });
10252
+ (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
9722
10253
  const code = defaultRunOrInjected(deps)(editor, file);
9723
10254
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
9724
- const edited = (0, import_node_fs17.readFileSync)(file, "utf8");
10255
+ const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
9725
10256
  return edited === initial ? null : edited;
9726
10257
  } finally {
9727
- (0, import_node_fs17.rmSync)(dir, { recursive: true, force: true });
10258
+ (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
9728
10259
  }
9729
10260
  }
9730
10261
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -9762,10 +10293,16 @@ function credentialKind(identity) {
9762
10293
  return identity.scopes.length ? "device token (scoped)" : "device token or session";
9763
10294
  }
9764
10295
  async function whoamiCommand(parsed, deps = {}) {
9765
- assertArgs(parsed, ["config", "token", "email", "json"], 1);
10296
+ assertArgs(
10297
+ parsed,
10298
+ ["config", "context", "platform", "token", "email", "json"],
10299
+ 1
10300
+ );
9766
10301
  const out = deps.stdout ?? console;
9767
10302
  const doFetch = deps.fetch ?? fetch;
9768
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10303
+ const { cfg } = await resolveOperatorContext(parsed, {
10304
+ allowMissingConfig: true
10305
+ });
9769
10306
  const token = await getDeveloperToken(
9770
10307
  cfg,
9771
10308
  {
@@ -9814,34 +10351,24 @@ var ALLOWED3 = [
9814
10351
  "dry-run",
9815
10352
  "limit",
9816
10353
  "base",
9817
- "requires"
10354
+ "requires",
10355
+ "platform",
10356
+ "context"
9818
10357
  ];
9819
10358
  function requireSlug(slug, action) {
9820
10359
  if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
9821
10360
  return slug;
9822
10361
  }
9823
10362
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
9824
- var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
9825
- async function loadOrDefaultConfig(configPath, action, appId) {
9826
- const projectScoped = appId !== void 0;
9827
- if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs18.existsSync)((0, import_node_path14.resolve)(configPath))) {
9828
- const rootDir = import_node_process10.default.cwd();
9829
- return {
9830
- configPath,
9831
- platformUrl: import_node_process10.default.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
9832
- rootDir,
9833
- app: { id: "", name: "" },
9834
- local: { tokenFile: (0, import_node_path14.join)(rootDir, ".odla/dev-token.json") }
9835
- };
9836
- }
9837
- return loadProjectConfig(configPath);
9838
- }
9839
10363
  async function buildContext3(parsed, deps, action) {
9840
- const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
9841
- const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
10364
+ const appIdOption = stringOpt(parsed.options.app);
10365
+ const context = await resolveOperatorContext(parsed, {
10366
+ allowMissingConfig: true
10367
+ });
10368
+ const { cfg } = context;
9842
10369
  const doFetch = deps.fetch ?? fetch;
9843
10370
  const out = deps.stdout ?? console;
9844
- const appId = stringOpt(parsed.options.app) ?? PLATFORM_SCOPE;
10371
+ const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
9845
10372
  const dryRun = parsed.options["dry-run"] === true;
9846
10373
  if (action === "import" && dryRun) {
9847
10374
  return {
@@ -9862,8 +10389,10 @@ async function buildContext3(parsed, deps, action) {
9862
10389
  fetch: doFetch,
9863
10390
  stdout: out,
9864
10391
  openApprovalUrl: deps.openUrl,
9865
- // Anchor the grant cache to this project, not the shell's cwd.
9866
- rootDir: cfg.rootDir
10392
+ // A project, named context, or the global operator context owns the
10393
+ // exact-scope cache; it never follows an arbitrary shell directory.
10394
+ rootDir: cfg.rootDir,
10395
+ tokenFile: context.credentials.scopedTokenFile
9867
10396
  }) : await getDeveloperToken(
9868
10397
  cfg,
9869
10398
  {
@@ -10138,7 +10667,7 @@ function hostedSeverity(value, flag) {
10138
10667
  var import_security2 = require("@odla-ai/security");
10139
10668
 
10140
10669
  // src/security.ts
10141
- var import_node_path15 = require("path");
10670
+ var import_node_path16 = require("path");
10142
10671
  var import_security = require("@odla-ai/security");
10143
10672
  var import_node3 = require("@odla-ai/security/node");
10144
10673
  async function runHostedSecurity(options) {
@@ -10150,9 +10679,9 @@ async function runHostedSecurity(options) {
10150
10679
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
10151
10680
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
10152
10681
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
10153
- const target = (0, import_node_path15.resolve)(options.target ?? cfg?.rootDir ?? ".");
10154
- const output = (0, import_node_path15.resolve)(options.out ?? (0, import_node_path15.resolve)(target, ".odla/security/hosted"));
10155
- const outputRelative = (0, import_node_path15.relative)(target, output).split(import_node_path15.sep).join("/");
10682
+ const target = (0, import_node_path16.resolve)(options.target ?? cfg?.rootDir ?? ".");
10683
+ const output = (0, import_node_path16.resolve)(options.out ?? (0, import_node_path16.resolve)(target, ".odla/security/hosted"));
10684
+ const outputRelative = (0, import_node_path16.relative)(target, output).split(import_node_path16.sep).join("/");
10156
10685
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
10157
10686
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
10158
10687
  const tokenRequest = {
@@ -10164,7 +10693,7 @@ async function runHostedSecurity(options) {
10164
10693
  };
10165
10694
  const token = await injectedToken(options, tokenRequest);
10166
10695
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
10167
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path15.isAbsolute)(outputRelative) ? [outputRelative] : []
10696
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path16.isAbsolute)(outputRelative) ? [outputRelative] : []
10168
10697
  });
10169
10698
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
10170
10699
  platform,
@@ -10182,7 +10711,7 @@ async function runHostedSecurity(options) {
10182
10711
  });
10183
10712
  const harness = (0, import_security.createSecurityHarness)({
10184
10713
  profile,
10185
- store: new import_node3.FileRunStore((0, import_node_path15.resolve)(output, "state")),
10714
+ store: new import_node3.FileRunStore((0, import_node_path16.resolve)(output, "state")),
10186
10715
  discoveryReasoner: hosted.discoveryReasoner,
10187
10716
  validationReasoner: hosted.validationReasoner,
10188
10717
  policy: {
@@ -10206,7 +10735,7 @@ async function runHostedSecurity(options) {
10206
10735
  function selectEnv(requested, declared, configPath, rootDir) {
10207
10736
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
10208
10737
  if (!env || !declared.includes(env)) {
10209
- const shown = (0, import_node_path15.relative)(rootDir, configPath) || configPath;
10738
+ const shown = (0, import_node_path16.relative)(rootDir, configPath) || configPath;
10210
10739
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
10211
10740
  }
10212
10741
  return env;
@@ -10235,7 +10764,7 @@ function printSummary(out, appId, env, run, report4, output) {
10235
10764
  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}`);
10236
10765
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
10237
10766
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
10238
- out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
10767
+ out.log(` report: ${(0, import_node_path16.resolve)(output, "REPORT.md")}`);
10239
10768
  }
10240
10769
  function formatBudget(usage) {
10241
10770
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -10715,6 +11244,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10715
11244
  await whoamiCommand(parsed, runtime);
10716
11245
  return;
10717
11246
  }
11247
+ if (command === "context") {
11248
+ await contextCommand(parsed, runtime);
11249
+ return;
11250
+ }
10718
11251
  if (command === "runbook") {
10719
11252
  await runbookCommand(parsed, runtime);
10720
11253
  return;