@odla-ai/cli 0.25.3 → 0.25.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1935,6 +1935,37 @@ function redactSecrets(value) {
1935
1935
  for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
1936
1936
  return result;
1937
1937
  }
1938
+ function redactingOutput(output) {
1939
+ const redactArgs = (args) => args.map((value) => redactOutputValue(value, /* @__PURE__ */ new WeakMap()));
1940
+ return {
1941
+ log: (...args) => output.log(...redactArgs(args)),
1942
+ error: (...args) => output.error(...redactArgs(args))
1943
+ };
1944
+ }
1945
+ function redactOutputValue(value, seen) {
1946
+ if (typeof value === "string") return redactSecrets(value);
1947
+ if (value instanceof Error) {
1948
+ const redacted = new Error(redactSecrets(value.message));
1949
+ redacted.name = value.name;
1950
+ if (value.stack) redacted.stack = redactSecrets(value.stack);
1951
+ return redacted;
1952
+ }
1953
+ if (!value || typeof value !== "object") return value;
1954
+ const prior = seen.get(value);
1955
+ if (prior) return prior;
1956
+ if (Array.isArray(value)) {
1957
+ const next2 = [];
1958
+ seen.set(value, next2);
1959
+ for (const item of value) next2.push(redactOutputValue(item, seen));
1960
+ return next2;
1961
+ }
1962
+ const prototype = Object.getPrototypeOf(value);
1963
+ if (prototype !== Object.prototype && prototype !== null) return value;
1964
+ const next = {};
1965
+ seen.set(value, next);
1966
+ for (const [key, item] of Object.entries(value)) next[key] = redactOutputValue(item, seen);
1967
+ return next;
1968
+ }
1938
1969
  function looksSecret(value) {
1939
1970
  return redactSecrets(value) !== value || value.includes("-----BEGIN");
1940
1971
  }
