@cursor/july 0.1.101 → 0.1.102

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 (374) hide show
  1. package/README.md +27 -0
  2. package/dist/channels/slack/api.d.ts +1 -1
  3. package/dist/channels/slack/api.d.ts.map +1 -1
  4. package/dist/channels/slack/api.js +3 -1
  5. package/dist/channels/slack/assistant.d.ts +1 -1
  6. package/dist/channels/slack/assistant.d.ts.map +1 -1
  7. package/dist/channels/slack/dispatch.d.ts +3 -2
  8. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  9. package/dist/channels/slack/dispatch.js +28 -8
  10. package/dist/channels/slack/inbound.d.ts +8 -0
  11. package/dist/channels/slack/inbound.d.ts.map +1 -1
  12. package/dist/channels/slack/inbound.js +28 -0
  13. package/dist/channels/slack/manifest.d.ts.map +1 -1
  14. package/dist/channels/slack/manifest.js +2 -1
  15. package/dist/docs/404.html +2 -2
  16. package/dist/docs/ab.html +4 -4
  17. package/dist/docs/assets/{app.BASO9Kih.js → app.CLA03RCz.js} +1 -1
  18. package/dist/docs/assets/chunks/@localSearchIndexroot.FAqdWIXo.js +1 -0
  19. package/dist/docs/assets/chunks/{VPLocalSearchBox.DwkqxoE0.js → VPLocalSearchBox.ifxv-cJV.js} +1 -1
  20. package/dist/docs/assets/chunks/{theme.CCil_13e.js → theme.DWCwM6p-.js} +2 -2
  21. package/dist/docs/assets/concepts.md.rPzW2HZN.js +1 -0
  22. package/dist/docs/assets/guides_cloud-agents.md.DumwWXba.js +9 -0
  23. package/dist/docs/assets/guides_cloud-agents.md.DumwWXba.lean.js +1 -0
  24. package/dist/docs/assets/{guides_cloud-runtime.md.CkYbjnAX.js → guides_cloud-runtime.md.BRirbRAN.js} +1 -1
  25. package/dist/docs/assets/index.md.Kiht7ell.js +5 -0
  26. package/dist/docs/assets/{index.md.Bb4k8kUm.lean.js → index.md.Kiht7ell.lean.js} +1 -1
  27. package/dist/docs/assets/reference_extensions.md.9WQMZe1l.js +56 -0
  28. package/dist/docs/assets/reference_extensions.md.9WQMZe1l.lean.js +1 -0
  29. package/dist/docs/assets/{reference_project-layout.md.Bv4KOtlB.js → reference_project-layout.md.CA6QP6Lw.js} +2 -2
  30. package/dist/docs/assets/{reference_project-layout.md.Bv4KOtlB.lean.js → reference_project-layout.md.CA6QP6Lw.lean.js} +1 -1
  31. package/dist/docs/assets/{reference_skills.md.BFW9retM.js → reference_skills.md.l2ezAcaN.js} +1 -1
  32. package/dist/docs/assets/{reference_tools.md.CvAHsdSp.js → reference_tools.md.B1up2r9W.js} +1 -1
  33. package/dist/docs/assets/skills_framework-map.md.DDa4MeEW.js +1 -0
  34. package/dist/docs/assets/{skills_framework-map.md.BTi817yv.lean.js → skills_framework-map.md.DDa4MeEW.lean.js} +1 -1
  35. package/dist/docs/building-with-agents.html +4 -4
  36. package/dist/docs/concepts.html +5 -5
  37. package/dist/docs/concepts.md +3 -3
  38. package/dist/docs/deployment.html +4 -4
  39. package/dist/docs/evals.html +4 -4
  40. package/dist/docs/guides/agent-to-agent.html +4 -4
  41. package/dist/docs/guides/cloud-agents.html +35 -0
  42. package/dist/docs/guides/cloud-agents.md +93 -0
  43. package/dist/docs/guides/cloud-runtime.html +6 -6
  44. package/dist/docs/guides/cloud-runtime.md +2 -0
  45. package/dist/docs/guides/convert-automation.html +4 -4
  46. package/dist/docs/guides/github.html +4 -4
  47. package/dist/docs/guides/human-in-the-loop.html +4 -4
  48. package/dist/docs/guides/mcp-oauth.html +4 -4
  49. package/dist/docs/guides/opentelemetry.html +5 -5
  50. package/dist/docs/guides/slack.html +4 -4
  51. package/dist/docs/guides/webhooks.html +4 -4
  52. package/dist/docs/hashmap.json +1 -1
  53. package/dist/docs/hillclimbing.html +4 -4
  54. package/dist/docs/index.html +6 -6
  55. package/dist/docs/index.md +5 -1
  56. package/dist/docs/llms-full.txt +321 -4
  57. package/dist/docs/llms.txt +2 -0
  58. package/dist/docs/quickstart.html +4 -4
  59. package/dist/docs/reference/agent-config.html +4 -4
  60. package/dist/docs/reference/artifacts.html +4 -4
  61. package/dist/docs/reference/channels.html +5 -5
  62. package/dist/docs/reference/cli.html +4 -4
  63. package/dist/docs/reference/connections.html +4 -4
  64. package/dist/docs/reference/extensions.html +82 -0
  65. package/dist/docs/reference/extensions.md +200 -0
  66. package/dist/docs/reference/hooks.html +4 -4
  67. package/dist/docs/reference/http-api.html +4 -4
  68. package/dist/docs/reference/instructions.html +4 -4
  69. package/dist/docs/reference/playground.html +4 -4
  70. package/dist/docs/reference/project-layout.html +6 -6
  71. package/dist/docs/reference/project-layout.md +3 -0
  72. package/dist/docs/reference/prompt.html +4 -4
  73. package/dist/docs/reference/schedules.html +4 -4
  74. package/dist/docs/reference/sessions.html +4 -4
  75. package/dist/docs/reference/skills.html +6 -6
  76. package/dist/docs/reference/skills.md +2 -0
  77. package/dist/docs/reference/subagents.html +5 -5
  78. package/dist/docs/reference/tools.html +6 -6
  79. package/dist/docs/reference/tools.md +2 -0
  80. package/dist/docs/scaffolding-agents.html +4 -4
  81. package/dist/docs/skills/ab.html +4 -4
  82. package/dist/docs/skills/create-agent.html +4 -4
  83. package/dist/docs/skills/debug.html +4 -4
  84. package/dist/docs/skills/evals.html +4 -4
  85. package/dist/docs/skills/framework-map.html +5 -5
  86. package/dist/docs/skills/framework-map.md +1 -0
  87. package/dist/docs/skills/github.html +4 -4
  88. package/dist/docs/skills/hillclimb.html +4 -4
  89. package/dist/docs/skills/index.html +4 -4
  90. package/dist/docs/skills/mcp-auth.html +4 -4
  91. package/dist/docs/skills/otel.html +4 -4
  92. package/dist/docs/skills/setup-slack.html +4 -4
  93. package/dist/docs/storage.html +4 -4
  94. package/dist/docs/templates/agentic-owners.html +4 -4
  95. package/dist/docs/templates/agents-md.html +4 -4
  96. package/dist/docs/templates/code-wiki.html +4 -4
  97. package/dist/docs/templates/demo.html +4 -4
  98. package/dist/docs/templates/pr-autofixer.html +4 -4
  99. package/dist/docs/templates/security-help.html +4 -4
  100. package/dist/docs/templates/security-reviewer.html +4 -4
  101. package/dist/docs/templates/triage.html +4 -4
  102. package/dist/docs/troubleshooting.html +4 -4
  103. package/dist/extensions/cursor-cloud-agents/extension.d.ts +9 -7
  104. package/dist/extensions/cursor-cloud-agents/extension.d.ts.map +1 -1
  105. package/dist/extensions/cursor-cloud-agents/extension.js +9 -7
  106. package/dist/extensions/cursor-cloud-agents/lib/backend.d.ts +78 -0
  107. package/dist/extensions/cursor-cloud-agents/lib/backend.d.ts.map +1 -0
  108. package/dist/extensions/cursor-cloud-agents/lib/backend.js +286 -0
  109. package/dist/extensions/cursor-cloud-agents/lib/inputs.d.ts +5 -0
  110. package/dist/extensions/cursor-cloud-agents/lib/inputs.d.ts.map +1 -0
  111. package/dist/extensions/cursor-cloud-agents/lib/inputs.js +4 -0
  112. package/dist/extensions/cursor-cloud-agents/lib/transcript.d.ts +8 -0
  113. package/dist/extensions/cursor-cloud-agents/lib/transcript.d.ts.map +1 -0
  114. package/dist/extensions/cursor-cloud-agents/lib/transcript.js +15 -0
  115. package/dist/extensions/cursor-cloud-agents/skills/handoff.md +21 -7
  116. package/dist/extensions/cursor-cloud-agents/tools/cancel.d.ts +4 -0
  117. package/dist/extensions/cursor-cloud-agents/tools/cancel.d.ts.map +1 -0
  118. package/dist/extensions/cursor-cloud-agents/tools/cancel.js +26 -0
  119. package/dist/extensions/cursor-cloud-agents/tools/dump.d.ts +4 -0
  120. package/dist/extensions/cursor-cloud-agents/tools/dump.d.ts.map +1 -0
  121. package/dist/extensions/cursor-cloud-agents/tools/dump.js +54 -0
  122. package/dist/extensions/cursor-cloud-agents/tools/get.d.ts +4 -0
  123. package/dist/extensions/cursor-cloud-agents/tools/get.d.ts.map +1 -0
  124. package/dist/extensions/cursor-cloud-agents/tools/get.js +25 -0
  125. package/dist/extensions/cursor-cloud-agents/tools/launch.d.ts.map +1 -1
  126. package/dist/extensions/cursor-cloud-agents/tools/launch.js +14 -38
  127. package/dist/extensions/cursor-cloud-agents/tools/list.js +1 -1
  128. package/dist/extensions/cursor-cloud-agents/tools/reply.d.ts +4 -0
  129. package/dist/extensions/cursor-cloud-agents/tools/reply.d.ts.map +1 -0
  130. package/dist/extensions/cursor-cloud-agents/tools/{follow_up.js → reply.js} +12 -14
  131. package/dist/extensions.d.ts +38 -14
  132. package/dist/extensions.d.ts.map +1 -1
  133. package/dist/extensions.js +49 -12
  134. package/dist/files-backends/cursor-hosted.d.ts +0 -8
  135. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  136. package/dist/files-backends/cursor-hosted.js +1 -11
  137. package/dist/files.d.ts +5 -1
  138. package/dist/files.d.ts.map +1 -1
  139. package/dist/files.js +22 -18
  140. package/dist/index.d.ts +3 -3
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +2 -2
  143. package/dist/internal/conversation-mirror.d.ts.map +1 -1
  144. package/dist/internal/conversation-mirror.js +13 -15
  145. package/dist/internal/cursor/backend-client.d.ts +8 -1
  146. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  147. package/dist/internal/cursor/backend-client.js +25 -9
  148. package/dist/internal/cursor/hosted-store-secrets.d.ts +11 -0
  149. package/dist/internal/cursor/hosted-store-secrets.d.ts.map +1 -1
  150. package/dist/internal/cursor/hosted-store-secrets.js +15 -0
  151. package/dist/internal/discovery/ab.d.ts +9 -0
  152. package/dist/internal/discovery/ab.d.ts.map +1 -0
  153. package/dist/internal/discovery/ab.js +101 -0
  154. package/dist/internal/discovery/agent-config.d.ts +35 -0
  155. package/dist/internal/discovery/agent-config.d.ts.map +1 -0
  156. package/dist/internal/discovery/agent-config.js +173 -0
  157. package/dist/internal/discovery/agent.d.ts +11 -0
  158. package/dist/internal/discovery/agent.d.ts.map +1 -0
  159. package/dist/internal/discovery/agent.js +306 -0
  160. package/dist/internal/discovery/channels.d.ts +10 -0
  161. package/dist/internal/discovery/channels.d.ts.map +1 -0
  162. package/dist/internal/discovery/channels.js +97 -0
  163. package/dist/internal/discovery/connections.d.ts +17 -0
  164. package/dist/internal/discovery/connections.d.ts.map +1 -0
  165. package/dist/internal/discovery/connections.js +436 -0
  166. package/dist/internal/discovery/context.d.ts +20 -0
  167. package/dist/internal/discovery/context.d.ts.map +1 -0
  168. package/dist/internal/discovery/context.js +30 -0
  169. package/dist/internal/discovery/cursor-plugin.d.ts +25 -0
  170. package/dist/internal/discovery/cursor-plugin.d.ts.map +1 -0
  171. package/dist/internal/discovery/cursor-plugin.js +570 -0
  172. package/dist/internal/discovery/extensions.d.ts +19 -0
  173. package/dist/internal/discovery/extensions.d.ts.map +1 -0
  174. package/dist/internal/discovery/extensions.js +238 -0
  175. package/dist/internal/discovery/fs.d.ts +15 -0
  176. package/dist/internal/discovery/fs.d.ts.map +1 -0
  177. package/dist/internal/discovery/fs.js +115 -0
  178. package/dist/internal/discovery/hooks.d.ts +7 -0
  179. package/dist/internal/discovery/hooks.d.ts.map +1 -0
  180. package/dist/internal/discovery/hooks.js +48 -0
  181. package/dist/internal/discovery/info.d.ts +6 -0
  182. package/dist/internal/discovery/info.d.ts.map +1 -0
  183. package/dist/internal/discovery/info.js +141 -0
  184. package/dist/internal/discovery/instructions.d.ts +7 -0
  185. package/dist/internal/discovery/instructions.d.ts.map +1 -0
  186. package/dist/internal/discovery/instructions.js +95 -0
  187. package/dist/internal/discovery/modules.d.ts +20 -0
  188. package/dist/internal/discovery/modules.d.ts.map +1 -0
  189. package/dist/internal/discovery/modules.js +101 -0
  190. package/dist/internal/discovery/names.d.ts +12 -0
  191. package/dist/internal/discovery/names.d.ts.map +1 -0
  192. package/dist/internal/discovery/names.js +36 -0
  193. package/dist/internal/discovery/project.d.ts +53 -0
  194. package/dist/internal/discovery/project.d.ts.map +1 -0
  195. package/dist/internal/discovery/project.js +239 -0
  196. package/dist/internal/discovery/runtime.d.ts +34 -0
  197. package/dist/internal/discovery/runtime.d.ts.map +1 -0
  198. package/dist/internal/discovery/runtime.js +98 -0
  199. package/dist/internal/discovery/schedules.d.ts +7 -0
  200. package/dist/internal/discovery/schedules.d.ts.map +1 -0
  201. package/dist/internal/discovery/schedules.js +97 -0
  202. package/dist/internal/discovery/seed-files.d.ts +7 -0
  203. package/dist/internal/discovery/seed-files.d.ts.map +1 -0
  204. package/dist/internal/discovery/seed-files.js +41 -0
  205. package/dist/internal/discovery/services.d.ts +17 -0
  206. package/dist/internal/discovery/services.d.ts.map +1 -0
  207. package/dist/internal/discovery/services.js +104 -0
  208. package/dist/internal/discovery/skills.d.ts +16 -0
  209. package/dist/internal/discovery/skills.d.ts.map +1 -0
  210. package/dist/internal/discovery/skills.js +151 -0
  211. package/dist/internal/discovery/subagent.d.ts +13 -0
  212. package/dist/internal/discovery/subagent.d.ts.map +1 -0
  213. package/dist/internal/discovery/subagent.js +19 -0
  214. package/dist/internal/discovery/tools.d.ts +18 -0
  215. package/dist/internal/discovery/tools.d.ts.map +1 -0
  216. package/dist/internal/discovery/tools.js +159 -0
  217. package/dist/internal/discovery.d.ts +8 -62
  218. package/dist/internal/discovery.d.ts.map +1 -1
  219. package/dist/internal/discovery.js +8 -2505
  220. package/dist/internal/framework-file-storage.js +6 -6
  221. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  222. package/dist/internal/framework-storage-selection.js +8 -7
  223. package/dist/internal/frontmatter.d.ts +11 -3
  224. package/dist/internal/frontmatter.d.ts.map +1 -1
  225. package/dist/internal/frontmatter.js +29 -3
  226. package/dist/internal/mcp-host.d.ts.map +1 -1
  227. package/dist/internal/mcp-host.js +37 -20
  228. package/dist/internal/otel/collector.d.ts +13 -0
  229. package/dist/internal/otel/collector.d.ts.map +1 -1
  230. package/dist/internal/otel/collector.js +84 -17
  231. package/dist/internal/otel/http.d.ts.map +1 -1
  232. package/dist/internal/otel/http.js +2 -10
  233. package/dist/internal/otel/mcp.d.ts +16 -0
  234. package/dist/internal/otel/mcp.d.ts.map +1 -0
  235. package/dist/internal/otel/mcp.js +68 -0
  236. package/dist/internal/otel/propagation.d.ts +6 -0
  237. package/dist/internal/otel/propagation.d.ts.map +1 -0
  238. package/dist/internal/otel/propagation.js +14 -0
  239. package/dist/internal/otel/runtime.d.ts.map +1 -1
  240. package/dist/internal/otel/runtime.js +2 -0
  241. package/dist/internal/session-engine.d.ts +5 -0
  242. package/dist/internal/session-engine.d.ts.map +1 -1
  243. package/dist/internal/session-engine.js +68 -32
  244. package/dist/internal/session-run-log.d.ts +2 -1
  245. package/dist/internal/session-run-log.d.ts.map +1 -1
  246. package/dist/internal/session-run-log.js +13 -14
  247. package/dist/internal/start-log-store.d.ts +1 -1
  248. package/dist/internal/start-log-store.d.ts.map +1 -1
  249. package/dist/internal/start-log-store.js +12 -14
  250. package/dist/internal/storage-coordinator.d.ts +4 -1
  251. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  252. package/dist/internal/storage-coordinator.js +15 -13
  253. package/dist/internal/tool-catalog.d.ts.map +1 -1
  254. package/dist/internal/tool-catalog.js +9 -6
  255. package/dist/internal/turn-cost.d.ts +14 -0
  256. package/dist/internal/turn-cost.d.ts.map +1 -1
  257. package/dist/internal/turn-cost.js +41 -9
  258. package/dist/internal/v2-hosting.d.ts +22 -0
  259. package/dist/internal/v2-hosting.d.ts.map +1 -0
  260. package/dist/internal/v2-hosting.js +23 -0
  261. package/dist/otel.d.ts +16 -0
  262. package/dist/otel.d.ts.map +1 -1
  263. package/dist/otel.js +26 -0
  264. package/dist/playground/assets/index-B6C9S9nG.css +1 -0
  265. package/dist/playground/index.html +2 -2
  266. package/dist/storage-protocol.d.ts +24 -23
  267. package/dist/storage-protocol.d.ts.map +1 -1
  268. package/dist/storage-protocol.js +68 -65
  269. package/dist/storage.d.ts +2 -2
  270. package/dist/storage.d.ts.map +1 -1
  271. package/dist/storage.js +1 -1
  272. package/dist/tools.d.ts +9 -0
  273. package/dist/tools.d.ts.map +1 -1
  274. package/dist/tools.js +9 -0
  275. package/dist/types.d.ts +39 -3
  276. package/dist/types.d.ts.map +1 -1
  277. package/docs/README.md +5 -1
  278. package/docs/concepts.md +3 -3
  279. package/docs/guides/cloud-agents.md +98 -0
  280. package/docs/guides/cloud-runtime.md +2 -0
  281. package/docs/reference/extensions.md +205 -0
  282. package/docs/reference/project-layout.md +3 -0
  283. package/docs/reference/skills.md +2 -0
  284. package/docs/reference/tools.md +2 -0
  285. package/package.json +1 -1
  286. package/skills/framework-map/SKILL.md +1 -0
  287. package/src/channels/slack/api.ts +4 -2
  288. package/src/channels/slack/assistant.ts +1 -1
  289. package/src/channels/slack/dispatch.ts +27 -2
  290. package/src/channels/slack/inbound.ts +37 -0
  291. package/src/channels/slack/manifest.ts +2 -1
  292. package/src/extensions/cursor-cloud-agents/extension.ts +9 -7
  293. package/src/extensions/cursor-cloud-agents/lib/backend.ts +395 -0
  294. package/src/extensions/cursor-cloud-agents/lib/inputs.ts +5 -0
  295. package/src/extensions/cursor-cloud-agents/lib/transcript.ts +23 -0
  296. package/src/extensions/cursor-cloud-agents/skills/handoff.md +21 -7
  297. package/src/extensions/cursor-cloud-agents/tools/cancel.ts +19 -0
  298. package/src/extensions/cursor-cloud-agents/tools/dump.ts +51 -0
  299. package/src/extensions/cursor-cloud-agents/tools/get.ts +18 -0
  300. package/src/extensions/cursor-cloud-agents/tools/launch.ts +22 -42
  301. package/src/extensions/cursor-cloud-agents/tools/list.ts +1 -1
  302. package/src/extensions/cursor-cloud-agents/tools/reply.ts +34 -0
  303. package/src/extensions.ts +55 -12
  304. package/src/files-backends/cursor-hosted.ts +1 -17
  305. package/src/files.ts +43 -34
  306. package/src/index.ts +10 -1
  307. package/src/internal/conversation-mirror.ts +23 -18
  308. package/src/internal/cursor/backend-client.ts +32 -10
  309. package/src/internal/cursor/hosted-store-secrets.ts +27 -0
  310. package/src/internal/discovery/ab.ts +111 -0
  311. package/src/internal/discovery/agent-config.ts +222 -0
  312. package/src/internal/discovery/agent.ts +360 -0
  313. package/src/internal/discovery/channels.ts +103 -0
  314. package/src/internal/discovery/connections.ts +540 -0
  315. package/src/internal/discovery/context.ts +46 -0
  316. package/src/internal/discovery/cursor-plugin.ts +708 -0
  317. package/src/internal/discovery/extensions.ts +348 -0
  318. package/src/internal/discovery/fs.ts +108 -0
  319. package/src/internal/discovery/hooks.ts +45 -0
  320. package/src/internal/discovery/info.ts +179 -0
  321. package/src/internal/discovery/instructions.ts +113 -0
  322. package/src/internal/discovery/modules.ts +107 -0
  323. package/src/internal/discovery/names.ts +44 -0
  324. package/src/internal/discovery/project.ts +270 -0
  325. package/src/internal/discovery/runtime.ts +159 -0
  326. package/src/internal/discovery/schedules.ts +94 -0
  327. package/src/internal/discovery/seed-files.ts +37 -0
  328. package/src/internal/discovery/services.ts +124 -0
  329. package/src/internal/discovery/skills.ts +176 -0
  330. package/src/internal/discovery/subagent.ts +29 -0
  331. package/src/internal/discovery/tools.ts +182 -0
  332. package/src/internal/discovery.ts +20 -3081
  333. package/src/internal/framework-file-storage.ts +6 -6
  334. package/src/internal/framework-storage-selection.ts +9 -9
  335. package/src/internal/frontmatter.ts +36 -3
  336. package/src/internal/mcp-host.ts +84 -31
  337. package/src/internal/otel/collector.ts +125 -25
  338. package/src/internal/otel/http.ts +2 -12
  339. package/src/internal/otel/mcp.ts +69 -0
  340. package/src/internal/otel/propagation.ts +17 -0
  341. package/src/internal/otel/runtime.ts +2 -0
  342. package/src/internal/session-engine.ts +96 -11
  343. package/src/internal/session-run-log.ts +17 -16
  344. package/src/internal/start-log-store.ts +13 -16
  345. package/src/internal/storage-coordinator.ts +19 -16
  346. package/src/internal/tool-catalog.ts +9 -6
  347. package/src/internal/turn-cost.ts +47 -4
  348. package/src/internal/v2-hosting.ts +38 -0
  349. package/src/otel.ts +38 -0
  350. package/src/storage-protocol.ts +101 -91
  351. package/src/storage.ts +4 -0
  352. package/src/tools.ts +18 -0
  353. package/src/types.ts +43 -2
  354. package/dist/docs/assets/chunks/@localSearchIndexroot.DbFXINq2.js +0 -1
  355. package/dist/docs/assets/concepts.md.lwAgBIMI.js +0 -1
  356. package/dist/docs/assets/index.md.Bb4k8kUm.js +0 -5
  357. package/dist/docs/assets/skills_framework-map.md.BTi817yv.js +0 -1
  358. package/dist/extensions/cursor-cloud-agents/lib/cloud.d.ts +0 -30
  359. package/dist/extensions/cursor-cloud-agents/lib/cloud.d.ts.map +0 -1
  360. package/dist/extensions/cursor-cloud-agents/lib/cloud.js +0 -45
  361. package/dist/extensions/cursor-cloud-agents/tools/follow_up.d.ts +0 -4
  362. package/dist/extensions/cursor-cloud-agents/tools/follow_up.d.ts.map +0 -1
  363. package/dist/extensions/cursor-cloud-agents/tools/status.d.ts +0 -4
  364. package/dist/extensions/cursor-cloud-agents/tools/status.d.ts.map +0 -1
  365. package/dist/extensions/cursor-cloud-agents/tools/status.js +0 -41
  366. package/dist/playground/assets/index-DBQCwFp0.css +0 -1
  367. package/src/extensions/cursor-cloud-agents/lib/cloud.ts +0 -79
  368. package/src/extensions/cursor-cloud-agents/tools/follow_up.ts +0 -29
  369. package/src/extensions/cursor-cloud-agents/tools/status.ts +0 -39
  370. /package/dist/docs/assets/{concepts.md.lwAgBIMI.lean.js → concepts.md.rPzW2HZN.lean.js} +0 -0
  371. /package/dist/docs/assets/{guides_cloud-runtime.md.CkYbjnAX.lean.js → guides_cloud-runtime.md.BRirbRAN.lean.js} +0 -0
  372. /package/dist/docs/assets/{reference_skills.md.BFW9retM.lean.js → reference_skills.md.l2ezAcaN.lean.js} +0 -0
  373. /package/dist/docs/assets/{reference_tools.md.CvAHsdSp.lean.js → reference_tools.md.B1up2r9W.lean.js} +0 -0
  374. /package/dist/playground/assets/{index-BnNN1-EK.js → index-ZqNoT8Ie.js} +0 -0
