@jackwener/opencli 1.7.14 → 1.7.16

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 (153) hide show
  1. package/README.md +9 -6
  2. package/README.zh-CN.md +9 -6
  3. package/cli-manifest.json +374 -74
  4. package/clis/bilibili/subtitle.js +1 -1
  5. package/clis/chatgpt/ask.js +2 -1
  6. package/clis/chatgpt/detail.js +6 -1
  7. package/clis/chatgpt/read.js +2 -1
  8. package/clis/chatgpt/send.js +2 -1
  9. package/clis/chatgpt/utils.js +54 -12
  10. package/clis/chatgpt/utils.test.js +36 -1
  11. package/clis/claude/ask.js +22 -7
  12. package/clis/claude/detail.js +9 -2
  13. package/clis/claude/new.js +8 -2
  14. package/clis/claude/read.js +2 -1
  15. package/clis/claude/send.js +8 -3
  16. package/clis/claude/utils.js +27 -4
  17. package/clis/deepseek/ask.js +21 -8
  18. package/clis/deepseek/detail.js +9 -1
  19. package/clis/deepseek/new.js +13 -2
  20. package/clis/deepseek/read.js +2 -1
  21. package/clis/deepseek/utils.js +8 -1
  22. package/clis/dianping/cityResolver.js +185 -0
  23. package/clis/dianping/dianping.test.js +154 -0
  24. package/clis/dianping/search.js +6 -3
  25. package/clis/douyin/_shared/browser-fetch.js +14 -2
  26. package/clis/douyin/_shared/browser-fetch.test.js +13 -0
  27. package/clis/douyin/stats.js +1 -1
  28. package/clis/douyin/update.js +1 -1
  29. package/clis/jike/search.js +1 -1
  30. package/clis/linkedin/search.js +8 -11
  31. package/clis/maimai/search-talents.js +10 -6
  32. package/clis/openreview/author.js +58 -0
  33. package/clis/openreview/openreview.test.js +83 -1
  34. package/clis/openreview/utils.js +14 -0
  35. package/clis/reddit/comment.js +1 -0
  36. package/clis/reddit/frontpage.js +1 -0
  37. package/clis/reddit/popular.js +1 -0
  38. package/clis/reddit/read.js +2 -0
  39. package/clis/reddit/read.test.js +4 -0
  40. package/clis/reddit/save.js +1 -0
  41. package/clis/reddit/saved.js +1 -0
  42. package/clis/reddit/search.js +2 -1
  43. package/clis/reddit/subreddit.js +2 -1
  44. package/clis/reddit/subscribe.js +1 -0
  45. package/clis/reddit/upvote.js +1 -0
  46. package/clis/reddit/upvoted.js +1 -0
  47. package/clis/reddit/user-comments.js +2 -1
  48. package/clis/reddit/user-posts.js +2 -1
  49. package/clis/reddit/user.js +2 -1
  50. package/clis/twitter/article.js +9 -5
  51. package/clis/twitter/bookmark-folder.js +187 -0
  52. package/clis/twitter/bookmark-folder.test.js +337 -0
  53. package/clis/twitter/bookmark-folders.js +115 -0
  54. package/clis/twitter/bookmark-folders.test.js +152 -0
  55. package/clis/twitter/bookmark.js +15 -6
  56. package/clis/twitter/bookmark.test.js +74 -0
  57. package/clis/twitter/bookmarks.js +10 -10
  58. package/clis/twitter/delete.js +11 -35
  59. package/clis/twitter/delete.test.js +21 -9
  60. package/clis/twitter/download.js +6 -5
  61. package/clis/twitter/followers.js +10 -3
  62. package/clis/twitter/following.js +14 -11
  63. package/clis/twitter/following.test.js +2 -1
  64. package/clis/twitter/hide-reply.js +24 -5
  65. package/clis/twitter/hide-reply.test.js +76 -0
  66. package/clis/twitter/like.js +21 -11
  67. package/clis/twitter/like.test.js +73 -0
  68. package/clis/twitter/likes.js +11 -11
  69. package/clis/twitter/list-add.js +8 -7
  70. package/clis/twitter/list-add.test.js +23 -1
  71. package/clis/twitter/list-remove.js +8 -7
  72. package/clis/twitter/list-remove.test.js +23 -1
  73. package/clis/twitter/list-tweets.js +9 -9
  74. package/clis/twitter/lists.js +6 -8
  75. package/clis/twitter/notifications.js +3 -2
  76. package/clis/twitter/profile.js +11 -7
  77. package/clis/twitter/quote.js +60 -32
  78. package/clis/twitter/quote.test.js +96 -8
  79. package/clis/twitter/reply.js +24 -178
  80. package/clis/twitter/reply.test.js +29 -11
  81. package/clis/twitter/retweet.js +9 -14
  82. package/clis/twitter/retweet.test.js +5 -1
  83. package/clis/twitter/search.js +176 -23
  84. package/clis/twitter/search.test.js +266 -1
  85. package/clis/twitter/shared.js +43 -0
  86. package/clis/twitter/shared.test.js +107 -1
  87. package/clis/twitter/thread.js +11 -11
  88. package/clis/twitter/timeline.js +13 -13
  89. package/clis/twitter/trending.js +4 -4
  90. package/clis/twitter/tweets.js +8 -9
  91. package/clis/twitter/unbookmark.js +13 -6
  92. package/clis/twitter/unbookmark.test.js +73 -0
  93. package/clis/twitter/unlike.js +6 -13
  94. package/clis/twitter/unlike.test.js +5 -2
  95. package/clis/twitter/unretweet.js +9 -14
  96. package/clis/twitter/unretweet.test.js +5 -1
  97. package/clis/twitter/utils.js +286 -0
  98. package/clis/twitter/utils.test.js +169 -0
  99. package/clis/youtube/like.js +6 -2
  100. package/clis/youtube/subscribe.js +6 -2
  101. package/clis/youtube/unlike.js +6 -2
  102. package/clis/youtube/unsubscribe.js +6 -2
  103. package/clis/youtube/utils.js +19 -13
  104. package/clis/youtube/utils.test.js +17 -1
  105. package/dist/src/browser/ax-snapshot.d.ts +37 -0
  106. package/dist/src/browser/ax-snapshot.js +217 -0
  107. package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
  108. package/dist/src/browser/ax-snapshot.test.js +91 -0
  109. package/dist/src/browser/base-page.d.ts +51 -0
  110. package/dist/src/browser/base-page.js +545 -2
  111. package/dist/src/browser/base-page.test.js +520 -4
  112. package/dist/src/browser/bridge.d.ts +1 -0
  113. package/dist/src/browser/bridge.js +1 -1
  114. package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
  115. package/dist/src/browser/cdp-click-fixture.test.js +87 -0
  116. package/dist/src/browser/cdp.d.ts +1 -0
  117. package/dist/src/browser/cdp.js +5 -0
  118. package/dist/src/browser/cdp.test.js +1 -0
  119. package/dist/src/browser/daemon-client.d.ts +5 -3
  120. package/dist/src/browser/daemon-client.js +6 -3
  121. package/dist/src/browser/daemon-client.test.js +10 -0
  122. package/dist/src/browser/find.d.ts +9 -1
  123. package/dist/src/browser/find.js +219 -0
  124. package/dist/src/browser/find.test.js +61 -1
  125. package/dist/src/browser/page.d.ts +4 -2
  126. package/dist/src/browser/page.js +18 -1
  127. package/dist/src/browser/page.test.js +28 -0
  128. package/dist/src/browser/target-errors.d.ts +3 -1
  129. package/dist/src/browser/target-errors.js +2 -0
  130. package/dist/src/browser/target-resolver.d.ts +14 -0
  131. package/dist/src/browser/target-resolver.js +28 -0
  132. package/dist/src/browser/visual-refs.d.ts +11 -0
  133. package/dist/src/browser/visual-refs.js +108 -0
  134. package/dist/src/build-manifest.d.ts +23 -0
  135. package/dist/src/build-manifest.js +34 -0
  136. package/dist/src/build-manifest.test.js +108 -1
  137. package/dist/src/cli.js +630 -60
  138. package/dist/src/cli.test.js +731 -1
  139. package/dist/src/commanderAdapter.js +7 -0
  140. package/dist/src/doctor.js +2 -2
  141. package/dist/src/doctor.test.js +4 -4
  142. package/dist/src/execution.d.ts +2 -0
  143. package/dist/src/execution.js +31 -6
  144. package/dist/src/execution.test.js +43 -16
  145. package/dist/src/external-clis.yaml +24 -0
  146. package/dist/src/help.d.ts +33 -0
  147. package/dist/src/help.js +174 -0
  148. package/dist/src/main.js +4 -14
  149. package/dist/src/runtime.d.ts +3 -0
  150. package/dist/src/runtime.js +1 -0
  151. package/dist/src/types.d.ts +83 -1
  152. package/package.json +1 -1
  153. package/scripts/typed-error-lint-baseline.json +18 -18
