@jackwener/opencli 1.5.8 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +17 -1
  3. package/README.zh-CN.md +17 -1
  4. package/dist/browser/base-page.d.ts +48 -0
  5. package/dist/browser/base-page.js +160 -0
  6. package/dist/browser/cdp.js +4 -106
  7. package/dist/browser/daemon-client.d.ts +1 -7
  8. package/dist/browser/daemon-client.js +2 -9
  9. package/dist/browser/discover.d.ts +1 -4
  10. package/dist/browser/discover.js +1 -4
  11. package/dist/browser/errors.d.ts +4 -0
  12. package/dist/browser/errors.js +20 -0
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.js +1 -1
  15. package/dist/browser/page.d.ts +6 -35
  16. package/dist/browser/page.js +10 -189
  17. package/dist/browser/tabs.js +5 -5
  18. package/dist/browser.test.js +15 -15
  19. package/dist/cli-manifest.json +294 -22
  20. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  21. package/dist/clis/amazon/bestsellers.js +130 -0
  22. package/dist/clis/amazon/bestsellers.test.js +20 -0
  23. package/dist/clis/amazon/discussion.d.ts +20 -0
  24. package/dist/clis/amazon/discussion.js +91 -0
  25. package/dist/clis/amazon/discussion.test.js +36 -0
  26. package/dist/clis/amazon/offer.d.ts +23 -0
  27. package/dist/clis/amazon/offer.js +140 -0
  28. package/dist/clis/amazon/offer.test.d.ts +1 -0
  29. package/dist/clis/amazon/offer.test.js +29 -0
  30. package/dist/clis/amazon/product.d.ts +18 -0
  31. package/dist/clis/amazon/product.js +92 -0
  32. package/dist/clis/amazon/product.test.d.ts +1 -0
  33. package/dist/clis/amazon/product.test.js +24 -0
  34. package/dist/clis/amazon/search.d.ts +18 -0
  35. package/dist/clis/amazon/search.js +87 -0
  36. package/dist/clis/amazon/search.test.d.ts +1 -0
  37. package/dist/clis/amazon/search.test.js +22 -0
  38. package/dist/clis/amazon/shared.d.ts +64 -0
  39. package/dist/clis/amazon/shared.js +255 -0
  40. package/dist/clis/amazon/shared.test.d.ts +1 -0
  41. package/dist/clis/amazon/shared.test.js +33 -0
  42. package/dist/clis/gemini/ask.d.ts +1 -0
  43. package/dist/clis/gemini/ask.js +40 -0
  44. package/dist/clis/gemini/image.d.ts +1 -0
  45. package/dist/clis/gemini/image.js +105 -0
  46. package/dist/clis/gemini/new.d.ts +1 -0
  47. package/dist/clis/gemini/new.js +20 -0
  48. package/dist/clis/gemini/utils.d.ts +34 -0
  49. package/dist/clis/gemini/utils.js +463 -0
  50. package/dist/clis/gemini/utils.test.d.ts +1 -0
  51. package/dist/clis/gemini/utils.test.js +31 -0
  52. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  53. package/dist/clis/notebooklm/compat.test.js +3 -3
  54. package/dist/clis/notebooklm/current.js +2 -3
  55. package/dist/clis/notebooklm/get.js +2 -3
  56. package/dist/clis/notebooklm/history.js +2 -3
  57. package/dist/clis/notebooklm/note-list.js +2 -3
  58. package/dist/clis/notebooklm/notes-get.js +2 -3
  59. package/dist/clis/notebooklm/open.d.ts +1 -0
  60. package/dist/clis/notebooklm/open.js +41 -0
  61. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  62. package/dist/clis/notebooklm/open.test.js +63 -0
  63. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  64. package/dist/clis/notebooklm/source-get.js +2 -3
  65. package/dist/clis/notebooklm/source-guide.js +2 -3
  66. package/dist/clis/notebooklm/source-list.js +2 -3
  67. package/dist/clis/notebooklm/status.js +1 -2
  68. package/dist/clis/notebooklm/summary.js +2 -3
  69. package/dist/clis/notebooklm/utils.d.ts +2 -1
  70. package/dist/clis/notebooklm/utils.js +20 -21
  71. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  72. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  73. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  74. package/dist/commanderAdapter.js +6 -3
  75. package/dist/commanderAdapter.test.js +33 -0
  76. package/dist/commands/daemon.js +1 -1
  77. package/dist/commands/daemon.test.js +1 -1
  78. package/dist/doctor.d.ts +1 -2
  79. package/dist/doctor.js +7 -8
  80. package/dist/explore.js +1 -1
  81. package/dist/output.js +28 -0
  82. package/dist/output.test.js +15 -0
  83. package/dist/pipeline/executor.js +2 -7
  84. package/dist/pipeline/steps/browser.js +1 -1
  85. package/dist/pipeline/template.js +25 -3
  86. package/dist/record.d.ts +50 -0
  87. package/dist/record.js +298 -57
  88. package/dist/record.test.d.ts +1 -0
  89. package/dist/record.test.js +293 -0
  90. package/dist/registry.d.ts +2 -0
  91. package/dist/registry.js +1 -0
  92. package/dist/registry.test.js +10 -0
  93. package/dist/runtime.js +3 -3
  94. package/dist/snapshotFormatter.d.ts +1 -1
  95. package/dist/snapshotFormatter.js +4 -4
  96. package/dist/snapshotFormatter.test.d.ts +1 -1
  97. package/dist/snapshotFormatter.test.js +2 -2
  98. package/dist/types.d.ts +3 -1
  99. package/dist/types.js +1 -1
  100. package/docs/.vitepress/config.mts +2 -0
  101. package/docs/adapters/browser/amazon.md +53 -0
  102. package/docs/adapters/browser/gemini.md +72 -0
  103. package/docs/adapters/browser/notebooklm.md +5 -5
  104. package/docs/adapters/index.md +3 -1
  105. package/extension/dist/background.js +5 -143
  106. package/extension/src/background.test.ts +7 -163
  107. package/extension/src/background.ts +7 -157
  108. package/extension/src/protocol.ts +1 -5
  109. package/package.json +1 -1
  110. package/skills/opencli-explorer/SKILL.md +847 -0
  111. package/skills/opencli-oneshot/SKILL.md +216 -0
  112. package/skills/opencli-usage/SKILL.md +71 -0
  113. package/skills/opencli-usage/browser.md +429 -0
  114. package/skills/opencli-usage/desktop.md +118 -0
  115. package/skills/opencli-usage/plugins.md +82 -0
  116. package/skills/opencli-usage/public-api.md +149 -0
  117. package/src/browser/base-page.ts +197 -0
  118. package/src/browser/cdp.ts +7 -131
  119. package/src/browser/daemon-client.ts +3 -14
  120. package/src/browser/discover.ts +1 -4
  121. package/src/browser/errors.ts +22 -0
  122. package/src/browser/index.ts +1 -1
  123. package/src/browser/page.ts +13 -212
  124. package/src/browser/tabs.ts +5 -5
  125. package/src/browser.test.ts +15 -15
  126. package/src/clis/amazon/bestsellers.test.ts +22 -0
  127. package/src/clis/amazon/bestsellers.ts +180 -0
  128. package/src/clis/amazon/discussion.test.ts +38 -0
  129. package/src/clis/amazon/discussion.ts +131 -0
  130. package/src/clis/amazon/offer.test.ts +35 -0
  131. package/src/clis/amazon/offer.ts +185 -0
  132. package/src/clis/amazon/product.test.ts +26 -0
  133. package/src/clis/amazon/product.ts +131 -0
  134. package/src/clis/amazon/search.test.ts +24 -0
  135. package/src/clis/amazon/search.ts +128 -0
  136. package/src/clis/amazon/shared.test.ts +37 -0
  137. package/src/clis/amazon/shared.ts +316 -0
  138. package/src/clis/gemini/ask.ts +46 -0
  139. package/src/clis/gemini/image.ts +115 -0
  140. package/src/clis/gemini/new.ts +22 -0
  141. package/src/clis/gemini/utils.test.ts +36 -0
  142. package/src/clis/gemini/utils.ts +523 -0
  143. package/src/clis/notebooklm/compat.test.ts +3 -3
  144. package/src/clis/notebooklm/current.ts +2 -3
  145. package/src/clis/notebooklm/get.ts +1 -3
  146. package/src/clis/notebooklm/history.ts +1 -3
  147. package/src/clis/notebooklm/note-list.ts +1 -3
  148. package/src/clis/notebooklm/notes-get.ts +1 -3
  149. package/src/clis/notebooklm/open.test.ts +78 -0
  150. package/src/clis/notebooklm/open.ts +61 -0
  151. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  152. package/src/clis/notebooklm/source-get.ts +1 -3
  153. package/src/clis/notebooklm/source-guide.ts +1 -3
  154. package/src/clis/notebooklm/source-list.ts +1 -3
  155. package/src/clis/notebooklm/status.ts +1 -2
  156. package/src/clis/notebooklm/summary.ts +1 -3
  157. package/src/clis/notebooklm/utils.ts +29 -20
  158. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  159. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  160. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  161. package/src/commanderAdapter.test.ts +47 -0
  162. package/src/commanderAdapter.ts +7 -3
  163. package/src/commands/daemon.test.ts +1 -1
  164. package/src/commands/daemon.ts +1 -1
  165. package/src/doctor.ts +7 -8
  166. package/src/explore.ts +1 -1
  167. package/src/output.test.ts +17 -0
  168. package/src/output.ts +27 -0
  169. package/src/pipeline/executor.ts +2 -7
  170. package/src/pipeline/steps/browser.ts +1 -1
  171. package/src/pipeline/template.ts +27 -4
  172. package/src/record.test.ts +362 -0
  173. package/src/record.ts +341 -62
  174. package/src/registry.test.ts +12 -0
  175. package/src/registry.ts +3 -0
  176. package/src/runtime.ts +3 -3
  177. package/src/snapshotFormatter.test.ts +2 -2
  178. package/src/snapshotFormatter.ts +4 -4
  179. package/src/types.ts +3 -1
  180. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  181. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  182. package/SKILL.md +0 -879
  183. package/dist/clis/notebooklm/bind-current.js +0 -29
  184. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  185. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  186. package/dist/clis/notebooklm/binding.test.js +0 -44
  187. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  188. package/src/clis/notebooklm/bind-current.ts +0 -36
  189. package/src/clis/notebooklm/binding.test.ts +0 -53
  190. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  191. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  192. /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  193. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
  194. /package/src/browser/{mcp.ts → bridge.ts} +0 -0