@@ -4,3084 +4,23 @@
4
4
  * manifest. Identity comes from the path — a tool at `agent/tools/foo.ts`
5
5
  * is named `foo`, a subagent at `agent/subagents/researcher/` is named
6
6
  * `researcher` — so no definition carries a `name` field.
7
- */
8
-
9
- import type { Dirent } from "node:fs";
10
- import { readdir, readFile, stat } from "node:fs/promises";
11
- import {
12
- basename,
13
- extname,
14
- isAbsolute,
15
- join,
16
- relative,
17
- resolve,
18
- } from "node:path";
19
- import { pathToFileURL } from "node:url";
20
- import { type ABConfigFile, resolveABMaxPlaygroundSessions } from "../ab.js";
21
- import { resolveArtifactsMax } from "../artifacts.js";
22
- import { httpChannel } from "../channels.js";
23
- import type { OtelDefinition } from "../otel.js";
24
- import type { StorageDefinition } from "../storage.js";
25
- import {
26
- type AgentArchitecture,
27
- type AgentConcurrencyOptions,
28
- type AgentConfigDefinition,
29
- type AgentHostingOptions,
30
- type AgentProject,
31
- type AgentProjectInfo,
32
- type AgentRuntime,
33
- type ArtifactsDefinition,
34
- type BuiltinToolsConfig,
35
- type ChannelDefinition,
36
- type ConnectionDefinition,
37
- type ConnectionTransport,
38
- cursorAccountServersHasMixedWildcard,
39
- DEFAULT_MODEL,
40
- type Diagnostic,
41
- type DiscoveredAB,
42
- type DiscoveredChannel,
43
- type DiscoveredConnection,
44
- type DiscoveredExtension,
45
- type DiscoveredHook,
46
- type DiscoveredSchedule,
47
- type DiscoveredSkill,
48
- type DiscoveredTool,
49
- type ExtensionMount,
50
- type HookDefinition,
51
- type HttpChannelDefinition,
52
- type InlineStdioConnectionConfig,
53
- type InstructionsDefinition,
54
- isInlineStdioConnection,
55
- isToolEffect,
56
- MAX_RUNNING_TURNS_LIMIT,
57
- type ModelSetting,
58
- normalizeConnectionEffects,
59
- normalizeConnectionTools,
60
- normalizeCursorAccountServers,
61
- type ResolvedAgent,
62
- type ScheduleDefinition,
63
- type SeedFile,
64
- type SkillConfig,
65
- type SkillDefinition,
66
- type SkillFileSource,
67
- type ToolDefinition,
68
- type ToolEffect,
69
- toPublicCloudInfo,
70
- } from "../types.js";
71
- import { registerAuthoredModuleLoaders } from "./authored-loaders.js";
72
- import { normalizePositiveInt } from "./bounded-int.js";
73
- import { getDefinitionKind } from "./brand.js";
74
- import {
75
- artifactBuiltinTool,
76
- builtinTools,
77
- TAG_ARTIFACT_TOOL_NAME,
78
- } from "./builtin-tools/index.js";
79
- import { applyAgentContinuationDefaults } from "./continuation-channel.js";
80
- import { isValidCron } from "./cron.js";
81
- import { parseFrontmatter } from "./frontmatter.js";
82
- import {
83
- isReservedSecretName,
84
- MAX_EGRESS_DOMAINS,
85
- validateEgressDomain,
86
- validateSecretName,
87
- } from "./hosting.js";
88
- import { toolInputJsonSchema, toolOutputJsonSchema } from "./json-schema.js";
89
- import { MCP_ENDPOINT_PATH, mcpEndpointToolNames } from "./mcp-endpoint.js";
90
- import { packageInlineStdioConnection } from "./package-inline-stdio.js";
91
- import {
92
- defaultLocalHarnessCwd,
93
- isNestedInGitRepo,
94
- withIdentityPreamble,
95
- } from "./workspace.js";
96
-
97
- const MODULE_EXTENSIONS = new Set([".ts", ".mts", ".js", ".mjs"]);
98
- const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
99
- const SKILL_DESCRIPTION_FALLBACK = (name: string): string =>
100
- `Instructions for the ${name} skill.`;
101
-
102
- export interface LoadAgentProjectOptions {
103
- /** Override the module loader (embedders / tests). */
104
- importModule?: (absolutePath: string) => Promise<Record<string, unknown>>;
105
- }
106
-
107
- type ImportModule = (absolutePath: string) => Promise<Record<string, unknown>>;
108
-
109
- async function defaultImportModule(
110
- absolutePath: string
111
- ): Promise<Record<string, unknown>> {
112
- return (await import(pathToFileURL(absolutePath).href)) as Record<
113
- string,
114
- unknown
115
- >;
116
- }
117
-
118
- interface DiscoveryContext {
119
- rootDir: string;
120
- diagnostics: Diagnostic[];
121
- importModule: ImportModule;
122
- }
123
-
124
- /**
125
- * True when `dir` looks like an agent project root: it has a nested `agent/`
126
- * directory, or flat-layout markers (`agent.ts` / `instructions.md`) directly
127
- * inside it.
128
- */
129
- export async function isAgentProjectDir(dir: string): Promise<boolean> {
130
- const resolved = resolve(dir);
131
- if (!(await isDirectory(resolved))) {
132
- return false;
133
- }
134
- if (await isDirectory(join(resolved, "agent"))) {
135
- return true;
136
- }
137
- return hasFlatLayoutMarkers(resolved);
138
- }
139
-
140
- /** One immediate child agent directory under a multi-agent parent. */
141
- export interface ChildAgentDir {
142
- slug: string;
143
- dir: string;
144
- }
145
-
146
- /**
147
- * List agent projects directly under `rootDir` (one level deep) without
148
- * loading them. Empty when the path is missing or has no child agents.
149
- * Slug rules match {@link loadAgentProjects} / multi-agent serve.
150
- */
151
- export async function listChildAgentDirs(
152
- rootDir: string
153
- ): Promise<ChildAgentDir[]> {
154
- const resolved = resolve(rootDir);
155
- if (!(await isDirectory(resolved))) {
156
- return [];
157
- }
158
-
159
- const results: ChildAgentDir[] = [];
160
- const seenSlugs = new Set<string>();
161
- for (const entry of await sortedEntries(resolved)) {
162
- if (!entry.isDirectory() || entry.name.startsWith(".")) {
163
- continue;
164
- }
165
- if (entry.name === "node_modules") {
166
- continue;
167
- }
168
- const childDir = join(resolved, entry.name);
169
- if (!(await isAgentProjectDir(childDir))) {
170
- continue;
171
- }
172
- const slug = assertSlug(entry.name, resolved);
173
- if (seenSlugs.has(slug)) {
174
- throw new Error(`Duplicate agent slug "${slug}" under ${resolved}.`);
175
- }
176
- seenSlugs.add(slug);
177
- results.push({ slug, dir: childDir });
178
- }
179
- return results;
180
- }
181
-
182
- /**
183
- * Discover the agent projects directly under `rootDir` (one level deep):
184
- * each immediate child directory that is itself an agent project becomes a
185
- * mounted agent, keyed by a slug derived from its directory name. Used for
186
- * multi-agent serving, where one server hosts many agents under
187
- * `/<slug>/...`. Throws when no child projects are found.
188
- */
189
- export async function loadAgentProjects(
190
- rootDir: string,
191
- options: LoadAgentProjectOptions = {}
192
- ): Promise<Array<{ slug: string; project: AgentProject }>> {
193
- const resolved = resolve(rootDir);
194
- if (!(await isDirectory(resolved))) {
195
- throw new Error(`Agent projects directory does not exist: ${resolved}`);
196
- }
197
-
198
- const children = await listChildAgentDirs(resolved);
199
- if (children.length === 0) {
200
- throw new Error(
201
- `No agent projects found directly under ${resolved}. Each agent should be a subdirectory with its own agent/ directory (or flat-layout markers).`
202
- );
203
- }
204
- const results: Array<{ slug: string; project: AgentProject }> = [];
205
- // Storage namespaces (session mirror, host kv) key on the project
206
- // NAME, not the mount slug — two co-hosted agents sharing a name would
207
- // silently share sessions and KV state.
208
- const slugByName = new Map<string, string>();
209
- for (const child of children) {
210
- const project = await loadAgentProject(child.dir, options);
211
- const priorSlug = slugByName.get(project.name);
212
- if (priorSlug !== undefined) {
213
- throw new Error(
214
- `Duplicate agent name "${project.name}" under ${resolved}: agents "${priorSlug}" and "${child.slug}" both resolve to it (package.json "name", or the directory name when unset). Co-hosted agents must have distinct names — their storage and session namespaces key on the name.`
215
- );
216
- }
217
- slugByName.set(project.name, child.slug);
218
- results.push({ slug: child.slug, project });
219
- }
220
- return results;
221
- }
222
-
223
- /**
224
- * Resolve mounts for `serve` / CLI. Defaults to multi-agent layout:
225
- * - `mode: "single"` — one agent at `/v1/*` (empty slug).
226
- * - otherwise, if `rootDir` is itself an agent project — one agent under
227
- * `/<dirname>/...`.
228
- * - otherwise, every immediate child agent project under `/<slug>/...`.
229
- */
230
- export async function resolveAgentMounts(
231
- rootDir: string,
232
- options: LoadAgentProjectOptions & {
233
- mode?: "single" | "multi";
234
- } = {}
235
- ): Promise<Array<{ slug: string; project: AgentProject }>> {
236
- const { mode = "multi", ...discovery } = options;
237
- if (mode === "single") {
238
- return [{ slug: "", project: await loadAgentProject(rootDir, discovery) }];
239
- }
240
- if (await isAgentProjectDir(rootDir)) {
241
- const resolved = resolve(rootDir);
242
- const slug = assertSlug(basename(resolved), resolved);
243
- return [
244
- {
245
- slug,
246
- project: await loadAgentProject(rootDir, discovery),
247
- },
248
- ];
249
- }
250
- return loadAgentProjects(rootDir, discovery);
251
- }
252
-
253
- function assertSlug(name: string, parentDir: string): string {
254
- if (!NAME_PATTERN.test(name) || RESERVED_SLUGS.has(name)) {
255
- throw new Error(
256
- `Cannot mount agent directory "${name}" under ${parentDir}: its name must match ${NAME_PATTERN} and not be one of ${[...RESERVED_SLUGS].join(", ")}.`
257
- );
258
- }
259
- return name;
260
- }
261
-
262
- /** URL path segments reserved by the server; agents cannot use them as slugs. */
263
- export const RESERVED_SLUGS: Set<string> = new Set([
264
- "v1",
265
- "playground",
266
- "docs",
267
- ]);
268
-
269
- export async function loadAgentProject(
270
- dir: string,
271
- options: LoadAgentProjectOptions = {}
272
- ): Promise<AgentProject> {
273
- // Public API: embedders can call discovery directly (without `serve()` or
274
- // the CLI), and it dynamically imports authored TypeScript.
275
- await registerAuthoredModuleLoaders();
276
- const rootDir = resolve(dir);
277
- if (!(await isDirectory(rootDir))) {
278
- throw new Error(`Agent project directory does not exist: ${rootDir}`);
279
- }
280
-
281
- const nestedAgentDir = join(rootDir, "agent");
282
- let agentDir: string;
283
- if (await isDirectory(nestedAgentDir)) {
284
- agentDir = nestedAgentDir;
285
- } else if (await hasFlatLayoutMarkers(rootDir)) {
286
- agentDir = rootDir;
287
- } else {
288
- throw new Error(
289
- `No agent found under ${rootDir}: expected an agent/ directory (or a flat layout with agent.ts / instructions.md at the root)`
290
- );
291
- }
292
-
293
- const diagnostics: Diagnostic[] = [];
294
- const ctx: DiscoveryContext = {
295
- rootDir,
296
- diagnostics,
297
- importModule: options.importModule ?? defaultImportModule,
298
- };
299
-
300
- const name = (await readPackageName(rootDir)) ?? basename(rootDir);
301
- const agent = await loadAgent(ctx, agentDir, {
302
- name,
303
- isRoot: true,
304
- });
305
-
306
- const { channels, httpChannelDefinition } = await loadChannels(
307
- ctx,
308
- join(agentDir, "channels")
309
- );
310
- const schedules = await loadSchedules(ctx, join(agentDir, "schedules"));
311
- const hooks = await loadHooks(ctx, join(agentDir, "hooks"));
312
- const abs = await loadABs(ctx, agentDir);
313
- const abConfig = await loadABConfig(ctx, agentDir);
314
- const storage = await loadStorage(ctx, agentDir, agent.architecture);
315
- const artifacts = await loadArtifacts(ctx, agentDir);
316
- const otel = await loadOtel(ctx, agentDir);
317
- await appendArtifactTool(ctx, agentDir, agent, artifacts);
318
-
319
- if (agentDir !== rootDir) {
320
- await warnUnknownSlots(ctx, agentDir);
321
- }
322
-
323
- const resolvedHttpChannel = httpChannelDefinition ?? httpChannel();
324
- applyAgentContinuationDefaults({
325
- agent,
326
- channels,
327
- httpChannel: resolvedHttpChannel,
328
- });
329
-
330
- return {
331
- rootDir,
332
- agentDir,
333
- name: agent.name,
334
- agent,
335
- httpChannel: resolvedHttpChannel,
336
- channels,
337
- schedules,
338
- hooks,
339
- abs,
340
- abConfig,
341
- storage,
342
- artifacts,
343
- otel,
344
- diagnostics,
345
- };
346
- }
347
-
348
- /** True when the project has diagnostics that must block serving. */
349
- export function hasErrorDiagnostics(project: AgentProject): boolean {
350
- return project.diagnostics.some((d) => d.severity === "error");
351
- }
352
-
353
- export function formatDiagnostics(diagnostics: Diagnostic[]): string {
354
- return diagnostics
355
- .map((d) => {
356
- const location = d.path === undefined ? "" : ` (${d.path})`;
357
- return `${d.severity}: ${d.message}${location}`;
358
- })
359
- .join("\n");
360
- }
361
-
362
- // ============================================================================
363
- // Agents (root + subagents)
364
- // ============================================================================
365
-
366
- async function loadAgent(
367
- ctx: DiscoveryContext,
368
- agentDir: string,
369
- options: { name: string; isRoot: boolean }
370
- ): Promise<ResolvedAgent> {
371
- const config = await loadAgentConfig(ctx, agentDir, options.isRoot);
372
- const name = config?.name ?? options.name;
373
- const authoredInstructions = await loadInstructions(
374
- ctx,
375
- agentDir,
376
- config,
377
- options.isRoot
378
- );
379
- // Root agents without authored instructions stay undefined (diagnostic
380
- // already recorded). Subagents still get the identity line alone.
381
- const instructions =
382
- authoredInstructions === undefined && options.isRoot
383
- ? undefined
384
- : withIdentityPreamble(name, authoredInstructions);
385
-
386
- // Subagents run as Cursor SDK custom subagents (prompt + description +
387
- // model); their other slots are inert today, so don't load them — just
388
- // warn that they exist.
389
- if (!options.isRoot) {
390
- await warnUnsupportedSubagentSlots(ctx, agentDir);
391
- if (config?.builtinTools !== undefined) {
392
- ctx.diagnostics.push({
393
- severity: "warning",
394
- path: displayPath(ctx, join(agentDir, "agent.ts")),
395
- message: "builtinTools is not supported on subagents; ignoring.",
396
- });
397
- }
398
- if (config?.tools !== undefined) {
399
- ctx.diagnostics.push({
400
- severity: "warning",
401
- path: displayPath(ctx, join(agentDir, "agent.ts")),
402
- message:
403
- "tools is not supported on subagents (they keep the harness task-tool toolset); ignoring.",
404
- });
405
- }
406
- if (config?.architecture !== undefined) {
407
- ctx.diagnostics.push({
408
- severity: "warning",
409
- path: displayPath(ctx, join(agentDir, "agent.ts")),
410
- message: "architecture is not supported on subagents; ignoring.",
411
- });
412
- }
413
- const artifactsPath = await findModuleFile(agentDir, "artifacts");
414
- if (artifactsPath !== undefined) {
415
- ctx.diagnostics.push({
416
- severity: "warning",
417
- path: displayPath(ctx, artifactsPath),
418
- message: "artifacts are not supported on subagents; ignoring.",
419
- });
420
- }
421
- return {
422
- name,
423
- ...(config?.description === undefined
424
- ? {}
425
- : { description: config.description }),
426
- ...(config?.model === undefined ? {} : { model: config.model }),
427
- ...(instructions === undefined ? {} : { instructions }),
428
- runtime: "local",
429
- tools: [],
430
- skills: [],
431
- connections: [],
432
- hostConnections: [],
433
- subagents: [],
434
- seedFiles: [],
435
- };
436
- }
437
-
438
- const tools = await loadTools(ctx, join(agentDir, "tools"));
439
- const skills = await loadSkills(ctx, join(agentDir, "skills"));
440
- const extensions = await appendExtensions(
441
- ctx,
442
- join(agentDir, "extensions"),
443
- tools,
444
- skills
445
- );
446
- const connections = await loadConnections(
447
- ctx,
448
- await resolveMcpConnectionsDir(ctx, agentDir)
449
- );
450
- const hostConnections = await loadHostConnections(ctx, agentDir, connections);
451
- const seedFiles = await loadSeedFiles(ctx, agentDir);
452
- const subagents = await loadSubagents(ctx, join(agentDir, "subagents"));
453
- const runtime = resolveRuntime(ctx, agentDir, config?.runtime);
454
- // Keep authored `cloud` even when default runtime is local — per-session
455
- // `send({ cloud })` merges it under cloudOverride for handoff sessions.
456
- const cloud = config?.cloud;
457
- const localWorkspaceDir = resolveAgentCwd(ctx, config?.local?.workspaceDir);
458
- const localCwd = await resolveLocalCwd(
459
- ctx,
460
- config?.local?.cwd,
461
- config?.local?.workspaceDir
462
- );
463
- const localSandbox = config?.local?.sandbox === true;
464
- // Built-in tool allowlist (top-level `tools`). `[]` is meaningful (no
465
- // native built-in tools), so presence is keyed on the field, not on
466
- // emptiness. "mcp" is always granted alongside a configured allowlist:
467
- // it is transport, not capability — this kit's own server tools ride the
468
- // SDK custom-tool MCP path, so omitting it silently breaks
469
- // `agent/tools/`, and what MCP actually exposes stays controlled by the
470
- // second layer (authored tools, mcp-connections, the harness cwd's
471
- // ambient config).
472
- const authoredAllowlist = config?.tools;
473
- const allowedTools =
474
- authoredAllowlist === undefined || authoredAllowlist.includes("mcp")
475
- ? authoredAllowlist
476
- : [...authoredAllowlist, "mcp" as const];
477
- // The allowlist states runtime-agnostic intent, but only the local
478
- // harness can enforce it (the Cursor SDK rejects tool restrictions on
479
- // cloud agents, and the Grok Bot harness has its own fixed toolset) — a
480
- // non-local agent that declares one must fail at serve start, never run
481
- // unrestricted. (Hybrid per-send cloud sessions fail closed in the
482
- // session engine.)
483
- if (allowedTools !== undefined && runtime !== "local") {
484
- ctx.diagnostics.push({
485
- severity: "error",
486
- path: displayPath(ctx, join(agentDir, "agent.ts")),
487
- message: `tools cannot be enforced on runtime: "${runtime}". Remove \`tools\` or run the agent on the local runtime.`,
488
- });
489
- }
490
- // Per-session connection features (defineConnection auth/advertiseTools)
491
- // only run on local turns: advertised tools are in-process server tools,
492
- // and per-session headers cannot reach a cloud VM's MCP config. A
493
- // non-local agent that declares them must fail at serve start — never
494
- // run without the connection's tools or identity. (Hybrid per-send
495
- // cloud sessions fail closed in the session engine.)
496
- const perSessionConnection = connections.find(
497
- (connection) =>
498
- connection.advertiseTools === true || connection.auth !== undefined
499
- );
500
- if (perSessionConnection !== undefined && runtime !== "local") {
501
- ctx.diagnostics.push({
502
- severity: "error",
503
- path: displayPath(ctx, join(agentDir, "agent.ts")),
504
- message: `MCP connection "${perSessionConnection.name}" uses advertiseTools/auth, which only run on runtime: "local". Remove them or run the agent on the local runtime.`,
505
- });
506
- }
507
- // `execution: "agent"` tool scripts run through the shell tool. Shell is
508
- // a real capability, so it is never injected — but an allowlist that
509
- // strands authored scripts deserves a loud diagnostic, not silence.
510
- if (
511
- allowedTools !== undefined &&
512
- !allowedTools.includes("shell") &&
513
- tools.some((tool) => tool.execution === "agent")
514
- ) {
515
- ctx.diagnostics.push({
516
- severity: "warning",
517
- path: displayPath(ctx, join(agentDir, "agent.ts")),
518
- message:
519
- '`tools` omits "shell" but this agent has execution: "agent" tools, which run as shell scripts; the model will not be able to run them.',
520
- });
521
- }
522
- if (localCwd !== undefined && localWorkspaceDir !== undefined) {
523
- ctx.diagnostics.push({
524
- severity: "warning",
525
- path: displayPath(ctx, join(agentDir, "agent.ts")),
526
- message:
527
- "local.workspaceDir pins every session to one directory, so local.cwd is unused. Drop one of them.",
528
- });
529
- }
530
- const local =
531
- localCwd === undefined && localWorkspaceDir === undefined && !localSandbox
532
- ? undefined
533
- : {
534
- cwd: localCwd,
535
- workspaceDir: localWorkspaceDir,
536
- sandbox: localSandbox,
537
- };
538
- const hosting = resolveHosting(ctx, agentDir, config?.hosting);
539
- const concurrency = resolveConcurrency(ctx, agentDir, config?.concurrency);
540
- const architecture = resolveArchitecture(ctx, agentDir, config?.architecture);
541
- const runtimeArchitectureError = runtimeArchitectureCompatibilityError({
542
- runtime,
543
- architecture,
544
- });
545
- if (runtimeArchitectureError !== undefined) {
546
- ctx.diagnostics.push({
547
- severity: "error",
548
- path: displayPath(ctx, join(agentDir, "agent.ts")),
549
- message: runtimeArchitectureError,
550
- });
551
- }
552
-
553
- if (runtime === "cloud") {
554
- warnCloudRuntimeLimitations(ctx, agentDir, {
555
- agentTools: tools.filter((tool) => tool.execution === "agent").length,
556
- skills: skills.length,
557
- seedFiles: seedFiles.length,
558
- hasCloudOptions: cloud !== undefined,
559
- });
560
- }
561
- appendBuiltinTools(ctx, agentDir, tools, config?.builtinTools);
562
-
563
- // After appendBuiltinTools so opted-in built-ins (server-executed) count.
564
- if (runtime === "grokbot") {
565
- checkGrokBotRuntimeLimitations(ctx, agentDir, {
566
- serverTools: tools.filter((tool) => tool.execution === "server").length,
567
- agentTools: tools.filter((tool) => tool.execution === "agent").length,
568
- skills: skills.length,
569
- seedFiles: seedFiles.length,
570
- connections: connections.length + hostConnections.length,
571
- subagents: subagents.length,
572
- hasCloudOptions: cloud !== undefined,
573
- hasLocalSandbox: localSandbox,
574
- });
575
- }
576
-
577
- return {
578
- name,
579
- description: config?.description,
580
- // Root agents always get a concrete model: authored value or DEFAULT_MODEL.
581
- model: isValidModelSetting(config?.model) ? config.model : DEFAULT_MODEL,
582
- instructions,
583
- runtime,
584
- cloud,
585
- local,
586
- ...(allowedTools === undefined ? {} : { allowedTools }),
587
- hosting,
588
- concurrency,
589
- ...(architecture === undefined ? {} : { architecture }),
590
- tools,
591
- skills,
592
- extensions,
593
- connections,
594
- hostConnections,
595
- subagents,
596
- seedFiles,
597
- };
598
- }
599
-
600
- export function runtimeArchitectureCompatibilityError(agent: {
601
- runtime: AgentRuntime;
602
- architecture?: AgentArchitecture;
603
- }): string | undefined {
604
- return agent.architecture === "v2" && agent.runtime === "grokbot"
605
- ? 'architecture: "v2" cannot use runtime: "grokbot": the grokbot runtime executes through the signed-in account\'s hosted /v0/grokbot box, not a deployment-scoped v2 runtime.'
606
- : undefined;
607
- }
608
-
609
- function resolveArchitecture(
610
- ctx: DiscoveryContext,
611
- agentDir: string,
612
- architecture: AgentArchitecture | undefined
613
- ): AgentArchitecture | undefined {
614
- if (
615
- architecture === undefined ||
616
- architecture === "v1" ||
617
- architecture === "v2"
618
- ) {
619
- return architecture;
620
- }
621
- ctx.diagnostics.push({
622
- severity: "error",
623
- path: displayPath(ctx, join(agentDir, "agent.ts")),
624
- message: `architecture must be "v1" or "v2"; got ${JSON.stringify(architecture)}.`,
625
- });
626
- return undefined;
627
- }
628
-
629
- /** Absolute harness cwd parent, or undefined when unset / blank. */
630
- function resolveAgentCwd(
631
- ctx: DiscoveryContext,
632
- authored: string | undefined
633
- ): string | undefined {
634
- if (authored === undefined) {
635
- return undefined;
636
- }
637
- const trimmed = authored.trim();
638
- if (trimmed === "") {
639
- return undefined;
640
- }
641
- return isAbsolute(trimmed) ? trimmed : resolve(ctx.rootDir, trimmed);
642
- }
643
-
644
- /**
645
- * Nested git checkouts inherit ancestor AGENTS.md / .cursor rules and MCP.
646
- * Point those harnesses at ~/.cache/agent-serve/<dir> unless the agent
647
- * already set cwd or workspaceDir.
648
- */
649
- async function resolveLocalCwd(
650
- ctx: DiscoveryContext,
651
- authoredCwd: string | undefined,
652
- authoredWorkspaceDir: string | undefined
653
- ): Promise<string | undefined> {
654
- const cwd = resolveAgentCwd(ctx, authoredCwd);
655
- if (cwd !== undefined) {
656
- return cwd;
657
- }
658
- if (resolveAgentCwd(ctx, authoredWorkspaceDir) !== undefined) {
659
- return undefined;
660
- }
661
- if (!(await isNestedInGitRepo(ctx.rootDir))) {
662
- return undefined;
663
- }
664
- return defaultLocalHarnessCwd(basename(ctx.rootDir));
665
- }
666
-
667
- /**
668
- * Validate the authored `hosting` block against the same grammar the
669
- * backend enforces on deploy. Violations are warning diagnostics (reported
670
- * by `agent-sdk validate`), not hard failures: the backend stays
671
- * authoritative and `agent-sdk deploy` re-checks before any network call.
672
- */
673
- function resolveHosting(
674
- ctx: DiscoveryContext,
675
- agentDir: string,
676
- hosting: AgentHostingOptions | undefined
677
- ): AgentHostingOptions | undefined {
678
- if (hosting === undefined) {
679
- return undefined;
680
- }
681
- const agentPath = displayPath(ctx, join(agentDir, "agent.ts"));
682
- const warn = (message: string): void => {
683
- ctx.diagnostics.push({ severity: "warning", path: agentPath, message });
684
- };
685
-
686
- const egressDomains = hosting.egressDomains ?? [];
687
- for (const domain of egressDomains) {
688
- const domainError = validateEgressDomain(domain);
689
- if (domainError !== undefined) {
690
- warn(`hosting.egressDomains: ${domainError}`);
691
- }
692
- }
693
- if (egressDomains.length > MAX_EGRESS_DOMAINS) {
694
- warn(
695
- `hosting.egressDomains lists ${egressDomains.length} domains; deploys are capped at ${MAX_EGRESS_DOMAINS}.`
696
- );
697
- }
698
-
699
- for (const name of hosting.secretNames ?? []) {
700
- const nameError = validateSecretName(name);
701
- if (nameError !== undefined) {
702
- warn(`hosting.secretNames: ${nameError}`);
703
- } else if (isReservedSecretName(name)) {
704
- warn(
705
- `hosting.secretNames: ${JSON.stringify(name)} is platform-managed (CURSOR_* / reserved hosted-storage names); omit it from secretNames.`
706
- );
707
- }
708
- }
709
-
710
- return {
711
- ...(hosting.egressDomains === undefined
712
- ? {}
713
- : { egressDomains: [...hosting.egressDomains] }),
714
- ...(hosting.secretNames === undefined
715
- ? {}
716
- : { secretNames: [...hosting.secretNames] }),
717
- };
718
- }
719
-
720
- /**
721
- * Validate the authored `concurrency` block. Invalid values are **error**
722
- * diagnostics (serve refuses to start): silently dropping a turn ceiling
723
- * would fail open to unlimited parallelism, the opposite of what the
724
- * author asked for.
725
- */
726
- function resolveConcurrency(
727
- ctx: DiscoveryContext,
728
- agentDir: string,
729
- concurrency: AgentConcurrencyOptions | undefined
730
- ): AgentConcurrencyOptions | undefined {
731
- if (concurrency === undefined || concurrency.maxRunningTurns === undefined) {
732
- return undefined;
733
- }
734
- try {
735
- return {
736
- maxRunningTurns: normalizePositiveInt(concurrency.maxRunningTurns, {
737
- label: "concurrency.maxRunningTurns",
738
- max: MAX_RUNNING_TURNS_LIMIT,
739
- }),
740
- };
741
- } catch (error) {
742
- ctx.diagnostics.push({
743
- severity: "error",
744
- path: displayPath(ctx, join(agentDir, "agent.ts")),
745
- message: error instanceof Error ? error.message : String(error),
746
- });
747
- return undefined;
748
- }
749
- }
750
-
751
- async function loadAgentConfig(
752
- ctx: DiscoveryContext,
753
- agentDir: string,
754
- isRoot: boolean
755
- ): Promise<AgentConfigDefinition | undefined> {
756
- const configPath = await findModuleFile(agentDir, "agent");
757
- if (configPath === undefined) {
758
- ctx.diagnostics.push({
759
- severity: "error",
760
- path: displayPath(ctx, join(agentDir, "agent.ts")),
761
- message: isRoot
762
- ? "agent.ts is required on the root agent: default-export defineAgent({ ... })"
763
- : "agent.ts is required on a subagent: default-export defineAgent({ description: ... })",
764
- });
765
- return undefined;
766
- }
767
-
768
- const definition = await importDefault(ctx, configPath, "agent");
769
- if (definition === undefined) {
770
- return undefined;
771
- }
772
- const config = definition as AgentConfigDefinition;
773
-
774
- if (!isRoot && isBlank(config.description)) {
775
- ctx.diagnostics.push({
776
- severity: "error",
777
- path: displayPath(ctx, configPath),
778
- message:
779
- "Subagents require a description; the parent model reads it to decide when to delegate.",
780
- });
781
- }
782
- return config;
783
- }
784
-
785
- async function loadInstructions(
786
- ctx: DiscoveryContext,
787
- agentDir: string,
788
- config: AgentConfigDefinition | undefined,
789
- isRoot: boolean
790
- ): Promise<string | undefined> {
791
- const parts: string[] = [];
792
-
793
- const markdownPath = join(agentDir, "instructions.md");
794
- if (await isFile(markdownPath)) {
795
- parts.push((await readFile(markdownPath, "utf8")).trim());
796
- }
797
-
798
- const modulePath = await findModuleFile(agentDir, "instructions");
799
- if (modulePath !== undefined) {
800
- const loaded = await loadInstructionsModule(ctx, modulePath);
801
- if (loaded !== undefined) {
802
- parts.push(loaded);
803
- }
804
- }
805
-
806
- const instructionsDir = join(agentDir, "instructions");
807
- if (await isDirectory(instructionsDir)) {
808
- for (const entry of await sortedEntries(instructionsDir)) {
809
- if (!entry.isFile()) {
810
- continue;
811
- }
812
- const entryPath = join(instructionsDir, entry.name);
813
- if (entry.name.endsWith(".md")) {
814
- parts.push((await readFile(entryPath, "utf8")).trim());
815
- } else if (isModuleFile(entry.name)) {
816
- const loaded = await loadInstructionsModule(ctx, entryPath);
817
- if (loaded !== undefined) {
818
- parts.push(loaded);
819
- }
820
- }
821
- }
822
- }
823
-
824
- if (config?.instructions !== undefined && !isBlank(config.instructions)) {
825
- parts.push(config.instructions.trim());
826
- }
827
-
828
- const combined = parts.filter((part) => part !== "").join("\n\n");
829
- if (combined === "") {
830
- if (isRoot) {
831
- ctx.diagnostics.push({
832
- severity: "error",
833
- path: displayPath(ctx, markdownPath),
834
- message:
835
- "instructions.md is required on the root agent (the always-on system prompt).",
836
- });
837
- }
838
- return undefined;
839
- }
840
- return combined;
841
- }
842
-
843
- async function loadInstructionsModule(
844
- ctx: DiscoveryContext,
845
- modulePath: string
846
- ): Promise<string | undefined> {
847
- let mod: Record<string, unknown>;
848
- try {
849
- mod = await ctx.importModule(modulePath);
850
- } catch (error) {
851
- ctx.diagnostics.push({
852
- severity: "error",
853
- path: displayPath(ctx, modulePath),
854
- message: `Failed to load module: ${errorMessage(error)}`,
855
- });
856
- return undefined;
857
- }
858
- const value = mod.default;
859
- if (typeof value === "string") {
860
- return value.trim();
861
- }
862
- if (getDefinitionKind(value) === "instructions") {
863
- return (value as InstructionsDefinition).markdown.trim();
864
- }
865
- ctx.diagnostics.push({
866
- severity: "error",
867
- path: displayPath(ctx, modulePath),
868
- message:
869
- "Instructions modules must default-export a string or defineInstructions({ markdown }).",
870
- });
871
- return undefined;
872
- }
873
-
874
- // ============================================================================
875
- // Tools
876
- // ============================================================================
877
-
878
- async function loadTools(
879
- ctx: DiscoveryContext,
880
- toolsDir: string
881
- ): Promise<DiscoveredTool[]> {
882
- const tools: DiscoveredTool[] = [];
883
- for (const { name, path } of await toolModuleEntries(ctx, toolsDir)) {
884
- const definition = await importDefault(ctx, path, "tool");
885
- if (definition === undefined) {
886
- continue;
887
- }
888
- const tool = compileTool(ctx, path, name, definition as ToolDefinition);
889
- if (tool !== undefined) {
890
- tools.push(tool);
891
- }
892
- }
893
- return tools;
894
- }
895
-
896
- /** The module files of a `tools/` directory whose filenames are valid tool names. */
897
- async function toolModuleEntries(
898
- ctx: DiscoveryContext,
899
- toolsDir: string
900
- ): Promise<Array<{ name: string; path: string }>> {
901
- if (!(await isDirectory(toolsDir))) {
902
- return [];
903
- }
904
- const entries: Array<{ name: string; path: string }> = [];
905
- for (const entry of await sortedEntries(toolsDir)) {
906
- const path = join(toolsDir, entry.name);
907
- if (entry.isDirectory()) {
908
- ctx.diagnostics.push({
909
- severity: "warning",
910
- path: displayPath(ctx, path),
911
- message: "Nested tool directories are not supported; ignoring.",
912
- });
913
- continue;
914
- }
915
- if (!entry.isFile() || !isModuleFile(entry.name)) {
916
- continue;
917
- }
918
- const name = moduleStem(entry.name);
919
- if (validateName(ctx, "tool", name, path)) {
920
- entries.push({ name, path });
921
- }
922
- }
923
- return entries;
924
- }
925
-
926
- /**
927
- * Validate one tool definition against the tool contract and convert its
928
- * schemas. Diagnostics point at `entryPath` (the authored file, or the
929
- * extension mount that contributed the tool).
930
- */
931
- function compileTool(
932
- ctx: DiscoveryContext,
933
- entryPath: string,
934
- name: string,
935
- tool: ToolDefinition
936
- ): DiscoveredTool | undefined {
937
- if (isBlank(tool.description)) {
938
- ctx.diagnostics.push({
939
- severity: "error",
940
- path: displayPath(ctx, entryPath),
941
- message: "Tools require a non-empty description.",
942
- });
943
- return undefined;
944
- }
945
- const execution = tool.execution === "agent" ? "agent" : "server";
946
- const needsApprovalConfigured =
947
- tool.needsApproval === true || typeof tool.needsApproval === "function";
948
- const effect = declaredEffect(tool);
949
- if (execution === "agent") {
950
- if (isBlank(tool.script)) {
951
- ctx.diagnostics.push({
952
- severity: "error",
953
- path: displayPath(ctx, entryPath),
954
- message:
955
- 'Tools with execution: "agent" require a non-empty `script` (shell body; JSON on stdin, result on stdout).',
956
- });
957
- return undefined;
958
- }
959
- if (typeof tool.execute === "function") {
960
- ctx.diagnostics.push({
961
- severity: "warning",
962
- path: displayPath(ctx, entryPath),
963
- message:
964
- '`execute` is ignored when execution is "agent"; the Cursor agent runs `script` instead.',
965
- });
966
- }
967
- if (needsApprovalConfigured) {
968
- ctx.diagnostics.push({
969
- severity: "error",
970
- path: displayPath(ctx, entryPath),
971
- message:
972
- '`needsApproval` is only supported for execution: "server" tools (in-process).',
973
- });
974
- return undefined;
975
- }
976
- } else if (typeof tool.execute !== "function") {
977
- ctx.diagnostics.push({
978
- severity: "error",
979
- path: displayPath(ctx, entryPath),
980
- message:
981
- 'Tools require an execute(input, ctx) function (or set execution: "agent" with a script).',
982
- });
983
- return undefined;
984
- }
985
- let inputSchema: DiscoveredTool["inputSchema"];
986
- try {
987
- inputSchema = toolInputJsonSchema(tool.inputSchema);
988
- } catch (error) {
989
- ctx.diagnostics.push({
990
- severity: "error",
991
- path: displayPath(ctx, entryPath),
992
- message: `Failed to convert inputSchema to JSON Schema: ${errorMessage(error)}`,
993
- });
994
- return undefined;
995
- }
996
- let outputSchema: DiscoveredTool["outputSchema"];
997
- try {
998
- outputSchema = toolOutputJsonSchema(tool.outputSchema);
999
- } catch (error) {
1000
- ctx.diagnostics.push({
1001
- severity: "error",
1002
- path: displayPath(ctx, entryPath),
1003
- message: `Failed to convert outputSchema to JSON Schema: ${errorMessage(error)}`,
1004
- });
1005
- return undefined;
1006
- }
1007
- return {
1008
- name,
1009
- description: tool.description,
1010
- inputSchema,
1011
- outputSchema,
1012
- execution,
1013
- needsApproval: needsApprovalConfigured,
1014
- effect,
1015
- script: execution === "agent" ? tool.script : undefined,
1016
- definition: tool,
1017
- };
1018
- }
1019
-
1020
- /**
1021
- * Static view of a tool's `effect` for the manifest: the literal when it is
1022
- * one, `"dynamic"` for a per-call predicate, `undefined` when undeclared. A
1023
- * malformed value is left undeclared rather than guessed — undeclared is the
1024
- * safe state (a dry run stubs it), where a guess could be the unsafe one.
1025
- */
1026
- function declaredEffect(
1027
- tool: ToolDefinition
1028
- ): ToolEffect | "dynamic" | undefined {
1029
- if (typeof tool.effect === "function") {
1030
- return "dynamic";
1031
- }
1032
- return tool.effect === "read" || tool.effect === "write"
1033
- ? tool.effect
1034
- : undefined;
1035
- }
1036
-
1037
- // ============================================================================
1038
- // Skills
1039
- // ============================================================================
1040
-
1041
- async function loadSkills(
1042
- ctx: DiscoveryContext,
1043
- skillsDir: string
1044
- ): Promise<DiscoveredSkill[]> {
1045
- if (!(await isDirectory(skillsDir))) {
1046
- return [];
1047
- }
1048
- const skills: DiscoveredSkill[] = [];
1049
- for (const entry of await sortedEntries(skillsDir)) {
1050
- const entryPath = join(skillsDir, entry.name);
1051
- if (entry.isDirectory()) {
1052
- const skill = await loadPackagedSkill(ctx, entryPath, entry.name);
1053
- if (skill !== undefined) {
1054
- skills.push(skill);
1055
- }
1056
- continue;
1057
- }
1058
- if (!entry.isFile()) {
1059
- continue;
1060
- }
1061
- if (entry.name.endsWith(".md")) {
1062
- const name = entry.name.slice(0, -".md".length);
1063
- if (!validateName(ctx, "skill", name, entryPath)) {
1064
- continue;
1065
- }
1066
- const { data, body } = parseFrontmatter(
1067
- await readFile(entryPath, "utf8")
1068
- );
1069
- skills.push({
1070
- name,
1071
- description:
1072
- data.description ??
1073
- deriveSkillDescription(body) ??
1074
- SKILL_DESCRIPTION_FALLBACK(name),
1075
- markdown: body.trim(),
1076
- files: {},
1077
- });
1078
- continue;
1079
- }
1080
- if (isModuleFile(entry.name)) {
1081
- const name = moduleStem(entry.name);
1082
- if (!validateName(ctx, "skill", name, entryPath)) {
1083
- continue;
1084
- }
1085
- const definition = await importDefault(ctx, entryPath, "skill");
1086
- if (definition === undefined) {
1087
- continue;
1088
- }
1089
- skills.push(compileSkill(name, definition as SkillDefinition));
1090
- }
1091
- }
1092
- return skills;
1093
- }
1094
-
1095
- /** A skill whose body and sibling files are in memory (`defineSkill`, extensions). */
1096
- function compileSkill(name: string, skill: SkillConfig): DiscoveredSkill {
1097
- const files: Record<string, SkillFileSource> = {};
1098
- for (const [rel, contents] of Object.entries(skill.files ?? {})) {
1099
- files[rel] = { contents };
1100
- }
1101
- return {
1102
- name,
1103
- description:
1104
- skill.description ??
1105
- deriveSkillDescription(skill.markdown) ??
1106
- SKILL_DESCRIPTION_FALLBACK(name),
1107
- markdown: skill.markdown.trim(),
1108
- files,
1109
- };
1110
- }
1111
-
1112
- // ============================================================================
1113
- // Extensions (agent/extensions/<ns>.ts | agent/extensions/<ns>/extension.ts)
1114
- // ============================================================================
1115
-
1116
- /**
1117
- * Mount every extension under `agent/extensions/` and append its tools and
1118
- * skills to the authored lists as `<ns>__<name>`, stamped with `extension`.
1119
- * The namespace is the mount's filename stem or directory name.
1120
- */
1121
- async function appendExtensions(
1122
- ctx: DiscoveryContext,
1123
- extensionsDir: string,
1124
- tools: DiscoveredTool[],
1125
- skills: DiscoveredSkill[]
1126
- ): Promise<DiscoveredExtension[]> {
1127
- if (!(await isDirectory(extensionsDir))) {
1128
- return [];
1129
- }
1130
- const extensions: DiscoveredExtension[] = [];
1131
- for (const entry of await sortedEntries(extensionsDir)) {
1132
- const path = join(extensionsDir, entry.name);
1133
- const site = await resolveMountSite(ctx, path, entry);
1134
- if (site === undefined) {
1135
- continue;
1136
- }
1137
- if (extensions.some((extension) => extension.name === site.ns)) {
1138
- ctx.diagnostics.push({
1139
- severity: "error",
1140
- path: displayPath(ctx, path),
1141
- message: `Extension "${site.ns}" is mounted twice (as a file and as a directory).`,
1142
- });
1143
- continue;
1144
- }
1145
- const mount = await importDefault(ctx, site.mountPath, "extension");
1146
- if (mount === undefined) {
1147
- continue;
1148
- }
1149
- extensions.push(
1150
- await mountExtension(ctx, site, mount as ExtensionMount, tools, skills)
1151
- );
1152
- }
1153
- return extensions;
1154
- }
1155
-
1156
- interface MountSite {
1157
- ns: string;
1158
- /** The module default-exporting the {@link ExtensionMount}. */
1159
- mountPath: string;
1160
- /** A directory mount; its `tools/` and `skills/` overlay the extension's. */
1161
- overlayDir?: string;
1162
- }
1163
-
1164
- async function resolveMountSite(
1165
- ctx: DiscoveryContext,
1166
- path: string,
1167
- entry: Dirent
1168
- ): Promise<MountSite | undefined> {
1169
- if (entry.isDirectory()) {
1170
- if (!validateName(ctx, "extension", entry.name, path)) {
1171
- return undefined;
1172
- }
1173
- const mountPath = await findModuleFile(path, "extension");
1174
- if (mountPath === undefined) {
1175
- ctx.diagnostics.push({
1176
- severity: "error",
1177
- path: displayPath(ctx, path),
1178
- message:
1179
- "A directory mount needs an extension.ts default-exporting the mount (`export default crm({ ... })`).",
1180
- });
1181
- return undefined;
1182
- }
1183
- return { ns: entry.name, mountPath, overlayDir: path };
1184
- }
1185
- if (!entry.isFile() || !isModuleFile(entry.name)) {
1186
- return undefined;
1187
- }
1188
- const ns = moduleStem(entry.name);
1189
- return validateName(ctx, "extension", ns, path)
1190
- ? { ns, mountPath: path }
1191
- : undefined;
1192
- }
1193
-
1194
- /**
1195
- * Read the extension's tree with the agent loaders, lay the mount's overlay
1196
- * on top (a same-named file replaces, `disableTool()` removes), then
1197
- * namespace what is left.
1198
- */
1199
- async function mountExtension(
1200
- ctx: DiscoveryContext,
1201
- site: MountSite,
1202
- mount: ExtensionMount,
1203
- tools: DiscoveredTool[],
1204
- skills: DiscoveredSkill[]
1205
- ): Promise<DiscoveredExtension> {
1206
- const mounted: DiscoveredExtension = {
1207
- name: site.ns,
1208
- description: mount.extension.description,
1209
- tools: [],
1210
- skills: [],
1211
- };
1212
- const root = mount.extension.root;
1213
- if (!(await isDirectory(root))) {
1214
- ctx.diagnostics.push({
1215
- severity: "error",
1216
- path: displayPath(ctx, site.mountPath),
1217
- message: `Extension "${site.ns}" declares root ${root}, which is not a directory. Pass \`import.meta.dirname\` from extension.ts.`,
1218
- });
1219
- return mounted;
1220
- }
1221
- await warnUnsupportedExtensionSlots(ctx, root);
1222
- const contributedTools = new Map(
1223
- (await loadTools(ctx, join(root, "tools"))).map((tool) => [tool.name, tool])
1224
- );
1225
- const contributedSkills = new Map(
1226
- (await loadSkills(ctx, join(root, "skills"))).map((skill) => [
1227
- skill.name,
1228
- skill,
1229
- ])
1230
- );
1231
- if (site.overlayDir !== undefined) {
1232
- const overlay = await loadOverlayTools(ctx, join(site.overlayDir, "tools"));
1233
- for (const tool of overlay.tools) {
1234
- contributedTools.set(tool.name, tool);
1235
- }
1236
- for (const disabled of overlay.disabled) {
1237
- if (!contributedTools.delete(disabled.name)) {
1238
- ctx.diagnostics.push({
1239
- severity: "error",
1240
- path: displayPath(ctx, disabled.path),
1241
- message: `disableTool() targets "${disabled.name}", which extension "${site.ns}" does not contribute.`,
1242
- });
1243
- }
1244
- }
1245
- for (const skill of await loadSkills(
1246
- ctx,
1247
- join(site.overlayDir, "skills")
1248
- )) {
1249
- contributedSkills.set(skill.name, skill);
1250
- }
1251
- }
1252
- for (const [name, tool] of contributedTools) {
1253
- const composed = `${site.ns}__${name}`;
1254
- tools.push({ ...tool, name: composed, extension: site.ns });
1255
- mounted.tools.push(composed);
1256
- }
1257
- for (const [name, skill] of contributedSkills) {
1258
- const composed = `${site.ns}__${name}`;
1259
- skills.push({ ...skill, name: composed, extension: site.ns });
1260
- mounted.skills.push(composed);
1261
- }
1262
- return mounted;
1263
- }
1264
-
1265
- /** A directory mount's `tools/`: replacements plus `disableTool()` markers. */
1266
- async function loadOverlayTools(
1267
- ctx: DiscoveryContext,
1268
- toolsDir: string
1269
- ): Promise<{
1270
- tools: DiscoveredTool[];
1271
- disabled: Array<{ name: string; path: string }>;
1272
- }> {
1273
- const tools: DiscoveredTool[] = [];
1274
- const disabled: Array<{ name: string; path: string }> = [];
1275
- for (const { name, path } of await toolModuleEntries(ctx, toolsDir)) {
1276
- let mod: Record<string, unknown>;
1277
- try {
1278
- mod = await ctx.importModule(path);
1279
- } catch (error) {
1280
- ctx.diagnostics.push({
1281
- severity: "error",
1282
- path: displayPath(ctx, path),
1283
- message: `Failed to load module: ${errorMessage(error)}`,
1284
- });
1285
- continue;
1286
- }
1287
- const kind = getDefinitionKind(mod.default);
1288
- if (kind === "disabled-tool") {
1289
- disabled.push({ name, path });
1290
- continue;
1291
- }
1292
- if (kind !== "tool") {
1293
- ctx.diagnostics.push({
1294
- severity: "error",
1295
- path: displayPath(ctx, path),
1296
- message:
1297
- "An override must default-export defineTool({ ... }) or disableTool().",
1298
- });
1299
- continue;
1300
- }
1301
- const tool = compileTool(ctx, path, name, mod.default as ToolDefinition);
1302
- if (tool !== undefined) {
1303
- tools.push(tool);
1304
- }
1305
- }
1306
- return { tools, disabled };
1307
- }
1308
-
1309
- /** Slots an agent has that an extension tree may not contribute (yet). */
1310
- async function warnUnsupportedExtensionSlots(
1311
- ctx: DiscoveryContext,
1312
- root: string
1313
- ): Promise<void> {
1314
- const unsupported = [
1315
- ["instructions.md", "instructions"],
1316
- ["connections", "connections"],
1317
- ["mcp-connections", "MCP connections"],
1318
- ["hooks", "hooks"],
1319
- ["subagents", "subagents"],
1320
- ["channels", "channels"],
1321
- ["schedules", "schedules"],
1322
- ["extensions", "nested extensions"],
1323
- ] as const;
1324
- for (const [slot, label] of unsupported) {
1325
- const path = join(root, slot);
1326
- if ((await isDirectory(path)) || (await isFile(path))) {
1327
- ctx.diagnostics.push({
1328
- severity: "warning",
1329
- path: displayPath(ctx, path),
1330
- message: `Extension ${label} are not supported yet; only tools/ and skills/ are mounted. Ignoring.`,
1331
- });
1332
- }
1333
- }
1334
- }
1335
-
1336
- async function loadPackagedSkill(
1337
- ctx: DiscoveryContext,
1338
- skillDir: string,
1339
- name: string
1340
- ): Promise<DiscoveredSkill | undefined> {
1341
- const skillMdPath = join(skillDir, "SKILL.md");
1342
- if (!(await isFile(skillMdPath))) {
1343
- ctx.diagnostics.push({
1344
- severity: "warning",
1345
- path: displayPath(ctx, skillDir),
1346
- message: "Skill directories must contain a SKILL.md; ignoring.",
1347
- });
1348
- return undefined;
1349
- }
1350
- if (!validateName(ctx, "skill", name, skillDir)) {
1351
- return undefined;
1352
- }
1353
- const { data, body } = parseFrontmatter(await readFile(skillMdPath, "utf8"));
1354
- if (data.description === undefined || isBlank(data.description)) {
1355
- ctx.diagnostics.push({
1356
- severity: "error",
1357
- path: displayPath(ctx, skillMdPath),
1358
- message:
1359
- "Packaged skills require `description` frontmatter in SKILL.md (there is no filename slug to fall back on).",
1360
- });
1361
- return undefined;
1362
- }
1363
-
1364
- const files: Record<string, SkillFileSource> = {};
1365
- for (const filePath of await walkFiles(skillDir)) {
1366
- const rel = toPosix(relative(skillDir, filePath));
1367
- if (rel === "SKILL.md") {
1368
- continue;
1369
- }
1370
- files[rel] = { sourcePath: filePath };
1371
- }
1372
-
1373
- return {
1374
- name,
1375
- description: data.description,
1376
- markdown: body.trim(),
1377
- files,
1378
- };
1379
- }
1380
-
1381
- /** One-line `/v1/info` label for a connection transport. */
1382
- function connectionTransportLabel(transport: ConnectionTransport): string {
1383
- if ("cursorAccount" in transport) {
1384
- return transport.servers === undefined
1385
- ? "cursor-account"
1386
- : `cursor-account:${transport.servers.join(",")}`;
1387
- }
1388
- if ("agent" in transport) {
1389
- return `agent:${transport.agent}`;
1390
- }
1391
- if ("url" in transport) {
1392
- return transport.oauth === true
1393
- ? `${transport.url} (oauth)`
1394
- : transport.url;
1395
- }
1396
- return transport.command;
1397
- }
1398
-
1399
- function hasAuthorizationHeader(
1400
- headers: Record<string, string> | undefined
1401
- ): boolean {
1402
- if (headers === undefined) {
1403
- return false;
1404
- }
1405
- return Object.keys(headers).some(
1406
- (key) => key.toLowerCase() === "authorization"
1407
- );
1408
- }
1409
-
1410
- function parseConnectionEffects(
1411
- effects: unknown
1412
- ): { ok: true; value: ToolEffect[] | undefined } | { ok: false } {
1413
- if (effects === undefined) {
1414
- return { ok: true, value: undefined };
1415
- }
1416
- const raw = typeof effects === "string" ? [effects] : effects;
1417
- if (!Array.isArray(raw) || raw.length === 0) {
1418
- return { ok: false };
1419
- }
1420
- const parsed: ToolEffect[] = [];
1421
- for (const value of raw) {
1422
- if (!isToolEffect(value)) {
1423
- return { ok: false };
1424
- }
1425
- parsed.push(value);
1426
- }
1427
- return { ok: true, value: normalizeConnectionEffects(parsed) };
1428
- }
1429
-
1430
- function parseConnectionTools(
1431
- tools: unknown
1432
- ): { ok: true; value: string[] | undefined } | { ok: false } {
1433
- if (tools === undefined) {
1434
- return { ok: true, value: undefined };
1435
- }
1436
- if (!Array.isArray(tools) || tools.length === 0) {
1437
- return { ok: false };
1438
- }
1439
- if (tools.some((name) => typeof name !== "string")) {
1440
- return { ok: false };
1441
- }
1442
- const normalized = normalizeConnectionTools(tools);
1443
- if (normalized.length === 0) {
1444
- return { ok: false };
1445
- }
1446
- return { ok: true, value: normalized };
1447
- }
1448
-
1449
- function connectionHasSpawnOrRemote(connection: ConnectionDefinition): boolean {
1450
- if ("cursorAccount" in connection && connection.cursorAccount === true) {
1451
- return true;
1452
- }
1453
- if ("agent" in connection && !isBlank(connection.agent)) {
1454
- return true;
1455
- }
1456
- if ("url" in connection && !isBlank(connection.url)) {
1457
- return true;
1458
- }
1459
- return "command" in connection && !isBlank(connection.command);
1460
- }
1461
-
1462
- function connectionTransport(
1463
- connection: ConnectionDefinition
1464
- ): ConnectionTransport | undefined {
1465
- if ("cursorAccount" in connection && connection.cursorAccount === true) {
1466
- const servers = normalizeCursorAccountServers(connection.servers);
1467
- return {
1468
- cursorAccount: true,
1469
- ...(servers === undefined ? {} : { servers }),
1470
- };
1471
- }
1472
- if ("agent" in connection && !isBlank(connection.agent)) {
1473
- return { agent: connection.agent };
1474
- }
1475
- if ("url" in connection && !isBlank(connection.url)) {
1476
- return {
1477
- type: connection.type,
1478
- url: connection.url,
1479
- headers: connection.headers,
1480
- oauth: connection.oauth === true ? true : undefined,
1481
- };
1482
- }
1483
- if ("command" in connection && !isBlank(connection.command)) {
1484
- return {
1485
- ...(connection.type === undefined ? {} : { type: connection.type }),
1486
- command: connection.command,
1487
- ...(connection.args === undefined ? {} : { args: connection.args }),
1488
- ...(connection.env === undefined ? {} : { env: connection.env }),
1489
- ...(connection.cwd === undefined ? {} : { cwd: connection.cwd }),
1490
- };
1491
- }
1492
- return undefined;
1493
- }
1494
-
1495
- /**
1496
- * Filenames become the names the model sees. `__` is reserved: it joins an
1497
- * extension's namespace to its contribution (`crm__search`).
1498
- */
1499
- function validateName(
1500
- ctx: DiscoveryContext,
1501
- kind: "skill" | "tool" | "extension",
1502
- name: string,
1503
- entryPath: string
1504
- ): boolean {
1505
- const label = { skill: "Skill", tool: "Tool", extension: "Extension" }[kind];
1506
- if (!NAME_PATTERN.test(name)) {
1507
- ctx.diagnostics.push({
1508
- severity: "error",
1509
- path: displayPath(ctx, entryPath),
1510
- message: `${label} name "${name}" must match ${NAME_PATTERN}.`,
1511
- });
1512
- return false;
1513
- }
1514
- if (name.includes("__")) {
1515
- ctx.diagnostics.push({
1516
- severity: "error",
1517
- path: displayPath(ctx, entryPath),
1518
- message: `${label} name "${name}" may not contain "__"; it is reserved for extension contributions (\`<ns>__<name>\`).`,
1519
- });
1520
- return false;
1521
- }
1522
- return true;
1523
- }
1524
-
1525
- /**
1526
- * Advertise the first non-empty, non-code-fence line of the body, with any
1527
- * leading `#`, `>`, `*`, or `-` markers stripped.
1528
- */
1529
- export function deriveSkillDescription(markdown: string): string | undefined {
1530
- let inFence = false;
1531
- for (const rawLine of markdown.split(/\r?\n/)) {
1532
- const line = rawLine.trim();
1533
- if (line.startsWith("```")) {
1534
- inFence = !inFence;
1535
- continue;
1536
- }
1537
- if (inFence || line === "") {
1538
- continue;
1539
- }
1540
- const stripped = line.replace(/^[#>*\s-]+/, "").trim();
1541
- if (stripped !== "") {
1542
- return stripped.length > 200 ? `${stripped.slice(0, 197)}...` : stripped;
1543
- }
1544
- }
1545
- return undefined;
1546
- }
1547
-
1548
- // ============================================================================
1549
- // MCP connections
1550
- // ============================================================================
1551
-
1552
- /**
1553
- * Prefer `agent/mcp-connections/`. Keep loading `agent/connections/` with a
1554
- * deprecation warning so existing agents keep working during the rename.
1555
- */
1556
- async function resolveMcpConnectionsDir(
1557
- ctx: DiscoveryContext,
1558
- agentDir: string
1559
- ): Promise<string> {
1560
- const preferred = join(agentDir, "mcp-connections");
1561
- const legacy = join(agentDir, "connections");
1562
- const hasPreferred = await isDirectory(preferred);
1563
- const hasLegacy = await isDirectory(legacy);
1564
- if (hasPreferred && hasLegacy) {
1565
- ctx.diagnostics.push({
1566
- severity: "warning",
1567
- path: displayPath(ctx, legacy),
1568
- message:
1569
- "Both agent/mcp-connections/ and agent/connections/ exist; using mcp-connections/ and ignoring connections/. Rename or remove the legacy directory.",
1570
- });
1571
- return preferred;
1572
- }
1573
- if (hasLegacy) {
1574
- ctx.diagnostics.push({
1575
- severity: "warning",
1576
- path: displayPath(ctx, legacy),
1577
- message:
1578
- "agent/connections/ is deprecated; rename it to agent/mcp-connections/. Loading from the legacy path for now.",
1579
- });
1580
- return legacy;
1581
- }
1582
- return preferred;
1583
- }
1584
-
1585
- async function loadHostConnections(
1586
- ctx: DiscoveryContext,
1587
- agentDir: string,
1588
- modelConnections: readonly DiscoveredConnection[]
1589
- ): Promise<DiscoveredConnection[]> {
1590
- const hostDir = join(agentDir, "host-connections");
1591
- const loaded = await loadConnections(ctx, hostDir, "host");
1592
- const modelNames = new Set(
1593
- modelConnections.map((connection) => connection.name)
1594
- );
1595
- const hostConnections: DiscoveredConnection[] = [];
1596
- for (const connection of loaded) {
1597
- if (modelNames.has(connection.name)) {
1598
- ctx.diagnostics.push({
1599
- severity: "error",
1600
- path: displayPath(ctx, join(hostDir, `${connection.name}.ts`)),
1601
- message: `Host connection "${connection.name}" collides with agent/mcp-connections/${connection.name}. Rename one of them.`,
1602
- });
1603
- continue;
1604
- }
1605
- if (connection.advertiseTools === true) {
1606
- ctx.diagnostics.push({
1607
- severity: "error",
1608
- path: displayPath(ctx, join(hostDir, `${connection.name}.ts`)),
1609
- message:
1610
- "Host connections are off the model. `advertiseTools` cannot apply. Drop it.",
1611
- });
1612
- continue;
1613
- }
1614
- if (connection.auth !== undefined) {
1615
- ctx.diagnostics.push({
1616
- severity: "error",
1617
- path: displayPath(ctx, join(hostDir, `${connection.name}.ts`)),
1618
- message:
1619
- "Host connections are off the model. `auth` cannot apply. Use static headers or `oauth: true`.",
1620
- });
1621
- continue;
1622
- }
1623
- hostConnections.push(connection);
1624
- }
1625
- return hostConnections;
1626
- }
1627
-
1628
- async function loadConnections(
1629
- ctx: DiscoveryContext,
1630
- connectionsDir: string,
1631
- role: "model" | "host" = "model"
1632
- ): Promise<DiscoveredConnection[]> {
1633
- if (!(await isDirectory(connectionsDir))) {
1634
- return [];
1635
- }
1636
- const connections: DiscoveredConnection[] = [];
1637
- for (const entry of await sortedEntries(connectionsDir)) {
1638
- const entryPath = join(connectionsDir, entry.name);
1639
- if (!entry.isFile() || !isModuleFile(entry.name)) {
1640
- continue;
1641
- }
1642
- const name = moduleStem(entry.name);
1643
- if (!NAME_PATTERN.test(name)) {
1644
- ctx.diagnostics.push({
1645
- severity: "error",
1646
- path: displayPath(ctx, entryPath),
1647
- message: `MCP connection name "${name}" must match ${NAME_PATTERN}.`,
1648
- });
1649
- continue;
1650
- }
1651
- const definition = await importDefault(ctx, entryPath, "connection");
1652
- if (definition === undefined) {
1653
- continue;
1654
- }
1655
- const connection = definition as ConnectionDefinition;
1656
- if (Object.hasOwn(connection, "hostOnly")) {
1657
- ctx.diagnostics.push({
1658
- severity: "error",
1659
- path: displayPath(ctx, entryPath),
1660
- message:
1661
- "MCP connection `hostOnly` was removed. Files under `agent/mcp-connections/` are always visible to the model. Put privileged servers in `agent/host-connections/` so only host tools can call them.",
1662
- });
1663
- continue;
1664
- }
1665
- if (
1666
- isInlineStdioConnection(connection) &&
1667
- connectionHasSpawnOrRemote(connection)
1668
- ) {
1669
- ctx.diagnostics.push({
1670
- severity: "error",
1671
- path: displayPath(ctx, entryPath),
1672
- message:
1673
- "MCP connection `tools` cannot be combined with `command`, `url`, `agent`, or `cursorAccount`. Author TypeScript tools, or point at an existing server, not both.",
1674
- });
1675
- continue;
1676
- }
1677
- if (
1678
- connection.auth !== undefined &&
1679
- typeof connection.auth !== "function"
1680
- ) {
1681
- ctx.diagnostics.push({
1682
- severity: "error",
1683
- path: displayPath(ctx, entryPath),
1684
- message:
1685
- "MCP connection `auth` must be a function: (session) => { headers }.",
1686
- });
1687
- continue;
1688
- }
1689
- if (connection.auth !== undefined && !("url" in connection)) {
1690
- ctx.diagnostics.push({
1691
- severity: "error",
1692
- path: displayPath(ctx, entryPath),
1693
- message:
1694
- "MCP connection `auth` (per-session headers) is only supported on http/sse connections (`url`).",
1695
- });
1696
- continue;
1697
- }
1698
- if (
1699
- connection.advertiseTools !== undefined &&
1700
- typeof connection.advertiseTools !== "boolean"
1701
- ) {
1702
- ctx.diagnostics.push({
1703
- severity: "error",
1704
- path: displayPath(ctx, entryPath),
1705
- message: "MCP connection `advertiseTools` must be a boolean.",
1706
- });
1707
- continue;
1708
- }
1709
- const parsedEffects = parseConnectionEffects(connection.effects);
1710
- if (!parsedEffects.ok) {
1711
- ctx.diagnostics.push({
1712
- severity: "error",
1713
- path: displayPath(ctx, entryPath),
1714
- message:
1715
- 'MCP connection `effects` must be "read", "write", or a non-empty array of those.',
1716
- });
1717
- continue;
1718
- }
1719
- if (
1720
- parsedEffects.value !== undefined &&
1721
- connection.advertiseTools !== true
1722
- ) {
1723
- ctx.diagnostics.push({
1724
- severity: "error",
1725
- path: displayPath(ctx, entryPath),
1726
- message:
1727
- "MCP connection `effects` filters advertised tools; set `advertiseTools: true` or drop `effects`.",
1728
- });
1729
- continue;
1730
- }
1731
- const parsedTools = isInlineStdioConnection(connection)
1732
- ? { ok: true as const, value: undefined }
1733
- : parseConnectionTools(connection.tools);
1734
- if (!parsedTools.ok) {
1735
- ctx.diagnostics.push({
1736
- severity: "error",
1737
- path: displayPath(ctx, entryPath),
1738
- message:
1739
- "MCP connection `tools` allowlist must be a non-empty array of tool names.",
1740
- });
1741
- continue;
1742
- }
1743
- if (
1744
- isInlineStdioConnection(connection) &&
1745
- connection.disallowedTools !== undefined
1746
- ) {
1747
- ctx.diagnostics.push({
1748
- severity: "error",
1749
- path: displayPath(ctx, entryPath),
1750
- message:
1751
- "MCP connection `disallowedTools` is not supported on inline `tools` connections — author only the tools you want.",
1752
- });
1753
- continue;
1754
- }
1755
- const parsedDisallowed = isInlineStdioConnection(connection)
1756
- ? { ok: true as const, value: undefined }
1757
- : parseConnectionTools(connection.disallowedTools);
1758
- if (!parsedDisallowed.ok) {
1759
- ctx.diagnostics.push({
1760
- severity: "error",
1761
- path: displayPath(ctx, entryPath),
1762
- message:
1763
- "MCP connection `disallowedTools` must be a non-empty array of tool names.",
1764
- });
1765
- continue;
1766
- }
1767
- if (
1768
- (parsedTools.value !== undefined ||
1769
- parsedDisallowed.value !== undefined) &&
1770
- role === "model" &&
1771
- connection.advertiseTools !== true
1772
- ) {
1773
- ctx.diagnostics.push({
1774
- severity: "error",
1775
- path: displayPath(ctx, entryPath),
1776
- message:
1777
- "MCP connection `tools` / `disallowedTools` filter advertised tools; set `advertiseTools: true` or drop the name list. Put the connection in `agent/host-connections/` to restrict `host.mcp` only.",
1778
- });
1779
- continue;
1780
- }
1781
- if (
1782
- connection.advertiseTools === true &&
1783
- isInlineStdioConnection(connection)
1784
- ) {
1785
- ctx.diagnostics.push({
1786
- severity: "error",
1787
- path: displayPath(ctx, entryPath),
1788
- message:
1789
- "MCP connection `advertiseTools` is not supported on inline `tools` connections — author those as agent/tools/ server tools instead.",
1790
- });
1791
- continue;
1792
- }
1793
- if (
1794
- connection.auth !== undefined &&
1795
- "url" in connection &&
1796
- connection.oauth === true
1797
- ) {
1798
- ctx.diagnostics.push({
1799
- severity: "error",
1800
- path: displayPath(ctx, entryPath),
1801
- message:
1802
- "MCP connection `oauth: true` cannot be combined with `auth` (per-session headers): the host OAuth provider owns the Authorization header. Drop one of them.",
1803
- });
1804
- continue;
1805
- }
1806
- if (isInlineStdioConnection(connection)) {
1807
- const packed = await loadInlineStdioConnection(
1808
- ctx,
1809
- name,
1810
- entryPath,
1811
- connection
1812
- );
1813
- if (packed === undefined) {
1814
- continue;
1815
- }
1816
- connections.push(packed);
1817
- continue;
1818
- }
1819
- if (
1820
- "url" in connection &&
1821
- connection.oauth === true &&
1822
- hasAuthorizationHeader(connection.headers)
1823
- ) {
1824
- ctx.diagnostics.push({
1825
- severity: "error",
1826
- path: displayPath(ctx, entryPath),
1827
- message:
1828
- "MCP connection `oauth: true` cannot set an Authorization header. Drop the header or remove `oauth`.",
1829
- });
1830
- continue;
1831
- }
1832
- if (
1833
- "cursorAccount" in connection &&
1834
- connection.cursorAccount === true &&
1835
- cursorAccountServersHasMixedWildcard(connection.servers)
1836
- ) {
1837
- ctx.diagnostics.push({
1838
- severity: "error",
1839
- path: displayPath(ctx, entryPath),
1840
- message:
1841
- 'Cursor account MCP connection `servers` cannot mix `"*"` with connector names. Use `"*"` / `["*"]` (or omit `servers`) for every connected server, or list only concrete connector names.',
1842
- });
1843
- continue;
1844
- }
1845
- const transport = connectionTransport(connection);
1846
- if (transport === undefined) {
1847
- ctx.diagnostics.push({
1848
- severity: "error",
1849
- path: displayPath(ctx, entryPath),
1850
- message:
1851
- "MCP connections require a `url` (http/sse), a `command` (stdio), `tools` (TypeScript packaged as stdio), an `agent` (peer agent slug on the same serve host), or `cursorAccount: true` (the Cursor account's MCP connectors).",
1852
- });
1853
- continue;
1854
- }
1855
- if (
1856
- "cursorAccount" in transport &&
1857
- transport.servers !== undefined &&
1858
- (transport.servers.length === 0 ||
1859
- transport.servers.some((server) => isBlank(server)))
1860
- ) {
1861
- ctx.diagnostics.push({
1862
- severity: "error",
1863
- path: displayPath(ctx, entryPath),
1864
- message:
1865
- 'Cursor account MCP connection `servers` must be a non-empty list of connector names, `"*"` / `["*"]` for every connected server, or omitted for the same all-connectors behavior.',
1866
- });
1867
- continue;
1868
- }
1869
- if ("agent" in transport && !NAME_PATTERN.test(transport.agent)) {
1870
- ctx.diagnostics.push({
1871
- severity: "error",
1872
- path: displayPath(ctx, entryPath),
1873
- message: `Peer MCP connection agent "${transport.agent}" must be a mount slug matching ${NAME_PATTERN}.`,
1874
- });
1875
- continue;
1876
- }
1877
- connections.push({
1878
- name,
1879
- transport,
1880
- description: connection.description,
1881
- auth: connection.auth,
1882
- advertiseTools: connection.advertiseTools,
1883
- effects: parsedEffects.value,
1884
- tools: parsedTools.value,
1885
- disallowedTools: parsedDisallowed.value,
1886
- });
1887
- }
1888
- return connections;
1889
- }
1890
-
1891
- async function loadInlineStdioConnection(
1892
- ctx: DiscoveryContext,
1893
- name: string,
1894
- entryPath: string,
1895
- connection: InlineStdioConnectionConfig & {
1896
- description?: string;
1897
- }
1898
- ): Promise<DiscoveredConnection | undefined> {
1899
- const tools = connection.tools;
1900
- const names = Object.keys(tools);
1901
- if (names.length === 0) {
1902
- ctx.diagnostics.push({
1903
- severity: "error",
1904
- path: displayPath(ctx, entryPath),
1905
- message: "MCP connection `tools` must declare at least one tool.",
1906
- });
1907
- return undefined;
1908
- }
1909
- for (const toolName of names) {
1910
- if (!NAME_PATTERN.test(toolName)) {
1911
- ctx.diagnostics.push({
1912
- severity: "error",
1913
- path: displayPath(ctx, entryPath),
1914
- message: `MCP tool name "${toolName}" must match ${NAME_PATTERN}.`,
1915
- });
1916
- return undefined;
1917
- }
1918
- const tool = tools[toolName];
1919
- if (tool === undefined || isBlank(tool.description)) {
1920
- ctx.diagnostics.push({
1921
- severity: "error",
1922
- path: displayPath(ctx, entryPath),
1923
- message: `MCP tool "${toolName}" requires a non-empty description.`,
1924
- });
1925
- return undefined;
1926
- }
1927
- if (typeof tool.execute !== "function") {
1928
- ctx.diagnostics.push({
1929
- severity: "error",
1930
- path: displayPath(ctx, entryPath),
1931
- message: `MCP tool "${toolName}" requires an execute(input) function.`,
1932
- });
1933
- return undefined;
1934
- }
1935
- }
1936
- try {
1937
- const transport = await packageInlineStdioConnection({
1938
- name,
1939
- entryPath,
1940
- tools,
1941
- absWorkingDir: ctx.rootDir,
1942
- });
1943
- return {
1944
- name,
1945
- transport,
1946
- description: connection.description,
1947
- };
1948
- } catch (error) {
1949
- ctx.diagnostics.push({
1950
- severity: "error",
1951
- path: displayPath(ctx, entryPath),
1952
- message: `Failed to package inline stdio MCP connection: ${errorMessage(error)}`,
1953
- });
1954
- return undefined;
1955
- }
1956
- }
1957
-
1958
- // ============================================================================
1959
- // Subagents
1960
- // ============================================================================
1961
-
1962
- async function loadSubagents(
1963
- ctx: DiscoveryContext,
1964
- subagentsDir: string
1965
- ): Promise<ResolvedAgent[]> {
1966
- if (!(await isDirectory(subagentsDir))) {
1967
- return [];
1968
- }
1969
- const subagents: ResolvedAgent[] = [];
1970
- for (const entry of await sortedEntries(subagentsDir)) {
1971
- if (!entry.isDirectory()) {
1972
- continue;
1973
- }
1974
- const subagentDir = join(subagentsDir, entry.name);
1975
- if (!NAME_PATTERN.test(entry.name)) {
1976
- ctx.diagnostics.push({
1977
- severity: "error",
1978
- path: displayPath(ctx, subagentDir),
1979
- message: `Subagent name "${entry.name}" must match ${NAME_PATTERN}.`,
1980
- });
1981
- continue;
1982
- }
1983
- subagents.push(
1984
- await loadAgent(ctx, subagentDir, { name: entry.name, isRoot: false })
1985
- );
1986
- }
1987
- return subagents;
1988
- }
1989
-
1990
- async function warnUnsupportedSubagentSlots(
1991
- ctx: DiscoveryContext,
1992
- subagentDir: string
1993
- ): Promise<void> {
1994
- const unsupported = [
1995
- ["tools", "tools"],
1996
- ["skills", "skills"],
1997
- ["extensions", "extensions"],
1998
- ["mcp-connections", "MCP connections"],
1999
- ["host-connections", "host MCP connections"],
2000
- ["connections", "MCP connections"],
2001
- ["subagents", "nested subagents"],
2002
- ["channels", "channels"],
2003
- ["schedules", "schedules"],
2004
- ["hooks", "hooks"],
2005
- ["sandbox", "sandbox files"],
2006
- ] as const;
2007
- for (const [slot, label] of unsupported) {
2008
- if (await isDirectory(join(subagentDir, slot))) {
2009
- ctx.diagnostics.push({
2010
- severity: "warning",
2011
- path: displayPath(ctx, join(subagentDir, slot)),
2012
- message: `Subagent ${label} are not supported on the Cursor harness yet; the subagent runs with the parent's execution surface. Ignoring.`,
2013
- });
2014
- }
2015
- }
2016
- }
2017
-
2018
- /**
2019
- * Append enabled built-in tools after authored tools. Authored tools with
2020
- * the same name win.
2021
- */
2022
- function appendBuiltinTools(
2023
- ctx: DiscoveryContext,
2024
- agentDir: string,
2025
- tools: DiscoveredTool[],
2026
- config: BuiltinToolsConfig | undefined
2027
- ): void {
2028
- const builtin = builtinTools(config);
2029
- if (builtin.length === 0) {
2030
- return;
2031
- }
2032
- const agentPath = displayPath(ctx, join(agentDir, "agent.ts"));
2033
- for (const tool of builtin) {
2034
- const existing = tools.find((t) => t.name === tool.name);
2035
- if (existing !== undefined) {
2036
- const owner =
2037
- existing.extension === undefined
2038
- ? "Authored"
2039
- : `Extension "${existing.extension}"`;
2040
- ctx.diagnostics.push({
2041
- severity: "warning",
2042
- path: agentPath,
2043
- message: `${owner} tool "${tool.name}" shadows the built-in tool of the same name; the built-in is skipped.`,
2044
- });
2045
- continue;
2046
- }
2047
- tools.push(tool);
2048
- }
2049
- }
2050
-
2051
- /**
2052
- * Validate the authored runtime. An unrecognized value is an error
2053
- * diagnostic and falls back to local — a typo must not silently pick a
2054
- * different harness for every turn.
2055
- */
2056
- function resolveRuntime(
2057
- ctx: DiscoveryContext,
2058
- agentDir: string,
2059
- runtime: AgentRuntime | undefined
2060
- ): AgentRuntime {
2061
- if (
2062
- runtime === undefined ||
2063
- runtime === "local" ||
2064
- runtime === "cloud" ||
2065
- runtime === "grokbot"
2066
- ) {
2067
- return runtime ?? "local";
2068
- }
2069
- ctx.diagnostics.push({
2070
- severity: "error",
2071
- path: displayPath(ctx, join(agentDir, "agent.ts")),
2072
- message: `runtime must be "local", "cloud", or "grokbot"; got ${JSON.stringify(runtime)}.`,
2073
- });
2074
- return "local";
2075
- }
2076
-
2077
- /**
2078
- * Grok Bot turns run on the Sand harness with its own toolset: authored
2079
- * server tools, MCP connections, and subagents have no path onto that
2080
- * toolset yet, so an agent declaring them must fail at serve start rather
2081
- * than run with capabilities silently missing.
2082
- */
2083
- function checkGrokBotRuntimeLimitations(
2084
- ctx: DiscoveryContext,
2085
- agentDir: string,
2086
- counts: {
2087
- serverTools: number;
2088
- agentTools: number;
2089
- skills: number;
2090
- seedFiles: number;
2091
- connections: number;
2092
- subagents: number;
2093
- hasCloudOptions: boolean;
2094
- hasLocalSandbox: boolean;
2095
- }
2096
- ): void {
2097
- const agentPath = displayPath(ctx, join(agentDir, "agent.ts"));
2098
- const error = (path: string, message: string): void => {
2099
- ctx.diagnostics.push({ severity: "error", path, message });
2100
- };
2101
- const warn = (path: string, message: string): void => {
2102
- ctx.diagnostics.push({ severity: "warning", path, message });
2103
- };
2104
- if (counts.serverTools > 0) {
2105
- error(
2106
- displayPath(ctx, join(agentDir, "tools")),
2107
- 'Server tools (execution: "server", including opted-in builtinTools) are not reachable from grokbot turns yet. Remove them or run the agent on the local runtime.'
2108
- );
2109
- }
2110
- if (counts.connections > 0) {
2111
- error(
2112
- displayPath(ctx, agentDir),
2113
- "MCP connections (agent/mcp-connections/ or agent/host-connections/) are not wired into grokbot turns yet. Remove them or run the agent on the local runtime."
2114
- );
2115
- }
2116
- if (counts.subagents > 0) {
2117
- error(
2118
- displayPath(ctx, join(agentDir, "subagents")),
2119
- "Authored subagents are not available on grokbot turns (the Grok Bot harness runs its own subagent set). Remove them or run the agent on the local runtime."
2120
- );
2121
- }
2122
- // The remaining declarations mirror the cloud runtime's warnings: the
2123
- // turn runs on a hosted machine (the account's box), so local-workspace
2124
- // materialization never reaches it.
2125
- if (counts.agentTools > 0) {
2126
- warn(
2127
- displayPath(ctx, join(agentDir, "tools")),
2128
- 'Agent tools (execution: "agent") are described in the hosted session\'s instructions (with script bodies); they are not written onto the box filesystem automatically.'
2129
- );
2130
- }
2131
- if (counts.skills > 0) {
2132
- warn(
2133
- displayPath(ctx, join(agentDir, "skills")),
2134
- "Authored skills are materialized into the local session workspace only; the hosted Grok Bot box does not see them."
2135
- );
2136
- }
2137
- if (counts.seedFiles > 0) {
2138
- warn(
2139
- displayPath(ctx, join(agentDir, "sandbox")),
2140
- "sandbox/workspace seed files apply to the local session workspace only; grokbot turns run on the account's hosted box."
2141
- );
2142
- }
2143
- if (counts.hasCloudOptions) {
2144
- warn(
2145
- agentPath,
2146
- 'The `cloud` block is unused on runtime: "grokbot", and grokbot sessions refuse per-session cloud attach.'
2147
- );
2148
- }
2149
- if (counts.hasLocalSandbox) {
2150
- warn(
2151
- agentPath,
2152
- 'local.sandbox does not apply to runtime: "grokbot"; the box is the account\'s Cursor-hosted computer.'
2153
- );
2154
- }
2155
- }
2156
-
2157
- function warnCloudRuntimeLimitations(
2158
- ctx: DiscoveryContext,
2159
- agentDir: string,
2160
- counts: {
2161
- agentTools: number;
2162
- skills: number;
2163
- seedFiles: number;
2164
- hasCloudOptions: boolean;
2165
- }
2166
- ): void {
2167
- const agentPath = displayPath(ctx, join(agentDir, "agent.ts"));
2168
- if (!counts.hasCloudOptions) {
2169
- ctx.diagnostics.push({
2170
- severity: "warning",
2171
- path: agentPath,
2172
- message:
2173
- 'runtime: "cloud" usually needs a `cloud` block (e.g. repos / env). Without it the Cursor cloud API may reject create.',
2174
- });
2175
- }
2176
- if (counts.agentTools > 0) {
2177
- ctx.diagnostics.push({
2178
- severity: "warning",
2179
- path: displayPath(ctx, join(agentDir, "tools")),
2180
- message:
2181
- 'Agent tools (execution: "agent") are described on the first cloud prompt (with script bodies); they are not written into the cloud VM filesystem automatically.',
2182
- });
2183
- }
2184
- if (counts.skills > 0) {
2185
- ctx.diagnostics.push({
2186
- severity: "warning",
2187
- path: displayPath(ctx, join(agentDir, "skills")),
2188
- message:
2189
- "Authored skills reach cloud agents through native store discovery: hosted deployments write the deployment store's skills/ directory; local serve/run with a personal API key writes agent-serve/<agent>/skills/ on the USER store. Without a reachable store, only skills already in the cloud repo are visible.",
2190
- });
2191
- }
2192
- if (counts.seedFiles > 0) {
2193
- ctx.diagnostics.push({
2194
- severity: "warning",
2195
- path: displayPath(ctx, join(agentDir, "sandbox")),
2196
- message:
2197
- "sandbox/workspace seed files apply to the local session workspace only; cloud agents use the configured cloud repos.",
2198
- });
2199
- }
2200
- }
2201
-
2202
- // ============================================================================
2203
- // Channels
2204
- // ============================================================================
2205
-
2206
- async function loadChannels(
2207
- ctx: DiscoveryContext,
2208
- channelsDir: string
2209
- ): Promise<{
2210
- channels: DiscoveredChannel[];
2211
- httpChannelDefinition: HttpChannelDefinition | undefined;
2212
- }> {
2213
- const channels: DiscoveredChannel[] = [];
2214
- let httpChannelDefinition: HttpChannelDefinition | undefined;
2215
- if (!(await isDirectory(channelsDir))) {
2216
- return { channels, httpChannelDefinition };
2217
- }
2218
-
2219
- for (const entry of await sortedEntries(channelsDir)) {
2220
- const entryPath = join(channelsDir, entry.name);
2221
- if (!entry.isFile() || !isModuleFile(entry.name)) {
2222
- continue;
2223
- }
2224
- const id = moduleStem(entry.name);
2225
- if (!NAME_PATTERN.test(id)) {
2226
- ctx.diagnostics.push({
2227
- severity: "error",
2228
- path: displayPath(ctx, entryPath),
2229
- message: `Channel id "${id}" must match ${NAME_PATTERN}.`,
2230
- });
2231
- continue;
2232
- }
2233
-
2234
- let mod: Record<string, unknown>;
2235
- try {
2236
- mod = await ctx.importModule(entryPath);
2237
- } catch (error) {
2238
- ctx.diagnostics.push({
2239
- severity: "error",
2240
- path: displayPath(ctx, entryPath),
2241
- message: `Failed to load module: ${errorMessage(error)}`,
2242
- });
2243
- continue;
2244
- }
2245
- const kind = getDefinitionKind(mod.default);
2246
-
2247
- if (kind === "http-channel") {
2248
- if (id !== "http") {
2249
- ctx.diagnostics.push({
2250
- severity: "error",
2251
- path: displayPath(ctx, entryPath),
2252
- message:
2253
- "httpChannel() configures the built-in HTTP channel and must be authored at channels/http.ts.",
2254
- });
2255
- continue;
2256
- }
2257
- httpChannelDefinition = mod.default as HttpChannelDefinition;
2258
- continue;
2259
- }
2260
-
2261
- if (kind !== "channel") {
2262
- ctx.diagnostics.push({
2263
- severity: "error",
2264
- path: displayPath(ctx, entryPath),
2265
- message:
2266
- id === "http"
2267
- ? "channels/http.ts must default-export httpChannel({ ... })."
2268
- : "Channel modules must default-export defineChannel({ ... }).",
2269
- });
2270
- continue;
2271
- }
2272
- if (id === "http") {
2273
- ctx.diagnostics.push({
2274
- severity: "error",
2275
- path: displayPath(ctx, entryPath),
2276
- message:
2277
- "The channel id `http` is reserved for the built-in HTTP channel; use httpChannel() to configure it.",
2278
- });
2279
- continue;
2280
- }
2281
- const definition = mod.default as ChannelDefinition;
2282
- for (const notice of definition.meta?.deprecations ?? []) {
2283
- ctx.diagnostics.push({
2284
- severity: "warning",
2285
- path: displayPath(ctx, entryPath),
2286
- message: notice,
2287
- });
2288
- }
2289
- channels.push({ id, definition });
2290
- }
2291
- return { channels, httpChannelDefinition };
2292
- }
2293
-
2294
- // ============================================================================
2295
- // Schedules
2296
- // ============================================================================
2297
-
2298
- async function loadSchedules(
2299
- ctx: DiscoveryContext,
2300
- schedulesDir: string
2301
- ): Promise<DiscoveredSchedule[]> {
2302
- if (!(await isDirectory(schedulesDir))) {
2303
- return [];
2304
- }
2305
- const schedules: DiscoveredSchedule[] = [];
2306
- for (const filePath of await walkFiles(schedulesDir)) {
2307
- const rel = toPosix(relative(schedulesDir, filePath));
2308
- if (rel.endsWith(".md")) {
2309
- const name = rel.slice(0, -".md".length);
2310
- const { data, body } = parseFrontmatter(await readFile(filePath, "utf8"));
2311
- const cron = data.cron;
2312
- if (cron === undefined || !isValidCron(cron)) {
2313
- ctx.diagnostics.push({
2314
- severity: "error",
2315
- path: displayPath(ctx, filePath),
2316
- message:
2317
- cron === undefined
2318
- ? "Markdown schedules require `cron` frontmatter."
2319
- : `Invalid cron expression: "${cron}".`,
2320
- });
2321
- continue;
2322
- }
2323
- const markdown = body.trim();
2324
- if (markdown === "") {
2325
- ctx.diagnostics.push({
2326
- severity: "error",
2327
- path: displayPath(ctx, filePath),
2328
- message: "Markdown schedules require a non-empty prompt body.",
2329
- });
2330
- continue;
2331
- }
2332
- schedules.push({
2333
- name,
2334
- cron,
2335
- kind: "markdown",
2336
- definition: {
2337
- __agentServe: "schedule",
2338
- cron,
2339
- markdown,
2340
- },
2341
- });
2342
- continue;
2343
- }
2344
-
2345
- if (!isModuleFile(basename(filePath))) {
2346
- continue;
2347
- }
2348
- const name = rel.slice(0, -extname(rel).length);
2349
- const definition = await importDefault(ctx, filePath, "schedule");
2350
- if (definition === undefined) {
2351
- continue;
2352
- }
2353
- const schedule = definition as ScheduleDefinition;
2354
- if ((schedule.markdown === undefined) === (schedule.run === undefined)) {
2355
- ctx.diagnostics.push({
2356
- severity: "error",
2357
- path: displayPath(ctx, filePath),
2358
- message: "Schedules require exactly one of `markdown` or `run`.",
2359
- });
2360
- continue;
2361
- }
2362
- if (!isValidCron(schedule.cron)) {
2363
- ctx.diagnostics.push({
2364
- severity: "error",
2365
- path: displayPath(ctx, filePath),
2366
- message: `Invalid cron expression: "${schedule.cron}".`,
2367
- });
2368
- continue;
2369
- }
2370
- schedules.push({
2371
- name,
2372
- cron: schedule.cron,
2373
- kind: schedule.run === undefined ? "markdown" : "run",
2374
- definition: schedule,
2375
- });
2376
- }
2377
- return schedules;
2378
- }
2379
-
2380
- // ============================================================================
2381
- // Hooks
2382
- // ============================================================================
2383
-
2384
- async function loadHooks(
2385
- ctx: DiscoveryContext,
2386
- hooksDir: string
2387
- ): Promise<DiscoveredHook[]> {
2388
- if (!(await isDirectory(hooksDir))) {
2389
- return [];
2390
- }
2391
- const hooks: DiscoveredHook[] = [];
2392
- for (const filePath of await walkFiles(hooksDir)) {
2393
- if (!isModuleFile(basename(filePath))) {
2394
- continue;
2395
- }
2396
- const rel = toPosix(relative(hooksDir, filePath));
2397
- const name = rel.slice(0, -extname(rel).length);
2398
- const definition = await importDefault(ctx, filePath, "hook");
2399
- if (definition === undefined) {
2400
- continue;
2401
- }
2402
- const hook = definition as HookDefinition;
2403
- if (
2404
- typeof hook.events !== "object" ||
2405
- hook.events === null ||
2406
- Object.keys(hook.events).length === 0
2407
- ) {
2408
- ctx.diagnostics.push({
2409
- severity: "warning",
2410
- path: displayPath(ctx, filePath),
2411
- message: "Hook declares no event handlers.",
2412
- });
2413
- continue;
2414
- }
2415
- hooks.push({ name, definition: hook });
2416
- }
2417
- return hooks;
2418
- }
2419
-
2420
- // ============================================================================
2421
- // A/B metrics (agent/ab.ts or agent/ab/*.ts) + agent/ab.config.ts
2422
- // ============================================================================
2423
-
2424
- async function loadABs(
2425
- ctx: DiscoveryContext,
2426
- agentDir: string
2427
- ): Promise<DiscoveredAB[]> {
2428
- const abs: DiscoveredAB[] = [];
2429
- const seen = new Set<string>();
2430
-
2431
- const single = await findModuleFile(agentDir, "ab");
2432
- if (single !== undefined) {
2433
- const definition = await importDefault(ctx, single, "ab");
2434
- if (definition !== undefined) {
2435
- const name = resolveABName(definition, "ab");
2436
- seen.add(name);
2437
- abs.push({
2438
- name,
2439
- definition: definition as DiscoveredAB["definition"],
2440
- });
2441
- }
2442
- }
2443
-
2444
- const absDir = join(agentDir, "ab");
2445
- if (await isDirectory(absDir)) {
2446
- for (const filePath of await walkFiles(absDir)) {
2447
- if (!isModuleFile(basename(filePath))) {
2448
- continue;
2449
- }
2450
- const rel = toPosix(relative(absDir, filePath));
2451
- const stem = rel.slice(0, -extname(rel).length);
2452
- const definition = await importDefault(ctx, filePath, "ab");
2453
- if (definition === undefined) {
2454
- continue;
2455
- }
2456
- const name = resolveABName(definition, stem);
2457
- if (seen.has(name)) {
2458
- ctx.diagnostics.push({
2459
- severity: "error",
2460
- path: displayPath(ctx, filePath),
2461
- message: `Duplicate A/B experiment name ${JSON.stringify(name)}.`,
2462
- });
2463
- continue;
2464
- }
2465
- seen.add(name);
2466
- abs.push({
2467
- name,
2468
- definition: definition as DiscoveredAB["definition"],
2469
- });
2470
- }
2471
- }
2472
-
2473
- return abs;
2474
- }
2475
-
2476
- async function loadABConfig(
2477
- ctx: DiscoveryContext,
2478
- agentDir: string
2479
- ): Promise<ABConfigFile | undefined> {
2480
- const path = await findModuleFile(agentDir, "ab.config");
2481
- if (path === undefined) {
2482
- return undefined;
2483
- }
2484
- try {
2485
- const mod = await ctx.importModule(path);
2486
- if (mod.default === null || typeof mod.default !== "object") {
2487
- ctx.diagnostics.push({
2488
- severity: "error",
2489
- path: displayPath(ctx, path),
2490
- message:
2491
- "ab.config.ts must default-export defineABConfig({ ... }) from @cursor/july/ab.",
2492
- });
2493
- return undefined;
2494
- }
2495
- return mod.default as ABConfigFile;
2496
- } catch (error) {
2497
- ctx.diagnostics.push({
2498
- severity: "error",
2499
- path: displayPath(ctx, path),
2500
- message: `Failed to load ab.config: ${error instanceof Error ? error.message : String(error)}`,
2501
- });
2502
- return undefined;
2503
- }
2504
- }
2505
-
2506
- // ============================================================================
2507
- // Storage (agent/storage.ts)
2508
- // ============================================================================
2509
-
2510
- async function loadStorage(
2511
- ctx: DiscoveryContext,
2512
- agentDir: string,
2513
- architecture: AgentArchitecture | undefined
2514
- ): Promise<StorageDefinition | undefined> {
2515
- const path = await findModuleFile(agentDir, "storage");
2516
- if (path === undefined) {
2517
- return undefined;
2518
- }
2519
- if (architecture === "v2") {
2520
- // Storage is a hosting-mode decision on v2, made by
2521
- // framework-storage-selection.ts: the state root when self-hosted,
2522
- // Cursor-managed storage on Cursor hosting. Fail early (error
2523
- // diagnostics refuse serve and deploy) rather than silently running
2524
- // with storage the author believes they configured. Not imported.
2525
- ctx.diagnostics.push({
2526
- severity: "error",
2527
- path: displayPath(ctx, path),
2528
- message:
2529
- 'agent/storage.ts is not supported on architecture "v2": the platform selects storage (the local state root when self-hosted; Cursor-managed storage on Cursor hosting). Delete the file.',
2530
- });
2531
- return undefined;
2532
- }
2533
- // v1 keeps loading the authored sink, with notice: defineStorage is going
2534
- // away with the v1 control plane.
2535
- ctx.diagnostics.push({
2536
- severity: "warning",
2537
- path: displayPath(ctx, path),
2538
- message:
2539
- 'agent/storage.ts (defineStorage) is deprecated and going away: architecture "v2" selects storage automatically (the local state root when self-hosted; Cursor-managed storage on Cursor hosting). Still honored on v1.',
2540
- });
2541
- const definition = await importDefault(ctx, path, "storage");
2542
- return definition as StorageDefinition | undefined;
2543
- }
2544
-
2545
- // ============================================================================
2546
- // OpenTelemetry (agent/otel.ts)
2547
- // ============================================================================
2548
-
2549
- async function loadOtel(
2550
- ctx: DiscoveryContext,
2551
- agentDir: string
2552
- ): Promise<OtelDefinition | undefined> {
2553
- const path = await findModuleFile(agentDir, "otel");
2554
- if (path === undefined) {
2555
- return undefined;
2556
- }
2557
- const definition = await importDefault(ctx, path, "otel");
2558
- return definition as OtelDefinition | undefined;
2559
- }
2560
-
2561
- // ============================================================================
2562
- // Artifacts (agent/artifacts.ts)
2563
- // ============================================================================
2564
-
2565
- async function loadArtifacts(
2566
- ctx: DiscoveryContext,
2567
- agentDir: string
2568
- ): Promise<ArtifactsDefinition | undefined> {
2569
- const path = await findModuleFile(agentDir, "artifacts");
2570
- if (path === undefined) {
2571
- return undefined;
2572
- }
2573
- const imported = await importDefault(ctx, path, "artifacts");
2574
- if (imported === undefined) {
2575
- return undefined;
2576
- }
2577
- const definition = imported as ArtifactsDefinition;
2578
- if (
2579
- definition.agentTool === true &&
2580
- Object.keys(definition.kinds ?? {}).length === 0
2581
- ) {
2582
- ctx.diagnostics.push({
2583
- severity: "error",
2584
- path: displayPath(ctx, path),
2585
- message:
2586
- "artifacts.agentTool requires kinds: the tag_artifact tool needs at least one declared kind; its descriptions are the tool prompt.",
2587
- });
2588
- }
2589
- return definition;
2590
- }
2591
-
2592
- /**
2593
- * Materialize the `tag_artifact` built-in when `defineArtifacts` set
2594
- * `agentTool: true`. Appended after `loadAgent` because the tool is
2595
- * generated from the artifacts kinds registry, not `AgentConfig.builtinTools`.
2596
- */
2597
- async function appendArtifactTool(
2598
- ctx: DiscoveryContext,
2599
- agentDir: string,
2600
- agent: ResolvedAgent,
2601
- artifacts: ArtifactsDefinition | undefined
2602
- ): Promise<void> {
2603
- const tool = artifactBuiltinTool(artifacts);
2604
- if (tool === undefined) {
2605
- return;
2606
- }
2607
- const artifactsPath = displayPath(
2608
- ctx,
2609
- (await findModuleFile(agentDir, "artifacts")) ??
2610
- join(agentDir, "artifacts.ts")
2611
- );
2612
- if (agent.tools.some((t) => t.name === TAG_ARTIFACT_TOOL_NAME)) {
2613
- ctx.diagnostics.push({
2614
- severity: "warning",
2615
- path: artifactsPath,
2616
- message: `Authored tool "${TAG_ARTIFACT_TOOL_NAME}" shadows the built-in tool of the same name; the authored tool wins.`,
2617
- });
2618
- return;
2619
- }
2620
- agent.tools.push(tool);
2621
- }
2622
-
2623
- function resolveABName(definition: unknown, fallback: string): string {
2624
- if (
2625
- typeof definition === "object" &&
2626
- definition !== null &&
2627
- "name" in definition &&
2628
- typeof (definition as { name: unknown }).name === "string" &&
2629
- (definition as { name: string }).name.length > 0
2630
- ) {
2631
- return (definition as { name: string }).name;
2632
- }
2633
- return fallback;
2634
- }
2635
-
2636
- // ============================================================================
2637
- // Sandbox seed files
2638
- // ============================================================================
2639
-
2640
- async function loadSeedFiles(
2641
- ctx: DiscoveryContext,
2642
- agentDir: string
2643
- ): Promise<SeedFile[]> {
2644
- const sandboxDir = join(agentDir, "sandbox");
2645
- for (const candidate of ["sandbox.ts", join("sandbox", "sandbox.ts")]) {
2646
- if (await isFile(join(agentDir, candidate))) {
2647
- ctx.diagnostics.push({
2648
- severity: "warning",
2649
- path: displayPath(ctx, join(agentDir, candidate)),
2650
- message:
2651
- "Custom sandbox backends are not supported; the agent runs in the session workspace directory. Ignoring.",
2652
- });
2653
- }
2654
- }
2655
- const workspaceDir = join(sandboxDir, "workspace");
2656
- if (!(await isDirectory(workspaceDir))) {
2657
- return [];
2658
- }
2659
- const seedFiles: SeedFile[] = [];
2660
- for (const filePath of await walkFiles(workspaceDir)) {
2661
- seedFiles.push({
2662
- relativePath: toPosix(relative(workspaceDir, filePath)),
2663
- sourcePath: filePath,
2664
- });
2665
- }
2666
- return seedFiles;
2667
- }
2668
-
2669
- // ============================================================================
2670
- // Info projection
2671
- // ============================================================================
2672
-
2673
- export function projectInfo(project: AgentProject): AgentProjectInfo {
2674
- const agent = project.agent;
2675
- const httpRoutes: AgentProjectInfo["channels"][number]["routes"] = [
2676
- { method: "POST", path: "/v1/session", url: "/v1/session" },
2677
- {
2678
- method: "POST",
2679
- path: "/v1/session/:sessionId",
2680
- url: "/v1/session/:sessionId",
2681
- },
2682
- {
2683
- method: "POST",
2684
- path: "/v1/session/:sessionId/stop",
2685
- url: "/v1/session/:sessionId/stop",
2686
- },
2687
- {
2688
- method: "GET",
2689
- path: "/v1/session/:sessionId/stream",
2690
- url: "/v1/session/:sessionId/stream",
2691
- },
2692
- {
2693
- method: "GET",
2694
- path: "/v1/session/:sessionId/approvals",
2695
- url: "/v1/session/:sessionId/approvals",
2696
- },
2697
- {
2698
- method: "POST",
2699
- path: "/v1/session/:sessionId/approvals/:callId",
2700
- url: "/v1/session/:sessionId/approvals/:callId",
2701
- },
2702
- ];
2703
- return {
2704
- name: project.name,
2705
- description: agent.description,
2706
- model: agent.model,
2707
- runtime: agent.runtime,
2708
- cloud:
2709
- agent.cloud === undefined ? undefined : toPublicCloudInfo(agent.cloud),
2710
- local: agent.local,
2711
- hosting: agent.hosting,
2712
- concurrency: agent.concurrency,
2713
- mcp: { path: MCP_ENDPOINT_PATH, tools: mcpEndpointToolNames(project) },
2714
- instructions:
2715
- agent.instructions === undefined
2716
- ? undefined
2717
- : {
2718
- chars: agent.instructions.length,
2719
- preview:
2720
- agent.instructions.length > 280
2721
- ? `${agent.instructions.slice(0, 277)}...`
2722
- : agent.instructions,
2723
- },
2724
- tools: agent.tools.map((tool) => ({
2725
- name: tool.name,
2726
- description: tool.description,
2727
- execution: tool.execution,
2728
- needsApproval: tool.needsApproval,
2729
- effect: tool.effect,
2730
- inputSchema: tool.inputSchema,
2731
- })),
2732
- skills: agent.skills.map((skill) => ({
2733
- name: skill.name,
2734
- description: skill.description,
2735
- })),
2736
- extensions: agent.extensions ?? [],
2737
- connections: agent.connections.map((connection) => ({
2738
- name: connection.name,
2739
- transport: connectionTransportLabel(connection.transport),
2740
- advertiseTools: connection.advertiseTools,
2741
- effects: connection.effects,
2742
- tools: connection.tools,
2743
- disallowedTools: connection.disallowedTools,
2744
- cursorAccountServers:
2745
- "cursorAccount" in connection.transport
2746
- ? (connection.transport.servers ?? "*")
2747
- : undefined,
2748
- })),
2749
- subagents: agent.subagents.map((subagent) => ({
2750
- name: subagent.name,
2751
- description: subagent.description,
2752
- model: subagent.model,
2753
- })),
2754
- channels: [
2755
- { id: "http", routes: httpRoutes },
2756
- ...project.channels.map((channel) => {
2757
- const info: AgentProjectInfo["channels"][number] = {
2758
- id: channel.id,
2759
- routes: (channel.definition.routes ?? []).map((r) => {
2760
- const routeInfo: AgentProjectInfo["channels"][number]["routes"][number] =
2761
- {
2762
- method: r.method,
2763
- path: r.path,
2764
- url: `/v1/channels/${channel.id}${r.path}`,
2765
- };
2766
- if (r.description !== undefined) {
2767
- routeInfo.description = r.description;
2768
- }
2769
- if (r.bodySchema !== undefined) {
2770
- routeInfo.bodySchema = toolInputJsonSchema(r.bodySchema);
2771
- }
2772
- if (r.querySchema !== undefined) {
2773
- routeInfo.querySchema = toolInputJsonSchema(r.querySchema);
2774
- }
2775
- return routeInfo;
2776
- }),
2777
- };
2778
- const meta = channel.definition.meta;
2779
- if (meta?.kind !== undefined) {
2780
- info.kind = meta.kind;
2781
- }
2782
- if (meta?.githubWebhookEvents !== undefined) {
2783
- info.githubWebhookEvents = meta.githubWebhookEvents;
2784
- }
2785
- if (meta?.originWebhookEvents !== undefined) {
2786
- info.originWebhookEvents = meta.originWebhookEvents;
2787
- }
2788
- if (meta?.deploymentEventTypes !== undefined) {
2789
- info.deploymentEventTypes = meta.deploymentEventTypes;
2790
- }
2791
- if (meta?.slackChannelPosts !== undefined) {
2792
- info.slackChannelPosts = meta.slackChannelPosts;
2793
- }
2794
- return info;
2795
- }),
2796
- ],
2797
- schedules: project.schedules.map((schedule) => ({
2798
- name: schedule.name,
2799
- cron: schedule.cron,
2800
- kind: schedule.kind,
2801
- })),
2802
- hooks: project.hooks.map((hook) => hook.name),
2803
- abs: project.abs.map((experiment) => experiment.name),
2804
- ...(project.abConfig === undefined && project.abs.length === 0
2805
- ? {}
2806
- : {
2807
- abConfig: {
2808
- maxPlaygroundSessions: resolveABMaxPlaygroundSessions(
2809
- project.abConfig?.maxPlaygroundSessions
2810
- ),
2811
- durableSamples: project.storage?.abs !== undefined,
2812
- durableSnapshots: project.storage?.abs?.putSnapshot !== undefined,
2813
- },
2814
- }),
2815
- storage:
2816
- project.storage === undefined
2817
- ? undefined
2818
- : { name: project.storage.name },
2819
- artifacts:
2820
- project.artifacts === undefined
2821
- ? undefined
2822
- : {
2823
- kinds: Object.keys(project.artifacts.kinds ?? {}),
2824
- agentTool: project.artifacts.agentTool === true,
2825
- max: resolveArtifactsMax(project.artifacts),
2826
- },
2827
- otel:
2828
- project.otel === undefined
2829
- ? undefined
2830
- : {
2831
- enabled: true as const,
2832
- serviceName: project.otel.serviceName,
2833
- },
2834
- diagnostics: project.diagnostics,
2835
- };
2836
- }
2837
-
2838
- // ============================================================================
2839
- // Shared helpers
2840
- // ============================================================================
2841
-
2842
- async function importDefault(
2843
- ctx: DiscoveryContext,
2844
- modulePath: string,
2845
- expectedKind:
2846
- | "agent"
2847
- | "tool"
2848
- | "skill"
2849
- | "extension"
2850
- | "connection"
2851
- | "schedule"
2852
- | "hook"
2853
- | "ab"
2854
- | "storage"
2855
- | "artifacts"
2856
- | "otel"
2857
- ): Promise<unknown> {
2858
- let mod: Record<string, unknown>;
2859
- try {
2860
- mod = await ctx.importModule(modulePath);
2861
- } catch (error) {
2862
- ctx.diagnostics.push({
2863
- severity: "error",
2864
- path: displayPath(ctx, modulePath),
2865
- message: `Failed to load module: ${errorMessage(error)}`,
2866
- });
2867
- return undefined;
2868
- }
2869
- const kind = getDefinitionKind(mod.default);
2870
- if (kind !== expectedKind) {
2871
- const helper = DEFINE_HELPER_NAME[expectedKind];
2872
- ctx.diagnostics.push({
2873
- severity: "error",
2874
- path: displayPath(ctx, modulePath),
2875
- message:
2876
- kind === undefined
2877
- ? `Module must default-export ${helper}({ ... }).`
2878
- : `Expected a ${expectedKind} definition but found "${kind}" (wrong authored slot?).`,
2879
- });
2880
- return undefined;
2881
- }
2882
- return mod.default;
2883
- }
2884
-
2885
- const DEFINE_HELPER_NAME: Record<
2886
- | "agent"
2887
- | "instructions"
2888
- | "tool"
2889
- | "skill"
2890
- | "extension"
2891
- | "connection"
2892
- | "channel"
2893
- | "http-channel"
2894
- | "schedule"
2895
- | "hook"
2896
- | "ab"
2897
- | "storage"
2898
- | "artifacts"
2899
- | "otel",
2900
- string
2901
- > = {
2902
- agent: "defineAgent",
2903
- instructions: "defineInstructions",
2904
- tool: "defineTool",
2905
- skill: "defineSkill",
2906
- extension: "defineExtension",
2907
- connection: "defineConnection",
2908
- channel: "defineChannel",
2909
- "http-channel": "httpChannel",
2910
- schedule: "defineSchedule",
2911
- hook: "defineHook",
2912
- ab: "defineAB",
2913
- storage: "defineStorage",
2914
- artifacts: "defineArtifacts",
2915
- otel: "defineOtel",
2916
- };
2917
-
2918
- async function warnUnknownSlots(
2919
- ctx: DiscoveryContext,
2920
- agentDir: string
2921
- ): Promise<void> {
2922
- const known = new Set([
2923
- "agent",
2924
- "instructions",
2925
- "tools",
2926
- "skills",
2927
- "extensions",
2928
- "mcp-connections",
2929
- "host-connections",
2930
- // Legacy slot name; resolveMcpConnectionsDir warns and still loads it.
2931
- "connections",
2932
- "subagents",
2933
- "channels",
2934
- "schedules",
2935
- "hooks",
2936
- "ab",
2937
- "sandbox",
2938
- "lib",
2939
- // Vite playground custom tool chips (agent/playground/tools).
2940
- "playground",
2941
- ]);
2942
- for (const entry of await sortedEntries(agentDir)) {
2943
- if (!entry.isDirectory() || entry.name.startsWith(".")) {
2944
- continue;
2945
- }
2946
- if (entry.name === "node_modules" || known.has(entry.name)) {
2947
- continue;
2948
- }
2949
- // Footgun: evals live at project-root `evals/`, not under `agent/`.
2950
- if (entry.name === "evals") {
2951
- ctx.diagnostics.push({
2952
- severity: "warning",
2953
- path: displayPath(ctx, join(agentDir, entry.name)),
2954
- message:
2955
- 'Directory "evals" under agent/ is ignored — put defineEval files at project-root evals/ instead.',
2956
- });
2957
- continue;
2958
- }
2959
- ctx.diagnostics.push({
2960
- severity: "warning",
2961
- path: displayPath(ctx, join(agentDir, entry.name)),
2962
- message: `Unrecognized directory "${entry.name}" under agent/ (not an authored slot; use lib/ for shared code).`,
2963
- });
2964
- }
2965
- }
2966
-
2967
- async function hasFlatLayoutMarkers(rootDir: string): Promise<boolean> {
2968
- if (await isFile(join(rootDir, "instructions.md"))) {
2969
- return true;
2970
- }
2971
- return (await findModuleFile(rootDir, "agent")) !== undefined;
2972
- }
2973
-
2974
- async function readPackageName(rootDir: string): Promise<string | undefined> {
2975
- const packageJsonPath = join(rootDir, "package.json");
2976
- if (!(await isFile(packageJsonPath))) {
2977
- return undefined;
2978
- }
2979
- try {
2980
- const parsed = JSON.parse(await readFile(packageJsonPath, "utf8")) as {
2981
- name?: unknown;
2982
- };
2983
- if (typeof parsed.name === "string" && parsed.name !== "") {
2984
- return parsed.name;
2985
- }
2986
- } catch {
2987
- // Malformed package.json falls back to the directory name.
2988
- }
2989
- return undefined;
2990
- }
2991
-
2992
- async function findModuleFile(
2993
- dir: string,
2994
- stem: string
2995
- ): Promise<string | undefined> {
2996
- for (const extension of [".ts", ".mts", ".js", ".mjs"]) {
2997
- const candidate = join(dir, `${stem}${extension}`);
2998
- if (await isFile(candidate)) {
2999
- return candidate;
3000
- }
3001
- }
3002
- return undefined;
3003
- }
3004
-
3005
- function isModuleFile(fileName: string): boolean {
3006
- if (fileName.endsWith(".d.ts") || fileName.startsWith(".")) {
3007
- return false;
3008
- }
3009
- if (!MODULE_EXTENSIONS.has(extname(fileName))) {
3010
- return false;
3011
- }
3012
- // Colocated unit tests (Vitest/Jest convention) are not definitions.
3013
- // Without this, `channels/slack.test.ts` is scanned as a channel named
3014
- // "slack.test", fails NAME_PATTERN, and the whole agent refuses to serve.
3015
- const stem = moduleStem(fileName);
3016
- return !stem.endsWith(".test") && !stem.endsWith(".spec");
3017
- }
3018
-
3019
- function moduleStem(fileName: string): string {
3020
- return fileName.slice(0, -extname(fileName).length);
3021
- }
3022
-
3023
- async function sortedEntries(dir: string): Promise<Dirent[]> {
3024
- const entries = await readdir(dir, { withFileTypes: true });
3025
- return entries.sort((a, b) => a.name.localeCompare(b.name));
3026
- }
3027
-
3028
- async function walkFiles(dir: string): Promise<string[]> {
3029
- const files: string[] = [];
3030
- for (const entry of await sortedEntries(dir)) {
3031
- if (entry.name.startsWith(".")) {
3032
- continue;
3033
- }
3034
- const entryPath = join(dir, entry.name);
3035
- if (entry.isDirectory()) {
3036
- files.push(...(await walkFiles(entryPath)));
3037
- } else if (entry.isFile()) {
3038
- files.push(entryPath);
3039
- }
3040
- }
3041
- return files;
3042
- }
3043
-
3044
- async function isDirectory(path: string): Promise<boolean> {
3045
- try {
3046
- return (await stat(path)).isDirectory();
3047
- } catch {
3048
- return false;
3049
- }
3050
- }
3051
-
3052
- async function isFile(path: string): Promise<boolean> {
3053
- try {
3054
- return (await stat(path)).isFile();
3055
- } catch {
3056
- return false;
3057
- }
3058
- }
3059
-
3060
- function isValidModelSetting(model: unknown): model is ModelSetting {
3061
- if (typeof model === "string") {
3062
- return model.trim() !== "";
3063
- }
3064
- return (
3065
- typeof model === "object" &&
3066
- model !== null &&
3067
- typeof (model as { id?: unknown }).id === "string" &&
3068
- (model as { id: string }).id.trim() !== ""
3069
- );
3070
- }
3071
-
3072
- function isBlank(value: unknown): boolean {
3073
- return typeof value !== "string" || value.trim() === "";
3074
- }
3075
-
3076
- function toPosix(path: string): string {
3077
- return path.split("\\").join("/");
3078
- }
3079
-
3080
- function displayPath(ctx: DiscoveryContext, absolutePath: string): string {
3081
- const rel = relative(ctx.rootDir, absolutePath);
3082
- return rel.startsWith("..") ? absolutePath : toPosix(rel);
3083
- }
3084
-
3085
- function errorMessage(error: unknown): string {
3086
- return error instanceof Error ? error.message : String(error);
3087
- }
7
+ *
8
+ * The loaders live under `./discovery/`, one module per authored slot;
9
+ * this module is the public surface the CLI, server, and tests import.
10
+ */
11
+
12
+ export { runtimeArchitectureCompatibilityError } from "./discovery/agent-config.js";
13
+ export type { LoadAgentProjectOptions } from "./discovery/context.js";
14
+ export { projectInfo } from "./discovery/info.js";
15
+ export {
16
+ type ChildAgentDir,
17
+ formatDiagnostics,
18
+ hasErrorDiagnostics,
19
+ isAgentProjectDir,
20
+ listChildAgentDirs,
21
+ loadAgentProject,
22
+ loadAgentProjects,
23
+ RESERVED_SLUGS,
24
+ resolveAgentMounts,
25
+ } from "./discovery/project.js";
26
+ export { deriveSkillDescription } from "./discovery/skills.js";