@jackwener/opencli 0.4.2 → 0.4.4

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 (65) hide show
  1. package/{CLI-CREATOR.md → CLI-EXPLORER.md} +15 -11
  2. package/CLI-ONESHOT.md +216 -0
  3. package/LICENSE +28 -0
  4. package/README.md +114 -63
  5. package/README.zh-CN.md +115 -63
  6. package/SKILL.md +25 -6
  7. package/dist/browser.d.ts +53 -10
  8. package/dist/browser.js +491 -111
  9. package/dist/browser.test.d.ts +1 -0
  10. package/dist/browser.test.js +56 -0
  11. package/dist/build-manifest.js +4 -0
  12. package/dist/cli-manifest.json +279 -3
  13. package/dist/clis/boss/search.js +186 -30
  14. package/dist/clis/twitter/delete.d.ts +1 -0
  15. package/dist/clis/twitter/delete.js +73 -0
  16. package/dist/clis/twitter/followers.d.ts +1 -0
  17. package/dist/clis/twitter/followers.js +104 -0
  18. package/dist/clis/twitter/following.d.ts +1 -0
  19. package/dist/clis/twitter/following.js +90 -0
  20. package/dist/clis/twitter/like.d.ts +1 -0
  21. package/dist/clis/twitter/like.js +69 -0
  22. package/dist/clis/twitter/notifications.d.ts +1 -0
  23. package/dist/clis/twitter/notifications.js +109 -0
  24. package/dist/clis/twitter/post.d.ts +1 -0
  25. package/dist/clis/twitter/post.js +63 -0
  26. package/dist/clis/twitter/reply.d.ts +1 -0
  27. package/dist/clis/twitter/reply.js +57 -0
  28. package/dist/clis/v2ex/daily.d.ts +1 -0
  29. package/dist/clis/v2ex/daily.js +98 -0
  30. package/dist/clis/v2ex/me.d.ts +1 -0
  31. package/dist/clis/v2ex/me.js +99 -0
  32. package/dist/clis/v2ex/notifications.d.ts +1 -0
  33. package/dist/clis/v2ex/notifications.js +72 -0
  34. package/dist/doctor.d.ts +50 -0
  35. package/dist/doctor.js +372 -0
  36. package/dist/doctor.test.d.ts +1 -0
  37. package/dist/doctor.test.js +114 -0
  38. package/dist/main.js +47 -5
  39. package/dist/output.test.d.ts +1 -0
  40. package/dist/output.test.js +20 -0
  41. package/dist/registry.d.ts +4 -0
  42. package/dist/registry.js +1 -0
  43. package/dist/runtime.d.ts +3 -1
  44. package/dist/runtime.js +2 -2
  45. package/package.json +2 -2
  46. package/src/browser.test.ts +77 -0
  47. package/src/browser.ts +541 -99
  48. package/src/build-manifest.ts +4 -0
  49. package/src/clis/boss/search.ts +196 -29
  50. package/src/clis/twitter/delete.ts +78 -0
  51. package/src/clis/twitter/followers.ts +119 -0
  52. package/src/clis/twitter/following.ts +105 -0
  53. package/src/clis/twitter/like.ts +74 -0
  54. package/src/clis/twitter/notifications.ts +119 -0
  55. package/src/clis/twitter/post.ts +68 -0
  56. package/src/clis/twitter/reply.ts +62 -0
  57. package/src/clis/v2ex/daily.ts +105 -0
  58. package/src/clis/v2ex/me.ts +103 -0
  59. package/src/clis/v2ex/notifications.ts +77 -0
  60. package/src/doctor.test.ts +133 -0
  61. package/src/doctor.ts +424 -0
  62. package/src/main.ts +47 -4
  63. package/src/output.test.ts +27 -0
  64. package/src/registry.ts +5 -0
  65. package/src/runtime.ts +2 -1
