@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.
- package/README.md +6 -4
- package/dist/channels/feishu/bootstrap-token.d.ts +42 -0
- package/dist/channels/feishu/bootstrap-token.js +94 -0
- package/dist/channels/feishu/card.d.ts +32 -0
- package/dist/channels/feishu/card.js +66 -0
- package/dist/channels/feishu/cloud.d.ts +17 -0
- package/dist/channels/feishu/cloud.js +19 -0
- package/dist/channels/feishu/context-buffer.d.ts +46 -0
- package/dist/channels/feishu/context-buffer.js +133 -0
- package/dist/channels/feishu/crypto.d.ts +15 -0
- package/dist/channels/feishu/crypto.js +43 -0
- package/dist/channels/feishu/feishu-api.d.ts +108 -0
- package/dist/channels/feishu/feishu-api.js +325 -0
- package/dist/channels/feishu/feishu.d.ts +46 -0
- package/dist/channels/feishu/feishu.js +472 -0
- package/dist/channels/feishu/invoke-turn.d.ts +65 -0
- package/dist/channels/feishu/invoke-turn.js +157 -0
- package/dist/channels/feishu/model.d.ts +97 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +22 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +11 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +43 -0
- package/dist/channels/feishu/parse.js +65 -0
- package/dist/channels/feishu/preview.d.ts +36 -0
- package/dist/channels/feishu/preview.js +387 -0
- package/dist/channels/feishu/register-app.d.ts +70 -0
- package/dist/channels/feishu/register-app.js +141 -0
- package/dist/channels/feishu/register-webhook.d.ts +22 -0
- package/dist/channels/feishu/register-webhook.js +106 -0
- package/dist/channels/feishu/scaffold/channel.ts +41 -0
- package/dist/channels/feishu/scaffold/feishu-send.ts +87 -0
- package/dist/channels/feishu/seen.d.ts +5 -0
- package/dist/channels/feishu/seen.js +47 -0
- package/dist/channels/feishu/text.d.ts +13 -0
- package/dist/channels/feishu/text.js +63 -0
- package/dist/channels/lark/lark.d.ts +15 -0
- package/dist/channels/lark/lark.js +10 -0
- package/dist/channels/lark/onboard.d.ts +39 -0
- package/dist/channels/lark/onboard.js +58 -0
- package/dist/channels/lark/scaffold/channel.ts +39 -0
- package/dist/channels/lark/scaffold/lark-send.ts +87 -0
- package/dist/channels/registration.d.ts +15 -0
- package/dist/channels/registration.js +1 -0
- package/dist/channels/{telegram/state.js → state.js} +6 -4
- package/dist/channels/telegram/context-buffer.js +1 -1
- package/dist/channels/telegram/register-webhook.d.ts +4 -1
- package/dist/channels/telegram/register-webhook.js +17 -26
- package/dist/channels/telegram/telegram.js +2 -2
- package/dist/channels/telegram/turn-store.d.ts +8 -21
- package/dist/channels/telegram/turn-store.js +11 -130
- package/dist/channels/{telegram/turn-queue.js → turn-queue.js} +3 -3
- package/dist/channels/turn-store.d.ts +42 -0
- package/dist/channels/turn-store.js +139 -0
- package/dist/channels/wait-health.d.ts +6 -0
- package/dist/channels/wait-health.js +27 -0
- package/dist/cli/commands/add.d.ts +9 -0
- package/dist/cli/commands/add.js +142 -0
- package/dist/cli/commands/chat.d.ts +3 -0
- package/dist/cli/commands/chat.js +16 -0
- package/dist/cli/commands/deploy.d.ts +13 -0
- package/dist/cli/commands/deploy.js +338 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +76 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +29 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +63 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +90 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +37 -0
- package/dist/cli/fail.d.ts +19 -0
- package/dist/cli/fail.js +32 -0
- package/dist/cli/kernel.d.ts +89 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +421 -0
- package/dist/cli/serve.d.ts +28 -0
- package/dist/cli/serve.js +90 -0
- package/dist/cli/shared.d.ts +24 -0
- package/dist/cli/shared.js +116 -0
- package/dist/cli-add-feishu.d.ts +8 -0
- package/dist/cli-add-feishu.js +223 -0
- package/dist/cli.js +8 -1284
- package/dist/deploy/container.js +10 -6
- package/dist/deploy/docker/plan.d.ts +45 -0
- package/dist/deploy/docker/plan.js +139 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- package/dist/deploy/fly/plan.d.ts +1 -1
- package/dist/deploy/fly/plan.js +15 -4
- package/dist/deploy/fly/run.d.ts +7 -4
- package/dist/deploy/fly/run.js +26 -5
- package/dist/deploy/preflight.js +4 -3
- package/dist/deploy/railway/plan.d.ts +1 -1
- package/dist/deploy/railway/plan.js +17 -5
- package/dist/deploy/railway/run.d.ts +6 -3
- package/dist/deploy/railway/run.js +26 -4
- package/dist/deploy/registration-gate.d.ts +20 -0
- package/dist/deploy/registration-gate.js +20 -0
- package/dist/deploy/runner.d.ts +4 -1
- package/dist/deploy/runner.js +1 -0
- package/dist/deploy/secrets.d.ts +10 -9
- package/dist/deploy/secrets.js +15 -14
- package/dist/dev-supervisor.js +2 -1
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/chat.js +80 -7
- package/dist/engines/pi/config.d.ts +12 -3
- package/dist/engines/pi/config.js +16 -1
- package/dist/engines/pi/create.d.ts +14 -6
- package/dist/engines/pi/create.js +52 -16
- package/dist/engines/pi/harness.d.ts +16 -1
- package/dist/engines/pi/harness.js +77 -1
- package/dist/engines/pi/invoke.d.ts +1 -1
- package/dist/engines/pi/invoke.js +37 -2
- package/dist/engines/pi/login.js +1 -1
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/tool-context.d.ts +28 -0
- package/dist/engines/pi/tool-context.js +8 -0
- package/dist/engines/pi/tool.d.ts +32 -1
- package/dist/engines/pi/tool.js +42 -1
- package/dist/engines/pi/workspace.d.ts +4 -1
- package/dist/engines/pi/workspace.js +3 -1
- package/dist/feishu.d.ts +2 -0
- package/dist/feishu.js +2 -0
- package/dist/lark.d.ts +3 -0
- package/dist/lark.js +3 -0
- package/dist/open-url.d.ts +2 -0
- package/dist/open-url.js +6 -0
- package/dist/pi.d.ts +2 -1
- package/dist/scaffold/add-channel.d.ts +9 -5
- package/dist/scaffold/add-channel.js +75 -7
- package/dist/scaffold/templates/fastagent.config.mjs +1 -0
- package/dist/tunnel.d.ts +9 -6
- package/dist/tunnel.js +48 -31
- package/package.json +18 -4
- /package/dist/channels/{telegram/state.d.ts → state.d.ts} +0 -0
- /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,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,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,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,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
|
+
}
|