pandora-cli-skills 1.1.42 → 1.1.43

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/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.42
4
+ version: 1.1.43
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -93,7 +93,7 @@ pandora [--output table|json] markets list [--limit <n>] [--after <cursor>] [--b
93
93
  pandora [--output table|json] markets get [--id <id> ...] [--stdin]
94
94
  pandora [--output table|json] sports books list|events list|events live|odds snapshot|odds bulk|consensus|create plan|create run|sync once|sync run|sync start|sync stop|sync status|resolve plan [flags]
95
95
  pandora [--output table|json] lifecycle start --config <path>|status --id <id>|resolve --id <id> --confirm
96
- pandora arb scan --markets <csv> --output ndjson [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--iterations <n>] [--interval-ms <ms>]
96
+ pandora arb scan --markets <csv> --output ndjson|json [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--iterations <n>] [--interval-ms <ms>]
97
97
  pandora [--output table|json] odds record --competition <id> --interval <sec> [--max-samples <n>] [--event-id <id>] [--venues pandora_amm,polymarket]
98
98
  pandora [--output table|json] odds history --event-id <id> --output csv|json [--limit <n>]
99
99
  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>]
@@ -443,6 +443,12 @@ Common structured error codes for automation:
443
443
  - `INDEXER_HTTP_ERROR` / `INDEXER_TIMEOUT`: indexer transport failure.
444
444
  - `MIRROR_*`: mirror pipeline/service failures (plan/deploy/verify/sync/go/status).
445
445
  - `POLYMARKET_*`: Polymarket resolution/auth/order/preflight failures.
446
+ - `RISK_*`: risk state/guardrail/panic failures (`RISK_PANIC_ACTIVE`, `RISK_GUARDRAIL_BLOCKED`, etc.).
447
+ - `LIFECYCLE_*`: lifecycle state-machine file operations (`LIFECYCLE_EXISTS`, `LIFECYCLE_NOT_FOUND`).
448
+ - `ODDS_*`: odds record/history storage and connector failures.
449
+ - `ARB_*`: arb scan parsing/execution output-mode failures.
450
+ - `CONFIG_*`: config read/parse failures (for example `CONFIG_FILE_NOT_FOUND`).
451
+ - `MCP_FILE_ACCESS_BLOCKED`: MCP-mode file path denied outside workspace root.
446
452
  - `WEBHOOK_DELIVERY_FAILED`: webhook hard-fail when `--fail-on-webhook-error` is set.
447
453
 
448
454
  Error envelope:
@@ -14,7 +14,7 @@ const ARB_MARKET_FIELDS = [
14
14
  ];
15
15
 
16
16
  const ARB_USAGE =
17
- 'pandora arb scan --markets <csv> --output ndjson [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
17
+ 'pandora arb scan --markets <csv> --output ndjson|json [--min-net-spread-pct <n>] [--fee-pct-per-leg <n>] [--amount-usdc <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
18
18
 
