@fastagent-sh/fastagent 0.14.0 → 0.15.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 (209) hide show
  1. package/README.md +3 -3
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/context-buffer.d.ts +30 -0
  5. package/dist/channels/context-buffer.js +98 -0
  6. package/dist/channels/control.d.ts +28 -0
  7. package/dist/channels/control.js +214 -0
  8. package/dist/channels/feishu/card.js +1 -1
  9. package/dist/channels/feishu/context-buffer.d.ts +8 -11
  10. package/dist/channels/feishu/context-buffer.js +6 -67
  11. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  12. package/dist/channels/feishu/feishu-api.js +20 -2
  13. package/dist/channels/feishu/feishu.d.ts +28 -16
  14. package/dist/channels/feishu/feishu.js +210 -188
  15. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  16. package/dist/channels/feishu/invoke-turn.js +10 -62
  17. package/dist/channels/feishu/model.d.ts +2 -1
  18. package/dist/channels/feishu/normalize.d.ts +2 -1
  19. package/dist/channels/feishu/owned-threads.d.ts +2 -6
  20. package/dist/channels/feishu/parse.d.ts +1 -1
  21. package/dist/channels/feishu/preview.d.ts +4 -7
  22. package/dist/channels/feishu/preview.js +24 -140
  23. package/dist/channels/feishu/register-app.d.ts +2 -1
  24. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  25. package/dist/channels/feishu/setup-mode.d.ts +8 -0
  26. package/dist/channels/feishu/setup-mode.js +2 -0
  27. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  28. package/dist/channels/feishu/ws-ingress.js +136 -0
  29. package/dist/channels/github/github.js +8 -6
  30. package/dist/channels/http.d.ts +14 -0
  31. package/dist/channels/http.js +35 -2
  32. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  33. package/dist/channels/invoke-turn-kit.js +87 -0
  34. package/dist/channels/lark/lark.d.ts +4 -2
  35. package/dist/channels/lark/lark.js +4 -1
  36. package/dist/channels/lark/onboard.d.ts +8 -4
  37. package/dist/channels/lark/onboard.js +8 -0
  38. package/dist/channels/preview-kit.d.ts +109 -0
  39. package/dist/channels/preview-kit.js +183 -0
  40. package/dist/channels/seen.d.ts +5 -0
  41. package/dist/channels/seen.js +35 -0
  42. package/dist/channels/slack/bot-auth.d.ts +15 -0
  43. package/dist/channels/slack/bot-auth.js +146 -0
  44. package/dist/channels/slack/config-api.d.ts +60 -0
  45. package/dist/channels/slack/config-api.js +149 -0
  46. package/dist/channels/slack/context-buffer.d.ts +24 -0
  47. package/dist/channels/slack/context-buffer.js +37 -0
  48. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  49. package/dist/channels/slack/invoke-turn.js +63 -0
  50. package/dist/channels/slack/manifest.d.ts +49 -0
  51. package/dist/channels/slack/manifest.js +69 -0
  52. package/dist/channels/slack/model.d.ts +67 -0
  53. package/dist/channels/slack/model.js +2 -0
  54. package/dist/channels/slack/onboard.d.ts +41 -0
  55. package/dist/channels/slack/onboard.js +120 -0
  56. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  57. package/dist/channels/slack/onboarding-state.js +69 -0
  58. package/dist/channels/slack/owned-threads.d.ts +6 -0
  59. package/dist/channels/slack/owned-threads.js +43 -0
  60. package/dist/channels/slack/parse.d.ts +23 -0
  61. package/dist/channels/slack/parse.js +81 -0
  62. package/dist/channels/slack/preview.d.ts +24 -0
  63. package/dist/channels/slack/preview.js +359 -0
  64. package/dist/channels/slack/reaction.d.ts +24 -0
  65. package/dist/channels/slack/reaction.js +62 -0
  66. package/dist/channels/slack/register-webhook.d.ts +10 -0
  67. package/dist/channels/slack/register-webhook.js +49 -0
  68. package/dist/channels/slack/scaffold/channel.ts +33 -0
  69. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  70. package/dist/channels/slack/setup-server.d.ts +17 -0
  71. package/dist/channels/slack/setup-server.js +103 -0
  72. package/dist/channels/slack/slack-api.d.ts +77 -0
  73. package/dist/channels/slack/slack-api.js +415 -0
  74. package/dist/channels/slack/slack.d.ts +58 -0
  75. package/dist/channels/slack/slack.js +451 -0
  76. package/dist/channels/slack/welcomed.d.ts +5 -0
  77. package/dist/channels/slack/welcomed.js +32 -0
  78. package/dist/channels/state.js +3 -3
  79. package/dist/channels/stop-command.d.ts +6 -0
  80. package/dist/channels/stop-command.js +36 -0
  81. package/dist/channels/tasks.d.ts +13 -0
  82. package/dist/channels/tasks.js +10 -0
  83. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  84. package/dist/channels/telegram/context-buffer.js +6 -85
  85. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  86. package/dist/channels/telegram/invoke-turn.js +11 -58
  87. package/dist/channels/telegram/preview.d.ts +4 -7
  88. package/dist/channels/telegram/preview.js +24 -142
  89. package/dist/channels/telegram/telegram.js +23 -9
  90. package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
  91. package/dist/channels/{feishu/text.js → text.js} +1 -1
  92. package/dist/channels/turn-queue.js +1 -1
  93. package/dist/channels/turn-store.d.ts +1 -1
  94. package/dist/channels/turn-store.js +2 -3
  95. package/dist/cli/add-feishu.d.ts +27 -0
  96. package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
  97. package/dist/cli/add-slack.d.ts +10 -0
  98. package/dist/cli/add-slack.js +204 -0
  99. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  100. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  101. package/dist/cli/commands/add.d.ts +4 -0
  102. package/dist/cli/commands/add.js +152 -20
  103. package/dist/cli/commands/attach.d.ts +82 -0
  104. package/dist/cli/commands/attach.js +559 -0
  105. package/dist/cli/commands/chat.d.ts +1 -0
  106. package/dist/cli/commands/chat.js +6 -1
  107. package/dist/cli/commands/deploy.d.ts +2 -0
  108. package/dist/cli/commands/deploy.js +78 -22
  109. package/dist/cli/commands/dev.js +11 -5
  110. package/dist/cli/commands/fire.js +3 -3
  111. package/dist/cli/commands/info.js +2 -2
  112. package/dist/cli/commands/invoke.js +2 -3
  113. package/dist/cli/commands/login.js +14 -25
  114. package/dist/cli/commands/models.js +1 -1
  115. package/dist/cli/commands/start.js +15 -8
  116. package/dist/cli/commands/tool.js +2 -1
  117. package/dist/cli/fail.d.ts +0 -2
  118. package/dist/cli/fail.js +1 -1
  119. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  120. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  121. package/dist/cli/kernel.d.ts +3 -2
  122. package/dist/cli/models-view.d.ts +21 -0
  123. package/dist/cli/models-view.js +66 -0
  124. package/dist/cli/program.js +79 -21
  125. package/dist/cli/serve.d.ts +38 -18
  126. package/dist/cli/serve.js +197 -39
  127. package/dist/cli/shared.d.ts +26 -6
  128. package/dist/cli/shared.js +163 -33
  129. package/dist/collect.d.ts +14 -3
  130. package/dist/collect.js +24 -0
  131. package/dist/core.d.ts +3 -1
  132. package/dist/core.js +2 -0
  133. package/dist/deploy/docker/plan.d.ts +2 -2
  134. package/dist/deploy/docker/plan.js +8 -6
  135. package/dist/deploy/fly/plan.d.ts +3 -1
  136. package/dist/deploy/fly/plan.js +12 -7
  137. package/dist/deploy/fly/run.d.ts +5 -4
  138. package/dist/deploy/fly/run.js +16 -5
  139. package/dist/deploy/preflight.d.ts +10 -4
  140. package/dist/deploy/preflight.js +53 -9
  141. package/dist/deploy/railway/plan.d.ts +3 -1
  142. package/dist/deploy/railway/plan.js +12 -4
  143. package/dist/deploy/railway/run.d.ts +5 -4
  144. package/dist/deploy/railway/run.js +15 -5
  145. package/dist/deploy/runner.d.ts +2 -1
  146. package/dist/deploy/secrets.d.ts +2 -1
  147. package/dist/deploy/secrets.js +23 -3
  148. package/dist/dev-supervisor.d.ts +0 -2
  149. package/dist/dev-supervisor.js +7 -3
  150. package/dist/engines/pi/channel.d.ts +22 -16
  151. package/dist/engines/pi/channel.js +90 -60
  152. package/dist/engines/pi/chat.d.ts +4 -16
  153. package/dist/engines/pi/chat.js +8 -261
  154. package/dist/engines/pi/config.d.ts +14 -9
  155. package/dist/engines/pi/config.js +33 -18
  156. package/dist/engines/pi/create.d.ts +22 -2
  157. package/dist/engines/pi/create.js +20 -14
  158. package/dist/engines/pi/harness.d.ts +49 -0
  159. package/dist/engines/pi/harness.js +90 -2
  160. package/dist/engines/pi/invoke.d.ts +55 -2
  161. package/dist/engines/pi/invoke.js +304 -19
  162. package/dist/engines/pi/login.d.ts +11 -0
  163. package/dist/engines/pi/login.js +16 -4
  164. package/dist/engines/pi/models.d.ts +56 -10
  165. package/dist/engines/pi/models.js +61 -23
  166. package/dist/engines/pi/session-builder.d.ts +16 -0
  167. package/dist/engines/pi/session-builder.js +308 -0
  168. package/dist/engines/pi/session-control.d.ts +50 -0
  169. package/dist/engines/pi/session-control.js +604 -0
  170. package/dist/engines/pi/sessions.d.ts +17 -2
  171. package/dist/engines/pi/sessions.js +9 -0
  172. package/dist/engines/pi/tool-context.d.ts +18 -11
  173. package/dist/engines/pi/tool-context.js +3 -9
  174. package/dist/engines/pi/tool.d.ts +6 -8
  175. package/dist/engines/pi/tool.js +6 -1
  176. package/dist/engines/pi/wake-tool.d.ts +0 -3
  177. package/dist/engines/pi/wake-tool.js +9 -7
  178. package/dist/engines/pi/workspace.d.ts +52 -0
  179. package/dist/engines/pi/workspace.js +72 -15
  180. package/dist/feishu.d.ts +1 -1
  181. package/dist/feishu.js +1 -1
  182. package/dist/host/node.d.ts +23 -6
  183. package/dist/host/node.js +5 -4
  184. package/dist/index.d.ts +1 -0
  185. package/dist/index.js +1 -0
  186. package/dist/lark.d.ts +1 -1
  187. package/dist/lark.js +1 -1
  188. package/dist/observe.js +3 -0
  189. package/dist/pi.d.ts +5 -4
  190. package/dist/pi.js +2 -1
  191. package/dist/scaffold/add-channel.d.ts +19 -6
  192. package/dist/scaffold/add-channel.js +126 -17
  193. package/dist/scaffold/templates/fastagent.config.mjs +4 -3
  194. package/dist/schedule/wakeups.d.ts +0 -3
  195. package/dist/schedule/wakeups.js +1 -1
  196. package/dist/session-remote.d.ts +53 -0
  197. package/dist/session-remote.js +336 -0
  198. package/dist/session.d.ts +265 -0
  199. package/dist/session.js +37 -0
  200. package/dist/slack.d.ts +2 -0
  201. package/dist/slack.js +2 -0
  202. package/dist/tunnel.d.ts +7 -4
  203. package/dist/tunnel.js +21 -10
  204. package/package.json +19 -7
  205. package/dist/channels/feishu/seen.d.ts +0 -5
  206. package/dist/channels/feishu/seen.js +0 -47
  207. package/dist/cli-add-feishu.d.ts +0 -8
  208. package/dist/cli-models.d.ts +0 -11
  209. package/dist/cli-models.js +0 -20
