@jackwener/opencli 0.7.10 → 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.
@@ -0,0 +1,30 @@
1
+ name: Publish Any Commit
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+ pull_request:
7
+ branches: [main, dev]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ publish:
13
+ if: ${{ vars.PKG_PR_NEW_ENABLED == 'true' }}
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '22'
21
+ cache: 'npm'
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Build
27
+ run: npm run build
28
+
29
+ - name: Publish to pkg.pr.new
30
+ run: npx pkg-pr-new publish
package/README.md CHANGED
@@ -44,6 +44,7 @@ A CLI tool that turns **any website** into a command-line interface — Bilibili
44
44
  > **⚠️ Important**: Browser commands reuse your Chrome login session. You must be logged into the target website in Chrome before running commands. If you get empty data or errors, check your login status first.
45
45
 
46
46
  OpenCLI connects to your browser through the Playwright MCP Bridge extension.
47
+ It prefers an existing local/global `@playwright/mcp` install and falls back to `npx -y @playwright/mcp@latest` automatically when no local MCP server is found.
47
48
 
48
49
  ### Playwright MCP Bridge Extension Setup
49
50
 
package/README.zh-CN.md CHANGED
@@ -43,6 +43,7 @@ OpenCLI 将任何网站变成命令行工具 — B站、知乎、小红书、Twi
43
43
  > **⚠️ 重要**:大多数命令复用你的 Chrome 登录状态。运行命令前,你必须已在 Chrome 中打开目标网站并完成登录。如果获取到空数据或报错,请先检查你的浏览器登录状态。
44
44
 
45
45
  OpenCLI 通过 Playwright MCP Bridge 扩展与你的浏览器通信。
46
+ 它会优先复用本地或全局已安装的 `@playwright/mcp`,如果没有嗅探到可用 MCP server,则会自动回退到 `npx -y @playwright/mcp@latest` 启动。
46
47
 
47
48
  ### Playwright MCP Bridge 扩展配置
48
49
 
@@ -1,8 +1,23 @@
1
1
  /**
2
2
  * MCP server path discovery and argument building.
3
3
  */
4
+ import { execSync } from 'node:child_process';
5
+ import * as fs from 'node:fs';
6
+ export declare function resetMcpServerPathCache(): void;
7
+ export declare function setMcpDiscoveryTestHooks(input?: {
8
+ existsSync?: typeof fs.existsSync;
9
+ execSync?: typeof execSync;
10
+ }): void;
4
11
  export declare function findMcpServerPath(): string | null;
5
12
  export declare function buildMcpArgs(input: {
6
13
  mcpPath: string;
7
14
  executablePath?: string | null;
8
15
  }): string[];
16
+ export declare function buildMcpLaunchSpec(input: {
17
+ mcpPath?: string | null;
18
+ executablePath?: string | null;
19
+ }): {
20
+ command: string;
21
+ args: string[];
22
+ usedNpxFallback: boolean;
23
+ };
@@ -7,27 +7,57 @@ import * as fs from 'node:fs';
7
7
  import * as os from 'node:os';
8
8
  import * as path from 'node:path';
9
9
  let _cachedMcpServerPath;
