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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/_connectorId-CDCuSgpN.js +1 -0
- package/.output/public/assets/_loopId-DOnIsVTW.js +1 -0
- package/.output/public/assets/_taskId-BO5MZnlZ.js +1 -0
- package/.output/public/assets/agents-CBozi7oi.js +1 -0
- package/.output/public/assets/alert-Bn_wjgTX.js +1 -0
- package/.output/public/assets/button-K3ypIhAZ.js +1 -0
- package/.output/public/assets/card-DiVnaTIE.js +1 -0
- package/.output/public/assets/chevron-right-BrctLtFK.js +1 -0
- package/.output/public/assets/connection-status-DKWelvLO.js +1 -0
- package/.output/public/assets/connector-icon-CEbdE71G.js +1 -0
- package/.output/public/assets/connectors-JyX3IzJj.js +1 -0
- package/.output/public/assets/gaps-PjmcbaWo.js +1 -0
- package/.output/public/assets/inbox-CujRR0Cz.js +1 -0
- package/.output/public/assets/index-C9SQ_Baq.js +16 -0
- package/.output/public/assets/loop-form-Dw3cuTzw.js +1 -0
- package/.output/public/assets/loops-DcEfZa2_.js +1 -0
- package/.output/public/assets/manifests-yJpevyye.js +1 -0
- package/.output/public/assets/new-CwsnNuRW.js +1 -0
- package/.output/public/assets/page-bvnfRMum.js +1 -0
- package/.output/public/assets/play-yHLs4DjK.js +1 -0
- package/.output/public/assets/plus-Ber25lrU.js +1 -0
- package/.output/public/assets/preload-helper-BoU_7EQC.js +3 -0
- package/.output/public/assets/refresh-cw-BzSY7M8I.js +1 -0
- package/.output/public/assets/rotate-ccw-DLv9EfCX.js +1 -0
- package/.output/public/assets/runners-DzX-RU3i.js +2 -0
- package/.output/public/assets/select-C9grIFwG.js +1 -0
- package/.output/public/assets/setting-fields-DUc3npgh.js +3 -0
- package/.output/public/assets/styles-B9B98zLk.css +2 -0
- package/.output/public/assets/switch-DwABVI6d.js +1 -0
- package/.output/public/assets/task-list-Ddk8-qt7.js +1 -0
- package/.output/public/assets/textarea-Ds-gIYUQ.js +1 -0
- package/.output/public/assets/time-goBWd-5J.js +1 -0
- package/.output/public/assets/use-live-tasks-DZNNuECy.js +1 -0
- package/.output/public/assets/useCompositeListItem-P4Nmmn7j.js +1 -0
- package/.output/public/assets/usePositioner-B7i9Ejzh.js +1 -0
- package/.output/server/_chunks/ssr-renderer.mjs +1 -1
- package/.output/server/_connectorId-COuQN02a.mjs +512 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +1410 -804
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
- package/.output/server/_libs/lucide-react.mjs +93 -84
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_loopId-CZh6_Yhr.mjs +310 -0
- package/.output/server/_ssr/{callback-D20cVD2w.mjs → access-DTttr563.mjs} +37 -20
- package/.output/server/_ssr/{agents-CrUmIQpi.mjs → agents-B_qo1gpL.mjs} +196 -7
- package/.output/server/_ssr/{agents-Dms_6TBF.mjs → agents-Bz10gz_m.mjs} +8 -8
- package/.output/server/_ssr/agents-CjbOUp3Z.mjs +206 -0
- package/.output/server/_ssr/{alert-DQvzE7bu.mjs → alert-C2WBWfzo.mjs} +2 -2
- package/.output/server/_ssr/card-r1cV-PCN.mjs +36 -0
- package/.output/server/_ssr/{client-BtVRVhQ2.mjs → client-Dk3SD8Il.mjs} +5 -1
- package/.output/server/_ssr/connection-status-BogAL-Fj.mjs +32 -0
- package/.output/server/_ssr/{connections-ypex3qUd.mjs → connections-Cqvb6Gw2.mjs} +1109 -263
- package/.output/server/_ssr/connector-icon-DrhVbhs6.mjs +190 -0
- package/.output/server/_ssr/connectors-BfS5tQKr.mjs +81 -0
- package/.output/server/_ssr/{connectors-BZly0yGF.mjs → connectors-DJI-rzPU.mjs} +21 -10
- package/.output/server/_ssr/{createServerRpc-DPDrnFdH.mjs → createServerRpc-DDrp2nLT.mjs} +2 -2
- package/.output/server/_ssr/{gaps-D2Zh8sEo.mjs → gaps-CcvHiLcj.mjs} +18 -10
- package/.output/server/_ssr/inbox-D1ef761v.mjs +178 -0
- package/.output/server/_ssr/loop-form-DWwOR05R.mjs +346 -0
- package/.output/server/_ssr/{loops-Og_G3KyZ.mjs → loops-6WhUe_lc.mjs} +228 -25
- package/.output/server/_ssr/loops-DHPjzG-t.mjs +217 -0
- package/.output/server/_ssr/new-ChKBdcXJ.mjs +245 -0
- package/.output/server/_ssr/page-DZVxKvdn.mjs +120 -0
- package/.output/server/_ssr/{router-BbrBz36P.mjs → router-DdwREiOf.mjs} +167 -489
- package/.output/server/_ssr/runners-CK9Cx9fS.mjs +217 -0
- package/.output/server/_ssr/runners-CrznFPHt.mjs +140 -0
- package/.output/server/_ssr/runners-W-pgo5yo.mjs +55 -0
- package/.output/server/_ssr/{secrets-CRnpwGBh.mjs → secrets-CV5scFlt.mjs} +2 -2
- package/.output/server/_ssr/select-BtndHhHb.mjs +76 -0
- package/.output/server/_ssr/{setting-fields-mpXfGftI.mjs → setting-fields-LNgywAW7.mjs} +25 -20
- package/.output/server/_ssr/ssr.mjs +49 -40
- package/.output/server/_ssr/{start-BgTj-h_C.mjs → start-C_j-e4vF.mjs} +10 -6
- package/.output/server/_ssr/{switch-1pnd_bPJ.mjs → switch-odxNO_S4.mjs} +3 -3
- package/.output/server/_ssr/task-list-Dnmq_WeE.mjs +150 -0
- package/.output/server/_ssr/{tasks-LIi-W4cy.mjs → tasks-D-kXLgVP.mjs} +13 -13
- package/.output/server/_ssr/{tasks-B_QmDFVs.mjs → tasks-D7aUvA33.mjs} +55 -17
- package/.output/server/_ssr/{textarea-CHJTKtsJ.mjs → textarea-D2c_vTIC.mjs} +2 -2
- package/.output/server/_ssr/time-Cy8TZP0Z.mjs +13 -0
- package/.output/server/_ssr/use-live-tasks-BzhTkEOL.mjs +40 -0
- package/.output/server/_tanstack-start-manifest_v-2HUdn-2E.mjs +166 -0
- package/.output/server/_taskId-D3KkNJW1.mjs +263 -0
- package/.output/server/index.mjs +204 -162
- package/README.md +54 -19
- package/dist/cli.mjs +185 -67
- package/dist/dispatcher.mjs +1443 -277
- package/dist/runner.mjs +333 -46
- package/drizzle/0025_prompt_tasks_done.sql +2 -0
- package/drizzle/0026_guidance_into_prompt.sql +3 -0
- package/drizzle/0027_relative_folders.sql +5 -0
- package/drizzle/0028_runner_version.sql +5 -0
- package/drizzle/meta/_journal.json +29 -1
- package/package.json +23 -17
- package/.output/public/assets/_connectorId-m67WbiJ8.js +0 -1
- package/.output/public/assets/_loopId-Bf4K5sdd.js +0 -1
- package/.output/public/assets/_taskId-eCKkcVOx.js +0 -1
- package/.output/public/assets/alert-BT2EVMJP.js +0 -1
- package/.output/public/assets/arrow-left-CWUZt6k2.js +0 -1
- package/.output/public/assets/arrow-right-dy_oOg_c.js +0 -1
- package/.output/public/assets/connection-status-DCqqmY_6.js +0 -1
- package/.output/public/assets/connector-icon-qGpNsPgH.js +0 -1
- package/.output/public/assets/connectors-DxXlc9zF.js +0 -1
- package/.output/public/assets/external-link-DC11orFA.js +0 -1
- package/.output/public/assets/gaps-CoTA-PzA.js +0 -1
- package/.output/public/assets/inbox-CLEXUXCI.js +0 -1
- package/.output/public/assets/index-Cndsw84p.js +0 -16
- package/.output/public/assets/loader-B4Epchm_.js +0 -1
- package/.output/public/assets/loop-form-BvizlZdR.js +0 -1
- package/.output/public/assets/loops-Dbe_mf5T.js +0 -1
- package/.output/public/assets/new-DPEaXKAY.js +0 -1
- package/.output/public/assets/play-DD4xQgvi.js +0 -1
- package/.output/public/assets/preload-helper-CfdAYKD7.js +0 -3
- package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +0 -1
- package/.output/public/assets/runners-BVBggS5B.js +0 -1
- package/.output/public/assets/select-Cppu-hti.js +0 -1
- package/.output/public/assets/setting-fields-BT1naVJX.js +0 -3
- package/.output/public/assets/styles-D3sonCfX.css +0 -2
- package/.output/public/assets/switch-hRIbNGOj.js +0 -1
- package/.output/public/assets/task-list-CrptnI1N.js +0 -1
- package/.output/public/assets/textarea-Fyuzi6Vp.js +0 -1
- package/.output/public/assets/trash-2-BXZzBsoV.js +0 -1
- package/.output/public/assets/useValueChanged-VVyq4hb9.js +0 -1
- package/.output/server/_connectorId-DcSnoRjk.mjs +0 -421
- package/.output/server/_loopId-G_TAkYtS.mjs +0 -272
- package/.output/server/_ssr/access-B7TcOZ7F.mjs +0 -42
- package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +0 -31
- package/.output/server/_ssr/connector-icon-CFrrHjzR.mjs +0 -37
- package/.output/server/_ssr/connectors-4yzKb0KG.mjs +0 -106
- package/.output/server/_ssr/inbox-C2VBGLa5.mjs +0 -114
- package/.output/server/_ssr/loop-form-D-U-kU4k.mjs +0 -413
- package/.output/server/_ssr/loops-D0XJ8eBb.mjs +0 -222
- package/.output/server/_ssr/new-CajvgIog.mjs +0 -169
- package/.output/server/_ssr/runners-B_WuF8UJ.mjs +0 -32
- package/.output/server/_ssr/runners-D-0_lfiq.mjs +0 -106
- package/.output/server/_ssr/runners-zqF_zbEc.mjs +0 -116
- package/.output/server/_ssr/task-list-BnQJ94Dk.mjs +0 -187
- package/.output/server/_tanstack-start-manifest_v-BCHAazE8.mjs +0 -129
- package/.output/server/_taskId-CIv0ajIx.mjs +0 -256
package/dist/dispatcher.mjs
CHANGED
|
@@ -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
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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) =>
|
|
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 =
|
|
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,
|
|
311
|
+
async function probe(bin, args4, timeoutMs = 1e4) {
|
|
152
312
|
try {
|
|
153
|
-
const { stdout, stderr } = await exec(bin,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
669
|
+
return { bin: codexManifest.binaries[0], args: args2(input) };
|
|
353
670
|
},
|
|
354
671
|
async run(input) {
|
|
355
|
-
const bin = await
|
|
356
|
-
const argv =
|
|
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
|
|
718
|
+
function parse2(line) {
|
|
402
719
|
const trimmed = line.trim();
|
|
403
720
|
if (!trimmed.startsWith("{")) return null;
|
|
404
721
|
try {
|
|
@@ -407,27 +724,27 @@ function parse(line) {
|
|
|
407
724
|
return null;
|
|
408
725
|
}
|
|
409
726
|
}
|
|
410
|
-
function
|
|
727
|
+
function textOf3(event) {
|
|
411
728
|
return (event.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
|
|
412
729
|
}
|
|
413
|
-
function
|
|
414
|
-
const [name, body] = Object.entries(
|
|
730
|
+
function describeTool2(call3, cwd) {
|
|
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
|
|
418
|
-
const subject =
|
|
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
|
|
424
|
-
const event =
|
|
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 ${
|
|
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,32 +757,32 @@ function describeEvent(line, cwd) {
|
|
|
440
757
|
return null;
|
|
441
758
|
}
|
|
442
759
|
}
|
|
443
|
-
function
|
|
444
|
-
const events = stdout.split("\n").map(
|
|
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(
|
|
448
765
|
(event) => event?.type === "assistant" && event.timestamp_ms === void 0
|
|
449
766
|
);
|
|
450
|
-
if (whole >= 0) return { text:
|
|
767
|
+
if (whole >= 0) return { text: textOf3(events[whole]).trim(), failed };
|
|
451
768
|
const lastTool = events.findLastIndex((event) => event?.type === "tool_call");
|
|
452
|
-
const partial = events.slice(lastTool + 1).filter((event) => event?.type === "assistant").map((event) =>
|
|
769
|
+
const partial = events.slice(lastTool + 1).filter((event) => event?.type === "assistant").map((event) => textOf3(event));
|
|
453
770
|
if (partial.length > 0) return { text: partial.join("").trim(), failed };
|
|
454
771
|
return { text: (result?.result ?? "").trim(), failed };
|
|
455
772
|
}
|
|
456
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
500
|
-
|
|
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:
|
|
836
|
+
return { bin: cursorAgentManifest.binaries[0], args: args3(input) };
|
|
520
837
|
},
|
|
521
838
|
async run(input) {
|
|
522
|
-
const bin = await
|
|
523
|
-
const argv =
|
|
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) =>
|
|
848
|
+
logLine: (line) => describeEvent2(line, input.cwd)
|
|
532
849
|
});
|
|
533
850
|
const command = `${bin} ${argv.join(" ")}`;
|
|
534
|
-
const answer =
|
|
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
|
|
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, ...
|
|
827
|
-
return fn(...
|
|
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
|
|
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 =
|
|
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.
|
|
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: "
|
|
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: "
|
|
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
|
|
2544
|
-
"
|
|
2862
|
+
var REPLY_PROMPT = [
|
|
2863
|
+
"Answer this issue in a comment, the way a teammate who knows this codebase would.",
|
|
2545
2864
|
"",
|
|
2546
|
-
"
|
|
2547
|
-
"
|
|
2548
|
-
"
|
|
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.",
|
|
2549
2869
|
"",
|
|
2550
|
-
"
|
|
2551
|
-
|
|
2552
|
-
var IMPLEMENT_PROMPT = [
|
|
2553
|
-
"Implement this issue in the checkout you are in.",
|
|
2554
|
-
"",
|
|
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.",
|
|
2558
|
-
"",
|
|
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
|
|
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
|
-
|
|
2890
|
+
triggers: [
|
|
2581
2891
|
{
|
|
2582
2892
|
id: "github.review_requested",
|
|
2583
|
-
|
|
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: "
|
|
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
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
-
//
|
|
2668
|
-
// as, so a second
|
|
2669
|
-
// first. Connections are still rows, so this
|
|
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 = [
|
|
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: "
|
|
2680
|
-
help: "Gmail search terms,
|
|
2681
|
-
placeholder: "from:
|
|
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
|
|
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
|
|
2690
|
-
{ value: "10", label: "10
|
|
2691
|
-
{ value: "25", label: "25
|
|
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
|
|
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: "
|
|
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
|
-
|
|
3018
|
+
triggers: [
|
|
2738
3019
|
{
|
|
2739
3020
|
id: "gmail.new_mail",
|
|
2740
|
-
|
|
2741
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
//
|
|
2760
|
-
|
|
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
|
-
//
|
|
2774
|
-
//
|
|
2775
|
-
|
|
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: "
|
|
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: "
|
|
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,42 +3176,203 @@ var scheduleManifest = defineManifest({
|
|
|
2907
3176
|
allowsMultipleAccounts: false,
|
|
2908
3177
|
byHand: { kind: "now" },
|
|
2909
3178
|
settings: [],
|
|
3179
|
+
triggers: [
|
|
3180
|
+
{
|
|
3181
|
+
id: "schedule.recurring",
|
|
3182
|
+
when: "On a schedule",
|
|
3183
|
+
trigger: "the time you set comes round",
|
|
3184
|
+
// No query to show. A clock is not something one can be written for,
|
|
3185
|
+
// and an invented one shown as if it were real is worse than nothing.
|
|
3186
|
+
runsIn: "folder",
|
|
3187
|
+
settings: [every, at, weekday],
|
|
3188
|
+
answer: "text",
|
|
3189
|
+
actionId: "schedule.record"
|
|
3190
|
+
}
|
|
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
|
+
],
|
|
3201
|
+
actions: [
|
|
3202
|
+
{
|
|
3203
|
+
id: "schedule.record",
|
|
3204
|
+
name: "Keep in Inbox",
|
|
3205
|
+
summary: "Keeps the answer in Inbox and sends it nowhere.",
|
|
3206
|
+
target: [],
|
|
3207
|
+
accepts: ["text", "review"]
|
|
3208
|
+
}
|
|
3209
|
+
]
|
|
3210
|
+
});
|
|
3211
|
+
|
|
3212
|
+
// src/connectors/slack/manifest.ts
|
|
3213
|
+
var channel = {
|
|
3214
|
+
key: "channel",
|
|
3215
|
+
kind: "text",
|
|
3216
|
+
label: "Channel",
|
|
3217
|
+
help: "Empty replies where it was asked. Or a channel ID.",
|
|
3218
|
+
placeholder: "C0123456789"
|
|
3219
|
+
};
|
|
3220
|
+
var ASK_PROMPT = [
|
|
3221
|
+
"Someone has sent you a message in Slack. Do what it asks, working in this directory,",
|
|
3222
|
+
"and then answer them.",
|
|
3223
|
+
"",
|
|
3224
|
+
"You are answering into Slack. Be brief: a couple of short paragraphs at most,",
|
|
3225
|
+
"no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
|
|
3226
|
+
"",
|
|
3227
|
+
"If the message asks a question about this code, read enough of it to answer",
|
|
3228
|
+
"properly rather than guessing. If it asks for a change, make the change and",
|
|
3229
|
+
"say in one line what you did. If the message is too vague to act on, say what",
|
|
3230
|
+
"you need to know instead of assuming."
|
|
3231
|
+
].join("\n");
|
|
3232
|
+
var slackManifest = defineManifest({
|
|
3233
|
+
id: "slack",
|
|
3234
|
+
name: "Slack bot",
|
|
3235
|
+
tagline: "Paste a bot token from a Slack app \u2014 not a Slack login. @mention or DM the bot, and it answers in the thread.",
|
|
3236
|
+
docsUrl: "https://api.slack.com/apps",
|
|
3237
|
+
icon: "Slack",
|
|
3238
|
+
accent: "bg-[#4a154b] text-white",
|
|
3239
|
+
auth: {
|
|
3240
|
+
kind: "token",
|
|
3241
|
+
helpUrl: "https://api.slack.com/apps",
|
|
3242
|
+
note: "This is a Slack app in the workspace, not a Slack account sign-in. Create an app at api.slack.com/apps, add bot scopes (channels:history, channels:read, groups:history, groups:read, im:history, im:read, mpim:history, mpim:read, chat:write), Install to Workspace, then paste the Bot User OAuth Token. Invite the bot to any channel it should watch.",
|
|
3243
|
+
fields: [
|
|
3244
|
+
{
|
|
3245
|
+
key: "botToken",
|
|
3246
|
+
label: "Bot User OAuth Token",
|
|
3247
|
+
placeholder: "xoxb-...",
|
|
3248
|
+
secret: true
|
|
3249
|
+
}
|
|
3250
|
+
]
|
|
3251
|
+
},
|
|
3252
|
+
triggers: [
|
|
3253
|
+
{
|
|
3254
|
+
id: "slack.ask",
|
|
3255
|
+
when: "The Slack bot is mentioned",
|
|
3256
|
+
trigger: "someone @mentions the Slack bot in a channel it is in, or DMs it",
|
|
3257
|
+
runsIn: "folder",
|
|
3258
|
+
settings: [],
|
|
3259
|
+
answer: "text",
|
|
3260
|
+
actionId: "slack.reply",
|
|
3261
|
+
pollEveryMs: 6e4
|
|
3262
|
+
}
|
|
3263
|
+
],
|
|
2910
3264
|
workflows: [
|
|
2911
3265
|
{
|
|
2912
|
-
id: "
|
|
2913
|
-
name: "Do
|
|
2914
|
-
summary: "Runs
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
+
],
|
|
3273
|
+
actions: [
|
|
3274
|
+
{
|
|
3275
|
+
id: "slack.reply",
|
|
3276
|
+
name: "Reply in Slack",
|
|
3277
|
+
summary: "Post a message back to the thread that asked, or to a channel you name.",
|
|
3278
|
+
target: [channel],
|
|
3279
|
+
accepts: ["text"]
|
|
3280
|
+
}
|
|
3281
|
+
],
|
|
3282
|
+
settings: [],
|
|
3283
|
+
allowsMultipleAccounts: false,
|
|
3284
|
+
byHand: { kind: "none" }
|
|
3285
|
+
});
|
|
3286
|
+
|
|
3287
|
+
// src/connectors/feishu/manifest.ts
|
|
3288
|
+
var chat = {
|
|
3289
|
+
key: "chat",
|
|
3290
|
+
kind: "text",
|
|
3291
|
+
label: "Chat ID",
|
|
3292
|
+
help: "Empty replies where it was asked. Or a chat ID (oc_\u2026).",
|
|
3293
|
+
placeholder: "oc_..."
|
|
3294
|
+
};
|
|
3295
|
+
var ASK_PROMPT2 = [
|
|
3296
|
+
"Someone has sent you a message in Feishu. Do what it asks, working in this directory,",
|
|
3297
|
+
"and then answer them.",
|
|
3298
|
+
"",
|
|
3299
|
+
"You are answering into Feishu. Be brief: a couple of short paragraphs at most,",
|
|
3300
|
+
"no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
|
|
3301
|
+
"",
|
|
3302
|
+
"If the message asks a question about this code, read enough of it to answer",
|
|
3303
|
+
"properly rather than guessing. If it asks for a change, make the change and",
|
|
3304
|
+
"say in one line what you did. If the message is too vague to act on, say what",
|
|
3305
|
+
"you need to know instead of assuming."
|
|
3306
|
+
].join("\n");
|
|
3307
|
+
var feishuManifest = defineManifest({
|
|
3308
|
+
id: "feishu",
|
|
3309
|
+
name: "Feishu / Lark bot",
|
|
3310
|
+
tagline: "Paste App ID and App Secret from a Feishu or Lark app \u2014 not a Feishu login. @mention the bot in a group, and it answers in the thread.",
|
|
3311
|
+
docsUrl: "https://open.feishu.cn/app",
|
|
3312
|
+
icon: "Feishu",
|
|
3313
|
+
accent: "bg-[#3370ff] text-white",
|
|
3314
|
+
auth: {
|
|
3315
|
+
kind: "token",
|
|
3316
|
+
helpUrl: "https://open.feishu.cn/app",
|
|
3317
|
+
note: "This is a custom Feishu or Lark app, not a Feishu account sign-in. Create an app, enable Bot, add permissions (im:chat:readonly, im:message, im:message.group_msg, im:message:send_as_bot), create a version and publish it, then paste App ID and App Secret. Invite the bot to any group it should watch. Put lark in Open platform for Lark (international).",
|
|
3318
|
+
fields: [
|
|
3319
|
+
{
|
|
3320
|
+
key: "appId",
|
|
3321
|
+
label: "App ID",
|
|
3322
|
+
placeholder: "cli_...",
|
|
3323
|
+
secret: false
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
key: "appSecret",
|
|
3327
|
+
label: "App Secret",
|
|
3328
|
+
placeholder: "App Secret",
|
|
3329
|
+
secret: true
|
|
3330
|
+
},
|
|
3331
|
+
{
|
|
3332
|
+
key: "host",
|
|
3333
|
+
label: "Open platform",
|
|
3334
|
+
placeholder: "feishu or lark",
|
|
3335
|
+
secret: false,
|
|
3336
|
+
optional: true
|
|
3337
|
+
}
|
|
3338
|
+
]
|
|
3339
|
+
},
|
|
3340
|
+
triggers: [
|
|
3341
|
+
{
|
|
3342
|
+
id: "feishu.ask",
|
|
3343
|
+
when: "The Feishu / Lark bot is mentioned",
|
|
3344
|
+
trigger: "someone @mentions the Feishu / Lark bot in a group it is in",
|
|
2919
3345
|
runsIn: "folder",
|
|
2920
|
-
settings: [
|
|
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.",
|
|
3346
|
+
settings: [],
|
|
2923
3347
|
answer: "text",
|
|
2924
|
-
actionId: "
|
|
3348
|
+
actionId: "feishu.reply",
|
|
3349
|
+
pollEveryMs: 6e4
|
|
3350
|
+
}
|
|
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
|
|
2925
3359
|
}
|
|
2926
3360
|
],
|
|
2927
3361
|
actions: [
|
|
2928
3362
|
{
|
|
2929
|
-
id: "
|
|
2930
|
-
name: "
|
|
2931
|
-
summary: "
|
|
2932
|
-
target: [],
|
|
2933
|
-
accepts: ["text"
|
|
3363
|
+
id: "feishu.reply",
|
|
3364
|
+
name: "Reply in Feishu / Lark",
|
|
3365
|
+
summary: "Post a message back to the thread that asked, or to a chat you name.",
|
|
3366
|
+
target: [chat],
|
|
3367
|
+
accepts: ["text"]
|
|
2934
3368
|
}
|
|
2935
|
-
]
|
|
3369
|
+
],
|
|
3370
|
+
settings: [],
|
|
3371
|
+
allowsMultipleAccounts: false,
|
|
3372
|
+
byHand: { kind: "none" }
|
|
2936
3373
|
});
|
|
2937
3374
|
|
|
2938
3375
|
// src/connectors/wechat/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
3376
|
var askers = {
|
|
2947
3377
|
key: "askers",
|
|
2948
3378
|
kind: "select",
|
|
@@ -2957,14 +3387,14 @@ var recipient = {
|
|
|
2957
3387
|
key: "to",
|
|
2958
3388
|
kind: "select",
|
|
2959
3389
|
label: "Who to send it to",
|
|
2960
|
-
help: "
|
|
3390
|
+
help: '"Me" only arrives if you messaged the bot in the last half day.',
|
|
2961
3391
|
options: [
|
|
2962
3392
|
{ value: "source", label: "Whoever asked" },
|
|
2963
3393
|
{ value: "me", label: "Me" }
|
|
2964
3394
|
],
|
|
2965
3395
|
default: "source"
|
|
2966
3396
|
};
|
|
2967
|
-
var
|
|
3397
|
+
var ASK_PROMPT3 = [
|
|
2968
3398
|
"Someone has sent you a message. Do what it asks, working in this directory,",
|
|
2969
3399
|
"and then answer them.",
|
|
2970
3400
|
"",
|
|
@@ -2988,23 +3418,28 @@ var wechatManifest = {
|
|
|
2988
3418
|
kind: "qr_scan",
|
|
2989
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."
|
|
2990
3420
|
},
|
|
2991
|
-
|
|
3421
|
+
triggers: [
|
|
2992
3422
|
{
|
|
2993
3423
|
id: "wechat.ask",
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
trigger: "you send the bot a message",
|
|
3424
|
+
when: "The WeChat bot gets a message",
|
|
3425
|
+
trigger: "someone sends the WeChat bot a message",
|
|
2997
3426
|
// No query to show: messages arrive on a stream rather than being found
|
|
2998
3427
|
// by asking, so there is nothing here that could be checked.
|
|
2999
|
-
writes: "a reply in the chat",
|
|
3000
3428
|
runsIn: "folder",
|
|
3001
|
-
settings: [
|
|
3002
|
-
prompt: ASK_PROMPT,
|
|
3003
|
-
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],
|
|
3004
3430
|
answer: "text",
|
|
3005
3431
|
actionId: "wechat.reply"
|
|
3006
3432
|
}
|
|
3007
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
|
+
],
|
|
3008
3443
|
actions: [
|
|
3009
3444
|
{
|
|
3010
3445
|
id: "wechat.reply",
|
|
@@ -3027,6 +3462,8 @@ var wechatManifest = {
|
|
|
3027
3462
|
// src/connectors/manifests.ts
|
|
3028
3463
|
var CONNECTOR_MANIFESTS = [
|
|
3029
3464
|
githubManifest,
|
|
3465
|
+
slackManifest,
|
|
3466
|
+
feishuManifest,
|
|
3030
3467
|
gmailManifest,
|
|
3031
3468
|
wechatManifest,
|
|
3032
3469
|
scheduleManifest
|
|
@@ -3037,8 +3474,36 @@ function connectorManifest(id) {
|
|
|
3037
3474
|
function needsAccount(id) {
|
|
3038
3475
|
return connectorManifest(id)?.auth.kind !== "none";
|
|
3039
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
|
+
}
|
|
3040
3487
|
function connectorWorkflow(connectorId, workflowId) {
|
|
3041
|
-
|
|
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;
|
|
3042
3507
|
}
|
|
3043
3508
|
function connectorAction(connectorId, actionId) {
|
|
3044
3509
|
return connectorManifest(connectorId)?.actions.find((entry) => entry.id === actionId);
|
|
@@ -3445,10 +3910,10 @@ function candidates(items, repositories2) {
|
|
|
3445
3910
|
return repo && repoMatches(repo, repositories2) ? [{ repo, item }] : [];
|
|
3446
3911
|
});
|
|
3447
3912
|
}
|
|
3448
|
-
function query(
|
|
3449
|
-
const
|
|
3450
|
-
if (!
|
|
3451
|
-
return
|
|
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;
|
|
3452
3917
|
}
|
|
3453
3918
|
async function pollGithub(input) {
|
|
3454
3919
|
const repositories2 = stringList(input.settings.repositories);
|
|
@@ -3724,11 +4189,17 @@ var githubRuntime = defineRuntime({
|
|
|
3724
4189
|
|
|
3725
4190
|
// src/connectors/gmail/api.ts
|
|
3726
4191
|
var BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
3727
|
-
async function request2(accessToken, path) {
|
|
4192
|
+
async function request2(accessToken, path, init = {}) {
|
|
3728
4193
|
let res;
|
|
3729
4194
|
try {
|
|
3730
4195
|
res = await fetch(`${BASE}${path}`, {
|
|
3731
|
-
|
|
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) }
|
|
3732
4203
|
});
|
|
3733
4204
|
} catch (error) {
|
|
3734
4205
|
throw new TransientError(
|
|
@@ -3740,6 +4211,9 @@ async function request2(accessToken, path) {
|
|
|
3740
4211
|
const message = `Gmail ${res.status} on ${path}: ${detail}`;
|
|
3741
4212
|
const backoff = res.status === 403 && /rateLimitExceeded|userRateLimitExceeded/i.test(detail);
|
|
3742
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
|
+
}
|
|
3743
4217
|
throw new Error(message);
|
|
3744
4218
|
}
|
|
3745
4219
|
return await res.json();
|
|
@@ -3759,6 +4233,12 @@ function getMessageHeaders(accessToken, id) {
|
|
|
3759
4233
|
function getMessage(accessToken, id) {
|
|
3760
4234
|
return request2(accessToken, `/messages/${id}?format=full`);
|
|
3761
4235
|
}
|
|
4236
|
+
function sendMessage(accessToken, raw, threadId) {
|
|
4237
|
+
return request2(accessToken, "/messages/send", {
|
|
4238
|
+
method: "POST",
|
|
4239
|
+
body: threadId ? { raw, threadId } : { raw }
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
3762
4242
|
|
|
3763
4243
|
// src/connectors/gmail/mail.ts
|
|
3764
4244
|
function mailRef(id) {
|
|
@@ -3848,12 +4328,31 @@ function mailContext(message) {
|
|
|
3848
4328
|
].join("\n")
|
|
3849
4329
|
};
|
|
3850
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
|
+
}
|
|
3851
4350
|
|
|
3852
4351
|
// src/connectors/gmail/poll.ts
|
|
3853
4352
|
function baseQuery(workflowId) {
|
|
3854
|
-
const
|
|
3855
|
-
if (!
|
|
3856
|
-
return
|
|
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;
|
|
3857
4356
|
}
|
|
3858
4357
|
function boolean2(value, fallback) {
|
|
3859
4358
|
return typeof value === "boolean" ? value : fallback;
|
|
@@ -4068,8 +4567,33 @@ var gmailRuntime = defineRuntime({
|
|
|
4068
4567
|
})
|
|
4069
4568
|
};
|
|
4070
4569
|
},
|
|
4071
|
-
|
|
4072
|
-
|
|
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
|
+
},
|
|
4073
4597
|
auth: {
|
|
4074
4598
|
async startAuthorization({ redirectUri, state }) {
|
|
4075
4599
|
const registration = requireRegistration2();
|
|
@@ -4203,6 +4727,618 @@ var scheduleRuntime = {
|
|
|
4203
4727
|
}
|
|
4204
4728
|
};
|
|
4205
4729
|
|
|
4730
|
+
// src/connectors/slack/api.ts
|
|
4731
|
+
var API = "https://slack.com/api";
|
|
4732
|
+
async function slack(path, token, init = {}) {
|
|
4733
|
+
const url = new URL(`${API}/${path}`);
|
|
4734
|
+
for (const [key, value] of Object.entries(init.query ?? {})) {
|
|
4735
|
+
url.searchParams.set(key, value);
|
|
4736
|
+
}
|
|
4737
|
+
let response;
|
|
4738
|
+
try {
|
|
4739
|
+
response = await fetch(url.href, {
|
|
4740
|
+
method: init.method ?? "GET",
|
|
4741
|
+
headers: {
|
|
4742
|
+
Authorization: `Bearer ${token}`,
|
|
4743
|
+
...init.body ? { "Content-Type": "application/json" } : {}
|
|
4744
|
+
},
|
|
4745
|
+
body: init.body ? JSON.stringify(init.body) : void 0
|
|
4746
|
+
});
|
|
4747
|
+
} catch (error) {
|
|
4748
|
+
throw new TransientError(`Slack is unreachable: ${error.message}`);
|
|
4749
|
+
}
|
|
4750
|
+
if (response.status >= 500 || response.status === 429) {
|
|
4751
|
+
throw new TransientError(`Slack answered ${response.status}`);
|
|
4752
|
+
}
|
|
4753
|
+
if (!response.ok) throw new Error(`Slack answered ${response.status}`);
|
|
4754
|
+
return await response.json();
|
|
4755
|
+
}
|
|
4756
|
+
function refuse(error) {
|
|
4757
|
+
if (error === "invalid_auth" || error === "not_authed" || error === "token_revoked" || error === "account_inactive") {
|
|
4758
|
+
throw new Error("Slack did not accept that token.");
|
|
4759
|
+
}
|
|
4760
|
+
throw new Error(error ? `Slack refused: ${error}` : "Slack refused the request.");
|
|
4761
|
+
}
|
|
4762
|
+
async function authTest(botToken) {
|
|
4763
|
+
const body = await slack("auth.test", botToken);
|
|
4764
|
+
if (!body.ok) refuse(body.error);
|
|
4765
|
+
const teamUrl = typeof body.url === "string" ? body.url : "";
|
|
4766
|
+
const team = typeof body.team === "string" ? body.team : "";
|
|
4767
|
+
const teamId = typeof body.team_id === "string" ? body.team_id : "";
|
|
4768
|
+
const botUserId = typeof body.user_id === "string" ? body.user_id : "";
|
|
4769
|
+
const botId = typeof body.bot_id === "string" ? body.bot_id : "";
|
|
4770
|
+
const botName = typeof body.user === "string" ? body.user : "";
|
|
4771
|
+
if (!teamId || !botUserId) throw new Error("Slack did not return which workspace this bot belongs to.");
|
|
4772
|
+
return { botToken, teamId, team, teamUrl, botUserId, botId, botName };
|
|
4773
|
+
}
|
|
4774
|
+
async function listConversations(credential) {
|
|
4775
|
+
const body = await slack("conversations.list", credential.botToken, {
|
|
4776
|
+
query: {
|
|
4777
|
+
types: "public_channel,private_channel,im,mpim",
|
|
4778
|
+
exclude_archived: "true",
|
|
4779
|
+
limit: "200"
|
|
4780
|
+
}
|
|
4781
|
+
});
|
|
4782
|
+
if (!body.ok) refuse(body.error);
|
|
4783
|
+
const channels = Array.isArray(body.channels) ? body.channels : [];
|
|
4784
|
+
return channels.map((raw) => {
|
|
4785
|
+
const channel2 = raw;
|
|
4786
|
+
if (!channel2.id) return null;
|
|
4787
|
+
return { id: channel2.id, name: channel2.name ?? "", isIm: channel2.is_im === true };
|
|
4788
|
+
}).filter((channel2) => channel2 !== null);
|
|
4789
|
+
}
|
|
4790
|
+
async function history(credential, channelId, oldest) {
|
|
4791
|
+
const query2 = {
|
|
4792
|
+
channel: channelId,
|
|
4793
|
+
limit: "100",
|
|
4794
|
+
inclusive: "false"
|
|
4795
|
+
};
|
|
4796
|
+
if (oldest) query2.oldest = oldest;
|
|
4797
|
+
const body = await slack("conversations.history", credential.botToken, { query: query2 });
|
|
4798
|
+
if (!body.ok) {
|
|
4799
|
+
if (body.error === "not_in_channel" || body.error === "channel_not_found") return [];
|
|
4800
|
+
refuse(body.error);
|
|
4801
|
+
}
|
|
4802
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
4803
|
+
const parsed = [];
|
|
4804
|
+
for (const raw of messages) {
|
|
4805
|
+
const message = raw;
|
|
4806
|
+
if (!message.ts) continue;
|
|
4807
|
+
parsed.push({
|
|
4808
|
+
ts: message.ts,
|
|
4809
|
+
...message.user ? { user: message.user } : {},
|
|
4810
|
+
...message.bot_id ? { botId: message.bot_id } : {},
|
|
4811
|
+
text: message.text ?? "",
|
|
4812
|
+
...message.thread_ts ? { threadTs: message.thread_ts } : {}
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
return parsed.sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
4816
|
+
}
|
|
4817
|
+
async function postMessage(credential, input) {
|
|
4818
|
+
const body = await slack("chat.postMessage", credential.botToken, {
|
|
4819
|
+
method: "POST",
|
|
4820
|
+
body: {
|
|
4821
|
+
channel: input.channel,
|
|
4822
|
+
text: input.text,
|
|
4823
|
+
...input.threadTs ? { thread_ts: input.threadTs } : {}
|
|
4824
|
+
}
|
|
4825
|
+
});
|
|
4826
|
+
if (!body.ok) refuse(body.error);
|
|
4827
|
+
const ts = typeof body.ts === "string" ? body.ts : "";
|
|
4828
|
+
const channel2 = typeof body.channel === "string" ? body.channel : input.channel;
|
|
4829
|
+
if (!ts) throw new Error("Slack accepted the message but did not say where it landed.");
|
|
4830
|
+
return { ts, channel: channel2 };
|
|
4831
|
+
}
|
|
4832
|
+
function permalink2(credential, channel2, ts) {
|
|
4833
|
+
const host = credential.teamUrl.replace(/\/$/, "");
|
|
4834
|
+
return `${host}/archives/${channel2}/p${ts.replace(".", "")}`;
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
// src/connectors/slack/runtime.ts
|
|
4838
|
+
function asAsked(value) {
|
|
4839
|
+
const record = value;
|
|
4840
|
+
if (!record || typeof record.channel !== "string" || typeof record.ts !== "string") {
|
|
4841
|
+
throw new Error("This task has lost the message it was about.");
|
|
4842
|
+
}
|
|
4843
|
+
return {
|
|
4844
|
+
channel: record.channel,
|
|
4845
|
+
ts: record.ts,
|
|
4846
|
+
user: typeof record.user === "string" ? record.user : void 0,
|
|
4847
|
+
text: typeof record.text === "string" ? record.text : "",
|
|
4848
|
+
threadTs: typeof record.threadTs === "string" ? record.threadTs : record.ts
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
function asCursor(value) {
|
|
4852
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
4853
|
+
const out = {};
|
|
4854
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4855
|
+
if (typeof entry === "string") out[key] = entry;
|
|
4856
|
+
}
|
|
4857
|
+
return out;
|
|
4858
|
+
}
|
|
4859
|
+
function summarise2(text3) {
|
|
4860
|
+
const line = text3.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
4861
|
+
if (!line) return "Slack message";
|
|
4862
|
+
return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
4863
|
+
}
|
|
4864
|
+
function isAsk(input) {
|
|
4865
|
+
if (input.user === input.botUserId || input.messageBotId && input.messageBotId === input.botId) {
|
|
4866
|
+
return false;
|
|
4867
|
+
}
|
|
4868
|
+
if (input.isIm) return Boolean(input.text.trim());
|
|
4869
|
+
return input.text.includes(`<@${input.botUserId}>`);
|
|
4870
|
+
}
|
|
4871
|
+
function destination(target, source) {
|
|
4872
|
+
if (typeof target.channel === "string" && target.channel.trim()) {
|
|
4873
|
+
return { channel: target.channel.trim() };
|
|
4874
|
+
}
|
|
4875
|
+
if (source.connectorId !== "slack") {
|
|
4876
|
+
throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
|
|
4877
|
+
}
|
|
4878
|
+
const carry = source.carry ?? {};
|
|
4879
|
+
if (!carry.channel) throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
|
|
4880
|
+
return { channel: carry.channel, threadTs: carry.ts };
|
|
4881
|
+
}
|
|
4882
|
+
function accountOf2(credential) {
|
|
4883
|
+
return {
|
|
4884
|
+
id: 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 "),
|
|
4888
|
+
url: credential.teamUrl || void 0
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
var slackRuntime = {
|
|
4892
|
+
async readiness() {
|
|
4893
|
+
return { ready: true };
|
|
4894
|
+
},
|
|
4895
|
+
async poll({ workflowId, credential, cursor }) {
|
|
4896
|
+
if (workflowId !== "slack.ask") {
|
|
4897
|
+
throw new Error(`Slack cannot watch for ${workflowId}.`);
|
|
4898
|
+
}
|
|
4899
|
+
const account2 = credential;
|
|
4900
|
+
const previous = asCursor(cursor);
|
|
4901
|
+
const channels = await listConversations(account2);
|
|
4902
|
+
const next = { ...previous };
|
|
4903
|
+
const signals2 = [];
|
|
4904
|
+
for (const channel2 of channels) {
|
|
4905
|
+
const oldest = previous[channel2.id];
|
|
4906
|
+
const messages = await history(account2, channel2.id, oldest);
|
|
4907
|
+
if (messages.length === 0) {
|
|
4908
|
+
if (!next[channel2.id]) next[channel2.id] = String(Date.now() / 1e3);
|
|
4909
|
+
continue;
|
|
4910
|
+
}
|
|
4911
|
+
next[channel2.id] = messages[messages.length - 1].ts;
|
|
4912
|
+
if (!oldest) continue;
|
|
4913
|
+
for (const message of messages) {
|
|
4914
|
+
if (!isAsk({
|
|
4915
|
+
text: message.text,
|
|
4916
|
+
user: message.user,
|
|
4917
|
+
messageBotId: message.botId,
|
|
4918
|
+
isIm: channel2.isIm,
|
|
4919
|
+
botUserId: account2.botUserId,
|
|
4920
|
+
botId: account2.botId
|
|
4921
|
+
})) {
|
|
4922
|
+
continue;
|
|
4923
|
+
}
|
|
4924
|
+
const asked = {
|
|
4925
|
+
channel: channel2.id,
|
|
4926
|
+
ts: message.ts,
|
|
4927
|
+
user: message.user,
|
|
4928
|
+
text: message.text,
|
|
4929
|
+
threadTs: message.threadTs ?? message.ts
|
|
4930
|
+
};
|
|
4931
|
+
signals2.push({
|
|
4932
|
+
key: `${channel2.id}:${message.ts}`,
|
|
4933
|
+
kind: "message",
|
|
4934
|
+
ref: `${channel2.id}:${message.ts}`,
|
|
4935
|
+
title: summarise2(message.text),
|
|
4936
|
+
url: permalink2(account2, channel2.id, message.ts),
|
|
4937
|
+
payload: asked
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
return { signals: signals2, cursor: next };
|
|
4942
|
+
},
|
|
4943
|
+
async resolveWorkItem({ payload }) {
|
|
4944
|
+
const asked = asAsked(payload);
|
|
4945
|
+
return {
|
|
4946
|
+
kind: "message",
|
|
4947
|
+
ref: `${asked.channel}:${asked.ts}`,
|
|
4948
|
+
title: summarise2(asked.text),
|
|
4949
|
+
url: "",
|
|
4950
|
+
carry: { channel: asked.channel, ts: asked.threadTs },
|
|
4951
|
+
context: [
|
|
4952
|
+
{
|
|
4953
|
+
name: "MESSAGE.md",
|
|
4954
|
+
body: [`# What you were asked`, ``, asked.text, ``].join("\n")
|
|
4955
|
+
}
|
|
4956
|
+
]
|
|
4957
|
+
};
|
|
4958
|
+
},
|
|
4959
|
+
async applyAction({ actionId, target, source, body, credential }) {
|
|
4960
|
+
if (actionId !== "slack.reply") throw new Error(`Slack cannot ${actionId}.`);
|
|
4961
|
+
const account2 = credential;
|
|
4962
|
+
const to = destination(target, source);
|
|
4963
|
+
const posted = await postMessage(account2, {
|
|
4964
|
+
channel: to.channel,
|
|
4965
|
+
text: body.trim(),
|
|
4966
|
+
threadTs: to.threadTs
|
|
4967
|
+
});
|
|
4968
|
+
return { url: permalink2(account2, posted.channel, posted.ts) };
|
|
4969
|
+
},
|
|
4970
|
+
auth: {
|
|
4971
|
+
async connectWithFields(fields) {
|
|
4972
|
+
const botToken = fields.botToken?.trim() ?? "";
|
|
4973
|
+
if (!botToken.startsWith("xoxb-")) {
|
|
4974
|
+
throw new Error("Paste the Bot User OAuth Token from your Slack app. It starts with xoxb-.");
|
|
4975
|
+
}
|
|
4976
|
+
const credential = await authTest(botToken);
|
|
4977
|
+
return { credential, account: accountOf2(credential) };
|
|
4978
|
+
},
|
|
4979
|
+
async identity(credential) {
|
|
4980
|
+
const stored = credential;
|
|
4981
|
+
const fresh = await authTest(stored.botToken);
|
|
4982
|
+
return { account: accountOf2(fresh) };
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
};
|
|
4986
|
+
|
|
4987
|
+
// src/connectors/feishu/api.ts
|
|
4988
|
+
var API2 = {
|
|
4989
|
+
feishu: "https://open.feishu.cn/open-apis",
|
|
4990
|
+
lark: "https://open.larksuite.com/open-apis"
|
|
4991
|
+
};
|
|
4992
|
+
var APPLINK = {
|
|
4993
|
+
feishu: "https://applink.feishu.cn/client/chat/open",
|
|
4994
|
+
lark: "https://applink.larksuite.com/client/chat/open"
|
|
4995
|
+
};
|
|
4996
|
+
function parseHost(value) {
|
|
4997
|
+
const raw = (value ?? "feishu").trim().toLowerCase();
|
|
4998
|
+
if (!raw || raw === "feishu" || raw === "feishu.cn" || raw.includes("feishu.cn")) return "feishu";
|
|
4999
|
+
if (raw === "lark" || raw === "larksuite" || raw.includes("larksuite.com") || raw.includes("larkoffice.com")) {
|
|
5000
|
+
return "lark";
|
|
5001
|
+
}
|
|
5002
|
+
throw new Error("Open platform must be feishu (China) or lark (international).");
|
|
5003
|
+
}
|
|
5004
|
+
function refuse2(code, msg) {
|
|
5005
|
+
if (code === 10003 || code === 10012 || code === 10014) {
|
|
5006
|
+
throw new Error("Feishu did not accept those app credentials.");
|
|
5007
|
+
}
|
|
5008
|
+
throw new Error(msg ? `Feishu refused: ${msg}` : "Feishu refused the request.");
|
|
5009
|
+
}
|
|
5010
|
+
async function call(host, path, init = {}) {
|
|
5011
|
+
const url = new URL(`${API2[host]}${path}`);
|
|
5012
|
+
for (const [key, value] of Object.entries(init.query ?? {})) {
|
|
5013
|
+
url.searchParams.set(key, value);
|
|
5014
|
+
}
|
|
5015
|
+
let response;
|
|
5016
|
+
try {
|
|
5017
|
+
response = await fetch(url.href, {
|
|
5018
|
+
method: init.method ?? "GET",
|
|
5019
|
+
headers: {
|
|
5020
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
5021
|
+
...init.token ? { Authorization: `Bearer ${init.token}` } : {}
|
|
5022
|
+
},
|
|
5023
|
+
body: init.body ? JSON.stringify(init.body) : void 0
|
|
5024
|
+
});
|
|
5025
|
+
} catch (error) {
|
|
5026
|
+
throw new TransientError(`Feishu is unreachable: ${error.message}`);
|
|
5027
|
+
}
|
|
5028
|
+
if (response.status >= 500 || response.status === 429) {
|
|
5029
|
+
throw new TransientError(`Feishu answered ${response.status}`);
|
|
5030
|
+
}
|
|
5031
|
+
let body;
|
|
5032
|
+
try {
|
|
5033
|
+
body = await response.json();
|
|
5034
|
+
} catch {
|
|
5035
|
+
throw new Error(`Feishu answered ${response.status}`);
|
|
5036
|
+
}
|
|
5037
|
+
return body;
|
|
5038
|
+
}
|
|
5039
|
+
function dataOf(body) {
|
|
5040
|
+
if (body.code && body.code !== 0) refuse2(body.code, body.msg);
|
|
5041
|
+
return body.data ?? {};
|
|
5042
|
+
}
|
|
5043
|
+
async function tenantAccessToken(host, appId, appSecret) {
|
|
5044
|
+
const body = await call(host, "/auth/v3/tenant_access_token/internal", {
|
|
5045
|
+
method: "POST",
|
|
5046
|
+
body: { app_id: appId, app_secret: appSecret }
|
|
5047
|
+
});
|
|
5048
|
+
if (body.code && body.code !== 0) refuse2(body.code, body.msg);
|
|
5049
|
+
const token = body.tenant_access_token ?? "";
|
|
5050
|
+
const expire = typeof body.expire === "number" ? body.expire : 7200;
|
|
5051
|
+
if (!token) throw new Error("Feishu did not return a tenant token.");
|
|
5052
|
+
return { token, expiresAt: Date.now() + expire * 1e3 - 6e4 };
|
|
5053
|
+
}
|
|
5054
|
+
async function botInfo(credential) {
|
|
5055
|
+
const body = await call(credential.host, "/bot/v3/info", { token: credential.tenantAccessToken });
|
|
5056
|
+
if (body.code && body.code !== 0) refuse2(body.code, body.msg);
|
|
5057
|
+
const bot = body.bot ?? {};
|
|
5058
|
+
const openId = typeof bot.open_id === "string" ? bot.open_id : "";
|
|
5059
|
+
const name = typeof bot.app_name === "string" ? bot.app_name : "";
|
|
5060
|
+
const avatarUrl = typeof bot.avatar_url === "string" ? bot.avatar_url : void 0;
|
|
5061
|
+
if (!openId) throw new Error("Feishu did not return which bot this app is.");
|
|
5062
|
+
return { openId, name, ...avatarUrl ? { avatarUrl } : {} };
|
|
5063
|
+
}
|
|
5064
|
+
async function listChats(credential) {
|
|
5065
|
+
const chats = [];
|
|
5066
|
+
let pageToken = "";
|
|
5067
|
+
do {
|
|
5068
|
+
const query2 = { page_size: "100" };
|
|
5069
|
+
if (pageToken) query2.page_token = pageToken;
|
|
5070
|
+
const body = await call(credential.host, "/im/v1/chats", {
|
|
5071
|
+
token: credential.tenantAccessToken,
|
|
5072
|
+
query: query2
|
|
5073
|
+
});
|
|
5074
|
+
const data = dataOf(body);
|
|
5075
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
5076
|
+
for (const raw of items) {
|
|
5077
|
+
const chat2 = raw;
|
|
5078
|
+
if (!chat2.chat_id) continue;
|
|
5079
|
+
chats.push({ id: chat2.chat_id, name: chat2.name ?? "" });
|
|
5080
|
+
}
|
|
5081
|
+
pageToken = data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
|
|
5082
|
+
} while (pageToken);
|
|
5083
|
+
return chats;
|
|
5084
|
+
}
|
|
5085
|
+
function textOf(content) {
|
|
5086
|
+
if (!content) return "";
|
|
5087
|
+
try {
|
|
5088
|
+
const parsed = JSON.parse(content);
|
|
5089
|
+
return typeof parsed.text === "string" ? parsed.text : content;
|
|
5090
|
+
} catch {
|
|
5091
|
+
return content;
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
function mentionsOf(raw) {
|
|
5095
|
+
if (!Array.isArray(raw)) return [];
|
|
5096
|
+
return raw.map((entry) => {
|
|
5097
|
+
const mention = entry;
|
|
5098
|
+
return typeof mention.id === "string" ? mention.id : "";
|
|
5099
|
+
}).filter(Boolean);
|
|
5100
|
+
}
|
|
5101
|
+
async function history2(credential, chatId, oldestMs) {
|
|
5102
|
+
const parsed = [];
|
|
5103
|
+
let pageToken = "";
|
|
5104
|
+
do {
|
|
5105
|
+
const query2 = {
|
|
5106
|
+
container_id_type: "chat",
|
|
5107
|
+
container_id: chatId,
|
|
5108
|
+
sort_type: oldestMs ? "ByCreateTimeAsc" : "ByCreateTimeDesc",
|
|
5109
|
+
page_size: oldestMs ? "50" : "1"
|
|
5110
|
+
};
|
|
5111
|
+
if (oldestMs) query2.start_time = String(Math.floor(Number(oldestMs) / 1e3));
|
|
5112
|
+
if (pageToken) query2.page_token = pageToken;
|
|
5113
|
+
const body = await call(credential.host, "/im/v1/messages", {
|
|
5114
|
+
token: credential.tenantAccessToken,
|
|
5115
|
+
query: query2
|
|
5116
|
+
});
|
|
5117
|
+
if (body.code && body.code !== 0) {
|
|
5118
|
+
if (body.code === 230002 || body.code === 231203) return parsed;
|
|
5119
|
+
refuse2(body.code, body.msg);
|
|
5120
|
+
}
|
|
5121
|
+
const data = body.data ?? {};
|
|
5122
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
5123
|
+
for (const raw of items) {
|
|
5124
|
+
const message = raw;
|
|
5125
|
+
if (!message.message_id || message.deleted) continue;
|
|
5126
|
+
parsed.push({
|
|
5127
|
+
id: message.message_id,
|
|
5128
|
+
chatId: message.chat_id ?? chatId,
|
|
5129
|
+
createTime: message.create_time ?? "",
|
|
5130
|
+
text: textOf(message.body?.content),
|
|
5131
|
+
...message.sender?.id ? { senderId: message.sender.id } : {},
|
|
5132
|
+
...message.sender?.sender_type ? { senderType: message.sender.sender_type } : {},
|
|
5133
|
+
mentionIds: mentionsOf(message.mentions)
|
|
5134
|
+
});
|
|
5135
|
+
}
|
|
5136
|
+
pageToken = oldestMs && data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
|
|
5137
|
+
} while (pageToken);
|
|
5138
|
+
return parsed.sort((a, b) => Number(a.createTime) - Number(b.createTime));
|
|
5139
|
+
}
|
|
5140
|
+
async function replyMessage(credential, messageId, text3) {
|
|
5141
|
+
const body = await call(credential.host, `/im/v1/messages/${messageId}/reply`, {
|
|
5142
|
+
token: credential.tenantAccessToken,
|
|
5143
|
+
method: "POST",
|
|
5144
|
+
body: { msg_type: "text", content: JSON.stringify({ text: text3 }) }
|
|
5145
|
+
});
|
|
5146
|
+
const data = dataOf(body);
|
|
5147
|
+
const id = typeof data.message_id === "string" ? data.message_id : "";
|
|
5148
|
+
const chatId = typeof data.chat_id === "string" ? data.chat_id : "";
|
|
5149
|
+
if (!id) throw new Error("Feishu accepted the reply but did not say where it landed.");
|
|
5150
|
+
return { id, chatId };
|
|
5151
|
+
}
|
|
5152
|
+
async function sendMessage2(credential, chatId, text3) {
|
|
5153
|
+
const body = await call(credential.host, "/im/v1/messages", {
|
|
5154
|
+
token: credential.tenantAccessToken,
|
|
5155
|
+
method: "POST",
|
|
5156
|
+
query: { receive_id_type: "chat_id" },
|
|
5157
|
+
body: { receive_id: chatId, msg_type: "text", content: JSON.stringify({ text: text3 }) }
|
|
5158
|
+
});
|
|
5159
|
+
const data = dataOf(body);
|
|
5160
|
+
const id = typeof data.message_id === "string" ? data.message_id : "";
|
|
5161
|
+
const landed = typeof data.chat_id === "string" ? data.chat_id : chatId;
|
|
5162
|
+
if (!id) throw new Error("Feishu accepted the message but did not say where it landed.");
|
|
5163
|
+
return { id, chatId: landed };
|
|
5164
|
+
}
|
|
5165
|
+
function permalink3(host, chatId) {
|
|
5166
|
+
return `${APPLINK[host]}?openChatId=${chatId}`;
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
// src/connectors/feishu/runtime.ts
|
|
5170
|
+
function asAsked2(value) {
|
|
5171
|
+
const record = value;
|
|
5172
|
+
if (!record || typeof record.chatId !== "string" || typeof record.messageId !== "string") {
|
|
5173
|
+
throw new Error("This task has lost the message it was about.");
|
|
5174
|
+
}
|
|
5175
|
+
return {
|
|
5176
|
+
chatId: record.chatId,
|
|
5177
|
+
messageId: record.messageId,
|
|
5178
|
+
user: typeof record.user === "string" ? record.user : void 0,
|
|
5179
|
+
text: typeof record.text === "string" ? record.text : ""
|
|
5180
|
+
};
|
|
5181
|
+
}
|
|
5182
|
+
function asCursor2(value) {
|
|
5183
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
5184
|
+
const out = {};
|
|
5185
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
5186
|
+
if (typeof entry === "string") out[key] = entry;
|
|
5187
|
+
}
|
|
5188
|
+
return out;
|
|
5189
|
+
}
|
|
5190
|
+
function summarise3(text3) {
|
|
5191
|
+
const line = text3.replace(/@_user_\d+/g, "").replace(/\s+/g, " ").trim();
|
|
5192
|
+
if (!line) return "Feishu message";
|
|
5193
|
+
return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
5194
|
+
}
|
|
5195
|
+
function isAsk2(input) {
|
|
5196
|
+
if (input.senderType === "app" || input.senderId === input.botOpenId) return false;
|
|
5197
|
+
return input.mentionIds.includes(input.botOpenId);
|
|
5198
|
+
}
|
|
5199
|
+
function destination2(target, source) {
|
|
5200
|
+
if (typeof target.chat === "string" && target.chat.trim()) {
|
|
5201
|
+
return { chatId: target.chat.trim() };
|
|
5202
|
+
}
|
|
5203
|
+
if (source.connectorId !== "feishu") {
|
|
5204
|
+
throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
|
|
5205
|
+
}
|
|
5206
|
+
const carry = source.carry ?? {};
|
|
5207
|
+
if (!carry.chatId && !carry.messageId) {
|
|
5208
|
+
throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
|
|
5209
|
+
}
|
|
5210
|
+
return { chatId: carry.chatId, messageId: carry.messageId };
|
|
5211
|
+
}
|
|
5212
|
+
function accountOf3(credential) {
|
|
5213
|
+
return {
|
|
5214
|
+
id: credential.botOpenId,
|
|
5215
|
+
label: credential.botName || credential.appId,
|
|
5216
|
+
...credential.avatarUrl ? { avatarUrl: credential.avatarUrl } : {}
|
|
5217
|
+
};
|
|
5218
|
+
}
|
|
5219
|
+
async function usable(credential) {
|
|
5220
|
+
if (Date.now() < credential.expiresAt) return { credential };
|
|
5221
|
+
const minted = await tenantAccessToken(credential.host, credential.appId, credential.appSecret);
|
|
5222
|
+
const refreshed = {
|
|
5223
|
+
...credential,
|
|
5224
|
+
tenantAccessToken: minted.token,
|
|
5225
|
+
expiresAt: minted.expiresAt
|
|
5226
|
+
};
|
|
5227
|
+
return { credential: refreshed, refreshed };
|
|
5228
|
+
}
|
|
5229
|
+
var feishuRuntime = {
|
|
5230
|
+
async readiness() {
|
|
5231
|
+
return { ready: true };
|
|
5232
|
+
},
|
|
5233
|
+
async poll({ workflowId, credential, cursor }) {
|
|
5234
|
+
if (workflowId !== "feishu.ask") {
|
|
5235
|
+
throw new Error(`Feishu cannot watch for ${workflowId}.`);
|
|
5236
|
+
}
|
|
5237
|
+
const { credential: account2 } = await usable(credential);
|
|
5238
|
+
const previous = asCursor2(cursor);
|
|
5239
|
+
const chats = await listChats(account2);
|
|
5240
|
+
const next = { ...previous };
|
|
5241
|
+
const signals2 = [];
|
|
5242
|
+
for (const chat2 of chats) {
|
|
5243
|
+
const oldest = previous[chat2.id];
|
|
5244
|
+
const messages = await history2(account2, chat2.id, oldest);
|
|
5245
|
+
if (messages.length === 0) {
|
|
5246
|
+
if (!next[chat2.id]) next[chat2.id] = String(Date.now());
|
|
5247
|
+
continue;
|
|
5248
|
+
}
|
|
5249
|
+
next[chat2.id] = messages[messages.length - 1].createTime;
|
|
5250
|
+
if (!oldest) continue;
|
|
5251
|
+
for (const message of messages) {
|
|
5252
|
+
if (oldest && Number(message.createTime) <= Number(oldest)) continue;
|
|
5253
|
+
if (!isAsk2({
|
|
5254
|
+
senderId: message.senderId,
|
|
5255
|
+
senderType: message.senderType,
|
|
5256
|
+
mentionIds: message.mentionIds,
|
|
5257
|
+
botOpenId: account2.botOpenId
|
|
5258
|
+
})) {
|
|
5259
|
+
continue;
|
|
5260
|
+
}
|
|
5261
|
+
const asked = {
|
|
5262
|
+
chatId: message.chatId,
|
|
5263
|
+
messageId: message.id,
|
|
5264
|
+
user: message.senderId,
|
|
5265
|
+
text: message.text
|
|
5266
|
+
};
|
|
5267
|
+
signals2.push({
|
|
5268
|
+
key: `${message.chatId}:${message.id}`,
|
|
5269
|
+
kind: "message",
|
|
5270
|
+
ref: message.id,
|
|
5271
|
+
title: summarise3(message.text),
|
|
5272
|
+
url: permalink3(account2.host, message.chatId),
|
|
5273
|
+
payload: asked
|
|
5274
|
+
});
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
return { signals: signals2, cursor: next };
|
|
5278
|
+
},
|
|
5279
|
+
async resolveWorkItem({ payload }) {
|
|
5280
|
+
const asked = asAsked2(payload);
|
|
5281
|
+
return {
|
|
5282
|
+
kind: "message",
|
|
5283
|
+
ref: asked.messageId,
|
|
5284
|
+
title: summarise3(asked.text),
|
|
5285
|
+
url: "",
|
|
5286
|
+
carry: { chatId: asked.chatId, messageId: asked.messageId },
|
|
5287
|
+
context: [
|
|
5288
|
+
{
|
|
5289
|
+
name: "MESSAGE.md",
|
|
5290
|
+
body: [`# What you were asked`, ``, asked.text, ``].join("\n")
|
|
5291
|
+
}
|
|
5292
|
+
]
|
|
5293
|
+
};
|
|
5294
|
+
},
|
|
5295
|
+
async applyAction({ actionId, target, source, body, credential }) {
|
|
5296
|
+
if (actionId !== "feishu.reply") throw new Error(`Feishu cannot ${actionId}.`);
|
|
5297
|
+
const { credential: account2 } = await usable(credential);
|
|
5298
|
+
const to = destination2(target, source);
|
|
5299
|
+
const text3 = body.trim();
|
|
5300
|
+
const posted = to.messageId && source.connectorId === "feishu" ? await replyMessage(account2, to.messageId, text3) : await sendMessage2(account2, to.chatId ?? "", text3);
|
|
5301
|
+
return { url: permalink3(account2.host, posted.chatId || to.chatId || "") };
|
|
5302
|
+
},
|
|
5303
|
+
auth: {
|
|
5304
|
+
async connectWithFields(fields) {
|
|
5305
|
+
const appId = fields.appId?.trim() ?? "";
|
|
5306
|
+
const appSecret = fields.appSecret?.trim() ?? "";
|
|
5307
|
+
if (!appId) throw new Error("Paste the App ID from your Feishu or Lark app.");
|
|
5308
|
+
if (!appSecret) throw new Error("Paste the App Secret from your Feishu or Lark app.");
|
|
5309
|
+
const host = parseHost(fields.host);
|
|
5310
|
+
const minted = await tenantAccessToken(host, appId, appSecret);
|
|
5311
|
+
const bot = await botInfo({ host, tenantAccessToken: minted.token });
|
|
5312
|
+
const stored = {
|
|
5313
|
+
appId,
|
|
5314
|
+
appSecret,
|
|
5315
|
+
host,
|
|
5316
|
+
botOpenId: bot.openId,
|
|
5317
|
+
botName: bot.name,
|
|
5318
|
+
...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {},
|
|
5319
|
+
tenantAccessToken: minted.token,
|
|
5320
|
+
expiresAt: minted.expiresAt
|
|
5321
|
+
};
|
|
5322
|
+
return { credential: stored, account: accountOf3(stored) };
|
|
5323
|
+
},
|
|
5324
|
+
async identity(credential) {
|
|
5325
|
+
const stored = credential;
|
|
5326
|
+
const { credential: fresh, refreshed } = await usable(stored);
|
|
5327
|
+
const bot = await botInfo(fresh);
|
|
5328
|
+
const updated = {
|
|
5329
|
+
...fresh,
|
|
5330
|
+
botOpenId: bot.openId,
|
|
5331
|
+
botName: bot.name,
|
|
5332
|
+
...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {}
|
|
5333
|
+
};
|
|
5334
|
+
return {
|
|
5335
|
+
account: accountOf3(updated),
|
|
5336
|
+
...refreshed ? { renewedCredential: updated } : {}
|
|
5337
|
+
};
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
};
|
|
5341
|
+
|
|
4206
5342
|
// src/connectors/wechat/runtime.ts
|
|
4207
5343
|
import { randomUUID } from "node:crypto";
|
|
4208
5344
|
|
|
@@ -4227,7 +5363,7 @@ function postHeaders(token) {
|
|
|
4227
5363
|
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
4228
5364
|
};
|
|
4229
5365
|
}
|
|
4230
|
-
async function
|
|
5366
|
+
async function call2(url, init) {
|
|
4231
5367
|
let response;
|
|
4232
5368
|
try {
|
|
4233
5369
|
response = await fetch(url, init);
|
|
@@ -4241,7 +5377,7 @@ async function call(url, init) {
|
|
|
4241
5377
|
return await response.json();
|
|
4242
5378
|
}
|
|
4243
5379
|
async function requestQrCode() {
|
|
4244
|
-
const body = await
|
|
5380
|
+
const body = await call2(
|
|
4245
5381
|
`${WECHAT_API}/ilink/bot/get_bot_qrcode?bot_type=3`,
|
|
4246
5382
|
{
|
|
4247
5383
|
method: "POST",
|
|
@@ -4261,7 +5397,7 @@ function absolute(host) {
|
|
|
4261
5397
|
async function qrCodeStatus(qrcode, host) {
|
|
4262
5398
|
let body;
|
|
4263
5399
|
try {
|
|
4264
|
-
body = await
|
|
5400
|
+
body = await call2(
|
|
4265
5401
|
`${absolute(host)}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
|
|
4266
5402
|
{ headers: APP_HEADERS }
|
|
4267
5403
|
);
|
|
@@ -4314,7 +5450,7 @@ function refusal(body) {
|
|
|
4314
5450
|
return body.errmsg || `WeChat refused the request (${bad}).`;
|
|
4315
5451
|
}
|
|
4316
5452
|
async function notifyStart(credential) {
|
|
4317
|
-
const body = await
|
|
5453
|
+
const body = await call2(`${credential.baseUrl}/ilink/bot/msg/notifystart`, {
|
|
4318
5454
|
method: "POST",
|
|
4319
5455
|
headers: postHeaders(credential.botToken),
|
|
4320
5456
|
body: JSON.stringify({ base_info: BASE_INFO })
|
|
@@ -4324,7 +5460,7 @@ async function notifyStart(credential) {
|
|
|
4324
5460
|
}
|
|
4325
5461
|
var FROM_USER = 1;
|
|
4326
5462
|
var TEXT_ITEM = 1;
|
|
4327
|
-
function
|
|
5463
|
+
function textOf2(message) {
|
|
4328
5464
|
return (message.item_list ?? []).filter((item) => item.type === TEXT_ITEM).map((item) => item.text_item?.text?.trim()).filter((text3) => Boolean(text3)).join("\n").trim();
|
|
4329
5465
|
}
|
|
4330
5466
|
function isFromPerson(message) {
|
|
@@ -4336,7 +5472,7 @@ async function getUpdates(credential, cursor) {
|
|
|
4336
5472
|
const timer = setTimeout(() => abort.abort(), WAIT_MS);
|
|
4337
5473
|
let body;
|
|
4338
5474
|
try {
|
|
4339
|
-
body = await
|
|
5475
|
+
body = await call2(`${credential.baseUrl}/ilink/bot/getupdates`, {
|
|
4340
5476
|
method: "POST",
|
|
4341
5477
|
headers: postHeaders(credential.botToken),
|
|
4342
5478
|
body: JSON.stringify({ get_updates_buf: cursor, base_info: BASE_INFO }),
|
|
@@ -4352,8 +5488,8 @@ async function getUpdates(credential, cursor) {
|
|
|
4352
5488
|
if (refused) throw new Error(refused);
|
|
4353
5489
|
return { messages: body.msgs ?? [], cursor: body.get_updates_buf || cursor };
|
|
4354
5490
|
}
|
|
4355
|
-
async function
|
|
4356
|
-
const body = await
|
|
5491
|
+
async function sendMessage3(credential, input) {
|
|
5492
|
+
const body = await call2(`${credential.baseUrl}/ilink/bot/sendmessage`, {
|
|
4357
5493
|
method: "POST",
|
|
4358
5494
|
headers: postHeaders(credential.botToken),
|
|
4359
5495
|
body: JSON.stringify({
|
|
@@ -4388,7 +5524,7 @@ function asAttempt(value) {
|
|
|
4388
5524
|
}
|
|
4389
5525
|
return { qrcode: record.qrcode, host: record.host };
|
|
4390
5526
|
}
|
|
4391
|
-
function
|
|
5527
|
+
function asAsked3(value) {
|
|
4392
5528
|
const record = value;
|
|
4393
5529
|
if (!record || typeof record.fromUserId !== "string" || typeof record.text !== "string") {
|
|
4394
5530
|
throw new Error("This task has lost the message it was about.");
|
|
@@ -4400,7 +5536,7 @@ function asAsked(value) {
|
|
|
4400
5536
|
text: record.text
|
|
4401
5537
|
};
|
|
4402
5538
|
}
|
|
4403
|
-
function
|
|
5539
|
+
function summarise4(text3) {
|
|
4404
5540
|
const line = text3.split("\n")[0].trim();
|
|
4405
5541
|
return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
4406
5542
|
}
|
|
@@ -4427,7 +5563,7 @@ function recipientFor(target, source, account2) {
|
|
|
4427
5563
|
if (!carry.toUserId) throw new Error("There is nobody to reply to on this task.");
|
|
4428
5564
|
return { userId: carry.toUserId, contextToken: carry.contextToken };
|
|
4429
5565
|
}
|
|
4430
|
-
function
|
|
5566
|
+
function accountOf4(credential) {
|
|
4431
5567
|
return {
|
|
4432
5568
|
id: credential.botId || credential.userId,
|
|
4433
5569
|
label: credential.botId ? `WeChat bot ${credential.botId}` : "WeChat bot"
|
|
@@ -4455,7 +5591,7 @@ var wechatRuntime = {
|
|
|
4455
5591
|
const signals2 = [];
|
|
4456
5592
|
for (const message of messages) {
|
|
4457
5593
|
if (!isFromPerson(message)) continue;
|
|
4458
|
-
const text3 =
|
|
5594
|
+
const text3 = textOf2(message);
|
|
4459
5595
|
if (!text3) continue;
|
|
4460
5596
|
const fromUserId = message.from_user_id ?? "";
|
|
4461
5597
|
const asked = {
|
|
@@ -4468,7 +5604,7 @@ var wechatRuntime = {
|
|
|
4468
5604
|
key: `msg#${asked.messageId}`,
|
|
4469
5605
|
kind: "message",
|
|
4470
5606
|
ref: `msg#${asked.messageId}`,
|
|
4471
|
-
title:
|
|
5607
|
+
title: summarise4(text3),
|
|
4472
5608
|
// A message in a chat is not somewhere a browser can be sent.
|
|
4473
5609
|
url: "",
|
|
4474
5610
|
// Held rather than dropped, so a message that was sent and never
|
|
@@ -4481,11 +5617,11 @@ var wechatRuntime = {
|
|
|
4481
5617
|
return { signals: signals2, cursor: next };
|
|
4482
5618
|
},
|
|
4483
5619
|
async resolveWorkItem({ payload }) {
|
|
4484
|
-
const asked =
|
|
5620
|
+
const asked = asAsked3(payload);
|
|
4485
5621
|
return {
|
|
4486
5622
|
kind: "message",
|
|
4487
5623
|
ref: `msg#${asked.messageId}`,
|
|
4488
|
-
title:
|
|
5624
|
+
title: summarise4(asked.text),
|
|
4489
5625
|
url: "",
|
|
4490
5626
|
carry: { toUserId: asked.fromUserId, contextToken: asked.contextToken },
|
|
4491
5627
|
context: [
|
|
@@ -4501,7 +5637,7 @@ var wechatRuntime = {
|
|
|
4501
5637
|
const account2 = credential;
|
|
4502
5638
|
const to = recipientFor(target, source, account2);
|
|
4503
5639
|
for (const part of chunked(body)) {
|
|
4504
|
-
await
|
|
5640
|
+
await sendMessage3(account2, {
|
|
4505
5641
|
toUserId: to.userId,
|
|
4506
5642
|
contextToken: to.contextToken,
|
|
4507
5643
|
// Unique per piece, so a retry cannot post half an answer twice.
|
|
@@ -4527,7 +5663,7 @@ var wechatRuntime = {
|
|
|
4527
5663
|
case "confirmed":
|
|
4528
5664
|
return {
|
|
4529
5665
|
state: "confirmed",
|
|
4530
|
-
result: { credential: status.credential, account:
|
|
5666
|
+
result: { credential: status.credential, account: accountOf4(status.credential) }
|
|
4531
5667
|
};
|
|
4532
5668
|
case "expired":
|
|
4533
5669
|
return { state: "expired", reason: status.reason };
|
|
@@ -4542,7 +5678,7 @@ var wechatRuntime = {
|
|
|
4542
5678
|
async identity(credential) {
|
|
4543
5679
|
const stored = credential;
|
|
4544
5680
|
await notifyStart(stored);
|
|
4545
|
-
return { account:
|
|
5681
|
+
return { account: accountOf4(stored) };
|
|
4546
5682
|
}
|
|
4547
5683
|
}
|
|
4548
5684
|
};
|
|
@@ -4550,6 +5686,8 @@ var wechatRuntime = {
|
|
|
4550
5686
|
// src/connectors/runtimes.ts
|
|
4551
5687
|
var RUNTIMES = {
|
|
4552
5688
|
github: githubRuntime,
|
|
5689
|
+
slack: slackRuntime,
|
|
5690
|
+
feishu: feishuRuntime,
|
|
4553
5691
|
gmail: gmailRuntime,
|
|
4554
5692
|
wechat: wechatRuntime,
|
|
4555
5693
|
schedule: scheduleRuntime
|
|
@@ -6289,7 +7427,7 @@ var SQLiteSelectQueryBuilderBase = class extends TypedQueryBuilder {
|
|
|
6289
7427
|
const baseTableName = this.tableName;
|
|
6290
7428
|
const tableName = getTableLikeName(table);
|
|
6291
7429
|
for (const item of extractUsedTable(table)) this.usedTables.add(item);
|
|
6292
|
-
if (typeof tableName === "string" && this.config.joins?.some((
|
|
7430
|
+
if (typeof tableName === "string" && this.config.joins?.some((join9) => join9.alias === tableName)) {
|
|
6293
7431
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
6294
7432
|
}
|
|
6295
7433
|
if (!this.isPartialSelect) {
|
|
@@ -7148,7 +8286,7 @@ var SQLiteUpdateBase = class extends QueryPromise {
|
|
|
7148
8286
|
createJoin(joinType) {
|
|
7149
8287
|
return (table, on2) => {
|
|
7150
8288
|
const tableName = getTableLikeName(table);
|
|
7151
|
-
if (typeof tableName === "string" && this.config.joins.some((
|
|
8289
|
+
if (typeof tableName === "string" && this.config.joins.some((join9) => join9.alias === tableName)) {
|
|
7152
8290
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
7153
8291
|
}
|
|
7154
8292
|
if (typeof on2 === "function") {
|
|
@@ -8607,6 +9745,10 @@ var runners = sqliteTable("runners", {
|
|
|
8607
9745
|
status: text2("status").$type().notNull().default("offline"),
|
|
8608
9746
|
inventory: text2("inventory", { mode: "json" }).$type().notNull().default([]),
|
|
8609
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"),
|
|
8610
9752
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
|
|
8611
9753
|
});
|
|
8612
9754
|
|
|
@@ -8629,13 +9771,13 @@ function migrationsFolder() {
|
|
|
8629
9771
|
join3(here, "../../../drizzle"),
|
|
8630
9772
|
join3(process.cwd(), "drizzle")
|
|
8631
9773
|
];
|
|
8632
|
-
return candidates2.find((
|
|
9774
|
+
return candidates2.find((folder) => existsSync2(folder)) ?? join3(process.cwd(), "drizzle");
|
|
8633
9775
|
}
|
|
8634
9776
|
async function migrateIfNeeded() {
|
|
8635
9777
|
if (process.env.LOOPABLE_SKIP_MIGRATE === "1") return;
|
|
8636
|
-
const
|
|
8637
|
-
if (!existsSync2(
|
|
8638
|
-
await migrate(db(), { migrationsFolder:
|
|
9778
|
+
const folder = migrationsFolder();
|
|
9779
|
+
if (!existsSync2(folder)) return;
|
|
9780
|
+
await migrate(db(), { migrationsFolder: folder });
|
|
8639
9781
|
}
|
|
8640
9782
|
function rowsChanged(result) {
|
|
8641
9783
|
return Number(result.rowsAffected ?? result.changes ?? 0);
|
|
@@ -8752,14 +9894,14 @@ async function decrypt(blob2) {
|
|
|
8752
9894
|
return new TextDecoder().decode(opened);
|
|
8753
9895
|
}
|
|
8754
9896
|
function bytesToBase64(bytes) {
|
|
8755
|
-
let
|
|
8756
|
-
for (const byte of bytes)
|
|
8757
|
-
return btoa(
|
|
9897
|
+
let binary4 = "";
|
|
9898
|
+
for (const byte of bytes) binary4 += String.fromCharCode(byte);
|
|
9899
|
+
return btoa(binary4);
|
|
8758
9900
|
}
|
|
8759
9901
|
function base64ToBytes(blob2) {
|
|
8760
|
-
const
|
|
8761
|
-
const bytes = new Uint8Array(
|
|
8762
|
-
for (let i = 0; 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);
|
|
8763
9905
|
return bytes;
|
|
8764
9906
|
}
|
|
8765
9907
|
var tableStore = {
|
|
@@ -8877,12 +10019,13 @@ async function checkConnection(id) {
|
|
|
8877
10019
|
// src/server/tasks.ts
|
|
8878
10020
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8879
10021
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, rmSync as rmSync3, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8880
|
-
import {
|
|
10022
|
+
import { join as join7 } from "node:path";
|
|
8881
10023
|
|
|
8882
10024
|
// src/agents/manifests.ts
|
|
8883
10025
|
init_manifest();
|
|
8884
10026
|
init_manifest2();
|
|
8885
|
-
|
|
10027
|
+
init_manifest3();
|
|
10028
|
+
var AGENT_MANIFESTS = [codexManifest, claudeManifest, cursorAgentManifest];
|
|
8886
10029
|
function agentManifest(id) {
|
|
8887
10030
|
return AGENT_MANIFESTS.find((manifest) => manifest.id === id);
|
|
8888
10031
|
}
|
|
@@ -8909,6 +10052,10 @@ var REVIEW_FORMAT = [
|
|
|
8909
10052
|
`- "findings" may be empty when the summary says everything worth saying.`
|
|
8910
10053
|
].join("\n");
|
|
8911
10054
|
|
|
10055
|
+
// src/server/tasks.ts
|
|
10056
|
+
init_agent_job();
|
|
10057
|
+
init_version();
|
|
10058
|
+
|
|
8912
10059
|
// src/server/agents.ts
|
|
8913
10060
|
init_runtimes();
|
|
8914
10061
|
var DEFAULT_AGENT_KEY = "defaultAgentId";
|
|
@@ -8980,7 +10127,11 @@ function branchFor(input) {
|
|
|
8980
10127
|
|
|
8981
10128
|
// src/server/runners.ts
|
|
8982
10129
|
import { hostname as osHostname } from "node:os";
|
|
10130
|
+
init_version();
|
|
8983
10131
|
var ONLINE_MS = 45e3;
|
|
10132
|
+
function protocolOf(row) {
|
|
10133
|
+
return row.protocol ?? LEGACY_RUNNER_PROTOCOL;
|
|
10134
|
+
}
|
|
8984
10135
|
function isRunnerOnline(row, now = Date.now()) {
|
|
8985
10136
|
if (row.status !== "online") return false;
|
|
8986
10137
|
if (!row.lastSeenAt) return false;
|
|
@@ -8994,7 +10145,10 @@ function toView2(row, now = Date.now()) {
|
|
|
8994
10145
|
hostname: row.hostname,
|
|
8995
10146
|
status: online ? "online" : "offline",
|
|
8996
10147
|
inventory: row.inventory,
|
|
8997
|
-
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)
|
|
8998
10152
|
};
|
|
8999
10153
|
}
|
|
9000
10154
|
async function listRunners() {
|
|
@@ -9018,11 +10172,13 @@ async function busyRunnerIds() {
|
|
|
9018
10172
|
)
|
|
9019
10173
|
);
|
|
9020
10174
|
}
|
|
9021
|
-
async function availableAgentIds(requiresHost = false) {
|
|
10175
|
+
async function availableAgentIds(requiresHost = false, minProtocol = 0) {
|
|
9022
10176
|
const local = osHostname();
|
|
9023
10177
|
return [
|
|
9024
10178
|
...new Set(
|
|
9025
|
-
(await listRunners()).filter(
|
|
10179
|
+
(await listRunners()).filter(
|
|
10180
|
+
(row) => row.status === "online" && (!requiresHost || row.hostname === local) && row.protocol >= minProtocol
|
|
10181
|
+
).flatMap(
|
|
9026
10182
|
(row) => row.inventory.filter((entry) => entry.installed && entry.signedIn).map((entry) => entry.agentId)
|
|
9027
10183
|
)
|
|
9028
10184
|
)
|
|
@@ -9031,9 +10187,11 @@ async function availableAgentIds(requiresHost = false) {
|
|
|
9031
10187
|
async function pickRunner(input) {
|
|
9032
10188
|
const now = Date.now();
|
|
9033
10189
|
const local = osHostname();
|
|
9034
|
-
|
|
10190
|
+
const minProtocol = input.minProtocol ?? 0;
|
|
10191
|
+
const capable = (await db().select().from(runners).all()).filter(
|
|
9035
10192
|
(row) => isRunnerOnline(row, now) && runnerCanRun(row, input.agentId)
|
|
9036
10193
|
);
|
|
10194
|
+
let candidates2 = capable.filter((row) => protocolOf(row) >= minProtocol);
|
|
9037
10195
|
if (input.requiresHost) {
|
|
9038
10196
|
candidates2 = candidates2.filter((row) => row.hostname === local);
|
|
9039
10197
|
}
|
|
@@ -9044,6 +10202,11 @@ async function pickRunner(input) {
|
|
|
9044
10202
|
if (input.requiresHost) {
|
|
9045
10203
|
throw new Error("This job needs a runner on the same host as Loopable.");
|
|
9046
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
|
+
}
|
|
9047
10210
|
throw new Error("No runner is online that can run this agent.");
|
|
9048
10211
|
}
|
|
9049
10212
|
|
|
@@ -9185,11 +10348,11 @@ async function enqueueSignal(input) {
|
|
|
9185
10348
|
});
|
|
9186
10349
|
}
|
|
9187
10350
|
function sweepRunDirs(maxAgeMs = 7 * 24 * 60 * 60 * 1e3) {
|
|
9188
|
-
const root =
|
|
10351
|
+
const root = join7(dataDir(), "runs");
|
|
9189
10352
|
if (!existsSync4(root)) return 0;
|
|
9190
10353
|
let removed = 0;
|
|
9191
10354
|
for (const entry of readdirSync(root)) {
|
|
9192
|
-
const dir =
|
|
10355
|
+
const dir = join7(root, entry);
|
|
9193
10356
|
try {
|
|
9194
10357
|
if (Date.now() - statSync(dir).mtimeMs < maxAgeMs) continue;
|
|
9195
10358
|
rmSync3(dir, { recursive: true, force: true });
|
|
@@ -9252,7 +10415,7 @@ async function preparePromptTask(id, signal) {
|
|
|
9252
10415
|
let logPath = null;
|
|
9253
10416
|
if (!isHosted()) {
|
|
9254
10417
|
const workspace = runDir(id);
|
|
9255
|
-
logPath =
|
|
10418
|
+
logPath = join7(workspace, "agent.log");
|
|
9256
10419
|
writeFileSync2(logPath, "", { flag: "a" });
|
|
9257
10420
|
}
|
|
9258
10421
|
const runnerId = await pickRunner({ agentId: task.agentId, requiresHost: false });
|
|
@@ -9276,14 +10439,17 @@ async function prepareTask(id, signal) {
|
|
|
9276
10439
|
if (!isHosted()) {
|
|
9277
10440
|
const workspace = runDir(id);
|
|
9278
10441
|
for (const file of item.context) {
|
|
9279
|
-
writeFileSync2(
|
|
10442
|
+
writeFileSync2(join7(workspace, file.name), file.body);
|
|
9280
10443
|
}
|
|
9281
|
-
logPath =
|
|
10444
|
+
logPath = join7(workspace, "agent.log");
|
|
9282
10445
|
writeFileSync2(logPath, "", { flag: "a" });
|
|
9283
10446
|
}
|
|
10447
|
+
const folder = workflow.runsIn === "folder" && typeof loop.settings.folder === "string" ? loop.settings.folder : null;
|
|
9284
10448
|
const runnerId = await pickRunner({
|
|
9285
10449
|
agentId,
|
|
9286
|
-
requiresHost:
|
|
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
|
|
9287
10453
|
});
|
|
9288
10454
|
await updateTask(id, {
|
|
9289
10455
|
state: "awaiting_agent",
|
|
@@ -9458,7 +10624,7 @@ async function pollLoop(loop, claimed) {
|
|
|
9458
10624
|
if (!runtime.poll) throw new Error(`${manifest.name} cannot watch for anything yet.`);
|
|
9459
10625
|
const { credential } = await credentialForConnector(loop.connectorId);
|
|
9460
10626
|
const answer = await runtime.poll({
|
|
9461
|
-
workflowId:
|
|
10627
|
+
workflowId: workflow.triggerId,
|
|
9462
10628
|
settings: loop.settings,
|
|
9463
10629
|
credential,
|
|
9464
10630
|
cursor: loop.pollCursor ?? null
|
|
@@ -9478,7 +10644,7 @@ async function pollLoop(loop, claimed) {
|
|
|
9478
10644
|
signals: fresh,
|
|
9479
10645
|
agentId: await defaultAgentFor(loop.agentId),
|
|
9480
10646
|
guidance: loop.guidance,
|
|
9481
|
-
cwd: mkdtempSync(
|
|
10647
|
+
cwd: mkdtempSync(join8(tmpdir(), "loopable-triage-"))
|
|
9482
10648
|
});
|
|
9483
10649
|
}
|
|
9484
10650
|
for (const signal of found) {
|