@hasna/loops 0.3.0 → 0.3.2

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 CHANGED
@@ -76,6 +76,17 @@ loops create agent supply-chain-watch \
76
76
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
77
77
  ```
78
78
 
79
+ Run a Codewith loop with a Codewith-native auth profile:
80
+
81
+ ```bash
82
+ loops create agent supply-chain-watch \
83
+ --provider codewith \
84
+ --auth-profile account001 \
85
+ --every 15m \
86
+ --cwd /path/to/repo \
87
+ --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
88
+ ```
89
+
79
90
  For `codewith` and `aicopilot` account isolation, register matching OpenAccounts tools first if they are not built in on the machine:
80
91
 
81
92
  ```bash
@@ -157,7 +168,7 @@ loops remove <id-or-name>
157
168
  loops run-now <id-or-name>
158
169
  ```
159
170
 
160
- Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr are redacted by default in status output.
171
+ Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr are redacted by default in status output. `loops run-now` exits non-zero when the recorded run fails or times out.
161
172
 
162
173
  ## Daemon
163
174
 
@@ -203,10 +214,12 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
203
214
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
204
215
 
205
216
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
206
- - Codewith uses `codewith exec --json --ephemeral --ask-for-approval never`.
217
+ - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`.
207
218
  - AI Copilot and OpenCode use `run --format json --pure`.
208
219
  - Cursor is CLI-first for now via `cursor-agent -p`; treat output as less stable until a stronger public SDK contract is selected.
209
220
  - Codex uses `codex exec --json --ephemeral --ask-for-approval never`.
210
221
  - 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.
222
+ - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `exec`; they do not call OpenAccounts.
223
+ - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
211
224
 
212
225
  For production loops that can mutate repos, prefer disposable worktrees and explicit prompts that name allowed write scope.
package/dist/cli/index.js CHANGED
@@ -260,6 +260,11 @@ function validateTarget(value, label) {
260
260
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
261
261
  if (!providers.includes(value.provider))
262
262
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
263
+ if (value.authProfile !== undefined) {
264
+ assertString(value.authProfile, `${label}.authProfile`);
265
+ if (value.provider !== "codewith")
266
+ throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
267
+ }
263
268
  return value;
264
269
  }
265
270
  throw new Error(`${label}.type must be command or agent`);
@@ -1305,10 +1310,11 @@ class Store {
1305
1310
  }
1306
1311
 
1307
1312
  // src/cli/index.ts
1308
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1313
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1309
1314
  import { Command } from "commander";
1310
1315
 
1311
1316
  // src/lib/format.ts
