@odla-ai/cli 0.25.11 → 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, 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",
@@ -1638,7 +1639,7 @@ var CAPABILITIES = {
1638
1639
  "approve GitHub App repository access separately from redacted-snippet disclosure"
1639
1640
  ],
1640
1641
  studio: [
1641
- "view application telemetry, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
1642
+ "view application telemetry grouped by exact Worker version, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
1642
1643
  "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
1643
1644
  "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
1644
1645
  "perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
@@ -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({
@@ -9022,11 +9474,138 @@ function sourceHttp(reasons, source, read3, prefix = "") {
9022
9474
  });
9023
9475
  }
9024
9476
 
9477
+ // src/o11y-output.ts
9478
+ function printO11yStatus(status, out) {
9479
+ out.log(
9480
+ `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
9481
+ );
9482
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
9483
+ const requests = routes.reduce(
9484
+ (total, row) => total + numeric3(row.requests),
9485
+ 0
9486
+ );
9487
+ const errors = routes.reduce(
9488
+ (total, row) => total + numeric3(row.errors),
9489
+ 0
9490
+ );
9491
+ out.log(
9492
+ `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
9493
+ );
9494
+ const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record6) : [];
9495
+ out.log(
9496
+ `application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
9497
+ (row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
9498
+ ).join(", ") : "none observed"}`
9499
+ );
9500
+ out.log(liveSyncLine(status.liveSync));
9501
+ const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
9502
+ out.log(
9503
+ `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
9504
+ );
9505
+ const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
9506
+ const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
9507
+ out.log(
9508
+ `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9509
+ );
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 : {};
9522
+ out.log(
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`
9524
+ );
9525
+ const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9526
+ const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
9527
+ out.log(
9528
+ `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
9529
+ );
9530
+ out.log(
9531
+ `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
9532
+ );
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
+ }
9555
+ function liveSyncLine(read3) {
9556
+ const performance = record6(read3.body.performance) ? read3.body.performance : {};
9557
+ const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
9558
+ return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
9559
+ }
9560
+ function record6(value) {
9561
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9562
+ }
9563
+ function numeric3(value) {
9564
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
9565
+ }
9566
+ function optionalNumeric(value) {
9567
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
9568
+ }
9569
+ function optionalAge(value) {
9570
+ if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
9571
+ return `${Math.max(0, Math.round(value / 1e3))}s`;
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
+ }
9593
+
9025
9594
  // src/o11y-command.ts
9026
9595
  async function o11yCommand(parsed, deps = {}) {
9027
9596
  assertArgs(
9028
9597
  parsed,
9029
- ["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
+ ],
9030
9609
  2
9031
9610
  );
9032
9611
  const action = parsed.positionals[1];
@@ -9038,9 +9617,12 @@ async function o11yCommand(parsed, deps = {}) {
9038
9617
  const minutes = statusMinutes(
9039
9618
  numberOpt(parsed.options.minutes, "--minutes") ?? 60
9040
9619
  );
9041
- const cfg = await loadProjectConfig(
9042
- stringOpt(parsed.options.config) ?? "odla.config.mjs"
9043
- );
9620
+ const context = await resolveOperatorContext(parsed, {
9621
+ allowMissingConfig: true,
9622
+ defaultEnvironment: "prod",
9623
+ requireApp: true
9624
+ });
9625
+ const cfg = context.cfg;
9044
9626
  const doFetch = deps.fetch ?? fetch;
9045
9627
  const out = deps.stdout ?? console;
9046
9628
  const token = await getDeveloperToken(
@@ -9054,13 +9636,30 @@ async function o11yCommand(parsed, deps = {}) {
9054
9636
  doFetch,
9055
9637
  out
9056
9638
  );
9057
- const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
9058
- const env = stringOpt(parsed.options.env)?.trim() || "prod";
9639
+ const appId = context.app.value;
9640
+ const env = context.environment.value;
9059
9641
  const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
9060
9642
  const query = new URLSearchParams({ env, minutes: String(minutes) });
9061
9643
  const headers = { authorization: `Bearer ${token}` };
9062
- const [application, liveSync, canary, collector, provider, providerHistory] = await Promise.all([
9644
+ const versionsQuery = new URLSearchParams({
9645
+ env,
9646
+ minutes: String(minutes),
9647
+ dimension: "version",
9648
+ metric: "requests"
9649
+ });
9650
+ const [
9651
+ application,
9652
+ applicationVersions,
9653
+ liveSync,
9654
+ canary,
9655
+ collector,
9656
+ provider,
9657
+ providerCapacity,
9658
+ providerReconciliation,
9659
+ providerHistory
9660
+ ] = await Promise.all([
9063
9661
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
9662
+ read2(`${base}/explore?${versionsQuery}`, headers, doFetch),
9064
9663
  read2(
9065
9664
  `${base}/live-sync/health?${query}`,
9066
9665
  headers,
@@ -9073,6 +9672,19 @@ async function o11yCommand(parsed, deps = {}) {
9073
9672
  ),
9074
9673
  read2(`${base}/self-health?${query}`, headers, doFetch),
9075
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
+ ),
9076
9688
  read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
9077
9689
  ]);
9078
9690
  const verdict = statusVerdict({
@@ -9081,25 +9693,30 @@ async function o11yCommand(parsed, deps = {}) {
9081
9693
  canary,
9082
9694
  collector,
9083
9695
  provider,
9696
+ providerCapacity,
9697
+ providerReconciliation,
9084
9698
  providerHistory
9085
9699
  });
9086
9700
  const status = {
9087
- schemaVersion: 5,
9701
+ schemaVersion: 9,
9088
9702
  scope: { appId, env, minutes },
9089
9703
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
9090
9704
  verdict,
9091
9705
  application,
9706
+ applicationVersions,
9092
9707
  liveSync,
9093
9708
  canary,
9094
9709
  collector,
9095
9710
  provider,
9711
+ providerCapacity,
9712
+ providerReconciliation,
9096
9713
  providerHistory
9097
9714
  };
9098
9715
  if (parsed.options.json === true) {
9099
9716
  out.log(JSON.stringify(status, null, 2));
9100
9717
  return;
9101
9718
  }
9102
- printStatus2(status, out);
9719
+ printO11yStatus(status, out);
9103
9720
  }
9104
9721
  function statusMinutes(value) {
9105
9722
  if (!Number.isSafeInteger(value) || value < 1 || value > 7 * 24 * 60) {
@@ -9121,71 +9738,12 @@ async function read2(url, headers, doFetch) {
9121
9738
  }
9122
9739
  return { httpStatus: response2.status, body };
9123
9740
  }
9124
- function printStatus2(status, out) {
9125
- out.log(
9126
- `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
9127
- );
9128
- const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
9129
- const requests = routes.reduce(
9130
- (total, row) => total + numeric3(row.requests),
9131
- 0
9132
- );
9133
- const errors = routes.reduce(
9134
- (total, row) => total + numeric3(row.errors),
9135
- 0
9136
- );
9137
- out.log(
9138
- `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
9139
- );
9140
- out.log(
9141
- liveSyncLine(status.liveSync)
9142
- );
9143
- const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
9144
- out.log(
9145
- `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
9146
- );
9147
- const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
9148
- const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
9149
- out.log(
9150
- `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9151
- );
9152
- const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
9153
- out.log(
9154
- `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
9155
- );
9156
- const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9157
- const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
9158
- out.log(
9159
- `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
9160
- );
9161
- out.log(
9162
- `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
9163
- );
9164
- }
9165
- function liveSyncLine(read3) {
9166
- const performance = record6(read3.body.performance) ? read3.body.performance : {};
9167
- const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
9168
- return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
9169
- }
9170
- function record6(value) {
9171
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9172
- }
9173
- function numeric3(value) {
9174
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
9175
- }
9176
- function optionalNumeric(value) {
9177
- return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
9178
- }
9179
- function optionalAge(value) {
9180
- if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
9181
- return `${Math.max(0, Math.round(value / 1e3))}s`;
9182
- }
9183
9741
 
