@jackwener/opencli 0.4.2 → 0.4.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 (64) hide show
  1. package/CLI-CREATOR.md +10 -10
  2. package/LICENSE +28 -0
  3. package/README.md +113 -63
  4. package/README.zh-CN.md +114 -63
  5. package/SKILL.md +21 -4
  6. package/dist/browser.d.ts +21 -2
  7. package/dist/browser.js +269 -15
  8. package/dist/browser.test.d.ts +1 -0
  9. package/dist/browser.test.js +43 -0
  10. package/dist/build-manifest.js +4 -0
  11. package/dist/cli-manifest.json +279 -3
  12. package/dist/clis/boss/search.js +186 -30
  13. package/dist/clis/twitter/delete.d.ts +1 -0
  14. package/dist/clis/twitter/delete.js +73 -0
  15. package/dist/clis/twitter/followers.d.ts +1 -0
  16. package/dist/clis/twitter/followers.js +104 -0
  17. package/dist/clis/twitter/following.d.ts +1 -0
  18. package/dist/clis/twitter/following.js +90 -0
  19. package/dist/clis/twitter/like.d.ts +1 -0
  20. package/dist/clis/twitter/like.js +69 -0
  21. package/dist/clis/twitter/notifications.d.ts +1 -0
  22. package/dist/clis/twitter/notifications.js +109 -0
  23. package/dist/clis/twitter/post.d.ts +1 -0
  24. package/dist/clis/twitter/post.js +63 -0
  25. package/dist/clis/twitter/reply.d.ts +1 -0
  26. package/dist/clis/twitter/reply.js +57 -0
  27. package/dist/clis/v2ex/daily.d.ts +1 -0
  28. package/dist/clis/v2ex/daily.js +98 -0
  29. package/dist/clis/v2ex/me.d.ts +1 -0
  30. package/dist/clis/v2ex/me.js +99 -0
  31. package/dist/clis/v2ex/notifications.d.ts +1 -0
  32. package/dist/clis/v2ex/notifications.js +72 -0
  33. package/dist/doctor.d.ts +50 -0
  34. package/dist/doctor.js +372 -0
  35. package/dist/doctor.test.d.ts +1 -0
  36. package/dist/doctor.test.js +114 -0
  37. package/dist/main.js +47 -5
  38. package/dist/output.test.d.ts +1 -0
  39. package/dist/output.test.js +20 -0
  40. package/dist/registry.d.ts +4 -0
  41. package/dist/registry.js +1 -0
  42. package/dist/runtime.d.ts +3 -1
  43. package/dist/runtime.js +2 -2
  44. package/package.json +2 -2
  45. package/src/browser.test.ts +51 -0
  46. package/src/browser.ts +318 -22
  47. package/src/build-manifest.ts +4 -0
  48. package/src/clis/boss/search.ts +196 -29
  49. package/src/clis/twitter/delete.ts +78 -0
  50. package/src/clis/twitter/followers.ts +119 -0
  51. package/src/clis/twitter/following.ts +105 -0
  52. package/src/clis/twitter/like.ts +74 -0
  53. package/src/clis/twitter/notifications.ts +119 -0
  54. package/src/clis/twitter/post.ts +68 -0
  55. package/src/clis/twitter/reply.ts +62 -0
  56. package/src/clis/v2ex/daily.ts +105 -0
  57. package/src/clis/v2ex/me.ts +103 -0
  58. package/src/clis/v2ex/notifications.ts +77 -0
  59. package/src/doctor.test.ts +133 -0
  60. package/src/doctor.ts +424 -0
  61. package/src/main.ts +47 -4
  62. package/src/output.test.ts +27 -0
  63. package/src/registry.ts +5 -0
  64. package/src/runtime.ts +2 -1
