@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/bin.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  exitCodeFor,
4
4
  redactSecrets,
5
5
  runCli
6
- } from "./chunk-PM5FBL3S.js";
6
+ } from "./chunk-335ODYSF.js";
7
7
 
8
8
  // src/bin.ts
9
9
  runCli().catch((err) => {
@@ -4035,13 +4035,13 @@ async function createConversionRegistry(config) {
4035
4035
  policies.set(policy.conversionId, Object.freeze(policy));
4036
4036
  }
4037
4037
  const outputCounts = /* @__PURE__ */ new Map();
4038
- const get2 = (id, kind) => {
4038
+ const get = (id, kind) => {
4039
4039
  const policy = policies.get(id);
4040
4040
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
4041
4041
  return policy;
4042
4042
  };
4043
4043
  const checked = (source, id, kind) => {
4044
- const policy = get2(id, kind);
4044
+ const policy = get(id, kind);
4045
4045
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
4046
4046
  return policy;
4047
4047
  };
@@ -5990,12 +5990,12 @@ Usage:
5990
5990
  odla-ai pm handoff --app <id> [--json]
5991
5991
  odla-ai discuss groups [--json]
5992
5992
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
5993
- odla-ai discuss read <topic> [--json]
5993
+ odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
5994
5994
  odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
5995
5995
  odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
5996
5996
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
5997
5997
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
5998
- odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
5998
+ odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
5999
5999
  odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6000
6000
  odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6001
6001
  odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
@@ -8208,22 +8208,59 @@ async function discussList(ctx, parsed) {
8208
8208
  }
8209
8209
  });
8210
8210
  }
