@fastagent-sh/fastagent 0.12.1 → 0.14.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 (151) hide show
  1. package/README.md +6 -4
  2. package/dist/channels/feishu/bootstrap-token.d.ts +42 -0
  3. package/dist/channels/feishu/bootstrap-token.js +94 -0
  4. package/dist/channels/feishu/card.d.ts +32 -0
  5. package/dist/channels/feishu/card.js +66 -0
  6. package/dist/channels/feishu/cloud.d.ts +17 -0
  7. package/dist/channels/feishu/cloud.js +19 -0
  8. package/dist/channels/feishu/context-buffer.d.ts +46 -0
  9. package/dist/channels/feishu/context-buffer.js +133 -0
  10. package/dist/channels/feishu/crypto.d.ts +15 -0
  11. package/dist/channels/feishu/crypto.js +43 -0
  12. package/dist/channels/feishu/feishu-api.d.ts +108 -0
  13. package/dist/channels/feishu/feishu-api.js +325 -0
  14. package/dist/channels/feishu/feishu.d.ts +46 -0
  15. package/dist/channels/feishu/feishu.js +472 -0
  16. package/dist/channels/feishu/invoke-turn.d.ts +65 -0
  17. package/dist/channels/feishu/invoke-turn.js +157 -0
  18. package/dist/channels/feishu/model.d.ts +97 -0
  19. package/dist/channels/feishu/model.js +9 -0
  20. package/dist/channels/feishu/normalize.d.ts +22 -0
  21. package/dist/channels/feishu/normalize.js +132 -0
  22. package/dist/channels/feishu/owned-threads.d.ts +11 -0
  23. package/dist/channels/feishu/owned-threads.js +47 -0
  24. package/dist/channels/feishu/parse.d.ts +43 -0
  25. package/dist/channels/feishu/parse.js +65 -0
  26. package/dist/channels/feishu/preview.d.ts +36 -0
  27. package/dist/channels/feishu/preview.js +387 -0
  28. package/dist/channels/feishu/register-app.d.ts +70 -0
  29. package/dist/channels/feishu/register-app.js +141 -0
  30. package/dist/channels/feishu/register-webhook.d.ts +22 -0
  31. package/dist/channels/feishu/register-webhook.js +106 -0
  32. package/dist/channels/feishu/scaffold/channel.ts +41 -0
  33. package/dist/channels/feishu/scaffold/feishu-send.ts +87 -0
  34. package/dist/channels/feishu/seen.d.ts +5 -0
  35. package/dist/channels/feishu/seen.js +47 -0
  36. package/dist/channels/feishu/text.d.ts +13 -0
  37. package/dist/channels/feishu/text.js +63 -0
  38. package/dist/channels/lark/lark.d.ts +15 -0
  39. package/dist/channels/lark/lark.js +10 -0
  40. package/dist/channels/lark/onboard.d.ts +39 -0
  41. package/dist/channels/lark/onboard.js +58 -0
  42. package/dist/channels/lark/scaffold/channel.ts +39 -0
  43. package/dist/channels/lark/scaffold/lark-send.ts +87 -0
  44. package/dist/channels/registration.d.ts +15 -0
  45. package/dist/channels/registration.js +1 -0
  46. package/dist/channels/{telegram/state.js → state.js} +6 -4
  47. package/dist/channels/telegram/context-buffer.js +1 -1
  48. package/dist/channels/telegram/register-webhook.d.ts +4 -1
  49. package/dist/channels/telegram/register-webhook.js +17 -26
  50. package/dist/channels/telegram/telegram.js +2 -2
  51. package/dist/channels/telegram/turn-store.d.ts +8 -21
  52. package/dist/channels/telegram/turn-store.js +11 -130
  53. package/dist/channels/{telegram/turn-queue.js → turn-queue.js} +3 -3
  54. package/dist/channels/turn-store.d.ts +42 -0
  55. package/dist/channels/turn-store.js +139 -0
  56. package/dist/channels/wait-health.d.ts +6 -0
  57. package/dist/channels/wait-health.js +27 -0
  58. package/dist/cli/commands/add.d.ts +9 -0
  59. package/dist/cli/commands/add.js +142 -0
  60. package/dist/cli/commands/chat.d.ts +3 -0
  61. package/dist/cli/commands/chat.js +16 -0
  62. package/dist/cli/commands/deploy.d.ts +13 -0
  63. package/dist/cli/commands/deploy.js +338 -0
  64. package/dist/cli/commands/dev.d.ts +11 -0
  65. package/dist/cli/commands/dev.js +76 -0
  66. package/dist/cli/commands/fire.d.ts +7 -0
  67. package/dist/cli/commands/fire.js +45 -0
  68. package/dist/cli/commands/info.d.ts +7 -0
  69. package/dist/cli/commands/info.js +108 -0
  70. package/dist/cli/commands/init.d.ts +8 -0
  71. package/dist/cli/commands/init.js +81 -0
  72. package/dist/cli/commands/invoke.d.ts +7 -0
  73. package/dist/cli/commands/invoke.js +29 -0
  74. package/dist/cli/commands/login.d.ts +6 -0
  75. package/dist/cli/commands/login.js +63 -0
  76. package/dist/cli/commands/models.d.ts +1 -0
  77. package/dist/cli/commands/models.js +15 -0
  78. package/dist/cli/commands/schedule.d.ts +12 -0
  79. package/dist/cli/commands/schedule.js +89 -0
  80. package/dist/cli/commands/start.d.ts +10 -0
  81. package/dist/cli/commands/start.js +90 -0
  82. package/dist/cli/commands/tool.d.ts +1 -0
  83. package/dist/cli/commands/tool.js +37 -0
  84. package/dist/cli/fail.d.ts +19 -0
  85. package/dist/cli/fail.js +32 -0
  86. package/dist/cli/kernel.d.ts +89 -0
  87. package/dist/cli/kernel.js +190 -0
  88. package/dist/cli/program.d.ts +11 -0
  89. package/dist/cli/program.js +421 -0
  90. package/dist/cli/serve.d.ts +28 -0
  91. package/dist/cli/serve.js +90 -0
  92. package/dist/cli/shared.d.ts +24 -0
  93. package/dist/cli/shared.js +116 -0
  94. package/dist/cli-add-feishu.d.ts +8 -0
  95. package/dist/cli-add-feishu.js +223 -0
  96. package/dist/cli.js +8 -1284
  97. package/dist/deploy/container.js +10 -6
  98. package/dist/deploy/docker/plan.d.ts +45 -0
  99. package/dist/deploy/docker/plan.js +139 -0
  100. package/dist/deploy/docker/run.d.ts +40 -0
  101. package/dist/deploy/docker/run.js +126 -0
  102. package/dist/deploy/fly/plan.d.ts +1 -1
  103. package/dist/deploy/fly/plan.js +15 -4
  104. package/dist/deploy/fly/run.d.ts +7 -4
  105. package/dist/deploy/fly/run.js +26 -5
  106. package/dist/deploy/preflight.js +4 -3
  107. package/dist/deploy/railway/plan.d.ts +1 -1
  108. package/dist/deploy/railway/plan.js +17 -5
  109. package/dist/deploy/railway/run.d.ts +6 -3
  110. package/dist/deploy/railway/run.js +26 -4
  111. package/dist/deploy/registration-gate.d.ts +20 -0
  112. package/dist/deploy/registration-gate.js +20 -0
  113. package/dist/deploy/runner.d.ts +4 -1
  114. package/dist/deploy/runner.js +1 -0
  115. package/dist/deploy/secrets.d.ts +10 -9
  116. package/dist/deploy/secrets.js +15 -14
  117. package/dist/dev-supervisor.js +2 -1
  118. package/dist/engines/pi/auth.js +160 -46
  119. package/dist/engines/pi/chat.js +80 -7
  120. package/dist/engines/pi/config.d.ts +12 -3
  121. package/dist/engines/pi/config.js +16 -1
  122. package/dist/engines/pi/create.d.ts +14 -6
  123. package/dist/engines/pi/create.js +52 -16
  124. package/dist/engines/pi/harness.d.ts +16 -1
  125. package/dist/engines/pi/harness.js +77 -1
  126. package/dist/engines/pi/invoke.d.ts +1 -1
  127. package/dist/engines/pi/invoke.js +37 -2
  128. package/dist/engines/pi/login.js +1 -1
  129. package/dist/engines/pi/search-tools.d.ts +10 -0
  130. package/dist/engines/pi/search-tools.js +138 -0
  131. package/dist/engines/pi/tool-context.d.ts +28 -0
  132. package/dist/engines/pi/tool-context.js +8 -0
  133. package/dist/engines/pi/tool.d.ts +32 -1
  134. package/dist/engines/pi/tool.js +42 -1
  135. package/dist/engines/pi/workspace.d.ts +4 -1
  136. package/dist/engines/pi/workspace.js +3 -1
  137. package/dist/feishu.d.ts +2 -0
  138. package/dist/feishu.js +2 -0
  139. package/dist/lark.d.ts +3 -0
  140. package/dist/lark.js +3 -0
  141. package/dist/open-url.d.ts +2 -0
  142. package/dist/open-url.js +6 -0
  143. package/dist/pi.d.ts +2 -1
  144. package/dist/scaffold/add-channel.d.ts +9 -5
  145. package/dist/scaffold/add-channel.js +75 -7
  146. package/dist/scaffold/templates/fastagent.config.mjs +1 -0
  147. package/dist/tunnel.d.ts +9 -6
  148. package/dist/tunnel.js +48 -31
  149. package/package.json +18 -4
  150. /package/dist/channels/{telegram/state.d.ts → state.d.ts} +0 -0
  151. /package/dist/channels/{telegram/turn-queue.d.ts → turn-queue.d.ts} +0 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `fastagent fire <name> [dir]`: run ONE schedule's turn immediately — the authoring loop for schedules
