@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.
Files changed (47) hide show
  1. package/.github/workflows/pkg-pr-new.yml +30 -0
  2. package/CDP.md +103 -0
  3. package/CDP.zh-CN.md +103 -0
  4. package/README.md +5 -0
  5. package/README.zh-CN.md +5 -0
  6. package/dist/browser/discover.d.ts +30 -0
  7. package/dist/browser/discover.js +128 -14
  8. package/dist/browser/errors.d.ts +2 -1
  9. package/dist/browser/errors.js +13 -0
  10. package/dist/browser/index.d.ts +6 -1
  11. package/dist/browser/index.js +6 -1
  12. package/dist/browser/mcp.js +14 -8
  13. package/dist/browser/page.js +11 -2
  14. package/dist/browser.test.js +135 -1
  15. package/dist/cli-manifest.json +163 -0
  16. package/dist/clis/barchart/flow.d.ts +1 -0
  17. package/dist/clis/barchart/flow.js +115 -0
  18. package/dist/clis/barchart/greeks.d.ts +1 -0
  19. package/dist/clis/barchart/greeks.js +119 -0
  20. package/dist/clis/barchart/options.d.ts +1 -0
  21. package/dist/clis/barchart/options.js +106 -0
  22. package/dist/clis/barchart/quote.d.ts +1 -0
  23. package/dist/clis/barchart/quote.js +133 -0
  24. package/dist/doctor.js +8 -0
  25. package/dist/engine.d.ts +1 -1
  26. package/dist/engine.js +59 -1
  27. package/dist/main.js +2 -15
  28. package/dist/pipeline/executor.js +2 -24
  29. package/dist/pipeline/registry.d.ts +19 -0
  30. package/dist/pipeline/registry.js +41 -0
  31. package/package.json +1 -1
  32. package/src/browser/discover.ts +149 -14
  33. package/src/browser/errors.ts +17 -1
  34. package/src/browser/index.ts +6 -1
  35. package/src/browser/mcp.ts +14 -7
  36. package/src/browser/page.ts +21 -2
  37. package/src/browser.test.ts +140 -1
  38. package/src/clis/barchart/flow.ts +120 -0
  39. package/src/clis/barchart/greeks.ts +123 -0
  40. package/src/clis/barchart/options.ts +110 -0
  41. package/src/clis/barchart/quote.ts +137 -0
  42. package/src/doctor.ts +9 -0
  43. package/src/engine.ts +58 -1
  44. package/src/main.ts +6 -11
  45. package/src/pipeline/executor.ts +2 -28
  46. package/src/pipeline/registry.ts +60 -0
  47. package/vitest.config.ts +7 -0
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Barchart options chain — strike, bid/ask, volume, OI, greeks, IV.
3
+ * Auth: CSRF token from <meta name="csrf-token"> + session cookies.
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'barchart',
8
+ name: 'options',
9
+ description: 'Barchart options chain with greeks, IV, volume, and open interest',
10
+ domain: 'www.barchart.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
14
+ { name: 'type', type: 'str', default: 'Call', help: 'Option type: Call or Put', choices: ['Call', 'Put'] },
15
+ { name: 'limit', type: 'int', default: 20, help: 'Max number of strikes to return' },
16
+ ],
17
+ columns: [
18
+ 'strike', 'bid', 'ask', 'last', 'change', 'volume', 'openInterest',
19
+ 'iv', 'delta', 'gamma', 'theta', 'vega', 'expiration',
20
+ ],
21
+ func: async (page, kwargs) => {
22
+ const symbol = kwargs.symbol.toUpperCase().trim();
23
+ const optType = kwargs.type || 'Call';
24
+ const limit = kwargs.limit ?? 20;
25
+ await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
26
+ await page.wait(4);
27
+ const data = await page.evaluate(`
28
+ (async () => {
29
+ const sym = '${symbol}';
30
+ const type = '${optType}';
31
+ const limit = ${limit};
32
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
33
+ const headers = { 'X-CSRF-TOKEN': csrf };
34
+
35
+ // API: options chain with greeks
36
+ try {
37
+ const fields = [
38
+ 'strikePrice','bidPrice','askPrice','lastPrice','priceChange',
39
+ 'volume','openInterest','volatility',
40
+ 'delta','gamma','theta','vega',
41
+ 'expirationDate','optionType','percentFromLast',
42
+ ].join(',');
43
+
44
+ const url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
45
+ + '&fields=' + fields + '&raw=1';
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
+ // Filter by type
52
+ items = items.filter(i => {
53
+ const t = (i.raw || i).optionType || '';
54
+ return t.toLowerCase() === type.toLowerCase();
55
+ });
56
+
57
+ // Sort by closeness to current price
58
+ items.sort((a, b) => {
59
+ const aD = Math.abs((a.raw || a).percentFromLast || 999);
60
+ const bD = Math.abs((b.raw || b).percentFromLast || 999);
61
+ return aD - bD;
62
+ });
63
+
64
+ return items.slice(0, limit).map(i => {
65
+ const r = i.raw || i;
66
+ return {
67
+ strike: r.strikePrice,
68
+ bid: r.bidPrice,
69
+ ask: r.askPrice,
70
+ last: r.lastPrice,
71
+ change: r.priceChange,
72
+ volume: r.volume,
73
+ openInterest: r.openInterest,
74
+ iv: r.volatility,
75
+ delta: r.delta,
76
+ gamma: r.gamma,
77
+ theta: r.theta,
78
+ vega: r.vega,
79
+ expiration: r.expirationDate,
80
+ };
81
+ });
82
+ }
83
+ } catch(e) {}
84
+
85
+ return [];
86
+ })()
87
+ `);
88
+ if (!data || !Array.isArray(data))
89
+ return [];
90
+ return data.map(r => ({
91
+ strike: r.strike,
92
+ bid: r.bid != null ? Number(Number(r.bid).toFixed(2)) : null,
93
+ ask: r.ask != null ? Number(Number(r.ask).toFixed(2)) : null,
94
+ last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
95
+ change: r.change != null ? Number(Number(r.change).toFixed(2)) : null,
96
+ volume: r.volume,
97
+ openInterest: r.openInterest,
98
+ iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
99
+ delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
100
+ gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
101
+ theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
102
+ vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
103
+ expiration: r.expiration ?? null,
104
+ }));
105
+ },
106
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Barchart stock quote — price, volume, market cap, P/E, EPS, and key metrics.
3
+ * Auth: CSRF token from <meta name="csrf-token"> + session cookies.
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+ cli({
7
+ site: 'barchart',
8
+ name: 'quote',
9
+ description: 'Barchart stock quote with price, volume, and key metrics',
10
+ domain: 'www.barchart.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
14
+ ],
15
+ columns: [
16
+ 'symbol', 'name', 'price', 'change', 'changePct',
17
+ 'open', 'high', 'low', 'prevClose', 'volume',
18
+ 'avgVolume', 'marketCap', 'peRatio', 'eps',
19
+ ],
20
+ func: async (page, kwargs) => {
21
+ const symbol = kwargs.symbol.toUpperCase().trim();
22
+ await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/overview`);
23
+ await page.wait(4);
24
+ const data = await page.evaluate(`
25
+ (async () => {
26
+ const sym = '${symbol}';
27
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
28
+
29
+ // Strategy 1: internal proxy API with CSRF token
30
+ try {
31
+ const fields = [
32
+ 'symbol','symbolName','lastPrice','priceChange','percentChange',
33
+ 'highPrice','lowPrice','openPrice','previousPrice','volume','averageVolume',
34
+ 'marketCap','peRatio','earningsPerShare','tradeTime',
35
+ ].join(',');
36
+ const url = '/proxies/core-api/v1/quotes/get?symbol=' + encodeURIComponent(sym) + '&fields=' + fields;
37
+ const resp = await fetch(url, {
38
+ credentials: 'include',
39
+ headers: { 'X-CSRF-TOKEN': csrf },
40
+ });
41
+ if (resp.ok) {
42
+ const d = await resp.json();
43
+ const row = d?.data?.[0] || null;
44
+ if (row) {
45
+ return { source: 'api', row };
46
+ }
47
+ }
48
+ } catch(e) {}
49
+
50
+ // Strategy 2: parse from DOM
51
+ try {
52
+ const priceEl = document.querySelector('span.last-change');
53
+ const price = priceEl ? priceEl.textContent.trim() : null;
54
+
55
+ // Change values are sibling spans inside .pricechangerow > .last-change
56
+ const changeParent = priceEl?.parentElement;
57
+ const changeSpans = changeParent ? changeParent.querySelectorAll('span') : [];
58
+ let change = null;
59
+ let changePct = null;
60
+ for (const s of changeSpans) {
61
+ const t = s.textContent.trim();
62
+ if (s === priceEl) continue;
63
+ if (t.includes('%')) changePct = t.replace(/[()]/g, '');
64
+ else if (t.match(/^[+-]?[\\d.]+$/)) change = t;
65
+ }
66
+
67
+ // Financial data rows
68
+ const rows = document.querySelectorAll('.financial-data-row');
69
+ const fdata = {};
70
+ for (const row of rows) {
71
+ const spans = row.querySelectorAll('span');
72
+ if (spans.length >= 2) {
73
+ const label = spans[0].textContent.trim();
74
+ const valSpan = row.querySelector('span.right span:not(.ng-hide)');
75
+ fdata[label] = valSpan ? valSpan.textContent.trim() : '';
76
+ }
77
+ }
78
+
79
+ // Day high/low from row chart
80
+ const dayLow = document.querySelector('.bc-quote-row-chart .small-6:first-child .inline:not(.ng-hide)');
81
+ const dayHigh = document.querySelector('.bc-quote-row-chart .text-right .inline:not(.ng-hide)');
82
+ const openEl = document.querySelector('.mark span');
83
+ const openText = openEl ? openEl.textContent.trim().replace('Open ', '') : null;
84
+
85
+ const name = document.querySelector('h1 span.symbol');
86
+
87
+ return {
88
+ source: 'dom',
89
+ row: {
90
+ symbol: sym,
91
+ symbolName: name ? name.textContent.trim() : sym,
92
+ lastPrice: price,
93
+ priceChange: change,
94
+ percentChange: changePct,
95
+ open: openText,
96
+ highPrice: dayHigh ? dayHigh.textContent.trim() : null,
97
+ lowPrice: dayLow ? dayLow.textContent.trim() : null,
98
+ previousClose: fdata['Previous Close'] || null,
99
+ volume: fdata['Volume'] || null,
100
+ averageVolume: fdata['Average Volume'] || null,
101
+ marketCap: null,
102
+ peRatio: null,
103
+ earningsPerShare: null,
104
+ }
105
+ };
106
+ } catch(e) {
107
+ return { error: 'Could not fetch quote for ' + sym + ': ' + e.message };
108
+ }
109
+ })()
110
+ `);
111
+ if (!data || data.error)
112
+ return [];
113
+ const r = data.row || {};
114
+ // API returns formatted strings like "+1.41" and "+0.56%"; use raw if available
115
+ const raw = r.raw || {};
116
+ return [{
117
+ symbol: r.symbol || symbol,
118
+ name: r.symbolName || r.name || symbol,
119
+ price: r.lastPrice ?? null,
120
+ change: r.priceChange ?? null,
121
+ changePct: r.percentChange ?? null,
122
+ open: r.openPrice ?? r.open ?? null,
123
+ high: r.highPrice ?? null,
124
+ low: r.lowPrice ?? null,
125
+ prevClose: r.previousPrice ?? r.previousClose ?? null,
126
+ volume: r.volume ?? null,
127
+ avgVolume: r.averageVolume ?? null,
128
+ marketCap: r.marketCap ?? null,
129
+ peRatio: r.peRatio ?? null,
130
+ eps: r.earningsPerShare ?? null,
131
+ }];
132
+ },
133
+ });
package/dist/doctor.js CHANGED
@@ -504,6 +504,14 @@ export function renderBrowserDoctorReport(report) {
504
504
  const uniqueFingerprints = [...new Set(tokenFingerprints)];
505
505
  const hasMismatch = uniqueFingerprints.length > 1;
506
506
  const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
507
+ // CDP endpoint mode (for remote/server environments)
508
+ const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
509
+ if (cdpEndpoint) {
510
+ lines.push(statusLine('OK', `CDP endpoint: ${chalk.cyan(cdpEndpoint)}`));
511
+ lines.push(chalk.dim(' → Remote Chrome mode: extension token not required'));
512
+ lines.push('');
513
+ return lines.join('\n');
514
+ }
507
515
  const installStatus = report.extensionInstalled ? 'OK' : 'MISSING';
508
516
  const installDetail = report.extensionInstalled
509
517
  ? `Extension installed (${report.extensionBrowsers.join(', ')})`
package/dist/engine.d.ts CHANGED
@@ -17,4 +17,4 @@ export declare function discoverClis(...dirs: string[]): Promise<void>;
17
17
  /**
18
18
  * Execute a CLI command. Handles lazy-loading of TS modules.
19
19
  */
20
- export declare function executeCommand(cmd: CliCommand, page: IPage | null, kwargs: Record<string, any>, debug?: boolean): Promise<any>;
20
+ export declare function executeCommand(cmd: CliCommand, page: IPage | null, rawKwargs: Record<string, any>, debug?: boolean): Promise<any>;
package/dist/engine.js CHANGED
@@ -155,10 +155,68 @@ function registerYamlCli(filePath, defaultSite) {
155
155
  log.warn(`Failed to load ${filePath}: ${err.message}`);
156
156
  }
157
157
  }
158
+ /**
159
+ * Validates and coerces arguments based on the command's Arg definitions.
160
+ */
161
+ function coerceAndValidateArgs(cmdArgs, kwargs) {
162
+ const result = { ...kwargs };
163
+ for (const argDef of cmdArgs) {
164
+ const val = result[argDef.name];
165
+ // 1. Check required
166
+ if (argDef.required && (val === undefined || val === null || val === '')) {
167
+ throw new Error(`Argument "${argDef.name}" is required.\n${argDef.help ? `Hint: ${argDef.help}` : ''}`);
168
+ }
169
+ if (val !== undefined && val !== null) {
170
+ // 2. Type coercion
171
+ if (argDef.type === 'int' || argDef.type === 'number') {
172
+ const num = Number(val);
173
+ if (Number.isNaN(num)) {
174
+ throw new Error(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`);
175
+ }
176
+ result[argDef.name] = num;
177
+ }
178
+ else if (argDef.type === 'boolean' || argDef.type === 'bool') {
179
+ if (typeof val === 'string') {
180
+ const lower = val.toLowerCase();
181
+ if (lower === 'true' || lower === '1')
182
+ result[argDef.name] = true;
183
+ else if (lower === 'false' || lower === '0')
184
+ result[argDef.name] = false;
185
+ else
186
+ throw new Error(`Argument "${argDef.name}" must be a boolean (true/false). Received: "${val}"`);
187
+ }
188
+ else {
189
+ result[argDef.name] = Boolean(val);
190
+ }
191
+ }
192
+ // 3. Choices validation
193
+ const coercedVal = result[argDef.name];
194
+ if (argDef.choices && argDef.choices.length > 0) {
195
+ // Only stringent check for string/number types against choices array
196
+ if (!argDef.choices.map(String).includes(String(coercedVal))) {
197
+ throw new Error(`Argument "${argDef.name}" must be one of: ${argDef.choices.join(', ')}. Received: "${coercedVal}"`);
198
+ }
199
+ }
200
+ }
201
+ else if (argDef.default !== undefined) {
202
+ // Set default if value is missing
203
+ result[argDef.name] = argDef.default;
204
+ }
205
+ }
206
+ return result;
207
+ }
158
208
  /**
159
209
  * Execute a CLI command. Handles lazy-loading of TS modules.
160
210
  */
