@fastagent-sh/fastagent 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +5 -5
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/context-buffer.d.ts +30 -0
  5. package/dist/channels/context-buffer.js +98 -0
  6. package/dist/channels/control.d.ts +28 -0
  7. package/dist/channels/control.js +214 -0
  8. package/dist/channels/feishu/card.js +1 -1
  9. package/dist/channels/feishu/context-buffer.d.ts +43 -0
  10. package/dist/channels/feishu/context-buffer.js +72 -0
  11. package/dist/channels/feishu/crypto.d.ts +4 -2
  12. package/dist/channels/feishu/crypto.js +4 -2
  13. package/dist/channels/feishu/feishu-api.d.ts +15 -7
  14. package/dist/channels/feishu/feishu-api.js +22 -4
  15. package/dist/channels/feishu/feishu.d.ts +38 -16
  16. package/dist/channels/feishu/feishu.js +286 -151
  17. package/dist/channels/feishu/invoke-turn.d.ts +24 -31
  18. package/dist/channels/feishu/invoke-turn.js +61 -62
  19. package/dist/channels/feishu/model.d.ts +98 -0
  20. package/dist/channels/feishu/model.js +9 -0
  21. package/dist/channels/feishu/normalize.d.ts +23 -0
  22. package/dist/channels/feishu/normalize.js +132 -0
  23. package/dist/channels/feishu/owned-threads.d.ts +7 -0
  24. package/dist/channels/feishu/owned-threads.js +47 -0
  25. package/dist/channels/feishu/parse.d.ts +21 -103
  26. package/dist/channels/feishu/parse.js +35 -145
  27. package/dist/channels/feishu/preview.d.ts +4 -7
  28. package/dist/channels/feishu/preview.js +26 -142
  29. package/dist/channels/feishu/register-app.d.ts +2 -1
  30. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  31. package/dist/channels/feishu/scaffold/channel.ts +10 -3
  32. package/dist/channels/feishu/setup-mode.d.ts +8 -0
  33. package/dist/channels/feishu/setup-mode.js +2 -0
  34. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  35. package/dist/channels/feishu/ws-ingress.js +136 -0
  36. package/dist/channels/github/github.js +8 -6
  37. package/dist/channels/http.d.ts +14 -0
  38. package/dist/channels/http.js +35 -2
  39. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  40. package/dist/channels/invoke-turn-kit.js +87 -0
  41. package/dist/channels/lark/lark.d.ts +4 -2
  42. package/dist/channels/lark/lark.js +4 -1
  43. package/dist/channels/lark/onboard.d.ts +8 -4
  44. package/dist/channels/lark/onboard.js +8 -0
  45. package/dist/channels/lark/scaffold/channel.ts +10 -3
  46. package/dist/channels/preview-kit.d.ts +109 -0
  47. package/dist/channels/preview-kit.js +183 -0
  48. package/dist/channels/seen.d.ts +5 -0
  49. package/dist/channels/seen.js +35 -0
  50. package/dist/channels/slack/bot-auth.d.ts +15 -0
  51. package/dist/channels/slack/bot-auth.js +146 -0
  52. package/dist/channels/slack/config-api.d.ts +60 -0
  53. package/dist/channels/slack/config-api.js +149 -0
  54. package/dist/channels/slack/context-buffer.d.ts +24 -0
  55. package/dist/channels/slack/context-buffer.js +37 -0
  56. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  57. package/dist/channels/slack/invoke-turn.js +63 -0
  58. package/dist/channels/slack/manifest.d.ts +49 -0
  59. package/dist/channels/slack/manifest.js +69 -0
  60. package/dist/channels/slack/model.d.ts +67 -0
  61. package/dist/channels/slack/model.js +2 -0
  62. package/dist/channels/slack/onboard.d.ts +41 -0
  63. package/dist/channels/slack/onboard.js +120 -0
  64. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  65. package/dist/channels/slack/onboarding-state.js +69 -0
  66. package/dist/channels/slack/owned-threads.d.ts +6 -0
  67. package/dist/channels/slack/owned-threads.js +43 -0
  68. package/dist/channels/slack/parse.d.ts +23 -0
  69. package/dist/channels/slack/parse.js +81 -0
  70. package/dist/channels/slack/preview.d.ts +24 -0
  71. package/dist/channels/slack/preview.js +359 -0
  72. package/dist/channels/slack/reaction.d.ts +24 -0
  73. package/dist/channels/slack/reaction.js +62 -0
  74. package/dist/channels/slack/register-webhook.d.ts +10 -0
  75. package/dist/channels/slack/register-webhook.js +49 -0
  76. package/dist/channels/slack/scaffold/channel.ts +33 -0
  77. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  78. package/dist/channels/slack/setup-server.d.ts +17 -0
  79. package/dist/channels/slack/setup-server.js +103 -0
  80. package/dist/channels/slack/slack-api.d.ts +77 -0
  81. package/dist/channels/slack/slack-api.js +415 -0
  82. package/dist/channels/slack/slack.d.ts +58 -0
  83. package/dist/channels/slack/slack.js +451 -0
  84. package/dist/channels/slack/welcomed.d.ts +5 -0
  85. package/dist/channels/slack/welcomed.js +32 -0
  86. package/dist/channels/state.js +3 -3
  87. package/dist/channels/stop-command.d.ts +6 -0
  88. package/dist/channels/stop-command.js +36 -0
  89. package/dist/channels/tasks.d.ts +13 -0
  90. package/dist/channels/tasks.js +10 -0
  91. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  92. package/dist/channels/telegram/context-buffer.js +6 -85
  93. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  94. package/dist/channels/telegram/invoke-turn.js +11 -58
  95. package/dist/channels/telegram/preview.d.ts +4 -7
  96. package/dist/channels/telegram/preview.js +24 -142
  97. package/dist/channels/telegram/telegram.js +23 -9
  98. package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
  99. package/dist/channels/{feishu/text.js → text.js} +1 -1
  100. package/dist/channels/turn-queue.js +1 -1
  101. package/dist/channels/turn-store.d.ts +1 -1
  102. package/dist/channels/turn-store.js +2 -3
  103. package/dist/cli/add-feishu.d.ts +27 -0
  104. package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
  105. package/dist/cli/add-slack.d.ts +10 -0
  106. package/dist/cli/add-slack.js +204 -0
  107. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  108. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  109. package/dist/cli/commands/add.d.ts +13 -0
  110. package/dist/cli/commands/add.js +274 -0
  111. package/dist/cli/commands/attach.d.ts +82 -0
  112. package/dist/cli/commands/attach.js +559 -0
  113. package/dist/cli/commands/chat.d.ts +4 -0
  114. package/dist/cli/commands/chat.js +21 -0
  115. package/dist/cli/commands/deploy.d.ts +15 -0
  116. package/dist/cli/commands/deploy.js +394 -0
  117. package/dist/cli/commands/dev.d.ts +11 -0
  118. package/dist/cli/commands/dev.js +82 -0
  119. package/dist/cli/commands/fire.d.ts +7 -0
  120. package/dist/cli/commands/fire.js +45 -0
  121. package/dist/cli/commands/info.d.ts +7 -0
  122. package/dist/cli/commands/info.js +108 -0
  123. package/dist/cli/commands/init.d.ts +8 -0
  124. package/dist/cli/commands/init.js +81 -0
  125. package/dist/cli/commands/invoke.d.ts +7 -0
  126. package/dist/cli/commands/invoke.js +28 -0
  127. package/dist/cli/commands/login.d.ts +6 -0
  128. package/dist/cli/commands/login.js +52 -0
  129. package/dist/cli/commands/models.d.ts +1 -0
  130. package/dist/cli/commands/models.js +15 -0
  131. package/dist/cli/commands/schedule.d.ts +12 -0
  132. package/dist/cli/commands/schedule.js +89 -0
  133. package/dist/cli/commands/start.d.ts +10 -0
  134. package/dist/cli/commands/start.js +97 -0
  135. package/dist/cli/commands/tool.d.ts +1 -0
  136. package/dist/cli/commands/tool.js +38 -0
  137. package/dist/cli/fail.d.ts +17 -0
  138. package/dist/cli/fail.js +32 -0
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +90 -0
  142. package/dist/cli/kernel.js +190 -0
  143. package/dist/cli/models-view.d.ts +21 -0
  144. package/dist/cli/models-view.js +66 -0
  145. package/dist/cli/program.d.ts +11 -0
  146. package/dist/cli/program.js +479 -0
  147. package/dist/cli/serve.d.ts +48 -0
  148. package/dist/cli/serve.js +248 -0
  149. package/dist/cli/shared.d.ts +44 -0
  150. package/dist/cli/shared.js +246 -0
  151. package/dist/cli.js +8 -1329
  152. package/dist/collect.d.ts +14 -3
  153. package/dist/collect.js +24 -0
  154. package/dist/core.d.ts +3 -1
  155. package/dist/core.js +2 -0
  156. package/dist/deploy/docker/plan.d.ts +45 -0
  157. package/dist/deploy/docker/plan.js +141 -0
  158. package/dist/deploy/docker/run.d.ts +40 -0
  159. package/dist/deploy/docker/run.js +126 -0
  160. package/dist/deploy/fly/plan.d.ts +3 -1
  161. package/dist/deploy/fly/plan.js +12 -7
  162. package/dist/deploy/fly/run.d.ts +5 -4
  163. package/dist/deploy/fly/run.js +16 -5
  164. package/dist/deploy/preflight.d.ts +10 -4
  165. package/dist/deploy/preflight.js +57 -12
  166. package/dist/deploy/railway/plan.d.ts +3 -1
  167. package/dist/deploy/railway/plan.js +12 -4
  168. package/dist/deploy/railway/run.d.ts +5 -4
  169. package/dist/deploy/railway/run.js +15 -5
  170. package/dist/deploy/runner.d.ts +6 -2
  171. package/dist/deploy/runner.js +1 -0
  172. package/dist/deploy/secrets.d.ts +2 -1
  173. package/dist/deploy/secrets.js +23 -3
  174. package/dist/dev-supervisor.d.ts +0 -2
  175. package/dist/dev-supervisor.js +7 -3
  176. package/dist/engines/pi/auth.js +160 -46
  177. package/dist/engines/pi/channel.d.ts +22 -16
  178. package/dist/engines/pi/channel.js +90 -60
  179. package/dist/engines/pi/chat.d.ts +4 -16
  180. package/dist/engines/pi/chat.js +8 -188
  181. package/dist/engines/pi/config.d.ts +23 -9
  182. package/dist/engines/pi/config.js +35 -5
  183. package/dist/engines/pi/create.d.ts +36 -7
  184. package/dist/engines/pi/create.js +63 -22
  185. package/dist/engines/pi/harness.d.ts +65 -1
  186. package/dist/engines/pi/harness.js +166 -2
  187. package/dist/engines/pi/invoke.d.ts +56 -3
  188. package/dist/engines/pi/invoke.js +340 -20
  189. package/dist/engines/pi/login.d.ts +11 -0
  190. package/dist/engines/pi/login.js +17 -5
  191. package/dist/engines/pi/models.d.ts +56 -10
  192. package/dist/engines/pi/models.js +61 -23
  193. package/dist/engines/pi/search-tools.d.ts +10 -0
  194. package/dist/engines/pi/search-tools.js +138 -0
  195. package/dist/engines/pi/session-builder.d.ts +16 -0
  196. package/dist/engines/pi/session-builder.js +308 -0
  197. package/dist/engines/pi/session-control.d.ts +50 -0
  198. package/dist/engines/pi/session-control.js +604 -0
  199. package/dist/engines/pi/sessions.d.ts +17 -2
  200. package/dist/engines/pi/sessions.js +9 -0
  201. package/dist/engines/pi/tool-context.d.ts +46 -11
  202. package/dist/engines/pi/tool-context.js +11 -9
  203. package/dist/engines/pi/tool.d.ts +35 -6
  204. package/dist/engines/pi/tool.js +47 -1
  205. package/dist/engines/pi/wake-tool.d.ts +0 -3
  206. package/dist/engines/pi/wake-tool.js +9 -7
  207. package/dist/engines/pi/workspace.d.ts +56 -1
  208. package/dist/engines/pi/workspace.js +75 -16
  209. package/dist/feishu.d.ts +1 -1
  210. package/dist/feishu.js +1 -1
  211. package/dist/host/node.d.ts +23 -6
  212. package/dist/host/node.js +5 -4
  213. package/dist/index.d.ts +1 -0
  214. package/dist/index.js +1 -0
  215. package/dist/lark.d.ts +1 -1
  216. package/dist/lark.js +1 -1
  217. package/dist/observe.js +3 -0
  218. package/dist/pi.d.ts +6 -4
  219. package/dist/pi.js +2 -1
  220. package/dist/scaffold/add-channel.d.ts +19 -6
  221. package/dist/scaffold/add-channel.js +127 -16
  222. package/dist/scaffold/templates/fastagent.config.mjs +5 -3
  223. package/dist/schedule/wakeups.d.ts +0 -3
  224. package/dist/schedule/wakeups.js +1 -1
  225. package/dist/session-remote.d.ts +53 -0
  226. package/dist/session-remote.js +336 -0
  227. package/dist/session.d.ts +265 -0
  228. package/dist/session.js +37 -0
  229. package/dist/slack.d.ts +2 -0
  230. package/dist/slack.js +2 -0
  231. package/dist/tunnel.d.ts +7 -4
  232. package/dist/tunnel.js +21 -10
  233. package/package.json +22 -7
  234. package/dist/channels/feishu/seen.d.ts +0 -5
  235. package/dist/channels/feishu/seen.js +0 -47
  236. package/dist/cli-add-feishu.d.ts +0 -8
  237. package/dist/cli-models.d.ts +0 -11
  238. package/dist/cli-models.js +0 -20
