summer-engine 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +27 -5
- package/.codex-plugin/plugin.json +28 -6
- package/.cursor-plugin/plugin.json +27 -5
- package/.factory-plugin/plugin.json +27 -5
- package/.opencode/plugins/summer.js +1 -1
- package/CHANGELOG.md +102 -0
- package/README.md +18 -3
- package/assets/audit/scene_audit.gd +4136 -0
- package/assets/seeing/seeing_probe.gd +1873 -0
- package/dist/cli/commands/skills.js +20 -2
- package/dist/core/api-client.d.ts +9 -1
- package/dist/core/api-client.js +21 -4
- package/dist/core/capabilities/agent-playbook.js +46 -0
- package/dist/core/capabilities/audit/accept.d.ts +55 -0
- package/dist/core/capabilities/audit/accept.js +147 -0
- package/dist/core/capabilities/audit/args.d.ts +95 -0
- package/dist/core/capabilities/audit/args.js +89 -0
- package/dist/core/capabilities/audit/audit.d.ts +56 -0
- package/dist/core/capabilities/audit/audit.js +287 -0
- package/dist/core/capabilities/audit/frame.d.ts +14 -0
- package/dist/core/capabilities/audit/frame.js +35 -0
- package/dist/core/capabilities/audit/gaps.d.ts +96 -0
- package/dist/core/capabilities/audit/gaps.js +257 -0
- package/dist/core/capabilities/audit/judge.d.ts +162 -0
- package/dist/core/capabilities/audit/judge.js +1134 -0
- package/dist/core/capabilities/audit/math.d.ts +110 -0
- package/dist/core/capabilities/audit/math.js +263 -0
- package/dist/core/capabilities/audit/summary.d.ts +74 -0
- package/dist/core/capabilities/audit/summary.js +212 -0
- package/dist/core/capabilities/board.d.ts +31 -0
- package/dist/core/capabilities/board.js +43 -0
- package/dist/core/capabilities/camera-view.d.ts +4 -2
- package/dist/core/capabilities/camera-view.js +6 -3
- package/dist/core/capabilities/connect-signal.d.ts +85 -0
- package/dist/core/capabilities/connect-signal.js +285 -0
- package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
- package/dist/core/capabilities/engine-fallbacks.js +5 -0
- package/dist/core/capabilities/engine-ops.d.ts +5 -1
- package/dist/core/capabilities/engine-ops.js +0 -3
- package/dist/core/capabilities/engine-receipt.d.ts +12 -0
- package/dist/core/capabilities/engine-receipt.js +26 -0
- package/dist/core/capabilities/file-read.d.ts +64 -0
- package/dist/core/capabilities/file-read.js +325 -0
- package/dist/core/capabilities/game-task-plan.js +4 -2
- package/dist/core/capabilities/grep.d.ts +60 -0
- package/dist/core/capabilities/grep.js +190 -0
- package/dist/core/capabilities/inspect-node.d.ts +56 -0
- package/dist/core/capabilities/inspect-node.js +225 -0
- package/dist/core/capabilities/inspect-resource.d.ts +61 -0
- package/dist/core/capabilities/inspect-resource.js +376 -0
- package/dist/core/capabilities/math3d.d.ts +28 -0
- package/dist/core/capabilities/math3d.js +88 -0
- package/dist/core/capabilities/motion-generation.d.ts +62 -0
- package/dist/core/capabilities/motion-generation.js +138 -0
- package/dist/core/capabilities/placement-batch.d.ts +102 -0
- package/dist/core/capabilities/placement-batch.js +483 -0
- package/dist/core/capabilities/placement-math.d.ts +70 -0
- package/dist/core/capabilities/placement-math.js +209 -0
- package/dist/core/capabilities/placement-script.d.ts +42 -0
- package/dist/core/capabilities/placement-script.js +1096 -0
- package/dist/core/capabilities/placement.d.ts +485 -0
- package/dist/core/capabilities/placement.js +1731 -0
- package/dist/core/capabilities/project-context.d.ts +33 -2
- package/dist/core/capabilities/project-context.js +87 -4
- package/dist/core/capabilities/replace-node.d.ts +91 -0
- package/dist/core/capabilities/replace-node.js +423 -0
- package/dist/core/capabilities/runtime-control.d.ts +18 -2
- package/dist/core/capabilities/runtime-control.js +67 -2
- package/dist/core/capabilities/scene-batch.d.ts +63 -0
- package/dist/core/capabilities/scene-batch.js +394 -0
- package/dist/core/capabilities/scene-readback.d.ts +36 -0
- package/dist/core/capabilities/scene-readback.js +54 -0
- package/dist/core/capabilities/seeing/args.d.ts +348 -0
- package/dist/core/capabilities/seeing/args.js +178 -0
- package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
- package/dist/core/capabilities/seeing/candidates.js +282 -0
- package/dist/core/capabilities/seeing/math.d.ts +134 -0
- package/dist/core/capabilities/seeing/math.js +269 -0
- package/dist/core/capabilities/seeing/probe.d.ts +62 -0
- package/dist/core/capabilities/seeing/probe.js +188 -0
- package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
- package/dist/core/capabilities/seeing/scoring.js +639 -0
- package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
- package/dist/core/capabilities/seeing/seeing.js +1341 -0
- package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
- package/dist/core/capabilities/seeing/shot-store.js +204 -0
- package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
- package/dist/core/capabilities/surface-snap-mesh.js +425 -0
- package/dist/core/capabilities/surface-snap.d.ts +77 -0
- package/dist/core/capabilities/surface-snap.js +461 -0
- package/dist/core/capabilities/tool-dispatch.js +231 -72
- package/dist/core/capabilities/tscn.d.ts +85 -0
- package/dist/core/capabilities/tscn.js +391 -0
- package/dist/core/capabilities/world-snapshot.d.ts +58 -0
- package/dist/core/capabilities/world-snapshot.js +150 -0
- package/dist/core/capability-skew.d.ts +5 -5
- package/dist/core/capability-skew.js +14 -6
- package/dist/core/headless/worker-engine-client.d.ts +1 -1
- package/dist/core/headless/worker-engine-client.js +2 -2
- package/dist/core/library-read.d.ts +37 -5
- package/dist/core/library-read.js +264 -33
- package/dist/core/library-search.d.ts +2 -2
- package/dist/installer/agent-config.js +5 -3
- package/dist/installer/agent-table.d.ts +16 -0
- package/dist/installer/agent-table.js +52 -17
- package/dist/mcp/server.js +9 -3
- package/dist/mcp/tools/asset-tools.js +21 -3
- package/dist/mcp/tools/audit-tools.d.ts +23 -0
- package/dist/mcp/tools/audit-tools.js +60 -0
- package/dist/mcp/tools/debug-tools.js +3 -1
- package/dist/mcp/tools/file-tools.js +17 -4
- package/dist/mcp/tools/generate-tools.js +74 -66
- package/dist/mcp/tools/library-tools.js +2 -0
- package/dist/mcp/tools/perception-tools.js +6 -19
- package/dist/mcp/tools/placement-tools.d.ts +24 -0
- package/dist/mcp/tools/placement-tools.js +91 -0
- package/dist/mcp/tools/project-tools.js +18 -15
- package/dist/mcp/tools/scene-tools.js +71 -83
- package/dist/mcp/tools/seeing-tools.d.ts +24 -0
- package/dist/mcp/tools/seeing-tools.js +64 -0
- package/dist/mcp/tools/spatial-tools.js +17 -9
- package/dist/mcp/tools/visual-tools.js +59 -7
- package/gemini-extension.json +26 -4
- package/integrations/amp/README.md +1 -1
- package/integrations/crush/README.md +1 -1
- package/integrations/grok-build/README.md +1 -1
- package/integrations/hermes/README.md +1 -1
- package/integrations/kimi-code/README.md +1 -1
- package/integrations/mistral-vibe/README.md +1 -1
- package/integrations/opencode/README.md +1 -1
- package/integrations/rovo-dev/README.md +1 -1
- package/integrations/vscode-copilot/README.md +1 -1
- package/integrations/warp/README.md +1 -1
- package/integrations/windsurf/README.md +1 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/references/summer-folder/resource.yaml +1 -1
- package/library/references/summer-folder/summer-folder.md +1 -1
- package/library/skills/3d-lighting/SKILL.md +1 -1
- package/library/skills/README.md +36 -14
- package/library/skills/adaptive-music/SKILL.md +1 -1
- package/library/skills/agent-playtesting/SKILL.md +1 -1
- package/library/skills/ai-characters-in-games/SKILL.md +220 -0
- package/library/skills/ai-characters-in-games/resource.yaml +30 -0
- package/library/skills/ambient-bed/SKILL.md +1 -1
- package/library/skills/animated-loop/SKILL.md +1 -1
- package/library/skills/animation-tree/SKILL.md +1 -1
- package/library/skills/art-direction/SKILL.md +1 -1
- package/library/skills/asset-strategy/SKILL.md +2 -1
- package/library/skills/asset-strategy/resource.yaml +2 -1
- package/library/skills/audio-direction/SKILL.md +1 -1
- package/library/skills/auto-fire-targeting/SKILL.md +1 -1
- package/library/skills/brainstorm-game/SKILL.md +1 -1
- package/library/skills/brainstorming/SKILL.md +1 -1
- package/library/skills/browse-templates/SKILL.md +3 -3
- package/library/skills/browse-templates/resource.yaml +1 -1
- package/library/skills/camera-rigs/SKILL.md +128 -0
- package/library/skills/camera-rigs/resource.yaml +29 -0
- package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
- package/library/skills/character-animation-wiring/SKILL.md +1 -1
- package/library/skills/character-model/SKILL.md +1 -1
- package/library/skills/character-movement/SKILL.md +326 -0
- package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
- package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
- package/library/skills/character-movement/references/movement-3d.md +136 -0
- package/library/skills/character-movement/references/platformer-feel.md +83 -0
- package/library/skills/character-movement/resource.yaml +40 -0
- package/library/skills/character-portrait/SKILL.md +1 -1
- package/library/skills/cinematic-cutscene/SKILL.md +1 -1
- package/library/skills/combat-basics/SKILL.md +159 -0
- package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
- package/library/skills/combat-basics/resource.yaml +34 -0
- package/library/skills/concept-art/SKILL.md +1 -1
- package/library/skills/create-asset-sheet/SKILL.md +1 -1
- package/library/skills/debug/SKILL.md +1 -1
- package/library/skills/debugging-game-feel/SKILL.md +1 -1
- package/library/skills/design-level/SKILL.md +1 -1
- package/library/skills/design-mechanic/SKILL.md +1 -1
- package/library/skills/design-npc/SKILL.md +1 -1
- package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
- package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
- package/library/skills/environment-kit/SKILL.md +1 -1
- package/library/skills/export-and-ship/SKILL.md +1 -1
- package/library/skills/fabricating-assets/SKILL.md +1 -1
- package/library/skills/facial-and-lipsync/SKILL.md +1 -1
- package/library/skills/fps-controller/SKILL.md +9 -7
- package/library/skills/fps-controller/resource.yaml +4 -1
- package/library/skills/game-feel/SKILL.md +1 -1
- package/library/skills/gameskill/SKILL.md +1 -1
- package/library/skills/gdscript-patterns/SKILL.md +1 -1
- package/library/skills/generate-motion/SKILL.md +9 -9
- package/library/skills/generate-motion/resource.yaml +2 -1
- package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
- package/library/skills/headless-scripting/SKILL.md +1 -1
- package/library/skills/in-game-purchases/SKILL.md +254 -0
- package/library/skills/in-game-purchases/resource.yaml +29 -0
- package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
- package/library/skills/investigating-bugs/SKILL.md +1 -1
- package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
- package/library/skills/make-game/SKILL.md +25 -3
- package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
- package/library/skills/make-game/references/2d-platformer.md +117 -0
- package/library/skills/make-game/references/2d-rpg-content.md +234 -0
- package/library/skills/make-game/references/2d-rpg.md +241 -0
- package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
- package/library/skills/make-game/references/2d-survivors.md +237 -0
- package/library/skills/make-game/references/3d-fps-systems.md +216 -0
- package/library/skills/make-game/references/3d-fps.md +177 -0
- package/library/skills/make-game/references/3d-horror-systems.md +186 -0
- package/library/skills/make-game/references/3d-horror.md +201 -0
- package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
- package/library/skills/make-game/references/3d-third-person.md +215 -0
- package/library/skills/make-game/resource.yaml +6 -1
- package/library/skills/multiplayer/SKILL.md +150 -0
- package/library/skills/multiplayer/resource.yaml +33 -0
- package/library/skills/multiplayer-movement/SKILL.md +510 -0
- package/library/skills/multiplayer-movement/files.md +501 -0
- package/library/skills/multiplayer-movement/resource.yaml +30 -0
- package/library/skills/multiplayer-project/SKILL.md +521 -0
- package/library/skills/multiplayer-project/files.md +448 -0
- package/library/skills/multiplayer-project/resource.yaml +36 -0
- package/library/skills/multiplayer-publish/SKILL.md +229 -0
- package/library/skills/multiplayer-publish/export-errors.md +89 -0
- package/library/skills/multiplayer-publish/resource.yaml +28 -0
- package/library/skills/multiplayer-state/SKILL.md +438 -0
- package/library/skills/multiplayer-state/files.md +592 -0
- package/library/skills/multiplayer-state/resource.yaml +35 -0
- package/library/skills/multiplayer-testing/SKILL.md +236 -0
- package/library/skills/multiplayer-testing/resource.yaml +39 -0
- package/library/skills/music-track/SKILL.md +1 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/new-project/SKILL.md +1 -1
- package/library/skills/organic-model/SKILL.md +1 -1
- package/library/skills/pixel-art/SKILL.md +1 -1
- package/library/skills/play/SKILL.md +1 -1
- package/library/skills/playtesting-a-feature/SKILL.md +1 -1
- package/library/skills/procedural-animation/SKILL.md +2 -2
- package/library/skills/procedural-animation/resource.yaml +1 -1
- package/library/skills/prop-model/SKILL.md +1 -1
- package/library/skills/psx-retro-rendering/SKILL.md +1 -1
- package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
- package/library/skills/remote-deploy/SKILL.md +1 -1
- package/library/skills/retarget/SKILL.md +1 -1
- package/library/skills/running-in-the-cloud/SKILL.md +1 -1
- package/library/skills/save-load/SKILL.md +156 -0
- package/library/skills/save-load/resource.yaml +27 -0
- package/library/skills/scene-composition/SKILL.md +1 -1
- package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
- package/library/skills/scene-scripting/SKILL.md +1 -1
- package/library/skills/scene-to-level/SKILL.md +1 -1
- package/library/skills/skill-create/SKILL.md +1 -1
- package/library/skills/skill-improve/SKILL.md +1 -1
- package/library/skills/skill-test/SKILL.md +1 -1
- package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
- package/library/skills/skybox-panorama/SKILL.md +1 -1
- package/library/skills/sound-effect/SKILL.md +1 -1
- package/library/skills/spatial-placement/SKILL.md +134 -3
- package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
- package/library/skills/spatial-placement/resource.yaml +16 -2
- package/library/skills/sprite-sheet/SKILL.md +1 -1
- package/library/skills/summer-analytics/SKILL.md +147 -0
- package/library/skills/summer-analytics/resource.yaml +28 -0
- package/library/skills/summer-friends/SKILL.md +226 -0
- package/library/skills/summer-friends/resource.yaml +28 -0
- package/library/skills/summer-leaderboards/SKILL.md +231 -0
- package/library/skills/summer-leaderboards/resource.yaml +30 -0
- package/library/skills/summer-match-results/SKILL.md +286 -0
- package/library/skills/summer-match-results/resource.yaml +32 -0
- package/library/skills/summer-matchmaking/SKILL.md +301 -0
- package/library/skills/summer-matchmaking/resource.yaml +32 -0
- package/library/skills/summer-parties/SKILL.md +253 -0
- package/library/skills/summer-parties/resource.yaml +30 -0
- package/library/skills/summer-player-data/SKILL.md +322 -0
- package/library/skills/summer-player-data/resource.yaml +30 -0
- package/library/skills/summer-store/SKILL.md +271 -0
- package/library/skills/summer-store/resource.yaml +29 -0
- package/library/skills/summer-world-chat/SKILL.md +195 -0
- package/library/skills/summer-world-chat/resource.yaml +30 -0
- package/library/skills/summer-world-saves/SKILL.md +284 -0
- package/library/skills/summer-world-saves/resource.yaml +29 -0
- package/library/skills/text-to-motion/SKILL.md +99 -0
- package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
- package/library/skills/text-to-motion/resource.yaml +41 -0
- package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
- package/library/skills/tileable-texture/SKILL.md +1 -1
- package/library/skills/trailer-shot/SKILL.md +1 -1
- package/library/skills/tune-performance/SKILL.md +1 -1
- package/library/skills/ui-basics/SKILL.md +10 -1
- package/library/skills/ui-graphics/SKILL.md +1 -1
- package/library/skills/use-widget-asset/SKILL.md +1 -1
- package/library/skills/using-summer/SKILL.md +2 -2
- package/library/skills/using-summer/resource.yaml +2 -2
- package/library/skills/vehicle-model/SKILL.md +1 -1
- package/library/skills/verification-before-completion/SKILL.md +1 -1
- package/library/skills/verifying-scenes/SKILL.md +76 -2
- package/library/skills/verifying-scenes/resource.yaml +13 -2
- package/library/skills/vfx-dissolve/SKILL.md +1 -1
- package/library/skills/vfx-fire/SKILL.md +1 -1
- package/library/skills/vfx-hit-spark/SKILL.md +1 -1
- package/library/skills/vfx-lightning/SKILL.md +1 -1
- package/library/skills/vfx-magic-glow/SKILL.md +1 -1
- package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
- package/library/skills/vfx-smoke/SKILL.md +1 -1
- package/library/skills/vfx-water-ripple/SKILL.md +1 -1
- package/library/skills/voice-line/SKILL.md +1 -1
- package/library/skills/web-and-touch-input/SKILL.md +38 -0
- package/library/skills/web-and-touch-input/resource.yaml +28 -0
- package/library/skills/web-compatibility-look/SKILL.md +62 -0
- package/library/skills/web-compatibility-look/resource.yaml +30 -0
- package/library/skills/world-building-3d/SKILL.md +8 -1
- package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
- package/library/skills/world-building-3d/resource.yaml +2 -1
- package/library/skills/writing-plans/SKILL.md +1 -1
- package/library/skills/writing-skills/SKILL.md +1 -1
- package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
- package/library/tools/attach-to-surface/resource.yaml +142 -0
- package/library/tools/batch/resource.yaml +9 -3
- package/library/tools/connect-ports/resource.yaml +120 -0
- package/library/tools/connect-signal/resource.yaml +3 -2
- package/library/tools/debug-views/resource.yaml +94 -0
- package/library/tools/frame-nodes/resource.yaml +104 -0
- package/library/tools/frame-shot/resource.yaml +134 -0
- package/library/tools/generate-motion/resource.yaml +44 -9
- package/library/tools/get-board/resource.yaml +50 -0
- package/library/tools/get-project-context/resource.yaml +13 -2
- package/library/tools/grep/resource.yaml +77 -0
- package/library/tools/inspect-asset/resource.yaml +71 -0
- package/library/tools/inspect-node/resource.yaml +9 -2
- package/library/tools/inspect-resource/resource.yaml +16 -5
- package/library/tools/instantiate-scene/resource.yaml +27 -2
- package/library/tools/measure/resource.yaml +96 -0
- package/library/tools/place-adjacent/resource.yaml +113 -0
- package/library/tools/play/resource.yaml +15 -2
- package/library/tools/raycast/resource.yaml +101 -0
- package/library/tools/read-file/resource.yaml +36 -2
- package/library/tools/read-library/resource.yaml +2 -2
- package/library/tools/repeat-along/resource.yaml +129 -0
- package/library/tools/replace-node/resource.yaml +11 -5
- package/library/tools/scene-audit/resource.yaml +144 -0
- package/library/tools/screenshot/resource.yaml +13 -2
- package/library/tools/search-assets/resource.yaml +18 -0
- package/library/tools/shot-sheet/resource.yaml +110 -0
- package/library/tools/snap-to-surface/resource.yaml +4 -1
- package/library/tools/world-snapshot/resource.yaml +24 -3
- package/library/tools/zoom/resource.yaml +105 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +17 -3
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +26 -4
- package/registry/generated/index.json +2154 -469
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +27 -5
- package/registry/generated/plugin.codex.json +28 -6
- package/registry/generated/plugin.cursor.json +27 -5
- package/registry/generated/plugin.factory.json +27 -5
- package/registry/generated/skills-index.md +36 -14
- package/registry/generated/skills-registry.json +421 -124
- package/registry/generated/templates-registry.json +2 -2
- package/library/skills/host-authoritative-state/SKILL.md +0 -332
- package/library/skills/host-authoritative-state/resource.yaml +0 -28
- package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
- package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
- package/library/skills/setup-multiplayer/SKILL.md +0 -336
- package/library/skills/setup-multiplayer/resource.yaml +0 -28
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-read — ONE implementation of `summer_read_file` for both faces
|
|
3
|
+
* (src/mcp/tools/file-tools.ts and tool-dispatch.ts).
|
|
4
|
+
*
|
|
5
|
+
* The engine's state:read-file returns the first maxBytes of a file plus a
|
|
6
|
+
* sha256 over the WHOLE file; it has no offset (StateProvider::read_file_state).
|
|
7
|
+
* Paging and JSON filtering therefore happen here: the engine reads up to its
|
|
8
|
+
* 1 MB window once, and this module returns only the requested part — a line
|
|
9
|
+
* or byte window, or the entries of a .json picked by json_path / keys — so an
|
|
10
|
+
* agent can read one entry of a large JSON file without the host refusing
|
|
11
|
+
* the result. The sha256 stays the full-file receipt, so a guarded overwrite
|
|
12
|
+
* still works after a windowed read.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
/** The engine's own read cap (and the most this module ever asks for). */
|
|
16
|
+
export declare const FILE_READ_WINDOW_BYTES = 1000000;
|
|
17
|
+
export declare const DEFAULT_READ_MAX_BYTES = 200000;
|
|
18
|
+
export declare const readFileInputShape: {
|
|
19
|
+
path: z.ZodString;
|
|
20
|
+
max_bytes: z.ZodDefault<z.ZodNumber>;
|
|
21
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
unit: z.ZodOptional<z.ZodEnum<["lines", "bytes"]>>;
|
|
24
|
+
json_path: z.ZodOptional<z.ZodString>;
|
|
25
|
+
keys: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
26
|
+
keys_only: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
};
|
|
28
|
+
export declare const readFileInputSchema: z.ZodObject<{
|
|
29
|
+
path: z.ZodString;
|
|
30
|
+
max_bytes: z.ZodDefault<z.ZodNumber>;
|
|
31
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
32
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
unit: z.ZodOptional<z.ZodEnum<["lines", "bytes"]>>;
|
|
34
|
+
json_path: z.ZodOptional<z.ZodString>;
|
|
35
|
+
keys: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
36
|
+
keys_only: z.ZodOptional<z.ZodBoolean>;
|
|
37
|
+
}, "strict", z.ZodTypeAny, {
|
|
38
|
+
path: string;
|
|
39
|
+
max_bytes: number;
|
|
40
|
+
keys?: string[] | undefined;
|
|
41
|
+
limit?: number | undefined;
|
|
42
|
+
offset?: number | undefined;
|
|
43
|
+
unit?: "bytes" | "lines" | undefined;
|
|
44
|
+
json_path?: string | undefined;
|
|
45
|
+
keys_only?: boolean | undefined;
|
|
46
|
+
}, {
|
|
47
|
+
path: string;
|
|
48
|
+
keys?: string[] | undefined;
|
|
49
|
+
limit?: number | undefined;
|
|
50
|
+
offset?: number | undefined;
|
|
51
|
+
max_bytes?: number | undefined;
|
|
52
|
+
unit?: "bytes" | "lines" | undefined;
|
|
53
|
+
json_path?: string | undefined;
|
|
54
|
+
keys_only?: boolean | undefined;
|
|
55
|
+
}>;
|
|
56
|
+
export type ReadFileArgs = z.infer<typeof readFileInputSchema>;
|
|
57
|
+
export interface ReadFileClient {
|
|
58
|
+
readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
59
|
+
}
|
|
60
|
+
type JsonSegment = string | number;
|
|
61
|
+
/** Parse `a.b[0]["c d"]['e']` (optionally `$`-prefixed) into segments. */
|
|
62
|
+
export declare function parseJsonPath(path: string): JsonSegment[];
|
|
63
|
+
export declare function readProjectFileWindow(client: ReadFileClient, args: ReadFileArgs): Promise<unknown>;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-read — ONE implementation of `summer_read_file` for both faces
|
|
3
|
+
* (src/mcp/tools/file-tools.ts and tool-dispatch.ts).
|
|
4
|
+
*
|
|
5
|
+
* The engine's state:read-file returns the first maxBytes of a file plus a
|
|
6
|
+
* sha256 over the WHOLE file; it has no offset (StateProvider::read_file_state).
|
|
7
|
+
* Paging and JSON filtering therefore happen here: the engine reads up to its
|
|
8
|
+
* 1 MB window once, and this module returns only the requested part — a line
|
|
9
|
+
* or byte window, or the entries of a .json picked by json_path / keys — so an
|
|
10
|
+
* agent can read one entry of a large JSON file without the host refusing
|
|
11
|
+
* the result. The sha256 stays the full-file receipt, so a guarded overwrite
|
|
12
|
+
* still works after a windowed read.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { ToolInputError } from "../tool-errors.js";
|
|
16
|
+
import { asRecord } from "../util/json.js";
|
|
17
|
+
import { safeProjectPath } from "./engine-ops.js";
|
|
18
|
+
/** The engine's own read cap (and the most this module ever asks for). */
|
|
19
|
+
export const FILE_READ_WINDOW_BYTES = 1_000_000;
|
|
20
|
+
export const DEFAULT_READ_MAX_BYTES = 200_000;
|
|
21
|
+
// Mirrors library/tools/read-file/resource.yaml input_schema (parity-tested).
|
|
22
|
+
export const readFileInputShape = {
|
|
23
|
+
path: z.string().describe("Project path, e.g. res://scripts/player.gd"),
|
|
24
|
+
max_bytes: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.positive()
|
|
28
|
+
.max(FILE_READ_WINDOW_BYTES)
|
|
29
|
+
.default(DEFAULT_READ_MAX_BYTES)
|
|
30
|
+
.describe("Most bytes of content to return (UTF-8). Default 200000. A window or JSON selection is cut at this size too."),
|
|
31
|
+
offset: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.min(0)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Skip this many units (lines by default, see unit) before returning content. 0 = from the start."),
|
|
37
|
+
limit: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.positive()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Return at most this many units (lines by default). The result's window.next_offset continues the read."),
|
|
43
|
+
unit: z
|
|
44
|
+
.enum(["lines", "bytes"])
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("What offset/limit count: 'lines' (default) or 'bytes' (cut on UTF-8 character boundaries)."),
|
|
47
|
+
json_path: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("For a JSON file: return only this value, e.g. 'pieces.wall_a', 'pieces[\"a b\"]', 'items[3].size_m'."),
|
|
51
|
+
keys: z
|
|
52
|
+
.array(z.string())
|
|
53
|
+
.max(64)
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("For a JSON object (the file root or the json_path value): keep only these keys; * and ? wildcards, e.g. ['wall_*']."),
|
|
56
|
+
keys_only: z
|
|
57
|
+
.boolean()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("For a JSON object: return only its key names (after the keys filter), not the values — a cheap table of contents."),
|
|
60
|
+
};
|
|
61
|
+
export const readFileInputSchema = z.object(readFileInputShape).strict();
|
|
62
|
+
function failure(failure_reason, error, extra = {}) {
|
|
63
|
+
return { ok: false, failure_reason, error, ...extra };
|
|
64
|
+
}
|
|
65
|
+
/** Parse `a.b[0]["c d"]['e']` (optionally `$`-prefixed) into segments. */
|
|
66
|
+
export function parseJsonPath(path) {
|
|
67
|
+
let rest = path.trim();
|
|
68
|
+
if (rest.startsWith("$"))
|
|
69
|
+
rest = rest.slice(1);
|
|
70
|
+
const segments = [];
|
|
71
|
+
while (rest.length > 0) {
|
|
72
|
+
if (rest.startsWith(".")) {
|
|
73
|
+
rest = rest.slice(1);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (rest.startsWith("[")) {
|
|
77
|
+
const match = /^\[\s*(?:(-?\d+)|"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')\s*\]/.exec(rest);
|
|
78
|
+
if (!match) {
|
|
79
|
+
throw new ToolInputError(`Invalid json_path near "${rest.slice(0, 40)}": use dots and brackets, e.g. pieces.wall_01, pieces["a b"], items[3].`);
|
|
80
|
+
}
|
|
81
|
+
if (match[1] !== undefined)
|
|
82
|
+
segments.push(Number(match[1]));
|
|
83
|
+
else
|
|
84
|
+
segments.push((match[2] ?? match[3] ?? "").replace(/\\(.)/g, "$1"));
|
|
85
|
+
rest = rest.slice(match[0].length);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const match = /^[^.[\]]+/.exec(rest);
|
|
89
|
+
segments.push(match[0]);
|
|
90
|
+
rest = rest.slice(match[0].length);
|
|
91
|
+
}
|
|
92
|
+
return segments;
|
|
93
|
+
}
|
|
94
|
+
function globToRegExp(glob) {
|
|
95
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
96
|
+
return new RegExp(`^${escaped}$`);
|
|
97
|
+
}
|
|
98
|
+
function describeJsonType(value) {
|
|
99
|
+
if (Array.isArray(value))
|
|
100
|
+
return "array";
|
|
101
|
+
if (value === null)
|
|
102
|
+
return "null";
|
|
103
|
+
return typeof value;
|
|
104
|
+
}
|
|
105
|
+
function keysPreview(value) {
|
|
106
|
+
if (Array.isArray(value))
|
|
107
|
+
return { type: "array", length: value.length };
|
|
108
|
+
const record = asRecord(value);
|
|
109
|
+
if (record) {
|
|
110
|
+
const keys = Object.keys(record);
|
|
111
|
+
return { type: "object", total_keys: keys.length, keys: keys.slice(0, 60), ...(keys.length > 60 ? { keys_truncated: true } : {}) };
|
|
112
|
+
}
|
|
113
|
+
return { type: describeJsonType(value) };
|
|
114
|
+
}
|
|
115
|
+
function selectJson(root, args) {
|
|
116
|
+
let current = root;
|
|
117
|
+
const resolved = [];
|
|
118
|
+
for (const segment of args.json_path !== undefined ? parseJsonPath(args.json_path) : []) {
|
|
119
|
+
let next;
|
|
120
|
+
let found = false;
|
|
121
|
+
if (Array.isArray(current)) {
|
|
122
|
+
const index = typeof segment === "number" ? segment : /^-?\d+$/.test(segment) ? Number(segment) : Number.NaN;
|
|
123
|
+
const at = index < 0 ? current.length + index : index;
|
|
124
|
+
if (Number.isInteger(at) && at >= 0 && at < current.length) {
|
|
125
|
+
next = current[at];
|
|
126
|
+
found = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const record = asRecord(current);
|
|
131
|
+
const key = String(segment);
|
|
132
|
+
if (record && Object.prototype.hasOwnProperty.call(record, key)) {
|
|
133
|
+
next = record[key];
|
|
134
|
+
found = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!found) {
|
|
138
|
+
const at = resolved.length > 0 ? resolved.map(String).join(".") : "(root)";
|
|
139
|
+
return failure("json_path_not_found", `json_path ${args.json_path}: "${String(segment)}" does not exist under ${at}.`, { resolved: at, here: keysPreview(current) });
|
|
140
|
+
}
|
|
141
|
+
current = next;
|
|
142
|
+
resolved.push(segment);
|
|
143
|
+
}
|
|
144
|
+
const meta = { ...(args.json_path !== undefined ? { json_path: args.json_path } : {}), type: describeJsonType(current) };
|
|
145
|
+
const wantsKeys = args.keys !== undefined || args.keys_only === true;
|
|
146
|
+
if (!wantsKeys) {
|
|
147
|
+
if (Array.isArray(current))
|
|
148
|
+
meta.length = current.length;
|
|
149
|
+
else if (asRecord(current))
|
|
150
|
+
meta.total_keys = Object.keys(current).length;
|
|
151
|
+
return { value: current, meta };
|
|
152
|
+
}
|
|
153
|
+
const record = asRecord(current);
|
|
154
|
+
if (!record) {
|
|
155
|
+
return failure("not_an_object", `keys/keys_only apply to a JSON object, but ${args.json_path ?? "the file root"} is ${Array.isArray(current) ? `an array of ${current.length} items — select one with json_path, e.g. [0]` : `a ${describeJsonType(current)}`}.`);
|
|
156
|
+
}
|
|
157
|
+
const allKeys = Object.keys(record);
|
|
158
|
+
const patterns = (args.keys ?? []).map(globToRegExp);
|
|
159
|
+
const matched = patterns.length > 0 ? allKeys.filter((key) => patterns.some((pattern) => pattern.test(key))) : allKeys;
|
|
160
|
+
meta.total_keys = allKeys.length;
|
|
161
|
+
meta.matched_keys = matched.length;
|
|
162
|
+
if (args.keys !== undefined)
|
|
163
|
+
meta.keys = args.keys;
|
|
164
|
+
if (args.keys_only === true)
|
|
165
|
+
return { value: matched, meta: { ...meta, keys_only: true } };
|
|
166
|
+
const picked = {};
|
|
167
|
+
for (const key of matched)
|
|
168
|
+
picked[key] = record[key];
|
|
169
|
+
return { value: picked, meta };
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Windows
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
function utf8Length(text) {
|
|
175
|
+
return Buffer.byteLength(text, "utf8");
|
|
176
|
+
}
|
|
177
|
+
function lineWindow(text, offset, limit, maxBytes, sourceCut) {
|
|
178
|
+
const lines = text.split("\n");
|
|
179
|
+
// A trailing newline ends the last line; it does not start another one.
|
|
180
|
+
if (lines.length > 1 && lines[lines.length - 1] === "")
|
|
181
|
+
lines.pop();
|
|
182
|
+
const total = lines.length;
|
|
183
|
+
const wanted = lines.slice(offset, limit !== undefined ? offset + limit : undefined);
|
|
184
|
+
const kept = [];
|
|
185
|
+
let bytes = 0;
|
|
186
|
+
let cutByMaxBytes = false;
|
|
187
|
+
for (const line of wanted) {
|
|
188
|
+
const cost = utf8Length(line) + 1;
|
|
189
|
+
if (bytes + cost > maxBytes && kept.length > 0) {
|
|
190
|
+
cutByMaxBytes = true;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (bytes + cost > maxBytes) {
|
|
194
|
+
// One line alone is bigger than max_bytes: return its head.
|
|
195
|
+
const buffer = Buffer.from(line, "utf8");
|
|
196
|
+
let end = maxBytes;
|
|
197
|
+
while (end > 0 && (buffer[end] & 0xc0) === 0x80)
|
|
198
|
+
end--;
|
|
199
|
+
kept.push(buffer.subarray(0, end).toString("utf8"));
|
|
200
|
+
cutByMaxBytes = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
kept.push(line);
|
|
204
|
+
bytes += cost;
|
|
205
|
+
}
|
|
206
|
+
const returned = kept.length;
|
|
207
|
+
const nextOffset = offset + returned;
|
|
208
|
+
const eof = !sourceCut && !cutByMaxBytes && nextOffset >= total;
|
|
209
|
+
return {
|
|
210
|
+
content: kept.join("\n"),
|
|
211
|
+
complete: offset === 0 && eof,
|
|
212
|
+
meta: {
|
|
213
|
+
unit: "lines",
|
|
214
|
+
offset,
|
|
215
|
+
...(limit !== undefined ? { limit } : {}),
|
|
216
|
+
returned,
|
|
217
|
+
start_line: returned > 0 ? offset + 1 : null,
|
|
218
|
+
end_line: returned > 0 ? offset + returned : null,
|
|
219
|
+
total_lines: total,
|
|
220
|
+
...(sourceCut ? { total_lines_is_lower_bound: true } : {}),
|
|
221
|
+
next_offset: eof ? null : nextOffset,
|
|
222
|
+
eof,
|
|
223
|
+
...(cutByMaxBytes ? { cut_by_max_bytes: true } : {}),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function byteWindow(text, offset, limit, maxBytes, totalBytes, sourceCut) {
|
|
228
|
+
const buffer = Buffer.from(text, "utf8");
|
|
229
|
+
if (offset > 0 && offset >= buffer.length && sourceCut) {
|
|
230
|
+
return failure("offset_beyond_read_window", `offset ${offset} is past the engine's 1 MB read window for this file (${totalBytes} bytes). Only the first 1 MB can be read.`);
|
|
231
|
+
}
|
|
232
|
+
let start = Math.min(offset, buffer.length);
|
|
233
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80)
|
|
234
|
+
start++;
|
|
235
|
+
const span = Math.min(limit ?? maxBytes, maxBytes);
|
|
236
|
+
let end = Math.min(buffer.length, start + span);
|
|
237
|
+
while (end > start && end < buffer.length && (buffer[end] & 0xc0) === 0x80)
|
|
238
|
+
end--;
|
|
239
|
+
const cutByMaxBytes = limit !== undefined && limit > maxBytes && end < buffer.length;
|
|
240
|
+
const eof = end >= buffer.length && !sourceCut;
|
|
241
|
+
return {
|
|
242
|
+
content: buffer.subarray(start, end).toString("utf8"),
|
|
243
|
+
complete: start === 0 && eof,
|
|
244
|
+
meta: {
|
|
245
|
+
unit: "bytes",
|
|
246
|
+
offset,
|
|
247
|
+
...(limit !== undefined ? { limit } : {}),
|
|
248
|
+
start_byte: start,
|
|
249
|
+
end_byte: end,
|
|
250
|
+
returned: end - start,
|
|
251
|
+
total_bytes: totalBytes,
|
|
252
|
+
next_offset: eof ? null : end,
|
|
253
|
+
eof,
|
|
254
|
+
...(cutByMaxBytes ? { cut_by_max_bytes: true } : {}),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Entry point
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
export async function readProjectFileWindow(client, args) {
|
|
262
|
+
const path = safeProjectPath(args.path);
|
|
263
|
+
const maxBytes = args.max_bytes ?? DEFAULT_READ_MAX_BYTES;
|
|
264
|
+
const windowed = args.offset !== undefined || args.limit !== undefined || args.unit !== undefined;
|
|
265
|
+
const jsonMode = args.json_path !== undefined || args.keys !== undefined || args.keys_only === true;
|
|
266
|
+
if (args.json_path !== undefined)
|
|
267
|
+
parseJsonPath(args.json_path); // validate before reading
|
|
268
|
+
if (!windowed && !jsonMode)
|
|
269
|
+
return client.readProjectFile(path, maxBytes);
|
|
270
|
+
const unit = args.unit ?? "lines";
|
|
271
|
+
const offset = args.offset ?? 0;
|
|
272
|
+
// A byte window from the start of the file needs only that many bytes.
|
|
273
|
+
const engineBytes = !jsonMode && unit === "bytes" && args.limit !== undefined
|
|
274
|
+
? Math.min(FILE_READ_WINDOW_BYTES, offset + Math.min(args.limit, maxBytes) + 4)
|
|
275
|
+
: FILE_READ_WINDOW_BYTES;
|
|
276
|
+
const read = await client.readProjectFile(path, engineBytes);
|
|
277
|
+
const root = asRecord(read);
|
|
278
|
+
const data = asRecord(root?.data);
|
|
279
|
+
if (!root || root.ok === false || !data)
|
|
280
|
+
return read;
|
|
281
|
+
if (data.encoding === "binary" || typeof data.content !== "string") {
|
|
282
|
+
return failure("binary_file", `${path} is not a text file; offset/limit and json_path apply to text only.`);
|
|
283
|
+
}
|
|
284
|
+
const totalBytes = typeof data.size === "number" ? data.size : utf8Length(data.content);
|
|
285
|
+
const sourceCut = data.truncated === true;
|
|
286
|
+
let text = data.content;
|
|
287
|
+
let json;
|
|
288
|
+
if (jsonMode) {
|
|
289
|
+
if (sourceCut) {
|
|
290
|
+
return failure("file_too_large_for_json", `${path} is ${totalBytes} bytes, over the 1 MB read window, so it cannot be parsed as a whole. Page it with offset/limit instead.`);
|
|
291
|
+
}
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(text);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
return failure("not_json", `${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}); json_path/keys apply to JSON files only.`);
|
|
298
|
+
}
|
|
299
|
+
const selected = selectJson(parsed, args);
|
|
300
|
+
if ("ok" in selected && selected.ok === false)
|
|
301
|
+
return selected;
|
|
302
|
+
const { value, meta } = selected;
|
|
303
|
+
text = JSON.stringify(value, null, 2) ?? "null";
|
|
304
|
+
json = meta;
|
|
305
|
+
}
|
|
306
|
+
const window = unit === "bytes"
|
|
307
|
+
? byteWindow(text, offset, args.limit, maxBytes, jsonMode ? utf8Length(text) : totalBytes, sourceCut && !jsonMode)
|
|
308
|
+
: lineWindow(text, offset, args.limit, maxBytes, sourceCut && !jsonMode);
|
|
309
|
+
if ("ok" in window && window.ok === false)
|
|
310
|
+
return window;
|
|
311
|
+
const { content, complete, meta } = window;
|
|
312
|
+
const { content: _full, ...rest } = data;
|
|
313
|
+
void _full;
|
|
314
|
+
return {
|
|
315
|
+
...root,
|
|
316
|
+
data: {
|
|
317
|
+
...rest,
|
|
318
|
+
content,
|
|
319
|
+
// truncated: the returned content is not the whole file (or selection).
|
|
320
|
+
truncated: !complete,
|
|
321
|
+
window: meta,
|
|
322
|
+
...(json ? { json } : {}),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
@@ -291,8 +291,10 @@ function inferSkills(mode, target, goal) {
|
|
|
291
291
|
addSkill(routes, "gdscript-patterns", "Typed signal and state patterns for UI code.", 150);
|
|
292
292
|
}
|
|
293
293
|
if (target === "multiplayer") {
|
|
294
|
-
addSkill(routes, "
|
|
295
|
-
addSkill(routes, "
|
|
294
|
+
addSkill(routes, "multiplayer", "How Summer multiplayer works and which skill builds each piece.", 270);
|
|
295
|
+
addSkill(routes, "multiplayer-project", "Client and authority scenes, World, queue and Local Play.", 240);
|
|
296
|
+
addSkill(routes, "multiplayer-movement", "Client-side player movement checked by the authority.", 220);
|
|
297
|
+
addSkill(routes, "multiplayer-state", "Authority-owned state, Commands and Events.", 210);
|
|
296
298
|
}
|
|
297
299
|
if (includesAny(text, ["player controller", "fps", "first person", "wasd", "camera"])) {
|
|
298
300
|
addSkill(routes, "fps-controller", "Player controller, camera, collision, and input bindings.", 240);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grep — ONE implementation of `summer_grep` for both faces
|
|
3
|
+
* (src/mcp/tools/file-tools.ts and tool-dispatch.ts), over the engine's
|
|
4
|
+
* ripgrep-backed Grep op (modules/1summer_engine/editor/ops/search_ops.cpp).
|
|
5
|
+
*
|
|
6
|
+
* The engine op returns {file, line, content} per match and parses only
|
|
7
|
+
* ripgrep's "match" events, so its contextLines flag adds nothing to the
|
|
8
|
+
* result. Context lines are therefore cut here, from the matched files read
|
|
9
|
+
* back through state:read-file, under a file and size budget. Every line is
|
|
10
|
+
* clipped to max_line_chars so a minified file cannot flood the reply.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { type CapabilityAdvertisingClient } from "../capability-skew.js";
|
|
14
|
+
export declare const GREP_DEFAULT_MAX_RESULTS = 50;
|
|
15
|
+
export declare const GREP_MAX_RESULTS = 500;
|
|
16
|
+
export declare const GREP_DEFAULT_LINE_CHARS = 240;
|
|
17
|
+
export declare const grepInputShape: {
|
|
18
|
+
pattern: z.ZodString;
|
|
19
|
+
path: z.ZodOptional<z.ZodString>;
|
|
20
|
+
glob: z.ZodOptional<z.ZodString>;
|
|
21
|
+
case_sensitive: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
+
context_lines: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
max_results: z.ZodOptional<z.ZodNumber>;
|
|
24
|
+
max_line_chars: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
};
|
|
27
|
+
export declare const grepInputSchema: z.ZodObject<{
|
|
28
|
+
pattern: z.ZodString;
|
|
29
|
+
path: z.ZodOptional<z.ZodString>;
|
|
30
|
+
glob: z.ZodOptional<z.ZodString>;
|
|
31
|
+
case_sensitive: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
context_lines: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
max_results: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
max_line_chars: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
36
|
+
}, "strict", z.ZodTypeAny, {
|
|
37
|
+
pattern: string;
|
|
38
|
+
path?: string | undefined;
|
|
39
|
+
glob?: string | undefined;
|
|
40
|
+
case_sensitive?: boolean | undefined;
|
|
41
|
+
context_lines?: number | undefined;
|
|
42
|
+
max_results?: number | undefined;
|
|
43
|
+
max_line_chars?: number | undefined;
|
|
44
|
+
multiline?: boolean | undefined;
|
|
45
|
+
}, {
|
|
46
|
+
pattern: string;
|
|
47
|
+
path?: string | undefined;
|
|
48
|
+
glob?: string | undefined;
|
|
49
|
+
case_sensitive?: boolean | undefined;
|
|
50
|
+
context_lines?: number | undefined;
|
|
51
|
+
max_results?: number | undefined;
|
|
52
|
+
max_line_chars?: number | undefined;
|
|
53
|
+
multiline?: boolean | undefined;
|
|
54
|
+
}>;
|
|
55
|
+
export type GrepArgs = z.infer<typeof grepInputSchema>;
|
|
56
|
+
export interface GrepClient extends CapabilityAdvertisingClient {
|
|
57
|
+
executeOps(ops: Record<string, unknown>[], options?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
58
|
+
readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
59
|
+
}
|
|
60
|
+
export declare function grepProject(client: GrepClient, args: GrepArgs): Promise<unknown>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grep — ONE implementation of `summer_grep` for both faces
|
|
3
|
+
* (src/mcp/tools/file-tools.ts and tool-dispatch.ts), over the engine's
|
|
4
|
+
* ripgrep-backed Grep op (modules/1summer_engine/editor/ops/search_ops.cpp).
|
|
5
|
+
*
|
|
6
|
+
* The engine op returns {file, line, content} per match and parses only
|
|
7
|
+
* ripgrep's "match" events, so its contextLines flag adds nothing to the
|
|
8
|
+
* result. Context lines are therefore cut here, from the matched files read
|
|
9
|
+
* back through state:read-file, under a file and size budget. Every line is
|
|
10
|
+
* clipped to max_line_chars so a minified file cannot flood the reply.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { missingEngineOpResult } from "../capability-skew.js";
|
|
14
|
+
import { asRecord } from "../util/json.js";
|
|
15
|
+
import { safeProjectPath } from "./engine-ops.js";
|
|
16
|
+
import { extractOpError, withOldEngineHint } from "./engine-receipt.js";
|
|
17
|
+
import { GREP_FALLBACK } from "./engine-fallbacks.js";
|
|
18
|
+
export const GREP_DEFAULT_MAX_RESULTS = 50;
|
|
19
|
+
export const GREP_MAX_RESULTS = 500;
|
|
20
|
+
export const GREP_DEFAULT_LINE_CHARS = 240;
|
|
21
|
+
/** Distinct files read back for context lines. */
|
|
22
|
+
const CONTEXT_MAX_FILES = 25;
|
|
23
|
+
/** Soft cap on the whole reply's context text. */
|
|
24
|
+
const CONTEXT_BUDGET_CHARS = 60_000;
|
|
25
|
+
// Mirrors library/tools/grep/resource.yaml input_schema (parity-tested).
|
|
26
|
+
export const grepInputShape = {
|
|
27
|
+
pattern: z.string().min(1).describe("Ripgrep regular expression, e.g. '\"fits_into\"' or 'func _on_.*_pressed'"),
|
|
28
|
+
path: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Search scope: a res:// directory or file, e.g. 'res://kit/' or 'res://kit/kit_manifest.json'. Default: the whole project."),
|
|
32
|
+
glob: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("File filter passed to ripgrep --glob, e.g. '*.gd', '**/*.json', '!addons/**'. A glob also reaches files .gitignore would skip."),
|
|
36
|
+
case_sensitive: z.boolean().optional().describe("Match case exactly (default false: case-insensitive)."),
|
|
37
|
+
context_lines: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.min(0)
|
|
41
|
+
.max(10)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Lines of context before and after each match (0-10, default 0)."),
|
|
44
|
+
max_results: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.positive()
|
|
48
|
+
.max(GREP_MAX_RESULTS)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(`Most matches to return (default ${GREP_DEFAULT_MAX_RESULTS}, max ${GREP_MAX_RESULTS}). truncated:true means there were more.`),
|
|
51
|
+
max_line_chars: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.min(40)
|
|
55
|
+
.max(2000)
|
|
56
|
+
.optional()
|
|
57
|
+
.describe(`Clip every returned line to this many characters (default ${GREP_DEFAULT_LINE_CHARS}).`),
|
|
58
|
+
multiline: z.boolean().optional().describe("Let the pattern span lines (ripgrep -U --multiline-dotall)."),
|
|
59
|
+
};
|
|
60
|
+
export const grepInputSchema = z.object(grepInputShape).strict();
|
|
61
|
+
function clip(line, max) {
|
|
62
|
+
return line.length > max ? `${line.slice(0, max)}…[+${line.length - max} chars]` : line;
|
|
63
|
+
}
|
|
64
|
+
function grepPayload(result) {
|
|
65
|
+
const root = asRecord(result);
|
|
66
|
+
if (!root)
|
|
67
|
+
return null;
|
|
68
|
+
if (Array.isArray(root.matches))
|
|
69
|
+
return root;
|
|
70
|
+
const results = Array.isArray(root.results) ? root.results : [];
|
|
71
|
+
return asRecord(results.find((entry) => asRecord(entry)?.op === "Grep") ?? results[0]) ?? null;
|
|
72
|
+
}
|
|
73
|
+
/** Turn the engine's raw ripgrep failures into a classified result. */
|
|
74
|
+
function classifyGrepFailure(result, path) {
|
|
75
|
+
const message = extractOpError(result);
|
|
76
|
+
if (!message)
|
|
77
|
+
return result;
|
|
78
|
+
const lower = message.toLowerCase();
|
|
79
|
+
if (lower.includes("no such file") || lower.includes("os error 2") || lower.includes("cannot find the path")) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
failure_reason: "path_not_found",
|
|
83
|
+
error: `summer_grep: ${path ?? "the search path"} does not exist in the project. Check the res:// path (or drop path to search the whole project).`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (lower.includes("ripgrep") && (lower.includes("unavailable") || lower.includes("not found") || lower.includes("failed to start"))) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
failure_reason: "ripgrep_unavailable",
|
|
90
|
+
error: `summer_grep: this engine build has no usable ripgrep (${message}). ${GREP_FALLBACK}; or send the slower SearchInFiles op through summer_batch.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
export async function grepProject(client, args) {
|
|
96
|
+
const path = args.path !== undefined && args.path.trim() !== "" ? safeProjectPath(args.path) : undefined;
|
|
97
|
+
const maxResults = args.max_results ?? GREP_DEFAULT_MAX_RESULTS;
|
|
98
|
+
const lineChars = args.max_line_chars ?? GREP_DEFAULT_LINE_CHARS;
|
|
99
|
+
const contextLines = args.context_lines ?? 0;
|
|
100
|
+
const missing = missingEngineOpResult(client, "Grep", GREP_FALLBACK);
|
|
101
|
+
if (missing)
|
|
102
|
+
return { ...missing };
|
|
103
|
+
const op = { op: "Grep", pattern: args.pattern, maxResults };
|
|
104
|
+
if (path)
|
|
105
|
+
op.path = path;
|
|
106
|
+
if (args.glob)
|
|
107
|
+
op.glob = args.glob;
|
|
108
|
+
if (args.case_sensitive)
|
|
109
|
+
op.caseSensitive = true;
|
|
110
|
+
if (args.multiline)
|
|
111
|
+
op.multiline = true;
|
|
112
|
+
const result = withOldEngineHint(await client.executeOps([op]), "Grep", GREP_FALLBACK);
|
|
113
|
+
if (extractOpError(result))
|
|
114
|
+
return classifyGrepFailure(result, path);
|
|
115
|
+
const payload = grepPayload(result);
|
|
116
|
+
if (!payload || !Array.isArray(payload.matches))
|
|
117
|
+
return result;
|
|
118
|
+
const rawMatches = payload.matches
|
|
119
|
+
.map((m) => asRecord(m))
|
|
120
|
+
.filter((m) => !!m && typeof m.file === "string");
|
|
121
|
+
const matches = rawMatches.slice(0, maxResults).map((m) => ({
|
|
122
|
+
file: m.file,
|
|
123
|
+
line: typeof m.line === "number" ? m.line : Number(m.line ?? 0),
|
|
124
|
+
text: clip(String(m.content ?? ""), lineChars),
|
|
125
|
+
}));
|
|
126
|
+
const notes = [];
|
|
127
|
+
let contextTruncated = false;
|
|
128
|
+
if (contextLines > 0 && matches.length > 0) {
|
|
129
|
+
const files = [...new Set(matches.map((m) => String(m.file)))];
|
|
130
|
+
const readable = files.filter((f) => f.startsWith("res://")).slice(0, CONTEXT_MAX_FILES);
|
|
131
|
+
if (readable.length < files.length)
|
|
132
|
+
contextTruncated = true;
|
|
133
|
+
const linesByFile = new Map();
|
|
134
|
+
for (const file of readable) {
|
|
135
|
+
try {
|
|
136
|
+
const read = asRecord(await client.readProjectFile(file, 1_000_000));
|
|
137
|
+
const content = asRecord(read?.data)?.content;
|
|
138
|
+
if (read?.ok !== false && typeof content === "string")
|
|
139
|
+
linesByFile.set(file, content.replace(/\r\n?/g, "\n").split("\n"));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A file that cannot be read back keeps its match without context.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let budget = CONTEXT_BUDGET_CHARS;
|
|
146
|
+
for (const match of matches) {
|
|
147
|
+
const lines = linesByFile.get(String(match.file));
|
|
148
|
+
const line = Number(match.line);
|
|
149
|
+
if (!lines || !Number.isInteger(line) || line < 1 || line > lines.length)
|
|
150
|
+
continue;
|
|
151
|
+
if (budget <= 0) {
|
|
152
|
+
contextTruncated = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const before = lines.slice(Math.max(0, line - 1 - contextLines), line - 1).map((l) => clip(l, lineChars));
|
|
156
|
+
const after = lines.slice(line, line + contextLines).map((l) => clip(l, lineChars));
|
|
157
|
+
match.text = clip(lines[line - 1], lineChars);
|
|
158
|
+
if (before.length)
|
|
159
|
+
match.before = before;
|
|
160
|
+
if (after.length)
|
|
161
|
+
match.after = after;
|
|
162
|
+
budget -= [...before, ...after].reduce((sum, l) => sum + l.length + 1, 0);
|
|
163
|
+
}
|
|
164
|
+
if (contextTruncated) {
|
|
165
|
+
notes.push(`context lines were added for the first matches only (budget ${CONTEXT_MAX_FILES} files / ${CONTEXT_BUDGET_CHARS} chars); narrow path/glob for more`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const engineTruncated = payload.truncated === true || rawMatches.length > maxResults;
|
|
169
|
+
if (matches.length === 0 && !args.glob) {
|
|
170
|
+
notes.push("no matches: ripgrep skips files ignored by .gitignore/.ignore — pass a glob (e.g. '*.json') to search those too");
|
|
171
|
+
}
|
|
172
|
+
if (engineTruncated) {
|
|
173
|
+
notes.push(`stopped at ${maxResults} matches; narrow the pattern/path/glob or raise max_results`);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
pattern: args.pattern,
|
|
178
|
+
...(path ? { path } : {}),
|
|
179
|
+
...(args.glob ? { glob: args.glob } : {}),
|
|
180
|
+
returned: matches.length,
|
|
181
|
+
// ripgrep stops early once max_results is reached, so this counts what it saw.
|
|
182
|
+
matches_seen: typeof payload.totalMatches === "number" ? payload.totalMatches : rawMatches.length,
|
|
183
|
+
files_with_matches: new Set(rawMatches.map((m) => String(m.file))).size,
|
|
184
|
+
truncated: engineTruncated,
|
|
185
|
+
...(contextLines > 0 ? { context_lines: contextLines } : {}),
|
|
186
|
+
...(contextTruncated ? { context_truncated: true } : {}),
|
|
187
|
+
matches,
|
|
188
|
+
...(notes.length ? { notes } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|