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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/_connectorId-CDCuSgpN.js +1 -0
- package/.output/public/assets/_loopId-DOnIsVTW.js +1 -0
- package/.output/public/assets/_taskId-BO5MZnlZ.js +1 -0
- package/.output/public/assets/agents-CBozi7oi.js +1 -0
- package/.output/public/assets/alert-Bn_wjgTX.js +1 -0
- package/.output/public/assets/button-K3ypIhAZ.js +1 -0
- package/.output/public/assets/card-DiVnaTIE.js +1 -0
- package/.output/public/assets/chevron-right-BrctLtFK.js +1 -0
- package/.output/public/assets/connection-status-DKWelvLO.js +1 -0
- package/.output/public/assets/connector-icon-CEbdE71G.js +1 -0
- package/.output/public/assets/connectors-JyX3IzJj.js +1 -0
- package/.output/public/assets/gaps-PjmcbaWo.js +1 -0
- package/.output/public/assets/inbox-CujRR0Cz.js +1 -0
- package/.output/public/assets/index-C9SQ_Baq.js +16 -0
- package/.output/public/assets/loop-form-Dw3cuTzw.js +1 -0
- package/.output/public/assets/loops-DcEfZa2_.js +1 -0
- package/.output/public/assets/manifests-yJpevyye.js +1 -0
- package/.output/public/assets/new-CwsnNuRW.js +1 -0
- package/.output/public/assets/page-bvnfRMum.js +1 -0
- package/.output/public/assets/play-yHLs4DjK.js +1 -0
- package/.output/public/assets/plus-Ber25lrU.js +1 -0
- package/.output/public/assets/preload-helper-BoU_7EQC.js +3 -0
- package/.output/public/assets/refresh-cw-BzSY7M8I.js +1 -0
- package/.output/public/assets/rotate-ccw-DLv9EfCX.js +1 -0
- package/.output/public/assets/runners-DzX-RU3i.js +2 -0
- package/.output/public/assets/select-C9grIFwG.js +1 -0
- package/.output/public/assets/setting-fields-DUc3npgh.js +3 -0
- package/.output/public/assets/styles-B9B98zLk.css +2 -0
- package/.output/public/assets/switch-DwABVI6d.js +1 -0
- package/.output/public/assets/task-list-Ddk8-qt7.js +1 -0
- package/.output/public/assets/textarea-Ds-gIYUQ.js +1 -0
- package/.output/public/assets/time-goBWd-5J.js +1 -0
- package/.output/public/assets/use-live-tasks-DZNNuECy.js +1 -0
- package/.output/public/assets/useCompositeListItem-P4Nmmn7j.js +1 -0
- package/.output/public/assets/usePositioner-B7i9Ejzh.js +1 -0
- package/.output/server/_chunks/ssr-renderer.mjs +1 -1
- package/.output/server/_connectorId-COuQN02a.mjs +512 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +1410 -804
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
- package/.output/server/_libs/lucide-react.mjs +93 -84
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_loopId-CZh6_Yhr.mjs +310 -0
- package/.output/server/_ssr/{callback-D20cVD2w.mjs → access-DTttr563.mjs} +37 -20
- package/.output/server/_ssr/{agents-CrUmIQpi.mjs → agents-B_qo1gpL.mjs} +196 -7
- package/.output/server/_ssr/{agents-Dms_6TBF.mjs → agents-Bz10gz_m.mjs} +8 -8
- package/.output/server/_ssr/agents-CjbOUp3Z.mjs +206 -0
- package/.output/server/_ssr/{alert-DQvzE7bu.mjs → alert-C2WBWfzo.mjs} +2 -2
- package/.output/server/_ssr/card-r1cV-PCN.mjs +36 -0
- package/.output/server/_ssr/{client-BtVRVhQ2.mjs → client-Dk3SD8Il.mjs} +5 -1
- package/.output/server/_ssr/connection-status-BogAL-Fj.mjs +32 -0
- package/.output/server/_ssr/{connections-ypex3qUd.mjs → connections-Cqvb6Gw2.mjs} +1109 -263
- package/.output/server/_ssr/connector-icon-DrhVbhs6.mjs +190 -0
- package/.output/server/_ssr/connectors-BfS5tQKr.mjs +81 -0
- package/.output/server/_ssr/{connectors-BZly0yGF.mjs → connectors-DJI-rzPU.mjs} +21 -10
- package/.output/server/_ssr/{createServerRpc-DPDrnFdH.mjs → createServerRpc-DDrp2nLT.mjs} +2 -2
- package/.output/server/_ssr/{gaps-D2Zh8sEo.mjs → gaps-CcvHiLcj.mjs} +18 -10
- package/.output/server/_ssr/inbox-D1ef761v.mjs +178 -0
- package/.output/server/_ssr/loop-form-DWwOR05R.mjs +346 -0
- package/.output/server/_ssr/{loops-Og_G3KyZ.mjs → loops-6WhUe_lc.mjs} +228 -25
- package/.output/server/_ssr/loops-DHPjzG-t.mjs +217 -0
- package/.output/server/_ssr/new-ChKBdcXJ.mjs +245 -0
- package/.output/server/_ssr/page-DZVxKvdn.mjs +120 -0
- package/.output/server/_ssr/{router-BbrBz36P.mjs → router-DdwREiOf.mjs} +167 -489
- package/.output/server/_ssr/runners-CK9Cx9fS.mjs +217 -0
- package/.output/server/_ssr/runners-CrznFPHt.mjs +140 -0
- package/.output/server/_ssr/runners-W-pgo5yo.mjs +55 -0
- package/.output/server/_ssr/{secrets-CRnpwGBh.mjs → secrets-CV5scFlt.mjs} +2 -2
- package/.output/server/_ssr/select-BtndHhHb.mjs +76 -0
- package/.output/server/_ssr/{setting-fields-mpXfGftI.mjs → setting-fields-LNgywAW7.mjs} +25 -20
- package/.output/server/_ssr/ssr.mjs +49 -40
- package/.output/server/_ssr/{start-BgTj-h_C.mjs → start-C_j-e4vF.mjs} +10 -6
- package/.output/server/_ssr/{switch-1pnd_bPJ.mjs → switch-odxNO_S4.mjs} +3 -3
- package/.output/server/_ssr/task-list-Dnmq_WeE.mjs +150 -0
- package/.output/server/_ssr/{tasks-LIi-W4cy.mjs → tasks-D-kXLgVP.mjs} +13 -13
- package/.output/server/_ssr/{tasks-B_QmDFVs.mjs → tasks-D7aUvA33.mjs} +55 -17
- package/.output/server/_ssr/{textarea-CHJTKtsJ.mjs → textarea-D2c_vTIC.mjs} +2 -2
- package/.output/server/_ssr/time-Cy8TZP0Z.mjs +13 -0
- package/.output/server/_ssr/use-live-tasks-BzhTkEOL.mjs +40 -0
- package/.output/server/_tanstack-start-manifest_v-2HUdn-2E.mjs +166 -0
- package/.output/server/_taskId-D3KkNJW1.mjs +263 -0
- package/.output/server/index.mjs +204 -162
- package/README.md +54 -19
- package/dist/cli.mjs +185 -67
- package/dist/dispatcher.mjs +1443 -277
- package/dist/runner.mjs +333 -46
- package/drizzle/0025_prompt_tasks_done.sql +2 -0
- package/drizzle/0026_guidance_into_prompt.sql +3 -0
- package/drizzle/0027_relative_folders.sql +5 -0
- package/drizzle/0028_runner_version.sql +5 -0
- package/drizzle/meta/_journal.json +29 -1
- package/package.json +23 -17
- package/.output/public/assets/_connectorId-m67WbiJ8.js +0 -1
- package/.output/public/assets/_loopId-Bf4K5sdd.js +0 -1
- package/.output/public/assets/_taskId-eCKkcVOx.js +0 -1
- package/.output/public/assets/alert-BT2EVMJP.js +0 -1
- package/.output/public/assets/arrow-left-CWUZt6k2.js +0 -1
- package/.output/public/assets/arrow-right-dy_oOg_c.js +0 -1
- package/.output/public/assets/connection-status-DCqqmY_6.js +0 -1
- package/.output/public/assets/connector-icon-qGpNsPgH.js +0 -1
- package/.output/public/assets/connectors-DxXlc9zF.js +0 -1
- package/.output/public/assets/external-link-DC11orFA.js +0 -1
- package/.output/public/assets/gaps-CoTA-PzA.js +0 -1
- package/.output/public/assets/inbox-CLEXUXCI.js +0 -1
- package/.output/public/assets/index-Cndsw84p.js +0 -16
- package/.output/public/assets/loader-B4Epchm_.js +0 -1
- package/.output/public/assets/loop-form-BvizlZdR.js +0 -1
- package/.output/public/assets/loops-Dbe_mf5T.js +0 -1
- package/.output/public/assets/new-DPEaXKAY.js +0 -1
- package/.output/public/assets/play-DD4xQgvi.js +0 -1
- package/.output/public/assets/preload-helper-CfdAYKD7.js +0 -3
- package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +0 -1
- package/.output/public/assets/runners-BVBggS5B.js +0 -1
- package/.output/public/assets/select-Cppu-hti.js +0 -1
- package/.output/public/assets/setting-fields-BT1naVJX.js +0 -3
- package/.output/public/assets/styles-D3sonCfX.css +0 -2
- package/.output/public/assets/switch-hRIbNGOj.js +0 -1
- package/.output/public/assets/task-list-CrptnI1N.js +0 -1
- package/.output/public/assets/textarea-Fyuzi6Vp.js +0 -1
- package/.output/public/assets/trash-2-BXZzBsoV.js +0 -1
- package/.output/public/assets/useValueChanged-VVyq4hb9.js +0 -1
- package/.output/server/_connectorId-DcSnoRjk.mjs +0 -421
- package/.output/server/_loopId-G_TAkYtS.mjs +0 -272
- package/.output/server/_ssr/access-B7TcOZ7F.mjs +0 -42
- package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +0 -31
- package/.output/server/_ssr/connector-icon-CFrrHjzR.mjs +0 -37
- package/.output/server/_ssr/connectors-4yzKb0KG.mjs +0 -106
- package/.output/server/_ssr/inbox-C2VBGLa5.mjs +0 -114
- package/.output/server/_ssr/loop-form-D-U-kU4k.mjs +0 -413
- package/.output/server/_ssr/loops-D0XJ8eBb.mjs +0 -222
- package/.output/server/_ssr/new-CajvgIog.mjs +0 -169
- package/.output/server/_ssr/runners-B_WuF8UJ.mjs +0 -32
- package/.output/server/_ssr/runners-D-0_lfiq.mjs +0 -106
- package/.output/server/_ssr/runners-zqF_zbEc.mjs +0 -116
- package/.output/server/_ssr/task-list-BnQJ94Dk.mjs +0 -187
- package/.output/server/_tanstack-start-manifest_v-BCHAazE8.mjs +0 -129
- 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
|
|
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: "
|
|
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: "
|
|
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,
|
|
118
|
+
async function probe(bin, args4, timeoutMs = 1e4) {
|
|
106
119
|
try {
|
|
107
|
-
const { stdout, stderr } = await exec(bin,
|
|
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/
|
|
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
|
|
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:
|
|
434
|
+
return { bin: codexManifest.binaries[0], args: args2(input) };
|
|
280
435
|
},
|
|
281
436
|
async run(input) {
|
|
282
|
-
const bin = await
|
|
283
|
-
const argv =
|
|
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
|
|
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
|
|
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
|
|
343
|
-
const subject =
|
|
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
|
|
349
|
-
const event =
|
|
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 ${
|
|
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
|
|
369
|
-
const events = stdout.split("\n").map(
|
|
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
|
|
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
|
|
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:
|
|
585
|
+
return { bin: cursorAgentManifest.binaries[0], args: args3(input) };
|
|
431
586
|
},
|
|
432
587
|
async run(input) {
|
|
433
|
-
const bin = await
|
|
434
|
-
const argv =
|
|
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) =>
|
|
597
|
+
logLine: (line) => describeEvent2(line, input.cwd)
|
|
443
598
|
});
|
|
444
599
|
const command = `${bin} ${argv.join(" ")}`;
|
|
445
|
-
const answer =
|
|
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 {
|
|
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
|
|
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 ??
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
626
|
-
const stopLog = watchLog(
|
|
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,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
|
+
}
|