@hasna/loops 0.3.21 → 0.3.22
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/dist/cli/index.js +107 -15
- package/dist/daemon/index.js +57 -6
- package/dist/index.js +56 -5
- package/dist/sdk/index.js +56 -5
- package/docs/USAGE.md +22 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2833,6 +2833,7 @@ function commandSpec(target) {
|
|
|
2833
2833
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2834
2834
|
account: agentTarget.account,
|
|
2835
2835
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2836
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2836
2837
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2837
2838
|
stdin: agentTarget.prompt,
|
|
2838
2839
|
allowlist: agentTarget.allowlist
|
|
@@ -2914,6 +2915,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2914
2915
|
if (spec.preflightAnyOf?.length) {
|
|
2915
2916
|
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
2916
2917
|
}
|
|
2918
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2919
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2920
|
+
}
|
|
2917
2921
|
return lines.join(`
|
|
2918
2922
|
`);
|
|
2919
2923
|
}
|
|
@@ -2929,6 +2933,29 @@ function transportEnv(opts) {
|
|
|
2929
2933
|
env.PATH = normalizeExecutionPath(env);
|
|
2930
2934
|
return env;
|
|
2931
2935
|
}
|
|
2936
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2937
|
+
if (!spec.nativeAuthProfile)
|
|
2938
|
+
return;
|
|
2939
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2940
|
+
return;
|
|
2941
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2942
|
+
encoding: "utf8",
|
|
2943
|
+
env,
|
|
2944
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2945
|
+
timeout: 15000
|
|
2946
|
+
});
|
|
2947
|
+
if (result.error) {
|
|
2948
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2949
|
+
}
|
|
2950
|
+
if ((result.status ?? 1) !== 0) {
|
|
2951
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2952
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2953
|
+
}
|
|
2954
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2955
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2956
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2932
2959
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2933
2960
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2934
2961
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -3070,6 +3097,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
3070
3097
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
3071
3098
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
3072
3099
|
}
|
|
3100
|
+
preflightNativeAuthProfile(spec, env);
|
|
3073
3101
|
return {
|
|
3074
3102
|
command: spec.command,
|
|
3075
3103
|
accountProfile: spec.account?.profile,
|
|
@@ -3111,6 +3139,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3111
3139
|
durationMs: 0
|
|
3112
3140
|
};
|
|
3113
3141
|
}
|
|
3142
|
+
try {
|
|
3143
|
+
preflightNativeAuthProfile(spec, env);
|
|
3144
|
+
} catch (err) {
|
|
3145
|
+
return {
|
|
3146
|
+
status: "failed",
|
|
3147
|
+
stdout: "",
|
|
3148
|
+
stderr: "",
|
|
3149
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3150
|
+
startedAt,
|
|
3151
|
+
finishedAt: nowIso(),
|
|
3152
|
+
durationMs: 0
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3114
3155
|
const child = spawn(spec.command, spec.args, {
|
|
3115
3156
|
cwd: spec.cwd,
|
|
3116
3157
|
env,
|
|
@@ -3767,11 +3808,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3767
3808
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3768
3809
|
}
|
|
3769
3810
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3770
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3811
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3812
|
+
try {
|
|
3813
|
+
return {
|
|
3814
|
+
workflowStepId: step.id,
|
|
3815
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3816
|
+
workflowId: workflow.id,
|
|
3817
|
+
workflowName: workflow.name,
|
|
3818
|
+
workflowStepId: step.id
|
|
3819
|
+
}, opts)
|
|
3820
|
+
};
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3823
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3824
|
+
}
|
|
3825
|
+
});
|
|
3775
3826
|
}
|
|
3776
3827
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3777
3828
|
if (loop.target.type !== "workflow") {
|
|
@@ -5092,7 +5143,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5092
5143
|
// package.json
|
|
5093
5144
|
var package_default = {
|
|
5094
5145
|
name: "@hasna/loops",
|
|
5095
|
-
version: "0.3.
|
|
5146
|
+
version: "0.3.22",
|
|
5096
5147
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5097
5148
|
type: "module",
|
|
5098
5149
|
main: "dist/index.js",
|
|
@@ -5576,6 +5627,41 @@ function print(value, human) {
|
|
|
5576
5627
|
else
|
|
5577
5628
|
console.log(human);
|
|
5578
5629
|
}
|
|
5630
|
+
function printCreatedLoop(loop, human, preflight) {
|
|
5631
|
+
if (preflight !== undefined)
|
|
5632
|
+
print({ loop: publicLoop(loop), preflight }, human);
|
|
5633
|
+
else
|
|
5634
|
+
print(publicLoop(loop), human);
|
|
5635
|
+
}
|
|
5636
|
+
function preflightFailed(error, context) {
|
|
5637
|
+
if (!isJson())
|
|
5638
|
+
throw error;
|
|
5639
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5640
|
+
print({
|
|
5641
|
+
ok: false,
|
|
5642
|
+
created: false,
|
|
5643
|
+
preflight: {
|
|
5644
|
+
ok: false,
|
|
5645
|
+
error: redact(message, 320)
|
|
5646
|
+
},
|
|
5647
|
+
...context
|
|
5648
|
+
});
|
|
5649
|
+
process.exit(1);
|
|
5650
|
+
}
|
|
5651
|
+
function preflightLoopTarget(target, context, metadata, opts) {
|
|
5652
|
+
try {
|
|
5653
|
+
return preflightTarget(target, metadata, opts);
|
|
5654
|
+
} catch (error) {
|
|
5655
|
+
preflightFailed(error, context);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
function preflightStoredWorkflow(workflow, context, opts) {
|
|
5659
|
+
try {
|
|
5660
|
+
return preflightWorkflow(workflow, opts);
|
|
5661
|
+
} catch (error) {
|
|
5662
|
+
preflightFailed(error, context);
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5579
5665
|
function printTextOutput(value) {
|
|
5580
5666
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
5581
5667
|
console.log(line);
|
|
@@ -6020,7 +6106,7 @@ function permissionModeFromOpts(opts, provider) {
|
|
|
6020
6106
|
return mode;
|
|
6021
6107
|
}
|
|
6022
6108
|
var create = program.command("create").description("create loops");
|
|
6023
|
-
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell"))))).action((name, opts) => {
|
|
6109
|
+
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
6024
6110
|
const store = new Store;
|
|
6025
6111
|
try {
|
|
6026
6112
|
const target = {
|
|
@@ -6031,13 +6117,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
6031
6117
|
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
|
|
6032
6118
|
account: accountFromOpts(opts)
|
|
6033
6119
|
};
|
|
6034
|
-
const
|
|
6035
|
-
|
|
6120
|
+
const input = baseCreateInput(name, opts, target);
|
|
6121
|
+
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "command" }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
6122
|
+
const loop = store.createLoop(input);
|
|
6123
|
+
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
6036
6124
|
} finally {
|
|
6037
6125
|
store.close();
|
|
6038
6126
|
}
|
|
6039
6127
|
});
|
|
6040
|
-
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe"))))).action((name, opts) => {
|
|
6128
|
+
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
6041
6129
|
const provider = opts.provider;
|
|
6042
6130
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
6043
6131
|
throw new Error("unsupported provider");
|
|
@@ -6063,13 +6151,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
6063
6151
|
allowlist: allowlistFromOpts(opts),
|
|
6064
6152
|
account: accountFromOpts(opts)
|
|
6065
6153
|
};
|
|
6066
|
-
const
|
|
6067
|
-
|
|
6154
|
+
const input = baseCreateInput(name, opts, target);
|
|
6155
|
+
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
6156
|
+
const loop = store.createLoop(input);
|
|
6157
|
+
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
6068
6158
|
} finally {
|
|
6069
6159
|
store.close();
|
|
6070
6160
|
}
|
|
6071
6161
|
});
|
|
6072
|
-
addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name")))).action((name, opts) => {
|
|
6162
|
+
addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
|
|
6073
6163
|
const store = new Store;
|
|
6074
6164
|
try {
|
|
6075
6165
|
const workflow = store.requireWorkflow(opts.workflow);
|
|
@@ -6077,8 +6167,10 @@ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <na
|
|
|
6077
6167
|
type: "workflow",
|
|
6078
6168
|
workflowId: workflow.id
|
|
6079
6169
|
};
|
|
6080
|
-
const
|
|
6081
|
-
|
|
6170
|
+
const input = baseCreateInput(name, opts, target);
|
|
6171
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflow, { name, type: "workflow", workflow: workflow.name }, { machine: input.machine }) : undefined;
|
|
6172
|
+
const loop = store.createLoop(input);
|
|
6173
|
+
printCreatedLoop(loop, `created workflow loop ${loop.id} (${loop.name}) workflow=${workflow.name} next=${loop.nextRunAt}`, preflight);
|
|
6082
6174
|
} finally {
|
|
6083
6175
|
store.close();
|
|
6084
6176
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -2724,6 +2724,7 @@ function commandSpec(target) {
|
|
|
2724
2724
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2725
2725
|
account: agentTarget.account,
|
|
2726
2726
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2727
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2727
2728
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2728
2729
|
stdin: agentTarget.prompt,
|
|
2729
2730
|
allowlist: agentTarget.allowlist
|
|
@@ -2805,6 +2806,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2805
2806
|
if (spec.preflightAnyOf?.length) {
|
|
2806
2807
|
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
2807
2808
|
}
|
|
2809
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2810
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2811
|
+
}
|
|
2808
2812
|
return lines.join(`
|
|
2809
2813
|
`);
|
|
2810
2814
|
}
|
|
@@ -2820,6 +2824,29 @@ function transportEnv(opts) {
|
|
|
2820
2824
|
env.PATH = normalizeExecutionPath(env);
|
|
2821
2825
|
return env;
|
|
2822
2826
|
}
|
|
2827
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2828
|
+
if (!spec.nativeAuthProfile)
|
|
2829
|
+
return;
|
|
2830
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2831
|
+
return;
|
|
2832
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2833
|
+
encoding: "utf8",
|
|
2834
|
+
env,
|
|
2835
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2836
|
+
timeout: 15000
|
|
2837
|
+
});
|
|
2838
|
+
if (result.error) {
|
|
2839
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2840
|
+
}
|
|
2841
|
+
if ((result.status ?? 1) !== 0) {
|
|
2842
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2843
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2844
|
+
}
|
|
2845
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2846
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2847
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2823
2850
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2824
2851
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2825
2852
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -2961,6 +2988,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2961
2988
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2962
2989
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2963
2990
|
}
|
|
2991
|
+
preflightNativeAuthProfile(spec, env);
|
|
2964
2992
|
return {
|
|
2965
2993
|
command: spec.command,
|
|
2966
2994
|
accountProfile: spec.account?.profile,
|
|
@@ -3002,6 +3030,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3002
3030
|
durationMs: 0
|
|
3003
3031
|
};
|
|
3004
3032
|
}
|
|
3033
|
+
try {
|
|
3034
|
+
preflightNativeAuthProfile(spec, env);
|
|
3035
|
+
} catch (err) {
|
|
3036
|
+
return {
|
|
3037
|
+
status: "failed",
|
|
3038
|
+
stdout: "",
|
|
3039
|
+
stderr: "",
|
|
3040
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3041
|
+
startedAt,
|
|
3042
|
+
finishedAt: nowIso(),
|
|
3043
|
+
durationMs: 0
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3005
3046
|
const child = spawn(spec.command, spec.args, {
|
|
3006
3047
|
cwd: spec.cwd,
|
|
3007
3048
|
env,
|
|
@@ -3658,11 +3699,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3658
3699
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3659
3700
|
}
|
|
3660
3701
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3661
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3702
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3703
|
+
try {
|
|
3704
|
+
return {
|
|
3705
|
+
workflowStepId: step.id,
|
|
3706
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3707
|
+
workflowId: workflow.id,
|
|
3708
|
+
workflowName: workflow.name,
|
|
3709
|
+
workflowStepId: step.id
|
|
3710
|
+
}, opts)
|
|
3711
|
+
};
|
|
3712
|
+
} catch (error) {
|
|
3713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3714
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3715
|
+
}
|
|
3716
|
+
});
|
|
3666
3717
|
}
|
|
3667
3718
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3668
3719
|
if (loop.target.type !== "workflow") {
|
|
@@ -4419,7 +4470,7 @@ function enableStartup(result) {
|
|
|
4419
4470
|
// package.json
|
|
4420
4471
|
var package_default = {
|
|
4421
4472
|
name: "@hasna/loops",
|
|
4422
|
-
version: "0.3.
|
|
4473
|
+
version: "0.3.22",
|
|
4423
4474
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4424
4475
|
type: "module",
|
|
4425
4476
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -2714,6 +2714,7 @@ function commandSpec(target) {
|
|
|
2714
2714
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2715
2715
|
account: agentTarget.account,
|
|
2716
2716
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2717
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2717
2718
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2718
2719
|
stdin: agentTarget.prompt,
|
|
2719
2720
|
allowlist: agentTarget.allowlist
|
|
@@ -2795,6 +2796,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2795
2796
|
if (spec.preflightAnyOf?.length) {
|
|
2796
2797
|
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
2797
2798
|
}
|
|
2799
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2800
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2801
|
+
}
|
|
2798
2802
|
return lines.join(`
|
|
2799
2803
|
`);
|
|
2800
2804
|
}
|
|
@@ -2810,6 +2814,29 @@ function transportEnv(opts) {
|
|
|
2810
2814
|
env.PATH = normalizeExecutionPath(env);
|
|
2811
2815
|
return env;
|
|
2812
2816
|
}
|
|
2817
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2818
|
+
if (!spec.nativeAuthProfile)
|
|
2819
|
+
return;
|
|
2820
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2821
|
+
return;
|
|
2822
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2823
|
+
encoding: "utf8",
|
|
2824
|
+
env,
|
|
2825
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2826
|
+
timeout: 15000
|
|
2827
|
+
});
|
|
2828
|
+
if (result.error) {
|
|
2829
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2830
|
+
}
|
|
2831
|
+
if ((result.status ?? 1) !== 0) {
|
|
2832
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2833
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2834
|
+
}
|
|
2835
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2836
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2837
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2813
2840
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2814
2841
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2815
2842
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -2951,6 +2978,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2951
2978
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2952
2979
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2953
2980
|
}
|
|
2981
|
+
preflightNativeAuthProfile(spec, env);
|
|
2954
2982
|
return {
|
|
2955
2983
|
command: spec.command,
|
|
2956
2984
|
accountProfile: spec.account?.profile,
|
|
@@ -2992,6 +3020,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
2992
3020
|
durationMs: 0
|
|
2993
3021
|
};
|
|
2994
3022
|
}
|
|
3023
|
+
try {
|
|
3024
|
+
preflightNativeAuthProfile(spec, env);
|
|
3025
|
+
} catch (err) {
|
|
3026
|
+
return {
|
|
3027
|
+
status: "failed",
|
|
3028
|
+
stdout: "",
|
|
3029
|
+
stderr: "",
|
|
3030
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3031
|
+
startedAt,
|
|
3032
|
+
finishedAt: nowIso(),
|
|
3033
|
+
durationMs: 0
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
2995
3036
|
const child = spawn(spec.command, spec.args, {
|
|
2996
3037
|
cwd: spec.cwd,
|
|
2997
3038
|
env,
|
|
@@ -3648,11 +3689,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3648
3689
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3649
3690
|
}
|
|
3650
3691
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3651
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3692
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3693
|
+
try {
|
|
3694
|
+
return {
|
|
3695
|
+
workflowStepId: step.id,
|
|
3696
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3697
|
+
workflowId: workflow.id,
|
|
3698
|
+
workflowName: workflow.name,
|
|
3699
|
+
workflowStepId: step.id
|
|
3700
|
+
}, opts)
|
|
3701
|
+
};
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3704
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3705
|
+
}
|
|
3706
|
+
});
|
|
3656
3707
|
}
|
|
3657
3708
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3658
3709
|
if (loop.target.type !== "workflow") {
|
package/dist/sdk/index.js
CHANGED
|
@@ -2714,6 +2714,7 @@ function commandSpec(target) {
|
|
|
2714
2714
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2715
2715
|
account: agentTarget.account,
|
|
2716
2716
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2717
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2717
2718
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2718
2719
|
stdin: agentTarget.prompt,
|
|
2719
2720
|
allowlist: agentTarget.allowlist
|
|
@@ -2795,6 +2796,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2795
2796
|
if (spec.preflightAnyOf?.length) {
|
|
2796
2797
|
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
2797
2798
|
}
|
|
2799
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2800
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2801
|
+
}
|
|
2798
2802
|
return lines.join(`
|
|
2799
2803
|
`);
|
|
2800
2804
|
}
|
|
@@ -2810,6 +2814,29 @@ function transportEnv(opts) {
|
|
|
2810
2814
|
env.PATH = normalizeExecutionPath(env);
|
|
2811
2815
|
return env;
|
|
2812
2816
|
}
|
|
2817
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2818
|
+
if (!spec.nativeAuthProfile)
|
|
2819
|
+
return;
|
|
2820
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2821
|
+
return;
|
|
2822
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2823
|
+
encoding: "utf8",
|
|
2824
|
+
env,
|
|
2825
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2826
|
+
timeout: 15000
|
|
2827
|
+
});
|
|
2828
|
+
if (result.error) {
|
|
2829
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2830
|
+
}
|
|
2831
|
+
if ((result.status ?? 1) !== 0) {
|
|
2832
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2833
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2834
|
+
}
|
|
2835
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2836
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2837
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2813
2840
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2814
2841
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2815
2842
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -2951,6 +2978,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2951
2978
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2952
2979
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2953
2980
|
}
|
|
2981
|
+
preflightNativeAuthProfile(spec, env);
|
|
2954
2982
|
return {
|
|
2955
2983
|
command: spec.command,
|
|
2956
2984
|
accountProfile: spec.account?.profile,
|
|
@@ -2992,6 +3020,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
2992
3020
|
durationMs: 0
|
|
2993
3021
|
};
|
|
2994
3022
|
}
|
|
3023
|
+
try {
|
|
3024
|
+
preflightNativeAuthProfile(spec, env);
|
|
3025
|
+
} catch (err) {
|
|
3026
|
+
return {
|
|
3027
|
+
status: "failed",
|
|
3028
|
+
stdout: "",
|
|
3029
|
+
stderr: "",
|
|
3030
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3031
|
+
startedAt,
|
|
3032
|
+
finishedAt: nowIso(),
|
|
3033
|
+
durationMs: 0
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
2995
3036
|
const child = spawn(spec.command, spec.args, {
|
|
2996
3037
|
cwd: spec.cwd,
|
|
2997
3038
|
env,
|
|
@@ -3648,11 +3689,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3648
3689
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3649
3690
|
}
|
|
3650
3691
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3651
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3692
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3693
|
+
try {
|
|
3694
|
+
return {
|
|
3695
|
+
workflowStepId: step.id,
|
|
3696
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3697
|
+
workflowId: workflow.id,
|
|
3698
|
+
workflowName: workflow.name,
|
|
3699
|
+
workflowStepId: step.id
|
|
3700
|
+
}, opts)
|
|
3701
|
+
};
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3704
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3705
|
+
}
|
|
3706
|
+
});
|
|
3656
3707
|
}
|
|
3657
3708
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3658
3709
|
if (loop.target.type !== "workflow") {
|
package/docs/USAGE.md
CHANGED
|
@@ -49,6 +49,28 @@ Run a deterministic command every minute:
|
|
|
49
49
|
loops create command repo-status --every 1m --cmd "git status --short" --cwd /path/to/repo
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Validate the target before storing the loop:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
loops create command repo-status \
|
|
56
|
+
--every 1m \
|
|
57
|
+
--cmd git \
|
|
58
|
+
--no-shell \
|
|
59
|
+
--preflight
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`--preflight` is available on `loops create command`, `loops create agent`, and
|
|
63
|
+
`loops create workflow`. It checks target executables and configured account
|
|
64
|
+
profiles before the loop row is stored, so a missing command, provider binary,
|
|
65
|
+
OpenAccounts profile, or workflow step dependency fails without creating a
|
|
66
|
+
scheduled loop. Use `--json` with `--preflight` to capture stable machine-readable
|
|
67
|
+
preflight evidence.
|
|
68
|
+
|
|
69
|
+
For shell command loops, preflight can only verify the shell plus configured
|
|
70
|
+
accounts because the command string is interpreted later by the shell. Use
|
|
71
|
+
`--no-shell` or workflow command `args` when you need executable-level
|
|
72
|
+
validation before storing the loop.
|
|
73
|
+
|
|
52
74
|
Run a Claude loop every morning:
|
|
53
75
|
|
|
54
76
|
```bash
|
package/package.json
CHANGED