9184
9742
  // src/record.ts
9185
9743
  import { appendFileSync } from "fs";
9186
- import process9 from "process";
9744
+ import process12 from "process";
9187
9745
  function recordInvocation(parsed) {
9188
- const file = process9.env.ODLA_CLI_RECORD;
9746
+ const file = process12.env.ODLA_CLI_RECORD;
9189
9747
  if (!file) return;
9190
9748
  try {
9191
9749
  const entry = {
@@ -9199,13 +9757,8 @@ function recordInvocation(parsed) {
9199
9757
  }
9200
9758
  }
9201
9759
 
9202
- // src/runbook-command.ts
9203
- import { existsSync as existsSync10 } from "fs";
9204
- import { join as join12, resolve as resolve10 } from "path";
9205
- import process11 from "process";
9206
-
9207
9760
  // src/runbook-actions.ts
9208
- import { readFileSync as readFileSync8 } from "fs";
9761
+ import { readFileSync as readFileSync9 } from "fs";
9209
9762
 
9210
9763
  // src/runbook-requires.ts
9211
9764
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -9290,7 +9843,7 @@ async function bySlug(ctx, slug) {
9290
9843
  function readBody(file, inline) {
9291
9844
  if (inline !== void 0) return inline;
9292
9845
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
9293
- return readFileSync8(file === "-" ? 0 : file, "utf8");
9846
+ return readFileSync9(file === "-" ? 0 : file, "utf8");
9294
9847
  }
9295
9848
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
9296
9849
  async function runbookList(ctx, all, query) {
@@ -9377,8 +9930,8 @@ async function runbookRemove(ctx, slug) {
9377
9930
  }
9378
9931
 
9379
9932
  // src/runbook-import.ts
9380
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync } from "fs";
9381
- 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";
9382
9935
  function parseRunbook(text, slug) {
9383
9936
  let rest = text;
9384
9937
  const meta = {};
@@ -9408,7 +9961,7 @@ function readRunbookDir(dir) {
9408
9961
  if (!files.length) throw new Error(`no .md files in ${dir}`);
9409
9962
  return files.map((file) => {
9410
9963
  const slug = basename2(file, ".md");
9411
- const parsed = parseRunbook(readFileSync9(join9(dir, file), "utf8"), slug);
9964
+ const parsed = parseRunbook(readFileSync10(join11(dir, file), "utf8"), slug);
9412
9965
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
9413
9966
  });
9414
9967
  }
@@ -9480,8 +10033,8 @@ async function upsert(ctx, r, visibility) {
9480
10033
 
9481
10034
  // src/runbook-impact.ts
9482
10035
  import { execFileSync as execFileSync2 } from "child_process";
9483
- import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
9484
- import { join as join10 } from "path";
10036
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
10037
+ import { join as join12 } from "path";
9485
10038
 
9486
10039
  // src/runbook-impact-scan.ts
9487
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$]*)/;
@@ -9650,10 +10203,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
9650
10203
  }
9651
10204
  function manifestLabeller(root) {
9652
10205
  return (workspace) => {
9653
- const manifest = join10(root, workspace, "package.json");
9654
- if (!existsSync9(manifest)) return void 0;
10206
+ const manifest = join12(root, workspace, "package.json");
10207
+ if (!existsSync11(manifest)) return void 0;
9655
10208
  try {
9656
- const name = JSON.parse(readFileSync10(manifest, "utf8")).name;
10209
+ const name = JSON.parse(readFileSync11(manifest, "utf8")).name;
9657
10210
  return typeof name === "string" ? name : void 0;
9658
10211
  } catch {
9659
10212
  return void 0;
@@ -9720,7 +10273,7 @@ function report3(ctx, impacts) {
9720
10273
  async function runbookImpact(ctx, options, deps = {}) {
9721
10274
  const cwd = deps.cwd ?? process.cwd();
9722
10275
  const runGit = deps.runGit ?? gitRunner(cwd);
9723
- const read3 = deps.readRepoFile ?? ((path) => readFileSync10(join10(cwd, path), "utf8"));
10276
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync11(join12(cwd, path), "utf8"));
9724
10277
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
9725
10278
  if (!surfaces.length) {
9726
10279
  return ctx.out.log(
@@ -9853,12 +10406,12 @@ async function runbookComment(ctx, slug, body) {
9853
10406
 
9854
10407
  // src/runbook-editor.ts
9855
10408
  import { spawnSync } from "child_process";
9856
- 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";
9857
10410
  import { tmpdir as tmpdir4 } from "os";
9858
- import { join as join11 } from "path";
9859
- import process10 from "process";
10411
+ import { join as join13 } from "path";
10412
+ import process13 from "process";
9860
10413
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
9861
- function resolveEditor(env = process10.env) {
10414
+ function resolveEditor(env = process13.env) {
9862
10415
  for (const name of EDITOR_ENV) {
9863
10416
  const value = env[name];
9864
10417
  if (value && value.trim()) return value.trim();
@@ -9872,8 +10425,8 @@ function defaultRun(command, path) {
9872
10425
  return result.status ?? 0;
9873
10426
  }
9874
10427
  function editText(initial, slug, deps = {}) {
9875
- const env = deps.env ?? process10.env;
9876
- const interactive = deps.interactive ?? (() => Boolean(process10.stdin.isTTY));
10428
+ const env = deps.env ?? process13.env;
10429
+ const interactive = deps.interactive ?? (() => Boolean(process13.stdin.isTTY));
9877
10430
  const editor = resolveEditor(env);
9878
10431
  if (!editor)
9879
10432
  throw new Error(
@@ -9881,13 +10434,13 @@ function editText(initial, slug, deps = {}) {
9881
10434
  );
9882
10435
  if (!interactive())
9883
10436
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
9884
- const dir = mkdtempSync(join11(tmpdir4(), "odla-runbook-"));
9885
- const file = join11(dir, `${slug}.md`);
10437
+ const dir = mkdtempSync(join13(tmpdir4(), "odla-runbook-"));
10438
+ const file = join13(dir, `${slug}.md`);
9886
10439
  try {
9887
10440
  writeFileSync4(file, initial, { mode: 384 });
9888
10441
  const code = defaultRunOrInjected(deps)(editor, file);
9889
10442
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
9890
- const edited = readFileSync11(file, "utf8");
10443
+ const edited = readFileSync12(file, "utf8");
9891
10444
  return edited === initial ? null : edited;
9892
10445
  } finally {
9893
10446
  rmSync2(dir, { recursive: true, force: true });
@@ -9928,10 +10481,16 @@ function credentialKind(identity) {
9928
10481
  return identity.scopes.length ? "device token (scoped)" : "device token or session";
9929
10482
  }
9930
10483
  async function whoamiCommand(parsed, deps = {}) {
9931
- assertArgs(parsed, ["config", "token", "email", "json"], 1);
10484
+ assertArgs(
10485
+ parsed,
10486
+ ["config", "context", "platform", "token", "email", "json"],
10487
+ 1
10488
+ );
9932
10489
  const out = deps.stdout ?? console;
9933
10490
  const doFetch = deps.fetch ?? fetch;
9934
- const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10491
+ const { cfg } = await resolveOperatorContext(parsed, {
10492
+ allowMissingConfig: true
10493
+ });
9935
10494
  const token = await getDeveloperToken(
9936
10495
  cfg,
9937
10496
  {
@@ -9980,34 +10539,24 @@ var ALLOWED3 = [
9980
10539
  "dry-run",
9981
10540
  "limit",
9982
10541
  "base",
9983
- "requires"
10542
+ "requires",
10543
+ "platform",
10544
+ "context"
9984
10545
  ];
9985
10546
  function requireSlug(slug, action) {
9986
10547
  if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
9987
10548
  return slug;
9988
10549
  }
9989
10550
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
9990
- var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
9991
- async function loadOrDefaultConfig(configPath, action, appId) {
9992
- const projectScoped = appId !== void 0;
9993
- if (!projectScoped && CONFIG_FREE.has(action) && !existsSync10(resolve10(configPath))) {
9994
- const rootDir = process11.cwd();
9995
- return {
9996
- configPath,
9997
- platformUrl: process11.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
9998
- rootDir,
9999
- app: { id: "", name: "" },
10000
- local: { tokenFile: join12(rootDir, ".odla/dev-token.json") }
10001
- };
10002
- }
10003
- return loadProjectConfig(configPath);
10004
- }
10005
10551
  async function buildContext3(parsed, deps, action) {
10006
- const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
10007
- 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;
10008
10557
  const doFetch = deps.fetch ?? fetch;
10009
10558
  const out = deps.stdout ?? console;
10010
- 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;
10011
10560
  const dryRun = parsed.options["dry-run"] === true;
10012
10561
  if (action === "import" && dryRun) {
10013
10562
  return {
@@ -10028,8 +10577,10 @@ async function buildContext3(parsed, deps, action) {
10028
10577
  fetch: doFetch,
10029
10578
  stdout: out,
10030
10579
  openApprovalUrl: deps.openUrl,
10031
- // Anchor the grant cache to this project, not the shell's cwd.
10032
- 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
10033
10584
  }) : await getDeveloperToken(
10034
10585
  cfg,
10035
10586
  {
@@ -10612,6 +11163,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10612
11163
  await whoamiCommand(parsed, runtime);
10613
11164
  return;
10614
11165
  }
11166
+ if (command === "context") {
11167
+ await contextCommand(parsed, runtime);
11168
+ return;
11169
+ }
10615
11170
  if (command === "runbook") {
10616
11171
  await runbookCommand(parsed, runtime);
10617
11172
  return;
@@ -10771,4 +11326,4 @@ export {
10771
11326
  exitCodeFor,
10772
11327
  runCli
10773
11328
  };
10774
- //# sourceMappingURL=chunk-FYSV7POZ.js.map
11329
+ //# sourceMappingURL=chunk-5MRY3V2K.js.map