loopable-cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/_connectorId-CDCuSgpN.js +1 -0
  3. package/.output/public/assets/_loopId-DOnIsVTW.js +1 -0
  4. package/.output/public/assets/_taskId-BO5MZnlZ.js +1 -0
  5. package/.output/public/assets/agents-CBozi7oi.js +1 -0
  6. package/.output/public/assets/alert-Bn_wjgTX.js +1 -0
  7. package/.output/public/assets/button-K3ypIhAZ.js +1 -0
  8. package/.output/public/assets/card-DiVnaTIE.js +1 -0
  9. package/.output/public/assets/chevron-right-BrctLtFK.js +1 -0
  10. package/.output/public/assets/connection-status-DKWelvLO.js +1 -0
  11. package/.output/public/assets/connector-icon-CEbdE71G.js +1 -0
  12. package/.output/public/assets/connectors-JyX3IzJj.js +1 -0
  13. package/.output/public/assets/gaps-PjmcbaWo.js +1 -0
  14. package/.output/public/assets/inbox-CujRR0Cz.js +1 -0
  15. package/.output/public/assets/index-C9SQ_Baq.js +16 -0
  16. package/.output/public/assets/loop-form-Dw3cuTzw.js +1 -0
  17. package/.output/public/assets/loops-DcEfZa2_.js +1 -0
  18. package/.output/public/assets/manifests-yJpevyye.js +1 -0
  19. package/.output/public/assets/new-CwsnNuRW.js +1 -0
  20. package/.output/public/assets/page-bvnfRMum.js +1 -0
  21. package/.output/public/assets/play-yHLs4DjK.js +1 -0
  22. package/.output/public/assets/plus-Ber25lrU.js +1 -0
  23. package/.output/public/assets/preload-helper-BoU_7EQC.js +3 -0
  24. package/.output/public/assets/refresh-cw-BzSY7M8I.js +1 -0
  25. package/.output/public/assets/rotate-ccw-DLv9EfCX.js +1 -0
  26. package/.output/public/assets/runners-DzX-RU3i.js +2 -0
  27. package/.output/public/assets/select-C9grIFwG.js +1 -0
  28. package/.output/public/assets/setting-fields-DUc3npgh.js +3 -0
  29. package/.output/public/assets/styles-B9B98zLk.css +2 -0
  30. package/.output/public/assets/switch-DwABVI6d.js +1 -0
  31. package/.output/public/assets/task-list-Ddk8-qt7.js +1 -0
  32. package/.output/public/assets/textarea-Ds-gIYUQ.js +1 -0
  33. package/.output/public/assets/time-goBWd-5J.js +1 -0
  34. package/.output/public/assets/use-live-tasks-DZNNuECy.js +1 -0
  35. package/.output/public/assets/useCompositeListItem-P4Nmmn7j.js +1 -0
  36. package/.output/public/assets/usePositioner-B7i9Ejzh.js +1 -0
  37. package/.output/server/_chunks/ssr-renderer.mjs +1 -1
  38. package/.output/server/_connectorId-COuQN02a.mjs +512 -0
  39. package/.output/server/_libs/@base-ui/react+[...].mjs +1410 -804
  40. package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
  41. package/.output/server/_libs/lucide-react.mjs +93 -84
  42. package/.output/server/_libs/next-themes.mjs +1 -1
  43. package/.output/server/_libs/sonner.mjs +1 -1
  44. package/.output/server/_loopId-CZh6_Yhr.mjs +310 -0
  45. package/.output/server/_ssr/{callback-D20cVD2w.mjs → access-DTttr563.mjs} +37 -20
  46. package/.output/server/_ssr/{agents-CrUmIQpi.mjs → agents-B_qo1gpL.mjs} +196 -7
  47. package/.output/server/_ssr/{agents-Dms_6TBF.mjs → agents-Bz10gz_m.mjs} +8 -8
  48. package/.output/server/_ssr/agents-CjbOUp3Z.mjs +206 -0
  49. package/.output/server/_ssr/{alert-DQvzE7bu.mjs → alert-C2WBWfzo.mjs} +2 -2
  50. package/.output/server/_ssr/card-r1cV-PCN.mjs +36 -0
  51. package/.output/server/_ssr/{client-BtVRVhQ2.mjs → client-Dk3SD8Il.mjs} +5 -1
  52. package/.output/server/_ssr/connection-status-BogAL-Fj.mjs +32 -0
  53. package/.output/server/_ssr/{connections-ypex3qUd.mjs → connections-Cqvb6Gw2.mjs} +1109 -263
  54. package/.output/server/_ssr/connector-icon-DrhVbhs6.mjs +190 -0
  55. package/.output/server/_ssr/connectors-BfS5tQKr.mjs +81 -0
  56. package/.output/server/_ssr/{connectors-BZly0yGF.mjs → connectors-DJI-rzPU.mjs} +21 -10
  57. package/.output/server/_ssr/{createServerRpc-DPDrnFdH.mjs → createServerRpc-DDrp2nLT.mjs} +2 -2
  58. package/.output/server/_ssr/{gaps-D2Zh8sEo.mjs → gaps-CcvHiLcj.mjs} +18 -10
  59. package/.output/server/_ssr/inbox-D1ef761v.mjs +178 -0
  60. package/.output/server/_ssr/loop-form-DWwOR05R.mjs +346 -0
  61. package/.output/server/_ssr/{loops-Og_G3KyZ.mjs → loops-6WhUe_lc.mjs} +228 -25
  62. package/.output/server/_ssr/loops-DHPjzG-t.mjs +217 -0
  63. package/.output/server/_ssr/new-ChKBdcXJ.mjs +245 -0
  64. package/.output/server/_ssr/page-DZVxKvdn.mjs +120 -0
  65. package/.output/server/_ssr/{router-BbrBz36P.mjs → router-DdwREiOf.mjs} +167 -489
  66. package/.output/server/_ssr/runners-CK9Cx9fS.mjs +217 -0
  67. package/.output/server/_ssr/runners-CrznFPHt.mjs +140 -0
  68. package/.output/server/_ssr/runners-W-pgo5yo.mjs +55 -0
  69. package/.output/server/_ssr/{secrets-CRnpwGBh.mjs → secrets-CV5scFlt.mjs} +2 -2
  70. package/.output/server/_ssr/select-BtndHhHb.mjs +76 -0
  71. package/.output/server/_ssr/{setting-fields-mpXfGftI.mjs → setting-fields-LNgywAW7.mjs} +25 -20
  72. package/.output/server/_ssr/ssr.mjs +49 -40
  73. package/.output/server/_ssr/{start-BgTj-h_C.mjs → start-C_j-e4vF.mjs} +10 -6
  74. package/.output/server/_ssr/{switch-1pnd_bPJ.mjs → switch-odxNO_S4.mjs} +3 -3
  75. package/.output/server/_ssr/task-list-Dnmq_WeE.mjs +150 -0
  76. package/.output/server/_ssr/{tasks-LIi-W4cy.mjs → tasks-D-kXLgVP.mjs} +13 -13
  77. package/.output/server/_ssr/{tasks-B_QmDFVs.mjs → tasks-D7aUvA33.mjs} +55 -17
  78. package/.output/server/_ssr/{textarea-CHJTKtsJ.mjs → textarea-D2c_vTIC.mjs} +2 -2
  79. package/.output/server/_ssr/time-Cy8TZP0Z.mjs +13 -0
  80. package/.output/server/_ssr/use-live-tasks-BzhTkEOL.mjs +40 -0
  81. package/.output/server/_tanstack-start-manifest_v-2HUdn-2E.mjs +166 -0
  82. package/.output/server/_taskId-D3KkNJW1.mjs +263 -0
  83. package/.output/server/index.mjs +204 -162
  84. package/README.md +54 -19
  85. package/dist/cli.mjs +185 -67
  86. package/dist/dispatcher.mjs +1443 -277
  87. package/dist/runner.mjs +333 -46
  88. package/drizzle/0025_prompt_tasks_done.sql +2 -0
  89. package/drizzle/0026_guidance_into_prompt.sql +3 -0
  90. package/drizzle/0027_relative_folders.sql +5 -0
  91. package/drizzle/0028_runner_version.sql +5 -0
  92. package/drizzle/meta/_journal.json +29 -1
  93. package/package.json +23 -17
  94. package/.output/public/assets/_connectorId-m67WbiJ8.js +0 -1
  95. package/.output/public/assets/_loopId-Bf4K5sdd.js +0 -1
  96. package/.output/public/assets/_taskId-eCKkcVOx.js +0 -1
  97. package/.output/public/assets/alert-BT2EVMJP.js +0 -1
  98. package/.output/public/assets/arrow-left-CWUZt6k2.js +0 -1
  99. package/.output/public/assets/arrow-right-dy_oOg_c.js +0 -1
  100. package/.output/public/assets/connection-status-DCqqmY_6.js +0 -1
  101. package/.output/public/assets/connector-icon-qGpNsPgH.js +0 -1
  102. package/.output/public/assets/connectors-DxXlc9zF.js +0 -1
  103. package/.output/public/assets/external-link-DC11orFA.js +0 -1
  104. package/.output/public/assets/gaps-CoTA-PzA.js +0 -1
  105. package/.output/public/assets/inbox-CLEXUXCI.js +0 -1
  106. package/.output/public/assets/index-Cndsw84p.js +0 -16
  107. package/.output/public/assets/loader-B4Epchm_.js +0 -1
  108. package/.output/public/assets/loop-form-BvizlZdR.js +0 -1
  109. package/.output/public/assets/loops-Dbe_mf5T.js +0 -1
  110. package/.output/public/assets/new-DPEaXKAY.js +0 -1
  111. package/.output/public/assets/play-DD4xQgvi.js +0 -1
  112. package/.output/public/assets/preload-helper-CfdAYKD7.js +0 -3
  113. package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +0 -1
  114. package/.output/public/assets/runners-BVBggS5B.js +0 -1
  115. package/.output/public/assets/select-Cppu-hti.js +0 -1
  116. package/.output/public/assets/setting-fields-BT1naVJX.js +0 -3
  117. package/.output/public/assets/styles-D3sonCfX.css +0 -2
  118. package/.output/public/assets/switch-hRIbNGOj.js +0 -1
  119. package/.output/public/assets/task-list-CrptnI1N.js +0 -1
  120. package/.output/public/assets/textarea-Fyuzi6Vp.js +0 -1
  121. package/.output/public/assets/trash-2-BXZzBsoV.js +0 -1
  122. package/.output/public/assets/useValueChanged-VVyq4hb9.js +0 -1
  123. package/.output/server/_connectorId-DcSnoRjk.mjs +0 -421
  124. package/.output/server/_loopId-G_TAkYtS.mjs +0 -272
  125. package/.output/server/_ssr/access-B7TcOZ7F.mjs +0 -42
  126. package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +0 -31
  127. package/.output/server/_ssr/connector-icon-CFrrHjzR.mjs +0 -37
  128. package/.output/server/_ssr/connectors-4yzKb0KG.mjs +0 -106
  129. package/.output/server/_ssr/inbox-C2VBGLa5.mjs +0 -114
  130. package/.output/server/_ssr/loop-form-D-U-kU4k.mjs +0 -413
  131. package/.output/server/_ssr/loops-D0XJ8eBb.mjs +0 -222
  132. package/.output/server/_ssr/new-CajvgIog.mjs +0 -169
  133. package/.output/server/_ssr/runners-B_WuF8UJ.mjs +0 -32
  134. package/.output/server/_ssr/runners-D-0_lfiq.mjs +0 -106
  135. package/.output/server/_ssr/runners-zqF_zbEc.mjs +0 -116
  136. package/.output/server/_ssr/task-list-BnQJ94Dk.mjs +0 -187
  137. package/.output/server/_tanstack-start-manifest_v-BCHAazE8.mjs +0 -129
  138. package/.output/server/_taskId-CIv0ajIx.mjs +0 -256
