tt-help-cli-ycl 1.4.26 → 1.4.28

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * raw-sync CLI 入口
3
3
  *
4
- * 启动 raw_jobs 同步程序(独立子进程)。
4
+ * 启动 raw_jobs 同步与任务补充程序(独立子进程)。
5
5
  */
6
6
 
7
7
  import { spawn } from "child_process";
@@ -16,10 +16,7 @@ export function handleRawSync(parsed) {
16
16
 
17
17
  if (parsed.rawSyncInterval != null)
18
18
  args.push("--interval", String(parsed.rawSyncInterval));
19
- if (parsed.rawSyncThreshold != null)
20
- args.push("--threshold", String(parsed.rawSyncThreshold));
21
- if (parsed.rawSyncBatch != null)
22
- args.push("--batch", String(parsed.rawSyncBatch));
19
+ if (parsed.rawSyncLlm) args.push("--llm");
23
20
  if (parsed.rawSyncOnce) args.push("--once");
24
21
 
25
22
  console.error(`[raw-sync] 启动同步程序: ${SCRIPT}`);
@@ -0,0 +1,160 @@
1
+ /**
2
+ * handleSearch — 基于 productive 标签搜索 TikTok 作者
3
+ *
4
+ * 流程:
5
+ * 1. GET /api/tags/search-task → 领取标签
6
+ * 2. POST /api/tags/search-claim → 锁定标签
7
+ * 3. collectSearchAuthors() → 并行搜索 5 个 tab
8
+ * 4. POST /api/tags/search-result → 提交结果
9
+ */
10
+
11
+ import { randomUUID } from "crypto";
12
+ import { ensureBrowserReady } from "../lib/browser/cdp.js";
13
+ import { getOrCreatePage, safeCheckLogin } from "../lib/browser/page.js";
14
+ import { collectSearchAuthors } from "../lib/search-collector.js";
15
+ import {
16
+ server as cfgServer,
17
+ proxy as configuredProxy,
18
+ } from "../lib/constants.js";
19
+
20
+ const DEFAULT_SERVER = cfgServer || "http://127.0.0.1:3000";
21
+
22
+ export async function handleSearch(parsed) {
23
+ const { search: searchOpts } = parsed;
24
+ const { serverUrl, port, proxy: cliProxy } = searchOpts || {};
25
+
26
+ const baseUrl = serverUrl || DEFAULT_SERVER;
27
+ const cdpPort = port || 3333;
28
+ const effectiveProxy = cliProxy || configuredProxy;
29
+
30
+ const log = (...args) => process.stderr.write(args.join(" ") + "\n");
31
+
32
+ log("");
33
+ log("╔════════════════════════════════════╗");
34
+ log("║ TikTok 搜索作者采集(标签驱动)");
35
+ log("║ 服务端: " + baseUrl);
36
+ log(
37
+ "║ CDP 端口: " +
38
+ cdpPort +
39
+ (effectiveProxy ? " 代理: " + effectiveProxy : ""),
40
+ );
41
+ log("║ score>70 的 productive 标签");
42
+ log("╚════════════════════════════════════╝");
43
+ log("");
44
+
45
+ // 连接 CDP 浏览器
46
+ const cdpOpts = { port: cdpPort };
47
+ if (effectiveProxy) cdpOpts.proxyServer = effectiveProxy;
48
+ const browser = await ensureBrowserReady(cdpOpts);
49
+ const page = await getOrCreatePage(browser);
50
+
51
+ // 先导航到 TikTok 页面,再检测登录(参照 explore.js 模式)
52
+ log("导航到 TikTok...");
53
+ try {
54
+ await page.goto("https://www.tiktok.com/@ycl5007", {
55
+ waitUntil: "domcontentloaded",
56
+ timeout: 30000,
57
+ });
58
+ } catch (e) {
59
+ log(`⚠️ 导航失败: ${e.message}`);
60
+ }
61
+
62
+ const loggedIn = await safeCheckLogin(page);
63
+ if (!loggedIn) {
64
+ log("❌ 未登录 TikTok,请在浏览器中登录后重试");
65
+ await browser.close();
66
+ process.exit(1);
67
+ }
68
+ log("✅ 已登录 TikTok");
69
+
70
+ const clientId = randomUUID();
71
+ const clientMeta = { type: "tag-search", port: cdpPort };
72
+ const headers = (extra = {}) => ({
73
+ "X-Client-Id": clientId,
74
+ "X-Client-Info": JSON.stringify(clientMeta),
75
+ ...extra,
76
+ });
77
+ log(`客户端 ID: ${clientId.substring(0, 8)}...`);
78
+ log("");
79
+
80
+ let totalSearched = 0;
81
+ let emptyRounds = 0;
82
+
83
+ const cleanup = () => {
84
+ log("\n正在清理...");
85
+ browser.close().catch(() => {});
86
+ process.exit(0);
87
+ };
88
+ process.on("SIGINT", cleanup);
89
+
90
+ while (true) {
91
+ try {
92
+ // 1. 获取搜索任务
93
+ const taskRes = await fetch(`${baseUrl}/api/tags/search-task`, {
94
+ headers: headers(),
95
+ });
96
+ const task = await taskRes.json();
97
+
98
+ if (!task.tag) {
99
+ emptyRounds++;
100
+ if (emptyRounds >= 5) {
101
+ log(`⏳ 连续 ${emptyRounds} 轮无标签可搜索(30 秒后重试)...`);
102
+ await new Promise((r) => setTimeout(r, 30000));
103
+ emptyRounds = 0;
104
+ continue;
105
+ }
106
+ log(`⏳ 暂无标签可搜索(连续 ${emptyRounds} 轮),10 秒后重试...`);
107
+ await new Promise((r) => setTimeout(r, 10000));
108
+ continue;
109
+ }
110
+ emptyRounds = 0;
111
+
112
+ const tag = task.tag;
113
+ const country = task.country;
114
+ log(
115
+ `[${totalSearched + 1}] 搜索: "${tag}" (score: ${task.score}, 国家: ${country})`,
116
+ );
117
+
118
+ // 2. 锁定标签
119
+ const claimRes = await fetch(`${baseUrl}/api/tags/search-claim`, {
120
+ method: "POST",
121
+ headers: headers({ "Content-Type": "application/json" }),
122
+ body: JSON.stringify({ tag }),
123
+ });
124
+ const claimData = await claimRes.json();
125
+ if (!claimData.ok) {
126
+ log(` ⏭️ ${claimData.error},跳过`);
127
+ continue;
128
+ }
129
+
130
+ // 3. 并行搜索
131
+ const startTime = Date.now();
132
+ const ids = await collectSearchAuthors(browser, tag, { log });
133
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
134
+ log(` 搜索完成: ${ids.length} 个作者 (${elapsed}s)`);
135
+
136
+ // 4. 提交结果
137
+ if (ids.length > 0) {
138
+ const resultRes = await fetch(`${baseUrl}/api/tags/search-result`, {
139
+ method: "POST",
140
+ headers: headers({ "Content-Type": "application/json" }),
141
+ body: JSON.stringify({ tag, authors: ids, country }),
142
+ });
143
+ const result = await resultRes.json();
144
+ log(` 提交: 新增 ${result.pushed} 个, 跳过 ${result.skipped} 个`);
145
+ } else {
146
+ await fetch(`${baseUrl}/api/tags/search-result`, {
147
+ method: "POST",
148
+ headers: headers({ "Content-Type": "application/json" }),
149
+ body: JSON.stringify({ tag, authors: [], country }),
150
+ });
151
+ log(` 提交: 0 个作者`);
152
+ }
153
+
154
+ totalSearched++;
155
+ } catch (e) {
156
+ log(`❌ 异常: ${e.message}`);
157
+ await new Promise((r) => setTimeout(r, 5000));
158
+ }
159
+ }
160
+ }
package/src/lib/args.js CHANGED
@@ -556,6 +556,38 @@ function parseRefreshArgs(args) {
556
556
  };
557
557
  }