@@ -0,0 +1,98 @@
1
+ /**
2
+ * V2EX Daily Check-in adapter.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ cli({
6
+ site: 'v2ex',
7
+ name: 'daily',
8
+ description: 'V2EX 每日签到并领取铜币',
9
+ domain: 'www.v2ex.com',
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ forceExtension: true,
13
+ args: [],
14
+ columns: ['status', 'message'],
15
+ func: async (page) => {
16
+ if (!page)
17
+ throw new Error('Browser page required');
18
+ if (process.env.OPENCLI_VERBOSE) {
19
+ console.error('[opencli:v2ex] Navigating to /mission/daily');
20
+ }
21
+ await page.goto('https://www.v2ex.com/mission/daily');
22
+ // Cloudflare challenge bypass wait
23
+ for (let i = 0; i < 5; i++) {
24
+ await new Promise(r => setTimeout(r, 1500));
25
+ const title = await page.evaluate(`() => document.title`);
26
+ if (!title?.includes('Just a moment'))
27
+ break;
28
+ if (process.env.OPENCLI_VERBOSE)
29
+ console.error('[opencli:v2ex] Waiting for Cloudflare...');
30
+ }
31
+ // Evaluate DOM to find if we need to check in
32
+ const checkResult = await page.evaluate(`
33
+ async () => {
34
+ const btn = document.querySelector('input.super.normal.button');
35
+ if (!btn || !btn.value.includes('领取')) {
36
+ return { claimed: true, message: '今日奖励已发/无需领取' };
37
+ }
38
+
39
+ const onclick = btn.getAttribute('onclick');
40
+ if (onclick) {
41
+ const match = onclick.match(/once=(\\d+)/);
42
+ if (match) {
43
+ return { claimed: false, once: match[1], message: btn.value };
44
+ }
45
+ }
46
+
47
+ return {
48
+ claimed: false,
49
+ error: '找到了按钮,但未能提取 once token',
50
+ debug_title: document.title,
51
+ debug_body: document.body.innerText.substring(0, 200).replace(/\\n/g, ' ')
52
+ };
53
+ }
54
+ `);
55
+ if (checkResult.error) {
56
+ if (process.env.OPENCLI_VERBOSE) {
57
+ console.error(`[opencli:v2ex:debug] Page Title: ${checkResult.debug_title}`);
58
+ console.error(`[opencli:v2ex:debug] Page Body: ${checkResult.debug_body}`);
59
+ }
60
+ throw new Error(checkResult.error);
61
+ }
62
+ if (checkResult.claimed) {
63
+ return [{ status: '✅ 已签到', message: checkResult.message }];
64
+ }
65
+ // Perform check in
66
+ if (process.env.OPENCLI_VERBOSE) {
67
+ console.error(`[opencli:v2ex] Found check-in token: once=${checkResult.once}. Checking in...`);
68
+ }
69
+ await page.goto(`https://www.v2ex.com/mission/daily/redeem?once=${checkResult.once}`);
70
+ await new Promise(resolve => setTimeout(resolve, 3000)); // wait longer for redirect
71
+ // Verify result
72
+ const verifyResult = await page.evaluate(`
73
+ async () => {
74
+ const btn = document.querySelector('input.super.normal.button');
75
+ if (!btn || !btn.value.includes('领取')) {
76
+ // fetch balance to show user
77
+ let balance = '';
78
+ const balanceLink = document.querySelector('a.balance_area');
79
+ if (balanceLink) {
80
+ balance = Array.from(balanceLink.childNodes)
81
+ .filter(n => n.nodeType === 3)
82
+ .map(n => n.textContent?.trim())
83
+ .join(' ')
84
+ .trim();
85
+ }
86
+ return { success: true, balance };
87
+ }
88
+ return { success: false };
89
+ }
90
+ `);
91
+ if (verifyResult.success) {
92
+ return [{ status: '🎉 签到成功', message: `当前余额: ${verifyResult.balance || '未知'}` }];
93
+ }
94
+ else {
95
+ return [{ status: '❌ 签到失败', message: '未能确认签到结果,请手动检查' }];
96
+ }
97
+ },
98
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,99 @@
1
+ /**
2
+ * V2EX Me (Profile/Balance) adapter.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ cli({
6
+ site: 'v2ex',
7
+ name: 'me',
8
+ description: 'V2EX 获取个人资料 (余额/未读提醒)',
9
+ domain: 'www.v2ex.com',
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ forceExtension: true,
13
+ args: [],
14
+ columns: ['username', 'balance', 'unread_notifications', 'daily_reward_ready'],
15
+ func: async (page) => {
16
+ if (!page)
17
+ throw new Error('Browser page required');
18
+ if (process.env.OPENCLI_VERBOSE) {
19
+ console.error('[opencli:v2ex] Navigating to /');
20
+ }
21
+ await page.goto('https://www.v2ex.com/');
22
+ // Cloudflare challenge bypass wait
23
+ for (let i = 0; i < 5; i++) {
24
+ await new Promise(r => setTimeout(r, 1500));
25
+ const title = await page.evaluate(`() => document.title`);
26
+ if (!title?.includes('Just a moment'))
27
+ break;
28
+ if (process.env.OPENCLI_VERBOSE)
29
+ console.error('[opencli:v2ex] Waiting for Cloudflare...');
30
+ }
31
+ // Evaluate DOM to extract user profile
32
+ const data = await page.evaluate(`
33
+ async () => {
34
+ let username = 'Unknown';
35
+ const navLinks = Array.from(document.querySelectorAll('a.top')).map(a => a.textContent?.trim());
36
+ if (navLinks.length > 1 && navLinks[0] === '首页') {
37
+ username = navLinks[1] || 'Unknown';
38
+ }
39
+
40
+ if (username === 'Unknown') {
41
+ // Fallback check just in case
42
+ const profileEl = document.querySelector('a[href^="/member/"]');
43
+ if (profileEl && profileEl.textContent && profileEl.textContent.trim().length > 0) {
44
+ username = profileEl.textContent.trim();
45
+ }
46
+ }
47
+
48
+ let balance = '0';
49
+ const balanceLink = document.querySelector('a.balance_area');
50
+ if (balanceLink) {
51
+ balance = Array.from(balanceLink.childNodes)
52
+ .filter(n => n.nodeType === 3)
53
+ .map(n => n.textContent?.trim())
54
+ .join(' ')
55
+ .trim();
56
+ }
57
+
58
+ let unread_notifications = '0';
59
+ const notesEl = document.querySelector('a[href="/notifications"]');
60
+ if (notesEl) {
61
+ const text = notesEl.textContent?.trim() || '';
62
+ const match = text.match(/(\\d+)\\s*未读提醒/);
63
+ if (match) {
64
+ unread_notifications = match[1];
65
+ }
66
+ }
67
+
68
+ let daily_reward_ready = false;
69
+ const dailyEl = document.querySelector('a[href^="/mission/daily"]');
70
+ if (dailyEl && dailyEl.textContent?.includes('领取今日的登录奖励')) {
71
+ daily_reward_ready = true;
72
+ }
73
+
74
+ if (username === 'Unknown') {
75
+ return {
76
+ error: '请先登录 V2EX(可能是 Cookie 未配置或已失效)',
77
+ debug_title: document.title,
78
+ debug_body: document.body.innerText.substring(0, 200).replace(/\\n/g, ' ')
79
+ };
80
+ }
81
+
82
+ return {
83
+ username,
84
+ balance,
85
+ unread_notifications,
86
+ daily_reward_ready: daily_reward_ready ? '是' : '否'
87
+ };
88
+ }
89
+ `);
90
+ if (data.error) {
91
+ if (process.env.OPENCLI_VERBOSE) {
92
+ console.error(`[opencli:v2ex:debug] Page Title: ${data.debug_title}`);
93
+ console.error(`[opencli:v2ex:debug] Page Body: ${data.debug_body}`);
94
+ }
95
+ throw new Error(data.error);
96
+ }
97
+ return [data];
98
+ },
99
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * V2EX Notifications adapter.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ cli({
6
+ site: 'v2ex',
7
+ name: 'notifications',
8
+ description: 'V2EX 获取提醒 (回复/由于)',
9
+ domain: 'www.v2ex.com',
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ forceExtension: true,
13
+ args: [
14
+ { name: 'limit', type: 'int', default: 20, help: 'Number of notifications' }
15
+ ],
16
+ columns: ['type', 'content', 'time'],
17
+ func: async (page, kwargs) => {
18
+ if (!page)
19
+ throw new Error('Browser page required');
20
+ if (process.env.OPENCLI_VERBOSE) {
21
+ console.error('[opencli:v2ex] Navigating to /notifications');
22
+ }
23
+ await page.goto('https://www.v2ex.com/notifications');
24
+ await new Promise(r => setTimeout(r, 1500)); // waitForLoadState doesn't always work robustly
25
+ // Evaluate DOM to extract notifications
26
+ const data = await page.evaluate(`
27
+ async () => {
28
+ const items = Array.from(document.querySelectorAll('#Main .box .cell[id^="n_"]'));
29
+ return items.map(item => {
30
+ let type = '通知';
31
+ let time = '';
32
+
33
+ // determine type based on text content
34
+ const text = item.textContent || '';
35
+ if (text.includes('回复了你')) type = '回复';
36
+ else if (text.includes('感谢了你')) type = '感谢';
37
+ else if (text.includes('收藏了你')) type = '收藏';
38
+ else if (text.includes('提及你')) type = '提及';
39
+
40
+ const timeEl = item.querySelector('.snow');
41
+ if (timeEl) {
42
+ time = timeEl.textContent?.trim() || '';
43
+ }
44
+
45
+ // payload contains the actual reply text if any
46
+ let payload = '';
47
+ const payloadEl = item.querySelector('.payload');
48
+ if (payloadEl) {
49
+ payload = payloadEl.textContent?.trim() || '';
50
+ }
51
+
52
+ // fallback to full text cleaning if no payload (e.g. for favorites/thanks)
53
+ let content = payload;
54
+ if (!content) {
55
+ content = text.replace(/\\s+/g, ' ').trim();
56
+ // strip out time from content if present
57
+ if (time && content.includes(time)) {
58
+ content = content.replace(time, '').trim();
59
+ }
60
+ }
61
+
62
+ return { type, content, time };
63
+ });
64
+ }
65
+ `);
66
+ if (!Array.isArray(data)) {
67
+ throw new Error('Failed to parse notifications data');
68
+ }
69
+ const limit = kwargs.limit || 20;
70
+ return data.slice(0, limit);
71
+ },
72
+ });
@@ -0,0 +1,50 @@
1
+ export type DoctorOptions = {
2
+ fix?: boolean;
3
+ yes?: boolean;
4
+ shellRc?: string;
5
+ configPaths?: string[];
6
+ token?: string;
7
+ cliVersion?: string;
8
+ };
9
+ export type ShellFileStatus = {
10
+ path: string;
11
+ exists: boolean;
12
+ token: string | null;
13
+ fingerprint: string | null;
14
+ };
15
+ export type McpConfigFormat = 'json' | 'toml';
16
+ export type McpConfigStatus = {
17
+ path: string;
18
+ exists: boolean;
19
+ format: McpConfigFormat;
20
+ token: string | null;
21
+ fingerprint: string | null;
22
+ writable: boolean;
23
+ parseError?: string;
24
+ };
25
+ export type DoctorReport = {
26
+ cliVersion?: string;
27
+ envToken: string | null;
28
+ envFingerprint: string | null;
29
+ shellFiles: ShellFileStatus[];
30
+ configs: McpConfigStatus[];
31
+ remoteDebuggingEnabled: boolean;
32
+ remoteDebuggingEndpoint: string | null;
33
+ cdpEnabled: boolean;
34
+ cdpToken: string | null;
35
+ cdpFingerprint: string | null;
36
+ recommendedToken: string | null;
37
+ recommendedFingerprint: string | null;
38
+ warnings: string[];
39
+ issues: string[];
40
+ };
41
+ export declare function getDefaultShellRcPath(): string;
42
+ export declare function getDefaultMcpConfigPaths(cwd?: string): string[];
43
+ export declare function readTokenFromShellContent(content: string): string | null;
44
+ export declare function upsertShellToken(content: string, token: string): string;
45
+ export declare function upsertJsonConfigToken(content: string, token: string): string;
46
+ export declare function readTomlConfigToken(content: string): string | null;
47
+ export declare function upsertTomlConfigToken(content: string, token: string): string;
48
+ export declare function runBrowserDoctor(opts?: DoctorOptions): Promise<DoctorReport>;
49
+ export declare function renderBrowserDoctorReport(report: DoctorReport): string;
50
+ export declare function applyBrowserDoctorFix(report: DoctorReport, opts?: DoctorOptions): Promise<string[]>;