@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.
- package/CHANGELOG.md +265 -0
- package/LICENSE +197 -13
- package/README.md +95 -85
- package/dist/api/index.d.ts +11 -0
- package/dist/api/index.js +333 -0
- package/dist/cli/index.js +8013 -6659
- package/dist/daemon/control.d.ts +21 -1
- package/dist/daemon/daemon.d.ts +5 -0
- package/dist/daemon/index.js +2836 -1222
- package/dist/daemon/install.d.ts +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.js +5715 -4541
- package/dist/lib/accounts.d.ts +6 -1
- package/dist/lib/agent-adapter.d.ts +74 -0
- package/dist/lib/backup.d.ts +25 -0
- package/dist/lib/errors.d.ts +21 -0
- package/dist/lib/executor.d.ts +10 -1
- package/dist/lib/goal/metadata.d.ts +16 -0
- package/dist/lib/goal/model-factory.d.ts +1 -0
- package/dist/lib/goal/prompts.d.ts +3 -1
- package/dist/lib/goal/types.d.ts +3 -0
- package/dist/lib/health.d.ts +1 -1
- package/dist/lib/ids.d.ts +8 -1
- package/dist/lib/machines.d.ts +6 -0
- package/dist/lib/mode.d.ts +48 -0
- package/dist/lib/mode.js +260 -0
- package/dist/lib/process-identity.d.ts +16 -0
- package/dist/lib/{schedule.d.ts → recurrence.d.ts} +1 -0
- package/dist/lib/redact.d.ts +21 -0
- package/dist/lib/route/cursors.d.ts +18 -0
- package/dist/lib/route/drain.d.ts +6 -0
- package/dist/lib/route/fields.d.ts +27 -0
- package/dist/lib/route/gates.d.ts +26 -0
- package/dist/lib/route/index.d.ts +18 -0
- package/dist/lib/route/options.d.ts +31 -0
- package/dist/lib/route/parse.d.ts +8 -0
- package/dist/lib/route/pr-review.d.ts +11 -0
- package/dist/lib/route/provider.d.ts +41 -0
- package/dist/lib/route/route-event.d.ts +23 -0
- package/dist/lib/route/route-tasks.d.ts +75 -0
- package/dist/lib/route/throttle.d.ts +47 -0
- package/dist/lib/route/todos-cli.d.ts +21 -0
- package/dist/lib/route/types.d.ts +88 -0
- package/dist/lib/run-artifacts.d.ts +17 -2
- package/dist/lib/run-envelope.d.ts +41 -0
- package/dist/lib/scheduler.d.ts +78 -2
- package/dist/lib/store.d.ts +63 -0
- package/dist/lib/store.js +1007 -287
- package/dist/lib/template-kit.d.ts +70 -0
- package/dist/lib/templates-custom.d.ts +34 -0
- package/dist/lib/templates.d.ts +13 -81
- package/dist/lib/workflow-runner.d.ts +2 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.js +3231 -1382
- package/dist/runner/index.d.ts +9 -0
- package/dist/runner/index.js +295 -0
- package/dist/sdk/index.d.ts +29 -3
- package/dist/sdk/index.js +2743 -1044
- package/dist/test-helpers.d.ts +37 -0
- package/dist/types.d.ts +3 -1
- package/docs/DEPLOYMENT_MODES.md +140 -0
- package/docs/USAGE.md +102 -46
- package/package.json +23 -5
package/dist/lib/accounts.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { AccountRef, AgentProvider } from "../types.js";
|
|
2
2
|
export declare function accountToolForProvider(provider: AgentProvider): string;
|
|
3
3
|
export declare function parseAccountExportLines(output: string): Record<string, string>;
|
|
4
|
-
export declare function resolveAccountEnv(account: AccountRef | undefined, toolHint?: string, env?: NodeJS.ProcessEnv): Record<string, string
|
|
4
|
+
export declare function resolveAccountEnv(account: AccountRef | undefined, toolHint?: string, env?: NodeJS.ProcessEnv): Promise<Record<string, string>>;
|
|
5
|
+
/**
|
|
6
|
+
* Synchronous variant reserved for the synchronous preflight path; execution
|
|
7
|
+
* paths must use the async resolveAccountEnv so the daemon event loop never blocks.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveAccountEnvSync(account: AccountRef | undefined, toolHint?: string, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AgentProvider, AgentSandbox, AgentTarget } from "../types.js";
|
|
2
|
+
export type ProviderPromptChannel = "stdin" | "argv";
|
|
3
|
+
export interface ProviderCapabilities {
|
|
4
|
+
/** Sandbox values the provider CLI accepts; empty when sandboxing is unsupported. */
|
|
5
|
+
sandbox: readonly AgentSandbox[];
|
|
6
|
+
/** Whether the provider runs as a durable background agent instead of a one-shot process. */
|
|
7
|
+
durable: boolean;
|
|
8
|
+
/** Whether the provider can be dispatched to a remote machine transport. */
|
|
9
|
+
remote: boolean;
|
|
10
|
+
/** How the prompt reaches the provider process. */
|
|
11
|
+
promptChannel: ProviderPromptChannel;
|
|
12
|
+
}
|
|
13
|
+
export interface AgentInvocation {
|
|
14
|
+
command: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
/** Prompt delivered on stdin; unset only for argv-channel providers. */
|
|
17
|
+
stdin?: string;
|
|
18
|
+
/** Executables of which at least one must exist besides the command itself. */
|
|
19
|
+
preflightAnyOf?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface ProviderAdapter {
|
|
22
|
+
provider: AgentProvider;
|
|
23
|
+
capabilities: ProviderCapabilities;
|
|
24
|
+
validate(target: AgentTarget, label?: string): void;
|
|
25
|
+
buildInvocation(target: AgentTarget): AgentInvocation;
|
|
26
|
+
}
|
|
27
|
+
export declare const UNSAFE_CODEWITH_DURABLE_EXTRA_ARGS: Set<string>;
|
|
28
|
+
export declare const PROVIDER_ADAPTERS: Record<AgentProvider, ProviderAdapter>;
|
|
29
|
+
export declare const AGENT_PROVIDERS: AgentProvider[];
|
|
30
|
+
export declare function providerAdapter(provider: AgentProvider): ProviderAdapter;
|
|
31
|
+
export interface SpawnCaptureOptions {
|
|
32
|
+
cwd?: string;
|
|
33
|
+
env?: NodeJS.ProcessEnv;
|
|
34
|
+
timeoutMs: number;
|
|
35
|
+
maxOutputBytes?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface CapturedProcessResult {
|
|
38
|
+
status: number | null;
|
|
39
|
+
signal: NodeJS.Signals | null;
|
|
40
|
+
stdout: string;
|
|
41
|
+
stderr: string;
|
|
42
|
+
error?: string;
|
|
43
|
+
timedOut: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function killProcessGroup(pgid: number): void;
|
|
46
|
+
/**
|
|
47
|
+
* Byte-bounded UTF-8 output accumulator shared by the capture paths
|
|
48
|
+
* (spawnCapture here, executeTarget/executeRemoteSpec in executor.ts).
|
|
49
|
+
*
|
|
50
|
+
* - Callers feed decoded strings (`stream.setEncoding("utf8")` keeps
|
|
51
|
+
* multi-byte sequences split across pipe chunks intact); the buffer never
|
|
52
|
+
* decodes chunks itself.
|
|
53
|
+
* - Truncation keeps at most `maxBytes` BYTES of tail, cutting at a UTF-8
|
|
54
|
+
* sequence boundary so the retained text never starts mid-character.
|
|
55
|
+
* - The truncation marker reports the CUMULATIVE dropped byte count.
|
|
56
|
+
* - The accumulated text is scrubbed BEFORE every cut: truncation can bisect
|
|
57
|
+
* a credential so the surviving fragment no longer matches any scrub
|
|
58
|
+
* pattern, while scrubbing first turns the intact token into [SCRUBBED]
|
|
59
|
+
* and the cut then slices through the marker harmlessly (scrubSecrets is
|
|
60
|
+
* idempotent, so store-time re-scrubbing stays safe).
|
|
61
|
+
*/
|
|
62
|
+
export declare class BoundedOutputBuffer {
|
|
63
|
+
private readonly maxBytes;
|
|
64
|
+
private text;
|
|
65
|
+
private truncatedBytes;
|
|
66
|
+
constructor(maxBytes: number);
|
|
67
|
+
append(chunk: string): void;
|
|
68
|
+
value(): string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Async replacement for short spawnSync calls: never blocks the event loop and
|
|
72
|
+
* always enforces an explicit timeout that kills the child's process group.
|
|
73
|
+
*/
|
|
74
|
+
export declare function spawnCapture(command: string, args: string[], opts: SpawnCaptureOptions): Promise<CapturedProcessResult>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface BackupDatabaseOptions {
|
|
2
|
+
/** Why this backup is being taken (e.g. "pre-migration", "daily"). Debounce and retention are tracked per reason. */
|
|
3
|
+
reason: string;
|
|
4
|
+
/** How many backups to retain per reason (newest first). */
|
|
5
|
+
keep?: number;
|
|
6
|
+
/** Source database file; defaults to the live store path. Tests must pass a temp path. */
|
|
7
|
+
dbFile?: string;
|
|
8
|
+
/** Directory for backup files; defaults to `<db dir>/backups`. */
|
|
9
|
+
backupsDir?: string;
|
|
10
|
+
/** Injectable clock for tests. */
|
|
11
|
+
now?: Date;
|
|
12
|
+
}
|
|
13
|
+
export interface BackupDatabaseResult {
|
|
14
|
+
/** Absolute path of the backup that was written (absent when skipped). */
|
|
15
|
+
path?: string;
|
|
16
|
+
skipped: boolean;
|
|
17
|
+
skipReason?: string;
|
|
18
|
+
/** Backups removed by retention pruning. */
|
|
19
|
+
prunedPaths: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Snapshot the loops database with `VACUUM INTO`. Per-reason backups are
|
|
23
|
+
* debounced to at most one per hour and pruned to the `keep` most recent.
|
|
24
|
+
*/
|
|
25
|
+
export declare function backupDatabase(opts: BackupDatabaseOptions): BackupDatabaseResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coded errors thrown by the store and CLI layers. Each class carries a stable
|
|
3
|
+
* machine-readable `.code` so callers can branch on failure kind without
|
|
4
|
+
* parsing human-readable messages.
|
|
5
|
+
*/
|
|
6
|
+
export declare class CodedError extends Error {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
constructor(code: string, message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class LoopNotFoundError extends CodedError {
|
|
11
|
+
constructor(idOrName: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class LoopArchivedError extends CodedError {
|
|
14
|
+
constructor(idOrName: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class AmbiguousNameError extends CodedError {
|
|
17
|
+
constructor(name: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class ValidationError extends CodedError {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
package/dist/lib/executor.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { LanguageModel } from "ai";
|
|
2
|
-
import type { ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
|
|
2
|
+
import type { AgentTarget, ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
|
|
3
|
+
export interface SpawnedProcessInfo {
|
|
4
|
+
pid: number;
|
|
5
|
+
pgid: number;
|
|
6
|
+
processStartedAt: string;
|
|
7
|
+
}
|
|
3
8
|
export interface ExecuteOptions extends PersistGuardOptions {
|
|
4
9
|
maxOutputBytes?: number;
|
|
5
10
|
env?: NodeJS.ProcessEnv;
|
|
@@ -7,6 +12,8 @@ export interface ExecuteOptions extends PersistGuardOptions {
|
|
|
7
12
|
log?: (message: string) => void;
|
|
8
13
|
signal?: AbortSignal;
|
|
9
14
|
onSpawn?: (pid: number) => void;
|
|
15
|
+
/** Children are spawned detached in their own process group, so pgid === pid. */
|
|
16
|
+
onSpawnProcess?: (info: SpawnedProcessInfo) => void;
|
|
10
17
|
machine?: LoopMachineRef;
|
|
11
18
|
machineResolver?: (machine: LoopMachineRef) => LoopMachineRef;
|
|
12
19
|
machineCommandResolver?: (machineId: string, command: string) => MachineCommandPlan;
|
|
@@ -34,6 +41,8 @@ interface MachineCommandPlan {
|
|
|
34
41
|
args: string[];
|
|
35
42
|
source: string;
|
|
36
43
|
}
|
|
44
|
+
/** Exported for tests: resolves the default idle watchdog for agent targets. */
|
|
45
|
+
export declare function defaultAgentIdleTimeoutMs(target: AgentTarget, opts: ExecuteOptions): number | undefined;
|
|
37
46
|
export declare function preflightTarget(target: ExecutableTarget, metadata?: ExecutionMetadata, opts?: ExecuteOptions): PreflightResult;
|
|
38
47
|
export declare function executeTarget(target: ExecutableTarget, metadata?: ExecutionMetadata, opts?: ExecuteOptions): Promise<ExecutorResult>;
|
|
39
48
|
export declare function executeLoop(loop: Loop, run: LoopRun, opts?: ExecuteOptions): Promise<ExecutorResult>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Loop, LoopRun, WorkflowSpec } from "../../types.js";
|
|
2
|
+
import type { Goal, GoalExecutionContext, GoalPlanNode } from "./types.js";
|
|
3
|
+
export interface GoalContextParts {
|
|
4
|
+
loop?: Pick<Loop, "id" | "name">;
|
|
5
|
+
loopRun?: Pick<LoopRun, "id" | "scheduledFor">;
|
|
6
|
+
scheduledFor?: string;
|
|
7
|
+
workflow?: Pick<WorkflowSpec, "id" | "name">;
|
|
8
|
+
workflowRunId?: string;
|
|
9
|
+
workflowStepId?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Single source for the goal execution context previously copy-pasted across runners. */
|
|
12
|
+
export declare function goalExecutionContext(parts: GoalContextParts): GoalExecutionContext;
|
|
13
|
+
/** Executor metadata (LOOPS_* env) derived from a goal execution context; loopRunId maps to runId. */
|
|
14
|
+
export declare function executionMetadata(context: GoalExecutionContext | undefined, goal?: Pick<Goal, "goalId" | "objective">, node?: Pick<GoalPlanNode, "key">): Record<string, string | undefined>;
|
|
15
|
+
/** Exposes the executing plan node to child processes so command targets can honor node objectives. */
|
|
16
|
+
export declare function withGoalNodeEnv(env: NodeJS.ProcessEnv | undefined, node: Pick<GoalPlanNode, "key" | "objective">): NodeJS.ProcessEnv;
|
|
@@ -7,3 +7,4 @@ export interface ResolveGoalModelOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const DEFAULT_GOAL_MODEL = "openai/gpt-4o-mini";
|
|
9
9
|
export declare function resolveGoalModel(opts?: ResolveGoalModelOptions): LanguageModel;
|
|
10
|
+
export declare function resolveGoalVerifierModel(opts?: ResolveGoalModelOptions): LanguageModel;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { ExecutableTarget } from "../../types.js";
|
|
1
2
|
import type { Goal, GoalPlanNode, GoalSpec } from "./types.js";
|
|
2
3
|
export declare function planPrompt(spec: GoalSpec): string;
|
|
3
|
-
export declare function iterationPrompt(goal: Goal,
|
|
4
|
+
export declare function iterationPrompt(goal: Pick<Goal, "objective">, node: Pick<GoalPlanNode, "key" | "objective">): string;
|
|
5
|
+
export declare function goalNodeTarget(target: ExecutableTarget, goal: Pick<Goal, "objective">, node: Pick<GoalPlanNode, "key" | "objective">): ExecutableTarget;
|
|
4
6
|
export declare function achievementPrompt(goal: Goal, nodes: GoalPlanNode[], evidence: string[]): string;
|
package/dist/lib/goal/types.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface GoalSpec {
|
|
|
11
11
|
objective: string;
|
|
12
12
|
tokenBudget?: number;
|
|
13
13
|
model?: string;
|
|
14
|
+
/** Independent model for the achievement audit; defaults to LOOPS_GOAL_VERIFIER_MODEL, then the planner model. */
|
|
15
|
+
verifierModel?: string;
|
|
14
16
|
maxTurns?: number;
|
|
15
17
|
maxTokens?: number;
|
|
16
18
|
autoExecute?: GoalAutoExecute;
|
|
@@ -105,6 +107,7 @@ export interface GoalExecutionContext {
|
|
|
105
107
|
export interface RunGoalOptions {
|
|
106
108
|
store?: Store;
|
|
107
109
|
model?: LanguageModel;
|
|
110
|
+
verifierModel?: LanguageModel;
|
|
108
111
|
target?: ExecutableTarget;
|
|
109
112
|
executeNode?: (node: GoalPlanNode, metadata: Record<string, string | undefined>) => Promise<ExecutorResult>;
|
|
110
113
|
env?: NodeJS.ProcessEnv;
|
package/dist/lib/health.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Loop, LoopRun } from "../types.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
|
-
export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
|
|
3
|
+
export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "skipped_previous_active" | "circuit_breaker" | "unknown";
|
|
4
4
|
export interface RunFailureSignal {
|
|
5
5
|
classification: RunFailureClassification;
|
|
6
6
|
fingerprint: string;
|
package/dist/lib/ids.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Generate a 128-bit, time-sortable identifier (ULID-like): a 48-bit
|
|
3
|
+
* millisecond timestamp prefix (12 lowercase hex chars) followed by 80 random
|
|
4
|
+
* bits (20 lowercase hex chars). Fixed width keeps lexicographic order aligned
|
|
5
|
+
* with creation time, and plain lowercase hex stays compatible with the
|
|
6
|
+
* existing TEXT primary keys written by earlier releases.
|
|
7
|
+
*/
|
|
8
|
+
export declare function genId(): string;
|
|
2
9
|
export declare function nowIso(): string;
|
package/dist/lib/machines.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { LoopMachineRef } from "../types.js";
|
|
2
|
+
export interface MachineCommandPlan {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
source: string;
|
|
6
|
+
}
|
|
2
7
|
export interface OpenMachineSummary {
|
|
3
8
|
id: string;
|
|
4
9
|
hostname?: string;
|
|
@@ -13,4 +18,5 @@ export interface OpenMachineSummary {
|
|
|
13
18
|
}
|
|
14
19
|
export declare function listOpenMachines(): OpenMachineSummary[];
|
|
15
20
|
export declare function resolveLoopMachine(machineId: string): LoopMachineRef;
|
|
21
|
+
export declare function resolveMachineCommand(machineId: string, command: string): MachineCommandPlan;
|
|
16
22
|
export declare function refreshLoopMachine(machine: LoopMachineRef): LoopMachineRef;
|
|
@@ -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 {};
|
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
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const START_TIME_TOLERANCE_MS = 5000;
|
|
2
|
+
export declare function procStatFields(path: string): string[] | undefined;
|
|
3
|
+
export declare function processStartTimeMs(pid: number): number | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Lenient fingerprint comparison: unknown data (no recorded start time, an
|
|
6
|
+
* unparsable record, or an unresolvable actual start time) counts as a match.
|
|
7
|
+
* Use for non-destructive liveness checks where "possibly alive" is the safe
|
|
8
|
+
* answer (pidfile checks, deciding not to take over a lease).
|
|
9
|
+
*/
|
|
10
|
+
export declare function sameProcessStart(recorded: string | number | undefined, actualMs: number | undefined, toleranceMs?: number): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Strict fingerprint comparison for destructive paths (signalling processes):
|
|
13
|
+
* true only when both the recorded and actual start times resolve and match
|
|
14
|
+
* within tolerance. Unverifiable identity fails closed as a mismatch.
|
|
15
|
+
*/
|
|
16
|
+
export declare function verifiedProcessStart(recorded: string | number | undefined, actualMs: number | undefined, toleranceMs?: number): boolean;
|
|
@@ -16,6 +16,7 @@ export interface DuePlan {
|
|
|
16
16
|
slots: string[];
|
|
17
17
|
skippedToNextRunAt?: string;
|
|
18
18
|
}
|
|
19
|
+
export declare const MAX_CATCH_UP_SLOTS = 1000;
|
|
19
20
|
export declare function dueSlots(loop: Loop, now: Date): DuePlan;
|
|
20
21
|
export declare function parseDuration(input: string): number;
|
|
21
22
|
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-path secret scrubbing. `scrubSecrets` removes credential material from
|
|
3
|
+
* text before it is persisted (run stdout/stderr/error, goal evidence, raw
|
|
4
|
+
* model responses). Display-layer truncation/redaction lives in format.ts;
|
|
5
|
+
* this module is about never writing recoverable secrets to disk at all.
|
|
6
|
+
*
|
|
7
|
+
* The function is idempotent (`scrubSecrets(scrubSecrets(x)) === scrubSecrets(x)`)
|
|
8
|
+
* and linear over the input, so it stays fast on multi-hundred-KB outputs.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Replace credential tokens in `text` with "[SCRUBBED]". Idempotent and safe
|
|
12
|
+
* to run on JSON-encoded payloads (replacements never introduce quotes).
|
|
13
|
+
*/
|
|
14
|
+
export declare function scrubSecrets(text: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Recursively scrub the string leaves of a JSON-serializable value. Run this
|
|
17
|
+
* BEFORE `JSON.stringify` when persisting structured payloads (goal evidence,
|
|
18
|
+
* raw model responses): stringify escapes quotes, which would otherwise hide
|
|
19
|
+
* quoted secrets from the flat text patterns above.
|
|
20
|
+
*/
|
|
21
|
+
export declare function scrubSecretsDeep<T>(value: T): T;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RouteSelection<T> {
|
|
2
|
+
selected: T[];
|
|
3
|
+
cursor: {
|
|
4
|
+
key: string;
|
|
5
|
+
total: number;
|
|
6
|
+
maxActions: number;
|
|
7
|
+
previousFingerprint?: string;
|
|
8
|
+
startIndex: number;
|
|
9
|
+
lastFingerprint?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function writeRouteCursor(key: string, lastFingerprint: string | undefined): void;
|
|
13
|
+
export declare function writeRouteEvidence(kind: string, value: unknown, evidenceDir: string | undefined): string | undefined;
|
|
14
|
+
export declare function selectRouteItems<T>(items: T[], maxActions: number, cursorKey: string, fingerprintOf: (item: T) => string): RouteSelection<T>;
|
|
15
|
+
export declare function routeCursorKey(kind: "health" | "hygiene", parts: unknown[], opts: {
|
|
16
|
+
autoRoute?: boolean;
|
|
17
|
+
routeProjectPath?: string;
|
|
18
|
+
}): string;
|