@@ -408,9 +408,44 @@
408
408
  {
409
409
  "name": "city",
410
410
  "type": "str",
411
- "default": "101010100",
411
+ "default": "北京",
412
412
  "required": false,
413
- "help": "City code (101010100=北京, 101020100=上海, 101210100=杭州, 101280100=广州)"
413
+ "help": "City name or code (e.g. 杭州, 上海, 101010100)"
414
+ },
415
+ {
416
+ "name": "experience",
417
+ "type": "str",
418
+ "default": "",
419
+ "required": false,
420
+ "help": "Experience: 应届/1年以内/1-3年/3-5年/5-10年/10年以上"
421
+ },
422
+ {
423
+ "name": "degree",
424
+ "type": "str",
425
+ "default": "",
426
+ "required": false,
427
+ "help": "Degree: 大专/本科/硕士/博士"
428
+ },
429
+ {
430
+ "name": "salary",
431
+ "type": "str",
432
+ "default": "",
433
+ "required": false,
434
+ "help": "Salary: 3K以下/3-5K/5-10K/10-15K/15-20K/20-30K/30-50K/50K以上"
435
+ },
436
+ {
437
+ "name": "industry",
438
+ "type": "str",
439
+ "default": "",
440
+ "required": false,
441
+ "help": "Industry code or name (e.g. 100020, 互联网)"
442
+ },
443
+ {
444
+ "name": "page",
445
+ "type": "int",
446
+ "default": 1,
447
+ "required": false,
448
+ "help": "Page number"
414
449
  },
415
450
  {
416
451
  "name": "limit",
@@ -427,9 +462,10 @@
427
462
  "name",
428
463
  "salary",
429
464
  "company",
430
- "city",
465
+ "area",
431
466
  "experience",
432
467
  "degree",
468
+ "skills",
433
469
  "boss",
434
470
  "url"
435
471
  ]
@@ -846,6 +882,161 @@
846
882
  ],
847
883
  "type": "yaml"
848
884
  },
885
+ {
886
+ "site": "twitter",
887
+ "name": "delete",
888
+ "description": "Delete a specific tweet by URL",
889
+ "strategy": "ui",
890
+ "browser": true,
891
+ "args": [
892
+ {
893
+ "name": "url",
894
+ "type": "string",
895
+ "required": true,
896
+ "help": "The URL of the tweet to delete"
897
+ }
898
+ ],
899
+ "type": "ts",
900
+ "modulePath": "twitter/delete.js",
901
+ "domain": "x.com",
902
+ "columns": [
903
+ "status",
904
+ "message"
905
+ ]
906
+ },
907
+ {
908
+ "site": "twitter",
909
+ "name": "followers",
910
+ "description": "Get accounts following a Twitter/X user",
911
+ "strategy": "intercept",
912
+ "browser": true,
913
+ "args": [
914
+ {
915
+ "name": "user",
916
+ "type": "string",
917
+ "required": false,
918
+ "help": ""
919
+ },
920
+ {
921
+ "name": "limit",
922
+ "type": "int",
923
+ "default": 50,
924
+ "required": false,
925
+ "help": ""
926
+ }
927
+ ],
928
+ "type": "ts",
929
+ "modulePath": "twitter/followers.js",
930
+ "domain": "x.com",
931
+ "columns": [
932
+ "screen_name",
933
+ "name",
934
+ "bio",
935
+ "followers"
936
+ ]
937
+ },
938
+ {
939
+ "site": "twitter",
940
+ "name": "following",
941
+ "description": "Get accounts a Twitter/X user is following",
942
+ "strategy": "intercept",
943
+ "browser": true,
944
+ "args": [
945
+ {
946
+ "name": "user",
947
+ "type": "string",
948
+ "required": false,
949
+ "help": ""
950
+ },
951
+ {
952
+ "name": "limit",
953
+ "type": "int",
954
+ "default": 50,
955
+ "required": false,
956
+ "help": ""
957
+ }
958
+ ],
959
+ "type": "ts",
960
+ "modulePath": "twitter/following.js",
961
+ "domain": "x.com",
962
+ "columns": [
963
+ "screen_name",
964
+ "name",
965
+ "bio",
966
+ "followers"
967
+ ]
968
+ },
969
+ {
970
+ "site": "twitter",
971
+ "name": "like",
972
+ "description": "Like a specific tweet",
973
+ "strategy": "ui",
974
+ "browser": true,
975
+ "args": [
976
+ {
977
+ "name": "url",
978
+ "type": "string",
979
+ "required": true,
980
+ "help": "The URL of the tweet to like"
981
+ }
982
+ ],
983
+ "type": "ts",
984
+ "modulePath": "twitter/like.js",
985
+ "domain": "x.com",
986
+ "columns": [
987
+ "status",
988
+ "message"
989
+ ]
990
+ },
991
+ {
992
+ "site": "twitter",
993
+ "name": "notifications",
994
+ "description": "Get Twitter/X notifications",
995
+ "strategy": "intercept",
996
+ "browser": true,
997
+ "args": [
998
+ {
999
+ "name": "limit",
1000
+ "type": "int",
1001
+ "default": 20,
1002
+ "required": false,
1003
+ "help": ""
1004
+ }
1005
+ ],
1006
+ "type": "ts",
1007
+ "modulePath": "twitter/notifications.js",
1008
+ "domain": "x.com",
1009
+ "columns": [
1010
+ "id",
1011
+ "action",
1012
+ "author",
1013
+ "text",
1014
+ "url"
1015
+ ]
1016
+ },
1017
+ {
1018
+ "site": "twitter",
1019
+ "name": "post",
1020
+ "description": "Post a new tweet/thread",
1021
+ "strategy": "ui",
1022
+ "browser": true,
1023
+ "args": [
1024
+ {
1025
+ "name": "text",
1026
+ "type": "string",
1027
+ "required": true,
1028
+ "help": "The text content of the tweet"
1029
+ }
1030
+ ],
1031
+ "type": "ts",
1032
+ "modulePath": "twitter/post.js",
1033
+ "domain": "x.com",
1034
+ "columns": [
1035
+ "status",
1036
+ "message",
1037
+ "text"
1038
+ ]
1039
+ },
849
1040
  {
850
1041
  "site": "twitter",
851
1042
  "name": "profile",
@@ -878,6 +1069,35 @@
878
1069
  "url"
879
1070
  ]
