cc-wechat 0.1.0

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 (48) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/.mcp.json +8 -0
  3. package/.pace/stop-block-count +1 -0
  4. package/LICENSE +21 -0
  5. package/README.md +83 -0
  6. package/dist/auth.d.ts +18 -0
  7. package/dist/auth.js +351 -0
  8. package/dist/auth.js.map +1 -0
  9. package/dist/cdn.d.ts +39 -0
  10. package/dist/cdn.js +228 -0
  11. package/dist/cdn.js.map +1 -0
  12. package/dist/cli.d.ts +5 -0
  13. package/dist/cli.js +127 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/ilink-api.d.ts +33 -0
  16. package/dist/ilink-api.js +206 -0
  17. package/dist/ilink-api.js.map +1 -0
  18. package/dist/patch.d.ts +7 -0
  19. package/dist/patch.js +165 -0
  20. package/dist/patch.js.map +1 -0
  21. package/dist/server.d.ts +6 -0
  22. package/dist/server.js +406 -0
  23. package/dist/server.js.map +1 -0
  24. package/dist/store.d.ts +24 -0
  25. package/dist/store.js +57 -0
  26. package/dist/store.js.map +1 -0
  27. package/dist/text-utils.d.ts +7 -0
  28. package/dist/text-utils.js +56 -0
  29. package/dist/text-utils.js.map +1 -0
  30. package/dist/types.d.ts +98 -0
  31. package/dist/types.js +13 -0
  32. package/dist/types.js.map +1 -0
  33. package/package.json +24 -0
  34. package/packages/cc-channel-patch/README.md +36 -0
  35. package/packages/cc-channel-patch/index.mjs +228 -0
  36. package/packages/cc-channel-patch/package.json +11 -0
  37. package/skills/configure/SKILL.md +32 -0
  38. package/src/auth.ts +400 -0
  39. package/src/cdn.ts +261 -0
  40. package/src/cli.ts +121 -0
  41. package/src/ilink-api.ts +279 -0
  42. package/src/patch.ts +182 -0
  43. package/src/qrcode-terminal.d.ts +10 -0
  44. package/src/server.ts +445 -0
  45. package/src/store.ts +62 -0
  46. package/src/text-utils.ts +56 -0
  47. package/src/types.ts +94 -0
  48. package/tsconfig.json +17 -0