@@ -48,6 +48,11 @@ export function registerCommandToProgram(siteCmd, cmd) {
48
48
  .option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
49
49
  .option('--trace <mode>', 'Trace capture: off, on, retain-on-failure', 'off')
50
50
  .option('-v, --verbose', 'Debug output', false);
51
+ if (cmd.browser) {
52
+ subCmd
53
+ .option('--window <mode>', 'Browser window mode: foreground or background')
54
+ .option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
55
+ }
51
56
  const originalHelpInformation = subCmd.helpInformation.bind(subCmd);
52
57
  subCmd.helpInformation = ((contextOptions) => {
53
58
  const format = getRequestedHelpFormat();
@@ -102,6 +107,8 @@ export function registerCommandToProgram(siteCmd, cmd) {
102
107
  prepared: true,
103
108
  ...(typeof globals.profile === 'string' && globals.profile.trim() ? { profile: globals.profile.trim() } : {}),
104
109
  ...(typeof optionsRecord.trace === 'string' && optionsRecord.trace !== 'off' ? { trace: optionsRecord.trace } : {}),
110
+ ...(cmd.browser && typeof optionsRecord.window === 'string' ? { windowMode: optionsRecord.window } : {}),
111
+ ...(cmd.browser && typeof optionsRecord.keepTab === 'string' ? { keepTab: optionsRecord.keepTab } : {}),
105
112
  });
106
113
  if (result === null || result === undefined) {
107
114
  return;
@@ -271,8 +271,8 @@ export function renderBrowserDoctorReport(report) {
271
271
  ? `tab ${session.preferredTabId}`
272
272
  : `window ${session.windowId ?? 'unknown'}`;
273
273
  const mode = session.ownership ?? (session.owned === false ? 'borrowed' : 'owned');
274
- const surface = session.surface ? `, surface=${session.surface}` : '';
275
- lines.push(styleText('dim', ` • ${session.workspace ?? 'default'} → ${target}, mode=${mode}${surface}, tabs=${session.tabCount ?? 0}, idle=${idle}`));
274
+ const windowRole = session.windowRole ? `, window=${session.windowRole}` : '';
275
+ lines.push(styleText('dim', ` • ${session.workspace ?? 'default'} → ${target}, mode=${mode}${windowRole}, tabs=${session.tabCount ?? 0}, idle=${idle}`));
276
276
  }
277
277
  }
278
278
  }
@@ -116,13 +116,13 @@ describe('doctor report rendering', () => {
116
116
  windowId: 2,
117
117
  preferredTabId: 42,
118
118
  ownership: 'borrowed',
119
- surface: 'borrowed-user-tab',
119
+ windowRole: 'borrowed-user',
120
120
  tabCount: 1,
121
121
  idleMsRemaining: null,
122
122
  },
123
123
  ],
124
124
  }));
