@hasna/loops 0.3.0 → 0.3.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/README.md +2 -1
- package/dist/cli/index.js +118 -29
- package/dist/daemon/index.js +77 -22
- package/dist/index.js +73 -23
- package/dist/lib/env.d.ts +4 -0
- package/dist/lib/format.d.ts +3 -0
- package/dist/sdk/index.js +66 -16
- package/docs/USAGE.md +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -203,10 +203,11 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
|
|
|
203
203
|
The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
|
|
204
204
|
|
|
205
205
|
- Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
|
|
206
|
-
- Codewith uses `codewith exec --json --ephemeral --
|
|
206
|
+
- Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`.
|
|
207
207
|
- AI Copilot and OpenCode use `run --format json --pure`.
|
|
208
208
|
- Cursor is CLI-first for now via `cursor-agent -p`; treat output as less stable until a stronger public SDK contract is selected.
|
|
209
209
|
- Codex uses `codex exec --json --ephemeral --ask-for-approval never`.
|
|
210
210
|
- When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
|
|
211
|
+
- Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
|
|
211
212
|
|
|
212
213
|
For production loops that can mutate repos, prefer disposable worktrees and explicit prompts that name allowed write scope.
|
package/dist/cli/index.js
CHANGED
|
@@ -1305,10 +1305,11 @@ class Store {
|
|
|
1305
1305
|
}
|
|
1306
1306
|
|
|
1307
1307
|
// src/cli/index.ts
|
|
1308
|
-
import { existsSync as
|
|
1308
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1309
1309
|
import { Command } from "commander";
|
|
1310
1310
|
|
|
1311
1311
|
// src/lib/format.ts
|
|
1312
|
+
var TEXT_OUTPUT_LIMIT = 32 * 1024;
|
|
1312
1313
|
function redact(value, visible = 80) {
|
|
1313
1314
|
if (!value)
|
|
1314
1315
|
return value;
|
|
@@ -1316,6 +1317,29 @@ function redact(value, visible = 80) {
|
|
|
1316
1317
|
return value;
|
|
1317
1318
|
return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
|
|
1318
1319
|
}
|
|
1320
|
+
function truncateTextOutput(value) {
|
|
1321
|
+
if (value.length <= TEXT_OUTPUT_LIMIT)
|
|
1322
|
+
return value;
|
|
1323
|
+
return `${value.slice(0, TEXT_OUTPUT_LIMIT)}
|
|
1324
|
+
[truncated ${value.length - TEXT_OUTPUT_LIMIT} chars]`;
|
|
1325
|
+
}
|
|
1326
|
+
function textOutputBlocks(value, opts = {}) {
|
|
1327
|
+
const indent = opts.indent ?? "";
|
|
1328
|
+
const nested = `${indent} `;
|
|
1329
|
+
const blocks = [];
|
|
1330
|
+
for (const [label, output] of [
|
|
1331
|
+
["stdout", value.stdout],
|
|
1332
|
+
["stderr", value.stderr]
|
|
1333
|
+
]) {
|
|
1334
|
+
if (!output)
|
|
1335
|
+
continue;
|
|
1336
|
+
blocks.push(`${indent}${label}:`);
|
|
1337
|
+
for (const line of truncateTextOutput(output).replace(/\s+$/, "").split(/\r?\n/)) {
|
|
1338
|
+
blocks.push(`${nested}${line}`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return blocks;
|
|
1342
|
+
}
|
|
1319
1343
|
function publicLoop(loop) {
|
|
1320
1344
|
const target = loop.target.type === "command" ? { ...loop.target, env: loop.target.env ? "[redacted]" : undefined } : loop.target.type === "agent" ? { ...loop.target, prompt: redact(loop.target.prompt) } : loop.target;
|
|
1321
1345
|
return {
|
|
@@ -1354,9 +1378,8 @@ function publicWorkflowEvent(event) {
|
|
|
1354
1378
|
}
|
|
1355
1379
|
|
|
1356
1380
|
// src/lib/executor.ts
|
|
1357
|
-
import { spawn
|
|
1381
|
+
import { spawn } from "child_process";
|
|
1358
1382
|
import { once } from "events";
|
|
1359
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1360
1383
|
|
|
1361
1384
|
// src/lib/accounts.ts
|
|
1362
1385
|
import { spawnSync } from "child_process";
|
|
@@ -1454,6 +1477,65 @@ function resolveAccountEnv(account, toolHint, env) {
|
|
|
1454
1477
|
};
|
|
1455
1478
|
}
|
|
1456
1479
|
|
|
1480
|
+
// src/lib/env.ts
|
|
1481
|
+
import { accessSync, constants } from "fs";
|
|
1482
|
+
import { homedir as homedir2 } from "os";
|
|
1483
|
+
import { delimiter, join as join2 } from "path";
|
|
1484
|
+
function compactPathParts(parts) {
|
|
1485
|
+
const seen = new Set;
|
|
1486
|
+
const result = [];
|
|
1487
|
+
for (const part of parts) {
|
|
1488
|
+
const value = part?.trim();
|
|
1489
|
+
if (!value || seen.has(value))
|
|
1490
|
+
continue;
|
|
1491
|
+
seen.add(value);
|
|
1492
|
+
result.push(value);
|
|
1493
|
+
}
|
|
1494
|
+
return result;
|
|
1495
|
+
}
|
|
1496
|
+
function commonExecutableDirs(env = process.env) {
|
|
1497
|
+
const home = env.HOME || homedir2();
|
|
1498
|
+
return compactPathParts([
|
|
1499
|
+
join2(home, ".local", "bin"),
|
|
1500
|
+
join2(home, ".bun", "bin"),
|
|
1501
|
+
join2(home, ".cargo", "bin"),
|
|
1502
|
+
join2(home, ".npm-global", "bin"),
|
|
1503
|
+
join2(home, "bin"),
|
|
1504
|
+
env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
|
|
1505
|
+
env.PNPM_HOME,
|
|
1506
|
+
env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
|
|
1507
|
+
"/opt/homebrew/bin",
|
|
1508
|
+
"/usr/local/bin",
|
|
1509
|
+
"/usr/bin",
|
|
1510
|
+
"/bin",
|
|
1511
|
+
"/usr/sbin",
|
|
1512
|
+
"/sbin"
|
|
1513
|
+
]);
|
|
1514
|
+
}
|
|
1515
|
+
function normalizeExecutionPath(env = process.env) {
|
|
1516
|
+
return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
|
|
1517
|
+
}
|
|
1518
|
+
function isExecutable(path) {
|
|
1519
|
+
try {
|
|
1520
|
+
accessSync(path, constants.X_OK);
|
|
1521
|
+
return true;
|
|
1522
|
+
} catch {
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function executableExists(command, env = process.env) {
|
|
1527
|
+
if (command.includes("/"))
|
|
1528
|
+
return isExecutable(command);
|
|
1529
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
1530
|
+
if (dir && isExecutable(join2(dir, command)))
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
function commandNotFoundMessage(command, env = process.env) {
|
|
1536
|
+
return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1457
1539
|
// src/lib/executor.ts
|
|
1458
1540
|
var DEFAULT_TIMEOUT_MS = 30 * 60000;
|
|
1459
1541
|
var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
@@ -1539,7 +1621,7 @@ function agentArgs(target) {
|
|
|
1539
1621
|
args.push(...target.extraArgs ?? [], target.prompt);
|
|
1540
1622
|
return args;
|
|
1541
1623
|
case "codewith":
|
|
1542
|
-
args.push("
|
|
1624
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
|
|
1543
1625
|
if (isolation === "safe")
|
|
1544
1626
|
args.push("--ignore-rules");
|
|
1545
1627
|
if (target.cwd)
|
|
@@ -1619,6 +1701,7 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1619
1701
|
Object.assign(env, accountEnv);
|
|
1620
1702
|
}
|
|
1621
1703
|
Object.assign(env, spec.env ?? {});
|
|
1704
|
+
env.PATH = normalizeExecutionPath(env);
|
|
1622
1705
|
if (metadata.loopId)
|
|
1623
1706
|
env.LOOPS_LOOP_ID = metadata.loopId;
|
|
1624
1707
|
if (metadata.loopName)
|
|
@@ -1637,20 +1720,11 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1637
1720
|
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
1638
1721
|
return env;
|
|
1639
1722
|
}
|
|
1640
|
-
function commandExists(command, env) {
|
|
1641
|
-
if (command.includes("/") && existsSync2(command))
|
|
1642
|
-
return true;
|
|
1643
|
-
const result = spawnSync2("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], {
|
|
1644
|
-
env,
|
|
1645
|
-
stdio: "ignore"
|
|
1646
|
-
});
|
|
1647
|
-
return (result.status ?? 1) === 0;
|
|
1648
|
-
}
|
|
1649
1723
|
function preflightTarget(target, metadata = {}, opts = {}) {
|
|
1650
1724
|
const spec = commandSpec(target);
|
|
1651
1725
|
const env = executionEnv(spec, metadata, opts);
|
|
1652
|
-
if (!spec.shell && !
|
|
1653
|
-
throw new Error(
|
|
1726
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1727
|
+
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
1654
1728
|
}
|
|
1655
1729
|
return {
|
|
1656
1730
|
command: spec.command,
|
|
@@ -1668,12 +1742,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1668
1742
|
let exitCode;
|
|
1669
1743
|
let error;
|
|
1670
1744
|
const env = executionEnv(spec, metadata, opts);
|
|
1671
|
-
if (!spec.shell && !
|
|
1745
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1672
1746
|
return {
|
|
1673
1747
|
status: "failed",
|
|
1674
1748
|
stdout: "",
|
|
1675
1749
|
stderr: "",
|
|
1676
|
-
error:
|
|
1750
|
+
error: commandNotFoundMessage(spec.command, env),
|
|
1677
1751
|
startedAt,
|
|
1678
1752
|
finishedAt: nowIso(),
|
|
1679
1753
|
durationMs: 0
|
|
@@ -2112,7 +2186,7 @@ async function tick(deps) {
|
|
|
2112
2186
|
}
|
|
2113
2187
|
|
|
2114
2188
|
// src/daemon/control.ts
|
|
2115
|
-
import { existsSync as
|
|
2189
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2116
2190
|
import { hostname } from "os";
|
|
2117
2191
|
import { dirname as dirname2 } from "path";
|
|
2118
2192
|
|
|
@@ -2140,7 +2214,7 @@ async function runLoop(opts) {
|
|
|
2140
2214
|
|
|
2141
2215
|
// src/daemon/control.ts
|
|
2142
2216
|
function readPid(path = pidFilePath()) {
|
|
2143
|
-
if (!
|
|
2217
|
+
if (!existsSync2(path))
|
|
2144
2218
|
return;
|
|
2145
2219
|
try {
|
|
2146
2220
|
const pid = Number(readFileSync(path, "utf8").trim());
|
|
@@ -2376,10 +2450,11 @@ async function startDaemon(opts) {
|
|
|
2376
2450
|
|
|
2377
2451
|
// src/daemon/install.ts
|
|
2378
2452
|
import { chmodSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
2379
|
-
import { spawnSync as
|
|
2453
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2380
2454
|
import { dirname as dirname3 } from "path";
|
|
2381
2455
|
function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
|
|
2382
2456
|
const command = [execPath, cliEntry, ...args].join(" ");
|
|
2457
|
+
const pathEnv = normalizeExecutionPath(process.env);
|
|
2383
2458
|
if (process.platform === "linux") {
|
|
2384
2459
|
const path = systemdServicePath();
|
|
2385
2460
|
mkdirSync4(dirname3(path), { recursive: true, mode: 448 });
|
|
@@ -2392,7 +2467,7 @@ Type=simple
|
|
|
2392
2467
|
ExecStart=${command}
|
|
2393
2468
|
Restart=always
|
|
2394
2469
|
RestartSec=5
|
|
2395
|
-
Environment=PATH=${
|
|
2470
|
+
Environment=PATH=${pathEnv}
|
|
2396
2471
|
|
|
2397
2472
|
[Install]
|
|
2398
2473
|
WantedBy=default.target
|
|
@@ -2424,6 +2499,10 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
|
|
|
2424
2499
|
</array>
|
|
2425
2500
|
<key>RunAtLoad</key><true/>
|
|
2426
2501
|
<key>KeepAlive</key><true/>
|
|
2502
|
+
<key>EnvironmentVariables</key>
|
|
2503
|
+
<dict>
|
|
2504
|
+
<key>PATH</key><string>${pathEnv}</string>
|
|
2505
|
+
</dict>
|
|
2427
2506
|
<key>StandardOutPath</key><string>${daemonLogPath()}</string>
|
|
2428
2507
|
<key>StandardErrorPath</key><string>${daemonLogPath()}</string>
|
|
2429
2508
|
</dict>
|
|
@@ -2441,7 +2520,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
|
|
|
2441
2520
|
function enableStartup(result) {
|
|
2442
2521
|
const commands = result.platform === "linux" ? ["systemctl --user daemon-reload", "systemctl --user enable --now loops-daemon.service"] : result.platform === "darwin" ? [`launchctl load -w ${result.path}`] : [];
|
|
2443
2522
|
return commands.map((command) => {
|
|
2444
|
-
const run =
|
|
2523
|
+
const run = spawnSync2("sh", ["-c", command], {
|
|
2445
2524
|
encoding: "utf8",
|
|
2446
2525
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2447
2526
|
});
|
|
@@ -2455,8 +2534,8 @@ function enableStartup(result) {
|
|
|
2455
2534
|
}
|
|
2456
2535
|
|
|
2457
2536
|
// src/lib/doctor.ts
|
|
2458
|
-
import { spawnSync as
|
|
2459
|
-
import { accessSync, constants } from "fs";
|
|
2537
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2538
|
+
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
2460
2539
|
var PROVIDER_COMMANDS = [
|
|
2461
2540
|
"claude",
|
|
2462
2541
|
"cursor-agent",
|
|
@@ -2466,11 +2545,11 @@ var PROVIDER_COMMANDS = [
|
|
|
2466
2545
|
"codex"
|
|
2467
2546
|
];
|
|
2468
2547
|
function hasCommand(command) {
|
|
2469
|
-
const result =
|
|
2548
|
+
const result = spawnSync3("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
|
|
2470
2549
|
return (result.status ?? 1) === 0;
|
|
2471
2550
|
}
|
|
2472
2551
|
function commandVersion(command) {
|
|
2473
|
-
const result =
|
|
2552
|
+
const result = spawnSync3(command, ["--version"], {
|
|
2474
2553
|
encoding: "utf8",
|
|
2475
2554
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2476
2555
|
});
|
|
@@ -2482,7 +2561,7 @@ function runDoctor(store) {
|
|
|
2482
2561
|
const checks = [];
|
|
2483
2562
|
try {
|
|
2484
2563
|
const dir = ensureDataDir();
|
|
2485
|
-
|
|
2564
|
+
accessSync2(dir, constants2.R_OK | constants2.W_OK);
|
|
2486
2565
|
checks.push({ id: "data-dir", status: "ok", message: "data directory is writable", detail: dir });
|
|
2487
2566
|
} catch (error) {
|
|
2488
2567
|
checks.push({
|
|
@@ -2531,7 +2610,7 @@ function runDoctor(store) {
|
|
|
2531
2610
|
|
|
2532
2611
|
// src/cli/index.ts
|
|
2533
2612
|
var program = new Command;
|
|
2534
|
-
program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.3.
|
|
2613
|
+
program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.3.1");
|
|
2535
2614
|
program.option("-j, --json", "print JSON");
|
|
2536
2615
|
function isJson() {
|
|
2537
2616
|
return Boolean(program.opts().json);
|
|
@@ -2542,6 +2621,10 @@ function print(value, human) {
|
|
|
2542
2621
|
else
|
|
2543
2622
|
console.log(human);
|
|
2544
2623
|
}
|
|
2624
|
+
function printTextOutput(value) {
|
|
2625
|
+
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
2626
|
+
console.log(line);
|
|
2627
|
+
}
|
|
2545
2628
|
function parseSchedule(opts) {
|
|
2546
2629
|
const count = [opts.at, opts.every, opts.cron, opts.dynamic ? "dynamic" : undefined].filter(Boolean).length;
|
|
2547
2630
|
if (count !== 1)
|
|
@@ -2772,6 +2855,8 @@ workflows.command("run <idOrName>").option("--show-output", "show step stdout/st
|
|
|
2772
2855
|
console.log(`${run?.id ?? workflow.id} ${result.status}`);
|
|
2773
2856
|
for (const step of steps) {
|
|
2774
2857
|
console.log(` ${String(step.sequence).padStart(2, "0")} ${step.status.padEnd(10)} ${step.stepId} ${step.error ?? ""}`);
|
|
2858
|
+
if (opts.showOutput)
|
|
2859
|
+
printTextOutput(step);
|
|
2775
2860
|
}
|
|
2776
2861
|
}
|
|
2777
2862
|
} finally {
|
|
@@ -2872,6 +2957,8 @@ program.command("runs [idOrName]").option("--limit <n>", "limit", "50").option("
|
|
|
2872
2957
|
else {
|
|
2873
2958
|
for (const run of runs) {
|
|
2874
2959
|
console.log(`${run.id} ${run.status.padEnd(10)} attempt=${run.attempt} slot=${run.scheduledFor} ${run.loopName}`);
|
|
2960
|
+
if (opts.showOutput)
|
|
2961
|
+
printTextOutput(run);
|
|
2875
2962
|
}
|
|
2876
2963
|
}
|
|
2877
2964
|
} finally {
|
|
@@ -2916,6 +3003,8 @@ program.command("run-now <idOrName>").option("--show-output", "show stdout/stder
|
|
|
2916
3003
|
advanceLoop(store, claim.loop, run, new Date(run.finishedAt ?? new Date), run.status === "succeeded");
|
|
2917
3004
|
}
|
|
2918
3005
|
print(publicRun(run, opts.showOutput), `${run.id} ${run.status}`);
|
|
3006
|
+
if (!isJson() && opts.showOutput)
|
|
3007
|
+
printTextOutput(run);
|
|
2919
3008
|
} finally {
|
|
2920
3009
|
store.close();
|
|
2921
3010
|
}
|
|
@@ -2978,7 +3067,7 @@ ${result.instructions.join(`
|
|
|
2978
3067
|
});
|
|
2979
3068
|
daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) => {
|
|
2980
3069
|
const path = daemonLogPath();
|
|
2981
|
-
if (!
|
|
3070
|
+
if (!existsSync3(path)) {
|
|
2982
3071
|
console.log("");
|
|
2983
3072
|
return;
|
|
2984
3073
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -1313,9 +1313,8 @@ import { hostname as hostname2 } from "os";
|
|
|
1313
1313
|
import { spawn as spawn2 } from "child_process";
|
|
1314
1314
|
|
|
1315
1315
|
// src/lib/executor.ts
|
|
1316
|
-
import { spawn
|
|
1316
|
+
import { spawn } from "child_process";
|
|
1317
1317
|
import { once } from "events";
|
|
1318
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1319
1318
|
|
|
1320
1319
|
// src/lib/accounts.ts
|
|
1321
1320
|
import { spawnSync } from "child_process";
|
|
@@ -1413,6 +1412,65 @@ function resolveAccountEnv(account, toolHint, env) {
|
|
|
1413
1412
|
};
|
|
1414
1413
|
}
|
|
1415
1414
|
|
|
1415
|
+
// src/lib/env.ts
|
|
1416
|
+
import { accessSync, constants } from "fs";
|
|
1417
|
+
import { homedir as homedir2 } from "os";
|
|
1418
|
+
import { delimiter, join as join2 } from "path";
|
|
1419
|
+
function compactPathParts(parts) {
|
|
1420
|
+
const seen = new Set;
|
|
1421
|
+
const result = [];
|
|
1422
|
+
for (const part of parts) {
|
|
1423
|
+
const value = part?.trim();
|
|
1424
|
+
if (!value || seen.has(value))
|
|
1425
|
+
continue;
|
|
1426
|
+
seen.add(value);
|
|
1427
|
+
result.push(value);
|
|
1428
|
+
}
|
|
1429
|
+
return result;
|
|
1430
|
+
}
|
|
1431
|
+
function commonExecutableDirs(env = process.env) {
|
|
1432
|
+
const home = env.HOME || homedir2();
|
|
1433
|
+
return compactPathParts([
|
|
1434
|
+
join2(home, ".local", "bin"),
|
|
1435
|
+
join2(home, ".bun", "bin"),
|
|
1436
|
+
join2(home, ".cargo", "bin"),
|
|
1437
|
+
join2(home, ".npm-global", "bin"),
|
|
1438
|
+
join2(home, "bin"),
|
|
1439
|
+
env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
|
|
1440
|
+
env.PNPM_HOME,
|
|
1441
|
+
env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
|
|
1442
|
+
"/opt/homebrew/bin",
|
|
1443
|
+
"/usr/local/bin",
|
|
1444
|
+
"/usr/bin",
|
|
1445
|
+
"/bin",
|
|
1446
|
+
"/usr/sbin",
|
|
1447
|
+
"/sbin"
|
|
1448
|
+
]);
|
|
1449
|
+
}
|
|
1450
|
+
function normalizeExecutionPath(env = process.env) {
|
|
1451
|
+
return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
|
|
1452
|
+
}
|
|
1453
|
+
function isExecutable(path) {
|
|
1454
|
+
try {
|
|
1455
|
+
accessSync(path, constants.X_OK);
|
|
1456
|
+
return true;
|
|
1457
|
+
} catch {
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
function executableExists(command, env = process.env) {
|
|
1462
|
+
if (command.includes("/"))
|
|
1463
|
+
return isExecutable(command);
|
|
1464
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
1465
|
+
if (dir && isExecutable(join2(dir, command)))
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
function commandNotFoundMessage(command, env = process.env) {
|
|
1471
|
+
return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1416
1474
|
// src/lib/executor.ts
|
|
1417
1475
|
var DEFAULT_TIMEOUT_MS = 30 * 60000;
|
|
1418
1476
|
var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
@@ -1498,7 +1556,7 @@ function agentArgs(target) {
|
|
|
1498
1556
|
args.push(...target.extraArgs ?? [], target.prompt);
|
|
1499
1557
|
return args;
|
|
1500
1558
|
case "codewith":
|
|
1501
|
-
args.push("
|
|
1559
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
|
|
1502
1560
|
if (isolation === "safe")
|
|
1503
1561
|
args.push("--ignore-rules");
|
|
1504
1562
|
if (target.cwd)
|
|
@@ -1578,6 +1636,7 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1578
1636
|
Object.assign(env, accountEnv);
|
|
1579
1637
|
}
|
|
1580
1638
|
Object.assign(env, spec.env ?? {});
|
|
1639
|
+
env.PATH = normalizeExecutionPath(env);
|
|
1581
1640
|
if (metadata.loopId)
|
|
1582
1641
|
env.LOOPS_LOOP_ID = metadata.loopId;
|
|
1583
1642
|
if (metadata.loopName)
|
|
@@ -1596,20 +1655,11 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1596
1655
|
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
1597
1656
|
return env;
|
|
1598
1657
|
}
|
|
1599
|
-
function commandExists(command, env) {
|
|
1600
|
-
if (command.includes("/") && existsSync2(command))
|
|
1601
|
-
return true;
|
|
1602
|
-
const result = spawnSync2("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], {
|
|
1603
|
-
env,
|
|
1604
|
-
stdio: "ignore"
|
|
1605
|
-
});
|
|
1606
|
-
return (result.status ?? 1) === 0;
|
|
1607
|
-
}
|
|
1608
1658
|
function preflightTarget(target, metadata = {}, opts = {}) {
|
|
1609
1659
|
const spec = commandSpec(target);
|
|
1610
1660
|
const env = executionEnv(spec, metadata, opts);
|
|
1611
|
-
if (!spec.shell && !
|
|
1612
|
-
throw new Error(
|
|
1661
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1662
|
+
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
1613
1663
|
}
|
|
1614
1664
|
return {
|
|
1615
1665
|
command: spec.command,
|
|
@@ -1627,12 +1677,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1627
1677
|
let exitCode;
|
|
1628
1678
|
let error;
|
|
1629
1679
|
const env = executionEnv(spec, metadata, opts);
|
|
1630
|
-
if (!spec.shell && !
|
|
1680
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1631
1681
|
return {
|
|
1632
1682
|
status: "failed",
|
|
1633
1683
|
stdout: "",
|
|
1634
1684
|
stderr: "",
|
|
1635
|
-
error:
|
|
1685
|
+
error: commandNotFoundMessage(spec.command, env),
|
|
1636
1686
|
startedAt,
|
|
1637
1687
|
finishedAt: nowIso(),
|
|
1638
1688
|
durationMs: 0
|
|
@@ -2071,7 +2121,7 @@ async function tick(deps) {
|
|
|
2071
2121
|
}
|
|
2072
2122
|
|
|
2073
2123
|
// src/daemon/control.ts
|
|
2074
|
-
import { existsSync as
|
|
2124
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2075
2125
|
import { hostname } from "os";
|
|
2076
2126
|
import { dirname as dirname2 } from "path";
|
|
2077
2127
|
|
|
@@ -2099,7 +2149,7 @@ async function runLoop(opts) {
|
|
|
2099
2149
|
|
|
2100
2150
|
// src/daemon/control.ts
|
|
2101
2151
|
function readPid(path = pidFilePath()) {
|
|
2102
|
-
if (!
|
|
2152
|
+
if (!existsSync2(path))
|
|
2103
2153
|
return;
|
|
2104
2154
|
try {
|
|
2105
2155
|
const pid = Number(readFileSync(path, "utf8").trim());
|
|
@@ -2332,10 +2382,11 @@ async function startDaemon(opts) {
|
|
|
2332
2382
|
|
|
2333
2383
|
// src/daemon/install.ts
|
|
2334
2384
|
import { chmodSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
2335
|
-
import { spawnSync as
|
|
2385
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2336
2386
|
import { dirname as dirname3 } from "path";
|
|
2337
2387
|
function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
|
|
2338
2388
|
const command = [execPath, cliEntry, ...args].join(" ");
|
|
2389
|
+
const pathEnv = normalizeExecutionPath(process.env);
|
|
2339
2390
|
if (process.platform === "linux") {
|
|
2340
2391
|
const path = systemdServicePath();
|
|
2341
2392
|
mkdirSync4(dirname3(path), { recursive: true, mode: 448 });
|
|
@@ -2348,7 +2399,7 @@ Type=simple
|
|
|
2348
2399
|
ExecStart=${command}
|
|
2349
2400
|
Restart=always
|
|
2350
2401
|
RestartSec=5
|
|
2351
|
-
Environment=PATH=${
|
|
2402
|
+
Environment=PATH=${pathEnv}
|
|
2352
2403
|
|
|
2353
2404
|
[Install]
|
|
2354
2405
|
WantedBy=default.target
|
|
@@ -2380,6 +2431,10 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
|
|
|
2380
2431
|
</array>
|
|
2381
2432
|
<key>RunAtLoad</key><true/>
|
|
2382
2433
|
<key>KeepAlive</key><true/>
|
|
2434
|
+
<key>EnvironmentVariables</key>
|
|
2435
|
+
<dict>
|
|
2436
|
+
<key>PATH</key><string>${pathEnv}</string>
|
|
2437
|
+
</dict>
|
|
2383
2438
|
<key>StandardOutPath</key><string>${daemonLogPath()}</string>
|
|
2384
2439
|
<key>StandardErrorPath</key><string>${daemonLogPath()}</string>
|
|
2385
2440
|
</dict>
|
|
@@ -2397,7 +2452,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
|
|
|
2397
2452
|
function enableStartup(result) {
|
|
2398
2453
|
const commands = result.platform === "linux" ? ["systemctl --user daemon-reload", "systemctl --user enable --now loops-daemon.service"] : result.platform === "darwin" ? [`launchctl load -w ${result.path}`] : [];
|
|
2399
2454
|
return commands.map((command) => {
|
|
2400
|
-
const run =
|
|
2455
|
+
const run = spawnSync2("sh", ["-c", command], {
|
|
2401
2456
|
encoding: "utf8",
|
|
2402
2457
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2403
2458
|
});
|
|
@@ -2412,7 +2467,7 @@ function enableStartup(result) {
|
|
|
2412
2467
|
|
|
2413
2468
|
// src/daemon/index.ts
|
|
2414
2469
|
var program = new Command;
|
|
2415
|
-
program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.
|
|
2470
|
+
program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.1");
|
|
2416
2471
|
program.command("run").option("--interval-ms <ms>", "tick interval", (value) => Number(value)).action(async (opts) => runDaemon({ intervalMs: opts.intervalMs }));
|
|
2417
2472
|
program.command("start").action(async () => {
|
|
2418
2473
|
const result = await startDaemon({ cliEntry: process.argv[1] ?? "loops-daemon", args: ["run"] });
|
package/dist/index.js
CHANGED
|
@@ -1303,9 +1303,8 @@ class Store {
|
|
|
1303
1303
|
}
|
|
1304
1304
|
|
|
1305
1305
|
// src/lib/executor.ts
|
|
1306
|
-
import { spawn
|
|
1306
|
+
import { spawn } from "child_process";
|
|
1307
1307
|
import { once } from "events";
|
|
1308
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1309
1308
|
|
|
1310
1309
|
// src/lib/accounts.ts
|
|
1311
1310
|
import { spawnSync } from "child_process";
|
|
@@ -1403,6 +1402,65 @@ function resolveAccountEnv(account, toolHint, env) {
|
|
|
1403
1402
|
};
|
|
1404
1403
|
}
|
|
1405
1404
|
|
|
1405
|
+
// src/lib/env.ts
|
|
1406
|
+
import { accessSync, constants } from "fs";
|
|
1407
|
+
import { homedir as homedir2 } from "os";
|
|
1408
|
+
import { delimiter, join as join2 } from "path";
|
|
1409
|
+
function compactPathParts(parts) {
|
|
1410
|
+
const seen = new Set;
|
|
1411
|
+
const result = [];
|
|
1412
|
+
for (const part of parts) {
|
|
1413
|
+
const value = part?.trim();
|
|
1414
|
+
if (!value || seen.has(value))
|
|
1415
|
+
continue;
|
|
1416
|
+
seen.add(value);
|
|
1417
|
+
result.push(value);
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
function commonExecutableDirs(env = process.env) {
|
|
1422
|
+
const home = env.HOME || homedir2();
|
|
1423
|
+
return compactPathParts([
|
|
1424
|
+
join2(home, ".local", "bin"),
|
|
1425
|
+
join2(home, ".bun", "bin"),
|
|
1426
|
+
join2(home, ".cargo", "bin"),
|
|
1427
|
+
join2(home, ".npm-global", "bin"),
|
|
1428
|
+
join2(home, "bin"),
|
|
1429
|
+
env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
|
|
1430
|
+
env.PNPM_HOME,
|
|
1431
|
+
env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
|
|
1432
|
+
"/opt/homebrew/bin",
|
|
1433
|
+
"/usr/local/bin",
|
|
1434
|
+
"/usr/bin",
|
|
1435
|
+
"/bin",
|
|
1436
|
+
"/usr/sbin",
|
|
1437
|
+
"/sbin"
|
|
1438
|
+
]);
|
|
1439
|
+
}
|
|
1440
|
+
function normalizeExecutionPath(env = process.env) {
|
|
1441
|
+
return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
|
|
1442
|
+
}
|
|
1443
|
+
function isExecutable(path) {
|
|
1444
|
+
try {
|
|
1445
|
+
accessSync(path, constants.X_OK);
|
|
1446
|
+
return true;
|
|
1447
|
+
} catch {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function executableExists(command, env = process.env) {
|
|
1452
|
+
if (command.includes("/"))
|
|
1453
|
+
return isExecutable(command);
|
|
1454
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
1455
|
+
if (dir && isExecutable(join2(dir, command)))
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
function commandNotFoundMessage(command, env = process.env) {
|
|
1461
|
+
return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1406
1464
|
// src/lib/executor.ts
|
|
1407
1465
|
var DEFAULT_TIMEOUT_MS = 30 * 60000;
|
|
1408
1466
|
var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
@@ -1488,7 +1546,7 @@ function agentArgs(target) {
|
|
|
1488
1546
|
args.push(...target.extraArgs ?? [], target.prompt);
|
|
1489
1547
|
return args;
|
|
1490
1548
|
case "codewith":
|
|
1491
|
-
args.push("
|
|
1549
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
|
|
1492
1550
|
if (isolation === "safe")
|
|
1493
1551
|
args.push("--ignore-rules");
|
|
1494
1552
|
if (target.cwd)
|
|
@@ -1568,6 +1626,7 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1568
1626
|
Object.assign(env, accountEnv);
|
|
1569
1627
|
}
|
|
1570
1628
|
Object.assign(env, spec.env ?? {});
|
|
1629
|
+
env.PATH = normalizeExecutionPath(env);
|
|
1571
1630
|
if (metadata.loopId)
|
|
1572
1631
|
env.LOOPS_LOOP_ID = metadata.loopId;
|
|
1573
1632
|
if (metadata.loopName)
|
|
@@ -1586,20 +1645,11 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1586
1645
|
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
1587
1646
|
return env;
|
|
1588
1647
|
}
|
|
1589
|
-
function commandExists(command, env) {
|
|
1590
|
-
if (command.includes("/") && existsSync2(command))
|
|
1591
|
-
return true;
|
|
1592
|
-
const result = spawnSync2("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], {
|
|
1593
|
-
env,
|
|
1594
|
-
stdio: "ignore"
|
|
1595
|
-
});
|
|
1596
|
-
return (result.status ?? 1) === 0;
|
|
1597
|
-
}
|
|
1598
1648
|
function preflightTarget(target, metadata = {}, opts = {}) {
|
|
1599
1649
|
const spec = commandSpec(target);
|
|
1600
1650
|
const env = executionEnv(spec, metadata, opts);
|
|
1601
|
-
if (!spec.shell && !
|
|
1602
|
-
throw new Error(
|
|
1651
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1652
|
+
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
1603
1653
|
}
|
|
1604
1654
|
return {
|
|
1605
1655
|
command: spec.command,
|
|
@@ -1617,12 +1667,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1617
1667
|
let exitCode;
|
|
1618
1668
|
let error;
|
|
1619
1669
|
const env = executionEnv(spec, metadata, opts);
|
|
1620
|
-
if (!spec.shell && !
|
|
1670
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1621
1671
|
return {
|
|
1622
1672
|
status: "failed",
|
|
1623
1673
|
stdout: "",
|
|
1624
1674
|
stderr: "",
|
|
1625
|
-
error:
|
|
1675
|
+
error: commandNotFoundMessage(spec.command, env),
|
|
1626
1676
|
startedAt,
|
|
1627
1677
|
finishedAt: nowIso(),
|
|
1628
1678
|
durationMs: 0
|
|
@@ -2123,11 +2173,11 @@ function loops(opts = {}) {
|
|
|
2123
2173
|
return new LoopsClient(opts);
|
|
2124
2174
|
}
|
|
2125
2175
|
// src/lib/doctor.ts
|
|
2126
|
-
import { spawnSync as
|
|
2127
|
-
import { accessSync, constants } from "fs";
|
|
2176
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2177
|
+
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
2128
2178
|
|
|
2129
2179
|
// src/daemon/control.ts
|
|
2130
|
-
import { existsSync as
|
|
2180
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2131
2181
|
import { hostname } from "os";
|
|
2132
2182
|
import { dirname as dirname2 } from "path";
|
|
2133
2183
|
|
|
@@ -2155,7 +2205,7 @@ async function runLoop(opts) {
|
|
|
2155
2205
|
|
|
2156
2206
|
// src/daemon/control.ts
|
|
2157
2207
|
function readPid(path = pidFilePath()) {
|
|
2158
|
-
if (!
|
|
2208
|
+
if (!existsSync2(path))
|
|
2159
2209
|
return;
|
|
2160
2210
|
try {
|
|
2161
2211
|
const pid = Number(readFileSync(path, "utf8").trim());
|
|
@@ -2261,11 +2311,11 @@ var PROVIDER_COMMANDS = [
|
|
|
2261
2311
|
"codex"
|
|
2262
2312
|
];
|
|
2263
2313
|
function hasCommand(command) {
|
|
2264
|
-
const result =
|
|
2314
|
+
const result = spawnSync2("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
|
|
2265
2315
|
return (result.status ?? 1) === 0;
|
|
2266
2316
|
}
|
|
2267
2317
|
function commandVersion(command) {
|
|
2268
|
-
const result =
|
|
2318
|
+
const result = spawnSync2(command, ["--version"], {
|
|
2269
2319
|
encoding: "utf8",
|
|
2270
2320
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2271
2321
|
});
|
|
@@ -2277,7 +2327,7 @@ function runDoctor(store) {
|
|
|
2277
2327
|
const checks = [];
|
|
2278
2328
|
try {
|
|
2279
2329
|
const dir = ensureDataDir();
|
|
2280
|
-
|
|
2330
|
+
accessSync2(dir, constants2.R_OK | constants2.W_OK);
|
|
2281
2331
|
checks.push({ id: "data-dir", status: "ok", message: "data directory is writable", detail: dir });
|
|
2282
2332
|
} catch (error) {
|
|
2283
2333
|
checks.push({
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function commonExecutableDirs(env?: NodeJS.ProcessEnv): string[];
|
|
2
|
+
export declare function normalizeExecutionPath(env?: NodeJS.ProcessEnv): string;
|
|
3
|
+
export declare function executableExists(command: string, env?: NodeJS.ProcessEnv): boolean;
|
|
4
|
+
export declare function commandNotFoundMessage(command: string, env?: NodeJS.ProcessEnv): string;
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Loop, LoopRun, WorkflowEvent, WorkflowRun, WorkflowSpec, WorkflowStepRun } from "../types.js";
|
|
2
2
|
export declare function redact(value: string | undefined, visible?: number): string | undefined;
|
|
3
|
+
export declare function textOutputBlocks(value: Pick<LoopRun | WorkflowStepRun, "stdout" | "stderr">, opts?: {
|
|
4
|
+
indent?: string;
|
|
5
|
+
}): string[];
|
|
3
6
|
export declare function publicLoop(loop: Loop): Record<string, unknown>;
|
|
4
7
|
export declare function publicRun(run: LoopRun, showOutput?: boolean): Record<string, unknown>;
|
|
5
8
|
export declare function publicWorkflow(workflow: WorkflowSpec): Record<string, unknown>;
|
package/dist/sdk/index.js
CHANGED
|
@@ -1303,9 +1303,8 @@ class Store {
|
|
|
1303
1303
|
}
|
|
1304
1304
|
|
|
1305
1305
|
// src/lib/executor.ts
|
|
1306
|
-
import { spawn
|
|
1306
|
+
import { spawn } from "child_process";
|
|
1307
1307
|
import { once } from "events";
|
|
1308
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1309
1308
|
|
|
1310
1309
|
// src/lib/accounts.ts
|
|
1311
1310
|
import { spawnSync } from "child_process";
|
|
@@ -1403,6 +1402,65 @@ function resolveAccountEnv(account, toolHint, env) {
|
|
|
1403
1402
|
};
|
|
1404
1403
|
}
|
|
1405
1404
|
|
|
1405
|
+
// src/lib/env.ts
|
|
1406
|
+
import { accessSync, constants } from "fs";
|
|
1407
|
+
import { homedir as homedir2 } from "os";
|
|
1408
|
+
import { delimiter, join as join2 } from "path";
|
|
1409
|
+
function compactPathParts(parts) {
|
|
1410
|
+
const seen = new Set;
|
|
1411
|
+
const result = [];
|
|
1412
|
+
for (const part of parts) {
|
|
1413
|
+
const value = part?.trim();
|
|
1414
|
+
if (!value || seen.has(value))
|
|
1415
|
+
continue;
|
|
1416
|
+
seen.add(value);
|
|
1417
|
+
result.push(value);
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
function commonExecutableDirs(env = process.env) {
|
|
1422
|
+
const home = env.HOME || homedir2();
|
|
1423
|
+
return compactPathParts([
|
|
1424
|
+
join2(home, ".local", "bin"),
|
|
1425
|
+
join2(home, ".bun", "bin"),
|
|
1426
|
+
join2(home, ".cargo", "bin"),
|
|
1427
|
+
join2(home, ".npm-global", "bin"),
|
|
1428
|
+
join2(home, "bin"),
|
|
1429
|
+
env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
|
|
1430
|
+
env.PNPM_HOME,
|
|
1431
|
+
env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
|
|
1432
|
+
"/opt/homebrew/bin",
|
|
1433
|
+
"/usr/local/bin",
|
|
1434
|
+
"/usr/bin",
|
|
1435
|
+
"/bin",
|
|
1436
|
+
"/usr/sbin",
|
|
1437
|
+
"/sbin"
|
|
1438
|
+
]);
|
|
1439
|
+
}
|
|
1440
|
+
function normalizeExecutionPath(env = process.env) {
|
|
1441
|
+
return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
|
|
1442
|
+
}
|
|
1443
|
+
function isExecutable(path) {
|
|
1444
|
+
try {
|
|
1445
|
+
accessSync(path, constants.X_OK);
|
|
1446
|
+
return true;
|
|
1447
|
+
} catch {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function executableExists(command, env = process.env) {
|
|
1452
|
+
if (command.includes("/"))
|
|
1453
|
+
return isExecutable(command);
|
|
1454
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
1455
|
+
if (dir && isExecutable(join2(dir, command)))
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
function commandNotFoundMessage(command, env = process.env) {
|
|
1461
|
+
return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1406
1464
|
// src/lib/executor.ts
|
|
1407
1465
|
var DEFAULT_TIMEOUT_MS = 30 * 60000;
|
|
1408
1466
|
var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
@@ -1488,7 +1546,7 @@ function agentArgs(target) {
|
|
|
1488
1546
|
args.push(...target.extraArgs ?? [], target.prompt);
|
|
1489
1547
|
return args;
|
|
1490
1548
|
case "codewith":
|
|
1491
|
-
args.push("
|
|
1549
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
|
|
1492
1550
|
if (isolation === "safe")
|
|
1493
1551
|
args.push("--ignore-rules");
|
|
1494
1552
|
if (target.cwd)
|
|
@@ -1568,6 +1626,7 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1568
1626
|
Object.assign(env, accountEnv);
|
|
1569
1627
|
}
|
|
1570
1628
|
Object.assign(env, spec.env ?? {});
|
|
1629
|
+
env.PATH = normalizeExecutionPath(env);
|
|
1571
1630
|
if (metadata.loopId)
|
|
1572
1631
|
env.LOOPS_LOOP_ID = metadata.loopId;
|
|
1573
1632
|
if (metadata.loopName)
|
|
@@ -1586,20 +1645,11 @@ function executionEnv(spec, metadata, opts) {
|
|
|
1586
1645
|
env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
|
|
1587
1646
|
return env;
|
|
1588
1647
|
}
|
|
1589
|
-
function commandExists(command, env) {
|
|
1590
|
-
if (command.includes("/") && existsSync2(command))
|
|
1591
|
-
return true;
|
|
1592
|
-
const result = spawnSync2("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], {
|
|
1593
|
-
env,
|
|
1594
|
-
stdio: "ignore"
|
|
1595
|
-
});
|
|
1596
|
-
return (result.status ?? 1) === 0;
|
|
1597
|
-
}
|
|
1598
1648
|
function preflightTarget(target, metadata = {}, opts = {}) {
|
|
1599
1649
|
const spec = commandSpec(target);
|
|
1600
1650
|
const env = executionEnv(spec, metadata, opts);
|
|
1601
|
-
if (!spec.shell && !
|
|
1602
|
-
throw new Error(
|
|
1651
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1652
|
+
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
1603
1653
|
}
|
|
1604
1654
|
return {
|
|
1605
1655
|
command: spec.command,
|
|
@@ -1617,12 +1667,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
1617
1667
|
let exitCode;
|
|
1618
1668
|
let error;
|
|
1619
1669
|
const env = executionEnv(spec, metadata, opts);
|
|
1620
|
-
if (!spec.shell && !
|
|
1670
|
+
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
1621
1671
|
return {
|
|
1622
1672
|
status: "failed",
|
|
1623
1673
|
stdout: "",
|
|
1624
1674
|
stderr: "",
|
|
1625
|
-
error:
|
|
1675
|
+
error: commandNotFoundMessage(spec.command, env),
|
|
1626
1676
|
startedAt,
|
|
1627
1677
|
finishedAt: nowIso(),
|
|
1628
1678
|
durationMs: 0
|
package/docs/USAGE.md
CHANGED
|
@@ -203,10 +203,11 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
|
|
|
203
203
|
The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
|
|
204
204
|
|
|
205
205
|
- Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
|
|
206
|
-
- Codewith uses `codewith exec --json --ephemeral --
|
|
206
|
+
- Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`.
|
|
207
207
|
- AI Copilot and OpenCode use `run --format json --pure`.
|
|
208
208
|
- Cursor is CLI-first for now via `cursor-agent -p`; treat output as less stable until a stronger public SDK contract is selected.
|
|
209
209
|
- Codex uses `codex exec --json --ephemeral --ask-for-approval never`.
|
|
210
210
|
- When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
|
|
211
|
+
- Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
|
|
211
212
|
|
|
212
213
|
For production loops that can mutate repos, prefer disposable worktrees and explicit prompts that name allowed write scope.
|