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 +1 -0
- package/dist/client/index.html +14 -0
- package/dist/server/index.js +145 -8
- package/dist/server/terminal-qr.js +17 -314
- package/package.json +3 -1
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
|
}
|
package/dist/client/index.html
CHANGED
|
@@ -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>
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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, '&')
|
|
1173
|
+
.replace(/</g, '<')
|
|
1174
|
+
.replace(/>/g, '>')
|
|
1175
|
+
.replace(/"/g, '"')
|
|
1176
|
+
.replace(/'/g, ''');
|
|
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:
|
|
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
|
-
|
|
1336
|
-
|
|
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], {
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
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",
|