@hasna/loops 0.4.0 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +75 -33
- package/dist/api/index.d.ts +11 -0
- package/dist/api/index.js +333 -0
- package/dist/cli/index.js +282 -107
- package/dist/daemon/index.js +18 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +267 -105
- package/dist/lib/mode.d.ts +48 -0
- package/dist/lib/mode.js +260 -0
- package/dist/mcp/index.js +18 -3
- package/dist/runner/index.d.ts +9 -0
- package/dist/runner/index.js +295 -0
- package/dist/sdk/index.js +3 -3
- package/dist/types.d.ts +1 -1
- package/docs/DEPLOYMENT_MODES.md +140 -0
- package/docs/USAGE.md +66 -32
- package/package.json +18 -3
package/dist/lib/mode.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// package.json
|
|
3
|
+
var package_default = {
|
|
4
|
+
name: "@hasna/loops",
|
|
5
|
+
version: "0.4.1",
|
|
6
|
+
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7
|
+
type: "module",
|
|
8
|
+
main: "dist/index.js",
|
|
9
|
+
types: "dist/index.d.ts",
|
|
10
|
+
bin: {
|
|
11
|
+
loops: "dist/cli/index.js",
|
|
12
|
+
"loops-daemon": "dist/daemon/index.js",
|
|
13
|
+
"loops-api": "dist/api/index.js",
|
|
14
|
+
"loops-runner": "dist/runner/index.js",
|
|
15
|
+
"loops-mcp": "dist/mcp/index.js"
|
|
16
|
+
},
|
|
17
|
+
exports: {
|
|
18
|
+
".": {
|
|
19
|
+
types: "./dist/index.d.ts",
|
|
20
|
+
import: "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./sdk": {
|
|
23
|
+
types: "./dist/sdk/index.d.ts",
|
|
24
|
+
import: "./dist/sdk/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./mcp": {
|
|
27
|
+
types: "./dist/mcp/index.d.ts",
|
|
28
|
+
import: "./dist/mcp/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./api": {
|
|
31
|
+
types: "./dist/api/index.d.ts",
|
|
32
|
+
import: "./dist/api/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./runner": {
|
|
35
|
+
types: "./dist/runner/index.d.ts",
|
|
36
|
+
import: "./dist/runner/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./mode": {
|
|
39
|
+
types: "./dist/lib/mode.d.ts",
|
|
40
|
+
import: "./dist/lib/mode.js"
|
|
41
|
+
},
|
|
42
|
+
"./storage": {
|
|
43
|
+
types: "./dist/lib/store.d.ts",
|
|
44
|
+
import: "./dist/lib/store.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
files: [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"docs",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
],
|
|
54
|
+
scripts: {
|
|
55
|
+
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",
|
|
56
|
+
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
57
|
+
typecheck: "tsc --noEmit",
|
|
58
|
+
test: "bun test",
|
|
59
|
+
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
60
|
+
prepare: "test -d dist || bun run build",
|
|
61
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
62
|
+
"dev:daemon": "bun run src/daemon/index.ts",
|
|
63
|
+
prepublishOnly: "bun run build && bun run test:boundary"
|
|
64
|
+
},
|
|
65
|
+
keywords: [
|
|
66
|
+
"loops",
|
|
67
|
+
"scheduler",
|
|
68
|
+
"daemon",
|
|
69
|
+
"agents",
|
|
70
|
+
"claude",
|
|
71
|
+
"cursor",
|
|
72
|
+
"codewith",
|
|
73
|
+
"opencode",
|
|
74
|
+
"cli"
|
|
75
|
+
],
|
|
76
|
+
repository: {
|
|
77
|
+
type: "git",
|
|
78
|
+
url: "git+https://github.com/hasna/loops.git"
|
|
79
|
+
},
|
|
80
|
+
homepage: "https://github.com/hasna/loops#readme",
|
|
81
|
+
bugs: {
|
|
82
|
+
url: "https://github.com/hasna/loops/issues"
|
|
83
|
+
},
|
|
84
|
+
author: "Hasna <andrei@hasna.com>",
|
|
85
|
+
license: "Apache-2.0",
|
|
86
|
+
engines: {
|
|
87
|
+
bun: ">=1.0.0"
|
|
88
|
+
},
|
|
89
|
+
dependencies: {
|
|
90
|
+
"@hasna/events": "^0.1.9",
|
|
91
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
92
|
+
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
93
|
+
ai: "6.0.204",
|
|
94
|
+
commander: "^13.1.0",
|
|
95
|
+
zod: "4.4.3"
|
|
96
|
+
},
|
|
97
|
+
optionalDependencies: {
|
|
98
|
+
"@hasna/machines": "0.0.49"
|
|
99
|
+
},
|
|
100
|
+
devDependencies: {
|
|
101
|
+
"@types/bun": "latest",
|
|
102
|
+
typescript: "^5.7.3"
|
|
103
|
+
},
|
|
104
|
+
publishConfig: {
|
|
105
|
+
registry: "https://registry.npmjs.org",
|
|
106
|
+
access: "public"
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/lib/version.ts
|
|
111
|
+
function packageVersion() {
|
|
112
|
+
if (typeof package_default.version !== "string" || package_default.version.trim() === "")
|
|
113
|
+
throw new Error("package.json version is missing");
|
|
114
|
+
return package_default.version;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/lib/mode.ts
|
|
118
|
+
var LOOP_DEPLOYMENT_MODES = ["local", "self_hosted", "cloud"];
|
|
119
|
+
var MODE_ENV_KEYS = ["LOOPS_MODE", "HASNA_LOOPS_MODE"];
|
|
120
|
+
var API_URL_ENV_KEYS = ["LOOPS_API_URL", "HASNA_LOOPS_API_URL"];
|
|
121
|
+
var CLOUD_API_URL_ENV_KEYS = ["LOOPS_CLOUD_API_URL", "HASNA_LOOPS_CLOUD_API_URL"];
|
|
122
|
+
var DATABASE_URL_ENV_KEYS = ["LOOPS_DATABASE_URL", "HASNA_LOOPS_DATABASE_URL"];
|
|
123
|
+
var API_TOKEN_ENV_KEYS = ["LOOPS_API_TOKEN", "HASNA_LOOPS_API_TOKEN"];
|
|
124
|
+
var CLOUD_TOKEN_ENV_KEYS = ["LOOPS_CLOUD_TOKEN", "HASNA_LOOPS_CLOUD_TOKEN"];
|
|
125
|
+
var TOKEN_ENV_KEYS = [...API_TOKEN_ENV_KEYS, ...CLOUD_TOKEN_ENV_KEYS];
|
|
126
|
+
function envValue(env, keys) {
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
const value = env[key]?.trim();
|
|
129
|
+
if (value)
|
|
130
|
+
return { key, value };
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
function normalizeLoopDeploymentMode(value) {
|
|
135
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
136
|
+
if (normalized === "local")
|
|
137
|
+
return "local";
|
|
138
|
+
if (normalized === "self_hosted" || normalized === "selfhosted")
|
|
139
|
+
return "self_hosted";
|
|
140
|
+
if (normalized === "cloud" || normalized === "saas")
|
|
141
|
+
return "cloud";
|
|
142
|
+
throw new Error(`unsupported OpenLoops deployment mode "${value}"; expected local, self_hosted, or cloud`);
|
|
143
|
+
}
|
|
144
|
+
function resolveLoopDeploymentMode(env = process.env) {
|
|
145
|
+
const explicitMode = envValue(env, MODE_ENV_KEYS);
|
|
146
|
+
if (explicitMode) {
|
|
147
|
+
return {
|
|
148
|
+
deploymentMode: normalizeLoopDeploymentMode(explicitMode.value),
|
|
149
|
+
source: explicitMode.key
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const cloudApiUrl = envValue(env, CLOUD_API_URL_ENV_KEYS);
|
|
153
|
+
if (cloudApiUrl)
|
|
154
|
+
return { deploymentMode: "cloud", source: cloudApiUrl.key };
|
|
155
|
+
const apiUrl = envValue(env, API_URL_ENV_KEYS);
|
|
156
|
+
if (apiUrl)
|
|
157
|
+
return { deploymentMode: "self_hosted", source: apiUrl.key };
|
|
158
|
+
const databaseUrl = envValue(env, DATABASE_URL_ENV_KEYS);
|
|
159
|
+
if (databaseUrl)
|
|
160
|
+
return { deploymentMode: "self_hosted", source: databaseUrl.key };
|
|
161
|
+
return { deploymentMode: "local", source: "default" };
|
|
162
|
+
}
|
|
163
|
+
function loopControlPlaneConfig(env = process.env) {
|
|
164
|
+
return {
|
|
165
|
+
apiUrl: envValue(env, API_URL_ENV_KEYS)?.value,
|
|
166
|
+
cloudApiUrl: envValue(env, CLOUD_API_URL_ENV_KEYS)?.value,
|
|
167
|
+
databaseUrlPresent: Boolean(envValue(env, DATABASE_URL_ENV_KEYS)),
|
|
168
|
+
apiAuthTokenPresent: Boolean(envValue(env, API_TOKEN_ENV_KEYS)),
|
|
169
|
+
cloudAuthTokenPresent: Boolean(envValue(env, CLOUD_TOKEN_ENV_KEYS)),
|
|
170
|
+
authTokenPresent: Boolean(envValue(env, TOKEN_ENV_KEYS))
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function sourceOfTruthForMode(mode) {
|
|
174
|
+
if (mode === "local")
|
|
175
|
+
return "local_sqlite";
|
|
176
|
+
if (mode === "self_hosted")
|
|
177
|
+
return "self_hosted_control_plane";
|
|
178
|
+
return "cloud_control_plane";
|
|
179
|
+
}
|
|
180
|
+
function displayControlPlaneUrl(value) {
|
|
181
|
+
if (!value)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
const url = new URL(value);
|
|
185
|
+
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/g, "");
|
|
186
|
+
return `${url.origin}${path}`;
|
|
187
|
+
} catch {
|
|
188
|
+
return "[invalid-url]";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function buildDeploymentStatus(opts = {}) {
|
|
192
|
+
const env = opts.env ?? process.env;
|
|
193
|
+
const active = resolveLoopDeploymentMode(env);
|
|
194
|
+
const deploymentMode = opts.perspective ?? active.deploymentMode;
|
|
195
|
+
const config = loopControlPlaneConfig(env);
|
|
196
|
+
const rawApiUrl = deploymentMode === "cloud" ? config.cloudApiUrl : config.apiUrl;
|
|
197
|
+
const apiUrl = displayControlPlaneUrl(rawApiUrl);
|
|
198
|
+
const controlPlaneKind = deploymentMode === "local" ? "none" : deploymentMode;
|
|
199
|
+
const deploymentAuthTokenPresent = deploymentMode === "cloud" ? config.cloudAuthTokenPresent : deploymentMode === "self_hosted" ? config.apiAuthTokenPresent : false;
|
|
200
|
+
const controlPlaneConfigured = deploymentMode === "local" ? false : deploymentMode === "self_hosted" ? Boolean(config.apiUrl || config.databaseUrlPresent) : Boolean(config.cloudApiUrl && deploymentAuthTokenPresent);
|
|
201
|
+
const warnings = [];
|
|
202
|
+
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
203
|
+
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
204
|
+
}
|
|
205
|
+
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
206
|
+
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
207
|
+
}
|
|
208
|
+
if (deploymentMode === "cloud" && !config.cloudApiUrl) {
|
|
209
|
+
warnings.push("cloud mode is a public contract until LOOPS_CLOUD_API_URL is configured");
|
|
210
|
+
}
|
|
211
|
+
if (deploymentMode === "cloud" && config.cloudApiUrl && !deploymentAuthTokenPresent) {
|
|
212
|
+
warnings.push("cloud mode needs LOOPS_CLOUD_TOKEN or HASNA_LOOPS_CLOUD_TOKEN before it can become ready");
|
|
213
|
+
}
|
|
214
|
+
if (opts.perspective && opts.perspective !== active.deploymentMode) {
|
|
215
|
+
warnings.push(`active deployment mode is ${active.deploymentMode}; this is the ${opts.perspective} perspective`);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
packageVersion: packageVersion(),
|
|
219
|
+
deploymentMode,
|
|
220
|
+
activeDeploymentMode: active.deploymentMode,
|
|
221
|
+
active: deploymentMode === active.deploymentMode,
|
|
222
|
+
deploymentModeSource: active.source,
|
|
223
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
224
|
+
localStore: {
|
|
225
|
+
role: deploymentMode === "local" ? "authoritative" : "cache_and_spool"
|
|
226
|
+
},
|
|
227
|
+
controlPlane: {
|
|
228
|
+
kind: controlPlaneKind,
|
|
229
|
+
configured: controlPlaneConfigured,
|
|
230
|
+
apiUrl,
|
|
231
|
+
databaseUrlPresent: config.databaseUrlPresent,
|
|
232
|
+
authTokenPresent: deploymentAuthTokenPresent
|
|
233
|
+
},
|
|
234
|
+
runner: {
|
|
235
|
+
required: deploymentMode !== "local",
|
|
236
|
+
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
237
|
+
},
|
|
238
|
+
warnings
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function deploymentStatusLine(status) {
|
|
242
|
+
const configured = status.controlPlane.kind === "none" ? "none" : String(status.controlPlane.configured);
|
|
243
|
+
const active = status.active ? "active" : `active=${status.activeDeploymentMode}`;
|
|
244
|
+
return [
|
|
245
|
+
`deploymentMode=${status.deploymentMode}`,
|
|
246
|
+
active,
|
|
247
|
+
`source=${status.deploymentModeSource}`,
|
|
248
|
+
`truth=${status.sourceOfTruth}`,
|
|
249
|
+
`local=${status.localStore.role}`,
|
|
250
|
+
`control_plane=${configured}`
|
|
251
|
+
].join(" ");
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
resolveLoopDeploymentMode,
|
|
255
|
+
normalizeLoopDeploymentMode,
|
|
256
|
+
loopControlPlaneConfig,
|
|
257
|
+
deploymentStatusLine,
|
|
258
|
+
buildDeploymentStatus,
|
|
259
|
+
LOOP_DEPLOYMENT_MODES
|
|
260
|
+
};
|
package/dist/mcp/index.js
CHANGED
|
@@ -7328,7 +7328,7 @@ async function tick(deps) {
|
|
|
7328
7328
|
// package.json
|
|
7329
7329
|
var package_default = {
|
|
7330
7330
|
name: "@hasna/loops",
|
|
7331
|
-
version: "0.4.
|
|
7331
|
+
version: "0.4.1",
|
|
7332
7332
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7333
7333
|
type: "module",
|
|
7334
7334
|
main: "dist/index.js",
|
|
@@ -7336,6 +7336,8 @@ var package_default = {
|
|
|
7336
7336
|
bin: {
|
|
7337
7337
|
loops: "dist/cli/index.js",
|
|
7338
7338
|
"loops-daemon": "dist/daemon/index.js",
|
|
7339
|
+
"loops-api": "dist/api/index.js",
|
|
7340
|
+
"loops-runner": "dist/runner/index.js",
|
|
7339
7341
|
"loops-mcp": "dist/mcp/index.js"
|
|
7340
7342
|
},
|
|
7341
7343
|
exports: {
|
|
@@ -7351,6 +7353,18 @@ var package_default = {
|
|
|
7351
7353
|
types: "./dist/mcp/index.d.ts",
|
|
7352
7354
|
import: "./dist/mcp/index.js"
|
|
7353
7355
|
},
|
|
7356
|
+
"./api": {
|
|
7357
|
+
types: "./dist/api/index.d.ts",
|
|
7358
|
+
import: "./dist/api/index.js"
|
|
7359
|
+
},
|
|
7360
|
+
"./runner": {
|
|
7361
|
+
types: "./dist/runner/index.d.ts",
|
|
7362
|
+
import: "./dist/runner/index.js"
|
|
7363
|
+
},
|
|
7364
|
+
"./mode": {
|
|
7365
|
+
types: "./dist/lib/mode.d.ts",
|
|
7366
|
+
import: "./dist/lib/mode.js"
|
|
7367
|
+
},
|
|
7354
7368
|
"./storage": {
|
|
7355
7369
|
types: "./dist/lib/store.d.ts",
|
|
7356
7370
|
import: "./dist/lib/store.js"
|
|
@@ -7364,14 +7378,15 @@ var package_default = {
|
|
|
7364
7378
|
"LICENSE"
|
|
7365
7379
|
],
|
|
7366
7380
|
scripts: {
|
|
7367
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
7381
|
+
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",
|
|
7368
7382
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
7369
7383
|
typecheck: "tsc --noEmit",
|
|
7370
7384
|
test: "bun test",
|
|
7385
|
+
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
7371
7386
|
prepare: "test -d dist || bun run build",
|
|
7372
7387
|
"dev:cli": "bun run src/cli/index.ts",
|
|
7373
7388
|
"dev:daemon": "bun run src/daemon/index.ts",
|
|
7374
|
-
prepublishOnly: "bun run build"
|
|
7389
|
+
prepublishOnly: "bun run build && bun run test:boundary"
|
|
7375
7390
|
},
|
|
7376
7391
|
keywords: [
|
|
7377
7392
|
"loops",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
export declare function runnerStatus(machineId?: string | undefined): {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
service: string;
|
|
5
|
+
machineId: string | undefined;
|
|
6
|
+
deployment: import("../index.js").LoopDeploymentStatus;
|
|
7
|
+
state: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function main(argv?: string[]): Promise<void>;
|
|
@@ -0,0 +1,295 @@
|
|
|
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/runner/index.ts
|
|
256
|
+
import { Command } from "commander";
|
|
257
|
+
var program = new Command;
|
|
258
|
+
program.name("loops-runner").description("OpenLoops control-plane runner foundation").version(packageVersion()).option("-j, --json", "print JSON");
|
|
259
|
+
function runnerStatus(machineId = process.env.LOOPS_RUNNER_MACHINE_ID || process.env.HASNA_MACHINE_ID) {
|
|
260
|
+
const deployment = buildDeploymentStatus();
|
|
261
|
+
return {
|
|
262
|
+
ok: deployment.deploymentMode === "local" || deployment.controlPlane.configured,
|
|
263
|
+
service: "loops-runner",
|
|
264
|
+
machineId,
|
|
265
|
+
deployment,
|
|
266
|
+
state: deployment.deploymentMode === "local" ? "local_daemon_authoritative" : deployment.controlPlane.configured ? "ready_for_control_plane" : "missing_control_plane_configuration"
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function wantsJson(opts) {
|
|
270
|
+
return Boolean(program.opts().json || opts?.json);
|
|
271
|
+
}
|
|
272
|
+
function printStatus(opts) {
|
|
273
|
+
const status = runnerStatus();
|
|
274
|
+
if (wantsJson(opts))
|
|
275
|
+
console.log(JSON.stringify(status, null, 2));
|
|
276
|
+
else
|
|
277
|
+
console.log(`${deploymentStatusLine(status.deployment)} runner=${status.state}${status.machineId ? ` machine=${status.machineId}` : ""}`);
|
|
278
|
+
if (!status.ok)
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
}
|
|
281
|
+
async function main(argv = process.argv) {
|
|
282
|
+
await program.parseAsync(argv);
|
|
283
|
+
}
|
|
284
|
+
program.action(() => printStatus());
|
|
285
|
+
program.command("status").option("-j, --json", "print JSON").action((opts) => printStatus(opts));
|
|
286
|
+
if (import.meta.main) {
|
|
287
|
+
main().catch((error) => {
|
|
288
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
289
|
+
process.exit(1);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
export {
|
|
293
|
+
runnerStatus,
|
|
294
|
+
main
|
|
295
|
+
};
|
package/dist/sdk/index.js
CHANGED
|
@@ -7417,15 +7417,15 @@ function openAutomationsRuntimeBinding(overrides = {}) {
|
|
|
7417
7417
|
failCommand: "automations queue fail",
|
|
7418
7418
|
eventHandoff: {
|
|
7419
7419
|
envelopeCommand: "automations webhooks event",
|
|
7420
|
-
handlerCommand: "loops
|
|
7421
|
-
pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json
|
|
7420
|
+
handlerCommand: "loops routes create generic",
|
|
7421
|
+
pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json routes create generic",
|
|
7422
7422
|
boundary: "Use only for explicit event-envelope workflow handoff. OpenAutomations still owns deterministic automation materialization and queue state; OpenLoops owns workflow invocation."
|
|
7423
7423
|
},
|
|
7424
7424
|
requiredEnvironment: ["HASNA_AUTOMATIONS_DIR"],
|
|
7425
7425
|
guarantees: [
|
|
7426
7426
|
"OpenAutomations owns automation specs, run materialization, queue state, DLQ, replay, idempotency, and approvals.",
|
|
7427
7427
|
"OpenLoops may execute claimed actions through explicit command or SDK handoff only.",
|
|
7428
|
-
"OpenLoops may consume exported event envelopes only through explicit
|
|
7428
|
+
"OpenLoops may consume exported event envelopes only through explicit routes create commands.",
|
|
7429
7429
|
"Workers must complete or fail actions by action id and runner id so OpenAutomations can enforce queue leases."
|
|
7430
7430
|
],
|
|
7431
7431
|
nonGoals: [
|
package/dist/types.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface OpenAutomationsRuntimeBinding {
|
|
|
55
55
|
failCommand: "automations queue fail";
|
|
56
56
|
eventHandoff: {
|
|
57
57
|
envelopeCommand: "automations webhooks event";
|
|
58
|
-
handlerCommand: "loops
|
|
58
|
+
handlerCommand: "loops routes create generic";
|
|
59
59
|
pipeExample: string;
|
|
60
60
|
boundary: string;
|
|
61
61
|
};
|