880
1071
  },
1072
+ {
1073
+ "site": "twitter",
1074
+ "name": "reply",
1075
+ "description": "Reply to a specific tweet",
1076
+ "strategy": "ui",
1077
+ "browser": true,
1078
+ "args": [
1079
+ {
1080
+ "name": "url",
1081
+ "type": "string",
1082
+ "required": true,
1083
+ "help": "The URL of the tweet to reply to"
1084
+ },
1085
+ {
1086
+ "name": "text",
1087
+ "type": "string",
1088
+ "required": true,
1089
+ "help": "The text content of your reply"
1090
+ }
1091
+ ],
1092
+ "type": "ts",
1093
+ "modulePath": "twitter/reply.js",
1094
+ "domain": "x.com",
1095
+ "columns": [
1096
+ "status",
1097
+ "message",
1098
+ "text"
1099
+ ]
1100
+ },
881
1101
  {
882
1102
  "site": "twitter",
883
1103
  "name": "search",
@@ -975,6 +1195,21 @@
975
1195
  ],
976
1196
  "type": "yaml"
977
1197
  },
1198
+ {
1199
+ "site": "v2ex",
1200
+ "name": "daily",
1201
+ "description": "V2EX 每日签到并领取铜币",
1202
+ "strategy": "cookie",
1203
+ "browser": true,
1204
+ "args": [],
1205
+ "type": "ts",
1206
+ "modulePath": "v2ex/daily.js",
1207
+ "domain": "www.v2ex.com",
1208
+ "columns": [
1209
+ "status",
1210
+ "message"
1211
+ ]
1212
+ },
978
1213
  {
979
1214
  "site": "v2ex",
980
1215
  "name": "hot",
@@ -1055,6 +1290,47 @@
1055
1290
  ],
1056
1291
  "type": "yaml"
1057
1292
  },
