@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/index.js
CHANGED
|
@@ -3962,6 +3962,257 @@ class Store {
|
|
|
3962
3962
|
this.db.close();
|
|
3963
3963
|
}
|
|
3964
3964
|
}
|
|
3965
|
+
// package.json
|
|
3966
|
+
var package_default = {
|
|
3967
|
+
name: "@hasna/loops",
|
|
3968
|
+
version: "0.4.1",
|
|
3969
|
+
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
3970
|
+
type: "module",
|
|
3971
|
+
main: "dist/index.js",
|
|
3972
|
+
types: "dist/index.d.ts",
|
|
3973
|
+
bin: {
|
|
3974
|
+
loops: "dist/cli/index.js",
|
|
3975
|
+
"loops-daemon": "dist/daemon/index.js",
|
|
3976
|
+
"loops-api": "dist/api/index.js",
|
|
3977
|
+
"loops-runner": "dist/runner/index.js",
|
|
3978
|
+
"loops-mcp": "dist/mcp/index.js"
|
|
3979
|
+
},
|
|
3980
|
+
exports: {
|
|
3981
|
+
".": {
|
|
3982
|
+
types: "./dist/index.d.ts",
|
|
3983
|
+
import: "./dist/index.js"
|
|
3984
|
+
},
|
|
3985
|
+
"./sdk": {
|
|
3986
|
+
types: "./dist/sdk/index.d.ts",
|
|
3987
|
+
import: "./dist/sdk/index.js"
|
|
3988
|
+
},
|
|
3989
|
+
"./mcp": {
|
|
3990
|
+
types: "./dist/mcp/index.d.ts",
|
|
3991
|
+
import: "./dist/mcp/index.js"
|
|
3992
|
+
},
|
|
3993
|
+
"./api": {
|
|
3994
|
+
types: "./dist/api/index.d.ts",
|
|
3995
|
+
import: "./dist/api/index.js"
|
|
3996
|
+
},
|
|
3997
|
+
"./runner": {
|
|
3998
|
+
types: "./dist/runner/index.d.ts",
|
|
3999
|
+
import: "./dist/runner/index.js"
|
|
4000
|
+
},
|
|
4001
|
+
"./mode": {
|
|
4002
|
+
types: "./dist/lib/mode.d.ts",
|
|
4003
|
+
import: "./dist/lib/mode.js"
|
|
4004
|
+
},
|
|
4005
|
+
"./storage": {
|
|
4006
|
+
types: "./dist/lib/store.d.ts",
|
|
4007
|
+
import: "./dist/lib/store.js"
|
|
4008
|
+
}
|
|
4009
|
+
},
|
|
4010
|
+
files: [
|
|
4011
|
+
"dist",
|
|
4012
|
+
"README.md",
|
|
4013
|
+
"CHANGELOG.md",
|
|
4014
|
+
"docs",
|
|
4015
|
+
"LICENSE"
|
|
4016
|
+
],
|
|
4017
|
+
scripts: {
|
|
4018
|
+
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",
|
|
4019
|
+
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
4020
|
+
typecheck: "tsc --noEmit",
|
|
4021
|
+
test: "bun test",
|
|
4022
|
+
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
4023
|
+
prepare: "test -d dist || bun run build",
|
|
4024
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
4025
|
+
"dev:daemon": "bun run src/daemon/index.ts",
|
|
4026
|
+
prepublishOnly: "bun run build && bun run test:boundary"
|
|
4027
|
+
},
|
|
4028
|
+
keywords: [
|
|
4029
|
+
"loops",
|
|
4030
|
+
"scheduler",
|
|
4031
|
+
"daemon",
|
|
4032
|
+
"agents",
|
|
4033
|
+
"claude",
|
|
4034
|
+
"cursor",
|
|
4035
|
+
"codewith",
|
|
4036
|
+
"opencode",
|
|
4037
|
+
"cli"
|
|
4038
|
+
],
|
|
4039
|
+
repository: {
|
|
4040
|
+
type: "git",
|
|
4041
|
+
url: "git+https://github.com/hasna/loops.git"
|
|
4042
|
+
},
|
|
4043
|
+
homepage: "https://github.com/hasna/loops#readme",
|
|
4044
|
+
bugs: {
|
|
4045
|
+
url: "https://github.com/hasna/loops/issues"
|
|
4046
|
+
},
|
|
4047
|
+
author: "Hasna <andrei@hasna.com>",
|
|
4048
|
+
license: "Apache-2.0",
|
|
4049
|
+
engines: {
|
|
4050
|
+
bun: ">=1.0.0"
|
|
4051
|
+
},
|
|
4052
|
+
dependencies: {
|
|
4053
|
+
"@hasna/events": "^0.1.9",
|
|
4054
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
4055
|
+
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
4056
|
+
ai: "6.0.204",
|
|
4057
|
+
commander: "^13.1.0",
|
|
4058
|
+
zod: "4.4.3"
|
|
4059
|
+
},
|
|
4060
|
+
optionalDependencies: {
|
|
4061
|
+
"@hasna/machines": "0.0.49"
|
|
4062
|
+
},
|
|
4063
|
+
devDependencies: {
|
|
4064
|
+
"@types/bun": "latest",
|
|
4065
|
+
typescript: "^5.7.3"
|
|
4066
|
+
},
|
|
4067
|
+
publishConfig: {
|
|
4068
|
+
registry: "https://registry.npmjs.org",
|
|
4069
|
+
access: "public"
|
|
4070
|
+
}
|
|
4071
|
+
};
|
|
4072
|
+
|
|
4073
|
+
// src/lib/version.ts
|
|
4074
|
+
function packageVersion() {
|
|
4075
|
+
if (typeof package_default.version !== "string" || package_default.version.trim() === "")
|
|
4076
|
+
throw new Error("package.json version is missing");
|
|
4077
|
+
return package_default.version;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
// src/lib/mode.ts
|
|
4081
|
+
var LOOP_DEPLOYMENT_MODES = ["local", "self_hosted", "cloud"];
|
|
4082
|
+
var MODE_ENV_KEYS = ["LOOPS_MODE", "HASNA_LOOPS_MODE"];
|
|
4083
|
+
var API_URL_ENV_KEYS = ["LOOPS_API_URL", "HASNA_LOOPS_API_URL"];
|
|
4084
|
+
var CLOUD_API_URL_ENV_KEYS = ["LOOPS_CLOUD_API_URL", "HASNA_LOOPS_CLOUD_API_URL"];
|
|
4085
|
+
var DATABASE_URL_ENV_KEYS = ["LOOPS_DATABASE_URL", "HASNA_LOOPS_DATABASE_URL"];
|
|
4086
|
+
var API_TOKEN_ENV_KEYS = ["LOOPS_API_TOKEN", "HASNA_LOOPS_API_TOKEN"];
|
|
4087
|
+
var CLOUD_TOKEN_ENV_KEYS = ["LOOPS_CLOUD_TOKEN", "HASNA_LOOPS_CLOUD_TOKEN"];
|
|
4088
|
+
var TOKEN_ENV_KEYS = [...API_TOKEN_ENV_KEYS, ...CLOUD_TOKEN_ENV_KEYS];
|
|
4089
|
+
function envValue(env, keys) {
|
|
4090
|
+
for (const key of keys) {
|
|
4091
|
+
const value = env[key]?.trim();
|
|
4092
|
+
if (value)
|
|
4093
|
+
return { key, value };
|
|
4094
|
+
}
|
|
4095
|
+
return;
|
|
4096
|
+
}
|
|
4097
|
+
function normalizeLoopDeploymentMode(value) {
|
|
4098
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
4099
|
+
if (normalized === "local")
|
|
4100
|
+
return "local";
|
|
4101
|
+
if (normalized === "self_hosted" || normalized === "selfhosted")
|
|
4102
|
+
return "self_hosted";
|
|
4103
|
+
if (normalized === "cloud" || normalized === "saas")
|
|
4104
|
+
return "cloud";
|
|
4105
|
+
throw new Error(`unsupported OpenLoops deployment mode "${value}"; expected local, self_hosted, or cloud`);
|
|
4106
|
+
}
|
|
4107
|
+
function resolveLoopDeploymentMode(env = process.env) {
|
|
4108
|
+
const explicitMode = envValue(env, MODE_ENV_KEYS);
|
|
4109
|
+
if (explicitMode) {
|
|
4110
|
+
return {
|
|
4111
|
+
deploymentMode: normalizeLoopDeploymentMode(explicitMode.value),
|
|
4112
|
+
source: explicitMode.key
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
const cloudApiUrl = envValue(env, CLOUD_API_URL_ENV_KEYS);
|
|
4116
|
+
if (cloudApiUrl)
|
|
4117
|
+
return { deploymentMode: "cloud", source: cloudApiUrl.key };
|
|
4118
|
+
const apiUrl = envValue(env, API_URL_ENV_KEYS);
|
|
4119
|
+
if (apiUrl)
|
|
4120
|
+
return { deploymentMode: "self_hosted", source: apiUrl.key };
|
|
4121
|
+
const databaseUrl = envValue(env, DATABASE_URL_ENV_KEYS);
|
|
4122
|
+
if (databaseUrl)
|
|
4123
|
+
return { deploymentMode: "self_hosted", source: databaseUrl.key };
|
|
4124
|
+
return { deploymentMode: "local", source: "default" };
|
|
4125
|
+
}
|
|
4126
|
+
function loopControlPlaneConfig(env = process.env) {
|
|
4127
|
+
return {
|
|
4128
|
+
apiUrl: envValue(env, API_URL_ENV_KEYS)?.value,
|
|
4129
|
+
cloudApiUrl: envValue(env, CLOUD_API_URL_ENV_KEYS)?.value,
|
|
4130
|
+
databaseUrlPresent: Boolean(envValue(env, DATABASE_URL_ENV_KEYS)),
|
|
4131
|
+
apiAuthTokenPresent: Boolean(envValue(env, API_TOKEN_ENV_KEYS)),
|
|
4132
|
+
cloudAuthTokenPresent: Boolean(envValue(env, CLOUD_TOKEN_ENV_KEYS)),
|
|
4133
|
+
authTokenPresent: Boolean(envValue(env, TOKEN_ENV_KEYS))
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
function sourceOfTruthForMode(mode) {
|
|
4137
|
+
if (mode === "local")
|
|
4138
|
+
return "local_sqlite";
|
|
4139
|
+
if (mode === "self_hosted")
|
|
4140
|
+
return "self_hosted_control_plane";
|
|
4141
|
+
return "cloud_control_plane";
|
|
4142
|
+
}
|
|
4143
|
+
function displayControlPlaneUrl(value) {
|
|
4144
|
+
if (!value)
|
|
4145
|
+
return;
|
|
4146
|
+
try {
|
|
4147
|
+
const url = new URL(value);
|
|
4148
|
+
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/g, "");
|
|
4149
|
+
return `${url.origin}${path}`;
|
|
4150
|
+
} catch {
|
|
4151
|
+
return "[invalid-url]";
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
function buildDeploymentStatus(opts = {}) {
|
|
4155
|
+
const env = opts.env ?? process.env;
|
|
4156
|
+
const active = resolveLoopDeploymentMode(env);
|
|
4157
|
+
const deploymentMode = opts.perspective ?? active.deploymentMode;
|
|
4158
|
+
const config = loopControlPlaneConfig(env);
|
|
4159
|
+
const rawApiUrl = deploymentMode === "cloud" ? config.cloudApiUrl : config.apiUrl;
|
|
4160
|
+
const apiUrl = displayControlPlaneUrl(rawApiUrl);
|
|
4161
|
+
const controlPlaneKind = deploymentMode === "local" ? "none" : deploymentMode;
|
|
4162
|
+
const deploymentAuthTokenPresent = deploymentMode === "cloud" ? config.cloudAuthTokenPresent : deploymentMode === "self_hosted" ? config.apiAuthTokenPresent : false;
|
|
4163
|
+
const controlPlaneConfigured = deploymentMode === "local" ? false : deploymentMode === "self_hosted" ? Boolean(config.apiUrl || config.databaseUrlPresent) : Boolean(config.cloudApiUrl && deploymentAuthTokenPresent);
|
|
4164
|
+
const warnings = [];
|
|
4165
|
+
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
4166
|
+
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
4167
|
+
}
|
|
4168
|
+
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
4169
|
+
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
4170
|
+
}
|
|
4171
|
+
if (deploymentMode === "cloud" && !config.cloudApiUrl) {
|
|
4172
|
+
warnings.push("cloud mode is a public contract until LOOPS_CLOUD_API_URL is configured");
|
|
4173
|
+
}
|
|
4174
|
+
if (deploymentMode === "cloud" && config.cloudApiUrl && !deploymentAuthTokenPresent) {
|
|
4175
|
+
warnings.push("cloud mode needs LOOPS_CLOUD_TOKEN or HASNA_LOOPS_CLOUD_TOKEN before it can become ready");
|
|
4176
|
+
}
|
|
4177
|
+
if (opts.perspective && opts.perspective !== active.deploymentMode) {
|
|
4178
|
+
warnings.push(`active deployment mode is ${active.deploymentMode}; this is the ${opts.perspective} perspective`);
|
|
4179
|
+
}
|
|
4180
|
+
return {
|
|
4181
|
+
packageVersion: packageVersion(),
|
|
4182
|
+
deploymentMode,
|
|
4183
|
+
activeDeploymentMode: active.deploymentMode,
|
|
4184
|
+
active: deploymentMode === active.deploymentMode,
|
|
4185
|
+
deploymentModeSource: active.source,
|
|
4186
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
4187
|
+
localStore: {
|
|
4188
|
+
role: deploymentMode === "local" ? "authoritative" : "cache_and_spool"
|
|
4189
|
+
},
|
|
4190
|
+
controlPlane: {
|
|
4191
|
+
kind: controlPlaneKind,
|
|
4192
|
+
configured: controlPlaneConfigured,
|
|
4193
|
+
apiUrl,
|
|
4194
|
+
databaseUrlPresent: config.databaseUrlPresent,
|
|
4195
|
+
authTokenPresent: deploymentAuthTokenPresent
|
|
4196
|
+
},
|
|
4197
|
+
runner: {
|
|
4198
|
+
required: deploymentMode !== "local",
|
|
4199
|
+
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
4200
|
+
},
|
|
4201
|
+
warnings
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
4204
|
+
function deploymentStatusLine(status) {
|
|
4205
|
+
const configured = status.controlPlane.kind === "none" ? "none" : String(status.controlPlane.configured);
|
|
4206
|
+
const active = status.active ? "active" : `active=${status.activeDeploymentMode}`;
|
|
4207
|
+
return [
|
|
4208
|
+
`deploymentMode=${status.deploymentMode}`,
|
|
4209
|
+
active,
|
|
4210
|
+
`source=${status.deploymentModeSource}`,
|
|
4211
|
+
`truth=${status.sourceOfTruth}`,
|
|
4212
|
+
`local=${status.localStore.role}`,
|
|
4213
|
+
`control_plane=${configured}`
|
|
4214
|
+
].join(" ");
|
|
4215
|
+
}
|
|
3965
4216
|
|
|
3966
4217
|
// src/mcp/index.ts
|
|
3967
4218
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -7323,105 +7574,6 @@ async function tick(deps) {
|
|
|
7323
7574
|
}
|
|
7324
7575
|
return { claimed, completed, skipped, recovered, expired };
|
|
7325
7576
|
}
|
|
7326
|
-
// package.json
|
|
7327
|
-
var package_default = {
|
|
7328
|
-
name: "@hasna/loops",
|
|
7329
|
-
version: "0.4.0",
|
|
7330
|
-
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7331
|
-
type: "module",
|
|
7332
|
-
main: "dist/index.js",
|
|
7333
|
-
types: "dist/index.d.ts",
|
|
7334
|
-
bin: {
|
|
7335
|
-
loops: "dist/cli/index.js",
|
|
7336
|
-
"loops-daemon": "dist/daemon/index.js",
|
|
7337
|
-
"loops-mcp": "dist/mcp/index.js"
|
|
7338
|
-
},
|
|
7339
|
-
exports: {
|
|
7340
|
-
".": {
|
|
7341
|
-
types: "./dist/index.d.ts",
|
|
7342
|
-
import: "./dist/index.js"
|
|
7343
|
-
},
|
|
7344
|
-
"./sdk": {
|
|
7345
|
-
types: "./dist/sdk/index.d.ts",
|
|
7346
|
-
import: "./dist/sdk/index.js"
|
|
7347
|
-
},
|
|
7348
|
-
"./mcp": {
|
|
7349
|
-
types: "./dist/mcp/index.d.ts",
|
|
7350
|
-
import: "./dist/mcp/index.js"
|
|
7351
|
-
},
|
|
7352
|
-
"./storage": {
|
|
7353
|
-
types: "./dist/lib/store.d.ts",
|
|
7354
|
-
import: "./dist/lib/store.js"
|
|
7355
|
-
}
|
|
7356
|
-
},
|
|
7357
|
-
files: [
|
|
7358
|
-
"dist",
|
|
7359
|
-
"README.md",
|
|
7360
|
-
"CHANGELOG.md",
|
|
7361
|
-
"docs",
|
|
7362
|
-
"LICENSE"
|
|
7363
|
-
],
|
|
7364
|
-
scripts: {
|
|
7365
|
-
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",
|
|
7366
|
-
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
7367
|
-
typecheck: "tsc --noEmit",
|
|
7368
|
-
test: "bun test",
|
|
7369
|
-
prepare: "test -d dist || bun run build",
|
|
7370
|
-
"dev:cli": "bun run src/cli/index.ts",
|
|
7371
|
-
"dev:daemon": "bun run src/daemon/index.ts",
|
|
7372
|
-
prepublishOnly: "bun run build"
|
|
7373
|
-
},
|
|
7374
|
-
keywords: [
|
|
7375
|
-
"loops",
|
|
7376
|
-
"scheduler",
|
|
7377
|
-
"daemon",
|
|
7378
|
-
"agents",
|
|
7379
|
-
"claude",
|
|
7380
|
-
"cursor",
|
|
7381
|
-
"codewith",
|
|
7382
|
-
"opencode",
|
|
7383
|
-
"cli"
|
|
7384
|
-
],
|
|
7385
|
-
repository: {
|
|
7386
|
-
type: "git",
|
|
7387
|
-
url: "git+https://github.com/hasna/loops.git"
|
|
7388
|
-
},
|
|
7389
|
-
homepage: "https://github.com/hasna/loops#readme",
|
|
7390
|
-
bugs: {
|
|
7391
|
-
url: "https://github.com/hasna/loops/issues"
|
|
7392
|
-
},
|
|
7393
|
-
author: "Hasna <andrei@hasna.com>",
|
|
7394
|
-
license: "Apache-2.0",
|
|
7395
|
-
engines: {
|
|
7396
|
-
bun: ">=1.0.0"
|
|
7397
|
-
},
|
|
7398
|
-
dependencies: {
|
|
7399
|
-
"@hasna/events": "^0.1.9",
|
|
7400
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
7401
|
-
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
7402
|
-
ai: "6.0.204",
|
|
7403
|
-
commander: "^13.1.0",
|
|
7404
|
-
zod: "4.4.3"
|
|
7405
|
-
},
|
|
7406
|
-
optionalDependencies: {
|
|
7407
|
-
"@hasna/machines": "0.0.49"
|
|
7408
|
-
},
|
|
7409
|
-
devDependencies: {
|
|
7410
|
-
"@types/bun": "latest",
|
|
7411
|
-
typescript: "^5.7.3"
|
|
7412
|
-
},
|
|
7413
|
-
publishConfig: {
|
|
7414
|
-
registry: "https://registry.npmjs.org",
|
|
7415
|
-
access: "public"
|
|
7416
|
-
}
|
|
7417
|
-
};
|
|
7418
|
-
|
|
7419
|
-
// src/lib/version.ts
|
|
7420
|
-
function packageVersion() {
|
|
7421
|
-
if (typeof package_default.version !== "string" || package_default.version.trim() === "")
|
|
7422
|
-
throw new Error("package.json version is missing");
|
|
7423
|
-
return package_default.version;
|
|
7424
|
-
}
|
|
7425
7577
|
|
|
7426
7578
|
// src/mcp/index.ts
|
|
7427
7579
|
var LOOP_STATUSES = ["active", "paused", "stopped", "expired"];
|
|
@@ -8141,15 +8293,15 @@ function openAutomationsRuntimeBinding(overrides = {}) {
|
|
|
8141
8293
|
failCommand: "automations queue fail",
|
|
8142
8294
|
eventHandoff: {
|
|
8143
8295
|
envelopeCommand: "automations webhooks event",
|
|
8144
|
-
handlerCommand: "loops
|
|
8145
|
-
pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json
|
|
8296
|
+
handlerCommand: "loops routes create generic",
|
|
8297
|
+
pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json routes create generic",
|
|
8146
8298
|
boundary: "Use only for explicit event-envelope workflow handoff. OpenAutomations still owns deterministic automation materialization and queue state; OpenLoops owns workflow invocation."
|
|
8147
8299
|
},
|
|
8148
8300
|
requiredEnvironment: ["HASNA_AUTOMATIONS_DIR"],
|
|
8149
8301
|
guarantees: [
|
|
8150
8302
|
"OpenAutomations owns automation specs, run materialization, queue state, DLQ, replay, idempotency, and approvals.",
|
|
8151
8303
|
"OpenLoops may execute claimed actions through explicit command or SDK handoff only.",
|
|
8152
|
-
"OpenLoops may consume exported event envelopes only through explicit
|
|
8304
|
+
"OpenLoops may consume exported event envelopes only through explicit routes create commands.",
|
|
8153
8305
|
"Workers must complete or fail actions by action id and runner id so OpenAutomations can enforce queue leases."
|
|
8154
8306
|
],
|
|
8155
8307
|
nonGoals: [
|
|
@@ -9955,6 +10107,7 @@ function renderLoopTemplate(id, values, opts = {}) {
|
|
|
9955
10107
|
}
|
|
9956
10108
|
// src/lib/hygiene.ts
|
|
9957
10109
|
import { basename as basename4 } from "path";
|
|
10110
|
+
import { homedir as homedir4 } from "os";
|
|
9958
10111
|
var PROVIDER_TOKENS = new Set([
|
|
9959
10112
|
"codewith",
|
|
9960
10113
|
"claude",
|
|
@@ -9969,11 +10122,14 @@ var PROVIDER_TOKENS = new Set([
|
|
|
9969
10122
|
var REPO_GENERIC_TOKENS = new Set(["repo", "repoops"]);
|
|
9970
10123
|
var CADENCE_SUFFIX_TOKENS = new Set(["hourly", "daily", "weekly", "monthly"]);
|
|
9971
10124
|
var CADENCE_SUFFIX_PATTERN = /^(?:every-?)?\d+(?:s|m|h|d|w)$/;
|
|
10125
|
+
function userHome() {
|
|
10126
|
+
return process.env.HOME || homedir4();
|
|
10127
|
+
}
|
|
9972
10128
|
function slugify(value) {
|
|
9973
10129
|
return value.normalize("NFKD").replace(/[^\w\s.-]/g, "-").replace(/[_\s.:/]+/g, "-").replace(/[^a-zA-Z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
9974
10130
|
}
|
|
9975
10131
|
function repoSlugFromCwd(cwd) {
|
|
9976
|
-
if (!cwd || cwd ===
|
|
10132
|
+
if (!cwd || cwd === userHome())
|
|
9977
10133
|
return "";
|
|
9978
10134
|
if (cwd.includes("/.hasna/loops/"))
|
|
9979
10135
|
return "";
|
|
@@ -10156,7 +10312,7 @@ function commandText(loop) {
|
|
|
10156
10312
|
return [loop.target.command, ...loop.target.args ?? []].join(" ");
|
|
10157
10313
|
}
|
|
10158
10314
|
function scriptNeedles(scriptsDir) {
|
|
10159
|
-
const home =
|
|
10315
|
+
const home = userHome();
|
|
10160
10316
|
const normalized = scriptsDir.replace(/\/+$/g, "");
|
|
10161
10317
|
const values = [
|
|
10162
10318
|
normalized,
|
|
@@ -10174,7 +10330,7 @@ function scriptNeedles(scriptsDir) {
|
|
|
10174
10330
|
return [...new Set(values)];
|
|
10175
10331
|
}
|
|
10176
10332
|
function buildScriptInventoryReport(store, opts = {}) {
|
|
10177
|
-
const scriptsDir = opts.scriptsDir ?? `${
|
|
10333
|
+
const scriptsDir = opts.scriptsDir ?? `${userHome()}/.hasna/loops/scripts`;
|
|
10178
10334
|
const needles = scriptNeedles(scriptsDir);
|
|
10179
10335
|
const loops2 = managedLoops(store, { includeInactive: opts.includeInactive, includeStopped: true, limit: opts.limit });
|
|
10180
10336
|
const scriptBacked = loops2.map((loop) => {
|
|
@@ -10210,6 +10366,7 @@ export {
|
|
|
10210
10366
|
runDoctor,
|
|
10211
10367
|
rollupSummary,
|
|
10212
10368
|
resolveLoopMachine,
|
|
10369
|
+
resolveLoopDeploymentMode,
|
|
10213
10370
|
resolveGoalModel,
|
|
10214
10371
|
renderTodosTaskWorkerVerifierWorkflow,
|
|
10215
10372
|
renderLoopTemplate,
|
|
@@ -10222,8 +10379,10 @@ export {
|
|
|
10222
10379
|
parseDuration,
|
|
10223
10380
|
parseCron,
|
|
10224
10381
|
openAutomationsRuntimeBinding,
|
|
10382
|
+
normalizeLoopDeploymentMode,
|
|
10225
10383
|
nextCronRun,
|
|
10226
10384
|
loops,
|
|
10385
|
+
loopControlPlaneConfig,
|
|
10227
10386
|
listToolsForCli,
|
|
10228
10387
|
listOpenMachines,
|
|
10229
10388
|
listLoopTemplates,
|
|
@@ -10235,6 +10394,7 @@ export {
|
|
|
10235
10394
|
executeTarget,
|
|
10236
10395
|
executeLoopTarget,
|
|
10237
10396
|
executeLoop,
|
|
10397
|
+
deploymentStatusLine,
|
|
10238
10398
|
createLoopsMcpServer,
|
|
10239
10399
|
computeNextAfter,
|
|
10240
10400
|
classifyRunFailure,
|
|
@@ -10242,12 +10402,14 @@ export {
|
|
|
10242
10402
|
buildNameHygieneReport,
|
|
10243
10403
|
buildHealthReport,
|
|
10244
10404
|
buildDuplicateOverlapReport,
|
|
10405
|
+
buildDeploymentStatus,
|
|
10245
10406
|
ValidationError,
|
|
10246
10407
|
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
10247
10408
|
Store,
|
|
10248
10409
|
LoopsClient,
|
|
10249
10410
|
LoopNotFoundError,
|
|
10250
10411
|
LoopArchivedError,
|
|
10412
|
+
LOOP_DEPLOYMENT_MODES,
|
|
10251
10413
|
LOOPS_MCP_TOOLS,
|
|
10252
10414
|
EVENT_WORKER_VERIFIER_TEMPLATE_ID,
|
|
10253
10415
|
CodedError,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare const LOOP_DEPLOYMENT_MODES: readonly ["local", "self_hosted", "cloud"];
|
|
2
|
+
export type LoopDeploymentMode = (typeof LOOP_DEPLOYMENT_MODES)[number];
|
|
3
|
+
export type LoopSourceOfTruth = "local_sqlite" | "self_hosted_control_plane" | "cloud_control_plane";
|
|
4
|
+
export interface LoopModeResolution {
|
|
5
|
+
deploymentMode: LoopDeploymentMode;
|
|
6
|
+
source: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LoopControlPlaneConfig {
|
|
9
|
+
apiUrl?: string;
|
|
10
|
+
cloudApiUrl?: string;
|
|
11
|
+
databaseUrlPresent: boolean;
|
|
12
|
+
apiAuthTokenPresent: boolean;
|
|
13
|
+
cloudAuthTokenPresent: boolean;
|
|
14
|
+
authTokenPresent: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface LoopDeploymentStatus {
|
|
17
|
+
packageVersion: string;
|
|
18
|
+
deploymentMode: LoopDeploymentMode;
|
|
19
|
+
activeDeploymentMode: LoopDeploymentMode;
|
|
20
|
+
active: boolean;
|
|
21
|
+
deploymentModeSource: string;
|
|
22
|
+
sourceOfTruth: LoopSourceOfTruth;
|
|
23
|
+
localStore: {
|
|
24
|
+
role: "authoritative" | "cache_and_spool";
|
|
25
|
+
};
|
|
26
|
+
controlPlane: {
|
|
27
|
+
kind: "none" | "self_hosted" | "cloud";
|
|
28
|
+
configured: boolean;
|
|
29
|
+
apiUrl?: string;
|
|
30
|
+
databaseUrlPresent: boolean;
|
|
31
|
+
authTokenPresent: boolean;
|
|
32
|
+
};
|
|
33
|
+
runner: {
|
|
34
|
+
required: boolean;
|
|
35
|
+
role: "daemon" | "control_plane_worker";
|
|
36
|
+
};
|
|
37
|
+
warnings: string[];
|
|
38
|
+
}
|
|
39
|
+
type Env = Record<string, string | undefined>;
|
|
40
|
+
export declare function normalizeLoopDeploymentMode(value: string): LoopDeploymentMode;
|
|
41
|
+
export declare function resolveLoopDeploymentMode(env?: Env): LoopModeResolution;
|
|
42
|
+
export declare function loopControlPlaneConfig(env?: Env): LoopControlPlaneConfig;
|
|
43
|
+
export declare function buildDeploymentStatus(opts?: {
|
|
44
|
+
env?: Env;
|
|
45
|
+
perspective?: LoopDeploymentMode;
|
|
46
|
+
}): LoopDeploymentStatus;
|
|
47
|
+
export declare function deploymentStatusLine(status: LoopDeploymentStatus): string;
|
|
48
|
+
export {};
|