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