1293
+ {
1294
+ "site": "v2ex",
1295
+ "name": "me",
1296
+ "description": "V2EX 获取个人资料 (余额/未读提醒)",
1297
+ "strategy": "cookie",
1298
+ "browser": true,
1299
+ "args": [],
1300
+ "type": "ts",
1301
+ "modulePath": "v2ex/me.js",
1302
+ "domain": "www.v2ex.com",
1303
+ "columns": [
1304
+ "username",
1305
+ "balance",
1306
+ "unread_notifications",
1307
+ "daily_reward_ready"
1308
+ ]
1309
+ },
1310
+ {
1311
+ "site": "v2ex",
1312
+ "name": "notifications",
1313
+ "description": "V2EX 获取提醒 (回复/由于)",
1314
+ "strategy": "cookie",
1315
+ "browser": true,
1316
+ "args": [
1317
+ {
1318
+ "name": "limit",
1319
+ "type": "int",
1320
+ "default": 20,
1321
+ "required": false,
1322
+ "help": "Number of notifications"
1323
+ }
1324
+ ],
1325
+ "type": "ts",
1326
+ "modulePath": "v2ex/notifications.js",
1327
+ "domain": "www.v2ex.com",
1328
+ "columns": [
1329
+ "type",
1330
+ "content",
1331
+ "time"
1332
+ ]
1333
+ },
1058
1334
  {
1059
1335
  "site": "v2ex",
1060
1336
  "name": "topic",
@@ -1,47 +1,203 @@
1
1
  /**
2
2
  * BOSS直聘 job search — browser cookie API.
3
- * Source: bb-sites/boss/search.js
4
3
  */
5
4
  import { cli, Strategy } from '../../registry.js';
5
+ /** City name → BOSS Zhipin city code mapping */
6
+ const CITY_CODES = {
7
+ '全国': '100010000', '北京': '101010100', '上海': '101020100',
8
+ '广州': '101280100', '深圳': '101280600', '杭州': '101210100',
9
+ '成都': '101270100', '南京': '101190100', '武汉': '101200100',
10
+ '西安': '101110100', '苏州': '101190400', '长沙': '101250100',
11
+ '天津': '101030100', '重庆': '101040100', '郑州': '101180100',
12
+ '东莞': '101281600', '青岛': '101120200', '合肥': '101220100',
13
+ '佛山': '101280800', '宁波': '101210400', '厦门': '101230200',
14
+ '大连': '101070200', '珠海': '101280700', '无锡': '101190200',
15
+ '济南': '101120100', '福州': '101230100', '昆明': '101290100',
16
+ '哈尔滨': '101050100', '沈阳': '101070100', '石家庄': '101090100',
17
+ '贵阳': '101260100', '南宁': '101300100', '太原': '101100100',
18
+ '海口': '101310100', '兰州': '101160100', '乌鲁木齐': '101130100',
19
+ '长春': '101060100', '南昌': '101240100', '常州': '101191100',
20
+ '温州': '101210700', '嘉兴': '101210300', '徐州': '101190800',
21
+ '香港': '101320100',
22
+ };
23
+ const EXP_MAP = {
24
+ '不限': '0', '在校/应届': '108', '应届': '108', '1年以内': '101',
25
+ '1-3年': '102', '3-5年': '103', '5-10年': '104', '10年以上': '105',
26
+ };
27
+ const DEGREE_MAP = {
28
+ '不限': '0', '初中及以下': '209', '中专/中技': '208', '高中': '206',
29
+ '大专': '202', '本科': '203', '硕士': '204', '博士': '205',
30
+ };
31
+ const SALARY_MAP = {
32
+ '不限': '0', '3K以下': '401', '3-5K': '402', '5-10K': '403',
33
+ '10-15K': '404', '15-20K': '405', '20-30K': '406', '30-50K': '407', '50K以上': '408',
34
+ };
35
+ const INDUSTRY_MAP = {
36
+ '不限': '0', '互联网': '100020', '电子商务': '100021', '游戏': '100024',
37
+ '人工智能': '100901', '大数据': '100902', '金融': '100101',
38
+ '教育培训': '100200', '医疗健康': '100300',
39
+ };
40
+ function resolveCity(input) {
41
+ if (!input)
42
+ return '101010100';
43
+ if (/^\d+$/.test(input))
44
+ return input;
45
+ if (CITY_CODES[input])
46
+ return CITY_CODES[input];
47
+ for (const [name, code] of Object.entries(CITY_CODES)) {
48
+ if (name.includes(input))
49
+ return code;
50
+ }
51
+ return '101010100';
52
+ }
53
+ function resolveMap(input, map) {
54
+ if (!input)
55
+ return '';
56
+ if (map[input] !== undefined)
57
+ return map[input];
58
+ for (const [key, val] of Object.entries(map)) {
59
+ if (key.includes(input))
60
+ return val;
61
+ }
62
+ return input;
63
+ }
6
64
  cli({
7
65
  site: 'boss',
8
66
  name: 'search',
9
67
  description: 'BOSS直聘搜索职位',
10
68
  domain: 'www.zhipin.com',
11
69
  strategy: Strategy.COOKIE,
70
+ forceExtension: true, // BOSS Zhipin detects CDP mode — must use extension bridge
71
+ browser: true,
12
72
  args: [
13
73
  { name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },
14
- { name: 'city', default: '101010100', help: 'City code (101010100=北京, 101020100=上海, 101210100=杭州, 101280100=广州)' },
74
+ { name: 'city', default: '北京', help: 'City name or code (e.g. 杭州, 上海, 101010100)' },
75
+ { name: 'experience', default: '', help: 'Experience: 应届/1年以内/1-3年/3-5年/5-10年/10年以上' },
76
+ { name: 'degree', default: '', help: 'Degree: 大专/本科/硕士/博士' },
77
+ { name: 'salary', default: '', help: 'Salary: 3K以下/3-5K/5-10K/10-15K/15-20K/20-30K/30-50K/50K以上' },
78
+ { name: 'industry', default: '', help: 'Industry code or name (e.g. 100020, 互联网)' },
79
+ { name: 'page', type: 'int', default: 1, help: 'Page number' },
15
80
  { name: 'limit', type: 'int', default: 15, help: 'Number of results' },
16
81
  ],
17
- columns: ['name', 'salary', 'company', 'city', 'experience', 'degree', 'boss', 'url'],
82
+ columns: ['name', 'salary', 'company', 'area', 'experience', 'degree', 'skills', 'boss', 'url'],
18
83
  func: async (page, kwargs) => {
19
- await page.goto('https://www.zhipin.com');
20
- await page.wait(2);
21
- const data = await page.evaluate(`
22
- (async () => {
23
- const params = new URLSearchParams({
24
- scene: '1', query: '${kwargs.query.replace(/'/g, "\\'")}',
25
- city: '${kwargs.city || '101010100'}', page: '1', pageSize: '15',
26
- experience: '', degree: '', payType: '', partTime: '',
27
- industry: '', scale: '', stage: '', position: '',
28
- jobType: '', salary: '', multiBusinessDistrict: '', multiSubway: ''
29
- });
30
- const resp = await fetch('/wapi/zpgeek/search/joblist.json?' + params.toString(), {credentials: 'include'});
31
- if (!resp.ok) return {error: 'HTTP ' + resp.status};
32
- const d = await resp.json();
33
- if (d.code !== 0) return {error: d.message || 'API error'};
34
- const zpData = d.zpData || {};
35
- return (zpData.jobList || []).map(j => ({
36
- name: j.jobName, salary: j.salaryDesc, company: j.brandName,
37
- city: j.cityName, experience: j.jobExperience, degree: j.jobDegree,
38
- boss: j.bossName + ' · ' + j.bossTitle,
39
- url: j.encryptJobId ? 'https://www.zhipin.com/job_detail/' + j.encryptJobId + '.html' : ''
40
- }));
41
- })()
42
- `);
43
- if (!Array.isArray(data))
44
- return [];
45
- return data.slice(0, kwargs.limit || 15);
84
+ if (!page)
85
+ throw new Error('Browser page required');
86
+ const cityCode = resolveCity(kwargs.city);
87
+ if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {
88
+ console.error(`[opencli:boss] Navigating to set referrer context...`);
89
+ }
90
+ // Navigate to the Web search view first to establish proper referrer context
91
+ // This is a lesson learned from boss-cli: referrer is important
92
+ await page.goto(`https://www.zhipin.com/web/geek/job?query=${encodeURIComponent(kwargs.query)}&city=${cityCode}`);
93
+ // Give the page a tiny bit of time to settle to avoid immediate 403s
94
+ await new Promise(r => setTimeout(r, 1000));
95
+ const expVal = resolveMap(kwargs.experience, EXP_MAP);
96
+ const degreeVal = resolveMap(kwargs.degree, DEGREE_MAP);
97
+ const salaryVal = resolveMap(kwargs.salary, SALARY_MAP);
98
+ const industryVal = resolveMap(kwargs.industry, INDUSTRY_MAP);
99
+ const limit = kwargs.limit || 15;
100
+ let currentPage = kwargs.page || 1;
101
+ let allJobs = [];
102
+ const seenIds = new Set();
103
+ while (allJobs.length < limit) {
104
+ if (allJobs.length > 0) {
105
+ // Human-like pause between page fetches (1-3 seconds)
106
+ await new Promise(r => setTimeout(r, 1000 + Math.random() * 2000));
107
+ }
108
+ const qs = new URLSearchParams({
109
+ scene: '1',
110
+ query: kwargs.query,
111
+ city: cityCode,
112
+ page: String(currentPage),
113
+ pageSize: '15',
114
+ });
115
+ if (expVal)
116
+ qs.set('experience', expVal);
117
+ if (degreeVal)
118
+ qs.set('degree', degreeVal);
119
+ if (salaryVal)
120
+ qs.set('salary', salaryVal);
121
+ if (industryVal)
122
+ qs.set('industry', industryVal);
123
+ const targetUrl = `https://www.zhipin.com/wapi/zpgeek/search/joblist.json?${qs.toString()}`;
124
+ if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {
125
+ console.error(`[opencli:boss] Fetching page ${currentPage}... (current jobs: ${allJobs.length})`);
126
+ }
127
+ const evaluateScript = `
128
+ async () => {
129
+ return new Promise((resolve, reject) => {
130
+ const xhr = new window.XMLHttpRequest();
131
+ xhr.open('GET', '${targetUrl}', true);
132
+ xhr.withCredentials = true;
133
+ xhr.timeout = 15000; // 15s timeout
134
+ xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
135
+ xhr.onload = () => {
136
+ if (xhr.status >= 200 && xhr.status < 300) {
137
+ try {
138
+ resolve(JSON.parse(xhr.responseText));
139
+ } catch (e) {
140
+ reject(new Error('Failed to parse JSON. Raw (200 chars): ' + xhr.responseText.substring(0, 200)));
141
+ }
142
+ } else {
143
+ reject(new Error('XHR HTTP Status: ' + xhr.status));
144
+ }
145
+ };
146
+ xhr.onerror = () => reject(new Error('XHR Network Error'));
147
+ xhr.ontimeout = () => reject(new Error('XHR Timeout'));
148
+ xhr.send();
149
+ });
150
+ }
151
+ `;
152
+ let data;
153
+ try {
154
+ data = await page.evaluate(evaluateScript);
155
+ }
156
+ catch (e) {
157
+ throw new Error('API evaluate failed: ' + e.message);
158
+ }
159
+ if (data.code !== 0) {
160
+ if (data.code === 37) {
161
+ throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
162
+ }
163
+ throw new Error(`BOSS API error: ${data.message || 'Unknown'} (code=${data.code})\nRaw data: ${JSON.stringify(data)}`);
164
+ }
165
+ const zpData = data.zpData || {};
166
+ const batch = zpData.jobList || [];
167
+ if (batch.length === 0) {
168
+ break; // No more results
169
+ }
170
+ let addedInBatch = 0;
171
+ for (const j of batch) {
172
+ if (!j.encryptJobId || seenIds.has(j.encryptJobId))
173
+ continue;
174
+ seenIds.add(j.encryptJobId);
175
+ allJobs.push({
176
+ name: j.jobName,
177
+ salary: j.salaryDesc,
178
+ company: j.brandName,
179
+ area: [j.cityName, j.areaDistrict, j.businessDistrict].filter(Boolean).join('·'),
180
+ experience: j.jobExperience,
181
+ degree: j.jobDegree,
182
+ skills: (j.skills || []).join(','),
183
+ boss: j.bossName + ' · ' + j.bossTitle,
184
+ url: 'https://www.zhipin.com/job_detail/' + j.encryptJobId + '.html',
185
+ });
186
+ addedInBatch++;
187
+ if (allJobs.length >= limit)
188
+ break;
189
+ }
190
+ if (addedInBatch === 0) {
191
+ // Boss API is repeating identical pages, we've hit the pagination limit
192
+ if (process.env.OPENCLI_VERBOSE)
193
+ console.error(`[opencli:boss] API returned duplicate page, stopping pagination at ${allJobs.length} items`);
194
+ break;
195
+ }
196
+ if (!zpData.hasMore) {
197
+ break; // API says no more pages
198
+ }
199
+ currentPage++;
200
+ }
201
+ return allJobs;
46
202
  },
47
203
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ cli({
3
+ site: 'twitter',
4
+ name: 'delete',
5
+ description: 'Delete a specific tweet by URL',
6
+ domain: 'x.com',
7
+ strategy: Strategy.UI, // Utilizes internal DOM flows for interaction
8
+ browser: true,
9
+ args: [
10
+ { name: 'url', type: 'string', required: true, help: 'The URL of the tweet to delete' },
11
+ ],
12
+ columns: ['status', 'message'],
13
+ func: async (page, kwargs) => {
14
+ if (!page)
15
+ throw new Error('Requires browser');
16
+ console.log(`Navigating to tweet: ${kwargs.url}`);
17
+ await page.goto(kwargs.url);
18
+ await page.wait(5); // Wait for tweet to load completely
19
+ const result = await page.evaluate(`(async () => {
20
+ try {
21
+ // Wait for caret button (which has 'More' aria-label) within the main tweet body
22
+ // Getting the first 'More' usually corresponds to the main displayed tweet of the URL
23
+ const moreMenu = document.querySelector('[aria-label="More"]');
24
+ if (!moreMenu) {
25
+ return { ok: false, message: 'Could not find the "More" context menu on this tweet. Are you sure you are logged in and looking at a valid tweet?' };
26
+ }
27
+
28
+ // Click the 'More' 3 dots button to open the dropdown menu
29
+ moreMenu.click();
30
+ await new Promise(r => setTimeout(r, 1000));
31
+
32
+ // Wait for dropdown pop-out to appear and look for the 'Delete' option
33
+ const items = document.querySelectorAll('[role="menuitem"]');
34
+ let deleteBtn = null;
35
+ for (const item of items) {
36
+ if (item.textContent.includes('Delete') && !item.textContent.includes('List')) {
37
+ deleteBtn = item;
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (!deleteBtn) {
43
+ // If there's no Delete button, it's not our tweet OR localization is not English.
44
+ // Assuming English default for now.
45
+ return { ok: false, message: 'This tweet does not seem to belong to you, or the Delete option is missing (not your tweet).' };
46
+ }
47
+
48
+ // Click Delete
49
+ deleteBtn.click();
50
+ await new Promise(r => setTimeout(r, 1000));
51
+
52
+ // Find and click the confirmation 'Delete' prompt inside the modal
53
+ const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
54
+ if (confirmBtn) {
55
+ confirmBtn.click();
56
+ return { ok: true, message: 'Tweet successfully deleted.' };
57
+ } else {
58
+ return { ok: false, message: 'Delete confirmation dialog did not appear.' };
59
+ }
60
+ } catch (e) {
61
+ return { ok: false, message: e.toString() };
62
+ }
63
+ })()`);
64
+ if (result.ok) {
65
+ // Wait for the deletion request to be processed
66
+ await page.wait(2);
67
+ }
68
+ return [{
69
+ status: result.ok ? 'success' : 'failed',
70
+ message: result.message
71
+ }];
72
+ }
73
+ });
@@ -0,0 +1 @@
1
+ export {};