@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.
@@ -138,7 +138,7 @@ import process2 from "process";
138
138
  async function openUrl(url, options = {}) {
139
139
  const command = openerFor(options.platform ?? process2.platform);
140
140
  const doSpawn = options.spawnImpl ?? spawn;
141
- await new Promise((resolve11, reject) => {
141
+ await new Promise((resolve12, reject) => {
142
142
  const child = doSpawn(command.cmd, [...command.args, url], {
143
143
  stdio: "ignore",
144
144
  detached: true
@@ -146,7 +146,7 @@ async function openUrl(url, options = {}) {
146
146
  child.once("error", reject);
147
147
  child.once("spawn", () => {
148
148
  child.unref();
149
- resolve11();
149
+ resolve12();
150
150
  });
151
151
  });
152
152
  }
@@ -1414,8 +1414,8 @@ function credential(value) {
1414
1414
  // src/calendar-poll.ts
1415
1415
  async function waitForCalendarPoll(milliseconds, signal) {
1416
1416
  if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
1417
- await new Promise((resolve11, reject) => {
1418
- const timer = setTimeout(resolve11, milliseconds);
1417
+ await new Promise((resolve12, reject) => {
1418
+ const timer = setTimeout(resolve12, milliseconds);
1419
1419
  signal?.addEventListener("abort", () => {
1420
1420
  clearTimeout(timer);
1421
1421
  reject(signal.reason ?? new Error("calendar connection aborted"));
@@ -1616,7 +1616,8 @@ var CAPABILITIES = {
1616
1616
  "compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
1617
1617
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
1618
1618
  "validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
1619
- "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",
1619
+ "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",
1620
+ "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",
1620
1621
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
1621
1622
  "run app-attributed hosted security discovery and independent validation without provider keys",
1622
1623
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -2822,8 +2823,8 @@ function hostedPollTimeout(value = 10 * 6e4) {
2822
2823
  }
2823
2824
  async function waitForHostedPoll(milliseconds, signal) {
2824
2825
  if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
2825
- await new Promise((resolve11, reject) => {
2826
- const timer = setTimeout(resolve11, milliseconds);
2826
+ await new Promise((resolve12, reject) => {
2827
+ const timer = setTimeout(resolve12, milliseconds);
2827
2828
  signal?.addEventListener("abort", () => {
2828
2829
  clearTimeout(timer);
2829
2830
  reject(signal.reason ?? new DOMException("aborted", "AbortError"));
@@ -4843,21 +4844,21 @@ async function verifyCodeCandidate(input) {
4843
4844
  const recipes = [];
4844
4845
  const logs = [];
4845
4846
  for (const recipe2 of policy.recipes) {
4846
- const clean = await stageWorkspace(staged.workspaceDir, limits);
4847
+ const clean4 = await stageWorkspace(staged.workspaceDir, limits);
4847
4848
  try {
4848
- if (await digestStagedWorkspace(clean.workspaceDir, limits) !== sourceDigest) {
4849
+ if (await digestStagedWorkspace(clean4.workspaceDir, limits) !== sourceDigest) {
4849
4850
  throw new TypeError("clean verifier source changed before execution");
4850
4851
  }
4851
4852
  const result = checkedResult(await input.recipeExecutor.run({
4852
- workspaceDir: clean.workspaceDir,
4853
+ workspaceDir: clean4.workspaceDir,
4853
4854
  recipe: recipe2,
4854
4855
  signal: input.signal
4855
4856
  }), recipe2.maxOutputBytes);
4856
- const artifacts = await inspectArtifacts(clean.workspaceDir, recipe2);
4857
+ const artifacts = await inspectArtifacts(clean4.workspaceDir, recipe2);
4857
4858
  recipes.push(recipeReceipt(recipe2, result, artifacts));
4858
4859
  logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
4859
4860
  } finally {
4860
- await clean.cleanup();
4861
+ await clean4.cleanup();
4861
4862
  }
4862
4863
  }
4863
4864
  const fields = {
@@ -5998,8 +5999,12 @@ Usage:
5998
5999
  odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
5999
6000
  odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6000
6001
  odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6001
- odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
6002
- odla-ai whoami [--json]
6002
+ odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
6003
+ odla-ai context list [--json]
6004
+ odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
6005
+ odla-ai context remove <name> --yes [--json]
6006
+ odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
6007
+ odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
6003
6008
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6004
6009
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6005
6010
  odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
@@ -6064,6 +6069,11 @@ Commands:
6064
6069
  whoami Report who this terminal is authenticated as, and whether it holds
6065
6070
  platform admin. A handshake-minted device token is never admin,
6066
6071
  however the human who approved it is configured.
6072
+ context Explain selected config, platform, app, environment, and
6073
+ developer-token provenance without printing credentials or
6074
+ starting a device handshake. Operator work can run outside a
6075
+ project checkout with explicit flags or ODLA_* environment
6076
+ variables.
6067
6077
  setup Install offline odla runbooks for common coding-agent harnesses.
6068
6078
  init Create a generic odla.config.mjs plus starter schema/rules files.
6069
6079
  doctor Validate and summarize the project config without network calls.
@@ -6135,6 +6145,15 @@ Safety:
6135
6145
  Provision caches the approved developer token and service credentials under
6136
6146
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
6137
6147
  preflights Wrangler before any shown-once issuance or destructive rotation.
6148
+ Projectless PM, Discussions, o11y, runbook, and identity commands use
6149
+ --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
6150
+ ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
6151
+ select it explicitly with --context or ODLA_CONTEXT. Each named context gets
6152
+ isolated developer and scoped-token caches. Override their locations with
6153
+ ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
6154
+ the metadata file. Flags and specific ODLA_* scope variables beat a selected
6155
+ context, which beats project config. There is no ambient current context.
6156
+ "context show" reports only provenance and cache state and never authenticates.
6138
6157
  Provision opens the approval page in your browser automatically whenever the
6139
6158
  machine can show one, including agent-driven runs; only CI, SSH, and
6140
6159
  display-less hosts skip it. Use --open to force or --no-open to suppress.
@@ -6848,6 +6867,7 @@ var COMMAND_SURFACE = {
6848
6867
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
6849
6868
  capabilities: {},
6850
6869
  code: { connect: {} },
6870
+ context: { show: {}, list: {}, save: {}, remove: {} },
6851
6871
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
6852
6872
  discuss: {
6853
6873
  groups: {},
@@ -8135,6 +8155,384 @@ async function codeCommand(parsed, dependencies) {
8135
8155
  });
8136
8156
  }
8137
8157
 
8158
+ // src/operator-credentials.ts
8159
+ import process9 from "process";
8160
+ function developerTokenStatus(context, parsed, now = Date.now()) {
8161
+ const cached = readJsonFile(context.cfg.local.tokenFile);
8162
+ const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8163
+ const source = clean(
8164
+ stringOpt(parsed.options.token)
8165
+ ) ? "flag" : clean(process9.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8166
+ return {
8167
+ source,
8168
+ cacheFile: context.cfg.local.tokenFile,
8169
+ cacheStatus
8170
+ };
8171
+ }
8172
+ function clean(value) {
8173
+ const normalized = value?.trim();
8174
+ return normalized || void 0;
8175
+ }
8176
+
8177
+ // src/operator-context.ts
8178
+ import { existsSync as existsSync10 } from "fs";
8179
+ import { join as join10, resolve as resolve11 } from "path";
8180
+ import process11 from "process";
8181
+
8182
+ // src/operator-profiles.ts
8183
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
8184
+ import { homedir as homedir2 } from "os";
8185
+ import { dirname as dirname7, join as join9, resolve as resolve10 } from "path";
8186
+ import process10 from "process";
8187
+ function operatorProfileFile() {
8188
+ return resolve10(
8189
+ clean2(process10.env.ODLA_CONTEXT_FILE) ?? join9(homedir2(), ".odla", "contexts.json")
8190
+ );
8191
+ }
8192
+ function resolveOperatorProfile(parsed) {
8193
+ const fromFlag = clean2(stringOpt(parsed.options.context));
8194
+ const fromEnvironment = clean2(process10.env.ODLA_CONTEXT);
8195
+ const name = fromFlag ?? fromEnvironment ?? null;
8196
+ const file = operatorProfileFile();
8197
+ if (!name) {
8198
+ return { name: null, source: "unresolved", file, value: null };
8199
+ }
8200
+ assertOperatorName(name, "context");
8201
+ const profiles = readOperatorProfiles(file);
8202
+ const value = Object.hasOwn(profiles, name) ? profiles[name] : void 0;
8203
+ if (!value) {
8204
+ throw new Error(
8205
+ `operator context "${name}" not found in ${file}; run "odla-ai context list" or save it first`
8206
+ );
8207
+ }
8208
+ return {
8209
+ name,
8210
+ source: fromFlag ? "flag" : "environment",
8211
+ file,
8212
+ value
8213
+ };
8214
+ }
8215
+ function listOperatorProfiles(file = operatorProfileFile()) {
8216
+ return Object.entries(readOperatorProfiles(file)).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({ name, ...profile }));
8217
+ }
8218
+ function saveOperatorProfile(name, profile, file = operatorProfileFile()) {
8219
+ assertOperatorName(name, "context");
8220
+ const normalized = validateProfile(profile, `operator context "${name}"`);
8221
+ const profiles = readOperatorProfiles(file);
8222
+ profiles[name] = normalized;
8223
+ writePrivateJson(file, { schemaVersion: 1, profiles });
8224
+ }
8225
+ function removeOperatorProfile(name, file = operatorProfileFile()) {
8226
+ assertOperatorName(name, "context");
8227
+ const profiles = readOperatorProfiles(file);
8228
+ if (!Object.hasOwn(profiles, name)) return false;
8229
+ delete profiles[name];
8230
+ writePrivateJson(file, { schemaVersion: 1, profiles });
8231
+ return true;
8232
+ }
8233
+ function operatorCredentialFiles(selection) {
8234
+ const base = selection.name ? join9(dirname7(selection.file), "profiles", selection.name) : join9(homedir2(), ".odla");
8235
+ return {
8236
+ developer: join9(base, "dev-token.json"),
8237
+ scoped: join9(base, "admin-token.local.json")
8238
+ };
8239
+ }
8240
+ function assertOperatorName(value, label) {
8241
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
8242
+ throw new Error(
8243
+ `${label} must contain lowercase letters, numbers, and hyphens`
8244
+ );
8245
+ }
8246
+ }
8247
+ function readOperatorProfiles(file) {
8248
+ if (!existsSync9(file)) return emptyProfiles();
8249
+ let raw;
8250
+ try {
8251
+ raw = JSON.parse(readFileSync8(file, "utf8"));
8252
+ } catch {
8253
+ throw new Error(`operator context file ${file} is not valid JSON`);
8254
+ }
8255
+ if (!raw || typeof raw !== "object" || raw.schemaVersion !== 1 || !raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles)) {
8256
+ throw new Error(`operator context file ${file} has an invalid shape`);
8257
+ }
8258
+ const unknown = Object.keys(raw).filter(
8259
+ (key) => !["schemaVersion", "profiles"].includes(key)
8260
+ );
8261
+ if (unknown.length > 0) {
8262
+ throw new Error(
8263
+ `operator context file ${file} contains unsupported field(s): ${unknown.sort().join(", ")}`
8264
+ );
8265
+ }
8266
+ const profiles = emptyProfiles();
8267
+ for (const [name, value] of Object.entries(
8268
+ raw.profiles
8269
+ )) {
8270
+ assertOperatorName(name, "context");
8271
+ profiles[name] = validateProfile(value, `operator context "${name}"`);
8272
+ }
8273
+ return profiles;
8274
+ }
8275
+ function emptyProfiles() {
8276
+ return /* @__PURE__ */ Object.create(null);
8277
+ }
8278
+ function validateProfile(value, label) {
8279
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8280
+ throw new Error(`${label} has an invalid shape`);
8281
+ }
8282
+ const unknown = Object.keys(value).filter(
8283
+ (key) => !["platform", "app", "environment"].includes(key)
8284
+ );
8285
+ if (unknown.length > 0) {
8286
+ throw new Error(
8287
+ `${label} contains unsupported field(s): ${unknown.sort().join(", ")}; contexts store scope metadata only, never credentials`
8288
+ );
8289
+ }
8290
+ const candidate = value;
8291
+ if (typeof candidate.platform !== "string") {
8292
+ throw new Error(`${label} needs an absolute platform URL`);
8293
+ }
8294
+ const platform = platformAudience(candidate.platform);
8295
+ const app = clean2(candidate.app);
8296
+ const environment2 = clean2(candidate.environment);
8297
+ if (app) assertOperatorName(app, "app");
8298
+ if (environment2) assertOperatorName(environment2, "environment");
8299
+ return {
8300
+ platform,
8301
+ ...app ? { app } : {},
8302
+ ...environment2 ? { environment: environment2 } : {}
8303
+ };
8304
+ }
8305
+ function clean2(value) {
8306
+ const normalized = value?.trim();
8307
+ return normalized || void 0;
8308
+ }
8309
+
8310
+ // src/operator-context.ts
8311
+ var DEFAULT_PLATFORM2 = "https://odla.ai";
8312
+ async function resolveOperatorContext(parsed, options = {}) {
8313
+ const profile = resolveOperatorProfile(parsed);
8314
+ const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
8315
+ const configPath = resolve11(configArgument);
8316
+ const explicitConfig = parsed.options.config !== void 0;
8317
+ const hasConfig = existsSync10(configPath);
8318
+ if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
8319
+ await loadProjectConfig(configArgument);
8320
+ }
8321
+ const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
8322
+ const platformFlag = clean3(stringOpt(parsed.options.platform));
8323
+ const platformEnvironment = clean3(process11.env.ODLA_PLATFORM_URL);
8324
+ const platformValue = platformAudience(
8325
+ platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
8326
+ );
8327
+ const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
8328
+ const appFlag = clean3(stringOpt(parsed.options.app));
8329
+ const appEnvironment = clean3(process11.env.ODLA_APP_ID);
8330
+ const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
8331
+ const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
8332
+ if (appValue) assertOperatorName(appValue, "app");
8333
+ if (options.requireApp && !appValue) {
8334
+ throw new Error(
8335
+ "app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
8336
+ );
8337
+ }
8338
+ const envFlag = clean3(stringOpt(parsed.options.env));
8339
+ const envEnvironment = clean3(process11.env.ODLA_ENV);
8340
+ const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
8341
+ const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
8342
+ if (environmentValue) {
8343
+ assertOperatorName(environmentValue, "environment");
8344
+ }
8345
+ const rootDir = loaded?.rootDir ?? process11.cwd();
8346
+ const profileCredentials = operatorCredentialFiles(profile);
8347
+ const tokenFile = clean3(process11.env.ODLA_DEV_TOKEN_FILE) ? resolve11(process11.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
8348
+ const scopedTokenFile = clean3(process11.env.ODLA_ADMIN_TOKEN_FILE) ? resolve11(process11.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join10(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
8349
+ const cfg = loaded ? {
8350
+ ...loaded,
8351
+ platformUrl: platformValue,
8352
+ app: appValue ? {
8353
+ id: appValue,
8354
+ name: appValue === loaded.app.id ? loaded.app.name : appValue
8355
+ } : loaded.app,
8356
+ local: { ...loaded.local, tokenFile }
8357
+ } : {
8358
+ configPath,
8359
+ rootDir,
8360
+ platformUrl: platformValue,
8361
+ dbEndpoint: platformValue,
8362
+ app: {
8363
+ id: appValue ?? "operator",
8364
+ name: appValue ?? "Operator"
8365
+ },
8366
+ envs: environmentValue ? [environmentValue] : [],
8367
+ services: [],
8368
+ local: {
8369
+ tokenFile,
8370
+ credentialsFile: join10(rootDir, ".odla", "credentials.local.json"),
8371
+ devVarsFile: join10(rootDir, ".dev.vars"),
8372
+ gitignore: true
8373
+ }
8374
+ };
8375
+ return {
8376
+ cfg,
8377
+ profile: {
8378
+ name: profile.name,
8379
+ source: profile.source,
8380
+ file: profile.file
8381
+ },
8382
+ config: {
8383
+ path: configPath,
8384
+ status: loaded ? "loaded" : "absent",
8385
+ explicit: explicitConfig
8386
+ },
8387
+ platform: { value: platformValue, source: platformSource },
8388
+ app: { value: appValue, source: appSource },
8389
+ environment: {
8390
+ value: environmentValue,
8391
+ source: environmentSource
8392
+ },
8393
+ credentials: {
8394
+ developerTokenFile: tokenFile,
8395
+ scopedTokenFile
8396
+ }
8397
+ };
8398
+ }
8399
+ function clean3(value) {
8400
+ const normalized = value?.trim();
8401
+ return normalized || void 0;
8402
+ }
8403
+
8404
+ // src/context-command.ts
8405
+ async function contextCommand(parsed, deps = {}) {
8406
+ assertArgs(
8407
+ parsed,
8408
+ [
8409
+ "config",
8410
+ "context",
8411
+ "platform",
8412
+ "app",
8413
+ "env",
8414
+ "token",
8415
+ "json",
8416
+ "yes"
8417
+ ],
8418
+ 3
8419
+ );
8420
+ const action = parsed.positionals[1];
8421
+ const out = deps.stdout ?? console;
8422
+ if (action === "list") {
8423
+ const profiles = listOperatorProfiles();
8424
+ if (parsed.options.json === true) {
8425
+ out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
8426
+ return;
8427
+ }
8428
+ if (profiles.length === 0) {
8429
+ out.log("No named operator contexts.");
8430
+ return;
8431
+ }
8432
+ out.log("name platform app environment");
8433
+ for (const profile of profiles) {
8434
+ out.log(
8435
+ `${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
8436
+ );
8437
+ }
8438
+ return;
8439
+ }
8440
+ if (action === "save") {
8441
+ if (parsed.options.token !== void 0) {
8442
+ throw new Error(
8443
+ "context save does not accept --token; contexts store scope metadata only"
8444
+ );
8445
+ }
8446
+ const name = requireName(parsed);
8447
+ const context2 = await resolveOperatorContext(parsed, {
8448
+ allowMissingConfig: true
8449
+ });
8450
+ const profile = {
8451
+ platform: context2.platform.value,
8452
+ ...context2.app.value ? { app: context2.app.value } : {},
8453
+ ...context2.environment.value ? { environment: context2.environment.value } : {}
8454
+ };
8455
+ saveOperatorProfile(name, profile, context2.profile.file);
8456
+ if (parsed.options.json === true) {
8457
+ out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
8458
+ } else {
8459
+ out.log(`saved operator context "${name}" (${profile.platform})`);
8460
+ }
8461
+ return;
8462
+ }
8463
+ if (action === "remove") {
8464
+ const name = requireName(parsed);
8465
+ if (parsed.options.yes !== true) {
8466
+ throw new Error(`context remove "${name}" requires --yes`);
8467
+ }
8468
+ const removed = removeOperatorProfile(name);
8469
+ const result2 = {
8470
+ schemaVersion: 1,
8471
+ name,
8472
+ removed,
8473
+ credentialCachesRemoved: false,
8474
+ grantsRevoked: false
8475
+ };
8476
+ if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
8477
+ else {
8478
+ const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
8479
+ out.log(`${status}; credential caches were not deleted or revoked`);
8480
+ }
8481
+ return;
8482
+ }
8483
+ if (action !== "show") {
8484
+ throw new Error(
8485
+ `unknown context action "${action ?? ""}". Try show|list|save|remove.`
8486
+ );
8487
+ }
8488
+ const context = await resolveOperatorContext(parsed, {
8489
+ allowMissingConfig: true,
8490
+ defaultEnvironment: "prod"
8491
+ });
8492
+ const token = developerTokenStatus(context, parsed);
8493
+ const result = {
8494
+ schemaVersion: 1,
8495
+ profile: context.profile,
8496
+ config: context.config,
8497
+ platform: context.platform,
8498
+ app: context.app,
8499
+ environment: context.environment,
8500
+ authentication: {
8501
+ developerToken: token,
8502
+ scopedTokenCacheFile: context.credentials.scopedTokenFile,
8503
+ handshakeStarted: false
8504
+ }
8505
+ };
8506
+ if (parsed.options.json === true) {
8507
+ out.log(JSON.stringify(result, null, 2));
8508
+ return;
8509
+ }
8510
+ out.log(`config: ${context.config.status} (${context.config.path})`);
8511
+ out.log(
8512
+ `context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
8513
+ );
8514
+ out.log(
8515
+ `platform: ${context.platform.value} (${context.platform.source})`
8516
+ );
8517
+ out.log(
8518
+ `app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
8519
+ );
8520
+ out.log(
8521
+ `env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
8522
+ );
8523
+ out.log(
8524
+ `auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
8525
+ );
8526
+ out.log("handshake: not started");
8527
+ }
8528
+ function requireName(parsed) {
8529
+ const name = parsed.positionals[2]?.trim();
8530
+ if (!name) {
8531
+ throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
8532
+ }
8533
+ return name;
8534
+ }
8535
+
8138
8536
  // src/discuss-actions.ts
8139
8537
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8140
8538
  async function request(ctx, method, path, body) {
@@ -8186,8 +8584,9 @@ async function discussGroups(ctx) {
8186
8584
  }
8187
8585
  async function discussList(ctx, parsed) {
8188
8586
  const query = new URLSearchParams();
8587
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8588
+ if (app) query.set("app", app);
8189
8589
  for (const [flag, param] of [
8190
- ["app", "app"],
8191
8590
  ["q", "q"],
8192
8591
  ["state", "state"],
8193
8592
  ["limit", "limit"],
@@ -8263,7 +8662,7 @@ function renderRead(ctx, topic, posts) {
8263
8662
  }
8264
8663
  }
8265
8664
  async function discussPost(ctx, parsed) {
8266
- const appId = stringOpt(parsed.options.app);
8665
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8267
8666
  if (!appId) throw new Error("discuss post needs --app <appId>");
8268
8667
  const subject = stringOpt(parsed.options.subject);
8269
8668
  if (!subject) throw new Error('discuss post needs --subject "\u2026"');
@@ -8295,7 +8694,7 @@ async function discussResolve(ctx, id, resolved, parsed) {
8295
8694
  }
8296
8695
  async function discussWho(ctx, parsed) {
8297
8696
  const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
8298
- const app = stringOpt(parsed.options.app);
8697
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8299
8698
  if (app) query.set("app", app);
8300
8699
  const kinds = stringOpt(parsed.options.kinds);
8301
8700
  if (kinds) query.set("kinds", kinds);
@@ -8399,14 +8798,14 @@ function jsonl(ctx, parsed, value) {
8399
8798
  }
8400
8799
  async function discussWatch(ctx, topicId, parsed) {
8401
8800
  if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
8402
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
8801
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
8403
8802
  const now = ctx.now ?? Date.now;
8404
8803
  const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
8405
8804
  const timeoutSeconds = numberOpt2(parsed, "timeout");
8406
8805
  const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
8407
8806
  const by = stringOpt(parsed.options.by);
8408
8807
  const self = stringOpt(parsed.options.self);
8409
- const app = stringOpt(parsed.options.app);
8808
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8410
8809
  let cursor = stringOpt(parsed.options.cursor);
8411
8810
  let firstSuccess = true;
8412
8811
  let consecutiveFailures = 0;
@@ -8551,14 +8950,19 @@ var ALLOWED = [
8551
8950
  "timeout",
8552
8951
  "cursor",
8553
8952
  "jsonl",
8554
- "mutation-id"
8953
+ "mutation-id",
8954
+ "platform",
8955
+ "context"
8555
8956
  ];
8556
8957
  function requireId(id, action) {
8557
8958
  if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
8558
8959
  return id;
8559
8960
  }
8560
8961
  async function buildContext(parsed, deps) {
8561
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8962
+ const context = await resolveOperatorContext(parsed, {
8963
+ allowMissingConfig: true
8964
+ });
8965
+ const { cfg } = context;
8562
8966
  const doFetch = deps.fetch ?? fetch;
8563
8967
  const out = deps.stdout ?? console;
8564
8968
  const token = await getDeveloperToken(
@@ -8572,7 +8976,16 @@ async function buildContext(parsed, deps) {
8572
8976
  doFetch,
8573
8977
  out
8574
8978
  );
8575
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
8979
+ return {
8980
+ platformUrl: cfg.platformUrl,
8981
+ token,
8982
+ doFetch,
8983
+ out,
8984
+ json: parsed.options.json === true,
8985
+ // Preserve Discussions' existing cross-project default for config-backed
8986
+ // use while allowing remote agents to scope via ODLA_APP_ID or a profile.
8987
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
8988
+ };
8576
8989
  }
8577
8990
  async function discussCommand(parsed, deps = {}) {
8578
8991
  assertArgs(parsed, ALLOWED, 3);
@@ -8684,7 +9097,7 @@ async function pmList(ctx, entity, parsed) {
8684
9097
  assignee: "assigneeId",
8685
9098
  decision: "decisionId"
8686
9099
  };
8687
- const app = stringOpt(parsed.options.app);
9100
+ const app = stringOpt(parsed.options.app) ?? ctx.appId;
8688
9101
  if (app) q.set("app", app);
8689
9102
  for (const [flag, param] of Object.entries(filters)) {
8690
9103
  const v = stringOpt(parsed.options[flag]);
@@ -8703,7 +9116,7 @@ async function pmList(ctx, entity, parsed) {
8703
9116
  });
8704
9117
  }
8705
9118
  async function pmAdd(ctx, entity, parsed) {
8706
- const appId = stringOpt(parsed.options.app);
9119
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8707
9120
  if (!appId) throw new Error("pm add needs --app <appId>");
8708
9121
  const input = collectEntityFields(entity, parsed, false);
8709
9122
  if (!input.title) throw new Error("pm add needs --title <title>");
@@ -8754,7 +9167,7 @@ async function allRecords(ctx, entity, appId) {
8754
9167
  }
8755
9168
  }
8756
9169
  async function pmHandoff(ctx, parsed) {
8757
- const appId = stringOpt(parsed.options.app);
9170
+ const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8758
9171
  if (!appId) throw new Error("pm handoff needs --app <appId>");
8759
9172
  const [goals, tasks, bugs] = await Promise.all([
8760
9173
  allRecords(ctx, "goal", appId),
@@ -8849,14 +9262,19 @@ var ALLOWED2 = [
8849
9262
  "limit",
8850
9263
  "offset",
8851
9264
  "q",
8852
- "mutation-id"
9265
+ "mutation-id",
9266
+ "platform",
9267
+ "context"
8853
9268
  ];
8854
9269
  function requireId2(id, action) {
8855
9270
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
8856
9271
  return id;
8857
9272
  }
8858
9273
  async function buildContext2(parsed, deps) {
8859
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9274
+ const context = await resolveOperatorContext(parsed, {
9275
+ allowMissingConfig: true
9276
+ });
9277
+ const { cfg } = context;
8860
9278
  const doFetch = deps.fetch ?? fetch;
8861
9279
  const out = deps.stdout ?? console;
8862
9280
  const token = await getDeveloperToken(
@@ -8865,7 +9283,16 @@ async function buildContext2(parsed, deps) {
8865
9283
  doFetch,
8866
9284
  out
8867
9285
  );
8868
- return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
9286
+ return {
9287
+ platformUrl: cfg.platformUrl,
9288
+ token,
9289
+ doFetch,
9290
+ out,
9291
+ json: parsed.options.json === true,
9292
+ // Preserve PM's existing cross-project default when a config is present;
9293
+ // only an explicit remote-agent environment or profile selects an app.
9294
+ appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
9295
+ };
8869
9296
  }
8870
9297
  async function pmCommand(parsed, deps = {}) {
8871
9298
  const word = parsed.positionals[1] ?? "";
@@ -8914,6 +9341,20 @@ function statusVerdict(reads) {
8914
9341
  sourceHttp(reasons, "canary", reads.canary);
8915
9342
  sourceHttp(reasons, "collector", reads.collector);
8916
9343
  sourceHttp(reasons, "provider", reads.provider);
9344
+ if (reads.providerCapacity) {
9345
+ sourceHttp(
9346
+ reasons,
9347
+ "provider",
9348
+ reads.providerCapacity,
9349
+ "provider_capacity_"
9350
+ );
9351
+ }
9352
+ sourceHttp(
9353
+ reasons,
9354
+ "provider",
9355
+ reads.providerReconciliation,
9356
+ "provider_reconciliation_"
9357
+ );
8917
9358
  sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
8918
9359
  const liveStatus = String(reads.liveSync.body.status ?? "");
8919
9360
  if (liveStatus === "partial") {
@@ -8986,6 +9427,17 @@ function statusVerdict(reads) {
8986
9427
  severity: "degraded"
8987
9428
  });
8988
9429
  }
9430
+ const reconciliationStatus = String(
9431
+ reads.providerReconciliation.body.status ?? ""
9432
+ );
9433
+ if (reads.providerReconciliation.httpStatus >= 200 && reads.providerReconciliation.httpStatus < 300 && reconciliationStatus && reconciliationStatus !== "within_budget") {
9434
+ const reconciliationReason = reads.providerReconciliation.body.reason;
9435
+ reasons.push({
9436
+ source: "provider",
9437
+ code: reconciliationStatus === "outside_budget" ? typeof reconciliationReason === "string" && reconciliationReason ? reconciliationReason : "request_coverage_outside_budget" : `request_reconciliation_${reconciliationStatus}`,
9438
+ severity: "degraded"
9439
+ });
9440
+ }
8989
9441
  const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
8990
9442
  if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
8991
9443
  reasons.push({
@@ -9056,8 +9508,19 @@ function printO11yStatus(status, out) {
9056
9508
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9057
9509
  );
9058
9510
  const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
9511
+ const providerCapacity = record6(status.provider.body.capacity) ? status.provider.body.capacity : {};
9512
+ const workerMemory = record6(providerCapacity.memory) ? providerCapacity.memory : {};
9513
+ out.log(
9514
+ `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`
9515
+ );
9516
+ for (const line of providerCapacityLines(status.providerCapacity)) {
9517
+ out.log(line);
9518
+ }
9519
+ const coverage = record6(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
9520
+ const coverageCounts = record6(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
9521
+ const coverageBudget = record6(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
9059
9522
  out.log(
9060
- `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
9523
+ `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`
9061
9524
  );
9062
9525
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9063
9526
  const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
@@ -9068,6 +9531,27 @@ function printO11yStatus(status, out) {
9068
9531
  `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
9069
9532
  );
9070
9533
  }
9534
+ function providerCapacityLines(read3) {
9535
+ const resources = record6(read3.body.resources) ? read3.body.resources : {};
9536
+ const durableObjects = record6(resources.durableObjects) ? resources.durableObjects : {};
9537
+ const periodic = record6(durableObjects.periodic) ? durableObjects.periodic : {};
9538
+ const storage = record6(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
9539
+ const d1 = record6(resources.d1) ? resources.d1 : {};
9540
+ const d1Activity = record6(d1.activity) ? d1.activity : {};
9541
+ const d1Storage = record6(d1.storage) ? d1.storage : {};
9542
+ const d1Latency = record6(d1Activity.latency) ? d1Activity.latency : {};
9543
+ const r2 = record6(resources.r2) ? resources.r2 : {};
9544
+ const r2Operations = record6(r2.operations) ? r2.operations : {};
9545
+ const r2Storage = record6(r2.storage) ? r2.storage : {};
9546
+ const status = String(
9547
+ read3.body.status ?? read3.body.error ?? "unavailable"
9548
+ );
9549
+ return [
9550
+ `cloudflare-capacity ${read3.httpStatus} ${status} ${optionalBytes(storage.storedBytes)} SQLite DO storage ${optionalCount(periodic.activeWebsocketConnectionsMax, "active socket max")} ${optionalAgeSeconds(storage.ageSeconds)} storage evidence age`,
9551
+ `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`,
9552
+ `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`
9553
+ ];
9554
+ }
9071
9555
  function liveSyncLine(read3) {
9072
9556
  const performance = record6(read3.body.performance) ? read3.body.performance : {};
9073
9557
  const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
@@ -9086,12 +9570,42 @@ function optionalAge(value) {
9086
9570
  if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
9087
9571
  return `${Math.max(0, Math.round(value / 1e3))}s`;
9088
9572
  }
9573
+ function optionalPercent(value) {
9574
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value * 100)}%` : "\u2014";
9575
+ }
9576
+ function optionalBytes(value) {
9577
+ if (typeof value !== "number" || !Number.isFinite(value)) return "\u2014";
9578
+ if (value >= 1024 * 1024 * 1024) {
9579
+ return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
9580
+ }
9581
+ if (value >= 1024 * 1024) {
9582
+ return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
9583
+ }
9584
+ if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
9585
+ return `${Math.round(value)} B`;
9586
+ }
9587
+ function optionalCount(value, unit) {
9588
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ${unit}` : `\u2014 ${unit}`;
9589
+ }
9590
+ function optionalAgeSeconds(value) {
9591
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.max(0, Math.round(value))}s` : "unknown";
9592
+ }
9089
9593
 
9090
9594
  // src/o11y-command.ts
9091
9595
  async function o11yCommand(parsed, deps = {}) {
9092
9596
  assertArgs(
9093
9597
  parsed,
9094
- ["config", "token", "email", "json", "app", "env", "minutes"],
9598
+ [
9599
+ "config",
9600
+ "context",
9601
+ "platform",
9602
+ "token",
9603
+ "email",
9604
+ "json",
9605
+ "app",
9606
+ "env",
9607
+ "minutes"
9608
+ ],
9095
9609
  2
9096
9610
  );
9097
9611
  const action = parsed.positionals[1];
@@ -9103,9 +9617,12 @@ async function o11yCommand(parsed, deps = {}) {
9103
9617
  const minutes = statusMinutes(
9104
9618
  numberOpt(parsed.options.minutes, "--minutes") ?? 60
9105
9619
  );
9106
- const cfg = await loadProjectConfig(
9107
- stringOpt(parsed.options.config) ?? "odla.config.mjs"
9108
- );
9620
+ const context = await resolveOperatorContext(parsed, {
9621
+ allowMissingConfig: true,
9622
+ defaultEnvironment: "prod",
9623
+ requireApp: true
9624
+ });
9625
+ const cfg = context.cfg;
9109
9626
  const doFetch = deps.fetch ?? fetch;
9110
9627
  const out = deps.stdout ?? console;
9111
9628
  const token = await getDeveloperToken(
@@ -9119,8 +9636,8 @@ async function o11yCommand(parsed, deps = {}) {
9119
9636
  doFetch,
9120
9637
  out
9121
9638
  );
9122
- const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
9123
- const env = stringOpt(parsed.options.env)?.trim() || "prod";
9639
+ const appId = context.app.value;
9640
+ const env = context.environment.value;
9124
9641
  const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
9125
9642
  const query = new URLSearchParams({ env, minutes: String(minutes) });
9126
9643
  const headers = { authorization: `Bearer ${token}` };
@@ -9137,6 +9654,8 @@ async function o11yCommand(parsed, deps = {}) {
9137
9654
  canary,
9138
9655
  collector,
9139
9656
  provider,
9657
+ providerCapacity,
9658
+ providerReconciliation,
9140
9659
  providerHistory
9141
9660
  ] = await Promise.all([
9142
9661
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
@@ -9153,6 +9672,19 @@ async function o11yCommand(parsed, deps = {}) {
9153
9672
  ),
9154
9673
  read2(`${base}/self-health?${query}`, headers, doFetch),
9155
9674
  read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
9675
+ appId === "odla-db" ? read2(`${base}/provider/cloudflare/capacity`, headers, doFetch) : Promise.resolve({
9676
+ httpStatus: 204,
9677
+ body: {
9678
+ status: "not_applicable",
9679
+ scope: "cloudflare_account",
9680
+ reason: "account-scoped Durable Object capacity is exposed only through odla-db"
9681
+ }
9682
+ }),
9683
+ read2(
9684
+ `${base}/provider/cloudflare/reconciliation?${query}`,
9685
+ headers,
9686
+ doFetch
9687
+ ),
9156
9688
  read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
9157
9689
  ]);
9158
9690
  const verdict = statusVerdict({
@@ -9161,10 +9693,12 @@ async function o11yCommand(parsed, deps = {}) {
9161
9693
  canary,
9162
9694
  collector,
9163
9695
  provider,
9696
+ providerCapacity,
9697
+ providerReconciliation,
9164
9698
  providerHistory
9165
9699
  });
9166
9700
  const status = {
9167
- schemaVersion: 6,
9701
+ schemaVersion: 9,
9168
9702
  scope: { appId, env, minutes },
9169
9703
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
9170
9704
  verdict,
@@ -9174,6 +9708,8 @@ async function o11yCommand(parsed, deps = {}) {
9174
9708
  canary,
9175
9709
  collector,
9176
9710
  provider,
9711
+ providerCapacity,
9712
+ providerReconciliation,
9177
9713
  providerHistory
9178
9714
  };
9179
9715
  if (parsed.options.json === true) {
@@ -9205,9 +9741,9 @@ async function read2(url, headers, doFetch) {
9205
9741
 
9206
9742
  // src/record.ts
9207
9743
  import { appendFileSync } from "fs";
9208
- import process9 from "process";
9744
+ import process12 from "process";
9209
9745
  function recordInvocation(parsed) {
9210
- const file = process9.env.ODLA_CLI_RECORD;
9746
+ const file = process12.env.ODLA_CLI_RECORD;
9211
9747
  if (!file) return;
9212
9748
  try {
9213
9749
  const entry = {
@@ -9221,13 +9757,8 @@ function recordInvocation(parsed) {
9221
9757
  }
9222
9758
  }
9223
9759
 
9224
- // src/runbook-command.ts
9225
- import { existsSync as existsSync10 } from "fs";
9226
- import { join as join12, resolve as resolve10 } from "path";
9227
- import process11 from "process";
9228
-
9229
9760
  // src/runbook-actions.ts
9230
- import { readFileSync as readFileSync8 } from "fs";
9761
+ import { readFileSync as readFileSync9 } from "fs";
9231
9762
 
9232
9763
  // src/runbook-requires.ts
9233
9764
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -9312,7 +9843,7 @@ async function bySlug(ctx, slug) {
9312
9843
  function readBody(file, inline) {
9313
9844
  if (inline !== void 0) return inline;
9314
9845
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
9315
- return readFileSync8(file === "-" ? 0 : file, "utf8");
9846
+ return readFileSync9(file === "-" ? 0 : file, "utf8");
9316
9847
  }
9317
9848
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
9318
9849
  async function runbookList(ctx, all, query) {
@@ -9399,8 +9930,8 @@ async function runbookRemove(ctx, slug) {
9399
9930
  }
9400
9931
 
9401
9932
  // src/runbook-import.ts
9402
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync } from "fs";
9403
- import { basename as basename2, join as join9 } from "path";
9933
+ import { readFileSync as readFileSync10, readdirSync as readdirSync2, statSync } from "fs";
9934
+ import { basename as basename2, join as join11 } from "path";
9404
9935
  function parseRunbook(text, slug) {
9405
9936
  let rest = text;
9406
9937
  const meta = {};
@@ -9430,7 +9961,7 @@ function readRunbookDir(dir) {
9430
9961
  if (!files.length) throw new Error(`no .md files in ${dir}`);
9431
9962
  return files.map((file) => {
9432
9963
  const slug = basename2(file, ".md");
9433
- const parsed = parseRunbook(readFileSync9(join9(dir, file), "utf8"), slug);
9964
+ const parsed = parseRunbook(readFileSync10(join11(dir, file), "utf8"), slug);
9434
9965
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
9435
9966
  });
9436
9967
  }
@@ -9502,8 +10033,8 @@ async function upsert(ctx, r, visibility) {
9502
10033
 
9503
10034
  // src/runbook-impact.ts
9504
10035
  import { execFileSync as execFileSync2 } from "child_process";
9505
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
9506
- import { join as join10 } from "path";
10036
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
10037
+ import { join as join12 } from "path";
9507
10038
 
9508
10039
  // src/runbook-impact-scan.ts
9509
10040
  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$]*)/;
@@ -9672,10 +10203,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
9672
10203
  }
9673
10204
  function manifestLabeller(root) {
9674
10205
  return (workspace) => {
9675
- const manifest = join10(root, workspace, "package.json");
9676
- if (!existsSync9(manifest)) return void 0;
10206
+ const manifest = join12(root, workspace, "package.json");
10207
+ if (!existsSync11(manifest)) return void 0;
9677
10208
  try {
9678
- const name = JSON.parse(readFileSync10(manifest, "utf8")).name;
10209
+ const name = JSON.parse(readFileSync11(manifest, "utf8")).name;
9679
10210
  return typeof name === "string" ? name : void 0;
9680
10211
  } catch {
9681
10212
  return void 0;
@@ -9742,7 +10273,7 @@ function report3(ctx, impacts) {
9742
10273
  async function runbookImpact(ctx, options, deps = {}) {
9743
10274
  const cwd = deps.cwd ?? process.cwd();
9744
10275
  const runGit = deps.runGit ?? gitRunner(cwd);
9745
- const read3 = deps.readRepoFile ?? ((path) => readFileSync10(join10(cwd, path), "utf8"));
10276
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync11(join12(cwd, path), "utf8"));
9746
10277
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
9747
10278
  if (!surfaces.length) {
9748
10279
  return ctx.out.log(
@@ -9875,12 +10406,12 @@ async function runbookComment(ctx, slug, body) {
9875
10406
 
9876
10407
  // src/runbook-editor.ts
9877
10408
  import { spawnSync } from "child_process";
9878
- import { mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
10409
+ import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
9879
10410
  import { tmpdir as tmpdir4 } from "os";
9880
- import { join as join11 } from "path";
9881
- import process10 from "process";
10411
+ import { join as join13 } from "path";
10412
+ import process13 from "process";
9882
10413
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
9883
- function resolveEditor(env = process10.env) {
10414
+ function resolveEditor(env = process13.env) {
9884
10415
  for (const name of EDITOR_ENV) {
9885
10416
  const value = env[name];
9886
10417
  if (value && value.trim()) return value.trim();
@@ -9894,8 +10425,8 @@ function defaultRun(command, path) {
9894
10425
  return result.status ?? 0;
9895
10426
  }
9896
10427
  function editText(initial, slug, deps = {}) {
9897
- const env = deps.env ?? process10.env;
9898
- const interactive = deps.interactive ?? (() => Boolean(process10.stdin.isTTY));
10428
+ const env = deps.env ?? process13.env;
10429
+ const interactive = deps.interactive ?? (() => Boolean(process13.stdin.isTTY));
9899
10430
  const editor = resolveEditor(env);
9900
10431
  if (!editor)
9901
10432
  throw new Error(
@@ -9903,13 +10434,13 @@ function editText(initial, slug, deps = {}) {
9903
10434
  );
9904
10435
  if (!interactive())
9905
10436
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
9906
- const dir = mkdtempSync(join11(tmpdir4(), "odla-runbook-"));
9907
- const file = join11(dir, `${slug}.md`);
10437
+ const dir = mkdtempSync(join13(tmpdir4(), "odla-runbook-"));
10438
+ const file = join13(dir, `${slug}.md`);
9908
10439
  try {
9909
10440
  writeFileSync4(file, initial, { mode: 384 });
9910
10441
  const code = defaultRunOrInjected(deps)(editor, file);
9911
10442
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
9912
- const edited = readFileSync11(file, "utf8");
10443
+ const edited = readFileSync12(file, "utf8");
9913
10444
  return edited === initial ? null : edited;
9914
10445
  } finally {
9915
10446
  rmSync2(dir, { recursive: true, force: true });
@@ -9950,10 +10481,16 @@ function credentialKind(identity) {
9950
10481
  return identity.scopes.length ? "device token (scoped)" : "device token or session";
9951
10482
  }
9952
10483
  async function whoamiCommand(parsed, deps = {}) {
9953
- assertArgs(parsed, ["config", "token", "email", "json"], 1);
10484
+ assertArgs(
10485
+ parsed,
10486
+ ["config", "context", "platform", "token", "email", "json"],
10487
+ 1
10488
+ );
9954
10489
  const out = deps.stdout ?? console;
9955
10490
  const doFetch = deps.fetch ?? fetch;
9956
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10491
+ const { cfg } = await resolveOperatorContext(parsed, {
10492
+ allowMissingConfig: true
10493
+ });
9957
10494
  const token = await getDeveloperToken(
9958
10495
  cfg,
9959
10496
  {
@@ -10002,34 +10539,24 @@ var ALLOWED3 = [
10002
10539
  "dry-run",
10003
10540
  "limit",
10004
10541
  "base",
10005
- "requires"
10542
+ "requires",
10543
+ "platform",
10544
+ "context"
10006
10545
  ];
10007
10546
  function requireSlug(slug, action) {
10008
10547
  if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
10009
10548
  return slug;
10010
10549
  }
10011
10550
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
10012
- var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
10013
- async function loadOrDefaultConfig(configPath, action, appId) {
10014
- const projectScoped = appId !== void 0;
10015
- if (!projectScoped && CONFIG_FREE.has(action) && !existsSync10(resolve10(configPath))) {
10016
- const rootDir = process11.cwd();
10017
- return {
10018
- configPath,
10019
- platformUrl: process11.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
10020
- rootDir,
10021
- app: { id: "", name: "" },
10022
- local: { tokenFile: join12(rootDir, ".odla/dev-token.json") }
10023
- };
10024
- }
10025
- return loadProjectConfig(configPath);
10026
- }
10027
10551
  async function buildContext3(parsed, deps, action) {
10028
- const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
10029
- const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
10552
+ const appIdOption = stringOpt(parsed.options.app);
10553
+ const context = await resolveOperatorContext(parsed, {
10554
+ allowMissingConfig: true
10555
+ });
10556
+ const { cfg } = context;
10030
10557
  const doFetch = deps.fetch ?? fetch;
10031
10558
  const out = deps.stdout ?? console;
10032
- const appId = stringOpt(parsed.options.app) ?? PLATFORM_SCOPE;
10559
+ const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
10033
10560
  const dryRun = parsed.options["dry-run"] === true;
10034
10561
  if (action === "import" && dryRun) {
10035
10562
  return {
@@ -10050,8 +10577,10 @@ async function buildContext3(parsed, deps, action) {
10050
10577
  fetch: doFetch,
10051
10578
  stdout: out,
10052
10579
  openApprovalUrl: deps.openUrl,
10053
- // Anchor the grant cache to this project, not the shell's cwd.
10054
- rootDir: cfg.rootDir
10580
+ // A project, named context, or the global operator context owns the
10581
+ // exact-scope cache; it never follows an arbitrary shell directory.
10582
+ rootDir: cfg.rootDir,
10583
+ tokenFile: context.credentials.scopedTokenFile
10055
10584
  }) : await getDeveloperToken(
10056
10585
  cfg,
10057
10586
  {
@@ -10634,6 +11163,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10634
11163
  await whoamiCommand(parsed, runtime);
10635
11164
  return;
10636
11165
  }
11166
+ if (command === "context") {
11167
+ await contextCommand(parsed, runtime);
11168
+ return;
11169
+ }
10637
11170
  if (command === "runbook") {
10638
11171
  await runbookCommand(parsed, runtime);
10639
11172
  return;
@@ -10793,4 +11326,4 @@ export {
10793
11326
  exitCodeFor,
10794
11327
  runCli
10795
11328
  };
10796
- //# sourceMappingURL=chunk-MFC4TX6W.js.map
11329
+ //# sourceMappingURL=chunk-5MRY3V2K.js.map