@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.
- package/README.md +27 -0
- package/dist/channels/slack/api.d.ts +1 -1
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +3 -1
- package/dist/channels/slack/assistant.d.ts +1 -1
- package/dist/channels/slack/assistant.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.d.ts +3 -2
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +28 -8
- package/dist/channels/slack/inbound.d.ts +8 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +28 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +2 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.BASO9Kih.js → app.CLA03RCz.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.FAqdWIXo.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DwkqxoE0.js → VPLocalSearchBox.ifxv-cJV.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CCil_13e.js → theme.DWCwM6p-.js} +2 -2
- package/dist/docs/assets/concepts.md.rPzW2HZN.js +1 -0
- package/dist/docs/assets/guides_cloud-agents.md.DumwWXba.js +9 -0
- package/dist/docs/assets/guides_cloud-agents.md.DumwWXba.lean.js +1 -0
- package/dist/docs/assets/{guides_cloud-runtime.md.CkYbjnAX.js → guides_cloud-runtime.md.BRirbRAN.js} +1 -1
- package/dist/docs/assets/index.md.Kiht7ell.js +5 -0
- package/dist/docs/assets/{index.md.Bb4k8kUm.lean.js → index.md.Kiht7ell.lean.js} +1 -1
- package/dist/docs/assets/reference_extensions.md.9WQMZe1l.js +56 -0
- package/dist/docs/assets/reference_extensions.md.9WQMZe1l.lean.js +1 -0
- package/dist/docs/assets/{reference_project-layout.md.Bv4KOtlB.js → reference_project-layout.md.CA6QP6Lw.js} +2 -2
- package/dist/docs/assets/{reference_project-layout.md.Bv4KOtlB.lean.js → reference_project-layout.md.CA6QP6Lw.lean.js} +1 -1
- package/dist/docs/assets/{reference_skills.md.BFW9retM.js → reference_skills.md.l2ezAcaN.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.CvAHsdSp.js → reference_tools.md.B1up2r9W.js} +1 -1
- package/dist/docs/assets/skills_framework-map.md.DDa4MeEW.js +1 -0
- package/dist/docs/assets/{skills_framework-map.md.BTi817yv.lean.js → skills_framework-map.md.DDa4MeEW.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +5 -5
- package/dist/docs/concepts.md +3 -3
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-agents.html +35 -0
- package/dist/docs/guides/cloud-agents.md +93 -0
- package/dist/docs/guides/cloud-runtime.html +6 -6
- package/dist/docs/guides/cloud-runtime.md +2 -0
- package/dist/docs/guides/convert-automation.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +4 -4
- package/dist/docs/guides/opentelemetry.html +5 -5
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/index.md +5 -1
- package/dist/docs/llms-full.txt +321 -4
- package/dist/docs/llms.txt +2 -0
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +5 -5
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/extensions.html +82 -0
- package/dist/docs/reference/extensions.md +200 -0
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +6 -6
- package/dist/docs/reference/project-layout.md +3 -0
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/skills.md +2 -0
- package/dist/docs/reference/subagents.html +5 -5
- package/dist/docs/reference/tools.html +6 -6
- package/dist/docs/reference/tools.md +2 -0
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/skills/ab.html +4 -4
- package/dist/docs/skills/create-agent.html +4 -4
- package/dist/docs/skills/debug.html +4 -4
- package/dist/docs/skills/evals.html +4 -4
- package/dist/docs/skills/framework-map.html +5 -5
- package/dist/docs/skills/framework-map.md +1 -0
- package/dist/docs/skills/github.html +4 -4
- package/dist/docs/skills/hillclimb.html +4 -4
- package/dist/docs/skills/index.html +4 -4
- package/dist/docs/skills/mcp-auth.html +4 -4
- package/dist/docs/skills/otel.html +4 -4
- package/dist/docs/skills/setup-slack.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +4 -4
- package/dist/docs/templates/agents-md.html +4 -4
- package/dist/docs/templates/code-wiki.html +4 -4
- package/dist/docs/templates/demo.html +4 -4
- package/dist/docs/templates/pr-autofixer.html +4 -4
- package/dist/docs/templates/security-help.html +4 -4
- package/dist/docs/templates/security-reviewer.html +4 -4
- package/dist/docs/templates/triage.html +4 -4
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/extensions/cursor-cloud-agents/extension.d.ts +9 -7
- package/dist/extensions/cursor-cloud-agents/extension.d.ts.map +1 -1
- package/dist/extensions/cursor-cloud-agents/extension.js +9 -7
- package/dist/extensions/cursor-cloud-agents/lib/backend.d.ts +78 -0
- package/dist/extensions/cursor-cloud-agents/lib/backend.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/lib/backend.js +286 -0
- package/dist/extensions/cursor-cloud-agents/lib/inputs.d.ts +5 -0
- package/dist/extensions/cursor-cloud-agents/lib/inputs.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/lib/inputs.js +4 -0
- package/dist/extensions/cursor-cloud-agents/lib/transcript.d.ts +8 -0
- package/dist/extensions/cursor-cloud-agents/lib/transcript.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/lib/transcript.js +15 -0
- package/dist/extensions/cursor-cloud-agents/skills/handoff.md +21 -7
- package/dist/extensions/cursor-cloud-agents/tools/cancel.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/cancel.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/cancel.js +26 -0
- package/dist/extensions/cursor-cloud-agents/tools/dump.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/dump.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/dump.js +54 -0
- package/dist/extensions/cursor-cloud-agents/tools/get.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/get.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/get.js +25 -0
- package/dist/extensions/cursor-cloud-agents/tools/launch.d.ts.map +1 -1
- package/dist/extensions/cursor-cloud-agents/tools/launch.js +14 -38
- package/dist/extensions/cursor-cloud-agents/tools/list.js +1 -1
- package/dist/extensions/cursor-cloud-agents/tools/reply.d.ts +4 -0
- package/dist/extensions/cursor-cloud-agents/tools/reply.d.ts.map +1 -0
- package/dist/extensions/cursor-cloud-agents/tools/{follow_up.js → reply.js} +12 -14
- package/dist/extensions.d.ts +38 -14
- package/dist/extensions.d.ts.map +1 -1
- package/dist/extensions.js +49 -12
- package/dist/files-backends/cursor-hosted.d.ts +0 -8
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +1 -11
- package/dist/files.d.ts +5 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +22 -18
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/conversation-mirror.d.ts.map +1 -1
- package/dist/internal/conversation-mirror.js +13 -15
- package/dist/internal/cursor/backend-client.d.ts +8 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +25 -9
- package/dist/internal/cursor/hosted-store-secrets.d.ts +11 -0
- package/dist/internal/cursor/hosted-store-secrets.d.ts.map +1 -1
- package/dist/internal/cursor/hosted-store-secrets.js +15 -0
- package/dist/internal/discovery/ab.d.ts +9 -0
- package/dist/internal/discovery/ab.d.ts.map +1 -0
- package/dist/internal/discovery/ab.js +101 -0
- package/dist/internal/discovery/agent-config.d.ts +35 -0
- package/dist/internal/discovery/agent-config.d.ts.map +1 -0
- package/dist/internal/discovery/agent-config.js +173 -0
- package/dist/internal/discovery/agent.d.ts +11 -0
- package/dist/internal/discovery/agent.d.ts.map +1 -0
- package/dist/internal/discovery/agent.js +306 -0
- package/dist/internal/discovery/channels.d.ts +10 -0
- package/dist/internal/discovery/channels.d.ts.map +1 -0
- package/dist/internal/discovery/channels.js +97 -0
- package/dist/internal/discovery/connections.d.ts +17 -0
- package/dist/internal/discovery/connections.d.ts.map +1 -0
- package/dist/internal/discovery/connections.js +436 -0
- package/dist/internal/discovery/context.d.ts +20 -0
- package/dist/internal/discovery/context.d.ts.map +1 -0
- package/dist/internal/discovery/context.js +30 -0
- package/dist/internal/discovery/cursor-plugin.d.ts +25 -0
- package/dist/internal/discovery/cursor-plugin.d.ts.map +1 -0
- package/dist/internal/discovery/cursor-plugin.js +570 -0
- package/dist/internal/discovery/extensions.d.ts +19 -0
- package/dist/internal/discovery/extensions.d.ts.map +1 -0
- package/dist/internal/discovery/extensions.js +238 -0
- package/dist/internal/discovery/fs.d.ts +15 -0
- package/dist/internal/discovery/fs.d.ts.map +1 -0
- package/dist/internal/discovery/fs.js +115 -0
- package/dist/internal/discovery/hooks.d.ts +7 -0
- package/dist/internal/discovery/hooks.d.ts.map +1 -0
- package/dist/internal/discovery/hooks.js +48 -0
- package/dist/internal/discovery/info.d.ts +6 -0
- package/dist/internal/discovery/info.d.ts.map +1 -0
- package/dist/internal/discovery/info.js +141 -0
- package/dist/internal/discovery/instructions.d.ts +7 -0
- package/dist/internal/discovery/instructions.d.ts.map +1 -0
- package/dist/internal/discovery/instructions.js +95 -0
- package/dist/internal/discovery/modules.d.ts +20 -0
- package/dist/internal/discovery/modules.d.ts.map +1 -0
- package/dist/internal/discovery/modules.js +101 -0
- package/dist/internal/discovery/names.d.ts +12 -0
- package/dist/internal/discovery/names.d.ts.map +1 -0
- package/dist/internal/discovery/names.js +36 -0
- package/dist/internal/discovery/project.d.ts +53 -0
- package/dist/internal/discovery/project.d.ts.map +1 -0
- package/dist/internal/discovery/project.js +239 -0
- package/dist/internal/discovery/runtime.d.ts +34 -0
- package/dist/internal/discovery/runtime.d.ts.map +1 -0
- package/dist/internal/discovery/runtime.js +98 -0
- package/dist/internal/discovery/schedules.d.ts +7 -0
- package/dist/internal/discovery/schedules.d.ts.map +1 -0
- package/dist/internal/discovery/schedules.js +97 -0
- package/dist/internal/discovery/seed-files.d.ts +7 -0
- package/dist/internal/discovery/seed-files.d.ts.map +1 -0
- package/dist/internal/discovery/seed-files.js +41 -0
- package/dist/internal/discovery/services.d.ts +17 -0
- package/dist/internal/discovery/services.d.ts.map +1 -0
- package/dist/internal/discovery/services.js +104 -0
- package/dist/internal/discovery/skills.d.ts +16 -0
- package/dist/internal/discovery/skills.d.ts.map +1 -0
- package/dist/internal/discovery/skills.js +151 -0
- package/dist/internal/discovery/subagent.d.ts +13 -0
- package/dist/internal/discovery/subagent.d.ts.map +1 -0
- package/dist/internal/discovery/subagent.js +19 -0
- package/dist/internal/discovery/tools.d.ts +18 -0
- package/dist/internal/discovery/tools.d.ts.map +1 -0
- package/dist/internal/discovery/tools.js +159 -0
- package/dist/internal/discovery.d.ts +8 -62
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +8 -2505
- package/dist/internal/framework-file-storage.js +6 -6
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/framework-storage-selection.js +8 -7
- package/dist/internal/frontmatter.d.ts +11 -3
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +29 -3
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +37 -20
- package/dist/internal/otel/collector.d.ts +13 -0
- package/dist/internal/otel/collector.d.ts.map +1 -1
- package/dist/internal/otel/collector.js +84 -17
- package/dist/internal/otel/http.d.ts.map +1 -1
- package/dist/internal/otel/http.js +2 -10
- package/dist/internal/otel/mcp.d.ts +16 -0
- package/dist/internal/otel/mcp.d.ts.map +1 -0
- package/dist/internal/otel/mcp.js +68 -0
- package/dist/internal/otel/propagation.d.ts +6 -0
- package/dist/internal/otel/propagation.d.ts.map +1 -0
- package/dist/internal/otel/propagation.js +14 -0
- package/dist/internal/otel/runtime.d.ts.map +1 -1
- package/dist/internal/otel/runtime.js +2 -0
- package/dist/internal/session-engine.d.ts +5 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +68 -32
- package/dist/internal/session-run-log.d.ts +2 -1
- package/dist/internal/session-run-log.d.ts.map +1 -1
- package/dist/internal/session-run-log.js +13 -14
- package/dist/internal/start-log-store.d.ts +1 -1
- package/dist/internal/start-log-store.d.ts.map +1 -1
- package/dist/internal/start-log-store.js +12 -14
- package/dist/internal/storage-coordinator.d.ts +4 -1
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +15 -13
- package/dist/internal/tool-catalog.d.ts.map +1 -1
- package/dist/internal/tool-catalog.js +9 -6
- package/dist/internal/turn-cost.d.ts +14 -0
- package/dist/internal/turn-cost.d.ts.map +1 -1
- package/dist/internal/turn-cost.js +41 -9
- package/dist/internal/v2-hosting.d.ts +22 -0
- package/dist/internal/v2-hosting.d.ts.map +1 -0
- package/dist/internal/v2-hosting.js +23 -0
- package/dist/otel.d.ts +16 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +26 -0
- package/dist/playground/assets/index-B6C9S9nG.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-protocol.d.ts +24 -23
- package/dist/storage-protocol.d.ts.map +1 -1
- package/dist/storage-protocol.js +68 -65
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/tools.d.ts +9 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +9 -0
- package/dist/types.d.ts +39 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +5 -1
- package/docs/concepts.md +3 -3
- package/docs/guides/cloud-agents.md +98 -0
- package/docs/guides/cloud-runtime.md +2 -0
- package/docs/reference/extensions.md +205 -0
- package/docs/reference/project-layout.md +3 -0
- package/docs/reference/skills.md +2 -0
- package/docs/reference/tools.md +2 -0
- package/package.json +1 -1
- package/skills/framework-map/SKILL.md +1 -0
- package/src/channels/slack/api.ts +4 -2
- package/src/channels/slack/assistant.ts +1 -1
- package/src/channels/slack/dispatch.ts +27 -2
- package/src/channels/slack/inbound.ts +37 -0
- package/src/channels/slack/manifest.ts +2 -1
- package/src/extensions/cursor-cloud-agents/extension.ts +9 -7
- package/src/extensions/cursor-cloud-agents/lib/backend.ts +395 -0
- package/src/extensions/cursor-cloud-agents/lib/inputs.ts +5 -0
- package/src/extensions/cursor-cloud-agents/lib/transcript.ts +23 -0
- package/src/extensions/cursor-cloud-agents/skills/handoff.md +21 -7
- package/src/extensions/cursor-cloud-agents/tools/cancel.ts +19 -0
- package/src/extensions/cursor-cloud-agents/tools/dump.ts +51 -0
- package/src/extensions/cursor-cloud-agents/tools/get.ts +18 -0
- package/src/extensions/cursor-cloud-agents/tools/launch.ts +22 -42
- package/src/extensions/cursor-cloud-agents/tools/list.ts +1 -1
- package/src/extensions/cursor-cloud-agents/tools/reply.ts +34 -0
- package/src/extensions.ts +55 -12
- package/src/files-backends/cursor-hosted.ts +1 -17
- package/src/files.ts +43 -34
- package/src/index.ts +10 -1
- package/src/internal/conversation-mirror.ts +23 -18
- package/src/internal/cursor/backend-client.ts +32 -10
- package/src/internal/cursor/hosted-store-secrets.ts +27 -0
- package/src/internal/discovery/ab.ts +111 -0
- package/src/internal/discovery/agent-config.ts +222 -0
- package/src/internal/discovery/agent.ts +360 -0
- package/src/internal/discovery/channels.ts +103 -0
- package/src/internal/discovery/connections.ts +540 -0
- package/src/internal/discovery/context.ts +46 -0
- package/src/internal/discovery/cursor-plugin.ts +708 -0
- package/src/internal/discovery/extensions.ts +348 -0
- package/src/internal/discovery/fs.ts +108 -0
- package/src/internal/discovery/hooks.ts +45 -0
- package/src/internal/discovery/info.ts +179 -0
- package/src/internal/discovery/instructions.ts +113 -0
- package/src/internal/discovery/modules.ts +107 -0
- package/src/internal/discovery/names.ts +44 -0
- package/src/internal/discovery/project.ts +270 -0
- package/src/internal/discovery/runtime.ts +159 -0
- package/src/internal/discovery/schedules.ts +94 -0
- package/src/internal/discovery/seed-files.ts +37 -0
- package/src/internal/discovery/services.ts +124 -0
- package/src/internal/discovery/skills.ts +176 -0
- package/src/internal/discovery/subagent.ts +29 -0
- package/src/internal/discovery/tools.ts +182 -0
- package/src/internal/discovery.ts +20 -3081
- package/src/internal/framework-file-storage.ts +6 -6
- package/src/internal/framework-storage-selection.ts +9 -9
- package/src/internal/frontmatter.ts +36 -3
- package/src/internal/mcp-host.ts +84 -31
- package/src/internal/otel/collector.ts +125 -25
- package/src/internal/otel/http.ts +2 -12
- package/src/internal/otel/mcp.ts +69 -0
- package/src/internal/otel/propagation.ts +17 -0
- package/src/internal/otel/runtime.ts +2 -0
- package/src/internal/session-engine.ts +96 -11
- package/src/internal/session-run-log.ts +17 -16
- package/src/internal/start-log-store.ts +13 -16
- package/src/internal/storage-coordinator.ts +19 -16
- package/src/internal/tool-catalog.ts +9 -6
- package/src/internal/turn-cost.ts +47 -4
- package/src/internal/v2-hosting.ts +38 -0
- package/src/otel.ts +38 -0
- package/src/storage-protocol.ts +101 -91
- package/src/storage.ts +4 -0
- package/src/tools.ts +18 -0
- package/src/types.ts +43 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.DbFXINq2.js +0 -1
- package/dist/docs/assets/concepts.md.lwAgBIMI.js +0 -1
- package/dist/docs/assets/index.md.Bb4k8kUm.js +0 -5
- package/dist/docs/assets/skills_framework-map.md.BTi817yv.js +0 -1
- package/dist/extensions/cursor-cloud-agents/lib/cloud.d.ts +0 -30
- package/dist/extensions/cursor-cloud-agents/lib/cloud.d.ts.map +0 -1
- package/dist/extensions/cursor-cloud-agents/lib/cloud.js +0 -45
- package/dist/extensions/cursor-cloud-agents/tools/follow_up.d.ts +0 -4
- package/dist/extensions/cursor-cloud-agents/tools/follow_up.d.ts.map +0 -1
- package/dist/extensions/cursor-cloud-agents/tools/status.d.ts +0 -4
- package/dist/extensions/cursor-cloud-agents/tools/status.d.ts.map +0 -1
- package/dist/extensions/cursor-cloud-agents/tools/status.js +0 -41
- package/dist/playground/assets/index-DBQCwFp0.css +0 -1
- package/src/extensions/cursor-cloud-agents/lib/cloud.ts +0 -79
- package/src/extensions/cursor-cloud-agents/tools/follow_up.ts +0 -29
- package/src/extensions/cursor-cloud-agents/tools/status.ts +0 -39
- /package/dist/docs/assets/{concepts.md.lwAgBIMI.lean.js → concepts.md.rPzW2HZN.lean.js} +0 -0
- /package/dist/docs/assets/{guides_cloud-runtime.md.CkYbjnAX.lean.js → guides_cloud-runtime.md.BRirbRAN.lean.js} +0 -0
- /package/dist/docs/assets/{reference_skills.md.BFW9retM.lean.js → reference_skills.md.l2ezAcaN.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.CvAHsdSp.lean.js → reference_tools.md.B1up2r9W.lean.js} +0 -0
- /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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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";
|