@fastagent-sh/fastagent 0.13.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 (238) hide show
  1. package/README.md +5 -5
  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 +43 -0
  10. package/dist/channels/feishu/context-buffer.js +72 -0
  11. package/dist/channels/feishu/crypto.d.ts +4 -2
  12. package/dist/channels/feishu/crypto.js +4 -2
  13. package/dist/channels/feishu/feishu-api.d.ts +15 -7
  14. package/dist/channels/feishu/feishu-api.js +22 -4
  15. package/dist/channels/feishu/feishu.d.ts +38 -16
  16. package/dist/channels/feishu/feishu.js +286 -151
  17. package/dist/channels/feishu/invoke-turn.d.ts +24 -31
  18. package/dist/channels/feishu/invoke-turn.js +61 -62
  19. package/dist/channels/feishu/model.d.ts +98 -0
  20. package/dist/channels/feishu/model.js +9 -0
  21. package/dist/channels/feishu/normalize.d.ts +23 -0
  22. package/dist/channels/feishu/normalize.js +132 -0
  23. package/dist/channels/feishu/owned-threads.d.ts +7 -0
  24. package/dist/channels/feishu/owned-threads.js +47 -0
  25. package/dist/channels/feishu/parse.d.ts +21 -103
  26. package/dist/channels/feishu/parse.js +35 -145
  27. package/dist/channels/feishu/preview.d.ts +4 -7
  28. package/dist/channels/feishu/preview.js +26 -142
  29. package/dist/channels/feishu/register-app.d.ts +2 -1
  30. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  31. package/dist/channels/feishu/scaffold/channel.ts +10 -3
  32. package/dist/channels/feishu/setup-mode.d.ts +8 -0
  33. package/dist/channels/feishu/setup-mode.js +2 -0
  34. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  35. package/dist/channels/feishu/ws-ingress.js +136 -0
  36. package/dist/channels/github/github.js +8 -6
  37. package/dist/channels/http.d.ts +14 -0
  38. package/dist/channels/http.js +35 -2
  39. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  40. package/dist/channels/invoke-turn-kit.js +87 -0
  41. package/dist/channels/lark/lark.d.ts +4 -2
  42. package/dist/channels/lark/lark.js +4 -1
  43. package/dist/channels/lark/onboard.d.ts +8 -4
  44. package/dist/channels/lark/onboard.js +8 -0
  45. package/dist/channels/lark/scaffold/channel.ts +10 -3
  46. package/dist/channels/preview-kit.d.ts +109 -0
  47. package/dist/channels/preview-kit.js +183 -0
  48. package/dist/channels/seen.d.ts +5 -0
  49. package/dist/channels/seen.js +35 -0
  50. package/dist/channels/slack/bot-auth.d.ts +15 -0
  51. package/dist/channels/slack/bot-auth.js +146 -0
  52. package/dist/channels/slack/config-api.d.ts +60 -0
  53. package/dist/channels/slack/config-api.js +149 -0
  54. package/dist/channels/slack/context-buffer.d.ts +24 -0
  55. package/dist/channels/slack/context-buffer.js +37 -0
  56. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  57. package/dist/channels/slack/invoke-turn.js +63 -0
  58. package/dist/channels/slack/manifest.d.ts +49 -0
  59. package/dist/channels/slack/manifest.js +69 -0
  60. package/dist/channels/slack/model.d.ts +67 -0
  61. package/dist/channels/slack/model.js +2 -0
  62. package/dist/channels/slack/onboard.d.ts +41 -0
  63. package/dist/channels/slack/onboard.js +120 -0
  64. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  65. package/dist/channels/slack/onboarding-state.js +69 -0
  66. package/dist/channels/slack/owned-threads.d.ts +6 -0
  67. package/dist/channels/slack/owned-threads.js +43 -0
  68. package/dist/channels/slack/parse.d.ts +23 -0
  69. package/dist/channels/slack/parse.js +81 -0
  70. package/dist/channels/slack/preview.d.ts +24 -0
  71. package/dist/channels/slack/preview.js +359 -0
  72. package/dist/channels/slack/reaction.d.ts +24 -0
  73. package/dist/channels/slack/reaction.js +62 -0
  74. package/dist/channels/slack/register-webhook.d.ts +10 -0
  75. package/dist/channels/slack/register-webhook.js +49 -0
  76. package/dist/channels/slack/scaffold/channel.ts +33 -0
  77. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  78. package/dist/channels/slack/setup-server.d.ts +17 -0
  79. package/dist/channels/slack/setup-server.js +103 -0
  80. package/dist/channels/slack/slack-api.d.ts +77 -0
  81. package/dist/channels/slack/slack-api.js +415 -0
  82. package/dist/channels/slack/slack.d.ts +58 -0
  83. package/dist/channels/slack/slack.js +451 -0
  84. package/dist/channels/slack/welcomed.d.ts +5 -0
  85. package/dist/channels/slack/welcomed.js +32 -0
  86. package/dist/channels/state.js +3 -3
  87. package/dist/channels/stop-command.d.ts +6 -0
  88. package/dist/channels/stop-command.js +36 -0
  89. package/dist/channels/tasks.d.ts +13 -0
  90. package/dist/channels/tasks.js +10 -0
  91. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  92. package/dist/channels/telegram/context-buffer.js +6 -85
  93. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  94. package/dist/channels/telegram/invoke-turn.js +11 -58
  95. package/dist/channels/telegram/preview.d.ts +4 -7
  96. package/dist/channels/telegram/preview.js +24 -142
  97. package/dist/channels/telegram/telegram.js +23 -9
  98. package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
  99. package/dist/channels/{feishu/text.js → text.js} +1 -1
  100. package/dist/channels/turn-queue.js +1 -1
  101. package/dist/channels/turn-store.d.ts +1 -1
  102. package/dist/channels/turn-store.js +2 -3
  103. package/dist/cli/add-feishu.d.ts +27 -0
  104. package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
  105. package/dist/cli/add-slack.d.ts +10 -0
  106. package/dist/cli/add-slack.js +204 -0
  107. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  108. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  109. package/dist/cli/commands/add.d.ts +13 -0
  110. package/dist/cli/commands/add.js +274 -0
  111. package/dist/cli/commands/attach.d.ts +82 -0
  112. package/dist/cli/commands/attach.js +559 -0
  113. package/dist/cli/commands/chat.d.ts +4 -0
  114. package/dist/cli/commands/chat.js +21 -0
  115. package/dist/cli/commands/deploy.d.ts +15 -0
  116. package/dist/cli/commands/deploy.js +394 -0
  117. package/dist/cli/commands/dev.d.ts +11 -0
  118. package/dist/cli/commands/dev.js +82 -0
  119. package/dist/cli/commands/fire.d.ts +7 -0
  120. package/dist/cli/commands/fire.js +45 -0
  121. package/dist/cli/commands/info.d.ts +7 -0
  122. package/dist/cli/commands/info.js +108 -0
  123. package/dist/cli/commands/init.d.ts +8 -0
  124. package/dist/cli/commands/init.js +81 -0
  125. package/dist/cli/commands/invoke.d.ts +7 -0
  126. package/dist/cli/commands/invoke.js +28 -0
  127. package/dist/cli/commands/login.d.ts +6 -0
  128. package/dist/cli/commands/login.js +52 -0
  129. package/dist/cli/commands/models.d.ts +1 -0
  130. package/dist/cli/commands/models.js +15 -0
  131. package/dist/cli/commands/schedule.d.ts +12 -0
  132. package/dist/cli/commands/schedule.js +89 -0
  133. package/dist/cli/commands/start.d.ts +10 -0
  134. package/dist/cli/commands/start.js +97 -0
  135. package/dist/cli/commands/tool.d.ts +1 -0
  136. package/dist/cli/commands/tool.js +38 -0
  137. package/dist/cli/fail.d.ts +17 -0
  138. package/dist/cli/fail.js +32 -0
  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 +90 -0
  142. package/dist/cli/kernel.js +190 -0
  143. package/dist/cli/models-view.d.ts +21 -0
  144. package/dist/cli/models-view.js +66 -0
  145. package/dist/cli/program.d.ts +11 -0
  146. package/dist/cli/program.js +479 -0
  147. package/dist/cli/serve.d.ts +48 -0
  148. package/dist/cli/serve.js +248 -0
  149. package/dist/cli/shared.d.ts +44 -0
  150. package/dist/cli/shared.js +246 -0
  151. package/dist/cli.js +8 -1329
  152. package/dist/collect.d.ts +14 -3
  153. package/dist/collect.js +24 -0
  154. package/dist/core.d.ts +3 -1
  155. package/dist/core.js +2 -0
  156. package/dist/deploy/docker/plan.d.ts +45 -0
  157. package/dist/deploy/docker/plan.js +141 -0
  158. package/dist/deploy/docker/run.d.ts +40 -0
  159. package/dist/deploy/docker/run.js +126 -0
  160. package/dist/deploy/fly/plan.d.ts +3 -1
  161. package/dist/deploy/fly/plan.js +12 -7
  162. package/dist/deploy/fly/run.d.ts +5 -4
  163. package/dist/deploy/fly/run.js +16 -5
  164. package/dist/deploy/preflight.d.ts +10 -4
  165. package/dist/deploy/preflight.js +57 -12
  166. package/dist/deploy/railway/plan.d.ts +3 -1
  167. package/dist/deploy/railway/plan.js +12 -4
  168. package/dist/deploy/railway/run.d.ts +5 -4
  169. package/dist/deploy/railway/run.js +15 -5
  170. package/dist/deploy/runner.d.ts +6 -2
  171. package/dist/deploy/runner.js +1 -0
  172. package/dist/deploy/secrets.d.ts +2 -1
  173. package/dist/deploy/secrets.js +23 -3
  174. package/dist/dev-supervisor.d.ts +0 -2
  175. package/dist/dev-supervisor.js +7 -3
  176. package/dist/engines/pi/auth.js +160 -46
  177. package/dist/engines/pi/channel.d.ts +22 -16
  178. package/dist/engines/pi/channel.js +90 -60
  179. package/dist/engines/pi/chat.d.ts +4 -16
  180. package/dist/engines/pi/chat.js +8 -188
  181. package/dist/engines/pi/config.d.ts +23 -9
  182. package/dist/engines/pi/config.js +35 -5
  183. package/dist/engines/pi/create.d.ts +36 -7
  184. package/dist/engines/pi/create.js +63 -22
  185. package/dist/engines/pi/harness.d.ts +65 -1
  186. package/dist/engines/pi/harness.js +166 -2
  187. package/dist/engines/pi/invoke.d.ts +56 -3
  188. package/dist/engines/pi/invoke.js +340 -20
  189. package/dist/engines/pi/login.d.ts +11 -0
  190. package/dist/engines/pi/login.js +17 -5
  191. package/dist/engines/pi/models.d.ts +56 -10
  192. package/dist/engines/pi/models.js +61 -23
  193. package/dist/engines/pi/search-tools.d.ts +10 -0
  194. package/dist/engines/pi/search-tools.js +138 -0
  195. package/dist/engines/pi/session-builder.d.ts +16 -0
  196. package/dist/engines/pi/session-builder.js +308 -0
  197. package/dist/engines/pi/session-control.d.ts +50 -0
  198. package/dist/engines/pi/session-control.js +604 -0
  199. package/dist/engines/pi/sessions.d.ts +17 -2
  200. package/dist/engines/pi/sessions.js +9 -0
  201. package/dist/engines/pi/tool-context.d.ts +46 -11
  202. package/dist/engines/pi/tool-context.js +11 -9
  203. package/dist/engines/pi/tool.d.ts +35 -6
  204. package/dist/engines/pi/tool.js +47 -1
  205. package/dist/engines/pi/wake-tool.d.ts +0 -3
  206. package/dist/engines/pi/wake-tool.js +9 -7
  207. package/dist/engines/pi/workspace.d.ts +56 -1
  208. package/dist/engines/pi/workspace.js +75 -16
  209. package/dist/feishu.d.ts +1 -1
  210. package/dist/feishu.js +1 -1
  211. package/dist/host/node.d.ts +23 -6
  212. package/dist/host/node.js +5 -4
  213. package/dist/index.d.ts +1 -0
  214. package/dist/index.js +1 -0
  215. package/dist/lark.d.ts +1 -1
  216. package/dist/lark.js +1 -1
  217. package/dist/observe.js +3 -0
  218. package/dist/pi.d.ts +6 -4
  219. package/dist/pi.js +2 -1
  220. package/dist/scaffold/add-channel.d.ts +19 -6
  221. package/dist/scaffold/add-channel.js +127 -16
  222. package/dist/scaffold/templates/fastagent.config.mjs +5 -3
  223. package/dist/schedule/wakeups.d.ts +0 -3
  224. package/dist/schedule/wakeups.js +1 -1
  225. package/dist/session-remote.d.ts +53 -0
  226. package/dist/session-remote.js +336 -0
  227. package/dist/session.d.ts +265 -0
  228. package/dist/session.js +37 -0
  229. package/dist/slack.d.ts +2 -0
  230. package/dist/slack.js +2 -0
  231. package/dist/tunnel.d.ts +7 -4
  232. package/dist/tunnel.js +21 -10
  233. package/package.json +22 -7
  234. package/dist/channels/feishu/seen.d.ts +0 -5
  235. package/dist/channels/feishu/seen.js +0 -47
  236. package/dist/cli-add-feishu.d.ts +0 -8
  237. package/dist/cli-models.d.ts +0 -11
  238. package/dist/cli-models.js +0 -20
