@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.
@@ -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.0",
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 events handle generic",
7421
- pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json events handle generic",
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 events handle commands.",
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 events handle generic";
58
+ handlerCommand: "loops routes create generic";
59
59
  pipeExample: string;
60
60
  boundary: string;
61
61
  };