@@ -0,0 +1,217 @@
1
+ import { f as eq } from "../_libs/drizzle-orm.mjs";
2
+ import { f as runners, g as tasks, s as db } from "./client-Dk3SD8Il.mjs";
3
+ import { a as runnerTokenKey, o as secretStore, r as joinTokenKey } from "./secrets-CV5scFlt.mjs";
4
+ import { randomBytes, randomUUID } from "node:crypto";
5
+ import { hostname } from "node:os";
6
+ //#region node_modules/.nitro/vite/services/ssr/assets/runners-CK9Cx9fS.js
7
+ /** This build's version, as published to npm. */
8
+ var VERSION = {
9
+ name: "loopable-cli",
10
+ version: "0.2.1",
11
+ description: "CLI to run Loopable: App, Dispatcher, and Runner.",
12
+ license: "MIT",
13
+ type: "module",
14
+ bin: { "loopable": "./dist/cli.mjs" },
15
+ files: [
16
+ ".output",
17
+ "dist",
18
+ "drizzle"
19
+ ],
20
+ engines: { "node": ">=22" },
21
+ packageManager: "pnpm@11.22.0",
22
+ repository: {
23
+ "type": "git",
24
+ "url": "git+https://github.com/imsobear/loopable.git"
25
+ },
26
+ bugs: { "url": "https://github.com/imsobear/loopable/issues" },
27
+ homepage: "https://github.com/imsobear/loopable#readme",
28
+ publishConfig: { "access": "public" },
29
+ imports: { "#/*": "./src/*" },
30
+ scripts: {
31
+ "dev": "node --experimental-strip-types scripts/dev.ts",
32
+ "start": "node dist/cli.mjs start",
33
+ "dispatcher": "LOOPABLE_DEV=1 node --experimental-strip-types src/cli.ts dispatcher",
34
+ "runner": "LOOPABLE_DEV=1 node --experimental-strip-types src/cli.ts runner",
35
+ "generate-routes": "tsr generate",
36
+ "build": "vite build && node scripts/build-cli.mjs",
37
+ "preview": "vite preview",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "db:generate": "drizzle-kit generate",
41
+ "db:studio": "LOOPABLE_DEV=1 drizzle-kit studio",
42
+ "typecheck": "tsc --noEmit",
43
+ "release": "node scripts/release.mjs",
44
+ "deploy": "LOOPABLE_CF=1 vite build && wrangler deploy",
45
+ "site:dev": "pnpm --filter loopable-site dev",
46
+ "site:build": "pnpm --filter loopable-site build",
47
+ "site:deploy": "pnpm --filter loopable-site deploy",
48
+ "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
49
+ },
50
+ dependencies: { "@libsql/client": "^0.18.0" },
51
+ devDependencies: {
52
+ "@base-ui/react": "^1.7.0",
53
+ "@cloudflare/containers": "^0.3.7",
54
+ "@cloudflare/vite-plugin": "^1.54.10",
55
+ "@fontsource-variable/geist": "^5.3.0",
56
+ "@tailwindcss/vite": "4.1.18",
57
+ "@tanstack/devtools-vite": "0.8.5",
58
+ "@tanstack/react-devtools": "0.10.12",
59
+ "@tanstack/react-router": "1.170.32",
60
+ "@tanstack/react-router-devtools": "1.167.1",
61
+ "@tanstack/react-start": "1.168.49",
62
+ "@tanstack/router-cli": "1.167.33",
63
+ "@types/node": "^22.10.2",
64
+ "@types/qrcode": "^1.5.6",
65
+ "@types/react": "^19.2.0",
66
+ "@types/react-dom": "^19.2.0",
67
+ "@vitejs/plugin-react": "^6.0.1",
68
+ "class-variance-authority": "^0.7.1",
69
+ "clsx": "^2.1.1",
70
+ "drizzle-kit": "^0.31.10",
71
+ "drizzle-orm": "^0.45.2",
72
+ "esbuild": "^0.28.2",
73
+ "lucide-react": "^1.39.0",
74
+ "next-themes": "^0.4.6",
75
+ "nitro": "3.0.260610-beta",
76
+ "qrcode": "^1.5.4",
77
+ "react": "19.2.0",
78
+ "react-dom": "19.2.0",
79
+ "shadcn": "^4.19.1",
80
+ "sonner": "^2.0.8",
81
+ "tailwind-merge": "^3.6.0",
82
+ "tailwindcss": "4.1.18",
83
+ "tw-animate-css": "^1.4.0",
84
+ "typescript": "^6.0.2",
85
+ "vite": "^8.0.0",
86
+ "vitest": "^4.1.11",
87
+ "wrangler": "^4.132.0"
88
+ }
89
+ }.version;
90
+ /** Whether `a` is an older x.y.z than `b`. Anything unreadable counts as older. */
91
+ function olderThan(a, b) {
92
+ if (!a) return true;
93
+ const pa = a.split(".").map((part) => Number.parseInt(part, 10));
94
+ const pb = b.split(".").map((part) => Number.parseInt(part, 10));
95
+ for (let i = 0; i < 3; i++) {
96
+ const x = pa[i] ?? 0;
97
+ const y = pb[i] ?? 0;
98
+ if (Number.isNaN(x)) return true;
99
+ if (x !== y) return x < y;
100
+ }
101
+ return false;
102
+ }
103
+ var ONLINE_MS = 45e3;
104
+ function protocolOf(row) {
105
+ return row.protocol ?? 1;
106
+ }
107
+ function buildFields(build) {
108
+ return {
109
+ version: typeof build.version === "string" && build.version ? build.version : null,
110
+ protocol: typeof build.protocol === "number" ? build.protocol : null
111
+ };
112
+ }
113
+ function isRunnerOnline(row, now = Date.now()) {
114
+ if (row.status !== "online") return false;
115
+ if (!row.lastSeenAt) return false;
116
+ return now - row.lastSeenAt.getTime() < ONLINE_MS;
117
+ }
118
+ function toView(row, now = Date.now()) {
119
+ const online = isRunnerOnline(row, now);
120
+ return {
121
+ id: row.id,
122
+ name: row.name,
123
+ hostname: row.hostname,
124
+ status: online ? "online" : "offline",
125
+ inventory: row.inventory,
126
+ lastSeenAt: row.lastSeenAt?.toISOString() ?? null,
127
+ version: row.version,
128
+ protocol: protocolOf(row),
129
+ outdated: protocolOf(row) < 2 || olderThan(row.version, VERSION)
130
+ };
131
+ }
132
+ async function listRunners() {
133
+ await db().delete(runners).where(eq(runners.id, "host")).run();
134
+ const now = Date.now();
135
+ return (await db().select().from(runners).all()).map((row) => toView(row, now));
136
+ }
137
+ async function getRunner(id) {
138
+ return await db().select().from(runners).where(eq(runners.id, id)).get();
139
+ }
140
+ async function onlineRunnerCount() {
141
+ return (await listRunners()).filter((row) => row.status === "online").length;
142
+ }
143
+ /** Agent ids signed in on an online runner. */
144
+ async function availableAgentIds(requiresHost = false, minProtocol = 0) {
145
+ const local = hostname();
146
+ return [...new Set((await listRunners()).filter((row) => row.status === "online" && (!requiresHost || row.hostname === local) && row.protocol >= minProtocol).flatMap((row) => row.inventory.filter((entry) => entry.installed && entry.signedIn).map((entry) => entry.agentId)))];
147
+ }
148
+ async function getJoinToken() {
149
+ const existing = await secretStore().get(joinTokenKey());
150
+ if (existing) return existing;
151
+ const token = randomBytes(24).toString("base64url");
152
+ await secretStore().set(joinTokenKey(), token);
153
+ return token;
154
+ }
155
+ async function rotateJoinToken() {
156
+ const token = randomBytes(24).toString("base64url");
157
+ await secretStore().set(joinTokenKey(), token);
158
+ return token;
159
+ }
160
+ async function heartbeatRunner(runnerId, inventory, build = {}) {
161
+ if (!await getRunner(runnerId)) throw new Error("Unknown runner.");
162
+ await db().update(runners).set({
163
+ status: "online",
164
+ inventory,
165
+ lastSeenAt: /* @__PURE__ */ new Date(),
166
+ ...buildFields(build)
167
+ }).where(eq(runners.id, runnerId)).run();
168
+ return toView(await getRunner(runnerId));
169
+ }
170
+ async function joinRunner(input) {
171
+ const expected = await getJoinToken();
172
+ if (!input.joinToken || input.joinToken !== expected) throw new Error("That join token is not valid.");
173
+ const existing = (await db().select().from(runners).all()).find((row) => row.hostname === input.hostname);
174
+ const id = existing?.id ?? randomUUID();
175
+ const name = existing?.name ?? input.hostname;
176
+ const now = /* @__PURE__ */ new Date();
177
+ if (existing) await db().update(runners).set({
178
+ status: "online",
179
+ inventory: input.inventory,
180
+ lastSeenAt: now,
181
+ hostname: input.hostname,
182
+ ...buildFields(input)
183
+ }).where(eq(runners.id, id)).run();
184
+ else await db().insert(runners).values({
185
+ id,
186
+ name,
187
+ hostname: input.hostname,
188
+ status: "online",
189
+ inventory: input.inventory,
190
+ lastSeenAt: now,
191
+ ...buildFields(input)
192
+ }).run();
193
+ const token = randomBytes(24).toString("base64url");
194
+ await secretStore().set(runnerTokenKey(id), token);
195
+ return {
196
+ runnerId: id,
197
+ runnerToken: token,
198
+ name
199
+ };
200
+ }
201
+ async function runnerIdForToken(token) {
202
+ if (!token) return null;
203
+ for (const row of await db().select().from(runners).all()) {
204
+ const stored = await secretStore().get(runnerTokenKey(row.id)) ?? await secretStore().get(`runners.machine.${row.id}`);
205
+ if (stored && stored === token) return row.id;
206
+ }
207
+ return null;
208
+ }
209
+ async function forgetRunner(id) {
210
+ if (!await getRunner(id)) throw new Error("Unknown runner.");
211
+ if ((await db().select().from(tasks).all()).some((task) => task.runnerId === id && task.state === "awaiting_agent")) throw new Error("This runner still has a run in flight.");
212
+ await db().delete(runners).where(eq(runners.id, id)).run();
213
+ await secretStore().delete(runnerTokenKey(id));
214
+ await secretStore().delete(`runners.machine.${id}`);
215
+ }
216
+ //#endregion
217
+ export { getRunner as a, joinRunner as c, protocolOf as d, rotateJoinToken as f, getJoinToken as i, listRunners as l, availableAgentIds as n, heartbeatRunner as o, runnerIdForToken as p, forgetRunner as r, isRunnerOnline as s, VERSION as t, onlineRunnerCount as u };
@@ -0,0 +1,140 @@
1
+ import { K as require_jsx_runtime } from "../_libs/@base-ui/react+[...].mjs";
2
+ import { m as useRouter } from "../_libs/@tanstack/react-router+[...].mjs";
3
+ import { t as VERSION } from "./runners-CK9Cx9fS.mjs";
4
+ import { n as agentManifest } from "./agents-B_qo1gpL.mjs";
5
+ import { o as Server, s as RotateCcw, w as Copy } from "../_libs/lucide-react.mjs";
6
+ import { n as toast } from "../_libs/sonner.mjs";
7
+ import { P as cn, i as rotateRunnerJoinToken, n as Route$7, r as removeRunner, v as Button$1 } from "./router-DdwREiOf.mjs";
8
+ import { a as List, o as PageHeader, t as Empty } from "./page-DZVxKvdn.mjs";
9
+ import { n as useLiveRefresh } from "./use-live-tasks-BzhTkEOL.mjs";
10
+ //#region node_modules/.nitro/vite/services/ssr/assets/runners-CrznFPHt.js
11
+ var import_jsx_runtime = require_jsx_runtime();
12
+ /** Which agents this runner has, in a few words. */
13
+ function agentsOn(runner) {
14
+ if (runner.inventory.length === 0) return "No agents found";
15
+ return runner.inventory.map((entry) => {
16
+ const name = agentManifest(entry.agentId)?.name ?? entry.agentId;
17
+ return entry.signedIn ? name : entry.installed ? `${name} (not signed in)` : `${name} (missing)`;
18
+ }).join(", ");
19
+ }
20
+ function RunnersPage() {
21
+ const { runners, joinToken, origin } = Route$7.useLoaderData();
22
+ const router = useRouter();
23
+ useLiveRefresh();
24
+ const commands = ["npm install -g loopable-cli", `loopable runner --url ${origin} --token ${joinToken}`].join("\n");
25
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
26
+ className: "flex flex-col gap-6",
27
+ children: [
28
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageHeader, {
29
+ title: "Runners",
30
+ description: `Machines that run agents for your loops. This App is v${VERSION}.`
31
+ }),
32
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
33
+ className: "flex flex-col gap-2",
34
+ children: [
35
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
36
+ className: "flex items-center justify-between gap-3",
37
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
38
+ className: "text-sm font-medium",
39
+ children: "Join a runner"
40
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
41
+ variant: "ghost",
42
+ size: "sm",
43
+ onClick: async () => {
44
+ if (!window.confirm("Rotate the join token? Runners already joined keep working.")) return;
45
+ try {
46
+ await rotateRunnerJoinToken();
47
+ await router.invalidate();
48
+ toast.success("Join token rotated");
49
+ } catch (error) {
50
+ toast.error(error instanceof Error ? error.message : String(error));
51
+ }
52
+ },
53
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RotateCcw, {}), "Rotate token"]
54
+ })]
55
+ }),
56
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
57
+ className: "flex items-start gap-2 rounded-xl bg-muted p-1 pl-3",
58
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
59
+ className: "min-w-0 flex-1 overflow-x-auto py-1.5 text-xs leading-relaxed",
60
+ children: commands
61
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
62
+ variant: "ghost",
63
+ size: "icon-sm",
64
+ "aria-label": "Copy",
65
+ onClick: async () => {
66
+ await navigator.clipboard.writeText(commands);
67
+ toast.success("Copied");
68
+ },
69
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, {})
70
+ })]
71
+ }),
72
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
73
+ className: "text-xs text-muted-foreground",
74
+ children: "Needs Node 22+ and an agent CLI signed in on that machine. Skip the first line if the CLI is already installed."
75
+ })
76
+ ]
77
+ }),
78
+ runners.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Empty, { children: "No runner has joined yet. Tasks wait until one is online." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(List, { children: runners.map((runner) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
79
+ className: "flex items-center gap-3 px-4 py-3",
80
+ children: [
81
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
82
+ className: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground",
83
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Server, { className: "size-4" })
84
+ }),
85
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
86
+ className: "min-w-0 flex-1",
87
+ children: [
88
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
89
+ className: "flex items-center gap-2",
90
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
91
+ className: "truncate text-sm font-medium",
92
+ children: runner.name
93
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
94
+ className: "flex items-center gap-1.5 text-xs text-muted-foreground",
95
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn("size-1.5 rounded-full", runner.status === "online" ? "bg-emerald-500" : "bg-muted-foreground/50") }), runner.status === "online" ? "Online" : "Offline"]
96
+ })]
97
+ }),
98
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
99
+ className: "mt-0.5 truncate text-xs text-muted-foreground",
100
+ children: [
101
+ runner.hostname,
102
+ " · ",
103
+ runner.version ? `v${runner.version}` : "version unknown",
104
+ " ·",
105
+ " ",
106
+ agentsOn(runner)
107
+ ]
108
+ }),
109
+ runner.outdated ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
110
+ className: "mt-1 text-xs text-amber-700 dark:text-amber-500",
111
+ children: [
112
+ "Older than this App (v",
113
+ VERSION,
114
+ "), so it may not take every job. On that machine, run ",
115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "npm install -g loopable-cli" }),
116
+ " and restart the runner."
117
+ ]
118
+ }) : null
119
+ ]
120
+ }),
121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
122
+ variant: "ghost",
123
+ size: "sm",
124
+ onClick: async () => {
125
+ try {
126
+ await removeRunner({ data: { id: runner.id } });
127
+ await router.invalidate();
128
+ } catch (error) {
129
+ toast.error(error instanceof Error ? error.message : String(error));
130
+ }
131
+ },
132
+ children: "Forget"
133
+ })
134
+ ]
135
+ }, runner.id)) })
136
+ ]
137
+ });
138
+ }
139
+ //#endregion
140
+ export { RunnersPage as component };
@@ -0,0 +1,55 @@
1
+ import { r as createServerFn } from "./ssr.mjs";
2
+ import { f as rotateJoinToken, i as getJoinToken, l as listRunners, r as forgetRunner } from "./runners-CK9Cx9fS.mjs";
3
+ import { t as createServerRpc } from "./createServerRpc-DDrp2nLT.mjs";
4
+ import { networkInterfaces } from "node:os";
5
+ //#region node_modules/.nitro/vite/services/ssr/assets/runners-W-pgo5yo.js
6
+ function configuredOrigin() {
7
+ return process.env.LOOPABLE_BASE_URL?.replace(/\/$/, "") || null;
8
+ }
9
+ /** First non-internal IPv4, preferring typical LAN ranges. */
10
+ function lanIPv4(nics = networkInterfaces()) {
11
+ const found = [];
12
+ for (const addrs of Object.values(nics)) for (const addr of addrs ?? []) {
13
+ if (addr.internal) continue;
14
+ if (!["IPv4", "4"].includes(String(addr.family))) continue;
15
+ found.push(addr.address);
16
+ }
17
+ return found.find((ip) => ip.startsWith("192.168.")) ?? found.find((ip) => ip.startsWith("10.")) ?? found.find((ip) => {
18
+ const b = Number(ip.split(".")[1]);
19
+ return ip.startsWith("172.") && b >= 16 && b <= 31;
20
+ }) ?? found[0] ?? null;
21
+ }
22
+ /** Where runners should address this Loopable. A LAN IP when one exists. */
23
+ function loopableOrigin(nics = networkInterfaces()) {
24
+ const configured = configuredOrigin();
25
+ if (configured) return configured;
26
+ const port = process.env.PORT ?? process.env.NITRO_PORT ?? "4321";
27
+ return `http://${lanIPv4(nics) ?? "127.0.0.1"}:${port}`;
28
+ }
29
+ var getRunnersPage_createServerFn_handler = createServerRpc({
30
+ id: "45dbbb612763589268984d236edcd17f5868ecf716df9625bfa46e7fec691f4e",
31
+ name: "getRunnersPage",
32
+ filename: "src/server/functions/runners.ts"
33
+ }, (opts) => getRunnersPage.__executeServer(opts));
34
+ var getRunnersPage = createServerFn({ method: "GET" }).handler(getRunnersPage_createServerFn_handler, async () => ({
35
+ runners: await listRunners(),
36
+ joinToken: await getJoinToken(),
37
+ origin: loopableOrigin()
38
+ }));
39
+ var rotateRunnerJoinToken_createServerFn_handler = createServerRpc({
40
+ id: "b4f7a66e1bb69cc14ab84d8f5dc7f85119c9a9b1d065a6b4e51dd0ba84c3f60c",
41
+ name: "rotateRunnerJoinToken",
42
+ filename: "src/server/functions/runners.ts"
43
+ }, (opts) => rotateRunnerJoinToken.__executeServer(opts));
44
+ var rotateRunnerJoinToken = createServerFn({ method: "POST" }).handler(rotateRunnerJoinToken_createServerFn_handler, async () => await rotateJoinToken());
45
+ var removeRunner_createServerFn_handler = createServerRpc({
46
+ id: "611491b32d9583ccaae637ddf9fb2f842ff75994f50513ecd8273e305e889eb8",
47
+ name: "removeRunner",
48
+ filename: "src/server/functions/runners.ts"
49
+ }, (opts) => removeRunner.__executeServer(opts));
50
+ var removeRunner = createServerFn({ method: "POST" }).validator((data) => data).handler(removeRunner_createServerFn_handler, async ({ data }) => {
51
+ await forgetRunner(data.id);
52
+ return await listRunners();
53
+ });
54
+ //#endregion
55
+ export { getRunnersPage_createServerFn_handler, removeRunner_createServerFn_handler, rotateRunnerJoinToken_createServerFn_handler };
@@ -1,9 +1,9 @@
1
1
  import { f as eq } from "../_libs/drizzle-orm.mjs";