@@ -1,27 +1,88 @@
1
1
  /**
2
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 cli-models.ts/cli-auth.ts. The POLICY already lives in
3
+ * (which self-executes on import), mirroring models-view.ts/auth-view.ts. The POLICY already lives in
4
4
  * testable modules (register-app.ts, bootstrap-token.ts, lark/onboard.ts); this layer is the terminal
5
5
  * wiring: clack prompts, .env staging, browser opens, progress lines.
6
6
  *
7
- * Feishu (scan-to-create): the device flow creates the app; App ID/Secret are persisted at the
8
- * irreversible creation boundary, then the Verification Token is captured over a throwaway tunnel and
9
- * persisted as a second stage a re-run RESUMES the app instead of minting another. Lark
10
- * (guided-console): open the unbound launcher, validate entered credentials, probe the same
11
- * webhook/token bootstrap, and fall back to a manual token prompt on the definitive config-route 404.
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.
12
11
  */
13
12
  import { readFile } from "node:fs/promises";
14
13
  import { join } from "node:path";
15
14
  import { isCancel, log as clackLog, password, text as clackText } from "@clack/prompts";
16
- import { bootstrapFeishuVerificationToken } from "./channels/feishu/bootstrap-token.js";
17
- import { cloudFor } from "./channels/feishu/cloud.js";
18
- import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "./channels/feishu/feishu-api.js";
19
- import { registerFeishuApp } from "./channels/feishu/register-app.js";
20
- import { onboardLarkApp } from "./channels/lark/onboard.js";
21
- import { parseEnvContent } from "./env.js";
22
- import { openExternalUrl } from "./open-url.js";
23
- import { appendChannelDotEnv } from "./scaffold/add-channel.js";
24
- import { startCloudflareTunnel } from "./tunnel.js";
15
+ import { bootstrapFeishuVerificationToken } from "../channels/feishu/bootstrap-token.js";
16
+ import { FEISHU_GROUP_CONTEXT_SCOPE, } from "../channels/feishu/setup-mode.js";
17
+ import { cloudFor } from "../channels/feishu/cloud.js";
18
+ import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "../channels/feishu/feishu-api.js";
19
+ import { registerFeishuApp } from "../channels/feishu/register-app.js";
20
+ import { onboardLarkApp } from "../channels/lark/onboard.js";
21
+ import { parseEnvContent } from "../env.js";
22
+ import { openExternalUrl } from "../open-url.js";
23
+ import { appendChannelDotEnv } from "../scaffold/add-channel.js";
24
+ import { startCloudflareTunnel } from "../tunnel.js";
25
+ export async function configureGroupBehavior(input) {
26
+ const { kind, appId, apiBase, api, behavior, explicit } = input;
27
+ const note = input.note ?? ((message) => console.error(message));
28
+ const openUrl = input.openUrl ?? openExternalUrl;
29
+ let scopes;
30
+ let inspected = true;
31
+ try {
32
+ scopes = await api.listAppScopes();
33
+ }
34
+ catch (error) {
35
+ note(`[fastagent] warn: could not inspect ${kind} group-message permission: ${String(error)} — check Permissions & Scopes manually`);
36
+ inspected = false;
37
+ scopes = [];
38
+ }
39
+ const groupScope = scopes.find((scope) => scope.name === FEISHU_GROUP_CONTEXT_SCOPE && (scope.type === undefined || scope.type === "tenant"));
40
+ if (behavior === "mentions") {
41
+ if (!inspected) {
42
+ const permissionUrl = `${apiBase}/app/${encodeURIComponent(appId)}/permission`;
43
+ note(`[fastagent] mention-only scope state could not be verified — check Permissions before publishing. Opening ${permissionUrl}`);
44
+ openUrl(permissionUrl);
45
+ return { publishReady: false };
46
+ }
47
+ if (groupScope?.grantStatus === 1) {
48
+ const permissionUrl = `${apiBase}/app/${encodeURIComponent(appId)}/permission`;
49
+ 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}`);
50
+ openUrl(permissionUrl);
51
+ return { publishReady: false };
52
+ }
53
+ note(`[fastagent] group behavior: mention-only — bare managed-thread replies and group context buffering are disabled`);
54
+ return { publishReady: true };
55
+ }
56
+ note(`[fastagent] group behavior: context-aware (recommended) — ${kind} will deliver all group messages; ` +
57
+ `FastAgent invokes @Agent + bare managed-thread replies and durably buffers other discussion`);
58
+ if (groupScope?.grantStatus === 1) {
59
+ note(`[fastagent] ${FEISHU_GROUP_CONTEXT_SCOPE} is already granted`);
60
+ return { publishReady: true };
61
+ }
62
+ const permissionUrl = `${apiBase}/app/${encodeURIComponent(appId)}/permission`;
63
+ if (groupScope) {
64
+ note(`[fastagent] ${FEISHU_GROUP_CONTEXT_SCOPE} is awaiting approval — complete tenant-admin approval before publishing. Opening ${permissionUrl}`);
65
+ openUrl(permissionUrl);
66
+ return { publishReady: false };
67
+ }
68
+ if (!explicit) {
69
+ // Defaulted, not chosen: report the gap and how to opt in, but leave the app's requested
70
+ // permission set untouched (a scripted re-run must not silently escalate a mention-only app).
71
+ note(`[fastagent] ${FEISHU_GROUP_CONTEXT_SCOPE} is not granted (or could not be verified) — group behavior was ` +
72
+ `defaulted, so it was not requested. Re-run with --group-behavior context to add it to the app draft, ` +
73
+ `or --group-behavior mentions to stay least-privilege: ${permissionUrl}`);
74
+ return { publishReady: false };
75
+ }
76
+ try {
77
+ await api.addAppScopes(appId, [FEISHU_GROUP_CONTEXT_SCOPE]);
78
+ note(`[fastagent] added ${FEISHU_GROUP_CONTEXT_SCOPE} to the app draft — complete tenant-admin approval before publishing. Opening ${permissionUrl}`);
79
+ }
80
+ catch (error) {
81
+ note(`[fastagent] warn: could not add ${FEISHU_GROUP_CONTEXT_SCOPE} automatically: ${String(error)} — add it manually before publishing. Opening ${permissionUrl}`);
82
+ }
83
+ openUrl(permissionUrl);
84
+ return { publishReady: false };
85
+ }
25
86
  /**
26
87
  * Create or resume the platform app behind `add feishu` / `add lark`. Returns credentials for the
27
88
  * caller's generic .env write (the guided Lark path), or undefined when nothing remains to write —
@@ -29,7 +90,7 @@ import { startCloudflareTunnel } from "./tunnel.js";
29
90
  * irreversible and must not wait for the caller). Throws on refusal (a committable .env, a
30
91
  * non-interactive lark run); the caller surfaces that as a startup failure.
31
92
  */
32
- export async function onboardFeishuCloudApp(target, kind, envIgnored) {
93
+ export async function onboardFeishuCloudApp(target, kind, envIgnored, ingress = "webhook", groupBehavior = { behavior: "context", explicit: false }) {
33
94
  const { envPrefix, apiBase, capabilities } = cloudFor(kind);
34
95
  // The CLI must never materialize a real credential into a committable file — refuse, don't warn.
35
96
  if (!envIgnored) {
@@ -37,24 +98,40 @@ export async function onboardFeishuCloudApp(target, kind, envIgnored) {
37
98
  ? "`add feishu` creates an app and writes real credentials to .env — add .env to .gitignore/.fastagentignore first, then re-run"
38
99
  : "`add lark` writes real app credentials to .env — add .env to .gitignore/.fastagentignore first, then re-run");
39
100
  }
40
- const existing = await activeDotEnvValues(target, [
101
+ const requiredNames = [
41
102
  `${envPrefix}_APP_ID`,
42
103
  `${envPrefix}_APP_SECRET`,
43
- `${envPrefix}_VERIFICATION_TOKEN`,
44
- ]);
45
- if (Object.keys(existing).length === 3) {
46
- console.error(`[fastagent] ${envPrefix}_APP_ID/SECRET/VERIFICATION_TOKEN already set in .env — keeping them`);
47
- return undefined;
104
+ ...(ingress === "webhook" ? [`${envPrefix}_VERIFICATION_TOKEN`] : []),
105
+ ];
106
+ const existing = await activeDotEnvValues(target, requiredNames);
107
+ if (Object.keys(existing).length === requiredNames.length) {
108
+ console.error(`[fastagent] ${requiredNames.join("/")} already set in .env — keeping them`);
109
+ // WebSocket still needs its console mode/publish guidance. A complete webhook can skip the rest of
110
+ // onboarding, but group visibility must still be inspected/configured on every explicit re-run.
111
+ if (ingress === "webhook") {
112
+ const appId = existing[`${envPrefix}_APP_ID`];
113
+ const appSecret = existing[`${envPrefix}_APP_SECRET`];
114
+ await configureGroupBehavior({
115
+ kind,
116
+ appId,
117
+ apiBase,
118
+ api: createFeishuApi({ kind, baseUrl: apiBase, appId, appSecret }),
119
+ behavior: groupBehavior.behavior,
120
+ explicit: groupBehavior.explicit,
121
+ });
122
+ return undefined;
123
+ }
48
124
  }
49
125
  if (capabilities.appCreation === "scan-to-create") {
50
- await createFeishuAppFlow(target, existing);
126
+ await createFeishuAppFlow(target, existing, ingress, groupBehavior);
51
127
  return undefined;
52
128
  }
53
129
  // guided-console (lark): the intl cloud cannot complete the bound device flow — collect + validate.
54
- if (!(process.stdin.isTTY && process.stdout.isTTY)) {
130
+ if (!(process.stdin.isTTY && process.stdout.isTTY) &&
131
+ !(existing[`${envPrefix}_APP_ID`] && existing[`${envPrefix}_APP_SECRET`])) {
55
132
  throw new Error("`add lark` needs an interactive terminal to onboard the Lark app credentials — re-run it in a terminal");
56
133
  }
57
- return onboardLarkApp({
134
+ const credentials = await onboardLarkApp({
58
135
  openUrl: openExternalUrl,
59
136
  note: (message) => clackLog.info(message),
60
137
  async prompt(message, opts) {
@@ -63,6 +140,8 @@ export async function onboardFeishuCloudApp(target, kind, envIgnored) {
63
140
  },
64
141
  }, {
65
142
  existing,
143
+ ingress,
144
+ groupBehavior: groupBehavior.behavior,
66
145
  verifyCredentials: async (appId, appSecret) => {
67
146
  await createFeishuApi({ kind: "lark", baseUrl: apiBase, appId, appSecret }).verifyCredentials();
68
147
  console.error(`[fastagent] Lark App ID / Secret verified`);
@@ -94,37 +173,54 @@ export async function onboardFeishuCloudApp(target, kind, envIgnored) {
94
173
  }
95
174
  },
96
175
  });
176
+ await configureGroupBehavior({
177
+ kind: "lark",
178
+ appId: credentials.LARK_APP_ID,
179
+ apiBase,
180
+ api: createFeishuApi({
181
+ kind: "lark",
182
+ baseUrl: apiBase,
183
+ appId: credentials.LARK_APP_ID,
184
+ appSecret: credentials.LARK_APP_SECRET,
185
+ }),
186
+ behavior: groupBehavior.behavior,
187
+ explicit: groupBehavior.explicit,
188
+ });
189
+ return Object.fromEntries(Object.entries(credentials).filter((entry) => typeof entry[1] === "string"));
97
190
  }
98
191
  /**
99
192
  * The scan-to-create flow `add feishu` runs by default. The device-authorization grant
100
193
  * creates a pre-configured agent app (bot capability, messaging scopes, event subscriptions) when the
101
- * user confirms a link in the app, and hands back the credentials; App ID/Secret are persisted at that
102
- * irreversible boundary before the platform-generated Verification Token is captured from the
103
- * registration challenge (bootstrap-token.ts). The Token is persisted as a second stage, so .env is
104
- * complete before the one remaining version-publish action. The event Request URL is NOT left pointing at the throwaway
105
- * tunnel for long: `dev --tunnel` / `deploy --run` re-register it against the live URL.
194
+ * user confirms a link, then persists App ID/Secret at the irreversible boundary. That completes
195
+ * WebSocket credentials; webhook continues through challenge-captured Token persistence. The throwaway
196
+ * Request URL is later replaced by `dev --tunnel` / `deploy --run`.
106
197
  *
107
198
  * Feishu is the reference cloud and the only kind that runs this BOUND device flow. Lark is an explicit
108
199
  * compatibility profile: its lagging control plane uses the unbound launcher + guided credentials,
109
200
  * then probes the canonical token/mode bootstrap with a manual fallback.
110
201
  */
111
- async function createFeishuAppFlow(target, existing) {
202
+ async function createFeishuAppFlow(target, existing, ingress, groupBehavior) {
112
203
  const { apiBase } = cloudFor("feishu");
113
204
  let appId = existing.FEISHU_APP_ID;
114
205
  let appSecret = existing.FEISHU_APP_SECRET;
115
206
  if (appId && appSecret) {
116
- console.error(`[fastagent] resuming Feishu app ${appId} from .env to capture its missing Verification Token`);
207
+ console.error(ingress === "webhook"
208
+ ? `[fastagent] resuming Feishu app ${appId} from .env to capture its missing Verification Token`
209
+ : `[fastagent] reusing Feishu app ${appId} from .env for WebSocket ingress`);
117
210
  }
118
211
  else {
119
212
  console.error(`[fastagent] creating the Feishu app (confirm in the app)…`);
120
213
  const app = await registerFeishuApp({
121
214
  name: "{user}'s agent", // the platform expands {user} to the confirming user's name; editable on the page
122
215
  desc: "Served by fastagent",
123
- // The agent template alone is not enough to SERVE: the v7 config PATCH (webhook auto-registration
124
- // in `dev --tunnel` / `deploy --run`) demands application:application:patch, and the app must
125
- // subscribe the receive event. Addons merge both onto the confirm page no manual app setup.
216
+ // The agent template alone is not enough to SERVE: v7 config PATCHes (webhook registration and
217
+ // context-aware group scope setup) demand application:application:patch, and the app must subscribe
218
+ // the receive event. Addons merge those BASE capabilities onto the confirm page; sensitive group
219
+ // permission approval and version publishing remain explicit console work.
126
220
  addons: {
127
- scopes: { tenant: ["application:application:patch"] },
221
+ ...(ingress === "webhook" || (groupBehavior.behavior === "context" && groupBehavior.explicit)
222
+ ? { scopes: { tenant: ["application:application:patch"] } }
223
+ : {}),
128
224
  events: { items: { tenant: ["im.message.receive_v1"] } },
129
225
  },
130
226
  onVerificationUrl: ({ url, expiresInS }) => {
@@ -144,14 +240,36 @@ async function createFeishuAppFlow(target, existing) {
144
240
  // IRREVERSIBLE BOUNDARY: the remote app now exists and its one-time Secret is in memory. Persist
145
241
  // both before any config read, temporary tunnel, or Token bootstrap can be interrupted. Partial old
146
242
  // lines are overwritten because these newly-minted credentials are authoritative as one pair.
147
- await appendChannelDotEnv(target, "feishu", {
243
+ const staged = {
148
244
  FEISHU_APP_ID: appId,
149
245
  FEISHU_APP_SECRET: appSecret,
150
- // A Token from a partial OLD credential set belongs to another App. Clear it at the same
151
- // boundary; successful bootstrap below replaces the empty line with this App's Token.
152
- FEISHU_VERIFICATION_TOKEN: "",
153
- }, ["FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_VERIFICATION_TOKEN"]);
154
- console.error(`[fastagent] wrote FEISHU_APP_ID, FEISHU_APP_SECRET to .env before Token bootstrap`);
246
+ ...(ingress === "webhook" ? { FEISHU_VERIFICATION_TOKEN: "" } : {}),
247
+ };
248
+ await appendChannelDotEnv(target, "feishu", staged, Object.keys(staged), ingress);
249
+ console.error(ingress === "webhook"
250
+ ? `[fastagent] wrote FEISHU_APP_ID, FEISHU_APP_SECRET to .env before Token bootstrap`
251
+ : `[fastagent] wrote FEISHU_APP_ID, FEISHU_APP_SECRET to .env`);
252
+ }
253
+ const groupSetup = await configureGroupBehavior({
254
+ kind: "feishu",
255
+ appId,
256
+ apiBase,
257
+ api: createFeishuApi({ kind: "feishu", baseUrl: apiBase, appId, appSecret }),
258
+ behavior: groupBehavior.behavior,
259
+ explicit: groupBehavior.explicit,
260
+ });
261
+ if (ingress === "websocket") {
262
+ const versionUrl = `${apiBase}/app/${appId}/version`;
263
+ if (groupSetup.publishReady) {
264
+ console.error(`[fastagent] WebSocket ingress needs no Verification Token, Encrypt Key, Request URL, or tunnel. ` +
265
+ `Choose long connection in Events & Callbacks, then CREATE + PUBLISH a version. Opening ${versionUrl}`);
266
+ openExternalUrl(versionUrl);
267
+ }
268
+ else {
269
+ console.error(`[fastagent] WebSocket ingress needs no Verification Token, Encrypt Key, Request URL, or tunnel. ` +
270
+ `Choose long connection in Events & Callbacks, finish the permission work opened above, then CREATE + PUBLISH: ${versionUrl}`);
271
+ }
272
+ return;
155
273
  }
156
274
  // The webhook channel authenticates plaintext events by the platform-generated Verification Token.
157
275
  // Try the cheap read first (the v6 detail MAY someday return `encryption`), then the real path: the
@@ -199,8 +317,13 @@ async function createFeishuAppFlow(target, existing) {
199
317
  // The bootstrap's PATCH flipped event mode in the DRAFT. It takes effect only after a version
200
318
  // publish, which has no API; later dev/deploy runs change only the Request URL immediately.
201
319
  const versionUrl = `${apiBase}/app/${appId}/version`;
202
- console.error(`[fastagent] one console click remains: CREATE + PUBLISH a version (self-approved) — the switch to webhook mode takes effect on publish. Opening ${versionUrl}`);
203
- openExternalUrl(versionUrl);
320
+ if (groupSetup.publishReady) {
321
+ console.error(`[fastagent] one console click remains: CREATE + PUBLISH a version (self-approved) — the switch to webhook mode takes effect on publish. Opening ${versionUrl}`);
322
+ openExternalUrl(versionUrl);
323
+ }
324
+ else {
325
+ console.error(`[fastagent] after the permission work opened above, CREATE + PUBLISH a version — the switch to webhook mode takes effect on publish: ${versionUrl}`);
326
+ }
204
327
  }
205
328
  }
206
329
  /** Active run-root `.env` values for the requested names — decided by THE .env parser, so this
@@ -0,0 +1,10 @@
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
+ target: string;
5
+ stateRoot: string;
6
+ envIgnored: boolean;
7
+ groupBehavior: GroupBehaviorChoice;
8
+ /** `--replace-config`: go straight to replacing the local App Configuration token pair. */
9
+ replaceConfig?: boolean;
10
+ }): Promise<void>;
@@ -0,0 +1,204 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, join } 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 { ensureStateRootSelfIgnored } from "../engines/pi/definition.js";
6
+ import { parseEnvContent } from "../env.js";
7
+ import { openExternalUrl } from "../open-url.js";
8
+ import { installProxyFetch } from "../proxy.js";
9
+ import { appendChannelDotEnv } from "../scaffold/add-channel.js";
10
+ import { newSlackOnboardingState, onboardSlackApp } from "../channels/slack/onboard.js";
11
+ import { readSlackOnboardingState, writeSlackOnboardingState } from "../channels/slack/onboarding-state.js";
12
+ import { startSlackSetupServer } from "../channels/slack/setup-server.js";
13
+ import { startCloudflareTunnel } from "../tunnel.js";
14
+ const CONFIG_TOKEN_URL = "https://api.slack.com/apps";
15
+ async function promptValue(message, hidden = false, initialValue) {
16
+ const result = hidden ? await password({ message }) : await clackText({ message, initialValue });
17
+ if (isCancel(result))
18
+ throw new Error("Slack onboarding cancelled");
19
+ const value = String(result).trim();
20
+ if (!value)
21
+ throw new Error(`${message}: value is required`);
22
+ return value;
23
+ }
24
+ /** Interactive single-workspace internal-app creation + installation. Safe to re-run after interruption. */
25
+ export async function onboardSlackInternalApp(input) {
26
+ installProxyFetch();
27
+ if (!input.envIgnored) {
28
+ throw new Error("`add slack` onboarding creates an app and writes real credentials to .env — " +
29
+ "add .env to .gitignore/.fastagentignore first, then re-run");
30
+ }
31
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
32
+ throw new Error("`add slack` needs an interactive terminal for internal-app creation and OAuth — " +
33
+ "re-run in a terminal, or pass --no-onboard to scaffold only");
34
+ }
35
+ await ensureStateRootSelfIgnored(input.target, input.stateRoot);
36
+ let state = await readSlackOnboardingState(input.stateRoot);
37
+ const resumed = state !== undefined;
38
+ if (input.replaceConfig && !state) {
39
+ throw new Error("--replace-config found no local Slack onboarding state on this machine — nothing to replace. " +
40
+ "Run `fastagent add slack` to onboard, or update the Request URL manually in the Slack console");
41
+ }
42
+ if (state?.installedAt) {
43
+ const env = await readFile(join(input.target, ".env"), "utf8")
44
+ .then(parseEnvContent)
45
+ .catch((error) => {
46
+ if (error.code === "ENOENT")
47
+ return new Map();
48
+ throw error;
49
+ });
50
+ const missingRuntime = [
51
+ "SLACK_BOT_TOKEN",
52
+ "SLACK_BOT_REFRESH_TOKEN",
53
+ "SLACK_BOT_TOKEN_EXPIRES_AT",
54
+ "SLACK_CLIENT_ID",
55
+ "SLACK_CLIENT_SECRET",
56
+ "SLACK_SIGNING_SECRET",
57
+ ].filter((name) => !((process.env[name] ?? env.get(name))?.trim() ?? ""));
58
+ if (missingRuntime.length > 0) {
59
+ throw new Error(`Slack app ${state.appId ?? "(unknown)"} is installed but .env is missing ${missingRuntime.join(", ")} — ` +
60
+ "restore them from the Slack app console, or delete the app + onboarding state and create a new one");
61
+ }
62
+ if (input.groupBehavior.explicit && state.groupBehavior !== input.groupBehavior.behavior) {
63
+ throw new Error(`the onboarded Slack app uses group behavior ${state.groupBehavior}; changing an installed app's ` +
64
+ "OAuth scopes is a migration. Keep the existing choice, or remove the app + Slack onboarding state and create a new app");
65
+ }
66
+ let action = "replace-config";
67
+ if (!input.replaceConfig) {
68
+ const answer = await select({
69
+ message: `Slack app ${state.appId ?? "(unknown)"} is already installed${state.teamName ? ` in ${state.teamName}` : ""}`,
70
+ initialValue: "keep",
71
+ options: [
72
+ { value: "keep", label: "Keep the installed app" },
73
+ {
74
+ value: "replace-config",
75
+ label: "Replace App Configuration tokens",
76
+ hint: "repair automatic dev/deploy Request URL updates",
77
+ },
78
+ ],
79
+ });
80
+ if (isCancel(answer))
81
+ throw new Error("Slack onboarding cancelled");
82
+ action = answer;
83
+ }
84
+ if (action === "replace-config") {
85
+ console.error(`[fastagent] generate a fresh App Configuration Token pair at ${CONFIG_TOKEN_URL}`);
86
+ openExternalUrl(CONFIG_TOKEN_URL);
87
+ const configToken = await promptValue("Slack configuration access token (xoxe.xoxp-…)", true);
88
+ const configRefreshToken = await promptValue("Slack configuration refresh token (xoxe-…)", true);
89
+ if (!configToken.startsWith("xoxe.") || !configRefreshToken.startsWith("xoxe-")) {
90
+ throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
91
+ }
92
+ await writeSlackOnboardingState(input.stateRoot, {
93
+ ...state,
94
+ configToken,
95
+ configRefreshToken,
96
+ configTokenExpiresAt: Date.now() + 11 * 60 * 60_000,
97
+ });
98
+ console.error("[fastagent] replaced local Slack App Configuration tokens; runtime app credentials are unchanged");
99
+ }
100
+ else {
101
+ console.error("[fastagent] keeping the installed Slack app and local configuration tokens");
102
+ }
103
+ return;
104
+ }
105
+ if (!state) {
106
+ const appName = await promptValue("Slack app name", false, `FastAgent ${basename(input.target)}`);
107
+ clackLog.info("Slack's configuration refresh token can manage apps owned by your user in this workspace. " +
108
+ "FastAgent stores it only in owner-readable local state; it is never deployed.");
109
+ console.error(`[fastagent] generate an App Configuration Token at ${CONFIG_TOKEN_URL}`);
110
+ openExternalUrl(CONFIG_TOKEN_URL);
111
+ const configToken = await promptValue("Slack configuration access token (xoxe.xoxp-…)", true);
112
+ const configRefreshToken = await promptValue("Slack configuration refresh token (xoxe-…)", true);
113
+ if (!configToken.startsWith("xoxe."))
114
+ throw new Error("Slack configuration access token must start with xoxe.");
115
+ if (!configRefreshToken.startsWith("xoxe-")) {
116
+ throw new Error("Slack configuration refresh token must start with xoxe-");
117
+ }
118
+ state = newSlackOnboardingState({
119
+ appName,
120
+ groupBehavior: input.groupBehavior.behavior,
121
+ configToken,
122
+ configRefreshToken,
123
+ });
124
+ await writeSlackOnboardingState(input.stateRoot, state);
125
+ }
126
+ else if (input.groupBehavior.explicit && !state.appId) {
127
+ state = { ...state, groupBehavior: input.groupBehavior.behavior };
128
+ await writeSlackOnboardingState(input.stateRoot, state);
129
+ }
130
+ if (state.createAttemptedAt && !state.appId) {
131
+ throw new Error(`a prior Slack app creation attempt at ${state.createAttemptedAt} returned no app ID — ` +
132
+ `inspect ${CONFIG_TOKEN_URL}; delete any incomplete app and ${input.stateRoot}/channels/slack/onboarding.json before retrying`);
133
+ }
134
+ // `--replace-config` also covers the created-but-not-installed state, where a revoked token would
135
+ // otherwise strand the resume (rotation fails and no menu offers replacement).
136
+ if (resumed && (!state.appId || input.replaceConfig)) {
137
+ let action = "replace-config";
138
+ if (!input.replaceConfig) {
139
+ const answer = await select({
140
+ message: "Resume Slack onboarding with which App Configuration tokens?",
141
+ initialValue: "keep",
142
+ options: [
143
+ { value: "keep", label: "Use the saved token pair" },
144
+ { value: "replace-config", label: "Paste a fresh token pair" },
145
+ ],
146
+ });
147
+ if (isCancel(answer))
148
+ throw new Error("Slack onboarding cancelled");
149
+ action = answer;
150
+ }
151
+ if (action === "replace-config") {
152
+ openExternalUrl(CONFIG_TOKEN_URL);
153
+ state = {
154
+ ...state,
155
+ configToken: await promptValue("Slack configuration access token (xoxe.xoxp-…)", true),
156
+ configRefreshToken: await promptValue("Slack configuration refresh token (xoxe-…)", true),
157
+ configTokenExpiresAt: Date.now() + 11 * 60 * 60_000,
158
+ };
159
+ if (!state.configToken.startsWith("xoxe.") || !state.configRefreshToken.startsWith("xoxe-")) {
160
+ throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
161
+ }
162
+ await writeSlackOnboardingState(input.stateRoot, state);
163
+ }
164
+ }
165
+ const server = await startSlackSetupServer();
166
+ const tunnel = await startCloudflareTunnel(server.port);
167
+ if (!tunnel) {
168
+ await server.close();
169
+ throw new Error("Slack onboarding needs a temporary HTTPS tunnel — install cloudflared and re-run");
170
+ }
171
+ const requestUrl = `${tunnel.url}${server.requestPath}`;
172
+ const redirectUrl = `${tunnel.url}${server.redirectPath}`;
173
+ console.error(`[fastagent] temporary Slack setup tunnel ready → ${tunnel.url}`);
174
+ try {
175
+ if (!(await waitForHealth(`${tunnel.url}/health`, 45_000, 500))) {
176
+ throw new Error("the temporary Slack setup tunnel did not become reachable; no app was created");
177
+ }
178
+ await onboardSlackApp({ stateRoot: input.stateRoot, state, requestUrl, redirectUrl }, {
179
+ note: (message) => clackLog.info(message),
180
+ openUrl: openExternalUrl,
181
+ waitForOAuth: () => server.waitForOAuth(),
182
+ writeRuntimeSecrets: async ({ botToken, botRefreshToken, botTokenExpiresAt, clientId, clientSecret, signingSecret, }) => {
183
+ const values = {
184
+ ...(botToken ? { SLACK_BOT_TOKEN: botToken } : {}),
185
+ ...(botRefreshToken ? { SLACK_BOT_REFRESH_TOKEN: botRefreshToken } : {}),
186
+ ...(botTokenExpiresAt ? { SLACK_BOT_TOKEN_EXPIRES_AT: String(botTokenExpiresAt) } : {}),
187
+ ...(clientId ? { SLACK_CLIENT_ID: clientId } : {}),
188
+ ...(clientSecret ? { SLACK_CLIENT_SECRET: clientSecret } : {}),
189
+ ...(signingSecret ? { SLACK_SIGNING_SECRET: signingSecret } : {}),
190
+ };
191
+ if (Object.keys(values).length > 0) {
192
+ await appendChannelDotEnv(input.target, "slack", values, Object.keys(values));
193
+ }
194
+ },
195
+ });
196
+ console.error("[fastagent] Slack app installed; rotating bot credentials and Signing Secret written to .env");
197
+ console.error(`[fastagent] run \`fastagent dev --tunnel\` next — FastAgent will rotate the config token and ` +
198
+ "replace the temporary Events API URL automatically");
199
+ }
200
+ finally {
201
+ tunnel.close();
202
+ await server.close().catch(() => { });
203
+ }
204
+ }
@@ -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: {