@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
@@ -43,11 +43,39 @@ const CHANNEL_SCAFFOLDS = {
43
43
  "the agent can send messages or files back by calling the scaffolded {tools}/telegram-send.ts tool",
44
44
  ],
45
45
  },
46
+ slack: {
47
+ env: [
48
+ { name: "SLACK_BOT_TOKEN", hint: "Slack app → rotating Bot User OAuth access token", required: true },
49
+ {
50
+ name: "SLACK_BOT_REFRESH_TOKEN",
51
+ hint: "Slack OAuth bot refresh token (required when token rotation is enabled)",
52
+ required: false,
53
+ },
54
+ {
55
+ name: "SLACK_BOT_TOKEN_EXPIRES_AT",
56
+ hint: "Slack rotating bot access-token expiry (epoch milliseconds)",
57
+ required: false,
58
+ },
59
+ { name: "SLACK_CLIENT_ID", hint: "Slack app OAuth client ID (for bot-token rotation)", required: false },
60
+ {
61
+ name: "SLACK_CLIENT_SECRET",
62
+ hint: "Slack app OAuth client secret (for bot-token rotation)",
63
+ required: false,
64
+ },
65
+ { name: "SLACK_SIGNING_SECRET", hint: "Slack app → Basic Information → App Credentials", required: true },
66
+ ],
67
+ steps: [
68
+ "Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write, channels:history, groups:history, mpim:history",
69
+ "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",
70
+ "reinstall the app after changing scopes, then invite it to each channel it should read",
71
+ "the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
72
+ ],
73
+ },
46
74
  // Feishu is the canonical engine/cloud; Lark international reuses its protocol through a degraded
47
75
  // 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.
76
+ // This table is the webhook setup; continuous mode below selects only App ID/Secret. No `generate`
77
+ // in either: values come FROM the platform. `add feishu` scan-creates the app; Lark lacks that
78
+ // control-plane capability, so `add lark` guides console credential collection.
51
79
  feishu: {
52
80
  env: [
53
81
  {
@@ -72,8 +100,8 @@ const CHANNEL_SCAFFOLDS = {
72
100
  },
73
101
  ],
74
102
  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)",
103
+ "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 managed-thread replies can invoke and other unsummoned discussion can buffer",
104
+ "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
105
  "edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
78
106
  "the event Request URL is auto-registered by `dev --tunnel` / `deploy --run`",
79
107
  "the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
@@ -96,7 +124,7 @@ const CHANNEL_SCAFFOLDS = {
96
124
  ],
97
125
  steps: [
98
126
  "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",
127
+ "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 managed-thread replies can invoke and other unsummoned discussion can buffer",
100
128
  "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
129
  "the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
102
130
  ],
@@ -104,17 +132,61 @@ const CHANNEL_SCAFFOLDS = {
104
132
  };
105
133
  /** The channel kinds `fastagent add <kind>` can scaffold. */
106
134
  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 };
135
+ const WEBSOCKET_SETUPS = {
136
+ feishu: {
137
+ env: CHANNEL_SCAFFOLDS.feishu.env.filter((entry) => ["FEISHU_APP_ID", "FEISHU_APP_SECRET"].includes(entry.name)),
138
+ steps: [
139
+ "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 managed-thread replies can invoke and other unsummoned discussion can buffer",
140
+ "PUBLISH the app version in the developer console after permission approval — long-connection event subscriptions become active with the published version",
141
+ "edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
142
+ "run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
143
+ "the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
144
+ ],
145
+ },
146
+ lark: {
147
+ env: CHANNEL_SCAFFOLDS.lark.env.filter((entry) => ["LARK_APP_ID", "LARK_APP_SECRET"].includes(entry.name)),
148
+ steps: [
149
+ "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 managed-thread replies can invoke and other unsummoned discussion can buffer",
150
+ "in Events & Callbacks choose long connection, subscribe im.message.receive_v1, then create + publish a version",
151
+ "edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
152
+ "run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
153
+ "the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
154
+ ],
155
+ },
156
+ };
157
+ /** The mode-specific env vars + next-step lines a scaffolded channel needs. */
158
+ export function channelSetup(kind, ingress = "webhook", groupBehavior) {
159
+ const behavior = groupBehavior ?? "context";
160
+ const setup = ingress === "websocket" && (kind === "feishu" || kind === "lark")
161
+ ? WEBSOCKET_SETUPS[kind]
162
+ : CHANNEL_SCAFFOLDS[kind];
163
+ if ((kind === "feishu" || kind === "lark") && behavior === "mentions") {
164
+ return {
165
+ env: setup.env,
166
+ steps: setup.steps.map((step) => step.includes("im:message.group_msg")
167
+ ? "group behavior: mention-only — do not grant im:message.group_msg; bare managed-thread replies and group context buffering remain disabled"
168
+ : step),
169
+ };
170
+ }
171
+ if (kind === "slack" && behavior === "mentions") {
172
+ return {
173
+ env: setup.env,
174
+ steps: [
175
+ "Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write (no channel/group/mpim history scopes)",
176
+ "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",
177
+ "group behavior: mention-only — bare managed-thread replies and unsummoned group context remain disabled",
178
+ ...setup.steps.slice(2),
179
+ ],
180
+ };
181
+ }
182
+ return { env: setup.env, steps: setup.steps };
111
183
  }
