@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.
- package/README.md +3 -3
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/context-buffer.d.ts +30 -0
- package/dist/channels/context-buffer.js +98 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +8 -11
- package/dist/channels/feishu/context-buffer.js +6 -67
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +20 -2
- package/dist/channels/feishu/feishu.d.ts +28 -16
- package/dist/channels/feishu/feishu.js +210 -188
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +10 -62
- package/dist/channels/feishu/model.d.ts +2 -1
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/owned-threads.d.ts +2 -6
- package/dist/channels/feishu/parse.d.ts +1 -1
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +24 -140
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/setup-mode.d.ts +8 -0
- package/dist/channels/feishu/setup-mode.js +2 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +8 -6
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +35 -2
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/preview-kit.d.ts +109 -0
- package/dist/channels/preview-kit.js +183 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/owned-threads.d.ts +6 -0
- package/dist/channels/slack/owned-threads.js +43 -0
- package/dist/channels/slack/parse.d.ts +23 -0
- package/dist/channels/slack/parse.js +81 -0
- package/dist/channels/slack/preview.d.ts +24 -0
- package/dist/channels/slack/preview.js +359 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +77 -0
- package/dist/channels/slack/slack-api.js +415 -0
- package/dist/channels/slack/slack.d.ts +58 -0
- package/dist/channels/slack/slack.js +451 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.js +3 -3
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +13 -0
- package/dist/channels/tasks.js +10 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/telegram.js +23 -9
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
- package/dist/channels/{feishu/text.js → text.js} +1 -1
- package/dist/channels/turn-queue.js +1 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +27 -0
- package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
- package/dist/cli/add-slack.d.ts +10 -0
- package/dist/cli/add-slack.js +204 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +152 -20
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +6 -1
- package/dist/cli/commands/deploy.d.ts +2 -0
- package/dist/cli/commands/deploy.js +78 -22
- package/dist/cli/commands/dev.js +11 -5
- package/dist/cli/commands/fire.js +3 -3
- package/dist/cli/commands/info.js +2 -2
- package/dist/cli/commands/invoke.js +2 -3
- package/dist/cli/commands/login.js +14 -25
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/start.js +15 -8
- package/dist/cli/commands/tool.js +2 -1
- package/dist/cli/fail.d.ts +0 -2
- package/dist/cli/fail.js +1 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +79 -21
- package/dist/cli/serve.d.ts +38 -18
- package/dist/cli/serve.js +197 -39
- package/dist/cli/shared.d.ts +26 -6
- package/dist/cli/shared.js +163 -33
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/docker/plan.d.ts +2 -2
- package/dist/deploy/docker/plan.js +8 -6
- package/dist/deploy/fly/plan.d.ts +3 -1
- package/dist/deploy/fly/plan.js +12 -7
- package/dist/deploy/fly/run.d.ts +5 -4
- package/dist/deploy/fly/run.js +16 -5
- package/dist/deploy/preflight.d.ts +10 -4
- package/dist/deploy/preflight.js +53 -9
- package/dist/deploy/railway/plan.d.ts +3 -1
- package/dist/deploy/railway/plan.js +12 -4
- package/dist/deploy/railway/run.d.ts +5 -4
- package/dist/deploy/railway/run.js +15 -5
- package/dist/deploy/runner.d.ts +2 -1
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +0 -2
- package/dist/dev-supervisor.js +7 -3
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +90 -60
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +14 -9
- package/dist/engines/pi/config.js +33 -18
- package/dist/engines/pi/create.d.ts +22 -2
- package/dist/engines/pi/create.js +20 -14
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +11 -0
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +6 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/engines/pi/workspace.d.ts +52 -0
- package/dist/engines/pi/workspace.js +72 -15
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +23 -6
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/pi.d.ts +5 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +126 -17
- package/dist/scaffold/templates/fastagent.config.mjs +4 -3
- package/dist/schedule/wakeups.d.ts +0 -3
- package/dist/schedule/wakeups.js +1 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +21 -10
- package/package.json +19 -7
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
package/dist/deploy/preflight.js
CHANGED
|
@@ -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 {
|
|
15
|
-
import {
|
|
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,
|
|
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
|
|
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 (
|
|
44
|
-
|
|
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 =
|
|
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 {
|
|
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
|
-
/**
|
|
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") ||
|
|
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
|
-
:
|
|
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;
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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;
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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…`);
|
package/dist/deploy/runner.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 {};
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -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;
|
package/dist/deploy/secrets.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/dist/dev-supervisor.d.ts
CHANGED
|
@@ -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
|
package/dist/dev-supervisor.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
18
|
+
export declare function inspectChannels(dir: string): Promise<{
|
|
19
|
+
channels: string[];
|
|
20
|
+
routeChannels: string[];
|
|
21
|
+
longConnectionChannels: string[];
|
|
22
|
+
failures: ModuleLoadFailure[];
|
|
23
|
+
}>;
|
|
15
24
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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)
|
|
3
|
-
*
|
|
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
|
|
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((
|
|
67
|
+
.map((name) => name.replace(/\.(ts|js|mjs)$/, ""))
|
|
32
68
|
.sort();
|
|
33
69
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
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?:
|
|
7
|
+
export declare function runPiChat(dir: string, options?: BuildSessionRuntimeOptions): Promise<void>;
|