@fastagent-sh/fastagent 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +6 -4
  2. package/dist/channels/feishu/bootstrap-token.d.ts +42 -0
  3. package/dist/channels/feishu/bootstrap-token.js +94 -0
  4. package/dist/channels/feishu/card.d.ts +32 -0
  5. package/dist/channels/feishu/card.js +66 -0
  6. package/dist/channels/feishu/cloud.d.ts +17 -0
  7. package/dist/channels/feishu/cloud.js +19 -0
  8. package/dist/channels/feishu/context-buffer.d.ts +46 -0
  9. package/dist/channels/feishu/context-buffer.js +133 -0
  10. package/dist/channels/feishu/crypto.d.ts +15 -0
  11. package/dist/channels/feishu/crypto.js +43 -0
  12. package/dist/channels/feishu/feishu-api.d.ts +108 -0
  13. package/dist/channels/feishu/feishu-api.js +325 -0
  14. package/dist/channels/feishu/feishu.d.ts +46 -0
  15. package/dist/channels/feishu/feishu.js +472 -0
  16. package/dist/channels/feishu/invoke-turn.d.ts +65 -0
  17. package/dist/channels/feishu/invoke-turn.js +157 -0
  18. package/dist/channels/feishu/model.d.ts +97 -0
  19. package/dist/channels/feishu/model.js +9 -0
  20. package/dist/channels/feishu/normalize.d.ts +22 -0
  21. package/dist/channels/feishu/normalize.js +132 -0
  22. package/dist/channels/feishu/owned-threads.d.ts +11 -0
  23. package/dist/channels/feishu/owned-threads.js +47 -0
  24. package/dist/channels/feishu/parse.d.ts +43 -0
  25. package/dist/channels/feishu/parse.js +65 -0
  26. package/dist/channels/feishu/preview.d.ts +36 -0
  27. package/dist/channels/feishu/preview.js +387 -0
  28. package/dist/channels/feishu/register-app.d.ts +70 -0
  29. package/dist/channels/feishu/register-app.js +141 -0
  30. package/dist/channels/feishu/register-webhook.d.ts +22 -0
  31. package/dist/channels/feishu/register-webhook.js +106 -0
  32. package/dist/channels/feishu/scaffold/channel.ts +41 -0
  33. package/dist/channels/feishu/scaffold/feishu-send.ts +87 -0
  34. package/dist/channels/feishu/seen.d.ts +5 -0
  35. package/dist/channels/feishu/seen.js +47 -0
  36. package/dist/channels/feishu/text.d.ts +13 -0
  37. package/dist/channels/feishu/text.js +63 -0
  38. package/dist/channels/lark/lark.d.ts +15 -0
  39. package/dist/channels/lark/lark.js +10 -0
  40. package/dist/channels/lark/onboard.d.ts +39 -0
  41. package/dist/channels/lark/onboard.js +58 -0
  42. package/dist/channels/lark/scaffold/channel.ts +39 -0
  43. package/dist/channels/lark/scaffold/lark-send.ts +87 -0
  44. package/dist/channels/registration.d.ts +15 -0
  45. package/dist/channels/registration.js +1 -0
  46. package/dist/channels/{telegram/state.js → state.js} +6 -4
  47. package/dist/channels/telegram/context-buffer.js +1 -1
  48. package/dist/channels/telegram/register-webhook.d.ts +4 -1
  49. package/dist/channels/telegram/register-webhook.js +17 -26
  50. package/dist/channels/telegram/telegram.js +2 -2
  51. package/dist/channels/telegram/turn-store.d.ts +8 -21
  52. package/dist/channels/telegram/turn-store.js +11 -130
  53. package/dist/channels/{telegram/turn-queue.js → turn-queue.js} +3 -3
  54. package/dist/channels/turn-store.d.ts +42 -0
  55. package/dist/channels/turn-store.js +139 -0
  56. package/dist/channels/wait-health.d.ts +6 -0
  57. package/dist/channels/wait-health.js +27 -0
  58. package/dist/cli/commands/add.d.ts +9 -0
  59. package/dist/cli/commands/add.js +142 -0
  60. package/dist/cli/commands/chat.d.ts +3 -0
  61. package/dist/cli/commands/chat.js +16 -0
  62. package/dist/cli/commands/deploy.d.ts +13 -0
  63. package/dist/cli/commands/deploy.js +338 -0
  64. package/dist/cli/commands/dev.d.ts +11 -0
  65. package/dist/cli/commands/dev.js +76 -0
  66. package/dist/cli/commands/fire.d.ts +7 -0
  67. package/dist/cli/commands/fire.js +45 -0
  68. package/dist/cli/commands/info.d.ts +7 -0
  69. package/dist/cli/commands/info.js +108 -0
  70. package/dist/cli/commands/init.d.ts +8 -0
  71. package/dist/cli/commands/init.js +81 -0
  72. package/dist/cli/commands/invoke.d.ts +7 -0
  73. package/dist/cli/commands/invoke.js +29 -0
  74. package/dist/cli/commands/login.d.ts +6 -0
  75. package/dist/cli/commands/login.js +63 -0
  76. package/dist/cli/commands/models.d.ts +1 -0
  77. package/dist/cli/commands/models.js +15 -0
  78. package/dist/cli/commands/schedule.d.ts +12 -0
  79. package/dist/cli/commands/schedule.js +89 -0
  80. package/dist/cli/commands/start.d.ts +10 -0
  81. package/dist/cli/commands/start.js +90 -0
  82. package/dist/cli/commands/tool.d.ts +1 -0
  83. package/dist/cli/commands/tool.js +37 -0
  84. package/dist/cli/fail.d.ts +19 -0
  85. package/dist/cli/fail.js +32 -0
  86. package/dist/cli/kernel.d.ts +89 -0
  87. package/dist/cli/kernel.js +190 -0
  88. package/dist/cli/program.d.ts +11 -0
  89. package/dist/cli/program.js +421 -0
  90. package/dist/cli/serve.d.ts +28 -0
  91. package/dist/cli/serve.js +90 -0
  92. package/dist/cli/shared.d.ts +24 -0
  93. package/dist/cli/shared.js +116 -0
  94. package/dist/cli-add-feishu.d.ts +8 -0
  95. package/dist/cli-add-feishu.js +223 -0
  96. package/dist/cli.js +8 -1284
  97. package/dist/deploy/container.js +10 -6
  98. package/dist/deploy/docker/plan.d.ts +45 -0
  99. package/dist/deploy/docker/plan.js +139 -0
  100. package/dist/deploy/docker/run.d.ts +40 -0
  101. package/dist/deploy/docker/run.js +126 -0
  102. package/dist/deploy/fly/plan.d.ts +1 -1
  103. package/dist/deploy/fly/plan.js +15 -4
  104. package/dist/deploy/fly/run.d.ts +7 -4
  105. package/dist/deploy/fly/run.js +26 -5
  106. package/dist/deploy/preflight.js +4 -3
  107. package/dist/deploy/railway/plan.d.ts +1 -1
  108. package/dist/deploy/railway/plan.js +17 -5
  109. package/dist/deploy/railway/run.d.ts +6 -3
  110. package/dist/deploy/railway/run.js +26 -4
  111. package/dist/deploy/registration-gate.d.ts +20 -0
  112. package/dist/deploy/registration-gate.js +20 -0
  113. package/dist/deploy/runner.d.ts +4 -1
  114. package/dist/deploy/runner.js +1 -0
  115. package/dist/deploy/secrets.d.ts +10 -9
  116. package/dist/deploy/secrets.js +15 -14
  117. package/dist/dev-supervisor.js +2 -1
  118. package/dist/engines/pi/auth.js +160 -46
  119. package/dist/engines/pi/chat.js +80 -7
  120. package/dist/engines/pi/config.d.ts +12 -3
  121. package/dist/engines/pi/config.js +16 -1
  122. package/dist/engines/pi/create.d.ts +14 -6
  123. package/dist/engines/pi/create.js +52 -16
  124. package/dist/engines/pi/harness.d.ts +16 -1
  125. package/dist/engines/pi/harness.js +77 -1
  126. package/dist/engines/pi/invoke.d.ts +1 -1
  127. package/dist/engines/pi/invoke.js +37 -2
  128. package/dist/engines/pi/login.js +1 -1
  129. package/dist/engines/pi/search-tools.d.ts +10 -0
  130. package/dist/engines/pi/search-tools.js +138 -0
  131. package/dist/engines/pi/tool-context.d.ts +28 -0
  132. package/dist/engines/pi/tool-context.js +8 -0
  133. package/dist/engines/pi/tool.d.ts +32 -1
  134. package/dist/engines/pi/tool.js +42 -1
  135. package/dist/engines/pi/workspace.d.ts +4 -1
  136. package/dist/engines/pi/workspace.js +3 -1
  137. package/dist/feishu.d.ts +2 -0
  138. package/dist/feishu.js +2 -0
  139. package/dist/lark.d.ts +3 -0
  140. package/dist/lark.js +3 -0
  141. package/dist/open-url.d.ts +2 -0
  142. package/dist/open-url.js +6 -0
  143. package/dist/pi.d.ts +2 -1
  144. package/dist/scaffold/add-channel.d.ts +9 -5
  145. package/dist/scaffold/add-channel.js +75 -7
  146. package/dist/scaffold/templates/fastagent.config.mjs +1 -0
  147. package/dist/tunnel.d.ts +9 -6
  148. package/dist/tunnel.js +48 -31
  149. package/package.json +18 -4
  150. /package/dist/channels/{telegram/state.d.ts → state.d.ts} +0 -0
  151. /package/dist/channels/{telegram/turn-queue.d.ts → turn-queue.d.ts} +0 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Helpers shared across command modules: interactivity gates, port parsing, the startup auth report,