125
- expect(text).toContain('bound:default → tab 42, mode=borrowed, surface=borrowed-user-tab, tabs=1, idle=none');
125
+ expect(text).toContain('bound:default → tab 42, mode=borrowed, window=borrowed-user, tabs=1, idle=none');
126
126
  });
127
127
  it('renders connected profiles and groups sessions by profile', () => {
128
128
  const text = strip(renderBrowserDoctorReport({
@@ -140,7 +140,7 @@ describe('doctor report rendering', () => {
140
140
  windowId: 2,
141
141
  preferredTabId: 42,
142
142
  ownership: 'borrowed',
143
- surface: 'borrowed-user-tab',
143
+ windowRole: 'borrowed-user',
144
144
  tabCount: 1,
145
145
  idleMsRemaining: null,
146
146
  },
@@ -150,7 +150,7 @@ describe('doctor report rendering', () => {
150
150
  windowId: 1,
151
151
  preferredTabId: 10,
152
152
  ownership: 'owned',
153
- surface: 'dedicated-container',
153
+ windowRole: 'automation',
154
154
  tabCount: 1,
155
155
  idleMsRemaining: 1000,
156
156
  },
@@ -16,6 +16,8 @@ export declare function executeCommand(cmd: CliCommand, rawKwargs: CommandArgs,
16
16
  prepared?: boolean;
17
17
  profile?: string;
18
18
  trace?: string;
19
+ keepTab?: string;
20
+ windowMode?: string;
19
21
  onTraceExport?: (trace: ObservationExportResult) => void;
20
22
  }): Promise<unknown>;
21
23
  export declare function prepareCommandArgs(cmd: CliCommand, rawKwargs: CommandArgs): CommandArgs;
@@ -215,6 +215,8 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
215
215
  const browserReuse = resolveBrowserSessionReuse(cmd);
216
216
  const workspace = resolveBrowserWorkspace(cmd, browserReuse);
217
217
  const idleTimeout = browserReuse === 'site' ? INTERACTIVE_BROWSER_IDLE_TIMEOUT_SECONDS : undefined;
218
+ const keepTab = resolveKeepTab(browserReuse, opts.keepTab);
219
+ const windowMode = resolveBrowserWindowMode('background', opts.windowMode);
218
220
  result = await browserSession(BrowserFactory, async (page) => {
219
221
  const observation = traceMode === 'off'
220
222
  ? null
@@ -281,9 +283,6 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
281
283
  throw wrapped;
282
284
  }
283
285
  }
284
- // --live / OPENCLI_LIVE=1 keeps the current automation tab lease after
285
- // the command finishes, so agents (or humans) can inspect the page state.
286
- const keepOpen = browserReuse !== 'none' || process.env.OPENCLI_LIVE === '1' || process.env.OPENCLI_LIVE === 'true';
287
286
  try {
288
287
  const browserTimeout = userTimeoutSec !== null
289
288
  ? userTimeoutSec + RUNTIME_TIMEOUT_PADDING_SECONDS
@@ -304,7 +303,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
304
303
  // Adapter commands are one-shot — release the current tab lease immediately
305
304
  // instead of waiting for the 30s idle timeout. The automation container
306
305
  // window stays open for reuse.
307
- if (!keepOpen)
306
+ if (!keepTab)
308
307
  await page.closeWindow?.().catch(() => { });
309
308
  return result;
310
309
  }
@@ -329,11 +328,11 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
329
328
  // Release the tab lease on failure too — without this, the lease lingers
330
329
  // until the extension's idle timer fires (unreliable on Windows where
331
330
  // MV3 service workers may be suspended before setTimeout triggers).
332
- if (!keepOpen)
331
+ if (!keepTab)
333
332
  await page.closeWindow?.().catch(() => { });
334
333
  throw err;
335
334
  }
336
- }, { workspace, cdpEndpoint, contextId, idleTimeout });
335
+ }, { workspace, cdpEndpoint, contextId, idleTimeout, windowMode });
337
336
  }
