loopable-cli 0.1.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 +17 -0
- package/.output/public/apple-touch-icon.png +0 -0
- package/.output/public/assets/_connectorId-BWjYn5tm.js +1 -0
- package/.output/public/assets/_loopId-Ds4RkyEc.js +1 -0
- package/.output/public/assets/_taskId-BOzICXDf.js +1 -0
- package/.output/public/assets/alert-Bn9-Zm0l.js +1 -0
- package/.output/public/assets/arrow-left-BF9juV9T.js +1 -0
- package/.output/public/assets/arrow-right-CzqvnOY1.js +1 -0
- package/.output/public/assets/button-HCY6DCY-.js +1 -0
- package/.output/public/assets/connection-status-CYZB8kLy.js +1 -0
- package/.output/public/assets/connector-icon-BZ_MUJV_.js +1 -0
- package/.output/public/assets/connectors-BvzttfSu.js +1 -0
- package/.output/public/assets/connectors-DdJNwRN_.js +1 -0
- package/.output/public/assets/external-link-D46X-bXa.js +1 -0
- package/.output/public/assets/gaps-D-I6xiDc.js +1 -0
- package/.output/public/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/.output/public/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/.output/public/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/.output/public/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/.output/public/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/.output/public/assets/inbox-BUUu9xxE.js +1 -0
- package/.output/public/assets/index-ztzex0FN.js +10 -0
- package/.output/public/assets/loader-CcBl2u8m.js +1 -0
- package/.output/public/assets/loop-form-dB3CLcZb.js +1 -0
- package/.output/public/assets/loops-DRunQrxL.js +1 -0
- package/.output/public/assets/loops-oSdqMd29.js +1 -0
- package/.output/public/assets/manifests-DNMqf7Vi.js +7 -0
- package/.output/public/assets/new-uyFxVjuB.js +1 -0
- package/.output/public/assets/preload-helper-ORbuV86A.js +3 -0
- package/.output/public/assets/runners-DSU3r5Ww.js +1 -0
- package/.output/public/assets/setting-fields-ixkpdBb-.js +3 -0
- package/.output/public/assets/styles-DZL5V1o7.css +2 -0
- package/.output/public/assets/switch-d5ummmHY.js +1 -0
- package/.output/public/assets/task-list-BpOStt9v.js +1 -0
- package/.output/public/assets/tasks-B8MsKgic.js +1 -0
- package/.output/public/assets/trash-2-D6u6NpzO.js +1 -0
- package/.output/public/favicon.svg +9 -0
- package/.output/server/_chunks/ssr-renderer.mjs +26 -0
- package/.output/server/_connectorId-8nb5yOzG.mjs +421 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +12841 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +14349 -0
- package/.output/server/_libs/class-variance-authority+clsx.mjs +69 -0
- package/.output/server/_libs/dijkstrajs.mjs +118 -0
- package/.output/server/_libs/drizzle-orm.mjs +4349 -0
- package/.output/server/_libs/h3+rou3+srvx.mjs +1361 -0
- package/.output/server/_libs/lucide-react.mjs +911 -0
- package/.output/server/_libs/next-themes.mjs +53 -0
- package/.output/server/_libs/pngjs.mjs +1680 -0
- package/.output/server/_libs/qrcode.mjs +2583 -0
- package/.output/server/_libs/sonner.mjs +955 -0
- package/.output/server/_libs/tailwind-merge.mjs +3380 -0
- package/.output/server/_libs/tanstack__history.mjs +341 -0
- package/.output/server/_libs/ufo.mjs +64 -0
- package/.output/server/_loopId-BsUL1SeL.mjs +272 -0
- package/.output/server/_runtime.mjs +35 -0
- package/.output/server/_ssr/access-zIl15o8y.mjs +42 -0
- package/.output/server/_ssr/agents-Btql102J.mjs +657 -0
- package/.output/server/_ssr/agents-DAOZPgO2.mjs +76 -0
- package/.output/server/_ssr/alert-NEiOCTOl.mjs +37 -0
- package/.output/server/_ssr/callback-CUINvl7M.mjs +62 -0
- package/.output/server/_ssr/client-CKkOfh-s.mjs +335 -0
- package/.output/server/_ssr/connection-status-Bi5peUAk.mjs +31 -0
- package/.output/server/_ssr/connections-qnw2GVlP.mjs +2542 -0
- package/.output/server/_ssr/connector-icon-DZXQLLFo.mjs +37 -0
- package/.output/server/_ssr/connectors-BHNwuIMj.mjs +106 -0
- package/.output/server/_ssr/connectors-C28g0i4c.mjs +83 -0
- package/.output/server/_ssr/createServerRpc-DPDrnFdH.mjs +12 -0
- package/.output/server/_ssr/empty-plugin-adapters-BJ4m9L8g.mjs +5 -0
- package/.output/server/_ssr/gaps-RAFSXLZf.mjs +53 -0
- package/.output/server/_ssr/inbox-k5zHergU.mjs +35 -0
- package/.output/server/_ssr/loop-form-DlObSewZ.mjs +412 -0
- package/.output/server/_ssr/loops-APFcP0y6.mjs +311 -0
- package/.output/server/_ssr/loops-xt2Bn7_q.mjs +222 -0
- package/.output/server/_ssr/new-DfKyd2Wa.mjs +169 -0
- package/.output/server/_ssr/router-3WmpE5b_.mjs +1553 -0
- package/.output/server/_ssr/runners-By-mwc3F.mjs +32 -0
- package/.output/server/_ssr/runners-DI_NxpHC.mjs +116 -0
- package/.output/server/_ssr/runners-DtXs4Q8a.mjs +106 -0
- package/.output/server/_ssr/secrets-Hqti-niS.mjs +199 -0
- package/.output/server/_ssr/setting-fields-8vKFj4df.mjs +92 -0
- package/.output/server/_ssr/ssr.mjs +6605 -0
- package/.output/server/_ssr/start-Bdc2GJUI.mjs +51 -0
- package/.output/server/_ssr/switch-ReNglgH6.mjs +18 -0
- package/.output/server/_ssr/task-list-CUITIqkb.mjs +186 -0
- package/.output/server/_ssr/tasks-SAjNovX6.mjs +110 -0
- package/.output/server/_ssr/tasks-vcoodp_D.mjs +533 -0
- package/.output/server/_tanstack-start-manifest_v-C0cP5F0M.mjs +125 -0
- package/.output/server/_taskId-CXS3lod0.mjs +249 -0
- package/.output/server/index.mjs +541 -0
- package/README.md +81 -0
- package/dist/cli.mjs +165 -0
- package/dist/dispatcher.mjs +9871 -0
- package/dist/runner.mjs +657 -0
- package/drizzle/0000_cold_demogoblin.sql +29 -0
- package/drizzle/0001_fancy_blonde_phantom.sql +13 -0
- package/drizzle/0002_spooky_smiling_tiger.sql +16 -0
- package/drizzle/0003_small_golden_guardian.sql +25 -0
- package/drizzle/0004_wonderful_thunderbolts.sql +39 -0
- package/drizzle/0005_named_workflows.sql +7 -0
- package/drizzle/0006_signals.sql +18 -0
- package/drizzle/0007_review_comments.sql +1 -0
- package/drizzle/0008_held_signals.sql +1 -0
- package/drizzle/0009_work_item_ref.sql +58 -0
- package/drizzle/0010_signal_payload.sql +2 -0
- package/drizzle/0011_rule_poll_cursor.sql +1 -0
- package/drizzle/0012_loops.sql +63 -0
- package/drizzle/0013_loop_owns_action_and_prompt.sql +154 -0
- package/drizzle/0014_loop_poll_every.sql +4 -0
- package/drizzle/0015_machines.sql +10 -0
- package/drizzle/0016_task_machine.sql +1 -0
- package/drizzle/0017_loop_machine.sql +1 -0
- package/drizzle/0018_runners.sql +1 -0
- package/drizzle/0019_task_runner.sql +1 -0
- package/drizzle/0020_loop_runner.sql +1 -0
- package/drizzle/0021_drop_runner_kind.sql +1 -0
- package/drizzle/0022_drop_loop_runner.sql +1 -0
- package/drizzle/0023_shared_host.sql +6 -0
- package/drizzle/meta/0000_snapshot.json +206 -0
- package/drizzle/meta/0001_snapshot.json +284 -0
- package/drizzle/meta/0002_snapshot.json +390 -0
- package/drizzle/meta/0003_snapshot.json +566 -0
- package/drizzle/meta/0004_snapshot.json +633 -0
- package/drizzle/meta/0005_snapshot.json +626 -0
- package/drizzle/meta/0006_snapshot.json +752 -0
- package/drizzle/meta/0007_snapshot.json +759 -0
- package/drizzle/meta/0008_snapshot.json +766 -0
- package/drizzle/meta/0009_snapshot.json +752 -0
- package/drizzle/meta/0010_snapshot.json +766 -0
- package/drizzle/meta/0011_snapshot.json +773 -0
- package/drizzle/meta/0012_snapshot.json +773 -0
- package/drizzle/meta/0013_snapshot.json +817 -0
- package/drizzle/meta/_journal.json +174 -0
- package/package.json +88 -0
package/dist/runner.mjs
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
// src/runner/client.ts
|
|
2
|
+
import { closeSync, existsSync, fstatSync, mkdtempSync, openSync, readSync, rmSync as rmSync2 } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { hostname as osHostname } from "node:os";
|
|
5
|
+
import { join as join4 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/agents/define.ts
|
|
8
|
+
function defineAgentManifest(manifest) {
|
|
9
|
+
return manifest;
|
|
10
|
+
}
|
|
11
|
+
function defineAgentRuntime(runtime) {
|
|
12
|
+
return runtime;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/agents/codex/manifest.ts
|
|
16
|
+
var codexManifest = defineAgentManifest({
|
|
17
|
+
id: "codex",
|
|
18
|
+
name: "Codex",
|
|
19
|
+
tagline: "OpenAI's coding agent, driven non-interactively through codex exec.",
|
|
20
|
+
docsUrl: "https://developers.openai.com/codex/cli",
|
|
21
|
+
icon: "Terminal",
|
|
22
|
+
accent: "bg-neutral-900 text-white",
|
|
23
|
+
binaries: ["codex"],
|
|
24
|
+
installHint: "Install with npm i -g @openai/codex, then run codex login.",
|
|
25
|
+
permissionModes: ["read_only", "workspace_write"],
|
|
26
|
+
supportsModel: true,
|
|
27
|
+
modelPlaceholder: "leave empty for the Codex default",
|
|
28
|
+
defaults: { permissionMode: "read_only", model: null, timeoutMs: 3e5 }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// src/agents/cursor-agent/manifest.ts
|
|
32
|
+
var cursorAgentManifest = defineAgentManifest({
|
|
33
|
+
id: "cursor-agent",
|
|
34
|
+
name: "Cursor Agent",
|
|
35
|
+
tagline: "Cursor's CLI agent, driven through its print mode.",
|
|
36
|
+
docsUrl: "https://cursor.com/docs/cli",
|
|
37
|
+
icon: "MousePointerClick",
|
|
38
|
+
accent: "bg-blue-600 text-white",
|
|
39
|
+
binaries: ["cursor-agent"],
|
|
40
|
+
installHint: "Install with curl https://cursor.com/install -fsS | bash, then run cursor-agent login.",
|
|
41
|
+
permissionModes: ["read_only", "workspace_write"],
|
|
42
|
+
supportsModel: true,
|
|
43
|
+
modelPlaceholder: "e.g. gpt-5 or sonnet-4.5",
|
|
44
|
+
defaults: { permissionMode: "read_only", model: null, timeoutMs: 3e5 }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// src/agents/manifests.ts
|
|
48
|
+
var AGENT_MANIFESTS = [codexManifest, cursorAgentManifest];
|
|
49
|
+
|
|
50
|
+
// src/agents/codex/runtime.ts
|
|
51
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
52
|
+
|
|
53
|
+
// src/agents/discover.ts
|
|
54
|
+
import { execFile } from "node:child_process";
|
|
55
|
+
import { accessSync, constants } from "node:fs";
|
|
56
|
+
import { homedir } from "node:os";
|
|
57
|
+
import { join } from "node:path";
|
|
58
|
+
import { promisify } from "node:util";
|
|
59
|
+
var exec = promisify(execFile);
|
|
60
|
+
var HOME_DIRS = [".local/bin", ".bun/bin", ".cargo/bin", ".volta/bin", ".deno/bin"];
|
|
61
|
+
var SYSTEM_DIRS = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
|
62
|
+
function unique(values) {
|
|
63
|
+
return [...new Set(values.filter(Boolean))];
|
|
64
|
+
}
|
|
65
|
+
function inheritedDirs() {
|
|
66
|
+
const home = homedir();
|
|
67
|
+
return unique([
|
|
68
|
+
...(process.env.PATH ?? "").split(":"),
|
|
69
|
+
...HOME_DIRS.map((dir) => join(home, dir)),
|
|
70
|
+
...SYSTEM_DIRS
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
var loginDirs = null;
|
|
74
|
+
async function loginShellDirs() {
|
|
75
|
+
if (loginDirs) return loginDirs;
|
|
76
|
+
try {
|
|
77
|
+
const shell = process.env.SHELL ?? "/bin/zsh";
|
|
78
|
+
const { stdout } = await exec(shell, ["-lc", 'printf %s "$PATH"'], { timeout: 5e3 });
|
|
79
|
+
loginDirs = unique(stdout.split(":"));
|
|
80
|
+
} catch {
|
|
81
|
+
loginDirs = [];
|
|
82
|
+
}
|
|
83
|
+
return loginDirs;
|
|
84
|
+
}
|
|
85
|
+
function executableIn(dirs, names) {
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const candidate = join(dir, name);
|
|
89
|
+
try {
|
|
90
|
+
accessSync(candidate, constants.X_OK);
|
|
91
|
+
return candidate;
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
async function resolveBinary(names) {
|
|
99
|
+
return executableIn(inheritedDirs(), names) ?? executableIn(await loginShellDirs(), names);
|
|
100
|
+
}
|
|
101
|
+
var ANSI = /\u001B\[[0-9;]*[A-Za-z]/g;
|
|
102
|
+
function clean(text) {
|
|
103
|
+
return text.replace(ANSI, "").trim();
|
|
104
|
+
}
|
|
105
|
+
async function probe(bin, args3, timeoutMs = 1e4) {
|
|
106
|
+
try {
|
|
107
|
+
const { stdout, stderr } = await exec(bin, args3, { timeout: timeoutMs });
|
|
108
|
+
return { ok: true, text: clean(stdout) || clean(stderr) };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const failure = error;
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
text: clean(failure.stdout ?? "") || clean(failure.stderr ?? "") || (failure.message ?? "failed")
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function firstLine(text) {
|
|
118
|
+
return clean(text).split("\n")[0]?.trim() ?? "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/agents/process.ts
|
|
122
|
+
import { spawn } from "node:child_process";
|
|
123
|
+
import { createWriteStream } from "node:fs";
|
|
124
|
+
var WITHHELD = [/^GITHUB_/, /^GH_/, /^LOOPABLE_/];
|
|
125
|
+
function agentEnvironment() {
|
|
126
|
+
const env = {};
|
|
127
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
128
|
+
if (WITHHELD.some((pattern) => pattern.test(key))) continue;
|
|
129
|
+
env[key] = value;
|
|
130
|
+
}
|
|
131
|
+
return env;
|
|
132
|
+
}
|
|
133
|
+
function signalGroup(pid, signal) {
|
|
134
|
+
if (pid === void 0) return;
|
|
135
|
+
try {
|
|
136
|
+
process.kill(-pid, signal);
|
|
137
|
+
} catch {
|
|
138
|
+
try {
|
|
139
|
+
process.kill(pid, signal);
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function runProcess(input) {
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
const started = Date.now();
|
|
147
|
+
if (input.signal?.aborted) {
|
|
148
|
+
resolve({ code: null, stdout: "", stderr: "", timedOut: false, aborted: true, durationMs: 0 });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const child = spawn(input.bin, input.args, {
|
|
152
|
+
cwd: input.cwd,
|
|
153
|
+
env: agentEnvironment(),
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
155
|
+
// Its own process group, so it can be stopped as a family. Agent CLIs
|
|
156
|
+
// start helpers of their own, and signalling only the process we
|
|
157
|
+
// launched leaves those behind, reparented to init and still working.
|
|
158
|
+
detached: true
|
|
159
|
+
});
|
|
160
|
+
let stdout = "";
|
|
161
|
+
let stderr = "";
|
|
162
|
+
let timedOut = false;
|
|
163
|
+
let aborted = false;
|
|
164
|
+
const halt = () => {
|
|
165
|
+
signalGroup(child.pid, "SIGTERM");
|
|
166
|
+
setTimeout(() => signalGroup(child.pid, "SIGKILL"), 3e3).unref();
|
|
167
|
+
};
|
|
168
|
+
const timer = setTimeout(() => {
|
|
169
|
+
timedOut = true;
|
|
170
|
+
halt();
|
|
171
|
+
}, input.timeoutMs);
|
|
172
|
+
const onAbort = () => {
|
|
173
|
+
aborted = true;
|
|
174
|
+
halt();
|
|
175
|
+
};
|
|
176
|
+
input.signal?.addEventListener("abort", onAbort);
|
|
177
|
+
const logFile = input.logPath ? createWriteStream(input.logPath, { flags: "a" }) : null;
|
|
178
|
+
logFile?.on("error", () => {
|
|
179
|
+
});
|
|
180
|
+
const lineWriter = () => {
|
|
181
|
+
let pending = "";
|
|
182
|
+
const emit = (line) => {
|
|
183
|
+
const text = input.logLine ? input.logLine(line) : line;
|
|
184
|
+
if (text !== null && text !== "") logFile?.write(`${text}
|
|
185
|
+
`);
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
take(chunk) {
|
|
189
|
+
if (!logFile) return;
|
|
190
|
+
pending += chunk;
|
|
191
|
+
const lines = pending.split("\n");
|
|
192
|
+
pending = lines.pop() ?? "";
|
|
193
|
+
for (const line of lines) emit(line);
|
|
194
|
+
},
|
|
195
|
+
flush() {
|
|
196
|
+
if (logFile && pending !== "") emit(pending);
|
|
197
|
+
pending = "";
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
const outLines = lineWriter();
|
|
202
|
+
const errLines = lineWriter();
|
|
203
|
+
child.stdout.on("data", (chunk) => {
|
|
204
|
+
const text = chunk.toString();
|
|
205
|
+
stdout += text;
|
|
206
|
+
outLines.take(text);
|
|
207
|
+
});
|
|
208
|
+
child.stderr.on("data", (chunk) => {
|
|
209
|
+
const text = chunk.toString();
|
|
210
|
+
stderr += text;
|
|
211
|
+
errLines.take(text);
|
|
212
|
+
});
|
|
213
|
+
const finish = (code, failure) => {
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
216
|
+
outLines.flush();
|
|
217
|
+
errLines.flush();
|
|
218
|
+
if (failure) logFile?.write(`
|
|
219
|
+
${failure}
|
|
220
|
+
`);
|
|
221
|
+
logFile?.end();
|
|
222
|
+
resolve({
|
|
223
|
+
code,
|
|
224
|
+
stdout: clean(stdout),
|
|
225
|
+
stderr: clean(failure ? `${stderr}
|
|
226
|
+
${failure}` : stderr),
|
|
227
|
+
timedOut,
|
|
228
|
+
aborted,
|
|
229
|
+
durationMs: Date.now() - started
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
child.on("error", (error) => finish(null, error.message));
|
|
233
|
+
child.on("close", (code) => finish(code));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/agents/codex/runtime.ts
|
|
238
|
+
async function binary() {
|
|
239
|
+
const found = await resolveBinary(codexManifest.binaries);
|
|
240
|
+
if (!found) throw new Error("The codex binary was not found on this host.");
|
|
241
|
+
return found;
|
|
242
|
+
}
|
|
243
|
+
function args(input) {
|
|
244
|
+
const list = [
|
|
245
|
+
"exec",
|
|
246
|
+
"--sandbox",
|
|
247
|
+
input.settings.permissionMode === "read_only" ? "read-only" : "workspace-write",
|
|
248
|
+
// The workspace is a scratch directory rather than a checkout, and session
|
|
249
|
+
// files would outlive a run that is meant to leave nothing behind.
|
|
250
|
+
"--skip-git-repo-check",
|
|
251
|
+
"--ephemeral",
|
|
252
|
+
"--color",
|
|
253
|
+
"never",
|
|
254
|
+
"-C",
|
|
255
|
+
input.cwd
|
|
256
|
+
];
|
|
257
|
+
if (input.settings.model) list.push("-m", input.settings.model);
|
|
258
|
+
if (input.outputFile) list.push("-o", input.outputFile);
|
|
259
|
+
list.push(input.prompt);
|
|
260
|
+
return list;
|
|
261
|
+
}
|
|
262
|
+
var codexRuntime = defineAgentRuntime({
|
|
263
|
+
async detect() {
|
|
264
|
+
const found = await resolveBinary(codexManifest.binaries);
|
|
265
|
+
if (!found) return { installed: false };
|
|
266
|
+
const version = await probe(found, ["--version"]);
|
|
267
|
+
return { installed: true, binaryPath: found, version: firstLine(version.text) };
|
|
268
|
+
},
|
|
269
|
+
async authStatus() {
|
|
270
|
+
const found = await resolveBinary(codexManifest.binaries);
|
|
271
|
+
if (!found) return { signedIn: false, detail: "Not installed." };
|
|
272
|
+
const result = await probe(found, ["login", "status"]);
|
|
273
|
+
return {
|
|
274
|
+
signedIn: result.ok,
|
|
275
|
+
detail: firstLine(result.text) || (result.ok ? "Signed in." : "Run codex login.")
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
invocation(input) {
|
|
279
|
+
return { bin: codexManifest.binaries[0], args: args(input) };
|
|
280
|
+
},
|
|
281
|
+
async run(input) {
|
|
282
|
+
const bin = await binary();
|
|
283
|
+
const argv = args(input);
|
|
284
|
+
const command = `${bin} ${argv.join(" ")}`;
|
|
285
|
+
const outcome = await runProcess({
|
|
286
|
+
bin,
|
|
287
|
+
args: argv,
|
|
288
|
+
cwd: input.cwd,
|
|
289
|
+
timeoutMs: input.settings.timeoutMs,
|
|
290
|
+
signal: input.signal,
|
|
291
|
+
logPath: input.logFile
|
|
292
|
+
});
|
|
293
|
+
let output = "";
|
|
294
|
+
if (input.outputFile) {
|
|
295
|
+
try {
|
|
296
|
+
output = readFileSync(input.outputFile, "utf8").trim();
|
|
297
|
+
rmSync(input.outputFile, { force: true });
|
|
298
|
+
} catch {
|
|
299
|
+
output = "";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!output) output = outcome.stdout;
|
|
303
|
+
if (outcome.aborted) {
|
|
304
|
+
return { ok: false, aborted: true, output, durationMs: outcome.durationMs, command };
|
|
305
|
+
}
|
|
306
|
+
if (outcome.timedOut) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
output,
|
|
310
|
+
detail: `Codex did not finish within ${Math.round(input.settings.timeoutMs / 1e3)}s.`,
|
|
311
|
+
durationMs: outcome.durationMs,
|
|
312
|
+
command
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
ok: outcome.code === 0,
|
|
317
|
+
output,
|
|
318
|
+
detail: outcome.code === 0 ? void 0 : outcome.stderr || `Exited with code ${outcome.code}.`,
|
|
319
|
+
durationMs: outcome.durationMs,
|
|
320
|
+
command
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// src/agents/cursor-agent/events.ts
|
|
326
|
+
function parse(line) {
|
|
327
|
+
const trimmed = line.trim();
|
|
328
|
+
if (!trimmed.startsWith("{")) return null;
|
|
329
|
+
try {
|
|
330
|
+
return JSON.parse(trimmed);
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function textOf(event) {
|
|
336
|
+
return (event.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
|
|
337
|
+
}
|
|
338
|
+
function describeTool(call, cwd) {
|
|
339
|
+
const [name, body] = Object.entries(call ?? {})[0] ?? [];
|
|
340
|
+
if (!name) return "a tool";
|
|
341
|
+
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;
|
|
344
|
+
if (typeof subject !== "string") return verb;
|
|
345
|
+
const short = cwd && subject.startsWith(`${cwd}/`) ? subject.slice(cwd.length + 1) : subject;
|
|
346
|
+
return `${verb} ${short}`;
|
|
347
|
+
}
|
|
348
|
+
function describeEvent(line, cwd) {
|
|
349
|
+
const event = parse(line);
|
|
350
|
+
if (!event) return line.trim() === "" ? null : line;
|
|
351
|
+
switch (event.type) {
|
|
352
|
+
case "system":
|
|
353
|
+
return event.model ? `Started with ${event.model}.` : "Started.";
|
|
354
|
+
case "tool_call":
|
|
355
|
+
return event.subtype === "started" ? `Running ${describeTool(event.tool_call, cwd)}` : null;
|
|
356
|
+
case "assistant":
|
|
357
|
+
return null;
|
|
358
|
+
case "result": {
|
|
359
|
+
const seconds = event.duration_ms ? (event.duration_ms / 1e3).toFixed(0) : "?";
|
|
360
|
+
const tokens = event.usage?.outputTokens;
|
|
361
|
+
const spent = tokens === void 0 ? "" : `, ${tokens} tokens out`;
|
|
362
|
+
return event.is_error ? `Failed after ${seconds}s${spent}.` : `Answered after ${seconds}s${spent}.`;
|
|
363
|
+
}
|
|
364
|
+
default:
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function answerFrom(stdout) {
|
|
369
|
+
const events = stdout.split("\n").map(parse);
|
|
370
|
+
const result = events.filter((event) => event?.type === "result").at(-1);
|
|
371
|
+
const failed = result?.is_error === true;
|
|
372
|
+
const whole = events.findLastIndex(
|
|
373
|
+
(event) => event?.type === "assistant" && event.timestamp_ms === void 0
|
|
374
|
+
);
|
|
375
|
+
if (whole >= 0) return { text: textOf(events[whole]).trim(), failed };
|
|
376
|
+
const lastTool = events.findLastIndex((event) => event?.type === "tool_call");
|
|
377
|
+
const partial = events.slice(lastTool + 1).filter((event) => event?.type === "assistant").map((event) => textOf(event));
|
|
378
|
+
if (partial.length > 0) return { text: partial.join("").trim(), failed };
|
|
379
|
+
return { text: (result?.result ?? "").trim(), failed };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/agents/cursor-agent/runtime.ts
|
|
383
|
+
async function binary2() {
|
|
384
|
+
const found = await resolveBinary(cursorAgentManifest.binaries);
|
|
385
|
+
if (!found) throw new Error("The cursor-agent binary was not found on this host.");
|
|
386
|
+
return found;
|
|
387
|
+
}
|
|
388
|
+
function args2(input) {
|
|
389
|
+
const list = [
|
|
390
|
+
"-p",
|
|
391
|
+
// A stream of events rather than one block at the end: it can be watched
|
|
392
|
+
// while it runs, and the final answer arrives as a field of its own
|
|
393
|
+
// instead of as whatever reached stdout.
|
|
394
|
+
"--output-format",
|
|
395
|
+
"stream-json",
|
|
396
|
+
"--stream-partial-output",
|
|
397
|
+
"--workspace",
|
|
398
|
+
input.cwd,
|
|
399
|
+
// Without this the agent stops to ask whether the workspace is trusted,
|
|
400
|
+
// and in print mode there is nobody to answer.
|
|
401
|
+
"--trust"
|
|
402
|
+
];
|
|
403
|
+
if (input.settings.permissionMode === "read_only") {
|
|
404
|
+
list.push("--mode", "ask");
|
|
405
|
+
} else {
|
|
406
|
+
list.push("--force");
|
|
407
|
+
}
|
|
408
|
+
if (input.settings.model) list.push("--model", input.settings.model);
|
|
409
|
+
list.push(input.prompt);
|
|
410
|
+
return list;
|
|
411
|
+
}
|
|
412
|
+
var cursorAgentRuntime = defineAgentRuntime({
|
|
413
|
+
async detect() {
|
|
414
|
+
const found = await resolveBinary(cursorAgentManifest.binaries);
|
|
415
|
+
if (!found) return { installed: false };
|
|
416
|
+
const version = await probe(found, ["--version"]);
|
|
417
|
+
return { installed: true, binaryPath: found, version: firstLine(version.text) };
|
|
418
|
+
},
|
|
419
|
+
async authStatus() {
|
|
420
|
+
const found = await resolveBinary(cursorAgentManifest.binaries);
|
|
421
|
+
if (!found) return { signedIn: false, detail: "Not installed." };
|
|
422
|
+
const result = await probe(found, ["status"]);
|
|
423
|
+
const detail = firstLine(result.text);
|
|
424
|
+
return {
|
|
425
|
+
signedIn: result.ok && !/not logged in|logged out/i.test(detail),
|
|
426
|
+
detail: detail || (result.ok ? "Signed in." : "Run cursor-agent login.")
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
invocation(input) {
|
|
430
|
+
return { bin: cursorAgentManifest.binaries[0], args: args2(input) };
|
|
431
|
+
},
|
|
432
|
+
async run(input) {
|
|
433
|
+
const bin = await binary2();
|
|
434
|
+
const argv = args2(input);
|
|
435
|
+
const outcome = await runProcess({
|
|
436
|
+
bin,
|
|
437
|
+
args: argv,
|
|
438
|
+
cwd: input.cwd,
|
|
439
|
+
timeoutMs: input.settings.timeoutMs,
|
|
440
|
+
signal: input.signal,
|
|
441
|
+
logPath: input.logFile,
|
|
442
|
+
logLine: (line) => describeEvent(line, input.cwd)
|
|
443
|
+
});
|
|
444
|
+
const command = `${bin} ${argv.join(" ")}`;
|
|
445
|
+
const answer = answerFrom(outcome.stdout);
|
|
446
|
+
if (outcome.aborted) {
|
|
447
|
+
return { ok: false, aborted: true, output: answer.text, durationMs: outcome.durationMs, command };
|
|
448
|
+
}
|
|
449
|
+
if (outcome.timedOut) {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
output: answer.text,
|
|
453
|
+
detail: `Cursor Agent did not finish within ${Math.round(input.settings.timeoutMs / 1e3)}s.`,
|
|
454
|
+
durationMs: outcome.durationMs,
|
|
455
|
+
command
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
ok: outcome.code === 0 && !answer.failed,
|
|
460
|
+
output: answer.text,
|
|
461
|
+
detail: outcome.code === 0 && !answer.failed ? void 0 : answer.failed ? answer.text || "The agent reported an error." : outcome.stderr || `Exited with code ${outcome.code}.`,
|
|
462
|
+
durationMs: outcome.durationMs,
|
|
463
|
+
command
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// src/agents/runtimes.ts
|
|
469
|
+
var RUNTIMES = {
|
|
470
|
+
codex: codexRuntime,
|
|
471
|
+
"cursor-agent": cursorAgentRuntime
|
|
472
|
+
};
|
|
473
|
+
function agentRuntime(id) {
|
|
474
|
+
const runtime = RUNTIMES[id];
|
|
475
|
+
if (!runtime) throw new Error(`Unknown agent: ${id}`);
|
|
476
|
+
return runtime;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/agents/inventory.ts
|
|
480
|
+
async function detectAgents() {
|
|
481
|
+
return Promise.all(
|
|
482
|
+
AGENT_MANIFESTS.map(async (manifest) => {
|
|
483
|
+
const runtime = agentRuntime(manifest.id);
|
|
484
|
+
const detection = await runtime.detect();
|
|
485
|
+
const auth = detection.installed ? await runtime.authStatus() : null;
|
|
486
|
+
return {
|
|
487
|
+
agentId: manifest.id,
|
|
488
|
+
installed: detection.installed,
|
|
489
|
+
version: detection.installed ? detection.version : null,
|
|
490
|
+
signedIn: Boolean(auth?.signedIn),
|
|
491
|
+
detail: auth?.detail ?? ""
|
|
492
|
+
};
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/server/agent-runner.ts
|
|
498
|
+
import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
499
|
+
import { join as join3 } from "node:path";
|
|
500
|
+
|
|
501
|
+
// src/server/paths.ts
|
|
502
|
+
import { mkdirSync } from "node:fs";
|
|
503
|
+
import { homedir as homedir2 } from "node:os";
|
|
504
|
+
import { join as join2 } from "node:path";
|
|
505
|
+
function isDevProfile() {
|
|
506
|
+
return process.env.LOOPABLE_DEV === "1";
|
|
507
|
+
}
|
|
508
|
+
function dataDir() {
|
|
509
|
+
const dir = process.env.LOOPABLE_HOME ?? join2(homedir2(), isDevProfile() ? ".loopable-dev" : ".loopable");
|
|
510
|
+
mkdirSync(dir, { recursive: true });
|
|
511
|
+
return dir;
|
|
512
|
+
}
|
|
513
|
+
function runDir(taskId) {
|
|
514
|
+
const dir = join2(dataDir(), "runs", taskId);
|
|
515
|
+
mkdirSync(dir, { recursive: true });
|
|
516
|
+
return dir;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/server/agent-runner.ts
|
|
520
|
+
async function runAgentJob(job, options = {}) {
|
|
521
|
+
const workspace = options.workspace ?? runDir(job.taskId);
|
|
522
|
+
mkdirSync2(workspace, { recursive: true });
|
|
523
|
+
for (const file of job.files) {
|
|
524
|
+
writeFileSync(join3(workspace, file.name), file.body);
|
|
525
|
+
}
|
|
526
|
+
const logFile = join3(workspace, "agent.log");
|
|
527
|
+
const outputFile = join3(workspace, "answer.txt");
|
|
528
|
+
const cwd = job.cwd ?? workspace;
|
|
529
|
+
const run = options.run ?? ((input) => agentRuntime(job.agentId).run(input));
|
|
530
|
+
const result = await run({
|
|
531
|
+
prompt: job.prompt,
|
|
532
|
+
cwd,
|
|
533
|
+
settings: job.settings,
|
|
534
|
+
outputFile,
|
|
535
|
+
logFile,
|
|
536
|
+
signal: options.signal
|
|
537
|
+
});
|
|
538
|
+
return {
|
|
539
|
+
ok: result.ok,
|
|
540
|
+
output: result.output,
|
|
541
|
+
detail: result.detail,
|
|
542
|
+
aborted: result.aborted,
|
|
543
|
+
durationMs: result.durationMs,
|
|
544
|
+
command: result.command
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/runner/client.ts
|
|
549
|
+
async function json(url2, token, path, body) {
|
|
550
|
+
const res = await fetch(`${url2}${path}`, {
|
|
551
|
+
method: "POST",
|
|
552
|
+
headers: {
|
|
553
|
+
Authorization: `Bearer ${token}`,
|
|
554
|
+
"Content-Type": "application/json"
|
|
555
|
+
},
|
|
556
|
+
body: JSON.stringify(body)
|
|
557
|
+
});
|
|
558
|
+
const text = await res.text();
|
|
559
|
+
let parsed = null;
|
|
560
|
+
try {
|
|
561
|
+
parsed = text ? JSON.parse(text) : null;
|
|
562
|
+
} catch {
|
|
563
|
+
parsed = { error: text };
|
|
564
|
+
}
|
|
565
|
+
if (!res.ok) {
|
|
566
|
+
const error = parsed?.error ?? res.statusText;
|
|
567
|
+
throw new Error(error);
|
|
568
|
+
}
|
|
569
|
+
return parsed;
|
|
570
|
+
}
|
|
571
|
+
function watchLog(path, onChunk) {
|
|
572
|
+
let offset = 0;
|
|
573
|
+
const flush = () => {
|
|
574
|
+
if (!existsSync(path)) return;
|
|
575
|
+
const fd = openSync(path, "r");
|
|
576
|
+
try {
|
|
577
|
+
const size = fstatSync(fd).size;
|
|
578
|
+
if (size <= offset) return;
|
|
579
|
+
const buf = Buffer.alloc(size - offset);
|
|
580
|
+
readSync(fd, buf, 0, buf.length, offset);
|
|
581
|
+
offset = size;
|
|
582
|
+
onChunk(buf.toString("utf8"));
|
|
583
|
+
} finally {
|
|
584
|
+
closeSync(fd);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
const timer = setInterval(flush, 400);
|
|
588
|
+
return () => {
|
|
589
|
+
clearInterval(timer);
|
|
590
|
+
flush();
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async function runRemoteLoop(input) {
|
|
594
|
+
const url2 = input.url.replace(/\/$/, "");
|
|
595
|
+
const inventory = await detectAgents();
|
|
596
|
+
const hello = await json(url2, input.joinToken, "/api/runners/hello", {
|
|
597
|
+
hostname: osHostname(),
|
|
598
|
+
inventory
|
|
599
|
+
});
|
|
600
|
+
input.log(`joined as ${hello.name} (${hello.runnerId})`);
|
|
601
|
+
const token = hello.runnerToken;
|
|
602
|
+
async function heartbeat() {
|
|
603
|
+
await json(url2, token, "/api/runners/heartbeat", { inventory: await detectAgents() });
|
|
604
|
+
}
|
|
605
|
+
await heartbeat();
|
|
606
|
+
const beat = setInterval(() => void heartbeat().catch((error) => {
|
|
607
|
+
input.log(`heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
608
|
+
}), 15e3);
|
|
609
|
+
let stopping = false;
|
|
610
|
+
const shutdown = () => {
|
|
611
|
+
stopping = true;
|
|
612
|
+
clearInterval(beat);
|
|
613
|
+
};
|
|
614
|
+
process.on("SIGINT", shutdown);
|
|
615
|
+
process.on("SIGTERM", shutdown);
|
|
616
|
+
while (!stopping) {
|
|
617
|
+
try {
|
|
618
|
+
const claimed = await json(url2, token, "/api/runners/claim", {});
|
|
619
|
+
if (!claimed.job) {
|
|
620
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const job = claimed.job;
|
|
624
|
+
input.log(`running ${job.taskId}`);
|
|
625
|
+
const workspace = mkdtempSync(join4(tmpdir(), `loopable-${job.taskId}-`));
|
|
626
|
+
const stopLog = watchLog(join4(workspace, "agent.log"), (chunk) => {
|
|
627
|
+
void json(url2, token, `/api/runners/jobs/${job.taskId}/log`, { chunk }).catch((error) => {
|
|
628
|
+
input.log(`log failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
try {
|
|
632
|
+
const result = await runAgentJob(job, { workspace });
|
|
633
|
+
await json(url2, token, `/api/runners/jobs/${job.taskId}/complete`, result);
|
|
634
|
+
} finally {
|
|
635
|
+
stopLog();
|
|
636
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
637
|
+
}
|
|
638
|
+
} catch (error) {
|
|
639
|
+
input.log(`claim failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
640
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/runner/main.ts
|
|
646
|
+
function log(message) {
|
|
647
|
+
process.stdout.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}
|
|
648
|
+
`);
|
|
649
|
+
}
|
|
650
|
+
var url = process.env.LOOPABLE_URL;
|
|
651
|
+
var joinToken = process.env.LOOPABLE_RUNNER_TOKEN;
|
|
652
|
+
if (!url || !joinToken) {
|
|
653
|
+
log("Set LOOPABLE_URL and LOOPABLE_RUNNER_TOKEN, then start again.");
|
|
654
|
+
process.exitCode = 1;
|
|
655
|
+
} else {
|
|
656
|
+
await runRemoteLoop({ url, joinToken, log });
|
|
657
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
CREATE TABLE `auth_attempts` (
|
|
2
|
+
`state` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`connector_id` text NOT NULL,
|
|
4
|
+
`verifier` text NOT NULL,
|
|
5
|
+
`redirect_uri` text NOT NULL,
|
|
6
|
+
`return_to` text,
|
|
7
|
+
`expires_at` integer NOT NULL,
|
|
8
|
+
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
9
|
+
);
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
CREATE TABLE `connections` (
|
|
12
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
13
|
+
`connector_id` text NOT NULL,
|
|
14
|
+
`status` text DEFAULT 'connected' NOT NULL,
|
|
15
|
+
`account_id` text NOT NULL,
|
|
16
|
+
`account_label` text NOT NULL,
|
|
17
|
+
`account_url` text,
|
|
18
|
+
`avatar_url` text,
|
|
19
|
+
`scopes` text,
|
|
20
|
+
`settings` text DEFAULT '{}' NOT NULL,
|
|
21
|
+
`cursor` text,
|
|
22
|
+
`last_synced_at` integer,
|
|
23
|
+
`last_error` text,
|
|
24
|
+
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
25
|
+
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
26
|
+
);
|
|
27
|
+
--> statement-breakpoint
|
|
28
|
+
CREATE UNIQUE INDEX `connections_account_unique` ON `connections` (`connector_id`,`account_id`);--> statement-breakpoint
|
|
29
|
+
CREATE INDEX `connections_connector_idx` ON `connections` (`connector_id`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE `agent_settings` (
|
|
2
|
+
`agent_id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`permission_mode` text NOT NULL,
|
|
4
|
+
`model` text,
|
|
5
|
+
`timeout_ms` integer NOT NULL,
|
|
6
|
+
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE TABLE `app_settings` (
|
|
10
|
+
`key` text PRIMARY KEY NOT NULL,
|
|
11
|
+
`value` text NOT NULL,
|
|
12
|
+
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
13
|
+
);
|