@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/scaffold/init.js
CHANGED
|
@@ -1,161 +1,118 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Init: scaffold a runnable fastagent
|
|
2
|
+
* Init: scaffold a runnable fastagent agent, offline. Default = a COMPLETE agent (persona.md +
|
|
3
3
|
* the writing-great-skills skill + a fetch-url code tool + fastagent.config.mjs + package.json +
|
|
4
|
-
* .gitignore); `--minimal` drops the code tool and package.json. persona.md is the agent's
|
|
5
|
-
* (prompt segment ①); an existing AGENTS.md is never written or touched — it is project
|
|
6
|
-
* kept as-is. skills/ and tools/ are the agent's self-editable capabilities (re-read each
|
|
4
|
+
* .gitignore + .secrets/); `--minimal` drops the code tool and package.json. persona.md is the agent's
|
|
5
|
+
* identity (prompt segment ①); an existing AGENTS.md is never written or touched — it is project
|
|
6
|
+
* context (②), kept as-is. skills/ and tools/ are the agent's self-editable capabilities (re-read each
|
|
7
|
+
* turn).
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Placement — no detection and no prompt, just a default and one flag. By DEFAULT the whole agent —
|
|
10
|
+
* definition, config, `.secrets/`, machinery — lands in `<dir>/fastagent/`; the surrounding tree gets
|
|
11
|
+
* ZERO writes and becomes the workspace the agent works on. `--agent-dir <name>` picks another name for
|
|
12
|
+
* that directory (`fastagent.config.*` is the marker of what IS an agent, never the name), and `--agent-dir .`
|
|
13
|
+
* (spelled `--flat`) lands the identical shape in `dir` itself, for the case where the directory IS the
|
|
14
|
+
* agent (a standalone agent repo, a monorepo package).
|
|
15
|
+
*
|
|
16
|
+
* Which of the two a served agent turns out to be is NOT decided here: the workspace is whatever
|
|
17
|
+
* fastagent is later pointed at (resolvePlacement). This module only chooses where the files land — so
|
|
18
|
+
* its one placement duty is to refuse a target that the lookup would not return (see below).
|
|
15
19
|
*
|
|
16
20
|
* Scope: init is best-effort atomic for ORDINARY inputs — it never overwrites existing files,
|
|
17
|
-
* preflights non-directory scaffold parents, and rolls back a partial write (
|
|
18
|
-
*
|
|
21
|
+
* preflights non-directory scaffold parents, and rolls back a partial write (files AND the
|
|
22
|
+
* `fastagent/` tree it created for them, so a retry sees a clean slate). It does not defend against
|
|
19
23
|
* every pathological target state (TOCTOU, FIFOs, disk-full): recover by delete-and-retry.
|
|
20
24
|
*
|
|
21
25
|
* Sibling scaffold modules: add-channel.ts (`add <channel>`), vendor-skill.ts (`add skill`). The files
|
|
22
26
|
* this module writes are real templates under templates/, read through templates.ts.
|
|
23
27
|
*/
|
|
24
|
-
import {
|
|
25
|
-
import { dirname, join,
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { loadRootIgnore } from "../workspace.js";
|
|
29
|
-
import { baseTemplate, configTemplate, packageJson, personaTemplate, toPackageName } from "./templates.js";
|
|
28
|
+
import { lstat, mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
|
|
29
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
30
|
+
import { AGENT_CONFIG_NAMES, DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentDefinitionOwner, agentsAt, displayPath, exists, } from "../paths.js";
|
|
31
|
+
import { baseTemplate, packageJson, toPackageName } from "./templates.js";
|
|
30
32
|
import { fastagentVersion } from "../version.js";
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Jurisdiction signals: evidence that an existing system already claims this tree, so a flat agent kit
|
|
40
|
-
* would put each side's files under the other's jurisdiction (host tsc sweeps agent .ts; fastagent
|
|
41
|
-
* scans host tools/). Three classes, derived from the actual failure modes — a toolchain config, a
|
|
42
|
-
* deploy manifest, or fastagent's convention names already occupied. Any hit → the kit defaults into
|
|
43
|
-
* `./agent`. Deliberately NOT signals: "dir is non-empty", "has package.json", "has src/" — markdown
|
|
44
|
-
* and loose scripts are claimed by nobody, and "a directory is an agent" stays the default. Known
|
|
45
|
-
* tradeoff, decided for visibility: a HAND-BUILT agent dir (skills//tools/ authored for the agent, no
|
|
46
|
-
* config yet) also hits the occupation signal and defaults to ./agent — wrong for that case, but the
|
|
47
|
-
* reason is printed and `--flat` overrides; the reverse default would silently mis-scan a host's dirs.
|
|
48
|
-
*/
|
|
49
|
-
export async function detectHostSignals(dir) {
|
|
50
|
-
// Only ENOENT/ENOTDIR mean "nothing there" (fresh dir → flat). A real IO failure (EACCES…) must
|
|
51
|
-
// surface, not silently decide the layout — init is about to write into this directory anyway.
|
|
52
|
-
const absent = (err) => {
|
|
53
|
-
const code = err.code;
|
|
54
|
-
if (code === "ENOENT" || code === "ENOTDIR")
|
|
55
|
-
return [];
|
|
56
|
-
throw err;
|
|
57
|
-
};
|
|
58
|
-
const signals = [];
|
|
59
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(absent);
|
|
60
|
-
for (const e of entries) {
|
|
61
|
-
if (e.isFile() && (TOOLCHAIN_RE.test(e.name) || DEPLOY_RE.test(e.name)))
|
|
62
|
-
signals.push(e.name);
|
|
63
|
-
}
|
|
64
|
-
for (const name of ["tools", "channels", "skills"]) {
|
|
65
|
-
const st = await lstat(join(dir, name)).catch((err) => {
|
|
66
|
-
absent(err);
|
|
67
|
-
return undefined;
|
|
68
|
-
});
|
|
69
|
-
// Dotfiles (.DS_Store, .gitkeep) are not agent surface — the loaders would never scan them, so
|
|
70
|
-
// they must not count as "occupied" either (the signal mirrors what fastagent would actually scan).
|
|
71
|
-
const occupants = st?.isDirectory()
|
|
72
|
-
? (await readdir(join(dir, name)).catch(absent)).filter((f) => !f.startsWith("."))
|
|
73
|
-
: [];
|
|
74
|
-
if (occupants.length > 0)
|
|
75
|
-
signals.push(`${name}/`);
|
|
76
|
-
}
|
|
77
|
-
return signals.sort();
|
|
33
|
+
/** The agent directory name for a raw `--agent-dir` value: the default when unset, and `./bot` read as
|
|
34
|
+
* `bot` — `basename` already says that is what it means, so rejecting the spelling would be pedantry. */
|
|
35
|
+
export function agentDirName(raw) {
|
|
36
|
+
if (raw === undefined)
|
|
37
|
+
return DEFAULT_AGENT_DIRNAME;
|
|
38
|
+
const trimmed = raw.replace(/^\.[/\\]/, "");
|
|
39
|
+
return trimmed === "" ? raw : trimmed;
|
|
78
40
|
}
|
|
79
|
-
/**
|
|
80
|
-
* the
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
41
|
+
/** Why `name` cannot be an agent directory name, or undefined when it can. It must stay ONE segment
|
|
42
|
+
* inside the target: anything else (a separator, `..`, an absolute path) would land the agent where the
|
|
43
|
+
* one-level lookup cannot see it — an agent nothing would ever serve.
|
|
44
|
+
*
|
|
45
|
+
* Returns the CONSTRAINT, not a sentence: the CLI prefixes the flag it owns and reports it as the usage
|
|
46
|
+
* error it is (exit 2), while {@link scaffoldAgent} prefixes the option name for a programmatic caller,
|
|
47
|
+
* who never passed a flag and should not be told to fix one. */
|
|
48
|
+
export function agentDirNameError(name) {
|
|
49
|
+
if (name === "." || (name !== "" && name !== ".." && name === basename(name)))
|
|
84
50
|
return undefined;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const escapes = rel === ".." || /^\.\.[/\\]/.test(rel);
|
|
88
|
-
return escapes ? dir : rel;
|
|
89
|
-
}
|
|
90
|
-
/** Does a path exist? (async; shared with the sibling scaffold modules). */
|
|
91
|
-
export async function exists(p) {
|
|
92
|
-
return access(p).then(() => true, () => false);
|
|
51
|
+
return (`must be a single directory name (or "." for the target itself) — a path would put the agent outside ` +
|
|
52
|
+
`the target directory, where fastagent would not find it`);
|
|
93
53
|
}
|
|
94
54
|
/**
|
|
95
|
-
* Scaffold a runnable
|
|
96
|
-
* (persona.md + the writing-great-skills skill +
|
|
97
|
-
* code tool
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
55
|
+
* Scaffold a runnable agent into `<dir>/<agentDir>/` — or into `dir` itself when `agentDir` is `"."`
|
|
56
|
+
* (both created if missing). Default is a complete agent (persona.md + the writing-great-skills skill +
|
|
57
|
+
* a code tool + package.json); `--minimal` drops the code tool and package.json.
|
|
58
|
+
*
|
|
59
|
+
* A SUBDIRECTORY target must be empty (any content there is an unfinished agent or something unrelated,
|
|
60
|
+
* and landing persona.md beside it would be a silent mix), while `"."` is a directory being adopted —
|
|
61
|
+
* content is expected. So `"."` KEEPS every file that already exists (reported, never overwritten, never
|
|
62
|
+
* verified) and refuses only on a config, which means the directory is already an agent. An existing
|
|
63
|
+
* AGENTS.md is untouched either way: that is the project's context, adopted as-is.
|
|
101
64
|
*/
|
|
102
|
-
export async function
|
|
65
|
+
export async function scaffoldAgent(dir, options = {}) {
|
|
103
66
|
const minimal = options.minimal ?? false;
|
|
104
|
-
const
|
|
67
|
+
const root = agentDirName(options.agentDir);
|
|
68
|
+
const flat = root === ".";
|
|
69
|
+
const invalid = agentDirNameError(root);
|
|
70
|
+
if (invalid)
|
|
71
|
+
throw new Error(`agentDir "${root}" ${invalid}`);
|
|
105
72
|
const skill = (name) => ({
|
|
106
|
-
rel: join(
|
|
73
|
+
rel: join(root, "skills", "writing-great-skills", name),
|
|
107
74
|
content: baseTemplate(`skills/writing-great-skills/${name}`),
|
|
108
75
|
});
|
|
109
76
|
const files = [
|
|
110
77
|
// ① identity. AGENTS.md is deliberately NOT scaffolded: a fresh agent has no project context, and
|
|
111
|
-
// an existing repo already owns its AGENTS.md (kept untouched, read as ② context from
|
|
112
|
-
{ rel: join(
|
|
78
|
+
// an existing repo already owns its AGENTS.md (kept untouched, read as ② context from the workspace).
|
|
79
|
+
{ rel: join(root, "persona.md"), content: baseTemplate("persona.md") },
|
|
113
80
|
// The example skill: how to author skills well — the core of self-iteration. Markdown, so it
|
|
114
81
|
// ships in --minimal too. Vendored verbatim from mattpocock/skills (MIT); LICENSE sits beside it.
|
|
115
82
|
skill("SKILL.md"),
|
|
116
83
|
skill("GLOSSARY.md"),
|
|
117
84
|
skill("LICENSE"),
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
85
|
+
{ rel: join(root, "fastagent.config.mjs"), content: baseTemplate("fastagent.config.mjs") },
|
|
86
|
+
// Two ignore files, scaffolded ONCE and owned by the author from then on — no command rewrites,
|
|
87
|
+
// reads or verifies them. The agent's own covers node_modules/machinery/a stray .env; `.secrets/`
|
|
88
|
+
// carries its own because the root file is the one the author has reason to edit, and git's
|
|
89
|
+
// nested-ignore precedence keeps the credentials protected whatever happens up there.
|
|
90
|
+
{ rel: join(root, ".gitignore"), content: baseTemplate("gitignore") },
|
|
91
|
+
{ rel: join(root, SECRETS_DIRNAME, ".gitignore"), content: baseTemplate("secrets.gitignore") },
|
|
92
|
+
{ rel: join(root, SECRETS_DIRNAME, ".env.example"), content: baseTemplate("env.example") },
|
|
122
93
|
];
|
|
123
94
|
if (!minimal) {
|
|
124
|
-
files.push({ rel: join(
|
|
125
|
-
// The
|
|
126
|
-
//
|
|
127
|
-
// which subdirectory it happens to live in.
|
|
95
|
+
files.push({ rel: join(root, "tools", "fetch-url.ts"), content: baseTemplate("tools/fetch-url.ts") },
|
|
96
|
+
// The agent's own manifest, named after the directory it serves (`<dir>-agent`) — except when it
|
|
97
|
+
// IS that directory, where it takes the name straight.
|
|
128
98
|
{
|
|
129
|
-
rel: join(
|
|
130
|
-
content: packageJson(
|
|
131
|
-
}
|
|
99
|
+
rel: join(root, "package.json"),
|
|
100
|
+
content: packageJson(flat ? toPackageName(dir) : `${toPackageName(dir)}-agent`, await fastagentVersion()),
|
|
101
|
+
});
|
|
132
102
|
}
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
throw new Error(`"${dir}" already has ${conflicts.join(", ")} — already a fastagent workspace`);
|
|
103
|
+
// Inside another agent's DEFINITION (its `skills/`, `tools/`, `channels/` or `schedules/`): the outer
|
|
104
|
+
// agent would load the new one as its own content. Note what this deliberately ALLOWS — a package
|
|
105
|
+
// inside an agent's repository, which is the author's tree and not part of what that agent loads.
|
|
106
|
+
const owner = agentDefinitionOwner(dir);
|
|
107
|
+
if (owner) {
|
|
108
|
+
throw new Error(`"${dir}" is inside the definition of the agent at ${owner} — an agent scaffolded here would be ` +
|
|
109
|
+
`part of THAT agent's surface, not one of its own. Init outside it.`);
|
|
141
110
|
}
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
if (options.agentDir) {
|
|
148
|
-
const occupants = (await readdir(join(dir, kit)).catch(() => [])).filter((f) => !f.startsWith("."));
|
|
149
|
-
if (occupants.length > 0) {
|
|
150
|
-
throw new Error(`"${kit}" already exists and is not empty — if it is unrelated to the agent, pick another name ` +
|
|
151
|
-
`(--agent-dir <name>) or go flat (--flat); to adopt it as the kit, empty it first`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
// Was the target non-empty BEFORE we wrote anything? (missing dir = empty).
|
|
155
|
-
const intoNonEmpty = (await readdir(dir).catch(() => [])).length > 0;
|
|
156
|
-
// Preflight scaffold parent dirs: a pre-existing non-directory there would make mkdir fail mid-loop
|
|
157
|
-
// AFTER the first write, leaving a half-scaffold. Detect it before any write (lstat, not stat: a
|
|
158
|
-
// symlinked parent must be rejected, not followed — it would write outside the workspace).
|
|
111
|
+
// Preflight scaffold parent dirs FIRST: a pre-existing non-directory there would make mkdir fail
|
|
112
|
+
// mid-loop AFTER the first write, leaving a half-scaffold — and a file or symlink named `fastagent`
|
|
113
|
+
// must be named as such here, before the occupancy check below tries to read it as a directory
|
|
114
|
+
// (lstat, not stat: a symlinked parent must be rejected, not followed — it would write outside the
|
|
115
|
+
// agent dir).
|
|
159
116
|
const parents = new Set();
|
|
160
117
|
for (const file of files) {
|
|
161
118
|
let p = dirname(file.rel);
|
|
@@ -170,80 +127,99 @@ export async function scaffoldWorkspace(dir, options = {}) {
|
|
|
170
127
|
throw new Error(`cannot scaffold: "${rel}" exists and is not a directory (a regular file or symlink) — remove it, or init elsewhere`);
|
|
171
128
|
}
|
|
172
129
|
}
|
|
130
|
+
// Refuse an occupied agent dir. A config inside means it IS an agent already (name the marker so the
|
|
131
|
+
// message is actionable); any other content means an unfinished agent or something unrelated, and
|
|
132
|
+
// landing persona.md beside it would be a silent mix. Only ENOENT reads as "empty" — any other fault
|
|
133
|
+
// (EACCES…) must surface here rather than as a raw errno mid-write.
|
|
134
|
+
// (AGENTS.md outside is NOT a marker — it is context, adopted untouched.)
|
|
135
|
+
// `.DS_Store`/`.gitkeep`/`.keep` are not evidence of anyone's content: Finder noise, and the standard
|
|
136
|
+
// way to commit an empty directory (someone reserving the name in git ahead of init).
|
|
137
|
+
const occupants = (await readdir(join(dir, root)).catch((e) => {
|
|
138
|
+
if (e.code === "ENOENT")
|
|
139
|
+
return [];
|
|
140
|
+
throw e;
|
|
141
|
+
})).filter((f) => ![".DS_Store", ".gitkeep", ".keep"].includes(f));
|
|
142
|
+
// Could `dir` still SELECT the agent this run creates? SIBLINGS are fine — several agents at one level
|
|
143
|
+
// is a supported shape (different roles driving one repository), picked between by FASTAGENT_AGENT or
|
|
144
|
+
// the default name. A SHADOW is not: a config AT `dir` wins over everything inside it, so scaffolding
|
|
145
|
+
// under one (or scaffolding one over existing children) makes an agent the lookup can never return.
|
|
146
|
+
// The target itself is never in the way — a config already there means "already an agent", which the
|
|
147
|
+
// refusal below says in those words.
|
|
148
|
+
const existing = agentsAt(dir).filter((a) => a !== resolve(dir, root));
|
|
149
|
+
const shadowed = flat
|
|
150
|
+
? existing // the new agent lands AT `dir` and hides everything inside it
|
|
151
|
+
: existing.filter((a) => a === resolve(dir)); // an agent AT `dir` hides the new one inside it
|
|
152
|
+
if (shadowed.length > 0) {
|
|
153
|
+
throw new Error(`"${dir}" already resolves to ${shadowed.map((a) => displayPath(process.cwd(), a) ?? a).join(", ")} — ` +
|
|
154
|
+
`an agent scaffolded ${flat ? "here" : `in ./${root}/`} would be hidden by it and never served ` +
|
|
155
|
+
`from "${dir}" (an agent AT a directory wins over any inside it). Use that agent, move it away, ` +
|
|
156
|
+
`or init in a different directory.`);
|
|
157
|
+
}
|
|
158
|
+
// ONE coordinate system for both refusals — `displayPath` is the shared policy (relative inside the
|
|
159
|
+
// cwd, absolute when it climbs out); the earlier pair mixed an absolute path with a basename-relative
|
|
160
|
+
// one, in adjacent branches of the same command.
|
|
161
|
+
const target = displayPath(process.cwd(), join(dir, root)) ?? join(dir, root);
|
|
162
|
+
const config = occupants.filter((f) => AGENT_CONFIG_NAMES.includes(f));
|
|
163
|
+
if (config.length > 0) {
|
|
164
|
+
throw new Error(`"${target}" already has ${config.join(", ")} — already a fastagent agent`);
|
|
165
|
+
}
|
|
166
|
+
// Only a SUBDIRECTORY target must be empty. `.` is a directory being adopted — content is expected,
|
|
167
|
+
// and every existing file is kept below.
|
|
168
|
+
if (!flat && occupants.length > 0) {
|
|
169
|
+
throw new Error(`"${target}" already holds ${occupants.join(", ")} — move it away first, or run ` +
|
|
170
|
+
`\`fastagent init\` in a different directory`);
|
|
171
|
+
}
|
|
172
|
+
// Which directories were OURS to create? "Empty" is not ownership: a user may have pre-created any of
|
|
173
|
+
// them (`--agent-dir .` adopts a directory that can already carry an empty `skills/` or `.secrets/`),
|
|
174
|
+
// and the rollback below must delete only what THIS run made. Recorded before the first write, since
|
|
175
|
+
// afterwards the two are indistinguishable.
|
|
176
|
+
const preexisting = new Set();
|
|
177
|
+
for (const rel of parents)
|
|
178
|
+
if (await exists(join(dir, rel)))
|
|
179
|
+
preexisting.add(rel);
|
|
180
|
+
const agentDirExisted = await exists(join(dir, root));
|
|
173
181
|
await mkdir(dir, { recursive: true });
|
|
174
182
|
const created = [];
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
183
|
+
const kept = [];
|
|
184
|
+
// ONE rollback scope: any failure removes what THIS run created — files AND the directories it made
|
|
185
|
+
// for them. Leaving the empty dirs behind would be worse than untidy: the occupancy refusal above
|
|
186
|
+
// would then report the next `init` as occupied, blaming the user for our own debris.
|
|
187
|
+
//
|
|
188
|
+
// `wx` never clobbers. A subdirectory target was proven empty, so EEXIST means a concurrent writer —
|
|
189
|
+
// an error. `.` is a directory being adopted, so an existing `.gitignore`/`package.json` is the
|
|
190
|
+
// author's — keep it and report it.
|
|
181
191
|
try {
|
|
182
192
|
for (const file of files) {
|
|
183
193
|
const abs = join(dir, file.rel);
|
|
184
194
|
await mkdir(dirname(abs), { recursive: true });
|
|
185
195
|
try {
|
|
186
|
-
await writeFile(abs, file.content, { flag: "wx" });
|
|
196
|
+
await writeFile(abs, file.content, { flag: "wx" });
|
|
187
197
|
created.push(file.rel);
|
|
188
198
|
}
|
|
189
|
-
catch (
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
throw error;
|
|
199
|
+
catch (e) {
|
|
200
|
+
if (!flat || e.code !== "EEXIST")
|
|
201
|
+
throw e;
|
|
202
|
+
kept.push(file.rel);
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
|
-
// Ignore ownership follows layout jurisdiction. Run-root state/secrets live at `dir`, so the ROOT
|
|
197
|
-
// .gitignore owns `.env` + `.fastagent`. The kit's npm deps live where the kit lives: flat → root
|
|
198
|
-
// `node_modules/`; agentDir → `<agentDir>/.gitignore` owns its own `node_modules/`. This avoids
|
|
199
|
-
// patching a host repo's root ignore with agent-internal dependency paths while still preventing the
|
|
200
|
-
// post-init 25k-file untracked flood.
|
|
201
|
-
const coveredBy = (ig, p) => (ig?.ignores(p) ?? false) || (ig?.ignores(`${p}/`) ?? false);
|
|
202
|
-
const rootIgnore = await loadRootIgnore(dir);
|
|
203
|
-
const rootRequired = [
|
|
204
|
-
{ path: ".env", pattern: ".env" },
|
|
205
|
-
{ path: ".fastagent", pattern: ".fastagent" },
|
|
206
|
-
...(!minimal && !options.agentDir ? [{ path: "node_modules", pattern: "node_modules/" }] : []),
|
|
207
|
-
];
|
|
208
|
-
const rootNeed = rootRequired.filter((e) => !coveredBy(rootIgnore, e.path));
|
|
209
|
-
if (rootNeed.length > 0) {
|
|
210
|
-
await appendFile(join(dir, ".gitignore"), `\n# fastagent\n${rootNeed.map((e) => e.pattern).join("\n")}\n`);
|
|
211
|
-
patched.push(".gitignore");
|
|
212
|
-
// A .fastagentignore is applied LAST and can re-include what .gitignore excludes — the append
|
|
213
|
-
// cannot fix that, so re-check and keep it a visible warning instead of false assurance.
|
|
214
|
-
const after = await loadRootIgnore(dir);
|
|
215
|
-
const still = rootNeed.filter((e) => !coveredBy(after, e.path)).map((e) => e.path);
|
|
216
|
-
if (still.length > 0) {
|
|
217
|
-
warnings.push(`your .gitignore/.fastagentignore does not exclude ${still.map((s) => `"${s}"`).join(", ")} — a deploy that copies the directory may ship secrets/state`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (!minimal && options.agentDir) {
|
|
221
|
-
// Deliberately checks ONLY the kit's own ignore, not the root: the kit is the portable agent
|
|
222
|
-
// directory, and its dependency ignore must travel WITH it (a fresh kit .gitignore carries
|
|
223
|
-
// node_modules/ unconditionally for the same reason) — a root-level `node_modules/` that happens
|
|
224
|
-
// to cover it today doesn't survive the kit being copied out of the host repo.
|
|
225
|
-
const kitIgnore = await loadRootIgnore(join(dir, kit));
|
|
226
|
-
if (!coveredBy(kitIgnore, "node_modules")) {
|
|
227
|
-
const rel = join(kit, ".gitignore");
|
|
228
|
-
await appendFile(join(dir, rel), `\n# fastagent\nnode_modules/\n`);
|
|
229
|
-
patched.push(rel);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// A kept package.json won't carry the tool's deps — the example tool would not resolve. The install
|
|
233
|
-
// command matches the workspace's runtime (bun.lock → bun add).
|
|
234
|
-
const keptPkg = join(kit, "package.json");
|
|
235
|
-
if (!minimal && skipped.includes(keptPkg)) {
|
|
236
|
-
const kitAbs = join(dir, kit);
|
|
237
|
-
const add = detectRuntime(kitAbs, await readPackageJson(kitAbs)).runtime === "bun" ? "bun add" : "npm install";
|
|
238
|
-
warnings.push(`kept the existing ${keptPkg} — run \`${add} @fastagent-sh/fastagent\` there so the example tool resolves`);
|
|
239
|
-
}
|
|
240
205
|
}
|
|
241
206
|
catch (error) {
|
|
242
|
-
// Best-effort rollback of a partial scaffold:
|
|
243
|
-
// error below is the one worth surfacing — a cleanup failure must not mask it).
|
|
207
|
+
// Best-effort rollback of a partial scaffold: anything that won't delete is left behind (the
|
|
208
|
+
// original error below is the one worth surfacing — a cleanup failure must not mask it). Files
|
|
209
|
+
// first, then the directories THIS RUN created, deepest first; `rmdir` additionally removes only
|
|
210
|
+
// what is empty, so a pre-existing sibling inside one of ours is never touched. The agent root goes
|
|
211
|
+
// last, and only when this run created it — "empty" was never proof of ownership. (Reaching here at all takes a
|
|
212
|
+
// real fs fault: the occupancy refusal above proved the target empty, so nothing else can fail
|
|
213
|
+
// mid-loop. The covered case is a permission fault before the first write.)
|
|
244
214
|
for (const rel of created.reverse())
|
|
245
215
|
await rm(join(dir, rel), { force: true }).catch(() => { });
|
|
216
|
+
for (const rel of [...parents].sort((a, b) => b.split(sep).length - a.split(sep).length)) {
|
|
217
|
+
if (rel !== root && !preexisting.has(rel))
|
|
218
|
+
await rmdir(join(dir, rel)).catch(() => { });
|
|
219
|
+
}
|
|
220
|
+
if (!agentDirExisted)
|
|
221
|
+
await rmdir(join(dir, root)).catch(() => { });
|
|
246
222
|
throw error;
|
|
247
223
|
}
|
|
248
|
-
return { dir, complete: !minimal, agentDir:
|
|
224
|
+
return { dir, complete: !minimal, agentDir: root, created, kept };
|
|
249
225
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Environment for this agent. Copy to .env (
|
|
2
|
-
# Everything here is OPTIONAL — the defaults work without a .env.
|
|
1
|
+
# Environment for this agent. Copy to .env IN THIS DIRECTORY (.secrets/.env — gitignored, managed by
|
|
2
|
+
# fastagent) and uncomment what you need. Everything here is OPTIONAL — the defaults work without a .env.
|
|
3
3
|
|
|
4
4
|
# --- Model auth ---
|
|
5
5
|
# Pick auth once: `fastagent login` (a subscription/OAuth provider like openai-codex, or an API key),
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
# OPENAI_API_KEY=
|
|
9
9
|
# ANTHROPIC_API_KEY=
|
|
10
10
|
|
|
11
|
+
# --- Which agent (only when a workspace holds several) ---
|
|
12
|
+
# FASTAGENT_AGENT names the agent directory to serve. It CANNOT be set here: placement resolves before
|
|
13
|
+
# this file is read (it is what decides whose .env this is). Put it in your shell, an .envrc, or the
|
|
14
|
+
# command: `FASTAGENT_AGENT=pm fastagent dev`.
|
|
15
|
+
|
|
11
16
|
# --- Model selection (overrides fastagent.config) ---
|
|
12
17
|
# Precedence: --model flag > FASTAGENT_MODEL > config. `fastagent dev` writes your first-run pick to
|
|
13
18
|
# fastagent.config; set it here to override per-environment.
|
|
@@ -16,9 +21,13 @@
|
|
|
16
21
|
# --- Serving (fastagent start) ---
|
|
17
22
|
# Port precedence: --port > PORT > config.http.port > 8787
|
|
18
23
|
# PORT=8787
|
|
19
|
-
# Machine-state root: sessions,
|
|
20
|
-
# Default: <dir>/.
|
|
21
|
-
# directory keeps conversations
|
|
22
|
-
# FASTAGENT_STATE_DIR=/data
|
|
24
|
+
# Machine-state root: sessions, schedule state, and channel state derive from it.
|
|
25
|
+
# Default: <agent dir>/.state. Point it at a mounted volume so a redeploy that replaces the
|
|
26
|
+
# directory keeps conversations.
|
|
27
|
+
# FASTAGENT_STATE_DIR=/data/.state
|
|
28
|
+
# Secrets home (this .env + auth.json). Default: <agent dir>/.secrets. Deployments point it at
|
|
29
|
+
# the volume so a rotated OAuth credential persists across restarts. Set HERE it moves only
|
|
30
|
+
# auth.json — a value inside .env cannot relocate the .env being read.
|
|
31
|
+
# FASTAGENT_SECRETS_DIR=/data/.secrets
|
|
23
32
|
# Override just the sessions path (default: <state root>/sessions):
|
|
24
33
|
# FASTAGENT_SESSIONS_DIR=/data/sessions
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// fastagent.config.mjs — deployment choices only (model / http; code tools auto-discover from tools/).
|
|
2
2
|
// Your agent's identity lives in persona.md; its capabilities in skills/ + tools/ — never here.
|
|
3
|
-
// An AGENTS.md
|
|
3
|
+
// An AGENTS.md in the WORKSPACE (the directory the agent is started in) is read as project context.
|
|
4
4
|
// Model precedence: `--model` flag > FASTAGENT_MODEL env > this default.
|
|
5
|
-
// No model is preset: `fastagent dev`
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// No model is preset: `fastagent dev` shows the full model catalog (models you already have
|
|
6
|
+
// credentials for come first; picking one that needs auth logs you in inline) and writes your choice
|
|
7
|
+
// below. Or set it by hand to a "provider/modelId" (`fastagent models` lists them).
|
|
8
8
|
export default {
|
|
9
9
|
// model: "openai-codex/gpt-5.5",
|
|
10
10
|
// thinkingLevel: "high", // reasoning effort (off|minimal|low|medium|high|xhigh|max); default "medium" (pi TUI parity)
|
|
11
11
|
http: { port: 8787 },
|
|
12
12
|
// selfSchedule: true, // mount the built-in `wake` tool: the agent schedules its own follow-up turns
|
|
13
13
|
// // ("check the deploy in 10 min"). Cron jobs need no opt-in — drop a schedules/<name>.ts.
|
|
14
|
+
// sessionControl: true, // serve /control/* for remote observation + steering (fastagent attach / Web panel)
|
|
14
15
|
// deploy: what the agent needs on the box (so `fastagent deploy` doesn't need a hand-written Dockerfile
|
|
15
16
|
// or hand-set host variables). Uncomment as needed:
|
|
16
17
|
// deploy: {
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
#
|
|
2
|
-
.
|
|
1
|
+
# This file is yours — fastagent writes it once, at `init`, and never touches git again.
|
|
2
|
+
# (`.secrets/` carries its own, so the credentials stay protected however you edit this one.)
|
|
3
|
+
|
|
4
|
+
# dependencies (reinstalled at deploy). No trailing slash on purpose: `node_modules/` matches only a
|
|
5
|
+
# DIRECTORY, so a symlinked one (pnpm, a shared store) would show up as untracked.
|
|
6
|
+
node_modules
|
|
3
7
|
|
|
4
|
-
#
|
|
5
|
-
|
|
8
|
+
# machine state: sessions, channel state, schedule state
|
|
9
|
+
.state
|
|
10
|
+
.cache
|
|
6
11
|
|
|
7
|
-
# fastagent
|
|
8
|
-
.
|
|
12
|
+
# fastagent reads the agent's env from .secrets/.env (which un-ignores its own .env.example). A bare
|
|
13
|
+
# .env at THIS level is the file habit puts here — `dev` says so when it finds one; this keeps it
|
|
14
|
+
# uncommittable meanwhile.
|
|
15
|
+
.env
|
|
16
|
+
.env.*
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Persona
|
|
2
2
|
|
|
3
|
-
You are this workspace's agent. This file is your identity — it overrides the engine's default identity line, and it is re-read every turn along with the rest of your definition (`skills/` — capabilities you load when a task calls for them; `tools/` — code tools your author added, in the same directory as this file). An edit to any of them takes effect on your next message, no restart.
|
|
3
|
+
You are this workspace's agent. This file is your identity — it overrides the engine's default identity line, and it is re-read every turn along with the rest of your definition (`skills/` — capabilities you load when a task calls for them; `tools/` — code tools your author added, in the same directory as this file). An edit to any of them takes effect on your next message, no restart.
|
|
4
|
+
|
|
5
|
+
Your definition is this directory: `persona.md`, `skills/`, `tools/`, and the config beside them. Your WORKSPACE is the directory you were started in — the project you work on, and where your `read` / `write` / `edit` / `bash` tools operate. It may be this same directory, or the one containing it; `fastagent info` prints both. If the workspace has an `AGENTS.md`, it is project context — read it to learn the project's conventions.
|
|
4
6
|
|
|
5
7
|
You can improve yourself. When a task reveals something durable — a repeatable process, a standing preference, a hard-won fact — write it into your definition instead of losing it:
|
|
6
8
|
|
|
7
|
-
- A repeatable process or capability → a new skill beside this file: `skills/<name>/SKILL.md`. Read `skills/writing-great-skills/SKILL.md` first; it is the guide to authoring skills well.
|
|
9
|
+
- A repeatable process or capability → a new skill beside this file: `skills/<name>/SKILL.md`. Only the `skills/` next to this file is scanned. Read `skills/writing-great-skills/SKILL.md` first; it is the guide to authoring skills well.
|
|
8
10
|
- A standing instruction or fact → edit this file.
|
|
9
11
|
|
|
10
12
|
Keep both lean: include only what changes your behavior, and delete what no longer earns its place.
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
/** Read a base
|
|
1
|
+
/** Read a base agent template (src/scaffold/templates/<name>). */
|
|
2
2
|
export declare const baseTemplate: (name: string) => string;
|
|
3
|
-
/** The persona template; in the agentDir layout a locator note is appended (anchor-free — no silent-miss
|
|
4
|
-
* risk) so the self-iteration guidance ("write skills beside this file") points into the kit, not the
|
|
5
|
-
* run root — a skill written to the root `skills/` would never be scanned. */
|
|
6
|
-
export declare function personaTemplate(agentDir?: string): string;
|
|
7
|
-
/** The config template; when the kit is placed in a subdirectory, `agentDir` is injected as the first key. */
|
|
8
|
-
export declare function configTemplate(agentDir?: string): string;
|
|
9
3
|
/** Read one file from a channel's scaffold bundle (src/channels/<kind>/scaffold/<name>). */
|
|
10
4
|
export declare const channelTemplate: (kind: string, name: string) => string;
|
|
11
5
|
/** The .ts files in a channel's bundle: `channel.ts` is the channel adapter; the rest are companion tools. */
|
|
@@ -1,36 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scaffold template ACCESS (data, not logic): readers for the real files `init`/`add` write into
|
|
3
|
-
*
|
|
2
|
+
* Scaffold template ACCESS (data, not logic): readers for the real files `init`/`add` write into an
|
|
3
|
+
* agent dir, plus the parametric pieces. Base agent templates live under ./templates/; each
|
|
4
4
|
* channel's bundle lives WITH the channel at ../channels/<kind>/scaffold/ (so a channel owns its
|
|
5
5
|
* starter kit and could ship as its own package). Both trees are excluded from this package's tsc +
|
|
6
6
|
* biome (they import the published @fastagent-sh/fastagent, not this source) and copied into dist/ by the build.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, readdirSync } from "node:fs";
|
|
9
9
|
import { basename, resolve } from "node:path";
|
|
10
|
-
/** Read a base
|
|
10
|
+
/** Read a base agent template (src/scaffold/templates/<name>). */
|
|
11
11
|
export const baseTemplate = (name) => readFileSync(new URL(`./templates/${name}`, import.meta.url), "utf8");
|
|
12
|
-
/** The persona template; in the agentDir layout a locator note is appended (anchor-free — no silent-miss
|
|
13
|
-
* risk) so the self-iteration guidance ("write skills beside this file") points into the kit, not the
|
|
14
|
-
* run root — a skill written to the root `skills/` would never be scanned. */
|
|
15
|
-
export function personaTemplate(agentDir) {
|
|
16
|
-
const base = baseTemplate("persona.md");
|
|
17
|
-
if (!agentDir)
|
|
18
|
-
return base;
|
|
19
|
-
return `${base}\nNote: your definition lives under \`${agentDir}/\` relative to the workspace root — this file is \`${agentDir}/persona.md\`, and a new skill goes to \`${agentDir}/skills/<name>/SKILL.md\` (a \`skills/\` at the root is not scanned).\n`;
|
|
20
|
-
}
|
|
21
|
-
/** The config template; when the kit is placed in a subdirectory, `agentDir` is injected as the first key. */
|
|
22
|
-
export function configTemplate(agentDir) {
|
|
23
|
-
const base = baseTemplate("fastagent.config.mjs");
|
|
24
|
-
if (!agentDir)
|
|
25
|
-
return base;
|
|
26
|
-
const out = base.replace("export default {\n", `export default {\n agentDir: ${JSON.stringify(agentDir)}, // the agent's own surface (persona.md / skills / tools / channels) lives there\n`);
|
|
27
|
-
// Fail visibly if the template drifted and the anchor no longer matches — a config that silently
|
|
28
|
-
// doesn't point at the kit would assemble an EMPTY agent with no error. Check the OPERATION happened
|
|
29
|
-
// (out !== base), not a substring — a commented `agentDir:` example in the template would fool that.
|
|
30
|
-
if (out === base)
|
|
31
|
-
throw new Error("configTemplate: anchor not found in fastagent.config.mjs template");
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
12
|
const channelScaffoldDir = (kind) => new URL(`../channels/${kind}/scaffold/`, import.meta.url);
|
|
35
13
|
/** Read one file from a channel's scaffold bundle (src/channels/<kind>/scaffold/<name>). */
|
|
36
14
|
export const channelTemplate = (kind, name) => readFileSync(new URL(name, channelScaffoldDir(kind)), "utf8");
|
|
@@ -13,12 +13,12 @@ export interface VendoredSkill {
|
|
|
13
13
|
overwritten: boolean;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Vendor an Agent Skills skill into `<
|
|
16
|
+
* Vendor an Agent Skills skill into `<agent dir>/skills/<name>/` from a giget ref (github default), a
|
|
17
17
|
* local path, or a bare name (resolved against the local global skill dirs). Copy-in, git-tracked.
|
|
18
18
|
* Refuses to overwrite unless `options.update` (then a plain git-tracked overwrite, never a merge).
|
|
19
19
|
* Validates a staging copy with the runtime loader BEFORE replacing, so a bad fetch never destroys an
|
|
20
20
|
* existing skill.
|
|
21
21
|
*/
|
|
22
|
-
export declare function vendorSkill(
|
|
22
|
+
export declare function vendorSkill(agentDir: string, source: string, options?: {
|
|
23
23
|
update?: boolean;
|
|
24
24
|
}): Promise<VendoredSkill>;
|