@@ -0,0 +1,108 @@
1
+ /** `fastagent info [dir] [--json]`: print what the directory ASSEMBLES into, WITHOUT booting a server. Read-only. */
2
+ import { resolve } from "node:path";
3
+ import { loadDotEnv } from "../../env.js";
4
+ import { discoverChannelFiles } from "../../engines/pi/channel.js";
5
+ import { defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPath, resolveModelSpec, resolveSessionsDirOverride, resolveStateRoot, } from "../../engines/pi/config.js";
6
+ import { resolveWorkspaceTools } from "../../engines/pi/create.js";
7
+ import { loadAgentDefinition } from "../../engines/pi/definition.js";
8
+ import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
9
+ import { log } from "../../log.js";
10
+ import { nextRun } from "../../schedule/cron.js";
11
+ import { loadSchedules } from "../../schedule/discover.js";
12
+ import { failStartup } from "../fail.js";
13
+ export async function runInfo(dirArg, opts) {
14
+ const dir = resolve(dirArg);
15
+ loadDotEnv(dir); // skills/tools may read env at load time
16
+ const { config, path: configPath } = await loadConfig(dir).catch(failStartup);
17
+ const modelSpec = resolveModelSpec(opts.model, config);
18
+ // dir = the run root (cwd, whose AGENTS.md is ② context); the agent's own surface lives in agentDir.
19
+ const agentDir = resolveAgentDir(dir, config);
20
+ const definition = await loadAgentDefinition(agentDir, { cwd: dir }).catch(failStartup);
21
+ // A tool that fails to load, for any reason (a missing dep, a top-level throw, or just not being a
22
+ // tool), is isolated the same way everywhere (G2): info, dev, AND start report it and keep going with
23
+ // the tools that loaded. The `error`/`.catch` below only fires for a whole-load fault (an unreadable
24
+ // tools/ dir), not a single bad file.
25
+ const tools = await resolveWorkspaceTools(config, agentDir, dir)
26
+ .then((r) => ({
27
+ names: r.toolNames,
28
+ deferred: r.deferredToolNames,
29
+ collisions: r.toolCollisions,
30
+ failures: r.toolFailures,
31
+ error: undefined,
32
+ }))
33
+ .catch((e) => ({
34
+ names: [],
35
+ deferred: [],
36
+ collisions: [],
37
+ failures: [],
38
+ error: e.message,
39
+ }));
40
+ const channels = await discoverChannelFiles(agentDir).catch(failStartup);
41
+ // Loaded (imported + validated), not just discovered: info's job is "fix only what it reports", so a
42
+ // broken schedule file (bad cron/tz, failed import) must show up HERE, not first at dev/start — and
43
+ // loading is what makes the next fire instant printable. Consistent with tools (info imports those too).
44
+ const sched = await loadSchedules(agentDir).catch(failStartup);
45
+ const schedules = sched.schedules.map((s) => ({
46
+ name: s.name,
47
+ cron: s.cron,
48
+ tz: s.tz ?? null,
49
+ next: nextRun(s.cron, s.tz, new Date())?.toISOString() ?? null,
50
+ }));
51
+ // The default sessions/auth paths WITHOUT creating anything (info is read-only; dev/start mkdir/login
52
+ // create them, info must not).
53
+ const stateRoot = resolveStateRoot(dir);
54
+ const sessionsDir = resolveSessionsDirOverride(opts.sessionsDir) ?? defaultSessionsDir(stateRoot);
55
+ const authPath = resolveAuthPath(dir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
56
+ if (opts.json) {
57
+ console.log(JSON.stringify({
58
+ dir,
59
+ agentDir,
60
+ configPath: configPath ?? null,
61
+ model: modelSpec ?? null,
62
+ thinkingLevel: config.thinkingLevel ?? null,
63
+ context: definition.contextFiles.map((f) => f.path),
64
+ persona: definition.persona !== undefined,
65
+ skills: definition.skills.map((skill) => ({ name: skill.name, description: skill.description })),
66
+ tools: tools.names,
67
+ deferredTools: tools.deferred,
68
+ toolError: tools.error ?? null,
69
+ channels,
70
+ schedules,
71
+ scheduleFailures: sched.failures,
72
+ selfSchedule: config.selfSchedule ?? false,
73
+ stateRoot,
74
+ sessionsDir,
75
+ authPath,
76
+ diagnostics: definition.diagnostics,
77
+ skillCollisions: definition.collisions,
78
+ toolCollisions: tools.collisions,
79
+ toolFailures: tools.failures,
80
+ }, null, 2));
81
+ return;
82
+ }
83
+ console.log(`dir: ${dir}`);
84
+ if (agentDir !== dir)
85
+ console.log(`agent: ${agentDir}`);
86
+ console.log(`config: ${configPath ?? "(none)"}`);
87
+ console.log(`model: ${modelSpec ?? "(not set — pass --model, set FASTAGENT_MODEL, or config.model)"}`);
88
+ if (config.thinkingLevel)
89
+ console.log(`thinking: ${config.thinkingLevel}`);
90
+ console.log(`context: ${definition.contextFiles.map((f) => f.path).join(", ") || "(none)"}`);
91
+ console.log(`persona: ${definition.persona ? "persona.md" : "(none)"}`);
92
+ console.log(`skills: ${definition.skills.map((skill) => skill.name).join(", ") || "(none)"}`);
93
+ console.log(`tools: ${tools.error ? "(could not load — see warning below)" : tools.names.join(", ") || "(none)"}`);
94
+ if (tools.deferred.length > 0)
95
+ console.log(`deferred: ${tools.deferred.join(", ")} (activated via search_tools)`);
96
+ console.log(`channels: ${channels.join(", ") || "(none)"}`);
97
+ console.log(`schedules: ${schedules.map((s) => `${s.name} (next ${s.next ?? "never"})`).join(", ") || "(none)"}`);
98
+ console.log(`selfSchedule: ${config.selfSchedule ? "on (mounts the wake tool when serving)" : "off"}`);
99
+ console.log(`state: ${stateRoot}`);
100
+ console.log(`sessions: ${sessionsDir}`);
101
+ console.log(`auth: ${authPath}`);
102
+ reportToolCollisions(tools.collisions);
103
+ reportModuleLoadFailures(tools.failures);
104
+ reportModuleLoadFailures(sched.failures);
105
+ if (tools.error)
106
+ log.warn(`[fastagent] ${tools.error}`);
107
+ reportDefinitionWarnings(definition.collisions, definition.diagnostics);
108
+ }
@@ -0,0 +1,8 @@
1
+ export interface InitOptions {
2
+ minimal: boolean;
3
+ /** false ⇔ `--no-install`. */
4
+ install: boolean;
5
+ flat: boolean;
6
+ agentDir?: string;
7
+ }
8
+ export declare function runInit(dirArg: string, opts: InitOptions): Promise<void>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `fastagent init [dir]`: scaffold a runnable agent and install its dependencies. Layout: flags force;
3
+ * otherwise the jurisdiction rule decides (see detectHostSignals) and the reason is printed.
4
+ * Deliberately no prompt — non-interactive executors (coding agents) get a deterministic default they
5
+ * can read and override.
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { detectHostSignals, nextStepCd, scaffoldWorkspace } from "../../scaffold/init.js";
10
+ import { failStartup, failUsage } from "../fail.js";
11
+ export async function runInit(dirArg, opts) {
12
+ const dir = resolve(dirArg);
13
+ let agentDir;
14
+ let signals = [];
15
+ if (opts.agentDir) {
16
+ // Same containment contract loadConfig enforces on config.agentDir: an escaping value would write
17
+ // the kit outside the workspace AND produce a config that can never load — refuse up front.
18
+ // POSIX-normalized: this lands verbatim in the generated config (agentDir: "./a/b") and the persona
19
+ // locator note — a Windows `relative()` would write backslashes into both.
20
+ const rel = relative(dir, resolve(dir, opts.agentDir)).split(sep).join("/");
21
+ if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel)) {
22
+ // An invalid flag VALUE is a usage error (exit 2), same class as a value the parser rejects.
23
+ failUsage(`--agent-dir ("${opts.agentDir}") must be a subdirectory of ${dir}`);
24
+ }
25
+ agentDir = `./${rel}`;
26
+ }
27
+ else if (!opts.flat) {
28
+ signals = await detectHostSignals(dir).catch(failStartup);
29
+ if (signals.length > 0)
30
+ agentDir = "./agent";
31
+ }
32
+ const { complete, created, skipped, patched, intoNonEmpty, warnings } = await scaffoldWorkspace(dir, {
33
+ minimal: opts.minimal,
34
+ agentDir,
35
+ }).catch(failStartup);
36
+ // The layout reason prints only once the scaffold actually happened — an "already a workspace" refusal
37
+ // must not be preceded by an announced decision that then never takes place.
38
+ if (signals.length > 0) {
39
+ console.error(`[fastagent] found ${signals.join(", ")} — an existing toolchain/deploy claims this directory, so the agent kit goes into ./agent (its own namespace; config.agentDir points there). cwd stays this directory. Override: --flat`);
40
+ }
41
+ console.error(`[fastagent] initialized ${dir}${complete ? "" : " (minimal)"}${agentDir ? ` — agent kit in ${agentDir}` : ""}`);
42
+ if (created.length > 0)
43
+ console.error(` created: ${created.join(", ")}`);
44
+ if (skipped.length > 0)
45
+ console.error(` kept existing: ${skipped.join(", ")}`);
46
+ if (patched.length > 0)
47
+ console.error(` updated: ${patched.join(", ")} (missing fastagent excludes appended)`);
48
+ if (intoNonEmpty && !agentDir) {
49
+ console.error(` note: scaffolded flat into a non-empty directory (nothing claims it — the directory is the agent); use --agent-dir <name> to put the kit in a subdir instead`);
50
+ }
51
+ for (const w of warnings)
52
+ console.error(`[fastagent] warn: ${w}`);
53
+ // Install deps only for a complete agent whose package.json we just wrote (a kept one is not ours).
54
+ // The manifest lives with the kit (agentDir when set), so the install runs there — never against a
55
+ // host repo's own package.json.
56
+ const kitDir = resolve(dir, agentDir ?? ".");
57
+ const willInstall = complete && opts.install && created.includes(join(agentDir ?? ".", "package.json"));
58
+ let installFailed = false;
59
+ if (willInstall) {
60
+ console.error(`[fastagent] installing dependencies (npm install${agentDir ? ` in ${agentDir}` : ""})…`);
61
+ installFailed = (await npmInstall(kitDir)) !== 0;
62
+ if (installFailed)
63
+ console.error(`[fastagent] warn: npm install failed — run it manually in ${kitDir} before \`fastagent dev\``);
64
+ }
65
+ console.error(` next steps:`);
66
+ const cdTarget = nextStepCd(process.cwd(), dir);
67
+ if (cdTarget)
68
+ console.error(` cd ${cdTarget}`);
69
+ if (complete && (!opts.install || installFailed))
70
+ console.error(` ${agentDir ? `(cd ${agentDir} && npm install)` : "npm install"}`);
71
+ console.error(` fastagent dev # serve locally and iterate`);
72
+ console.error(` fastagent add skill <owner/repo/path> # vendor more skills from GitHub`);
73
+ }
74
+ /** Run `npm install` in `cwd` (inherit stdio). Returns the exit code. */
75
+ function npmInstall(cwd) {
76
+ return new Promise((resolveCode) => {
77
+ const child = spawn("npm", ["install"], { cwd, stdio: "inherit" });
78
+ child.on("close", (code) => resolveCode(code ?? 1));
79
+ child.on("error", () => resolveCode(1));
80
+ });
81
+ }
@@ -0,0 +1,7 @@
1
+ export interface InvokeOptions {
2
+ model?: string;
3
+ authPath?: string;
4
+ /** false ⇔ `--no-input`. */
5
+ input?: boolean;
6
+ }
7
+ export declare function runInvoke(message: string, dirArg: string, opts: InvokeOptions): Promise<void>;
@@ -0,0 +1,28 @@
1
+ /** `fastagent invoke <message> [dir]`: run ONE turn against the assembled agent, then exit. */
2
+ import { randomUUID } from "node:crypto";
3
+ import { resolve } from "node:path";
4
+ import { loadDotEnv } from "../../env.js";
5
+ import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
6
+ import { runInvokeStream } from "../invoke-stream.js";
7
+ import { installProxyFetch } from "../../proxy.js";
8
+ import { failStartup } from "../fail.js";
9
+ import { reportAuth, resolveFirstRunModel } from "../shared.js";
10
+ export async function runInvoke(message, dirArg, opts) {
11
+ const invokeDir = resolve(dirArg);
12
+ loadDotEnv(invokeDir);
13
+ installProxyFetch();
14
+ await resolveFirstRunModel(invokeDir, opts);
15
+ const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(invokeDir, {
16
+ model: opts.model,
17
+ authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
18
+ }).catch(failStartup);
19
+ console.error(`[fastagent] invoke: ${invokeDir} (${modelSpec})`);
20
+ await reportAuth(modelSpec, authPath);
21
+ // Fresh session per invoke (one-shot, no resume). runInvokeStream maps events→IO: reply→stdout,
22
+ // tool/failure→stderr, exit 1 iff the turn failed (so CI can gate on it).
23
+ const exitCode = await runInvokeStream(agent.invoke({ session: randomUUID() }, { text: message }), (text) => process.stdout.write(text), (line) => console.error(line));
24
+ process.stdout.write("\n");
25
+ // Always exit explicitly: the undici proxy agent's keep-alive sockets would otherwise hold the
26
+ // event loop open after a successful one-shot turn.
27
+ process.exit(exitCode);
28
+ }
@@ -0,0 +1,6 @@
1
+ export interface LoginOptions {
2
+ authPath?: string;
3
+ /** false ⇔ `--no-input`. */
4
+ input?: boolean;
5
+ }
6
+ export declare function runLogin(provider: string | undefined, opts: LoginOptions): Promise<void>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `fastagent login [provider]`: authenticate a model provider into the project-level auth file
3
+ * (`<cwd>/.fastagent/auth.json`) by default, or `--auth-path`/`FASTAGENT_AUTH_PATH`. The positional is
4
+ * the PROVIDER (not a dir), so the project is cwd — `cd` into your agent before logging in (running it
5
+ * from $HOME writes the global `~/.fastagent/auth.json`).
6
+ *
7
+ * Creates and self-ignores `<cwd>/.fastagent/` (the credential's gitignored home) BEFORE the auth flow,
8
+ * so the secret can never land untracked — a flow that then fails (bad provider, abort) leaves that
9
+ * empty state dir behind, by design (no secret without its `.gitignore`). Skipped for the HOME-global dir.
10
+ */
11
+ import { loadDotEnv } from "../../env.js";
12
+ import { resolveAuthPath, resolveStateRoot } from "../../engines/pi/config.js";
13
+ import { ensureStateRootSelfIgnored, isUnderDir } from "../../engines/pi/definition.js";
14
+ import { LoginCancelled } from "../../engines/pi/login.js";
15
+ import { installProxyFetch } from "../../proxy.js";
16
+ import { failStartup } from "../fail.js";
17
+ import { isInteractive, loginWithKeyCheck } from "../shared.js";
18
+ export async function runLogin(provider, opts) {
19
+ const loginDir = process.cwd();
20
+ loadDotEnv(loginDir); // FASTAGENT_AUTH_PATH / a proxy (HTTPS_PROXY) may be configured in the project .env
21
+ installProxyFetch(); // the OAuth token exchange must go through HTTPS_PROXY (region-locked providers)
22
+ const stateRoot = resolveStateRoot(loginDir);
23
+ const authPath = resolveAuthPath(loginDir, opts.authPath); // flag > FASTAGENT_AUTH_PATH > default — the one owner
24
+ // login is the command that CREATES the credential file, so the leak guard binds HERE too (not only
25
+ // in the opener): on an adapted project dir, a `login` before the first dev/start would otherwise
26
+ // leave the secret untracked-but-committable. Unlike the opener (which populates the WHOLE root, so
27
+ // it always self-ignores an in-tree root), login writes ONLY auth.json — so guard iff the credential
28
+ // actually lands under the in-tree root. An external `--auth-path`/`FASTAGENT_AUTH_PATH` writes
29
+ // nothing in-tree (don't create an empty `.fastagent`); the guard also skips the HOME-global root.
30
+ if (isUnderDir(authPath, stateRoot))
31
+ await ensureStateRootSelfIgnored(loginDir, stateRoot);
32
+ // login is inherently interactive — loginFlow renders provider/method menus and opens a browser (or
33
+ // prompts for a key). In a non-TTY (a pipe, CI, a coding-agent shell) the menu can't receive keystrokes
34
+ // and would hang; --no-input asks for the same posture explicitly. Fail fast with the reason instead
35
+ // of stalling on an unanswerable prompt. (After the secret-hygiene self-ignore above, which is cheap
36
+ // prep, so a later terminal login is safe.)
37
+ if (opts.input === false || !isInteractive()) {
38
+ failStartup(new Error(`login is interactive (it shows a menu and opens a browser) — run it in a terminal, not a pipe/CI`));
39
+ }
40
+ // loginWithKeyCheck: an entered API key is verified with one minimal request; a rejected key (401)
41
+ // re-prompts in place, so a returned result is always a stored-and-not-definitively-bad credential.
42
+ const result = await loginWithKeyCheck(provider, authPath).catch((error) => {
43
+ if (error instanceof LoginCancelled) {
44
+ // A decision, not a failure — neutral wording; non-zero exit because no credential was stored.
45
+ console.error(`[fastagent] login cancelled`);
46
+ process.exit(1);
47
+ }
48
+ failStartup(error);
49
+ });
50
+ console.error(`[fastagent] logged in to ${result.provider} (${result.method}) — saved to ${authPath}`);
51
+ process.exit(0); // the undici proxy agent's keep-alive sockets would otherwise hold the event loop open
52
+ }
@@ -0,0 +1 @@
1
+ export declare function runModels(search: string | undefined): void;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `fastagent models [search]`: print every registered "provider/modelId"; `[search]` filters by
3
+ * substring. This module pulls the pi model catalog (heavy) — it is lazy-imported by the spec, so
4
+ * only an actual `models` invocation pays for it.
5
+ */
6
+ import { formatModelsCommand } from "../models-view.js";
7
+ import { listModels } from "../../engines/pi/config.js";
8
+ import { createPiModels } from "../../engines/pi/models.js";
9
+ export function runModels(search) {
10
+ const { lines, error } = formatModelsCommand(listModels(createPiModels()), search);
11
+ for (const spec of lines)
12
+ console.log(spec);
13
+ if (error)
14
+ console.error(error);
15
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `fastagent schedule history <name> [dir]`: print the run audit for one schedule (or "wake") — fired
3
+ * time, outcome, duration, reply/error. Read-only (reads `<stateRoot>/schedule/runs.jsonl`); the answer
4
+ * to "did last night's run silently fail?". Text mode previews the reply/error; --json is the full record.
5
+ */
6
+ export declare function runScheduleHistory(name: string, dirArg: string, json: boolean): void;
7
+ /** `fastagent schedule list [dir]`: everything that will fire — BOTH producers: the static `schedules/`
8
+ * files (with their next instant) and the agent's pending self-scheduled wake-ups. Read-only. */
9
+ export declare function runScheduleList(dirArg: string, json: boolean): Promise<void>;
10
+ /** `fastagent schedule cancel <id> [dir]`: remove a pending wake-up — the operator's kill switch (the
11
+ * agent's own is the `unwake` tool). Unlike unwake it is NOT session-scoped: the operator owns the box. */
12
+ export declare function runScheduleCancel(id: string, dirArg: string): void;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `fastagent schedule history|list|cancel` — the operator's surface over schedule state. history and
3
+ * list are read-only; cancel is the kill switch for a pending wake-up (the agent's own is the `unwake`
4
+ * tool). All three read the SAME state root the scheduler writes (FASTAGENT_STATE_DIR may live in .env).
5
+ */
6
+ import { resolve } from "node:path";
7
+ import { loadDotEnv } from "../../env.js";
8
+ import { loadConfig, resolveAgentDir, resolveStateRoot } from "../../engines/pi/config.js";
9
+ import { reportModuleLoadFailures } from "../../engines/pi/report.js";
10
+ import { readRuns } from "../../schedule/audit.js";
11
+ import { nextRun } from "../../schedule/cron.js";
12
+ import { loadSchedules } from "../../schedule/discover.js";
13
+ import { listWakeups, removeWakeup } from "../../schedule/wakeups.js";
14
+ import { failStartup } from "../fail.js";
15
+ /**
16
+ * `fastagent schedule history <name> [dir]`: print the run audit for one schedule (or "wake") — fired
17
+ * time, outcome, duration, reply/error. Read-only (reads `<stateRoot>/schedule/runs.jsonl`); the answer
18
+ * to "did last night's run silently fail?". Text mode previews the reply/error; --json is the full record.
19
+ */
20
+ export function runScheduleHistory(name, dirArg, json) {
21
+ const target = resolve(dirArg);
22
+ loadDotEnv(target); // FASTAGENT_STATE_DIR may live in .env — read the SAME state root the scheduler wrote
23
+ const runs = readRuns(resolveStateRoot(target), name);
24
+ if (json) {
25
+ console.log(JSON.stringify(runs, null, 2));
26
+ return;
27
+ }
28
+ if (runs.length === 0) {
29
+ console.error(`no recorded runs for "${name}" (state: ${resolveStateRoot(target)})`);
30
+ return;
31
+ }
32
+ // The question is "did LAST NIGHT's run fail?" — so text mode tails the most recent runs (chronological
33
+ // within the tail); --json above returns the full history.
34
+ const TAIL = 20;
35
+ const shown = runs.slice(-TAIL);
36
+ if (runs.length > shown.length) {
37
+ console.error(`(showing the last ${shown.length} of ${runs.length} runs — --json for all)`);
38
+ }
39
+ for (const r of shown) {
40
+ const detail = r.error ?? r.reply ?? "";
41
+ const preview = detail.replace(/\s+/g, " ").slice(0, 100);
42
+ console.log(`${r.firedAt} ${r.outcome.padEnd(9)} ${String(r.ms).padStart(6)}ms ${preview}`);
43
+ }
44
+ }
45
+ /** `fastagent schedule list [dir]`: everything that will fire — BOTH producers: the static `schedules/`
46
+ * files (with their next instant) and the agent's pending self-scheduled wake-ups. Read-only. */
47
+ export async function runScheduleList(dirArg, json) {
48
+ const target = resolve(dirArg);
49
+ loadDotEnv(target);
50
+ const { config } = await loadConfig(target).catch(failStartup);
51
+ const agentDir = resolveAgentDir(target, config);
52
+ const { schedules, failures } = await loadSchedules(agentDir).catch(failStartup);
53
+ reportModuleLoadFailures(failures);
54
+ const wakeups = listWakeups(resolveStateRoot(target));
55
+ if (json) {
56
+ console.log(JSON.stringify({
57
+ schedules: schedules.map((s) => ({ ...s, next: nextRun(s.cron, s.tz, new Date())?.toISOString() })),
58
+ wakeups,
59
+ }, null, 2));
60
+ return;
61
+ }
62
+ if (schedules.length === 0 && wakeups.length === 0) {
63
+ console.error(`nothing scheduled — no schedules/ files, no pending wake-ups (state: ${resolveStateRoot(target)})`);
64
+ return;
65
+ }
66
+ for (const s of schedules) {
67
+ const next = nextRun(s.cron, s.tz, new Date())?.toISOString() ?? "(never)";
68
+ console.log(`schedule ${s.name.padEnd(20)} ${next} cron ${s.cron}${s.tz ? ` ${s.tz}` : ""}`);
69
+ }
70
+ for (const w of wakeups) {
71
+ const kind = w.cron ? `cron ${w.cron}${w.tz ? ` ${w.tz}` : ""}` : "one-shot";
72
+ console.log(`wake ${w.id} ${w.fireAt} ${kind} session=${w.session} ${w.prompt.slice(0, 60)}`);
73
+ }
74
+ }
75
+ /** `fastagent schedule cancel <id> [dir]`: remove a pending wake-up — the operator's kill switch (the
76
+ * agent's own is the `unwake` tool). Unlike unwake it is NOT session-scoped: the operator owns the box. */
77
+ export function runScheduleCancel(id, dirArg) {
78
+ const target = resolve(dirArg);
79
+ loadDotEnv(target);
80
+ if (removeWakeup(resolveStateRoot(target), id)) {
81
+ // ponytail: the store's load→save is lock-free — a serving scheduler's claim-advance can race this
82
+ // write (window = ms around each fire). Tell the operator to verify; a lockfile/CAS is the upgrade
83
+ // path if it ever bites.
84
+ console.error(`[fastagent] cancelled wake-up ${id} — if a server is running, verify with \`fastagent schedule list\``);
85
+ }
86
+ else {
87
+ failStartup(new Error(`no pending wake-up ${id} (state: ${resolveStateRoot(target)}) — \`fastagent schedule list\` shows ids`));
88
+ }
89
+ }
@@ -0,0 +1,10 @@
1
+ export interface StartOptions {
2
+ port?: string;
3
+ model?: string;
4
+ sessionsDir?: string;
5
+ authPath?: string;
6
+ tunnel?: boolean;
7
+ /** false ⇔ `--no-input`. */
8
+ input?: boolean;
9
+ }
10
+ export declare function runStart(dirArg: string, opts: StartOptions): Promise<void>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `fastagent start [dir]`: run the agent in production posture — the SAME assembly as dev (your
3
+ * directory is the agent), just no file-watching. No build step: start reads the definition directly.
4
+ */
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
+ import { dirname, resolve } from "node:path";
7
+ import { authSeedBytes } from "../../deploy/fly/run.js";
8
+ import { loadDotEnv } from "../../env.js";
9
+ import { resolveAuthPath, resolveSessionsDirOverride } from "../../engines/pi/config.js";
10
+ import { isUnderDir } from "../../engines/pi/definition.js";
11
+ import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "../../engines/pi/report.js";
12
+ import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
13
+ import { log, setLogLevel } from "../../log.js";
14
+ import { logAgentLoop } from "../../observe.js";
15
+ import { installProxyFetch } from "../../proxy.js";
16
+ import { exists } from "../../scaffold/init.js";
17
+ import { failStartup } from "../fail.js";
18
+ import { maybeTunnel, mountSessionControl, routesFor, serve, startSchedules } from "../serve.js";
19
+ import { parsePort, reportAuth, resolveFirstRunModel } from "../shared.js";
20
+ export async function runStart(dirArg, opts) {
21
+ const dir = resolve(dirArg);
22
+ setLogLevel("info"); // production posture: info+, the debug turn trace (and its end-user content) gated out
23
+ const portFlag = parsePort(opts.port, "--port", "flag");
24
+ loadDotEnv(dir);
25
+ installProxyFetch();
26
+ await resolveFirstRunModel(dir, opts);
27
+ // A `deploy --run` may carry the operator's local credential as FASTAGENT_AUTH_SEED —
28
+ // materialize it onto the writable state root BEFORE the opener resolves auth (once, absent-only).
29
+ // Same resolveAuthPath the opener uses — ONE owner of the flag > env > default chain.
30
+ await maybeSeedAuth(resolveAuthPath(dir, opts.authPath));
31
+ // The same opener dev uses (single assembly source), just no watch.
32
+ const sessionsDirOverride = resolveSessionsDirOverride(opts.sessionsDir);
33
+ const { agent, definition, agentDir, config, modelSpec, stateRoot, sessionsDir, authPath, toolNames, deferredToolNames, toolCollisions, toolFailures, sessionControl, } = await createPiAgentFromWorkspace(dir, {
34
+ model: opts.model,
35
+ sessionsDir: sessionsDirOverride,
36
+ authPath: opts.authPath,
37
+ serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
38
+ }).catch(failStartup);
39
+ log.info(`[fastagent] start: ${dir}`);
40
+ if (agentDir !== dir)
41
+ log.info(`[fastagent] agent: ${agentDir}`);
42
+ log.info(`[fastagent] model: ${modelSpec}${config.thinkingLevel ? ` (thinking: ${config.thinkingLevel})` : ""}`);
43
+ await reportAuth(modelSpec, authPath);
44
+ log.info(`[fastagent] context: ${definition.contextFiles.map((f) => f.path).join(", ") || "(none)"}`);
45
+ if (definition.persona)
46
+ log.info(`[fastagent] persona: persona.md`);
47
+ log.info(`[fastagent] skills: ${definition.skills.map((s) => s.name).join(", ") || "(none)"}`);
48
+ if (toolNames.length > 0)
49
+ log.info(`[fastagent] tools: ${toolNames.join(", ")}`);
50
+ if (deferredToolNames.length > 0) {
51
+ log.info(`[fastagent] deferred: ${deferredToolNames.join(", ")} (activated via search_tools)`);
52
+ }
53
+ reportToolCollisions(toolCollisions);
54
+ reportModuleLoadFailures(toolFailures);
55
+ log.info(`[fastagent] state: ${stateRoot}`);
56
+ log.info(`[fastagent] sessions: ${sessionsDir}`);
57
+ // State defaults under the definition dir, which a redeploy may replace wholesale. Gate on where the
58
+ // root ACTUALLY resolved (in-tree?), not on the raw env var: an empty `FASTAGENT_STATE_DIR=""` reads
59
+ // as unset (resolveStateRoot) and still lands in-tree, so a raw `=== undefined` check would wrongly
60
+ // silence the warning. A sessions/auth override to a volume does not help — channel state (the
61
+ // telegram turn/context files replay depends on) is still in-tree.
62
+ if (isUnderDir(stateRoot, dir)) {
63
+ log.info(`[fastagent] note: state (auth, sessions, channel state) lives under the definition dir; point ` +
64
+ `FASTAGENT_STATE_DIR at a persistent volume so a redeploy that replaces the dir does not wipe it.`);
65
+ }
66
+ reportDefinitionWarnings(definition.collisions, definition.diagnostics);
67
+ // Same debug turn trace as dev; gated out here by the info level (see dev.ts serveOnce).
68
+ const traced = logAgentLoop(agent);
69
+ const routed = await routesFor(agentDir, traced, stateRoot, sessionControl).catch(failStartup);
70
+ const withControl = mountSessionControl(routed.routes, sessionControl, stateRoot, {
71
+ tunnel: opts.tunnel ?? false,
72
+ agent: traced,
73
+ });
74
+ await startSchedules(agentDir, traced, stateRoot, config.selfSchedule ?? false);
75
+ serve({ ...routed, routes: withControl.routes }, portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, (p) => {
76
+ withControl.announce(p);
77
+ maybeTunnel(dir, routed.routeChannels, p, opts.tunnel ?? false, stateRoot);
78
+ });
79
+ // No graceful drain: webhook turns run fire-and-forget; SIGTERM just exits mid-turn. Whether an
80
+ // in-flight turn is LOST depends on the channel: the Telegram channel persists turn intent pre-ACK
81
+ // and replays it next start (turn-store.ts, L1 durable execution, at-least-once); HTTP and other
82
+ // channels have no such layer, so their in-flight turns are still lost (the asker re-invokes).
83
+ }
84
+ /**
85
+ * Materialize `FASTAGENT_AUTH_SEED` (base64 of an auth.json, set by `deploy --run`) onto the
86
+ * writable state root ONCE — only when the seed is set AND the auth file is absent, so a refreshed
87
+ * volume copy is never clobbered by the stale seed. Lets a deploy carry the operator's local
88
+ * OAuth/API credential so the box runs on the SAME subscription. No-op locally (the seed is unset).
89
+ */
90
+ async function maybeSeedAuth(authPath) {
91
+ const bytes = authSeedBytes(process.env.FASTAGENT_AUTH_SEED, await exists(authPath));
92
+ if (!bytes)
93
+ return;
94
+ await mkdir(dirname(authPath), { recursive: true });
95
+ await writeFile(authPath, bytes);
96
+ log.info(`[fastagent] seeded ${authPath} from FASTAGENT_AUTH_SEED (first boot)`);
97
+ }
@@ -0,0 +1 @@
1
+ export declare function runTool(name: string, argsJson: string, dirArg: string): Promise<void>;
@@ -0,0 +1,38 @@
1
+ /** `fastagent tool <name> '<json>' [dir]`: run one tool's body directly with JSON args — no model. */
2
+ import { resolve } from "node:path";
3
+ import { loadDotEnv } from "../../env.js";
4
+ import { loadConfig, resolveAgentDir } from "../../engines/pi/config.js";
5
+ import { resolveWorkspaceTools } from "../../engines/pi/create.js";
6
+ import { reportModuleLoadFailures } from "../../engines/pi/report.js";
7
+ import { turnContext } from "../../engines/pi/tool-context.js";
8
+ import { failStartup, failUsage } from "../fail.js";
9
+ export async function runTool(name, argsJson, dirArg) {
10
+ const toolDir = resolve(dirArg);
11
+ loadDotEnv(toolDir); // a tool may read a key from .env
12
+ const { config } = await loadConfig(toolDir).catch(failStartup);
13
+ // The same tool set dev/start mount (defaults + config.tools + discovered, deduped), so the runner
14
+ // exercises exactly what gets served — a shadowed tool is surfaced, not silently run. Resolve agentDir
15
+ // like the openers so `fastagent tool` finds the SAME tools/ as dev/start when config.agentDir is set.
16
+ const agentDir = resolveAgentDir(toolDir, config);
17
+ const { tools, toolCollisions, toolFailures } = await resolveWorkspaceTools(config, agentDir, toolDir).catch(failStartup);
18
+ for (const c of toolCollisions) {
19
+ console.error(`[fastagent] warn: tool "${c.name}" (${c.source}) is shadowed by a default/config tool — not mounted`);
20
+ }
21
+ reportModuleLoadFailures(toolFailures);
22
+ const tool = tools.find((t) => t.name === name);
23
+ if (!tool) {
24
+ failStartup(new Error(`unknown tool "${name}". available: ${tools.map((t) => t.name).join(", ") || "(none)"}`));
25
+ }
26
+ let args;
27
+ try {
28
+ args = JSON.parse(argsJson);
29
+ }
30
+ catch {
31
+ failUsage(`invalid JSON args: ${argsJson}`); // malformed input syntax = usage error, exit 2
32
+ }
33
+ const result = await turnContext.run({ cwd: toolDir }, () => tool.execute(`cli-${name}`, args)).catch(failStartup);
34
+ const out = result?.details !== undefined
35
+ ? result.details
36
+ : (result?.content ?? []).map((c) => ("text" in c ? c.text : "")).join("");
37
+ console.log(typeof out === "string" ? out : JSON.stringify(out, null, 2));
38
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The ONE error prefix every error message carries — bold red when stderr renders color, plain
3
+ * otherwise. Errors are the only place the CLI uses color at all.
4
+ */
5
+ export declare function errorPrefix(colors?: boolean): string;
6
+ /**
7
+ * User-fixable startup problems (missing model / bad config / broken definition) are thrown as plain
8
+ * `Error` — print just the message. Anything else (TypeError, non-Error) is a bug: keep the stack.
9
+ * Shared by the kernel and the command modules; exit 1 (runtime failure).
10
+ */
11
+ export declare function failStartup(error: unknown): never;
12
+ /**
13
+ * A usage error the parser could not catch (a bad value shape, an invalid flag/argument combination
14
+ * discovered in a command body): print the message and exit 2 — the same class as a parse error.
15
+ * Exit codes follow responsibility, not the layer that happens to discover the problem.
16
+ */
17
+ export declare function failUsage(message: string): never;
@@ -0,0 +1,32 @@
1
+ /** stderr renders color: a color TTY, with Node's `hasColors()` carrying the NO_COLOR/TERM=dumb veto. */
2
+ function stderrHasColors() {
3
+ return process.stderr.isTTY === true && (process.stderr.hasColors?.() ?? false);
4
+ }
5
+ /**
6
+ * The ONE error prefix every error message carries — bold red when stderr renders color, plain
7
+ * otherwise. Errors are the only place the CLI uses color at all.
8
+ */
9
+ export function errorPrefix(colors = stderrHasColors()) {
10
+ return colors ? "\x1b[1;31mError:\x1b[0m" : "Error:";
11
+ }
12
+ /**
13
+ * User-fixable startup problems (missing model / bad config / broken definition) are thrown as plain
14
+ * `Error` — print just the message. Anything else (TypeError, non-Error) is a bug: keep the stack.
15
+ * Shared by the kernel and the command modules; exit 1 (runtime failure).
16
+ */
17
+ export function failStartup(error) {
18
+ if (error instanceof Error && error.constructor === Error)
19
+ console.error(`${errorPrefix()} ${error.message}`);
20
+ else
21
+ console.error(errorPrefix(), error);
22
+ process.exit(1);
23
+ }
24
+ /**
25
+ * A usage error the parser could not catch (a bad value shape, an invalid flag/argument combination
26
+ * discovered in a command body): print the message and exit 2 — the same class as a parse error.
27
+ * Exit codes follow responsibility, not the layer that happens to discover the problem.
28
+ */
29
+ export function failUsage(message) {
30
+ console.error(`${errorPrefix()} ${message}`);
31
+ process.exit(2);
32
+ }
@@ -6,5 +6,5 @@
6
6
  * An errored tool inside an otherwise-completed turn still surfaces (the diagnostic the operator
7
7
  * needs); tool_ended carries no name, so it is remembered from the matching tool_started.
8
8
  */
9
- import type { AgentEvent } from "./agent.ts";
9
+ import type { AgentEvent } from "../agent.ts";
10
10
  export declare function runInvokeStream(events: AsyncIterable<AgentEvent>, out: (text: string) => void, err: (line: string) => void): Promise<number>;