@fastagent-sh/fastagent 0.29.0 → 0.30.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 (310) hide show
  1. package/README.md +9 -4
  2. package/dist/atomic-write.d.ts +4 -5
  3. package/dist/atomic-write.js +4 -5
  4. package/dist/authoring.d.ts +32 -15
  5. package/dist/authoring.js +147 -127
  6. package/dist/channels/agentcore-protocol.d.ts +8 -3
  7. package/dist/channels/agentcore-protocol.js +3 -1
  8. package/dist/channels/agentcore-service.d.ts +1 -0
  9. package/dist/channels/agentcore-service.js +14 -4
  10. package/dist/channels/agentcore.d.ts +9 -0
  11. package/dist/channels/agentcore.js +29 -4
  12. package/dist/channels/context.d.ts +2 -0
  13. package/dist/channels/context.js +5 -1
  14. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -3
  16. package/dist/channels/feishu/feishu.js +10 -10
  17. package/dist/channels/feishu/register-app.js +3 -5
  18. package/dist/channels/feishu/scaffold/channel.ts +32 -34
  19. package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
  20. package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
  21. package/dist/channels/feishu/setup-mode.d.ts +2 -12
  22. package/dist/channels/feishu/setup-mode.js +2 -22
  23. package/dist/channels/feishu/setup.d.ts +12 -0
  24. package/dist/channels/feishu/setup.js +197 -0
  25. package/dist/channels/kit/abortable.d.ts +8 -0
  26. package/dist/channels/kit/abortable.js +22 -0
  27. package/dist/channels/lark/lark.d.ts +0 -3
  28. package/dist/channels/lark/lark.js +0 -5
  29. package/dist/channels/lark/scaffold/channel.ts +33 -35
  30. package/dist/channels/lark/scaffold/lark-send.ts +5 -5
  31. package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
  32. package/dist/channels/lark/setup.d.ts +9 -0
  33. package/dist/channels/lark/setup.js +57 -0
  34. package/dist/channels/setup.d.ts +112 -0
  35. package/dist/channels/setup.js +31 -0
  36. package/dist/channels/slack/history.js +1 -1
  37. package/dist/channels/slack/manifest.d.ts +17 -7
  38. package/dist/channels/slack/manifest.js +12 -16
  39. package/dist/channels/slack/scaffold/channel.ts +44 -30
  40. package/dist/channels/slack/scaffold/slack-send.ts +8 -8
  41. package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
  42. package/dist/channels/slack/setup.d.ts +7 -0
  43. package/dist/channels/slack/setup.js +119 -0
  44. package/dist/channels/slack/slack-api.d.ts +2 -0
  45. package/dist/channels/slack/slack-api.js +4 -0
  46. package/dist/channels/slack/slack.d.ts +21 -2
  47. package/dist/channels/slack/slack.js +398 -338
  48. package/dist/channels/slack/socket.d.ts +57 -0
  49. package/dist/channels/slack/socket.js +305 -0
  50. package/dist/channels/telegram/polling.d.ts +27 -0
  51. package/dist/channels/telegram/polling.js +105 -0
  52. package/dist/channels/telegram/scaffold/channel.ts +52 -32
  53. package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
  54. package/dist/channels/telegram/setup.d.ts +2 -0
  55. package/dist/channels/telegram/setup.js +42 -0
  56. package/dist/channels/telegram/telegram-api.d.ts +27 -1
  57. package/dist/channels/telegram/telegram-api.js +7 -6
  58. package/dist/channels/telegram/telegram.d.ts +11 -3
  59. package/dist/channels/telegram/telegram.js +184 -157
  60. package/dist/channels/webhook-evidence.d.ts +11 -0
  61. package/dist/channels/webhook-evidence.js +71 -0
  62. package/dist/channels/webhooks.d.ts +37 -0
  63. package/dist/channels/webhooks.js +70 -0
  64. package/dist/child-env.d.ts +16 -0
  65. package/dist/child-env.js +30 -0
  66. package/dist/cli/box-login.js +4 -1
  67. package/dist/cli/channel-app.d.ts +3 -0
  68. package/dist/cli/channel-app.js +35 -0
  69. package/dist/cli/channel-setup.d.ts +38 -0
  70. package/dist/cli/channel-setup.js +195 -0
  71. package/dist/cli/commands/add.d.ts +6 -14
  72. package/dist/cli/commands/add.js +45 -303
  73. package/dist/cli/commands/auth.js +8 -2
  74. package/dist/cli/commands/build.d.ts +5 -0
  75. package/dist/cli/commands/build.js +16 -0
  76. package/dist/cli/commands/channels.d.ts +19 -0
  77. package/dist/cli/commands/channels.js +230 -0
  78. package/dist/cli/commands/content.d.ts +7 -3
  79. package/dist/cli/commands/content.js +16 -10
  80. package/dist/cli/commands/deploy/agentcore.js +55 -23
  81. package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
  82. package/dist/cli/commands/deploy/channel-apps.js +30 -0
  83. package/dist/cli/commands/deploy/docker.js +32 -65
  84. package/dist/cli/commands/deploy/fly.js +28 -12
  85. package/dist/cli/commands/deploy/railway.js +38 -13
  86. package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
  87. package/dist/cli/commands/deploy/secrets-step.js +66 -0
  88. package/dist/cli/commands/deploy/shared.d.ts +16 -5
  89. package/dist/cli/commands/deploy/shared.js +25 -8
  90. package/dist/cli/commands/deploy.js +53 -60
  91. package/dist/cli/commands/dev.d.ts +2 -0
  92. package/dist/cli/commands/dev.js +15 -3
  93. package/dist/cli/commands/env.js +1 -1
  94. package/dist/cli/commands/info.d.ts +2 -0
  95. package/dist/cli/commands/info.js +60 -25
  96. package/dist/cli/commands/init.js +3 -1
  97. package/dist/cli/commands/invoke.d.ts +2 -0
  98. package/dist/cli/commands/invoke.js +20 -7
  99. package/dist/cli/commands/login.d.ts +0 -7
  100. package/dist/cli/commands/login.js +22 -10
  101. package/dist/cli/commands/logout.js +1 -1
  102. package/dist/cli/commands/mcp.js +14 -6
  103. package/dist/cli/commands/models.js +3 -2
  104. package/dist/cli/commands/schedules.d.ts +4 -2
  105. package/dist/cli/commands/schedules.js +14 -10
  106. package/dist/cli/commands/secrets.d.ts +14 -0
  107. package/dist/cli/commands/secrets.js +213 -0
  108. package/dist/cli/commands/start.d.ts +14 -14
  109. package/dist/cli/commands/start.js +46 -60
  110. package/dist/cli/content-view.d.ts +4 -6
  111. package/dist/cli/content-view.js +5 -11
  112. package/dist/cli/first-run-channels.d.ts +3 -0
  113. package/dist/cli/first-run-channels.js +63 -0
  114. package/dist/cli/instance.d.ts +41 -0
  115. package/dist/cli/instance.js +155 -0
  116. package/dist/cli/invoke-stream.d.ts +5 -2
  117. package/dist/cli/invoke-stream.js +7 -2
  118. package/dist/cli/program.js +282 -110
  119. package/dist/cli/serve.d.ts +4 -5
  120. package/dist/cli/serve.js +4 -7
  121. package/dist/cli/shared.d.ts +11 -2
  122. package/dist/cli/shared.js +32 -19
  123. package/dist/cli/ticket.js +50 -6
  124. package/dist/content/declare.d.ts +31 -37
  125. package/dist/content/declare.js +59 -60
  126. package/dist/content/file.d.ts +23 -12
  127. package/dist/content/file.js +48 -35
  128. package/dist/content/git.d.ts +0 -10
  129. package/dist/content/git.js +5 -39
  130. package/dist/content/resolve.d.ts +18 -22
  131. package/dist/content/resolve.js +32 -66
  132. package/dist/content/source.d.ts +7 -8
  133. package/dist/content/source.js +10 -28
  134. package/dist/credentials-page.d.ts +26 -0
  135. package/dist/credentials-page.js +185 -0
  136. package/dist/declared-secrets.d.ts +10 -3
  137. package/dist/declared-secrets.js +8 -4
  138. package/dist/deploy/agentcore/destroy.d.ts +4 -1
  139. package/dist/deploy/agentcore/destroy.js +25 -1
  140. package/dist/deploy/agentcore/forwarder.js +1 -1
  141. package/dist/deploy/agentcore/plan.d.ts +7 -21
  142. package/dist/deploy/agentcore/plan.js +47 -49
  143. package/dist/deploy/agentcore/run.d.ts +18 -10
  144. package/dist/deploy/agentcore/run.js +93 -70
  145. package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
  146. package/dist/deploy/agentcore/runtime-secrets.js +108 -0
  147. package/dist/deploy/agentcore/shell.js +12 -19
  148. package/dist/deploy/agentcore/sigv4.d.ts +7 -0
  149. package/dist/deploy/agentcore/sigv4.js +15 -0
  150. package/dist/deploy/agentcore/slr.d.ts +18 -0
  151. package/dist/deploy/agentcore/slr.js +27 -0
  152. package/dist/deploy/agentcore/store.d.ts +7 -0
  153. package/dist/deploy/agentcore/store.js +115 -0
  154. package/dist/deploy/api.d.ts +14 -14
  155. package/dist/deploy/api.js +26 -24
  156. package/dist/deploy/box-shell.d.ts +3 -1
  157. package/dist/deploy/build-context.d.ts +4 -4
  158. package/dist/deploy/build-context.js +11 -15
  159. package/dist/deploy/channel-ingress.d.ts +35 -9
  160. package/dist/deploy/channel-ingress.js +119 -22
  161. package/dist/deploy/container.d.ts +35 -10
  162. package/dist/deploy/container.js +79 -37
  163. package/dist/deploy/delivered-text.d.ts +13 -0
  164. package/dist/deploy/delivered-text.js +86 -0
  165. package/dist/deploy/docker/plan.d.ts +7 -8
  166. package/dist/deploy/docker/plan.js +31 -52
  167. package/dist/deploy/docker/run.d.ts +5 -43
  168. package/dist/deploy/docker/run.js +9 -84
  169. package/dist/deploy/file-store.d.ts +2 -0
  170. package/dist/deploy/file-store.js +17 -0
  171. package/dist/deploy/fly/plan.d.ts +3 -2
  172. package/dist/deploy/fly/plan.js +20 -9
  173. package/dist/deploy/fly/run.d.ts +18 -8
  174. package/dist/deploy/fly/run.js +24 -42
  175. package/dist/deploy/fly/store.d.ts +12 -0
  176. package/dist/deploy/fly/store.js +83 -0
  177. package/dist/deploy/hosts.d.ts +1 -2
  178. package/dist/deploy/hosts.js +1 -2
  179. package/dist/deploy/preflight.d.ts +18 -5
  180. package/dist/deploy/preflight.js +135 -63
  181. package/dist/deploy/railway/ignore.d.ts +8 -0
  182. package/dist/deploy/railway/ignore.js +57 -0
  183. package/dist/deploy/railway/plan.d.ts +1 -2
  184. package/dist/deploy/railway/plan.js +8 -6
  185. package/dist/deploy/railway/run.d.ts +21 -15
  186. package/dist/deploy/railway/run.js +32 -78
  187. package/dist/deploy/railway/store.d.ts +29 -0
  188. package/dist/deploy/railway/store.js +143 -0
  189. package/dist/deploy/runner.d.ts +5 -2
  190. package/dist/deploy/runner.js +4 -3
  191. package/dist/deploy/secret-store.d.ts +63 -0
  192. package/dist/deploy/secret-store.js +98 -0
  193. package/dist/deploy/secrets.d.ts +29 -46
  194. package/dist/deploy/secrets.js +49 -122
  195. package/dist/deploy/workspace.d.ts +6 -12
  196. package/dist/deploy/workspace.js +18 -94
  197. package/dist/dev-supervisor.js +6 -8
  198. package/dist/environment/declare.js +3 -2
  199. package/dist/environment/mise.d.ts +3 -1
  200. package/dist/environment/mise.js +4 -2
  201. package/dist/feishu.d.ts +1 -1
  202. package/dist/feishu.js +1 -1
  203. package/dist/harnesses/models/catalog.d.ts +1 -0
  204. package/dist/harnesses/models/catalog.js +1 -2
  205. package/dist/harnesses/models/environment.d.ts +7 -2
  206. package/dist/harnesses/models/environment.js +9 -5
  207. package/dist/harnesses/models/host.d.ts +0 -7
  208. package/dist/harnesses/models/host.js +10 -1
  209. package/dist/harnesses/models/login.d.ts +0 -8
  210. package/dist/harnesses/models/login.js +5 -4
  211. package/dist/harnesses/models/models.d.ts +7 -2
  212. package/dist/harnesses/models/models.js +28 -4
  213. package/dist/harnesses/pi-durable/agent.js +59 -38
  214. package/dist/harnesses/pi-durable/build.d.ts +12 -0
  215. package/dist/harnesses/pi-durable/build.js +270 -0
  216. package/dist/harnesses/pi-durable/built.d.ts +24 -0
  217. package/dist/harnesses/pi-durable/built.js +136 -0
  218. package/dist/harnesses/pi-durable/config.d.ts +81 -0
  219. package/dist/harnesses/pi-durable/config.js +266 -0
  220. package/dist/harnesses/pi-durable/define.d.ts +63 -13
  221. package/dist/harnesses/pi-durable/define.js +23 -2
  222. package/dist/harnesses/pi-durable/definition.d.ts +72 -0
  223. package/dist/harnesses/pi-durable/definition.js +143 -0
  224. package/dist/harnesses/pi-durable/events.d.ts +4 -3
  225. package/dist/harnesses/pi-durable/events.js +179 -136
  226. package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
  227. package/dist/harnesses/pi-durable/execution-env.js +11 -0
  228. package/dist/harnesses/pi-durable/mcp.js +1 -3
  229. package/dist/harnesses/pi-durable/open.d.ts +35 -37
  230. package/dist/harnesses/pi-durable/open.js +91 -153
  231. package/dist/harnesses/pi-durable/reads.d.ts +0 -1
  232. package/dist/harnesses/pi-durable/reads.js +1 -3
  233. package/dist/harnesses/pi-durable/registry.d.ts +4 -6
  234. package/dist/harnesses/pi-durable/registry.js +29 -26
  235. package/dist/harnesses/pi-durable/release.d.ts +2 -2
  236. package/dist/harnesses/pi-durable/release.js +10 -7
  237. package/dist/harnesses/pi-durable/serve.d.ts +91 -15
  238. package/dist/harnesses/pi-durable/serve.js +109 -47
  239. package/dist/harnesses/pi-durable/sessions.js +15 -12
  240. package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
  241. package/dist/harnesses/pi-durable/text-sources.js +55 -0
  242. package/dist/harnesses/pi-durable/text.d.ts +16 -4
  243. package/dist/harnesses/pi-durable/text.js +115 -59
  244. package/dist/harnesses/pi-durable/wake.js +8 -8
  245. package/dist/http/client.js +9 -3
  246. package/dist/http/contract.d.ts +3 -3
  247. package/dist/http/server.d.ts +1 -1
  248. package/dist/http/server.js +1 -1
  249. package/dist/index.d.ts +8 -5
  250. package/dist/index.js +4 -1
  251. package/dist/instance-name.d.ts +7 -0
  252. package/dist/instance-name.js +26 -0
  253. package/dist/lark.d.ts +1 -1
  254. package/dist/lark.js +1 -1
  255. package/dist/mcp/client.js +11 -2
  256. package/dist/mcp/config.d.ts +70 -18
  257. package/dist/mcp/config.js +136 -53
  258. package/dist/mcp/host.d.ts +5 -2
  259. package/dist/mcp/host.js +14 -6
  260. package/dist/mcp/sign-in.js +2 -2
  261. package/dist/mcp-api.d.ts +14 -0
  262. package/dist/paths.d.ts +22 -8
  263. package/dist/paths.js +44 -11
  264. package/dist/runtime-websocket.d.ts +14 -0
  265. package/dist/runtime-websocket.js +39 -0
  266. package/dist/scaffold/add-channel.d.ts +9 -16
  267. package/dist/scaffold/add-channel.js +13 -83
  268. package/dist/scaffold/templates/env.example +4 -5
  269. package/dist/scaffold/templates/fastagent.config.ts +20 -14
  270. package/dist/scaffold/templates/gitignore +9 -6
  271. package/dist/scaffold/templates.d.ts +6 -2
  272. package/dist/scaffold/templates.js +7 -2
  273. package/dist/scaffold/vendor-skill.js +4 -2
  274. package/dist/service.d.ts +44 -6
  275. package/dist/service.js +110 -10
  276. package/dist/serving-command.d.ts +10 -0
  277. package/dist/serving-command.js +1 -0
  278. package/dist/slack.d.ts +1 -1
  279. package/dist/slack.js +1 -1
  280. package/dist/strip-edges.d.ts +10 -0
  281. package/dist/strip-edges.js +22 -0
  282. package/dist/telegram.d.ts +1 -1
  283. package/dist/telegram.js +1 -1
  284. package/dist/tunnel.d.ts +0 -1
  285. package/dist/tunnel.js +2 -8
  286. package/dist/value-file.d.ts +11 -6
  287. package/dist/value-file.js +21 -9
  288. package/dist/version.d.ts +7 -1
  289. package/dist/version.js +9 -3
  290. package/package.json +6 -5
  291. package/dist/channels/lark/credentials-page.d.ts +0 -25
  292. package/dist/channels/lark/credentials-page.js +0 -206
  293. package/dist/channels/lark/onboard.d.ts +0 -52
  294. package/dist/channels/lark/onboard.js +0 -86
  295. package/dist/channels/slack/config-api.d.ts +0 -69
  296. package/dist/channels/slack/config-api.js +0 -162
  297. package/dist/channels/slack/onboard.d.ts +0 -40
  298. package/dist/channels/slack/onboard.js +0 -136
  299. package/dist/channels/slack/onboarding-state.d.ts +0 -31
  300. package/dist/channels/slack/onboarding-state.js +0 -64
  301. package/dist/channels/slack/register-webhook.d.ts +0 -12
  302. package/dist/channels/slack/register-webhook.js +0 -67
  303. package/dist/channels/slack/setup-server.d.ts +0 -32
  304. package/dist/channels/slack/setup-server.js +0 -246
  305. package/dist/cli/add-feishu.d.ts +0 -41
  306. package/dist/cli/add-feishu.js +0 -391
  307. package/dist/cli/add-slack.d.ts +0 -18
  308. package/dist/cli/add-slack.js +0 -216
  309. package/dist/harnesses/pi-durable/parked.d.ts +0 -18
  310. package/dist/harnesses/pi-durable/parked.js +0 -27