@@ -301,11 +301,6 @@ function resetWindowIdleTimer(workspace) {
301
301
  session.idleTimer = setTimeout(async () => {
302
302
  const current = automationSessions.get(workspace);
303
303
  if (!current) return;
304
- if (!current.owned) {
305
- console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`);
306
- automationSessions.delete(workspace);
307
- return;
308
- }
309
304
  try {
310
305
  await chrome.windows.remove(current.windowId);
311
306
  console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
@@ -334,9 +329,7 @@ async function getAutomationWindow(workspace) {
334
329
  const session = {
335
330
  windowId: win.id,
336
331
  idleTimer: null,
337
- idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
338
- owned: true,
339
- preferredTabId: null
332
+ idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
340
333
  };
341
334
  automationSessions.set(workspace, session);
342
335
  console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
@@ -401,8 +394,6 @@ async function handleCommand(cmd) {
401
394
  return await handleSessions(cmd);
402
395
  case "set-file-input":
403
396
  return await handleSetFileInput(cmd, workspace);
404
- case "bind-current":
405
- return await handleBindCurrent(cmd, workspace);
406
397
  default:
407
398
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
408
399
  }
@@ -438,88 +429,12 @@ function normalizeUrlForComparison(url) {
438
429
  function isTargetUrl(currentUrl, targetUrl) {
439
430
  return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
440
431
  }
441
- function matchesDomain(url, domain) {
442
- if (!url) return false;
443
- try {
444
- const parsed = new URL(url);
445
- return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
446
- } catch {
447
- return false;
448
- }
449
- }
450
- function matchesBindCriteria(tab, cmd) {
451
- if (!tab.id || !isDebuggableUrl(tab.url)) return false;
452
- if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false;
453
- if (cmd.matchPathPrefix) {
454
- try {
455
- const parsed = new URL(tab.url);
456
- if (!parsed.pathname.startsWith(cmd.matchPathPrefix)) return false;
457
- } catch {
458
- return false;
459
- }
460
- }
461
- return true;
462
- }
463
- function isNotebooklmWorkspace(workspace) {
464
- return workspace === "site:notebooklm";
465
- }
466
- function classifyNotebooklmUrl(url) {
467
- if (!url) return "other";
468
- try {
469
- const parsed = new URL(url);
470
- if (parsed.hostname !== "notebooklm.google.com") return "other";
471
- return parsed.pathname.startsWith("/notebook/") ? "notebook" : "home";
472
- } catch {
473
- return "other";
474
- }
475
- }
476
- function scoreWorkspaceTab(workspace, tab) {
477
- if (!tab.id || !isDebuggableUrl(tab.url)) return -1;
478
- if (isNotebooklmWorkspace(workspace)) {
479
- const kind = classifyNotebooklmUrl(tab.url);
480
- if (kind === "other") return -1;
481
- if (kind === "notebook") return tab.active ? 400 : 300;
482
- return tab.active ? 200 : 100;
483
- }
484
- return -1;
485
- }
486
- function setWorkspaceSession(workspace, session) {
487
- const existing = automationSessions.get(workspace);
488
- if (existing?.idleTimer) clearTimeout(existing.idleTimer);
489
- automationSessions.set(workspace, {
490
- ...session,
491
- idleTimer: null,
492
- idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
493
- });
494
- }
495
- async function maybeBindWorkspaceToExistingTab(workspace) {
496
- if (!isNotebooklmWorkspace(workspace)) return null;
497
- const tabs = await chrome.tabs.query({});
498
- let bestTab = null;
499
- let bestScore = -1;
500
- for (const tab of tabs) {
501
- const score = scoreWorkspaceTab(workspace, tab);
502
- if (score > bestScore) {
503
- bestScore = score;
504
- bestTab = tab;
505
- }
506
- }
507
- if (!bestTab?.id || bestScore < 0) return null;
508
- setWorkspaceSession(workspace, {
509
- windowId: bestTab.windowId,
510
- owned: false,
511
- preferredTabId: bestTab.id
512
- });
513
- console.log(`[opencli] Workspace ${workspace} bound to existing tab ${bestTab.id} in window ${bestTab.windowId}`);
514
- resetWindowIdleTimer(workspace);
515
- return bestTab.id;
516
- }
517
432
  async function resolveTabId(tabId, workspace) {
518
433
  if (tabId !== void 0) {
519
434
  try {
520
435
  const tab = await chrome.tabs.get(tabId);
521
436
  const session = automationSessions.get(workspace);
522
- const matchesSession = session ? session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId : false;
437
+ const matchesSession = session ? tab.windowId === session.windowId : false;
523
438
  if (isDebuggableUrl(tab.url) && matchesSession) return tabId;
524
439
  if (session && !matchesSession) {
525
440
  console.warn(`[opencli] Tab ${tabId} is not bound to workspace ${workspace}, re-resolving`);
@@ -530,18 +445,6 @@ async function resolveTabId(tabId, workspace) {
530
445
  console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
531
446
  }
532
447
  }
533
- const adoptedTabId = await maybeBindWorkspaceToExistingTab(workspace);
534
- if (adoptedTabId !== null) return adoptedTabId;
535
- const existingSession = automationSessions.get(workspace);
536
- if (existingSession && existingSession.preferredTabId !== null) {
537
- try {
538
- const preferredTabId = existingSession.preferredTabId;
539
- const preferredTab = await chrome.tabs.get(preferredTabId);
540
- if (isDebuggableUrl(preferredTab.url)) return preferredTab.id;
541
- } catch {
542
- automationSessions.delete(workspace);
543
- }
544
- }
545
448
  const windowId = await getAutomationWindow(workspace);
546
449
  const tabs = await chrome.tabs.query({ windowId });
547
450
  const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
@@ -564,14 +467,6 @@ async function resolveTabId(tabId, workspace) {
564
467
  async function listAutomationTabs(workspace) {
565
468
  const session = automationSessions.get(workspace);
566
469
  if (!session) return [];
567
- if (session.preferredTabId !== null) {
568
- try {
569
- return [await chrome.tabs.get(session.preferredTabId)];
570
- } catch {
571
- automationSessions.delete(workspace);
572
- return [];
573
- }
574
- }
575
470
  try {
576
471
  return await chrome.tabs.query({ windowId: session.windowId });
577
472
  } catch {
@@ -753,11 +648,9 @@ async function handleScreenshot(cmd, workspace) {
753
648
  async function handleCloseWindow(cmd, workspace) {
754
649
  const session = automationSessions.get(workspace);
755
650
  if (session) {
756
- if (session.owned) {
757
- try {
758
- await chrome.windows.remove(session.windowId);
759
- } catch {
760
- }
651
+ try {
652
+ await chrome.windows.remove(session.windowId);
653
+ } catch {
761
654
  }
762
655
  if (session.idleTimer) clearTimeout(session.idleTimer);
763
656
  automationSessions.delete(workspace);
@@ -786,34 +679,3 @@ async function handleSessions(cmd) {
786
679
  })));
787
680
  return { id: cmd.id, ok: true, data };
788
681
  }
789
- async function handleBindCurrent(cmd, workspace) {
790
- const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
791
- const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
792
- const allTabs = await chrome.tabs.query({});
793
- const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd)) ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd));
794
- if (!boundTab?.id) {
795
- return {
796
- id: cmd.id,
797
- ok: false,
798
- error: cmd.matchDomain || cmd.matchPathPrefix ? `No visible tab matching ${cmd.matchDomain ?? "domain"}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ""}` : "No active debuggable tab found"
799
- };
800
- }
801
- setWorkspaceSession(workspace, {
802
- windowId: boundTab.windowId,
803
- owned: false,
804
- preferredTabId: boundTab.id
805
- });
806
- resetWindowIdleTimer(workspace);
807
- console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
808
- return {
809
- id: cmd.id,
810
- ok: true,
811
- data: {
812
- tabId: boundTab.id,
813
- windowId: boundTab.windowId,
814
- url: boundTab.url,
815
- title: boundTab.title,
816
- workspace
817
- }
818
- };
819
- }
@@ -200,7 +200,7 @@ describe('background tab isolation', () => {
200
200
  ]));
201
201
  });
202
202
 
203
- it('rebinds site:notebooklm to the active notebook tab instead of a home tab', async () => {
203
+ it('keeps site:notebooklm inside its owned automation window instead of rebinding to a user tab', async () => {
204
204
  const { chrome, tabs } = createChromeMock();
205
205
  tabs[0].url = 'https://notebooklm.google.com/';
206
206
  tabs[0].title = 'NotebookLM Home';
@@ -213,184 +213,28 @@ describe('background tab isolation', () => {
213
213
 
214
214
  const tabId = await mod.__test__.resolveTabId(undefined, 'site:notebooklm');
215
215
 
216
- expect(tabId).toBe(2);
216
+ expect(tabId).toBe(1);
217
217
  expect(mod.__test__.getSession('site:notebooklm')).toEqual(expect.objectContaining({
218
- windowId: 2,
219
- preferredTabId: 2,
220
- owned: false,
218
+ windowId: 1,
221
219
  }));
222
220
  });
223
221
 
224
- it('prefers a notebook tab over an active home tab for site:notebooklm', async () => {
222
+ it('idle timeout closes the automation window for site:notebooklm', async () => {
225
223
  const { chrome, tabs } = createChromeMock();
226
224
  tabs[0].url = 'https://notebooklm.google.com/';
227
225
  tabs[0].title = 'NotebookLM Home';
228
226
  tabs[0].active = true;
229
- tabs[1].url = 'https://notebooklm.google.com/notebook/nb-passive';
230
- tabs[1].title = 'Notebook';
231
- tabs[1].active = false;
232
- vi.stubGlobal('chrome', chrome);
233
-
234
- const mod = await import('./background');
235
- mod.__test__.setAutomationWindowId('site:notebooklm', 1);
236
-
237
- const tabId = await mod.__test__.resolveTabId(undefined, 'site:notebooklm');
238
-
239
- expect(tabId).toBe(2);
240
- expect(mod.__test__.getSession('site:notebooklm')).toEqual(expect.objectContaining({
241
- windowId: 2,
242
- preferredTabId: 2,
243
- owned: false,
244
- }));
245
- });
246
227
 
247
- it('detaches an adopted workspace session on idle instead of closing the user window', async () => {
248
- const { chrome } = createChromeMock();
249
- vi.stubGlobal('chrome', chrome);
250
228
  vi.useFakeTimers();
229
+ vi.stubGlobal('chrome', chrome);
251
230
 
252
231
  const mod = await import('./background');
253
- mod.__test__.setSession('site:notebooklm', {
254
- windowId: 2,
255
- preferredTabId: 2,
256
- owned: false,
257
- });
232
+ mod.__test__.setAutomationWindowId('site:notebooklm', 1);
258
233
 
259
234
  mod.__test__.resetWindowIdleTimer('site:notebooklm');
260
235
  await vi.advanceTimersByTimeAsync(30001);
261
236
 
262
- expect(chrome.windows.remove).not.toHaveBeenCalled();
237
+ expect(chrome.windows.remove).toHaveBeenCalledWith(1);
263
238
  expect(mod.__test__.getSession('site:notebooklm')).toBeNull();
264
239
  });
265
-
266
- it('binds the active NotebookLM tab into the workspace explicitly', async () => {
267
- const { chrome, tabs } = createChromeMock();
268
- tabs[1].url = 'https://notebooklm.google.com/notebook/nb-active';
269
- tabs[1].title = 'Bound Notebook';
270
- tabs[1].active = true;
271
- vi.stubGlobal('chrome', chrome);
272
-
273
- const mod = await import('./background');
274
- const result = await mod.__test__.handleBindCurrent(
275
- {
276
- id: 'bind-current',
277
- action: 'bind-current',
278
- workspace: 'site:notebooklm',
279
- matchDomain: 'notebooklm.google.com',
280
- matchPathPrefix: '/notebook/',
281
- },
282
- 'site:notebooklm',
283
- );
284
-
285
- expect(result).toEqual({
286
- id: 'bind-current',
287
- ok: true,
288
- data: expect.objectContaining({
289
- tabId: 2,
290
- windowId: 2,
291
- url: 'https://notebooklm.google.com/notebook/nb-active',
292
- title: 'Bound Notebook',
293
- workspace: 'site:notebooklm',
294
- }),
295
- });
296
- expect(mod.__test__.getSession('site:notebooklm')).toEqual(expect.objectContaining({
297
- windowId: 2,
298
- preferredTabId: 2,
299
- owned: false,
300
- }));
301
- });
302
-
303
- it('bind-current falls back to another matching notebook tab in the current window', async () => {
304
- const { chrome, tabs } = createChromeMock();
305
- tabs[0].windowId = 2;
306
- tabs[0].url = 'https://notebooklm.google.com/';
307
- tabs[0].title = 'NotebookLM Home';
308
- tabs[0].active = true;
309
- tabs[1].url = 'https://notebooklm.google.com/notebook/nb-passive';
310
- tabs[1].title = 'Passive Notebook';
311
- tabs[1].active = false;
312
- vi.stubGlobal('chrome', chrome);
313
-
314
- const mod = await import('./background');
315
- const result = await mod.__test__.handleBindCurrent(
316
- {
317
- id: 'bind-fallback',
318
- action: 'bind-current',
319
- workspace: 'site:notebooklm',
320
- matchDomain: 'notebooklm.google.com',
321
- matchPathPrefix: '/notebook/',
322
- },
323
- 'site:notebooklm',
324
- );
325
-
326
- expect(result).toEqual({
327
- id: 'bind-fallback',
328
- ok: true,
329
- data: expect.objectContaining({
330
- tabId: 2,
331
- windowId: 2,
332
- url: 'https://notebooklm.google.com/notebook/nb-passive',
333
- title: 'Passive Notebook',
334
- }),
335
- });
336
- });
337
-
338
- it('bind-current falls back to a matching notebook tab in another window of the same profile', async () => {
339
- const { chrome, tabs } = createChromeMock();
340
- tabs[0].windowId = 3;
341
- tabs[0].url = 'https://notebooklm.google.com/';
342
- tabs[0].title = 'NotebookLM Home';
343
- tabs[0].active = true;
344
- tabs[1].windowId = 2;
345
- tabs[1].url = 'https://notebooklm.google.com/notebook/nb-other-window';
346
- tabs[1].title = 'Notebook In Other Window';
347
- tabs[1].active = false;
348
- vi.stubGlobal('chrome', chrome);
349
-
350
- const mod = await import('./background');
351
- const result = await mod.__test__.handleBindCurrent(
352
- {
353
- id: 'bind-cross-window',
354
- action: 'bind-current',
355
- workspace: 'site:notebooklm',
356
- matchDomain: 'notebooklm.google.com',
357
- matchPathPrefix: '/notebook/',
358
- },
359
- 'site:notebooklm',
360
- );
361
-
362
- expect(result).toEqual({
363
- id: 'bind-cross-window',
364
- ok: true,
365
- data: expect.objectContaining({
366
- tabId: 2,
367
- windowId: 2,
368
- url: 'https://notebooklm.google.com/notebook/nb-other-window',
369
- title: 'Notebook In Other Window',
370
- }),
371
- });
372
- });
373
-
374
- it('rejects bind-current when the active tab is not NotebookLM', async () => {
375
- const { chrome } = createChromeMock();
376
- vi.stubGlobal('chrome', chrome);
377
-
378
- const mod = await import('./background');
379
- const result = await mod.__test__.handleBindCurrent(
380
- {
381
- id: 'bind-miss',
382
- action: 'bind-current',
383
- workspace: 'site:notebooklm',
384
- matchDomain: 'notebooklm.google.com',
385
- matchPathPrefix: '/notebook/',
386
- },
387
- 'site:notebooklm',
388
- );
389
-
390
- expect(result).toEqual({
391
- id: 'bind-miss',
392
- ok: false,
393
- error: 'No visible tab matching notebooklm.google.com /notebook/',
394
- });
395
- });
396
240
  });
@@ -117,8 +117,6 @@ type AutomationSession = {
117
117
  windowId: number;
118
118
  idleTimer: ReturnType<typeof setTimeout> | null;
119
119
  idleDeadlineAt: number;
120
- owned: boolean;
121
- preferredTabId: number | null;
122
120
  };
123
121
 
124
122
  const automationSessions = new Map<string, AutomationSession>();
@@ -136,11 +134,6 @@ function resetWindowIdleTimer(workspace: string): void {
136
134
  session.idleTimer = setTimeout(async () => {
137
135
  const current = automationSessions.get(workspace);
138
136
  if (!current) return;
139
- if (!current.owned) {
140
- console.log(`[opencli] Borrowed workspace ${workspace} detached from window ${current.windowId} (idle timeout)`);
141
- automationSessions.delete(workspace);
142
- return;
143
- }
144
137
  try {
145
138
  await chrome.windows.remove(current.windowId);
146
139
  console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
@@ -180,8 +173,6 @@ async function getAutomationWindow(workspace: string): Promise<number> {
180
173
  windowId: win.id!,
181
174
  idleTimer: null,
182
175
  idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT,
183
- owned: true,
184
- preferredTabId: null,
185
176
  };
186
177
  automationSessions.set(workspace, session);
187
178
  console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
@@ -263,8 +254,6 @@ async function handleCommand(cmd: Command): Promise<Result> {
263
254
  return await handleSessions(cmd);
264
255
  case 'set-file-input':
265
256
  return await handleSetFileInput(cmd, workspace);
266
- case 'bind-current':
267
- return await handleBindCurrent(cmd, workspace);
268
257
  default:
269
258
  return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` };
270
259
  }
@@ -312,57 +301,7 @@ function isTargetUrl(currentUrl: string | undefined, targetUrl: string): boolean
312
301
  return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
313
302
  }
314
303
 
315
- function matchesDomain(url: string | undefined, domain: string): boolean {
316
- if (!url) return false;
317
- try {
318
- const parsed = new URL(url);
319
- return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
320
- } catch {
321
- return false;
322
- }
323
- }
324
-
325
- function matchesBindCriteria(tab: chrome.tabs.Tab, cmd: Command): boolean {
326
- if (!tab.id || !isDebuggableUrl(tab.url)) return false;
327
- if (cmd.matchDomain && !matchesDomain(tab.url, cmd.matchDomain)) return false;
328
- if (cmd.matchPathPrefix) {
329
- try {
330
- const parsed = new URL(tab.url!);
331
- if (!parsed.pathname.startsWith(cmd.matchPathPrefix)) return false;
332
- } catch {
333
- return false;
334
- }
335
- }
336
- return true;
337
- }
338
-
339
- function isNotebooklmWorkspace(workspace: string): boolean {
340
- return workspace === 'site:notebooklm';
341
- }
342
-
343
- function classifyNotebooklmUrl(url?: string): 'notebook' | 'home' | 'other' {
344
- if (!url) return 'other';
345
- try {
346
- const parsed = new URL(url);
347
- if (parsed.hostname !== 'notebooklm.google.com') return 'other';
348
- return parsed.pathname.startsWith('/notebook/') ? 'notebook' : 'home';
349
- } catch {
350
- return 'other';
351
- }
352
- }
353
-
354
- function scoreWorkspaceTab(workspace: string, tab: chrome.tabs.Tab): number {
355
- if (!tab.id || !isDebuggableUrl(tab.url)) return -1;
356
- if (isNotebooklmWorkspace(workspace)) {
357
- const kind = classifyNotebooklmUrl(tab.url);
358
- if (kind === 'other') return -1;
359
- if (kind === 'notebook') return tab.active ? 400 : 300;
360
- return tab.active ? 200 : 100;
361
- }
362
- return -1;
363
- }
364
-
365
- function setWorkspaceSession(workspace: string, session: Omit<AutomationSession, 'idleTimer' | 'idleDeadlineAt'>): void {
304
+ function setWorkspaceSession(workspace: string, session: Pick<AutomationSession, 'windowId'>): void {
366
305
  const existing = automationSessions.get(workspace);
367
306
  if (existing?.idleTimer) clearTimeout(existing.idleTimer);
368
307
  automationSessions.set(workspace, {
@@ -372,29 +311,6 @@ function setWorkspaceSession(workspace: string, session: Omit<AutomationSession,
372
311
  });
373
312
  }
374
313
 
375
- async function maybeBindWorkspaceToExistingTab(workspace: string): Promise<number | null> {
376
- if (!isNotebooklmWorkspace(workspace)) return null;
377
- const tabs = await chrome.tabs.query({});
378
- let bestTab: chrome.tabs.Tab | null = null;
379
- let bestScore = -1;
380
- for (const tab of tabs) {
381
- const score = scoreWorkspaceTab(workspace, tab);
382
- if (score > bestScore) {
383
- bestScore = score;
384
- bestTab = tab;
385
- }
386
- }
387
- if (!bestTab?.id || bestScore < 0) return null;
388
- setWorkspaceSession(workspace, {
389
- windowId: bestTab.windowId,
390
- owned: false,
391
- preferredTabId: bestTab.id,
392
- });
393
- console.log(`[opencli] Workspace ${workspace} bound to existing tab ${bestTab.id} in window ${bestTab.windowId}`);
394
- resetWindowIdleTimer(workspace);
395
- return bestTab.id;
396
- }
397
-
398
314
  /**
399
315
  * Resolve target tab in the automation window.
400
316
  * If explicit tabId is given, use that directly.
@@ -408,9 +324,7 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
408
324
  try {
409
325
  const tab = await chrome.tabs.get(tabId);
410
326
  const session = automationSessions.get(workspace);
411
- const matchesSession = session
412
- ? (session.preferredTabId !== null ? session.preferredTabId === tabId : tab.windowId === session.windowId)
413
- : false;
327
+ const matchesSession = session ? tab.windowId === session.windowId : false;
414
328
  if (isDebuggableUrl(tab.url) && matchesSession) return tabId;
415
329
  if (session && !matchesSession) {
416
330
  console.warn(`[opencli] Tab ${tabId} is not bound to workspace ${workspace}, re-resolving`);
@@ -424,20 +338,6 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
424
338
  }
425
339
  }
426
340
 
427
- const adoptedTabId = await maybeBindWorkspaceToExistingTab(workspace);
428
- if (adoptedTabId !== null) return adoptedTabId;
429
-
430
- const existingSession = automationSessions.get(workspace);
431
- if (existingSession && existingSession.preferredTabId !== null) {
432
- try {
433
- const preferredTabId = existingSession.preferredTabId;
434
- const preferredTab = await chrome.tabs.get(preferredTabId);
435
- if (isDebuggableUrl(preferredTab.url)) return preferredTab.id!;
436
- } catch {
437
- automationSessions.delete(workspace);
438
- }
439
- }
440
-
441
341
  // Get (or create) the automation window
442
342
  const windowId = await getAutomationWindow(workspace);
443
343
 
@@ -470,14 +370,6 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
470
370
  async function listAutomationTabs(workspace: string): Promise<chrome.tabs.Tab[]> {
471
371
  const session = automationSessions.get(workspace);
472
372
  if (!session) return [];
473
- if (session.preferredTabId !== null) {
474
- try {
475
- return [await chrome.tabs.get(session.preferredTabId)];
476
- } catch {
477
- automationSessions.delete(workspace);
478
- return [];
479
- }
480
- }
481
373
  try {
482
374
  return await chrome.tabs.query({ windowId: session.windowId });
483
375
  } catch {
@@ -689,12 +581,10 @@ async function handleScreenshot(cmd: Command, workspace: string): Promise<Result
689
581
  async function handleCloseWindow(cmd: Command, workspace: string): Promise<Result> {
690
582
  const session = automationSessions.get(workspace);
691
583
  if (session) {
692
- if (session.owned) {
693
- try {
694
- await chrome.windows.remove(session.windowId);
695
- } catch {
696
- // Window may already be closed
697
- }
584
+ try {
585
+ await chrome.windows.remove(session.windowId);
586
+ } catch {
587
+ // Window may already be closed
698
588
  }
699
589
  if (session.idleTimer) clearTimeout(session.idleTimer);
700
590
  automationSessions.delete(workspace);
@@ -726,49 +616,11 @@ async function handleSessions(cmd: Command): Promise<Result> {
726
616
  return { id: cmd.id, ok: true, data };
727
617
  }
728
618
 
729
- async function handleBindCurrent(cmd: Command, workspace: string): Promise<Result> {
730
- const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
731
- const fallbackTabs = await chrome.tabs.query({ lastFocusedWindow: true });
732
- const allTabs = await chrome.tabs.query({});
733
- const boundTab = activeTabs.find((tab) => matchesBindCriteria(tab, cmd))
734
- ?? fallbackTabs.find((tab) => matchesBindCriteria(tab, cmd))
735
- ?? allTabs.find((tab) => matchesBindCriteria(tab, cmd));
736
- if (!boundTab?.id) {
737
- return {
738
- id: cmd.id,
739
- ok: false,
740
- error: cmd.matchDomain || cmd.matchPathPrefix
741
- ? `No visible tab matching ${cmd.matchDomain ?? 'domain'}${cmd.matchPathPrefix ? ` ${cmd.matchPathPrefix}` : ''}`
742
- : 'No active debuggable tab found',
743
- };
744
- }
745
-
746
- setWorkspaceSession(workspace, {
747
- windowId: boundTab.windowId,
748
- owned: false,
749
- preferredTabId: boundTab.id,
750
- });
751
- resetWindowIdleTimer(workspace);
752
- console.log(`[opencli] Workspace ${workspace} explicitly bound to tab ${boundTab.id} (${boundTab.url})`);
753
- return {
754
- id: cmd.id,
755
- ok: true,
756
- data: {
757
- tabId: boundTab.id,
758
- windowId: boundTab.windowId,
759
- url: boundTab.url,
760
- title: boundTab.title,
761
- workspace,
762
- },
763
- };
764
- }
765
-
766
619
  export const __test__ = {
767
620
  handleNavigate,
768
621
  isTargetUrl,
769
622
  handleTabs,
770
623
  handleSessions,
771
- handleBindCurrent,
772
624
  resolveTabId,
773
625
  resetWindowIdleTimer,
774
626
  getSession: (workspace: string = 'default') => automationSessions.get(workspace) ?? null,
@@ -782,11 +634,9 @@ export const __test__ = {
782
634
  }
783
635
  setWorkspaceSession(workspace, {
784
636
  windowId,
785
- owned: true,
786
- preferredTabId: null,
787
637
  });
788
638
  },
789
- setSession: (workspace: string, session: { windowId: number; owned: boolean; preferredTabId: number | null }) => {
639
+ setSession: (workspace: string, session: { windowId: number }) => {
790
640
  setWorkspaceSession(workspace, session);
791
641
  },
792
642
  };
@@ -5,7 +5,7 @@
5
5
  * Everything else is just JS code sent via 'exec'.
6
6
  */
7
7
 
8
- export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'bind-current';
8
+ export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input';
9
9
 
10
10
  export interface Command {
11
11
  /** Unique request ID */
@@ -26,10 +26,6 @@ export interface Command {
26
26
  index?: number;
27
27
  /** Cookie domain filter */
28
28
  domain?: string;
29
- /** Optional hostname/domain to require for current-tab binding */
30
- matchDomain?: string;
31
- /** Optional pathname prefix to require for current-tab binding */
32
- matchPathPrefix?: string;
33
29
  /** Screenshot format: png (default) or jpeg */
34
30
  format?: 'png' | 'jpeg';
35
31
  /** JPEG quality (0-100), only for jpeg format */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "1.5.8",
3
+ "version": "1.5.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },