@hasna/loops 0.3.60 → 0.4.1

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 (63) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/LICENSE +197 -13
  3. package/README.md +95 -85
  4. package/dist/api/index.d.ts +11 -0
  5. package/dist/api/index.js +333 -0
  6. package/dist/cli/index.js +8013 -6659
  7. package/dist/daemon/control.d.ts +21 -1
  8. package/dist/daemon/daemon.d.ts +5 -0
  9. package/dist/daemon/index.js +2836 -1222
  10. package/dist/daemon/install.d.ts +1 -1
  11. package/dist/index.d.ts +23 -8
  12. package/dist/index.js +5715 -4541
  13. package/dist/lib/accounts.d.ts +6 -1
  14. package/dist/lib/agent-adapter.d.ts +74 -0
  15. package/dist/lib/backup.d.ts +25 -0
  16. package/dist/lib/errors.d.ts +21 -0
  17. package/dist/lib/executor.d.ts +10 -1
  18. package/dist/lib/goal/metadata.d.ts +16 -0
  19. package/dist/lib/goal/model-factory.d.ts +1 -0
  20. package/dist/lib/goal/prompts.d.ts +3 -1
  21. package/dist/lib/goal/types.d.ts +3 -0
  22. package/dist/lib/health.d.ts +1 -1
  23. package/dist/lib/ids.d.ts +8 -1
  24. package/dist/lib/machines.d.ts +6 -0
  25. package/dist/lib/mode.d.ts +48 -0
  26. package/dist/lib/mode.js +260 -0
  27. package/dist/lib/process-identity.d.ts +16 -0
  28. package/dist/lib/{schedule.d.ts → recurrence.d.ts} +1 -0
  29. package/dist/lib/redact.d.ts +21 -0
  30. package/dist/lib/route/cursors.d.ts +18 -0
  31. package/dist/lib/route/drain.d.ts +6 -0
  32. package/dist/lib/route/fields.d.ts +27 -0
  33. package/dist/lib/route/gates.d.ts +26 -0
  34. package/dist/lib/route/index.d.ts +18 -0
  35. package/dist/lib/route/options.d.ts +31 -0
  36. package/dist/lib/route/parse.d.ts +8 -0
  37. package/dist/lib/route/pr-review.d.ts +11 -0
  38. package/dist/lib/route/provider.d.ts +41 -0
  39. package/dist/lib/route/route-event.d.ts +23 -0
  40. package/dist/lib/route/route-tasks.d.ts +75 -0
  41. package/dist/lib/route/throttle.d.ts +47 -0
  42. package/dist/lib/route/todos-cli.d.ts +21 -0
  43. package/dist/lib/route/types.d.ts +88 -0
  44. package/dist/lib/run-artifacts.d.ts +17 -2
  45. package/dist/lib/run-envelope.d.ts +41 -0
  46. package/dist/lib/scheduler.d.ts +78 -2
  47. package/dist/lib/store.d.ts +63 -0
  48. package/dist/lib/store.js +1007 -287
  49. package/dist/lib/template-kit.d.ts +70 -0
  50. package/dist/lib/templates-custom.d.ts +34 -0
  51. package/dist/lib/templates.d.ts +13 -81
  52. package/dist/lib/workflow-runner.d.ts +2 -0
  53. package/dist/mcp/index.d.ts +3 -0
  54. package/dist/mcp/index.js +3231 -1382
  55. package/dist/runner/index.d.ts +9 -0
  56. package/dist/runner/index.js +295 -0
  57. package/dist/sdk/index.d.ts +29 -3
  58. package/dist/sdk/index.js +2743 -1044
  59. package/dist/test-helpers.d.ts +37 -0
  60. package/dist/types.d.ts +3 -1
  61. package/docs/DEPLOYMENT_MODES.md +140 -0
  62. package/docs/USAGE.md +102 -46
  63. package/package.json +23 -5
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ // package.json
4
+ var package_default = {
5
+ name: "@hasna/loops",
6
+ version: "0.4.1",
7
+ description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
8
+ type: "module",
9
+ main: "dist/index.js",
10
+ types: "dist/index.d.ts",
11
+ bin: {
12
+ loops: "dist/cli/index.js",
13
+ "loops-daemon": "dist/daemon/index.js",
14
+ "loops-api": "dist/api/index.js",
15
+ "loops-runner": "dist/runner/index.js",
16
+ "loops-mcp": "dist/mcp/index.js"
17
+ },
18
+ exports: {
19
+ ".": {
20
+ types: "./dist/index.d.ts",
21
+ import: "./dist/index.js"
22
+ },
23
+ "./sdk": {
24
+ types: "./dist/sdk/index.d.ts",
25
+ import: "./dist/sdk/index.js"
26
+ },
27
+ "./mcp": {
28
+ types: "./dist/mcp/index.d.ts",
29
+ import: "./dist/mcp/index.js"
30
+ },
31
+ "./api": {
32
+ types: "./dist/api/index.d.ts",
33
+ import: "./dist/api/index.js"
34
+ },
35
+ "./runner": {
36
+ types: "./dist/runner/index.d.ts",
37
+ import: "./dist/runner/index.js"
38
+ },
39
+ "./mode": {
40
+ types: "./dist/lib/mode.d.ts",
41
+ import: "./dist/lib/mode.js"
42
+ },
43
+ "./storage": {
44
+ types: "./dist/lib/store.d.ts",
45
+ import: "./dist/lib/store.js"
46
+ }
47
+ },
48
+ files: [
49
+ "dist",
50
+ "README.md",
51
+ "CHANGELOG.md",
52
+ "docs",
53
+ "LICENSE"
54
+ ],
55
+ scripts: {
56
+ 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 --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",
57
+ "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
58
+ typecheck: "tsc --noEmit",
59
+ test: "bun test",
60
+ "test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
61
+ prepare: "test -d dist || bun run build",
62
+ "dev:cli": "bun run src/cli/index.ts",
63
+ "dev:daemon": "bun run src/daemon/index.ts",
64
+ prepublishOnly: "bun run build && bun run test:boundary"
65
+ },
66
+ keywords: [
67
+ "loops",
68
+ "scheduler",
69
+ "daemon",
70
+ "agents",
71
+ "claude",
72
+ "cursor",
73
+ "codewith",
74
+ "opencode",
75
+ "cli"
76
+ ],
77
+ repository: {
78
+ type: "git",
79
+ url: "git+https://github.com/hasna/loops.git"
80
+ },
81
+ homepage: "https://github.com/hasna/loops#readme",
82
+ bugs: {
83
+ url: "https://github.com/hasna/loops/issues"
84
+ },
85
+ author: "Hasna <andrei@hasna.com>",
86
+ license: "Apache-2.0",
87
+ engines: {
88
+ bun: ">=1.0.0"
89
+ },
90
+ dependencies: {
91
+ "@hasna/events": "^0.1.9",
92
+ "@modelcontextprotocol/sdk": "^1.29.0",
93
+ "@openrouter/ai-sdk-provider": "2.9.1",
94
+ ai: "6.0.204",
95
+ commander: "^13.1.0",
96
+ zod: "4.4.3"
97
+ },
98
+ optionalDependencies: {
99
+ "@hasna/machines": "0.0.49"
100
+ },
101
+ devDependencies: {
102
+ "@types/bun": "latest",
103
+ typescript: "^5.7.3"
104
+ },
105
+ publishConfig: {
106
+ registry: "https://registry.npmjs.org",
107
+ access: "public"
108
+ }
109
+ };
110
+
111
+ // src/lib/version.ts
112
+ function packageVersion() {
113
+ if (typeof package_default.version !== "string" || package_default.version.trim() === "")
114
+ throw new Error("package.json version is missing");
115
+ return package_default.version;
116
+ }
117
+
118
+ // src/lib/mode.ts
119
+ var LOOP_DEPLOYMENT_MODES = ["local", "self_hosted", "cloud"];
120
+ var MODE_ENV_KEYS = ["LOOPS_MODE", "HASNA_LOOPS_MODE"];
121
+ var API_URL_ENV_KEYS = ["LOOPS_API_URL", "HASNA_LOOPS_API_URL"];
122
+ var CLOUD_API_URL_ENV_KEYS = ["LOOPS_CLOUD_API_URL", "HASNA_LOOPS_CLOUD_API_URL"];
123
+ var DATABASE_URL_ENV_KEYS = ["LOOPS_DATABASE_URL", "HASNA_LOOPS_DATABASE_URL"];
124
+ var API_TOKEN_ENV_KEYS = ["LOOPS_API_TOKEN", "HASNA_LOOPS_API_TOKEN"];
125
+ var CLOUD_TOKEN_ENV_KEYS = ["LOOPS_CLOUD_TOKEN", "HASNA_LOOPS_CLOUD_TOKEN"];
126
+ var TOKEN_ENV_KEYS = [...API_TOKEN_ENV_KEYS, ...CLOUD_TOKEN_ENV_KEYS];
127
+ function envValue(env, keys) {
128
+ for (const key of keys) {
129
+ const value = env[key]?.trim();
130
+ if (value)
131
+ return { key, value };
132
+ }
133
+ return;
134
+ }
135
+ function normalizeLoopDeploymentMode(value) {
136
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
137
+ if (normalized === "local")
138
+ return "local";
139
+ if (normalized === "self_hosted" || normalized === "selfhosted")
140
+ return "self_hosted";
141
+ if (normalized === "cloud" || normalized === "saas")
142
+ return "cloud";
143
+ throw new Error(`unsupported OpenLoops deployment mode "${value}"; expected local, self_hosted, or cloud`);
144
+ }
145
+ function resolveLoopDeploymentMode(env = process.env) {
146
+ const explicitMode = envValue(env, MODE_ENV_KEYS);
147
+ if (explicitMode) {
148
+ return {
149
+ deploymentMode: normalizeLoopDeploymentMode(explicitMode.value),
150
+ source: explicitMode.key
151
+ };
152
+ }
153
+ const cloudApiUrl = envValue(env, CLOUD_API_URL_ENV_KEYS);
154
+ if (cloudApiUrl)
155
+ return { deploymentMode: "cloud", source: cloudApiUrl.key };
156
+ const apiUrl = envValue(env, API_URL_ENV_KEYS);
157
+ if (apiUrl)
158
+ return { deploymentMode: "self_hosted", source: apiUrl.key };
159
+ const databaseUrl = envValue(env, DATABASE_URL_ENV_KEYS);
160
+ if (databaseUrl)
161
+ return { deploymentMode: "self_hosted", source: databaseUrl.key };
162
+ return { deploymentMode: "local", source: "default" };
163
+ }
164
+ function loopControlPlaneConfig(env = process.env) {
165
+ return {
166
+ apiUrl: envValue(env, API_URL_ENV_KEYS)?.value,
167
+ cloudApiUrl: envValue(env, CLOUD_API_URL_ENV_KEYS)?.value,
168
+ databaseUrlPresent: Boolean(envValue(env, DATABASE_URL_ENV_KEYS)),
169
+ apiAuthTokenPresent: Boolean(envValue(env, API_TOKEN_ENV_KEYS)),
170
+ cloudAuthTokenPresent: Boolean(envValue(env, CLOUD_TOKEN_ENV_KEYS)),
171
+ authTokenPresent: Boolean(envValue(env, TOKEN_ENV_KEYS))
172
+ };
173
+ }
174
+ function sourceOfTruthForMode(mode) {
175
+ if (mode === "local")
176
+ return "local_sqlite";
177
+ if (mode === "self_hosted")
178
+ return "self_hosted_control_plane";
179
+ return "cloud_control_plane";
180
+ }
181
+ function displayControlPlaneUrl(value) {
182
+ if (!value)
183
+ return;
184
+ try {
185
+ const url = new URL(value);
186
+ const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/g, "");
187
+ return `${url.origin}${path}`;
188
+ } catch {
189
+ return "[invalid-url]";
190
+ }
191
+ }
192
+ function buildDeploymentStatus(opts = {}) {
193
+ const env = opts.env ?? process.env;
194
+ const active = resolveLoopDeploymentMode(env);
195
+ const deploymentMode = opts.perspective ?? active.deploymentMode;
196
+ const config = loopControlPlaneConfig(env);
197
+ const rawApiUrl = deploymentMode === "cloud" ? config.cloudApiUrl : config.apiUrl;
198
+ const apiUrl = displayControlPlaneUrl(rawApiUrl);
199
+ const controlPlaneKind = deploymentMode === "local" ? "none" : deploymentMode;
200
+ const deploymentAuthTokenPresent = deploymentMode === "cloud" ? config.cloudAuthTokenPresent : deploymentMode === "self_hosted" ? config.apiAuthTokenPresent : false;
201
+ const controlPlaneConfigured = deploymentMode === "local" ? false : deploymentMode === "self_hosted" ? Boolean(config.apiUrl || config.databaseUrlPresent) : Boolean(config.cloudApiUrl && deploymentAuthTokenPresent);
202
+ const warnings = [];
203
+ if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
204
+ warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
205
+ }
206
+ if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
207
+ warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
208
+ }
209
+ if (deploymentMode === "cloud" && !config.cloudApiUrl) {
210
+ warnings.push("cloud mode is a public contract until LOOPS_CLOUD_API_URL is configured");
211
+ }
212
+ if (deploymentMode === "cloud" && config.cloudApiUrl && !deploymentAuthTokenPresent) {
213
+ warnings.push("cloud mode needs LOOPS_CLOUD_TOKEN or HASNA_LOOPS_CLOUD_TOKEN before it can become ready");
214
+ }
215
+ if (opts.perspective && opts.perspective !== active.deploymentMode) {
216
+ warnings.push(`active deployment mode is ${active.deploymentMode}; this is the ${opts.perspective} perspective`);
217
+ }
218
+ return {
219
+ packageVersion: packageVersion(),
220
+ deploymentMode,
221
+ activeDeploymentMode: active.deploymentMode,
222
+ active: deploymentMode === active.deploymentMode,
223
+ deploymentModeSource: active.source,
224
+ sourceOfTruth: sourceOfTruthForMode(deploymentMode),
225
+ localStore: {
226
+ role: deploymentMode === "local" ? "authoritative" : "cache_and_spool"
227
+ },
228
+ controlPlane: {
229
+ kind: controlPlaneKind,
230
+ configured: controlPlaneConfigured,
231
+ apiUrl,
232
+ databaseUrlPresent: config.databaseUrlPresent,
233
+ authTokenPresent: deploymentAuthTokenPresent
234
+ },
235
+ runner: {
236
+ required: deploymentMode !== "local",
237
+ role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
238
+ },
239
+ warnings
240
+ };
241
+ }
242
+ function deploymentStatusLine(status) {
243
+ const configured = status.controlPlane.kind === "none" ? "none" : String(status.controlPlane.configured);
244
+ const active = status.active ? "active" : `active=${status.activeDeploymentMode}`;
245
+ return [
246
+ `deploymentMode=${status.deploymentMode}`,
247
+ active,
248
+ `source=${status.deploymentModeSource}`,
249
+ `truth=${status.sourceOfTruth}`,
250
+ `local=${status.localStore.role}`,
251
+ `control_plane=${configured}`
252
+ ].join(" ");
253
+ }
254
+
255
+ // src/api/index.ts
256
+ import { Command } from "commander";
257
+ var program = new Command;
258
+ program.name("loops-api").description("OpenLoops self-hosted control-plane API foundation").version(packageVersion()).option("-j, --json", "print JSON");
259
+ function wantsJson(opts) {
260
+ return Boolean(program.opts().json || opts?.json);
261
+ }
262
+ function printStatus(opts) {
263
+ const status = buildDeploymentStatus({ perspective: "self_hosted" });
264
+ if (wantsJson(opts))
265
+ console.log(JSON.stringify(apiStatus(), null, 2));
266
+ else
267
+ console.log(deploymentStatusLine(status));
268
+ }
269
+ function configuredAuthToken() {
270
+ return process.env.LOOPS_API_TOKEN?.trim() || process.env.HASNA_LOOPS_API_TOKEN?.trim();
271
+ }
272
+ function isLocalBind(host) {
273
+ return ["127.0.0.1", "localhost", "::1"].includes(host);
274
+ }
275
+ function authorizeRequest(request, host) {
276
+ if (isLocalBind(host))
277
+ return;
278
+ const token = configuredAuthToken();
279
+ if (!token)
280
+ return Response.json({ ok: false, error: "auth_required" }, { status: 401 });
281
+ const authorization = request.headers.get("authorization") ?? "";
282
+ return authorization === `Bearer ${token}` ? undefined : Response.json({ ok: false, error: "unauthorized" }, { status: 401 });
283
+ }
284
+ function apiStatus() {
285
+ return {
286
+ ok: true,
287
+ service: "loops-api",
288
+ status: buildDeploymentStatus({ perspective: "self_hosted" })
289
+ };
290
+ }
291
+ function createLoopsApiServer(opts = {}) {
292
+ const host = opts.host ?? "127.0.0.1";
293
+ const port = opts.port ?? 8787;
294
+ if (!isLocalBind(host) && !configuredAuthToken()) {
295
+ throw new Error("non-local loops-api binds require LOOPS_API_TOKEN or HASNA_LOOPS_API_TOKEN");
296
+ }
297
+ return Bun.serve({
298
+ hostname: host,
299
+ port,
300
+ fetch(request) {
301
+ const unauthorized = authorizeRequest(request, host);
302
+ if (unauthorized)
303
+ return unauthorized;
304
+ const url = new URL(request.url);
305
+ if (url.pathname === "/health" || url.pathname === "/status") {
306
+ return Response.json(apiStatus());
307
+ }
308
+ return Response.json({ ok: false, error: "not_found" }, { status: 404 });
309
+ }
310
+ });
311
+ }
312
+ async function main(argv = process.argv) {
313
+ await program.parseAsync(argv);
314
+ }
315
+ program.action(() => printStatus());
316
+ program.command("status").option("-j, --json", "print JSON").action((opts) => printStatus(opts));
317
+ program.command("serve").description("serve the foundation health/status endpoints").option("--host <host>", "host", "127.0.0.1").option("--port <port>", "port", (value) => Number(value), 8787).action((opts) => {
318
+ const host = String(opts.host);
319
+ const port = Number(opts.port);
320
+ const server = createLoopsApiServer({ host, port });
321
+ console.log(`loops-api listening on http://${server.hostname}:${server.port}`);
322
+ });
323
+ if (import.meta.main) {
324
+ main().catch((error) => {
325
+ console.error(error instanceof Error ? error.message : String(error));
326
+ process.exit(1);
327
+ });
328
+ }
329
+ export {
330
+ main,
331
+ createLoopsApiServer,
332
+ apiStatus
333
+ };