@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
@@ -10,6 +10,8 @@
10
10
  import { mkdir, readFile, writeFile } from "node:fs/promises";
11
11
  import { basename, dirname, join, resolve } from "node:path";
12
12
  import { registerFeishuWebhook } from "../../channels/feishu/register-webhook.js";
13
+ import { readSlackBotAuthEnv } from "../../channels/slack/bot-auth.js";
14
+ import { registerSlackWebhook } from "../../channels/slack/register-webhook.js";
13
15
  import { registerTelegramWebhook } from "../../channels/telegram/register-webhook.js";
14
16
  import { isGeneratedDockerfile } from "../../deploy/container.js";
15
17
  import { composeHasTunnelService, dockerWebhookPaths, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../deploy/docker/plan.js";
@@ -22,12 +24,13 @@ import { deployRailwayRun } from "../../deploy/railway/run.js";
22
24
  import { spawnRunner } from "../../deploy/runner.js";
23
25
  import { assembleSecrets } from "../../deploy/secrets.js";
24
26
  import { loadDotEnv } from "../../env.js";
25
- import { loadConfig, resolveAgentDir, resolveAuthPathOverride, resolveModelSpec } from "../../engines/pi/config.js";
27
+ import { loadConfig, resolveAgentDir, resolveModelSpec, resolveStateRoot } from "../../engines/pi/config.js";
26
28
  import { installProxyFetch } from "../../proxy.js";
27
29
  import { openExternalUrl } from "../../open-url.js";
28
30
  import { exists } from "../../scaffold/init.js";
29
31
  import { announceWebhooks } from "../../tunnel.js";
30
32
  import { failStartup, failUsage } from "../fail.js";
33
+ import { resolveFirstRunModel } from "../shared.js";
31
34
  export async function runDeploy(host, dirArg, opts) {
32
35
  const target = resolve(dirArg);
33
36
  if (opts.tunnel && host !== "docker") {
@@ -36,6 +39,11 @@ export async function runDeploy(host, dirArg, opts) {
36
39
  }
37
40
  loadDotEnv(target); // a custom provider/tool may read a key at config load
38
41
  installProxyFetch(); // post-deploy channel API calls must honor HTTP(S)_PROXY under Node
42
+ // First-run funnel, FULL picker: the write-back lands the model in fastagent.config.* — exactly what
43
+ // the model-travel gate below requires (--model/env don't reach the deployed box) — and an inline
44
+ // login stores the credential `--run` then carries. Runs BEFORE loadConfig; the read-back sees the
45
+ // rewritten file because loadConfig cache-busts on mtime (a failed write-back still gates, correctly).
46
+ await resolveFirstRunModel(target, { model: opts.model, authPath: opts.authPath, input: opts.input });
39
47
  const { config } = await loadConfig(target).catch(failStartup);
40
48
  const agentDir = resolveAgentDir(target, config);
41
49
  const modelSpec = resolveModelSpec(opts.model, config);
@@ -49,13 +57,14 @@ export async function runDeploy(host, dirArg, opts) {
49
57
  modelSpec,
50
58
  run: !!opts.run,
51
59
  force: !!opts.force,
52
- authPathOverride: resolveAuthPathOverride(opts.authPath),
60
+ authPathFlag: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by preflight (one owner)
53
61
  }).catch(failStartup);
54
62
  if (!pre.ok)
55
63
  failStartup(new Error(`deploy stopped: ${pre.gate}`));
56
64
  for (const m of pre.messages)
57
65
  console.error(`[fastagent] ${m.level}: ${m.text}`);
58
- const { channels, hasTimeTriggers, modelAuth, authPath, container, port, extraSecrets } = pre;
66
+ const { channels, routeChannels, longConnectionChannels, hasTimeTriggers, modelAuth, authPath, container, port, extraSecrets, } = pre;
67
+ const hasDeclaredChannels = routeChannels.length + longConnectionChannels.length > 0;
59
68
  // Docker: one app service + loopback port + state volume. `--tunnel` shapes the generated topology
60
69
  // with an optional Quick Tunnel service; `--run` alone decides whether Docker receives side effects.
61
70
  if (host === "docker") {
@@ -66,8 +75,20 @@ export async function runDeploy(host, dirArg, opts) {
66
75
  console.error(`[fastagent] warn: --into-linked is Railway-only — ignored for local Docker`);
67
76
  }
68
77
  const projectName = toDockerProjectName(basename(target));
69
- const dockerPlan = (tunnel) => planDockerDeploy({ projectName, port, modelAuth, channels, tunnel, extraSecrets, ...container });
70
- const requestedTunnel = !!opts.tunnel;
78
+ const dockerPlan = (tunnel) => planDockerDeploy({
79
+ projectName,
80
+ port,
81
+ modelAuth,
82
+ channels,
83
+ longConnectionChannels,
84
+ tunnel,
85
+ extraSecrets,
86
+ ...container,
87
+ });
88
+ const requestedTunnel = !!opts.tunnel && (!hasDeclaredChannels || routeChannels.length > 0);
89
+ if (opts.tunnel && hasDeclaredChannels && routeChannels.length === 0) {
90
+ console.error(`[fastagent] note: --tunnel skipped — every channel uses a long connection`);
91
+ }
71
92
  let plan = dockerPlan(requestedTunnel);
72
93
  // An existing Compose file is authoritative: shape its comparison/runbook from the topology on disk,
73
94
  // regardless of the current flag. `--force` is the explicit reset to the requested generated shape.
@@ -85,13 +106,13 @@ export async function runDeploy(host, dirArg, opts) {
85
106
  if (opts.run) {
86
107
  return runDeployDocker({
87
108
  target,
88
- agentDir,
89
109
  composeFile: plan.composePath,
90
110
  port,
91
111
  requireTunnel: requestedTunnel,
92
112
  modelAuth,
93
113
  authPath,
94
114
  channels,
115
+ longConnectionChannels,
95
116
  extraSecrets,
96
117
  });
97
118
  }
@@ -114,7 +135,15 @@ export async function runDeploy(host, dirArg, opts) {
114
135
  const serviceName = basename(target)
115
136
  .replace(/[^a-zA-Z0-9-]+/g, "-")
116
137
  .replace(/^-+|-+$/g, "") || "agent";
117
- const plan = planRailwayDeploy({ serviceName, modelAuth, channels, extraSecrets, hasTimeTriggers, ...container });
138
+ const plan = planRailwayDeploy({
139
+ serviceName,
140
+ modelAuth,
141
+ channels,
142
+ longConnectionChannels,
143
+ extraSecrets,
144
+ hasTimeTriggers,
145
+ ...container,
146
+ });
118
147
  await writeArtifacts(target, plan.artifacts, {
119
148
  force: !!opts.force,
120
149
  neverForce: container.kitDir ? [".dockerignore"] : [],
@@ -126,6 +155,7 @@ export async function runDeploy(host, dirArg, opts) {
126
155
  modelAuth,
127
156
  authPath,
128
157
  channels,
158
+ longConnectionChannels,
129
159
  extraSecrets,
130
160
  intoLinked: !!opts.intoLinked,
131
161
  });
@@ -171,14 +201,16 @@ export async function runDeploy(host, dirArg, opts) {
171
201
  // Under `--run` this is a GATE (same discipline as the model-travel gate): a full deploy whose schedules
172
202
  // silently never fire is worse than a crash-loop — nothing fails visibly when a cron instant passes on a
173
203
  // sleeping machine, and unlike github's min=0 there is no legitimate trade to accept here.
174
- if (flyTomlExists && !opts.force && hasTimeTriggers) {
204
+ if (flyTomlExists && !opts.force && (hasTimeTriggers || longConnectionChannels.length > 0)) {
175
205
  const min = parseFlyMinMachines(await readFile(flyTomlPath, "utf8"));
176
206
  if ((min ?? 0) === 0) {
177
207
  // undefined = the line is absent — Fly's platform default for min_machines_running is 0, so a
178
208
  // hand-written fly.toml without the line scales to zero exactly like an explicit 0.
209
+ const reason = hasTimeTriggers
210
+ ? `schedules/self-scheduling need a running machine (no external wake-up)`
211
+ : `long-connection channel (${longConnectionChannels.join(", ")}) needs an always-on outbound connection`;
179
212
  const msg = `your kept fly.toml scales to zero (min_machines_running = ${min ?? "absent → platform default 0"}), but ` +
180
- `schedules/self-scheduling need a running machine (no external wake-up). Set min_machines_running = 1, ` +
181
- `or pass --force to regenerate.`;
213
+ `${reason}. Set min_machines_running = 1, or pass --force to regenerate.`;
182
214
  if (opts.run)
183
215
  failStartup(new Error(`deploy stopped: ${msg}`));
184
216
  console.error(`[fastagent] warn: ${msg}`);
@@ -189,6 +221,7 @@ export async function runDeploy(host, dirArg, opts) {
189
221
  port,
190
222
  modelAuth,
191
223
  channels,
224
+ longConnectionChannels,
192
225
  extraSecrets,
193
226
  hasTimeTriggers,
194
227
  ...container,
@@ -199,8 +232,18 @@ export async function runDeploy(host, dirArg, opts) {
199
232
  force: !!opts.force,
200
233
  neverForce: container.kitDir ? [".dockerignore"] : [],
201
234
  });
202
- if (opts.run)
203
- return runDeployFly({ target, appName, modelAuth, authPath, channels, flyTomlPath, extraSecrets });
235
+ if (opts.run) {
236
+ return runDeployFly({
237
+ target,
238
+ appName,
239
+ modelAuth,
240
+ authPath,
241
+ channels,
242
+ longConnectionChannels,
243
+ flyTomlPath,
244
+ extraSecrets,
245
+ });
246
+ }
204
247
  console.log(plan.runbook.join("\n"));
205
248
  }
206
249
  /**
@@ -238,19 +281,26 @@ async function writeArtifacts(target, artifacts, options) {
238
281
  console.error(`[fastagent] wrote ${a.path}`);
239
282
  }
240
283
  }
284
+ function deployEnvironment(target, channels) {
285
+ if (!channels.includes("slack"))
286
+ return process.env;
287
+ const latest = readSlackBotAuthEnv(join(resolveStateRoot(target), "channels", "slack", "bot-auth.json"));
288
+ return { ...process.env, ...latest };
289
+ }
241
290
  /**
242
291
  * `deploy docker --run`: carry local credentials into Compose's child environment, then reconcile the
243
292
  * user-owned local topology. Docker owns container/network/volume lifecycle. A Compose tunnel service,
244
293
  * when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
245
294
  */
246
295
  async function runDeployDocker(params) {
247
- const { target, agentDir, composeFile, port, requireTunnel, modelAuth, authPath, channels, extraSecrets } = params;
296
+ const { target, composeFile, port, requireTunnel, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, } = params;
248
297
  const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
249
298
  modelAuth,
250
299
  authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
251
300
  channels,
301
+ longConnectionChannels,
252
302
  extraSecrets,
253
- env: process.env,
303
+ env: deployEnvironment(target, channels),
254
304
  });
255
305
  const outcome = await deployDockerRun({ composeFile, port, secrets, missingSecrets, needsModelCredential, requireTunnel }, spawnRunner("docker", target), (message) => console.error(`[fastagent] ${message}`));
256
306
  if (!outcome.ok)
@@ -263,12 +313,16 @@ async function runDeployDocker(params) {
263
313
  // Docker Desktop commonly injects a host proxy. The Quick Tunnel hostname may be resolvable only
264
314
  // through it, exactly like provider/channel APIs; use the same Node dispatcher as dev/start/login.
265
315
  installProxyFetch();
266
- await announceWebhooks(agentDir, outcome.tunnelUrl, { openUrl: openExternalUrl });
316
+ await announceWebhooks(target, outcome.tunnelUrl, {
317
+ openUrl: openExternalUrl,
318
+ routeChannels: channels.filter((kind) => !longConnectionChannels.includes(kind)),
319
+ stateRoot: resolveStateRoot(target),
320
+ });
267
321
  console.error(`[fastagent] note: Quick Tunnel URLs are ephemeral — after the tunnel container/Docker daemon ` +
268
322
  `restarts, re-run this deploy so webhooks receive the new URL`);
269
323
  return;
270
324
  }
271
- const paths = dockerWebhookPaths(channels);
325
+ const paths = dockerWebhookPaths(channels.filter((kind) => !longConnectionChannels.includes(kind)));
272
326
  if (paths.length > 0) {
273
327
  console.error(`[fastagent] note: public ingress is operator-owned — configure your tunnel/proxy, then wire the ` +
274
328
  `default webhook path(s): ${paths.join(", ")} (or your remapped channel routes)`);
@@ -282,7 +336,7 @@ async function runDeployDocker(params) {
282
336
  * behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the agent).
283
337
  */
284
338
  async function runDeployFly(params) {
285
- const { target, appName, modelAuth, authPath, channels, flyTomlPath, extraSecrets } = params;
339
+ const { target, appName, modelAuth, authPath, channels, longConnectionChannels, flyTomlPath, extraSecrets } = params;
286
340
  const fly = spawnRunner("fly", target);
287
341
  // Fail fast if flyctl is absent (spawn ENOENT → 127), with the install link — not a confusing auth gate.
288
342
  if ((await fly(["version"], { capture: true })).code === 127) {
@@ -293,15 +347,16 @@ async function runDeployFly(params) {
293
347
  modelAuth,
294
348
  authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
295
349
  channels,
350
+ longConnectionChannels,
296
351
  extraSecrets,
297
- env: process.env,
352
+ env: deployEnvironment(target, channels),
298
353
  });
299
354
  // Model credential has its OWN remediation (login), distinct from a missing secret's (.env) — gate it
300
355
  // here, not through missingSecrets, so the message isn't a contradictory mash of both.
301
356
  if (needsModelCredential) {
302
357
  failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
303
358
  }
304
- const outcome = await deployFlyRun({ appName, region, secrets, missingSecrets, channels, flyConfig: "fly.toml" }, fly, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind));
359
+ const outcome = await deployFlyRun({ appName, region, secrets, missingSecrets, channels, longConnectionChannels, flyConfig: "fly.toml" }, fly, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind), (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(target) }));
305
360
  if (!outcome.ok)
306
361
  failStartup(new Error(`deploy stopped: ${outcome.gate}`));
307
362
  console.error(`[fastagent] deployed → https://${appName}.fly.dev`);
@@ -314,7 +369,7 @@ async function runDeployFly(params) {
314
369
  * webhook) lives in {@link deployRailwayRun}; see there for why Railway differs from Fly.
315
370
  */
316
371
  async function runDeployRailway(params) {
317
- const { target, name, modelAuth, authPath, channels, extraSecrets, intoLinked } = params;
372
+ const { target, name, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, intoLinked } = params;
318
373
  const railway = spawnRunner("railway", target);
319
374
  // Fail fast if the railway CLI is absent (spawn ENOENT → 127), with the install link.
320
375
  if ((await railway(["--version"], { capture: true })).code === 127) {
@@ -324,14 +379,15 @@ async function runDeployRailway(params) {
324
379
  modelAuth,
325
380
  authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
326
381
  channels,
382
+ longConnectionChannels,
327
383
  extraSecrets,
328
- env: process.env,
384
+ env: deployEnvironment(target, channels),
329
385
  });
330
386
  // Model credential has its OWN remediation (login), distinct from a missing secret's (.env).
331
387
  if (needsModelCredential) {
332
388
  failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
333
389
  }
334
- const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, intoLinked }, railway, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind));
390
+ const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, longConnectionChannels, intoLinked }, railway, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind), (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(target) }));
335
391
  if (!outcome.ok)
336
392
  failStartup(new Error(`deploy stopped: ${outcome.gate}`));
337
393
  console.error(`[fastagent] deployed → ${outcome.url}`);
@@ -6,14 +6,13 @@
6
6
  import { resolve } from "node:path";
7
7
  import { runDevSupervisor } from "../../dev-supervisor.js";
8
8
  import { loadDotEnv } from "../../env.js";
9
- import { resolveAuthPathOverride } from "../../engines/pi/config.js";
10
9
  import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
11
10
  import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
12
11
  import { log, setLogLevel } from "../../log.js";
13
12
  import { logAgentLoop } from "../../observe.js";
14
13
  import { installProxyFetch } from "../../proxy.js";
15
14
  import { failStartup } from "../fail.js";
16
- import { maybeTunnel, routesFor, serve, startSchedules } from "../serve.js";
15
+ import { maybeTunnel, mountSessionControl, routesFor, serve, startSchedules } from "../serve.js";
17
16
  import { parsePort, reportAuth, resolveFirstRunModel } from "../shared.js";
18
17
  export async function runDev(dirArg, opts) {
19
18
  const dir = resolve(dirArg);
@@ -42,7 +41,7 @@ async function serveOnce(dir, opts) {
42
41
  installProxyFetch();
43
42
  const a = await createPiAgentFromWorkspace(dir, {
44
43
  model: opts.model,
45
- authPath: resolveAuthPathOverride(opts.authPath),
44
+ authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
46
45
  serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
47
46
  }).catch(failStartup);
48
47
  log.info(`[fastagent] dir: ${dir}`);
@@ -55,9 +54,16 @@ async function serveOnce(dir, opts) {
55
54
  // Trace each turn's agent loop (tool calls + reply) to the log at debug level — shown in dev, gated
56
55
  // out in start (level info), keeping end-user content out of production logs. Wired in both postures.
57
56
  const traced = logAgentLoop(a.agent);
58
- const routes = await routesFor(a.agentDir, traced, a.stateRoot).catch(failStartup);
57
+ const routed = await routesFor(a.agentDir, traced, a.stateRoot, a.sessionControl).catch(failStartup);
58
+ const withControl = mountSessionControl(routed.routes, a.sessionControl, a.stateRoot, {
59
+ tunnel: opts.tunnel ?? false,
60
+ agent: traced, // the remote data plane (POST /control/invoke) drives the SAME traced agent
61
+ });
59
62
  await startSchedules(a.agentDir, traced, a.stateRoot, a.config.selfSchedule ?? false);
60
- serve(routes, portFlag ?? a.config.http?.port ?? 8787, (p) => maybeTunnel(a.agentDir, p, opts.tunnel ?? false));
63
+ serve({ ...routed, routes: withControl.routes }, portFlag ?? a.config.http?.port ?? 8787, (p) => {
64
+ withControl.announce(p);
65
+ maybeTunnel(dir, routed.routeChannels, p, opts.tunnel ?? false, a.stateRoot);
66
+ });
61
67
  }
62
68
  /** The agents/skills/tools/collisions report lines. */
63
69
  function reportAgentsSkillsTools(a) {
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import { relative, resolve, sep } from "node:path";
8
8
  import { loadDotEnv } from "../../env.js";
9
- import { loadConfig, resolveAgentDir, resolveAuthPathOverride } from "../../engines/pi/config.js";
9
+ import { loadConfig, resolveAgentDir } from "../../engines/pi/config.js";
10
10
  import { reportModuleLoadFailures } from "../../engines/pi/report.js";
11
11
  import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
12
- import { runInvokeStream } from "../../invoke-stream.js";
12
+ import { runInvokeStream } from "../invoke-stream.js";
13
13
  import { installProxyFetch } from "../../proxy.js";
14
14
  import { loadSchedules } from "../../schedule/discover.js";
15
15
  import { scheduleSession } from "../../schedule/scheduler.js";
@@ -35,7 +35,7 @@ export async function runFire(name, dirArg, opts) {
35
35
  }
36
36
  const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(fireDir, {
37
37
  model: opts.model,
38
- authPath: resolveAuthPathOverride(opts.authPath),
38
+ authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
39
39
  }).catch(failStartup);
40
40
  console.error(`[fastagent] fire: ${name} (${modelSpec})`);
41
41
  await reportAuth(modelSpec, authPath);
@@ -2,7 +2,7 @@
2
2
  import { resolve } from "node:path";
3
3
  import { loadDotEnv } from "../../env.js";
4
4
  import { discoverChannelFiles } from "../../engines/pi/channel.js";
5
- import { defaultAuthPath, defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPathOverride, resolveModelSpec, resolveSessionsDirOverride, resolveStateRoot, } from "../../engines/pi/config.js";
5
+ import { defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPath, resolveModelSpec, resolveSessionsDirOverride, resolveStateRoot, } from "../../engines/pi/config.js";
6
6
  import { resolveWorkspaceTools } from "../../engines/pi/create.js";
7
7
  import { loadAgentDefinition } from "../../engines/pi/definition.js";
8
8
  import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
@@ -52,7 +52,7 @@ export async function runInfo(dirArg, opts) {
52
52
  // create them, info must not).
53
53
  const stateRoot = resolveStateRoot(dir);
54
54
  const sessionsDir = resolveSessionsDirOverride(opts.sessionsDir) ?? defaultSessionsDir(stateRoot);
55
- const authPath = resolveAuthPathOverride(opts.authPath) ?? defaultAuthPath(stateRoot);
55
+ const authPath = resolveAuthPath(dir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
56
56
  if (opts.json) {
57
57
  console.log(JSON.stringify({
58
58
  dir,
@@ -2,9 +2,8 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { resolve } from "node:path";
4
4
  import { loadDotEnv } from "../../env.js";
5
- import { resolveAuthPathOverride } from "../../engines/pi/config.js";
6
5
  import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
7
- import { runInvokeStream } from "../../invoke-stream.js";
6
+ import { runInvokeStream } from "../invoke-stream.js";
8
7
  import { installProxyFetch } from "../../proxy.js";
9
8
  import { failStartup } from "../fail.js";
10
9
  import { reportAuth, resolveFirstRunModel } from "../shared.js";
@@ -15,7 +14,7 @@ export async function runInvoke(message, dirArg, opts) {
15
14
  await resolveFirstRunModel(invokeDir, opts);
16
15
  const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(invokeDir, {
17
16
  model: opts.model,
18
- authPath: resolveAuthPathOverride(opts.authPath),
17
+ authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
19
18
  }).catch(failStartup);
20
19
  console.error(`[fastagent] invoke: ${invokeDir} (${modelSpec})`);
21
20
  await reportAuth(modelSpec, authPath);
@@ -8,21 +8,19 @@
8
8
  * so the secret can never land untracked — a flow that then fails (bad provider, abort) leaves that
9
9
  * empty state dir behind, by design (no secret without its `.gitignore`). Skipped for the HOME-global dir.
10
10
  */
11
- import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
12
11
  import { loadDotEnv } from "../../env.js";
13
- import { defaultAuthPath, resolveAuthPathOverride, resolveStateRoot } from "../../engines/pi/config.js";
12
+ import { resolveAuthPath, resolveStateRoot } from "../../engines/pi/config.js";
14
13
  import { ensureStateRootSelfIgnored, isUnderDir } from "../../engines/pi/definition.js";
15
- import { loginFlow } from "../../engines/pi/login.js";
16
- import { openExternalUrl } from "../../open-url.js";
14
+ import { LoginCancelled } from "../../engines/pi/login.js";
17
15
  import { installProxyFetch } from "../../proxy.js";
18
16
  import { failStartup } from "../fail.js";
19
- import { isInteractive } from "../shared.js";
17
+ import { isInteractive, loginWithKeyCheck } from "../shared.js";
20
18
  export async function runLogin(provider, opts) {
21
19
  const loginDir = process.cwd();
22
20
  loadDotEnv(loginDir); // FASTAGENT_AUTH_PATH / a proxy (HTTPS_PROXY) may be configured in the project .env
23
21
  installProxyFetch(); // the OAuth token exchange must go through HTTPS_PROXY (region-locked providers)
24
22
  const stateRoot = resolveStateRoot(loginDir);
25
- const authPath = resolveAuthPathOverride(opts.authPath) ?? defaultAuthPath(stateRoot);
23
+ const authPath = resolveAuthPath(loginDir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
26
24
  // login is the command that CREATES the credential file, so the leak guard binds HERE too (not only
27
25
  // in the opener): on an adapted project dir, a `login` before the first dev/start would otherwise
28
26
  // leave the secret untracked-but-committable. Unlike the opener (which populates the WHOLE root, so
@@ -39,25 +37,16 @@ export async function runLogin(provider, opts) {
39
37
  if (opts.input === false || !isInteractive()) {
40
38
  failStartup(new Error(`login is interactive (it shows a menu and opens a browser) — run it in a terminal, not a pipe/CI`));
41
39
  }
42
- const io = terminalLoginIO();
43
- const result = await loginFlow(io, { provider, authPath }).catch(failStartup);
40
+ // loginWithKeyCheck: an entered API key is verified with one minimal request; a rejected key (401)
41
+ // re-prompts in place, so a returned result is always a stored-and-not-definitively-bad credential.
42
+ const result = await loginWithKeyCheck(provider, authPath).catch((error) => {
43
+ if (error instanceof LoginCancelled) {
44
+ // A decision, not a failure — neutral wording; non-zero exit because no credential was stored.
45
+ console.error(`[fastagent] login cancelled`);
46
+ process.exit(1);
47
+ }
48
+ failStartup(error);
49
+ });
44
50
  console.error(`[fastagent] logged in to ${result.provider} (${result.method}) — saved to ${authPath}`);
45
51
  process.exit(0); // the undici proxy agent's keep-alive sockets would otherwise hold the event loop open
46
52
  }
47
- /** Login terminal IO via @clack/prompts: a searchable list once long, a hidden prompt for keys. */
48
- function terminalLoginIO() {
49
- return {
50
- async select(message, options) {
51
- const r = await (options.length > 7 ? autocomplete : select)({ message, options });
52
- return isCancel(r) ? undefined : r;
53
- },
54
- async prompt(message, opts) {
55
- const r = opts?.hidden
56
- ? await password({ message, signal: opts.signal })
57
- : await clackText({ message, signal: opts?.signal });
58
- return isCancel(r) ? undefined : r;
59
- },
60
- note: (message) => clackLog.info(message),
61
- openUrl: openExternalUrl,
62
- };
63
- }
@@ -3,7 +3,7 @@
3
3
  * substring. This module pulls the pi model catalog (heavy) — it is lazy-imported by the spec, so
4
4
  * only an actual `models` invocation pays for it.
5
5
  */
6
- import { formatModelsCommand } from "../../cli-models.js";
6
+ import { formatModelsCommand } from "../models-view.js";
7
7
  import { listModels } from "../../engines/pi/config.js";
8
8
  import { createPiModels } from "../../engines/pi/models.js";
9
9
  export function runModels(search) {
@@ -6,7 +6,7 @@ import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { dirname, resolve } from "node:path";
7
7
  import { authSeedBytes } from "../../deploy/fly/run.js";
8
8
  import { loadDotEnv } from "../../env.js";
9
- import { defaultAuthPath, resolveAuthPathOverride, resolveSessionsDirOverride, resolveStateRoot, } from "../../engines/pi/config.js";
9
+ import { resolveAuthPath, resolveSessionsDirOverride } from "../../engines/pi/config.js";
10
10
  import { isUnderDir } from "../../engines/pi/definition.js";
11
11
  import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
12
12
  import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
@@ -15,7 +15,7 @@ import { logAgentLoop } from "../../observe.js";
15
15
  import { installProxyFetch } from "../../proxy.js";
16
16
  import { exists } from "../../scaffold/init.js";
17
17
  import { failStartup } from "../fail.js";
18
- import { maybeTunnel, routesFor, serve, startSchedules } from "../serve.js";
18
+ import { maybeTunnel, mountSessionControl, routesFor, serve, startSchedules } from "../serve.js";
19
19
  import { parsePort, reportAuth, resolveFirstRunModel } from "../shared.js";
20
20
  export async function runStart(dirArg, opts) {
21
21
  const dir = resolve(dirArg);
@@ -26,14 +26,14 @@ export async function runStart(dirArg, opts) {
26
26
  await resolveFirstRunModel(dir, opts);
27
27
  // A `deploy --run` may carry the operator's local credential as FASTAGENT_AUTH_SEED —
28
28
  // materialize it onto the writable state root BEFORE the opener resolves auth (once, absent-only).
29
- const authPathOverride = resolveAuthPathOverride(opts.authPath);
30
- await maybeSeedAuth(authPathOverride ?? defaultAuthPath(resolveStateRoot(dir)));
29
+ // Same resolveAuthPath the opener uses — ONE owner of the flag > env > default chain.
30
+ await maybeSeedAuth(resolveAuthPath(dir, opts.authPath));
31
31
  // The same opener dev uses (single assembly source), just no watch.
32
32
  const sessionsDirOverride = resolveSessionsDirOverride(opts.sessionsDir);
33
- const { agent, definition, agentDir, config, modelSpec, stateRoot, sessionsDir, authPath, toolNames, deferredToolNames, toolCollisions, toolFailures, } = await createPiAgentFromWorkspace(dir, {
33
+ const { agent, definition, agentDir, config, modelSpec, stateRoot, sessionsDir, authPath, toolNames, deferredToolNames, toolCollisions, toolFailures, sessionControl, } = await createPiAgentFromWorkspace(dir, {
34
34
  model: opts.model,
35
35
  sessionsDir: sessionsDirOverride,
36
- authPath: authPathOverride,
36
+ authPath: opts.authPath,
37
37
  serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
38
38
  }).catch(failStartup);
39
39
  log.info(`[fastagent] start: ${dir}`);
@@ -66,9 +66,16 @@ export async function runStart(dirArg, opts) {
66
66
  reportDefinitionWarnings(definition.collisions, definition.diagnostics);
67
67
  // Same debug turn trace as dev; gated out here by the info level (see dev.ts serveOnce).
68
68
  const traced = logAgentLoop(agent);
69
- const routes = await routesFor(agentDir, traced, stateRoot).catch(failStartup);
69
+ const routed = await routesFor(agentDir, traced, stateRoot, sessionControl).catch(failStartup);
70
+ const withControl = mountSessionControl(routed.routes, sessionControl, stateRoot, {
71
+ tunnel: opts.tunnel ?? false,
72
+ agent: traced,
73
+ });
70
74
  await startSchedules(agentDir, traced, stateRoot, config.selfSchedule ?? false);
71
- serve(routes, portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, (p) => maybeTunnel(agentDir, p, opts.tunnel ?? false));
75
+ serve({ ...routed, routes: withControl.routes }, portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, (p) => {
76
+ withControl.announce(p);
77
+ maybeTunnel(dir, routed.routeChannels, p, opts.tunnel ?? false, stateRoot);
78
+ });
72
79
  // No graceful drain: webhook turns run fire-and-forget; SIGTERM just exits mid-turn. Whether an
73
80
  // in-flight turn is LOST depends on the channel: the Telegram channel persists turn intent pre-ACK
74
81
  // and replays it next start (turn-store.ts, L1 durable execution, at-least-once); HTTP and other
@@ -4,6 +4,7 @@ import { loadDotEnv } from "../../env.js";
4
4
  import { loadConfig, resolveAgentDir } from "../../engines/pi/config.js";
5
5
  import { resolveWorkspaceTools } from "../../engines/pi/create.js";
6
6
  import { reportModuleLoadFailures } from "../../engines/pi/report.js";
7
+ import { turnContext } from "../../engines/pi/tool-context.js";
7
8
  import { failStartup, failUsage } from "../fail.js";
8
9
  export async function runTool(name, argsJson, dirArg) {
9
10
  const toolDir = resolve(dirArg);
@@ -29,7 +30,7 @@ export async function runTool(name, argsJson, dirArg) {
29
30
  catch {
30
31
  failUsage(`invalid JSON args: ${argsJson}`); // malformed input syntax = usage error, exit 2
31
32
  }
32
- const result = await tool.execute(`cli-${name}`, args).catch(failStartup);
33
+ const result = await turnContext.run({ cwd: toolDir }, () => tool.execute(`cli-${name}`, args)).catch(failStartup);
33
34
  const out = result?.details !== undefined
34
35
  ? result.details
35
36
  : (result?.content ?? []).map((c) => ("text" in c ? c.text : "")).join("");
@@ -1,5 +1,3 @@
1
- /** stderr renders color: a color TTY, with Node's `hasColors()` carrying the NO_COLOR/TERM=dumb veto. */
2
- export declare function stderrHasColors(): boolean;
3
1
  /**
4
2
  * The ONE error prefix every error message carries — bold red when stderr renders color, plain
5
3
  * otherwise. Errors are the only place the CLI uses color at all.
package/dist/cli/fail.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** stderr renders color: a color TTY, with Node's `hasColors()` carrying the NO_COLOR/TERM=dumb veto. */
2
- export function stderrHasColors() {
2
+ function stderrHasColors() {
3
3
  return process.stderr.isTTY === true && (process.stderr.hasColors?.() ?? false);
4
4
  }
5
5
  /**
@@ -6,5 +6,5 @@
6
6
  * An errored tool inside an otherwise-completed turn still surfaces (the diagnostic the operator
7
7
  * needs); tool_ended carries no name, so it is remembered from the matching tool_started.
8
8
  */
9
- import type { AgentEvent } from "./agent.ts";
9
+ import type { AgentEvent } from "../agent.ts";
10
10
  export declare function runInvokeStream(events: AsyncIterable<AgentEvent>, out: (text: string) => void, err: (line: string) => void): Promise<number>;
@@ -14,6 +14,10 @@ export async function runInvokeStream(events, out, err) {
14
14
  if (event.isError)
15
15
  err(`[tool] ${toolName.get(event.id) ?? event.id} failed`);
16
16
  break;
17
+ case "retrying":
18
+ // Operator-facing: include the reason (channels show a neutral customer line instead).
19
+ err(`[fastagent] transient failure — retrying (${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms): ${event.reason}`);
20
+ break;
17
21
  case "failed":
18
22
  err(`[fastagent] failed: ${event.details}${event.retryable ? " (retryable)" : ""}`);
19
23
  exitCode = 1;
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { Command } from "commander";
15
15
  /** One positional argument, in commander syntax: `<name>` required, `[dir]` optional. */
16
- export interface ArgSpec {
16
+ interface ArgSpec {
17
17
  name: string;
18
18
  description: string;
19
19
  default?: string;
@@ -29,7 +29,7 @@ export interface FlagSpec {
29
29
  /** Mutually exclusive with these {@link optionKey} values — validated at build time. */
30
30
  conflicts?: string[];
31
31
  }
32
- export interface ExampleSpec {
32
+ interface ExampleSpec {
33
33
  cmd: string;
34
34
  note?: string;
35
35
  }
@@ -87,3 +87,4 @@ export interface ProgramOptions {
87
87
  export declare function optionKey(flags: string): string;
88
88
  /** Build the commander program for `specs`. The CLI entry parses with it; tests inject the IO seams. */
89
89
  export declare function buildProgram(specs: readonly CommandSpec[], options?: ProgramOptions): Command;
90
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { ProviderAuthStatus } from "../engines/pi/models.ts";
2
+ /** One first-run picker entry (@clack/prompts option shape). */
3
+ export interface ModelPickerOption {
4
+ value: string;
5
+ label: string;
6
+ hint: string;
7
+ }
8
+ /**
9
+ * The first-run picker menu: the FULL model catalog, each spec annotated with its provider's auth
10
+ * status — ready first (usable now, with the credential source so "which account pays" is visible at
11
+ * the decision point), then the rest with their remedy. Order within each group preserves `specs`
12
+ * (sorted by the caller). A broken provider (expired/corrupt credential) is annotated, not dropped —
13
+ * fail visibly.
14
+ */
15
+ export declare function buildModelPickerOptions(specs: string[], statuses: Map<string, ProviderAuthStatus>): ModelPickerOption[];
16
+ /** The output of `fastagent models [search]`: the spec `lines` to print to stdout (a case-insensitive
17
+ * substring filter; no search → all), and an stderr `error` diagnostic when a search matches nothing. */
18
+ export declare function formatModelsCommand(specs: string[], search?: string): {
19
+ lines: string[];
20
+ error?: string;
21
+ };