@odla-ai/cli 0.25.6 → 0.25.8

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
@@ -1080,11 +1080,6 @@ async function adminCommand(parsed) {
1080
1080
  });
1081
1081
  }
1082
1082
 
1083
- // src/app-export.ts
1084
- var import_node_fs5 = require("fs");
1085
- var import_node_stream = require("stream");
1086
- var import_promises = require("stream/promises");
1087
-
1088
1083
  // src/config.ts
1089
1084
  var import_node_fs4 = require("fs");
1090
1085
  var import_node_path4 = require("path");
@@ -1393,7 +1388,90 @@ function bothTenants(cfg) {
1393
1388
  return { sandbox: (0, import_apps.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps.tenantIdFor)(cfg.app.id, "prod") };
1394
1389
  }
1395
1390
 
1391
+ // src/agent-command.ts
1392
+ async function agentCommand(parsed, deps = {}) {
1393
+ const action = parsed.positionals[1];
1394
+ if (action !== "jobs" && action !== "retry") {
1395
+ throw new Error(`unknown agent action "${action ?? ""}". Try "odla-ai agent jobs --json".`);
1396
+ }
1397
+ assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action === "jobs" ? 2 : 3);
1398
+ if (action === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
1399
+ throw new Error('--state and --limit are supported only by "agent jobs"');
1400
+ }
1401
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
1402
+ const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
1403
+ const doFetch = deps.fetch ?? fetch;
1404
+ const out = deps.stdout ?? console;
1405
+ const credential2 = await getDeveloperToken(
1406
+ cfg,
1407
+ {
1408
+ configPath: cfg.configPath,
1409
+ token: stringOpt(parsed.options.token),
1410
+ email: stringOpt(parsed.options.email),
1411
+ open: false
1412
+ },
1413
+ doFetch,
1414
+ out
1415
+ );
1416
+ const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
1417
+ const headers = { authorization: `Bearer ${credential2}` };
1418
+ if (action === "retry") {
1419
+ const id = parsed.positionals[2];
1420
+ const res2 = await doFetch(`${base}/${encodeURIComponent(id)}/retry`, { method: "POST", headers });
1421
+ const body2 = await readJson(res2);
1422
+ if (!res2.ok) throw new Error(`agent retry failed (${res2.status}): ${errorMessage(body2)}`);
1423
+ const result2 = { v: 1, appId: cfg.app.id, env, tenant, ...body2 };
1424
+ if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
1425
+ else out.log(`${tenant}: requeued ${id}`);
1426
+ return;
1427
+ }
1428
+ const state2 = stringOpt(parsed.options.state);
1429
+ const allowed = ["pending", "running", "succeeded", "dead_letter"];
1430
+ if (state2 && !allowed.includes(state2)) {
1431
+ throw new Error(`--state must be one of ${allowed.join(", ")}`);
1432
+ }
1433
+ const url = new URL(base);
1434
+ if (state2) url.searchParams.set("state", state2);
1435
+ const limit = numberOpt(parsed.options.limit, "--limit");
1436
+ if (limit) url.searchParams.set("limit", String(limit));
1437
+ const res = await doFetch(url, { headers });
1438
+ const body = await readJson(res);
1439
+ if (!res.ok) throw new Error(`agent jobs failed (${res.status}): ${errorMessage(body)}`);
1440
+ const result = { v: 1, appId: cfg.app.id, env, tenant, jobs: body.jobs ?? [], summary: body.summary };
1441
+ if (parsed.options.json === true) {
1442
+ out.log(JSON.stringify(result, null, 2));
1443
+ return;
1444
+ }
1445
+ const summary = body.summary;
1446
+ out.log(
1447
+ `${tenant}: ${summary?.pending ?? 0} pending, ${summary?.running ?? 0} running, ${summary?.deadLetter ?? 0} dead-letter, ${summary?.succeeded ?? 0} succeeded`
1448
+ );
1449
+ for (const job of body.jobs ?? []) {
1450
+ out.log(
1451
+ [job.state, job.id, job.triggerId, `${job.entityNs}/${job.entityId}`, `attempts=${job.attempts}`, job.lastErrorCode ?? ""].filter(Boolean).join(" ")
1452
+ );
1453
+ }
1454
+ }
1455
+ async function readJson(res) {
1456
+ try {
1457
+ return await res.json();
1458
+ } catch {
1459
+ return {};
1460
+ }
1461
+ }
1462
+ function errorMessage(body) {
1463
+ const error = body.error;
1464
+ if (typeof error === "string") return error;
1465
+ if (error && typeof error === "object" && typeof error.message === "string") {
1466
+ return error.message;
1467
+ }
1468
+ return "request failed";
1469
+ }
1470
+
1396
1471
  // src/app-export.ts