3
+ * (like `invoke` is for a prompt). Fires `schedules/<name>.ts` now, without waiting for its cron, using
4
+ * the schedule's stable session (faithful to the served behavior). Does NOT advance the schedule's fire
5
+ * state — a test run must never make the scheduler skip the real next run.
6
+ */
7
+ import { relative, resolve, sep } from "node:path";
8
+ import { loadDotEnv } from "../../env.js";
9
+ import { loadConfig, resolveAgentDir, resolveAuthPathOverride } from "../../engines/pi/config.js";
10
+ import { reportModuleLoadFailures } from "../../engines/pi/report.js";
11
+ import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
12
+ import { runInvokeStream } from "../../invoke-stream.js";
13
+ import { installProxyFetch } from "../../proxy.js";
14
+ import { loadSchedules } from "../../schedule/discover.js";
15
+ import { scheduleSession } from "../../schedule/scheduler.js";
16
+ import { failStartup } from "../fail.js";
17
+ import { reportAuth, resolveFirstRunModel } from "../shared.js";
18
+ export async function runFire(name, dirArg, opts) {
19
+ const fireDir = resolve(dirArg);
20
+ loadDotEnv(fireDir);
21
+ installProxyFetch();
22
+ await resolveFirstRunModel(fireDir, opts);
23
+ // Schedules are agent surface — discover them where dev/start/`schedule list` do (agentDir), not the
24
+ // run root, so `fire` sees the same set the scheduler serves in the kit layout.
25
+ const { config: fireConfig } = await loadConfig(fireDir).catch(failStartup);
26
+ const fireAgentDir = resolveAgentDir(fireDir, fireConfig);
27
+ const { schedules, failures } = await loadSchedules(fireAgentDir).catch(failStartup);
28
+ reportModuleLoadFailures(failures);
29
+ const schedule = schedules.find((s) => s.name === name);
30
+ if (!schedule) {
31
+ // Name the discovery path in the kit layout: a schedule misplaced at the run root should read as
32
+ // "wrong place", not "broken file".
33
+ const looked = fireAgentDir === fireDir ? "" : ` (looked in ${relative(fireDir, fireAgentDir).split(sep).join("/")}/schedules)`;
34
+ failStartup(new Error(`unknown schedule "${name}"${looked}. available: ${schedules.map((s) => s.name).join(", ") || "(none)"}`));
35
+ }
36
+ const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(fireDir, {
37
+ model: opts.model,
38
+ authPath: resolveAuthPathOverride(opts.authPath),
39
+ }).catch(failStartup);
40
+ console.error(`[fastagent] fire: ${name} (${modelSpec})`);
41
+ await reportAuth(modelSpec, authPath);
42
+ const exitCode = await runInvokeStream(agent.invoke({ session: scheduleSession(name) }, { text: schedule.prompt }), (text) => process.stdout.write(text), (line) => console.error(line));
43
+ process.stdout.write("\n");
44
+ process.exit(exitCode);
45
+ }
@@ -0,0 +1,7 @@
1
+ export interface InfoOptions {
2
+ json?: boolean;
3
+ model?: string;
4
+ authPath?: string;
5
+ sessionsDir?: string;
6
+ }
7
+ export declare function runInfo(dirArg: string, opts: InfoOptions): Promise<void>;
@@ -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 { defaultAuthPath, defaultSessionsDir, loadConfig, resolveAgentDir, resolveAuthPathOverride, 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 = resolveAuthPathOverride(opts.authPath) ?? defaultAuthPath(stateRoot);
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,29 @@
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 { resolveAuthPathOverride } from "../../engines/pi/config.js";
6
+ import { createPiAgentFromWorkspace } from "../../engines/pi/workspace.js";
7
+ import { runInvokeStream } from "../../invoke-stream.js";
8
+ import { installProxyFetch } from "../../proxy.js";
9
+ import { failStartup } from "../fail.js";
10
+ import { reportAuth, resolveFirstRunModel } from "../shared.js";
11
+ export async function runInvoke(message, dirArg, opts) {
12
+ const invokeDir = resolve(dirArg);
13
+ loadDotEnv(invokeDir);
14
+ installProxyFetch();
15
+ await resolveFirstRunModel(invokeDir, opts);
16
+ const { agent, modelSpec, authPath } = await createPiAgentFromWorkspace(invokeDir, {
17
+ model: opts.model,
18
+ authPath: resolveAuthPathOverride(opts.authPath),
19
+ }).catch(failStartup);
20
+ console.error(`[fastagent] invoke: ${invokeDir} (${modelSpec})`);
21
+ await reportAuth(modelSpec, authPath);
22
+ // Fresh session per invoke (one-shot, no resume). runInvokeStream maps events→IO: reply→stdout,
23
+ // tool/failure→stderr, exit 1 iff the turn failed (so CI can gate on it).
24
+ const exitCode = await runInvokeStream(agent.invoke({ session: randomUUID() }, { text: message }), (text) => process.stdout.write(text), (line) => console.error(line));
25
+ process.stdout.write("\n");
26
+ // Always exit explicitly: the undici proxy agent's keep-alive sockets would otherwise hold the
27
+ // event loop open after a successful one-shot turn.
28
+ process.exit(exitCode);
29
+ }
@@ -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,63 @@
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 { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
12
+ import { loadDotEnv } from "../../env.js";
13
+ import { defaultAuthPath, resolveAuthPathOverride, resolveStateRoot } from "../../engines/pi/config.js";
14
+ import { ensureStateRootSelfIgnored, isUnderDir } from "../../engines/pi/definition.js";
15
+ import { loginFlow } from "../../engines/pi/login.js";
16
+ import { openExternalUrl } from "../../open-url.js";
17
+ import { installProxyFetch } from "../../proxy.js";
18
+ import { failStartup } from "../fail.js";
19
+ import { isInteractive } from "../shared.js";
20
+ export async function runLogin(provider, opts) {
21
+ const loginDir = process.cwd();
22
+ loadDotEnv(loginDir); // FASTAGENT_AUTH_PATH / a proxy (HTTPS_PROXY) may be configured in the project .env
23
+ installProxyFetch(); // the OAuth token exchange must go through HTTPS_PROXY (region-locked providers)
24
+ const stateRoot = resolveStateRoot(loginDir);
25
+ const authPath = resolveAuthPathOverride(opts.authPath) ?? defaultAuthPath(stateRoot);
26
+ // login is the command that CREATES the credential file, so the leak guard binds HERE too (not only
27
+ // in the opener): on an adapted project dir, a `login` before the first dev/start would otherwise
28
+ // leave the secret untracked-but-committable. Unlike the opener (which populates the WHOLE root, so
29
+ // it always self-ignores an in-tree root), login writes ONLY auth.json — so guard iff the credential
30
+ // actually lands under the in-tree root. An external `--auth-path`/`FASTAGENT_AUTH_PATH` writes
31
+ // nothing in-tree (don't create an empty `.fastagent`); the guard also skips the HOME-global root.
32
+ if (isUnderDir(authPath, stateRoot))
33
+ await ensureStateRootSelfIgnored(loginDir, stateRoot);
34
+ // login is inherently interactive — loginFlow renders provider/method menus and opens a browser (or
35
+ // prompts for a key). In a non-TTY (a pipe, CI, a coding-agent shell) the menu can't receive keystrokes
36
+ // and would hang; --no-input asks for the same posture explicitly. Fail fast with the reason instead
37
+ // of stalling on an unanswerable prompt. (After the secret-hygiene self-ignore above, which is cheap
38
+ // prep, so a later terminal login is safe.)
39
+ if (opts.input === false || !isInteractive()) {
40
+ failStartup(new Error(`login is interactive (it shows a menu and opens a browser) — run it in a terminal, not a pipe/CI`));
41
+ }
42
+ const io = terminalLoginIO();
43
+ const result = await loginFlow(io, { provider, authPath }).catch(failStartup);
44
+ console.error(`[fastagent] logged in to ${result.provider} (${result.method}) — saved to ${authPath}`);
45
+ process.exit(0); // the undici proxy agent's keep-alive sockets would otherwise hold the event loop open
46
+ }
47
+ /** Login terminal IO via @clack/prompts: a searchable list once long, a hidden prompt for keys. */
48
+ function terminalLoginIO() {
49
+ return {
50
+ async select(message, options) {
51
+ const r = await (options.length > 7 ? autocomplete : select)({ message, options });
52
+ return isCancel(r) ? undefined : r;
53
+ },
54
+ async prompt(message, opts) {
55
+ const r = opts?.hidden
56
+ ? await password({ message, signal: opts.signal })
57
+ : await clackText({ message, signal: opts?.signal });
58
+ return isCancel(r) ? undefined : r;
59
+ },
60
+ note: (message) => clackLog.info(message),
61
+ openUrl: openExternalUrl,
62
+ };
63
+ }
@@ -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 "../../cli-models.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,90 @@
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 { defaultAuthPath, resolveAuthPathOverride, resolveSessionsDirOverride, resolveStateRoot, } 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, 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
+ const authPathOverride = resolveAuthPathOverride(opts.authPath);
30
+ await maybeSeedAuth(authPathOverride ?? defaultAuthPath(resolveStateRoot(dir)));
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, } = await createPiAgentFromWorkspace(dir, {
34
+ model: opts.model,
35
+ sessionsDir: sessionsDirOverride,
36
+ authPath: authPathOverride,
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 routes = await routesFor(agentDir, traced, stateRoot).catch(failStartup);
70
+ await startSchedules(agentDir, traced, stateRoot, config.selfSchedule ?? false);
71
+ serve(routes, portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, (p) => maybeTunnel(agentDir, p, opts.tunnel ?? false));
72
+ // No graceful drain: webhook turns run fire-and-forget; SIGTERM just exits mid-turn. Whether an
73
+ // in-flight turn is LOST depends on the channel: the Telegram channel persists turn intent pre-ACK
74
+ // and replays it next start (turn-store.ts, L1 durable execution, at-least-once); HTTP and other
75
+ // channels have no such layer, so their in-flight turns are still lost (the asker re-invokes).
76
+ }
77
+ /**
78
+ * Materialize `FASTAGENT_AUTH_SEED` (base64 of an auth.json, set by `deploy --run`) onto the
79
+ * writable state root ONCE — only when the seed is set AND the auth file is absent, so a refreshed
80
+ * volume copy is never clobbered by the stale seed. Lets a deploy carry the operator's local
81
+ * OAuth/API credential so the box runs on the SAME subscription. No-op locally (the seed is unset).
82
+ */
83
+ async function maybeSeedAuth(authPath) {
84
+ const bytes = authSeedBytes(process.env.FASTAGENT_AUTH_SEED, await exists(authPath));
85
+ if (!bytes)
86
+ return;
87
+ await mkdir(dirname(authPath), { recursive: true });
88
+ await writeFile(authPath, bytes);
89
+ log.info(`[fastagent] seeded ${authPath} from FASTAGENT_AUTH_SEED (first boot)`);
90
+ }
@@ -0,0 +1 @@
1
+ export declare function runTool(name: string, argsJson: string, dirArg: string): Promise<void>;
@@ -0,0 +1,37 @@
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 { failStartup, failUsage } from "../fail.js";
8
+ export async function runTool(name, argsJson, dirArg) {
9
+ const toolDir = resolve(dirArg);
10
+ loadDotEnv(toolDir); // a tool may read a key from .env
11
+ const { config } = await loadConfig(toolDir).catch(failStartup);
12
+ // The same tool set dev/start mount (defaults + config.tools + discovered, deduped), so the runner
13
+ // exercises exactly what gets served — a shadowed tool is surfaced, not silently run. Resolve agentDir
14
+ // like the openers so `fastagent tool` finds the SAME tools/ as dev/start when config.agentDir is set.
15
+ const agentDir = resolveAgentDir(toolDir, config);
16
+ const { tools, toolCollisions, toolFailures } = await resolveWorkspaceTools(config, agentDir, toolDir).catch(failStartup);
17
+ for (const c of toolCollisions) {
18
+ console.error(`[fastagent] warn: tool "${c.name}" (${c.source}) is shadowed by a default/config tool — not mounted`);
19
+ }
20
+ reportModuleLoadFailures(toolFailures);
21
+ const tool = tools.find((t) => t.name === name);
22
+ if (!tool) {
23
+ failStartup(new Error(`unknown tool "${name}". available: ${tools.map((t) => t.name).join(", ") || "(none)"}`));
24
+ }
25
+ let args;
26
+ try {
27
+ args = JSON.parse(argsJson);
28
+ }
29
+ catch {
30
+ failUsage(`invalid JSON args: ${argsJson}`); // malformed input syntax = usage error, exit 2
31
+ }
32
+ const result = await tool.execute(`cli-${name}`, args).catch(failStartup);
33
+ const out = result?.details !== undefined
34
+ ? result.details
35
+ : (result?.content ?? []).map((c) => ("text" in c ? c.text : "")).join("");
36
+ console.log(typeof out === "string" ? out : JSON.stringify(out, null, 2));
37
+ }