@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/README.md +13 -0
- package/dist/bin.cjs +201 -48
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-PM5FBL3S.js → chunk-HXUVCUEM.js} +202 -49
- package/dist/chunk-HXUVCUEM.js.map +1 -0
- package/dist/index.cjs +201 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-PM5FBL3S.js.map +0 -1
package/README.md
CHANGED
|
@@ -195,6 +195,8 @@ npx odla-ai provision --email owner@example.com --write-dev-vars --push-secrets
|
|
|
195
195
|
npx odla-ai smoke --env dev
|
|
196
196
|
npx odla-ai agent jobs --env dev --state dead_letter --json
|
|
197
197
|
npx odla-ai agent retry <job-id> --env dev --json
|
|
198
|
+
npx odla-ai discuss watch <topic-id> --jsonl
|
|
199
|
+
npx odla-ai discuss watch <topic-id> --cursor <saved-cursor> --jsonl
|
|
198
200
|
npx odla-ai secrets push --env dev
|
|
199
201
|
npx odla-ai secrets set clerk_webhook_secret --env dev --stdin
|
|
200
202
|
npx odla-ai secrets set-clerk-key --env dev --from-env CLERK_SECRET_KEY
|
|
@@ -225,6 +227,17 @@ currently running work. Successful receipts are retained for seven days (and
|
|
|
225
227
|
bounded to the newest 1,000 per tenant); unresolved dead letters remain until
|
|
226
228
|
an operator requeues them.
|
|
227
229
|
|
|
230
|
+
`discuss watch` waits at a server-issued snapshot boundary instead of comparing
|
|
231
|
+
client clocks or repeatedly reading a topic's oldest 200 posts. It is unbounded
|
|
232
|
+
unless `--timeout <seconds>` is supplied. `--jsonl` emits versioned
|
|
233
|
+
`checkpoint`, `event`, `heartbeat`, and degraded `status` records; persist the
|
|
234
|
+
latest cursor only after consuming its record and pass it back with `--cursor`
|
|
235
|
+
after a restart. Event IDs are stable for at-least-once deduplication. Transient
|
|
236
|
+
429/5xx/network failures retry with bounded exponential backoff without
|
|
237
|
+
advancing the cursor. A restore or truncated history exits 3 and emits an
|
|
238
|
+
explicit replacement checkpoint; an explicit timeout exits 75, and an
|
|
239
|
+
exhausted remote retry exits 6.
|
|
240
|
+
|
|
228
241
|
`code connect` is the first-party personal Code terminal runtime. Run the exact
|
|
229
242
|
command copied from **Studio → Code → Terminal** while your shell is in the
|
|
230
243
|
local Git checkout. `--platform`, `--app-id`, and `--env` make the requested
|
package/dist/bin.cjs
CHANGED
|
@@ -4608,13 +4608,13 @@ async function createConversionRegistry(config) {
|
|
|
4608
4608
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
4609
4609
|
}
|
|
4610
4610
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
4611
|
-
const
|
|
4611
|
+
const get = (id, kind) => {
|
|
4612
4612
|
const policy = policies.get(id);
|
|
4613
4613
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
4614
4614
|
return policy;
|
|
4615
4615
|
};
|
|
4616
4616
|
const checked = (source, id, kind) => {
|
|
4617
|
-
const policy =
|
|
4617
|
+
const policy = get(id, kind);
|
|
4618
4618
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
4619
4619
|
return policy;
|
|
4620
4620
|
};
|
|
@@ -6568,7 +6568,7 @@ Usage:
|
|
|
6568
6568
|
odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
|
|
6569
6569
|
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
6570
6570
|
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
6571
|
-
odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
|
|
6571
|
+
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
6572
6572
|
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
6573
6573
|
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
6574
6574
|
odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
|
|
@@ -7473,61 +7473,214 @@ async function discussWho(ctx, parsed) {
|
|
|
7473
7473
|
});
|
|
7474
7474
|
}
|
|
7475
7475
|
|
|
7476
|
+
// src/discuss-watch-http.ts
|
|
7477
|
+
var WatchRequestError = class extends Error {
|
|
7478
|
+
constructor(message2, retryable, status, code) {
|
|
7479
|
+
super(message2);
|
|
7480
|
+
this.retryable = retryable;
|
|
7481
|
+
this.status = status;
|
|
7482
|
+
this.code = code;
|
|
7483
|
+
this.name = "WatchRequestError";
|
|
7484
|
+
}
|
|
7485
|
+
retryable;
|
|
7486
|
+
status;
|
|
7487
|
+
code;
|
|
7488
|
+
};
|
|
7489
|
+
var WatchCheckpointError = class extends Error {
|
|
7490
|
+
constructor(cursor, streamId) {
|
|
7491
|
+
super("discussion cursor requires a new checkpoint");
|
|
7492
|
+
this.cursor = cursor;
|
|
7493
|
+
this.streamId = streamId;
|
|
7494
|
+
this.name = "WatchCheckpointError";
|
|
7495
|
+
}
|
|
7496
|
+
cursor;
|
|
7497
|
+
streamId;
|
|
7498
|
+
code = "checkpoint_required";
|
|
7499
|
+
};
|
|
7500
|
+
var WatchRemoteError = class extends Error {
|
|
7501
|
+
constructor(cursor, cause) {
|
|
7502
|
+
super("discussion watch dependency remained unavailable", { cause });
|
|
7503
|
+
this.cursor = cursor;
|
|
7504
|
+
this.name = "WatchRemoteError";
|
|
7505
|
+
}
|
|
7506
|
+
cursor;
|
|
7507
|
+
code = "remote_unavailable";
|
|
7508
|
+
};
|
|
7509
|
+
var WatchTimeoutError = class extends Error {
|
|
7510
|
+
constructor(cursor) {
|
|
7511
|
+
super("nothing new before the timeout");
|
|
7512
|
+
this.cursor = cursor;
|
|
7513
|
+
this.name = "WatchTimeoutError";
|
|
7514
|
+
}
|
|
7515
|
+
cursor;
|
|
7516
|
+
code = "watch_timeout";
|
|
7517
|
+
};
|
|
7518
|
+
function requestPath(topicId, app, cursor) {
|
|
7519
|
+
const params = new URLSearchParams();
|
|
7520
|
+
if (topicId) params.set("topic", topicId);
|
|
7521
|
+
if (app) params.set("app", app);
|
|
7522
|
+
if (cursor) params.set("cursor", cursor);
|
|
7523
|
+
return `/watch?${params}`;
|
|
7524
|
+
}
|
|
7525
|
+
async function getWatchPage(ctx, path) {
|
|
7526
|
+
let res;
|
|
7527
|
+
try {
|
|
7528
|
+
res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
7529
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
7530
|
+
});
|
|
7531
|
+
} catch (error) {
|
|
7532
|
+
throw new WatchRequestError(
|
|
7533
|
+
`discuss watch request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7534
|
+
true
|
|
7535
|
+
);
|
|
7536
|
+
}
|
|
7537
|
+
const data = await res.json().catch(() => ({}));
|
|
7538
|
+
if (res.status === 409 && data.code === "checkpoint_required") {
|
|
7539
|
+
throw new WatchCheckpointError(data.cursor, data.streamId);
|
|
7540
|
+
}
|
|
7541
|
+
if (!res.ok) {
|
|
7542
|
+
throw new WatchRequestError(
|
|
7543
|
+
`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`,
|
|
7544
|
+
res.status === 429 || res.status >= 500,
|
|
7545
|
+
res.status,
|
|
7546
|
+
data.code ?? (res.status === 401 || res.status === 403 ? "auth_failed" : void 0)
|
|
7547
|
+
);
|
|
7548
|
+
}
|
|
7549
|
+
return data;
|
|
7550
|
+
}
|
|
7551
|
+
|
|
7476
7552
|
// src/discuss-watch.ts
|
|
7477
7553
|
var DEFAULT_INTERVAL_MS = 15e3;
|
|
7478
|
-
var
|
|
7554
|
+
var MAX_CONSECUTIVE_FAILURES = 5;
|
|
7555
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
7479
7556
|
function numberOpt2(parsed, flag, fallback) {
|
|
7480
7557
|
const raw = stringOpt(parsed.options[flag]);
|
|
7481
7558
|
const value = raw == null ? NaN : Number(raw);
|
|
7482
7559
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
7483
7560
|
}
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
headers: { authorization: `Bearer ${ctx.token}` }
|
|
7487
|
-
});
|
|
7488
|
-
const data = await res.json().catch(() => ({}));
|
|
7489
|
-
if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
7490
|
-
return data;
|
|
7561
|
+
function jsonl(ctx, parsed, value) {
|
|
7562
|
+
if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value }));
|
|
7491
7563
|
}
|
|
7492
7564
|
async function discussWatch(ctx, topicId, parsed) {
|
|
7565
|
+
if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
|
|
7493
7566
|
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
7494
7567
|
const now = ctx.now ?? Date.now;
|
|
7495
|
-
const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
7496
|
-
const
|
|
7568
|
+
const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
7569
|
+
const timeoutSeconds = numberOpt2(parsed, "timeout");
|
|
7570
|
+
const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
|
|
7497
7571
|
const by = stringOpt(parsed.options.by);
|
|
7498
7572
|
const self = stringOpt(parsed.options.self);
|
|
7499
|
-
const
|
|
7500
|
-
|
|
7501
|
-
let
|
|
7502
|
-
let
|
|
7503
|
-
if (topicId) {
|
|
7504
|
-
const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
7505
|
-
seen = new Set(first.posts.map((post) => post.id));
|
|
7506
|
-
}
|
|
7573
|
+
const app = stringOpt(parsed.options.app);
|
|
7574
|
+
let cursor = stringOpt(parsed.options.cursor);
|
|
7575
|
+
let firstSuccess = true;
|
|
7576
|
+
let consecutiveFailures = 0;
|
|
7507
7577
|
for (; ; ) {
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7578
|
+
let page;
|
|
7579
|
+
try {
|
|
7580
|
+
page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
|
|
7581
|
+
consecutiveFailures = 0;
|
|
7582
|
+
} catch (error) {
|
|
7583
|
+
if (!(error instanceof WatchRequestError) || !error.retryable) {
|
|
7584
|
+
if (error instanceof WatchCheckpointError) {
|
|
7585
|
+
jsonl(ctx, parsed, {
|
|
7586
|
+
type: "status",
|
|
7587
|
+
state: "checkpoint_required",
|
|
7588
|
+
retryable: false,
|
|
7589
|
+
...error.cursor ? { cursor: error.cursor } : {},
|
|
7590
|
+
...error.streamId ? { streamId: error.streamId } : {}
|
|
7591
|
+
});
|
|
7592
|
+
}
|
|
7593
|
+
throw error;
|
|
7594
|
+
}
|
|
7595
|
+
consecutiveFailures++;
|
|
7596
|
+
jsonl(ctx, parsed, {
|
|
7597
|
+
type: "status",
|
|
7598
|
+
state: "degraded",
|
|
7599
|
+
retryable: true,
|
|
7600
|
+
attempt: consecutiveFailures,
|
|
7601
|
+
...cursor ? { cursor } : {},
|
|
7602
|
+
...error.status ? { status: error.status } : {}
|
|
7603
|
+
});
|
|
7604
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
7605
|
+
throw new WatchRemoteError(cursor, error);
|
|
7606
|
+
}
|
|
7607
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
7608
|
+
const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
7609
|
+
throw new WatchTimeoutError(result.cursor);
|
|
7610
|
+
}
|
|
7611
|
+
const base = Math.min(intervalMs, 1e3);
|
|
7612
|
+
const backoff = Math.min(MAX_BACKOFF_MS, base * 2 ** (consecutiveFailures - 1));
|
|
7613
|
+
await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
|
|
7614
|
+
continue;
|
|
7615
|
+
}
|
|
7616
|
+
cursor = page.cursor;
|
|
7617
|
+
const baseline = firstSuccess && page.events.length === 0;
|
|
7618
|
+
if (baseline) {
|
|
7619
|
+
jsonl(ctx, parsed, {
|
|
7620
|
+
type: "checkpoint",
|
|
7621
|
+
streamId: page.streamId,
|
|
7622
|
+
cursor,
|
|
7623
|
+
serverTime: page.serverTime
|
|
7624
|
+
});
|
|
7522
7625
|
}
|
|
7523
|
-
|
|
7524
|
-
|
|
7626
|
+
firstSuccess = false;
|
|
7627
|
+
const matching = page.events.filter((event) => {
|
|
7628
|
+
if (topicId && (event.type !== "message" || event.action !== "created")) return false;
|
|
7629
|
+
return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
|
|
7630
|
+
});
|
|
7631
|
+
for (const event of matching) {
|
|
7632
|
+
jsonl(ctx, parsed, {
|
|
7633
|
+
type: "event",
|
|
7634
|
+
streamId: page.streamId,
|
|
7635
|
+
eventId: event.id,
|
|
7636
|
+
cursor: event.cursor,
|
|
7637
|
+
event
|
|
7638
|
+
});
|
|
7639
|
+
}
|
|
7640
|
+
if (matching.length > 0) {
|
|
7641
|
+
jsonl(ctx, parsed, {
|
|
7642
|
+
type: "checkpoint",
|
|
7643
|
+
streamId: page.streamId,
|
|
7644
|
+
cursor,
|
|
7645
|
+
serverTime: page.serverTime
|
|
7646
|
+
});
|
|
7647
|
+
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
7648
|
+
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
7649
|
+
return report2(ctx, parsed, {
|
|
7650
|
+
found: true,
|
|
7651
|
+
cursor,
|
|
7652
|
+
events: matching,
|
|
7653
|
+
...posts && posts.length > 0 ? { posts } : {},
|
|
7654
|
+
...topics && topics.length > 0 ? { topics } : {}
|
|
7655
|
+
});
|
|
7656
|
+
}
|
|
7657
|
+
if (page.events.length > 0) {
|
|
7658
|
+
jsonl(ctx, parsed, {
|
|
7659
|
+
type: "checkpoint",
|
|
7660
|
+
streamId: page.streamId,
|
|
7661
|
+
cursor,
|
|
7662
|
+
serverTime: page.serverTime
|
|
7663
|
+
});
|
|
7664
|
+
} else if (!baseline) {
|
|
7665
|
+
jsonl(ctx, parsed, {
|
|
7666
|
+
type: "heartbeat",
|
|
7667
|
+
streamId: page.streamId,
|
|
7668
|
+
cursor,
|
|
7669
|
+
serverTime: page.serverTime
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7672
|
+
if (page.hasMore) continue;
|
|
7673
|
+
if (deadline !== void 0 && now() >= deadline) {
|
|
7674
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
7675
|
+
}
|
|
7676
|
+
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
7677
|
+
await sleep(wait2);
|
|
7525
7678
|
}
|
|
7526
7679
|
}
|
|
7527
|
-
function report2(ctx, result) {
|
|
7680
|
+
function report2(ctx, parsed, result) {
|
|
7528
7681
|
if (ctx.json) {
|
|
7529
7682
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
7530
|
-
} else if (result.found) {
|
|
7683
|
+
} else if (parsed.options.jsonl !== true && result.found) {
|
|
7531
7684
|
for (const post of result.posts ?? []) {
|
|
7532
7685
|
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
7533
7686
|
ctx.out.log(`\u2014 ${who}
|
|
@@ -7539,13 +7692,6 @@ ${post.body}`);
|
|
|
7539
7692
|
}
|
|
7540
7693
|
return result;
|
|
7541
7694
|
}
|
|
7542
|
-
var WatchTimeoutError = class extends Error {
|
|
7543
|
-
code = "watch_timeout";
|
|
7544
|
-
constructor() {
|
|
7545
|
-
super("nothing new before the timeout");
|
|
7546
|
-
this.name = "WatchTimeoutError";
|
|
7547
|
-
}
|
|
7548
|
-
};
|
|
7549
7695
|
|
|
7550
7696
|
// src/discuss-command.ts
|
|
7551
7697
|
var ALLOWED = [
|
|
@@ -7567,6 +7713,8 @@ var ALLOWED = [
|
|
|
7567
7713
|
"self",
|
|
7568
7714
|
"interval",
|
|
7569
7715
|
"timeout",
|
|
7716
|
+
"cursor",
|
|
7717
|
+
"jsonl",
|
|
7570
7718
|
"mutation-id"
|
|
7571
7719
|
];
|
|
7572
7720
|
function requireId(id, action) {
|
|
@@ -7614,7 +7762,7 @@ async function discussCommand(parsed, deps = {}) {
|
|
|
7614
7762
|
return discussWho(ctx, parsed);
|
|
7615
7763
|
case "watch": {
|
|
7616
7764
|
const result = await discussWatch(ctx, id, parsed);
|
|
7617
|
-
if (!result.found) throw new WatchTimeoutError();
|
|
7765
|
+
if (!result.found) throw new WatchTimeoutError(result.cursor);
|
|
7618
7766
|
return;
|
|
7619
7767
|
}
|
|
7620
7768
|
default:
|
|
@@ -10378,7 +10526,12 @@ async function securityStatus(parsed, dependencies) {
|
|
|
10378
10526
|
// src/cli.ts
|
|
10379
10527
|
function exitCodeFor(err) {
|
|
10380
10528
|
const code = err?.code;
|
|
10381
|
-
|
|
10529
|
+
if (code === "handshake_pending" || code === "watch_timeout") return 75;
|
|
10530
|
+
if (code === "checkpoint_required") return 3;
|
|
10531
|
+
if (code === "remote_unavailable") return 6;
|
|
10532
|
+
if (code === "auth_failed") return 5;
|
|
10533
|
+
if (code === "invalid_cursor") return 2;
|
|
10534
|
+
return 1;
|
|
10382
10535
|
}
|
|
10383
10536
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
10384
10537
|
const runtime = {
|