@fastagent-sh/fastagent 0.21.1 → 0.22.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 (388) hide show
  1. package/README.md +9 -12
  2. package/dist/agent.d.ts +24 -37
  3. package/dist/agent.js +7 -15
  4. package/dist/atomic-write.d.ts +13 -17
  5. package/dist/atomic-write.js +18 -29
  6. package/dist/bind.d.ts +12 -22
  7. package/dist/bind.js +18 -27
  8. package/dist/channel.d.ts +11 -33
  9. package/dist/channels/agentcore-limits.d.ts +2 -13
  10. package/dist/channels/agentcore-limits.js +2 -13
  11. package/dist/channels/agentcore-protocol.d.ts +67 -58
  12. package/dist/channels/agentcore-protocol.js +23 -15
  13. package/dist/channels/agentcore-service.d.ts +19 -33
  14. package/dist/channels/agentcore-service.js +176 -59
  15. package/dist/channels/agentcore.d.ts +39 -33
  16. package/dist/channels/agentcore.js +90 -224
  17. package/dist/channels/body.d.ts +15 -2
  18. package/dist/channels/body.js +24 -2
  19. package/dist/channels/busy.d.ts +1 -21
  20. package/dist/channels/busy.js +2 -39
  21. package/dist/channels/control.d.ts +17 -62
  22. package/dist/channels/control.js +74 -182
  23. package/dist/channels/define-channel.d.ts +39 -0
  24. package/dist/channels/define-channel.js +9 -0
  25. package/dist/channels/discover.d.ts +10 -31
  26. package/dist/channels/discover.js +37 -31
  27. package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
  28. package/dist/channels/feishu/bootstrap-token.js +7 -23
  29. package/dist/channels/feishu/card.d.ts +14 -28
  30. package/dist/channels/feishu/card.js +16 -30
  31. package/dist/channels/feishu/cloud.d.ts +1 -5
  32. package/dist/channels/feishu/context-buffer.d.ts +4 -23
  33. package/dist/channels/feishu/context-buffer.js +8 -31
  34. package/dist/channels/feishu/crypto.d.ts +10 -5
  35. package/dist/channels/feishu/crypto.js +12 -19
  36. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  37. package/dist/channels/feishu/feishu-api.js +5 -4
  38. package/dist/channels/feishu/feishu.d.ts +4 -10
  39. package/dist/channels/feishu/feishu.js +113 -227
  40. package/dist/channels/feishu/invoke-turn.d.ts +5 -4
  41. package/dist/channels/feishu/invoke-turn.js +36 -54
  42. package/dist/channels/feishu/model.d.ts +4 -21
  43. package/dist/channels/feishu/model.js +1 -8
  44. package/dist/channels/feishu/normalize.d.ts +2 -10
  45. package/dist/channels/feishu/normalize.js +7 -42
  46. package/dist/channels/feishu/parse.d.ts +9 -29
  47. package/dist/channels/feishu/parse.js +8 -26
  48. package/dist/channels/feishu/preview.d.ts +12 -14
  49. package/dist/channels/feishu/preview.js +108 -207
  50. package/dist/channels/feishu/register-app.d.ts +8 -23
  51. package/dist/channels/feishu/register-app.js +7 -20
  52. package/dist/channels/feishu/register-webhook.d.ts +2 -9
  53. package/dist/channels/feishu/register-webhook.js +9 -33
  54. package/dist/channels/feishu/scaffold/channel.ts +27 -19
  55. package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
  56. package/dist/channels/feishu/setup-mode.d.ts +11 -36
  57. package/dist/channels/feishu/setup-mode.js +7 -26
  58. package/dist/channels/feishu/ws-ingress.d.ts +1 -2
  59. package/dist/channels/feishu/ws-ingress.js +6 -8
  60. package/dist/channels/http.d.ts +15 -15
  61. package/dist/channels/http.js +19 -9
  62. package/dist/channels/kit/attachment-path.d.ts +4 -5
  63. package/dist/channels/kit/attachment-path.js +10 -34
  64. package/dist/channels/kit/context-buffer.d.ts +14 -21
  65. package/dist/channels/kit/context-buffer.js +25 -38
  66. package/dist/channels/kit/delivery.d.ts +35 -0
  67. package/dist/channels/kit/delivery.js +129 -0
  68. package/dist/channels/kit/event-stream.d.ts +5 -0
  69. package/dist/channels/kit/event-stream.js +29 -0
  70. package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
  71. package/dist/channels/kit/invoke-turn-kit.js +89 -32
  72. package/dist/channels/kit/preview-kit.d.ts +18 -72
  73. package/dist/channels/kit/preview-kit.js +22 -88
  74. package/dist/channels/kit/signature.d.ts +3 -9
  75. package/dist/channels/kit/signature.js +3 -9
  76. package/dist/channels/kit/state.d.ts +1 -2
  77. package/dist/channels/kit/state.js +5 -14
  78. package/dist/channels/kit/stop-command.d.ts +1 -2
  79. package/dist/channels/kit/stop-command.js +6 -9
  80. package/dist/channels/kit/tasks.d.ts +1 -1
  81. package/dist/channels/kit/tasks.js +8 -21
  82. package/dist/channels/kit/text.d.ts +2 -16
  83. package/dist/channels/kit/text.js +23 -31
  84. package/dist/channels/kit/thread-participants.d.ts +4 -17
  85. package/dist/channels/kit/thread-participants.js +8 -55
  86. package/dist/channels/kit/transport.d.ts +26 -0
  87. package/dist/channels/kit/transport.js +20 -0
  88. package/dist/channels/kit/turn-queue.d.ts +7 -11
  89. package/dist/channels/kit/turn-queue.js +21 -39
  90. package/dist/channels/kit/turn-runner.d.ts +32 -18
  91. package/dist/channels/kit/turn-runner.js +98 -53
  92. package/dist/channels/kit/turn-store.d.ts +32 -32
  93. package/dist/channels/kit/turn-store.js +45 -88
  94. package/dist/channels/lark/lark.d.ts +1 -5
  95. package/dist/channels/lark/onboard.d.ts +3 -12
  96. package/dist/channels/lark/onboard.js +3 -13
  97. package/dist/channels/lark/scaffold/channel.ts +27 -19
  98. package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
  99. package/dist/channels/registration.d.ts +9 -35
  100. package/dist/channels/registration.js +6 -37
  101. package/dist/channels/secret.js +2 -5
  102. package/dist/channels/serve.d.ts +47 -44
  103. package/dist/channels/serve.js +165 -101
  104. package/dist/channels/slack/config-api.d.ts +4 -27
  105. package/dist/channels/slack/config-api.js +5 -34
  106. package/dist/channels/slack/context-buffer.d.ts +2 -3
  107. package/dist/channels/slack/context-buffer.js +5 -5
  108. package/dist/channels/slack/invoke-turn.d.ts +3 -1
  109. package/dist/channels/slack/invoke-turn.js +25 -34
  110. package/dist/channels/slack/manifest.js +1 -3
  111. package/dist/channels/slack/model.d.ts +1 -2
  112. package/dist/channels/slack/model.js +1 -1
  113. package/dist/channels/slack/onboard.d.ts +4 -3
  114. package/dist/channels/slack/onboard.js +8 -11
  115. package/dist/channels/slack/onboarding-state.d.ts +3 -5
  116. package/dist/channels/slack/onboarding-state.js +9 -6
  117. package/dist/channels/slack/parse.d.ts +4 -13
  118. package/dist/channels/slack/parse.js +7 -28
  119. package/dist/channels/slack/preview.d.ts +12 -2
  120. package/dist/channels/slack/preview.js +292 -269
  121. package/dist/channels/slack/reaction.d.ts +11 -3
  122. package/dist/channels/slack/reaction.js +19 -8
  123. package/dist/channels/slack/register-webhook.js +4 -7
  124. package/dist/channels/slack/scaffold/channel.ts +20 -13
  125. package/dist/channels/slack/scaffold/slack-send.ts +2 -2
  126. package/dist/channels/slack/setup-server.d.ts +1 -5
  127. package/dist/channels/slack/setup-server.js +1 -5
  128. package/dist/channels/slack/shared-api.d.ts +1 -4
  129. package/dist/channels/slack/shared-api.js +2 -13
  130. package/dist/channels/slack/slack-api.d.ts +2 -1
  131. package/dist/channels/slack/slack-api.js +6 -4
  132. package/dist/channels/slack/slack.d.ts +11 -14
  133. package/dist/channels/slack/slack.js +121 -122
  134. package/dist/channels/sse.d.ts +1 -1
  135. package/dist/channels/sse.js +1 -1
  136. package/dist/channels/telegram/context-buffer.d.ts +7 -22
  137. package/dist/channels/telegram/context-buffer.js +6 -23
  138. package/dist/channels/telegram/invoke-turn.d.ts +7 -7
  139. package/dist/channels/telegram/invoke-turn.js +24 -43
  140. package/dist/channels/telegram/parse.d.ts +24 -51
  141. package/dist/channels/telegram/parse.js +32 -84
  142. package/dist/channels/telegram/preview.d.ts +10 -1
  143. package/dist/channels/telegram/preview.js +79 -100
  144. package/dist/channels/telegram/register-webhook.js +5 -23
  145. package/dist/channels/telegram/scaffold/channel.ts +25 -18
  146. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
  147. package/dist/channels/telegram/telegram-api.d.ts +4 -3
  148. package/dist/channels/telegram/telegram-api.js +9 -7
  149. package/dist/channels/telegram/telegram.d.ts +4 -18
  150. package/dist/channels/telegram/telegram.js +38 -94
  151. package/dist/channels/telegram/turn-store.d.ts +11 -14
  152. package/dist/channels/telegram/turn-store.js +8 -6
  153. package/dist/channels/wait-health.d.ts +4 -6
  154. package/dist/channels/wait-health.js +10 -14
  155. package/dist/cli/add-feishu.d.ts +2 -12
  156. package/dist/cli/add-feishu.js +24 -71
  157. package/dist/cli/add-slack.d.ts +5 -3
  158. package/dist/cli/add-slack.js +53 -76
  159. package/dist/cli/auth-view.d.ts +2 -16
  160. package/dist/cli/auth-view.js +2 -16
  161. package/dist/cli/commands/add.js +25 -40
  162. package/dist/cli/commands/chat.d.ts +0 -1
  163. package/dist/cli/commands/chat.js +5 -6
  164. package/dist/cli/commands/deploy/agentcore.js +60 -65
  165. package/dist/cli/commands/deploy/docker.js +50 -23
  166. package/dist/cli/commands/deploy/fly.js +23 -61
  167. package/dist/cli/commands/deploy/railway.js +23 -18
  168. package/dist/cli/commands/deploy/shared.d.ts +38 -57
  169. package/dist/cli/commands/deploy/shared.js +40 -59
  170. package/dist/cli/commands/deploy.d.ts +10 -27
  171. package/dist/cli/commands/deploy.js +38 -60
  172. package/dist/cli/commands/destroy.d.ts +4 -0
  173. package/dist/cli/commands/destroy.js +56 -0
  174. package/dist/cli/commands/dev.d.ts +2 -1
  175. package/dist/cli/commands/dev.js +20 -23
  176. package/dist/cli/commands/info.d.ts +0 -2
  177. package/dist/cli/commands/info.js +83 -49
  178. package/dist/cli/commands/init.d.ts +1 -2
  179. package/dist/cli/commands/init.js +16 -55
  180. package/dist/cli/commands/invoke.d.ts +0 -1
  181. package/dist/cli/commands/invoke.js +5 -7
  182. package/dist/cli/commands/login.d.ts +2 -1
  183. package/dist/cli/commands/login.js +33 -38
  184. package/dist/cli/commands/logs.js +3 -4
  185. package/dist/cli/commands/models.js +1 -5
  186. package/dist/cli/commands/routine-run.d.ts +6 -0
  187. package/dist/cli/commands/routine-run.js +47 -0
  188. package/dist/cli/commands/routine.d.ts +25 -0
  189. package/dist/cli/commands/routine.js +122 -0
  190. package/dist/cli/commands/start.d.ts +31 -3
  191. package/dist/cli/commands/start.js +131 -81
  192. package/dist/cli/commands/tool.js +26 -12
  193. package/dist/cli/fail.d.ts +13 -23
  194. package/dist/cli/fail.js +20 -23
  195. package/dist/cli/invoke-stream.d.ts +1 -8
  196. package/dist/cli/kernel.d.ts +6 -38
  197. package/dist/cli/kernel.js +14 -77
  198. package/dist/cli/models-view.d.ts +1 -9
  199. package/dist/cli/models-view.js +11 -22
  200. package/dist/cli/program.d.ts +5 -6
  201. package/dist/cli/program.js +120 -157
  202. package/dist/cli/serve.d.ts +57 -41
  203. package/dist/cli/serve.js +94 -121
  204. package/dist/cli/shared.d.ts +15 -61
  205. package/dist/cli/shared.js +55 -181
  206. package/dist/cli.js +1 -8
  207. package/dist/collect.d.ts +6 -0
  208. package/dist/collect.js +6 -0
  209. package/dist/core.d.ts +4 -1
  210. package/dist/core.js +9 -7
  211. package/dist/declared-secrets.d.ts +55 -0
  212. package/dist/declared-secrets.js +88 -0
  213. package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
  214. package/dist/deploy/agentcore/aws-cli.js +64 -0
  215. package/dist/deploy/agentcore/destroy.d.ts +51 -0
  216. package/dist/deploy/agentcore/destroy.js +273 -0
  217. package/dist/deploy/agentcore/forwarder.js +28 -79
  218. package/dist/deploy/agentcore/logs.d.ts +2 -18
  219. package/dist/deploy/agentcore/logs.js +27 -51
  220. package/dist/deploy/agentcore/plan.d.ts +85 -105
  221. package/dist/deploy/agentcore/plan.js +152 -314
  222. package/dist/deploy/agentcore/run.d.ts +28 -28
  223. package/dist/deploy/agentcore/run.js +227 -306
  224. package/dist/deploy/agentcore/zip.d.ts +2 -15
  225. package/dist/deploy/agentcore/zip.js +3 -17
  226. package/dist/deploy/build-context.d.ts +21 -0
  227. package/dist/deploy/build-context.js +137 -0
  228. package/dist/deploy/channel-ingress.d.ts +26 -39
  229. package/dist/deploy/channel-ingress.js +57 -28
  230. package/dist/deploy/container.d.ts +20 -34
  231. package/dist/deploy/container.js +49 -100
  232. package/dist/deploy/docker/plan.d.ts +12 -11
  233. package/dist/deploy/docker/plan.js +43 -24
  234. package/dist/deploy/docker/run.d.ts +22 -25
  235. package/dist/deploy/docker/run.js +39 -37
  236. package/dist/deploy/fly/plan.d.ts +15 -44
  237. package/dist/deploy/fly/plan.js +41 -73
  238. package/dist/deploy/fly/run.d.ts +16 -51
  239. package/dist/deploy/fly/run.js +42 -95
  240. package/dist/deploy/hosts.d.ts +24 -3
  241. package/dist/deploy/hosts.js +24 -3
  242. package/dist/deploy/preflight.d.ts +47 -41
  243. package/dist/deploy/preflight.js +282 -383
  244. package/dist/deploy/railway/plan.d.ts +16 -42
  245. package/dist/deploy/railway/plan.js +42 -60
  246. package/dist/deploy/railway/run.d.ts +30 -68
  247. package/dist/deploy/railway/run.js +86 -100
  248. package/dist/deploy/registration-gate.d.ts +6 -13
  249. package/dist/deploy/residency.d.ts +37 -0
  250. package/dist/deploy/residency.js +24 -0
  251. package/dist/deploy/runner.d.ts +10 -9
  252. package/dist/deploy/runner.js +7 -18
  253. package/dist/deploy/secrets.d.ts +40 -41
  254. package/dist/deploy/secrets.js +117 -103
  255. package/dist/deploy/workspace.d.ts +50 -0
  256. package/dist/deploy/workspace.js +197 -0
  257. package/dist/dev-supervisor.d.ts +1 -9
  258. package/dist/dev-supervisor.js +21 -64
  259. package/dist/effect-port.d.ts +50 -0
  260. package/dist/effect-port.js +77 -0
  261. package/dist/engines/pi/agent-session-factory.d.ts +50 -69
  262. package/dist/engines/pi/agent-session-factory.js +249 -146
  263. package/dist/engines/pi/auth.d.ts +15 -11
  264. package/dist/engines/pi/auth.js +96 -99
  265. package/dist/engines/pi/chat.d.ts +1 -5
  266. package/dist/engines/pi/chat.js +2 -14
  267. package/dist/engines/pi/config.d.ts +53 -51
  268. package/dist/engines/pi/config.js +83 -92
  269. package/dist/engines/pi/create.d.ts +60 -75
  270. package/dist/engines/pi/create.js +108 -141
  271. package/dist/engines/pi/definition.d.ts +14 -58
  272. package/dist/engines/pi/definition.js +21 -85
  273. package/dist/engines/pi/invoke-session.d.ts +5 -25
  274. package/dist/engines/pi/invoke-session.js +209 -244
  275. package/dist/engines/pi/login.d.ts +95 -35
  276. package/dist/engines/pi/login.js +259 -107
  277. package/dist/engines/pi/machine.d.ts +31 -0
  278. package/dist/engines/pi/machine.js +106 -0
  279. package/dist/engines/pi/models.d.ts +70 -89
  280. package/dist/engines/pi/models.js +188 -81
  281. package/dist/engines/pi/open.d.ts +65 -53
  282. package/dist/engines/pi/open.js +129 -69
  283. package/dist/engines/pi/report.d.ts +3 -12
  284. package/dist/engines/pi/report.js +2 -16
  285. package/dist/engines/pi/search-tools.d.ts +1 -9
  286. package/dist/engines/pi/search-tools.js +26 -57
  287. package/dist/engines/pi/service.d.ts +2 -16
  288. package/dist/engines/pi/service.js +2 -16
  289. package/dist/engines/pi/session-builder.d.ts +5 -9
  290. package/dist/engines/pi/session-builder.js +44 -82
  291. package/dist/engines/pi/session-control.js +286 -269
  292. package/dist/engines/pi/session-effects.d.ts +14 -0
  293. package/dist/engines/pi/session-effects.js +30 -0
  294. package/dist/engines/pi/session-inheritance.d.ts +9 -18
  295. package/dist/engines/pi/session-inheritance.js +103 -160
  296. package/dist/engines/pi/session-markers.d.ts +31 -30
  297. package/dist/engines/pi/session-markers.js +26 -25
  298. package/dist/engines/pi/session-settings.d.ts +6 -22
  299. package/dist/engines/pi/session-settings.js +10 -21
  300. package/dist/engines/pi/session-store.d.ts +26 -92
  301. package/dist/engines/pi/session-store.js +96 -310
  302. package/dist/engines/pi/tool-context.d.ts +17 -45
  303. package/dist/engines/pi/tool-context.js +7 -26
  304. package/dist/engines/pi/tool.d.ts +45 -36
  305. package/dist/engines/pi/tool.js +43 -57
  306. package/dist/engines/pi/turn-kit.d.ts +14 -52
  307. package/dist/engines/pi/turn-kit.js +18 -67
  308. package/dist/engines/pi/wake-tool.d.ts +2 -10
  309. package/dist/engines/pi/wake-tool.js +7 -27
  310. package/dist/env.d.ts +27 -28
  311. package/dist/env.js +45 -50
  312. package/dist/index.js +0 -2
  313. package/dist/lark.d.ts +1 -2
  314. package/dist/lark.js +1 -2
  315. package/dist/loader.d.ts +14 -55
  316. package/dist/loader.js +24 -43
  317. package/dist/log.d.ts +2 -12
  318. package/dist/log.js +3 -24
  319. package/dist/node.d.ts +2 -15
  320. package/dist/node.js +2 -18
  321. package/dist/observe.d.ts +2 -8
  322. package/dist/open-url.d.ts +1 -1
  323. package/dist/open-url.js +1 -1
  324. package/dist/paths.d.ts +70 -134
  325. package/dist/paths.js +165 -232
  326. package/dist/pi.d.ts +4 -6
  327. package/dist/pi.js +3 -3
  328. package/dist/proxy.d.ts +16 -6
  329. package/dist/proxy.js +52 -16
  330. package/dist/runtime.d.ts +18 -12
  331. package/dist/runtime.js +18 -11
  332. package/dist/scaffold/add-channel.d.ts +14 -31
  333. package/dist/scaffold/add-channel.js +48 -112
  334. package/dist/scaffold/init.d.ts +9 -31
  335. package/dist/scaffold/init.js +45 -138
  336. package/dist/scaffold/templates/env.example +6 -4
  337. package/dist/scaffold/templates/fastagent.config.ts +36 -0
  338. package/dist/scaffold/templates.d.ts +3 -3
  339. package/dist/scaffold/templates.js +5 -8
  340. package/dist/scaffold/vendor-skill.d.ts +2 -5
  341. package/dist/scaffold/vendor-skill.js +19 -29
  342. package/dist/schedule/cron.d.ts +9 -5
  343. package/dist/schedule/cron.js +9 -11
  344. package/dist/schedule/discover.d.ts +12 -12
  345. package/dist/schedule/discover.js +43 -34
  346. package/dist/schedule/routine.d.ts +43 -0
  347. package/dist/schedule/routine.js +32 -0
  348. package/dist/schedule/run.d.ts +55 -0
  349. package/dist/schedule/run.js +146 -0
  350. package/dist/schedule/scheduler.d.ts +44 -55
  351. package/dist/schedule/scheduler.js +291 -265
  352. package/dist/schedule/state.d.ts +105 -10
  353. package/dist/schedule/state.js +293 -19
  354. package/dist/schedule/wake-alarm.d.ts +10 -23
  355. package/dist/schedule/wake-alarm.js +91 -121
  356. package/dist/schedule/wakeups.d.ts +23 -39
  357. package/dist/schedule/wakeups.js +33 -75
  358. package/dist/secrets-gate.d.ts +32 -0
  359. package/dist/secrets-gate.js +33 -0
  360. package/dist/service.d.ts +137 -98
  361. package/dist/service.js +250 -252
  362. package/dist/session-remote.d.ts +33 -44
  363. package/dist/session-remote.js +203 -192
  364. package/dist/session.d.ts +153 -228
  365. package/dist/session.js +33 -55
  366. package/dist/tunnel.d.ts +8 -36
  367. package/dist/tunnel.js +24 -65
  368. package/dist/version.d.ts +2 -2
  369. package/dist/version.js +2 -2
  370. package/package.json +10 -16
  371. package/dist/channels/agentcore-state.d.ts +0 -77
  372. package/dist/channels/agentcore-state.js +0 -276
  373. package/dist/channels/github/github.d.ts +0 -31
  374. package/dist/channels/github/github.js +0 -89
  375. package/dist/channels/github/scaffold/channel.ts +0 -25
  376. package/dist/cli/commands/attach.d.ts +0 -108
  377. package/dist/cli/commands/attach.js +0 -637
  378. package/dist/cli/commands/fire.d.ts +0 -7
  379. package/dist/cli/commands/fire.js +0 -38
  380. package/dist/cli/commands/schedule.d.ts +0 -12
  381. package/dist/cli/commands/schedule.js +0 -87
  382. package/dist/github.d.ts +0 -3
  383. package/dist/github.js +0 -2
  384. package/dist/scaffold/templates/fastagent.config.mjs +0 -23
  385. package/dist/schedule/audit.d.ts +0 -18
  386. package/dist/schedule/audit.js +0 -57
  387. package/dist/schedule/schedule.d.ts +0 -35
  388. package/dist/schedule/schedule.js +0 -23
