@odla-ai/cli 0.25.6 → 0.25.8
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 -1
- package/dist/bin.cjs +144 -30
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-MWLZO2EZ.js → chunk-PM5FBL3S.js} +145 -31
- package/dist/chunk-PM5FBL3S.js.map +1 -0
- package/dist/index.cjs +144 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +4 -0
- package/dist/chunk-MWLZO2EZ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1080,11 +1080,6 @@ async function adminCommand(parsed) {
|
|
|
1080
1080
|
});
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
|
-
// src/app-export.ts
|
|
1084
|
-
var import_node_fs5 = require("fs");
|
|
1085
|
-
var import_node_stream = require("stream");
|
|
1086
|
-
var import_promises = require("stream/promises");
|
|
1087
|
-
|
|
1088
1083
|
// src/config.ts
|
|
1089
1084
|
var import_node_fs4 = require("fs");
|
|
1090
1085
|
var import_node_path4 = require("path");
|
|
@@ -1393,7 +1388,90 @@ function bothTenants(cfg) {
|
|
|
1393
1388
|
return { sandbox: (0, import_apps.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps.tenantIdFor)(cfg.app.id, "prod") };
|
|
1394
1389
|
}
|
|
1395
1390
|
|
|
1391
|
+
// src/agent-command.ts
|
|
1392
|
+
async function agentCommand(parsed, deps = {}) {
|
|
1393
|
+
const action = parsed.positionals[1];
|
|
1394
|
+
if (action !== "jobs" && action !== "retry") {
|
|
1395
|
+
throw new Error(`unknown agent action "${action ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
1396
|
+
}
|
|
1397
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action === "jobs" ? 2 : 3);
|
|
1398
|
+
if (action === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
1399
|
+
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
1400
|
+
}
|
|
1401
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
1402
|
+
const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
|
|
1403
|
+
const doFetch = deps.fetch ?? fetch;
|
|
1404
|
+
const out = deps.stdout ?? console;
|
|
1405
|
+
const credential2 = await getDeveloperToken(
|
|
1406
|
+
cfg,
|
|
1407
|
+
{
|
|
1408
|
+
configPath: cfg.configPath,
|
|
1409
|
+
token: stringOpt(parsed.options.token),
|
|
1410
|
+
email: stringOpt(parsed.options.email),
|
|
1411
|
+
open: false
|
|
1412
|
+
},
|
|
1413
|
+
doFetch,
|
|
1414
|
+
out
|
|
1415
|
+
);
|
|
1416
|
+
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
1417
|
+
const headers = { authorization: `Bearer ${credential2}` };
|
|
1418
|
+
if (action === "retry") {
|
|
1419
|
+
const id = parsed.positionals[2];
|
|
1420
|
+
const res2 = await doFetch(`${base}/${encodeURIComponent(id)}/retry`, { method: "POST", headers });
|
|
1421
|
+
const body2 = await readJson(res2);
|
|
1422
|
+
if (!res2.ok) throw new Error(`agent retry failed (${res2.status}): ${errorMessage(body2)}`);
|
|
1423
|
+
const result2 = { v: 1, appId: cfg.app.id, env, tenant, ...body2 };
|
|
1424
|
+
if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
|
|
1425
|
+
else out.log(`${tenant}: requeued ${id}`);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const state2 = stringOpt(parsed.options.state);
|
|
1429
|
+
const allowed = ["pending", "running", "succeeded", "dead_letter"];
|
|
1430
|
+
if (state2 && !allowed.includes(state2)) {
|
|
1431
|
+
throw new Error(`--state must be one of ${allowed.join(", ")}`);
|
|
1432
|
+
}
|
|
1433
|
+
const url = new URL(base);
|
|
1434
|
+
if (state2) url.searchParams.set("state", state2);
|
|
1435
|
+
const limit = numberOpt(parsed.options.limit, "--limit");
|
|
1436
|
+
if (limit) url.searchParams.set("limit", String(limit));
|
|
1437
|
+
const res = await doFetch(url, { headers });
|
|
1438
|
+
const body = await readJson(res);
|
|
1439
|
+
if (!res.ok) throw new Error(`agent jobs failed (${res.status}): ${errorMessage(body)}`);
|
|
1440
|
+
const result = { v: 1, appId: cfg.app.id, env, tenant, jobs: body.jobs ?? [], summary: body.summary };
|
|
1441
|
+
if (parsed.options.json === true) {
|
|
1442
|
+
out.log(JSON.stringify(result, null, 2));
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const summary = body.summary;
|
|
1446
|
+
out.log(
|
|
1447
|
+
`${tenant}: ${summary?.pending ?? 0} pending, ${summary?.running ?? 0} running, ${summary?.deadLetter ?? 0} dead-letter, ${summary?.succeeded ?? 0} succeeded`
|
|
1448
|
+
);
|
|
1449
|
+
for (const job of body.jobs ?? []) {
|
|
1450
|
+
out.log(
|
|
1451
|
+
[job.state, job.id, job.triggerId, `${job.entityNs}/${job.entityId}`, `attempts=${job.attempts}`, job.lastErrorCode ?? ""].filter(Boolean).join(" ")
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function readJson(res) {
|
|
1456
|
+
try {
|
|
1457
|
+
return await res.json();
|
|
1458
|
+
} catch {
|
|
1459
|
+
return {};
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function errorMessage(body) {
|
|
1463
|
+
const error = body.error;
|
|
1464
|
+
if (typeof error === "string") return error;
|
|
1465
|
+
if (error && typeof error === "object" && typeof error.message === "string") {
|
|
1466
|
+
return error.message;
|
|
1467
|
+
}
|
|
1468
|
+
return "request failed";
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1396
1471
|
// src/app-export.ts
|
|
1472
|
+
var import_node_fs5 = require("fs");
|
|
1473
|
+
var import_node_stream = require("stream");
|
|
1474
|
+
var import_promises = require("stream/promises");
|
|
1397
1475
|
async function appExport(options) {
|
|
1398
1476
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1399
1477
|
const out = options.stdout ?? console;
|
|
@@ -2371,6 +2449,7 @@ var CAPABILITIES = {
|
|
|
2371
2449
|
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
2372
2450
|
"validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
|
|
2373
2451
|
"read one versioned o11y status envelope spanning application RED, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, and a bounded machine verdict",
|
|
2452
|
+
"inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
|
|
2374
2453
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
2375
2454
|
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
2376
2455
|
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
@@ -4006,8 +4085,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
4006
4085
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
4007
4086
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
4008
4087
|
const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
|
|
4009
|
-
const entries = inventory.flatMap((
|
|
4010
|
-
const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(
|
|
4088
|
+
const entries = inventory.flatMap((record7) => {
|
|
4089
|
+
const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record7);
|
|
4011
4090
|
return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
|
|
4012
4091
|
});
|
|
4013
4092
|
if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
@@ -4255,8 +4334,8 @@ function normalize(value) {
|
|
|
4255
4334
|
if (Array.isArray(value)) return value.map(normalize);
|
|
4256
4335
|
if (value instanceof Uint8Array) return { $bytes: [...value] };
|
|
4257
4336
|
if (typeof value === "object") {
|
|
4258
|
-
const
|
|
4259
|
-
return Object.fromEntries(Object.keys(
|
|
4337
|
+
const record7 = value;
|
|
4338
|
+
return Object.fromEntries(Object.keys(record7).filter((key) => record7[key] !== void 0).sort().map((key) => [key, normalize(record7[key])]));
|
|
4260
4339
|
}
|
|
4261
4340
|
throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
|
|
4262
4341
|
}
|
|
@@ -6550,6 +6629,8 @@ Usage:
|
|
|
6550
6629
|
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
6551
6630
|
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
6552
6631
|
odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
|
|
6632
|
+
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
6633
|
+
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
6553
6634
|
odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
|
|
6554
6635
|
odla-ai whoami [--json]
|
|
6555
6636
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
@@ -6597,6 +6678,8 @@ Usage:
|
|
|
6597
6678
|
odla-ai version
|
|
6598
6679
|
|
|
6599
6680
|
Commands:
|
|
6681
|
+
agent Inspect durable agent wakeups and explicitly requeue a
|
|
6682
|
+
dead-lettered job; JSON output is stable for remote operators.
|
|
6600
6683
|
runbook odla's operational procedures, stored in the database and read at
|
|
6601
6684
|
the moment they are followed. "ask" gives a written, cited answer;
|
|
6602
6685
|
"search" the passages behind it; "get" the whole document.
|
|
@@ -7710,8 +7793,8 @@ async function pmAdd(ctx, entity, parsed) {
|
|
|
7710
7793
|
emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
7711
7794
|
}
|
|
7712
7795
|
async function pmGet(ctx, entity, id) {
|
|
7713
|
-
const { record:
|
|
7714
|
-
emit2(ctx,
|
|
7796
|
+
const { record: record7 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
7797
|
+
emit2(ctx, record7, () => printRecord(ctx, entity, record7));
|
|
7715
7798
|
}
|
|
7716
7799
|
async function pmSet(ctx, entity, id, parsed) {
|
|
7717
7800
|
const patch2 = collectEntityFields(entity, parsed, true);
|
|
@@ -7756,9 +7839,9 @@ async function pmHandoff(ctx, parsed) {
|
|
|
7756
7839
|
]);
|
|
7757
7840
|
const handoff = {
|
|
7758
7841
|
appId,
|
|
7759
|
-
unmetGoals: goals.filter((
|
|
7760
|
-
activeTasks: tasks.filter((
|
|
7761
|
-
openBugs: bugs.filter((
|
|
7842
|
+
unmetGoals: goals.filter((record7) => record7.status !== "met"),
|
|
7843
|
+
activeTasks: tasks.filter((record7) => record7.column !== "done"),
|
|
7844
|
+
openBugs: bugs.filter((record7) => record7.status !== "fixed" && record7.status !== "wontfix")
|
|
7762
7845
|
};
|
|
7763
7846
|
const result = {
|
|
7764
7847
|
...handoff,
|
|
@@ -7777,10 +7860,10 @@ async function pmHandoff(ctx, parsed) {
|
|
|
7777
7860
|
]) {
|
|
7778
7861
|
ctx.out.log(`${label}:`);
|
|
7779
7862
|
if (!records.length) ctx.out.log("- (none)");
|
|
7780
|
-
else for (const
|
|
7863
|
+
else for (const record7 of records) printRecord(
|
|
7781
7864
|
ctx,
|
|
7782
7865
|
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
7783
|
-
|
|
7866
|
+
record7
|
|
7784
7867
|
);
|
|
7785
7868
|
}
|
|
7786
7869
|
});
|
|
@@ -7921,6 +8004,21 @@ function statusVerdict(reads) {
|
|
|
7921
8004
|
severity: "degraded"
|
|
7922
8005
|
});
|
|
7923
8006
|
}
|
|
8007
|
+
const performance = record5(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
|
|
8008
|
+
if (performance?.status === "unavailable") {
|
|
8009
|
+
reasons.push({
|
|
8010
|
+
source: "liveSync",
|
|
8011
|
+
code: "subscription_metrics_unavailable",
|
|
8012
|
+
severity: "degraded"
|
|
8013
|
+
});
|
|
8014
|
+
}
|
|
8015
|
+
if (numeric2(performance?.telemetryDropped) > 0) {
|
|
8016
|
+
reasons.push({
|
|
8017
|
+
source: "liveSync",
|
|
8018
|
+
code: "subscription_telemetry_dropped",
|
|
8019
|
+
severity: "degraded"
|
|
8020
|
+
});
|
|
8021
|
+
}
|
|
7924
8022
|
const canaryStatus = String(reads.canary.body.status ?? "");
|
|
7925
8023
|
if (canaryStatus === "failing") {
|
|
7926
8024
|
reasons.push({
|
|
@@ -7968,6 +8066,12 @@ function statusVerdict(reads) {
|
|
|
7968
8066
|
reasons
|
|
7969
8067
|
};
|
|
7970
8068
|
}
|
|
8069
|
+
function record5(value) {
|
|
8070
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8071
|
+
}
|
|
8072
|
+
function numeric2(value) {
|
|
8073
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
8074
|
+
}
|
|
7971
8075
|
function collectorReason(read3, fallback) {
|
|
7972
8076
|
const reasons = read3.body.reasons;
|
|
7973
8077
|
if (!Array.isArray(reasons)) return fallback;
|
|
@@ -8025,7 +8129,7 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8025
8129
|
const [application, liveSync, canary, collector, provider] = await Promise.all([
|
|
8026
8130
|
read2(`${base}/metrics/red?${query}`, headers, doFetch),
|
|
8027
8131
|
read2(
|
|
8028
|
-
`${base}/live-sync/health?${
|
|
8132
|
+
`${base}/live-sync/health?${query}`,
|
|
8029
8133
|
headers,
|
|
8030
8134
|
doFetch
|
|
8031
8135
|
),
|
|
@@ -8045,7 +8149,7 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8045
8149
|
provider
|
|
8046
8150
|
});
|
|
8047
8151
|
const status = {
|
|
8048
|
-
schemaVersion:
|
|
8152
|
+
schemaVersion: 4,
|
|
8049
8153
|
scope: { appId, env, minutes },
|
|
8050
8154
|
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8051
8155
|
verdict,
|
|
@@ -8085,42 +8189,47 @@ function printStatus2(status, out) {
|
|
|
8085
8189
|
out.log(
|
|
8086
8190
|
`o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
|
|
8087
8191
|
);
|
|
8088
|
-
const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(
|
|
8192
|
+
const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
|
|
8089
8193
|
const requests = routes.reduce(
|
|
8090
|
-
(total, row) => total +
|
|
8194
|
+
(total, row) => total + numeric3(row.requests),
|
|
8091
8195
|
0
|
|
8092
8196
|
);
|
|
8093
8197
|
const errors = routes.reduce(
|
|
8094
|
-
(total, row) => total +
|
|
8198
|
+
(total, row) => total + numeric3(row.errors),
|
|
8095
8199
|
0
|
|
8096
8200
|
);
|
|
8097
8201
|
out.log(
|
|
8098
8202
|
`application ${status.application.httpStatus} ${requests} requests ${errors} errors`
|
|
8099
8203
|
);
|
|
8100
8204
|
out.log(
|
|
8101
|
-
|
|
8205
|
+
liveSyncLine(status.liveSync)
|
|
8102
8206
|
);
|
|
8103
|
-
const canaryDurations =
|
|
8207
|
+
const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
|
|
8104
8208
|
out.log(
|
|
8105
8209
|
`canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
|
|
8106
8210
|
);
|
|
8107
|
-
const collectorIngest =
|
|
8108
|
-
const collectorStorage =
|
|
8211
|
+
const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
|
|
8212
|
+
const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
|
|
8109
8213
|
out.log(
|
|
8110
|
-
`collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${
|
|
8214
|
+
`collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
|
|
8111
8215
|
);
|
|
8112
|
-
const providerMetrics =
|
|
8216
|
+
const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
|
|
8113
8217
|
out.log(
|
|
8114
|
-
`cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${
|
|
8218
|
+
`cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
|
|
8115
8219
|
);
|
|
8116
8220
|
out.log(
|
|
8117
8221
|
`verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
|
|
8118
8222
|
);
|
|
8119
8223
|
}
|
|
8120
|
-
function
|
|
8224
|
+
function liveSyncLine(read3) {
|
|
8225
|
+
const performance = record6(read3.body.performance) ? read3.body.performance : {};
|
|
8226
|
+
const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
|
|
8227
|
+
return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
|
|
8228
|
+
}
|
|
8229
|
+
function record6(value) {
|
|
8121
8230
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8122
8231
|
}
|
|
8123
|
-
function
|
|
8232
|
+
function numeric3(value) {
|
|
8124
8233
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
8125
8234
|
}
|
|
8126
8235
|
function optionalNumeric(value) {
|
|
@@ -8553,6 +8662,7 @@ var PM_ENTITIES = Object.fromEntries(
|
|
|
8553
8662
|
["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
|
|
8554
8663
|
);
|
|
8555
8664
|
var COMMAND_SURFACE = {
|
|
8665
|
+
agent: { jobs: {}, retry: {} },
|
|
8556
8666
|
admin: {
|
|
8557
8667
|
ai: {
|
|
8558
8668
|
show: {},
|
|
@@ -10388,6 +10498,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
10388
10498
|
await adminCommand(parsed);
|
|
10389
10499
|
return;
|
|
10390
10500
|
}
|
|
10501
|
+
if (command === "agent") {
|
|
10502
|
+
await agentCommand(parsed, runtime);
|
|
10503
|
+
return;
|
|
10504
|
+
}
|
|
10391
10505
|
if (command === "app") {
|
|
10392
10506
|
await appCommand(parsed, runtime);
|
|
10393
10507
|
return;
|