@hasna/loops 0.4.14 → 0.4.22
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/CHANGELOG.md +25 -0
- package/README.md +119 -29
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +825 -7
- package/dist/cli/index.js +5419 -2619
- package/dist/daemon/index.js +478 -84
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/index.js +2521 -226
- package/dist/lib/cloud/mode.d.ts +17 -0
- package/dist/lib/cloud/resolve.d.ts +16 -0
- package/dist/lib/cloud/storage.d.ts +82 -0
- package/dist/lib/cloud/transport.d.ts +148 -0
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/migration.d.ts +40 -0
- package/dist/lib/mode.js +3 -3
- package/dist/lib/route/index.d.ts +2 -0
- package/dist/lib/route/policies.d.ts +51 -0
- package/dist/lib/route/provider-admission.d.ts +37 -0
- package/dist/lib/route/throttle.d.ts +4 -0
- package/dist/lib/route/types.d.ts +8 -0
- package/dist/lib/run-receipts.d.ts +14 -0
- package/dist/lib/storage/contract.d.ts +7 -1
- package/dist/lib/storage/index.js +710 -31
- package/dist/lib/storage/postgres-loop-storage.d.ts +6 -0
- package/dist/lib/storage/postgres-schema.js +29 -0
- package/dist/lib/storage/postgres.js +29 -0
- package/dist/lib/storage/sqlite.d.ts +6 -0
- package/dist/lib/storage/sqlite.js +412 -18
- package/dist/lib/store/index.d.ts +268 -0
- package/dist/lib/store.d.ts +48 -1
- package/dist/lib/store.js +396 -18
- package/dist/lib/template-kit.d.ts +25 -0
- package/dist/lib/templates.d.ts +16 -1
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/index.js +1658 -168
- package/dist/runner/index.js +76 -9
- package/dist/sdk/http.d.ts +67 -0
- package/dist/sdk/http.js +21 -0
- package/dist/sdk/index.d.ts +50 -17
- package/dist/sdk/index.js +1509 -132
- package/dist/serve/index.js +1598 -122
- package/dist/types.d.ts +49 -0
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
- package/docs/CUTOVER-RUNBOOK.md +73 -56
- package/docs/DEPLOYMENT_MODES.md +35 -18
- package/docs/RUNTIME_BOUNDARY.md +203 -0
- package/docs/USAGE.md +145 -27
- package/package.json +3 -3
package/dist/mcp/index.js
CHANGED
|
@@ -1204,6 +1204,175 @@ function discardWorkflowRunManifest(staged) {
|
|
|
1204
1204
|
} catch {}
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
+
// src/lib/run-receipts.ts
|
|
1208
|
+
import { createHash as createHash2 } from "crypto";
|
|
1209
|
+
import { hostname } from "os";
|
|
1210
|
+
|
|
1211
|
+
// src/lib/run-envelope.ts
|
|
1212
|
+
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
1213
|
+
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
1214
|
+
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
1215
|
+
if (!text)
|
|
1216
|
+
return;
|
|
1217
|
+
const scrubbed = scrubSecrets(text);
|
|
1218
|
+
if (scrubbed.length <= limit * 2)
|
|
1219
|
+
return scrubbed;
|
|
1220
|
+
const omitted = scrubbed.length - limit * 2;
|
|
1221
|
+
return `${scrubbed.slice(0, limit)}
|
|
1222
|
+
[... ${omitted} chars omitted ...]
|
|
1223
|
+
${scrubbed.slice(-limit)}`;
|
|
1224
|
+
}
|
|
1225
|
+
function summarizeOutput(stdout, stderr) {
|
|
1226
|
+
return {
|
|
1227
|
+
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
1228
|
+
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
1229
|
+
stdoutExcerpt: boundedExcerpt(stdout),
|
|
1230
|
+
stderrExcerpt: boundedExcerpt(stderr)
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function summarizeExecutorResult(result) {
|
|
1234
|
+
return {
|
|
1235
|
+
...summarizeOutput(result.stdout, result.stderr),
|
|
1236
|
+
status: result.status,
|
|
1237
|
+
exitCode: result.exitCode,
|
|
1238
|
+
durationMs: result.durationMs,
|
|
1239
|
+
error: boundedExcerpt(result.error)
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function isBlockedStepRun(step) {
|
|
1243
|
+
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
1244
|
+
}
|
|
1245
|
+
function summarizeWorkflowStepRun(step) {
|
|
1246
|
+
return {
|
|
1247
|
+
...summarizeOutput(step.stdout, step.stderr),
|
|
1248
|
+
stepId: step.stepId,
|
|
1249
|
+
status: step.status,
|
|
1250
|
+
exitCode: step.exitCode,
|
|
1251
|
+
durationMs: step.durationMs,
|
|
1252
|
+
error: boundedExcerpt(step.error),
|
|
1253
|
+
blocked: isBlockedStepRun(step)
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function workflowRunEnvelope(run, steps) {
|
|
1257
|
+
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/lib/run-receipts.ts
|
|
1261
|
+
var RUN_RECEIPT_SUMMARY_TEXT_CHARS = 4096;
|
|
1262
|
+
var RUN_RECEIPT_MAX_IDS = 100;
|
|
1263
|
+
var RUN_RECEIPT_MAX_EVIDENCE_PATHS = 100;
|
|
1264
|
+
var RUN_RECEIPT_MAX_PATH_CHARS = 1024;
|
|
1265
|
+
function canonicalJson(value) {
|
|
1266
|
+
if (Array.isArray(value))
|
|
1267
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1268
|
+
if (value && typeof value === "object") {
|
|
1269
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
1270
|
+
}
|
|
1271
|
+
return JSON.stringify(value);
|
|
1272
|
+
}
|
|
1273
|
+
function digestReceipt(receipt) {
|
|
1274
|
+
return `sha256:${createHash2("sha256").update(canonicalJson(receipt)).digest("hex")}`;
|
|
1275
|
+
}
|
|
1276
|
+
function isoOrNull(value, label) {
|
|
1277
|
+
if (value === undefined || value === null || value === "")
|
|
1278
|
+
return null;
|
|
1279
|
+
const date = new Date(value);
|
|
1280
|
+
if (Number.isNaN(date.getTime()))
|
|
1281
|
+
throw new Error(`${label} must be a valid ISO date/time`);
|
|
1282
|
+
return date.toISOString();
|
|
1283
|
+
}
|
|
1284
|
+
function nonEmpty(value, label) {
|
|
1285
|
+
const trimmed = value?.trim();
|
|
1286
|
+
if (!trimmed)
|
|
1287
|
+
throw new Error(`${label} must be non-empty`);
|
|
1288
|
+
return trimmed;
|
|
1289
|
+
}
|
|
1290
|
+
function normalizedStringArray(values, opts) {
|
|
1291
|
+
const out = [];
|
|
1292
|
+
const seen = new Set;
|
|
1293
|
+
for (const raw of values ?? []) {
|
|
1294
|
+
const value = String(raw).trim();
|
|
1295
|
+
if (!value || seen.has(value))
|
|
1296
|
+
continue;
|
|
1297
|
+
seen.add(value);
|
|
1298
|
+
out.push(opts.itemMax ? value.slice(0, opts.itemMax) : value);
|
|
1299
|
+
if (out.length >= opts.max)
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
if ((values?.length ?? 0) > opts.max) {
|
|
1303
|
+
out.push(`[truncated ${values.length - opts.max} ${opts.label}]`);
|
|
1304
|
+
}
|
|
1305
|
+
return out;
|
|
1306
|
+
}
|
|
1307
|
+
function receiptMachine(input, opts) {
|
|
1308
|
+
if (typeof input.machine === "string")
|
|
1309
|
+
return nonEmpty(input.machine, "machine");
|
|
1310
|
+
if (input.machine && typeof input.machine === "object")
|
|
1311
|
+
return input.machine;
|
|
1312
|
+
if (opts.loop?.machine)
|
|
1313
|
+
return { ...opts.loop.machine };
|
|
1314
|
+
if (typeof opts.defaultMachine === "string")
|
|
1315
|
+
return nonEmpty(opts.defaultMachine, "machine");
|
|
1316
|
+
if (opts.defaultMachine && typeof opts.defaultMachine === "object")
|
|
1317
|
+
return opts.defaultMachine;
|
|
1318
|
+
return hostname();
|
|
1319
|
+
}
|
|
1320
|
+
function normalizeSummary(input, run) {
|
|
1321
|
+
const stdout = input.stdout ?? run?.stdout;
|
|
1322
|
+
const stderr = input.stderr ?? run?.stderr;
|
|
1323
|
+
const output = summarizeOutput(stdout, stderr);
|
|
1324
|
+
const summaryInput = input.summary;
|
|
1325
|
+
const provided = typeof summaryInput === "object" && summaryInput !== null ? summaryInput : {};
|
|
1326
|
+
const text = typeof summaryInput === "string" ? boundedExcerpt(summaryInput, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : typeof provided.text === "string" ? boundedExcerpt(provided.text, Math.floor(RUN_RECEIPT_SUMMARY_TEXT_CHARS / 2)) : undefined;
|
|
1327
|
+
return {
|
|
1328
|
+
text,
|
|
1329
|
+
stdout_bytes: Number.isFinite(provided.stdout_bytes) ? Number(provided.stdout_bytes) : output.stdoutBytes,
|
|
1330
|
+
stderr_bytes: Number.isFinite(provided.stderr_bytes) ? Number(provided.stderr_bytes) : output.stderrBytes,
|
|
1331
|
+
stdout_excerpt: typeof provided.stdout_excerpt === "string" ? boundedExcerpt(provided.stdout_excerpt) : output.stdoutExcerpt,
|
|
1332
|
+
stderr_excerpt: typeof provided.stderr_excerpt === "string" ? boundedExcerpt(provided.stderr_excerpt) : output.stderrExcerpt,
|
|
1333
|
+
error: typeof provided.error === "string" ? boundedExcerpt(provided.error) : boundedExcerpt(input.error ?? run?.error),
|
|
1334
|
+
duration_ms: Number.isFinite(provided.duration_ms) ? Number(provided.duration_ms) : input.duration_ms ?? run?.durationMs
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
function normalizeRunReceipt(input, opts = {}) {
|
|
1338
|
+
const now = (opts.now ?? new Date).toISOString();
|
|
1339
|
+
const run = opts.run;
|
|
1340
|
+
const loopId = input.loop_id ?? run?.loopId ?? opts.loop?.id;
|
|
1341
|
+
const normalized = {
|
|
1342
|
+
loop_id: nonEmpty(loopId, "loop_id"),
|
|
1343
|
+
run_id: nonEmpty(input.run_id ?? run?.id, "run_id"),
|
|
1344
|
+
machine: receiptMachine(input, opts),
|
|
1345
|
+
repo: nonEmpty(input.repo ?? opts.defaultRepo ?? targetRepo(opts.loop) ?? process.cwd(), "repo"),
|
|
1346
|
+
task_ids: normalizedStringArray(input.task_ids, { max: RUN_RECEIPT_MAX_IDS, label: "task_ids" }),
|
|
1347
|
+
knowledge_ids: normalizedStringArray(input.knowledge_ids, { max: RUN_RECEIPT_MAX_IDS, label: "knowledge_ids" }),
|
|
1348
|
+
started_at: isoOrNull(input.started_at ?? run?.startedAt, "started_at"),
|
|
1349
|
+
finished_at: isoOrNull(input.finished_at ?? run?.finishedAt, "finished_at"),
|
|
1350
|
+
status: nonEmpty(input.status ?? run?.status, "status"),
|
|
1351
|
+
exit_code: input.exit_code === undefined ? run?.exitCode ?? null : input.exit_code,
|
|
1352
|
+
summary: normalizeSummary(input, run),
|
|
1353
|
+
evidence_paths: normalizedStringArray(input.evidence_paths, {
|
|
1354
|
+
max: RUN_RECEIPT_MAX_EVIDENCE_PATHS,
|
|
1355
|
+
label: "evidence_paths",
|
|
1356
|
+
itemMax: RUN_RECEIPT_MAX_PATH_CHARS
|
|
1357
|
+
})
|
|
1358
|
+
};
|
|
1359
|
+
return {
|
|
1360
|
+
...normalized,
|
|
1361
|
+
digest_id: input.digest_id?.trim() || digestReceipt(normalized),
|
|
1362
|
+
created_at: opts.existing?.created_at ?? now,
|
|
1363
|
+
updated_at: now
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function targetRepo(loop) {
|
|
1367
|
+
if (!loop)
|
|
1368
|
+
return;
|
|
1369
|
+
if (loop.target.type === "command")
|
|
1370
|
+
return loop.target.cwd;
|
|
1371
|
+
if (loop.target.type === "agent")
|
|
1372
|
+
return loop.target.cwd;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1207
1376
|
// src/lib/route/todos-cli.ts
|
|
1208
1377
|
import { closeSync, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
|
|
1209
1378
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -1275,6 +1444,9 @@ function publicRun(run, showOutput = false, opts = {}) {
|
|
|
1275
1444
|
error: opts.redactError ? redact(run.error) : run.error
|
|
1276
1445
|
};
|
|
1277
1446
|
}
|
|
1447
|
+
function publicRunReceipt(receipt) {
|
|
1448
|
+
return { ...receipt };
|
|
1449
|
+
}
|
|
1278
1450
|
function publicExecutorResult(result, showOutput = false) {
|
|
1279
1451
|
return {
|
|
1280
1452
|
...result,
|
|
@@ -1398,7 +1570,7 @@ function todosMutationSummary(result) {
|
|
|
1398
1570
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
1399
1571
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
1400
1572
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
1401
|
-
var SCHEMA_USER_VERSION =
|
|
1573
|
+
var SCHEMA_USER_VERSION = 8;
|
|
1402
1574
|
var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
1403
1575
|
var PRUNE_BATCH_SIZE = 400;
|
|
1404
1576
|
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
@@ -1454,6 +1626,25 @@ function rowToRun(row) {
|
|
|
1454
1626
|
updatedAt: row.updated_at
|
|
1455
1627
|
};
|
|
1456
1628
|
}
|
|
1629
|
+
function rowToRunReceipt(row) {
|
|
1630
|
+
return {
|
|
1631
|
+
loop_id: row.loop_id,
|
|
1632
|
+
run_id: row.run_id,
|
|
1633
|
+
machine: JSON.parse(row.machine_json),
|
|
1634
|
+
repo: row.repo,
|
|
1635
|
+
task_ids: JSON.parse(row.task_ids_json),
|
|
1636
|
+
knowledge_ids: JSON.parse(row.knowledge_ids_json),
|
|
1637
|
+
digest_id: row.digest_id,
|
|
1638
|
+
started_at: row.started_at,
|
|
1639
|
+
finished_at: row.finished_at,
|
|
1640
|
+
status: row.status,
|
|
1641
|
+
exit_code: row.exit_code,
|
|
1642
|
+
summary: JSON.parse(row.summary_json),
|
|
1643
|
+
evidence_paths: JSON.parse(row.evidence_paths_json),
|
|
1644
|
+
created_at: row.created_at,
|
|
1645
|
+
updated_at: row.updated_at
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1457
1648
|
function latestRunTime(row) {
|
|
1458
1649
|
return row.finished_at ?? row.started_at ?? row.created_at;
|
|
1459
1650
|
}
|
|
@@ -1517,6 +1708,7 @@ function rowToWorkflowWorkItem(row) {
|
|
|
1517
1708
|
subjectRef: row.subject_ref,
|
|
1518
1709
|
projectKey: row.project_key ?? undefined,
|
|
1519
1710
|
projectGroup: row.project_group ?? undefined,
|
|
1711
|
+
machineId: row.machine_id ?? undefined,
|
|
1520
1712
|
routeScope: row.route_scope ?? undefined,
|
|
1521
1713
|
priority: row.priority,
|
|
1522
1714
|
status: row.status,
|
|
@@ -1674,6 +1866,7 @@ function scrubbedOrNull(value) {
|
|
|
1674
1866
|
return value == null ? null : scrubSecrets(value);
|
|
1675
1867
|
}
|
|
1676
1868
|
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1869
|
+
var MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS = 64 * 1024;
|
|
1677
1870
|
function clampPersistedRunOutput(value) {
|
|
1678
1871
|
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1679
1872
|
return value;
|
|
@@ -1688,6 +1881,42 @@ ${tail}`;
|
|
|
1688
1881
|
function persistedRunOutput(value) {
|
|
1689
1882
|
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1690
1883
|
}
|
|
1884
|
+
function clampTextToChars(value, maxChars, reason) {
|
|
1885
|
+
if (value.length <= maxChars)
|
|
1886
|
+
return value;
|
|
1887
|
+
const marker = `
|
|
1888
|
+
...[truncated by ${reason}]...
|
|
1889
|
+
`;
|
|
1890
|
+
const budget = Math.max(0, maxChars - marker.length);
|
|
1891
|
+
const headLength = Math.ceil(budget / 2);
|
|
1892
|
+
const tailLength = Math.floor(budget / 2);
|
|
1893
|
+
return `${value.slice(0, headLength)}${marker}${value.slice(value.length - tailLength)}`;
|
|
1894
|
+
}
|
|
1895
|
+
function boundedWorkflowEventPayloadJson(scrubbedJson) {
|
|
1896
|
+
if (scrubbedJson.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS)
|
|
1897
|
+
return scrubbedJson;
|
|
1898
|
+
const base = {
|
|
1899
|
+
truncated: true,
|
|
1900
|
+
originalChars: scrubbedJson.length,
|
|
1901
|
+
maxChars: MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS,
|
|
1902
|
+
preview: ""
|
|
1903
|
+
};
|
|
1904
|
+
const baseChars = JSON.stringify(base).length;
|
|
1905
|
+
let previewBudget = Math.max(0, MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS - baseChars - 64);
|
|
1906
|
+
while (true) {
|
|
1907
|
+
const preview = clampTextToChars(scrubbedJson, previewBudget, "loops workflow-event payload retention");
|
|
1908
|
+
const bounded = JSON.stringify({ ...base, preview });
|
|
1909
|
+
if (bounded.length <= MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS || previewBudget === 0)
|
|
1910
|
+
return bounded;
|
|
1911
|
+
previewBudget = Math.max(0, previewBudget - (bounded.length - MAX_PERSISTED_WORKFLOW_EVENT_PAYLOAD_CHARS) - 64);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function persistedWorkflowEventPayload(payload) {
|
|
1915
|
+
if (payload == null)
|
|
1916
|
+
return null;
|
|
1917
|
+
const scrubbed = scrubSecretsDeep(payload);
|
|
1918
|
+
return boundedWorkflowEventPayloadJson(scrubSecrets(JSON.stringify(scrubbed)));
|
|
1919
|
+
}
|
|
1691
1920
|
function chmodIfExists(path, mode) {
|
|
1692
1921
|
try {
|
|
1693
1922
|
if (existsSync(path))
|
|
@@ -1805,6 +2034,17 @@ class Store {
|
|
|
1805
2034
|
this.addColumnIfMissing("workflow_work_items", "route_scope", "TEXT");
|
|
1806
2035
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status)");
|
|
1807
2036
|
}
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
id: "0009_run_receipts",
|
|
2040
|
+
apply: () => this.createRunReceiptsSchema()
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
id: "0010_work_item_machine_id",
|
|
2044
|
+
apply: () => {
|
|
2045
|
+
this.addColumnIfMissing("workflow_work_items", "machine_id", "TEXT");
|
|
2046
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_machine ON workflow_work_items(machine_id, status)");
|
|
2047
|
+
}
|
|
1808
2048
|
}
|
|
1809
2049
|
];
|
|
1810
2050
|
}
|
|
@@ -1866,6 +2106,28 @@ class Store {
|
|
|
1866
2106
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1867
2107
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1868
2108
|
|
|
2109
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2110
|
+
run_id TEXT PRIMARY KEY,
|
|
2111
|
+
loop_id TEXT NOT NULL,
|
|
2112
|
+
machine_json TEXT NOT NULL,
|
|
2113
|
+
repo TEXT NOT NULL,
|
|
2114
|
+
task_ids_json TEXT NOT NULL,
|
|
2115
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2116
|
+
digest_id TEXT NOT NULL,
|
|
2117
|
+
started_at TEXT,
|
|
2118
|
+
finished_at TEXT,
|
|
2119
|
+
status TEXT NOT NULL,
|
|
2120
|
+
exit_code INTEGER,
|
|
2121
|
+
summary_json TEXT NOT NULL,
|
|
2122
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2123
|
+
created_at TEXT NOT NULL,
|
|
2124
|
+
updated_at TEXT NOT NULL
|
|
2125
|
+
);
|
|
2126
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2127
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2128
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2129
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2130
|
+
|
|
1869
2131
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1870
2132
|
id TEXT PRIMARY KEY,
|
|
1871
2133
|
pid INTEGER NOT NULL,
|
|
@@ -1952,6 +2214,7 @@ class Store {
|
|
|
1952
2214
|
subject_ref TEXT NOT NULL,
|
|
1953
2215
|
project_key TEXT,
|
|
1954
2216
|
project_group TEXT,
|
|
2217
|
+
machine_id TEXT,
|
|
1955
2218
|
route_scope TEXT,
|
|
1956
2219
|
priority INTEGER NOT NULL,
|
|
1957
2220
|
status TEXT NOT NULL,
|
|
@@ -1970,10 +2233,10 @@ class Store {
|
|
|
1970
2233
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
1971
2234
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
1972
2235
|
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
1973
|
-
--
|
|
1974
|
-
--
|
|
2236
|
+
-- New-column indexes (route_scope, machine_id, etc.) are created ONLY by
|
|
2237
|
+
-- their additive migrations, never here: this baseline DDL
|
|
1975
2238
|
-- re-runs on EVERY open (0001 is not skip-guarded), and on a pre-0008
|
|
1976
|
-
-- database the CREATE TABLE above is a no-op, so an index on
|
|
2239
|
+
-- database the CREATE TABLE above is a no-op, so an index on a new column
|
|
1977
2240
|
-- here would execute before the column exists and crash the open
|
|
1978
2241
|
-- ("no such column: route_scope"). New columns may be folded into the
|
|
1979
2242
|
-- CREATE TABLE (fresh-db only); their indexes must live in the migration.
|
|
@@ -2084,6 +2347,31 @@ class Store {
|
|
|
2084
2347
|
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
2085
2348
|
`);
|
|
2086
2349
|
}
|
|
2350
|
+
createRunReceiptsSchema() {
|
|
2351
|
+
this.db.exec(`
|
|
2352
|
+
CREATE TABLE IF NOT EXISTS run_receipts (
|
|
2353
|
+
run_id TEXT PRIMARY KEY,
|
|
2354
|
+
loop_id TEXT NOT NULL,
|
|
2355
|
+
machine_json TEXT NOT NULL,
|
|
2356
|
+
repo TEXT NOT NULL,
|
|
2357
|
+
task_ids_json TEXT NOT NULL,
|
|
2358
|
+
knowledge_ids_json TEXT NOT NULL,
|
|
2359
|
+
digest_id TEXT NOT NULL,
|
|
2360
|
+
started_at TEXT,
|
|
2361
|
+
finished_at TEXT,
|
|
2362
|
+
status TEXT NOT NULL,
|
|
2363
|
+
exit_code INTEGER,
|
|
2364
|
+
summary_json TEXT NOT NULL,
|
|
2365
|
+
evidence_paths_json TEXT NOT NULL,
|
|
2366
|
+
created_at TEXT NOT NULL,
|
|
2367
|
+
updated_at TEXT NOT NULL
|
|
2368
|
+
);
|
|
2369
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_loop ON run_receipts(loop_id, created_at DESC);
|
|
2370
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_repo ON run_receipts(repo, created_at DESC);
|
|
2371
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_digest ON run_receipts(digest_id);
|
|
2372
|
+
CREATE INDEX IF NOT EXISTS idx_run_receipts_status ON run_receipts(status, created_at DESC);
|
|
2373
|
+
`);
|
|
2374
|
+
}
|
|
2087
2375
|
addColumnIfMissing(table, column, definition) {
|
|
2088
2376
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
2089
2377
|
if (columns.some((c) => c.name === column))
|
|
@@ -2195,19 +2483,24 @@ class Store {
|
|
|
2195
2483
|
}
|
|
2196
2484
|
listLoops(opts = {}) {
|
|
2197
2485
|
const limit = opts.limit ?? 200;
|
|
2486
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
2487
|
+
if (opts.name != null) {
|
|
2488
|
+
const rows2 = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.name, limit, offset);
|
|
2489
|
+
return this.withLatestRunSummaries(rows2.map(rowToLoop));
|
|
2490
|
+
}
|
|
2198
2491
|
let rows;
|
|
2199
2492
|
if (opts.status && opts.archived) {
|
|
2200
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2493
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2201
2494
|
} else if (opts.status && opts.includeArchived) {
|
|
2202
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2495
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2203
2496
|
} else if (opts.status) {
|
|
2204
|
-
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
2497
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
2205
2498
|
} else if (opts.archived) {
|
|
2206
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
|
|
2499
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2207
2500
|
} else if (opts.includeArchived) {
|
|
2208
|
-
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2501
|
+
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2209
2502
|
} else {
|
|
2210
|
-
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
2503
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ? OFFSET ?").all(limit, offset);
|
|
2211
2504
|
}
|
|
2212
2505
|
return this.withLatestRunSummaries(rows.map(rowToLoop));
|
|
2213
2506
|
}
|
|
@@ -2885,10 +3178,10 @@ class Store {
|
|
|
2885
3178
|
const id = genId();
|
|
2886
3179
|
const status = input.status ?? "queued";
|
|
2887
3180
|
this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
|
|
2888
|
-
subject_ref, project_key, project_group, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
3181
|
+
subject_ref, project_key, project_group, machine_id, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
2889
3182
|
workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
|
|
2890
3183
|
VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
|
|
2891
|
-
$projectKey, $projectGroup, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
3184
|
+
$projectKey, $projectGroup, $machineId, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
2892
3185
|
$lastReason, $created, $updated)
|
|
2893
3186
|
ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
|
|
2894
3187
|
invocation_id=excluded.invocation_id,
|
|
@@ -2897,6 +3190,10 @@ class Store {
|
|
|
2897
3190
|
subject_ref=excluded.subject_ref,
|
|
2898
3191
|
project_key=excluded.project_key,
|
|
2899
3192
|
project_group=excluded.project_group,
|
|
3193
|
+
machine_id=CASE
|
|
3194
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.machine_id
|
|
3195
|
+
ELSE excluded.machine_id
|
|
3196
|
+
END,
|
|
2900
3197
|
route_scope=excluded.route_scope,
|
|
2901
3198
|
priority=excluded.priority,
|
|
2902
3199
|
status=CASE
|
|
@@ -2939,6 +3236,7 @@ class Store {
|
|
|
2939
3236
|
$subjectRef: input.subjectRef,
|
|
2940
3237
|
$projectKey: input.projectKey ?? null,
|
|
2941
3238
|
$projectGroup: input.projectGroup ?? null,
|
|
3239
|
+
$machineId: input.machineId ?? null,
|
|
2942
3240
|
$routeScope: input.routeScope ?? null,
|
|
2943
3241
|
$priority: input.priority ?? 0,
|
|
2944
3242
|
$status: status,
|
|
@@ -3448,7 +3746,7 @@ class Store {
|
|
|
3448
3746
|
VALUES ($id, $workflowRunId, 1, 'created', NULL, $payload, $created)`).run({
|
|
3449
3747
|
$id: genId(),
|
|
3450
3748
|
$workflowRunId: runId,
|
|
3451
|
-
$payload:
|
|
3749
|
+
$payload: persistedWorkflowEventPayload({
|
|
3452
3750
|
workflowId: input.workflow.id,
|
|
3453
3751
|
workflowName: input.workflow.name,
|
|
3454
3752
|
stepCount: input.workflow.steps.length,
|
|
@@ -3779,7 +4077,7 @@ class Store {
|
|
|
3779
4077
|
$sequence: sequence,
|
|
3780
4078
|
$eventType: eventType,
|
|
3781
4079
|
$stepId: stepId ?? null,
|
|
3782
|
-
$payload:
|
|
4080
|
+
$payload: persistedWorkflowEventPayload(payload),
|
|
3783
4081
|
$created: now
|
|
3784
4082
|
});
|
|
3785
4083
|
const event = this.db.query("SELECT * FROM workflow_events WHERE id = ?").get(id);
|
|
@@ -4102,18 +4400,93 @@ class Store {
|
|
|
4102
4400
|
}
|
|
4103
4401
|
listRuns(opts = {}) {
|
|
4104
4402
|
const limit = opts.limit ?? 100;
|
|
4403
|
+
const offset = Math.max(0, Math.floor(opts.offset ?? 0));
|
|
4105
4404
|
let rows;
|
|
4106
4405
|
if (opts.loopId && opts.status) {
|
|
4107
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, opts.status, limit);
|
|
4406
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? AND status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, opts.status, limit, offset);
|
|
4108
4407
|
} else if (opts.loopId) {
|
|
4109
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ?").all(opts.loopId, limit);
|
|
4408
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE loop_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.loopId, limit, offset);
|
|
4110
4409
|
} else if (opts.status) {
|
|
4111
|
-
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ?").all(opts.status, limit);
|
|
4410
|
+
rows = this.db.query("SELECT * FROM loop_runs WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.status, limit, offset);
|
|
4112
4411
|
} else {
|
|
4113
|
-
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
4412
|
+
rows = this.db.query("SELECT * FROM loop_runs ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
4114
4413
|
}
|
|
4115
4414
|
return rows.map(rowToRun);
|
|
4116
4415
|
}
|
|
4416
|
+
writeRunReceipt(input, opts = {}) {
|
|
4417
|
+
const inputRunId = typeof input.run_id === "string" && input.run_id.trim() ? input.run_id : undefined;
|
|
4418
|
+
const existing = inputRunId ? this.getRunReceipt(inputRunId) : undefined;
|
|
4419
|
+
const run = inputRunId ? this.getRun(inputRunId) : undefined;
|
|
4420
|
+
const loop = input.loop_id ? this.getLoop(input.loop_id) : run ? this.getLoop(run.loopId) : undefined;
|
|
4421
|
+
const receipt = normalizeRunReceipt(input, { now: opts.now, run, loop, existing });
|
|
4422
|
+
this.db.query(`INSERT INTO run_receipts (run_id, loop_id, machine_json, repo, task_ids_json, knowledge_ids_json, digest_id,
|
|
4423
|
+
started_at, finished_at, status, exit_code, summary_json, evidence_paths_json, created_at, updated_at)
|
|
4424
|
+
VALUES ($runId, $loopId, $machineJson, $repo, $taskIdsJson, $knowledgeIdsJson, $digestId,
|
|
4425
|
+
$startedAt, $finishedAt, $status, $exitCode, $summaryJson, $evidencePathsJson, $createdAt, $updatedAt)
|
|
4426
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
4427
|
+
loop_id=excluded.loop_id,
|
|
4428
|
+
machine_json=excluded.machine_json,
|
|
4429
|
+
repo=excluded.repo,
|
|
4430
|
+
task_ids_json=excluded.task_ids_json,
|
|
4431
|
+
knowledge_ids_json=excluded.knowledge_ids_json,
|
|
4432
|
+
digest_id=excluded.digest_id,
|
|
4433
|
+
started_at=excluded.started_at,
|
|
4434
|
+
finished_at=excluded.finished_at,
|
|
4435
|
+
status=excluded.status,
|
|
4436
|
+
exit_code=excluded.exit_code,
|
|
4437
|
+
summary_json=excluded.summary_json,
|
|
4438
|
+
evidence_paths_json=excluded.evidence_paths_json,
|
|
4439
|
+
updated_at=excluded.updated_at`).run({
|
|
4440
|
+
$runId: receipt.run_id,
|
|
4441
|
+
$loopId: receipt.loop_id,
|
|
4442
|
+
$machineJson: JSON.stringify(receipt.machine),
|
|
4443
|
+
$repo: receipt.repo,
|
|
4444
|
+
$taskIdsJson: JSON.stringify(receipt.task_ids),
|
|
4445
|
+
$knowledgeIdsJson: JSON.stringify(receipt.knowledge_ids),
|
|
4446
|
+
$digestId: receipt.digest_id,
|
|
4447
|
+
$startedAt: receipt.started_at,
|
|
4448
|
+
$finishedAt: receipt.finished_at,
|
|
4449
|
+
$status: receipt.status,
|
|
4450
|
+
$exitCode: receipt.exit_code,
|
|
4451
|
+
$summaryJson: JSON.stringify(receipt.summary),
|
|
4452
|
+
$evidencePathsJson: JSON.stringify(receipt.evidence_paths),
|
|
4453
|
+
$createdAt: receipt.created_at,
|
|
4454
|
+
$updatedAt: receipt.updated_at
|
|
4455
|
+
});
|
|
4456
|
+
return this.getRunReceipt(receipt.run_id) ?? receipt;
|
|
4457
|
+
}
|
|
4458
|
+
getRunReceipt(runId) {
|
|
4459
|
+
const row = this.db.query("SELECT * FROM run_receipts WHERE run_id = ?").get(runId);
|
|
4460
|
+
return row ? rowToRunReceipt(row) : undefined;
|
|
4461
|
+
}
|
|
4462
|
+
listRunReceipts(opts = {}) {
|
|
4463
|
+
const limit = opts.limit ?? 100;
|
|
4464
|
+
const filters = [];
|
|
4465
|
+
const params = [];
|
|
4466
|
+
if (opts.loopId) {
|
|
4467
|
+
filters.push("loop_id = ?");
|
|
4468
|
+
params.push(opts.loopId);
|
|
4469
|
+
}
|
|
4470
|
+
if (opts.repo) {
|
|
4471
|
+
filters.push("repo = ?");
|
|
4472
|
+
params.push(opts.repo);
|
|
4473
|
+
}
|
|
4474
|
+
if (opts.status) {
|
|
4475
|
+
filters.push("status = ?");
|
|
4476
|
+
params.push(opts.status);
|
|
4477
|
+
}
|
|
4478
|
+
if (opts.taskId) {
|
|
4479
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.task_ids_json) WHERE value = ?)");
|
|
4480
|
+
params.push(opts.taskId);
|
|
4481
|
+
}
|
|
4482
|
+
if (opts.knowledgeId) {
|
|
4483
|
+
filters.push("EXISTS (SELECT 1 FROM json_each(run_receipts.knowledge_ids_json) WHERE value = ?)");
|
|
4484
|
+
params.push(opts.knowledgeId);
|
|
4485
|
+
}
|
|
4486
|
+
const where = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
|
|
4487
|
+
const rows = this.db.query(`SELECT * FROM run_receipts ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
4488
|
+
return rows.map(rowToRunReceipt);
|
|
4489
|
+
}
|
|
4117
4490
|
deferLiveExpiredRun(id, now, opts = {}) {
|
|
4118
4491
|
const updated = now.toISOString();
|
|
4119
4492
|
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
@@ -4272,6 +4645,9 @@ class Store {
|
|
|
4272
4645
|
const runs = includeRuns ? this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC").all().map(rowToRun) : [];
|
|
4273
4646
|
return { schemaVersion: SCHEMA_USER_VERSION, workflows, loops, runs, checks: this.migrationChecks() };
|
|
4274
4647
|
}
|
|
4648
|
+
exportMigrationRunPage(opts) {
|
|
4649
|
+
return this.db.query("SELECT * FROM loop_runs ORDER BY created_at ASC, id ASC LIMIT ? OFFSET ?").all(opts.limit, opts.offset).map(rowToRun);
|
|
4650
|
+
}
|
|
4275
4651
|
countTable(table) {
|
|
4276
4652
|
const row = this.db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
4277
4653
|
return row?.count ?? 0;
|
|
@@ -4596,7 +4972,7 @@ class Store {
|
|
|
4596
4972
|
// package.json
|
|
4597
4973
|
var package_default = {
|
|
4598
4974
|
name: "@hasna/loops",
|
|
4599
|
-
version: "0.4.
|
|
4975
|
+
version: "0.4.22",
|
|
4600
4976
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4601
4977
|
type: "module",
|
|
4602
4978
|
main: "dist/index.js",
|
|
@@ -4706,7 +5082,6 @@ var package_default = {
|
|
|
4706
5082
|
bun: ">=1.0.0"
|
|
4707
5083
|
},
|
|
4708
5084
|
dependencies: {
|
|
4709
|
-
"@hasna/contracts": "^0.4.1",
|
|
4710
5085
|
"@hasna/events": "^0.1.9",
|
|
4711
5086
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
4712
5087
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
@@ -4716,7 +5091,8 @@ var package_default = {
|
|
|
4716
5091
|
zod: "4.4.3"
|
|
4717
5092
|
},
|
|
4718
5093
|
optionalDependencies: {
|
|
4719
|
-
"@hasna/machines": "0.0.49"
|
|
5094
|
+
"@hasna/machines": "0.0.49",
|
|
5095
|
+
"@hasna/contracts": "^0.4.2"
|
|
4720
5096
|
},
|
|
4721
5097
|
devDependencies: {
|
|
4722
5098
|
"@types/bun": "latest",
|
|
@@ -4931,12 +5307,108 @@ function deploymentStatusLine(status) {
|
|
|
4931
5307
|
// src/mcp/index.ts
|
|
4932
5308
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4933
5309
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5310
|
+
|
|
5311
|
+
// src/mcp/http.ts
|
|
5312
|
+
import { createServer } from "http";
|
|
5313
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5314
|
+
var MCP_HTTP_SERVICE_NAME = "loops";
|
|
5315
|
+
var DEFAULT_MCP_HTTP_PORT = 8890;
|
|
5316
|
+
function isStdioMode(argv = process.argv, env = process.env) {
|
|
5317
|
+
return argv.includes("--stdio") || env.MCP_STDIO === "1";
|
|
5318
|
+
}
|
|
5319
|
+
function resolveMcpHttpPort(argv = process.argv, env = process.env) {
|
|
5320
|
+
const portIdx = argv.indexOf("--port");
|
|
5321
|
+
if (portIdx !== -1 && argv[portIdx + 1]) {
|
|
5322
|
+
return parsePort(argv[portIdx + 1], "--port");
|
|
5323
|
+
}
|
|
5324
|
+
if (env.MCP_HTTP_PORT) {
|
|
5325
|
+
return parsePort(env.MCP_HTTP_PORT, "MCP_HTTP_PORT");
|
|
5326
|
+
}
|
|
5327
|
+
return DEFAULT_MCP_HTTP_PORT;
|
|
5328
|
+
}
|
|
5329
|
+
function parsePort(raw, source) {
|
|
5330
|
+
const parsed = Number(raw);
|
|
5331
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
5332
|
+
throw new Error(`Invalid ${source} value "${raw}". Expected 0-65535.`);
|
|
5333
|
+
}
|
|
5334
|
+
return parsed;
|
|
5335
|
+
}
|
|
5336
|
+
async function readJsonBody(req) {
|
|
5337
|
+
const chunks = [];
|
|
5338
|
+
for await (const chunk of req) {
|
|
5339
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
5340
|
+
}
|
|
5341
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
5342
|
+
if (!text)
|
|
5343
|
+
return;
|
|
5344
|
+
return JSON.parse(text);
|
|
5345
|
+
}
|
|
5346
|
+
async function startMcpHttpServer(buildServer, options) {
|
|
5347
|
+
const host = options?.host ?? "127.0.0.1";
|
|
5348
|
+
const requestedPort = options?.port ?? resolveMcpHttpPort();
|
|
5349
|
+
const serviceName = options?.serviceName ?? MCP_HTTP_SERVICE_NAME;
|
|
5350
|
+
const httpServer = createServer(async (req, res) => {
|
|
5351
|
+
try {
|
|
5352
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
5353
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
5354
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5355
|
+
res.end(JSON.stringify({ status: "ok", name: serviceName }));
|
|
5356
|
+
return;
|
|
5357
|
+
}
|
|
5358
|
+
if (url.pathname !== "/mcp") {
|
|
5359
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
5360
|
+
res.end("Not Found");
|
|
5361
|
+
return;
|
|
5362
|
+
}
|
|
5363
|
+
const server = buildServer();
|
|
5364
|
+
const transport = new StreamableHTTPServerTransport({
|
|
5365
|
+
sessionIdGenerator: undefined
|
|
5366
|
+
});
|
|
5367
|
+
await server.connect(transport);
|
|
5368
|
+
let parsedBody;
|
|
5369
|
+
if (req.method === "POST") {
|
|
5370
|
+
parsedBody = await readJsonBody(req);
|
|
5371
|
+
}
|
|
5372
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
5373
|
+
res.on("close", () => {
|
|
5374
|
+
transport.close();
|
|
5375
|
+
server.close();
|
|
5376
|
+
});
|
|
5377
|
+
} catch (error) {
|
|
5378
|
+
console.error(`[${serviceName}-mcp] HTTP error:`, error);
|
|
5379
|
+
if (!res.headersSent) {
|
|
5380
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5381
|
+
res.end(JSON.stringify({
|
|
5382
|
+
jsonrpc: "2.0",
|
|
5383
|
+
error: { code: -32603, message: "Internal server error" },
|
|
5384
|
+
id: null
|
|
5385
|
+
}));
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
});
|
|
5389
|
+
await new Promise((resolve2, reject) => {
|
|
5390
|
+
httpServer.once("error", reject);
|
|
5391
|
+
httpServer.listen(requestedPort, host, () => resolve2());
|
|
5392
|
+
});
|
|
5393
|
+
const addr = httpServer.address();
|
|
5394
|
+
const port = typeof addr === "object" && addr ? addr.port : requestedPort;
|
|
5395
|
+
console.error(`[${serviceName}-mcp] Streamable HTTP listening on http://${host}:${port}/mcp`);
|
|
5396
|
+
return {
|
|
5397
|
+
port,
|
|
5398
|
+
host,
|
|
5399
|
+
close: () => new Promise((resolve2, reject) => {
|
|
5400
|
+
httpServer.close((err) => err ? reject(err) : resolve2());
|
|
5401
|
+
})
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
// src/mcp/index.ts
|
|
4934
5406
|
import { z as z2 } from "zod/v3";
|
|
4935
5407
|
|
|
4936
5408
|
// src/daemon/control.ts
|
|
4937
5409
|
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
4938
5410
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4939
|
-
import { hostname } from "os";
|
|
5411
|
+
import { hostname as hostname2 } from "os";
|
|
4940
5412
|
import { dirname as dirname3, join as join5 } from "path";
|
|
4941
5413
|
|
|
4942
5414
|
// src/daemon/loop.ts
|
|
@@ -5089,7 +5561,7 @@ function daemonStatus(store, path = pidFilePath()) {
|
|
|
5089
5561
|
return {
|
|
5090
5562
|
...isDaemonRunning(path),
|
|
5091
5563
|
lease: store.getDaemonLease(),
|
|
5092
|
-
host:
|
|
5564
|
+
host: hostname2(),
|
|
5093
5565
|
loops: {
|
|
5094
5566
|
total: store.countLoops(),
|
|
5095
5567
|
active: store.countLoops("active"),
|
|
@@ -5446,6 +5918,7 @@ var TRANSPORT_ENV_KEYS = new Set([
|
|
|
5446
5918
|
"LOGNAME",
|
|
5447
5919
|
"PATH",
|
|
5448
5920
|
"SHELL",
|
|
5921
|
+
"SHLVL",
|
|
5449
5922
|
"SSH_AGENT_PID",
|
|
5450
5923
|
"SSH_AUTH_SOCK",
|
|
5451
5924
|
"TERM",
|
|
@@ -5667,6 +6140,7 @@ function composeExecutionEnv(spec, metadata, opts, accountEnv) {
|
|
|
5667
6140
|
Object.assign(env, spec.env ?? {});
|
|
5668
6141
|
Object.assign(env, allowlistEnv(spec.allowlist));
|
|
5669
6142
|
env.PATH = normalizeExecutionPath(env);
|
|
6143
|
+
env.SHLVL ||= "1";
|
|
5670
6144
|
Object.assign(env, metadataEnv(metadata));
|
|
5671
6145
|
return env;
|
|
5672
6146
|
}
|
|
@@ -5738,7 +6212,7 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
5738
6212
|
return [
|
|
5739
6213
|
"__openloops_prepare_worktree() {",
|
|
5740
6214
|
` local repo=${shellQuote(repoRoot)} path=${shellQuote(path)} branch=${shellQuote(branch)}`,
|
|
5741
|
-
" local top expected_common actual_common current",
|
|
6215
|
+
" local top expected_common actual_common current status recovered",
|
|
5742
6216
|
' if [ -L "$path" ]; then echo "refusing symlinked worktree path $path" >&2; return 1; fi',
|
|
5743
6217
|
' if [ -e "$path" ]; then',
|
|
5744
6218
|
' top="$(git -C "$path" rev-parse --show-toplevel 2>/dev/null)" || { echo "refusing to reuse non-worktree path: $path" >&2; return 1; }',
|
|
@@ -5747,7 +6221,19 @@ function remoteWorktreePrepareLines(worktree) {
|
|
|
5747
6221
|
' actual_common="$(cd "$path" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd -P)"',
|
|
5748
6222
|
' if [ -z "$expected_common" ] || [ "$expected_common" != "$actual_common" ]; then echo "existing worktree $path belongs to a different git common dir" >&2; return 1; fi',
|
|
5749
6223
|
' current="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
5750
|
-
' if [ "$current" != "$branch" ]; then
|
|
6224
|
+
' if [ "$current" != "$branch" ]; then',
|
|
6225
|
+
' status="$(git -C "$path" status --porcelain=v1 --untracked-files=all 2>/dev/null)" || { echo "existing worktree $path is on branch ${current:-unknown}, expected $branch, and cleanliness could not be checked; inspect or remove the stale worktree" >&2; return 1; }',
|
|
6226
|
+
' if [ -n "$status" ]; then echo "existing worktree $path is on branch ${current:-unknown}, expected $branch, and has local changes; commit/stash them or remove the stale worktree before retrying" >&2; return 1; fi',
|
|
6227
|
+
' if git -C "$repo" show-ref --verify --quiet "refs/heads/$branch"; then',
|
|
6228
|
+
' git -C "$path" checkout "$branch" 1>&2 || { echo "existing worktree $path is clean but could not switch from branch ${current:-unknown} to expected $branch; remove/prune the stale worktree or free the branch before retrying" >&2; return 1; }',
|
|
6229
|
+
' elif [ -z "$current" ]; then',
|
|
6230
|
+
' git -C "$path" checkout -b "$branch" 1>&2 || { echo "existing worktree $path is clean but could not recreate expected branch $branch at the current detached HEAD; remove/prune the stale worktree before retrying" >&2; return 1; }',
|
|
6231
|
+
" else",
|
|
6232
|
+
' echo "existing worktree $path is on branch $current, expected $branch, but expected branch does not exist; remove/prune the stale worktree or recreate the expected branch before retrying" >&2; return 1',
|
|
6233
|
+
" fi",
|
|
6234
|
+
' recovered="$(git -C "$path" branch --show-current 2>/dev/null)"',
|
|
6235
|
+
' if [ "$recovered" != "$branch" ]; then echo "existing worktree $path branch recovery ended on ${recovered:-unknown}, expected $branch; remove/prune the stale worktree before retrying" >&2; return 1; fi',
|
|
6236
|
+
" fi",
|
|
5751
6237
|
" return 0",
|
|
5752
6238
|
" fi",
|
|
5753
6239
|
' git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "worktree repoRoot is not a git repository: $repo" >&2; return 1; }',
|
|
@@ -5828,8 +6314,21 @@ function transportEnv(opts) {
|
|
|
5828
6314
|
env[key] = value;
|
|
5829
6315
|
}
|
|
5830
6316
|
env.PATH = normalizeExecutionPath(env);
|
|
6317
|
+
env.SHLVL ||= "1";
|
|
5831
6318
|
return env;
|
|
5832
6319
|
}
|
|
6320
|
+
function remotePreflightFailureMessage(machineId, detail) {
|
|
6321
|
+
const normalized = detail.trim();
|
|
6322
|
+
if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED|Offending .*key in .*known_hosts/i.test(normalized)) {
|
|
6323
|
+
return [
|
|
6324
|
+
`remote preflight failed on ${machineId}: SSH host key verification failed.`,
|
|
6325
|
+
`Verify ${machineId}'s host identity and repair SSH known_hosts/trust material outside OpenLoops;`,
|
|
6326
|
+
"OpenLoops will not disable host-key checking or modify known_hosts automatically.",
|
|
6327
|
+
normalized ? `Transport detail: ${normalized}` : ""
|
|
6328
|
+
].filter(Boolean).join(" ");
|
|
6329
|
+
}
|
|
6330
|
+
return `remote preflight failed on ${machineId}${normalized ? `: ${normalized}` : ""}`;
|
|
6331
|
+
}
|
|
5833
6332
|
function assertCodewithProfileListed(profile, result) {
|
|
5834
6333
|
const profiles = codewithProfileSetFromResult(result);
|
|
5835
6334
|
if (!profiles.has(profile)) {
|
|
@@ -5881,6 +6380,9 @@ async function preflightNativeAuthProfile(spec, env) {
|
|
|
5881
6380
|
const tableResult = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
5882
6381
|
assertCodewithProfileListed(spec.nativeAuthProfile.profile, tableResult);
|
|
5883
6382
|
}
|
|
6383
|
+
function spawnDetail(result) {
|
|
6384
|
+
return (result.stderr || result.stdout || result.error || "").toString().trim();
|
|
6385
|
+
}
|
|
5884
6386
|
function resolvedDirEquals(left, right) {
|
|
5885
6387
|
try {
|
|
5886
6388
|
return realpathSync(left) === realpathSync(right);
|
|
@@ -5930,8 +6432,45 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
5930
6432
|
}
|
|
5931
6433
|
const current = await git(["-C", path, "branch", "--show-current"]);
|
|
5932
6434
|
const actualBranch = current.stdout.trim();
|
|
5933
|
-
if (current.error || (current.status ?? 1) !== 0
|
|
5934
|
-
return { error: `existing worktree ${path}
|
|
6435
|
+
if (current.error || (current.status ?? 1) !== 0) {
|
|
6436
|
+
return { error: `existing worktree ${path} branch could not be inspected${spawnDetail(current) ? `: ${spawnDetail(current)}` : ""}` };
|
|
6437
|
+
}
|
|
6438
|
+
if (actualBranch !== branch) {
|
|
6439
|
+
const actualLabel = actualBranch || "unknown";
|
|
6440
|
+
const status = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
|
|
6441
|
+
if (status.error || (status.status ?? 1) !== 0) {
|
|
6442
|
+
const detail = spawnDetail(status);
|
|
6443
|
+
return {
|
|
6444
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and cleanliness could not be checked; inspect or remove the stale worktree${detail ? `: ${detail}` : ""}`
|
|
6445
|
+
};
|
|
6446
|
+
}
|
|
6447
|
+
if (status.stdout.trim()) {
|
|
6448
|
+
return {
|
|
6449
|
+
error: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, and has local changes; commit/stash them or remove the stale worktree before retrying`
|
|
6450
|
+
};
|
|
6451
|
+
}
|
|
6452
|
+
const hasBranch2 = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
6453
|
+
const checkout = hasBranch2.status === 0 ? await git(["-C", path, "checkout", branch]) : actualBranch ? {
|
|
6454
|
+
status: 1,
|
|
6455
|
+
stdout: "",
|
|
6456
|
+
stderr: `existing worktree ${path} is on branch ${actualLabel}, expected ${branch}, but expected branch does not exist; remove/prune the stale worktree or recreate the expected branch before retrying`,
|
|
6457
|
+
signal: null,
|
|
6458
|
+
timedOut: false
|
|
6459
|
+
} : await git(["-C", path, "checkout", "-b", branch]);
|
|
6460
|
+
if (checkout.error || (checkout.status ?? 1) !== 0) {
|
|
6461
|
+
const detail = spawnDetail(checkout);
|
|
6462
|
+
return {
|
|
6463
|
+
error: `existing worktree ${path} is clean but could not recover branch ${branch} from ${actualLabel}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
6464
|
+
};
|
|
6465
|
+
}
|
|
6466
|
+
const recovered = await git(["-C", path, "branch", "--show-current"]);
|
|
6467
|
+
if (recovered.error || (recovered.status ?? 1) !== 0 || recovered.stdout.trim() !== branch) {
|
|
6468
|
+
const recoveredBranch = recovered.stdout.trim() || "unknown";
|
|
6469
|
+
const detail = spawnDetail(recovered);
|
|
6470
|
+
return {
|
|
6471
|
+
error: `existing worktree ${path} branch recovery ended on ${recoveredBranch}, expected ${branch}; remove/prune the stale worktree before retrying${detail ? `: ${detail}` : ""}`
|
|
6472
|
+
};
|
|
6473
|
+
}
|
|
5935
6474
|
}
|
|
5936
6475
|
return { cwd: worktree.cwd };
|
|
5937
6476
|
}
|
|
@@ -5947,7 +6486,7 @@ async function ensureLocalWorktree(worktree, env) {
|
|
|
5947
6486
|
const hasBranch = await git(["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
5948
6487
|
const add = hasBranch.status === 0 ? await git(["-C", repoRoot, "worktree", "add", path, branch]) : await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
5949
6488
|
if (add.error || (add.status ?? 1) !== 0) {
|
|
5950
|
-
const detail = (add
|
|
6489
|
+
const detail = spawnDetail(add);
|
|
5951
6490
|
return { error: `git worktree add failed for ${path}${detail ? `: ${detail}` : ""}` };
|
|
5952
6491
|
}
|
|
5953
6492
|
return { cwd: worktree.cwd };
|
|
@@ -5993,7 +6532,7 @@ function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
|
5993
6532
|
throw new Error(`remote preflight failed on ${machine.id}: ${result.error.message}`);
|
|
5994
6533
|
if ((result.status ?? 1) !== 0) {
|
|
5995
6534
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5996
|
-
throw new Error(
|
|
6535
|
+
throw new Error(remotePreflightFailureMessage(machine.id, detail));
|
|
5997
6536
|
}
|
|
5998
6537
|
}
|
|
5999
6538
|
async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
|
|
@@ -6253,7 +6792,7 @@ async function executeLoop(loop, run, opts = {}) {
|
|
|
6253
6792
|
}
|
|
6254
6793
|
|
|
6255
6794
|
// src/lib/health.ts
|
|
6256
|
-
import { createHash as
|
|
6795
|
+
import { createHash as createHash3 } from "crypto";
|
|
6257
6796
|
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
6258
6797
|
import { join as join7 } from "path";
|
|
6259
6798
|
var EVIDENCE_CHARS = 2000;
|
|
@@ -6315,7 +6854,7 @@ function tagsFromValue(value) {
|
|
|
6315
6854
|
return [];
|
|
6316
6855
|
}
|
|
6317
6856
|
function stableFingerprint(parts) {
|
|
6318
|
-
return
|
|
6857
|
+
return createHash3("sha256").update(parts.join(`
|
|
6319
6858
|
`)).digest("hex").slice(0, 16);
|
|
6320
6859
|
}
|
|
6321
6860
|
function stableScanFingerprint(parts) {
|
|
@@ -7294,55 +7833,6 @@ ${evidence.join(`
|
|
|
7294
7833
|
import { generateObject } from "ai";
|
|
7295
7834
|
import { z } from "zod";
|
|
7296
7835
|
|
|
7297
|
-
// src/lib/run-envelope.ts
|
|
7298
|
-
var ENVELOPE_EXCERPT_CHARS = 2048;
|
|
7299
|
-
var BLOCKED_STEP_ERROR_PREFIX = "blocked:";
|
|
7300
|
-
function boundedExcerpt(text, limit = ENVELOPE_EXCERPT_CHARS) {
|
|
7301
|
-
if (!text)
|
|
7302
|
-
return;
|
|
7303
|
-
const scrubbed = scrubSecrets(text);
|
|
7304
|
-
if (scrubbed.length <= limit * 2)
|
|
7305
|
-
return scrubbed;
|
|
7306
|
-
const omitted = scrubbed.length - limit * 2;
|
|
7307
|
-
return `${scrubbed.slice(0, limit)}
|
|
7308
|
-
[... ${omitted} chars omitted ...]
|
|
7309
|
-
${scrubbed.slice(-limit)}`;
|
|
7310
|
-
}
|
|
7311
|
-
function summarizeOutput(stdout, stderr) {
|
|
7312
|
-
return {
|
|
7313
|
-
stdoutBytes: stdout ? Buffer.byteLength(stdout, "utf8") : 0,
|
|
7314
|
-
stderrBytes: stderr ? Buffer.byteLength(stderr, "utf8") : 0,
|
|
7315
|
-
stdoutExcerpt: boundedExcerpt(stdout),
|
|
7316
|
-
stderrExcerpt: boundedExcerpt(stderr)
|
|
7317
|
-
};
|
|
7318
|
-
}
|
|
7319
|
-
function summarizeExecutorResult(result) {
|
|
7320
|
-
return {
|
|
7321
|
-
...summarizeOutput(result.stdout, result.stderr),
|
|
7322
|
-
status: result.status,
|
|
7323
|
-
exitCode: result.exitCode,
|
|
7324
|
-
durationMs: result.durationMs,
|
|
7325
|
-
error: boundedExcerpt(result.error)
|
|
7326
|
-
};
|
|
7327
|
-
}
|
|
7328
|
-
function isBlockedStepRun(step) {
|
|
7329
|
-
return step?.status === "skipped" && (step.error?.startsWith(BLOCKED_STEP_ERROR_PREFIX) ?? false);
|
|
7330
|
-
}
|
|
7331
|
-
function summarizeWorkflowStepRun(step) {
|
|
7332
|
-
return {
|
|
7333
|
-
...summarizeOutput(step.stdout, step.stderr),
|
|
7334
|
-
stepId: step.stepId,
|
|
7335
|
-
status: step.status,
|
|
7336
|
-
exitCode: step.exitCode,
|
|
7337
|
-
durationMs: step.durationMs,
|
|
7338
|
-
error: boundedExcerpt(step.error),
|
|
7339
|
-
blocked: isBlockedStepRun(step)
|
|
7340
|
-
};
|
|
7341
|
-
}
|
|
7342
|
-
function workflowRunEnvelope(run, steps) {
|
|
7343
|
-
return JSON.stringify({ workflowRun: run, steps: steps.map(summarizeWorkflowStepRun) }, null, 2);
|
|
7344
|
-
}
|
|
7345
|
-
|
|
7346
7836
|
// src/lib/goal/model-factory.ts
|
|
7347
7837
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
7348
7838
|
var DEFAULT_GOAL_MODEL = "openai/gpt-4o-mini";
|
|
@@ -8582,30 +9072,942 @@ async function tick(deps) {
|
|
|
8582
9072
|
return { claimed, completed, skipped, recovered, expired };
|
|
8583
9073
|
}
|
|
8584
9074
|
|
|
8585
|
-
// src/
|
|
8586
|
-
var
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
9075
|
+
// src/lib/cloud/mode.ts
|
|
9076
|
+
var DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
|
|
9077
|
+
function normalizeStorageMode(value) {
|
|
9078
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
9079
|
+
if (normalized === "local")
|
|
9080
|
+
return { mode: "local", deprecatedAlias: null };
|
|
9081
|
+
if (normalized === "cloud")
|
|
9082
|
+
return { mode: "cloud", deprecatedAlias: null };
|
|
9083
|
+
if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
|
|
9084
|
+
return { mode: "cloud", deprecatedAlias: normalized };
|
|
9085
|
+
}
|
|
9086
|
+
throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
|
|
9087
|
+
}
|
|
9088
|
+
function envToken(name) {
|
|
9089
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
9090
|
+
}
|
|
9091
|
+
|
|
9092
|
+
// src/lib/cloud/transport.ts
|
|
9093
|
+
function defaultCloudBaseUrl(name) {
|
|
9094
|
+
return `https://${name}.hasna.xyz`;
|
|
9095
|
+
}
|
|
9096
|
+
function clientTransportEnvKeys(name) {
|
|
9097
|
+
const token = envToken(name);
|
|
9098
|
+
return {
|
|
9099
|
+
modeKeys: [
|
|
9100
|
+
`HASNA_${token}_STORAGE_MODE`,
|
|
9101
|
+
`HASNA_${token}_MODE`,
|
|
9102
|
+
`${token}_STORAGE_MODE`,
|
|
9103
|
+
`${token}_MODE`
|
|
9104
|
+
],
|
|
9105
|
+
apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
|
|
9106
|
+
apiKeyKeys: [
|
|
9107
|
+
`HASNA_${token}_API_KEY`,
|
|
9108
|
+
`${token}_API_KEY`,
|
|
9109
|
+
`HASNA_${token}_API_TOKEN`,
|
|
9110
|
+
`${token}_API_TOKEN`
|
|
9111
|
+
]
|
|
9112
|
+
};
|
|
9113
|
+
}
|
|
9114
|
+
function firstEnv(env, keys) {
|
|
9115
|
+
for (const key of keys) {
|
|
9116
|
+
const value = env[key]?.trim();
|
|
9117
|
+
if (value)
|
|
9118
|
+
return { key, value };
|
|
9119
|
+
}
|
|
9120
|
+
return null;
|
|
9121
|
+
}
|
|
9122
|
+
function toV1BaseUrl(apiUrl) {
|
|
9123
|
+
const url = new URL(apiUrl);
|
|
9124
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
9125
|
+
throw new Error("API URL must use http or https.");
|
|
9126
|
+
}
|
|
9127
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
9128
|
+
if (path.endsWith("/v1"))
|
|
9129
|
+
path = path.slice(0, -"/v1".length);
|
|
9130
|
+
url.pathname = `${path}/v1`;
|
|
9131
|
+
url.search = "";
|
|
9132
|
+
url.hash = "";
|
|
9133
|
+
return url.toString().replace(/\/+$/, "");
|
|
9134
|
+
}
|
|
9135
|
+
function resolveClientTransport(name, env = process.env) {
|
|
9136
|
+
const keys = clientTransportEnvKeys(name);
|
|
9137
|
+
const modeHit = firstEnv(env, keys.modeKeys);
|
|
9138
|
+
const urlHit = firstEnv(env, keys.apiUrlKeys);
|
|
9139
|
+
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
9140
|
+
let mode = "local";
|
|
9141
|
+
let deprecatedAlias = null;
|
|
9142
|
+
let modeSource = "default";
|
|
9143
|
+
const warnings = [];
|
|
9144
|
+
if (modeHit) {
|
|
9145
|
+
const normalized = normalizeStorageMode(modeHit.value);
|
|
9146
|
+
mode = normalized.mode;
|
|
9147
|
+
deprecatedAlias = normalized.deprecatedAlias;
|
|
9148
|
+
modeSource = modeHit.key;
|
|
9149
|
+
if (deprecatedAlias) {
|
|
9150
|
+
warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
if (mode === "local") {
|
|
9154
|
+
return {
|
|
9155
|
+
transport: "local",
|
|
9156
|
+
mode,
|
|
9157
|
+
deprecatedAlias,
|
|
9158
|
+
modeSource,
|
|
9159
|
+
baseUrl: null,
|
|
9160
|
+
apiUrlSource: null,
|
|
9161
|
+
apiKeyPresent: Boolean(keyHit),
|
|
9162
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
9163
|
+
misconfigured: false,
|
|
9164
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
9165
|
+
};
|
|
9166
|
+
}
|
|
9167
|
+
if (!keyHit) {
|
|
9168
|
+
warnings.push(`${modeSource}=cloud but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to cloud; using local store. Set ${keys.apiKeyKeys[0]} to enable the cloud client.`);
|
|
9169
|
+
return {
|
|
9170
|
+
transport: "local",
|
|
9171
|
+
mode,
|
|
9172
|
+
deprecatedAlias,
|
|
9173
|
+
modeSource,
|
|
9174
|
+
baseUrl: null,
|
|
9175
|
+
apiUrlSource: null,
|
|
9176
|
+
apiKeyPresent: false,
|
|
9177
|
+
apiKeySource: null,
|
|
9178
|
+
misconfigured: true,
|
|
9179
|
+
warning: warnings.join(" ")
|
|
9180
|
+
};
|
|
9181
|
+
}
|
|
9182
|
+
const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
|
|
9183
|
+
const apiUrlSource = urlHit ? urlHit.key : "default";
|
|
9184
|
+
let baseUrl;
|
|
9185
|
+
try {
|
|
9186
|
+
baseUrl = toV1BaseUrl(rawUrl);
|
|
9187
|
+
} catch (error) {
|
|
9188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9189
|
+
warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
|
|
9190
|
+
return {
|
|
9191
|
+
transport: "local",
|
|
9192
|
+
mode,
|
|
9193
|
+
deprecatedAlias,
|
|
9194
|
+
modeSource,
|
|
9195
|
+
baseUrl: null,
|
|
9196
|
+
apiUrlSource: null,
|
|
9197
|
+
apiKeyPresent: true,
|
|
9198
|
+
apiKeySource: keyHit.key,
|
|
9199
|
+
misconfigured: true,
|
|
9200
|
+
warning: warnings.join(" ")
|
|
9201
|
+
};
|
|
9202
|
+
}
|
|
9203
|
+
return {
|
|
9204
|
+
transport: "cloud-http",
|
|
9205
|
+
mode,
|
|
9206
|
+
deprecatedAlias,
|
|
9207
|
+
modeSource,
|
|
9208
|
+
baseUrl,
|
|
9209
|
+
apiUrlSource,
|
|
9210
|
+
apiKeyPresent: true,
|
|
9211
|
+
apiKeySource: keyHit.key,
|
|
9212
|
+
misconfigured: false,
|
|
9213
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
9214
|
+
};
|
|
9215
|
+
}
|
|
9216
|
+
|
|
9217
|
+
class HasnaHttpError extends Error {
|
|
9218
|
+
status;
|
|
9219
|
+
method;
|
|
9220
|
+
path;
|
|
9221
|
+
body;
|
|
9222
|
+
constructor(method, path, status, body) {
|
|
9223
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
|
|
9224
|
+
this.name = "HasnaHttpError";
|
|
9225
|
+
this.status = status;
|
|
9226
|
+
this.method = method;
|
|
9227
|
+
this.path = path;
|
|
9228
|
+
this.body = body;
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
9232
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
9233
|
+
function appendQuery(path, query) {
|
|
9234
|
+
if (!query)
|
|
9235
|
+
return path;
|
|
9236
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
9237
|
+
if (!(query instanceof URLSearchParams)) {
|
|
9238
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9239
|
+
if (value === null || value === undefined)
|
|
9240
|
+
continue;
|
|
9241
|
+
if (Array.isArray(value)) {
|
|
9242
|
+
for (const v of value)
|
|
9243
|
+
params.append(key, String(v));
|
|
9244
|
+
} else {
|
|
9245
|
+
params.append(key, String(value));
|
|
9246
|
+
}
|
|
9247
|
+
}
|
|
9248
|
+
}
|
|
9249
|
+
const qs = params.toString();
|
|
9250
|
+
if (!qs)
|
|
9251
|
+
return path;
|
|
9252
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
9253
|
+
}
|
|
9254
|
+
var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
9255
|
+
function createHasnaHttpTransport(options) {
|
|
9256
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
9257
|
+
const base = options.baseUrl.replace(/\/+$/, "");
|
|
9258
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
9259
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
9260
|
+
const defaultRetry = options.retry;
|
|
9261
|
+
function resolveRetry(callRetry) {
|
|
9262
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
9263
|
+
if (chosen === false)
|
|
9264
|
+
return null;
|
|
9265
|
+
const r = chosen ?? {};
|
|
9266
|
+
return {
|
|
9267
|
+
retries: r.retries ?? 2,
|
|
9268
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
9269
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
9270
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
9271
|
+
};
|
|
9272
|
+
}
|
|
9273
|
+
async function once2(method, rel, url, body, opts) {
|
|
9274
|
+
const headers = {
|
|
9275
|
+
"x-api-key": options.apiKey,
|
|
9276
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
9277
|
+
Accept: "application/json",
|
|
9278
|
+
...options.headers ?? {},
|
|
9279
|
+
...opts.headers ?? {}
|
|
9280
|
+
};
|
|
9281
|
+
if (opts.idempotencyKey)
|
|
9282
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
9283
|
+
const init = { method, headers };
|
|
9284
|
+
if (body !== undefined) {
|
|
9285
|
+
headers["Content-Type"] = "application/json";
|
|
9286
|
+
init.body = JSON.stringify(body);
|
|
9287
|
+
}
|
|
9288
|
+
const controller = new AbortController;
|
|
9289
|
+
const onAbort = () => controller.abort();
|
|
9290
|
+
if (opts.signal) {
|
|
9291
|
+
if (opts.signal.aborted)
|
|
9292
|
+
controller.abort();
|
|
9293
|
+
else
|
|
9294
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
9295
|
+
}
|
|
9296
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
9297
|
+
init.signal = controller.signal;
|
|
9298
|
+
let response;
|
|
9299
|
+
try {
|
|
9300
|
+
response = await fetchImpl(url, init);
|
|
9301
|
+
} catch (error) {
|
|
9302
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
9303
|
+
if (opts.signal?.aborted)
|
|
9304
|
+
return { ok: false, retryable: false, error: err };
|
|
9305
|
+
return { ok: false, retryable: true, error: err };
|
|
9306
|
+
} finally {
|
|
9307
|
+
clearTimeout(timer);
|
|
9308
|
+
if (opts.signal)
|
|
9309
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
9310
|
+
}
|
|
9311
|
+
const text = await response.text();
|
|
9312
|
+
let parsed = undefined;
|
|
9313
|
+
if (text.length > 0) {
|
|
9314
|
+
try {
|
|
9315
|
+
parsed = JSON.parse(text);
|
|
9316
|
+
} catch {
|
|
9317
|
+
parsed = text;
|
|
9318
|
+
}
|
|
9319
|
+
}
|
|
9320
|
+
if (!response.ok) {
|
|
9321
|
+
const retry = resolveRetry(opts.retry);
|
|
9322
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
9323
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
9324
|
+
}
|
|
9325
|
+
return { ok: true, value: parsed };
|
|
9326
|
+
}
|
|
9327
|
+
async function request(method, path, body, opts = {}) {
|
|
9328
|
+
const upper = method.toUpperCase();
|
|
9329
|
+
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
9330
|
+
const url = `${base}${rel}`;
|
|
9331
|
+
const retry = resolveRetry(opts.retry);
|
|
9332
|
+
const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
|
|
9333
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
9334
|
+
let last = null;
|
|
9335
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
9336
|
+
const result = await once2(upper, rel, url, body, opts);
|
|
9337
|
+
if (result.ok)
|
|
9338
|
+
return result.value;
|
|
9339
|
+
last = result;
|
|
9340
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
9341
|
+
if (!canRetry)
|
|
9342
|
+
break;
|
|
9343
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
9344
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
9345
|
+
await sleep(backoff + jitter);
|
|
9346
|
+
}
|
|
9347
|
+
throw last.error;
|
|
9348
|
+
}
|
|
9349
|
+
return {
|
|
9350
|
+
baseUrl: base,
|
|
9351
|
+
request,
|
|
9352
|
+
get: (path, opts) => request("GET", path, undefined, opts),
|
|
9353
|
+
post: (path, body, opts) => request("POST", path, body, opts),
|
|
9354
|
+
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
9355
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
9356
|
+
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
9357
|
+
};
|
|
9358
|
+
}
|
|
9359
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
9360
|
+
const resolution = resolveClientTransport(name, env);
|
|
9361
|
+
if (resolution.misconfigured) {
|
|
9362
|
+
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
|
|
9363
|
+
}
|
|
9364
|
+
if (resolution.transport === "local" || !resolution.baseUrl) {
|
|
9365
|
+
return { transport: "local", client: null, resolution };
|
|
9366
|
+
}
|
|
9367
|
+
const keys = clientTransportEnvKeys(name);
|
|
9368
|
+
const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
|
|
9369
|
+
if (!apiKey) {
|
|
9370
|
+
throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
|
|
9371
|
+
}
|
|
9372
|
+
return {
|
|
9373
|
+
transport: "cloud-http",
|
|
9374
|
+
client: createHasnaHttpTransport({
|
|
9375
|
+
name,
|
|
9376
|
+
baseUrl: resolution.baseUrl,
|
|
9377
|
+
apiKey,
|
|
9378
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
9379
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
9380
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
9381
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
9382
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
9383
|
+
}),
|
|
9384
|
+
resolution
|
|
9385
|
+
};
|
|
9386
|
+
}
|
|
9387
|
+
|
|
9388
|
+
// src/lib/cloud/storage.ts
|
|
9389
|
+
function resourcePath(resource) {
|
|
9390
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
9391
|
+
if (!trimmed)
|
|
9392
|
+
throw new Error("resource must be a non-empty path segment");
|
|
9393
|
+
return `/${trimmed}`;
|
|
9394
|
+
}
|
|
9395
|
+
function entityPath(resource, id) {
|
|
9396
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
9397
|
+
throw new Error("id must be a non-empty string");
|
|
9398
|
+
}
|
|
9399
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
9400
|
+
}
|
|
9401
|
+
function newIdempotencyKey() {
|
|
9402
|
+
const g = globalThis;
|
|
9403
|
+
if (g.crypto?.randomUUID)
|
|
9404
|
+
return g.crypto.randomUUID();
|
|
9405
|
+
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
9406
|
+
}
|
|
9407
|
+
function extractItems(raw) {
|
|
9408
|
+
if (Array.isArray(raw))
|
|
9409
|
+
return raw;
|
|
9410
|
+
if (raw && typeof raw === "object") {
|
|
9411
|
+
const obj = raw;
|
|
9412
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
9413
|
+
if (Array.isArray(obj[key]))
|
|
9414
|
+
return obj[key];
|
|
9415
|
+
}
|
|
9416
|
+
}
|
|
9417
|
+
return [];
|
|
9418
|
+
}
|
|
9419
|
+
function extractTotal(raw) {
|
|
9420
|
+
if (raw && typeof raw === "object") {
|
|
9421
|
+
const obj = raw;
|
|
9422
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
9423
|
+
if (typeof obj[key] === "number")
|
|
9424
|
+
return obj[key];
|
|
9425
|
+
}
|
|
9426
|
+
}
|
|
9427
|
+
return null;
|
|
9428
|
+
}
|
|
9429
|
+
function extractCursor(raw) {
|
|
9430
|
+
if (raw && typeof raw === "object") {
|
|
9431
|
+
const obj = raw;
|
|
9432
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
9433
|
+
if (typeof obj[key] === "string")
|
|
9434
|
+
return obj[key];
|
|
9435
|
+
}
|
|
9436
|
+
}
|
|
9437
|
+
return null;
|
|
9438
|
+
}
|
|
9439
|
+
function createHasnaStorageClient(name, transport) {
|
|
9440
|
+
return {
|
|
9441
|
+
name,
|
|
9442
|
+
baseUrl: transport.baseUrl,
|
|
9443
|
+
transport,
|
|
9444
|
+
async list(resource, options = {}) {
|
|
9445
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
9446
|
+
return {
|
|
9447
|
+
items: extractItems(raw),
|
|
9448
|
+
total: extractTotal(raw),
|
|
9449
|
+
cursor: extractCursor(raw),
|
|
9450
|
+
raw
|
|
9451
|
+
};
|
|
9452
|
+
},
|
|
9453
|
+
async get(resource, id, options = {}) {
|
|
9454
|
+
try {
|
|
9455
|
+
return await transport.get(entityPath(resource, id), options);
|
|
9456
|
+
} catch (error) {
|
|
9457
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
9458
|
+
return null;
|
|
9459
|
+
throw error;
|
|
9460
|
+
}
|
|
9461
|
+
},
|
|
9462
|
+
async create(resource, body, options = {}) {
|
|
9463
|
+
const { idempotencyKey, ...rest } = options;
|
|
9464
|
+
return transport.post(resourcePath(resource), body, {
|
|
9465
|
+
...rest,
|
|
9466
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
9467
|
+
});
|
|
9468
|
+
},
|
|
9469
|
+
async update(resource, id, patch, options = {}) {
|
|
9470
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
9471
|
+
const call = method === "PUT" ? transport.put : transport.patch;
|
|
9472
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
9473
|
+
},
|
|
9474
|
+
async delete(resource, id, options = {}) {
|
|
9475
|
+
try {
|
|
9476
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
9477
|
+
} catch (error) {
|
|
9478
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
9479
|
+
return;
|
|
9480
|
+
throw error;
|
|
9481
|
+
}
|
|
9482
|
+
}
|
|
9483
|
+
};
|
|
9484
|
+
}
|
|
9485
|
+
|
|
9486
|
+
// src/lib/cloud/resolve.ts
|
|
9487
|
+
function firstValue(env, keys) {
|
|
9488
|
+
for (const key of keys) {
|
|
9489
|
+
const value = env[key]?.trim();
|
|
9490
|
+
if (value)
|
|
9491
|
+
return value;
|
|
9492
|
+
}
|
|
9493
|
+
return;
|
|
9494
|
+
}
|
|
9495
|
+
function resolveCloudStorage(name, env = process.env) {
|
|
9496
|
+
const token = envToken(name);
|
|
9497
|
+
const modeKeys = [`HASNA_${token}_STORAGE_MODE`, `HASNA_${token}_MODE`, `${token}_STORAGE_MODE`, `${token}_MODE`];
|
|
9498
|
+
const apiUrlKeys = [`HASNA_${token}_API_URL`, `${token}_API_URL`];
|
|
9499
|
+
const apiKeyKeys = [
|
|
9500
|
+
`HASNA_${token}_API_KEY`,
|
|
9501
|
+
`${token}_API_KEY`,
|
|
9502
|
+
`HASNA_${token}_API_TOKEN`,
|
|
9503
|
+
`${token}_API_TOKEN`
|
|
9504
|
+
];
|
|
9505
|
+
const explicitMode = firstValue(env, modeKeys);
|
|
9506
|
+
if (explicitMode && normalizeStorageMode(explicitMode).mode === "local") {
|
|
9507
|
+
return { transport: "local", client: null };
|
|
9508
|
+
}
|
|
9509
|
+
const apiUrl = firstValue(env, apiUrlKeys);
|
|
9510
|
+
const apiKey = firstValue(env, apiKeyKeys);
|
|
9511
|
+
if (!apiUrl || !apiKey) {
|
|
9512
|
+
return { transport: "local", client: null };
|
|
9513
|
+
}
|
|
9514
|
+
const cloudEnv = { ...env, [`HASNA_${token}_STORAGE_MODE`]: "self_hosted" };
|
|
9515
|
+
const wired = createClientTransport(name, cloudEnv);
|
|
9516
|
+
if (wired.transport !== "cloud-http") {
|
|
9517
|
+
return { transport: "local", client: null };
|
|
9518
|
+
}
|
|
9519
|
+
return {
|
|
9520
|
+
transport: "cloud-http",
|
|
9521
|
+
client: createHasnaStorageClient(name, wired.client),
|
|
9522
|
+
baseUrl: wired.client.baseUrl
|
|
9523
|
+
};
|
|
9524
|
+
}
|
|
9525
|
+
|
|
9526
|
+
// src/lib/store/index.ts
|
|
9527
|
+
class CloudUnsupportedError extends Error {
|
|
9528
|
+
constructor(operation) {
|
|
9529
|
+
super(`operation not supported over the hosted OpenLoops API: ${operation}. ` + `Run it on a machine using local storage, or unset HASNA_LOOPS_API_URL/HASNA_LOOPS_API_KEY.`);
|
|
9530
|
+
this.name = "CloudUnsupportedError";
|
|
9531
|
+
}
|
|
9532
|
+
}
|
|
9533
|
+
|
|
9534
|
+
class LocalStore {
|
|
9535
|
+
transport = "local";
|
|
9536
|
+
store;
|
|
9537
|
+
constructor(store = new Store) {
|
|
9538
|
+
this.store = store;
|
|
9539
|
+
}
|
|
9540
|
+
get raw() {
|
|
9541
|
+
return this.store;
|
|
9542
|
+
}
|
|
9543
|
+
async close() {
|
|
9544
|
+
this.store.close();
|
|
9545
|
+
}
|
|
9546
|
+
async createLoop(input, from) {
|
|
9547
|
+
return from ? this.store.createLoop(input, from) : this.store.createLoop(input);
|
|
9548
|
+
}
|
|
9549
|
+
async getLoop(id) {
|
|
9550
|
+
return this.store.getLoop(id);
|
|
9551
|
+
}
|
|
9552
|
+
async findLoopByName(name) {
|
|
9553
|
+
return this.store.findLoopByName(name);
|
|
9554
|
+
}
|
|
9555
|
+
async requireLoop(idOrName) {
|
|
9556
|
+
return this.store.requireLoop(idOrName);
|
|
9557
|
+
}
|
|
9558
|
+
async requireUniqueLoop(idOrName) {
|
|
9559
|
+
return this.store.requireUniqueLoop(idOrName);
|
|
9560
|
+
}
|
|
9561
|
+
async listLoops(opts = {}) {
|
|
9562
|
+
return this.store.listLoops(opts);
|
|
9563
|
+
}
|
|
9564
|
+
async countLoops(status, opts = {}) {
|
|
9565
|
+
return this.store.countLoops(status, opts);
|
|
9566
|
+
}
|
|
9567
|
+
async updateLoop(id, patch) {
|
|
9568
|
+
return this.store.updateLoop(id, patch);
|
|
9569
|
+
}
|
|
9570
|
+
async renameLoop(id, name) {
|
|
9571
|
+
return this.store.renameLoop(id, name);
|
|
9572
|
+
}
|
|
9573
|
+
async archiveLoop(idOrName) {
|
|
9574
|
+
return this.store.archiveLoop(idOrName);
|
|
9575
|
+
}
|
|
9576
|
+
async unarchiveLoop(idOrName) {
|
|
9577
|
+
return this.store.unarchiveLoop(idOrName);
|
|
9578
|
+
}
|
|
9579
|
+
async deleteLoop(idOrName) {
|
|
9580
|
+
return this.store.deleteLoop(idOrName);
|
|
9581
|
+
}
|
|
9582
|
+
async createWorkflow(input) {
|
|
9583
|
+
return this.store.createWorkflow(input);
|
|
9584
|
+
}
|
|
9585
|
+
async getWorkflow(id) {
|
|
9586
|
+
return this.store.getWorkflow(id);
|
|
9587
|
+
}
|
|
9588
|
+
async findWorkflowByName(name) {
|
|
9589
|
+
return this.store.findWorkflowByName(name);
|
|
9590
|
+
}
|
|
9591
|
+
async requireWorkflow(idOrName) {
|
|
9592
|
+
return this.store.requireWorkflow(idOrName);
|
|
9593
|
+
}
|
|
9594
|
+
async listWorkflows(opts = {}) {
|
|
9595
|
+
return this.store.listWorkflows(opts);
|
|
9596
|
+
}
|
|
9597
|
+
async countWorkflows(opts = {}) {
|
|
9598
|
+
return this.store.countWorkflows(opts);
|
|
9599
|
+
}
|
|
9600
|
+
async archiveWorkflow(idOrName) {
|
|
9601
|
+
return this.store.archiveWorkflow(idOrName);
|
|
9602
|
+
}
|
|
9603
|
+
async getWorkflowRun(id) {
|
|
9604
|
+
return this.store.getWorkflowRun(id);
|
|
9605
|
+
}
|
|
9606
|
+
async requireWorkflowRun(id) {
|
|
9607
|
+
return this.store.requireWorkflowRun(id);
|
|
9608
|
+
}
|
|
9609
|
+
async listWorkflowRuns(opts = {}) {
|
|
9610
|
+
return this.store.listWorkflowRuns(opts);
|
|
9611
|
+
}
|
|
9612
|
+
async listWorkflowStepRuns(workflowRunId) {
|
|
9613
|
+
return this.store.listWorkflowStepRuns(workflowRunId);
|
|
9614
|
+
}
|
|
9615
|
+
async listWorkflowEvents(workflowRunId, limit) {
|
|
9616
|
+
return limit === undefined ? this.store.listWorkflowEvents(workflowRunId) : this.store.listWorkflowEvents(workflowRunId, limit);
|
|
9617
|
+
}
|
|
9618
|
+
async recoverWorkflowRun(workflowRunId, reason) {
|
|
9619
|
+
return reason === undefined ? this.store.recoverWorkflowRun(workflowRunId) : this.store.recoverWorkflowRun(workflowRunId, reason);
|
|
9620
|
+
}
|
|
9621
|
+
async cancelWorkflowRun(workflowRunId, reason) {
|
|
9622
|
+
return reason === undefined ? this.store.cancelWorkflowRun(workflowRunId) : this.store.cancelWorkflowRun(workflowRunId, reason);
|
|
9623
|
+
}
|
|
9624
|
+
async listWorkflowWorkItems(opts = {}) {
|
|
9625
|
+
return this.store.listWorkflowWorkItems(opts);
|
|
9626
|
+
}
|
|
9627
|
+
async getWorkflowWorkItem(id) {
|
|
9628
|
+
return this.store.getWorkflowWorkItem(id);
|
|
9629
|
+
}
|
|
9630
|
+
async requeueWorkflowWorkItem(id, patch = {}) {
|
|
9631
|
+
return this.store.requeueWorkflowWorkItem(id, patch);
|
|
9632
|
+
}
|
|
9633
|
+
async listWorkflowInvocations(opts = {}) {
|
|
9634
|
+
return this.store.listWorkflowInvocations(opts);
|
|
9635
|
+
}
|
|
9636
|
+
async getWorkflowInvocation(id) {
|
|
9637
|
+
return this.store.getWorkflowInvocation(id);
|
|
9638
|
+
}
|
|
9639
|
+
async getGoal(id) {
|
|
9640
|
+
return this.store.getGoal(id);
|
|
9641
|
+
}
|
|
9642
|
+
async findGoalByLoop(idOrName) {
|
|
9643
|
+
return this.store.findGoalByLoop(idOrName);
|
|
9644
|
+
}
|
|
9645
|
+
async findGoalByRunId(id) {
|
|
9646
|
+
return this.store.findGoalByRunId(id);
|
|
9647
|
+
}
|
|
9648
|
+
async listGoals(opts = {}) {
|
|
9649
|
+
return this.store.listGoals(opts);
|
|
9650
|
+
}
|
|
9651
|
+
async listGoalPlanNodes(goalIdOrPlanId) {
|
|
9652
|
+
return this.store.listGoalPlanNodes(goalIdOrPlanId);
|
|
9653
|
+
}
|
|
9654
|
+
async listGoalRuns(opts = {}) {
|
|
9655
|
+
return this.store.listGoalRuns(opts);
|
|
9656
|
+
}
|
|
9657
|
+
async listRuns(opts = {}) {
|
|
9658
|
+
return this.store.listRuns(opts);
|
|
9659
|
+
}
|
|
9660
|
+
async getRun(id) {
|
|
9661
|
+
return this.store.getRun(id);
|
|
9662
|
+
}
|
|
9663
|
+
async writeRunReceipt(input) {
|
|
9664
|
+
return this.store.writeRunReceipt(input);
|
|
9665
|
+
}
|
|
9666
|
+
async getRunReceipt(runId) {
|
|
9667
|
+
return this.store.getRunReceipt(runId);
|
|
9668
|
+
}
|
|
9669
|
+
async listRunReceipts(opts = {}) {
|
|
9670
|
+
return this.store.listRunReceipts(opts);
|
|
9671
|
+
}
|
|
9672
|
+
async pruneHistory(opts) {
|
|
9673
|
+
return this.store.pruneHistory(opts);
|
|
9674
|
+
}
|
|
9675
|
+
}
|
|
9676
|
+
function clean(query) {
|
|
9677
|
+
const out = {};
|
|
9678
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9679
|
+
if (value !== undefined && value !== null && value !== "")
|
|
9680
|
+
out[key] = value;
|
|
9681
|
+
}
|
|
9682
|
+
return out;
|
|
9683
|
+
}
|
|
9684
|
+
function pickArray(raw, key, fallback = []) {
|
|
9685
|
+
if (raw && typeof raw === "object" && Array.isArray(raw[key])) {
|
|
9686
|
+
return raw[key];
|
|
9687
|
+
}
|
|
9688
|
+
return fallback;
|
|
9689
|
+
}
|
|
9690
|
+
function pickObject(raw, key) {
|
|
9691
|
+
if (raw && typeof raw === "object") {
|
|
9692
|
+
const value = raw[key];
|
|
9693
|
+
return value == null ? undefined : value;
|
|
9694
|
+
}
|
|
9695
|
+
return raw == null ? undefined : raw;
|
|
9696
|
+
}
|
|
9697
|
+
|
|
9698
|
+
class ApiStore {
|
|
9699
|
+
client;
|
|
9700
|
+
baseUrl;
|
|
9701
|
+
transport = "cloud-http";
|
|
9702
|
+
constructor(client, baseUrl) {
|
|
9703
|
+
this.client = client;
|
|
9704
|
+
this.baseUrl = baseUrl;
|
|
9705
|
+
}
|
|
9706
|
+
get t() {
|
|
9707
|
+
return this.client.transport;
|
|
9708
|
+
}
|
|
9709
|
+
async close() {}
|
|
9710
|
+
async createLoop(input) {
|
|
9711
|
+
return pickObject(await this.t.post("/loops", input), "loop");
|
|
9712
|
+
}
|
|
9713
|
+
async getLoop(id) {
|
|
9714
|
+
try {
|
|
9715
|
+
return pickObject(await this.t.get(`/loops/${encodeURIComponent(id)}`), "loop");
|
|
9716
|
+
} catch {
|
|
9717
|
+
return;
|
|
9718
|
+
}
|
|
9719
|
+
}
|
|
9720
|
+
async findLoopByName(name) {
|
|
9721
|
+
const matches = (await this.listLoops({ name, limit: 1000 })).filter((loop) => loop.name === name);
|
|
9722
|
+
return matches[0];
|
|
9723
|
+
}
|
|
9724
|
+
async requireLoop(idOrName) {
|
|
9725
|
+
const loop = await this.resolveLoop(idOrName);
|
|
9726
|
+
if (!loop)
|
|
9727
|
+
throw new LoopNotFoundError(idOrName);
|
|
9728
|
+
return loop;
|
|
9729
|
+
}
|
|
9730
|
+
async requireUniqueLoop(idOrName) {
|
|
9731
|
+
const byId = await this.getLoop(idOrName);
|
|
9732
|
+
if (byId)
|
|
9733
|
+
return byId;
|
|
9734
|
+
const matches = (await this.listLoops({ name: idOrName, limit: 1000 })).filter((loop) => loop.name === idOrName);
|
|
9735
|
+
if (matches.length === 0)
|
|
9736
|
+
throw new LoopNotFoundError(idOrName);
|
|
9737
|
+
if (matches.length === 1)
|
|
9738
|
+
return matches[0];
|
|
9739
|
+
const active = matches.filter((loop) => !loop.archivedAt);
|
|
9740
|
+
if (active.length !== 1)
|
|
9741
|
+
throw new AmbiguousNameError(idOrName);
|
|
9742
|
+
return active[0];
|
|
9743
|
+
}
|
|
9744
|
+
async resolveLoop(idOrName) {
|
|
9745
|
+
const byId = await this.getLoop(idOrName);
|
|
9746
|
+
if (byId)
|
|
9747
|
+
return byId;
|
|
9748
|
+
const matches = (await this.listLoops({ name: idOrName, limit: 1000 })).filter((loop) => loop.name === idOrName);
|
|
9749
|
+
if (matches.length === 0)
|
|
9750
|
+
return;
|
|
9751
|
+
if (matches.length === 1)
|
|
9752
|
+
return matches[0];
|
|
9753
|
+
const active = matches.filter((loop) => !loop.archivedAt);
|
|
9754
|
+
if (active.length === 1)
|
|
9755
|
+
return active[0];
|
|
9756
|
+
throw new AmbiguousNameError(idOrName);
|
|
9757
|
+
}
|
|
9758
|
+
async listLoops(opts = {}) {
|
|
9759
|
+
const raw = await this.t.get("/loops", { query: clean({ ...opts }) });
|
|
9760
|
+
return pickArray(raw, "loops");
|
|
9761
|
+
}
|
|
9762
|
+
async countLoops(status, opts = {}) {
|
|
9763
|
+
const raw = await this.t.get("/loops/count", { query: clean({ status, ...opts }) });
|
|
9764
|
+
return Number(pickObject(raw, "count") ?? 0);
|
|
9765
|
+
}
|
|
9766
|
+
async updateLoop(id, patch) {
|
|
9767
|
+
return pickObject(await this.t.patch(`/loops/${encodeURIComponent(id)}`, patch), "loop");
|
|
9768
|
+
}
|
|
9769
|
+
async renameLoop(id, name) {
|
|
9770
|
+
return pickObject(await this.t.post(`/loops/${encodeURIComponent(id)}/rename`, { name }), "loop");
|
|
9771
|
+
}
|
|
9772
|
+
async archiveLoop(idOrName) {
|
|
9773
|
+
const loop = await this.requireLoop(idOrName);
|
|
9774
|
+
return pickObject(await this.t.post(`/loops/${encodeURIComponent(loop.id)}/archive`), "loop");
|
|
9775
|
+
}
|
|
9776
|
+
async unarchiveLoop(idOrName) {
|
|
9777
|
+
const loop = await this.requireLoop(idOrName);
|
|
9778
|
+
return pickObject(await this.t.post(`/loops/${encodeURIComponent(loop.id)}/unarchive`), "loop");
|
|
9779
|
+
}
|
|
9780
|
+
async deleteLoop(idOrName) {
|
|
9781
|
+
const loop = await this.resolveLoop(idOrName);
|
|
9782
|
+
if (!loop)
|
|
9783
|
+
return false;
|
|
9784
|
+
const raw = await this.t.request("DELETE", `/loops/${encodeURIComponent(loop.id)}`);
|
|
9785
|
+
return Boolean(pickObject(raw, "deleted") ?? true);
|
|
9786
|
+
}
|
|
9787
|
+
async createWorkflow(input) {
|
|
9788
|
+
return pickObject(await this.t.post("/workflows", input), "workflow");
|
|
9789
|
+
}
|
|
9790
|
+
async getWorkflow(id) {
|
|
9791
|
+
try {
|
|
9792
|
+
return pickObject(await this.t.get(`/workflows/${encodeURIComponent(id)}`), "workflow");
|
|
9793
|
+
} catch {
|
|
9794
|
+
return;
|
|
9795
|
+
}
|
|
9796
|
+
}
|
|
9797
|
+
async findWorkflowByName(name) {
|
|
9798
|
+
const matches = (await this.listWorkflows({ limit: 1000 })).filter((wf) => wf.name === name);
|
|
9799
|
+
return matches[0];
|
|
9800
|
+
}
|
|
9801
|
+
async requireWorkflow(idOrName) {
|
|
9802
|
+
const byId = await this.getWorkflow(idOrName);
|
|
9803
|
+
if (byId)
|
|
9804
|
+
return byId;
|
|
9805
|
+
const byName = await this.findWorkflowByName(idOrName);
|
|
9806
|
+
if (byName)
|
|
9807
|
+
return byName;
|
|
9808
|
+
throw new Error(`workflow not found: ${idOrName}`);
|
|
9809
|
+
}
|
|
9810
|
+
async listWorkflows(opts = {}) {
|
|
9811
|
+
const raw = await this.t.get("/workflows", { query: clean({ ...opts }) });
|
|
9812
|
+
return pickArray(raw, "workflows");
|
|
9813
|
+
}
|
|
9814
|
+
async countWorkflows(opts = {}) {
|
|
9815
|
+
const raw = await this.t.get("/workflows/count", { query: clean({ ...opts }) });
|
|
9816
|
+
return Number(pickObject(raw, "count") ?? 0);
|
|
9817
|
+
}
|
|
9818
|
+
async archiveWorkflow(idOrName) {
|
|
9819
|
+
const wf = await this.requireWorkflow(idOrName);
|
|
9820
|
+
return pickObject(await this.t.post(`/workflows/${encodeURIComponent(wf.id)}/archive`), "workflow");
|
|
9821
|
+
}
|
|
9822
|
+
async getWorkflowRun(id) {
|
|
9823
|
+
try {
|
|
9824
|
+
return pickObject(await this.t.get(`/workflow-runs/${encodeURIComponent(id)}`), "workflowRun");
|
|
9825
|
+
} catch {
|
|
9826
|
+
return;
|
|
9827
|
+
}
|
|
9828
|
+
}
|
|
9829
|
+
async requireWorkflowRun(id) {
|
|
9830
|
+
const run = await this.getWorkflowRun(id);
|
|
9831
|
+
if (!run)
|
|
9832
|
+
throw new Error(`workflow run not found: ${id}`);
|
|
9833
|
+
return run;
|
|
9834
|
+
}
|
|
9835
|
+
async listWorkflowRuns(opts = {}) {
|
|
9836
|
+
const raw = await this.t.get("/workflow-runs", { query: clean({ ...opts }) });
|
|
9837
|
+
return pickArray(raw, "workflowRuns");
|
|
9838
|
+
}
|
|
9839
|
+
async listWorkflowStepRuns(workflowRunId) {
|
|
9840
|
+
const raw = await this.t.get(`/workflow-runs/${encodeURIComponent(workflowRunId)}/steps`);
|
|
9841
|
+
return pickArray(raw, "steps");
|
|
9842
|
+
}
|
|
9843
|
+
async listWorkflowEvents(workflowRunId, limit) {
|
|
9844
|
+
const raw = await this.t.get(`/workflow-runs/${encodeURIComponent(workflowRunId)}/events`, { query: clean({ limit }) });
|
|
9845
|
+
return pickArray(raw, "events");
|
|
9846
|
+
}
|
|
9847
|
+
async recoverWorkflowRun(workflowRunId, reason) {
|
|
9848
|
+
const raw = await this.t.post(`/workflow-runs/${encodeURIComponent(workflowRunId)}/recover`, { reason });
|
|
9849
|
+
return {
|
|
9850
|
+
run: pickObject(raw, "workflowRun"),
|
|
9851
|
+
recoveredSteps: pickArray(raw, "recoveredSteps")
|
|
9852
|
+
};
|
|
9853
|
+
}
|
|
9854
|
+
async cancelWorkflowRun(_workflowRunId, _reason) {
|
|
9855
|
+
throw new CloudUnsupportedError("workflows cancel");
|
|
9856
|
+
}
|
|
9857
|
+
async listWorkflowWorkItems(opts = {}) {
|
|
9858
|
+
const raw = await this.t.get("/work-items", { query: clean({ ...opts }) });
|
|
9859
|
+
return pickArray(raw, "workItems");
|
|
9860
|
+
}
|
|
9861
|
+
async getWorkflowWorkItem(id) {
|
|
9862
|
+
try {
|
|
9863
|
+
return pickObject(await this.t.get(`/work-items/${encodeURIComponent(id)}`), "workItem");
|
|
9864
|
+
} catch {
|
|
9865
|
+
return;
|
|
9866
|
+
}
|
|
9867
|
+
}
|
|
9868
|
+
async requeueWorkflowWorkItem(_id, _patch = {}) {
|
|
9869
|
+
throw new CloudUnsupportedError("routes requeue");
|
|
9870
|
+
}
|
|
9871
|
+
async listWorkflowInvocations(opts = {}) {
|
|
9872
|
+
const raw = await this.t.get("/invocations", { query: clean({ ...opts }) });
|
|
9873
|
+
return pickArray(raw, "invocations");
|
|
9874
|
+
}
|
|
9875
|
+
async getWorkflowInvocation(id) {
|
|
9876
|
+
try {
|
|
9877
|
+
return pickObject(await this.t.get(`/invocations/${encodeURIComponent(id)}`), "invocation");
|
|
9878
|
+
} catch {
|
|
9879
|
+
return;
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
async getGoal(id) {
|
|
9883
|
+
try {
|
|
9884
|
+
return pickObject(await this.t.get(`/goals/${encodeURIComponent(id)}`), "goal");
|
|
9885
|
+
} catch {
|
|
9886
|
+
return;
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
async findGoalByLoop(idOrName) {
|
|
9890
|
+
try {
|
|
9891
|
+
return pickObject(await this.t.get("/goals", { query: clean({ loop: idOrName }) }), "goal");
|
|
9892
|
+
} catch {
|
|
9893
|
+
return;
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9896
|
+
async findGoalByRunId(id) {
|
|
9897
|
+
try {
|
|
9898
|
+
return pickObject(await this.t.get("/goals", { query: clean({ runId: id }) }), "goal");
|
|
9899
|
+
} catch {
|
|
9900
|
+
return;
|
|
9901
|
+
}
|
|
9902
|
+
}
|
|
9903
|
+
async listGoals(opts = {}) {
|
|
9904
|
+
const raw = await this.t.get("/goals", { query: clean({ ...opts }) });
|
|
9905
|
+
return pickArray(raw, "goals");
|
|
9906
|
+
}
|
|
9907
|
+
async listGoalPlanNodes(goalIdOrPlanId) {
|
|
9908
|
+
const raw = await this.t.get(`/goals/${encodeURIComponent(goalIdOrPlanId)}/plan-nodes`);
|
|
9909
|
+
return pickArray(raw, "nodes");
|
|
9910
|
+
}
|
|
9911
|
+
async listGoalRuns(opts = {}) {
|
|
9912
|
+
const raw = await this.t.get("/goal-runs", { query: clean({ ...opts }) });
|
|
9913
|
+
return pickArray(raw, "goalRuns");
|
|
9914
|
+
}
|
|
9915
|
+
async listRuns(opts = {}) {
|
|
9916
|
+
const raw = await this.t.get("/runs", { query: clean({ ...opts, showOutput: true }) });
|
|
9917
|
+
return pickArray(raw, "runs");
|
|
9918
|
+
}
|
|
9919
|
+
async getRun(id) {
|
|
9920
|
+
try {
|
|
9921
|
+
return pickObject(await this.t.get(`/runs/${encodeURIComponent(id)}`, { query: { showOutput: true } }), "run");
|
|
9922
|
+
} catch {
|
|
9923
|
+
return;
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9926
|
+
async writeRunReceipt(input) {
|
|
9927
|
+
return pickObject(await this.t.post("/receipts", input), "receipt");
|
|
9928
|
+
}
|
|
9929
|
+
async getRunReceipt(runId) {
|
|
9930
|
+
try {
|
|
9931
|
+
return pickObject(await this.t.get(`/receipts/${encodeURIComponent(runId)}`), "receipt");
|
|
9932
|
+
} catch {
|
|
9933
|
+
return;
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9936
|
+
async listRunReceipts(opts = {}) {
|
|
9937
|
+
const raw = await this.t.get("/receipts", { query: clean({ ...opts }) });
|
|
9938
|
+
return pickArray(raw, "receipts");
|
|
9939
|
+
}
|
|
9940
|
+
async pruneHistory(opts) {
|
|
9941
|
+
const raw = await this.t.post("/history/prune", {
|
|
9942
|
+
maxAgeDays: opts.maxAgeDays,
|
|
9943
|
+
keepPerLoop: opts.keepPerLoop,
|
|
9944
|
+
dryRun: opts.dryRun
|
|
9945
|
+
});
|
|
9946
|
+
return pickObject(raw, "history");
|
|
9947
|
+
}
|
|
9948
|
+
}
|
|
9949
|
+
function getStore(env = process.env) {
|
|
9950
|
+
const resolution = resolveCloudStorage("loops", env);
|
|
9951
|
+
if (resolution.transport === "cloud-http")
|
|
9952
|
+
return new ApiStore(resolution.client, resolution.baseUrl);
|
|
9953
|
+
return new LocalStore;
|
|
9954
|
+
}
|
|
9955
|
+
function isCloudStore(env = process.env) {
|
|
9956
|
+
return resolveCloudStorage("loops", env).transport === "cloud-http";
|
|
9957
|
+
}
|
|
9958
|
+
|
|
9959
|
+
// src/mcp/index.ts
|
|
9960
|
+
var LOOP_STATUSES = ["active", "paused", "stopped", "expired"];
|
|
9961
|
+
var LOOP_STATUS_FILTERS = [...LOOP_STATUSES, "all"];
|
|
9962
|
+
var RUN_STATUSES = ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
9963
|
+
var WORKFLOW_STATUSES = ["active", "archived"];
|
|
9964
|
+
var WORKFLOW_STATUS_FILTERS = [...WORKFLOW_STATUSES, "all"];
|
|
9965
|
+
var CATCH_UP_POLICIES = ["none", "latest", "all"];
|
|
9966
|
+
var OVERLAP_POLICIES = ["skip", "allow"];
|
|
9967
|
+
var INTERVAL_ANCHORS = ["fixed_rate", "fixed_delay"];
|
|
9968
|
+
var MAX_LIMIT = 500;
|
|
9969
|
+
var MUTATION_ENV = "LOOPS_MCP_ALLOW_MUTATIONS";
|
|
9970
|
+
var loopIdOrNameSchema = z2.string().min(1).describe("Loop id or exact loop name. Names resolve on exact match only; ambiguous names require the id.");
|
|
9971
|
+
var workflowIdOrNameSchema = z2.string().min(1).describe("Workflow id or exact workflow name.");
|
|
9972
|
+
var showOutputSchema = z2.boolean().optional().describe("Include raw stdout/stderr (default false: only redacted lengths are returned).");
|
|
9973
|
+
var limitSchema = z2.number().int().min(1).max(MAX_LIMIT).optional().describe(`Maximum entries to return (1-${MAX_LIMIT}).`);
|
|
9974
|
+
var runReceiptSummarySchema = z2.object({
|
|
9975
|
+
text: z2.string().optional().describe("Short human summary. It is scrubbed and bounded before storage."),
|
|
9976
|
+
stdout_bytes: z2.number().int().min(0).optional().describe("Original stdout byte count."),
|
|
9977
|
+
stderr_bytes: z2.number().int().min(0).optional().describe("Original stderr byte count."),
|
|
9978
|
+
stdout_excerpt: z2.string().optional().describe("Bounded stdout excerpt. Raw unbounded stdout is never required."),
|
|
9979
|
+
stderr_excerpt: z2.string().optional().describe("Bounded stderr excerpt. Raw unbounded stderr is never required."),
|
|
9980
|
+
error: z2.string().optional().describe("Bounded error excerpt."),
|
|
9981
|
+
duration_ms: z2.number().int().min(0).optional().describe("Run duration in milliseconds.")
|
|
9982
|
+
});
|
|
9983
|
+
var runReceiptInputSchema = {
|
|
9984
|
+
loop_id: z2.string().min(1).optional().describe("OpenLoops loop id. Optional when run_id references an existing loop run."),
|
|
9985
|
+
run_id: z2.string().min(1).describe("Scheduler-neutral run id. Existing values are updated idempotently."),
|
|
9986
|
+
machine: z2.union([z2.string().min(1), z2.record(z2.string(), z2.unknown())]).optional().describe("Machine id/name or machine metadata object."),
|
|
9987
|
+
repo: z2.string().min(1).optional().describe("Repository path or owner/repo string. Defaults from the loop target cwd when possible."),
|
|
9988
|
+
task_ids: z2.array(z2.string().min(1)).optional().describe("Task ids associated with this run."),
|
|
9989
|
+
knowledge_ids: z2.array(z2.string().min(1)).optional().describe("Knowledge record ids associated with this run."),
|
|
9990
|
+
digest_id: z2.string().min(1).optional().describe("Stable digest id. Computed from normalized receipt content when omitted."),
|
|
9991
|
+
started_at: z2.string().nullable().optional().describe("Run start timestamp."),
|
|
9992
|
+
finished_at: z2.string().nullable().optional().describe("Run finish timestamp."),
|
|
9993
|
+
status: z2.string().min(1).optional().describe("Run status."),
|
|
9994
|
+
exit_code: z2.number().int().nullable().optional().describe("Process exit code."),
|
|
9995
|
+
summary: z2.union([z2.string(), runReceiptSummarySchema]).nullable().optional().describe("Bounded structured summary; may be a string shorthand."),
|
|
9996
|
+
evidence_paths: z2.array(z2.string().min(1)).optional().describe("Bounded paths to durable evidence artifacts."),
|
|
9997
|
+
stdout: z2.string().optional().describe("Optional raw stdout to summarize and bound before storage."),
|
|
9998
|
+
stderr: z2.string().optional().describe("Optional raw stderr to summarize and bound before storage."),
|
|
9999
|
+
error: z2.string().optional().describe("Optional raw error text to summarize and bound before storage."),
|
|
10000
|
+
duration_ms: z2.number().int().min(0).optional().describe("Run duration in milliseconds.")
|
|
10001
|
+
};
|
|
10002
|
+
var optionalTimeoutSchema = z2.number().int().positive().nullable().optional().describe("Per-run timeout in milliseconds; null disables the timeout.");
|
|
10003
|
+
var catchUpSchema = z2.enum(CATCH_UP_POLICIES).optional().describe("Missed-slot policy after downtime: none skips them, latest replays the newest slot, all replays every slot.");
|
|
10004
|
+
var overlapSchema = z2.enum(OVERLAP_POLICIES).optional().describe("Behavior when a slot comes due while a previous run is active: skip records a skipped run, allow runs concurrently.");
|
|
10005
|
+
var scheduleSchema = z2.discriminatedUnion("type", [
|
|
10006
|
+
z2.object({
|
|
10007
|
+
type: z2.literal("once"),
|
|
10008
|
+
at: z2.string().describe("Absolute date/time parseable by JavaScript Date (ISO 8601 recommended).")
|
|
10009
|
+
}),
|
|
10010
|
+
z2.object({
|
|
8609
10011
|
type: z2.literal("interval"),
|
|
8610
10012
|
everyMs: z2.number().int().positive().describe("Interval between runs in milliseconds."),
|
|
8611
10013
|
anchor: z2.enum(INTERVAL_ANCHORS).optional().describe("fixed_rate anchors slots to the original cadence; fixed_delay measures from the previous finish.")
|
|
@@ -8666,6 +10068,17 @@ function errorResult(error) {
|
|
|
8666
10068
|
};
|
|
8667
10069
|
}
|
|
8668
10070
|
async function withStore(fn) {
|
|
10071
|
+
const store = getStore();
|
|
10072
|
+
try {
|
|
10073
|
+
return await fn(store);
|
|
10074
|
+
} finally {
|
|
10075
|
+
await store.close();
|
|
10076
|
+
}
|
|
10077
|
+
}
|
|
10078
|
+
async function withLocalStore(operation, fn) {
|
|
10079
|
+
if (isCloudStore()) {
|
|
10080
|
+
throw new Error(`'${operation}' inspects this machine's local OpenLoops runtime and is not available while flipped to the hosted OpenLoops API. ` + `Unset HASNA_LOOPS_API_URL/HASNA_LOOPS_API_KEY (or set HASNA_LOOPS_STORAGE_MODE=local) to run it here.`);
|
|
10081
|
+
}
|
|
8669
10082
|
const store = new Store;
|
|
8670
10083
|
try {
|
|
8671
10084
|
return await fn(store);
|
|
@@ -8679,7 +10092,7 @@ function requireMutationsEnabled() {
|
|
|
8679
10092
|
throw new Error(`MCP mutation tools require ${MUTATION_ENV}=true`);
|
|
8680
10093
|
}
|
|
8681
10094
|
}
|
|
8682
|
-
function
|
|
10095
|
+
function nonEmpty2(value, label) {
|
|
8683
10096
|
const trimmed = value.trim();
|
|
8684
10097
|
if (!trimmed)
|
|
8685
10098
|
throw new Error(`${label} must be non-empty`);
|
|
@@ -8697,7 +10110,7 @@ function normalizeSchedule(input) {
|
|
|
8697
10110
|
if (input.type === "interval")
|
|
8698
10111
|
return { type: "interval", everyMs: input.everyMs, anchor: input.anchor ?? "fixed_rate" };
|
|
8699
10112
|
if (input.type === "cron")
|
|
8700
|
-
return { type: "cron", expression:
|
|
10113
|
+
return { type: "cron", expression: nonEmpty2(input.expression, "schedule.expression") };
|
|
8701
10114
|
return { type: "dynamic", minIntervalMs: input.minIntervalMs };
|
|
8702
10115
|
}
|
|
8703
10116
|
function durationLabel(ms) {
|
|
@@ -8740,7 +10153,7 @@ function defaultLoopDescription(name, schedule, target) {
|
|
|
8740
10153
|
].join(" ");
|
|
8741
10154
|
}
|
|
8742
10155
|
function commonCreateInput(input) {
|
|
8743
|
-
const name =
|
|
10156
|
+
const name = nonEmpty2(input.name, "name");
|
|
8744
10157
|
const schedule = normalizeSchedule(input.schedule);
|
|
8745
10158
|
return {
|
|
8746
10159
|
name,
|
|
@@ -8796,13 +10209,13 @@ var TOOL_REGISTRATIONS = [
|
|
|
8796
10209
|
includeArchived: z2.boolean().optional().describe("Include archived loops alongside live ones (default false)."),
|
|
8797
10210
|
archivedOnly: z2.boolean().optional().describe("Return only archived loops (default false).")
|
|
8798
10211
|
},
|
|
8799
|
-
handler: ({ status, limit, includeArchived, archivedOnly }) => withStore((store) => ({
|
|
8800
|
-
loops: store.listLoops({
|
|
10212
|
+
handler: ({ status, limit, includeArchived, archivedOnly }) => withStore(async (store) => ({
|
|
10213
|
+
loops: (await store.listLoops({
|
|
8801
10214
|
status: filteredLoopStatus(status),
|
|
8802
10215
|
limit,
|
|
8803
10216
|
includeArchived: includeArchived ?? false,
|
|
8804
10217
|
archived: archivedOnly ?? false
|
|
8805
|
-
}).map(publicLoop)
|
|
10218
|
+
})).map(publicLoop)
|
|
8806
10219
|
}))
|
|
8807
10220
|
},
|
|
8808
10221
|
{
|
|
@@ -8815,9 +10228,9 @@ var TOOL_REGISTRATIONS = [
|
|
|
8815
10228
|
includeLatestRun: z2.boolean().optional().describe("Include the most recent run record (default false)."),
|
|
8816
10229
|
showOutput: showOutputSchema
|
|
8817
10230
|
},
|
|
8818
|
-
handler: ({ idOrName, includeLatestRun, showOutput }) => withStore((store) => {
|
|
8819
|
-
const loop = store.requireLoop(idOrName);
|
|
8820
|
-
const latestRun = includeLatestRun ? store.listRuns({ loopId: loop.id, limit: 1 })[0] : undefined;
|
|
10231
|
+
handler: ({ idOrName, includeLatestRun, showOutput }) => withStore(async (store) => {
|
|
10232
|
+
const loop = await store.requireLoop(idOrName);
|
|
10233
|
+
const latestRun = includeLatestRun ? (await store.listRuns({ loopId: loop.id, limit: 1 }))[0] : undefined;
|
|
8821
10234
|
return {
|
|
8822
10235
|
loop: publicLoop(loop),
|
|
8823
10236
|
latestRun: latestRun ? publicRun(latestRun, showOutput ?? false) : undefined
|
|
@@ -8836,23 +10249,67 @@ var TOOL_REGISTRATIONS = [
|
|
|
8836
10249
|
limit: limitSchema,
|
|
8837
10250
|
showOutput: showOutputSchema
|
|
8838
10251
|
},
|
|
8839
|
-
handler: ({ idOrName, status, limit, showOutput }) => withStore((store) => {
|
|
8840
|
-
const loop = idOrName ? store.requireLoop(idOrName) : undefined;
|
|
8841
|
-
const runs = store.listRuns({
|
|
10252
|
+
handler: ({ idOrName, status, limit, showOutput }) => withStore(async (store) => {
|
|
10253
|
+
const loop = idOrName ? await store.requireLoop(idOrName) : undefined;
|
|
10254
|
+
const runs = (await store.listRuns({
|
|
8842
10255
|
loopId: loop?.id,
|
|
8843
10256
|
status,
|
|
8844
10257
|
limit
|
|
8845
|
-
}).map((run) => publicRun(run, showOutput ?? false));
|
|
10258
|
+
})).map((run) => publicRun(run, showOutput ?? false));
|
|
8846
10259
|
return { loop: loop ? publicLoop(loop) : undefined, runs };
|
|
8847
10260
|
})
|
|
8848
10261
|
},
|
|
10262
|
+
{
|
|
10263
|
+
name: "loops_receipts_list",
|
|
10264
|
+
description: "List scheduler-neutral run receipts with bounded summaries and evidence paths.",
|
|
10265
|
+
readOnly: true,
|
|
10266
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
10267
|
+
inputSchema: {
|
|
10268
|
+
loop_id: z2.string().min(1).optional().describe("Filter by loop_id."),
|
|
10269
|
+
repo: z2.string().min(1).optional().describe("Filter by repo."),
|
|
10270
|
+
task_id: z2.string().min(1).optional().describe("Filter by task id."),
|
|
10271
|
+
knowledge_id: z2.string().min(1).optional().describe("Filter by knowledge id."),
|
|
10272
|
+
status: z2.string().min(1).optional().describe("Filter by receipt status."),
|
|
10273
|
+
limit: limitSchema
|
|
10274
|
+
},
|
|
10275
|
+
handler: ({ loop_id, repo, task_id, knowledge_id, status, limit }) => withStore(async (store) => ({
|
|
10276
|
+
receipts: (await store.listRunReceipts({ loopId: loop_id, repo, taskId: task_id, knowledgeId: knowledge_id, status, limit })).map(publicRunReceipt)
|
|
10277
|
+
}))
|
|
10278
|
+
},
|
|
10279
|
+
{
|
|
10280
|
+
name: "loops_receipt_read",
|
|
10281
|
+
description: "Read one scheduler-neutral run receipt by run id.",
|
|
10282
|
+
readOnly: true,
|
|
10283
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
10284
|
+
inputSchema: {
|
|
10285
|
+
run_id: z2.string().min(1).describe("Run id.")
|
|
10286
|
+
},
|
|
10287
|
+
handler: ({ run_id }) => withStore(async (store) => {
|
|
10288
|
+
const receipt = await store.getRunReceipt(run_id);
|
|
10289
|
+
if (!receipt)
|
|
10290
|
+
throw new Error(`run receipt not found: ${run_id}`);
|
|
10291
|
+
return { receipt: publicRunReceipt(receipt) };
|
|
10292
|
+
})
|
|
10293
|
+
},
|
|
10294
|
+
{
|
|
10295
|
+
name: "loops_receipt_write",
|
|
10296
|
+
description: `Write a scheduler-neutral run receipt. Requires ${MUTATION_ENV}=true on the MCP server process.`,
|
|
10297
|
+
readOnly: false,
|
|
10298
|
+
guarded: true,
|
|
10299
|
+
annotations: mutationAnnotations({ idempotent: true }),
|
|
10300
|
+
inputSchema: runReceiptInputSchema,
|
|
10301
|
+
handler: (input) => {
|
|
10302
|
+
requireMutationsEnabled();
|
|
10303
|
+
return withStore(async (store) => ({ receipt: publicRunReceipt(await store.writeRunReceipt(input)) }));
|
|
10304
|
+
}
|
|
10305
|
+
},
|
|
8849
10306
|
{
|
|
8850
10307
|
name: "loops_doctor",
|
|
8851
10308
|
description: "Run OpenLoops runtime diagnostics.",
|
|
8852
10309
|
readOnly: true,
|
|
8853
10310
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
8854
10311
|
inputSchema: {},
|
|
8855
|
-
handler: () =>
|
|
10312
|
+
handler: () => withLocalStore("loops_doctor", (store) => runDoctor(store))
|
|
8856
10313
|
},
|
|
8857
10314
|
{
|
|
8858
10315
|
name: "loops_health",
|
|
@@ -8864,7 +10321,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8864
10321
|
includeInactive: z2.boolean().optional().describe("Include stopped/expired loops (default false: active and paused only)."),
|
|
8865
10322
|
limit: limitSchema
|
|
8866
10323
|
},
|
|
8867
|
-
handler: ({ includeArchived, includeInactive, limit }) =>
|
|
10324
|
+
handler: ({ includeArchived, includeInactive, limit }) => withLocalStore("loops_health", (store) => buildHealthReport(store, { includeArchived, includeInactive, limit }))
|
|
8868
10325
|
},
|
|
8869
10326
|
{
|
|
8870
10327
|
name: "loops_health_scan",
|
|
@@ -8881,7 +10338,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8881
10338
|
maxFindings: z2.number().int().min(0).max(MAX_LIMIT).optional().describe(`Maximum findings to return (0-${MAX_LIMIT}, default 100).`),
|
|
8882
10339
|
limit: limitSchema
|
|
8883
10340
|
},
|
|
8884
|
-
handler: ({ includeStatuses, includeArchived, latestRun, doctor, daemon, staleRunningMs, maxFindings, limit }) =>
|
|
10341
|
+
handler: ({ includeStatuses, includeArchived, latestRun, doctor, daemon, staleRunningMs, maxFindings, limit }) => withLocalStore("loops_health_scan", (store) => buildHealthScan(store, {
|
|
8885
10342
|
includeStatuses,
|
|
8886
10343
|
includeArchived,
|
|
8887
10344
|
latestRun,
|
|
@@ -8903,7 +10360,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8903
10360
|
runLimit: z2.number().int().min(1).max(50).optional().describe("How many recent runs to classify (1-50, default 5)."),
|
|
8904
10361
|
showOutput: showOutputSchema
|
|
8905
10362
|
},
|
|
8906
|
-
handler: ({ idOrName, runLimit, showOutput }) =>
|
|
10363
|
+
handler: ({ idOrName, runLimit, showOutput }) => withLocalStore("loops_diagnose", (store) => {
|
|
8907
10364
|
const loop = store.requireLoop(idOrName);
|
|
8908
10365
|
const runs = store.listRuns({ loopId: loop.id, limit: runLimit ?? 5 });
|
|
8909
10366
|
return {
|
|
@@ -8923,7 +10380,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
8923
10380
|
readOnly: true,
|
|
8924
10381
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
8925
10382
|
inputSchema: {},
|
|
8926
|
-
handler: () =>
|
|
10383
|
+
handler: () => withLocalStore("loops_daemon_status", (store) => daemonStatus(store))
|
|
8927
10384
|
},
|
|
8928
10385
|
{
|
|
8929
10386
|
name: "loops_workflows_list",
|
|
@@ -8936,12 +10393,12 @@ var TOOL_REGISTRATIONS = [
|
|
|
8936
10393
|
limit: limitSchema,
|
|
8937
10394
|
offset: z2.number().int().min(0).optional().describe("Entries to skip before returning results (pagination).")
|
|
8938
10395
|
},
|
|
8939
|
-
handler: ({ status, limit, offset }) => withStore((store) => ({
|
|
8940
|
-
workflows: store.listWorkflows({
|
|
10396
|
+
handler: ({ status, limit, offset }) => withStore(async (store) => ({
|
|
10397
|
+
workflows: (await store.listWorkflows({
|
|
8941
10398
|
status: filteredWorkflowStatus(status),
|
|
8942
10399
|
limit,
|
|
8943
10400
|
offset
|
|
8944
|
-
}).map(publicWorkflow)
|
|
10401
|
+
})).map(publicWorkflow)
|
|
8945
10402
|
}))
|
|
8946
10403
|
},
|
|
8947
10404
|
{
|
|
@@ -8957,16 +10414,16 @@ var TOOL_REGISTRATIONS = [
|
|
|
8957
10414
|
runLimit: limitSchema,
|
|
8958
10415
|
showOutput: showOutputSchema
|
|
8959
10416
|
},
|
|
8960
|
-
handler: ({ idOrName, includeRuns, includeEvents, runLimit, showOutput }) => withStore((store) => {
|
|
8961
|
-
const workflow = store.requireWorkflow(idOrName);
|
|
8962
|
-
const runs = includeRuns ? store.listWorkflowRuns({ workflowId: workflow.id, limit: runLimit ?? 20 }) : [];
|
|
10417
|
+
handler: ({ idOrName, includeRuns, includeEvents, runLimit, showOutput }) => withStore(async (store) => {
|
|
10418
|
+
const workflow = await store.requireWorkflow(idOrName);
|
|
10419
|
+
const runs = includeRuns ? await store.listWorkflowRuns({ workflowId: workflow.id, limit: runLimit ?? 20 }) : [];
|
|
8963
10420
|
return {
|
|
8964
10421
|
workflow: publicWorkflow(workflow),
|
|
8965
|
-
runs: runs.map((run) => ({
|
|
10422
|
+
runs: await Promise.all(runs.map(async (run) => ({
|
|
8966
10423
|
...publicWorkflowRun(run),
|
|
8967
|
-
steps: store.listWorkflowStepRuns(run.id).map((step) => publicWorkflowStepRun(step, showOutput ?? false)),
|
|
8968
|
-
events: includeEvents ? store.listWorkflowEvents(run.id).map(publicWorkflowEvent) : undefined
|
|
8969
|
-
}))
|
|
10424
|
+
steps: (await store.listWorkflowStepRuns(run.id)).map((step) => publicWorkflowStepRun(step, showOutput ?? false)),
|
|
10425
|
+
events: includeEvents ? (await store.listWorkflowEvents(run.id)).map(publicWorkflowEvent) : undefined
|
|
10426
|
+
})))
|
|
8970
10427
|
};
|
|
8971
10428
|
})
|
|
8972
10429
|
},
|
|
@@ -9008,14 +10465,14 @@ var TOOL_REGISTRATIONS = [
|
|
|
9008
10465
|
includeEvents: z2.boolean().optional().describe("Include the workflow event log (default true)."),
|
|
9009
10466
|
showOutput: showOutputSchema
|
|
9010
10467
|
},
|
|
9011
|
-
handler: ({ runId, includeEvents, showOutput }) => withStore((store) => {
|
|
9012
|
-
const run = store.getWorkflowRun(runId);
|
|
10468
|
+
handler: ({ runId, includeEvents, showOutput }) => withStore(async (store) => {
|
|
10469
|
+
const run = await store.getWorkflowRun(runId);
|
|
9013
10470
|
if (!run)
|
|
9014
10471
|
throw new Error(`workflow run not found: ${runId}`);
|
|
9015
10472
|
return {
|
|
9016
10473
|
run: publicWorkflowRun(run),
|
|
9017
|
-
steps: store.listWorkflowStepRuns(run.id).map((step) => publicWorkflowStepRun(step, showOutput ?? false)),
|
|
9018
|
-
events: includeEvents ?? true ? store.listWorkflowEvents(run.id).map(publicWorkflowEvent) : undefined
|
|
10474
|
+
steps: (await store.listWorkflowStepRuns(run.id)).map((step) => publicWorkflowStepRun(step, showOutput ?? false)),
|
|
10475
|
+
events: includeEvents ?? true ? (await store.listWorkflowEvents(run.id)).map(publicWorkflowEvent) : undefined
|
|
9019
10476
|
};
|
|
9020
10477
|
})
|
|
9021
10478
|
},
|
|
@@ -9027,7 +10484,12 @@ var TOOL_REGISTRATIONS = [
|
|
|
9027
10484
|
guarded: true,
|
|
9028
10485
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
9029
10486
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
9030
|
-
handler: ({ idOrName }) => withStore((store) =>
|
|
10487
|
+
handler: ({ idOrName }) => withStore(async (store) => {
|
|
10488
|
+
const loop = await store.requireUniqueLoop(idOrName);
|
|
10489
|
+
if (loop.archivedAt)
|
|
10490
|
+
throw new LoopArchivedError(idOrName);
|
|
10491
|
+
return { loop: publicLoop(await store.updateLoop(loop.id, { status: "paused" })) };
|
|
10492
|
+
})
|
|
9031
10493
|
},
|
|
9032
10494
|
{
|
|
9033
10495
|
name: "loops_resume",
|
|
@@ -9037,14 +10499,16 @@ var TOOL_REGISTRATIONS = [
|
|
|
9037
10499
|
guarded: true,
|
|
9038
10500
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
9039
10501
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
9040
|
-
handler: ({ idOrName }) => withStore((store) => {
|
|
9041
|
-
const loop = store.requireUniqueLoop(idOrName);
|
|
10502
|
+
handler: ({ idOrName }) => withStore(async (store) => {
|
|
10503
|
+
const loop = await store.requireUniqueLoop(idOrName);
|
|
10504
|
+
if (loop.archivedAt)
|
|
10505
|
+
throw new LoopArchivedError(idOrName);
|
|
9042
10506
|
let nextRunAt = loop.nextRunAt;
|
|
9043
10507
|
if (!nextRunAt) {
|
|
9044
10508
|
const now = new Date;
|
|
9045
10509
|
nextRunAt = computeNextAfter(loop.schedule, now, now);
|
|
9046
10510
|
}
|
|
9047
|
-
return { loop: publicLoop(store.updateLoop(loop.id, { status: "active", nextRunAt })) };
|
|
10511
|
+
return { loop: publicLoop(await store.updateLoop(loop.id, { status: "active", nextRunAt })) };
|
|
9048
10512
|
})
|
|
9049
10513
|
},
|
|
9050
10514
|
{
|
|
@@ -9055,9 +10519,12 @@ var TOOL_REGISTRATIONS = [
|
|
|
9055
10519
|
guarded: true,
|
|
9056
10520
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
9057
10521
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
9058
|
-
handler: ({ idOrName }) => withStore((store) =>
|
|
9059
|
-
loop
|
|
9060
|
-
|
|
10522
|
+
handler: ({ idOrName }) => withStore(async (store) => {
|
|
10523
|
+
const loop = await store.requireUniqueLoop(idOrName);
|
|
10524
|
+
if (loop.archivedAt)
|
|
10525
|
+
throw new LoopArchivedError(idOrName);
|
|
10526
|
+
return { loop: publicLoop(await store.updateLoop(loop.id, { status: "stopped", nextRunAt: undefined })) };
|
|
10527
|
+
})
|
|
9061
10528
|
},
|
|
9062
10529
|
{
|
|
9063
10530
|
name: "loops_run_now",
|
|
@@ -9068,8 +10535,22 @@ var TOOL_REGISTRATIONS = [
|
|
|
9068
10535
|
annotations: mutationAnnotations(),
|
|
9069
10536
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
9070
10537
|
handler: ({ idOrName }) => withStore(async (store) => {
|
|
9071
|
-
|
|
9072
|
-
|
|
10538
|
+
if (store.transport === "cloud-http") {
|
|
10539
|
+
const loop = await store.requireUniqueLoop(idOrName);
|
|
10540
|
+
if (loop.archivedAt)
|
|
10541
|
+
throw new LoopArchivedError(idOrName);
|
|
10542
|
+
const now = new Date().toISOString();
|
|
10543
|
+
const updated = await store.updateLoop(loop.id, { status: "active", nextRunAt: now });
|
|
10544
|
+
return {
|
|
10545
|
+
scheduledFor: now,
|
|
10546
|
+
loop: publicLoop(updated),
|
|
10547
|
+
daemon: undefined,
|
|
10548
|
+
warning: "loops is flipped to self_hosted: the loop is marked due on the hosted control plane; a self-hosted runner must execute it."
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
const raw = store.raw;
|
|
10552
|
+
const daemon = daemonStatus(raw);
|
|
10553
|
+
const result = await runLoopNow({ store: raw, idOrName: raw.requireUniqueLoop(idOrName).id, runnerId: `mcp:${process.pid}`, mode: "schedule" });
|
|
9073
10554
|
return {
|
|
9074
10555
|
scheduledFor: result.scheduledFor,
|
|
9075
10556
|
loop: publicLoop(result.loop),
|
|
@@ -9085,7 +10566,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
9085
10566
|
guarded: true,
|
|
9086
10567
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
9087
10568
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
9088
|
-
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.archiveLoop(idOrName)) }))
|
|
10569
|
+
handler: ({ idOrName }) => withStore(async (store) => ({ loop: publicLoop(await store.archiveLoop(idOrName)) }))
|
|
9089
10570
|
},
|
|
9090
10571
|
{
|
|
9091
10572
|
name: "loops_unarchive",
|
|
@@ -9094,7 +10575,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
9094
10575
|
guarded: true,
|
|
9095
10576
|
annotations: mutationAnnotations({ idempotent: true }),
|
|
9096
10577
|
inputSchema: { idOrName: loopIdOrNameSchema },
|
|
9097
|
-
handler: ({ idOrName }) => withStore((store) => ({ loop: publicLoop(store.unarchiveLoop(idOrName)) }))
|
|
10578
|
+
handler: ({ idOrName }) => withStore(async (store) => ({ loop: publicLoop(await store.unarchiveLoop(idOrName)) }))
|
|
9098
10579
|
},
|
|
9099
10580
|
{
|
|
9100
10581
|
name: "loops_create_command",
|
|
@@ -9110,21 +10591,21 @@ var TOOL_REGISTRATIONS = [
|
|
|
9110
10591
|
cwd: z2.string().optional().describe("Working directory for the command."),
|
|
9111
10592
|
shell: z2.never().optional().describe("Not supported over MCP: shell execution is rejected. Use 'command' plus 'args' instead.")
|
|
9112
10593
|
},
|
|
9113
|
-
handler: (input) =>
|
|
10594
|
+
handler: (input) => {
|
|
9114
10595
|
const timeoutMs = input.timeoutMs;
|
|
9115
|
-
const
|
|
10596
|
+
const createInput = commonCreateInput({
|
|
9116
10597
|
...input,
|
|
9117
10598
|
target: {
|
|
9118
10599
|
type: "command",
|
|
9119
|
-
command:
|
|
10600
|
+
command: nonEmpty2(input.command, "command"),
|
|
9120
10601
|
args: input.args,
|
|
9121
10602
|
cwd: input.cwd,
|
|
9122
10603
|
shell: false,
|
|
9123
10604
|
timeoutMs
|
|
9124
10605
|
}
|
|
9125
|
-
})
|
|
9126
|
-
return { loop: publicLoop(
|
|
9127
|
-
}
|
|
10606
|
+
});
|
|
10607
|
+
return withStore(async (store) => ({ loop: publicLoop(await store.createLoop(createInput)) }));
|
|
10608
|
+
}
|
|
9128
10609
|
},
|
|
9129
10610
|
{
|
|
9130
10611
|
name: "loops_create_workflow",
|
|
@@ -9138,20 +10619,17 @@ var TOOL_REGISTRATIONS = [
|
|
|
9138
10619
|
workflow: workflowIdOrNameSchema,
|
|
9139
10620
|
workflowInput: z2.record(z2.string(), z2.string()).optional().describe("String key/value input passed to the workflow on each run.")
|
|
9140
10621
|
},
|
|
9141
|
-
handler: (input) =>
|
|
9142
|
-
const workflow = store.requireWorkflow(input.workflow);
|
|
10622
|
+
handler: (input) => {
|
|
9143
10623
|
const timeoutMs = input.timeoutMs;
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
workflowId:
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
return { workflow: publicWorkflow(workflow), loop: publicLoop(loop) };
|
|
9154
|
-
})
|
|
10624
|
+
return withStore(async (store) => {
|
|
10625
|
+
const wf = await store.requireWorkflow(input.workflow);
|
|
10626
|
+
const createInput = commonCreateInput({
|
|
10627
|
+
...input,
|
|
10628
|
+
target: { type: "workflow", workflowId: wf.id, input: input.workflowInput, timeoutMs }
|
|
10629
|
+
});
|
|
10630
|
+
return { workflow: publicWorkflow(wf), loop: publicLoop(await store.createLoop(createInput)) };
|
|
10631
|
+
});
|
|
10632
|
+
}
|
|
9155
10633
|
}
|
|
9156
10634
|
];
|
|
9157
10635
|
function toolDescription(tool) {
|
|
@@ -9228,8 +10706,20 @@ async function main() {
|
|
|
9228
10706
|
console.log(JSON.stringify(listToolsForCli(), null, 2));
|
|
9229
10707
|
return;
|
|
9230
10708
|
}
|
|
9231
|
-
|
|
9232
|
-
|
|
10709
|
+
if (isStdioMode()) {
|
|
10710
|
+
const server = createLoopsMcpServer();
|
|
10711
|
+
await server.connect(new StdioServerTransport);
|
|
10712
|
+
return;
|
|
10713
|
+
}
|
|
10714
|
+
const handle = await startMcpHttpServer(() => createLoopsMcpServer(), {
|
|
10715
|
+
port: resolveMcpHttpPort()
|
|
10716
|
+
});
|
|
10717
|
+
process.on("SIGINT", () => {
|
|
10718
|
+
handle.close().finally(() => process.exit(0));
|
|
10719
|
+
});
|
|
10720
|
+
process.on("SIGTERM", () => {
|
|
10721
|
+
handle.close().finally(() => process.exit(0));
|
|
10722
|
+
});
|
|
9233
10723
|
}
|
|
9234
10724
|
if (import.meta.main) {
|
|
9235
10725
|
main().catch((error) => {
|