pandora-cli-skills 1.1.33 → 1.1.34

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,6 +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.34
4
5
  ---
5
6
 
6
7
  # Pandora CLI & Skills
@@ -9,6 +9,7 @@ function createCliOutputService(options = {}) {
9
9
  ? options.defaultSchemaVersion.trim()
10
10
  : '1.0.0';
11
11
  const CliError = options.CliError;
12
+ const getRecoveryForError = typeof options.getRecoveryForError === 'function' ? options.getRecoveryForError : null;
12
13
 
13
14
  if (typeof CliError !== 'function') {
14
15
  throw new Error('createCliOutputService requires CliError class.');
@@ -43,16 +44,36 @@ function createCliOutputService(options = {}) {
43
44
  if (error.details !== undefined) {
44
45
  envelope.error.details = error.details;
45
46
  }
47
+ if (getRecoveryForError) {
48
+ const recovery = getRecoveryForError({
49
+ code: error.code,
50
+ message: error.message,
51
+ details: error.details,
52
+ });
53
+ if (recovery) {
54
+ envelope.error.recovery = recovery;
55
+ }
56
+ }
46
57
  return envelope;
47
58
  }
48
59
 
49
- return {
60
+ const fallback = {
50
61
  ok: false,
51
62
  error: {
52
63
  code: 'UNEXPECTED_ERROR',
53
64
  message: formatErrorValue(error && error.message ? error.message : error),
54
65
  },
55
66
  };
67
+ if (getRecoveryForError) {
68
+ const recovery = getRecoveryForError({
69
+ code: 'UNEXPECTED_ERROR',
70
+ message: fallback.error.message,
71
+ });
72
+ if (recovery) {
73
+ fallback.error.recovery = recovery;
74
+ }
75
+ }
76
+ return fallback;
56
77
  }
57
78
 
58
79
  function attachJsonMetadata(data) {
@@ -100,6 +121,9 @@ function createCliOutputService(options = {}) {
100
121
  console.error(`Details: ${String(envelope.error.details)}`);
101
122
  }
102
123
  }
124
+ if (envelope.error.recovery && envelope.error.recovery.command) {
125
+ console.error(`Next: ${envelope.error.recovery.command}`);
126
+ }
103
127
  }
104
128
 
105
129
  process.exit(error instanceof CliError ? error.exitCode : 1);
@@ -0,0 +1,123 @@
1
+ const path = require('path');
2
+ const { spawnSync } = require('child_process');
3
+
4
+ function coerceErrorMessage(value) {
5
+ if (typeof value === 'string') return value;
6
+ if (value && typeof value.message === 'string') return value.message;
7
+ try {
8
+ return JSON.stringify(value);
9
+ } catch {
10
+ return String(value);
11
+ }
12
+ }
13
+
14
+ function parseEnvelopeFromOutput(stdout, stderr) {
15
+ const candidates = [stdout, stderr, `${stdout || ''}\n${stderr || ''}`]
16
+ .map((value) => String(value || '').trim())
17
+ .filter(Boolean);
18
+
19
+ for (const candidate of candidates) {
20
+ try {
21
+ return JSON.parse(candidate);
22
+ } catch {
23
+ const start = candidate.indexOf('{');
24
+ const end = candidate.lastIndexOf('}');
25
+ if (start >= 0 && end > start) {
26
+ const slice = candidate.slice(start, end + 1);
27
+ try {
28
+ return JSON.parse(slice);
29
+ } catch {
30
+ // continue
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Build a child-process command executor for JSON-mode CLI invocations.
41
+ * @param {{cliPath?: string, defaultTimeoutMs?: number, env?: object}} [options]
42
+ * @returns {{executeJsonCommand: (commandArgs: string[], runtime?: {timeoutMs?: number, env?: object}) => {ok: boolean, envelope: object, exitCode: number, stdout: string, stderr: string}}}
43
+ */
44
+ function createCommandExecutorService(options = {}) {
45
+ const cliPath =
46
+ typeof options.cliPath === 'string' && options.cliPath.trim()
47
+ ? options.cliPath.trim()
48
+ : path.resolve(__dirname, '..', 'pandora.cjs');
49
+ const defaultTimeoutMs = Number.isFinite(options.defaultTimeoutMs) ? Math.max(1_000, Math.trunc(options.defaultTimeoutMs)) : 60_000;
50
+ const baseEnv = options.env && typeof options.env === 'object' ? options.env : process.env;
51
+
52
+ function executeJsonCommand(commandArgs, runtime = {}) {
53
+ const timeoutMs = Number.isFinite(runtime.timeoutMs)
54
+ ? Math.max(1_000, Math.trunc(runtime.timeoutMs))
55
+ : defaultTimeoutMs;
56
+ const env = runtime.env && typeof runtime.env === 'object' ? runtime.env : baseEnv;
57
+ const argv = ['--output', 'json', ...commandArgs];
58
+
59
+ const result = spawnSync(process.execPath, [cliPath, ...argv], {
60
+ encoding: 'utf8',
61
+ env,
62
+ timeout: timeoutMs,
63
+ killSignal: 'SIGKILL',
64
+ maxBuffer: 10 * 1024 * 1024,
65
+ });
66
+
67
+ if (result.error && result.error.code === 'ETIMEDOUT') {
68
+ return {
69
+ ok: false,
70
+ exitCode: 1,
71
+ stdout: result.stdout || '',
72
+ stderr: result.stderr || '',
73
+ envelope: {
74
+ ok: false,
75
+ error: {
76
+ code: 'COMMAND_TIMEOUT',
77
+ message: `Command timed out after ${timeoutMs}ms.`,
78
+ details: { commandArgs, timeoutMs },
79
+ },
80
+ },
81
+ };
82
+ }
83
+
84
+ const envelope = parseEnvelopeFromOutput(result.stdout, result.stderr);
85
+ if (!envelope) {
86
+ return {
87
+ ok: false,
88
+ exitCode: typeof result.status === 'number' ? result.status : 1,
89
+ stdout: result.stdout || '',
90
+ stderr: result.stderr || '',
91
+ envelope: {
92
+ ok: false,
93
+ error: {
94
+ code: 'COMMAND_OUTPUT_PARSE_FAILED',
95
+ message: 'CLI command returned non-JSON output.',
96
+ details: {
97
+ commandArgs,
98
+ stdout: String(result.stdout || '').slice(0, 10_000),
99
+ stderr: String(result.stderr || '').slice(0, 10_000),
100
+ },
101
+ },
102
+ },
103
+ };
104
+ }
105
+
106
+ return {
107
+ ok: envelope && envelope.ok === true,
108
+ exitCode: typeof result.status === 'number' ? result.status : 1,
109
+ stdout: result.stdout || '',
110
+ stderr: result.stderr || '',
111
+ envelope,
112
+ };
113
+ }
114
+
115
+ return {
116
+ executeJsonCommand,
117
+ coerceErrorMessage,
118
+ };
119
+ }
120
+
121
+ module.exports = {
122
+ createCommandExecutorService,
123
+ };
@@ -37,6 +37,8 @@ function createCommandRouter(deps = {}) {
37
37
  runSuggestCommand,
38
38
  runResolveCommand,
39
39
  runLpCommand,
40
+ runMcpCommand,
41
+ runStreamCommand,
40
42
  runScriptCommand,
41
43
  } = deps;
42
44
 
@@ -131,6 +133,8 @@ function createCommandRouter(deps = {}) {
131
133
  suggest: async (handlerArgs, handlerContext) => runSuggestCommand(handlerArgs, handlerContext),
132
134
  resolve: async (handlerArgs, handlerContext) => runResolveCommand(handlerArgs, handlerContext),
133
135
  lp: async (handlerArgs, handlerContext) => runLpCommand(handlerArgs, handlerContext),
136
+ mcp: async (handlerArgs, handlerContext) => runMcpCommand(handlerArgs, handlerContext),
137
+ stream: async (handlerArgs, handlerContext) => runStreamCommand(handlerArgs, handlerContext),
134
138
  schema: async (handlerArgs, handlerContext) => deps.runSchemaCommand(handlerArgs, handlerContext),
135
139
  launch: async (handlerArgs, handlerContext) => {
136
140
  if (handlerContext.outputMode === 'json') {
@@ -0,0 +1,108 @@
1
+ function cleanToken(value, fallback) {
2
+ const normalized = String(value === undefined || value === null ? '' : value).trim();
3
+ return normalized || fallback;
4
+ }
5
+
6
+ function toAddressOrPlaceholder(value, placeholder = '<address>') {
7
+ const normalized = cleanToken(value, '');
8
+ if (/^0x[a-fA-F0-9]{40}$/.test(normalized)) {
9
+ return normalized;
10
+ }
11
+ return placeholder;
12
+ }
13
+
14
+ function toSideOrPlaceholder(value) {
15
+ const normalized = cleanToken(value, '').toLowerCase();
16
+ return normalized === 'yes' || normalized === 'no' ? normalized : 'yes';
17
+ }
18
+
19
+ function toPositiveNumberOrPlaceholder(value, placeholder = '<amount>') {
20
+ const numeric = Number(value);
21
+ if (Number.isFinite(numeric) && numeric > 0) {
22
+ return String(numeric);
23
+ }
24
+ return placeholder;
25
+ }
26
+
27
+ function buildTradeRetryCommand(cliName, details) {
28
+ const marketAddress = toAddressOrPlaceholder(details && details.marketAddress);
29
+ const side = toSideOrPlaceholder(details && details.side);
30
+ const amountUsdc = toPositiveNumberOrPlaceholder(details && details.amountUsdc);
31
+ return `${cliName} trade --dry-run --market-address ${marketAddress} --side ${side} --amount-usdc ${amountUsdc}`;
32
+ }
33
+
34
+ function buildPolymarketApproveCommand(cliName) {
35
+ return `${cliName} polymarket approve --dry-run`;
36
+ }
37
+
38
+ function buildPolymarketPreflightCommand(cliName) {
39
+ return `${cliName} polymarket preflight`;
40
+ }
41
+
42
+ /**
43
+ * Build deterministic Next-Best-Action recovery hints for JSON errors.
44
+ * @param {{cliName?: string}} [options]
45
+ * @returns {{getRecoveryForError: (errorLike: any) => (null|{action: string, command: string, retryable: boolean})}}
46
+ */
47
+ function createErrorRecoveryService(options = {}) {
48
+ const cliName = cleanToken(options.cliName, 'pandora');
49
+
50
+ function getRecoveryForError(errorLike) {
51
+ const code = cleanToken(errorLike && errorLike.code, '');
52
+ const details = errorLike && typeof errorLike.details === 'object' && errorLike.details ? errorLike.details : {};
53
+
54
+ switch (code) {
55
+ case 'TRADE_RISK_GUARD':
56
+ return {
57
+ action: 'Adjust risk guard inputs',
58
+ command: buildTradeRetryCommand(cliName, details),
59
+ retryable: true,
60
+ };
61
+ case 'ALLOWANCE_READ_FAILED':
62
+ case 'APPROVE_SIMULATION_FAILED':
63
+ case 'APPROVE_EXECUTION_FAILED':
64
+ case 'TRADE_EXECUTION_FAILED':
65
+ return {
66
+ action: 'Re-run trade planning before execute',
67
+ command: buildTradeRetryCommand(cliName, details),
68
+ retryable: true,
69
+ };
70
+ case 'POLYMARKET_APPROVE_FAILED':
71
+ case 'POLYMARKET_PROXY_APPROVAL_REQUIRES_MANUAL_EXECUTION':
72
+ return {
73
+ action: 'Run Polymarket approve flow first',
74
+ command: buildPolymarketApproveCommand(cliName),
75
+ retryable: true,
76
+ };
77
+ case 'POLYMARKET_TRADE_FAILED':
78
+ case 'POLYMARKET_PREFLIGHT_FAILED':
79
+ case 'POLYMARKET_CHECK_FAILED':
80
+ case 'POLYMARKET_MARKET_RESOLUTION_FAILED':
81
+ return {
82
+ action: 'Run Polymarket preflight diagnostics',
83
+ command: buildPolymarketPreflightCommand(cliName),
84
+ retryable: true,
85
+ };
86
+ case 'MISSING_REQUIRED_FLAG':
87
+ case 'MISSING_FLAG_VALUE':
88
+ case 'INVALID_FLAG_VALUE':
89
+ case 'UNKNOWN_FLAG':
90
+ case 'INVALID_ARGS':
91
+ return {
92
+ action: 'Inspect command help and retry',
93
+ command: `${cliName} help`,
94
+ retryable: true,
95
+ };
96
+ default:
97
+ return null;
98
+ }
99
+ }
100
+
101
+ return {
102
+ getRecoveryForError,
103
+ };
104
+ }
105
+
106
+ module.exports = {
107
+ createErrorRecoveryService,
108
+ };
@@ -0,0 +1,79 @@
1
+ function toServiceError(code, message, details = undefined) {
2
+ const error = new Error(message);
3
+ error.code = code;
4
+ if (details !== undefined) {
5
+ error.details = details;
6
+ }
7
+ return error;
8
+ }
9
+
10
+ function toIntegerOrNull(value) {
11
+ if (value === null || value === undefined || value === '') return null;
12
+ const parsed = Number(value);
13
+ if (!Number.isInteger(parsed)) return null;
14
+ return parsed;
15
+ }
16
+
17
+ /**
18
+ * Resolve attach-only fork runtime settings.
19
+ * Precedence in fork mode: --fork-rpc-url > FORK_RPC_URL (when --fork).
20
+ * @param {object} [options]
21
+ * @param {{env?: object, isSecureHttpUrlOrLocal?: (url: string) => boolean, defaultChainId?: number}} [runtime]
22
+ * @returns {{mode: 'live'|'fork', rpcUrl: (string|null), chainId: (number|null)}}
23
+ */
24
+ function resolveForkRuntime(options = {}, runtime = {}) {
25
+ const env = runtime.env && typeof runtime.env === 'object' ? runtime.env : process.env;
26
+ const isSecureHttpUrlOrLocal =
27
+ typeof runtime.isSecureHttpUrlOrLocal === 'function' ? runtime.isSecureHttpUrlOrLocal : () => true;
28
+ const defaultChainId = Number.isInteger(runtime.defaultChainId) ? runtime.defaultChainId : 1;
29
+
30
+ const forkExplicit = options.fork === true;
31
+ const forkRpcUrlFromFlag = String(options.forkRpcUrl || '').trim();
32
+ const forkRequested = forkExplicit || Boolean(forkRpcUrlFromFlag);
33
+
34
+ if (!forkRequested) {
35
+ const chainId = toIntegerOrNull(options.chainId) ?? toIntegerOrNull(env.CHAIN_ID) ?? defaultChainId;
36
+ return {
37
+ mode: 'live',
38
+ rpcUrl: null,
39
+ chainId,
40
+ };
41
+ }
42
+
43
+ const envForkRpcUrl = String(env.FORK_RPC_URL || '').trim();
44
+ let rpcUrl = forkRpcUrlFromFlag || (forkExplicit ? envForkRpcUrl : '');
45
+
46
+ if (!rpcUrl) {
47
+ throw toServiceError(
48
+ 'MISSING_REQUIRED_FLAG',
49
+ '--fork requires FORK_RPC_URL env var or explicit --fork-rpc-url <url>.',
50
+ {
51
+ hints: ['Set FORK_RPC_URL=http://127.0.0.1:8545 and rerun with --fork.'],
52
+ },
53
+ );
54
+ }
55
+
56
+ if (!isSecureHttpUrlOrLocal(rpcUrl)) {
57
+ throw toServiceError(
58
+ 'INVALID_FLAG_VALUE',
59
+ '--fork-rpc-url must use https:// (or http://localhost/127.0.0.1 for local testing).',
60
+ { rpcUrl },
61
+ );
62
+ }
63
+
64
+ const chainId =
65
+ toIntegerOrNull(options.forkChainId)
66
+ ?? toIntegerOrNull(options.chainId)
67
+ ?? toIntegerOrNull(env.CHAIN_ID)
68
+ ?? defaultChainId;
69
+
70
+ return {
71
+ mode: 'fork',
72
+ rpcUrl,
73
+ chainId,
74
+ };
75
+ }
76
+
77
+ module.exports = {
78
+ resolveForkRuntime,
79
+ };
@@ -0,0 +1,147 @@
1
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
2
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
3
+ const {
4
+ ListToolsRequestSchema,
5
+ CallToolRequestSchema,
6
+ McpError,
7
+ ErrorCode,
8
+ } = require('@modelcontextprotocol/sdk/types.js');
9
+
10
+ const { createMcpToolRegistry } = require('./mcp_tool_registry.cjs');
11
+ const { createCommandExecutorService } = require('./command_executor_service.cjs');
12
+
13
+ function coerceErrorMessage(value) {
14
+ if (typeof value === 'string') return value;
15
+ if (value && typeof value.message === 'string') return value.message;
16
+ try {
17
+ return JSON.stringify(value);
18
+ } catch {
19
+ return String(value);
20
+ }
21
+ }
22
+
23
+ function asCliErrorEnvelope(error) {
24
+ const code = error && error.code ? String(error.code) : 'MCP_TOOL_FAILED';
25
+ const envelope = {
26
+ ok: false,
27
+ error: {
28
+ code,
29
+ message: coerceErrorMessage(error),
30
+ },
31
+ };
32
+ if (error && error.details !== undefined) {
33
+ envelope.error.details = error.details;
34
+ }
35
+ return envelope;
36
+ }
37
+
38
+ function asToolResult(envelope) {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: JSON.stringify(envelope, null, 2),
44
+ },
45
+ ],
46
+ structuredContent: envelope,
47
+ isError: envelope && envelope.ok === false,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Create the MCP stdio server runner for `pandora mcp`.
53
+ * @param {{packageVersion?: string, cliPath?: string}} [options]
54
+ * @returns {{runMcpServer: (args: string[], context: {outputMode: 'table'|'json'}) => Promise<void>}}
55
+ */
56
+ function createRunMcpServer(options = {}) {
57
+ const packageVersion =
58
+ typeof options.packageVersion === 'string' && options.packageVersion.trim()
59
+ ? options.packageVersion.trim()
60
+ : '0.0.0';
61
+ const registry = createMcpToolRegistry();
62
+ const executor = createCommandExecutorService({
63
+ cliPath: options.cliPath,
64
+ defaultTimeoutMs: 60_000,
65
+ });
66
+
67
+ async function runMcpServer(args, context) {
68
+ const mcpArgs = Array.isArray(args) ? args : [];
69
+ if (mcpArgs.includes('--help') || mcpArgs.includes('-h')) {
70
+ if (context && context.outputMode === 'json') {
71
+ const usageEnvelope = {
72
+ ok: true,
73
+ command: 'mcp.help',
74
+ data: {
75
+ usage: 'pandora mcp',
76
+ notes: [
77
+ 'Runs an MCP stdio server.',
78
+ 'Do not pass --output json to this command in normal MCP operation.',
79
+ ],
80
+ },
81
+ };
82
+ process.stdout.write(`${JSON.stringify(usageEnvelope, null, 2)}\n`);
83
+ } else {
84
+ // eslint-disable-next-line no-console
85
+ console.log('Usage: pandora mcp');
86
+ // eslint-disable-next-line no-console
87
+ console.log('Runs Pandora as an MCP stdio server.');
88
+ }
89
+ return;
90
+ }
91
+
92
+ if (context && context.outputMode === 'json') {
93
+ throw new Error('pandora mcp must be run without --output json because MCP uses raw stdio transport.');
94
+ }
95
+
96
+ const server = new Server(
97
+ {
98
+ name: 'pandora-cli-skills',
99
+ version: packageVersion,
100
+ },
101
+ {
102
+ capabilities: {
103
+ tools: { listChanged: false },
104
+ },
105
+ },
106
+ );
107
+
108
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
109
+ tools: registry.listTools(),
110
+ }));
111
+
112
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
113
+ try {
114
+ const params = request && request.params ? request.params : {};
115
+ const toolName = String(params.name || '').trim();
116
+ if (!toolName) {
117
+ throw new McpError(ErrorCode.InvalidParams, 'tools/call requires params.name.');
118
+ }
119
+
120
+ const toolArgs = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
121
+ const invocation = registry.prepareInvocation(toolName, toolArgs);
122
+ const execution = executor.executeJsonCommand(invocation.argv);
123
+ return asToolResult(execution.envelope);
124
+ } catch (err) {
125
+ if (err instanceof McpError) throw err;
126
+ return asToolResult(asCliErrorEnvelope(err));
127
+ }
128
+ });
129
+
130
+ const transport = new StdioServerTransport(process.stdin, process.stdout);
131
+ await server.connect(transport);
132
+
133
+ await new Promise((resolve, reject) => {
134
+ transport.onclose = () => resolve();
135
+ transport.onerror = (error) => reject(error);
136
+ server.onclose = () => resolve();
137
+ });
138
+ }
139
+
140
+ return {
141
+ runMcpServer,
142
+ };
143
+ }
144
+
145
+ module.exports = {
146
+ createRunMcpServer,
147
+ };