2
- import { m as secrets, o as dataDir, p as secretNamespace, s as db } from "./client-BtVRVhQ2.mjs";
2
+ import { m as secrets, o as dataDir, p as secretNamespace, s as db } from "./client-Dk3SD8Il.mjs";
3
3
  import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
- //#region node_modules/.nitro/vite/services/ssr/assets/secrets-CRnpwGBh.js
6
+ //#region node_modules/.nitro/vite/services/ssr/assets/secrets-CV5scFlt.js
7
7
  /**
8
8
  * Hosted Loopable shares a network database between the App Worker and the
9
9
  * Dispatcher. A local laptop still uses a file, and needs none of this.
@@ -0,0 +1,76 @@
1
+ import "../_runtime.mjs";
2
+ import { C as SelectValue$1, K as require_jsx_runtime, S as SelectIcon, T as SelectRoot, _ as SelectItem$1, b as SelectPositioner, g as SelectItemIndicator, h as SelectItemText, m as SelectScrollDownArrow, p as SelectScrollUpArrow, q as require_react, v as SelectList, w as SelectTrigger$1, x as SelectPortal, y as SelectPopup } from "../_libs/@base-ui/react+[...].mjs";
3
+ import { L as ChevronDown, P as ChevronUp, R as Check } from "../_libs/lucide-react.mjs";
4
+ import { P as cn } from "./router-DdwREiOf.mjs";
5
+ require_react();
6
+ var import_jsx_runtime = require_jsx_runtime();
7
+ var Select$1 = SelectRoot;
8
+ function SelectValue({ className, ...props }) {
9
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue$1, {
10
+ "data-slot": "select-value",
11
+ className: cn("flex flex-1 text-left", className),
12
+ ...props
13
+ });
14
+ }
15
+ function SelectTrigger({ className, size = "default", children, ...props }) {
16
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectTrigger$1, {
17
+ "data-slot": "select-trigger",
18
+ "data-size": size,
19
+ className: cn("flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
20
+ ...props,
21
+ children: [children, /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectIcon, { render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, { className: "pointer-events-none size-4 text-muted-foreground" }) })]
22
+ });
23
+ }
24
+ function SelectContent({ className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, alignItemWithTrigger = true, ...props }) {
25
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectPortal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectPositioner, {
26
+ side,
27
+ sideOffset,
28
+ align,
29
+ alignOffset,
30
+ alignItemWithTrigger,
31
+ className: "isolate z-50",
32
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectPopup, {
33
+ "data-slot": "select-content",
34
+ "data-align-trigger": alignItemWithTrigger,
35
+ className: cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className),
36
+ ...props,
37
+ children: [
38
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollUpButton, {}),
39
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectList, { children }),
40
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollDownButton, {})
41
+ ]
42
+ })
43
+ }) });
44
+ }
45
+ function SelectItem({ className, children, ...props }) {
46
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectItem$1, {
47
+ "data-slot": "select-item",
48
+ className: cn("relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className),
49
+ ...props,
50
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItemText, {
51
+ className: "flex flex-1 shrink-0 gap-2 whitespace-nowrap",
52
+ children
53
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItemIndicator, {
54
+ render: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center" }),
55
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "pointer-events-none" })
56
+ })]
57
+ });
58
+ }
59
+ function SelectScrollUpButton({ className, ...props }) {
60
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollUpArrow, {
61
+ "data-slot": "select-scroll-up-button",
62
+ className: cn("top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", className),
63
+ ...props,
64
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronUp, {})
65
+ });
66
+ }
67
+ function SelectScrollDownButton({ className, ...props }) {
68
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectScrollDownArrow, {
69
+ "data-slot": "select-scroll-down-button",
70
+ className: cn("bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", className),
71
+ ...props,
72
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, {})
73
+ });
74
+ }
75
+ //#endregion
76
+ export { SelectValue as a, SelectTrigger as i, SelectContent as n, SelectItem as r, Select$1 as t };
@@ -1,8 +1,10 @@
1
- import { U as require_jsx_runtime } from "../_libs/@base-ui/react+[...].mjs";
2
- import { S as Input$1, _ as SelectItem, b as Label, g as SelectContent, h as Select$1, v as SelectTrigger, y as SelectValue } from "./router-BbrBz36P.mjs";
3
- import { t as Switch$1 } from "./switch-1pnd_bPJ.mjs";
4
- import { t as Textarea } from "./textarea-CHJTKtsJ.mjs";
5
- //#region node_modules/.nitro/vite/services/ssr/assets/setting-fields-mpXfGftI.js
1
+ import { K as require_jsx_runtime } from "../_libs/@base-ui/react+[...].mjs";
2
+ import { g as Input$1 } from "./router-DdwREiOf.mjs";
3
+ import { r as Label } from "./page-DZVxKvdn.mjs";
4
+ import { a as SelectValue, i as SelectTrigger, n as SelectContent, r as SelectItem, t as Select$1 } from "./select-BtndHhHb.mjs";
5
+ import { t as Switch$1 } from "./switch-odxNO_S4.mjs";
6
+ import { t as Textarea } from "./textarea-D2c_vTIC.mjs";
7
+ //#region node_modules/.nitro/vite/services/ssr/assets/setting-fields-LNgywAW7.js
6
8
  var import_jsx_runtime = require_jsx_runtime();
7
9
  /**
8
10
  * One renderer for anything a connector declares as a field, so connection
@@ -31,16 +33,19 @@ function SettingFieldInputs({ fields, values, onChange, idPrefix = "" }) {
31
33
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fields.map((field) => {
32
34
  const id = `${idPrefix}${field.key}`;
33
35
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
34
- className: "flex flex-col gap-2",
36
+ className: "flex flex-col gap-1.5",
35
37
  children: field.kind === "boolean" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
36
38
  className: "flex items-center justify-between gap-4",
37
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
38
- htmlFor: id,
39
- children: field.label
40
- }), field.help ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
41
- className: "mt-1 text-xs text-muted-foreground",
42
- children: field.help
43
- }) : null] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch$1, {
39
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
40
+ className: "flex flex-col gap-0.5",
41
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
42
+ htmlFor: id,
43
+ children: field.label
44
+ }), field.help ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
45
+ className: "text-xs text-muted-foreground",
46
+ children: field.help
47
+ }) : null]
48
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch$1, {
44
49
  id,
45
50
  checked: Boolean(values[field.key]),
46
51
  onCheckedChange: (checked) => onChange(field.key, checked)
@@ -50,13 +55,9 @@ function SettingFieldInputs({ fields, values, onChange, idPrefix = "" }) {
50
55
  htmlFor: id,
51
56
  children: field.label
52
57
  }),
53
- field.help ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
54
- className: "text-xs text-muted-foreground",
55
- children: field.help
56
- }) : null,
57
58
  field.kind === "string_list" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
58
59
  id,
59
- rows: 4,
60
+ rows: 3,
60
61
  placeholder: field.placeholder,
61
62
  value: (values[field.key] ?? []).join("\n"),
62
63
  onChange: (event) => onChange(field.key, event.target.value.split("\n").map((line) => line.trim()).filter(Boolean))
@@ -65,7 +66,7 @@ function SettingFieldInputs({ fields, values, onChange, idPrefix = "" }) {
65
66
  onValueChange: (value) => onChange(field.key, value),
66
67
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
67
68
  id,
68
- className: "w-full",
69
+ className: "w-full sm:max-w-xs",
69
70
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, { children: (value) => field.options.find((option) => option.value === value)?.label ?? value })
70
71
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: field.options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
71
72
  value: option.value,
@@ -76,7 +77,11 @@ function SettingFieldInputs({ fields, values, onChange, idPrefix = "" }) {
76
77
  placeholder: field.placeholder,
77
78
  value: values[field.key],
78
79
  onChange: (event) => onChange(field.key, event.target.value)
79
- })
80
+ }),
81
+ field.help ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
82
+ className: "text-xs text-muted-foreground",
83
+ children: field.help
84
+ }) : null
80
85
  ] })
81
86
  }, field.key);
82
87
  }) });