112
184
  /**
113
185
  * Append a channel's env vars (commented placeholders + hints) to `.env.example`, so a developer who
114
186
  * copies it to `.env` finds the vars already there. No-op when there is no `.env.example` or the block
115
187
  * is already present. Placeholders only — no real secret lands in the committable template.
116
188
  */
117
- export async function appendChannelEnv(dir, kind) {
189
+ export async function appendChannelEnv(dir, kind, ingress = "webhook") {
118
190
  const file = join(dir, ".env.example");
119
191
  let current;
120
192
  try {
@@ -131,7 +203,9 @@ export async function appendChannelEnv(dir, kind) {
131
203
  // Hint on its OWN line above the placeholder (like the base env.example template) — never inline
132
204
  // after `=`: loadEnvFile does not strip trailing comments, so an uncommented `KEY= # hint` (or a
133
205
  // 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`;
206
+ const block = `\n${marker}\n${channelSetup(kind, ingress)
207
+ .env.map((e) => `# ${e.hint}\n# ${e.name}=`)
208
+ .join("\n")}\n`;
135
209
  await appendFile(file, block);
136
210
  return true;
137
211
  }
@@ -153,7 +227,7 @@ function mentionsEnvName(content, name) {
153
227
  * (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
154
228
  * ready to edit while no fake secret is committed to the user's mental model.
155
229
  */
156
- export async function appendChannelDotEnv(dir, kind, generated, overwrite = []) {
230
+ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
157
231
  const file = join(dir, ".env");
158
232
  let current = "";
159
233
  try {
@@ -163,14 +237,15 @@ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [])
163
237
  if (e.code !== "ENOENT")
164
238
  throw e;
165
239
  }
166
- const alreadySet = CHANNEL_SCAFFOLDS[kind].env
240
+ const env = channelSetup(kind, ingress).env;
241
+ const alreadySet = env
167
242
  .filter((e) => !overwrite.includes(e.name) && hasActiveEnvValue(current, e.name))
168
243
  .map((e) => e.name);
169
244
  const lines = [];
170
245
  const written = [];
171
246
  const contentLines = current.split("\n");
172
247
  let replacedInPlace = false;
173
- for (const e of CHANNEL_SCAFFOLDS[kind].env) {
248
+ for (const e of env) {
174
249
  if (alreadySet.includes(e.name))
175
250
  continue;
176
251
  const value = generated[e.name];
@@ -230,7 +305,7 @@ export async function channelExists(dir, kind) {
230
305
  * Scaffold `channels/<kind>.ts` into {@link dir}. Never clobbers an existing file (the glue is
231
306
  * authored content). The wx write is the TOCTOU safety net behind {@link channelExists}.
232
307
  */
233
- export async function scaffoldChannel(dir, kind) {
308
+ export async function scaffoldChannel(dir, kind, options = {}) {
234
309
  const channelsDir = join(dir, "channels");
235
310
  // Don't write through a channels/ symlink that escapes the workspace; an in-workspace one is fine.
236
311
  await assertInsideWorkspace(dir, "channels");
@@ -242,8 +317,42 @@ export async function scaffoldChannel(dir, kind) {
242
317
  // `channel.ts` is THE adapter (→ channels/<kind>.ts); any other .ts in the bundle is a companion tool
243
318
  // (→ tools/<name>, never clobbering an authored one).
244
319
  for (const name of channelBundleFiles(kind)) {
245
- const content = channelTemplate(kind, name);
320
+ let content = channelTemplate(kind, name);
246
321
  if (name === "channel.ts") {
322
+ if ((kind === "feishu" || kind === "lark") && options.ingress === "websocket") {
323
+ const factory = `${kind}Channel`;
324
+ const wsFactory = `${kind}WebSocketChannel`;
325
+ let configured = content
326
+ .replace(`import { ${factory} }`, `import { ${wsFactory} }`)
327
+ .replace(`export default ${factory}({`, `export default ${wsFactory}({`);
328
+ if (configured === content)
329
+ throw new Error(`${kind} channel template has no factory anchors`);
330
+ const prefix = kind === "feishu" ? "FEISHU" : "LARK";
331
+ const exportAt = configured.indexOf("export default");
332
+ const importEnd = configured.indexOf("\n\n");
333
+ if (exportAt < 0 || importEnd < 0)
334
+ throw new Error(`${kind} channel template header anchors are missing`);
335
+ const brand = kind === "feishu" ? "Feishu" : "Lark";
336
+ configured =
337
+ `${configured.slice(0, importEnd)}\n\n` +
338
+ `// ${brand} WebSocket long connection: the process connects OUT to the platform, so no public URL,\n` +
339
+ `// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long\n` +
340
+ `// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process\n` +
341
+ `// running in production: scale-to-zero/App Sleeping would disconnect ingress.\n` +
342
+ configured.slice(exportAt);
343
+ configured = configured
344
+ .split("\n")
345
+ .filter((line) => !line.includes(`verificationToken: process.env.${prefix}_VERIFICATION_TOKEN`) &&
346
+ !line.includes(`encryptKey: process.env.${prefix}_ENCRYPT_KEY`))
347
+ .join("\n");
348
+ content = configured;
349
+ }
350
+ if (kind === "slack" && options.groupBehavior === "mentions") {
351
+ const configured = content.replace('groupBehavior: "context"', 'groupBehavior: "mentions"');
352
+ if (configured === content)
353
+ throw new Error("slack channel template has no groupBehavior anchor");
354
+ content = configured;
355
+ }
247
356
  await writeFile(file, content, { flag: "wx" });
248
357
  continue;
249
358
  }
@@ -2,15 +2,16 @@
2
2
  // Your agent's identity lives in persona.md; its capabilities in skills/ + tools/ — never here.
3
3
  // An AGENTS.md at the workspace root (yours or the host repo's) is read as project context.
4
4
  // Model precedence: `--model` flag > FASTAGENT_MODEL env > this default.
5
- // No model is preset: `fastagent dev` prompts you to pick one from the providers you're logged into
6
- // (run `fastagent login` first) and writes your choice below. Or set it by hand to a "provider/modelId"
7
- // you have access to (`fastagent models` lists them).
5
+ // No model is preset: `fastagent dev` shows the full model catalog (models you already have
6
+ // credentials for come first; picking one that needs auth logs you in inline) and writes your choice
7
+ // below. Or set it by hand to a "provider/modelId" (`fastagent models` lists them).
8
8
  export default {
9
9
  // model: "openai-codex/gpt-5.5",
10
10
  // thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max); default "medium" (pi TUI parity)
11
11
  http: { port: 8787 },
12
12
  // selfSchedule: true, // mount the built-in `wake` tool: the agent schedules its own follow-up turns
13
13
  // // ("check the deploy in 10 min"). Cron jobs need no opt-in — drop a schedules/<name>.ts.
14
+ // sessionControl: true, // serve /control/* for remote observation + steering (fastagent attach / Web panel)
14
15
  // deploy: what the agent needs on the box (so `fastagent deploy` doesn't need a hand-written Dockerfile
15
16
  // or hand-set host variables). Uncomment as needed:
16
17
  // deploy: {
@@ -26,9 +26,6 @@ export declare const MAX_PENDING_WAKEUPS = 20;
26
26
  * wake into an active conversation fires in the first gap between the user's turns; this generous
27
27
  * ceiling (~1h) only gives up on a pathologically stuck session (then logs, operator-visible). */
28
28
  export declare const MAX_WAKE_ATTEMPTS = 120;
29
- /** The minimum gap between two consecutive fires of a RECURRING wake — stricter than the one-shot floor:
30
- * a recurring runs forever, so a tight cron is a permanent token burner, not a one-time mistake. */
31
- export declare const MIN_RECURRING_GAP_MS: number;
32
29
  /** The current pending wake-ups (`fastagent schedule list` uses this). */
33
30
  export declare function listWakeups(stateRoot: string): Wakeup[];
34
31
  export type AddWakeupResult = {
@@ -26,7 +26,7 @@ export const MAX_PENDING_WAKEUPS = 20;
26
26
  export const MAX_WAKE_ATTEMPTS = 120;
27
27
  /** The minimum gap between two consecutive fires of a RECURRING wake — stricter than the one-shot floor:
28
28
  * a recurring runs forever, so a tight cron is a permanent token burner, not a one-time mistake. */
29
- export const MIN_RECURRING_GAP_MS = 10 * 60_000; // 10 minutes
29
+ const MIN_RECURRING_GAP_MS = 10 * 60_000; // 10 minutes
30
30
  /** A stored entry is a real Wakeup: the fields are present and `fireAt` is a parseable date. A malformed
31
31
  * one (bad/missing fireAt) would compare NaN <= now = false forever — never due, never cleared, but still
32
32
  * eating the pending quota. So validate at this IO boundary and drop it (warn), like a corrupt file. */
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The remote `SessionControl` — the client half of the Phase 3 transport (design §13). Engine- and
3
+ * server-neutral: speaks only the wire protocol `controlRoutes` serves (HTTP JSON + SSE with the
4
+ * {sessionId, epoch, seq, event} envelope) and re-exposes the SAME `SessionControl` interface, so
5
+ * local and remote consumers are isomorphic — client code does not change when the agent moves out
6
+ * of process.
7
+ *
8
+ * Envelope consumption is internal: a seq gap (loss in transit on this connection) — and any
9
+ * mid-stream transport failure, a server restart included (its connections drop) — THROWS from
10
+ * the events iterator, so the consumer's failure handling and budget own it; only the consumer's
11
+ * own detach reads as a clean end. Recovery is the standard reconnect steps (`entries({ since })`
12
+ * → `state()` → resubscribe), exactly as after any disconnect. The envelope's `epoch` is
13
+ * informational for consumers that correlate ACROSS connections — within one connection it cannot
14
+ * change, so this client does not compare it. Nothing here retries silently: a broken stream is
15
+ * visible as a thrown iteration error, a failed request as a rejected promise.
16
+ */
17
+ import type { Agent } from "./agent.ts";
18
+ import type { SessionControl } from "./session.ts";
19
+ /** A control request the server answered with a non-2xx status. Carries the STRUCTURED status so a
20
+ * consumer distinguishing auth failure (401 — stale token, unrecoverable) from transient transport
21
+ * trouble branches on `status`, never on message prose. */
22
+ export declare class ControlRequestError extends Error {
23
+ readonly status: number;
24
+ constructor(status: number, body: string);
25
+ }
26
+ /** Connection parameters shared by BOTH remote planes (`connectSessionControl` and
27
+ * `connectAgent`) — plane-neutral on purpose: one endpoint, one token, two contracts. */
28
+ export interface RemoteEndpointOptions {
29
+ /** Base URL of the serving process (e.g. `http://127.0.0.1:8787`); `/control/*` is appended. */
30
+ url: string;
31
+ /** The shared bearer secret (`<stateRoot>/control.json` on the serving machine). */
32
+ token: string;
33
+ /** Injectable for tests. Defaults to global fetch. */
34
+ fetchFn?: typeof fetch;
35
+ }
36
+ /**
37
+ * Connect and return a remote `SessionControl`. Async because `capabilities()` is synchronous in
38
+ * the contract: the static declaration is fetched ONCE here and served from memory — which also
39
+ * makes a wrong URL/token fail at connect time, not on first use.
40
+ */
41
+ export declare function connectSessionControl(options: RemoteEndpointOptions): Promise<SessionControl>;
42
+ /**
43
+ * The remote DATA plane: an `Agent` whose `invoke` drives `POST /control/invoke` on a serving
44
+ * process — paired with {@link connectSessionControl}, a client holds a full remote fastagent
45
+ * instance through the same two contracts local code uses. A REAL Agent, failure discipline
46
+ * included: SPEC MUST 2 forbids iteration throws, so every failure — transport (401/refused/
47
+ * dropped mid-stream), protocol, and the images precheck — becomes a terminal `failed` event
48
+ * (`retryable` from the HTTP status where one exists; network trouble is retryable). Breaking out
49
+ * of iteration disconnects the request, which cancels the run (SPEC cancellation semantics travel
50
+ * the wire). The invoke wire is text-only for now: a prompt with images fails visibly instead of
51
+ * silently dropping them (steer/follow_up on the control plane carry full Prompts).
52
+ */
53
+ export declare function connectAgent(options: RemoteEndpointOptions): Agent;