loopable-cli 0.2.1 → 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 (134) 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-C3xBnKfy.js → refresh-cw-BzSY7M8I.js} +1 -1
  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-C9yydJrC.mjs → _connectorId-COuQN02a.mjs} +198 -188
  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/{agents-DG643yoJ.mjs → agents-B_qo1gpL.mjs} +195 -6
  46. package/.output/server/_ssr/{agents-C7B6RJ6-.mjs → agents-Bz10gz_m.mjs} +8 -8
  47. package/.output/server/_ssr/agents-CjbOUp3Z.mjs +206 -0
  48. package/.output/server/_ssr/{alert-DQvzE7bu.mjs → alert-C2WBWfzo.mjs} +2 -2
  49. package/.output/server/_ssr/card-r1cV-PCN.mjs +36 -0
  50. package/.output/server/_ssr/{client-BtVRVhQ2.mjs → client-Dk3SD8Il.mjs} +5 -1
  51. package/.output/server/_ssr/connection-status-BogAL-Fj.mjs +32 -0
  52. package/.output/server/_ssr/{connections-Df-aDpfJ.mjs → connections-Cqvb6Gw2.mjs} +372 -325
  53. package/.output/server/_ssr/connector-icon-DrhVbhs6.mjs +190 -0
  54. package/.output/server/_ssr/connectors-BfS5tQKr.mjs +81 -0
  55. package/.output/server/_ssr/{connectors-e0mf2pMs.mjs → connectors-DJI-rzPU.mjs} +10 -10
  56. package/.output/server/_ssr/{createServerRpc-DPDrnFdH.mjs → createServerRpc-DDrp2nLT.mjs} +2 -2
  57. package/.output/server/_ssr/{gaps-Cz76ZicM.mjs → gaps-CcvHiLcj.mjs} +15 -7
  58. package/.output/server/_ssr/inbox-D1ef761v.mjs +178 -0
  59. package/.output/server/_ssr/loop-form-DWwOR05R.mjs +346 -0
  60. package/.output/server/_ssr/{loops-D8WuK7Ke.mjs → loops-6WhUe_lc.mjs} +37 -27
  61. package/.output/server/_ssr/loops-DHPjzG-t.mjs +217 -0
  62. package/.output/server/_ssr/new-ChKBdcXJ.mjs +245 -0
  63. package/.output/server/_ssr/page-DZVxKvdn.mjs +120 -0
  64. package/.output/server/_ssr/{router-54IDWTVz.mjs → router-DdwREiOf.mjs} +162 -489
  65. package/.output/server/_ssr/runners-CK9Cx9fS.mjs +217 -0
  66. package/.output/server/_ssr/runners-CrznFPHt.mjs +140 -0
  67. package/.output/server/_ssr/{runners-Cj6z9Mqh.mjs → runners-W-pgo5yo.mjs} +5 -5
  68. package/.output/server/_ssr/{secrets-CRnpwGBh.mjs → secrets-CV5scFlt.mjs} +2 -2
  69. package/.output/server/_ssr/select-BtndHhHb.mjs +76 -0
  70. package/.output/server/_ssr/{setting-fields-DzBlslan.mjs → setting-fields-LNgywAW7.mjs} +25 -20
  71. package/.output/server/_ssr/ssr.mjs +43 -42
  72. package/.output/server/_ssr/{start-Vq3Oan3v.mjs → start-C_j-e4vF.mjs} +9 -4
  73. package/.output/server/_ssr/{switch-1pnd_bPJ.mjs → switch-odxNO_S4.mjs} +3 -3
  74. package/.output/server/_ssr/task-list-Dnmq_WeE.mjs +150 -0
  75. package/.output/server/_ssr/{tasks-Qtml5TlK.mjs → tasks-D-kXLgVP.mjs} +13 -13
  76. package/.output/server/_ssr/{tasks-BDqznm57.mjs → tasks-D7aUvA33.mjs} +44 -17
  77. package/.output/server/_ssr/{textarea-CHJTKtsJ.mjs → textarea-D2c_vTIC.mjs} +2 -2
  78. package/.output/server/_ssr/time-Cy8TZP0Z.mjs +13 -0
  79. package/.output/server/_ssr/use-live-tasks-BzhTkEOL.mjs +40 -0
  80. package/.output/server/_tanstack-start-manifest_v-2HUdn-2E.mjs +166 -0
  81. package/.output/server/_taskId-D3KkNJW1.mjs +263 -0
  82. package/.output/server/index.mjs +199 -164
  83. package/README.md +36 -6
  84. package/dist/cli.mjs +161 -8
  85. package/dist/dispatcher.mjs +671 -285
  86. package/dist/runner.mjs +333 -46
  87. package/drizzle/0025_prompt_tasks_done.sql +2 -0
  88. package/drizzle/0026_guidance_into_prompt.sql +3 -0
  89. package/drizzle/0027_relative_folders.sql +5 -0
  90. package/drizzle/0028_runner_version.sql +5 -0
  91. package/drizzle/meta/_journal.json +29 -1
  92. package/package.json +4 -2
  93. package/.output/public/assets/_connectorId-CkygNLny.js +0 -1
  94. package/.output/public/assets/_loopId-Cshlc5Ja.js +0 -1
  95. package/.output/public/assets/_taskId-BKDIJ_Fg.js +0 -1
  96. package/.output/public/assets/alert-BT2EVMJP.js +0 -1
  97. package/.output/public/assets/arrow-left-CWUZt6k2.js +0 -1
  98. package/.output/public/assets/arrow-right-dy_oOg_c.js +0 -1
  99. package/.output/public/assets/connection-status-DCqqmY_6.js +0 -1
  100. package/.output/public/assets/connector-icon-BUoK6Cru.js +0 -1
  101. package/.output/public/assets/connectors-CeLBtEtg.js +0 -1
  102. package/.output/public/assets/external-link-DC11orFA.js +0 -1
  103. package/.output/public/assets/gaps-BtFoD7mF.js +0 -1
  104. package/.output/public/assets/inbox-BYNjkNDJ.js +0 -1
  105. package/.output/public/assets/index-0yP3Jn_G.js +0 -18
  106. package/.output/public/assets/loader-B4Epchm_.js +0 -1
  107. package/.output/public/assets/loop-form-DnSJNc1k.js +0 -1
  108. package/.output/public/assets/loops-1CvbqO6m.js +0 -1
  109. package/.output/public/assets/new-m5NlqYQT.js +0 -1
  110. package/.output/public/assets/play-DD4xQgvi.js +0 -1
  111. package/.output/public/assets/preload-helper-CfdAYKD7.js +0 -3
  112. package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +0 -1
  113. package/.output/public/assets/runners-D8QVl-Y6.js +0 -1
  114. package/.output/public/assets/select-Cppu-hti.js +0 -1
  115. package/.output/public/assets/setting-fields-bJ_gTG7Q.js +0 -3
  116. package/.output/public/assets/styles-ClTGaXU0.css +0 -2
  117. package/.output/public/assets/switch-hRIbNGOj.js +0 -1
  118. package/.output/public/assets/task-list-BAxjcVgr.js +0 -1
  119. package/.output/public/assets/textarea-Fyuzi6Vp.js +0 -1
  120. package/.output/public/assets/trash-2-BXZzBsoV.js +0 -1
  121. package/.output/public/assets/useValueChanged-VVyq4hb9.js +0 -1
  122. package/.output/server/_loopId-B0VG-eBC.mjs +0 -301
  123. package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +0 -31
  124. package/.output/server/_ssr/connector-icon-WvxwZ5TF.mjs +0 -56
  125. package/.output/server/_ssr/connectors-BU0IiRge.mjs +0 -107
  126. package/.output/server/_ssr/inbox-CAUBa1I3.mjs +0 -114
  127. package/.output/server/_ssr/loop-form-FqSaXiHl.mjs +0 -413
  128. package/.output/server/_ssr/loops-Dqoebe8q.mjs +0 -222
  129. package/.output/server/_ssr/new-bBsVaWD6.mjs +0 -169
  130. package/.output/server/_ssr/runners-D2UAdLyj.mjs +0 -106
  131. package/.output/server/_ssr/runners-DGWaVvWi.mjs +0 -116
  132. package/.output/server/_ssr/task-list-_qgBS9LU.mjs +0 -187
  133. package/.output/server/_tanstack-start-manifest_v-CvheE7ZF.mjs +0 -131
  134. package/.output/server/_taskId-V4QLBO2N.mjs +0 -256
@@ -55,9 +55,32 @@ var init_define = __esm({
55
55
  }
56
56
  });
57
57
 
58
+ // src/agents/claude/manifest.ts
59
+ var claudeManifest;
60
+ var init_manifest = __esm({
61
+ "src/agents/claude/manifest.ts"() {
62
+ "use strict";
63
+ init_define();
64
+ claudeManifest = defineAgentManifest({
65
+ id: "claude",
66
+ name: "Claude Code",
67
+ tagline: "Anthropic's coding agent, driven through its print mode.",
68
+ docsUrl: "https://docs.claude.com/en/docs/claude-code/cli-reference",
69
+ icon: "ClaudeCode",
70
+ accent: "bg-[#d97757] text-white",
71
+ binaries: ["claude"],
72
+ installHint: "Install with curl -fsSL https://claude.ai/install.sh | bash, then run claude auth login.",
73
+ permissionModes: ["read_only", "workspace_write"],
74
+ supportsModel: true,
75
+ modelPlaceholder: "e.g. sonnet or opus; leave empty for the default",
76
+ defaults: { permissionMode: "read_only", model: null, timeoutMs: 3e5 }
77
+ });
78
+ }
79
+ });
80
+
58
81
  // src/agents/codex/manifest.ts
59
82
  var codexManifest;
