cli-link 0.0.4 → 0.0.6

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.
package/bin/agentpilot.js CHANGED
@@ -241,6 +241,7 @@ async function main() {
241
241
  process.env.PORT = port;
242
242
  process.env.HOST = host;
243
243
  process.env.AGENTPILOT_NPX = '1';
244
+ process.env.AGENTPILOT_PACKAGE_VERSION = pkg.version;
244
245
  if (options.noToken && options.token) {
245
246
  throw new Error('--token cannot be used with --no-token');
246
247
  }
@@ -3,13 +3,27 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
+ <meta name="description" content="AgentPilot 是一个移动端优先的 AI 编程 Agent 控制台,可以在手机浏览器里遥控本机 Claude Code CLI 和 Codex CLI。" />
6
7
  <meta name="theme-color" content="#030712" />
7
8
  <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#030712" />
9
+ <meta property="og:type" content="website" />
10
+ <meta property="og:site_name" content="AgentPilot" />
11
+ <meta property="og:title" content="AgentPilot - AI Agent 遥控器" />
12
+ <meta property="og:description" content="在手机浏览器里遥控本机 Claude Code CLI 和 Codex CLI:发任务、看输出、审确认、切换工作目录、查看代码和历史。" />
13
+ <meta property="og:image" content="/icons/icon-512.png" />
14
+ <meta property="og:image:width" content="512" />
15
+ <meta property="og:image:height" content="512" />
16
+ <meta name="twitter:card" content="summary" />
17
+ <meta name="twitter:title" content="AgentPilot - AI Agent 遥控器" />
18
+ <meta name="twitter:description" content="在手机浏览器里遥控本机 Claude Code CLI 和 Codex CLI。" />
19
+ <meta name="twitter:image" content="/icons/icon-512.png" />
8
20
  <meta name="mobile-web-app-capable" content="yes" />
9
21
  <meta name="apple-mobile-web-app-capable" content="yes" />
10
22
  <meta name="apple-mobile-web-app-title" content="AgentPilot" />
11
23
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
12
24
  <link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
25
+ <link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />
26
+ <link rel="icon" type="image/png" sizes="512x512" href="/icons/icon-512.png" />
13
27
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
14
28
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
15
29
  <title>AgentPilot - AI Agent 遥控器</title>
@@ -19,6 +19,11 @@ const MAC_IDLE_SLEEP_PREVENTED = process.env.AGENTPILOT_CAFFEINATE === '1';
19
19
  const AUTH_COOKIE_NAME = 'agentpilot_token';
20
20
  const AUTH_TOKEN = (process.env.AGENTPILOT_AUTH_TOKEN || '').trim();
21
21
  const AUTH_ENABLED = AUTH_TOKEN.length > 0;
22
+ const PACKAGE_VERSION = (process.env.AGENTPILOT_PACKAGE_VERSION || '').trim();
23
+ const AUTH_REQUIRED_MESSAGE = '访问 token 缺失或无效,请使用启动终端打印的链接重新打开。';
24
+ const SHARE_TITLE = 'AgentPilot - AI Agent 遥控器';
25
+ const SHARE_DESCRIPTION = '在手机浏览器里遥控本机 Claude Code CLI 和 Codex CLI:发任务、看输出、审确认、切换工作目录、查看代码和历史。';
26
+ const SHARE_IMAGE_PATH = '/icons/icon-512.png';
22
27
  function resolveEnvPath(value) {
23
28
  if (value === '~')
24
29
  return os.homedir();
@@ -1135,9 +1140,120 @@ function maybeSetAuthCookie(req, res, url) {
1135
1140
  const secure = httpsOptions ? '; Secure' : '';
1136
1141
  appendSetCookie(res, `${AUTH_COOKIE_NAME}=${encodeURIComponent(AUTH_TOKEN)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400${secure}`);
1137
1142
  }
1138
- function sendAuthRequired(res) {
1143
+ function isPublicShareAssetPath(pathname) {
1144
+ let decodedPath;
1145
+ try {
1146
+ decodedPath = decodeURIComponent(pathname);
1147
+ }
1148
+ catch {
1149
+ return false;
1150
+ }
1151
+ if (decodedPath.includes('..') || decodedPath.includes('\\'))
1152
+ return false;
1153
+ return (decodedPath === '/favicon.svg' ||
1154
+ decodedPath === '/favicon.ico' ||
1155
+ /^\/icons\/[A-Za-z0-9._-]+\.(?:png|jpg|jpeg|webp|ico|svg)$/.test(decodedPath));
1156
+ }
1157
+ function shouldSendAuthSharePage(req, url) {
1158
+ if (req.method !== 'GET' && req.method !== 'HEAD')
1159
+ return false;
1160
+ if (url.pathname.startsWith('/api/'))
1161
+ return false;
1162
+ if (isPublicShareAssetPath(url.pathname))
1163
+ return false;
1164
+ const ext = path.extname(url.pathname).toLowerCase();
1165
+ if (ext && ext !== '.html')
1166
+ return false;
1167
+ const accept = getHeaderValue(req.headers.accept).toLowerCase();
1168
+ return !accept || accept.includes('text/html') || accept.includes('*/*');
1169
+ }
1170
+ function escapeHtml(value) {
1171
+ return value
1172
+ .replace(/&/g, '&amp;')
1173
+ .replace(/</g, '&lt;')
1174
+ .replace(/>/g, '&gt;')
1175
+ .replace(/"/g, '&quot;')
1176
+ .replace(/'/g, '&#39;');
1177
+ }
1178
+ function getRequestOrigin(req) {
1179
+ const forwardedProto = getHeaderValue(req.headers['x-forwarded-proto']).split(',')[0]?.trim().toLowerCase();
1180
+ const proto = forwardedProto === 'http' || forwardedProto === 'https' ? forwardedProto : httpScheme;
1181
+ const forwardedHost = getHeaderValue(req.headers['x-forwarded-host']).split(',')[0]?.trim();
1182
+ const host = forwardedHost || getHeaderValue(req.headers.host) || `localhost:${PORT}`;
1183
+ return `${proto}://${host}`;
1184
+ }
1185
+ function toAbsoluteRequestUrl(req, pathname) {
1186
+ try {
1187
+ return new URL(pathname, `${getRequestOrigin(req)}/`).toString();
1188
+ }
1189
+ catch {
1190
+ return pathname;
1191
+ }
1192
+ }
1193
+ function buildAuthRequiredHtml(req, url) {
1194
+ const title = escapeHtml(SHARE_TITLE);
1195
+ const description = escapeHtml(SHARE_DESCRIPTION);
1196
+ const message = escapeHtml(AUTH_REQUIRED_MESSAGE);
1197
+ const imageUrl = escapeHtml(toAbsoluteRequestUrl(req, SHARE_IMAGE_PATH));
1198
+ const pageUrl = escapeHtml(toAbsoluteRequestUrl(req, url.pathname));
1199
+ return `<!DOCTYPE html>
1200
+ <html lang="zh-CN">
1201
+ <head>
1202
+ <meta charset="UTF-8" />
1203
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1204
+ <meta name="description" content="${description}" />
1205
+ <meta name="theme-color" content="#030712" />
1206
+ <meta property="og:type" content="website" />
1207
+ <meta property="og:site_name" content="AgentPilot" />
1208
+ <meta property="og:title" content="${title}" />
1209
+ <meta property="og:description" content="${description}" />
1210
+ <meta property="og:url" content="${pageUrl}" />
1211
+ <meta property="og:image" content="${imageUrl}" />
1212
+ <meta property="og:image:width" content="512" />
1213
+ <meta property="og:image:height" content="512" />
1214
+ <meta name="twitter:card" content="summary" />
1215
+ <meta name="twitter:title" content="${title}" />
1216
+ <meta name="twitter:description" content="${description}" />
1217
+ <meta name="twitter:image" content="${imageUrl}" />
1218
+ <link rel="icon" type="image/png" sizes="512x512" href="${imageUrl}" />
1219
+ <title>${title}</title>
1220
+ <style>
1221
+ html { color-scheme: dark; background: #030712; color: #e5e7eb; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
1222
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; box-sizing: border-box; }
1223
+ main { max-width: 520px; }
1224
+ h1 { margin: 0 0 12px; font-size: 28px; line-height: 1.2; }
1225
+ p { margin: 0; color: #9ca3af; line-height: 1.7; }
1226
+ </style>
1227
+ </head>
1228
+ <body>
1229
+ <main>
1230
+ <h1>${title}</h1>
1231
+ <p>${message}</p>
1232
+ </main>
1233
+ </body>
1234
+ </html>`;
1235
+ }
1236
+ function sendHtml(req, res, status, html) {
1237
+ if (res.writableEnded)
1238
+ return;
1239
+ setCorsHeaders(res);
1240
+ res.writeHead(status, {
1241
+ 'Content-Type': 'text/html; charset=utf-8',
1242
+ 'Cache-Control': 'no-cache',
1243
+ });
1244
+ if (req.method === 'HEAD') {
1245
+ res.end();
1246
+ return;
1247
+ }
1248
+ res.end(html);
1249
+ }
1250
+ function sendAuthRequired(req, res, url) {
1251
+ if (shouldSendAuthSharePage(req, url)) {
1252
+ sendHtml(req, res, 200, buildAuthRequiredHtml(req, url));
1253
+ return;
1254
+ }
1139
1255
  sendJson(res, 401, {
1140
- error: '访问 token 缺失或无效,请使用启动终端打印的链接重新打开。',
1256
+ error: AUTH_REQUIRED_MESSAGE,
1141
1257
  authRequired: true,
1142
1258
  });
1143
1259
  }
@@ -1332,12 +1448,16 @@ async function handleHttpRequest(req, res) {
1332
1448
  res.end();
1333
1449
  return;
1334
1450
  }
1335
- if (!isRequestAuthorized(req, url)) {
1336
- sendAuthRequired(res);
1451
+ const pathname = url.pathname;
1452
+ const authorized = isRequestAuthorized(req, url);
1453
+ if (!authorized && isPublicShareAssetPath(pathname) && serveStaticClient(req, res, pathname)) {
1454
+ return;
1455
+ }
1456
+ if (!authorized) {
1457
+ sendAuthRequired(req, res, url);
1337
1458
  return;
1338
1459
  }
1339
1460
  maybeSetAuthCookie(req, res, url);
1340
- const pathname = url.pathname;
1341
1461
  if (pathname === '/health') {
1342
1462
  sendText(res, 200, 'AgentPilot Server');
1343
1463
  return;
@@ -2182,6 +2302,14 @@ function getNetworkUrls() {
2182
2302
  function shouldUseColor() {
2183
2303
  return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
2184
2304
  }
2305
+ function getQrRenderMode() {
2306
+ const requested = (process.env.AGENTPILOT_QR_STYLE || '').trim().toLowerCase();
2307
+ if (requested === 'compact' || requested === 'small')
2308
+ return 'compact';
2309
+ if (requested === 'large' || requested === 'safe')
2310
+ return 'large';
2311
+ return process.env.TERM_PROGRAM === 'Apple_Terminal' ? 'large' : 'compact';
2312
+ }
2185
2313
  const terminalStyle = {
2186
2314
  reset: '\x1b[0m',
2187
2315
  bold: '\x1b[1m',
@@ -2225,8 +2353,10 @@ function printMacSleepHint() {
2225
2353
  console.log(' 这样电脑接入电源且显示器关闭后,AgentPilot 后端和底层 CLI 更不容易被系统睡眠中断。');
2226
2354
  console.log('');
2227
2355
  }
2228
- function printStartupInfo() {
2356
+ async function printStartupInfo() {
2229
2357
  if (!IS_NPX_ENTRY) {
2358
+ if (PACKAGE_VERSION)
2359
+ console.log(`[AgentPilot] Version ${PACKAGE_VERSION}`);
2230
2360
  console.log(`[AgentPilot] Server running on ${withStartupToken(`${httpScheme}://${HOST}:${PORT}`)}`);
2231
2361
  console.log(`[AgentPilot] WebSocket ready on ${wsScheme}://${HOST}:${PORT}`);
2232
2362
  if (AUTH_ENABLED)
@@ -2246,7 +2376,10 @@ function printStartupInfo() {
2246
2376
  printSection('open on mobile');
2247
2377
  printLine('Mobile', networkUrls[0], true);
2248
2378
  printLine('Scan', 'use the QR code below');
2249
- const qr = renderTerminalQr(networkUrls[0], { color: shouldUseColor() });
2379
+ const qr = await renderTerminalQr(networkUrls[0], {
2380
+ color: shouldUseColor(),
2381
+ mode: getQrRenderMode(),
2382
+ });
2250
2383
  if (qr) {
2251
2384
  console.log('');
2252
2385
  console.log(colorize('QR CODE', terminalStyle.bold, terminalStyle.dim));
@@ -2270,6 +2403,8 @@ function printStartupInfo() {
2270
2403
  printLine('Token', tokenStatus, AUTH_ENABLED);
2271
2404
  console.log('');
2272
2405
  printSection('runtime');
2406
+ if (PACKAGE_VERSION)
2407
+ printLine('Version', PACKAGE_VERSION);
2273
2408
  printLine('Workdir', config.workDir);
2274
2409
  printLine('CLI', `${config.cliCommand} (${config.cliType})`);
2275
2410
  console.log('');
@@ -2287,5 +2422,7 @@ process.on('SIGTERM', () => {
2287
2422
  process.exit(0);
2288
2423
  });
2289
2424
  server.listen(PORT, HOST, () => {
2290
- printStartupInfo();
2425
+ printStartupInfo().catch((err) => {
2426
+ console.warn(`[AgentPilot] Failed to print startup info: ${err?.message || err}`);
2427
+ });
2291
2428
  });
@@ -1,317 +1,20 @@
1
- const QR_TOTAL_CODEWORDS = [0, 26, 44, 70, 100, 134, 172];
2
- const QR_EC_CODEWORDS_PER_BLOCK_M = [0, 10, 16, 26, 18, 24, 16];
3
- const QR_NUM_ERROR_CORRECTION_BLOCKS_M = [0, 1, 1, 1, 2, 2, 4];
4
- const QR_ALIGNMENT_POSITIONS = [
5
- [],
6
- [],
7
- [6, 18],
8
- [6, 22],
9
- [6, 26],
10
- [6, 30],
11
- [6, 34],
12
- ];
13
- class BitBuffer {
14
- bits = [];
15
- append(value, length) {
16
- for (let i = length - 1; i >= 0; i -= 1) {
17
- this.bits.push((value >>> i) & 1);
18
- }
19
- }
20
- toBytes() {
21
- const bytes = [];
22
- for (let i = 0; i < this.bits.length; i += 8) {
23
- let value = 0;
24
- for (let j = 0; j < 8; j += 1) {
25
- value = (value << 1) | (this.bits[i + j] || 0);
26
- }
27
- bytes.push(value);
28
- }
29
- return bytes;
30
- }
31
- }
32
- function getQrDataCodewordCount(version) {
33
- return QR_TOTAL_CODEWORDS[version] - QR_EC_CODEWORDS_PER_BLOCK_M[version] * QR_NUM_ERROR_CORRECTION_BLOCKS_M[version];
34
- }
35
- function selectQrVersion(data) {
36
- for (let version = 1; version < QR_TOTAL_CODEWORDS.length; version += 1) {
37
- const capacityBits = getQrDataCodewordCount(version) * 8;
38
- if (4 + 8 + data.length * 8 <= capacityBits)
39
- return version;
40
- }
41
- return null;
42
- }
43
- function qrMultiply(x, y) {
44
- let z = 0;
45
- for (let i = 7; i >= 0; i -= 1) {
46
- z = (z << 1) ^ ((z >>> 7) * 0x11d);
47
- z ^= ((y >>> i) & 1) * x;
48
- }
49
- return z;
50
- }
51
- function qrReedSolomonDivisor(degree) {
52
- const result = Array(degree).fill(0);
53
- result[degree - 1] = 1;
54
- let root = 1;
55
- for (let i = 0; i < degree; i += 1) {
56
- for (let j = 0; j < result.length; j += 1) {
57
- result[j] = qrMultiply(result[j], root);
58
- if (j + 1 < result.length)
59
- result[j] ^= result[j + 1];
60
- }
61
- root = qrMultiply(root, 0x02);
62
- }
63
- return result;
64
- }
65
- function qrReedSolomonRemainder(data, divisor) {
66
- const result = Array(divisor.length).fill(0);
67
- for (const byte of data) {
68
- const factor = byte ^ (result.shift() || 0);
69
- result.push(0);
70
- for (let i = 0; i < divisor.length; i += 1) {
71
- result[i] ^= qrMultiply(divisor[i], factor);
72
- }
73
- }
74
- return result;
75
- }
76
- function qrAddEccAndInterleave(dataCodewords, version) {
77
- const numBlocks = QR_NUM_ERROR_CORRECTION_BLOCKS_M[version];
78
- const blockEccLen = QR_EC_CODEWORDS_PER_BLOCK_M[version];
79
- const rawCodewords = QR_TOTAL_CODEWORDS[version];
80
- const numShortBlocks = numBlocks - (rawCodewords % numBlocks);
81
- const shortBlockLen = Math.floor(rawCodewords / numBlocks);
82
- const divisor = qrReedSolomonDivisor(blockEccLen);
83
- const blocks = [];
84
- for (let i = 0, offset = 0; i < numBlocks; i += 1) {
85
- const dataLen = shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1);
86
- const data = dataCodewords.slice(offset, offset + dataLen);
87
- offset += dataLen;
88
- const ecc = qrReedSolomonRemainder(data, divisor);
89
- if (i < numShortBlocks)
90
- data.push(0);
91
- blocks.push(data.concat(ecc));
92
- }
93
- const result = [];
94
- for (let i = 0; i < blocks[0].length; i += 1) {
95
- for (let j = 0; j < blocks.length; j += 1) {
96
- if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks) {
97
- result.push(blocks[j][i]);
98
- }
99
- }
100
- }
101
- return result;
102
- }
103
- function qrFormatBits(mask) {
104
- const data = mask; // Error correction level M has format bits 00.
105
- let rem = data;
106
- for (let i = 0; i < 10; i += 1) {
107
- rem = (rem << 1) ^ (((rem >>> 9) & 1) * 0x537);
108
- }
109
- return ((data << 10) | rem) ^ 0x5412;
110
- }
111
- function qrMask(mask, x, y) {
112
- switch (mask) {
113
- case 0: return (x + y) % 2 === 0;
114
- case 1: return y % 2 === 0;
115
- case 2: return x % 3 === 0;
116
- case 3: return (x + y) % 3 === 0;
117
- case 4: return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0;
118
- case 5: return ((x * y) % 2) + ((x * y) % 3) === 0;
119
- case 6: return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
120
- case 7: return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
121
- default: return false;
122
- }
123
- }
124
- function createQrMatrix(text) {
125
- const data = new TextEncoder().encode(text);
126
- const version = selectQrVersion(data);
127
- if (!version)
1
+ import QRCode from 'qrcode';
2
+ export async function renderTerminalQr(text, options = {}) {
3
+ try {
4
+ if (options.color === false) {
5
+ return await QRCode.toString(text, {
6
+ type: 'utf8',
7
+ errorCorrectionLevel: 'M',
8
+ });
9
+ }
10
+ return await QRCode.toString(text, {
11
+ type: 'terminal',
12
+ small: options.mode !== 'large',
13
+ errorCorrectionLevel: 'M',
14
+ });
15
+ }
16
+ catch (err) {
17
+ console.warn(`[AgentPilot] Failed to render terminal QR code: ${err instanceof Error ? err.message : String(err)}`);
128
18
  return null;
129
- const dataCodewordCount = getQrDataCodewordCount(version);
130
- const buffer = new BitBuffer();
131
- buffer.append(0b0100, 4);
132
- buffer.append(data.length, 8);
133
- for (const byte of data)
134
- buffer.append(byte, 8);
135
- const capacityBits = dataCodewordCount * 8;
136
- buffer.append(0, Math.min(4, capacityBits - buffer.bits.length));
137
- while (buffer.bits.length % 8 !== 0)
138
- buffer.append(0, 1);
139
- const dataCodewords = buffer.toBytes();
140
- for (let pad = 0xec; dataCodewords.length < dataCodewordCount; pad ^= 0xec ^ 0x11) {
141
- dataCodewords.push(pad);
142
- }
143
- const codewords = qrAddEccAndInterleave(dataCodewords, version);
144
- const size = version * 4 + 17;
145
- const modules = Array.from({ length: size }, () => Array(size).fill(false));
146
- const isFunction = Array.from({ length: size }, () => Array(size).fill(false));
147
- const setFunction = (x, y, dark) => {
148
- if (x < 0 || y < 0 || x >= size || y >= size)
149
- return;
150
- modules[y][x] = dark;
151
- isFunction[y][x] = true;
152
- };
153
- const drawFinder = (cx, cy) => {
154
- for (let dy = -4; dy <= 4; dy += 1) {
155
- for (let dx = -4; dx <= 4; dx += 1) {
156
- const dist = Math.max(Math.abs(dx), Math.abs(dy));
157
- setFunction(cx + dx, cy + dy, dist !== 2 && dist !== 4);
158
- }
159
- }
160
- };
161
- const drawAlignment = (cx, cy) => {
162
- for (let dy = -2; dy <= 2; dy += 1) {
163
- for (let dx = -2; dx <= 2; dx += 1) {
164
- setFunction(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
165
- }
166
- }
167
- };
168
- const drawFormat = (mask) => {
169
- const bits = qrFormatBits(mask);
170
- for (let i = 0; i <= 5; i += 1)
171
- setFunction(8, i, ((bits >>> i) & 1) !== 0);
172
- setFunction(8, 7, ((bits >>> 6) & 1) !== 0);
173
- setFunction(8, 8, ((bits >>> 7) & 1) !== 0);
174
- setFunction(7, 8, ((bits >>> 8) & 1) !== 0);
175
- for (let i = 9; i < 15; i += 1)
176
- setFunction(14 - i, 8, ((bits >>> i) & 1) !== 0);
177
- for (let i = 0; i < 8; i += 1)
178
- setFunction(size - 1 - i, 8, ((bits >>> i) & 1) !== 0);
179
- for (let i = 8; i < 15; i += 1)
180
- setFunction(8, size - 15 + i, ((bits >>> i) & 1) !== 0);
181
- setFunction(8, size - 8, true);
182
- };
183
- drawFinder(3, 3);
184
- drawFinder(size - 4, 3);
185
- drawFinder(3, size - 4);
186
- const alignment = QR_ALIGNMENT_POSITIONS[version];
187
- for (const x of alignment) {
188
- for (const y of alignment) {
189
- if (isFunction[y]?.[x])
190
- continue;
191
- drawAlignment(x, y);
192
- }
193
- }
194
- for (let i = 8; i < size - 8; i += 1) {
195
- setFunction(6, i, i % 2 === 0);
196
- setFunction(i, 6, i % 2 === 0);
197
- }
198
- drawFormat(0);
199
- let bitIndex = 0;
200
- for (let right = size - 1; right >= 1; right -= 2) {
201
- if (right === 6)
202
- right -= 1;
203
- for (let vert = 0; vert < size; vert += 1) {
204
- const y = ((right + 1) & 2) === 0 ? size - 1 - vert : vert;
205
- for (let j = 0; j < 2; j += 1) {
206
- const x = right - j;
207
- if (isFunction[y][x])
208
- continue;
209
- modules[y][x] = ((codewords[Math.floor(bitIndex / 8)] >>> (7 - (bitIndex % 8))) & 1) !== 0;
210
- bitIndex += 1;
211
- }
212
- }
213
- }
214
- let bestMask = 0;
215
- let bestPenalty = Infinity;
216
- for (let mask = 0; mask < 8; mask += 1) {
217
- const penalty = scoreQrMask(modules, isFunction, mask);
218
- if (penalty < bestPenalty) {
219
- bestMask = mask;
220
- bestPenalty = penalty;
221
- }
222
- }
223
- for (let y = 0; y < size; y += 1) {
224
- for (let x = 0; x < size; x += 1) {
225
- if (!isFunction[y][x] && qrMask(bestMask, x, y))
226
- modules[y][x] = !modules[y][x];
227
- }
228
- }
229
- drawFormat(bestMask);
230
- return modules;
231
- }
232
- function scoreQrMask(modules, isFunction, mask) {
233
- const size = modules.length;
234
- const get = (x, y) => modules[y][x] !== (!isFunction[y][x] && qrMask(mask, x, y));
235
- let penalty = 0;
236
- for (let y = 0; y < size; y += 1) {
237
- let runColor = get(0, y);
238
- let runLen = 1;
239
- for (let x = 1; x < size; x += 1) {
240
- const color = get(x, y);
241
- if (color === runColor) {
242
- runLen += 1;
243
- if (runLen === 5)
244
- penalty += 3;
245
- else if (runLen > 5)
246
- penalty += 1;
247
- }
248
- else {
249
- runColor = color;
250
- runLen = 1;
251
- }
252
- }
253
- }
254
- for (let x = 0; x < size; x += 1) {
255
- let runColor = get(x, 0);
256
- let runLen = 1;
257
- for (let y = 1; y < size; y += 1) {
258
- const color = get(x, y);
259
- if (color === runColor) {
260
- runLen += 1;
261
- if (runLen === 5)
262
- penalty += 3;
263
- else if (runLen > 5)
264
- penalty += 1;
265
- }
266
- else {
267
- runColor = color;
268
- runLen = 1;
269
- }
270
- }
271
- }
272
- let dark = 0;
273
- for (let y = 0; y < size; y += 1) {
274
- for (let x = 0; x < size; x += 1) {
275
- if (get(x, y))
276
- dark += 1;
277
- if (x + 1 < size && y + 1 < size) {
278
- const color = get(x, y);
279
- if (color === get(x + 1, y) && color === get(x, y + 1) && color === get(x + 1, y + 1)) {
280
- penalty += 3;
281
- }
282
- }
283
- }
284
- }
285
- const total = size * size;
286
- penalty += Math.floor(Math.abs(dark * 20 - total * 10) / total) * 10;
287
- return penalty;
288
- }
289
- export function renderTerminalQr(text, options = {}) {
290
- const modules = createQrMatrix(text);
291
- if (!modules)
292
- return null;
293
- const size = modules.length;
294
- const quiet = 2;
295
- const lines = [];
296
- if (options.color === false) {
297
- for (let y = -quiet; y < size + quiet; y += 1) {
298
- let line = '';
299
- for (let x = -quiet; x < size + quiet; x += 1) {
300
- const dark = x >= 0 && x < size && y >= 0 && y < size && modules[y][x];
301
- line += dark ? '██' : ' ';
302
- }
303
- lines.push(line);
304
- }
305
- return lines.join('\n');
306
- }
307
- for (let y = -quiet; y < size + quiet; y += 2) {
308
- let line = '\x1b[30;47m';
309
- for (let x = -quiet; x < size + quiet; x += 1) {
310
- const top = x >= 0 && x < size && y >= 0 && y < size && modules[y][x];
311
- const bottom = x >= 0 && x < size && y + 1 >= 0 && y + 1 < size && modules[y + 1][x];
312
- line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' ';
313
- }
314
- lines.push(`${line}\x1b[0m`);
315
19
  }
316
- return lines.join('\n');
317
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-link",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cli-link": "bin/agentpilot.js"
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "better-sqlite3": "^12.9.0",
35
+ "qrcode": "^1.5.4",
35
36
  "ws": "^8.16.0"
36
37
  },
37
38
  "pnpm": {
@@ -45,6 +46,7 @@
45
46
  "@tanstack/react-virtual": "^3.13.23",
46
47
  "@types/better-sqlite3": "^7.6.13",
47
48
  "@types/node": "^25.6.0",
49
+ "@types/qrcode": "^1.5.6",
48
50
  "@types/react": "^18.2.0",
49
51
  "@types/react-dom": "^18.2.0",
50
52
  "@types/ws": "^8.5.10",