@jackwener/opencli 1.7.18 → 1.7.20

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 (120) hide show
  1. package/README.md +18 -17
  2. package/README.zh-CN.md +16 -18
  3. package/cli-manifest.json +311 -186
  4. package/clis/ctrip/ctrip.test.js +486 -1
  5. package/clis/ctrip/flight.js +136 -0
  6. package/clis/ctrip/hotel-search.js +132 -0
  7. package/clis/ctrip/utils.js +298 -0
  8. package/clis/google/search.js +16 -6
  9. package/clis/google-scholar/search.js +20 -5
  10. package/clis/google-scholar/search.test.js +35 -2
  11. package/clis/reddit/home.js +117 -0
  12. package/clis/reddit/home.test.js +127 -0
  13. package/clis/reddit/read.js +400 -54
  14. package/clis/reddit/read.test.js +315 -12
  15. package/clis/reddit/subreddit-info.js +117 -0
  16. package/clis/reddit/subreddit-info.test.js +163 -0
  17. package/clis/reddit/whoami.js +84 -0
  18. package/clis/reddit/whoami.test.js +105 -0
  19. package/clis/rednote/search.js +6 -2
  20. package/clis/twitter/bookmark-folder.js +8 -4
  21. package/clis/twitter/bookmark-folder.test.js +59 -1
  22. package/clis/twitter/bookmarks.js +12 -4
  23. package/clis/twitter/bookmarks.test.js +205 -0
  24. package/clis/twitter/followers.js +20 -5
  25. package/clis/twitter/followers.test.js +44 -0
  26. package/clis/twitter/following.js +36 -20
  27. package/clis/twitter/following.test.js +60 -8
  28. package/clis/twitter/likes.js +28 -13
  29. package/clis/twitter/likes.test.js +111 -1
  30. package/clis/twitter/list-add.js +128 -204
  31. package/clis/twitter/list-add.test.js +97 -1
  32. package/clis/twitter/list-tweets.js +13 -4
  33. package/clis/twitter/list-tweets.test.js +48 -0
  34. package/clis/twitter/lists.js +5 -2
  35. package/clis/twitter/post.js +23 -4
  36. package/clis/twitter/post.test.js +30 -0
  37. package/clis/twitter/profile.js +16 -8
  38. package/clis/twitter/profile.test.js +39 -0
  39. package/clis/twitter/reply.js +133 -10
  40. package/clis/twitter/reply.test.js +55 -0
  41. package/clis/twitter/search.js +188 -170
  42. package/clis/twitter/search.test.js +96 -258
  43. package/clis/twitter/shared.js +167 -16
  44. package/clis/twitter/shared.test.js +102 -1
  45. package/clis/twitter/timeline.js +3 -1
  46. package/clis/twitter/tweets.js +147 -51
  47. package/clis/twitter/tweets.test.js +238 -1
  48. package/clis/xiaohongshu/comments.js +23 -2
  49. package/clis/xiaohongshu/comments.test.js +63 -1
  50. package/clis/xiaohongshu/search.js +168 -13
  51. package/clis/xiaohongshu/search.test.js +82 -8
  52. package/clis/xueqiu/earnings-date.js +2 -2
  53. package/clis/xueqiu/kline.js +2 -2
  54. package/clis/xueqiu/utils.js +19 -0
  55. package/clis/xueqiu/utils.test.js +26 -0
  56. package/clis/zhihu/answer-detail.js +233 -0
  57. package/clis/zhihu/answer-detail.test.js +330 -0
  58. package/clis/zhihu/question.js +44 -10
  59. package/clis/zhihu/question.test.js +78 -1
  60. package/clis/zhihu/recommend.js +103 -0
  61. package/clis/zhihu/recommend.test.js +143 -0
  62. package/dist/src/browser/base-page.d.ts +3 -2
  63. package/dist/src/browser/base-page.test.js +2 -2
  64. package/dist/src/browser/cdp.js +3 -3
  65. package/dist/src/browser/daemon-client.d.ts +1 -0
  66. package/dist/src/browser/daemon-client.js +3 -0
  67. package/dist/src/browser/daemon-client.test.js +20 -0
  68. package/dist/src/browser/page.d.ts +3 -2
  69. package/dist/src/browser/page.js +4 -4
  70. package/dist/src/browser/page.test.js +31 -0
  71. package/dist/src/browser/utils.d.ts +10 -0
  72. package/dist/src/browser/utils.js +37 -0
  73. package/dist/src/browser/utils.test.d.ts +1 -0
  74. package/dist/src/browser/utils.test.js +29 -0
  75. package/dist/src/cli-argv-preprocess.d.ts +37 -0
  76. package/dist/src/cli-argv-preprocess.js +131 -0
  77. package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
  78. package/dist/src/cli-argv-preprocess.test.js +130 -0
  79. package/dist/src/cli.js +131 -89
  80. package/dist/src/cli.test.js +34 -28
  81. package/dist/src/commands/daemon.js +6 -7
  82. package/dist/src/daemon-utils.d.ts +18 -0
  83. package/dist/src/daemon-utils.js +37 -0
  84. package/dist/src/daemon.d.ts +1 -1
  85. package/dist/src/daemon.js +44 -13
  86. package/dist/src/daemon.test.js +42 -1
  87. package/dist/src/doctor.js +15 -16
  88. package/dist/src/download/progress.js +15 -11
  89. package/dist/src/download/progress.test.d.ts +1 -0
  90. package/dist/src/download/progress.test.js +25 -0
  91. package/dist/src/electron-apps.js +0 -1
  92. package/dist/src/electron-apps.test.js +1 -0
  93. package/dist/src/execution.js +1 -3
  94. package/dist/src/execution.test.js +4 -16
  95. package/dist/src/external-clis.yaml +12 -3
  96. package/dist/src/external.d.ts +4 -0
  97. package/dist/src/external.js +3 -0
  98. package/dist/src/external.test.js +24 -1
  99. package/dist/src/help.d.ts +16 -1
  100. package/dist/src/help.js +50 -8
  101. package/dist/src/help.test.js +5 -1
  102. package/dist/src/logger.js +8 -9
  103. package/dist/src/main.js +16 -0
  104. package/dist/src/output.js +4 -5
  105. package/dist/src/runtime-detect.d.ts +1 -1
  106. package/dist/src/runtime-detect.js +1 -1
  107. package/dist/src/runtime-detect.test.js +3 -2
  108. package/dist/src/tui.d.ts +0 -1
  109. package/dist/src/tui.js +9 -22
  110. package/dist/src/types.d.ts +3 -1
  111. package/dist/src/update-check.js +4 -5
  112. package/package.json +5 -4
  113. package/clis/notion/export.js +0 -32
  114. package/clis/notion/favorites.js +0 -85
  115. package/clis/notion/new.js +0 -35
  116. package/clis/notion/read.js +0 -31
  117. package/clis/notion/search.js +0 -47
  118. package/clis/notion/sidebar.js +0 -42
  119. package/clis/notion/status.js +0 -17
  120. package/clis/notion/write.js +0 -41
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.7.18",
3
+ "version": "1.7.20",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "description": "Make any website or Electron App your CLI. AI-powered.",
8
8
  "engines": {
9
- "node": ">=21.0.0"
9
+ "node": ">=20.0.0"
10
10
  },