@@ -0,0 +1,197 @@
1
+ /**
2
+ * What a Feishu or Lark app's setup asks for (channels/setup.ts). The two share one engine: the app's credentials come
3
+ * first (Feishu creates the app by scan-to-create and keeps what it returns; Lark's person creates it in its console and
4
+ * pastes them), then, for a webhook, the Verification Token, captured by a throwaway webhook registration when the app
5
+ * allows it and pasted when it does not. What is left to the person is the permission work and the publish, which
6
+ * depend on what the platform says about the app.
7
+ */
8
+ import { pasteOne } from "../setup.js";
9
+ import { bootstrapFeishuVerificationToken } from "./bootstrap-token.js";
10
+ import { cloudFor } from "./cloud.js";
11
+ import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "./feishu-api.js";
12
+ import { registerFeishuApp } from "./register-app.js";
13
+ import { FEISHU_APP_CONFIG_SCOPE, FEISHU_MESSAGE_RECEIVE_EVENT, feishuAppAddons, feishuAppScopes, feishuScopeRequestUrl, scopeSatisfied, } from "./setup-mode.js";
14
+ const say = (message) => console.error(`[fastagent] ${message}`);
15
+ /** The page of an app where its Verification Token is, and where the subscription mode is chosen. */
16
+ const eventPage = (kind, appId) => `${cloudFor(kind).apiBase}/app/${encodeURIComponent(appId)}/event?tab=safe`;
17
+ /**
18
+ * What the app holds of what an app of this way needs ({@link feishuAppScopes}). A created app asks for them on its
19
+ * confirm page, and a tenant may still withhold some (its approval policy, or an app made by hand): those are NAMED,
20
+ * with what each one costs, and the console page that requests them is linked pre-filled. Nothing is requested through
21
+ * the config API: that needs a scope of its own, which a tenant can withhold just the same.
22
+ */
23
+ async function agentScopes(kind, way, appId, api) {
24
+ const apiBase = cloudFor(kind).apiBase;
25
+ const needed = feishuAppScopes(way);
26
+ const requested = needed.map((entry) => entry.request);
27
+ let scopes;
28
+ try {
29
+ scopes = await api.listAppScopes();
30
+ }
31
+ catch (error) {
32
+ return {
33
+ missing: [],
34
+ lines: [
35
+ `could not read the ${kind} app's permissions (${String(error)}): make sure it holds ${requested.join(", ")} ` +
36
+ `before publishing: ${feishuScopeRequestUrl(apiBase, appId, requested)}`,
37
+ ],
38
+ };
39
+ }
40
+ // A user-type entry is not the tenant scope the app acts with.
41
+ const tenant = scopes.filter((scope) => scope.type === undefined || scope.type === "tenant");
42
+ const granted = (name) => tenant.some((scope) => scope.name === name && scope.grantStatus === 1);
43
+ const missing = needed.filter((entry) => !scopeSatisfied(entry, granted));
44
+ if (missing.length === 0) {
45
+ say(`${kind} app permissions: ${requested.join(", ")} granted`);
46
+ return { missing: [], lines: [] };
47
+ }
48
+ // "Not granted", not "not requested": the list omits a scope still under review (measured), so the two look alike.
49
+ const describe = (entry) => `\n ${entry.request}: without it ${entry.withoutIt}`;
50
+ const url = feishuScopeRequestUrl(apiBase, appId, missing.map((entry) => entry.request));
51
+ return {
52
+ missing: missing.map((entry) => entry.request),
53
+ lines: [
54
+ `the ${kind} app is not granted:${missing.map(describe).join("")}\n` +
55
+ `Tick and enable them at ${url} (one may already be awaiting a tenant admin's approval), before publishing`,
56
+ ],
57
+ };
58
+ }
59
+ /** The setup of a Feishu or Lark app whose credentials are `credentials`. */
60
+ export function feishuEngine(kind, credentials, context) {
61
+ const { way, publicUrl, startTunnel } = context;
62
+ const { envPrefix, apiBase } = cloudFor(kind);
63
+ const idName = `${envPrefix}_APP_ID`;
64
+ const tokenName = `${envPrefix}_VERIFICATION_TOKEN`;
65
+ const idOf = (held) => held.get(idName);
66
+ const apiOf = (held) => createFeishuApi({
67
+ kind,
68
+ baseUrl: apiBase,
69
+ appId: idOf(held),
70
+ appSecret: held.get(`${envPrefix}_APP_SECRET`),
71
+ });
72
+ let scopes;
73
+ const scopesOf = (held) => (scopes ??= agentScopes(kind, way, idOf(held), apiOf(held)));
74
+ /** How the Verification Token came, when the setup went looking for it. */
75
+ let token;
76
+ async function capture(held) {
77
+ const appId = idOf(held);
78
+ const api = apiOf(held);
79
+ try {
80
+ const config = await api.getAppConfig(appId);
81
+ if (config.verificationToken) {
82
+ token = "read";
83
+ return config.verificationToken;
84
+ }
85
+ }
86
+ catch (error) {
87
+ if (isFeishuConfigApiMissing(error)) {
88
+ say(`this ${kind} app returned HTTP 404 for the application-config API, so its Verification Token cannot be captured automatically`);
89
+ token = "manual";
90
+ return undefined;
91
+ }
92
+ say(`could not read the app's Verification Token directly: ${error.message}`);
93
+ }
94
+ // The capture registers a Request URL through the config scope; without it every attempt is refused.
95
+ if ((await scopesOf(held)).missing.includes(FEISHU_APP_CONFIG_SCOPE)) {
96
+ say(`not capturing the Verification Token: the app is not granted ${FEISHU_APP_CONFIG_SCOPE}. Paste it from the ` +
97
+ `console, or have the permission approved and run \`${context.rerun}\` again`);
98
+ token = "pasted";
99
+ return undefined;
100
+ }
101
+ say("capturing the Verification Token: a throwaway webhook registration delivers it (a temporary tunnel; can take a few minutes on a slow edge)…");
102
+ try {
103
+ const captured = await bootstrapFeishuVerificationToken({
104
+ api,
105
+ appId,
106
+ kind,
107
+ startTunnel,
108
+ onTunnelReady: (url) => say(`temporary tunnel ready → ${url}; registering webhook mode now…`),
109
+ onPatchRetry: ({ error, attempt, attempts, retryMs }) => say(`${kind} could not validate the fresh tunnel yet (${String(error)}); retrying PATCH ${attempt + 1}/${attempts} in ${Math.round(retryMs / 1000)}s…`),
110
+ // Only edge weather is retried; a refusal (scope, auth, app under review) is reported as itself.
111
+ shouldRetryPatch: (error) => !isFeishuConfigApiMissing(error) && isTransientFeishuRegistrationError(error),
112
+ });
113
+ say("Verification Token captured; Subscription mode changed to webhook in the app draft");
114
+ token = "captured";
115
+ return captured;
116
+ }
117
+ catch (error) {
118
+ say(`warn: could not capture the Verification Token: ${String(error)}: paste it from the console`);
119
+ token = "pasted";
120
+ return undefined;
121
+ }
122
+ }
123
+ const values = [
124
+ credentials,
125
+ ...(way === "webhook"
126
+ ? [
127
+ pasteOne({
128
+ name: tokenName,
129
+ label: "Verification Token",
130
+ guide: (held) => ({
131
+ instructions: "Open Events & Callbacks → Encryption Strategy and copy the Verification Token.",
132
+ url: eventPage(kind, idOf(held)),
133
+ }),
134
+ // The first signed event says whether it is right; nothing can be asked of the platform before.
135
+ check: async () => undefined,
136
+ capture,
137
+ }),
138
+ ]
139
+ : []),
140
+ ];
141
+ return {
142
+ values,
143
+ async finish(held) {
144
+ const appId = idOf(held);
145
+ const findings = await scopesOf(held);
146
+ const ready = findings.lines.length === 0;
147
+ const then = ready ? "" : "after the permission work above, ";
148
+ const publish = `CREATE + PUBLISH a version: ${apiBase}/app/${encodeURIComponent(appId)}/version`;
149
+ const lines = [...findings.lines];
150
+ if (way === "connection") {
151
+ // A Feishu app made by scan-to-create is on a long connection already, with the event subscribed (measured).
152
+ lines.push(kind === "feishu"
153
+ ? `${then}${publish}`
154
+ : `in Events & Callbacks choose long connection and make sure ${FEISHU_MESSAGE_RECEIVE_EVENT} is subscribed ` +
155
+ `(no Verification Token, Request URL or tunnel is needed): ${eventPage(kind, appId)}; then ${then}${publish}`);
156
+ }
157
+ else if (token === "captured") {
158
+ lines.push(`the switch to webhook mode takes effect on publish: ${then}${publish}${kind === "feishu" ? " (self-approved)" : ""}`);
159
+ }
160
+ else if (token === "manual") {
161
+ lines.push(`this ${kind} app has no application-config API, so the instance cannot set its Request URL: choose webhook in ` +
162
+ `Events & Callbacks and set the Request URL to ${publicUrl ?? "https://<where it is served>"}/${kind}, ` +
163
+ `${then}${publish}, then run \`fastagent channels setup ${kind} --record-url --config <file>\``);
164
+ }
165
+ else if (token === "pasted") {
166
+ lines.push(`the instance sets the Subscription mode and Request URL itself when the app holds ${FEISHU_APP_CONFIG_SCOPE}; ` +
167
+ `otherwise choose webhook in Events & Callbacks and set the Request URL to ` +
168
+ `${publicUrl ?? "https://<where it is served>"}/${kind}, then ${then}${publish}`);
169
+ }
170
+ return lines;
171
+ },
172
+ };
173
+ }
174
+ export const feishuSetup = (context) => {
175
+ const { way, dev } = context;
176
+ return feishuEngine("feishu", {
177
+ kind: "create",
178
+ names: ["FEISHU_APP_ID", "FEISHU_APP_SECRET"],
179
+ async create(person) {
180
+ say("creating the Feishu app (confirm in the app)…");
181
+ const app = await registerFeishuApp({
182
+ name: dev ? "{user}'s agent" : "{user}'s production agent",
183
+ desc: "Served by fastagent",
184
+ // The agent template alone is not enough to SERVE, nor to hear a group (see feishuAppAddons).
185
+ addons: feishuAppAddons(way),
186
+ onVerificationUrl: ({ url, expiresInS }) => person.waitsOn(url, "confirm the new Feishu app (in a browser, or in Feishu, or as a QR code)", Date.now() + expiresInS * 1000),
187
+ });
188
+ person.carriesOn();
189
+ say(`app created: ${app.appId}${app.tenantBrand ? ` (${app.tenantBrand} tenant)` : ""}`);
190
+ // A cross-brand confirmation should be impossible (each confirm page refuses the other brand's code).
191
+ if (app.tenantBrand && app.tenantBrand !== "feishu") {
192
+ throw new Error(`the confirming account is a ${app.tenantBrand} tenant, but this is feishu: run \`fastagent channels setup ${app.tenantBrand}\` instead`);
193
+ }
194
+ return { FEISHU_APP_ID: app.appId, FEISHU_APP_SECRET: app.appSecret };
195
+ },
196
+ }, context);
197
+ };
@@ -0,0 +1,8 @@
1
+ /** Waiting that an abort ends: what a connection that retries and reconnects does between its attempts. */
2
+ /**
3
+ * Run `fn` when `signal` aborts; the returned function stops listening. A listener that is never removed stays on a
4
+ * signal that lives as long as the connection, and Node warns past ten of them.
5
+ */
6
+ export declare function whenAborted(signal: AbortSignal, fn: () => void): () => void;
7
+ /** Wait `ms`, or until `signal` aborts. */
8
+ export declare function pause(ms: number, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,22 @@
1
+ /** Waiting that an abort ends: what a connection that retries and reconnects does between its attempts. */
2
+ /**
3
+ * Run `fn` when `signal` aborts; the returned function stops listening. A listener that is never removed stays on a
4
+ * signal that lives as long as the connection, and Node warns past ten of them.
5
+ */
6
+ export function whenAborted(signal, fn) {
7
+ signal.addEventListener("abort", fn, { once: true });
8
+ return () => signal.removeEventListener("abort", fn);
9
+ }
10
+ /** Wait `ms`, or until `signal` aborts. */
11
+ export function pause(ms, signal) {
12
+ return new Promise((resolve) => {
13
+ const stopListening = whenAborted(signal, () => {
14
+ clearTimeout(timer);
15
+ resolve();
16
+ });
17
+ const timer = setTimeout(() => {
18
+ stopListening();
19
+ resolve();
20
+ }, ms);
21
+ });
22
+ }
@@ -1,7 +1,6 @@
1
1
  /** Lark-international compatibility adapter. */
2
2
  import type { ChannelModule, LongConnectionChannelModule } from "../../channel.ts";
3
3
  import { type FeishuChannelOptions, type FeishuFailure, type FeishuMessage, type FeishuMessageEvent, type FeishuRoute, type FeishuWebSocketChannelOptions, defaultFeishuRoute } from "../feishu/feishu.ts";
4
- import { type FeishuSubscriptionMode } from "../feishu/setup-mode.ts";
5
4
  export type LarkChannelOptions = FeishuChannelOptions;
6
5
  export type LarkWebSocketChannelOptions = FeishuWebSocketChannelOptions;
7
6
  export type LarkFailure = FeishuFailure;
@@ -11,6 +10,4 @@ export type LarkRoute = FeishuRoute;
11
10
  export declare const defaultLarkRoute: typeof defaultFeishuRoute;
12
11
  export declare function larkEnvelope(event: LarkMessageEvent): string;
13
12
  export declare function larkChannel(opts: LarkChannelOptions): ChannelModule;
14
- /** How this process receives the Lark channel: `LARK_INGRESS`, else WebSocket under `dev` and webhook otherwise. */
15
- export declare function larkIngress(): FeishuSubscriptionMode;
16
13
  export declare function larkWebSocketChannel(opts: LarkWebSocketChannelOptions): LongConnectionChannelModule;
@@ -1,7 +1,6 @@
1
1
  import { LARK_COMPAT_CLOUD } from "../feishu/cloud.js";
2
2
  import { buildFeishuChannel, buildFeishuWebSocketChannel, defaultFeishuRoute, } from "../feishu/feishu.js";
3
3
  import { cloudEnvelope } from "../feishu/parse.js";
4
- import { feishuIngressFor } from "../feishu/setup-mode.js";
5
4
  export const defaultLarkRoute = defaultFeishuRoute;
6
5
  export function larkEnvelope(event) {
7
6
  return cloudEnvelope(event, "lark");
@@ -9,10 +8,6 @@ export function larkEnvelope(event) {
9
8
  export function larkChannel(opts) {
10
9
  return buildFeishuChannel(LARK_COMPAT_CLOUD, opts, larkChannel.name);
11
10
  }
12
- /** How this process receives the Lark channel: `LARK_INGRESS`, else WebSocket under `dev` and webhook otherwise. */
13
- export function larkIngress() {
14
- return feishuIngressFor("lark");
15
- }
16
11
  export function larkWebSocketChannel(opts) {
17
12
  return buildFeishuWebSocketChannel(LARK_COMPAT_CLOUD, opts, larkWebSocketChannel.name);
18
13
  }
@@ -1,22 +1,19 @@
1
- import { needs } from "@fastagent-sh/fastagent";
2
- import {
3
- type LarkChannelOptions,
4
- larkChannel,
5
- larkIngress,
6
- larkWebSocketChannel,
7
- } from "@fastagent-sh/fastagent/lark";
1
+ import { byIngress, needs } from "@fastagent-sh/fastagent";
2
+ import { type LarkChannelOptions, larkChannel, larkWebSocketChannel } from "@fastagent-sh/fastagent/lark";
8
3
 
9
4
  // The branded Lark adapter over fastagent's canonical Feishu engine (verify + run + reply), configured
10
5
  // with YOUR policy. Lark international (open.larksuite.com) only; a Feishu tenant uses `fastagent add
11
6
  // feishu` instead.
12
7
  //
13
- // How it receives is LARK_INGRESS (webhook | websocket). Unset, `fastagent dev` connects OUT by
14
- // WebSocket (no public URL, no tunnel), and everything else — `fastagent start`, every deployment —
15
- // receives by webhook at POST /lark. Dev reads .secrets/.env; production reads
16
- // .secrets/production/.env. Interactive `deploy --run` guides creation of a separate production app;
17
- // if Lark refuses its config API, set the Request URL in the console once.
8
+ // How it receives is the instance's choice, `channels.lark.ingress` in its fastagent.json (webhook |
9
+ // connection). Unset, `fastagent dev` opens a long connection OUT (no public URL, no tunnel), and everything
10
+ // else — `fastagent start`, every deployment — receives by webhook at POST /lark, the way a host that
11
+ // wakes on a request is reached.
12
+ // Dev reads .secrets/.env; production reads .secrets/production/.env. Interactive `deploy --run`
13
+ // guides creation of a separate production app; if Lark refuses its config API, set the Request URL
14
+ // in the console once.
18
15
  //
19
- // `fastagent add lark` guides the console setup and validates the credentials. In the developer
16
+ // `fastagent channels setup lark` guides the console setup and validates the credentials. In the developer
20
17
  // console:
21
18
  // 1. create a custom app → enable the BOT capability → copy App ID / App Secret into .env
22
19
  // 2. Permissions: add `im:message.p2p_msg:readonly` (direct messages), `im:message.group_at_msg:readonly`
@@ -47,25 +44,26 @@ const policy: Pick<LarkChannelOptions, "onError" | "route"> = {
47
44
  // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
48
45
  };
49
46
 
50
- // `needs` declares the env vars this channel reads: fastagent carries them to a deployed box and
51
- // refuses to serve while a required one is unset. LARK_ENCRYPT_KEY is optional.
52
- export default larkIngress() === "webhook"
53
- ? needs(
54
- [
55
- "LARK_APP_ID",
56
- "LARK_APP_SECRET",
57
- "LARK_VERIFICATION_TOKEN",
58
- { name: "LARK_ENCRYPT_KEY", description: "when set, plaintext events are refused", optional: true },
59
- ],
60
- (secrets) =>
61
- larkChannel({
62
- appId: secrets.LARK_APP_ID,
63
- appSecret: secrets.LARK_APP_SECRET,
64
- verificationToken: secrets.LARK_VERIFICATION_TOKEN, // authenticates inbound events
65
- encryptKey: secrets.LARK_ENCRYPT_KEY,
66
- ...policy,
67
- }),
68
- )
69
- : needs(["LARK_APP_ID", "LARK_APP_SECRET"], (secrets) =>
70
- larkWebSocketChannel({ appId: secrets.LARK_APP_ID, appSecret: secrets.LARK_APP_SECRET, ...policy }),
71
- );
47
+ // Each way's `needs` declares the env vars it reads: an instance is asked only for its way's, and
48
+ // refused to serve while a required one is unset. LARK_ENCRYPT_KEY is optional.
49
+ export default byIngress({
50
+ webhook: needs(
51
+ [
52
+ "LARK_APP_ID",
53
+ "LARK_APP_SECRET",
54
+ "LARK_VERIFICATION_TOKEN",
55
+ { name: "LARK_ENCRYPT_KEY", description: "when set, plaintext events are refused", optional: true },
56
+ ],
57
+ (secrets) =>
58
+ larkChannel({
59
+ appId: secrets.LARK_APP_ID,
60
+ appSecret: secrets.LARK_APP_SECRET,
61
+ verificationToken: secrets.LARK_VERIFICATION_TOKEN, // authenticates inbound events
62
+ encryptKey: secrets.LARK_ENCRYPT_KEY,
63
+ ...policy,
64
+ }),
65
+ ),
66
+ connection: needs(["LARK_APP_ID", "LARK_APP_SECRET"], (secrets) =>
67
+ larkWebSocketChannel({ appId: secrets.LARK_APP_ID, appSecret: secrets.LARK_APP_SECRET, ...policy }),
68
+ ),
69
+ });
@@ -1,4 +1,4 @@
1
- import { Type } from "@earendil-works/pi-ai";
1
+ import { obj, opt, str } from "@earendil-works/pi-schema";
2
2
  import { defineTool } from "@earendil-works/pi-durable";
3
3
  import { needs } from "@fastagent-sh/fastagent";
4
4
  import { larkTransport } from "@fastagent-sh/fastagent/lark";
@@ -18,10 +18,10 @@ export default needs(["LARK_APP_ID", "LARK_APP_SECRET"], (secrets) =>
18
18
  "or memory); the [lark: chat …] context line only identifies the chat you are answering, the " +
19
19
  "one chat this tool must not target in a chat turn. Pass exactly ONE of `text` (plain) or " +
20
20
  "`markdown` (rendered as a card: headings, bold, code blocks, links).",
21
- parameters: Type.Object({
22
- chatId: Type.String({ description: "target chat id (oc_…)" }),
23
- text: Type.Optional(Type.String({ description: "plain text message to send" })),
24
- markdown: Type.Optional(Type.String({ description: "markdown to send as a card" })),
21
+ parameters: obj({
22
+ chatId: str({ description: "target chat id (oc_…)" }),
23
+ text: opt(str({ description: "plain text message to send" })),
24
+ markdown: opt(str({ description: "markdown to send as a card" })),
25
25
  }),
26
26
  execute: async ({ chatId, text, markdown }) => {
27
27
  if ((text === undefined) === (markdown === undefined)) {
@@ -1,4 +1,4 @@
1
- import { Type } from "@earendil-works/pi-ai";
1
+ import { obj, opt, str } from "@earendil-works/pi-schema";
2
2
  import { defineTool } from "@earendil-works/pi-durable";
3
3
  import { sessionIdOf } from "@fastagent-sh/fastagent";
4
4
  import { larkThreads } from "@fastagent-sh/fastagent/lark";
@@ -14,8 +14,8 @@ export default defineTool({
14
14
  "it when a question here concerns something discussed in a thread, such as whether a problem raised there was " +
15
15
  "resolved: the answer is often what a person wrote in the thread. Only this chat's threads can be read, and " +
16
16
  "only in a group chat turn.",
17
- parameters: Type.Object({
18
- threadId: Type.Optional(Type.String({ description: "a thread id from the list (omt_…); omit it to list" })),
17
+ parameters: obj({
18
+ threadId: opt(str({ description: "a thread id from the list (omt_…); omit it to list" })),
19
19
  }),
20
20
  execute: async ({ threadId }, api, context) => {
21
21
  const threads = larkThreads(await sessionIdOf(api, context));
@@ -0,0 +1,9 @@
1
+ import { type ChannelSetup, type SetupWay } from "../setup.ts";
2
+ /**
3
+ * Lark's one-click launcher, where the person creates the app. The launcher is unbound (the bound confirm page is broken
4
+ * on the international cloud), but it merges the `addons` query parameter onto the app it creates, as Feishu's does: the
5
+ * scopes and the event the channel needs arrive with the app, so nothing is ticked in the console afterwards. A tenant
6
+ * may still withhold a scope; the permission check that follows creation names it.
7
+ */
8
+ export declare function larkLauncherUrl(way: SetupWay): string;
9
+ export declare const larkSetup: ChannelSetup;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * What a Lark app's setup asks for (channels/setup.ts). Lark's international cloud cannot complete the bound
3
+ * scan-to-create flow, so its person creates the app on the one-click launcher and pastes the App ID and App Secret,
4
+ * which Lark checks as a pair; the rest is the Feishu engine's.
5
+ */
6
+ import { encodeAddons } from "../feishu/register-app.js";
7
+ import { createFeishuApi, isFeishuCredentialsRefused } from "../feishu/feishu-api.js";
8
+ import { feishuEngine } from "../feishu/setup.js";
9
+ import { feishuAppAddons } from "../feishu/setup-mode.js";
10
+ import { SetupRefused } from "../setup.js";
11
+ import { cloudFor } from "../feishu/cloud.js";
12
+ /**
13
+ * Lark's one-click launcher, where the person creates the app. The launcher is unbound (the bound confirm page is broken
14
+ * on the international cloud), but it merges the `addons` query parameter onto the app it creates, as Feishu's does: the
15
+ * scopes and the event the channel needs arrive with the app, so nothing is ticked in the console afterwards. A tenant
16
+ * may still withhold a scope; the permission check that follows creation names it.
17
+ */
18
+ export function larkLauncherUrl(way) {
19
+ const url = new URL("https://open.larksuite.com/page/launcher");
20
+ url.searchParams.set("from", "backend_oneclick");
21
+ url.searchParams.set("addons", encodeAddons(feishuAppAddons(way)));
22
+ return url.toString();
23
+ }
24
+ export const larkSetup = (context) => feishuEngine("lark", {
25
+ kind: "paste",
26
+ label: "App ID and App Secret",
27
+ guide: {
28
+ instructions: "Create the app on Lark's one-click launcher (it comes with the scopes and the event the channel needs), " +
29
+ "then copy its App ID and App Secret from Credentials & Basic Info.",
30
+ url: larkLauncherUrl(context.way),
31
+ },
32
+ fields: [
33
+ { name: "LARK_APP_ID", label: "App ID", secret: false },
34
+ { name: "LARK_APP_SECRET", label: "App Secret" },
35
+ ],
36
+ async check(values) {
37
+ const api = createFeishuApi({
38
+ kind: "lark",
39
+ baseUrl: cloudFor("lark").apiBase,
40
+ appId: values.LARK_APP_ID,
41
+ appSecret: values.LARK_APP_SECRET,
42
+ });
43
+ try {
44
+ await api.verifyCredentials();
45
+ return undefined;
46
+ }
47
+ catch (error) {
48
+ // Only a known credential refusal is a wrong pair; the network and the platform being down end the setup.
49
+ if (!isFeishuCredentialsRefused(error))
50
+ throw error;
51
+ throw new SetupRefused("Lark refused that App ID or Secret.", {
52
+ fix: "Copy both again from Credentials & Basic Info.",
53
+ why: `Lark said: ${error instanceof Error ? error.message : String(error)}`,
54
+ });
55
+ }
56
+ },
57
+ }, context);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * What setting up a channel's platform app asks of a person (channel-setup.md §2): the values only they have, where
3
+ * to find each one, and how to tell a wrong one from a right one before it is kept. A channel's own module says it
4
+ * (`channels/<kind>/setup.ts`); `cli/channel-setup.ts` asks, in a terminal or on a page, and writes what is accepted.
5
+ */
6
+ /** How a channel receives (SPEC.md §8): which values its app needs depends on it. */
7
+ export type SetupWay = "webhook" | "connection";
8
+ /** Where a value is found: what to do, and the page to do it on. */
9
+ export interface SetupGuide {
10
+ instructions: string;
11
+ url?: string;
12
+ }
13
+ /** The values of this setup so far: what the value file held and what each step has since kept. */
14
+ export type HeldValues = ReadonlyMap<string, string>;
15
+ /** One variable a pasted step asks for. */
16
+ interface PasteField {
17
+ name: string;
18
+ /** What the platform calls it, so the person need not map one name to another. */
19
+ label: string;
20
+ /** Whether it is hidden as typed; every value is unless it is only an identifier (default true). */
21
+ secret?: boolean;
22
+ }
23
+ /**
24
+ * Values the person pastes: one step, its fields asked together and checked together (a Lark App ID is only right
25
+ * with its Secret).
26
+ */
27
+ export interface PastedValues {
28
+ kind: "paste";
29
+ /** What the step is called on a page: "Bot User OAuth Token". */
30
+ label: string;
31
+ /** Where to find them; a function when the page depends on what an earlier step kept (an app's own console page). */
32
+ guide: SetupGuide | ((held: HeldValues) => SetupGuide);
33
+ fields: PasteField[];
34
+ /**
35
+ * Confirm `values` with the platform. Resolves with a note on what they are ("bot @acme_bot"), throws {@link
36
+ * SetupRefused} for values the platform refuses, and any other error for a check that could not be made (the
37
+ * network, the platform down), which ends the setup instead of re-asking for a value that may be right.
38
+ */
39
+ check(values: Record<string, string>): Promise<string | undefined>;
40
+ /**
41
+ * A way to get the value without asking (one field only): tried first, and the person is asked only when it
42
+ * resolves undefined, having said why. Not tried for a value that is already held.
43
+ */
44
+ capture?(held: HeldValues): Promise<string | undefined>;
45
+ }
46
+ /** One pasted variable, checked on its own. */
47
+ export declare function pasteOne(input: {
48
+ name: string;
49
+ label: string;
50
+ secret?: boolean;
51
+ guide: PastedValues["guide"];
52
+ check(value: string): Promise<string | undefined>;
53
+ capture?: PastedValues["capture"];
54
+ }): PastedValues;
55
+ /** Values the platform makes when an app is created, which setup makes happen: all of them, or none. */
56
+ interface CreatedValues {
57
+ kind: "create";
58
+ names: string[];
59
+ /** Create the app. `person` takes the step only they can (confirming it in the platform's own app). */
60
+ create(person: SetupPerson): Promise<Record<string, string>>;
61
+ }
62
+ /** What a creation waits on a person for; the parts of a `cli/person.ts` Person it uses. */
63
+ interface SetupPerson {
64
+ waitsOn(url: string, instructions: string, until?: number): void;
65
+ carriesOn(): void;
66
+ }
67
+ /** A value setup makes itself. */
68
+ interface MadeValue {
69
+ kind: "make";
70
+ name: string;
71
+ make(): string;
72
+ }
73
+ export type SetupValue = PastedValues | CreatedValues | MadeValue;
74
+ /** What a channel needs to say its values: facts about the instance it is set up for. */
75
+ interface SetupContext {
76
+ way: SetupWay;
77
+ /** A name for an app created for it (the agent's directory). */
78
+ appName: string;
79
+ /** Whether it is the development instance. */
80
+ dev: boolean;
81
+ /** Where the platform will reach it, when the configuration names one. */
82
+ publicUrl?: string;
83
+ /** The command that runs this setup again, for what a person is told to do before it. */
84
+ rerun: string;
85
+ /** A public URL for a local port, for a setup the platform must reach while it runs (a challenge to answer). */
86
+ startTunnel(port: number): Promise<{
87
+ url: string;
88
+ close(): void;
89
+ } | undefined>;
90
+ }
91
+ /**
92
+ * What a channel's setup asks for, in order, and what is left to the person once it is done (`finish`, told every
93
+ * value the instance now holds: what is left can depend on what the platform said about the app).
94
+ */
95
+ interface SetupPlan {
96
+ values: SetupValue[];
97
+ finish(held: HeldValues): Promise<string[]> | string[];
98
+ }
99
+ export type ChannelSetup = (context: SetupContext) => SetupPlan;
100
+ /**
101
+ * A pasted value the platform refuses. `fix` is what to do about it; `why` is the platform's own words, for someone
102
+ * who has to look it up.
103
+ */
104
+ export declare class SetupRefused extends Error {
105
+ readonly fix: string | undefined;
106
+ readonly why: string | undefined;
107
+ constructor(headline: string, options?: {
108
+ fix?: string;
109
+ why?: string;
110
+ });
111
+ }
112
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * What setting up a channel's platform app asks of a person (channel-setup.md §2): the values only they have, where
3
+ * to find each one, and how to tell a wrong one from a right one before it is kept. A channel's own module says it
4
+ * (`channels/<kind>/setup.ts`); `cli/channel-setup.ts` asks, in a terminal or on a page, and writes what is accepted.
5
+ */
6
+ /** One pasted variable, checked on its own. */
7
+ export function pasteOne(input) {
8
+ const { name, label, secret, guide, check, capture } = input;
9
+ return {
10
+ kind: "paste",
11
+ label,
12
+ guide,
13
+ fields: [{ name, label, ...(secret === undefined ? {} : { secret }) }],
14
+ check: (values) => check(values[name]),
15
+ ...(capture === undefined ? {} : { capture }),
16
+ };
17
+ }
18
+ /**
19
+ * A pasted value the platform refuses. `fix` is what to do about it; `why` is the platform's own words, for someone
20
+ * who has to look it up.
21
+ */
22
+ export class SetupRefused extends Error {
23
+ fix;
24
+ why;
25
+ constructor(headline, options = {}) {
26
+ super(headline);
27
+ this.name = "SetupRefused";
28
+ this.fix = options.fix;
29
+ this.why = options.why;
30
+ }
31
+ }
@@ -186,7 +186,7 @@ export function createSlackPlaceHistory(deps) {
186
186
  names.scopeMissing = true;
187
187
  if (!scopeWarned) {
188
188
  scopeWarned = true;
189
- log.warn(`${deps.label} users:read is not granted, so people are shown by user id — add it under the Slack app's OAuth & Permissions → Bot Token Scopes (an app \`fastagent add slack\` created gets it with the next \`dev --tunnel\` or \`deploy --run\`), then Reinstall to Workspace; names appear within 10 minutes, or after a restart if the reinstall issued a new Bot Token`);
189
+ log.warn(`${deps.label} users:read is not granted, so people are shown by user id — add it under the Slack app's OAuth & Permissions → Bot Token Scopes (an app \`fastagent channels setup slack\` creates has it), then Reinstall to Workspace; names appear within 10 minutes, or after a restart if the reinstall issued a new Bot Token`);
190
190
  }
191
191
  }
192
192
  else if (failures.length > 0) {
@@ -26,11 +26,10 @@ export interface SlackAppManifest {
26
26
  scopes: {
27
27
  bot: string[];
28
28
  };
29
- redirect_urls?: string[];
30
29
  };
31
30
  settings: {
32
- event_subscriptions?: {
33
- request_url: string;
31
+ event_subscriptions: {
32
+ request_url?: string;
34
33
  bot_events: string[];
35
34
  };
36
35
  org_deploy_enabled: boolean;
@@ -38,9 +37,20 @@ export interface SlackAppManifest {
38
37
  token_rotation_enabled: boolean;
39
38
  };
40
39
  }
41
- export declare function slackBotScopes(): string[];
40
+ /**
41
+ * What an app is created for: a Socket Mode app receives over a socket it opens and has no Request URL; one that
42
+ * receives by webhook needs one, because Slack refuses a manifest that subscribes to events without it. Slack creates
43
+ * the app whether or not that URL answers yet (it shows "Your URL didn't respond" and a Retry), so a placeholder stands
44
+ * in until a person sets the real one on the Event Subscriptions page.
45
+ */
46
+ export type SlackManifestWay = {
47
+ ingress: "connection";
48
+ } | {
49
+ ingress: "webhook";
50
+ requestUrl: string;
51
+ };
42
52
  export declare function buildSlackManifest(input: {
43
53
  name: string;
44
- requestUrl?: string;
45
- redirectUrl?: string;
46
- }): SlackAppManifest;
54
+ } & SlackManifestWay): SlackAppManifest;
55
+ /** The page that creates an app from `manifest`, filled in: the person picks a workspace and clicks Create. */
56
+ export declare function slackManifestLink(manifest: SlackAppManifest): string;