@jackwener/opencli 0.7.10 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pkg-pr-new.yml +30 -0
- package/CDP.md +103 -0
- package/CDP.zh-CN.md +103 -0
- package/README.md +5 -0
- package/README.zh-CN.md +5 -0
- package/dist/browser/discover.d.ts +30 -0
- package/dist/browser/discover.js +128 -14
- package/dist/browser/errors.d.ts +2 -1
- package/dist/browser/errors.js +13 -0
- package/dist/browser/index.d.ts +6 -1
- package/dist/browser/index.js +6 -1
- package/dist/browser/mcp.js +14 -8
- package/dist/browser/page.js +11 -2
- package/dist/browser.test.js +135 -1
- package/dist/cli-manifest.json +163 -0
- package/dist/clis/barchart/flow.d.ts +1 -0
- package/dist/clis/barchart/flow.js +115 -0
- package/dist/clis/barchart/greeks.d.ts +1 -0
- package/dist/clis/barchart/greeks.js +119 -0
- package/dist/clis/barchart/options.d.ts +1 -0
- package/dist/clis/barchart/options.js +106 -0
- package/dist/clis/barchart/quote.d.ts +1 -0
- package/dist/clis/barchart/quote.js +133 -0
- package/dist/doctor.js +8 -0
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +59 -1
- package/dist/main.js +2 -15
- package/dist/pipeline/executor.js +2 -24
- package/dist/pipeline/registry.d.ts +19 -0
- package/dist/pipeline/registry.js +41 -0
- package/package.json +1 -1
- package/src/browser/discover.ts +149 -14
- package/src/browser/errors.ts +17 -1
- package/src/browser/index.ts +6 -1
- package/src/browser/mcp.ts +14 -7
- package/src/browser/page.ts +21 -2
- package/src/browser.test.ts +140 -1
- package/src/clis/barchart/flow.ts +120 -0
- package/src/clis/barchart/greeks.ts +123 -0
- package/src/clis/barchart/options.ts +110 -0
- package/src/clis/barchart/quote.ts +137 -0
- package/src/doctor.ts +9 -0
- package/src/engine.ts +58 -1
- package/src/main.ts +6 -11
- package/src/pipeline/executor.ts +2 -28
- package/src/pipeline/registry.ts +60 -0
- package/vitest.config.ts +7 -0
package/dist/browser/mcp.js
CHANGED
|
@@ -7,7 +7,7 @@ import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
|
|
|
7
7
|
import { PKG_VERSION } from '../version.js';
|
|
8
8
|
import { Page } from './page.js';
|
|
9
9
|
import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js';
|
|
10
|
-
import { findMcpServerPath,
|
|
10
|
+
import { findMcpServerPath, buildMcpLaunchSpec, resolveCdpEndpoint } from './discover.js';
|
|
11
11
|
import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
12
12
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
13
13
|
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
@@ -102,8 +102,6 @@ export class PlaywrightMCP {
|
|
|
102
102
|
if (this._state === 'closed')
|
|
103
103
|
throw new Error('Playwright MCP session is closed');
|
|
104
104
|
const mcpPath = findMcpServerPath();
|
|
105
|
-
if (!mcpPath)
|
|
106
|
-
throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
|
|
107
105
|
PlaywrightMCP._registerGlobalCleanup();
|
|
108
106
|
PlaywrightMCP._activeInsts.add(this);
|
|
109
107
|
this._state = 'connecting';
|
|
@@ -111,7 +109,8 @@ export class PlaywrightMCP {
|
|
|
111
109
|
return new Promise((resolve, reject) => {
|
|
112
110
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
113
111
|
const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
114
|
-
const
|
|
112
|
+
const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint();
|
|
113
|
+
const useExtension = !requestedCdp;
|
|
115
114
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
116
115
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
117
116
|
let stderrBuffer = '';
|
|
@@ -146,19 +145,24 @@ export class PlaywrightMCP {
|
|
|
146
145
|
settleError(inferConnectFailureKind({
|
|
147
146
|
hasExtensionToken: !!extensionToken,
|
|
148
147
|
stderr: stderrBuffer,
|
|
148
|
+
isCdpMode: requestedCdp,
|
|
149
149
|
}));
|
|
150
150
|
}, timeout * 1000);
|
|
151
|
-
const
|
|
151
|
+
const launchSpec = buildMcpLaunchSpec({
|
|
152
152
|
mcpPath,
|
|
153
153
|
executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
|
|
154
|
+
cdpEndpoint,
|
|
154
155
|
});
|
|
155
156
|
if (process.env.OPENCLI_VERBOSE) {
|
|
156
|
-
console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
|
|
157
|
+
console.error(`[opencli] Mode: ${requestedCdp ? 'CDP' : useExtension ? 'extension' : 'standalone'}`);
|
|
157
158
|
if (useExtension)
|
|
158
159
|
console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
|
|
160
|
+
if (launchSpec.usedNpxFallback) {
|
|
161
|
+
console.error('[opencli] Playwright MCP not found locally; bootstrapping via npx @playwright/mcp@latest');
|
|
162
|
+
}
|
|
159
163
|
}
|
|
160
|
-
debugLog(`Spawning
|
|
161
|
-
this._proc = spawn(
|
|
164
|
+
debugLog(`Spawning ${launchSpec.command} ${launchSpec.args.join(' ')}`);
|
|
165
|
+
this._proc = spawn(launchSpec.command, launchSpec.args, {
|
|
162
166
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
163
167
|
env: { ...process.env },
|
|
164
168
|
});
|
|
@@ -209,6 +213,7 @@ export class PlaywrightMCP {
|
|
|
209
213
|
hasExtensionToken: !!extensionToken,
|
|
210
214
|
stderr: stderrBuffer,
|
|
211
215
|
exited: true,
|
|
216
|
+
isCdpMode: requestedCdp,
|
|
212
217
|
}), { exitCode: code });
|
|
213
218
|
}
|
|
214
219
|
});
|
|
@@ -225,6 +230,7 @@ export class PlaywrightMCP {
|
|
|
225
230
|
hasExtensionToken: !!extensionToken,
|
|
226
231
|
stderr: stderrBuffer,
|
|
227
232
|
rawMessage: `MCP init failed: ${resp.error.message}`,
|
|
233
|
+
isCdpMode: requestedCdp,
|
|
228
234
|
}), { rawMessage: resp.error.message });
|
|
229
235
|
return;
|
|
230
236
|
}
|
package/dist/browser/page.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
5
5
|
import { normalizeEvaluateSource } from '../pipeline/template.js';
|
|
6
6
|
import { generateInterceptorJs, generateReadInterceptedJs } from '../interceptor.js';
|
|
7
|
+
import { BrowserConnectError } from '../errors.js';
|
|
7
8
|
/**
|
|
8
9
|
* Page abstraction wrapping JSON-RPC calls to Playwright MCP.
|
|
9
10
|
*/
|
|
@@ -18,10 +19,18 @@ export class Page {
|
|
|
18
19
|
throw new Error(`page.${method}: ${resp.error.message ?? JSON.stringify(resp.error)}`);
|
|
19
20
|
// Extract text content from MCP result
|
|
20
21
|
const result = resp.result;
|
|
22
|
+
if (result?.isError) {
|
|
23
|
+
const errorText = result.content?.find((c) => c.type === 'text')?.text || 'Unknown MCP Error';
|
|
24
|
+
throw new BrowserConnectError(errorText, 'Please check if the browser is running or if the Playwright MCP / CDP connection is configured correctly.');
|
|
25
|
+
}
|
|
21
26
|
if (result?.content) {
|
|
22
27
|
const textParts = result.content.filter((c) => c.type === 'text');
|
|
23
|
-
if (textParts.length
|
|
24
|
-
let text = textParts[
|
|
28
|
+
if (textParts.length >= 1) {
|
|
29
|
+
let text = textParts[textParts.length - 1].text; // Usually the main output is in the last text block
|
|
30
|
+
// Some versions of the MCP return error text without the `isError` boolean flag
|
|
31
|
+
if (typeof text === 'string' && text.trim().startsWith('### Error')) {
|
|
32
|
+
throw new BrowserConnectError(text.trim(), 'Please check if the browser is running or if the Playwright MCP / CDP connection is configured correctly.');
|
|
33
|
+
}
|
|
25
34
|
// MCP browser_evaluate returns: "[JSON]\n### Ran Playwright code\n```js\n...\n```"
|
|
26
35
|
// Strip the "### Ran Playwright code" suffix to get clean JSON
|
|
27
36
|
const codeMarker = text.indexOf('### Ran Playwright code');
|
package/dist/browser.test.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { PlaywrightMCP, __test__ } from './browser/index.js';
|
|
3
|
+
afterEach(() => {
|
|
4
|
+
__test__.resetMcpServerPathCache();
|
|
5
|
+
__test__.setMcpDiscoveryTestHooks();
|
|
6
|
+
delete process.env.OPENCLI_MCP_SERVER_PATH;
|
|
7
|
+
});
|
|
3
8
|
describe('browser helpers', () => {
|
|
4
9
|
it('creates JSON-RPC requests with unique ids', () => {
|
|
5
10
|
const first = __test__.createJsonRpcRequest('tools/call', { name: 'browser_tabs' });
|
|
@@ -91,9 +96,138 @@ describe('browser helpers', () => {
|
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
});
|
|
99
|
+
it('builds a direct node launch spec when a local MCP path is available', () => {
|
|
100
|
+
const savedCI = process.env.CI;
|
|
101
|
+
delete process.env.CI;
|
|
102
|
+
try {
|
|
103
|
+
expect(__test__.buildMcpLaunchSpec({
|
|
104
|
+
mcpPath: '/tmp/cli.js',
|
|
105
|
+
executablePath: '/usr/bin/google-chrome',
|
|
106
|
+
})).toEqual({
|
|
107
|
+
command: 'node',
|
|
108
|
+
args: ['/tmp/cli.js', '--extension', '--executable-path', '/usr/bin/google-chrome'],
|
|
109
|
+
usedNpxFallback: false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
if (savedCI !== undefined) {
|
|
114
|
+
process.env.CI = savedCI;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
delete process.env.CI;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
it('falls back to npx bootstrap when no MCP path is available', () => {
|
|
122
|
+
const savedCI = process.env.CI;
|
|
123
|
+
delete process.env.CI;
|
|
124
|
+
try {
|
|
125
|
+
expect(__test__.buildMcpLaunchSpec({
|
|
126
|
+
mcpPath: null,
|
|
127
|
+
})).toEqual({
|
|
128
|
+
command: 'npx',
|
|
129
|
+
args: ['-y', '@playwright/mcp@latest', '--extension'],
|
|
130
|
+
usedNpxFallback: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (savedCI !== undefined) {
|
|
135
|
+
process.env.CI = savedCI;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
delete process.env.CI;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
94
142
|
it('times out slow promises', async () => {
|
|
95
143
|
await expect(__test__.withTimeoutMs(new Promise(() => { }), 10, 'timeout')).rejects.toThrow('timeout');
|
|
96
144
|
});
|
|
145
|
+
it('prefers OPENCLI_MCP_SERVER_PATH over discovered locations', () => {
|
|
146
|
+
process.env.OPENCLI_MCP_SERVER_PATH = '/env/mcp/cli.js';
|
|
147
|
+
const existsSync = vi.fn((candidate) => candidate === '/env/mcp/cli.js');
|
|
148
|
+
const execSync = vi.fn();
|
|
149
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync });
|
|
150
|
+
expect(__test__.findMcpServerPath()).toBe('/env/mcp/cli.js');
|
|
151
|
+
expect(execSync).not.toHaveBeenCalled();
|
|
152
|
+
expect(existsSync).toHaveBeenCalledWith('/env/mcp/cli.js');
|
|
153
|
+
});
|
|
154
|
+
it('discovers global @playwright/mcp from the current Node runtime prefix', () => {
|
|
155
|
+
const originalExecPath = process.execPath;
|
|
156
|
+
const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
|
|
157
|
+
const runtimeGlobalMcp = '/opt/homebrew/Cellar/node/25.2.1/lib/node_modules/@playwright/mcp/cli.js';
|
|
158
|
+
Object.defineProperty(process, 'execPath', {
|
|
159
|
+
value: runtimeExecPath,
|
|
160
|
+
configurable: true,
|
|
161
|
+
});
|
|
162
|
+
const existsSync = vi.fn((candidate) => candidate === runtimeGlobalMcp);
|
|
163
|
+
const execSync = vi.fn();
|
|
164
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync });
|
|
165
|
+
try {
|
|
166
|
+
expect(__test__.findMcpServerPath()).toBe(runtimeGlobalMcp);
|
|
167
|
+
expect(execSync).not.toHaveBeenCalled();
|
|
168
|
+
expect(existsSync).toHaveBeenCalledWith(runtimeGlobalMcp);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
Object.defineProperty(process, 'execPath', {
|
|
172
|
+
value: originalExecPath,
|
|
173
|
+
configurable: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
it('falls back to npm root -g when runtime prefix lookup misses', () => {
|
|
178
|
+
const originalExecPath = process.execPath;
|
|
179
|
+
const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
|
|
180
|
+
const runtimeGlobalMcp = '/opt/homebrew/Cellar/node/25.2.1/lib/node_modules/@playwright/mcp/cli.js';
|
|
181
|
+
const npmRootGlobal = '/Users/jakevin/.nvm/versions/node/v22.14.0/lib/node_modules';
|
|
182
|
+
const npmGlobalMcp = '/Users/jakevin/.nvm/versions/node/v22.14.0/lib/node_modules/@playwright/mcp/cli.js';
|
|
183
|
+
Object.defineProperty(process, 'execPath', {
|
|
184
|
+
value: runtimeExecPath,
|
|
185
|
+
configurable: true,
|
|
186
|
+
});
|
|
187
|
+
const existsSync = vi.fn((candidate) => candidate === npmGlobalMcp);
|
|
188
|
+
const execSync = vi.fn((command) => {
|
|
189
|
+
if (String(command).includes('npm root -g'))
|
|
190
|
+
return `${npmRootGlobal}\n`;
|
|
191
|
+
throw new Error(`unexpected command: ${String(command)}`);
|
|
192
|
+
});
|
|
193
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync });
|
|
194
|
+
try {
|
|
195
|
+
expect(__test__.findMcpServerPath()).toBe(npmGlobalMcp);
|
|
196
|
+
expect(execSync).toHaveBeenCalledOnce();
|
|
197
|
+
expect(existsSync).toHaveBeenCalledWith(runtimeGlobalMcp);
|
|
198
|
+
expect(existsSync).toHaveBeenCalledWith(npmGlobalMcp);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
Object.defineProperty(process, 'execPath', {
|
|
202
|
+
value: originalExecPath,
|
|
203
|
+
configurable: true,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
it('returns null when new global discovery paths are unavailable', () => {
|
|
208
|
+
const originalExecPath = process.execPath;
|
|
209
|
+
const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
|
|
210
|
+
Object.defineProperty(process, 'execPath', {
|
|
211
|
+
value: runtimeExecPath,
|
|
212
|
+
configurable: true,
|
|
213
|
+
});
|
|
214
|
+
const existsSync = vi.fn(() => false);
|
|
215
|
+
const execSync = vi.fn((command) => {
|
|
216
|
+
if (String(command).includes('npm root -g'))
|
|
217
|
+
return '/missing/global/node_modules\n';
|
|
218
|
+
throw new Error(`missing command: ${String(command)}`);
|
|
219
|
+
});
|
|
220
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync });
|
|
221
|
+
try {
|
|
222
|
+
expect(__test__.findMcpServerPath()).toBeNull();
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
Object.defineProperty(process, 'execPath', {
|
|
226
|
+
value: originalExecPath,
|
|
227
|
+
configurable: true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
97
231
|
});
|
|
98
232
|
describe('PlaywrightMCP state', () => {
|
|
99
233
|
it('transitions to closed after close()', async () => {
|
package/dist/cli-manifest.json
CHANGED
|
@@ -1,4 +1,167 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"site": "barchart",
|
|
4
|
+
"name": "flow",
|
|
5
|
+
"description": "Barchart unusual options activity / options flow",
|
|
6
|
+
"strategy": "cookie",
|
|
7
|
+
"browser": true,
|
|
8
|
+
"args": [
|
|
9
|
+
{
|
|
10
|
+
"name": "type",
|
|
11
|
+
"type": "str",
|
|
12
|
+
"default": "all",
|
|
13
|
+
"required": false,
|
|
14
|
+
"help": "Filter: all, call, or put"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "limit",
|
|
18
|
+
"type": "int",
|
|
19
|
+
"default": 20,
|
|
20
|
+
"required": false,
|
|
21
|
+
"help": "Number of results"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"type": "ts",
|
|
25
|
+
"modulePath": "barchart/flow.js",
|
|
26
|
+
"domain": "www.barchart.com",
|
|
27
|
+
"columns": [
|
|
28
|
+
"symbol",
|
|
29
|
+
"type",
|
|
30
|
+
"strike",
|
|
31
|
+
"expiration",
|
|
32
|
+
"last",
|
|
33
|
+
"volume",
|
|
34
|
+
"openInterest",
|
|
35
|
+
"volOiRatio",
|
|
36
|
+
"iv"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"site": "barchart",
|
|
41
|
+
"name": "greeks",
|
|
42
|
+
"description": "Barchart options greeks overview (IV, delta, gamma, theta, vega)",
|
|
43
|
+
"strategy": "cookie",
|
|
44
|
+
"browser": true,
|
|
45
|
+
"args": [
|
|
46
|
+
{
|
|
47
|
+
"name": "symbol",
|
|
48
|
+
"type": "str",
|
|
49
|
+
"required": true,
|
|
50
|
+
"help": "Stock ticker (e.g. AAPL)"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "expiration",
|
|
54
|
+
"type": "str",
|
|
55
|
+
"required": false,
|
|
56
|
+
"help": "Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "limit",
|
|
60
|
+
"type": "int",
|
|
61
|
+
"default": 10,
|
|
62
|
+
"required": false,
|
|
63
|
+
"help": "Number of near-the-money strikes per type"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"type": "ts",
|
|
67
|
+
"modulePath": "barchart/greeks.js",
|
|
68
|
+
"domain": "www.barchart.com",
|
|
69
|
+
"columns": [
|
|
70
|
+
"type",
|
|
71
|
+
"strike",
|
|
72
|
+
"last",
|
|
73
|
+
"iv",
|
|
74
|
+
"delta",
|
|
75
|
+
"gamma",
|
|
76
|
+
"theta",
|
|
77
|
+
"vega",
|
|
78
|
+
"rho",
|
|
79
|
+
"volume",
|
|
80
|
+
"openInterest",
|
|
81
|
+
"expiration"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"site": "barchart",
|
|
86
|
+
"name": "options",
|
|
87
|
+
"description": "Barchart options chain with greeks, IV, volume, and open interest",
|
|
88
|
+
"strategy": "cookie",
|
|
89
|
+
"browser": true,
|
|
90
|
+
"args": [
|
|
91
|
+
{
|
|
92
|
+
"name": "symbol",
|
|
93
|
+
"type": "str",
|
|
94
|
+
"required": true,
|
|
95
|
+
"help": "Stock ticker (e.g. AAPL)"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "type",
|
|
99
|
+
"type": "str",
|
|
100
|
+
"default": "Call",
|
|
101
|
+
"required": false,
|
|
102
|
+
"help": "Option type: Call or Put"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "limit",
|
|
106
|
+
"type": "int",
|
|
107
|
+
"default": 20,
|
|
108
|
+
"required": false,
|
|
109
|
+
"help": "Max number of strikes to return"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"type": "ts",
|
|
113
|
+
"modulePath": "barchart/options.js",
|
|
114
|
+
"domain": "www.barchart.com",
|
|
115
|
+
"columns": [
|
|
116
|
+
"strike",
|
|
117
|
+
"bid",
|
|
118
|
+
"ask",
|
|
119
|
+
"last",
|
|
120
|
+
"change",
|
|
121
|
+
"volume",
|
|
122
|
+
"openInterest",
|
|
123
|
+
"iv",
|
|
124
|
+
"delta",
|
|
125
|
+
"gamma",
|
|
126
|
+
"theta",
|
|
127
|
+
"vega",
|
|
128
|
+
"expiration"
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"site": "barchart",
|
|
133
|
+
"name": "quote",
|
|
134
|
+
"description": "Barchart stock quote with price, volume, and key metrics",
|
|
135
|
+
"strategy": "cookie",
|
|
136
|
+
"browser": true,
|
|
137
|
+
"args": [
|
|
138
|
+
{
|
|
139
|
+
"name": "symbol",
|
|
140
|
+
"type": "str",
|
|
141
|
+
"required": true,
|
|
142
|
+
"help": "Stock ticker (e.g. AAPL, MSFT, TSLA)"
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"type": "ts",
|
|
146
|
+
"modulePath": "barchart/quote.js",
|
|
147
|
+
"domain": "www.barchart.com",
|
|
148
|
+
"columns": [
|
|
149
|
+
"symbol",
|
|
150
|
+
"name",
|
|
151
|
+
"price",
|
|
152
|
+
"change",
|
|
153
|
+
"changePct",
|
|
154
|
+
"open",
|
|
155
|
+
"high",
|
|
156
|
+
"low",
|
|
157
|
+
"prevClose",
|
|
158
|
+
"volume",
|
|
159
|
+
"avgVolume",
|
|
160
|
+
"marketCap",
|
|
161
|
+
"peRatio",
|
|
162
|
+
"eps"
|
|
163
|
+
]
|
|
164
|
+
},
|
|
2
165
|
{
|
|
3
166
|
"site": "bbc",
|
|
4
167
|
"name": "news",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart unusual options activity (options flow).
|
|
3
|
+
* Shows high volume/OI ratio trades that may indicate institutional activity.
|
|
4
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
cli({
|
|
8
|
+
site: 'barchart',
|
|
9
|
+
name: 'flow',
|
|
10
|
+
description: 'Barchart unusual options activity / options flow',
|
|
11
|
+
domain: 'www.barchart.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'type', type: 'str', default: 'all', help: 'Filter: all, call, or put', choices: ['all', 'call', 'put'] },
|
|
15
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
16
|
+
],
|
|
17
|
+
columns: [
|
|
18
|
+
'symbol', 'type', 'strike', 'expiration', 'last',
|
|
19
|
+
'volume', 'openInterest', 'volOiRatio', 'iv',
|
|
20
|
+
],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
const optionType = kwargs.type || 'all';
|
|
23
|
+
const limit = kwargs.limit ?? 20;
|
|
24
|
+
await page.goto('https://www.barchart.com/options/unusual-activity/stocks');
|
|
25
|
+
await page.wait(5);
|
|
26
|
+
const data = await page.evaluate(`
|
|
27
|
+
(async () => {
|
|
28
|
+
const limit = ${limit};
|
|
29
|
+
const typeFilter = '${optionType}'.toLowerCase();
|
|
30
|
+
|
|
31
|
+
// Wait for CSRF token to appear (Angular may inject it after initial render)
|
|
32
|
+
let csrf = '';
|
|
33
|
+
for (let i = 0; i < 10; i++) {
|
|
34
|
+
csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
35
|
+
if (csrf) break;
|
|
36
|
+
await new Promise(r => setTimeout(r, 500));
|
|
37
|
+
}
|
|
38
|
+
if (!csrf) return { error: 'no-csrf' };
|
|
39
|
+
|
|
40
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
41
|
+
const fields = [
|
|
42
|
+
'baseSymbol','strikePrice','expirationDate','optionType',
|
|
43
|
+
'lastPrice','volume','openInterest','volumeOpenInterestRatio','volatility',
|
|
44
|
+
].join(',');
|
|
45
|
+
|
|
46
|
+
// Fetch extra rows when filtering by type since server-side filter doesn't work
|
|
47
|
+
const fetchLimit = typeFilter !== 'all' ? limit * 3 : limit;
|
|
48
|
+
|
|
49
|
+
// Try unusual_activity first, fall back to mostActive (unusual_activity is
|
|
50
|
+
// empty outside market hours)
|
|
51
|
+
const lists = [
|
|
52
|
+
'options.unusual_activity.stocks.us',
|
|
53
|
+
'options.mostActive.us',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const list of lists) {
|
|
57
|
+
try {
|
|
58
|
+
const url = '/proxies/core-api/v1/options/get?list=' + list
|
|
59
|
+
+ '&fields=' + fields
|
|
60
|
+
+ '&orderBy=volumeOpenInterestRatio&orderDir=desc'
|
|
61
|
+
+ '&raw=1&limit=' + fetchLimit;
|
|
62
|
+
|
|
63
|
+
const resp = await fetch(url, { credentials: 'include', headers });
|
|
64
|
+
if (!resp.ok) continue;
|
|
65
|
+
const d = await resp.json();
|
|
66
|
+
let items = d?.data || [];
|
|
67
|
+
if (items.length === 0) continue;
|
|
68
|
+
|
|
69
|
+
// Apply client-side type filter
|
|
70
|
+
if (typeFilter !== 'all') {
|
|
71
|
+
items = items.filter(i => {
|
|
72
|
+
const t = ((i.raw || i).optionType || '').toLowerCase();
|
|
73
|
+
return t === typeFilter;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return items.slice(0, limit).map(i => {
|
|
77
|
+
const r = i.raw || i;
|
|
78
|
+
return {
|
|
79
|
+
symbol: r.baseSymbol || r.symbol,
|
|
80
|
+
type: r.optionType,
|
|
81
|
+
strike: r.strikePrice,
|
|
82
|
+
expiration: r.expirationDate,
|
|
83
|
+
last: r.lastPrice,
|
|
84
|
+
volume: r.volume,
|
|
85
|
+
openInterest: r.openInterest,
|
|
86
|
+
volOiRatio: r.volumeOpenInterestRatio,
|
|
87
|
+
iv: r.volatility,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
} catch(e) {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [];
|
|
94
|
+
})()
|
|
95
|
+
`);
|
|
96
|
+
if (!data)
|
|
97
|
+
return [];
|
|
98
|
+
if (data.error === 'no-csrf') {
|
|
99
|
+
throw new Error('Could not extract CSRF token from barchart.com. Make sure you are logged in.');
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(data))
|
|
102
|
+
return [];
|
|
103
|
+
return data.slice(0, limit).map(r => ({
|
|
104
|
+
symbol: r.symbol || '',
|
|
105
|
+
type: r.type || '',
|
|
106
|
+
strike: r.strike,
|
|
107
|
+
expiration: r.expiration ?? null,
|
|
108
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
109
|
+
volume: r.volume,
|
|
110
|
+
openInterest: r.openInterest,
|
|
111
|
+
volOiRatio: r.volOiRatio != null ? Number(Number(r.volOiRatio).toFixed(2)) : null,
|
|
112
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart options greeks overview — IV, delta, gamma, theta, vega, rho
|
|
3
|
+
* for near-the-money options on a given symbol.
|
|
4
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
cli({
|
|
8
|
+
site: 'barchart',
|
|
9
|
+
name: 'greeks',
|
|
10
|
+
description: 'Barchart options greeks overview (IV, delta, gamma, theta, vega)',
|
|
11
|
+
domain: 'www.barchart.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
|
|
15
|
+
{ name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration.' },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of near-the-money strikes per type' },
|
|
17
|
+
],
|
|
18
|
+
columns: [
|
|
19
|
+
'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
|
|
20
|
+
'volume', 'openInterest', 'expiration',
|
|
21
|
+
],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const symbol = kwargs.symbol.toUpperCase().trim();
|
|
24
|
+
const expiration = kwargs.expiration ?? '';
|
|
25
|
+
const limit = kwargs.limit ?? 10;
|
|
26
|
+
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
|
|
27
|
+
await page.wait(4);
|
|
28
|
+
const data = await page.evaluate(`
|
|
29
|
+
(async () => {
|
|
30
|
+
const sym = '${symbol}';
|
|
31
|
+
const expDate = '${expiration}';
|
|
32
|
+
const limit = ${limit};
|
|
33
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
34
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const fields = [
|
|
38
|
+
'strikePrice','lastPrice','volume','openInterest',
|
|
39
|
+
'volatility','delta','gamma','theta','vega','rho',
|
|
40
|
+
'expirationDate','optionType','percentFromLast',
|
|
41
|
+
].join(',');
|
|
42
|
+
|
|
43
|
+
let url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
|
|
44
|
+
+ '&fields=' + fields + '&raw=1';
|
|
45
|
+
if (expDate) url += '&expirationDate=' + encodeURIComponent(expDate);
|
|
46
|
+
const resp = await fetch(url, { credentials: 'include', headers });
|
|
47
|
+
if (resp.ok) {
|
|
48
|
+
const d = await resp.json();
|
|
49
|
+
let items = d?.data || [];
|
|
50
|
+
|
|
51
|
+
if (!expDate) {
|
|
52
|
+
const expirations = items
|
|
53
|
+
.map(i => (i.raw || i).expirationDate || null)
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.sort((a, b) => {
|
|
56
|
+
const aTime = Date.parse(a);
|
|
57
|
+
const bTime = Date.parse(b);
|
|
58
|
+
if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
|
|
59
|
+
if (Number.isNaN(aTime)) return 1;
|
|
60
|
+
if (Number.isNaN(bTime)) return -1;
|
|
61
|
+
return aTime - bTime;
|
|
62
|
+
});
|
|
63
|
+
const nearestExpiration = expirations[0];
|
|
64
|
+
if (nearestExpiration) {
|
|
65
|
+
items = items.filter(i => ((i.raw || i).expirationDate || null) === nearestExpiration);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Separate calls and puts, sort by distance from current price
|
|
70
|
+
const calls = items
|
|
71
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'call')
|
|
72
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
73
|
+
.slice(0, limit);
|
|
74
|
+
const puts = items
|
|
75
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put')
|
|
76
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
77
|
+
.slice(0, limit);
|
|
78
|
+
|
|
79
|
+
return [...calls, ...puts].map(i => {
|
|
80
|
+
const r = i.raw || i;
|
|
81
|
+
return {
|
|
82
|
+
type: r.optionType,
|
|
83
|
+
strike: r.strikePrice,
|
|
84
|
+
last: r.lastPrice,
|
|
85
|
+
iv: r.volatility,
|
|
86
|
+
delta: r.delta,
|
|
87
|
+
gamma: r.gamma,
|
|
88
|
+
theta: r.theta,
|
|
89
|
+
vega: r.vega,
|
|
90
|
+
rho: r.rho,
|
|
91
|
+
volume: r.volume,
|
|
92
|
+
openInterest: r.openInterest,
|
|
93
|
+
expiration: r.expirationDate,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} catch(e) {}
|
|
98
|
+
|
|
99
|
+
return [];
|
|
100
|
+
})()
|
|
101
|
+
`);
|
|
102
|
+
if (!data || !Array.isArray(data))
|
|
103
|
+
return [];
|
|
104
|
+
return data.map(r => ({
|
|
105
|
+
type: r.type || '',
|
|
106
|
+
strike: r.strike,
|
|
107
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
108
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
109
|
+
delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
|
|
110
|
+
gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
|
|
111
|
+
theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
|
|
112
|
+
vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
|
|
113
|
+
rho: r.rho != null ? Number(Number(r.rho).toFixed(4)) : null,
|
|
114
|
+
volume: r.volume,
|
|
115
|
+
openInterest: r.openInterest,
|
|
116
|
+
expiration: r.expiration ?? null,
|
|
117
|
+
}));
|
|
118
|
+
},
|
|
119
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|