11
11
  "type": "module",
12
12
  "main": "dist/src/main.js",
@@ -41,7 +41,8 @@
41
41
  "scripts": {
42
42
  "dev": "tsx src/main.ts",
43
43
  "dev:bun": "bun src/main.ts",
44
- "build": "npm run clean-dist && tsc && npm run copy-yaml && npm run build-manifest",
44
+ "build": "npm run clean-dist && npm run copy-yaml && npm run build-manifest",
45
+ "prebuild-manifest": "tsc --build",
45
46
  "build-manifest": "tsx src/build-manifest.ts",
46
47
  "clean-dist": "node scripts/clean-dist.cjs",
47
48
  "copy-yaml": "node scripts/copy-yaml.cjs",
@@ -83,7 +84,7 @@
83
84
  "js-yaml": "^4.1.0",
84
85
  "turndown": "^7.2.2",
85
86
  "turndown-plugin-gfm": "^1.0.2",
86
- "undici": "^8.0.2",
87
+ "undici": "^6.25.0",
87
88
  "ws": "^8.18.0"
88
89
  },
89
90
  "devDependencies": {
@@ -1,32 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import { cli, Strategy } from '@jackwener/opencli/registry';
3
- export const exportCommand = cli({
4
- site: 'notion',
5
- name: 'export',
6
- access: 'read',
7
- description: 'Export the current Notion page as Markdown',
8
- domain: 'localhost',
9
- strategy: Strategy.UI,
10
- browser: true,
11
- args: [
12
- { name: 'output', required: false, help: 'Output file (default: /tmp/notion-export.md)' },
13
- ],
14
- columns: ['Status', 'File'],
15
- func: async (page, kwargs) => {
16
- const outputPath = kwargs.output || '/tmp/notion-export.md';
17
- const result = await page.evaluate(`
18
- (function() {
19
- const titleEl = document.querySelector('[data-block-id] [placeholder="Untitled"], h1.notion-title, [class*="title"]');
20
- const title = titleEl ? (titleEl.textContent || '').trim() : document.title;
21
-
22
- const frame = document.querySelector('.notion-page-content, [class*="page-content"], main');
23
- const content = frame ? (frame.innerText || '').trim() : document.body.innerText;
24
-
25
- return { title, content };
26
- })()
27
- `);
28
- const md = `# ${result.title}\n\n${result.content}`;
29
- fs.writeFileSync(outputPath, md);
30
- return [{ Status: 'Success', File: outputPath }];
31
- },
32
- });
@@ -1,85 +0,0 @@
1
- import { cli, Strategy } from '@jackwener/opencli/registry';
2
- export const favoritesCommand = cli({
3
- site: 'notion',
4
- name: 'favorites',
5
- access: 'read',
6
- description: 'List pages from the Notion Favorites section in the sidebar',
7
- domain: 'localhost',
8
- strategy: Strategy.UI,
9
- browser: true,
10
- args: [],
11
- columns: ['Index', 'Title', 'Icon'],
12
- func: async (page) => {
13
- const items = await page.evaluate(`
14
- (function() {
15
- const results = [];
16
-
17
- // Strategy 1: Use Notion's own class 'notion-outliner-bookmarks-header-container'
18
- const headerContainer = document.querySelector('.notion-outliner-bookmarks-header-container');
19
- if (headerContainer) {
20
- // Walk up to the section parent that wraps header + items
21
- let section = headerContainer.parentElement;
22
- if (section && section.children.length === 1) section = section.parentElement;
23
-
24
- if (section) {
25
- const treeItems = section.querySelectorAll('[role="treeitem"]');
26
- treeItems.forEach((item) => {
27
- // Title text is in a div.notranslate sibling of the icon area
28
- const titleEl = item.querySelector('div.notranslate:not(.notion-record-icon)');
29
- const title = titleEl
30
- ? titleEl.textContent.trim()
31
- : (item.textContent || '').trim().substring(0, 80);
32
-
33
- // Icon/emoji is in the notion-record-icon element
34
- const iconEl = item.querySelector('.notion-record-icon');
35
- const icon = iconEl ? iconEl.textContent.trim().substring(0, 4) : '';
36
-
37
- if (title && title.length > 0) {
38
- results.push({ Index: results.length + 1, Title: title, Icon: icon || '📄' });
39
- }
40
- });
41
- }
42
- }
43
-
44
- // Strategy 2: Fallback — find "Favorites" text node and walk DOM
45
- if (results.length === 0) {
46
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
47
- let node;
48
- let favEl = null;
49
- while (node = walker.nextNode()) {
50
- const text = node.textContent.trim();
51
- if (text === 'Favorites' || text === '收藏' || text === '收藏夹') {
52
- favEl = node.parentElement;
53
- break;
54
- }
55
- }
56
-
57
- if (favEl) {
58
- let section = favEl;
59
- for (let i = 0; i < 6; i++) {
60
- const p = section.parentElement;
61
- if (!p || p === document.body) break;
62
- const treeItems = p.querySelectorAll(':scope > [role="treeitem"]');
63
- if (treeItems.length > 0) { section = p; break; }
64
- section = p;
65
- }
66
-
67
- const treeItems = section.querySelectorAll('[role="treeitem"]');
68
- treeItems.forEach((item) => {
69
- const text = (item.textContent || '').trim().substring(0, 120);
70
- if (text && text.length > 1 && !text.match(/^(Favorites|收藏夹?)$/)) {
71
- results.push({ Index: results.length + 1, Title: text, Icon: '📄' });
72
- }
73
- });
74
- }
75
- }
76
-
77
- return results;
78
- })()
79
- `);
80
- if (items.length === 0) {
81
- return [{ Index: 0, Title: 'No favorites found. Make sure sidebar is visible and you have favorites.', Icon: '⚠️' }];
82
- }
83
- return items;
84
- },
85
- });
@@ -1,35 +0,0 @@
1
- import { cli, Strategy } from '@jackwener/opencli/registry';
2
- export const newCommand = cli({
3
- site: 'notion',
4
- name: 'new',
5
- access: 'write',
6
- description: 'Create a new page in Notion',
7
- domain: 'localhost',
8
- strategy: Strategy.UI,
9
- browser: true,
10
- args: [
11
- { name: 'title', required: false, positional: true, help: 'Page title (optional)' },
12
- ],
13
- columns: ['Status'],
14
- func: async (page, kwargs) => {
15
- const title = kwargs.title;
16
- // Cmd+N creates a new page in Notion
17
- const isMac = process.platform === 'darwin';
18
- await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
19
- await page.wait(1);
20
- // If title is provided, type it into the title field
21
- if (title) {
22
- await page.evaluate(`
23
- (function(t) {
24
- const titleEl = document.querySelector('[placeholder="Untitled"], [data-content-editable-leaf] [placeholder]');
25
- if (titleEl) {
26
- titleEl.focus();
27
- document.execCommand('insertText', false, t);
28
- }
29
- })(${JSON.stringify(title)})
30
- `);
31
- await page.wait(0.5);
32
- }
33
- return [{ Status: title ? `Created page: ${title}` : 'New blank page created' }];
34
- },
35
- });
@@ -1,31 +0,0 @@
1
- import { cli, Strategy } from '@jackwener/opencli/registry';
2
- export const readCommand = cli({
3
- site: 'notion',
4
- name: 'read',
5
- access: 'read',
6
- description: 'Read the content of the currently open Notion page',
7
- domain: 'localhost',
8
- strategy: Strategy.UI,
9
- browser: true,
10
- args: [],
11
- columns: ['Title', 'Content'],
12
- func: async (page) => {
13
- const result = await page.evaluate(`
14
- (function() {
15
- // Get the page title
16
- const titleEl = document.querySelector('[data-block-id] [placeholder="Untitled"], .notion-page-block .notranslate, h1.notion-title, [class*="title"]');
17
- const title = titleEl ? (titleEl.textContent || '').trim() : document.title;
18
-
19
- // Get the page content — Notion renders blocks in a frame
20
- const frame = document.querySelector('.notion-page-content, [class*="page-content"], .layout-content, main');
21
- const content = frame ? (frame.innerText || frame.textContent || '').trim() : '';
22
-
23
- return { title, content };
24
- })()
25
- `);
26
- return [{
27
- Title: result.title || 'Untitled',
28
- Content: result.content || '(empty page)',
29
- }];
30
- },
31
- });
@@ -1,47 +0,0 @@
1
- import { cli, Strategy } from '@jackwener/opencli/registry';
2
- export const searchCommand = cli({
3
- site: 'notion',
4
- name: 'search',
5
- access: 'read',
6
- description: 'Search pages and databases in Notion via Quick Find (Cmd+P)',
7
- domain: 'localhost',
8
- strategy: Strategy.UI,
9
- browser: true,
10
- args: [{ name: 'query', required: true, positional: true, help: 'Search query' }],
11
- columns: ['Index', 'Title'],
12
- func: async (page, kwargs) => {
13
- const query = kwargs.query;
14
- // Open Quick Find
15
- const isMac = process.platform === 'darwin';
16
- await page.pressKey(isMac ? 'Meta+P' : 'Control+P');
17
- await page.wait(0.5);
18
- // Type the search query
19
- await page.evaluate(`
20
- (function(q) {
21
- const input = document.querySelector('input[placeholder*="Search"], input[type="text"]');
22
- if (input) {
23
- const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
24
- setter.call(input, q);
25
- input.dispatchEvent(new Event('input', { bubbles: true }));
26
- }
27
- })(${JSON.stringify(query)})
28
- `);
29
- await page.wait(1.5);
30
- // Scrape results
31
- const results = await page.evaluate(`
32
- (function() {
33
- const items = document.querySelectorAll('[role="option"], [class*="searchResult"], [class*="quick-find"] [role="button"]');
34
- return Array.from(items).slice(0, 20).map((item, i) => ({
35
- Index: i + 1,
36
- Title: (item.textContent || '').trim().substring(0, 120),
37
- }));
38
- })()
39
- `);
40
- // Close Quick Find
41
- await page.pressKey('Escape');
42
- if (results.length === 0) {
43
- return [{ Index: 0, Title: `No results for "${query}"` }];
44
- }
45
- return results;
46
- },
47
- });
@@ -1,42 +0,0 @@
1
- import { cli, Strategy } from '@jackwener/opencli/registry';
2
- export const sidebarCommand = cli({
3
- site: 'notion',
4
- name: 'sidebar',
5
- access: 'read',
6
- description: 'List pages and databases from the Notion sidebar',
7
- domain: 'localhost',
8
- strategy: Strategy.UI,
9
- browser: true,
10
- args: [],
11
- columns: ['Index', 'Title'],
12
- func: async (page) => {
13
- const items = await page.evaluate(`
14
- (function() {
15
- const results = [];
16
- // Notion sidebar items
17
- const selectors = [
18
- '[class*="sidebar"] [role="treeitem"]',
19
- '[class*="sidebar"] a',
20
- '.notion-sidebar [role="button"]',
21
- 'nav [role="treeitem"]',
22
- ];
23
-
24
- for (const sel of selectors) {
25
- const nodes = document.querySelectorAll(sel);
26
- if (nodes.length > 0) {
27
- nodes.forEach((n, i) => {
28
- const text = (n.textContent || '').trim().substring(0, 100);
29
- if (text && text.length > 1) results.push({ Index: i + 1, Title: text });
30
- });
31
- break;
32
- }
33
- }
34
- return results;
35
- })()
36
- `);
37
- if (items.length === 0) {
38
- return [{ Index: 0, Title: 'No sidebar items found. Toggle the sidebar first.' }];
39
- }
40
- return items;
41
- },
42
- });
@@ -1,17 +0,0 @@
1
- import { cli, Strategy } from '@jackwener/opencli/registry';
2
- export const statusCommand = cli({
3
- site: 'notion',
4
- name: 'status',
5
- access: 'read',
6
- description: 'Check active CDP connection to Notion Desktop',
7
- domain: 'localhost',
8
- strategy: Strategy.UI,
9
- browser: true,
10
- args: [],
11
- columns: ['Status', 'Url', 'Title'],
12
- func: async (page) => {
13
- const url = await page.evaluate('window.location.href');
14
- const title = await page.evaluate('document.title');
15
- return [{ Status: 'Connected', Url: url, Title: title }];
16
- },
17
- });
@@ -1,41 +0,0 @@
1
- import { cli, Strategy } from '@jackwener/opencli/registry';
2
- export const writeCommand = cli({
3
- site: 'notion',
4
- name: 'write',
5
- access: 'write',
6
- description: 'Append text content to the currently open Notion page',
7
- domain: 'localhost',
8
- strategy: Strategy.UI,
9
- browser: true,
10
- args: [{ name: 'text', required: true, positional: true, help: 'Text to append to the page' }],
11
- columns: ['Status'],
12
- func: async (page, kwargs) => {
13
- const text = kwargs.text;
14
- // Focus the page body and move to the end
15
- await page.evaluate(`
16
- (function(text) {
17
- // Find the editable area in Notion
18
- const editables = document.querySelectorAll('.notion-page-content [contenteditable="true"], [class*="page-content"] [contenteditable="true"]');
19
- let target = editables.length > 0 ? editables[editables.length - 1] : null;
20
-
21
- if (!target) {
22
- // Fallback: just find any contenteditable
23
- const all = document.querySelectorAll('[contenteditable="true"]');
24
- target = all.length > 0 ? all[all.length - 1] : null;
25
- }
26
-
27
- if (!target) throw new Error('Could not find editable area in Notion page');
28
-
29
- target.focus();
30
- // Move to end
31
- const sel = window.getSelection();
32
- sel.selectAllChildren(target);
33
- sel.collapseToEnd();
34
-
35
- document.execCommand('insertText', false, text);
36
- })(${JSON.stringify(text)})
37
- `);
38
- await page.wait(0.5);
39
- return [{ Status: 'Text appended successfully' }];
40
- },
41
- });