loopable-cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/_connectorId-CDCuSgpN.js +1 -0
  3. package/.output/public/assets/_loopId-DOnIsVTW.js +1 -0
  4. package/.output/public/assets/_taskId-BO5MZnlZ.js +1 -0
  5. package/.output/public/assets/agents-CBozi7oi.js +1 -0
  6. package/.output/public/assets/alert-Bn_wjgTX.js +1 -0
  7. package/.output/public/assets/button-K3ypIhAZ.js +1 -0
  8. package/.output/public/assets/card-DiVnaTIE.js +1 -0
  9. package/.output/public/assets/chevron-right-BrctLtFK.js +1 -0
  10. package/.output/public/assets/connection-status-DKWelvLO.js +1 -0
  11. package/.output/public/assets/connector-icon-CEbdE71G.js +1 -0
  12. package/.output/public/assets/connectors-JyX3IzJj.js +1 -0
  13. package/.output/public/assets/gaps-PjmcbaWo.js +1 -0
  14. package/.output/public/assets/inbox-CujRR0Cz.js +1 -0
  15. package/.output/public/assets/index-C9SQ_Baq.js +16 -0
  16. package/.output/public/assets/loop-form-Dw3cuTzw.js +1 -0
  17. package/.output/public/assets/loops-DcEfZa2_.js +1 -0
  18. package/.output/public/assets/manifests-yJpevyye.js +1 -0
  19. package/.output/public/assets/new-CwsnNuRW.js +1 -0
  20. package/.output/public/assets/page-bvnfRMum.js +1 -0
  21. package/.output/public/assets/play-yHLs4DjK.js +1 -0
  22. package/.output/public/assets/plus-Ber25lrU.js +1 -0
  23. package/.output/public/assets/preload-helper-BoU_7EQC.js +3 -0
  24. package/.output/public/assets/{refresh-cw-C3xBnKfy.js → refresh-cw-BzSY7M8I.js} +1 -1
  25. package/.output/public/assets/rotate-ccw-DLv9EfCX.js +1 -0
  26. package/.output/public/assets/runners-DzX-RU3i.js +2 -0
  27. package/.output/public/assets/select-C9grIFwG.js +1 -0
  28. package/.output/public/assets/setting-fields-DUc3npgh.js +3 -0
  29. package/.output/public/assets/styles-B9B98zLk.css +2 -0
  30. package/.output/public/assets/switch-DwABVI6d.js +1 -0
  31. package/.output/public/assets/task-list-Ddk8-qt7.js +1 -0
  32. package/.output/public/assets/textarea-Ds-gIYUQ.js +1 -0
  33. package/.output/public/assets/time-goBWd-5J.js +1 -0
  34. package/.output/public/assets/use-live-tasks-DZNNuECy.js +1 -0
  35. package/.output/public/assets/useCompositeListItem-P4Nmmn7j.js +1 -0
  36. package/.output/public/assets/usePositioner-B7i9Ejzh.js +1 -0
  37. package/.output/server/_chunks/ssr-renderer.mjs +1 -1
  38. package/.output/server/{_connectorId-C9yydJrC.mjs → _connectorId-COuQN02a.mjs} +198 -188
  39. package/.output/server/_libs/@base-ui/react+[...].mjs +1410 -804
  40. package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
  41. package/.output/server/_libs/lucide-react.mjs +93 -84
  42. package/.output/server/_libs/next-themes.mjs +1 -1
  43. package/.output/server/_libs/sonner.mjs +1 -1
  44. package/.output/server/_loopId-CZh6_Yhr.mjs +310 -0
  45. package/.output/server/_ssr/{agents-DG643yoJ.mjs → agents-B_qo1gpL.mjs} +195 -6
  46. package/.output/server/_ssr/{agents-C7B6RJ6-.mjs → agents-Bz10gz_m.mjs} +8 -8
  47. package/.output/server/_ssr/agents-CjbOUp3Z.mjs +206 -0
  48. package/.output/server/_ssr/{alert-DQvzE7bu.mjs → alert-C2WBWfzo.mjs} +2 -2
  49. package/.output/server/_ssr/card-r1cV-PCN.mjs +36 -0
  50. package/.output/server/_ssr/{client-BtVRVhQ2.mjs → client-Dk3SD8Il.mjs} +5 -1
  51. package/.output/server/_ssr/connection-status-BogAL-Fj.mjs +32 -0
  52. package/.output/server/_ssr/{connections-Df-aDpfJ.mjs → connections-Cqvb6Gw2.mjs} +372 -325
  53. package/.output/server/_ssr/connector-icon-DrhVbhs6.mjs +190 -0
  54. package/.output/server/_ssr/connectors-BfS5tQKr.mjs +81 -0
  55. package/.output/server/_ssr/{connectors-e0mf2pMs.mjs → connectors-DJI-rzPU.mjs} +10 -10
  56. package/.output/server/_ssr/{createServerRpc-DPDrnFdH.mjs → createServerRpc-DDrp2nLT.mjs} +2 -2
  57. package/.output/server/_ssr/{gaps-Cz76ZicM.mjs → gaps-CcvHiLcj.mjs} +15 -7
  58. package/.output/server/_ssr/inbox-D1ef761v.mjs +178 -0
  59. package/.output/server/_ssr/loop-form-DWwOR05R.mjs +346 -0
  60. package/.output/server/_ssr/{loops-D8WuK7Ke.mjs → loops-6WhUe_lc.mjs} +37 -27
  61. package/.output/server/_ssr/loops-DHPjzG-t.mjs +217 -0
  62. package/.output/server/_ssr/new-ChKBdcXJ.mjs +245 -0
  63. package/.output/server/_ssr/page-DZVxKvdn.mjs +120 -0
  64. package/.output/server/_ssr/{router-54IDWTVz.mjs → router-DdwREiOf.mjs} +162 -489
  65. package/.output/server/_ssr/runners-CK9Cx9fS.mjs +217 -0
  66. package/.output/server/_ssr/runners-CrznFPHt.mjs +140 -0
  67. package/.output/server/_ssr/{runners-Cj6z9Mqh.mjs → runners-W-pgo5yo.mjs} +5 -5
  68. package/.output/server/_ssr/{secrets-CRnpwGBh.mjs → secrets-CV5scFlt.mjs} +2 -2
  69. package/.output/server/_ssr/select-BtndHhHb.mjs +76 -0
  70. package/.output/server/_ssr/{setting-fields-DzBlslan.mjs → setting-fields-LNgywAW7.mjs} +25 -20
  71. package/.output/server/_ssr/ssr.mjs +43 -42
  72. package/.output/server/_ssr/{start-Vq3Oan3v.mjs → start-C_j-e4vF.mjs} +9 -4
  73. package/.output/server/_ssr/{switch-1pnd_bPJ.mjs → switch-odxNO_S4.mjs} +3 -3
  74. package/.output/server/_ssr/task-list-Dnmq_WeE.mjs +150 -0
  75. package/.output/server/_ssr/{tasks-Qtml5TlK.mjs → tasks-D-kXLgVP.mjs} +13 -13
  76. package/.output/server/_ssr/{tasks-BDqznm57.mjs → tasks-D7aUvA33.mjs} +44 -17
  77. package/.output/server/_ssr/{textarea-CHJTKtsJ.mjs → textarea-D2c_vTIC.mjs} +2 -2
  78. package/.output/server/_ssr/time-Cy8TZP0Z.mjs +13 -0
  79. package/.output/server/_ssr/use-live-tasks-BzhTkEOL.mjs +40 -0
  80. package/.output/server/_tanstack-start-manifest_v-2HUdn-2E.mjs +166 -0
  81. package/.output/server/_taskId-D3KkNJW1.mjs +263 -0
  82. package/.output/server/index.mjs +199 -164
  83. package/README.md +36 -6
  84. package/dist/cli.mjs +161 -8
  85. package/dist/dispatcher.mjs +671 -285
  86. package/dist/runner.mjs +333 -46
  87. package/drizzle/0025_prompt_tasks_done.sql +2 -0
  88. package/drizzle/0026_guidance_into_prompt.sql +3 -0
  89. package/drizzle/0027_relative_folders.sql +5 -0
  90. package/drizzle/0028_runner_version.sql +5 -0
  91. package/drizzle/meta/_journal.json +29 -1
  92. package/package.json +4 -2
  93. package/.output/public/assets/_connectorId-CkygNLny.js +0 -1
  94. package/.output/public/assets/_loopId-Cshlc5Ja.js +0 -1
  95. package/.output/public/assets/_taskId-BKDIJ_Fg.js +0 -1
  96. package/.output/public/assets/alert-BT2EVMJP.js +0 -1
  97. package/.output/public/assets/arrow-left-CWUZt6k2.js +0 -1
  98. package/.output/public/assets/arrow-right-dy_oOg_c.js +0 -1
  99. package/.output/public/assets/connection-status-DCqqmY_6.js +0 -1
  100. package/.output/public/assets/connector-icon-BUoK6Cru.js +0 -1
  101. package/.output/public/assets/connectors-CeLBtEtg.js +0 -1
  102. package/.output/public/assets/external-link-DC11orFA.js +0 -1
  103. package/.output/public/assets/gaps-BtFoD7mF.js +0 -1
  104. package/.output/public/assets/inbox-BYNjkNDJ.js +0 -1
  105. package/.output/public/assets/index-0yP3Jn_G.js +0 -18
  106. package/.output/public/assets/loader-B4Epchm_.js +0 -1
  107. package/.output/public/assets/loop-form-DnSJNc1k.js +0 -1
  108. package/.output/public/assets/loops-1CvbqO6m.js +0 -1
  109. package/.output/public/assets/new-m5NlqYQT.js +0 -1
  110. package/.output/public/assets/play-DD4xQgvi.js +0 -1
  111. package/.output/public/assets/preload-helper-CfdAYKD7.js +0 -3
  112. package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +0 -1
  113. package/.output/public/assets/runners-D8QVl-Y6.js +0 -1
  114. package/.output/public/assets/select-Cppu-hti.js +0 -1
  115. package/.output/public/assets/setting-fields-bJ_gTG7Q.js +0 -3
  116. package/.output/public/assets/styles-ClTGaXU0.css +0 -2
  117. package/.output/public/assets/switch-hRIbNGOj.js +0 -1
  118. package/.output/public/assets/task-list-BAxjcVgr.js +0 -1
  119. package/.output/public/assets/textarea-Fyuzi6Vp.js +0 -1
  120. package/.output/public/assets/trash-2-BXZzBsoV.js +0 -1
  121. package/.output/public/assets/useValueChanged-VVyq4hb9.js +0 -1
  122. package/.output/server/_loopId-B0VG-eBC.mjs +0 -301
  123. package/.output/server/_ssr/connection-status-BZr3Y-IC.mjs +0 -31
  124. package/.output/server/_ssr/connector-icon-WvxwZ5TF.mjs +0 -56
  125. package/.output/server/_ssr/connectors-BU0IiRge.mjs +0 -107
  126. package/.output/server/_ssr/inbox-CAUBa1I3.mjs +0 -114
  127. package/.output/server/_ssr/loop-form-FqSaXiHl.mjs +0 -413
  128. package/.output/server/_ssr/loops-Dqoebe8q.mjs +0 -222
  129. package/.output/server/_ssr/new-bBsVaWD6.mjs +0 -169
  130. package/.output/server/_ssr/runners-D2UAdLyj.mjs +0 -106
  131. package/.output/server/_ssr/runners-DGWaVvWi.mjs +0 -116
  132. package/.output/server/_ssr/task-list-_qgBS9LU.mjs +0 -187
  133. package/.output/server/_tanstack-start-manifest_v-CvheE7ZF.mjs +0 -131
  134. package/.output/server/_taskId-V4QLBO2N.mjs +0 -256
@@ -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-BtVRVhQ2.mjs";
3
- import { i as readCredential, s as writeCredential, t as deleteCredential } from "./secrets-CRnpwGBh.mjs";
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-Df-aDpfJ.js
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. Wildcards work: acme/* or */*. Leave empty for every repository you can see.",
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: "Dependabot and friends open a great many, and each one costs a full run.",
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: "A GitHub URL. Leave empty to write back to whatever triggered the loop.",
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 PLAN_PROMPT = [
95
- "Write a short implementation plan for this issue.",
94
+ var REPLY_PROMPT = [
95
+ "Answer this issue in a comment, the way a teammate who knows this codebase would.",
96
96
  "",
97
- "Say what should change, in which files where you can tell, and what to watch",
98
- "out for. Stay inside what the issue actually supports: if it is too vague to",
99
- "plan, say what is missing rather than inventing the requirements.",
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
- "Do not write the implementation."
102
- ].join("\n");
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 review requests and assigned issues.",
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
- workflows: [
132
- {
133
- id: "github.review_requested",
134
- name: "Review pull requests I am asked to review",
135
- summary: "Reads the change and posts a review whenever someone asks for yours.",
136
- trigger: "your review is requested, either directly or through a team you belong to",
137
- watches: "is:open is:pr review-requested:@me",
138
- writes: "a review on the pull request, as a comment rather than an approval",
139
- settings: [
140
- repositories,
141
- ignoreDrafts,
142
- ignoreBots,
143
- sizeLimit
144
- ],
145
- prompt: REVIEW_PROMPT,
146
- guidancePlaceholder: "Anything specific to your team. For example: we require a test for every new endpoint.",
147
- answer: "review",
148
- runsIn: "checkout",
149
- actionId: "github.submit_review"
150
- },
151
- {
152
- id: "github.issue_assigned",
153
- name: "Plan issues assigned to me",
154
- summary: "Posts a short implementation plan when an issue lands on you.",
155
- trigger: "an issue is assigned to you",
156
- watches: "is:open is:issue assignee:@me",
157
- writes: "a comment on the issue",
158
- settings: [repositories],
159
- prompt: PLAN_PROMPT,
160
- guidancePlaceholder: "Anything specific to this codebase worth knowing before planning.",
161
- answer: "text",
162
- runsIn: "checkout",
163
- actionId: "github.post_issue_comment"
164
- },
165
- {
166
- id: "github.issue_implement",
167
- name: "Implement issues assigned to me",
168
- summary: "Writes the change and opens a draft pull request.",
169
- trigger: "an issue is assigned to you",
170
- watches: "is:open is:issue assignee:@me",
171
- writes: "a draft pull request",
172
- runsIn: "checkout",
173
- settings: [repositories],
174
- prompt: IMPLEMENT_PROMPT,
175
- guidancePlaceholder: "How work is done here. For example: every new endpoint needs a test, and we do not add dependencies without asking.",
176
- answer: "code",
177
- actionId: "github.open_pull_request"
178
- }
179
- ],
180
- actions: [
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
- * Reading is the whole of it. Gmail has no send scope here on purpose: an
212
- * agent that can reply as you, to anyone, is a different and much larger thing
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.
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.
220
178
  */
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.
258
- */
259
- var needsMeOnly = {
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: "Read new mail and have an agent tell you what matters.",
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
- workflows: [{
192
+ triggers: [{
300
193
  id: "gmail.new_mail",
301
- name: "Tell me about new mail",
302
- summary: "Reads mail as it arrives and sends you a short account of it somewhere you look.",
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
- extraQuery,
309
- needsMeOnly,
310
- perPoll,
311
- skipOwn
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: "wechat",
317
- actionId: "wechat.reply",
318
- actionTarget: { to: "me" },
241
+ actionConnectorId: "schedule",
242
+ actionId: "schedule.record",
319
243
  pollEveryMs: 18e5
320
244
  }],
321
- actions: [],
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: "On a 24-hour clock, in this host's own time zone. Only the minutes are used by an hourly loop.",
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: "Only used by a loop that runs once a week.",
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$3 = {
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,58 +428,34 @@ var scheduleManifest = defineManifest({
504
428
  allowsMultipleAccounts: false,
505
429
  byHand: { kind: "now" },
506
430
  settings: [],
507
- workflows: [{
431
+ triggers: [{
508
432
  id: "schedule.recurring",
509
- name: "Do something on a schedule",
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$3
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 it in the log",
528
- summary: "Leaves the answer here and sends it nowhere. Every run is recorded anyway, so this is what to use until it is worth pushing somewhere.",
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
- var folder$2 = {
534
- key: "folder",
535
- kind: "text",
536
- label: "Folder to work in",
537
- help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
538
- placeholder: "/Users/you/code/your-project"
539
- };
540
- var channel = {
541
- key: "channel",
542
- kind: "text",
543
- label: "Channel",
544
- help: "Leave empty to reply in the thread that asked. Set a channel ID when the work started somewhere else.",
545
- placeholder: "C0123456789"
546
- };
547
- var ASK_PROMPT$2 = [
548
- "Someone has sent you a message in Slack. Do what it asks, working in this directory,",
549
- "and then answer them.",
550
- "",
551
- "You are answering into Slack. Be brief: a couple of short paragraphs at most,",
552
- "no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
553
- "",
554
- "If the message asks a question about this code, read enough of it to answer",
555
- "properly rather than guessing. If it asks for a change, make the change and",
556
- "say in one line what you did. If the message is too vague to act on, say what",
557
- "you need to know instead of assuming."
558
- ].join("\n");
559
459
  var slackManifest = defineManifest({
560
460
  id: "slack",
561
461
  name: "Slack bot",
@@ -574,57 +474,51 @@ var slackManifest = defineManifest({
574
474
  secret: true
575
475
  }]
576
476
  },
577
- workflows: [{
477
+ triggers: [{
578
478
  id: "slack.ask",
579
- name: "Do what I ask the bot",
580
- summary: "Runs your agent when someone @mentions the bot or DMs it, and answers in the thread.",
581
- trigger: "the bot is @mentioned, or someone DMs it",
582
- writes: "a reply in the Slack thread",
479
+ when: "The Slack bot is mentioned",
480
+ trigger: "someone @mentions the Slack bot in a channel it is in, or DMs it",
583
481
  runsIn: "folder",
584
- settings: [folder$2],
585
- prompt: ASK_PROMPT$2,
586
- guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
482
+ settings: [],
587
483
  answer: "text",
588
484
  actionId: "slack.reply",
589
485
  pollEveryMs: 6e4
590
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
+ }],
591
505
  actions: [{
592
506
  id: "slack.reply",
593
507
  name: "Reply in Slack",
594
508
  summary: "Post a message back to the thread that asked, or to a channel you name.",
595
- target: [channel],
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
+ }],
596
516
  accepts: ["text"]
597
517
  }],
598
518
  settings: [],
599
519
  allowsMultipleAccounts: false,
600
520
  byHand: { kind: "none" }
601
521
  });
602
- var folder$1 = {
603
- key: "folder",
604
- kind: "text",
605
- label: "Folder to work in",
606
- help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
607
- placeholder: "/Users/you/code/your-project"
608
- };
609
- var chat = {
610
- key: "chat",
611
- kind: "text",
612
- label: "Chat ID",
613
- help: "Leave empty to reply to the message that asked. Set a chat ID (oc_…) when the work started somewhere else.",
614
- placeholder: "oc_..."
615
- };
616
- var ASK_PROMPT$1 = [
617
- "Someone has sent you a message in Feishu. Do what it asks, working in this directory,",
618
- "and then answer them.",
619
- "",
620
- "You are answering into Feishu. Be brief: a couple of short paragraphs at most,",
621
- "no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
622
- "",
623
- "If the message asks a question about this code, read enough of it to answer",
624
- "properly rather than guessing. If it asks for a change, make the change and",
625
- "say in one line what you did. If the message is too vague to act on, say what",
626
- "you need to know instead of assuming."
627
- ].join("\n");
628
522
  var feishuManifest = defineManifest({
629
523
  id: "feishu",
630
524
  name: "Feishu / Lark bot",
@@ -658,25 +552,45 @@ var feishuManifest = defineManifest({
658
552
  }
659
553
  ]
660
554
  },
661
- workflows: [{
555
+ triggers: [{
662
556
  id: "feishu.ask",
663
- name: "Do what I ask the bot",
664
- summary: "Runs your agent when someone @mentions the bot in a group it is in, and answers in the thread.",
665
- trigger: "the bot is @mentioned in a group it is in",
666
- writes: "a reply in the Feishu thread",
557
+ when: "The Feishu / Lark bot is mentioned",
558
+ trigger: "someone @mentions the Feishu / Lark bot in a group it is in",
667
559
  runsIn: "folder",
668
- settings: [folder$1],
669
- prompt: ASK_PROMPT$1,
670
- guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
560
+ settings: [],
671
561
  answer: "text",
672
562
  actionId: "feishu.reply",
673
563
  pollEveryMs: 6e4
674
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
+ }],
675
583
  actions: [{
676
584
  id: "feishu.reply",
677
- name: "Reply in Feishu",
585
+ name: "Reply in Feishu / Lark",
678
586
  summary: "Post a message back to the thread that asked, or to a chat you name.",
679
- target: [chat],
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
+ }],
680
594
  accepts: ["text"]
681
595
  }],
682
596
  settings: [],
@@ -684,18 +598,6 @@ var feishuManifest = defineManifest({
684
598
  byHand: { kind: "none" }
685
599
  });
686
600
  /**
687
- * Where the agent runs. Asking a phone "what broke the build" is only worth
688
- * anything if the agent can read the build, so this workflow works in a real
689
- * checkout rather than an empty directory.
690
- */
691
- var folder = {
692
- key: "folder",
693
- kind: "text",
694
- label: "Folder to work in",
695
- help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
696
- placeholder: "/Users/you/code/your-project"
697
- };
698
- /**
699
601
  * A bot bound to your account can be messaged by other people, and this
700
602
  * workflow runs a coding agent on a runner. Answering only yourself is the
701
603
  * default because the alternative hands strangers your terminal.
@@ -724,7 +626,7 @@ var recipient = {
724
626
  key: "to",
725
627
  kind: "select",
726
628
  label: "Who to send it to",
727
- help: "WeChat only reliably delivers a reply to a conversation you started. Sending to yourself out of the blue is refused unless you have written to the bot in the last half day or so, which makes this a poor fit for anything that has to arrive overnight.",
629
+ help: "\"Me\" only arrives if you messaged the bot in the last half day.",
728
630
  options: [{
729
631
  value: "source",
730
632
  label: "Whoever asked"
@@ -767,19 +669,22 @@ var CONNECTOR_MANIFESTS = [
767
669
  kind: "qr_scan",
768
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."
769
671
  },
770
- workflows: [{
672
+ triggers: [{
771
673
  id: "wechat.ask",
772
- name: "Do what I ask the bot",
773
- summary: "Runs your agent on a runner when you message the bot, and answers in the chat.",
774
- trigger: "you send the bot a message",
775
- writes: "a reply in the chat",
674
+ when: "The WeChat bot gets a message",
675
+ trigger: "someone sends the WeChat bot a message",
776
676
  runsIn: "folder",
777
- settings: [folder, askers],
778
- prompt: ASK_PROMPT,
779
- 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],
780
678
  answer: "text",
781
679
  actionId: "wechat.reply"
782
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
+ }],
783
688
  actions: [{
784
689
  id: "wechat.reply",
785
690
  name: "Reply in the chat",
@@ -813,17 +718,90 @@ function needsAccount(id) {
813
718
  function connectionNoun(id) {
814
719
  return connectorManifest(id)?.auth.kind === "token" ? "bot" : "account";
815
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
+ */
816
736
  function connectorWorkflow(connectorId, workflowId) {
817
- return connectorManifest(connectorId)?.workflows.find((entry) => entry.id === workflowId);
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;
818
756
  }
819
757
  function connectorAction(connectorId, actionId) {
820
758
  return connectorManifest(connectorId)?.actions.find((entry) => entry.id === actionId);
821
759
  }
822
- /** Every workflow on offer, for the page that asks which one to turn on. */
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
+ */
823
794
  function allWorkflows() {
824
- return CONNECTOR_MANIFESTS.flatMap((connector) => connector.workflows.map((workflow) => ({
795
+ return [...CONNECTOR_MANIFESTS.filter((connector) => connector.id === "schedule"), ...CONNECTOR_MANIFESTS.filter((connector) => connector.id !== "schedule")].flatMap((connector) => connector.workflows.map((entry) => ({
825
796
  connector,
826
- workflow
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) => ({
803
+ connector,
804
+ trigger
827
805
  })));
828
806
  }
829
807
  /**
@@ -1286,15 +1264,15 @@ function candidates(items, repositories) {
1286
1264
  });
1287
1265
  }
1288
1266
  /**
1289
- * What the workflow says it watches for, which is what gets asked. Taking the
1267
+ * What the trigger says it watches for, which is what gets asked. Taking the
1290
1268
  * query from the manifest is the point: it is shown on the loop page, and a
1291
1269
  * search that quietly differed from the one on screen would be the worst kind
1292
1270
  * of wrong.
1293
1271
  */
1294
- function query(workflowId) {
1295
- const workflow = githubManifest.workflows.find((entry) => entry.id === workflowId);
1296
- if (!workflow?.watches) throw new Error(`GitHub cannot watch for ${workflowId}.`);
1297
- return workflow.watches;
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;
1298
1276
  }
1299
1277
  async function pollGithub(input) {
1300
1278
  const repositories = stringList(input.settings.repositories);
@@ -1567,13 +1545,18 @@ var githubRuntime = defineRuntime({
1567
1545
  }
1568
1546
  });
1569
1547
  var BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
1570
- async function request(accessToken, path) {
1548
+ async function request(accessToken, path, init = {}) {
1571
1549
  let res;
1572
1550
  try {
1573
- res = await fetch(`${BASE}${path}`, { headers: {
1574
- Authorization: `Bearer ${accessToken}`,
1575
- Accept: "application/json"
1576
- } });
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
+ });
1577
1560
  } catch (error) {
1578
1561
  throw new TransientError(`Gmail could not be reached: ${error instanceof Error ? error.message : String(error)}`);
1579
1562
  }
@@ -1582,6 +1565,7 @@ async function request(accessToken, path) {
1582
1565
  const message = `Gmail ${res.status} on ${path}: ${detail}`;
1583
1566
  const backoff = res.status === 403 && /rateLimitExceeded|userRateLimitExceeded/i.test(detail);
1584
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.");
1585
1569
  throw new Error(message);
1586
1570
  }
1587
1571
  return await res.json();
@@ -1618,6 +1602,16 @@ function getMessageHeaders(accessToken, id) {
1618
1602
  function getMessage(accessToken, id) {
1619
1603
  return request(accessToken, `/messages/${id}?format=full`);
1620
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
+ }
1621
1615
  /** A Gmail message id, as a ref: short, stable, and unique to the mailbox. */
1622
1616
  function mailRef(id) {
1623
1617
  return `mail#${id}`;
@@ -1729,6 +1723,30 @@ function mailContext(message) {
1729
1723
  ].join("\n")
1730
1724
  };
1731
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
+ }
1732
1750
  /**
1733
1751
  * What the workflow says it watches for, which is what gets asked. Taken from
1734
1752
  * the manifest rather than written again here, because it is shown on the loop
@@ -1736,9 +1754,9 @@ function mailContext(message) {
1736
1754
  * worst kind of wrong.
1737
1755
  */
1738
1756
  function baseQuery(workflowId) {
1739
- const workflow = gmailManifest.workflows.find((entry) => entry.id === workflowId);
1740
- if (!workflow?.watches) throw new Error(`Gmail cannot watch for ${workflowId}.`);
1741
- return workflow.watches;
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;
1742
1760
  }
1743
1761
  /**
1744
1762
  * Missing means "the workflow's default", not "off". A loop saved before a
@@ -1906,7 +1924,7 @@ function accountOf$3(emailAddress) {
1906
1924
  };
1907
1925
  }
1908
1926
  /**
1909
- * Which message this task is about. The poll keeps the id, and a pasted link
1927
+ * Which message this task is about. A task keeps the id, and a pasted link
1910
1928
  * carries one too, so either is enough to fetch the mail again.
1911
1929
  */
1912
1930
  function messageIdFrom(url, payload) {
@@ -1954,6 +1972,33 @@ var gmailRuntime = defineRuntime({
1954
1972
  emailAddress: (await getProfile(accessToken)).emailAddress
1955
1973
  }) };
1956
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
+ },
1957
2002
  auth: {
1958
2003
  async startAuthorization({ redirectUri, state }) {
1959
2004
  const registration = requireRegistration();
@@ -2128,6 +2173,7 @@ async function authTest(botToken) {
2128
2173
  const teamId = typeof body.team_id === "string" ? body.team_id : "";
2129
2174
  const botUserId = typeof body.user_id === "string" ? body.user_id : "";
2130
2175
  const botId = typeof body.bot_id === "string" ? body.bot_id : "";
2176
+ const botName = typeof body.user === "string" ? body.user : "";
2131
2177
  if (!teamId || !botUserId) throw new Error("Slack did not return which workspace this bot belongs to.");
2132
2178
  return {
2133
2179
  botToken,
@@ -2135,7 +2181,8 @@ async function authTest(botToken) {
2135
2181
  team,
2136
2182
  teamUrl,
2137
2183
  botUserId,
2138
- botId
2184
+ botId,
2185
+ botName
2139
2186
  };
2140
2187
  }
2141
2188
  async function listConversations(credential) {
@@ -2243,7 +2290,7 @@ function destination$1(target, source) {
2243
2290
  function accountOf$2(credential) {
2244
2291
  return {
2245
2292
  id: credential.teamId,
2246
- label: credential.team || credential.teamId,
2293
+ label: [credential.botName ? `@${credential.botName}` : null, credential.team || credential.teamId].filter(Boolean).join(" · "),
2247
2294
  url: credential.teamUrl || void 0
2248
2295
  };
2249
2296
  }
@@ -3338,4 +3385,4 @@ async function checkConnection(id) {
3338
3385
  return toView(await getConnection(id));
3339
3386
  }
3340
3387
  //#endregion
3341
- export { connectorAction as a, connectorWorkflow as c, needsAccount as d, removeConnection as f, updateConnectionSettings as g, takeAuthAttempt as h, connectionNoun as i, credentialForConnector as l, startAuthAttempt as m, allWorkflows as n, connectorManifest as o, saveAuthorizedConnection as p, checkConnection as r, connectorRuntime as s, CONNECTOR_MANIFESTS as t, listConnections as u };
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 };