8211
- async function discussRead(ctx, id) {
8212
- const result = await request(
8213
- ctx,
8214
- "GET",
8215
- `/topics/${encodeURIComponent(id)}`
8216
- );
8217
- emit(ctx, result, () => {
8218
- ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
8219
- for (const post of result.posts) {
8220
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8221
- ctx.out.log(`
8222
- \u2014 ${who}`);
8223
- ctx.out.log(bodyWithRefs(post));
8224
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8211
+ async function discussRead(ctx, id, parsed) {
8212
+ const requestedLimit = stringOpt(parsed.options.limit);
8213
+ const requestedOffset = stringOpt(parsed.options.offset);
8214
+ if (requestedLimit !== void 0 || requestedOffset !== void 0) {
8215
+ const query = new URLSearchParams({
8216
+ limit: requestedLimit ?? "200",
8217
+ offset: requestedOffset ?? "0"
8218
+ });
8219
+ const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
8220
+ emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
8221
+ return;
8222
+ }
8223
+ for (let scan = 0; scan < 3; scan++) {
8224
+ const posts = /* @__PURE__ */ new Map();
8225
+ let topic = null;
8226
+ let offset = 0;
8227
+ for (; ; ) {
8228
+ const page = await request(
8229
+ ctx,
8230
+ "GET",
8231
+ `/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
8232
+ );
8233
+ topic = page.topic;
8234
+ for (const post of page.posts) posts.set(post.id, post);
8235
+ if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
8236
+ if (!page.page?.hasMore) break;
8237
+ if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
8238
+ throw new Error("discuss read failed: registry returned a non-advancing post page");
8239
+ }
8240
+ offset = page.page.nextOffset;
8225
8241
  }
8226
- });
8242
+ const ordered = [...posts.values()].sort(
8243
+ (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
8244
+ );
8245
+ const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
8246
+ if (ordered.length === expected) {
8247
+ const result = { topic, posts: ordered };
8248
+ emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
8249
+ return;
8250
+ }
8251
+ if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
8252
+ }
8253
+ throw new Error("discuss read failed: conversation changed during every complete-read attempt");
8254
+ }
8255
+ function renderRead(ctx, topic, posts) {
8256
+ ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
8257
+ for (const post of posts) {
8258
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8259
+ ctx.out.log(`
8260
+ \u2014 ${who}`);
8261
+ ctx.out.log(bodyWithRefs(post));
8262
+ for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8263
+ }
8227
8264
  }
8228
8265
  async function discussPost(ctx, parsed) {
8229
8266
  const appId = stringOpt(parsed.options.app);
@@ -8272,61 +8309,214 @@ async function discussWho(ctx, parsed) {
8272
8309
  });
8273
8310
  }
8274
8311
 
8312
+ // src/discuss-watch-http.ts
8313
+ var WatchRequestError = class extends Error {
8314
+ constructor(message2, retryable, status, code) {
8315
+ super(message2);
8316
+ this.retryable = retryable;
8317
+ this.status = status;
8318
+ this.code = code;
8319
+ this.name = "WatchRequestError";
8320
+ }
8321
+ retryable;
8322
+ status;
8323
+ code;
8324
+ };
8325
+ var WatchCheckpointError = class extends Error {
8326
+ constructor(cursor, streamId) {
8327
+ super("discussion cursor requires a new checkpoint");
8328
+ this.cursor = cursor;
8329
+ this.streamId = streamId;
8330
+ this.name = "WatchCheckpointError";
8331
+ }
8332
+ cursor;
8333
+ streamId;
8334
+ code = "checkpoint_required";
8335
+ };
8336
+ var WatchRemoteError = class extends Error {
8337
+ constructor(cursor, cause) {
8338
+ super("discussion watch dependency remained unavailable", { cause });
8339
+ this.cursor = cursor;
8340
+ this.name = "WatchRemoteError";
8341
+ }
8342
+ cursor;
8343
+ code = "remote_unavailable";
8344
+ };
8345
+ var WatchTimeoutError = class extends Error {
8346
+ constructor(cursor) {
8347
+ super("nothing new before the timeout");
8348
+ this.cursor = cursor;
8349
+ this.name = "WatchTimeoutError";
8350
+ }
8351
+ cursor;
8352
+ code = "watch_timeout";
8353
+ };
8354
+ function requestPath(topicId, app, cursor) {
8355
+ const params = new URLSearchParams();
8356
+ if (topicId) params.set("topic", topicId);
8357
+ if (app) params.set("app", app);
8358
+ if (cursor) params.set("cursor", cursor);
8359
+ return `/watch?${params}`;
8360
+ }
8361
+ async function getWatchPage(ctx, path) {
8362
+ let res;
8363
+ try {
8364
+ res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
8365
+ headers: { authorization: `Bearer ${ctx.token}` }
8366
+ });
8367
+ } catch (error) {
8368
+ throw new WatchRequestError(
8369
+ `discuss watch request failed: ${error instanceof Error ? error.message : String(error)}`,
8370
+ true
8371
+ );
8372
+ }
8373
+ const data = await res.json().catch(() => ({}));
8374
+ if (res.status === 409 && data.code === "checkpoint_required") {
8375
+ throw new WatchCheckpointError(data.cursor, data.streamId);
8376
+ }
8377
+ if (!res.ok) {
8378
+ throw new WatchRequestError(
8379
+ `discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`,
8380
+ res.status === 429 || res.status >= 500,
8381
+ res.status,
8382
+ data.code ?? (res.status === 401 || res.status === 403 ? "auth_failed" : void 0)
8383
+ );
8384
+ }
8385
+ return data;
8386
+ }
8387
+
8275
8388
  // src/discuss-watch.ts
8276
8389
  var DEFAULT_INTERVAL_MS = 15e3;
8277
- var DEFAULT_TIMEOUT_MS = 15 * 6e4;
8390
+ var MAX_CONSECUTIVE_FAILURES = 5;
8391
+ var MAX_BACKOFF_MS = 3e4;
8278
8392
  function numberOpt2(parsed, flag, fallback) {
8279
8393
  const raw = stringOpt(parsed.options[flag]);
8280
8394
  const value = raw == null ? NaN : Number(raw);
8281
8395
  return Number.isFinite(value) && value > 0 ? value : fallback;
8282
8396
  }
8283
- async function get(ctx, path) {
8284
- const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
8285
- headers: { authorization: `Bearer ${ctx.token}` }
8286
- });
8287
- const data = await res.json().catch(() => ({}));
8288
- if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
8289
- return data;
8397
+ function jsonl(ctx, parsed, value) {
8398
+ if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value }));
8290
8399
  }
8291
8400
  async function discussWatch(ctx, topicId, parsed) {
8401
+ if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
8292
8402
  const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
8293
8403
  const now = ctx.now ?? Date.now;
8294
- const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
8295
- const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
8404
+ const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
8405
+ const timeoutSeconds = numberOpt2(parsed, "timeout");
8406
+ const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
8296
8407
  const by = stringOpt(parsed.options.by);
8297
8408
  const self = stringOpt(parsed.options.self);
8298
- const deadline = now() + timeoutMs;
8299
- const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
8300
- let seen = /* @__PURE__ */ new Set();
8301
- let since = now();
8302
- if (topicId) {
8303
- const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
8304
- seen = new Set(first.posts.map((post) => post.id));
8305
- }
8409
+ const app = stringOpt(parsed.options.app);
8410
+ let cursor = stringOpt(parsed.options.cursor);
8411
+ let firstSuccess = true;
8412
+ let consecutiveFailures = 0;
8306
8413
  for (; ; ) {
8307
- if (topicId) {
8308
- const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
8309
- const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
8310
- if (added.length > 0) return report2(ctx, { found: true, posts: added });
8311
- for (const post of posts) seen.add(post.id);
8312
- } else {
8313
- const app = stringOpt(parsed.options.app);
8314
- const { topics } = await get(
8315
- ctx,
8316
- `/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
8317
- );
8318
- const active = topics.filter((topic) => topic.lastActivityAt > since);
8319
- if (active.length > 0) return report2(ctx, { found: true, topics: active });
8320
- since = now();
8414
+ let page;
8415
+ try {
8416
+ page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
8417
+ consecutiveFailures = 0;
8418
+ } catch (error) {
8419
+ if (!(error instanceof WatchRequestError) || !error.retryable) {
8420
+ if (error instanceof WatchCheckpointError) {
8421
+ jsonl(ctx, parsed, {
8422
+ type: "status",
8423
+ state: "checkpoint_required",
8424
+ retryable: false,
8425
+ ...error.cursor ? { cursor: error.cursor } : {},
8426
+ ...error.streamId ? { streamId: error.streamId } : {}
8427
+ });
8428
+ }
8429
+ throw error;
8430
+ }
8431
+ consecutiveFailures++;
8432
+ jsonl(ctx, parsed, {
8433
+ type: "status",
8434
+ state: "degraded",
8435
+ retryable: true,
8436
+ attempt: consecutiveFailures,
8437
+ ...cursor ? { cursor } : {},
8438
+ ...error.status ? { status: error.status } : {}
8439
+ });
8440
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
8441
+ throw new WatchRemoteError(cursor, error);
8442
+ }
8443
+ if (deadline !== void 0 && now() >= deadline) {
8444
+ const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
8445
+ throw new WatchTimeoutError(result.cursor);
8446
+ }
8447
+ const base = Math.min(intervalMs, 1e3);
8448
+ const backoff = Math.min(MAX_BACKOFF_MS, base * 2 ** (consecutiveFailures - 1));
8449
+ await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
8450
+ continue;
8451
+ }
8452
+ cursor = page.cursor;
8453
+ const baseline = firstSuccess && page.events.length === 0;
8454
+ if (baseline) {
8455
+ jsonl(ctx, parsed, {
8456
+ type: "checkpoint",
8457
+ streamId: page.streamId,
8458
+ cursor,
8459
+ serverTime: page.serverTime
8460
+ });
8461
+ }
8462
+ firstSuccess = false;
8463
+ const matching = page.events.filter((event) => {
8464
+ if (topicId && (event.type !== "message" || event.action !== "created")) return false;
8465
+ return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
8466
+ });
8467
+ for (const event of matching) {
8468
+ jsonl(ctx, parsed, {
8469
+ type: "event",
8470
+ streamId: page.streamId,
8471
+ eventId: event.id,
8472
+ cursor: event.cursor,
8473
+ event
8474
+ });
8475
+ }
8476
+ if (matching.length > 0) {
8477
+ jsonl(ctx, parsed, {
8478
+ type: "checkpoint",
8479
+ streamId: page.streamId,
8480
+ cursor,
8481
+ serverTime: page.serverTime
8482
+ });
8483
+ const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
8484
+ const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
8485
+ return report2(ctx, parsed, {
8486
+ found: true,
8487
+ cursor,
8488
+ events: matching,
8489
+ ...posts && posts.length > 0 ? { posts } : {},
8490
+ ...topics && topics.length > 0 ? { topics } : {}
8491
+ });
8321
8492
  }
8322
- if (now() >= deadline) return report2(ctx, { found: false });
8323
- await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
8493
+ if (page.events.length > 0) {
8494
+ jsonl(ctx, parsed, {
8495
+ type: "checkpoint",
8496
+ streamId: page.streamId,
8497
+ cursor,
8498
+ serverTime: page.serverTime
8499
+ });
8500
+ } else if (!baseline) {
8501
+ jsonl(ctx, parsed, {
8502
+ type: "heartbeat",
8503
+ streamId: page.streamId,
8504
+ cursor,
8505
+ serverTime: page.serverTime
8506
+ });
8507
+ }
8508
+ if (page.hasMore) continue;
8509
+ if (deadline !== void 0 && now() >= deadline) {
8510
+ return report2(ctx, parsed, { found: false, cursor });
8511
+ }
8512
+ const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
8513
+ await sleep(wait2);
8324
8514
  }
8325
8515
  }
8326
- function report2(ctx, result) {
8516
+ function report2(ctx, parsed, result) {
8327
8517
  if (ctx.json) {
8328
8518
  ctx.out.log(JSON.stringify(result, null, 2));
8329
- } else if (result.found) {
8519
+ } else if (parsed.options.jsonl !== true && result.found) {
8330
8520
  for (const post of result.posts ?? []) {
8331
8521
  const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8332
8522
  ctx.out.log(`\u2014 ${who}
@@ -8338,13 +8528,6 @@ ${post.body}`);
8338
8528
  }
8339
8529
  return result;
8340
8530
  }
8341
- var WatchTimeoutError = class extends Error {
8342
- code = "watch_timeout";
8343
- constructor() {
8344
- super("nothing new before the timeout");
8345
- this.name = "WatchTimeoutError";
8346
- }
8347
- };
8348
8531
 
8349
8532
  // src/discuss-command.ts
8350
8533
  var ALLOWED = [
@@ -8366,6 +8549,8 @@ var ALLOWED = [
8366
8549
  "self",
8367
8550
  "interval",
8368
8551
  "timeout",
8552
+ "cursor",
8553
+ "jsonl",
8369
8554
  "mutation-id"
8370
8555
  ];
8371
8556
  function requireId(id, action) {
@@ -8402,7 +8587,7 @@ async function discussCommand(parsed, deps = {}) {
8402
8587
  case "topics":
8403
8588
  return discussList(ctx, parsed);
8404
8589
  case "read":
8405
- return discussRead(ctx, requireId(id, "read"));
8590
+ return discussRead(ctx, requireId(id, "read"), parsed);
8406
8591
  case "post":
8407
8592
  return discussPost(ctx, parsed);
8408
8593
  case "reply":
@@ -8413,7 +8598,7 @@ async function discussCommand(parsed, deps = {}) {
8413
8598
  return discussWho(ctx, parsed);
8414
8599
  case "watch": {
8415
8600
  const result = await discussWatch(ctx, id, parsed);
8416
- if (!result.found) throw new WatchTimeoutError();
8601
+ if (!result.found) throw new WatchTimeoutError(result.cursor);
8417
8602
  return;
8418
8603
  }
8419
8604
  default:
@@ -10377,7 +10562,12 @@ async function securityStatus(parsed, dependencies) {
10377
10562
  // src/cli.ts
10378
10563
  function exitCodeFor(err) {
10379
10564
  const code = err?.code;
10380
- return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
10565
+ if (code === "handshake_pending" || code === "watch_timeout") return 75;
10566
+ if (code === "checkpoint_required") return 3;
10567
+ if (code === "remote_unavailable") return 6;
10568
+ if (code === "auth_failed") return 5;
10569
+ if (code === "invalid_cursor") return 2;
10570
+ return 1;
10381
10571
  }
10382
10572
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10383
10573
  const runtime = {
@@ -10560,4 +10750,4 @@ export {
10560
10750
  exitCodeFor,
10561
10751
  runCli
10562
10752
  };
10563
- //# sourceMappingURL=chunk-PM5FBL3S.js.map
10753
+ //# sourceMappingURL=chunk-335ODYSF.js.map