@odla-ai/cli 0.25.8 → 0.25.9

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
@@ -4668,13 +4668,13 @@ async function createConversionRegistry(config) {
4668
4668
  policies.set(policy.conversionId, Object.freeze(policy));
4669
4669
  }
4670
4670
  const outputCounts = /* @__PURE__ */ new Map();
4671
- const get2 = (id, kind) => {
4671
+ const get = (id, kind) => {
4672
4672
  const policy = policies.get(id);
4673
4673
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
4674
4674
  return policy;
4675
4675
  };
4676
4676
  const checked = (source, id, kind) => {
4677
- const policy = get2(id, kind);
4677
+ const policy = get(id, kind);
4678
4678
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
4679
4679
  return policy;
4680
4680
  };
@@ -6628,7 +6628,7 @@ Usage:
6628
6628
  odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6629
6629
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6630
6630
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6631
- odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
6631
+ odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
6632
6632
  odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6633
6633
  odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6634
6634
  odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
@@ -7533,61 +7533,214 @@ async function discussWho(ctx, parsed) {
7533
7533
  });
7534
7534
  }
7535
7535
 
7536
+ // src/discuss-watch-http.ts
7537
+ var WatchRequestError = class extends Error {
7538
+ constructor(message2, retryable, status, code) {
7539
+ super(message2);
7540
+ this.retryable = retryable;
7541
+ this.status = status;
7542
+ this.code = code;
7543
+ this.name = "WatchRequestError";
7544
+ }
7545
+ retryable;
7546
+ status;
7547
+ code;
7548
+ };
7549
+ var WatchCheckpointError = class extends Error {
7550
+ constructor(cursor, streamId) {
7551
+ super("discussion cursor requires a new checkpoint");
7552
+ this.cursor = cursor;
7553
+ this.streamId = streamId;
7554
+ this.name = "WatchCheckpointError";
7555
+ }
7556
+ cursor;
7557
+ streamId;
7558
+ code = "checkpoint_required";
7559
+ };
7560
+ var WatchRemoteError = class extends Error {
7561
+ constructor(cursor, cause) {
7562
+ super("discussion watch dependency remained unavailable", { cause });
7563
+ this.cursor = cursor;
7564
+ this.name = "WatchRemoteError";
7565
+ }
7566
+ cursor;
7567
+ code = "remote_unavailable";
7568
+ };
7569
+ var WatchTimeoutError = class extends Error {
7570
+ constructor(cursor) {
7571
+ super("nothing new before the timeout");
7572
+ this.cursor = cursor;
7573
+ this.name = "WatchTimeoutError";
7574
+ }
7575
+ cursor;
7576
+ code = "watch_timeout";
7577
+ };
7578
+ function requestPath(topicId, app, cursor) {
7579
+ const params = new URLSearchParams();
7580
+ if (topicId) params.set("topic", topicId);
7581
+ if (app) params.set("app", app);
7582
+ if (cursor) params.set("cursor", cursor);
7583
+ return `/watch?${params}`;
7584
+ }
7585
+ async function getWatchPage(ctx, path) {
7586
+ let res;
7587
+ try {
7588
+ res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7589
+ headers: { authorization: `Bearer ${ctx.token}` }
7590
+ });
7591
+ } catch (error) {
7592
+ throw new WatchRequestError(
7593
+ `discuss watch request failed: ${error instanceof Error ? error.message : String(error)}`,
7594
+ true
7595
+ );
7596
+ }
7597
+ const data = await res.json().catch(() => ({}));
7598
+ if (res.status === 409 && data.code === "checkpoint_required") {
7599
+ throw new WatchCheckpointError(data.cursor, data.streamId);
7600
+ }
7601
+ if (!res.ok) {
7602
+ throw new WatchRequestError(
7603
+ `discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`,
7604
+ res.status === 429 || res.status >= 500,
7605
+ res.status,
7606
+ data.code ?? (res.status === 401 || res.status === 403 ? "auth_failed" : void 0)
7607
+ );
7608
+ }
7609
+ return data;
7610
+ }
7611
+
7536
7612
  // src/discuss-watch.ts
7537
7613
  var DEFAULT_INTERVAL_MS = 15e3;
7538
- var DEFAULT_TIMEOUT_MS = 15 * 6e4;
7614
+ var MAX_CONSECUTIVE_FAILURES = 5;
7615
+ var MAX_BACKOFF_MS = 3e4;
7539
7616
  function numberOpt2(parsed, flag, fallback) {
7540
7617
  const raw = stringOpt(parsed.options[flag]);
7541
7618
  const value = raw == null ? NaN : Number(raw);
7542
7619
  return Number.isFinite(value) && value > 0 ? value : fallback;
7543
7620
  }
7544
- async function get(ctx, path) {
7545
- const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7546
- headers: { authorization: `Bearer ${ctx.token}` }
7547
- });
7548
- const data = await res.json().catch(() => ({}));
7549
- if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
7550
- return data;
7621
+ function jsonl(ctx, parsed, value) {
7622
+ if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value }));
7551
7623
  }
7552
7624
  async function discussWatch(ctx, topicId, parsed) {
7625
+ if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
7553
7626
  const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
7554
7627
  const now = ctx.now ?? Date.now;
7555
- const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7556
- const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
7628
+ const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7629
+ const timeoutSeconds = numberOpt2(parsed, "timeout");
7630
+ const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
7557
7631
  const by = stringOpt(parsed.options.by);
7558
7632
  const self = stringOpt(parsed.options.self);
7559
- const deadline = now() + timeoutMs;
7560
- const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
7561
- let seen = /* @__PURE__ */ new Set();
7562
- let since = now();
7563
- if (topicId) {
7564
- const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7565
- seen = new Set(first.posts.map((post) => post.id));
7566
- }
7633
+ const app = stringOpt(parsed.options.app);
7634
+ let cursor = stringOpt(parsed.options.cursor);
7635
+ let firstSuccess = true;
7636
+ let consecutiveFailures = 0;
7567
7637
  for (; ; ) {
7568
- if (topicId) {
7569
- const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7570
- const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
7571
- if (added.length > 0) return report2(ctx, { found: true, posts: added });
7572
- for (const post of posts) seen.add(post.id);
7573
- } else {
7574
- const app = stringOpt(parsed.options.app);
7575
- const { topics } = await get(
7576
- ctx,
7577
- `/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
7578
- );
7579
- const active = topics.filter((topic) => topic.lastActivityAt > since);
7580
- if (active.length > 0) return report2(ctx, { found: true, topics: active });
7581
- since = now();
7638
+ let page;
7639
+ try {
7640
+ page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
7641
+ consecutiveFailures = 0;
7642
+ } catch (error) {
7643
+ if (!(error instanceof WatchRequestError) || !error.retryable) {
7644
+ if (error instanceof WatchCheckpointError) {
7645
+ jsonl(ctx, parsed, {
7646
+ type: "status",
7647
+ state: "checkpoint_required",
7648
+ retryable: false,
7649
+ ...error.cursor ? { cursor: error.cursor } : {},
7650
+ ...error.streamId ? { streamId: error.streamId } : {}
7651
+ });
7652
+ }
7653
+ throw error;
7654
+ }
7655
+ consecutiveFailures++;
7656
+ jsonl(ctx, parsed, {
7657
+ type: "status",
7658
+ state: "degraded",
7659
+ retryable: true,
7660
+ attempt: consecutiveFailures,
7661
+ ...cursor ? { cursor } : {},
7662
+ ...error.status ? { status: error.status } : {}
7663
+ });
7664
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
7665
+ throw new WatchRemoteError(cursor, error);
7666
+ }
7667
+ if (deadline !== void 0 && now() >= deadline) {
7668
+ const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
7669
+ throw new WatchTimeoutError(result.cursor);
7670
+ }
7671
+ const base = Math.min(intervalMs, 1e3);
7672
+ const backoff = Math.min(MAX_BACKOFF_MS, base * 2 ** (consecutiveFailures - 1));
7673
+ await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
7674
+ continue;
7675
+ }
7676
+ cursor = page.cursor;
7677
+ const baseline = firstSuccess && page.events.length === 0;
7678
+ if (baseline) {
7679
+ jsonl(ctx, parsed, {
7680
+ type: "checkpoint",
7681
+ streamId: page.streamId,
7682
+ cursor,
7683
+ serverTime: page.serverTime
7684
+ });
7582
7685
  }
7583
- if (now() >= deadline) return report2(ctx, { found: false });
7584
- await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
7686
+ firstSuccess = false;
7687
+ const matching = page.events.filter((event) => {
7688
+ if (topicId && (event.type !== "message" || event.action !== "created")) return false;
7689
+ return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
7690
+ });
7691
+ for (const event of matching) {
7692
+ jsonl(ctx, parsed, {
7693
+ type: "event",
7694
+ streamId: page.streamId,
7695
+ eventId: event.id,
7696
+ cursor: event.cursor,
7697
+ event
7698
+ });
7699
+ }
7700
+ if (matching.length > 0) {
7701
+ jsonl(ctx, parsed, {
7702
+ type: "checkpoint",
7703
+ streamId: page.streamId,
7704
+ cursor,
7705
+ serverTime: page.serverTime
7706
+ });
7707
+ const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
7708
+ const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
7709
+ return report2(ctx, parsed, {
7710
+ found: true,
7711
+ cursor,
7712
+ events: matching,
7713
+ ...posts && posts.length > 0 ? { posts } : {},
7714
+ ...topics && topics.length > 0 ? { topics } : {}
7715
+ });
7716
+ }
7717
+ if (page.events.length > 0) {
7718
+ jsonl(ctx, parsed, {
7719
+ type: "checkpoint",
7720
+ streamId: page.streamId,
7721
+ cursor,
7722
+ serverTime: page.serverTime
7723
+ });
7724
+ } else if (!baseline) {
7725
+ jsonl(ctx, parsed, {
7726
+ type: "heartbeat",
7727
+ streamId: page.streamId,
7728
+ cursor,
7729
+ serverTime: page.serverTime
7730
+ });
7731
+ }
7732
+ if (page.hasMore) continue;
7733
+ if (deadline !== void 0 && now() >= deadline) {
7734
+ return report2(ctx, parsed, { found: false, cursor });
7735
+ }
7736
+ const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
7737
+ await sleep(wait2);
7585
7738
  }
7586
7739
  }
7587
- function report2(ctx, result) {
7740
+ function report2(ctx, parsed, result) {
7588
7741
  if (ctx.json) {
7589
7742
  ctx.out.log(JSON.stringify(result, null, 2));
7590
- } else if (result.found) {
7743
+ } else if (parsed.options.jsonl !== true && result.found) {
7591
7744
  for (const post of result.posts ?? []) {
7592
7745
  const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7593
7746
  ctx.out.log(`\u2014 ${who}
@@ -7599,13 +7752,6 @@ ${post.body}`);
7599
7752
  }
7600
7753
  return result;
7601
7754
  }
7602
- var WatchTimeoutError = class extends Error {
7603
- code = "watch_timeout";
7604
- constructor() {
7605
- super("nothing new before the timeout");
7606
- this.name = "WatchTimeoutError";
7607
- }
7608
- };
7609
7755
 
7610
7756
  // src/discuss-command.ts
7611
7757
  var ALLOWED = [
@@ -7627,6 +7773,8 @@ var ALLOWED = [
7627
7773
  "self",
7628
7774
  "interval",
7629
7775
  "timeout",
7776
+ "cursor",
7777
+ "jsonl",
7630
7778
  "mutation-id"
7631
7779
  ];
7632
7780
  function requireId(id, action) {
@@ -7674,7 +7822,7 @@ async function discussCommand(parsed, deps = {}) {
7674
7822
  return discussWho(ctx, parsed);
7675
7823
  case "watch": {
7676
7824
  const result = await discussWatch(ctx, id, parsed);
7677
- if (!result.found) throw new WatchTimeoutError();
7825
+ if (!result.found) throw new WatchTimeoutError(result.cursor);
7678
7826
  return;
7679
7827
  }
7680
7828
  default:
@@ -10458,7 +10606,12 @@ async function securityStatus(parsed, dependencies) {
10458
10606
  // src/cli.ts
10459
10607
  function exitCodeFor(err) {
10460
10608
  const code = err?.code;
10461
- return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
10609
+ if (code === "handshake_pending" || code === "watch_timeout") return 75;
10610
+ if (code === "checkpoint_required") return 3;
10611
+ if (code === "remote_unavailable") return 6;
10612
+ if (code === "auth_failed") return 5;
10613
+ if (code === "invalid_cursor") return 2;
10614
+ return 1;
10462
10615
  }
10463
10616
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10464
10617
  const runtime = {