@fastagent-sh/fastagent 0.15.0 → 0.16.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 (168) hide show
  1. package/README.md +13 -3
  2. package/dist/channels/agentcore-limits.d.ts +7 -0
  3. package/dist/channels/agentcore-limits.js +9 -0
  4. package/dist/channels/agentcore-state.d.ts +83 -0
  5. package/dist/channels/agentcore-state.js +258 -0
  6. package/dist/channels/agentcore.d.ts +98 -0
  7. package/dist/channels/agentcore.js +260 -0
  8. package/dist/channels/busy.d.ts +23 -0
  9. package/dist/channels/busy.js +53 -0
  10. package/dist/channels/context-buffer.d.ts +5 -0
  11. package/dist/channels/context-buffer.js +5 -0
  12. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  13. package/dist/channels/feishu/context-buffer.js +60 -6
  14. package/dist/channels/feishu/feishu-api.js +4 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -12
  16. package/dist/channels/feishu/feishu.js +143 -54
  17. package/dist/channels/feishu/invoke-turn.js +35 -18
  18. package/dist/channels/feishu/model.d.ts +0 -1
  19. package/dist/channels/feishu/normalize.js +0 -1
  20. package/dist/channels/feishu/parse.d.ts +21 -7
  21. package/dist/channels/feishu/parse.js +24 -7
  22. package/dist/channels/feishu/preview.js +3 -2
  23. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  24. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  25. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  26. package/dist/channels/feishu/setup-mode.js +27 -1
  27. package/dist/channels/github/github.js +8 -1
  28. package/dist/channels/http.js +1 -1
  29. package/dist/channels/lark/scaffold/channel.ts +9 -8
  30. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  31. package/dist/channels/preview-kit.d.ts +7 -1
  32. package/dist/channels/preview-kit.js +3 -2
  33. package/dist/channels/slack/parse.d.ts +16 -1
  34. package/dist/channels/slack/parse.js +46 -3
  35. package/dist/channels/slack/preview.d.ts +1 -2
  36. package/dist/channels/slack/preview.js +68 -24
  37. package/dist/channels/slack/scaffold/channel.ts +5 -5
  38. package/dist/channels/slack/slack-api.d.ts +3 -23
  39. package/dist/channels/slack/slack-api.js +6 -22
  40. package/dist/channels/slack/slack.d.ts +13 -20
  41. package/dist/channels/slack/slack.js +95 -50
  42. package/dist/channels/state.d.ts +11 -4
  43. package/dist/channels/state.js +19 -12
  44. package/dist/channels/tasks.d.ts +0 -6
  45. package/dist/channels/tasks.js +16 -1
  46. package/dist/channels/telegram/parse.d.ts +0 -7
  47. package/dist/channels/telegram/parse.js +4 -2
  48. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  49. package/dist/channels/telegram/telegram.js +1 -1
  50. package/dist/channels/text.d.ts +14 -0
  51. package/dist/channels/text.js +14 -0
  52. package/dist/channels/thread-participants.d.ts +21 -0
  53. package/dist/channels/thread-participants.js +132 -0
  54. package/dist/channels/turn-queue.js +7 -0
  55. package/dist/cli/add-feishu.d.ts +7 -4
  56. package/dist/cli/add-feishu.js +57 -37
  57. package/dist/cli/add-slack.d.ts +2 -1
  58. package/dist/cli/add-slack.js +6 -11
  59. package/dist/cli/commands/add.js +50 -51
  60. package/dist/cli/commands/attach.js +8 -4
  61. package/dist/cli/commands/chat.js +8 -8
  62. package/dist/cli/commands/deploy.d.ts +1 -1
  63. package/dist/cli/commands/deploy.js +323 -77
  64. package/dist/cli/commands/dev.js +24 -22
  65. package/dist/cli/commands/fire.js +15 -16
  66. package/dist/cli/commands/info.js +36 -29
  67. package/dist/cli/commands/init.d.ts +1 -1
  68. package/dist/cli/commands/init.js +65 -53
  69. package/dist/cli/commands/invoke.js +9 -6
  70. package/dist/cli/commands/login.js +35 -21
  71. package/dist/cli/commands/schedule.js +6 -8
  72. package/dist/cli/commands/start.js +93 -37
  73. package/dist/cli/commands/tool.js +22 -18
  74. package/dist/cli/fail.d.ts +17 -0
  75. package/dist/cli/fail.js +24 -0
  76. package/dist/cli/program.js +57 -36
  77. package/dist/cli/serve.d.ts +26 -6
  78. package/dist/cli/serve.js +62 -15
  79. package/dist/cli/shared.d.ts +15 -2
  80. package/dist/cli/shared.js +30 -18
  81. package/dist/deploy/agentcore/plan.d.ts +117 -0
  82. package/dist/deploy/agentcore/plan.js +721 -0
  83. package/dist/deploy/agentcore/run.d.ts +73 -0
  84. package/dist/deploy/agentcore/run.js +412 -0
  85. package/dist/deploy/agentcore/zip.d.ts +17 -0
  86. package/dist/deploy/agentcore/zip.js +68 -0
  87. package/dist/deploy/container.d.ts +26 -25
  88. package/dist/deploy/container.js +93 -89
  89. package/dist/deploy/docker/plan.d.ts +1 -1
  90. package/dist/deploy/docker/plan.js +12 -17
  91. package/dist/deploy/fly/plan.d.ts +2 -0
  92. package/dist/deploy/fly/plan.js +27 -19
  93. package/dist/deploy/fly/run.d.ts +12 -1
  94. package/dist/deploy/fly/run.js +36 -2
  95. package/dist/deploy/preflight.d.ts +11 -5
  96. package/dist/deploy/preflight.js +217 -65
  97. package/dist/deploy/railway/plan.d.ts +7 -0
  98. package/dist/deploy/railway/plan.js +41 -16
  99. package/dist/deploy/railway/run.d.ts +8 -1
  100. package/dist/deploy/railway/run.js +7 -2
  101. package/dist/deploy/runner.d.ts +5 -2
  102. package/dist/deploy/runner.js +9 -3
  103. package/dist/dev-supervisor.d.ts +11 -8
  104. package/dist/dev-supervisor.js +53 -51
  105. package/dist/engines/pi/auth.d.ts +8 -7
  106. package/dist/engines/pi/auth.js +12 -10
  107. package/dist/engines/pi/channel.d.ts +1 -1
  108. package/dist/engines/pi/channel.js +5 -5
  109. package/dist/engines/pi/chat.js +2 -2
  110. package/dist/engines/pi/config.d.ts +6 -46
  111. package/dist/engines/pi/config.js +21 -108
  112. package/dist/engines/pi/create.d.ts +8 -8
  113. package/dist/engines/pi/create.js +13 -12
  114. package/dist/engines/pi/definition.d.ts +7 -26
  115. package/dist/engines/pi/definition.js +8 -54
  116. package/dist/engines/pi/login.d.ts +1 -1
  117. package/dist/engines/pi/models.d.ts +3 -3
  118. package/dist/engines/pi/models.js +1 -1
  119. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +29 -23
  120. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  121. package/dist/engines/pi/session-builder.d.ts +2 -2
  122. package/dist/engines/pi/session-builder.js +11 -11
  123. package/dist/engines/pi/tool.js +4 -0
  124. package/dist/env.d.ts +16 -4
  125. package/dist/env.js +43 -5
  126. package/dist/host/node.d.ts +2 -2
  127. package/dist/loader.d.ts +2 -2
  128. package/dist/loader.js +3 -3
  129. package/dist/log.d.ts +1 -1
  130. package/dist/log.js +1 -1
  131. package/dist/paths.d.ts +138 -0
  132. package/dist/paths.js +326 -0
  133. package/dist/pi.d.ts +1 -1
  134. package/dist/pi.js +2 -2
  135. package/dist/runtime.d.ts +7 -5
  136. package/dist/runtime.js +2 -2
  137. package/dist/scaffold/add-channel.d.ts +7 -3
  138. package/dist/scaffold/add-channel.js +55 -29
  139. package/dist/scaffold/init.d.ts +32 -41
  140. package/dist/scaffold/init.js +161 -185
  141. package/dist/scaffold/templates/env.example +15 -6
  142. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  143. package/dist/scaffold/templates/gitignore +14 -6
  144. package/dist/scaffold/templates/persona.md +4 -2
  145. package/dist/scaffold/templates/secrets.gitignore +5 -0
  146. package/dist/scaffold/templates.d.ts +1 -7
  147. package/dist/scaffold/templates.js +3 -25
  148. package/dist/scaffold/vendor-skill.d.ts +2 -2
  149. package/dist/scaffold/vendor-skill.js +13 -13
  150. package/dist/schedule/discover.js +4 -4
  151. package/dist/schedule/scheduler.d.ts +40 -1
  152. package/dist/schedule/scheduler.js +89 -56
  153. package/dist/schedule/state.js +1 -1
  154. package/dist/schedule/wake-alarm.d.ts +47 -0
  155. package/dist/schedule/wake-alarm.js +136 -0
  156. package/dist/schedule/wakeups.d.ts +1 -0
  157. package/dist/schedule/wakeups.js +18 -0
  158. package/dist/tunnel.d.ts +3 -3
  159. package/dist/tunnel.js +7 -7
  160. package/package.json +4 -1
  161. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  162. package/dist/channels/feishu/owned-threads.js +0 -47
  163. package/dist/channels/slack/owned-threads.d.ts +0 -6
  164. package/dist/channels/slack/owned-threads.js +0 -43
  165. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  166. package/dist/scaffold/templates/gitignore.kit +0 -2
  167. package/dist/workspace.d.ts +0 -9
  168. package/dist/workspace.js +0 -45
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The `fastagent dev` process supervisor: re-spawn the CLI as a worker (`FASTAGENT_DEV_WORKER=1`) and
3
- * restart it on debounced edits to the workspace's CODE inputs. Each restart is a fresh process
3
+ * restart it on debounced edits to the agent's CODE inputs. Each restart is a fresh process
4
4
  * (always-latest, no stale module cache). The supervisor never exits on a bad edit — the worker fails