package/dist/paths.js CHANGED
@@ -1,74 +1,71 @@
1
1
  /**
2
- * PLACEMENT: which directory holds the agent, and which directory the agent works ON — plus the
3
- * machinery paths that follow from it.
4
- *
5
- * ONE marker and ONE rule. `fastagent.config.*` declares an agent; the WORKSPACE is the directory you
6
- * pointed fastagent at. Nothing here reads a directory NAME, so an agent directory can be called
7
- * anything — and the same tree answers two ways depending on where you aim it: point at the project and
8
- * the agent directory inside it answers with the project as its workspace; point at the agent directory
9
- * itself (all a container may have been shipped) and it works on itself. That is not an ambiguity to
10
- * resolve but the honest answer — an agent alone on a box has no project to work on, and a rule that
11
- * insisted otherwise would hand it the container root.
12
- *
13
- * Engine-neutral by nature (pure fs/path: existence checks and one shallow scan), so it lives here
14
- * rather than under engines/pi. Not a filing preference: the scaffold, the deploy planners, the dev
15
- * watcher and env.ts all need these facts, and routing them through the engine would make neutral
16
- * modules depend on it for something the engine has no say in.
2
+ * PLACEMENT: which directory holds the agent, and which directory the agent works ON — plus the machinery paths that
3
+ * follow from it.
17
4
  */
