@jackwener/opencli 1.7.19 → 1.7.21

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 (88) hide show
  1. package/README.md +11 -9
  2. package/README.zh-CN.md +9 -10
  3. package/cli-manifest.json +239 -249
  4. package/clis/_shared/search-adapter.js +70 -0
  5. package/clis/boss/chatlist.js +96 -14
  6. package/clis/boss/chatlist.test.js +211 -0
  7. package/clis/boss/chatmsg.js +98 -24
  8. package/clis/boss/chatmsg.test.js +230 -0
  9. package/clis/boss/utils.js +240 -11
  10. package/clis/brave/search.js +80 -0
  11. package/clis/brave/search.test.js +76 -0
  12. package/clis/duckduckgo/search.js +131 -0
  13. package/clis/duckduckgo/search.test.js +128 -0
  14. package/clis/duckduckgo/suggest.js +45 -0
  15. package/clis/duckduckgo/suggest.test.js +66 -0
  16. package/clis/facebook/feed.js +301 -56
  17. package/clis/facebook/feed.test.js +169 -0
  18. package/clis/reddit/comment.js +0 -1
  19. package/clis/reddit/frontpage.js +0 -1
  20. package/clis/reddit/home.js +0 -1
  21. package/clis/reddit/popular.js +0 -1
  22. package/clis/reddit/read.js +0 -1
  23. package/clis/reddit/read.test.js +2 -2
  24. package/clis/reddit/save.js +0 -1
  25. package/clis/reddit/saved.js +0 -1
  26. package/clis/reddit/search.js +0 -1
  27. package/clis/reddit/subreddit-info.js +0 -1
  28. package/clis/reddit/subreddit.js +0 -1
  29. package/clis/reddit/subscribe.js +0 -1
  30. package/clis/reddit/upvote.js +0 -1
  31. package/clis/reddit/upvoted.js +0 -1
  32. package/clis/reddit/user-comments.js +0 -1
  33. package/clis/reddit/user-posts.js +0 -1
  34. package/clis/reddit/user.js +0 -1
  35. package/clis/reddit/whoami.js +0 -1
  36. package/clis/rednote/rednote.test.js +65 -0
  37. package/clis/rednote/search.js +11 -5
  38. package/clis/twitter/article.js +0 -1
  39. package/clis/twitter/bookmark-folder.js +5 -4
  40. package/clis/twitter/bookmark-folder.test.js +59 -1
  41. package/clis/twitter/bookmark-folders.js +0 -1
  42. package/clis/twitter/bookmarks.js +9 -4
  43. package/clis/twitter/bookmarks.test.js +205 -0
  44. package/clis/twitter/download.js +0 -1
  45. package/clis/twitter/followers.js +0 -1
  46. package/clis/twitter/following.js +0 -1
  47. package/clis/twitter/likes.js +0 -1
  48. package/clis/twitter/list-tweets.js +0 -1
  49. package/clis/twitter/lists.js +0 -1
  50. package/clis/twitter/notifications.js +0 -1
  51. package/clis/twitter/profile.js +0 -1
  52. package/clis/twitter/search.js +0 -1
  53. package/clis/twitter/thread.js +0 -1
  54. package/clis/twitter/timeline.js +0 -1
  55. package/clis/twitter/trending.js +0 -1
  56. package/clis/twitter/tweets.js +0 -1
  57. package/clis/xiaohongshu/search.js +34 -16
  58. package/clis/xiaohongshu/search.test.js +66 -11
  59. package/clis/yahoo/search.js +92 -0
  60. package/clis/yahoo/search.test.js +94 -0
  61. package/dist/src/browser/daemon-client.d.ts +1 -0
  62. package/dist/src/browser/daemon-client.js +3 -0
  63. package/dist/src/browser/daemon-client.test.js +20 -0
  64. package/dist/src/cli.js +8 -3
  65. package/dist/src/cli.test.js +1 -0
  66. package/dist/src/daemon-utils.d.ts +18 -0
  67. package/dist/src/daemon-utils.js +37 -0
  68. package/dist/src/daemon.d.ts +1 -1
  69. package/dist/src/daemon.js +44 -13
  70. package/dist/src/daemon.test.js +42 -1
  71. package/dist/src/electron-apps.js +0 -1
  72. package/dist/src/electron-apps.test.js +1 -0
  73. package/dist/src/external-clis.yaml +12 -3
  74. package/dist/src/external.d.ts +4 -0
  75. package/dist/src/external.js +3 -0
  76. package/dist/src/external.test.js +24 -1
  77. package/dist/src/help.d.ts +5 -1
  78. package/dist/src/help.js +4 -3
  79. package/dist/src/help.test.js +5 -1
  80. package/package.json +1 -1
  81. package/clis/notion/export.js +0 -32
  82. package/clis/notion/favorites.js +0 -85
  83. package/clis/notion/new.js +0 -35
  84. package/clis/notion/read.js +0 -31
  85. package/clis/notion/search.js +0 -47
  86. package/clis/notion/sidebar.js +0 -42
  87. package/clis/notion/status.js +0 -17
  88. package/clis/notion/write.js +0 -41
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { getResponseCorsHeaders } from './daemon.js';
2
+ import { COMMAND_RESULT_UNKNOWN_CODE, COMMAND_RESULT_UNKNOWN_HINT, buildCommandDispatchFailure, buildExtensionDisconnectFailure, commandResultUnknownMessage, getResponseCorsHeaders, } from './daemon-utils.js';
3
3
  describe('getResponseCorsHeaders', () => {
4
4
  it('allows the Browser Bridge extension origin to read /ping', () => {
5
5
  expect(getResponseCorsHeaders('/ping', 'chrome-extension://abc123')).toEqual({
@@ -17,3 +17,44 @@ describe('getResponseCorsHeaders', () => {
17
17
  expect(getResponseCorsHeaders('/command', 'chrome-extension://abc123')).toBeUndefined();
18
18
  });
19
19
  });
20
+ describe('daemon command dispatch', () => {
21
+ it('uses a distinct command_result_unknown contract for ambiguous dispatched commands', () => {
22
+ expect(COMMAND_RESULT_UNKNOWN_CODE).toBe('command_result_unknown');
23
+ expect(commandResultUnknownMessage('navigate')).toContain('navigate command was dispatched');
24
+ expect(COMMAND_RESULT_UNKNOWN_HINT).toContain('Inspect the browser/session state');
25
+ expect(COMMAND_RESULT_UNKNOWN_HINT).toContain('Do not blindly retry write commands');
26
+ });
27
+ it('classifies dispatched extension disconnects as command_result_unknown', () => {
28
+ expect(buildExtensionDisconnectFailure({
29
+ contextId: 'work',
30
+ action: 'navigate',
31
+ dispatched: true,
32
+ })).toEqual({
33
+ message: 'Browser connection dropped after the navigate command was dispatched; it may have completed.',
34
+ errorCode: 'command_result_unknown',
35
+ errorHint: COMMAND_RESULT_UNKNOWN_HINT,
36
+ status: 503,
37
+ countAsCommandResultUnknown: true,
38
+ });
39
+ });
40
+ it('classifies pre-dispatch extension disconnects as profile_disconnected', () => {
41
+ expect(buildExtensionDisconnectFailure({
42
+ contextId: 'work',
43
+ action: 'navigate',
44
+ dispatched: false,
45
+ })).toMatchObject({
46
+ message: 'Browser profile "work" disconnected before command dispatch',
47
+ errorCode: 'profile_disconnected',
48
+ status: 503,
49
+ countAsCommandResultUnknown: false,
50
+ });
51
+ });
52
+ it('classifies ws.send dispatch failures as profile_disconnected', () => {
53
+ expect(buildCommandDispatchFailure('work')).toMatchObject({
54
+ message: 'Browser profile "work" disconnected before command dispatch',
55
+ errorCode: 'profile_disconnected',
56
+ status: 503,
57
+ countAsCommandResultUnknown: false,
58
+ });
59
+ });
60
+ });
@@ -12,7 +12,6 @@ export const builtinApps = {
12
12
  cursor: { port: 9226, processName: 'Cursor', bundleId: 'com.todesktop.runtime.Cursor', displayName: 'Cursor' },
13
13
  codex: { port: 9222, processName: 'Codex', bundleId: 'com.openai.codex', displayName: 'Codex' },
14
14
  chatwise: { port: 9228, processName: 'ChatWise', bundleId: 'com.chatwise.app', displayName: 'ChatWise' },
15
- notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' },
16
15
  'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' },
17
16
  'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' },
18
17
  antigravity: {
@@ -23,6 +23,7 @@ describe('electron-apps registry', () => {
23
23
  });
24
24
  it('isElectronApp returns false for non-Electron sites', () => {
25
25
  expect(isElectronApp('bilibili')).toBe(false);
26
+ expect(isElectronApp('notion')).toBe(false);
26
27
  expect(isElectronApp('unknown-app')).toBe(false);
27
28
  });
28
29
  it('loadApps merges user config additively', () => {
@@ -14,6 +14,12 @@
14
14
  install:
15
15
  mac: "brew install --cask obsidian"
16
16
 
17
+ - name: ntn
18
+ binary: ntn
19
+ description: "Notion CLI — official Notion API CLI for pages, databases, blocks, search, comments"
20
+ homepage: "https://ntn.dev"
21
+ tags: [notion, notes, knowledge, productivity]
22
+
17
23
  - name: docker
18
24
  binary: docker
19
25
  description: "Docker command-line interface"
@@ -55,24 +61,27 @@
55
61
  install:
56
62
  default: "npm install -g vercel"
57
63
 
58
- - name: tg-cli
64
+ - name: tg
59
65
  binary: tg
66
+ package: tg-cli
60
67
  description: "Telegram CLI — local-first sync, search, export via MTProto for AI agents"
61
68
  homepage: "https://github.com/jackwener/tg-cli"
62
69
  tags: [telegram, messaging, search, export, ai-agent]
63
70
  install:
64
71
  default: "uv tool install kabi-tg-cli"
65
72
 
66
- - name: discord-cli
73
+ - name: discord
67
74
  binary: discord
75
+ package: discord-cli
68
76
  description: "Discord CLI — local-first sync, search, export via SQLite for AI agents"
69
77
  homepage: "https://github.com/jackwener/discord-cli"
70
78
  tags: [discord, messaging, search, export, ai-agent]
71
79
  install:
72
80
  default: "uv tool install kabi-discord-cli"
73
81
 
74
- - name: wx-cli
82
+ - name: wx
75
83
  binary: wx
84
+ package: wx-cli
76
85
  description: "WeChat local data CLI — sessions, messages, search, contacts, export for AI agents"
77
86
  homepage: "https://github.com/jackwener/wx-cli"
78
87
  tags: [wechat, messaging, search, export, ai-agent]
@@ -5,8 +5,11 @@ export interface ExternalCliInstall {
5
5
  default?: string;
6
6
  }
7
7
  export interface ExternalCliConfig {
8
+ /** User-facing OpenCLI subcommand and, by default, the executable name. */
8
9
  name: string;
9
10
  binary: string;
11
+ /** Distribution/project name when it differs from the executable name. */
12
+ package?: string;
10
13
  description?: string;
11
14
  homepage?: string;
12
15
  tags?: string[];
@@ -15,6 +18,7 @@ export interface ExternalCliConfig {
15
18
  export declare function loadExternalClis(): ExternalCliConfig[];
16
19
  export declare function isBinaryInstalled(binary: string): boolean;
17
20
  export declare function getInstallCmd(installConfig?: ExternalCliInstall): string | null;
21
+ export declare function formatExternalCliLabel(cli: ExternalCliConfig): string;
18
22
  /**
19
23
  * Safely parses a command string into a binary and argument list.
20
24
  * Rejects commands containing shell operators (&&, ||, |, ;, >, <, `) that
@@ -70,6 +70,9 @@ export function getInstallCmd(installConfig) {
70
70
  return installConfig.default;
71
71
  return null;
72
72
  }
73
+ export function formatExternalCliLabel(cli) {
74
+ return cli.package && cli.package !== cli.name ? `${cli.name}(${cli.package})` : cli.name;
75
+ }
73
76
  /**
74
77
  * Safely parses a command string into a binary and argument list.
75
78
  * Rejects commands containing shell operators (&&, ||, |, ;, >, <, `) that
@@ -1,4 +1,8 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import yaml from 'js-yaml';
2
6
  const { mockExecFileSync, mockPlatform } = vi.hoisted(() => ({
3
7
  mockExecFileSync: vi.fn(),
4
8
  mockPlatform: vi.fn(() => 'darwin'),
@@ -14,7 +18,8 @@ vi.mock('node:os', async () => {
14
18
  platform: mockPlatform,
15
19
  };
16
20
  });
17
- import { installExternalCli, parseCommand } from './external.js';
21
+ import { formatExternalCliLabel, installExternalCli, parseCommand } from './external.js';
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
23
  describe('parseCommand', () => {
19
24
  it('splits binaries and quoted arguments without invoking a shell', () => {
20
25
  expect(parseCommand('npm install -g "@scope/tool name"')).toEqual({
@@ -29,6 +34,24 @@ describe('parseCommand', () => {
29
34
  expect(() => parseCommand('brew install $(whoami)')).toThrow('Install command contains unsafe shell operators');
30
35
  expect(() => parseCommand('brew install gh\nrm -rf /')).toThrow('Install command contains unsafe shell operators');
31
36
  });
37
+ it('keeps built-in install commands compatible with the shell-free parser', () => {
38
+ const raw = fs.readFileSync(path.join(__dirname, 'external-clis.yaml'), 'utf8');
39
+ const entries = (yaml.load(raw) || []);
40
+ for (const entry of entries) {
41
+ for (const command of Object.values(entry.install ?? {})) {
42
+ if (command)
43
+ expect(() => parseCommand(command)).not.toThrow();
44
+ }
45
+ }
46
+ });
47
+ });
48
+ describe('formatExternalCliLabel', () => {
49
+ it('shows the package name when the executable name differs', () => {
50
+ expect(formatExternalCliLabel({ name: 'wx', binary: 'wx', package: 'wx-cli' })).toBe('wx(wx-cli)');
51
+ });
52
+ it('keeps the label compact when package and name match', () => {
53
+ expect(formatExternalCliLabel({ name: 'docker', binary: 'docker', package: 'docker' })).toBe('docker');
54
+ });
32
55
  });
33
56
  describe('installExternalCli', () => {
34
57
  const cli = {
@@ -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.21",
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
- });