pandora-cli-skills 1.1.49 → 1.1.51
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_FOR_SHARING.md +1 -1
- package/SKILL.md +3 -3
- package/cli/lib/contract_error_decoder.cjs +9 -0
- package/cli/lib/mirror_sync_service.cjs +126 -65
- package/cli/lib/parsers/mirror_deploy_flags.cjs +18 -0
- package/cli/lib/parsers/mirror_go_flags.cjs +18 -0
- package/cli/lib/resolve_command_service.cjs +8 -4
- package/cli/pandora.cjs +12 -3
- package/package.json +2 -1
- package/tests/cli/cli.integration.test.cjs +23 -0
- package/tests/unit/new-features.test.cjs +234 -0
package/README_FOR_SHARING.md
CHANGED
|
@@ -367,7 +367,7 @@ Mirror advanced flags (for operator tuning):
|
|
|
367
367
|
|
|
368
368
|
### Resolve command
|
|
369
369
|
- Usage:
|
|
370
|
-
- `pandora [--output table|json] resolve --poll-address <address> --answer yes|no --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
|
|
370
|
+
- `pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
|
|
371
371
|
- Behavior:
|
|
372
372
|
- `--dry-run` returns a deterministic execution plan.
|
|
373
373
|
- `--execute` submits the resolution transaction with decoded revert diagnostics on failure.
|
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.51
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Pandora CLI & Skills
|
|
@@ -145,7 +145,7 @@ pandora [--output table|json] webhook test [--webhook-url <url>] [--webhook-temp
|
|
|
145
145
|
pandora [--output table|json] leaderboard [--metric profit|volume|win-rate] [--chain-id <id>] [--limit <n>] [--min-trades <n>]
|
|
146
146
|
pandora [--output table|json] analyze --market-address <address> [--provider <name>] [--model <id>] [--max-cost-usd <n>] [--temperature <n>] [--timeout-ms <ms>]
|
|
147
147
|
pandora [--output table|json] suggest --wallet <address> --risk low|medium|high --budget <amount> [--count <n>] [--include-venues pandora,polymarket]
|
|
148
|
-
pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
|
|
148
|
+
pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
|
|
149
149
|
pandora [--output table|json] lp add|remove|positions [--market-address <address>] [--wallet <address>] [--amount-usdc <n>] [--lp-tokens <n>] [--dry-run|--execute] [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--usdc <address>] [--deadline-seconds <n>] [--indexer-url <url>] [--timeout-ms <ms>]
|
|
150
150
|
pandora [--output table|json] risk show|panic [--risk-file <path>] [--clear] [--reason <text>] [--actor <id>]
|
|
151
151
|
pandora stream prices|events [--indexer-url <url>] [--indexer-ws-url <url>] [--timeout-ms <ms>] [--interval-ms <ms>] [--market-address <address>] [--chain-id <id>] [--limit <n>]
|
|
@@ -418,7 +418,7 @@ pandora --output json schema
|
|
|
418
418
|
|
|
419
419
|
### Resolve command
|
|
420
420
|
- Usage:
|
|
421
|
-
- `pandora [--output table|json] resolve --poll-address <address> --answer yes|no --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
|
|
421
|
+
- `pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]`
|
|
422
422
|
- Behavior:
|
|
423
423
|
- `--dry-run` returns the call plan and decode-ready payload.
|
|
424
424
|
- `--execute` submits on-chain resolution through configured oracle/operator path.
|
|
@@ -35,6 +35,11 @@ const CONTRACT_ERROR_ABI = [
|
|
|
35
35
|
},
|
|
36
36
|
];
|
|
37
37
|
|
|
38
|
+
const REVERT_SELECTOR_HINTS = {
|
|
39
|
+
// Market-specific minimum-notional guard seen on some Pandora AMM deployments.
|
|
40
|
+
'0x7e2d7787': 'Trade too small for this market. Increase --amount-usdc and retry.',
|
|
41
|
+
};
|
|
42
|
+
|
|
38
43
|
function isHexData(value) {
|
|
39
44
|
return /^0x[0-9a-fA-F]*$/.test(String(value || ''));
|
|
40
45
|
}
|
|
@@ -126,6 +131,10 @@ function formatDecodedContractError(decoded) {
|
|
|
126
131
|
return decoded.errorName;
|
|
127
132
|
}
|
|
128
133
|
if (decoded.data) {
|
|
134
|
+
const selector = String(decoded.data).slice(0, 10).toLowerCase();
|
|
135
|
+
if (REVERT_SELECTOR_HINTS[selector]) {
|
|
136
|
+
return `${REVERT_SELECTOR_HINTS[selector]} (selector ${selector})`;
|
|
137
|
+
}
|
|
129
138
|
return `Contract reverted (${decoded.data})`;
|
|
130
139
|
}
|
|
131
140
|
return null;
|
|
@@ -102,82 +102,143 @@ async function runMirrorSync(options, deps = {}) {
|
|
|
102
102
|
break;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
105
|
+
try {
|
|
106
|
+
resetDailyCountersIfNeeded(state, tickAt);
|
|
107
|
+
|
|
108
|
+
const verifyPayload =
|
|
109
|
+
iteration === 1 && startupVerifyPayload
|
|
110
|
+
? startupVerifyPayload
|
|
111
|
+
: await verifyFn(buildVerifyRequest(options));
|
|
112
|
+
|
|
113
|
+
const snapshotMetrics = evaluateSnapshot(verifyPayload, options);
|
|
114
|
+
const plan = buildTickPlan({
|
|
115
|
+
snapshotMetrics,
|
|
116
|
+
state,
|
|
117
|
+
options,
|
|
118
|
+
});
|
|
119
|
+
const depth = await fetchDepthSnapshot({
|
|
120
|
+
depthFn,
|
|
121
|
+
verifyPayload,
|
|
122
|
+
options,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const gate = applyGateBypassPolicy(
|
|
126
|
+
evaluateStrictGates(
|
|
127
|
+
buildTickGateContext({
|
|
128
|
+
verifyPayload,
|
|
129
|
+
options,
|
|
130
|
+
state,
|
|
131
|
+
plan,
|
|
132
|
+
snapshotMetrics,
|
|
133
|
+
depth,
|
|
134
|
+
minimumTimeToCloseSec,
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
options,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const snapshot = buildTickSnapshot({
|
|
141
|
+
iteration,
|
|
142
|
+
tickAt,
|
|
143
|
+
verifyPayload,
|
|
144
|
+
options,
|
|
145
|
+
snapshotMetrics,
|
|
146
|
+
plan,
|
|
147
|
+
depth,
|
|
148
|
+
gate,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (snapshotMetrics.driftTriggered || plan.hedgeTriggered) {
|
|
152
|
+
await processTriggeredAction({
|
|
128
153
|
options,
|
|
129
154
|
state,
|
|
155
|
+
snapshot,
|
|
130
156
|
plan,
|
|
157
|
+
gate,
|
|
158
|
+
tickAt,
|
|
159
|
+
loadedFilePath: loaded.filePath,
|
|
160
|
+
rebalanceFn,
|
|
161
|
+
hedgeFn,
|
|
162
|
+
sendWebhook,
|
|
163
|
+
strategyHash: hash,
|
|
164
|
+
iteration,
|
|
165
|
+
actions,
|
|
166
|
+
webhookReports,
|
|
131
167
|
snapshotMetrics,
|
|
168
|
+
verifyPayload,
|
|
132
169
|
depth,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const snapshot = buildTickSnapshot({
|
|
140
|
-
iteration,
|
|
141
|
-
tickAt,
|
|
142
|
-
verifyPayload,
|
|
143
|
-
options,
|
|
144
|
-
snapshotMetrics,
|
|
145
|
-
plan,
|
|
146
|
-
depth,
|
|
147
|
-
gate,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
if (snapshotMetrics.driftTriggered || plan.hedgeTriggered) {
|
|
151
|
-
await processTriggeredAction({
|
|
152
|
-
options,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await persistTickSnapshot({
|
|
174
|
+
loadedFilePath: loaded.filePath,
|
|
153
175
|
state,
|
|
154
|
-
snapshot,
|
|
155
|
-
plan,
|
|
156
|
-
gate,
|
|
157
176
|
tickAt,
|
|
177
|
+
snapshot,
|
|
178
|
+
snapshots,
|
|
179
|
+
onTick,
|
|
180
|
+
iteration,
|
|
181
|
+
});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const errorCode = err && err.code ? String(err.code) : 'MIRROR_SYNC_TICK_FAILED';
|
|
184
|
+
const errorMessage = err && err.message ? err.message : String(err);
|
|
185
|
+
const errorDetails = err && err.details !== undefined ? err.details : null;
|
|
186
|
+
const timestamp = tickAt.toISOString();
|
|
187
|
+
|
|
188
|
+
const diagnostic = {
|
|
189
|
+
level: 'error',
|
|
190
|
+
scope: 'tick',
|
|
191
|
+
iteration,
|
|
192
|
+
timestamp,
|
|
193
|
+
code: errorCode,
|
|
194
|
+
message: errorMessage,
|
|
195
|
+
retryable: options.mode !== 'once',
|
|
196
|
+
};
|
|
197
|
+
if (errorDetails !== null) diagnostic.details = errorDetails;
|
|
198
|
+
diagnostics.push(diagnostic);
|
|
199
|
+
|
|
200
|
+
const snapshot = {
|
|
201
|
+
schemaVersion: MIRROR_SYNC_SCHEMA_VERSION,
|
|
202
|
+
timestamp,
|
|
203
|
+
iteration,
|
|
204
|
+
metrics: {
|
|
205
|
+
driftBps: null,
|
|
206
|
+
plannedRebalanceUsdc: 0,
|
|
207
|
+
plannedHedgeUsdc: 0,
|
|
208
|
+
},
|
|
209
|
+
strictGate: {
|
|
210
|
+
ok: false,
|
|
211
|
+
failedChecks: [],
|
|
212
|
+
checks: [],
|
|
213
|
+
},
|
|
214
|
+
action: {
|
|
215
|
+
status: 'error',
|
|
216
|
+
failedChecks: [],
|
|
217
|
+
forcedGateBypass: false,
|
|
218
|
+
errorCode,
|
|
219
|
+
errorMessage,
|
|
220
|
+
},
|
|
221
|
+
error: {
|
|
222
|
+
code: errorCode,
|
|
223
|
+
message: errorMessage,
|
|
224
|
+
details: errorDetails,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await persistTickSnapshot({
|
|
158
229
|
loadedFilePath: loaded.filePath,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
230
|
+
state,
|
|
231
|
+
tickAt,
|
|
232
|
+
snapshot,
|
|
233
|
+
snapshots,
|
|
234
|
+
onTick,
|
|
163
235
|
iteration,
|
|
164
|
-
actions,
|
|
165
|
-
webhookReports,
|
|
166
|
-
snapshotMetrics,
|
|
167
|
-
verifyPayload,
|
|
168
|
-
depth,
|
|
169
236
|
});
|
|
170
|
-
}
|
|
171
237
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
snapshot,
|
|
177
|
-
snapshots,
|
|
178
|
-
onTick,
|
|
179
|
-
iteration,
|
|
180
|
-
});
|
|
238
|
+
if (options.mode === 'once') {
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
181
242
|
|
|
182
243
|
if (shouldStop) break;
|
|
183
244
|
if (iteration >= maxIterations) break;
|
|
@@ -7,6 +7,18 @@ function requireDep(deps, name) {
|
|
|
7
7
|
return deps[name];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function normalizeSources(entries) {
|
|
11
|
+
const values = [];
|
|
12
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
13
|
+
const parts = String(entry || '').split(/[\n,]/g);
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
const normalized = String(part || '').trim();
|
|
16
|
+
if (normalized) values.push(normalized);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* Creates the mirror deploy flags parser.
|
|
12
24
|
* @param {object} deps
|
|
@@ -250,6 +262,12 @@ function createParseMirrorDeployFlags(deps) {
|
|
|
250
262
|
) {
|
|
251
263
|
throw new CliError('INVALID_ARGS', '--distribution-yes + --distribution-no must equal 1000000000.');
|
|
252
264
|
}
|
|
265
|
+
if (options.sourcesProvided && normalizeSources(options.sources).length < 2) {
|
|
266
|
+
throw new CliError(
|
|
267
|
+
'INVALID_FLAG_VALUE',
|
|
268
|
+
'--sources requires at least two non-empty URLs when explicitly provided.',
|
|
269
|
+
);
|
|
270
|
+
}
|
|
253
271
|
|
|
254
272
|
return options;
|
|
255
273
|
};
|
|
@@ -7,6 +7,18 @@ function requireDep(deps, name) {
|
|
|
7
7
|
return deps[name];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function normalizeSources(entries) {
|
|
11
|
+
const values = [];
|
|
12
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
13
|
+
const parts = String(entry || '').split(/[\n,]/g);
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
const normalized = String(part || '').trim();
|
|
16
|
+
if (normalized) values.push(normalized);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* Creates the mirror go flags parser.
|
|
12
24
|
* @param {object} deps
|
|
@@ -354,6 +366,12 @@ function createParseMirrorGoFlags(deps) {
|
|
|
354
366
|
);
|
|
355
367
|
}
|
|
356
368
|
}
|
|
369
|
+
if (options.sourcesProvided && normalizeSources(options.sources).length < 2) {
|
|
370
|
+
throw new CliError(
|
|
371
|
+
'INVALID_FLAG_VALUE',
|
|
372
|
+
'--sources requires at least two non-empty URLs when explicitly provided.',
|
|
373
|
+
);
|
|
374
|
+
}
|
|
357
375
|
|
|
358
376
|
return options;
|
|
359
377
|
};
|
|
@@ -14,6 +14,8 @@ function createRunResolveCommand(deps) {
|
|
|
14
14
|
const includesHelpFlag = requireDep(deps, 'includesHelpFlag');
|
|
15
15
|
const emitSuccess = requireDep(deps, 'emitSuccess');
|
|
16
16
|
const commandHelpPayload = requireDep(deps, 'commandHelpPayload');
|
|
17
|
+
const parseIndexerSharedFlags = requireDep(deps, 'parseIndexerSharedFlags');
|
|
18
|
+
const maybeLoadTradeEnv = requireDep(deps, 'maybeLoadTradeEnv');
|
|
17
19
|
const parseResolveFlags = requireDep(deps, 'parseResolveFlags');
|
|
18
20
|
const runResolve = requireDep(deps, 'runResolve');
|
|
19
21
|
const renderSingleEntityTable = requireDep(deps, 'renderSingleEntityTable');
|
|
@@ -21,24 +23,26 @@ function createRunResolveCommand(deps) {
|
|
|
21
23
|
const assertLiveWriteAllowed = typeof deps.assertLiveWriteAllowed === 'function' ? deps.assertLiveWriteAllowed : null;
|
|
22
24
|
|
|
23
25
|
return async function runResolveCommand(args, context) {
|
|
24
|
-
|
|
26
|
+
const shared = parseIndexerSharedFlags(args);
|
|
27
|
+
if (includesHelpFlag(shared.rest)) {
|
|
25
28
|
if (context.outputMode === 'json') {
|
|
26
29
|
emitSuccess(
|
|
27
30
|
context.outputMode,
|
|
28
31
|
'resolve.help',
|
|
29
32
|
commandHelpPayload(
|
|
30
|
-
'pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
|
|
33
|
+
'pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
|
|
31
34
|
),
|
|
32
35
|
);
|
|
33
36
|
} else {
|
|
34
37
|
// eslint-disable-next-line no-console
|
|
35
38
|
console.log(
|
|
36
|
-
'Usage: pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
|
|
39
|
+
'Usage: pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]',
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
maybeLoadTradeEnv(shared);
|
|
45
|
+
const options = parseResolveFlags(shared.rest);
|
|
42
46
|
if (options.execute && assertLiveWriteAllowed) {
|
|
43
47
|
await assertLiveWriteAllowed('resolve.execute', {
|
|
44
48
|
runtimeMode: options.fork || options.forkRpcUrl ? 'fork' : 'live',
|
package/cli/pandora.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const { spawnSync } = require('child_process');
|
|
6
7
|
const { createCommandRouter } = require('./lib/command_router.cjs');
|
|
@@ -513,7 +514,13 @@ function getDoctorServiceInstance() {
|
|
|
513
514
|
}
|
|
514
515
|
|
|
515
516
|
const ROOT = path.resolve(__dirname, '..');
|
|
516
|
-
const
|
|
517
|
+
const DEFAULT_ENV_FILE_PRIMARY = path.join(ROOT, 'scripts', '.env');
|
|
518
|
+
const DEFAULT_ENV_FILE_FALLBACK = path.join(os.homedir(), '.pandora-cli.env');
|
|
519
|
+
const DEFAULT_ENV_FILE = fs.existsSync(DEFAULT_ENV_FILE_FALLBACK)
|
|
520
|
+
? DEFAULT_ENV_FILE_FALLBACK
|
|
521
|
+
: fs.existsSync(DEFAULT_ENV_FILE_PRIMARY)
|
|
522
|
+
? DEFAULT_ENV_FILE_PRIMARY
|
|
523
|
+
: DEFAULT_ENV_FILE_FALLBACK;
|
|
517
524
|
const DEFAULT_ENV_EXAMPLE = path.join(ROOT, 'scripts', '.env.example');
|
|
518
525
|
const DEFAULT_INDEXER_URL = SHARED_DEFAULT_INDEXER_URL;
|
|
519
526
|
let PACKAGE_VERSION = '0.0.0';
|
|
@@ -793,7 +800,7 @@ Usage:
|
|
|
793
800
|
pandora [--output table|json] leaderboard [--metric profit|volume|win-rate] [--chain-id <id>] [--limit <n>] [--min-trades <n>]
|
|
794
801
|
pandora [--output table|json] analyze --market-address <address> [--provider <name>] [--model <id>] [--max-cost-usd <n>] [--temperature <n>] [--timeout-ms <ms>]
|
|
795
802
|
pandora [--output table|json] suggest --wallet <address> --risk low|medium|high --budget <amount> [--count <n>] [--include-venues pandora,polymarket]
|
|
796
|
-
pandora [--output table|json] resolve --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
|
|
803
|
+
pandora [--output table|json] resolve [--dotenv-path <path>] [--skip-dotenv] --poll-address <address> --answer yes|no|invalid --reason <text> --dry-run|--execute [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>]
|
|
797
804
|
pandora [--output table|json] lp add|remove|positions [--market-address <address>] [--wallet <address>] [--amount-usdc <n>] [--lp-tokens <n>|--all] [--dry-run|--execute] [--fork] [--fork-rpc-url <url>] [--fork-chain-id <id>] [--chain-id <id>] [--rpc-url <url>] [--private-key <hex>] [--usdc <address>] [--deadline-seconds <n>] [--indexer-url <url>] [--timeout-ms <ms>]
|
|
798
805
|
pandora [--output table|json] risk show|panic [--risk-file <path>] [--clear] [--reason <text>] [--actor <id>]
|
|
799
806
|
pandora stream prices|events [--indexer-url <url>] [--indexer-ws-url <url>] [--timeout-ms <ms>] [--interval-ms <ms>] [--market-address <address>] [--chain-id <id>] [--limit <n>]
|
|
@@ -852,7 +859,7 @@ Examples:
|
|
|
852
859
|
|
|
853
860
|
Notes:
|
|
854
861
|
- launch/clone-bet forward unknown flags directly to underlying scripts.
|
|
855
|
-
-
|
|
862
|
+
- Env auto-load default: ~/.pandora-cli.env when present; otherwise scripts/.env. Use --skip-dotenv to disable.
|
|
856
863
|
- --output json is supported for all commands except launch/clone-bet.
|
|
857
864
|
- Indexer URL resolution order: --indexer-url, PANDORA_INDEXER_URL, INDEXER_URL, default public indexer.
|
|
858
865
|
- mirror status --with-live can enrich output with Polymarket position data when POLYMARKET_* credentials are set; missing endpoints/creds return diagnostics instead of hard failures.
|
|
@@ -4746,6 +4753,8 @@ const runResolveCommandFromService = createRunResolveCommand({
|
|
|
4746
4753
|
includesHelpFlag,
|
|
4747
4754
|
emitSuccess,
|
|
4748
4755
|
commandHelpPayload,
|
|
4756
|
+
parseIndexerSharedFlags,
|
|
4757
|
+
maybeLoadTradeEnv,
|
|
4749
4758
|
parseResolveFlags: parseResolveFlagsFromModule,
|
|
4750
4759
|
runResolve,
|
|
4751
4760
|
renderSingleEntityTable,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pandora-cli-skills",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.51",
|
|
4
4
|
"description": "Pandora CLI & Skills",
|
|
5
5
|
"main": "cli/pandora.cjs",
|
|
6
6
|
"bin": {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
70
70
|
"@polymarket/clob-client": "^5.2.4",
|
|
71
|
+
"playwright-core": "^1.58.2",
|
|
71
72
|
"tsx": "^4.21.0",
|
|
72
73
|
"viem": "^2.46.2",
|
|
73
74
|
"ws": "^8.19.0"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const test = require('node:test');
|
|
2
2
|
const assert = require('node:assert/strict');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
|
|
6
7
|
const {
|
|
@@ -5612,6 +5613,28 @@ test('resolve and lp commands are enabled', () => {
|
|
|
5612
5613
|
assert.equal(lpPayload.data.wallet, ADDRESSES.wallet1.toLowerCase());
|
|
5613
5614
|
});
|
|
5614
5615
|
|
|
5616
|
+
test('resolve accepts --dotenv-path and returns env-file errors instead of unknown-flag', () => {
|
|
5617
|
+
const missingFile = path.join(os.tmpdir(), `pandora-missing-env-${Date.now()}.env`);
|
|
5618
|
+
const result = runCli([
|
|
5619
|
+
'--output',
|
|
5620
|
+
'json',
|
|
5621
|
+
'resolve',
|
|
5622
|
+
'--dotenv-path',
|
|
5623
|
+
missingFile,
|
|
5624
|
+
'--poll-address',
|
|
5625
|
+
'0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
5626
|
+
'--answer',
|
|
5627
|
+
'yes',
|
|
5628
|
+
'--reason',
|
|
5629
|
+
'fixture',
|
|
5630
|
+
'--dry-run',
|
|
5631
|
+
]);
|
|
5632
|
+
|
|
5633
|
+
assert.equal(result.status, 1);
|
|
5634
|
+
const payload = parseJsonOutput(result);
|
|
5635
|
+
assert.equal(payload.error.code, 'ENV_FILE_NOT_FOUND');
|
|
5636
|
+
});
|
|
5637
|
+
|
|
5615
5638
|
test('launch enforces mode flag and dry-run reaches deterministic preflight', () => {
|
|
5616
5639
|
const args = buildLaunchArgs();
|
|
5617
5640
|
|
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
runPolymarketPreflight,
|
|
44
44
|
POLYMARKET_OPS_SCHEMA_VERSION,
|
|
45
45
|
} = require('../../cli/lib/polymarket_ops_service.cjs');
|
|
46
|
+
const { formatDecodedContractError } = require('../../cli/lib/contract_error_decoder.cjs');
|
|
46
47
|
const { runMirrorSync } = require('../../cli/lib/mirror_sync_service.cjs');
|
|
47
48
|
const { createRunMirrorCommand } = require('../../cli/lib/mirror_command_service.cjs');
|
|
48
49
|
const { resolveForkRuntime } = require('../../cli/lib/fork_runtime_service.cjs');
|
|
@@ -260,6 +261,12 @@ test('evaluateMarket throws deterministic error without provider', async () => {
|
|
|
260
261
|
);
|
|
261
262
|
});
|
|
262
263
|
|
|
264
|
+
test('contract error formatter maps known minimum-trade selector to actionable hint', () => {
|
|
265
|
+
const message = formatDecodedContractError({ data: '0x7e2d7787' });
|
|
266
|
+
assert.match(message, /trade too small/i);
|
|
267
|
+
assert.match(message, /--amount-usdc/i);
|
|
268
|
+
});
|
|
269
|
+
|
|
263
270
|
test('autopilot state helpers are deterministic', () => {
|
|
264
271
|
const hash1 = strategyHash({ a: 1, b: 'x' });
|
|
265
272
|
const hash2 = strategyHash({ a: 1, b: 'x' });
|
|
@@ -1486,6 +1493,165 @@ test('runMirrorSync handles thrown hedgeFn errors without consuming idempotency'
|
|
|
1486
1493
|
}
|
|
1487
1494
|
});
|
|
1488
1495
|
|
|
1496
|
+
test('runMirrorSync run mode continues after transient tick verification failures', async () => {
|
|
1497
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-mirror-sync-tick-retry-'));
|
|
1498
|
+
const stateFile = path.join(tempDir, 'mirror-state.json');
|
|
1499
|
+
const killSwitchFile = path.join(tempDir, 'STOP');
|
|
1500
|
+
|
|
1501
|
+
const verifyPayload = {
|
|
1502
|
+
matchConfidence: 0.99,
|
|
1503
|
+
gateResult: {
|
|
1504
|
+
ok: true,
|
|
1505
|
+
failedChecks: [],
|
|
1506
|
+
checks: [{ code: 'CLOSE_TIME_DELTA', ok: true, meta: { closeDeltaHours: 0 } }],
|
|
1507
|
+
},
|
|
1508
|
+
sourceMarket: {
|
|
1509
|
+
source: 'polymarket',
|
|
1510
|
+
marketId: 'poly-cond-1',
|
|
1511
|
+
yesPct: 60,
|
|
1512
|
+
yesTokenId: 'yes-token',
|
|
1513
|
+
noTokenId: 'no-token',
|
|
1514
|
+
},
|
|
1515
|
+
pandora: {
|
|
1516
|
+
yesPct: 55,
|
|
1517
|
+
reserveYes: 5,
|
|
1518
|
+
reserveNo: 5,
|
|
1519
|
+
},
|
|
1520
|
+
expiry: { minTimeToExpirySec: 7200 },
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
let verifyCallCount = 0;
|
|
1524
|
+
|
|
1525
|
+
try {
|
|
1526
|
+
const payload = await runMirrorSync(
|
|
1527
|
+
{
|
|
1528
|
+
mode: 'run',
|
|
1529
|
+
iterations: 3,
|
|
1530
|
+
indexerUrl: 'https://example.invalid/graphql',
|
|
1531
|
+
timeoutMs: 1000,
|
|
1532
|
+
pandoraMarketAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
1533
|
+
polymarketMarketId: 'poly-cond-1',
|
|
1534
|
+
executeLive: false,
|
|
1535
|
+
trustDeploy: false,
|
|
1536
|
+
hedgeEnabled: false,
|
|
1537
|
+
hedgeRatio: 1,
|
|
1538
|
+
intervalMs: 1,
|
|
1539
|
+
driftTriggerBps: 10000,
|
|
1540
|
+
hedgeTriggerUsdc: 1000,
|
|
1541
|
+
maxRebalanceUsdc: 25,
|
|
1542
|
+
maxHedgeUsdc: 10,
|
|
1543
|
+
maxOpenExposureUsdc: 100,
|
|
1544
|
+
maxTradesPerDay: 10,
|
|
1545
|
+
cooldownMs: 1000,
|
|
1546
|
+
depthSlippageBps: 100,
|
|
1547
|
+
stateFile,
|
|
1548
|
+
killSwitchFile,
|
|
1549
|
+
polymarketHost: 'https://clob.polymarket.com',
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
verifyFn: async () => {
|
|
1553
|
+
verifyCallCount += 1;
|
|
1554
|
+
if (verifyCallCount === 2) {
|
|
1555
|
+
const error = new Error('temporary indexer timeout');
|
|
1556
|
+
error.code = 'INDEXER_TIMEOUT';
|
|
1557
|
+
throw error;
|
|
1558
|
+
}
|
|
1559
|
+
return verifyPayload;
|
|
1560
|
+
},
|
|
1561
|
+
depthFn: async () => ({
|
|
1562
|
+
depthWithinSlippageUsd: 1000,
|
|
1563
|
+
yesDepth: { depthUsd: 1000, midPrice: 0.4, worstPrice: 0.41 },
|
|
1564
|
+
noDepth: { depthUsd: 1000, midPrice: 0.6, worstPrice: 0.61 },
|
|
1565
|
+
}),
|
|
1566
|
+
sleep: async () => {},
|
|
1567
|
+
},
|
|
1568
|
+
);
|
|
1569
|
+
|
|
1570
|
+
assert.equal(payload.iterationsCompleted, 3);
|
|
1571
|
+
assert.equal(payload.snapshots.length, 3);
|
|
1572
|
+
assert.equal(payload.diagnostics.length, 1);
|
|
1573
|
+
assert.equal(payload.diagnostics[0].code, 'INDEXER_TIMEOUT');
|
|
1574
|
+
assert.equal(payload.diagnostics[0].scope, 'tick');
|
|
1575
|
+
assert.equal(payload.snapshots[1].action.status, 'error');
|
|
1576
|
+
assert.equal(payload.snapshots[1].error.code, 'INDEXER_TIMEOUT');
|
|
1577
|
+
} finally {
|
|
1578
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
test('runMirrorSync once mode still fails fast on tick errors', async () => {
|
|
1583
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-mirror-sync-once-fail-'));
|
|
1584
|
+
const stateFile = path.join(tempDir, 'mirror-state.json');
|
|
1585
|
+
const killSwitchFile = path.join(tempDir, 'STOP');
|
|
1586
|
+
|
|
1587
|
+
const verifyPayload = {
|
|
1588
|
+
matchConfidence: 0.99,
|
|
1589
|
+
gateResult: {
|
|
1590
|
+
ok: true,
|
|
1591
|
+
failedChecks: [],
|
|
1592
|
+
checks: [{ code: 'CLOSE_TIME_DELTA', ok: true, meta: { closeDeltaHours: 0 } }],
|
|
1593
|
+
},
|
|
1594
|
+
sourceMarket: {
|
|
1595
|
+
source: 'polymarket',
|
|
1596
|
+
marketId: 'poly-cond-1',
|
|
1597
|
+
yesPct: 60,
|
|
1598
|
+
yesTokenId: 'yes-token',
|
|
1599
|
+
noTokenId: 'no-token',
|
|
1600
|
+
},
|
|
1601
|
+
pandora: {
|
|
1602
|
+
yesPct: 55,
|
|
1603
|
+
reserveYes: 5,
|
|
1604
|
+
reserveNo: 5,
|
|
1605
|
+
},
|
|
1606
|
+
expiry: { minTimeToExpirySec: 7200 },
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
try {
|
|
1610
|
+
await assert.rejects(
|
|
1611
|
+
runMirrorSync(
|
|
1612
|
+
{
|
|
1613
|
+
mode: 'once',
|
|
1614
|
+
indexerUrl: 'https://example.invalid/graphql',
|
|
1615
|
+
timeoutMs: 1000,
|
|
1616
|
+
pandoraMarketAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
1617
|
+
polymarketMarketId: 'poly-cond-1',
|
|
1618
|
+
executeLive: false,
|
|
1619
|
+
trustDeploy: false,
|
|
1620
|
+
hedgeEnabled: false,
|
|
1621
|
+
hedgeRatio: 1,
|
|
1622
|
+
intervalMs: 1,
|
|
1623
|
+
driftTriggerBps: 10000,
|
|
1624
|
+
hedgeTriggerUsdc: 1000,
|
|
1625
|
+
maxRebalanceUsdc: 25,
|
|
1626
|
+
maxHedgeUsdc: 10,
|
|
1627
|
+
maxOpenExposureUsdc: 100,
|
|
1628
|
+
maxTradesPerDay: 10,
|
|
1629
|
+
cooldownMs: 1000,
|
|
1630
|
+
depthSlippageBps: 100,
|
|
1631
|
+
stateFile,
|
|
1632
|
+
killSwitchFile,
|
|
1633
|
+
polymarketHost: 'https://clob.polymarket.com',
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
verifyFn: async () => verifyPayload,
|
|
1637
|
+
depthFn: async () => {
|
|
1638
|
+
const error = new Error('depth fetch unavailable');
|
|
1639
|
+
error.code = 'DEPTH_FETCH_FAILED';
|
|
1640
|
+
throw error;
|
|
1641
|
+
},
|
|
1642
|
+
sleep: async () => {},
|
|
1643
|
+
},
|
|
1644
|
+
),
|
|
1645
|
+
(error) => {
|
|
1646
|
+
assert.equal(error.code, 'DEPTH_FETCH_FAILED');
|
|
1647
|
+
return true;
|
|
1648
|
+
},
|
|
1649
|
+
);
|
|
1650
|
+
} finally {
|
|
1651
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1489
1655
|
test('runAutopilot does not consume budget/idempotency when executeFn throws', async () => {
|
|
1490
1656
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pandora-autopilot-failure-'));
|
|
1491
1657
|
const stateFile = path.join(tempDir, 'autopilot-state.json');
|
|
@@ -1732,6 +1898,41 @@ test('createParseMirrorDeployFlags enforces secure --rpc-url and drops dead allo
|
|
|
1732
1898
|
);
|
|
1733
1899
|
});
|
|
1734
1900
|
|
|
1901
|
+
test('createParseMirrorDeployFlags rejects explicit empty or underspecified --sources early', () => {
|
|
1902
|
+
const parseMirrorDeployFlags = createParseMirrorDeployFlags(buildParserDeps());
|
|
1903
|
+
|
|
1904
|
+
assert.throws(
|
|
1905
|
+
() =>
|
|
1906
|
+
parseMirrorDeployFlags([
|
|
1907
|
+
'--plan-file',
|
|
1908
|
+
'/tmp/plan.json',
|
|
1909
|
+
'--dry-run',
|
|
1910
|
+
'--sources',
|
|
1911
|
+
'',
|
|
1912
|
+
]),
|
|
1913
|
+
(error) => {
|
|
1914
|
+
assert.equal(error.code, 'INVALID_FLAG_VALUE');
|
|
1915
|
+
assert.match(error.message, /at least two non-empty urls/i);
|
|
1916
|
+
return true;
|
|
1917
|
+
},
|
|
1918
|
+
);
|
|
1919
|
+
|
|
1920
|
+
assert.throws(
|
|
1921
|
+
() =>
|
|
1922
|
+
parseMirrorDeployFlags([
|
|
1923
|
+
'--plan-file',
|
|
1924
|
+
'/tmp/plan.json',
|
|
1925
|
+
'--dry-run',
|
|
1926
|
+
'--sources',
|
|
1927
|
+
'https://example.com/one',
|
|
1928
|
+
]),
|
|
1929
|
+
(error) => {
|
|
1930
|
+
assert.equal(error.code, 'INVALID_FLAG_VALUE');
|
|
1931
|
+
return true;
|
|
1932
|
+
},
|
|
1933
|
+
);
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1735
1936
|
test('mirror sync gates normalize and selectively bypass failed checks', () => {
|
|
1736
1937
|
assert.deepEqual(
|
|
1737
1938
|
normalizeSkipGateChecks([' depth_coverage ', 'unknown_check', 'MAX_TRADES_PER_DAY', 'DEPTH_COVERAGE']),
|
|
@@ -1884,6 +2085,39 @@ test('createParseMirrorGoFlags treats bare --skip-gate as force gate mode', () =
|
|
|
1884
2085
|
assert.deepEqual(options.skipGateChecks, []);
|
|
1885
2086
|
});
|
|
1886
2087
|
|
|
2088
|
+
test('createParseMirrorGoFlags rejects explicit empty or underspecified --sources early', () => {
|
|
2089
|
+
const parseMirrorGoFlags = createParseMirrorGoFlags(buildParserDeps());
|
|
2090
|
+
|
|
2091
|
+
assert.throws(
|
|
2092
|
+
() =>
|
|
2093
|
+
parseMirrorGoFlags([
|
|
2094
|
+
'--polymarket-market-id',
|
|
2095
|
+
'poly-1',
|
|
2096
|
+
'--sources',
|
|
2097
|
+
'',
|
|
2098
|
+
]),
|
|
2099
|
+
(error) => {
|
|
2100
|
+
assert.equal(error.code, 'INVALID_FLAG_VALUE');
|
|
2101
|
+
assert.match(error.message, /at least two non-empty urls/i);
|
|
2102
|
+
return true;
|
|
2103
|
+
},
|
|
2104
|
+
);
|
|
2105
|
+
|
|
2106
|
+
assert.throws(
|
|
2107
|
+
() =>
|
|
2108
|
+
parseMirrorGoFlags([
|
|
2109
|
+
'--polymarket-market-id',
|
|
2110
|
+
'poly-1',
|
|
2111
|
+
'--sources',
|
|
2112
|
+
'https://example.com/one',
|
|
2113
|
+
]),
|
|
2114
|
+
(error) => {
|
|
2115
|
+
assert.equal(error.code, 'INVALID_FLAG_VALUE');
|
|
2116
|
+
return true;
|
|
2117
|
+
},
|
|
2118
|
+
);
|
|
2119
|
+
});
|
|
2120
|
+
|
|
1887
2121
|
test('createParseLifecycleFlags validates start|status|resolve contracts', () => {
|
|
1888
2122
|
const parseLifecycleFlags = createParseLifecycleFlags({
|
|
1889
2123
|
CliError: ParserCliError,
|