@fastagent-sh/fastagent 0.28.0 → 0.30.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 (325) hide show
  1. package/README.md +10 -4
  2. package/dist/atomic-write.d.ts +4 -5
  3. package/dist/atomic-write.js +4 -5
  4. package/dist/authoring.d.ts +32 -15
  5. package/dist/authoring.js +147 -127
  6. package/dist/channels/agentcore-protocol.d.ts +8 -3
  7. package/dist/channels/agentcore-protocol.js +3 -1
  8. package/dist/channels/agentcore-service.d.ts +1 -0
  9. package/dist/channels/agentcore-service.js +14 -4
  10. package/dist/channels/agentcore.d.ts +9 -0
  11. package/dist/channels/agentcore.js +29 -4
  12. package/dist/channels/context.d.ts +2 -0
  13. package/dist/channels/context.js +5 -1
  14. package/dist/channels/feishu/feishu-api.d.ts +3 -1
  15. package/dist/channels/feishu/feishu-api.js +4 -0
  16. package/dist/channels/feishu/feishu.d.ts +0 -3
  17. package/dist/channels/feishu/feishu.js +10 -10
  18. package/dist/channels/feishu/register-app.d.ts +2 -0
  19. package/dist/channels/feishu/register-app.js +4 -6
  20. package/dist/channels/feishu/scaffold/channel.ts +32 -34
  21. package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
  22. package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
  23. package/dist/channels/feishu/setup-mode.d.ts +2 -12
  24. package/dist/channels/feishu/setup-mode.js +2 -22
  25. package/dist/channels/feishu/setup.d.ts +12 -0
  26. package/dist/channels/feishu/setup.js +197 -0
  27. package/dist/channels/kit/abortable.d.ts +8 -0
  28. package/dist/channels/kit/abortable.js +22 -0
  29. package/dist/channels/lark/lark.d.ts +0 -3
  30. package/dist/channels/lark/lark.js +0 -5
  31. package/dist/channels/lark/scaffold/channel.ts +33 -35
  32. package/dist/channels/lark/scaffold/lark-send.ts +5 -5
  33. package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
  34. package/dist/channels/lark/setup.d.ts +9 -0
  35. package/dist/channels/lark/setup.js +57 -0
  36. package/dist/channels/setup.d.ts +112 -0
  37. package/dist/channels/setup.js +31 -0
  38. package/dist/channels/slack/history.js +1 -1
  39. package/dist/channels/slack/manifest.d.ts +17 -7
  40. package/dist/channels/slack/manifest.js +12 -16
  41. package/dist/channels/slack/scaffold/channel.ts +44 -30
  42. package/dist/channels/slack/scaffold/slack-send.ts +8 -8
  43. package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
  44. package/dist/channels/slack/setup.d.ts +7 -0
  45. package/dist/channels/slack/setup.js +119 -0
  46. package/dist/channels/slack/slack-api.d.ts +2 -0
  47. package/dist/channels/slack/slack-api.js +4 -0
  48. package/dist/channels/slack/slack.d.ts +21 -2
  49. package/dist/channels/slack/slack.js +398 -338
  50. package/dist/channels/slack/socket.d.ts +57 -0
  51. package/dist/channels/slack/socket.js +305 -0
  52. package/dist/channels/telegram/polling.d.ts +27 -0
  53. package/dist/channels/telegram/polling.js +105 -0
  54. package/dist/channels/telegram/scaffold/channel.ts +52 -32
  55. package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
  56. package/dist/channels/telegram/setup.d.ts +2 -0
  57. package/dist/channels/telegram/setup.js +42 -0
  58. package/dist/channels/telegram/telegram-api.d.ts +27 -1
  59. package/dist/channels/telegram/telegram-api.js +7 -6
  60. package/dist/channels/telegram/telegram.d.ts +11 -3
  61. package/dist/channels/telegram/telegram.js +184 -157
  62. package/dist/channels/wait-health.js +1 -1
  63. package/dist/channels/webhook-evidence.d.ts +11 -0
  64. package/dist/channels/webhook-evidence.js +71 -0
  65. package/dist/channels/webhooks.d.ts +37 -0
  66. package/dist/channels/webhooks.js +70 -0
  67. package/dist/child-env.d.ts +16 -0
  68. package/dist/child-env.js +30 -0
  69. package/dist/cli/box-login.js +4 -1
  70. package/dist/cli/channel-app.d.ts +3 -0
  71. package/dist/cli/channel-app.js +35 -0
  72. package/dist/cli/channel-setup.d.ts +38 -0
  73. package/dist/cli/channel-setup.js +195 -0
  74. package/dist/cli/commands/add.d.ts +6 -14
  75. package/dist/cli/commands/add.js +45 -310
  76. package/dist/cli/commands/auth.js +8 -2
  77. package/dist/cli/commands/build.d.ts +5 -0
  78. package/dist/cli/commands/build.js +16 -0
  79. package/dist/cli/commands/channels.d.ts +19 -0
  80. package/dist/cli/commands/channels.js +230 -0
  81. package/dist/cli/commands/content.d.ts +7 -3
  82. package/dist/cli/commands/content.js +16 -10
  83. package/dist/cli/commands/deploy/agentcore.js +55 -23
  84. package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
  85. package/dist/cli/commands/deploy/channel-apps.js +30 -0
  86. package/dist/cli/commands/deploy/docker.js +32 -65
  87. package/dist/cli/commands/deploy/fly.js +28 -12
  88. package/dist/cli/commands/deploy/railway.js +38 -13
  89. package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
  90. package/dist/cli/commands/deploy/secrets-step.js +66 -0
  91. package/dist/cli/commands/deploy/shared.d.ts +16 -5
  92. package/dist/cli/commands/deploy/shared.js +25 -8
  93. package/dist/cli/commands/deploy.js +53 -67
  94. package/dist/cli/commands/dev.d.ts +2 -0
  95. package/dist/cli/commands/dev.js +15 -3
  96. package/dist/cli/commands/env.js +1 -1
  97. package/dist/cli/commands/info.d.ts +2 -0
  98. package/dist/cli/commands/info.js +63 -24
  99. package/dist/cli/commands/init.js +3 -1
  100. package/dist/cli/commands/invoke.d.ts +2 -0
  101. package/dist/cli/commands/invoke.js +20 -7
  102. package/dist/cli/commands/login.d.ts +0 -7
  103. package/dist/cli/commands/login.js +22 -10
  104. package/dist/cli/commands/logout.js +1 -1
  105. package/dist/cli/commands/mcp.d.ts +12 -0
  106. package/dist/cli/commands/mcp.js +164 -0
  107. package/dist/cli/commands/models.js +3 -2
  108. package/dist/cli/commands/schedules.d.ts +4 -2
  109. package/dist/cli/commands/schedules.js +14 -10
  110. package/dist/cli/commands/secrets.d.ts +14 -0
  111. package/dist/cli/commands/secrets.js +213 -0
  112. package/dist/cli/commands/start.d.ts +14 -14
  113. package/dist/cli/commands/start.js +46 -60
  114. package/dist/cli/content-view.d.ts +4 -6
  115. package/dist/cli/content-view.js +5 -11
  116. package/dist/cli/first-run-channels.d.ts +3 -0
  117. package/dist/cli/first-run-channels.js +63 -0
  118. package/dist/cli/instance.d.ts +41 -0
  119. package/dist/cli/instance.js +155 -0
  120. package/dist/cli/invoke-stream.d.ts +5 -2
  121. package/dist/cli/invoke-stream.js +7 -2
  122. package/dist/cli/program.js +339 -108
  123. package/dist/cli/serve.d.ts +4 -5
  124. package/dist/cli/serve.js +4 -7
  125. package/dist/cli/shared.d.ts +11 -2
  126. package/dist/cli/shared.js +32 -19
  127. package/dist/cli/ticket.js +50 -6
  128. package/dist/content/declare.d.ts +31 -37
  129. package/dist/content/declare.js +59 -60
  130. package/dist/content/file.d.ts +23 -12
  131. package/dist/content/file.js +48 -35
  132. package/dist/content/git.d.ts +0 -10
  133. package/dist/content/git.js +5 -39
  134. package/dist/content/resolve.d.ts +18 -22
  135. package/dist/content/resolve.js +32 -66
  136. package/dist/content/source.d.ts +7 -8
  137. package/dist/content/source.js +10 -28
  138. package/dist/credentials-page.d.ts +26 -0
  139. package/dist/credentials-page.js +185 -0
  140. package/dist/declared-secrets.d.ts +10 -3
  141. package/dist/declared-secrets.js +8 -4
  142. package/dist/deploy/agentcore/destroy.d.ts +4 -1
  143. package/dist/deploy/agentcore/destroy.js +26 -2
  144. package/dist/deploy/agentcore/forwarder.js +1 -1
  145. package/dist/deploy/agentcore/plan.d.ts +7 -21
  146. package/dist/deploy/agentcore/plan.js +47 -49
  147. package/dist/deploy/agentcore/run.d.ts +18 -10
  148. package/dist/deploy/agentcore/run.js +93 -70
  149. package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
  150. package/dist/deploy/agentcore/runtime-secrets.js +108 -0
  151. package/dist/deploy/agentcore/shell.js +12 -19
  152. package/dist/deploy/agentcore/sigv4.d.ts +7 -0
  153. package/dist/deploy/agentcore/sigv4.js +15 -0
  154. package/dist/deploy/agentcore/slr.d.ts +18 -0
  155. package/dist/deploy/agentcore/slr.js +27 -0
  156. package/dist/deploy/agentcore/store.d.ts +7 -0
  157. package/dist/deploy/agentcore/store.js +115 -0
  158. package/dist/deploy/api.d.ts +14 -14
  159. package/dist/deploy/api.js +26 -24
  160. package/dist/deploy/box-shell.d.ts +3 -1
  161. package/dist/deploy/build-context.d.ts +4 -4
  162. package/dist/deploy/build-context.js +11 -11
  163. package/dist/deploy/channel-ingress.d.ts +35 -9
  164. package/dist/deploy/channel-ingress.js +119 -22
  165. package/dist/deploy/container.d.ts +35 -10
  166. package/dist/deploy/container.js +80 -39
  167. package/dist/deploy/delivered-text.d.ts +13 -0
  168. package/dist/deploy/delivered-text.js +86 -0
  169. package/dist/deploy/docker/plan.d.ts +7 -8
  170. package/dist/deploy/docker/plan.js +31 -52
  171. package/dist/deploy/docker/run.d.ts +18 -43
  172. package/dist/deploy/docker/run.js +57 -79
  173. package/dist/deploy/file-store.d.ts +2 -0
  174. package/dist/deploy/file-store.js +17 -0
  175. package/dist/deploy/fly/plan.d.ts +3 -2
  176. package/dist/deploy/fly/plan.js +20 -9
  177. package/dist/deploy/fly/run.d.ts +18 -22
  178. package/dist/deploy/fly/run.js +25 -44
  179. package/dist/deploy/fly/store.d.ts +12 -0
  180. package/dist/deploy/fly/store.js +83 -0
  181. package/dist/deploy/hosts.d.ts +1 -2
  182. package/dist/deploy/hosts.js +1 -2
  183. package/dist/deploy/preflight.d.ts +18 -5
  184. package/dist/deploy/preflight.js +135 -63
  185. package/dist/deploy/railway/ignore.d.ts +8 -0
  186. package/dist/deploy/railway/ignore.js +57 -0
  187. package/dist/deploy/railway/plan.d.ts +1 -2
  188. package/dist/deploy/railway/plan.js +8 -6
  189. package/dist/deploy/railway/run.d.ts +21 -15
  190. package/dist/deploy/railway/run.js +32 -78
  191. package/dist/deploy/railway/store.d.ts +29 -0
  192. package/dist/deploy/railway/store.js +143 -0
  193. package/dist/deploy/runner.d.ts +5 -2
  194. package/dist/deploy/runner.js +4 -3
  195. package/dist/deploy/secret-store.d.ts +63 -0
  196. package/dist/deploy/secret-store.js +98 -0
  197. package/dist/deploy/secrets.d.ts +29 -46
  198. package/dist/deploy/secrets.js +49 -122
  199. package/dist/deploy/workspace.d.ts +6 -12
  200. package/dist/deploy/workspace.js +18 -94
  201. package/dist/dev-supervisor.js +6 -7
  202. package/dist/environment/declare.js +3 -2
  203. package/dist/environment/mise.d.ts +3 -1
  204. package/dist/environment/mise.js +4 -2
  205. package/dist/feishu.d.ts +1 -1
  206. package/dist/feishu.js +1 -1
  207. package/dist/harnesses/models/catalog.d.ts +1 -0
  208. package/dist/harnesses/models/catalog.js +1 -2
  209. package/dist/harnesses/models/environment.d.ts +7 -2
  210. package/dist/harnesses/models/environment.js +9 -5
  211. package/dist/harnesses/models/host.d.ts +0 -7
  212. package/dist/harnesses/models/host.js +10 -1
  213. package/dist/harnesses/models/login.d.ts +0 -8
  214. package/dist/harnesses/models/login.js +5 -4
  215. package/dist/harnesses/models/models.d.ts +7 -2
  216. package/dist/harnesses/models/models.js +28 -4
  217. package/dist/harnesses/pi-durable/agent.js +59 -38
  218. package/dist/harnesses/pi-durable/build.d.ts +12 -0
  219. package/dist/harnesses/pi-durable/build.js +270 -0
  220. package/dist/harnesses/pi-durable/built.d.ts +24 -0
  221. package/dist/harnesses/pi-durable/built.js +136 -0
  222. package/dist/harnesses/pi-durable/config.d.ts +81 -0
  223. package/dist/harnesses/pi-durable/config.js +266 -0
  224. package/dist/harnesses/pi-durable/define.d.ts +63 -13
  225. package/dist/harnesses/pi-durable/define.js +23 -2
  226. package/dist/harnesses/pi-durable/definition.d.ts +72 -0
  227. package/dist/harnesses/pi-durable/definition.js +143 -0
  228. package/dist/harnesses/pi-durable/events.d.ts +4 -3
  229. package/dist/harnesses/pi-durable/events.js +179 -136
  230. package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
  231. package/dist/harnesses/pi-durable/execution-env.js +11 -0
  232. package/dist/harnesses/pi-durable/mcp.d.ts +3 -0
  233. package/dist/harnesses/pi-durable/mcp.js +32 -0
  234. package/dist/harnesses/pi-durable/open.d.ts +36 -38
  235. package/dist/harnesses/pi-durable/open.js +97 -150
  236. package/dist/harnesses/pi-durable/reads.d.ts +0 -1
  237. package/dist/harnesses/pi-durable/reads.js +1 -3
  238. package/dist/harnesses/pi-durable/registry.d.ts +4 -6
  239. package/dist/harnesses/pi-durable/registry.js +29 -26
  240. package/dist/harnesses/pi-durable/release.d.ts +2 -2
  241. package/dist/harnesses/pi-durable/release.js +10 -7
  242. package/dist/harnesses/pi-durable/serve.d.ts +95 -15
  243. package/dist/harnesses/pi-durable/serve.js +151 -59
  244. package/dist/harnesses/pi-durable/sessions.js +15 -12
  245. package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
  246. package/dist/harnesses/pi-durable/text-sources.js +55 -0
  247. package/dist/harnesses/pi-durable/text.d.ts +16 -4
  248. package/dist/harnesses/pi-durable/text.js +115 -59
  249. package/dist/harnesses/pi-durable/wake.js +8 -8
  250. package/dist/http/client.js +9 -3
  251. package/dist/http/server.d.ts +1 -1
  252. package/dist/http/server.js +1 -1
  253. package/dist/index.d.ts +11 -4
  254. package/dist/index.js +8 -1
  255. package/dist/instance-name.d.ts +7 -0
  256. package/dist/instance-name.js +26 -0
  257. package/dist/lark.d.ts +1 -1
  258. package/dist/lark.js +1 -1
  259. package/dist/locked-file.d.ts +2 -2
  260. package/dist/locked-file.js +8 -3
  261. package/dist/mcp/auth.d.ts +22 -0
  262. package/dist/mcp/auth.js +180 -0
  263. package/dist/mcp/client.d.ts +15 -0
  264. package/dist/mcp/client.js +179 -0
  265. package/dist/mcp/config.d.ts +86 -0
  266. package/dist/mcp/config.js +225 -0
  267. package/dist/mcp/grants.d.ts +4 -0
  268. package/dist/mcp/grants.js +132 -0
  269. package/dist/mcp/host.d.ts +19 -0
  270. package/dist/mcp/host.js +128 -0
  271. package/dist/mcp/network.d.ts +12 -0
  272. package/dist/mcp/network.js +150 -0
  273. package/dist/mcp/sign-in.d.ts +2 -0
  274. package/dist/mcp/sign-in.js +204 -0
  275. package/dist/mcp-api.d.ts +107 -0
  276. package/dist/mcp-api.js +10 -0
  277. package/dist/paths.d.ts +22 -8
  278. package/dist/paths.js +44 -11
  279. package/dist/runtime-websocket.d.ts +14 -0
  280. package/dist/runtime-websocket.js +39 -0
  281. package/dist/scaffold/add-channel.d.ts +9 -16
  282. package/dist/scaffold/add-channel.js +13 -83
  283. package/dist/scaffold/templates/env.example +4 -5
  284. package/dist/scaffold/templates/fastagent.config.ts +20 -14
  285. package/dist/scaffold/templates/gitignore +9 -6
  286. package/dist/scaffold/templates.d.ts +6 -2
  287. package/dist/scaffold/templates.js +7 -2
  288. package/dist/scaffold/vendor-skill.js +4 -2
  289. package/dist/service.d.ts +44 -6
  290. package/dist/service.js +110 -10
  291. package/dist/serving-command.d.ts +10 -0
  292. package/dist/serving-command.js +1 -0
  293. package/dist/setup-page.d.ts +51 -0
  294. package/dist/setup-page.js +149 -0
  295. package/dist/slack.d.ts +1 -1
  296. package/dist/slack.js +1 -1
  297. package/dist/strip-edges.d.ts +10 -0
  298. package/dist/strip-edges.js +22 -0
  299. package/dist/telegram.d.ts +1 -1
  300. package/dist/telegram.js +1 -1
  301. package/dist/tunnel.d.ts +0 -1
  302. package/dist/tunnel.js +2 -8
  303. package/dist/value-file.d.ts +12 -6
  304. package/dist/value-file.js +36 -25
  305. package/dist/version.d.ts +7 -1
  306. package/dist/version.js +9 -3
  307. package/package.json +8 -6
  308. package/dist/channels/lark/onboard.d.ts +0 -36
  309. package/dist/channels/lark/onboard.js +0 -56
  310. package/dist/channels/slack/config-api.d.ts +0 -69
  311. package/dist/channels/slack/config-api.js +0 -162
  312. package/dist/channels/slack/onboard.d.ts +0 -40
  313. package/dist/channels/slack/onboard.js +0 -136
  314. package/dist/channels/slack/onboarding-state.d.ts +0 -31
  315. package/dist/channels/slack/onboarding-state.js +0 -64
  316. package/dist/channels/slack/register-webhook.d.ts +0 -12
  317. package/dist/channels/slack/register-webhook.js +0 -67
  318. package/dist/channels/slack/setup-server.d.ts +0 -32
  319. package/dist/channels/slack/setup-server.js +0 -201
  320. package/dist/cli/add-feishu.d.ts +0 -50
  321. package/dist/cli/add-feishu.js +0 -367
  322. package/dist/cli/add-slack.d.ts +0 -18
  323. package/dist/cli/add-slack.js +0 -216
  324. package/dist/harnesses/pi-durable/parked.d.ts +0 -18
  325. package/dist/harnesses/pi-durable/parked.js +0 -27
