@jackwener/opencli 0.7.9 → 0.7.11

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.
Files changed (66) hide show
  1. package/.github/workflows/pkg-pr-new.yml +30 -0
  2. package/README.md +1 -0
  3. package/README.zh-CN.md +1 -0
  4. package/dist/browser/discover.d.ts +15 -0
  5. package/dist/browser/discover.js +60 -12
  6. package/dist/browser/index.d.ts +5 -1
  7. package/dist/browser/index.js +5 -1
  8. package/dist/browser/mcp.js +7 -6
  9. package/dist/browser.test.js +135 -1
  10. package/dist/cli-manifest.json +170 -88
  11. package/dist/clis/barchart/flow.js +117 -0
  12. package/dist/clis/barchart/greeks.js +119 -0
  13. package/dist/clis/barchart/options.js +106 -0
  14. package/dist/clis/barchart/quote.js +133 -0
  15. package/dist/clis/twitter/bookmarks.d.ts +1 -0
  16. package/dist/clis/twitter/bookmarks.js +171 -0
  17. package/dist/clis/twitter/delete.js +0 -1
  18. package/dist/clis/twitter/followers.js +5 -16
  19. package/dist/clis/twitter/following.js +3 -4
  20. package/dist/clis/twitter/like.js +0 -1
  21. package/dist/clis/twitter/notifications.js +17 -7
  22. package/dist/clis/twitter/search.js +14 -6
  23. package/dist/clis/twitter/trending.yaml +8 -2
  24. package/dist/main.js +0 -0
  25. package/package.json +1 -1
  26. package/src/browser/discover.ts +73 -12
  27. package/src/browser/index.ts +5 -1
  28. package/src/browser/mcp.ts +7 -5
  29. package/src/browser.test.ts +140 -1
  30. package/src/clis/barchart/flow.ts +121 -0
  31. package/src/clis/barchart/greeks.ts +123 -0
  32. package/src/clis/barchart/options.ts +110 -0
  33. package/src/clis/barchart/quote.ts +137 -0
  34. package/src/clis/twitter/bookmarks.ts +201 -0
  35. package/src/clis/twitter/delete.ts +0 -1
  36. package/src/clis/twitter/followers.ts +5 -16
  37. package/src/clis/twitter/following.ts +3 -5
  38. package/src/clis/twitter/like.ts +0 -1
  39. package/src/clis/twitter/notifications.ts +18 -9
  40. package/src/clis/twitter/search.ts +14 -7
  41. package/src/clis/twitter/trending.yaml +8 -2
  42. package/vitest.config.ts +7 -0
  43. package/dist/_debug.js +0 -7
  44. package/dist/browser-tab.d.ts +0 -2
  45. package/dist/browser-tab.js +0 -30
  46. package/dist/browser.d.ts +0 -105
  47. package/dist/browser.js +0 -644
  48. package/dist/clis/github/search.js +0 -20
  49. package/dist/clis/index.d.ts +0 -27
  50. package/dist/clis/index.js +0 -41
  51. package/dist/clis/twitter/bookmarks.yaml +0 -85
  52. package/dist/clis/xiaohongshu/me.js +0 -86
  53. package/dist/pipeline/_debug.js +0 -7
  54. package/dist/promote.d.ts +0 -1
  55. package/dist/promote.js +0 -3
  56. package/dist/register.d.ts +0 -2
  57. package/dist/register.js +0 -2
  58. package/dist/scaffold.d.ts +0 -2
  59. package/dist/scaffold.js +0 -2
  60. package/dist/smoke.d.ts +0 -2
  61. package/dist/smoke.js +0 -2
  62. package/src/clis/twitter/bookmarks.yaml +0 -85
  63. /package/dist/{_debug.d.ts → clis/barchart/flow.d.ts} +0 -0
  64. /package/dist/clis/{github/search.d.ts → barchart/greeks.d.ts} +0 -0
  65. /package/dist/clis/{xiaohongshu/me.d.ts → barchart/options.d.ts} +0 -0
  66. /package/dist/{pipeline/_debug.d.ts → clis/barchart/quote.d.ts} +0 -0