558
558
 
559
+ function parseSearchArgs(args) {
560
+ let serverUrl = null;
561
+ let port = 3333;
562
+ let proxy = null;
563
+
564
+ for (let i = 0; i < args.length; i++) {
565
+ const arg = args[i];
566
+ if (arg === "-s" || arg === "--server") {
567
+ serverUrl = args[++i];
568
+ } else if (arg === "--port") {
569
+ port = parseInt(args[++i]) || 3333;
570
+ } else if (arg === "--proxy") {
571
+ proxy = args[++i];
572
+ }
573
+ }
574
+
575
+ return {
576
+ subcommand: "search",
577
+ search: { serverUrl, port, proxy },
578
+ urls: [],
579
+ outputFormat: "json",
580
+ exploreCount: 0,
581
+ showConfig: false,
582
+ showHelp: false,
583
+ customProxy: null,
584
+ configAction: null,
585
+ configValue: null,
586
+ pipeMode: false,
587
+ filterStr: null,
588
+ };
589
+ }
590
+
559
591
  function parseAttachArgs(args) {
560
592
  let parallel = 1;
561
593
  let interval = 10;
@@ -691,7 +723,9 @@ function parseRawSyncArgs(args) {
691
723
  let rawSyncArchive = null;
692
724
  let rawSyncInterval = null;
693
725
  let rawSyncThreshold = null;
694
- let rawSyncBatch = null;
726
+ let rawSyncLlm = false;
727
+ let rawSyncLlmMin = null;
728
+ let rawSyncLlmSample = null;
695
729
  let rawSyncOnce = false;
696
730
 
697
731
  for (let i = 0; i < args.length; i++) {
@@ -702,10 +736,17 @@ function parseRawSyncArgs(args) {
702
736
  rawSyncArchive = args[++i];
703
737
  } else if (arg === "--interval" && i + 1 < args.length) {
704
738
  rawSyncInterval = parseInt(args[++i]) || null;
705
- } else if (arg === "--threshold" && i + 1 < args.length) {
739
+ } else if (
740
+ (arg === "--threshold" || arg === "--max") &&
741
+ i + 1 < args.length
742
+ ) {
706
743
  rawSyncThreshold = parseInt(args[++i]) || null;
707
- } else if (arg === "--batch" && i + 1 < args.length) {
708
- rawSyncBatch = parseInt(args[++i]) || null;
744
+ } else if (arg === "--llm") {
745
+ rawSyncLlm = true;
746
+ } else if (arg === "--llm-min" && i + 1 < args.length) {
747
+ rawSyncLlmMin = parseInt(args[++i]) || null;
748
+ } else if (arg === "--llm-sample" && i + 1 < args.length) {
749
+ rawSyncLlmSample = parseInt(args[++i]) || null;
709
750
  } else if (arg === "--once") {
710
751
  rawSyncOnce = true;
711
752
  }
@@ -717,7 +758,9 @@ function parseRawSyncArgs(args) {
717
758
  rawSyncArchive,
718
759
  rawSyncInterval,
719
760
  rawSyncThreshold,
720
- rawSyncBatch,
761
+ rawSyncLlm,
762
+ rawSyncLlmMin,
763
+ rawSyncLlmSample,
721
764
  rawSyncOnce,
722
765
  urls: [],
723
766
  outputFormat: "json",
@@ -855,8 +898,10 @@ function parseTagArgs(args) {
855
898
  let isScore = false;
856
899
  let isScoreAll = false;
857
900
  let isRefresh = false;
901
+ let isSearch = false;
858
902
  let scoreAllPort = 7222;
859
903
  let refreshPort = 4222;
904
+ let searchPort = 3333;
860
905
  let scoreProxy = null;
861
906
  let scoreTag = null;
862
907
  let scoreCountries = null;
@@ -938,6 +983,8 @@ function parseTagArgs(args) {
938
983
  isScoreAll = true;
939
984
  } else if (cleaned.toLowerCase() === "refresh") {
940
985
  isRefresh = true;
986
+ } else if (cleaned.toLowerCase() === "search") {
987
+ isSearch = true;
941
988
  } else {
942
989
  tags.push(cleaned.toLowerCase());
943
990
  }
@@ -948,6 +995,7 @@ function parseTagArgs(args) {
948
995
  if (rawTagPort !== null && !isNaN(rawTagPort)) {
949
996
  if (isScoreAll) scoreAllPort = rawTagPort;
950
997
  else if (isRefresh) refreshPort = rawTagPort;
998
+ else if (isSearch) searchPort = rawTagPort;
951
999
  else scorePort = rawTagPort;
952
1000
  }
953
1001
 
@@ -1039,6 +1087,27 @@ function parseTagArgs(args) {
1039
1087
  };
1040
1088
  }
1041
1089
 
1090
+ if (isSearch) {
1091
+ return {
1092
+ subcommand: "search",
1093
+ search: {
1094
+ serverUrl,
1095
+ port: searchPort,
1096
+ proxy: scoreProxy,
1097
+ },
1098
+ urls: [],
1099
+ outputFormat: "json",
1100
+ exploreCount: 0,
1101
+ showConfig: false,
1102
+ showHelp: false,
1103
+ customProxy: null,
1104
+ configAction: null,
1105
+ configValue: null,
1106
+ pipeMode: false,
1107
+ filterStr: null,
1108
+ };
1109
+ }
1110
+
1042
1111
  return {
1043
1112
  subcommand: "tag",
1044
1113
  tagTags: {
@@ -1143,6 +1212,10 @@ export function parseArgs() {
1143
1212
  return parseRefreshArgs(args.slice(1));
1144
1213
  }
1145
1214
 
1215
+ if (args.length > 0 && args[0] === "search") {
1216
+ return parseSearchArgs(args.slice(1));
1217
+ }
1218
+
1146
1219
  if (args.length > 0 && args[0] === "raw-sync") {
1147
1220
  return parseRawSyncArgs(args.slice(1));
1148
1221
  }
@@ -314,18 +314,37 @@ const HELP_TEXT = [
314
314
  " tt-help tag refresh -s http://127.0.0.1:3001",
315
315
  " tt-help tag refresh --port 8222 -s http://127.0.0.1:3001",
316
316
  "",
317
+ " search [选项]",
318
+ " 基于 productive 标签搜索 TikTok 作者(score>70 的标签作为搜索词)",
319
+ " 5 个搜索 Tab 并行采集(综合/视频/用户/直播/照片),去重后提交到 job_base",
320
+ " 选项:",
321
+ " -s, --server <URL> 服务端地址(默认 http://127.0.0.1:3000)",
322
+ " --port <N> CDP 端口(默认 3333)",
323
+ " --proxy <地址> 浏览器代理",
324
+ " 行为:",
325
+ " - 从 tags 表取 score>70 的 productive 标签",
326
+ " - 去掉 # 号后作为搜索关键词",
327
+ " - 并行搜索 5 个 tab,去重后提交到 job_base(来源=search)",
328
+ " - 猜测国家取标签 countries 数组的第一个",
329
+ " 示例: tt-help search",
330
+ " tt-help search -s http://127.0.0.1:3001",
331
+ " tt-help search --port 3333 -s http://127.0.0.1:3001",
332
+ " tt-help search --proxy http://127.0.0.1:7890",
333
+ "",
317
334
  " raw-sync --db <主库路径> --archive <归档库路径> [选项]",
318
- " 从归档库向主库 raw_jobs 同步数据,按国家保持最低保有量",
335
+ " 归档新用户 + 按需补充任务(从归档库向主库 jobs 移入)",
319
336
  " 选项:",
320
337
  " --db <路径> 主库路径(必填)",
321
338
  " --archive <路径> 归档库路径(必填)",
322
- " --interval <秒> 检测间隔,默认 30",
323
- " --threshold <条> 每个国家最低保有量,默认 1000",
324
- " --batch <条> 每次搬移上限,默认 500",
339
+ " --interval <秒> 检测间隔,默认 120(2分钟)",
340
+ " --threshold <条> pending 低于此值时触发补任务,默认 50",
341
+ " --llm 启用 LLM 打分模式",
342
+ " --llm-min <分> LLM 最低分阈值,默认 60",
343
+ " --llm-sample <条> LLM 每批采样数,默认 100",
325
344
  " --once 只执行一次后退出",
326
345
  " 示例: tt-help raw-sync --db data/result.db --archive data/result-archive.db",
327
346
  " tt-help raw-sync --db data/result.db --archive data/result-archive.db --once",
328
- " tt-help raw-sync --db data/result.db --archive data/result-archive.db --interval 60 --threshold 2000",
347
+ " tt-help raw-sync --db data/result.db --archive data/result-archive.db --llm --threshold 300",
329
348
  "",
330
349
  " config [show|set|unset|reset]",
331
350
  " config 查看当前配置",
@@ -0,0 +1,194 @@
1
+ /**
2
+ * TikTok 搜索页作者采集 — DOM 滚动版
3
+ *
4
+ * 用法:
5
+ * import { collectSearchAuthors } from "../lib/search-collector.js";
6
+ * const ids = await collectSearchAuthors(browser, "tienda");
7
+ */
8
+
9
+ const TAB_URLS = {
10
+ 综合: (q) => `https://www.tiktok.com/search?q=${encodeURIComponent(q)}`,
11
+ 视频: (q) => `https://www.tiktok.com/search/video?q=${encodeURIComponent(q)}`,
12
+ 用户: (q) => `https://www.tiktok.com/search/user?q=${encodeURIComponent(q)}`,
13
+ 直播: (q) => `https://www.tiktok.com/search/live?q=${encodeURIComponent(q)}`,
14
+ 照片: (q) => `https://www.tiktok.com/search/photo?q=${encodeURIComponent(q)}`,
15
+ };
16
+
17
+ const DEFAULT_TABS = ["综合", "视频", "用户", "直播", "照片"];
18
+
19
+ const SCROLL = {
20
+ maxRounds: 60,
21
+ staleThreshold: 8,
22
+ delayMin: 800,
23
+ delayMax: 1500,
24
+ waitAfterScroll: 4000,
25
+ };
26
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
27
+ const randDelay = () =>
28
+ delay(
29
+ Math.floor(Math.random() * (SCROLL.delayMax - SCROLL.delayMin + 1)) +
30
+ SCROLL.delayMin,
31
+ );
32
+
33
+ async function extractAuthors(page) {
34
+ return page.evaluate(() => {
35
+ const ids = new Set();
36
+ for (const a of document.querySelectorAll('a[href*="/@"]')) {
37
+ const m = (a.getAttribute("href") || "").match(/\/@([^/?]+)/);
38
+ if (m && m[1] !== "undefined" && m[1].length <= 30) ids.add(m[1]);
39
+ }
40
+ return [...ids];
41
+ });
42
+ }
43
+
44
+ async function scrollPage(page) {
45
+ await page.evaluate(() => {
46
+ const main = document.querySelector("main");
47
+ if (main && main.scrollHeight > main.clientHeight + 10) {
48
+ main.scrollBy(0, main.clientHeight * 0.8);
49
+ } else {
50
+ window.scrollBy(0, window.innerHeight * 0.8);
51
+ }
52
+ });
53
+ }
54
+
55
+ async function processOneTab(page, tabName, keyword, allIds, log) {
56
+ const urlFn = TAB_URLS[tabName];
57
+ if (!urlFn) {
58
+ log(`[${tabName}] 不支持`);
59
+ return;
60
+ }
61
+
62
+ const tag = `[${tabName}]`;
63
+ const url = urlFn(keyword);
64
+
65
+ // 首屏正常加载
66
+ log(`${tag} 加载...`);
67
+ try {
68
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
69
+ } catch (e) {
70
+ log(`${tag} 导航失败: ${e.message}`);
71
+ return;
72
+ }
73
+ await delay(4000);
74
+
75
+ // 首屏渲染完成后屏蔽图片/字体/媒体,加速后续滚动
76
+ await page.route("**/*", (route) => {
77
+ const t = route.request().resourceType();
78
+ if (t === "image" || t === "font" || t === "media") route.abort();
79
+ else route.continue();
80
+ });
81
+
82
+ const tabSet = new Set();
83
+ let stale = 0;
84
+
85
+ for (let round = 0; round <= SCROLL.maxRounds; round++) {
86
+ if (round > 0) {
87
+ await scrollPage(page);
88
+ await randDelay();
89
+ await delay(SCROLL.waitAfterScroll);
90
+ }
91
+
92
+ const ids = await extractAuthors(page);
93
+ let added = 0;
94
+ for (const id of ids) {
95
+ if (!tabSet.has(id)) {
96
+ tabSet.add(id);
97
+ added++;
98
+ }
99
+ }
100
+
101
+ if (round === 0) {
102
+ log(`${tag} 初始: ${tabSet.size}`);
103
+ continue;
104
+ }
105
+
106
+ if (added === 0) {
107
+ stale++;
108
+ if (stale >= SCROLL.staleThreshold) {
109
+ log(`${tag} ${SCROLL.staleThreshold}轮无新`);
110
+ break;
111
+ }
112
+ } else {
113
+ stale = 0;
114
+ if (round % 5 === 0 || added >= 5)
115
+ log(`${tag} R${round}: +${added}, 计${tabSet.size}`);
116
+ }
117
+ if (tabSet.size >= 500) break;
118
+ }
119
+
120
+ let added = 0;
121
+ for (const id of tabSet) {
122
+ if (!allIds.has(id)) {
123
+ allIds.add(id);
124
+ added++;
125
+ }
126
+ }
127
+ log(`${tag} 完成: ${tabSet.size}个, 新增${added} (累计${allIds.size})`);
128
+ }
129
+
130
+ /**
131
+ * @param {import("playwright").Browser} browser
132
+ * @param {string} keyword
133
+ * @param {object} [opts]
134
+ * @param {string[]} [opts.tabs]
135
+ * @param {function} [opts.log] - 日志函数,默认 console.error
136
+ * @returns {Promise<string[]>}
137
+ */
138
+ export async function collectSearchAuthors(browser, keyword, opts = {}) {
139
+ const tabs = opts.tabs || DEFAULT_TABS;
140
+ const log = opts.log || ((...args) => console.error(...args));
141
+
142
+ log(`[collect] "${keyword}" Tab: [${tabs.join(", ")}]`);
143
+
144
+ const context = browser.contexts()[0];
145
+ const existingPage = context.pages()[0];
146
+ const allIds = new Set();
147
+
148
+ // 并行:每个 tab 独立页面同时跑
149
+ const newPages = []; // 追踪所有新建页面,确保清理
150
+ const tasks = tabs.map(async (tabName, i) => {
151
+ let page = null;
152
+ try {
153
+ page = i === 0 ? existingPage : await context.newPage();
154
+ if (i > 0) newPages.push(page);
155
+ await processOneTab(page, tabName, keyword, allIds, log);
156
+ } catch (e) {
157
+ log(`[${tabName}] 异常: ${e.message}`);
158
+ } finally {
159
+ if (i > 0 && page) {
160
+ try {
161
+ await page.close();
162
+ } catch {}
163
+ }
164
+ }
165
+ });
166
+
167
+ await Promise.all(tasks);
168
+
169
+ // 兜底:确保所有新建页面都已关闭
170
+ for (const p of newPages) {
171
+ try {
172
+ if (!p.isClosed()) await p.close();
173
+ } catch {}
174
+ }
175
+
176
+ return [...allIds].sort();
177
+ }
178
+
179
+ /**
180
+ * 采集并保存到文件(便捷函数)
181
+ * @param {import("playwright").Browser} browser
182
+ * @param {string} keyword
183
+ * @param {string} outputFile - 输出文件路径
184
+ * @param {object} [opts] - 同 collectSearchAuthors
185
+ * @returns {Promise<string[]>}
186
+ */
187
+ export async function searchAndSave(browser, keyword, outputFile, opts = {}) {
188
+ const fs = await import("fs");
189
+ const path = await import("path");
190
+ const ids = await collectSearchAuthors(browser, keyword, opts);
191
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
192
+ fs.writeFileSync(outputFile, ids.join("\n") + "\n", "utf-8");
193
+ return ids;
194
+ }
@@ -12,6 +12,7 @@ const DEFAULT_TARGET_LOCATIONS = [
12
12
  "IT",
13
13
  "IE",
14
14
  "AT",
15
+ "US",
15
16
  ];
16
17
 
17
18
  const DEFAULT_TARGET_LOCATIONS_CSV = DEFAULT_TARGET_LOCATIONS.join(",");
package/src/main.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  handleTagRefresh,
21
21
  } from "./cli/tag.js";
22
22
  import { handleRawSync } from "./cli/raw-sync.js";
23
+ import { handleSearch } from "./cli/search.js";
23
24
 
24
25
  async function main() {
25
26
  const parsed = parseArgs();
@@ -59,6 +60,8 @@ async function main() {
59
60
  return handleScoreAll(parsed);
60
61
  case "tag-refresh":
61
62
  return handleTagRefresh(parsed);
63
+ case "search":
64
+ return handleSearch(parsed);
62
65
  }
63
66
 
64
67
  const {
package/src/npm-main.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  handleTagRefresh,
17
17
  } from "./cli/tag.js";
18
18
  import { handleRawSync } from "./cli/raw-sync.js";
19
+ import { handleSearch } from "./cli/search.js";
19
20
 
20
21
  function exitUnsupportedCommand(command) {
21
22
  console.error(
@@ -59,6 +60,8 @@ async function main() {
59
60
  return handleTagRefresh(parsed);
60
61
  case "raw-sync":
61
62
  return handleRawSync(parsed);
63
+ case "search":
64
+ return handleSearch(parsed);
62
65
  }
63
66
 
64
67
  const {