@fastagent-sh/fastagent 0.14.0 → 0.16.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 +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +35 -0
- package/dist/channels/context-buffer.js +103 -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 +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -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 +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- 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/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +115 -0
- package/dist/channels/preview-kit.js +184 -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/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -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 +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -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/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -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 +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -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 +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -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 +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- 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/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- 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 +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- 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 +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- 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/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +5 -4
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -3
- package/dist/schedule/wakeups.js +19 -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 +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- 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-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -2,28 +2,50 @@ import { containerArtifacts } from "../container.js";
|
|
|
2
2
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
3
|
/** State root = the volume mount path, kept in lockstep. `/data` matches the Fly recipe. */
|
|
4
4
|
const MOUNT = "/data";
|
|
5
|
+
/** The `RAILWAY_DOCKERFILE_PATH` value for an agent under `prefix` — repo-root-anchored with a leading
|
|
6
|
+
* slash, the form Railway's builds/dockerfiles docs use for a Dockerfile in another directory. The
|
|
7
|
+
* config file's `dockerfilePath` spells it WITHOUT the slash (the config-as-code schema's own
|
|
8
|
+
* convention); two mechanisms, two documented spellings, one fact each. */
|
|
9
|
+
export const dockerfilePathVar = (prefix) => `/${prefix}Dockerfile`;
|
|
5
10
|
/** railway.json — build/deploy only (Railway's config-as-code scope). No env/volume/sleeping here: those
|
|
6
11
|
* are service settings the runbook applies via CLI. healthcheckPath gates routing on a live server. */
|
|
7
|
-
|
|
12
|
+
/** railway.json is JSON, so its ownership marker is a KEY rather than a comment line. Railway ignores
|
|
13
|
+
* unknown keys; the predicate below is what lets `--force` reset OUR file and keep a hand-written one. */
|
|
14
|
+
const GENERATED_RAILWAY_KEY = "x-generated-by";
|
|
15
|
+
const GENERATED_RAILWAY_VALUE = "fastagent deploy railway";
|
|
16
|
+
/** Did fastagent generate this `railway.json`? Unparseable or unmarked reads as the author's. */
|
|
17
|
+
export function isGeneratedRailwayJson(content) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(content)[GENERATED_RAILWAY_KEY] === GENERATED_RAILWAY_VALUE;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function railwayJson(prefix) {
|
|
8
26
|
return `${JSON.stringify({
|
|
9
27
|
$schema: "https://railway.com/railway.schema.json",
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
[GENERATED_RAILWAY_KEY]: GENERATED_RAILWAY_VALUE,
|
|
29
|
+
// dockerfilePath is relative to the workspace root (`railway up`'s upload context).
|
|
30
|
+
build: { builder: "DOCKERFILE", dockerfilePath: `${prefix}Dockerfile` },
|
|
12
31
|
deploy: { healthcheckPath: "/health", restartPolicyType: "ON_FAILURE" },
|
|
13
32
|
}, null, 2)}\n`;
|
|
14
33
|
}
|
|
15
34
|
/** Compute the Railway deploy plan from the resolved definition. */
|
|
16
35
|
export function planRailwayDeploy(input) {
|
|
17
36
|
const { serviceName, modelAuth, channels } = input;
|
|
18
|
-
//
|
|
19
|
-
// railway.toml/json for the product). Railway reads config-as-code from the repo root by default
|
|
20
|
-
//
|
|
21
|
-
|
|
37
|
+
// railway.json is namespaced under the agent dir too (the workspace may carry its own
|
|
38
|
+
// railway.toml/json for the product). Railway reads config-as-code from the repo root by default and
|
|
39
|
+
// pointing it at a custom path is DASHBOARD-ONLY — so the BUILD entry travels as the scriptable
|
|
40
|
+
// RAILWAY_DOCKERFILE_PATH service variable instead (Railway's documented non-root-Dockerfile route),
|
|
41
|
+
// and the config-as-code pointer degrades to an OPTIONAL enhancement: the /health gate (Railway's
|
|
42
|
+
// default restart policy already matches the file's ON_FAILURE).
|
|
43
|
+
const configPath = `${input.agentPrefix}railway.json`;
|
|
22
44
|
const artifacts = [
|
|
23
|
-
{ path: configPath, content: railwayJson(input.
|
|
45
|
+
{ path: configPath, content: railwayJson(input.agentPrefix) },
|
|
24
46
|
...containerArtifacts(input),
|
|
25
47
|
];
|
|
26
|
-
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
48
|
+
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
|
|
27
49
|
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
28
50
|
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
29
51
|
// Order matters, not cosmetics: `railway init` creates a PROJECT with no service, but the volume and
|
|
@@ -47,11 +69,13 @@ export function planRailwayDeploy(input) {
|
|
|
47
69
|
`# the later commands resolve it without --service (--run passes --service to stay non-interactive).`,
|
|
48
70
|
`railway add --service ${serviceName}`,
|
|
49
71
|
``,
|
|
50
|
-
`# Persistent volume at ${MOUNT} — sessions,
|
|
72
|
+
`# Persistent volume at ${MOUNT} — .state (sessions, channel state) + .secrets (seeded auth).`,
|
|
51
73
|
`railway volume add --mount-path ${MOUNT}`,
|
|
52
74
|
``,
|
|
53
75
|
`# Variables — set BEFORE the first deploy so the box boots with them. Railway injects PORT itself.`,
|
|
54
|
-
|
|
76
|
+
`# RAILWAY_DOCKERFILE_PATH points the build at the agent's Dockerfile — a service variable,`,
|
|
77
|
+
`# Railway's documented route to a non-root Dockerfile (no dashboard step needed for the build).`,
|
|
78
|
+
`railway variables set FASTAGENT_STATE_DIR=${MOUNT}/.state FASTAGENT_SECRETS_DIR=${MOUNT}/.secrets RAILWAY_DOCKERFILE_PATH=${dockerfilePathVar(input.agentPrefix)}`,
|
|
55
79
|
];
|
|
56
80
|
if (requiredSecrets.length > 0) {
|
|
57
81
|
runbook.push(`# Required secrets:`, `# ${requiredSecrets.map((s) => `${s.name}: ${s.hint}`).join("\n# ")}`, `railway variables set ${requiredSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
@@ -63,20 +87,24 @@ export function planRailwayDeploy(input) {
|
|
|
63
87
|
// credential's value (OAuth or a stored key) to set it — same wording discipline as the Fly plan.
|
|
64
88
|
if (!isEnvKey(modelAuth)) {
|
|
65
89
|
runbook.push(modelAuth === undefined
|
|
66
|
-
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/auth.json), or \`--run\` carries it automatically.`
|
|
90
|
+
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
|
|
67
91
|
: `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a variable.`, `# Set your provider API key as a variable (railway variables set KEY=...), OR place auth.json on the ${MOUNT} volume.`);
|
|
68
92
|
}
|
|
69
|
-
|
|
70
|
-
runbook.push(``, `# Repo-as-workspace: point the service at the kit's config file BEFORE the first deploy —`, `# dashboard-only, like App Sleeping (no CLI flag): Service → Settings → Config-as-code →`, `# set the file path to ${configPath}. Without it Railway would read the repo root's own config.`);
|
|
71
|
-
}
|
|
93
|
+
runbook.push(``, `# OPTIONAL — the build already uses the agent's Dockerfile via RAILWAY_DOCKERFILE_PATH (set above),`, `# and Railway's default restart policy equals what ${configPath} declares (ON_FAILURE).`, `# Pointing the service at ${configPath} (Service → Settings → Config-as-code — dashboard-only) adds`, `# the /health healthcheck gate: a boot-crashing deploy is marked FAILED instead of going live dead.`, `# (Zero-downtime switching doesn't apply either way — the ${MOUNT} volume allows one active deployment.)`);
|
|
72
94
|
runbook.push(``, `# Deploy — uploads this dir and builds the Dockerfile on Railway (no local Docker needed). This is`, `# also the ENTIRE redeploy: re-run \`railway up\` alone (the one-time setup above is not repeated).`, `railway up`);
|
|
73
|
-
if (input.
|
|
74
|
-
runbook.push(``, `#
|
|
95
|
+
if (input.shipsGit) {
|
|
96
|
+
runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. Freshness/durability run through git, driven`, `# by the agent itself (pull to freshen, commit/push to write back; creds ride config.deploy.secrets;`, `# git is baked into the image). CAVEAT — \`railway up\` is known to strip .git from its upload:`, `# expect NO baked history on the box; the agent should \`git clone\` its repo in the workspace`, `# (same token) before making changes.`, `# Un-pushed changes on the box never survive a redeploy; durability lives in git.`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. No .git here, so no history ships and the`, `# generated image does not install git — changes on the box are ephemeral and never survive a`, `# redeploy. If the agent should clone/push repos as part of its work, add deploy: { apt: ["git"] }.`);
|
|
75
100
|
}
|
|
76
101
|
// The public URL is minted, not deterministic (unlike Fly's <app>.fly.dev) — ONE mint step, then each
|
|
77
102
|
// channel's webhook uses that domain (mint once even when both channels are present).
|
|
78
103
|
const hasFeishuCloudChannel = ["feishu", "lark"].some((kind) => channels.includes(kind));
|
|
79
|
-
if (channels.includes("telegram") ||
|
|
104
|
+
if (channels.includes("telegram") ||
|
|
105
|
+
channels.includes("github") ||
|
|
106
|
+
channels.includes("slack") ||
|
|
107
|
+
hasFeishuCloudChannel) {
|
|
80
108
|
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
109
|
}
|
|
82
110
|
if (channels.includes("telegram")) {
|
|
@@ -85,8 +113,11 @@ export function planRailwayDeploy(input) {
|
|
|
85
113
|
if (channels.includes("github")) {
|
|
86
114
|
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
115
|
}
|
|
116
|
+
if (channels.includes("slack")) {
|
|
117
|
+
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`);
|
|
118
|
+
}
|
|
88
119
|
for (const kind of ["feishu", "lark"]) {
|
|
89
|
-
if (!channels.includes(kind))
|
|
120
|
+
if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
|
|
90
121
|
continue;
|
|
91
122
|
const label = kind === "feishu" ? "Feishu" : "Lark";
|
|
92
123
|
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 +129,8 @@ export function planRailwayDeploy(input) {
|
|
|
98
129
|
? `# Scale-to-zero: do NOT enable App Sleeping — github turns have no replay, a sleep mid-review is lost.`
|
|
99
130
|
: input.hasTimeTriggers
|
|
100
131
|
? `# Scale-to-zero: do NOT enable App Sleeping — schedules/wake-ups have no external wake-up; a sleeping service sleeps through them.`
|
|
101
|
-
:
|
|
132
|
+
: (input.longConnectionChannels?.length ?? 0) > 0
|
|
133
|
+
? `# Scale-to-zero: do NOT enable App Sleeping — a long-connection channel must remain connected.`
|
|
134
|
+
: `# 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
135
|
return { artifacts, runbook };
|
|
103
136
|
}
|
|
@@ -30,7 +30,8 @@ export interface RailwayRunPlan {
|
|
|
30
30
|
/** Names both the project (`railway init --name`) and the service (`railway add --service`). Railway
|
|
31
31
|
* names are project-scoped, not globally unique — the CLI derives it from the dir basename. */
|
|
32
32
|
name: string;
|
|
33
|
-
/** The volume mount path
|
|
33
|
+
/** The volume mount path; `FASTAGENT_STATE_DIR`/`FASTAGENT_SECRETS_DIR` are set to `.state`/`.secrets`
|
|
34
|
+
* under it (kept in lockstep). `/data`, matching the Fly recipe. */
|
|
34
35
|
mountPath: string;
|
|
35
36
|
/** `KEY=value` secrets set one-per-`variable set --stdin`: model key (env auth) or `FASTAGENT_AUTH_SEED`
|
|
36
37
|
* (file auth) + channel secrets. Never on argv. */
|
|
@@ -38,10 +39,17 @@ export interface RailwayRunPlan {
|
|
|
38
39
|
/** Required secret names with NO local value — the run gates on these before any side effect. */
|
|
39
40
|
missingSecrets: string[];
|
|
40
41
|
channels: ChannelKind[];
|
|
42
|
+
longConnectionChannels?: string[];
|
|
41
43
|
/** Opt-in (CLI `--into-linked`) to provision INTO the project this directory is already linked to. Off
|
|
42
44
|
* by default so `--run` only creates on an unlinked dir and never deploys into a pre-existing (possibly
|
|
43
45
|
* unrelated/production) project; the flag is the operator's explicit "yes, this project". */
|
|
44
46
|
intoLinked: boolean;
|
|
47
|
+
/** `RAILWAY_DOCKERFILE_PATH` value (`/fastagent/Dockerfile`) — Railway's service-variable route to a
|
|
48
|
+
* non-root Dockerfile (builds/dockerfiles docs), set with the machinery variables BEFORE the first
|
|
49
|
+
* `up` so the build never falls back to auto-detecting the workspace root. The config-as-code file
|
|
50
|
+
* also carries the path, but pointing Railway at `fastagent/railway.json` is dashboard-only — the
|
|
51
|
+
* variable is the scriptable way. */
|
|
52
|
+
dockerfilePath: string;
|
|
45
53
|
}
|
|
46
54
|
/** Done (with the live URL), or a gate the operator must clear before re-running (printed + non-zero
|
|
47
55
|
* exit by the CLI). */
|
|
@@ -76,9 +84,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
|
|
|
76
84
|
*/
|
|
77
85
|
export declare function parseHasVolume(stdout: string, mountPath: string): boolean;
|
|
78
86
|
/**
|
|
79
|
-
* Run the deploy through `railway`. `log` reports progress;
|
|
80
|
-
*
|
|
81
|
-
*
|
|
87
|
+
* Run the deploy through `railway`. `log` reports progress; the injected Telegram/Feishu/Slack
|
|
88
|
+
* registrars perform post-deploy webhook steps from the builder machine (Slack's control credential
|
|
89
|
+
* never travels to the host). Absent, the manual console
|
|
82
90
|
* instruction is printed. Every gate is fail-visible.
|
|
83
91
|
*/
|
|
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>;
|
|
92
|
+
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
|
|
@@ -128,8 +128,13 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
|
|
|
128
128
|
// volume (which has no --service and would otherwise attach to that service) — so the mismatch fails
|
|
129
129
|
// visibly with NO side effect. State root on argv (not secret); secrets one-per-`set --stdin` (value
|
|
130
130
|
// on stdin, never argv). Idempotent. (Order vs the volume is free — both just need to precede `up`.)
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
const machineryVars = [
|
|
132
|
+
`FASTAGENT_STATE_DIR=${plan.mountPath}/.state`,
|
|
133
|
+
`FASTAGENT_SECRETS_DIR=${plan.mountPath}/.secrets`,
|
|
134
|
+
`RAILWAY_DOCKERFILE_PATH=${plan.dockerfilePath}`,
|
|
135
|
+
];
|
|
136
|
+
log(`setting ${machineryVars.map((v) => v.split("=")[0]).join("/")} + ${Object.keys(plan.secrets).length} secret(s)…`);
|
|
137
|
+
if ((await railway(["variables", "set", ...machineryVars, ...svc])).code !== 0) {
|
|
133
138
|
return gate("`railway variables set` failed — see the railway output above");
|
|
134
139
|
}
|
|
135
140
|
for (const [k, v] of Object.entries(plan.secrets)) {
|
|
@@ -179,8 +184,18 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
|
|
|
179
184
|
log(`github: set the webhook in the repo (Settings → Webhooks) → ${url}/webhook`);
|
|
180
185
|
reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
|
|
181
186
|
}
|
|
187
|
+
if (plan.channels.includes("slack")) {
|
|
188
|
+
if (registerSlack) {
|
|
189
|
+
log("registering slack event URL…");
|
|
190
|
+
reg.track("slack", await registerSlack(url));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
|
|
194
|
+
reg.track("slack", "manual");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
182
197
|
for (const kind of ["feishu", "lark"]) {
|
|
183
|
-
if (!plan.channels.includes(kind))
|
|
198
|
+
if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
|
|
184
199
|
continue;
|
|
185
200
|
if (registerFeishu) {
|
|
186
201
|
log(`registering ${kind} event URL…`);
|
package/dist/deploy/runner.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
interface RunResult {
|
|
2
2
|
code: number;
|
|
3
|
-
/** Captured stdout (for `--json` queries); empty when the command streamed to the terminal.
|
|
4
|
-
* stderr is always inherited straight to the terminal, so it is not a field here. */
|
|
3
|
+
/** Captured stdout (for `--json` queries); empty when the command streamed to the terminal. */
|
|
5
4
|
stdout: string;
|
|
5
|
+
/** Captured stderr — ONLY when `captureStderr` was set (a caller that must CLASSIFY a failure, e.g.
|
|
6
|
+
* "not found" vs "denied"); otherwise undefined and stderr streams to the terminal as always. */
|
|
7
|
+
stderr?: string;
|
|
6
8
|
}
|
|
7
9
|
/** Run `bin args`: `capture` collects stdout (for `--json` queries), else the command streams to the
|
|
8
10
|
* terminal (create/deploy) and stdout is empty; `input` is fed to stdin (secrets over stdin, never argv).
|
|
@@ -10,6 +12,7 @@ export interface RunResult {
|
|
|
10
12
|
* values in argv or mutating the long-lived CLI process. */
|
|
11
13
|
export type CliRunner = (args: string[], opts?: {
|
|
12
14
|
capture?: boolean;
|
|
15
|
+
captureStderr?: boolean;
|
|
13
16
|
input?: string;
|
|
14
17
|
env?: NodeJS.ProcessEnv;
|
|
15
18
|
}) => Promise<RunResult>;
|
|
@@ -19,3 +22,4 @@ export type CliRunner = (args: string[], opts?: {
|
|
|
19
22
|
* ENOENT (the CLI not on PATH) resolves to code 127 so the caller can gate with an install hint.
|
|
20
23
|
*/
|
|
21
24
|
export declare function spawnRunner(bin: string, cwd: string): CliRunner;
|
|
25
|
+
export {};
|
package/dist/deploy/runner.js
CHANGED
|
@@ -14,13 +14,19 @@ export function spawnRunner(bin, cwd) {
|
|
|
14
14
|
const child = spawn(bin, args, {
|
|
15
15
|
cwd,
|
|
16
16
|
env: opts?.env ? { ...process.env, ...opts.env } : process.env,
|
|
17
|
-
stdio: [
|
|
17
|
+
stdio: [
|
|
18
|
+
opts?.input ? "pipe" : "inherit",
|
|
19
|
+
opts?.capture ? "pipe" : "inherit",
|
|
20
|
+
opts?.captureStderr ? "pipe" : "inherit",
|
|
21
|
+
],
|
|
18
22
|
});
|
|
19
23
|
let out = "";
|
|
24
|
+
let err = "";
|
|
20
25
|
child.stdout?.on("data", (d) => (out += String(d)));
|
|
26
|
+
child.stderr?.on("data", (d) => (err += String(d)));
|
|
21
27
|
if (opts?.input)
|
|
22
28
|
child.stdin?.end(opts.input);
|
|
23
|
-
child.on("close", (code) => res({ code: code ?? 1, stdout: out }));
|
|
24
|
-
child.on("error", () => res({ code: 127, stdout: "" })); // ENOENT
|
|
29
|
+
child.on("close", (code) => res({ code: code ?? 1, stdout: out, stderr: opts?.captureStderr ? err : undefined }));
|
|
30
|
+
child.on("error", () => res({ code: 127, stdout: "", stderr: opts?.captureStderr ? "" : undefined })); // ENOENT
|
|
25
31
|
});
|
|
26
32
|
}
|
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,14 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const WATCHED_HINT = "tools/, channels/, package.json (agent dir), fastagent.config.*, .env (run root)";
|
|
1
|
+
import { type ResolvedPlacement } from "./paths.ts";
|
|
3
2
|
/**
|
|
4
|
-
* chokidar `ignored` matcher for the narrow watch scope (true = ignore)
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
|
|
4
|
+
* the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
|
|
5
|
+
* when the agent IS the workspace the root is that tree, and the allowlist below is what keeps the
|
|
6
|
+
* author's own files out of scope. Ignoring a directory prunes the whole subtree, so everything outside it —
|
|
7
|
+
* `.state/` machine state, node_modules, .git, and any file/dir the agent writes as work product —
|
|
8
|
+
* costs no watchers and triggers no restarts. Helper code imported from OUTSIDE tools//channels/ is
|
|
9
|
+
* out of scope by design (keep it under tools/, or restart manually) — the startup log names the set.
|
|
9
10
|
*/
|
|
10
|
-
export declare function devWatchIgnored(
|
|
11
|
-
/** Spawn the dev worker and restart it on
|
|
12
|
-
export declare function runDevSupervisor(
|
|
11
|
+
export declare function devWatchIgnored(root: string, envFile: string): (path: string) => boolean;
|
|
12
|
+
/** Spawn the dev worker and restart it on agent-dir edits; supervise its lifecycle until the process exits. */
|
|
13
|
+
export declare function runDevSupervisor(placement: ResolvedPlacement, options?: {
|
|
13
14
|
tunnel?: boolean;
|
|
14
15
|
}): Promise<void>;
|
package/dist/dev-supervisor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `fastagent dev` process supervisor: re-spawn the CLI as a worker (`FASTAGENT_DEV_WORKER=1`) and
|
|
3
|
-
* restart it on debounced edits to the
|
|
3
|
+
* restart it on debounced edits to the agent's CODE inputs. Each restart is a fresh process
|
|
4
4
|
* (always-latest, no stale module cache). The supervisor never exits on a bad edit — the worker fails
|
|
5
5
|
* loudly and it waits for the next save.
|
|
6
6
|
*
|
|
@@ -13,65 +13,61 @@
|
|
|
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 {
|
|
16
|
+
import { AGENT_CONFIG_NAMES, resolveStateRoot } from "./paths.js";
|
|
17
|
+
import { isUnderDir } from "./engines/pi/definition.js";
|
|
18
|
+
import { dotEnvPath } from "./env.js";
|
|
17
19
|
import { log } from "./log.js";
|
|
18
20
|
import { installProxyFetch } from "./proxy.js";
|
|
19
21
|
import { openExternalUrl } from "./open-url.js";
|
|
20
22
|
import { announceWebhooks, startCloudflareTunnel } from "./tunnel.js";
|
|
21
|
-
/** What the dev watcher restarts on (
|
|
22
|
-
|
|
23
|
+
/** What the dev watcher restarts on (agent-dir-relative): the process-bound code inputs only. */
|
|
24
|
+
const WATCHED_HINT = "tools/, channels/, schedules/, package.json, fastagent.config.*, .secrets/.env";
|
|
23
25
|
/**
|
|
24
|
-
* chokidar `ignored` matcher for the narrow watch scope (true = ignore)
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
|
|
27
|
+
* the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
|
|
28
|
+
* when the agent IS the workspace the root is that tree, and the allowlist below is what keeps the
|
|
29
|
+
* author's own files out of scope. Ignoring a directory prunes the whole subtree, so everything outside it —
|
|
30
|
+
* `.state/` machine state, node_modules, .git, and any file/dir the agent writes as work product —
|
|
31
|
+
* costs no watchers and triggers no restarts. Helper code imported from OUTSIDE tools//channels/ is
|
|
32
|
+
* out of scope by design (keep it under tools/, or restart manually) — the startup log names the set.
|
|
29
33
|
*/
|
|
30
|
-
export function devWatchIgnored(
|
|
34
|
+
export function devWatchIgnored(root, envFile) {
|
|
35
|
+
// The `.env` is allow-listed by its RESOLVED path, not by the `.secrets` name: FASTAGENT_SECRETS_DIR
|
|
36
|
+
// can put it in an in-agent directory called anything, and a name-based rule would prune the very
|
|
37
|
+
// file the worker loads (a credential edit would then silently never restart it).
|
|
38
|
+
const envRel = relative(root, envFile).split(sep);
|
|
31
39
|
return (path) => {
|
|
32
|
-
if (path ===
|
|
33
|
-
return false; // the
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
40
|
+
if (path === root)
|
|
41
|
+
return false; // the root itself must not be pruned
|
|
42
|
+
const rel = relative(root, path);
|
|
43
|
+
// Code inputs at the agent dir root: config, package.json, and the dirs loaded once per worker
|
|
44
|
+
// (a restart is their only re-read). Everything else (skills/, persona.md, AGENTS.md) is
|
|
45
|
+
// live-read — pruned, no restart.
|
|
46
|
+
// The config NAMES come from paths.ts, not a regex spelled here: adding a name there must not
|
|
47
|
+
// silently stop `dev` restarting on edits to it.
|
|
48
|
+
if (AGENT_CONFIG_NAMES.includes(rel))
|
|
37
49
|
return false;
|
|
38
|
-
|
|
39
|
-
// Run-root (cwd) inputs: .env + fastagent.config.* live where config lives, not in agentDir.
|
|
40
|
-
if (rel === ".env")
|
|
50
|
+
if (rel === "package.json")
|
|
41
51
|
return false;
|
|
42
|
-
|
|
52
|
+
const segments = rel.split(sep);
|
|
53
|
+
if (segments[0] === "tools" || segments[0] === "channels" || segments[0] === "schedules")
|
|
54
|
+
return false;
|
|
55
|
+
// The `.env` restarts too (credentials are process-bound). Keep it AND its ancestor directories
|
|
56
|
+
// un-pruned so chokidar can descend to it; every sibling inside them (auth.json, .env.example)
|
|
57
|
+
// prunes normally. An out-of-agent `.env` yields a `..`-prefixed envRel that matches nothing here
|
|
58
|
+
// — the supervisor warns about that case instead of pretending to watch it.
|
|
59
|
+
if (segments.length <= envRel.length && segments.every((seg, i) => seg === envRel[i]))
|
|
43
60
|
return false;
|
|
44
|
-
// Agent code inputs live in agentDir: tools/, channels/, schedules/ (loaded once per worker — a
|
|
45
|
-
// restart is their only re-read), package.json (its own deps). Everything else under agentDir
|
|
46
|
-
// (skills/, persona.md, AGENTS.md) is live-read — pruned, no restart.
|
|
47
|
-
const relAgent = relative(agentDir, path);
|
|
48
|
-
if (!relAgent.startsWith("..")) {
|
|
49
|
-
const [head] = relAgent.split(sep);
|
|
50
|
-
if (head === "tools" || head === "channels" || head === "schedules")
|
|
51
|
-
return false;
|
|
52
|
-
if (relAgent === "package.json")
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
61
|
return true;
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
|
-
/** Spawn the dev worker and restart it on
|
|
59
|
-
export async function runDevSupervisor(
|
|
60
|
-
// The
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// of scope for watch-scope re-sync (it triggers a worker restart regardless).
|
|
66
|
-
// A genuine config error (not just a missing agentDir key) — debug-log it here so the silence is not
|
|
67
|
-
// total before the spawned worker crash-loops and surfaces the real message; default agentDir=dir.
|
|
68
|
-
const config = await loadConfig(dir)
|
|
69
|
-
.then((r) => r.config)
|
|
70
|
-
.catch((err) => {
|
|
71
|
-
log.debug(`[fastagent] dev: config load failed while resolving agentDir (worker will report): ${String(err)}`);
|
|
72
|
-
return {};
|
|
73
|
-
});
|
|
74
|
-
const agentDir = resolveAgentDir(dir, config);
|
|
64
|
+
/** Spawn the dev worker and restart it on agent-dir edits; supervise its lifecycle until the process exits. */
|
|
65
|
+
export async function runDevSupervisor(placement, options = {}) {
|
|
66
|
+
// The placement arrives RESOLVED from the command (which already routed its refusal through
|
|
67
|
+
// failStartup): re-resolving here would duplicate the rule and surface the same user-fixable
|
|
68
|
+
// refusal as a raw stack. The watch root is the AGENT DIR — every restart-relevant code input lives
|
|
69
|
+
// under it, so the surrounding workspace costs no watchers at all. Placement is assumed STATIC for
|
|
70
|
+
// the session (creating/removing `fastagent/` mid-session is out of scope for watch re-sync).
|
|
75
71
|
let worker;
|
|
76
72
|
let reloadPending = false;
|
|
77
73
|
let everServed = false; // has any worker successfully bound (sent `ready`) yet?
|
|
@@ -100,7 +96,11 @@ export async function runDevSupervisor(dir, options = {}) {
|
|
|
100
96
|
void startCloudflareTunnel(m.port).then((t) => {
|
|
101
97
|
if (t) {
|
|
102
98
|
tunnel = t;
|
|
103
|
-
void announceWebhooks(agentDir, t.url, {
|
|
99
|
+
void announceWebhooks(placement.agentDir, t.url, {
|
|
100
|
+
openUrl: openExternalUrl,
|
|
101
|
+
routeChannels: m.routeChannels,
|
|
102
|
+
stateRoot: resolveStateRoot(placement.agentDir),
|
|
103
|
+
});
|
|
104
104
|
}
|
|
105
105
|
});
|
|
106
106
|
}
|
|
@@ -137,9 +137,9 @@ export async function runDevSupervisor(dir, options = {}) {
|
|
|
137
137
|
};
|
|
138
138
|
// chokidar gives reliable cross-platform recursion + structural ignore that native fs.watch
|
|
139
139
|
// cannot; devWatchIgnored (above) narrows the scope to the process-bound code inputs.
|
|
140
|
-
const watcher = watchTree(
|
|
140
|
+
const watcher = watchTree(placement.agentDir, {
|
|
141
141
|
ignoreInitial: true, // the startup scan is not a change
|
|
142
|
-
ignored: devWatchIgnored(
|
|
142
|
+
ignored: devWatchIgnored(placement.agentDir, dotEnvPath(placement.agentDir)),
|
|
143
143
|
});
|
|
144
144
|
watcher.on("all", () => {
|
|
145
145
|
clearTimeout(timer);
|
|
@@ -147,6 +147,12 @@ export async function runDevSupervisor(dir, options = {}) {
|
|
|
147
147
|
});
|
|
148
148
|
watcher.on("error", (error) => log.warn(`[fastagent] file watching error (${error.message}); some edits may need a manual restart`));
|
|
149
149
|
log.info(`[fastagent] watching ${WATCHED_HINT} — code edits restart the dev worker (--no-watch to disable); AGENTS.md/persona.md/skills edits go live next turn without a restart`);
|
|
150
|
+
// FASTAGENT_SECRETS_DIR can move the `.env` OUT of the agent dir entirely; the watcher follows it
|
|
151
|
+
// anywhere inside (the resolved path is allow-listed above), but outside the watch root the worker
|
|
152
|
+
// would load a file no watcher sees. Say so once instead of leaving the hint above lying.
|
|
153
|
+
if (!isUnderDir(dotEnvPath(placement.agentDir), placement.agentDir)) {
|
|
154
|
+
log.warn(`[fastagent] .env lives outside the agent dir (FASTAGENT_SECRETS_DIR → ${dotEnvPath(placement.agentDir)}) — it is NOT watched; restart dev after editing it`);
|
|
155
|
+
}
|
|
150
156
|
const shutdown = () => {
|
|
151
157
|
worker?.kill("SIGTERM");
|
|
152
158
|
tunnel?.close();
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { CredentialStore } from "@earendil-works/pi-ai";
|
|
2
2
|
/**
|
|
3
|
-
* The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery
|
|
4
|
+
* home `~/.fastagent/` — which carries the same unified shape as a workspace (`.secrets/auth.json`).
|
|
5
|
+
* The project-level default is `<agentDir>/.secrets/auth.json` (computed by the opener and by
|
|
6
|
+
* `fastagent login`); this is only the `loginFlow()` PROGRAMMATIC fallback (when a caller omits
|
|
7
|
+
* `authPath`) and the path to point `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE
|
|
8
|
+
* credential file across projects (safe — one file, one lock-serialized refresh lifecycle). The
|
|
9
|
+
* `fastagent login` CLI is project-level by default, never this.
|
|
9
10
|
*/
|
|
10
11
|
export declare const GLOBAL_AUTH_PATH: string;
|
|
11
12
|
export interface FastagentAuthOptions {
|
|
@@ -13,5 +14,5 @@ export interface FastagentAuthOptions {
|
|
|
13
14
|
warn?: (message: string) => void;
|
|
14
15
|
}
|
|
15
16
|
/** A read-write `CredentialStore` backed by the given credentials file (default {@link GLOBAL_AUTH_PATH};
|
|
16
|
-
* the directory opener passes the project-level `<
|
|
17
|
+
* the directory opener passes the project-level `<root>/.secrets/auth.json`). */
|
|
17
18
|
export declare function fastagentCredentialStore(authPath?: string, options?: FastagentAuthOptions): CredentialStore;
|