@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
package/dist/cli/commands/add.js
CHANGED
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `fastagent add <channel>|skill` — scaffold channel glue (`channels/<kind>.ts`) or vendor an Agent
|
|
3
|
-
* Skills skill. feishu/lark additionally CREATE OR RESUME the platform app
|
|
3
|
+
* Skills skill. slack/feishu/lark additionally CREATE OR RESUME the platform app.
|
|
4
4
|
*/
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
6
7
|
import { join, relative, resolve } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { isCancel, select } from "@clack/prompts";
|
|
9
|
+
import { onboardFeishuCloudApp } from "../add-feishu.js";
|
|
10
|
+
import { dotEnvPath, loadDotEnv } from "../../env.js";
|
|
11
|
+
import { resolveStateRoot } from "../../paths.js";
|
|
12
|
+
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
9
13
|
import { detectRuntime, readPackageJson } from "../../runtime.js";
|
|
14
|
+
import { isUnderDir } from "../../engines/pi/definition.js";
|
|
15
|
+
import { displayPath } from "../../paths.js";
|
|
10
16
|
import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, } from "../../scaffold/add-channel.js";
|
|
17
|
+
import { exists } from "../../paths.js";
|
|
11
18
|
import { vendorSkill } from "../../scaffold/vendor-skill.js";
|
|
12
|
-
import {
|
|
13
|
-
import { failStartup, failUsage } from "../fail.js";
|
|
19
|
+
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
14
20
|
/** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
|
|
15
21
|
export async function runAddChannel(channelKind, dirArg, opts) {
|
|
16
|
-
|
|
22
|
+
// The channel (glue + companion tool + secrets) is agent surface — everything lands in the
|
|
23
|
+
// AGENT DIR (`fastagent/`), the same place dev/start discover channels/.
|
|
24
|
+
// Flag conflicts first: a bad combination is a USAGE error (exit 2), and reporting it must not depend
|
|
25
|
+
// on the directory being an agent (a runtime/environment failure, exit 1) — the same order start/tool
|
|
26
|
+
// follow.
|
|
27
|
+
if (opts.replaceConfig && opts.onboard === false) {
|
|
28
|
+
failUsage("--replace-config replaces onboarding credentials; it cannot be combined with --no-onboard");
|
|
29
|
+
}
|
|
30
|
+
const { agentDir: target } = placementOrExit(resolve(dirArg));
|
|
31
|
+
loadDotEnv(target); // onboarding state follows the same FASTAGENT_STATE_DIR as serving/deploy
|
|
32
|
+
// Paths are printed to someone standing in their CWD, usually the workspace, while every file
|
|
33
|
+
// belongs to the AGENT dir — prefix them, or they point at nothing. `displayPath` owns the
|
|
34
|
+
// relative-vs-absolute policy (shared with `init`). The `.env` label names the file actually
|
|
35
|
+
// WRITTEN (FASTAGENT_SECRETS_DIR relocates it, possibly out of the agent), never the default spelling.
|
|
36
|
+
const agentFromCwd = displayPath(process.cwd(), target);
|
|
37
|
+
const inAgent = (p) => (agentFromCwd === undefined ? p : join(agentFromCwd, p));
|
|
38
|
+
const envPath = dotEnvPath(target);
|
|
39
|
+
const envLabel = isUnderDir(envPath, target) ? inAgent(relative(target, envPath)) : envPath;
|
|
17
40
|
// App creation is not a flag — it is what `add feishu` IS (the scan-to-create flow is the default
|
|
18
41
|
// and only path there). The retired --create-app spelling gets a pointer, not silence.
|
|
19
42
|
if (opts.createApp) {
|
|
@@ -27,89 +50,199 @@ export async function runAddChannel(channelKind, dirArg, opts) {
|
|
|
27
50
|
failStartup(new Error("--create-app is retired — app creation is the default behavior of `add feishu`"));
|
|
28
51
|
}
|
|
29
52
|
}
|
|
30
|
-
//
|
|
31
|
-
// target when flat), the same place dev/start discover channels/. .env(.example) and the secret
|
|
32
|
-
// hygiene stay at the run root, where .env is actually read.
|
|
33
|
-
const { config: addConfig } = await loadConfig(target).catch(failStartup);
|
|
34
|
-
const channelHome = resolveAgentDir(target, addConfig);
|
|
35
|
-
// Preconditions before the write, so a refusal is side-effect-free. feishu/lark are exceptions:
|
|
53
|
+
// Preconditions before the write, so a refusal is side-effect-free. slack/feishu/lark are exceptions:
|
|
36
54
|
// their add is scaffold + ONBOARD THE APP, so an existing scaffold skips the write and continues (a
|
|
37
|
-
// failed or
|
|
38
|
-
const file = join(
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
// failed/cancelled app or OAuth flow must be re-runnable without hand-deleting authored glue).
|
|
56
|
+
const file = join(target, "channels", `${channelKind}.ts`);
|
|
57
|
+
const slackToolFile = join(target, "tools", "slack-send.ts");
|
|
58
|
+
const slackToolExisted = channelKind === "slack" ? await exists(slackToolFile) : false;
|
|
59
|
+
const existsAlready = await channelExists(target, channelKind).catch(failStartup);
|
|
60
|
+
const ingress = await resolveIngress(channelKind, file, existsAlready, opts.ingress);
|
|
61
|
+
const groupBehavior = await resolveGroupBehavior(channelKind, opts.groupBehavior);
|
|
62
|
+
if (existsAlready) {
|
|
63
|
+
if (channelKind !== "slack" && channelKind !== "feishu" && channelKind !== "lark") {
|
|
41
64
|
failStartup(new Error(`${relative(target, file)} already exists — edit it, or remove it to re-scaffold`));
|
|
42
65
|
}
|
|
43
66
|
console.error(`[fastagent] ${relative(target, file)} already exists — keeping it`);
|
|
44
67
|
}
|
|
45
68
|
else {
|
|
46
|
-
await assertChannelReady(
|
|
47
|
-
await scaffoldChannel(
|
|
69
|
+
await assertChannelReady(target).catch(failStartup);
|
|
70
|
+
await scaffoldChannel(target, channelKind, { ingress, groupBehavior: groupBehavior.behavior }).catch(failStartup);
|
|
48
71
|
console.error(`[fastagent] created ${relative(target, file)}`);
|
|
72
|
+
if (channelKind === "slack") {
|
|
73
|
+
console.error(`[fastagent] ${slackToolExisted ? "kept existing" : "created"} ${relative(target, slackToolFile)}`);
|
|
74
|
+
}
|
|
49
75
|
}
|
|
50
|
-
if (await appendChannelEnv(target, channelKind).catch(failStartup)) {
|
|
51
|
-
console.error(`[fastagent] added ${channelKind} env vars to .env.example`);
|
|
52
|
-
}
|
|
53
|
-
// Secret hygiene: a channel's GENERATED secret (a random string the user contributes nothing to) is
|
|
54
|
-
// written into `.env` — but only when `.env` is already gitignored: the CLI must never materialize a
|
|
55
|
-
// secret into a committable file. Warn, not refuse, when it is exposed — channel glue may read a real
|
|
56
|
-
// env var instead.
|
|
57
|
-
const envIgnored = (await loadRootIgnore(target).catch(failStartup))?.ignores(".env") ?? false;
|
|
58
|
-
if (!envIgnored) {
|
|
59
|
-
console.error(`[fastagent] warn: .env is not gitignored — a deploy that copies the directory would ship a secret placed there; add .env to .gitignore/.fastagentignore, or use a real env var`);
|
|
76
|
+
if (await appendChannelEnv(target, channelKind, ingress).catch(failStartup)) {
|
|
77
|
+
console.error(`[fastagent] added ${channelKind} env vars to ${inAgent(join(SECRETS_DIRNAME, ".env.example"))}`);
|
|
60
78
|
}
|
|
61
|
-
//
|
|
62
|
-
// its
|
|
63
|
-
// generic .env write below.
|
|
79
|
+
// Stateful app onboarding is re-runnable after the scaffold boundary. Slack's internal-app path
|
|
80
|
+
// persists its manifest/OAuth recovery state separately and writes runtime secrets directly.
|
|
64
81
|
let created;
|
|
65
|
-
if (channelKind === "
|
|
66
|
-
|
|
82
|
+
if (channelKind === "slack" && opts.onboard !== false) {
|
|
83
|
+
const { onboardSlackInternalApp } = await import("../add-slack.js");
|
|
84
|
+
created = await onboardSlackInternalApp({
|
|
85
|
+
target,
|
|
86
|
+
stateRoot: resolveStateRoot(target),
|
|
87
|
+
groupBehavior,
|
|
88
|
+
replaceConfig: opts.replaceConfig,
|
|
89
|
+
})
|
|
90
|
+
.then(() => undefined)
|
|
91
|
+
.catch(failStartup);
|
|
67
92
|
}
|
|
68
|
-
|
|
93
|
+
else if (channelKind === "feishu" || channelKind === "lark") {
|
|
94
|
+
created = await onboardFeishuCloudApp(target, channelKind, ingress, groupBehavior).catch(failStartup);
|
|
95
|
+
}
|
|
96
|
+
const setup = channelSetup(channelKind, ingress, groupBehavior.behavior);
|
|
97
|
+
const env = setup.env;
|
|
98
|
+
const steps = channelKind === "slack" && opts.onboard !== false
|
|
99
|
+
? [
|
|
100
|
+
`Slack internal app created/configured/installed through OAuth; runtime credentials are in ${envLabel}`,
|
|
101
|
+
"run fastagent dev --tunnel to replace the temporary Events API URL automatically",
|
|
102
|
+
"invite the app to every channel it should read",
|
|
103
|
+
"the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
|
|
104
|
+
]
|
|
105
|
+
: setup.steps;
|
|
69
106
|
const generated = Object.fromEntries(env.filter((e) => e.generate).map((e) => [e.name, randomBytes(24).toString("hex")]));
|
|
70
107
|
// Kind-neutral: every channel's generated secrets get the same treatment (github's webhook secret is
|
|
71
108
|
// the same class of value as telegram's); guided Lark credentials ride the same write as overwrites.
|
|
72
|
-
// Feishu's irreversible credentials were already staged inside
|
|
73
|
-
const dotEnv =
|
|
74
|
-
|
|
75
|
-
:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
109
|
+
// Feishu's irreversible credentials were already staged inside add-feishu.ts before bootstrap.
|
|
110
|
+
const dotEnv = await appendChannelDotEnv(target, channelKind, { ...generated, ...created }, Object.keys(created ?? {}), ingress).catch(failStartup);
|
|
111
|
+
if (dotEnv.unprotectedSecretsDir) {
|
|
112
|
+
console.error(`[fastagent] note: ${dotEnv.unprotectedSecretsDir} (FASTAGENT_SECRETS_DIR) now holds a secret and has ` +
|
|
113
|
+
`no .gitignore — that directory is yours, so fastagent will not write one into it. Make sure git ` +
|
|
114
|
+
`does not track what is in there.`);
|
|
115
|
+
}
|
|
116
|
+
if (dotEnv.written.length > 0) {
|
|
117
|
+
console.error(`[fastagent] wrote ${dotEnv.written.join(", ")} to ${envLabel}`);
|
|
118
|
+
}
|
|
119
|
+
const install = detectRuntime(target, await readPackageJson(target)).runtime === "bun" ? "bun install" : "npm install";
|
|
82
120
|
console.error(` next steps:`);
|
|
83
|
-
console.error(` ${
|
|
121
|
+
console.error(` ${agentFromCwd === undefined ? install : `(cd ${agentFromCwd} && ${install})`} # if @fastagent-sh/fastagent is not installed yet`);
|
|
84
122
|
for (const e of env) {
|
|
85
|
-
if (dotEnv
|
|
123
|
+
if (dotEnv.alreadySet.includes(e.name))
|
|
86
124
|
continue; // the user already has it — nothing to do
|
|
87
|
-
if (dotEnv
|
|
125
|
+
if (dotEnv.written.includes(e.name)) {
|
|
88
126
|
// Written, but its hint may still carry an action (github: paste the same value into the webhook
|
|
89
127
|
// UI) — keep the variable visible instead of silently absorbing it.
|
|
90
|
-
console.error(` ${e.name} — ${e.generate ? "generated and " : ""}written to
|
|
128
|
+
console.error(` ${e.name} — ${e.generate ? "generated and " : ""}written to ${envLabel} # ${e.hint}`);
|
|
91
129
|
continue;
|
|
92
130
|
}
|
|
93
131
|
const value = e.generate ? `=${generated[e.name]}` : "";
|
|
94
132
|
const action = e.required ? "set" : "optionally set";
|
|
95
|
-
console.error(` ${action} ${e.name}${value} in
|
|
133
|
+
console.error(` ${action} ${e.name}${value} in ${envLabel} # ${e.hint}`);
|
|
96
134
|
}
|
|
97
135
|
// Steps carry `{channel}`/`{tools}` path placeholders (their filenames are the scaffold's private
|
|
98
|
-
// knowledge) — resolve them to the real
|
|
99
|
-
const kitPrefix = channelHome === target ? "" : `${relative(target, channelHome)}/`;
|
|
136
|
+
// knowledge) — resolve them to the real agent-dir-relative locations here.
|
|
100
137
|
for (const s of steps) {
|
|
101
|
-
console.error(` ${s.replace("{channel}", relative(target, file)).replace("{tools}",
|
|
138
|
+
console.error(` ${s.replace("{channel}", inAgent(relative(target, file))).replace("{tools}", inAgent("tools"))}`);
|
|
139
|
+
}
|
|
140
|
+
if (ingress === "websocket") {
|
|
141
|
+
console.error(` fastagent dev # no public URL or tunnel required`);
|
|
142
|
+
}
|
|
143
|
+
else if (channelKind === "slack") {
|
|
144
|
+
console.error(` fastagent dev --tunnel # serve locally; ${opts.onboard === false ? "print the Request URL for manual Slack setup" : "auto-update the onboarded Slack Request URL"}`);
|
|
145
|
+
}
|
|
146
|
+
else if (channelKind === "github") {
|
|
147
|
+
console.error(` fastagent dev --tunnel # serve locally + print the URL for manual GitHub webhook setup`);
|
|
102
148
|
}
|
|
103
|
-
if (channelKind !== "lark") {
|
|
149
|
+
else if (channelKind !== "lark") {
|
|
104
150
|
console.error(` fastagent dev --tunnel # serve locally + a public URL, auto-registering the webhook`);
|
|
105
151
|
}
|
|
106
|
-
//
|
|
107
|
-
//
|
|
152
|
+
// App-creation flows leave platform/tunnel sockets behind that would otherwise hold the one-shot
|
|
153
|
+
// scaffold command open after all durable boundaries have completed — exit crisply.
|
|
108
154
|
process.exit(0);
|
|
109
155
|
}
|
|
156
|
+
async function resolveIngress(kind, file, existsAlready, raw) {
|
|
157
|
+
if (raw !== undefined && raw !== "webhook" && raw !== "websocket") {
|
|
158
|
+
failUsage(`--ingress must be "webhook" or "websocket", got "${raw}"`);
|
|
159
|
+
}
|
|
160
|
+
const requested = raw;
|
|
161
|
+
if (kind !== "feishu" && kind !== "lark")
|
|
162
|
+
return "webhook";
|
|
163
|
+
if (existsAlready) {
|
|
164
|
+
const source = await readFile(file, "utf8").catch(failStartup);
|
|
165
|
+
const factory = source.match(/\b(?:feishu|lark)(WebSocket)?Channel\s*\(/);
|
|
166
|
+
const existing = factory
|
|
167
|
+
? factory[1] === "WebSocket"
|
|
168
|
+
? "websocket"
|
|
169
|
+
: "webhook"
|
|
170
|
+
: undefined;
|
|
171
|
+
if (!existing) {
|
|
172
|
+
if (!requested) {
|
|
173
|
+
failUsage(`${file} uses an unrecognized channel factory — re-run with --ingress webhook|websocket to confirm its mode`);
|
|
174
|
+
}
|
|
175
|
+
return requested;
|
|
176
|
+
}
|
|
177
|
+
if (requested && requested !== existing) {
|
|
178
|
+
failStartup(new Error(`${file} already selects ${existing} delivery — changing mode is a migration; edit the factory and platform subscription together`));
|
|
179
|
+
}
|
|
180
|
+
return existing;
|
|
181
|
+
}
|
|
182
|
+
if (requested)
|
|
183
|
+
return requested;
|
|
184
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
185
|
+
console.error(`[fastagent] no interactive terminal — defaulting ${kind} ingress to webhook (use --ingress websocket)`);
|
|
186
|
+
return "webhook";
|
|
187
|
+
}
|
|
188
|
+
const answer = await select({
|
|
189
|
+
message: `How should ${kind === "feishu" ? "Feishu" : "Lark"} deliver events?`,
|
|
190
|
+
options: [
|
|
191
|
+
{
|
|
192
|
+
value: "websocket",
|
|
193
|
+
label: "WebSocket long connection",
|
|
194
|
+
hint: "no public URL; requires an always-on process",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
value: "webhook",
|
|
198
|
+
label: "Webhook endpoint",
|
|
199
|
+
hint: "supports scale-to-zero; requires a public HTTPS URL",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
if (isCancel(answer))
|
|
204
|
+
failStartup(new Error(`${kind} onboarding cancelled`));
|
|
205
|
+
return answer;
|
|
206
|
+
}
|
|
207
|
+
async function resolveGroupBehavior(kind, raw) {
|
|
208
|
+
if (raw !== undefined && raw !== "context" && raw !== "mentions") {
|
|
209
|
+
failUsage(`--group-behavior must be "context" or "mentions", got "${raw}"`);
|
|
210
|
+
}
|
|
211
|
+
if (kind !== "feishu" && kind !== "lark" && kind !== "slack")
|
|
212
|
+
return { behavior: "context", explicit: false };
|
|
213
|
+
if (raw !== undefined)
|
|
214
|
+
return { behavior: raw, explicit: true };
|
|
215
|
+
const defaultBehavior = "context";
|
|
216
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
217
|
+
console.error(`[fastagent] no interactive terminal — assuming ${kind} group behavior ${defaultBehavior}; pass --group-behavior explicitly to override`);
|
|
218
|
+
return { behavior: defaultBehavior, explicit: false };
|
|
219
|
+
}
|
|
220
|
+
const choices = [
|
|
221
|
+
{
|
|
222
|
+
value: "context",
|
|
223
|
+
label: "Context-aware groups (recommended)",
|
|
224
|
+
hint: kind === "slack"
|
|
225
|
+
? "bare replies in the Agent's threads + buffer; requires channel/group/mpim history scopes"
|
|
226
|
+
: "bare replies in the Agent's threads + buffer; im:message.group_msg delivers all group messages",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
value: "mentions",
|
|
230
|
+
label: "Mention-only (least privilege)",
|
|
231
|
+
hint: "only explicit @Agent messages; no group-wide message permission",
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
const answer = await select({
|
|
235
|
+
message: "Choose group-chat behavior",
|
|
236
|
+
initialValue: defaultBehavior,
|
|
237
|
+
options: choices,
|
|
238
|
+
});
|
|
239
|
+
if (isCancel(answer))
|
|
240
|
+
failStartup(new Error(`${kind} onboarding cancelled`));
|
|
241
|
+
return { behavior: answer, explicit: true };
|
|
242
|
+
}
|
|
110
243
|
/** `fastagent add skill <source> [dir]`: vendor an Agent Skills skill into <dir>/skills/<name>/. */
|
|
111
244
|
export async function runAddSkill(source, dirArg, opts) {
|
|
112
|
-
const target = resolve(dirArg);
|
|
245
|
+
const { agentDir: target } = placementOrExit(resolve(dirArg));
|
|
113
246
|
if (!source) {
|
|
114
247
|
// A missing source is a usage error (exit 2), but the guide is worth more than a bare
|
|
115
248
|
// missing-argument line — the common path (writing your own skill) needs no command at all.
|
|
@@ -122,10 +255,8 @@ export async function runAddSkill(source, dirArg, opts) {
|
|
|
122
255
|
` bare name found in your global skill dirs (~/.agents/skills, ~/.pi/agent/skills)\n` +
|
|
123
256
|
` --update overwrites an existing skill (re-fetch from source); review with git diff`);
|
|
124
257
|
}
|
|
125
|
-
// Skills are agent surface — vendored into
|
|
126
|
-
const {
|
|
127
|
-
const skillHome = resolveAgentDir(target, skillConfig);
|
|
128
|
-
const { name, description, dest, hasScripts, diagnostics, overwritten } = await vendorSkill(skillHome, source, {
|
|
258
|
+
// Skills are agent surface — vendored into the agent dir's `skills/`.
|
|
259
|
+
const { name, description, dest, hasScripts, diagnostics, overwritten } = await vendorSkill(target, source, {
|
|
129
260
|
update: opts.update ?? false,
|
|
130
261
|
}).catch(failStartup);
|
|
131
262
|
console.error(`[fastagent] ${overwritten ? "updated" : "vendored"} skill "${name}" → ${dest}/`);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { SessionControl } from "../../session.ts";
|
|
2
|
+
export interface AttachOptions {
|
|
3
|
+
/** Override the control endpoint (skip control.json discovery) — for a remote serve. */
|
|
4
|
+
url?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function runAttach(sessionArg: string, dirArg: string | undefined, opts: AttachOptions): Promise<void>;
|
|
8
|
+
/** One round's observed facts, gathered by the loop (IO) and judged by {@link decideRound} (pure). */
|
|
9
|
+
export type RoundOutcome = {
|
|
10
|
+
type: "progress";
|
|
11
|
+
}
|
|
12
|
+
/** Clean end that delivered nothing — indistinguishable from a half-dead proxy closing every stream. */
|
|
13
|
+
| {
|
|
14
|
+
type: "empty";
|
|
15
|
+
} | {
|
|
16
|
+
type: "error";
|
|
17
|
+
error: unknown;
|
|
18
|
+
isAuth: boolean;
|
|
19
|
+
/** discover(dir) compared against the current endpoint; remote endpoints report "unavailable". */
|
|
20
|
+
discovery: "unchanged" | "changed" | "unavailable";
|
|
21
|
+
/** The freshly discovered credentials when {@link discovery} is "changed". */
|
|
22
|
+
fresh?: {
|
|
23
|
+
url: string;
|
|
24
|
+
token: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export type RoundDecision = {
|
|
28
|
+
kind: "reset";
|
|
29
|
+
} | {
|
|
30
|
+
kind: "exit";
|
|
31
|
+
message: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "try-reattach";
|
|
34
|
+
fresh: {
|
|
35
|
+
url: string;
|
|
36
|
+
token: string;
|
|
37
|
+
};
|
|
38
|
+
} | {
|
|
39
|
+
kind: "retry";
|
|
40
|
+
warn: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The reconnect policy for BOTH phases (`startup` = connectWithGrace, `steady` = the round loop),
|
|
44
|
+
* pure and testable: every exit diagnosis and budget claim lives here. `downMs` is wall-clock time
|
|
45
|
+
* since the phase's anchor (first connect attempt / first non-progress round). The phases differ
|
|
46
|
+
* on priors, not principle: startup has never succeeded (short budget; a remote non-auth error
|
|
47
|
+
* exits at once), steady-state had a working attach (longer budgets); the local-401-unchanged
|
|
48
|
+
* diagnosis and reattach-on-changed-credentials apply identically to both.
|
|
49
|
+
*/
|
|
50
|
+
export declare function decideRound(outcome: RoundOutcome, ctx: {
|
|
51
|
+
discovered: boolean;
|
|
52
|
+
downMs: number;
|
|
53
|
+
phase?: "startup" | "steady";
|
|
54
|
+
}): RoundDecision;
|
|
55
|
+
/** What one ROUND prints through (streamed deltas included), so tests can observe every path a
|
|
56
|
+
* round writes. Round output only: stdin-side acceptance/rejection lines print directly — they
|
|
57
|
+
* are immediate feedback to a user action and may interleave with a replay block. Timing knobs
|
|
58
|
+
* travel as their own parameter. */
|
|
59
|
+
export interface AttachIo {
|
|
60
|
+
println: (line: string) => void;
|
|
61
|
+
/** Raw chunk, no newline — the streamed message_delta path. */
|
|
62
|
+
write: (chunk: string) => void;
|
|
63
|
+
warn: (line: string) => void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* ONE attach round: subscribe → backfill (render the durable record since `cursor`) → drain live
|
|
67
|
+
* until the stream drops. Returns the advanced cursor. Subscribing first + the server's eager
|
|
68
|
+
* registration (subscribed before response headers) covers the cursor→subscription window in the
|
|
69
|
+
* normal case; `settleMs` is a HEURISTIC — on a very slow link an event can still land between the
|
|
70
|
+
* backfill and the subscription taking effect, surfacing only at the next round's replay. Live
|
|
71
|
+
* events carry no entry ids, so the cursor advances only on backfill; a replay may overlap what
|
|
72
|
+
* was already seen live — labeled, not silently dropped. A 401 (stale token after a serve restart)
|
|
73
|
+
* is thrown to the caller: unrecoverable here, never retried silently.
|
|
74
|
+
*/
|
|
75
|
+
export declare function attachRound(control: SessionControl, session: string, cursor: string | undefined, io: AttachIo,
|
|
76
|
+
/** The subscribe→sync settle heuristic (see the round comment). Tests shrink it. */
|
|
77
|
+
settleMs?: number): Promise<{
|
|
78
|
+
cursor: string | undefined;
|
|
79
|
+
sawProgress: boolean;
|
|
80
|
+
}>;
|
|
81
|
+
/** The cursor a failed round reached, if it recorded one. */
|
|
82
|
+
export declare function roundCursor(error: unknown): string | undefined;
|