@odla-ai/cli 0.25.9 → 0.25.11

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
@@ -6623,7 +6623,7 @@ 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>]
@@ -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);
@@ -7811,7 +7848,7 @@ async function discussCommand(parsed, deps = {}) {
7811
7848
  case "topics":
7812
7849
  return discussList(ctx, parsed);
7813
7850
  case "read":
7814
- return discussRead(ctx, requireId(id, "read"));
7851
+ return discussRead(ctx, requireId(id, "read"), parsed);
7815
7852
  case "post":
7816
7853
  return discussPost(ctx, parsed);
7817
7854
  case "reply":
@@ -8138,6 +8175,7 @@ function statusVerdict(reads) {
8138
8175
  sourceHttp(reasons, "canary", reads.canary);
8139
8176
  sourceHttp(reasons, "collector", reads.collector);
8140
8177
  sourceHttp(reasons, "provider", reads.provider);
8178
+ sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
8141
8179
  const liveStatus = String(reads.liveSync.body.status ?? "");
8142
8180
  if (liveStatus === "partial") {
8143
8181
  reasons.push({
@@ -8209,6 +8247,14 @@ function statusVerdict(reads) {
8209
8247
  severity: "degraded"
8210
8248
  });
8211
8249
  }
8250
+ const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
8251
+ if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
8252
+ reasons.push({
8253
+ source: "provider",
8254
+ code: `provider_history_${providerHistoryStatus}`,
8255
+ severity: "degraded"
8256
+ });
8257
+ }
8212
8258
  return {
8213
8259
  status: reasons.some((reason) => reason.severity === "unhealthy") ? "unhealthy" : reasons.length > 0 ? "degraded" : "healthy",
8214
8260
  reasons
@@ -8228,11 +8274,11 @@ function collectorReason(read3, fallback) {
8228
8274
  );
8229
8275
  return typeof first?.code === "string" && first.code ? first.code : fallback;
8230
8276
  }
8231
- function sourceHttp(reasons, source, read3) {
8277
+ function sourceHttp(reasons, source, read3, prefix = "") {
8232
8278
  if (read3.httpStatus >= 200 && read3.httpStatus < 300) return;
8233
8279
  reasons.push({
8234
8280
  source,
8235
- code: `http_${read3.httpStatus}`,
8281
+ code: `${prefix}http_${read3.httpStatus}`,
8236
8282
  severity: read3.httpStatus >= 500 && read3.httpStatus !== 501 ? "unhealthy" : "degraded"
8237
8283
  });
8238
8284
  }
@@ -8274,7 +8320,7 @@ async function o11yCommand(parsed, deps = {}) {
8274
8320
  const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
8275
8321
  const query = new URLSearchParams({ env, minutes: String(minutes) });
8276
8322
  const headers = { authorization: `Bearer ${token}` };
8277
- const [application, liveSync, canary, collector, provider] = await Promise.all([
8323
+ const [application, liveSync, canary, collector, provider, providerHistory] = await Promise.all([
8278
8324
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
8279
8325
  read2(
8280
8326
  `${base}/live-sync/health?${query}`,
@@ -8287,17 +8333,19 @@ async function o11yCommand(parsed, deps = {}) {
8287
8333
  doFetch
8288
8334
  ),
8289
8335
  read2(`${base}/self-health?${query}`, headers, doFetch),
8290
- read2(`${base}/provider/cloudflare?${query}`, headers, doFetch)
8336
+ read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
8337
+ read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
8291
8338
  ]);
8292
8339
  const verdict = statusVerdict({
8293
8340
  application,
8294
8341
  liveSync,
8295
8342
  canary,
8296
8343
  collector,
8297
- provider
8344
+ provider,
8345
+ providerHistory
8298
8346
  });
8299
8347
  const status = {
8300
- schemaVersion: 4,
8348
+ schemaVersion: 5,
8301
8349
  scope: { appId, env, minutes },
8302
8350
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
8303
8351
  verdict,
@@ -8305,7 +8353,8 @@ async function o11yCommand(parsed, deps = {}) {
8305
8353
  liveSync,
8306
8354
  canary,
8307
8355
  collector,
8308
- provider
8356
+ provider,
8357
+ providerHistory
8309
8358
  };
8310
8359
  if (parsed.options.json === true) {
8311
8360
  out.log(JSON.stringify(status, null, 2));
@@ -8365,6 +8414,11 @@ function printStatus2(status, out) {
8365
8414
  out.log(
8366
8415
  `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
8367
8416
  );
8417
+ const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
8418
+ const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
8419
+ out.log(
8420
+ `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
8421
+ );
8368
8422
  out.log(
8369
8423
  `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8370
8424
  );
@@ -8383,6 +8437,10 @@ function numeric3(value) {
8383
8437
  function optionalNumeric(value) {
8384
8438
  return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
8385
8439
  }
8440
+ function optionalAge(value) {
8441
+ if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
8442
+ return `${Math.max(0, Math.round(value / 1e3))}s`;
8443
+ }
8386
8444
 
8387
8445
  // src/provision.ts
8388
8446
  var import_apps5 = require("@odla-ai/apps");