@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
package/dist/api/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.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: {
@@ -815,8 +827,639 @@ function scrubSecretsDeep(value) {
815
827
  }
816
828
  return value;
817
829
  }
830
+ // openapi/loops.json
831
+ var loops_default = {
832
+ openapi: "3.1.0",
833
+ info: {
834
+ title: "OpenLoops",
835
+ version: "0.4.14",
836
+ description: "OpenLoops self-hosted control-plane HTTP API (loops-serve). RDS-direct (Amendment A1). All /v1 routes require an API key (x-api-key or Authorization: Bearer) with a loops:* scope."
837
+ },
838
+ servers: [
839
+ {
840
+ url: "https://loops.hasna.xyz"
841
+ }
842
+ ],
843
+ paths: {
844
+ "/health": {
845
+ get: {
846
+ operationId: "healthCheck",
847
+ summary: "Liveness probe",
848
+ responses: {
849
+ "200": {
850
+ description: "alive",
851
+ content: {
852
+ "application/json": {
853
+ schema: {
854
+ $ref: "#/components/schemas/Foundation"
855
+ }
856
+ }
857
+ }
858
+ }
859
+ }
860
+ }
861
+ },
862
+ "/ready": {
863
+ get: {
864
+ operationId: "readyCheck",
865
+ summary: "Readiness probe (storage reachable + migrated)",
866
+ responses: {
867
+ "200": {
868
+ description: "ready",
869
+ content: {
870
+ "application/json": {
871
+ schema: {
872
+ $ref: "#/components/schemas/Foundation"
873
+ }
874
+ }
875
+ }
876
+ },
877
+ "503": {
878
+ description: "not ready",
879
+ content: {
880
+ "application/json": {
881
+ schema: {
882
+ $ref: "#/components/schemas/Foundation"
883
+ }
884
+ }
885
+ }
886
+ }
887
+ }
888
+ }
889
+ },
890
+ "/version": {
891
+ get: {
892
+ operationId: "getVersion",
893
+ summary: "Service version",
894
+ responses: {
895
+ "200": {
896
+ description: "version",
897
+ content: {
898
+ "application/json": {
899
+ schema: {
900
+ $ref: "#/components/schemas/Foundation"
901
+ }
902
+ }
903
+ }
904
+ }
905
+ }
906
+ }
907
+ },
908
+ "/v1/loops": {
909
+ get: {
910
+ operationId: "listLoops",
911
+ summary: "List loops",
912
+ parameters: [
913
+ {
914
+ name: "status",
915
+ in: "query",
916
+ required: false,
917
+ schema: {
918
+ type: "string",
919
+ enum: [
920
+ "active",
921
+ "paused",
922
+ "stopped",
923
+ "expired"
924
+ ]
925
+ }
926
+ },
927
+ {
928
+ name: "limit",
929
+ in: "query",
930
+ required: false,
931
+ schema: {
932
+ type: "integer"
933
+ }
934
+ },
935
+ {
936
+ name: "includeArchived",
937
+ in: "query",
938
+ required: false,
939
+ schema: {
940
+ type: "boolean"
941
+ }
942
+ },
943
+ {
944
+ name: "archived",
945
+ in: "query",
946
+ required: false,
947
+ schema: {
948
+ type: "boolean"
949
+ }
950
+ }
951
+ ],
952
+ responses: {
953
+ "200": {
954
+ description: "loops",
955
+ content: {
956
+ "application/json": {
957
+ schema: {
958
+ $ref: "#/components/schemas/LoopListResponse"
959
+ }
960
+ }
961
+ }
962
+ }
963
+ }
964
+ },
965
+ post: {
966
+ operationId: "createLoop",
967
+ summary: "Create a loop",
968
+ requestBody: {
969
+ required: true,
970
+ content: {
971
+ "application/json": {
972
+ schema: {
973
+ $ref: "#/components/schemas/CreateLoopInput"
974
+ }
975
+ }
976
+ }
977
+ },
978
+ responses: {
979
+ "201": {
980
+ description: "created",
981
+ content: {
982
+ "application/json": {
983
+ schema: {
984
+ $ref: "#/components/schemas/LoopResponse"
985
+ }
986
+ }
987
+ }
988
+ }
989
+ }
990
+ }
991
+ },
992
+ "/v1/loops/{id}": {
993
+ get: {
994
+ operationId: "getLoop",
995
+ summary: "Get a loop by id",
996
+ parameters: [
997
+ {
998
+ name: "id",
999
+ in: "path",
1000
+ required: true,
1001
+ schema: {
1002
+ type: "string"
1003
+ }
1004
+ }
1005
+ ],
1006
+ responses: {
1007
+ "200": {
1008
+ description: "loop",
1009
+ content: {
1010
+ "application/json": {
1011
+ schema: {
1012
+ $ref: "#/components/schemas/LoopResponse"
1013
+ }
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+ },
1019
+ patch: {
1020
+ operationId: "updateLoop",
1021
+ summary: "Update a loop (status / schedule fields)",
1022
+ parameters: [
1023
+ {
1024
+ name: "id",
1025
+ in: "path",
1026
+ required: true,
1027
+ schema: {
1028
+ type: "string"
1029
+ }
1030
+ }
1031
+ ],
1032
+ requestBody: {
1033
+ required: true,
1034
+ content: {
1035
+ "application/json": {
1036
+ schema: {
1037
+ $ref: "#/components/schemas/UpdateLoopInput"
1038
+ }
1039
+ }
1040
+ }
1041
+ },
1042
+ responses: {
1043
+ "200": {
1044
+ description: "loop",
1045
+ content: {
1046
+ "application/json": {
1047
+ schema: {
1048
+ $ref: "#/components/schemas/LoopResponse"
1049
+ }
1050
+ }
1051
+ }
1052
+ }
1053
+ }
1054
+ },
1055
+ delete: {
1056
+ operationId: "deleteLoop",
1057
+ summary: "Delete a loop",
1058
+ parameters: [
1059
+ {
1060
+ name: "id",
1061
+ in: "path",
1062
+ required: true,
1063
+ schema: {
1064
+ type: "string"
1065
+ }
1066
+ }
1067
+ ],
1068
+ responses: {
1069
+ "200": {
1070
+ description: "deleted",
1071
+ content: {
1072
+ "application/json": {
1073
+ schema: {
1074
+ $ref: "#/components/schemas/DeleteResponse"
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ },
1082
+ "/v1/loops/{id}/archive": {
1083
+ post: {
1084
+ operationId: "archiveLoop",
1085
+ summary: "Archive a loop",
1086
+ parameters: [
1087
+ {
1088
+ name: "id",
1089
+ in: "path",
1090
+ required: true,
1091
+ schema: {
1092
+ type: "string"
1093
+ }
1094
+ }
1095
+ ],
1096
+ responses: {
1097
+ "200": {
1098
+ description: "loop",
1099
+ content: {
1100
+ "application/json": {
1101
+ schema: {
1102
+ $ref: "#/components/schemas/LoopResponse"
1103
+ }
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ },
1110
+ "/v1/loops/{id}/unarchive": {
1111
+ post: {
1112
+ operationId: "unarchiveLoop",
1113
+ summary: "Unarchive a loop",
1114
+ parameters: [
1115
+ {
1116
+ name: "id",
1117
+ in: "path",
1118
+ required: true,
1119
+ schema: {
1120
+ type: "string"
1121
+ }
1122
+ }
1123
+ ],
1124
+ responses: {
1125
+ "200": {
1126
+ description: "loop",
1127
+ content: {
1128
+ "application/json": {
1129
+ schema: {
1130
+ $ref: "#/components/schemas/LoopResponse"
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ }
1136
+ }
1137
+ },
1138
+ "/v1/runs": {
1139
+ get: {
1140
+ operationId: "listRuns",
1141
+ summary: "List runs",
1142
+ parameters: [
1143
+ {
1144
+ name: "loopId",
1145
+ in: "query",
1146
+ required: false,
1147
+ schema: {
1148
+ type: "string"
1149
+ }
1150
+ },
1151
+ {
1152
+ name: "status",
1153
+ in: "query",
1154
+ required: false,
1155
+ schema: {
1156
+ type: "string"
1157
+ }
1158
+ },
1159
+ {
1160
+ name: "limit",
1161
+ in: "query",
1162
+ required: false,
1163
+ schema: {
1164
+ type: "integer"
1165
+ }
1166
+ }
1167
+ ],
1168
+ responses: {
1169
+ "200": {
1170
+ description: "runs",
1171
+ content: {
1172
+ "application/json": {
1173
+ schema: {
1174
+ $ref: "#/components/schemas/RunListResponse"
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ }
1181
+ },
1182
+ "/v1/runs/{id}": {
1183
+ get: {
1184
+ operationId: "getRun",
1185
+ summary: "Get a run by id",
1186
+ parameters: [
1187
+ {
1188
+ name: "id",
1189
+ in: "path",
1190
+ required: true,
1191
+ schema: {
1192
+ type: "string"
1193
+ }
1194
+ }
1195
+ ],
1196
+ responses: {
1197
+ "200": {
1198
+ description: "run",
1199
+ content: {
1200
+ "application/json": {
1201
+ schema: {
1202
+ $ref: "#/components/schemas/RunResponse"
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+ },
1211
+ components: {
1212
+ schemas: {
1213
+ Foundation: {
1214
+ type: "object",
1215
+ properties: {
1216
+ status: {
1217
+ type: "string"
1218
+ },
1219
+ version: {
1220
+ type: "string"
1221
+ },
1222
+ mode: {
1223
+ type: "string"
1224
+ },
1225
+ service: {
1226
+ type: "string"
1227
+ },
1228
+ detail: {
1229
+ type: "string"
1230
+ }
1231
+ },
1232
+ required: [
1233
+ "status",
1234
+ "version",
1235
+ "mode"
1236
+ ]
1237
+ },
1238
+ Loop: {
1239
+ type: "object",
1240
+ properties: {
1241
+ id: {
1242
+ type: "string"
1243
+ },
1244
+ name: {
1245
+ type: "string"
1246
+ },
1247
+ description: {
1248
+ type: "string",
1249
+ nullable: true
1250
+ },
1251
+ status: {
1252
+ type: "string"
1253
+ },
1254
+ schedule: {
1255
+ type: "object",
1256
+ additionalProperties: true
1257
+ },
1258
+ target: {
1259
+ type: "object",
1260
+ additionalProperties: true
1261
+ },
1262
+ nextRunAt: {
1263
+ type: "string",
1264
+ nullable: true
1265
+ },
1266
+ createdAt: {
1267
+ type: "string"
1268
+ },
1269
+ updatedAt: {
1270
+ type: "string"
1271
+ }
1272
+ },
1273
+ required: [
1274
+ "id",
1275
+ "name",
1276
+ "status"
1277
+ ]
1278
+ },
1279
+ CreateLoopInput: {
1280
+ type: "object",
1281
+ properties: {
1282
+ name: {
1283
+ type: "string"
1284
+ },
1285
+ description: {
1286
+ type: "string"
1287
+ },
1288
+ schedule: {
1289
+ type: "object",
1290
+ additionalProperties: true
1291
+ },
1292
+ target: {
1293
+ type: "object",
1294
+ additionalProperties: true
1295
+ }
1296
+ },
1297
+ required: [
1298
+ "name",
1299
+ "schedule",
1300
+ "target"
1301
+ ]
1302
+ },
1303
+ UpdateLoopInput: {
1304
+ type: "object",
1305
+ properties: {
1306
+ status: {
1307
+ type: "string",
1308
+ enum: [
1309
+ "active",
1310
+ "paused",
1311
+ "stopped",
1312
+ "expired"
1313
+ ]
1314
+ },
1315
+ nextRunAt: {
1316
+ type: "string",
1317
+ nullable: true
1318
+ },
1319
+ retryScheduledFor: {
1320
+ type: "string",
1321
+ nullable: true
1322
+ },
1323
+ expiresAt: {
1324
+ type: "string",
1325
+ nullable: true
1326
+ }
1327
+ }
1328
+ },
1329
+ Run: {
1330
+ type: "object",
1331
+ properties: {
1332
+ id: {
1333
+ type: "string"
1334
+ },
1335
+ loopId: {
1336
+ type: "string"
1337
+ },
1338
+ status: {
1339
+ type: "string"
1340
+ },
1341
+ attempt: {
1342
+ type: "integer"
1343
+ },
1344
+ scheduledFor: {
1345
+ type: "string"
1346
+ },
1347
+ startedAt: {
1348
+ type: "string",
1349
+ nullable: true
1350
+ },
1351
+ finishedAt: {
1352
+ type: "string",
1353
+ nullable: true
1354
+ }
1355
+ },
1356
+ required: [
1357
+ "id",
1358
+ "loopId",
1359
+ "status"
1360
+ ]
1361
+ },
1362
+ LoopResponse: {
1363
+ type: "object",
1364
+ properties: {
1365
+ ok: {
1366
+ type: "boolean"
1367
+ },
1368
+ loop: {
1369
+ $ref: "#/components/schemas/Loop"
1370
+ }
1371
+ },
1372
+ required: [
1373
+ "ok",
1374
+ "loop"
1375
+ ]
1376
+ },
1377
+ LoopListResponse: {
1378
+ type: "object",
1379
+ properties: {
1380
+ ok: {
1381
+ type: "boolean"
1382
+ },
1383
+ loops: {
1384
+ type: "array",
1385
+ items: {
1386
+ $ref: "#/components/schemas/Loop"
1387
+ }
1388
+ }
1389
+ },
1390
+ required: [
1391
+ "ok",
1392
+ "loops"
1393
+ ]
1394
+ },
1395
+ RunResponse: {
1396
+ type: "object",
1397
+ properties: {
1398
+ ok: {
1399
+ type: "boolean"
1400
+ },
1401
+ run: {
1402
+ $ref: "#/components/schemas/Run"
1403
+ }
1404
+ },
1405
+ required: [
1406
+ "ok",
1407
+ "run"
1408
+ ]
1409
+ },
1410
+ RunListResponse: {
1411
+ type: "object",
1412
+ properties: {
1413
+ ok: {
1414
+ type: "boolean"
1415
+ },
1416
+ runs: {
1417
+ type: "array",
1418
+ items: {
1419
+ $ref: "#/components/schemas/Run"
1420
+ }
1421
+ }
1422
+ },
1423
+ required: [
1424
+ "ok",
1425
+ "runs"
1426
+ ]
1427
+ },
1428
+ DeleteResponse: {
1429
+ type: "object",
1430
+ properties: {
1431
+ ok: {
1432
+ type: "boolean"
1433
+ },
1434
+ deleted: {
1435
+ type: "boolean"
1436
+ }
1437
+ },
1438
+ required: [
1439
+ "ok",
1440
+ "deleted"
1441
+ ]
1442
+ }
1443
+ },
1444
+ securitySchemes: {
1445
+ apiKey: {
1446
+ type: "apiKey",
1447
+ in: "header",
1448
+ name: "x-api-key"
1449
+ }
1450
+ }
1451
+ },
1452
+ security: [
1453
+ {
1454
+ apiKey: []
1455
+ }
1456
+ ]
1457
+ };
818
1458
 
819
1459
  // src/api/index.ts
1460
+ function openApiDocument() {
1461
+ return { ...loops_default, info: { ...loops_default.info, version: packageVersion() } };
1462
+ }
820
1463
  var program = new Command;
821
1464
  var DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
822
1465
  var DEFAULT_EVIDENCE_LIMIT_BYTES = 256 * 1024;
@@ -866,21 +1509,62 @@ function apiStatus() {
866
1509
  status: buildDeploymentStatus({ perspective: "self_hosted" })
867
1510
  };
868
1511
  }
1512
+ function foundationMode() {
1513
+ return buildDeploymentStatus({}).activeDeploymentMode;
1514
+ }
1515
+ function foundationEnvelope(status, extra = {}) {
1516
+ return { status, version: packageVersion(), mode: foundationMode(), service: "loops", ...extra };
1517
+ }
869
1518
  function createLoopsApiServer(opts = {}) {
870
1519
  const host = opts.host ?? "127.0.0.1";
871
1520
  const port = opts.port ?? 8787;
872
- if (!isLocalBind(host) && !configuredAuthToken()) {
873
- throw new Error("non-local loops-api binds require LOOPS_API_TOKEN or HASNA_LOOPS_API_TOKEN");
1521
+ if (!isLocalBind(host) && !opts.authenticator && !configuredAuthToken()) {
1522
+ throw new Error("non-local loops-serve binds require an API-key authenticator or LOOPS_API_TOKEN");
874
1523
  }
1524
+ const defaultReady = async () => {
1525
+ if (!opts.storage)
1526
+ return { ready: false, detail: "storage_unconfigured" };
1527
+ try {
1528
+ await opts.storage.listLoops({ limit: 1 });
1529
+ return { ready: true };
1530
+ } catch (error) {
1531
+ return { ready: false, detail: error instanceof Error ? error.message : "storage_unreachable" };
1532
+ }
1533
+ };
1534
+ const readyCheck = opts.readyCheck ?? defaultReady;
875
1535
  return Bun.serve({
876
1536
  hostname: host,
877
1537
  port,
878
- fetch(request) {
879
- const unauthorized = authorizeRequest(request, host);
880
- if (unauthorized)
881
- return unauthorized;
1538
+ idleTimeout: 60,
1539
+ async fetch(request) {
882
1540
  const url = new URL(request.url);
883
- if (url.pathname === "/health" || url.pathname === "/status") {
1541
+ if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/healthz")) {
1542
+ return Response.json(foundationEnvelope("ok"));
1543
+ }
1544
+ if (request.method === "GET" && (url.pathname === "/version" || url.pathname === "/v1/version")) {
1545
+ return Response.json(foundationEnvelope("ok"));
1546
+ }
1547
+ if (request.method === "GET" && (url.pathname === "/ready" || url.pathname === "/readyz")) {
1548
+ const result = await readyCheck();
1549
+ return Response.json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), { status: result.ready ? 200 : 503 });
1550
+ }
1551
+ if (request.method === "GET" && url.pathname === "/openapi.json") {
1552
+ return Response.json(openApiDocument());
1553
+ }
1554
+ if (opts.authenticator) {
1555
+ const decision = await opts.authenticator.authenticate(request.headers, {
1556
+ method: request.method,
1557
+ path: url.pathname
1558
+ });
1559
+ if (!decision.ok) {
1560
+ return Response.json({ ok: false, error: decision.reason ?? "unauthorized", message: decision.message }, { status: decision.status || 401 });
1561
+ }
1562
+ } else {
1563
+ const unauthorized = authorizeRequest(request, host);
1564
+ if (unauthorized)
1565
+ return unauthorized;
1566
+ }
1567
+ if (request.method === "GET" && url.pathname === "/status") {
884
1568
  return Response.json(apiStatus());
885
1569
  }
886
1570
  return handleV1Request({
@@ -1341,13 +2025,14 @@ program.command("serve").description("serve the foundation health/status endpoin
1341
2025
  const server = createLoopsApiServer({ host, port });
1342
2026
  console.log(`loops-api listening on http://${server.hostname}:${server.port}`);
1343
2027
  });
1344
- if (import.meta.main) {
2028
+ if (import.meta.main && (import.meta.url.endsWith("api/index.ts") || import.meta.url.endsWith("api/index.js"))) {
1345
2029
  main().catch((error) => {
1346
2030
  console.error(error instanceof Error ? error.message : String(error));
1347
2031
  process.exit(1);
1348
2032
  });
1349
2033
  }
1350
2034
  export {
2035
+ openApiDocument,
1351
2036
  main,
1352
2037
  createLoopsApiServer,
1353
2038
  apiStatus