19
19
  function requireDep(deps, name) {
20
20
  if (!deps || typeof deps[name] !== 'function') {
@@ -210,8 +210,8 @@ function parseArbScanFlags(args, deps) {
210
210
  throw new CliError('MISSING_REQUIRED_FLAG', 'arb scan requires at least two markets via --markets <csv>.');
211
211
  }
212
212
 
213
- if (options.output !== 'ndjson') {
214
- throw new CliError('INVALID_FLAG_VALUE', 'arb scan currently supports only --output ndjson.');
213
+ if (!['ndjson', 'json'].includes(options.output)) {
214
+ throw new CliError('INVALID_FLAG_VALUE', 'arb scan supports --output ndjson|json.');
215
215
  }
216
216
 
217
217
  if (options.minNetSpreadPct < 0) {
@@ -291,8 +291,29 @@ function createRunArbCommand(deps) {
291
291
  parsePositiveInteger,
292
292
  });
293
293
 
294
+ const mcpMode = String(process.env.PANDORA_MCP_MODE || '').trim() === '1';
295
+ if (mcpMode && options.output !== 'json') {
296
+ throw new CliError('MCP_LONG_RUNNING_MODE_BLOCKED', 'arb scan via MCP requires --output json and a bounded iteration count.', {
297
+ toolName: 'arb.scan',
298
+ hints: ['Use arb.scan with --output json --iterations 1 when calling via MCP.'],
299
+ });
300
+ }
301
+
302
+ if (options.output === 'json' && !Number.isInteger(options.iterations)) {
303
+ options.iterations = 1;
304
+ }
305
+
306
+ if (options.output === 'json' && Number.isInteger(options.iterations) && options.iterations > 1) {
307
+ const code = mcpMode ? 'MCP_LONG_RUNNING_MODE_BLOCKED' : 'INVALID_FLAG_VALUE';
308
+ throw new CliError(code, 'arb scan --output json supports only --iterations 1.', {
309
+ toolName: 'arb.scan',
310
+ hints: ['Use --output ndjson for streaming multi-iteration scans.'],
311
+ });
312
+ }
313
+
294
314
  const maxIterations = Number.isInteger(options.iterations) ? options.iterations : Number.POSITIVE_INFINITY;
295
315
  let iteration = 0;
316
+ const snapshots = [];
296
317
 
297
318
  while (iteration < maxIterations) {
298
319
  iteration += 1;
@@ -308,23 +329,58 @@ function createRunArbCommand(deps) {
308
329
  amountUsdc: options.amountUsdc,
309
330
  });
310
331
 
311
- for (const opportunity of opportunities) {
312
- // eslint-disable-next-line no-console
313
- console.log(
314
- JSON.stringify({
315
- type: 'arb.scan.opportunity',
316
- timestamp: new Date().toISOString(),
317
- iteration,
318
- indexerUrl,
319
- ...opportunity,
320
- }),
321
- );
332
+ if (options.output === 'ndjson') {
333
+ for (const opportunity of opportunities) {
334
+ // eslint-disable-next-line no-console
335
+ console.log(
336
+ JSON.stringify({
337
+ type: 'arb.scan.opportunity',
338
+ timestamp: new Date().toISOString(),
339
+ iteration,
340
+ indexerUrl,
341
+ ...opportunity,
342
+ }),
343
+ );
344
+ }
345
+ } else {
346
+ snapshots.push({
347
+ iteration,
348
+ observedAt: new Date().toISOString(),
349
+ count: opportunities.length,
350
+ opportunities,
351
+ });
322
352
  }
323
353
 
324
354
  if (iteration < maxIterations) {
325
355
  await sleepMs(options.intervalMs);
326
356
  }
327
357
  }
358
+
359
+ if (options.output === 'json') {
360
+ const payload = {
361
+ action: 'scan',
362
+ indexerUrl,
363
+ iterationsCompleted: iteration,
364
+ requestedIterations: options.iterations,
365
+ intervalMs: options.intervalMs,
366
+ filters: {
367
+ markets: options.markets,
368
+ minNetSpreadPct: options.minNetSpreadPct,
369
+ feePctPerLeg: options.feePctPerLeg,
370
+ amountUsdc: options.amountUsdc,
371
+ },
372
+ opportunities: snapshots.flatMap((row) => row.opportunities),
373
+ snapshots,
374
+ diagnostics: [],
375
+ };
376
+
377
+ if (context.outputMode === 'json') {
378
+ emitSuccess(context.outputMode, 'arb.scan', payload);
379
+ } else {
380
+ // eslint-disable-next-line no-console
381
+ console.log(JSON.stringify(payload, null, 2));
382
+ }
383
+ }
328
384
  };
329
385
  }
330
386
 
@@ -149,11 +149,16 @@ function createCommandExecutorService(options = {}) {
149
149
  ? Math.max(1_000, Math.trunc(runtime.timeoutMs))
150
150
  : defaultTimeoutMs;
151
151
  const env = runtime.env && typeof runtime.env === 'object' ? runtime.env : baseEnv;
152
+ const childEnv = {
153
+ ...env,
154
+ // Mark child CLI invocations as MCP-controlled to enable stricter file/path safety guards.
155
+ PANDORA_MCP_MODE: '1',
156
+ };
152
157
  const argv = ['--output', 'json', ...commandArgs];
153
158
 
154
159
  const result = spawnSync(process.execPath, [cliPath, ...argv], {
155
160
  encoding: 'utf8',
156
- env,
161
+ env: childEnv,
157
162
  timeout: timeoutMs,
158
163
  killSignal: 'SIGKILL',
159
164
  maxBuffer: 10 * 1024 * 1024,
@@ -99,6 +99,30 @@ function buildSportsResolvePlanRetryCommand(cliName, details) {
99
99
  return `${cliName} sports resolve plan --event-id ${eventId}`;
100
100
  }
101
101
 
102
+ function buildLifecycleStatusCommand(cliName, details) {
103
+ const id = cleanToken(details && details.id, '<lifecycle-id>');
104
+ return `${cliName} lifecycle status --id ${id}`;
105
+ }
106
+
107
+ function buildLifecycleStartCommand(cliName, details) {
108
+ const configPath = cleanToken(details && details.configPath, '<config-file>');
109
+ return `${cliName} lifecycle start --config ${configPath}`;
110
+ }
111
+
112
+ function buildOddsRecordCommand(cliName, details) {
113
+ const competition = cleanToken(details && details.competition, '<competition>');
114
+ return `${cliName} odds record --competition ${competition} --interval 60 --max-samples 1`;
115
+ }
116
+
117
+ function buildOddsHistoryCommand(cliName, details) {
118
+ const eventId = cleanToken(details && details.eventId, '<event-id>');
119
+ return `${cliName} odds history --event-id ${eventId} --output json`;
120
+ }
121
+
122
+ function buildArbScanCommand(cliName) {
123
+ return `${cliName} arb scan --markets <market-a>,<market-b> --output json --iterations 1`;
124
+ }
125
+
102
126
  /**
103
127
  * Build deterministic Next-Best-Action recovery hints for JSON errors.
104
128
  * @param {{cliName?: string}} [options]
@@ -264,6 +288,52 @@ function createErrorRecoveryService(options = {}) {
264
288
  command: buildSportsResolvePlanRetryCommand(cliName, details),
265
289
  retryable: true,
266
290
  };
291
+ case 'LIFECYCLE_EXISTS':
292
+ return {
293
+ action: 'Inspect existing lifecycle state before creating another run',
294
+ command: buildLifecycleStatusCommand(cliName, details),
295
+ retryable: true,
296
+ };
297
+ case 'CONFIG_FILE_NOT_FOUND':
298
+ return {
299
+ action: 'Create/fix lifecycle config file and retry start',
300
+ command: buildLifecycleStartCommand(cliName, details),
301
+ retryable: true,
302
+ };
303
+ case 'LIFECYCLE_NOT_FOUND':
304
+ return {
305
+ action: 'Verify lifecycle id and inspect currently tracked runs',
306
+ command: `${cliName} lifecycle status --id <lifecycle-id>`,
307
+ retryable: true,
308
+ };
309
+ case 'ODDS_RECORD_CONNECTOR_FAILED':
310
+ case 'ODDS_RECORD_FAILED':
311
+ case 'ODDS_RECORD_WRITE_FAILED':
312
+ return {
313
+ action: 'Run one bounded odds capture sample to isolate connector/storage errors',
314
+ command: buildOddsRecordCommand(cliName, details),
315
+ retryable: true,
316
+ };
317
+ case 'ODDS_HISTORY_READ_FAILED':
318
+ case 'ODDS_HISTORY_FAILED':
319
+ return {
320
+ action: 'Retry odds history read with explicit event id and JSON output',
321
+ command: buildOddsHistoryCommand(cliName, details),
322
+ retryable: true,
323
+ };
324
+ case 'ARB_SCAN_FAILED':
325
+ case 'ARB_SCAN_INVALID_OUTPUT':
326
+ return {
327
+ action: 'Run a bounded arb scan iteration for deterministic diagnostics',
328
+ command: buildArbScanCommand(cliName),
329
+ retryable: true,
330
+ };
331
+ case 'MCP_FILE_ACCESS_BLOCKED':
332
+ return {
333
+ action: 'Use a workspace-relative file path when invoking tools via MCP',
334
+ command: `${cliName} help`,
335
+ retryable: true,
336
+ };
267
337
  case 'MISSING_REQUIRED_FLAG':
268
338
  case 'MISSING_FLAG_VALUE':
269
339
  case 'INVALID_FLAG_VALUE':
@@ -59,7 +59,10 @@ function readJsonFile(filePath, CliError) {
59
59
  raw = fs.readFileSync(filePath, 'utf8');
60
60
  } catch (err) {
61
61
  if (err && err.code === 'ENOENT') {
62
- throw new CliError('NOT_FOUND', `Lifecycle not found for id: ${path.basename(filePath, '.json')}`);
62
+ throw new CliError('LIFECYCLE_NOT_FOUND', `Lifecycle not found for id: ${path.basename(filePath, '.json')}`, {
63
+ id: path.basename(filePath, '.json'),
64
+ filePath,
65
+ });
63
66
  }
64
67
  throw err;
65
68
  }
@@ -84,6 +87,15 @@ function writeJsonFileAtomic(filePath, payload) {
84
87
  }
85
88
  }
86
89
 
90
+ function isMcpMode() {
91
+ return String(process.env.PANDORA_MCP_MODE || '').trim() === '1';
92
+ }
93
+
94
+ function isPathInside(baseDir, candidatePath) {
95
+ const relative = path.relative(baseDir, candidatePath);
96
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
97
+ }
98
+
87
99
  function renderLifecycleTable(payload) {
88
100
  // eslint-disable-next-line no-console
89
101
  console.log(`Lifecycle: ${payload.id}`);
@@ -111,6 +123,24 @@ function createRunLifecycleCommand(deps) {
111
123
  const commandHelpPayload = requireDep(deps, 'commandHelpPayload');
112
124
  const parseLifecycleFlags = requireDep(deps, 'parseLifecycleFlags');
113
125
 
126
+ function assertMcpReadablePathAllowed(targetPath, flagName) {
127
+ if (!isMcpMode()) return;
128
+ const workspaceRoot = path.resolve(process.cwd());
129
+ const resolvedPath = path.resolve(targetPath);
130
+ if (!isPathInside(workspaceRoot, resolvedPath)) {
131
+ throw new CliError(
132
+ 'MCP_FILE_ACCESS_BLOCKED',
133
+ `${flagName} must point to a file within the current workspace when running via MCP.`,
134
+ {
135
+ flag: flagName,
136
+ requestedPath: targetPath,
137
+ resolvedPath,
138
+ workspaceRoot,
139
+ },
140
+ );
141
+ }
142
+ }
143
+
114
144
  return async function runLifecycleCommand(args, context) {
115
145
  const action = args[0];
116
146
  const actionArgs = args.slice(1);
@@ -165,12 +195,15 @@ function createRunLifecycleCommand(deps) {
165
195
 
166
196
  if (options.action === 'start') {
167
197
  const configPath = path.resolve(process.cwd(), options.configPath);
198
+ assertMcpReadablePathAllowed(configPath, '--config');
168
199
  let configRaw;
169
200
  try {
170
201
  configRaw = fs.readFileSync(configPath, 'utf8');
171
202
  } catch (err) {
172
203
  if (err && err.code === 'ENOENT') {
173
- throw new CliError('CONFIG_FILE_NOT_FOUND', `Lifecycle config file not found: ${configPath}`);
204
+ throw new CliError('CONFIG_FILE_NOT_FOUND', `Lifecycle config file not found: ${configPath}`, {
205
+ configPath,
206
+ });
174
207
  }
175
208
  throw err;
176
209
  }
@@ -179,7 +212,9 @@ function createRunLifecycleCommand(deps) {
179
212
  try {
180
213
  config = JSON.parse(configRaw);
181
214
  } catch {
182
- throw new CliError('INVALID_JSON', `Lifecycle config must be valid JSON: ${configPath}`);
215
+ throw new CliError('INVALID_JSON', `Lifecycle config must be valid JSON: ${configPath}`, {
216
+ configPath,
217
+ });
183
218
  }
184
219
 
185
220
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
@@ -197,7 +232,10 @@ function createRunLifecycleCommand(deps) {
197
232
 
198
233
  const filePath = lifecycleFilePath(lifecycleDir, id);
199
234
  if (fs.existsSync(filePath)) {
200
- throw new CliError('LIFECYCLE_EXISTS', `Lifecycle already exists for id: ${id}`);
235
+ throw new CliError('LIFECYCLE_EXISTS', `Lifecycle already exists for id: ${id}`, {
236
+ id,
237
+ filePath,
238
+ });
201
239
  }
202
240
 
203
241
  const nowIso = new Date().toISOString();
@@ -243,8 +243,14 @@ const TOOL_DEFINITIONS = [
243
243
  name: 'odds.record',
244
244
  command: ['odds', 'record'],
245
245
  description: 'Record venue odds snapshots into local history storage.',
246
+ longRunningBlocked: true,
246
247
  mutating: true,
247
248
  },
249
+ {
250
+ name: 'arb.scan',
251
+ command: ['arb', 'scan', '--output', 'json', '--iterations', '1'],
252
+ description: 'Run one bounded arb scan iteration and return a structured payload.',
253
+ },
248
254
  { name: 'lifecycle.status', command: ['lifecycle', 'status'], description: 'Inspect lifecycle state by id.' },
249
255
  {
250
256
  name: 'lifecycle.start',
@@ -0,0 +1,168 @@
1
+ function requireDep(deps, name) {
2
+ if (!deps || typeof deps[name] !== 'function') {
3
+ throw new Error(`createRunOddsCommand requires deps.${name}()`);
4
+ }
5
+ return deps[name];
6
+ }
7
+
8
+ const ODDS_RECORD_USAGE =
9
+ 'pandora [--output table|json] odds record --competition <id> --interval <sec> [--max-samples <n>] [--event-id <id>] [--venues pandora_amm,polymarket] [--indexer-url <url>] [--polymarket-host <url>] [--polymarket-mock-url <url>] [--timeout-ms <ms>]';
10
+ const ODDS_HISTORY_USAGE =
11
+ 'pandora [--output table|json] odds history --event-id <id> --output csv|json [--limit <n>]';
12
+
13
+ /**
14
+ * Create runner for `pandora odds` commands.
15
+ * @param {object} deps
16
+ * @returns {(args: string[], context: {outputMode: 'table'|'json'}) => Promise<void>}
17
+ */
18
+ function createRunOddsCommand(deps) {
19
+ const parseIndexerSharedFlags = requireDep(deps, 'parseIndexerSharedFlags');
20
+ const includesHelpFlag = requireDep(deps, 'includesHelpFlag');
21
+ const maybeLoadIndexerEnv = requireDep(deps, 'maybeLoadIndexerEnv');
22
+ const resolveIndexerUrl = requireDep(deps, 'resolveIndexerUrl');
23
+ const parseOddsFlags = requireDep(deps, 'parseOddsFlags');
24
+ const createOddsHistoryService = requireDep(deps, 'createOddsHistoryService');
25
+ const createVenueConnectorFactory = requireDep(deps, 'createVenueConnectorFactory');
26
+ const sleepMs = requireDep(deps, 'sleepMs');
27
+ const emitSuccess = requireDep(deps, 'emitSuccess');
28
+ const renderSingleEntityTable = requireDep(deps, 'renderSingleEntityTable');
29
+
30
+ return async function runOddsCommand(args, context) {
31
+ const shared = parseIndexerSharedFlags(args);
32
+ if (!shared.rest.length || includesHelpFlag(shared.rest)) {
33
+ if (context.outputMode === 'json') {
34
+ emitSuccess(context.outputMode, 'odds.help', {
35
+ usage: ODDS_RECORD_USAGE,
36
+ historyUsage: ODDS_HISTORY_USAGE,
37
+ });
38
+ } else {
39
+ // eslint-disable-next-line no-console
40
+ console.log(`Usage: ${ODDS_RECORD_USAGE}`);
41
+ // eslint-disable-next-line no-console
42
+ console.log(` ${ODDS_HISTORY_USAGE}`);
43
+ }
44
+ return;
45
+ }
46
+
47
+ maybeLoadIndexerEnv(shared);
48
+ const indexerUrl = resolveIndexerUrl(shared.indexerUrl);
49
+ const parsed = parseOddsFlags(shared.rest);
50
+ const options = parsed.options || {};
51
+
52
+ const historyService = createOddsHistoryService();
53
+ const connectorFactory = createVenueConnectorFactory();
54
+
55
+ if (parsed.action === 'record') {
56
+ const intervalMs = Number(options.intervalSec) * 1000;
57
+ const maxSamples = Number.isInteger(options.maxSamples) && options.maxSamples > 0 ? options.maxSamples : 1;
58
+ const sampleResults = [];
59
+ let insertedTotal = 0;
60
+
61
+ for (let sample = 1; sample <= maxSamples; sample += 1) {
62
+ const rows = [];
63
+ const diagnostics = [];
64
+ for (const venue of options.venues) {
65
+ try {
66
+ const connector = connectorFactory.createConnector(venue, {
67
+ indexerUrl,
68
+ host: options.polymarketHost || null,
69
+ mockUrl: options.polymarketMockUrl || null,
70
+ timeoutMs: options.timeoutMs || shared.timeoutMs,
71
+ });
72
+ const pricePayload = await connector.getPrice({
73
+ competition: options.competition,
74
+ eventId: options.eventId,
75
+ indexerUrl,
76
+ host: options.polymarketHost || null,
77
+ mockUrl: options.polymarketMockUrl || null,
78
+ timeoutMs: options.timeoutMs || shared.timeoutMs,
79
+ });
80
+ if (pricePayload && Array.isArray(pricePayload.items)) {
81
+ rows.push(...pricePayload.items);
82
+ }
83
+ } catch (err) {
84
+ diagnostics.push({
85
+ venue,
86
+ code: err && err.code ? String(err.code) : 'ODDS_RECORD_CONNECTOR_FAILED',
87
+ message: err && err.message ? err.message : String(err),
88
+ });
89
+ }
90
+ }
91
+
92
+ const writeResult = historyService.recordEntries(rows);
93
+ insertedTotal += writeResult.inserted;
94
+ sampleResults.push({
95
+ sample,
96
+ observedAt: new Date().toISOString(),
97
+ inserted: writeResult.inserted,
98
+ diagnostics,
99
+ });
100
+
101
+ if (sample < maxSamples) {
102
+ await sleepMs(intervalMs);
103
+ }
104
+ }
105
+
106
+ emitSuccess(context.outputMode, 'odds.record', {
107
+ schemaVersion: '1.0.0',
108
+ generatedAt: new Date().toISOString(),
109
+ action: 'record',
110
+ competition: options.competition,
111
+ eventId: options.eventId || null,
112
+ intervalSec: options.intervalSec,
113
+ maxSamples,
114
+ venues: options.venues,
115
+ backend: historyService.backend,
116
+ storage: historyService.paths,
117
+ insertedTotal,
118
+ samples: sampleResults,
119
+ }, renderSingleEntityTable);
120
+ return;
121
+ }
122
+
123
+ const rows = historyService.queryByEventId(options.eventId, {
124
+ limit: options.limit,
125
+ });
126
+ const basePayload = {
127
+ schemaVersion: '1.0.0',
128
+ generatedAt: new Date().toISOString(),
129
+ action: 'history',
130
+ eventId: options.eventId,
131
+ backend: historyService.backend,
132
+ storage: historyService.paths,
133
+ count: rows.length,
134
+ items: rows,
135
+ };
136
+
137
+ if (options.output === 'csv') {
138
+ const csv = historyService.formatRows(rows, 'csv');
139
+ if (context.outputMode === 'json') {
140
+ emitSuccess(context.outputMode, 'odds.history', {
141
+ ...basePayload,
142
+ output: 'csv',
143
+ csv,
144
+ });
145
+ } else {
146
+ // eslint-disable-next-line no-console
147
+ console.log(csv);
148
+ }
149
+ return;
150
+ }
151
+
152
+ if (context.outputMode === 'json') {
153
+ emitSuccess(context.outputMode, 'odds.history', {
154
+ ...basePayload,
155
+ output: 'json',
156
+ });
157
+ } else {
158
+ // eslint-disable-next-line no-console
159
+ console.log(JSON.stringify(basePayload, null, 2));
160
+ }
161
+ };
162
+ }
163
+
164
+ module.exports = {
165
+ createRunOddsCommand,
166
+ ODDS_RECORD_USAGE,
167
+ ODDS_HISTORY_USAGE,
168
+ };
@@ -29,6 +29,7 @@ function createParseOddsFlags(deps) {
29
29
  const requireFlagValue = requireDep(deps, 'requireFlagValue');
30
30
  const parsePositiveInteger = requireDep(deps, 'parsePositiveInteger');
31
31
  const parseCsvList = requireDep(deps, 'parseCsvList');
32
+ const isSecureHttpUrlOrLocal = requireDep(deps, 'isSecureHttpUrlOrLocal');
32
33
 
33
34
  return function parseOddsFlags(args) {
34
35
  const action = String((args && args[0]) || '').trim().toLowerCase();
@@ -79,16 +80,34 @@ function createParseOddsFlags(deps) {
79
80
  }
80
81
  if (token === '--indexer-url') {
81
82
  options.indexerUrl = requireFlagValue(rest, i, '--indexer-url');
83
+ if (!isSecureHttpUrlOrLocal(options.indexerUrl)) {
84
+ throw new CliError(
85
+ 'INVALID_FLAG_VALUE',
86
+ `--indexer-url must use https:// (or http://localhost/127.0.0.1). Received: "${options.indexerUrl}"`,
87
+ );
88
+ }
82
89
  i += 1;
83
90
  continue;
84
91
  }
85
92
  if (token === '--polymarket-host') {
86
93
  options.polymarketHost = requireFlagValue(rest, i, '--polymarket-host');
94
+ if (!isSecureHttpUrlOrLocal(options.polymarketHost)) {
95
+ throw new CliError(
96
+ 'INVALID_FLAG_VALUE',
97
+ `--polymarket-host must use https:// (or http://localhost/127.0.0.1). Received: "${options.polymarketHost}"`,
98
+ );
99
+ }
87
100
  i += 1;
88
101
  continue;
89
102
  }
90
103
  if (token === '--polymarket-mock-url') {
91
104
  options.polymarketMockUrl = requireFlagValue(rest, i, '--polymarket-mock-url');
105
+ if (!isSecureHttpUrlOrLocal(options.polymarketMockUrl)) {
106
+ throw new CliError(
107
+ 'INVALID_FLAG_VALUE',
108
+ `--polymarket-mock-url must use https:// (or http://localhost/127.0.0.1). Received: "${options.polymarketMockUrl}"`,
109
+ );
110
+ }
92
111
  i += 1;
93
112
  continue;
94
113
  }