@fastagent-sh/fastagent 0.15.0 → 0.16.1
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 +14 -4
- package/dist/bind.d.ts +34 -0
- package/dist/bind.js +74 -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 +266 -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 +5 -0
- package/dist/channels/context-buffer.js +5 -0
- package/dist/channels/feishu/context-buffer.d.ts +13 -8
- package/dist/channels/feishu/context-buffer.js +60 -6
- package/dist/channels/feishu/feishu-api.js +4 -1
- package/dist/channels/feishu/feishu.d.ts +0 -12
- package/dist/channels/feishu/feishu.js +143 -54
- package/dist/channels/feishu/invoke-turn.js +35 -18
- package/dist/channels/feishu/model.d.ts +0 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +21 -7
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.js +3 -2
- 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 +30 -1
- package/dist/channels/feishu/setup-mode.js +27 -1
- package/dist/channels/github/github.js +8 -1
- package/dist/channels/http.js +1 -1
- 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 +7 -1
- package/dist/channels/preview-kit.js +3 -2
- package/dist/channels/slack/parse.d.ts +16 -1
- package/dist/channels/slack/parse.js +46 -3
- package/dist/channels/slack/preview.d.ts +1 -2
- package/dist/channels/slack/preview.js +68 -24
- package/dist/channels/slack/scaffold/channel.ts +5 -5
- package/dist/channels/slack/slack-api.d.ts +3 -23
- package/dist/channels/slack/slack-api.js +6 -22
- package/dist/channels/slack/slack.d.ts +13 -20
- package/dist/channels/slack/slack.js +95 -50
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +19 -12
- package/dist/channels/tasks.d.ts +0 -6
- package/dist/channels/tasks.js +16 -1
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +1 -1
- package/dist/channels/text.d.ts +14 -0
- package/dist/channels/text.js +14 -0
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +7 -0
- package/dist/cli/add-feishu.d.ts +7 -4
- package/dist/cli/add-feishu.js +57 -37
- package/dist/cli/add-slack.d.ts +2 -1
- package/dist/cli/add-slack.js +6 -11
- package/dist/cli/commands/add.js +50 -51
- package/dist/cli/commands/attach.js +8 -4
- package/dist/cli/commands/chat.js +8 -8
- package/dist/cli/commands/deploy.d.ts +14 -1
- package/dist/cli/commands/deploy.js +330 -78
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +38 -25
- package/dist/cli/commands/fire.js +15 -16
- 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 +9 -6
- package/dist/cli/commands/login.js +35 -21
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +101 -37
- package/dist/cli/commands/tool.js +30 -18
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +24 -0
- package/dist/cli/program.js +66 -36
- package/dist/cli/serve.d.ts +52 -8
- package/dist/cli/serve.js +133 -32
- package/dist/cli/shared.d.ts +21 -2
- package/dist/cli/shared.js +44 -18
- 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 +1 -1
- package/dist/deploy/docker/plan.js +12 -17
- package/dist/deploy/fly/plan.d.ts +2 -0
- package/dist/deploy/fly/plan.js +27 -19
- package/dist/deploy/fly/run.d.ts +12 -1
- package/dist/deploy/fly/run.js +36 -2
- package/dist/deploy/preflight.d.ts +11 -5
- package/dist/deploy/preflight.js +235 -65
- package/dist/deploy/railway/plan.d.ts +7 -0
- package/dist/deploy/railway/plan.js +41 -16
- package/dist/deploy/railway/run.d.ts +8 -1
- package/dist/deploy/railway/run.js +7 -2
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +9 -3
- package/dist/dev-supervisor.d.ts +11 -8
- package/dist/dev-supervisor.js +53 -51
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +1 -1
- package/dist/engines/pi/channel.js +5 -5
- package/dist/engines/pi/chat.js +2 -2
- package/dist/engines/pi/config.d.ts +12 -48
- package/dist/engines/pi/config.js +29 -110
- package/dist/engines/pi/create.d.ts +32 -24
- package/dist/engines/pi/create.js +47 -23
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +19 -5
- package/dist/engines/pi/harness.js +3 -5
- package/dist/engines/pi/login.d.ts +1 -1
- package/dist/engines/pi/models.d.ts +3 -3
- package/dist/engines/pi/models.js +1 -1
- package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
- package/dist/engines/pi/{workspace.js → open.js} +27 -29
- package/dist/engines/pi/read-image.d.ts +4 -0
- package/dist/engines/pi/read-image.js +62 -0
- package/dist/engines/pi/search-tools.d.ts +6 -4
- package/dist/engines/pi/search-tools.js +3 -1
- package/dist/engines/pi/session-builder.d.ts +2 -2
- package/dist/engines/pi/session-builder.js +18 -13
- package/dist/engines/pi/tool.d.ts +13 -5
- package/dist/engines/pi/tool.js +4 -0
- package/dist/engines/pi/wake-tool.d.ts +3 -3
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/host/node.d.ts +4 -2
- package/dist/host/node.js +2 -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/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +2 -2
- package/dist/pi.js +2 -2
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +7 -3
- package/dist/scaffold/add-channel.js +55 -29
- 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 +1 -1
- 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 -0
- package/dist/schedule/wakeups.js +18 -0
- package/dist/tunnel.d.ts +3 -3
- package/dist/tunnel.js +7 -7
- package/package.json +7 -4
- package/dist/channels/feishu/owned-threads.d.ts +0 -7
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/slack/owned-threads.d.ts +0 -6
- package/dist/channels/slack/owned-threads.js +0 -43
- 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/paths.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLACEMENT: which directory holds the agent, and which directory the agent works ON — plus the
|
|
3
|
+
* machinery paths that follow from it.
|
|
4
|
+
*
|
|
5
|
+
* ONE marker and ONE rule. `fastagent.config.*` declares an agent; the WORKSPACE is the directory you
|
|
6
|
+
* pointed fastagent at. Nothing here reads a directory NAME, so an agent directory can be called
|
|
7
|
+
* anything — and the same tree answers two ways depending on where you aim it: point at the project and
|
|
8
|
+
* the agent directory inside it answers with the project as its workspace; point at the agent directory
|
|
9
|
+
* itself (all a container may have been shipped) and it works on itself. That is not an ambiguity to
|
|
10
|
+
* resolve but the honest answer — an agent alone on a box has no project to work on, and a rule that
|
|
11
|
+
* insisted otherwise would hand it the container root.
|
|
12
|
+
*
|
|
13
|
+
* Engine-neutral by nature (pure fs/path: existence checks and one shallow scan), so it lives here
|
|
14
|
+
* rather than under engines/pi. Not a filing preference: the scaffold, the deploy planners, the dev
|
|
15
|
+
* watcher and env.ts all need these facts, and routing them through the engine would make neutral
|
|
16
|
+
* modules depend on it for something the engine has no say in.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
19
|
+
import { access } from "node:fs/promises";
|
|
20
|
+
import { realpath } from "node:fs/promises";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
|
+
/**
|
|
24
|
+
* The directory name `init` gives a nested agent (`<workspace>/fastagent/`) unless `--agent-dir` names
|
|
25
|
+
* another. A DEFAULT, not a rule: resolution reads the config marker and never a name, so renaming an
|
|
26
|
+
* agent directory changes nothing about how it resolves. Visible on purpose — the agent directory holds
|
|
27
|
+
* the AUTHOR's content (persona, skills, tool code: code, not tool configuration), so it follows the
|
|
28
|
+
* repo convention for code (a plain directory), while fastagent's own machinery inside it (`.secrets/`,
|
|
29
|
+
* `.state/`) keeps the dot prefix.
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_AGENT_DIRNAME = "fastagent";
|
|
32
|
+
/** The user-global machinery home under `$HOME` — hidden, per the dotfile convention for per-user
|
|
33
|
+
* tool homes (`~/.cargo`, `~/.docker`); unrelated to {@link DEFAULT_AGENT_DIRNAME}, which only names
|
|
34
|
+
* what `init` creates.
|
|
35
|
+
* It carries the same shape inside it as an agent dir does (`~/.fastagent/.secrets/auth.json`), so the
|
|
36
|
+
* resolvers below need no special case: `login` outside any agent simply hands them this directory. */
|
|
37
|
+
export const GLOBAL_HOME_DIR = ".fastagent";
|
|
38
|
+
/** The secrets segment inside an agent dir (or the global home): every PATH fastagent resolves —
|
|
39
|
+
* `.env`, `.env.example`, auth.json, the scaffold's write — derives from it, so they cannot drift
|
|
40
|
+
* apart. `FASTAGENT_SECRETS_DIR` relocates the RESOLVED dir ({@link resolveSecretsDir}), never this
|
|
41
|
+
* name. The scaffold's ignore templates are real files the author owns from `init` on, so they spell
|
|
42
|
+
* their rules out as literal text — renaming this constant means editing them too. */
|
|
43
|
+
export const SECRETS_DIRNAME = ".secrets";
|
|
44
|
+
/** The state segment inside an agent dir — same rule and same template caveat as {@link SECRETS_DIRNAME}. */
|
|
45
|
+
export const STATE_DIRNAME = ".state";
|
|
46
|
+
/** The config filenames, in load precedence. ONE source: the loader (below) and `scaffoldAgent`'s
|
|
47
|
+
* already-an-agent refusal both read this, so "is there a config?" can't diverge between them. */
|
|
48
|
+
export const AGENT_CONFIG_NAMES = ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
|
|
49
|
+
/** The definition paths an agent LOADS content from — the surface a second agent must not be scaffolded
|
|
50
|
+
* inside ({@link agentDefinitionOwner}), because the outer agent would read it as its own skills/tools.
|
|
51
|
+
* NOT evidence of an agent: `tools/` and `skills/` are ordinary names half the world's repositories
|
|
52
|
+
* use, and the config is the only marker. */
|
|
53
|
+
const LOADED_SURFACE = ["persona.md", "skills", "tools", "channels", "schedules"];
|
|
54
|
+
function isDir(p) {
|
|
55
|
+
return statSync(p, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
56
|
+
}
|
|
57
|
+
/** THE marker: a directory that declares itself an agent with a `fastagent.config.*`. One file decides
|
|
58
|
+
* it, at every position — which is why an agent directory needs no reserved name and why there is no
|
|
59
|
+
* zero-config agent. */
|
|
60
|
+
function hasConfig(p) {
|
|
61
|
+
return isDir(p) && AGENT_CONFIG_NAMES.some((name) => existsSync(join(p, name)));
|
|
62
|
+
}
|
|
63
|
+
/** The agent directories DIRECTLY inside `dir` — the one-level scan that finds an agent without knowing
|
|
64
|
+
* its name. ONE level: deeper is that directory's own workspace, not this one's agent. Sorted, so a
|
|
65
|
+
* refusal names them in a stable order. A missing `dir` yields none (callers resolve paths that may not
|
|
66
|
+
* exist); a permission fault surfaces. */
|
|
67
|
+
function agentChildren(dir) {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
const code = e.code;
|
|
74
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
75
|
+
return [];
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
return entries
|
|
79
|
+
.filter((e) => !e.isFile() && hasConfig(join(dir, e.name)))
|
|
80
|
+
.map((e) => join(dir, e.name))
|
|
81
|
+
.sort();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The agents `dir` resolves over: ITSELF when it holds a config, else the ones directly inside it —
|
|
85
|
+
* never both, because aiming at an agent can only mean that agent. Exported for `init`, which asks the
|
|
86
|
+
* same question this lookup asks: which agents would `dir` resolve over, before and after scaffolding.
|
|
87
|
+
*/
|
|
88
|
+
export function agentsAt(dir) {
|
|
89
|
+
const base = resolve(dir);
|
|
90
|
+
return hasConfig(base) ? [base] : agentChildren(base);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* WHICH agent is meant, when a directory holds several: an engineer's, a PM's and a content owner's
|
|
94
|
+
* agent can each drive the same repository, so they must be selectable rather than refused.
|
|
95
|
+
*
|
|
96
|
+
* `FASTAGENT_AGENT` reaches this from the REAL environment only. Placement resolves before anything
|
|
97
|
+
* reads the agent's `.env` — it is what tells the loader which agent's `.env` to read — so a value set
|
|
98
|
+
* there is a silent no-op (the same ordering caveat `FASTAGENT_SECRETS_DIR` carries). A shell, an
|
|
99
|
+
* `.envrc`, or the command itself.
|
|
100
|
+
*
|
|
101
|
+
* It is an ASSERTION, not a preference: it names the agent, and a directory without one
|
|
102
|
+
* by that name resolves to nothing — even when exactly one agent is sitting there. Serving a DIFFERENT
|
|
103
|
+
* agent than the one asked for is the silent-wrong-target this codebase refuses everywhere else, and the
|
|
104
|
+
* uniform rule ("it names the agent") beats one that changes meaning with the sibling count. The stated
|
|
105
|
+
* cost: a `FASTAGENT_AGENT` exported in a shell PROFILE refuses in every unrelated directory it travels
|
|
106
|
+
* into. Scope it per-repo (an `.envrc`) or pass it per-command; the refusal names the way out.
|
|
107
|
+
*
|
|
108
|
+
* With nothing asserted, the {@link DEFAULT_AGENT_DIRNAME} breaks the tie. That is the one place a
|
|
109
|
+
* directory NAME carries weight, and deliberately not an identity rule — the config alone says what IS
|
|
110
|
+
* an agent; the name only decides which already-identified one answers. What it buys: adding a second
|
|
111
|
+
* agent to a working `<workspace>/fastagent/` setup does not break the command everyone already types.
|
|
112
|
+
*/
|
|
113
|
+
function selectAgent(agents, env) {
|
|
114
|
+
const wanted = env.FASTAGENT_AGENT;
|
|
115
|
+
if (wanted)
|
|
116
|
+
return agents.find((a) => basename(a) === wanted);
|
|
117
|
+
const [only, ...rest] = agents;
|
|
118
|
+
return rest.length === 0 ? only : agents.find((a) => basename(a) === DEFAULT_AGENT_DIRNAME);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolve `dir` into its placement, or undefined when nothing selects one agent. The whole rule: `dir`
|
|
122
|
+
* is the WORKSPACE, and the agent is a `fastagent.config.*` holder at it or one level inside
|
|
123
|
+
* ({@link agentsAt}), narrowed by {@link selectAgent} when there are several.
|
|
124
|
+
*/
|
|
125
|
+
function findPlacement(dir, env = process.env) {
|
|
126
|
+
const base = resolve(dir);
|
|
127
|
+
const agentDir = selectAgent(agentsAt(base), env);
|
|
128
|
+
return agentDir === undefined ? undefined : { agentDir, workspace: base };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The one-line hint for "you pointed at the agent, but the project around it is what you meant" — or
|
|
132
|
+
* undefined. The workspace being whatever you aimed at is deliberate (a deployed box may hold nothing
|
|
133
|
+
* but the agent), and the cost is that `cd my-agent && fastagent dev` legitimately narrows the agent's
|
|
134
|
+
* WORKSPACE to its own directory: its cwd, its coding tools' root, and deploy's build context. (②
|
|
135
|
+
* context is not affected — that walk climbs ancestors either way.) Resolution must not guess which you
|
|
136
|
+
* wanted, so this is a HINT — and a hint may use the heuristic ("the parent carries an AGENTS.md or a
|
|
137
|
+
* .git") that a rule may not.
|
|
138
|
+
*
|
|
139
|
+
* It suggests a command only after RUNNING the lookup that command would run, because a hint that dead-ends
|
|
140
|
+
* is worse than none: with several agents beside this one, `..` refuses and names them — sending the
|
|
141
|
+
* reader to a refusal whose own advice points back here.
|
|
142
|
+
*/
|
|
143
|
+
export function workspaceHint({ agentDir, workspace }, env = process.env) {
|
|
144
|
+
if (agentDir !== workspace)
|
|
145
|
+
return undefined;
|
|
146
|
+
const parent = dirname(agentDir);
|
|
147
|
+
if (parent === agentDir)
|
|
148
|
+
return undefined;
|
|
149
|
+
// Would `..` actually serve THIS agent? A parent holding a config is an agent itself, and one holding
|
|
150
|
+
// siblings resolves to none without a selector — neither is the project view asked about here.
|
|
151
|
+
if (findPlacement(parent, env)?.agentDir !== agentDir)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (!["AGENTS.md", ".git"].some((name) => existsSync(join(parent, name))))
|
|
154
|
+
return undefined;
|
|
155
|
+
return `${parent} looks like a project — point fastagent at it (\`..\`) to have the agent work ON it`;
|
|
156
|
+
}
|
|
157
|
+
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair,
|
|
158
|
+
* and without the throw. `login` is the caller: it is the one command allowed to run outside an agent,
|
|
159
|
+
* so it needs the answer as a value rather than as a refusal. */
|
|
160
|
+
export function findAgentDir(dir) {
|
|
161
|
+
return findPlacement(dir)?.agentDir;
|
|
162
|
+
}
|
|
163
|
+
/** The agent `dir` sits INSIDE (the nearest proper ancestor that IS an agent dir), or undefined.
|
|
164
|
+
* Module-private: the two questions callers actually ask are "where do I `cd` to?"
|
|
165
|
+
* ({@link placementDeadEnd}) and "would a new agent here become part of an existing one's definition?"
|
|
166
|
+
* ({@link agentDefinitionOwner}) — and those answers differ (an agent's `src/` is inside it, but is not
|
|
167
|
+
* part of what it loads), so exporting the raw containment fact would invite conflating them.
|
|
168
|
+
* Placement resolution deliberately never walks up — the answer must not depend on how deep you stand
|
|
169
|
+
* — but "you are inside an agent, just not at its root" is the likeliest reason resolution fails, and
|
|
170
|
+
* both the refusal below and `login`'s global-fallback decision need to tell that case apart. Uses the
|
|
171
|
+
* same marker as resolution (an ancestor holding a config), so it never claims a position it cannot
|
|
172
|
+
* justify. */
|
|
173
|
+
function enclosingAgentDir(dir) {
|
|
174
|
+
let candidate = dirname(resolve(dir));
|
|
175
|
+
for (let prev = ""; candidate !== prev; prev = candidate, candidate = dirname(candidate)) {
|
|
176
|
+
if (hasConfig(candidate))
|
|
177
|
+
return candidate;
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The agent whose DEFINITION contains `dir` — scaffolding there would make the new agent part of the
|
|
183
|
+
* outer one's loaded surface rather than an agent of its own. Narrower than {@link enclosingAgentDir} on
|
|
184
|
+
* purpose: an agent owns only what it LOADS ({@link LOADED_SURFACE}); the rest of its directory is the
|
|
185
|
+
* author's tree, where a second agent (a monorepo package, say) is a legitimate thing to create.
|
|
186
|
+
* `enclosingAgentDir` answers a different question ("where do I `cd` to?"), and for that an agent's
|
|
187
|
+
* `src/` genuinely IS inside it.
|
|
188
|
+
*/
|
|
189
|
+
export function agentDefinitionOwner(dir) {
|
|
190
|
+
const base = resolve(dir);
|
|
191
|
+
const agent = enclosingAgentDir(base);
|
|
192
|
+
if (!agent)
|
|
193
|
+
return undefined;
|
|
194
|
+
const [head] = relative(agent, base).split(sep);
|
|
195
|
+
return head && LOADED_SURFACE.includes(head) ? agent : undefined;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Why `dir` is not an agent, when it has its OWN way out — or undefined when it is simply not near one.
|
|
199
|
+
* Two positions qualify: standing INSIDE an agent (its `tools/`, its `src/`), and standing on a
|
|
200
|
+
* directory whose several agents nothing selects between. Both matter because the generic advice ("run
|
|
201
|
+
* `fastagent init`") would not help — the agent already exists, one step away.
|
|
202
|
+
*
|
|
203
|
+
* Exported because `login` is the one command allowed to run outside an agent, and it must tell "truly
|
|
204
|
+
* outside" (→ the global credential) from "a dead end" (→ refuse, like every other command).
|
|
205
|
+
*/
|
|
206
|
+
export function placementDeadEnd(dir, env = process.env) {
|
|
207
|
+
const base = resolve(dir);
|
|
208
|
+
const enclosing = enclosingAgentDir(base);
|
|
209
|
+
if (enclosing) {
|
|
210
|
+
return `${base} is inside the agent ${enclosing} but is not its root — \`cd\` there (or to its workspace) and re-run`;
|
|
211
|
+
}
|
|
212
|
+
const agents = agentsAt(base).map((a) => basename(a));
|
|
213
|
+
const listed = `${base} holds ${agents.length} agent${agents.length === 1 ? "" : "s"} (${agents.join(", ")})`;
|
|
214
|
+
// Asserting a name that is not here is a different mistake from asserting none, and it is worth its own
|
|
215
|
+
// message at ANY count: the value sits in the environment (often a shell profile carried in from
|
|
216
|
+
// somewhere else), so echo it back with what is actually here instead of restating the general rule.
|
|
217
|
+
if (env.FASTAGENT_AGENT && agents.length > 0) {
|
|
218
|
+
return (`${listed}, and FASTAGENT_AGENT asserts "${env.FASTAGENT_AGENT}", which is not one of them — set it ` +
|
|
219
|
+
`to one of those, unset it, or scope it to the repository that needs it (an .envrc)`);
|
|
220
|
+
}
|
|
221
|
+
if (agents.length > 1) {
|
|
222
|
+
return (`${listed} and none of them is named "${DEFAULT_AGENT_DIRNAME}" (the default) — pick one with ` +
|
|
223
|
+
`FASTAGENT_AGENT=<name>, or point fastagent at the one you want (it then works on ITSELF)`);
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolve a directory into its placement — the ONE owner of the rule ({@link findPlacement}): `dir` is
|
|
229
|
+
* the workspace, and the agent is the single `fastagent.config.*` holder at it or one level inside.
|
|
230
|
+
* Placement is never configured and never detected from surroundings; it is that lookup and the
|
|
231
|
+
* directory you pointed at.
|
|
232
|
+
*
|
|
233
|
+
* Anything else throws (fail visibly). Resolution never walks UP — an agent must not be claimed from
|
|
234
|
+
* arbitrarily deep inside it — but the MESSAGE reads the path, so each dead end gets the exit that fits
|
|
235
|
+
* it ({@link placementDeadEnd}).
|
|
236
|
+
*/
|
|
237
|
+
export function resolvePlacement(dir, env = process.env) {
|
|
238
|
+
const placement = findPlacement(dir, env);
|
|
239
|
+
if (!placement) {
|
|
240
|
+
const base = resolve(dir);
|
|
241
|
+
throw new Error(placementDeadEnd(base, env) ??
|
|
242
|
+
`${base} is not a fastagent agent — no fastagent.config.* here, and no directory holding one ` +
|
|
243
|
+
`directly inside; run \`fastagent init\` to scaffold one`);
|
|
244
|
+
}
|
|
245
|
+
return placement;
|
|
246
|
+
}
|
|
247
|
+
/** How to WRITE a path for someone standing in `cwd`: relative when it is inside `cwd`, absolute when
|
|
248
|
+
* it climbs out (a `../../..` is noise), and undefined when it IS `cwd` (nothing to say). ONE policy,
|
|
249
|
+
* shared by `init`'s `cd` step, `add`'s next-steps paths and `fire`'s "looked in" hint — they all answer
|
|
250
|
+
* the same question, which is a placement-PRESENTATION question, not a scaffolding one. */
|
|
251
|
+
export function displayPath(cwd, dir) {
|
|
252
|
+
const rel = relative(cwd, dir);
|
|
253
|
+
if (rel === "")
|
|
254
|
+
return undefined;
|
|
255
|
+
// "Climbs out" is a path-SEGMENT check — rel is ".." or starts with "../" (or "..\" on Windows). A
|
|
256
|
+
// bare startsWith("..") would wrongly flag an in-cwd directory literally named "..agent".
|
|
257
|
+
const escapes = rel === ".." || /^\.\.[/\\]/.test(rel);
|
|
258
|
+
return escapes ? dir : rel;
|
|
259
|
+
}
|
|
260
|
+
/** Does a path exist? Plain fs, no placement in it — it lives here because `paths.ts` is where the
|
|
261
|
+
* neutral path helpers are, and the scaffolder is not a utility home for the CLI and deploy. */
|
|
262
|
+
export async function exists(p) {
|
|
263
|
+
return access(p).then(() => true, () => false);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
|
|
267
|
+
* leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
|
|
268
|
+
* never gets the shell's `~` expansion, so a bare `resolve("~/x")` would silently create a literal `~`
|
|
269
|
+
* directory — a fail-silently footgun for a secret/state path. Expanding here makes `~` mean home
|
|
270
|
+
* everywhere these knobs are read.
|
|
271
|
+
*/
|
|
272
|
+
export function resolveOverridePath(raw) {
|
|
273
|
+
if (!raw)
|
|
274
|
+
return undefined;
|
|
275
|
+
const expanded = raw === "~" ? homedir() : raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw;
|
|
276
|
+
return resolve(expanded);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The resolved state root — the durable machine-state home (sessions/, channels/<kind>/, schedule/,
|
|
280
|
+
* control.json): `FASTAGENT_STATE_DIR` env > `<agentDir>/.state`. Absolute, so channels and the
|
|
281
|
+
* startup report agree regardless of cwd. Definition: mutable runtime state — single lifecycle
|
|
282
|
+
* (precious, survives redeploy), single process; a container points this at its mounted volume.
|
|
283
|
+
* Secrets are NOT here — they live under {@link resolveSecretsDir} (a different deploy lifecycle:
|
|
284
|
+
* secret store vs volume). The finer knob (`FASTAGENT_SESSIONS_DIR`) still overrides its path on top.
|
|
285
|
+
*
|
|
286
|
+
* `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
|
|
287
|
+
* — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
|
|
288
|
+
* DEFAULT (`<root>/.state`) is dir-anchored.
|
|
289
|
+
*/
|
|
290
|
+
export function resolveStateRoot(dir, env = process.env) {
|
|
291
|
+
return resolveOverridePath(env.FASTAGENT_STATE_DIR) ?? join(resolve(dir), STATE_DIRNAME);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The resolved secrets dir — everything fastagent manages that must NEVER leave the machine (the
|
|
295
|
+
* agent's `.env` + auth.json): `FASTAGENT_SECRETS_DIR` env > `<agentDir>/.secrets`. Split from
|
|
296
|
+
* the state root on deploy lifecycle: secrets travel through the host's secret store (env vars / the
|
|
297
|
+
* auth seed), state through a volume. A deployed box sets both env knobs at its volume (e.g.
|
|
298
|
+
* `/data/.secrets`, `/data/.state`) so a seeded-then-ROTATED OAuth credential persists across
|
|
299
|
+
* restarts. The `.env`'s OWN location resolves from the REAL environment — commands locate and load
|
|
300
|
+
* `.env` before anything else, so a `FASTAGENT_SECRETS_DIR` set INSIDE `.env` still relocates
|
|
301
|
+
* auth.json but cannot move the file it is read from (env.ts dotEnvPath).
|
|
302
|
+
*/
|
|
303
|
+
export function resolveSecretsDir(dir, env = process.env) {
|
|
304
|
+
return resolveOverridePath(env.FASTAGENT_SECRETS_DIR) ?? join(resolve(dir), SECRETS_DIRNAME);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
|
|
308
|
+
* absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
|
|
309
|
+
* A missing target is fine (nothing to guard yet).
|
|
310
|
+
*/
|
|
311
|
+
export async function assertInsideAgentDir(agentDir, name) {
|
|
312
|
+
const target = join(agentDir, name);
|
|
313
|
+
const real = await realpath(target).catch((e) => {
|
|
314
|
+
if (e.code === "ENOENT" || e.code === "not_found")
|
|
315
|
+
return undefined;
|
|
316
|
+
throw e;
|
|
317
|
+
});
|
|
318
|
+
if (real === undefined)
|
|
319
|
+
return;
|
|
320
|
+
const root = await realpath(agentDir).catch(() => resolve(agentDir));
|
|
321
|
+
const rel = relative(root, real);
|
|
322
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
323
|
+
throw new Error(`${target} resolves outside the agent dir (${real}) — it must live inside the definition directory; ` +
|
|
324
|
+
`use a real directory or a symlink that stays within it`);
|
|
325
|
+
}
|
|
326
|
+
}
|
package/dist/pi.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { createPiAgent, createPiAgentFromDefinition, type CreatePiAgentFromDefinitionOptions, type CreatePiAgentOptions, } from "./engines/pi/create.ts";
|
|
2
|
-
export { defineTool, loadTools, type DefineToolOptions, type FastagentTool, type ToolCollision, type ToolContext, } from "./engines/pi/tool.ts";
|
|
2
|
+
export { defineTool, loadTools, type DefineToolOptions, type FastagentTool, type MountedTool, type ToolCollision, type ToolContext, } from "./engines/pi/tool.ts";
|
|
3
3
|
export type { ReadonlySessionManager, ToolActivation } from "./engines/pi/tool-context.ts";
|
|
4
4
|
export { z } from "zod";
|
|
5
5
|
export type { AgentTool, ExecutionEnv, Session, SessionTreeEntry, Skill, SkillDiagnostic, } from "@earendil-works/pi-agent-core";
|
|
6
6
|
export { loadChannels, type ChannelCollision } from "./engines/pi/channel.ts";
|
|
7
|
-
export {
|
|
7
|
+
export { createPiAgentFromDir, type CreatePiAgentFromDirOptions, } from "./engines/pi/open.ts";
|
|
8
8
|
export type { LoadedDefinition, SkillCollision } from "./engines/pi/definition.ts";
|
|
9
9
|
export { defineConfig, listModels, resolveModel, type FastagentConfig } from "./engines/pi/config.ts";
|
|
10
10
|
export { inProcessLease, type Lease, type Release, type SessionObserver } from "./engines/pi/invoke.ts";
|
package/dist/pi.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// The pi reference implementation: assembly,
|
|
1
|
+
// The pi reference implementation: assembly, agent discovery, tools, config, models, auth, and state ports.
|
|
2
2
|
export { createPiAgent, createPiAgentFromDefinition, } from "./engines/pi/create.js";
|
|
3
3
|
export { defineTool, loadTools, } from "./engines/pi/tool.js";
|
|
4
4
|
export { z } from "zod";
|
|
5
5
|
export { loadChannels } from "./engines/pi/channel.js";
|
|
6
|
-
export {
|
|
6
|
+
export { createPiAgentFromDir, } from "./engines/pi/open.js";
|
|
7
7
|
export { defineConfig, listModels, resolveModel } from "./engines/pi/config.js";
|
|
8
8
|
export { inProcessLease } from "./engines/pi/invoke.js";
|
|
9
9
|
export { createPiSessionControl, } from "./engines/pi/session-control.js";
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** How to install and run the AGENT: its package.json + lockfile decide, never the surrounding
|
|
2
|
+
* workspace's (whose toolchain is the agent's runtime concern, not fastagent's). */
|
|
3
|
+
export interface AgentRuntime {
|
|
4
|
+
/** The JS runtime the agent targets — drives the generated Dockerfile base + install/run commands
|
|
3
5
|
* and the package-manager hints in `init`/`add`. */
|
|
4
6
|
runtime: "node" | "bun";
|
|
5
7
|
/** For `runtime: "bun"`, the version from package.json's `packageManager: "bun@x"` (undefined if a bun
|
|
@@ -9,14 +11,14 @@ export interface WorkspaceRuntime {
|
|
|
9
11
|
hasLockfile: boolean;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
|
-
* Detect which JS runtime
|
|
14
|
+
* Detect which JS runtime an agent targets: `bun` when package.json's `packageManager` is `bun@…` OR a
|
|
13
15
|
* bun lockfile (bun.lock/bun.lockb) is present, else `node`. `pkg` is the already-parsed package.json (or
|
|
14
16
|
* `{}` when there is none / it is malformed). One source for the deploy Dockerfile and the CLI hints, so
|
|
15
|
-
* they agree on what the
|
|
17
|
+
* they agree on what the agent is.
|
|
16
18
|
*/
|
|
17
19
|
export declare function detectRuntime(dir: string, pkg: {
|
|
18
20
|
packageManager?: unknown;
|
|
19
|
-
}):
|
|
21
|
+
}): AgentRuntime;
|
|
20
22
|
/** Parse `<dir>/package.json`, or `{}` when absent/malformed (a real build surfaces the actual error).
|
|
21
23
|
* The already-parsed input to {@link detectRuntime} and the deploy dep/lockfile checks. */
|
|
22
24
|
export declare function readPackageJson(dir: string): Promise<{
|
package/dist/runtime.js
CHANGED
|
@@ -2,10 +2,10 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
/**
|
|
5
|
-
* Detect which JS runtime
|
|
5
|
+
* Detect which JS runtime an agent targets: `bun` when package.json's `packageManager` is `bun@…` OR a
|
|
6
6
|
* bun lockfile (bun.lock/bun.lockb) is present, else `node`. `pkg` is the already-parsed package.json (or
|
|
7
7
|
* `{}` when there is none / it is malformed). One source for the deploy Dockerfile and the CLI hints, so
|
|
8
|
-
* they agree on what the
|
|
8
|
+
* they agree on what the agent is.
|
|
9
9
|
*/
|
|
10
10
|
export function detectRuntime(dir, pkg) {
|
|
11
11
|
const pm = typeof pkg.packageManager === "string" ? pkg.packageManager : "";
|
|
@@ -35,10 +35,14 @@ export interface DotEnvWriteResult {
|
|
|
35
35
|
written: string[];
|
|
36
36
|
/** Vars already present with a non-empty active value; left untouched and omitted from next steps. */
|
|
37
37
|
alreadySet: string[];
|
|
38
|
+
/** Set when the secrets dir is an operator-chosen one (`FASTAGENT_SECRETS_DIR`) carrying no
|
|
39
|
+
* `.gitignore`: a secret was just written into a directory fastagent does not own, so the caller
|
|
40
|
+
* states the fact rather than dropping a `*`-ignoring file into someone else's path. */
|
|
41
|
+
unprotectedSecretsDir?: string;
|
|
38
42
|
}
|
|
39
43
|
/**
|
|
40
|
-
* Append generated channel secrets to the
|
|
41
|
-
*
|
|
44
|
+
* Append generated channel secrets to the agent's `.env` (`.secrets/.env` — never `.env.example`)
|
|
45
|
+
* Existing non-empty values are kept — EXCEPT the names listed in
|
|
42
46
|
* `overwrite`: those are authoritative (e.g. the credentials of an app `add feishu` JUST minted —
|
|
43
47
|
* skipping them for a stale value would silently discard a fresh, unrecoverable secret). Manual values
|
|
44
48
|
* (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
|
|
@@ -56,7 +60,7 @@ export declare function scaffoldChannel(dir: string, kind: ChannelKind, options?
|
|
|
56
60
|
groupBehavior?: GroupBehavior;
|
|
57
61
|
}): Promise<string>;
|
|
58
62
|
/**
|
|
59
|
-
* Verify the
|
|
63
|
+
* Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
|
|
60
64
|
* `@fastagent-sh/fastagent` (the channel file imports it). `add` checks and guides, never bootstraps — that
|
|
61
65
|
* is `init`'s job.
|
|
62
66
|
*/
|
|
@@ -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",
|
|
@@ -100,7 +99,7 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
100
99
|
},
|
|
101
100
|
],
|
|
102
101
|
steps: [
|
|
103
|
-
"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
|
|
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",
|
|
104
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)",
|
|
105
104
|
"edit {channel} — routing policy (the header walks through the console setup, for hand-made apps)",
|
|
106
105
|
"the event Request URL is auto-registered by `dev --tunnel` / `deploy --run`",
|
|
@@ -124,7 +123,7 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
124
123
|
],
|
|
125
124
|
steps: [
|
|
126
125
|
"finish the console setup: enable Bot and add the required permissions + im.message.receive_v1 event listed in {channel} (do not publish yet)",
|
|
127
|
-
"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
|
|
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",
|
|
128
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",
|
|
129
128
|
"the agent can push messages from scheduled turns via the scaffolded {tools}/lark-send.ts tool",
|
|
130
129
|
],
|
|
@@ -136,7 +135,7 @@ const WEBSOCKET_SETUPS = {
|
|
|
136
135
|
feishu: {
|
|
137
136
|
env: CHANNEL_SCAFFOLDS.feishu.env.filter((entry) => ["FEISHU_APP_ID", "FEISHU_APP_SECRET"].includes(entry.name)),
|
|
138
137
|
steps: [
|
|
139
|
-
"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
|
|
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",
|
|
140
139
|
"PUBLISH the app version in the developer console after permission approval — long-connection event subscriptions become active with the published version",
|
|
141
140
|
"edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
|
|
142
141
|
"run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
|
|
@@ -146,7 +145,7 @@ const WEBSOCKET_SETUPS = {
|
|
|
146
145
|
lark: {
|
|
147
146
|
env: CHANNEL_SCAFFOLDS.lark.env.filter((entry) => ["LARK_APP_ID", "LARK_APP_SECRET"].includes(entry.name)),
|
|
148
147
|
steps: [
|
|
149
|
-
"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
|
|
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",
|
|
150
149
|
"in Events & Callbacks choose long connection, subscribe im.message.receive_v1, then create + publish a version",
|
|
151
150
|
"edit {channel} — routing policy (the scaffold is already set to WebSocket ingress)",
|
|
152
151
|
"run `fastagent dev` without --tunnel; deployments must keep one process running (no scale-to-zero)",
|
|
@@ -164,7 +163,7 @@ export function channelSetup(kind, ingress = "webhook", groupBehavior) {
|
|
|
164
163
|
return {
|
|
165
164
|
env: setup.env,
|
|
166
165
|
steps: setup.steps.map((step) => step.includes("im:message.group_msg")
|
|
167
|
-
? "group behavior: mention-only — do not grant im:message.group_msg; bare
|
|
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)"
|
|
168
167
|
: step),
|
|
169
168
|
};
|
|
170
169
|
}
|
|
@@ -174,7 +173,7 @@ export function channelSetup(kind, ingress = "webhook", groupBehavior) {
|
|
|
174
173
|
steps: [
|
|
175
174
|
"Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write (no channel/group/mpim history scopes)",
|
|
176
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",
|
|
177
|
-
"group behavior: mention-only — bare
|
|
176
|
+
"group behavior: mention-only — bare thread replies and unsummoned group context remain disabled",
|
|
178
177
|
...setup.steps.slice(2),
|
|
179
178
|
],
|
|
180
179
|
};
|
|
@@ -187,7 +186,7 @@ export function channelSetup(kind, ingress = "webhook", groupBehavior) {
|
|
|
187
186
|
* is already present. Placeholders only — no real secret lands in the committable template.
|
|
188
187
|
*/
|
|
189
188
|
export async function appendChannelEnv(dir, kind, ingress = "webhook") {
|
|
190
|
-
const file =
|
|
189
|
+
const file = envExamplePath(dir);
|
|
191
190
|
let current;
|
|
192
191
|
try {
|
|
193
192
|
current = await readFile(file, "utf8");
|
|
@@ -220,15 +219,42 @@ function mentionsEnvName(content, name) {
|
|
|
220
219
|
return content.split("\n").some((line) => new RegExp(`^\\s*#?\\s*${name}\\s*=`).test(line));
|
|
221
220
|
}
|
|
222
221
|
/**
|
|
223
|
-
* Append generated channel secrets to the
|
|
224
|
-
*
|
|
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
|
|
225
224
|
* `overwrite`: those are authoritative (e.g. the credentials of an app `add feishu` JUST minted —
|
|
226
225
|
* skipping them for a stale value would silently discard a fresh, unrecoverable secret). Manual values
|
|
227
226
|
* (e.g. TELEGRAM_BOT_TOKEN from BotFather) are added only as commented placeholders, so the file is
|
|
228
227
|
* ready to edit while no fake secret is committed to the user's mental model.
|
|
229
228
|
*/
|
|
230
229
|
export async function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
|
|
231
|
-
const file =
|
|
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
|
+
}
|
|
232
258
|
let current = "";
|
|
233
259
|
try {
|
|
234
260
|
current = await readFile(file, "utf8");
|
|
@@ -291,7 +317,7 @@ export async function appendChannelDotEnv(dir, kind, generated, overwrite = [],
|
|
|
291
317
|
await appendFile(file, `${prefix}${marker}\n${lines.join("\n")}\n`);
|
|
292
318
|
}
|
|
293
319
|
}
|
|
294
|
-
return { written, alreadySet };
|
|
320
|
+
return { written, alreadySet, unprotectedSecretsDir };
|
|
295
321
|
}
|
|
296
322
|
/** The path `add <kind>` scaffolds to. */
|
|
297
323
|
function channelPath(dir, kind) {
|
|
@@ -307,8 +333,8 @@ export async function channelExists(dir, kind) {
|
|
|
307
333
|
*/
|
|
308
334
|
export async function scaffoldChannel(dir, kind, options = {}) {
|
|
309
335
|
const channelsDir = join(dir, "channels");
|
|
310
|
-
// Don't write through a channels/ symlink that escapes the
|
|
311
|
-
await
|
|
336
|
+
// Don't write through a channels/ symlink that escapes the agent dir; one inside it is fine.
|
|
337
|
+
await assertInsideAgentDir(dir, "channels");
|
|
312
338
|
const file = channelPath(dir, kind);
|
|
313
339
|
if (await exists(file)) {
|
|
314
340
|
throw new Error(`${file} already exists — edit it, or remove it to re-scaffold`);
|
|
@@ -365,7 +391,7 @@ export async function scaffoldChannel(dir, kind, options = {}) {
|
|
|
365
391
|
return file;
|
|
366
392
|
}
|
|
367
393
|
/**
|
|
368
|
-
* Verify the
|
|
394
|
+
* Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
|
|
369
395
|
* `@fastagent-sh/fastagent` (the channel file imports it). `add` checks and guides, never bootstraps — that
|
|
370
396
|
* is `init`'s job.
|
|
371
397
|
*/
|
|
@@ -377,11 +403,11 @@ export async function assertChannelReady(dir) {
|
|
|
377
403
|
}
|
|
378
404
|
catch (e) {
|
|
379
405
|
if (e.code === "ENOENT") {
|
|
380
|
-
// `dir` is
|
|
381
|
-
//
|
|
382
|
-
throw new Error(`${dir}: no package.json — a channel adapter is code and needs the
|
|
383
|
-
`
|
|
384
|
-
`
|
|
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`);
|
|
385
411
|
}
|
|
386
412
|
throw e;
|
|
387
413
|
}
|