@hasna/loops 0.4.13 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +74 -3
  2. package/README.md +54 -12
  3. package/dist/api/index.d.ts +35 -0
  4. package/dist/api/index.js +695 -10
  5. package/dist/cli/index.js +1633 -366
  6. package/dist/cli/ui.d.ts +44 -0
  7. package/dist/daemon/index.js +948 -212
  8. package/dist/generated/storage-kit/health.d.ts +19 -0
  9. package/dist/generated/storage-kit/index.d.ts +7 -0
  10. package/dist/generated/storage-kit/migrations.d.ts +47 -0
  11. package/dist/generated/storage-kit/mode.d.ts +47 -0
  12. package/dist/generated/storage-kit/pool.d.ts +33 -0
  13. package/dist/generated/storage-kit/query.d.ts +35 -0
  14. package/dist/generated/storage-kit/tls.d.ts +25 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +3567 -2010
  17. package/dist/lib/health.d.ts +83 -3
  18. package/dist/lib/mode.js +14 -2
  19. package/dist/lib/scheduler.d.ts +5 -2
  20. package/dist/lib/storage/index.d.ts +1 -0
  21. package/dist/lib/storage/index.js +1329 -211
  22. package/dist/lib/storage/pg-executor.d.ts +27 -0
  23. package/dist/lib/storage/pg-runner-claim.d.ts +40 -0
  24. package/dist/lib/storage/postgres-loop-storage.d.ts +114 -0
  25. package/dist/lib/storage/sqlite.js +336 -8
  26. package/dist/lib/store.d.ts +234 -0
  27. package/dist/lib/store.js +350 -8
  28. package/dist/mcp/index.js +1067 -306
  29. package/dist/runner/index.js +124 -25
  30. package/dist/sdk/http.d.ts +115 -0
  31. package/dist/sdk/http.js +145 -0
  32. package/dist/sdk/index.d.ts +5 -1
  33. package/dist/sdk/index.js +1052 -312
  34. package/dist/serve/index.d.ts +4 -0
  35. package/dist/serve/index.js +7619 -0
  36. package/dist/types.d.ts +3 -0
  37. package/docs/CUTOVER-RUNBOOK.md +61 -0
  38. package/docs/USAGE.md +50 -11
  39. package/package.json +14 -2
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hasna/loops",
6
- version: "0.4.13",
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: {
@@ -1118,6 +1130,32 @@ function timeoutResult(startedAt, error, fields = {}) {
1118
1130
  function successResult(startedAt, fields = {}) {
1119
1131
  return buildResult("succeeded", startedAt, fields);
1120
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
+ }
1121
1159
  function notifySpawn(pid, opts) {
1122
1160
  if (!pid)
1123
1161
  return;
@@ -1133,10 +1171,48 @@ function shellQuote(value) {
1133
1171
  return `'${value.replace(/'/g, `'\\''`)}'`;
1134
1172
  }
1135
1173
  function codewithProfileCandidateFromLine(line) {
1136
- const cols = line.trim().split(/\s+/);
1137
- if (cols[0] === "*")
1138
- return cols[1];
1139
- return cols[0];
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);
1140
1216
  }
1141
1217
  function codewithProfileForError(profile) {
1142
1218
  return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
@@ -1229,6 +1305,7 @@ function commandSpec(target, opts) {
1229
1305
  preflightAnyOf: invocation.preflightAnyOf,
1230
1306
  stdin: invocation.stdin,
1231
1307
  allowlist: agentTarget.allowlist,
1308
+ agentProvider: agentTarget.provider,
1232
1309
  worktree: agentTarget.worktree
1233
1310
  };
1234
1311
  }
@@ -1388,7 +1465,7 @@ function remotePreflightScript(spec, metadata) {
1388
1465
  }
1389
1466
  if (spec.nativeAuthProfile?.provider === "codewith") {
1390
1467
  const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
1391
- lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", `__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { candidate = ($1 == "*" ? $2 : $1); if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", "fi");
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");
1392
1469
  }
1393
1470
  return lines.join(`
1394
1471
  `);
@@ -1406,6 +1483,12 @@ function transportEnv(opts) {
1406
1483
  return env;
1407
1484
  }
1408
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) {
1409
1492
  if (result.error) {
1410
1493
  throw new Error(`codewith auth profile preflight failed: ${result.error}`);
1411
1494
  }
@@ -1413,32 +1496,42 @@ function assertCodewithProfileListed(profile, result) {
1413
1496
  const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
1414
1497
  throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
1415
1498
  }
1416
- const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
1417
- if (!profiles.has(profile)) {
1418
- throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
1419
- }
1499
+ return new Set(codewithProfileCandidatesFromOutput(result.stdout || ""));
1420
1500
  }
1421
1501
  function preflightNativeAuthProfileSync(spec, env) {
1422
1502
  if (spec.nativeAuthProfile?.provider !== "codewith")
1423
1503
  return;
1424
- const result = spawnSync3(spec.command, ["profile", "list"], {
1425
- encoding: "utf8",
1426
- env,
1427
- stdio: ["ignore", "pipe", "pipe"],
1428
- timeout: 15000
1429
- });
1430
- assertCodewithProfileListed(spec.nativeAuthProfile.profile, {
1431
- status: result.status,
1432
- stdout: result.stdout ?? "",
1433
- stderr: result.stderr ?? "",
1434
- error: result.error?.message
1435
- });
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"]));
1436
1524
  }
1437
1525
  async function preflightNativeAuthProfile(spec, env) {
1438
1526
  if (spec.nativeAuthProfile?.provider !== "codewith")
1439
1527
  return;
1440
- const result = await spawnCapture(spec.command, ["profile", "list"], { env, timeoutMs: 15000 });
1441
- assertCodewithProfileListed(spec.nativeAuthProfile.profile, result);
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);
1442
1535
  }
1443
1536
  function resolvedDirEquals(left, right) {
1444
1537
  try {
@@ -1640,6 +1733,9 @@ async function executeRemoteSpec(spec, machine, metadata, opts, fallbackSpec) {
1640
1733
  if (timedOut || idleTimedOut) {
1641
1734
  return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
1642
1735
  }
1736
+ if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
1737
+ return successResult(startedAt, fields);
1738
+ }
1643
1739
  if (error || exitCode !== 0) {
1644
1740
  return failureResult(startedAt, error ?? `remote process on ${machine.id} exited with code ${exitCode ?? "unknown"}`, fields);
1645
1741
  }
@@ -1775,6 +1871,9 @@ async function executeTarget(target, metadata = {}, opts = {}) {
1775
1871
  if (timedOut || idleTimedOut) {
1776
1872
  return timeoutResult(startedAt, idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`, fields);
1777
1873
  }
1874
+ if (!error && exitCode !== 0 && codewithJsonlReconciledSuccess(spec, fields)) {
1875
+ return successResult(startedAt, fields);
1876
+ }
1778
1877
  if (error || exitCode !== 0) {
1779
1878
  return failureResult(startedAt, error ?? `process exited with code ${exitCode ?? "unknown"}`, fields);
1780
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
+ }
@@ -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
+ };
@@ -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[];