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
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { f as eq } from "../_libs/drizzle-orm.mjs";
|
|
2
|
+
import { n as agentSettings, r as appSettings, s as db } from "./client-CKkOfh-s.mjs";
|
|
3
|
+
import { n as isHosted } from "./secrets-Hqti-niS.mjs";
|
|
4
|
+
import { accessSync, constants, createWriteStream, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { execFile, spawn } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/agents-Btql102J.js
|
|
10
|
+
function defineAgentManifest(manifest) {
|
|
11
|
+
return manifest;
|
|
12
|
+
}
|
|
13
|
+
function defineAgentRuntime(runtime) {
|
|
14
|
+
return runtime;
|
|
15
|
+
}
|
|
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: {
|
|
29
|
+
permissionMode: "read_only",
|
|
30
|
+
model: null,
|
|
31
|
+
timeoutMs: 3e5
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
var cursorAgentManifest = defineAgentManifest({
|
|
35
|
+
id: "cursor-agent",
|
|
36
|
+
name: "Cursor Agent",
|
|
37
|
+
tagline: "Cursor's CLI agent, driven through its print mode.",
|
|
38
|
+
docsUrl: "https://cursor.com/docs/cli",
|
|
39
|
+
icon: "MousePointerClick",
|
|
40
|
+
accent: "bg-blue-600 text-white",
|
|
41
|
+
binaries: ["cursor-agent"],
|
|
42
|
+
installHint: "Install with curl https://cursor.com/install -fsS | bash, then run cursor-agent login.",
|
|
43
|
+
permissionModes: ["read_only", "workspace_write"],
|
|
44
|
+
supportsModel: true,
|
|
45
|
+
modelPlaceholder: "e.g. gpt-5 or sonnet-4.5",
|
|
46
|
+
defaults: {
|
|
47
|
+
permissionMode: "read_only",
|
|
48
|
+
model: null,
|
|
49
|
+
timeoutMs: 3e5
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
/** Client-safe registry, listing only agents whose invocation we have verified. */
|
|
53
|
+
var AGENT_MANIFESTS = [codexManifest, cursorAgentManifest];
|
|
54
|
+
function agentManifest(id) {
|
|
55
|
+
return AGENT_MANIFESTS.find((manifest) => manifest.id === id);
|
|
56
|
+
}
|
|
57
|
+
var exec = promisify(execFile);
|
|
58
|
+
var HOME_DIRS = [
|
|
59
|
+
".local/bin",
|
|
60
|
+
".bun/bin",
|
|
61
|
+
".cargo/bin",
|
|
62
|
+
".volta/bin",
|
|
63
|
+
".deno/bin"
|
|
64
|
+
];
|
|
65
|
+
var SYSTEM_DIRS = [
|
|
66
|
+
"/opt/homebrew/bin",
|
|
67
|
+
"/usr/local/bin",
|
|
68
|
+
"/usr/bin",
|
|
69
|
+
"/bin"
|
|
70
|
+
];
|
|
71
|
+
function unique(values) {
|
|
72
|
+
return [...new Set(values.filter(Boolean))];
|
|
73
|
+
}
|
|
74
|
+
function inheritedDirs() {
|
|
75
|
+
const home = homedir();
|
|
76
|
+
return unique([
|
|
77
|
+
...(process.env.PATH ?? "").split(":"),
|
|
78
|
+
...HOME_DIRS.map((dir) => join(home, dir)),
|
|
79
|
+
...SYSTEM_DIRS
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
var loginDirs = null;
|
|
83
|
+
/**
|
|
84
|
+
* A packaged app or a launchd job inherits a minimal PATH, so agents installed
|
|
85
|
+
* under a shell-managed directory would look missing. Asking the login shell is
|
|
86
|
+
* the fallback, and it only happens when the usual places come up empty.
|
|
87
|
+
*/
|
|
88
|
+
async function loginShellDirs() {
|
|
89
|
+
if (loginDirs) return loginDirs;
|
|
90
|
+
try {
|
|
91
|
+
const { stdout } = await exec(process.env.SHELL ?? "/bin/zsh", ["-lc", "printf %s \"$PATH\""], { timeout: 5e3 });
|
|
92
|
+
loginDirs = unique(stdout.split(":"));
|
|
93
|
+
} catch {
|
|
94
|
+
loginDirs = [];
|
|
95
|
+
}
|
|
96
|
+
return loginDirs;
|
|
97
|
+
}
|
|
98
|
+
function executableIn(dirs, names) {
|
|
99
|
+
for (const dir of dirs) for (const name of names) {
|
|
100
|
+
const candidate = join(dir, name);
|
|
101
|
+
try {
|
|
102
|
+
accessSync(candidate, constants.X_OK);
|
|
103
|
+
return candidate;
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
async function resolveBinary(names) {
|
|
109
|
+
return executableIn(inheritedDirs(), names) ?? executableIn(await loginShellDirs(), names);
|
|
110
|
+
}
|
|
111
|
+
var ANSI = /\u001B\[[0-9;]*[A-Za-z]/g;
|
|
112
|
+
function clean(text) {
|
|
113
|
+
return text.replace(ANSI, "").trim();
|
|
114
|
+
}
|
|
115
|
+
/** Runs a short informational command such as --version or a status check. */
|
|
116
|
+
async function probe(bin, args, timeoutMs = 1e4) {
|
|
117
|
+
try {
|
|
118
|
+
const { stdout, stderr } = await exec(bin, args, { timeout: timeoutMs });
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
text: clean(stdout) || clean(stderr)
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const failure = error;
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
text: clean(failure.stdout ?? "") || clean(failure.stderr ?? "") || (failure.message ?? "failed")
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function firstLine(text) {
|
|
132
|
+
return clean(text).split("\n")[0]?.trim() ?? "";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Loopable's own credentials never reach an agent process. The agent keeps its
|
|
136
|
+
* own model credentials, which is why this is a denylist rather than an
|
|
137
|
+
* allowlist: CURSOR_API_KEY and friends have to survive.
|
|
138
|
+
*/
|
|
139
|
+
var WITHHELD = [
|
|
140
|
+
/^GITHUB_/,
|
|
141
|
+
/^GH_/,
|
|
142
|
+
/^LOOPABLE_/
|
|
143
|
+
];
|
|
144
|
+
function agentEnvironment() {
|
|
145
|
+
const env = {};
|
|
146
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
147
|
+
if (WITHHELD.some((pattern) => pattern.test(key))) continue;
|
|
148
|
+
env[key] = value;
|
|
149
|
+
}
|
|
150
|
+
return env;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A negative pid means the process group. Failing is normal here: the group
|
|
154
|
+
* is usually already gone, which is exactly what we wanted.
|
|
155
|
+
*/
|
|
156
|
+
function signalGroup(pid, signal) {
|
|
157
|
+
if (pid === void 0) return;
|
|
158
|
+
try {
|
|
159
|
+
process.kill(-pid, signal);
|
|
160
|
+
} catch {
|
|
161
|
+
try {
|
|
162
|
+
process.kill(pid, signal);
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* An agent that waits for input would otherwise hang a loop forever, so stdin
|
|
168
|
+
* is closed immediately and the timeout is enforced by us rather than trusted
|
|
169
|
+
* to the tool.
|
|
170
|
+
*/
|
|
171
|
+
function runProcess(input) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const started = Date.now();
|
|
174
|
+
if (input.signal?.aborted) {
|
|
175
|
+
resolve({
|
|
176
|
+
code: null,
|
|
177
|
+
stdout: "",
|
|
178
|
+
stderr: "",
|
|
179
|
+
timedOut: false,
|
|
180
|
+
aborted: true,
|
|
181
|
+
durationMs: 0
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const child = spawn(input.bin, input.args, {
|
|
186
|
+
cwd: input.cwd,
|
|
187
|
+
env: agentEnvironment(),
|
|
188
|
+
stdio: [
|
|
189
|
+
"ignore",
|
|
190
|
+
"pipe",
|
|
191
|
+
"pipe"
|
|
192
|
+
],
|
|
193
|
+
detached: true
|
|
194
|
+
});
|
|
195
|
+
let stdout = "";
|
|
196
|
+
let stderr = "";
|
|
197
|
+
let timedOut = false;
|
|
198
|
+
let aborted = false;
|
|
199
|
+
/** Ask the whole group first, insist shortly after. */
|
|
200
|
+
const halt = () => {
|
|
201
|
+
signalGroup(child.pid, "SIGTERM");
|
|
202
|
+
setTimeout(() => signalGroup(child.pid, "SIGKILL"), 3e3).unref();
|
|
203
|
+
};
|
|
204
|
+
const timer = setTimeout(() => {
|
|
205
|
+
timedOut = true;
|
|
206
|
+
halt();
|
|
207
|
+
}, input.timeoutMs);
|
|
208
|
+
const onAbort = () => {
|
|
209
|
+
aborted = true;
|
|
210
|
+
halt();
|
|
211
|
+
};
|
|
212
|
+
input.signal?.addEventListener("abort", onAbort);
|
|
213
|
+
const logFile = input.logPath ? createWriteStream(input.logPath, { flags: "a" }) : null;
|
|
214
|
+
logFile?.on("error", () => {});
|
|
215
|
+
/**
|
|
216
|
+
* A chunk is not a line, so each stream keeps its own remainder. Sharing
|
|
217
|
+
* one buffer would splice half a line of stderr into the middle of stdout.
|
|
218
|
+
*/
|
|
219
|
+
const lineWriter = () => {
|
|
220
|
+
let pending = "";
|
|
221
|
+
const emit = (line) => {
|
|
222
|
+
const text = input.logLine ? input.logLine(line) : line;
|
|
223
|
+
if (text !== null && text !== "") logFile?.write(`${text}\n`);
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
take(chunk) {
|
|
227
|
+
if (!logFile) return;
|
|
228
|
+
pending += chunk;
|
|
229
|
+
const lines = pending.split("\n");
|
|
230
|
+
pending = lines.pop() ?? "";
|
|
231
|
+
for (const line of lines) emit(line);
|
|
232
|
+
},
|
|
233
|
+
flush() {
|
|
234
|
+
if (logFile && pending !== "") emit(pending);
|
|
235
|
+
pending = "";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
const outLines = lineWriter();
|
|
240
|
+
const errLines = lineWriter();
|
|
241
|
+
child.stdout.on("data", (chunk) => {
|
|
242
|
+
const text = chunk.toString();
|
|
243
|
+
stdout += text;
|
|
244
|
+
outLines.take(text);
|
|
245
|
+
});
|
|
246
|
+
child.stderr.on("data", (chunk) => {
|
|
247
|
+
const text = chunk.toString();
|
|
248
|
+
stderr += text;
|
|
249
|
+
errLines.take(text);
|
|
250
|
+
});
|
|
251
|
+
const finish = (code, failure) => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
254
|
+
outLines.flush();
|
|
255
|
+
errLines.flush();
|
|
256
|
+
if (failure) logFile?.write(`\n${failure}\n`);
|
|
257
|
+
logFile?.end();
|
|
258
|
+
resolve({
|
|
259
|
+
code,
|
|
260
|
+
stdout: clean(stdout),
|
|
261
|
+
stderr: clean(failure ? `${stderr}\n${failure}` : stderr),
|
|
262
|
+
timedOut,
|
|
263
|
+
aborted,
|
|
264
|
+
durationMs: Date.now() - started
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
child.on("error", (error) => finish(null, error.message));
|
|
268
|
+
child.on("close", (code) => finish(code));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async function binary$1() {
|
|
272
|
+
const found = await resolveBinary(codexManifest.binaries);
|
|
273
|
+
if (!found) throw new Error("The codex binary was not found on this host.");
|
|
274
|
+
return found;
|
|
275
|
+
}
|
|
276
|
+
function args$1(input) {
|
|
277
|
+
const list = [
|
|
278
|
+
"exec",
|
|
279
|
+
"--sandbox",
|
|
280
|
+
input.settings.permissionMode === "read_only" ? "read-only" : "workspace-write",
|
|
281
|
+
"--skip-git-repo-check",
|
|
282
|
+
"--ephemeral",
|
|
283
|
+
"--color",
|
|
284
|
+
"never",
|
|
285
|
+
"-C",
|
|
286
|
+
input.cwd
|
|
287
|
+
];
|
|
288
|
+
if (input.settings.model) list.push("-m", input.settings.model);
|
|
289
|
+
if (input.outputFile) list.push("-o", input.outputFile);
|
|
290
|
+
list.push(input.prompt);
|
|
291
|
+
return list;
|
|
292
|
+
}
|
|
293
|
+
var codexRuntime = defineAgentRuntime({
|
|
294
|
+
async detect() {
|
|
295
|
+
const found = await resolveBinary(codexManifest.binaries);
|
|
296
|
+
if (!found) return { installed: false };
|
|
297
|
+
return {
|
|
298
|
+
installed: true,
|
|
299
|
+
binaryPath: found,
|
|
300
|
+
version: firstLine((await probe(found, ["--version"])).text)
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
async authStatus() {
|
|
304
|
+
const found = await resolveBinary(codexManifest.binaries);
|
|
305
|
+
if (!found) return {
|
|
306
|
+
signedIn: false,
|
|
307
|
+
detail: "Not installed."
|
|
308
|
+
};
|
|
309
|
+
const result = await probe(found, ["login", "status"]);
|
|
310
|
+
return {
|
|
311
|
+
signedIn: result.ok,
|
|
312
|
+
detail: firstLine(result.text) || (result.ok ? "Signed in." : "Run codex login.")
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
invocation(input) {
|
|
316
|
+
return {
|
|
317
|
+
bin: codexManifest.binaries[0],
|
|
318
|
+
args: args$1(input)
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
async run(input) {
|
|
322
|
+
const bin = await binary$1();
|
|
323
|
+
const argv = args$1(input);
|
|
324
|
+
const command = `${bin} ${argv.join(" ")}`;
|
|
325
|
+
const outcome = await runProcess({
|
|
326
|
+
bin,
|
|
327
|
+
args: argv,
|
|
328
|
+
cwd: input.cwd,
|
|
329
|
+
timeoutMs: input.settings.timeoutMs,
|
|
330
|
+
signal: input.signal,
|
|
331
|
+
logPath: input.logFile
|
|
332
|
+
});
|
|
333
|
+
let output = "";
|
|
334
|
+
if (input.outputFile) try {
|
|
335
|
+
output = readFileSync(input.outputFile, "utf8").trim();
|
|
336
|
+
rmSync(input.outputFile, { force: true });
|
|
337
|
+
} catch {
|
|
338
|
+
output = "";
|
|
339
|
+
}
|
|
340
|
+
if (!output) output = outcome.stdout;
|
|
341
|
+
if (outcome.aborted) return {
|
|
342
|
+
ok: false,
|
|
343
|
+
aborted: true,
|
|
344
|
+
output,
|
|
345
|
+
durationMs: outcome.durationMs,
|
|
346
|
+
command
|
|
347
|
+
};
|
|
348
|
+
if (outcome.timedOut) return {
|
|
349
|
+
ok: false,
|
|
350
|
+
output,
|
|
351
|
+
detail: `Codex did not finish within ${Math.round(input.settings.timeoutMs / 1e3)}s.`,
|
|
352
|
+
durationMs: outcome.durationMs,
|
|
353
|
+
command
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
ok: outcome.code === 0,
|
|
357
|
+
output,
|
|
358
|
+
detail: outcome.code === 0 ? void 0 : outcome.stderr || `Exited with code ${outcome.code}.`,
|
|
359
|
+
durationMs: outcome.durationMs,
|
|
360
|
+
command
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
function parse(line) {
|
|
365
|
+
const trimmed = line.trim();
|
|
366
|
+
if (!trimmed.startsWith("{")) return null;
|
|
367
|
+
try {
|
|
368
|
+
return JSON.parse(trimmed);
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function textOf(event) {
|
|
374
|
+
return (event.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
|
|
375
|
+
}
|
|
376
|
+
/** `readToolCall` with a path becomes `read <path>`. */
|
|
377
|
+
function describeTool(call, cwd) {
|
|
378
|
+
const [name, body] = Object.entries(call ?? {})[0] ?? [];
|
|
379
|
+
if (!name) return "a tool";
|
|
380
|
+
const verb = name.replace(/ToolCall$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
381
|
+
const args = body?.args ?? {};
|
|
382
|
+
const subject = args.path ?? args.command ?? args.query ?? args.pattern;
|
|
383
|
+
if (typeof subject !== "string") return verb;
|
|
384
|
+
return `${verb} ${cwd && subject.startsWith(`${cwd}/`) ? subject.slice(cwd.length + 1) : subject}`;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* One readable line for the log, or nothing when the event says nothing a
|
|
388
|
+
* person would want to read. Partial answer deltas are dropped: the whole
|
|
389
|
+
* answer arrives at the end, and showing both would print it twice.
|
|
390
|
+
*/
|
|
391
|
+
function describeEvent(line, cwd) {
|
|
392
|
+
const event = parse(line);
|
|
393
|
+
if (!event) return line.trim() === "" ? null : line;
|
|
394
|
+
switch (event.type) {
|
|
395
|
+
case "system": return event.model ? `Started with ${event.model}.` : "Started.";
|
|
396
|
+
case "tool_call": return event.subtype === "started" ? `Running ${describeTool(event.tool_call, cwd)}` : null;
|
|
397
|
+
case "assistant": return null;
|
|
398
|
+
case "result": {
|
|
399
|
+
const seconds = event.duration_ms ? (event.duration_ms / 1e3).toFixed(0) : "?";
|
|
400
|
+
const tokens = event.usage?.outputTokens;
|
|
401
|
+
const spent = tokens === void 0 ? "" : `, ${tokens} tokens out`;
|
|
402
|
+
return event.is_error ? `Failed after ${seconds}s${spent}.` : `Answered after ${seconds}s${spent}.`;
|
|
403
|
+
}
|
|
404
|
+
default: return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The answer, which is the agent's last message and not everything it said.
|
|
409
|
+
*
|
|
410
|
+
* The result event looks like the obvious source and is the wrong one: it
|
|
411
|
+
* concatenates every word of the run, so an answer taken from there begins
|
|
412
|
+
* "I'll start by reading the two files" and that is what gets posted. Each
|
|
413
|
+
* block of speech is streamed in stamped pieces and then repeated whole and
|
|
414
|
+
* unstamped, so the last unstamped one is the answer and the earlier ones are
|
|
415
|
+
* the agent thinking out loud between tools.
|
|
416
|
+
*/
|
|
417
|
+
function answerFrom(stdout) {
|
|
418
|
+
const events = stdout.split("\n").map(parse);
|
|
419
|
+
const result = events.filter((event) => event?.type === "result").at(-1);
|
|
420
|
+
const failed = result?.is_error === true;
|
|
421
|
+
const whole = events.findLastIndex((event) => event?.type === "assistant" && event.timestamp_ms === void 0);
|
|
422
|
+
if (whole >= 0) return {
|
|
423
|
+
text: textOf(events[whole]).trim(),
|
|
424
|
+
failed
|
|
425
|
+
};
|
|
426
|
+
const lastTool = events.findLastIndex((event) => event?.type === "tool_call");
|
|
427
|
+
const partial = events.slice(lastTool + 1).filter((event) => event?.type === "assistant").map((event) => textOf(event));
|
|
428
|
+
if (partial.length > 0) return {
|
|
429
|
+
text: partial.join("").trim(),
|
|
430
|
+
failed
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
text: (result?.result ?? "").trim(),
|
|
434
|
+
failed
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async function binary() {
|
|
438
|
+
const found = await resolveBinary(cursorAgentManifest.binaries);
|
|
439
|
+
if (!found) throw new Error("The cursor-agent binary was not found on this host.");
|
|
440
|
+
return found;
|
|
441
|
+
}
|
|
442
|
+
function args(input) {
|
|
443
|
+
const list = [
|
|
444
|
+
"-p",
|
|
445
|
+
"--output-format",
|
|
446
|
+
"stream-json",
|
|
447
|
+
"--stream-partial-output",
|
|
448
|
+
"--workspace",
|
|
449
|
+
input.cwd,
|
|
450
|
+
"--trust"
|
|
451
|
+
];
|
|
452
|
+
if (input.settings.permissionMode === "read_only") list.push("--mode", "ask");
|
|
453
|
+
else list.push("--force");
|
|
454
|
+
if (input.settings.model) list.push("--model", input.settings.model);
|
|
455
|
+
list.push(input.prompt);
|
|
456
|
+
return list;
|
|
457
|
+
}
|
|
458
|
+
/** Server-only registry. Never import this from a component. */
|
|
459
|
+
var RUNTIMES = {
|
|
460
|
+
codex: codexRuntime,
|
|
461
|
+
"cursor-agent": defineAgentRuntime({
|
|
462
|
+
async detect() {
|
|
463
|
+
const found = await resolveBinary(cursorAgentManifest.binaries);
|
|
464
|
+
if (!found) return { installed: false };
|
|
465
|
+
return {
|
|
466
|
+
installed: true,
|
|
467
|
+
binaryPath: found,
|
|
468
|
+
version: firstLine((await probe(found, ["--version"])).text)
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
async authStatus() {
|
|
472
|
+
const found = await resolveBinary(cursorAgentManifest.binaries);
|
|
473
|
+
if (!found) return {
|
|
474
|
+
signedIn: false,
|
|
475
|
+
detail: "Not installed."
|
|
476
|
+
};
|
|
477
|
+
const result = await probe(found, ["status"]);
|
|
478
|
+
const detail = firstLine(result.text);
|
|
479
|
+
return {
|
|
480
|
+
signedIn: result.ok && !/not logged in|logged out/i.test(detail),
|
|
481
|
+
detail: detail || (result.ok ? "Signed in." : "Run cursor-agent login.")
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
invocation(input) {
|
|
485
|
+
return {
|
|
486
|
+
bin: cursorAgentManifest.binaries[0],
|
|
487
|
+
args: args(input)
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
async run(input) {
|
|
491
|
+
const bin = await binary();
|
|
492
|
+
const argv = args(input);
|
|
493
|
+
const outcome = await runProcess({
|
|
494
|
+
bin,
|
|
495
|
+
args: argv,
|
|
496
|
+
cwd: input.cwd,
|
|
497
|
+
timeoutMs: input.settings.timeoutMs,
|
|
498
|
+
signal: input.signal,
|
|
499
|
+
logPath: input.logFile,
|
|
500
|
+
logLine: (line) => describeEvent(line, input.cwd)
|
|
501
|
+
});
|
|
502
|
+
const command = `${bin} ${argv.join(" ")}`;
|
|
503
|
+
const answer = answerFrom(outcome.stdout);
|
|
504
|
+
if (outcome.aborted) return {
|
|
505
|
+
ok: false,
|
|
506
|
+
aborted: true,
|
|
507
|
+
output: answer.text,
|
|
508
|
+
durationMs: outcome.durationMs,
|
|
509
|
+
command
|
|
510
|
+
};
|
|
511
|
+
if (outcome.timedOut) return {
|
|
512
|
+
ok: false,
|
|
513
|
+
output: answer.text,
|
|
514
|
+
detail: `Cursor Agent did not finish within ${Math.round(input.settings.timeoutMs / 1e3)}s.`,
|
|
515
|
+
durationMs: outcome.durationMs,
|
|
516
|
+
command
|
|
517
|
+
};
|
|
518
|
+
return {
|
|
519
|
+
ok: outcome.code === 0 && !answer.failed,
|
|
520
|
+
output: answer.text,
|
|
521
|
+
detail: outcome.code === 0 && !answer.failed ? void 0 : answer.failed ? answer.text || "The agent reported an error." : outcome.stderr || `Exited with code ${outcome.code}.`,
|
|
522
|
+
durationMs: outcome.durationMs,
|
|
523
|
+
command
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
};
|
|
528
|
+
function agentRuntime(id) {
|
|
529
|
+
const runtime = RUNTIMES[id];
|
|
530
|
+
if (!runtime) throw new Error(`Unknown agent: ${id}`);
|
|
531
|
+
return runtime;
|
|
532
|
+
}
|
|
533
|
+
var DEFAULT_AGENT_KEY = "defaultAgentId";
|
|
534
|
+
async function storedSetting(key) {
|
|
535
|
+
const row = await db().select().from(appSettings).where(eq(appSettings.key, key)).get();
|
|
536
|
+
return typeof row?.value === "string" ? row.value : null;
|
|
537
|
+
}
|
|
538
|
+
async function settingsFor(agentId) {
|
|
539
|
+
const manifest = agentManifest(agentId);
|
|
540
|
+
if (!manifest) throw new Error(`Unknown agent: ${agentId}`);
|
|
541
|
+
const row = await db().select().from(agentSettings).where(eq(agentSettings.agentId, agentId)).get();
|
|
542
|
+
if (!row) return manifest.defaults;
|
|
543
|
+
return {
|
|
544
|
+
permissionMode: row.permissionMode,
|
|
545
|
+
model: row.model,
|
|
546
|
+
timeoutMs: row.timeoutMs
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Detection, authorization state and settings for every known agent. Detection
|
|
551
|
+
* runs on every call because installing or removing a CLI should show up
|
|
552
|
+
* without anyone pressing refresh.
|
|
553
|
+
*/
|
|
554
|
+
async function listAgents() {
|
|
555
|
+
const chosen = await storedSetting(DEFAULT_AGENT_KEY);
|
|
556
|
+
const hosted = isHosted();
|
|
557
|
+
return (await Promise.all(AGENT_MANIFESTS.map(async (manifest) => {
|
|
558
|
+
const runtime = agentRuntime(manifest.id);
|
|
559
|
+
const settings = await settingsFor(manifest.id);
|
|
560
|
+
const preview = runtime.invocation({
|
|
561
|
+
prompt: "<prompt>",
|
|
562
|
+
cwd: "<workspace>",
|
|
563
|
+
settings
|
|
564
|
+
});
|
|
565
|
+
if (hosted) return {
|
|
566
|
+
agentId: manifest.id,
|
|
567
|
+
installed: false,
|
|
568
|
+
binaryPath: "",
|
|
569
|
+
version: null,
|
|
570
|
+
auth: null,
|
|
571
|
+
settings,
|
|
572
|
+
isDefault: false,
|
|
573
|
+
defaultIsImplicit: false,
|
|
574
|
+
commandPreview: `${preview.bin} ${preview.args.join(" ")}`,
|
|
575
|
+
onRunners: []
|
|
576
|
+
};
|
|
577
|
+
const detection = await runtime.detect();
|
|
578
|
+
const auth = detection.installed ? await runtime.authStatus() : null;
|
|
579
|
+
return {
|
|
580
|
+
agentId: manifest.id,
|
|
581
|
+
installed: detection.installed,
|
|
582
|
+
binaryPath: detection.installed ? detection.binaryPath : null,
|
|
583
|
+
version: detection.installed ? detection.version : null,
|
|
584
|
+
auth,
|
|
585
|
+
settings,
|
|
586
|
+
isDefault: false,
|
|
587
|
+
defaultIsImplicit: false,
|
|
588
|
+
commandPreview: `${preview.bin} ${preview.args.join(" ")}`,
|
|
589
|
+
onRunners: []
|
|
590
|
+
};
|
|
591
|
+
}))).map((view) => view.agentId === chosen ? {
|
|
592
|
+
...view,
|
|
593
|
+
isDefault: true,
|
|
594
|
+
defaultIsImplicit: false
|
|
595
|
+
} : view);
|
|
596
|
+
}
|
|
597
|
+
async function saveAgentSettings(agentId, values) {
|
|
598
|
+
const manifest = agentManifest(agentId);
|
|
599
|
+
if (!manifest) throw new Error(`Unknown agent: ${agentId}`);
|
|
600
|
+
if (!manifest.permissionModes.includes(values.permissionMode)) throw new Error(`${manifest.name} cannot run in ${values.permissionMode} mode.`);
|
|
601
|
+
if (values.timeoutMs < 1e4 || values.timeoutMs > 36e5) throw new Error("The timeout must be between 10 seconds and one hour.");
|
|
602
|
+
const model = values.model?.trim() ? values.model.trim() : null;
|
|
603
|
+
await db().insert(agentSettings).values({
|
|
604
|
+
agentId,
|
|
605
|
+
permissionMode: values.permissionMode,
|
|
606
|
+
model,
|
|
607
|
+
timeoutMs: values.timeoutMs
|
|
608
|
+
}).onConflictDoUpdate({
|
|
609
|
+
target: agentSettings.agentId,
|
|
610
|
+
set: {
|
|
611
|
+
permissionMode: values.permissionMode,
|
|
612
|
+
model,
|
|
613
|
+
timeoutMs: values.timeoutMs,
|
|
614
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
615
|
+
}
|
|
616
|
+
}).run();
|
|
617
|
+
}
|
|
618
|
+
async function setDefaultAgent(agentId) {
|
|
619
|
+
if (!agentManifest(agentId)) throw new Error(`Unknown agent: ${agentId}`);
|
|
620
|
+
await db().insert(appSettings).values({
|
|
621
|
+
key: DEFAULT_AGENT_KEY,
|
|
622
|
+
value: agentId
|
|
623
|
+
}).onConflictDoUpdate({
|
|
624
|
+
target: appSettings.key,
|
|
625
|
+
set: {
|
|
626
|
+
value: agentId,
|
|
627
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
628
|
+
}
|
|
629
|
+
}).run();
|
|
630
|
+
}
|
|
631
|
+
var TEST_WORD = "loopable-ok";
|
|
632
|
+
/**
|
|
633
|
+
* A real run, because a version check cannot tell you whether the agent can
|
|
634
|
+
* reach a model. It stays deliberately tiny: read one file, say one word.
|
|
635
|
+
*/
|
|
636
|
+
async function testAgent(agentId) {
|
|
637
|
+
if (isHosted()) throw new Error("Test an agent on a runner.");
|
|
638
|
+
const runtime = agentRuntime(agentId);
|
|
639
|
+
const settings = await settingsFor(agentId);
|
|
640
|
+
const cwd = mkdtempSync(join(tmpdir(), "loopable-agent-test-"));
|
|
641
|
+
writeFileSync(join(cwd, "NOTE.md"), `${TEST_WORD}\n`);
|
|
642
|
+
const result = await runtime.run({
|
|
643
|
+
prompt: "Read NOTE.md in your working directory and reply with only the word it contains.",
|
|
644
|
+
cwd,
|
|
645
|
+
outputFile: join(cwd, "last-message.txt"),
|
|
646
|
+
settings: {
|
|
647
|
+
...settings,
|
|
648
|
+
timeoutMs: Math.min(settings.timeoutMs, 12e4)
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
return {
|
|
652
|
+
...result,
|
|
653
|
+
passed: result.ok && result.output.includes(TEST_WORD)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
//#endregion
|
|
657
|
+
export { setDefaultAgent as a, saveAgentSettings as i, agentManifest as n, settingsFor as o, listAgents as r, testAgent as s, AGENT_MANIFESTS as t };
|