@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.
- package/.github/workflows/pkg-pr-new.yml +30 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/browser/discover.d.ts +15 -0
- package/dist/browser/discover.js +60 -12
- package/dist/browser/index.d.ts +5 -1
- package/dist/browser/index.js +5 -1
- package/dist/browser/mcp.js +7 -6
- 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 +117 -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/package.json +1 -1
- package/src/browser/discover.ts +73 -12
- package/src/browser/index.ts +5 -1
- package/src/browser/mcp.ts +7 -5
- package/src/browser.test.ts +140 -1
- package/src/clis/barchart/flow.ts +121 -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/vitest.config.ts +7 -0
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
31
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
32
|
+
|
|
33
|
+
const fields = [
|
|
34
|
+
'baseSymbol','strikePrice','expirationDate','optionType',
|
|
35
|
+
'lastPrice','volume','openInterest','volumeOpenInterestRatio','volatility',
|
|
36
|
+
].join(',');
|
|
37
|
+
|
|
38
|
+
// Fetch extra rows when filtering by type since server-side filter may not work
|
|
39
|
+
const fetchLimit = typeFilter !== 'all' ? limit * 3 : limit;
|
|
40
|
+
try {
|
|
41
|
+
const url = '/proxies/core-api/v1/options/get?list=options.unusual_activity.stocks.us'
|
|
42
|
+
+ '&fields=' + fields
|
|
43
|
+
+ '&orderBy=volumeOpenInterestRatio&orderDir=desc'
|
|
44
|
+
+ '&raw=1&limit=' + fetchLimit;
|
|
45
|
+
|
|
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
|
+
if (items.length > 0) {
|
|
51
|
+
// Apply client-side type filter
|
|
52
|
+
if (typeFilter !== 'all') {
|
|
53
|
+
items = items.filter(i => {
|
|
54
|
+
const t = ((i.raw || i).optionType || '').toLowerCase();
|
|
55
|
+
return t === typeFilter;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return items.slice(0, limit).map(i => {
|
|
59
|
+
const r = i.raw || i;
|
|
60
|
+
return {
|
|
61
|
+
symbol: r.baseSymbol || r.symbol,
|
|
62
|
+
type: r.optionType,
|
|
63
|
+
strike: r.strikePrice,
|
|
64
|
+
expiration: r.expirationDate,
|
|
65
|
+
last: r.lastPrice,
|
|
66
|
+
volume: r.volume,
|
|
67
|
+
openInterest: r.openInterest,
|
|
68
|
+
volOiRatio: r.volumeOpenInterestRatio,
|
|
69
|
+
iv: r.volatility,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch(e) {}
|
|
75
|
+
|
|
76
|
+
// Fallback: parse from DOM table
|
|
77
|
+
try {
|
|
78
|
+
const rows = document.querySelectorAll('tr[data-ng-repeat], tbody tr');
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
const cells = row.querySelectorAll('td');
|
|
82
|
+
if (cells.length < 6) continue;
|
|
83
|
+
const getText = (idx) => cells[idx]?.textContent?.trim() || null;
|
|
84
|
+
results.push({
|
|
85
|
+
symbol: getText(0),
|
|
86
|
+
type: getText(1),
|
|
87
|
+
strike: getText(2),
|
|
88
|
+
expiration: getText(3),
|
|
89
|
+
last: getText(4),
|
|
90
|
+
volume: getText(5),
|
|
91
|
+
openInterest: cells.length > 6 ? getText(6) : null,
|
|
92
|
+
volOiRatio: cells.length > 7 ? getText(7) : null,
|
|
93
|
+
iv: cells.length > 8 ? getText(8) : null,
|
|
94
|
+
});
|
|
95
|
+
if (results.length >= limit) break;
|
|
96
|
+
}
|
|
97
|
+
return results;
|
|
98
|
+
} catch(e) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
})()
|
|
102
|
+
`);
|
|
103
|
+
if (!data || !Array.isArray(data))
|
|
104
|
+
return [];
|
|
105
|
+
return data.slice(0, limit).map(r => ({
|
|
106
|
+
symbol: r.symbol || '',
|
|
107
|
+
type: r.type || '',
|
|
108
|
+
strike: r.strike,
|
|
109
|
+
expiration: r.expiration ?? null,
|
|
110
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
111
|
+
volume: r.volume,
|
|
112
|
+
openInterest: r.openInterest,
|
|
113
|
+
volOiRatio: r.volOiRatio != null ? Number(Number(r.volOiRatio).toFixed(2)) : null,
|
|
114
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -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 {};
|
|
@@ -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/package.json
CHANGED
package/src/browser/discover.ts
CHANGED
|
@@ -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 &&
|
|
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 (
|
|
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 (
|
|
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 =
|
|
47
|
-
if (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 =
|
|
56
|
-
if (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 (!
|
|
100
|
+
if (!_existsSync(base)) continue;
|
|
65
101
|
try {
|
|
66
|
-
const found =
|
|
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
|
-
|
|
79
|
-
const args = [
|
|
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
|
|
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
|
+
}
|
package/src/browser/index.ts
CHANGED
|
@@ -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
|
};
|