@odla-ai/cli 0.25.8 → 0.25.10

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
  };
@@ -6623,12 +6623,12 @@ Usage:
6623
6623
  odla-ai pm handoff --app <id> [--json]
6624
6624
  odla-ai discuss groups [--json]
6625
6625
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6626
- odla-ai discuss read <topic> [--json]
6626
+ odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
6627
6627
  odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
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]
@@ -7469,22 +7469,59 @@ async function discussList(ctx, parsed) {
7469
7469
  }
7470
7470
  });
7471
7471
  }
7472
- async function discussRead(ctx, id) {
7473
- const result = await request(
7474
- ctx,
7475
- "GET",
7476
- `/topics/${encodeURIComponent(id)}`
7477
- );
7478
- emit(ctx, result, () => {
7479
- ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
7480
- for (const post of result.posts) {
7481
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7482
- ctx.out.log(`
7483
- \u2014 ${who}`);
7484
- ctx.out.log(bodyWithRefs(post));
7485
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
7472
+ async function discussRead(ctx, id, parsed) {
7473
+ const requestedLimit = stringOpt(parsed.options.limit);
7474
+ const requestedOffset = stringOpt(parsed.options.offset);
7475
+ if (requestedLimit !== void 0 || requestedOffset !== void 0) {
7476
+ const query = new URLSearchParams({
7477
+ limit: requestedLimit ?? "200",
7478
+ offset: requestedOffset ?? "0"
7479
+ });
7480
+ const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
7481
+ emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
7482
+ return;
7483
+ }
7484
+ for (let scan = 0; scan < 3; scan++) {
7485
+ const posts = /* @__PURE__ */ new Map();
7486
+ let topic = null;
7487
+ let offset = 0;
7488
+ for (; ; ) {
7489
+ const page = await request(
7490
+ ctx,
7491
+ "GET",
7492
+ `/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
7493
+ );
7494
+ topic = page.topic;
7495
+ for (const post of page.posts) posts.set(post.id, post);
7496
+ if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
7497
+ if (!page.page?.hasMore) break;
7498
+ if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
7499
+ throw new Error("discuss read failed: registry returned a non-advancing post page");
7500
+ }
7501
+ offset = page.page.nextOffset;
7486
7502
  }
7487
- });
7503
+ const ordered = [...posts.values()].sort(
7504
+ (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
7505
+ );
7506
+ const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
7507
+ if (ordered.length === expected) {
7508
+ const result = { topic, posts: ordered };
7509
+ emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
7510
+ return;
7511
+ }
7512
+ if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
7513
+ }
7514
+ throw new Error("discuss read failed: conversation changed during every complete-read attempt");
7515
+ }
7516
+ function renderRead(ctx, topic, posts) {
7517
+ ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
7518
+ for (const post of posts) {
7519
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7520
+ ctx.out.log(`
7521
+ \u2014 ${who}`);
7522
+ ctx.out.log(bodyWithRefs(post));
7523
+ for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
7524
+ }
7488
7525
  }
7489
7526
  async function discussPost(ctx, parsed) {
7490
7527
  const appId = stringOpt(parsed.options.app);
@@ -7533,61 +7570,214 @@ async function discussWho(ctx, parsed) {
7533
7570
  });
7534
7571
  }
7535
7572
 
7573
+ // src/discuss-watch-http.ts
7574
+ var WatchRequestError = class extends Error {
7575
+ constructor(message2, retryable, status, code) {
7576
+ super(message2);
7577
+ this.retryable = retryable;
7578
+ this.status = status;
7579
+ this.code = code;
7580
+ this.name = "WatchRequestError";
7581
+ }
7582
+ retryable;
7583
+ status;
7584
+ code;
7585
+ };
7586
+ var WatchCheckpointError = class extends Error {
7587
+ constructor(cursor, streamId) {
7588
+ super("discussion cursor requires a new checkpoint");
7589
+ this.cursor = cursor;
7590
+ this.streamId = streamId;
7591
+ this.name = "WatchCheckpointError";
7592
+ }
7593
+ cursor;
7594
+ streamId;
7595
+ code = "checkpoint_required";
7596
+ };
7597
+ var WatchRemoteError = class extends Error {
7598
+ constructor(cursor, cause) {
7599
+ super("discussion watch dependency remained unavailable", { cause });
7600
+ this.cursor = cursor;
7601
+ this.name = "WatchRemoteError";
7602
+ }
7603
+ cursor;
7604
+ code = "remote_unavailable";
7605
+ };
7606
+ var WatchTimeoutError = class extends Error {
7607
+ constructor(cursor) {
7608
+ super("nothing new before the timeout");
7609
+ this.cursor = cursor;
7610
+ this.name = "WatchTimeoutError";
7611
+ }
7612
+ cursor;
7613
+ code = "watch_timeout";
7614
+ };
7615
+ function requestPath(topicId, app, cursor) {
7616
+ const params = new URLSearchParams();
7617
+ if (topicId) params.set("topic", topicId);
7618
+ if (app) params.set("app", app);
7619
+ if (cursor) params.set("cursor", cursor);
7620
+ return `/watch?${params}`;
7621
+ }
7622
+ async function getWatchPage(ctx, path) {
7623
+ let res;
7624
+ try {
7625
+ res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7626
+ headers: { authorization: `Bearer ${ctx.token}` }
7627
+ });
7628
+ } catch (error) {
7629
+ throw new WatchRequestError(
7630
+ `discuss watch request failed: ${error instanceof Error ? error.message : String(error)}`,
7631
+ true
7632
+ );
7633
+ }
7634
+ const data = await res.json().catch(() => ({}));
7635
+ if (res.status === 409 && data.code === "checkpoint_required") {
7636
+ throw new WatchCheckpointError(data.cursor, data.streamId);
7637
+ }
7638
+ if (!res.ok) {
7639
+ throw new WatchRequestError(
7640
+ `discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`,
7641
+ res.status === 429 || res.status >= 500,
7642
+ res.status,
7643
+ data.code ?? (res.status === 401 || res.status === 403 ? "auth_failed" : void 0)
7644
+ );
7645
+ }
7646
+ return data;
7647
+ }
7648
+
7536
7649
  // src/discuss-watch.ts
7537
7650
  var DEFAULT_INTERVAL_MS = 15e3;
7538
- var DEFAULT_TIMEOUT_MS = 15 * 6e4;
7651
+ var MAX_CONSECUTIVE_FAILURES = 5;
7652
+ var MAX_BACKOFF_MS = 3e4;
7539
7653
  function numberOpt2(parsed, flag, fallback) {
7540
7654
  const raw = stringOpt(parsed.options[flag]);
7541
7655
  const value = raw == null ? NaN : Number(raw);
7542
7656
  return Number.isFinite(value) && value > 0 ? value : fallback;
7543
7657
  }
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;
7658
+ function jsonl(ctx, parsed, value) {
7659
+ if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value }));
7551
7660
  }
7552
7661
  async function discussWatch(ctx, topicId, parsed) {
7662
+ if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
7553
7663
  const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
7554
7664
  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;
7665
+ const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7666
+ const timeoutSeconds = numberOpt2(parsed, "timeout");
7667
+ const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
7557
7668
  const by = stringOpt(parsed.options.by);
7558
7669
  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
- }
7670
+ const app = stringOpt(parsed.options.app);
7671
+ let cursor = stringOpt(parsed.options.cursor);
7672
+ let firstSuccess = true;
7673
+ let consecutiveFailures = 0;
7567
7674
  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();
7675
+ let page;
7676
+ try {
7677
+ page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
7678
+ consecutiveFailures = 0;
7679
+ } catch (error) {
7680
+ if (!(error instanceof WatchRequestError) || !error.retryable) {
7681
+ if (error instanceof WatchCheckpointError) {
7682
+ jsonl(ctx, parsed, {
7683
+ type: "status",
7684
+ state: "checkpoint_required",
7685
+ retryable: false,
7686
+ ...error.cursor ? { cursor: error.cursor } : {},
7687
+ ...error.streamId ? { streamId: error.streamId } : {}
7688
+ });
7689
+ }
7690
+ throw error;
7691
+ }
7692
+ consecutiveFailures++;
7693
+ jsonl(ctx, parsed, {
7694
+ type: "status",
7695
+ state: "degraded",
7696
+ retryable: true,
7697
+ attempt: consecutiveFailures,
7698
+ ...cursor ? { cursor } : {},
7699
+ ...error.status ? { status: error.status } : {}
7700
+ });
7701
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
7702
+ throw new WatchRemoteError(cursor, error);
7703
+ }
7704
+ if (deadline !== void 0 && now() >= deadline) {
7705
+ const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
7706
+ throw new WatchTimeoutError(result.cursor);
7707
+ }
7708
+ const base = Math.min(intervalMs, 1e3);
7709
+ const backoff = Math.min(MAX_BACKOFF_MS, base * 2 ** (consecutiveFailures - 1));
7710
+ await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
7711
+ continue;
7712
+ }
7713
+ cursor = page.cursor;
7714
+ const baseline = firstSuccess && page.events.length === 0;
7715
+ if (baseline) {
7716
+ jsonl(ctx, parsed, {
7717
+ type: "checkpoint",
7718
+ streamId: page.streamId,
7719
+ cursor,
7720
+ serverTime: page.serverTime
7721
+ });
7722
+ }
7723
+ firstSuccess = false;
7724
+ const matching = page.events.filter((event) => {
7725
+ if (topicId && (event.type !== "message" || event.action !== "created")) return false;
7726
+ return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
7727
+ });
7728
+ for (const event of matching) {
7729
+ jsonl(ctx, parsed, {
7730
+ type: "event",
7731
+ streamId: page.streamId,
7732
+ eventId: event.id,
7733
+ cursor: event.cursor,
7734
+ event
7735
+ });
7736
+ }
7737
+ if (matching.length > 0) {
7738
+ jsonl(ctx, parsed, {
7739
+ type: "checkpoint",
7740
+ streamId: page.streamId,
7741
+ cursor,
7742
+ serverTime: page.serverTime
7743
+ });
7744
+ const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
7745
+ const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
7746
+ return report2(ctx, parsed, {
7747
+ found: true,
7748
+ cursor,
7749
+ events: matching,
7750
+ ...posts && posts.length > 0 ? { posts } : {},
7751
+ ...topics && topics.length > 0 ? { topics } : {}
7752
+ });
7582
7753
  }
7583
- if (now() >= deadline) return report2(ctx, { found: false });
7584
- await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
7754
+ if (page.events.length > 0) {
7755
+ jsonl(ctx, parsed, {
7756
+ type: "checkpoint",
7757
+ streamId: page.streamId,
7758
+ cursor,
7759
+ serverTime: page.serverTime
7760
+ });
7761
+ } else if (!baseline) {
7762
+ jsonl(ctx, parsed, {
7763
+ type: "heartbeat",
7764
+ streamId: page.streamId,
7765
+ cursor,
7766
+ serverTime: page.serverTime
7767
+ });
7768
+ }
7769
+ if (page.hasMore) continue;
7770
+ if (deadline !== void 0 && now() >= deadline) {
7771
+ return report2(ctx, parsed, { found: false, cursor });
7772
+ }
7773
+ const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
7774
+ await sleep(wait2);
7585
7775
  }
7586
7776
  }
7587
- function report2(ctx, result) {
7777
+ function report2(ctx, parsed, result) {
7588
7778
  if (ctx.json) {
7589
7779
  ctx.out.log(JSON.stringify(result, null, 2));
7590
- } else if (result.found) {
7780
+ } else if (parsed.options.jsonl !== true && result.found) {
7591
7781
  for (const post of result.posts ?? []) {
7592
7782
  const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7593
7783
  ctx.out.log(`\u2014 ${who}
@@ -7599,13 +7789,6 @@ ${post.body}`);
7599
7789
  }
7600
7790
  return result;
7601
7791
  }
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
7792
 
7610
7793
  // src/discuss-command.ts
7611
7794
  var ALLOWED = [
@@ -7627,6 +7810,8 @@ var ALLOWED = [
7627
7810
  "self",
7628
7811
  "interval",
7629
7812
  "timeout",
7813
+ "cursor",
7814
+ "jsonl",
7630
7815
  "mutation-id"
7631
7816
  ];
7632
7817
  function requireId(id, action) {
@@ -7663,7 +7848,7 @@ async function discussCommand(parsed, deps = {}) {
7663
7848
  case "topics":
7664
7849
  return discussList(ctx, parsed);
7665
7850
  case "read":
7666
- return discussRead(ctx, requireId(id, "read"));
7851
+ return discussRead(ctx, requireId(id, "read"), parsed);
7667
7852
  case "post":
7668
7853
  return discussPost(ctx, parsed);
7669
7854
  case "reply":
@@ -7674,7 +7859,7 @@ async function discussCommand(parsed, deps = {}) {
7674
7859
  return discussWho(ctx, parsed);
7675
7860
  case "watch": {
7676
7861
  const result = await discussWatch(ctx, id, parsed);
7677
- if (!result.found) throw new WatchTimeoutError();
7862
+ if (!result.found) throw new WatchTimeoutError(result.cursor);
7678
7863
  return;
7679
7864
  }
7680
7865
  default:
@@ -10458,7 +10643,12 @@ async function securityStatus(parsed, dependencies) {
10458
10643
  // src/cli.ts
10459
10644
  function exitCodeFor(err) {
10460
10645
  const code = err?.code;
10461
- return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
10646
+ if (code === "handshake_pending" || code === "watch_timeout") return 75;
10647
+ if (code === "checkpoint_required") return 3;
10648
+ if (code === "remote_unavailable") return 6;
10649
+ if (code === "auth_failed") return 5;
10650
+ if (code === "invalid_cursor") return 2;
10651
+ return 1;
10462
10652
  }
10463
10653
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10464
10654
  const runtime = {