@fastagent-sh/fastagent 0.12.0 → 0.13.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 +42 -36
- package/dist/channels/feishu/bootstrap-token.d.ts +42 -0
- package/dist/channels/feishu/bootstrap-token.js +94 -0
- package/dist/channels/feishu/card.d.ts +32 -0
- package/dist/channels/feishu/card.js +66 -0
- package/dist/channels/feishu/cloud.d.ts +17 -0
- package/dist/channels/feishu/cloud.js +19 -0
- package/dist/channels/feishu/crypto.d.ts +13 -0
- package/dist/channels/feishu/crypto.js +41 -0
- package/dist/channels/feishu/feishu-api.d.ts +108 -0
- package/dist/channels/feishu/feishu-api.js +325 -0
- package/dist/channels/feishu/feishu.d.ts +36 -0
- package/dist/channels/feishu/feishu.js +359 -0
- package/dist/channels/feishu/invoke-turn.d.ts +59 -0
- package/dist/channels/feishu/invoke-turn.js +106 -0
- package/dist/channels/feishu/parse.d.ts +125 -0
- package/dist/channels/feishu/parse.js +175 -0
- package/dist/channels/feishu/preview.d.ts +36 -0
- package/dist/channels/feishu/preview.js +387 -0
- package/dist/channels/feishu/register-app.d.ts +70 -0
- package/dist/channels/feishu/register-app.js +141 -0
- package/dist/channels/feishu/register-webhook.d.ts +22 -0
- package/dist/channels/feishu/register-webhook.js +106 -0
- package/dist/channels/feishu/scaffold/channel.ts +34 -0
- package/dist/channels/feishu/scaffold/feishu-send.ts +87 -0
- package/dist/channels/feishu/seen.d.ts +5 -0
- package/dist/channels/feishu/seen.js +47 -0
- package/dist/channels/feishu/text.d.ts +13 -0
- package/dist/channels/feishu/text.js +63 -0
- package/dist/channels/lark/lark.d.ts +15 -0
- package/dist/channels/lark/lark.js +10 -0
- package/dist/channels/lark/onboard.d.ts +39 -0
- package/dist/channels/lark/onboard.js +58 -0
- package/dist/channels/lark/scaffold/channel.ts +32 -0
- package/dist/channels/lark/scaffold/lark-send.ts +87 -0
- package/dist/channels/registration.d.ts +15 -0
- package/dist/channels/registration.js +1 -0
- package/dist/channels/{telegram/state.js → state.js} +6 -4
- package/dist/channels/telegram/context-buffer.js +1 -1
- package/dist/channels/telegram/register-webhook.d.ts +4 -1
- package/dist/channels/telegram/register-webhook.js +17 -26
- package/dist/channels/telegram/telegram.js +2 -2
- package/dist/channels/telegram/turn-store.d.ts +8 -21
- package/dist/channels/telegram/turn-store.js +11 -130
- package/dist/channels/{telegram/turn-queue.js → turn-queue.js} +3 -3
- package/dist/channels/turn-store.d.ts +42 -0
- package/dist/channels/turn-store.js +139 -0
- package/dist/channels/wait-health.d.ts +6 -0
- package/dist/channels/wait-health.js +27 -0
- package/dist/cli-add-feishu.d.ts +8 -0
- package/dist/cli-add-feishu.js +223 -0
- package/dist/cli.js +68 -23
- package/dist/deploy/container.js +10 -6
- package/dist/deploy/fly/plan.d.ts +1 -1
- package/dist/deploy/fly/plan.js +15 -4
- package/dist/deploy/fly/run.d.ts +7 -4
- package/dist/deploy/fly/run.js +26 -5
- package/dist/deploy/railway/plan.d.ts +1 -1
- package/dist/deploy/railway/plan.js +17 -5
- package/dist/deploy/railway/run.d.ts +6 -3
- package/dist/deploy/railway/run.js +26 -4
- package/dist/deploy/registration-gate.d.ts +20 -0
- package/dist/deploy/registration-gate.js +20 -0
- package/dist/deploy/secrets.d.ts +10 -9
- package/dist/deploy/secrets.js +15 -14
- package/dist/dev-supervisor.js +2 -1
- package/dist/engines/pi/chat.js +3 -3
- package/dist/engines/pi/create.d.ts +0 -1
- package/dist/engines/pi/create.js +8 -7
- package/dist/feishu.d.ts +2 -0
- package/dist/feishu.js +2 -0
- package/dist/lark.d.ts +3 -0
- package/dist/lark.js +3 -0
- package/dist/open-url.d.ts +2 -0
- package/dist/open-url.js +6 -0
- package/dist/scaffold/add-channel.d.ts +9 -5
- package/dist/scaffold/add-channel.js +73 -7
- package/dist/tunnel.d.ts +9 -6
- package/dist/tunnel.js +48 -31
- package/package.json +19 -8
- /package/dist/channels/{telegram/state.d.ts → state.d.ts} +0 -0
- /package/dist/channels/{telegram/turn-queue.d.ts → turn-queue.d.ts} +0 -0
|
@@ -28,7 +28,7 @@ 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
|
-
/** Channels discovered in the workspace — each contributes its
|
|
31
|
+
/** Channels discovered in the workspace — each contributes its secret metadata + webhook step. */
|
|
32
32
|
channels: ChannelKind[];
|
|
33
33
|
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
34
34
|
extraSecrets?: string[];
|
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { containerArtifacts } from "../container.js";
|
|
2
|
-
import {
|
|
2
|
+
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
3
|
function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers) {
|
|
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
|
|
@@ -60,7 +60,9 @@ export function planFlyDeploy(input) {
|
|
|
60
60
|
// model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
|
|
61
61
|
// COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
|
|
62
62
|
// `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
|
|
63
|
-
const secrets =
|
|
63
|
+
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
64
|
+
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
65
|
+
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
64
66
|
const deployCmd = kitDir
|
|
65
67
|
? `fly deploy . --config ${kitDir}/fly.toml --dockerfile ${kitDir}/Dockerfile --app ${appName}`
|
|
66
68
|
: `fly deploy --app ${appName}`;
|
|
@@ -77,8 +79,11 @@ export function planFlyDeploy(input) {
|
|
|
77
79
|
`# is the single source for the region; skip this if the volume exists (fly volumes list --app ${appName}):`,
|
|
78
80
|
`fly volumes create data --app ${appName} --region <region> --size 1`,
|
|
79
81
|
];
|
|
80
|
-
if (
|
|
81
|
-
runbook.push(``, `#
|
|
82
|
+
if (requiredSecrets.length > 0) {
|
|
83
|
+
runbook.push(``, `# Required secrets (replace each <value>):`, ...requiredSecrets.map((s) => `# ${s.name}: ${s.hint}`), `fly secrets set --app ${appName} ${requiredSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
84
|
+
}
|
|
85
|
+
if (optionalSecrets.length > 0) {
|
|
86
|
+
runbook.push(``, `# Optional secrets — set only when the matching feature is configured:`, ...optionalSecrets.map((s) => `# ${s.name}: ${s.hint}`), `# fly secrets set --app ${appName} ${optionalSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
82
87
|
}
|
|
83
88
|
if (kitDir) {
|
|
84
89
|
runbook.push(``, `# Repo-as-workspace: the build context is the REPO ROOT (the whole repo is the agent's cwd); the`, `# config/Dockerfile live under ${kitDir}/ so they never collide with the repo's own deploy files.`, `# Run this from the repo root:`);
|
|
@@ -106,6 +111,12 @@ export function planFlyDeploy(input) {
|
|
|
106
111
|
if (channels.includes("github")) {
|
|
107
112
|
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`);
|
|
108
113
|
}
|
|
114
|
+
for (const kind of ["feishu", "lark"]) {
|
|
115
|
+
if (!channels.includes(kind))
|
|
116
|
+
continue;
|
|
117
|
+
const label = kind === "feishu" ? "Feishu" : "Lark";
|
|
118
|
+
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}`);
|
|
119
|
+
}
|
|
109
120
|
if (post.length > 0)
|
|
110
121
|
runbook.push(``, ...post);
|
|
111
122
|
// Single-machine tier: state lives on ONE volume tied to ONE machine. Scaling to multiple machines
|
package/dist/deploy/fly/run.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* secrets / deploy) that the plain runbook hands to the operator; `--run` executes it instead, so a
|
|
4
4
|
* coding agent runs ONE command. Idempotent (app/volume check-then-act; channel secrets come from the
|
|
5
5
|
* local env — NOT minted — so a re-run sets the same values) and resumable: it STOPS at a human gate
|
|
6
|
-
* (not logged in, a missing secret value, a taken app name) with one actionable line and a non-zero
|
|
6
|
+
* (not logged in, a missing secret value, a taken app name, a failed webhook registration) with one actionable line and a non-zero
|
|
7
7
|
* exit, so the agent clears the gate and re-runs. A `generate` channel secret absent from `.env` is a
|
|
8
8
|
* gate too (`missingSecrets`), not a silent mint — fill it in `.env` (use the random string that
|
|
9
9
|
* `add <channel>` prints).
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
|
|
18
18
|
* never land in argv/process listings.
|
|
19
19
|
*/
|
|
20
|
+
import type { RegistrationOutcome } from "../../channels/registration.ts";
|
|
20
21
|
import type { ChannelKind } from "../../scaffold/add-channel.ts";
|
|
21
22
|
import type { CliRunner } from "../runner.ts";
|
|
22
23
|
/**
|
|
@@ -45,7 +46,9 @@ export type FlyRunOutcome = {
|
|
|
45
46
|
gate: string;
|
|
46
47
|
};
|
|
47
48
|
/**
|
|
48
|
-
* Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)`
|
|
49
|
-
* post-deploy webhook
|
|
49
|
+
* Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)` /
|
|
50
|
+
* `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
|
|
51
|
+
* Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
|
|
52
|
+
* instruction is printed. Every gate is fail-visible.
|
|
50
53
|
*/
|
|
51
|
-
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<
|
|
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>;
|
package/dist/deploy/fly/run.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registrationGate } from "../registration-gate.js";
|
|
1
2
|
/**
|
|
2
3
|
* The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
|
|
3
4
|
* FASTAGENT_AUTH_SEED materialization. ABSENT-ONLY by design: a present file (a refreshed volume copy)
|
|
@@ -18,10 +19,12 @@ function listHasName(stdout, name) {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
|
-
* Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)`
|
|
22
|
-
* post-deploy webhook
|
|
22
|
+
* Run the deploy through `fly`. `log` reports progress; `registerTelegram(baseUrl)` /
|
|
23
|
+
* `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
|
|
24
|
+
* Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
|
|
25
|
+
* instruction is printed. Every gate is fail-visible.
|
|
23
26
|
*/
|
|
24
|
-
export async function deployFlyRun(plan, fly, log, registerTelegram) {
|
|
27
|
+
export async function deployFlyRun(plan, fly, log, registerTelegram, registerFeishu) {
|
|
25
28
|
const gate = (g) => ({ ok: false, gate: g });
|
|
26
29
|
// 1. Auth is the one gate a coding agent can't clear itself (browser OAuth). `whoami` succeeds with
|
|
27
30
|
// either an interactive login or FLY_API_TOKEN, so one check covers both.
|
|
@@ -77,13 +80,31 @@ export async function deployFlyRun(plan, fly, log, registerTelegram) {
|
|
|
77
80
|
return gate("`fly deploy` failed — see the flyctl output above; fix and re-run");
|
|
78
81
|
}
|
|
79
82
|
// 7. Post-deploy webhook — telegram end-to-end (fastagent has the token + the live URL); github is a
|
|
80
|
-
// repo-settings step only a human can do.
|
|
83
|
+
// repo-settings step only a human can do. Gate policy is the shared registration-gate kernel
|
|
84
|
+
// (registrars report facts, it owns the policy); all channels are attempted first.
|
|
85
|
+
const reg = registrationGate(log, "re-run to retry registration (steps already done are skipped)");
|
|
81
86
|
if (plan.channels.includes("telegram")) {
|
|
82
87
|
log("registering telegram webhook…");
|
|
83
|
-
await registerTelegram(`https://${plan.appName}.fly.dev`);
|
|
88
|
+
reg.track("telegram", await registerTelegram(`https://${plan.appName}.fly.dev`));
|
|
84
89
|
}
|
|
85
90
|
if (plan.channels.includes("github")) {
|
|
86
91
|
log(`github: set the webhook in the repo (Settings → Webhooks) → https://${plan.appName}.fly.dev/webhook`);
|
|
92
|
+
reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
|
|
87
93
|
}
|
|
94
|
+
for (const kind of ["feishu", "lark"]) {
|
|
95
|
+
if (!plan.channels.includes(kind))
|
|
96
|
+
continue;
|
|
97
|
+
if (registerFeishu) {
|
|
98
|
+
log(`registering ${kind} event URL…`);
|
|
99
|
+
reg.track(kind, await registerFeishu(`https://${plan.appName}.fly.dev`, kind));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
log(`${kind}: set the event Request URL in the developer console (Events & Callbacks) → https://${plan.appName}.fly.dev/${kind} (the app must be running when you save)`);
|
|
103
|
+
reg.track(kind, "manual"); // no registrar wired — the console step above is the operator's
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const registrationGateMsg = reg.gate();
|
|
107
|
+
if (registrationGateMsg)
|
|
108
|
+
return gate(registrationGateMsg);
|
|
88
109
|
return { ok: true };
|
|
89
110
|
}
|
|
@@ -29,7 +29,7 @@ export interface RailwayPlanInput extends ContainerInput {
|
|
|
29
29
|
serviceName: string;
|
|
30
30
|
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
31
31
|
modelAuth: string | undefined;
|
|
32
|
-
/** Channels discovered in the workspace — each contributes its
|
|
32
|
+
/** Channels discovered in the workspace — each contributes its secret metadata + webhook step. */
|
|
33
33
|
channels: ChannelKind[];
|
|
34
34
|
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
35
35
|
extraSecrets?: string[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { containerArtifacts } from "../container.js";
|
|
2
|
-
import {
|
|
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
5
|
/** railway.json — build/deploy only (Railway's config-as-code scope). No env/volume/sleeping here: those
|
|
@@ -23,7 +23,9 @@ export function planRailwayDeploy(input) {
|
|
|
23
23
|
{ path: configPath, content: railwayJson(input.kitDir) },
|
|
24
24
|
...containerArtifacts(input),
|
|
25
25
|
];
|
|
26
|
-
const secrets =
|
|
26
|
+
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
27
|
+
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
28
|
+
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
27
29
|
// Order matters, not cosmetics: `railway init` creates a PROJECT with no service, but the volume and
|
|
28
30
|
// variables are service-scoped and `railway up` deploys THE service — so the service must exist first
|
|
29
31
|
// (`railway add --service`), and variables must be set BEFORE the first `up` or the box boots without a
|
|
@@ -51,8 +53,11 @@ export function planRailwayDeploy(input) {
|
|
|
51
53
|
`# Variables — set BEFORE the first deploy so the box boots with them. Railway injects PORT itself.`,
|
|
52
54
|
`railway variables set FASTAGENT_STATE_DIR=${MOUNT}`,
|
|
53
55
|
];
|
|
54
|
-
if (
|
|
55
|
-
runbook.push(`# ${
|
|
56
|
+
if (requiredSecrets.length > 0) {
|
|
57
|
+
runbook.push(`# Required secrets:`, `# ${requiredSecrets.map((s) => `${s.name}: ${s.hint}`).join("\n# ")}`, `railway variables set ${requiredSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
58
|
+
}
|
|
59
|
+
if (optionalSecrets.length > 0) {
|
|
60
|
+
runbook.push(`# Optional secrets — set only when the matching feature is configured:`, `# ${optionalSecrets.map((s) => `${s.name}: ${s.hint}`).join("\n# ")}`, `# railway variables set ${optionalSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
56
61
|
}
|
|
57
62
|
// Model-auth guidance: an env key becomes a variable above. Otherwise the plan can't read the local
|
|
58
63
|
// credential's value (OAuth or a stored key) to set it — same wording discipline as the Fly plan.
|
|
@@ -70,7 +75,8 @@ export function planRailwayDeploy(input) {
|
|
|
70
75
|
}
|
|
71
76
|
// The public URL is minted, not deterministic (unlike Fly's <app>.fly.dev) — ONE mint step, then each
|
|
72
77
|
// channel's webhook uses that domain (mint once even when both channels are present).
|
|
73
|
-
|
|
78
|
+
const hasFeishuCloudChannel = ["feishu", "lark"].some((kind) => channels.includes(kind));
|
|
79
|
+
if (channels.includes("telegram") || channels.includes("github") || hasFeishuCloudChannel) {
|
|
74
80
|
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`);
|
|
75
81
|
}
|
|
76
82
|
if (channels.includes("telegram")) {
|
|
@@ -79,6 +85,12 @@ export function planRailwayDeploy(input) {
|
|
|
79
85
|
if (channels.includes("github")) {
|
|
80
86
|
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`);
|
|
81
87
|
}
|
|
88
|
+
for (const kind of ["feishu", "lark"]) {
|
|
89
|
+
if (!channels.includes(kind))
|
|
90
|
+
continue;
|
|
91
|
+
const label = kind === "feishu" ? "Feishu" : "Lark";
|
|
92
|
+
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}`);
|
|
93
|
+
}
|
|
82
94
|
// Scale-to-zero: App Sleeping is dashboard-only (no CLI/API) — a manual step, not a generated setting.
|
|
83
95
|
// A github channel should NOT enable it: fire-and-forget reviews have no replay (unlike Telegram's L1
|
|
84
96
|
// turn store), so a sleep mid-review would drop it — the same floor the Fly plan enforces via config.
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
|
|
24
24
|
* `RAILWAY_API_KEY`), not a project token: `init` creates a project that a project token can't predate.
|
|
25
25
|
*/
|
|
26
|
+
import type { RegistrationOutcome } from "../../channels/registration.ts";
|
|
26
27
|
import type { ChannelKind } from "../../scaffold/add-channel.ts";
|
|
27
28
|
import type { CliRunner } from "../runner.ts";
|
|
28
29
|
export interface RailwayRunPlan {
|
|
@@ -75,7 +76,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
|
|
|
75
76
|
*/
|
|
76
77
|
export declare function parseHasVolume(stdout: string, mountPath: string): boolean;
|
|
77
78
|
/**
|
|
78
|
-
* Run the deploy through `railway`. `log` reports progress; `registerTelegram(baseUrl)`
|
|
79
|
-
* post-deploy webhook
|
|
79
|
+
* Run the deploy through `railway`. `log` reports progress; `registerTelegram(baseUrl)` /
|
|
80
|
+
* `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
|
|
81
|
+
* Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
|
|
82
|
+
* instruction is printed. Every gate is fail-visible.
|
|
80
83
|
*/
|
|
81
|
-
export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<
|
|
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>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registrationGate } from "../registration-gate.js";
|
|
1
2
|
/**
|
|
2
3
|
* Whether `railway status --json` shows a linked project: non-empty stdout. Unlinked prints its message
|
|
3
4
|
* to stderr and leaves stdout EMPTY (the exit code is 0 either way, so it can't be the signal). ANY
|
|
@@ -61,10 +62,12 @@ export function parseHasVolume(stdout, mountPath) {
|
|
|
61
62
|
return jsonStrings(stdout).includes(mountPath);
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
|
-
* Run the deploy through `railway`. `log` reports progress; `registerTelegram(baseUrl)`
|
|
65
|
-
* post-deploy webhook
|
|
65
|
+
* Run the deploy through `railway`. `log` reports progress; `registerTelegram(baseUrl)` /
|
|
66
|
+
* `registerFeishu(baseUrl, kind)` perform the post-deploy webhook steps (the CLI passes its canonical
|
|
67
|
+
* Feishu registrar, which also serves the Lark compatibility profile). Absent, the manual console
|
|
68
|
+
* instruction is printed. Every gate is fail-visible.
|
|
66
69
|
*/
|
|
67
|
-
export async function deployRailwayRun(plan, railway, log, registerTelegram) {
|
|
70
|
+
export async function deployRailwayRun(plan, railway, log, registerTelegram, registerFeishu) {
|
|
68
71
|
const gate = (g) => ({ ok: false, gate: g });
|
|
69
72
|
// Every --service below targets plan.name — the name this tool gives BOTH the project and the service
|
|
70
73
|
// (`init --name` + `add --service`). On a fresh create they match; on `--into-linked` into a hand-made
|
|
@@ -165,12 +168,31 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram) {
|
|
|
165
168
|
if (!url) {
|
|
166
169
|
return gate("couldn't read a domain from `railway domain` — run `railway domain` manually, then set any webhook");
|
|
167
170
|
}
|
|
171
|
+
// 7. Post-deploy webhook — gate policy is the shared registration-gate kernel (registrars report
|
|
172
|
+
// facts, it owns the policy); all channels are attempted first.
|
|
173
|
+
const reg = registrationGate(log, "re-run with --into-linked to retry registration");
|
|
168
174
|
if (plan.channels.includes("telegram")) {
|
|
169
175
|
log("registering telegram webhook…");
|
|
170
|
-
await registerTelegram(url);
|
|
176
|
+
reg.track("telegram", await registerTelegram(url));
|
|
171
177
|
}
|
|
172
178
|
if (plan.channels.includes("github")) {
|
|
173
179
|
log(`github: set the webhook in the repo (Settings → Webhooks) → ${url}/webhook`);
|
|
180
|
+
reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
|
|
181
|
+
}
|
|
182
|
+
for (const kind of ["feishu", "lark"]) {
|
|
183
|
+
if (!plan.channels.includes(kind))
|
|
184
|
+
continue;
|
|
185
|
+
if (registerFeishu) {
|
|
186
|
+
log(`registering ${kind} event URL…`);
|
|
187
|
+
reg.track(kind, await registerFeishu(url, kind));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
log(`${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${url}/${kind} (the service must be running when you save)`);
|
|
191
|
+
reg.track(kind, "manual"); // no registrar wired — the console step above is the operator's
|
|
192
|
+
}
|
|
174
193
|
}
|
|
194
|
+
const registrationGateMsg = reg.gate();
|
|
195
|
+
if (registrationGateMsg)
|
|
196
|
+
return gate(registrationGateMsg);
|
|
175
197
|
return { ok: true, url };
|
|
176
198
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-NEUTRAL step-7 gate policy, shared by the host runners: the registrars report facts
|
|
3
|
+
* ({@link RegistrationOutcome}), this module owns what to do about them.
|
|
4
|
+
*
|
|
5
|
+
* - "failed" gates — exit 0 would tell a coding agent "done" while the deployed agent cannot receive
|
|
6
|
+
* messages, and the runners' earlier steps are idempotent so a re-run just retries registration.
|
|
7
|
+
* - "manual" does NOT gate — re-running can never change it (an unclearable gate would spin a coding
|
|
8
|
+
* agent forever) — but is re-surfaced after all registrar output (and before any gate message) so
|
|
9
|
+
* the operator cannot miss it under the registrar's earlier log lines.
|
|
10
|
+
*
|
|
11
|
+
* The runners attempt ALL channels first (one failure doesn't skip the rest), then apply this policy
|
|
12
|
+
* once. Only the retry remediation differs per host (`retryHint`).
|
|
13
|
+
*/
|
|
14
|
+
import type { RegistrationOutcome } from "../channels/registration.ts";
|
|
15
|
+
export declare function registrationGate(log: (msg: string) => void, retryHint: string): {
|
|
16
|
+
track: (kind: string, outcome: RegistrationOutcome) => void;
|
|
17
|
+
/** Logs the manual notices; returns the gate message (composes with cli.ts's "deploy stopped:"
|
|
18
|
+
* prefix — it leads with the failure, not "the deploy succeeded"), or undefined for no gate. */
|
|
19
|
+
gate: () => string | undefined;
|
|
20
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function registrationGate(log, retryHint) {
|
|
2
|
+
const unregistered = [];
|
|
3
|
+
const manual = [];
|
|
4
|
+
return {
|
|
5
|
+
track(kind, outcome) {
|
|
6
|
+
if (outcome === "failed")
|
|
7
|
+
unregistered.push(kind);
|
|
8
|
+
if (outcome === "manual")
|
|
9
|
+
manual.push(kind);
|
|
10
|
+
},
|
|
11
|
+
gate() {
|
|
12
|
+
for (const kind of manual) {
|
|
13
|
+
log(`${kind}: webhook registration needs a one-time manual step — see the instructions above`);
|
|
14
|
+
}
|
|
15
|
+
if (unregistered.length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
return `webhook registration failed for: ${unregistered.join(", ")} — the app itself deployed; fix the error above, then ${retryHint}`;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The secret set a deployed agent needs, computed from the definition — host-neutral.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The runbooks list these NAMES; `--run` (per host) reads their VALUES from the local env.
|
|
2
|
+
* The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
|
|
3
|
+
* gate every target; optional channel values travel only when configured. Only the SET command differs
|
|
4
|
+
* (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
|
|
6
5
|
*/
|
|
7
6
|
import { type ChannelKind } from "../scaffold/add-channel.ts";
|
|
8
7
|
/**
|
|
@@ -13,13 +12,15 @@ import { type ChannelKind } from "../scaffold/add-channel.ts";
|
|
|
13
12
|
*/
|
|
14
13
|
export declare function isEnvKey(source: string | undefined): source is string;
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Secret NAMES + hints for a runbook: the model key (when local auth is an env key), discovered channel
|
|
16
|
+
* secrets, and config extras. Channel metadata keeps optional values visible without presenting them as
|
|
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
|
|
20
|
+
export declare function deploymentSecrets(modelAuth: string | undefined, channels: ChannelKind[], extraSecrets?: string[]): {
|
|
21
21
|
name: string;
|
|
22
22
|
hint: string;
|
|
23
|
+
required: boolean;
|
|
23
24
|
}[];
|
|
24
25
|
/**
|
|
25
26
|
* Assemble the secret VALUES a `--run` deploy sets on the host, from the local credential + channels —
|
|
@@ -32,8 +33,8 @@ export declare function requiredSecrets(modelAuth: string | undefined, channels:
|
|
|
32
33
|
*
|
|
33
34
|
* Channel secrets come from the local env only — NEVER minted. A random mint would be wrong for a
|
|
34
35
|
* human-shared secret (github's webhook secret must match the value the operator enters in the repo,
|
|
35
|
-
* which a silent mint never surfaces) and would rotate every run (breaking idempotency).
|
|
36
|
-
* `missingSecrets
|
|
36
|
+
* which a silent mint never surfaces) and would rotate every run (breaking idempotency). An absent
|
|
37
|
+
* required value enters `missingSecrets`; an absent optional value is simply omitted.
|
|
37
38
|
*/
|
|
38
39
|
export declare function assembleSecrets(input: {
|
|
39
40
|
modelAuth: string | undefined;
|
package/dist/deploy/secrets.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The secret set a deployed agent needs, computed from the definition — host-neutral.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* The runbooks list these NAMES; `--run` (per host) reads their VALUES from the local env.
|
|
2
|
+
* The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
|
|
3
|
+
* gate every target; optional channel values travel only when configured. Only the SET command differs
|
|
4
|
+
* (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
|
|
6
5
|
*/
|
|
7
6
|
import { channelSetup } from "../scaffold/add-channel.js";
|
|
8
7
|
/**
|
|
@@ -15,22 +14,23 @@ export function isEnvKey(source) {
|
|
|
15
14
|
return source !== undefined && /^[A-Z][A-Z0-9_]*$/.test(source);
|
|
16
15
|
}
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Secret NAMES + hints for a runbook: the model key (when local auth is an env key), discovered channel
|
|
18
|
+
* secrets, and config extras. Channel metadata keeps optional values visible without presenting them as
|
|
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
|
|
22
|
+
export function deploymentSecrets(modelAuth, channels, extraSecrets = []) {
|
|
23
23
|
const secrets = [];
|
|
24
24
|
if (isEnvKey(modelAuth))
|
|
25
|
-
secrets.push({ name: modelAuth, hint: "your model provider key" });
|
|
25
|
+
secrets.push({ name: modelAuth, hint: "your model provider key", required: true });
|
|
26
26
|
for (const kind of channels) {
|
|
27
27
|
for (const e of channelSetup(kind).env)
|
|
28
|
-
secrets.push({ name: e.name, hint: e.hint });
|
|
28
|
+
secrets.push({ name: e.name, hint: e.hint, required: e.required });
|
|
29
29
|
}
|
|
30
30
|
// Dedup: a name already covered by the model key / a channel secret must not appear twice in the runbook.
|
|
31
31
|
for (const name of extraSecrets) {
|
|
32
32
|
if (!secrets.some((s) => s.name === name)) {
|
|
33
|
-
secrets.push({ name, hint: "declared in fastagent.config deploy.secrets" });
|
|
33
|
+
secrets.push({ name, hint: "declared in fastagent.config deploy.secrets", required: true });
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
return secrets;
|
|
@@ -46,8 +46,8 @@ export function requiredSecrets(modelAuth, channels, extraSecrets = []) {
|
|
|
46
46
|
*
|
|
47
47
|
* Channel secrets come from the local env only — NEVER minted. A random mint would be wrong for a
|
|
48
48
|
* human-shared secret (github's webhook secret must match the value the operator enters in the repo,
|
|
49
|
-
* which a silent mint never surfaces) and would rotate every run (breaking idempotency).
|
|
50
|
-
* `missingSecrets
|
|
49
|
+
* which a silent mint never surfaces) and would rotate every run (breaking idempotency). An absent
|
|
50
|
+
* required value enters `missingSecrets`; an absent optional value is simply omitted.
|
|
51
51
|
*/
|
|
52
52
|
export function assembleSecrets(input) {
|
|
53
53
|
const secrets = {};
|
|
@@ -70,9 +70,10 @@ export function assembleSecrets(input) {
|
|
|
70
70
|
for (const e of channelSetup(kind).env) {
|
|
71
71
|
const v = input.env[e.name];
|
|
72
72
|
if (v)
|
|
73
|
-
secrets[e.name] = v;
|
|
74
|
-
else
|
|
73
|
+
secrets[e.name] = v; // optional channel values travel when configured
|
|
74
|
+
else if (e.required) {
|
|
75
75
|
missingSecrets.push(e.name); // operator-provided (in .env); a human-shared secret can't be minted
|
|
76
|
+
}
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
for (const name of input.extraSecrets ?? []) {
|
package/dist/dev-supervisor.js
CHANGED
|
@@ -16,6 +16,7 @@ import { watch as watchTree } from "chokidar";
|
|
|
16
16
|
import { loadConfig, resolveAgentDir } from "./engines/pi/config.js";
|
|
17
17
|
import { log } from "./log.js";
|
|
18
18
|
import { installProxyFetch } from "./proxy.js";
|
|
19
|
+
import { openExternalUrl } from "./open-url.js";
|
|
19
20
|
import { announceWebhooks, startCloudflareTunnel } from "./tunnel.js";
|
|
20
21
|
/** What the dev watcher restarts on (workspace-relative): the process-bound code inputs only. */
|
|
21
22
|
export const WATCHED_HINT = "tools/, channels/, package.json (agent dir), fastagent.config.*, .env (run root)";
|
|
@@ -99,7 +100,7 @@ export async function runDevSupervisor(dir, options = {}) {
|
|
|
99
100
|
void startCloudflareTunnel(m.port).then((t) => {
|
|
100
101
|
if (t) {
|
|
101
102
|
tunnel = t;
|
|
102
|
-
void announceWebhooks(agentDir, t.url);
|
|
103
|
+
void announceWebhooks(agentDir, t.url, { openUrl: openExternalUrl });
|
|
103
104
|
}
|
|
104
105
|
});
|
|
105
106
|
}
|
package/dist/engines/pi/chat.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* AGENTS.md up to the repo root and discover skills from pi's own global dirs). So fastagent's
|
|
8
8
|
* assembly is INJECTED into pi's session:
|
|
9
9
|
* - prompt → systemPromptOverride = base + instructions ONLY; pi appends the skill section and env
|
|
10
|
-
* (
|
|
10
|
+
* (cwd) itself (including it here would duplicate it).
|
|
11
11
|
* - skills → skillsOverride (fastagent's skills, for the section + invocation).
|
|
12
12
|
* - tools → default coding tools by NAME (pi rebuilds them cwd-bound for rich rendering) +
|
|
13
13
|
* fastagent's custom tools via pi's customTools path (so they survive /new, /resume, fork).
|
|
@@ -68,8 +68,8 @@ sessionManager) {
|
|
|
68
68
|
parameters: t.parameters,
|
|
69
69
|
execute: (id, params, signal) => t.execute(id, params, signal),
|
|
70
70
|
}));
|
|
71
|
-
// base + instructions ONLY — pi appends the skill section and env (
|
|
72
|
-
// them here would duplicate
|
|
71
|
+
// base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
|
|
72
|
+
// them here would duplicate them).
|
|
73
73
|
const systemPrompt = assembleSystemPrompt({
|
|
74
74
|
base: piBasePrompt({ tools, persona: definition.persona }),
|
|
75
75
|
contextFiles: definition.contextFiles,
|
|
@@ -46,7 +46,6 @@ export interface AssembleSystemPromptOptions {
|
|
|
46
46
|
/** ③ Skills for the <available_skills> listing. */
|
|
47
47
|
skills?: Skill[];
|
|
48
48
|
/** ④ Env context, caller-provided (keeps this function pure). Omitted = segment omitted. */
|
|
49
|
-
date?: string;
|
|
50
49
|
cwd?: string;
|
|
51
50
|
}
|
|
52
51
|
export declare function assembleSystemPrompt(options: AssembleSystemPromptOptions): string;
|
|
@@ -54,10 +54,14 @@ export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
|
|
|
54
54
|
//
|
|
55
55
|
// systemPrompt = ① base (engine asset; a persona.md persona overrides its identity line)
|
|
56
56
|
// + ② project context (AGENTS.md files via pi's loadProjectContextFiles, <project_context>-wrapped)
|
|
57
|
-
// + ③ skills listing + ④ env context (
|
|
57
|
+
// + ③ skills listing + ④ env context (cwd)
|
|
58
58
|
//
|
|
59
|
-
// AGENTS.md ≠ system prompt. Pure functions: segment ④
|
|
60
|
-
// same inputs always produce the same prompt (testable, reproducible).
|
|
59
|
+
// AGENTS.md ≠ system prompt. Pure functions: segment ④ input (cwd) is caller-provided, so the
|
|
60
|
+
// same inputs always produce the same prompt (testable, reproducible). No date: a date line would
|
|
61
|
+
// invalidate the provider prompt cache (a prefix cache) for every session at each day boundary —
|
|
62
|
+
// channel sessions routinely live for weeks (pi ≥0.80.7 dropped it from its default prompt for the
|
|
63
|
+
// same reason). The model gets the date when it needs it: `bash date`, and the wake tool takes
|
|
64
|
+
// relative delays ("30m") / cron — never an absolute now-derived instant.
|
|
61
65
|
/**
|
|
62
66
|
* The pi engine's base prompt (segment ①), mirroring pi-coding-agent's default path with two
|
|
63
67
|
* deviations: the pi-TUI docs section is dropped (those paths don't exist in deployments), and the
|
|
@@ -96,8 +100,6 @@ export function assembleSystemPrompt(options) {
|
|
|
96
100
|
if (options.skills && options.skills.length > 0) {
|
|
97
101
|
prompt += `\n${formatSkillsForSystemPrompt(options.skills)}\n`;
|
|
98
102
|
}
|
|
99
|
-
if (options.date)
|
|
100
|
-
prompt += `\nCurrent date: ${options.date}`;
|
|
101
103
|
if (options.cwd)
|
|
102
104
|
prompt += `\nCurrent working directory: ${options.cwd}`;
|
|
103
105
|
return prompt;
|
|
@@ -185,7 +187,7 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
185
187
|
// The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
|
|
186
188
|
// edits (the author's, or the agent's own self-modification) take effect on the next turn with
|
|
187
189
|
// no process restart — restarts are reserved for code (tools/channels/config, module cache).
|
|
188
|
-
// One read yields prompt AND skills (they can never diverge),
|
|
190
|
+
// One read yields prompt AND skills (they can never diverge), and the
|
|
189
191
|
// fs cost is a few reads against a model call. Broken edits stay visible: a throw-class problem
|
|
190
192
|
// (unreadable AGENTS.md) fails that turn's invoke, and the loader's NON-fatal findings (bad
|
|
191
193
|
// SKILL.md frontmatter, name collisions — returned as data, not thrown) are warned the moment
|
|
@@ -207,7 +209,6 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
207
209
|
// ② project context: AGENTS.md files (agentDir + cwd-ancestor walk) via loadProjectContextFiles.
|
|
208
210
|
contextFiles: def.contextFiles,
|
|
209
211
|
skills: def.skills,
|
|
210
|
-
date: new Date().toISOString().slice(0, 10),
|
|
211
212
|
cwd: env.cwd,
|
|
212
213
|
}),
|
|
213
214
|
skills: def.skills,
|
package/dist/feishu.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** `@fastagent-sh/fastagent/feishu` — the canonical Feishu (open.feishu.cn) bot-channel surface. */
|
|
2
|
+
export { feishuChannel, defaultFeishuRoute, feishuEnvelope, type FeishuChannelOptions, type FeishuMessageEvent, type FeishuMessage, type FeishuRoute, type FeishuFailure, } from "./channels/feishu/feishu.ts";
|
package/dist/feishu.js
ADDED
package/dist/lark.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** `@fastagent-sh/fastagent/lark` — the Lark-international compatibility surface over the canonical
|
|
2
|
+
* Feishu engine. Feishu tenants use `@fastagent-sh/fastagent/feishu`. */
|
|
3
|
+
export { larkChannel, defaultLarkRoute, larkEnvelope, type LarkChannelOptions, type LarkMessageEvent, type LarkMessage, type LarkRoute, type LarkFailure, } from "./channels/lark/lark.ts";
|
package/dist/lark.js
ADDED
package/dist/open-url.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/** Best-effort open a URL in the default browser. Callers must also print the URL for headless use. */
|
|
3
|
+
export function openExternalUrl(url) {
|
|
4
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
5
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).on("error", () => { });
|
|
6
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export type ChannelKind = "github" | "telegram";
|
|
1
|
+
export type ChannelKind = "github" | "telegram" | "feishu" | "lark";
|
|
2
2
|
/** An env var a scaffolded channel reads. `generate` = a random-string secret the CLI can pre-fill. */
|
|
3
3
|
export interface ChannelEnv {
|
|
4
4
|
name: string;
|
|
5
5
|
hint: string;
|
|
6
|
+
/** Required for the channel to run. Optional values are deployed when present but never gate deploy. */
|
|
7
|
+
required: boolean;
|
|
6
8
|
generate?: boolean;
|
|
7
9
|
}
|
|
8
10
|
/** The channel kinds `fastagent add <kind>` can scaffold. */
|
|
@@ -26,11 +28,13 @@ export interface DotEnvWriteResult {
|
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Append generated channel secrets to the run-root `.env` (never `.env.example`) after the CLI has
|
|
29
|
-
* verified that `.env` is gitignored. Existing non-empty values are kept
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* verified that `.env` is gitignored. Existing non-empty values are kept — EXCEPT the names listed in
|
|
32
|
+
* `overwrite`: those are authoritative (e.g. the credentials of an app `add feishu` JUST minted —
|
|
33
|
+
* skipping them for a stale value would silently discard a fresh, unrecoverable secret). Manual values
|
|
34
|
+
* (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
|
|
35
|
+
* ready to edit while no fake secret is committed to the user's mental model.
|
|
32
36
|
*/
|
|
33
|
-
export declare function appendChannelDotEnv(dir: string, kind: ChannelKind, generated: Record<string, string
|
|
37
|
+
export declare function appendChannelDotEnv(dir: string, kind: ChannelKind, generated: Record<string, string>, overwrite?: readonly string[]): Promise<DotEnvWriteResult>;
|
|
34
38
|
/** Whether a channel file already exists — checked before any mutation, so a no-clobber re-add is side-effect-free. */
|
|
35
39
|
export declare function channelExists(dir: string, kind: ChannelKind): Promise<boolean>;
|
|
36
40
|
/**
|