@jackwener/opencli 0.6.3 → 0.7.2

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 (81) hide show
  1. package/LICENSE +190 -28
  2. package/README.md +4 -4
  3. package/README.zh-CN.md +4 -4
  4. package/SKILL.md +22 -6
  5. package/dist/browser.js +2 -3
  6. package/dist/build-manifest.js +2 -0
  7. package/dist/cli-manifest.json +604 -24
  8. package/dist/clis/reddit/comment.d.ts +1 -0
  9. package/dist/clis/reddit/comment.js +57 -0
  10. package/dist/clis/reddit/popular.yaml +40 -0
  11. package/dist/clis/reddit/read.yaml +76 -0
  12. package/dist/clis/reddit/save.d.ts +1 -0
  13. package/dist/clis/reddit/save.js +51 -0
  14. package/dist/clis/reddit/saved.d.ts +1 -0
  15. package/dist/clis/reddit/saved.js +46 -0
  16. package/dist/clis/reddit/search.yaml +37 -11
  17. package/dist/clis/reddit/subreddit.yaml +14 -4
  18. package/dist/clis/reddit/subscribe.d.ts +1 -0
  19. package/dist/clis/reddit/subscribe.js +50 -0
  20. package/dist/clis/reddit/upvote.d.ts +1 -0
  21. package/dist/clis/reddit/upvote.js +64 -0
  22. package/dist/clis/reddit/upvoted.d.ts +1 -0
  23. package/dist/clis/reddit/upvoted.js +46 -0
  24. package/dist/clis/reddit/user-comments.yaml +45 -0
  25. package/dist/clis/reddit/user-posts.yaml +43 -0
  26. package/dist/clis/reddit/user.yaml +39 -0
  27. package/dist/clis/twitter/article.d.ts +1 -0
  28. package/dist/clis/twitter/article.js +157 -0
  29. package/dist/clis/twitter/bookmark.d.ts +1 -0
  30. package/dist/clis/twitter/bookmark.js +63 -0
  31. package/dist/clis/twitter/follow.d.ts +1 -0
  32. package/dist/clis/twitter/follow.js +65 -0
  33. package/dist/clis/twitter/profile.js +110 -42
  34. package/dist/clis/twitter/thread.d.ts +1 -0
  35. package/dist/clis/twitter/thread.js +150 -0
  36. package/dist/clis/twitter/unbookmark.d.ts +1 -0
  37. package/dist/clis/twitter/unbookmark.js +62 -0
  38. package/dist/clis/twitter/unfollow.d.ts +1 -0
  39. package/dist/clis/twitter/unfollow.js +71 -0
  40. package/dist/engine.js +2 -1
  41. package/dist/main.js +41 -10
  42. package/dist/output.js +2 -1
  43. package/dist/registry.d.ts +2 -8
  44. package/dist/snapshotFormatter.d.ts +9 -0
  45. package/dist/snapshotFormatter.js +352 -15
  46. package/dist/snapshotFormatter.test.d.ts +7 -0
  47. package/dist/snapshotFormatter.test.js +521 -0
  48. package/dist/validate.d.ts +14 -2
  49. package/dist/verify.d.ts +14 -2
  50. package/package.json +2 -2
  51. package/src/browser.ts +2 -4
  52. package/src/build-manifest.ts +3 -0
  53. package/src/clis/reddit/comment.ts +60 -0
  54. package/src/clis/reddit/popular.yaml +40 -0
  55. package/src/clis/reddit/read.yaml +76 -0
  56. package/src/clis/reddit/save.ts +54 -0
  57. package/src/clis/reddit/saved.ts +48 -0
  58. package/src/clis/reddit/search.yaml +37 -11
  59. package/src/clis/reddit/subreddit.yaml +14 -4
  60. package/src/clis/reddit/subscribe.ts +53 -0
  61. package/src/clis/reddit/upvote.ts +67 -0
  62. package/src/clis/reddit/upvoted.ts +48 -0
  63. package/src/clis/reddit/user-comments.yaml +45 -0
  64. package/src/clis/reddit/user-posts.yaml +43 -0
  65. package/src/clis/reddit/user.yaml +39 -0
  66. package/src/clis/twitter/article.ts +161 -0
  67. package/src/clis/twitter/bookmark.ts +67 -0
  68. package/src/clis/twitter/follow.ts +69 -0
  69. package/src/clis/twitter/profile.ts +113 -45
  70. package/src/clis/twitter/thread.ts +181 -0
  71. package/src/clis/twitter/unbookmark.ts +66 -0
  72. package/src/clis/twitter/unfollow.ts +75 -0
  73. package/src/engine.ts +4 -1
  74. package/src/main.ts +34 -7
  75. package/src/output.ts +2 -1
  76. package/src/registry.ts +2 -8
  77. package/src/snapshotFormatter.test.ts +579 -0
  78. package/src/snapshotFormatter.ts +399 -13
  79. package/src/validate.ts +19 -4
  80. package/src/verify.ts +17 -3
  81. package/vitest.config.ts +15 -1
