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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { d as and, f as eq, h as lt, l as asc } from "../_libs/drizzle-orm.mjs";
|
|
2
|
-
import { a as connections, i as authAttempts, s as db } from "./client-
|
|
3
|
-
import { i as readCredential, s as writeCredential, t as deleteCredential } from "./secrets-
|
|
2
|
+
import { a as connections, i as authAttempts, s as db } from "./client-Dk3SD8Il.mjs";
|
|
3
|
+
import { i as readCredential, s as writeCredential, t as deleteCredential } from "./secrets-CV5scFlt.mjs";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
//#region node_modules/.nitro/vite/services/ssr/assets/connections-
|
|
7
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/connections-Cqvb6Gw2.js
|
|
8
8
|
function defineManifest(manifest) {
|
|
9
9
|
return manifest;
|
|
10
10
|
}
|
|
@@ -16,7 +16,7 @@ var repositories = {
|
|
|
16
16
|
key: "repositories",
|
|
17
17
|
kind: "string_list",
|
|
18
18
|
label: "Repositories",
|
|
19
|
-
help: "owner/name, one per line.
|
|
19
|
+
help: "owner/name, one per line. acme/* works. Empty means all.",
|
|
20
20
|
placeholder: "acme/web"
|
|
21
21
|
};
|
|
22
22
|
var ignoreDrafts = {
|
|
@@ -29,7 +29,7 @@ var ignoreBots = {
|
|
|
29
29
|
key: "ignoreBots",
|
|
30
30
|
kind: "boolean",
|
|
31
31
|
label: "Skip pull requests opened by bots",
|
|
32
|
-
help: "
|
|
32
|
+
help: "Such as Dependabot.",
|
|
33
33
|
default: true
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
@@ -69,7 +69,7 @@ var issueTarget = {
|
|
|
69
69
|
key: "issue",
|
|
70
70
|
kind: "text",
|
|
71
71
|
label: "Issue or pull request",
|
|
72
|
-
help: "
|
|
72
|
+
help: "Empty replies where it was asked. Or a GitHub issue URL.",
|
|
73
73
|
placeholder: "https://github.com/acme/web/issues/12"
|
|
74
74
|
};
|
|
75
75
|
/**
|
|
@@ -91,115 +91,80 @@ var REVIEW_PROMPT = [
|
|
|
91
91
|
"Where you cannot see enough of the code to judge something, say so instead of",
|
|
92
92
|
"guessing."
|
|
93
93
|
].join("\n");
|
|
94
|
-
var
|
|
95
|
-
"
|
|
94
|
+
var REPLY_PROMPT = [
|
|
95
|
+
"Answer this issue in a comment, the way a teammate who knows this codebase would.",
|
|
96
96
|
"",
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
97
|
+
"Work out what the issue is asking for. If it is a question, answer it from the",
|
|
98
|
+
"code rather than from memory. If it reports a bug, find the likely cause and",
|
|
99
|
+
"point at the files and lines involved. If it asks for a change, say how you",
|
|
100
|
+
"would make it, where, and what to watch out for.",
|
|
100
101
|
"",
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
var IMPLEMENT_PROMPT = [
|
|
104
|
-
"Implement this issue in the checkout you are in.",
|
|
105
|
-
"",
|
|
106
|
-
"Read enough of the surrounding code first that what you write looks like it",
|
|
107
|
-
"belongs: the same patterns, the same names, the same way of handling errors.",
|
|
108
|
-
"Where the project has tests for work like this, add one.",
|
|
109
|
-
"",
|
|
110
|
-
"Stay inside what the issue asks for. A change that also tidies three other",
|
|
111
|
-
"things is a change nobody can review. If the issue is too vague to implement,",
|
|
112
|
-
"or doing it properly needs a decision that is not yours to make, reply with",
|
|
113
|
-
"exactly NOTHING_TO_DO rather than guessing: an unwanted pull request costs",
|
|
114
|
-
"more attention than a missing one.",
|
|
115
|
-
"",
|
|
116
|
-
"Run the project's tests if you can work out how, and say in your description",
|
|
117
|
-
"whether you did and what happened."
|
|
102
|
+
"Read the code; do not change it. If the issue is too vague to answer, say what",
|
|
103
|
+
"is missing rather than inventing it."
|
|
118
104
|
].join("\n");
|
|
119
105
|
var githubManifest = defineManifest({
|
|
120
106
|
id: "github",
|
|
121
107
|
name: "GitHub",
|
|
122
|
-
tagline: "Pick up
|
|
108
|
+
tagline: "Pick up reviews and issues sent to your team's shared GitHub account.",
|
|
123
109
|
docsUrl: "https://docs.github.com/rest",
|
|
124
110
|
icon: "Github",
|
|
125
111
|
accent: "bg-neutral-900 text-white",
|
|
126
112
|
auth: {
|
|
127
113
|
kind: "oauth_redirect",
|
|
128
114
|
scopes: GITHUB_SCOPES,
|
|
129
|
-
needsAppRegistration: true
|
|
115
|
+
needsAppRegistration: true,
|
|
116
|
+
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."
|
|
130
117
|
},
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{
|
|
182
|
-
id: "github.submit_review",
|
|
183
|
-
name: "Submit a review",
|
|
184
|
-
summary: "Post a review on a pull request, as a comment rather than an approval.",
|
|
185
|
-
target: [],
|
|
186
|
-
accepts: ["text", "review"]
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
id: "github.post_issue_comment",
|
|
190
|
-
name: "Comment on an issue",
|
|
191
|
-
summary: "Post a comment on an issue or pull request.",
|
|
192
|
-
target: [issueTarget],
|
|
193
|
-
accepts: ["text"]
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
id: "github.open_pull_request",
|
|
197
|
-
name: "Open a draft pull request",
|
|
198
|
-
summary: "Open a draft pull request for the branch the agent already pushed.",
|
|
199
|
-
target: [],
|
|
200
|
-
accepts: ["code"]
|
|
201
|
-
}
|
|
202
|
-
],
|
|
118
|
+
triggers: [{
|
|
119
|
+
id: "github.review_requested",
|
|
120
|
+
when: "A GitHub review is requested",
|
|
121
|
+
trigger: "someone requests a review from the connected account, directly or through its team",
|
|
122
|
+
watches: "is:open is:pr review-requested:@me",
|
|
123
|
+
settings: [
|
|
124
|
+
repositories,
|
|
125
|
+
ignoreDrafts,
|
|
126
|
+
ignoreBots,
|
|
127
|
+
sizeLimit
|
|
128
|
+
],
|
|
129
|
+
runsIn: "checkout",
|
|
130
|
+
answer: "review",
|
|
131
|
+
actionId: "github.submit_review"
|
|
132
|
+
}, {
|
|
133
|
+
id: "github.issue_assigned",
|
|
134
|
+
when: "A GitHub issue is assigned",
|
|
135
|
+
trigger: "an issue is assigned to the connected account",
|
|
136
|
+
watches: "is:open is:issue assignee:@me",
|
|
137
|
+
settings: [repositories],
|
|
138
|
+
runsIn: "checkout",
|
|
139
|
+
answer: "text",
|
|
140
|
+
actionId: "github.post_issue_comment"
|
|
141
|
+
}],
|
|
142
|
+
workflows: [{
|
|
143
|
+
id: "github.review_requested",
|
|
144
|
+
name: "Review pull requests",
|
|
145
|
+
summary: "Posts a review when someone requests one from the connected account.",
|
|
146
|
+
triggerId: "github.review_requested",
|
|
147
|
+
prompt: REVIEW_PROMPT
|
|
148
|
+
}, {
|
|
149
|
+
id: "github.issue_assigned",
|
|
150
|
+
name: "Reply to issues",
|
|
151
|
+
summary: "Answers an issue in a comment when it is assigned to the connected account.",
|
|
152
|
+
triggerId: "github.issue_assigned",
|
|
153
|
+
prompt: REPLY_PROMPT
|
|
154
|
+
}],
|
|
155
|
+
actions: [{
|
|
156
|
+
id: "github.submit_review",
|
|
157
|
+
name: "Submit a review",
|
|
158
|
+
summary: "Post a review on a pull request, as a comment rather than an approval.",
|
|
159
|
+
target: [],
|
|
160
|
+
accepts: ["text", "review"]
|
|
161
|
+
}, {
|
|
162
|
+
id: "github.post_issue_comment",
|
|
163
|
+
name: "Comment on an issue",
|
|
164
|
+
summary: "Post a comment on an issue or pull request.",
|
|
165
|
+
target: [issueTarget],
|
|
166
|
+
accepts: ["text"]
|
|
167
|
+
}],
|
|
203
168
|
settings: [],
|
|
204
169
|
allowsMultipleAccounts: false,
|
|
205
170
|
byHand: {
|
|
@@ -208,86 +173,14 @@ var githubManifest = defineManifest({
|
|
|
208
173
|
}
|
|
209
174
|
});
|
|
210
175
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* to agree to than one that reads your inbox and tells you about it.
|
|
214
|
-
*/
|
|
215
|
-
var GMAIL_SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"];
|
|
216
|
-
/**
|
|
217
|
-
* Gmail's own search, appended to what the workflow already asks. This is the
|
|
218
|
-
* setting that makes the difference between a useful loop and a firehose, so
|
|
219
|
-
* it leads.
|
|
220
|
-
*/
|
|
221
|
-
var extraQuery = {
|
|
222
|
-
key: "extraQuery",
|
|
223
|
-
kind: "text",
|
|
224
|
-
label: "Narrow it down",
|
|
225
|
-
help: "Gmail search terms, added to the query above. For example: from:bank.example.com, or subject:invoice, or -from:notifications@github.com.",
|
|
226
|
-
placeholder: "from:boss@example.com"
|
|
227
|
-
};
|
|
228
|
-
/**
|
|
229
|
-
* A cap on how many mails one look can turn into runs, which is a cost limit
|
|
230
|
-
* rather than a filter: every mail past it is still noticed on the next poll,
|
|
231
|
-
* because nothing has marked it as dealt with.
|
|
232
|
-
*/
|
|
233
|
-
var perPoll = {
|
|
234
|
-
key: "maxPerPoll",
|
|
235
|
-
kind: "select",
|
|
236
|
-
label: "At most, per look",
|
|
237
|
-
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.",
|
|
238
|
-
options: [
|
|
239
|
-
{
|
|
240
|
-
value: "3",
|
|
241
|
-
label: "3 messages"
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
value: "10",
|
|
245
|
-
label: "10 messages"
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
value: "25",
|
|
249
|
-
label: "25 messages"
|
|
250
|
-
}
|
|
251
|
-
],
|
|
252
|
-
default: "10"
|
|
253
|
-
};
|
|
254
|
-
/**
|
|
255
|
-
* The setting that decides whether a quiet mailbox is quiet. Without it every
|
|
256
|
-
* receipt arrives as a notification saying a receipt arrived, which costs a
|
|
257
|
-
* run each and trains you to stop looking.
|
|
176
|
+
* Read, and send. Sending is only ever to the connected account itself, the
|
|
177
|
+
* way a bot answers whoever asked it; the agent never holds the token.
|
|
258
178
|
*/
|
|
259
|
-
var
|
|
260
|
-
key: "needsMeOnly",
|
|
261
|
-
kind: "boolean",
|
|
262
|
-
label: "Only tell me when it needs me",
|
|
263
|
-
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.",
|
|
264
|
-
default: true
|
|
265
|
-
};
|
|
266
|
-
var skipOwn = {
|
|
267
|
-
key: "skipOwn",
|
|
268
|
-
kind: "boolean",
|
|
269
|
-
label: "Skip mail I sent",
|
|
270
|
-
help: "Your own messages show up in threads you are part of, and being told about what you just sent is not useful.",
|
|
271
|
-
default: true
|
|
272
|
-
};
|
|
273
|
-
var TELL_ME_PROMPT = [
|
|
274
|
-
"Read the email in EMAIL.md and tell me what it says.",
|
|
275
|
-
"",
|
|
276
|
-
"You are writing a phone notification, not a summary document. Two or three",
|
|
277
|
-
"sentences. No headings, no bullet lists, no preamble like \"this email is\".",
|
|
278
|
-
"Lead with who it is from and what they want.",
|
|
279
|
-
"",
|
|
280
|
-
"Say plainly if it needs something from me and by when. If there is a",
|
|
281
|
-
"deadline, an amount of money, a date, or a link I have to act on, include it",
|
|
282
|
-
"exactly as written rather than describing it.",
|
|
283
|
-
"",
|
|
284
|
-
"If it is an automated notification, a newsletter, or otherwise needs nothing",
|
|
285
|
-
"from me, say so in one line and stop."
|
|
286
|
-
].join("\n");
|
|
179
|
+
var GMAIL_SCOPES = ["https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.send"];
|
|
287
180
|
var gmailManifest = defineManifest({
|
|
288
181
|
id: "gmail",
|
|
289
182
|
name: "Gmail",
|
|
290
|
-
tagline: "
|
|
183
|
+
tagline: "Start a loop when an email arrives, or email an answer to the connected account.",
|
|
291
184
|
docsUrl: "https://developers.google.com/gmail/api/guides",
|
|
292
185
|
icon: "Gmail",
|
|
293
186
|
accent: "bg-[#ea4335] text-white",
|
|
@@ -296,29 +189,67 @@ var gmailManifest = defineManifest({
|
|
|
296
189
|
scopes: GMAIL_SCOPES,
|
|
297
190
|
needsAppRegistration: true
|
|
298
191
|
},
|
|
299
|
-
|
|
192
|
+
triggers: [{
|
|
300
193
|
id: "gmail.new_mail",
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
trigger: "mail arrives in your inbox",
|
|
194
|
+
when: "An email arrives",
|
|
195
|
+
trigger: "an email arrives in the connected inbox",
|
|
304
196
|
watches: "in:inbox newer_than:1d",
|
|
305
|
-
writes: "a message telling you what arrived",
|
|
306
197
|
runsIn: "temp",
|
|
307
198
|
settings: [
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
199
|
+
{
|
|
200
|
+
key: "extraQuery",
|
|
201
|
+
kind: "text",
|
|
202
|
+
label: "Only mail matching",
|
|
203
|
+
help: "Gmail search terms, such as from:billing@vendor.com or subject:invoice.",
|
|
204
|
+
placeholder: "from:alerts@example.com"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: "needsMeOnly",
|
|
208
|
+
kind: "boolean",
|
|
209
|
+
label: "Skip mail that needs nothing",
|
|
210
|
+
help: "A quick look first decides which mail asks for something. The rest is kept, not run.",
|
|
211
|
+
default: true
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
key: "maxPerPoll",
|
|
215
|
+
kind: "select",
|
|
216
|
+
label: "At most, per check",
|
|
217
|
+
options: [
|
|
218
|
+
{
|
|
219
|
+
value: "3",
|
|
220
|
+
label: "3 emails"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
value: "10",
|
|
224
|
+
label: "10 emails"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
value: "25",
|
|
228
|
+
label: "25 emails"
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
default: "10"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
key: "skipOwn",
|
|
235
|
+
kind: "boolean",
|
|
236
|
+
label: "Skip mail sent from this account",
|
|
237
|
+
default: true
|
|
238
|
+
}
|
|
312
239
|
],
|
|
313
|
-
prompt: TELL_ME_PROMPT,
|
|
314
|
-
guidancePlaceholder: "What you care about in your mail. For example: I only need to know about anything involving money or a deadline.",
|
|
315
240
|
answer: "text",
|
|
316
|
-
actionConnectorId: "
|
|
317
|
-
actionId: "
|
|
318
|
-
actionTarget: { to: "me" },
|
|
241
|
+
actionConnectorId: "schedule",
|
|
242
|
+
actionId: "schedule.record",
|
|
319
243
|
pollEveryMs: 18e5
|
|
320
244
|
}],
|
|
321
|
-
|
|
245
|
+
workflows: [],
|
|
246
|
+
actions: [{
|
|
247
|
+
id: "gmail.send",
|
|
248
|
+
name: "Email this account",
|
|
249
|
+
summary: "Send the answer to the connected account's own inbox, in the same thread when it started from an email there.",
|
|
250
|
+
target: [],
|
|
251
|
+
accepts: ["text"]
|
|
252
|
+
}],
|
|
322
253
|
settings: [],
|
|
323
254
|
allowsMultipleAccounts: false,
|
|
324
255
|
byHand: { kind: "none" }
|
|
@@ -454,27 +385,20 @@ var at = {
|
|
|
454
385
|
key: "at",
|
|
455
386
|
kind: "text",
|
|
456
387
|
label: "At",
|
|
457
|
-
help: "
|
|
388
|
+
help: "24-hour time, in this host's time zone.",
|
|
458
389
|
placeholder: "09:00"
|
|
459
390
|
};
|
|
460
391
|
var weekday = {
|
|
461
392
|
key: "weekday",
|
|
462
393
|
kind: "select",
|
|
463
394
|
label: "On",
|
|
464
|
-
help: "
|
|
395
|
+
help: "For weekly loops.",
|
|
465
396
|
options: WEEKDAY_NAMES.map((name, index) => ({
|
|
466
397
|
value: String(index),
|
|
467
398
|
label: name
|
|
468
399
|
})),
|
|
469
400
|
default: "1"
|
|
470
401
|
};
|
|
471
|
-
var folder$1 = {
|
|
472
|
-
key: "folder",
|
|
473
|
-
kind: "text",
|
|
474
|
-
label: "Folder to work in",
|
|
475
|
-
help: "An absolute path. Nothing arrives to be looked at on a schedule, so this is the whole of what the agent is given.",
|
|
476
|
-
placeholder: "/Users/you/code/web"
|
|
477
|
-
};
|
|
478
402
|
/**
|
|
479
403
|
* Deliberately a frame rather than a job. Every other workflow ships a prompt
|
|
480
404
|
* because "review a pull request" is a problem worth solving once and getting
|
|
@@ -504,44 +428,175 @@ var scheduleManifest = defineManifest({
|
|
|
504
428
|
allowsMultipleAccounts: false,
|
|
505
429
|
byHand: { kind: "now" },
|
|
506
430
|
settings: [],
|
|
507
|
-
|
|
431
|
+
triggers: [{
|
|
508
432
|
id: "schedule.recurring",
|
|
509
|
-
|
|
510
|
-
summary: "Runs an agent in a folder you name, and keeps what it says.",
|
|
433
|
+
when: "On a schedule",
|
|
511
434
|
trigger: "the time you set comes round",
|
|
512
|
-
writes: "a note in the log",
|
|
513
435
|
runsIn: "folder",
|
|
514
436
|
settings: [
|
|
515
437
|
every,
|
|
516
438
|
at,
|
|
517
|
-
weekday
|
|
518
|
-
folder$1
|
|
439
|
+
weekday
|
|
519
440
|
],
|
|
520
|
-
prompt: RECURRING_PROMPT,
|
|
521
|
-
guidancePlaceholder: "Anything that holds every time this runs. For example: this repository releases on Thursdays, so say if anything is unmerged by Wednesday evening.",
|
|
522
441
|
answer: "text",
|
|
523
442
|
actionId: "schedule.record"
|
|
524
443
|
}],
|
|
444
|
+
workflows: [{
|
|
445
|
+
id: "schedule.recurring",
|
|
446
|
+
name: "Do something on a schedule",
|
|
447
|
+
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.",
|
|
448
|
+
triggerId: "schedule.recurring",
|
|
449
|
+
prompt: RECURRING_PROMPT
|
|
450
|
+
}],
|
|
525
451
|
actions: [{
|
|
526
452
|
id: "schedule.record",
|
|
527
|
-
name: "Keep
|
|
528
|
-
summary: "
|
|
453
|
+
name: "Keep in Inbox",
|
|
454
|
+
summary: "Keeps the answer in Inbox and sends it nowhere.",
|
|
529
455
|
target: [],
|
|
530
456
|
accepts: ["text", "review"]
|
|
531
457
|
}]
|
|
532
458
|
});
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
459
|
+
var slackManifest = defineManifest({
|
|
460
|
+
id: "slack",
|
|
461
|
+
name: "Slack bot",
|
|
462
|
+
tagline: "Paste a bot token from a Slack app — not a Slack login. @mention or DM the bot, and it answers in the thread.",
|
|
463
|
+
docsUrl: "https://api.slack.com/apps",
|
|
464
|
+
icon: "Slack",
|
|
465
|
+
accent: "bg-[#4a154b] text-white",
|
|
466
|
+
auth: {
|
|
467
|
+
kind: "token",
|
|
468
|
+
helpUrl: "https://api.slack.com/apps",
|
|
469
|
+
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.",
|
|
470
|
+
fields: [{
|
|
471
|
+
key: "botToken",
|
|
472
|
+
label: "Bot User OAuth Token",
|
|
473
|
+
placeholder: "xoxb-...",
|
|
474
|
+
secret: true
|
|
475
|
+
}]
|
|
476
|
+
},
|
|
477
|
+
triggers: [{
|
|
478
|
+
id: "slack.ask",
|
|
479
|
+
when: "The Slack bot is mentioned",
|
|
480
|
+
trigger: "someone @mentions the Slack bot in a channel it is in, or DMs it",
|
|
481
|
+
runsIn: "folder",
|
|
482
|
+
settings: [],
|
|
483
|
+
answer: "text",
|
|
484
|
+
actionId: "slack.reply",
|
|
485
|
+
pollEveryMs: 6e4
|
|
486
|
+
}],
|
|
487
|
+
workflows: [{
|
|
488
|
+
id: "slack.ask",
|
|
489
|
+
name: "Do what I ask the Slack bot",
|
|
490
|
+
summary: "Runs the agent when someone @mentions the Slack bot or DMs it, and answers in the thread.",
|
|
491
|
+
triggerId: "slack.ask",
|
|
492
|
+
prompt: [
|
|
493
|
+
"Someone has sent you a message in Slack. Do what it asks, working in this directory,",
|
|
494
|
+
"and then answer them.",
|
|
495
|
+
"",
|
|
496
|
+
"You are answering into Slack. Be brief: a couple of short paragraphs at most,",
|
|
497
|
+
"no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
|
|
498
|
+
"",
|
|
499
|
+
"If the message asks a question about this code, read enough of it to answer",
|
|
500
|
+
"properly rather than guessing. If it asks for a change, make the change and",
|
|
501
|
+
"say in one line what you did. If the message is too vague to act on, say what",
|
|
502
|
+
"you need to know instead of assuming."
|
|
503
|
+
].join("\n")
|
|
504
|
+
}],
|
|
505
|
+
actions: [{
|
|
506
|
+
id: "slack.reply",
|
|
507
|
+
name: "Reply in Slack",
|
|
508
|
+
summary: "Post a message back to the thread that asked, or to a channel you name.",
|
|
509
|
+
target: [{
|
|
510
|
+
key: "channel",
|
|
511
|
+
kind: "text",
|
|
512
|
+
label: "Channel",
|
|
513
|
+
help: "Empty replies where it was asked. Or a channel ID.",
|
|
514
|
+
placeholder: "C0123456789"
|
|
515
|
+
}],
|
|
516
|
+
accepts: ["text"]
|
|
517
|
+
}],
|
|
518
|
+
settings: [],
|
|
519
|
+
allowsMultipleAccounts: false,
|
|
520
|
+
byHand: { kind: "none" }
|
|
521
|
+
});
|
|
522
|
+
var feishuManifest = defineManifest({
|
|
523
|
+
id: "feishu",
|
|
524
|
+
name: "Feishu / Lark bot",
|
|
525
|
+
tagline: "Paste App ID and App Secret from a Feishu or Lark app — not a Feishu login. @mention the bot in a group, and it answers in the thread.",
|
|
526
|
+
docsUrl: "https://open.feishu.cn/app",
|
|
527
|
+
icon: "Feishu",
|
|
528
|
+
accent: "bg-[#3370ff] text-white",
|
|
529
|
+
auth: {
|
|
530
|
+
kind: "token",
|
|
531
|
+
helpUrl: "https://open.feishu.cn/app",
|
|
532
|
+
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).",
|
|
533
|
+
fields: [
|
|
534
|
+
{
|
|
535
|
+
key: "appId",
|
|
536
|
+
label: "App ID",
|
|
537
|
+
placeholder: "cli_...",
|
|
538
|
+
secret: false
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
key: "appSecret",
|
|
542
|
+
label: "App Secret",
|
|
543
|
+
placeholder: "App Secret",
|
|
544
|
+
secret: true
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
key: "host",
|
|
548
|
+
label: "Open platform",
|
|
549
|
+
placeholder: "feishu or lark",
|
|
550
|
+
secret: false,
|
|
551
|
+
optional: true
|
|
552
|
+
}
|
|
553
|
+
]
|
|
554
|
+
},
|
|
555
|
+
triggers: [{
|
|
556
|
+
id: "feishu.ask",
|
|
557
|
+
when: "The Feishu / Lark bot is mentioned",
|
|
558
|
+
trigger: "someone @mentions the Feishu / Lark bot in a group it is in",
|
|
559
|
+
runsIn: "folder",
|
|
560
|
+
settings: [],
|
|
561
|
+
answer: "text",
|
|
562
|
+
actionId: "feishu.reply",
|
|
563
|
+
pollEveryMs: 6e4
|
|
564
|
+
}],
|
|
565
|
+
workflows: [{
|
|
566
|
+
id: "feishu.ask",
|
|
567
|
+
name: "Do what I ask the Feishu / Lark bot",
|
|
568
|
+
summary: "Runs the agent when someone @mentions the Feishu / Lark bot in a group, and answers in the thread.",
|
|
569
|
+
triggerId: "feishu.ask",
|
|
570
|
+
prompt: [
|
|
571
|
+
"Someone has sent you a message in Feishu. Do what it asks, working in this directory,",
|
|
572
|
+
"and then answer them.",
|
|
573
|
+
"",
|
|
574
|
+
"You are answering into Feishu. Be brief: a couple of short paragraphs at most,",
|
|
575
|
+
"no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
|
|
576
|
+
"",
|
|
577
|
+
"If the message asks a question about this code, read enough of it to answer",
|
|
578
|
+
"properly rather than guessing. If it asks for a change, make the change and",
|
|
579
|
+
"say in one line what you did. If the message is too vague to act on, say what",
|
|
580
|
+
"you need to know instead of assuming."
|
|
581
|
+
].join("\n")
|
|
582
|
+
}],
|
|
583
|
+
actions: [{
|
|
584
|
+
id: "feishu.reply",
|
|
585
|
+
name: "Reply in Feishu / Lark",
|
|
586
|
+
summary: "Post a message back to the thread that asked, or to a chat you name.",
|
|
587
|
+
target: [{
|
|
588
|
+
key: "chat",
|
|
589
|
+
kind: "text",
|
|
590
|
+
label: "Chat ID",
|
|
591
|
+
help: "Empty replies where it was asked. Or a chat ID (oc_…).",
|
|
592
|
+
placeholder: "oc_..."
|
|
593
|
+
}],
|
|
594
|
+
accepts: ["text"]
|
|
595
|
+
}],
|
|
596
|
+
settings: [],
|
|
597
|
+
allowsMultipleAccounts: false,
|
|
598
|
+
byHand: { kind: "none" }
|
|
599
|
+
});
|
|
545
600
|
/**
|
|
546
601
|
* A bot bound to your account can be messaged by other people, and this
|
|
547
602
|
* workflow runs a coding agent on a runner. Answering only yourself is the
|
|
@@ -571,7 +626,7 @@ var recipient = {
|
|
|
571
626
|
key: "to",
|
|
572
627
|
kind: "select",
|
|
573
628
|
label: "Who to send it to",
|
|
574
|
-
help: "
|
|
629
|
+
help: "\"Me\" only arrives if you messaged the bot in the last half day.",
|
|
575
630
|
options: [{
|
|
576
631
|
value: "source",
|
|
577
632
|
label: "Whoever asked"
|
|
@@ -600,6 +655,8 @@ var ASK_PROMPT = [
|
|
|
600
655
|
*/
|
|
601
656
|
var CONNECTOR_MANIFESTS = [
|
|
602
657
|
githubManifest,
|
|
658
|
+
slackManifest,
|
|
659
|
+
feishuManifest,
|
|
603
660
|
gmailManifest,
|
|
604
661
|
{
|
|
605
662
|
id: "wechat",
|
|
@@ -612,19 +669,22 @@ var CONNECTOR_MANIFESTS = [
|
|
|
612
669
|
kind: "qr_scan",
|
|
613
670
|
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."
|
|
614
671
|
},
|
|
615
|
-
|
|
672
|
+
triggers: [{
|
|
616
673
|
id: "wechat.ask",
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
trigger: "you send the bot a message",
|
|
620
|
-
writes: "a reply in the chat",
|
|
674
|
+
when: "The WeChat bot gets a message",
|
|
675
|
+
trigger: "someone sends the WeChat bot a message",
|
|
621
676
|
runsIn: "folder",
|
|
622
|
-
settings: [
|
|
623
|
-
prompt: ASK_PROMPT,
|
|
624
|
-
guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
|
|
677
|
+
settings: [askers],
|
|
625
678
|
answer: "text",
|
|
626
679
|
actionId: "wechat.reply"
|
|
627
680
|
}],
|
|
681
|
+
workflows: [{
|
|
682
|
+
id: "wechat.ask",
|
|
683
|
+
name: "Do what I ask the WeChat bot",
|
|
684
|
+
summary: "Runs the agent when someone messages the WeChat bot, and answers in the chat.",
|
|
685
|
+
triggerId: "wechat.ask",
|
|
686
|
+
prompt: ASK_PROMPT
|
|
687
|
+
}],
|
|
628
688
|
actions: [{
|
|
629
689
|
id: "wechat.reply",
|
|
630
690
|
name: "Reply in the chat",
|
|
@@ -642,26 +702,106 @@ function connectorManifest(id) {
|
|
|
642
702
|
return CONNECTOR_MANIFESTS.find((manifest) => manifest.id === id);
|
|
643
703
|
}
|
|
644
704
|
/**
|
|
645
|
-
* Whether this connector is somewhere you have
|
|
705
|
+
* Whether this connector is somewhere you have a Connection.
|
|
646
706
|
*
|
|
647
707
|
* Asked in three places that would otherwise each assume every connector is:
|
|
648
|
-
* the page listing
|
|
708
|
+
* the page listing connections, the check for what stands between a loop and
|
|
649
709
|
* running, and the fetch of a credential for a run.
|
|
650
710
|
*/
|
|
651
711
|
function needsAccount(id) {
|
|
652
712
|
return connectorManifest(id)?.auth.kind !== "none";
|
|
653
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* What one Connection of this connector is called in the UI.
|
|
716
|
+
* A Slack or Feishu bot is a pasted app credential, not a login.
|
|
717
|
+
*/
|
|
718
|
+
function connectionNoun(id) {
|
|
719
|
+
return connectorManifest(id)?.auth.kind === "token" ? "bot" : "account";
|
|
720
|
+
}
|
|
721
|
+
/** A trigger with nothing on top: what a custom loop runs. */
|
|
722
|
+
function customOn(trigger) {
|
|
723
|
+
return {
|
|
724
|
+
...trigger,
|
|
725
|
+
triggerId: trigger.id,
|
|
726
|
+
name: trigger.when,
|
|
727
|
+
summary: `When ${trigger.trigger}.`,
|
|
728
|
+
prompt: "",
|
|
729
|
+
custom: true
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* What a loop runs, by the id it stores: a workflow, merged with the trigger
|
|
734
|
+
* it sits on, or a trigger alone for a custom loop.
|
|
735
|
+
*/
|
|
654
736
|
function connectorWorkflow(connectorId, workflowId) {
|
|
655
|
-
|
|
737
|
+
const manifest = connectorManifest(connectorId);
|
|
738
|
+
if (!manifest) return void 0;
|
|
739
|
+
const defined = manifest.workflows.find((entry) => entry.id === workflowId);
|
|
740
|
+
if (defined) {
|
|
741
|
+
const trigger = manifest.triggers.find((entry) => entry.id === defined.triggerId);
|
|
742
|
+
if (!trigger) return void 0;
|
|
743
|
+
return {
|
|
744
|
+
...trigger,
|
|
745
|
+
...defined,
|
|
746
|
+
actionId: defined.actionId ?? trigger.actionId,
|
|
747
|
+
actionConnectorId: defined.actionConnectorId ?? trigger.actionConnectorId,
|
|
748
|
+
actionTarget: defined.actionTarget ?? trigger.actionTarget,
|
|
749
|
+
pollEveryMs: defined.pollEveryMs ?? trigger.pollEveryMs,
|
|
750
|
+
triggerId: trigger.id,
|
|
751
|
+
custom: false
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
const trigger = manifest.triggers.find((entry) => entry.id === workflowId);
|
|
755
|
+
return trigger ? customOn(trigger) : void 0;
|
|
656
756
|
}
|
|
657
757
|
function connectorAction(connectorId, actionId) {
|
|
658
758
|
return connectorManifest(connectorId)?.actions.find((entry) => entry.id === actionId);
|
|
659
759
|
}
|
|
660
|
-
/**
|
|
760
|
+
/**
|
|
761
|
+
* Keeping the answer in Inbox is the clock's action, because the clock is the
|
|
762
|
+
* one connector with nowhere of its own to write. To a person it is not a
|
|
763
|
+
* Schedule thing at all, so it is named for where the answer stays.
|
|
764
|
+
*/
|
|
765
|
+
var INBOX_ONLY = {
|
|
766
|
+
connectorId: "schedule",
|
|
767
|
+
actionId: "schedule.record"
|
|
768
|
+
};
|
|
769
|
+
function isInboxOnly(connectorId, actionId) {
|
|
770
|
+
return connectorId === INBOX_ONLY.connectorId && actionId === INBOX_ONLY.actionId;
|
|
771
|
+
}
|
|
772
|
+
/** What to call a connector as a place answers go. */
|
|
773
|
+
function writerName(connectorId) {
|
|
774
|
+
if (connectorId === INBOX_ONLY.connectorId) return "Inbox";
|
|
775
|
+
return connectorManifest(connectorId)?.name ?? connectorId;
|
|
776
|
+
}
|
|
777
|
+
/** Where a loop's answer goes, in a few words: "GitHub · Submit a review", or "Inbox only". */
|
|
778
|
+
function destinationLabel(connectorId, actionId) {
|
|
779
|
+
if (isInboxOnly(connectorId, actionId)) return "Inbox only";
|
|
780
|
+
const action = connectorAction(connectorId, actionId);
|
|
781
|
+
return action ? `${writerName(connectorId)} · ${action.name}` : "No destination";
|
|
782
|
+
}
|
|
783
|
+
/** Connectors that can write, Inbox first, for the question of where an answer goes. */
|
|
784
|
+
function writers() {
|
|
785
|
+
const all = CONNECTOR_MANIFESTS.filter((entry) => entry.actions.length > 0);
|
|
786
|
+
return [...all.filter((entry) => entry.id === INBOX_ONLY.connectorId), ...all.filter((entry) => entry.id !== INBOX_ONLY.connectorId)];
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Every workflow on offer, for the page that asks which one to turn on.
|
|
790
|
+
*
|
|
791
|
+
* The clock leads: it needs no account, and a scheduled job is the easiest
|
|
792
|
+
* first loop to try. The rest follow the connectors' own order.
|
|
793
|
+
*/
|
|
661
794
|
function allWorkflows() {
|
|
662
|
-
return CONNECTOR_MANIFESTS.flatMap((connector) => connector.workflows.map((
|
|
795
|
+
return [...CONNECTOR_MANIFESTS.filter((connector) => connector.id === "schedule"), ...CONNECTOR_MANIFESTS.filter((connector) => connector.id !== "schedule")].flatMap((connector) => connector.workflows.map((entry) => ({
|
|
796
|
+
connector,
|
|
797
|
+
workflow: connectorWorkflow(connector.id, entry.id)
|
|
798
|
+
})));
|
|
799
|
+
}
|
|
800
|
+
/** Every trigger on offer, the clock first, for choosing what starts a custom loop. */
|
|
801
|
+
function allTriggers() {
|
|
802
|
+
return [...CONNECTOR_MANIFESTS.filter((connector) => connector.id === "schedule"), ...CONNECTOR_MANIFESTS.filter((connector) => connector.id !== "schedule")].flatMap((connector) => connector.triggers.map((trigger) => ({
|
|
663
803
|
connector,
|
|
664
|
-
|
|
804
|
+
trigger
|
|
665
805
|
})));
|
|
666
806
|
}
|
|
667
807
|
/**
|
|
@@ -1124,15 +1264,15 @@ function candidates(items, repositories) {
|
|
|
1124
1264
|
});
|
|
1125
1265
|
}
|
|
1126
1266
|
/**
|
|
1127
|
-
* What the
|
|
1267
|
+
* What the trigger says it watches for, which is what gets asked. Taking the
|
|
1128
1268
|
* query from the manifest is the point: it is shown on the loop page, and a
|
|
1129
1269
|
* search that quietly differed from the one on screen would be the worst kind
|
|
1130
1270
|
* of wrong.
|
|
1131
1271
|
*/
|
|
1132
|
-
function query(
|
|
1133
|
-
const
|
|
1134
|
-
if (!
|
|
1135
|
-
return
|
|
1272
|
+
function query(triggerId) {
|
|
1273
|
+
const trigger = githubManifest.triggers.find((entry) => entry.id === triggerId);
|
|
1274
|
+
if (!trigger?.watches) throw new Error(`GitHub cannot watch for ${triggerId}.`);
|
|
1275
|
+
return trigger.watches;
|
|
1136
1276
|
}
|
|
1137
1277
|
async function pollGithub(input) {
|
|
1138
1278
|
const repositories = stringList(input.settings.repositories);
|
|
@@ -1405,13 +1545,18 @@ var githubRuntime = defineRuntime({
|
|
|
1405
1545
|
}
|
|
1406
1546
|
});
|
|
1407
1547
|
var BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
1408
|
-
async function request(accessToken, path) {
|
|
1548
|
+
async function request(accessToken, path, init = {}) {
|
|
1409
1549
|
let res;
|
|
1410
1550
|
try {
|
|
1411
|
-
res = await fetch(`${BASE}${path}`, {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1551
|
+
res = await fetch(`${BASE}${path}`, {
|
|
1552
|
+
method: init.method ?? "GET",
|
|
1553
|
+
headers: {
|
|
1554
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1555
|
+
Accept: "application/json",
|
|
1556
|
+
...init.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
1557
|
+
},
|
|
1558
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
|
|
1559
|
+
});
|
|
1415
1560
|
} catch (error) {
|
|
1416
1561
|
throw new TransientError(`Gmail could not be reached: ${error instanceof Error ? error.message : String(error)}`);
|
|
1417
1562
|
}
|
|
@@ -1420,6 +1565,7 @@ async function request(accessToken, path) {
|
|
|
1420
1565
|
const message = `Gmail ${res.status} on ${path}: ${detail}`;
|
|
1421
1566
|
const backoff = res.status === 403 && /rateLimitExceeded|userRateLimitExceeded/i.test(detail);
|
|
1422
1567
|
if (res.status === 429 || res.status >= 500 || backoff) throw new TransientError(message);
|
|
1568
|
+
if (res.status === 403 && /insufficient.*scope|ACCESS_TOKEN_SCOPE_INSUFFICIENT/i.test(detail)) throw new Error("This Gmail connection cannot send mail. Reconnect Gmail to allow it.");
|
|
1423
1569
|
throw new Error(message);
|
|
1424
1570
|
}
|
|
1425
1571
|
return await res.json();
|
|
@@ -1456,6 +1602,16 @@ function getMessageHeaders(accessToken, id) {
|
|
|
1456
1602
|
function getMessage(accessToken, id) {
|
|
1457
1603
|
return request(accessToken, `/messages/${id}?format=full`);
|
|
1458
1604
|
}
|
|
1605
|
+
/** Send a message already written as RFC 2822 text, in a thread when one is given. */
|
|
1606
|
+
function sendMessage$2(accessToken, raw, threadId) {
|
|
1607
|
+
return request(accessToken, "/messages/send", {
|
|
1608
|
+
method: "POST",
|
|
1609
|
+
body: threadId ? {
|
|
1610
|
+
raw,
|
|
1611
|
+
threadId
|
|
1612
|
+
} : { raw }
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1459
1615
|
/** A Gmail message id, as a ref: short, stable, and unique to the mailbox. */
|
|
1460
1616
|
function mailRef(id) {
|
|
1461
1617
|
return `mail#${id}`;
|
|
@@ -1464,7 +1620,7 @@ function mailRef(id) {
|
|
|
1464
1620
|
* Where to send someone who wants to see the mail itself. Gmail has no
|
|
1465
1621
|
* per-message API link, but the web client will open one by id.
|
|
1466
1622
|
*/
|
|
1467
|
-
function permalink(id) {
|
|
1623
|
+
function permalink$2(id) {
|
|
1468
1624
|
return `https://mail.google.com/mail/u/0/#all/${id}`;
|
|
1469
1625
|
}
|
|
1470
1626
|
function headerOf(message, name) {
|
|
@@ -1532,7 +1688,7 @@ function attachmentNames(message) {
|
|
|
1532
1688
|
return names;
|
|
1533
1689
|
}
|
|
1534
1690
|
/** Enough of a message to pick it out of a list. */
|
|
1535
|
-
function summarise$
|
|
1691
|
+
function summarise$3(message) {
|
|
1536
1692
|
const subject = headerOf(message, "Subject") || "(no subject)";
|
|
1537
1693
|
const who = senderName(headerOf(message, "From"));
|
|
1538
1694
|
const title = who ? `${who}: ${subject}` : subject;
|
|
@@ -1567,6 +1723,30 @@ function mailContext(message) {
|
|
|
1567
1723
|
].join("\n")
|
|
1568
1724
|
};
|
|
1569
1725
|
}
|
|
1726
|
+
/** Base64url, as Gmail wants a raw message. */
|
|
1727
|
+
function base64url(text) {
|
|
1728
|
+
return Buffer.from(text, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1729
|
+
}
|
|
1730
|
+
/** A header value that may hold any text, encoded so non-ASCII survives. */
|
|
1731
|
+
function encodedWord(text) {
|
|
1732
|
+
return /^[\x20-\x7e]*$/.test(text) ? text : `=?UTF-8?B?${Buffer.from(text, "utf8").toString("base64")}?=`;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* A plain-text email as Gmail's `raw`. The body is base64 so any line length
|
|
1736
|
+
* and any language go through untouched.
|
|
1737
|
+
*/
|
|
1738
|
+
function composeMail(input) {
|
|
1739
|
+
return base64url([
|
|
1740
|
+
`To: ${input.to}`,
|
|
1741
|
+
`Subject: ${encodedWord(input.subject)}`,
|
|
1742
|
+
...input.inReplyTo ? [`In-Reply-To: ${input.inReplyTo}`, `References: ${input.inReplyTo}`] : [],
|
|
1743
|
+
"MIME-Version: 1.0",
|
|
1744
|
+
"Content-Type: text/plain; charset=\"UTF-8\"",
|
|
1745
|
+
"Content-Transfer-Encoding: base64",
|
|
1746
|
+
"",
|
|
1747
|
+
Buffer.from(input.body, "utf8").toString("base64").replace(/.{76}/g, "$&\r\n")
|
|
1748
|
+
].join("\r\n"));
|
|
1749
|
+
}
|
|
1570
1750
|
/**
|
|
1571
1751
|
* What the workflow says it watches for, which is what gets asked. Taken from
|
|
1572
1752
|
* the manifest rather than written again here, because it is shown on the loop
|
|
@@ -1574,9 +1754,9 @@ function mailContext(message) {
|
|
|
1574
1754
|
* worst kind of wrong.
|
|
1575
1755
|
*/
|
|
1576
1756
|
function baseQuery(workflowId) {
|
|
1577
|
-
const
|
|
1578
|
-
if (!
|
|
1579
|
-
return
|
|
1757
|
+
const trigger = gmailManifest.triggers.find((entry) => entry.id === workflowId);
|
|
1758
|
+
if (!trigger?.watches) throw new Error(`Gmail cannot watch for ${workflowId}.`);
|
|
1759
|
+
return trigger.watches;
|
|
1580
1760
|
}
|
|
1581
1761
|
/**
|
|
1582
1762
|
* Missing means "the workflow's default", not "off". A loop saved before a
|
|
@@ -1611,8 +1791,8 @@ async function pollGmail(input) {
|
|
|
1611
1791
|
key: mailRef(id),
|
|
1612
1792
|
kind: "email",
|
|
1613
1793
|
ref: mailRef(id),
|
|
1614
|
-
title: summarise$
|
|
1615
|
-
url: permalink(id),
|
|
1794
|
+
title: summarise$3(message),
|
|
1795
|
+
url: permalink$2(id),
|
|
1616
1796
|
preview: (message.snippet ?? "").trim() || void 0,
|
|
1617
1797
|
payload: { messageId: id }
|
|
1618
1798
|
});
|
|
@@ -1737,14 +1917,14 @@ async function usableToken(credential) {
|
|
|
1737
1917
|
};
|
|
1738
1918
|
}
|
|
1739
1919
|
/** The mailbox, as a person would recognise it. */
|
|
1740
|
-
function accountOf$
|
|
1920
|
+
function accountOf$3(emailAddress) {
|
|
1741
1921
|
return {
|
|
1742
1922
|
id: emailAddress,
|
|
1743
1923
|
label: emailAddress
|
|
1744
1924
|
};
|
|
1745
1925
|
}
|
|
1746
1926
|
/**
|
|
1747
|
-
* Which message this task is about.
|
|
1927
|
+
* Which message this task is about. A task keeps the id, and a pasted link
|
|
1748
1928
|
* carries one too, so either is enough to fetch the mail again.
|
|
1749
1929
|
*/
|
|
1750
1930
|
function messageIdFrom(url, payload) {
|
|
@@ -1778,8 +1958,8 @@ var gmailRuntime = defineRuntime({
|
|
|
1778
1958
|
return {
|
|
1779
1959
|
kind: "email",
|
|
1780
1960
|
ref: mailRef(message.id),
|
|
1781
|
-
title: summarise$
|
|
1782
|
-
url: permalink(message.id),
|
|
1961
|
+
title: summarise$3(message),
|
|
1962
|
+
url: permalink$2(message.id),
|
|
1783
1963
|
context: [mailContext(message)]
|
|
1784
1964
|
};
|
|
1785
1965
|
},
|
|
@@ -1792,6 +1972,33 @@ var gmailRuntime = defineRuntime({
|
|
|
1792
1972
|
emailAddress: (await getProfile(accessToken)).emailAddress
|
|
1793
1973
|
}) };
|
|
1794
1974
|
},
|
|
1975
|
+
/**
|
|
1976
|
+
* Answers go to the connected account itself, the way a bot answers
|
|
1977
|
+
* whoever asked it. When the run started from an email in this inbox, the
|
|
1978
|
+
* answer joins that thread.
|
|
1979
|
+
*/
|
|
1980
|
+
async applyAction({ actionId, source, body, credential }) {
|
|
1981
|
+
if (actionId !== "gmail.send") throw new Error(`Gmail cannot ${actionId}.`);
|
|
1982
|
+
const { accessToken } = await usableToken(credential);
|
|
1983
|
+
const profile = await getProfile(accessToken);
|
|
1984
|
+
let subject = source.title || "From Loopable";
|
|
1985
|
+
let threadId;
|
|
1986
|
+
let inReplyTo;
|
|
1987
|
+
const sourceId = source.connectorId === "gmail" ? source.ref.replace(/^mail#/, "") : "";
|
|
1988
|
+
if (sourceId) {
|
|
1989
|
+
const original = await getMessage(accessToken, sourceId);
|
|
1990
|
+
threadId = original.threadId;
|
|
1991
|
+
inReplyTo = headerOf(original, "Message-ID") || void 0;
|
|
1992
|
+
const said = headerOf(original, "Subject");
|
|
1993
|
+
if (said) subject = /^re:/i.test(said) ? said : `Re: ${said}`;
|
|
1994
|
+
}
|
|
1995
|
+
return { url: permalink$2((await sendMessage$2(accessToken, composeMail({
|
|
1996
|
+
to: profile.emailAddress,
|
|
1997
|
+
subject,
|
|
1998
|
+
body,
|
|
1999
|
+
inReplyTo
|
|
2000
|
+
}), threadId)).id) };
|
|
2001
|
+
},
|
|
1795
2002
|
auth: {
|
|
1796
2003
|
async startAuthorization({ redirectUri, state }) {
|
|
1797
2004
|
const registration = requireRegistration();
|
|
@@ -1817,12 +2024,12 @@ var gmailRuntime = defineRuntime({
|
|
|
1817
2024
|
});
|
|
1818
2025
|
return {
|
|
1819
2026
|
credential,
|
|
1820
|
-
account: accountOf$
|
|
2027
|
+
account: accountOf$3((await getProfile(credential.accessToken)).emailAddress)
|
|
1821
2028
|
};
|
|
1822
2029
|
},
|
|
1823
2030
|
async identity(credential) {
|
|
1824
2031
|
const { accessToken, refreshed } = await usableToken(credential);
|
|
1825
|
-
const account = accountOf$
|
|
2032
|
+
const account = accountOf$3((await getProfile(accessToken)).emailAddress);
|
|
1826
2033
|
return refreshed ? {
|
|
1827
2034
|
account,
|
|
1828
2035
|
renewedCredential: refreshed
|
|
@@ -1930,6 +2137,643 @@ var scheduleRuntime = {
|
|
|
1930
2137
|
}
|
|
1931
2138
|
};
|
|
1932
2139
|
/**
|
|
2140
|
+
* Slack Web API, called with a bot token pasted from a workspace-installed app.
|
|
2141
|
+
* Loopable polls; Slack never has to reach this machine.
|
|
2142
|
+
*/
|
|
2143
|
+
var API$1 = "https://slack.com/api";
|
|
2144
|
+
async function slack(path, token, init = {}) {
|
|
2145
|
+
const url = new URL(`${API$1}/${path}`);
|
|
2146
|
+
for (const [key, value] of Object.entries(init.query ?? {})) url.searchParams.set(key, value);
|
|
2147
|
+
let response;
|
|
2148
|
+
try {
|
|
2149
|
+
response = await fetch(url.href, {
|
|
2150
|
+
method: init.method ?? "GET",
|
|
2151
|
+
headers: {
|
|
2152
|
+
Authorization: `Bearer ${token}`,
|
|
2153
|
+
...init.body ? { "Content-Type": "application/json" } : {}
|
|
2154
|
+
},
|
|
2155
|
+
body: init.body ? JSON.stringify(init.body) : void 0
|
|
2156
|
+
});
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
throw new TransientError(`Slack is unreachable: ${error.message}`);
|
|
2159
|
+
}
|
|
2160
|
+
if (response.status >= 500 || response.status === 429) throw new TransientError(`Slack answered ${response.status}`);
|
|
2161
|
+
if (!response.ok) throw new Error(`Slack answered ${response.status}`);
|
|
2162
|
+
return await response.json();
|
|
2163
|
+
}
|
|
2164
|
+
function refuse$1(error) {
|
|
2165
|
+
if (error === "invalid_auth" || error === "not_authed" || error === "token_revoked" || error === "account_inactive") throw new Error("Slack did not accept that token.");
|
|
2166
|
+
throw new Error(error ? `Slack refused: ${error}` : "Slack refused the request.");
|
|
2167
|
+
}
|
|
2168
|
+
async function authTest(botToken) {
|
|
2169
|
+
const body = await slack("auth.test", botToken);
|
|
2170
|
+
if (!body.ok) refuse$1(body.error);
|
|
2171
|
+
const teamUrl = typeof body.url === "string" ? body.url : "";
|
|
2172
|
+
const team = typeof body.team === "string" ? body.team : "";
|
|
2173
|
+
const teamId = typeof body.team_id === "string" ? body.team_id : "";
|
|
2174
|
+
const botUserId = typeof body.user_id === "string" ? body.user_id : "";
|
|
2175
|
+
const botId = typeof body.bot_id === "string" ? body.bot_id : "";
|
|
2176
|
+
const botName = typeof body.user === "string" ? body.user : "";
|
|
2177
|
+
if (!teamId || !botUserId) throw new Error("Slack did not return which workspace this bot belongs to.");
|
|
2178
|
+
return {
|
|
2179
|
+
botToken,
|
|
2180
|
+
teamId,
|
|
2181
|
+
team,
|
|
2182
|
+
teamUrl,
|
|
2183
|
+
botUserId,
|
|
2184
|
+
botId,
|
|
2185
|
+
botName
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
async function listConversations(credential) {
|
|
2189
|
+
const body = await slack("conversations.list", credential.botToken, { query: {
|
|
2190
|
+
types: "public_channel,private_channel,im,mpim",
|
|
2191
|
+
exclude_archived: "true",
|
|
2192
|
+
limit: "200"
|
|
2193
|
+
} });
|
|
2194
|
+
if (!body.ok) refuse$1(body.error);
|
|
2195
|
+
return (Array.isArray(body.channels) ? body.channels : []).map((raw) => {
|
|
2196
|
+
const channel = raw;
|
|
2197
|
+
if (!channel.id) return null;
|
|
2198
|
+
return {
|
|
2199
|
+
id: channel.id,
|
|
2200
|
+
name: channel.name ?? "",
|
|
2201
|
+
isIm: channel.is_im === true
|
|
2202
|
+
};
|
|
2203
|
+
}).filter((channel) => channel !== null);
|
|
2204
|
+
}
|
|
2205
|
+
async function history$1(credential, channelId, oldest) {
|
|
2206
|
+
const query = {
|
|
2207
|
+
channel: channelId,
|
|
2208
|
+
limit: "100",
|
|
2209
|
+
inclusive: "false"
|
|
2210
|
+
};
|
|
2211
|
+
if (oldest) query.oldest = oldest;
|
|
2212
|
+
const body = await slack("conversations.history", credential.botToken, { query });
|
|
2213
|
+
if (!body.ok) {
|
|
2214
|
+
if (body.error === "not_in_channel" || body.error === "channel_not_found") return [];
|
|
2215
|
+
refuse$1(body.error);
|
|
2216
|
+
}
|
|
2217
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
2218
|
+
const parsed = [];
|
|
2219
|
+
for (const raw of messages) {
|
|
2220
|
+
const message = raw;
|
|
2221
|
+
if (!message.ts) continue;
|
|
2222
|
+
parsed.push({
|
|
2223
|
+
ts: message.ts,
|
|
2224
|
+
...message.user ? { user: message.user } : {},
|
|
2225
|
+
...message.bot_id ? { botId: message.bot_id } : {},
|
|
2226
|
+
text: message.text ?? "",
|
|
2227
|
+
...message.thread_ts ? { threadTs: message.thread_ts } : {}
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
return parsed.sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
2231
|
+
}
|
|
2232
|
+
async function postMessage(credential, input) {
|
|
2233
|
+
const body = await slack("chat.postMessage", credential.botToken, {
|
|
2234
|
+
method: "POST",
|
|
2235
|
+
body: {
|
|
2236
|
+
channel: input.channel,
|
|
2237
|
+
text: input.text,
|
|
2238
|
+
...input.threadTs ? { thread_ts: input.threadTs } : {}
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
if (!body.ok) refuse$1(body.error);
|
|
2242
|
+
const ts = typeof body.ts === "string" ? body.ts : "";
|
|
2243
|
+
const channel = typeof body.channel === "string" ? body.channel : input.channel;
|
|
2244
|
+
if (!ts) throw new Error("Slack accepted the message but did not say where it landed.");
|
|
2245
|
+
return {
|
|
2246
|
+
ts,
|
|
2247
|
+
channel
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function permalink$1(credential, channel, ts) {
|
|
2251
|
+
return `${credential.teamUrl.replace(/\/$/, "")}/archives/${channel}/p${ts.replace(".", "")}`;
|
|
2252
|
+
}
|
|
2253
|
+
function asAsked$2(value) {
|
|
2254
|
+
const record = value;
|
|
2255
|
+
if (!record || typeof record.channel !== "string" || typeof record.ts !== "string") throw new Error("This task has lost the message it was about.");
|
|
2256
|
+
return {
|
|
2257
|
+
channel: record.channel,
|
|
2258
|
+
ts: record.ts,
|
|
2259
|
+
user: typeof record.user === "string" ? record.user : void 0,
|
|
2260
|
+
text: typeof record.text === "string" ? record.text : "",
|
|
2261
|
+
threadTs: typeof record.threadTs === "string" ? record.threadTs : record.ts
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
function asCursor$1(value) {
|
|
2265
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2266
|
+
const out = {};
|
|
2267
|
+
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") out[key] = entry;
|
|
2268
|
+
return out;
|
|
2269
|
+
}
|
|
2270
|
+
function summarise$2(text) {
|
|
2271
|
+
const line = text.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
2272
|
+
if (!line) return "Slack message";
|
|
2273
|
+
return line.length > 80 ? `${line.slice(0, 79)}…` : line;
|
|
2274
|
+
}
|
|
2275
|
+
function isAsk$1(input) {
|
|
2276
|
+
if (input.user === input.botUserId || input.messageBotId && input.messageBotId === input.botId) return false;
|
|
2277
|
+
if (input.isIm) return Boolean(input.text.trim());
|
|
2278
|
+
return input.text.includes(`<@${input.botUserId}>`);
|
|
2279
|
+
}
|
|
2280
|
+
function destination$1(target, source) {
|
|
2281
|
+
if (typeof target.channel === "string" && target.channel.trim()) return { channel: target.channel.trim() };
|
|
2282
|
+
if (source.connectorId !== "slack") throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
|
|
2283
|
+
const carry = source.carry ?? {};
|
|
2284
|
+
if (!carry.channel) throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
|
|
2285
|
+
return {
|
|
2286
|
+
channel: carry.channel,
|
|
2287
|
+
threadTs: carry.ts
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
function accountOf$2(credential) {
|
|
2291
|
+
return {
|
|
2292
|
+
id: credential.teamId,
|
|
2293
|
+
label: [credential.botName ? `@${credential.botName}` : null, credential.team || credential.teamId].filter(Boolean).join(" · "),
|
|
2294
|
+
url: credential.teamUrl || void 0
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
var slackRuntime = {
|
|
2298
|
+
async readiness() {
|
|
2299
|
+
return { ready: true };
|
|
2300
|
+
},
|
|
2301
|
+
async poll({ workflowId, credential, cursor }) {
|
|
2302
|
+
if (workflowId !== "slack.ask") throw new Error(`Slack cannot watch for ${workflowId}.`);
|
|
2303
|
+
const account = credential;
|
|
2304
|
+
const previous = asCursor$1(cursor);
|
|
2305
|
+
const channels = await listConversations(account);
|
|
2306
|
+
const next = { ...previous };
|
|
2307
|
+
const signals = [];
|
|
2308
|
+
for (const channel of channels) {
|
|
2309
|
+
const oldest = previous[channel.id];
|
|
2310
|
+
const messages = await history$1(account, channel.id, oldest);
|
|
2311
|
+
if (messages.length === 0) {
|
|
2312
|
+
if (!next[channel.id]) next[channel.id] = String(Date.now() / 1e3);
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
next[channel.id] = messages[messages.length - 1].ts;
|
|
2316
|
+
if (!oldest) continue;
|
|
2317
|
+
for (const message of messages) {
|
|
2318
|
+
if (!isAsk$1({
|
|
2319
|
+
text: message.text,
|
|
2320
|
+
user: message.user,
|
|
2321
|
+
messageBotId: message.botId,
|
|
2322
|
+
isIm: channel.isIm,
|
|
2323
|
+
botUserId: account.botUserId,
|
|
2324
|
+
botId: account.botId
|
|
2325
|
+
})) continue;
|
|
2326
|
+
const asked = {
|
|
2327
|
+
channel: channel.id,
|
|
2328
|
+
ts: message.ts,
|
|
2329
|
+
user: message.user,
|
|
2330
|
+
text: message.text,
|
|
2331
|
+
threadTs: message.threadTs ?? message.ts
|
|
2332
|
+
};
|
|
2333
|
+
signals.push({
|
|
2334
|
+
key: `${channel.id}:${message.ts}`,
|
|
2335
|
+
kind: "message",
|
|
2336
|
+
ref: `${channel.id}:${message.ts}`,
|
|
2337
|
+
title: summarise$2(message.text),
|
|
2338
|
+
url: permalink$1(account, channel.id, message.ts),
|
|
2339
|
+
payload: asked
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
return {
|
|
2344
|
+
signals,
|
|
2345
|
+
cursor: next
|
|
2346
|
+
};
|
|
2347
|
+
},
|
|
2348
|
+
async resolveWorkItem({ payload }) {
|
|
2349
|
+
const asked = asAsked$2(payload);
|
|
2350
|
+
return {
|
|
2351
|
+
kind: "message",
|
|
2352
|
+
ref: `${asked.channel}:${asked.ts}`,
|
|
2353
|
+
title: summarise$2(asked.text),
|
|
2354
|
+
url: "",
|
|
2355
|
+
carry: {
|
|
2356
|
+
channel: asked.channel,
|
|
2357
|
+
ts: asked.threadTs
|
|
2358
|
+
},
|
|
2359
|
+
context: [{
|
|
2360
|
+
name: "MESSAGE.md",
|
|
2361
|
+
body: [
|
|
2362
|
+
`# What you were asked`,
|
|
2363
|
+
``,
|
|
2364
|
+
asked.text,
|
|
2365
|
+
``
|
|
2366
|
+
].join("\n")
|
|
2367
|
+
}]
|
|
2368
|
+
};
|
|
2369
|
+
},
|
|
2370
|
+
async applyAction({ actionId, target, source, body, credential }) {
|
|
2371
|
+
if (actionId !== "slack.reply") throw new Error(`Slack cannot ${actionId}.`);
|
|
2372
|
+
const account = credential;
|
|
2373
|
+
const to = destination$1(target, source);
|
|
2374
|
+
const posted = await postMessage(account, {
|
|
2375
|
+
channel: to.channel,
|
|
2376
|
+
text: body.trim(),
|
|
2377
|
+
threadTs: to.threadTs
|
|
2378
|
+
});
|
|
2379
|
+
return { url: permalink$1(account, posted.channel, posted.ts) };
|
|
2380
|
+
},
|
|
2381
|
+
auth: {
|
|
2382
|
+
async connectWithFields(fields) {
|
|
2383
|
+
const botToken = fields.botToken?.trim() ?? "";
|
|
2384
|
+
if (!botToken.startsWith("xoxb-")) throw new Error("Paste the Bot User OAuth Token from your Slack app. It starts with xoxb-.");
|
|
2385
|
+
const credential = await authTest(botToken);
|
|
2386
|
+
return {
|
|
2387
|
+
credential,
|
|
2388
|
+
account: accountOf$2(credential)
|
|
2389
|
+
};
|
|
2390
|
+
},
|
|
2391
|
+
async identity(credential) {
|
|
2392
|
+
return { account: accountOf$2(await authTest(credential.botToken)) };
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
/**
|
|
2397
|
+
* Feishu / Lark Open API, called with a custom app's id and secret.
|
|
2398
|
+
* Loopable polls groups the bot is in; Feishu never has to reach this machine.
|
|
2399
|
+
*/
|
|
2400
|
+
var API = {
|
|
2401
|
+
feishu: "https://open.feishu.cn/open-apis",
|
|
2402
|
+
lark: "https://open.larksuite.com/open-apis"
|
|
2403
|
+
};
|
|
2404
|
+
var APPLINK = {
|
|
2405
|
+
feishu: "https://applink.feishu.cn/client/chat/open",
|
|
2406
|
+
lark: "https://applink.larksuite.com/client/chat/open"
|
|
2407
|
+
};
|
|
2408
|
+
function parseHost(value) {
|
|
2409
|
+
const raw = (value ?? "feishu").trim().toLowerCase();
|
|
2410
|
+
if (!raw || raw === "feishu" || raw === "feishu.cn" || raw.includes("feishu.cn")) return "feishu";
|
|
2411
|
+
if (raw === "lark" || raw === "larksuite" || raw.includes("larksuite.com") || raw.includes("larkoffice.com")) return "lark";
|
|
2412
|
+
throw new Error("Open platform must be feishu (China) or lark (international).");
|
|
2413
|
+
}
|
|
2414
|
+
function refuse(code, msg) {
|
|
2415
|
+
if (code === 10003 || code === 10012 || code === 10014) throw new Error("Feishu did not accept those app credentials.");
|
|
2416
|
+
throw new Error(msg ? `Feishu refused: ${msg}` : "Feishu refused the request.");
|
|
2417
|
+
}
|
|
2418
|
+
async function call$1(host, path, init = {}) {
|
|
2419
|
+
const url = new URL(`${API[host]}${path}`);
|
|
2420
|
+
for (const [key, value] of Object.entries(init.query ?? {})) url.searchParams.set(key, value);
|
|
2421
|
+
let response;
|
|
2422
|
+
try {
|
|
2423
|
+
response = await fetch(url.href, {
|
|
2424
|
+
method: init.method ?? "GET",
|
|
2425
|
+
headers: {
|
|
2426
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
2427
|
+
...init.token ? { Authorization: `Bearer ${init.token}` } : {}
|
|
2428
|
+
},
|
|
2429
|
+
body: init.body ? JSON.stringify(init.body) : void 0
|
|
2430
|
+
});
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
throw new TransientError(`Feishu is unreachable: ${error.message}`);
|
|
2433
|
+
}
|
|
2434
|
+
if (response.status >= 500 || response.status === 429) throw new TransientError(`Feishu answered ${response.status}`);
|
|
2435
|
+
let body;
|
|
2436
|
+
try {
|
|
2437
|
+
body = await response.json();
|
|
2438
|
+
} catch {
|
|
2439
|
+
throw new Error(`Feishu answered ${response.status}`);
|
|
2440
|
+
}
|
|
2441
|
+
return body;
|
|
2442
|
+
}
|
|
2443
|
+
function dataOf(body) {
|
|
2444
|
+
if (body.code && body.code !== 0) refuse(body.code, body.msg);
|
|
2445
|
+
return body.data ?? {};
|
|
2446
|
+
}
|
|
2447
|
+
async function tenantAccessToken(host, appId, appSecret) {
|
|
2448
|
+
const body = await call$1(host, "/auth/v3/tenant_access_token/internal", {
|
|
2449
|
+
method: "POST",
|
|
2450
|
+
body: {
|
|
2451
|
+
app_id: appId,
|
|
2452
|
+
app_secret: appSecret
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
if (body.code && body.code !== 0) refuse(body.code, body.msg);
|
|
2456
|
+
const token = body.tenant_access_token ?? "";
|
|
2457
|
+
const expire = typeof body.expire === "number" ? body.expire : 7200;
|
|
2458
|
+
if (!token) throw new Error("Feishu did not return a tenant token.");
|
|
2459
|
+
return {
|
|
2460
|
+
token,
|
|
2461
|
+
expiresAt: Date.now() + expire * 1e3 - 6e4
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
async function botInfo(credential) {
|
|
2465
|
+
const body = await call$1(credential.host, "/bot/v3/info", { token: credential.tenantAccessToken });
|
|
2466
|
+
if (body.code && body.code !== 0) refuse(body.code, body.msg);
|
|
2467
|
+
const bot = body.bot ?? {};
|
|
2468
|
+
const openId = typeof bot.open_id === "string" ? bot.open_id : "";
|
|
2469
|
+
const name = typeof bot.app_name === "string" ? bot.app_name : "";
|
|
2470
|
+
const avatarUrl = typeof bot.avatar_url === "string" ? bot.avatar_url : void 0;
|
|
2471
|
+
if (!openId) throw new Error("Feishu did not return which bot this app is.");
|
|
2472
|
+
return {
|
|
2473
|
+
openId,
|
|
2474
|
+
name,
|
|
2475
|
+
...avatarUrl ? { avatarUrl } : {}
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
async function listChats(credential) {
|
|
2479
|
+
const chats = [];
|
|
2480
|
+
let pageToken = "";
|
|
2481
|
+
do {
|
|
2482
|
+
const query = { page_size: "100" };
|
|
2483
|
+
if (pageToken) query.page_token = pageToken;
|
|
2484
|
+
const data = dataOf(await call$1(credential.host, "/im/v1/chats", {
|
|
2485
|
+
token: credential.tenantAccessToken,
|
|
2486
|
+
query
|
|
2487
|
+
}));
|
|
2488
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
2489
|
+
for (const raw of items) {
|
|
2490
|
+
const chat = raw;
|
|
2491
|
+
if (!chat.chat_id) continue;
|
|
2492
|
+
chats.push({
|
|
2493
|
+
id: chat.chat_id,
|
|
2494
|
+
name: chat.name ?? ""
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
pageToken = data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
|
|
2498
|
+
} while (pageToken);
|
|
2499
|
+
return chats;
|
|
2500
|
+
}
|
|
2501
|
+
function textOf$1(content) {
|
|
2502
|
+
if (!content) return "";
|
|
2503
|
+
try {
|
|
2504
|
+
const parsed = JSON.parse(content);
|
|
2505
|
+
return typeof parsed.text === "string" ? parsed.text : content;
|
|
2506
|
+
} catch {
|
|
2507
|
+
return content;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
function mentionsOf(raw) {
|
|
2511
|
+
if (!Array.isArray(raw)) return [];
|
|
2512
|
+
return raw.map((entry) => {
|
|
2513
|
+
const mention = entry;
|
|
2514
|
+
return typeof mention.id === "string" ? mention.id : "";
|
|
2515
|
+
}).filter(Boolean);
|
|
2516
|
+
}
|
|
2517
|
+
async function history(credential, chatId, oldestMs) {
|
|
2518
|
+
const parsed = [];
|
|
2519
|
+
let pageToken = "";
|
|
2520
|
+
do {
|
|
2521
|
+
const query = {
|
|
2522
|
+
container_id_type: "chat",
|
|
2523
|
+
container_id: chatId,
|
|
2524
|
+
sort_type: oldestMs ? "ByCreateTimeAsc" : "ByCreateTimeDesc",
|
|
2525
|
+
page_size: oldestMs ? "50" : "1"
|
|
2526
|
+
};
|
|
2527
|
+
if (oldestMs) query.start_time = String(Math.floor(Number(oldestMs) / 1e3));
|
|
2528
|
+
if (pageToken) query.page_token = pageToken;
|
|
2529
|
+
const body = await call$1(credential.host, "/im/v1/messages", {
|
|
2530
|
+
token: credential.tenantAccessToken,
|
|
2531
|
+
query
|
|
2532
|
+
});
|
|
2533
|
+
if (body.code && body.code !== 0) {
|
|
2534
|
+
if (body.code === 230002 || body.code === 231203) return parsed;
|
|
2535
|
+
refuse(body.code, body.msg);
|
|
2536
|
+
}
|
|
2537
|
+
const data = body.data ?? {};
|
|
2538
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
2539
|
+
for (const raw of items) {
|
|
2540
|
+
const message = raw;
|
|
2541
|
+
if (!message.message_id || message.deleted) continue;
|
|
2542
|
+
parsed.push({
|
|
2543
|
+
id: message.message_id,
|
|
2544
|
+
chatId: message.chat_id ?? chatId,
|
|
2545
|
+
createTime: message.create_time ?? "",
|
|
2546
|
+
text: textOf$1(message.body?.content),
|
|
2547
|
+
...message.sender?.id ? { senderId: message.sender.id } : {},
|
|
2548
|
+
...message.sender?.sender_type ? { senderType: message.sender.sender_type } : {},
|
|
2549
|
+
mentionIds: mentionsOf(message.mentions)
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
pageToken = oldestMs && data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
|
|
2553
|
+
} while (pageToken);
|
|
2554
|
+
return parsed.sort((a, b) => Number(a.createTime) - Number(b.createTime));
|
|
2555
|
+
}
|
|
2556
|
+
async function replyMessage(credential, messageId, text) {
|
|
2557
|
+
const data = dataOf(await call$1(credential.host, `/im/v1/messages/${messageId}/reply`, {
|
|
2558
|
+
token: credential.tenantAccessToken,
|
|
2559
|
+
method: "POST",
|
|
2560
|
+
body: {
|
|
2561
|
+
msg_type: "text",
|
|
2562
|
+
content: JSON.stringify({ text })
|
|
2563
|
+
}
|
|
2564
|
+
}));
|
|
2565
|
+
const id = typeof data.message_id === "string" ? data.message_id : "";
|
|
2566
|
+
const chatId = typeof data.chat_id === "string" ? data.chat_id : "";
|
|
2567
|
+
if (!id) throw new Error("Feishu accepted the reply but did not say where it landed.");
|
|
2568
|
+
return {
|
|
2569
|
+
id,
|
|
2570
|
+
chatId
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
async function sendMessage$1(credential, chatId, text) {
|
|
2574
|
+
const data = dataOf(await call$1(credential.host, "/im/v1/messages", {
|
|
2575
|
+
token: credential.tenantAccessToken,
|
|
2576
|
+
method: "POST",
|
|
2577
|
+
query: { receive_id_type: "chat_id" },
|
|
2578
|
+
body: {
|
|
2579
|
+
receive_id: chatId,
|
|
2580
|
+
msg_type: "text",
|
|
2581
|
+
content: JSON.stringify({ text })
|
|
2582
|
+
}
|
|
2583
|
+
}));
|
|
2584
|
+
const id = typeof data.message_id === "string" ? data.message_id : "";
|
|
2585
|
+
const landed = typeof data.chat_id === "string" ? data.chat_id : chatId;
|
|
2586
|
+
if (!id) throw new Error("Feishu accepted the message but did not say where it landed.");
|
|
2587
|
+
return {
|
|
2588
|
+
id,
|
|
2589
|
+
chatId: landed
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
function permalink(host, chatId) {
|
|
2593
|
+
return `${APPLINK[host]}?openChatId=${chatId}`;
|
|
2594
|
+
}
|
|
2595
|
+
function asAsked$1(value) {
|
|
2596
|
+
const record = value;
|
|
2597
|
+
if (!record || typeof record.chatId !== "string" || typeof record.messageId !== "string") throw new Error("This task has lost the message it was about.");
|
|
2598
|
+
return {
|
|
2599
|
+
chatId: record.chatId,
|
|
2600
|
+
messageId: record.messageId,
|
|
2601
|
+
user: typeof record.user === "string" ? record.user : void 0,
|
|
2602
|
+
text: typeof record.text === "string" ? record.text : ""
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
function asCursor(value) {
|
|
2606
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2607
|
+
const out = {};
|
|
2608
|
+
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") out[key] = entry;
|
|
2609
|
+
return out;
|
|
2610
|
+
}
|
|
2611
|
+
function summarise$1(text) {
|
|
2612
|
+
const line = text.replace(/@_user_\d+/g, "").replace(/\s+/g, " ").trim();
|
|
2613
|
+
if (!line) return "Feishu message";
|
|
2614
|
+
return line.length > 80 ? `${line.slice(0, 79)}…` : line;
|
|
2615
|
+
}
|
|
2616
|
+
function isAsk(input) {
|
|
2617
|
+
if (input.senderType === "app" || input.senderId === input.botOpenId) return false;
|
|
2618
|
+
return input.mentionIds.includes(input.botOpenId);
|
|
2619
|
+
}
|
|
2620
|
+
function destination(target, source) {
|
|
2621
|
+
if (typeof target.chat === "string" && target.chat.trim()) return { chatId: target.chat.trim() };
|
|
2622
|
+
if (source.connectorId !== "feishu") throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
|
|
2623
|
+
const carry = source.carry ?? {};
|
|
2624
|
+
if (!carry.chatId && !carry.messageId) throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
|
|
2625
|
+
return {
|
|
2626
|
+
chatId: carry.chatId,
|
|
2627
|
+
messageId: carry.messageId
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
function accountOf$1(credential) {
|
|
2631
|
+
return {
|
|
2632
|
+
id: credential.botOpenId,
|
|
2633
|
+
label: credential.botName || credential.appId,
|
|
2634
|
+
...credential.avatarUrl ? { avatarUrl: credential.avatarUrl } : {}
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Tenant tokens last two hours. Minting on every poll would trip Feishu's
|
|
2639
|
+
* rate limit, so a still-valid one is reused and a fresh one is stored.
|
|
2640
|
+
*/
|
|
2641
|
+
async function usable(credential) {
|
|
2642
|
+
if (Date.now() < credential.expiresAt) return { credential };
|
|
2643
|
+
const minted = await tenantAccessToken(credential.host, credential.appId, credential.appSecret);
|
|
2644
|
+
const refreshed = {
|
|
2645
|
+
...credential,
|
|
2646
|
+
tenantAccessToken: minted.token,
|
|
2647
|
+
expiresAt: minted.expiresAt
|
|
2648
|
+
};
|
|
2649
|
+
return {
|
|
2650
|
+
credential: refreshed,
|
|
2651
|
+
refreshed
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
var feishuRuntime = {
|
|
2655
|
+
async readiness() {
|
|
2656
|
+
return { ready: true };
|
|
2657
|
+
},
|
|
2658
|
+
async poll({ workflowId, credential, cursor }) {
|
|
2659
|
+
if (workflowId !== "feishu.ask") throw new Error(`Feishu cannot watch for ${workflowId}.`);
|
|
2660
|
+
const { credential: account } = await usable(credential);
|
|
2661
|
+
const previous = asCursor(cursor);
|
|
2662
|
+
const chats = await listChats(account);
|
|
2663
|
+
const next = { ...previous };
|
|
2664
|
+
const signals = [];
|
|
2665
|
+
for (const chat of chats) {
|
|
2666
|
+
const oldest = previous[chat.id];
|
|
2667
|
+
const messages = await history(account, chat.id, oldest);
|
|
2668
|
+
if (messages.length === 0) {
|
|
2669
|
+
if (!next[chat.id]) next[chat.id] = String(Date.now());
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
next[chat.id] = messages[messages.length - 1].createTime;
|
|
2673
|
+
if (!oldest) continue;
|
|
2674
|
+
for (const message of messages) {
|
|
2675
|
+
if (oldest && Number(message.createTime) <= Number(oldest)) continue;
|
|
2676
|
+
if (!isAsk({
|
|
2677
|
+
senderId: message.senderId,
|
|
2678
|
+
senderType: message.senderType,
|
|
2679
|
+
mentionIds: message.mentionIds,
|
|
2680
|
+
botOpenId: account.botOpenId
|
|
2681
|
+
})) continue;
|
|
2682
|
+
const asked = {
|
|
2683
|
+
chatId: message.chatId,
|
|
2684
|
+
messageId: message.id,
|
|
2685
|
+
user: message.senderId,
|
|
2686
|
+
text: message.text
|
|
2687
|
+
};
|
|
2688
|
+
signals.push({
|
|
2689
|
+
key: `${message.chatId}:${message.id}`,
|
|
2690
|
+
kind: "message",
|
|
2691
|
+
ref: message.id,
|
|
2692
|
+
title: summarise$1(message.text),
|
|
2693
|
+
url: permalink(account.host, message.chatId),
|
|
2694
|
+
payload: asked
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return {
|
|
2699
|
+
signals,
|
|
2700
|
+
cursor: next
|
|
2701
|
+
};
|
|
2702
|
+
},
|
|
2703
|
+
async resolveWorkItem({ payload }) {
|
|
2704
|
+
const asked = asAsked$1(payload);
|
|
2705
|
+
return {
|
|
2706
|
+
kind: "message",
|
|
2707
|
+
ref: asked.messageId,
|
|
2708
|
+
title: summarise$1(asked.text),
|
|
2709
|
+
url: "",
|
|
2710
|
+
carry: {
|
|
2711
|
+
chatId: asked.chatId,
|
|
2712
|
+
messageId: asked.messageId
|
|
2713
|
+
},
|
|
2714
|
+
context: [{
|
|
2715
|
+
name: "MESSAGE.md",
|
|
2716
|
+
body: [
|
|
2717
|
+
`# What you were asked`,
|
|
2718
|
+
``,
|
|
2719
|
+
asked.text,
|
|
2720
|
+
``
|
|
2721
|
+
].join("\n")
|
|
2722
|
+
}]
|
|
2723
|
+
};
|
|
2724
|
+
},
|
|
2725
|
+
async applyAction({ actionId, target, source, body, credential }) {
|
|
2726
|
+
if (actionId !== "feishu.reply") throw new Error(`Feishu cannot ${actionId}.`);
|
|
2727
|
+
const { credential: account } = await usable(credential);
|
|
2728
|
+
const to = destination(target, source);
|
|
2729
|
+
const text = body.trim();
|
|
2730
|
+
const posted = to.messageId && source.connectorId === "feishu" ? await replyMessage(account, to.messageId, text) : await sendMessage$1(account, to.chatId ?? "", text);
|
|
2731
|
+
return { url: permalink(account.host, posted.chatId || to.chatId || "") };
|
|
2732
|
+
},
|
|
2733
|
+
auth: {
|
|
2734
|
+
async connectWithFields(fields) {
|
|
2735
|
+
const appId = fields.appId?.trim() ?? "";
|
|
2736
|
+
const appSecret = fields.appSecret?.trim() ?? "";
|
|
2737
|
+
if (!appId) throw new Error("Paste the App ID from your Feishu or Lark app.");
|
|
2738
|
+
if (!appSecret) throw new Error("Paste the App Secret from your Feishu or Lark app.");
|
|
2739
|
+
const host = parseHost(fields.host);
|
|
2740
|
+
const minted = await tenantAccessToken(host, appId, appSecret);
|
|
2741
|
+
const bot = await botInfo({
|
|
2742
|
+
host,
|
|
2743
|
+
tenantAccessToken: minted.token
|
|
2744
|
+
});
|
|
2745
|
+
const stored = {
|
|
2746
|
+
appId,
|
|
2747
|
+
appSecret,
|
|
2748
|
+
host,
|
|
2749
|
+
botOpenId: bot.openId,
|
|
2750
|
+
botName: bot.name,
|
|
2751
|
+
...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {},
|
|
2752
|
+
tenantAccessToken: minted.token,
|
|
2753
|
+
expiresAt: minted.expiresAt
|
|
2754
|
+
};
|
|
2755
|
+
return {
|
|
2756
|
+
credential: stored,
|
|
2757
|
+
account: accountOf$1(stored)
|
|
2758
|
+
};
|
|
2759
|
+
},
|
|
2760
|
+
async identity(credential) {
|
|
2761
|
+
const { credential: fresh, refreshed } = await usable(credential);
|
|
2762
|
+
const bot = await botInfo(fresh);
|
|
2763
|
+
const updated = {
|
|
2764
|
+
...fresh,
|
|
2765
|
+
botOpenId: bot.openId,
|
|
2766
|
+
botName: bot.name,
|
|
2767
|
+
...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {}
|
|
2768
|
+
};
|
|
2769
|
+
return {
|
|
2770
|
+
account: accountOf$1(updated),
|
|
2771
|
+
...refreshed ? { renewedCredential: updated } : {}
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
/**
|
|
1933
2777
|
* Tencent's iLink bot API, as documented for clients that are not OpenClaw:
|
|
1934
2778
|
* https://github.com/Tencent/openclaw-weixin/blob/main/docs/protocol.md
|
|
1935
2779
|
*
|
|
@@ -2264,6 +3108,8 @@ function accountOf(credential) {
|
|
|
2264
3108
|
/** Server-only registry. Never import this from a component. */
|
|
2265
3109
|
var RUNTIMES = {
|
|
2266
3110
|
github: githubRuntime,
|
|
3111
|
+
slack: slackRuntime,
|
|
3112
|
+
feishu: feishuRuntime,
|
|
2267
3113
|
gmail: gmailRuntime,
|
|
2268
3114
|
wechat: {
|
|
2269
3115
|
async readiness() {
|
|
@@ -2539,4 +3385,4 @@ async function checkConnection(id) {
|
|
|
2539
3385
|
return toView(await getConnection(id));
|
|
2540
3386
|
}
|
|
2541
3387
|
//#endregion
|
|
2542
|
-
export {
|
|
3388
|
+
export { startAuthAttempt as _, connectionNoun as a, writerName as b, connectorRuntime as c, destinationLabel as d, isInboxOnly as f, saveAuthorizedConnection as g, removeConnection as h, checkConnection as i, connectorWorkflow as l, needsAccount as m, allTriggers as n, connectorAction as o, listConnections as p, allWorkflows as r, connectorManifest as s, CONNECTOR_MANIFESTS as t, credentialForConnector as u, takeAuthAttempt as v, writers as x, updateConnectionSettings as y };
|