18
- import { existsSync, readdirSync, statSync } from "node:fs";
19
- import { access, chmod, mkdir, readFile, realpath } from "node:fs/promises";
5
+ import { readdirSync, statSync } from "node:fs";
6
+ import { access, readFile, realpath } from "node:fs/promises";
20
7
  import { homedir } from "node:os";
21
8
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
22
- /**
23
- * The directory name `init` gives a nested agent (`<workspace>/fastagent/`) unless `--agent-dir` names
24
- * another. A DEFAULT, not a rule: resolution reads the config marker and never a name, so renaming an
25
- * agent directory changes nothing about how it resolves. Visible on purpose — the agent directory holds
26
- * the AUTHOR's content (persona, skills, tool code: code, not tool configuration), so it follows the
27
- * repo convention for code (a plain directory), while fastagent's own machinery inside it (`.secrets/`,
28
- * `.state/`) keeps the dot prefix.
29
- */
9
+ /** The directory name `init` gives an agent (`<workspace>/fastagent/`) unless `--agent-dir` names another. */
30
10
  export const DEFAULT_AGENT_DIRNAME = "fastagent";
31
- /** The user-global machinery home under `$HOME` — hidden, per the dotfile convention for per-user
32
- * tool homes (`~/.cargo`, `~/.docker`); unrelated to {@link DEFAULT_AGENT_DIRNAME}, which only names
33
- * what `init` creates.
34
- * It carries the same shape inside it as an agent dir does (`~/.fastagent/.secrets/auth.json`), so the
35
- * resolvers below need no special case: `login` outside any agent simply hands them this directory. */
11
+ /** The user-global machinery home under `$HOME`. */
36
12
  export const GLOBAL_HOME_DIR = ".fastagent";