@@ -2339,6 +2370,7 @@ var CAPABILITIES = {
2339
2370
  "compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
2340
2371
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
2341
2372
  "validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
2373
+ "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",
2342
2374
  "run app-attributed hosted security discovery and independent validation without provider keys",
2343
2375
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
2344
2376
  "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
@@ -2359,7 +2391,7 @@ var CAPABILITIES = {
2359
2391
  "approve GitHub App repository access separately from redacted-snippet disclosure"
2360
2392
  ],
2361
2393
  studio: [
2362
- "view telemetry and environment state",
2394
+ "view application telemetry, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
2363
2395
  "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
2364
2396
  "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
2365
2397
  "perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
@@ -3351,9 +3383,17 @@ async function smoke(options) {
3351
3383
  throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
3352
3384
  }
3353
3385
  const entry = credentials.envs[env];
3354
- if (!entry?.tenantId || !entry.dbKey) {
3386
+ if (!entry?.tenantId) {
3387
+ throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
3388
+ }
3389
+ const hasDb = cfg.services.includes("db");
3390
+ const hasO11y = cfg.services.includes("o11y");
3391
+ if (hasDb && !entry.dbKey) {
3355
3392
  throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
3356
3393
  }
3394
+ if (hasO11y && !entry.o11yToken) {
3395
+ throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
3396
+ }
3357
3397
  const doFetch = options.fetch ?? fetch;
3358
3398
  out.log(`smoke: ${cfg.app.id}/${env}`);
3359
3399
  out.log(` tenant: ${entry.tenantId}`);
@@ -3366,6 +3406,7 @@ async function smoke(options) {
3366
3406
  }
3367
3407
  out.log(` ai: ${provider}`);
3368
3408
  }
3409
+ if (hasO11y) out.log(` o11y: credentials present`);
3369
3410
  if (cfg.services.includes("calendar")) {
3370
3411
  const token = await getDeveloperToken(
3371
3412
  cfg,
@@ -3384,26 +3425,30 @@ async function smoke(options) {
3384
3425
  out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
3385
3426
  }
3386
3427
  const database = await resolveDatabaseConfig(cfg);
3387
- const expectedSchema = database.schema;
3388
- const expectedEntities = serializedEntities(expectedSchema);
3389
- const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
3390
- const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
3391
- const liveEntities = serializedEntities(liveSchema);
3392
- if (expectedEntities.length) {
3393
- const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
3394
- if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
3395
- }
3396
- out.log(` schema: ${liveEntities.length} entities`);
3397
- const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
3398
- if (aggregateEntity) {
3399
- const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
3400
- ns: aggregateEntity,
3401
- aggregate: { count: true }
3402
- });
3403
- const count = aggregate.aggregate?.count;
3404
- out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
3428
+ if (hasDb) {
3429
+ const expectedSchema = database.schema;
3430
+ const expectedEntities = serializedEntities(expectedSchema);
3431
+ const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
3432
+ const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
3433
+ const liveEntities = serializedEntities(liveSchema);
3434
+ if (expectedEntities.length) {
3435
+ const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
3436
+ if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
3437
+ }
3438
+ out.log(` schema: ${liveEntities.length} entities`);
3439
+ const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
3440
+ if (aggregateEntity) {
3441
+ const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
3442
+ ns: aggregateEntity,
3443
+ aggregate: { count: true }
3444
+ });
3445
+ const count = aggregate.aggregate?.count;
3446
+ out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
3447
+ } else {
3448
+ out.log(` aggregate: skipped (schema has no entities)`);
3449
+ }
3405
3450
  } else {
3406
- out.log(` aggregate: skipped (schema has no entities)`);
3451
+ out.log(` db: skipped (not enabled)`);
3407
3452
  }
3408
3453
  const probes = database.integrations.flatMap(
3409
3454
  (integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
@@ -3456,7 +3501,7 @@ function publicConfigUrl(platformUrl, appId, env) {
3456
3501
  }
3457
3502
  async function safeText3(res) {
3458
3503
  try {
3459
- return (await res.text()).slice(0, 500);
3504
+ return redactSecrets((await res.text()).slice(0, 500));
3460
3505
  } catch {
3461
3506
  return "";
3462
3507
  }
@@ -3961,8 +4006,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
3961
4006
  const maxFiles = options.maxFiles ?? 2e4;
3962
4007
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
3963
4008
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
3964
- const entries = inventory.flatMap((record5) => {
3965
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record5);
4009
+ const entries = inventory.flatMap((record7) => {
4010
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record7);
3966
4011
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
3967
4012
  });
3968
4013
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -4210,8 +4255,8 @@ function normalize(value) {
4210
4255
  if (Array.isArray(value)) return value.map(normalize);
4211
4256
  if (value instanceof Uint8Array) return { $bytes: [...value] };
4212
4257
  if (typeof value === "object") {
4213
- const record5 = value;
4214
- return Object.fromEntries(Object.keys(record5).filter((key) => record5[key] !== void 0).sort().map((key) => [key, normalize(record5[key])]));
4258
+ const record7 = value;
4259
+ return Object.fromEntries(Object.keys(record7).filter((key) => record7[key] !== void 0).sort().map((key) => [key, normalize(record7[key])]));
4215
4260
  }
4216
4261
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
4217
4262
  }
@@ -6456,8 +6501,8 @@ function cliVersion() {
6456
6501
  const pkg = JSON.parse((0, import_node_fs11.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
6457
6502
  return pkg.version ?? "unknown";
6458
6503
  }
6459
- function printHelp() {
6460
- console.log(`odla-ai
6504
+ function printHelp(output = console) {
6505
+ output.log(`odla-ai
6461
6506
 
6462
6507
  Start here:
6463
6508
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -6489,22 +6534,23 @@ Usage:
6489
6534
  odla-ai app owners add <email> [--email <odla-account>] [--json]
6490
6535
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
6491
6536
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
6492
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
6537
+ odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>] [--mutation-id <id>]
6493
6538
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
6494
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
6495
- odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
6496
- odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
6539
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>] [--mutation-id <id>]
6540
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
6541
+ odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
6497
6542
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6498
6543
  odla-ai pm <goal|task|decision|bug> rm <id>
6499
6544
  odla-ai pm handoff --app <id> [--json]
6500
6545
  odla-ai discuss groups [--json]
6501
6546
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6502
6547
  odla-ai discuss read <topic> [--json]
6503
- odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
6504
- odla-ai discuss reply <topic> --body "..." [--markup "..."]
6505
- odla-ai discuss resolve <topic> [--reopen]
6548
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
6549
+ odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6550
+ odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6506
6551
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6507
6552
  odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
6553
+ odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
6508
6554
  odla-ai whoami [--json]
6509
6555
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6510
6556
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6616,6 +6662,11 @@ Commands:
6616
6662
  (not 1) when it times out with nothing new, so a script can tell
6617
6663
  "no answer yet" from a failure and just rerun. Use "who" to look
6618
6664
  up the exact @[Label](kind/id) markup for a mention.
6665
+ o11y Read one stable status envelope for application RED, current
6666
+ live-sync load/freshness, the protected commit-to-visible
6667
+ canary, collector ingest/scheduler trust, Cloudflare-owned
6668
+ runtime metrics, and a machine verdict.
6669
+ --json keeps auth progress on stderr for unattended agents.
6619
6670
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6620
6671
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6621
6672
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -7263,6 +7314,7 @@ async function codeCommand(parsed, dependencies) {
7263
7314
  }
7264
7315
 
7265
7316
  // src/discuss-actions.ts
7317
+ var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
7266
7318
  async function request(ctx, method, path, body) {
7267
7319
  const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7268
7320
  method,
@@ -7359,7 +7411,8 @@ async function discussPost(ctx, parsed) {
7359
7411
  const created = await request(ctx, "POST", "/topics", {
7360
7412
  appId,
7361
7413
  subject,
7362
- ...content(parsed)
7414
+ ...content(parsed),
7415
+ mutationId: writeMutationId(parsed)
7363
7416
  });
7364
7417
  emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
7365
7418
  }
@@ -7368,16 +7421,16 @@ async function discussReply(ctx, id, parsed) {
7368
7421
  ctx,
7369
7422
  "POST",
7370
7423
  `/topics/${encodeURIComponent(id)}/replies`,
7371
- content(parsed)
7424
+ { ...content(parsed), mutationId: writeMutationId(parsed) }
7372
7425
  );
7373
7426
  emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
7374
7427
  }
7375
- async function discussResolve(ctx, id, resolved) {
7428
+ async function discussResolve(ctx, id, resolved, parsed) {
7376
7429
  const result = await request(
7377
7430
  ctx,
7378
7431
  "PATCH",
7379
7432
  `/topics/${encodeURIComponent(id)}`,
7380
- { resolved }
7433
+ { resolved, mutationId: writeMutationId(parsed) }
7381
7434
  );
7382
7435
  emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
7383
7436
  }
@@ -7490,7 +7543,8 @@ var ALLOWED = [
7490
7543
  "by",
7491
7544
  "self",
7492
7545
  "interval",
7493
- "timeout"
7546
+ "timeout",
7547
+ "mutation-id"
7494
7548
  ];
7495
7549
  function requireId(id, action) {
7496
7550
  if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
@@ -7532,7 +7586,7 @@ async function discussCommand(parsed, deps = {}) {
7532
7586
  case "reply":
7533
7587
  return discussReply(ctx, requireId(id, "reply"), parsed);
7534
7588
  case "resolve":
7535
- return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
7589
+ return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true, parsed);
7536
7590
  case "who":
7537
7591
  return discussWho(ctx, parsed);
7538
7592
  case "watch": {
@@ -7546,6 +7600,7 @@ async function discussCommand(parsed, deps = {}) {
7546
7600
  }
7547
7601
 
7548
7602
  // src/pm-actions.ts
7603
+ var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
7549
7604
  var FIELD_MAP = {
7550
7605
  title: { key: "title" },
7551
7606
  status: { key: "status" },
@@ -7647,25 +7702,35 @@ async function pmAdd(ctx, entity, parsed) {
7647
7702
  if (!input.title) throw new Error("pm add needs --title <title>");
7648
7703
  if (entity === "bug" && !input.description)
7649
7704
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
7650
- const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
7705
+ const res = await pmRequest(ctx, "POST", `/${entity}`, {
7706
+ appId,
7707
+ input,
7708
+ mutationId: writeMutationId2(parsed)
7709
+ });
7651
7710
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7652
7711
  }
7653
7712
  async function pmGet(ctx, entity, id) {
7654
- const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7655
- emit2(ctx, record5, () => printRecord(ctx, entity, record5));
7713
+ const { record: record7 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7714
+ emit2(ctx, record7, () => printRecord(ctx, entity, record7));
7656
7715
  }
7657
7716
  async function pmSet(ctx, entity, id, parsed) {
7658
7717
  const patch2 = collectEntityFields(entity, parsed, true);
7659
7718
  if (Object.keys(patch2).length === 0)
7660
7719
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
7661
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7720
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, {
7721
+ patch: patch2,
7722
+ mutationId: writeMutationId2(parsed)
7723
+ });
7662
7724
  emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
7663
7725
  }
7664
7726
  async function pmDone(ctx, entity, id, parsed) {
7665
7727
  const decisionId = stringOpt(parsed.options.decision);
7666
7728
  if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
7667
7729
  const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
7668
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7730
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, {
7731
+ patch: patch2,
7732
+ mutationId: writeMutationId2(parsed)
7733
+ });
7669
7734
  emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7670
7735
  }
7671
7736
  async function allRecords(ctx, entity, appId) {
@@ -7691,9 +7756,9 @@ async function pmHandoff(ctx, parsed) {
7691
7756
  ]);
7692
7757
  const handoff = {
7693
7758
  appId,
7694
- unmetGoals: goals.filter((record5) => record5.status !== "met"),
7695
- activeTasks: tasks.filter((record5) => record5.column !== "done"),
7696
- openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
7759
+ unmetGoals: goals.filter((record7) => record7.status !== "met"),
7760
+ activeTasks: tasks.filter((record7) => record7.column !== "done"),
7761
+ openBugs: bugs.filter((record7) => record7.status !== "fixed" && record7.status !== "wontfix")
7697
7762
  };
7698
7763
  const result = {
7699
7764
  ...handoff,
@@ -7712,10 +7777,10 @@ async function pmHandoff(ctx, parsed) {
7712
7777
  ]) {
7713
7778
  ctx.out.log(`${label}:`);
7714
7779
  if (!records.length) ctx.out.log("- (none)");
7715
- else for (const record5 of records) printRecord(
7780
+ else for (const record7 of records) printRecord(
7716
7781
  ctx,
7717
7782
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
7718
- record5
7783
+ record7
7719
7784
  );
7720
7785
  }
7721
7786
  });
@@ -7723,7 +7788,10 @@ async function pmHandoff(ctx, parsed) {
7723
7788
  async function pmComment(ctx, entity, id, parsed) {
7724
7789
  const body = stringOpt(parsed.options.body);
7725
7790
  if (!body) throw new Error('pm comment needs --body "..."');
7726
- await pmRequest(ctx, "POST", `/${entity}/${encodeURIComponent(id)}/comments`, { body });
7791
+ await pmRequest(ctx, "POST", `/${entity}/${encodeURIComponent(id)}/comments`, {
7792
+ body,
7793
+ mutationId: writeMutationId2(parsed)
7794
+ });
7727
7795
  ctx.out.log(`commented on ${entity} ${id}`);
7728
7796
  }
7729
7797
  async function pmComments(ctx, entity, id) {
@@ -7773,7 +7841,8 @@ var ALLOWED2 = [
7773
7841
  "decision",
7774
7842
  "limit",
7775
7843
  "offset",
7776
- "q"
7844
+ "q",
7845
+ "mutation-id"
7777
7846
  ];
7778
7847
  function requireId2(id, action) {
7779
7848
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
@@ -7830,6 +7899,260 @@ async function pmCommand(parsed, deps = {}) {
7830
7899
  }
7831
7900
  }
7832
7901
 
7902
+ // src/o11y-verdict.ts
7903
+ function statusVerdict(reads) {
7904
+ const reasons = [];
7905
+ sourceHttp(reasons, "application", reads.application);
7906
+ sourceHttp(reasons, "liveSync", reads.liveSync);
7907
+ sourceHttp(reasons, "canary", reads.canary);
7908
+ sourceHttp(reasons, "collector", reads.collector);
7909
+ sourceHttp(reasons, "provider", reads.provider);
7910
+ const liveStatus = String(reads.liveSync.body.status ?? "");
7911
+ if (liveStatus === "partial") {
7912
+ reasons.push({
7913
+ source: "liveSync",
7914
+ code: "partial_reconciliation",
7915
+ severity: "degraded"
7916
+ });
7917
+ } else if (liveStatus === "stale" || liveStatus === "unavailable") {
7918
+ reasons.push({
7919
+ source: "liveSync",
7920
+ code: `reconciliation_${liveStatus}`,
7921
+ severity: "degraded"
7922
+ });
7923
+ }
7924
+ const performance = record5(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
7925
+ if (performance?.status === "unavailable") {
7926
+ reasons.push({
7927
+ source: "liveSync",
7928
+ code: "subscription_metrics_unavailable",
7929
+ severity: "degraded"
7930
+ });
7931
+ }
7932
+ if (numeric2(performance?.telemetryDropped) > 0) {
7933
+ reasons.push({
7934
+ source: "liveSync",
7935
+ code: "subscription_telemetry_dropped",
7936
+ severity: "degraded"
7937
+ });
7938
+ }
7939
+ const canaryStatus = String(reads.canary.body.status ?? "");
7940
+ if (canaryStatus === "failing") {
7941
+ reasons.push({
7942
+ source: "canary",
7943
+ code: String(reads.canary.body.errorCode ?? "journey_failed"),
7944
+ severity: "unhealthy"
7945
+ });
7946
+ } else if (canaryStatus === "stale") {
7947
+ reasons.push({
7948
+ source: "canary",
7949
+ code: "journey_stale",
7950
+ severity: "unhealthy"
7951
+ });
7952
+ } else if (canaryStatus === "pending" || canaryStatus === "not_configured") {
7953
+ reasons.push({
7954
+ source: "canary",
7955
+ code: `journey_${canaryStatus}`,
7956
+ severity: "degraded"
7957
+ });
7958
+ }
7959
+ const collectorStatus = String(reads.collector.body.status ?? "");
7960
+ if (collectorStatus === "unhealthy") {
7961
+ reasons.push({
7962
+ source: "collector",
7963
+ code: collectorReason(reads.collector, "collector_unhealthy"),
7964
+ severity: "unhealthy"
7965
+ });
7966
+ } else if (collectorStatus === "degraded") {
7967
+ reasons.push({
7968
+ source: "collector",
7969
+ code: collectorReason(reads.collector, "collector_degraded"),
7970
+ severity: "degraded"
7971
+ });
7972
+ }
7973
+ const providerStatus = String(reads.provider.body.status ?? "");
7974
+ if (providerStatus && providerStatus !== "observed") {
7975
+ reasons.push({
7976
+ source: "provider",
7977
+ code: `provider_${providerStatus}`,
7978
+ severity: "degraded"
7979
+ });
7980
+ }
7981
+ return {
7982
+ status: reasons.some((reason) => reason.severity === "unhealthy") ? "unhealthy" : reasons.length > 0 ? "degraded" : "healthy",
7983
+ reasons
7984
+ };
7985
+ }
7986
+ function record5(value) {
7987
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7988
+ }
7989
+ function numeric2(value) {
7990
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
7991
+ }
7992
+ function collectorReason(read3, fallback) {
7993
+ const reasons = read3.body.reasons;
7994
+ if (!Array.isArray(reasons)) return fallback;
7995
+ const first = reasons.find(
7996
+ (reason) => Boolean(reason) && typeof reason === "object" && !Array.isArray(reason)
7997
+ );
7998
+ return typeof first?.code === "string" && first.code ? first.code : fallback;
7999
+ }
8000
+ function sourceHttp(reasons, source, read3) {
8001
+ if (read3.httpStatus >= 200 && read3.httpStatus < 300) return;
8002
+ reasons.push({
8003
+ source,
8004
+ code: `http_${read3.httpStatus}`,
8005
+ severity: read3.httpStatus >= 500 && read3.httpStatus !== 501 ? "unhealthy" : "degraded"
8006
+ });
8007
+ }
8008
+
8009
+ // src/o11y-command.ts
8010
+ async function o11yCommand(parsed, deps = {}) {
8011
+ assertArgs(
8012
+ parsed,
8013
+ ["config", "token", "email", "json", "app", "env", "minutes"],
8014
+ 2
8015
+ );
8016
+ const action = parsed.positionals[1];
8017
+ if (action !== "status") {
8018
+ throw new Error(
8019
+ `unknown o11y action "${action ?? ""}". Try "odla-ai o11y status --json".`
8020
+ );
8021
+ }
8022
+ const minutes = statusMinutes(
8023
+ numberOpt(parsed.options.minutes, "--minutes") ?? 60
8024
+ );
8025
+ const cfg = await loadProjectConfig(
8026
+ stringOpt(parsed.options.config) ?? "odla.config.mjs"
8027
+ );
8028
+ const doFetch = deps.fetch ?? fetch;
8029
+ const out = deps.stdout ?? console;
8030
+ const token = await getDeveloperToken(
8031
+ cfg,
8032
+ {
8033
+ configPath: cfg.configPath,
8034
+ token: stringOpt(parsed.options.token),
8035
+ email: stringOpt(parsed.options.email),
8036
+ open: false
8037
+ },
8038
+ doFetch,
8039
+ out
8040
+ );
8041
+ const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
8042
+ const env = stringOpt(parsed.options.env)?.trim() || "prod";
8043
+ const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
8044
+ const query = new URLSearchParams({ env, minutes: String(minutes) });
8045
+ const headers = { authorization: `Bearer ${token}` };
8046
+ const [application, liveSync, canary, collector, provider] = await Promise.all([
8047
+ read2(`${base}/metrics/red?${query}`, headers, doFetch),
8048
+ read2(
8049
+ `${base}/live-sync/health?${query}`,
8050
+ headers,
8051
+ doFetch
8052
+ ),
8053
+ read2(
8054
+ `${base}/live-sync/canary?${new URLSearchParams({ env })}`,
8055
+ headers,
8056
+ doFetch
8057
+ ),
8058
+ read2(`${base}/self-health?${query}`, headers, doFetch),
8059
+ read2(`${base}/provider/cloudflare?${query}`, headers, doFetch)
8060
+ ]);
8061
+ const verdict = statusVerdict({
8062
+ application,
8063
+ liveSync,
8064
+ canary,
8065
+ collector,
8066
+ provider
8067
+ });
8068
+ const status = {
8069
+ schemaVersion: 4,
8070
+ scope: { appId, env, minutes },
8071
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
8072
+ verdict,
8073
+ application,
8074
+ liveSync,
8075
+ canary,
8076
+ collector,
8077
+ provider
8078
+ };
8079
+ if (parsed.options.json === true) {
8080
+ out.log(JSON.stringify(status, null, 2));
8081
+ return;
8082
+ }
8083
+ printStatus2(status, out);
8084
+ }
8085
+ function statusMinutes(value) {
8086
+ if (!Number.isSafeInteger(value) || value < 1 || value > 7 * 24 * 60) {
8087
+ throw new Error("--minutes must be an integer from 1 to 10080");
8088
+ }
8089
+ return value;
8090
+ }
8091
+ async function read2(url, headers, doFetch) {
8092
+ const response2 = await doFetch(url, { headers });
8093
+ const text = await response2.text();
8094
+ let body = {};
8095
+ if (text) {
8096
+ try {
8097
+ const value = JSON.parse(text);
8098
+ body = value && typeof value === "object" && !Array.isArray(value) ? value : { value };
8099
+ } catch {
8100
+ body = { message: text.slice(0, 300) };
8101
+ }
8102
+ }
8103
+ return { httpStatus: response2.status, body };
8104
+ }
8105
+ function printStatus2(status, out) {
8106
+ out.log(
8107
+ `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
8108
+ );
8109
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
8110
+ const requests = routes.reduce(
8111
+ (total, row) => total + numeric3(row.requests),
8112
+ 0
8113
+ );
8114
+ const errors = routes.reduce(
8115
+ (total, row) => total + numeric3(row.errors),
8116
+ 0
8117
+ );
8118
+ out.log(
8119
+ `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
8120
+ );
8121
+ out.log(
8122
+ liveSyncLine(status.liveSync)
8123
+ );
8124
+ const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
8125
+ out.log(
8126
+ `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
8127
+ );
8128
+ const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
8129
+ const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
8130
+ out.log(
8131
+ `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
8132
+ );
8133
+ const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
8134
+ out.log(
8135
+ `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
8136
+ );
8137
+ out.log(
8138
+ `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8139
+ );
8140
+ }
8141
+ function liveSyncLine(read3) {
8142
+ const performance = record6(read3.body.performance) ? read3.body.performance : {};
8143
+ const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
8144
+ 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`;
8145
+ }
8146
+ function record6(value) {
8147
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8148
+ }
8149
+ function numeric3(value) {
8150
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
8151
+ }
8152
+ function optionalNumeric(value) {
8153
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
8154
+ }
8155
+
7833
8156
  // src/provision.ts
7834
8157
  var import_apps5 = require("@odla-ai/apps");
7835
8158
  var import_ai3 = require("@odla-ai/ai");
@@ -8007,15 +8330,6 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
8007
8330
  }
8008
8331
  throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
8009
8332
  }
8010
- async function readRegistryApp(cfg, token, doFetch) {
8011
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
8012
- headers: { authorization: `Bearer ${token}` }
8013
- });
8014
- if (res.status === 404) return null;
8015
- if (!res.ok) return null;
8016
- const json = await res.json();
8017
- return json.app ?? null;
8018
- }
8019
8333
  async function postJson3(doFetch, url, bearer, body) {
8020
8334
  const res = await doFetch(url, {
8021
8335
  method: "POST",
@@ -8114,7 +8428,7 @@ async function provision(options) {
8114
8428
  const doFetch = options.fetch ?? fetch;
8115
8429
  const token = await getDeveloperToken(cfg, options, doFetch, out);
8116
8430
  const apps = (0, import_apps5.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
8117
- const existing = await readRegistryApp(cfg, token, doFetch);
8431
+ const existing = await apps.resolveApp(cfg.app.id);
8118
8432
  if (existing) {
8119
8433
  out.log(`app: ${cfg.app.id} already exists`);
8120
8434
  } else {
@@ -8305,6 +8619,7 @@ var COMMAND_SURFACE = {
8305
8619
  doctor: {},
8306
8620
  help: {},
8307
8621
  init: {},
8622
+ o11y: { status: {} },
8308
8623
  pm: {
8309
8624
  ...PM_ENTITIES,
8310
8625
  handoff: {}
@@ -8814,7 +9129,7 @@ var SOURCE2 = /\.(ts|tsx|js|jsx|mts|cts)$/;
8814
9129
  function gitRunner(cwd) {
8815
9130
  return (args) => (0, import_node_child_process7.execFileSync)("git", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
8816
9131
  }
8817
- function collectDiff(runGit, base, read2) {
9132
+ function collectDiff(runGit, base, read3) {
8818
9133
  let merged = "";
8819
9134
  try {
8820
9135
  merged += runGit(["diff", "--unified=3", `${base}...HEAD`]);
@@ -8827,9 +9142,9 @@ function collectDiff(runGit, base, read2) {
8827
9142
  merged += runGit(["diff", "--unified=3", "HEAD"]);
8828
9143
  } catch {
8829
9144
  }
8830
- return merged + untrackedDiff(runGit, read2);
9145
+ return merged + untrackedDiff(runGit, read3);
8831
9146
  }
8832
- function untrackedDiff(runGit, read2) {
9147
+ function untrackedDiff(runGit, read3) {
8833
9148
  let listed = "";
8834
9149
  try {
8835
9150
  listed = runGit(["ls-files", "--others", "--exclude-standard"]);
@@ -8845,7 +9160,7 @@ function untrackedDiff(runGit, read2) {
8845
9160
  if (!SOURCE2.test(path)) continue;
8846
9161
  let body;
8847
9162
  try {
8848
- body = read2(path);
9163
+ body = read3(path);
8849
9164
  } catch {
8850
9165
  continue;
8851
9166
  }
@@ -8927,8 +9242,8 @@ function report3(ctx, impacts) {
8927
9242
  async function runbookImpact(ctx, options, deps = {}) {
8928
9243
  const cwd = deps.cwd ?? process.cwd();
8929
9244
  const runGit = deps.runGit ?? gitRunner(cwd);
8930
- const read2 = deps.readRepoFile ?? ((path) => (0, import_node_fs16.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
8931
- const surfaces = changedSurfaces(collectDiff(runGit, options.base, read2), manifestLabeller(cwd));
9245
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs16.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
9246
+ const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
8932
9247
  if (!surfaces.length) {
8933
9248
  return ctx.out.log(
8934
9249
  ctx.json ? JSON.stringify({ base: options.base, impacts: [] }, null, 2) : `no changes against ${options.base}`
@@ -10062,33 +10377,37 @@ function exitCodeFor(err) {
10062
10377
  return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
10063
10378
  }
10064
10379
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10380
+ const runtime = {
10381
+ ...dependencies,
10382
+ stdout: redactingOutput(dependencies.stdout ?? console)
10383
+ };
10065
10384
  const parsed = parseArgv(argv);
10066
10385
  recordInvocation(parsed);
10067
10386
  const command = parsed.positionals[0] ?? "help";
10068
10387
  if (command === "version" || command === "-v" || parsed.options.version === true) {
10069
10388
  assertArgs(parsed, ["version"], 1);
10070
- console.log(cliVersion());
10389
+ runtime.stdout.log(cliVersion());
10071
10390
  return;
10072
10391
  }
10073
10392
  if (command === "help" || command === "--help" || command === "-h") {
10074
10393
  assertArgs(parsed, ["help"], 1);
10075
- printHelp();
10394
+ printHelp(runtime.stdout);
10076
10395
  return;
10077
10396
  }
10078
10397
  if (command === "whoami") {
10079
- await whoamiCommand(parsed, dependencies);
10398
+ await whoamiCommand(parsed, runtime);
10080
10399
  return;
10081
10400
  }
10082
10401
  if (command === "runbook") {
10083
- await runbookCommand(parsed, dependencies);
10402
+ await runbookCommand(parsed, runtime);
10084
10403
  return;
10085
10404
  }
10086
10405
  if (command === "calendar") {
10087
- await calendarCommand(parsed, dependencies);
10406
+ await calendarCommand(parsed, runtime);
10088
10407
  return;
10089
10408
  }
10090
10409
  if (command === "code") {
10091
- await codeCommand(parsed, dependencies);
10410
+ await codeCommand(parsed, runtime);
10092
10411
  return;
10093
10412
  }
10094
10413
  if (command === "admin") {
@@ -10096,26 +10415,30 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10096
10415
  return;
10097
10416
  }
10098
10417
  if (command === "app") {
10099
- await appCommand(parsed, dependencies);
10418
+ await appCommand(parsed, runtime);
10100
10419
  return;
10101
10420
  }
10102
10421
  if (command === "security") {
10103
- await securityCommand(parsed, dependencies);
10422
+ await securityCommand(parsed, runtime);
10104
10423
  return;
10105
10424
  }
10106
10425
  if (command === "pm") {
10107
- await pmCommand(parsed, dependencies);
10426
+ await pmCommand(parsed, runtime);
10108
10427
  return;
10109
10428
  }
10110
10429
  if (command === "discuss") {
10111
- await discussCommand(parsed, dependencies);
10430
+ await discussCommand(parsed, runtime);
10431
+ return;
10432
+ }
10433
+ if (command === "o11y") {
10434
+ await o11yCommand(parsed, runtime);
10112
10435
  return;
10113
10436
  }
10114
10437
  if (command === "provision") {
10115
- await provisionCommand(parsed, dependencies);
10438
+ await provisionCommand(parsed, runtime);
10116
10439
  return;
10117
10440
  }
10118
- if (await projectCommand(command, parsed, dependencies)) return;
10441
+ if (await projectCommand(command, parsed, runtime)) return;
10119
10442
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
10120
10443
  }
10121
10444
  async function provisionCommand(parsed, dependencies) {