@fastagent-sh/fastagent 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +3 -3
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/context-buffer.d.ts +30 -0
  5. package/dist/channels/context-buffer.js +98 -0
  6. package/dist/channels/control.d.ts +28 -0
  7. package/dist/channels/control.js +214 -0
  8. package/dist/channels/feishu/card.js +1 -1
  9. package/dist/channels/feishu/context-buffer.d.ts +8 -11
  10. package/dist/channels/feishu/context-buffer.js +6 -67
  11. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  12. package/dist/channels/feishu/feishu-api.js +20 -2
  13. package/dist/channels/feishu/feishu.d.ts +28 -16
  14. package/dist/channels/feishu/feishu.js +210 -188
  15. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  16. package/dist/channels/feishu/invoke-turn.js +10 -62
  17. package/dist/channels/feishu/model.d.ts +2 -1
  18. package/dist/channels/feishu/normalize.d.ts +2 -1
  19. package/dist/channels/feishu/owned-threads.d.ts +2 -6
  20. package/dist/channels/feishu/parse.d.ts +1 -1
  21. package/dist/channels/feishu/preview.d.ts +4 -7
  22. package/dist/channels/feishu/preview.js +24 -140
  23. package/dist/channels/feishu/register-app.d.ts +2 -1
  24. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  25. package/dist/channels/feishu/setup-mode.d.ts +8 -0
  26. package/dist/channels/feishu/setup-mode.js +2 -0
  27. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  28. package/dist/channels/feishu/ws-ingress.js +136 -0
  29. package/dist/channels/github/github.js +8 -6
  30. package/dist/channels/http.d.ts +14 -0
  31. package/dist/channels/http.js +35 -2
  32. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  33. package/dist/channels/invoke-turn-kit.js +87 -0
  34. package/dist/channels/lark/lark.d.ts +4 -2
  35. package/dist/channels/lark/lark.js +4 -1
  36. package/dist/channels/lark/onboard.d.ts +8 -4
  37. package/dist/channels/lark/onboard.js +8 -0
  38. package/dist/channels/preview-kit.d.ts +109 -0
  39. package/dist/channels/preview-kit.js +183 -0
  40. package/dist/channels/seen.d.ts +5 -0
  41. package/dist/channels/seen.js +35 -0
  42. package/dist/channels/slack/bot-auth.d.ts +15 -0
  43. package/dist/channels/slack/bot-auth.js +146 -0
  44. package/dist/channels/slack/config-api.d.ts +60 -0
  45. package/dist/channels/slack/config-api.js +149 -0
  46. package/dist/channels/slack/context-buffer.d.ts +24 -0
  47. package/dist/channels/slack/context-buffer.js +37 -0
  48. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  49. package/dist/channels/slack/invoke-turn.js +63 -0
  50. package/dist/channels/slack/manifest.d.ts +49 -0
  51. package/dist/channels/slack/manifest.js +69 -0
  52. package/dist/channels/slack/model.d.ts +67 -0
  53. package/dist/channels/slack/model.js +2 -0
  54. package/dist/channels/slack/onboard.d.ts +41 -0
  55. package/dist/channels/slack/onboard.js +120 -0
  56. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  57. package/dist/channels/slack/onboarding-state.js +69 -0
  58. package/dist/channels/slack/owned-threads.d.ts +6 -0
  59. package/dist/channels/slack/owned-threads.js +43 -0
  60. package/dist/channels/slack/parse.d.ts +23 -0
  61. package/dist/channels/slack/parse.js +81 -0
  62. package/dist/channels/slack/preview.d.ts +24 -0
  63. package/dist/channels/slack/preview.js +359 -0
  64. package/dist/channels/slack/reaction.d.ts +24 -0
  65. package/dist/channels/slack/reaction.js +62 -0
  66. package/dist/channels/slack/register-webhook.d.ts +10 -0
  67. package/dist/channels/slack/register-webhook.js +49 -0
  68. package/dist/channels/slack/scaffold/channel.ts +33 -0
  69. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  70. package/dist/channels/slack/setup-server.d.ts +17 -0
  71. package/dist/channels/slack/setup-server.js +103 -0
  72. package/dist/channels/slack/slack-api.d.ts +77 -0
  73. package/dist/channels/slack/slack-api.js +415 -0
  74. package/dist/channels/slack/slack.d.ts +58 -0
  75. package/dist/channels/slack/slack.js +451 -0
  76. package/dist/channels/slack/welcomed.d.ts +5 -0
  77. package/dist/channels/slack/welcomed.js +32 -0
  78. package/dist/channels/state.js +3 -3
  79. package/dist/channels/stop-command.d.ts +6 -0
  80. package/dist/channels/stop-command.js +36 -0
  81. package/dist/channels/tasks.d.ts +13 -0
  82. package/dist/channels/tasks.js +10 -0
  83. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  84. package/dist/channels/telegram/context-buffer.js +6 -85
  85. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  86. package/dist/channels/telegram/invoke-turn.js +11 -58
  87. package/dist/channels/telegram/preview.d.ts +4 -7
  88. package/dist/channels/telegram/preview.js +24 -142
  89. package/dist/channels/telegram/telegram.js +23 -9
  90. package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
  91. package/dist/channels/{feishu/text.js → text.js} +1 -1
  92. package/dist/channels/turn-queue.js +1 -1
  93. package/dist/channels/turn-store.d.ts +1 -1
  94. package/dist/channels/turn-store.js +2 -3
  95. package/dist/cli/add-feishu.d.ts +27 -0
  96. package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
  97. package/dist/cli/add-slack.d.ts +10 -0
  98. package/dist/cli/add-slack.js +204 -0
  99. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  100. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  101. package/dist/cli/commands/add.d.ts +4 -0
  102. package/dist/cli/commands/add.js +152 -20
  103. package/dist/cli/commands/attach.d.ts +82 -0
  104. package/dist/cli/commands/attach.js +559 -0
  105. package/dist/cli/commands/chat.d.ts +1 -0
  106. package/dist/cli/commands/chat.js +6 -1
  107. package/dist/cli/commands/deploy.d.ts +2 -0
  108. package/dist/cli/commands/deploy.js +78 -22
  109. package/dist/cli/commands/dev.js +11 -5
  110. package/dist/cli/commands/fire.js +3 -3
  111. package/dist/cli/commands/info.js +2 -2
  112. package/dist/cli/commands/invoke.js +2 -3
  113. package/dist/cli/commands/login.js +14 -25
  114. package/dist/cli/commands/models.js +1 -1
  115. package/dist/cli/commands/start.js +15 -8
  116. package/dist/cli/commands/tool.js +2 -1
  117. package/dist/cli/fail.d.ts +0 -2
  118. package/dist/cli/fail.js +1 -1
  119. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  120. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  121. package/dist/cli/kernel.d.ts +3 -2
  122. package/dist/cli/models-view.d.ts +21 -0
  123. package/dist/cli/models-view.js +66 -0
  124. package/dist/cli/program.js +79 -21
  125. package/dist/cli/serve.d.ts +38 -18
  126. package/dist/cli/serve.js +197 -39
  127. package/dist/cli/shared.d.ts +26 -6
  128. package/dist/cli/shared.js +163 -33
  129. package/dist/collect.d.ts +14 -3
  130. package/dist/collect.js +24 -0
  131. package/dist/core.d.ts +3 -1
  132. package/dist/core.js +2 -0
  133. package/dist/deploy/docker/plan.d.ts +2 -2
  134. package/dist/deploy/docker/plan.js +8 -6
  135. package/dist/deploy/fly/plan.d.ts +3 -1
  136. package/dist/deploy/fly/plan.js +12 -7
  137. package/dist/deploy/fly/run.d.ts +5 -4
  138. package/dist/deploy/fly/run.js +16 -5
  139. package/dist/deploy/preflight.d.ts +10 -4
  140. package/dist/deploy/preflight.js +53 -9
  141. package/dist/deploy/railway/plan.d.ts +3 -1
  142. package/dist/deploy/railway/plan.js +12 -4
  143. package/dist/deploy/railway/run.d.ts +5 -4
  144. package/dist/deploy/railway/run.js +15 -5
  145. package/dist/deploy/runner.d.ts +2 -1
  146. package/dist/deploy/secrets.d.ts +2 -1
  147. package/dist/deploy/secrets.js +23 -3
  148. package/dist/dev-supervisor.d.ts +0 -2
  149. package/dist/dev-supervisor.js +7 -3
  150. package/dist/engines/pi/channel.d.ts +22 -16
  151. package/dist/engines/pi/channel.js +90 -60
  152. package/dist/engines/pi/chat.d.ts +4 -16
  153. package/dist/engines/pi/chat.js +8 -261
  154. package/dist/engines/pi/config.d.ts +14 -9
  155. package/dist/engines/pi/config.js +33 -18
  156. package/dist/engines/pi/create.d.ts +22 -2
  157. package/dist/engines/pi/create.js +20 -14
  158. package/dist/engines/pi/harness.d.ts +49 -0
  159. package/dist/engines/pi/harness.js +90 -2
  160. package/dist/engines/pi/invoke.d.ts +55 -2
  161. package/dist/engines/pi/invoke.js +304 -19
  162. package/dist/engines/pi/login.d.ts +11 -0
  163. package/dist/engines/pi/login.js +16 -4
  164. package/dist/engines/pi/models.d.ts +56 -10
  165. package/dist/engines/pi/models.js +61 -23
  166. package/dist/engines/pi/session-builder.d.ts +16 -0
  167. package/dist/engines/pi/session-builder.js +308 -0
  168. package/dist/engines/pi/session-control.d.ts +50 -0
  169. package/dist/engines/pi/session-control.js +604 -0
  170. package/dist/engines/pi/sessions.d.ts +17 -2
  171. package/dist/engines/pi/sessions.js +9 -0
  172. package/dist/engines/pi/tool-context.d.ts +18 -11
  173. package/dist/engines/pi/tool-context.js +3 -9
  174. package/dist/engines/pi/tool.d.ts +6 -8
  175. package/dist/engines/pi/tool.js +6 -1
  176. package/dist/engines/pi/wake-tool.d.ts +0 -3
  177. package/dist/engines/pi/wake-tool.js +9 -7
  178. package/dist/engines/pi/workspace.d.ts +52 -0
  179. package/dist/engines/pi/workspace.js +72 -15
  180. package/dist/feishu.d.ts +1 -1
  181. package/dist/feishu.js +1 -1
  182. package/dist/host/node.d.ts +23 -6
  183. package/dist/host/node.js +5 -4
  184. package/dist/index.d.ts +1 -0
  185. package/dist/index.js +1 -0
  186. package/dist/lark.d.ts +1 -1
  187. package/dist/lark.js +1 -1
  188. package/dist/observe.js +3 -0
  189. package/dist/pi.d.ts +5 -4
  190. package/dist/pi.js +2 -1
  191. package/dist/scaffold/add-channel.d.ts +19 -6
  192. package/dist/scaffold/add-channel.js +126 -17
  193. package/dist/scaffold/templates/fastagent.config.mjs +4 -3
  194. package/dist/schedule/wakeups.d.ts +0 -3
  195. package/dist/schedule/wakeups.js +1 -1
  196. package/dist/session-remote.d.ts +53 -0
  197. package/dist/session-remote.js +336 -0
  198. package/dist/session.d.ts +265 -0
  199. package/dist/session.js +37 -0
  200. package/dist/slack.d.ts +2 -0
  201. package/dist/slack.js +2 -0
  202. package/dist/tunnel.d.ts +7 -4
  203. package/dist/tunnel.js +21 -10
  204. package/package.json +19 -7
  205. package/dist/channels/feishu/seen.d.ts +0 -5
  206. package/dist/channels/feishu/seen.js +0 -47
  207. package/dist/cli-add-feishu.d.ts +0 -8
  208. package/dist/cli-models.d.ts +0 -11
  209. package/dist/cli-models.js +0 -20
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Helpers shared across command modules: interactivity gates, port parsing, the startup auth report,
3
- * and first-run model resolution. Bodies moved verbatim from cli.ts; the module-scoped flag access
4
- * (`values.*`) became parameters.
3
+ * first-run model resolution, and the login terminal IO. Bodies moved verbatim from cli.ts; the
4
+ * module-scoped flag access (`values.*`) became parameters.
5
5
  */
