@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
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * `fastagent add <channel>`: drop a `channels/<kind>.ts` adapter-glue file (+ any companion tool, +
3
- * `.env.example` vars) into an existing workspace. `add` checks and guides; it never bootstraps a
4
- * workspace (that is `init`'s job). Each channel's template files live in its own bundle at
5
- * src/channels/<kind>/scaffold/, read here at scaffold time.
3
+ * `.secrets/.env.example` vars) into an existing agent. `add` checks and guides; it never
4
+ * bootstraps an agent (that is `init`'s job). Each channel's template files live in its own bundle
5
+ * at src/channels/<kind>/scaffold/, read here at scaffold time.
6
6
  */
7
7
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
8
- import { join } from "node:path";
8
+ import { dirname, join } from "node:path";
9
9
  import { detectRuntime } from "../runtime.js";
10
- import { assertInsideWorkspace } from "../workspace.js";
11
- import { channelBundleFiles, channelTemplate } from "./templates.js";
12
- import { exists } from "./init.js";
13
- import { parseEnvContent } from "../env.js";
10
+ import { SECRETS_DIRNAME, assertInsideAgentDir, exists } from "../paths.js";
11
+ import { baseTemplate, channelBundleFiles, channelTemplate } from "./templates.js";
12
+ import { dotEnvPath, envExamplePath, parseEnvContent } from "../env.js";
14
13
  const CHANNEL_SCAFFOLDS = {
15
14
  github: {
16
15
  env: [
@@ -21,8 +20,8 @@ const CHANNEL_SCAFFOLDS = {
21
20
  generate: true,
22
21
  },
23
22
  ],
24
- // `{channel}` / `{tools}` are path placeholders the CLI resolves to the real workspace-relative
25
- // location (agentDir-aware) — the CLI holds no channel-private filenames.
23
+ // `{channel}` / `{tools}` are path placeholders the CLI resolves to the real agent-dir-relative
24
+ // location — the CLI holds no channel-private filenames.
26
25
  steps: [
27
26
  "edit {channel} — map events to intents in on()",
28
27
  "add the webhook in your repo (Settings → Webhooks): Payload URL = <public-url>/webhook, content type application/json",
@@ -43,11 +42,39 @@ const CHANNEL_SCAFFOLDS = {
43
42
  "the agent can send messages or files back by calling the scaffolded {tools}/telegram-send.ts tool",
44
43
  ],
45
44
  },
45
+ slack: {
46
+ env: [
47
+ { name: "SLACK_BOT_TOKEN", hint: "Slack app → rotating Bot User OAuth access token", required: true },
48
+ {
49
+ name: "SLACK_BOT_REFRESH_TOKEN",
50
+ hint: "Slack OAuth bot refresh token (required when token rotation is enabled)",
51
+ required: false,
52
+ },
53
+ {
54
+ name: "SLACK_BOT_TOKEN_EXPIRES_AT",
55
+ hint: "Slack rotating bot access-token expiry (epoch milliseconds)",
56
+ required: false,
57
+ },
58
+ { name: "SLACK_CLIENT_ID", hint: "Slack app OAuth client ID (for bot-token rotation)", required: false },
59
+ {
60
+ name: "SLACK_CLIENT_SECRET",
61
+ hint: "Slack app OAuth client secret (for bot-token rotation)",
62
+ required: false,
63
+ },
64
+ { name: "SLACK_SIGNING_SECRET", hint: "Slack app → Basic Information → App Credentials", required: true },
65
+ ],
66
+ steps: [
67
+ "Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write, channels:history, groups:history, mpim:history",
68
+ "enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, message.im, message.channels, message.groups, message.mpim; set Request URL to <public-url>/slack",
69
+ "reinstall the app after changing scopes, then invite it to each channel it should read",
70
+ "the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
71
+ ],
72
+ },
46
73
  // Feishu is the canonical engine/cloud; Lark international reuses its protocol through a degraded
47
74
  // compatibility profile. Each remains its own channel KIND: route, env, state, console, onboarding.
48
- // No `generate` in either: every value comes FROM the platform (a locally generated Encrypt Key
49
- // would break inbound events until mirrored in the console). `add feishu` scan-creates the app;
50
- // Lark lacks that control-plane capability, so `add lark` guides console credential collection.
75
+ // This table is the webhook setup; continuous mode below selects only App ID/Secret. No `generate`
76
+ // in either: values come FROM the platform. `add feishu` scan-creates the app; Lark lacks that
77
+ // control-plane capability, so `add lark` guides console credential collection.
51
78
  feishu: {
52
79
  env: [
53
80
  {
@@ -72,8 +99,8 @@ const CHANNEL_SCAFFOLDS = {
72
99
  },
73
100
  ],
74
101
  steps: [
75
- "optional before publishing: add the sensitive im:message.group_msg permission (tenant-admin approval) to receive unmentioned managed-thread continuations and buffer other group discussion",
76
- "PUBLISH the app version on the page the CLI opened — the switch to webhook mode takes effect on publish (one click, once ever; no API for it)",
102
+ "before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (the CLI adds it to the app draft when supported); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
103
+ "PUBLISH the app version in the developer console after permission approval — the switch to webhook mode takes effect on publish (one click, once ever; no API for it)",
77
104
  "edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
78
105
  "the event Request URL is auto-registered by `dev --tunnel` / `deploy --run`",
79
106
  "the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
@@ -96,7 +123,7 @@ const CHANNEL_SCAFFOLDS = {
96
123
  ],
97
124
  steps: [
98
125
  "finish the console setup: enable Bot and add the required permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
99
- "optional before publishing: add the sensitive im:message.group_msg permission (tenant-admin approval) to receive unmentioned managed-thread continuations and buffer other group discussion",
126
+ "before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (add it manually if Lark's config API fallback was used); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
100
127
  "run `fastagent dev --tunnel` and keep it running; if auto-registration reports a config-API 404, manually switch Subscription mode to webhook, set its printed https://…/lark Request URL, save, then create + publish a version",
101
128
  "the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
102
129
  ],
@@ -104,18 +131,62 @@ const CHANNEL_SCAFFOLDS = {
104
131
  };
105
132
  /** The channel kinds `fastagent add <kind>` can scaffold. */
106
133
  export const CHANNEL_KINDS = Object.keys(CHANNEL_SCAFFOLDS);
107
- /** The env vars + next-step lines a scaffolded channel needs (for the CLI to print). */
108
- export function channelSetup(kind) {
109
- const { env, steps } = CHANNEL_SCAFFOLDS[kind];
110
- return { env, steps };
134
+ const WEBSOCKET_SETUPS = {
135
+ feishu: {
136
+ env: CHANNEL_SCAFFOLDS.feishu.env.filter((entry) => ["FEISHU_APP_ID", "FEISHU_APP_SECRET"].includes(entry.name)),
137
+ steps: [
138
+ "before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (the CLI adds it to the app draft when supported); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
139
+ "PUBLISH the app version in the developer console after permission approval — long-connection event subscriptions become active with the published version",
140
+ "edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
141
+ "run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
142
+ "the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
143
+ ],
144
+ },
145
+ lark: {
146
+ env: CHANNEL_SCAFFOLDS.lark.env.filter((entry) => ["LARK_APP_ID", "LARK_APP_SECRET"].includes(entry.name)),
147
+ steps: [
148
+ "before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (add it manually if Lark's config API fallback was used); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
149
+ "in Events & Callbacks choose long connection, subscribe im.message.receive_v1, then create + publish a version",
150
+ "edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
151
+ "run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
152
+ "the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
153
+ ],
154
+ },
155
+ };
156
+ /** The mode-specific env vars + next-step lines a scaffolded channel needs. */
157
+ export function channelSetup(kind, ingress = "webhook", groupBehavior) {
158
+ const behavior = groupBehavior ?? "context";
159
+ const setup = ingress === "websocket" && (kind === "feishu" || kind === "lark")
160
+ ? WEBSOCKET_SETUPS[kind]
161
+ : CHANNEL_SCAFFOLDS[kind];
162
+ if ((kind === "feishu" || kind === "lark") && behavior === "mentions") {
163
+ return {
164
+ env: setup.env,
165
+ steps: setup.steps.map((step) => step.includes("im:message.group_msg")
166
+ ? "group behavior: mention-only — do not grant im:message.group_msg; bare thread replies and group context buffering remain disabled. im:message:readonly is independent of this choice: add it if you want an @mention to carry the message it quotes (without it that quote degrades to a marker)"
167
+ : step),
168
+ };
169
+ }
170
+ if (kind === "slack" && behavior === "mentions") {
171
+ return {
172
+ env: setup.env,
173
+ steps: [
174
+ "Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write (no channel/group/mpim history scopes)",
175
+ "enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, and message.im; set Request URL to <public-url>/slack",
176
+ "group behavior: mention-only — bare thread replies and unsummoned group context remain disabled",
177
+ ...setup.steps.slice(2),
178
+ ],
179
+ };
180
+ }
181
+ return { env: setup.env, steps: setup.steps };
111
182
  }
112
183
  /**
113
184
  * Append a channel's env vars (commented placeholders + hints) to `.env.example`, so a developer who
114
185
  * copies it to `.env` finds the vars already there. No-op when there is no `.env.example` or the block
115
186
  * is already present. Placeholders only — no real secret lands in the committable template.
116
187
  */
117
- export async function appendChannelEnv(dir, kind) {
118
- const file = join(dir, ".env.example");
188
+ export async function appendChannelEnv(dir, kind, ingress = "webhook") {
189
+ const file = envExamplePath(dir);
119
190
  let current;
120
191
  try {
121
192
  current = await readFile(file, "utf8");
@@ -131,7 +202,9 @@ export async function appendChannelEnv(dir, kind) {
131
202
  // Hint on its OWN line above the placeholder (like the base env.example template) — never inline
132
203
  // after `=`: loadEnvFile does not strip trailing comments, so an uncommented `KEY= # hint` (or a
133
204
  // value pasted before the `#`) would carry the hint text into the parsed value.
134
- const block = `\n${marker}\n${CHANNEL_SCAFFOLDS[kind].env.map((e) => `# ${e.hint}\n# ${e.name}=`).join("\n")}\n`;
205
+ const block = `\n${marker}\n${channelSetup(kind, ingress)
206
+ .env.map((e) => `# ${e.hint}\n# ${e.name}=`)
207
+ .join("\n")}\n`;
135
208
  await appendFile(file, block);
136
209
  return true;
137
210
  }
@@ -146,15 +219,42 @@ function mentionsEnvName(content, name) {
146
219
  return content.split("\n").some((line) => new RegExp(`^\\s*#?\\s*${name}\\s*=`).test(line));
147
220
  }
148
221
  /**
149
- * Append generated channel secrets to the run-root `.env` (never `.env.example`) after the CLI has
150
- * verified that `.env` is gitignored. Existing non-empty values are kept — EXCEPT the names listed in
222
+ * Append generated channel secrets to the agent's `.env` (`.secrets/.env` — never `.env.example`)
223
+ * Existing non-empty values are kept — EXCEPT the names listed in
151
224
  * `overwrite`: those are authoritative (e.g. the credentials of an app `add feishu` JUST minted —
152
225
  * skipping them for a stale value would silently discard a fresh, unrecoverable secret). Manual values
153
226
  * (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
154
227
  * ready to edit while no fake secret is committed to the user's mental model.
155
228
  */
156
- export async function appendChannelDotEnv(dir, kind, generated, overwrite = []) {
157
- const file = join(dir, ".env");
229
+ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
230
+ const file = dotEnvPath(dir);
231
+ const secretsDir = dirname(file);
232
+ await mkdir(secretsDir, { recursive: true });
233
+ // THE one exception to "fastagent has no opinion about git": the directory it writes secrets into
234
+ // carries its own `.gitignore`. `init` writes it, and so does this — the reachable case where it is
235
+ // missing (a hand-made agent) is exactly the one where the next line mints an unrecoverable app
236
+ // secret. `wx`, so a file the author wrote is never touched; the accepted cost is that someone who
237
+ // DELETED it to track secrets deliberately gets it back once. The risk is not symmetric — that is an
238
+ // annoyance; the other way is a published credential.
239
+ //
240
+ // Scoped to the DEFAULT `<agentDir>/.secrets`, which is fastagent's own directory. A dir named by
241
+ // `FASTAGENT_SECRETS_DIR` belongs to the operator, and this template is `*` plus two negations —
242
+ // dropping it there would hide that directory's OTHER contents from their `git add`, which is a
243
+ // bigger harm than the one it prevents, and inflicted on a path they chose deliberately. They get the
244
+ // fact instead, and own the decision.
245
+ const owned = secretsDir === join(dir, SECRETS_DIRNAME);
246
+ let unprotectedSecretsDir;
247
+ if (owned) {
248
+ // Only EEXIST is tolerable (already protected, or a concurrent writer). A permission/disk failure on
249
+ // the file that keeps credentials out of git must surface, not be swallowed.
250
+ await writeFile(join(secretsDir, ".gitignore"), baseTemplate("secrets.gitignore"), { flag: "wx" }).catch((e) => {
251
+ if (e.code !== "EEXIST")
252
+ throw e;
253
+ });
254
+ }
255
+ else if (!(await exists(join(secretsDir, ".gitignore")))) {
256
+ unprotectedSecretsDir = secretsDir;
257
+ }
158
258
  let current = "";
159
259
  try {
160
260
  current = await readFile(file, "utf8");
@@ -163,14 +263,15 @@ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [])
163
263
  if (e.code !== "ENOENT")
164
264
  throw e;
165
265
  }
166
- const alreadySet = CHANNEL_SCAFFOLDS[kind].env
266
+ const env = channelSetup(kind, ingress).env;
267
+ const alreadySet = env
167
268
  .filter((e) => !overwrite.includes(e.name) && hasActiveEnvValue(current, e.name))
168
269
  .map((e) => e.name);
169
270
  const lines = [];
170
271
  const written = [];
171
272
  const contentLines = current.split("\n");
172
273
  let replacedInPlace = false;
173
- for (const e of CHANNEL_SCAFFOLDS[kind].env) {
274
+ for (const e of env) {
174
275
  if (alreadySet.includes(e.name))
175
276
  continue;
176
277
  const value = generated[e.name];
@@ -216,7 +317,7 @@ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [])
216
317
  await appendFile(file, `${prefix}${marker}\n${lines.join("\n")}\n`);
217
318
  }
218
319
  }
219
- return { written, alreadySet };
320
+ return { written, alreadySet, unprotectedSecretsDir };
220
321
  }
221
322
  /** The path `add <kind>` scaffolds to. */
222
323
  function channelPath(dir, kind) {
@@ -230,10 +331,10 @@ export async function channelExists(dir, kind) {
230
331
  * Scaffold `channels/<kind>.ts` into {@link dir}. Never clobbers an existing file (the glue is
231
332
  * authored content). The wx write is the TOCTOU safety net behind {@link channelExists}.
232
333
  */
233
- export async function scaffoldChannel(dir, kind) {
334
+ export async function scaffoldChannel(dir, kind, options = {}) {
234
335
  const channelsDir = join(dir, "channels");
235
- // Don't write through a channels/ symlink that escapes the workspace; an in-workspace one is fine.
236
- await assertInsideWorkspace(dir, "channels");
336
+ // Don't write through a channels/ symlink that escapes the agent dir; one inside it is fine.
337
+ await assertInsideAgentDir(dir, "channels");
237
338
  const file = channelPath(dir, kind);
238
339
  if (await exists(file)) {
239
340
  throw new Error(`${file} already exists — edit it, or remove it to re-scaffold`);
@@ -242,8 +343,42 @@ export async function scaffoldChannel(dir, kind) {
242
343
  // `channel.ts` is THE adapter (→ channels/<kind>.ts); any other .ts in the bundle is a companion tool
243
344
  // (→ tools/<name>, never clobbering an authored one).
244
345
  for (const name of channelBundleFiles(kind)) {
245
- const content = channelTemplate(kind, name);
346
+ let content = channelTemplate(kind, name);
246
347
  if (name === "channel.ts") {
348
+ if ((kind === "feishu" || kind === "lark") && options.ingress === "websocket") {
349
+ const factory = `${kind}Channel`;
350
+ const wsFactory = `${kind}WebSocketChannel`;
351
+ let configured = content
352
+ .replace(`import { ${factory} }`, `import { ${wsFactory} }`)
353
+ .replace(`export default ${factory}({`, `export default ${wsFactory}({`);
354
+ if (configured === content)
355
+ throw new Error(`${kind} channel template has no factory anchors`);
356
+ const prefix = kind === "feishu" ? "FEISHU" : "LARK";
357
+ const exportAt = configured.indexOf("export default");
358
+ const importEnd = configured.indexOf("\n\n");
359
+ if (exportAt < 0 || importEnd < 0)
360
+ throw new Error(`${kind} channel template header anchors are missing`);
361
+ const brand = kind === "feishu" ? "Feishu" : "Lark";
362
+ configured =
363
+ `${configured.slice(0, importEnd)}\n\n` +
364
+ `// ${brand} WebSocket long connection: the process connects OUT to the platform, so no public URL,\n` +
365
+ `// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long\n` +
366
+ `// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process\n` +
367
+ `// running in production: scale-to-zero/App Sleeping would disconnect ingress.\n` +
368
+ configured.slice(exportAt);
369
+ configured = configured
370
+ .split("\n")
371
+ .filter((line) => !line.includes(`verificationToken: process.env.${prefix}_VERIFICATION_TOKEN`) &&
372
+ !line.includes(`encryptKey: process.env.${prefix}_ENCRYPT_KEY`))
373
+ .join("\n");
374
+ content = configured;
375
+ }
376
+ if (kind === "slack" && options.groupBehavior === "mentions") {
377
+ const configured = content.replace('groupBehavior: "context"', 'groupBehavior: "mentions"');
378
+ if (configured === content)
379
+ throw new Error("slack channel template has no groupBehavior anchor");
380
+ content = configured;
381
+ }
247
382
  await writeFile(file, content, { flag: "wx" });
248
383
  continue;
249
384
  }
@@ -256,7 +391,7 @@ export async function scaffoldChannel(dir, kind) {
256
391
  return file;
257
392
  }
258
393
  /**
259
- * Verify the workspace is ready to host a channel: an ESM package.json that declares
394
+ * Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
260
395
  * `@fastagent-sh/fastagent` (the channel file imports it). `add` checks and guides, never bootstraps — that
261
396
  * is `init`'s job.
262
397
  */
@@ -268,11 +403,11 @@ export async function assertChannelReady(dir) {
268
403
  }
269
404
  catch (e) {
270
405
  if (e.code === "ENOENT") {
271
- // `dir` is where the kit lives (agentDir when set) "run init" is only the right advice when no
272
- // workspace exists yet; a kit missing its manifest (e.g. a --minimal init) needs the manifest, not init.
273
- throw new Error(`${dir}: no package.json — a channel adapter is code and needs the kit's own manifest. ` +
274
- `Run \`fastagent init\` for a fresh workspace, or add a package.json with @fastagent-sh/fastagent there ` +
275
- `(a --minimal init has none)`);
406
+ // `dir` is the AGENT dir, so `fastagent init` here would nest a second agent inside it the
407
+ // right remedy is the missing manifest (a --minimal init writes none), or init in the workspace.
408
+ throw new Error(`${dir}: no package.json — a channel adapter is code and needs the agent's own manifest. ` +
409
+ `Add a package.json declaring @fastagent-sh/fastagent there (a --minimal init writes none), ` +
410
+ `or run \`fastagent init\` in the workspace for a fresh agent`);
276
411
  }
277
412
  throw e;
278
413
  }
@@ -1,54 +1,45 @@
1
+ /** The agent directory name for a raw `--agent-dir` value: the default when unset, and `./bot` read as
2
+ * `bot` — `basename` already says that is what it means, so rejecting the spelling would be pedantry. */
3
+ export declare function agentDirName(raw: string | undefined): string;
4
+ /** Why `name` cannot be an agent directory name, or undefined when it can. It must stay ONE segment
5
+ * inside the target: anything else (a separator, `..`, an absolute path) would land the agent where the
6
+ * one-level lookup cannot see it — an agent nothing would ever serve.
7
+ *
8
+ * Returns the CONSTRAINT, not a sentence: the CLI prefixes the flag it owns and reports it as the usage
9
+ * error it is (exit 2), while {@link scaffoldAgent} prefixes the option name for a programmatic caller,
10
+ * who never passed a flag and should not be told to fix one. */
11
+ export declare function agentDirNameError(name: string): string | undefined;
1
12
  export interface ScaffoldOptions {
2
13
  /** Scaffold the markdown-only unit (no package.json, no tool, no install) instead of a complete agent. */
3
14
  minimal?: boolean;
4
- /**
5
- * Place the agent kit (persona.md/skills/tools/package.json) in this subdirectory (e.g. "./agent")
6
- * instead of flat in `dir`; the config is written at the root with `agentDir` pointing there.
7
- * Undefined = flat. The CLI decides (jurisdiction detection + flags); this stays mechanical.
8
- */
15
+ /** The agent directory's name inside `dir` — default {@link DEFAULT_AGENT_DIRNAME}, `"."` for `dir`
16
+ * itself. ONE path segment: a separator or `..` would put the agent outside the directory the author
17
+ * named, where the one-level lookup could never find it. */
9
18
  agentDir?: string;
10
19
  }
11
20
  export interface ScaffoldResult {
12
21
  dir: string;
13
22
  /** Whether a complete (code-tool) agent was scaffolded (false for --minimal). */
14
23
  complete: boolean;
15
- /** The kit subdirectory in effect (relative, e.g. "./agent"); undefined = flat. */
16
- agentDir?: string;
17
- /** Files written by this run (relative paths). */
24
+ /** The agent dir relative to `dir`: the {@link ScaffoldOptions.agentDir} that was used. */
25
+ agentDir: string;
26
+ /** Files written by this run (relative to `dir`). */
18
27
  created: string[];
19
- /** Files that already existed and were kept untouched (e.g. a pre-existing .gitignore). */
20
- skipped: string[];
21
- /** Kept ignore files appended with missing fastagent excludes (root .gitignore: .env/.fastagent, plus
22
- * node_modules/ in the flat layout; kit .gitignore in the agentDir layout: node_modules/). */
23
- patched: string[];
24
- /** True if the target already had content before this run (init into an existing/non-empty dir). */
25
- intoNonEmpty: boolean;
26
- /** Non-fatal advisories the caller MUST surface. */
27
- warnings: string[];
28
+ /** Files that already existed and were KEPT untouched. Only reachable with `agentDir: "."` (a
29
+ * subdirectory target is proven empty first): adopting a directory means its `.gitignore`/
30
+ * `package.json` are the author's. The caller surfaces them silently skipping a file the user
31
+ * expected would be worse. */
32
+ kept: string[];
28
33
  }
29
34
  /**
30
- * Jurisdiction signals: evidence that an existing system already claims this tree, so a flat agent kit
31
- * would put each side's files under the other's jurisdiction (host tsc sweeps agent .ts; fastagent
32
- * scans host tools/). Three classes, derived from the actual failure modes — a toolchain config, a
33
- * deploy manifest, or fastagent's convention names already occupied. Any hit → the kit defaults into
34
- * `./agent`. Deliberately NOT signals: "dir is non-empty", "has package.json", "has src/" — markdown
35
- * and loose scripts are claimed by nobody, and "a directory is an agent" stays the default. Known
36
- * tradeoff, decided for visibility: a HAND-BUILT agent dir (skills//tools/ authored for the agent, no
37
- * config yet) also hits the occupation signal and defaults to ./agent wrong for that case, but the
38
- * reason is printed and `--flat` overrides; the reverse default would silently mis-scan a host's dirs.
39
- */
40
- export declare function detectHostSignals(dir: string): Promise<string[]>;
41
- /** The `cd` target to show in `init`'s next-steps: the relative path when the target is inside `cwd`,
42
- * the absolute path when it climbs out (a `../../..` is noise), or undefined when already in `cwd`. */
43
- export declare function nextStepCd(cwd: string, dir: string): string | undefined;
44
- /** Does a path exist? (async; shared with the sibling scaffold modules). */
45
- export declare function exists(p: string): Promise<boolean>;
46
- /**
47
- * Scaffold a runnable workspace into {@link dir} (created if missing). Default is a complete agent
48
- * (persona.md + the writing-great-skills skill + a code tool + package.json); `--minimal` drops the
49
- * code tool and package.json. The kit goes flat into `dir`, or into {@link ScaffoldOptions.agentDir}
50
- * (config at the root pointing there). Refuses only an existing fastagent.config.* (the ownership
51
- * marker — already a workspace); every other pre-existing file (AGENTS.md, .gitignore, package.json)
52
- * is kept, never overwritten — an existing AGENTS.md is the project's context, adopted as-is.
35
+ * Scaffold a runnable agent into `<dir>/<agentDir>/` or into `dir` itself when `agentDir` is `"."`
36
+ * (both created if missing). Default is a complete agent (persona.md + the writing-great-skills skill +
37
+ * a code tool + package.json); `--minimal` drops the code tool and package.json.
38
+ *
39
+ * A SUBDIRECTORY target must be empty (any content there is an unfinished agent or something unrelated,
40
+ * and landing persona.md beside it would be a silent mix), while `"."` is a directory being adopted
41
+ * content is expected. So `"."` KEEPS every file that already exists (reported, never overwritten, never
42
+ * verified) and refuses only on a config, which means the directory is already an agent. An existing
43
+ * AGENTS.md is untouched either way: that is the project's context, adopted as-is.
53
44
  */
54
- export declare function scaffoldWorkspace(dir: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
45
+ export declare function scaffoldAgent(dir: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;