@hasna/loops 0.3.12 → 0.3.13

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.
@@ -370,6 +370,35 @@ function validateTarget(value, label) {
370
370
  if (value.provider !== "codewith")
371
371
  throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
372
372
  }
373
+ if (value.variant !== undefined)
374
+ assertString(value.variant, `${label}.variant`);
375
+ if (value.permissionMode !== undefined) {
376
+ assertString(value.permissionMode, `${label}.permissionMode`);
377
+ const permissionModes = ["default", "plan", "auto", "bypass"];
378
+ if (!permissionModes.includes(value.permissionMode)) {
379
+ throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
380
+ }
381
+ if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
382
+ throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
383
+ }
384
+ if (value.permissionMode === "auto" && value.provider !== "claude") {
385
+ throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
386
+ }
387
+ }
388
+ if (value.sandbox !== undefined) {
389
+ assertString(value.sandbox, `${label}.sandbox`);
390
+ const codexLike = ["read-only", "workspace-write", "danger-full-access"];
391
+ const cursorLike = ["enabled", "disabled"];
392
+ if (["codewith", "codex"].includes(value.provider)) {
393
+ if (!codexLike.includes(value.sandbox))
394
+ throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
395
+ } else if (value.provider === "cursor") {
396
+ if (!cursorLike.includes(value.sandbox))
397
+ throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
398
+ } else {
399
+ throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
400
+ }
401
+ }
373
402
  return value;
374
403
  }
375
404
  throw new Error(`${label}.type must be command or agent`);
@@ -2363,7 +2392,7 @@ function providerCommand(provider) {
2363
2392
  case "claude":
2364
2393
  return "claude";
2365
2394
  case "cursor":
2366
- return "cursor-agent";
2395
+ return "sh";
2367
2396
  case "codewith":
2368
2397
  return "codewith";
2369
2398
  case "aicopilot":
@@ -2374,22 +2403,107 @@ function providerCommand(provider) {
2374
2403
  return "codex";
2375
2404
  }
2376
2405
  }