60
- var init_manifest = __esm({
83
+ var init_manifest2 = __esm({
61
84
  "src/agents/codex/manifest.ts"() {
62
85
  "use strict";
63
86
  init_define();
@@ -66,7 +89,7 @@ var init_manifest = __esm({
66
89
  name: "Codex",
67
90
  tagline: "OpenAI's coding agent, driven non-interactively through codex exec.",
68
91
  docsUrl: "https://developers.openai.com/codex/cli",
69
- icon: "Terminal",
92
+ icon: "Codex",
70
93
  accent: "bg-neutral-900 text-white",
71
94
  binaries: ["codex"],
72
95
  installHint: "Install with npm i -g @openai/codex, then run codex login.",
@@ -80,7 +103,7 @@ var init_manifest = __esm({
80
103
 
81
104
  // src/agents/cursor-agent/manifest.ts
82
105
  var cursorAgentManifest;
83
- var init_manifest2 = __esm({
106
+ var init_manifest3 = __esm({
84
107
  "src/agents/cursor-agent/manifest.ts"() {
85
108
  "use strict";
86
109
  init_define();
@@ -89,7 +112,7 @@ var init_manifest2 = __esm({
89
112
  name: "Cursor Agent",
90
113
  tagline: "Cursor's CLI agent, driven through its print mode.",
91
114
  docsUrl: "https://cursor.com/docs/cli",
92
- icon: "MousePointerClick",
115
+ icon: "Cursor",
93
116
  accent: "bg-blue-600 text-white",
94
117
  binaries: ["cursor-agent"],
95
118
  installHint: "Install with curl https://cursor.com/install -fsS | bash, then run cursor-agent login.",
@@ -101,11 +124,148 @@ var init_manifest2 = __esm({
101
124
  }
102
125
  });
103
126
 
127
+ // package.json
128
+ var package_default;
129
+ var init_package = __esm({
130
+ "package.json"() {
131
+ package_default = {
132
+ name: "loopable-cli",
133
+ version: "0.2.1",
134
+ description: "CLI to run Loopable: App, Dispatcher, and Runner.",
135
+ license: "MIT",
136
+ type: "module",
137
+ bin: {
138
+ loopable: "./dist/cli.mjs"
139
+ },
140
+ files: [
141
+ ".output",
142
+ "dist",
143
+ "drizzle"
144
+ ],
145
+ engines: {
146
+ node: ">=22"
147
+ },
148
+ packageManager: "pnpm@11.22.0",
149
+ repository: {
150
+ type: "git",
151
+ url: "git+https://github.com/imsobear/loopable.git"
152
+ },
153
+ bugs: {
154
+ url: "https://github.com/imsobear/loopable/issues"
155
+ },
156
+ homepage: "https://github.com/imsobear/loopable#readme",
157
+ publishConfig: {
158
+ access: "public"
159
+ },
160
+ imports: {
161
+ "#/*": "./src/*"
162
+ },
163
+ scripts: {
164
+ dev: "node --experimental-strip-types scripts/dev.ts",
165
+ start: "node dist/cli.mjs start",
166
+ dispatcher: "LOOPABLE_DEV=1 node --experimental-strip-types src/cli.ts dispatcher",
167
+ runner: "LOOPABLE_DEV=1 node --experimental-strip-types src/cli.ts runner",
168
+ "generate-routes": "tsr generate",
169
+ build: "vite build && node scripts/build-cli.mjs",
170
+ preview: "vite preview",
171
+ test: "vitest run",
172
+ "test:watch": "vitest",
173
+ "db:generate": "drizzle-kit generate",
174
+ "db:studio": "LOOPABLE_DEV=1 drizzle-kit studio",
175
+ typecheck: "tsc --noEmit",
176
+ release: "node scripts/release.mjs",
177
+ deploy: "LOOPABLE_CF=1 vite build && wrangler deploy",
178
+ "site:dev": "pnpm --filter loopable-site dev",
179
+ "site:build": "pnpm --filter loopable-site build",
180
+ "site:deploy": "pnpm --filter loopable-site deploy",
181
+ prepublishOnly: "pnpm typecheck && pnpm test && pnpm build"
182
+ },
183
+ dependencies: {
184
+ "@libsql/client": "^0.18.0"
185
+ },
186
+ devDependencies: {
187
+ "@base-ui/react": "^1.7.0",
188
+ "@cloudflare/containers": "^0.3.7",
189
+ "@cloudflare/vite-plugin": "^1.54.10",
190
+ "@fontsource-variable/geist": "^5.3.0",
191
+ "@tailwindcss/vite": "4.1.18",
192
+ "@tanstack/devtools-vite": "0.8.5",
193
+ "@tanstack/react-devtools": "0.10.12",
194
+ "@tanstack/react-router": "1.170.32",
195
+ "@tanstack/react-router-devtools": "1.167.1",
196
+ "@tanstack/react-start": "1.168.49",
197
+ "@tanstack/router-cli": "1.167.33",
198
+ "@types/node": "^22.10.2",
199
+ "@types/qrcode": "^1.5.6",
200
+ "@types/react": "^19.2.0",
201
+ "@types/react-dom": "^19.2.0",
202
+ "@vitejs/plugin-react": "^6.0.1",
203
+ "class-variance-authority": "^0.7.1",
204
+ clsx: "^2.1.1",
205
+ "drizzle-kit": "^0.31.10",
206
+ "drizzle-orm": "^0.45.2",
207
+ esbuild: "^0.28.2",
208
+ "lucide-react": "^1.39.0",
209
+ "next-themes": "^0.4.6",
210
+ nitro: "3.0.260610-beta",
211
+ qrcode: "^1.5.4",
212
+ react: "19.2.0",
213
+ "react-dom": "19.2.0",
214
+ shadcn: "^4.19.1",
215
+ sonner: "^2.0.8",
216
+ "tailwind-merge": "^3.6.0",
217
+ tailwindcss: "4.1.18",
218
+ "tw-animate-css": "^1.4.0",
219
+ typescript: "^6.0.2",
220
+ vite: "^8.0.0",
221
+ vitest: "^4.1.11",
222
+ wrangler: "^4.132.0"
223
+ }
224
+ };
225
+ }
226
+ });
227
+
228
+ // src/lib/version.ts
229
+ function olderThan(a, b) {
230
+ if (!a) return true;
231
+ const pa = a.split(".").map((part) => Number.parseInt(part, 10));
232
+ const pb = b.split(".").map((part) => Number.parseInt(part, 10));
233
+ for (let i = 0; i < 3; i++) {
234
+ const x = pa[i] ?? 0;
235
+ const y = pb[i] ?? 0;
236
+ if (Number.isNaN(x)) return true;
237
+ if (x !== y) return x < y;
238
+ }
239
+ return false;
240
+ }
241
+ var VERSION, RUNNER_PROTOCOL, LEGACY_RUNNER_PROTOCOL, FOLDER_JOB_PROTOCOL;
242
+ var init_version = __esm({
243
+ "src/lib/version.ts"() {
244
+ init_package();
245
+ VERSION = package_default.version;
246
+ RUNNER_PROTOCOL = 2;
247
+ LEGACY_RUNNER_PROTOCOL = 1;
248
+ FOLDER_JOB_PROTOCOL = 2;
249
+ }
250
+ });
251
+
252
+ // src/server/agent-job.ts
253
+ import { isAbsolute, join as join5 } from "node:path";
254
+ function folderIsLegacy(folder) {
255
+ return isAbsolute(folder.trim());
256
+ }
257
+ var init_agent_job = __esm({
258
+ "src/server/agent-job.ts"() {
259
+ "use strict";
260
+ init_version();
261
+ }
262
+ });
263
+
104
264
  // src/agents/discover.ts
105
265
  import { execFile } from "node:child_process";
106
266
  import { accessSync, constants } from "node:fs";
107
267
  import { homedir as homedir2 } from "node:os";
108
- import { join as join5 } from "node:path";
268
+ import { join as join6 } from "node:path";
109
269
  import { promisify } from "node:util";
110
270
  function unique(values) {
111
271
  return [...new Set(values.filter(Boolean))];
@@ -114,7 +274,7 @@ function inheritedDirs() {
114
274
  const home = homedir2();
115
275
  return unique([
116
276
  ...(process.env.PATH ?? "").split(":"),
117
- ...HOME_DIRS.map((dir) => join5(home, dir)),
277
+ ...HOME_DIRS.map((dir) => join6(home, dir)),
118
278
  ...SYSTEM_DIRS
119
279
  ]);
120
280
  }
@@ -132,7 +292,7 @@ async function loginShellDirs() {
132
292
  function executableIn(dirs, names) {
133
293
  for (const dir of dirs) {
134
294
  for (const name of names) {
135
- const candidate = join5(dir, name);
295
+ const candidate = join6(dir, name);
136
296
  try {
137
297
  accessSync(candidate, constants.X_OK);
138
298
  return candidate;
@@ -148,9 +308,9 @@ async function resolveBinary(names) {
148
308
  function clean(text3) {
149
309
  return text3.replace(ANSI, "").trim();
150
310
  }
151
- async function probe(bin, args3, timeoutMs = 1e4) {
311
+ async function probe(bin, args4, timeoutMs = 1e4) {
152
312
  try {
153
- const { stdout, stderr } = await exec(bin, args3, { timeout: timeoutMs });
313
+ const { stdout, stderr } = await exec(bin, args4, { timeout: timeoutMs });
154
314
  return { ok: true, text: clean(stdout) || clean(stderr) };
155
315
  } catch (error) {
156
316
  const failure = error;
@@ -298,14 +458,171 @@ var init_process = __esm({
298
458
  }
299
459
  });
300
460
 
461
+ // src/agents/claude/events.ts
462
+ function parse(line) {
463
+ const trimmed = line.trim();
464
+ if (!trimmed.startsWith("{")) return null;
465
+ try {
466
+ return JSON.parse(trimmed);
467
+ } catch {
468
+ return null;
469
+ }
470
+ }
471
+ function blocks(event) {
472
+ return event?.type === "assistant" ? event.message?.content ?? [] : [];
473
+ }
474
+ function describeTool(block, cwd) {
475
+ const verb = (block.name ?? "a tool").toLowerCase();
476
+ const input = block.input ?? {};
477
+ const subject = input.file_path ?? input.path ?? input.command ?? input.pattern ?? input.url ?? input.query;
478
+ if (typeof subject !== "string") return verb;
479
+ const short = cwd && subject.startsWith(`${cwd}/`) ? subject.slice(cwd.length + 1) : subject;
480
+ return `${verb} ${short}`;
481
+ }
482
+ function describeEvent(line, cwd) {
483
+ const event = parse(line);
484
+ if (!event) return line.trim() === "" ? null : line;
485
+ switch (event.type) {
486
+ case "system":
487
+ if (event.subtype !== "init") return null;
488
+ return event.model ? `Started with ${event.model}.` : "Started.";
489
+ case "assistant": {
490
+ const tools = blocks(event).filter((block) => block.type === "tool_use").map((block) => `Running ${describeTool(block, cwd)}`);
491
+ return tools.length > 0 ? tools.join("\n") : null;
492
+ }
493
+ case "result": {
494
+ const seconds = event.duration_ms !== void 0 ? (event.duration_ms / 1e3).toFixed(0) : "?";
495
+ const tokens = event.usage?.output_tokens;
496
+ const spent = tokens === void 0 ? "" : `, ${tokens} tokens out`;
497
+ return event.is_error ? `Failed after ${seconds}s${spent}: ${event.result ?? event.subtype ?? "unknown error"}` : `Answered after ${seconds}s${spent}.`;
498
+ }
499
+ default:
500
+ return null;
501
+ }
502
+ }
503
+ function answerFrom(stdout) {
504
+ const events = stdout.split("\n").map(parse);
505
+ const result = events.filter((event) => event?.type === "result").at(-1);
506
+ const failed = result?.is_error === true;
507
+ if (typeof result?.result === "string" && result.result.trim() !== "") {
508
+ return { text: result.result.trim(), failed };
509
+ }
510
+ const said = events.flatMap(blocks);
511
+ const lastTool = said.findLastIndex((block) => block.type === "tool_use");
512
+ const text3 = said.slice(lastTool + 1).filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n").trim();
513
+ return { text: text3, failed };
514
+ }
515
+ var init_events = __esm({
516
+ "src/agents/claude/events.ts"() {
517
+ "use strict";
518
+ }
519
+ });
520
+
521
+ // src/agents/claude/runtime.ts
522
+ async function binary() {
523
+ const found = await resolveBinary(claudeManifest.binaries);
524
+ if (!found) throw new Error("The claude binary was not found on this host.");
525
+ return found;
526
+ }
527
+ function args(input) {
528
+ const list = [
529
+ "-p",
530
+ // A stream of events rather than one block at the end, so a run can be
531
+ // watched. Print mode refuses stream-json without --verbose.
532
+ "--output-format",
533
+ "stream-json",
534
+ "--verbose",
535
+ // Session files would outlive a run that is meant to leave nothing behind.
536
+ "--no-session-persistence"
537
+ ];
538
+ if (input.settings.permissionMode === "read_only") {
539
+ list.push("--permission-mode", "dontAsk", "--tools", "Read,Grep,Glob");
540
+ } else {
541
+ list.push("--permission-mode", "acceptEdits", "--allowedTools", "Bash");
542
+ }
543
+ if (input.settings.model) list.push("--model", input.settings.model);
544
+ list.push("--", input.prompt);
545
+ return list;
546
+ }
547
+ var claudeRuntime;
548
+ var init_runtime = __esm({
549
+ "src/agents/claude/runtime.ts"() {
550
+ "use strict";
551
+ init_define();
552
+ init_discover();
553
+ init_process();
554
+ init_events();
555
+ init_manifest();
556
+ claudeRuntime = defineAgentRuntime({
557
+ async detect() {
558
+ const found = await resolveBinary(claudeManifest.binaries);
559
+ if (!found) return { installed: false };
560
+ const version2 = await probe(found, ["--version"]);
561
+ return { installed: true, binaryPath: found, version: firstLine(version2.text) };
562
+ },
563
+ async authStatus() {
564
+ const found = await resolveBinary(claudeManifest.binaries);
565
+ if (!found) return { signedIn: false, detail: "Not installed." };
566
+ const result = await probe(found, ["auth", "status", "--json"]);
567
+ try {
568
+ const status = JSON.parse(result.text);
569
+ return status.loggedIn ? { signedIn: true, detail: `Signed in (${status.authMethod ?? "unknown method"}).` } : { signedIn: false, detail: "Run claude auth login." };
570
+ } catch {
571
+ return {
572
+ signedIn: result.ok,
573
+ detail: firstLine(result.text) || (result.ok ? "Signed in." : "Run claude auth login.")
574
+ };
575
+ }
576
+ },
577
+ invocation(input) {
578
+ return { bin: claudeManifest.binaries[0], args: args(input) };
579
+ },
580
+ async run(input) {
581
+ const bin = await binary();
582
+ const argv = args(input);
583
+ const outcome = await runProcess({
584
+ bin,
585
+ args: argv,
586
+ cwd: input.cwd,
587
+ timeoutMs: input.settings.timeoutMs,
588
+ signal: input.signal,
589
+ logPath: input.logFile,
590
+ logLine: (line) => describeEvent(line, input.cwd)
591
+ });
592
+ const command = `${bin} ${argv.join(" ")}`;
593
+ const answer = answerFrom(outcome.stdout);
594
+ if (outcome.aborted) {
595
+ return { ok: false, aborted: true, output: answer.text, durationMs: outcome.durationMs, command };
596
+ }
597
+ if (outcome.timedOut) {
598
+ return {
599
+ ok: false,
600
+ output: answer.text,
601
+ detail: `Claude Code did not finish within ${Math.round(input.settings.timeoutMs / 1e3)}s.`,
602
+ durationMs: outcome.durationMs,
603
+ command
604
+ };
605
+ }
606
+ return {
607
+ ok: outcome.code === 0 && !answer.failed,
608
+ output: answer.text,
609
+ detail: outcome.code === 0 && !answer.failed ? void 0 : answer.failed ? answer.text || "The agent reported an error." : outcome.stderr || `Exited with code ${outcome.code}.`,
610
+ durationMs: outcome.durationMs,
611
+ command
612
+ };
613
+ }
614
+ });
615
+ }
616
+ });
617
+
301
618
  // src/agents/codex/runtime.ts
302
619
  import { readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
303
- async function binary() {
620
+ async function binary2() {
304
621
  const found = await resolveBinary(codexManifest.binaries);
305
622
  if (!found) throw new Error("The codex binary was not found on this host.");
306
623
  return found;
307
624
  }
308
- function args(input) {
625
+ function args2(input) {
309
626
  const list = [
310
627
  "exec",
311
628
  "--sandbox",
@@ -325,13 +642,13 @@ function args(input) {
325
642
  return list;
326
643
  }
327
644
  var codexRuntime;
328
- var init_runtime = __esm({
645
+ var init_runtime2 = __esm({
329
646
  "src/agents/codex/runtime.ts"() {
330
647
  "use strict";
331
648
  init_define();
332
649
  init_discover();
333
650
  init_process();
334
- init_manifest();
651
+ init_manifest2();
335
652
  codexRuntime = defineAgentRuntime({
336
653
  async detect() {
337
654
  const found = await resolveBinary(codexManifest.binaries);
@@ -349,11 +666,11 @@ var init_runtime = __esm({
349
666
  };
350
667
  },
351
668
  invocation(input) {
352
- return { bin: codexManifest.binaries[0], args: args(input) };
669
+ return { bin: codexManifest.binaries[0], args: args2(input) };
353
670
  },
354
671
  async run(input) {
355
- const bin = await binary();
356
- const argv = args(input);
672
+ const bin = await binary2();
673
+ const argv = args2(input);
357
674
  const command = `${bin} ${argv.join(" ")}`;
358
675
  const outcome = await runProcess({
359
676
  bin,
@@ -398,7 +715,7 @@ var init_runtime = __esm({
398
715
  });
399
716
 
400
717
  // src/agents/cursor-agent/events.ts
401
- function parse(line) {
718
+ function parse2(line) {
402
719
  const trimmed = line.trim();
403
720
  if (!trimmed.startsWith("{")) return null;
404
721
  try {
@@ -410,24 +727,24 @@ function parse(line) {
410
727
  function textOf3(event) {
411
728
  return (event.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
412
729
  }
413
- function describeTool(call3, cwd) {
730
+ function describeTool2(call3, cwd) {
414
731
  const [name, body] = Object.entries(call3 ?? {})[0] ?? [];
415
732
  if (!name) return "a tool";
416
733
  const verb = name.replace(/ToolCall$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
417
- const args3 = body?.args ?? {};
418
- const subject = args3.path ?? args3.command ?? args3.query ?? args3.pattern;
734
+ const args4 = body?.args ?? {};
735
+ const subject = args4.path ?? args4.command ?? args4.query ?? args4.pattern;
419
736
  if (typeof subject !== "string") return verb;
420
737
  const short = cwd && subject.startsWith(`${cwd}/`) ? subject.slice(cwd.length + 1) : subject;
421
738
  return `${verb} ${short}`;
422
739
  }
423
- function describeEvent(line, cwd) {
424
- const event = parse(line);
740
+ function describeEvent2(line, cwd) {
741
+ const event = parse2(line);
425
742
  if (!event) return line.trim() === "" ? null : line;
426
743
  switch (event.type) {
427
744
  case "system":
428
745
  return event.model ? `Started with ${event.model}.` : "Started.";
429
746
  case "tool_call":
430
- return event.subtype === "started" ? `Running ${describeTool(event.tool_call, cwd)}` : null;
747
+ return event.subtype === "started" ? `Running ${describeTool2(event.tool_call, cwd)}` : null;
431
748
  case "assistant":
432
749
  return null;
433
750
  case "result": {
@@ -440,8 +757,8 @@ function describeEvent(line, cwd) {
440
757
  return null;
441
758
  }
442
759
  }
443
- function answerFrom(stdout) {
444
- const events = stdout.split("\n").map(parse);
760
+ function answerFrom2(stdout) {
761
+ const events = stdout.split("\n").map(parse2);
445
762
  const result = events.filter((event) => event?.type === "result").at(-1);
446
763
  const failed = result?.is_error === true;
447
764
  const whole = events.findLastIndex(
@@ -453,19 +770,19 @@ function answerFrom(stdout) {
453
770
  if (partial.length > 0) return { text: partial.join("").trim(), failed };
454
771
  return { text: (result?.result ?? "").trim(), failed };
455
772
  }
456
- var init_events = __esm({
773
+ var init_events2 = __esm({
457
774
  "src/agents/cursor-agent/events.ts"() {
458
775
  "use strict";
459
776
  }
460
777
  });
461
778
 
462
779
  // src/agents/cursor-agent/runtime.ts
463
- async function binary2() {
780
+ async function binary3() {
464
781
  const found = await resolveBinary(cursorAgentManifest.binaries);
465
782
  if (!found) throw new Error("The cursor-agent binary was not found on this host.");
466
783
  return found;
467
784
  }
468
- function args2(input) {
785
+ function args3(input) {
469
786
  const list = [
470
787
  "-p",
471
788
  // A stream of events rather than one block at the end: it can be watched
@@ -490,14 +807,14 @@ function args2(input) {
490
807
  return list;
491
808
  }
492
809
  var cursorAgentRuntime;
493
- var init_runtime2 = __esm({
810
+ var init_runtime3 = __esm({
494
811
  "src/agents/cursor-agent/runtime.ts"() {
495
812
  "use strict";
496
813
  init_define();
497
814
  init_discover();
498
815
  init_process();
499
- init_events();
500
- init_manifest2();
816
+ init_events2();
817
+ init_manifest3();
501
818
  cursorAgentRuntime = defineAgentRuntime({
502
819
  async detect() {
503
820
  const found = await resolveBinary(cursorAgentManifest.binaries);
@@ -516,11 +833,11 @@ var init_runtime2 = __esm({
516
833
  };
517
834
  },
518
835
  invocation(input) {
519
- return { bin: cursorAgentManifest.binaries[0], args: args2(input) };
836
+ return { bin: cursorAgentManifest.binaries[0], args: args3(input) };
520
837
  },
521
838
  async run(input) {
522
- const bin = await binary2();
523
- const argv = args2(input);
839
+ const bin = await binary3();
840
+ const argv = args3(input);
524
841
  const outcome = await runProcess({
525
842
  bin,
526
843
  args: argv,
@@ -528,10 +845,10 @@ var init_runtime2 = __esm({
528
845
  timeoutMs: input.settings.timeoutMs,
529
846
  signal: input.signal,
530
847
  logPath: input.logFile,
531
- logLine: (line) => describeEvent(line, input.cwd)
848
+ logLine: (line) => describeEvent2(line, input.cwd)
532
849
  });
533
850
  const command = `${bin} ${argv.join(" ")}`;
534
- const answer = answerFrom(outcome.stdout);
851
+ const answer = answerFrom2(outcome.stdout);
535
852
  if (outcome.aborted) {
536
853
  return { ok: false, aborted: true, output: answer.text, durationMs: outcome.durationMs, command };
537
854
  }
@@ -567,8 +884,10 @@ var init_runtimes = __esm({
567
884
  "src/agents/runtimes.ts"() {
568
885
  init_runtime();
569
886
  init_runtime2();
887
+ init_runtime3();
570
888
  RUNTIMES2 = {
571
889
  codex: codexRuntime,
890
+ claude: claudeRuntime,
572
891
  "cursor-agent": cursorAgentRuntime
573
892
  };
574
893
  }
@@ -580,7 +899,7 @@ import { createServer } from "node:http";
580
899
  // src/server/signals.ts
581
900
  import { mkdtempSync } from "node:fs";
582
901
  import { tmpdir } from "node:os";
583
- import { join as join7 } from "node:path";
902
+ import { join as join8 } from "node:path";
584
903
 
585
904
  // node_modules/.pnpm/drizzle-orm@0.45.2_@libsql+client@0.18.0_@types+better-sqlite3@9.6.0_better-sqlite3@13.0.3/node_modules/drizzle-orm/entity.js
586
905
  var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
@@ -823,8 +1142,8 @@ var ForeignKey = class {
823
1142
  };
824
1143
 
825
1144
  // node_modules/.pnpm/drizzle-orm@0.45.2_@libsql+client@0.18.0_@types+better-sqlite3@9.6.0_better-sqlite3@13.0.3/node_modules/drizzle-orm/tracing-utils.js
826
- function iife(fn, ...args3) {
827
- return fn(...args3);
1145
+ function iife(fn, ...args4) {
1146
+ return fn(...args4);
828
1147
  }
829
1148
 
830
1149
  // node_modules/.pnpm/drizzle-orm@0.45.2_@libsql+client@0.18.0_@types+better-sqlite3@9.6.0_better-sqlite3@13.0.3/node_modules/drizzle-orm/pg-core/unique-constraint.js
@@ -1612,7 +1931,7 @@ function sql(strings, ...params) {
1612
1931
  return new SQL([new StringChunk(str)]);
1613
1932
  }
1614
1933
  sql2.raw = raw;
1615
- function join8(chunks, separator) {
1934
+ function join9(chunks, separator) {
1616
1935
  const result = [];
1617
1936
  for (const [i, chunk] of chunks.entries()) {
1618
1937
  if (i > 0 && separator !== void 0) {
@@ -1622,7 +1941,7 @@ function sql(strings, ...params) {
1622
1941
  }
1623
1942
  return new SQL(result);
1624
1943
  }
1625
- sql2.join = join8;
1944
+ sql2.join = join9;
1626
1945
  function identifier(value) {
1627
1946
  return new Name(value);
1628
1947
  }
@@ -2490,7 +2809,7 @@ var repositories = {
2490
2809
  key: "repositories",
2491
2810
  kind: "string_list",
2492
2811
  label: "Repositories",
2493
- help: "owner/name, one per line. Wildcards work: acme/* or */*. Leave empty for every repository you can see.",
2812
+ help: "owner/name, one per line. acme/* works. Empty means all.",
2494
2813
  placeholder: "acme/web"
2495
2814
  };
2496
2815
  var ignoreDrafts = {
@@ -2503,7 +2822,7 @@ var ignoreBots = {
2503
2822
  key: "ignoreBots",
2504
2823
  kind: "boolean",
2505
2824
  label: "Skip pull requests opened by bots",
2506
- help: "Dependabot and friends open a great many, and each one costs a full run.",
2825
+ help: "Such as Dependabot.",
2507
2826
  default: true
2508
2827
  };
2509
2828
  var sizeLimit = {
@@ -2522,7 +2841,7 @@ var issueTarget = {
2522
2841
  key: "issue",
2523
2842
  kind: "text",
2524
2843
  label: "Issue or pull request",
2525
- help: "A GitHub URL. Leave empty to write back to whatever triggered the loop.",
2844
+ help: "Empty replies where it was asked. Or a GitHub issue URL.",
2526
2845
  placeholder: "https://github.com/acme/web/issues/12"
2527
2846
  };
2528
2847
  var REVIEW_PROMPT = [
@@ -2540,94 +2859,77 @@ var REVIEW_PROMPT = [
2540
2859
  "Where you cannot see enough of the code to judge something, say so instead of",
2541
2860
  "guessing."
2542
2861
  ].join("\n");
2543
- var PLAN_PROMPT = [
2544
- "Write a short implementation plan for this issue.",
2545
- "",
2546
- "Say what should change, in which files where you can tell, and what to watch",
2547
- "out for. Stay inside what the issue actually supports: if it is too vague to",
2548
- "plan, say what is missing rather than inventing the requirements.",
2549
- "",
2550
- "Do not write the implementation."
2551
- ].join("\n");
2552
- var IMPLEMENT_PROMPT = [
2553
- "Implement this issue in the checkout you are in.",
2862
+ var REPLY_PROMPT = [
2863
+ "Answer this issue in a comment, the way a teammate who knows this codebase would.",
2554
2864
  "",
2555
- "Read enough of the surrounding code first that what you write looks like it",
2556
- "belongs: the same patterns, the same names, the same way of handling errors.",
2557
- "Where the project has tests for work like this, add one.",
2865
+ "Work out what the issue is asking for. If it is a question, answer it from the",
2866
+ "code rather than from memory. If it reports a bug, find the likely cause and",
2867
+ "point at the files and lines involved. If it asks for a change, say how you",
2868
+ "would make it, where, and what to watch out for.",
2558
2869
  "",
2559
- "Stay inside what the issue asks for. A change that also tidies three other",
2560
- "things is a change nobody can review. If the issue is too vague to implement,",
2561
- "or doing it properly needs a decision that is not yours to make, reply with",
2562
- "exactly NOTHING_TO_DO rather than guessing: an unwanted pull request costs",
2563
- "more attention than a missing one.",
2564
- "",
2565
- "Run the project's tests if you can work out how, and say in your description",
2566
- "whether you did and what happened."
2870
+ "Read the code; do not change it. If the issue is too vague to answer, say what",
2871
+ "is missing rather than inventing it."
2567
2872
  ].join("\n");
2568
2873
  var githubManifest = defineManifest({
2569
2874
  id: "github",
2570
2875
  name: "GitHub",
2571
- tagline: "Pick up review requests and assigned issues.",
2876
+ tagline: "Pick up reviews and issues sent to your team's shared GitHub account.",
2572
2877
  docsUrl: "https://docs.github.com/rest",
2573
2878
  icon: "Github",
2574
2879
  accent: "bg-neutral-900 text-white",
2575
2880
  auth: {
2576
2881
  kind: "oauth_redirect",
2577
2882
  scopes: GITHUB_SCOPES,
2578
- needsAppRegistration: true
2883
+ needsAppRegistration: true,
2884
+ // The Connection is the team's agent on GitHub: whatever it writes appears
2885
+ // under its name, and the work it picks up is whatever the team sends to
2886
+ // it. An account made for this is cleanest, but a teammate's own works too
2887
+ // if they are happy for it to be shared.
2888
+ note: "Connect the account the team will send work to. An account made for this is cleanest, but your own works too if you are happy to share it. Reviews and comments appear under its name, and it sees only the repositories it can access. Sign in to GitHub as that account first; GitHub authorizes whichever account the browser is signed in as."
2579
2889
  },
2580
- workflows: [
2890
+ triggers: [
2581
2891
  {
2582
2892
  id: "github.review_requested",
2583
- name: "Review pull requests I am asked to review",
2584
- summary: "Reads the change and posts a review whenever someone asks for yours.",
2893
+ when: "A GitHub review is requested",
2585
2894
  // Team requests matter more than they sound: in most repositories with a
2586
2895
  // CODEOWNERS file, review arrives addressed to a team rather than a person.
2587
- trigger: "your review is requested, either directly or through a team you belong to",
2896
+ trigger: "someone requests a review from the connected account, directly or through its team",
2588
2897
  // review-requested covers teams; user-review-requested would not.
2589
2898
  watches: "is:open is:pr review-requested:@me",
2590
- writes: "a review on the pull request, as a comment rather than an approval",
2591
2899
  settings: [repositories, ignoreDrafts, ignoreBots, sizeLimit],
2592
- prompt: REVIEW_PROMPT,
2593
- guidancePlaceholder: "Anything specific to your team. For example: we require a test for every new endpoint.",
2594
- answer: "review",
2595
2900
  runsIn: "checkout",
2901
+ answer: "review",
2596
2902
  actionId: "github.submit_review"
2597
2903
  },
2598
2904
  {
2599
2905
  id: "github.issue_assigned",
2600
- name: "Plan issues assigned to me",
2601
- summary: "Posts a short implementation plan when an issue lands on you.",
2602
- trigger: "an issue is assigned to you",
2603
- // is:issue matters: without it this would pick up your own pull requests.
2906
+ when: "A GitHub issue is assigned",
2907
+ trigger: "an issue is assigned to the connected account",
2908
+ // is:issue matters: without it this would pick up pull requests too.
2604
2909
  watches: "is:open is:issue assignee:@me",
2605
- writes: "a comment on the issue",
2606
2910
  settings: [repositories],
2607
- prompt: PLAN_PROMPT,
2608
- guidancePlaceholder: "Anything specific to this codebase worth knowing before planning.",
2609
- answer: "text",
2610
2911
  runsIn: "checkout",
2912
+ answer: "text",
2611
2913
  actionId: "github.post_issue_comment"
2914
+ }
2915
+ ],
2916
+ workflows: [
2917
+ {
2918
+ id: "github.review_requested",
2919
+ name: "Review pull requests",
2920
+ summary: "Posts a review when someone requests one from the connected account.",
2921
+ triggerId: "github.review_requested",
2922
+ prompt: REVIEW_PROMPT
2612
2923
  },
2613
2924
  {
2614
- id: "github.issue_implement",
2615
- name: "Implement issues assigned to me",
2616
- summary: "Writes the change and opens a draft pull request.",
2617
- trigger: "an issue is assigned to you",
2618
- watches: "is:open is:issue assignee:@me",
2619
- writes: "a draft pull request",
2620
- // The only workflow that writes code, and so the only one whose agent
2621
- // gets somewhere to write. Kept apart from "Plan issues assigned to me"
2622
- // rather than replacing it: asking for a plan and asking for the change
2623
- // are different jobs, and which one an issue deserves is a judgement
2624
- // about the issue.
2625
- runsIn: "checkout",
2626
- settings: [repositories],
2627
- prompt: IMPLEMENT_PROMPT,
2628
- guidancePlaceholder: "How work is done here. For example: every new endpoint needs a test, and we do not add dependencies without asking.",
2629
- answer: "code",
2630
- actionId: "github.open_pull_request"
2925
+ // The id is the one "Plan issues assigned to me" had. Planning was one
2926
+ // kind of answer to an issue; replying covers it and the rest, and loops
2927
+ // made for planning keep the prompt they copied.
2928
+ id: "github.issue_assigned",
2929
+ name: "Reply to issues",
2930
+ summary: "Answers an issue in a comment when it is assigned to the connected account.",
2931
+ triggerId: "github.issue_assigned",
2932
+ prompt: REPLY_PROMPT
2631
2933
  }
2632
2934
  ],
2633
2935
  actions: [
@@ -2650,80 +2952,59 @@ var githubManifest = defineManifest({
2650
2952
  // A comment is one body of markdown with nowhere to attach anything, so
2651
2953
  // a review arrives here with its findings written into the prose.
2652
2954
  accepts: ["text"]
2653
- },
2654
- {
2655
- id: "github.open_pull_request",
2656
- name: "Open a draft pull request",
2657
- summary: "Open a draft pull request for the branch the agent already pushed.",
2658
- // The agent cloned and pushed with the machine's git. This action only
2659
- // names the pull request through the Connection.
2660
- target: [],
2661
- accepts: ["code"]
2662
2955
  }
2663
2956
  ],
2664
2957
  // Nothing to configure per account: what the account can see is what GitHub
2665
2958
  // decides, and every narrowing choice belongs to a loop.
2666
2959
  settings: [],
2667
- // The redirect authorizes whichever account the browser is already signed in
2668
- // as, so a second account is out of reach without signing out of GitHub
2669
- // first. Connections are still rows, so this is a product decision only.
2960
+ // One shared account per team. The redirect also authorizes whichever account
2961
+ // the browser is already signed in as, so a second one is out of reach
2962
+ // without signing out of GitHub first. Connections are still rows, so this
2963
+ // is a product decision only.
2670
2964
  allowsMultipleAccounts: false,
2671
2965
  byHand: { kind: "link", placeholder: "https://github.com/acme/web/pull/123" }
2672
2966
  });
2673
2967
 
2674
2968
  // src/connectors/gmail/manifest.ts
2675
- var GMAIL_SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"];
2969
+ var GMAIL_SCOPES = [
2970
+ "https://www.googleapis.com/auth/gmail.readonly",
2971
+ "https://www.googleapis.com/auth/gmail.send"
2972
+ ];
2676
2973
  var extraQuery = {
2677
2974
  key: "extraQuery",
2678
2975
  kind: "text",
2679
- label: "Narrow it down",
2680
- help: "Gmail search terms, added to the query above. For example: from:bank.example.com, or subject:invoice, or -from:notifications@github.com.",
2681
- placeholder: "from:boss@example.com"
2976
+ label: "Only mail matching",
2977
+ help: "Gmail search terms, such as from:billing@vendor.com or subject:invoice.",
2978
+ placeholder: "from:alerts@example.com"
2979
+ };
2980
+ var needsMeOnly = {
2981
+ key: "needsMeOnly",
2982
+ kind: "boolean",
2983
+ label: "Skip mail that needs nothing",
2984
+ help: "A quick look first decides which mail asks for something. The rest is kept, not run.",
2985
+ default: true
2682
2986
  };
2683
2987
  var perPoll = {
2684
2988
  key: "maxPerPoll",
2685
2989
  kind: "select",
2686
- label: "At most, per look",
2687
- help: "Each mail costs a full agent run. A quiet mailbox never reaches this; a busy one should be narrowed above rather than raised here.",
2990
+ label: "At most, per check",
2688
2991
  options: [
2689
- { value: "3", label: "3 messages" },
2690
- { value: "10", label: "10 messages" },
2691
- { value: "25", label: "25 messages" }
2992
+ { value: "3", label: "3 emails" },
2993
+ { value: "10", label: "10 emails" },
2994
+ { value: "25", label: "25 emails" }
2692
2995
  ],
2693
2996
  default: "10"
2694
2997
  };
2695
- var needsMeOnly = {
2696
- key: "needsMeOnly",
2697
- kind: "boolean",
2698
- label: "Only tell me when it needs me",
2699
- help: "One cheap look at everything that arrived decides which mails are actually asking something of you, from the sender, subject and first line. The rest wait in the backlog with a reason, so you can still run one if this got it wrong.",
2700
- default: true
2701
- };
2702
2998
  var skipOwn = {
2703
2999
  key: "skipOwn",
2704
3000
  kind: "boolean",
2705
- label: "Skip mail I sent",
2706
- help: "Your own messages show up in threads you are part of, and being told about what you just sent is not useful.",
3001
+ label: "Skip mail sent from this account",
2707
3002
  default: true
2708
3003
  };
2709
- var TELL_ME_PROMPT = [
2710
- "Read the email in EMAIL.md and tell me what it says.",
2711
- "",
2712
- "You are writing a phone notification, not a summary document. Two or three",
2713
- 'sentences. No headings, no bullet lists, no preamble like "this email is".',
2714
- "Lead with who it is from and what they want.",
2715
- "",
2716
- "Say plainly if it needs something from me and by when. If there is a",
2717
- "deadline, an amount of money, a date, or a link I have to act on, include it",
2718
- "exactly as written rather than describing it.",
2719
- "",
2720
- "If it is an automated notification, a newsletter, or otherwise needs nothing",
2721
- "from me, say so in one line and stop."
2722
- ].join("\n");
2723
3004
  var gmailManifest = defineManifest({
2724
3005
  id: "gmail",
2725
3006
  name: "Gmail",
2726
- tagline: "Read new mail and have an agent tell you what matters.",
3007
+ tagline: "Start a loop when an email arrives, or email an answer to the connected account.",
2727
3008
  docsUrl: "https://developers.google.com/gmail/api/guides",
2728
3009
  icon: "Gmail",
2729
3010
  // Gmail's own red, now that the mark is Gmail's own too. A near miss on a
@@ -2734,45 +3015,40 @@ var gmailManifest = defineManifest({
2734
3015
  scopes: GMAIL_SCOPES,
2735
3016
  needsAppRegistration: true
2736
3017
  },
2737
- workflows: [
3018
+ triggers: [
2738
3019
  {
2739
3020
  id: "gmail.new_mail",
2740
- name: "Tell me about new mail",
2741
- summary: "Reads mail as it arrives and sends you a short account of it somewhere you look.",
2742
- trigger: "mail arrives in your inbox",
3021
+ when: "An email arrives",
3022
+ trigger: "an email arrives in the connected inbox",
2743
3023
  // A day's window rather than "unread": reading a mail on your phone
2744
3024
  // should not decide whether the loop ever saw it. What stops a mail
2745
- // being handled twice is its id, not its state, so re-seeing yesterday's
2746
- // costs nothing and a poller that was off for an hour misses nothing.
3025
+ // being handled twice is its id, not its state.
2747
3026
  watches: "in:inbox newer_than:1d",
2748
- writes: "a message telling you what arrived",
2749
- // Nothing to check out: the mail is the whole of the work, and it is
2750
- // handed over as a file.
3027
+ // The mail is the whole of the work, handed over as a file.
2751
3028
  runsIn: "temp",
2752
3029
  settings: [extraQuery, needsMeOnly, perPoll, skipOwn],
2753
- prompt: TELL_ME_PROMPT,
2754
- guidancePlaceholder: "What you care about in your mail. For example: I only need to know about anything involving money or a deadline.",
2755
3030
  answer: "text",
2756
- // Gmail cannot write, so this answers on WeChat, which is the point of
2757
- // the workflow rather than an accident of it: mail you are already
2758
- // reading in Gmail does not need telling about. `source` would mean
2759
- // nothing here, since no chat started this.
2760
- actionConnectorId: "wechat",
2761
- actionId: "wechat.reply",
2762
- actionTarget: { to: "me" },
2763
- // Half an hour, because mail turns up one piece at a time and the whole
2764
- // of deciding what is worth a run is having several to compare. Looking
2765
- // every two minutes meant almost always finding exactly one, which is
2766
- // not a batch, so every newsletter got an agent to itself.
2767
- //
2768
- // The cost of waiting is being told half an hour late about something
2769
- // that sat unread for hours before it was sent.
3031
+ // Kept in Inbox until a loop says where it should go.
3032
+ actionConnectorId: "schedule",
3033
+ actionId: "schedule.record",
3034
+ // Mail turns up one piece at a time, and deciding what deserves a run
3035
+ // needs several to compare.
2770
3036
  pollEveryMs: 30 * 6e4
2771
3037
  }
2772
3038
  ],
2773
- // Read-only, so there is nothing to offer. A loop built on Gmail writes
2774
- // through whichever connector it picks.
2775
- actions: [],
3039
+ // No ready-made job: what to do with mail is the team's to say, in a
3040
+ // custom loop.
3041
+ workflows: [],
3042
+ actions: [
3043
+ {
3044
+ id: "gmail.send",
3045
+ name: "Email this account",
3046
+ summary: "Send the answer to the connected account's own inbox, in the same thread when it started from an email there.",
3047
+ target: [],
3048
+ // A review sent here reads as prose, with file and line written in.
3049
+ accepts: ["text"]
3050
+ }
3051
+ ],
2776
3052
  settings: [],
2777
3053
  // The redirect authorizes whichever account the browser is signed in as, so
2778
3054
  // reaching a second one means signing out of Google first.
@@ -2868,24 +3144,17 @@ var at = {
2868
3144
  key: "at",
2869
3145
  kind: "text",
2870
3146
  label: "At",
2871
- help: "On a 24-hour clock, in this host's own time zone. Only the minutes are used by an hourly loop.",
3147
+ help: "24-hour time, in this host's time zone.",
2872
3148
  placeholder: "09:00"
2873
3149
  };
2874
3150
  var weekday = {
2875
3151
  key: "weekday",
2876
3152
  kind: "select",
2877
3153
  label: "On",
2878
- help: "Only used by a loop that runs once a week.",
3154
+ help: "For weekly loops.",
2879
3155
  options: WEEKDAY_NAMES.map((name, index2) => ({ value: String(index2), label: name })),
2880
3156
  default: "1"
2881
3157
  };
2882
- var folder = {
2883
- key: "folder",
2884
- kind: "text",
2885
- label: "Folder to work in",
2886
- help: "An absolute path. Nothing arrives to be looked at on a schedule, so this is the whole of what the agent is given.",
2887
- placeholder: "/Users/you/code/web"
2888
- };
2889
3158
  var RECURRING_PROMPT = [
2890
3159
  "Nothing has arrived: this is running because the time came round.",
2891
3160
  "",
@@ -2907,28 +3176,33 @@ var scheduleManifest = defineManifest({
2907
3176
  allowsMultipleAccounts: false,
2908
3177
  byHand: { kind: "now" },
2909
3178
  settings: [],
2910
- workflows: [
3179
+ triggers: [
2911
3180
  {
2912
3181
  id: "schedule.recurring",
2913
- name: "Do something on a schedule",
2914
- summary: "Runs an agent in a folder you name, and keeps what it says.",
3182
+ when: "On a schedule",
2915
3183
  trigger: "the time you set comes round",
2916
3184
  // No query to show. A clock is not something one can be written for,
2917
3185
  // and an invented one shown as if it were real is worse than nothing.
2918
- writes: "a note in the log",
2919
3186
  runsIn: "folder",
2920
- settings: [every, at, weekday, folder],
2921
- prompt: RECURRING_PROMPT,
2922
- guidancePlaceholder: "Anything that holds every time this runs. For example: this repository releases on Thursdays, so say if anything is unmerged by Wednesday evening.",
3187
+ settings: [every, at, weekday],
2923
3188
  answer: "text",
2924
3189
  actionId: "schedule.record"
2925
3190
  }
2926
3191
  ],
3192
+ workflows: [
3193
+ {
3194
+ id: "schedule.recurring",
3195
+ name: "Do something on a schedule",
3196
+ summary: "Runs the agent at the times you set, in a folder you name. The answer stays in Inbox, or goes to Slack, Lark, or GitHub.",
3197
+ triggerId: "schedule.recurring",
3198
+ prompt: RECURRING_PROMPT
3199
+ }
3200
+ ],
2927
3201
  actions: [
2928
3202
  {
2929
3203
  id: "schedule.record",
2930
- name: "Keep it in the log",
2931
- summary: "Leaves the answer here and sends it nowhere. Every run is recorded anyway, so this is what to use until it is worth pushing somewhere.",
3204
+ name: "Keep in Inbox",
3205
+ summary: "Keeps the answer in Inbox and sends it nowhere.",
2932
3206
  target: [],
2933
3207
  accepts: ["text", "review"]
2934
3208
  }
@@ -2936,18 +3210,11 @@ var scheduleManifest = defineManifest({
2936
3210
  });
2937
3211
 
2938
3212
  // src/connectors/slack/manifest.ts
2939
- var folder2 = {
2940
- key: "folder",
2941
- kind: "text",
2942
- label: "Folder to work in",
2943
- help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
2944
- placeholder: "/Users/you/code/your-project"
2945
- };
2946
3213
  var channel = {
2947
3214
  key: "channel",
2948
3215
  kind: "text",
2949
3216
  label: "Channel",
2950
- help: "Leave empty to reply in the thread that asked. Set a channel ID when the work started somewhere else.",
3217
+ help: "Empty replies where it was asked. Or a channel ID.",
2951
3218
  placeholder: "C0123456789"
2952
3219
  };
2953
3220
  var ASK_PROMPT = [
@@ -2982,22 +3249,27 @@ var slackManifest = defineManifest({
2982
3249
  }
2983
3250
  ]
2984
3251
  },
2985
- workflows: [
3252
+ triggers: [
2986
3253
  {
2987
3254
  id: "slack.ask",
2988
- name: "Do what I ask the bot",
2989
- summary: "Runs your agent when someone @mentions the bot or DMs it, and answers in the thread.",
2990
- trigger: "the bot is @mentioned, or someone DMs it",
2991
- writes: "a reply in the Slack thread",
3255
+ when: "The Slack bot is mentioned",
3256
+ trigger: "someone @mentions the Slack bot in a channel it is in, or DMs it",
2992
3257
  runsIn: "folder",
2993
- settings: [folder2],
2994
- prompt: ASK_PROMPT,
2995
- guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
3258
+ settings: [],
2996
3259
  answer: "text",
2997
3260
  actionId: "slack.reply",
2998
3261
  pollEveryMs: 6e4
2999
3262
  }
3000
3263
  ],
3264
+ workflows: [
3265
+ {
3266
+ id: "slack.ask",
3267
+ name: "Do what I ask the Slack bot",
3268
+ summary: "Runs the agent when someone @mentions the Slack bot or DMs it, and answers in the thread.",
3269
+ triggerId: "slack.ask",
3270
+ prompt: ASK_PROMPT
3271
+ }
3272
+ ],
3001
3273
  actions: [
3002
3274
  {
3003
3275
  id: "slack.reply",
@@ -3013,18 +3285,11 @@ var slackManifest = defineManifest({
3013
3285
  });
3014
3286
 
3015
3287
  // src/connectors/feishu/manifest.ts
3016
- var folder3 = {
3017
- key: "folder",
3018
- kind: "text",
3019
- label: "Folder to work in",
3020
- help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
3021
- placeholder: "/Users/you/code/your-project"
3022
- };
3023
3288
  var chat = {
3024
3289
  key: "chat",
3025
3290
  kind: "text",
3026
3291
  label: "Chat ID",
3027
- help: "Leave empty to reply to the message that asked. Set a chat ID (oc_\u2026) when the work started somewhere else.",
3292
+ help: "Empty replies where it was asked. Or a chat ID (oc_\u2026).",
3028
3293
  placeholder: "oc_..."
3029
3294
  };
3030
3295
  var ASK_PROMPT2 = [
@@ -3072,26 +3337,31 @@ var feishuManifest = defineManifest({
3072
3337
  }
3073
3338
  ]
3074
3339
  },
3075
- workflows: [
3340
+ triggers: [
3076
3341
  {
3077
3342
  id: "feishu.ask",
3078
- name: "Do what I ask the bot",
3079
- summary: "Runs your agent when someone @mentions the bot in a group it is in, and answers in the thread.",
3080
- trigger: "the bot is @mentioned in a group it is in",
3081
- writes: "a reply in the Feishu thread",
3343
+ when: "The Feishu / Lark bot is mentioned",
3344
+ trigger: "someone @mentions the Feishu / Lark bot in a group it is in",
3082
3345
  runsIn: "folder",
3083
- settings: [folder3],
3084
- prompt: ASK_PROMPT2,
3085
- guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
3346
+ settings: [],
3086
3347
  answer: "text",
3087
3348
  actionId: "feishu.reply",
3088
3349
  pollEveryMs: 6e4
3089
3350
  }
3090
3351
  ],
3352
+ workflows: [
3353
+ {
3354
+ id: "feishu.ask",
3355
+ name: "Do what I ask the Feishu / Lark bot",
3356
+ summary: "Runs the agent when someone @mentions the Feishu / Lark bot in a group, and answers in the thread.",
3357
+ triggerId: "feishu.ask",
3358
+ prompt: ASK_PROMPT2
3359
+ }
3360
+ ],
3091
3361
  actions: [
3092
3362
  {
3093
3363
  id: "feishu.reply",
3094
- name: "Reply in Feishu",
3364
+ name: "Reply in Feishu / Lark",
3095
3365
  summary: "Post a message back to the thread that asked, or to a chat you name.",
3096
3366
  target: [chat],
3097
3367
  accepts: ["text"]
@@ -3103,13 +3373,6 @@ var feishuManifest = defineManifest({
3103
3373
  });
3104
3374
 
3105
3375
  // src/connectors/wechat/manifest.ts
3106
- var folder4 = {
3107
- key: "folder",
3108
- kind: "text",
3109
- label: "Folder to work in",
3110
- help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
3111
- placeholder: "/Users/you/code/your-project"
3112
- };
3113
3376
  var askers = {
3114
3377
  key: "askers",
3115
3378
  kind: "select",
@@ -3124,7 +3387,7 @@ var recipient = {
3124
3387
  key: "to",
3125
3388
  kind: "select",
3126
3389
  label: "Who to send it to",
3127
- help: "WeChat only reliably delivers a reply to a conversation you started. Sending to yourself out of the blue is refused unless you have written to the bot in the last half day or so, which makes this a poor fit for anything that has to arrive overnight.",
3390
+ help: '"Me" only arrives if you messaged the bot in the last half day.',
3128
3391
  options: [
3129
3392
  { value: "source", label: "Whoever asked" },
3130
3393
  { value: "me", label: "Me" }
@@ -3155,23 +3418,28 @@ var wechatManifest = {
3155
3418
  kind: "qr_scan",
3156
3419
  note: "Scanning binds a bot to your WeChat account. Loopable can then read what people send that bot and answer as it, and nothing else on your account."
3157
3420
  },
3158
- workflows: [
3421
+ triggers: [
3159
3422
  {
3160
3423
  id: "wechat.ask",
3161
- name: "Do what I ask the bot",
3162
- summary: "Runs your agent on a runner when you message the bot, and answers in the chat.",
3163
- trigger: "you send the bot a message",
3424
+ when: "The WeChat bot gets a message",
3425
+ trigger: "someone sends the WeChat bot a message",
3164
3426
  // No query to show: messages arrive on a stream rather than being found
3165
3427
  // by asking, so there is nothing here that could be checked.
3166
- writes: "a reply in the chat",
3167
3428
  runsIn: "folder",
3168
- settings: [folder4, askers],
3169
- prompt: ASK_PROMPT3,
3170
- guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
3429
+ settings: [askers],
3171
3430
  answer: "text",
3172
3431
  actionId: "wechat.reply"
3173
3432
  }
3174
3433
  ],
3434
+ workflows: [
3435
+ {
3436
+ id: "wechat.ask",
3437
+ name: "Do what I ask the WeChat bot",
3438
+ summary: "Runs the agent when someone messages the WeChat bot, and answers in the chat.",
3439
+ triggerId: "wechat.ask",
3440
+ prompt: ASK_PROMPT3
3441
+ }
3442
+ ],
3175
3443
  actions: [
3176
3444
  {
3177
3445
  id: "wechat.reply",
@@ -3206,8 +3474,36 @@ function connectorManifest(id) {
3206
3474
  function needsAccount(id) {
3207
3475
  return connectorManifest(id)?.auth.kind !== "none";
3208
3476
  }
3477
+ function customOn(trigger) {
3478
+ return {
3479
+ ...trigger,
3480
+ triggerId: trigger.id,
3481
+ name: trigger.when,
3482
+ summary: `When ${trigger.trigger}.`,
3483
+ prompt: "",
3484
+ custom: true
3485
+ };
3486
+ }
3209
3487
  function connectorWorkflow(connectorId, workflowId) {
3210
- return connectorManifest(connectorId)?.workflows.find((entry) => entry.id === workflowId);
3488
+ const manifest = connectorManifest(connectorId);
3489
+ if (!manifest) return void 0;
3490
+ const defined = manifest.workflows.find((entry) => entry.id === workflowId);
3491
+ if (defined) {
3492
+ const trigger2 = manifest.triggers.find((entry) => entry.id === defined.triggerId);
3493
+ if (!trigger2) return void 0;
3494
+ return {
3495
+ ...trigger2,
3496
+ ...defined,
3497
+ actionId: defined.actionId ?? trigger2.actionId,
3498
+ actionConnectorId: defined.actionConnectorId ?? trigger2.actionConnectorId,
3499
+ actionTarget: defined.actionTarget ?? trigger2.actionTarget,
3500
+ pollEveryMs: defined.pollEveryMs ?? trigger2.pollEveryMs,
3501
+ triggerId: trigger2.id,
3502
+ custom: false
3503
+ };
3504
+ }
3505
+ const trigger = manifest.triggers.find((entry) => entry.id === workflowId);
3506
+ return trigger ? customOn(trigger) : void 0;
3211
3507
  }
3212
3508
  function connectorAction(connectorId, actionId) {
3213
3509
  return connectorManifest(connectorId)?.actions.find((entry) => entry.id === actionId);
@@ -3614,10 +3910,10 @@ function candidates(items, repositories2) {
3614
3910
  return repo && repoMatches(repo, repositories2) ? [{ repo, item }] : [];
3615
3911
  });
3616
3912
  }
3617
- function query(workflowId) {
3618
- const workflow = githubManifest.workflows.find((entry) => entry.id === workflowId);
3619
- if (!workflow?.watches) throw new Error(`GitHub cannot watch for ${workflowId}.`);
3620
- return workflow.watches;
3913
+ function query(triggerId) {
3914
+ const trigger = githubManifest.triggers.find((entry) => entry.id === triggerId);
3915
+ if (!trigger?.watches) throw new Error(`GitHub cannot watch for ${triggerId}.`);
3916
+ return trigger.watches;
3621
3917
  }
3622
3918
  async function pollGithub(input) {
3623
3919
  const repositories2 = stringList(input.settings.repositories);
@@ -3893,11 +4189,17 @@ var githubRuntime = defineRuntime({
3893
4189
 
3894
4190
  // src/connectors/gmail/api.ts
3895
4191
  var BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
3896
- async function request2(accessToken, path) {
4192
+ async function request2(accessToken, path, init = {}) {
3897
4193
  let res;
3898
4194
  try {
3899
4195
  res = await fetch(`${BASE}${path}`, {
3900
- headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" }
4196
+ method: init.method ?? "GET",
4197
+ headers: {
4198
+ Authorization: `Bearer ${accessToken}`,
4199
+ Accept: "application/json",
4200
+ ...init.body === void 0 ? {} : { "Content-Type": "application/json" }
4201
+ },
4202
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
3901
4203
  });
3902
4204
  } catch (error) {
3903
4205
  throw new TransientError(
@@ -3909,6 +4211,9 @@ async function request2(accessToken, path) {
3909
4211
  const message = `Gmail ${res.status} on ${path}: ${detail}`;
3910
4212
  const backoff = res.status === 403 && /rateLimitExceeded|userRateLimitExceeded/i.test(detail);
3911
4213
  if (res.status === 429 || res.status >= 500 || backoff) throw new TransientError(message);
4214
+ if (res.status === 403 && /insufficient.*scope|ACCESS_TOKEN_SCOPE_INSUFFICIENT/i.test(detail)) {
4215
+ throw new Error("This Gmail connection cannot send mail. Reconnect Gmail to allow it.");
4216
+ }
3912
4217
  throw new Error(message);
3913
4218
  }
3914
4219
  return await res.json();
@@ -3928,6 +4233,12 @@ function getMessageHeaders(accessToken, id) {
3928
4233
  function getMessage(accessToken, id) {
3929
4234
  return request2(accessToken, `/messages/${id}?format=full`);
3930
4235
  }
4236
+ function sendMessage(accessToken, raw, threadId) {
4237
+ return request2(accessToken, "/messages/send", {
4238
+ method: "POST",
4239
+ body: threadId ? { raw, threadId } : { raw }
4240
+ });
4241
+ }
3931
4242
 
3932
4243
  // src/connectors/gmail/mail.ts
3933
4244
  function mailRef(id) {
@@ -4017,12 +4328,31 @@ function mailContext(message) {
4017
4328
  ].join("\n")
4018
4329
  };
4019
4330
  }
4331
+ function base64url(text3) {
4332
+ return Buffer.from(text3, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4333
+ }
4334
+ function encodedWord(text3) {
4335
+ return /^[\x20-\x7e]*$/.test(text3) ? text3 : `=?UTF-8?B?${Buffer.from(text3, "utf8").toString("base64")}?=`;
4336
+ }
4337
+ function composeMail(input) {
4338
+ const lines = [
4339
+ `To: ${input.to}`,
4340
+ `Subject: ${encodedWord(input.subject)}`,
4341
+ ...input.inReplyTo ? [`In-Reply-To: ${input.inReplyTo}`, `References: ${input.inReplyTo}`] : [],
4342
+ "MIME-Version: 1.0",
4343
+ 'Content-Type: text/plain; charset="UTF-8"',
4344
+ "Content-Transfer-Encoding: base64",
4345
+ "",
4346
+ Buffer.from(input.body, "utf8").toString("base64").replace(/.{76}/g, "$&\r\n")
4347
+ ];
4348
+ return base64url(lines.join("\r\n"));
4349
+ }
4020
4350
 
4021
4351
  // src/connectors/gmail/poll.ts
4022
4352
  function baseQuery(workflowId) {
4023
- const workflow = gmailManifest.workflows.find((entry) => entry.id === workflowId);
4024
- if (!workflow?.watches) throw new Error(`Gmail cannot watch for ${workflowId}.`);
4025
- return workflow.watches;
4353
+ const trigger = gmailManifest.triggers.find((entry) => entry.id === workflowId);
4354
+ if (!trigger?.watches) throw new Error(`Gmail cannot watch for ${workflowId}.`);
4355
+ return trigger.watches;
4026
4356
  }
4027
4357
  function boolean2(value, fallback) {
4028
4358
  return typeof value === "boolean" ? value : fallback;
@@ -4237,8 +4567,33 @@ var gmailRuntime = defineRuntime({
4237
4567
  })
4238
4568
  };
4239
4569
  },
4240
- // No applyAction: this connector reads. A loop built on it answers through
4241
- // whichever connector it is pointed at.
4570
+ /**
4571
+ * Answers go to the connected account itself, the way a bot answers
4572
+ * whoever asked it. When the run started from an email in this inbox, the
4573
+ * answer joins that thread.
4574
+ */
4575
+ async applyAction({ actionId, source, body, credential }) {
4576
+ if (actionId !== "gmail.send") throw new Error(`Gmail cannot ${actionId}.`);
4577
+ const { accessToken } = await usableToken2(credential);
4578
+ const profile = await getProfile(accessToken);
4579
+ let subject = source.title || "From Loopable";
4580
+ let threadId;
4581
+ let inReplyTo;
4582
+ const sourceId = source.connectorId === "gmail" ? source.ref.replace(/^mail#/, "") : "";
4583
+ if (sourceId) {
4584
+ const original = await getMessage(accessToken, sourceId);
4585
+ threadId = original.threadId;
4586
+ inReplyTo = headerOf(original, "Message-ID") || void 0;
4587
+ const said = headerOf(original, "Subject");
4588
+ if (said) subject = /^re:/i.test(said) ? said : `Re: ${said}`;
4589
+ }
4590
+ const sent = await sendMessage(
4591
+ accessToken,
4592
+ composeMail({ to: profile.emailAddress, subject, body, inReplyTo }),
4593
+ threadId
4594
+ );
4595
+ return { url: permalink(sent.id) };
4596
+ },
4242
4597
  auth: {
4243
4598
  async startAuthorization({ redirectUri, state }) {
4244
4599
  const registration = requireRegistration2();
@@ -4412,8 +4767,9 @@ async function authTest(botToken) {
4412
4767
  const teamId = typeof body.team_id === "string" ? body.team_id : "";
4413
4768
  const botUserId = typeof body.user_id === "string" ? body.user_id : "";
4414
4769
  const botId = typeof body.bot_id === "string" ? body.bot_id : "";
4770
+ const botName = typeof body.user === "string" ? body.user : "";
4415
4771
  if (!teamId || !botUserId) throw new Error("Slack did not return which workspace this bot belongs to.");
4416
- return { botToken, teamId, team, teamUrl, botUserId, botId };
4772
+ return { botToken, teamId, team, teamUrl, botUserId, botId, botName };
4417
4773
  }
4418
4774
  async function listConversations(credential) {
4419
4775
  const body = await slack("conversations.list", credential.botToken, {
@@ -4526,7 +4882,9 @@ function destination(target, source) {
4526
4882
  function accountOf2(credential) {
4527
4883
  return {
4528
4884
  id: credential.teamId,
4529
- label: credential.team || credential.teamId,
4885
+ // The bot, then the workspace it is in: "@loopable · Acme". The workspace
4886
+ // alone read as if it were the bot's name.
4887
+ label: [credential.botName ? `@${credential.botName}` : null, credential.team || credential.teamId].filter(Boolean).join(" \xB7 "),
4530
4888
  url: credential.teamUrl || void 0
4531
4889
  };
4532
4890
  }
@@ -4791,7 +5149,7 @@ async function replyMessage(credential, messageId, text3) {
4791
5149
  if (!id) throw new Error("Feishu accepted the reply but did not say where it landed.");
4792
5150
  return { id, chatId };
4793
5151
  }
4794
- async function sendMessage(credential, chatId, text3) {
5152
+ async function sendMessage2(credential, chatId, text3) {
4795
5153
  const body = await call(credential.host, "/im/v1/messages", {
4796
5154
  token: credential.tenantAccessToken,
4797
5155
  method: "POST",
@@ -4939,7 +5297,7 @@ var feishuRuntime = {
4939
5297
  const { credential: account2 } = await usable(credential);
4940
5298
  const to = destination2(target, source);
4941
5299
  const text3 = body.trim();
4942
- const posted = to.messageId && source.connectorId === "feishu" ? await replyMessage(account2, to.messageId, text3) : await sendMessage(account2, to.chatId ?? "", text3);
5300
+ const posted = to.messageId && source.connectorId === "feishu" ? await replyMessage(account2, to.messageId, text3) : await sendMessage2(account2, to.chatId ?? "", text3);
4943
5301
  return { url: permalink3(account2.host, posted.chatId || to.chatId || "") };
4944
5302
  },
4945
5303
  auth: {
@@ -5130,7 +5488,7 @@ async function getUpdates(credential, cursor) {
5130
5488
  if (refused) throw new Error(refused);
5131
5489
  return { messages: body.msgs ?? [], cursor: body.get_updates_buf || cursor };
5132
5490
  }
5133
- async function sendMessage2(credential, input) {
5491
+ async function sendMessage3(credential, input) {
5134
5492
  const body = await call2(`${credential.baseUrl}/ilink/bot/sendmessage`, {
5135
5493
  method: "POST",
5136
5494
  headers: postHeaders(credential.botToken),
@@ -5279,7 +5637,7 @@ var wechatRuntime = {
5279
5637
  const account2 = credential;
5280
5638
  const to = recipientFor(target, source, account2);
5281
5639
  for (const part of chunked(body)) {
5282
- await sendMessage2(account2, {
5640
+ await sendMessage3(account2, {
5283
5641
  toUserId: to.userId,
5284
5642
  contextToken: to.contextToken,
5285
5643
  // Unique per piece, so a retry cannot post half an answer twice.
@@ -7069,7 +7427,7 @@ var SQLiteSelectQueryBuilderBase = class extends TypedQueryBuilder {
7069
7427
  const baseTableName = this.tableName;
7070
7428
  const tableName = getTableLikeName(table);
7071
7429
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
7072
- if (typeof tableName === "string" && this.config.joins?.some((join8) => join8.alias === tableName)) {
7430
+ if (typeof tableName === "string" && this.config.joins?.some((join9) => join9.alias === tableName)) {
7073
7431
  throw new Error(`Alias "${tableName}" is already used in this query`);
7074
7432
  }
7075
7433
  if (!this.isPartialSelect) {
@@ -7928,7 +8286,7 @@ var SQLiteUpdateBase = class extends QueryPromise {
7928
8286
  createJoin(joinType) {
7929
8287
  return (table, on2) => {
7930
8288
  const tableName = getTableLikeName(table);
7931
- if (typeof tableName === "string" && this.config.joins.some((join8) => join8.alias === tableName)) {
8289
+ if (typeof tableName === "string" && this.config.joins.some((join9) => join9.alias === tableName)) {
7932
8290
  throw new Error(`Alias "${tableName}" is already used in this query`);
7933
8291
  }
7934
8292
  if (typeof on2 === "function") {
@@ -9387,6 +9745,10 @@ var runners = sqliteTable("runners", {
9387
9745
  status: text2("status").$type().notNull().default("offline"),
9388
9746
  inventory: text2("inventory", { mode: "json" }).$type().notNull().default([]),
9389
9747
  lastSeenAt: integer("last_seen_at", { mode: "timestamp_ms" }),
9748
+ /** The runner's loopable-cli version. Null for runners before 0.3. */
9749
+ version: text2("version"),
9750
+ /** What jobs it understands; see RUNNER_PROTOCOL. Null means the first. */
9751
+ protocol: integer("protocol"),
9390
9752
  createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
9391
9753
  });
9392
9754
 
@@ -9409,13 +9771,13 @@ function migrationsFolder() {
9409
9771
  join3(here, "../../../drizzle"),
9410
9772
  join3(process.cwd(), "drizzle")
9411
9773
  ];
9412
- return candidates2.find((folder5) => existsSync2(folder5)) ?? join3(process.cwd(), "drizzle");
9774
+ return candidates2.find((folder) => existsSync2(folder)) ?? join3(process.cwd(), "drizzle");
9413
9775
  }
9414
9776
  async function migrateIfNeeded() {
9415
9777
  if (process.env.LOOPABLE_SKIP_MIGRATE === "1") return;
9416
- const folder5 = migrationsFolder();
9417
- if (!existsSync2(folder5)) return;
9418
- await migrate(db(), { migrationsFolder: folder5 });
9778
+ const folder = migrationsFolder();
9779
+ if (!existsSync2(folder)) return;
9780
+ await migrate(db(), { migrationsFolder: folder });
9419
9781
  }
9420
9782
  function rowsChanged(result) {
9421
9783
  return Number(result.rowsAffected ?? result.changes ?? 0);
@@ -9532,14 +9894,14 @@ async function decrypt(blob2) {
9532
9894
  return new TextDecoder().decode(opened);
9533
9895
  }
9534
9896
  function bytesToBase64(bytes) {
9535
- let binary3 = "";
9536
- for (const byte of bytes) binary3 += String.fromCharCode(byte);
9537
- return btoa(binary3);
9897
+ let binary4 = "";
9898
+ for (const byte of bytes) binary4 += String.fromCharCode(byte);
9899
+ return btoa(binary4);
9538
9900
  }
9539
9901
  function base64ToBytes(blob2) {
9540
- const binary3 = atob(blob2);
9541
- const bytes = new Uint8Array(binary3.length);
9542
- for (let i = 0; i < binary3.length; i += 1) bytes[i] = binary3.charCodeAt(i);
9902
+ const binary4 = atob(blob2);
9903
+ const bytes = new Uint8Array(binary4.length);
9904
+ for (let i = 0; i < binary4.length; i += 1) bytes[i] = binary4.charCodeAt(i);
9543
9905
  return bytes;
9544
9906
  }
9545
9907
  var tableStore = {
@@ -9657,12 +10019,13 @@ async function checkConnection(id) {
9657
10019
  // src/server/tasks.ts
9658
10020
  import { randomUUID as randomUUID2 } from "node:crypto";
9659
10021
  import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, rmSync as rmSync3, statSync, writeFileSync as writeFileSync2 } from "node:fs";
9660
- import { isAbsolute, join as join6 } from "node:path";
10022
+ import { join as join7 } from "node:path";
9661
10023
 
9662
10024
  // src/agents/manifests.ts
9663
10025
  init_manifest();
9664
10026
  init_manifest2();
9665
- var AGENT_MANIFESTS = [codexManifest, cursorAgentManifest];
10027
+ init_manifest3();
10028
+ var AGENT_MANIFESTS = [codexManifest, claudeManifest, cursorAgentManifest];
9666
10029
  function agentManifest(id) {
9667
10030
  return AGENT_MANIFESTS.find((manifest) => manifest.id === id);
9668
10031
  }
@@ -9689,6 +10052,10 @@ var REVIEW_FORMAT = [
9689
10052
  `- "findings" may be empty when the summary says everything worth saying.`
9690
10053
  ].join("\n");
9691
10054
 
10055
+ // src/server/tasks.ts
10056
+ init_agent_job();
10057
+ init_version();
10058
+
9692
10059
  // src/server/agents.ts
9693
10060
  init_runtimes();
9694
10061
  var DEFAULT_AGENT_KEY = "defaultAgentId";
@@ -9760,7 +10127,11 @@ function branchFor(input) {
9760
10127
 
9761
10128
  // src/server/runners.ts
9762
10129
  import { hostname as osHostname } from "node:os";
10130
+ init_version();
9763
10131
  var ONLINE_MS = 45e3;
10132
+ function protocolOf(row) {
10133
+ return row.protocol ?? LEGACY_RUNNER_PROTOCOL;
10134
+ }
9764
10135
  function isRunnerOnline(row, now = Date.now()) {
9765
10136
  if (row.status !== "online") return false;
9766
10137
  if (!row.lastSeenAt) return false;
@@ -9774,7 +10145,10 @@ function toView2(row, now = Date.now()) {
9774
10145
  hostname: row.hostname,
9775
10146
  status: online ? "online" : "offline",
9776
10147
  inventory: row.inventory,
9777
- lastSeenAt: row.lastSeenAt?.toISOString() ?? null
10148
+ lastSeenAt: row.lastSeenAt?.toISOString() ?? null,
10149
+ version: row.version,
10150
+ protocol: protocolOf(row),
10151
+ outdated: protocolOf(row) < RUNNER_PROTOCOL || olderThan(row.version, VERSION)
9778
10152
  };
9779
10153
  }
9780
10154
  async function listRunners() {
@@ -9798,11 +10172,13 @@ async function busyRunnerIds() {
9798
10172
  )
9799
10173
  );
9800
10174
  }
9801
- async function availableAgentIds(requiresHost = false) {
10175
+ async function availableAgentIds(requiresHost = false, minProtocol = 0) {
9802
10176
  const local = osHostname();
9803
10177
  return [
9804
10178
  ...new Set(
9805
- (await listRunners()).filter((row) => row.status === "online" && (!requiresHost || row.hostname === local)).flatMap(
10179
+ (await listRunners()).filter(
10180
+ (row) => row.status === "online" && (!requiresHost || row.hostname === local) && row.protocol >= minProtocol
10181
+ ).flatMap(
9806
10182
  (row) => row.inventory.filter((entry) => entry.installed && entry.signedIn).map((entry) => entry.agentId)
9807
10183
  )
9808
10184
  )
@@ -9811,9 +10187,11 @@ async function availableAgentIds(requiresHost = false) {
9811
10187
  async function pickRunner(input) {
9812
10188
  const now = Date.now();
9813
10189
  const local = osHostname();
9814
- let candidates2 = (await db().select().from(runners).all()).filter(
10190
+ const minProtocol = input.minProtocol ?? 0;
10191
+ const capable = (await db().select().from(runners).all()).filter(
9815
10192
  (row) => isRunnerOnline(row, now) && runnerCanRun(row, input.agentId)
9816
10193
  );
10194
+ let candidates2 = capable.filter((row) => protocolOf(row) >= minProtocol);
9817
10195
  if (input.requiresHost) {
9818
10196
  candidates2 = candidates2.filter((row) => row.hostname === local);
9819
10197
  }
@@ -9824,6 +10202,11 @@ async function pickRunner(input) {
9824
10202
  if (input.requiresHost) {
9825
10203
  throw new Error("This job needs a runner on the same host as Loopable.");
9826
10204
  }
10205
+ if (capable.length > 0) {
10206
+ throw new Error(
10207
+ "Every runner with this agent is too old for this job. Upgrade them with npm install -g loopable-cli."
10208
+ );
10209
+ }
9827
10210
  throw new Error("No runner is online that can run this agent.");
9828
10211
  }
9829
10212
 
@@ -9965,11 +10348,11 @@ async function enqueueSignal(input) {
9965
10348
  });
9966
10349
  }
9967
10350
  function sweepRunDirs(maxAgeMs = 7 * 24 * 60 * 60 * 1e3) {
9968
- const root = join6(dataDir(), "runs");
10351
+ const root = join7(dataDir(), "runs");
9969
10352
  if (!existsSync4(root)) return 0;
9970
10353
  let removed = 0;
9971
10354
  for (const entry of readdirSync(root)) {
9972
- const dir = join6(root, entry);
10355
+ const dir = join7(root, entry);
9973
10356
  try {
9974
10357
  if (Date.now() - statSync(dir).mtimeMs < maxAgeMs) continue;
9975
10358
  rmSync3(dir, { recursive: true, force: true });
@@ -10032,7 +10415,7 @@ async function preparePromptTask(id, signal) {
10032
10415
  let logPath = null;
10033
10416
  if (!isHosted()) {
10034
10417
  const workspace = runDir(id);
10035
- logPath = join6(workspace, "agent.log");
10418
+ logPath = join7(workspace, "agent.log");
10036
10419
  writeFileSync2(logPath, "", { flag: "a" });
10037
10420
  }
10038
10421
  const runnerId = await pickRunner({ agentId: task.agentId, requiresHost: false });
@@ -10056,14 +10439,17 @@ async function prepareTask(id, signal) {
10056
10439
  if (!isHosted()) {
10057
10440
  const workspace = runDir(id);
10058
10441
  for (const file of item.context) {
10059
- writeFileSync2(join6(workspace, file.name), file.body);
10442
+ writeFileSync2(join7(workspace, file.name), file.body);
10060
10443
  }
10061
- logPath = join6(workspace, "agent.log");
10444
+ logPath = join7(workspace, "agent.log");
10062
10445
  writeFileSync2(logPath, "", { flag: "a" });
10063
10446
  }
10447
+ const folder = workflow.runsIn === "folder" && typeof loop.settings.folder === "string" ? loop.settings.folder : null;
10064
10448
  const runnerId = await pickRunner({
10065
10449
  agentId,
10066
- requiresHost: workflow.runsIn === "folder"
10450
+ requiresHost: folder !== null && folderIsLegacy(folder),
10451
+ // A relative folder needs a runner that resolves it under its own home.
10452
+ minProtocol: folder !== null && !folderIsLegacy(folder) ? FOLDER_JOB_PROTOCOL : 0
10067
10453
  });
10068
10454
  await updateTask(id, {
10069
10455
  state: "awaiting_agent",
@@ -10238,7 +10624,7 @@ async function pollLoop(loop, claimed) {
10238
10624
  if (!runtime.poll) throw new Error(`${manifest.name} cannot watch for anything yet.`);
10239
10625
  const { credential } = await credentialForConnector(loop.connectorId);
10240
10626
  const answer = await runtime.poll({
10241
- workflowId: loop.workflowId,
10627
+ workflowId: workflow.triggerId,
10242
10628
  settings: loop.settings,
10243
10629
  credential,
10244
10630
  cursor: loop.pollCursor ?? null
@@ -10258,7 +10644,7 @@ async function pollLoop(loop, claimed) {
10258
10644
  signals: fresh,
10259
10645
  agentId: await defaultAgentFor(loop.agentId),
10260
10646
  guidance: loop.guidance,
10261
- cwd: mkdtempSync(join7(tmpdir(), "loopable-triage-"))
10647
+ cwd: mkdtempSync(join8(tmpdir(), "loopable-triage-"))
10262
10648
  });
10263
10649
  }
10264
10650
  for (const signal of found) {