package/src/cli.ts ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cc-wechat CLI 入口 — install/login/status/help 命令
4
+ */
5
+
6
+ import { execSync } from 'node:child_process';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { loginTerminal } from './auth.js';
10
+ import { saveAccount, getActiveAccount } from './store.js';
11
+
12
+ // ─── help ────────────────────────────────────────────
13
+
14
+ /** 打印帮助信息 */
15
+ function help(): void {
16
+ console.log(`
17
+ cc-wechat — 微信 Claude Code Channel 插件
18
+
19
+ 用法: npx cc-wechat <命令>
20
+
21
+ 命令:
22
+ install 注册 MCP server + 扫码登录
23
+ patch 修补 Claude Code 以启用 Channels 功能
24
+ unpatch 恢复原始 Claude Code
25
+ login 重新扫码登录
26
+ status 查看连接状态
27
+ help 显示帮助
28
+ `);
29
+ }
30
+
31
+ // ─── install ─────────────────────────────────────────
32
+
33
+ /** 注册 MCP server + 扫码登录 */
34
+ async function install(): Promise<void> {
35
+ console.log('\n🔧 cc-wechat 安装向导\n');
36
+
37
+ // [1/3] 注册 MCP server
38
+ console.log('[1/3] 注册 MCP server...');
39
+ const serverPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'server.js');
40
+
41
+ try {
42
+ execSync('claude mcp add -s user wechat-channel node ' + serverPath, { stdio: 'pipe' });
43
+ console.log(' ✅ MCP server 已注册');
44
+ } catch {
45
+ try {
46
+ execSync('claude mcp remove wechat-channel', { stdio: 'pipe' });
47
+ execSync('claude mcp add -s user wechat-channel node ' + serverPath, { stdio: 'pipe' });
48
+ console.log(' ✅ MCP server 已重新注册');
49
+ } catch (e) {
50
+ console.error(' ❌ MCP server 注册失败:', (e as Error).message);
51
+ }
52
+ }
53
+
54
+ // [2/3] 微信扫码登录
55
+ console.log('\n[2/3] 微信扫码登录...');
56
+ const existing = getActiveAccount();
57
+ if (existing) {
58
+ console.log(' ⏭️ 已有登录账号,跳过扫码。如需重新登录请运行: npx cc-wechat login');
59
+ } else {
60
+ const { token, accountId, baseUrl } = await loginTerminal();
61
+ saveAccount({
62
+ token,
63
+ baseUrl: baseUrl ?? '',
64
+ botId: accountId.replace(/@/g, '-').replace(/\./g, '-'),
65
+ savedAt: new Date().toISOString(),
66
+ });
67
+ console.log(' ✅ 登录成功');
68
+ }
69
+
70
+ // [3/3] 完成
71
+ console.log('\n[3/3] 安装完成!');
72
+ console.log('\n启动 Claude Code 时使用:');
73
+ console.log(' claude --dangerously-load-development-channels server:wechat-channel\n');
74
+ }
75
+
76
+ // ─── login ───────────────────────────────────────────
77
+
78
+ /** 重新扫码登录 */
79
+ async function login(): Promise<void> {
80
+ console.log('\n🔑 微信扫码登录\n');
81
+ const { token, accountId, baseUrl } = await loginTerminal();
82
+ saveAccount({
83
+ token,
84
+ baseUrl: baseUrl ?? '',
85
+ botId: accountId.replace(/@/g, '-').replace(/\./g, '-'),
86
+ savedAt: new Date().toISOString(),
87
+ });
88
+ console.log('\n✅ 登录成功!账号已保存。\n');
89
+ }
90
+
91
+ // ─── status ──────────────────────────────────────────
92
+
93
+ /** 查看连接状态 */
94
+ function status(): void {
95
+ const account = getActiveAccount();
96
+ if (account) {
97
+ console.log('\n📋 当前账号状态:\n');
98
+ console.log(` botId: ${account.botId.substring(0, 12)}...`);
99
+ console.log(` baseUrl: ${account.baseUrl}`);
100
+ console.log(` savedAt: ${account.savedAt}\n`);
101
+ } else {
102
+ console.log('\n❌ 尚未登录。请运行: npx cc-wechat install\n');
103
+ }
104
+ }
105
+
106
+ // ─── main ────────────────────────────────────────────
107
+
108
+ const command = process.argv[2];
109
+ switch (command) {
110
+ case 'install': case 'setup': install(); break;
111
+ case 'login': login(); break;
112
+ case 'patch': case 'unpatch': {
113
+ // 动态导入 patch 模块,传递命令
114
+ process.argv[2] = command;
115
+ await import('./patch.js');
116
+ break;
117
+ }
118
+ case 'status': status(); break;
119
+ case 'help': case '--help': case '-h': case undefined: help(); break;
120
+ default: console.error(`未知命令: ${command}`); help(); process.exit(1);
121
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * cc-wechat iLink Bot API 封装 — 7 个 HTTP API
3
+ */
4
+ import { randomBytes } from 'node:crypto';
5
+ import type {
6
+ BaseInfo,
7
+ QRCodeResponse,
8
+ QRStatusResponse,
9
+ GetUpdatesResp,
10
+ GetConfigResp,
11
+ GetUploadUrlResp,
12
+ } from './types.js';
13
+
14
+ // 默认 iLink 服务地址
15
+ export const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
16
+ const DEFAULT_BOT_TYPE = '3';
17
+ const LONG_POLL_TIMEOUT_MS = 35_000;
18
+
19
+ /** 构造 base_info 通用字段 */
20
+ export function buildBaseInfo(): BaseInfo {
21
+ return { channel_version: '0.1.0' };
22
+ }
23
+
24
+ /** 生成随机 wechat uin(4 字节随机数 → 十进制 → base64) */
25
+ export function randomWechatUin(): string {
26
+ const num = randomBytes(4).readUInt32BE(0);
27
+ const str = num.toString(10);
28
+ return Buffer.from(str, 'utf-8').toString('base64');
29
+ }
30
+
31
+ /** 构造请求头 */
32
+ export function buildHeaders(
33
+ token?: string,
34
+ body?: string,
35
+ ): Record<string, string> {
36
+ const headers: Record<string, string> = {
37
+ 'Content-Type': 'application/json',
38
+ 'AuthorizationType': 'ilink_bot_token',
39
+ 'X-WECHAT-UIN': randomWechatUin(),
40
+ };
41
+ if (token) {
42
+ headers['Authorization'] = `Bearer ${token}`;
43
+ }
44
+ if (body) {
45
+ headers['Content-Length'] = String(Buffer.byteLength(body, 'utf-8'));
46
+ }
47
+ return headers;
48
+ }
49
+
50
+ /** 通用 HTTP 请求 */
51
+ export async function apiFetch(params: {
52
+ baseUrl?: string;
53
+ endpoint: string;
54
+ body?: string;
55
+ token?: string;
56
+ timeoutMs: number;
57
+ label: string;
58
+ method?: string;
59
+ extraHeaders?: Record<string, string>;
60
+ }): Promise<string> {
61
+ const {
62
+ endpoint,
63
+ body,
64
+ token,
65
+ timeoutMs,
66
+ label,
67
+ method = 'POST',
68
+ } = params;
69
+
70
+ // 确保 base URL 末尾有 /
71
+ let base = params.baseUrl ?? DEFAULT_BASE_URL;
72
+ if (!base.endsWith('/')) base += '/';
73
+
74
+ const url = `${base}${endpoint}`;
75
+ const isGet = method === 'GET';
76
+
77
+ // GET 请求不传 body 和 Content-Type/Content-Length
78
+ const headers: Record<string, string> = isGet
79
+ ? {
80
+ 'AuthorizationType': 'ilink_bot_token',
81
+ 'X-WECHAT-UIN': randomWechatUin(),
82
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
83
+ }
84
+ : buildHeaders(token, body);
85
+
86
+ // 合并额外请求头
87
+ if (params.extraHeaders) {
88
+ Object.assign(headers, params.extraHeaders);
89
+ }
90
+
91
+ const controller = new AbortController();
92
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
93
+
94
+ try {
95
+ const resp = await fetch(url, {
96
+ method,
97
+ headers,
98
+ ...(isGet ? {} : { body }),
99
+ signal: controller.signal,
100
+ });
101
+ const text = await resp.text();
102
+ if (!resp.ok) {
103
+ throw new Error(`[${label}] HTTP ${resp.status}: ${text}`);
104
+ }
105
+ return text;
106
+ } finally {
107
+ clearTimeout(timer);
108
+ }
109
+ }
110
+
111
+ // ─── QR 登录 API(GET 请求) ─────────────────────────
112
+
113
+ /** 获取登录二维码 */
114
+ export async function getQRCode(
115
+ baseUrl?: string,
116
+ ): Promise<QRCodeResponse> {
117
+ const text = await apiFetch({
118
+ baseUrl,
119
+ endpoint: `ilink/bot/get_bot_qrcode?bot_type=${DEFAULT_BOT_TYPE}`,
120
+ timeoutMs: 10_000,
121
+ label: 'getQRCode',
122
+ method: 'GET',
123
+ });
124
+ return JSON.parse(text) as QRCodeResponse;
125
+ }
126
+
127
+ /** 轮询二维码扫描状态(长轮询,35s 超时) */
128
+ export async function pollQRStatus(
129
+ qrcode: string,
130
+ baseUrl?: string,
131
+ ): Promise<QRStatusResponse> {
132
+ try {
133
+ const text = await apiFetch({
134
+ baseUrl,
135
+ endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
136
+ timeoutMs: LONG_POLL_TIMEOUT_MS,
137
+ label: 'pollQRStatus',
138
+ method: 'GET',
139
+ extraHeaders: { 'iLink-App-ClientVersion': '1' },
140
+ });
141
+ return JSON.parse(text) as QRStatusResponse;
142
+ } catch (err: unknown) {
143
+ if (err instanceof Error && err.name === 'AbortError') {
144
+ return { status: 'wait' };
145
+ }
146
+ throw err;
147
+ }
148
+ }
149
+
150
+ // ─── 消息 API(POST 请求) ───────────────────────────
151
+
152
+ /** 长轮询获取新消息 */
153
+ export async function getUpdates(
154
+ token: string,
155
+ buf: string,
156
+ baseUrl?: string,
157
+ timeoutMs?: number,
158
+ ): Promise<GetUpdatesResp> {
159
+ const body = JSON.stringify({
160
+ get_updates_buf: buf,
161
+ base_info: buildBaseInfo(),
162
+ });
163
+ try {
164
+ const text = await apiFetch({
165
+ baseUrl,
166
+ endpoint: 'ilink/bot/getupdates',
167
+ body,
168
+ token,
169
+ timeoutMs: timeoutMs ?? LONG_POLL_TIMEOUT_MS,
170
+ label: 'getUpdates',
171
+ });
172
+ return JSON.parse(text) as GetUpdatesResp;
173
+ } catch (err: unknown) {
174
+ if (err instanceof Error && err.name === 'AbortError') {
175
+ return { ret: 0, msgs: [], get_updates_buf: buf };
176
+ }
177
+ throw err;
178
+ }
179
+ }
180
+
181
+ /** 发送文本消息,返回 client_id */
182
+ export async function sendMessage(
183
+ token: string,
184
+ to: string,
185
+ text: string,
186
+ contextToken: string,
187
+ baseUrl?: string,
188
+ ): Promise<string> {
189
+ const clientId = `cc-wechat-${randomBytes(4).toString('hex')}`;
190
+ const body = JSON.stringify({
191
+ msg: {
192
+ from_user_id: '',
193
+ to_user_id: to,
194
+ client_id: clientId,
195
+ message_type: 2,
196
+ message_state: 2,
197
+ item_list: [{ type: 1, text_item: { text } }],
198
+ context_token: contextToken,
199
+ },
200
+ base_info: buildBaseInfo(),
201
+ });
202
+ await apiFetch({
203
+ baseUrl,
204
+ endpoint: 'ilink/bot/sendmessage',
205
+ body,
206
+ token,
207
+ timeoutMs: 10_000,
208
+ label: 'sendMessage',
209
+ });
210
+ return clientId;
211
+ }
212
+
213
+ /** 发送输入状态指示 */
214
+ export async function sendTyping(
215
+ token: string,
216
+ userId: string,
217
+ ticket: string,
218
+ status: number,
219
+ baseUrl?: string,
220
+ ): Promise<void> {
221
+ const body = JSON.stringify({
222
+ user_id: userId,
223
+ typing_ticket: ticket,
224
+ typing_status: status,
225
+ base_info: buildBaseInfo(),
226
+ });
227
+ await apiFetch({
228
+ baseUrl,
229
+ endpoint: 'ilink/bot/sendtyping',
230
+ body,
231
+ token,
232
+ timeoutMs: 5_000,
233
+ label: 'sendTyping',
234
+ });
235
+ }
236
+
237
+ /** 获取配置(typing ticket 等) */
238
+ export async function getConfig(
239
+ token: string,
240
+ userId: string,
241
+ contextToken?: string,
242
+ baseUrl?: string,
243
+ ): Promise<GetConfigResp> {
244
+ const body = JSON.stringify({
245
+ user_id: userId,
246
+ ...(contextToken ? { context_token: contextToken } : {}),
247
+ base_info: buildBaseInfo(),
248
+ });
249
+ const text = await apiFetch({
250
+ baseUrl,
251
+ endpoint: 'ilink/bot/getconfig',
252
+ body,
253
+ token,
254
+ timeoutMs: 10_000,
255
+ label: 'getConfig',
256
+ });
257
+ return JSON.parse(text) as GetConfigResp;
258
+ }
259
+
260
+ /** 获取文件上传地址 */
261
+ export async function getUploadUrl(
262
+ token: string,
263
+ params: Record<string, unknown>,
264
+ baseUrl?: string,
265
+ ): Promise<GetUploadUrlResp> {
266
+ const body = JSON.stringify({
267
+ ...params,
268
+ base_info: buildBaseInfo(),
269
+ });
270
+ const text = await apiFetch({
271
+ baseUrl,
272
+ endpoint: 'ilink/bot/getuploadurl',
273
+ body,
274
+ token,
275
+ timeoutMs: 10_000,
276
+ label: 'getUploadUrl',
277
+ });
278
+ return JSON.parse(text) as GetUploadUrlResp;
279
+ }
package/src/patch.ts ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cc-wechat 补丁工具
4
+ * 绕过 Claude Code 的 Channels 云控检查(tengu_harbor feature flag + accessToken auth)
5
+ * 用法:node dist/patch.js 或 npx cc-wechat patch
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { homedir } from 'node:os';
11
+
12
+ // 补丁定义:[特征字符串, 替换字符串, 说明]
13
+ const PATCHES: Array<[string, string, string]> = [
14
+ // 1. Channels feature flag 云控
15
+ [
16
+ 'function PaH(){return lA("tengu_harbor",!1)}',
17
+ 'function PaH(){return !0 }',
18
+ 'Channels feature flag (tengu_harbor)',
19
+ ],
20
+ // 2. S1_ gate auth 检查
21
+ [
22
+ 'if(!yf()?.accessToken)',
23
+ 'if( false )',
24
+ 'Channel gate accessToken check',
25
+ ],
26
+ // 3. UI 层 noAuth 检查
27
+ [
28
+ 'noAuth:!yf()?.accessToken',
29
+ 'noAuth: false ',
30
+ 'UI noAuth display check',
31
+ ],
32
+ ];
33
+
34
+ /** 查找 claude.exe 路径 */
35
+ function findClaudeExe(): string | null {
36
+ const candidates = [
37
+ path.join(homedir(), '.local', 'bin', 'claude.exe'),
38
+ path.join(homedir(), '.local', 'bin', 'claude'),
39
+ // npm 全局安装
40
+ path.join(homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
41
+ ];
42
+
43
+ for (const p of candidates) {
44
+ if (fs.existsSync(p)) return p;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function patch(): void {
50
+ console.log('\n cc-wechat patch — Claude Code Channels 补丁\n');
51
+
52
+ const exePath = findClaudeExe();
53
+ if (!exePath) {
54
+ console.error(' 找不到 Claude Code。请确认已安装。');
55
+ process.exit(1);
56
+ }
57
+ console.log(` 目标: ${exePath}`);
58
+
59
+ // 读取二进制
60
+ const buf = fs.readFileSync(exePath);
61
+ let totalPatched = 0;
62
+ let alreadyPatched = 0;
63
+
64
+ for (const [original, replacement, desc] of PATCHES) {
65
+ if (original.length !== replacement.length) {
66
+ console.error(` 错误: "${desc}" 长度不匹配 (${original.length} vs ${replacement.length})`);
67
+ process.exit(1);
68
+ }
69
+
70
+ const origBuf = Buffer.from(original);
71
+ const patchBuf = Buffer.from(replacement);
72
+
73
+ // 搜索所有出现位置
74
+ let pos = 0;
75
+ let count = 0;
76
+ let alreadyCount = 0;
77
+
78
+ while (true) {
79
+ const idx = buf.indexOf(origBuf, pos);
80
+ if (idx === -1) break;
81
+ patchBuf.copy(buf, idx);
82
+ count++;
83
+ pos = idx + 1;
84
+ }
85
+
86
+ // 检查是否已经 patch 过
87
+ pos = 0;
88
+ while (true) {
89
+ const idx = buf.indexOf(patchBuf, pos);
90
+ if (idx === -1) break;
91
+ alreadyCount++;
92
+ pos = idx + 1;
93
+ }
94
+
95
+ if (count > 0) {
96
+ console.log(` [PATCH] ${desc}: ${count} 处已修补`);
97
+ totalPatched += count;
98
+ } else if (alreadyCount > 0) {
99
+ console.log(` [SKIP] ${desc}: 已修补过 (${alreadyCount} 处)`);
100
+ alreadyPatched += alreadyCount;
101
+ } else {
102
+ console.log(` [WARN] ${desc}: 未找到特征字符串(CC 版本可能已更新)`);
103
+ }
104
+ }
105
+
106
+ if (totalPatched === 0 && alreadyPatched > 0) {
107
+ console.log('\n 所有补丁已生效,无需操作。\n');
108
+ return;
109
+ }
110
+
111
+ if (totalPatched === 0) {
112
+ console.error('\n 未找到任何可修补的位置。Claude Code 版本可能不兼容。\n');
113
+ process.exit(1);
114
+ }
115
+
116
+ // 备份
117
+ const backupPath = exePath + '.bak';
118
+ if (!fs.existsSync(backupPath)) {
119
+ fs.copyFileSync(exePath, backupPath);
120
+ console.log(` [BACKUP] 已备份到 ${backupPath}`);
121
+ }
122
+
123
+ // 写入 — 先写临时文件再替换
124
+ const tmpPath = exePath + '.patched';
125
+ fs.writeFileSync(tmpPath, buf);
126
+
127
+ console.log(`\n 补丁已写入 ${tmpPath}`);
128
+ console.log(' 请关闭所有 Claude Code 进程后手动替换:\n');
129
+
130
+ if (process.platform === 'win32') {
131
+ const dir = path.dirname(exePath);
132
+ const name = path.basename(exePath);
133
+ console.log(` cd ${dir}`);
134
+ console.log(` Move-Item ${name} ${name}.old -Force`);
135
+ console.log(` Move-Item ${name}.patched ${name} -Force\n`);
136
+ } else {
137
+ console.log(` mv "${exePath}" "${exePath}.old"`);
138
+ console.log(` mv "${tmpPath}" "${exePath}"\n`);
139
+ }
140
+
141
+ console.log(` 恢复方法: 用 ${backupPath} 替换即可\n`);
142
+ }
143
+
144
+ function unpatch(): void {
145
+ console.log('\n cc-wechat unpatch — 恢复原始 Claude Code\n');
146
+
147
+ const exePath = findClaudeExe();
148
+ if (!exePath) {
149
+ console.error(' 找不到 Claude Code。');
150
+ process.exit(1);
151
+ }
152
+
153
+ const backupPath = exePath + '.bak';
154
+ if (!fs.existsSync(backupPath)) {
155
+ console.error(` 未找到备份文件 ${backupPath}`);
156
+ process.exit(1);
157
+ }
158
+
159
+ const tmpPath = exePath + '.restore';
160
+ fs.copyFileSync(backupPath, tmpPath);
161
+ console.log(` 已准备恢复文件 ${tmpPath}`);
162
+ console.log(' 请关闭所有 Claude Code 进程后手动替换:\n');
163
+
164
+ if (process.platform === 'win32') {
165
+ const dir = path.dirname(exePath);
166
+ const name = path.basename(exePath);
167
+ console.log(` cd ${dir}`);
168
+ console.log(` Move-Item ${name} ${name}.patched -Force`);
169
+ console.log(` Move-Item ${name}.restore ${name} -Force\n`);
170
+ } else {
171
+ console.log(` mv "${exePath}" "${exePath}.patched"`);
172
+ console.log(` mv "${tmpPath}" "${exePath}"\n`);
173
+ }
174
+ }
175
+
176
+ // 入口
177
+ const cmd = process.argv[2];
178
+ if (cmd === 'unpatch' || cmd === 'restore') {
179
+ unpatch();
180
+ } else {
181
+ patch();
182
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * qrcode-terminal 类型声明
3
+ */
4
+ declare module 'qrcode-terminal' {
5
+ export function generate(
6
+ text: string,
7
+ opts: { small: boolean },
8
+ cb: (qr: string) => void,
9
+ ): void;
10
+ }