@fastagent-sh/fastagent 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +35 -0
- package/dist/channels/context-buffer.js +103 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +115 -0
- package/dist/channels/preview-kit.js +184 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +5 -4
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -3
- package/dist/schedule/wakeups.js +19 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `fastagent add <channel>`: drop a `channels/<kind>.ts` adapter-glue file (+ any companion tool, +
|
|
3
|
-
* `.env.example` vars) into an existing
|
|
4
|
-
*
|
|
5
|
-
* src/channels/<kind>/scaffold/, read here at scaffold time.
|
|
3
|
+
* `.secrets/.env.example` vars) into an existing agent. `add` checks and guides; it never
|
|
4
|
+
* bootstraps an agent (that is `init`'s job). Each channel's template files live in its own bundle
|
|
5
|
+
* at src/channels/<kind>/scaffold/, read here at scaffold time.
|
|
6
6
|
*/
|
|
7
7
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
9
|
import { detectRuntime } from "../runtime.js";
|
|
10
|
-
import {
|
|
11
|
-
import { channelBundleFiles, channelTemplate } from "./templates.js";
|
|
12
|
-
import {
|
|
13
|
-
import { parseEnvContent } from "../env.js";
|
|
10
|
+
import { SECRETS_DIRNAME, assertInsideAgentDir, exists } from "../paths.js";
|
|
11
|
+
import { baseTemplate, channelBundleFiles, channelTemplate } from "./templates.js";
|
|
12
|
+
import { dotEnvPath, envExamplePath, parseEnvContent } from "../env.js";
|
|
14
13
|
const CHANNEL_SCAFFOLDS = {
|
|
15
14
|
github: {
|
|
16
15
|
env: [
|
|
@@ -21,8 +20,8 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
21
20
|
generate: true,
|
|
22
21
|
},
|
|
23
22
|
],
|
|
24
|
-
// `{channel}` / `{tools}` are path placeholders the CLI resolves to the real
|
|
25
|
-
// location
|
|
23
|
+
// `{channel}` / `{tools}` are path placeholders the CLI resolves to the real agent-dir-relative
|
|
24
|
+
// location — the CLI holds no channel-private filenames.
|
|
26
25
|
steps: [
|
|
27
26
|
"edit {channel} — map events to intents in on()",
|
|
28
27
|
"add the webhook in your repo (Settings → Webhooks): Payload URL = <public-url>/webhook, content type application/json",
|
|
@@ -43,11 +42,39 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
43
42
|
"the agent can send messages or files back by calling the scaffolded {tools}/telegram-send.ts tool",
|
|
44
43
|
],
|
|
45
44
|
},
|
|
45
|
+
slack: {
|
|
46
|
+
env: [
|
|
47
|
+
{ name: "SLACK_BOT_TOKEN", hint: "Slack app → rotating Bot User OAuth access token", required: true },
|
|
48
|
+
{
|
|
49
|
+
name: "SLACK_BOT_REFRESH_TOKEN",
|
|
50
|
+
hint: "Slack OAuth bot refresh token (required when token rotation is enabled)",
|
|
51
|
+
required: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "SLACK_BOT_TOKEN_EXPIRES_AT",
|
|
55
|
+
hint: "Slack rotating bot access-token expiry (epoch milliseconds)",
|
|
56
|
+
required: false,
|
|
57
|
+
},
|
|
58
|
+
{ name: "SLACK_CLIENT_ID", hint: "Slack app OAuth client ID (for bot-token rotation)", required: false },
|
|
59
|
+
{
|
|
60
|
+
name: "SLACK_CLIENT_SECRET",
|
|
61
|
+
hint: "Slack app OAuth client secret (for bot-token rotation)",
|
|
62
|
+
required: false,
|
|
63
|
+
},
|
|
64
|
+
{ name: "SLACK_SIGNING_SECRET", hint: "Slack app → Basic Information → App Credentials", required: true },
|
|
65
|
+
],
|
|
66
|
+
steps: [
|
|
67
|
+
"Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write, channels:history, groups:history, mpim:history",
|
|
68
|
+
"enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, message.im, message.channels, message.groups, message.mpim; set Request URL to <public-url>/slack",
|
|
69
|
+
"reinstall the app after changing scopes, then invite it to each channel it should read",
|
|
70
|
+
"the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
|
|
71
|
+
],
|
|
72
|
+
},
|
|
46
73
|
// Feishu is the canonical engine/cloud; Lark international reuses its protocol through a degraded
|
|
47
74
|
// compatibility profile. Each remains its own channel KIND: route, env, state, console, onboarding.
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
75
|
+
// This table is the webhook setup; continuous mode below selects only App ID/Secret. No `generate`
|
|
76
|
+
// in either: values come FROM the platform. `add feishu` scan-creates the app; Lark lacks that
|
|
77
|
+
// control-plane capability, so `add lark` guides console credential collection.
|
|
51
78
|
feishu: {
|
|
52
79
|
env: [
|
|
53
80
|
{
|
|
@@ -72,8 +99,8 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
72
99
|
},
|
|
73
100
|
],
|
|
74
101
|
steps: [
|
|
75
|
-
"
|
|
76
|
-
"PUBLISH the app version
|
|
102
|
+
"before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (the CLI adds it to the app draft when supported); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
|
|
103
|
+
"PUBLISH the app version in the developer console after permission approval — the switch to webhook mode takes effect on publish (one click, once ever; no API for it)",
|
|
77
104
|
"edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
|
|
78
105
|
"the event Request URL is auto-registered by `dev --tunnel` / `deploy --run`",
|
|
79
106
|
"the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
|
|
@@ -96,7 +123,7 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
96
123
|
],
|
|
97
124
|
steps: [
|
|
98
125
|
"finish the console setup: enable Bot and add the required permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
|
|
99
|
-
"
|
|
126
|
+
"before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (add it manually if Lark's config API fallback was used); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
|
|
100
127
|
"run `fastagent dev --tunnel` and keep it running; if auto-registration reports a config-API 404, manually switch Subscription mode to webhook, set its printed https://…/lark Request URL, save, then create + publish a version",
|
|
101
128
|
"the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
|
|
102
129
|
],
|
|
@@ -104,18 +131,62 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
104
131
|
};
|
|
105
132
|
/** The channel kinds `fastagent add <kind>` can scaffold. */
|
|
106
133
|
export const CHANNEL_KINDS = Object.keys(CHANNEL_SCAFFOLDS);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
const WEBSOCKET_SETUPS = {
|
|
135
|
+
feishu: {
|
|
136
|
+
env: CHANNEL_SCAFFOLDS.feishu.env.filter((entry) => ["FEISHU_APP_ID", "FEISHU_APP_SECRET"].includes(entry.name)),
|
|
137
|
+
steps: [
|
|
138
|
+
"before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (the CLI adds it to the app draft when supported); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
|
|
139
|
+
"PUBLISH the app version in the developer console after permission approval — long-connection event subscriptions become active with the published version",
|
|
140
|
+
"edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
|
|
141
|
+
"run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
|
|
142
|
+
"the agent can push messages from scheduled turns via the scaffolded {tools}/feishu-send.ts tool",
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
lark: {
|
|
146
|
+
env: CHANNEL_SCAFFOLDS.lark.env.filter((entry) => ["LARK_APP_ID", "LARK_APP_SECRET"].includes(entry.name)),
|
|
147
|
+
steps: [
|
|
148
|
+
"before publishing: approve the sensitive im:message.group_msg permission for context-aware groups (add it manually if Lark's config API fallback was used); it delivers all group messages so bare replies in the agent's own threads can invoke and other unsummoned discussion can buffer. im:message:readonly is requested alongside it so a thread's opening ask can carry the message it quotes; both need the same approval",
|
|
149
|
+
"in Events & Callbacks choose long connection, subscribe im.message.receive_v1, then create + publish a version",
|
|
150
|
+
"edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
|
|
151
|
+
"run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
|
|
152
|
+
"the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
/** The mode-specific env vars + next-step lines a scaffolded channel needs. */
|
|
157
|
+
export function channelSetup(kind, ingress = "webhook", groupBehavior) {
|
|
158
|
+
const behavior = groupBehavior ?? "context";
|
|
159
|
+
const setup = ingress === "websocket" && (kind === "feishu" || kind === "lark")
|
|
160
|
+
? WEBSOCKET_SETUPS[kind]
|
|
161
|
+
: CHANNEL_SCAFFOLDS[kind];
|
|
162
|
+
if ((kind === "feishu" || kind === "lark") && behavior === "mentions") {
|
|
163
|
+
return {
|
|
164
|
+
env: setup.env,
|
|
165
|
+
steps: setup.steps.map((step) => step.includes("im:message.group_msg")
|
|
166
|
+
? "group behavior: mention-only — do not grant im:message.group_msg; bare thread replies and group context buffering remain disabled. im:message:readonly is independent of this choice: add it if you want an @mention to carry the message it quotes (without it that quote degrades to a marker)"
|
|
167
|
+
: step),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (kind === "slack" && behavior === "mentions") {
|
|
171
|
+
return {
|
|
172
|
+
env: setup.env,
|
|
173
|
+
steps: [
|
|
174
|
+
"Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write (no channel/group/mpim history scopes)",
|
|
175
|
+
"enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, and message.im; set Request URL to <public-url>/slack",
|
|
176
|
+
"group behavior: mention-only — bare thread replies and unsummoned group context remain disabled",
|
|
177
|
+
...setup.steps.slice(2),
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { env: setup.env, steps: setup.steps };
|
|
111
182
|
}
|
|
112
183
|
/**
|
|
113
184
|
* Append a channel's env vars (commented placeholders + hints) to `.env.example`, so a developer who
|
|
114
185
|
* copies it to `.env` finds the vars already there. No-op when there is no `.env.example` or the block
|
|
115
186
|
* is already present. Placeholders only — no real secret lands in the committable template.
|
|
116
187
|
*/
|
|
117
|
-
export async function appendChannelEnv(dir, kind) {
|
|
118
|
-
const file =
|
|
188
|
+
export async function appendChannelEnv(dir, kind, ingress = "webhook") {
|
|
189
|
+
const file = envExamplePath(dir);
|
|
119
190
|
let current;
|
|
120
191
|
try {
|
|
121
192
|
current = await readFile(file, "utf8");
|
|
@@ -131,7 +202,9 @@ export async function appendChannelEnv(dir, kind) {
|
|
|
131
202
|
// Hint on its OWN line above the placeholder (like the base env.example template) — never inline
|
|
132
203
|
// after `=`: loadEnvFile does not strip trailing comments, so an uncommented `KEY= # hint` (or a
|
|
133
204
|
// value pasted before the `#`) would carry the hint text into the parsed value.
|
|
134
|
-
const block = `\n${marker}\n${
|
|
205
|
+
const block = `\n${marker}\n${channelSetup(kind, ingress)
|
|
206
|
+
.env.map((e) => `# ${e.hint}\n# ${e.name}=`)
|
|
207
|
+
.join("\n")}\n`;
|
|
135
208
|
await appendFile(file, block);
|
|
136
209
|
return true;
|
|
137
210
|
}
|
|
@@ -146,15 +219,42 @@ function mentionsEnvName(content, name) {
|
|
|
146
219
|
return content.split("\n").some((line) => new RegExp(`^\\s*#?\\s*${name}\\s*=`).test(line));
|
|
147
220
|
}
|
|
148
221
|
/**
|
|
149
|
-
* Append generated channel secrets to the
|
|
150
|
-
*
|
|
222
|
+
* Append generated channel secrets to the agent's `.env` (`.secrets/.env` — never `.env.example`)
|
|
223
|
+
* Existing non-empty values are kept — EXCEPT the names listed in
|
|
151
224
|
* `overwrite`: those are authoritative (e.g. the credentials of an app `add feishu` JUST minted —
|
|
152
225
|
* skipping them for a stale value would silently discard a fresh, unrecoverable secret). Manual values
|
|
153
226
|
* (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
|
|
154
227
|
* ready to edit while no fake secret is committed to the user's mental model.
|
|
155
228
|
*/
|
|
156
|
-
export async function appendChannelDotEnv(dir, kind, generated, overwrite = []) {
|
|
157
|
-
const file =
|
|
229
|
+
export async function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
|
|
230
|
+
const file = dotEnvPath(dir);
|
|
231
|
+
const secretsDir = dirname(file);
|
|
232
|
+
await mkdir(secretsDir, { recursive: true });
|
|
233
|
+
// THE one exception to "fastagent has no opinion about git": the directory it writes secrets into
|
|
234
|
+
// carries its own `.gitignore`. `init` writes it, and so does this — the reachable case where it is
|
|
235
|
+
// missing (a hand-made agent) is exactly the one where the next line mints an unrecoverable app
|
|
236
|
+
// secret. `wx`, so a file the author wrote is never touched; the accepted cost is that someone who
|
|
237
|
+
// DELETED it to track secrets deliberately gets it back once. The risk is not symmetric — that is an
|
|
238
|
+
// annoyance; the other way is a published credential.
|
|
239
|
+
//
|
|
240
|
+
// Scoped to the DEFAULT `<agentDir>/.secrets`, which is fastagent's own directory. A dir named by
|
|
241
|
+
// `FASTAGENT_SECRETS_DIR` belongs to the operator, and this template is `*` plus two negations —
|
|
242
|
+
// dropping it there would hide that directory's OTHER contents from their `git add`, which is a
|
|
243
|
+
// bigger harm than the one it prevents, and inflicted on a path they chose deliberately. They get the
|
|
244
|
+
// fact instead, and own the decision.
|
|
245
|
+
const owned = secretsDir === join(dir, SECRETS_DIRNAME);
|
|
246
|
+
let unprotectedSecretsDir;
|
|
247
|
+
if (owned) {
|
|
248
|
+
// Only EEXIST is tolerable (already protected, or a concurrent writer). A permission/disk failure on
|
|
249
|
+
// the file that keeps credentials out of git must surface, not be swallowed.
|
|
250
|
+
await writeFile(join(secretsDir, ".gitignore"), baseTemplate("secrets.gitignore"), { flag: "wx" }).catch((e) => {
|
|
251
|
+
if (e.code !== "EEXIST")
|
|
252
|
+
throw e;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
else if (!(await exists(join(secretsDir, ".gitignore")))) {
|
|
256
|
+
unprotectedSecretsDir = secretsDir;
|
|
257
|
+
}
|
|
158
258
|
let current = "";
|
|
159
259
|
try {
|
|
160
260
|
current = await readFile(file, "utf8");
|
|
@@ -163,14 +263,15 @@ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [])
|
|
|
163
263
|
if (e.code !== "ENOENT")
|
|
164
264
|
throw e;
|
|
165
265
|
}
|
|
166
|
-
const
|
|
266
|
+
const env = channelSetup(kind, ingress).env;
|
|
267
|
+
const alreadySet = env
|
|
167
268
|
.filter((e) => !overwrite.includes(e.name) && hasActiveEnvValue(current, e.name))
|
|
168
269
|
.map((e) => e.name);
|
|
169
270
|
const lines = [];
|
|
170
271
|
const written = [];
|
|
171
272
|
const contentLines = current.split("\n");
|
|
172
273
|
let replacedInPlace = false;
|
|
173
|
-
for (const e of
|
|
274
|
+
for (const e of env) {
|
|
174
275
|
if (alreadySet.includes(e.name))
|
|
175
276
|
continue;
|
|
176
277
|
const value = generated[e.name];
|
|
@@ -216,7 +317,7 @@ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [])
|
|
|
216
317
|
await appendFile(file, `${prefix}${marker}\n${lines.join("\n")}\n`);
|
|
217
318
|
}
|
|
218
319
|
}
|
|
219
|
-
return { written, alreadySet };
|
|
320
|
+
return { written, alreadySet, unprotectedSecretsDir };
|
|
220
321
|
}
|
|
221
322
|
/** The path `add <kind>` scaffolds to. */
|
|
222
323
|
function channelPath(dir, kind) {
|
|
@@ -230,10 +331,10 @@ export async function channelExists(dir, kind) {
|
|
|
230
331
|
* Scaffold `channels/<kind>.ts` into {@link dir}. Never clobbers an existing file (the glue is
|
|
231
332
|
* authored content). The wx write is the TOCTOU safety net behind {@link channelExists}.
|
|
232
333
|
*/
|
|
233
|
-
export async function scaffoldChannel(dir, kind) {
|
|
334
|
+
export async function scaffoldChannel(dir, kind, options = {}) {
|
|
234
335
|
const channelsDir = join(dir, "channels");
|
|
235
|
-
// Don't write through a channels/ symlink that escapes the
|
|
236
|
-
await
|
|
336
|
+
// Don't write through a channels/ symlink that escapes the agent dir; one inside it is fine.
|
|
337
|
+
await assertInsideAgentDir(dir, "channels");
|
|
237
338
|
const file = channelPath(dir, kind);
|
|
238
339
|
if (await exists(file)) {
|
|
239
340
|
throw new Error(`${file} already exists — edit it, or remove it to re-scaffold`);
|
|
@@ -242,8 +343,42 @@ export async function scaffoldChannel(dir, kind) {
|
|
|
242
343
|
// `channel.ts` is THE adapter (→ channels/<kind>.ts); any other .ts in the bundle is a companion tool
|
|
243
344
|
// (→ tools/<name>, never clobbering an authored one).
|
|
244
345
|
for (const name of channelBundleFiles(kind)) {
|
|
245
|
-
|
|
346
|
+
let content = channelTemplate(kind, name);
|
|
246
347
|
if (name === "channel.ts") {
|
|
348
|
+
if ((kind === "feishu" || kind === "lark") && options.ingress === "websocket") {
|
|
349
|
+
const factory = `${kind}Channel`;
|
|
350
|
+
const wsFactory = `${kind}WebSocketChannel`;
|
|
351
|
+
let configured = content
|
|
352
|
+
.replace(`import { ${factory} }`, `import { ${wsFactory} }`)
|
|
353
|
+
.replace(`export default ${factory}({`, `export default ${wsFactory}({`);
|
|
354
|
+
if (configured === content)
|
|
355
|
+
throw new Error(`${kind} channel template has no factory anchors`);
|
|
356
|
+
const prefix = kind === "feishu" ? "FEISHU" : "LARK";
|
|
357
|
+
const exportAt = configured.indexOf("export default");
|
|
358
|
+
const importEnd = configured.indexOf("\n\n");
|
|
359
|
+
if (exportAt < 0 || importEnd < 0)
|
|
360
|
+
throw new Error(`${kind} channel template header anchors are missing`);
|
|
361
|
+
const brand = kind === "feishu" ? "Feishu" : "Lark";
|
|
362
|
+
configured =
|
|
363
|
+
`${configured.slice(0, importEnd)}\n\n` +
|
|
364
|
+
`// ${brand} WebSocket long connection: the process connects OUT to the platform, so no public URL,\n` +
|
|
365
|
+
`// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long\n` +
|
|
366
|
+
`// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process\n` +
|
|
367
|
+
`// running in production: scale-to-zero/App Sleeping would disconnect ingress.\n` +
|
|
368
|
+
configured.slice(exportAt);
|
|
369
|
+
configured = configured
|
|
370
|
+
.split("\n")
|
|
371
|
+
.filter((line) => !line.includes(`verificationToken: process.env.${prefix}_VERIFICATION_TOKEN`) &&
|
|
372
|
+
!line.includes(`encryptKey: process.env.${prefix}_ENCRYPT_KEY`))
|
|
373
|
+
.join("\n");
|
|
374
|
+
content = configured;
|
|
375
|
+
}
|
|
376
|
+
if (kind === "slack" && options.groupBehavior === "mentions") {
|
|
377
|
+
const configured = content.replace('groupBehavior: "context"', 'groupBehavior: "mentions"');
|
|
378
|
+
if (configured === content)
|
|
379
|
+
throw new Error("slack channel template has no groupBehavior anchor");
|
|
380
|
+
content = configured;
|
|
381
|
+
}
|
|
247
382
|
await writeFile(file, content, { flag: "wx" });
|
|
248
383
|
continue;
|
|
249
384
|
}
|
|
@@ -256,7 +391,7 @@ export async function scaffoldChannel(dir, kind) {
|
|
|
256
391
|
return file;
|
|
257
392
|
}
|
|
258
393
|
/**
|
|
259
|
-
* Verify the
|
|
394
|
+
* Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
|
|
260
395
|
* `@fastagent-sh/fastagent` (the channel file imports it). `add` checks and guides, never bootstraps — that
|
|
261
396
|
* is `init`'s job.
|
|
262
397
|
*/
|
|
@@ -268,11 +403,11 @@ export async function assertChannelReady(dir) {
|
|
|
268
403
|
}
|
|
269
404
|
catch (e) {
|
|
270
405
|
if (e.code === "ENOENT") {
|
|
271
|
-
// `dir` is
|
|
272
|
-
//
|
|
273
|
-
throw new Error(`${dir}: no package.json — a channel adapter is code and needs the
|
|
274
|
-
`
|
|
275
|
-
`
|
|
406
|
+
// `dir` is the AGENT dir, so `fastagent init` here would nest a second agent inside it — the
|
|
407
|
+
// right remedy is the missing manifest (a --minimal init writes none), or init in the workspace.
|
|
408
|
+
throw new Error(`${dir}: no package.json — a channel adapter is code and needs the agent's own manifest. ` +
|
|
409
|
+
`Add a package.json declaring @fastagent-sh/fastagent there (a --minimal init writes none), ` +
|
|
410
|
+
`or run \`fastagent init\` in the workspace for a fresh agent`);
|
|
276
411
|
}
|
|
277
412
|
throw e;
|
|
278
413
|
}
|
package/dist/scaffold/init.d.ts
CHANGED
|
@@ -1,54 +1,45 @@
|
|
|
1
|
+
/** The agent directory name for a raw `--agent-dir` value: the default when unset, and `./bot` read as
|
|
2
|
+
* `bot` — `basename` already says that is what it means, so rejecting the spelling would be pedantry. */
|
|
3
|
+
export declare function agentDirName(raw: string | undefined): string;
|
|
4
|
+
/** Why `name` cannot be an agent directory name, or undefined when it can. It must stay ONE segment
|
|
5
|
+
* inside the target: anything else (a separator, `..`, an absolute path) would land the agent where the
|
|
6
|
+
* one-level lookup cannot see it — an agent nothing would ever serve.
|
|
7
|
+
*
|
|
8
|
+
* Returns the CONSTRAINT, not a sentence: the CLI prefixes the flag it owns and reports it as the usage
|
|
9
|
+
* error it is (exit 2), while {@link scaffoldAgent} prefixes the option name for a programmatic caller,
|
|
10
|
+
* who never passed a flag and should not be told to fix one. */
|
|
11
|
+
export declare function agentDirNameError(name: string): string | undefined;
|
|
1
12
|
export interface ScaffoldOptions {
|
|
2
13
|
/** Scaffold the markdown-only unit (no package.json, no tool, no install) instead of a complete agent. */
|
|
3
14
|
minimal?: boolean;
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Undefined = flat. The CLI decides (jurisdiction detection + flags); this stays mechanical.
|
|
8
|
-
*/
|
|
15
|
+
/** The agent directory's name inside `dir` — default {@link DEFAULT_AGENT_DIRNAME}, `"."` for `dir`
|
|
16
|
+
* itself. ONE path segment: a separator or `..` would put the agent outside the directory the author
|
|
17
|
+
* named, where the one-level lookup could never find it. */
|
|
9
18
|
agentDir?: string;
|
|
10
19
|
}
|
|
11
20
|
export interface ScaffoldResult {
|
|
12
21
|
dir: string;
|
|
13
22
|
/** Whether a complete (code-tool) agent was scaffolded (false for --minimal). */
|
|
14
23
|
complete: boolean;
|
|
15
|
-
/** The
|
|
16
|
-
agentDir
|
|
17
|
-
/** Files written by this run (relative
|
|
24
|
+
/** The agent dir relative to `dir`: the {@link ScaffoldOptions.agentDir} that was used. */
|
|
25
|
+
agentDir: string;
|
|
26
|
+
/** Files written by this run (relative to `dir`). */
|
|
18
27
|
created: string[];
|
|
19
|
-
/** Files that already existed and were
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
/** True if the target already had content before this run (init into an existing/non-empty dir). */
|
|
25
|
-
intoNonEmpty: boolean;
|
|
26
|
-
/** Non-fatal advisories the caller MUST surface. */
|
|
27
|
-
warnings: string[];
|
|
28
|
+
/** Files that already existed and were KEPT untouched. Only reachable with `agentDir: "."` (a
|
|
29
|
+
* subdirectory target is proven empty first): adopting a directory means its `.gitignore`/
|
|
30
|
+
* `package.json` are the author's. The caller surfaces them — silently skipping a file the user
|
|
31
|
+
* expected would be worse. */
|
|
32
|
+
kept: string[];
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* and
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*/
|
|
40
|
-
export declare function detectHostSignals(dir: string): Promise<string[]>;
|
|
41
|
-
/** The `cd` target to show in `init`'s next-steps: the relative path when the target is inside `cwd`,
|
|
42
|
-
* the absolute path when it climbs out (a `../../..` is noise), or undefined when already in `cwd`. */
|
|
43
|
-
export declare function nextStepCd(cwd: string, dir: string): string | undefined;
|
|
44
|
-
/** Does a path exist? (async; shared with the sibling scaffold modules). */
|
|
45
|
-
export declare function exists(p: string): Promise<boolean>;
|
|
46
|
-
/**
|
|
47
|
-
* Scaffold a runnable workspace into {@link dir} (created if missing). Default is a complete agent
|
|
48
|
-
* (persona.md + the writing-great-skills skill + a code tool + package.json); `--minimal` drops the
|
|
49
|
-
* code tool and package.json. The kit goes flat into `dir`, or into {@link ScaffoldOptions.agentDir}
|
|
50
|
-
* (config at the root pointing there). Refuses only an existing fastagent.config.* (the ownership
|
|
51
|
-
* marker — already a workspace); every other pre-existing file (AGENTS.md, .gitignore, package.json)
|
|
52
|
-
* is kept, never overwritten — an existing AGENTS.md is the project's context, adopted as-is.
|
|
35
|
+
* Scaffold a runnable agent into `<dir>/<agentDir>/` — or into `dir` itself when `agentDir` is `"."`
|
|
36
|
+
* (both created if missing). Default is a complete agent (persona.md + the writing-great-skills skill +
|
|
37
|
+
* a code tool + package.json); `--minimal` drops the code tool and package.json.
|
|
38
|
+
*
|
|
39
|
+
* A SUBDIRECTORY target must be empty (any content there is an unfinished agent or something unrelated,
|
|
40
|
+
* and landing persona.md beside it would be a silent mix), while `"."` is a directory being adopted —
|
|
41
|
+
* content is expected. So `"."` KEEPS every file that already exists (reported, never overwritten, never
|
|
42
|
+
* verified) and refuses only on a config, which means the directory is already an agent. An existing
|
|
43
|
+
* AGENTS.md is untouched either way: that is the project's context, adopted as-is.
|
|
53
44
|
*/
|
|
54
|
-
export declare function
|
|
45
|
+
export declare function scaffoldAgent(dir: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
|