37
- /** The secrets segment inside an agent dir (or the global home): every PATH fastagent resolves —
38
- * `.env`, `.env.example`, auth.json, the scaffold's write — derives from it, so they cannot drift
39
- * apart. `FASTAGENT_SECRETS_DIR` relocates the RESOLVED dir ({@link resolveSecretsDir}), never this
40
- * name. The scaffold's ignore templates are real files the author owns from `init` on, so they spell
41
- * their rules out as literal text — renaming this constant means editing them too. */
13
+ /**
14
+ * The secrets segment inside an agent dir (or the global home). Every path fastagent resolves — `.env`,
15
+ * `.env.example`, auth.json, the scaffold's write — derives from it, so they cannot drift apart.
16
+ * `FASTAGENT_SECRETS_DIR` relocates the RESOLVED dir ({@link resolveSecretsDir}), never this name; the scaffolded
17
+ * ignore templates spell it out as literal text, so renaming this constant means editing them too.
18
+ */
42
19
  export const SECRETS_DIRNAME = ".secrets";
43
20
  /** The state segment inside an agent dir — same rule and same template caveat as {@link SECRETS_DIRNAME}. */
44
21
  export const STATE_DIRNAME = ".state";
45
- /** The config filenames, in load precedence. ONE source: the loader (below) and `scaffoldAgent`'s
46
- * already-an-agent refusal both read this, so "is there a config?" can't diverge between them. */
47
- export const AGENT_CONFIG_NAMES = ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
48
- /** The optional custom-model-endpoint file inside an agent dir (pi's models.json schema). Definition
49
- * data, not machinery: it declares WHICH endpoint the agent talks to, so it belongs beside the config
50
- * and travels into the deployed image. The name lives HERE, with the other placement facts, because
51
- * two neutral readers need it — the loader in engines/pi/models.ts and `dev`'s watcher, whose restart
52
- * scope must not silently drift from what the worker actually loads. */
22
+ /**
23
+ * THE config filename. One spelling, not a family: fastagent generates this file, so a choice of extension buys
24
+ * an author nothing and costs a precedence order plus a "you have two of them" failure path. Everything else an
25
+ * author writes (`tools/`, `channels/`, `routines/`) still accepts `.ts`/`.js`/`.mjs` — that is THEIR code.
26
+ */
27
+ export const AGENT_CONFIG_FILE = "fastagent.config.ts";
28
+ /** The optional custom-model-endpoint file inside an agent dir (pi's models.json schema). */
53
29
  export const AGENT_MODELS_FILE = "models.json";
54
- /** The definition paths an agent LOADS content from — the surface a second agent must not be scaffolded
55
- * inside ({@link agentDefinitionOwner}), because the outer agent would read it as its own skills/tools.
56
- * NOT evidence of an agent: `tools/` and `skills/` are ordinary names half the world's repositories
57
- * use, and the config is the only marker. */
58
- const LOADED_SURFACE = ["persona.md", "skills", "tools", "channels", "schedules"];
30
+ /**
31
+ * The definition paths an agent LOADS content from — the surface a second agent must not be scaffolded inside
32
+ * ({@link agentDefinitionOwner}), because the outer agent would read it as its own skills/tools.
33
+ */
34
+ const LOADED_SURFACE = ["persona.md", "skills", "tools", "channels", "routines"];
35
+ /**
36
+ * "Not there" and "could not look" are different answers, and only the first may read as an absence: an agent
37
+ * directory the caller cannot enter (EACCES) reported as no-agent-here ends in `run \`fastagent init\` to
38
+ * scaffold one` — over a definition that exists. So ENOENT/ENOTDIR are absence, and every other errno travels
39
+ * with its path, as {@link agentChildren} already did for the scan itself.
40
+ */
41
+ function statOrAbsent(p) {
42
+ try {
43
+ return statSync(p);
44
+ }
45
+ catch (e) {
46
+ const code = e.code;
47
+ if (code === "ENOENT" || code === "ENOTDIR")
48
+ return undefined;
49
+ throw e;
50
+ }
51
+ }
59
52
  function isDir(p) {
60
- return statSync(p, { throwIfNoEntry: false })?.isDirectory() === true;
53
+ return statOrAbsent(p)?.isDirectory() === true;
61
54
  }
62
- /** THE marker: a directory that declares itself an agent with a `fastagent.config.*`. One file decides
63
- * it, at every position — which is why an agent directory needs no reserved name and why there is no
64
- * zero-config agent. */
55
+ /** THE marker: a directory that declares itself an agent with a {@link AGENT_CONFIG_FILE}. */
65
56
  function hasConfig(p) {
66
- return isDir(p) && AGENT_CONFIG_NAMES.some((name) => existsSync(join(p, name)));
57
+ return isDir(p) && statOrAbsent(join(p, AGENT_CONFIG_FILE)) !== undefined;
67
58
  }