338
337
  else {
339
338
  // Non-browser commands: enforce a timeout only when the command exposes
@@ -459,6 +458,32 @@ function resolveBrowserWorkspace(cmd, reuse) {
459
458
  return `site:${cmd.site}`;
460
459
  return `site:${cmd.site}:${crypto.randomUUID()}`;
461
460
  }
461
+ function normalizeBooleanOption(name, raw) {
462
+ if (raw === undefined || raw === '')
463
+ return null;
464
+ if (raw === 'true')
465
+ return true;
466
+ if (raw === 'false')
467
+ return false;
468
+ throw new ArgumentError(`${name} must be one of: true, false. Received: "${String(raw)}"`);
469
+ }
470
+ function resolveKeepTab(reuse, rawOption) {
471
+ return normalizeBooleanOption('--keep-tab', rawOption)
472
+ ?? normalizeBooleanOption('OPENCLI_KEEP_TAB', process.env.OPENCLI_KEEP_TAB)
473
+ ?? reuse !== 'none';
474
+ }
475
+ function normalizeWindowMode(name, raw) {
476
+ if (raw === undefined || raw === '')
477
+ return null;
478
+ if (raw === 'foreground' || raw === 'background')
479
+ return raw;
480
+ throw new ArgumentError(`${name} must be one of: foreground, background. Received: "${String(raw)}"`);
481
+ }
482
+ function resolveBrowserWindowMode(defaultMode = 'background', rawOption) {
483
+ return normalizeWindowMode('--window', rawOption)
484
+ ?? normalizeWindowMode('OPENCLI_WINDOW', process.env.OPENCLI_WINDOW)
485
+ ?? defaultMode;
486
+ }
462
487
  /**
463
488
  * Resolve the user-controllable `--timeout` arg, in seconds.
464
489
  *
@@ -157,8 +157,8 @@ describe('executeCommand — non-browser timeout', () => {
157
157
  await executeCommand(cmd, {});
158
158
  await executeCommand(cmd, {});
159
159
  expect(sessionOpts).toHaveLength(2);
160
- expect(sessionOpts[0]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600 });
161
- expect(sessionOpts[1]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600 });
160
+ expect(sessionOpts[0]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600, windowMode: 'background' });
161
+ expect(sessionOpts[1]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600, windowMode: 'background' });
162
162
  expect(closeWindow).not.toHaveBeenCalled();
163
163
  vi.restoreAllMocks();
164
164
  });
@@ -187,6 +187,8 @@ describe('executeCommand — non-browser timeout', () => {
187
187
  expect(sessionOpts[0]?.workspace).not.toBe(sessionOpts[1]?.workspace);
188
188
  expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
189
189
  expect(sessionOpts[1]?.idleTimeout).toBeUndefined();
190
+ expect(sessionOpts[0]?.windowMode).toBe('background');
191
+ expect(sessionOpts[1]?.windowMode).toBe('background');
190
192
  expect(closeWindow).toHaveBeenCalledTimes(2);
191
193
  vi.restoreAllMocks();
192
194
  });
@@ -372,18 +374,18 @@ describe('executeCommand — non-browser timeout', () => {
372
374
  expect(closeWindow).toHaveBeenCalledTimes(1);
373
375
  vi.restoreAllMocks();
374
376
  });
375
- it('skips closeWindow when OPENCLI_LIVE=1 (success path)', async () => {
377
+ it('skips closeWindow when OPENCLI_KEEP_TAB=true (success path)', async () => {
376
378
  const closeWindow = vi.fn().mockResolvedValue(undefined);
377
379
  const mockPage = { closeWindow };
378
380
  vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
379
381
  vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
380
- const prev = process.env.OPENCLI_LIVE;
381
- process.env.OPENCLI_LIVE = '1';
382
+ const prev = process.env.OPENCLI_KEEP_TAB;
383
+ process.env.OPENCLI_KEEP_TAB = 'true';
382
384
  try {
383
385
  const cmd = cli({
384
386
  site: 'test-execution',
385
- name: 'browser-live-success', access: 'read',
386
- description: 'test closeWindow skipped with --live on success',
387
+ name: 'browser-keep-tab-success', access: 'read',
388
+ description: 'test closeWindow skipped with --keep-tab on success',
387
389
  browser: true,
388
390
  strategy: Strategy.PUBLIC,
389
391
  func: async () => [{ ok: true }],
@@ -393,24 +395,24 @@ describe('executeCommand — non-browser timeout', () => {
393
395
  }
394
396
  finally {
395
397
  if (prev === undefined)
396
- delete process.env.OPENCLI_LIVE;
398
+ delete process.env.OPENCLI_KEEP_TAB;
397
399
  else
398
- process.env.OPENCLI_LIVE = prev;
400
+ process.env.OPENCLI_KEEP_TAB = prev;
399
401
  vi.restoreAllMocks();
400
402
  }
401
403
  });
402
- it('skips closeWindow when OPENCLI_LIVE=1 (failure path)', async () => {
404
+ it('skips closeWindow when OPENCLI_KEEP_TAB=true (failure path)', async () => {
403
405
  const closeWindow = vi.fn().mockResolvedValue(undefined);
404
406
  const mockPage = { closeWindow };
405
407
  vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
406
408
  vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
407
- const prev = process.env.OPENCLI_LIVE;
408
- process.env.OPENCLI_LIVE = '1';
409
+ const prev = process.env.OPENCLI_KEEP_TAB;
410
+ process.env.OPENCLI_KEEP_TAB = 'true';
409
411
  try {
410
412
  const cmd = cli({
411
413
  site: 'test-execution',
412
- name: 'browser-live-failure', access: 'read',
413
- description: 'test closeWindow skipped with --live on failure',
414
+ name: 'browser-keep-tab-failure', access: 'read',
415
+ description: 'test closeWindow skipped with --keep-tab on failure',
414
416
  browser: true,
415
417
  strategy: Strategy.PUBLIC,
416
418
  func: async () => { throw new Error('adapter failure'); },
@@ -420,12 +422,37 @@ describe('executeCommand — non-browser timeout', () => {
420
422
  }
421
423
  finally {
422
424
  if (prev === undefined)
423
- delete process.env.OPENCLI_LIVE;
425
+ delete process.env.OPENCLI_KEEP_TAB;
424
426
  else
425
- process.env.OPENCLI_LIVE = prev;
427
+ process.env.OPENCLI_KEEP_TAB = prev;
426
428
  vi.restoreAllMocks();
427
429
  }
428
430
  });
431
+ it('lets browser common options override adapter window and keep-tab defaults', async () => {
432
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
433
+ const mockPage = { closeWindow };
434
+ const sessionOpts = [];
435
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
436
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
437
+ sessionOpts.push(opts ?? {});
438
+ return fn(mockPage);
439
+ });
440
+ const cmd = cli({
441
+ site: 'test-execution',
442
+ name: 'browser-window-options', access: 'read',
443
+ description: 'test browser common options',
444
+ browser: true,
445
+ strategy: Strategy.PUBLIC,
446
+ func: async () => [{ ok: true }],
447
+ });
448
+ await executeCommand(cmd, {}, false, {
449
+ windowMode: 'foreground',
450
+ keepTab: 'true',
451
+ });
452
+ expect(sessionOpts[0]).toMatchObject({ windowMode: 'foreground' });
453
+ expect(closeWindow).not.toHaveBeenCalled();
454
+ vi.restoreAllMocks();
455
+ });
429
456
  it('does not re-run custom validation when args are already prepared', async () => {
430
457
  const validateArgs = vi.fn();
431
458
  const cmd = {
@@ -54,3 +54,27 @@
54
54
  tags: [vercel, deployment, serverless, frontend, devops]
55
55
  install:
56
56
  default: "npm install -g vercel"
57
+
58
+ - name: tg-cli
59
+ binary: tg
60
+ description: "Telegram CLI — local-first sync, search, export via MTProto for AI agents"
61
+ homepage: "https://github.com/jackwener/tg-cli"
62
+ tags: [telegram, messaging, search, export, ai-agent]
63
+ install:
64
+ default: "uv tool install kabi-tg-cli"
65
+
66
+ - name: discord-cli
67
+ binary: discord
68
+ description: "Discord CLI — local-first sync, search, export via SQLite for AI agents"
69
+ homepage: "https://github.com/jackwener/discord-cli"
70
+ tags: [discord, messaging, search, export, ai-agent]
71
+ install:
72
+ default: "uv tool install kabi-discord-cli"
73
+
74
+ - name: wx-cli
75
+ binary: wx
76
+ description: "WeChat local data CLI — sessions, messages, search, contacts, export for AI agents"
77
+ homepage: "https://github.com/jackwener/wx-cli"
78
+ tags: [wechat, messaging, search, export, ai-agent]
79
+ install:
80
+ default: "npm install -g @jackwener/wx-cli"
@@ -1,6 +1,24 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CliCommand } from './registry.js';
3
3
  export type StructuredHelpFormat = 'yaml' | 'json';
4
+ export interface ArgSpec {
5
+ name: string;
6
+ required?: true;
7
+ variadic?: true;
8
+ help?: string;
9
+ default?: unknown;
10
+ choices?: string[];
11
+ }
12
+ export interface OptionSpec {
13
+ name: string;
14
+ flags: string;
15
+ help?: string;
16
+ takes_value?: 'required' | 'optional';
17
+ required?: true;
18
+ default?: unknown;
19
+ choices?: string[];
20
+ negate?: true;
21
+ }
4
22
  export declare function getRequestedHelpFormat(argv?: readonly string[]): StructuredHelpFormat | undefined;
5
23
  export declare function renderStructuredHelp(data: unknown, format: StructuredHelpFormat): string;
6
24
  export declare function wrapCommaList(items: readonly string[], opts?: {
@@ -29,11 +47,26 @@ export interface RootAdapterGroups {
29
47
  sites: readonly string[];
30
48
  }
31
49
  export declare function formatRootAdapterHelpText(groups: RootAdapterGroups): string;
50
+ export declare function commanderNamespaceHelpData(namespaceRoot: Command, opts?: {
51
+ globalCommand?: Command;
52
+ description?: string;
53
+ }): Record<string, unknown>;
54
+ export declare function commanderCommandHelpData(namespaceRoot: Command, command: Command, opts?: {
55
+ globalCommand?: Command;
56
+ }): Record<string, unknown>;
57
+ export declare function commanderGroupHelpData(namespaceRoot: Command, groupCommand: Command, opts?: {
58
+ globalCommand?: Command;
59
+ }): Record<string, unknown>;
60
+ export declare function installCommanderNamespaceStructuredHelp(namespaceRoot: Command, opts?: {
61
+ globalCommand?: Command;
62
+ description?: string;
63
+ }): void;
32
64
  export declare function formatCommandListTerm(cmd: CliCommand): string;
33
65
  export declare function rootHelpData(program: Command, groups: RootAdapterGroups): Record<string, unknown>;
34
66
  export declare function siteHelpData(site: string, commands: readonly CliCommand[]): Record<string, unknown>;
35
67
  export declare function commandHelpData(cmd: CliCommand): Record<string, unknown>;
36
68
  export declare function formatCommonOptionsHelpText(): string;
69
+ export declare function formatBrowserCommonOptionsHelpText(): string;
37
70
  export declare function formatSiteHelpText(site: string, commands: readonly CliCommand[]): string;
38
71
  export declare function formatCommandHelpText(cmd: CliCommand): string;
39
72
  export declare function installStructuredHelp(command: Command, data: () => unknown, textSuffix?: string | (() => string)): void;
package/dist/src/help.js CHANGED
@@ -28,6 +28,20 @@ const COMMON_OPTIONS = [
28
28
  help: 'display help for command',
29
29
  },
30
30
  ];
31
+ const BROWSER_COMMON_OPTIONS = [
32
+ {
33
+ flags: '--window <mode>',
34
+ name: 'window',
35
+ help: 'Browser window mode: foreground or background',
36
+ choices: ['foreground', 'background'],
37
+ },
38
+ {
39
+ flags: '--keep-tab <bool>',
40
+ name: 'keep-tab',
41
+ help: 'Keep the browser tab lease after the command finishes',
42
+ choices: ['true', 'false'],
43
+ },
44
+ ];
31
45
  function normalizeStructuredHelpFormat(value) {
32
46
  const normalized = value?.toLowerCase();
33
47
  if (normalized === 'yaml' || normalized === 'yml')
@@ -125,6 +139,151 @@ function compactCommonOption(option) {
125
139
  ...('choices' in option ? { choices: option.choices } : {}),
126
140
  };
127
141
  }
142
+ function compactCommanderArgument(arg) {
143
+ return {
144
+ name: arg.name(),
145
+ ...(arg.required ? { required: true } : {}),
146
+ ...(arg.variadic ? { variadic: true } : {}),
147
+ ...(arg.description ? { help: arg.description } : {}),
148
+ ...(arg.defaultValue !== undefined ? { default: arg.defaultValue } : {}),
149
+ ...(arg.argChoices?.length ? { choices: [...arg.argChoices] } : {}),
150
+ };
151
+ }
152
+ function compactCommanderOption(option) {
153
+ if (option.hidden)
154
+ return null;
155
+ return {
156
+ name: option.attributeName(),
157
+ flags: option.flags,
158
+ ...(option.description ? { help: option.description } : {}),
159
+ ...(option.required ? { takes_value: 'required' } : {}),
160
+ ...(option.optional ? { takes_value: 'optional' } : {}),
161
+ ...(option.mandatory ? { required: true } : {}),
162
+ ...(option.defaultValue !== undefined ? { default: option.defaultValue } : {}),
163
+ ...(option.argChoices?.length ? { choices: [...option.argChoices] } : {}),
164
+ ...(option.negate ? { negate: true } : {}),
165
+ };
166
+ }
167
+ function compactCommanderOptions(options) {
168
+ return options
169
+ .map(compactCommanderOption)
170
+ .filter((option) => option !== null);
171
+ }
172
+ function commanderPath(command) {
173
+ const parts = [];
174
+ let current = command;
175
+ while (current) {
176
+ const name = current.name();
177
+ if (name)
178
+ parts.push(name);
179
+ current = current.parent;
180
+ }
181
+ return parts.reverse();
182
+ }
183
+ function commandPathFromRoot(namespaceRoot, command) {
184
+ const rootPath = commanderPath(namespaceRoot);
185
+ const commandPath = commanderPath(command);
186
+ return commandPath.slice(rootPath.length);
187
+ }
188
+ function collectLeafCommands(command) {
189
+ if (command.commands.length === 0)
190
+ return [command];
191
+ return command.commands.flatMap(child => collectLeafCommands(child));
192
+ }
193
+ function collectDescendantCommands(command) {
194
+ return command.commands.flatMap(child => [child, ...collectDescendantCommands(child)]);
195
+ }
196
+ function formatCommanderPositionals(args) {
197
+ return args
198
+ .map(arg => {
199
+ const name = `${arg.name()}${arg.variadic ? '...' : ''}`;
200
+ return arg.required ? `<${name}>` : `[${name}]`;
201
+ })
202
+ .join(' ');
203
+ }
204
+ function formatCommanderUsage(command, opts = {}) {
205
+ const path = commanderPath(command).join(' ');
206
+ const positionalText = formatCommanderPositionals(command.registeredArguments);
207
+ const hasOptions = compactCommanderOptions(command.options).length > 0
208
+ || (opts.namespaceRoot ? compactCommanderOptions(opts.namespaceRoot.options).length > 0 : false)
209
+ || (opts.globalCommand ? compactCommanderOptions(opts.globalCommand.options).length > 0 : false);
210
+ const optionText = hasOptions ? ' [options]' : '';
211
+ return `${path}${positionalText ? ` ${positionalText}` : ''}${optionText}`;
212
+ }
213
+ function compactCommanderCommand(namespaceRoot, command, opts = {}) {
214
+ const relativePath = commandPathFromRoot(namespaceRoot, command);
215
+ return {
216
+ name: relativePath.join(' '),
217
+ command: commanderPath(command).join(' '),
218
+ usage: formatCommanderUsage(command, { namespaceRoot, globalCommand: opts.globalCommand }),
219
+ description: command.description(),
220
+ ...(command.aliases().length ? { aliases: command.aliases() } : {}),
221
+ positionals: command.registeredArguments.map(compactCommanderArgument),
222
+ command_options: compactCommanderOptions(command.options),
223
+ };
224
+ }
225
+ export function commanderNamespaceHelpData(namespaceRoot, opts = {}) {
226
+ const leaves = collectLeafCommands(namespaceRoot)
227
+ .filter(command => command !== namespaceRoot)
228
+ .sort((a, b) => commandPathFromRoot(namespaceRoot, a).join(' ').localeCompare(commandPathFromRoot(namespaceRoot, b).join(' ')));
229
+ return {
230
+ namespace: namespaceRoot.name(),
231
+ command: commanderPath(namespaceRoot).join(' '),
232
+ usage: `${commanderPath(namespaceRoot).join(' ')} <command> [args] [options]`,
233
+ description: opts.description ?? namespaceRoot.description(),
234
+ command_count: leaves.length,
235
+ commands: leaves.map(command => compactCommanderCommand(namespaceRoot, command, opts)),
236
+ namespace_options: compactCommanderOptions(namespaceRoot.options),
237
+ ...(opts.globalCommand ? { global_options: compactCommanderOptions(opts.globalCommand.options) } : {}),
238
+ structured_help: {
239
+ formats: ['yaml', 'json'],
240
+ usage: `${commanderPath(namespaceRoot).join(' ')} --help -f yaml`,
241
+ },
242
+ };
243
+ }
244
+ export function commanderCommandHelpData(namespaceRoot, command, opts = {}) {
245
+ return {
246
+ namespace: namespaceRoot.name(),
247
+ ...compactCommanderCommand(namespaceRoot, command, opts),
248
+ namespace_options: compactCommanderOptions(namespaceRoot.options),
249
+ ...(opts.globalCommand ? { global_options: compactCommanderOptions(opts.globalCommand.options) } : {}),
250
+ structured_help: {
251
+ formats: ['yaml', 'json'],
252
+ usage: `${commanderPath(command).join(' ')} --help -f yaml`,
253
+ },
254
+ };
255
+ }
256
+ export function commanderGroupHelpData(namespaceRoot, groupCommand, opts = {}) {
257
+ const leaves = collectLeafCommands(groupCommand)
258
+ .filter(command => command !== groupCommand)
259
+ .sort((a, b) => commandPathFromRoot(namespaceRoot, a).join(' ').localeCompare(commandPathFromRoot(namespaceRoot, b).join(' ')));
260
+ return {
261
+ namespace: namespaceRoot.name(),
262
+ group: commandPathFromRoot(namespaceRoot, groupCommand).join(' '),
263
+ command: commanderPath(groupCommand).join(' '),
264
+ usage: `${commanderPath(groupCommand).join(' ')} <command> [args] [options]`,
265
+ description: groupCommand.description(),
266
+ command_count: leaves.length,
267
+ commands: leaves.map(command => compactCommanderCommand(namespaceRoot, command, opts)),
268
+ namespace_options: compactCommanderOptions(namespaceRoot.options),
269
+ ...(opts.globalCommand ? { global_options: compactCommanderOptions(opts.globalCommand.options) } : {}),
270
+ structured_help: {
271
+ formats: ['yaml', 'json'],
272
+ usage: `${commanderPath(groupCommand).join(' ')} --help -f yaml`,
273
+ },
274
+ };
275
+ }
276
+ export function installCommanderNamespaceStructuredHelp(namespaceRoot, opts = {}) {
277
+ installStructuredHelp(namespaceRoot, () => commanderNamespaceHelpData(namespaceRoot, opts));
278
+ for (const command of collectDescendantCommands(namespaceRoot)) {
279
+ if (command.commands.length > 0) {
280
+ installStructuredHelp(command, () => commanderGroupHelpData(namespaceRoot, command, opts));
281
+ }
282
+ else {
283
+ installStructuredHelp(command, () => commanderCommandHelpData(namespaceRoot, command, opts));
284
+ }
285
+ }
286
+ }
128
287
  function positionals(cmd) {
129
288
  return cmd.args.filter(arg => arg.positional);
130
289
  }
@@ -162,6 +321,7 @@ function compactCommand(cmd) {
162
321
  ...(cmd.aliases?.length ? { aliases: cmd.aliases } : {}),
163
322
  positionals: positionals(cmd).map(compactArg),
164
323
  command_options: commandOptions(cmd).map(compactArg),
324
+ ...(cmd.browser ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
165
325
  example: formatCommandExample(cmd),
166
326
  ...(cmd.browserSession ? { browserSession: cmd.browserSession } : {}),
167
327
  ...(cmd.defaultFormat ? { defaultFormat: cmd.defaultFormat } : {}),
@@ -208,6 +368,7 @@ export function siteHelpData(site, commands) {
208
368
  command_count: unique.length,
209
369
  commands: unique.map(cmd => compactCommand(cmd)),
210
370
  common_options: COMMON_OPTIONS.map(compactCommonOption),
371
+ ...(unique.some(cmd => cmd.browser) ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
211
372
  next: [
212
373
  `opencli ${site} <command> --help -f yaml`,
213
374
  `opencli ${site} <command> -f yaml`,
@@ -219,6 +380,7 @@ export function commandHelpData(cmd) {
219
380
  site: cmd.site,
220
381
  ...compactCommand(cmd),
221
382
  common_options: COMMON_OPTIONS.map(compactCommonOption),
383
+ ...(cmd.browser ? { browser_common_options: BROWSER_COMMON_OPTIONS.map(compactCommonOption) } : {}),
222
384
  output_formats: ['table', 'plain', 'yaml', 'json', 'md', 'csv'],
223
385
  };
224
386
  }
@@ -249,6 +411,15 @@ export function formatCommonOptionsHelpText() {
249
411
  });
250
412
  return ['Common options:', ...formatRows(rows)].join('\n');
251
413
  }
414
+ export function formatBrowserCommonOptionsHelpText() {
415
+ const rows = BROWSER_COMMON_OPTIONS.map(option => {
416
+ const details = [option.help];
417
+ if ('choices' in option)
418
+ details.push(`choices: ${option.choices.join(', ')}`);
419
+ return [option.flags, details.join(' ')];
420
+ });
421
+ return ['Browser common options:', ...formatRows(rows)].join('\n');
422
+ }
252
423
  export function formatSiteHelpText(site, commands) {
253
424
  const unique = [...new Map(commands.map(cmd => [fullName(cmd), cmd])).values()]
254
425
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -261,6 +432,7 @@ export function formatSiteHelpText(site, commands) {
261
432
  ...formatRows(unique.map(cmd => [formatCommandListTerm(cmd), formatSiteCommandDescription(cmd)])),
262
433
  '',
263
434
  formatCommonOptionsHelpText(),
435
+ ...(unique.some(cmd => cmd.browser) ? ['', formatBrowserCommonOptionsHelpText()] : []),
264
436
  '',
265
437
  `Agent tip: use 'opencli ${site} --help -f yaml' to get all command args/options in one structured response.`,
266
438
  '',
@@ -289,6 +461,8 @@ export function formatCommandHelpText(cmd) {
289
461
  lines.push('Command options:', ...formatRows(optionRows), '');
290
462
  }
291
463
  lines.push(formatCommonOptionsHelpText(), '');
464
+ if (cmd.browser)
465
+ lines.push(formatBrowserCommonOptionsHelpText(), '');
292
466
  const meta = [];
293
467
  meta.push(`Access: ${cmd.access}`);
294
468
  meta.push(`Browser: ${cmd.browser ? 'yes' : 'no'}`);
package/dist/src/main.js CHANGED
@@ -27,21 +27,11 @@ const __dirname = path.dirname(__filename);
27
27
  // Use findPackageRoot so the path works both in dev (src/main.ts) and prod (dist/src/main.js).
28
28
  const BUILTIN_CLIS = path.join(findPackageRoot(__filename), 'clis');
29
29
  const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
30
- // ── Session lifecycle flags ──────────────────────────────────────────────
31
- // `--live` / `--focus` / `--reuse` are top-level-ish toggles that tweak the automation
32
- // window's lifecycle. We strip them from argv before Commander runs so they
33
- // can be placed anywhere and work on any subcommand (adapter or browser).
30
+ // ── Browser reuse flag ───────────────────────────────────────────────────
31
+ // `--reuse` is a runtime-level browser session override rather than an adapter
32
+ // arg. Strip it before Commander runs and expose it through an explicit env
33
+ // name. Window/keep-tab are registered on browser-backed commands directly.
34
34
  {
35
- const liveIdx = process.argv.indexOf('--live');
36
- if (liveIdx !== -1) {
37
- process.env.OPENCLI_LIVE = '1';
38
- process.argv.splice(liveIdx, 1);
39
- }
40
- const focusIdx = process.argv.indexOf('--focus');
41
- if (focusIdx !== -1) {
42
- process.env.OPENCLI_WINDOW_FOCUSED = '1';
43
- process.argv.splice(focusIdx, 1);
44
- }
45
35
  const reuseIdx = process.argv.findIndex(arg => arg === '--reuse' || arg.startsWith('--reuse='));
46
36
  if (reuseIdx !== -1) {
47
37
  const arg = process.argv[reuseIdx];
@@ -6,6 +6,7 @@ import type { IPage } from './types.js';
6
6
  export declare function getBrowserFactory(site?: string): new () => IBrowserFactory;
7
7
  export declare const DEFAULT_BROWSER_CONNECT_TIMEOUT: number;
8
8
  export declare const DEFAULT_BROWSER_COMMAND_TIMEOUT: number;
9
+ export type BrowserWindowMode = 'foreground' | 'background';
9
10
  /**
10
11
  * Timeout with seconds unit. Used for high-level command timeouts.
11
12
  */
@@ -28,6 +29,7 @@ export interface IBrowserFactory {
28
29
  cdpEndpoint?: string;
29
30
  contextId?: string;
30
31
  idleTimeout?: number;
32
+ windowMode?: BrowserWindowMode;
31
33
  }): Promise<IPage>;
32
34
  close(): Promise<void>;
33
35
  }
@@ -36,4 +38,5 @@ export declare function browserSession<T>(BrowserFactory: new () => IBrowserFact
36
38
  cdpEndpoint?: string;
37
39
  contextId?: string;
38
40
  idleTimeout?: number;
41
+ windowMode?: BrowserWindowMode;
39
42
  }): Promise<T>;
@@ -54,6 +54,7 @@ export async function browserSession(BrowserFactory, fn, opts = {}) {
54
54
  cdpEndpoint: opts.cdpEndpoint,
55
55
  contextId: opts.contextId,
56
56
  idleTimeout: opts.idleTimeout,
57
+ windowMode: opts.windowMode,
57
58
  });
58
59
  return await fn(page);
59
60
  }