@@ -9,19 +9,33 @@ import * as os from 'node:os';
9
9
  import * as path from 'node:path';
10
10
 
11
11
  let _cachedMcpServerPath: string | null | undefined;
12
+ let _existsSync = fs.existsSync;
13
+ let _execSync = execSync;
14
+
15
+ export function resetMcpServerPathCache(): void {
16
+ _cachedMcpServerPath = undefined;
17
+ }
18
+
19
+ export function setMcpDiscoveryTestHooks(input?: {
20
+ existsSync?: typeof fs.existsSync;
21
+ execSync?: typeof execSync;
22
+ }): void {
23
+ _existsSync = input?.existsSync ?? fs.existsSync;
24
+ _execSync = input?.execSync ?? execSync;
25
+ }
12
26
 
13
27
  export function findMcpServerPath(): string | null {
14
28
  if (_cachedMcpServerPath !== undefined) return _cachedMcpServerPath;
15
29
 
16
30
  const envMcp = process.env.OPENCLI_MCP_SERVER_PATH;
17
- if (envMcp && fs.existsSync(envMcp)) {
31
+ if (envMcp && _existsSync(envMcp)) {
18
32
  _cachedMcpServerPath = envMcp;
19
33
  return _cachedMcpServerPath;
20
34
  }
21
35
 
22
36
  // Check local node_modules first (@playwright/mcp is the modern package)
23
37
  const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
24
- if (fs.existsSync(localMcp)) {
38
+ if (_existsSync(localMcp)) {
25
39
  _cachedMcpServerPath = localMcp;
26
40
  return _cachedMcpServerPath;
27
41
  }
@@ -29,11 +43,33 @@ export function findMcpServerPath(): string | null {
29
43
  // Check project-relative path
30
44
  const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
31
45
  const projectMcp = path.resolve(__dirname2, '..', '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
32
- if (fs.existsSync(projectMcp)) {
46
+ if (_existsSync(projectMcp)) {
33
47
  _cachedMcpServerPath = projectMcp;
34
48
  return _cachedMcpServerPath;
35
49
  }
36
50
 
51
+ // Check global npm/yarn locations derived from current Node runtime.
52
+ const nodePrefix = path.resolve(path.dirname(process.execPath), '..');
53
+ const globalNodeModules = path.join(nodePrefix, 'lib', 'node_modules');
54
+ const globalMcp = path.join(globalNodeModules, '@playwright', 'mcp', 'cli.js');
55
+ if (_existsSync(globalMcp)) {
56
+ _cachedMcpServerPath = globalMcp;
57
+ return _cachedMcpServerPath;
58
+ }
59
+
60
+ // Check npm global root directly.
61
+ try {
62
+ const npmRootGlobal = _execSync('npm root -g 2>/dev/null', {
63
+ encoding: 'utf-8',
64
+ timeout: 5000,
65
+ }).trim();
66
+ const npmGlobalMcp = path.join(npmRootGlobal, '@playwright', 'mcp', 'cli.js');
67
+ if (npmRootGlobal && _existsSync(npmGlobalMcp)) {
68
+ _cachedMcpServerPath = npmGlobalMcp;
69
+ return _cachedMcpServerPath;
70
+ }
71
+ } catch {}
72
+
37
73
  // Check common locations
38
74
  const candidates = [
39
75
  path.join(os.homedir(), '.npm', '_npx'),
@@ -43,8 +79,8 @@ export function findMcpServerPath(): string | null {
43
79
 
44
80
  // Try npx resolution (legacy package name)
45
81
  try {
46
- const result = execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
47
- if (result && fs.existsSync(result)) {
82
+ const result = _execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
83
+ if (result && _existsSync(result)) {
48
84
  _cachedMcpServerPath = result;
49
85
  return _cachedMcpServerPath;
50
86
  }
@@ -52,8 +88,8 @@ export function findMcpServerPath(): string | null {
52
88
 
53
89
  // Try which
54
90
  try {
55
- const result = execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
56
- if (result && fs.existsSync(result)) {
91
+ const result = _execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
92
+ if (result && _existsSync(result)) {
57
93
  _cachedMcpServerPath = result;
58
94
  return _cachedMcpServerPath;
59
95
  }
@@ -61,9 +97,9 @@ export function findMcpServerPath(): string | null {
61
97
 
62
98
  // Search in common npx cache
63
99
  for (const base of candidates) {
64
- if (!fs.existsSync(base)) continue;
100
+ if (!_existsSync(base)) continue;
65
101
  try {
66
- const found = execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
102
+ const found = _execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
67
103
  if (found) {
68
104
  _cachedMcpServerPath = found;
69
105
  return _cachedMcpServerPath;
@@ -75,16 +111,41 @@ export function findMcpServerPath(): string | null {
75
111
  return _cachedMcpServerPath;
76
112
  }
77
113
 
78
- export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] {
79
- const args = [input.mcpPath];
114
+ function buildRuntimeArgs(input?: { executablePath?: string | null }): string[] {
115
+ const args: string[] = [];
80
116
  if (!process.env.CI) {
81
117
  // Local: always connect to user's running Chrome via MCP Bridge extension
82
118
  args.push('--extension');
83
119
  }
84
120
  // CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
85
121
  // xvfb provides a virtual display for headed mode in GitHub Actions.
86
- if (input.executablePath) {
122
+ if (input?.executablePath) {
87
123
  args.push('--executable-path', input.executablePath);
88
124
  }
89
125
  return args;
90
126
  }
127
+
128
+ export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null }): string[] {
129
+ return [input.mcpPath, ...buildRuntimeArgs(input)];
130
+ }
131
+
132
+ export function buildMcpLaunchSpec(input: { mcpPath?: string | null; executablePath?: string | null }): {
133
+ command: string;
134
+ args: string[];
135
+ usedNpxFallback: boolean;
136
+ } {
137
+ const runtimeArgs = buildRuntimeArgs(input);
138
+ if (input.mcpPath) {
139
+ return {
140
+ command: 'node',
141
+ args: [input.mcpPath, ...runtimeArgs],
142
+ usedNpxFallback: false,
143
+ };
144
+ }
145
+
146
+ return {
147
+ command: 'npx',
148
+ args: ['-y', '@playwright/mcp@latest', ...runtimeArgs],
149
+ usedNpxFallback: true,
150
+ };
151
+ }
@@ -13,7 +13,7 @@ export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
13
13
  // Test-only helpers — exposed for unit tests
14
14
  import { createJsonRpcRequest } from './mcp.js';
15
15
  import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
16
- import { buildMcpArgs } from './discover.js';
16
+ import { buildMcpArgs, buildMcpLaunchSpec, findMcpServerPath, resetMcpServerPathCache, setMcpDiscoveryTestHooks } from './discover.js';
17
17
  import { withTimeoutMs } from '../runtime.js';
18
18
 
19
19
  export const __test__ = {
@@ -22,5 +22,9 @@ export const __test__ = {
22
22
  diffTabIndexes,
23
23
  appendLimited,
24
24
  buildMcpArgs,
25
+ buildMcpLaunchSpec,
26
+ findMcpServerPath,
27
+ resetMcpServerPathCache,
28
+ setMcpDiscoveryTestHooks,
25
29
  withTimeoutMs,
26
30
  };
@@ -9,7 +9,7 @@ import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
9
9
  import { PKG_VERSION } from '../version.js';
10
10
  import { Page } from './page.js';
11
11
  import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js';
12
- import { findMcpServerPath, buildMcpArgs } from './discover.js';
12
+ import { findMcpServerPath, buildMcpLaunchSpec } from './discover.js';
13
13
  import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
14
14
 
15
15
  const STDERR_BUFFER_LIMIT = 16 * 1024;
@@ -105,7 +105,6 @@ export class PlaywrightMCP {
105
105
  if (this._state === 'closed') throw new Error('Playwright MCP session is closed');
106
106
 
107
107
  const mcpPath = findMcpServerPath();
108
- if (!mcpPath) throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
109
108
 
110
109
  PlaywrightMCP._registerGlobalCleanup();
111
110
  PlaywrightMCP._activeInsts.add(this);
@@ -154,17 +153,20 @@ export class PlaywrightMCP {
154
153
  }));
155
154
  }, timeout * 1000);
156
155
 
157
- const mcpArgs = buildMcpArgs({
156
+ const launchSpec = buildMcpLaunchSpec({
158
157
  mcpPath,
159
158
  executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
160
159
  });
161
160
  if (process.env.OPENCLI_VERBOSE) {
162
161
  console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
163
162
  if (useExtension) console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
163
+ if (launchSpec.usedNpxFallback) {
164
+ console.error('[opencli] Playwright MCP not found locally; bootstrapping via npx @playwright/mcp@latest');
165
+ }
164
166
  }
165
- debugLog(`Spawning node ${mcpArgs.join(' ')}`);
167
+ debugLog(`Spawning ${launchSpec.command} ${launchSpec.args.join(' ')}`);
166
168
 
167
- this._proc = spawn('node', mcpArgs, {
169
+ this._proc = spawn(launchSpec.command, launchSpec.args, {
168
170
  stdio: ['pipe', 'pipe', 'pipe'],
169
171
  env: { ...process.env },
170
172
  });
@@ -1,6 +1,12 @@
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
3
 
4
+ afterEach(() => {
5
+ __test__.resetMcpServerPathCache();
6
+ __test__.setMcpDiscoveryTestHooks();
7
+ delete process.env.OPENCLI_MCP_SERVER_PATH;
8
+ });
9
+
4
10
  describe('browser helpers', () => {
5
11
  it('creates JSON-RPC requests with unique ids', () => {
6
12
  const first = __test__.createJsonRpcRequest('tools/call', { name: 'browser_tabs' });
@@ -106,9 +112,142 @@ describe('browser helpers', () => {
106
112
  }
107
113
  });
108
114
 
115
+ it('builds a direct node launch spec when a local MCP path is available', () => {
116
+ const savedCI = process.env.CI;
117
+ delete process.env.CI;
118
+ try {
119
+ expect(__test__.buildMcpLaunchSpec({
120
+ mcpPath: '/tmp/cli.js',
121
+ executablePath: '/usr/bin/google-chrome',
122
+ })).toEqual({
123
+ command: 'node',
124
+ args: ['/tmp/cli.js', '--extension', '--executable-path', '/usr/bin/google-chrome'],
125
+ usedNpxFallback: false,
126
+ });
127
+ } finally {
128
+ if (savedCI !== undefined) {
129
+ process.env.CI = savedCI;
130
+ } else {
131
+ delete process.env.CI;
132
+ }
133
+ }
134
+ });
135
+
136
+ it('falls back to npx bootstrap when no MCP path is available', () => {
137
+ const savedCI = process.env.CI;
138
+ delete process.env.CI;
139
+ try {
140
+ expect(__test__.buildMcpLaunchSpec({
141
+ mcpPath: null,
142
+ })).toEqual({
143
+ command: 'npx',
144
+ args: ['-y', '@playwright/mcp@latest', '--extension'],
145
+ usedNpxFallback: true,
146
+ });
147
+ } finally {
148
+ if (savedCI !== undefined) {
149
+ process.env.CI = savedCI;
150
+ } else {
151
+ delete process.env.CI;
152
+ }
153
+ }
154
+ });
155
+
109
156
  it('times out slow promises', async () => {
110
157
  await expect(__test__.withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout');
111
158
  });
159
+
160
+ it('prefers OPENCLI_MCP_SERVER_PATH over discovered locations', () => {
161
+ process.env.OPENCLI_MCP_SERVER_PATH = '/env/mcp/cli.js';
162
+ const existsSync = vi.fn((candidate: any) => candidate === '/env/mcp/cli.js');
163
+ const execSync = vi.fn();
164
+ __test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
165
+
166
+ expect(__test__.findMcpServerPath()).toBe('/env/mcp/cli.js');
167
+ expect(execSync).not.toHaveBeenCalled();
168
+ expect(existsSync).toHaveBeenCalledWith('/env/mcp/cli.js');
169
+ });
170
+
171
+ it('discovers global @playwright/mcp from the current Node runtime prefix', () => {
172
+ const originalExecPath = process.execPath;
173
+ const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
174
+ const runtimeGlobalMcp = '/opt/homebrew/Cellar/node/25.2.1/lib/node_modules/@playwright/mcp/cli.js';
175
+ Object.defineProperty(process, 'execPath', {
176
+ value: runtimeExecPath,
177
+ configurable: true,
178
+ });
179
+
180
+ const existsSync = vi.fn((candidate: any) => candidate === runtimeGlobalMcp);
181
+ const execSync = vi.fn();
182
+ __test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
183
+
184
+ try {
185
+ expect(__test__.findMcpServerPath()).toBe(runtimeGlobalMcp);
186
+ expect(execSync).not.toHaveBeenCalled();
187
+ expect(existsSync).toHaveBeenCalledWith(runtimeGlobalMcp);
188
+ } finally {
189
+ Object.defineProperty(process, 'execPath', {
190
+ value: originalExecPath,
191
+ configurable: true,
192
+ });
193
+ }
194
+ });
195
+
196
+ it('falls back to npm root -g when runtime prefix lookup misses', () => {
197
+ const originalExecPath = process.execPath;
198
+ const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
199
+ const runtimeGlobalMcp = '/opt/homebrew/Cellar/node/25.2.1/lib/node_modules/@playwright/mcp/cli.js';
200
+ const npmRootGlobal = '/Users/jakevin/.nvm/versions/node/v22.14.0/lib/node_modules';
201
+ const npmGlobalMcp = '/Users/jakevin/.nvm/versions/node/v22.14.0/lib/node_modules/@playwright/mcp/cli.js';
202
+ Object.defineProperty(process, 'execPath', {
203
+ value: runtimeExecPath,
204
+ configurable: true,
205
+ });
206
+
207
+ const existsSync = vi.fn((candidate: any) => candidate === npmGlobalMcp);
208
+ const execSync = vi.fn((command: string) => {
209
+ if (String(command).includes('npm root -g')) return `${npmRootGlobal}\n` as any;
210
+ throw new Error(`unexpected command: ${String(command)}`);
211
+ });
212
+ __test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
213
+
214
+ try {
215
+ expect(__test__.findMcpServerPath()).toBe(npmGlobalMcp);
216
+ expect(execSync).toHaveBeenCalledOnce();
217
+ expect(existsSync).toHaveBeenCalledWith(runtimeGlobalMcp);
218
+ expect(existsSync).toHaveBeenCalledWith(npmGlobalMcp);
219
+ } finally {
220
+ Object.defineProperty(process, 'execPath', {
221
+ value: originalExecPath,
222
+ configurable: true,
223
+ });
224
+ }
225
+ });
226
+
227
+ it('returns null when new global discovery paths are unavailable', () => {
228
+ const originalExecPath = process.execPath;
229
+ const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
230
+ Object.defineProperty(process, 'execPath', {
231
+ value: runtimeExecPath,
232
+ configurable: true,
233
+ });
234
+
235
+ const existsSync = vi.fn(() => false);
236
+ const execSync = vi.fn((command: string) => {
237
+ if (String(command).includes('npm root -g')) return '/missing/global/node_modules\n' as any;
238
+ throw new Error(`missing command: ${String(command)}`);
239
+ });
240
+ __test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
241
+
242
+ try {
243
+ expect(__test__.findMcpServerPath()).toBeNull();
244
+ } finally {
245
+ Object.defineProperty(process, 'execPath', {
246
+ value: originalExecPath,
247
+ configurable: true,
248
+ });
249
+ }
250
+ });
112
251
  });
113
252
 
114
253
  describe('PlaywrightMCP state', () => {
@@ -0,0 +1,121 @@
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
+
8
+ cli({
9
+ site: 'barchart',
10
+ name: 'flow',
11
+ description: 'Barchart unusual options activity / options flow',
12
+ domain: 'www.barchart.com',
13
+ strategy: Strategy.COOKIE,
14
+ args: [
15
+ { name: 'type', type: 'str', default: 'all', help: 'Filter: all, call, or put', choices: ['all', 'call', 'put'] },
16
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
17
+ ],
18
+ columns: [
19
+ 'symbol', 'type', 'strike', 'expiration', 'last',
20
+ 'volume', 'openInterest', 'volOiRatio', 'iv',
21
+ ],
22
+ func: async (page, kwargs) => {
23
+ const optionType = kwargs.type || 'all';
24
+ const limit = kwargs.limit ?? 20;
25
+
26
+ await page.goto('https://www.barchart.com/options/unusual-activity/stocks');
27
+ await page.wait(5);
28
+
29
+ const data = await page.evaluate(`
30
+ (async () => {
31
+ const limit = ${limit};
32
+ const typeFilter = '${optionType}'.toLowerCase();
33
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
34
+ const headers = { 'X-CSRF-TOKEN': csrf };
35
+
36
+ const fields = [
37
+ 'baseSymbol','strikePrice','expirationDate','optionType',
38
+ 'lastPrice','volume','openInterest','volumeOpenInterestRatio','volatility',
39
+ ].join(',');
40
+
41
+ // Fetch extra rows when filtering by type since server-side filter may not work
42
+ const fetchLimit = typeFilter !== 'all' ? limit * 3 : limit;
43
+ try {
44
+ const url = '/proxies/core-api/v1/options/get?list=options.unusual_activity.stocks.us'
45
+ + '&fields=' + fields
46
+ + '&orderBy=volumeOpenInterestRatio&orderDir=desc'
47
+ + '&raw=1&limit=' + fetchLimit;
48
+
49
+ const resp = await fetch(url, { credentials: 'include', headers });
50
+ if (resp.ok) {
51
+ const d = await resp.json();
52
+ let items = d?.data || [];
53
+ if (items.length > 0) {
54
+ // Apply client-side type filter
55
+ if (typeFilter !== 'all') {
56
+ items = items.filter(i => {
57
+ const t = ((i.raw || i).optionType || '').toLowerCase();
58
+ return t === typeFilter;
59
+ });
60
+ }
61
+ return items.slice(0, limit).map(i => {
62
+ const r = i.raw || i;
63
+ return {
64
+ symbol: r.baseSymbol || r.symbol,
65
+ type: r.optionType,
66
+ strike: r.strikePrice,
67
+ expiration: r.expirationDate,
68
+ last: r.lastPrice,
69
+ volume: r.volume,
70
+ openInterest: r.openInterest,
71
+ volOiRatio: r.volumeOpenInterestRatio,
72
+ iv: r.volatility,
73
+ };
74
+ });
75
+ }
76
+ }
77
+ } catch(e) {}
78
+
79
+ // Fallback: parse from DOM table
80
+ try {
81
+ const rows = document.querySelectorAll('tr[data-ng-repeat], tbody tr');
82
+ const results = [];
83
+ for (const row of rows) {
84
+ const cells = row.querySelectorAll('td');
85
+ if (cells.length < 6) continue;
86
+ const getText = (idx) => cells[idx]?.textContent?.trim() || null;
87
+ results.push({
88
+ symbol: getText(0),
89
+ type: getText(1),
90
+ strike: getText(2),
91
+ expiration: getText(3),
92
+ last: getText(4),
93
+ volume: getText(5),
94
+ openInterest: cells.length > 6 ? getText(6) : null,
95
+ volOiRatio: cells.length > 7 ? getText(7) : null,
96
+ iv: cells.length > 8 ? getText(8) : null,
97
+ });
98
+ if (results.length >= limit) break;
99
+ }
100
+ return results;
101
+ } catch(e) {
102
+ return [];
103
+ }
104
+ })()
105
+ `);
106
+
107
+ if (!data || !Array.isArray(data)) return [];
108
+
109
+ return data.slice(0, limit).map(r => ({
110
+ symbol: r.symbol || '',
111
+ type: r.type || '',
112
+ strike: r.strike,
113
+ expiration: r.expiration ?? null,
114
+ last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
115
+ volume: r.volume,
116
+ openInterest: r.openInterest,
117
+ volOiRatio: r.volOiRatio != null ? Number(Number(r.volOiRatio).toFixed(2)) : null,
118
+ iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
119
+ }));
120
+ },
121
+ });
@@ -0,0 +1,123 @@
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
+
8
+ cli({
9
+ site: 'barchart',
10
+ name: 'greeks',
11
+ description: 'Barchart options greeks overview (IV, delta, gamma, theta, vega)',
12
+ domain: 'www.barchart.com',
13
+ strategy: Strategy.COOKIE,
14
+ args: [
15
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
16
+ { name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration.' },
17
+ { name: 'limit', type: 'int', default: 10, help: 'Number of near-the-money strikes per type' },
18
+ ],
19
+ columns: [
20
+ 'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
21
+ 'volume', 'openInterest', 'expiration',
22
+ ],
23
+ func: async (page, kwargs) => {
24
+ const symbol = kwargs.symbol.toUpperCase().trim();
25
+ const expiration = kwargs.expiration ?? '';
26
+ const limit = kwargs.limit ?? 10;
27
+
28
+ await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
29
+ await page.wait(4);
30
+
31
+ const data = await page.evaluate(`
32
+ (async () => {
33
+ const sym = '${symbol}';
34
+ const expDate = '${expiration}';
35
+ const limit = ${limit};
36
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
37
+ const headers = { 'X-CSRF-TOKEN': csrf };
38
+
39
+ try {
40
+ const fields = [
41
+ 'strikePrice','lastPrice','volume','openInterest',
42
+ 'volatility','delta','gamma','theta','vega','rho',
43
+ 'expirationDate','optionType','percentFromLast',
44
+ ].join(',');
45
+
46
+ let url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
47
+ + '&fields=' + fields + '&raw=1';
48
+ if (expDate) url += '&expirationDate=' + encodeURIComponent(expDate);
49
+ const resp = await fetch(url, { credentials: 'include', headers });
50
+ if (resp.ok) {
51
+ const d = await resp.json();
52
+ let items = d?.data || [];
53
+
54
+ if (!expDate) {
55
+ const expirations = items
56
+ .map(i => (i.raw || i).expirationDate || null)
57
+ .filter(Boolean)
58
+ .sort((a, b) => {
59
+ const aTime = Date.parse(a);
60
+ const bTime = Date.parse(b);
61
+ if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
62
+ if (Number.isNaN(aTime)) return 1;
63
+ if (Number.isNaN(bTime)) return -1;
64
+ return aTime - bTime;
65
+ });
66
+ const nearestExpiration = expirations[0];
67
+ if (nearestExpiration) {
68
+ items = items.filter(i => ((i.raw || i).expirationDate || null) === nearestExpiration);
69
+ }
70
+ }
71
+
72
+ // Separate calls and puts, sort by distance from current price
73
+ const calls = items
74
+ .filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'call')
75
+ .sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
76
+ .slice(0, limit);
77
+ const puts = items
78
+ .filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put')
79
+ .sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
80
+ .slice(0, limit);
81
+
82
+ return [...calls, ...puts].map(i => {
83
+ const r = i.raw || i;
84
+ return {
85
+ type: r.optionType,
86
+ strike: r.strikePrice,
87
+ last: r.lastPrice,
88
+ iv: r.volatility,
89
+ delta: r.delta,
90
+ gamma: r.gamma,
91
+ theta: r.theta,
92
+ vega: r.vega,
93
+ rho: r.rho,
94
+ volume: r.volume,
95
+ openInterest: r.openInterest,
96
+ expiration: r.expirationDate,
97
+ };
98
+ });
99
+ }
100
+ } catch(e) {}
101
+
102
+ return [];
103
+ })()
104
+ `);
105
+
106
+ if (!data || !Array.isArray(data)) return [];
107
+
108
+ return data.map(r => ({
109
+ type: r.type || '',
110
+ strike: r.strike,
111
+ last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
112
+ iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
113
+ delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
114
+ gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
115
+ theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
116
+ vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
117
+ rho: r.rho != null ? Number(Number(r.rho).toFixed(4)) : null,
118
+ volume: r.volume,
119
+ openInterest: r.openInterest,
120
+ expiration: r.expiration ?? null,
121
+ }));
122
+ },
123
+ });