10
+ let _existsSync = fs.existsSync;
11
+ let _execSync = execSync;
12
+ export function resetMcpServerPathCache() {
13
+ _cachedMcpServerPath = undefined;
14
+ }
15
+ export function setMcpDiscoveryTestHooks(input) {
16
+ _existsSync = input?.existsSync ?? fs.existsSync;
17
+ _execSync = input?.execSync ?? execSync;
18
+ }
10
19
  export function findMcpServerPath() {
11
20
  if (_cachedMcpServerPath !== undefined)
12
21
  return _cachedMcpServerPath;
13
22
  const envMcp = process.env.OPENCLI_MCP_SERVER_PATH;
14
- if (envMcp && fs.existsSync(envMcp)) {
23
+ if (envMcp && _existsSync(envMcp)) {
15
24
  _cachedMcpServerPath = envMcp;
16
25
  return _cachedMcpServerPath;
17
26
  }
18
27
  // Check local node_modules first (@playwright/mcp is the modern package)
19
28
  const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
20
- if (fs.existsSync(localMcp)) {
29
+ if (_existsSync(localMcp)) {
21
30
  _cachedMcpServerPath = localMcp;
22
31
  return _cachedMcpServerPath;
23
32
  }
24
33
  // Check project-relative path
25
34
  const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
26
35
  const projectMcp = path.resolve(__dirname2, '..', '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
27
- if (fs.existsSync(projectMcp)) {
36
+ if (_existsSync(projectMcp)) {
28
37
  _cachedMcpServerPath = projectMcp;
29
38
  return _cachedMcpServerPath;
30
39
  }
40
+ // Check global npm/yarn locations derived from current Node runtime.
41
+ const nodePrefix = path.resolve(path.dirname(process.execPath), '..');
42
+ const globalNodeModules = path.join(nodePrefix, 'lib', 'node_modules');
43
+ const globalMcp = path.join(globalNodeModules, '@playwright', 'mcp', 'cli.js');
44
+ if (_existsSync(globalMcp)) {
45
+ _cachedMcpServerPath = globalMcp;
46
+ return _cachedMcpServerPath;
47
+ }
48
+ // Check npm global root directly.
49
+ try {
50
+ const npmRootGlobal = _execSync('npm root -g 2>/dev/null', {
51
+ encoding: 'utf-8',
52
+ timeout: 5000,
53
+ }).trim();
54
+ const npmGlobalMcp = path.join(npmRootGlobal, '@playwright', 'mcp', 'cli.js');
55
+ if (npmRootGlobal && _existsSync(npmGlobalMcp)) {
56
+ _cachedMcpServerPath = npmGlobalMcp;
57
+ return _cachedMcpServerPath;
58
+ }
59
+ }
60
+ catch { }
31
61
  // Check common locations
32
62
  const candidates = [
33
63
  path.join(os.homedir(), '.npm', '_npx'),
@@ -36,8 +66,8 @@ export function findMcpServerPath() {
36
66
  ];
37
67
  // Try npx resolution (legacy package name)
38
68
  try {
39
- const result = execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
40
- if (result && fs.existsSync(result)) {
69
+ const result = _execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
70
+ if (result && _existsSync(result)) {
41
71
  _cachedMcpServerPath = result;
42
72
  return _cachedMcpServerPath;
43
73
  }
@@ -45,8 +75,8 @@ export function findMcpServerPath() {
45
75
  catch { }
46
76
  // Try which
47
77
  try {
48
- const result = execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
49
- if (result && fs.existsSync(result)) {
78
+ const result = _execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
79
+ if (result && _existsSync(result)) {
50
80
  _cachedMcpServerPath = result;
51
81
  return _cachedMcpServerPath;
52
82
  }
@@ -54,10 +84,10 @@ export function findMcpServerPath() {
54
84
  catch { }
55
85
  // Search in common npx cache
56
86
  for (const base of candidates) {
57
- if (!fs.existsSync(base))
87
+ if (!_existsSync(base))
58
88
  continue;
59
89
  try {
60
- const found = execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
90
+ const found = _execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
61
91
  if (found) {
62
92
  _cachedMcpServerPath = found;
63
93
  return _cachedMcpServerPath;
@@ -68,16 +98,34 @@ export function findMcpServerPath() {
68
98
  _cachedMcpServerPath = null;
69
99
  return _cachedMcpServerPath;
70
100
  }
71
- export function buildMcpArgs(input) {
72
- const args = [input.mcpPath];
101
+ function buildRuntimeArgs(input) {
102
+ const args = [];
73
103
  if (!process.env.CI) {
74
104
  // Local: always connect to user's running Chrome via MCP Bridge extension
75
105
  args.push('--extension');
76
106
  }
77
107
  // CI: standalone mode — @playwright/mcp launches its own browser (headed by default).
78
108
  // xvfb provides a virtual display for headed mode in GitHub Actions.
79
- if (input.executablePath) {
109
+ if (input?.executablePath) {
80
110
  args.push('--executable-path', input.executablePath);
81
111
  }
82
112
  return args;
83
113
  }
114
+ export function buildMcpArgs(input) {
115
+ return [input.mcpPath, ...buildRuntimeArgs(input)];
116
+ }
117
+ export function buildMcpLaunchSpec(input) {
118
+ const runtimeArgs = buildRuntimeArgs(input);
119
+ if (input.mcpPath) {
120
+ return {
121
+ command: 'node',
122
+ args: [input.mcpPath, ...runtimeArgs],
123
+ usedNpxFallback: false,
124
+ };
125
+ }
126
+ return {
127
+ command: 'npx',
128
+ args: ['-y', '@playwright/mcp@latest', ...runtimeArgs],
129
+ usedNpxFallback: true,
130
+ };
131
+ }
@@ -10,7 +10,7 @@ export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
10
10
  export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
11
11
  import { createJsonRpcRequest } from './mcp.js';
12
12
  import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
13
- import { buildMcpArgs } from './discover.js';
13
+ import { buildMcpArgs, buildMcpLaunchSpec, findMcpServerPath, resetMcpServerPathCache, setMcpDiscoveryTestHooks } from './discover.js';
14
14
  import { withTimeoutMs } from '../runtime.js';
15
15
  export declare const __test__: {
16
16
  createJsonRpcRequest: typeof createJsonRpcRequest;
@@ -18,5 +18,9 @@ export declare const __test__: {
18
18
  diffTabIndexes: typeof diffTabIndexes;
19
19
  appendLimited: typeof appendLimited;
20
20
  buildMcpArgs: typeof buildMcpArgs;
21
+ buildMcpLaunchSpec: typeof buildMcpLaunchSpec;
22
+ findMcpServerPath: typeof findMcpServerPath;
23
+ resetMcpServerPathCache: typeof resetMcpServerPathCache;
24
+ setMcpDiscoveryTestHooks: typeof setMcpDiscoveryTestHooks;
21
25
  withTimeoutMs: typeof withTimeoutMs;
22
26
  };
@@ -10,7 +10,7 @@ export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
10
10
  // Test-only helpers — exposed for unit tests
11
11
  import { createJsonRpcRequest } from './mcp.js';
12
12
  import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
13
- import { buildMcpArgs } from './discover.js';
13
+ import { buildMcpArgs, buildMcpLaunchSpec, findMcpServerPath, resetMcpServerPathCache, setMcpDiscoveryTestHooks } from './discover.js';
14
14
  import { withTimeoutMs } from '../runtime.js';
15
15
  export const __test__ = {
16
16
  createJsonRpcRequest,
@@ -18,5 +18,9 @@ export const __test__ = {
18
18
  diffTabIndexes,
19
19
  appendLimited,
20
20
  buildMcpArgs,
21
+ buildMcpLaunchSpec,
22
+ findMcpServerPath,
23
+ resetMcpServerPathCache,
24
+ setMcpDiscoveryTestHooks,
21
25
  withTimeoutMs,
22
26
  };
@@ -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, buildMcpArgs } from './discover.js';
10
+ import { findMcpServerPath, buildMcpLaunchSpec } 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';
@@ -148,7 +146,7 @@ export class PlaywrightMCP {
148
146
  stderr: stderrBuffer,
149
147
  }));
150
148
  }, timeout * 1000);
151
- const mcpArgs = buildMcpArgs({
149
+ const launchSpec = buildMcpLaunchSpec({
152
150
  mcpPath,
153
151
  executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
154
152
  });
@@ -156,9 +154,12 @@ export class PlaywrightMCP {
156
154
  console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
157
155
  if (useExtension)
158
156
  console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
157
+ if (launchSpec.usedNpxFallback) {
158
+ console.error('[opencli] Playwright MCP not found locally; bootstrapping via npx @playwright/mcp@latest');
159
+ }
159
160
  }
160
- debugLog(`Spawning node ${mcpArgs.join(' ')}`);
161
- this._proc = spawn('node', mcpArgs, {
161
+ debugLog(`Spawning ${launchSpec.command} ${launchSpec.args.join(' ')}`);
162
+ this._proc = spawn(launchSpec.command, launchSpec.args, {
162
163
  stdio: ['pipe', 'pipe', 'pipe'],
163
164
  env: { ...process.env },
164
165
  });
@@ -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 () => {
@@ -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 {};