@jackwener/opencli 0.7.0 → 0.7.3

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 (50) hide show
  1. package/LICENSE +190 -28
  2. package/README.md +6 -5
  3. package/README.zh-CN.md +5 -4
  4. package/SKILL.md +18 -4
  5. package/dist/browser.js +2 -3
  6. package/dist/cli-manifest.json +195 -22
  7. package/dist/clis/linkedin/search.d.ts +1 -0
  8. package/dist/clis/linkedin/search.js +366 -0
  9. package/dist/clis/reddit/read.d.ts +1 -0
  10. package/dist/clis/reddit/read.js +184 -0
  11. package/dist/clis/youtube/transcript-group.d.ts +44 -0
  12. package/dist/clis/youtube/transcript-group.js +226 -0
  13. package/dist/clis/youtube/transcript-group.test.d.ts +1 -0
  14. package/dist/clis/youtube/transcript-group.test.js +99 -0
  15. package/dist/clis/youtube/transcript.d.ts +1 -0
  16. package/dist/clis/youtube/transcript.js +264 -0
  17. package/dist/clis/youtube/utils.d.ts +8 -0
  18. package/dist/clis/youtube/utils.js +28 -0
  19. package/dist/clis/youtube/video.d.ts +1 -0
  20. package/dist/clis/youtube/video.js +114 -0
  21. package/dist/engine.js +2 -1
  22. package/dist/main.js +10 -2
  23. package/dist/output.js +2 -1
  24. package/dist/registry.d.ts +1 -8
  25. package/dist/snapshotFormatter.d.ts +9 -0
  26. package/dist/snapshotFormatter.js +352 -15
  27. package/dist/snapshotFormatter.test.d.ts +7 -0
  28. package/dist/snapshotFormatter.test.js +521 -0
  29. package/dist/validate.d.ts +14 -2
  30. package/dist/verify.d.ts +14 -2
  31. package/package.json +2 -2
  32. package/src/browser.ts +2 -4
  33. package/src/clis/linkedin/search.ts +416 -0
  34. package/src/clis/reddit/read.ts +186 -0
  35. package/src/clis/youtube/transcript-group.test.ts +108 -0
  36. package/src/clis/youtube/transcript-group.ts +287 -0
  37. package/src/clis/youtube/transcript.ts +280 -0
  38. package/src/clis/youtube/utils.ts +28 -0
  39. package/src/clis/youtube/video.ts +116 -0
  40. package/src/engine.ts +4 -1
  41. package/src/main.ts +10 -2
  42. package/src/output.ts +2 -1
  43. package/src/registry.ts +1 -8
  44. package/src/snapshotFormatter.test.ts +579 -0
  45. package/src/snapshotFormatter.ts +399 -13
  46. package/src/validate.ts +19 -4
  47. package/src/verify.ts +17 -3
  48. package/vitest.config.ts +15 -1
  49. package/dist/clis/reddit/read.yaml +0 -76
  50. package/src/clis/reddit/read.yaml +0 -76
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared YouTube utilities — URL parsing, video ID extraction, etc.
3
+ */
4
+
5
+ /**
6
+ * Extract a YouTube video ID from a URL or bare video ID string.
7
+ * Supports: watch?v=, youtu.be/, /shorts/, /embed/, /live/, /v/
8
+ */
9
+ export function parseVideoId(input: string): string {
10
+ if (!input.startsWith('http')) return input;
11
+
12
+ try {
13
+ const parsed = new URL(input);
14
+ if (parsed.searchParams.has('v')) {
15
+ return parsed.searchParams.get('v')!;
16
+ }
17
+ if (parsed.hostname === 'youtu.be') {
18
+ return parsed.pathname.slice(1).split('/')[0];
19
+ }
20
+ // Handle /shorts/xxx, /embed/xxx, /live/xxx, /v/xxx
21
+ const pathMatch = parsed.pathname.match(/^\/(shorts|embed|live|v)\/([^/?]+)/);
22
+ if (pathMatch) return pathMatch[2];
23
+ } catch {
24
+ // Not a valid URL — treat entire input as video ID
25
+ }
26
+
27
+ return input;
28
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * YouTube video metadata — read ytInitialPlayerResponse + ytInitialData from video page.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { parseVideoId } from './utils.js';
6
+
7
+ cli({
8
+ site: 'youtube',
9
+ name: 'video',
10
+ description: 'Get YouTube video metadata (title, views, description, etc.)',
11
+ domain: 'www.youtube.com',
12
+ strategy: Strategy.COOKIE,
13
+ args: [
14
+ { name: 'url', required: true, help: 'YouTube video URL or video ID' },
15
+ ],
16
+ columns: ['field', 'value'],
17
+ func: async (page, kwargs) => {
18
+ const videoId = parseVideoId(kwargs.url);
19
+ const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
20
+ await page.goto(videoUrl);
21
+ await page.wait(3);
22
+
23
+ const data = await page.evaluate(`
24
+ (async () => {
25
+ const player = window.ytInitialPlayerResponse;
26
+ const yt = window.ytInitialData;
27
+ if (!player) return { error: 'ytInitialPlayerResponse not found' };
28
+
29
+ const details = player.videoDetails || {};
30
+ const microformat = player.microformat?.playerMicroformatRenderer || {};
31
+
32
+ // Try to get full description from ytInitialData
33
+ let fullDescription = details.shortDescription || '';
34
+ try {
35
+ const contents = yt?.contents?.twoColumnWatchNextResults
36
+ ?.results?.results?.contents;
37
+ if (contents) {
38
+ for (const c of contents) {
39
+ const desc = c.videoSecondaryInfoRenderer?.attributedDescription?.content;
40
+ if (desc) { fullDescription = desc; break; }
41
+ }
42
+ }
43
+ } catch {}
44
+
45
+ // Get like count if available
46
+ let likes = '';
47
+ try {
48
+ const contents = yt?.contents?.twoColumnWatchNextResults
49
+ ?.results?.results?.contents;
50
+ if (contents) {
51
+ for (const c of contents) {
52
+ const buttons = c.videoPrimaryInfoRenderer?.videoActions
53
+ ?.menuRenderer?.topLevelButtons;
54
+ if (buttons) {
55
+ for (const b of buttons) {
56
+ const toggle = b.segmentedLikeDislikeButtonViewModel
57
+ ?.likeButtonViewModel?.likeButtonViewModel?.toggleButtonViewModel
58
+ ?.toggleButtonViewModel?.defaultButtonViewModel?.buttonViewModel;
59
+ if (toggle?.title) { likes = toggle.title; break; }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ } catch {}
65
+
66
+ // Get publish date
67
+ const publishDate = microformat.publishDate
68
+ || microformat.uploadDate
69
+ || details.publishDate || '';
70
+
71
+ // Get category
72
+ const category = microformat.category || '';
73
+
74
+ // Get channel subscriber count if available
75
+ let subscribers = '';
76
+ try {
77
+ const contents = yt?.contents?.twoColumnWatchNextResults
78
+ ?.results?.results?.contents;
79
+ if (contents) {
80
+ for (const c of contents) {
81
+ const owner = c.videoSecondaryInfoRenderer?.owner
82
+ ?.videoOwnerRenderer?.subscriberCountText?.simpleText;
83
+ if (owner) { subscribers = owner; break; }
84
+ }
85
+ }
86
+ } catch {}
87
+
88
+ return {
89
+ title: details.title || '',
90
+ channel: details.author || '',
91
+ channelId: details.channelId || '',
92
+ videoId: details.videoId || '',
93
+ views: details.viewCount || '',
94
+ likes,
95
+ subscribers,
96
+ duration: details.lengthSeconds ? details.lengthSeconds + 's' : '',
97
+ publishDate,
98
+ category,
99
+ description: fullDescription,
100
+ keywords: (details.keywords || []).join(', '),
101
+ isLive: details.isLiveContent || false,
102
+ thumbnail: details.thumbnail?.thumbnails?.slice(-1)?.[0]?.url || '',
103
+ };
104
+ })()
105
+ `);
106
+
107
+ if (!data || typeof data !== 'object') throw new Error('Failed to extract video metadata from page');
108
+ if (data.error) throw new Error(data.error);
109
+
110
+ // Return as field/value pairs for table display
111
+ return Object.entries(data).map(([field, value]) => ({
112
+ field,
113
+ value: String(value),
114
+ }));
115
+ },
116
+ });
package/src/engine.ts CHANGED
@@ -101,7 +101,10 @@ async function discoverClisFromFs(dir: string): Promise<void> {
101
101
  const filePath = path.join(siteDir, file);
102
102
  if (file.endsWith('.yaml') || file.endsWith('.yml')) {
103
103
  registerYamlCli(filePath, site);
104
- } else if (file.endsWith('.js') && !file.endsWith('.d.js')) {
104
+ } else if (
105
+ (file.endsWith('.js') && !file.endsWith('.d.js')) ||
106
+ (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))
107
+ ) {
105
108
  promises.push(
106
109
  import(`file://${filePath}`).catch((err: any) => {
107
110
  process.stderr.write(`Warning: failed to load module ${filePath}: ${err.message}\n`);
package/src/main.ts CHANGED
@@ -62,10 +62,18 @@ program.command('list').description('List all available CLI commands').option('-
62
62
  });
63
63
 
64
64
  program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name')
65
- .action(async (target) => { const { validateClisWithTarget, renderValidationReport } = await import('./validate.js'); console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target))); });
65
+ .action(async (target) => {
66
+ const { validateClisWithTarget, renderValidationReport } = await import('./validate.js');
67
+ console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target)));
68
+ });
66
69
 
67
70
  program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false)
68
- .action(async (target, opts) => { const { verifyClis, renderVerifyReport } = await import('./verify.js'); const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke }); console.log(renderVerifyReport(r)); process.exitCode = r.ok ? 0 : 1; });
71
+ .action(async (target, opts) => {
72
+ const { verifyClis, renderVerifyReport } = await import('./verify.js');
73
+ const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
74
+ console.log(renderVerifyReport(r));
75
+ process.exitCode = r.ok ? 0 : 1;
76
+ });
69
77
 
70
78
  program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
71
79
  .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
package/src/output.ts CHANGED
@@ -82,7 +82,8 @@ function renderCsv(data: any, opts: RenderOptions): void {
82
82
  for (const row of rows) {
83
83
  console.log(columns.map(c => {
84
84
  const v = String(row[c] ?? '');
85
- return v.includes(',') || v.includes('"') ? `"${v.replace(/"/g, '""')}"` : v;
85
+ return v.includes(',') || v.includes('"') || v.includes('\n')
86
+ ? `"${v.replace(/"/g, '""')}"` : v;
86
87
  }).join(','));
87
88
  }
88
89
  }
package/src/registry.ts CHANGED
@@ -42,18 +42,11 @@ export interface InternalCliCommand extends CliCommand {
42
42
  _lazy?: boolean;
43
43
  _modulePath?: string;
44
44
  }
45
- export interface CliOptions {
45
+ export interface CliOptions extends Partial<Omit<CliCommand, 'args' | 'description'>> {
46
46
  site: string;
47
47
  name: string;
48
48
  description?: string;
49
- domain?: string;
50
- strategy?: Strategy;
51
- browser?: boolean;
52
49
  args?: Arg[];
53
- columns?: string[];
54
- func?: (page: IPage, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
55
- pipeline?: any[];
56
- timeoutSeconds?: number;
57
50
  }
58
51
  const _registry = new Map<string, CliCommand>();
59
52