@fastagent-sh/fastagent 0.13.0 → 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 +2 -2
- 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 +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +4 -4
- package/dist/channels/feishu/feishu-api.js +2 -2
- package/dist/channels/feishu/feishu.d.ts +11 -1
- package/dist/channels/feishu/feishu.js +143 -30
- package/dist/channels/feishu/invoke-turn.d.ts +19 -13
- package/dist/channels/feishu/invoke-turn.js +70 -19
- 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 +20 -102
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.js +2 -2
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- package/dist/channels/feishu/seen.js +12 -12
- package/dist/channels/lark/scaffold/channel.ts +10 -3
- 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.js +8 -1329
- 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/preflight.js +4 -3
- package/dist/deploy/runner.d.ts +4 -1
- package/dist/deploy/runner.js +1 -0
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/chat.js +77 -4
- 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 -5
- package/dist/engines/pi/create.js +44 -9
- 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/pi.d.ts +2 -1
- package/dist/scaffold/add-channel.js +3 -1
- package/dist/scaffold/templates/fastagent.config.mjs +1 -0
- package/package.json +7 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createInvokeHandler } from "../channels/http.js";
|
|
2
|
+
import { text } from "../channels/respond.js";
|
|
3
|
+
import { loadChannels } from "../engines/pi/channel.js";
|
|
4
|
+
import { reportModuleLoadFailures } from "../engines/pi/report.js";
|
|
5
|
+
import { parseRouteKey, router, serveNode } from "../host/node.js";
|
|
6
|
+
import { log } from "../log.js";
|
|
7
|
+
import { openExternalUrl } from "../open-url.js";
|
|
8
|
+
import { loadSchedules } from "../schedule/discover.js";
|
|
9
|
+
import { createScheduler } from "../schedule/scheduler.js";
|
|
10
|
+
import { announceWebhooks, startCloudflareTunnel } from "../tunnel.js";
|
|
11
|
+
import { failStartup } from "./fail.js";
|
|
12
|
+
/**
|
|
13
|
+
* The routes this deployment serves: a default `GET /health` plus the workspace's discovered
|
|
14
|
+
* `channels/` — or the default invoke channel at POST /invoke when none are declared.
|
|
15
|
+
*/
|
|
16
|
+
export async function routesFor(workspaceDir, agent, stateRoot) {
|
|
17
|
+
const { routes, collisions, failures } = await loadChannels(workspaceDir, { agent, stateRoot });
|
|
18
|
+
for (const c of collisions) {
|
|
19
|
+
console.error(`[fastagent] warn: channel route "${c.route}" (${c.source}) collides with an earlier channel — not mounted`);
|
|
20
|
+
}
|
|
21
|
+
reportModuleLoadFailures(failures);
|
|
22
|
+
if (failures.length > 0 || collisions.length > 0) {
|
|
23
|
+
throw new Error(`channel setup is invalid (${failures.length} load failure(s), ${collisions.length} route collision(s)) — ` +
|
|
24
|
+
`fix it, or rename an intentionally disabled file to *.disabled`);
|
|
25
|
+
}
|
|
26
|
+
const channels = Object.keys(routes).length > 0 ? routes : { "POST /invoke": createInvokeHandler(agent) };
|
|
27
|
+
// Add a default GET /health unless a channel already covers it (overlap, not exact-key: an
|
|
28
|
+
// any-method `/health` also handles GET, so the built-in steps aside).
|
|
29
|
+
const healthCovered = Object.keys(channels).some((k) => {
|
|
30
|
+
const e = parseRouteKey(k);
|
|
31
|
+
return e.path === "/health" && (e.method === undefined || e.method === "GET");
|
|
32
|
+
});
|
|
33
|
+
return healthCovered ? channels : { "GET /health": () => text("ok\n", 200), ...channels };
|
|
34
|
+
}
|
|
35
|
+
/** Serve `routes` via the Node host. serveNode owns binding; the CLI owns policy (errors, ready signal, log). */
|
|
36
|
+
export function serve(routes, port, onListening) {
|
|
37
|
+
serveNode(router(routes), { port }).listening.then((boundPort) => {
|
|
38
|
+
process.send?.({ type: "ready", port: boundPort }); // tell the dev supervisor we bound + on which port
|
|
39
|
+
log.info(`[fastagent] http channel on :${boundPort}`);
|
|
40
|
+
log.info(`[fastagent] routes: ${Object.keys(routes).join(", ") || "(none)"}`);
|
|
41
|
+
onListening?.(boundPort);
|
|
42
|
+
}, (error) => {
|
|
43
|
+
if (error.code === "EADDRINUSE")
|
|
44
|
+
failStartup(new Error(`port ${port} is already in use; choose another with --port`));
|
|
45
|
+
failStartup(new Error(`cannot bind http channel on :${port}: ${error.message}`));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Start a Cloudflare tunnel + announce/register webhooks once the server is bound — unless this is a
|
|
50
|
+
* watch-supervisor worker, where the supervisor owns the long-lived tunnel so the public URL survives
|
|
51
|
+
* reloads.
|
|
52
|
+
*/
|
|
53
|
+
export function maybeTunnel(workspaceDir, boundPort, tunnel) {
|
|
54
|
+
if (!tunnel || process.env.FASTAGENT_DEV_WORKER === "1")
|
|
55
|
+
return;
|
|
56
|
+
void startCloudflareTunnel(boundPort).then((t) => {
|
|
57
|
+
if (!t)
|
|
58
|
+
return;
|
|
59
|
+
void announceWebhooks(workspaceDir, t.url, { openUrl: openExternalUrl });
|
|
60
|
+
// Single-process (start / --no-watch): close the tunnel on exit (watch mode's supervisor owns its own).
|
|
61
|
+
const cleanup = () => {
|
|
62
|
+
t.close();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
};
|
|
65
|
+
process.once("SIGINT", cleanup);
|
|
66
|
+
process.once("SIGTERM", cleanup);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Load and start the workspace's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
|
|
71
|
+
* there are static schedules OR `selfSchedule` is on (the scheduler also polls the agent's self-scheduled
|
|
72
|
+
* wake-ups, which the built-in `wake` tool creates only when opted in). Shares the SAME (trace-wrapped)
|
|
73
|
+
* agent the routes serve, so a scheduled turn is observed like any other. Best-effort stop on exit; dev's
|
|
74
|
+
* watch restart re-reads schedules with the worker (schedules are a code input). Single-process.
|
|
75
|
+
*/
|
|
76
|
+
export async function startSchedules(workspaceDir, agent, stateRoot, selfSchedule) {
|
|
77
|
+
const { schedules, failures } = await loadSchedules(workspaceDir).catch(failStartup);
|
|
78
|
+
reportModuleLoadFailures(failures);
|
|
79
|
+
// Nothing to run when there are neither static `schedules/` nor self-scheduling (the `wake` tool, and
|
|
80
|
+
// thus any wake-up to poll, is mounted only when config.selfSchedule is on) — skip the poller entirely.
|
|
81
|
+
if (schedules.length === 0 && !selfSchedule)
|
|
82
|
+
return;
|
|
83
|
+
const scheduler = createScheduler({ agent, stateRoot, schedules });
|
|
84
|
+
scheduler.start();
|
|
85
|
+
if (schedules.length > 0)
|
|
86
|
+
log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}`);
|
|
87
|
+
const stop = () => scheduler.stop();
|
|
88
|
+
process.once("SIGINT", stop);
|
|
89
|
+
process.once("SIGTERM", stop);
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
|
|
2
|
+
export declare function isInteractive(): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Parse + range-check a port string (CLI flag or env). Empty/whitespace is "not set" → undefined, so
|
|
5
|
+
* the `??` chain falls through instead of binding port 0 (`Number("")` is 0). The exit code follows
|
|
6
|
+
* RESPONSIBILITY, not the layer that discovers the problem: a bad `--port` is a usage error (2), a
|
|
7
|
+
* bad `PORT` env is broken runtime configuration (1).
|
|
8
|
+
*/
|
|
9
|
+
export declare function parsePort(value: string | undefined, source: string, from: "flag" | "env"): number | undefined;
|
|
10
|
+
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
|
|
11
|
+
export declare function reportAuth(modelSpec: string, authPath: string): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* First-run model resolution for the serving commands. When no model is set (flag/env/config), and
|
|
14
|
+
* we're on a TTY, pick one from the providers the user is logged into and persist the choice. A no-op
|
|
15
|
+
* when a model is already set; on a non-TTY (CI/deploy), or with `--no-input`, or with nothing
|
|
16
|
+
* configured it stays silent and lets the opener raise its clear "missing model" error. The pick is
|
|
17
|
+
* exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it, and best-effort written back to
|
|
18
|
+
* the config so the next run is quiet.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveFirstRunModel(workspaceDir: string, options?: {
|
|
21
|
+
model?: string;
|
|
22
|
+
authPath?: string;
|
|
23
|
+
input?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared across command modules: interactivity gates, port parsing, the startup auth report,
|
|
3
|
+
* and first-run model resolution. Bodies moved verbatim from cli.ts; the module-scoped flag access
|
|
4
|
+
* (`values.*`) became parameters.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { relative } from "node:path";
|
|
8
|
+
import { autocomplete, isCancel, select } from "@clack/prompts";
|
|
9
|
+
import { fastagentCredentialStore } from "../engines/pi/auth.js";
|
|
10
|
+
import { isValidPort, loadConfig, resolveAuthPath, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
|
|
11
|
+
import { configuredModelSpecs, createPiModels, probeAuthSource } from "../engines/pi/models.js";
|
|
12
|
+
import { formatAuthReport } from "../cli-auth.js";
|
|
13
|
+
import { log } from "../log.js";
|
|
14
|
+
import { failStartup, failUsage } from "./fail.js";
|
|
15
|
+
/** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
|
|
16
|
+
export function isInteractive() {
|
|
17
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse + range-check a port string (CLI flag or env). Empty/whitespace is "not set" → undefined, so
|
|
21
|
+
* the `??` chain falls through instead of binding port 0 (`Number("")` is 0). The exit code follows
|
|
22
|
+
* RESPONSIBILITY, not the layer that discovers the problem: a bad `--port` is a usage error (2), a
|
|
23
|
+
* bad `PORT` env is broken runtime configuration (1).
|
|
24
|
+
*/
|
|
25
|
+
export function parsePort(value, source, from) {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (trimmed === "")
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!/^\d+$/.test(trimmed) || !isValidPort(Number(trimmed))) {
|
|
32
|
+
const message = `invalid ${source} "${value}": must be an integer 0-65535`;
|
|
33
|
+
if (from === "flag")
|
|
34
|
+
failUsage(message);
|
|
35
|
+
failStartup(new Error(message));
|
|
36
|
+
}
|
|
37
|
+
return Number(trimmed);
|
|
38
|
+
}
|
|
39
|
+
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
|
|
40
|
+
export async function reportAuth(modelSpec, authPath) {
|
|
41
|
+
const provider = modelSpec.slice(0, modelSpec.indexOf("/"));
|
|
42
|
+
const source = await probeAuthSource(createPiModels({ authPath }), modelSpec);
|
|
43
|
+
// Only when nothing satisfies auth do we read the store (refresh-FREE) to tell "nothing stored" from
|
|
44
|
+
// "stored but unusable" — see formatAuthReport for why. store.read warns on a corrupt file itself.
|
|
45
|
+
const stored = source === undefined
|
|
46
|
+
? await fastagentCredentialStore(authPath)
|
|
47
|
+
.read(provider)
|
|
48
|
+
.catch(() => undefined)
|
|
49
|
+
: undefined;
|
|
50
|
+
const report = formatAuthReport(provider, authPath, source, stored);
|
|
51
|
+
log.info(`[fastagent] ${report.line}`);
|
|
52
|
+
if (report.warn)
|
|
53
|
+
log.warn(`[fastagent] ${report.warn}`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* First-run model resolution for the serving commands. When no model is set (flag/env/config), and
|
|
57
|
+
* we're on a TTY, pick one from the providers the user is logged into and persist the choice. A no-op
|
|
58
|
+
* when a model is already set; on a non-TTY (CI/deploy), or with `--no-input`, or with nothing
|
|
59
|
+
* configured it stays silent and lets the opener raise its clear "missing model" error. The pick is
|
|
60
|
+
* exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it, and best-effort written back to
|
|
61
|
+
* the config so the next run is quiet.
|
|
62
|
+
*/
|
|
63
|
+
export async function resolveFirstRunModel(workspaceDir, options = {}) {
|
|
64
|
+
const { config, path: configPath } = await loadConfig(workspaceDir).catch(failStartup);
|
|
65
|
+
if (resolveModelSpec(options.model, config))
|
|
66
|
+
return; // already set (flag > FASTAGENT_MODEL > config)
|
|
67
|
+
if (options.input === false)
|
|
68
|
+
return; // --no-input: never prompt (clig) — the opener raises the clear error
|
|
69
|
+
if (!isInteractive())
|
|
70
|
+
return; // CI/deploy: the opener throws the actionable missing-model error
|
|
71
|
+
const authPath = resolveAuthPath(workspaceDir, options.authPath);
|
|
72
|
+
let specs;
|
|
73
|
+
try {
|
|
74
|
+
specs = await configuredModelSpecs(createPiModels({ authPath }));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Enumerating providers/auth threw — a system fault (a corrupt auth store, a throwing provider),
|
|
78
|
+
// NOT "not logged in". Surface it instead of masking it as the login hint; the opener then still
|
|
79
|
+
// raises the clear missing-model error.
|
|
80
|
+
log.warn(`[fastagent] could not list configured models: ${error.message}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (specs.length === 0) {
|
|
84
|
+
log.warn(`[fastagent] no model set and no authenticated provider — run \`fastagent login\`, then \`fastagent dev\``);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const r = await (specs.length > 7 ? autocomplete : select)({
|
|
88
|
+
message: "Choose a model for this agent",
|
|
89
|
+
options: specs.map((s) => ({ value: s, label: s })),
|
|
90
|
+
});
|
|
91
|
+
if (isCancel(r))
|
|
92
|
+
return; // cancelled: let the opener report the missing model
|
|
93
|
+
const chosen = r;
|
|
94
|
+
process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
|
|
95
|
+
await persistModelChoice(workspaceDir, configPath, chosen);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Best-effort persist the picked model so the next run does not prompt. Only rewrites the commented
|
|
99
|
+
* `model:` placeholder the scaffold writes (or an existing `model:` line); anything else (zero-config,
|
|
100
|
+
* a hand-shaped config) is left untouched with a printed hint. Never throws — persistence is a convenience.
|
|
101
|
+
*/
|
|
102
|
+
async function persistModelChoice(workspaceDir, configPath, spec) {
|
|
103
|
+
const hint = () => console.error(`[fastagent] using ${spec} for this run; set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
|
|
104
|
+
if (!configPath)
|
|
105
|
+
return hint();
|
|
106
|
+
try {
|
|
107
|
+
const replaced = rewriteConfigModel(await readFile(configPath, "utf8"), spec);
|
|
108
|
+
if (!replaced)
|
|
109
|
+
return hint();
|
|
110
|
+
await writeFile(configPath, replaced);
|
|
111
|
+
console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(workspaceDir, configPath)}`);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
hint();
|
|
115
|
+
}
|
|
116
|
+
}
|