package/dist/pi.d.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  export { createPiAgent, createPiAgentFromDefinition, type CreatePiAgentFromDefinitionOptions, type CreatePiAgentOptions, } from "./engines/pi/create.ts";
2
- export { defineTool, loadTools, type DefineToolOptions, type ToolCollision, type ToolContext, } from "./engines/pi/tool.ts";
2
+ export { defineTool, loadTools, type DefineToolOptions, type FastagentTool, type ToolCollision, type ToolContext, } from "./engines/pi/tool.ts";
3
+ export type { ReadonlySessionManager, ToolActivation } from "./engines/pi/tool-context.ts";
3
4
  export { z } from "zod";
4
- export type { AgentTool, ExecutionEnv, Session, Skill, SkillDiagnostic } from "@earendil-works/pi-agent-core";
5
+ export type { AgentTool, ExecutionEnv, Session, SessionTreeEntry, Skill, SkillDiagnostic, } from "@earendil-works/pi-agent-core";
5
6
  export { loadChannels, type ChannelCollision } from "./engines/pi/channel.ts";
6
7
  export { createPiAgentFromWorkspace, type CreatePiAgentFromWorkspaceOptions, } from "./engines/pi/workspace.ts";
7
8
  export type { LoadedDefinition, SkillCollision } from "./engines/pi/definition.ts";