2406
+ function codewithLikeSandbox(target) {
2407
+ const sandbox = target.sandbox ?? (target.permissionMode === "bypass" ? "danger-full-access" : "workspace-write");
2408
+ if (sandbox !== "read-only" && sandbox !== "workspace-write" && sandbox !== "danger-full-access") {
2409
+ throw new Error(`${target.provider} sandbox must be read-only, workspace-write, or danger-full-access`);
2410
+ }
2411
+ return sandbox;
2412
+ }
2413
+ function configStringValue(value) {
2414
+ return JSON.stringify(value);
2415
+ }
2416
+ function assertStringOption(value, label) {
2417
+ if (value !== undefined && typeof value !== "string")
2418
+ throw new Error(`${label} must be a string`);
2419
+ }
2420
+ function assertSupportedAgentOptions(target) {
2421
+ assertStringOption(target.variant, `${target.provider}.variant`);
2422
+ assertStringOption(target.model, `${target.provider}.model`);
2423
+ assertStringOption(target.agent, `${target.provider}.agent`);
2424
+ assertStringOption(target.authProfile, `${target.provider}.authProfile`);
2425
+ if (target.authProfile !== undefined && target.provider !== "codewith") {
2426
+ throw new Error(`${target.provider}.authProfile is supported only for codewith`);
2427
+ }
2428
+ if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
2429
+ throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
2430
+ }
2431
+ if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
2432
+ throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
2433
+ }
2434
+ if (["codewith", "codex"].includes(target.provider)) {
2435
+ if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
2436
+ throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
2437
+ }
2438
+ if (target.sandbox)
2439
+ codewithLikeSandbox(target);
2440
+ return;
2441
+ }
2442
+ if (target.provider === "claude") {
2443
+ if (target.sandbox !== undefined)
2444
+ throw new Error("claude.sandbox is not supported");
2445
+ return;
2446
+ }
2447
+ if (target.provider === "cursor") {
2448
+ if (target.permissionMode === "auto")
2449
+ throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
2450
+ if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
2451
+ throw new Error("cursor.sandbox must be enabled or disabled");
2452
+ }
2453
+ return;
2454
+ }
2455
+ if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
2456
+ throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
2457
+ }
2458
+ if (target.sandbox !== undefined)
2459
+ throw new Error(`${target.provider}.sandbox is not supported`);
2460
+ }
2377
2461
  function agentArgs(target) {
2462
+ assertSupportedAgentOptions(target);
2378
2463
  const isolation = target.configIsolation ?? "safe";
2464
+ const permissionMode = target.permissionMode ?? "default";
2379
2465
  const args = [];
2380
2466
  switch (target.provider) {
2381
2467
  case "claude":
2382
2468
  if (isolation === "safe")
2383
2469
  args.push("--safe-mode", "--setting-sources", "local", "--no-session-persistence");
2470
+ if (permissionMode !== "default") {
2471
+ const mode = permissionMode === "bypass" ? "bypassPermissions" : permissionMode === "plan" || permissionMode === "auto" ? permissionMode : undefined;
2472
+ if (mode)
2473
+ args.push("--permission-mode", mode);
2474
+ }
2384
2475
  args.push("-p", "--output-format", "json");
2385
2476
  if (target.model)
2386
2477
  args.push("--model", target.model);
2478
+ if (target.variant)
2479
+ args.push("--effort", target.variant);
2387
2480
  if (target.agent)
2388
2481
  args.push("--agent", target.agent);
2389
2482
  args.push(...target.extraArgs ?? []);
2390
2483
  return args;
2391
2484
  case "cursor":
2392
- args.push("agent", "-p");
2485
+ args.push("-c", [
2486
+ "set -eu",
2487
+ "if command -v cursor >/dev/null 2>&1; then",
2488
+ ' exec cursor agent "$@"',
2489
+ "elif command -v agent >/dev/null 2>&1; then",
2490
+ ' exec agent "$@"',
2491
+ "else",
2492
+ " echo 'Executable not found in PATH: cursor agent or agent' >&2",
2493
+ " exit 127",
2494
+ "fi"
2495
+ ].join(`
2496
+ `), "openloops-cursor", "-p");
2497
+ if (permissionMode === "plan")
2498
+ args.push("--mode", "plan");
2499
+ if (permissionMode === "bypass")
2500
+ args.push("--force");
2501
+ const cursorSandbox = target.sandbox ?? (isolation === "safe" ? "enabled" : undefined);
2502
+ if (cursorSandbox) {
2503
+ if (cursorSandbox !== "enabled" && cursorSandbox !== "disabled")
2504
+ throw new Error("cursor sandbox must be enabled or disabled");
2505
+ args.push("--sandbox", cursorSandbox);
2506
+ }
2393
2507
  if (target.model)
2394
2508
  args.push("--model", target.model);
2395
2509
  if (target.agent)
@@ -2397,7 +2511,10 @@ function agentArgs(target) {
2397
2511
  args.push(...target.extraArgs ?? []);
2398
2512
  return args;
2399
2513
  case "codewith":
2400
- args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
2514
+ args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
2515
+ if (target.variant)
2516
+ args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
2517
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
2401
2518
  if (isolation === "safe")
2402
2519
  args.push("--ignore-rules");
2403
2520
  if (target.cwd)
@@ -2409,7 +2526,9 @@ function agentArgs(target) {
2409
2526
  args.push(...target.extraArgs ?? []);
2410
2527
  return args;
2411
2528
  case "codex":
2412
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
2529
+ if (target.variant)
2530
+ args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
2531
+ args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
2413
2532
  if (isolation === "safe")
2414
2533
  args.push("--ignore-rules");
2415
2534
  if (target.cwd)
@@ -2422,10 +2541,14 @@ function agentArgs(target) {
2422
2541
  args.push("run", "--format", "json");
2423
2542
  if (isolation === "safe")
2424
2543
  args.push("--pure");
2544
+ if (permissionMode === "bypass")
2545
+ args.push("--dangerously-skip-permissions");
2425
2546
  if (target.cwd)
2426
2547
  args.push("--dir", target.cwd);
2427
2548
  if (target.model)
2428
2549
  args.push("--model", target.model);
2550
+ if (target.variant)
2551
+ args.push("--variant", target.variant);
2429
2552
  if (target.agent)
2430
2553
  args.push("--agent", target.agent);
2431
2554
  args.push(...target.extraArgs ?? []);
@@ -2434,10 +2557,14 @@ function agentArgs(target) {
2434
2557
  args.push("run", "--format", "json");
2435
2558
  if (isolation === "safe")
2436
2559
  args.push("--pure");
2560
+ if (permissionMode === "bypass")
2561
+ args.push("--dangerously-skip-permissions");
2437
2562
  if (target.cwd)
2438
2563
  args.push("--dir", target.cwd);
2439
2564
  if (target.model)
2440
2565
  args.push("--model", target.model);
2566
+ if (target.variant)
2567
+ args.push("--variant", target.variant);
2441
2568
  if (target.agent)
2442
2569
  args.push("--agent", target.agent);
2443
2570
  args.push(...target.extraArgs ?? []);
@@ -2466,6 +2593,7 @@ function commandSpec(target) {
2466
2593
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2467
2594
  account: agentTarget.account,
2468
2595
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2596
+ preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2469
2597
  stdin: agentTarget.prompt
2470
2598
  };
2471
2599
  }
@@ -2533,11 +2661,15 @@ function remoteScript(spec, metadata) {
2533
2661
  `;
2534
2662
  }
2535
2663
  function remotePreflightScript(spec, metadata) {
2536
- return [
2664
+ const lines = [
2537
2665
  ...remoteBootstrapLines(spec, metadata),
2538
2666
  "command -v bash >/dev/null 2>&1",
2539
2667
  `command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
2540
- ].join(`
2668
+ ];
2669
+ if (spec.preflightAnyOf?.length) {
2670
+ 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");
2671
+ }
2672
+ return lines.join(`
2541
2673
  `);
2542
2674
  }
2543
2675
  function transportEnv(opts) {
@@ -2690,6 +2822,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
2690
2822
  if (!spec.shell && !executableExists(spec.command, env)) {
2691
2823
  throw new Error(commandNotFoundMessage(spec.command, env));
2692
2824
  }
2825
+ if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2826
+ throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
2827
+ }
2693
2828
  return {
2694
2829
  command: spec.command,
2695
2830
  accountProfile: spec.account?.profile,
@@ -2720,6 +2855,17 @@ async function executeTarget(target, metadata = {}, opts = {}) {
2720
2855
  durationMs: 0
2721
2856
  };
2722
2857
  }
2858
+ if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2859
+ return {
2860
+ status: "failed",
2861
+ stdout,
2862
+ stderr,
2863
+ error: `none of required executables found: ${spec.preflightAnyOf.join(", ")}`,
2864
+ startedAt,
2865
+ finishedAt: nowIso(),
2866
+ durationMs: 0
2867
+ };
2868
+ }
2723
2869
  const child = spawn(spec.command, spec.args, {
2724
2870
  cwd: spec.cwd,
2725
2871
  env,
@@ -4130,7 +4276,7 @@ function enableStartup(result) {
4130
4276
  // package.json
4131
4277
  var package_default = {
4132
4278
  name: "@hasna/loops",
4133
- version: "0.3.12",
4279
+ version: "0.3.13",
4134
4280
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4135
4281
  type: "module",
4136
4282
  main: "dist/index.js",
@@ -4194,6 +4340,7 @@ var package_default = {
4194
4340
  bun: ">=1.0.0"
4195
4341
  },
4196
4342
  dependencies: {
4343
+ "@hasna/events": "^0.1.8",
4197
4344
  "@hasna/machines": "0.0.49",
4198
4345
  "@openrouter/ai-sdk-provider": "2.9.1",
4199
4346
  ai: "6.0.204",
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { listOpenMachines, refreshLoopMachine, resolveLoopMachine } from "./lib/
8
8
  export { tick } from "./lib/scheduler.js";
9
9
  export { executeWorkflow, executeLoopTarget, preflightWorkflow } from "./lib/workflow-runner.js";
10
10
  export { workflowExecutionOrder, workflowBodyFromJson } from "./lib/workflow-spec.js";
11
+ export { EVENT_WORKER_VERIFIER_TEMPLATE_ID, TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID, getLoopTemplate, listLoopTemplates, renderEventWorkerVerifierWorkflow, renderLoopTemplate, renderTodosTaskWorkerVerifierWorkflow, } from "./lib/templates.js";
11
12
  export { runDoctor } from "./lib/doctor.js";
12
13
  export { runGoal } from "./lib/goal/runner.js";
13
14
  export { resolveGoalModel } from "./lib/goal/model-factory.js";