@odla-ai/cli 0.25.2 → 0.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -1875,6 +1875,37 @@ function redactSecrets(value) {
1875
1875
  for (const [pattern, replacement] of REPLACEMENTS) result = result.replace(pattern, replacement);
1876
1876
  return result;
1877
1877
  }
1878
+ function redactingOutput(output) {
1879
+ const redactArgs = (args) => args.map((value) => redactOutputValue(value, /* @__PURE__ */ new WeakMap()));
1880
+ return {
1881
+ log: (...args) => output.log(...redactArgs(args)),
1882
+ error: (...args) => output.error(...redactArgs(args))
1883
+ };
1884
+ }
1885
+ function redactOutputValue(value, seen) {
1886
+ if (typeof value === "string") return redactSecrets(value);
1887
+ if (value instanceof Error) {
1888
+ const redacted = new Error(redactSecrets(value.message));
1889
+ redacted.name = value.name;
1890
+ if (value.stack) redacted.stack = redactSecrets(value.stack);
1891
+ return redacted;
1892
+ }
1893
+ if (!value || typeof value !== "object") return value;
1894
+ const prior = seen.get(value);
1895
+ if (prior) return prior;
1896
+ if (Array.isArray(value)) {
1897
+ const next2 = [];
1898
+ seen.set(value, next2);
1899
+ for (const item of value) next2.push(redactOutputValue(item, seen));
1900
+ return next2;
1901
+ }
1902
+ const prototype = Object.getPrototypeOf(value);
1903
+ if (prototype !== Object.prototype && prototype !== null) return value;
1904
+ const next = {};
1905
+ seen.set(value, next);
1906
+ for (const [key, item] of Object.entries(value)) next[key] = redactOutputValue(item, seen);
1907
+ return next;
1908
+ }
1878
1909
  function looksSecret(value) {
1879
1910
  return redactSecrets(value) !== value || value.includes("-----BEGIN");
1880
1911
  }