6
6
  import { readFile, writeFile } from "node:fs/promises";
7
7
  import { relative } from "node:path";
8
- import { autocomplete, isCancel, select } from "@clack/prompts";
8
+ import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
9
+ import { buildModelPickerOptions } from "./models-view.js";
9
10
  import { fastagentCredentialStore } from "../engines/pi/auth.js";
10
- import { isValidPort, loadConfig, resolveAuthPath, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
11
- import { configuredModelSpecs, createPiModels, probeAuthSource } from "../engines/pi/models.js";
12
- import { formatAuthReport } from "../cli-auth.js";
11
+ import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec, resolveStateRoot, rewriteConfigModel, } from "../engines/pi/config.js";
12
+ import { ensureStateRootSelfIgnored, isUnderDir } from "../engines/pi/definition.js";
13
+ import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
14
+ import { createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses } from "../engines/pi/models.js";
15
+ import { formatAuthReport } from "./auth-view.js";
13
16
  import { log } from "../log.js";
17
+ import { openExternalUrl } from "../open-url.js";
14
18
  import { failStartup, failUsage } from "./fail.js";
15
19
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
16
20
  export function isInteractive() {
@@ -38,7 +42,7 @@ export function parsePort(value, source, from) {
38
42
  }
39
43
  /** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
40
44
  export async function reportAuth(modelSpec, authPath) {
41
- const provider = modelSpec.slice(0, modelSpec.indexOf("/"));
45
+ const provider = providerOf(modelSpec);
42
46
  const source = await probeAuthSource(createPiModels({ authPath }), modelSpec);
43
47
  // Only when nothing satisfies auth do we read the store (refresh-FREE) to tell "nothing stored" from
44
48
  // "stored but unusable" — see formatAuthReport for why. store.read warns on a corrupt file itself.
@@ -53,12 +57,15 @@ export async function reportAuth(modelSpec, authPath) {
53
57
  log.warn(`[fastagent] ${report.warn}`);
54
58
  }
55
59
  /**
56
- * First-run model resolution for the serving commands. When no model is set (flag/env/config), and
57
- * we're on a TTY, pick one from the providers the user is logged into and persist the choice. A no-op
58
- * when a model is already set; on a non-TTY (CI/deploy), or with `--no-input`, or with nothing
59
- * configured it stays silent and lets the opener raise its clear "missing model" error. The pick is
60
- * exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it, and best-effort written back to
61
- * the config so the next run is quiet.
60
+ * First-run model resolution for every assembly command (dev/start/invoke/fire/chat/deploy): ONE
61
+ * funnel, no dead ends. When no model is set (flag/env/config) and we're on a TTY, show the FULL
62
+ * catalog annotated per provider ready (with the credential source, so which account pays is
63
+ * visible at the decision point) or login-required and, when the choice needs auth, run the login
64
+ * flow INLINE instead of exiting with "run `fastagent login` and come back". A no-op when a model is
65
+ * already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
66
+ * it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
67
+ * model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
68
+ * and best-effort written back to the config so the next run is quiet.
62
69
  */
63
70
  export async function resolveFirstRunModel(workspaceDir, options = {}) {
64
71
  const { config, path: configPath } = await loadConfig(workspaceDir).catch(failStartup);
@@ -69,38 +76,161 @@ export async function resolveFirstRunModel(workspaceDir, options = {}) {
69
76
  if (!isInteractive())
70
77
  return; // CI/deploy: the opener throws the actionable missing-model error
71
78
  const authPath = resolveAuthPath(workspaceDir, options.authPath);
72
- let specs;
79
+ const models = createPiModels({ authPath });
80
+ const chosen = await pickWithCredentials(workspaceDir, models, authPath);
81
+ if (chosen === undefined)
82
+ return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
83
+ process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
84
+ await persistModelChoice(workspaceDir, configPath, chosen);
85
+ }
86
+ /**
87
+ * The credential-aware pick: full catalog annotated per provider, then the post-pick auth policy —
88
+ * remedy warnings for providers no login flow can fix (the choice is KEPT: model validity is
89
+ * independent of credentials), or the inline login for the rest. Returns the chosen spec, or
90
+ * undefined when the pick should be discarded (picker cancel, login cancel, a failed auth probe).
91
+ */
92
+ async function pickWithCredentials(workspaceDir, models, authPath) {
93
+ let statuses;
73
94
  try {
74
- specs = await configuredModelSpecs(createPiModels({ authPath }));
95
+ statuses = await providerAuthStatuses(models);
75
96
  }
76
97
  catch (error) {
77
- // Enumerating providers/auth threw a system fault (a corrupt auth store, a throwing provider),
78
- // NOT "not logged in". Surface it instead of masking it as the login hint; the opener then still
79
- // raises the clear missing-model error.
80
- log.warn(`[fastagent] could not list configured models: ${error.message}`);
81
- return;
82
- }
83
- if (specs.length === 0) {
84
- log.warn(`[fastagent] no model set and no authenticated provider — run \`fastagent login\`, then \`fastagent dev\``);
85
- return;
98
+ // Per-provider auth throws are captured as `broken` INSIDE providerAuthStatuses; reaching here
99
+ // means the enumeration itself failed (getProviders / a provider with no probe-able surface) a
100
+ // system fault. Surface it; the opener then still raises the clear missing-model error.
101
+ log.warn(`[fastagent] could not probe provider auth: ${error.message}`);
102
+ return undefined;
86
103
  }
87
- const r = await (specs.length > 7 ? autocomplete : select)({
104
+ const r = await autocomplete({
88
105
  message: "Choose a model for this agent",
89
- options: specs.map((s) => ({ value: s, label: s })),
106
+ options: buildModelPickerOptions(listModels(models), statuses),
90
107
  });
91
108
  if (isCancel(r))
92
- return; // cancelled: let the opener report the missing model
109
+ return undefined; // cancelled: the caller raises its clear missing-model error
93
110
  const chosen = r;
94
- process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
95
- await persistModelChoice(workspaceDir, configPath, chosen);
111
+ const provider = providerOf(chosen);
112
+ const status = statuses.get(provider);
113
+ if (status?.state === "ready")
114
+ return chosen; // usable now — nothing to fix
115
+ if (status && status.login === "none") {
116
+ // No login flow exists for this provider — KEEP the choice and name the remedy. The remedy depends
117
+ // on WHY it is not ready: a BROKEN stored credential still owns the provider (env is consulted
118
+ // only when nothing is stored — createPiModels), so "set the env var" would not help there.
119
+ if (status.state === "broken") {
120
+ log.warn(`[fastagent] stored auth for "${provider}" is unusable: ${status.message} — fix or remove it in ${authPath}; invokes fail until then`);
121
+ }
122
+ else {
123
+ log.warn(`[fastagent] "${provider}" has no interactive login — set its API key env var; invokes fail until then`);
124
+ }
125
+ return chosen;
126
+ }
127
+ // Inline login. Same leak guard as `login`: self-ignore the state root BEFORE a credential
128
+ // can land in-tree, so the secret is never untracked-but-committable.
129
+ const stateRoot = resolveStateRoot(workspaceDir);
130
+ if (isUnderDir(authPath, stateRoot))
131
+ await ensureStateRootSelfIgnored(workspaceDir, stateRoot);
132
+ try {
133
+ // Verified against the CHOSEN model — the exact request the agent is about to make; a rejected
134
+ // key re-prompts inside the loop, so reaching here means a usable (or at worst unverifiable) key.
135
+ await loginWithKeyCheck(provider, authPath, chosen);
136
+ console.error(`[fastagent] logged in to ${provider} — saved to ${authPath}`);
137
+ }
138
+ catch (error) {
139
+ if (error instanceof LoginCancelled)
140
+ return undefined; // user backed out — discard the choice, like a picker cancel
141
+ // A FAILED login keeps the choice: the pick persists, the startup auth report names the remedy,
142
+ // and a later `fastagent login` fixes auth without re-picking the model.
143
+ log.warn(`[fastagent] login for "${provider}" failed: ${error.message} — model saved; run \`fastagent login\` to fix auth`);
144
+ }
145
+ return chosen;
146
+ }
147
+ /**
148
+ * Interactive login with the api_key quick-fail probe closed into a LOOP: a definitively rejected key
149
+ * (HTTP 401) deletes the bad credential and RE-PROMPTS immediately — the user's hands are on the
150
+ * keyboard NOW; parking the failure for a later `fastagent login` would waste that. The loop exits on
151
+ * a verified/unverifiable key (kept), an OAuth login (completing the flow already proved the
152
+ * credential), or cancel (LoginCancelled propagates to the caller's cancel policy). Used by both the
153
+ * `login` command and the first-run picker's inline login.
154
+ */
155
+ export async function loginWithKeyCheck(provider, authPath, spec,
156
+ // Test seams: this loop DESTROYS credential state on `rejected`, so its policy (rejected → delete →
157
+ // re-ask ONLY the key) is pinned by a test through fake flow/verify; production callers omit both.
158
+ seams = {}) {
159
+ const flow = seams.flow ?? loginFlow;
160
+ const verify = seams.verify ?? verifyApiKeyLogin;
161
+ const io = terminalLoginIO();
162
+ let method;
163
+ for (;;) {
164
+ const result = await flow(io, { provider, authPath, method });
165
+ if (result.method !== "api_key")
166
+ return result;
167
+ const verdict = await verify(result.provider, authPath, spec);
168
+ if (verdict !== "rejected")
169
+ return result;
170
+ // Retry re-asks ONLY the key: the provider/method choices weren't the mistake, the keystrokes were.
171
+ provider = result.provider;
172
+ method = "api_key";
173
+ }
174
+ }
175
+ /**
176
+ * Quick-fail check after an api_key login (OAuth needs none — completing the flow already proved the
177
+ * credential): probe the stored key with one minimal request against `spec`, or the provider's first
178
+ * model. Policy over {@link probeApiKey}'s verdict: `rejected` (definitive HTTP 401) DELETES the
179
+ * just-stored credential — a mistyped key must not persist as plausible state — and the caller
180
+ * ({@link loginWithKeyCheck}) re-prompts; `unknown` (network, quota, permissions) keeps it and prints
181
+ * the provider's message: the key may still be right, and wrongly destroying a good credential costs
182
+ * more than keeping a doubtful one.
183
+ */
184
+ async function verifyApiKeyLogin(provider, authPath, spec) {
185
+ const models = createPiModels({ authPath });
186
+ const model = spec ? resolveModel(models, spec) : models.getProvider(provider)?.getModels()[0];
187
+ if (!model) {
188
+ console.error(`[fastagent] cannot verify the key: provider "${provider}" lists no models — kept as stored`);
189
+ return "unknown";
190
+ }
191
+ const label = `${model.provider}/${model.id}`;
192
+ console.error(`[fastagent] verifying the key with ${label}…`);
193
+ const probe = await probeApiKey(models, model);
194
+ if (probe.state === "ok") {
195
+ console.error(`[fastagent] key verified — ${label} responded`);
196
+ }
197
+ else if (probe.state === "rejected") {
198
+ await fastagentCredentialStore(authPath).delete(provider);
199
+ console.error(`[fastagent] ${provider} rejected the API key (HTTP 401): ${probe.message} — enter it again (or cancel)`);
200
+ }
201
+ else {
202
+ console.error(`[fastagent] could not verify the key with ${label}: ${probe.message} — kept; invokes surface the provider's error`);
203
+ }
204
+ return probe.state;
205
+ }
206
+ /** Login terminal IO via @clack/prompts: a searchable list once long, a hidden prompt for keys. Shared
207
+ * by the `login` command and the first-run picker's inline login. */
208
+ function terminalLoginIO() {
209
+ return {
210
+ async select(message, options) {
211
+ const r = await (options.length > 7 ? autocomplete : select)({ message, options });
212
+ return isCancel(r) ? undefined : r;
213
+ },
214
+ async prompt(message, opts) {
215
+ const r = opts?.hidden
216
+ ? await password({ message, signal: opts.signal })
217
+ : await clackText({ message, signal: opts?.signal });
218
+ return isCancel(r) ? undefined : r;
219
+ },
220
+ note: (message) => clackLog.info(message),
221
+ openUrl: openExternalUrl,
222
+ };
96
223
  }
97
224
  /**
98
- * Best-effort persist the picked model so the next run does not prompt. Only rewrites the commented
99
- * `model:` placeholder the scaffold writes (or an existing `model:` line); anything else (zero-config,
100
- * a hand-shaped config) is left untouched with a printed hint. Never throws — persistence is a convenience.
225
+ * Best-effort persist the picked model so the next run does not prompt. Rewrites the commented
226
+ * `model:` placeholder the scaffold writes / an existing `model:` line, or re-inserts the line into a
227
+ * scaffold-shaped config (the hand-deleted-to-reset case); anything else (zero-config, a hand-shaped
228
+ * config) is left untouched with a printed hint. Never throws — persistence is a convenience.
101
229
  */
102
230
  async function persistModelChoice(workspaceDir, configPath, spec) {
103
- const hint = () => console.error(`[fastagent] using ${spec} for this run; set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
231
+ const hint = () => console.error(
232
+ // No "using it for this run" promise: deploy's model-travel gate rightly ignores the un-persisted pick.
233
+ `[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
104
234
  if (!configPath)
105
235
  return hint();
106
236
  try {
package/dist/collect.d.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  /**
2
- * Buffered consumption helper (caller-side, SPEC §7): reduce an AgentEvent stream to a final value,
3
- * encoding the terminal discipline (failed → throw, missing terminal → error). Streaming consumers
4
- * for-await themselves.
2
+ * Caller-side stream helpers: `collect` (buffered consumption, SPEC §7) reduces an AgentEvent
3
+ * stream to a final value, encoding the terminal discipline (failed → throw, missing terminal →
4
+ * error) — streaming consumers for-await themselves. `abortFirstIterator` is the shared
5
+ * cancellation protocol for generator-backed streams.
5
6
  */
6
7
  import type { AgentEvent, Json } from "./agent.ts";
8
+ /**
9
+ * The abort-first cancellation protocol, ONCE: an async generator suspended on a quiet await (a
10
+ * tool mid-execution, a silent SSE read) parks inside that await, and `gen.return()`/`gen.throw()`
11
+ * queue behind the pending `next()` FOREVER (async-generator semantics) — a consumer's cancel
12
+ * would deadlock. The wrapper's `return()` first runs `cancel` (abort the underlying work, which
13
+ * settles the suspension), then delegates to `gen.return`, swallowing its rejection (the
14
+ * generator's own catch/finally already surfaced the outcome). `throw()` tears down identically
15
+ * and rethrows the caller's error deterministically instead of poking a completed generator.
16
+ */
17
+ export declare function abortFirstIterator<T>(gen: AsyncGenerator<T>, cancel: () => void): AsyncIterator<T>;
7
18
  /** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
8
19
  * a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
9
20
  export declare class AgentFailure extends Error {
package/dist/collect.js CHANGED
@@ -1,3 +1,27 @@
1
+ /**
2
+ * The abort-first cancellation protocol, ONCE: an async generator suspended on a quiet await (a
3
+ * tool mid-execution, a silent SSE read) parks inside that await, and `gen.return()`/`gen.throw()`
4
+ * queue behind the pending `next()` FOREVER (async-generator semantics) — a consumer's cancel
5
+ * would deadlock. The wrapper's `return()` first runs `cancel` (abort the underlying work, which
6
+ * settles the suspension), then delegates to `gen.return`, swallowing its rejection (the
7
+ * generator's own catch/finally already surfaced the outcome). `throw()` tears down identically
8
+ * and rethrows the caller's error deterministically instead of poking a completed generator.
9
+ */
10
+ export function abortFirstIterator(gen, cancel) {
11
+ return {
12
+ next: () => gen.next(),
13
+ async return(value) {
14
+ cancel();
15
+ await gen.return(value).catch(() => { });
16
+ return { done: true, value: undefined };
17
+ },
18
+ async throw(error) {
19
+ cancel();
20
+ await gen.return(undefined).catch(() => { });
21
+ throw error;
22
+ },
23
+ };
24
+ }
1
25
  /** Exception form of a failed event (thrown by collect). Carries the failed event's fields verbatim, so
2
26
  * a buffered consumer can branch on `code` (SPEC §8 failure subdivision) just like a streaming one. */
3
27
  export class AgentFailure extends Error {
package/dist/core.d.ts CHANGED
@@ -2,9 +2,11 @@ export type { Agent, AgentEvent, ImageRef, Json, Prompt, Scope } from "./agent.t
2
2
  export { collect, AgentFailure, type CollectResult } from "./collect.ts";
3
3
  export type { ModuleLoadFailure } from "./loader.ts";
4
4
  export { createInvokeHandler, nodeListener } from "./channels/http.ts";
5
+ export { controlRoutes, type ControlRoutesOptions, type WireEvent } from "./channels/control.ts";
6
+ export { ControlRequestError, connectAgent, connectSessionControl, type RemoteEndpointOptions, } from "./session-remote.ts";
5
7
  export { readBodyCapped } from "./channels/body.ts";
6
8
  export { text, textHeaders } from "./channels/respond.ts";
7
- export { type ChannelContext, type ChannelHandler, type ChannelModule, type Routes, router, serveNode, } from "./host/node.ts";
9
+ export { type ChannelContext, type ChannelHandler, type ChannelModule, type LongConnection, type LongConnectionChannelModule, type Routes, router, serveNode, } from "./host/node.ts";
8
10
  export { defineSchedule, type LoadedSchedule, type Schedule } from "./schedule/schedule.ts";
9
11
  export { discoverScheduleFiles, loadSchedules } from "./schedule/discover.ts";
10
12
  export { createScheduler, scheduleSession, type Scheduler, type SchedulerOptions } from "./schedule/scheduler.ts";
package/dist/core.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { collect, AgentFailure } from "./collect.js";
2
2
  export { createInvokeHandler, nodeListener } from "./channels/http.js";
3
+ export { controlRoutes } from "./channels/control.js";
4
+ export { ControlRequestError, connectAgent, connectSessionControl, } from "./session-remote.js";
3
5
  export { readBodyCapped } from "./channels/body.js";
4
6
  export { text, textHeaders } from "./channels/respond.js";
5
7
  export { router, serveNode, } from "./host/node.js";
@@ -15,6 +15,8 @@ export interface DockerPlanInput extends ContainerInput {
15
15
  modelAuth: string | undefined;
16
16
  /** Known channels contribute their environment-variable names and webhook registration. */
17
17
  channels: ChannelKind[];
18
+ /** All long-connection channel basenames, including custom channels. */
19
+ longConnectionChannels?: string[];
18
20
  /** Generate an optional Cloudflare Quick Tunnel service in Compose. Generation only; `--run` starts it. */
19
21
  tunnel: boolean;
20
22
  /** Extra environment-variable names declared in config.deploy.secrets. */
@@ -28,8 +30,6 @@ export interface DockerPlan {
28
30
  /** Ordered local build/run/operate instructions. */
29
31
  runbook: string[];
30
32
  }
31
- export declare const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
32
- export declare const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
33
33
  export declare const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
34
34
  /** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
35
35
  export declare const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
@@ -1,8 +1,8 @@
1
1
  import { containerArtifacts } from "../container.js";
2
2
  import { deploymentSecrets, isEnvKey } from "../secrets.js";
3
3
  const MOUNT = "/data";
4
- export const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
5
- export const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
4
+ const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
5
+ const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
6
6
  export const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
7
7
  /** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
8
8
  export const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
@@ -29,6 +29,7 @@ export function dockerWebhookPaths(channels) {
29
29
  const path = {
30
30
  github: "/webhook",
31
31
  telegram: "/telegram",
32
+ slack: "/slack",
32
33
  feishu: "/feishu",
33
34
  lark: "/lark",
34
35
  };
@@ -48,7 +49,7 @@ function buildContext(kitDir) {
48
49
  .join("/");
49
50
  }
50
51
  function composeYaml(input) {
51
- const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
52
+ const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
52
53
  // Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
53
54
  // it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
54
55
  const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
@@ -104,10 +105,11 @@ export function planDockerDeploy(input) {
104
105
  const composePath = input.kitDir ? `${input.kitDir}/${DOCKER_COMPOSE_FILE}` : DOCKER_COMPOSE_FILE;
105
106
  const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
106
107
  const compose = `docker compose -f ${composePath}`;
107
- const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
108
+ const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
108
109
  const required = secrets.filter((secret) => secret.required);
109
110
  const optional = secrets.filter((secret) => !secret.required);
110
- const paths = dockerWebhookPaths(input.channels);
111
+ const routeChannels = input.channels.filter((kind) => !input.longConnectionChannels?.includes(kind));
112
+ const paths = dockerWebhookPaths(routeChannels);
111
113
  const runbook = [
112
114
  `# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
113
115
  `# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
@@ -129,7 +131,7 @@ export function planDockerDeploy(input) {
129
131
  }
130
132
  runbook.push(``, `# Build/create/reconcile the service. Re-running this is the complete local redeploy; state stays`, `# in the Compose volume mounted at ${MOUNT}.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: also deletes auth, sessions, and channel state`);
131
133
  if (input.tunnel) {
132
- runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Detected Telegram/Feishu/Lark channels auto-register; GitHub prints the URL. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
134
+ runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register; GitHub/manual Slack print console URLs. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
133
135
  }
134
136
  else if (paths.length > 0) {
135
137
  runbook.push(``, `# Public ingress is operator-owned: add your tunnel/proxy/DNS/TLS, then configure the`, `# detected default endpoint(s): ${paths.map((path) => `https://<your-domain>${path}`).join(", ")}.`, `# If your channels/*.ts glue remaps a route, use that path instead.`);
@@ -28,8 +28,10 @@ export interface FlyPlanInput extends ContainerInput {
28
28
  * `"OAuth"`/`"stored credential"` (a local login the server can't use), or undefined (unconfigured).
29
29
  */
30
30
  modelAuth: string | undefined;
31
- /** Channels discovered in the workspace — each contributes its secret metadata + webhook step. */
31
+ /** Known first-party channels — each contributes its secret metadata + webhook step. */
32
32
  channels: ChannelKind[];
33
+ /** All long-connection channel basenames, including custom channels — require one running machine. */
34
+ longConnectionChannels?: string[];
33
35
  /** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
34
36
  extraSecrets?: string[];
35
37
  /** `auto_stop_machines` — `"suspend"` (default, fast resume) or `"stop"` (cold start). CLI `--stop`. */
@@ -1,6 +1,6 @@
1
1
  import { containerArtifacts } from "../container.js";
2
2
  import { deploymentSecrets, isEnvKey } from "../secrets.js";
3
- function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers) {
3
+ function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers, hasLongConnectionChannel) {
4
4
  // min_machines_running: 1 (keep one up) when a github channel is present, TIME triggers exist, OR the
5
5
  // operator opted out of scale-to-zero. GitHub's is a SAFETY default — its fire-and-forget turns have no
6
6
  // replay, so scaling to zero could drop an in-flight review. Time triggers (schedules/wake) have no
@@ -10,9 +10,11 @@ function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTrigger
10
10
  ? ` min_machines_running = 1 # github turns have no replay — don't scale to zero (an in-flight review would be lost)`
11
11
  : hasTimeTriggers
12
12
  ? ` min_machines_running = 1 # schedules/wake-ups need a running machine (no external wake-up for a cron instant)`
13
- : !scaleToZero
14
- ? ` min_machines_running = 1 # kept running (--no-scale-to-zero)`
15
- : ` min_machines_running = 0 # scale to zero`;
13
+ : hasLongConnectionChannel
14
+ ? ` min_machines_running = 1 # long-connection channel needs a running machine (cannot wake from zero)`
15
+ : !scaleToZero
16
+ ? ` min_machines_running = 1 # kept running (--no-scale-to-zero)`
17
+ : ` min_machines_running = 0 # scale to zero`;
16
18
  const stopLine = autostop === "stop"
17
19
  ? ` auto_stop_machines = "stop" # stop on idle (cold start on the next webhook)`
18
20
  : ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
@@ -52,7 +54,7 @@ export function planFlyDeploy(input) {
52
54
  const artifacts = [
53
55
  {
54
56
  path: flyTomlPath,
55
- content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers),
57
+ content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers, (input.longConnectionChannels?.length ?? 0) > 0),
56
58
  },
57
59
  ...containerArtifacts(input),
58
60
  ];
@@ -60,7 +62,7 @@ export function planFlyDeploy(input) {
60
62
  // model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
61
63
  // COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
62
64
  // `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
63
- const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
65
+ const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
64
66
  const requiredSecrets = secrets.filter((secret) => secret.required);
65
67
  const optionalSecrets = secrets.filter((secret) => !secret.required);
66
68
  const deployCmd = kitDir
@@ -111,8 +113,11 @@ export function planFlyDeploy(input) {
111
113
  if (channels.includes("github")) {
112
114
  post.push(`# After deploy — set the GitHub webhook (repo Settings → Webhooks). Path assumes the default route`, `# (POST /webhook); if you remapped it in channels/github.ts, use your path:`, `# Payload URL = https://${appName}.fly.dev/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
113
115
  }
116
+ if (channels.includes("slack")) {
117
+ post.push(`# After deploy — set Slack Event Subscriptions → Request URL. Path assumes POST /slack;`, `# Slack verifies the running endpoint with a challenge:`, `# Request URL = https://${appName}.fly.dev/slack`, `# Ensure OAuth scopes + message.* subscriptions match the groupBehavior in channels/slack.ts.`);
118
+ }
114
119
  for (const kind of ["feishu", "lark"]) {
115
- if (!channels.includes(kind))
120
+ if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
116
121
  continue;
117
122
  const label = kind === "feishu" ? "Feishu" : "Lark";
118
123
  post.push(`# After deploy — set the ${label} event Request URL (developer console → Events & Callbacks).`, `# Path assumes the default route (POST /${kind}); the app must be RUNNING when you save (the console`, `# verifies the URL with a challenge):`, `# Request URL = https://${appName}.fly.dev/${kind}`);
@@ -35,6 +35,7 @@ export interface FlyRunPlan {
35
35
  /** Required secret names with NO local value — the run gates on these before any side effect. */
36
36
  missingSecrets: string[];
37
37
  channels: ChannelKind[];
38
+ longConnectionChannels?: string[];
38
39
  /** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace dir). */
39
40
  flyConfig: string;
40
41
  }
@@ -46,9 +47,9 @@ export type FlyRunOutcome = {
46
47
  gate: string;
47
48
  };
48
49
  /**
49
- * Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)` /
50
- * `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
51
- * Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
50
+ * Run the deploy through `fly`. `log` reports progress; the injected Telegram/Feishu/Slack registrars
51
+ * perform post-deploy webhook steps from the builder machine (Slack's control credential never travels
52
+ * to the host). Absent, the manual console
52
53
  * instruction is printed. Every gate is fail-visible.
53
54
  */
54
- export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>): Promise<FlyRunOutcome>;
55
+ export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome>): Promise<FlyRunOutcome>;
@@ -19,12 +19,12 @@ function listHasName(stdout, name) {
19
19
  }
20
20
  }
21
21
  /**
22
- * Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)` /
23
- * `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
24
- * Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
22
+ * Run the deploy through `fly`. `log` reports progress; the injected Telegram/Feishu/Slack registrars
23
+ * perform post-deploy webhook steps from the builder machine (Slack's control credential never travels
24
+ * to the host). Absent, the manual console
25
25
  * instruction is printed. Every gate is fail-visible.
26
26
  */
27
- export async function deployFlyRun(plan, fly, log, registerTelegram, registerFeishu) {
27
+ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFeishu, registerSlack) {
28
28
  const gate = (g) => ({ ok: false, gate: g });
29
29
  // 1. Auth is the one gate a coding agent can't clear itself (browser OAuth). `whoami` succeeds with
30
30
  // either an interactive login or FLY_API_TOKEN, so one check covers both.
@@ -91,8 +91,19 @@ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFei
91
91
  log(`github: set the webhook in the repo (Settings → Webhooks) → https://${plan.appName}.fly.dev/webhook`);
92
92
  reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
93
93
  }
94
+ if (plan.channels.includes("slack")) {
95
+ const baseUrl = `https://${plan.appName}.fly.dev`;
96
+ if (registerSlack) {
97
+ log("registering slack event URL…");
98
+ reg.track("slack", await registerSlack(baseUrl));
99
+ }
100
+ else {
101
+ log(`slack: set Event Subscriptions → Request URL → ${baseUrl}/slack`);
102
+ reg.track("slack", "manual");
103
+ }
104
+ }
94
105
  for (const kind of ["feishu", "lark"]) {
95
- if (!plan.channels.includes(kind))
106
+ if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
96
107
  continue;
97
108
  if (registerFeishu) {
98
109
  log(`registering ${kind} event URL…`);
@@ -2,14 +2,18 @@ import type { FastagentConfig } from "../engines/pi/config.ts";
2
2
  import { type ChannelKind } from "../scaffold/add-channel.ts";
3
3
  import { type ContainerInput } from "./container.ts";
4
4
  /** A stderr line the CLI prints (`[fastagent] warn: …` / `[fastagent] note: …`). Host-neutral advisories. */
5
- export interface DeployMessage {
5
+ interface DeployMessage {
6
6
  level: "warn" | "note";
7
7
  text: string;
8
8
  }
9
9
  /** The resolved facts every host plan needs (the container shape, channels, model auth, ports/secrets). */
10
- export interface DeployFacts {
10
+ interface DeployFacts {
11
11
  messages: DeployMessage[];
12
12
  channels: ChannelKind[];
13
+ /** Every structurally detected HTTP-route channel basename, including custom channels. */
14
+ routeChannels: string[];
15
+ /** Every structurally detected long-connection channel basename, including custom channels. */
16
+ longConnectionChannels: string[];
13
17
  /** Whether the agent has TIME triggers — `schedules/` files or `selfSchedule` (the wake tool). Cron/wake
14
18
  * has no external wake-up, so the deployment must keep one machine running: the fly plan forces
15
19
  * `min_machines_running=1`, the railway runbook forbids App Sleeping. */
@@ -46,8 +50,9 @@ export declare function preflightDeploy(input: {
46
50
  run: boolean;
47
51
  /** `--force` regenerates artifacts, so the kept-hand-written-Dockerfile apt warning does not apply. */
48
52
  force: boolean;
49
- /** `--auth-path` / `FASTAGENT_AUTH_PATH`; falls back to the project default `<state root>/auth.json`. */
50
- authPathOverride: string | undefined;
53
+ /** The raw `--auth-path` flag; the chain (flag > FASTAGENT_AUTH_PATH > `<state root>/auth.json`)
54
+ * is resolved HERE via {@link resolveAuthPath} — the one owner, same as every serving command. */
55
+ authPathFlag: string | undefined;
51
56
  }): Promise<DeployPreflight>;
52
57
  /**
53
58
  * Why the resolved model won't reach the deployed box, or undefined if it will — host-neutral. `fastagent.config.ts`
@@ -57,3 +62,4 @@ export declare function preflightDeploy(input: {
57
62
  * (`--run`). Single source on purpose — a host env block (fly.toml `[env]`) is NOT advertised as a second home.
58
63
  */
59
64
  export declare function modelTravelIssue(configModel: string | undefined, modelSpec: string | undefined): string | undefined;
65
+ export {};