loopable-cli 0.2.0 → 0.3.0

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.
Files changed (138) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/_connectorId-CDCuSgpN.js +1 -0
  3. package/.output/public/assets/_loopId-DOnIsVTW.js +1 -0
  4. package/.output/public/assets/_taskId-BO5MZnlZ.js +1 -0
  5. package/.output/public/assets/agents-CBozi7oi.js +1 -0
  6. package/.output/public/assets/alert-Bn_wjgTX.js +1 -0
  7. package/.output/public/assets/button-K3ypIhAZ.js +1 -0
  8. package/.output/public/assets/card-DiVnaTIE.js +1 -0
  9. package/.output/public/assets/chevron-right-BrctLtFK.js +1 -0
  10. package/.output/public/assets/connection-status-DKWelvLO.js +1 -0
  11. package/.output/public/assets/connector-icon-CEbdE71G.js +1 -0
  12. package/.output/public/assets/connectors-JyX3IzJj.js +1 -0
  13. package/.output/public/assets/gaps-PjmcbaWo.js +1 -0
  14. package/.output/public/assets/inbox-CujRR0Cz.js +1 -0
  15. package/.output/public/assets/index-C9SQ_Baq.js +16 -0
  16. package/.output/public/assets/loop-form-Dw3cuTzw.js +1 -0
  17. package/.output/public/assets/loops-DcEfZa2_.js +1 -0
  18. package/.output/public/assets/manifests-yJpevyye.js +1 -0
  19. package/.output/public/assets/new-CwsnNuRW.js +1 -0
  20. package/.output/public/assets/page-bvnfRMum.js +1 -0
  21. package/.output/public/assets/play-yHLs4DjK.js +1 -0
  22. package/.output/public/assets/plus-Ber25lrU.js +1 -0
  23. package/.output/public/assets/preload-helper-BoU_7EQC.js +3 -0
  24. package/.output/public/assets/refresh-cw-BzSY7M8I.js +1 -0
  25. package/.output/public/assets/rotate-ccw-DLv9EfCX.js +1 -0
  26. package/.output/public/assets/runners-DzX-RU3i.js +2 -0
  27. package/.output/public/assets/select-C9grIFwG.js +1 -0
  28. package/.output/public/assets/setting-fields-DUc3npgh.js +3 -0
  29. package/.output/public/assets/styles-B9B98zLk.css +2 -0
  30. package/.output/public/assets/switch-DwABVI6d.js +1 -0
  31. package/.output/public/assets/task-list-Ddk8-qt7.js +1 -0
  32. package/.output/public/assets/textarea-Ds-gIYUQ.js +1 -0
  33. package/.output/public/assets/time-goBWd-5J.js +1 -0
  34. package/.output/public/assets/use-live-tasks-DZNNuECy.js +1 -0
  35. package/.output/public/assets/useCompositeListItem-P4Nmmn7j.js +1 -0
  36. package/.output/public/assets/usePositioner-B7i9Ejzh.js +1 -0
  37. package/.output/server/_chunks/ssr-renderer.mjs +1 -1
  38. package/.output/server/_connectorId-COuQN02a.mjs +512 -0
  39. package/.output/server/_libs/@base-ui/react+[...].mjs +1410 -804
  40. package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
  41. package/.output/server/_libs/lucide-react.mjs +93 -84
  42. package/.output/server/_libs/next-themes.mjs +1 -1
  43. package/.output/server/_libs/sonner.mjs +1 -1
  44. package/.output/server/_loopId-CZh6_Yhr.mjs +310 -0
  45. package/.output/server/_ssr/{callback-D20cVD2w.mjs → access-DTttr563.mjs} +37 -20
  46. package/.output/server/_ssr/{agents-CrUmIQpi.mjs → agents-B_qo1gpL.mjs} +196 -7
  47. package/.output/server/_ssr/{agents-Dms_6TBF.mjs → agents-Bz10gz_m.mjs} +8 -8
  48. package/.output/server/_ssr/agents-CjbOUp3Z.mjs +206 -0
  49. package/.output/server/_ssr/{alert-DQvzE7bu.mjs → alert-C2WBWfzo.mjs} +2 -2
  50. package/.output/server/_ssr/card-r1cV-PCN.mjs +36 -0
  51. package/.output/server/_ssr/{client-BtVRVhQ2.mjs → client-Dk3SD8Il.mjs} +5 -1
  52. package/.output/server/_ssr/connection-status-BogAL-Fj.mjs +32 -0
  53. package/.output/server/_ssr/{connections-ypex3qUd.mjs → connections-Cqvb6Gw2.mjs} +1109 -263
  54. package/.output/server/_ssr/connector-icon-DrhVbhs6.mjs +190 -0
  55. package/.output/server/_ssr/connectors-BfS5tQKr.mjs +81 -0
  56. package/.output/server/_ssr/{connectors-BZly0yGF.mjs → connectors-DJI-rzPU.mjs} +21 -10
  57. package/.output/server/_ssr/{createServerRpc-DPDrnFdH.mjs → createServerRpc-DDrp2nLT.mjs} +2 -2
  58. package/.output/server/_ssr/{gaps-D2Zh8sEo.mjs → gaps-CcvHiLcj.mjs} +18 -10
  59. package/.output/server/_ssr/inbox-D1ef761v.mjs +178 -0
  60. package/.output/server/_ssr/loop-form-DWwOR05R.mjs +346 -0
  61. package/.output/server/_ssr/{loops-Og_G3KyZ.mjs → loops-6WhUe_lc.mjs} +228 -25
  62. package/.output/server/_ssr/loops-DHPjzG-t.mjs +217 -0
  63. package/.output/server/_ssr/new-ChKBdcXJ.mjs +245 -0
  64. package/.output/server/_ssr/page-DZVxKvdn.mjs +120 -0
  65. package/.output/server/_ssr/{router-BbrBz36P.mjs → router-DdwREiOf.mjs} +167 -489
  66. package/.output/server/_ssr/runners-CK9Cx9fS.mjs +217 -0
  67. package/.output/server/_ssr/runners-CrznFPHt.mjs +140 -0
  68. package/.output/server/_ssr/runners-W-pgo5yo.mjs +55 -0
  69. package/.output/server/_ssr/{secrets-CRnpwGBh.mjs → secrets-CV5scFlt.mjs} +2 -2
  70. package/.output/server/_ssr/select-BtndHhHb.mjs +76 -0
  71. package/.output/server/_ssr/{setting-fields-mpXfGftI.mjs → setting-fields-LNgywAW7.mjs} +25 -20
  72. package/.output/server/_ssr/ssr.mjs +49 -40
  73. package/.output/server/_ssr/{start-BgTj-h_C.mjs → start-C_j-e4vF.mjs} +10 -6
  74. package/.output/server/_ssr/{switch-1pnd_bPJ.mjs → switch-odxNO_S4.mjs} +3 -3
  75. package/.output/server/_ssr/task-list-Dnmq_WeE.mjs +150 -0
  76. package/.output/server/_ssr/{tasks-LIi-W4cy.mjs → tasks-D-kXLgVP.mjs} +13 -13
  77. package/.output/server/_ssr/{tasks-B_QmDFVs.mjs → tasks-D7aUvA33.mjs} +55 -17
  78. package/.output/server/_ssr/{textarea-CHJTKtsJ.mjs → textarea-D2c_vTIC.mjs} +2 -2
  79. package/.output/server/_ssr/time-Cy8TZP0Z.mjs +13 -0
  80. package/.output/server/_ssr/use-live-tasks-BzhTkEOL.mjs +40 -0
  81. package/.output/server/_tanstack-start-manifest_v-2HUdn-2E.mjs +166 -0
  82. package/.output/server/_taskId-D3KkNJW1.mjs +263 -0
  83. package/.output/server/index.mjs +204 -162
  84. package/README.md +54 -19
  85. package/dist/cli.mjs +185 -67
  86. package/dist/dispatcher.mjs +1443 -277
  87. package/dist/runner.mjs +333 -46
  88. package/drizzle/0025_prompt_tasks_done.sql +2 -0
  89. package/drizzle/0026_guidance_into_prompt.sql +3 -0
  90. package/drizzle/0027_relative_folders.sql +5 -0
  91. package/drizzle/0028_runner_version.sql +5 -0
  92. package/drizzle/meta/_journal.json +29 -1
  93. package/package.json +23 -17
  94. package/.output/public/assets/_connectorId-m67WbiJ8.js +0 -1
  95. package/.output/public/assets/_loopId-Bf4K5sdd.js +0 -1
  96. package/.output/public/assets/_taskId-eCKkcVOx.js +0 -1
  97. package/.output/public/assets/alert-BT2EVMJP.js +0 -1
  98. package/.output/public/assets/arrow-left-CWUZt6k2.js +0 -1
  99. package/.output/public/assets/arrow-right-dy_oOg_c.js +0 -1
  100. package/.output/public/assets/connection-status-DCqqmY_6.js +0 -1
  101. package/.output/public/assets/connector-icon-qGpNsPgH.js +0 -1
  102. package/.output/public/assets/connectors-DxXlc9zF.js +0 -1
  103. package/.output/public/assets/external-link-DC11orFA.js +0 -1
  104. package/.output/public/assets/gaps-CoTA-PzA.js +0 -1
  105. package/.output/public/assets/inbox-CLEXUXCI.js +0 -1
  106. package/.output/public/assets/index-Cndsw84p.js +0 -16
  107. package/.output/public/assets/loader-B4Epchm_.js +0 -1
  108. package/.output/public/assets/loop-form-BvizlZdR.js +0 -1
  109. package/.output/public/assets/loops-Dbe_mf5T.js +0 -1
  110. package/.output/public/assets/new-DPEaXKAY.js +0 -1
  111. package/.output/public/assets/play-DD4xQgvi.js +0 -1
  112. package/.output/public/assets/preload-helper-CfdAYKD7.js +0 -3
  113. package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +0 -1
  114. package/.output/public/assets/runners-BVBggS5B.js +0 -1
  115. package/.output/public/assets/select-Cppu-hti.js +0 -1
  116. package/.output/public/assets/setting-fields-BT1naVJX.js +0 -3
  117. package/.output/public/assets/styles-D3sonCfX.css +0 -2
  118. package/.output/public/assets/switch-hRIbNGOj.js +0 -1
  119. package/.output/public/assets/task-list-CrptnI1N.js +0 -1
  120. package/.output/public/assets/textarea-Fyuzi6Vp.js +0 -1
  121. package/.output/public/assets/trash-2-BXZzBsoV.js +0 -1
  122. package/.output/public/assets/useValueChanged-VVyq4hb9.js +0 -1
  123. package/.output/server/_connectorId-DcSnoRjk.mjs +0 -421
  124. package/.output/server/_loopId-G_TAkYtS.mjs +0 -272
  125. package/.output/server/_ssr/access-B7TcOZ7F.mjs +0 -42
  126. package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +0 -31
  127. package/.output/server/_ssr/connector-icon-CFrrHjzR.mjs +0 -37
  128. package/.output/server/_ssr/connectors-4yzKb0KG.mjs +0 -106
  129. package/.output/server/_ssr/inbox-C2VBGLa5.mjs +0 -114
  130. package/.output/server/_ssr/loop-form-D-U-kU4k.mjs +0 -413
  131. package/.output/server/_ssr/loops-D0XJ8eBb.mjs +0 -222
  132. package/.output/server/_ssr/new-CajvgIog.mjs +0 -169
  133. package/.output/server/_ssr/runners-B_WuF8UJ.mjs +0 -32
  134. package/.output/server/_ssr/runners-D-0_lfiq.mjs +0 -106
  135. package/.output/server/_ssr/runners-zqF_zbEc.mjs +0 -116
  136. package/.output/server/_ssr/task-list-BnQJ94Dk.mjs +0 -187
  137. package/.output/server/_tanstack-start-manifest_v-BCHAazE8.mjs +0 -129
  138. package/.output/server/_taskId-CIv0ajIx.mjs +0 -256
