@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
@@ -11,8 +11,8 @@
11
11
  */
12
12
  import { readFile } from "node:fs/promises";
13
13
  import { join, relative, sep } from "node:path";
14
- import { defaultAuthPath, resolveStateRoot } from "../engines/pi/config.js";
15
- import { discoverChannelFiles } from "../engines/pi/channel.js";
14
+ import { resolveAuthPath } from "../engines/pi/config.js";
15
+ import { inspectChannels } from "../engines/pi/channel.js";
16
16
  import { discoverScheduleFiles } from "../schedule/discover.js";
17
17
  import { createPiModels, probeAuthSource } from "../engines/pi/models.js";
18
18
  import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
@@ -25,7 +25,7 @@ import { isGeneratedDockerfile } from "./container.js";
25
25
  * provider) — the CLI wraps the call in its `failStartup` so the fault surfaces and exits, never silently.
26
26
  */
27
27
  export async function preflightDeploy(input) {
28
- const { target, agentDir, config, modelSpec, run, force, authPathOverride } = input;
28
+ const { target, agentDir, config, modelSpec, run, force, authPathFlag } = input;
29
29
  const messages = [];
30
30
  // The deployed box resolves the model from fastagent.config.ts ONLY (in the image); a model set via
31
31
  // env/flag/.env doesn't travel. `--run` would ship a known crash-loop — hard gate; generate-only warns.
@@ -35,20 +35,52 @@ export async function preflightDeploy(input) {
35
35
  return { ok: false, gate: modelIssue };
36
36
  messages.push({ level: "warn", text: modelIssue });
37
37
  }
38
+ // The control plane on a deployed box: `start` honors `sessionControl: true`, so `/control/*`
39
+ // (steer/abort/set_model) rides the PUBLIC host URL — protected only by a per-boot bearer token
40
+ // minted INSIDE the container (`<stateRoot>/control.json`), which external consumers cannot read.
41
+ // Publicly reachable yet unusable is the worst of both; the tunnel path warns loudly and deploy
42
+ // must not be the silent second way to break the loopback trust story.
43
+ if (config.sessionControl === true) {
44
+ messages.push({
45
+ level: "warn",
46
+ text: `sessionControl: true — the deployed box serves /control/* (steer/abort/set_model) at its public URL, ` +
47
+ `protected only by a per-boot token written inside the container. Read the TOKEN from ` +
48
+ `<stateRoot>/control.json on the box (its url field is container-loopback — pair the token with the ` +
49
+ `public host URL: attach --url <public-url> --token …), or front the endpoint with real auth (design §14)`,
50
+ });
51
+ }
38
52
  // Known channel kinds only — a custom channel's secrets/webhook are unknown to us; note and let the
39
53
  // author wire them.
40
- const discovered = await discoverChannelFiles(agentDir);
54
+ const inspected = await inspectChannels(agentDir);
55
+ if (inspected.failures.length > 0) {
56
+ throw new Error(`cannot inspect channel modules: ${inspected.failures.map((failure) => `${failure.label}: ${failure.message}`).join("; ")}`);
57
+ }
58
+ const discovered = inspected.channels;
41
59
  const channels = discovered.filter((c) => CHANNEL_KINDS.includes(c));
60
+ const routeChannels = inspected.routeChannels;
61
+ const longConnectionChannels = inspected.longConnectionChannels;
42
62
  for (const c of discovered) {
43
- if (!channels.includes(c)) {
44
- messages.push({ level: "note", text: `channel "${c}" is custom — set its secrets and webhook yourself` });
45
- }
63
+ if (channels.includes(c))
64
+ continue;
65
+ messages.push({
66
+ level: "note",
67
+ text: longConnectionChannels.includes(c)
68
+ ? `long-connection channel "${c}" is custom — configure its secrets yourself; generated deploy plans keep the process running and skip webhook registration`
69
+ : `route channel "${c}" is custom — configure its secrets and webhook yourself`,
70
+ });
46
71
  }
47
72
  // Time triggers (static schedules or self-scheduling) need a machine kept running — unlike a webhook,
48
73
  // nothing external wakes a scale-to-zero box for a cron instant or a wake-up. The note is CONDITIONAL
49
74
  // ("the generated plan…"): in KEEP mode an existing fly.toml is not rewritten — the CLI warns separately
50
75
  // when a kept fly.toml still scales to zero.
51
76
  const hasTimeTriggers = (await discoverScheduleFiles(agentDir)).length > 0 || !!config.selfSchedule;
77
+ if (longConnectionChannels.length > 0) {
78
+ messages.push({
79
+ level: "note",
80
+ text: `long-connection channel present (${longConnectionChannels.join(", ")}) — a GENERATED plan keeps one machine running ` +
81
+ `(an outbound connection cannot wake a scaled-to-zero service).`,
82
+ });
83
+ }
52
84
  if (hasTimeTriggers) {
53
85
  messages.push({
54
86
  level: "note",
@@ -58,7 +90,7 @@ export async function preflightDeploy(input) {
58
90
  }
59
91
  // Probe auth from the SAME project-level file the opener/login use — not the global default, which would
60
92
  // miss a `fastagent login` credential and falsely report "none configured".
61
- const authPath = authPathOverride ?? defaultAuthPath(resolveStateRoot(target));
93
+ const authPath = resolveAuthPath(target, authPathFlag);
62
94
  const modelAuth = modelSpec ? await probeAuthSource(createPiModels({ authPath }), modelSpec) : undefined;
63
95
  // Container facts (shared by every host) + the warnings that follow. Repo-as-workspace layout
64
96
  // (agentDir ≠ target): the facts describe the KIT — its package.json/runtime/lockfile drive the
@@ -173,7 +205,19 @@ export async function preflightDeploy(input) {
173
205
  });
174
206
  }
175
207
  }
176
- return { ok: true, messages, channels, hasTimeTriggers, modelAuth, authPath, container, port, extraSecrets };
208
+ return {
209
+ ok: true,
210
+ messages,
211
+ channels,
212
+ routeChannels,
213
+ longConnectionChannels,
214
+ hasTimeTriggers,
215
+ modelAuth,
216
+ authPath,
217
+ container,
218
+ port,
219
+ extraSecrets,
220
+ };
177
221
  }