@@ -1,12 +1,12 @@
1
1
  import type { ServedAgent, ServeOptions, Service } from "../service.ts";
2
- import type { AgentConfig } from "../harnesses/pi-durable/define.ts";
2
+ import type { EffectiveConfig } from "../harnesses/pi-durable/config.ts";
3
3
  /** Refuse `--tunnel` with a `--bind` that cloudflared cannot reach. */
4
4
  export declare function assertTunnelBindable(host: string | undefined, tunnel: boolean): void;
5
5
  /**
6
6
  * What the CLI adds to the assembly: its shutdown grace, exit on a connection that drops, and the API's keys, none
7
7
  * when the definition turns the API off, whatever key the environment holds (a host's from an earlier deploy).
8
8
  */
9
- export declare function cliMountOptions(config: AgentConfig): ServeOptions;
9
+ export declare function cliMountOptions(config: EffectiveConfig): ServeOptions;
10
10
  /**
11
11
  * Bind, report, warn about reach, open the tunnel, and close in order on a signal — the tail dev's worker and start
12
12
  * share once the service is assembled.
@@ -17,11 +17,10 @@ export declare function serveService(service: ServedAgent, bind: {
17
17
  }, posture: {
18
18
  tunnel: boolean;
19
19
  agentDir: string;
20
- stateRoot: string;
21
- config: AgentConfig;
20
+ config: EffectiveConfig;
22
21
  }): void;
23
22
  /** The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and what mounted. */