68
- /** The agent directories DIRECTLY inside `dir` — the one-level scan that finds an agent without knowing
69
- * its name. ONE level: deeper is that directory's own workspace, not this one's agent. Sorted, so a
70
- * refusal names them in a stable order. A missing `dir` yields none (callers resolve paths that may not
71
- * exist); a permission fault surfaces. */
59
+ /**
60
+ * DIRECTLY inside `dir`: the agent directories, and the ones the answer is missing for.
61
+ *
62
+ * A NAMED directory and a SCANNED one are different questions. `hasConfig` throws for the first, because the
63
+ * caller pointed at it. Here it cannot: every machine has directories this process may not enter, and one of
64
+ * them sitting next to the agent must not fail a command that found the agent — `workspaceHint` scans the
65
+ * PARENT for a hint, which on a CI runner means scanning `/tmp` and its `snap-private-tmp`. So an unreadable
66
+ * candidate is carried, not thrown, and {@link resolvePlacement} spends it where it is the answer: a caller
67
+ * that must produce an agent and found none says "I could not look at these", never `fastagent init`.
68
+ */
72
69
  function agentChildren(dir) {
73
70
  let entries;
74
71
  try {
@@ -77,44 +74,41 @@ function agentChildren(dir) {
77
74
  catch (e) {
78
75
  const code = e.code;
79
76
  if (code === "ENOENT" || code === "ENOTDIR")
80
- return [];
77
+ return { agents: [], unreadable: [] };
81
78
  throw e;
82
79
  }
83
- return entries
84
- .filter((e) => !e.isFile() && hasConfig(join(dir, e.name)))
85
- .map((e) => join(dir, e.name))
86
- .sort();
80
+ const agents = [];
81
+ const unreadable = [];
82
+ for (const entry of entries.filter((e) => !e.isFile())) {
83
+ const child = join(dir, entry.name);
84
+ try {
85
+ if (hasConfig(child))
86
+ agents.push(child);
87
+ }
88
+ catch (e) {
89
+ // PERMISSION ONLY. "Someone else's 0700 directory sits next to mine" is the normal case this carrying
90
+ // exists for, and it is the one an operator can act on. EIO, ELOOP, a non-errno throw: those are real
91
+ // failures with different fixes, and a scan has no business turning them into a line of prose.
92
+ const code = e.code;
93
+ if (code !== "EACCES" && code !== "EPERM")
94
+ throw e;
95
+ unreadable.push(entry.name);
96
+ }
97
+ }
98
+ return { agents: agents.sort(), unreadable: unreadable.sort() };
87
99
  }
88
100
  /**
89
- * The agents `dir` resolves over: ITSELF when it holds a config, else the ones directly inside it —
90
- * never both, because aiming at an agent can only mean that agent. Exported for `init`, which asks the
91
- * same question this lookup asks: which agents would `dir` resolve over, before and after scaffolding.
101
+ * The agents `dir` resolves over: ITSELF when it holds a config, else the ones directly inside it — never both,
102
+ * because aiming at an agent can only mean that agent.
92
103
  */
93
104
  export function agentsAt(dir) {
94
- const base = resolve(dir);
95
- return hasConfig(base) ? [base] : agentChildren(base);
105
+ return scanAgents(resolve(dir)).agents;
96
106
  }
97
- /**
98
- * WHICH agent is meant, when a directory holds several: an engineer's, a PM's and a content owner's
99
- * agent can each drive the same repository, so they must be selectable rather than refused.
100
- *
101
- * `FASTAGENT_AGENT` reaches this from the REAL environment only. Placement resolves before anything
102
- * reads the agent's `.env` — it is what tells the loader which agent's `.env` to read — so a value set
103
- * there is a silent no-op (the same ordering caveat `FASTAGENT_SECRETS_DIR` carries). A shell, an
104
- * `.envrc`, or the command itself.
105
- *
106
- * It is an ASSERTION, not a preference: it names the agent, and a directory without one
107
- * by that name resolves to nothing — even when exactly one agent is sitting there. Serving a DIFFERENT
108
- * agent than the one asked for is the silent-wrong-target this codebase refuses everywhere else, and the
109
- * uniform rule ("it names the agent") beats one that changes meaning with the sibling count. The stated
110
- * cost: a `FASTAGENT_AGENT` exported in a shell PROFILE refuses in every unrelated directory it travels
111
- * into. Scope it per-repo (an `.envrc`) or pass it per-command; the refusal names the way out.
112
- *
113
- * With nothing asserted, the {@link DEFAULT_AGENT_DIRNAME} breaks the tie. That is the one place a
114
- * directory NAME carries weight, and deliberately not an identity rule — the config alone says what IS
115
- * an agent; the name only decides which already-identified one answers. What it buys: adding a second
116
- * agent to a working `<workspace>/fastagent/` setup does not break the command everyone already types.
117
- */
107
+ /** {@link agentsAt} plus what the scan could not look at — one readdir answering both questions. */
108
+ function scanAgents(base) {
109
+ return hasConfig(base) ? { agents: [base], unreadable: [] } : agentChildren(base);
110
+ }
111
+ /** WHICH agent is meant, when a directory holds several. */
118
112
  function selectAgent(agents, env) {
119
113
  const wanted = env.FASTAGENT_AGENT;
120
114
  if (wanted)
@@ -122,59 +116,16 @@ function selectAgent(agents, env) {
122
116
  const [only, ...rest] = agents;
123
117
  return rest.length === 0 ? only : agents.find((a) => basename(a) === DEFAULT_AGENT_DIRNAME);
124
118
  }
125
- /**
126
- * Resolve `dir` into its placement, or undefined when nothing selects one agent. The whole rule: `dir`
127
- * is the WORKSPACE, and the agent is a `fastagent.config.*` holder at it or one level inside
128
- * ({@link agentsAt}), narrowed by {@link selectAgent} when there are several.
129
- */
119
+ /** Resolve `dir` into its placement, or undefined when nothing selects one agent. */
130
120
  function findPlacement(dir, env = process.env) {
131
- const base = resolve(dir);
132
- const agentDir = selectAgent(agentsAt(base), env);
133
- return agentDir === undefined ? undefined : { agentDir, workspace: base };
134
- }
135
- /**
136
- * The one-line hint for "you pointed at the agent, but the project around it is what you meant" — or
137
- * undefined. The workspace being whatever you aimed at is deliberate (a deployed box may hold nothing
138
- * but the agent), and the cost is that `cd my-agent && fastagent dev` legitimately narrows the agent's
139
- * WORKSPACE to its own directory: its cwd, its coding tools' root, and deploy's build context. (②
140
- * context is not affected — that walk climbs ancestors either way.) Resolution must not guess which you
141
- * wanted, so this is a HINT — and a hint may use the heuristic ("the parent carries an AGENTS.md or a
142
- * .git") that a rule may not.
143
- *
144
- * It suggests a command only after RUNNING the lookup that command would run, because a hint that dead-ends
145
- * is worse than none: with several agents beside this one, `..` refuses and names them — sending the
146
- * reader to a refusal whose own advice points back here.
147
- */
148
- export function workspaceHint({ agentDir, workspace }, env = process.env) {
149
- if (agentDir !== workspace)
150
- return undefined;
151
- const parent = dirname(agentDir);
152
- if (parent === agentDir)
153
- return undefined;
154
- // Would `..` actually serve THIS agent? A parent holding a config is an agent itself, and one holding
155
- // siblings resolves to none without a selector — neither is the project view asked about here.
156
- if (findPlacement(parent, env)?.agentDir !== agentDir)
157
- return undefined;
158
- if (!["AGENTS.md", ".git"].some((name) => existsSync(join(parent, name))))
159
- return undefined;
160
- return `${parent} looks like a project — point fastagent at it (\`..\`) to have the agent work ON it`;
121
+ const agentDir = selectAgent(agentsAt(resolve(dir)), env);
122
+ return agentDir === undefined ? undefined : { agentDir, workspace: dirname(agentDir) };
161
123
  }
162
- /** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
163
- * or the throw. Login and standalone channel senders also operate outside configured agents, so
164
- * they need absence as a value. Filesystem errors still propagate. */
124
+ /** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair or the throw. */
165
125
  export function findAgentDir(dir) {
166
126
  return findPlacement(dir)?.agentDir;
167
127
  }
168
- /** The agent `dir` sits INSIDE (the nearest proper ancestor that IS an agent dir), or undefined.
169
- * Module-private: the two questions callers actually ask are "where do I `cd` to?"
170
- * ({@link placementDeadEnd}) and "would a new agent here become part of an existing one's definition?"
171
- * ({@link agentDefinitionOwner}) — and those answers differ (an agent's `src/` is inside it, but is not
172
- * part of what it loads), so exporting the raw containment fact would invite conflating them.
173
- * Placement resolution deliberately never walks up — the answer must not depend on how deep you stand
174
- * — but "you are inside an agent, just not at its root" is the likeliest reason resolution fails, and
175
- * both the refusal below and `login`'s global-fallback decision need to tell that case apart. Uses the
176
- * same marker as resolution (an ancestor holding a config), so it never claims a position it cannot
177
- * justify. */
128
+ /** The agent `dir` sits INSIDE (the nearest proper ancestor that IS an agent dir), or undefined. */
178
129
  function enclosingAgentDir(dir) {
179
130
  let candidate = dirname(resolve(dir));
180
131
  for (let prev = ""; candidate !== prev; prev = candidate, candidate = dirname(candidate)) {
@@ -184,12 +135,8 @@ function enclosingAgentDir(dir) {
184
135
  return undefined;
185
136
  }
186
137
  /**
187
- * The agent whose DEFINITION contains `dir` — scaffolding there would make the new agent part of the
188
- * outer one's loaded surface rather than an agent of its own. Narrower than {@link enclosingAgentDir} on
189
- * purpose: an agent owns only what it LOADS ({@link LOADED_SURFACE}); the rest of its directory is the
190
- * author's tree, where a second agent (a monorepo package, say) is a legitimate thing to create.
191
- * `enclosingAgentDir` answers a different question ("where do I `cd` to?"), and for that an agent's
192
- * `src/` genuinely IS inside it.
138
+ * The agent whose DEFINITION contains `dir` — scaffolding there would make the new agent part of the outer one's
139
+ * loaded surface rather than an agent of its own.
193
140
  */
194
141
  export function agentDefinitionOwner(dir) {
195
142
  const base = resolve(dir);
@@ -199,15 +146,7 @@ export function agentDefinitionOwner(dir) {
199
146
  const [head] = relative(agent, base).split(sep);
200
147
  return head && LOADED_SURFACE.includes(head) ? agent : undefined;
201
148
  }
202
- /**
203
- * Why `dir` is not an agent, when it has its OWN way out — or undefined when it is simply not near one.
204
- * Two positions qualify: standing INSIDE an agent (its `tools/`, its `src/`), and standing on a
205
- * directory whose several agents nothing selects between. Both matter because the generic advice ("run
206
- * `fastagent init`") would not help — the agent already exists, one step away.
207
- *
208
- * Exported because `login` is the one command allowed to run outside an agent, and it must tell "truly
209
- * outside" (→ the global credential) from "a dead end" (→ refuse, like every other command).
210
- */
149
+ /** Why `dir` is not an agent, when it has its OWN way out — or undefined when it is simply not near one. */
211
150
  export function placementDeadEnd(dir, env = process.env) {
212
151
  const base = resolve(dir);
213
152
  const enclosing = enclosingAgentDir(base);
@@ -216,60 +155,72 @@ export function placementDeadEnd(dir, env = process.env) {
216
155
  }
217
156
  const agents = agentsAt(base).map((a) => basename(a));
218
157
  const listed = `${base} holds ${agents.length} agent${agents.length === 1 ? "" : "s"} (${agents.join(", ")})`;
219
- // Asserting a name that is not here is a different mistake from asserting none, and it is worth its own
220
- // message at ANY count: the value sits in the environment (often a shell profile carried in from
221
- // somewhere else), so echo it back with what is actually here instead of restating the general rule.
158
+ // Asserting a name that is not here is a different mistake from asserting none, and it is worth its own message at
159
+ // ANY count.
222
160
  if (env.FASTAGENT_AGENT && agents.length > 0) {
223
161
  return (`${listed}, and FASTAGENT_AGENT asserts "${env.FASTAGENT_AGENT}", which is not one of them — set it ` +
224
162
  `to one of those, unset it, or scope it to the repository that needs it (an .envrc)`);
225
163
  }
226
164
  if (agents.length > 1) {
227
165
  return (`${listed} and none of them is named "${DEFAULT_AGENT_DIRNAME}" (the default) — pick one with ` +
228
- `FASTAGENT_AGENT=<name>, or point fastagent at the one you want (it then works on ITSELF)`);
166
+ `FASTAGENT_AGENT=<name>, or point fastagent at the one you want`);
229
167
  }
230
168
  return undefined;
231
169
  }
232
170
  /**
233
- * Resolve a directory into its placement — the ONE owner of the rule ({@link findPlacement}): `dir` is
234
- * the workspace, and the agent is the single `fastagent.config.*` holder at it or one level inside.
235
- * Placement is never configured and never detected from surroundings; it is that lookup and the
236
- * directory you pointed at.
171
+ * Resolve a directory into its placement — the ONE owner of the rule ({@link findPlacement}).
237
172
  *
238
- * Anything else throws (fail visibly). Resolution never walks UP — an agent must not be claimed from
239
- * arbitrarily deep inside it — but the MESSAGE reads the path, so each dead end gets the exit that fits
240
- * it ({@link placementDeadEnd}).
173
+ * WHERE AN UNREADABLE CANDIDATE IS SPENT, and only here: this caller MUST produce an agent, so a directory it
174
+ * could not look into is the difference between "there is none" and "there might be". `placementDeadEnd` is
175
+ * asked a weaker question — is this place a dead end with its own way out — by callers like `login`, for which
176
+ * a neighbour it may not enter is environment noise, not a reason to exit instead of logging in globally. It
177
+ * also comes AFTER that call: with two agents to choose between, a permission problem in a third directory is
178
+ * not the message that gets the caller moving.
241
179
  */
242
180
  export function resolvePlacement(dir, env = process.env) {
243
181
  const placement = findPlacement(dir, env);
244
182
  if (!placement) {
245
183
  const base = resolve(dir);
246
- throw new Error(placementDeadEnd(base, env) ??
247
- `${base} is not a fastagent agent — no fastagent.config.* here, and no directory holding one ` +
248
- `directly inside; run \`fastagent init\` to scaffold one`);
184
+ throw new Error(placementDeadEnd(base, env) ?? `${base} is not a fastagent agent${noAgentHere(base)}`);
249
185
  }
250
186
  return placement;
251
187
  }
252
- /** How to WRITE a path for someone standing in `cwd`: relative when it is inside `cwd`, absolute when
253
- * it climbs out (a `../../..` is noise), and undefined when it IS `cwd` (nothing to say). ONE policy,
254
- * shared by `init`'s `cd` step, `add`'s next-steps paths and `fire`'s "looked in" hint — they all answer
255
- * the same question, which is a placement-PRESENTATION question, not a scaffolding one. */
188
+ /** At most this many names before the rest become a count — `/tmp` on a shared machine has plenty. */
189
+ const LISTED_UNREADABLE = 3;
190
+ /**
191
+ * The rest of the "not an agent" refusal: the way out, plus what could not be looked at.
192
+ *
193
+ * BOTH, never one or the other. `fastagent init` is the only line that tells the caller what to do, and a
194
+ * directory it cannot enter is exactly why that advice can be wrong (#571: scaffolding over a definition that
195
+ * is there but invisible). Substituting the second for the first sends anyone running in `/tmp`, or on a
196
+ * shared machine, to fix permissions on directories that were never theirs and hold no agent.
197
+ */
198
+ function noAgentHere(base) {
199
+ const { unreadable } = scanAgents(base);
200
+ const noConfig = ` — no fastagent.config.ts here, and no readable directory holding one directly inside`;
201
+ const scaffold = "run `fastagent init` to scaffold one";
202
+ if (unreadable.length === 0)
203
+ return `${noConfig}; ${scaffold}`;
204
+ const shown = unreadable.slice(0, LISTED_UNREADABLE).join(", ");
205
+ const rest = unreadable.length - LISTED_UNREADABLE;
206
+ // The unknown FIRST, because it is the one thing that can make the advice wrong.
207
+ return (`${noConfig}. ${unreadable.length} director${unreadable.length === 1 ? "y" : "ies"} here could not be ` +
208
+ `read (permission), so an agent may be inside one: ${shown}${rest > 0 ? `, +${rest} more` : ""}. Check ` +
209
+ `those first; ${scaffold} only if none of them holds an agent`);
210
+ }
211
+ /** How to WRITE a path for someone standing in `cwd`. */
256
212
  export function displayPath(cwd, dir) {
257
213
  const rel = relative(cwd, dir);
258
214
  if (rel === "")
259
215
  return undefined;
260
- // "Climbs out" is a path-SEGMENT check — rel is ".." or starts with "../" (or "..\" on Windows). A
261
- // bare startsWith("..") would wrongly flag an in-cwd directory literally named "..agent".
216
+ // "Climbs out" is a path-SEGMENT check — rel is ".." or starts with "../" (or "..\" on Windows).
262
217
  const escapes = rel === ".." || /^\.\.[/\\]/.test(rel);
263
218
  return escapes ? dir : rel;
264
219
  }
265
- /** Does a path exist? Plain fs, no placement in it — it lives here because `paths.ts` is where the
266
- * neutral path helpers are, and the scaffolder is not a utility home for the CLI and deploy. */
267
220
  export async function exists(p) {
268
221
  return access(p).then(() => true, () => false);
269
222
  }
270
- /** The file's text, or undefined when there is no file. ONLY absence reads as absence: a file that
271
- * exists but cannot be read (EACCES, a directory in its place) throws, because a decision made on
272
- * "not there" — regenerate it, skip its gate — is the wrong one for a file that is there. */
223
+ /** The file's text, or undefined when there is no file. */
273
224
  export async function readTextIfExists(p) {
274
225
  try {
275
226
  return await readFile(p, "utf8");
@@ -281,11 +232,8 @@ export async function readTextIfExists(p) {
281
232
  }
282
233
  }
283
234
  /**
284
- * Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
285
- * leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
286
- * never gets the shell's `~` expansion, so a bare `resolve("~/x")` would silently create a literal `~`
287
- * directory — a fail-silently footgun for a secret/state path. Expanding here makes `~` mean home
288
- * everywhere these knobs are read.
235
+ * Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a leading `~`/`~/`
236
+ * to the home dir FIRST.
289
237
  */
290
238
  export function resolveOverridePath(raw) {
291
239
  if (!raw)
@@ -294,64 +242,54 @@ export function resolveOverridePath(raw) {
294
242
  return resolve(expanded);
295
243
  }
296
244
  /**
297
- * The resolved state root — the durable machine-state home (sessions/, channels/<kind>/, schedule/,
298
- * control.json): `FASTAGENT_STATE_DIR` env > `<agentDir>/.state`. Absolute, so channels and the
299
- * startup report agree regardless of cwd. Definition: mutable runtime state — single lifecycle
300
- * (precious, survives redeploy), single process; a container points this at its mounted volume.
301
- * Secrets are NOT here — they live under {@link resolveSecretsDir} (a different deploy lifecycle:
302
- * secret store vs volume). The finer knob (`FASTAGENT_SESSIONS_DIR`) still overrides its path on top.
303
- *
304
- * `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
305
- * — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
306
- * DEFAULT (`<root>/.state`) is dir-anchored.
245
+ * The resolved state root — the durable machine-state home (sessions/, channels/<kind>/, schedule/):
246
+ * `FASTAGENT_STATE_DIR` env > `<agentDir>/.state`.
307
247
  */
308
248
  export function resolveStateRoot(dir, env = process.env) {
309
249
  return resolveOverridePath(env.FASTAGENT_STATE_DIR) ?? join(resolve(dir), STATE_DIRNAME);
310
250
  }
311
251
  /**
312
- * The resolved secrets dir — everything fastagent manages that must NEVER leave the machine (the
313
- * agent's `.env` + auth.json): `FASTAGENT_SECRETS_DIR` env > `<agentDir>/.secrets`. Split from
314
- * the state root on deploy lifecycle: secrets travel through the host's secret store (env vars / the
315
- * auth seed), state through a volume. A deployed box sets both env knobs at its volume (e.g.
316
- * `/data/.secrets`, `/data/.state`) so a seeded-then-ROTATED OAuth credential persists across
317
- * restarts. The `.env`'s OWN location resolves from the REAL environment — commands locate and load
318
- * `.env` before anything else, so a `FASTAGENT_SECRETS_DIR` set INSIDE `.env` still relocates
319
- * auth.json but cannot move the file it is read from (env.ts dotEnvPath).
252
+ * WHERE an agent's session records live: `<state root>/sessions`, always. Records are machine state like channel
253
+ * state and schedule claims, so they move with the ONE knob that moves all of it rather than with a second one that
254
+ * moves only them — a split every reader would then have to agree about, and which left channel state behind anyway.
255
+ * An embedder that really wants the records elsewhere passes `sessionsDir` to `createAgentService`; there is no env
256
+ * or flag spelling of it, for the reason `resolveAuthPathOverride` states.
257
+ */
258
+ export function resolveSessionsDir(dir, env = process.env) {
259
+ return join(resolveStateRoot(dir, env), "sessions");
260
+ }
261
+ /**
262
+ * The resolved secrets dir — everything fastagent manages that must NEVER leave the machine (the agent's `.env` +
263
+ * auth.json).
320
264
  */
321
265
  export function resolveSecretsDir(dir, env = process.env) {
322
266
  return resolveOverridePath(env.FASTAGENT_SECRETS_DIR) ?? join(resolve(dir), SECRETS_DIRNAME);
323
267
  }
324
- /** What a file under {@link resolveSecretsDir} is written with (auth.json, .env). */
325
- export const SECRET_FILE_MODE = 0o600;
326
- /** What the secrets directory itself is created with. */
327
- const SECRETS_DIR_MODE = 0o700;
268
+ /** Is this process the deployed container the generated artifacts describe? ONE reading of the
269
+ * environment fact the image states, so a second spelling cannot drift from it. */
270
+ export function isDeployedWorkspace() {
271
+ return Boolean(process.env.FASTAGENT_RELEASE_FILE);
272
+ }
273
+ /** Is this process running inside the AgentCore Runtime? Set by the generated deploy artifacts. */
274
+ export function isAgentcoreRuntime() {
275
+ return process.env.FASTAGENT_AGENTCORE === "1";
276
+ }
328
277
  /**
329
- * Create the secrets directory with the mode its contents require — and REPAIR it when it already
330
- * exists, which is the case that matters.
278
+ * The mode of a file fastagent CREATES to hold a secret: `auth.json`, a `.env` it makes itself,
279
+ * a host's parameter file. That is the whole extent of its opinion about permissions — a file it did not create
280
+ * keeps the mode its owner gave it, and no directory's mode is ever decided or repaired. Every comparable tool
281
+ * draws the line here: Rails chmods the `master.key` it generates and leaves `config/` alone
282
+ * (rails/rails@4c6c357), the aws CLI ships a 0600 `~/.aws/config` inside a 0755 `~/.aws`.
331
283
  *
332
- * The DIRECTORY is the boundary that actually protects a credential: without its `x` bit nothing
333
- * below it is reachable, whatever a file's own mode says. And it is decided ONCE, by whichever
334
- * writer gets there first — `mkdir`'s `mode` is ignored for a directory that already exists, so a
335
- * later, more careful caller silently inherits the first one's answer. Four callers create this
336
- * directory (`init`, `add <channel>`, the credential store, the deploy seed) and the ordinary order
337
- * is init → add → login, so the careful one is LAST: the rule has to live where all of them can
338
- * reach it, and it has to chmod rather than trust the create.
284
+ * "Who created the file" is the load-bearing half: asking instead whether a given WRITE carries a secret has a
285
+ * different answer at every call site and one more with each new writer.
339
286
  *
340
- * A chmod the caller never asked for owes them its reason: the raw `EPERM ... chmod '/shared/creds'`
341
- * reads as a bug in whatever they WERE doing (storing a credential), not as fastagent tightening a
342
- * directory they pointed it at, and says nothing about the way out.
343
- */
344
- export async function ensureSecretsDir(dir) {
345
- await mkdir(dir, { recursive: true, mode: SECRETS_DIR_MODE });
346
- await chmod(dir, SECRETS_DIR_MODE).catch((e) => {
347
- throw new Error(`cannot secure secrets dir ${dir} (fastagent keeps it 0700): ${e.message} — point --auth-path/FASTAGENT_SECRETS_DIR at a directory this process owns`, { cause: e });
348
- });
349
- }
350
- /**
351
- * Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
352
- * absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
353
- * A missing target is fine (nothing to guard yet).
287
+ * One consequence worth knowing: `writeFileAtomic` sets the mode on the temp file it renames into place, so a
288
+ * file it owns end-to-end (`auth.json`) is 0600 after every write, including one an operator had
289
+ * placed by hand. Appending to a file fastagent did not create (`.env`) cannot and does not do that.
354
290
  */
291
+ export const SECRET_FILE_MODE = 0o600;
292
+ /** Guard that `<agentDir>/<name>` resolves INSIDE the agent dir. */
355
293
  export async function assertInsideAgentDir(agentDir, name) {
356
294
  const target = join(agentDir, name);
357
295
  const real = await realpath(target).catch((e) => {
@@ -368,12 +306,7 @@ export async function assertInsideAgentDir(agentDir, name) {
368
306
  `use a real directory or a symlink that stays within it`);
369
307
  }
370
308
  }
371
- /**
372
- * Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
373
- * this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
374
- * dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
375
- * does not act on where a user's paths point.
376
- */
309
+ /** Whether `targetPath` lives inside `baseDir` (same path counts). */
377
310
  export function isUnderDir(targetPath, baseDir) {
378
311
  const rel = relative(baseDir, targetPath);
379
312
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
package/dist/pi.d.ts CHANGED
@@ -3,13 +3,9 @@ export { defineTool, type DefineToolOptions, type FastagentTool, type MountedToo
3
3
  export type { ReadonlySessionManager, ToolActivation } from "./engines/pi/tool-context.ts";
4
4
  export { z } from "zod";
5
5
  export type { AgentTool, ExecutionEnv, Skill, SkillDiagnostic } from "@earendil-works/pi-agent-core";
6
- /**
7
- * A conversation record, as the tool runtime and the control plane hold it. Its entries are pi's own
8
- * — exported under a qualified name because `SessionEntry` in this package is the NEUTRAL one the
9
- * control plane publishes (session.ts), and the two are different shapes.
10
- */
6
+ /** A conversation record, as the tool runtime and the control plane hold it. */
11
7
  export type { SessionManager, SessionEntry as PiSessionEntry } from "@earendil-works/pi-coding-agent";
12
- export { createPiAgentFromDir, type CreatePiAgentFromDirOptions, } from "./engines/pi/open.ts";
8
+ export { availableModelsFromDir, createPiAgentFromDir, type CreatePiAgentFromDirOptions, } from "./engines/pi/open.ts";
13
9
  export type { LoadedDefinition, SkillCollision } from "./engines/pi/definition.ts";
14
10
  export { defineConfig, listModels, resolveModel, type FastagentConfig, } from "./engines/pi/config.ts";
15
11
  export type { SessionObserver } from "./engines/pi/turn-kit.ts";
@@ -19,6 +15,8 @@ export { piInMemorySessionRecordStore, piSessionRecordStore, type PiSessionRecor
19
15
  export type { SessionInheritance } from "./engines/pi/session-inheritance.ts";
20
16
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore, type FastagentAuthOptions } from "./engines/pi/auth.ts";
21
17
  export { createPiModels, probeAuthSource, type CreatePiModelsOptions } from "./engines/pi/models.ts";
18
+ export { login, loginOptions, LoginCancelled, type LoginMethod, type LoginOption, type LoginRequest, type LoginResult, } from "./engines/pi/login.ts";
19
+ export type { AuthEvent, AuthInteraction, AuthPrompt } from "@earendil-works/pi-ai";
22
20
  export type { Models } from "@earendil-works/pi-ai";
23
21
  export type { Provider, ProviderAuth } from "@earendil-works/pi-ai";
24
22
  export type { Model } from "@earendil-works/pi-ai";
package/dist/pi.js CHANGED
@@ -2,13 +2,13 @@
2
2
  export { createPiAgent, createPiAgentFromDefinition, } from "./engines/pi/create.js";
3
3
  export { defineTool, } from "./engines/pi/tool.js";
4
4
  export { z } from "zod";
5
- export { createPiAgentFromDir, } from "./engines/pi/open.js";
5
+ export { availableModelsFromDir, createPiAgentFromDir, } from "./engines/pi/open.js";
6
6
  export { defineConfig, listModels, resolveModel, } from "./engines/pi/config.js";
7
7
  export { inProcessLease } from "./engines/pi/turn-kit.js";
8
8
  export { createPiSessionControl, } from "./engines/pi/session-control.js";
9
9
  export { piInMemorySessionRecordStore, piSessionRecordStore, } from "./engines/pi/session-store.js";
10
10
  export { GLOBAL_AUTH_PATH, fastagentCredentialStore } from "./engines/pi/auth.js";
11
11
  export { createPiModels, probeAuthSource } from "./engines/pi/models.js";
12
- // The product's one-call assembly: a directory becomes a live service. On the pi surface because
13
- // it opens a directory the pi way (channels, schedules, the assembled agent).
12
+ export { login, loginOptions, LoginCancelled, } from "./engines/pi/login.js";
13
+ // The product's one-call assembly: a directory becomes a live service.
14
14
  export { createAgentService } from "./engines/pi/service.js";
package/dist/proxy.d.ts CHANGED
@@ -1,9 +1,19 @@
1
1
  /**
2
- * Route fetch through HTTPS_PROXY and keep fetch + dispatcher on the SAME undici implementation. Any
3
- * process that hits a provider or a channel API needs this UNDER NODE: Node's fetch does not honor
4
- * HTTPS_PROXY by itself (so login's OAuth token exchange, model calls, and a webhook setWebhook would go
5
- * direct, bypassing a region proxy), and Node 26's bundled fetch skips gzip decompression without
6
- * install() (empty stopReason:"stop"). A process-global side effect — call it once at a command/entry
7
- * start. No-op under Bun, whose native fetch already does both (and lacks the undici entry points).
2
+ * The exemption list the dispatcher is built with: whatever the environment declares, plus {@link LOOPBACK}.
3
+ *
4
+ * Its own function because installing happens ONCE per process, so these branches are unreachable through
5
+ * {@link installProxyFetch} — and the wildcard one is the kind that is silently wrong until someone reads it.
6
+ */
7
+ export declare function noProxyList(env?: NodeJS.ProcessEnv): string;
8
+ /**
9
+ * Point this process's fetch at the proxy the environment declares, with loopback exempt by default.
10
+ *
11
+ * Two jobs in one, and both are needed UNDER NODE: Node's fetch does not honor HTTP(S)_PROXY at all, and Node 26's
12
+ * bundled fetch skips gzip decompression when it dispatches through npm undici (empty `stopReason:"stop"`), so fetch
13
+ * and the dispatcher must come from the SAME undici. No-op under Bun, whose native fetch already does both (and
14
+ * lacks these entry points).
15
+ *
16
+ * Call it through `enterAgentEnv` (env.ts) rather than directly: the proxy may be declared in the agent's own `.env`,
17
+ * so it has to be read first. The only direct callers are the ones with no agent directory to read.
8
18
  */
9
19
  export declare function installProxyFetch(): void;