@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/bin.cjs CHANGED
@@ -244,7 +244,7 @@ var import_node_process2 = __toESM(require("process"), 1);
244
244
  async function openUrl(url, options = {}) {
245
245
  const command = openerFor(options.platform ?? import_node_process2.default.platform);
246
246
  const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
247
- await new Promise((resolve11, reject) => {
247
+ await new Promise((resolve12, reject) => {
248
248
  const child = doSpawn(command.cmd, [...command.args, url], {
249
249
  stdio: "ignore",
250
250
  detached: true
@@ -252,7 +252,7 @@ async function openUrl(url, options = {}) {
252
252
  child.once("error", reject);
253
253
  child.once("spawn", () => {
254
254
  child.unref();
255
- resolve11();
255
+ resolve12();
256
256
  });
257
257
  });
258
258
  }
@@ -2186,8 +2186,8 @@ function credential(value) {
2186
2186
  // src/calendar-poll.ts
2187
2187
  async function waitForCalendarPoll(milliseconds, signal) {
2188
2188
  if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
2189
- await new Promise((resolve11, reject) => {
2190
- const timer = setTimeout(resolve11, milliseconds);
2189
+ await new Promise((resolve12, reject) => {
2190
+ const timer = setTimeout(resolve12, milliseconds);
2191
2191
  signal?.addEventListener("abort", () => {
2192
2192
  clearTimeout(timer);
2193
2193
  reject(signal.reason ?? new Error("calendar connection aborted"));
@@ -2388,7 +2388,8 @@ var CAPABILITIES = {
2388
2388
  "compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
2389
2389
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
2390
2390
  "validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
2391
- "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",
2391
+ "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",
2392
+ "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",
2392
2393
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
2393
2394
  "run app-attributed hosted security discovery and independent validation without provider keys",
2394
2395
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -5416,21 +5417,21 @@ async function verifyCodeCandidate(input) {
5416
5417
  const recipes = [];
5417
5418
  const logs = [];
5418
5419
  for (const recipe2 of policy.recipes) {
5419
- const clean = await stageWorkspace(staged.workspaceDir, limits);
5420
+ const clean4 = await stageWorkspace(staged.workspaceDir, limits);
5420
5421
  try {
5421
- if (await digestStagedWorkspace(clean.workspaceDir, limits) !== sourceDigest) {
5422
+ if (await digestStagedWorkspace(clean4.workspaceDir, limits) !== sourceDigest) {
5422
5423
  throw new TypeError("clean verifier source changed before execution");
5423
5424
  }
5424
5425
  const result = checkedResult(await input.recipeExecutor.run({
5425
- workspaceDir: clean.workspaceDir,
5426
+ workspaceDir: clean4.workspaceDir,
5426
5427
  recipe: recipe2,
5427
5428
  signal: input.signal
5428
5429
  }), recipe2.maxOutputBytes);
5429
- const artifacts = await inspectArtifacts(clean.workspaceDir, recipe2);
5430
+ const artifacts = await inspectArtifacts(clean4.workspaceDir, recipe2);
5430
5431
  recipes.push(recipeReceipt(recipe2, result, artifacts));
5431
5432
  logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
5432
5433
  } finally {
5433
- await clean.cleanup();
5434
+ await clean4.cleanup();
5434
5435
  }
5435
5436
  }
5436
5437
  const fields = {
@@ -6571,8 +6572,12 @@ Usage:
6571
6572
  odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
6572
6573
  odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6573
6574
  odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6574
- odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
6575
- odla-ai whoami [--json]
6575
+ odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
6576
+ odla-ai context list [--json]
6577
+ odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
6578
+ odla-ai context remove <name> --yes [--json]
6579
+ odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
6580
+ odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
6576
6581
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6577
6582
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6578
6583
  odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
@@ -6637,6 +6642,11 @@ Commands:
6637
6642
  whoami Report who this terminal is authenticated as, and whether it holds
6638
6643
  platform admin. A handshake-minted device token is never admin,
6639
6644
  however the human who approved it is configured.
6645
+ context Explain selected config, platform, app, environment, and
6646
+ developer-token provenance without printing credentials or
6647
+ starting a device handshake. Operator work can run outside a
6648
+ project checkout with explicit flags or ODLA_* environment
6649
+ variables.
6640
6650
  setup Install offline odla runbooks for common coding-agent harnesses.
6641
6651
  init Create a generic odla.config.mjs plus starter schema/rules files.
6642
6652
  doctor Validate and summarize the project config without network calls.
@@ -6708,6 +6718,15 @@ Safety:
6708
6718
  Provision caches the approved developer token and service credentials under
6709
6719
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
6710
6720
  preflights Wrangler before any shown-once issuance or destructive rotation.
6721
+ Projectless PM, Discussions, o11y, runbook, and identity commands use
6722
+ --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
6723
+ ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
6724
+ select it explicitly with --context or ODLA_CONTEXT. Each named context gets
6725
+ isolated developer and scoped-token caches. Override their locations with
6726
+ ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
6727
+ the metadata file. Flags and specific ODLA_* scope variables beat a selected
6728
+ context, which beats project config. There is no ambient current context.
6729
+ "context show" reports only provenance and cache state and never authenticates.
6711
6730
  Provision opens the approval page in your browser automatically whenever the
6712
6731
  machine can show one, including agent-driven runs; only CI, SSH, and
6713
6732
  display-less hosts skip it. Use --open to force or --no-open to suppress.
@@ -6825,8 +6844,8 @@ function hostedPollTimeout(value = 10 * 6e4) {
6825
6844
  }
6826
6845
  async function waitForHostedPoll(milliseconds, signal) {
6827
6846
  if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
6828
- await new Promise((resolve11, reject) => {
6829
- const timer = setTimeout(resolve11, milliseconds);
6847
+ await new Promise((resolve12, reject) => {
6848
+ const timer = setTimeout(resolve12, milliseconds);
6830
6849
  signal?.addEventListener("abort", () => {
6831
6850
  clearTimeout(timer);
6832
6851
  reject(signal.reason ?? new DOMException("aborted", "AbortError"));
@@ -7336,6 +7355,384 @@ async function codeCommand(parsed, dependencies) {
7336
7355
  });
7337
7356
  }
7338
7357
 
7358
+ // src/operator-credentials.ts
7359
+ var import_node_process7 = __toESM(require("process"), 1);
7360
+ function developerTokenStatus(context, parsed, now = Date.now()) {
7361
+ const cached = readJsonFile(context.cfg.local.tokenFile);
7362
+ const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
7363
+ const source = clean(
7364
+ stringOpt(parsed.options.token)
7365
+ ) ? "flag" : clean(import_node_process7.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
7366
+ return {
7367
+ source,
7368
+ cacheFile: context.cfg.local.tokenFile,
7369
+ cacheStatus
7370
+ };
7371
+ }
7372
+ function clean(value) {
7373
+ const normalized = value?.trim();
7374
+ return normalized || void 0;
7375
+ }
7376
+
7377
+ // src/operator-context.ts
7378
+ var import_node_fs14 = require("fs");
7379
+ var import_node_path12 = require("path");
7380
+ var import_node_process9 = __toESM(require("process"), 1);
7381
+
7382
+ // src/operator-profiles.ts
7383
+ var import_node_fs13 = require("fs");
7384
+ var import_node_os4 = require("os");
7385
+ var import_node_path11 = require("path");
7386
+ var import_node_process8 = __toESM(require("process"), 1);
7387
+ function operatorProfileFile() {
7388
+ return (0, import_node_path11.resolve)(
7389
+ clean2(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".odla", "contexts.json")
7390
+ );
7391
+ }
7392
+ function resolveOperatorProfile(parsed) {
7393
+ const fromFlag = clean2(stringOpt(parsed.options.context));
7394
+ const fromEnvironment = clean2(import_node_process8.default.env.ODLA_CONTEXT);
7395
+ const name = fromFlag ?? fromEnvironment ?? null;
7396
+ const file = operatorProfileFile();
7397
+ if (!name) {
7398
+ return { name: null, source: "unresolved", file, value: null };
7399
+ }
7400
+ assertOperatorName(name, "context");
7401
+ const profiles = readOperatorProfiles(file);
7402
+ const value = Object.hasOwn(profiles, name) ? profiles[name] : void 0;
7403
+ if (!value) {
7404
+ throw new Error(
7405
+ `operator context "${name}" not found in ${file}; run "odla-ai context list" or save it first`
7406
+ );
7407
+ }
7408
+ return {
7409
+ name,
7410
+ source: fromFlag ? "flag" : "environment",
7411
+ file,
7412
+ value
7413
+ };
7414
+ }
7415
+ function listOperatorProfiles(file = operatorProfileFile()) {
7416
+ return Object.entries(readOperatorProfiles(file)).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({ name, ...profile }));
7417
+ }
7418
+ function saveOperatorProfile(name, profile, file = operatorProfileFile()) {
7419
+ assertOperatorName(name, "context");
7420
+ const normalized = validateProfile(profile, `operator context "${name}"`);
7421
+ const profiles = readOperatorProfiles(file);
7422
+ profiles[name] = normalized;
7423
+ writePrivateJson(file, { schemaVersion: 1, profiles });
7424
+ }
7425
+ function removeOperatorProfile(name, file = operatorProfileFile()) {
7426
+ assertOperatorName(name, "context");
7427
+ const profiles = readOperatorProfiles(file);
7428
+ if (!Object.hasOwn(profiles, name)) return false;
7429
+ delete profiles[name];
7430
+ writePrivateJson(file, { schemaVersion: 1, profiles });
7431
+ return true;
7432
+ }
7433
+ function operatorCredentialFiles(selection) {
7434
+ 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");
7435
+ return {
7436
+ developer: (0, import_node_path11.join)(base, "dev-token.json"),
7437
+ scoped: (0, import_node_path11.join)(base, "admin-token.local.json")
7438
+ };
7439
+ }
7440
+ function assertOperatorName(value, label) {
7441
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
7442
+ throw new Error(
7443
+ `${label} must contain lowercase letters, numbers, and hyphens`
7444
+ );
7445
+ }
7446
+ }
7447
+ function readOperatorProfiles(file) {
7448
+ if (!(0, import_node_fs13.existsSync)(file)) return emptyProfiles();
7449
+ let raw;
7450
+ try {
7451
+ raw = JSON.parse((0, import_node_fs13.readFileSync)(file, "utf8"));
7452
+ } catch {
7453
+ throw new Error(`operator context file ${file} is not valid JSON`);
7454
+ }
7455
+ if (!raw || typeof raw !== "object" || raw.schemaVersion !== 1 || !raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles)) {
7456
+ throw new Error(`operator context file ${file} has an invalid shape`);
7457
+ }
7458
+ const unknown = Object.keys(raw).filter(
7459
+ (key) => !["schemaVersion", "profiles"].includes(key)
7460
+ );
7461
+ if (unknown.length > 0) {
7462
+ throw new Error(
7463
+ `operator context file ${file} contains unsupported field(s): ${unknown.sort().join(", ")}`
7464
+ );
7465
+ }
7466
+ const profiles = emptyProfiles();
7467
+ for (const [name, value] of Object.entries(
7468
+ raw.profiles
7469
+ )) {
7470
+ assertOperatorName(name, "context");
7471
+ profiles[name] = validateProfile(value, `operator context "${name}"`);
7472
+ }
7473
+ return profiles;
7474
+ }
7475
+ function emptyProfiles() {
7476
+ return /* @__PURE__ */ Object.create(null);
7477
+ }
7478
+ function validateProfile(value, label) {
7479
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7480
+ throw new Error(`${label} has an invalid shape`);
7481
+ }
7482
+ const unknown = Object.keys(value).filter(
7483
+ (key) => !["platform", "app", "environment"].includes(key)
7484
+ );
7485
+ if (unknown.length > 0) {
7486
+ throw new Error(
7487
+ `${label} contains unsupported field(s): ${unknown.sort().join(", ")}; contexts store scope metadata only, never credentials`
7488
+ );
7489
+ }
7490
+ const candidate = value;
7491
+ if (typeof candidate.platform !== "string") {
7492
+ throw new Error(`${label} needs an absolute platform URL`);
7493
+ }
7494
+ const platform = platformAudience(candidate.platform);
7495
+ const app = clean2(candidate.app);
7496
+ const environment2 = clean2(candidate.environment);
7497
+ if (app) assertOperatorName(app, "app");
7498
+ if (environment2) assertOperatorName(environment2, "environment");
7499
+ return {
7500
+ platform,
7501
+ ...app ? { app } : {},
7502
+ ...environment2 ? { environment: environment2 } : {}
7503
+ };
7504
+ }
7505
+ function clean2(value) {
7506
+ const normalized = value?.trim();
7507
+ return normalized || void 0;
7508
+ }
7509
+
7510
+ // src/operator-context.ts
7511
+ var DEFAULT_PLATFORM2 = "https://odla.ai";
7512
+ async function resolveOperatorContext(parsed, options = {}) {
7513
+ const profile = resolveOperatorProfile(parsed);
7514
+ const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
7515
+ const configPath = (0, import_node_path12.resolve)(configArgument);
7516
+ const explicitConfig = parsed.options.config !== void 0;
7517
+ const hasConfig = (0, import_node_fs14.existsSync)(configPath);
7518
+ if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
7519
+ await loadProjectConfig(configArgument);
7520
+ }
7521
+ const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
7522
+ const platformFlag = clean3(stringOpt(parsed.options.platform));
7523
+ const platformEnvironment = clean3(import_node_process9.default.env.ODLA_PLATFORM_URL);
7524
+ const platformValue = platformAudience(
7525
+ platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
7526
+ );
7527
+ const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
7528
+ const appFlag = clean3(stringOpt(parsed.options.app));
7529
+ const appEnvironment = clean3(import_node_process9.default.env.ODLA_APP_ID);
7530
+ const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
7531
+ const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
7532
+ if (appValue) assertOperatorName(appValue, "app");
7533
+ if (options.requireApp && !appValue) {
7534
+ throw new Error(
7535
+ "app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
7536
+ );
7537
+ }
7538
+ const envFlag = clean3(stringOpt(parsed.options.env));
7539
+ const envEnvironment = clean3(import_node_process9.default.env.ODLA_ENV);
7540
+ const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
7541
+ const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
7542
+ if (environmentValue) {
7543
+ assertOperatorName(environmentValue, "environment");
7544
+ }
7545
+ const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
7546
+ const profileCredentials = operatorCredentialFiles(profile);
7547
+ 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;
7548
+ 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;
7549
+ const cfg = loaded ? {
7550
+ ...loaded,
7551
+ platformUrl: platformValue,
7552
+ app: appValue ? {
7553
+ id: appValue,
7554
+ name: appValue === loaded.app.id ? loaded.app.name : appValue
7555
+ } : loaded.app,
7556
+ local: { ...loaded.local, tokenFile }
7557
+ } : {
7558
+ configPath,
7559
+ rootDir,
7560
+ platformUrl: platformValue,
7561
+ dbEndpoint: platformValue,
7562
+ app: {
7563
+ id: appValue ?? "operator",
7564
+ name: appValue ?? "Operator"
7565
+ },
7566
+ envs: environmentValue ? [environmentValue] : [],
7567
+ services: [],
7568
+ local: {
7569
+ tokenFile,
7570
+ credentialsFile: (0, import_node_path12.join)(rootDir, ".odla", "credentials.local.json"),
7571
+ devVarsFile: (0, import_node_path12.join)(rootDir, ".dev.vars"),
7572
+ gitignore: true
7573
+ }
7574
+ };
7575
+ return {
7576
+ cfg,
7577
+ profile: {
7578
+ name: profile.name,
7579
+ source: profile.source,
7580
+ file: profile.file
7581
+ },
7582
+ config: {
7583
+ path: configPath,
7584
+ status: loaded ? "loaded" : "absent",
7585
+ explicit: explicitConfig
7586
+ },
7587
+ platform: { value: platformValue, source: platformSource },
7588
+ app: { value: appValue, source: appSource },
7589
+ environment: {
7590
+ value: environmentValue,
7591
+ source: environmentSource
7592
+ },
7593
+ credentials: {
7594
+ developerTokenFile: tokenFile,
7595
+ scopedTokenFile
7596
+ }
7597
+ };
7598
+ }
7599
+ function clean3(value) {
7600
+ const normalized = value?.trim();
7601
+ return normalized || void 0;
7602
+ }
7603
+
7604
+ // src/context-command.ts
7605
+ async function contextCommand(parsed, deps = {}) {
7606
+ assertArgs(
7607
+ parsed,
7608
+ [
7609
+ "config",
7610
+ "context",
7611
+ "platform",
7612
+ "app",
7613
+ "env",
7614
+ "token",
7615
+ "json",
7616
+ "yes"
7617
+ ],
7618
+ 3
7619
+ );
7620
+ const action = parsed.positionals[1];
7621
+ const out = deps.stdout ?? console;
7622
+ if (action === "list") {
7623
+ const profiles = listOperatorProfiles();
7624
+ if (parsed.options.json === true) {
7625
+ out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
7626
+ return;
7627
+ }
7628
+ if (profiles.length === 0) {
7629
+ out.log("No named operator contexts.");
7630
+ return;
7631
+ }
7632
+ out.log("name platform app environment");
7633
+ for (const profile of profiles) {
7634
+ out.log(
7635
+ `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
7636
+ );
7637
+ }
7638
+ return;
7639
+ }
7640
+ if (action === "save") {
7641
+ if (parsed.options.token !== void 0) {
7642
+ throw new Error(
7643
+ "context save does not accept --token; contexts store scope metadata only"
7644
+ );
7645
+ }
7646
+ const name = requireName(parsed);
7647
+ const context2 = await resolveOperatorContext(parsed, {
7648
+ allowMissingConfig: true
7649
+ });
7650
+ const profile = {
7651
+ platform: context2.platform.value,
7652
+ ...context2.app.value ? { app: context2.app.value } : {},
7653
+ ...context2.environment.value ? { environment: context2.environment.value } : {}
7654
+ };
7655
+ saveOperatorProfile(name, profile, context2.profile.file);
7656
+ if (parsed.options.json === true) {
7657
+ out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
7658
+ } else {
7659
+ out.log(`saved operator context "${name}" (${profile.platform})`);
7660
+ }
7661
+ return;
7662
+ }
7663
+ if (action === "remove") {
7664
+ const name = requireName(parsed);
7665
+ if (parsed.options.yes !== true) {
7666
+ throw new Error(`context remove "${name}" requires --yes`);
7667
+ }
7668
+ const removed = removeOperatorProfile(name);
7669
+ const result2 = {
7670
+ schemaVersion: 1,
7671
+ name,
7672
+ removed,
7673
+ credentialCachesRemoved: false,
7674
+ grantsRevoked: false
7675
+ };
7676
+ if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
7677
+ else {
7678
+ const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
7679
+ out.log(`${status}; credential caches were not deleted or revoked`);
7680
+ }
7681
+ return;
7682
+ }
7683
+ if (action !== "show") {
7684
+ throw new Error(
7685
+ `unknown context action "${action ?? ""}". Try show|list|save|remove.`
7686
+ );
7687
+ }
7688
+ const context = await resolveOperatorContext(parsed, {
7689
+ allowMissingConfig: true,
7690
+ defaultEnvironment: "prod"
7691
+ });
7692
+ const token = developerTokenStatus(context, parsed);
7693
+ const result = {
7694
+ schemaVersion: 1,
7695
+ profile: context.profile,
7696
+ config: context.config,
7697
+ platform: context.platform,
7698
+ app: context.app,
7699
+ environment: context.environment,
7700
+ authentication: {
7701
+ developerToken: token,
7702
+ scopedTokenCacheFile: context.credentials.scopedTokenFile,
7703
+ handshakeStarted: false
7704
+ }
7705
+ };
7706
+ if (parsed.options.json === true) {
7707
+ out.log(JSON.stringify(result, null, 2));
7708
+ return;
7709
+ }
7710
+ out.log(`config: ${context.config.status} (${context.config.path})`);
7711
+ out.log(
7712
+ `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
7713
+ );
7714
+ out.log(
7715
+ `platform: ${context.platform.value} (${context.platform.source})`
7716
+ );
7717
+ out.log(
7718
+ `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
7719
+ );
7720
+ out.log(
7721
+ `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
7722
+ );
7723
+ out.log(
7724
+ `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
7725
+ );
7726
+ out.log("handshake: not started");
7727
+ }
7728
+ function requireName(parsed) {
7729
+ const name = parsed.positionals[2]?.trim();
7730
+ if (!name) {
7731
+ throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
7732
+ }
7733
+ return name;
7734
+ }
7735
+
7339
7736
  // src/discuss-actions.ts
7340
7737
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
7341
7738
  async function request(ctx, method, path, body) {
@@ -7387,8 +7784,9 @@ async function discussGroups(ctx) {
7387
7784
  }
7388
7785
  async function discussList(ctx, parsed) {
7389
7786
  const query = new URLSearchParams();
7787
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7788
+ if (app) query.set("app", app);
7390
7789
  for (const [flag, param] of [
7391
- ["app", "app"],
7392
7790
  ["q", "q"],
7393
7791
  ["state", "state"],
7394
7792
  ["limit", "limit"],
@@ -7464,7 +7862,7 @@ function renderRead(ctx, topic, posts) {
7464
7862
  }
7465
7863
  }
7466
7864
  async function discussPost(ctx, parsed) {
7467
- const appId = stringOpt(parsed.options.app);
7865
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
7468
7866
  if (!appId) throw new Error("discuss post needs --app <appId>");
7469
7867
  const subject = stringOpt(parsed.options.subject);
7470
7868
  if (!subject) throw new Error('discuss post needs --subject "\u2026"');
@@ -7496,7 +7894,7 @@ async function discussResolve(ctx, id, resolved, parsed) {
7496
7894
  }
7497
7895
  async function discussWho(ctx, parsed) {
7498
7896
  const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
7499
- const app = stringOpt(parsed.options.app);
7897
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7500
7898
  if (app) query.set("app", app);
7501
7899
  const kinds = stringOpt(parsed.options.kinds);
7502
7900
  if (kinds) query.set("kinds", kinds);
@@ -7600,14 +7998,14 @@ function jsonl(ctx, parsed, value) {
7600
7998
  }
7601
7999
  async function discussWatch(ctx, topicId, parsed) {
7602
8000
  if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
7603
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
8001
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
7604
8002
  const now = ctx.now ?? Date.now;
7605
8003
  const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7606
8004
  const timeoutSeconds = numberOpt2(parsed, "timeout");
7607
8005
  const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
7608
8006
  const by = stringOpt(parsed.options.by);
7609
8007
  const self = stringOpt(parsed.options.self);
7610
- const app = stringOpt(parsed.options.app);
8008
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7611
8009
  let cursor = stringOpt(parsed.options.cursor);
7612
8010
  let firstSuccess = true;
7613
8011
  let consecutiveFailures = 0;
@@ -7752,14 +8150,19 @@ var ALLOWED = [
7752
8150
  "timeout",
7753
8151
  "cursor",
7754
8152
  "jsonl",
7755
- "mutation-id"
8153
+ "mutation-id",
8154
+ "platform",
8155
+ "context"
7756
8156
  ];
7757
8157
  function requireId(id, action) {
7758
8158
  if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
7759
8159
  return id;
7760
8160
  }
7761
8161
  async function buildContext(parsed, deps) {
7762
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8162
+ const context = await resolveOperatorContext(parsed, {
8163
+ allowMissingConfig: true
8164
+ });
8165
+ const { cfg } = context;
7763
8166
  const doFetch = deps.fetch ?? fetch;
7764
8167
  const out = deps.stdout ?? console;
7765
8168
  const token = await getDeveloperToken(
@@ -7773,7 +8176,16 @@ async function buildContext(parsed, deps) {
7773
8176
  doFetch,
7774
8177
  out
7775
8178
  );
7776
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
8179
+ return {
8180
+ platformUrl: cfg.platformUrl,
8181
+ token,
8182
+ doFetch,
8183
+ out,
8184
+ json: parsed.options.json === true,
8185
+ // Preserve Discussions' existing cross-project default for config-backed
8186
+ // use while allowing remote agents to scope via ODLA_APP_ID or a profile.
8187
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
8188
+ };
7777
8189
  }
7778
8190
  async function discussCommand(parsed, deps = {}) {
7779
8191
  assertArgs(parsed, ALLOWED, 3);
@@ -7885,7 +8297,7 @@ async function pmList(ctx, entity, parsed) {
7885
8297
  assignee: "assigneeId",
7886
8298
  decision: "decisionId"
7887
8299
  };
7888
- const app = stringOpt(parsed.options.app);
8300
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
7889
8301
  if (app) q.set("app", app);
7890
8302
  for (const [flag, param] of Object.entries(filters)) {
7891
8303
  const v = stringOpt(parsed.options[flag]);
@@ -7904,7 +8316,7 @@ async function pmList(ctx, entity, parsed) {
7904
8316
  });
7905
8317
  }
7906
8318
  async function pmAdd(ctx, entity, parsed) {
7907
- const appId = stringOpt(parsed.options.app);
8319
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
7908
8320
  if (!appId) throw new Error("pm add needs --app <appId>");
7909
8321
  const input = collectEntityFields(entity, parsed, false);
7910
8322
  if (!input.title) throw new Error("pm add needs --title <title>");
@@ -7955,7 +8367,7 @@ async function allRecords(ctx, entity, appId) {
7955
8367
  }
7956
8368
  }
7957
8369
  async function pmHandoff(ctx, parsed) {
7958
- const appId = stringOpt(parsed.options.app);
8370
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
7959
8371
  if (!appId) throw new Error("pm handoff needs --app <appId>");
7960
8372
  const [goals, tasks, bugs] = await Promise.all([
7961
8373
  allRecords(ctx, "goal", appId),
@@ -8050,14 +8462,19 @@ var ALLOWED2 = [
8050
8462
  "limit",
8051
8463
  "offset",
8052
8464
  "q",
8053
- "mutation-id"
8465
+ "mutation-id",
8466
+ "platform",
8467
+ "context"
8054
8468
  ];
8055
8469
  function requireId2(id, action) {
8056
8470
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
8057
8471
  return id;
8058
8472
  }
8059
8473
  async function buildContext2(parsed, deps) {
8060
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8474
+ const context = await resolveOperatorContext(parsed, {
8475
+ allowMissingConfig: true
8476
+ });
8477
+ const { cfg } = context;
8061
8478
  const doFetch = deps.fetch ?? fetch;
8062
8479
  const out = deps.stdout ?? console;
8063
8480
  const token = await getDeveloperToken(
@@ -8066,7 +8483,16 @@ async function buildContext2(parsed, deps) {
8066
8483
  doFetch,
8067
8484
  out
8068
8485
  );
8069
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
8486
+ return {
8487
+ platformUrl: cfg.platformUrl,
8488
+ token,
8489
+ doFetch,
8490
+ out,
8491
+ json: parsed.options.json === true,
8492
+ // Preserve PM's existing cross-project default when a config is present;
8493
+ // only an explicit remote-agent environment or profile selects an app.
8494
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
8495
+ };
8070
8496
  }
8071
8497
  async function pmCommand(parsed, deps = {}) {
8072
8498
  const word = parsed.positionals[1] ?? "";
@@ -8115,6 +8541,20 @@ function statusVerdict(reads) {
8115
8541
  sourceHttp(reasons, "canary", reads.canary);
8116
8542
  sourceHttp(reasons, "collector", reads.collector);
8117
8543
  sourceHttp(reasons, "provider", reads.provider);
8544
+ if (reads.providerCapacity) {
8545
+ sourceHttp(
8546
+ reasons,
8547
+ "provider",
8548
+ reads.providerCapacity,
8549
+ "provider_capacity_"
8550
+ );
8551
+ }
8552
+ sourceHttp(
8553
+ reasons,
8554
+ "provider",
8555
+ reads.providerReconciliation,
8556
+ "provider_reconciliation_"
8557
+ );
8118
8558
  sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
8119
8559
  const liveStatus = String(reads.liveSync.body.status ?? "");
8120
8560
  if (liveStatus === "partial") {
@@ -8187,6 +8627,17 @@ function statusVerdict(reads) {
8187
8627
  severity: "degraded"
8188
8628
  });
8189
8629
  }
8630
+ const reconciliationStatus = String(
8631
+ reads.providerReconciliation.body.status ?? ""
8632
+ );
8633
+ if (reads.providerReconciliation.httpStatus >= 200 && reads.providerReconciliation.httpStatus < 300 && reconciliationStatus && reconciliationStatus !== "within_budget") {
8634
+ const reconciliationReason = reads.providerReconciliation.body.reason;
8635
+ reasons.push({
8636
+ source: "provider",
8637
+ code: reconciliationStatus === "outside_budget" ? typeof reconciliationReason === "string" && reconciliationReason ? reconciliationReason : "request_coverage_outside_budget" : `request_reconciliation_${reconciliationStatus}`,
8638
+ severity: "degraded"
8639
+ });
8640
+ }
8190
8641
  const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
8191
8642
  if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
8192
8643
  reasons.push({
@@ -8257,8 +8708,19 @@ function printO11yStatus(status, out) {
8257
8708
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
8258
8709
  );
8259
8710
  const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
8711
+ const providerCapacity = record6(status.provider.body.capacity) ? status.provider.body.capacity : {};
8712
+ const workerMemory = record6(providerCapacity.memory) ? providerCapacity.memory : {};
8713
+ out.log(
8714
+ `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`
8715
+ );
8716
+ for (const line of providerCapacityLines(status.providerCapacity)) {
8717
+ out.log(line);
8718
+ }
8719
+ const coverage = record6(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
8720
+ const coverageCounts = record6(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
8721
+ const coverageBudget = record6(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
8260
8722
  out.log(
8261
- `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
8723
+ `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`
8262
8724
  );
8263
8725
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
8264
8726
  const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
@@ -8269,6 +8731,27 @@ function printO11yStatus(status, out) {
8269
8731
  `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8270
8732
  );
8271
8733
  }
8734
+ function providerCapacityLines(read3) {
8735
+ const resources = record6(read3.body.resources) ? read3.body.resources : {};
8736
+ const durableObjects = record6(resources.durableObjects) ? resources.durableObjects : {};
8737
+ const periodic = record6(durableObjects.periodic) ? durableObjects.periodic : {};
8738
+ const storage = record6(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
8739
+ const d1 = record6(resources.d1) ? resources.d1 : {};
8740
+ const d1Activity = record6(d1.activity) ? d1.activity : {};
8741
+ const d1Storage = record6(d1.storage) ? d1.storage : {};
8742
+ const d1Latency = record6(d1Activity.latency) ? d1Activity.latency : {};
8743
+ const r2 = record6(resources.r2) ? resources.r2 : {};
8744
+ const r2Operations = record6(r2.operations) ? r2.operations : {};
8745
+ const r2Storage = record6(r2.storage) ? r2.storage : {};
8746
+ const status = String(
8747
+ read3.body.status ?? read3.body.error ?? "unavailable"
8748
+ );
8749
+ return [
8750
+ `cloudflare-capacity ${read3.httpStatus} ${status} ${optionalBytes(storage.storedBytes)} SQLite DO storage ${optionalCount(periodic.activeWebsocketConnectionsMax, "active socket max")} ${optionalAgeSeconds(storage.ageSeconds)} storage evidence age`,
8751
+ `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`,
8752
+ `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`
8753
+ ];
8754
+ }
8272
8755
  function liveSyncLine(read3) {
8273
8756
  const performance = record6(read3.body.performance) ? read3.body.performance : {};
8274
8757
  const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
@@ -8287,12 +8770,42 @@ function optionalAge(value) {
8287
8770
  if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
8288
8771
  return `${Math.max(0, Math.round(value / 1e3))}s`;
8289
8772
  }
8773
+ function optionalPercent(value) {
8774
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value * 100)}%` : "\u2014";
8775
+ }
8776
+ function optionalBytes(value) {
8777
+ if (typeof value !== "number" || !Number.isFinite(value)) return "\u2014";
8778
+ if (value >= 1024 * 1024 * 1024) {
8779
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
8780
+ }
8781
+ if (value >= 1024 * 1024) {
8782
+ return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
8783
+ }
8784
+ if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
8785
+ return `${Math.round(value)} B`;
8786
+ }
8787
+ function optionalCount(value, unit) {
8788
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ${unit}` : `\u2014 ${unit}`;
8789
+ }
8790
+ function optionalAgeSeconds(value) {
8791
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.max(0, Math.round(value))}s` : "unknown";
8792
+ }
8290
8793
 
8291
8794
  // src/o11y-command.ts
8292
8795
  async function o11yCommand(parsed, deps = {}) {
8293
8796
  assertArgs(
8294
8797
  parsed,
8295
- ["config", "token", "email", "json", "app", "env", "minutes"],
8798
+ [
8799
+ "config",
8800
+ "context",
8801
+ "platform",
8802
+ "token",
8803
+ "email",
8804
+ "json",
8805
+ "app",
8806
+ "env",
8807
+ "minutes"
8808
+ ],
8296
8809
  2
8297
8810
  );
8298
8811
  const action = parsed.positionals[1];
@@ -8304,9 +8817,12 @@ async function o11yCommand(parsed, deps = {}) {
8304
8817
  const minutes = statusMinutes(
8305
8818
  numberOpt(parsed.options.minutes, "--minutes") ?? 60
8306
8819
  );
8307
- const cfg = await loadProjectConfig(
8308
- stringOpt(parsed.options.config) ?? "odla.config.mjs"
8309
- );
8820
+ const context = await resolveOperatorContext(parsed, {
8821
+ allowMissingConfig: true,
8822
+ defaultEnvironment: "prod",
8823
+ requireApp: true
8824
+ });
8825
+ const cfg = context.cfg;
8310
8826
  const doFetch = deps.fetch ?? fetch;
8311
8827
  const out = deps.stdout ?? console;
8312
8828
  const token = await getDeveloperToken(
@@ -8320,8 +8836,8 @@ async function o11yCommand(parsed, deps = {}) {
8320
8836
  doFetch,
8321
8837
  out
8322
8838
  );
8323
- const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
8324
- const env = stringOpt(parsed.options.env)?.trim() || "prod";
8839
+ const appId = context.app.value;
8840
+ const env = context.environment.value;
8325
8841
  const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
8326
8842
  const query = new URLSearchParams({ env, minutes: String(minutes) });
8327
8843
  const headers = { authorization: `Bearer ${token}` };
@@ -8338,6 +8854,8 @@ async function o11yCommand(parsed, deps = {}) {
8338
8854
  canary,
8339
8855
  collector,
8340
8856
  provider,
8857
+ providerCapacity,
8858
+ providerReconciliation,
8341
8859
  providerHistory
8342
8860
  ] = await Promise.all([
8343
8861
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
@@ -8354,6 +8872,19 @@ async function o11yCommand(parsed, deps = {}) {
8354
8872
  ),
8355
8873
  read2(`${base}/self-health?${query}`, headers, doFetch),
8356
8874
  read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
8875
+ appId === "odla-db" ? read2(`${base}/provider/cloudflare/capacity`, headers, doFetch) : Promise.resolve({
8876
+ httpStatus: 204,
8877
+ body: {
8878
+ status: "not_applicable",
8879
+ scope: "cloudflare_account",
8880
+ reason: "account-scoped Durable Object capacity is exposed only through odla-db"
8881
+ }
8882
+ }),
8883
+ read2(
8884
+ `${base}/provider/cloudflare/reconciliation?${query}`,
8885
+ headers,
8886
+ doFetch
8887
+ ),
8357
8888
  read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
8358
8889
  ]);
8359
8890
  const verdict = statusVerdict({
@@ -8362,10 +8893,12 @@ async function o11yCommand(parsed, deps = {}) {
8362
8893
  canary,
8363
8894
  collector,
8364
8895
  provider,
8896
+ providerCapacity,
8897
+ providerReconciliation,
8365
8898
  providerHistory
8366
8899
  });
8367
8900
  const status = {
8368
- schemaVersion: 6,
8901
+ schemaVersion: 9,
8369
8902
  scope: { appId, env, minutes },
8370
8903
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
8371
8904
  verdict,
@@ -8375,6 +8908,8 @@ async function o11yCommand(parsed, deps = {}) {
8375
8908
  canary,
8376
8909
  collector,
8377
8910
  provider,
8911
+ providerCapacity,
8912
+ providerReconciliation,
8378
8913
  providerHistory
8379
8914
  };
8380
8915
  if (parsed.options.json === true) {
@@ -8407,7 +8942,7 @@ async function read2(url, headers, doFetch) {
8407
8942
  // src/provision.ts
8408
8943
  var import_apps5 = require("@odla-ai/apps");
8409
8944
  var import_ai3 = require("@odla-ai/ai");
8410
- var import_node_process7 = __toESM(require("process"), 1);
8945
+ var import_node_process10 = __toESM(require("process"), 1);
8411
8946
 
8412
8947
  // src/integration-provision.ts
8413
8948
  var import_db3 = require("@odla-ai/db");
@@ -8766,7 +9301,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
8766
9301
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
8767
9302
  }
8768
9303
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
8769
- const key = import_node_process7.default.env[cfg.ai.keyEnv];
9304
+ const key = import_node_process10.default.env[cfg.ai.keyEnv];
8770
9305
  if (key) {
8771
9306
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
8772
9307
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -8807,8 +9342,8 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
8807
9342
  }
8808
9343
 
8809
9344
  // src/record.ts
8810
- var import_node_fs13 = require("fs");
8811
- var import_node_process8 = __toESM(require("process"), 1);
9345
+ var import_node_fs15 = require("fs");
9346
+ var import_node_process11 = __toESM(require("process"), 1);
8812
9347
 
8813
9348
  // src/surface.ts
8814
9349
  var PM_ACTIONS = {
@@ -8856,6 +9391,7 @@ var COMMAND_SURFACE = {
8856
9391
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
8857
9392
  capabilities: {},
8858
9393
  code: { connect: {} },
9394
+ context: { show: {}, list: {}, save: {}, remove: {} },
8859
9395
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
8860
9396
  discuss: {
8861
9397
  groups: {},
@@ -8950,7 +9486,7 @@ function invocationPath(words2) {
8950
9486
 
8951
9487
  // src/record.ts
8952
9488
  function recordInvocation(parsed) {
8953
- const file = import_node_process8.default.env.ODLA_CLI_RECORD;
9489
+ const file = import_node_process11.default.env.ODLA_CLI_RECORD;
8954
9490
  if (!file) return;
8955
9491
  try {
8956
9492
  const entry = {
@@ -8958,19 +9494,14 @@ function recordInvocation(parsed) {
8958
9494
  options: Object.keys(parsed.options).sort()
8959
9495
  };
8960
9496
  if (!entry.path.length) return;
8961
- (0, import_node_fs13.appendFileSync)(file, `${JSON.stringify(entry)}
9497
+ (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
8962
9498
  `);
8963
9499
  } catch {
8964
9500
  }
8965
9501
  }
8966
9502
 
8967
- // src/runbook-command.ts
8968
- var import_node_fs18 = require("fs");
8969
- var import_node_path14 = require("path");
8970
- var import_node_process10 = __toESM(require("process"), 1);
8971
-
8972
9503
  // src/runbook-actions.ts
8973
- var import_node_fs14 = require("fs");
9504
+ var import_node_fs16 = require("fs");
8974
9505
 
8975
9506
  // src/runbook-requires.ts
8976
9507
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -9055,7 +9586,7 @@ async function bySlug(ctx, slug) {
9055
9586
  function readBody(file, inline) {
9056
9587
  if (inline !== void 0) return inline;
9057
9588
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
9058
- return (0, import_node_fs14.readFileSync)(file === "-" ? 0 : file, "utf8");
9589
+ return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
9059
9590
  }
9060
9591
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
9061
9592
  async function runbookList(ctx, all, query) {
@@ -9142,8 +9673,8 @@ async function runbookRemove(ctx, slug) {
9142
9673
  }
9143
9674
 
9144
9675
  // src/runbook-import.ts
9145
- var import_node_fs15 = require("fs");
9146
- var import_node_path11 = require("path");
9676
+ var import_node_fs17 = require("fs");
9677
+ var import_node_path13 = require("path");
9147
9678
  function parseRunbook(text, slug) {
9148
9679
  let rest = text;
9149
9680
  const meta = {};
@@ -9168,12 +9699,12 @@ function parseRunbook(text, slug) {
9168
9699
  };
9169
9700
  }
9170
9701
  function readRunbookDir(dir) {
9171
- if (!(0, import_node_fs15.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
9172
- const files = (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
9702
+ if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
9703
+ const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
9173
9704
  if (!files.length) throw new Error(`no .md files in ${dir}`);
9174
9705
  return files.map((file) => {
9175
- const slug = (0, import_node_path11.basename)(file, ".md");
9176
- const parsed = parseRunbook((0, import_node_fs15.readFileSync)((0, import_node_path11.join)(dir, file), "utf8"), slug);
9706
+ const slug = (0, import_node_path13.basename)(file, ".md");
9707
+ const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path13.join)(dir, file), "utf8"), slug);
9177
9708
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
9178
9709
  });
9179
9710
  }
@@ -9245,8 +9776,8 @@ async function upsert(ctx, r, visibility) {
9245
9776
 
9246
9777
  // src/runbook-impact.ts
9247
9778
  var import_node_child_process7 = require("child_process");
9248
- var import_node_fs16 = require("fs");
9249
- var import_node_path12 = require("path");
9779
+ var import_node_fs18 = require("fs");
9780
+ var import_node_path14 = require("path");
9250
9781
 
9251
9782
  // src/runbook-impact-scan.ts
9252
9783
  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$]*)/;
@@ -9415,10 +9946,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
9415
9946
  }
9416
9947
  function manifestLabeller(root) {
9417
9948
  return (workspace) => {
9418
- const manifest = (0, import_node_path12.join)(root, workspace, "package.json");
9419
- if (!(0, import_node_fs16.existsSync)(manifest)) return void 0;
9949
+ const manifest = (0, import_node_path14.join)(root, workspace, "package.json");
9950
+ if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
9420
9951
  try {
9421
- const name = JSON.parse((0, import_node_fs16.readFileSync)(manifest, "utf8")).name;
9952
+ const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
9422
9953
  return typeof name === "string" ? name : void 0;
9423
9954
  } catch {
9424
9955
  return void 0;
@@ -9485,7 +10016,7 @@ function report3(ctx, impacts) {
9485
10016
  async function runbookImpact(ctx, options, deps = {}) {
9486
10017
  const cwd = deps.cwd ?? process.cwd();
9487
10018
  const runGit = deps.runGit ?? gitRunner(cwd);
9488
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs16.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
10019
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path14.join)(cwd, path), "utf8"));
9489
10020
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
9490
10021
  if (!surfaces.length) {
9491
10022
  return ctx.out.log(
@@ -9618,12 +10149,12 @@ async function runbookComment(ctx, slug, body) {
9618
10149
 
9619
10150
  // src/runbook-editor.ts
9620
10151
  var import_node_child_process8 = require("child_process");
9621
- var import_node_fs17 = require("fs");
9622
- var import_node_os4 = require("os");
9623
- var import_node_path13 = require("path");
9624
- var import_node_process9 = __toESM(require("process"), 1);
10152
+ var import_node_fs19 = require("fs");
10153
+ var import_node_os5 = require("os");
10154
+ var import_node_path15 = require("path");
10155
+ var import_node_process12 = __toESM(require("process"), 1);
9625
10156
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
9626
- function resolveEditor(env = import_node_process9.default.env) {
10157
+ function resolveEditor(env = import_node_process12.default.env) {
9627
10158
  for (const name of EDITOR_ENV) {
9628
10159
  const value = env[name];
9629
10160
  if (value && value.trim()) return value.trim();
@@ -9637,8 +10168,8 @@ function defaultRun(command, path) {
9637
10168
  return result.status ?? 0;
9638
10169
  }
9639
10170
  function editText(initial, slug, deps = {}) {
9640
- const env = deps.env ?? import_node_process9.default.env;
9641
- const interactive = deps.interactive ?? (() => Boolean(import_node_process9.default.stdin.isTTY));
10171
+ const env = deps.env ?? import_node_process12.default.env;
10172
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
9642
10173
  const editor = resolveEditor(env);
9643
10174
  if (!editor)
9644
10175
  throw new Error(
@@ -9646,16 +10177,16 @@ function editText(initial, slug, deps = {}) {
9646
10177
  );
9647
10178
  if (!interactive())
9648
10179
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
9649
- const dir = (0, import_node_fs17.mkdtempSync)((0, import_node_path13.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
9650
- const file = (0, import_node_path13.join)(dir, `${slug}.md`);
10180
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10181
+ const file = (0, import_node_path15.join)(dir, `${slug}.md`);
9651
10182
  try {
9652
- (0, import_node_fs17.writeFileSync)(file, initial, { mode: 384 });
10183
+ (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
9653
10184
  const code = defaultRunOrInjected(deps)(editor, file);
9654
10185
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
9655
- const edited = (0, import_node_fs17.readFileSync)(file, "utf8");
10186
+ const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
9656
10187
  return edited === initial ? null : edited;
9657
10188
  } finally {
9658
- (0, import_node_fs17.rmSync)(dir, { recursive: true, force: true });
10189
+ (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
9659
10190
  }
9660
10191
  }
9661
10192
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -9693,10 +10224,16 @@ function credentialKind(identity) {
9693
10224
  return identity.scopes.length ? "device token (scoped)" : "device token or session";
9694
10225
  }
9695
10226
  async function whoamiCommand(parsed, deps = {}) {
9696
- assertArgs(parsed, ["config", "token", "email", "json"], 1);
10227
+ assertArgs(
10228
+ parsed,
10229
+ ["config", "context", "platform", "token", "email", "json"],
10230
+ 1
10231
+ );
9697
10232
  const out = deps.stdout ?? console;
9698
10233
  const doFetch = deps.fetch ?? fetch;
9699
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10234
+ const { cfg } = await resolveOperatorContext(parsed, {
10235
+ allowMissingConfig: true
10236
+ });
9700
10237
  const token = await getDeveloperToken(
9701
10238
  cfg,
9702
10239
  {
@@ -9745,34 +10282,24 @@ var ALLOWED3 = [
9745
10282
  "dry-run",
9746
10283
  "limit",
9747
10284
  "base",
9748
- "requires"
10285
+ "requires",
10286
+ "platform",
10287
+ "context"
9749
10288
  ];
9750
10289
  function requireSlug(slug, action) {
9751
10290
  if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
9752
10291
  return slug;
9753
10292
  }
9754
10293
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
9755
- var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
9756
- async function loadOrDefaultConfig(configPath, action, appId) {
9757
- const projectScoped = appId !== void 0;
9758
- if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs18.existsSync)((0, import_node_path14.resolve)(configPath))) {
9759
- const rootDir = import_node_process10.default.cwd();
9760
- return {
9761
- configPath,
9762
- platformUrl: import_node_process10.default.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
9763
- rootDir,
9764
- app: { id: "", name: "" },
9765
- local: { tokenFile: (0, import_node_path14.join)(rootDir, ".odla/dev-token.json") }
9766
- };
9767
- }
9768
- return loadProjectConfig(configPath);
9769
- }
9770
10294
  async function buildContext3(parsed, deps, action) {
9771
- const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
9772
- const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
10295
+ const appIdOption = stringOpt(parsed.options.app);
10296
+ const context = await resolveOperatorContext(parsed, {
10297
+ allowMissingConfig: true
10298
+ });
10299
+ const { cfg } = context;
9773
10300
  const doFetch = deps.fetch ?? fetch;
9774
10301
  const out = deps.stdout ?? console;
9775
- const appId = stringOpt(parsed.options.app) ?? PLATFORM_SCOPE;
10302
+ const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
9776
10303
  const dryRun = parsed.options["dry-run"] === true;
9777
10304
  if (action === "import" && dryRun) {
9778
10305
  return {
@@ -9793,8 +10320,10 @@ async function buildContext3(parsed, deps, action) {
9793
10320
  fetch: doFetch,
9794
10321
  stdout: out,
9795
10322
  openApprovalUrl: deps.openUrl,
9796
- // Anchor the grant cache to this project, not the shell's cwd.
9797
- rootDir: cfg.rootDir
10323
+ // A project, named context, or the global operator context owns the
10324
+ // exact-scope cache; it never follows an arbitrary shell directory.
10325
+ rootDir: cfg.rootDir,
10326
+ tokenFile: context.credentials.scopedTokenFile
9798
10327
  }) : await getDeveloperToken(
9799
10328
  cfg,
9800
10329
  {
@@ -10069,7 +10598,7 @@ function hostedSeverity(value, flag) {
10069
10598
  var import_security2 = require("@odla-ai/security");
10070
10599
 
10071
10600
  // src/security.ts
10072
- var import_node_path15 = require("path");
10601
+ var import_node_path16 = require("path");
10073
10602
  var import_security = require("@odla-ai/security");
10074
10603
  var import_node3 = require("@odla-ai/security/node");
10075
10604
  async function runHostedSecurity(options) {
@@ -10081,9 +10610,9 @@ async function runHostedSecurity(options) {
10081
10610
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
10082
10611
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
10083
10612
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
10084
- const target = (0, import_node_path15.resolve)(options.target ?? cfg?.rootDir ?? ".");
10085
- const output = (0, import_node_path15.resolve)(options.out ?? (0, import_node_path15.resolve)(target, ".odla/security/hosted"));
10086
- const outputRelative = (0, import_node_path15.relative)(target, output).split(import_node_path15.sep).join("/");
10613
+ const target = (0, import_node_path16.resolve)(options.target ?? cfg?.rootDir ?? ".");
10614
+ const output = (0, import_node_path16.resolve)(options.out ?? (0, import_node_path16.resolve)(target, ".odla/security/hosted"));
10615
+ const outputRelative = (0, import_node_path16.relative)(target, output).split(import_node_path16.sep).join("/");
10087
10616
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
10088
10617
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
10089
10618
  const tokenRequest = {
@@ -10095,7 +10624,7 @@ async function runHostedSecurity(options) {
10095
10624
  };
10096
10625
  const token = await injectedToken(options, tokenRequest);
10097
10626
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
10098
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path15.isAbsolute)(outputRelative) ? [outputRelative] : []
10627
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path16.isAbsolute)(outputRelative) ? [outputRelative] : []
10099
10628
  });
10100
10629
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
10101
10630
  platform,
@@ -10113,7 +10642,7 @@ async function runHostedSecurity(options) {
10113
10642
  });
10114
10643
  const harness = (0, import_security.createSecurityHarness)({
10115
10644
  profile,
10116
- store: new import_node3.FileRunStore((0, import_node_path15.resolve)(output, "state")),
10645
+ store: new import_node3.FileRunStore((0, import_node_path16.resolve)(output, "state")),
10117
10646
  discoveryReasoner: hosted.discoveryReasoner,
10118
10647
  validationReasoner: hosted.validationReasoner,
10119
10648
  policy: {
@@ -10137,7 +10666,7 @@ async function runHostedSecurity(options) {
10137
10666
  function selectEnv(requested, declared, configPath, rootDir) {
10138
10667
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
10139
10668
  if (!env || !declared.includes(env)) {
10140
- const shown = (0, import_node_path15.relative)(rootDir, configPath) || configPath;
10669
+ const shown = (0, import_node_path16.relative)(rootDir, configPath) || configPath;
10141
10670
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
10142
10671
  }
10143
10672
  return env;
@@ -10166,7 +10695,7 @@ function printSummary(out, appId, env, run, report4, output) {
10166
10695
  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}`);
10167
10696
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
10168
10697
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
10169
- out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
10698
+ out.log(` report: ${(0, import_node_path16.resolve)(output, "REPORT.md")}`);
10170
10699
  }
10171
10700
  function formatBudget(usage) {
10172
10701
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -10635,6 +11164,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10635
11164
  await whoamiCommand(parsed, runtime);
10636
11165
  return;
10637
11166
  }
11167
+ if (command === "context") {
11168
+ await contextCommand(parsed, runtime);
11169
+ return;
11170
+ }
10638
11171
  if (command === "runbook") {
10639
11172
  await runbookCommand(parsed, runtime);
10640
11173
  return;