5
5
  * loudly and it waits for the next save.
6
6
  *
@@ -13,65 +13,61 @@
13
13
  import { spawn } from "node:child_process";
14
14
  import { relative, sep } from "node:path";
15
15
  import { watch as watchTree } from "chokidar";
16
- import { loadConfig, resolveAgentDir, resolveStateRoot } from "./engines/pi/config.js";
16
+ import { AGENT_CONFIG_NAMES, resolveStateRoot } from "./paths.js";
17
+ import { isUnderDir } from "./engines/pi/definition.js";
18
+ import { dotEnvPath } from "./env.js";
17
19
  import { log } from "./log.js";
18
20
  import { installProxyFetch } from "./proxy.js";
19
21
  import { openExternalUrl } from "./open-url.js";
20
22
  import { announceWebhooks, startCloudflareTunnel } from "./tunnel.js";
21
- /** What the dev watcher restarts on (workspace-relative): the process-bound code inputs only. */
22
- const WATCHED_HINT = "tools/, channels/, package.json (agent dir), fastagent.config.*, .env (run root)";
23
+ /** What the dev watcher restarts on (agent-dir-relative): the process-bound code inputs only. */
24
+ const WATCHED_HINT = "tools/, channels/, schedules/, package.json, fastagent.config.*, .secrets/.env";
23
25
  /**
24
- * chokidar `ignored` matcher for the narrow watch scope (true = ignore). Ignoring a directory prunes
25
- * the whole subtree, so everything outside the allowlist .fastagent state, node_modules, .git, and
26
- * any file/dir the agent writes as work product costs no watchers and triggers no restarts.
27
- * Helper code imported from OUTSIDE tools//channels/ is out of scope by design (keep it under
28
- * tools/, or restart manually) the startup log names the watched set.
26
+ * chokidar `ignored` matcher for the narrow watch scope (true = ignore), rooted at the AGENT DIR. When
27
+ * the agent sits INSIDE the workspace that means the surrounding tree never triggers a restart at all;
28
+ * when the agent IS the workspace the root is that tree, and the allowlist below is what keeps the
29
+ * author's own files out of scope. Ignoring a directory prunes the whole subtree, so everything outside it
30
+ * `.state/` machine state, node_modules, .git, and any file/dir the agent writes as work product
31
+ * costs no watchers and triggers no restarts. Helper code imported from OUTSIDE tools//channels/ is
32
+ * out of scope by design (keep it under tools/, or restart manually) — the startup log names the set.
29
33
  */