@@ -2279,6 +2310,7 @@ var CAPABILITIES = {
2279
2310
  "compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
2280
2311
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
2281
2312
  "validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
2313
+ "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",
2282
2314
  "run app-attributed hosted security discovery and independent validation without provider keys",
2283
2315
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
2284
2316
  "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
@@ -2299,7 +2331,7 @@ var CAPABILITIES = {
2299
2331
  "approve GitHub App repository access separately from redacted-snippet disclosure"
2300
2332
  ],
2301
2333
  studio: [
2302
- "view telemetry and environment state",
2334
+ "view application telemetry, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
2303
2335
  "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
2304
2336
  "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
2305
2337
  "perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
@@ -3291,9 +3323,17 @@ async function smoke(options) {
3291
3323
  throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
3292
3324
  }
3293
3325
  const entry = credentials.envs[env];
3294
- if (!entry?.tenantId || !entry.dbKey) {
3326
+ if (!entry?.tenantId) {
3327
+ throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
3328
+ }
3329
+ const hasDb = cfg.services.includes("db");
3330
+ const hasO11y = cfg.services.includes("o11y");
3331
+ if (hasDb && !entry.dbKey) {
3295
3332
  throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
3296
3333
  }
3334
+ if (hasO11y && !entry.o11yToken) {
3335
+ throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
3336
+ }
3297
3337
  const doFetch = options.fetch ?? fetch;
3298
3338
  out.log(`smoke: ${cfg.app.id}/${env}`);
3299
3339
  out.log(` tenant: ${entry.tenantId}`);
@@ -3306,6 +3346,7 @@ async function smoke(options) {
3306
3346
  }
3307
3347
  out.log(` ai: ${provider}`);
3308
3348
  }
3349
+ if (hasO11y) out.log(` o11y: credentials present`);
3309
3350
  if (cfg.services.includes("calendar")) {
3310
3351
  const token = await getDeveloperToken(
3311
3352
  cfg,
@@ -3324,26 +3365,30 @@ async function smoke(options) {
3324
3365
  out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
3325
3366
  }
3326
3367
  const database = await resolveDatabaseConfig(cfg);
3327
- const expectedSchema = database.schema;
3328
- const expectedEntities = serializedEntities(expectedSchema);
3329
- const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
3330
- const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
3331
- const liveEntities = serializedEntities(liveSchema);
3332
- if (expectedEntities.length) {
3333
- const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
3334
- if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
3335
- }
3336
- out.log(` schema: ${liveEntities.length} entities`);
3337
- const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
3338
- if (aggregateEntity) {
3339
- const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
3340
- ns: aggregateEntity,
3341
- aggregate: { count: true }
3342
- });
3343
- const count = aggregate.aggregate?.count;
3344
- out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
3368
+ if (hasDb) {
3369
+ const expectedSchema = database.schema;
3370
+ const expectedEntities = serializedEntities(expectedSchema);
3371
+ const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
3372
+ const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
3373
+ const liveEntities = serializedEntities(liveSchema);
3374
+ if (expectedEntities.length) {
3375
+ const missing = expectedEntities.filter((entity) => !liveEntities.includes(entity));
3376
+ if (missing.length) throw new Error(`live schema is missing expected entities: ${missing.join(", ")}`);
3377
+ }
3378
+ out.log(` schema: ${liveEntities.length} entities`);
3379
+ const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
3380
+ if (aggregateEntity) {
3381
+ const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
3382
+ ns: aggregateEntity,
3383
+ aggregate: { count: true }
3384
+ });
3385
+ const count = aggregate.aggregate?.count;
3386
+ out.log(` aggregate: ${aggregateEntity}.count=${String(count ?? "ok")}`);
3387
+ } else {
3388
+ out.log(` aggregate: skipped (schema has no entities)`);
3389
+ }
3345
3390
  } else {
3346
- out.log(` aggregate: skipped (schema has no entities)`);
3391
+ out.log(` db: skipped (not enabled)`);
3347
3392
  }
3348
3393
  const probes = database.integrations.flatMap(
3349
3394
  (integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
@@ -3396,7 +3441,7 @@ function publicConfigUrl(platformUrl, appId, env) {
3396
3441
  }
3397
3442
  async function safeText3(res) {
3398
3443
  try {
3399
- return (await res.text()).slice(0, 500);
3444
+ return redactSecrets((await res.text()).slice(0, 500));
3400
3445
  } catch {
3401
3446
  return "";
3402
3447
  }
@@ -3901,8 +3946,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
3901
3946
  const maxFiles = options.maxFiles ?? 2e4;
3902
3947
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
3903
3948
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
3904
- const entries = inventory.flatMap((record5) => {
3905
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record5);
3949
+ const entries = inventory.flatMap((record6) => {
3950
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record6);
3906
3951
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
3907
3952
  });
3908
3953
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -4150,8 +4195,8 @@ function normalize(value) {
4150
4195
  if (Array.isArray(value)) return value.map(normalize);
4151
4196
  if (value instanceof Uint8Array) return { $bytes: [...value] };
4152
4197
  if (typeof value === "object") {
4153
- const record5 = value;
4154
- return Object.fromEntries(Object.keys(record5).filter((key) => record5[key] !== void 0).sort().map((key) => [key, normalize(record5[key])]));
4198
+ const record6 = value;
4199
+ return Object.fromEntries(Object.keys(record6).filter((key) => record6[key] !== void 0).sort().map((key) => [key, normalize(record6[key])]));
4155
4200
  }
4156
4201
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
4157
4202
  }
@@ -6396,8 +6441,8 @@ function cliVersion() {
6396
6441
  const pkg = JSON.parse((0, import_node_fs11.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
6397
6442
  return pkg.version ?? "unknown";
6398
6443
  }
6399
- function printHelp() {
6400
- console.log(`odla-ai
6444
+ function printHelp(output = console) {
6445
+ output.log(`odla-ai
6401
6446
 
6402
6447
  Start here:
6403
6448
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -6429,22 +6474,23 @@ Usage:
6429
6474
  odla-ai app owners add <email> [--email <odla-account>] [--json]
6430
6475
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
6431
6476
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
6432
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
6477
+ 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>]
6433
6478
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
6434
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
6435
- odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
6436
- odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
6479
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>] [--mutation-id <id>]
6480
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
6481
+ odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
6437
6482
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6438
6483
  odla-ai pm <goal|task|decision|bug> rm <id>
6439
6484
  odla-ai pm handoff --app <id> [--json]
6440
6485
  odla-ai discuss groups [--json]
6441
6486
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6442
6487
  odla-ai discuss read <topic> [--json]
6443
- odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
6444
- odla-ai discuss reply <topic> --body "..." [--markup "..."]
6445
- odla-ai discuss resolve <topic> [--reopen]
6488
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
6489
+ odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6490
+ odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6446
6491
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6447
6492
  odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
6493
+ odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
6448
6494
  odla-ai whoami [--json]
6449
6495
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6450
6496
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6556,6 +6602,11 @@ Commands:
6556
6602
  (not 1) when it times out with nothing new, so a script can tell
6557
6603
  "no answer yet" from a failure and just rerun. Use "who" to look
6558
6604
  up the exact @[Label](kind/id) markup for a mention.
6605
+ o11y Read one stable status envelope for application RED, current
6606
+ live-sync load/freshness, the protected commit-to-visible
6607
+ canary, collector ingest/scheduler trust, Cloudflare-owned
6608
+ runtime metrics, and a machine verdict.
6609
+ --json keeps auth progress on stderr for unattended agents.
6559
6610
  provision Register services, compose integrations, persist credentials, optionally push secrets.
6560
6611
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6561
6612
  skill Same installer; --agent accepts all, claude, codex, cursor,
@@ -7203,6 +7254,7 @@ async function codeCommand(parsed, dependencies) {
7203
7254
  }
7204
7255
 
7205
7256
  // src/discuss-actions.ts
7257
+ var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
7206
7258
  async function request(ctx, method, path, body) {
7207
7259
  const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7208
7260
  method,
@@ -7299,7 +7351,8 @@ async function discussPost(ctx, parsed) {
7299
7351
  const created = await request(ctx, "POST", "/topics", {
7300
7352
  appId,
7301
7353
  subject,
7302
- ...content(parsed)
7354
+ ...content(parsed),
7355
+ mutationId: writeMutationId(parsed)
7303
7356
  });
7304
7357
  emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
7305
7358
  }
@@ -7308,16 +7361,16 @@ async function discussReply(ctx, id, parsed) {
7308
7361
  ctx,
7309
7362
  "POST",
7310
7363
  `/topics/${encodeURIComponent(id)}/replies`,
7311
- content(parsed)
7364
+ { ...content(parsed), mutationId: writeMutationId(parsed) }
7312
7365
  );
7313
7366
  emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
7314
7367
  }
7315
- async function discussResolve(ctx, id, resolved) {
7368
+ async function discussResolve(ctx, id, resolved, parsed) {
7316
7369
  const result = await request(
7317
7370
  ctx,
7318
7371
  "PATCH",
7319
7372
  `/topics/${encodeURIComponent(id)}`,
7320
- { resolved }
7373
+ { resolved, mutationId: writeMutationId(parsed) }
7321
7374
  );
7322
7375
  emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
7323
7376
  }
@@ -7430,7 +7483,8 @@ var ALLOWED = [
7430
7483
  "by",
7431
7484
  "self",
7432
7485
  "interval",
7433
- "timeout"
7486
+ "timeout",
7487
+ "mutation-id"
7434
7488
  ];
7435
7489
  function requireId(id, action) {
7436
7490
  if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
@@ -7472,7 +7526,7 @@ async function discussCommand(parsed, deps = {}) {
7472
7526
  case "reply":
7473
7527
  return discussReply(ctx, requireId(id, "reply"), parsed);
7474
7528
  case "resolve":
7475
- return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
7529
+ return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true, parsed);
7476
7530
  case "who":
7477
7531
  return discussWho(ctx, parsed);
7478
7532
  case "watch": {
@@ -7486,6 +7540,7 @@ async function discussCommand(parsed, deps = {}) {
7486
7540
  }
7487
7541
 
7488
7542
  // src/pm-actions.ts
7543
+ var writeMutationId2 = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
7489
7544
  var FIELD_MAP = {
7490
7545
  title: { key: "title" },
7491
7546
  status: { key: "status" },
@@ -7587,25 +7642,35 @@ async function pmAdd(ctx, entity, parsed) {
7587
7642
  if (!input.title) throw new Error("pm add needs --title <title>");
7588
7643
  if (entity === "bug" && !input.description)
7589
7644
  throw new Error("pm bug add needs --description <context> (or --body <context>)");
7590
- const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
7645
+ const res = await pmRequest(ctx, "POST", `/${entity}`, {
7646
+ appId,
7647
+ input,
7648
+ mutationId: writeMutationId2(parsed)
7649
+ });
7591
7650
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7592
7651
  }
7593
7652
  async function pmGet(ctx, entity, id) {
7594
- const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7595
- emit2(ctx, record5, () => printRecord(ctx, entity, record5));
7653
+ const { record: record6 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7654
+ emit2(ctx, record6, () => printRecord(ctx, entity, record6));
7596
7655
  }
7597
7656
  async function pmSet(ctx, entity, id, parsed) {
7598
7657
  const patch2 = collectEntityFields(entity, parsed, true);
7599
7658
  if (Object.keys(patch2).length === 0)
7600
7659
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
7601
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7660
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, {
7661
+ patch: patch2,
7662
+ mutationId: writeMutationId2(parsed)
7663
+ });
7602
7664
  emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
7603
7665
  }
7604
7666
  async function pmDone(ctx, entity, id, parsed) {
7605
7667
  const decisionId = stringOpt(parsed.options.decision);
7606
7668
  if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
7607
7669
  const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
7608
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
7670
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, {
7671
+ patch: patch2,
7672
+ mutationId: writeMutationId2(parsed)
7673
+ });
7609
7674
  emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
7610
7675
  }
7611
7676
  async function allRecords(ctx, entity, appId) {
@@ -7631,9 +7696,9 @@ async function pmHandoff(ctx, parsed) {
7631
7696
  ]);
7632
7697
  const handoff = {
7633
7698
  appId,
7634
- unmetGoals: goals.filter((record5) => record5.status !== "met"),
7635
- activeTasks: tasks.filter((record5) => record5.column !== "done"),
7636
- openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
7699
+ unmetGoals: goals.filter((record6) => record6.status !== "met"),
7700
+ activeTasks: tasks.filter((record6) => record6.column !== "done"),
7701
+ openBugs: bugs.filter((record6) => record6.status !== "fixed" && record6.status !== "wontfix")
7637
7702
  };
7638
7703
  const result = {
7639
7704
  ...handoff,
@@ -7652,10 +7717,10 @@ async function pmHandoff(ctx, parsed) {
7652
7717
  ]) {
7653
7718
  ctx.out.log(`${label}:`);
7654
7719
  if (!records.length) ctx.out.log("- (none)");
7655
- else for (const record5 of records) printRecord(
7720
+ else for (const record6 of records) printRecord(
7656
7721
  ctx,
7657
7722
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
7658
- record5
7723
+ record6
7659
7724
  );
7660
7725
  }
7661
7726
  });
@@ -7663,7 +7728,10 @@ async function pmHandoff(ctx, parsed) {
7663
7728
  async function pmComment(ctx, entity, id, parsed) {
7664
7729
  const body = stringOpt(parsed.options.body);
7665
7730
  if (!body) throw new Error('pm comment needs --body "..."');
7666
- await pmRequest(ctx, "POST", `/${entity}/${encodeURIComponent(id)}/comments`, { body });
7731
+ await pmRequest(ctx, "POST", `/${entity}/${encodeURIComponent(id)}/comments`, {
7732
+ body,
7733
+ mutationId: writeMutationId2(parsed)
7734
+ });
7667
7735
  ctx.out.log(`commented on ${entity} ${id}`);
7668
7736
  }
7669
7737
  async function pmComments(ctx, entity, id) {
@@ -7713,7 +7781,8 @@ var ALLOWED2 = [
7713
7781
  "decision",
7714
7782
  "limit",
7715
7783
  "offset",
7716
- "q"
7784
+ "q",
7785
+ "mutation-id"
7717
7786
  ];
7718
7787
  function requireId2(id, action) {
7719
7788
  if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
@@ -7770,6 +7839,234 @@ async function pmCommand(parsed, deps = {}) {
7770
7839
  }
7771
7840
  }
7772
7841
 
7842
+ // src/o11y-verdict.ts
7843
+ function statusVerdict(reads) {
7844
+ const reasons = [];
7845
+ sourceHttp(reasons, "application", reads.application);
7846
+ sourceHttp(reasons, "liveSync", reads.liveSync);
7847
+ sourceHttp(reasons, "canary", reads.canary);
7848
+ sourceHttp(reasons, "collector", reads.collector);
7849
+ sourceHttp(reasons, "provider", reads.provider);
7850
+ const liveStatus = String(reads.liveSync.body.status ?? "");
7851
+ if (liveStatus === "partial") {
7852
+ reasons.push({
7853
+ source: "liveSync",
7854
+ code: "partial_reconciliation",
7855
+ severity: "degraded"
7856
+ });
7857
+ } else if (liveStatus === "stale" || liveStatus === "unavailable") {
7858
+ reasons.push({
7859
+ source: "liveSync",
7860
+ code: `reconciliation_${liveStatus}`,
7861
+ severity: "degraded"
7862
+ });
7863
+ }
7864
+ const canaryStatus = String(reads.canary.body.status ?? "");
7865
+ if (canaryStatus === "failing") {
7866
+ reasons.push({
7867
+ source: "canary",
7868
+ code: String(reads.canary.body.errorCode ?? "journey_failed"),
7869
+ severity: "unhealthy"
7870
+ });
7871
+ } else if (canaryStatus === "stale") {
7872
+ reasons.push({
7873
+ source: "canary",
7874
+ code: "journey_stale",
7875
+ severity: "unhealthy"
7876
+ });
7877
+ } else if (canaryStatus === "pending" || canaryStatus === "not_configured") {
7878
+ reasons.push({
7879
+ source: "canary",
7880
+ code: `journey_${canaryStatus}`,
7881
+ severity: "degraded"
7882
+ });
7883
+ }
7884
+ const collectorStatus = String(reads.collector.body.status ?? "");
7885
+ if (collectorStatus === "unhealthy") {
7886
+ reasons.push({
7887
+ source: "collector",
7888
+ code: collectorReason(reads.collector, "collector_unhealthy"),
7889
+ severity: "unhealthy"
7890
+ });
7891
+ } else if (collectorStatus === "degraded") {
7892
+ reasons.push({
7893
+ source: "collector",
7894
+ code: collectorReason(reads.collector, "collector_degraded"),
7895
+ severity: "degraded"
7896
+ });
7897
+ }
7898
+ const providerStatus = String(reads.provider.body.status ?? "");
7899
+ if (providerStatus && providerStatus !== "observed") {
7900
+ reasons.push({
7901
+ source: "provider",
7902
+ code: `provider_${providerStatus}`,
7903
+ severity: "degraded"
7904
+ });
7905
+ }
7906
+ return {
7907
+ status: reasons.some((reason) => reason.severity === "unhealthy") ? "unhealthy" : reasons.length > 0 ? "degraded" : "healthy",
7908
+ reasons
7909
+ };
7910
+ }
7911
+ function collectorReason(read3, fallback) {
7912
+ const reasons = read3.body.reasons;
7913
+ if (!Array.isArray(reasons)) return fallback;
7914
+ const first = reasons.find(
7915
+ (reason) => Boolean(reason) && typeof reason === "object" && !Array.isArray(reason)
7916
+ );
7917
+ return typeof first?.code === "string" && first.code ? first.code : fallback;
7918
+ }
7919
+ function sourceHttp(reasons, source, read3) {
7920
+ if (read3.httpStatus >= 200 && read3.httpStatus < 300) return;
7921
+ reasons.push({
7922
+ source,
7923
+ code: `http_${read3.httpStatus}`,
7924
+ severity: read3.httpStatus >= 500 && read3.httpStatus !== 501 ? "unhealthy" : "degraded"
7925
+ });
7926
+ }
7927
+
7928
+ // src/o11y-command.ts
7929
+ async function o11yCommand(parsed, deps = {}) {
7930
+ assertArgs(
7931
+ parsed,
7932
+ ["config", "token", "email", "json", "app", "env", "minutes"],
7933
+ 2
7934
+ );
7935
+ const action = parsed.positionals[1];
7936
+ if (action !== "status") {
7937
+ throw new Error(
7938
+ `unknown o11y action "${action ?? ""}". Try "odla-ai o11y status --json".`
7939
+ );
7940
+ }
7941
+ const minutes = statusMinutes(
7942
+ numberOpt(parsed.options.minutes, "--minutes") ?? 60
7943
+ );
7944
+ const cfg = await loadProjectConfig(
7945
+ stringOpt(parsed.options.config) ?? "odla.config.mjs"
7946
+ );
7947
+ const doFetch = deps.fetch ?? fetch;
7948
+ const out = deps.stdout ?? console;
7949
+ const token = await getDeveloperToken(
7950
+ cfg,
7951
+ {
7952
+ configPath: cfg.configPath,
7953
+ token: stringOpt(parsed.options.token),
7954
+ email: stringOpt(parsed.options.email),
7955
+ open: false
7956
+ },
7957
+ doFetch,
7958
+ out
7959
+ );
7960
+ const appId = stringOpt(parsed.options.app)?.trim() || cfg.app.id;
7961
+ const env = stringOpt(parsed.options.env)?.trim() || "prod";
7962
+ const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
7963
+ const query = new URLSearchParams({ env, minutes: String(minutes) });
7964
+ const headers = { authorization: `Bearer ${token}` };
7965
+ const [application, liveSync, canary, collector, provider] = await Promise.all([
7966
+ read2(`${base}/metrics/red?${query}`, headers, doFetch),
7967
+ read2(
7968
+ `${base}/live-sync/health?${new URLSearchParams({ env })}`,
7969
+ headers,
7970
+ doFetch
7971
+ ),
7972
+ read2(
7973
+ `${base}/live-sync/canary?${new URLSearchParams({ env })}`,
7974
+ headers,
7975
+ doFetch
7976
+ ),
7977
+ read2(`${base}/self-health?${query}`, headers, doFetch),
7978
+ read2(`${base}/provider/cloudflare?${query}`, headers, doFetch)
7979
+ ]);
7980
+ const verdict = statusVerdict({
7981
+ application,
7982
+ liveSync,
7983
+ canary,
7984
+ collector,
7985
+ provider
7986
+ });
7987
+ const status = {
7988
+ schemaVersion: 3,
7989
+ scope: { appId, env, minutes },
7990
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
7991
+ verdict,
7992
+ application,
7993
+ liveSync,
7994
+ canary,
7995
+ collector,
7996
+ provider
7997
+ };
7998
+ if (parsed.options.json === true) {
7999
+ out.log(JSON.stringify(status, null, 2));
8000
+ return;
8001
+ }
8002
+ printStatus2(status, out);
8003
+ }
8004
+ function statusMinutes(value) {
8005
+ if (!Number.isSafeInteger(value) || value < 1 || value > 7 * 24 * 60) {
8006
+ throw new Error("--minutes must be an integer from 1 to 10080");
8007
+ }
8008
+ return value;
8009
+ }
8010
+ async function read2(url, headers, doFetch) {
8011
+ const response2 = await doFetch(url, { headers });
8012
+ const text = await response2.text();
8013
+ let body = {};
8014
+ if (text) {
8015
+ try {
8016
+ const value = JSON.parse(text);
8017
+ body = value && typeof value === "object" && !Array.isArray(value) ? value : { value };
8018
+ } catch {
8019
+ body = { message: text.slice(0, 300) };
8020
+ }
8021
+ }
8022
+ return { httpStatus: response2.status, body };
8023
+ }
8024
+ function printStatus2(status, out) {
8025
+ out.log(
8026
+ `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
8027
+ );
8028
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record5) : [];
8029
+ const requests = routes.reduce(
8030
+ (total, row) => total + numeric2(row.requests),
8031
+ 0
8032
+ );
8033
+ const errors = routes.reduce(
8034
+ (total, row) => total + numeric2(row.errors),
8035
+ 0
8036
+ );
8037
+ out.log(
8038
+ `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
8039
+ );
8040
+ out.log(
8041
+ `live-sync ${status.liveSync.httpStatus} ${String(status.liveSync.body.status ?? status.liveSync.body.error ?? "unavailable")} ${numeric2(status.liveSync.body.activeConnections)} active`
8042
+ );
8043
+ const canaryDurations = record5(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
8044
+ out.log(
8045
+ `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
8046
+ );
8047
+ const collectorIngest = record5(status.collector.body.ingest) ? status.collector.body.ingest : {};
8048
+ const collectorStorage = record5(collectorIngest.storage) ? collectorIngest.storage : {};
8049
+ out.log(
8050
+ `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric2(collectorStorage.affectedPoints)} affected points`
8051
+ );
8052
+ const providerMetrics = record5(status.provider.body.metrics) ? status.provider.body.metrics : {};
8053
+ out.log(
8054
+ `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric2(providerMetrics.requests)} invocations ${numeric2(providerMetrics.errors)} runtime errors`
8055
+ );
8056
+ out.log(
8057
+ `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8058
+ );
8059
+ }
8060
+ function record5(value) {
8061
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8062
+ }
8063
+ function numeric2(value) {
8064
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
8065
+ }
8066
+ function optionalNumeric(value) {
8067
+ return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
8068
+ }
8069
+
7773
8070
  // src/provision.ts
7774
8071
  var import_apps5 = require("@odla-ai/apps");
7775
8072
  var import_ai3 = require("@odla-ai/ai");
@@ -7947,15 +8244,6 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
7947
8244
  }
7948
8245
  throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
7949
8246
  }
7950
- async function readRegistryApp(cfg, token, doFetch) {
7951
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
7952
- headers: { authorization: `Bearer ${token}` }
7953
- });
7954
- if (res.status === 404) return null;
7955
- if (!res.ok) return null;
7956
- const json = await res.json();
7957
- return json.app ?? null;
7958
- }
7959
8247
  async function postJson3(doFetch, url, bearer, body) {
7960
8248
  const res = await doFetch(url, {
7961
8249
  method: "POST",
@@ -8054,7 +8342,7 @@ async function provision(options) {
8054
8342
  const doFetch = options.fetch ?? fetch;
8055
8343
  const token = await getDeveloperToken(cfg, options, doFetch, out);
8056
8344
  const apps = (0, import_apps5.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
8057
- const existing = await readRegistryApp(cfg, token, doFetch);
8345
+ const existing = await apps.resolveApp(cfg.app.id);
8058
8346
  if (existing) {
8059
8347
  out.log(`app: ${cfg.app.id} already exists`);
8060
8348
  } else {
@@ -8245,6 +8533,7 @@ var COMMAND_SURFACE = {
8245
8533
  doctor: {},
8246
8534
  help: {},
8247
8535
  init: {},
8536
+ o11y: { status: {} },
8248
8537
  pm: {
8249
8538
  ...PM_ENTITIES,
8250
8539
  handoff: {}
@@ -8745,7 +9034,7 @@ var SOURCE2 = /\.(ts|tsx|js|jsx|mts|cts)$/;
8745
9034
  function gitRunner(cwd) {
8746
9035
  return (args) => (0, import_node_child_process7.execFileSync)("git", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
8747
9036
  }
8748
- function collectDiff(runGit, base, read2) {
9037
+ function collectDiff(runGit, base, read3) {
8749
9038
  let merged = "";
8750
9039
  try {
8751
9040
  merged += runGit(["diff", "--unified=3", `${base}...HEAD`]);
@@ -8758,9 +9047,9 @@ function collectDiff(runGit, base, read2) {
8758
9047
  merged += runGit(["diff", "--unified=3", "HEAD"]);
8759
9048
  } catch {
8760
9049
  }
8761
- return merged + untrackedDiff(runGit, read2);
9050
+ return merged + untrackedDiff(runGit, read3);
8762
9051
  }
8763
- function untrackedDiff(runGit, read2) {
9052
+ function untrackedDiff(runGit, read3) {
8764
9053
  let listed = "";
8765
9054
  try {
8766
9055
  listed = runGit(["ls-files", "--others", "--exclude-standard"]);
@@ -8776,7 +9065,7 @@ function untrackedDiff(runGit, read2) {
8776
9065
  if (!SOURCE2.test(path)) continue;
8777
9066
  let body;
8778
9067
  try {
8779
- body = read2(path);
9068
+ body = read3(path);
8780
9069
  } catch {
8781
9070
  continue;
8782
9071
  }
@@ -8858,8 +9147,8 @@ function report3(ctx, impacts) {
8858
9147
  async function runbookImpact(ctx, options, deps = {}) {
8859
9148
  const cwd = deps.cwd ?? process.cwd();
8860
9149
  const runGit = deps.runGit ?? gitRunner(cwd);
8861
- const read2 = deps.readRepoFile ?? ((path) => (0, import_node_fs16.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
8862
- const surfaces = changedSurfaces(collectDiff(runGit, options.base, read2), manifestLabeller(cwd));
9150
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs16.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
9151
+ const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
8863
9152
  if (!surfaces.length) {
8864
9153
  return ctx.out.log(
8865
9154
  ctx.json ? JSON.stringify({ base: options.base, impacts: [] }, null, 2) : `no changes against ${options.base}`
@@ -9982,33 +10271,37 @@ function exitCodeFor(err) {
9982
10271
  return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
9983
10272
  }
9984
10273
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10274
+ const runtime = {
10275
+ ...dependencies,
10276
+ stdout: redactingOutput(dependencies.stdout ?? console)
10277
+ };
9985
10278
  const parsed = parseArgv(argv);
9986
10279
  recordInvocation(parsed);
9987
10280
  const command = parsed.positionals[0] ?? "help";
9988
10281
  if (command === "version" || command === "-v" || parsed.options.version === true) {
9989
10282
  assertArgs(parsed, ["version"], 1);
9990
- console.log(cliVersion());
10283
+ runtime.stdout.log(cliVersion());
9991
10284
  return;
9992
10285
  }
9993
10286
  if (command === "help" || command === "--help" || command === "-h") {
9994
10287
  assertArgs(parsed, ["help"], 1);
9995
- printHelp();
10288
+ printHelp(runtime.stdout);
9996
10289
  return;
9997
10290
  }
9998
10291
  if (command === "whoami") {
9999
- await whoamiCommand(parsed, dependencies);
10292
+ await whoamiCommand(parsed, runtime);
10000
10293
  return;
10001
10294
  }
10002
10295
  if (command === "runbook") {
10003
- await runbookCommand(parsed, dependencies);
10296
+ await runbookCommand(parsed, runtime);
10004
10297
  return;
10005
10298
  }
10006
10299
  if (command === "calendar") {
10007
- await calendarCommand(parsed, dependencies);
10300
+ await calendarCommand(parsed, runtime);
10008
10301
  return;
10009
10302
  }
10010
10303
  if (command === "code") {
10011
- await codeCommand(parsed, dependencies);
10304
+ await codeCommand(parsed, runtime);
10012
10305
  return;
10013
10306
  }
10014
10307
  if (command === "admin") {
@@ -10016,26 +10309,30 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10016
10309
  return;
10017
10310
  }
10018
10311
  if (command === "app") {
10019
- await appCommand(parsed, dependencies);
10312
+ await appCommand(parsed, runtime);
10020
10313
  return;
10021
10314
  }
10022
10315
  if (command === "security") {
10023
- await securityCommand(parsed, dependencies);
10316
+ await securityCommand(parsed, runtime);
10024
10317
  return;
10025
10318
  }
10026
10319
  if (command === "pm") {
10027
- await pmCommand(parsed, dependencies);
10320
+ await pmCommand(parsed, runtime);
10028
10321
  return;
10029
10322
  }
10030
10323
  if (command === "discuss") {
10031
- await discussCommand(parsed, dependencies);
10324
+ await discussCommand(parsed, runtime);
10325
+ return;
10326
+ }
10327
+ if (command === "o11y") {
10328
+ await o11yCommand(parsed, runtime);
10032
10329
  return;
10033
10330
  }
10034
10331
  if (command === "provision") {
10035
- await provisionCommand(parsed, dependencies);
10332
+ await provisionCommand(parsed, runtime);
10036
10333
  return;
10037
10334
  }
10038
- if (await projectCommand(command, parsed, dependencies)) return;
10335
+ if (await projectCommand(command, parsed, runtime)) return;
10039
10336
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
10040
10337
  }
10041
10338
  async function provisionCommand(parsed, dependencies) {
@@ -10101,7 +10398,7 @@ async function calendarCommand(parsed, dependencies) {
10101
10398
 
10102
10399
  // src/bin.ts
10103
10400
  runCli().catch((err) => {
10104
- console.error(`odla-ai: ${err instanceof Error ? err.message : String(err)}`);
10401
+ console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
10105
10402
  process.exitCode = exitCodeFor(err);
10106
10403
  });
10107
10404
  //# sourceMappingURL=bin.cjs.map