24
- export declare function reportServing(service: ServedAgent, host: string | undefined, boundPort: number, config: AgentConfig): void;
23
+ export declare function reportServing(service: ServedAgent, host: string | undefined, boundPort: number, config: EffectiveConfig): void;
25
24
  /** The line that names WHERE the serve is — every posture reports it, including AgentCore's own surface. */
26
25
  export declare function bindLine(host: string | undefined, boundPort: number): string;
27
26
  /** Listen, and report ready — but only once the SERVICE is, which is not the same moment — until a signal closes it. */
package/dist/cli/serve.js CHANGED
@@ -30,10 +30,10 @@ export function cliMountOptions(config) {
30
30
  */
31
31
  export function serveService(service, bind, posture) {
32
32
  const { host } = bind;
33
- const { tunnel, agentDir, stateRoot, config } = posture;
33
+ const { tunnel, agentDir, config } = posture;
34
34
  serve(service, bind, (p) => {
35
35
  reportServing(service, host, p, config);
36
- maybeTunnel(agentDir, service.channels.routes, p, tunnel, stateRoot);
36
+ maybeTunnel(agentDir, service.channels.routes, p, tunnel);
37
37
  });
38
38
  }
39
39
  /** The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and what mounted. */
@@ -108,16 +108,13 @@ export function serve(service, bind, onListening) {
108
108
  });
