@jackwener/opencli 1.7.15 → 1.7.17
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 +15 -13
- package/README.zh-CN.md +15 -12
- package/cli-manifest.json +165 -209
- package/clis/chatgpt/ask.js +3 -2
- package/clis/chatgpt/commands.test.js +2 -2
- package/clis/chatgpt/detail.js +7 -2
- package/clis/chatgpt/history.js +1 -1
- package/clis/chatgpt/image.js +38 -4
- package/clis/chatgpt/image.test.js +68 -1
- package/clis/chatgpt/new.js +1 -1
- package/clis/chatgpt/read.js +3 -2
- package/clis/chatgpt/send.js +3 -2
- package/clis/chatgpt/status.js +1 -1
- package/clis/chatgpt/utils.js +259 -25
- package/clis/chatgpt/utils.test.js +166 -2
- package/clis/claude/ask.js +23 -8
- package/clis/claude/detail.js +10 -3
- package/clis/claude/history.js +1 -1
- package/clis/claude/new.js +9 -3
- package/clis/claude/read.js +3 -2
- package/clis/claude/send.js +9 -4
- package/clis/claude/status.js +1 -1
- package/clis/claude/utils.js +27 -4
- package/clis/deepseek/ask.js +22 -9
- package/clis/deepseek/detail.js +10 -2
- package/clis/deepseek/history.js +1 -1
- package/clis/deepseek/new.js +14 -3
- package/clis/deepseek/read.js +3 -2
- package/clis/deepseek/send.js +1 -1
- package/clis/deepseek/status.js +1 -1
- package/clis/deepseek/utils.js +8 -1
- package/clis/doubao/ask.js +1 -1
- package/clis/doubao/detail.js +1 -1
- package/clis/doubao/history.js +1 -1
- package/clis/doubao/meeting-summary.js +1 -1
- package/clis/doubao/meeting-transcript.js +1 -1
- package/clis/doubao/new.js +1 -1
- package/clis/doubao/read.js +1 -1
- package/clis/doubao/send.js +1 -1
- package/clis/doubao/status.js +1 -1
- package/clis/gemini/ask.js +1 -1
- package/clis/gemini/deep-research-result.js +1 -1
- package/clis/gemini/deep-research.js +1 -1
- package/clis/gemini/image.js +1 -1
- package/clis/gemini/new.js +1 -1
- package/clis/grok/ask.js +1 -1
- package/clis/grok/detail.js +1 -1
- package/clis/grok/history.js +1 -1
- package/clis/grok/image.js +1 -1
- package/clis/grok/new.js +1 -1
- package/clis/grok/read.js +1 -1
- package/clis/grok/send.js +1 -1
- package/clis/grok/status.js +1 -1
- package/clis/linkedin/search.js +8 -11
- package/clis/maimai/search-talents.js +10 -6
- package/clis/notebooklm/current.js +1 -1
- package/clis/notebooklm/get.js +1 -1
- package/clis/notebooklm/history.js +1 -1
- package/clis/notebooklm/note-list.js +1 -1
- package/clis/notebooklm/notes-get.js +1 -1
- package/clis/notebooklm/open.js +2 -2
- package/clis/notebooklm/open.test.js +1 -1
- package/clis/notebooklm/source-fulltext.js +1 -1
- package/clis/notebooklm/source-get.js +1 -1
- package/clis/notebooklm/source-guide.js +1 -1
- package/clis/notebooklm/source-list.js +1 -1
- package/clis/notebooklm/summary.js +1 -1
- package/clis/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +83 -1
- package/clis/openreview/utils.js +14 -0
- package/clis/qwen/ask.js +1 -1
- package/clis/qwen/detail.js +1 -1
- package/clis/qwen/history.js +1 -1
- package/clis/qwen/image.js +1 -1
- package/clis/qwen/new.js +1 -1
- package/clis/qwen/read.js +1 -1
- package/clis/qwen/send.js +1 -1
- package/clis/qwen/status.js +1 -1
- package/clis/reddit/comment.js +1 -0
- package/clis/reddit/frontpage.js +1 -0
- package/clis/reddit/popular.js +1 -0
- package/clis/reddit/read.js +2 -0
- package/clis/reddit/read.test.js +4 -0
- package/clis/reddit/save.js +1 -0
- package/clis/reddit/saved.js +1 -0
- package/clis/reddit/search.js +1 -0
- package/clis/reddit/subreddit.js +1 -0
- package/clis/reddit/subscribe.js +1 -0
- package/clis/reddit/upvote.js +1 -0
- package/clis/reddit/upvoted.js +1 -0
- package/clis/reddit/user-comments.js +1 -0
- package/clis/reddit/user-posts.js +1 -0
- package/clis/reddit/user.js +1 -0
- package/clis/twitter/article.js +7 -4
- package/clis/twitter/bookmark-folder.js +3 -5
- package/clis/twitter/bookmark-folder.test.js +5 -2
- package/clis/twitter/bookmark-folders.js +3 -5
- package/clis/twitter/bookmark-folders.test.js +3 -1
- package/clis/twitter/bookmarks.js +3 -5
- package/clis/twitter/download.js +1 -0
- package/clis/twitter/followers.js +1 -0
- package/clis/twitter/following.js +3 -6
- package/clis/twitter/following.test.js +2 -1
- package/clis/twitter/likes.js +3 -5
- package/clis/twitter/list-add.js +4 -3
- package/clis/twitter/list-add.test.js +23 -1
- package/clis/twitter/list-remove.js +4 -3
- package/clis/twitter/list-remove.test.js +23 -1
- package/clis/twitter/list-tweets.js +3 -5
- package/clis/twitter/lists.js +3 -5
- package/clis/twitter/notifications.js +1 -0
- package/clis/twitter/profile.js +7 -4
- package/clis/twitter/search.js +1 -0
- package/clis/twitter/thread.js +5 -7
- package/clis/twitter/timeline.js +5 -7
- package/clis/twitter/trending.js +4 -4
- package/clis/twitter/tweets.js +3 -6
- package/clis/youtube/like.js +6 -2
- package/clis/youtube/subscribe.js +6 -2
- package/clis/youtube/unlike.js +6 -2
- package/clis/youtube/unsubscribe.js +6 -2
- package/clis/youtube/utils.js +19 -13
- package/clis/youtube/utils.test.js +17 -1
- package/clis/yuanbao/ask.js +1 -1
- package/clis/yuanbao/detail.js +1 -1
- package/clis/yuanbao/history.js +1 -1
- package/clis/yuanbao/new.js +1 -1
- package/clis/yuanbao/read.js +1 -1
- package/clis/yuanbao/send.js +1 -1
- package/clis/yuanbao/status.js +1 -1
- package/dist/src/browser/bridge.d.ts +4 -1
- package/dist/src/browser/bridge.js +3 -1
- package/dist/src/browser/cdp.d.ts +4 -1
- package/dist/src/browser/daemon-client.d.ts +9 -16
- package/dist/src/browser/daemon-client.js +8 -9
- package/dist/src/browser/daemon-client.test.js +10 -0
- package/dist/src/browser/network-cache.d.ts +5 -5
- package/dist/src/browser/network-cache.js +8 -8
- package/dist/src/browser/network-cache.test.js +4 -4
- package/dist/src/browser/page.d.ts +9 -7
- package/dist/src/browser/page.js +27 -16
- package/dist/src/browser/page.test.js +60 -30
- package/dist/src/build-manifest.js +1 -1
- package/dist/src/cli.js +91 -125
- package/dist/src/cli.test.js +293 -180
- package/dist/src/commanderAdapter.js +9 -0
- package/dist/src/discovery.js +1 -1
- package/dist/src/doctor.d.ts +0 -4
- package/dist/src/doctor.js +8 -72
- package/dist/src/doctor.test.js +26 -97
- package/dist/src/execution.d.ts +3 -0
- package/dist/src/execution.js +47 -23
- package/dist/src/execution.test.js +68 -45
- package/dist/src/external-clis.yaml +24 -0
- package/dist/src/help.d.ts +1 -0
- package/dist/src/help.js +36 -1
- package/dist/src/main.js +0 -29
- package/dist/src/manifest-types.d.ts +2 -4
- package/dist/src/observation/artifact.js +1 -1
- package/dist/src/observation/artifact.test.js +3 -3
- package/dist/src/observation/events.d.ts +1 -1
- package/dist/src/observation/manager.js +1 -1
- package/dist/src/observation/manager.test.js +3 -3
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +3 -12
- package/dist/src/registry.js +6 -10
- package/dist/src/runtime.d.ts +10 -2
- package/dist/src/runtime.js +4 -1
- package/dist/src/serialization.d.ts +1 -1
- package/dist/src/serialization.js +1 -1
- package/dist/src/types.d.ts +0 -15
- package/package.json +1 -1
|
@@ -80,18 +80,22 @@ cli({
|
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
// Read csrftoken directly from the cookie store via CDP — zero page.evaluate round-trip
|
|
84
|
+
const cookies = await page.getCookies({ url: 'https://maimai.cn' });
|
|
85
|
+
const csrftokenFromCookie = cookies.find((c) => c.name === 'csrftoken')?.value || '';
|
|
86
|
+
|
|
83
87
|
// Execute the search API call in browser context
|
|
84
|
-
const data = await page.evaluate(async (
|
|
85
|
-
//
|
|
86
|
-
let csrftoken =
|
|
87
|
-
.find(row => row.startsWith('csrftoken='))
|
|
88
|
-
?.split('=')[1] || '';
|
|
88
|
+
const data = await page.evaluate(`async () => {
|
|
89
|
+
// Prefer cookie-derived csrftoken (hoisted from CDP); fall back to meta tag
|
|
90
|
+
let csrftoken = ${JSON.stringify(csrftokenFromCookie)};
|
|
89
91
|
|
|
90
92
|
if (!csrftoken) {
|
|
91
93
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
92
94
|
if (meta) csrftoken = meta.getAttribute('content') || '';
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
const body = ${JSON.stringify(requestBody)};
|
|
98
|
+
|
|
95
99
|
const res = await fetch('https://maimai.cn/api/ent/discover/search?channel=www&data_version=3.0&version=1.0.0', {
|
|
96
100
|
method: 'POST',
|
|
97
101
|
headers: {
|
|
@@ -117,7 +121,7 @@ cli({
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
return result;
|
|
120
|
-
}
|
|
124
|
+
}`);
|
|
121
125
|
|
|
122
126
|
// Extract talent list from response
|
|
123
127
|
const talentList = data.data?.list || data.data?.talent_list || data.list || data.talent_list || [];
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
await requireNotebooklmSession(page);
|
|
18
18
|
const state = await getNotebooklmPageState(page);
|
|
19
19
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm current', 'No NotebookLM notebook is open in the
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm current', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
21
21
|
}
|
|
22
22
|
const current = await readCurrentNotebooklm(page);
|
|
23
23
|
if (!current) {
|
package/clis/notebooklm/get.js
CHANGED
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
await requireNotebooklmSession(page);
|
|
19
19
|
const state = await getNotebooklmPageState(page);
|
|
20
20
|
if (state.kind !== 'notebook') {
|
|
21
|
-
throw new EmptyResultError('opencli notebooklm get', 'No NotebookLM notebook is open in the
|
|
21
|
+
throw new EmptyResultError('opencli notebooklm get', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
22
22
|
}
|
|
23
23
|
const rpcRow = await getNotebooklmDetailViaRpc(page).catch(() => null);
|
|
24
24
|
if (rpcRow)
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
await requireNotebooklmSession(page);
|
|
18
18
|
const state = await getNotebooklmPageState(page);
|
|
19
19
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm history', 'No NotebookLM notebook is open in the
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm history', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
21
21
|
}
|
|
22
22
|
const rows = await listNotebooklmHistoryViaRpc(page);
|
|
23
23
|
return rows;
|
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
await requireNotebooklmSession(page);
|
|
19
19
|
const state = await getNotebooklmPageState(page);
|
|
20
20
|
if (state.kind !== 'notebook') {
|
|
21
|
-
throw new EmptyResultError('opencli notebooklm note-list', 'No NotebookLM notebook is open in the
|
|
21
|
+
throw new EmptyResultError('opencli notebooklm note-list', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
22
22
|
}
|
|
23
23
|
const rows = await listNotebooklmNotesFromPage(page);
|
|
24
24
|
if (rows.length > 0)
|
|
@@ -31,7 +31,7 @@ cli({
|
|
|
31
31
|
await requireNotebooklmSession(page);
|
|
32
32
|
const state = await getNotebooklmPageState(page);
|
|
33
33
|
if (state.kind !== 'notebook') {
|
|
34
|
-
throw new EmptyResultError('opencli notebooklm notes-get', 'No NotebookLM notebook is open in the
|
|
34
|
+
throw new EmptyResultError('opencli notebooklm notes-get', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
35
35
|
}
|
|
36
36
|
const query = typeof kwargs.note === 'string' ? kwargs.note : String(kwargs.note ?? '');
|
|
37
37
|
const visible = await readNotebooklmVisibleNoteFromPage(page);
|
package/clis/notebooklm/open.js
CHANGED
|
@@ -7,7 +7,7 @@ cli({
|
|
|
7
7
|
name: 'open',
|
|
8
8
|
access: 'read',
|
|
9
9
|
aliases: ['select'],
|
|
10
|
-
description: 'Open one NotebookLM notebook in the
|
|
10
|
+
description: 'Open one NotebookLM notebook in the adapter session by id or URL',
|
|
11
11
|
domain: NOTEBOOKLM_DOMAIN,
|
|
12
12
|
strategy: Strategy.COOKIE,
|
|
13
13
|
browser: true,
|
|
@@ -28,7 +28,7 @@ cli({
|
|
|
28
28
|
await requireNotebooklmSession(page);
|
|
29
29
|
const state = await getNotebooklmPageState(page);
|
|
30
30
|
if (state.kind !== 'notebook') {
|
|
31
|
-
throw new CliError('NOTEBOOKLM_OPEN_FAILED', `NotebookLM notebook "${notebookId}" did not open in the
|
|
31
|
+
throw new CliError('NOTEBOOKLM_OPEN_FAILED', `NotebookLM notebook "${notebookId}" did not open in the adapter session`, 'Run `opencli notebooklm list -f json` first and pass a valid notebook id.');
|
|
32
32
|
}
|
|
33
33
|
if (state.notebookId !== notebookId) {
|
|
34
34
|
console.warn(`[notebooklm open] expected notebook "${notebookId}" but page reports "${state.notebookId}"; continuing`);
|
|
@@ -38,7 +38,7 @@ describe('notebooklm open', () => {
|
|
|
38
38
|
source: 'current-page',
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
-
it('opens a notebook by id in the
|
|
41
|
+
it('opens a notebook by id in the adapter session', async () => {
|
|
42
42
|
const page = {
|
|
43
43
|
goto: vi.fn(async () => { }),
|
|
44
44
|
wait: vi.fn(async () => { }),
|
|
@@ -24,7 +24,7 @@ cli({
|
|
|
24
24
|
await requireNotebooklmSession(page);
|
|
25
25
|
const state = await getNotebooklmPageState(page);
|
|
26
26
|
if (state.kind !== 'notebook') {
|
|
27
|
-
throw new EmptyResultError('opencli notebooklm source-fulltext', 'No NotebookLM notebook is open in the
|
|
27
|
+
throw new EmptyResultError('opencli notebooklm source-fulltext', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
28
28
|
}
|
|
29
29
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
30
30
|
const rows = rpcRows.length > 0 ? rpcRows : await listNotebooklmSourcesFromPage(page);
|
|
@@ -24,7 +24,7 @@ cli({
|
|
|
24
24
|
await requireNotebooklmSession(page);
|
|
25
25
|
const state = await getNotebooklmPageState(page);
|
|
26
26
|
if (state.kind !== 'notebook') {
|
|
27
|
-
throw new EmptyResultError('opencli notebooklm source-get', 'No NotebookLM notebook is open in the
|
|
27
|
+
throw new EmptyResultError('opencli notebooklm source-get', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
28
28
|
}
|
|
29
29
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
30
30
|
const rows = rpcRows.length > 0 ? rpcRows : await listNotebooklmSourcesFromPage(page);
|
|
@@ -24,7 +24,7 @@ cli({
|
|
|
24
24
|
await requireNotebooklmSession(page);
|
|
25
25
|
const state = await getNotebooklmPageState(page);
|
|
26
26
|
if (state.kind !== 'notebook') {
|
|
27
|
-
throw new EmptyResultError('opencli notebooklm source-guide', 'No NotebookLM notebook is open in the
|
|
27
|
+
throw new EmptyResultError('opencli notebooklm source-guide', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
28
28
|
}
|
|
29
29
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
30
30
|
const rows = rpcRows.length > 0 ? rpcRows : await listNotebooklmSourcesFromPage(page);
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
await requireNotebooklmSession(page);
|
|
18
18
|
const state = await getNotebooklmPageState(page);
|
|
19
19
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm source-list', 'No NotebookLM notebook is open in the
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm source-list', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
21
21
|
}
|
|
22
22
|
const rpcRows = await listNotebooklmSourcesViaRpc(page).catch(() => []);
|
|
23
23
|
if (rpcRows.length > 0)
|
|
@@ -17,7 +17,7 @@ cli({
|
|
|
17
17
|
await requireNotebooklmSession(page);
|
|
18
18
|
const state = await getNotebooklmPageState(page);
|
|
19
19
|
if (state.kind !== 'notebook') {
|
|
20
|
-
throw new EmptyResultError('opencli notebooklm summary', 'No NotebookLM notebook is open in the
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm summary', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
|
|
21
21
|
}
|
|
22
22
|
const domSummary = await readNotebooklmSummaryFromPage(page);
|
|
23
23
|
if (domSummary)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenReview submissions by author profile id (newest first).
|
|
3
|
+
*
|
|
4
|
+
* Pairs with `openreview paper <id>` and `openreview reviews <id>` for the
|
|
5
|
+
* full read-side workflow: list every submission an author put on
|
|
6
|
+
* OpenReview, then drill into a specific paper or its review thread.
|
|
7
|
+
*
|
|
8
|
+
* Uses the public v2 endpoint `/notes?content.authorids=~<profile-id>`,
|
|
9
|
+
* which returns the same note shape as `paper`, sorted by `cdate:desc`.
|
|
10
|
+
*/
|
|
11
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
12
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
13
|
+
import {
|
|
14
|
+
noteToRow,
|
|
15
|
+
openreviewFetch,
|
|
16
|
+
requireBoundedInt,
|
|
17
|
+
requireProfileId,
|
|
18
|
+
} from './utils.js';
|
|
19
|
+
|
|
20
|
+
cli({
|
|
21
|
+
site: 'openreview',
|
|
22
|
+
name: 'author',
|
|
23
|
+
access: 'read',
|
|
24
|
+
description: 'List OpenReview submissions by an author profile id (newest first)',
|
|
25
|
+
domain: 'openreview.net',
|
|
26
|
+
strategy: Strategy.PUBLIC,
|
|
27
|
+
browser: false,
|
|
28
|
+
args: [
|
|
29
|
+
{ name: 'profile', positional: true, required: true, help: 'OpenReview profile id (e.g. "~Yoshua_Bengio1"). Find it on the author profile URL on openreview.net.' },
|
|
30
|
+
{ name: 'limit', type: 'int', default: 50, help: 'Max submissions (1-1000)' },
|
|
31
|
+
],
|
|
32
|
+
columns: ['rank', 'id', 'title', 'authors', 'venue', 'pdate', 'url'],
|
|
33
|
+
func: async (args) => {
|
|
34
|
+
const profile = requireProfileId(args.profile);
|
|
35
|
+
const limit = requireBoundedInt(args.limit, 50, 1000);
|
|
36
|
+
const path = `/notes?content.authorids=${encodeURIComponent(profile)}&limit=${limit}&sort=cdate:desc`;
|
|
37
|
+
const json = await openreviewFetch(path, `openreview author ${profile}`);
|
|
38
|
+
const notes = Array.isArray(json?.notes) ? json.notes : [];
|
|
39
|
+
if (!notes.length) {
|
|
40
|
+
throw new EmptyResultError(
|
|
41
|
+
'openreview author',
|
|
42
|
+
`No OpenReview submissions found for profile "${profile}". Confirm the id format (~First_LastN) and that the profile has public submissions.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return notes.slice(0, limit).map((note, i) => {
|
|
46
|
+
const row = noteToRow(note);
|
|
47
|
+
return {
|
|
48
|
+
rank: i + 1,
|
|
49
|
+
id: row.id,
|
|
50
|
+
title: row.title,
|
|
51
|
+
authors: row.authors,
|
|
52
|
+
venue: row.venue,
|
|
53
|
+
pdate: row.pdate,
|
|
54
|
+
url: row.url,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
requireBoundedInt,
|
|
9
9
|
requireForumId,
|
|
10
10
|
requireNonNegativeInt,
|
|
11
|
+
requireProfileId,
|
|
11
12
|
} from './utils.js';
|
|
12
13
|
import './search.js';
|
|
13
14
|
import './venue.js';
|
|
14
15
|
import './paper.js';
|
|
15
16
|
import './reviews.js';
|
|
17
|
+
import './author.js';
|
|
16
18
|
|
|
17
19
|
const SAMPLE_NOTE = {
|
|
18
20
|
id: 'abc123XYZ_',
|
|
@@ -37,21 +39,24 @@ afterEach(() => {
|
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
describe('openreview adapter', () => {
|
|
40
|
-
it('registers all
|
|
42
|
+
it('registers all five commands with the expected columns', () => {
|
|
41
43
|
const search = getRegistry().get('openreview/search');
|
|
42
44
|
const venue = getRegistry().get('openreview/venue');
|
|
43
45
|
const paper = getRegistry().get('openreview/paper');
|
|
44
46
|
const reviews = getRegistry().get('openreview/reviews');
|
|
47
|
+
const author = getRegistry().get('openreview/author');
|
|
45
48
|
|
|
46
49
|
expect(search).toBeDefined();
|
|
47
50
|
expect(venue).toBeDefined();
|
|
48
51
|
expect(paper).toBeDefined();
|
|
49
52
|
expect(reviews).toBeDefined();
|
|
53
|
+
expect(author).toBeDefined();
|
|
50
54
|
|
|
51
55
|
expect(search.columns).toEqual(['rank', 'id', 'title', 'authors', 'venue', 'pdate', 'url']);
|
|
52
56
|
expect(venue.columns).toEqual(['rank', 'id', 'title', 'authors', 'keywords', 'primary_area', 'pdate', 'pdf', 'url']);
|
|
53
57
|
expect(paper.columns).toEqual(['id', 'title', 'authors', 'keywords', 'venue', 'venueid', 'primary_area', 'abstract', 'pdate', 'pdf', 'url']);
|
|
54
58
|
expect(reviews.columns).toEqual(['type', 'author', 'rating', 'confidence', 'text']);
|
|
59
|
+
expect(author.columns).toEqual(['rank', 'id', 'title', 'authors', 'venue', 'pdate', 'url']);
|
|
55
60
|
});
|
|
56
61
|
|
|
57
62
|
it('noteToRow extracts every wrapped v2 field, joins lists, and builds absolute URLs', () => {
|
|
@@ -109,6 +114,30 @@ describe('openreview adapter', () => {
|
|
|
109
114
|
expect(() => requireForumId('short')).toThrow('not a valid forum id');
|
|
110
115
|
});
|
|
111
116
|
|
|
117
|
+
it('requireProfileId accepts canonical profile ids and rejects malformed input', () => {
|
|
118
|
+
expect(requireProfileId('~Yoshua_Bengio1')).toBe('~Yoshua_Bengio1');
|
|
119
|
+
expect(requireProfileId('~Bo_Liu17')).toBe('~Bo_Liu17');
|
|
120
|
+
expect(requireProfileId('~Geoffrey_Everest_Hinton1')).toBe('~Geoffrey_Everest_Hinton1');
|
|
121
|
+
expect(requireProfileId('~Anne-Christin_Hauschild1')).toBe('~Anne-Christin_Hauschild1');
|
|
122
|
+
expect(requireProfileId('~S.Aruna_Deepthi1')).toBe('~S.Aruna_Deepthi1');
|
|
123
|
+
expect(requireProfileId('~Andrzej_Czyżewski1')).toBe('~Andrzej_Czyżewski1');
|
|
124
|
+
expect(requireProfileId('~August_Bøgh_Rønberg1')).toBe('~August_Bøgh_Rønberg1');
|
|
125
|
+
expect(requireProfileId('~Wagner_Meira_Jr.1')).toBe('~Wagner_Meira_Jr.1');
|
|
126
|
+
expect(() => requireProfileId('')).toThrow('required');
|
|
127
|
+
expect(() => requireProfileId(' ')).toThrow('required');
|
|
128
|
+
// Missing leading tilde.
|
|
129
|
+
expect(() => requireProfileId('Bo_Liu17')).toThrow('not a valid profile id');
|
|
130
|
+
// Missing trailing disambiguator number.
|
|
131
|
+
expect(() => requireProfileId('~Bo_Liu')).toThrow('not a valid profile id');
|
|
132
|
+
// Spaces / non-letter characters break the underscore-joined name.
|
|
133
|
+
expect(() => requireProfileId('~Bo Liu1')).toThrow('not a valid profile id');
|
|
134
|
+
// dblp-style PID must not silently fall through.
|
|
135
|
+
expect(() => requireProfileId('56/953')).toThrow('not a valid profile id');
|
|
136
|
+
expect(() => requireProfileId('~Bo_Liu1?evil=1')).toThrow('not a valid profile id');
|
|
137
|
+
expect(() => requireProfileId('~Bo/Liu1')).toThrow('not a valid profile id');
|
|
138
|
+
expect(() => requireProfileId('~123')).toThrow('not a valid profile id');
|
|
139
|
+
});
|
|
140
|
+
|
|
112
141
|
it('formatDate handles ms-since-epoch and rejects invalid input', () => {
|
|
113
142
|
expect(formatDate(1727524853394)).toBe('2024-09-28');
|
|
114
143
|
expect(formatDate(0)).toBe('');
|
|
@@ -342,4 +371,57 @@ describe('openreview adapter', () => {
|
|
|
342
371
|
expect(rows[1].text.length).toBe(500);
|
|
343
372
|
expect(rows[1].text.endsWith('...')).toBe(true);
|
|
344
373
|
});
|
|
374
|
+
|
|
375
|
+
it('author rejects invalid profile ids before calling the network', async () => {
|
|
376
|
+
const fetchMock = vi.fn();
|
|
377
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
378
|
+
const author = getRegistry().get('openreview/author');
|
|
379
|
+
await expect(author.func({ profile: 'Bo_Liu17', limit: 5 })).rejects.toMatchObject({ code: 'ARGUMENT' });
|
|
380
|
+
await expect(author.func({ profile: '', limit: 5 })).rejects.toMatchObject({ code: 'ARGUMENT' });
|
|
381
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('author throws EmptyResult when the profile has no submissions', async () => {
|
|
385
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({ notes: [] }), { status: 200 })));
|
|
386
|
+
const author = getRegistry().get('openreview/author');
|
|
387
|
+
await expect(author.func({ profile: '~No_Submissions1', limit: 5 })).rejects.toMatchObject({ code: 'EMPTY_RESULT' });
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('author wraps non-200 responses as CommandExecutionError', async () => {
|
|
391
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('rate limited', { status: 429 })));
|
|
392
|
+
const author = getRegistry().get('openreview/author');
|
|
393
|
+
await expect(author.func({ profile: '~Bo_Liu17', limit: 5 })).rejects.toMatchObject({ code: 'COMMAND_EXEC' });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('author wraps fetch network errors as CommandExecutionError', async () => {
|
|
397
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNRESET')));
|
|
398
|
+
const author = getRegistry().get('openreview/author');
|
|
399
|
+
await expect(author.func({ profile: '~Bo_Liu17', limit: 5 })).rejects.toMatchObject({
|
|
400
|
+
code: 'COMMAND_EXEC',
|
|
401
|
+
message: expect.stringContaining('Network failure'),
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('author hits /notes?content.authorids and returns rank-ordered rows', async () => {
|
|
406
|
+
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ notes: [SAMPLE_NOTE, SAMPLE_NOTE] }), { status: 200 }));
|
|
407
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
408
|
+
const author = getRegistry().get('openreview/author');
|
|
409
|
+
const rows = await author.func({ profile: '~Bo_Liu17', limit: 50 });
|
|
410
|
+
expect(rows).toHaveLength(2);
|
|
411
|
+
expect(rows[0]).toEqual({
|
|
412
|
+
rank: 1,
|
|
413
|
+
id: 'abc123XYZ_',
|
|
414
|
+
title: 'Test Paper Title with spaces',
|
|
415
|
+
authors: 'Alice Smith, Bob Jones',
|
|
416
|
+
venue: 'ICLR 2024 oral',
|
|
417
|
+
pdate: '2024-09-28',
|
|
418
|
+
url: 'https://openreview.net/forum?id=abc123XYZ_',
|
|
419
|
+
});
|
|
420
|
+
expect(rows[1].rank).toBe(2);
|
|
421
|
+
// Confirm the request shape: canonical authorids filter + cdate sort.
|
|
422
|
+
const url = fetchMock.mock.calls[0][0];
|
|
423
|
+
expect(url).toContain('content.authorids=');
|
|
424
|
+
expect(url).toContain(encodeURIComponent('~Bo_Liu17'));
|
|
425
|
+
expect(url).toContain('sort=cdate:desc');
|
|
426
|
+
});
|
|
345
427
|
});
|
package/clis/openreview/utils.js
CHANGED
|
@@ -55,6 +55,20 @@ export function requireForumId(value, label = 'id') {
|
|
|
55
55
|
return id;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** OpenReview profile ids are `~...N` slugs and may include dots, hyphens, and Unicode letters. */
|
|
59
|
+
const PROFILE_ID_PATTERN = /^~(?=.*\p{L})[\p{L}\p{M}0-9._-]+\d+$/u;
|
|
60
|
+
|
|
61
|
+
export function requireProfileId(value, label = 'profile') {
|
|
62
|
+
const id = String(value ?? '').trim();
|
|
63
|
+
if (!id) {
|
|
64
|
+
throw new ArgumentError(`openreview ${label} is required`);
|
|
65
|
+
}
|
|
66
|
+
if (!PROFILE_ID_PATTERN.test(id)) {
|
|
67
|
+
throw new ArgumentError(`openreview ${label} "${value}" is not a valid profile id (expected "~First_Last1" or similar; find it on the author's openreview.net profile URL)`);
|
|
68
|
+
}
|
|
69
|
+
return id;
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
/** Wrap fetch + json with typed errors so failures never look like empty results. */
|
|
59
73
|
export async function openreviewFetch(path, label) {
|
|
60
74
|
const url = `${OPENREVIEW_API}${path}`;
|
package/clis/qwen/ask.js
CHANGED
package/clis/qwen/detail.js
CHANGED
|
@@ -18,7 +18,7 @@ cli({
|
|
|
18
18
|
strategy: Strategy.COOKIE,
|
|
19
19
|
browser: true,
|
|
20
20
|
navigateBefore: false,
|
|
21
|
-
|
|
21
|
+
siteSession: 'persistent',
|
|
22
22
|
args: [
|
|
23
23
|
{ name: 'id', positional: true, required: true, help: 'Session ID (32-char hex) or full https://www.qianwen.com/chat/<id> URL' },
|
|
24
24
|
{ name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
|
package/clis/qwen/history.js
CHANGED
|
@@ -24,7 +24,7 @@ cli({
|
|
|
24
24
|
domain: QIANWEN_DOMAIN,
|
|
25
25
|
strategy: Strategy.COOKIE,
|
|
26
26
|
browser: true,
|
|
27
|
-
|
|
27
|
+
siteSession: 'persistent',
|
|
28
28
|
navigateBefore: false,
|
|
29
29
|
args: [
|
|
30
30
|
{ name: 'limit', type: 'int', default: 20, help: 'Max conversations to show (default 20, max 100)' },
|
package/clis/qwen/image.js
CHANGED
package/clis/qwen/new.js
CHANGED
package/clis/qwen/read.js
CHANGED
|
@@ -16,7 +16,7 @@ cli({
|
|
|
16
16
|
domain: QIANWEN_DOMAIN,
|
|
17
17
|
strategy: Strategy.COOKIE,
|
|
18
18
|
browser: true,
|
|
19
|
-
|
|
19
|
+
siteSession: 'persistent',
|
|
20
20
|
navigateBefore: false,
|
|
21
21
|
args: [
|
|
22
22
|
{ name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
|
package/clis/qwen/send.js
CHANGED
|
@@ -20,7 +20,7 @@ cli({
|
|
|
20
20
|
domain: QIANWEN_DOMAIN,
|
|
21
21
|
strategy: Strategy.COOKIE,
|
|
22
22
|
browser: true,
|
|
23
|
-
|
|
23
|
+
siteSession: 'persistent',
|
|
24
24
|
navigateBefore: false,
|
|
25
25
|
args: [
|
|
26
26
|
{ name: 'prompt', required: true, positional: true, help: 'Prompt to send to Qianwen' },
|
package/clis/qwen/status.js
CHANGED
package/clis/reddit/comment.js
CHANGED
|
@@ -8,6 +8,7 @@ cli({
|
|
|
8
8
|
domain: 'reddit.com',
|
|
9
9
|
strategy: Strategy.COOKIE,
|
|
10
10
|
browser: true,
|
|
11
|
+
siteSession: 'persistent',
|
|
11
12
|
args: [
|
|
12
13
|
{ name: 'post-id', type: 'string', required: true, positional: true, help: 'Post ID (e.g. 1abc123) or fullname (t3_xxx)' },
|
|
13
14
|
{ name: 'text', type: 'string', required: true, positional: true, help: 'Comment text' },
|
package/clis/reddit/frontpage.js
CHANGED
package/clis/reddit/popular.js
CHANGED
package/clis/reddit/read.js
CHANGED
|
@@ -15,6 +15,8 @@ cli({
|
|
|
15
15
|
description: 'Read a Reddit post and its comments',
|
|
16
16
|
domain: 'reddit.com',
|
|
17
17
|
strategy: Strategy.COOKIE,
|
|
18
|
+
browser: true,
|
|
19
|
+
siteSession: 'persistent',
|
|
18
20
|
args: [
|
|
19
21
|
{ name: 'post-id', required: true, positional: true, help: 'Post ID (e.g. 1abc123) or full URL' },
|
|
20
22
|
{ name: 'sort', default: 'best', help: 'Comment sort: best, top, new, controversial, old, qa' },
|
package/clis/reddit/read.test.js
CHANGED
|
@@ -3,6 +3,10 @@ import { getRegistry } from '@jackwener/opencli/registry';
|
|
|
3
3
|
import './read.js';
|
|
4
4
|
describe('reddit read adapter', () => {
|
|
5
5
|
const command = getRegistry().get('reddit/read');
|
|
6
|
+
it('opts into the Reddit persistent site session', () => {
|
|
7
|
+
expect(command?.browser).toBe(true);
|
|
8
|
+
expect(command?.siteSession).toBe('persistent');
|
|
9
|
+
});
|
|
6
10
|
it('returns threaded rows from the browser-evaluated payload', async () => {
|
|
7
11
|
const page = {
|
|
8
12
|
goto: vi.fn().mockResolvedValue(undefined),
|
package/clis/reddit/save.js
CHANGED
|
@@ -8,6 +8,7 @@ cli({
|
|
|
8
8
|
domain: 'reddit.com',
|
|
9
9
|
strategy: Strategy.COOKIE,
|
|
10
10
|
browser: true,
|
|
11
|
+
siteSession: 'persistent',
|
|
11
12
|
args: [
|
|
12
13
|
{ name: 'post-id', type: 'string', required: true, positional: true, help: 'Post ID (e.g. 1abc123) or fullname (t3_xxx)' },
|
|
13
14
|
{ name: 'undo', type: 'boolean', default: false, help: 'Unsave instead of save' },
|
package/clis/reddit/saved.js
CHANGED
package/clis/reddit/search.js
CHANGED
package/clis/reddit/subreddit.js
CHANGED
package/clis/reddit/subscribe.js
CHANGED
|
@@ -8,6 +8,7 @@ cli({
|
|
|
8
8
|
domain: 'reddit.com',
|
|
9
9
|
strategy: Strategy.COOKIE,
|
|
10
10
|
browser: true,
|
|
11
|
+
siteSession: 'persistent',
|
|
11
12
|
args: [
|
|
12
13
|
{ name: 'subreddit', type: 'string', required: true, positional: true, help: 'Subreddit name (e.g. python)' },
|
|
13
14
|
{ name: 'undo', type: 'boolean', default: false, help: 'Unsubscribe instead of subscribe' },
|
package/clis/reddit/upvote.js
CHANGED
|
@@ -8,6 +8,7 @@ cli({
|
|
|
8
8
|
domain: 'reddit.com',
|
|
9
9
|
strategy: Strategy.COOKIE,
|
|
10
10
|
browser: true,
|
|
11
|
+
siteSession: 'persistent',
|
|
11
12
|
args: [
|
|
12
13
|
{ name: 'post-id', type: 'string', required: true, positional: true, help: 'Post ID (e.g. 1abc123) or fullname (t3_xxx)' },
|
|
13
14
|
{ name: 'direction', type: 'string', default: 'up', help: 'Vote direction: up, down, none' },
|
package/clis/reddit/upvoted.js
CHANGED
|
@@ -7,6 +7,7 @@ cli({
|
|
|
7
7
|
domain: 'reddit.com',
|
|
8
8
|
strategy: Strategy.COOKIE,
|
|
9
9
|
browser: true,
|
|
10
|
+
siteSession: 'persistent',
|
|
10
11
|
args: [
|
|
11
12
|
{ name: 'username', type: 'string', required: true, positional: true, help: 'Reddit username (no `u/` prefix needed)' },
|
|
12
13
|
{ name: 'limit', type: 'int', default: 15 },
|
|
@@ -7,6 +7,7 @@ cli({
|
|
|
7
7
|
domain: 'reddit.com',
|
|
8
8
|
strategy: Strategy.COOKIE,
|
|
9
9
|
browser: true,
|
|
10
|
+
siteSession: 'persistent',
|
|
10
11
|
args: [
|
|
11
12
|
{ name: 'username', type: 'string', required: true, positional: true, help: 'Reddit username (no `u/` prefix needed)' },
|
|
12
13
|
{ name: 'limit', type: 'int', default: 15 },
|
package/clis/reddit/user.js
CHANGED