8
9
  export { defineConfig, listModels, resolveModel, type FastagentConfig } from "./engines/pi/config.ts";
9
- export { inProcessLease, type Lease, type Release } from "./engines/pi/invoke.ts";
10
+ export { inProcessLease, type Lease, type Release, type SessionObserver } from "./engines/pi/invoke.ts";
11
+ export { createPiSessionControl, type CreatePiSessionControlOptions, } from "./engines/pi/session-control.ts";
10
12
  export type { AnyModel } from "./engines/pi/harness.ts";
11
- export { inMemorySessionStore, jsonlSessionStore, type PiSessionStore } from "./engines/pi/sessions.ts";
13
+ export { inMemorySessionStore, jsonlSessionStore, type PiSessionReader, type PiSessionStore, } from "./engines/pi/sessions.ts";
12
14
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore, type FastagentAuthOptions } from "./engines/pi/auth.ts";
13
15
  export { createPiModels, probeAuthSource, type CreatePiModelsOptions } from "./engines/pi/models.ts";
14
16
  export type { Models } from "@earendil-works/pi-ai";
package/dist/pi.js CHANGED
@@ -6,7 +6,8 @@ export { loadChannels } from "./engines/pi/channel.js";
6
6
  export { createPiAgentFromWorkspace, } from "./engines/pi/workspace.js";