178
222
  /**
179
223
  * Why the resolved model won't reach the deployed box, or undefined if it will — host-neutral. `fastagent.config.ts`
@@ -29,8 +29,10 @@ export interface RailwayPlanInput extends ContainerInput {
29
29
  serviceName: string;
30
30
  /** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
31
31
  modelAuth: string | undefined;
32
- /** Channels discovered in the workspace — each contributes its secret metadata + webhook step. */
32
+ /** Known first-party channels — each contributes its secret metadata + webhook step. */
33
33
  channels: ChannelKind[];
34
+ /** All long-connection channel basenames, including custom channels — no App Sleeping. */
35
+ longConnectionChannels?: string[];
34
36
  /** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
35
37
  extraSecrets?: string[];
36
38
  /** Time triggers present (schedules/ or selfSchedule) — the runbook forbids App Sleeping: cron/wake has
@@ -23,7 +23,7 @@ export function planRailwayDeploy(input) {
23
23
  { path: configPath, content: railwayJson(input.kitDir) },
24
24
  ...containerArtifacts(input),
25
25
  ];
26
- const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
26
+ const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
27
27
  const requiredSecrets = secrets.filter((secret) => secret.required);
28
28
  const optionalSecrets = secrets.filter((secret) => !secret.required);
29
29
  // Order matters, not cosmetics: `railway init` creates a PROJECT with no service, but the volume and
@@ -76,7 +76,10 @@ export function planRailwayDeploy(input) {
76
76
  // The public URL is minted, not deterministic (unlike Fly's <app>.fly.dev) — ONE mint step, then each
77
77
  // channel's webhook uses that domain (mint once even when both channels are present).
78
78
  const hasFeishuCloudChannel = ["feishu", "lark"].some((kind) => channels.includes(kind));
79
- if (channels.includes("telegram") || channels.includes("github") || hasFeishuCloudChannel) {
79
+ if (channels.includes("telegram") ||
80
+ channels.includes("github") ||
81
+ channels.includes("slack") ||
82
+ hasFeishuCloudChannel) {
80
83
  runbook.push(``, `# Public URL — Railway mints a *.up.railway.app domain (NOT deterministic). Generate it, then read`, `# the printed https URL and use it as <your-domain> in the webhook step(s) below:`, `railway domain`);
81
84
  }
82
85
  if (channels.includes("telegram")) {
@@ -85,8 +88,11 @@ export function planRailwayDeploy(input) {
85
88
  if (channels.includes("github")) {
86
89
  runbook.push(`# Set the GitHub webhook (repo Settings → Webhooks). Default route POST /webhook; if you remapped it`, `# in channels/github.ts, use your path:`, `# Payload URL = https://<your-domain>/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
87
90
  }
91
+ if (channels.includes("slack")) {
92
+ runbook.push(`# Set Slack Event Subscriptions → Request URL (default route POST /slack; the running service`, `# answers Slack's challenge), and match scopes/subscriptions to channels/slack.ts groupBehavior:`, `# Request URL = https://<your-domain>/slack`);
93
+ }
88
94
  for (const kind of ["feishu", "lark"]) {
89
- if (!channels.includes(kind))
95
+ if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
90
96
  continue;
91
97
  const label = kind === "feishu" ? "Feishu" : "Lark";
92
98
  runbook.push(`# Set the ${label} event Request URL (developer console → Events & Callbacks). Default route`, `# POST /${kind}; the service must be RUNNING when you save (the console verifies with a challenge):`, `# Request URL = https://<your-domain>/${kind}`);
@@ -98,6 +104,8 @@ export function planRailwayDeploy(input) {
98
104
  ? `# Scale-to-zero: do NOT enable App Sleeping — github turns have no replay, a sleep mid-review is lost.`
99
105
  : input.hasTimeTriggers
100
106
  ? `# Scale-to-zero: do NOT enable App Sleeping — schedules/wake-ups have no external wake-up; a sleeping service sleeps through them.`
101
- : `# Scale-to-zero (optional, dashboard-only — no CLI/API): Settings → Deploy → Serverless → App Sleeping.`, `# Keep this a SINGLE service: the ${MOUNT} volume is tied to one service; extra replicas split state.`);
107
+ : (input.longConnectionChannels?.length ?? 0) > 0
108
+ ? `# Scale-to-zero: do NOT enable App Sleeping — a long-connection channel must remain connected.`
109
+ : `# Scale-to-zero (optional, dashboard-only — no CLI/API): Settings → Deploy → Serverless → App Sleeping.`, `# Keep this a SINGLE service: the ${MOUNT} volume is tied to one service; extra replicas split state.`);
102
110
  return { artifacts, runbook };
103
111
  }
@@ -38,6 +38,7 @@ export interface RailwayRunPlan {
38
38
  /** Required secret names with NO local value — the run gates on these before any side effect. */
