@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.
Files changed (90) hide show
  1. package/README.md +2 -2
  2. package/dist/channels/feishu/context-buffer.d.ts +46 -0
  3. package/dist/channels/feishu/context-buffer.js +133 -0
  4. package/dist/channels/feishu/crypto.d.ts +4 -2
  5. package/dist/channels/feishu/crypto.js +4 -2
  6. package/dist/channels/feishu/feishu-api.d.ts +4 -4
  7. package/dist/channels/feishu/feishu-api.js +2 -2
  8. package/dist/channels/feishu/feishu.d.ts +11 -1
  9. package/dist/channels/feishu/feishu.js +143 -30
  10. package/dist/channels/feishu/invoke-turn.d.ts +19 -13
  11. package/dist/channels/feishu/invoke-turn.js +70 -19
  12. package/dist/channels/feishu/model.d.ts +97 -0
  13. package/dist/channels/feishu/model.js +9 -0
  14. package/dist/channels/feishu/normalize.d.ts +22 -0
  15. package/dist/channels/feishu/normalize.js +132 -0
  16. package/dist/channels/feishu/owned-threads.d.ts +11 -0
  17. package/dist/channels/feishu/owned-threads.js +47 -0
  18. package/dist/channels/feishu/parse.d.ts +20 -102
  19. package/dist/channels/feishu/parse.js +35 -145
  20. package/dist/channels/feishu/preview.js +2 -2
  21. package/dist/channels/feishu/scaffold/channel.ts +10 -3
  22. package/dist/channels/feishu/seen.js +12 -12
  23. package/dist/channels/lark/scaffold/channel.ts +10 -3
  24. package/dist/cli/commands/add.d.ts +9 -0
  25. package/dist/cli/commands/add.js +142 -0
  26. package/dist/cli/commands/chat.d.ts +3 -0
  27. package/dist/cli/commands/chat.js +16 -0
  28. package/dist/cli/commands/deploy.d.ts +13 -0
  29. package/dist/cli/commands/deploy.js +338 -0
  30. package/dist/cli/commands/dev.d.ts +11 -0
  31. package/dist/cli/commands/dev.js +76 -0
  32. package/dist/cli/commands/fire.d.ts +7 -0
  33. package/dist/cli/commands/fire.js +45 -0
  34. package/dist/cli/commands/info.d.ts +7 -0
  35. package/dist/cli/commands/info.js +108 -0
  36. package/dist/cli/commands/init.d.ts +8 -0
  37. package/dist/cli/commands/init.js +81 -0
  38. package/dist/cli/commands/invoke.d.ts +7 -0
  39. package/dist/cli/commands/invoke.js +29 -0
  40. package/dist/cli/commands/login.d.ts +6 -0
  41. package/dist/cli/commands/login.js +63 -0
  42. package/dist/cli/commands/models.d.ts +1 -0
  43. package/dist/cli/commands/models.js +15 -0
  44. package/dist/cli/commands/schedule.d.ts +12 -0
  45. package/dist/cli/commands/schedule.js +89 -0
  46. package/dist/cli/commands/start.d.ts +10 -0
  47. package/dist/cli/commands/start.js +90 -0
  48. package/dist/cli/commands/tool.d.ts +1 -0
  49. package/dist/cli/commands/tool.js +37 -0
  50. package/dist/cli/fail.d.ts +19 -0
  51. package/dist/cli/fail.js +32 -0
  52. package/dist/cli/kernel.d.ts +89 -0
  53. package/dist/cli/kernel.js +190 -0
  54. package/dist/cli/program.d.ts +11 -0
  55. package/dist/cli/program.js +421 -0
  56. package/dist/cli/serve.d.ts +28 -0
  57. package/dist/cli/serve.js +90 -0
  58. package/dist/cli/shared.d.ts +24 -0
  59. package/dist/cli/shared.js +116 -0
  60. package/dist/cli.js +8 -1329
  61. package/dist/deploy/docker/plan.d.ts +45 -0
  62. package/dist/deploy/docker/plan.js +139 -0
  63. package/dist/deploy/docker/run.d.ts +40 -0
  64. package/dist/deploy/docker/run.js +126 -0
  65. package/dist/deploy/preflight.js +4 -3
  66. package/dist/deploy/runner.d.ts +4 -1
  67. package/dist/deploy/runner.js +1 -0
  68. package/dist/engines/pi/auth.js +160 -46
  69. package/dist/engines/pi/chat.js +77 -4
  70. package/dist/engines/pi/config.d.ts +12 -3
  71. package/dist/engines/pi/config.js +16 -1
  72. package/dist/engines/pi/create.d.ts +14 -5
  73. package/dist/engines/pi/create.js +44 -9
  74. package/dist/engines/pi/harness.d.ts +16 -1
  75. package/dist/engines/pi/harness.js +77 -1
  76. package/dist/engines/pi/invoke.d.ts +1 -1
  77. package/dist/engines/pi/invoke.js +37 -2
  78. package/dist/engines/pi/login.js +1 -1
  79. package/dist/engines/pi/search-tools.d.ts +10 -0
  80. package/dist/engines/pi/search-tools.js +138 -0
  81. package/dist/engines/pi/tool-context.d.ts +28 -0
  82. package/dist/engines/pi/tool-context.js +8 -0
  83. package/dist/engines/pi/tool.d.ts +32 -1
  84. package/dist/engines/pi/tool.js +42 -1
  85. package/dist/engines/pi/workspace.d.ts +4 -1
  86. package/dist/engines/pi/workspace.js +3 -1
  87. package/dist/pi.d.ts +2 -1
  88. package/dist/scaffold/add-channel.js +3 -1
  89. package/dist/scaffold/templates/fastagent.config.mjs +1 -0
  90. 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
+ }