3
+ * and first-run model resolution. Bodies moved verbatim from cli.ts; the module-scoped flag access
4
+ * (`values.*`) became parameters.
5
+ */
6
+ import { readFile, writeFile } from "node:fs/promises";
7
+ import { relative } from "node:path";
8
+ import { autocomplete, isCancel, select } from "@clack/prompts";
9
+ import { fastagentCredentialStore } from "../engines/pi/auth.js";
10
+ import { isValidPort, loadConfig, resolveAuthPath, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
11
+ import { configuredModelSpecs, createPiModels, probeAuthSource } from "../engines/pi/models.js";
12
+ import { formatAuthReport } from "../cli-auth.js";
13
+ import { log } from "../log.js";
14
+ import { failStartup, failUsage } from "./fail.js";
15
+ /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
16
+ export function isInteractive() {
17
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
18
+ }
19
+ /**
20
+ * Parse + range-check a port string (CLI flag or env). Empty/whitespace is "not set" → undefined, so
21
+ * the `??` chain falls through instead of binding port 0 (`Number("")` is 0). The exit code follows
22
+ * RESPONSIBILITY, not the layer that discovers the problem: a bad `--port` is a usage error (2), a
23
+ * bad `PORT` env is broken runtime configuration (1).
24
+ */
25
+ export function parsePort(value, source, from) {
26
+ if (value === undefined)
27
+ return undefined;
28
+ const trimmed = value.trim();
29
+ if (trimmed === "")
30
+ return undefined;
31
+ if (!/^\d+$/.test(trimmed) || !isValidPort(Number(trimmed))) {
32
+ const message = `invalid ${source} "${value}": must be an integer 0-65535`;
33
+ if (from === "flag")
34
+ failUsage(message);
35
+ failStartup(new Error(message));
36
+ }
37
+ return Number(trimmed);
38
+ }
39
+ /** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
40
+ export async function reportAuth(modelSpec, authPath) {
41
+ const provider = modelSpec.slice(0, modelSpec.indexOf("/"));
42
+ const source = await probeAuthSource(createPiModels({ authPath }), modelSpec);
43
+ // Only when nothing satisfies auth do we read the store (refresh-FREE) to tell "nothing stored" from
44
+ // "stored but unusable" — see formatAuthReport for why. store.read warns on a corrupt file itself.
45
+ const stored = source === undefined
46
+ ? await fastagentCredentialStore(authPath)
47
+ .read(provider)
48
+ .catch(() => undefined)
49
+ : undefined;
50
+ const report = formatAuthReport(provider, authPath, source, stored);
51
+ log.info(`[fastagent] ${report.line}`);
52
+ if (report.warn)
53
+ log.warn(`[fastagent] ${report.warn}`);
54
+ }
55
+ /**
56
+ * First-run model resolution for the serving commands. When no model is set (flag/env/config), and
57
+ * we're on a TTY, pick one from the providers the user is logged into and persist the choice. A no-op
58
+ * when a model is already set; on a non-TTY (CI/deploy), or with `--no-input`, or with nothing
59
+ * configured it stays silent and lets the opener raise its clear "missing model" error. The pick is
60
+ * exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it, and best-effort written back to
61
+ * the config so the next run is quiet.
62
+ */
63
+ export async function resolveFirstRunModel(workspaceDir, options = {}) {
64
+ const { config, path: configPath } = await loadConfig(workspaceDir).catch(failStartup);
65
+ if (resolveModelSpec(options.model, config))
66
+ return; // already set (flag > FASTAGENT_MODEL > config)
67
+ if (options.input === false)
68
+ return; // --no-input: never prompt (clig) — the opener raises the clear error
69
+ if (!isInteractive())
70
+ return; // CI/deploy: the opener throws the actionable missing-model error
71
+ const authPath = resolveAuthPath(workspaceDir, options.authPath);
72
+ let specs;
73
+ try {
74
+ specs = await configuredModelSpecs(createPiModels({ authPath }));
75
+ }
76
+ catch (error) {
77
+ // Enumerating providers/auth threw — a system fault (a corrupt auth store, a throwing provider),
78
+ // NOT "not logged in". Surface it instead of masking it as the login hint; the opener then still
79
+ // raises the clear missing-model error.
80
+ log.warn(`[fastagent] could not list configured models: ${error.message}`);
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;
86
+ }
87
+ const r = await (specs.length > 7 ? autocomplete : select)({
88
+ message: "Choose a model for this agent",
89
+ options: specs.map((s) => ({ value: s, label: s })),
90
+ });
91
+ if (isCancel(r))
92
+ return; // cancelled: let the opener report the missing model
93
+ const chosen = r;
94
+ process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
95
+ await persistModelChoice(workspaceDir, configPath, chosen);
96
+ }
97
+ /**
98
+ * Best-effort persist the picked model so the next run does not prompt. Only rewrites the commented
99
+ * `model:` placeholder the scaffold writes (or an existing `model:` line); anything else (zero-config,
100
+ * a hand-shaped config) is left untouched with a printed hint. Never throws — persistence is a convenience.
101
+ */
102
+ async function persistModelChoice(workspaceDir, configPath, spec) {
103
+ const hint = () => console.error(`[fastagent] using ${spec} for this run; set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
104
+ if (!configPath)
105
+ return hint();
106
+ try {
107
+ const replaced = rewriteConfigModel(await readFile(configPath, "utf8"), spec);
108
+ if (!replaced)
109
+ return hint();
110
+ await writeFile(configPath, replaced);
111
+ console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(workspaceDir, configPath)}`);
112
+ }
113
+ catch {
114
+ hint();
115
+ }
116
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Create or resume the platform app behind `add feishu` / `add lark`. Returns credentials for the
3
+ * caller's generic .env write (the guided Lark path), or undefined when nothing remains to write —
4
+ * the feishu path persists its own two credential stages internally (the App ID/Secret boundary is
5
+ * irreversible and must not wait for the caller). Throws on refusal (a committable .env, a
6
+ * non-interactive lark run); the caller surfaces that as a startup failure.
7
+ */
8
+ export declare function onboardFeishuCloudApp(target: string, kind: "feishu" | "lark", envIgnored: boolean): Promise<Record<string, string> | undefined>;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * `fastagent add feishu|lark` app onboarding — the cloud-facing half of runAdd, kept out of cli.ts
3
+ * (which self-executes on import), mirroring cli-models.ts/cli-auth.ts. The POLICY already lives in
4
+ * testable modules (register-app.ts, bootstrap-token.ts, lark/onboard.ts); this layer is the terminal
5
+ * wiring: clack prompts, .env staging, browser opens, progress lines.
6
+ *
7
+ * Feishu (scan-to-create): the device flow creates the app; App ID/Secret are persisted at the
8
+ * irreversible creation boundary, then the Verification Token is captured over a throwaway tunnel and
9
+ * persisted as a second stage — a re-run RESUMES the app instead of minting another. Lark
10
+ * (guided-console): open the unbound launcher, validate entered credentials, probe the same
11
+ * webhook/token bootstrap, and fall back to a manual token prompt on the definitive config-route 404.
12
+ */
13
+ import { readFile } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+ import { isCancel, log as clackLog, password, text as clackText } from "@clack/prompts";
16
+ import { bootstrapFeishuVerificationToken } from "./channels/feishu/bootstrap-token.js";
17
+ import { cloudFor } from "./channels/feishu/cloud.js";
18
+ import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "./channels/feishu/feishu-api.js";
19
+ import { registerFeishuApp } from "./channels/feishu/register-app.js";
20
+ import { onboardLarkApp } from "./channels/lark/onboard.js";
21
+ import { parseEnvContent } from "./env.js";
22
+ import { openExternalUrl } from "./open-url.js";
23
+ import { appendChannelDotEnv } from "./scaffold/add-channel.js";
24
+ import { startCloudflareTunnel } from "./tunnel.js";
25
+ /**
26
+ * Create or resume the platform app behind `add feishu` / `add lark`. Returns credentials for the
27
+ * caller's generic .env write (the guided Lark path), or undefined when nothing remains to write —
28
+ * the feishu path persists its own two credential stages internally (the App ID/Secret boundary is
29
+ * irreversible and must not wait for the caller). Throws on refusal (a committable .env, a
30
+ * non-interactive lark run); the caller surfaces that as a startup failure.
31
+ */
32
+ export async function onboardFeishuCloudApp(target, kind, envIgnored) {
33
+ const { envPrefix, apiBase, capabilities } = cloudFor(kind);
34
+ // The CLI must never materialize a real credential into a committable file — refuse, don't warn.
35
+ if (!envIgnored) {
36
+ throw new Error(kind === "feishu"
37
+ ? "`add feishu` creates an app and writes real credentials to .env — add .env to .gitignore/.fastagentignore first, then re-run"
38
+ : "`add lark` writes real app credentials to .env — add .env to .gitignore/.fastagentignore first, then re-run");
39
+ }
40
+ const existing = await activeDotEnvValues(target, [
41
+ `${envPrefix}_APP_ID`,
42
+ `${envPrefix}_APP_SECRET`,
43
+ `${envPrefix}_VERIFICATION_TOKEN`,
44
+ ]);
45
+ if (Object.keys(existing).length === 3) {
46
+ console.error(`[fastagent] ${envPrefix}_APP_ID/SECRET/VERIFICATION_TOKEN already set in .env — keeping them`);
47
+ return undefined;
48
+ }
49
+ if (capabilities.appCreation === "scan-to-create") {
50
+ await createFeishuAppFlow(target, existing);
51
+ return undefined;
52
+ }
53
+ // guided-console (lark): the intl cloud cannot complete the bound device flow — collect + validate.
54
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
55
+ throw new Error("`add lark` needs an interactive terminal to onboard the Lark app credentials — re-run it in a terminal");
56
+ }
57
+ return onboardLarkApp({
58
+ openUrl: openExternalUrl,
59
+ note: (message) => clackLog.info(message),
60
+ async prompt(message, opts) {
61
+ const result = opts?.hidden ? await password({ message }) : await clackText({ message });
62
+ return isCancel(result) ? undefined : result;
63
+ },
64
+ }, {
65
+ existing,
66
+ verifyCredentials: async (appId, appSecret) => {
67
+ await createFeishuApi({ kind: "lark", baseUrl: apiBase, appId, appSecret }).verifyCredentials();
68
+ console.error(`[fastagent] Lark App ID / Secret verified`);
69
+ },
70
+ bootstrapWebhook: async (appId, appSecret) => {
71
+ const api = createFeishuApi({ kind: "lark", baseUrl: apiBase, appId, appSecret });
72
+ console.error(`[fastagent] trying Lark's webhook-mode + Verification-Token bootstrap (temporary tunnel)…`);
73
+ try {
74
+ const token = await bootstrapFeishuVerificationToken({
75
+ api,
76
+ appId,
77
+ kind: "lark",
78
+ startTunnel: (port) => startCloudflareTunnel(port),
79
+ onTunnelReady: (url) => console.error(`[fastagent] temporary tunnel ready → ${url}; registering webhook mode now…`),
80
+ onPatchRetry: ({ error, attempt, attempts, retryMs }) => console.error(`[fastagent] Lark could not validate the fresh tunnel yet (${String(error)}); retrying PATCH ${attempt + 1}/${attempts} in ${Math.round(retryMs / 1000)}s…`),
81
+ // A route-level 404 is definitive, not edge weather: fall back immediately. Retry only
82
+ // actual edge/network weather; scope/auth/config failures remain immediate.
83
+ shouldRetryPatch: (error) => !isFeishuConfigApiMissing(error) && isTransientFeishuRegistrationError(error),
84
+ });
85
+ console.error(`[fastagent] Lark Verification Token captured; Subscription mode changed to webhook in the app draft`);
86
+ return { token };
87
+ }
88
+ catch (error) {
89
+ if (!isFeishuConfigApiMissing(error))
90
+ throw error;
91
+ const manualReason = "This Lark app returned HTTP 404 for the application-config API, so automatic mode/token bootstrap is unavailable.";
92
+ console.error(`[fastagent] ${manualReason}`);
93
+ return { manualReason };
94
+ }
95
+ },
96
+ });
97
+ }
98
+ /**
99
+ * The scan-to-create flow `add feishu` runs by default. The device-authorization grant
100
+ * creates a pre-configured agent app (bot capability, messaging scopes, event subscriptions) when the
101
+ * user confirms a link in the app, and hands back the credentials; App ID/Secret are persisted at that
102
+ * irreversible boundary before the platform-generated Verification Token is captured from the
103
+ * registration challenge (bootstrap-token.ts). The Token is persisted as a second stage, so .env is
104
+ * complete before the one remaining version-publish action. The event Request URL is NOT left pointing at the throwaway
105
+ * tunnel for long: `dev --tunnel` / `deploy --run` re-register it against the live URL.
106
+ *
107
+ * Feishu is the reference cloud and the only kind that runs this BOUND device flow. Lark is an explicit
108
+ * compatibility profile: its lagging control plane uses the unbound launcher + guided credentials,
109
+ * then probes the canonical token/mode bootstrap with a manual fallback.
110
+ */
111
+ async function createFeishuAppFlow(target, existing) {
112
+ const { apiBase } = cloudFor("feishu");
113
+ let appId = existing.FEISHU_APP_ID;
114
+ let appSecret = existing.FEISHU_APP_SECRET;
115
+ if (appId && appSecret) {
116
+ console.error(`[fastagent] resuming Feishu app ${appId} from .env to capture its missing Verification Token`);
117
+ }
118
+ else {
119
+ console.error(`[fastagent] creating the Feishu app (confirm in the app)…`);
120
+ const app = await registerFeishuApp({
121
+ name: "{user}'s agent", // the platform expands {user} to the confirming user's name; editable on the page
122
+ desc: "Served by fastagent",
123
+ // The agent template alone is not enough to SERVE: the v7 config PATCH (webhook auto-registration
124
+ // in `dev --tunnel` / `deploy --run`) demands application:application:patch, and the app must
125
+ // subscribe the receive event. Addons merge both onto the confirm page — no manual app setup.
126
+ addons: {
127
+ scopes: { tenant: ["application:application:patch"] },
128
+ events: { items: { tenant: ["im.message.receive_v1"] } },
129
+ },
130
+ onVerificationUrl: ({ url, expiresInS }) => {
131
+ console.error(`\n Opening the confirmation link in your browser (or open it in Feishu / render it as a QR code) — valid for ${Math.round(expiresInS / 60)} minutes:\n\n ${url}\n\n waiting for confirmation… (keep this running — the credentials are delivered here)`);
132
+ openExternalUrl(url); // best-effort, like `login` — the URL above is the fallback
133
+ },
134
+ });
135
+ console.error(`[fastagent] app created: ${app.appId}${app.tenantBrand ? ` (${app.tenantBrand} tenant)` : ""}`);
136
+ // A cross-brand confirmation should be impossible (each confirm page refuses the other brand's
137
+ // code) — but if the platform ever reports one, the credentials would land in the WRONG kind's env
138
+ // namespace and serve the wrong cloud. Fail visibly instead of writing them.
139
+ if (app.tenantBrand && app.tenantBrand !== "feishu") {
140
+ throw new Error(`the confirming account is a ${app.tenantBrand} tenant, but this is \`add feishu\` — run \`fastagent add ${app.tenantBrand}\` instead`);
141
+ }
142
+ appId = app.appId;
143
+ appSecret = app.appSecret;
144
+ // IRREVERSIBLE BOUNDARY: the remote app now exists and its one-time Secret is in memory. Persist
145
+ // both before any config read, temporary tunnel, or Token bootstrap can be interrupted. Partial old
146
+ // lines are overwritten because these newly-minted credentials are authoritative as one pair.
147
+ await appendChannelDotEnv(target, "feishu", {
148
+ FEISHU_APP_ID: appId,
149
+ FEISHU_APP_SECRET: appSecret,
150
+ // A Token from a partial OLD credential set belongs to another App. Clear it at the same
151
+ // boundary; successful bootstrap below replaces the empty line with this App's Token.
152
+ FEISHU_VERIFICATION_TOKEN: "",
153
+ }, ["FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_VERIFICATION_TOKEN"]);
154
+ console.error(`[fastagent] wrote FEISHU_APP_ID, FEISHU_APP_SECRET to .env before Token bootstrap`);
155
+ }
156
+ // The webhook channel authenticates plaintext events by the platform-generated Verification Token.
157
+ // Try the cheap read first (the v6 detail MAY someday return `encryption`), then the real path: the
158
+ // token's only programmatic delivery is the url_verification challenge during registration — capture
159
+ // it over a throwaway tunnel (bootstrap-token.ts). Failing both is a one-line manual copy; the staged
160
+ // ID/Secret pair makes a re-run resume this App rather than mint another one.
161
+ const tokenVar = "FEISHU_VERIFICATION_TOKEN";
162
+ const api = createFeishuApi({ baseUrl: apiBase, appId, appSecret });
163
+ let token;
164
+ let webhookModeChanged = false;
165
+ try {
166
+ const cfg = await api.getAppConfig(appId);
167
+ token = cfg.verificationToken;
168
+ }
169
+ catch {
170
+ /* the read surface is best-effort — the bootstrap below is the real path */
171
+ }
172
+ if (!token) {
173
+ console.error(`[fastagent] capturing the Verification Token — a throwaway webhook registration delivers it (spinning up a temporary tunnel; can take a few minutes on a slow edge)…`);
174
+ try {
175
+ token = await bootstrapFeishuVerificationToken({
176
+ api,
177
+ appId,
178
+ startTunnel: (port) => startCloudflareTunnel(port),
179
+ });
180
+ webhookModeChanged = true;
181
+ console.error(`[fastagent] Verification Token captured`);
182
+ }
183
+ catch (e) {
184
+ // Transient tunnel weather is the usual cause. Do NOT suggest re-running `add feishu` as a new
185
+ // scan: the staged pair makes the re-run resume THIS app; manual copy completes it too.
186
+ console.error(`[fastagent] warn: could not capture the Verification Token: ${String(e)} — usually a transient tunnel issue; finish this app with the manual copy below`);
187
+ }
188
+ }
189
+ if (token) {
190
+ // Persist the second credential stage immediately too — opening the publish page and generic
191
+ // scaffold finalization happen only after the complete runtime credential set is durable.
192
+ const staged = await appendChannelDotEnv(target, "feishu", { [tokenVar]: token }, [tokenVar]);
193
+ console.error(`[fastagent] wrote ${staged.written.join(", ")} to .env`);
194
+ }
195
+ else {
196
+ console.error(`[fastagent] copy it manually: developer console → Events & Callbacks → Encryption Strategy → Verification Token → ${tokenVar} in .env`);
197
+ }
198
+ if (webhookModeChanged) {
199
+ // The bootstrap's PATCH flipped event mode in the DRAFT. It takes effect only after a version
200
+ // publish, which has no API; later dev/deploy runs change only the Request URL immediately.
201
+ const versionUrl = `${apiBase}/app/${appId}/version`;
202
+ console.error(`[fastagent] one console click remains: CREATE + PUBLISH a version (self-approved) — the switch to webhook mode takes effect on publish. Opening ${versionUrl}`);
203
+ openExternalUrl(versionUrl);
204
+ }
205
+ }
206
+ /** Active run-root `.env` values for the requested names — decided by THE .env parser, so this
207
+ * check can never disagree with what `loadEnvFile` reads. Empty/commented values are absent. */
208
+ async function activeDotEnvValues(dir, names) {
209
+ let content;
210
+ try {
211
+ content = await readFile(join(dir, ".env"), "utf8");
212
+ }
213
+ catch (e) {
214
+ if (e.code === "ENOENT")
215
+ return {};
216
+ throw e;
217
+ }
218
+ const parsed = parseEnvContent(content);
219
+ return Object.fromEntries(names.flatMap((name) => {
220
+ const value = parsed.get(name)?.trim();
221
+ return value ? [[name, value]] : [];
222
+ }));
223
+ }