pandora-cli-skills 1.1.37 → 1.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/README_FOR_SHARING.md +10 -0
- package/SKILL.md +77 -1
- package/cli/lib/command_executor_service.cjs +51 -1
- package/cli/lib/command_router.cjs +3 -0
- package/cli/lib/error_recovery_service.cjs +73 -1
- package/cli/lib/mcp_tool_registry.cjs +131 -1
- package/cli/lib/parsers/sports_flags.cjs +539 -0
- package/cli/lib/schema_command_service.cjs +201 -0
- package/cli/lib/sports_command_service.cjs +496 -0
- package/cli/lib/sports_consensus_service.cjs +457 -0
- package/cli/lib/sports_creation_service.cjs +232 -0
- package/cli/lib/sports_event_normalizer.cjs +435 -0
- package/cli/lib/sports_provider_registry.cjs +609 -0
- package/cli/lib/sports_resolve_plan_service.cjs +428 -0
- package/cli/lib/sports_sync_service.cjs +490 -0
- package/cli/lib/sports_timing_service.cjs +357 -0
- package/cli/lib/stream_command_service.cjs +67 -0
- package/cli/pandora.cjs +78 -0
- package/package.json +3 -3
- package/tests/cli/sports.integration.test.cjs +188 -0
- package/tests/unit/sports_consensus.test.cjs +103 -0
- package/tests/unit/sports_creation.test.cjs +55 -0
- package/tests/unit/sports_timing.test.cjs +127 -0
package/README.md
CHANGED
|
@@ -49,6 +49,20 @@ pandora --output json trade --dry-run \
|
|
|
49
49
|
pandora stream prices --indexer-url https://pandoraindexer.up.railway.app/ --interval-ms 1000
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### Sports Quickstart
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# list upcoming soccer events
|
|
56
|
+
pandora --output json sports events list --competition <id-or-slug> --limit 5
|
|
57
|
+
|
|
58
|
+
# compute trimmed-median consensus for one event
|
|
59
|
+
pandora --output json sports consensus --event-id <event-id> --trim-percent 20
|
|
60
|
+
|
|
61
|
+
# build conservative create + resolve plans
|
|
62
|
+
pandora --output json sports create plan --event-id <event-id> --selection home
|
|
63
|
+
pandora --output json sports resolve plan --event-id <event-id> --poll-address <0x...>
|
|
64
|
+
```
|
|
65
|
+
|
|
52
66
|
## Fork Mode Notes
|
|
53
67
|
|
|
54
68
|
- Runtime marker is included in payloads: `data.runtime.mode = "fork" | "live"`.
|
package/README_FOR_SHARING.md
CHANGED
|
@@ -51,6 +51,16 @@ Prerequisite: Node.js `>=18`.
|
|
|
51
51
|
- `npm run dry-run:clone`
|
|
52
52
|
- `node cli/pandora.cjs help`
|
|
53
53
|
|
|
54
|
+
## Quickstart (Sports)
|
|
55
|
+
- List soccer events:
|
|
56
|
+
- `pandora --output json sports events list --competition <id-or-slug> --limit 5`
|
|
57
|
+
- Compute sportsbook consensus:
|
|
58
|
+
- `pandora --output json sports consensus --event-id <event-id> --trim-percent 20`
|
|
59
|
+
- Build creation plan:
|
|
60
|
+
- `pandora --output json sports create plan --event-id <event-id> --selection home`
|
|
61
|
+
- Build manual resolve recommendation:
|
|
62
|
+
- `pandora --output json sports resolve plan --event-id <event-id> --poll-address <0x...>`
|
|
63
|
+
|
|
54
64
|
## New CLI capabilities
|
|
55
65
|
- Global machine-readable output:
|
|
56
66
|
- `pandora --output json doctor`
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pandora-cli-skills
|
|
3
3
|
summary: Canonical skill and operator guide for Pandora CLI including mirror, polymarket, resolve, and LP flows.
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.39
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Pandora CLI & Skills
|
|
@@ -83,6 +83,7 @@ pandora [--output table|json] doctor [--dotenv-path <path>] [--skip-dotenv] [--c
|
|
|
83
83
|
pandora [--output table|json] setup [--force] [--dotenv-path <path>] [--example <path>] [--check-usdc-code] [--check-polymarket] [--rpc-timeout-ms <ms>]
|
|
84
84
|
pandora [--output table|json] markets list [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-by <field>] [--order-direction asc|desc] [--chain-id <id>] [--creator <address>] [--poll-address <address>] [--market-type <type>] [--where-json <json>] [--active|--resolved|--expiring-soon] [--expiring-hours <n>] [--expand] [--with-odds]
|
|
85
85
|
pandora [--output table|json] markets get [--id <id> ...] [--stdin]
|
|
86
|
+
pandora [--output table|json] sports books list|events list|events live|odds snapshot|consensus|create plan|create run|sync once|sync run|sync start|sync stop|sync status|resolve plan [flags]
|
|
86
87
|
pandora [--output table|json] polls list [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-by <field>] [--order-direction asc|desc] [--chain-id <id>] [--creator <address>] [--status <int>] [--category <int>] [--question-contains <text>] [--where-json <json>]
|
|
87
88
|
pandora [--output table|json] polls get --id <id>
|
|
88
89
|
pandora [--output table|json] events list [--type all|liquidity|oracle-fee|claim] [--limit <n>] [--after <cursor>] [--before <cursor>] [--order-direction asc|desc] [--chain-id <id>] [--wallet <address>] [--market-address <address>] [--poll-address <address>] [--tx-hash <hash>]
|
|
@@ -144,6 +145,81 @@ preflight [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--rpc-url <url
|
|
|
144
145
|
trade --condition-id <id>|--slug <slug>|--token-id <id> --token yes|no --amount-usdc <n> --dry-run|--execute [--side buy|sell] [--polymarket-host <url>] [--polymarket-mock-url <url>] [--timeout-ms <ms>] [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--funder <address>]
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
## Sports command matrix
|
|
149
|
+
| Command | Purpose | Primary flags |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `sports books list` | Show sportsbook provider health and active book preference list. | `--provider`, `--book-priority`, `--timeout-ms` |
|
|
152
|
+
| `sports events list` | List normalized soccer events. | `--competition`, `--kickoff-after`, `--kickoff-before`, `--limit` |
|
|
153
|
+
| `sports events live` | List only live/in-play events. | `--competition`, `--limit`, `--provider` |
|
|
154
|
+
| `sports odds snapshot` | Fetch event odds snapshot plus consensus context. | `--event-id`, `--trim-percent`, `--min-tier1-books`, `--min-total-books` |
|
|
155
|
+
| `sports consensus` | Compute trimmed-median consensus from live or offline checks. | `--event-id` or `--checks-json`, `--trim-percent`, `--book-priority` |
|
|
156
|
+
| `sports create plan` | Build conservative creation plan and safety gates. | `--event-id`, `--selection`, `--market-type`, `--creation-window-open-min`, `--creation-window-close-min` |
|
|
157
|
+
| `sports create run` | Dry-run or execute creation path. | `--event-id`, `--dry-run/--execute`, `--liquidity-usdc`, `--chain-id`, `--rpc-url` |
|
|
158
|
+
| `sports sync once|run|start|stop|status` | Evaluate and operate sports sync runtime state. | `--event-id` (required for `once|run|start`), `--risk-profile`, `--state-file`, `--paper/--execute-live` |
|
|
159
|
+
| `sports resolve plan` | Build manual-final resolution recommendation. | `--event-id` or `--checks-json/--checks-file`, `--poll-address`, `--settle-delay-ms`, `--consecutive-checks-required` |
|
|
160
|
+
|
|
161
|
+
## Sports consensus policy
|
|
162
|
+
- Odds are normalized to implied probability from decimal/American/fractional inputs.
|
|
163
|
+
- Consensus method is `trimmed-median` (v1), with default `--trim-percent 20`.
|
|
164
|
+
- Conservative coverage inputs default to `--min-tier1-books 3` and `--min-total-books 6`.
|
|
165
|
+
- Confidence can degrade when coverage policy is not satisfied.
|
|
166
|
+
- Consensus payload includes:
|
|
167
|
+
- `method`
|
|
168
|
+
- `tier1Coverage`
|
|
169
|
+
- `totalBooks`
|
|
170
|
+
- `includedBooks`
|
|
171
|
+
- `excludedBooks`
|
|
172
|
+
- `outliers`
|
|
173
|
+
- `consensusYesPct`
|
|
174
|
+
- `consensusNoPct`
|
|
175
|
+
|
|
176
|
+
## Sports timing policy
|
|
177
|
+
- Creation planning defaults:
|
|
178
|
+
- `--creation-window-open-min 1440` (24h before kickoff)
|
|
179
|
+
- `--creation-window-close-min 90` (90m before kickoff)
|
|
180
|
+
- Core timing module defaults (spec-level fallbacks):
|
|
181
|
+
- creation open lead `7d`, creation close lead `15m`
|
|
182
|
+
- assumed event duration `3h`
|
|
183
|
+
- resolve open delay `30m`, resolve target delay `2h`, resolve close delay `48h`
|
|
184
|
+
- Resolve plan safety defaults:
|
|
185
|
+
- `--settle-delay-ms 600000` (10m)
|
|
186
|
+
- `--consecutive-checks-required 2`
|
|
187
|
+
|
|
188
|
+
## Manual resolve workflow (sports)
|
|
189
|
+
1. Build resolve plan:
|
|
190
|
+
- `pandora --output json sports resolve plan --event-id <id> --poll-address <0x...>`
|
|
191
|
+
2. Confirm plan safety:
|
|
192
|
+
- require `safeToResolve=true`
|
|
193
|
+
- read `recommendedAnswer`, `stableWindowStartAt`, and diagnostics
|
|
194
|
+
3. Execute resolution:
|
|
195
|
+
- run `recommendedCommand` when present
|
|
196
|
+
- or run manual command: `pandora resolve --poll-address <0x...> --answer yes|no|invalid --reason "<text>" --execute`
|
|
197
|
+
4. If unsafe:
|
|
198
|
+
- continue collecting checks (`--checks-json`/`--checks-file`) and rerun until safety gates pass.
|
|
199
|
+
|
|
200
|
+
## Sports sync risk defaults
|
|
201
|
+
- Conservative (`--risk-profile conservative`):
|
|
202
|
+
- `maxDataAgeMs=120000`
|
|
203
|
+
- `minCoverageRatio=0.70`
|
|
204
|
+
- `maxCoverageDropRatio=0.25`
|
|
205
|
+
- `maxSpreadJumpBps=150`
|
|
206
|
+
- `maxConsecutiveFailures=3`
|
|
207
|
+
- `maxConsecutiveGateFailures=2`
|
|
208
|
+
- Balanced:
|
|
209
|
+
- `maxDataAgeMs=150000`
|
|
210
|
+
- `minCoverageRatio=0.60`
|
|
211
|
+
- `maxCoverageDropRatio=0.30`
|
|
212
|
+
- `maxSpreadJumpBps=200`
|
|
213
|
+
- `maxConsecutiveFailures=4`
|
|
214
|
+
- `maxConsecutiveGateFailures=3`
|
|
215
|
+
- Aggressive:
|
|
216
|
+
- `maxDataAgeMs=180000`
|
|
217
|
+
- `minCoverageRatio=0.50`
|
|
218
|
+
- `maxCoverageDropRatio=0.40`
|
|
219
|
+
- `maxSpreadJumpBps=250`
|
|
220
|
+
- `maxConsecutiveFailures=5`
|
|
221
|
+
- `maxConsecutiveGateFailures=4`
|
|
222
|
+
|
|
147
223
|
## Read-only indexer commands
|
|
148
224
|
Indexer URL resolution order:
|
|
149
225
|
1. `--indexer-url`
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { spawnSync } = require('child_process');
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Normalize unknown error-ish values into a loggable message string.
|
|
6
|
+
* @param {*} value
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
4
9
|
function coerceErrorMessage(value) {
|
|
5
10
|
if (typeof value === 'string') return value;
|
|
6
11
|
if (value && typeof value.message === 'string') return value.message;
|
|
@@ -11,6 +16,12 @@ function coerceErrorMessage(value) {
|
|
|
11
16
|
}
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Parse a candidate JSON envelope from raw process output.
|
|
21
|
+
* Accepts only objects that include a boolean `ok` field.
|
|
22
|
+
* @param {string} text
|
|
23
|
+
* @returns {object|null}
|
|
24
|
+
*/
|
|
14
25
|
function parseEnvelopeCandidate(text) {
|
|
15
26
|
const candidate = String(text || '').trim();
|
|
16
27
|
if (!candidate) return null;
|
|
@@ -25,6 +36,12 @@ function parseEnvelopeCandidate(text) {
|
|
|
25
36
|
return null;
|
|
26
37
|
}
|
|
27
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Extract the last syntactically valid top-level JSON object from text.
|
|
41
|
+
* Useful when diagnostics/noise precede the structured CLI envelope.
|
|
42
|
+
* @param {string} text
|
|
43
|
+
* @returns {object|null}
|
|
44
|
+
*/
|
|
28
45
|
function extractLastJsonObject(text) {
|
|
29
46
|
const source = String(text || '');
|
|
30
47
|
if (!source) return null;
|
|
@@ -70,6 +87,12 @@ function extractLastJsonObject(text) {
|
|
|
70
87
|
return parseEnvelopeCandidate(last);
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Parse a CLI JSON envelope from stdout/stderr with robust fallbacks.
|
|
92
|
+
* @param {string} stdout
|
|
93
|
+
* @param {string} stderr
|
|
94
|
+
* @returns {object|null}
|
|
95
|
+
*/
|
|
73
96
|
function parseEnvelopeFromOutput(stdout, stderr) {
|
|
74
97
|
const candidates = [stdout, stderr, `${stdout || ''}\n${stderr || ''}`];
|
|
75
98
|
for (const candidate of candidates) {
|
|
@@ -81,10 +104,31 @@ function parseEnvelopeFromOutput(stdout, stderr) {
|
|
|
81
104
|
return null;
|
|
82
105
|
}
|
|
83
106
|
|
|
107
|
+
/**
|
|
108
|
+
* @typedef {object} ExecutorEnvelope
|
|
109
|
+
* @property {boolean} ok
|
|
110
|
+
* @property {string} [command]
|
|
111
|
+
* @property {object} [data]
|
|
112
|
+
* @property {{code: string, message: string, details?: object}} [error]
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @typedef {object} ExecuteJsonCommandResult
|
|
117
|
+
* @property {boolean} ok
|
|
118
|
+
* @property {number} exitCode
|
|
119
|
+
* @property {string} stdout
|
|
120
|
+
* @property {string} stderr
|
|
121
|
+
* @property {ExecutorEnvelope} envelope
|
|
122
|
+
*/
|
|
123
|
+
|
|
84
124
|
/**
|
|
85
125
|
* Build a child-process command executor for JSON-mode CLI invocations.
|
|
126
|
+
* The executor always prepends `--output json` and normalizes failures into an envelope shape.
|
|
86
127
|
* @param {{cliPath?: string, defaultTimeoutMs?: number, env?: object}} [options]
|
|
87
|
-
* @returns {{
|
|
128
|
+
* @returns {{
|
|
129
|
+
* executeJsonCommand: (commandArgs: string[], runtime?: {timeoutMs?: number, env?: object}) => ExecuteJsonCommandResult,
|
|
130
|
+
* coerceErrorMessage: (value: *) => string
|
|
131
|
+
* }}
|
|
88
132
|
*/
|
|
89
133
|
function createCommandExecutorService(options = {}) {
|
|
90
134
|
const cliPath =
|
|
@@ -94,6 +138,12 @@ function createCommandExecutorService(options = {}) {
|
|
|
94
138
|
const defaultTimeoutMs = Number.isFinite(options.defaultTimeoutMs) ? Math.max(1_000, Math.trunc(options.defaultTimeoutMs)) : 60_000;
|
|
95
139
|
const baseEnv = options.env && typeof options.env === 'object' ? options.env : process.env;
|
|
96
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Execute the CLI synchronously and parse the envelope from process output.
|
|
143
|
+
* @param {string[]} commandArgs
|
|
144
|
+
* @param {{timeoutMs?: number, env?: object}} [runtime]
|
|
145
|
+
* @returns {ExecuteJsonCommandResult}
|
|
146
|
+
*/
|
|
97
147
|
function executeJsonCommand(commandArgs, runtime = {}) {
|
|
98
148
|
const timeoutMs = Number.isFinite(runtime.timeoutMs)
|
|
99
149
|
? Math.max(1_000, Math.trunc(runtime.timeoutMs))
|
|
@@ -18,6 +18,7 @@ function createCommandRouter(deps = {}) {
|
|
|
18
18
|
runSetup,
|
|
19
19
|
runMarketsCommand,
|
|
20
20
|
runScanCommand,
|
|
21
|
+
runSportsCommand,
|
|
21
22
|
runQuoteCommand,
|
|
22
23
|
runTradeCommand,
|
|
23
24
|
runPollsCommand,
|
|
@@ -65,6 +66,7 @@ function createCommandRouter(deps = {}) {
|
|
|
65
66
|
requireFn('runSetup', runSetup);
|
|
66
67
|
requireFn('runMarketsCommand', runMarketsCommand);
|
|
67
68
|
requireFn('runScanCommand', runScanCommand);
|
|
69
|
+
requireFn('runSportsCommand', runSportsCommand);
|
|
68
70
|
requireFn('runQuoteCommand', runQuoteCommand);
|
|
69
71
|
requireFn('runTradeCommand', runTradeCommand);
|
|
70
72
|
requireFn('runPollsCommand', runPollsCommand);
|
|
@@ -153,6 +155,7 @@ function createCommandRouter(deps = {}) {
|
|
|
153
155
|
},
|
|
154
156
|
markets: async (handlerArgs, handlerContext) => runMarketsCommand(handlerArgs, handlerContext),
|
|
155
157
|
scan: async (handlerArgs, handlerContext) => runScanCommand(handlerArgs, handlerContext),
|
|
158
|
+
sports: async (handlerArgs, handlerContext) => runSportsCommand(handlerArgs, handlerContext),
|
|
156
159
|
quote: async (handlerArgs, handlerContext) => runQuoteCommand(handlerArgs, handlerContext),
|
|
157
160
|
trade: async (handlerArgs, handlerContext) => runTradeCommand(handlerArgs, handlerContext),
|
|
158
161
|
polls: async (handlerArgs, handlerContext) => runPollsCommand(handlerArgs, handlerContext),
|
|
@@ -51,6 +51,38 @@ function buildMcpRestartCommand(cliName) {
|
|
|
51
51
|
return `${cliName} mcp`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function buildMcpBoundedCommand(cliName, details) {
|
|
55
|
+
const toolName = cleanToken(details && details.toolName, '');
|
|
56
|
+
if (toolName.startsWith('sports.sync.')) {
|
|
57
|
+
return `${cliName} sports sync once --help`;
|
|
58
|
+
}
|
|
59
|
+
if (toolName.startsWith('mirror.sync.')) {
|
|
60
|
+
return `${cliName} mirror sync once --help`;
|
|
61
|
+
}
|
|
62
|
+
if (toolName.startsWith('autopilot.')) {
|
|
63
|
+
return `${cliName} autopilot once --help`;
|
|
64
|
+
}
|
|
65
|
+
if (toolName.startsWith('watch')) {
|
|
66
|
+
return `${cliName} watch --help`;
|
|
67
|
+
}
|
|
68
|
+
return `${cliName} help`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildSportsConsensusRetryCommand(cliName, details) {
|
|
72
|
+
const eventId = cleanToken(details && details.eventId, '<event-id>');
|
|
73
|
+
return `${cliName} sports consensus --event-id ${eventId}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildSportsCreatePlanCommand(cliName, details) {
|
|
77
|
+
const eventId = cleanToken(details && details.eventId, '<event-id>');
|
|
78
|
+
return `${cliName} sports create plan --event-id ${eventId}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildSportsResolvePlanRetryCommand(cliName, details) {
|
|
82
|
+
const eventId = cleanToken(details && details.eventId, '<event-id>');
|
|
83
|
+
return `${cliName} sports resolve plan --event-id ${eventId}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
/**
|
|
55
87
|
* Build deterministic Next-Best-Action recovery hints for JSON errors.
|
|
56
88
|
* @param {{cliName?: string}} [options]
|
|
@@ -124,7 +156,7 @@ function createErrorRecoveryService(options = {}) {
|
|
|
124
156
|
case 'MCP_LONG_RUNNING_MODE_BLOCKED':
|
|
125
157
|
return {
|
|
126
158
|
action: 'Switch to a bounded command variant for MCP',
|
|
127
|
-
command:
|
|
159
|
+
command: buildMcpBoundedCommand(cliName, details),
|
|
128
160
|
retryable: true,
|
|
129
161
|
};
|
|
130
162
|
case 'MCP_TOOL_FAILED':
|
|
@@ -134,6 +166,46 @@ function createErrorRecoveryService(options = {}) {
|
|
|
134
166
|
command: `${cliName} --output json schema`,
|
|
135
167
|
retryable: true,
|
|
136
168
|
};
|
|
169
|
+
case 'SPORTS_PROVIDER_NOT_CONFIGURED':
|
|
170
|
+
case 'SPORTS_PROVIDER_FETCH_MISSING':
|
|
171
|
+
case 'SPORTS_LIST_COMPETITIONS_FAILED':
|
|
172
|
+
case 'SPORTS_LIST_EVENTS_FAILED':
|
|
173
|
+
case 'SPORTS_GET_EVENT_ODDS_FAILED':
|
|
174
|
+
case 'SPORTS_GET_EVENT_STATUS_FAILED':
|
|
175
|
+
return {
|
|
176
|
+
action: 'Retry sportsbook query with explicit provider settings',
|
|
177
|
+
command: `${cliName} sports books list --provider auto`,
|
|
178
|
+
retryable: true,
|
|
179
|
+
};
|
|
180
|
+
case 'SPORTS_PROVIDER_TIMEOUT':
|
|
181
|
+
case 'SPORTS_PROVIDER_REQUEST_FAILED':
|
|
182
|
+
case 'SPORTS_PROVIDER_HTTP_ERROR':
|
|
183
|
+
case 'SPORTS_PROVIDER_INVALID_JSON':
|
|
184
|
+
return {
|
|
185
|
+
action: 'Retry sportsbook read path with backup provider',
|
|
186
|
+
command: `${cliName} sports events list --provider backup --limit 10`,
|
|
187
|
+
retryable: true,
|
|
188
|
+
};
|
|
189
|
+
case 'SPORTS_CONSENSUS_FAILED':
|
|
190
|
+
case 'SPORTS_CONSENSUS_UNAVAILABLE':
|
|
191
|
+
return {
|
|
192
|
+
action: 'Re-run consensus for the target event',
|
|
193
|
+
command: buildSportsConsensusRetryCommand(cliName, details),
|
|
194
|
+
retryable: true,
|
|
195
|
+
};
|
|
196
|
+
case 'SPORTS_CREATE_BLOCKED':
|
|
197
|
+
case 'SPORTS_CREATE_FAILED':
|
|
198
|
+
return {
|
|
199
|
+
action: 'Rebuild creation plan and adjust timing/coverage inputs',
|
|
200
|
+
command: buildSportsCreatePlanCommand(cliName, details),
|
|
201
|
+
retryable: true,
|
|
202
|
+
};
|
|
203
|
+
case 'SPORTS_RESOLVE_PLAN_UNSAFE':
|
|
204
|
+
return {
|
|
205
|
+
action: 'Wait for stable final status and retry resolve plan',
|
|
206
|
+
command: buildSportsResolvePlanRetryCommand(cliName, details),
|
|
207
|
+
retryable: true,
|
|
208
|
+
};
|
|
137
209
|
case 'MISSING_REQUIRED_FLAG':
|
|
138
210
|
case 'MISSING_FLAG_VALUE':
|
|
139
211
|
case 'INVALID_FLAG_VALUE':
|
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {string|number|boolean|null|undefined|Array<string|number|boolean>} FlagInputValue
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{[flagName: string]: FlagInputValue}} ToolFlags
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* name: string,
|
|
12
|
+
* command: string[],
|
|
13
|
+
* description: string,
|
|
14
|
+
* mutating?: boolean,
|
|
15
|
+
* safeFlags?: string[],
|
|
16
|
+
* executeFlags?: string[],
|
|
17
|
+
* longRunningBlocked?: boolean
|
|
18
|
+
* }} ToolDefinition
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{
|
|
23
|
+
* positionals?: Array<string|number|boolean>,
|
|
24
|
+
* flags?: ToolFlags,
|
|
25
|
+
* intent?: { execute?: boolean }
|
|
26
|
+
* }} ToolInvocationArgs
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalize a flag token to a CLI-prefixed name.
|
|
31
|
+
* Leaves existing `--foo`/`-f` tokens unchanged and prefixes bare names.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} name Raw flag key.
|
|
34
|
+
* @returns {string} Normalized CLI flag token or empty string.
|
|
35
|
+
*/
|
|
1
36
|
function normalizeFlagName(name) {
|
|
2
37
|
const normalized = String(name || '').trim();
|
|
3
38
|
if (!normalized) return '';
|
|
@@ -6,6 +41,13 @@ function normalizeFlagName(name) {
|
|
|
6
41
|
return `--${normalized}`;
|
|
7
42
|
}
|
|
8
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Convert a flag value (including nested arrays) into scalar CLI values.
|
|
46
|
+
* Booleans are preserved so callers can emit bare switch flags.
|
|
47
|
+
*
|
|
48
|
+
* @param {FlagInputValue} value Flag value candidate.
|
|
49
|
+
* @returns {Array<string|boolean>} Flattened scalar values.
|
|
50
|
+
*/
|
|
9
51
|
function flagValueToStrings(value) {
|
|
10
52
|
if (value === null || value === undefined) return [];
|
|
11
53
|
if (Array.isArray(value)) {
|
|
@@ -17,6 +59,13 @@ function flagValueToStrings(value) {
|
|
|
17
59
|
return [String(value)];
|
|
18
60
|
}
|
|
19
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Build argv segments from a JSON-style flags map.
|
|
64
|
+
* Truthy booleans produce bare switch flags; scalar values produce pairs.
|
|
65
|
+
*
|
|
66
|
+
* @param {ToolFlags} flags Flag map where keys may be with/without `--`.
|
|
67
|
+
* @returns {string[]} CLI argv segment for flags.
|
|
68
|
+
*/
|
|
20
69
|
function buildFlagArgv(flags) {
|
|
21
70
|
if (!flags || typeof flags !== 'object') return [];
|
|
22
71
|
const argv = [];
|
|
@@ -37,6 +86,13 @@ function buildFlagArgv(flags) {
|
|
|
37
86
|
return argv;
|
|
38
87
|
}
|
|
39
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Collect normalized flags that are meaningfully provided by the caller.
|
|
91
|
+
* Used by execution guardrails to detect safe/live mode intent flags.
|
|
92
|
+
*
|
|
93
|
+
* @param {ToolFlags} flags Candidate flags object.
|
|
94
|
+
* @returns {Set<string>} Set of normalized provided flag names.
|
|
95
|
+
*/
|
|
40
96
|
function providedFlagSet(flags) {
|
|
41
97
|
const set = new Set();
|
|
42
98
|
if (!flags || typeof flags !== 'object' || Array.isArray(flags)) return set;
|
|
@@ -53,6 +109,12 @@ function providedFlagSet(flags) {
|
|
|
53
109
|
return set;
|
|
54
110
|
}
|
|
55
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Convert a tool definition to MCP descriptor format.
|
|
114
|
+
*
|
|
115
|
+
* @param {ToolDefinition} definition Tool registration definition.
|
|
116
|
+
* @returns {{name: string, description: string, inputSchema: object}} MCP tool descriptor.
|
|
117
|
+
*/
|
|
56
118
|
function toToolDescriptor(definition) {
|
|
57
119
|
return {
|
|
58
120
|
name: definition.name,
|
|
@@ -104,6 +166,7 @@ function toToolDescriptor(definition) {
|
|
|
104
166
|
};
|
|
105
167
|
}
|
|
106
168
|
|
|
169
|
+
/** @type {ToolDefinition[]} */
|
|
107
170
|
const TOOL_DEFINITIONS = [
|
|
108
171
|
{ name: 'help', command: ['help'], description: 'Show top-level command help.' },
|
|
109
172
|
{ name: 'version', command: ['version'], description: 'Return the installed CLI version.' },
|
|
@@ -112,6 +175,54 @@ const TOOL_DEFINITIONS = [
|
|
|
112
175
|
{ name: 'markets.list', command: ['markets', 'list'], description: 'List Pandora markets with filters.' },
|
|
113
176
|
{ name: 'markets.get', command: ['markets', 'get'], description: 'Get one or more markets by id.' },
|
|
114
177
|
{ name: 'scan', command: ['scan'], description: 'Scan markets with lifecycle filters.' },
|
|
178
|
+
{ name: 'sports.books.list', command: ['sports', 'books', 'list'], description: 'List sportsbook provider health and configured book priorities.' },
|
|
179
|
+
{ name: 'sports.events.list', command: ['sports', 'events', 'list'], description: 'List normalized soccer events from sportsbook providers.' },
|
|
180
|
+
{ name: 'sports.events.live', command: ['sports', 'events', 'live'], description: 'List currently-live soccer events from sportsbook providers.' },
|
|
181
|
+
{ name: 'sports.odds.snapshot', command: ['sports', 'odds', 'snapshot'], description: 'Get normalized event odds snapshot and consensus.' },
|
|
182
|
+
{ name: 'sports.consensus', command: ['sports', 'consensus'], description: 'Compute majority-book trimmed-median consensus for one event.' },
|
|
183
|
+
{ name: 'sports.create.plan', command: ['sports', 'create', 'plan'], description: 'Build conservative market creation plan from sportsbook consensus.' },
|
|
184
|
+
{
|
|
185
|
+
name: 'sports.create.run',
|
|
186
|
+
command: ['sports', 'create', 'run'],
|
|
187
|
+
description: 'Execute or dry-run sports market creation.',
|
|
188
|
+
mutating: true,
|
|
189
|
+
safeFlags: ['--dry-run', '--paper'],
|
|
190
|
+
executeFlags: ['--execute'],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'sports.sync.once',
|
|
194
|
+
command: ['sports', 'sync', 'once'],
|
|
195
|
+
description: 'Run one sports sync evaluation tick.',
|
|
196
|
+
mutating: true,
|
|
197
|
+
safeFlags: ['--paper', '--dry-run'],
|
|
198
|
+
executeFlags: ['--execute-live', '--execute'],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'sports.sync.run',
|
|
202
|
+
command: ['sports', 'sync', 'run'],
|
|
203
|
+
description: 'Continuous sports sync loop (blocked in MCP v1).',
|
|
204
|
+
longRunningBlocked: true,
|
|
205
|
+
mutating: true,
|
|
206
|
+
safeFlags: ['--paper', '--dry-run'],
|
|
207
|
+
executeFlags: ['--execute-live', '--execute'],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'sports.sync.start',
|
|
211
|
+
command: ['sports', 'sync', 'start'],
|
|
212
|
+
description: 'Start detached sports sync runtime (blocked in MCP v1).',
|
|
213
|
+
longRunningBlocked: true,
|
|
214
|
+
mutating: true,
|
|
215
|
+
safeFlags: ['--paper', '--dry-run'],
|
|
216
|
+
executeFlags: ['--execute-live', '--execute'],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'sports.sync.stop',
|
|
220
|
+
command: ['sports', 'sync', 'stop'],
|
|
221
|
+
description: 'Stop sports sync runtime.',
|
|
222
|
+
mutating: true,
|
|
223
|
+
},
|
|
224
|
+
{ name: 'sports.sync.status', command: ['sports', 'sync', 'status'], description: 'Inspect sports sync runtime status.' },
|
|
225
|
+
{ name: 'sports.resolve.plan', command: ['sports', 'resolve', 'plan'], description: 'Build manual-final resolution recommendation.' },
|
|
115
226
|
{ name: 'quote', command: ['quote'], description: 'Build YES/NO quote estimates.' },
|
|
116
227
|
{
|
|
117
228
|
name: 'trade',
|
|
@@ -276,15 +387,33 @@ const TOOL_DEFINITIONS = [
|
|
|
276
387
|
|
|
277
388
|
/**
|
|
278
389
|
* Registry for MCP-exposed Pandora tools with execution guardrails.
|
|
279
|
-
*
|
|
390
|
+
*
|
|
391
|
+
* @returns {{
|
|
392
|
+
* listTools: () => object[],
|
|
393
|
+
* prepareInvocation: (toolName: string, args?: ToolInvocationArgs) => {argv: string[]},
|
|
394
|
+
* hasTool: (toolName: string) => boolean
|
|
395
|
+
* }} MCP tool registry API.
|
|
280
396
|
*/
|
|
281
397
|
function createMcpToolRegistry() {
|
|
282
398
|
const byName = new Map(TOOL_DEFINITIONS.map((definition) => [definition.name, definition]));
|
|
283
399
|
|
|
400
|
+
/**
|
|
401
|
+
* List all MCP-exposed Pandora tools and their shared JSON contract.
|
|
402
|
+
*
|
|
403
|
+
* @returns {object[]} Tool descriptors.
|
|
404
|
+
*/
|
|
284
405
|
function listTools() {
|
|
285
406
|
return TOOL_DEFINITIONS.map((definition) => toToolDescriptor(definition));
|
|
286
407
|
}
|
|
287
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Validate and convert a tool invocation request into Pandora CLI argv.
|
|
411
|
+
* Applies guardrails for unknown/blocked tools and mutating intent rules.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} toolName Registered MCP tool name.
|
|
414
|
+
* @param {ToolInvocationArgs} [args={}] Invocation payload from MCP client.
|
|
415
|
+
* @returns {{argv: string[]}} Prepared command argv (without binary prefix).
|
|
416
|
+
*/
|
|
288
417
|
function prepareInvocation(toolName, args = {}) {
|
|
289
418
|
if (toolName === 'launch' || toolName === 'clone-bet') {
|
|
290
419
|
const unsupported = new Error(
|
|
@@ -310,6 +439,7 @@ function createMcpToolRegistry() {
|
|
|
310
439
|
);
|
|
311
440
|
blocked.code = 'MCP_LONG_RUNNING_MODE_BLOCKED';
|
|
312
441
|
blocked.details = {
|
|
442
|
+
toolName: definition.name,
|
|
313
443
|
hints: ['Use the non-long-running variant (for example, *.once) or call the CLI directly outside MCP.'],
|
|
314
444
|
};
|
|
315
445
|
throw blocked;
|