@@ -0,0 +1,66 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ cli({
5
+ site: 'twitter',
6
+ name: 'unbookmark',
7
+ description: 'Remove a tweet from bookmarks',
8
+ domain: 'x.com',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'url', type: 'string', positional: true, required: true, help: 'Tweet URL to unbookmark' },
13
+ ],
14
+ columns: ['status', 'message'],
15
+ func: async (page: IPage | null, kwargs: any) => {
16
+ if (!page) throw new Error('Requires browser');
17
+
18
+ await page.goto(kwargs.url);
19
+ await page.wait(5);
20
+
21
+ const result = await page.evaluate(`(async () => {
22
+ try {
23
+ let attempts = 0;
24
+ let removeBtn = null;
25
+
26
+ while (attempts < 20) {
27
+ // Check if not bookmarked
28
+ const bookmarkBtn = document.querySelector('[data-testid="bookmark"]');
29
+ if (bookmarkBtn) {
30
+ return { ok: true, message: 'Tweet is not bookmarked (already removed).' };
31
+ }
32
+
33
+ removeBtn = document.querySelector('[data-testid="removeBookmark"]');
34
+ if (removeBtn) break;
35
+
36
+ await new Promise(r => setTimeout(r, 500));
37
+ attempts++;
38
+ }
39
+
40
+ if (!removeBtn) {
41
+ return { ok: false, message: 'Could not find Remove Bookmark button. Are you logged in?' };
42
+ }
43
+
44
+ removeBtn.click();
45
+ await new Promise(r => setTimeout(r, 1000));
46
+
47
+ // Verify
48
+ const verify = document.querySelector('[data-testid="bookmark"]');
49
+ if (verify) {
50
+ return { ok: true, message: 'Tweet successfully removed from bookmarks.' };
51
+ } else {
52
+ return { ok: false, message: 'Unbookmark action initiated but UI did not update.' };
53
+ }
54
+ } catch (e) {
55
+ return { ok: false, message: e.toString() };
56
+ }
57
+ })()`);
58
+
59
+ if (result.ok) await page.wait(2);
60
+
61
+ return [{
62
+ status: result.ok ? 'success' : 'failed',
63
+ message: result.message
64
+ }];
65
+ }
66
+ });
@@ -0,0 +1,75 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ cli({
5
+ site: 'twitter',
6
+ name: 'unfollow',
7
+ description: 'Unfollow a Twitter user',
8
+ domain: 'x.com',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'username', type: 'string', positional: true, required: true, help: 'Twitter screen name (without @)' },
13
+ ],
14
+ columns: ['status', 'message'],
15
+ func: async (page: IPage | null, kwargs: any) => {
16
+ if (!page) throw new Error('Requires browser');
17
+ const username = kwargs.username.replace(/^@/, '');
18
+
19
+ await page.goto(`https://x.com/${username}`);
20
+ await page.wait(5);
21
+
22
+ const result = await page.evaluate(`(async () => {
23
+ try {
24
+ let attempts = 0;
25
+ let unfollowBtn = null;
26
+
27
+ while (attempts < 20) {
28
+ // Check if already not following
29
+ const followBtn = document.querySelector('[data-testid$="-follow"]');
30
+ if (followBtn) {
31
+ return { ok: true, message: 'Not following @${username} (already unfollowed).' };
32
+ }
33
+
34
+ unfollowBtn = document.querySelector('[data-testid$="-unfollow"]');
35
+ if (unfollowBtn) break;
36
+
37
+ await new Promise(r => setTimeout(r, 500));
38
+ attempts++;
39
+ }
40
+
41
+ if (!unfollowBtn) {
42
+ return { ok: false, message: 'Could not find Unfollow button. Are you logged in?' };
43
+ }
44
+
45
+ // Click the unfollow button — this opens a confirmation dialog
46
+ unfollowBtn.click();
47
+ await new Promise(r => setTimeout(r, 1000));
48
+
49
+ // Confirm the unfollow in the dialog
50
+ const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
51
+ if (confirmBtn) {
52
+ confirmBtn.click();
53
+ await new Promise(r => setTimeout(r, 1000));
54
+ }
55
+
56
+ // Verify
57
+ const verify = document.querySelector('[data-testid$="-follow"]');
58
+ if (verify) {
59
+ return { ok: true, message: 'Successfully unfollowed @${username}.' };
60
+ } else {
61
+ return { ok: false, message: 'Unfollow action initiated but UI did not update.' };
62
+ }
63
+ } catch (e) {
64
+ return { ok: false, message: e.toString() };
65
+ }
66
+ })()`);
67
+
68
+ if (result.ok) await page.wait(2);
69
+
70
+ return [{
71
+ status: result.ok ? 'success' : 'failed',
72
+ message: result.message
73
+ }];
74
+ }
75
+ });
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 }))); });
@@ -129,18 +137,37 @@ for (const [, cmd] of registry) {
129
137
  if (!siteCmd) { siteCmd = program.command(cmd.site).description(`${cmd.site} commands`); siteGroups.set(cmd.site, siteCmd); }
130
138
  const subCmd = siteCmd.command(cmd.name).description(cmd.description);
131
139
 
140
+ // Register positional args first, then named options
141
+ const positionalArgs: typeof cmd.args = [];
132
142
  for (const arg of cmd.args) {
133
- const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
134
- if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
135
- else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
136
- else subCmd.option(flag, arg.help ?? '');
143
+ if (arg.positional) {
144
+ const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
145
+ subCmd.argument(bracket, arg.help ?? '');
146
+ positionalArgs.push(arg);
147
+ } else {
148
+ const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
149
+ if (arg.required) subCmd.requiredOption(flag, arg.help ?? '');
150
+ else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default));
151
+ else subCmd.option(flag, arg.help ?? '');
152
+ }
137
153
  }
138
154
  subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
139
155
 
140
- subCmd.action(async (actionOpts) => {
156
+ subCmd.action(async (...actionArgs: any[]) => {
157
+ // Commander passes positional args first, then options object, then the Command
158
+ const actionOpts = actionArgs[positionalArgs.length] ?? {};
141
159
  const startTime = Date.now();
142
160
  const kwargs: Record<string, any> = {};
161
+ // Collect positional args
162
+ for (let i = 0; i < positionalArgs.length; i++) {
163
+ const arg = positionalArgs[i];
164
+ const v = actionArgs[i];
165
+ if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
166
+ else if (arg.default != null) kwargs[arg.name] = arg.default;
167
+ }
168
+ // Collect named options
143
169
  for (const arg of cmd.args) {
170
+ if (arg.positional) continue;
144
171
  const v = actionOpts[arg.name]; if (v !== undefined) kwargs[arg.name] = coerce(v, arg.type ?? 'str');
145
172
  else if (arg.default != null) kwargs[arg.name] = arg.default;
146
173
  }
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
@@ -17,6 +17,7 @@ export interface Arg {
17
17
  type?: string;
18
18
  default?: any;
19
19
  required?: boolean;
20
+ positional?: boolean;
20
21
  help?: string;
21
22
  choices?: string[];
22
23
  }
@@ -41,18 +42,11 @@ export interface InternalCliCommand extends CliCommand {
41
42
  _lazy?: boolean;
42
43
  _modulePath?: string;
43
44
  }
44
- export interface CliOptions {
45
+ export interface CliOptions extends Partial<Omit<CliCommand, 'args' | 'description'>> {
45
46
  site: string;
46
47
  name: string;
47
48
  description?: string;
48
- domain?: string;
49
- strategy?: Strategy;
50
- browser?: boolean;
51
49
  args?: Arg[];
52
- columns?: string[];
53
- func?: (page: IPage, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
54
- pipeline?: any[];
55
- timeoutSeconds?: number;
56
50
  }
57
51
  const _registry = new Map<string, CliCommand>();
58
52