30
- export function devWatchIgnored(dir, agentDir) {
34
+ export function devWatchIgnored(root, envFile) {
35
+ // The `.env` is allow-listed by its RESOLVED path, not by the `.secrets` name: FASTAGENT_SECRETS_DIR
36
+ // can put it in an in-agent directory called anything, and a name-based rule would prune the very
37
+ // file the worker loads (a credential edit would then silently never restart it).
38
+ const envRel = relative(root, envFile).split(sep);
31
39
  return (path) => {
32
- if (path === dir || path === agentDir)
33
- return false; // the roots themselves must not be pruned
34
- // Never prune a directory on the path from the watch root down to agentDir, so chokidar can descend
35
- // into `agentDir/tools` even when agentDir is a subdir (config.agentDir = "./agent").
36
- if (agentDir.startsWith(path + sep))
40
+ if (path === root)
41
+ return false; // the root itself must not be pruned
42
+ const rel = relative(root, path);
43
+ // Code inputs at the agent dir root: config, package.json, and the dirs loaded once per worker
44
+ // (a restart is their only re-read). Everything else (skills/, persona.md, AGENTS.md) is
45
+ // live-read — pruned, no restart.
46
+ // The config NAMES come from paths.ts, not a regex spelled here: adding a name there must not
47
+ // silently stop `dev` restarting on edits to it.
48
+ if (AGENT_CONFIG_NAMES.includes(rel))
37
49
  return false;
38
- const rel = relative(dir, path);
39
- // Run-root (cwd) inputs: .env + fastagent.config.* live where config lives, not in agentDir.
40
- if (rel === ".env")
50
+ if (rel === "package.json")
41
51
  return false;
42
- if (/^fastagent\.config\.[cm]?[jt]s$/.test(rel))
52
+ const segments = rel.split(sep);
53
+ if (segments[0] === "tools" || segments[0] === "channels" || segments[0] === "schedules")
54
+ return false;
55
+ // The `.env` restarts too (credentials are process-bound). Keep it AND its ancestor directories
56
+ // un-pruned so chokidar can descend to it; every sibling inside them (auth.json, .env.example)
57
+ // prunes normally. An out-of-agent `.env` yields a `..`-prefixed envRel that matches nothing here
58
+ // — the supervisor warns about that case instead of pretending to watch it.
59
+ if (segments.length <= envRel.length && segments.every((seg, i) => seg === envRel[i]))
43
60
  return false;
44
- // Agent code inputs live in agentDir: tools/, channels/, schedules/ (loaded once per worker — a
45
- // restart is their only re-read), package.json (its own deps). Everything else under agentDir
46
- // (skills/, persona.md, AGENTS.md) is live-read — pruned, no restart.
47
- const relAgent = relative(agentDir, path);
48
- if (!relAgent.startsWith("..")) {
49
- const [head] = relAgent.split(sep);
50
- if (head === "tools" || head === "channels" || head === "schedules")
51
- return false;
52
- if (relAgent === "package.json")
53
- return false;
54
- }
55
61
  return true;
56
62
  };
57
63
  }
58
- /** Spawn the dev worker and restart it on workspace edits; supervise its lifecycle until the process exits. */
59
- export async function runDevSupervisor(dir, options = {}) {
60
- // The watch root is `dir` (cwd); tools/channels the restart-watch cares about live in agentDir. On a
61
- // config error, default agentDir=dir and let the spawned worker surface the real error (fail-visibly).
62
- // agentDir is assumed STATIC for the dev session: the supervisor computes it once here and each spawned
63
- // worker recomputes its own from the same config config validation guarantees it stays under `dir`
64
- // (so the watch scope is always right); a config edit that changes agentDir mid-session (rare) is out
65
- // of scope for watch-scope re-sync (it triggers a worker restart regardless).
66
- // A genuine config error (not just a missing agentDir key) — debug-log it here so the silence is not
67
- // total before the spawned worker crash-loops and surfaces the real message; default agentDir=dir.
68
- const config = await loadConfig(dir)
69
- .then((r) => r.config)
70
- .catch((err) => {
71
- log.debug(`[fastagent] dev: config load failed while resolving agentDir (worker will report): ${String(err)}`);
72
- return {};
73
- });
74
- const agentDir = resolveAgentDir(dir, config);
64
+ /** Spawn the dev worker and restart it on agent-dir edits; supervise its lifecycle until the process exits. */
65
+ export async function runDevSupervisor(placement, options = {}) {
66
+ // The placement arrives RESOLVED from the command (which already routed its refusal through
67
+ // failStartup): re-resolving here would duplicate the rule and surface the same user-fixable
68
+ // refusal as a raw stack. The watch root is the AGENT DIR every restart-relevant code input lives
69
+ // under it, so the surrounding workspace costs no watchers at all. Placement is assumed STATIC for
70
+ // the session (creating/removing `fastagent/` mid-session is out of scope for watch re-sync).
75
71
  let worker;
76
72
  let reloadPending = false;
77
73
  let everServed = false; // has any worker successfully bound (sent `ready`) yet?
@@ -100,10 +96,10 @@ export async function runDevSupervisor(dir, options = {}) {
100
96
  void startCloudflareTunnel(m.port).then((t) => {
101
97
  if (t) {
102
98
  tunnel = t;
103
- void announceWebhooks(dir, t.url, {
99
+ void announceWebhooks(placement.agentDir, t.url, {
104
100
  openUrl: openExternalUrl,
105
101
  routeChannels: m.routeChannels,
106
- stateRoot: resolveStateRoot(dir),
102
+ stateRoot: resolveStateRoot(placement.agentDir),
107
103
  });
108
104
  }
109
105
  });
@@ -141,9 +137,9 @@ export async function runDevSupervisor(dir, options = {}) {
141
137
  };
142
138
  // chokidar gives reliable cross-platform recursion + structural ignore that native fs.watch
143
139
  // cannot; devWatchIgnored (above) narrows the scope to the process-bound code inputs.
144
- const watcher = watchTree(dir, {
140
+ const watcher = watchTree(placement.agentDir, {
145
141
  ignoreInitial: true, // the startup scan is not a change
146
- ignored: devWatchIgnored(dir, agentDir),
142
+ ignored: devWatchIgnored(placement.agentDir, dotEnvPath(placement.agentDir)),
147
143
  });
148
144
  watcher.on("all", () => {
149
145
  clearTimeout(timer);
@@ -151,6 +147,12 @@ export async function runDevSupervisor(dir, options = {}) {
151
147
  });
152
148
  watcher.on("error", (error) => log.warn(`[fastagent] file watching error (${error.message}); some edits may need a manual restart`));
153
149
  log.info(`[fastagent] watching ${WATCHED_HINT} — code edits restart the dev worker (--no-watch to disable); AGENTS.md/persona.md/skills edits go live next turn without a restart`);
150
+ // FASTAGENT_SECRETS_DIR can move the `.env` OUT of the agent dir entirely; the watcher follows it
151
+ // anywhere inside (the resolved path is allow-listed above), but outside the watch root the worker
152
+ // would load a file no watcher sees. Say so once instead of leaving the hint above lying.
153
+ if (!isUnderDir(dotEnvPath(placement.agentDir), placement.agentDir)) {
154
+ log.warn(`[fastagent] .env lives outside the agent dir (FASTAGENT_SECRETS_DIR → ${dotEnvPath(placement.agentDir)}) — it is NOT watched; restart dev after editing it`);
155
+ }
154
156
  const shutdown = () => {
155
157
  worker?.kill("SIGTERM");
156
158
  tunnel?.close();
@@ -1,11 +1,12 @@
1
1
  import type { CredentialStore } from "@earendil-works/pi-ai";
2
2
  /**
3
- * The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`). The project-level default is
4
- * `<dir>/.fastagent/auth.json` (computed by the opener and by `fastagent login`); this is only the
5
- * `loginFlow()` PROGRAMMATIC fallback (when a caller omits `authPath`) and the path to point
6
- * `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE credential file across projects
7
- * (safe one file, one lock-serialized refresh lifecycle). The `fastagent login` CLI is project-
8
- * level by default, never this.
3
+ * The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery
4
+ * home `~/.fastagent/` which carries the same unified shape as a workspace (`.secrets/auth.json`).
5
+ * The project-level default is `<agentDir>/.secrets/auth.json` (computed by the opener and by
6
+ * `fastagent login`); this is only the `loginFlow()` PROGRAMMATIC fallback (when a caller omits
7
+ * `authPath`) and the path to point `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE
8
+ * credential file across projects (safe — one file, one lock-serialized refresh lifecycle). The
9
+ * `fastagent login` CLI is project-level by default, never this.
9
10
  */
10
11
  export declare const GLOBAL_AUTH_PATH: string;
11
12
  export interface FastagentAuthOptions {
@@ -13,5 +14,5 @@ export interface FastagentAuthOptions {
13
14
  warn?: (message: string) => void;
14
15
  }
15
16
  /** A read-write `CredentialStore` backed by the given credentials file (default {@link GLOBAL_AUTH_PATH};
16
- * the directory opener passes the project-level `<dir>/.fastagent/auth.json`). */
17
+ * the directory opener passes the project-level `<root>/.secrets/auth.json`). */
17
18
  export declare function fastagentCredentialStore(authPath?: string, options?: FastagentAuthOptions): CredentialStore;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Auth for the pi engine: a read-WRITE {@link CredentialStore} over a fastagent credentials file,
3
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.
4
+ * (`<agentDir>/.secrets/auth.json`, resolved by the opener); {@link GLOBAL_AUTH_PATH} is the
5
+ * global location used as the override target / login default, not an implicit per-provider fallback.
6
6
  *
7
7
  * Project-level default + NO implicit project↔global fallback, for two reasons: (1) isolation — each
8
8
  * agent can use a different account/subscription; (2) fail-visibly — a missing credential surfaces at
@@ -27,18 +27,20 @@
27
27
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
28
28
  import { homedir } from "node:os";
29
29
  import { dirname, join } from "node:path";
30
+ import { GLOBAL_HOME_DIR, SECRETS_DIRNAME } from "../../paths.js";
30
31
  import { log } from "../../log.js";
31
32
  import { setTimeout as sleep } from "node:timers/promises";
32
33
  import lockfile from "proper-lockfile";
33
34
  /**
34
- * The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`). The project-level default is
35
- * `<dir>/.fastagent/auth.json` (computed by the opener and by `fastagent login`); this is only the
36
- * `loginFlow()` PROGRAMMATIC fallback (when a caller omits `authPath`) and the path to point
37
- * `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE credential file across projects
38
- * (safe one file, one lock-serialized refresh lifecycle). The `fastagent login` CLI is project-
39
- * level by default, never this.
35
+ * The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery
36
+ * home `~/.fastagent/` which carries the same unified shape as a workspace (`.secrets/auth.json`).
37
+ * The project-level default is `<agentDir>/.secrets/auth.json` (computed by the opener and by
38
+ * `fastagent login`); this is only the `loginFlow()` PROGRAMMATIC fallback (when a caller omits
39
+ * `authPath`) and the path to point `--auth-path`/`FASTAGENT_AUTH_PATH` at to deliberately share ONE
40
+ * credential file across projects (safe — one file, one lock-serialized refresh lifecycle). The
41
+ * `fastagent login` CLI is project-level by default, never this.
40
42
  */
41
- export const GLOBAL_AUTH_PATH = join(homedir(), ".fastagent", "auth.json");
43
+ export const GLOBAL_AUTH_PATH = join(homedir(), GLOBAL_HOME_DIR, SECRETS_DIRNAME, "auth.json");
42
44
  /** A valid stored credential, or undefined — a foreign/old entry reads as not-configured, not a crash. */
43
45
  function pick(creds, providerId) {
44
46
  const cred = creds[providerId];
@@ -183,7 +185,7 @@ function parseForWrite(raw, where) {
183
185
  return creds;
184
186
  }
185
187
  /** A read-write `CredentialStore` backed by the given credentials file (default {@link GLOBAL_AUTH_PATH};
186
- * the directory opener passes the project-level `<dir>/.fastagent/auth.json`). */
188
+ * the directory opener passes the project-level `<root>/.secrets/auth.json`). */
187
189
  export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options = {}) {
188
190
  const warn = options.warn ?? ((message) => log.warn(message));
189
191
  return {
@@ -23,7 +23,7 @@ export declare function inspectChannels(dir: string): Promise<{
23
23
  }>;
24
24
  /**
25
25
  * Channel file basenames under `<dir>/channels/` — the authoring view (`fastagent info`), which lists
26
- * WITHOUT importing. A symlinked channels directory must remain inside the workspace.
26
+ * WITHOUT importing. A symlinked channels directory must remain inside the agent dir.
27
27
  */
28
28
  export declare function discoverChannelFiles(dir: string): Promise<string[]>;
29
29
  /** Discover, validate, and bind all channel modules. No long connection is opened here; the CLI owns it. */
@@ -6,7 +6,7 @@ import { readdir } from "node:fs/promises";
6
6
  import { isAbsolute, join } from "node:path";
7
7
  import { parseRouteKey, } from "../../host/node.js";
8
8
  import { isModuleFile, loadModuleDir } from "../../loader.js";
9
- import { assertInsideWorkspace } from "../../workspace.js";
9
+ import { assertInsideAgentDir } from "../../paths.js";
10
10
  function longConnectionModule(value) {
11
11
  return value !== null && typeof value === "object" && typeof value.connect === "function";
12
12
  }
@@ -21,7 +21,7 @@ function validateLongConnectionModule(value, label) {
21
21
  * long-connection channels. There is no second ingress/lifecycle declaration to keep in sync.
22
22
  */
23
23
  export async function inspectChannels(dir) {
24
- await assertInsideWorkspace(dir, "channels");
24
+ await assertInsideAgentDir(dir, "channels");
25
25
  const { modules, failures } = await loadModuleDir(join(dir, "channels"));
26
26
  const channels = [];
27
27
  const routeChannels = [];
@@ -49,10 +49,10 @@ export async function inspectChannels(dir) {
49
49
  }
50
50
  /**
51
51
  * Channel file basenames under `<dir>/channels/` — the authoring view (`fastagent info`), which lists
52
- * WITHOUT importing. A symlinked channels directory must remain inside the workspace.
52
+ * WITHOUT importing. A symlinked channels directory must remain inside the agent dir.
53
53
  */
54
54
  export async function discoverChannelFiles(dir) {
55
- await assertInsideWorkspace(dir, "channels");
55
+ await assertInsideAgentDir(dir, "channels");
56
56
  let names;
57
57
  try {
58
58
  names = await readdir(join(dir, "channels"));
@@ -90,7 +90,7 @@ export async function loadChannels(dir, ctx) {
90
90
  if (!isAbsolute(ctx.stateRoot)) {
91
91
  throw new Error(`ChannelContext.stateRoot must be absolute, got "${ctx.stateRoot}"`);
92
92
  }
93
- await assertInsideWorkspace(dir, "channels");
93
+ await assertInsideAgentDir(dir, "channels");
94
94
  const { modules, failures } = await loadModuleDir(join(dir, "channels"));
95
95
  const routes = {};
96
96
  const longConnections = [];
@@ -8,13 +8,13 @@
8
8
  * pi's TUI `/login`, which writes through the same credential store into the workspace auth file).
9
9
  */
10
10
  import { InteractiveMode } from "@earendil-works/pi-coding-agent";
11
- import { buildWorkspaceSessionRuntime } from "./session-builder.js";
11
+ import { buildAgentSessionRuntime } from "./session-builder.js";
12
12
  /**
13
13
  * Open the workspace's agent in pi's interactive TUI and run until the user exits. The agent is
14
14
  * fastagent's assembled agent (same model/tools/skills/prompt/auth as dev/start serve); pi's TUI
15
15
  * handles login, rendering, and same-workspace sessions natively.
16
16
  */
17
17
  export async function runPiChat(dir, options = {}) {
18
- const runtime = await buildWorkspaceSessionRuntime(dir, options);
18
+ const runtime = await buildAgentSessionRuntime(dir, options);
19
19
  await new InteractiveMode(runtime, {}).run();
20
20
  }
@@ -9,14 +9,6 @@ export interface FastagentConfig {
9
9
  * "xhigh" | "max"). Unset = pi's default. Authors tune thinking in the pi TUI while vibing — this
10
10
  * is the serving-side counterpart (fidelity). Levels a model doesn't support are clamped by pi. */
11
11
  thinkingLevel?: ThinkingLevel;
12
- /**
13
- * The agent-definition subdirectory (persona.md, skills/, tools/, channels/), relative to the config
14
- * file's directory. Default: the config directory itself (flat — today's behaviour). Point it at a
15
- * sibling like `"./agent"` to serve an existing repo as a coding agent: the config dir stays the run
16
- * root (cwd, whose AGENTS.md the agent reads as ② context), while the agent's own surface lives in the
17
- * subdir and does not collide with the host's `tools/`/`src/` (core.md scenario grid).
18
- */
19
- agentDir?: string;
20
12
  /** Extra custom tools, appended after pi defaults — never replaces them. `FastagentTool` = AgentTool
21
13
  * plus the optional `deferred` marker (see defineTool). */
22
14
  tools?: FastagentTool[];
@@ -53,24 +45,13 @@ export interface FastagentConfig {
53
45
  export declare function defineConfig(config: FastagentConfig): FastagentConfig;
54
46
  export interface LoadedConfig {
55
47
  config: FastagentConfig;
56
- /** Config file path; undefined when running zero-config. */
48
+ /** Config file path; undefined when the loader was pointed at a directory holding none. */
57
49
  path?: string;
58
50
  }
59
51
  /** A valid bindable port. */
60
52
  export declare function isValidPort(n: number): boolean;
61
- /** The config filenames that make a directory a fastagent workspace, in load precedence. ONE source: the
62
- * loader (below) and `scaffoldWorkspace`'s already-a-workspace refusal both read this, so "is there a
63
- * config?" can't diverge between them when the set changes. */
64
- export declare const WORKSPACE_CONFIG_NAMES: readonly ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
65
- /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = zero-config; a wrong-shape file throws. */
53
+ /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = defaults; a wrong-shape file throws. */
66
54
  export declare function loadConfig(dir: string): Promise<LoadedConfig>;
67
- /**
68
- * The agent-definition dir from config: `config.agentDir` resolved against `dir`, or `dir` itself when
69
- * unset (flat). The ONE place this is computed — every opener (`dev`/`start`/`info`/`tool`/`deploy`/`chat`)
70
- * calls it, so the "relative to the config dir, default `.`" rule can never diverge. loadConfig has
71
- * already validated that agentDir stays under `dir`.
72
- */
73
- export declare function resolveAgentDir(dir: string, config: FastagentConfig): string;
74
55
  /** The provider prefix of a "provider/modelId" spec. A spec without "/" returns whole — downstream
75
56
  * lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
76
57
  * (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
@@ -82,7 +63,7 @@ export declare function listModels(models: Models): string[];
82
63
  /**
83
64
  * Rewrite the `model` in a config file's SOURCE TEXT to `spec`, for the first-run picker's write-back.
84
65
  * Handles the scaffold's commented placeholder (`// model: "…"`) and an existing `model:` line; returns
85
- * null when neither is present (zero-config or a hand-shaped config) so the caller falls back to a
66
+ * null when neither is present (no config file, or a hand-shaped one) so the caller falls back to a
86
67
  * printed hint instead of guessing where to insert. Text-level (not AST) on purpose — it only ever
87
68
  * touches a line it recognizes, never reformats the author's file.
88
69
  */
@@ -95,30 +76,9 @@ export declare function resolveModelSpec(flag: string | undefined, config: Fasta
95
76
  * Resolved to absolute so the store and the startup report agree regardless of cwd.
96
77
  */
97
78
  export declare function resolveSessionsDirOverride(flag: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
98
- /**
99
- * The auth-file override: `--auth-path` flag > `FASTAGENT_AUTH_PATH` env > undefined (the opener then
100
- * falls back to {@link defaultAuthPath} under the {@link resolveStateRoot} root). Resolved to absolute
101
- * so the store and the startup report agree regardless of cwd. No implicit project↔global fallback (isolation
102
- * + fail-visibly; see auth.ts); to share one account across projects, point this at the global
103
- * `~/.fastagent/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
104
- */
105
- export declare function resolveAuthPathOverride(flag: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
106
- /**
107
- * The resolved state root — the ONE durable machine-state home everything derives from (auth.json,
108
- * sessions/, channels/<kind>/): `FASTAGENT_STATE_DIR` env > `<dir>/.fastagent`. Absolute, so channels
109
- * and the startup report agree regardless of cwd. Definition: single lifecycle (precious, survives
110
- * redeploy), single process — a container mounts ONE volume here. The finer knobs
111
- * (`FASTAGENT_SESSIONS_DIR`, `FASTAGENT_AUTH_PATH`) still override their specific path on top.
112
- *
113
- * `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
114
- * — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
115
- * DEFAULT (`<dir>/.fastagent`) is dir-anchored. Deployments set an absolute path (a mounted volume);
116
- * a relative value is in-tree — hence self-ignored — only when run from the definition dir (cwd == dir).
117
- */
118
- export declare function resolveStateRoot(dir: string, env?: NodeJS.ProcessEnv): string;
119
- /** The default credentials file under a resolved state root ({@link resolveStateRoot}). */
120
- export declare function defaultAuthPath(stateRoot: string): string;
121
- /** The effective auth file for a workspace: override if present, else the project-level auth.json. */
79
+ /** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
80
+ export declare function defaultAuthPath(secretsDir: string): string;
81
+ /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
122
82
  export declare function resolveAuthPath(dir: string, flag: string | undefined, env?: NodeJS.ProcessEnv): string;
123
83
  /** The default sessions dir under a resolved state root ({@link resolveStateRoot}). */
124
84
  export declare function defaultSessionsDir(stateRoot: string): string;
@@ -11,15 +11,18 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
11
11
  * (resolveModel, resolveModelSpec). One concern: everything about fastagent.config.ts.
12
12
  *
13
13
  * Red line: config describes deployment/runtime choices, never authored identity or expertise (those
14
- * live in persona.md + skills, with AGENTS.md as project context). Deleting the config still leaves a
15
- * zero-config agent runnable with a model supplied by --model / FASTAGENT_MODEL.
14
+ * live in persona.md + skills, with AGENTS.md as project context). It has no say in PLACEMENT either —
15
+ * that rule lives in the neutral src/paths.ts — but its FILE is what marks a directory as an agent, so a
16
+ * served agent always has one (its contents may be `export default {}`; a model can still come from
17
+ * --model / FASTAGENT_MODEL). The loader below stays tolerant of a missing file because it is a loader:
18
+ * callers that have not resolved a placement (deploy inspection, tests) may point it anywhere.
16
19
  */
17
- import { existsSync, lstatSync, realpathSync, statSync } from "node:fs";
18
- import { homedir } from "node:os";
19
- import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
20
+ import { existsSync, statSync } from "node:fs";
21
+ import { basename, join } from "node:path";
20
22
  import { pathToFileURL } from "node:url";
21
23
  import { THINKING_LEVELS } from "./harness.js";
22
24
  import { moduleLoadHint } from "../../loader.js";
25
+ import { AGENT_CONFIG_NAMES, resolveOverridePath, resolveSecretsDir } from "../../paths.js";
23
26
  /** Identity function for typing and IDE completion (vite/next-style). */
24
27
  export function defineConfig(config) {
25
28
  return config;
@@ -41,13 +44,9 @@ function validateStringList(value, key, shape, desc, path) {
41
44
  }
42
45
  }
43
46
  }
44
- /** The config filenames that make a directory a fastagent workspace, in load precedence. ONE source: the
45
- * loader (below) and `scaffoldWorkspace`'s already-a-workspace refusal both read this, so "is there a
46
- * config?" can't diverge between them when the set changes. */
47
- export const WORKSPACE_CONFIG_NAMES = ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
48
- /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = zero-config; a wrong-shape file throws. */
47
+ /** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = defaults; a wrong-shape file throws. */
49
48
  export async function loadConfig(dir) {
50
- const found = WORKSPACE_CONFIG_NAMES.map((name) => join(dir, name)).filter((path) => existsSync(path));
49
+ const found = AGENT_CONFIG_NAMES.map((name) => join(dir, name)).filter((path) => existsSync(path));
51
50
  if (found.length === 0)
52
51
  return { config: {} };
53
52
  if (found.length > 1) {
@@ -75,17 +74,16 @@ export async function loadConfig(dir) {
75
74
  }
76
75
  const c = config;
77
76
  // Unknown keys throw: defineConfig only type-protects .ts authors; a typo in a .js/.mjs config
78
- // (`modle:`) must not silently degrade to zero-config.
77
+ // (`modle:`) must not silently degrade to defaults.
79
78
  for (const key of Object.keys(c)) {
80
79
  if (key !== "model" &&
81
80
  key !== "thinkingLevel" &&
82
- key !== "agentDir" &&
83
81
  key !== "tools" &&
84
82
  key !== "http" &&
85
83
  key !== "deploy" &&
86
84
  key !== "selfSchedule" &&
87
85
  key !== "sessionControl") {
88
- throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel, agentDir, tools, http, deploy, selfSchedule, sessionControl)`);
86
+ throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel, tools, http, deploy, selfSchedule, sessionControl)`);
89
87
  }
90
88
  }
91
89
  if (c.model !== undefined && typeof c.model !== "string") {
@@ -97,47 +95,6 @@ export async function loadConfig(dir) {
97
95
  if (c.thinkingLevel !== undefined && !THINKING_LEVELS.has(c.thinkingLevel)) {
98
96
  throw new Error(`${path}: "thinkingLevel" must be one of ${[...THINKING_LEVELS].join(", ")}`);
99
97
  }
100
- if (c.agentDir !== undefined && typeof c.agentDir !== "string") {
101
- throw new Error(`${path}: "agentDir" must be a string (a subdirectory relative to the config file)`);
102
- }
103
- if (typeof c.agentDir === "string") {
104
- // Enforce the documented "subdirectory of the config dir" contract: an escaping agentDir (e.g.
105
- // "../shared") would still resolve for tool/channel/persona discovery, but `dev`'s chokidar only
106
- // watches the config dir subtree — edits outside it would silently never trigger a restart. Reject
107
- // it here (fail visibly) rather than let hot-reload break without a signal.
108
- const rel = relative(dir, resolve(dir, c.agentDir));
109
- if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
110
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") must be a subdirectory of the config directory, not escape it`);
111
- }
112
- // An explicitly declared agentDir that doesn't exist is a typo until proven otherwise ("./agnet"):
113
- // without this check every opener would assemble an EMPTY agent (no persona, no skills, no tools)
114
- // with zero errors — the worst silent failure this config can produce. Deliberately NOT auto-created:
115
- // config load is read-only (no implicit operations), and a mkdir would turn the typo into a served
116
- // empty agent plus a junk directory.
117
- // lstat, not stat: a symlink would pass the literal containment check above while its TARGET lives
118
- // outside the config dir — exactly what that check exists to prevent (dev's watch would silently
119
- // never see edits). Same rule as init's parent preflight: reject, don't follow.
120
- const agentDirAbs = resolve(dir, c.agentDir);
121
- const st = lstatSync(agentDirAbs, { throwIfNoEntry: false });
122
- if (!st) {
123
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") does not exist — create it, or fix the path`);
124
- }
125
- if (st.isSymbolicLink()) {
126
- // Separate message: to its user a symlink LOOKS like a working directory — name the reason and the fix.
127
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") is a symlink — not allowed (its target can live outside the ` +
128
- `config directory, where dev's watch would never see edits); use a real directory, or point agentDir at the target's real path`);
129
- }
130
- if (!st.isDirectory()) {
131
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") is not a directory`);
132
- }
133
- // The leaf lstat can't see a symlinked INTERMEDIATE segment (agentDir "./a/b" with `a` → outside):
134
- // realpath equality covers every segment under the config dir in one comparison. dir itself is
135
- // realpath'd on both sides, so a symlinked config-dir path (macOS /tmp) stays legal.
136
- if (realpathSync(agentDirAbs) !== resolve(realpathSync(dir), relative(dir, agentDirAbs))) {
137
- throw new Error(`${path}: "agentDir" ("${c.agentDir}") resolves through a symlink — not allowed (the target can ` +
138
- `live outside the config directory, where dev's watch would never see edits); use the real path`);
139
- }
140
- }
141
98
  if (c.selfSchedule !== undefined && typeof c.selfSchedule !== "boolean") {
142
99
  throw new Error(`${path}: "selfSchedule" must be a boolean`);
143
100
  }
@@ -181,15 +138,6 @@ export async function loadConfig(dir) {
181
138
  validateStringList(c.deploy?.apt, "deploy.apt", /^[a-z0-9][a-z0-9.+-]*$/, "a Debian package name", path);
182
139
  return { config: c, path };
183
140
  }
184
- /**
185
- * The agent-definition dir from config: `config.agentDir` resolved against `dir`, or `dir` itself when
186
- * unset (flat). The ONE place this is computed — every opener (`dev`/`start`/`info`/`tool`/`deploy`/`chat`)
187
- * calls it, so the "relative to the config dir, default `.`" rule can never diverge. loadConfig has
188
- * already validated that agentDir stays under `dir`.
189
- */
190
- export function resolveAgentDir(dir, config) {
191
- return resolve(dir, config.agentDir ?? ".");
192
- }
193
141
  /** The provider prefix of a "provider/modelId" spec. A spec without "/" returns whole — downstream
194
142
  * lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
195
143
  * (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
@@ -223,7 +171,7 @@ export function listModels(models) {
223
171
  /**
224
172
  * Rewrite the `model` in a config file's SOURCE TEXT to `spec`, for the first-run picker's write-back.
225
173
  * Handles the scaffold's commented placeholder (`// model: "…"`) and an existing `model:` line; returns
226
- * null when neither is present (zero-config or a hand-shaped config) so the caller falls back to a
174
+ * null when neither is present (no config file, or a hand-shaped one) so the caller falls back to a
227
175
  * printed hint instead of guessing where to insert. Text-level (not AST) on purpose — it only ever
228
176
  * touches a line it recognizes, never reformats the author's file.
229
177
  */
@@ -248,19 +196,6 @@ export function rewriteConfigModel(src, spec) {
248
196
  export function resolveModelSpec(flag, config, env = process.env) {
249
197
  return flag ?? env.FASTAGENT_MODEL ?? config.model;
250
198
  }
251
- /**
252
- * Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
253
- * leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
254
- * never gets the shell's `~` expansion, so a bare `resolve("~/x")` would silently create a literal `~`
255
- * directory — a fail-silently footgun for a secret/state path. Expanding here makes `~` mean home
256
- * everywhere these knobs are read.
257
- */
258
- function resolveOverridePath(raw) {
259
- if (!raw)
260
- return undefined;
261
- const expanded = raw === "~" ? homedir() : raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw;
262
- return resolve(expanded);
263
- }
264
199
  /**
265
200
  * `start`'s sessions-dir override: `--sessions-dir` flag > `FASTAGENT_SESSIONS_DIR` env > undefined
266
201
  * (the opener then falls back to {@link defaultSessionsDir} under the {@link resolveStateRoot} root).
@@ -271,43 +206,21 @@ export function resolveSessionsDirOverride(flag, env = process.env) {
271
206
  }
272
207
  /**
273
208
  * The auth-file override: `--auth-path` flag > `FASTAGENT_AUTH_PATH` env > undefined (the opener then
274
- * falls back to {@link defaultAuthPath} under the {@link resolveStateRoot} root). Resolved to absolute
209
+ * falls back to {@link defaultAuthPath} under the {@link resolveSecretsDir} dir). Resolved to absolute
275
210
  * so the store and the startup report agree regardless of cwd. No implicit project↔global fallback (isolation
276
211
  * + fail-visibly; see auth.ts); to share one account across projects, point this at the global
277
- * `~/.fastagent/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
212
+ * `~/.fastagent/.secrets/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
278
213
  */
279
- export function resolveAuthPathOverride(flag, env = process.env) {
214
+ function resolveAuthPathOverride(flag, env = process.env) {
280
215
  return resolveOverridePath(flag ?? env.FASTAGENT_AUTH_PATH);
281
216
  }
282
- /**
283
- * The IN-TREE default state root, `<dir>/.fastagent` — what {@link resolveStateRoot} falls back to when
284
- * `FASTAGENT_STATE_DIR` moves state nowhere. THE single definition of that path segment.
285
- */
286
- function projectStateDir(dir) {
287
- return join(dir, ".fastagent");
288
- }
289
- /**
290
- * The resolved state root — the ONE durable machine-state home everything derives from (auth.json,
291
- * sessions/, channels/<kind>/): `FASTAGENT_STATE_DIR` env > `<dir>/.fastagent`. Absolute, so channels
292
- * and the startup report agree regardless of cwd. Definition: single lifecycle (precious, survives
293
- * redeploy), single process — a container mounts ONE volume here. The finer knobs
294
- * (`FASTAGENT_SESSIONS_DIR`, `FASTAGENT_AUTH_PATH`) still override their specific path on top.
295
- *
296
- * `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
297
- * — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
298
- * DEFAULT (`<dir>/.fastagent`) is dir-anchored. Deployments set an absolute path (a mounted volume);
299
- * a relative value is in-tree — hence self-ignored — only when run from the definition dir (cwd == dir).
300
- */
301
- export function resolveStateRoot(dir, env = process.env) {
302
- return resolveOverridePath(env.FASTAGENT_STATE_DIR) ?? resolve(projectStateDir(dir));
303
- }
304
- /** The default credentials file under a resolved state root ({@link resolveStateRoot}). */
305
- export function defaultAuthPath(stateRoot) {
306
- return join(stateRoot, "auth.json");
217
+ /** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
218
+ export function defaultAuthPath(secretsDir) {
219
+ return join(secretsDir, "auth.json");
307
220
  }
308
- /** The effective auth file for a workspace: override if present, else the project-level auth.json. */
221
+ /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
309
222
  export function resolveAuthPath(dir, flag, env = process.env) {
310
- return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(resolveStateRoot(dir, env));
223
+ return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(resolveSecretsDir(dir, env));
311
224
  }
312
225
  /** The default sessions dir under a resolved state root ({@link resolveStateRoot}). */
313
226
  export function defaultSessionsDir(stateRoot) {