@jackwener/opencli 1.5.6 → 1.5.7
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/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -1
- package/SKILL.md +879 -0
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.js +30 -27
- package/dist/browser/daemon-client.d.ts +7 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.js +1 -0
- package/dist/browser/dom-helpers.test.js +14 -1
- package/dist/browser/mcp.js +18 -13
- package/dist/browser/page.js +22 -2
- package/dist/browser/page.test.d.ts +1 -0
- package/dist/browser/page.test.js +44 -0
- package/dist/browser/stealth.js +198 -0
- package/dist/browser/stealth.test.d.ts +1 -0
- package/dist/browser/stealth.test.js +134 -0
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +5 -1
- package/dist/build-manifest.test.js +2 -0
- package/dist/cli-manifest.json +544 -137
- package/dist/cli.js +20 -3
- package/dist/clis/antigravity/serve.d.ts +1 -1
- package/dist/clis/antigravity/serve.js +5 -8
- package/dist/clis/bilibili/subtitle.js +4 -0
- package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.test.js +48 -0
- package/dist/clis/chatwise/ask.js +0 -2
- package/dist/clis/chatwise/export.js +0 -2
- package/dist/clis/chatwise/history.js +0 -2
- package/dist/clis/chatwise/model.js +0 -2
- package/dist/clis/chatwise/new.js +1 -2
- package/dist/clis/chatwise/read.js +0 -2
- package/dist/clis/chatwise/screenshot.js +1 -2
- package/dist/clis/chatwise/send.js +0 -2
- package/dist/clis/chatwise/status.js +1 -2
- package/dist/clis/ctrip/search.d.ts +13 -0
- package/dist/clis/ctrip/search.js +73 -48
- package/dist/clis/ctrip/search.test.d.ts +1 -0
- package/dist/clis/ctrip/search.test.js +64 -0
- package/dist/clis/douyin/_shared/sts2.js +8 -2
- package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/sts2.test.js +27 -0
- package/dist/clis/douyin/activities.js +4 -2
- package/dist/clis/douyin/activities.test.js +34 -1
- package/dist/clis/douyin/collections.js +1 -1
- package/dist/clis/douyin/collections.test.js +24 -2
- package/dist/clis/douyin/draft.d.ts +8 -11
- package/dist/clis/douyin/draft.js +302 -185
- package/dist/clis/douyin/draft.test.d.ts +1 -1
- package/dist/clis/douyin/draft.test.js +357 -2
- package/dist/clis/douyin/hashtag.js +9 -2
- package/dist/clis/douyin/hashtag.test.js +35 -2
- package/dist/clis/douyin/profile.js +1 -1
- package/dist/clis/douyin/profile.test.js +36 -1
- package/dist/clis/douyin/videos.js +22 -5
- package/dist/clis/douyin/videos.test.js +45 -2
- package/dist/clis/facebook/search.test.d.ts +5 -0
- package/dist/clis/facebook/search.test.js +60 -0
- package/dist/clis/facebook/search.yaml +4 -3
- package/dist/clis/instagram/download.d.ts +16 -0
- package/dist/clis/instagram/download.js +225 -0
- package/dist/clis/instagram/download.test.d.ts +1 -0
- package/dist/clis/instagram/download.test.js +118 -0
- package/dist/clis/notebooklm/bind-current.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.js +29 -0
- package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.test.js +35 -0
- package/dist/clis/notebooklm/binding.test.d.ts +1 -0
- package/dist/clis/notebooklm/binding.test.js +44 -0
- package/dist/clis/notebooklm/compat.test.d.ts +3 -0
- package/dist/clis/notebooklm/compat.test.js +16 -0
- package/dist/clis/notebooklm/current.d.ts +1 -0
- package/dist/clis/notebooklm/current.js +28 -0
- package/dist/clis/notebooklm/get.d.ts +1 -0
- package/dist/clis/notebooklm/get.js +37 -0
- package/dist/clis/notebooklm/history.d.ts +1 -0
- package/dist/clis/notebooklm/history.js +25 -0
- package/dist/clis/notebooklm/history.test.d.ts +1 -0
- package/dist/clis/notebooklm/history.test.js +58 -0
- package/dist/clis/notebooklm/list.d.ts +1 -0
- package/dist/clis/notebooklm/list.js +35 -0
- package/dist/clis/notebooklm/note-list.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.js +28 -0
- package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.test.js +56 -0
- package/dist/clis/notebooklm/notes-get.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.js +47 -0
- package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.test.js +72 -0
- package/dist/clis/notebooklm/rpc.d.ts +36 -0
- package/dist/clis/notebooklm/rpc.js +189 -0
- package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
- package/dist/clis/notebooklm/rpc.test.js +105 -0
- package/dist/clis/notebooklm/shared.d.ts +87 -0
- package/dist/clis/notebooklm/shared.js +3 -0
- package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.js +44 -0
- package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
- package/dist/clis/notebooklm/source-get.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.js +40 -0
- package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.test.js +84 -0
- package/dist/clis/notebooklm/source-guide.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.js +44 -0
- package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.test.js +104 -0
- package/dist/clis/notebooklm/source-list.d.ts +1 -0
- package/dist/clis/notebooklm/source-list.js +30 -0
- package/dist/clis/notebooklm/status.d.ts +1 -0
- package/dist/clis/notebooklm/status.js +31 -0
- package/dist/clis/notebooklm/summary.d.ts +1 -0
- package/dist/clis/notebooklm/summary.js +30 -0
- package/dist/clis/notebooklm/summary.test.d.ts +1 -0
- package/dist/clis/notebooklm/summary.test.js +78 -0
- package/dist/clis/notebooklm/utils.d.ts +37 -0
- package/dist/clis/notebooklm/utils.js +739 -0
- package/dist/clis/notebooklm/utils.test.d.ts +1 -0
- package/dist/clis/notebooklm/utils.test.js +390 -0
- package/dist/clis/substack/utils.d.ts +4 -0
- package/dist/clis/substack/utils.js +8 -2
- package/dist/clis/substack/utils.test.d.ts +1 -0
- package/dist/clis/substack/utils.test.js +46 -0
- package/dist/clis/v2ex/hot.yaml +4 -1
- package/dist/clis/v2ex/latest.yaml +4 -1
- package/dist/clis/v2ex/topic.yaml +6 -1
- package/dist/clis/weixin/download.d.ts +9 -0
- package/dist/clis/weixin/download.js +76 -6
- package/dist/clis/weread/book.js +108 -2
- package/dist/clis/weread/commands.test.js +262 -152
- package/dist/clis/weread/utils.d.ts +10 -0
- package/dist/clis/weread/utils.js +27 -7
- package/dist/clis/xiaohongshu/comments.d.ts +3 -0
- package/dist/clis/xiaohongshu/comments.js +76 -17
- package/dist/clis/xiaohongshu/comments.test.js +70 -9
- package/dist/clis/xiaohongshu/download.d.ts +4 -1
- package/dist/clis/xiaohongshu/download.js +83 -22
- package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/download.test.js +75 -0
- package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
- package/dist/clis/xiaohongshu/note-helpers.js +23 -0
- package/dist/clis/xiaohongshu/note.d.ts +7 -0
- package/dist/clis/xiaohongshu/note.js +76 -0
- package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/note.test.js +136 -0
- package/dist/clis/xiaohongshu/search.js +9 -0
- package/dist/clis/xiaohongshu/search.test.js +10 -4
- package/dist/clis/youtube/search.js +57 -17
- package/dist/clis/zhihu/question.js +19 -17
- package/dist/clis/zhihu/question.test.d.ts +1 -0
- package/dist/clis/zhihu/question.test.js +54 -0
- package/dist/commanderAdapter.js +9 -0
- package/dist/commanderAdapter.test.js +25 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +124 -0
- package/dist/commands/daemon.test.d.ts +1 -0
- package/dist/commands/daemon.test.js +185 -0
- package/dist/completion.js +3 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +25 -14
- package/dist/daemon.test.d.ts +1 -0
- package/dist/daemon.test.js +65 -0
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +47 -2
- package/dist/electron-apps.d.ts +29 -0
- package/dist/electron-apps.js +65 -0
- package/dist/electron-apps.test.d.ts +1 -0
- package/dist/electron-apps.test.js +43 -0
- package/dist/engine.test.js +41 -9
- package/dist/execution.js +20 -16
- package/dist/idle-manager.d.ts +19 -0
- package/dist/idle-manager.js +54 -0
- package/dist/launcher.d.ts +36 -0
- package/dist/launcher.js +152 -0
- package/dist/launcher.test.d.ts +1 -0
- package/dist/launcher.test.js +57 -0
- package/dist/main.js +3 -3
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +31 -3
- package/dist/registry.test.js +13 -0
- package/dist/runtime.d.ts +5 -3
- package/dist/runtime.js +12 -5
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +3 -0
- package/dist/serialization.test.js +17 -1
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +52 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +19 -0
- package/dist/weixin-download.test.js +14 -0
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/notebooklm.md +69 -0
- package/docs/adapters/browser/xiaohongshu.md +19 -10
- package/docs/adapters/index.md +67 -66
- package/docs/guide/browser-bridge.md +12 -0
- package/docs/guide/troubleshooting.md +9 -4
- package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
- package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
- package/docs/zh/guide/browser-bridge.md +12 -0
- package/extension/dist/background.js +794 -513
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +174 -10
- package/extension/src/cdp.ts +12 -0
- package/extension/src/protocol.ts +7 -5
- package/package.json +1 -1
- package/src/browser/cdp.ts +24 -17
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.test.ts +15 -1
- package/src/browser/dom-helpers.ts +1 -0
- package/src/browser/mcp.ts +18 -13
- package/src/browser/page.test.ts +58 -0
- package/src/browser/page.ts +18 -2
- package/src/browser/stealth.test.ts +153 -0
- package/src/browser/stealth.ts +198 -0
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.test.ts +2 -0
- package/src/build-manifest.ts +6 -1
- package/src/cli.ts +21 -3
- package/src/clis/antigravity/SKILL.md +3 -12
- package/src/clis/antigravity/serve.ts +5 -10
- package/src/clis/bilibili/subtitle.test.ts +60 -0
- package/src/clis/bilibili/subtitle.ts +4 -0
- package/src/clis/chatwise/ask.ts +0 -2
- package/src/clis/chatwise/export.ts +0 -2
- package/src/clis/chatwise/history.ts +0 -2
- package/src/clis/chatwise/model.ts +0 -2
- package/src/clis/chatwise/new.ts +1 -2
- package/src/clis/chatwise/read.ts +0 -2
- package/src/clis/chatwise/screenshot.ts +1 -2
- package/src/clis/chatwise/send.ts +0 -2
- package/src/clis/chatwise/status.ts +1 -2
- package/src/clis/ctrip/search.test.ts +73 -0
- package/src/clis/ctrip/search.ts +97 -47
- package/src/clis/douyin/_shared/sts2.test.ts +31 -0
- package/src/clis/douyin/_shared/sts2.ts +11 -3
- package/src/clis/douyin/activities.test.ts +41 -1
- package/src/clis/douyin/activities.ts +12 -3
- package/src/clis/douyin/collections.test.ts +35 -2
- package/src/clis/douyin/collections.ts +1 -1
- package/src/clis/douyin/draft.test.ts +444 -2
- package/src/clis/douyin/draft.ts +382 -218
- package/src/clis/douyin/hashtag.test.ts +42 -2
- package/src/clis/douyin/hashtag.ts +11 -3
- package/src/clis/douyin/profile.test.ts +43 -1
- package/src/clis/douyin/profile.ts +9 -2
- package/src/clis/douyin/videos.test.ts +52 -2
- package/src/clis/douyin/videos.ts +49 -15
- package/src/clis/facebook/search.test.ts +70 -0
- package/src/clis/facebook/search.yaml +4 -3
- package/src/clis/instagram/download.test.ts +159 -0
- package/src/clis/instagram/download.ts +286 -0
- package/src/clis/notebooklm/bind-current.test.ts +43 -0
- package/src/clis/notebooklm/bind-current.ts +36 -0
- package/src/clis/notebooklm/binding.test.ts +53 -0
- package/src/clis/notebooklm/compat.test.ts +19 -0
- package/src/clis/notebooklm/current.ts +38 -0
- package/src/clis/notebooklm/get.ts +53 -0
- package/src/clis/notebooklm/history.test.ts +70 -0
- package/src/clis/notebooklm/history.ts +36 -0
- package/src/clis/notebooklm/list.ts +40 -0
- package/src/clis/notebooklm/note-list.test.ts +64 -0
- package/src/clis/notebooklm/note-list.ts +42 -0
- package/src/clis/notebooklm/notes-get.test.ts +88 -0
- package/src/clis/notebooklm/notes-get.ts +67 -0
- package/src/clis/notebooklm/rpc.test.ts +126 -0
- package/src/clis/notebooklm/rpc.ts +286 -0
- package/src/clis/notebooklm/shared.ts +98 -0
- package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
- package/src/clis/notebooklm/source-fulltext.ts +69 -0
- package/src/clis/notebooklm/source-get.test.ts +100 -0
- package/src/clis/notebooklm/source-get.ts +60 -0
- package/src/clis/notebooklm/source-guide.test.ts +121 -0
- package/src/clis/notebooklm/source-guide.ts +69 -0
- package/src/clis/notebooklm/source-list.ts +45 -0
- package/src/clis/notebooklm/status.ts +34 -0
- package/src/clis/notebooklm/summary.test.ts +94 -0
- package/src/clis/notebooklm/summary.ts +45 -0
- package/src/clis/notebooklm/utils.test.ts +446 -0
- package/src/clis/notebooklm/utils.ts +893 -0
- package/src/clis/substack/utils.test.ts +54 -0
- package/src/clis/substack/utils.ts +10 -2
- package/src/clis/v2ex/hot.yaml +4 -1
- package/src/clis/v2ex/latest.yaml +4 -1
- package/src/clis/v2ex/topic.yaml +6 -1
- package/src/clis/weixin/download.ts +95 -6
- package/src/clis/weread/book.ts +142 -2
- package/src/clis/weread/commands.test.ts +314 -154
- package/src/clis/weread/utils.ts +33 -4
- package/src/clis/xiaohongshu/comments.test.ts +85 -9
- package/src/clis/xiaohongshu/comments.ts +76 -17
- package/src/clis/xiaohongshu/download.test.ts +96 -0
- package/src/clis/xiaohongshu/download.ts +83 -22
- package/src/clis/xiaohongshu/note-helpers.ts +25 -0
- package/src/clis/xiaohongshu/note.test.ts +164 -0
- package/src/clis/xiaohongshu/note.ts +86 -0
- package/src/clis/xiaohongshu/search.test.ts +11 -4
- package/src/clis/xiaohongshu/search.ts +13 -0
- package/src/clis/youtube/search.ts +57 -17
- package/src/clis/zhihu/question.test.ts +71 -0
- package/src/clis/zhihu/question.ts +27 -15
- package/src/commanderAdapter.test.ts +30 -0
- package/src/commanderAdapter.ts +7 -0
- package/src/commands/daemon.test.ts +238 -0
- package/src/commands/daemon.ts +135 -0
- package/src/completion.ts +2 -1
- package/src/constants.ts +3 -0
- package/src/daemon.test.ts +88 -0
- package/src/daemon.ts +26 -14
- package/src/discovery.ts +52 -2
- package/src/electron-apps.test.ts +50 -0
- package/src/electron-apps.ts +89 -0
- package/src/engine.test.ts +45 -9
- package/src/execution.ts +24 -19
- package/src/idle-manager.ts +60 -0
- package/src/launcher.test.ts +67 -0
- package/src/launcher.ts +185 -0
- package/src/main.ts +3 -2
- package/src/registry.test.ts +15 -0
- package/src/registry.ts +32 -3
- package/src/runtime.ts +13 -7
- package/src/serialization.test.ts +19 -1
- package/src/serialization.ts +2 -0
- package/src/tui.test.ts +23 -0
- package/src/tui.ts +65 -0
- package/src/weixin-download.test.ts +27 -0
- package/tests/e2e/browser-public-extended.test.ts +6 -2
- package/chatwise-opencli.ps1 +0 -82
- package/dist/clis/chatwise/shared.d.ts +0 -2
- package/dist/clis/chatwise/shared.js +0 -6
- package/src/clis/chatwise/shared.ts +0 -8
|
@@ -17,12 +17,13 @@ args:
|
|
|
17
17
|
pipeline:
|
|
18
18
|
- navigate: https://www.facebook.com
|
|
19
19
|
|
|
20
|
+
- navigate:
|
|
21
|
+
url: https://www.facebook.com/search/top?q=${{ args.query | urlencode }}
|
|
22
|
+
settleMs: 4000
|
|
23
|
+
|
|
20
24
|
- evaluate: |
|
|
21
25
|
(async () => {
|
|
22
|
-
const query = ${{ args.query | json }};
|
|
23
26
|
const limit = ${{ args.limit }};
|
|
24
|
-
window.location.href = 'https://www.facebook.com/search/top?q=' + encodeURIComponent(query);
|
|
25
|
-
await new Promise(r => setTimeout(r, 4000));
|
|
26
27
|
// Search results are typically in role="article" or role="listitem"
|
|
27
28
|
let items = document.querySelectorAll('[role="article"]');
|
|
28
29
|
if (items.length === 0) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface InstagramMediaTarget {
|
|
2
|
+
kind: 'p' | 'reel' | 'tv';
|
|
3
|
+
shortcode: string;
|
|
4
|
+
canonicalUrl: string;
|
|
5
|
+
}
|
|
6
|
+
interface InstagramPageMediaItem {
|
|
7
|
+
type: 'image' | 'video';
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
interface DownloadedMediaItem extends InstagramPageMediaItem {
|
|
11
|
+
filename: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseInstagramMediaTarget(input: string): InstagramMediaTarget;
|
|
14
|
+
export declare function buildInstagramDownloadItems(shortcode: string, items: InstagramPageMediaItem[]): DownloadedMediaItem[];
|
|
15
|
+
export declare function buildInstagramFetchScript(shortcode: string): string;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { cli, Strategy } from '../../registry.js';
|
|
5
|
+
import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError, EXIT_CODES } from '../../errors.js';
|
|
6
|
+
import { httpDownload } from '../../download/index.js';
|
|
7
|
+
const INSTAGRAM_GRAPHQL_DOC_ID = '8845758582119845';
|
|
8
|
+
const INSTAGRAM_GRAPHQL_APP_ID = '936619743392459';
|
|
9
|
+
const INSTAGRAM_HOST_SUFFIX = 'instagram.com';
|
|
10
|
+
const SUPPORTED_KINDS = new Set(['p', 'reel', 'tv']);
|
|
11
|
+
function displayPath(filePath) {
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
14
|
+
}
|
|
15
|
+
export function parseInstagramMediaTarget(input) {
|
|
16
|
+
const raw = String(input || '').trim();
|
|
17
|
+
if (!raw) {
|
|
18
|
+
throw new ArgumentError('Instagram URL is required', 'Expected https://www.instagram.com/p/... or https://www.instagram.com/reel/...');
|
|
19
|
+
}
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new ArgumentError(`Invalid Instagram URL: ${raw}`, 'Expected https://www.instagram.com/p/<shortcode>/ or /reel/<shortcode>/');
|
|
26
|
+
}
|
|
27
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
28
|
+
throw new ArgumentError(`Unsupported URL protocol: ${url.protocol}`);
|
|
29
|
+
}
|
|
30
|
+
const host = url.hostname.toLowerCase();
|
|
31
|
+
if (host !== INSTAGRAM_HOST_SUFFIX && !host.endsWith(`.${INSTAGRAM_HOST_SUFFIX}`)) {
|
|
32
|
+
throw new ArgumentError(`Unsupported host: ${host}`, 'Only instagram.com URLs are supported');
|
|
33
|
+
}
|
|
34
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
35
|
+
let kind;
|
|
36
|
+
let shortcode;
|
|
37
|
+
if (segments.length >= 2 && SUPPORTED_KINDS.has(segments[0])) {
|
|
38
|
+
kind = segments[0];
|
|
39
|
+
shortcode = segments[1];
|
|
40
|
+
}
|
|
41
|
+
else if (segments.length >= 3 && SUPPORTED_KINDS.has(segments[1])) {
|
|
42
|
+
kind = segments[1];
|
|
43
|
+
shortcode = segments[2];
|
|
44
|
+
}
|
|
45
|
+
if (!kind || !shortcode) {
|
|
46
|
+
throw new ArgumentError(`Unsupported Instagram media URL: ${raw}`, 'Only /p/<shortcode>/, /reel/<shortcode>/, and /tv/<shortcode>/ links are supported');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: kind,
|
|
50
|
+
shortcode,
|
|
51
|
+
canonicalUrl: `https://www.instagram.com/${kind}/${shortcode}/`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function buildInstagramDownloadItems(shortcode, items) {
|
|
55
|
+
return items
|
|
56
|
+
.filter((item) => item?.url)
|
|
57
|
+
.map((item, index) => {
|
|
58
|
+
const fallbackExt = item.type === 'video' ? '.mp4' : '.jpg';
|
|
59
|
+
let ext = fallbackExt;
|
|
60
|
+
try {
|
|
61
|
+
const pathname = new URL(item.url).pathname;
|
|
62
|
+
const candidateExt = path.extname(pathname).toLowerCase();
|
|
63
|
+
if (candidateExt && candidateExt.length <= 8)
|
|
64
|
+
ext = candidateExt;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
ext = fallbackExt;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
type: item.type,
|
|
71
|
+
url: item.url,
|
|
72
|
+
filename: `${shortcode}_${String(index + 1).padStart(2, '0')}${ext}`,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function buildInstagramFetchScript(shortcode) {
|
|
77
|
+
return `
|
|
78
|
+
(async () => {
|
|
79
|
+
const shortcode = ${JSON.stringify(shortcode)};
|
|
80
|
+
const docId = ${JSON.stringify(INSTAGRAM_GRAPHQL_DOC_ID)};
|
|
81
|
+
const variables = {
|
|
82
|
+
shortcode,
|
|
83
|
+
fetch_tagged_user_count: null,
|
|
84
|
+
hoisted_comment_id: null,
|
|
85
|
+
hoisted_reply_id: null,
|
|
86
|
+
};
|
|
87
|
+
const url = 'https://www.instagram.com/graphql/query/?doc_id=' + docId + '&variables=' + encodeURIComponent(JSON.stringify(variables));
|
|
88
|
+
const res = await fetch(url, {
|
|
89
|
+
credentials: 'include',
|
|
90
|
+
headers: {
|
|
91
|
+
'Accept': 'application/json,text/plain,*/*',
|
|
92
|
+
'X-IG-App-ID': ${JSON.stringify(INSTAGRAM_GRAPHQL_APP_ID)},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const rawText = await res.text();
|
|
96
|
+
|
|
97
|
+
let data = null;
|
|
98
|
+
try {
|
|
99
|
+
data = rawText ? JSON.parse(rawText) : null;
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
errorCode: 'COMMAND_EXEC',
|
|
104
|
+
error: 'Instagram returned non-JSON content while fetching media metadata',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const message = typeof data?.message === 'string' ? data.message : '';
|
|
109
|
+
const lowered = (message || '').toLowerCase();
|
|
110
|
+
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
if (res.status === 401 || res.status === 403 || data?.require_login) {
|
|
113
|
+
return { ok: false, errorCode: 'AUTH_REQUIRED', error: message || ('HTTP ' + res.status) };
|
|
114
|
+
}
|
|
115
|
+
if (res.status === 429) {
|
|
116
|
+
return { ok: false, errorCode: 'RATE_LIMITED', error: message || 'HTTP 429' };
|
|
117
|
+
}
|
|
118
|
+
if (res.status === 404 || res.status === 410) {
|
|
119
|
+
return { ok: false, errorCode: 'PRIVATE_OR_UNAVAILABLE', error: message || ('HTTP ' + res.status) };
|
|
120
|
+
}
|
|
121
|
+
return { ok: false, errorCode: 'COMMAND_EXEC', error: message || ('HTTP ' + res.status) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (data?.require_login) {
|
|
125
|
+
return { ok: false, errorCode: 'AUTH_REQUIRED', error: message || 'Instagram login required' };
|
|
126
|
+
}
|
|
127
|
+
if (lowered.includes('wait a few minutes') || lowered.includes('rate')) {
|
|
128
|
+
return { ok: false, errorCode: 'RATE_LIMITED', error: message || 'Instagram rate limit triggered' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const media = data?.data?.xdt_shortcode_media;
|
|
132
|
+
if (!media) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
errorCode: 'PRIVATE_OR_UNAVAILABLE',
|
|
136
|
+
error: message || 'Post may be private, unavailable, or inaccessible to the current browser session',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nodes = Array.isArray(media?.edge_sidecar_to_children?.edges) && media.edge_sidecar_to_children.edges.length > 0
|
|
141
|
+
? media.edge_sidecar_to_children.edges.map((edge) => edge?.node).filter(Boolean)
|
|
142
|
+
: [media];
|
|
143
|
+
|
|
144
|
+
const items = nodes
|
|
145
|
+
.map((node) => ({
|
|
146
|
+
type: node?.is_video ? 'video' : 'image',
|
|
147
|
+
url: String(node?.is_video ? (node?.video_url || '') : (node?.display_url || '')),
|
|
148
|
+
}))
|
|
149
|
+
.filter((item) => item.url);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
shortcode: media.shortcode || shortcode,
|
|
154
|
+
owner: media?.owner?.username || '',
|
|
155
|
+
items,
|
|
156
|
+
};
|
|
157
|
+
})()
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
function ensurePage(page) {
|
|
161
|
+
if (!page)
|
|
162
|
+
throw new CommandExecutionError('Browser session required');
|
|
163
|
+
return page;
|
|
164
|
+
}
|
|
165
|
+
function normalizeFetchResult(result) {
|
|
166
|
+
if (!result || typeof result !== 'object') {
|
|
167
|
+
throw new CommandExecutionError('Failed to fetch Instagram media metadata');
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
function handleFetchFailure(result) {
|
|
172
|
+
const message = result.error || 'Instagram media fetch failed';
|
|
173
|
+
if (result.errorCode === 'AUTH_REQUIRED') {
|
|
174
|
+
throw new AuthRequiredError('instagram.com', message);
|
|
175
|
+
}
|
|
176
|
+
if (result.errorCode === 'RATE_LIMITED') {
|
|
177
|
+
throw new CliError('RATE_LIMITED', message, 'Wait a few minutes and retry, or switch to a browser session with a warmer Instagram login state.', EXIT_CODES.TEMPFAIL);
|
|
178
|
+
}
|
|
179
|
+
if (result.errorCode === 'PRIVATE_OR_UNAVAILABLE') {
|
|
180
|
+
throw new CommandExecutionError(message, 'Open the post in a logged-in browser session and retry');
|
|
181
|
+
}
|
|
182
|
+
throw new CommandExecutionError(message);
|
|
183
|
+
}
|
|
184
|
+
async function downloadInstagramMedia(items, outputDir) {
|
|
185
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
186
|
+
for (const item of items) {
|
|
187
|
+
const destPath = path.join(outputDir, item.filename);
|
|
188
|
+
const result = await httpDownload(item.url, destPath, {
|
|
189
|
+
timeout: item.type === 'video' ? 120000 : 60000,
|
|
190
|
+
});
|
|
191
|
+
if (!result.success) {
|
|
192
|
+
throw new CommandExecutionError(`Failed to download ${item.filename}: ${result.error || 'unknown error'}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
cli({
|
|
197
|
+
site: 'instagram',
|
|
198
|
+
name: 'download',
|
|
199
|
+
description: 'Download images and videos from Instagram posts and reels',
|
|
200
|
+
domain: 'www.instagram.com',
|
|
201
|
+
strategy: Strategy.COOKIE,
|
|
202
|
+
navigateBefore: false,
|
|
203
|
+
args: [
|
|
204
|
+
{ name: 'url', positional: true, required: true, help: 'Instagram post / reel / tv URL' },
|
|
205
|
+
{ name: 'path', default: path.join(os.homedir(), 'Downloads', 'Instagram'), help: 'Download directory' },
|
|
206
|
+
],
|
|
207
|
+
func: async (page, kwargs) => {
|
|
208
|
+
const browserPage = ensurePage(page);
|
|
209
|
+
const target = parseInstagramMediaTarget(String(kwargs.url ?? ''));
|
|
210
|
+
const outputRoot = String(kwargs.path ?? path.join(os.homedir(), 'Downloads', 'Instagram'));
|
|
211
|
+
await browserPage.goto(target.canonicalUrl);
|
|
212
|
+
const fetchResult = normalizeFetchResult(await browserPage.evaluate(buildInstagramFetchScript(target.shortcode)));
|
|
213
|
+
if (!fetchResult.ok)
|
|
214
|
+
handleFetchFailure(fetchResult);
|
|
215
|
+
const shortcode = fetchResult.shortcode || target.shortcode;
|
|
216
|
+
const mediaItems = buildInstagramDownloadItems(shortcode, fetchResult.items || []);
|
|
217
|
+
if (mediaItems.length === 0) {
|
|
218
|
+
throw new CommandExecutionError('No downloadable media found');
|
|
219
|
+
}
|
|
220
|
+
const savedDir = path.join(outputRoot, shortcode);
|
|
221
|
+
await downloadInstagramMedia(mediaItems, savedDir);
|
|
222
|
+
console.log(`📁 saved: ${displayPath(savedDir)}`);
|
|
223
|
+
return null;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { getRegistry } from '../../registry.js';
|
|
4
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '../../errors.js';
|
|
5
|
+
const { mockHttpDownload, logSpy } = vi.hoisted(() => ({
|
|
6
|
+
mockHttpDownload: vi.fn(),
|
|
7
|
+
logSpy: vi.spyOn(console, 'log').mockImplementation(() => undefined),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../../download/index.js', async () => {
|
|
10
|
+
const actual = await vi.importActual('../../download/index.js');
|
|
11
|
+
return { ...actual, httpDownload: mockHttpDownload };
|
|
12
|
+
});
|
|
13
|
+
const { buildInstagramDownloadItems, parseInstagramMediaTarget, } = await import('./download.js');
|
|
14
|
+
let cmd;
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
cmd = getRegistry().get('instagram/download');
|
|
17
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
18
|
+
});
|
|
19
|
+
function createPageMock(evaluateResult) {
|
|
20
|
+
return {
|
|
21
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
describe('instagram download helpers', () => {
|
|
26
|
+
it('parses canonical and username-prefixed Instagram media URLs', () => {
|
|
27
|
+
expect(parseInstagramMediaTarget('https://www.instagram.com/reel/DWg8NuZEj9p/?utm_source=ig_web_copy_link')).toEqual({
|
|
28
|
+
kind: 'reel',
|
|
29
|
+
shortcode: 'DWg8NuZEj9p',
|
|
30
|
+
canonicalUrl: 'https://www.instagram.com/reel/DWg8NuZEj9p/',
|
|
31
|
+
});
|
|
32
|
+
expect(parseInstagramMediaTarget('https://www.instagram.com/nasa/p/DWUR_azCWbN/?img_index=1')).toEqual({
|
|
33
|
+
kind: 'p',
|
|
34
|
+
shortcode: 'DWUR_azCWbN',
|
|
35
|
+
canonicalUrl: 'https://www.instagram.com/p/DWUR_azCWbN/',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('rejects unsupported URLs early', () => {
|
|
39
|
+
expect(() => parseInstagramMediaTarget('https://example.com/p/abc')).toThrow(ArgumentError);
|
|
40
|
+
expect(() => parseInstagramMediaTarget('https://www.instagram.com/stories/abc/123')).toThrow(ArgumentError);
|
|
41
|
+
});
|
|
42
|
+
it('builds padded filenames and preserves known file extensions', () => {
|
|
43
|
+
expect(buildInstagramDownloadItems('DWUR_azCWbN', [
|
|
44
|
+
{ type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' },
|
|
45
|
+
{ type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2' },
|
|
46
|
+
{ type: 'image', url: 'not-a-valid-url' },
|
|
47
|
+
])).toEqual([
|
|
48
|
+
{ type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1', filename: 'DWUR_azCWbN_01.webp' },
|
|
49
|
+
{ type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2', filename: 'DWUR_azCWbN_02.mp4' },
|
|
50
|
+
{ type: 'image', url: 'not-a-valid-url', filename: 'DWUR_azCWbN_03.jpg' },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('instagram download command', () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
mockHttpDownload.mockReset();
|
|
57
|
+
logSpy.mockClear();
|
|
58
|
+
});
|
|
59
|
+
it('rejects invalid URLs before browser work', async () => {
|
|
60
|
+
const page = createPageMock({ ok: true, items: [] });
|
|
61
|
+
await expect(cmd.func(page, { url: 'https://example.com/not-instagram' })).rejects.toThrow(ArgumentError);
|
|
62
|
+
expect(page.goto.mock.calls).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
it('maps auth failures to AuthRequiredError', async () => {
|
|
65
|
+
const page = createPageMock({ ok: false, errorCode: 'AUTH_REQUIRED', error: 'Instagram login required' });
|
|
66
|
+
await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(AuthRequiredError);
|
|
67
|
+
expect(mockHttpDownload).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
it('maps rate limit failures to CliError with RATE_LIMITED code', async () => {
|
|
70
|
+
const page = createPageMock({ ok: false, errorCode: 'RATE_LIMITED', error: 'Please wait a few minutes' });
|
|
71
|
+
await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toMatchObject({ code: 'RATE_LIMITED' });
|
|
72
|
+
expect(mockHttpDownload).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
it('maps private/unavailable failures to CommandExecutionError', async () => {
|
|
75
|
+
const page = createPageMock({ ok: false, errorCode: 'PRIVATE_OR_UNAVAILABLE', error: 'Post may be private' });
|
|
76
|
+
await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(CommandExecutionError);
|
|
77
|
+
expect(mockHttpDownload).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it('throws when no downloadable media is found', async () => {
|
|
80
|
+
const page = createPageMock({ ok: true, shortcode: 'DWUR_azCWbN', items: [] });
|
|
81
|
+
await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(CommandExecutionError);
|
|
82
|
+
expect(mockHttpDownload).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
it('downloads media and prints saved directory', async () => {
|
|
85
|
+
mockHttpDownload
|
|
86
|
+
.mockResolvedValueOnce({ success: true, size: 120_000 })
|
|
87
|
+
.mockResolvedValueOnce({ success: true, size: 8_200_000 });
|
|
88
|
+
const page = createPageMock({
|
|
89
|
+
ok: true,
|
|
90
|
+
shortcode: 'DWUR_azCWbN',
|
|
91
|
+
items: [
|
|
92
|
+
{ type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' },
|
|
93
|
+
{ type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2' },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
const result = await cmd.func(page, {
|
|
97
|
+
url: 'https://www.instagram.com/nasa/p/DWUR_azCWbN/?img_index=1',
|
|
98
|
+
path: './instagram-test',
|
|
99
|
+
});
|
|
100
|
+
expect(result).toBeNull();
|
|
101
|
+
expect(page.goto.mock.calls[0]?.[0]).toBe('https://www.instagram.com/p/DWUR_azCWbN/');
|
|
102
|
+
expect(mockHttpDownload).toHaveBeenNthCalledWith(1, 'https://cdn.example.com/photo.webp?foo=1', expect.stringContaining('instagram-test/DWUR_azCWbN/DWUR_azCWbN_01.webp'), expect.objectContaining({ timeout: 60000 }));
|
|
103
|
+
expect(mockHttpDownload).toHaveBeenNthCalledWith(2, 'https://cdn.example.com/video.mp4?bar=2', expect.stringContaining('instagram-test/DWUR_azCWbN/DWUR_azCWbN_02.mp4'), expect.objectContaining({ timeout: 120000 }));
|
|
104
|
+
expect(logSpy).toHaveBeenCalledWith('📁 saved: instagram-test/DWUR_azCWbN');
|
|
105
|
+
});
|
|
106
|
+
it('uses a cross-platform Downloads default when path is omitted', async () => {
|
|
107
|
+
mockHttpDownload.mockResolvedValueOnce({ success: true, size: 120_000 });
|
|
108
|
+
const page = createPageMock({
|
|
109
|
+
ok: true,
|
|
110
|
+
shortcode: 'DWUR_azCWbN',
|
|
111
|
+
items: [
|
|
112
|
+
{ type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
await cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' });
|
|
116
|
+
expect(mockHttpDownload).toHaveBeenCalledWith('https://cdn.example.com/photo.webp?foo=1', expect.stringContaining(`${os.homedir()}/Downloads/Instagram/DWUR_azCWbN/DWUR_azCWbN_01.webp`), expect.objectContaining({ timeout: 60000 }));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { bindCurrentTab } from '../../browser/daemon-client.js';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { parseNotebooklmIdFromUrl } from './utils.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: NOTEBOOKLM_SITE,
|
|
7
|
+
name: 'bind-current',
|
|
8
|
+
aliases: ['use'],
|
|
9
|
+
description: 'Bind the current active NotebookLM notebook tab into the site:notebooklm workspace',
|
|
10
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
args: [],
|
|
15
|
+
columns: ['workspace', 'tab_id', 'notebook_id', 'title', 'url'],
|
|
16
|
+
func: async () => {
|
|
17
|
+
const result = await bindCurrentTab(`site:${NOTEBOOKLM_SITE}`, {
|
|
18
|
+
matchDomain: NOTEBOOKLM_DOMAIN,
|
|
19
|
+
matchPathPrefix: '/notebook/',
|
|
20
|
+
});
|
|
21
|
+
return [{
|
|
22
|
+
workspace: result.workspace ?? `site:${NOTEBOOKLM_SITE}`,
|
|
23
|
+
tab_id: result.tabId ?? null,
|
|
24
|
+
notebook_id: result.url ? parseNotebooklmIdFromUrl(result.url) : '',
|
|
25
|
+
title: result.title ?? '',
|
|
26
|
+
url: result.url ?? '',
|
|
27
|
+
}];
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './bind-current.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockBindCurrentTab } = vi.hoisted(() => ({
|
|
3
|
+
mockBindCurrentTab: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('../../browser/daemon-client.js', () => ({
|
|
6
|
+
bindCurrentTab: mockBindCurrentTab,
|
|
7
|
+
}));
|
|
8
|
+
import { getRegistry } from '../../registry.js';
|
|
9
|
+
import './bind-current.js';
|
|
10
|
+
describe('notebooklm bind-current', () => {
|
|
11
|
+
const command = getRegistry().get('notebooklm/bind-current');
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockBindCurrentTab.mockReset();
|
|
14
|
+
});
|
|
15
|
+
it('binds the current notebook tab into site:notebooklm', async () => {
|
|
16
|
+
mockBindCurrentTab.mockResolvedValue({
|
|
17
|
+
workspace: 'site:notebooklm',
|
|
18
|
+
tabId: 123,
|
|
19
|
+
title: 'Bound Notebook',
|
|
20
|
+
url: 'https://notebooklm.google.com/notebook/nb-live',
|
|
21
|
+
});
|
|
22
|
+
const result = await command.func({}, {});
|
|
23
|
+
expect(mockBindCurrentTab).toHaveBeenCalledWith('site:notebooklm', {
|
|
24
|
+
matchDomain: 'notebooklm.google.com',
|
|
25
|
+
matchPathPrefix: '/notebook/',
|
|
26
|
+
});
|
|
27
|
+
expect(result).toEqual([{
|
|
28
|
+
workspace: 'site:notebooklm',
|
|
29
|
+
tab_id: 123,
|
|
30
|
+
notebook_id: 'nb-live',
|
|
31
|
+
title: 'Bound Notebook',
|
|
32
|
+
url: 'https://notebooklm.google.com/notebook/nb-live',
|
|
33
|
+
}]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockBindCurrentTab } = vi.hoisted(() => ({
|
|
3
|
+
mockBindCurrentTab: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('../../browser/daemon-client.js', () => ({
|
|
6
|
+
bindCurrentTab: mockBindCurrentTab,
|
|
7
|
+
}));
|
|
8
|
+
import { ensureNotebooklmNotebookBinding } from './utils.js';
|
|
9
|
+
describe('notebooklm automatic binding', () => {
|
|
10
|
+
const originalEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockBindCurrentTab.mockReset();
|
|
13
|
+
if (originalEndpoint === undefined)
|
|
14
|
+
delete process.env.OPENCLI_CDP_ENDPOINT;
|
|
15
|
+
else
|
|
16
|
+
process.env.OPENCLI_CDP_ENDPOINT = originalEndpoint;
|
|
17
|
+
});
|
|
18
|
+
it('does nothing when the current page is already a notebook page', async () => {
|
|
19
|
+
const page = {
|
|
20
|
+
getCurrentUrl: async () => 'https://notebooklm.google.com/notebook/nb-demo',
|
|
21
|
+
};
|
|
22
|
+
await expect(ensureNotebooklmNotebookBinding(page)).resolves.toBe(false);
|
|
23
|
+
expect(mockBindCurrentTab).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
it('best-effort binds a notebook page through the browser bridge when currently on home', async () => {
|
|
26
|
+
const page = {
|
|
27
|
+
getCurrentUrl: async () => 'https://notebooklm.google.com/',
|
|
28
|
+
};
|
|
29
|
+
mockBindCurrentTab.mockResolvedValue({});
|
|
30
|
+
await expect(ensureNotebooklmNotebookBinding(page)).resolves.toBe(true);
|
|
31
|
+
expect(mockBindCurrentTab).toHaveBeenCalledWith('site:notebooklm', {
|
|
32
|
+
matchDomain: 'notebooklm.google.com',
|
|
33
|
+
matchPathPrefix: '/notebook/',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('skips daemon binding in direct CDP mode', async () => {
|
|
37
|
+
process.env.OPENCLI_CDP_ENDPOINT = 'ws://127.0.0.1:9222/devtools/page/1';
|
|
38
|
+
const page = {
|
|
39
|
+
getCurrentUrl: async () => 'https://notebooklm.google.com/',
|
|
40
|
+
};
|
|
41
|
+
await expect(ensureNotebooklmNotebookBinding(page)).resolves.toBe(false);
|
|
42
|
+
expect(mockBindCurrentTab).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import './bind-current.js';
|
|
4
|
+
import './get.js';
|
|
5
|
+
import './note-list.js';
|
|
6
|
+
describe('notebooklm compatibility aliases', () => {
|
|
7
|
+
it('registers use as a compatibility alias for bind-current', () => {
|
|
8
|
+
expect(getRegistry().get('notebooklm/use')).toBe(getRegistry().get('notebooklm/bind-current'));
|
|
9
|
+
});
|
|
10
|
+
it('registers metadata as a compatibility alias for get', () => {
|
|
11
|
+
expect(getRegistry().get('notebooklm/metadata')).toBe(getRegistry().get('notebooklm/get'));
|
|
12
|
+
});
|
|
13
|
+
it('registers notes-list as a compatibility alias for note-list', () => {
|
|
14
|
+
expect(getRegistry().get('notebooklm/notes-list')).toBe(getRegistry().get('notebooklm/note-list'));
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { EmptyResultError } from '../../errors.js';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { ensureNotebooklmNotebookBinding, getNotebooklmPageState, readCurrentNotebooklm, requireNotebooklmSession } from './utils.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: NOTEBOOKLM_SITE,
|
|
7
|
+
name: 'current',
|
|
8
|
+
description: 'Show metadata for the currently opened NotebookLM notebook tab',
|
|
9
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [],
|
|
14
|
+
columns: ['id', 'title', 'url', 'source'],
|
|
15
|
+
func: async (page) => {
|
|
16
|
+
await ensureNotebooklmNotebookBinding(page);
|
|
17
|
+
await requireNotebooklmSession(page);
|
|
18
|
+
const state = await getNotebooklmPageState(page);
|
|
19
|
+
if (state.kind !== 'notebook') {
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm current', 'Open a specific NotebookLM notebook tab first, then retry.');
|
|
21
|
+
}
|
|
22
|
+
const current = await readCurrentNotebooklm(page);
|
|
23
|
+
if (!current) {
|
|
24
|
+
throw new EmptyResultError('opencli notebooklm current', 'NotebookLM notebook metadata was not found on the current page.');
|
|
25
|
+
}
|
|
26
|
+
return [current];
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { EmptyResultError } from '../../errors.js';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { ensureNotebooklmNotebookBinding, getNotebooklmDetailViaRpc, getNotebooklmPageState, readCurrentNotebooklm, requireNotebooklmSession, } from './utils.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: NOTEBOOKLM_SITE,
|
|
7
|
+
name: 'get',
|
|
8
|
+
aliases: ['metadata'],
|
|
9
|
+
description: 'Get rich metadata for the currently opened NotebookLM notebook',
|
|
10
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
args: [],
|
|
15
|
+
columns: ['id', 'title', 'emoji', 'source_count', 'created_at', 'updated_at', 'url', 'source'],
|
|
16
|
+
func: async (page) => {
|
|
17
|
+
await ensureNotebooklmNotebookBinding(page);
|
|
18
|
+
await requireNotebooklmSession(page);
|
|
19
|
+
const state = await getNotebooklmPageState(page);
|
|
20
|
+
if (state.kind !== 'notebook') {
|
|
21
|
+
throw new EmptyResultError('opencli notebooklm get', 'Open a specific NotebookLM notebook tab first, then retry.');
|
|
22
|
+
}
|
|
23
|
+
const rpcRow = await getNotebooklmDetailViaRpc(page).catch(() => null);
|
|
24
|
+
if (rpcRow)
|
|
25
|
+
return [rpcRow];
|
|
26
|
+
const current = await readCurrentNotebooklm(page);
|
|
27
|
+
if (!current) {
|
|
28
|
+
throw new EmptyResultError('opencli notebooklm get', 'NotebookLM notebook metadata was not found on the current page.');
|
|
29
|
+
}
|
|
30
|
+
return [{
|
|
31
|
+
...current,
|
|
32
|
+
emoji: null,
|
|
33
|
+
source_count: null,
|
|
34
|
+
updated_at: null,
|
|
35
|
+
}];
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { EmptyResultError } from '../../errors.js';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { ensureNotebooklmNotebookBinding, getNotebooklmPageState, listNotebooklmHistoryViaRpc, requireNotebooklmSession, } from './utils.js';
|
|
5
|
+
cli({
|
|
6
|
+
site: NOTEBOOKLM_SITE,
|
|
7
|
+
name: 'history',
|
|
8
|
+
description: 'List NotebookLM conversation history threads in the current notebook',
|
|
9
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [],
|
|
14
|
+
columns: ['thread_id', 'item_count', 'preview', 'source', 'notebook_id', 'url'],
|
|
15
|
+
func: async (page) => {
|
|
16
|
+
await ensureNotebooklmNotebookBinding(page);
|
|
17
|
+
await requireNotebooklmSession(page);
|
|
18
|
+
const state = await getNotebooklmPageState(page);
|
|
19
|
+
if (state.kind !== 'notebook') {
|
|
20
|
+
throw new EmptyResultError('opencli notebooklm history', 'Open a specific NotebookLM notebook tab first, then retry.');
|
|
21
|
+
}
|
|
22
|
+
const rows = await listNotebooklmHistoryViaRpc(page);
|
|
23
|
+
return rows;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './history.js';
|