1472
+ var import_node_fs5 = require("fs");
1473
+ var import_node_stream = require("stream");
1474
+ var import_promises = require("stream/promises");
1397
1475
  async function appExport(options) {
1398
1476
  const cfg = await loadProjectConfig(options.configPath);
1399
1477
  const out = options.stdout ?? console;
@@ -2371,6 +2449,7 @@ var CAPABILITIES = {
2371
2449
  "apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
2372
2450
  "validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
2373
2451
  "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",
2452
+ "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
2374
2453
  "run app-attributed hosted security discovery and independent validation without provider keys",
2375
2454
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
2376
2455
  "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
@@ -4006,8 +4085,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
4006
4085
  const maxFiles = options.maxFiles ?? 2e4;
4007
4086
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
4008
4087
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
4009
- const entries = inventory.flatMap((record6) => {
4010
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record6);
4088
+ const entries = inventory.flatMap((record7) => {
4089
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record7);
4011
4090
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
4012
4091
  });
4013
4092
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -4255,8 +4334,8 @@ function normalize(value) {
4255
4334
  if (Array.isArray(value)) return value.map(normalize);
4256
4335
  if (value instanceof Uint8Array) return { $bytes: [...value] };
4257
4336
  if (typeof value === "object") {
4258
- const record6 = value;
4259
- return Object.fromEntries(Object.keys(record6).filter((key) => record6[key] !== void 0).sort().map((key) => [key, normalize(record6[key])]));
4337
+ const record7 = value;
4338
+ return Object.fromEntries(Object.keys(record7).filter((key) => record7[key] !== void 0).sort().map((key) => [key, normalize(record7[key])]));
4260
4339
  }
4261
4340
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
4262
4341
  }
@@ -6550,6 +6629,8 @@ Usage:
6550
6629
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6551
6630
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6552
6631
  odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
6632
+ odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6633
+ odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6553
6634
  odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
6554
6635
  odla-ai whoami [--json]
6555
6636
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
@@ -6597,6 +6678,8 @@ Usage:
6597
6678
  odla-ai version
6598
6679
 
6599
6680
  Commands:
6681
+ agent Inspect durable agent wakeups and explicitly requeue a
6682
+ dead-lettered job; JSON output is stable for remote operators.
6600
6683
  runbook odla's operational procedures, stored in the database and read at
6601
6684
  the moment they are followed. "ask" gives a written, cited answer;
6602
6685
  "search" the passages behind it; "get" the whole document.
@@ -7710,8 +7793,8 @@ async function pmAdd(ctx, entity, parsed) {
7710
7793
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
7711
7794
  }
7712
7795
  async function pmGet(ctx, entity, id) {
7713
- const { record: record6 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7714
- emit2(ctx, record6, () => printRecord(ctx, entity, record6));
7796
+ const { record: record7 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
7797
+ emit2(ctx, record7, () => printRecord(ctx, entity, record7));
7715
7798
  }
7716
7799
  async function pmSet(ctx, entity, id, parsed) {
7717
7800
  const patch2 = collectEntityFields(entity, parsed, true);
@@ -7756,9 +7839,9 @@ async function pmHandoff(ctx, parsed) {
7756
7839
  ]);
7757
7840
  const handoff = {
7758
7841
  appId,
7759
- unmetGoals: goals.filter((record6) => record6.status !== "met"),
7760
- activeTasks: tasks.filter((record6) => record6.column !== "done"),
7761
- openBugs: bugs.filter((record6) => record6.status !== "fixed" && record6.status !== "wontfix")
7842
+ unmetGoals: goals.filter((record7) => record7.status !== "met"),
7843
+ activeTasks: tasks.filter((record7) => record7.column !== "done"),
7844
+ openBugs: bugs.filter((record7) => record7.status !== "fixed" && record7.status !== "wontfix")
7762
7845
  };
7763
7846
  const result = {
7764
7847
  ...handoff,
@@ -7777,10 +7860,10 @@ async function pmHandoff(ctx, parsed) {
7777
7860
  ]) {
7778
7861
  ctx.out.log(`${label}:`);
7779
7862
  if (!records.length) ctx.out.log("- (none)");
7780
- else for (const record6 of records) printRecord(
7863
+ else for (const record7 of records) printRecord(
7781
7864
  ctx,
7782
7865
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
7783
- record6
7866
+ record7
7784
7867
  );
7785
7868
  }
7786
7869
  });
@@ -7921,6 +8004,21 @@ function statusVerdict(reads) {
7921
8004
  severity: "degraded"
7922
8005
  });
7923
8006
  }
8007
+ const performance = record5(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
8008
+ if (performance?.status === "unavailable") {
8009
+ reasons.push({
8010
+ source: "liveSync",
8011
+ code: "subscription_metrics_unavailable",
8012
+ severity: "degraded"
8013
+ });
8014
+ }
8015
+ if (numeric2(performance?.telemetryDropped) > 0) {
8016
+ reasons.push({
8017
+ source: "liveSync",
8018
+ code: "subscription_telemetry_dropped",
8019
+ severity: "degraded"
8020
+ });
8021
+ }
7924
8022
  const canaryStatus = String(reads.canary.body.status ?? "");
7925
8023
  if (canaryStatus === "failing") {
7926
8024
  reasons.push({
@@ -7968,6 +8066,12 @@ function statusVerdict(reads) {
7968
8066
  reasons
7969
8067
  };
7970
8068
  }
8069
+ function record5(value) {
8070
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8071
+ }
8072
+ function numeric2(value) {
8073
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
8074
+ }
7971
8075
  function collectorReason(read3, fallback) {
7972
8076
  const reasons = read3.body.reasons;
7973
8077
  if (!Array.isArray(reasons)) return fallback;
@@ -8025,7 +8129,7 @@ async function o11yCommand(parsed, deps = {}) {
8025
8129
  const [application, liveSync, canary, collector, provider] = await Promise.all([
8026
8130
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
8027
8131
  read2(
8028
- `${base}/live-sync/health?${new URLSearchParams({ env })}`,
8132
+ `${base}/live-sync/health?${query}`,
8029
8133
  headers,
8030
8134
  doFetch
8031
8135
  ),
@@ -8045,7 +8149,7 @@ async function o11yCommand(parsed, deps = {}) {
8045
8149
  provider
8046
8150
  });
8047
8151
  const status = {
8048
- schemaVersion: 3,
8152
+ schemaVersion: 4,
8049
8153
  scope: { appId, env, minutes },
8050
8154
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
8051
8155
  verdict,
@@ -8085,42 +8189,47 @@ function printStatus2(status, out) {
8085
8189
  out.log(
8086
8190
  `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
8087
8191
  );
8088
- const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record5) : [];
8192
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
8089
8193
  const requests = routes.reduce(
8090
- (total, row) => total + numeric2(row.requests),
8194
+ (total, row) => total + numeric3(row.requests),
8091
8195
  0
8092
8196
  );
8093
8197
  const errors = routes.reduce(
8094
- (total, row) => total + numeric2(row.errors),
8198
+ (total, row) => total + numeric3(row.errors),
8095
8199
  0
8096
8200
  );
8097
8201
  out.log(
8098
8202
  `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
8099
8203
  );
8100
8204
  out.log(
8101
- `live-sync ${status.liveSync.httpStatus} ${String(status.liveSync.body.status ?? status.liveSync.body.error ?? "unavailable")} ${numeric2(status.liveSync.body.activeConnections)} active`
8205
+ liveSyncLine(status.liveSync)
8102
8206
  );
8103
- const canaryDurations = record5(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
8207
+ const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
8104
8208
  out.log(
8105
8209
  `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
8106
8210
  );
8107
- const collectorIngest = record5(status.collector.body.ingest) ? status.collector.body.ingest : {};
8108
- const collectorStorage = record5(collectorIngest.storage) ? collectorIngest.storage : {};
8211
+ const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
8212
+ const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
8109
8213
  out.log(
8110
- `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric2(collectorStorage.affectedPoints)} affected points`
8214
+ `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
8111
8215
  );
8112
- const providerMetrics = record5(status.provider.body.metrics) ? status.provider.body.metrics : {};
8216
+ const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
8113
8217
  out.log(
8114
- `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric2(providerMetrics.requests)} invocations ${numeric2(providerMetrics.errors)} runtime errors`
8218
+ `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
8115
8219
  );
8116
8220
  out.log(
8117
8221
  `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8118
8222
  );
8119
8223
  }
8120
- function record5(value) {
8224
+ function liveSyncLine(read3) {
8225
+ const performance = record6(read3.body.performance) ? read3.body.performance : {};
8226
+ const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
8227
+ 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`;
8228
+ }
8229
+ function record6(value) {
8121
8230
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8122
8231
  }
8123
- function numeric2(value) {
8232
+ function numeric3(value) {
8124
8233
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
8125
8234
  }
8126
8235
  function optionalNumeric(value) {
@@ -8553,6 +8662,7 @@ var PM_ENTITIES = Object.fromEntries(
8553
8662
  ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
8554
8663
  );
8555
8664
  var COMMAND_SURFACE = {
8665
+ agent: { jobs: {}, retry: {} },
8556
8666
  admin: {
8557
8667
  ai: {
8558
8668
  show: {},
@@ -10388,6 +10498,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10388
10498
  await adminCommand(parsed);
10389
10499
  return;
10390
10500
  }
10501
+ if (command === "agent") {
10502
+ await agentCommand(parsed, runtime);
10503
+ return;
10504
+ }
10391
10505
  if (command === "app") {
10392
10506
  await appCommand(parsed, runtime);
10393
10507
  return;