@hasna/loops 0.4.12 → 0.4.14
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 +106 -3
- package/README.md +70 -17
- package/dist/api/index.d.ts +35 -0
- package/dist/api/index.js +750 -10
- package/dist/cli/index.js +1744 -361
- package/dist/cli/ui.d.ts +44 -0
- package/dist/daemon/index.js +948 -212
- package/dist/generated/storage-kit/health.d.ts +19 -0
- package/dist/generated/storage-kit/index.d.ts +7 -0
- package/dist/generated/storage-kit/migrations.d.ts +47 -0
- package/dist/generated/storage-kit/mode.d.ts +47 -0
- package/dist/generated/storage-kit/pool.d.ts +33 -0
- package/dist/generated/storage-kit/query.d.ts +35 -0
- package/dist/generated/storage-kit/tls.d.ts +25 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6513 -4840
- package/dist/lib/health.d.ts +83 -3
- package/dist/lib/mode.d.ts +27 -0
- package/dist/lib/mode.js +69 -2
- package/dist/lib/scheduler.d.ts +5 -2
- package/dist/lib/storage/index.d.ts +1 -0
- package/dist/lib/storage/index.js +1329 -211
- package/dist/lib/storage/pg-executor.d.ts +27 -0
- package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
- package/dist/lib/storage/postgres-loop-storage.d.ts +114 -0
- package/dist/lib/storage/sqlite.js +336 -8
- package/dist/lib/store.d.ts +234 -0
- package/dist/lib/store.js +350 -8
- package/dist/mcp/index.js +1447 -479
- package/dist/runner/index.js +179 -25
- package/dist/sdk/http.d.ts +115 -0
- package/dist/sdk/http.js +145 -0
- package/dist/sdk/index.d.ts +5 -1
- package/dist/sdk/index.js +1106 -296
- package/dist/serve/index.d.ts +4 -0
- package/dist/serve/index.js +7619 -0
- package/dist/types.d.ts +3 -0
- package/docs/CUTOVER-RUNBOOK.md +61 -0
- package/docs/DEPLOYMENT_MODES.md +23 -1
- package/docs/USAGE.md +66 -16
- package/package.json +14 -2
package/dist/runner/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hasna/loops",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.14",
|
|
7
7
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8
8
|
type: "module",
|
|
9
9
|
main: "dist/index.js",
|
|
@@ -12,6 +12,7 @@ var package_default = {
|
|
|
12
12
|
loops: "dist/cli/index.js",
|
|
13
13
|
"loops-daemon": "dist/daemon/index.js",
|
|
14
14
|
"loops-api": "dist/api/index.js",
|
|
15
|
+
"loops-serve": "dist/serve/index.js",
|
|
15
16
|
"loops-runner": "dist/runner/index.js",
|
|
16
17
|
"loops-mcp": "dist/mcp/index.js"
|
|
17
18
|
},
|
|
@@ -24,6 +25,14 @@ var package_default = {
|
|
|
24
25
|
types: "./dist/sdk/index.d.ts",
|
|
25
26
|
import: "./dist/sdk/index.js"
|
|
26
27
|
},
|
|
28
|
+
"./sdk/http": {
|
|
29
|
+
types: "./dist/sdk/http.d.ts",
|
|
30
|
+
import: "./dist/sdk/http.js"
|
|
31
|
+
},
|
|
32
|
+
"./serve": {
|
|
33
|
+
types: "./dist/serve/index.d.ts",
|
|
34
|
+
import: "./dist/serve/index.js"
|
|
35
|
+
},
|
|
27
36
|
"./mcp": {
|
|
28
37
|
types: "./dist/mcp/index.d.ts",
|
|
29
38
|
import: "./dist/mcp/index.js"
|
|
@@ -69,7 +78,7 @@ var package_default = {
|
|
|
69
78
|
"LICENSE"
|
|
70
79
|
],
|
|
71
80
|
scripts: {
|
|
72
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
81
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/serve/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/sdk/http.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/serve/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
73
82
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
74
83
|
typecheck: "tsc --noEmit",
|
|
75
84
|
test: "bun test",
|
|
@@ -104,11 +113,13 @@ var package_default = {
|
|
|
104
113
|
bun: ">=1.0.0"
|
|
105
114
|
},
|
|
106
115
|
dependencies: {
|
|
116
|
+
"@hasna/contracts": "^0.4.1",
|
|
107
117
|
"@hasna/events": "^0.1.9",
|
|
108
118
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
109
119
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
110
120
|
ai: "6.0.204",
|
|
111
121
|
commander: "^13.1.0",
|
|
122
|
+
pg: "^8.13.1",
|
|
112
123
|
zod: "4.4.3"
|
|
113
124
|
},
|
|
114
125
|
optionalDependencies: {
|
|
@@ -116,6 +127,7 @@ var package_default = {
|
|
|
116
127
|
},
|
|
117
128
|
devDependencies: {
|
|
118
129
|
"@types/bun": "latest",
|
|
130
|
+
"@types/pg": "^8.11.10",
|
|
119
131
|
typescript: "^5.7.3"
|
|
120
132
|
},
|
|
121
133
|
publishConfig: {
|
|
@@ -194,6 +206,50 @@ function sourceOfTruthForMode(mode) {
|
|
|
194
206
|
return "self_hosted_control_plane";
|
|
195
207
|
return "cloud_control_plane";
|
|
196
208
|
}
|
|
209
|
+
var ROUTE_ADMISSION_GATES = [
|
|
210
|
+
"max_dispatch",
|
|
211
|
+
"max_active",
|
|
212
|
+
"max_active_per_project",
|
|
213
|
+
"max_active_per_project_group",
|
|
214
|
+
"max_active_scope",
|
|
215
|
+
"max_per_profile"
|
|
216
|
+
];
|
|
217
|
+
function remoteSchedulerBackendForMode(mode, config) {
|
|
218
|
+
if (mode === "local")
|
|
219
|
+
return "none";
|
|
220
|
+
if (mode === "cloud")
|
|
221
|
+
return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
|
|
222
|
+
if (config.databaseUrlPresent)
|
|
223
|
+
return "postgres_contract";
|
|
224
|
+
if (config.apiUrl)
|
|
225
|
+
return "api_control_plane_contract";
|
|
226
|
+
return "unconfigured";
|
|
227
|
+
}
|
|
228
|
+
function schedulerStateForMode(args) {
|
|
229
|
+
const nonLocal = args.deploymentMode !== "local";
|
|
230
|
+
return {
|
|
231
|
+
authority: args.sourceOfTruth,
|
|
232
|
+
localStore: {
|
|
233
|
+
backend: "sqlite",
|
|
234
|
+
role: args.localRole,
|
|
235
|
+
runArtifacts: "local_files",
|
|
236
|
+
routeAdmissionState: "workflow_work_items"
|
|
237
|
+
},
|
|
238
|
+
remoteStore: {
|
|
239
|
+
backend: remoteSchedulerBackendForMode(args.deploymentMode, args.config),
|
|
240
|
+
configured: nonLocal && args.controlPlaneConfigured,
|
|
241
|
+
applySupported: false,
|
|
242
|
+
objectArtifacts: nonLocal ? "object_store_contract" : "none",
|
|
243
|
+
mutatesAws: false
|
|
244
|
+
},
|
|
245
|
+
routeAdmission: {
|
|
246
|
+
stateStore: nonLocal ? "control_plane_contract" : "local_sqlite",
|
|
247
|
+
activeStatuses: ["admitted", "running"],
|
|
248
|
+
gates: ROUTE_ADMISSION_GATES,
|
|
249
|
+
dryRunEvaluatesLiveCounts: false
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
197
253
|
function displayControlPlaneUrl(value) {
|
|
198
254
|
if (!value)
|
|
199
255
|
return;
|
|
@@ -219,6 +275,9 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
219
275
|
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
220
276
|
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
221
277
|
}
|
|
278
|
+
if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
|
|
279
|
+
warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
|
|
280
|
+
}
|
|
222
281
|
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
223
282
|
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
224
283
|
}
|
|
@@ -252,6 +311,13 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
252
311
|
required: deploymentMode !== "local",
|
|
253
312
|
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
254
313
|
},
|
|
314
|
+
schedulerState: schedulerStateForMode({
|
|
315
|
+
deploymentMode,
|
|
316
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
317
|
+
localRole: deploymentMode === "local" ? "authoritative" : "cache_and_spool",
|
|
318
|
+
controlPlaneConfigured,
|
|
319
|
+
config
|
|
320
|
+
}),
|
|
255
321
|
warnings
|
|
256
322
|
};
|
|
257
323
|
}
|
|
@@ -264,6 +330,7 @@ function deploymentStatusLine(status) {
|
|
|
264
330
|
`source=${status.deploymentModeSource}`,
|
|
265
331
|
`truth=${status.sourceOfTruth}`,
|
|
266
332
|
`local=${status.localStore.role}`,
|
|
333
|
+
`scheduler=${status.schedulerState.routeAdmission.stateStore}`,
|
|
267
334
|
`control_plane=${configured}`
|
|
268
335
|
].join(" ");
|
|
269
336
|
}
|
|
@@ -1063,6 +1130,32 @@ function timeoutResult(startedAt, error, fields = {}) {
|
|
|
1063
1130
|
function successResult(startedAt, fields = {}) {
|
|
1064
1131
|
return buildResult("succeeded", startedAt, fields);
|
|
1065
1132
|
}
|
|
1133
|
+
function codewithJsonlHasTerminalSuccess(stdout) {
|
|
1134
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1135
|
+
const trimmed = line.trim();
|
|
1136
|
+
if (!trimmed || !trimmed.startsWith("{"))
|
|
1137
|
+
continue;
|
|
1138
|
+
let event;
|
|
1139
|
+
try {
|
|
1140
|
+
event = JSON.parse(trimmed);
|
|
1141
|
+
} catch {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (!event || typeof event !== "object")
|
|
1145
|
+
continue;
|
|
1146
|
+
const record = event;
|
|
1147
|
+
if (record.type === "task_complete")
|
|
1148
|
+
return true;
|
|
1149
|
+
const payload = record.payload;
|
|
1150
|
+
if (payload && typeof payload === "object" && payload.type === "task_complete") {
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
function codewithJsonlReconciledSuccess(spec, fields) {
|
|
1157
|
+
return spec.agentProvider === "codewith" && codewithJsonlHasTerminalSuccess(fields.stdout ?? "");
|
|
1158
|
+
}
|
|
1066
1159
|
function notifySpawn(pid, opts) {
|
|
1067
1160
|
if (!pid)
|
|
1068
1161
|
return;
|
|
@@ -1078,10 +1171,48 @@ function shellQuote(value) {
|
|
|
1078
1171
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1079
1172
|
}
|
|
1080
1173
|
function codewithProfileCandidateFromLine(line) {
|
|
1081
|
-
const
|
|
1082
|
-
if (
|
|
1083
|
-
return
|
|
1084
|
-
|
|
1174
|
+
const trimmed = line.trim();
|
|
1175
|
+
if (!trimmed || trimmed === "No auth profiles saved.")
|
|
1176
|
+
return;
|
|
1177
|
+
const cols = trimmed.split(/\s+/);
|
|
1178
|
+
const candidate = cols[0] === "*" ? cols[1] : cols[0];
|
|
1179
|
+
if (candidate === "NAME" && (cols[1] === "ACCOUNT" || cols[2] === "ACCOUNT"))
|
|
1180
|
+
return;
|
|
1181
|
+
if (candidate === '"name":') {
|
|
1182
|
+
const jsonName = trimmed.match(/"name"\s*:\s*"([^"]+)"/)?.[1];
|
|
1183
|
+
if (jsonName)
|
|
1184
|
+
return jsonName;
|
|
1185
|
+
}
|
|
1186
|
+
return candidate;
|
|
1187
|
+
}
|
|
1188
|
+
function codewithProfileCandidatesFromJson(value) {
|
|
1189
|
+
const candidates = [];
|
|
1190
|
+
const root = value && typeof value === "object" ? value : undefined;
|
|
1191
|
+
const addProfile = (entry) => {
|
|
1192
|
+
if (!entry || typeof entry !== "object")
|
|
1193
|
+
return;
|
|
1194
|
+
const name = entry.name;
|
|
1195
|
+
if (typeof name === "string" && name)
|
|
1196
|
+
candidates.push(name);
|
|
1197
|
+
};
|
|
1198
|
+
const data = root?.data;
|
|
1199
|
+
if (Array.isArray(data))
|
|
1200
|
+
data.forEach(addProfile);
|
|
1201
|
+
const profiles = root?.profiles;
|
|
1202
|
+
if (Array.isArray(profiles))
|
|
1203
|
+
profiles.forEach(addProfile);
|
|
1204
|
+
return candidates;
|
|
1205
|
+
}
|
|
1206
|
+
function codewithProfileCandidatesFromOutput(output) {
|
|
1207
|
+
const trimmed = output.trim();
|
|
1208
|
+
const jsonStart = trimmed.indexOf("{");
|
|
1209
|
+
const jsonEnd = trimmed.lastIndexOf("}");
|
|
1210
|
+
if (jsonStart >= 0 && jsonEnd >= jsonStart) {
|
|
1211
|
+
try {
|
|
1212
|
+
return codewithProfileCandidatesFromJson(JSON.parse(trimmed.slice(jsonStart, jsonEnd + 1)));
|
|
1213
|
+
} catch {}
|
|
1214
|
+
}
|
|
1215
|
+
return output.split(/\r?\n/).map(codewithProfileCandidateFromLine).filter(Boolean);
|
|
1085
1216
|
}
|
|
1086
1217
|
function codewithProfileForError(profile) {
|
|
1087
1218
|
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
@@ -1174,6 +1305,7 @@ function commandSpec(target, opts) {
|
|
|
1174
1305
|
preflightAnyOf: invocation.preflightAnyOf,
|
|
1175
1306
|
stdin: invocation.stdin,
|
|
1176
1307
|
allowlist: agentTarget.allowlist,
|
|
1308
|
+
agentProvider: agentTarget.provider,
|
|
1177
1309
|
worktree: agentTarget.worktree
|
|
1178
1310
|
};
|
|
1179
1311
|
}
|
|
@@ -1333,7 +1465,7 @@ function remotePreflightScript(spec, metadata) {
|
|
|
1333
1465
|
}
|
|
1334
1466
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
1335
1467
|
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
1336
|
-
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE",
|
|
1468
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", "__openloops_codewith_profile_list_contains() {", ` printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk '{ line = $0; gsub(/^[[:space:]]+|[[:space:]]+$/, "", line); if (line == "" || line == "No auth profiles saved.") next; if (line ~ /"(data|profiles)"[[:space:]]*:[[:space:]]*\\[/) { in_profiles = 1; next } if (in_profiles && line ~ /^\\]/) { in_profiles = 0; next } json = line; if (in_profiles && json ~ /"name"[[:space:]]*:[[:space:]]*"/) { sub(/^.*"name"[[:space:]]*:[[:space:]]*"/, "", json); sub(/".*$/, "", json); if (json == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1; next } split(line, cols, /[[:space:]]+/); candidate = (cols[1] == "*" ? cols[2] : cols[1]); if (candidate == "NAME" && (cols[2] == "ACCOUNT" || cols[3] == "ACCOUNT")) next; if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'`, "}", `if __OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list --json 2>/dev/null)" && __openloops_codewith_profile_list_contains; then`, " :", "else", ` __OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", " }", " if ! __openloops_codewith_profile_list_contains; then", ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", " fi", "fi");
|
|
1337
1469
|
}
|
|
1338
1470
|
return lines.join(`
|
|
1339
1471
|
`);
|
|
@@ -1351,6 +1483,12 @@ function transportEnv(opts) {
|
|
|
1351
1483
|
return env;
|
|
1352
1484
|
}
|
|
1353
1485
|
function assertCodewithProfileListed(profile, result) {
|
|
1486
|
+
const profiles = codewithProfileSetFromResult(result);
|
|
1487
|
+
if (!profiles.has(profile)) {
|
|
1488
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function codewithProfileSetFromResult(result) {
|
|
1354
1492
|
if (result.error) {
|
|
1355
1493
|
throw new Error(`codewith auth profile preflight failed: ${result.error}`);
|
|
1356
1494
|
}
|
|
@@ -1358,32 +1496,42 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
1358
1496
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
1359
1497
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
1360
1498
|
}
|
|
1361
|
-
|
|
1362
|
-
if (!profiles.has(profile)) {
|
|
1363
|
-
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
1364
|
-
}
|
|
1499
|
+
return new Set(codewithProfileCandidatesFromOutput(result.stdout || ""));
|
|
1365
1500
|
}
|
|
1366
1501
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
1367
1502
|
if (spec.nativeAuthProfile?.provider !== "codewith")
|
|
1368
1503
|
return;
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1504
|
+
const runProfileList = (args) => {
|
|
1505
|
+
const result = spawnSync3(spec.command, args, {
|
|
1506
|
+
encoding: "utf8",
|
|
1507
|
+
env,
|
|
1508
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1509
|
+
timeout: 15000
|
|
1510
|
+
});
|
|
1511
|
+
return {
|
|
1512
|
+
status: result.status,
|
|
1513
|
+
stdout: result.stdout ?? "",
|
|
1514
|
+
stderr: result.stderr ?? "",
|
|
1515
|
+
error: result.error?.message
|
|
1516
|
+
};
|
|
1517
|
+
};
|
|
1518
|
+
const jsonResult = runProfileList(["profile", "list", "--json"]);
|
|
1519
|
+
try {
|
|
1520
|
+
if (codewithProfileSetFromResult(jsonResult).has(spec.nativeAuthProfile.profile))
|
|
1521
|
+
return;
|
|
1522
|
+
} catch {}
|
|
1523
|
+
assertCodewithProfileListed(spec.nativeAuthProfile.profile, runProfileList(["profile", "list"]));
|
|
1381
1524
|
}
|
|
1382
1525
|
async function preflightNativeAuthProfile(spec, env) {
|
|
1383
1526
|
if (spec.nativeAuthProfile?.provider !== "codewith")
|
|
1384
1527
|
return;
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1528
|
+
const jsonResult = await spawnCapture(spec.command, ["profile", "list", "--json"], { env, timeoutMs: 15000 });
|
|
1529
|
+
try {
|
|
1530
|
+
if (codewithProfileSetFromResult(jsonResult).has(spec.nativeAuthProfile.profile))
|
|
1531
|
+
return;
|
|
1532
|
+
} catch {}
|
|
1533
|
+
const tableResult = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
|
|
1534
|
+
assertCodewithProfileListed(spec.nativeAuthProfile.profile, tableResult);
|
|
1387
1535
|
}
|
|
1388
1536
|
function resolvedDirEquals(left, right) {
|
|
1389
1537
|
try {
|
|
@@ -1585,6 +1733,9 @@ async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
|
|
|
1585
1733
|
if (timedOut || idleTimedOut) {
|
|
1586
1734
|
return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
|
|
1587
1735
|
}
|
|
1736
|
+
if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
|
|
1737
|
+
return successResult(startedAt, fields);
|
|
1738
|
+
}
|
|
1588
1739
|
if (error || exitCode !== 0) {
|
|
1589
1740
|
return failureResult(startedAt, error ?? `remote process on ${machine.id} exited with code ${exitCode ?? "unknown"}`, fields);
|
|
1590
1741
|
}
|
|
@@ -1720,6 +1871,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1720
1871
|
if (timedOut || idleTimedOut) {
|
|
1721
1872
|
return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
|
|
1722
1873
|
}
|
|
1874
|
+
if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
|
|
1875
|
+
return successResult(startedAt, fields);
|
|
1876
|
+
}
|
|
1723
1877
|
if (error || exitCode !== 0) {
|
|
1724
1878
|
return failureResult(startedAt, error ?? `process exited with code ${exitCode ?? "unknown"}`, fields);
|
|
1725
1879
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface Foundation {
|
|
2
|
+
"status": string;
|
|
3
|
+
"version": string;
|
|
4
|
+
"mode": string;
|
|
5
|
+
"service"?: string;
|
|
6
|
+
"detail"?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Loop {
|
|
9
|
+
"id": string;
|
|
10
|
+
"name": string;
|
|
11
|
+
"description"?: string | null;
|
|
12
|
+
"status": string;
|
|
13
|
+
"schedule"?: Record<string, unknown>;
|
|
14
|
+
"target"?: Record<string, unknown>;
|
|
15
|
+
"nextRunAt"?: string | null;
|
|
16
|
+
"createdAt"?: string;
|
|
17
|
+
"updatedAt"?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CreateLoopInput {
|
|
20
|
+
"name": string;
|
|
21
|
+
"description"?: string;
|
|
22
|
+
"schedule": Record<string, unknown>;
|
|
23
|
+
"target": Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface UpdateLoopInput {
|
|
26
|
+
"status"?: "active" | "paused" | "stopped" | "expired";
|
|
27
|
+
"nextRunAt"?: string | null;
|
|
28
|
+
"retryScheduledFor"?: string | null;
|
|
29
|
+
"expiresAt"?: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface Run {
|
|
32
|
+
"id": string;
|
|
33
|
+
"loopId": string;
|
|
34
|
+
"status": string;
|
|
35
|
+
"attempt"?: number;
|
|
36
|
+
"scheduledFor"?: string;
|
|
37
|
+
"startedAt"?: string | null;
|
|
38
|
+
"finishedAt"?: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface LoopResponse {
|
|
41
|
+
"ok": boolean;
|
|
42
|
+
"loop": Loop;
|
|
43
|
+
}
|
|
44
|
+
export interface LoopListResponse {
|
|
45
|
+
"ok": boolean;
|
|
46
|
+
"loops": Array<Loop>;
|
|
47
|
+
}
|
|
48
|
+
export interface RunResponse {
|
|
49
|
+
"ok": boolean;
|
|
50
|
+
"run": Run;
|
|
51
|
+
}
|
|
52
|
+
export interface RunListResponse {
|
|
53
|
+
"ok": boolean;
|
|
54
|
+
"runs": Array<Run>;
|
|
55
|
+
}
|
|
56
|
+
export interface DeleteResponse {
|
|
57
|
+
"ok": boolean;
|
|
58
|
+
"deleted": boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface LoopsClientOptions {
|
|
61
|
+
/** Base URL, e.g. process.env.APP_API_URL. */
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
/** API key, e.g. process.env.APP_API_KEY. Sent as the 'x-api-key' header. */
|
|
64
|
+
apiKey?: string;
|
|
65
|
+
/** Custom fetch (defaults to global fetch). */
|
|
66
|
+
fetch?: typeof fetch;
|
|
67
|
+
/** Extra headers merged into every request. */
|
|
68
|
+
headers?: Record<string, string>;
|
|
69
|
+
}
|
|
70
|
+
export declare class ApiError extends Error {
|
|
71
|
+
readonly status: number;
|
|
72
|
+
readonly body: unknown;
|
|
73
|
+
constructor(status: number, message: string, body: unknown);
|
|
74
|
+
}
|
|
75
|
+
export declare class LoopsClient {
|
|
76
|
+
private readonly baseUrl;
|
|
77
|
+
private readonly apiKey;
|
|
78
|
+
private readonly fetchImpl;
|
|
79
|
+
private readonly baseHeaders;
|
|
80
|
+
constructor(options: LoopsClientOptions);
|
|
81
|
+
private request;
|
|
82
|
+
/** Liveness probe */
|
|
83
|
+
healthCheck(init?: RequestInit): Promise<Foundation>;
|
|
84
|
+
/** Readiness probe (storage reachable + migrated) */
|
|
85
|
+
readyCheck(init?: RequestInit): Promise<Foundation>;
|
|
86
|
+
/** List loops */
|
|
87
|
+
listLoops(query?: {
|
|
88
|
+
"status"?: "active" | "paused" | "stopped" | "expired";
|
|
89
|
+
"limit"?: number;
|
|
90
|
+
"includeArchived"?: boolean;
|
|
91
|
+
"archived"?: boolean;
|
|
92
|
+
}, init?: RequestInit): Promise<LoopListResponse>;
|
|
93
|
+
/** Create a loop */
|
|
94
|
+
createLoop(body: CreateLoopInput, init?: RequestInit): Promise<LoopResponse>;
|
|
95
|
+
/** Get a loop by id */
|
|
96
|
+
getLoop(id: string, init?: RequestInit): Promise<LoopResponse>;
|
|
97
|
+
/** Delete a loop */
|
|
98
|
+
deleteLoop(id: string, init?: RequestInit): Promise<DeleteResponse>;
|
|
99
|
+
/** Update a loop (status / schedule fields) */
|
|
100
|
+
updateLoop(id: string, body: UpdateLoopInput, init?: RequestInit): Promise<LoopResponse>;
|
|
101
|
+
/** Archive a loop */
|
|
102
|
+
archiveLoop(id: string, init?: RequestInit): Promise<LoopResponse>;
|
|
103
|
+
/** Unarchive a loop */
|
|
104
|
+
unarchiveLoop(id: string, init?: RequestInit): Promise<LoopResponse>;
|
|
105
|
+
/** List runs */
|
|
106
|
+
listRuns(query?: {
|
|
107
|
+
"loopId"?: string;
|
|
108
|
+
"status"?: string;
|
|
109
|
+
"limit"?: number;
|
|
110
|
+
}, init?: RequestInit): Promise<RunListResponse>;
|
|
111
|
+
/** Get a run by id */
|
|
112
|
+
getRun(id: string, init?: RequestInit): Promise<RunResponse>;
|
|
113
|
+
/** Service version */
|
|
114
|
+
getVersion(init?: RequestInit): Promise<Foundation>;
|
|
115
|
+
}
|
package/dist/sdk/http.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/sdk/http.ts
|
|
3
|
+
class ApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
body;
|
|
6
|
+
constructor(status, message, body) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
this.name = "ApiError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class LoopsClient {
|
|
15
|
+
baseUrl;
|
|
16
|
+
apiKey;
|
|
17
|
+
fetchImpl;
|
|
18
|
+
baseHeaders;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
if (!options.baseUrl)
|
|
21
|
+
throw new Error("LoopsClient requires a baseUrl.");
|
|
22
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
23
|
+
this.apiKey = options.apiKey;
|
|
24
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
25
|
+
this.baseHeaders = options.headers ?? {};
|
|
26
|
+
}
|
|
27
|
+
async request(method, path, opts) {
|
|
28
|
+
const url = new URL(this.baseUrl + path);
|
|
29
|
+
if (opts.query) {
|
|
30
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
31
|
+
if (value !== undefined && value !== null)
|
|
32
|
+
url.searchParams.set(key, String(value));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
|
|
36
|
+
if (this.apiKey)
|
|
37
|
+
headers["x-api-key"] = this.apiKey;
|
|
38
|
+
let payload;
|
|
39
|
+
if (opts.body !== undefined) {
|
|
40
|
+
headers["Content-Type"] = "application/json";
|
|
41
|
+
payload = JSON.stringify(opts.body);
|
|
42
|
+
}
|
|
43
|
+
const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
|
|
44
|
+
const text = await response.text();
|
|
45
|
+
const data = text ? (() => {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
} catch {
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
})() : undefined;
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
async healthCheck(init) {
|
|
58
|
+
return this.request("GET", `/health`, {
|
|
59
|
+
body: undefined,
|
|
60
|
+
query: undefined,
|
|
61
|
+
init
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async readyCheck(init) {
|
|
65
|
+
return this.request("GET", `/ready`, {
|
|
66
|
+
body: undefined,
|
|
67
|
+
query: undefined,
|
|
68
|
+
init
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async listLoops(query, init) {
|
|
72
|
+
return this.request("GET", `/v1/loops`, {
|
|
73
|
+
body: undefined,
|
|
74
|
+
query,
|
|
75
|
+
init
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async createLoop(body, init) {
|
|
79
|
+
return this.request("POST", `/v1/loops`, {
|
|
80
|
+
body,
|
|
81
|
+
query: undefined,
|
|
82
|
+
init
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async getLoop(id, init) {
|
|
86
|
+
return this.request("GET", `/v1/loops/${encodeURIComponent(String(id))}`, {
|
|
87
|
+
body: undefined,
|
|
88
|
+
query: undefined,
|
|
89
|
+
init
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async deleteLoop(id, init) {
|
|
93
|
+
return this.request("DELETE", `/v1/loops/${encodeURIComponent(String(id))}`, {
|
|
94
|
+
body: undefined,
|
|
95
|
+
query: undefined,
|
|
96
|
+
init
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async updateLoop(id, body, init) {
|
|
100
|
+
return this.request("PATCH", `/v1/loops/${encodeURIComponent(String(id))}`, {
|
|
101
|
+
body,
|
|
102
|
+
query: undefined,
|
|
103
|
+
init
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async archiveLoop(id, init) {
|
|
107
|
+
return this.request("POST", `/v1/loops/${encodeURIComponent(String(id))}/archive`, {
|
|
108
|
+
body: undefined,
|
|
109
|
+
query: undefined,
|
|
110
|
+
init
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async unarchiveLoop(id, init) {
|
|
114
|
+
return this.request("POST", `/v1/loops/${encodeURIComponent(String(id))}/unarchive`, {
|
|
115
|
+
body: undefined,
|
|
116
|
+
query: undefined,
|
|
117
|
+
init
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async listRuns(query, init) {
|
|
121
|
+
return this.request("GET", `/v1/runs`, {
|
|
122
|
+
body: undefined,
|
|
123
|
+
query,
|
|
124
|
+
init
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async getRun(id, init) {
|
|
128
|
+
return this.request("GET", `/v1/runs/${encodeURIComponent(String(id))}`, {
|
|
129
|
+
body: undefined,
|
|
130
|
+
query: undefined,
|
|
131
|
+
init
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async getVersion(init) {
|
|
135
|
+
return this.request("GET", `/version`, {
|
|
136
|
+
body: undefined,
|
|
137
|
+
query: undefined,
|
|
138
|
+
init
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
LoopsClient,
|
|
144
|
+
ApiError
|
|
145
|
+
};
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CreateLoopInput, Goal, GoalRun, Loop, LoopRun, LoopStatus, OpenAutomationsRuntimeBinding, RunStatus } from "../types.js";
|
|
2
2
|
import { type DoctorReport } from "../lib/doctor.js";
|
|
3
|
-
import { type LoopsHealthReport } from "../lib/health.js";
|
|
3
|
+
import { type BuildHealthScanOptions, type LoopsHealthReport, type LoopsHealthScan } from "../lib/health.js";
|
|
4
4
|
import { type ApplyLoopsMigrationResult, type ExportLoopsMigrationOptions, type ImportLoopsMigrationOptions, type LoopsMigrationBundle, type LoopsMigrationPlan, type SelfHostedPlanOptions } from "../lib/migration.js";
|
|
5
5
|
import { tick } from "../lib/scheduler.js";
|
|
6
6
|
import { Store } from "../lib/store.js";
|
|
@@ -50,6 +50,10 @@ export declare class LoopsClient {
|
|
|
50
50
|
includeInactive?: boolean;
|
|
51
51
|
limit?: number;
|
|
52
52
|
}): LoopsHealthReport;
|
|
53
|
+
healthScan(opts?: Omit<BuildHealthScanOptions, "doctor" | "daemon" | "selfHeals"> & {
|
|
54
|
+
doctor?: boolean;
|
|
55
|
+
daemon?: boolean;
|
|
56
|
+
}): LoopsHealthScan;
|
|
53
57
|
goal(idOrName: string): {
|
|
54
58
|
goal?: Goal;
|
|
55
59
|
runs: GoalRun[];
|