@fastagent-sh/fastagent 0.14.0 → 0.16.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 (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -0,0 +1,366 @@
1
+ /**
2
+ * `fastagent add feishu|lark` app onboarding — the cloud-facing half of runAdd, kept out of cli.ts
3
+ * (which self-executes on import), mirroring models-view.ts/auth-view.ts. The POLICY already lives in
4
+ * testable modules (register-app.ts, bootstrap-token.ts, lark/onboard.ts); this layer is the terminal
5
+ * wiring: clack prompts, .env staging, browser opens, progress lines.
6
+ *
7
+ * Feishu (scan-to-create): the device flow creates the app and persists App ID/Secret at the
8
+ * irreversible boundary. WebSocket stops there; webhook additionally captures the Verification Token
9
+ * over a throwaway tunnel. Lark uses the unbound launcher + credential validation, then either stops
10
+ * for WebSocket or probes the same webhook/token bootstrap with the config-route-404 manual fallback.
11
+ */
12
+ import { readFile } from "node:fs/promises";
13
+ import { isCancel, log as clackLog, password, text as clackText } from "@clack/prompts";
14
+ import { bootstrapFeishuVerificationToken } from "../channels/feishu/bootstrap-token.js";
15
+ import { FEISHU_GROUP_CONTEXT_SCOPE, FEISHU_CONTEXT_ONBOARDING_SCOPES, FEISHU_MESSAGE_READ_SCOPE, scopeSatisfied, } from "../channels/feishu/setup-mode.js";
16
+ import { cloudFor } from "../channels/feishu/cloud.js";
17
+ import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "../channels/feishu/feishu-api.js";
18
+ import { registerFeishuApp } from "../channels/feishu/register-app.js";
19
+ import { onboardLarkApp } from "../channels/lark/onboard.js";
20
+ import { dotEnvPath, parseEnvContent } from "../env.js";
21
+ import { openExternalUrl } from "../open-url.js";
22
+ import { appendChannelDotEnv } from "../scaffold/add-channel.js";
23
+ import { startCloudflareTunnel } from "../tunnel.js";
24
+ export async function configureGroupBehavior(input) {
25
+ const { kind, appId, apiBase, api, behavior, explicit } = input;
26
+ const note = input.note ?? ((message) => console.error(message));
27
+ const openUrl = input.openUrl ?? openExternalUrl;
28
+ let scopes;
29
+ let inspected = true;
30
+ try {
31
+ scopes = await api.listAppScopes();
32
+ }
33
+ catch (error) {
34
+ note(`[fastagent] warn: could not inspect ${kind} group-message permission: ${String(error)} — check Permissions & Scopes manually`);
35
+ inspected = false;
36
+ scopes = [];
37
+ }
38
+ const granted = (name) => scopes.some((scope) => scope.name === name && scope.grantStatus === 1 && (scope.type === undefined || scope.type === "tenant"));
39
+ // Same type filter as `granted`: a user-type entry is not the tenant scope this path needs, so
40
+ // treating one as "on the app" would report an approval that can never arrive and skip the PATCH
41
+ // that would actually add it.
42
+ const onApp = (name) => scopes.some((scope) => scope.name === name && (scope.type === undefined || scope.type === "tenant"));
43
+ // Both halves of the recommended path: delivery (the platform pushes un-mentioned group messages)
44
+ // and reading a quoted message (so a thread's opening ask carries what it replies to).
45
+ // The read capability has two spellings and `im:message` is the superset, so an app holding it needs
46
+ // nothing added — asking anyway would cost the author a second tenant-admin approval round.
47
+ const missing = FEISHU_CONTEXT_ONBOARDING_SCOPES.filter((entry) => !scopeSatisfied(entry, granted));
48
+ const missingNames = missing.map((entry) => entry.request);
49
+ if (behavior === "mentions") {
50
+ if (!inspected) {
51
+ const permissionUrl = `${apiBase}/app/${encodeURIComponent(appId)}/permission`;
52
+ note(`[fastagent] mention-only scope state could not be verified — check Permissions before publishing. Opening ${permissionUrl}`);
53
+ openUrl(permissionUrl);
54
+ return { publishReady: false };
55
+ }
56
+ if (granted(FEISHU_GROUP_CONTEXT_SCOPE)) {
57
+ const permissionUrl = `${apiBase}/app/${encodeURIComponent(appId)}/permission`;
58
+ note(`[fastagent] warn: mention-only was selected, but ${FEISHU_GROUP_CONTEXT_SCOPE} is already granted — remove it before publishing a new version to restore least-privilege platform delivery. Opening ${permissionUrl}`);
59
+ openUrl(permissionUrl);
60
+ return { publishReady: false };
61
+ }
62
+ note(`[fastagent] group behavior: mention-only — bare replies in the Agent's threads and group context buffering are disabled. ` +
63
+ `Add ${FEISHU_MESSAGE_READ_SCOPE} by hand if you want an @mention to carry the message it quotes`);
64
+ return { publishReady: true };
65
+ }
66
+ note(`[fastagent] group behavior: context-aware (recommended) — ${kind} will deliver all group messages; ` +
67
+ `FastAgent invokes @Agent, answers bare replies in threads it takes part in, and durably buffers ` +
68
+ `other discussion. ${FEISHU_GROUP_CONTEXT_SCOPE} (delivery) is required; ${FEISHU_MESSAGE_READ_SCOPE} ` +
69
+ `(reading a quoted message) is requested with it — without it a quoted message degrades to a marker`);
70
+ if (missing.length === 0) {
71
+ note(`[fastagent] ${FEISHU_CONTEXT_ONBOARDING_SCOPES.map((entry) => entry.request).join(" + ")} are already granted`);
72
+ return { publishReady: true };
73
+ }
74
+ const permissionUrl = `${apiBase}/app/${encodeURIComponent(appId)}/permission`;
75
+ // A missing scope is in one of two states, and they need different actions: already on the app but
76
+ // not yet approved (nothing to add — wait for the admin), or absent from the draft entirely.
77
+ // Same superset rule as `missing`: a draft already requesting `im:message` is awaiting approval, not
78
+ // missing something to add.
79
+ const awaitingApproval = missing.filter((entry) => scopeSatisfied(entry, onApp)).map((entry) => entry.request);
80
+ const toRequest = missing.filter((entry) => !scopeSatisfied(entry, onApp)).map((entry) => entry.request);
81
+ if (toRequest.length === 0) {
82
+ note(`[fastagent] ${awaitingApproval.join(" + ")} awaiting approval — complete tenant-admin approval before publishing. Opening ${permissionUrl}`);
83
+ openUrl(permissionUrl);
84
+ return { publishReady: false };
85
+ }
86
+ if (!explicit) {
87
+ // Defaulted, not chosen: report the gap and how to opt in, but leave the app's requested
88
+ // permission set untouched (a scripted re-run must not silently escalate a mention-only app).
89
+ // Name what is ACTUALLY missing: the common upgrade has the delivery scope already granted and
90
+ // only the read scope absent, and pointing at a granted permission sends the author looking in
91
+ // the wrong place.
92
+ note(`[fastagent] ${missingNames.join(" + ")} not granted (or could not be verified) — group behavior was ` +
93
+ `defaulted, so nothing was requested. Re-run with --group-behavior context to add ${missing.length > 1 ? "them" : "it"} ` +
94
+ `to the app draft, or --group-behavior mentions to stay least-privilege: ${permissionUrl}`);
95
+ return { publishReady: false };
96
+ }
97
+ try {
98
+ await api.addAppScopes(appId, toRequest);
99
+ note(`[fastagent] added ${toRequest.join(" + ")} to the app draft — complete tenant-admin approval before publishing. Opening ${permissionUrl}`);
100
+ }
101
+ catch (error) {
102
+ note(`[fastagent] warn: could not add ${toRequest.join(" + ")} automatically: ${String(error)} — add it manually before publishing. Opening ${permissionUrl}`);
103
+ }
104
+ openUrl(permissionUrl);
105
+ return { publishReady: false };
106
+ }
107
+ /**
108
+ * Create or resume the platform app behind `add feishu` / `add lark`. `target` is the AGENT DIR;
109
+ * credentials land in its `.env` — whose real path is {@link dotEnvPath}, printed rather than spelled,
110
+ * because `FASTAGENT_SECRETS_DIR` moves it and a hardcoded `.secrets/.env` would name a file this run
111
+ * did not write. Returns credentials for the
112
+ * caller's generic .env write (the guided Lark path), or undefined when nothing remains to write —
113
+ * the feishu path persists its own two credential stages internally (the App ID/Secret boundary is
114
+ * irreversible and must not wait for the caller). Throws on refusal (a non-interactive lark run);
115
+ * the caller surfaces that as a startup failure.
116
+ */
117
+ export async function onboardFeishuCloudApp(target, kind, ingress = "webhook", groupBehavior = { behavior: "context", explicit: false }) {
118
+ const env = dotEnvPath(target); // the file actually written — never the default spelling
119
+ const { envPrefix, apiBase, capabilities } = cloudFor(kind);
120
+ const requiredNames = [
121
+ `${envPrefix}_APP_ID`,
122
+ `${envPrefix}_APP_SECRET`,
123
+ ...(ingress === "webhook" ? [`${envPrefix}_VERIFICATION_TOKEN`] : []),
124
+ ];
125
+ const existing = await activeDotEnvValues(target, requiredNames);
126
+ if (Object.keys(existing).length === requiredNames.length) {
127
+ console.error(`[fastagent] ${requiredNames.join("/")} already set in ${env} — keeping them`);
128
+ // WebSocket still needs its console mode/publish guidance. A complete webhook can skip the rest of
129
+ // onboarding, but group visibility must still be inspected/configured on every explicit re-run.
130
+ if (ingress === "webhook") {
131
+ const appId = existing[`${envPrefix}_APP_ID`];
132
+ const appSecret = existing[`${envPrefix}_APP_SECRET`];
133
+ await configureGroupBehavior({
134
+ kind,
135
+ appId,
136
+ apiBase,
137
+ api: createFeishuApi({ kind, baseUrl: apiBase, appId, appSecret }),
138
+ behavior: groupBehavior.behavior,
139
+ explicit: groupBehavior.explicit,
140
+ });
141
+ return undefined;
142
+ }
143
+ }
144
+ if (capabilities.appCreation === "scan-to-create") {
145
+ await createFeishuAppFlow(target, existing, ingress, groupBehavior);
146
+ return undefined;
147
+ }
148
+ // guided-console (lark): the intl cloud cannot complete the bound device flow — collect + validate.
149
+ if (!(process.stdin.isTTY && process.stdout.isTTY) &&
150
+ !(existing[`${envPrefix}_APP_ID`] && existing[`${envPrefix}_APP_SECRET`])) {
151
+ throw new Error("`add lark` needs an interactive terminal to onboard the Lark app credentials — re-run it in a terminal");
152
+ }
153
+ const credentials = await onboardLarkApp({
154
+ openUrl: openExternalUrl,
155
+ note: (message) => clackLog.info(message),
156
+ async prompt(message, opts) {
157
+ const result = opts?.hidden ? await password({ message }) : await clackText({ message });
158
+ return isCancel(result) ? undefined : result;
159
+ },
160
+ }, {
161
+ existing,
162
+ ingress,
163
+ groupBehavior: groupBehavior.behavior,
164
+ verifyCredentials: async (appId, appSecret) => {
165
+ await createFeishuApi({ kind: "lark", baseUrl: apiBase, appId, appSecret }).verifyCredentials();
166
+ console.error(`[fastagent] Lark App ID / Secret verified`);
167
+ },
168
+ bootstrapWebhook: async (appId, appSecret) => {
169
+ const api = createFeishuApi({ kind: "lark", baseUrl: apiBase, appId, appSecret });
170
+ console.error(`[fastagent] trying Lark's webhook-mode + Verification-Token bootstrap (temporary tunnel)…`);
171
+ try {
172
+ const token = await bootstrapFeishuVerificationToken({
173
+ api,
174
+ appId,
175
+ kind: "lark",
176
+ startTunnel: (port) => startCloudflareTunnel(port),
177
+ onTunnelReady: (url) => console.error(`[fastagent] temporary tunnel ready → ${url}; registering webhook mode now…`),
178
+ onPatchRetry: ({ error, attempt, attempts, retryMs }) => console.error(`[fastagent] Lark could not validate the fresh tunnel yet (${String(error)}); retrying PATCH ${attempt + 1}/${attempts} in ${Math.round(retryMs / 1000)}s…`),
179
+ // A route-level 404 is definitive, not edge weather: fall back immediately. Retry only
180
+ // actual edge/network weather; scope/auth/config failures remain immediate.
181
+ shouldRetryPatch: (error) => !isFeishuConfigApiMissing(error) && isTransientFeishuRegistrationError(error),
182
+ });
183
+ console.error(`[fastagent] Lark Verification Token captured; Subscription mode changed to webhook in the app draft`);
184
+ return { token };
185
+ }
186
+ catch (error) {
187
+ if (!isFeishuConfigApiMissing(error))
188
+ throw error;
189
+ const manualReason = "This Lark app returned HTTP 404 for the application-config API, so automatic mode/token bootstrap is unavailable.";
190
+ console.error(`[fastagent] ${manualReason}`);
191
+ return { manualReason };
192
+ }
193
+ },
194
+ });
195
+ await configureGroupBehavior({
196
+ kind: "lark",
197
+ appId: credentials.LARK_APP_ID,
198
+ apiBase,
199
+ api: createFeishuApi({
200
+ kind: "lark",
201
+ baseUrl: apiBase,
202
+ appId: credentials.LARK_APP_ID,
203
+ appSecret: credentials.LARK_APP_SECRET,
204
+ }),
205
+ behavior: groupBehavior.behavior,
206
+ explicit: groupBehavior.explicit,
207
+ });
208
+ return Object.fromEntries(Object.entries(credentials).filter((entry) => typeof entry[1] === "string"));
209
+ }
210
+ /**
211
+ * The scan-to-create flow `add feishu` runs by default. The device-authorization grant
212
+ * creates a pre-configured agent app (bot capability, messaging scopes, event subscriptions) when the
213
+ * user confirms a link, then persists App ID/Secret at the irreversible boundary. That completes
214
+ * WebSocket credentials; webhook continues through challenge-captured Token persistence. The throwaway
215
+ * Request URL is later replaced by `dev --tunnel` / `deploy --run`.
216
+ *
217
+ * Feishu is the reference cloud and the only kind that runs this BOUND device flow. Lark is an explicit
218
+ * compatibility profile: its lagging control plane uses the unbound launcher + guided credentials,
219
+ * then probes the canonical token/mode bootstrap with a manual fallback.
220
+ */
221
+ async function createFeishuAppFlow(target, existing, ingress, groupBehavior) {
222
+ const env = dotEnvPath(target); // the file actually written — never the default spelling
223
+ const { apiBase } = cloudFor("feishu");
224
+ let appId = existing.FEISHU_APP_ID;
225
+ let appSecret = existing.FEISHU_APP_SECRET;
226
+ if (appId && appSecret) {
227
+ console.error(ingress === "webhook"
228
+ ? `[fastagent] resuming Feishu app ${appId} from ${env} to capture its missing Verification Token`
229
+ : `[fastagent] reusing Feishu app ${appId} from ${env} for WebSocket ingress`);
230
+ }
231
+ else {
232
+ console.error(`[fastagent] creating the Feishu app (confirm in the app)…`);
233
+ const app = await registerFeishuApp({
234
+ name: "{user}'s agent", // the platform expands {user} to the confirming user's name; editable on the page
235
+ desc: "Served by fastagent",
236
+ // The agent template alone is not enough to SERVE: v7 config PATCHes (webhook registration and
237
+ // context-aware group scope setup) demand application:application:patch, and the app must subscribe
238
+ // the receive event. Addons merge those BASE capabilities onto the confirm page; sensitive group
239
+ // permission approval and version publishing remain explicit console work.
240
+ addons: {
241
+ ...(ingress === "webhook" || (groupBehavior.behavior === "context" && groupBehavior.explicit)
242
+ ? { scopes: { tenant: ["application:application:patch"] } }
243
+ : {}),
244
+ events: { items: { tenant: ["im.message.receive_v1"] } },
245
+ },
246
+ onVerificationUrl: ({ url, expiresInS }) => {
247
+ console.error(`\n Opening the confirmation link in your browser (or open it in Feishu / render it as a QR code) — valid for ${Math.round(expiresInS / 60)} minutes:\n\n ${url}\n\n waiting for confirmation… (keep this running — the credentials are delivered here)`);
248
+ openExternalUrl(url); // best-effort, like `login` — the URL above is the fallback
249
+ },
250
+ });
251
+ console.error(`[fastagent] app created: ${app.appId}${app.tenantBrand ? ` (${app.tenantBrand} tenant)` : ""}`);
252
+ // A cross-brand confirmation should be impossible (each confirm page refuses the other brand's
253
+ // code) — but if the platform ever reports one, the credentials would land in the WRONG kind's env
254
+ // namespace and serve the wrong cloud. Fail visibly instead of writing them.
255
+ if (app.tenantBrand && app.tenantBrand !== "feishu") {
256
+ throw new Error(`the confirming account is a ${app.tenantBrand} tenant, but this is \`add feishu\` — run \`fastagent add ${app.tenantBrand}\` instead`);
257
+ }
258
+ appId = app.appId;
259
+ appSecret = app.appSecret;
260
+ // IRREVERSIBLE BOUNDARY: the remote app now exists and its one-time Secret is in memory. Persist
261
+ // both before any config read, temporary tunnel, or Token bootstrap can be interrupted. Partial old
262
+ // lines are overwritten because these newly-minted credentials are authoritative as one pair.
263
+ const staged = {
264
+ FEISHU_APP_ID: appId,
265
+ FEISHU_APP_SECRET: appSecret,
266
+ ...(ingress === "webhook" ? { FEISHU_VERIFICATION_TOKEN: "" } : {}),
267
+ };
268
+ await appendChannelDotEnv(target, "feishu", staged, Object.keys(staged), ingress);
269
+ console.error(ingress === "webhook"
270
+ ? `[fastagent] wrote FEISHU_APP_ID, FEISHU_APP_SECRET to ${env} before Token bootstrap`
271
+ : `[fastagent] wrote FEISHU_APP_ID, FEISHU_APP_SECRET to ${env}`);
272
+ }
273
+ const groupSetup = await configureGroupBehavior({
274
+ kind: "feishu",
275
+ appId,
276
+ apiBase,
277
+ api: createFeishuApi({ kind: "feishu", baseUrl: apiBase, appId, appSecret }),
278
+ behavior: groupBehavior.behavior,
279
+ explicit: groupBehavior.explicit,
280
+ });
281
+ if (ingress === "websocket") {
282
+ const versionUrl = `${apiBase}/app/${appId}/version`;
283
+ if (groupSetup.publishReady) {
284
+ console.error(`[fastagent] WebSocket ingress needs no Verification Token, Encrypt Key, Request URL, or tunnel. ` +
285
+ `Choose long connection in Events & Callbacks, then CREATE + PUBLISH a version. Opening ${versionUrl}`);
286
+ openExternalUrl(versionUrl);
287
+ }
288
+ else {
289
+ console.error(`[fastagent] WebSocket ingress needs no Verification Token, Encrypt Key, Request URL, or tunnel. ` +
290
+ `Choose long connection in Events & Callbacks, finish the permission work opened above, then CREATE + PUBLISH: ${versionUrl}`);
291
+ }
292
+ return;
293
+ }
294
+ // The webhook channel authenticates plaintext events by the platform-generated Verification Token.
295
+ // Try the cheap read first (the v6 detail MAY someday return `encryption`), then the real path: the
296
+ // token's only programmatic delivery is the url_verification challenge during registration — capture
297
+ // it over a throwaway tunnel (bootstrap-token.ts). Failing both is a one-line manual copy; the staged
298
+ // ID/Secret pair makes a re-run resume this App rather than mint another one.
299
+ const tokenVar = "FEISHU_VERIFICATION_TOKEN";
300
+ const api = createFeishuApi({ baseUrl: apiBase, appId, appSecret });
301
+ let token;
302
+ let webhookModeChanged = false;
303
+ try {
304
+ const cfg = await api.getAppConfig(appId);
305
+ token = cfg.verificationToken;
306
+ }
307
+ catch {
308
+ /* the read surface is best-effort — the bootstrap below is the real path */
309
+ }
310
+ if (!token) {
311
+ console.error(`[fastagent] capturing the Verification Token — a throwaway webhook registration delivers it (spinning up a temporary tunnel; can take a few minutes on a slow edge)…`);
312
+ try {
313
+ token = await bootstrapFeishuVerificationToken({
314
+ api,
315
+ appId,
316
+ startTunnel: (port) => startCloudflareTunnel(port),
317
+ });
318
+ webhookModeChanged = true;
319
+ console.error(`[fastagent] Verification Token captured`);
320
+ }
321
+ catch (e) {
322
+ // Transient tunnel weather is the usual cause. Do NOT suggest re-running `add feishu` as a new
323
+ // scan: the staged pair makes the re-run resume THIS app; manual copy completes it too.
324
+ console.error(`[fastagent] warn: could not capture the Verification Token: ${String(e)} — usually a transient tunnel issue; finish this app with the manual copy below`);
325
+ }
326
+ }
327
+ if (token) {
328
+ // Persist the second credential stage immediately too — opening the publish page and generic
329
+ // scaffold finalization happen only after the complete runtime credential set is durable.
330
+ const staged = await appendChannelDotEnv(target, "feishu", { [tokenVar]: token }, [tokenVar]);
331
+ console.error(`[fastagent] wrote ${staged.written.join(", ")} to ${env}`);
332
+ }
333
+ else {
334
+ console.error(`[fastagent] copy it manually: developer console → Events & Callbacks → Encryption Strategy → Verification Token → ${tokenVar} in ${env}`);
335
+ }
336
+ if (webhookModeChanged) {
337
+ // The bootstrap's PATCH flipped event mode in the DRAFT. It takes effect only after a version
338
+ // publish, which has no API; later dev/deploy runs change only the Request URL immediately.
339
+ const versionUrl = `${apiBase}/app/${appId}/version`;
340
+ if (groupSetup.publishReady) {
341
+ console.error(`[fastagent] one console click remains: CREATE + PUBLISH a version (self-approved) — the switch to webhook mode takes effect on publish. Opening ${versionUrl}`);
342
+ openExternalUrl(versionUrl);
343
+ }
344
+ else {
345
+ console.error(`[fastagent] after the permission work opened above, CREATE + PUBLISH a version — the switch to webhook mode takes effect on publish: ${versionUrl}`);
346
+ }
347
+ }
348
+ }
349
+ /** Active agent `.env` (`.secrets/.env`) values for the requested names — decided by THE .env
350
+ * parser, so this check can never disagree with what `loadEnvFile` reads. Empty/commented values are absent. */
351
+ async function activeDotEnvValues(dir, names) {
352
+ let content;
353
+ try {
354
+ content = await readFile(dotEnvPath(dir), "utf8");
355
+ }
356
+ catch (e) {
357
+ if (e.code === "ENOENT")
358
+ return {};
359
+ throw e;
360
+ }
361
+ const parsed = parseEnvContent(content);
362
+ return Object.fromEntries(names.flatMap((name) => {
363
+ const value = parsed.get(name)?.trim();
364
+ return value ? [[name, value]] : [];
365
+ }));
366
+ }
@@ -0,0 +1,11 @@
1
+ import { type GroupBehaviorChoice } from "../scaffold/add-channel.ts";
2
+ /** Interactive single-workspace internal-app creation + installation. Safe to re-run after interruption. */
3
+ export declare function onboardSlackInternalApp(input: {
4
+ /** The AGENT DIR — credentials land in its `.env` ({@link dotEnvPath}: `FASTAGENT_SECRETS_DIR` moves
5
+ * it, so messages print the resolved path rather than the default spelling). */
6
+ target: string;
7
+ stateRoot: string;
8
+ groupBehavior: GroupBehaviorChoice;
9
+ /** `--replace-config`: go straight to replacing the local App Configuration token pair. */
10
+ replaceConfig?: boolean;
11
+ }): Promise<void>;
@@ -0,0 +1,199 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
3
+ import { isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
4
+ import { waitForHealth } from "../channels/wait-health.js";
5
+ import { dotEnvPath, parseEnvContent } from "../env.js";
6
+ import { openExternalUrl } from "../open-url.js";
7
+ import { installProxyFetch } from "../proxy.js";
8
+ import { appendChannelDotEnv } from "../scaffold/add-channel.js";
9
+ import { newSlackOnboardingState, onboardSlackApp } from "../channels/slack/onboard.js";
10
+ import { readSlackOnboardingState, writeSlackOnboardingState } from "../channels/slack/onboarding-state.js";
11
+ import { startSlackSetupServer } from "../channels/slack/setup-server.js";
12
+ import { startCloudflareTunnel } from "../tunnel.js";
13
+ const CONFIG_TOKEN_URL = "https://api.slack.com/apps";
14
+ async function promptValue(message, hidden = false, initialValue) {
15
+ const result = hidden ? await password({ message }) : await clackText({ message, initialValue });
16
+ if (isCancel(result))
17
+ throw new Error("Slack onboarding cancelled");
18
+ const value = String(result).trim();
19
+ if (!value)
20
+ throw new Error(`${message}: value is required`);
21
+ return value;
22
+ }
23
+ /** Interactive single-workspace internal-app creation + installation. Safe to re-run after interruption. */
24
+ export async function onboardSlackInternalApp(input) {
25
+ installProxyFetch();
26
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
27
+ throw new Error("`add slack` needs an interactive terminal for internal-app creation and OAuth — " +
28
+ "re-run in a terminal, or pass --no-onboard to scaffold only");
29
+ }
30
+ let state = await readSlackOnboardingState(input.stateRoot);
31
+ const resumed = state !== undefined;
32
+ if (input.replaceConfig && !state) {
33
+ throw new Error("--replace-config found no local Slack onboarding state on this machine — nothing to replace. " +
34
+ "Run `fastagent add slack` to onboard, or update the Request URL manually in the Slack console");
35
+ }
36
+ if (state?.installedAt) {
37
+ const env = await readFile(dotEnvPath(input.target), "utf8")
38
+ .then(parseEnvContent)
39
+ .catch((error) => {
40
+ if (error.code === "ENOENT")
41
+ return new Map();
42
+ throw error;
43
+ });
44
+ const missingRuntime = [
45
+ "SLACK_BOT_TOKEN",
46
+ "SLACK_BOT_REFRESH_TOKEN",
47
+ "SLACK_BOT_TOKEN_EXPIRES_AT",
48
+ "SLACK_CLIENT_ID",
49
+ "SLACK_CLIENT_SECRET",
50
+ "SLACK_SIGNING_SECRET",
51
+ ].filter((name) => !((process.env[name] ?? env.get(name))?.trim() ?? ""));
52
+ if (missingRuntime.length > 0) {
53
+ throw new Error(`Slack app ${state.appId ?? "(unknown)"} is installed but ${dotEnvPath(input.target)} is missing ` +
54
+ `${missingRuntime.join(", ")} — ` +
55
+ "restore them from the Slack app console, or delete the app + onboarding state and create a new one");
56
+ }
57
+ if (input.groupBehavior.explicit && state.groupBehavior !== input.groupBehavior.behavior) {
58
+ throw new Error(`the onboarded Slack app uses group behavior ${state.groupBehavior}; changing an installed app's ` +
59
+ "OAuth scopes is a migration. Keep the existing choice, or remove the app + Slack onboarding state and create a new app");
60
+ }
61
+ let action = "replace-config";
62
+ if (!input.replaceConfig) {
63
+ const answer = await select({
64
+ message: `Slack app ${state.appId ?? "(unknown)"} is already installed${state.teamName ? ` in ${state.teamName}` : ""}`,
65
+ initialValue: "keep",
66
+ options: [
67
+ { value: "keep", label: "Keep the installed app" },
68
+ {
69
+ value: "replace-config",
70
+ label: "Replace App Configuration tokens",
71
+ hint: "repair automatic dev/deploy Request URL updates",
72
+ },
73
+ ],
74
+ });
75
+ if (isCancel(answer))
76
+ throw new Error("Slack onboarding cancelled");
77
+ action = answer;
78
+ }
79
+ if (action === "replace-config") {
80
+ console.error(`[fastagent] generate a fresh App Configuration Token pair at ${CONFIG_TOKEN_URL}`);
81
+ openExternalUrl(CONFIG_TOKEN_URL);
82
+ const configToken = await promptValue("Slack configuration access token (xoxe.xoxp-…)", true);
83
+ const configRefreshToken = await promptValue("Slack configuration refresh token (xoxe-…)", true);
84
+ if (!configToken.startsWith("xoxe.") || !configRefreshToken.startsWith("xoxe-")) {
85
+ throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
86
+ }
87
+ await writeSlackOnboardingState(input.stateRoot, {
88
+ ...state,
89
+ configToken,
90
+ configRefreshToken,
91
+ configTokenExpiresAt: Date.now() + 11 * 60 * 60_000,
92
+ });
93
+ console.error("[fastagent] replaced local Slack App Configuration tokens; runtime app credentials are unchanged");
94
+ }
95
+ else {
96
+ console.error("[fastagent] keeping the installed Slack app and local configuration tokens");
97
+ }
98
+ return;
99
+ }
100
+ if (!state) {
101
+ const appName = await promptValue("Slack app name", false, `FastAgent ${basename(input.target)}`);
102
+ clackLog.info("Slack's configuration refresh token can manage apps owned by your user in this workspace. " +
103
+ "FastAgent stores it only in owner-readable local state; it is never deployed.");
104
+ console.error(`[fastagent] generate an App Configuration Token at ${CONFIG_TOKEN_URL}`);
105
+ openExternalUrl(CONFIG_TOKEN_URL);
106
+ const configToken = await promptValue("Slack configuration access token (xoxe.xoxp-…)", true);
107
+ const configRefreshToken = await promptValue("Slack configuration refresh token (xoxe-…)", true);
108
+ if (!configToken.startsWith("xoxe."))
109
+ throw new Error("Slack configuration access token must start with xoxe.");
110
+ if (!configRefreshToken.startsWith("xoxe-")) {
111
+ throw new Error("Slack configuration refresh token must start with xoxe-");
112
+ }
113
+ state = newSlackOnboardingState({
114
+ appName,
115
+ groupBehavior: input.groupBehavior.behavior,
116
+ configToken,
117
+ configRefreshToken,
118
+ });
119
+ await writeSlackOnboardingState(input.stateRoot, state);
120
+ }
121
+ else if (input.groupBehavior.explicit && !state.appId) {
122
+ state = { ...state, groupBehavior: input.groupBehavior.behavior };
123
+ await writeSlackOnboardingState(input.stateRoot, state);
124
+ }
125
+ if (state.createAttemptedAt && !state.appId) {
126
+ throw new Error(`a prior Slack app creation attempt at ${state.createAttemptedAt} returned no app ID — ` +
127
+ `inspect ${CONFIG_TOKEN_URL}; delete any incomplete app and ${input.stateRoot}/channels/slack/onboarding.json before retrying`);
128
+ }
129
+ // `--replace-config` also covers the created-but-not-installed state, where a revoked token would
130
+ // otherwise strand the resume (rotation fails and no menu offers replacement).
131
+ if (resumed && (!state.appId || input.replaceConfig)) {
132
+ let action = "replace-config";
133
+ if (!input.replaceConfig) {
134
+ const answer = await select({
135
+ message: "Resume Slack onboarding with which App Configuration tokens?",
136
+ initialValue: "keep",
137
+ options: [
138
+ { value: "keep", label: "Use the saved token pair" },
139
+ { value: "replace-config", label: "Paste a fresh token pair" },
140
+ ],
141
+ });
142
+ if (isCancel(answer))
143
+ throw new Error("Slack onboarding cancelled");
144
+ action = answer;
145
+ }
146
+ if (action === "replace-config") {
147
+ openExternalUrl(CONFIG_TOKEN_URL);
148
+ state = {
149
+ ...state,
150
+ configToken: await promptValue("Slack configuration access token (xoxe.xoxp-…)", true),
151
+ configRefreshToken: await promptValue("Slack configuration refresh token (xoxe-…)", true),
152
+ configTokenExpiresAt: Date.now() + 11 * 60 * 60_000,
153
+ };
154
+ if (!state.configToken.startsWith("xoxe.") || !state.configRefreshToken.startsWith("xoxe-")) {
155
+ throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
156
+ }
157
+ await writeSlackOnboardingState(input.stateRoot, state);
158
+ }
159
+ }
160
+ const server = await startSlackSetupServer();
161
+ const tunnel = await startCloudflareTunnel(server.port);
162
+ if (!tunnel) {
163
+ await server.close();
164
+ throw new Error("Slack onboarding needs a temporary HTTPS tunnel — install cloudflared and re-run");
165
+ }
166
+ const requestUrl = `${tunnel.url}${server.requestPath}`;
167
+ const redirectUrl = `${tunnel.url}${server.redirectPath}`;
168
+ console.error(`[fastagent] temporary Slack setup tunnel ready → ${tunnel.url}`);
169
+ try {
170
+ if (!(await waitForHealth(`${tunnel.url}/health`, 45_000, 500))) {
171
+ throw new Error("the temporary Slack setup tunnel did not become reachable; no app was created");
172
+ }
173
+ await onboardSlackApp({ stateRoot: input.stateRoot, state, requestUrl, redirectUrl }, {
174
+ note: (message) => clackLog.info(message),
175
+ openUrl: openExternalUrl,
176
+ waitForOAuth: () => server.waitForOAuth(),
177
+ writeRuntimeSecrets: async ({ botToken, botRefreshToken, botTokenExpiresAt, clientId, clientSecret, signingSecret, }) => {
178
+ const values = {
179
+ ...(botToken ? { SLACK_BOT_TOKEN: botToken } : {}),
180
+ ...(botRefreshToken ? { SLACK_BOT_REFRESH_TOKEN: botRefreshToken } : {}),
181
+ ...(botTokenExpiresAt ? { SLACK_BOT_TOKEN_EXPIRES_AT: String(botTokenExpiresAt) } : {}),
182
+ ...(clientId ? { SLACK_CLIENT_ID: clientId } : {}),
183
+ ...(clientSecret ? { SLACK_CLIENT_SECRET: clientSecret } : {}),
184
+ ...(signingSecret ? { SLACK_SIGNING_SECRET: signingSecret } : {}),
185
+ };
186
+ if (Object.keys(values).length > 0) {
187
+ await appendChannelDotEnv(input.target, "slack", values, Object.keys(values));
188
+ }
189
+ },
190
+ });
191
+ console.error(`[fastagent] Slack app installed; rotating bot credentials and Signing Secret written to ${dotEnvPath(input.target)}`);
192
+ console.error(`[fastagent] run \`fastagent dev --tunnel\` next — FastAgent will rotate the config token and ` +
193
+ "replace the temporary Events API URL automatically");
194
+ }
195
+ finally {
196
+ tunnel.close();
197
+ await server.close().catch(() => { });
198
+ }
199
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI presenter for the auth-status line `reportAuth` prints (invoke/dev/start). Kept out of cli.ts —
3
3
  * which self-executes on import — so the expired-vs-missing DECISION is unit-testable without a real
4
- * credential round-trip, mirroring cli-models.ts.
4
+ * credential round-trip, mirroring models-view.ts.
5
5
  */
6
6
  /** A stored credential as `reportAuth` needs it: just its kind, for the status line. */
7
7
  export interface StoredCredentialInfo {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI presenter for the auth-status line `reportAuth` prints (invoke/dev/start). Kept out of cli.ts —
3
3
  * which self-executes on import — so the expired-vs-missing DECISION is unit-testable without a real
4
- * credential round-trip, mirroring cli-models.ts.
4
+ * credential round-trip, mirroring models-view.ts.
5
5
  */
6
6
  /**
7
7
  * Format the auth status for `spec`'s provider. `source` is {@link probeAuthSource}'s label (an env-var
@@ -2,6 +2,10 @@ import { type ChannelKind } from "../../scaffold/add-channel.ts";
2
2
  /** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
3
3
  export declare function runAddChannel(channelKind: ChannelKind, dirArg: string, opts: {
4
4
  createApp?: boolean;
5
+ ingress?: string;
6
+ groupBehavior?: string;
7
+ onboard?: boolean;
8
+ replaceConfig?: boolean;
5
9
  }): Promise<void>;
6
10
  /** `fastagent add skill <source> [dir]`: vendor an Agent Skills skill into <dir>/skills/<name>/. */
7
11
  export declare function runAddSkill(source: string | undefined, dirArg: string, opts: {