39
39
  missingSecrets: string[];
40
40
  channels: ChannelKind[];
41
+ longConnectionChannels?: string[];
41
42
  /** Opt-in (CLI `--into-linked`) to provision INTO the project this directory is already linked to. Off
42
43
  * by default so `--run` only creates on an unlinked dir and never deploys into a pre-existing (possibly
43
44
  * unrelated/production) project; the flag is the operator's explicit "yes, this project". */
@@ -76,9 +77,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
76
77
  */
77
78
  export declare function parseHasVolume(stdout: string, mountPath: string): boolean;
78
79
  /**
79
- * Run the deploy through `railway`. `log` reports progress; `registerTelegram(baseUrl)` /
80
- * `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
81
- * Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
80
+ * Run the deploy through `railway`. `log` reports progress; the injected Telegram/Feishu/Slack
81
+ * registrars perform post-deploy webhook steps from the builder machine (Slack's control credential
82
+ * never travels to the host). Absent, the manual console
82
83
  * instruction is printed. Every gate is fail-visible.
83
84
  */
84
- export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>): Promise<RailwayRunOutcome>;
85
+ export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome>): Promise<RailwayRunOutcome>;
@@ -62,12 +62,12 @@ export function parseHasVolume(stdout, mountPath) {
62
62
  return jsonStrings(stdout).includes(mountPath);
63
63
  }
64
64
  /**
65
- * Run the deploy through `railway`. `log` reports progress; `registerTelegram(baseUrl)` /
66
- * `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
67
- * Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
65
+ * Run the deploy through `railway`. `log` reports progress; the injected Telegram/Feishu/Slack
66
+ * registrars perform post-deploy webhook steps from the builder machine (Slack's control credential
67
+ * never travels to the host). Absent, the manual console
68
68
  * instruction is printed. Every gate is fail-visible.
69
69
  */