7
7
  export { defineConfig, listModels, resolveModel } from "./engines/pi/config.js";
8
8
  export { inProcessLease } from "./engines/pi/invoke.js";
9
- export { inMemorySessionStore, jsonlSessionStore } from "./engines/pi/sessions.js";
9
+ export { createPiSessionControl, } from "./engines/pi/session-control.js";
10
+ export { inMemorySessionStore, jsonlSessionStore, } from "./engines/pi/sessions.js";
10
11
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore } from "./engines/pi/auth.js";
11
12
  export { createPiModels, probeAuthSource } from "./engines/pi/models.js";
12
13
  export { createProvider } from "@earendil-works/pi-ai";
@@ -1,4 +1,14 @@
1
- export type ChannelKind = "github" | "telegram" | "feishu" | "lark";
1
+ import type { FeishuSubscriptionMode } from "../channels/feishu/setup-mode.ts";
2
+ export type ChannelKind = "github" | "telegram" | "slack" | "feishu" | "lark";
3
+ /** Group-visibility choice shared by the slack/feishu/lark onboarding flows. Each channel keeps its
4
+ * own channel-level type (`SlackGroupBehavior`, `FeishuGroupBehavior`) — this is the CLI-side value. */
5
+ export type GroupBehavior = "context" | "mentions";
6
+ /** A resolved group-behavior decision plus whether the author actually chose it (flag or prompt).
7
+ * A defaulted "context" (non-interactive, no flag) must never drive a sensitive-scope write. */
8
+ export interface GroupBehaviorChoice {
9
+ behavior: GroupBehavior;
10
+ explicit: boolean;
11
+ }
2
12
  /** An env var a scaffolded channel reads. `generate` = a random-string secret the CLI can pre-fill. */