161
- export async function executeCommand(cmd, page, kwargs, debug = false) {
211
+ export async function executeCommand(cmd, page, rawKwargs, debug = false) {
212
+ let kwargs;
213
+ try {
214
+ kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
215
+ }
216
+ catch (err) {
217
+ // Re-throw validation errors clearly
218
+ throw new Error(`[Argument Validation Error]\n${err.message}`);
219
+ }
162
220
  // Lazy-load TS module on first execution
163
221
  const internal = cmd;
164
222
  if (internal._lazy && internal._modulePath) {
package/dist/main.js CHANGED
@@ -199,9 +199,7 @@ for (const [, cmd] of registry) {
199
199
  const arg = positionalArgs[i];
200
200
  const v = actionArgs[i];
201
201
  if (v !== undefined)
202
- kwargs[arg.name] = coerce(v, arg.type ?? 'str');
203
- else if (arg.default != null)
204
- kwargs[arg.name] = arg.default;
202
+ kwargs[arg.name] = v;
205
203
  }
206
204
  // Collect named options
207
205
  for (const arg of cmd.args) {
@@ -209,9 +207,7 @@ for (const [, cmd] of registry) {
209
207
  continue;
210
208
  const v = actionOpts[arg.name];
211
209
  if (v !== undefined)
212
- kwargs[arg.name] = coerce(v, arg.type ?? 'str');
213
- else if (arg.default != null)
214
- kwargs[arg.name] = arg.default;
210
+ kwargs[arg.name] = v;
215
211
  }
216
212
  try {
217
213
  if (actionOpts.verbose)
@@ -244,13 +240,4 @@ for (const [, cmd] of registry) {
244
240
  }
245
241
  });
246
242
  }
247
- function coerce(v, t) {
248
- if (t === 'bool')
249
- return ['1', 'true', 'yes', 'on'].includes(String(v).toLowerCase());
250
- if (t === 'int')
251
- return parseInt(String(v), 10);
252
- if (t === 'float')
253
- return parseFloat(String(v));
254
- return String(v);
255
- }
256
243
  program.parse();
@@ -1,30 +1,8 @@
1
1
  /**
2
2
  * Pipeline executor: runs YAML pipeline steps sequentially.
3
3
  */
4
- import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
5
- import { stepFetch } from './steps/fetch.js';
6
- import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
7
- import { stepIntercept } from './steps/intercept.js';
8
- import { stepTap } from './steps/tap.js';
4
+ import { getStep } from './registry.js';
9
5
  import { log } from '../logger.js';
10
- /** Registry of all available step handlers */
11
- const STEP_HANDLERS = {
12
- navigate: stepNavigate,
13
- fetch: stepFetch,
14
- select: stepSelect,
15
- evaluate: stepEvaluate,
16
- snapshot: stepSnapshot,
17
- click: stepClick,
18
- type: stepType,
19
- wait: stepWait,
20
- press: stepPress,
21
- map: stepMap,
22
- filter: stepFilter,
23
- sort: stepSort,
24
- limit: stepLimit,
25
- intercept: stepIntercept,
26
- tap: stepTap,
27
- };
28
6
  export async function executePipeline(page, pipeline, ctx = {}) {
29
7
  const args = ctx.args ?? {};
30
8
  const debug = ctx.debug ?? false;
@@ -37,7 +15,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
37
15
  for (const [op, params] of Object.entries(step)) {
38
16
  if (debug)
39
17
  debugStepStart(i + 1, total, op, params);
40
- const handler = STEP_HANDLERS[op];
18
+ const handler = getStep(op);
41
19
  if (handler) {
42
20
  data = await handler(page, params, data, args);
43
21
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Dynamic registry for pipeline steps.
3
+ * Allows core and third-party plugins to register custom YAML operations.
4
+ */
5
+ import type { IPage } from '../types.js';
6
+ /**
7
+ * Step handler: all pipeline steps conform to this generic interface.
8
+ * TData is the type of the `data` state flowing into the step.
9
+ * TResult is the expected return type.
10
+ */
11
+ export type StepHandler<TData = any, TResult = any> = (page: IPage | null, params: any, data: TData, args: Record<string, any>) => Promise<TResult>;
12
+ /**
13
+ * Get a registered step handler by name.
14
+ */
15
+ export declare function getStep(name: string): StepHandler | undefined;
16
+ /**
17
+ * Register a new custom step handler for the YAML pipeline.
18
+ */
19
+ export declare function registerStep(name: string, handler: StepHandler): void;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Dynamic registry for pipeline steps.
3
+ * Allows core and third-party plugins to register custom YAML operations.
4
+ */
5
+ // Import core steps
6
+ import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
7
+ import { stepFetch } from './steps/fetch.js';
8
+ import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
9
+ import { stepIntercept } from './steps/intercept.js';
10
+ import { stepTap } from './steps/tap.js';
11
+ const _stepRegistry = new Map();
12
+ /**
13
+ * Get a registered step handler by name.
14
+ */
15
+ export function getStep(name) {
16
+ return _stepRegistry.get(name);
17
+ }
18
+ /**
19
+ * Register a new custom step handler for the YAML pipeline.
20
+ */
21
+ export function registerStep(name, handler) {
22
+ _stepRegistry.set(name, handler);
23
+ }
24
+ // -------------------------------------------------------------
25
+ // Auto-Register Core Steps
26
+ // -------------------------------------------------------------
27
+ registerStep('navigate', stepNavigate);
28
+ registerStep('fetch', stepFetch);
29
+ registerStep('select', stepSelect);
30
+ registerStep('evaluate', stepEvaluate);
31
+ registerStep('snapshot', stepSnapshot);
32
+ registerStep('click', stepClick);
33
+ registerStep('type', stepType);
34
+ registerStep('wait', stepWait);
35
+ registerStep('press', stepPress);
36
+ registerStep('map', stepMap);
37
+ registerStep('filter', stepFilter);
38
+ registerStep('sort', stepSort);
39
+ registerStep('limit', stepLimit);
40
+ registerStep('intercept', stepIntercept);
41
+ registerStep('tap', stepTap);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "0.7.10",
3
+ "version": "0.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },