@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.
- package/README.md +11 -9
- package/README.zh-CN.md +9 -10
- package/cli-manifest.json +239 -249
- package/clis/_shared/search-adapter.js +70 -0
- package/clis/boss/chatlist.js +96 -14
- package/clis/boss/chatlist.test.js +211 -0
- package/clis/boss/chatmsg.js +98 -24
- package/clis/boss/chatmsg.test.js +230 -0
- package/clis/boss/utils.js +240 -11
- package/clis/brave/search.js +80 -0
- package/clis/brave/search.test.js +76 -0
- package/clis/duckduckgo/search.js +131 -0
- package/clis/duckduckgo/search.test.js +128 -0
- package/clis/duckduckgo/suggest.js +45 -0
- package/clis/duckduckgo/suggest.test.js +66 -0
- package/clis/facebook/feed.js +301 -56
- package/clis/facebook/feed.test.js +169 -0
- package/clis/reddit/comment.js +0 -1
- package/clis/reddit/frontpage.js +0 -1
- package/clis/reddit/home.js +0 -1
- package/clis/reddit/popular.js +0 -1
- package/clis/reddit/read.js +0 -1
- package/clis/reddit/read.test.js +2 -2
- package/clis/reddit/save.js +0 -1
- package/clis/reddit/saved.js +0 -1
- package/clis/reddit/search.js +0 -1
- package/clis/reddit/subreddit-info.js +0 -1
- package/clis/reddit/subreddit.js +0 -1
- package/clis/reddit/subscribe.js +0 -1
- package/clis/reddit/upvote.js +0 -1
- package/clis/reddit/upvoted.js +0 -1
- package/clis/reddit/user-comments.js +0 -1
- package/clis/reddit/user-posts.js +0 -1
- package/clis/reddit/user.js +0 -1
- package/clis/reddit/whoami.js +0 -1
- package/clis/rednote/rednote.test.js +65 -0
- package/clis/rednote/search.js +11 -5
- package/clis/twitter/article.js +0 -1
- package/clis/twitter/bookmark-folder.js +5 -4
- package/clis/twitter/bookmark-folder.test.js +59 -1
- package/clis/twitter/bookmark-folders.js +0 -1
- package/clis/twitter/bookmarks.js +9 -4
- package/clis/twitter/bookmarks.test.js +205 -0
- package/clis/twitter/download.js +0 -1
- package/clis/twitter/followers.js +0 -1
- package/clis/twitter/following.js +0 -1
- package/clis/twitter/likes.js +0 -1
- package/clis/twitter/list-tweets.js +0 -1
- package/clis/twitter/lists.js +0 -1
- package/clis/twitter/notifications.js +0 -1
- package/clis/twitter/profile.js +0 -1
- package/clis/twitter/search.js +0 -1
- package/clis/twitter/thread.js +0 -1
- package/clis/twitter/timeline.js +0 -1
- package/clis/twitter/trending.js +0 -1
- package/clis/twitter/tweets.js +0 -1
- package/clis/xiaohongshu/search.js +34 -16
- package/clis/xiaohongshu/search.test.js +66 -11
- package/clis/yahoo/search.js +92 -0
- package/clis/yahoo/search.test.js +94 -0
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +3 -0
- package/dist/src/browser/daemon-client.test.js +20 -0
- package/dist/src/cli.js +8 -3
- package/dist/src/cli.test.js +1 -0
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +1 -1
- package/dist/src/daemon.js +44 -13
- package/dist/src/daemon.test.js +42 -1
- package/dist/src/electron-apps.js +0 -1
- package/dist/src/electron-apps.test.js +1 -0
- package/dist/src/external-clis.yaml +12 -3
- package/dist/src/external.d.ts +4 -0
- package/dist/src/external.js +3 -0
- package/dist/src/external.test.js +24 -1
- package/dist/src/help.d.ts +5 -1
- package/dist/src/help.js +4 -3
- package/dist/src/help.test.js +5 -1
- package/package.json +1 -1
- package/clis/notion/export.js +0 -32
- package/clis/notion/favorites.js +0 -85
- package/clis/notion/new.js +0 -35
- package/clis/notion/read.js +0 -31
- package/clis/notion/search.js +0 -47
- package/clis/notion/sidebar.js +0 -42
- package/clis/notion/status.js +0 -17
- package/clis/notion/write.js +0 -41
package/dist/src/daemon.test.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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]
|
package/dist/src/external.d.ts
CHANGED
|
@@ -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
|
package/dist/src/external.js
CHANGED
|
@@ -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 = {
|
package/dist/src/help.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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,
|
package/dist/src/help.test.js
CHANGED
|
@@ -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: [
|
|
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
package/clis/notion/export.js
DELETED
|
@@ -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
|
-
});
|
package/clis/notion/favorites.js
DELETED
|
@@ -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
|
-
});
|
package/clis/notion/new.js
DELETED
|
@@ -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
|
-
});
|
package/clis/notion/read.js
DELETED
|
@@ -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
|
-
});
|
package/clis/notion/search.js
DELETED
|
@@ -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
|
-
});
|
package/clis/notion/sidebar.js
DELETED
|
@@ -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
|
-
});
|
package/clis/notion/status.js
DELETED
|
@@ -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
|
-
});
|
package/clis/notion/write.js
DELETED
|
@@ -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
|
-
});
|