@jackwener/opencli 1.7.19 → 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.
@@ -40,12 +40,16 @@ export type AdapterKind = 'site' | 'app';
40
40
  export declare function classifyAdapter(domain: string | undefined): AdapterKind;
41
41
  export interface RootAdapterGroups {
42
42
  /** Externally-registered CLIs (docker, gh, vercel, ...) — passthrough binaries */
43
- external: readonly string[];
43
+ external: readonly RootExternalCli[];
44
44
  /** Desktop-app adapters (chatgpt-app, chatwise, codex, ...) */
45
45
  apps: readonly string[];
46
46
  /** Web-site adapters (bilibili, dianping, ...) */
47
47
  sites: readonly string[];
48
48
  }
49
+ export interface RootExternalCli {
50
+ name: string;
51
+ label: string;
52
+ }
49
53
  export declare function formatRootAdapterHelpText(groups: RootAdapterGroups): string;
50
54
  /**
51
55
  * Extracts a positional placeholder that should appear immediately after this
package/dist/src/help.js CHANGED
@@ -116,7 +116,7 @@ export function formatRootAdapterHelpText(groups) {
116
116
  if (total === 0)
117
117
  return '';
118
118
  const lines = [''];
119
- lines.push(...formatGroupSection('External CLIs', groups.external));
119
+ lines.push(...formatGroupSection('External CLIs', groups.external.map(cli => cli.label)));
120
120
  lines.push(...formatGroupSection('App adapters', groups.apps));
121
121
  lines.push(...formatGroupSection('Site adapters', groups.sites));
122
122
  lines.push("Run 'opencli list' for full command details, or 'opencli <site> --help' to inspect one site.");
@@ -376,7 +376,7 @@ function compactCommand(cmd) {
376
376
  };
377
377
  }
378
378
  export function rootHelpData(program, groups) {
379
- const adapterNames = new Set([...groups.external, ...groups.apps, ...groups.sites]);
379
+ const adapterNames = new Set([...groups.external.map(cli => cli.name), ...groups.apps, ...groups.sites]);
380
380
  const commands = program.commands
381
381
  .filter(command => !adapterNames.has(command.name()))
382
382
  .map(command => ({
@@ -390,7 +390,8 @@ export function rootHelpData(program, groups) {
390
390
  commands,
391
391
  external_clis: {
392
392
  count: groups.external.length,
393
- clis: [...groups.external].sort(sortLocale),
393
+ clis: groups.external.map(cli => cli.name).sort(sortLocale),
394
+ display: groups.external.map(cli => cli.label).sort(sortLocale),
394
395
  },
395
396
  app_adapters: {
396
397
  count: groups.apps.length,
@@ -20,13 +20,17 @@ describe('classifyAdapter', () => {
20
20
  describe('formatRootAdapterHelpText', () => {
21
21
  it('renders all three sections in External / App / Site order when populated', () => {
22
22
  const text = formatRootAdapterHelpText({
23
- external: ['gh', 'docker'],
23
+ external: [
24
+ { name: 'gh', label: 'gh' },
25
+ { name: 'wx', label: 'wx(wx-cli)' },
26
+ ],
24
27
  apps: ['chatwise', 'codex'],
25
28
  sites: ['bilibili'],
26
29
  });
27
30
  expect(text).toContain('External CLIs (2):');
28
31
  expect(text).toContain('App adapters (2):');
29
32
  expect(text).toContain('Site adapters (1):');
33
+ expect(text).toContain('wx(wx-cli)');
30
34
  expect(text.indexOf('External CLIs')).toBeLessThan(text.indexOf('App adapters'));
31
35
  expect(text.indexOf('App adapters')).toBeLessThan(text.indexOf('Site adapters'));
32
36
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.7.19",
3
+ "version": "1.7.20",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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
- });