@decibelsystems/tools 2.2.0-beta.0 → 3.1.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/CHANGELOG.md +226 -0
- package/dist/agentCommands.d.ts +0 -1
- package/dist/agentCommands.d.ts.map +1 -1
- package/dist/agentCommands.js +19 -8
- package/dist/agentCommands.js.map +1 -1
- package/dist/agentPresence.d.ts +82 -0
- package/dist/agentPresence.d.ts.map +1 -1
- package/dist/agentPresence.js +194 -26
- package/dist/agentPresence.js.map +1 -1
- package/dist/agentic/index.d.ts +18 -8
- package/dist/agentic/index.d.ts.map +1 -1
- package/dist/agentic/index.js +18 -144
- package/dist/agentic/index.js.map +1 -1
- package/dist/agentic/types.d.ts +106 -0
- package/dist/agentic/types.d.ts.map +1 -1
- package/dist/agentic/types.js +77 -1
- package/dist/agentic/types.js.map +1 -1
- package/dist/architectAdrs.d.ts +27 -3
- package/dist/architectAdrs.d.ts.map +1 -1
- package/dist/architectAdrs.js +92 -4
- package/dist/architectAdrs.js.map +1 -1
- package/dist/artifacts.d.ts +60 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +208 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/asks.d.ts +93 -0
- package/dist/asks.d.ts.map +1 -0
- package/dist/asks.js +383 -0
- package/dist/asks.js.map +1 -0
- package/dist/chat.d.ts +47 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +288 -0
- package/dist/chat.js.map +1 -0
- package/dist/client/transports.d.ts.map +1 -1
- package/dist/client/transports.js +13 -3
- package/dist/client/transports.js.map +1 -1
- package/dist/daemon.d.ts +33 -3
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +225 -52
- package/dist/daemon.js.map +1 -1
- package/dist/daemonConfig.d.ts +46 -0
- package/dist/daemonConfig.d.ts.map +1 -1
- package/dist/daemonConfig.js +57 -12
- package/dist/daemonConfig.js.map +1 -1
- package/dist/decibelPaths.d.ts.map +1 -1
- package/dist/decibelPaths.js +39 -6
- package/dist/decibelPaths.js.map +1 -1
- package/dist/domain/issue.d.ts +81 -0
- package/dist/domain/issue.d.ts.map +1 -0
- package/dist/domain/issue.js +106 -0
- package/dist/domain/issue.js.map +1 -0
- package/dist/domain/issueCodec.d.ts +39 -0
- package/dist/domain/issueCodec.d.ts.map +1 -0
- package/dist/domain/issueCodec.js +308 -0
- package/dist/domain/issueCodec.js.map +1 -0
- package/dist/domain/issueRepository.d.ts +124 -0
- package/dist/domain/issueRepository.d.ts.map +1 -0
- package/dist/domain/issueRepository.js +296 -0
- package/dist/domain/issueRepository.js.map +1 -0
- package/dist/domain/uid.d.ts +16 -0
- package/dist/domain/uid.d.ts.map +1 -0
- package/dist/domain/uid.js +43 -0
- package/dist/domain/uid.js.map +1 -0
- package/dist/facades/definitions.d.ts +0 -1
- package/dist/facades/definitions.d.ts.map +1 -1
- package/dist/facades/definitions.js +140 -116
- package/dist/facades/definitions.js.map +1 -1
- package/dist/facades/index.d.ts +2 -1
- package/dist/facades/index.d.ts.map +1 -1
- package/dist/facades/index.js +1 -1
- package/dist/facades/index.js.map +1 -1
- package/dist/facades/types.d.ts +17 -0
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/hq/defineAgent.d.ts.map +1 -1
- package/dist/hq/defineAgent.js +7 -11
- package/dist/hq/defineAgent.js.map +1 -1
- package/dist/httpArgs.d.ts +11 -0
- package/dist/httpArgs.d.ts.map +1 -0
- package/dist/httpArgs.js +39 -0
- package/dist/httpArgs.js.map +1 -0
- package/dist/httpServer.d.ts +1 -12
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +298 -144
- package/dist/httpServer.js.map +1 -1
- package/dist/identity.d.ts +123 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +312 -0
- package/dist/identity.js.map +1 -0
- package/dist/kernel.d.ts +46 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +334 -26
- package/dist/kernel.js.map +1 -1
- package/dist/killSwitch.d.ts +117 -0
- package/dist/killSwitch.d.ts.map +1 -0
- package/dist/killSwitch.js +161 -0
- package/dist/killSwitch.js.map +1 -0
- package/dist/lib/agent-services/assumptions.d.ts.map +1 -1
- package/dist/lib/agent-services/assumptions.js +8 -2
- package/dist/lib/agent-services/assumptions.js.map +1 -1
- package/dist/lib/atomicWrite.d.ts +49 -0
- package/dist/lib/atomicWrite.d.ts.map +1 -0
- package/dist/lib/atomicWrite.js +157 -0
- package/dist/lib/atomicWrite.js.map +1 -0
- package/dist/lib/envelope.d.ts +31 -0
- package/dist/lib/envelope.d.ts.map +1 -0
- package/dist/lib/envelope.js +80 -0
- package/dist/lib/envelope.js.map +1 -0
- package/dist/lib/fileLock.d.ts +38 -0
- package/dist/lib/fileLock.d.ts.map +1 -0
- package/dist/lib/fileLock.js +124 -0
- package/dist/lib/fileLock.js.map +1 -0
- package/dist/lib/recordIdAllocator.d.ts +102 -0
- package/dist/lib/recordIdAllocator.d.ts.map +1 -0
- package/dist/lib/recordIdAllocator.js +179 -0
- package/dist/lib/recordIdAllocator.js.map +1 -0
- package/dist/orgId.d.ts +23 -0
- package/dist/orgId.d.ts.map +1 -0
- package/dist/orgId.js +73 -0
- package/dist/orgId.js.map +1 -0
- package/dist/pairCli.d.ts +2 -0
- package/dist/pairCli.d.ts.map +1 -0
- package/dist/pairCli.js +97 -0
- package/dist/pairCli.js.map +1 -0
- package/dist/peerBroker.d.ts +40 -0
- package/dist/peerBroker.d.ts.map +1 -0
- package/dist/peerBroker.js +70 -0
- package/dist/peerBroker.js.map +1 -0
- package/dist/peerBrokerSupervisor.d.ts +21 -0
- package/dist/peerBrokerSupervisor.d.ts.map +1 -0
- package/dist/peerBrokerSupervisor.js +131 -0
- package/dist/peerBrokerSupervisor.js.map +1 -0
- package/dist/projectPaths.d.ts.map +1 -1
- package/dist/projectPaths.js +1 -19
- package/dist/projectPaths.js.map +1 -1
- package/dist/projectRegistry.d.ts +22 -0
- package/dist/projectRegistry.d.ts.map +1 -1
- package/dist/projectRegistry.js +69 -2
- package/dist/projectRegistry.js.map +1 -1
- package/dist/pty/boundary.d.ts +39 -0
- package/dist/pty/boundary.d.ts.map +1 -0
- package/dist/pty/boundary.js +100 -0
- package/dist/pty/boundary.js.map +1 -0
- package/dist/pty/errors.d.ts +6 -0
- package/dist/pty/errors.d.ts.map +1 -0
- package/dist/pty/errors.js +12 -0
- package/dist/pty/errors.js.map +1 -0
- package/dist/pty/fleet.d.ts +159 -0
- package/dist/pty/fleet.d.ts.map +1 -0
- package/dist/pty/fleet.js +543 -0
- package/dist/pty/fleet.js.map +1 -0
- package/dist/pty/launchProfile.d.ts +45 -0
- package/dist/pty/launchProfile.d.ts.map +1 -0
- package/dist/pty/launchProfile.js +149 -0
- package/dist/pty/launchProfile.js.map +1 -0
- package/dist/pty/openTerminal.d.ts +48 -0
- package/dist/pty/openTerminal.d.ts.map +1 -0
- package/dist/pty/openTerminal.js +185 -0
- package/dist/pty/openTerminal.js.map +1 -0
- package/dist/pty/pairDialog.d.ts +16 -0
- package/dist/pty/pairDialog.d.ts.map +1 -0
- package/dist/pty/pairDialog.js +121 -0
- package/dist/pty/pairDialog.js.map +1 -0
- package/dist/pty/pairing.d.ts +86 -0
- package/dist/pty/pairing.d.ts.map +1 -0
- package/dist/pty/pairing.js +351 -0
- package/dist/pty/pairing.js.map +1 -0
- package/dist/pty/routes.d.ts +49 -0
- package/dist/pty/routes.d.ts.map +1 -0
- package/dist/pty/routes.js +624 -0
- package/dist/pty/routes.js.map +1 -0
- package/dist/pty/sessions.d.ts +167 -0
- package/dist/pty/sessions.d.ts.map +1 -0
- package/dist/pty/sessions.js +513 -0
- package/dist/pty/sessions.js.map +1 -0
- package/dist/pty/tmux.d.ts +109 -0
- package/dist/pty/tmux.d.ts.map +1 -0
- package/dist/pty/tmux.js +271 -0
- package/dist/pty/tmux.js.map +1 -0
- package/dist/roster.d.ts +37 -0
- package/dist/roster.d.ts.map +1 -0
- package/dist/roster.js +126 -0
- package/dist/roster.js.map +1 -0
- package/dist/runtime/circuitBreaker.d.ts +95 -0
- package/dist/runtime/circuitBreaker.d.ts.map +1 -0
- package/dist/runtime/circuitBreaker.js +178 -0
- package/dist/runtime/circuitBreaker.js.map +1 -0
- package/dist/runtime/daemonMeta.d.ts +53 -0
- package/dist/runtime/daemonMeta.d.ts.map +1 -0
- package/dist/runtime/daemonMeta.js +121 -0
- package/dist/runtime/daemonMeta.js.map +1 -0
- package/dist/runtime/ensureRuntime.d.ts +103 -0
- package/dist/runtime/ensureRuntime.d.ts.map +1 -0
- package/dist/runtime/ensureRuntime.js +276 -0
- package/dist/runtime/ensureRuntime.js.map +1 -0
- package/dist/runtime/extensions.d.ts +57 -0
- package/dist/runtime/extensions.d.ts.map +1 -0
- package/dist/runtime/extensions.js +216 -0
- package/dist/runtime/extensions.js.map +1 -0
- package/dist/runtime/localHttp.d.ts +24 -0
- package/dist/runtime/localHttp.d.ts.map +1 -0
- package/dist/runtime/localHttp.js +84 -0
- package/dist/runtime/localHttp.js.map +1 -0
- package/dist/runtime/projectResolution.d.ts +29 -0
- package/dist/runtime/projectResolution.d.ts.map +1 -0
- package/dist/runtime/projectResolution.js +67 -0
- package/dist/runtime/projectResolution.js.map +1 -0
- package/dist/runtime/protocol.d.ts +62 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +74 -0
- package/dist/runtime/protocol.js.map +1 -0
- package/dist/sentinelIssues.d.ts +42 -2
- package/dist/sentinelIssues.d.ts.map +1 -1
- package/dist/sentinelIssues.js +167 -65
- package/dist/sentinelIssues.js.map +1 -1
- package/dist/server.js +123 -15
- package/dist/server.js.map +1 -1
- package/dist/toolConfig.d.ts.map +1 -1
- package/dist/toolConfig.js +12 -0
- package/dist/toolConfig.js.map +1 -1
- package/dist/tools/agentic/agentQueue.d.ts.map +1 -1
- package/dist/tools/agentic/agentQueue.js +4 -0
- package/dist/tools/agentic/agentQueue.js.map +1 -1
- package/dist/tools/agentic/index.d.ts.map +1 -1
- package/dist/tools/agentic/index.js +28 -2
- package/dist/tools/agentic/index.js.map +1 -1
- package/dist/tools/architect/index.d.ts +1 -0
- package/dist/tools/architect/index.d.ts.map +1 -1
- package/dist/tools/architect/index.js +52 -3
- package/dist/tools/architect/index.js.map +1 -1
- package/dist/tools/architect.d.ts.map +1 -1
- package/dist/tools/architect.js +59 -8
- package/dist/tools/architect.js.map +1 -1
- package/dist/tools/artifacts.d.ts +5 -0
- package/dist/tools/artifacts.d.ts.map +1 -0
- package/dist/tools/artifacts.js +89 -0
- package/dist/tools/artifacts.js.map +1 -0
- package/dist/tools/asks.d.ts +6 -0
- package/dist/tools/asks.d.ts.map +1 -0
- package/dist/tools/asks.js +104 -0
- package/dist/tools/asks.js.map +1 -0
- package/dist/tools/context.d.ts +10 -5
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +45 -18
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/coordinator/coordinator.d.ts.map +1 -1
- package/dist/tools/coordinator/coordinator.js +48 -19
- package/dist/tools/coordinator/coordinator.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +42 -2
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/crit.d.ts +2 -1
- package/dist/tools/crit.d.ts.map +1 -1
- package/dist/tools/crit.js +19 -4
- package/dist/tools/crit.js.map +1 -1
- package/dist/tools/data-inspector.d.ts +1 -1
- package/dist/tools/data-inspector.d.ts.map +1 -1
- package/dist/tools/data-inspector.js +1 -1
- package/dist/tools/data-inspector.js.map +1 -1
- package/dist/tools/designer/index.d.ts.map +1 -1
- package/dist/tools/designer/index.js +4 -3
- package/dist/tools/designer/index.js.map +1 -1
- package/dist/tools/designer/lateral-tools.d.ts.map +1 -1
- package/dist/tools/designer/lateral-tools.js +4 -2
- package/dist/tools/designer/lateral-tools.js.map +1 -1
- package/dist/tools/designer.d.ts +2 -1
- package/dist/tools/designer.d.ts.map +1 -1
- package/dist/tools/designer.js +24 -12
- package/dist/tools/designer.js.map +1 -1
- package/dist/tools/dojo/index.d.ts.map +1 -1
- package/dist/tools/dojo/index.js +52 -3
- package/dist/tools/dojo/index.js.map +1 -1
- package/dist/tools/dojo.d.ts +65 -3
- package/dist/tools/dojo.d.ts.map +1 -1
- package/dist/tools/dojo.js +148 -23
- package/dist/tools/dojo.js.map +1 -1
- package/dist/tools/friction.d.ts.map +1 -1
- package/dist/tools/friction.js +16 -6
- package/dist/tools/friction.js.map +1 -1
- package/dist/tools/guardian.d.ts +7 -0
- package/dist/tools/guardian.d.ts.map +1 -1
- package/dist/tools/guardian.js +152 -11
- package/dist/tools/guardian.js.map +1 -1
- package/dist/tools/hygiene/oracle-hygiene.d.ts.map +1 -1
- package/dist/tools/hygiene/oracle-hygiene.js +8 -2
- package/dist/tools/hygiene/oracle-hygiene.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +33 -18
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/killswitch/index.d.ts +13 -0
- package/dist/tools/killswitch/index.d.ts.map +1 -0
- package/dist/tools/killswitch/index.js +138 -0
- package/dist/tools/killswitch/index.js.map +1 -0
- package/dist/tools/learnings.d.ts +2 -1
- package/dist/tools/learnings.d.ts.map +1 -1
- package/dist/tools/learnings.js +31 -15
- package/dist/tools/learnings.js.map +1 -1
- package/dist/tools/oracle/index.d.ts.map +1 -1
- package/dist/tools/oracle/index.js +8 -0
- package/dist/tools/oracle/index.js.map +1 -1
- package/dist/tools/oracle.d.ts +7 -0
- package/dist/tools/oracle.d.ts.map +1 -1
- package/dist/tools/oracle.js +41 -12
- package/dist/tools/oracle.js.map +1 -1
- package/dist/tools/policy.d.ts +2 -1
- package/dist/tools/policy.d.ts.map +1 -1
- package/dist/tools/policy.js +28 -9
- package/dist/tools/policy.js.map +1 -1
- package/dist/tools/postoffice/index.d.ts +10 -0
- package/dist/tools/postoffice/index.d.ts.map +1 -0
- package/dist/tools/postoffice/index.js +218 -0
- package/dist/tools/postoffice/index.js.map +1 -0
- package/dist/tools/postoffice.d.ts +78 -0
- package/dist/tools/postoffice.d.ts.map +1 -0
- package/dist/tools/postoffice.js +199 -0
- package/dist/tools/postoffice.js.map +1 -0
- package/dist/tools/provenance.d.ts +18 -2
- package/dist/tools/provenance.d.ts.map +1 -1
- package/dist/tools/provenance.js +68 -40
- package/dist/tools/provenance.js.map +1 -1
- package/dist/tools/registry/index.d.ts.map +1 -1
- package/dist/tools/registry/index.js +24 -0
- package/dist/tools/registry/index.js.map +1 -1
- package/dist/tools/sentinel/index.d.ts +1 -0
- package/dist/tools/sentinel/index.d.ts.map +1 -1
- package/dist/tools/sentinel/index.js +142 -6
- package/dist/tools/sentinel/index.js.map +1 -1
- package/dist/tools/sentinel.d.ts +152 -1
- package/dist/tools/sentinel.d.ts.map +1 -1
- package/dist/tools/sentinel.js +609 -328
- package/dist/tools/sentinel.js.map +1 -1
- package/dist/tools/shared/index.d.ts +2 -1
- package/dist/tools/shared/index.d.ts.map +1 -1
- package/dist/tools/shared/index.js +2 -1
- package/dist/tools/shared/index.js.map +1 -1
- package/dist/tools/shared/response.d.ts +24 -0
- package/dist/tools/shared/response.d.ts.map +1 -1
- package/dist/tools/shared/response.js +61 -7
- package/dist/tools/shared/response.js.map +1 -1
- package/dist/tools/shared/runTracker.d.ts +2 -0
- package/dist/tools/shared/runTracker.d.ts.map +1 -1
- package/dist/tools/shared/runTracker.js +38 -0
- package/dist/tools/shared/runTracker.js.map +1 -1
- package/dist/tools/shared/storeRead.d.ts +153 -0
- package/dist/tools/shared/storeRead.d.ts.map +1 -0
- package/dist/tools/shared/storeRead.js +242 -0
- package/dist/tools/shared/storeRead.js.map +1 -0
- package/dist/tools/swarm.d.ts +3 -1
- package/dist/tools/swarm.d.ts.map +1 -1
- package/dist/tools/swarm.js +6 -12
- package/dist/tools/swarm.js.map +1 -1
- package/dist/tools/testSpec.d.ts +3 -2
- package/dist/tools/testSpec.d.ts.map +1 -1
- package/dist/tools/testSpec.js +51 -14
- package/dist/tools/testSpec.js.map +1 -1
- package/dist/tools/vector.d.ts +33 -1
- package/dist/tools/vector.d.ts.map +1 -1
- package/dist/tools/vector.js +31 -5
- package/dist/tools/vector.js.map +1 -1
- package/dist/tools/voice/index.d.ts +1 -0
- package/dist/tools/voice/index.d.ts.map +1 -1
- package/dist/tools/voice/index.js +33 -2
- package/dist/tools/voice/index.js.map +1 -1
- package/dist/tools/voice.d.ts +48 -1
- package/dist/tools/voice.d.ts.map +1 -1
- package/dist/tools/voice.js +63 -3
- package/dist/tools/voice.js.map +1 -1
- package/dist/tools/zoom/index.d.ts +15 -0
- package/dist/tools/zoom/index.d.ts.map +1 -0
- package/dist/tools/zoom/index.js +174 -0
- package/dist/tools/zoom/index.js.map +1 -0
- package/dist/tools/zoom.d.ts +234 -0
- package/dist/tools/zoom.d.ts.map +1 -0
- package/dist/tools/zoom.js +627 -0
- package/dist/tools/zoom.js.map +1 -0
- package/dist/transports/bridge.d.ts +1 -1
- package/dist/transports/bridge.d.ts.map +1 -1
- package/dist/transports/bridge.js +4 -1
- package/dist/transports/bridge.js.map +1 -1
- package/dist/transports/http.d.ts +1 -1
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +3 -1
- package/dist/transports/http.js.map +1 -1
- package/dist/transports/index.d.ts +1 -0
- package/dist/transports/index.d.ts.map +1 -1
- package/dist/transports/index.js +1 -0
- package/dist/transports/index.js.map +1 -1
- package/dist/transports/mcp.d.ts +1 -1
- package/dist/transports/mcp.d.ts.map +1 -1
- package/dist/transports/mcp.js +9 -4
- package/dist/transports/mcp.js.map +1 -1
- package/dist/transports/stdio.d.ts +1 -1
- package/dist/transports/stdio.d.ts.map +1 -1
- package/dist/transports/stdio.js +2 -0
- package/dist/transports/stdio.js.map +1 -1
- package/dist/transports/thinStdio.d.ts +22 -0
- package/dist/transports/thinStdio.d.ts.map +1 -0
- package/dist/transports/thinStdio.js +218 -0
- package/dist/transports/thinStdio.js.map +1 -0
- package/dist/transports/types.d.ts +8 -1
- package/dist/transports/types.d.ts.map +1 -1
- package/dist/types/agent-services.d.ts +7 -0
- package/dist/types/agent-services.d.ts.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +20 -4
- package/templates/AGENT.md +3 -0
- package/templates/CLAUDE-decibel.md +11 -0
- package/templates/com.decibel.daemon.plist +6 -1
- package/dist/tools/deck.d.ts +0 -11
- package/dist/tools/deck.d.ts.map +0 -1
- package/dist/tools/deck.js +0 -1656
- package/dist/tools/deck.js.map +0 -1
- package/dist/tools/mother.d.ts +0 -11
- package/dist/tools/mother.d.ts.map +0 -1
- package/dist/tools/mother.js +0 -503
- package/dist/tools/mother.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -3
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -189
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/senken.d.ts +0 -11
- package/dist/tools/senken.d.ts.map +0 -1
- package/dist/tools/senken.js +0 -482
- package/dist/tools/senken.js.map +0 -1
- package/dist/tools/terminal.d.ts +0 -17
- package/dist/tools/terminal.d.ts.map +0 -1
- package/dist/tools/terminal.js +0 -968
- package/dist/tools/terminal.js.map +0 -1
- package/dist/tools/toolsIndex.d.ts +0 -5
- package/dist/tools/toolsIndex.d.ts.map +0 -1
- package/dist/tools/toolsIndex.js +0 -37
- package/dist/tools/toolsIndex.js.map +0 -1
package/dist/tools/deck.js
DELETED
|
@@ -1,1656 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deck Tools - MTG Card Database (Supabase)
|
|
3
|
-
*
|
|
4
|
-
* Card catalog, price history, and market movers for Magic: The Gathering.
|
|
5
|
-
* Data lives in the Core Supabase project (dqftcixhsvcjtyxsxoao).
|
|
6
|
-
*
|
|
7
|
-
* Env var: DECK_SUPABASE_KEY (service role or anon key — tables have public SELECT policies)
|
|
8
|
-
*/
|
|
9
|
-
import { createClient } from '@supabase/supabase-js';
|
|
10
|
-
const CORE_SUPABASE_URL = 'https://dqftcixhsvcjtyxsxoao.supabase.co';
|
|
11
|
-
// Anon key for public read access (all deck tables have public SELECT RLS policies)
|
|
12
|
-
const CORE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRxZnRjaXhoc3ZjanR5eHN4b2FvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjcyMTgzMTYsImV4cCI6MjA4Mjc5NDMxNn0.erZq0zJMF-tATX7b0mofGsajFUvcqlZE5PQ4qcggmGc';
|
|
13
|
-
// Lazy-init client
|
|
14
|
-
let client = null;
|
|
15
|
-
function getClient() {
|
|
16
|
-
if (!client) {
|
|
17
|
-
const key = process.env.DECK_SUPABASE_KEY || CORE_ANON_KEY;
|
|
18
|
-
client = createClient(CORE_SUPABASE_URL, key, {
|
|
19
|
-
auth: { persistSession: false, autoRefreshToken: false },
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
return client;
|
|
23
|
-
}
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Helpers
|
|
26
|
-
// =============================================================================
|
|
27
|
-
function jsonResult(data, isError = false) {
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
30
|
-
isError,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Fallback to Scryfall API when a card isn't found in the local database.
|
|
35
|
-
* Returns results shaped like the Supabase cards table for consistency.
|
|
36
|
-
*/
|
|
37
|
-
async function scryfallSearch(name, setCode) {
|
|
38
|
-
let query = `name:${name}`;
|
|
39
|
-
if (setCode)
|
|
40
|
-
query += ` set:${setCode}`;
|
|
41
|
-
const url = `https://api.scryfall.com/cards/search?q=${encodeURIComponent(query)}&order=usd&dir=desc`;
|
|
42
|
-
const res = await fetch(url);
|
|
43
|
-
if (!res.ok)
|
|
44
|
-
return [];
|
|
45
|
-
const json = (await res.json());
|
|
46
|
-
if (!json.data?.length)
|
|
47
|
-
return [];
|
|
48
|
-
return json.data.slice(0, 50).map((card) => ({
|
|
49
|
-
name: card.name,
|
|
50
|
-
set_code: card.set,
|
|
51
|
-
set_name: card.set_name,
|
|
52
|
-
rarity: card.rarity,
|
|
53
|
-
price_usd: card.prices?.usd ?? null,
|
|
54
|
-
price_usd_foil: card.prices?.usd_foil ?? null,
|
|
55
|
-
legalities: card.legalities,
|
|
56
|
-
scryfall_id: card.id,
|
|
57
|
-
image_uri: card.image_uris?.normal ??
|
|
58
|
-
card.image_uris?.large ??
|
|
59
|
-
null,
|
|
60
|
-
type_line: card.type_line,
|
|
61
|
-
mana_cost: card.mana_cost,
|
|
62
|
-
source: 'scryfall',
|
|
63
|
-
}));
|
|
64
|
-
}
|
|
65
|
-
// =============================================================================
|
|
66
|
-
// Validation Helpers
|
|
67
|
-
// =============================================================================
|
|
68
|
-
const VALID_FORMATS = [
|
|
69
|
-
'standard', 'modern', 'pioneer', 'commander', 'legacy', 'vintage',
|
|
70
|
-
'pauper', 'historic', 'explorer', 'brawl', 'alchemy', 'penny', 'oathbreaker',
|
|
71
|
-
];
|
|
72
|
-
const VALID_RARITIES = ['common', 'uncommon', 'rare', 'mythic'];
|
|
73
|
-
function validateFormat(format) {
|
|
74
|
-
const f = format.toLowerCase();
|
|
75
|
-
if (!VALID_FORMATS.includes(f)) {
|
|
76
|
-
throw new Error(`Invalid format "${format}". Valid: ${VALID_FORMATS.join(', ')}`);
|
|
77
|
-
}
|
|
78
|
-
return f;
|
|
79
|
-
}
|
|
80
|
-
function validateRarity(rarity) {
|
|
81
|
-
const r = rarity.toLowerCase();
|
|
82
|
-
if (!VALID_RARITIES.includes(r)) {
|
|
83
|
-
throw new Error(`Invalid rarity "${rarity}". Valid: ${VALID_RARITIES.join(', ')}`);
|
|
84
|
-
}
|
|
85
|
-
return r;
|
|
86
|
-
}
|
|
87
|
-
/** Grade price stability from coefficient of variation */
|
|
88
|
-
function gradeStability(cv) {
|
|
89
|
-
if (cv < 5)
|
|
90
|
-
return { grade: 'A', label: 'Very Stable' };
|
|
91
|
-
if (cv < 10)
|
|
92
|
-
return { grade: 'B', label: 'Stable' };
|
|
93
|
-
if (cv < 20)
|
|
94
|
-
return { grade: 'C', label: 'Moderate' };
|
|
95
|
-
if (cv < 35)
|
|
96
|
-
return { grade: 'D', label: 'Volatile' };
|
|
97
|
-
return { grade: 'F', label: 'Highly Volatile' };
|
|
98
|
-
}
|
|
99
|
-
// =============================================================================
|
|
100
|
-
// Tool Specs
|
|
101
|
-
// =============================================================================
|
|
102
|
-
const deckSearch = {
|
|
103
|
-
definition: {
|
|
104
|
-
name: 'deck_search',
|
|
105
|
-
description: 'Search MTG cards by name. Returns card details with current prices.',
|
|
106
|
-
annotations: {
|
|
107
|
-
title: 'Search Cards',
|
|
108
|
-
readOnlyHint: true,
|
|
109
|
-
destructiveHint: false,
|
|
110
|
-
openWorldHint: true,
|
|
111
|
-
},
|
|
112
|
-
inputSchema: {
|
|
113
|
-
type: 'object',
|
|
114
|
-
properties: {
|
|
115
|
-
name: {
|
|
116
|
-
type: 'string',
|
|
117
|
-
description: 'Card name to search for (partial match, case-insensitive)',
|
|
118
|
-
},
|
|
119
|
-
set_code: {
|
|
120
|
-
type: 'string',
|
|
121
|
-
description: 'Optional: filter by set code (e.g. "MH3", "LCI")',
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
required: ['name'],
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
handler: async (args) => {
|
|
128
|
-
try {
|
|
129
|
-
const db = getClient();
|
|
130
|
-
let query = db
|
|
131
|
-
.from('cards')
|
|
132
|
-
.select('name, set_code, set_name, rarity, price_usd, price_usd_foil, legalities, scryfall_id, image_uri, type_line, mana_cost')
|
|
133
|
-
.ilike('name', `%${args.name}%`)
|
|
134
|
-
.order('price_usd', { ascending: false, nullsFirst: false })
|
|
135
|
-
.limit(50);
|
|
136
|
-
if (args.set_code) {
|
|
137
|
-
query = query.eq('set_code', args.set_code.toLowerCase());
|
|
138
|
-
}
|
|
139
|
-
const { data, error } = await query;
|
|
140
|
-
if (error)
|
|
141
|
-
throw new Error(error.message);
|
|
142
|
-
// Fallback to Scryfall API when local DB returns no results
|
|
143
|
-
if (!data || data.length === 0) {
|
|
144
|
-
const fallback = await scryfallSearch(args.name, args.set_code);
|
|
145
|
-
return jsonResult({
|
|
146
|
-
success: true,
|
|
147
|
-
cards: fallback,
|
|
148
|
-
count: fallback.length,
|
|
149
|
-
source: fallback.length > 0 ? 'scryfall_fallback' : 'none',
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return jsonResult({
|
|
153
|
-
success: true,
|
|
154
|
-
cards: data,
|
|
155
|
-
count: data.length,
|
|
156
|
-
source: 'database',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
161
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
const deckList = {
|
|
166
|
-
definition: {
|
|
167
|
-
name: 'deck_list',
|
|
168
|
-
description: 'List top price movers for a given period. Returns cards with the biggest price changes (gainers and losers).',
|
|
169
|
-
annotations: {
|
|
170
|
-
title: 'Top Movers',
|
|
171
|
-
readOnlyHint: true,
|
|
172
|
-
destructiveHint: false,
|
|
173
|
-
openWorldHint: true,
|
|
174
|
-
},
|
|
175
|
-
inputSchema: {
|
|
176
|
-
type: 'object',
|
|
177
|
-
properties: {
|
|
178
|
-
period: {
|
|
179
|
-
type: 'string',
|
|
180
|
-
description: 'Time period for price changes (default: "7d"). E.g. "1d", "7d", "30d"',
|
|
181
|
-
},
|
|
182
|
-
limit: {
|
|
183
|
-
type: 'number',
|
|
184
|
-
description: 'Max results (default: 50)',
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
required: [],
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
handler: async (args) => {
|
|
191
|
-
try {
|
|
192
|
-
const db = getClient();
|
|
193
|
-
const period = args.period || '7d';
|
|
194
|
-
const limit = args.limit || 50;
|
|
195
|
-
// Fetch movers — grab extra to cover abs() sorting
|
|
196
|
-
const { data: movers, error: moversErr } = await db
|
|
197
|
-
.from('price_movers')
|
|
198
|
-
.select('scryfall_id, period, price_from, price_to, change_pct, computed_at')
|
|
199
|
-
.eq('period', period)
|
|
200
|
-
.order('change_pct', { ascending: false })
|
|
201
|
-
.limit(limit * 2);
|
|
202
|
-
if (moversErr)
|
|
203
|
-
throw new Error(moversErr.message);
|
|
204
|
-
if (!movers || movers.length === 0) {
|
|
205
|
-
return jsonResult({ success: true, movers: [], count: 0, period });
|
|
206
|
-
}
|
|
207
|
-
// Sort by abs(change_pct), take top N
|
|
208
|
-
movers.sort((a, b) => Math.abs(b.change_pct) - Math.abs(a.change_pct));
|
|
209
|
-
const topMovers = movers.slice(0, limit);
|
|
210
|
-
// Batch-lookup card names
|
|
211
|
-
const ids = topMovers.map((m) => m.scryfall_id);
|
|
212
|
-
const { data: cards, error: cardsErr } = await db
|
|
213
|
-
.from('cards')
|
|
214
|
-
.select('scryfall_id, name, set_code')
|
|
215
|
-
.in('scryfall_id', ids);
|
|
216
|
-
if (cardsErr)
|
|
217
|
-
throw new Error(cardsErr.message);
|
|
218
|
-
const cardMap = new Map((cards || []).map((c) => [
|
|
219
|
-
c.scryfall_id,
|
|
220
|
-
c,
|
|
221
|
-
]));
|
|
222
|
-
const results = topMovers.map((m) => {
|
|
223
|
-
const card = cardMap.get(m.scryfall_id);
|
|
224
|
-
return {
|
|
225
|
-
name: card?.name || 'Unknown',
|
|
226
|
-
set_code: card?.set_code || '',
|
|
227
|
-
price_from: m.price_from,
|
|
228
|
-
price_to: m.price_to,
|
|
229
|
-
change_pct: m.change_pct,
|
|
230
|
-
computed_at: m.computed_at,
|
|
231
|
-
};
|
|
232
|
-
});
|
|
233
|
-
return jsonResult({
|
|
234
|
-
success: true,
|
|
235
|
-
movers: results,
|
|
236
|
-
count: results.length,
|
|
237
|
-
period,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
catch (error) {
|
|
241
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
242
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
const deckSummary = {
|
|
247
|
-
definition: {
|
|
248
|
-
name: 'deck_summary',
|
|
249
|
-
description: 'Get a summary of the card database: card count, price sources, last update time, available mover periods.',
|
|
250
|
-
annotations: {
|
|
251
|
-
title: 'Data Summary',
|
|
252
|
-
readOnlyHint: true,
|
|
253
|
-
destructiveHint: false,
|
|
254
|
-
openWorldHint: true,
|
|
255
|
-
},
|
|
256
|
-
inputSchema: {
|
|
257
|
-
type: 'object',
|
|
258
|
-
properties: {},
|
|
259
|
-
required: [],
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
handler: async () => {
|
|
263
|
-
try {
|
|
264
|
-
const db = getClient();
|
|
265
|
-
const [cardCount, sources, latestUpdate, periods] = await Promise.all([
|
|
266
|
-
db.from('cards').select('*', { count: 'exact', head: true }),
|
|
267
|
-
db.from('price_history').select('source').limit(1000),
|
|
268
|
-
db
|
|
269
|
-
.from('cards')
|
|
270
|
-
.select('price_updated_at')
|
|
271
|
-
.order('price_updated_at', { ascending: false })
|
|
272
|
-
.limit(1),
|
|
273
|
-
db.from('price_movers').select('period').limit(1000),
|
|
274
|
-
]);
|
|
275
|
-
const distinctSources = [
|
|
276
|
-
...new Set((sources.data || []).map((r) => r.source)),
|
|
277
|
-
];
|
|
278
|
-
const distinctPeriods = [
|
|
279
|
-
...new Set((periods.data || []).map((r) => r.period)),
|
|
280
|
-
];
|
|
281
|
-
return jsonResult({
|
|
282
|
-
success: true,
|
|
283
|
-
summary: {
|
|
284
|
-
card_count: cardCount.count || 0,
|
|
285
|
-
price_sources: distinctSources,
|
|
286
|
-
latest_price_update: latestUpdate.data?.[0]?.price_updated_at || null,
|
|
287
|
-
available_periods: distinctPeriods,
|
|
288
|
-
},
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
293
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
const deckHistory = {
|
|
298
|
-
definition: {
|
|
299
|
-
name: 'deck_history',
|
|
300
|
-
description: 'Get price history for a specific card. Accepts scryfall_id or card name. Returns up to 90 days of price data.',
|
|
301
|
-
annotations: {
|
|
302
|
-
title: 'Price History',
|
|
303
|
-
readOnlyHint: true,
|
|
304
|
-
destructiveHint: false,
|
|
305
|
-
openWorldHint: true,
|
|
306
|
-
},
|
|
307
|
-
inputSchema: {
|
|
308
|
-
type: 'object',
|
|
309
|
-
properties: {
|
|
310
|
-
scryfall_id: {
|
|
311
|
-
type: 'string',
|
|
312
|
-
description: 'Scryfall UUID of the card',
|
|
313
|
-
},
|
|
314
|
-
name: {
|
|
315
|
-
type: 'string',
|
|
316
|
-
description: 'Card name (exact match preferred; used if scryfall_id not provided)',
|
|
317
|
-
},
|
|
318
|
-
limit: {
|
|
319
|
-
type: 'number',
|
|
320
|
-
description: 'Max rows (default: 90 — ~3 months)',
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
required: [],
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
handler: async (args) => {
|
|
327
|
-
try {
|
|
328
|
-
if (!args.scryfall_id && !args.name) {
|
|
329
|
-
return jsonResult({ success: false, error: 'Provide either scryfall_id or name' }, true);
|
|
330
|
-
}
|
|
331
|
-
const db = getClient();
|
|
332
|
-
const limit = args.limit || 90;
|
|
333
|
-
let scryfallId = args.scryfall_id;
|
|
334
|
-
// Resolve name → scryfall_id if needed
|
|
335
|
-
if (!scryfallId && args.name) {
|
|
336
|
-
const { data: cards, error: cardErr } = await db
|
|
337
|
-
.from('cards')
|
|
338
|
-
.select('scryfall_id, name')
|
|
339
|
-
.ilike('name', args.name)
|
|
340
|
-
.limit(1);
|
|
341
|
-
if (cardErr)
|
|
342
|
-
throw new Error(cardErr.message);
|
|
343
|
-
if (!cards || cards.length === 0) {
|
|
344
|
-
return jsonResult({ success: false, error: `Card not found: ${args.name}` }, true);
|
|
345
|
-
}
|
|
346
|
-
scryfallId = cards[0]
|
|
347
|
-
.scryfall_id;
|
|
348
|
-
}
|
|
349
|
-
const { data, error } = await db
|
|
350
|
-
.from('price_history')
|
|
351
|
-
.select('date, price_usd, price_usd_foil, source')
|
|
352
|
-
.eq('scryfall_id', scryfallId)
|
|
353
|
-
.order('date', { ascending: false })
|
|
354
|
-
.limit(limit);
|
|
355
|
-
if (error)
|
|
356
|
-
throw new Error(error.message);
|
|
357
|
-
return jsonResult({
|
|
358
|
-
success: true,
|
|
359
|
-
scryfall_id: scryfallId,
|
|
360
|
-
history: data,
|
|
361
|
-
count: data?.length || 0,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
366
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
// =============================================================================
|
|
371
|
-
// Tool 5: Format Deals
|
|
372
|
-
// =============================================================================
|
|
373
|
-
const deckFormatDeals = {
|
|
374
|
-
definition: {
|
|
375
|
-
name: 'deck_format_deals',
|
|
376
|
-
description: 'Find underpriced cards legal in a specific format. Great for budget deck building.',
|
|
377
|
-
annotations: {
|
|
378
|
-
title: 'Format Deals',
|
|
379
|
-
readOnlyHint: true,
|
|
380
|
-
destructiveHint: false,
|
|
381
|
-
openWorldHint: true,
|
|
382
|
-
},
|
|
383
|
-
inputSchema: {
|
|
384
|
-
type: 'object',
|
|
385
|
-
properties: {
|
|
386
|
-
format: {
|
|
387
|
-
type: 'string',
|
|
388
|
-
description: `Format to check legality (${VALID_FORMATS.join(', ')})`,
|
|
389
|
-
},
|
|
390
|
-
max_price: {
|
|
391
|
-
type: 'number',
|
|
392
|
-
description: 'Maximum price in USD (default: no limit)',
|
|
393
|
-
},
|
|
394
|
-
min_price: {
|
|
395
|
-
type: 'number',
|
|
396
|
-
description: 'Minimum price in USD (default: 0.01 to skip bulk)',
|
|
397
|
-
},
|
|
398
|
-
rarity: {
|
|
399
|
-
type: 'string',
|
|
400
|
-
description: 'Filter by rarity (common, uncommon, rare, mythic)',
|
|
401
|
-
},
|
|
402
|
-
sort_by: {
|
|
403
|
-
type: 'string',
|
|
404
|
-
description: 'Sort order: price_asc (default) or price_desc',
|
|
405
|
-
},
|
|
406
|
-
limit: {
|
|
407
|
-
type: 'number',
|
|
408
|
-
description: 'Max results (default: 50, max: 200)',
|
|
409
|
-
},
|
|
410
|
-
},
|
|
411
|
-
required: ['format'],
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
handler: async (args) => {
|
|
415
|
-
try {
|
|
416
|
-
const format = validateFormat(args.format);
|
|
417
|
-
const minPrice = args.min_price ?? 0.01;
|
|
418
|
-
const limit = Math.min(args.limit || 50, 200);
|
|
419
|
-
const ascending = (args.sort_by || 'price_asc') === 'price_asc';
|
|
420
|
-
const db = getClient();
|
|
421
|
-
let query = db
|
|
422
|
-
.from('cards')
|
|
423
|
-
.select('name, set_code, set_name, rarity, price_usd, type_line, mana_cost, scryfall_id')
|
|
424
|
-
.filter(`legalities->>${format}`, 'eq', 'legal')
|
|
425
|
-
.gte('price_usd', minPrice)
|
|
426
|
-
.order('price_usd', { ascending, nullsFirst: false })
|
|
427
|
-
.limit(limit);
|
|
428
|
-
if (args.max_price != null) {
|
|
429
|
-
query = query.lte('price_usd', args.max_price);
|
|
430
|
-
}
|
|
431
|
-
if (args.rarity) {
|
|
432
|
-
query = query.eq('rarity', validateRarity(args.rarity));
|
|
433
|
-
}
|
|
434
|
-
const { data, error } = await query;
|
|
435
|
-
if (error)
|
|
436
|
-
throw new Error(error.message);
|
|
437
|
-
return jsonResult({
|
|
438
|
-
success: true,
|
|
439
|
-
format,
|
|
440
|
-
cards: data,
|
|
441
|
-
count: data?.length || 0,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
446
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
447
|
-
}
|
|
448
|
-
},
|
|
449
|
-
};
|
|
450
|
-
// =============================================================================
|
|
451
|
-
// Tool 6: Type Search
|
|
452
|
-
// =============================================================================
|
|
453
|
-
const deckTypeSearch = {
|
|
454
|
-
definition: {
|
|
455
|
-
name: 'deck_type_search',
|
|
456
|
-
description: 'Search cards by type line for tribal/archetype building (e.g. Elf, Planeswalker, Equipment).',
|
|
457
|
-
annotations: {
|
|
458
|
-
title: 'Type Search',
|
|
459
|
-
readOnlyHint: true,
|
|
460
|
-
destructiveHint: false,
|
|
461
|
-
openWorldHint: true,
|
|
462
|
-
},
|
|
463
|
-
inputSchema: {
|
|
464
|
-
type: 'object',
|
|
465
|
-
properties: {
|
|
466
|
-
type: {
|
|
467
|
-
type: 'string',
|
|
468
|
-
description: 'Type to search for (e.g. "Elf", "Planeswalker", "Equipment")',
|
|
469
|
-
},
|
|
470
|
-
format: {
|
|
471
|
-
type: 'string',
|
|
472
|
-
description: 'Filter by format legality',
|
|
473
|
-
},
|
|
474
|
-
max_price: {
|
|
475
|
-
type: 'number',
|
|
476
|
-
description: 'Maximum price in USD',
|
|
477
|
-
},
|
|
478
|
-
min_price: {
|
|
479
|
-
type: 'number',
|
|
480
|
-
description: 'Minimum price in USD',
|
|
481
|
-
},
|
|
482
|
-
set_code: {
|
|
483
|
-
type: 'string',
|
|
484
|
-
description: 'Filter by set code (e.g. "MH3")',
|
|
485
|
-
},
|
|
486
|
-
limit: {
|
|
487
|
-
type: 'number',
|
|
488
|
-
description: 'Max results (default: 50, max: 200)',
|
|
489
|
-
},
|
|
490
|
-
},
|
|
491
|
-
required: ['type'],
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
handler: async (args) => {
|
|
495
|
-
try {
|
|
496
|
-
const limit = Math.min(args.limit || 50, 200);
|
|
497
|
-
const db = getClient();
|
|
498
|
-
let query = db
|
|
499
|
-
.from('cards')
|
|
500
|
-
.select('name, set_code, set_name, rarity, price_usd, type_line, mana_cost, scryfall_id')
|
|
501
|
-
.ilike('type_line', `%${args.type}%`)
|
|
502
|
-
.order('price_usd', { ascending: false, nullsFirst: false })
|
|
503
|
-
.limit(limit);
|
|
504
|
-
if (args.format) {
|
|
505
|
-
const format = validateFormat(args.format);
|
|
506
|
-
query = query.filter(`legalities->>${format}`, 'eq', 'legal');
|
|
507
|
-
}
|
|
508
|
-
if (args.max_price != null) {
|
|
509
|
-
query = query.lte('price_usd', args.max_price);
|
|
510
|
-
}
|
|
511
|
-
if (args.min_price != null) {
|
|
512
|
-
query = query.gte('price_usd', args.min_price);
|
|
513
|
-
}
|
|
514
|
-
if (args.set_code) {
|
|
515
|
-
query = query.eq('set_code', args.set_code.toLowerCase());
|
|
516
|
-
}
|
|
517
|
-
const { data, error } = await query;
|
|
518
|
-
if (error)
|
|
519
|
-
throw new Error(error.message);
|
|
520
|
-
return jsonResult({
|
|
521
|
-
success: true,
|
|
522
|
-
type: args.type,
|
|
523
|
-
cards: data,
|
|
524
|
-
count: data?.length || 0,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
catch (error) {
|
|
528
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
529
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
};
|
|
533
|
-
// =============================================================================
|
|
534
|
-
// Tool 7: Price Bracket
|
|
535
|
-
// =============================================================================
|
|
536
|
-
const deckPriceBracket = {
|
|
537
|
-
definition: {
|
|
538
|
-
name: 'deck_price_bracket',
|
|
539
|
-
description: 'Multi-filter power search: combine price range, rarity, format, set, and type filters.',
|
|
540
|
-
annotations: {
|
|
541
|
-
title: 'Price Bracket',
|
|
542
|
-
readOnlyHint: true,
|
|
543
|
-
destructiveHint: false,
|
|
544
|
-
openWorldHint: true,
|
|
545
|
-
},
|
|
546
|
-
inputSchema: {
|
|
547
|
-
type: 'object',
|
|
548
|
-
properties: {
|
|
549
|
-
min_price: { type: 'number', description: 'Minimum price in USD' },
|
|
550
|
-
max_price: { type: 'number', description: 'Maximum price in USD' },
|
|
551
|
-
rarity: { type: 'string', description: 'Card rarity filter' },
|
|
552
|
-
format: { type: 'string', description: 'Format legality filter' },
|
|
553
|
-
set_code: { type: 'string', description: 'Set code filter' },
|
|
554
|
-
type_line: { type: 'string', description: 'Type line filter (partial match)' },
|
|
555
|
-
sort_by: {
|
|
556
|
-
type: 'string',
|
|
557
|
-
description: 'Sort: price_desc (default), price_asc, name',
|
|
558
|
-
},
|
|
559
|
-
limit: { type: 'number', description: 'Max results (default: 50, max: 200)' },
|
|
560
|
-
},
|
|
561
|
-
required: [],
|
|
562
|
-
},
|
|
563
|
-
},
|
|
564
|
-
handler: async (args) => {
|
|
565
|
-
try {
|
|
566
|
-
const hasFilter = args.min_price != null ||
|
|
567
|
-
args.max_price != null ||
|
|
568
|
-
args.rarity ||
|
|
569
|
-
args.format ||
|
|
570
|
-
args.set_code ||
|
|
571
|
-
args.type_line;
|
|
572
|
-
if (!hasFilter) {
|
|
573
|
-
return jsonResult({ success: false, error: 'Provide at least one filter (min_price, max_price, rarity, format, set_code, type_line)' }, true);
|
|
574
|
-
}
|
|
575
|
-
const limit = Math.min(args.limit || 50, 200);
|
|
576
|
-
const db = getClient();
|
|
577
|
-
const filtersApplied = [];
|
|
578
|
-
let query = db
|
|
579
|
-
.from('cards')
|
|
580
|
-
.select('name, set_code, set_name, rarity, price_usd, type_line, mana_cost, scryfall_id')
|
|
581
|
-
.limit(limit);
|
|
582
|
-
if (args.min_price != null) {
|
|
583
|
-
query = query.gte('price_usd', args.min_price);
|
|
584
|
-
filtersApplied.push(`min_price=${args.min_price}`);
|
|
585
|
-
}
|
|
586
|
-
if (args.max_price != null) {
|
|
587
|
-
query = query.lte('price_usd', args.max_price);
|
|
588
|
-
filtersApplied.push(`max_price=${args.max_price}`);
|
|
589
|
-
}
|
|
590
|
-
if (args.rarity) {
|
|
591
|
-
query = query.eq('rarity', validateRarity(args.rarity));
|
|
592
|
-
filtersApplied.push(`rarity=${args.rarity}`);
|
|
593
|
-
}
|
|
594
|
-
if (args.format) {
|
|
595
|
-
const format = validateFormat(args.format);
|
|
596
|
-
query = query.filter(`legalities->>${format}`, 'eq', 'legal');
|
|
597
|
-
filtersApplied.push(`format=${format}`);
|
|
598
|
-
}
|
|
599
|
-
if (args.set_code) {
|
|
600
|
-
query = query.eq('set_code', args.set_code.toLowerCase());
|
|
601
|
-
filtersApplied.push(`set_code=${args.set_code}`);
|
|
602
|
-
}
|
|
603
|
-
if (args.type_line) {
|
|
604
|
-
query = query.ilike('type_line', `%${args.type_line}%`);
|
|
605
|
-
filtersApplied.push(`type_line~${args.type_line}`);
|
|
606
|
-
}
|
|
607
|
-
// Sort
|
|
608
|
-
const sortBy = args.sort_by || 'price_desc';
|
|
609
|
-
if (sortBy === 'price_asc') {
|
|
610
|
-
query = query.order('price_usd', { ascending: true, nullsFirst: false });
|
|
611
|
-
}
|
|
612
|
-
else if (sortBy === 'name') {
|
|
613
|
-
query = query.order('name', { ascending: true });
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
query = query.order('price_usd', { ascending: false, nullsFirst: false });
|
|
617
|
-
}
|
|
618
|
-
const { data, error } = await query;
|
|
619
|
-
if (error)
|
|
620
|
-
throw new Error(error.message);
|
|
621
|
-
return jsonResult({
|
|
622
|
-
success: true,
|
|
623
|
-
filters_applied: filtersApplied,
|
|
624
|
-
cards: data,
|
|
625
|
-
count: data?.length || 0,
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
catch (error) {
|
|
629
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
630
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
631
|
-
}
|
|
632
|
-
},
|
|
633
|
-
};
|
|
634
|
-
// =============================================================================
|
|
635
|
-
// Tool 8: Set Analysis
|
|
636
|
-
// =============================================================================
|
|
637
|
-
const deckSetAnalysis = {
|
|
638
|
-
definition: {
|
|
639
|
-
name: 'deck_set_analysis',
|
|
640
|
-
description: 'Aggregate price stats for a set: EV breakdown by rarity, top cards, cheapest mythics.',
|
|
641
|
-
annotations: {
|
|
642
|
-
title: 'Set Analysis',
|
|
643
|
-
readOnlyHint: true,
|
|
644
|
-
destructiveHint: false,
|
|
645
|
-
openWorldHint: true,
|
|
646
|
-
},
|
|
647
|
-
inputSchema: {
|
|
648
|
-
type: 'object',
|
|
649
|
-
properties: {
|
|
650
|
-
set_code: {
|
|
651
|
-
type: 'string',
|
|
652
|
-
description: 'Set code (e.g. "MH3", "LCI", "ONE")',
|
|
653
|
-
},
|
|
654
|
-
},
|
|
655
|
-
required: ['set_code'],
|
|
656
|
-
},
|
|
657
|
-
},
|
|
658
|
-
handler: async (args) => {
|
|
659
|
-
try {
|
|
660
|
-
const db = getClient();
|
|
661
|
-
const setCode = args.set_code.toLowerCase();
|
|
662
|
-
// Use the deck_set_stats SQL function for efficient server-side aggregation
|
|
663
|
-
const { data: stats, error: statsErr } = await db.rpc('deck_set_stats', {
|
|
664
|
-
p_set_code: setCode,
|
|
665
|
-
});
|
|
666
|
-
if (statsErr)
|
|
667
|
-
throw new Error(statsErr.message);
|
|
668
|
-
if (!stats || stats.length === 0) {
|
|
669
|
-
return jsonResult({ success: false, error: `No cards found for set "${args.set_code}"` }, true);
|
|
670
|
-
}
|
|
671
|
-
const row = stats[0];
|
|
672
|
-
// Also fetch top 10 most expensive for detail
|
|
673
|
-
const { data: topCards, error: topErr } = await db
|
|
674
|
-
.from('cards')
|
|
675
|
-
.select('name, rarity, price_usd')
|
|
676
|
-
.eq('set_code', setCode)
|
|
677
|
-
.not('price_usd', 'is', null)
|
|
678
|
-
.gt('price_usd', 0)
|
|
679
|
-
.order('price_usd', { ascending: false, nullsFirst: false })
|
|
680
|
-
.limit(10);
|
|
681
|
-
if (topErr)
|
|
682
|
-
throw new Error(topErr.message);
|
|
683
|
-
return jsonResult({
|
|
684
|
-
success: true,
|
|
685
|
-
set_code: row.set_code,
|
|
686
|
-
set_name: row.set_name,
|
|
687
|
-
total_cards: row.total_cards,
|
|
688
|
-
cards_with_price: row.cards_with_price,
|
|
689
|
-
total_value: row.total_value,
|
|
690
|
-
avg_price: row.avg_price,
|
|
691
|
-
median_price: row.median_price,
|
|
692
|
-
max_price: row.max_price,
|
|
693
|
-
max_price_card: row.max_price_card,
|
|
694
|
-
rarity_averages: {
|
|
695
|
-
mythic: row.mythic_avg,
|
|
696
|
-
rare: row.rare_avg,
|
|
697
|
-
uncommon: row.uncommon_avg,
|
|
698
|
-
common: row.common_avg,
|
|
699
|
-
},
|
|
700
|
-
cheapest_mythic: row.cheapest_mythic_name
|
|
701
|
-
? { name: row.cheapest_mythic_name, price: row.cheapest_mythic_price }
|
|
702
|
-
: null,
|
|
703
|
-
top_10_expensive: topCards || [],
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
catch (error) {
|
|
707
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
708
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
709
|
-
}
|
|
710
|
-
},
|
|
711
|
-
};
|
|
712
|
-
// =============================================================================
|
|
713
|
-
// Tool 9: Volatility
|
|
714
|
-
// =============================================================================
|
|
715
|
-
const deckVolatility = {
|
|
716
|
-
definition: {
|
|
717
|
-
name: 'deck_volatility',
|
|
718
|
-
description: 'Price volatility scoring. Broad scan (no name) finds most volatile cards. Single card mode (with name/scryfall_id) gives detailed stability analysis with A-F grade.',
|
|
719
|
-
annotations: {
|
|
720
|
-
title: 'Volatility',
|
|
721
|
-
readOnlyHint: true,
|
|
722
|
-
destructiveHint: false,
|
|
723
|
-
openWorldHint: true,
|
|
724
|
-
},
|
|
725
|
-
inputSchema: {
|
|
726
|
-
type: 'object',
|
|
727
|
-
properties: {
|
|
728
|
-
name: {
|
|
729
|
-
type: 'string',
|
|
730
|
-
description: 'Card name for single-card analysis (omit for broad scan)',
|
|
731
|
-
},
|
|
732
|
-
scryfall_id: {
|
|
733
|
-
type: 'string',
|
|
734
|
-
description: 'Scryfall UUID for single-card analysis',
|
|
735
|
-
},
|
|
736
|
-
min_price: {
|
|
737
|
-
type: 'number',
|
|
738
|
-
description: 'Minimum price filter for broad scan (default: 1.00)',
|
|
739
|
-
},
|
|
740
|
-
days: {
|
|
741
|
-
type: 'number',
|
|
742
|
-
description: 'History window for single-card mode (default: 90)',
|
|
743
|
-
},
|
|
744
|
-
limit: {
|
|
745
|
-
type: 'number',
|
|
746
|
-
description: 'Max results for broad scan (default: 25, max: 100)',
|
|
747
|
-
},
|
|
748
|
-
},
|
|
749
|
-
required: [],
|
|
750
|
-
},
|
|
751
|
-
},
|
|
752
|
-
handler: async (args) => {
|
|
753
|
-
try {
|
|
754
|
-
const db = getClient();
|
|
755
|
-
// ── Single card mode ──
|
|
756
|
-
if (args.name || args.scryfall_id) {
|
|
757
|
-
let scryfallId = args.scryfall_id;
|
|
758
|
-
// Resolve name → scryfall_id
|
|
759
|
-
if (!scryfallId && args.name) {
|
|
760
|
-
const { data: cards, error: cardErr } = await db
|
|
761
|
-
.from('cards')
|
|
762
|
-
.select('scryfall_id, name, price_usd')
|
|
763
|
-
.ilike('name', args.name)
|
|
764
|
-
.limit(1);
|
|
765
|
-
if (cardErr)
|
|
766
|
-
throw new Error(cardErr.message);
|
|
767
|
-
if (!cards || cards.length === 0) {
|
|
768
|
-
return jsonResult({ success: false, error: `Card not found: ${args.name}` }, true);
|
|
769
|
-
}
|
|
770
|
-
scryfallId = cards[0].scryfall_id;
|
|
771
|
-
}
|
|
772
|
-
const days = args.days || 90;
|
|
773
|
-
// Fetch price history and movers in parallel
|
|
774
|
-
const [historyRes, moversRes] = await Promise.all([
|
|
775
|
-
db
|
|
776
|
-
.from('price_history')
|
|
777
|
-
.select('date, price_usd')
|
|
778
|
-
.eq('scryfall_id', scryfallId)
|
|
779
|
-
.order('date', { ascending: false })
|
|
780
|
-
.limit(days),
|
|
781
|
-
db
|
|
782
|
-
.from('price_movers')
|
|
783
|
-
.select('period, price_from, price_to, change_pct')
|
|
784
|
-
.eq('scryfall_id', scryfallId),
|
|
785
|
-
]);
|
|
786
|
-
if (historyRes.error)
|
|
787
|
-
throw new Error(historyRes.error.message);
|
|
788
|
-
const prices = (historyRes.data || [])
|
|
789
|
-
.map((r) => r.price_usd)
|
|
790
|
-
.filter((p) => p != null && p > 0);
|
|
791
|
-
if (prices.length < 2) {
|
|
792
|
-
return jsonResult({
|
|
793
|
-
success: true,
|
|
794
|
-
mode: 'single',
|
|
795
|
-
scryfall_id: scryfallId,
|
|
796
|
-
message: 'Not enough price history for volatility analysis',
|
|
797
|
-
data_points: prices.length,
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
const mean = prices.reduce((s, p) => s + p, 0) / prices.length;
|
|
801
|
-
const variance = prices.reduce((s, p) => s + (p - mean) ** 2, 0) / prices.length;
|
|
802
|
-
const stddev = Math.sqrt(variance);
|
|
803
|
-
const cv = (stddev / mean) * 100; // coefficient of variation as %
|
|
804
|
-
const stability = gradeStability(cv);
|
|
805
|
-
const trend = prices.length >= 2
|
|
806
|
-
? prices[0] > prices[prices.length - 1]
|
|
807
|
-
? 'rising'
|
|
808
|
-
: prices[0] < prices[prices.length - 1]
|
|
809
|
-
? 'falling'
|
|
810
|
-
: 'flat'
|
|
811
|
-
: 'unknown';
|
|
812
|
-
return jsonResult({
|
|
813
|
-
success: true,
|
|
814
|
-
mode: 'single',
|
|
815
|
-
scryfall_id: scryfallId,
|
|
816
|
-
data_points: prices.length,
|
|
817
|
-
stats: {
|
|
818
|
-
mean: Math.round(mean * 100) / 100,
|
|
819
|
-
stddev: Math.round(stddev * 100) / 100,
|
|
820
|
-
coefficient_of_variation: Math.round(cv * 100) / 100,
|
|
821
|
-
min: Math.min(...prices),
|
|
822
|
-
max: Math.max(...prices),
|
|
823
|
-
range: Math.round((Math.max(...prices) - Math.min(...prices)) * 100) / 100,
|
|
824
|
-
trend,
|
|
825
|
-
},
|
|
826
|
-
stability_grade: stability.grade,
|
|
827
|
-
stability_label: stability.label,
|
|
828
|
-
recent_movers: moversRes.data || [],
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
// ── Broad scan mode — use SQL function for real CV-based volatility ──
|
|
832
|
-
const minPrice = args.min_price ?? 1.0;
|
|
833
|
-
const limit = Math.min(args.limit || 25, 100);
|
|
834
|
-
const days = args.days || 30;
|
|
835
|
-
const { data: volData, error: volErr } = await db.rpc('deck_volatility_scores', { p_days: days, p_min_price: minPrice, p_limit: limit });
|
|
836
|
-
if (volErr)
|
|
837
|
-
throw new Error(volErr.message);
|
|
838
|
-
const results = (volData || []).map((r) => ({
|
|
839
|
-
name: r.name,
|
|
840
|
-
set_code: r.set_code,
|
|
841
|
-
rarity: r.rarity,
|
|
842
|
-
scryfall_id: r.scryfall_id,
|
|
843
|
-
current_price: r.current_price,
|
|
844
|
-
avg_price: r.avg_price,
|
|
845
|
-
stddev: r.stddev_price,
|
|
846
|
-
price_range: { min: r.min_price, max: r.max_price },
|
|
847
|
-
cv_pct: r.cv_pct,
|
|
848
|
-
stability: gradeStability(Number(r.cv_pct) || 0),
|
|
849
|
-
data_points: r.data_points,
|
|
850
|
-
}));
|
|
851
|
-
return jsonResult({
|
|
852
|
-
success: true,
|
|
853
|
-
mode: 'broad',
|
|
854
|
-
days,
|
|
855
|
-
min_price_filter: minPrice,
|
|
856
|
-
cards: results,
|
|
857
|
-
count: results.length,
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
catch (error) {
|
|
861
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
862
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
863
|
-
}
|
|
864
|
-
},
|
|
865
|
-
};
|
|
866
|
-
// =============================================================================
|
|
867
|
-
// Tool 10: Reprints
|
|
868
|
-
// =============================================================================
|
|
869
|
-
const deckReprints = {
|
|
870
|
-
definition: {
|
|
871
|
-
name: 'deck_reprints',
|
|
872
|
-
description: 'Cross-printing price comparison and arbitrage finder. Shows all printings of a card sorted by price.',
|
|
873
|
-
annotations: {
|
|
874
|
-
title: 'Reprints',
|
|
875
|
-
readOnlyHint: true,
|
|
876
|
-
destructiveHint: false,
|
|
877
|
-
openWorldHint: true,
|
|
878
|
-
},
|
|
879
|
-
inputSchema: {
|
|
880
|
-
type: 'object',
|
|
881
|
-
properties: {
|
|
882
|
-
name: {
|
|
883
|
-
type: 'string',
|
|
884
|
-
description: 'Card name (exact match, case-insensitive)',
|
|
885
|
-
},
|
|
886
|
-
},
|
|
887
|
-
required: ['name'],
|
|
888
|
-
},
|
|
889
|
-
},
|
|
890
|
-
handler: async (args) => {
|
|
891
|
-
try {
|
|
892
|
-
const db = getClient();
|
|
893
|
-
// Exact match (case-insensitive, no wildcards)
|
|
894
|
-
const { data, error } = await db
|
|
895
|
-
.from('cards')
|
|
896
|
-
.select('name, set_code, set_name, rarity, price_usd, price_usd_foil, scryfall_id, image_uri')
|
|
897
|
-
.ilike('name', args.name)
|
|
898
|
-
.order('price_usd', { ascending: true, nullsFirst: false })
|
|
899
|
-
.limit(100);
|
|
900
|
-
if (error)
|
|
901
|
-
throw new Error(error.message);
|
|
902
|
-
// No exact match — try partial for suggestions
|
|
903
|
-
if (!data || data.length === 0) {
|
|
904
|
-
const { data: suggestions, error: sugErr } = await db
|
|
905
|
-
.from('cards')
|
|
906
|
-
.select('name')
|
|
907
|
-
.ilike('name', `%${args.name}%`)
|
|
908
|
-
.limit(20);
|
|
909
|
-
if (sugErr)
|
|
910
|
-
throw new Error(sugErr.message);
|
|
911
|
-
const uniqueNames = [...new Set((suggestions || []).map((s) => s.name))].slice(0, 5);
|
|
912
|
-
return jsonResult({
|
|
913
|
-
success: false,
|
|
914
|
-
error: `No exact match for "${args.name}"`,
|
|
915
|
-
suggestions: uniqueNames.length > 0 ? uniqueNames : undefined,
|
|
916
|
-
}, true);
|
|
917
|
-
}
|
|
918
|
-
const printings = data;
|
|
919
|
-
// Compute summary stats
|
|
920
|
-
const priced = printings.filter((c) => c.price_usd != null && c.price_usd > 0);
|
|
921
|
-
const prices = priced.map((c) => c.price_usd);
|
|
922
|
-
const summary = prices.length > 0
|
|
923
|
-
? {
|
|
924
|
-
total_printings: printings.length,
|
|
925
|
-
priced_printings: prices.length,
|
|
926
|
-
cheapest: Math.min(...prices),
|
|
927
|
-
most_expensive: Math.max(...prices),
|
|
928
|
-
price_spread: Math.round((Math.max(...prices) - Math.min(...prices)) * 100) / 100,
|
|
929
|
-
average_price: Math.round((prices.reduce((s, p) => s + p, 0) / prices.length) * 100) / 100,
|
|
930
|
-
}
|
|
931
|
-
: {
|
|
932
|
-
total_printings: printings.length,
|
|
933
|
-
priced_printings: 0,
|
|
934
|
-
cheapest: null,
|
|
935
|
-
most_expensive: null,
|
|
936
|
-
price_spread: null,
|
|
937
|
-
average_price: null,
|
|
938
|
-
};
|
|
939
|
-
return jsonResult({
|
|
940
|
-
success: true,
|
|
941
|
-
name: printings[0].name,
|
|
942
|
-
summary,
|
|
943
|
-
printings: printings.map((c) => ({
|
|
944
|
-
set_code: c.set_code,
|
|
945
|
-
set_name: c.set_name,
|
|
946
|
-
rarity: c.rarity,
|
|
947
|
-
price_usd: c.price_usd,
|
|
948
|
-
price_usd_foil: c.price_usd_foil,
|
|
949
|
-
scryfall_id: c.scryfall_id,
|
|
950
|
-
})),
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
catch (error) {
|
|
954
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
955
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
956
|
-
}
|
|
957
|
-
},
|
|
958
|
-
};
|
|
959
|
-
// =============================================================================
|
|
960
|
-
// Tool 11: Match History
|
|
961
|
-
// =============================================================================
|
|
962
|
-
const deckMatchHistory = {
|
|
963
|
-
definition: {
|
|
964
|
-
name: 'deck_match_history',
|
|
965
|
-
description: 'Recent MTG Arena match history. Returns opponent, format, result, deck, duration.',
|
|
966
|
-
annotations: {
|
|
967
|
-
title: 'Match History',
|
|
968
|
-
readOnlyHint: true,
|
|
969
|
-
destructiveHint: false,
|
|
970
|
-
openWorldHint: true,
|
|
971
|
-
},
|
|
972
|
-
inputSchema: {
|
|
973
|
-
type: 'object',
|
|
974
|
-
properties: {
|
|
975
|
-
limit: {
|
|
976
|
-
type: 'number',
|
|
977
|
-
description: 'Max results (default: 20, max: 100)',
|
|
978
|
-
},
|
|
979
|
-
format: {
|
|
980
|
-
type: 'string',
|
|
981
|
-
description: 'Optional: filter by format (e.g. "Standard", "Historic")',
|
|
982
|
-
},
|
|
983
|
-
},
|
|
984
|
-
required: [],
|
|
985
|
-
},
|
|
986
|
-
},
|
|
987
|
-
handler: async (args) => {
|
|
988
|
-
try {
|
|
989
|
-
const db = getClient();
|
|
990
|
-
const limit = Math.min(args.limit || 20, 100);
|
|
991
|
-
let query = db
|
|
992
|
-
.from('match_summary')
|
|
993
|
-
.select('match_id, opponent, format, result, deck_name, duration_seconds, started_at')
|
|
994
|
-
.order('started_at_ms', { ascending: false })
|
|
995
|
-
.limit(limit);
|
|
996
|
-
if (args.format) {
|
|
997
|
-
query = query.ilike('format', args.format);
|
|
998
|
-
}
|
|
999
|
-
const { data, error } = await query;
|
|
1000
|
-
if (error)
|
|
1001
|
-
throw new Error(error.message);
|
|
1002
|
-
return jsonResult({
|
|
1003
|
-
success: true,
|
|
1004
|
-
matches: data || [],
|
|
1005
|
-
count: (data || []).length,
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
catch (error) {
|
|
1009
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1010
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1011
|
-
}
|
|
1012
|
-
},
|
|
1013
|
-
};
|
|
1014
|
-
// =============================================================================
|
|
1015
|
-
// Tool 12: Win Rate
|
|
1016
|
-
// =============================================================================
|
|
1017
|
-
const deckWinRate = {
|
|
1018
|
-
definition: {
|
|
1019
|
-
name: 'deck_win_rate',
|
|
1020
|
-
description: 'Win rate statistics grouped by format. Includes overall stats and current streak.',
|
|
1021
|
-
annotations: {
|
|
1022
|
-
title: 'Win Rate',
|
|
1023
|
-
readOnlyHint: true,
|
|
1024
|
-
destructiveHint: false,
|
|
1025
|
-
openWorldHint: true,
|
|
1026
|
-
},
|
|
1027
|
-
inputSchema: {
|
|
1028
|
-
type: 'object',
|
|
1029
|
-
properties: {
|
|
1030
|
-
format: {
|
|
1031
|
-
type: 'string',
|
|
1032
|
-
description: 'Optional: filter to a specific format',
|
|
1033
|
-
},
|
|
1034
|
-
},
|
|
1035
|
-
required: [],
|
|
1036
|
-
},
|
|
1037
|
-
},
|
|
1038
|
-
handler: async (args) => {
|
|
1039
|
-
try {
|
|
1040
|
-
const db = getClient();
|
|
1041
|
-
let query = db
|
|
1042
|
-
.from('match_summary')
|
|
1043
|
-
.select('format, result, started_at_ms')
|
|
1044
|
-
.order('started_at_ms', { ascending: false });
|
|
1045
|
-
if (args.format) {
|
|
1046
|
-
query = query.ilike('format', args.format);
|
|
1047
|
-
}
|
|
1048
|
-
const { data, error } = await query;
|
|
1049
|
-
if (error)
|
|
1050
|
-
throw new Error(error.message);
|
|
1051
|
-
const matches = (data || []);
|
|
1052
|
-
// Group by format
|
|
1053
|
-
const byFormat = new Map();
|
|
1054
|
-
for (const m of matches) {
|
|
1055
|
-
const fmt = m.format || 'Unknown';
|
|
1056
|
-
if (!byFormat.has(fmt))
|
|
1057
|
-
byFormat.set(fmt, { wins: 0, losses: 0, draws: 0, results: [] });
|
|
1058
|
-
const bucket = byFormat.get(fmt);
|
|
1059
|
-
const result = m.result || '';
|
|
1060
|
-
if (result === 'win')
|
|
1061
|
-
bucket.wins++;
|
|
1062
|
-
else if (result === 'loss' || result === 'concede')
|
|
1063
|
-
bucket.losses++;
|
|
1064
|
-
else
|
|
1065
|
-
bucket.draws++;
|
|
1066
|
-
bucket.results.push(result);
|
|
1067
|
-
}
|
|
1068
|
-
// Compute current streak from all matches (chronological, most recent first)
|
|
1069
|
-
const allResults = matches.map((m) => m.result);
|
|
1070
|
-
let streak = 0;
|
|
1071
|
-
const streakType = allResults[0] === 'win' ? 'W' : allResults[0] === 'loss' || allResults[0] === 'concede' ? 'L' : 'D';
|
|
1072
|
-
for (const r of allResults) {
|
|
1073
|
-
const rType = r === 'win' ? 'W' : r === 'loss' || r === 'concede' ? 'L' : 'D';
|
|
1074
|
-
if (rType === streakType)
|
|
1075
|
-
streak++;
|
|
1076
|
-
else
|
|
1077
|
-
break;
|
|
1078
|
-
}
|
|
1079
|
-
const formatStats = Array.from(byFormat.entries()).map(([fmt, b]) => {
|
|
1080
|
-
const total = b.wins + b.losses + b.draws;
|
|
1081
|
-
return {
|
|
1082
|
-
format: fmt,
|
|
1083
|
-
total,
|
|
1084
|
-
wins: b.wins,
|
|
1085
|
-
losses: b.losses,
|
|
1086
|
-
draws: b.draws,
|
|
1087
|
-
win_rate: total > 0 ? Math.round((b.wins / total) * 10000) / 100 : 0,
|
|
1088
|
-
};
|
|
1089
|
-
});
|
|
1090
|
-
// Overall
|
|
1091
|
-
const totalWins = formatStats.reduce((s, f) => s + f.wins, 0);
|
|
1092
|
-
const totalLosses = formatStats.reduce((s, f) => s + f.losses, 0);
|
|
1093
|
-
const totalDraws = formatStats.reduce((s, f) => s + f.draws, 0);
|
|
1094
|
-
const totalAll = totalWins + totalLosses + totalDraws;
|
|
1095
|
-
return jsonResult({
|
|
1096
|
-
success: true,
|
|
1097
|
-
overall: {
|
|
1098
|
-
total: totalAll,
|
|
1099
|
-
wins: totalWins,
|
|
1100
|
-
losses: totalLosses,
|
|
1101
|
-
draws: totalDraws,
|
|
1102
|
-
win_rate: totalAll > 0 ? Math.round((totalWins / totalAll) * 10000) / 100 : 0,
|
|
1103
|
-
current_streak: `${streak}${streakType}`,
|
|
1104
|
-
},
|
|
1105
|
-
by_format: formatStats,
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
catch (error) {
|
|
1109
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1110
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1111
|
-
}
|
|
1112
|
-
},
|
|
1113
|
-
};
|
|
1114
|
-
// =============================================================================
|
|
1115
|
-
// Tool 13: Card Performance
|
|
1116
|
-
// =============================================================================
|
|
1117
|
-
const deckCardPerformance = {
|
|
1118
|
-
definition: {
|
|
1119
|
-
name: 'deck_card_performance',
|
|
1120
|
-
description: 'Best and worst cards by win rate. Requires minimum match threshold to filter noise.',
|
|
1121
|
-
annotations: {
|
|
1122
|
-
title: 'Card Performance',
|
|
1123
|
-
readOnlyHint: true,
|
|
1124
|
-
destructiveHint: false,
|
|
1125
|
-
openWorldHint: true,
|
|
1126
|
-
},
|
|
1127
|
-
inputSchema: {
|
|
1128
|
-
type: 'object',
|
|
1129
|
-
properties: {
|
|
1130
|
-
format: {
|
|
1131
|
-
type: 'string',
|
|
1132
|
-
description: 'Optional: filter by format',
|
|
1133
|
-
},
|
|
1134
|
-
min_matches: {
|
|
1135
|
-
type: 'number',
|
|
1136
|
-
description: 'Minimum matches to include (default: 3)',
|
|
1137
|
-
},
|
|
1138
|
-
sort: {
|
|
1139
|
-
type: 'string',
|
|
1140
|
-
description: 'Sort order: "win_rate desc" (default), "win_rate asc", "matches desc"',
|
|
1141
|
-
},
|
|
1142
|
-
limit: {
|
|
1143
|
-
type: 'number',
|
|
1144
|
-
description: 'Max results (default: 20, max: 100)',
|
|
1145
|
-
},
|
|
1146
|
-
},
|
|
1147
|
-
required: [],
|
|
1148
|
-
},
|
|
1149
|
-
},
|
|
1150
|
-
handler: async (args) => {
|
|
1151
|
-
try {
|
|
1152
|
-
const db = getClient();
|
|
1153
|
-
const minMatches = args.min_matches || 3;
|
|
1154
|
-
const limit = Math.min(args.limit || 20, 100);
|
|
1155
|
-
let query = db
|
|
1156
|
-
.from('card_win_rates')
|
|
1157
|
-
.select('name, format, matches, wins, losses, win_rate, set_code')
|
|
1158
|
-
.gte('matches', minMatches);
|
|
1159
|
-
if (args.format) {
|
|
1160
|
-
query = query.ilike('format', args.format);
|
|
1161
|
-
}
|
|
1162
|
-
// Sort
|
|
1163
|
-
const sort = (args.sort || 'win_rate desc').toLowerCase();
|
|
1164
|
-
if (sort === 'win_rate asc') {
|
|
1165
|
-
query = query.order('win_rate', { ascending: true }).order('matches', { ascending: false });
|
|
1166
|
-
}
|
|
1167
|
-
else if (sort === 'matches desc') {
|
|
1168
|
-
query = query.order('matches', { ascending: false });
|
|
1169
|
-
}
|
|
1170
|
-
else {
|
|
1171
|
-
query = query.order('win_rate', { ascending: false }).order('matches', { ascending: false });
|
|
1172
|
-
}
|
|
1173
|
-
query = query.limit(limit);
|
|
1174
|
-
const { data, error } = await query;
|
|
1175
|
-
if (error)
|
|
1176
|
-
throw new Error(error.message);
|
|
1177
|
-
return jsonResult({
|
|
1178
|
-
success: true,
|
|
1179
|
-
cards: data || [],
|
|
1180
|
-
count: (data || []).length,
|
|
1181
|
-
min_matches_filter: minMatches,
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
catch (error) {
|
|
1185
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1186
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1187
|
-
}
|
|
1188
|
-
},
|
|
1189
|
-
};
|
|
1190
|
-
// =============================================================================
|
|
1191
|
-
// Tool 14: Deck Record
|
|
1192
|
-
// =============================================================================
|
|
1193
|
-
const deckRecord = {
|
|
1194
|
-
definition: {
|
|
1195
|
-
name: 'deck_record',
|
|
1196
|
-
description: 'Deck-level win/loss records from deck_stats. Search by name or list all.',
|
|
1197
|
-
annotations: {
|
|
1198
|
-
title: 'Deck Record',
|
|
1199
|
-
readOnlyHint: true,
|
|
1200
|
-
destructiveHint: false,
|
|
1201
|
-
openWorldHint: true,
|
|
1202
|
-
},
|
|
1203
|
-
inputSchema: {
|
|
1204
|
-
type: 'object',
|
|
1205
|
-
properties: {
|
|
1206
|
-
deck_name: {
|
|
1207
|
-
type: 'string',
|
|
1208
|
-
description: 'Optional: search by deck name (partial match)',
|
|
1209
|
-
},
|
|
1210
|
-
limit: {
|
|
1211
|
-
type: 'number',
|
|
1212
|
-
description: 'Max results (default: 20)',
|
|
1213
|
-
},
|
|
1214
|
-
},
|
|
1215
|
-
required: [],
|
|
1216
|
-
},
|
|
1217
|
-
},
|
|
1218
|
-
handler: async (args) => {
|
|
1219
|
-
try {
|
|
1220
|
-
const db = getClient();
|
|
1221
|
-
const limit = Math.min(args.limit || 20, 100);
|
|
1222
|
-
let query = db
|
|
1223
|
-
.from('deck_stats')
|
|
1224
|
-
.select('deck_name, total_matches, wins, losses, last_played_ms')
|
|
1225
|
-
.order('total_matches', { ascending: false })
|
|
1226
|
-
.limit(limit);
|
|
1227
|
-
if (args.deck_name) {
|
|
1228
|
-
query = query.ilike('deck_name', `%${args.deck_name}%`);
|
|
1229
|
-
}
|
|
1230
|
-
const { data, error } = await query;
|
|
1231
|
-
if (error)
|
|
1232
|
-
throw new Error(error.message);
|
|
1233
|
-
const decks = (data || []).map((d) => {
|
|
1234
|
-
const total = d.total_matches || 0;
|
|
1235
|
-
const wins = d.wins || 0;
|
|
1236
|
-
const losses = d.losses || 0;
|
|
1237
|
-
return {
|
|
1238
|
-
deck_name: d.deck_name,
|
|
1239
|
-
total_matches: total,
|
|
1240
|
-
wins,
|
|
1241
|
-
losses,
|
|
1242
|
-
win_rate: total > 0 ? Math.round((wins / total) * 10000) / 100 : 0,
|
|
1243
|
-
last_played: d.last_played_ms
|
|
1244
|
-
? new Date(Number(d.last_played_ms)).toISOString()
|
|
1245
|
-
: null,
|
|
1246
|
-
};
|
|
1247
|
-
});
|
|
1248
|
-
return jsonResult({
|
|
1249
|
-
success: true,
|
|
1250
|
-
decks,
|
|
1251
|
-
count: decks.length,
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
catch (error) {
|
|
1255
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1256
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1257
|
-
}
|
|
1258
|
-
},
|
|
1259
|
-
};
|
|
1260
|
-
// =============================================================================
|
|
1261
|
-
// Tool 15: Player Summary
|
|
1262
|
-
// =============================================================================
|
|
1263
|
-
const deckPlayerSummary = {
|
|
1264
|
-
definition: {
|
|
1265
|
-
name: 'deck_player_summary',
|
|
1266
|
-
description: 'Combined gameplay dashboard: total matches, overall win rate, best format, best deck, longest streak, recent form (last 10).',
|
|
1267
|
-
annotations: {
|
|
1268
|
-
title: 'Player Summary',
|
|
1269
|
-
readOnlyHint: true,
|
|
1270
|
-
destructiveHint: false,
|
|
1271
|
-
openWorldHint: true,
|
|
1272
|
-
},
|
|
1273
|
-
inputSchema: {
|
|
1274
|
-
type: 'object',
|
|
1275
|
-
properties: {},
|
|
1276
|
-
required: [],
|
|
1277
|
-
},
|
|
1278
|
-
},
|
|
1279
|
-
handler: async () => {
|
|
1280
|
-
try {
|
|
1281
|
-
const db = getClient();
|
|
1282
|
-
// Fetch matches and deck stats in parallel
|
|
1283
|
-
const [matchRes, deckRes] = await Promise.all([
|
|
1284
|
-
db
|
|
1285
|
-
.from('match_summary')
|
|
1286
|
-
.select('format, result, deck_name, started_at_ms')
|
|
1287
|
-
.order('started_at_ms', { ascending: false }),
|
|
1288
|
-
db
|
|
1289
|
-
.from('deck_stats')
|
|
1290
|
-
.select('deck_name, total_matches, wins, losses')
|
|
1291
|
-
.order('total_matches', { ascending: false }),
|
|
1292
|
-
]);
|
|
1293
|
-
if (matchRes.error)
|
|
1294
|
-
throw new Error(matchRes.error.message);
|
|
1295
|
-
if (deckRes.error)
|
|
1296
|
-
throw new Error(deckRes.error.message);
|
|
1297
|
-
const matches = (matchRes.data || []);
|
|
1298
|
-
const decks = (deckRes.data || []);
|
|
1299
|
-
// Overall stats
|
|
1300
|
-
const totalMatches = matches.length;
|
|
1301
|
-
const wins = matches.filter((m) => m.result === 'win').length;
|
|
1302
|
-
const losses = matches.filter((m) => m.result === 'loss' || m.result === 'concede').length;
|
|
1303
|
-
const overallWinRate = totalMatches > 0 ? Math.round((wins / totalMatches) * 10000) / 100 : 0;
|
|
1304
|
-
// Best format (by win rate, min 5 matches)
|
|
1305
|
-
const fmtMap = new Map();
|
|
1306
|
-
for (const m of matches) {
|
|
1307
|
-
const fmt = m.format || 'Unknown';
|
|
1308
|
-
if (!fmtMap.has(fmt))
|
|
1309
|
-
fmtMap.set(fmt, { w: 0, t: 0 });
|
|
1310
|
-
const b = fmtMap.get(fmt);
|
|
1311
|
-
b.t++;
|
|
1312
|
-
if (m.result === 'win')
|
|
1313
|
-
b.w++;
|
|
1314
|
-
}
|
|
1315
|
-
let bestFormat = null;
|
|
1316
|
-
for (const [fmt, b] of fmtMap) {
|
|
1317
|
-
if (b.t >= 5) {
|
|
1318
|
-
const wr = b.w / b.t;
|
|
1319
|
-
if (!bestFormat || wr > bestFormat.win_rate) {
|
|
1320
|
-
bestFormat = { format: fmt, win_rate: Math.round(wr * 10000) / 100, matches: b.t };
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
// Best deck (from deck_stats, min 3 matches)
|
|
1325
|
-
let bestDeck = null;
|
|
1326
|
-
for (const d of decks) {
|
|
1327
|
-
const total = d.total_matches || 0;
|
|
1328
|
-
const w = d.wins || 0;
|
|
1329
|
-
if (total >= 3) {
|
|
1330
|
-
const wr = w / total;
|
|
1331
|
-
if (!bestDeck || wr > bestDeck.win_rate) {
|
|
1332
|
-
bestDeck = {
|
|
1333
|
-
deck_name: d.deck_name,
|
|
1334
|
-
win_rate: Math.round(wr * 10000) / 100,
|
|
1335
|
-
matches: total,
|
|
1336
|
-
};
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
// Longest streak (all-time)
|
|
1341
|
-
let longestStreak = 0;
|
|
1342
|
-
let longestStreakType = '';
|
|
1343
|
-
let currentRun = 0;
|
|
1344
|
-
let currentType = '';
|
|
1345
|
-
for (const m of matches) {
|
|
1346
|
-
const r = m.result === 'win' ? 'W' : 'L';
|
|
1347
|
-
if (r === currentType) {
|
|
1348
|
-
currentRun++;
|
|
1349
|
-
}
|
|
1350
|
-
else {
|
|
1351
|
-
currentType = r;
|
|
1352
|
-
currentRun = 1;
|
|
1353
|
-
}
|
|
1354
|
-
if (currentRun > longestStreak) {
|
|
1355
|
-
longestStreak = currentRun;
|
|
1356
|
-
longestStreakType = currentType;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
// Recent form (last 10)
|
|
1360
|
-
const recentForm = matches.slice(0, 10).map((m) => {
|
|
1361
|
-
const r = m.result;
|
|
1362
|
-
return r === 'win' ? 'W' : r === 'loss' || r === 'concede' ? 'L' : 'D';
|
|
1363
|
-
});
|
|
1364
|
-
return jsonResult({
|
|
1365
|
-
success: true,
|
|
1366
|
-
total_matches: totalMatches,
|
|
1367
|
-
overall_win_rate: overallWinRate,
|
|
1368
|
-
wins,
|
|
1369
|
-
losses,
|
|
1370
|
-
best_format: bestFormat,
|
|
1371
|
-
best_deck: bestDeck,
|
|
1372
|
-
longest_streak: `${longestStreak}${longestStreakType}`,
|
|
1373
|
-
recent_form: recentForm.join(''),
|
|
1374
|
-
total_decks: decks.length,
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
catch (error) {
|
|
1378
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1379
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1380
|
-
}
|
|
1381
|
-
},
|
|
1382
|
-
};
|
|
1383
|
-
// =============================================================================
|
|
1384
|
-
// Tool 16: Draft Stats
|
|
1385
|
-
// =============================================================================
|
|
1386
|
-
const deckDraftStats = {
|
|
1387
|
-
definition: {
|
|
1388
|
-
name: 'deck_draft_stats',
|
|
1389
|
-
description: 'Draft history and performance from MTG Arena. Returns event, set, record, trophy status per draft.',
|
|
1390
|
-
annotations: {
|
|
1391
|
-
title: 'Draft Stats',
|
|
1392
|
-
readOnlyHint: true,
|
|
1393
|
-
destructiveHint: false,
|
|
1394
|
-
openWorldHint: true,
|
|
1395
|
-
},
|
|
1396
|
-
inputSchema: {
|
|
1397
|
-
type: 'object',
|
|
1398
|
-
properties: {
|
|
1399
|
-
set_code: {
|
|
1400
|
-
type: 'string',
|
|
1401
|
-
description: 'Optional: filter by set code (e.g. "OTJ", "MKM")',
|
|
1402
|
-
},
|
|
1403
|
-
limit: {
|
|
1404
|
-
type: 'number',
|
|
1405
|
-
description: 'Max results (default: 20, max: 100)',
|
|
1406
|
-
},
|
|
1407
|
-
},
|
|
1408
|
-
required: [],
|
|
1409
|
-
},
|
|
1410
|
-
},
|
|
1411
|
-
handler: async (args) => {
|
|
1412
|
-
try {
|
|
1413
|
-
const db = getClient();
|
|
1414
|
-
const limit = Math.min(args.limit || 20, 100);
|
|
1415
|
-
let query = db
|
|
1416
|
-
.from('draft_summary')
|
|
1417
|
-
.select('event_name, set_code, completed_at, total_picks, wins, losses')
|
|
1418
|
-
.order('completed_at', { ascending: false })
|
|
1419
|
-
.limit(limit);
|
|
1420
|
-
if (args.set_code) {
|
|
1421
|
-
query = query.ilike('set_code', args.set_code);
|
|
1422
|
-
}
|
|
1423
|
-
const { data, error } = await query;
|
|
1424
|
-
if (error)
|
|
1425
|
-
throw new Error(error.message);
|
|
1426
|
-
const drafts = (data || []).map((d) => {
|
|
1427
|
-
const wins = d.wins || 0;
|
|
1428
|
-
const losses = d.losses || 0;
|
|
1429
|
-
return {
|
|
1430
|
-
event_name: d.event_name,
|
|
1431
|
-
set_code: d.set_code,
|
|
1432
|
-
completed_at: d.completed_at,
|
|
1433
|
-
total_picks: d.total_picks,
|
|
1434
|
-
wins,
|
|
1435
|
-
losses,
|
|
1436
|
-
record: `${wins}-${losses}`,
|
|
1437
|
-
trophy: wins >= 7,
|
|
1438
|
-
};
|
|
1439
|
-
});
|
|
1440
|
-
return jsonResult({
|
|
1441
|
-
success: true,
|
|
1442
|
-
drafts,
|
|
1443
|
-
count: drafts.length,
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
catch (error) {
|
|
1447
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1448
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1449
|
-
}
|
|
1450
|
-
},
|
|
1451
|
-
};
|
|
1452
|
-
// =============================================================================
|
|
1453
|
-
// Intel Reports
|
|
1454
|
-
// =============================================================================
|
|
1455
|
-
const INTEL_TYPES = [
|
|
1456
|
-
'price_spike', 'price_crash', 'format_shakeup', 'sleeper_pick',
|
|
1457
|
-
'deck_tech', 'meta_shift', 'budget_alert', 'spec_target',
|
|
1458
|
-
'correlation', 'event_impact', 'reprint_arb', 'supply_signal',
|
|
1459
|
-
];
|
|
1460
|
-
const INTEL_URGENCIES = ['breaking', 'daily', 'weekly', 'background'];
|
|
1461
|
-
const deckIntelWrite = {
|
|
1462
|
-
definition: {
|
|
1463
|
-
name: 'deck_intel_write',
|
|
1464
|
-
description: 'Write an intel report to the intel_reports table. Used by the deckscry-intel agent to publish analysis results.',
|
|
1465
|
-
annotations: {
|
|
1466
|
-
title: 'Write Intel Report',
|
|
1467
|
-
readOnlyHint: false,
|
|
1468
|
-
destructiveHint: false,
|
|
1469
|
-
openWorldHint: true,
|
|
1470
|
-
},
|
|
1471
|
-
inputSchema: {
|
|
1472
|
-
type: 'object',
|
|
1473
|
-
properties: {
|
|
1474
|
-
type: {
|
|
1475
|
-
type: 'string',
|
|
1476
|
-
enum: [...INTEL_TYPES],
|
|
1477
|
-
description: 'Intel report type',
|
|
1478
|
-
},
|
|
1479
|
-
format: {
|
|
1480
|
-
type: 'string',
|
|
1481
|
-
description: 'MTG format (standard, pioneer, modern, etc.) or null for cross-format',
|
|
1482
|
-
},
|
|
1483
|
-
urgency: {
|
|
1484
|
-
type: 'string',
|
|
1485
|
-
enum: [...INTEL_URGENCIES],
|
|
1486
|
-
description: 'Report urgency (default: daily)',
|
|
1487
|
-
},
|
|
1488
|
-
title: {
|
|
1489
|
-
type: 'string',
|
|
1490
|
-
description: 'Report title — e.g. "Sheoldred spikes 40% after Pioneer RCQ results"',
|
|
1491
|
-
},
|
|
1492
|
-
summary: {
|
|
1493
|
-
type: 'string',
|
|
1494
|
-
description: '2-3 sentence analysis with evidence and reasoning',
|
|
1495
|
-
},
|
|
1496
|
-
cards: {
|
|
1497
|
-
type: 'array',
|
|
1498
|
-
items: { type: 'string' },
|
|
1499
|
-
description: 'Scryfall UUIDs of cards involved',
|
|
1500
|
-
},
|
|
1501
|
-
data: {
|
|
1502
|
-
type: 'object',
|
|
1503
|
-
description: 'Structured payload — analyzer name, confidence, evidence, involved_cards details',
|
|
1504
|
-
},
|
|
1505
|
-
social_hook: {
|
|
1506
|
-
type: 'string',
|
|
1507
|
-
description: 'Pre-written social media hook with specific numbers, timeframe, and why. Under 200 chars.',
|
|
1508
|
-
},
|
|
1509
|
-
},
|
|
1510
|
-
required: ['type', 'title', 'summary'],
|
|
1511
|
-
},
|
|
1512
|
-
},
|
|
1513
|
-
handler: async (args) => {
|
|
1514
|
-
try {
|
|
1515
|
-
const db = getClient();
|
|
1516
|
-
const { data, error } = await db
|
|
1517
|
-
.from('intel_reports')
|
|
1518
|
-
.insert({
|
|
1519
|
-
type: args.type,
|
|
1520
|
-
format: args.format || null,
|
|
1521
|
-
urgency: args.urgency || 'daily',
|
|
1522
|
-
title: args.title,
|
|
1523
|
-
summary: args.summary,
|
|
1524
|
-
cards: args.cards || [],
|
|
1525
|
-
data: args.data || {},
|
|
1526
|
-
social_hook: args.social_hook || null,
|
|
1527
|
-
})
|
|
1528
|
-
.select('id, type, format, urgency, title, created_at')
|
|
1529
|
-
.single();
|
|
1530
|
-
if (error)
|
|
1531
|
-
throw new Error(error.message);
|
|
1532
|
-
return jsonResult({ success: true, report: data });
|
|
1533
|
-
}
|
|
1534
|
-
catch (error) {
|
|
1535
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1536
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1537
|
-
}
|
|
1538
|
-
},
|
|
1539
|
-
};
|
|
1540
|
-
const deckIntelReports = {
|
|
1541
|
-
definition: {
|
|
1542
|
-
name: 'deck_intel_reports',
|
|
1543
|
-
description: 'Read intel reports. Filter by type, format, urgency, or unpublished status. Used by marketer and other consumers.',
|
|
1544
|
-
annotations: {
|
|
1545
|
-
title: 'Read Intel Reports',
|
|
1546
|
-
readOnlyHint: true,
|
|
1547
|
-
destructiveHint: false,
|
|
1548
|
-
openWorldHint: true,
|
|
1549
|
-
},
|
|
1550
|
-
inputSchema: {
|
|
1551
|
-
type: 'object',
|
|
1552
|
-
properties: {
|
|
1553
|
-
type: { type: 'string', description: 'Filter by intel type' },
|
|
1554
|
-
format: { type: 'string', description: 'Filter by MTG format' },
|
|
1555
|
-
urgency: { type: 'string', description: 'Filter by urgency level' },
|
|
1556
|
-
unpublished: { type: 'boolean', description: 'If true, only return reports not yet consumed by marketer' },
|
|
1557
|
-
limit: { type: 'number', description: 'Max reports to return (default: 20)' },
|
|
1558
|
-
},
|
|
1559
|
-
required: [],
|
|
1560
|
-
},
|
|
1561
|
-
},
|
|
1562
|
-
handler: async (args) => {
|
|
1563
|
-
try {
|
|
1564
|
-
const db = getClient();
|
|
1565
|
-
let query = db
|
|
1566
|
-
.from('intel_reports')
|
|
1567
|
-
.select('*')
|
|
1568
|
-
.order('created_at', { ascending: false })
|
|
1569
|
-
.limit(args.limit || 20);
|
|
1570
|
-
if (args.type)
|
|
1571
|
-
query = query.eq('type', args.type);
|
|
1572
|
-
if (args.format)
|
|
1573
|
-
query = query.eq('format', args.format);
|
|
1574
|
-
if (args.urgency)
|
|
1575
|
-
query = query.eq('urgency', args.urgency);
|
|
1576
|
-
if (args.unpublished)
|
|
1577
|
-
query = query.eq('published', false);
|
|
1578
|
-
const { data, error } = await query;
|
|
1579
|
-
if (error)
|
|
1580
|
-
throw new Error(error.message);
|
|
1581
|
-
return jsonResult({
|
|
1582
|
-
success: true,
|
|
1583
|
-
reports: data,
|
|
1584
|
-
count: data?.length || 0,
|
|
1585
|
-
});
|
|
1586
|
-
}
|
|
1587
|
-
catch (error) {
|
|
1588
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1589
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1590
|
-
}
|
|
1591
|
-
},
|
|
1592
|
-
};
|
|
1593
|
-
const deckIntelPublish = {
|
|
1594
|
-
definition: {
|
|
1595
|
-
name: 'deck_intel_publish',
|
|
1596
|
-
description: 'Mark intel reports as published (consumed by marketer). Accepts one or more report IDs.',
|
|
1597
|
-
annotations: {
|
|
1598
|
-
title: 'Mark Intel Published',
|
|
1599
|
-
readOnlyHint: false,
|
|
1600
|
-
destructiveHint: false,
|
|
1601
|
-
openWorldHint: true,
|
|
1602
|
-
},
|
|
1603
|
-
inputSchema: {
|
|
1604
|
-
type: 'object',
|
|
1605
|
-
properties: {
|
|
1606
|
-
report_ids: {
|
|
1607
|
-
type: 'array',
|
|
1608
|
-
items: { type: 'string' },
|
|
1609
|
-
description: 'UUIDs of reports to mark as published',
|
|
1610
|
-
},
|
|
1611
|
-
},
|
|
1612
|
-
required: ['report_ids'],
|
|
1613
|
-
},
|
|
1614
|
-
},
|
|
1615
|
-
handler: async (args) => {
|
|
1616
|
-
try {
|
|
1617
|
-
const db = getClient();
|
|
1618
|
-
const { error } = await db
|
|
1619
|
-
.from('intel_reports')
|
|
1620
|
-
.update({ published: true })
|
|
1621
|
-
.in('id', args.report_ids);
|
|
1622
|
-
if (error)
|
|
1623
|
-
throw new Error(error.message);
|
|
1624
|
-
return jsonResult({ success: true, published: args.report_ids.length });
|
|
1625
|
-
}
|
|
1626
|
-
catch (error) {
|
|
1627
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1628
|
-
return jsonResult({ success: false, error: msg }, true);
|
|
1629
|
-
}
|
|
1630
|
-
},
|
|
1631
|
-
};
|
|
1632
|
-
// =============================================================================
|
|
1633
|
-
// Export
|
|
1634
|
-
// =============================================================================
|
|
1635
|
-
export const deckTools = [
|
|
1636
|
-
deckSearch,
|
|
1637
|
-
deckList,
|
|
1638
|
-
deckSummary,
|
|
1639
|
-
deckHistory,
|
|
1640
|
-
deckFormatDeals,
|
|
1641
|
-
deckTypeSearch,
|
|
1642
|
-
deckPriceBracket,
|
|
1643
|
-
deckSetAnalysis,
|
|
1644
|
-
deckVolatility,
|
|
1645
|
-
deckReprints,
|
|
1646
|
-
deckMatchHistory,
|
|
1647
|
-
deckWinRate,
|
|
1648
|
-
deckCardPerformance,
|
|
1649
|
-
deckRecord,
|
|
1650
|
-
deckPlayerSummary,
|
|
1651
|
-
deckDraftStats,
|
|
1652
|
-
deckIntelWrite,
|
|
1653
|
-
deckIntelReports,
|
|
1654
|
-
deckIntelPublish,
|
|
1655
|
-
];
|
|
1656
|
-
//# sourceMappingURL=deck.js.map
|