package/dist/runner.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/runner/client.ts
2
- import { closeSync, existsSync, fstatSync, mkdtempSync, openSync, readSync, rmSync as rmSync2 } from "node:fs";
2
+ import { closeSync, existsSync as existsSync2, fstatSync, mkdtempSync, openSync, readSync, rmSync as rmSync2 } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { hostname as osHostname } from "node:os";
5
- import { join as join4 } from "node:path";
5
+ import { join as join5 } from "node:path";
6
6
 
7
7
  // src/agents/define.ts
8
8
  function defineAgentManifest(manifest) {
@@ -12,13 +12,29 @@ function defineAgentRuntime(runtime) {
12
12
  return runtime;
13
13
  }
14
14
 
15
+ // src/agents/claude/manifest.ts
16
+ var claudeManifest = defineAgentManifest({
17
+ id: "claude",
18
+ name: "Claude Code",
19
+ tagline: "Anthropic's coding agent, driven through its print mode.",
20
+ docsUrl: "https://docs.claude.com/en/docs/claude-code/cli-reference",
21
+ icon: "ClaudeCode",
22
+ accent: "bg-[#d97757] text-white",
23
+ binaries: ["claude"],
24
+ installHint: "Install with curl -fsSL https://claude.ai/install.sh | bash, then run claude auth login.",
25
+ permissionModes: ["read_only", "workspace_write"],
26
+ supportsModel: true,
27
+ modelPlaceholder: "e.g. sonnet or opus; leave empty for the default",
28
+ defaults: { permissionMode: "read_only", model: null, timeoutMs: 3e5 }
29
+ });
30
+
15
31
  // src/agents/codex/manifest.ts
16
32
  var codexManifest = defineAgentManifest({
17
33
  id: "codex",
18
34
  name: "Codex",
19
35
  tagline: "OpenAI's coding agent, driven non-interactively through codex exec.",
20
36
  docsUrl: "https://developers.openai.com/codex/cli",
21
- icon: "Terminal",
37
+ icon: "Codex",
22
38
  accent: "bg-neutral-900 text-white",
23
39
  binaries: ["codex"],
24
40
  installHint: "Install with npm i -g @openai/codex, then run codex login.",
@@ -34,7 +50,7 @@ var cursorAgentManifest = defineAgentManifest({
34
50
  name: "Cursor Agent",
35
51
  tagline: "Cursor's CLI agent, driven through its print mode.",
36
52
  docsUrl: "https://cursor.com/docs/cli",
37
- icon: "MousePointerClick",
53
+ icon: "Cursor",
38
54
  accent: "bg-blue-600 text-white",
39
55
  binaries: ["cursor-agent"],
40
56
  installHint: "Install with curl https://cursor.com/install -fsS | bash, then run cursor-agent login.",
@@ -45,10 +61,7 @@ var cursorAgentManifest = defineAgentManifest({
45
61
  });
46
62
 
47
63
  // src/agents/manifests.ts
48
- var AGENT_MANIFESTS = [codexManifest, cursorAgentManifest];
49
-
50
- // src/agents/codex/runtime.ts
51
- import { readFileSync, rmSync } from "node:fs";
64
+ var AGENT_MANIFESTS = [codexManifest, claudeManifest, cursorAgentManifest];
52
65
 
53
66
  // src/agents/discover.ts
54
67
  import { execFile } from "node:child_process";
@@ -102,9 +115,9 @@ var ANSI = /\u001B\[[0-9;]*[A-Za-z]/g;
102
115
  function clean(text) {
103
116
  return text.replace(ANSI, "").trim();
104
117
  }
105
- async function probe(bin, args3, timeoutMs = 1e4) {
118
+ async function probe(bin, args4, timeoutMs = 1e4) {
106
119
  try {
107
- const { stdout, stderr } = await exec(bin, args3, { timeout: timeoutMs });
120
+ const { stdout, stderr } = await exec(bin, args4, { timeout: timeoutMs });
108
121
  return { ok: true, text: clean(stdout) || clean(stderr) };
109
122
  } catch (error) {
110
123
  const failure = error;
@@ -234,13 +247,155 @@ ${failure}` : stderr),
234
247
  });
235
248
  }
236
249
 
237
- // src/agents/codex/runtime.ts
250
+ // src/agents/claude/events.ts
251
+ function parse(line) {
252
+ const trimmed = line.trim();
253
+ if (!trimmed.startsWith("{")) return null;
254
+ try {
255
+ return JSON.parse(trimmed);
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+ function blocks(event) {
261
+ return event?.type === "assistant" ? event.message?.content ?? [] : [];
262
+ }
263
+ function describeTool(block, cwd) {
264
+ const verb = (block.name ?? "a tool").toLowerCase();
265
+ const input = block.input ?? {};
266
+ const subject = input.file_path ?? input.path ?? input.command ?? input.pattern ?? input.url ?? input.query;
267
+ if (typeof subject !== "string") return verb;
268
+ const short = cwd && subject.startsWith(`${cwd}/`) ? subject.slice(cwd.length + 1) : subject;
269
+ return `${verb} ${short}`;
270
+ }
271
+ function describeEvent(line, cwd) {
272
+ const event = parse(line);
273
+ if (!event) return line.trim() === "" ? null : line;
274
+ switch (event.type) {
275
+ case "system":
276
+ if (event.subtype !== "init") return null;
277
+ return event.model ? `Started with ${event.model}.` : "Started.";
278
+ case "assistant": {
279
+ const tools = blocks(event).filter((block) => block.type === "tool_use").map((block) => `Running ${describeTool(block, cwd)}`);
280
+ return tools.length > 0 ? tools.join("\n") : null;
281
+ }
282
+ case "result": {
283
+ const seconds = event.duration_ms !== void 0 ? (event.duration_ms / 1e3).toFixed(0) : "?";
284
+ const tokens = event.usage?.output_tokens;
285
+ const spent = tokens === void 0 ? "" : `, ${tokens} tokens out`;
286
+ return event.is_error ? `Failed after ${seconds}s${spent}: ${event.result ?? event.subtype ?? "unknown error"}` : `Answered after ${seconds}s${spent}.`;
287
+ }
288
+ default:
289
+ return null;
290
+ }
291
+ }
292
+ function answerFrom(stdout) {
293
+ const events = stdout.split("\n").map(parse);
294
+ const result = events.filter((event) => event?.type === "result").at(-1);
295
+ const failed = result?.is_error === true;
296
+ if (typeof result?.result === "string" && result.result.trim() !== "") {
297
+ return { text: result.result.trim(), failed };
298
+ }
299
+ const said = events.flatMap(blocks);
300
+ const lastTool = said.findLastIndex((block) => block.type === "tool_use");
301
+ const text = said.slice(lastTool + 1).filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n").trim();
302
+ return { text, failed };
303
+ }
304
+
305
+ // src/agents/claude/runtime.ts
238
306
  async function binary() {
307
+ const found = await resolveBinary(claudeManifest.binaries);
308
+ if (!found) throw new Error("The claude binary was not found on this host.");
309
+ return found;
310
+ }
311
+ function args(input) {
312
+ const list = [
313
+ "-p",
314
+ // A stream of events rather than one block at the end, so a run can be
315
+ // watched. Print mode refuses stream-json without --verbose.
316
+ "--output-format",
317
+ "stream-json",
318
+ "--verbose",
319
+ // Session files would outlive a run that is meant to leave nothing behind.
320
+ "--no-session-persistence"
321
+ ];
322
+ if (input.settings.permissionMode === "read_only") {
323
+ list.push("--permission-mode", "dontAsk", "--tools", "Read,Grep,Glob");
324
+ } else {
325
+ list.push("--permission-mode", "acceptEdits", "--allowedTools", "Bash");
326
+ }
327
+ if (input.settings.model) list.push("--model", input.settings.model);
328
+ list.push("--", input.prompt);
329
+ return list;
330
+ }
331
+ var claudeRuntime = defineAgentRuntime({
332
+ async detect() {
333
+ const found = await resolveBinary(claudeManifest.binaries);
334
+ if (!found) return { installed: false };
335
+ const version = await probe(found, ["--version"]);
336
+ return { installed: true, binaryPath: found, version: firstLine(version.text) };
337
+ },
338
+ async authStatus() {
339
+ const found = await resolveBinary(claudeManifest.binaries);
340
+ if (!found) return { signedIn: false, detail: "Not installed." };
341
+ const result = await probe(found, ["auth", "status", "--json"]);
342
+ try {
343
+ const status = JSON.parse(result.text);
344
+ return status.loggedIn ? { signedIn: true, detail: `Signed in (${status.authMethod ?? "unknown method"}).` } : { signedIn: false, detail: "Run claude auth login." };
345
+ } catch {
346
+ return {
347
+ signedIn: result.ok,
348
+ detail: firstLine(result.text) || (result.ok ? "Signed in." : "Run claude auth login.")
349
+ };
350
+ }
351
+ },
352
+ invocation(input) {
353
+ return { bin: claudeManifest.binaries[0], args: args(input) };
354
+ },
355
+ async run(input) {
356
+ const bin = await binary();
357
+ const argv = args(input);
358
+ const outcome = await runProcess({
359
+ bin,
360
+ args: argv,
361
+ cwd: input.cwd,
362
+ timeoutMs: input.settings.timeoutMs,
363
+ signal: input.signal,
364
+ logPath: input.logFile,
365
+ logLine: (line) => describeEvent(line, input.cwd)
366
+ });
367
+ const command = `${bin} ${argv.join(" ")}`;
368
+ const answer = answerFrom(outcome.stdout);
369
+ if (outcome.aborted) {
370
+ return { ok: false, aborted: true, output: answer.text, durationMs: outcome.durationMs, command };
371
+ }
372
+ if (outcome.timedOut) {
373
+ return {
374
+ ok: false,
375
+ output: answer.text,
376
+ detail: `Claude Code did not finish within ${Math.round(input.settings.timeoutMs / 1e3)}s.`,
377
+ durationMs: outcome.durationMs,
378
+ command
379
+ };
380
+ }
381
+ return {
382
+ ok: outcome.code === 0 && !answer.failed,
383
+ output: answer.text,
384
+ detail: outcome.code === 0 && !answer.failed ? void 0 : answer.failed ? answer.text || "The agent reported an error." : outcome.stderr || `Exited with code ${outcome.code}.`,
385
+ durationMs: outcome.durationMs,
386
+ command
387
+ };
388
+ }
389
+ });
390
+
391
+ // src/agents/codex/runtime.ts
392
+ import { readFileSync, rmSync } from "node:fs";
393
+ async function binary2() {
239
394
  const found = await resolveBinary(codexManifest.binaries);
240
395
  if (!found) throw new Error("The codex binary was not found on this host.");
241
396
  return found;
242
397
  }
243
- function args(input) {
398
+ function args2(input) {
244
399
  const list = [
245
400
  "exec",
246
401
  "--sandbox",
@@ -276,11 +431,11 @@ var codexRuntime = defineAgentRuntime({
276
431
  };
277
432
  },
278
433
  invocation(input) {
279
- return { bin: codexManifest.binaries[0], args: args(input) };
434
+ return { bin: codexManifest.binaries[0], args: args2(input) };
280
435
  },
281
436
  async run(input) {
282
- const bin = await binary();
283
- const argv = args(input);
437
+ const bin = await binary2();
438
+ const argv = args2(input);
284
439
  const command = `${bin} ${argv.join(" ")}`;
285
440
  const outcome = await runProcess({
286
441
  bin,
@@ -323,7 +478,7 @@ var codexRuntime = defineAgentRuntime({
323
478
  });
324
479
 
325
480
  // src/agents/cursor-agent/events.ts
326
- function parse(line) {
481
+ function parse2(line) {
327
482
  const trimmed = line.trim();
328
483
  if (!trimmed.startsWith("{")) return null;
329
484
  try {
@@ -335,24 +490,24 @@ function parse(line) {
335
490
  function textOf(event) {
336
491
  return (event.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
337
492
  }
338
- function describeTool(call, cwd) {
493
+ function describeTool2(call, cwd) {
339
494
  const [name, body] = Object.entries(call ?? {})[0] ?? [];
340
495
  if (!name) return "a tool";
341
496
  const verb = name.replace(/ToolCall$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
342
- const args3 = body?.args ?? {};
343
- const subject = args3.path ?? args3.command ?? args3.query ?? args3.pattern;
497
+ const args4 = body?.args ?? {};
498
+ const subject = args4.path ?? args4.command ?? args4.query ?? args4.pattern;
344
499
  if (typeof subject !== "string") return verb;
345
500
  const short = cwd && subject.startsWith(`${cwd}/`) ? subject.slice(cwd.length + 1) : subject;
346
501
  return `${verb} ${short}`;
347
502
  }
348
- function describeEvent(line, cwd) {
349
- const event = parse(line);
503
+ function describeEvent2(line, cwd) {
504
+ const event = parse2(line);
350
505
  if (!event) return line.trim() === "" ? null : line;
351
506
  switch (event.type) {
352
507
  case "system":
353
508
  return event.model ? `Started with ${event.model}.` : "Started.";
354
509
  case "tool_call":
355
- return event.subtype === "started" ? `Running ${describeTool(event.tool_call, cwd)}` : null;
510
+ return event.subtype === "started" ? `Running ${describeTool2(event.tool_call, cwd)}` : null;
356
511
  case "assistant":
357
512
  return null;
358
513
  case "result": {
@@ -365,8 +520,8 @@ function describeEvent(line, cwd) {
365
520
  return null;
366
521
  }
367
522
  }
368
- function answerFrom(stdout) {
369
- const events = stdout.split("\n").map(parse);
523
+ function answerFrom2(stdout) {
524
+ const events = stdout.split("\n").map(parse2);
370
525
  const result = events.filter((event) => event?.type === "result").at(-1);
371
526
  const failed = result?.is_error === true;
372
527
  const whole = events.findLastIndex(
@@ -380,12 +535,12 @@ function answerFrom(stdout) {
380
535
  }
381
536
 
382
537
  // src/agents/cursor-agent/runtime.ts
383
- async function binary2() {
538
+ async function binary3() {
384
539
  const found = await resolveBinary(cursorAgentManifest.binaries);
385
540
  if (!found) throw new Error("The cursor-agent binary was not found on this host.");
386
541
  return found;
387
542
  }
388
- function args2(input) {
543
+ function args3(input) {
389
544
  const list = [
390
545
  "-p",
391
546
  // A stream of events rather than one block at the end: it can be watched
@@ -427,11 +582,11 @@ var cursorAgentRuntime = defineAgentRuntime({
427
582
  };
428
583
  },
429
584
  invocation(input) {
430
- return { bin: cursorAgentManifest.binaries[0], args: args2(input) };
585
+ return { bin: cursorAgentManifest.binaries[0], args: args3(input) };
431
586
  },
432
587
  async run(input) {
433
- const bin = await binary2();
434
- const argv = args2(input);
588
+ const bin = await binary3();
589
+ const argv = args3(input);
435
590
  const outcome = await runProcess({
436
591
  bin,
437
592
  args: argv,
@@ -439,10 +594,10 @@ var cursorAgentRuntime = defineAgentRuntime({
439
594
  timeoutMs: input.settings.timeoutMs,
440
595
  signal: input.signal,
441
596
  logPath: input.logFile,
442
- logLine: (line) => describeEvent(line, input.cwd)
597
+ logLine: (line) => describeEvent2(line, input.cwd)
443
598
  });
444
599
  const command = `${bin} ${argv.join(" ")}`;
445
- const answer = answerFrom(outcome.stdout);
600
+ const answer = answerFrom2(outcome.stdout);
446
601
  if (outcome.aborted) {
447
602
  return { ok: false, aborted: true, output: answer.text, durationMs: outcome.durationMs, command };
448
603
  }
@@ -468,6 +623,7 @@ var cursorAgentRuntime = defineAgentRuntime({
468
623
  // src/agents/runtimes.ts
469
624
  var RUNTIMES = {
470
625
  codex: codexRuntime,
626
+ claude: claudeRuntime,
471
627
  "cursor-agent": cursorAgentRuntime
472
628
  };
473
629
  function agentRuntime(id) {
@@ -495,23 +651,139 @@ async function detectAgents() {
495
651
  }
496
652
 
497
653
  // src/server/agent-runner.ts
498
- import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
499
- import { join as join3 } from "node:path";
654
+ import { existsSync, mkdirSync as mkdirSync2, statSync, writeFileSync } from "node:fs";
655
+ import { homedir as homedir3 } from "node:os";
656
+ import { join as join4 } from "node:path";
657
+
658
+ // src/server/agent-job.ts
659
+ import { isAbsolute, join as join2 } from "node:path";
660
+
661
+ // package.json
662
+ var package_default = {
663
+ name: "loopable-cli",
664
+ version: "0.2.1",
665
+ description: "CLI to run Loopable: App, Dispatcher, and Runner.",
666
+ license: "MIT",
667
+ type: "module",
668
+ bin: {
669
+ loopable: "./dist/cli.mjs"
670
+ },
671
+ files: [
672
+ ".output",
673
+ "dist",
674
+ "drizzle"
675
+ ],
676
+ engines: {
677
+ node: ">=22"
678
+ },
679
+ packageManager: "pnpm@11.22.0",
680
+ repository: {
681
+ type: "git",
682
+ url: "git+https://github.com/imsobear/loopable.git"
683
+ },
684
+ bugs: {
685
+ url: "https://github.com/imsobear/loopable/issues"
686
+ },
687
+ homepage: "https://github.com/imsobear/loopable#readme",
688
+ publishConfig: {
689
+ access: "public"
690
+ },
691
+ imports: {
692
+ "#/*": "./src/*"
693
+ },
694
+ scripts: {
695
+ dev: "node --experimental-strip-types scripts/dev.ts",
696
+ start: "node dist/cli.mjs start",
697
+ dispatcher: "LOOPABLE_DEV=1 node --experimental-strip-types src/cli.ts dispatcher",
698
+ runner: "LOOPABLE_DEV=1 node --experimental-strip-types src/cli.ts runner",
699
+ "generate-routes": "tsr generate",
700
+ build: "vite build && node scripts/build-cli.mjs",
701
+ preview: "vite preview",
702
+ test: "vitest run",
703
+ "test:watch": "vitest",
704
+ "db:generate": "drizzle-kit generate",
705
+ "db:studio": "LOOPABLE_DEV=1 drizzle-kit studio",
706
+ typecheck: "tsc --noEmit",
707
+ release: "node scripts/release.mjs",
708
+ deploy: "LOOPABLE_CF=1 vite build && wrangler deploy",
709
+ "site:dev": "pnpm --filter loopable-site dev",
710
+ "site:build": "pnpm --filter loopable-site build",
711
+ "site:deploy": "pnpm --filter loopable-site deploy",
712
+ prepublishOnly: "pnpm typecheck && pnpm test && pnpm build"
713
+ },
714
+ dependencies: {
715
+ "@libsql/client": "^0.18.0"
716
+ },
717
+ devDependencies: {
718
+ "@base-ui/react": "^1.7.0",
719
+ "@cloudflare/containers": "^0.3.7",
720
+ "@cloudflare/vite-plugin": "^1.54.10",
721
+ "@fontsource-variable/geist": "^5.3.0",
722
+ "@tailwindcss/vite": "4.1.18",
723
+ "@tanstack/devtools-vite": "0.8.5",
724
+ "@tanstack/react-devtools": "0.10.12",
725
+ "@tanstack/react-router": "1.170.32",
726
+ "@tanstack/react-router-devtools": "1.167.1",
727
+ "@tanstack/react-start": "1.168.49",
728
+ "@tanstack/router-cli": "1.167.33",
729
+ "@types/node": "^22.10.2",
730
+ "@types/qrcode": "^1.5.6",
731
+ "@types/react": "^19.2.0",
732
+ "@types/react-dom": "^19.2.0",
733
+ "@vitejs/plugin-react": "^6.0.1",
734
+ "class-variance-authority": "^0.7.1",
735
+ clsx: "^2.1.1",
736
+ "drizzle-kit": "^0.31.10",
737
+ "drizzle-orm": "^0.45.2",
738
+ esbuild: "^0.28.2",
739
+ "lucide-react": "^1.39.0",
740
+ "next-themes": "^0.4.6",
741
+ nitro: "3.0.260610-beta",
742
+ qrcode: "^1.5.4",
743
+ react: "19.2.0",
744
+ "react-dom": "19.2.0",
745
+ shadcn: "^4.19.1",
746
+ sonner: "^2.0.8",
747
+ "tailwind-merge": "^3.6.0",
748
+ tailwindcss: "4.1.18",
749
+ "tw-animate-css": "^1.4.0",
750
+ typescript: "^6.0.2",
751
+ vite: "^8.0.0",
752
+ vitest: "^4.1.11",
753
+ wrangler: "^4.132.0"
754
+ }
755
+ };
756
+
757
+ // src/lib/version.ts
758
+ var VERSION = package_default.version;
759
+ var RUNNER_PROTOCOL = 2;
760
+
761
+ // src/server/agent-job.ts
762
+ var WORKSPACE = "{{loopable:workspace}}";
763
+ function normalizeFolder(folder) {
764
+ const trimmed = folder.trim();
765
+ if (trimmed === "~") return "";
766
+ return trimmed.startsWith("~/") ? trimmed.slice(2) : trimmed;
767
+ }
768
+ function resolveFolder(folder, home) {
769
+ const normal = normalizeFolder(folder);
770
+ return isAbsolute(normal) ? normal : join2(home, normal);
771
+ }
500
772
 
501
773
  // src/server/paths.ts
502
774
  import { mkdirSync } from "node:fs";
503
775
  import { homedir as homedir2 } from "node:os";
504
- import { join as join2 } from "node:path";
776
+ import { join as join3 } from "node:path";
505
777
  function isDevProfile() {
506
778
  return process.env.LOOPABLE_DEV === "1";
507
779
  }
508
780
  function dataDir() {
509
- const dir = process.env.LOOPABLE_HOME ?? join2(homedir2(), isDevProfile() ? ".loopable-dev" : ".loopable");
781
+ const dir = process.env.LOOPABLE_HOME ?? join3(homedir2(), isDevProfile() ? ".loopable-dev" : ".loopable");
510
782
  mkdirSync(dir, { recursive: true });
511
783
  return dir;
512
784
  }
513
785
  function runDir(taskId) {
514
- const dir = join2(dataDir(), "runs", taskId);
786
+ const dir = join3(dataDir(), "runs", taskId);
515
787
  mkdirSync(dir, { recursive: true });
516
788
  return dir;
517
789
  }
@@ -521,14 +793,27 @@ async function runAgentJob(job, options = {}) {
521
793
  const workspace = options.workspace ?? runDir(job.taskId);
522
794
  mkdirSync2(workspace, { recursive: true });
523
795
  for (const file of job.files) {
524
- writeFileSync(join3(workspace, file.name), file.body);
796
+ writeFileSync(join4(workspace, file.name), file.body);
797
+ }
798
+ const logFile = join4(workspace, "agent.log");
799
+ const outputFile = join4(workspace, "answer.txt");
800
+ let cwd = workspace;
801
+ if (job.cwd) {
802
+ cwd = resolveFolder(job.cwd, homedir3());
803
+ if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
804
+ return {
805
+ ok: false,
806
+ output: "",
807
+ detail: `There is no folder at ${cwd} on this runner.`,
808
+ durationMs: 0,
809
+ command: ""
810
+ };
811
+ }
525
812
  }
526
- const logFile = join3(workspace, "agent.log");
527
- const outputFile = join3(workspace, "answer.txt");
528
- const cwd = job.cwd ?? workspace;
529
813
  const run = options.run ?? ((input) => agentRuntime(job.agentId).run(input));
530
814
  const result = await run({
531
- prompt: job.prompt,
815
+ // Context files sit in this runner's workspace, which only it knows.
816
+ prompt: job.prompt.replaceAll(WORKSPACE, workspace),
532
817
  cwd,
533
818
  settings: job.settings,
534
819
  outputFile,
@@ -546,6 +831,7 @@ async function runAgentJob(job, options = {}) {
546
831
  }
547
832
 
548
833
  // src/runner/client.ts
834
+ var BUILD = { version: VERSION, protocol: RUNNER_PROTOCOL };
549
835
  async function json(url2, token, path, body) {
550
836
  const res = await fetch(`${url2}${path}`, {
551
837
  method: "POST",
@@ -571,7 +857,7 @@ async function json(url2, token, path, body) {
571
857
  function watchLog(path, onChunk) {
572
858
  let offset = 0;
573
859
  const flush = () => {
574
- if (!existsSync(path)) return;
860
+ if (!existsSync2(path)) return;
575
861
  const fd = openSync(path, "r");
576
862
  try {
577
863
  const size = fstatSync(fd).size;
@@ -595,12 +881,13 @@ async function runRemoteLoop(input) {
595
881
  const inventory = await detectAgents();
596
882
  const hello = await json(url2, input.joinToken, "/api/runners/hello", {
597
883
  hostname: osHostname(),
598
- inventory
884
+ inventory,
885
+ ...BUILD
599
886
  });
600
887
  input.log(`joined as ${hello.name} (${hello.runnerId})`);
601
888
  const token = hello.runnerToken;
602
889
  async function heartbeat() {
603
- await json(url2, token, "/api/runners/heartbeat", { inventory: await detectAgents() });
890
+ await json(url2, token, "/api/runners/heartbeat", { inventory: await detectAgents(), ...BUILD });
604
891
  }
605
892
  await heartbeat();
606
893
  const beat = setInterval(() => void heartbeat().catch((error) => {
@@ -622,8 +909,8 @@ async function runRemoteLoop(input) {
622
909
  }
623
910
  const job = claimed.job;
624
911
  input.log(`running ${job.taskId}`);
625
- const workspace = mkdtempSync(join4(tmpdir(), `loopable-${job.taskId}-`));
626
- const stopLog = watchLog(join4(workspace, "agent.log"), (chunk) => {
912
+ const workspace = mkdtempSync(join5(tmpdir(), `loopable-${job.taskId}-`));
913
+ const stopLog = watchLog(join5(workspace, "agent.log"), (chunk) => {
627
914
  void json(url2, token, `/api/runners/jobs/${job.taskId}/log`, { chunk }).catch((error) => {
628
915
  input.log(`log failed: ${error instanceof Error ? error.message : String(error)}`);
629
916
  });
@@ -0,0 +1,2 @@
1
+ -- An inbox prompt has nothing to write, so a finished one is done, not prepared.
2
+ UPDATE `tasks` SET `state` = 'done' WHERE `source_kind` = 'prompt' AND `state` = 'prepared' AND `dry_run` = 0;
@@ -0,0 +1,3 @@
1
+ -- Loops no longer have separate team notes. Keep what was written by moving it
2
+ -- onto the end of the prompt, where it was sent anyway.
3
+ UPDATE `loops` SET `prompt` = `prompt` || char(10) || char(10) || trim(`guidance`), `guidance` = NULL WHERE `guidance` IS NOT NULL AND trim(`guidance`) <> '';
@@ -0,0 +1,5 @@
1
+ -- A loop's folder is relative to the runner's home now. Paths under a home
2
+ -- folder become the part after it; anything else is left as it was.
3
+ UPDATE `loops` SET `settings` = json_set(`settings`, '$.folder', substr(json_extract(`settings`, '$.folder'), 8 + instr(substr(json_extract(`settings`, '$.folder'), 8), '/'))) WHERE json_extract(`settings`, '$.folder') LIKE '/Users/%/%';
4
+ --> statement-breakpoint
5
+ UPDATE `loops` SET `settings` = json_set(`settings`, '$.folder', substr(json_extract(`settings`, '$.folder'), 7 + instr(substr(json_extract(`settings`, '$.folder'), 7), '/'))) WHERE json_extract(`settings`, '$.folder') LIKE '/home/%/%';
@@ -0,0 +1,5 @@
1
+ -- What a runner says it is: its package version, to show, and the protocol it
2
+ -- speaks, to decide which jobs it can take. Runners before 0.3 send neither.
3
+ ALTER TABLE `runners` ADD `version` text;
4
+ --> statement-breakpoint
5
+ ALTER TABLE `runners` ADD `protocol` integer;
@@ -176,6 +176,34 @@
176
176
  "when": 1789100000009,
177
177
  "tag": "0024_prompt_tasks",
178
178
  "breakpoints": true
179
+ },
180
+ {
181
+ "idx": 25,
182
+ "version": "6",
183
+ "when": 1789100000010,
184
+ "tag": "0025_prompt_tasks_done",
185
+ "breakpoints": true
186
+ },
187
+ {
188
+ "idx": 26,
189
+ "version": "6",
190
+ "when": 1789100000011,
191
+ "tag": "0026_guidance_into_prompt",
192
+ "breakpoints": true
193
+ },
194
+ {
195
+ "idx": 27,
196
+ "version": "6",
197
+ "when": 1789100000012,
198
+ "tag": "0027_relative_folders",
199
+ "breakpoints": true
200
+ },
201
+ {
202
+ "idx": 28,
203
+ "version": "6",
204
+ "when": 1789100000013,
205
+ "tag": "0028_runner_version",
206
+ "breakpoints": true
179
207
  }
180
208
  ]
181
- }
209
+ }