3
13
  export interface ChannelEnv {
4
14
  name: string;
@@ -9,8 +19,8 @@ export interface ChannelEnv {
9
19
  }
10
20
  /** The channel kinds `fastagent add <kind>` can scaffold. */
11
21
  export declare const CHANNEL_KINDS: ChannelKind[];
12
- /** The env vars + next-step lines a scaffolded channel needs (for the CLI to print). */
13
- export declare function channelSetup(kind: ChannelKind): {
22
+ /** The mode-specific env vars + next-step lines a scaffolded channel needs. */
23
+ export declare function channelSetup(kind: ChannelKind, ingress?: FeishuSubscriptionMode, groupBehavior?: GroupBehavior): {
14
24
  env: ChannelEnv[];
15
25
  steps: string[];
16
26
  };
@@ -19,7 +29,7 @@ export declare function channelSetup(kind: ChannelKind): {
19
29
  * copies it to `.env` finds the vars already there. No-op when there is no `.env.example` or the block
20
30
  * is already present. Placeholders only — no real secret lands in the committable template.
21
31
  */
22
- export declare function appendChannelEnv(dir: string, kind: ChannelKind): Promise<boolean>;
32
+ export declare function appendChannelEnv(dir: string, kind: ChannelKind, ingress?: FeishuSubscriptionMode): Promise<boolean>;
23
33
  export interface DotEnvWriteResult {
24
34
  /** Generated secret vars written as active `KEY=value` lines. */
25
35
  written: string[];
@@ -34,14 +44,17 @@ export interface DotEnvWriteResult {
34
44
  * (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
35
45
  * ready to edit while no fake secret is committed to the user's mental model.
36
46
  */
37
- export declare function appendChannelDotEnv(dir: string, kind: ChannelKind, generated: Record<string, string>, overwrite?: readonly string[]): Promise<DotEnvWriteResult>;
47
+ export declare function appendChannelDotEnv(dir: string, kind: ChannelKind, generated: Record<string, string>, overwrite?: readonly string[], ingress?: FeishuSubscriptionMode): Promise<DotEnvWriteResult>;
38
48
  /** Whether a channel file already exists — checked before any mutation, so a no-clobber re-add is side-effect-free. */
39
49
  export declare function channelExists(dir: string, kind: ChannelKind): Promise<boolean>;
40
50
  /**
41
51
  * Scaffold `channels/<kind>.ts` into {@link dir}. Never clobbers an existing file (the glue is
42
52
  * authored content). The wx write is the TOCTOU safety net behind {@link channelExists}.
43
53
  */
44
- export declare function scaffoldChannel(dir: string, kind: ChannelKind): Promise<string>;
54
+ export declare function scaffoldChannel(dir: string, kind: ChannelKind, options?: {
55
+ ingress?: FeishuSubscriptionMode;
56
+ groupBehavior?: GroupBehavior;
57
+ }): Promise<string>;
45
58
  /**
46
59
  * Verify the workspace is ready to host a channel: an ESM package.json that declares
47
60
  * `@fastagent-sh/fastagent` (the channel file imports it). `add` checks and guides, never bootstraps — that
@@ -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,7 +100,8 @@ const CHANNEL_SCAFFOLDS = {
72
100
  },
73
101
  ],
74
102
  steps: [
75
- "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)",
76
105
  "edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
77
106
  "the event Request URL is auto-registered by `dev --tunnel` / `deploy --run`",
78
107
  "the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
@@ -94,7 +123,8 @@ const CHANNEL_SCAFFOLDS = {
94
123
  },
95
124
  ],
96
125
  steps: [
97
- "finish the console setup: enable Bot and add the permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
126
+ "finish the console setup: enable Bot and add the required permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
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",
98
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",
99
129
  "the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
100
130
  ],
@@ -102,17 +132,61 @@ const CHANNEL_SCAFFOLDS = {
102
132
  };
103
133
  /** The channel kinds `fastagent add <kind>` can scaffold. */
104
134
  export const CHANNEL_KINDS = Object.keys(CHANNEL_SCAFFOLDS);
105
- /** The env vars + next-step lines a scaffolded channel needs (for the CLI to print). */
106
- export function channelSetup(kind) {
107
- const { env, steps } = CHANNEL_SCAFFOLDS[kind];
108
- 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 };
109
183
  }
110
184
  /**
111
185
  * Append a channel's env vars (commented placeholders + hints) to `.env.example`, so a developer who
112
186
  * copies it to `.env` finds the vars already there. No-op when there is no `.env.example` or the block
113
187
  * is already present. Placeholders only — no real secret lands in the committable template.
114
188
  */
115
- export async function appendChannelEnv(dir, kind) {
189
+ export async function appendChannelEnv(dir, kind, ingress = "webhook") {
116
190
  const file = join(dir, ".env.example");
117
191
  let current;
118
192
  try {
@@ -129,7 +203,9 @@ export async function appendChannelEnv(dir, kind) {
129
203
  // Hint on its OWN line above the placeholder (like the base env.example template) — never inline
130
204
  // after `=`: loadEnvFile does not strip trailing comments, so an uncommented `KEY= # hint` (or a
131
205
  // value pasted before the `#`) would carry the hint text into the parsed value.
132
- 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`;
133
209
  await appendFile(file, block);
134
210
  return true;
135
211
  }
@@ -151,7 +227,7 @@ function mentionsEnvName(content, name) {
151
227
  * (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
152
228
  * ready to edit while no fake secret is committed to the user's mental model.
153
229
  */
154
- export async function appendChannelDotEnv(dir, kind, generated, overwrite = []) {
230
+ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
155
231
  const file = join(dir, ".env");
156
232
  let current = "";
157
233
  try {
@@ -161,14 +237,15 @@ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [])
161
237
  if (e.code !== "ENOENT")
162
238
  throw e;
163
239
  }
164
- const alreadySet = CHANNEL_SCAFFOLDS[kind].env
240
+ const env = channelSetup(kind, ingress).env;
241
+ const alreadySet = env
165
242
  .filter((e) => !overwrite.includes(e.name) && hasActiveEnvValue(current, e.name))
166
243
  .map((e) => e.name);
167
244
  const lines = [];
168
245
  const written = [];
169
246
  const contentLines = current.split("\n");
170
247
  let replacedInPlace = false;
171
- for (const e of CHANNEL_SCAFFOLDS[kind].env) {
248
+ for (const e of env) {
172
249
  if (alreadySet.includes(e.name))
173
250
  continue;
174
251
  const value = generated[e.name];
@@ -228,7 +305,7 @@ export async function channelExists(dir, kind) {
228
305
  * Scaffold `channels/<kind>.ts` into {@link dir}. Never clobbers an existing file (the glue is
229
306
  * authored content). The wx write is the TOCTOU safety net behind {@link channelExists}.
230
307
  */
231
- export async function scaffoldChannel(dir, kind) {
308
+ export async function scaffoldChannel(dir, kind, options = {}) {
232
309
  const channelsDir = join(dir, "channels");
233
310
  // Don't write through a channels/ symlink that escapes the workspace; an in-workspace one is fine.
234
311
  await assertInsideWorkspace(dir, "channels");
@@ -240,8 +317,42 @@ export async function scaffoldChannel(dir, kind) {
240
317
  // `channel.ts` is THE adapter (→ channels/<kind>.ts); any other .ts in the bundle is a companion tool
241
318
  // (→ tools/<name>, never clobbering an authored one).
242
319
  for (const name of channelBundleFiles(kind)) {
243
- const content = channelTemplate(kind, name);
320
+ let content = channelTemplate(kind, name);
244
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
+ }
245
356
  await writeFile(file, content, { flag: "wx" });
246
357
  continue;
247
358
  }
@@ -2,14 +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
+ // thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max); default "medium" (pi TUI parity)
10
11
  http: { port: 8787 },
11
12
  // selfSchedule: true, // mount the built-in `wake` tool: the agent schedules its own follow-up turns
12
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)
13
15
  // deploy: what the agent needs on the box (so `fastagent deploy` doesn't need a hand-written Dockerfile
14
16
  // or hand-set host variables). Uncomment as needed:
15
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;