1317
+ var TEXT_OUTPUT_LIMIT = 32 * 1024;
1312
1318
  function redact(value, visible = 80) {
1313
1319
  if (!value)
1314
1320
  return value;
@@ -1316,6 +1322,29 @@ function redact(value, visible = 80) {
1316
1322
  return value;
1317
1323
  return `${value.slice(0, visible)}... [redacted ${value.length - visible} chars]`;
1318
1324
  }
1325
+ function truncateTextOutput(value) {
1326
+ if (value.length <= TEXT_OUTPUT_LIMIT)
1327
+ return value;
1328
+ return `${value.slice(0, TEXT_OUTPUT_LIMIT)}
1329
+ [truncated ${value.length - TEXT_OUTPUT_LIMIT} chars]`;
1330
+ }
1331
+ function textOutputBlocks(value, opts = {}) {
1332
+ const indent = opts.indent ?? "";
1333
+ const nested = `${indent} `;
1334
+ const blocks = [];
1335
+ for (const [label, output] of [
1336
+ ["stdout", value.stdout],
1337
+ ["stderr", value.stderr]
1338
+ ]) {
1339
+ if (!output)
1340
+ continue;
1341
+ blocks.push(`${indent}${label}:`);
1342
+ for (const line of truncateTextOutput(output).replace(/\s+$/, "").split(/\r?\n/)) {
1343
+ blocks.push(`${nested}${line}`);
1344
+ }
1345
+ }
1346
+ return blocks;
1347
+ }
1319
1348
  function publicLoop(loop) {
1320
1349
  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
1350
  return {
@@ -1354,9 +1383,8 @@ function publicWorkflowEvent(event) {
1354
1383
  }
1355
1384
 
1356
1385
  // src/lib/executor.ts
1357
- import { spawn, spawnSync as spawnSync2 } from "child_process";
1386
+ import { spawn } from "child_process";
1358
1387
  import { once } from "events";
1359
- import { existsSync as existsSync2 } from "fs";
1360
1388
 
1361
1389
  // src/lib/accounts.ts
1362
1390
  import { spawnSync } from "child_process";
@@ -1454,6 +1482,65 @@ function resolveAccountEnv(account, toolHint, env) {
1454
1482
  };
1455
1483
  }
1456
1484
 
1485
+ // src/lib/env.ts
1486
+ import { accessSync, constants } from "fs";
1487
+ import { homedir as homedir2 } from "os";
1488
+ import { delimiter, join as join2 } from "path";
1489
+ function compactPathParts(parts) {
1490
+ const seen = new Set;
1491
+ const result = [];
1492
+ for (const part of parts) {
1493
+ const value = part?.trim();
1494
+ if (!value || seen.has(value))
1495
+ continue;
1496
+ seen.add(value);
1497
+ result.push(value);
1498
+ }
1499
+ return result;
1500
+ }
1501
+ function commonExecutableDirs(env = process.env) {
1502
+ const home = env.HOME || homedir2();
1503
+ return compactPathParts([
1504
+ join2(home, ".local", "bin"),
1505
+ join2(home, ".bun", "bin"),
1506
+ join2(home, ".cargo", "bin"),
1507
+ join2(home, ".npm-global", "bin"),
1508
+ join2(home, "bin"),
1509
+ env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
1510
+ env.PNPM_HOME,
1511
+ env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
1512
+ "/opt/homebrew/bin",
1513
+ "/usr/local/bin",
1514
+ "/usr/bin",
1515
+ "/bin",
1516
+ "/usr/sbin",
1517
+ "/sbin"
1518
+ ]);
1519
+ }
1520
+ function normalizeExecutionPath(env = process.env) {
1521
+ return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
1522
+ }
1523
+ function isExecutable(path) {
1524
+ try {
1525
+ accessSync(path, constants.X_OK);
1526
+ return true;
1527
+ } catch {
1528
+ return false;
1529
+ }
1530
+ }
1531
+ function executableExists(command, env = process.env) {
1532
+ if (command.includes("/"))
1533
+ return isExecutable(command);
1534
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
1535
+ if (dir && isExecutable(join2(dir, command)))
1536
+ return true;
1537
+ }
1538
+ return false;
1539
+ }
1540
+ function commandNotFoundMessage(command, env = process.env) {
1541
+ return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
1542
+ }
1543
+
1457
1544
  // src/lib/executor.ts
1458
1545
  var DEFAULT_TIMEOUT_MS = 30 * 60000;
1459
1546
  var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
@@ -1539,7 +1626,7 @@ function agentArgs(target) {
1539
1626
  args.push(...target.extraArgs ?? [], target.prompt);
1540
1627
  return args;
1541
1628
  case "codewith":
1542
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
1629
+ args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
1543
1630
  if (isolation === "safe")
1544
1631
  args.push("--ignore-rules");
1545
1632
  if (target.cwd)
@@ -1619,6 +1706,7 @@ function executionEnv(spec, metadata, opts) {
1619
1706
  Object.assign(env, accountEnv);
1620
1707
  }
1621
1708
  Object.assign(env, spec.env ?? {});
1709
+ env.PATH = normalizeExecutionPath(env);
1622
1710
  if (metadata.loopId)
1623
1711
  env.LOOPS_LOOP_ID = metadata.loopId;
1624
1712
  if (metadata.loopName)
@@ -1637,20 +1725,11 @@ function executionEnv(spec, metadata, opts) {
1637
1725
  env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
1638
1726
  return env;
1639
1727
  }
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
1728
  function preflightTarget(target, metadata = {}, opts = {}) {
1650
1729
  const spec = commandSpec(target);
1651
1730
  const env = executionEnv(spec, metadata, opts);
1652
- if (!spec.shell && !commandExists(spec.command, env)) {
1653
- throw new Error(`Executable not found in PATH: ${spec.command}`);
1731
+ if (!spec.shell && !executableExists(spec.command, env)) {
1732
+ throw new Error(commandNotFoundMessage(spec.command, env));
1654
1733
  }
1655
1734
  return {
1656
1735
  command: spec.command,
@@ -1668,12 +1747,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
1668
1747
  let exitCode;
1669
1748
  let error;
1670
1749
  const env = executionEnv(spec, metadata, opts);
1671
- if (!spec.shell && !commandExists(spec.command, env)) {
1750
+ if (!spec.shell && !executableExists(spec.command, env)) {
1672
1751
  return {
1673
1752
  status: "failed",
1674
1753
  stdout: "",
1675
1754
  stderr: "",
1676
- error: `Executable not found in PATH: ${spec.command}`,
1755
+ error: commandNotFoundMessage(spec.command, env),
1677
1756
  startedAt,
1678
1757
  finishedAt: nowIso(),
1679
1758
  durationMs: 0
@@ -2112,7 +2191,7 @@ async function tick(deps) {
2112
2191
  }
2113
2192
 
2114
2193
  // src/daemon/control.ts
2115
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
2194
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
2116
2195
  import { hostname } from "os";
2117
2196
  import { dirname as dirname2 } from "path";
2118
2197
 
@@ -2140,7 +2219,7 @@ async function runLoop(opts) {
2140
2219
 
2141
2220
  // src/daemon/control.ts
2142
2221
  function readPid(path = pidFilePath()) {
2143
- if (!existsSync3(path))
2222
+ if (!existsSync2(path))
2144
2223
  return;
2145
2224
  try {
2146
2225
  const pid = Number(readFileSync(path, "utf8").trim());
@@ -2376,10 +2455,11 @@ async function startDaemon(opts) {
2376
2455
 
2377
2456
  // src/daemon/install.ts
2378
2457
  import { chmodSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
2379
- import { spawnSync as spawnSync3 } from "child_process";
2458
+ import { spawnSync as spawnSync2 } from "child_process";
2380
2459
  import { dirname as dirname3 } from "path";
2381
2460
  function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
2382
2461
  const command = [execPath, cliEntry, ...args].join(" ");
2462
+ const pathEnv = normalizeExecutionPath(process.env);
2383
2463
  if (process.platform === "linux") {
2384
2464
  const path = systemdServicePath();
2385
2465
  mkdirSync4(dirname3(path), { recursive: true, mode: 448 });
@@ -2392,7 +2472,7 @@ Type=simple
2392
2472
  ExecStart=${command}
2393
2473
  Restart=always
2394
2474
  RestartSec=5
2395
- Environment=PATH=${process.env.PATH ?? ""}
2475
+ Environment=PATH=${pathEnv}
2396
2476
 
2397
2477
  [Install]
2398
2478
  WantedBy=default.target
@@ -2424,6 +2504,10 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
2424
2504
  </array>
2425
2505
  <key>RunAtLoad</key><true/>
2426
2506
  <key>KeepAlive</key><true/>
2507
+ <key>EnvironmentVariables</key>
2508
+ <dict>
2509
+ <key>PATH</key><string>${pathEnv}</string>
2510
+ </dict>
2427
2511
  <key>StandardOutPath</key><string>${daemonLogPath()}</string>
2428
2512
  <key>StandardErrorPath</key><string>${daemonLogPath()}</string>
2429
2513
  </dict>
@@ -2441,7 +2525,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
2441
2525
  function enableStartup(result) {
2442
2526
  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
2527
  return commands.map((command) => {
2444
- const run = spawnSync3("sh", ["-c", command], {
2528
+ const run = spawnSync2("sh", ["-c", command], {
2445
2529
  encoding: "utf8",
2446
2530
  stdio: ["ignore", "pipe", "pipe"]
2447
2531
  });
@@ -2455,8 +2539,8 @@ function enableStartup(result) {
2455
2539
  }
2456
2540
 
2457
2541
  // src/lib/doctor.ts
2458
- import { spawnSync as spawnSync4 } from "child_process";
2459
- import { accessSync, constants } from "fs";
2542
+ import { spawnSync as spawnSync3 } from "child_process";
2543
+ import { accessSync as accessSync2, constants as constants2 } from "fs";
2460
2544
  var PROVIDER_COMMANDS = [
2461
2545
  "claude",
2462
2546
  "cursor-agent",
@@ -2466,11 +2550,11 @@ var PROVIDER_COMMANDS = [
2466
2550
  "codex"
2467
2551
  ];
2468
2552
  function hasCommand(command) {
2469
- const result = spawnSync4("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
2553
+ const result = spawnSync3("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
2470
2554
  return (result.status ?? 1) === 0;
2471
2555
  }
2472
2556
  function commandVersion(command) {
2473
- const result = spawnSync4(command, ["--version"], {
2557
+ const result = spawnSync3(command, ["--version"], {
2474
2558
  encoding: "utf8",
2475
2559
  stdio: ["ignore", "pipe", "pipe"]
2476
2560
  });
@@ -2482,7 +2566,7 @@ function runDoctor(store) {
2482
2566
  const checks = [];
2483
2567
  try {
2484
2568
  const dir = ensureDataDir();
2485
- accessSync(dir, constants.R_OK | constants.W_OK);
2569
+ accessSync2(dir, constants2.R_OK | constants2.W_OK);
2486
2570
  checks.push({ id: "data-dir", status: "ok", message: "data directory is writable", detail: dir });
2487
2571
  } catch (error) {
2488
2572
  checks.push({
@@ -2531,7 +2615,7 @@ function runDoctor(store) {
2531
2615
 
2532
2616
  // src/cli/index.ts
2533
2617
  var program = new Command;
2534
- program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.3.0");
2618
+ program.name("loops").description("Persistent local loops for commands and headless coding agents").version("0.3.2");
2535
2619
  program.option("-j, --json", "print JSON");
2536
2620
  function isJson() {
2537
2621
  return Boolean(program.opts().json);
@@ -2542,6 +2626,10 @@ function print(value, human) {
2542
2626
  else
2543
2627
  console.log(human);
2544
2628
  }
2629
+ function printTextOutput(value) {
2630
+ for (const line of textOutputBlocks(value, { indent: " " }))
2631
+ console.log(line);
2632
+ }
2545
2633
  function parseSchedule(opts) {
2546
2634
  const count = [opts.at, opts.every, opts.cron, opts.dynamic ? "dynamic" : undefined].filter(Boolean).length;
2547
2635
  if (count !== 1)
@@ -2621,6 +2709,13 @@ function accountFromOpts(opts) {
2621
2709
  throw new Error("--account-tool requires --account");
2622
2710
  return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
2623
2711
  }
2712
+ function providerAuthProfileFromOpts(opts, provider) {
2713
+ if (!opts.authProfile)
2714
+ return;
2715
+ if (provider !== "codewith")
2716
+ throw new Error("--auth-profile is currently supported only for --provider codewith");
2717
+ return opts.authProfile;
2718
+ }
2624
2719
  var create = program.command("create").description("create loops");
2625
2720
  addAccountOptions(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) => {
2626
2721
  const store = new Store;
@@ -2639,7 +2734,7 @@ addAccountOptions(addScheduleOptions(create.command("command <name>").descriptio
2639
2734
  store.close();
2640
2735
  }
2641
2736
  });
2642
- addAccountOptions(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("--agent <agent>", "provider-specific agent").option("--timeout <duration>", "run timeout").option("--config-isolation <mode>", "safe or none", "safe"))).action((name, opts) => {
2737
+ addAccountOptions(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("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--config-isolation <mode>", "safe or none", "safe"))).action((name, opts) => {
2643
2738
  const provider = opts.provider;
2644
2739
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
2645
2740
  throw new Error("unsupported provider");
@@ -2656,6 +2751,7 @@ addAccountOptions(addScheduleOptions(create.command("agent <name>").description(
2656
2751
  cwd: opts.cwd,
2657
2752
  model: opts.model,
2658
2753
  agent: opts.agent,
2754
+ authProfile: providerAuthProfileFromOpts(opts, provider),
2659
2755
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
2660
2756
  configIsolation: opts.configIsolation,
2661
2757
  account: accountFromOpts(opts)
@@ -2772,6 +2868,8 @@ workflows.command("run <idOrName>").option("--show-output", "show step stdout/st
2772
2868
  console.log(`${run?.id ?? workflow.id} ${result.status}`);
2773
2869
  for (const step of steps) {
2774
2870
  console.log(` ${String(step.sequence).padStart(2, "0")} ${step.status.padEnd(10)} ${step.stepId} ${step.error ?? ""}`);
2871
+ if (opts.showOutput)
2872
+ printTextOutput(step);
2775
2873
  }
2776
2874
  }
2777
2875
  } finally {
@@ -2872,6 +2970,8 @@ program.command("runs [idOrName]").option("--limit <n>", "limit", "50").option("
2872
2970
  else {
2873
2971
  for (const run of runs) {
2874
2972
  console.log(`${run.id} ${run.status.padEnd(10)} attempt=${run.attempt} slot=${run.scheduledFor} ${run.loopName}`);
2973
+ if (opts.showOutput)
2974
+ printTextOutput(run);
2875
2975
  }
2876
2976
  }
2877
2977
  } finally {
@@ -2916,6 +3016,10 @@ program.command("run-now <idOrName>").option("--show-output", "show stdout/stder
2916
3016
  advanceLoop(store, claim.loop, run, new Date(run.finishedAt ?? new Date), run.status === "succeeded");
2917
3017
  }
2918
3018
  print(publicRun(run, opts.showOutput), `${run.id} ${run.status}`);
3019
+ if (!isJson() && opts.showOutput)
3020
+ printTextOutput(run);
3021
+ if (run.status !== "succeeded")
3022
+ process.exitCode = 1;
2919
3023
  } finally {
2920
3024
  store.close();
2921
3025
  }
@@ -2978,7 +3082,7 @@ ${result.instructions.join(`
2978
3082
  });
2979
3083
  daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) => {
2980
3084
  const path = daemonLogPath();
2981
- if (!existsSync4(path)) {
3085
+ if (!existsSync3(path)) {
2982
3086
  console.log("");
2983
3087
  return;
2984
3088
  }
@@ -260,6 +260,11 @@ function validateTarget(value, label) {
260
260
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
261
261
  if (!providers.includes(value.provider))
262
262
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
263
+ if (value.authProfile !== undefined) {
264
+ assertString(value.authProfile, `${label}.authProfile`);
265
+ if (value.provider !== "codewith")
266
+ throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
267
+ }
263
268
  return value;
264
269
  }
265
270
  throw new Error(`${label}.type must be command or agent`);
@@ -1313,9 +1318,8 @@ import { hostname as hostname2 } from "os";
1313
1318
  import { spawn as spawn2 } from "child_process";
1314
1319
 
1315
1320
  // src/lib/executor.ts
1316
- import { spawn, spawnSync as spawnSync2 } from "child_process";
1321
+ import { spawn } from "child_process";
1317
1322
  import { once } from "events";
1318
- import { existsSync as existsSync2 } from "fs";
1319
1323
 
1320
1324
  // src/lib/accounts.ts
1321
1325
  import { spawnSync } from "child_process";
@@ -1413,6 +1417,65 @@ function resolveAccountEnv(account, toolHint, env) {
1413
1417
  };
1414
1418
  }
1415
1419
 
1420
+ // src/lib/env.ts
1421
+ import { accessSync, constants } from "fs";
1422
+ import { homedir as homedir2 } from "os";
1423
+ import { delimiter, join as join2 } from "path";
1424
+ function compactPathParts(parts) {
1425
+ const seen = new Set;
1426
+ const result = [];
1427
+ for (const part of parts) {
1428
+ const value = part?.trim();
1429
+ if (!value || seen.has(value))
1430
+ continue;
1431
+ seen.add(value);
1432
+ result.push(value);
1433
+ }
1434
+ return result;
1435
+ }
1436
+ function commonExecutableDirs(env = process.env) {
1437
+ const home = env.HOME || homedir2();
1438
+ return compactPathParts([
1439
+ join2(home, ".local", "bin"),
1440
+ join2(home, ".bun", "bin"),
1441
+ join2(home, ".cargo", "bin"),
1442
+ join2(home, ".npm-global", "bin"),
1443
+ join2(home, "bin"),
1444
+ env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
1445
+ env.PNPM_HOME,
1446
+ env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
1447
+ "/opt/homebrew/bin",
1448
+ "/usr/local/bin",
1449
+ "/usr/bin",
1450
+ "/bin",
1451
+ "/usr/sbin",
1452
+ "/sbin"
1453
+ ]);
1454
+ }
1455
+ function normalizeExecutionPath(env = process.env) {
1456
+ return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
1457
+ }
1458
+ function isExecutable(path) {
1459
+ try {
1460
+ accessSync(path, constants.X_OK);
1461
+ return true;
1462
+ } catch {
1463
+ return false;
1464
+ }
1465
+ }
1466
+ function executableExists(command, env = process.env) {
1467
+ if (command.includes("/"))
1468
+ return isExecutable(command);
1469
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
1470
+ if (dir && isExecutable(join2(dir, command)))
1471
+ return true;
1472
+ }
1473
+ return false;
1474
+ }
1475
+ function commandNotFoundMessage(command, env = process.env) {
1476
+ return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
1477
+ }
1478
+
1416
1479
  // src/lib/executor.ts
1417
1480
  var DEFAULT_TIMEOUT_MS = 30 * 60000;
1418
1481
  var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
@@ -1498,7 +1561,7 @@ function agentArgs(target) {
1498
1561
  args.push(...target.extraArgs ?? [], target.prompt);
1499
1562
  return args;
1500
1563
  case "codewith":
1501
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
1564
+ args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
1502
1565
  if (isolation === "safe")
1503
1566
  args.push("--ignore-rules");
1504
1567
  if (target.cwd)
@@ -1578,6 +1641,7 @@ function executionEnv(spec, metadata, opts) {
1578
1641
  Object.assign(env, accountEnv);
1579
1642
  }
1580
1643
  Object.assign(env, spec.env ?? {});
1644
+ env.PATH = normalizeExecutionPath(env);
1581
1645
  if (metadata.loopId)
1582
1646
  env.LOOPS_LOOP_ID = metadata.loopId;
1583
1647
  if (metadata.loopName)
@@ -1596,20 +1660,11 @@ function executionEnv(spec, metadata, opts) {
1596
1660
  env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
1597
1661
  return env;
1598
1662
  }
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
1663
  function preflightTarget(target, metadata = {}, opts = {}) {
1609
1664
  const spec = commandSpec(target);
1610
1665
  const env = executionEnv(spec, metadata, opts);
1611
- if (!spec.shell && !commandExists(spec.command, env)) {
1612
- throw new Error(`Executable not found in PATH: ${spec.command}`);
1666
+ if (!spec.shell && !executableExists(spec.command, env)) {
1667
+ throw new Error(commandNotFoundMessage(spec.command, env));
1613
1668
  }
1614
1669
  return {
1615
1670
  command: spec.command,
@@ -1627,12 +1682,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
1627
1682
  let exitCode;
1628
1683
  let error;
1629
1684
  const env = executionEnv(spec, metadata, opts);
1630
- if (!spec.shell && !commandExists(spec.command, env)) {
1685
+ if (!spec.shell && !executableExists(spec.command, env)) {
1631
1686
  return {
1632
1687
  status: "failed",
1633
1688
  stdout: "",
1634
1689
  stderr: "",
1635
- error: `Executable not found in PATH: ${spec.command}`,
1690
+ error: commandNotFoundMessage(spec.command, env),
1636
1691
  startedAt,
1637
1692
  finishedAt: nowIso(),
1638
1693
  durationMs: 0
@@ -2071,7 +2126,7 @@ async function tick(deps) {
2071
2126
  }
2072
2127
 
2073
2128
  // src/daemon/control.ts
2074
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
2129
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
2075
2130
  import { hostname } from "os";
2076
2131
  import { dirname as dirname2 } from "path";
2077
2132
 
@@ -2099,7 +2154,7 @@ async function runLoop(opts) {
2099
2154
 
2100
2155
  // src/daemon/control.ts
2101
2156
  function readPid(path = pidFilePath()) {
2102
- if (!existsSync3(path))
2157
+ if (!existsSync2(path))
2103
2158
  return;
2104
2159
  try {
2105
2160
  const pid = Number(readFileSync(path, "utf8").trim());
@@ -2332,10 +2387,11 @@ async function startDaemon(opts) {
2332
2387
 
2333
2388
  // src/daemon/install.ts
2334
2389
  import { chmodSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
2335
- import { spawnSync as spawnSync3 } from "child_process";
2390
+ import { spawnSync as spawnSync2 } from "child_process";
2336
2391
  import { dirname as dirname3 } from "path";
2337
2392
  function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
2338
2393
  const command = [execPath, cliEntry, ...args].join(" ");
2394
+ const pathEnv = normalizeExecutionPath(process.env);
2339
2395
  if (process.platform === "linux") {
2340
2396
  const path = systemdServicePath();
2341
2397
  mkdirSync4(dirname3(path), { recursive: true, mode: 448 });
@@ -2348,7 +2404,7 @@ Type=simple
2348
2404
  ExecStart=${command}
2349
2405
  Restart=always
2350
2406
  RestartSec=5
2351
- Environment=PATH=${process.env.PATH ?? ""}
2407
+ Environment=PATH=${pathEnv}
2352
2408
 
2353
2409
  [Install]
2354
2410
  WantedBy=default.target
@@ -2380,6 +2436,10 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
2380
2436
  </array>
2381
2437
  <key>RunAtLoad</key><true/>
2382
2438
  <key>KeepAlive</key><true/>
2439
+ <key>EnvironmentVariables</key>
2440
+ <dict>
2441
+ <key>PATH</key><string>${pathEnv}</string>
2442
+ </dict>
2383
2443
  <key>StandardOutPath</key><string>${daemonLogPath()}</string>
2384
2444
  <key>StandardErrorPath</key><string>${daemonLogPath()}</string>
2385
2445
  </dict>
@@ -2397,7 +2457,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
2397
2457
  function enableStartup(result) {
2398
2458
  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
2459
  return commands.map((command) => {
2400
- const run = spawnSync3("sh", ["-c", command], {
2460
+ const run = spawnSync2("sh", ["-c", command], {
2401
2461
  encoding: "utf8",
2402
2462
  stdio: ["ignore", "pipe", "pipe"]
2403
2463
  });
@@ -2412,7 +2472,7 @@ function enableStartup(result) {
2412
2472
 
2413
2473
  // src/daemon/index.ts
2414
2474
  var program = new Command;
2415
- program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.0");
2475
+ program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.2");
2416
2476
  program.command("run").option("--interval-ms <ms>", "tick interval", (value) => Number(value)).action(async (opts) => runDaemon({ intervalMs: opts.intervalMs }));
2417
2477
  program.command("start").action(async () => {
2418
2478
  const result = await startDaemon({ cliEntry: process.argv[1] ?? "loops-daemon", args: ["run"] });
package/dist/index.js CHANGED
@@ -258,6 +258,11 @@ function validateTarget(value, label) {
258
258
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
259
259
  if (!providers.includes(value.provider))
260
260
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
261
+ if (value.authProfile !== undefined) {
262
+ assertString(value.authProfile, `${label}.authProfile`);
263
+ if (value.provider !== "codewith")
264
+ throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
265
+ }
261
266
  return value;
262
267
  }
263
268
  throw new Error(`${label}.type must be command or agent`);
@@ -1303,9 +1308,8 @@ class Store {
1303
1308
  }
1304
1309
 
1305
1310
  // src/lib/executor.ts
1306
- import { spawn, spawnSync as spawnSync2 } from "child_process";
1311
+ import { spawn } from "child_process";
1307
1312
  import { once } from "events";
1308
- import { existsSync as existsSync2 } from "fs";
1309
1313
 
1310
1314
  // src/lib/accounts.ts
1311
1315
  import { spawnSync } from "child_process";
@@ -1403,6 +1407,65 @@ function resolveAccountEnv(account, toolHint, env) {
1403
1407
  };
1404
1408
  }
1405
1409
 
1410
+ // src/lib/env.ts
1411
+ import { accessSync, constants } from "fs";
1412
+ import { homedir as homedir2 } from "os";
1413
+ import { delimiter, join as join2 } from "path";
1414
+ function compactPathParts(parts) {
1415
+ const seen = new Set;
1416
+ const result = [];
1417
+ for (const part of parts) {
1418
+ const value = part?.trim();
1419
+ if (!value || seen.has(value))
1420
+ continue;
1421
+ seen.add(value);
1422
+ result.push(value);
1423
+ }
1424
+ return result;
1425
+ }
1426
+ function commonExecutableDirs(env = process.env) {
1427
+ const home = env.HOME || homedir2();
1428
+ return compactPathParts([
1429
+ join2(home, ".local", "bin"),
1430
+ join2(home, ".bun", "bin"),
1431
+ join2(home, ".cargo", "bin"),
1432
+ join2(home, ".npm-global", "bin"),
1433
+ join2(home, "bin"),
1434
+ env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
1435
+ env.PNPM_HOME,
1436
+ env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
1437
+ "/opt/homebrew/bin",
1438
+ "/usr/local/bin",
1439
+ "/usr/bin",
1440
+ "/bin",
1441
+ "/usr/sbin",
1442
+ "/sbin"
1443
+ ]);
1444
+ }
1445
+ function normalizeExecutionPath(env = process.env) {
1446
+ return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
1447
+ }
1448
+ function isExecutable(path) {
1449
+ try {
1450
+ accessSync(path, constants.X_OK);
1451
+ return true;
1452
+ } catch {
1453
+ return false;
1454
+ }
1455
+ }
1456
+ function executableExists(command, env = process.env) {
1457
+ if (command.includes("/"))
1458
+ return isExecutable(command);
1459
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
1460
+ if (dir && isExecutable(join2(dir, command)))
1461
+ return true;
1462
+ }
1463
+ return false;
1464
+ }
1465
+ function commandNotFoundMessage(command, env = process.env) {
1466
+ return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
1467
+ }
1468
+
1406
1469
  // src/lib/executor.ts
1407
1470
  var DEFAULT_TIMEOUT_MS = 30 * 60000;
1408
1471
  var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
@@ -1488,7 +1551,7 @@ function agentArgs(target) {
1488
1551
  args.push(...target.extraArgs ?? [], target.prompt);
1489
1552
  return args;
1490
1553
  case "codewith":
1491
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
1554
+ args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
1492
1555
  if (isolation === "safe")
1493
1556
  args.push("--ignore-rules");
1494
1557
  if (target.cwd)
@@ -1568,6 +1631,7 @@ function executionEnv(spec, metadata, opts) {
1568
1631
  Object.assign(env, accountEnv);
1569
1632
  }
1570
1633
  Object.assign(env, spec.env ?? {});
1634
+ env.PATH = normalizeExecutionPath(env);
1571
1635
  if (metadata.loopId)
1572
1636
  env.LOOPS_LOOP_ID = metadata.loopId;
1573
1637
  if (metadata.loopName)
@@ -1586,20 +1650,11 @@ function executionEnv(spec, metadata, opts) {
1586
1650
  env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
1587
1651
  return env;
1588
1652
  }
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
1653
  function preflightTarget(target, metadata = {}, opts = {}) {
1599
1654
  const spec = commandSpec(target);
1600
1655
  const env = executionEnv(spec, metadata, opts);
1601
- if (!spec.shell && !commandExists(spec.command, env)) {
1602
- throw new Error(`Executable not found in PATH: ${spec.command}`);
1656
+ if (!spec.shell && !executableExists(spec.command, env)) {
1657
+ throw new Error(commandNotFoundMessage(spec.command, env));
1603
1658
  }
1604
1659
  return {
1605
1660
  command: spec.command,
@@ -1617,12 +1672,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
1617
1672
  let exitCode;
1618
1673
  let error;
1619
1674
  const env = executionEnv(spec, metadata, opts);
1620
- if (!spec.shell && !commandExists(spec.command, env)) {
1675
+ if (!spec.shell && !executableExists(spec.command, env)) {
1621
1676
  return {
1622
1677
  status: "failed",
1623
1678
  stdout: "",
1624
1679
  stderr: "",
1625
- error: `Executable not found in PATH: ${spec.command}`,
1680
+ error: commandNotFoundMessage(spec.command, env),
1626
1681
  startedAt,
1627
1682
  finishedAt: nowIso(),
1628
1683
  durationMs: 0
@@ -2123,11 +2178,11 @@ function loops(opts = {}) {
2123
2178
  return new LoopsClient(opts);
2124
2179
  }
2125
2180
  // src/lib/doctor.ts
2126
- import { spawnSync as spawnSync3 } from "child_process";
2127
- import { accessSync, constants } from "fs";
2181
+ import { spawnSync as spawnSync2 } from "child_process";
2182
+ import { accessSync as accessSync2, constants as constants2 } from "fs";
2128
2183
 
2129
2184
  // src/daemon/control.ts
2130
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
2185
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, rmSync, writeFileSync } from "fs";
2131
2186
  import { hostname } from "os";
2132
2187
  import { dirname as dirname2 } from "path";
2133
2188
 
@@ -2155,7 +2210,7 @@ async function runLoop(opts) {
2155
2210
 
2156
2211
  // src/daemon/control.ts
2157
2212
  function readPid(path = pidFilePath()) {
2158
- if (!existsSync3(path))
2213
+ if (!existsSync2(path))
2159
2214
  return;
2160
2215
  try {
2161
2216
  const pid = Number(readFileSync(path, "utf8").trim());
@@ -2261,11 +2316,11 @@ var PROVIDER_COMMANDS = [
2261
2316
  "codex"
2262
2317
  ];
2263
2318
  function hasCommand(command) {
2264
- const result = spawnSync3("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
2319
+ const result = spawnSync2("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
2265
2320
  return (result.status ?? 1) === 0;
2266
2321
  }
2267
2322
  function commandVersion(command) {
2268
- const result = spawnSync3(command, ["--version"], {
2323
+ const result = spawnSync2(command, ["--version"], {
2269
2324
  encoding: "utf8",
2270
2325
  stdio: ["ignore", "pipe", "pipe"]
2271
2326
  });
@@ -2277,7 +2332,7 @@ function runDoctor(store) {
2277
2332
  const checks = [];
2278
2333
  try {
2279
2334
  const dir = ensureDataDir();
2280
- accessSync(dir, constants.R_OK | constants.W_OK);
2335
+ accessSync2(dir, constants2.R_OK | constants2.W_OK);
2281
2336
  checks.push({ id: "data-dir", status: "ok", message: "data directory is writable", detail: dir });
2282
2337
  } catch (error) {
2283
2338
  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;
@@ -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/lib/store.js CHANGED
@@ -258,6 +258,11 @@ function validateTarget(value, label) {
258
258
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
259
259
  if (!providers.includes(value.provider))
260
260
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
261
+ if (value.authProfile !== undefined) {
262
+ assertString(value.authProfile, `${label}.authProfile`);
263
+ if (value.provider !== "codewith")
264
+ throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
265
+ }
261
266
  return value;
262
267
  }
263
268
  throw new Error(`${label}.type must be command or agent`);
package/dist/sdk/index.js CHANGED
@@ -258,6 +258,11 @@ function validateTarget(value, label) {
258
258
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
259
259
  if (!providers.includes(value.provider))
260
260
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
261
+ if (value.authProfile !== undefined) {
262
+ assertString(value.authProfile, `${label}.authProfile`);
263
+ if (value.provider !== "codewith")
264
+ throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
265
+ }
261
266
  return value;
262
267
  }
263
268
  throw new Error(`${label}.type must be command or agent`);
@@ -1303,9 +1308,8 @@ class Store {
1303
1308
  }
1304
1309
 
1305
1310
  // src/lib/executor.ts
1306
- import { spawn, spawnSync as spawnSync2 } from "child_process";
1311
+ import { spawn } from "child_process";
1307
1312
  import { once } from "events";
1308
- import { existsSync as existsSync2 } from "fs";
1309
1313
 
1310
1314
  // src/lib/accounts.ts
1311
1315
  import { spawnSync } from "child_process";
@@ -1403,6 +1407,65 @@ function resolveAccountEnv(account, toolHint, env) {
1403
1407
  };
1404
1408
  }
1405
1409
 
1410
+ // src/lib/env.ts
1411
+ import { accessSync, constants } from "fs";
1412
+ import { homedir as homedir2 } from "os";
1413
+ import { delimiter, join as join2 } from "path";
1414
+ function compactPathParts(parts) {
1415
+ const seen = new Set;
1416
+ const result = [];
1417
+ for (const part of parts) {
1418
+ const value = part?.trim();
1419
+ if (!value || seen.has(value))
1420
+ continue;
1421
+ seen.add(value);
1422
+ result.push(value);
1423
+ }
1424
+ return result;
1425
+ }
1426
+ function commonExecutableDirs(env = process.env) {
1427
+ const home = env.HOME || homedir2();
1428
+ return compactPathParts([
1429
+ join2(home, ".local", "bin"),
1430
+ join2(home, ".bun", "bin"),
1431
+ join2(home, ".cargo", "bin"),
1432
+ join2(home, ".npm-global", "bin"),
1433
+ join2(home, "bin"),
1434
+ env.BUN_INSTALL ? join2(env.BUN_INSTALL, "bin") : undefined,
1435
+ env.PNPM_HOME,
1436
+ env.NPM_CONFIG_PREFIX ? join2(env.NPM_CONFIG_PREFIX, "bin") : undefined,
1437
+ "/opt/homebrew/bin",
1438
+ "/usr/local/bin",
1439
+ "/usr/bin",
1440
+ "/bin",
1441
+ "/usr/sbin",
1442
+ "/sbin"
1443
+ ]);
1444
+ }
1445
+ function normalizeExecutionPath(env = process.env) {
1446
+ return compactPathParts([...(env.PATH ?? "").split(delimiter), ...commonExecutableDirs(env)]).join(delimiter);
1447
+ }
1448
+ function isExecutable(path) {
1449
+ try {
1450
+ accessSync(path, constants.X_OK);
1451
+ return true;
1452
+ } catch {
1453
+ return false;
1454
+ }
1455
+ }
1456
+ function executableExists(command, env = process.env) {
1457
+ if (command.includes("/"))
1458
+ return isExecutable(command);
1459
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
1460
+ if (dir && isExecutable(join2(dir, command)))
1461
+ return true;
1462
+ }
1463
+ return false;
1464
+ }
1465
+ function commandNotFoundMessage(command, env = process.env) {
1466
+ return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
1467
+ }
1468
+
1406
1469
  // src/lib/executor.ts
1407
1470
  var DEFAULT_TIMEOUT_MS = 30 * 60000;
1408
1471
  var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
@@ -1488,7 +1551,7 @@ function agentArgs(target) {
1488
1551
  args.push(...target.extraArgs ?? [], target.prompt);
1489
1552
  return args;
1490
1553
  case "codewith":
1491
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
1554
+ args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
1492
1555
  if (isolation === "safe")
1493
1556
  args.push("--ignore-rules");
1494
1557
  if (target.cwd)
@@ -1568,6 +1631,7 @@ function executionEnv(spec, metadata, opts) {
1568
1631
  Object.assign(env, accountEnv);
1569
1632
  }
1570
1633
  Object.assign(env, spec.env ?? {});
1634
+ env.PATH = normalizeExecutionPath(env);
1571
1635
  if (metadata.loopId)
1572
1636
  env.LOOPS_LOOP_ID = metadata.loopId;
1573
1637
  if (metadata.loopName)
@@ -1586,20 +1650,11 @@ function executionEnv(spec, metadata, opts) {
1586
1650
  env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
1587
1651
  return env;
1588
1652
  }
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
1653
  function preflightTarget(target, metadata = {}, opts = {}) {
1599
1654
  const spec = commandSpec(target);
1600
1655
  const env = executionEnv(spec, metadata, opts);
1601
- if (!spec.shell && !commandExists(spec.command, env)) {
1602
- throw new Error(`Executable not found in PATH: ${spec.command}`);
1656
+ if (!spec.shell && !executableExists(spec.command, env)) {
1657
+ throw new Error(commandNotFoundMessage(spec.command, env));
1603
1658
  }
1604
1659
  return {
1605
1660
  command: spec.command,
@@ -1617,12 +1672,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
1617
1672
  let exitCode;
1618
1673
  let error;
1619
1674
  const env = executionEnv(spec, metadata, opts);
1620
- if (!spec.shell && !commandExists(spec.command, env)) {
1675
+ if (!spec.shell && !executableExists(spec.command, env)) {
1621
1676
  return {
1622
1677
  status: "failed",
1623
1678
  stdout: "",
1624
1679
  stderr: "",
1625
- error: `Executable not found in PATH: ${spec.command}`,
1680
+ error: commandNotFoundMessage(spec.command, env),
1626
1681
  startedAt,
1627
1682
  finishedAt: nowIso(),
1628
1683
  durationMs: 0
package/dist/types.d.ts CHANGED
@@ -44,6 +44,7 @@ export interface AgentTarget {
44
44
  cwd?: string;
45
45
  model?: string;
46
46
  agent?: string;
47
+ authProfile?: string;
47
48
  extraArgs?: string[];
48
49
  timeoutMs?: number;
49
50
  configIsolation?: AgentConfigIsolation;
package/docs/USAGE.md CHANGED
@@ -76,6 +76,17 @@ loops create agent supply-chain-watch \
76
76
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
77
77
  ```
78
78
 
79
+ Run a Codewith loop with a Codewith-native auth profile:
80
+
81
+ ```bash
82
+ loops create agent supply-chain-watch \
83
+ --provider codewith \
84
+ --auth-profile account001 \
85
+ --every 15m \
86
+ --cwd /path/to/repo \
87
+ --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
88
+ ```
89
+
79
90
  For `codewith` and `aicopilot` account isolation, register matching OpenAccounts tools first if they are not built in on the machine:
80
91
 
81
92
  ```bash
@@ -157,7 +168,7 @@ loops remove <id-or-name>
157
168
  loops run-now <id-or-name>
158
169
  ```
159
170
 
160
- Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr are redacted by default in status output.
171
+ Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr are redacted by default in status output. `loops run-now` exits non-zero when the recorded run fails or times out.
161
172
 
162
173
  ## Daemon
163
174
 
@@ -203,10 +214,12 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
203
214
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
204
215
 
205
216
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
206
- - Codewith uses `codewith exec --json --ephemeral --ask-for-approval never`.
217
+ - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`.
207
218
  - AI Copilot and OpenCode use `run --format json --pure`.
208
219
  - Cursor is CLI-first for now via `cursor-agent -p`; treat output as less stable until a stronger public SDK contract is selected.
209
220
  - Codex uses `codex exec --json --ephemeral --ask-for-approval never`.
210
221
  - 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.
222
+ - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `exec`; they do not call OpenAccounts.
223
+ - Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
211
224
 
212
225
  For production loops that can mutate repos, prefer disposable worktrees and explicit prompts that name allowed write scope.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",