@fastagent-sh/fastagent 0.12.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/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/agent.d.ts +72 -0
- package/dist/agent.js +14 -0
- package/dist/channels/body.d.ts +9 -0
- package/dist/channels/body.js +29 -0
- package/dist/channels/github/github.d.ts +31 -0
- package/dist/channels/github/github.js +80 -0
- package/dist/channels/github/scaffold/channel.ts +25 -0
- package/dist/channels/http.d.ts +13 -0
- package/dist/channels/http.js +161 -0
- package/dist/channels/respond.d.ts +5 -0
- package/dist/channels/respond.js +3 -0
- package/dist/channels/telegram/context-buffer.d.ts +59 -0
- package/dist/channels/telegram/context-buffer.js +139 -0
- package/dist/channels/telegram/invoke-turn.d.ts +53 -0
- package/dist/channels/telegram/invoke-turn.js +127 -0
- package/dist/channels/telegram/parse.d.ts +160 -0
- package/dist/channels/telegram/parse.js +153 -0
- package/dist/channels/telegram/preview.d.ts +22 -0
- package/dist/channels/telegram/preview.js +266 -0
- package/dist/channels/telegram/register-webhook.d.ts +13 -0
- package/dist/channels/telegram/register-webhook.js +73 -0
- package/dist/channels/telegram/scaffold/channel.ts +25 -0
- package/dist/channels/telegram/scaffold/telegram-send.ts +107 -0
- package/dist/channels/telegram/state.d.ts +9 -0
- package/dist/channels/telegram/state.js +58 -0
- package/dist/channels/telegram/telegram-api.d.ts +77 -0
- package/dist/channels/telegram/telegram-api.js +345 -0
- package/dist/channels/telegram/telegram.d.ts +35 -0
- package/dist/channels/telegram/telegram.js +352 -0
- package/dist/channels/telegram/turn-queue.d.ts +21 -0
- package/dist/channels/telegram/turn-queue.js +47 -0
- package/dist/channels/telegram/turn-store.d.ts +38 -0
- package/dist/channels/telegram/turn-store.js +150 -0
- package/dist/cli-auth.d.ts +24 -0
- package/dist/cli-auth.js +30 -0
- package/dist/cli-models.d.ts +11 -0
- package/dist/cli-models.js +20 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1288 -0
- package/dist/collect.d.ts +20 -0
- package/dist/collect.js +27 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +8 -0
- package/dist/deploy/container.d.ts +56 -0
- package/dist/deploy/container.js +169 -0
- package/dist/deploy/fly/plan.d.ts +65 -0
- package/dist/deploy/fly/plan.js +142 -0
- package/dist/deploy/fly/run.d.ts +51 -0
- package/dist/deploy/fly/run.js +89 -0
- package/dist/deploy/preflight.d.ts +59 -0
- package/dist/deploy/preflight.js +191 -0
- package/dist/deploy/railway/plan.d.ts +48 -0
- package/dist/deploy/railway/plan.js +91 -0
- package/dist/deploy/railway/run.d.ts +81 -0
- package/dist/deploy/railway/run.js +176 -0
- package/dist/deploy/runner.d.ts +18 -0
- package/dist/deploy/runner.js +25 -0
- package/dist/deploy/secrets.d.ts +49 -0
- package/dist/deploy/secrets.js +88 -0
- package/dist/dev-supervisor.d.ts +14 -0
- package/dist/dev-supervisor.js +158 -0
- package/dist/engines/pi/auth.d.ts +17 -0
- package/dist/engines/pi/auth.js +118 -0
- package/dist/engines/pi/channel.d.ts +31 -0
- package/dist/engines/pi/channel.js +113 -0
- package/dist/engines/pi/chat.d.ts +19 -0
- package/dist/engines/pi/chat.js +200 -0
- package/dist/engines/pi/config.d.ts +110 -0
- package/dist/engines/pi/config.js +285 -0
- package/dist/engines/pi/create.d.ts +127 -0
- package/dist/engines/pi/create.js +222 -0
- package/dist/engines/pi/definition.d.ts +72 -0
- package/dist/engines/pi/definition.js +129 -0
- package/dist/engines/pi/harness.d.ts +50 -0
- package/dist/engines/pi/harness.js +41 -0
- package/dist/engines/pi/invoke.d.ts +28 -0
- package/dist/engines/pi/invoke.js +298 -0
- package/dist/engines/pi/login.d.ts +51 -0
- package/dist/engines/pi/login.js +125 -0
- package/dist/engines/pi/models.d.ts +41 -0
- package/dist/engines/pi/models.js +76 -0
- package/dist/engines/pi/report.d.ts +13 -0
- package/dist/engines/pi/report.js +21 -0
- package/dist/engines/pi/sessions.d.ts +15 -0
- package/dist/engines/pi/sessions.js +130 -0
- package/dist/engines/pi/tool-context.d.ts +17 -0
- package/dist/engines/pi/tool-context.js +13 -0
- package/dist/engines/pi/tool.d.ts +45 -0
- package/dist/engines/pi/tool.js +85 -0
- package/dist/engines/pi/wake-tool.d.ts +20 -0
- package/dist/engines/pi/wake-tool.js +110 -0
- package/dist/engines/pi/workspace.d.ts +53 -0
- package/dist/engines/pi/workspace.js +70 -0
- package/dist/env.d.ts +24 -0
- package/dist/env.js +57 -0
- package/dist/github.d.ts +3 -0
- package/dist/github.js +2 -0
- package/dist/host/node.d.ts +41 -0
- package/dist/host/node.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/invoke-stream.d.ts +10 -0
- package/dist/invoke-stream.js +26 -0
- package/dist/loader.d.ts +38 -0
- package/dist/loader.js +69 -0
- package/dist/log.d.ts +26 -0
- package/dist/log.js +51 -0
- package/dist/observe.d.ts +12 -0
- package/dist/observe.js +44 -0
- package/dist/pi.d.ts +16 -0
- package/dist/pi.js +12 -0
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.js +23 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.js +29 -0
- package/dist/scaffold/add-channel.d.ts +46 -0
- package/dist/scaffold/add-channel.js +227 -0
- package/dist/scaffold/init.d.ts +54 -0
- package/dist/scaffold/init.js +249 -0
- package/dist/scaffold/templates/env.example +24 -0
- package/dist/scaffold/templates/fastagent.config.mjs +19 -0
- package/dist/scaffold/templates/gitignore +8 -0
- package/dist/scaffold/templates/gitignore.agentdir-root +5 -0
- package/dist/scaffold/templates/gitignore.kit +2 -0
- package/dist/scaffold/templates/persona.md +10 -0
- package/dist/scaffold/templates/skills/writing-great-skills/GLOSSARY.md +195 -0
- package/dist/scaffold/templates/skills/writing-great-skills/LICENSE +21 -0
- package/dist/scaffold/templates/skills/writing-great-skills/SKILL.md +82 -0
- package/dist/scaffold/templates/tools/fetch-url.ts +27 -0
- package/dist/scaffold/templates.d.ts +17 -0
- package/dist/scaffold/templates.js +56 -0
- package/dist/scaffold/vendor-skill.d.ts +24 -0
- package/dist/scaffold/vendor-skill.js +157 -0
- package/dist/schedule/audit.d.ts +18 -0
- package/dist/schedule/audit.js +57 -0
- package/dist/schedule/cron.d.ts +7 -0
- package/dist/schedule/cron.js +33 -0
- package/dist/schedule/discover.d.ts +17 -0
- package/dist/schedule/discover.js +64 -0
- package/dist/schedule/schedule.d.ts +35 -0
- package/dist/schedule/schedule.js +23 -0
- package/dist/schedule/scheduler.d.ts +36 -0
- package/dist/schedule/scheduler.js +253 -0
- package/dist/schedule/state.d.ts +14 -0
- package/dist/schedule/state.js +56 -0
- package/dist/schedule/wakeups.d.ts +82 -0
- package/dist/schedule/wakeups.js +196 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +2 -0
- package/dist/tunnel.d.ts +31 -0
- package/dist/tunnel.js +139 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +13 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.js +45 -0
- package/package.json +97 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth for the pi engine: a read-WRITE {@link CredentialStore} over a fastagent credentials file,
|
|
3
|
+
* consumed by the `Models` collection (models.ts). The path is project-level by default
|
|
4
|
+
* (`<dir>/.fastagent/auth.json`, resolved by the opener); {@link GLOBAL_AUTH_PATH} is the global
|
|
5
|
+
* location used as the override target / login default, not an implicit per-provider fallback.
|
|
6
|
+
*
|
|
7
|
+
* Project-level default + NO implicit project↔global fallback, for two reasons: (1) isolation — each
|
|
8
|
+
* agent can use a different account/subscription; (2) fail-visibly — a missing credential surfaces at
|
|
9
|
+
* startup instead of being masked by a machine-global one that won't exist on a fresh deploy box. A
|
|
10
|
+
* *fallback* specifically is refused because the only safe shape (read global, write the rotated token
|
|
11
|
+
* back to the project file) would diverge: OAuth refresh tokens are single-use, so consuming global's
|
|
12
|
+
* token and persisting the new one elsewhere leaves global stale for every other consumer.
|
|
13
|
+
*
|
|
14
|
+
* Sharing is still SAFE the right way: point everything at ONE file (`FASTAGENT_AUTH_PATH` → the
|
|
15
|
+
* global path). One file means one refresh lifecycle under `FileAuthStorageBackend`'s cross-process
|
|
16
|
+
* lock (refresh re-reads the latest token under the lock) — the documented same-machine pattern.
|
|
17
|
+
* fastagent's store stays SEPARATE from the pi CLI's `~/.pi/agent/auth.json` for the same single-
|
|
18
|
+
* lifecycle reason: two uncoordinated files over one grant would each rotate and break the other.
|
|
19
|
+
*
|
|
20
|
+
* Persistence + locking reuse pi's `FileAuthStorageBackend` (a cross-process file lock) on the WRITE
|
|
21
|
+
* path only. `read` is pi-ai's per-request hot path, so it stays UNLOCKED; the backend's in-place
|
|
22
|
+
* write opens only a sub-millisecond torn-read window, which `read` absorbs by re-reading. The write
|
|
23
|
+
* path refuses to overwrite a corrupt file (never clobbering other providers' credentials).
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { log } from "../../log.js";
|
|
29
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
30
|
+
import { FileAuthStorageBackend } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
/**
|
|
32
|
+
* The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`). The project-level default is
|
|
33
|
+
* `<dir>/.fastagent/auth.json` (computed by the opener and by `fastagent login`); this is only the
|
|
34
|
+
* `loginFlow()` PROGRAMMATIC fallback (when a caller omits `authPath`) and the path to point
|
|
35
|
+
* `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE credential file across projects
|
|
36
|
+
* (safe — one file, one lock-serialized refresh lifecycle). The `fastagent login` CLI is project-
|
|
37
|
+
* level by default, never this.
|
|
38
|
+
*/
|
|
39
|
+
export const GLOBAL_AUTH_PATH = join(homedir(), ".fastagent", "auth.json");
|
|
40
|
+
/** A valid stored credential, or undefined — a foreign/old entry reads as not-configured, not a crash. */
|
|
41
|
+
function pick(creds, providerId) {
|
|
42
|
+
const cred = creds[providerId];
|
|
43
|
+
return cred && (cred.type === "oauth" || cred.type === "api_key") ? cred : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse the credentials JSON for a WRITE: a corrupt file must THROW — serializing `{}` over it would
|
|
47
|
+
* wipe every other provider's credentials. The throw aborts the locked write, leaving the file intact.
|
|
48
|
+
*/
|
|
49
|
+
function parseForWrite(raw, where) {
|
|
50
|
+
if (!raw)
|
|
51
|
+
return {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(`refusing to overwrite corrupt auth file ${where} — fix or remove it`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** A read-write `CredentialStore` backed by the given credentials file (default {@link GLOBAL_AUTH_PATH};
|
|
60
|
+
* the directory opener passes the project-level `<dir>/.fastagent/auth.json`). */
|
|
61
|
+
export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options = {}) {
|
|
62
|
+
const warn = options.warn ?? ((message) => log.warn(message));
|
|
63
|
+
const backend = new FileAuthStorageBackend(authPath);
|
|
64
|
+
return {
|
|
65
|
+
async read(providerId) {
|
|
66
|
+
// UNLOCKED hot path: the only race is a sub-millisecond in-place write during an OAuth rotation,
|
|
67
|
+
// which can yield an empty/partial file. Re-read a few times before concluding it is corrupt;
|
|
68
|
+
// a valid `{}` (provider absent) returns immediately, so a not-configured read costs nothing.
|
|
69
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = readFileSync(authPath, "utf8");
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code === "ENOENT")
|
|
76
|
+
return undefined; // missing/deleted
|
|
77
|
+
warn(`[fastagent] cannot read ${authPath}: ${error.message}`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (raw !== "") {
|
|
81
|
+
try {
|
|
82
|
+
return pick(JSON.parse(raw), providerId);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// A partial read mid-write parses as garbage — fall through and retry.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (attempt < 2)
|
|
89
|
+
await sleep(2);
|
|
90
|
+
}
|
|
91
|
+
warn(`[fastagent] corrupt auth file ${authPath} — fix or remove it`);
|
|
92
|
+
return undefined;
|
|
93
|
+
},
|
|
94
|
+
modify(providerId, fn) {
|
|
95
|
+
return backend.withLockAsync(async (current) => {
|
|
96
|
+
const creds = parseForWrite(current, authPath); // corrupt → throw → no clobber
|
|
97
|
+
const next = await fn(pick(creds, providerId));
|
|
98
|
+
if (next === undefined)
|
|
99
|
+
return { result: pick(creds, providerId) }; // unchanged: no write
|
|
100
|
+
creds[providerId] = next;
|
|
101
|
+
return { result: next, next: `${JSON.stringify(creds, null, 2)}\n` };
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
async delete(providerId) {
|
|
105
|
+
// No-op when nothing is stored: do NOT take the lock (which would create the file via the
|
|
106
|
+
// backend's ensureFileExists) on a machine that never stored this provider.
|
|
107
|
+
if (!existsSync(authPath))
|
|
108
|
+
return;
|
|
109
|
+
await backend.withLockAsync(async (current) => {
|
|
110
|
+
const creds = parseForWrite(current, authPath);
|
|
111
|
+
if (!(providerId in creds))
|
|
112
|
+
return { result: undefined }; // absent: no write
|
|
113
|
+
delete creds[providerId];
|
|
114
|
+
return { result: undefined, next: `${JSON.stringify(creds, null, 2)}\n` };
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ChannelContext, type Routes } from "../../host/node.ts";
|
|
2
|
+
import { type ModuleLoadFailure } from "../../loader.ts";
|
|
3
|
+
/** A dropped route: two channels claim the same key. Surfaced, never silent. */
|
|
4
|
+
export interface ChannelCollision {
|
|
5
|
+
route: string;
|
|
6
|
+
source: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Channel file basenames under `<dir>/channels/` — the authoring view (`fastagent info`), which lists
|
|
10
|
+
* WITHOUT importing, unlike {@link loadChannels}. It enforces the SAME containment guard so info reports
|
|
11
|
+
* exactly the surface dev/start would accept: a channels/ symlink escaping the workspace is rejected
|
|
12
|
+
* here too. This path is independent of loadChannels', so it must guard the boundary on its own.
|
|
13
|
+
*/
|
|
14
|
+
export declare function discoverChannelFiles(dir: string): Promise<string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Discover channels in `<dir>/channels/`: each `*.ts|.js|.mjs` default-exports a `(ctx) => Routes`
|
|
17
|
+
* factory ({@link ChannelModule}), called here with the mount context; the returned route maps are
|
|
18
|
+
* merged (first file wins a route-key clash, the dropped route surfaced).
|
|
19
|
+
*
|
|
20
|
+
* A channel file broken for ANY reason — a failed import, a factory that throws when called (a missing
|
|
21
|
+
* env var is the common deploy case), or a malformed shape (not a function, not a Routes object, a bad
|
|
22
|
+
* handler/route key) — is collected in `failures` without preventing validation of sibling files. The
|
|
23
|
+
* serving CLI treats any such failure as fatal: a declared channel must not silently disappear or cause
|
|
24
|
+
* the default `/invoke` route to mount. Programmatic callers can inspect the returned data themselves.
|
|
25
|
+
* Routes are validated fully before any merge, so a throw mounts no partial routes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadChannels(dir: string, ctx: ChannelContext): Promise<{
|
|
28
|
+
routes: Routes;
|
|
29
|
+
collisions: ChannelCollision[];
|
|
30
|
+
failures: ModuleLoadFailure[];
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel discovery (the N axis, filesystem form): a workspace declares its inbound surface by
|
|
3
|
+
* dropping files in `channels/`, mirroring `tools/`. Each file wires a third-party adapter to the
|
|
4
|
+
* app's `on()` glue and returns the routes it mounts. There is no config-level channel list — a
|
|
5
|
+
* channel always needs glue, so it is always a file.
|
|
6
|
+
*/
|
|
7
|
+
import { readdir } from "node:fs/promises";
|
|
8
|
+
import { isAbsolute, join } from "node:path";
|
|
9
|
+
import { parseRouteKey } from "../../host/node.js";
|
|
10
|
+
import { assertInsideWorkspace } from "../../workspace.js";
|
|
11
|
+
import { isModuleFile, loadModuleDir } from "../../loader.js";
|
|
12
|
+
/**
|
|
13
|
+
* Channel file basenames under `<dir>/channels/` — the authoring view (`fastagent info`), which lists
|
|
14
|
+
* WITHOUT importing, unlike {@link loadChannels}. It enforces the SAME containment guard so info reports
|
|
15
|
+
* exactly the surface dev/start would accept: a channels/ symlink escaping the workspace is rejected
|
|
16
|
+
* here too. This path is independent of loadChannels', so it must guard the boundary on its own.
|
|
17
|
+
*/
|
|
18
|
+
export async function discoverChannelFiles(dir) {
|
|
19
|
+
await assertInsideWorkspace(dir, "channels");
|
|
20
|
+
let names;
|
|
21
|
+
try {
|
|
22
|
+
names = await readdir(join(dir, "channels"));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code === "ENOENT")
|
|
26
|
+
return [];
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return names
|
|
30
|
+
.filter(isModuleFile)
|
|
31
|
+
.map((n) => n.replace(/\.(ts|js|mjs)$/, ""))
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Discover channels in `<dir>/channels/`: each `*.ts|.js|.mjs` default-exports a `(ctx) => Routes`
|
|
36
|
+
* factory ({@link ChannelModule}), called here with the mount context; the returned route maps are
|
|
37
|
+
* merged (first file wins a route-key clash, the dropped route surfaced).
|
|
38
|
+
*
|
|
39
|
+
* A channel file broken for ANY reason — a failed import, a factory that throws when called (a missing
|
|
40
|
+
* env var is the common deploy case), or a malformed shape (not a function, not a Routes object, a bad
|
|
41
|
+
* handler/route key) — is collected in `failures` without preventing validation of sibling files. The
|
|
42
|
+
* serving CLI treats any such failure as fatal: a declared channel must not silently disappear or cause
|
|
43
|
+
* the default `/invoke` route to mount. Programmatic callers can inspect the returned data themselves.
|
|
44
|
+
* Routes are validated fully before any merge, so a throw mounts no partial routes.
|
|
45
|
+
*/
|
|
46
|
+
export async function loadChannels(dir, ctx) {
|
|
47
|
+
// The contract says stateRoot is absolute; enforce it at the mount boundary so a relative root fails
|
|
48
|
+
// fast HERE instead of silently re-anchoring some channel's state on the process cwd.
|
|
49
|
+
if (!isAbsolute(ctx.stateRoot)) {
|
|
50
|
+
throw new Error(`ChannelContext.stateRoot must be absolute, got "${ctx.stateRoot}"`);
|
|
51
|
+
}
|
|
52
|
+
// A symlinked channels/ is followed only if it stays inside the workspace, so a deploy that copies
|
|
53
|
+
// the dir includes it (the directory is the agent).
|
|
54
|
+
await assertInsideWorkspace(dir, "channels");
|
|
55
|
+
const { modules, failures } = await loadModuleDir(join(dir, "channels"));
|
|
56
|
+
const routes = {};
|
|
57
|
+
const collisions = [];
|
|
58
|
+
for (const { label, file, mod } of modules) {
|
|
59
|
+
// Collect every per-file failure so the caller can report all broken channels in one pass. The CLI
|
|
60
|
+
// then fails startup rather than silently dropping a declared route; direct callers own their policy.
|
|
61
|
+
// Routes are VALIDATED fully before any are merged, so a throw mid-validation mounts NO partial routes.
|
|
62
|
+
try {
|
|
63
|
+
const factory = mod.default;
|
|
64
|
+
if (typeof factory !== "function") {
|
|
65
|
+
throw new Error(`${label} must default-export (ctx) => Routes`);
|
|
66
|
+
}
|
|
67
|
+
const declared = factory(ctx);
|
|
68
|
+
// A Promise needs its own branch before the object check: mark it handled (a rejected async setup
|
|
69
|
+
// must not go unhandled) and reject it with a precise message rather than the zero-routes one.
|
|
70
|
+
if (declared !== null &&
|
|
71
|
+
typeof declared === "object" &&
|
|
72
|
+
typeof declared.then === "function") {
|
|
73
|
+
declared.catch(() => { });
|
|
74
|
+
throw new Error(`${label} must return Routes synchronously, not a Promise (an async factory is not supported)`);
|
|
75
|
+
}
|
|
76
|
+
if (declared === null || typeof declared !== "object") {
|
|
77
|
+
throw new Error(`${label} must return a Routes object, got ${declared === null ? "null" : typeof declared}`);
|
|
78
|
+
}
|
|
79
|
+
const declaredRoutes = Object.entries(declared);
|
|
80
|
+
if (declaredRoutes.length === 0) {
|
|
81
|
+
throw new Error(`${label} declared no routes — return a non-empty { "METHOD /path": handler } object (a Promise, Map, array, or {} yields none)`);
|
|
82
|
+
}
|
|
83
|
+
// Validate every route BEFORE merging any (no partial mount on a later throw).
|
|
84
|
+
for (const [route, handler] of declaredRoutes) {
|
|
85
|
+
if (typeof handler !== "function") {
|
|
86
|
+
throw new Error(`${label}: route "${route}" must map to a handler function, got ${typeof handler}`);
|
|
87
|
+
}
|
|
88
|
+
if (!parseRouteKey(route).path.startsWith("/")) {
|
|
89
|
+
throw new Error(`${label}: route "${route}" is not a valid route key (expected "METHOD /path" or "/path")`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const [route, handler] of declaredRoutes) {
|
|
93
|
+
const parsed = parseRouteKey(route);
|
|
94
|
+
// Overlap, not literal-key, equality: the router treats a bare `/path` as any-method, so
|
|
95
|
+
// `/webhook` and `POST /webhook` clash. `GET /x` vs `POST /x` is fine.
|
|
96
|
+
const clash = Object.keys(routes).some((k) => {
|
|
97
|
+
const e = parseRouteKey(k);
|
|
98
|
+
return (e.path === parsed.path &&
|
|
99
|
+
(e.method === undefined || parsed.method === undefined || e.method === parsed.method));
|
|
100
|
+
});
|
|
101
|
+
if (clash) {
|
|
102
|
+
collisions.push({ route, source: label });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
routes[route] = handler;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
failures.push({ label, file, message: error.message });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { routes, collisions, failures };
|
|
113
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AgentSessionRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface RunPiChatOptions {
|
|
3
|
+
/** Model spec override (the CLI --model flag). Precedence: this > FASTAGENT_MODEL > config.model. */
|
|
4
|
+
model?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
|
|
8
|
+
* skills resolved exactly as the dev opener does). Split from {@link runPiChat} so the assembly —
|
|
9
|
+
* the fidelity-critical part — is inspectable without launching the TUI.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildChatRuntime(dir: string, options?: RunPiChatOptions,
|
|
12
|
+
/** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
|
|
13
|
+
sessionManager?: SessionManager): Promise<AgentSessionRuntime>;
|
|
14
|
+
/**
|
|
15
|
+
* Open the workspace's agent in pi's interactive TUI and run until the user exits. The agent is
|
|
16
|
+
* fastagent's assembled agent (same model/tools/skills/prompt as dev/start serve); pi's TUI handles
|
|
17
|
+
* login, rendering, and same-workspace sessions natively.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runPiChat(dir: string, options?: RunPiChatOptions): Promise<void>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat: open a workspace into pi's interactive TUI (`fastagent chat`). A pi-specific COMMAND, not an
|
|
3
|
+
* engine-neutral channel: it drives pi's full session API (InteractiveMode) for fidelity, so it lives
|
|
4
|
+
* under engines/pi/ and is not re-exported.
|
|
5
|
+
*
|
|
6
|
+
* FIDELITY: chat must run the SAME agent dev/start serve, not pi's vanilla discovery (which would walk
|
|
7
|
+
* AGENTS.md up to the repo root and discover skills from pi's own global dirs). So fastagent's
|
|
8
|
+
* assembly is INJECTED into pi's session:
|
|
9
|
+
* - prompt → systemPromptOverride = base + instructions ONLY; pi appends the skill section and env
|
|
10
|
+
* (date/cwd) itself (including them here would duplicate both).
|
|
11
|
+
* - skills → skillsOverride (fastagent's skills, for the section + invocation).
|
|
12
|
+
* - tools → default coding tools by NAME (pi rebuilds them cwd-bound for rich rendering) +
|
|
13
|
+
* fastagent's custom tools via pi's customTools path (so they survive /new, /resume, fork).
|
|
14
|
+
*
|
|
15
|
+
* Cross-workspace session switches are rejected: `.env` is process-global, so one chat TUI is one
|
|
16
|
+
* workspace.
|
|
17
|
+
*
|
|
18
|
+
* AUTH: chat is the one command that does NOT use fastagent's credential file. It drives pi's own
|
|
19
|
+
* session services (`createAgentSessionServices`, auth from pi's `~/.pi` via `getAgentDir()`), so you
|
|
20
|
+
* log in through pi's TUI, not `fastagent login`. `--auth-path`/`FASTAGENT_AUTH_PATH` therefore do not
|
|
21
|
+
* apply here, and `createPiModels()` below is used only to RESOLVE the model descriptor (never for auth).
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { dirname, resolve } from "node:path";
|
|
25
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
26
|
+
import { InteractiveMode, SessionManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { loadConfig, resolveAgentDir, resolveModel, resolveModelSpec } from "./config.js";
|
|
28
|
+
import { assembleSystemPrompt, piBasePrompt, piDefaultTools, resolveTools } from "./create.js";
|
|
29
|
+
import { createPiModels } from "./models.js";
|
|
30
|
+
import { canonicalPath, loadAgentDefinition } from "./definition.js";
|
|
31
|
+
import { loadTools, mergeDiscoveredTools } from "./tool.js";
|
|
32
|
+
import { reportDefinitionWarnings, reportModuleLoadFailures, reportToolCollisions } from "./report.js";
|
|
33
|
+
/**
|
|
34
|
+
* Build pi's interactive runtime driven by fastagent's assembled agent (model, prompt, tools,
|
|
35
|
+
* skills resolved exactly as the dev opener does). Split from {@link runPiChat} so the assembly —
|
|
36
|
+
* the fidelity-critical part — is inspectable without launching the TUI.
|
|
37
|
+
*/
|
|
38
|
+
export async function buildChatRuntime(dir, options = {},
|
|
39
|
+
/** Session backend. Defaults to pi's project-scoped store; tests inject SessionManager.inMemory(). */
|
|
40
|
+
sessionManager) {
|
|
41
|
+
async function resolveAssembly(cwd) {
|
|
42
|
+
const { config } = await loadConfig(cwd);
|
|
43
|
+
const modelSpec = resolveModelSpec(options.model, config);
|
|
44
|
+
if (!modelSpec) {
|
|
45
|
+
throw new Error(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai-codex/gpt-5.5")`);
|
|
46
|
+
}
|
|
47
|
+
// Resolution only — the Models' auth is unused here; chat's auth is pi's own (~/.pi via the session
|
|
48
|
+
// services), so authPath is intentionally not threaded in. See the AUTH note in the header.
|
|
49
|
+
const model = resolveModel(createPiModels(), modelSpec);
|
|
50
|
+
const env = new NodeExecutionEnv({ cwd });
|
|
51
|
+
// Same agentDir/cwd split as dev/start: persona/skills/tools from agentDir, ② context walked from cwd.
|
|
52
|
+
const agentDir = resolveAgentDir(cwd, config);
|
|
53
|
+
const definition = await loadAgentDefinition(agentDir, { cwd, env });
|
|
54
|
+
reportDefinitionWarnings(definition.collisions, definition.diagnostics);
|
|
55
|
+
// Same tool resolution as the dev opener, then split: defaults go to pi by NAME (rebuilt cwd-bound
|
|
56
|
+
// for rich rendering); customs go through pi's `customTools` path so they survive /new, /resume, fork.
|
|
57
|
+
const discovered = await loadTools(agentDir);
|
|
58
|
+
const { tools, collisions: crossCollisions } = mergeDiscoveredTools(resolveTools(config, cwd), discovered.tools);
|
|
59
|
+
reportToolCollisions([...discovered.collisions, ...crossCollisions]);
|
|
60
|
+
reportModuleLoadFailures(discovered.failures);
|
|
61
|
+
const defaultNames = piDefaultTools(cwd).map((t) => t.name);
|
|
62
|
+
const customTools = tools.filter((t) => !defaultNames.includes(t.name));
|
|
63
|
+
// Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts it).
|
|
64
|
+
const customToolDefs = customTools.map((t) => ({
|
|
65
|
+
name: t.name,
|
|
66
|
+
label: t.name,
|
|
67
|
+
description: t.description ?? "",
|
|
68
|
+
parameters: t.parameters,
|
|
69
|
+
execute: (id, params, signal) => t.execute(id, params, signal),
|
|
70
|
+
}));
|
|
71
|
+
// base + instructions ONLY — pi appends the skill section and env (date/cwd) itself (including
|
|
72
|
+
// them here would duplicate both).
|
|
73
|
+
const systemPrompt = assembleSystemPrompt({
|
|
74
|
+
base: piBasePrompt({ tools, persona: definition.persona }),
|
|
75
|
+
contextFiles: definition.contextFiles,
|
|
76
|
+
});
|
|
77
|
+
return { model, definition, defaultNames, customTools, customToolDefs, systemPrompt };
|
|
78
|
+
}
|
|
79
|
+
// pi calls the factory again on /new, /resume, switch, and fork. Config/tools dynamic imports are
|
|
80
|
+
// ESM-cached, so treating same-cwd rebuilds as hot reload would yield a half-fresh agent (fresh
|
|
81
|
+
// AGENTS.md/skills, stale config/tools). Keep chat a coherent startup snapshot: restart to load
|
|
82
|
+
// edits. And keep it workspace-scoped — `.env` is process-global, so a switch to another cwd would
|
|
83
|
+
// leak env or require mutating global env at runtime.
|
|
84
|
+
const rootCwd = canonicalPath(dir);
|
|
85
|
+
let assembly;
|
|
86
|
+
const assemblyFor = (cwd) => {
|
|
87
|
+
// Canonical paths: pi's process.cwd() fallback is a realpath, so a symlinked workspace would
|
|
88
|
+
// otherwise mismatch a non-realpath rootCwd.
|
|
89
|
+
const activeCwd = canonicalPath(cwd);
|
|
90
|
+
if (activeCwd !== rootCwd) {
|
|
91
|
+
throw workspaceScopeError(activeCwd);
|
|
92
|
+
}
|
|
93
|
+
assembly ??= resolveAssembly(rootCwd);
|
|
94
|
+
return assembly;
|
|
95
|
+
};
|
|
96
|
+
const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
97
|
+
const { model, definition, defaultNames, customTools, customToolDefs, systemPrompt } = await assemblyFor(cwd);
|
|
98
|
+
const services = await createAgentSessionServices({
|
|
99
|
+
cwd,
|
|
100
|
+
resourceLoaderOptions: {
|
|
101
|
+
// Definition-only, like dev/start: suppress pi's machine-global discovery (the developer's own
|
|
102
|
+
// ~/.pi extensions, slash commands, global AGENTS.md, APPEND_SYSTEM.md) so chat runs the same
|
|
103
|
+
// agent that gets served, not the authoring machine's pi setup on top.
|
|
104
|
+
noExtensions: true,
|
|
105
|
+
noPromptTemplates: true,
|
|
106
|
+
noContextFiles: true,
|
|
107
|
+
systemPromptOverride: () => systemPrompt,
|
|
108
|
+
appendSystemPromptOverride: () => [],
|
|
109
|
+
// Replace pi's discovered skills with fastagent's. fastagent's Skill (content inline) is
|
|
110
|
+
// reshaped to pi-coding-agent's (read from filePath/baseDir at invocation time).
|
|
111
|
+
skillsOverride: (base) => ({
|
|
112
|
+
skills: definition.skills.map((s) => ({
|
|
113
|
+
name: s.name,
|
|
114
|
+
description: s.description,
|
|
115
|
+
filePath: s.filePath,
|
|
116
|
+
baseDir: dirname(s.filePath),
|
|
117
|
+
sourceInfo: {
|
|
118
|
+
path: s.filePath,
|
|
119
|
+
source: "fastagent",
|
|
120
|
+
scope: "project",
|
|
121
|
+
origin: "top-level",
|
|
122
|
+
baseDir: dirname(s.filePath),
|
|
123
|
+
},
|
|
124
|
+
disableModelInvocation: s.disableModelInvocation ?? false,
|
|
125
|
+
})),
|
|
126
|
+
diagnostics: base.diagnostics,
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const result = await createAgentSessionFromServices({
|
|
131
|
+
services,
|
|
132
|
+
sessionManager,
|
|
133
|
+
sessionStartEvent,
|
|
134
|
+
model,
|
|
135
|
+
tools: [...defaultNames, ...customTools.map((t) => t.name)],
|
|
136
|
+
customTools: customToolDefs,
|
|
137
|
+
});
|
|
138
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
139
|
+
};
|
|
140
|
+
const runtime = await createAgentSessionRuntime(createRuntime, {
|
|
141
|
+
cwd: rootCwd,
|
|
142
|
+
agentDir: getAgentDir(),
|
|
143
|
+
sessionManager: sessionManager ?? SessionManager.create(rootCwd),
|
|
144
|
+
});
|
|
145
|
+
enforceWorkspaceScopedSessionSwitches(runtime, rootCwd);
|
|
146
|
+
return runtime;
|
|
147
|
+
}
|
|
148
|
+
function workspaceScopeError(targetCwd) {
|
|
149
|
+
return new Error(`fastagent chat is workspace-scoped: cannot switch to ${targetCwd}; run \`fastagent chat ${targetCwd}\` instead`);
|
|
150
|
+
}
|
|
151
|
+
function readSessionHeaderCwd(sessionPath) {
|
|
152
|
+
const resolvedPath = resolve(sessionPath);
|
|
153
|
+
if (!existsSync(resolvedPath))
|
|
154
|
+
return undefined;
|
|
155
|
+
for (const line of readFileSync(resolvedPath, "utf8").split(/\r?\n/)) {
|
|
156
|
+
if (!line.trim())
|
|
157
|
+
continue;
|
|
158
|
+
try {
|
|
159
|
+
const entry = JSON.parse(line);
|
|
160
|
+
if (entry.type === "session")
|
|
161
|
+
return typeof entry.cwd === "string" ? canonicalPath(entry.cwd) : undefined;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Ignore malformed lines the same way pi's session loader does; no header cwd → caller pins root.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Keep resume/import inside the chat's single workspace, deciding BEFORE delegating to pi. The chat
|
|
171
|
+
* process is chdir'd into rootCwd, so a session with no cwd header already lands on rootCwd. The gap
|
|
172
|
+
* is a session that EXPLICITLY records a different cwd: pi would bind it and the factory would reject
|
|
173
|
+
* it — but only AFTER tearing the live session down. Reject such a switch up front.
|
|
174
|
+
*/
|
|
175
|
+
function enforceWorkspaceScopedSessionSwitches(runtime, rootCwd) {
|
|
176
|
+
const rejectForeignTarget = (sessionPath, cwdOverride) => {
|
|
177
|
+
const target = cwdOverride !== undefined ? canonicalPath(cwdOverride) : readSessionHeaderCwd(sessionPath);
|
|
178
|
+
if (target !== undefined && target !== rootCwd)
|
|
179
|
+
throw workspaceScopeError(target);
|
|
180
|
+
};
|
|
181
|
+
const switchSession = runtime.switchSession.bind(runtime);
|
|
182
|
+
runtime.switchSession = async (...args) => {
|
|
183
|
+
rejectForeignTarget(args[0], args[1]?.cwdOverride);
|
|
184
|
+
return switchSession(...args);
|
|
185
|
+
};
|
|
186
|
+
const importFromJsonl = runtime.importFromJsonl.bind(runtime);
|
|
187
|
+
runtime.importFromJsonl = async (...args) => {
|
|
188
|
+
rejectForeignTarget(args[0], args[1]);
|
|
189
|
+
return importFromJsonl(...args);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Open the workspace's agent in pi's interactive TUI and run until the user exits. The agent is
|
|
194
|
+
* fastagent's assembled agent (same model/tools/skills/prompt as dev/start serve); pi's TUI handles
|
|
195
|
+
* login, rendering, and same-workspace sessions natively.
|
|
196
|
+
*/
|
|
197
|
+
export async function runPiChat(dir, options = {}) {
|
|
198
|
+
const runtime = await buildChatRuntime(dir, options);
|
|
199
|
+
await new InteractiveMode(runtime, {}).run();
|
|
200
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Models } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { AnyModel } from "./harness.ts";
|
|
4
|
+
export interface FastagentConfig {
|
|
5
|
+
/** "provider/modelId". Precedence: CLI --model > FASTAGENT_MODEL > config. */
|
|
6
|
+
model?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The agent-definition subdirectory (persona.md, skills/, tools/, channels/), relative to the config
|
|
9
|
+
* file's directory. Default: the config directory itself (flat — today's behaviour). Point it at a
|
|
10
|
+
* sibling like `"./agent"` to serve an existing repo as a coding agent: the config dir stays the run
|
|
11
|
+
* root (cwd, whose AGENTS.md the agent reads as ② context), while the agent's own surface lives in the
|
|
12
|
+
* subdir and does not collide with the host's `tools/`/`src/` (core.md scenario grid).
|
|
13
|
+
*/
|
|
14
|
+
agentDir?: string;
|
|
15
|
+
/** Extra custom tools, appended after pi defaults — never replaces them. */
|
|
16
|
+
tools?: AgentTool[];
|
|
17
|
+
http?: {
|
|
18
|
+
port?: number;
|
|
19
|
+
};
|
|
20
|
+
/** Mount the built-in `wake` tool so the agent can schedule its OWN follow-up turns (self-scheduling).
|
|
21
|
+
* Off by default — self-scheduling is an autonomy capability, opt in when you want it. Only takes
|
|
22
|
+
* effect on the serving path (`dev`/`start`, where the scheduler poller honors a wake-up). */
|
|
23
|
+
selfSchedule?: boolean;
|
|
24
|
+
/** Deploy-time declarations for what the agent needs on the box, so real agents don't hand-write a
|
|
25
|
+
* Dockerfile / hand-set variables. */
|
|
26
|
+
deploy?: {
|
|
27
|
+
/** Extra secret env-var NAMES the deployed agent needs beyond the model key + channel secrets — e.g.
|
|
28
|
+
* a `GH_TOKEN` its tools use. `deploy` carries each from the LOCAL env to the host secret store and
|
|
29
|
+
* lists them in the runbook; a missing value gates `--run` (like a channel secret). */
|
|
30
|
+
secrets?: string[];
|
|
31
|
+
/** Extra apt packages baked into the generated image (Debian default repos: git, ripgrep, jq…). For a
|
|
32
|
+
* package needing a custom apt repo (e.g. gh) or a different base image, provide your own Dockerfile
|
|
33
|
+
* — `deploy` keeps an existing one. */
|
|
34
|
+
apt?: string[];
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Identity function for typing and IDE completion (vite/next-style). */
|
|
38
|
+
export declare function defineConfig(config: FastagentConfig): FastagentConfig;
|
|
39
|
+
export interface LoadedConfig {
|
|
40
|
+
config: FastagentConfig;
|
|
41
|
+
/** Config file path; undefined when running zero-config. */
|
|
42
|
+
path?: string;
|
|
43
|
+
}
|
|
44
|
+
/** A valid bindable port. */
|
|
45
|
+
export declare function isValidPort(n: number): boolean;
|
|
46
|
+
/** The config filenames that make a directory a fastagent workspace, in load precedence. ONE source: the
|
|
47
|
+
* loader (below) and `scaffoldWorkspace`'s already-a-workspace refusal both read this, so "is there a
|
|
48
|
+
* config?" can't diverge between them when the set changes. */
|
|
49
|
+
export declare const WORKSPACE_CONFIG_NAMES: readonly ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
|
|
50
|
+
/** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = zero-config; a wrong-shape file throws. */
|
|
51
|
+
export declare function loadConfig(dir: string): Promise<LoadedConfig>;
|
|
52
|
+
/**
|
|
53
|
+
* The agent-definition dir from config: `config.agentDir` resolved against `dir`, or `dir` itself when
|
|
54
|
+
* unset (flat). The ONE place this is computed — every opener (`dev`/`start`/`info`/`tool`/`deploy`/`chat`)
|
|
55
|
+
* calls it, so the "relative to the config dir, default `.`" rule can never diverge. loadConfig has
|
|
56
|
+
* already validated that agentDir stays under `dir`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveAgentDir(dir: string, config: FastagentConfig): string;
|
|
59
|
+
/** Resolve "provider/modelId" → a pi Model from `models`, so the harness resolves auth from the same collection. */
|
|
60
|
+
export declare function resolveModel(models: Models, spec: string): AnyModel;
|
|
61
|
+
/** All registered "provider/modelId" specs in `models`, sorted — the list behind `fastagent models`. */
|
|
62
|
+
export declare function listModels(models: Models): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Rewrite the `model` in a config file's SOURCE TEXT to `spec`, for the first-run picker's write-back.
|
|
65
|
+
* Handles the scaffold's commented placeholder (`// model: "…"`) and an existing `model:` line; returns
|
|
66
|
+
* null when neither is present (zero-config or a hand-shaped config) so the caller falls back to a
|
|
67
|
+
* printed hint instead of guessing where to insert. Text-level (not AST) on purpose — it only ever
|
|
68
|
+
* touches a line it recognizes, never reformats the author's file.
|
|
69
|
+
*/
|
|
70
|
+
export declare function rewriteConfigModel(src: string, spec: string): string | null;
|
|
71
|
+
/** Model selection precedence: CLI flag > FASTAGENT_MODEL env > config default. */
|
|
72
|
+
export declare function resolveModelSpec(flag: string | undefined, config: FastagentConfig, env?: NodeJS.ProcessEnv): string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* `start`'s sessions-dir override: `--sessions-dir` flag > `FASTAGENT_SESSIONS_DIR` env > undefined
|
|
75
|
+
* (the opener then falls back to {@link defaultSessionsDir} under the {@link resolveStateRoot} root).
|
|
76
|
+
* Resolved to absolute so the store and the startup report agree regardless of cwd.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveSessionsDirOverride(flag: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The auth-file override: `--auth-path` flag > `FASTAGENT_AUTH_PATH` env > undefined (the opener then
|
|
81
|
+
* falls back to {@link defaultAuthPath} under the {@link resolveStateRoot} root). Resolved to absolute
|
|
82
|
+
* so the store and the startup report agree regardless of cwd. No implicit project↔global fallback (isolation
|
|
83
|
+
* + fail-visibly; see auth.ts); to share one account across projects, point this at the global
|
|
84
|
+
* `~/.fastagent/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveAuthPathOverride(flag: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* The IN-TREE default state root, `<dir>/.fastagent` — what {@link resolveStateRoot} falls back to when
|
|
89
|
+
* `FASTAGENT_STATE_DIR` moves state nowhere. THE single definition of that path segment.
|
|
90
|
+
*/
|
|
91
|
+
export declare function projectStateDir(dir: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* The resolved state root — the ONE durable machine-state home everything derives from (auth.json,
|
|
94
|
+
* sessions/, channels/<kind>/): `FASTAGENT_STATE_DIR` env > `<dir>/.fastagent`. Absolute, so channels
|
|
95
|
+
* and the startup report agree regardless of cwd. Definition: single lifecycle (precious, survives
|
|
96
|
+
* redeploy), single process — a container mounts ONE volume here. The finer knobs
|
|
97
|
+
* (`FASTAGENT_SESSIONS_DIR`, `FASTAGENT_AUTH_PATH`) still override their specific path on top.
|
|
98
|
+
*
|
|
99
|
+
* `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
|
|
100
|
+
* — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
|
|
101
|
+
* DEFAULT (`<dir>/.fastagent`) is dir-anchored. Deployments set an absolute path (a mounted volume);
|
|
102
|
+
* a relative value is in-tree — hence self-ignored — only when run from the definition dir (cwd == dir).
|
|
103
|
+
*/
|
|
104
|
+
export declare function resolveStateRoot(dir: string, env?: NodeJS.ProcessEnv): string;
|
|
105
|
+
/** The default credentials file under a resolved state root ({@link resolveStateRoot}). */
|
|
106
|
+
export declare function defaultAuthPath(stateRoot: string): string;
|
|
107
|
+
/** The effective auth file for a workspace: override if present, else the project-level auth.json. */
|
|
108
|
+
export declare function resolveAuthPath(dir: string, flag: string | undefined, env?: NodeJS.ProcessEnv): string;
|
|
109
|
+
/** The default sessions dir under a resolved state root ({@link resolveStateRoot}). */
|
|
110
|
+
export declare function defaultSessionsDir(stateRoot: string): string;
|