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.
Files changed (133) hide show
  1. package/.output/nitro.json +17 -0
  2. package/.output/public/apple-touch-icon.png +0 -0
  3. package/.output/public/assets/_connectorId-BWjYn5tm.js +1 -0
  4. package/.output/public/assets/_loopId-Ds4RkyEc.js +1 -0
  5. package/.output/public/assets/_taskId-BOzICXDf.js +1 -0
  6. package/.output/public/assets/alert-Bn9-Zm0l.js +1 -0
  7. package/.output/public/assets/arrow-left-BF9juV9T.js +1 -0
  8. package/.output/public/assets/arrow-right-CzqvnOY1.js +1 -0
  9. package/.output/public/assets/button-HCY6DCY-.js +1 -0
  10. package/.output/public/assets/connection-status-CYZB8kLy.js +1 -0
  11. package/.output/public/assets/connector-icon-BZ_MUJV_.js +1 -0
  12. package/.output/public/assets/connectors-BvzttfSu.js +1 -0
  13. package/.output/public/assets/connectors-DdJNwRN_.js +1 -0
  14. package/.output/public/assets/external-link-D46X-bXa.js +1 -0
  15. package/.output/public/assets/gaps-D-I6xiDc.js +1 -0
  16. package/.output/public/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  17. package/.output/public/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  18. package/.output/public/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  19. package/.output/public/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  20. package/.output/public/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  21. package/.output/public/assets/inbox-BUUu9xxE.js +1 -0
  22. package/.output/public/assets/index-ztzex0FN.js +10 -0
  23. package/.output/public/assets/loader-CcBl2u8m.js +1 -0
  24. package/.output/public/assets/loop-form-dB3CLcZb.js +1 -0
  25. package/.output/public/assets/loops-DRunQrxL.js +1 -0
  26. package/.output/public/assets/loops-oSdqMd29.js +1 -0
  27. package/.output/public/assets/manifests-DNMqf7Vi.js +7 -0
  28. package/.output/public/assets/new-uyFxVjuB.js +1 -0
  29. package/.output/public/assets/preload-helper-ORbuV86A.js +3 -0
  30. package/.output/public/assets/runners-DSU3r5Ww.js +1 -0
  31. package/.output/public/assets/setting-fields-ixkpdBb-.js +3 -0
  32. package/.output/public/assets/styles-DZL5V1o7.css +2 -0
  33. package/.output/public/assets/switch-d5ummmHY.js +1 -0
  34. package/.output/public/assets/task-list-BpOStt9v.js +1 -0
  35. package/.output/public/assets/tasks-B8MsKgic.js +1 -0
  36. package/.output/public/assets/trash-2-D6u6NpzO.js +1 -0
  37. package/.output/public/favicon.svg +9 -0
  38. package/.output/server/_chunks/ssr-renderer.mjs +26 -0
  39. package/.output/server/_connectorId-8nb5yOzG.mjs +421 -0
  40. package/.output/server/_libs/@base-ui/react+[...].mjs +12841 -0
  41. package/.output/server/_libs/@tanstack/react-router+[...].mjs +14349 -0
  42. package/.output/server/_libs/class-variance-authority+clsx.mjs +69 -0
  43. package/.output/server/_libs/dijkstrajs.mjs +118 -0
  44. package/.output/server/_libs/drizzle-orm.mjs +4349 -0
  45. package/.output/server/_libs/h3+rou3+srvx.mjs +1361 -0
  46. package/.output/server/_libs/lucide-react.mjs +911 -0
  47. package/.output/server/_libs/next-themes.mjs +53 -0
  48. package/.output/server/_libs/pngjs.mjs +1680 -0
  49. package/.output/server/_libs/qrcode.mjs +2583 -0
  50. package/.output/server/_libs/sonner.mjs +955 -0
  51. package/.output/server/_libs/tailwind-merge.mjs +3380 -0
  52. package/.output/server/_libs/tanstack__history.mjs +341 -0
  53. package/.output/server/_libs/ufo.mjs +64 -0
  54. package/.output/server/_loopId-BsUL1SeL.mjs +272 -0
  55. package/.output/server/_runtime.mjs +35 -0
  56. package/.output/server/_ssr/access-zIl15o8y.mjs +42 -0
  57. package/.output/server/_ssr/agents-Btql102J.mjs +657 -0
  58. package/.output/server/_ssr/agents-DAOZPgO2.mjs +76 -0
  59. package/.output/server/_ssr/alert-NEiOCTOl.mjs +37 -0
  60. package/.output/server/_ssr/callback-CUINvl7M.mjs +62 -0
  61. package/.output/server/_ssr/client-CKkOfh-s.mjs +335 -0
  62. package/.output/server/_ssr/connection-status-Bi5peUAk.mjs +31 -0
  63. package/.output/server/_ssr/connections-qnw2GVlP.mjs +2542 -0
  64. package/.output/server/_ssr/connector-icon-DZXQLLFo.mjs +37 -0
  65. package/.output/server/_ssr/connectors-BHNwuIMj.mjs +106 -0
  66. package/.output/server/_ssr/connectors-C28g0i4c.mjs +83 -0
  67. package/.output/server/_ssr/createServerRpc-DPDrnFdH.mjs +12 -0
  68. package/.output/server/_ssr/empty-plugin-adapters-BJ4m9L8g.mjs +5 -0
  69. package/.output/server/_ssr/gaps-RAFSXLZf.mjs +53 -0
  70. package/.output/server/_ssr/inbox-k5zHergU.mjs +35 -0
  71. package/.output/server/_ssr/loop-form-DlObSewZ.mjs +412 -0
  72. package/.output/server/_ssr/loops-APFcP0y6.mjs +311 -0
  73. package/.output/server/_ssr/loops-xt2Bn7_q.mjs +222 -0
  74. package/.output/server/_ssr/new-DfKyd2Wa.mjs +169 -0
  75. package/.output/server/_ssr/router-3WmpE5b_.mjs +1553 -0
  76. package/.output/server/_ssr/runners-By-mwc3F.mjs +32 -0
  77. package/.output/server/_ssr/runners-DI_NxpHC.mjs +116 -0
  78. package/.output/server/_ssr/runners-DtXs4Q8a.mjs +106 -0
  79. package/.output/server/_ssr/secrets-Hqti-niS.mjs +199 -0
  80. package/.output/server/_ssr/setting-fields-8vKFj4df.mjs +92 -0
  81. package/.output/server/_ssr/ssr.mjs +6605 -0
  82. package/.output/server/_ssr/start-Bdc2GJUI.mjs +51 -0
  83. package/.output/server/_ssr/switch-ReNglgH6.mjs +18 -0
  84. package/.output/server/_ssr/task-list-CUITIqkb.mjs +186 -0
  85. package/.output/server/_ssr/tasks-SAjNovX6.mjs +110 -0
  86. package/.output/server/_ssr/tasks-vcoodp_D.mjs +533 -0
  87. package/.output/server/_tanstack-start-manifest_v-C0cP5F0M.mjs +125 -0
  88. package/.output/server/_taskId-CXS3lod0.mjs +249 -0
  89. package/.output/server/index.mjs +541 -0
  90. package/README.md +81 -0
  91. package/dist/cli.mjs +165 -0
  92. package/dist/dispatcher.mjs +9871 -0
  93. package/dist/runner.mjs +657 -0
  94. package/drizzle/0000_cold_demogoblin.sql +29 -0
  95. package/drizzle/0001_fancy_blonde_phantom.sql +13 -0
  96. package/drizzle/0002_spooky_smiling_tiger.sql +16 -0
  97. package/drizzle/0003_small_golden_guardian.sql +25 -0
  98. package/drizzle/0004_wonderful_thunderbolts.sql +39 -0
  99. package/drizzle/0005_named_workflows.sql +7 -0
  100. package/drizzle/0006_signals.sql +18 -0
  101. package/drizzle/0007_review_comments.sql +1 -0
  102. package/drizzle/0008_held_signals.sql +1 -0
  103. package/drizzle/0009_work_item_ref.sql +58 -0
  104. package/drizzle/0010_signal_payload.sql +2 -0
  105. package/drizzle/0011_rule_poll_cursor.sql +1 -0
  106. package/drizzle/0012_loops.sql +63 -0
  107. package/drizzle/0013_loop_owns_action_and_prompt.sql +154 -0
  108. package/drizzle/0014_loop_poll_every.sql +4 -0
  109. package/drizzle/0015_machines.sql +10 -0
  110. package/drizzle/0016_task_machine.sql +1 -0
  111. package/drizzle/0017_loop_machine.sql +1 -0
  112. package/drizzle/0018_runners.sql +1 -0
  113. package/drizzle/0019_task_runner.sql +1 -0
  114. package/drizzle/0020_loop_runner.sql +1 -0
  115. package/drizzle/0021_drop_runner_kind.sql +1 -0
  116. package/drizzle/0022_drop_loop_runner.sql +1 -0
  117. package/drizzle/0023_shared_host.sql +6 -0
  118. package/drizzle/meta/0000_snapshot.json +206 -0
  119. package/drizzle/meta/0001_snapshot.json +284 -0
  120. package/drizzle/meta/0002_snapshot.json +390 -0
  121. package/drizzle/meta/0003_snapshot.json +566 -0
  122. package/drizzle/meta/0004_snapshot.json +633 -0
  123. package/drizzle/meta/0005_snapshot.json +626 -0
  124. package/drizzle/meta/0006_snapshot.json +752 -0
  125. package/drizzle/meta/0007_snapshot.json +759 -0
  126. package/drizzle/meta/0008_snapshot.json +766 -0
  127. package/drizzle/meta/0009_snapshot.json +752 -0
  128. package/drizzle/meta/0010_snapshot.json +766 -0
  129. package/drizzle/meta/0011_snapshot.json +773 -0
  130. package/drizzle/meta/0012_snapshot.json +773 -0
  131. package/drizzle/meta/0013_snapshot.json +817 -0
  132. package/drizzle/meta/_journal.json +174 -0
  133. 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 };