109
109
  }
110
110
  /** Start a Cloudflare tunnel for route channels only. */
111
- function maybeTunnel(agentDir, routeChannels, boundPort, tunnel, stateRoot) {
111
+ function maybeTunnel(agentDir, routeChannels, boundPort, tunnel) {
112
112
  if (!tunnel || process.env.FASTAGENT_DEV_WORKER === "1")
113
113
  return;
114
114
  void startCloudflareTunnel(boundPort).then((instance) => {
115
115
  if (!instance)
116
116
  return;
117
- void announceWebhooks(agentDir, instance.url, declaredChannels(routeChannels), {
118
- openUrl: openExternalUrl,
119
- stateRoot,
120
- });
117
+ void announceWebhooks(agentDir, instance.url, declaredChannels(routeChannels), { openUrl: openExternalUrl });
121
118
  const cleanup = () => instance.close();
122
119
  process.once("SIGINT", cleanup);
123
120
  process.once("SIGTERM", cleanup);
@@ -1,6 +1,10 @@
1
1
  import { type LoginIO } from "../harnesses/models/login.ts";
2
2
  import { type ModelEnvironment } from "../harnesses/models/environment.ts";
3
3
  import type { OpenedServing } from "../harnesses/pi-durable/serve.ts";
4
+ import { type Project } from "../harnesses/pi-durable/open.ts";
5
+ import { type InstanceFlags } from "./instance.ts";
6
+ /** The project a command runs, when it is not the development one (`start` runs the build). */
7
+ type LoadProject = (agentDir: string) => Promise<Project>;
4
8
  /** An agent directory a command entered, and the default model that then resolves, if any. */
5
9
  export interface EnteredAgent {
6
10
  agentDir: string;
@@ -15,7 +19,8 @@ export interface EnteredAgent {
15
19
  export declare function enterAgentDirectory(dirArg: string, opts: {
16
20
  model?: string;
17
21
  input?: boolean;
18
- }, before?: (agentDir: string) => Promise<void>): Promise<EnteredAgent>;
22
+ instance?: InstanceFlags;
23
+ }, before?: (agentDir: string) => Promise<void>, load?: LoadProject): Promise<EnteredAgent>;
19
24
  /**
20
25
  * How every command that RUNS the agent on this machine enters it (dev, start, invoke, schedules run):
21
26
  * {@link enterAgentDirectory}, plus a default model. The agent opens without one, but a process here would then fail
@@ -24,7 +29,8 @@ export declare function enterAgentDirectory(dirArg: string, opts: {
24
29
  export declare function enterAgentCommand(dirArg: string, opts: {
25
30
  model?: string;
26
31
  input?: boolean;
27
- }, before?: (agentDir: string) => Promise<void>): Promise<Required<EnteredAgent>>;
32
+ instance?: InstanceFlags;
33
+ }, before?: (agentDir: string) => Promise<void>, load?: LoadProject): Promise<Required<EnteredAgent>>;
28
34
  /** What `dev` and `start` say about the agent they just opened, in the order they say it. */
29
35
  export declare function reportAssembly(opened: OpenedServing, modelSpec: string, extras?: [label: string, value: string][]): Promise<void>;
30
36
  /** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
@@ -38,7 +44,10 @@ export declare function parsePort(value: string | undefined, source: string, fro
38
44
  export declare function parseBind(value: string | undefined): string | undefined;
39
45
  /** Report which source provides the model's credentials, surfacing a remediation hint at startup. */
40
46
  export declare function reportAuth(models: ModelEnvironment, modelSpec: string, agentDir: string): Promise<void>;
47
+ /** The line of `fastagent.config.ts` that keeps a picked model: what the picker prints, and what it must accept. */
48
+ export declare function pickedModelLine(model: string): string;
41
49
  /** Login terminal IO via @clack/prompts: a searchable list once long, a hidden prompt for keys. */
42
50
  export declare function terminalLoginIO(): LoginIO;
43
51
  /** A copy/paste-safe POSIX shell argument for a command the CLI prints. */
44
52
  export declare function shellArg(value: string): string;
53
+ export {};
@@ -2,6 +2,7 @@
2
2
  * Helpers shared across command modules: interactivity gates, port parsing, the startup auth report, first-run model
3
3
  * resolution, and the login terminal IO.
4
4
  */
5
+ import { definedValues } from "../declared-secrets.js";
5
6
  import { resolve } from "node:path";
6
7
  import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
7
8
  import { buildModelPickerOptions } from "./models-view.js";
@@ -9,12 +10,13 @@ import { loginFlow } from "../harnesses/models/login.js";
9
10
  import { SignInCancelled } from "../sign-in-api.js";
10
11
  import { modelEnvironment } from "../harnesses/models/environment.js";
11
12
  import { listModels, providerAuthStatuses, providerOf } from "../harnesses/models/models.js";
12
- import { definitionProviders, loadConfig, modelSpecFor } from "../harnesses/pi-durable/open.js";
13
+ import { resolveConfig } from "../harnesses/pi-durable/config.js";
14
+ import { buildDefinitionShape, definitionProviders, loadDevProject, modelSpecFor, } from "../harnesses/pi-durable/open.js";
13
15
  import { formatAuthReport } from "./auth-view.js";
14
16
  import { contentLines } from "./content-view.js";
15
17
  import { deployedHost, isDeployedWorkspace } from "../paths.js";
16
18
  import { log } from "../log.js";
17
- import { enterAgentEnv } from "../env.js";
19
+ import { enterInstance } from "./instance.js";
18
20
  import { dotEnvPath } from "../value-file.js";
19
21
  import { openExternalUrl } from "../open-url.js";
20
22
  import { bindAddress, isBindAddress } from "../bind.js";
@@ -25,11 +27,11 @@ import { agentDirOrExit, failStartup, failUsage } from "./fail.js";
25
27
  * gates a missing model itself, by what will ship. `before`: what the command checks of the directory before a model
26
28
  * is asked for (`schedules run`: that the schedule it names exists).
27
29
  */
28
- export async function enterAgentDirectory(dirArg, opts, before) {
30
+ export async function enterAgentDirectory(dirArg, opts, before, load) {
29
31
  const agentDir = agentDirOrExit(resolve(dirArg));
30
- enterAgentEnv(agentDir);
32
+ enterInstance(agentDir, opts.instance);
31
33
  await before?.(agentDir);
32
- const modelSpec = await resolveFirstRunModel(agentDir, opts);
34
+ const modelSpec = await resolveFirstRunModel(agentDir, opts, load);
33
35
  return { agentDir, ...(modelSpec ? { modelSpec } : {}) };
34
36
  }
35
37
  /**
@@ -37,11 +39,11 @@ export async function enterAgentDirectory(dirArg, opts, before) {
37
39
  * {@link enterAgentDirectory}, plus a default model. The agent opens without one, but a process here would then fail
38
40
  * every new conversation, so it stops at startup instead: exit 2, the choice a terminal's picker would have asked for.
39
41
  */
40
- export async function enterAgentCommand(dirArg, opts, before) {
41
- const entered = await enterAgentDirectory(dirArg, opts, before);
42
+ export async function enterAgentCommand(dirArg, opts, before, load) {
43
+ const entered = await enterAgentDirectory(dirArg, opts, before, load);
42
44
  const { modelSpec } = entered;
43
45
  if (!modelSpec) {
44
- failUsage(`missing model: set --model, "model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai/gpt-5.5"; ` +
46
+ failUsage(`missing model: set --model, "defaults.model" in fastagent.config.ts, or FASTAGENT_MODEL (e.g. "openai/gpt-5.5"; ` +
45
47
  "`fastagent models` lists them)");
46
48
  }
47
49
  return { ...entered, modelSpec };
@@ -52,11 +54,11 @@ function reportLine(label, value) {
52
54
  }
53
55
  /** What `dev` and `start` say about the agent they just opened, in the order they say it. */
54
56
  export async function reportAssembly(opened, modelSpec, extras = []) {
55
- const { definition, code, models, agentDir, stateRoot } = opened;
56
- reportLine("agent", `${definition.name} — ${agentDir}`);
57
+ const { code, models, agentDir, workdir, stateRoot, config } = opened;
58
+ reportLine("agent", `${opened.name} — ${workdir}`);
57
59
  for (const [label, value] of contentLines(opened.content))
58
60
  reportLine(label, value);
59
- reportLine("model", `${modelSpec}${definition.config.thinkingLevel ? ` (thinking: ${definition.config.thinkingLevel})` : ""}`);
61
+ reportLine("model", `${modelSpec}${config.thinkingLevel ? ` (thinking: ${config.thinkingLevel})` : ""}`);
60
62
  await reportAuth(models, modelSpec, agentDir);
61
63
  const names = (list) => list.map(({ name }) => name).join(", ") || "(none)";
62
64
  reportLine("tools", names(code.tools));
@@ -126,24 +128,35 @@ export async function reportAuth(models, modelSpec, agentDir) {
126
128
  * The default model, for every command that runs the agent: `--model` > `FASTAGENT_MODEL` > `fastagent.config.ts`. With
127
129
  * none and a terminal, the first-run picker chooses one for this run (and logs in to its provider when needed).
128
130
  */
129
- async function resolveFirstRunModel(agentDir, options = {}) {
130
- const config = await loadConfig(agentDir).catch(failStartup);
131
- const configured = modelSpecFor(config, options.model);
132
- if (configured)
133
- return configured;
131
+ async function resolveFirstRunModel(agentDir, options = {}, load) {
132
+ const configured = async () => {
133
+ const project = await (load ?? loadDevProject)(agentDir);
134
+ return { project, config: resolveConfig(project.definition, project.instance) };
135
+ };
136
+ // A custom loader's refusal is the caller's to report: AgentCore runs this inside a request, where an exit kills the container.
137
+ const { project, config } = await (load === undefined ? configured().catch(failStartup) : configured());
138
+ const spec = modelSpecFor(config, options.model);
139
+ if (spec)
140
+ return spec;
134
141
  if (options.input === false)
135
142
  return undefined; // --no-input: never prompt (clig) — the caller decides what's missing
136
143
  if (!isInteractive())
137
144
  return undefined; // CI/deploy: the caller raises the actionable missing-model error
138
- const providers = await definitionProviders(agentDir).catch(failStartup);
139
- const environment = await modelEnvironment(agentDir, {}, providers);
145
+ const providers = load === undefined
146
+ ? await definitionProviders(agentDir, process.env).catch(failStartup)
147
+ : buildDefinitionShape(project.definition, process.env).providers;
148
+ const environment = await modelEnvironment(agentDir, {}, providers, { environment: definedValues(process.env) });
140
149
  const chosen = await pickWithCredentials(environment.models, environment.auth?.path, providers);
141
150
  if (chosen === undefined)
142
151
  return undefined; // cancelled (or auth probe failed): the caller raises its clear error
143
152
  process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
144
- console.error(`[fastagent] picked ${chosen} — set \`model: ${JSON.stringify(chosen)}\` in fastagent.config.ts to keep it`);
153
+ console.error(`[fastagent] picked ${chosen} — set \`${pickedModelLine(chosen)}\` in fastagent.config.ts to keep it`);
145
154
  return chosen;
146
155
  }
156
+ /** The line of `fastagent.config.ts` that keeps a picked model: what the picker prints, and what it must accept. */
157
+ export function pickedModelLine(model) {
158
+ return `defaults: { model: ${JSON.stringify(model)} }`;
159
+ }
147
160
  /** The credential-aware pick: full catalog annotated per provider, then the post-pick auth policy. */
148
161
  async function pickWithCredentials(models, authPath, providers) {
149
162
  let statuses;
@@ -6,8 +6,9 @@
6
6
  * there is something for the person to do. The command's `--check <ticket>` reads where it stands from any process,
7
7
  * and hands the step the address a browser ended on when that browser could not reach this machine.
8
8
  *
9
- * A ticket is a file in the machine's own state, ~/.fastagent/.state/tickets, written whole by one process at a time:
10
- * the starting command until the step starts, then the step alone (or whoever finds its process gone). One that has
9
+ * A ticket is a file in the machine's own state, ~/.fastagent/.state/tickets, written whole (`writeFileAtomic`), but by
10
+ * more than one process at once: the starting command until the step starts, then the step, and whoever finds its
11
+ * process gone or replaces it (every `--check`, a newer step for the subject). One that has
11
12
  * ended stays as it ended. A newer step for the same subject stops an older one still waiting on its person, whose
12
13
  * process would otherwise hold what it holds (a sign-in's callback port, OpenAI's fixed) until it expires; so does one
13
14
  * still getting ready (reaching a box, opening a tunnel), which may hang there. One finishing what its person did (a
@@ -18,9 +19,10 @@
18
19
  * stopped with it, by `stop` and when it exits.
19
20
  */
20
21
  import { randomBytes } from "node:crypto";
21
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync } from "node:fs";
22
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync, } from "node:fs";
22
23
  import { spawn } from "node:child_process";
23
24
  import { join } from "node:path";
25
+ import { stripVTControlCharacters } from "node:util";
24
26
  import { writeFileAtomic } from "../atomic-write.js";
25
27
  import { STATE_DIRNAME, globalHome } from "../paths.js";
26
28
  import { recordFailures } from "./fail.js";
@@ -50,7 +52,12 @@ function readTicket(id) {
50
52
  throw error;
51
53
  }
52
54
  }
53
- const writeTicket = (ticket) => writeFileAtomic(ticketPath(ticket.ticket), JSON.stringify(ticket));
55
+ /**
56
+ * A ticket is written by several processes: the command that started the step, the step's own process, and every
57
+ * `--check`. Each writes a temp of its own (`sharedWriters`): the fixed name is for one writer, and two of them rename
58
+ * each other's file away or write one temp at once, which a reader sees as `Unexpected non-whitespace character after JSON`.
59
+ */
60
+ const writeTicket = (ticket) => writeFileAtomic(ticketPath(ticket.ticket), JSON.stringify(ticket), undefined, true);
54
61
  const waiting = (ticket) => ticket.status === "starting" || ticket.status === "need_user_action" || ticket.status === "working";
55
62
  /**
56
63
  * Change the ticket `id` while its step still runs; one that has ended is returned as it ended. A writer that read it
@@ -246,7 +253,7 @@ export async function checkTicket(id, address) {
246
253
  if (ticket === undefined)
247
254
  return undefined;
248
255
  if (address !== undefined && ticket.status === "need_user_action") {
249
- writeFileAtomic(addressPath(id), address);
256
+ writeFileAtomic(addressPath(id), address, undefined, true);
250
257
  for (const deadline = Date.now() + ADDRESS_TIMEOUT_MS; Date.now() < deadline;) {
251
258
  ticket = readTicket(id);
252
259
  if (ticket.status !== "need_user_action" || !running(ticket.pid))
@@ -385,6 +392,33 @@ async function awaitAddress(id, signal) {
385
392
  }
386
393
  throw new Error("the step no longer waits for an address");
387
394
  }
395
+ const LOG_TAIL_LINES = 12;
396
+ const LOG_TAIL_BYTES = 8192;
397
+ const LOG_LINE_CHARS = 300;
398
+ /**
399
+ * The last lines of a ticket's log, without terminal colors: where a failed step says why. A coding agent reads a
400
+ * report once, and "see the output above" points at a log it has not opened.
401
+ */
402
+ function logTail(id) {
403
+ const path = ticketLogPath(id);
404
+ if (!existsSync(path))
405
+ return [];
406
+ const size = statSync(path).size;
407
+ const start = Math.max(0, size - LOG_TAIL_BYTES);
408
+ const fd = openSync(path, "r");
409
+ try {
410
+ const buffer = Buffer.alloc(size - start);
411
+ readSync(fd, buffer, 0, buffer.length, start);
412
+ return stripVTControlCharacters(buffer.toString("utf8"))
413
+ .split("\n")
414
+ .map((line) => line.trim().slice(0, LOG_LINE_CHARS))
415
+ .filter((line) => line !== "")
416
+ .slice(-LOG_TAIL_LINES);
417
+ }
418
+ finally {
419
+ closeSync(fd);
420
+ }
421
+ }
388
422
  /**
389
423
  * A ticket as its command reports it, with what to do next (`agent_next_steps`): one JSON object, or its lines for a
390
424
  * person. `done` is what the step's `result` adds to the report, beside a sentence for the agent.
@@ -412,17 +446,27 @@ export function ticketReport(ticket, when, done = (result) => ({ fields: result,
412
446
  const message = ticket.status === "expired" ? "it was not finished in time" : ticket.message;
413
447
  // What the step reached before it ended is still the person's: where to get the value it stopped for, say.
414
448
  const reached = left.length === 0 ? "" : `Tell the user what the steps it reached leave them: ${left.join(" ")} `;
449
+ // An expired step is not a fault: nobody finished it in time, and its log says nothing more.
450
+ const expired = ticket.status === "expired";
451
+ const tail = expired ? [] : logTail(ticket.ticket);
415
452
  return {
416
453
  json: {
417
454
  ...base,
418
455
  message,
419
456
  log,
457
+ ...(tail.length === 0 ? {} : { log_tail: tail }),
420
458
  ...(ticket.afterwards ? { afterwards: ticket.afterwards } : {}),
421
- agent_next_steps: `${reached}To try again, run: ${ticket.restart} --json`,
459
+ agent_next_steps: expired
460
+ ? `${reached}The step was not finished in time, which is not a fault: ask the user to complete it as soon ` +
461
+ `as you run: ${ticket.restart} --json`
462
+ : `${reached}The cause is in log_tail (the whole log is at log): fix what it names, then run: ` +
463
+ `${ticket.restart} --json. The same inputs fail the same way unless the log shows a transient network or ` +
464
+ `builder error.`,
422
465
  },
423
466
  text: [
424
467
  `${ticket.status}: ${message}`,
425
468
  `log: ${log}`,
469
+ ...tail.map((line) => ` | ${line}`),
426
470
  ...left.map((line) => `left for the person: ${line}`),
427
471
  `to try again: ${ticket.restart}`,
428
472
  ],
@@ -1,62 +1,56 @@
1
- /** One content entry as `context.json` holds it, under its name: a repository. */
2
- export interface ContentEntry {
1
+ /** One content entry as a file holds it, under its name: a repository, or a directory of this machine. */
2
+ export type ContentEntry = {
3
3
  /** The repository, `owner/repo`. */
4
4
  github: string;
5
5
  /** The branch, tag or commit to clone. */
6
6
  ref?: string;
7
7
  /** One sentence for the agent: what this is, and how to treat it. */
8
8
  description?: string;
9
- }
9
+ } | {
10
+ /** Absolute, `~/`, or relative to the instance configuration. */
11
+ path: string;
12
+ description?: string;
13
+ };
10
14
  /** A declared entry, read. */
11
- export interface DeclaredContent {
15
+ export type DeclaredContent = {
12
16
  name: string;
17
+ description?: string;
18
+ /** The input that declared it. */
19
+ origin: "program" | "instance";
20
+ } & ({
21
+ kind: "github";
13
22
  /** `owner/repo`. */
14
23
  repo: string;
15
24
  ref?: string;
16
- description?: string;
17
- }
18
- /**
19
- * One binding as `context.local.json` holds it, under its name: a directory of this machine. Under the name of an
20
- * entry `context.json` declares it is this machine's checkout of that repository, and carries only `path`.
21
- */
22
- export interface ContentBinding {
23
- /** Absolute, `~/`, or relative to the agent directory. */
24
- path: string;
25
- description?: string;
26
- }
27
- /** A binding, read: `path` absolute. */
28
- export type BoundContent = {
29
- name: string;
30
- path: string;
31
- } & ({
32
- /** This machine's checkout of the declared entry of the same name. */
33
- kind: "checkout";
34
25
  } | {
35
- /** A directory only this machine has. */
26
+ /** A directory of this machine, `path` absolute. */
36
27
  kind: "local";
37
- description?: string;
28
+ path: string;
38
29
  });
39
- /** What an agent's content is declared as: the shared declaration, and this machine's bindings. */
40
- export interface ContentDeclaration {
41
- declared: DeclaredContent[];
42
- bound: BoundContent[];
30
+ /** Where a `content` map was read from, for what a refusal says and how a path is read. */
31
+ export interface ContentSource {
32
+ origin: DeclaredContent["origin"];
33
+ /** The file's name, or what an instance configuration is called. */
34
+ file: string;
35
+ /** What a relative path is relative to: the instance configuration's directory. None for one that is no file. */
36
+ base?: string;
43
37
  }
44
38
  /** Whether `name` can name a content entry. */
45
39
  export declare function isContentName(name: string): boolean;
46
40
  /** Whether `repo` names a GitHub repository, `owner/repo`. */
47
41
  export declare function isGithubRepo(repo: string): boolean;
48
- /** `entry` with its keys in {@link ENTRY_KEYS} order: the form written. */
42
+ /** `entry` with its keys in the order they are written. */
49
43
  export declare function canonicalEntry(entry: ContentEntry): ContentEntry;
50
- /** `binding` with its keys in {@link BINDING_KEYS} order: the form written. */
51
- export declare function canonicalBinding(binding: ContentBinding): ContentBinding;
52
- /** Read `context.json`'s `content` map (undefined is none). Every refusal names the entry; nothing is defaulted. */
53
- export declare function declareContent(raw: unknown): DeclaredContent[];
54
44
  /**
55
- * Read `context.local.json`'s `content` map (undefined is none) beside `declared`, with relative paths resolved against
56
- * `agentDir`. A binding under a declared entry's name is that entry's checkout here; any other is a directory of this
57
- * machine alone.
45
+ * Read a `content` map (undefined is none) from `from`. Every refusal names the entry; nothing is defaulted. The
46
+ * program's declares repositories only, since it is shared; an instance's may name a directory too.
47
+ */
48
+ export declare function declareContent(raw: unknown, from: ContentSource): DeclaredContent[];
49
+ /**
50
+ * The program's entries and the instance's, as one list. An instance only adds: a name both declare (ignoring case,
51
+ * as names are a directory) is refused, naming both, so nothing is shadowed.
58
52
  */
59
- export declare function declareBindings(raw: unknown, declared: readonly DeclaredContent[], agentDir: string): BoundContent[];
53
+ export declare function joinContent(program: readonly DeclaredContent[], instance: readonly DeclaredContent[]): DeclaredContent[];
60
54
  /**
61
55
  * A directory the agent is bound to and the agent directory are kept apart (agent-model.md §2): a definition is
62
56
  * released, content is kept, and one directory cannot be both. The ONE statement of the rule; resolve.ts asks
@@ -1,18 +1,18 @@
1
1
  /**
2
- * WHAT AN AUTHOR DECLARES an agent works on, read and refused in ONE place (file.ts reads the files through
3
- * it): the `content` map of `context.json`, which is shared and so holds only repositories every instance reaches, and
4
- * the `content` map of `context.local.json`, the directories of this machine it is bound to. Pure: what is on the disk
5
- * is resolve.ts's question. docs/design/agent-model.md §3 is the rule.
2
+ * WHAT AN AGENT WORKS ON, declared and refused in ONE place (file.ts reads the files through it): the `content` map of
3
+ * the program's `context.json`, which holds only repositories every instance reaches, and the `content` map of an
4
+ * instance's `fastagent.json`, which adds repositories and directories of its own. An instance only adds: a name the
5
+ * program declares is not one it may take. Pure: what is on the disk is resolve.ts's question.
6
+ * docs/design/agent-artifact.md §3.3 and §4.2 are the rule.
6
7
  */
7
- import { homedir } from "node:os";
8
- import { isAbsolute, join, resolve } from "node:path";
9
- import { CONTEXT_FILE, LOCAL_CONTEXT_FILE, isUnderDir } from "../paths.js";
8
+ import { isAbsolute } from "node:path";
9
+ import { CONTEXT_FILE, configurationPath, isUnderDir } from "../paths.js";
10
10
  /**
11
- * The keys an entry and a binding carry, in the order they are written: where it is, then how it is treated. One list
12
- * each, so a key the reader accepts is one the writer keeps.
11
+ * The keys an entry carries, in the order they are written: where it is, then how it is treated. One list each, so a
12
+ * key the reader accepts is one the writer keeps.
13
13
  */
14
- const ENTRY_KEYS = ["github", "ref", "description"];
15
- const BINDING_KEYS = ["path", "description"];
14
+ const GITHUB_KEYS = ["github", "ref", "description"];
15
+ const PATH_KEYS = ["path", "description"];
16
16
  /** A name becomes a directory name, so it is one path segment (the spelling a release's agent name has). */
17
17
  const NAME = /^[A-Za-z0-9_-]+$/;
18
18
  /** Whether `name` can name a content entry. */
@@ -24,13 +24,9 @@ const GITHUB_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
24
24
  export function isGithubRepo(repo) {
25
25
  return GITHUB_REPO.test(repo);
26
26
  }
27
- /** `entry` with its keys in {@link ENTRY_KEYS} order: the form written. */
27
+ /** `entry` with its keys in the order they are written. */
28
28
  export function canonicalEntry(entry) {
29
- return pick(entry, ENTRY_KEYS);
30
- }
31
- /** `binding` with its keys in {@link BINDING_KEYS} order: the form written. */
32
- export function canonicalBinding(binding) {
33
- return pick(binding, BINDING_KEYS);
29
+ return pick(entry, "github" in entry ? GITHUB_KEYS : PATH_KEYS);
34
30
  }
35
31
  function pick(value, keys) {
36
32
  const out = {};
@@ -41,42 +37,26 @@ function pick(value, keys) {
41
37
  }
42
38
  return out;
43
39
  }
44
- /** Read `context.json`'s `content` map (undefined is none). Every refusal names the entry; nothing is defaulted. */
45
- export function declareContent(raw) {
46
- return entriesByName(raw).map(([name, entry]) => declareOne(name, entry));
40
+ /**
41
+ * Read a `content` map (undefined is none) from `from`. Every refusal names the entry; nothing is defaulted. The
42
+ * program's declares repositories only, since it is shared; an instance's may name a directory too.
43
+ */
44
+ export function declareContent(raw, from) {
45
+ return entriesByName(raw).map(([name, entry]) => declareOne(name, entry, from));
47
46
  }
48
47
  /**
49
- * Read `context.local.json`'s `content` map (undefined is none) beside `declared`, with relative paths resolved against
50
- * `agentDir`. A binding under a declared entry's name is that entry's checkout here; any other is a directory of this
51
- * machine alone.
48
+ * The program's entries and the instance's, as one list. An instance only adds: a name both declare (ignoring case,
49
+ * as names are a directory) is refused, naming both, so nothing is shadowed.
52
50
  */
53
- export function declareBindings(raw, declared, agentDir) {
54
- return entriesByName(raw).map(([name, binding]) => {
55
- const at = `content "${name}"`;
56
- const e = objectWithKeys(at, binding, BINDING_KEYS);
57
- if (typeof e.path !== "string" || e.path === "")
58
- throw new Error(`${at}: "path" must be a non-empty string`);
59
- const path = absolutePath(e.path, agentDir);
60
- const shared = declared.find((entry) => entry.name.toLowerCase() === name.toLowerCase());
61
- if (shared !== undefined) {
62
- if (shared.name !== name) {
63
- throw new Error(`${at} binds the entry ${CONTEXT_FILE} names "${shared.name}": spell it the same`);
64
- }
65
- if (e.description !== undefined) {
66
- throw new Error(`${at} binds this machine's checkout of the entry ${CONTEXT_FILE} declares, which says "description" for ` +
67
- `every instance: give it only "path"`);
68
- }
69
- return { name, path, kind: "checkout" };
51
+ export function joinContent(program, instance) {
52
+ for (const added of instance) {
53
+ const taken = program.find((entry) => entry.name.toLowerCase() === added.name.toLowerCase());
54
+ if (taken !== undefined) {
55
+ throw new Error(`content "${added.name}" is declared by the program (${CONTEXT_FILE} names "${taken.name}") and by the ` +
56
+ `instance: an instance adds to the program's content and does not replace it — name it otherwise`);
70
57
  }
71
- const description = descriptionOf(at, e);
72
- return { name, path, kind: "local", ...(description !== undefined ? { description } : {}) };
73
- });
74
- }
75
- /** `path` as written in `context.local.json`, absolute: `~/` is the home directory, a relative one the agent's. */
76
- function absolutePath(path, agentDir) {
77
- if (path === "~" || path.startsWith("~/"))
78
- return join(homedir(), path.slice(1));
79
- return isAbsolute(path) ? resolve(path) : resolve(agentDir, path);
58
+ }
59
+ return [...program, ...instance];
80
60
  }
81
61
  /** The entries of a `content` map, each name checked, unique ignoring case. */
82
62
  function entriesByName(raw) {
@@ -110,24 +90,43 @@ function objectWithKeys(at, value, keys) {
110
90
  }
111
91
  return e;
112
92
  }
113
- function descriptionOf(at, e) {
114
- if (e.description !== undefined && (typeof e.description !== "string" || e.description === "")) {
115
- throw new Error(`${at}: "description" must be a non-empty string`);
93
+ /** `path` as written in an instance configuration, absolute: `~/` is the home directory, a relative one `base`'s. */
94
+ function absolutePath(path, from, at) {
95
+ const absolute = configurationPath(path, from.base);
96
+ if (absolute === undefined) {
97
+ throw new Error(`${at}: "path" must be absolute here, since ${from.file} is not a file its directory could anchor`);
116
98
  }
117
- return e.description;
99
+ return absolute;
118
100
  }
119
- function declareOne(name, entry) {
101
+ function declareOne(name, entry, from) {
120
102
  const at = `content "${name}"`;
121
- const e = objectWithKeys(at, entry, ENTRY_KEYS);
122
- for (const key of ["github", "ref"]) {
103
+ const e = objectWithKeys(at, entry, [...GITHUB_KEYS, "path"]);
104
+ if (from.origin === "program" && e.path !== undefined) {
105
+ throw new Error(`${at}: ${from.file} is shared, so it declares only the repositories every instance reaches — add a directory ` +
106
+ `to the instance's configuration instead (fastagent content add <dir>)`);
107
+ }
108
+ for (const key of ["github", "ref", "path"]) {
123
109
  if (e[key] !== undefined && (typeof e[key] !== "string" || e[key] === "")) {
124
110
  throw new Error(`${at}: "${key}" must be a non-empty string`);
125
111
  }
126
112
  }
127
- const description = descriptionOf(at, e);
113
+ if (e.description !== undefined && (typeof e.description !== "string" || e.description === "")) {
114
+ throw new Error(`${at}: "description" must be a non-empty string`);
115
+ }
116
+ const base = {
117
+ name,
118
+ origin: from.origin,
119
+ ...(e.description !== undefined ? { description: e.description } : {}),
120
+ };
121
+ if (e.github !== undefined && e.path !== undefined)
122
+ throw new Error(`${at} names both "github" and "path": give one`);
123
+ if (e.path !== undefined) {
124
+ if (e.ref !== undefined)
125
+ throw new Error(`${at}: "ref" applies to a repository, and ${at} is a directory`);
126
+ return { ...base, kind: "local", path: absolutePath(e.path, from, at) };
127
+ }
128
128
  if (e.github === undefined) {
129
- throw new Error(`${at} needs "github": ${CONTEXT_FILE} is shared, so it declares only what every instance reaches — bind a ` +
130
- `directory of this machine in ${LOCAL_CONTEXT_FILE} instead (fastagent content add <dir>)`);
129
+ throw new Error(`${at} needs "github"${from.origin === "instance" ? ' or "path"' : ""}`);
131
130
  }
132
131
  const repo = e.github;
133
132
  if (!isGithubRepo(repo))
@@ -136,7 +135,7 @@ function declareOne(name, entry) {
136
135
  // git reads a leading "-" as an option, and no branch, tag or commit name has one.
137
136
  if (ref?.startsWith("-"))
138
137
  throw new Error(`${at}: "ref" must name a branch, tag or commit, got "${ref}"`);
139
- return { name, repo, ...(ref !== undefined ? { ref } : {}), ...(description !== undefined ? { description } : {}) };
138
+ return { ...base, kind: "github", repo, ...(ref !== undefined ? { ref } : {}) };
140
139
  }
141
140
  /**
142
141
  * A directory the agent is bound to and the agent directory are kept apart (agent-model.md §2): a definition is