70
- export async function deployRailwayRun(plan, railway, log, registerTelegram, registerFeishu) {
70
+ export async function deployRailwayRun(plan, railway, log, registerTelegram, registerFeishu, registerSlack) {
71
71
  const gate = (g) => ({ ok: false, gate: g });
72
72
  // Every --service below targets plan.name — the name this tool gives BOTH the project and the service
73
73
  // (`init --name` + `add --service`). On a fresh create they match; on `--into-linked` into a hand-made
@@ -179,8 +179,18 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
179
179
  log(`github: set the webhook in the repo (Settings → Webhooks) → ${url}/webhook`);
180
180
  reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
181
181
  }
182
+ if (plan.channels.includes("slack")) {
183
+ if (registerSlack) {
184
+ log("registering slack event URL…");
185
+ reg.track("slack", await registerSlack(url));
186
+ }
187
+ else {
188
+ log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
189
+ reg.track("slack", "manual");
190
+ }
191
+ }
182
192
  for (const kind of ["feishu", "lark"]) {
183
- if (!plan.channels.includes(kind))
193
+ if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
184
194
  continue;
185
195
  if (registerFeishu) {
186
196
  log(`registering ${kind} event URL…`);
@@ -1,4 +1,4 @@
1
- export interface RunResult {
1
+ interface RunResult {
2
2
  code: number;
3
3
  /** Captured stdout (for `--json` queries); empty when the command streamed to the terminal. The CLI's
4
4
  * stderr is always inherited straight to the terminal, so it is not a field here. */
@@ -19,3 +19,4 @@ export type CliRunner = (args: string[], opts?: {
19
19
  * ENOENT (the CLI not on PATH) resolves to code 127 so the caller can gate with an install hint.
20
20
  */
21
21
  export declare function spawnRunner(bin: string, cwd: string): CliRunner;
22
+ export {};
@@ -17,7 +17,7 @@ export declare function isEnvKey(source: string | undefined): source is string;
17
17
  * deployment prerequisites. An OAuth/stored login has no env key here — it carries as
18
18
  * `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
19
19
  */
20
- export declare function deploymentSecrets(modelAuth: string | undefined, channels: ChannelKind[], extraSecrets?: string[]): {
20
+ export declare function deploymentSecrets(modelAuth: string | undefined, channels: ChannelKind[], extraSecrets?: string[], longConnectionChannels?: string[]): {
21
21
  name: string;
22
22
  hint: string;
23
23
  required: boolean;
@@ -40,6 +40,7 @@ export declare function assembleSecrets(input: {
40
40
  modelAuth: string | undefined;
41
41
  authFile: Buffer | undefined;
42
42
  channels: ChannelKind[];
43
+ longConnectionChannels?: string[];
43
44
  /** Extra secret env-var names from `fastagent.config` deploy.secrets — carried like channel secrets. */
44
45
  extraSecrets?: string[];
45
46
  env: NodeJS.ProcessEnv;
@@ -19,13 +19,15 @@ export function isEnvKey(source) {
19
19
  * deployment prerequisites. An OAuth/stored login has no env key here — it carries as
20
20
  * `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
21
21
  */
22
- export function deploymentSecrets(modelAuth, channels, extraSecrets = []) {
22
+ export function deploymentSecrets(modelAuth, channels, extraSecrets = [], longConnectionChannels = []) {
23
23
  const secrets = [];
24
24
  if (isEnvKey(modelAuth))
25
25
  secrets.push({ name: modelAuth, hint: "your model provider key", required: true });
26
26
  for (const kind of channels) {
27
- for (const e of channelSetup(kind).env)
27
+ const setupMode = longConnectionChannels.includes(kind) ? "websocket" : "webhook";
28
+ for (const e of channelSetup(kind, setupMode).env) {
28
29
  secrets.push({ name: e.name, hint: e.hint, required: e.required });
30
+ }
29
31
  }
30
32
  // Dedup: a name already covered by the model key / a channel secret must not appear twice in the runbook.
31
33
  for (const name of extraSecrets) {
@@ -67,7 +69,8 @@ export function assembleSecrets(input) {
67
69
  needsModelCredential = true; // no env key, no auth.json — `fastagent login` remediation
68
70
  }
69
71
  for (const kind of input.channels) {
70
- for (const e of channelSetup(kind).env) {
72
+ const setupMode = input.longConnectionChannels?.includes(kind) ? "websocket" : "webhook";
73
+ for (const e of channelSetup(kind, setupMode).env) {
71
74
  const v = input.env[e.name];
72
75
  if (v)
73
76
  secrets[e.name] = v; // optional channel values travel when configured
@@ -76,6 +79,23 @@ export function assembleSecrets(input) {
76
79
  }
77
80
  }
78
81
  }
82
+ // Slack bot-token rotation is an all-or-nothing credential bundle. Its fields remain optional so a
83
+ // manually configured long-lived token works, but a partial bundle must gate before the container
84
+ // reaches slackChannel construction.
85
+ if (input.channels.includes("slack")) {
86
+ const rotation = [
87
+ "SLACK_BOT_REFRESH_TOKEN",
88
+ "SLACK_BOT_TOKEN_EXPIRES_AT",
89
+ "SLACK_CLIENT_ID",
90
+ "SLACK_CLIENT_SECRET",
91
+ ];
92
+ if (rotation.some((name) => !!input.env[name])) {
93
+ for (const name of rotation) {
94
+ if (!input.env[name] && !missingSecrets.includes(name))
95
+ missingSecrets.push(name);
96
+ }
97
+ }
98
+ }
79
99
  for (const name of input.extraSecrets ?? []) {
80
100
  if (name in secrets || missingSecrets.includes(name))
81
101
  continue; // already covered by model/channel — no dup
@@ -1,5 +1,3 @@
1
- /** What the dev watcher restarts on (workspace-relative): the process-bound code inputs only. */
2
- export declare const WATCHED_HINT = "tools/, channels/, package.json (agent dir), fastagent.config.*, .env (run root)";
3
1
  /**
4
2
  * chokidar `ignored` matcher for the narrow watch scope (true = ignore). Ignoring a directory prunes
5
3
  * the whole subtree, so everything outside the allowlist — .fastagent state, node_modules, .git, and
@@ -13,13 +13,13 @@
13
13
  import { spawn } from "node:child_process";
14
14
  import { relative, sep } from "node:path";
15
15
  import { watch as watchTree } from "chokidar";
16
- import { loadConfig, resolveAgentDir } from "./engines/pi/config.js";
16
+ import { loadConfig, resolveAgentDir, resolveStateRoot } from "./engines/pi/config.js";
17
17
  import { log } from "./log.js";
18
18
  import { installProxyFetch } from "./proxy.js";
19
19
  import { openExternalUrl } from "./open-url.js";
20
20
  import { announceWebhooks, startCloudflareTunnel } from "./tunnel.js";
21
21
  /** What the dev watcher restarts on (workspace-relative): the process-bound code inputs only. */
22
- export const WATCHED_HINT = "tools/, channels/, package.json (agent dir), fastagent.config.*, .env (run root)";
22
+ const WATCHED_HINT = "tools/, channels/, package.json (agent dir), fastagent.config.*, .env (run root)";
23
23
  /**
24
24
  * chokidar `ignored` matcher for the narrow watch scope (true = ignore). Ignoring a directory prunes
25
25
  * the whole subtree, so everything outside the allowlist — .fastagent state, node_modules, .git, and
@@ -100,7 +100,11 @@ export async function runDevSupervisor(dir, options = {}) {
100
100
  void startCloudflareTunnel(m.port).then((t) => {
101
101
  if (t) {
102
102
  tunnel = t;
103
- void announceWebhooks(agentDir, t.url, { openUrl: openExternalUrl });
103
+ void announceWebhooks(dir, t.url, {
104
+ openUrl: openExternalUrl,
105
+ routeChannels: m.routeChannels,
106
+ stateRoot: resolveStateRoot(dir),
107
+ });
104
108
  }
105
109
  });
106
110
  }
@@ -1,31 +1,37 @@
1
- import { type ChannelContext, type Routes } from "../../host/node.ts";
1
+ import { type ChannelContext, type LongConnection, type Routes } from "../../host/node.ts";
2
2
  import { type ModuleLoadFailure } from "../../loader.ts";
3
3
  /** A dropped route: two channels claim the same key. Surfaced, never silent. */
4
4
  export interface ChannelCollision {
5
5
  route: string;
6
6
  source: string;
7
7
  }
8
+ /** A long-connection module bound to the same context route factories receive. Internal serving shape. */
9
+ export interface LoadedLongConnectionChannel {
10
+ name: string;
11
+ connect(signal: AbortSignal): LongConnection;
12
+ }
8
13
  /**
9
- * Channel file basenames under `<dir>/channels/` the authoring view (`fastagent info`), which lists
10
- * WITHOUT importing, unlike {@link loadChannels}. It enforces the SAME containment guard so info reports
11
- * exactly the surface dev/start would accept: a channels/ symlink escaping the workspace is rejected
12
- * here too. This path is independent of loadChannels', so it must guard the boundary on its own.
14
+ * Import channel files without mounting route factories or opening connections. Deployment needs only
15
+ * the authored structural fact: function exports are route channels; `{ connect() }` exports are
16
+ * long-connection channels. There is no second ingress/lifecycle declaration to keep in sync.
13
17
  */
14
- export declare function discoverChannelFiles(dir: string): Promise<string[]>;
18
+ export declare function inspectChannels(dir: string): Promise<{
19
+ channels: string[];
20
+ routeChannels: string[];
21
+ longConnectionChannels: string[];
22
+ failures: ModuleLoadFailure[];
23
+ }>;
15
24
  /**
16
- * Discover channels in `<dir>/channels/`: each `*.ts|.js|.mjs` default-exports a `(ctx) => Routes`
17
- * factory ({@link ChannelModule}), called here with the mount context; the returned route maps are
18
- * merged (first file wins a route-key clash, the dropped route surfaced).
19
- *
20
- * A channel file broken for ANY reason — a failed import, a factory that throws when called (a missing
21
- * env var is the common deploy case), or a malformed shape (not a function, not a Routes object, a bad
22
- * handler/route key) — is collected in `failures` without preventing validation of sibling files. The
23
- * serving CLI treats any such failure as fatal: a declared channel must not silently disappear or cause
24
- * the default `/invoke` route to mount. Programmatic callers can inspect the returned data themselves.
25
- * Routes are validated fully before any merge, so a throw mounts no partial routes.
25
+ * Channel file basenames under `<dir>/channels/` the authoring view (`fastagent info`), which lists
26
+ * WITHOUT importing. A symlinked channels directory must remain inside the workspace.
26
27
  */
28
+ export declare function discoverChannelFiles(dir: string): Promise<string[]>;
29
+ /** Discover, validate, and bind all channel modules. No long connection is opened here; the CLI owns it. */
27
30
  export declare function loadChannels(dir: string, ctx: ChannelContext): Promise<{
28
31
  routes: Routes;
32
+ longConnections: LoadedLongConnectionChannel[];
33
+ routeChannels: string[];
34
+ longConnectionChannels: string[];
29
35
  collisions: ChannelCollision[];
30
36
  failures: ModuleLoadFailure[];
31
37
  }>;
@@ -1,19 +1,55 @@
1
1
  /**
2
- * Channel discovery (the N axis, filesystem form): a workspace declares its inbound surface by
3
- * dropping files in `channels/`, mirroring `tools/`. Each file wires a third-party adapter to the
4
- * app's `on()` glue and returns the routes it mounts. There is no config-level channel list — a
5
- * channel always needs glue, so it is always a file.
2
+ * Channel discovery (the N axis, filesystem form). A channel file default-exports either the existing
3
+ * route factory `(ctx) => Routes`, or an explicit long-connection module `{ name, connect(ctx, signal) }`.
6
4
  */
7
5
  import { readdir } from "node:fs/promises";
8
6
  import { isAbsolute, join } from "node:path";
9
- import { parseRouteKey } from "../../host/node.js";
10
- import { assertInsideWorkspace } from "../../workspace.js";
7
+ import { parseRouteKey, } from "../../host/node.js";
11
8
  import { isModuleFile, loadModuleDir } from "../../loader.js";
9
+ import { assertInsideWorkspace } from "../../workspace.js";
10
+ function longConnectionModule(value) {
11
+ return value !== null && typeof value === "object" && typeof value.connect === "function";
12
+ }
13
+ function validateLongConnectionModule(value, label) {
14
+ if (typeof value.name !== "string" || value.name.trim() === "") {
15
+ throw new Error(`${label}: long-connection channel name must be a non-empty string`);
16
+ }
17
+ }
18
+ /**
19
+ * Import channel files without mounting route factories or opening connections. Deployment needs only
20
+ * the authored structural fact: function exports are route channels; `{ connect() }` exports are
21
+ * long-connection channels. There is no second ingress/lifecycle declaration to keep in sync.
22
+ */
23
+ export async function inspectChannels(dir) {
24
+ await assertInsideWorkspace(dir, "channels");
25
+ const { modules, failures } = await loadModuleDir(join(dir, "channels"));
26
+ const channels = [];
27
+ const routeChannels = [];
28
+ const longConnectionChannels = [];
29
+ for (const { name, label, file, mod } of modules) {
30
+ try {
31
+ if (typeof mod.default === "function") {
32
+ channels.push(name);
33
+ routeChannels.push(name);
34
+ continue;
35
+ }
36
+ if (longConnectionModule(mod.default)) {
37
+ validateLongConnectionModule(mod.default, label);
38
+ channels.push(name);
39
+ longConnectionChannels.push(name);
40
+ continue;
41
+ }
42
+ throw new Error(`${label} must default-export (ctx) => Routes or { name, connect(ctx, signal) }`);
43
+ }
44
+ catch (error) {
45
+ failures.push({ label, file, message: error.message });
46
+ }
47
+ }
48
+ return { channels, routeChannels, longConnectionChannels, failures };
49
+ }
12
50
  /**
13
51
  * Channel file basenames under `<dir>/channels/` — the authoring view (`fastagent info`), which lists
14
- * WITHOUT importing, unlike {@link loadChannels}. It enforces the SAME containment guard so info reports
15
- * exactly the surface dev/start would accept: a channels/ symlink escaping the workspace is rejected
16
- * here too. This path is independent of loadChannels', so it must guard the boundary on its own.
52
+ * WITHOUT importing. A symlinked channels directory must remain inside the workspace.
17
53
  */
18
54
  export async function discoverChannelFiles(dir) {
19
55
  await assertInsideWorkspace(dir, "channels");
@@ -28,75 +64,68 @@ export async function discoverChannelFiles(dir) {
28
64
  }
29
65
  return names
30
66
  .filter(isModuleFile)
31
- .map((n) => n.replace(/\.(ts|js|mjs)$/, ""))
67
+ .map((name) => name.replace(/\.(ts|js|mjs)$/, ""))
32
68
  .sort();
33
69
  }
34
- /**
35
- * Discover channels in `<dir>/channels/`: each `*.ts|.js|.mjs` default-exports a `(ctx) => Routes`
36
- * factory ({@link ChannelModule}), called here with the mount context; the returned route maps are
37
- * merged (first file wins a route-key clash, the dropped route surfaced).
38
- *
39
- * A channel file broken for ANY reason — a failed import, a factory that throws when called (a missing
40
- * env var is the common deploy case), or a malformed shape (not a function, not a Routes object, a bad
41
- * handler/route key) — is collected in `failures` without preventing validation of sibling files. The
42
- * serving CLI treats any such failure as fatal: a declared channel must not silently disappear or cause
43
- * the default `/invoke` route to mount. Programmatic callers can inspect the returned data themselves.
44
- * Routes are validated fully before any merge, so a throw mounts no partial routes.
45
- */
70
+ function validateRoutes(value, label) {
71
+ if (value === null || typeof value !== "object" || value instanceof Map) {
72
+ throw new Error(`${label} must return a Routes object`);
73
+ }
74
+ const routes = Object.entries(value);
75
+ if (routes.length === 0) {
76
+ throw new Error(`${label} declared no routes return a non-empty { "METHOD /path": handler } object`);
77
+ }
78
+ for (const [route, handler] of routes) {
79
+ if (typeof handler !== "function") {
80
+ throw new Error(`${label}: route "${route}" must map to a handler function, got ${typeof handler}`);
81
+ }
82
+ if (!parseRouteKey(route).path.startsWith("/")) {
83
+ throw new Error(`${label}: route "${route}" is not a valid route key (expected "METHOD /path" or "/path")`);
84
+ }
85
+ }
86
+ return routes;
87
+ }
88
+ /** Discover, validate, and bind all channel modules. No long connection is opened here; the CLI owns it. */
46
89
  export async function loadChannels(dir, ctx) {
47
- // The contract says stateRoot is absolute; enforce it at the mount boundary so a relative root fails
48
- // fast HERE instead of silently re-anchoring some channel's state on the process cwd.
49
90
  if (!isAbsolute(ctx.stateRoot)) {
50
91
  throw new Error(`ChannelContext.stateRoot must be absolute, got "${ctx.stateRoot}"`);
51
92
  }
52
- // A symlinked channels/ is followed only if it stays inside the workspace, so a deploy that copies
53
- // the dir includes it (the directory is the agent).
54
93
  await assertInsideWorkspace(dir, "channels");
55
94
  const { modules, failures } = await loadModuleDir(join(dir, "channels"));
56
95
  const routes = {};
96
+ const longConnections = [];
97
+ const routeChannels = [];
98
+ const longConnectionChannels = [];
57
99
  const collisions = [];
58
- for (const { label, file, mod } of modules) {
59
- // Collect every per-file failure so the caller can report all broken channels in one pass. The CLI
60
- // then fails startup rather than silently dropping a declared route; direct callers own their policy.
61
- // Routes are VALIDATED fully before any are merged, so a throw mid-validation mounts NO partial routes.
100
+ for (const { name, label, file, mod } of modules) {
62
101
  try {
63
- const factory = mod.default;
64
- if (typeof factory !== "function") {
65
- throw new Error(`${label} must default-export (ctx) => Routes`);
102
+ if (longConnectionModule(mod.default)) {
103
+ validateLongConnectionModule(mod.default, label);
104
+ const channel = mod.default;
105
+ longConnections.push({
106
+ name: channel.name,
107
+ connect: (signal) => channel.connect(ctx, signal),
108
+ });
109
+ longConnectionChannels.push(name);
110
+ continue;
111
+ }
112
+ if (typeof mod.default !== "function") {
113
+ throw new Error(`${label} must default-export (ctx) => Routes or { name, connect(ctx, signal) }`);
66
114
  }
67
- const declared = factory(ctx);
68
- // A Promise needs its own branch before the object check: mark it handled (a rejected async setup
69
- // must not go unhandled) and reject it with a precise message rather than the zero-routes one.
115
+ const declared = mod.default(ctx);
70
116
  if (declared !== null &&
71
117
  typeof declared === "object" &&
72
118
  typeof declared.then === "function") {
73
119
  declared.catch(() => { });
74
- throw new Error(`${label} must return Routes synchronously, not a Promise (an async factory is not supported)`);
75
- }
76
- if (declared === null || typeof declared !== "object") {
77
- throw new Error(`${label} must return a Routes object, got ${declared === null ? "null" : typeof declared}`);
78
- }
79
- const declaredRoutes = Object.entries(declared);
80
- if (declaredRoutes.length === 0) {
81
- throw new Error(`${label} declared no routes — return a non-empty { "METHOD /path": handler } object (a Promise, Map, array, or {} yields none)`);
82
- }
83
- // Validate every route BEFORE merging any (no partial mount on a later throw).
84
- for (const [route, handler] of declaredRoutes) {
85
- if (typeof handler !== "function") {
86
- throw new Error(`${label}: route "${route}" must map to a handler function, got ${typeof handler}`);
87
- }
88
- if (!parseRouteKey(route).path.startsWith("/")) {
89
- throw new Error(`${label}: route "${route}" is not a valid route key (expected "METHOD /path" or "/path")`);
90
- }
120
+ throw new Error(`${label} must return Routes synchronously, not a Promise`);
91
121
  }
122
+ const declaredRoutes = validateRoutes(declared, label);
92
123
  for (const [route, handler] of declaredRoutes) {
93
124
  const parsed = parseRouteKey(route);
94
- // Overlap, not literal-key, equality: the router treats a bare `/path` as any-method, so
95
- // `/webhook` and `POST /webhook` clash. `GET /x` vs `POST /x` is fine.
96
- const clash = Object.keys(routes).some((k) => {
97
- const e = parseRouteKey(k);
98
- return (e.path === parsed.path &&
99
- (e.method === undefined || parsed.method === undefined || e.method === parsed.method));
125
+ const clash = Object.keys(routes).some((key) => {
126
+ const existing = parseRouteKey(key);
127
+ return (existing.path === parsed.path &&
128
+ (existing.method === undefined || parsed.method === undefined || existing.method === parsed.method));
100
129
  });
101
130
  if (clash) {
102
131
  collisions.push({ route, source: label });
@@ -104,10 +133,11 @@ export async function loadChannels(dir, ctx) {
104
133
  }
105
134
  routes[route] = handler;
106
135
  }
136
+ routeChannels.push(name);
107
137
  }
108
138
  catch (error) {
109
139
  failures.push({ label, file, message: error.message });
110
140
  }
111
141
  }
112
- return { routes, collisions, failures };
142
+ return { routes, longConnections, routeChannels, longConnectionChannels, collisions, failures };
113
143
  }
@@ -1,19 +1,7 @@
1
- import { type AgentSessionRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
2
- export interface RunPiChatOptions {
3
- /** Model spec override (the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
4
- model?: string;
5
- }
6
- /**
7
- * Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
8
- * skills resolved exactly as the dev opener does). Split from {@link runPiChat} so the assembly —
9
- * the fidelity-critical part — is inspectable without launching the TUI.
10
- */
11
- export declare function buildChatRuntime(dir: string, options?: RunPiChatOptions,
12
- /** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
13
- sessionManager?: SessionManager): Promise<AgentSessionRuntime>;
1
+ import { type BuildSessionRuntimeOptions } from "./session-builder.ts";
14
2
  /**
15
3
  * Open the workspace's agent in pi's interactive TUI and run until the user exits. The agent is
16
- * fastagent's assembled agent (same model/tools/skills/prompt as dev/start serve); pi's TUI handles
17
- * login, rendering, and same-workspace sessions natively.
4
+ * fastagent's assembled agent (same model/tools/skills/prompt/auth as dev/start serve); pi's TUI
5
+ * handles login, rendering, and same-workspace sessions natively.
18
6
  */
19
- export declare function runPiChat(dir: string, options?: RunPiChatOptions): Promise<void>;
7
+ export declare function runPiChat(dir: string, options?: BuildSessionRuntimeOptions): Promise<void>;