@poco-ai/tokenarena 0.1.5 → 0.2.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1066 -240
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1129,7 +1129,14 @@ function getRuntimeDir() {
|
|
|
1129
1129
|
var CONFIG_DIR = join9(getConfigHome(), "tokenarena");
|
|
1130
1130
|
var isDev = process.env.TOKEN_ARENA_DEV === "1";
|
|
1131
1131
|
var CONFIG_FILE = join9(CONFIG_DIR, isDev ? "config.dev.json" : "config.json");
|
|
1132
|
-
var DEFAULT_API_URL = "
|
|
1132
|
+
var DEFAULT_API_URL = "https://token.poco-ai.com";
|
|
1133
|
+
var VALID_CONFIG_KEYS = [
|
|
1134
|
+
"apiKey",
|
|
1135
|
+
"apiUrl",
|
|
1136
|
+
"deviceId",
|
|
1137
|
+
"syncInterval",
|
|
1138
|
+
"logLevel"
|
|
1139
|
+
];
|
|
1133
1140
|
function getConfigPath() {
|
|
1134
1141
|
return CONFIG_FILE;
|
|
1135
1142
|
}
|
|
@@ -1168,10 +1175,112 @@ function getOrCreateDeviceId(config) {
|
|
|
1168
1175
|
function validateApiKey(key) {
|
|
1169
1176
|
return key.startsWith("ta_");
|
|
1170
1177
|
}
|
|
1178
|
+
function isValidConfigKey(key) {
|
|
1179
|
+
return VALID_CONFIG_KEYS.includes(key);
|
|
1180
|
+
}
|
|
1171
1181
|
function getDefaultApiUrl() {
|
|
1172
1182
|
return process.env.TOKEN_ARENA_API_URL || DEFAULT_API_URL;
|
|
1173
1183
|
}
|
|
1174
1184
|
|
|
1185
|
+
// src/infrastructure/ui/format.ts
|
|
1186
|
+
var hasColor = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== "1");
|
|
1187
|
+
function withCode(code, value) {
|
|
1188
|
+
if (!hasColor) return value;
|
|
1189
|
+
return `\x1B[${code}m${value}\x1B[0m`;
|
|
1190
|
+
}
|
|
1191
|
+
function bold(value) {
|
|
1192
|
+
return withCode("1", value);
|
|
1193
|
+
}
|
|
1194
|
+
function dim(value) {
|
|
1195
|
+
return withCode("2", value);
|
|
1196
|
+
}
|
|
1197
|
+
function cyan(value) {
|
|
1198
|
+
return withCode("36", value);
|
|
1199
|
+
}
|
|
1200
|
+
function green(value) {
|
|
1201
|
+
return withCode("32", value);
|
|
1202
|
+
}
|
|
1203
|
+
function yellow(value) {
|
|
1204
|
+
return withCode("33", value);
|
|
1205
|
+
}
|
|
1206
|
+
function red(value) {
|
|
1207
|
+
return withCode("31", value);
|
|
1208
|
+
}
|
|
1209
|
+
function magenta(value) {
|
|
1210
|
+
return withCode("35", value);
|
|
1211
|
+
}
|
|
1212
|
+
function formatHeader(title, subtitle) {
|
|
1213
|
+
const lines = [`${cyan("\u25C8")} ${bold(title)}`];
|
|
1214
|
+
if (subtitle) {
|
|
1215
|
+
lines.push(dim(subtitle));
|
|
1216
|
+
}
|
|
1217
|
+
return `
|
|
1218
|
+
${lines.join("\n")}`;
|
|
1219
|
+
}
|
|
1220
|
+
function formatSection(title) {
|
|
1221
|
+
return `
|
|
1222
|
+
${bold(title)}`;
|
|
1223
|
+
}
|
|
1224
|
+
function formatKeyValue(label, value) {
|
|
1225
|
+
return ` ${dim(label.padEnd(14, " "))} ${value}`;
|
|
1226
|
+
}
|
|
1227
|
+
function formatBullet(value, tone = "neutral") {
|
|
1228
|
+
const icon = tone === "success" ? green("\u2714") : tone === "warning" ? yellow("!") : tone === "danger" ? red("\u2716") : cyan("\u2022");
|
|
1229
|
+
return ` ${icon} ${value}`;
|
|
1230
|
+
}
|
|
1231
|
+
function formatMutedPath(path) {
|
|
1232
|
+
return dim(path);
|
|
1233
|
+
}
|
|
1234
|
+
function maskSecret(value, visible = 8) {
|
|
1235
|
+
if (!value) return "(empty)";
|
|
1236
|
+
if (value.length <= visible) return value;
|
|
1237
|
+
return `${value.slice(0, visible)}\u2026`;
|
|
1238
|
+
}
|
|
1239
|
+
function formatStatusBadge(label, tone = "neutral") {
|
|
1240
|
+
if (tone === "success") return green(label);
|
|
1241
|
+
if (tone === "warning") return yellow(label);
|
|
1242
|
+
if (tone === "danger") return red(label);
|
|
1243
|
+
return magenta(label);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// src/infrastructure/ui/prompts.ts
|
|
1247
|
+
import {
|
|
1248
|
+
confirm,
|
|
1249
|
+
input,
|
|
1250
|
+
password,
|
|
1251
|
+
select
|
|
1252
|
+
} from "@inquirer/prompts";
|
|
1253
|
+
function isInteractiveTerminal() {
|
|
1254
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1255
|
+
}
|
|
1256
|
+
async function promptConfirm(options) {
|
|
1257
|
+
return confirm({
|
|
1258
|
+
message: options.message,
|
|
1259
|
+
default: options.defaultValue
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
async function promptText(options) {
|
|
1263
|
+
return input({
|
|
1264
|
+
message: options.message,
|
|
1265
|
+
default: options.defaultValue,
|
|
1266
|
+
validate: options.validate
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
async function promptPassword(options) {
|
|
1270
|
+
return password({
|
|
1271
|
+
message: options.message,
|
|
1272
|
+
mask: options.mask ?? "*",
|
|
1273
|
+
validate: options.validate
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
async function promptSelect(options) {
|
|
1277
|
+
return select({
|
|
1278
|
+
message: options.message,
|
|
1279
|
+
choices: [...options.choices],
|
|
1280
|
+
pageSize: Math.min(Math.max(options.choices.length, 6), 10)
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1175
1284
|
// src/utils/logger.ts
|
|
1176
1285
|
var LOG_LEVELS = {
|
|
1177
1286
|
debug: 0,
|
|
@@ -1219,13 +1328,220 @@ var logger = new Logger();
|
|
|
1219
1328
|
|
|
1220
1329
|
// src/commands/config.ts
|
|
1221
1330
|
var VALID_KEYS = ["apiKey", "apiUrl", "syncInterval", "logLevel"];
|
|
1222
|
-
function
|
|
1223
|
-
|
|
1331
|
+
function isConfigKey(value) {
|
|
1332
|
+
return isValidConfigKey(value) && VALID_KEYS.includes(value);
|
|
1333
|
+
}
|
|
1334
|
+
function formatConfigValue(key, value) {
|
|
1335
|
+
if (value === void 0 || value === null || value === "") {
|
|
1336
|
+
return "(empty)";
|
|
1337
|
+
}
|
|
1338
|
+
if (key === "apiKey") {
|
|
1339
|
+
return maskSecret(String(value));
|
|
1340
|
+
}
|
|
1341
|
+
if (key === "syncInterval") {
|
|
1342
|
+
const ms = Number(value);
|
|
1343
|
+
const minutes = Math.round(ms / 6e4);
|
|
1344
|
+
return `${minutes} \u5206\u949F (${ms} ms)`;
|
|
1345
|
+
}
|
|
1346
|
+
return String(value);
|
|
1347
|
+
}
|
|
1348
|
+
async function promptConfigSubcommand() {
|
|
1349
|
+
logger.info(
|
|
1350
|
+
formatHeader("\u914D\u7F6E\u4E2D\u5FC3", "\u901A\u8FC7\u4EA4\u4E92\u5F0F\u83DC\u5355\u67E5\u770B\u6216\u4FEE\u6539 TokenArena CLI \u914D\u7F6E\u3002")
|
|
1351
|
+
);
|
|
1352
|
+
return promptSelect({
|
|
1353
|
+
message: "\u8BF7\u9009\u62E9\u914D\u7F6E\u64CD\u4F5C",
|
|
1354
|
+
choices: [
|
|
1355
|
+
{
|
|
1356
|
+
name: "\u67E5\u770B\u5B8C\u6574\u914D\u7F6E",
|
|
1357
|
+
value: "show",
|
|
1358
|
+
description: "\u4EE5\u66F4\u9002\u5408\u9605\u8BFB\u7684\u65B9\u5F0F\u5C55\u793A\u5F53\u524D\u914D\u7F6E"
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
name: "\u8BFB\u53D6\u5355\u4E2A\u914D\u7F6E\u9879",
|
|
1362
|
+
value: "get",
|
|
1363
|
+
description: "\u67E5\u770B\u67D0\u4E2A\u914D\u7F6E\u952E\u5F53\u524D\u4FDD\u5B58\u7684\u503C"
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "\u4FEE\u6539\u914D\u7F6E\u9879",
|
|
1367
|
+
value: "set",
|
|
1368
|
+
description: "\u66F4\u65B0 API Key\u3001API \u5730\u5740\u3001\u540C\u6B65\u95F4\u9694\u6216\u65E5\u5FD7\u7EA7\u522B"
|
|
1369
|
+
}
|
|
1370
|
+
]
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
async function promptConfigKey(message) {
|
|
1374
|
+
return promptSelect({
|
|
1375
|
+
message,
|
|
1376
|
+
choices: [
|
|
1377
|
+
{
|
|
1378
|
+
name: "apiKey",
|
|
1379
|
+
value: "apiKey",
|
|
1380
|
+
description: "\u4E0A\u4F20\u6570\u636E\u65F6\u4F7F\u7528\u7684 CLI API Key"
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
name: "apiUrl",
|
|
1384
|
+
value: "apiUrl",
|
|
1385
|
+
description: "TokenArena \u670D\u52A1\u7AEF\u5730\u5740"
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
name: "syncInterval",
|
|
1389
|
+
value: "syncInterval",
|
|
1390
|
+
description: "daemon \u9ED8\u8BA4\u540C\u6B65\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09"
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
name: "logLevel",
|
|
1394
|
+
value: "logLevel",
|
|
1395
|
+
description: "CLI \u65E5\u5FD7\u7EA7\u522B"
|
|
1396
|
+
}
|
|
1397
|
+
]
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
async function promptSyncIntervalValue(existingValue) {
|
|
1401
|
+
const preset = await promptSelect({
|
|
1402
|
+
message: "\u8BF7\u9009\u62E9\u9ED8\u8BA4\u540C\u6B65\u95F4\u9694",
|
|
1403
|
+
choices: [
|
|
1404
|
+
{
|
|
1405
|
+
name: "5 \u5206\u949F",
|
|
1406
|
+
value: String(5 * 6e4),
|
|
1407
|
+
description: "\u9002\u5408\u4F5C\u4E3A\u9ED8\u8BA4\u503C"
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
name: "10 \u5206\u949F",
|
|
1411
|
+
value: String(10 * 6e4),
|
|
1412
|
+
description: "\u66F4\u7701\u7535\uFF0C\u4ECD\u4FDD\u6301\u8F83\u53CA\u65F6\u540C\u6B65"
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
name: "30 \u5206\u949F",
|
|
1416
|
+
value: String(30 * 6e4),
|
|
1417
|
+
description: "\u9002\u5408\u4F4E\u9891\u4F7F\u7528\u573A\u666F"
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
name: "60 \u5206\u949F",
|
|
1421
|
+
value: String(60 * 6e4),
|
|
1422
|
+
description: "\u957F\u5468\u671F\u540E\u53F0\u540C\u6B65"
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
name: "\u81EA\u5B9A\u4E49\uFF08\u6BEB\u79D2\uFF09",
|
|
1426
|
+
value: "custom",
|
|
1427
|
+
description: "\u8F93\u5165\u4EFB\u610F\u6B63\u6574\u6570\u6BEB\u79D2\u503C"
|
|
1428
|
+
}
|
|
1429
|
+
]
|
|
1430
|
+
});
|
|
1431
|
+
if (preset !== "custom") {
|
|
1432
|
+
return preset;
|
|
1433
|
+
}
|
|
1434
|
+
return promptText({
|
|
1435
|
+
message: "\u8BF7\u8F93\u5165 syncInterval\uFF08\u6BEB\u79D2\uFF09",
|
|
1436
|
+
defaultValue: existingValue ? String(existingValue) : void 0,
|
|
1437
|
+
validate: (value) => {
|
|
1438
|
+
const parsed = Number.parseInt(value, 10);
|
|
1439
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
1440
|
+
return "\u8BF7\u8F93\u5165\u5927\u4E8E 0 \u7684\u6BEB\u79D2\u6570\uFF0C\u4F8B\u5982 300000\u3002";
|
|
1441
|
+
}
|
|
1442
|
+
return true;
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
async function promptConfigValue(key, existingValue) {
|
|
1447
|
+
switch (key) {
|
|
1448
|
+
case "apiKey":
|
|
1449
|
+
return promptPassword({
|
|
1450
|
+
message: "\u8BF7\u8F93\u5165\u65B0\u7684 CLI API Key",
|
|
1451
|
+
validate: (value) => validateApiKey(value) || 'API Key \u5FC5\u987B\u4EE5 "ta_" \u5F00\u5934\u3002'
|
|
1452
|
+
});
|
|
1453
|
+
case "apiUrl":
|
|
1454
|
+
return promptText({
|
|
1455
|
+
message: "\u8BF7\u8F93\u5165 API \u670D\u52A1\u5730\u5740",
|
|
1456
|
+
defaultValue: typeof existingValue === "string" && existingValue.length > 0 ? existingValue : getDefaultApiUrl(),
|
|
1457
|
+
validate: (value) => {
|
|
1458
|
+
try {
|
|
1459
|
+
const url = new URL(value);
|
|
1460
|
+
return Boolean(url.protocol && url.host) || "\u8BF7\u8F93\u5165\u5408\u6CD5 URL\u3002";
|
|
1461
|
+
} catch {
|
|
1462
|
+
return "\u8BF7\u8F93\u5165\u5408\u6CD5 URL\u3002";
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
case "syncInterval":
|
|
1467
|
+
return promptSyncIntervalValue(
|
|
1468
|
+
typeof existingValue === "number" ? existingValue : void 0
|
|
1469
|
+
);
|
|
1470
|
+
case "logLevel":
|
|
1471
|
+
return promptSelect({
|
|
1472
|
+
message: "\u8BF7\u9009\u62E9\u65E5\u5FD7\u7EA7\u522B",
|
|
1473
|
+
choices: [
|
|
1474
|
+
{
|
|
1475
|
+
name: "info",
|
|
1476
|
+
value: "info",
|
|
1477
|
+
description: "\u9ED8\u8BA4\uFF0C\u8F93\u51FA\u5E38\u89C4\u8FDB\u5EA6\u4E0E\u63D0\u793A"
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
name: "warn",
|
|
1481
|
+
value: "warn",
|
|
1482
|
+
description: "\u4EC5\u8F93\u51FA\u8B66\u544A\u4E0E\u9519\u8BEF"
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
name: "error",
|
|
1486
|
+
value: "error",
|
|
1487
|
+
description: "\u53EA\u8F93\u51FA\u9519\u8BEF"
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
name: "debug",
|
|
1491
|
+
value: "debug",
|
|
1492
|
+
description: "\u8F93\u51FA\u66F4\u8BE6\u7EC6\u7684\u8C03\u8BD5\u65E5\u5FD7"
|
|
1493
|
+
}
|
|
1494
|
+
]
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
function printConfigShow() {
|
|
1499
|
+
const config = loadConfig();
|
|
1500
|
+
if (!config) {
|
|
1501
|
+
logger.info(formatHeader("\u5F53\u524D\u914D\u7F6E", "\u5C1A\u672A\u521B\u5EFA\u672C\u5730\u914D\u7F6E\u6587\u4EF6\u3002"));
|
|
1502
|
+
logger.info(formatBullet("\u8FD0\u884C tokenarena init \u5B8C\u6210\u9996\u6B21\u914D\u7F6E\u3002", "warning"));
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
logger.info(formatHeader("\u5F53\u524D\u914D\u7F6E"));
|
|
1506
|
+
logger.info(formatSection("\u57FA\u7840\u914D\u7F6E"));
|
|
1507
|
+
logger.info(formatKeyValue("API Key", maskSecret(config.apiKey || "")));
|
|
1508
|
+
logger.info(
|
|
1509
|
+
formatKeyValue("API \u5730\u5740", config.apiUrl || "https://token.poco-ai.com")
|
|
1510
|
+
);
|
|
1511
|
+
logger.info(
|
|
1512
|
+
formatKeyValue(
|
|
1513
|
+
"\u540C\u6B65\u95F4\u9694",
|
|
1514
|
+
config.syncInterval ? `${Math.round(config.syncInterval / 6e4)} \u5206\u949F (${config.syncInterval} ms)` : "\u672A\u8BBE\u7F6E\uFF08daemon \u9ED8\u8BA4 5 \u5206\u949F\uFF09"
|
|
1515
|
+
)
|
|
1516
|
+
);
|
|
1517
|
+
logger.info(formatKeyValue("\u65E5\u5FD7\u7EA7\u522B", config.logLevel || "info"));
|
|
1518
|
+
if (config.deviceId) {
|
|
1519
|
+
logger.info(formatKeyValue("\u8BBE\u5907 ID", maskSecret(config.deviceId, 12)));
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
async function handleConfig(args) {
|
|
1523
|
+
const interactive = isInteractiveTerminal();
|
|
1524
|
+
let sub = args[0];
|
|
1525
|
+
if (!sub) {
|
|
1526
|
+
if (!interactive) {
|
|
1527
|
+
logger.error("Usage: tokenarena config <get|set|show>");
|
|
1528
|
+
process.exit(1);
|
|
1529
|
+
}
|
|
1530
|
+
sub = await promptConfigSubcommand();
|
|
1531
|
+
}
|
|
1224
1532
|
switch (sub) {
|
|
1225
1533
|
case "get": {
|
|
1226
|
-
|
|
1534
|
+
let key = args[1];
|
|
1227
1535
|
if (!key) {
|
|
1228
|
-
|
|
1536
|
+
if (!interactive) {
|
|
1537
|
+
logger.error("Usage: tokenarena config get <key>");
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
}
|
|
1540
|
+
key = await promptConfigKey("\u8BF7\u9009\u62E9\u8981\u8BFB\u53D6\u7684\u914D\u7F6E\u9879");
|
|
1541
|
+
}
|
|
1542
|
+
if (!isConfigKey(key)) {
|
|
1543
|
+
logger.error(`Unknown config key: ${key}`);
|
|
1544
|
+
logger.error(`Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
1229
1545
|
process.exit(1);
|
|
1230
1546
|
}
|
|
1231
1547
|
const config = loadConfig();
|
|
@@ -1237,40 +1553,69 @@ function handleConfig(args) {
|
|
|
1237
1553
|
break;
|
|
1238
1554
|
}
|
|
1239
1555
|
case "set": {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1556
|
+
let key = args[1];
|
|
1557
|
+
if (!key) {
|
|
1558
|
+
if (!interactive) {
|
|
1559
|
+
logger.error("Usage: tokenarena config set <key> <value>");
|
|
1560
|
+
process.exit(1);
|
|
1561
|
+
}
|
|
1562
|
+
key = await promptConfigKey("\u8BF7\u9009\u62E9\u8981\u4FEE\u6539\u7684\u914D\u7F6E\u9879");
|
|
1245
1563
|
}
|
|
1246
|
-
if (!
|
|
1564
|
+
if (!isConfigKey(key)) {
|
|
1247
1565
|
logger.error(`Unknown config key: ${key}`);
|
|
1248
1566
|
logger.error(`Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
1249
1567
|
process.exit(1);
|
|
1250
1568
|
}
|
|
1251
1569
|
const config = loadConfig() || {
|
|
1252
1570
|
apiKey: "",
|
|
1253
|
-
apiUrl:
|
|
1571
|
+
apiUrl: getDefaultApiUrl()
|
|
1254
1572
|
};
|
|
1573
|
+
const record = config;
|
|
1574
|
+
let value = args[2];
|
|
1575
|
+
if (value === void 0) {
|
|
1576
|
+
if (!interactive) {
|
|
1577
|
+
logger.error("Usage: tokenarena config set <key> <value>");
|
|
1578
|
+
process.exit(1);
|
|
1579
|
+
}
|
|
1580
|
+
value = await promptConfigValue(key, record[key]);
|
|
1581
|
+
}
|
|
1582
|
+
let normalized = value;
|
|
1583
|
+
if (key === "apiKey" && !validateApiKey(value)) {
|
|
1584
|
+
logger.error('API Key must start with "ta_"');
|
|
1585
|
+
process.exit(1);
|
|
1586
|
+
}
|
|
1587
|
+
if (key === "apiUrl") {
|
|
1588
|
+
try {
|
|
1589
|
+
const url = new URL(value);
|
|
1590
|
+
normalized = url.toString().replace(/\/$/, "");
|
|
1591
|
+
} catch {
|
|
1592
|
+
logger.error("apiUrl must be a valid URL");
|
|
1593
|
+
process.exit(1);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1255
1596
|
if (key === "syncInterval") {
|
|
1256
|
-
|
|
1257
|
-
if (Number.isNaN(
|
|
1258
|
-
logger.error("syncInterval must be a number (milliseconds)");
|
|
1597
|
+
normalized = Number.parseInt(value, 10);
|
|
1598
|
+
if (Number.isNaN(normalized) || normalized <= 0) {
|
|
1599
|
+
logger.error("syncInterval must be a positive number (milliseconds)");
|
|
1259
1600
|
process.exit(1);
|
|
1260
1601
|
}
|
|
1261
1602
|
}
|
|
1262
|
-
|
|
1263
|
-
record[key] = value;
|
|
1603
|
+
record[key] = normalized;
|
|
1264
1604
|
saveConfig(config);
|
|
1265
|
-
|
|
1605
|
+
if (interactive) {
|
|
1606
|
+
logger.info(formatHeader("\u914D\u7F6E\u5DF2\u66F4\u65B0"));
|
|
1607
|
+
logger.info(formatKeyValue(key, formatConfigValue(key, normalized)));
|
|
1608
|
+
} else {
|
|
1609
|
+
logger.info(`Set ${key} = ${normalized}`);
|
|
1610
|
+
}
|
|
1266
1611
|
break;
|
|
1267
1612
|
}
|
|
1268
1613
|
case "show": {
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
console.log("{}");
|
|
1614
|
+
if (interactive) {
|
|
1615
|
+
printConfigShow();
|
|
1272
1616
|
} else {
|
|
1273
|
-
|
|
1617
|
+
const config = loadConfig();
|
|
1618
|
+
console.log(config ? JSON.stringify(config, null, 2) : "{}");
|
|
1274
1619
|
}
|
|
1275
1620
|
break;
|
|
1276
1621
|
}
|
|
@@ -1286,27 +1631,180 @@ import { hostname as hostname3 } from "os";
|
|
|
1286
1631
|
|
|
1287
1632
|
// src/domain/project-identity.ts
|
|
1288
1633
|
import { createHmac } from "crypto";
|
|
1289
|
-
function toProjectIdentity(
|
|
1290
|
-
if (
|
|
1634
|
+
function toProjectIdentity(input2) {
|
|
1635
|
+
if (input2.mode === "disabled") {
|
|
1291
1636
|
return { projectKey: "unknown", projectLabel: "Unknown Project" };
|
|
1292
1637
|
}
|
|
1293
|
-
if (
|
|
1294
|
-
return { projectKey:
|
|
1638
|
+
if (input2.mode === "raw") {
|
|
1639
|
+
return { projectKey: input2.project, projectLabel: input2.project };
|
|
1295
1640
|
}
|
|
1296
|
-
const projectKey = createHmac("sha256",
|
|
1641
|
+
const projectKey = createHmac("sha256", input2.salt).update(input2.project).digest("hex").slice(0, 16);
|
|
1297
1642
|
return {
|
|
1298
1643
|
projectKey,
|
|
1299
1644
|
projectLabel: `Project ${projectKey.slice(0, 6)}`
|
|
1300
1645
|
};
|
|
1301
1646
|
}
|
|
1302
1647
|
|
|
1648
|
+
// src/domain/upload-manifest.ts
|
|
1649
|
+
import { createHash as createHash2 } from "crypto";
|
|
1650
|
+
var MANIFEST_VERSION = 1;
|
|
1651
|
+
function shortHash(value) {
|
|
1652
|
+
return createHash2("sha256").update(value).digest("hex").slice(0, 16);
|
|
1653
|
+
}
|
|
1654
|
+
function normalizeApiUrl(apiUrl) {
|
|
1655
|
+
return apiUrl.replace(/\/+$/, "");
|
|
1656
|
+
}
|
|
1657
|
+
function buildUploadManifestScope(input2) {
|
|
1658
|
+
return {
|
|
1659
|
+
apiKeyHash: shortHash(input2.apiKey),
|
|
1660
|
+
apiUrl: normalizeApiUrl(input2.apiUrl),
|
|
1661
|
+
deviceId: input2.deviceId,
|
|
1662
|
+
projectHashSaltHash: shortHash(input2.settings.projectHashSalt),
|
|
1663
|
+
projectMode: input2.settings.projectMode
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function describeUploadManifestScopeChanges(previous, current) {
|
|
1667
|
+
const changes = [];
|
|
1668
|
+
if (previous.apiUrl !== current.apiUrl || previous.apiKeyHash !== current.apiKeyHash) {
|
|
1669
|
+
changes.push("server_or_api_key");
|
|
1670
|
+
}
|
|
1671
|
+
if (previous.deviceId !== current.deviceId) {
|
|
1672
|
+
changes.push("device_id");
|
|
1673
|
+
}
|
|
1674
|
+
if (previous.projectMode !== current.projectMode || previous.projectHashSaltHash !== current.projectHashSaltHash) {
|
|
1675
|
+
changes.push("project_identity");
|
|
1676
|
+
}
|
|
1677
|
+
return changes;
|
|
1678
|
+
}
|
|
1679
|
+
function getUploadBucketManifestKey(bucket) {
|
|
1680
|
+
return [
|
|
1681
|
+
bucket.deviceId,
|
|
1682
|
+
bucket.source,
|
|
1683
|
+
bucket.model,
|
|
1684
|
+
bucket.projectKey,
|
|
1685
|
+
bucket.bucketStart
|
|
1686
|
+
].join("|");
|
|
1687
|
+
}
|
|
1688
|
+
function getUploadSessionManifestKey(session) {
|
|
1689
|
+
return [session.deviceId, session.source, session.sessionHash].join("|");
|
|
1690
|
+
}
|
|
1691
|
+
function getUploadBucketContentHash(bucket) {
|
|
1692
|
+
return shortHash(
|
|
1693
|
+
JSON.stringify({
|
|
1694
|
+
cachedTokens: bucket.cachedTokens,
|
|
1695
|
+
hostname: bucket.hostname,
|
|
1696
|
+
inputTokens: bucket.inputTokens,
|
|
1697
|
+
outputTokens: bucket.outputTokens,
|
|
1698
|
+
projectLabel: bucket.projectLabel,
|
|
1699
|
+
reasoningTokens: bucket.reasoningTokens,
|
|
1700
|
+
totalTokens: bucket.totalTokens
|
|
1701
|
+
})
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
function getUploadSessionContentHash(session) {
|
|
1705
|
+
return shortHash(
|
|
1706
|
+
JSON.stringify({
|
|
1707
|
+
activeSeconds: session.activeSeconds,
|
|
1708
|
+
cachedTokens: session.cachedTokens,
|
|
1709
|
+
durationSeconds: session.durationSeconds,
|
|
1710
|
+
firstMessageAt: session.firstMessageAt,
|
|
1711
|
+
hostname: session.hostname,
|
|
1712
|
+
inputTokens: session.inputTokens,
|
|
1713
|
+
lastMessageAt: session.lastMessageAt,
|
|
1714
|
+
messageCount: session.messageCount,
|
|
1715
|
+
modelUsages: session.modelUsages,
|
|
1716
|
+
outputTokens: session.outputTokens,
|
|
1717
|
+
primaryModel: session.primaryModel,
|
|
1718
|
+
projectKey: session.projectKey,
|
|
1719
|
+
projectLabel: session.projectLabel,
|
|
1720
|
+
reasoningTokens: session.reasoningTokens,
|
|
1721
|
+
totalTokens: session.totalTokens,
|
|
1722
|
+
userMessageCount: session.userMessageCount
|
|
1723
|
+
})
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
function buildRecordHashes(items, getKey, getHash) {
|
|
1727
|
+
const hashes = {};
|
|
1728
|
+
for (const item of items) {
|
|
1729
|
+
hashes[getKey(item)] = getHash(item);
|
|
1730
|
+
}
|
|
1731
|
+
return hashes;
|
|
1732
|
+
}
|
|
1733
|
+
function createUploadManifest(input2) {
|
|
1734
|
+
return {
|
|
1735
|
+
buckets: buildRecordHashes(
|
|
1736
|
+
input2.buckets,
|
|
1737
|
+
getUploadBucketManifestKey,
|
|
1738
|
+
getUploadBucketContentHash
|
|
1739
|
+
),
|
|
1740
|
+
scope: input2.scope,
|
|
1741
|
+
sessions: buildRecordHashes(
|
|
1742
|
+
input2.sessions,
|
|
1743
|
+
getUploadSessionManifestKey,
|
|
1744
|
+
getUploadSessionContentHash
|
|
1745
|
+
),
|
|
1746
|
+
updatedAt: input2.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1747
|
+
version: MANIFEST_VERSION
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
function countRemovedRecords(previous, current) {
|
|
1751
|
+
let removed = 0;
|
|
1752
|
+
for (const key of Object.keys(previous)) {
|
|
1753
|
+
if (!(key in current)) {
|
|
1754
|
+
removed++;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return removed;
|
|
1758
|
+
}
|
|
1759
|
+
function diffUploadManifest(input2) {
|
|
1760
|
+
const nextManifest = createUploadManifest({
|
|
1761
|
+
buckets: input2.buckets,
|
|
1762
|
+
scope: input2.scope,
|
|
1763
|
+
sessions: input2.sessions,
|
|
1764
|
+
updatedAt: input2.updatedAt
|
|
1765
|
+
});
|
|
1766
|
+
const scopeChangedReasons = input2.previous ? describeUploadManifestScopeChanges(input2.previous.scope, input2.scope) : [];
|
|
1767
|
+
const previousBuckets = input2.previous && scopeChangedReasons.length === 0 ? input2.previous.buckets : {};
|
|
1768
|
+
const previousSessions = input2.previous && scopeChangedReasons.length === 0 ? input2.previous.sessions : {};
|
|
1769
|
+
const bucketsToUpload = input2.buckets.filter((bucket) => {
|
|
1770
|
+
const key = getUploadBucketManifestKey(bucket);
|
|
1771
|
+
return previousBuckets[key] !== nextManifest.buckets[key];
|
|
1772
|
+
});
|
|
1773
|
+
const sessionsToUpload = input2.sessions.filter((session) => {
|
|
1774
|
+
const key = getUploadSessionManifestKey(session);
|
|
1775
|
+
return previousSessions[key] !== nextManifest.sessions[key];
|
|
1776
|
+
});
|
|
1777
|
+
return {
|
|
1778
|
+
bucketsToUpload,
|
|
1779
|
+
nextManifest,
|
|
1780
|
+
removedBuckets: countRemovedRecords(previousBuckets, nextManifest.buckets),
|
|
1781
|
+
removedSessions: countRemovedRecords(
|
|
1782
|
+
previousSessions,
|
|
1783
|
+
nextManifest.sessions
|
|
1784
|
+
),
|
|
1785
|
+
scopeChangedReasons,
|
|
1786
|
+
sessionsToUpload,
|
|
1787
|
+
unchangedBuckets: input2.buckets.length - bucketsToUpload.length,
|
|
1788
|
+
unchangedSessions: input2.sessions.length - sessionsToUpload.length
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1303
1792
|
// src/infrastructure/api/client.ts
|
|
1304
1793
|
import http from "http";
|
|
1305
1794
|
import https from "https";
|
|
1306
|
-
import { URL } from "url";
|
|
1795
|
+
import { URL as URL2 } from "url";
|
|
1307
1796
|
var MAX_RETRIES = 3;
|
|
1308
1797
|
var INITIAL_DELAY = 1e3;
|
|
1309
1798
|
var TIMEOUT_MS = 6e4;
|
|
1799
|
+
function getIngestPayloadSize(device, buckets, sessions) {
|
|
1800
|
+
const payload = {
|
|
1801
|
+
schemaVersion: 2,
|
|
1802
|
+
device,
|
|
1803
|
+
buckets,
|
|
1804
|
+
sessions: sessions ?? []
|
|
1805
|
+
};
|
|
1806
|
+
return Buffer.byteLength(JSON.stringify(payload));
|
|
1807
|
+
}
|
|
1310
1808
|
var ApiClient = class {
|
|
1311
1809
|
constructor(apiUrl, apiKey) {
|
|
1312
1810
|
this.apiUrl = apiUrl;
|
|
@@ -1336,14 +1834,15 @@ var ApiClient = class {
|
|
|
1336
1834
|
}
|
|
1337
1835
|
sendIngest(device, buckets, sessions, onProgress) {
|
|
1338
1836
|
return new Promise((resolve2, reject) => {
|
|
1339
|
-
const url = new
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1837
|
+
const url = new URL2("/api/usage/ingest", this.apiUrl);
|
|
1838
|
+
const body = Buffer.from(
|
|
1839
|
+
JSON.stringify({
|
|
1840
|
+
schemaVersion: 2,
|
|
1841
|
+
device,
|
|
1842
|
+
buckets,
|
|
1843
|
+
sessions: sessions ?? []
|
|
1844
|
+
})
|
|
1845
|
+
);
|
|
1347
1846
|
const totalBytes = body.length;
|
|
1348
1847
|
const mod = url.protocol === "https:" ? https : http;
|
|
1349
1848
|
const req = mod.request(
|
|
@@ -1416,7 +1915,7 @@ var ApiClient = class {
|
|
|
1416
1915
|
*/
|
|
1417
1916
|
async fetchSettings() {
|
|
1418
1917
|
return new Promise((resolve2, reject) => {
|
|
1419
|
-
const url = new
|
|
1918
|
+
const url = new URL2("/api/usage/settings", this.apiUrl);
|
|
1420
1919
|
const mod = url.protocol === "https:" ? https : http;
|
|
1421
1920
|
const req = mod.request(
|
|
1422
1921
|
url,
|
|
@@ -1467,7 +1966,7 @@ var ApiClient = class {
|
|
|
1467
1966
|
*/
|
|
1468
1967
|
async deleteAllData(opts) {
|
|
1469
1968
|
return new Promise((resolve2, reject) => {
|
|
1470
|
-
const url = new
|
|
1969
|
+
const url = new URL2("/api/usage/ingest", this.apiUrl);
|
|
1471
1970
|
if (opts?.hostname) {
|
|
1472
1971
|
url.searchParams.set("hostname", opts.hostname);
|
|
1473
1972
|
}
|
|
@@ -1543,6 +2042,9 @@ function getSyncLockPath() {
|
|
|
1543
2042
|
function getSyncStatePath() {
|
|
1544
2043
|
return join10(getStateDir(), "status.json");
|
|
1545
2044
|
}
|
|
2045
|
+
function getUploadManifestPath() {
|
|
2046
|
+
return join10(getStateDir(), "upload-manifest.json");
|
|
2047
|
+
}
|
|
1546
2048
|
function ensureAppDirs() {
|
|
1547
2049
|
mkdirSync2(getRuntimeDirPath(), { recursive: true });
|
|
1548
2050
|
mkdirSync2(getStateDir(), { recursive: true });
|
|
@@ -1699,6 +2201,43 @@ function markSyncFailed(source, error, status) {
|
|
|
1699
2201
|
});
|
|
1700
2202
|
}
|
|
1701
2203
|
|
|
2204
|
+
// src/infrastructure/runtime/upload-manifest.ts
|
|
2205
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2206
|
+
function isRecordOfStrings(value) {
|
|
2207
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
return Object.values(value).every((entry) => typeof entry === "string");
|
|
2211
|
+
}
|
|
2212
|
+
function isUploadManifest(value) {
|
|
2213
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
const manifest = value;
|
|
2217
|
+
return manifest.version === 1 && !!manifest.scope && typeof manifest.scope === "object" && typeof manifest.scope.apiUrl === "string" && typeof manifest.scope.apiKeyHash === "string" && typeof manifest.scope.deviceId === "string" && typeof manifest.scope.projectMode === "string" && typeof manifest.scope.projectHashSaltHash === "string" && typeof manifest.updatedAt === "string" && isRecordOfStrings(manifest.buckets) && isRecordOfStrings(manifest.sessions);
|
|
2218
|
+
}
|
|
2219
|
+
function loadUploadManifest() {
|
|
2220
|
+
const path = getUploadManifestPath();
|
|
2221
|
+
if (!existsSync11(path)) {
|
|
2222
|
+
return null;
|
|
2223
|
+
}
|
|
2224
|
+
try {
|
|
2225
|
+
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
2226
|
+
return isUploadManifest(parsed) ? parsed : null;
|
|
2227
|
+
} catch {
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
function saveUploadManifest(manifest) {
|
|
2232
|
+
ensureAppDirs();
|
|
2233
|
+
writeFileSync4(
|
|
2234
|
+
getUploadManifestPath(),
|
|
2235
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
2236
|
+
`,
|
|
2237
|
+
"utf-8"
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
1702
2241
|
// src/services/parser-service.ts
|
|
1703
2242
|
async function runAllParsers() {
|
|
1704
2243
|
const allBuckets = [];
|
|
@@ -1731,16 +2270,45 @@ function getDetectedTools() {
|
|
|
1731
2270
|
// src/services/sync-service.ts
|
|
1732
2271
|
var BATCH_SIZE = 100;
|
|
1733
2272
|
var SESSION_BATCH_SIZE = 500;
|
|
2273
|
+
var PROGRESS_BAR_WIDTH = 28;
|
|
1734
2274
|
function formatBytes(bytes) {
|
|
1735
2275
|
if (bytes < 1024) return `${bytes}B`;
|
|
1736
2276
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1737
2277
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1738
2278
|
}
|
|
1739
|
-
function
|
|
1740
|
-
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
|
|
2279
|
+
function renderProgressBar(progress) {
|
|
2280
|
+
const safeProgress = Math.max(0, Math.min(progress, 1));
|
|
2281
|
+
const filled = Math.round(safeProgress * PROGRESS_BAR_WIDTH);
|
|
2282
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(PROGRESS_BAR_WIDTH - filled)}`;
|
|
2283
|
+
}
|
|
2284
|
+
function writeUploadProgress(sent, total, batchNum, totalBatches) {
|
|
2285
|
+
const pct = total > 0 ? Math.round(sent / total * 100) : 100;
|
|
2286
|
+
const progressBar = renderProgressBar(total > 0 ? sent / total : 1);
|
|
2287
|
+
const batchLabel = totalBatches > 1 ? ` \xB7 batch ${batchNum}/${totalBatches}` : "";
|
|
2288
|
+
process.stdout.write(
|
|
2289
|
+
`\r Uploading ${progressBar} ${String(pct).padStart(3, " ")}% \xB7 ${formatBytes(sent)}/${formatBytes(total)}${batchLabel}\x1B[K`
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
function formatScopeChangeReason(reason) {
|
|
2293
|
+
switch (reason) {
|
|
2294
|
+
case "server_or_api_key":
|
|
2295
|
+
return "server/API key";
|
|
2296
|
+
case "device_id":
|
|
2297
|
+
return "device ID";
|
|
2298
|
+
case "project_identity":
|
|
2299
|
+
return "project identity settings";
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function persistUploadManifest(manifest, quiet) {
|
|
2303
|
+
try {
|
|
2304
|
+
saveUploadManifest(manifest);
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
if (!quiet) {
|
|
2307
|
+
logger.warn(
|
|
2308
|
+
`Uploaded data, but failed to update the local sync manifest: ${error.message}`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
1744
2312
|
}
|
|
1745
2313
|
function toDeviceMetadata(config) {
|
|
1746
2314
|
return {
|
|
@@ -1887,59 +2455,135 @@ async function runSync(config, opts = {}) {
|
|
|
1887
2455
|
);
|
|
1888
2456
|
}
|
|
1889
2457
|
const device = toDeviceMetadata(config);
|
|
2458
|
+
const manifestScope = buildUploadManifestScope({
|
|
2459
|
+
apiKey: config.apiKey,
|
|
2460
|
+
apiUrl,
|
|
2461
|
+
deviceId: device.deviceId,
|
|
2462
|
+
settings
|
|
2463
|
+
});
|
|
1890
2464
|
const uploadBuckets = toUploadBuckets(allBuckets, settings, device);
|
|
1891
2465
|
const uploadSessions = toUploadSessions(allSessions, settings, device);
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2466
|
+
const uploadDiff = diffUploadManifest({
|
|
2467
|
+
buckets: uploadBuckets,
|
|
2468
|
+
previous: loadUploadManifest(),
|
|
2469
|
+
scope: manifestScope,
|
|
2470
|
+
sessions: uploadSessions
|
|
2471
|
+
});
|
|
2472
|
+
const changedBuckets = uploadDiff.bucketsToUpload;
|
|
2473
|
+
const changedSessions = uploadDiff.sessionsToUpload;
|
|
2474
|
+
if (!quiet && uploadDiff.scopeChangedReasons.length > 0) {
|
|
2475
|
+
logger.warn(
|
|
2476
|
+
`Upload scope changed (${uploadDiff.scopeChangedReasons.map(formatScopeChangeReason).join(", ")}). TokenArena will upload the current snapshot again, but existing remote records from the previous scope will not be deleted automatically.`
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
if (!quiet && (uploadDiff.removedBuckets > 0 || uploadDiff.removedSessions > 0)) {
|
|
2480
|
+
const parts = [];
|
|
2481
|
+
if (uploadDiff.removedBuckets > 0) {
|
|
2482
|
+
parts.push(`${uploadDiff.removedBuckets} buckets`);
|
|
2483
|
+
}
|
|
2484
|
+
if (uploadDiff.removedSessions > 0) {
|
|
2485
|
+
parts.push(`${uploadDiff.removedSessions} sessions`);
|
|
2486
|
+
}
|
|
2487
|
+
logger.warn(
|
|
2488
|
+
`Detected ${parts.join(" + ")} that were present in the previous local snapshot but are missing now. Remote deletions are not supported yet, so renamed projects or removed local logs may leave stale data online.`
|
|
2489
|
+
);
|
|
1899
2490
|
}
|
|
1900
|
-
|
|
2491
|
+
if (changedBuckets.length === 0 && changedSessions.length === 0) {
|
|
2492
|
+
if (!quiet) {
|
|
2493
|
+
const skippedParts = [];
|
|
2494
|
+
if (uploadDiff.unchangedBuckets > 0) {
|
|
2495
|
+
skippedParts.push(`${uploadDiff.unchangedBuckets} unchanged buckets`);
|
|
2496
|
+
}
|
|
2497
|
+
if (uploadDiff.unchangedSessions > 0) {
|
|
2498
|
+
skippedParts.push(
|
|
2499
|
+
`${uploadDiff.unchangedSessions} unchanged sessions`
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
logger.info(
|
|
2503
|
+
skippedParts.length > 0 ? `No new or updated usage data to upload. Skipped ${skippedParts.join(" + ")}.` : "No new or updated usage data to upload."
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
persistUploadManifest(uploadDiff.nextManifest, quiet);
|
|
2507
|
+
markSyncSucceeded(source, { buckets: 0, sessions: 0 });
|
|
2508
|
+
return { buckets: 0, sessions: 0 };
|
|
2509
|
+
}
|
|
2510
|
+
const bucketBatches = Math.ceil(changedBuckets.length / BATCH_SIZE);
|
|
1901
2511
|
const sessionBatches = Math.ceil(
|
|
1902
|
-
|
|
2512
|
+
changedSessions.length / SESSION_BATCH_SIZE
|
|
1903
2513
|
);
|
|
1904
2514
|
const totalBatches = Math.max(bucketBatches, sessionBatches, 1);
|
|
2515
|
+
const batchPayloadSizes = Array.from(
|
|
2516
|
+
{ length: totalBatches },
|
|
2517
|
+
(_, batchIdx) => getIngestPayloadSize(
|
|
2518
|
+
device,
|
|
2519
|
+
changedBuckets.slice(
|
|
2520
|
+
batchIdx * BATCH_SIZE,
|
|
2521
|
+
(batchIdx + 1) * BATCH_SIZE
|
|
2522
|
+
),
|
|
2523
|
+
changedSessions.slice(
|
|
2524
|
+
batchIdx * SESSION_BATCH_SIZE,
|
|
2525
|
+
(batchIdx + 1) * SESSION_BATCH_SIZE
|
|
2526
|
+
)
|
|
2527
|
+
)
|
|
2528
|
+
);
|
|
2529
|
+
const totalPayloadBytes = batchPayloadSizes.reduce(
|
|
2530
|
+
(sum, size) => sum + size,
|
|
2531
|
+
0
|
|
2532
|
+
);
|
|
2533
|
+
let uploadedBytesBeforeBatch = 0;
|
|
1905
2534
|
if (!quiet) {
|
|
1906
2535
|
const parts = [];
|
|
1907
|
-
if (
|
|
1908
|
-
parts.push(`${
|
|
2536
|
+
if (changedBuckets.length > 0) {
|
|
2537
|
+
parts.push(`${changedBuckets.length} buckets`);
|
|
1909
2538
|
}
|
|
1910
|
-
if (
|
|
1911
|
-
parts.push(`${
|
|
2539
|
+
if (changedSessions.length > 0) {
|
|
2540
|
+
parts.push(`${changedSessions.length} sessions`);
|
|
2541
|
+
}
|
|
2542
|
+
const skippedParts = [];
|
|
2543
|
+
if (uploadDiff.unchangedBuckets > 0) {
|
|
2544
|
+
skippedParts.push(`${uploadDiff.unchangedBuckets} unchanged buckets`);
|
|
2545
|
+
}
|
|
2546
|
+
if (uploadDiff.unchangedSessions > 0) {
|
|
2547
|
+
skippedParts.push(`${uploadDiff.unchangedSessions} unchanged sessions`);
|
|
1912
2548
|
}
|
|
1913
2549
|
logger.info(
|
|
1914
|
-
`Uploading ${parts.join(" + ")} (${totalBatches} batch${totalBatches > 1 ? "es" : ""})...`
|
|
2550
|
+
`Uploading ${parts.join(" + ")} (${totalBatches} batch${totalBatches > 1 ? "es" : ""}${skippedParts.length > 0 ? `, skipped ${skippedParts.join(" + ")}` : ""})...`
|
|
1915
2551
|
);
|
|
1916
2552
|
}
|
|
1917
2553
|
for (let batchIdx = 0; batchIdx < totalBatches; batchIdx++) {
|
|
1918
|
-
const batch =
|
|
2554
|
+
const batch = changedBuckets.slice(
|
|
1919
2555
|
batchIdx * BATCH_SIZE,
|
|
1920
2556
|
(batchIdx + 1) * BATCH_SIZE
|
|
1921
2557
|
);
|
|
1922
|
-
const batchSessions =
|
|
2558
|
+
const batchSessions = changedSessions.slice(
|
|
1923
2559
|
batchIdx * SESSION_BATCH_SIZE,
|
|
1924
2560
|
(batchIdx + 1) * SESSION_BATCH_SIZE
|
|
1925
2561
|
);
|
|
1926
2562
|
const batchNum = batchIdx + 1;
|
|
1927
|
-
const prefix = totalBatches > 1 ? ` [${batchNum}/${totalBatches}] ` : " ";
|
|
1928
2563
|
const result = await apiClient.ingest(
|
|
1929
2564
|
device,
|
|
1930
2565
|
batch,
|
|
1931
2566
|
batchSessions.length > 0 ? batchSessions : void 0,
|
|
1932
2567
|
quiet ? void 0 : (sent, total) => {
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2568
|
+
writeUploadProgress(
|
|
2569
|
+
uploadedBytesBeforeBatch + sent,
|
|
2570
|
+
totalPayloadBytes || total,
|
|
2571
|
+
batchNum,
|
|
2572
|
+
totalBatches
|
|
1936
2573
|
);
|
|
1937
2574
|
}
|
|
1938
2575
|
);
|
|
1939
2576
|
totalIngested += result.ingested ?? batch.length;
|
|
1940
2577
|
totalSessionsSynced += result.sessions ?? batchSessions.length;
|
|
1941
|
-
|
|
1942
|
-
|
|
2578
|
+
uploadedBytesBeforeBatch += batchPayloadSizes[batchIdx] ?? 0;
|
|
2579
|
+
}
|
|
2580
|
+
if (!quiet && (totalBatches > 1 || changedBuckets.length > 0)) {
|
|
2581
|
+
writeUploadProgress(
|
|
2582
|
+
totalPayloadBytes,
|
|
2583
|
+
totalPayloadBytes,
|
|
2584
|
+
totalBatches,
|
|
2585
|
+
totalBatches
|
|
2586
|
+
);
|
|
1943
2587
|
process.stdout.write("\n");
|
|
1944
2588
|
}
|
|
1945
2589
|
const syncParts = [`${totalIngested} buckets`];
|
|
@@ -1947,27 +2591,11 @@ async function runSync(config, opts = {}) {
|
|
|
1947
2591
|
syncParts.push(`${totalSessionsSynced} sessions`);
|
|
1948
2592
|
}
|
|
1949
2593
|
logger.info(`Synced ${syncParts.join(" + ")}.`);
|
|
1950
|
-
if (!quiet && totalSessionsSynced > 0) {
|
|
1951
|
-
const totalActive = uploadSessions.reduce(
|
|
1952
|
-
(sum, session) => sum + session.activeSeconds,
|
|
1953
|
-
0
|
|
1954
|
-
);
|
|
1955
|
-
const totalDuration = uploadSessions.reduce(
|
|
1956
|
-
(sum, session) => sum + session.durationSeconds,
|
|
1957
|
-
0
|
|
1958
|
-
);
|
|
1959
|
-
const totalMsgs = uploadSessions.reduce(
|
|
1960
|
-
(sum, session) => sum + session.messageCount,
|
|
1961
|
-
0
|
|
1962
|
-
);
|
|
1963
|
-
logger.info(
|
|
1964
|
-
` active: ${formatTime(totalActive)} / total: ${formatTime(totalDuration)}, ${totalMsgs} messages`
|
|
1965
|
-
);
|
|
1966
|
-
}
|
|
1967
2594
|
if (!quiet) {
|
|
1968
2595
|
logger.info(`
|
|
1969
2596
|
View your dashboard at: ${apiUrl}/usage`);
|
|
1970
2597
|
}
|
|
2598
|
+
persistUploadManifest(uploadDiff.nextManifest, quiet);
|
|
1971
2599
|
markSyncSucceeded(source, {
|
|
1972
2600
|
buckets: totalIngested,
|
|
1973
2601
|
sessions: totalSessionsSynced
|
|
@@ -2016,64 +2644,17 @@ View your dashboard at: ${apiUrl}/usage`);
|
|
|
2016
2644
|
process.exit(1);
|
|
2017
2645
|
}
|
|
2018
2646
|
|
|
2019
|
-
// src/commands/daemon.ts
|
|
2020
|
-
var DEFAULT_INTERVAL = 5 * 6e4;
|
|
2021
|
-
function log(msg) {
|
|
2022
|
-
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
2023
|
-
process.stdout.write(`[${ts}] ${msg}
|
|
2024
|
-
`);
|
|
2025
|
-
}
|
|
2026
|
-
function sleep(ms) {
|
|
2027
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2028
|
-
}
|
|
2029
|
-
async function runDaemon(opts = {}) {
|
|
2030
|
-
const config = loadConfig();
|
|
2031
|
-
if (!config?.apiKey) {
|
|
2032
|
-
logger.error("Not configured. Run `tokenarena init` first.");
|
|
2033
|
-
process.exit(1);
|
|
2034
|
-
}
|
|
2035
|
-
const interval = opts.interval || config.syncInterval || DEFAULT_INTERVAL;
|
|
2036
|
-
const intervalMin = Math.round(interval / 6e4);
|
|
2037
|
-
log(`Daemon started (sync every ${intervalMin}m, Ctrl+C to stop)`);
|
|
2038
|
-
while (true) {
|
|
2039
|
-
try {
|
|
2040
|
-
await runSync(config, {
|
|
2041
|
-
quiet: true,
|
|
2042
|
-
source: "daemon",
|
|
2043
|
-
throws: true
|
|
2044
|
-
});
|
|
2045
|
-
} catch (err) {
|
|
2046
|
-
if (err.message === "UNAUTHORIZED") {
|
|
2047
|
-
log("API key invalid. Exiting.");
|
|
2048
|
-
process.exit(1);
|
|
2049
|
-
}
|
|
2050
|
-
log(`Sync error: ${err.message}`);
|
|
2051
|
-
}
|
|
2052
|
-
await sleep(interval);
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
2647
|
// src/commands/init.ts
|
|
2057
2648
|
import { execFileSync as execFileSync2, spawn } from "child_process";
|
|
2058
|
-
import { existsSync as
|
|
2649
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2059
2650
|
import { appendFile, mkdir, readFile } from "fs/promises";
|
|
2060
2651
|
import { homedir as homedir8, platform } from "os";
|
|
2061
2652
|
import { dirname as dirname2, join as join11, posix, win32 } from "path";
|
|
2062
|
-
import { createInterface } from "readline";
|
|
2063
2653
|
function joinForPlatform(currentPlatform, ...parts) {
|
|
2064
2654
|
return currentPlatform === "win32" ? win32.join(...parts) : posix.join(...parts);
|
|
2065
2655
|
}
|
|
2066
|
-
function
|
|
2067
|
-
|
|
2068
|
-
return new Promise((resolve2) => {
|
|
2069
|
-
rl.question(question, (answer) => {
|
|
2070
|
-
rl.close();
|
|
2071
|
-
resolve2(answer.trim());
|
|
2072
|
-
});
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
function basenameLikeShell(input) {
|
|
2076
|
-
return input.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input;
|
|
2656
|
+
function basenameLikeShell(input2) {
|
|
2657
|
+
return input2.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input2;
|
|
2077
2658
|
}
|
|
2078
2659
|
function getBrowserLaunchCommand(url, currentPlatform = platform()) {
|
|
2079
2660
|
switch (currentPlatform) {
|
|
@@ -2142,7 +2723,7 @@ function resolveShellAliasSetup(options = {}) {
|
|
|
2142
2723
|
const currentPlatform = options.currentPlatform ?? platform();
|
|
2143
2724
|
const env = options.env ?? process.env;
|
|
2144
2725
|
const homeDir = options.homeDir ?? homedir8();
|
|
2145
|
-
const pathExists = options.exists ??
|
|
2726
|
+
const pathExists = options.exists ?? existsSync12;
|
|
2146
2727
|
const shellFromEnv = env.SHELL ? basenameLikeShell(env.SHELL).toLowerCase() : "";
|
|
2147
2728
|
const shellName = shellFromEnv || (currentPlatform === "win32" ? "powershell" : "");
|
|
2148
2729
|
const aliasName = "ta";
|
|
@@ -2210,43 +2791,62 @@ function resolveShellAliasSetup(options = {}) {
|
|
|
2210
2791
|
}
|
|
2211
2792
|
}
|
|
2212
2793
|
async function runInit(opts = {}) {
|
|
2213
|
-
logger.info("
|
|
2794
|
+
logger.info(formatHeader("TokenArena \u521D\u59CB\u5316"));
|
|
2214
2795
|
const existing = loadConfig();
|
|
2215
2796
|
if (existing?.apiKey) {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2797
|
+
logger.info(formatSection("\u68C0\u6D4B\u5230\u5DF2\u6709\u914D\u7F6E"));
|
|
2798
|
+
logger.info(formatKeyValue("\u5F53\u524D API Key", maskSecret(existing.apiKey)));
|
|
2799
|
+
logger.info(
|
|
2800
|
+
formatKeyValue(
|
|
2801
|
+
"\u5F53\u524D API \u5730\u5740",
|
|
2802
|
+
existing.apiUrl || "https://token.poco-ai.com"
|
|
2803
|
+
)
|
|
2804
|
+
);
|
|
2805
|
+
const shouldOverwrite = await promptConfirm({
|
|
2806
|
+
message: "\u5DF2\u7ECF\u5B58\u5728\u672C\u5730\u914D\u7F6E\uFF0C\u662F\u5426\u8986\u76D6\u5E76\u91CD\u65B0\u521D\u59CB\u5316\uFF1F",
|
|
2807
|
+
defaultValue: false
|
|
2808
|
+
});
|
|
2809
|
+
if (!shouldOverwrite) {
|
|
2810
|
+
logger.info(formatBullet("\u5DF2\u53D6\u6D88\u521D\u59CB\u5316\u3002", "warning"));
|
|
2219
2811
|
return;
|
|
2220
2812
|
}
|
|
2221
2813
|
}
|
|
2222
2814
|
const apiUrl = opts.apiUrl || getDefaultApiUrl();
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
}
|
|
2232
|
-
logger.info(
|
|
2233
|
-
|
|
2815
|
+
const cliKeysUrl = `${apiUrl}/zh/settings/cli-keys`;
|
|
2816
|
+
logger.info(formatSection("\u7B2C 1 \u6B65\uFF1A\u51C6\u5907 API Key"));
|
|
2817
|
+
logger.info(formatBullet("\u6D4F\u89C8\u5668\u5C06\u5C1D\u8BD5\u81EA\u52A8\u6253\u5F00 CLI Key \u9875\u9762\u3002"));
|
|
2818
|
+
logger.info(formatKeyValue("Key \u9875\u9762", formatMutedPath(cliKeysUrl)));
|
|
2819
|
+
openBrowser(cliKeysUrl);
|
|
2820
|
+
const apiKey = await promptPassword({
|
|
2821
|
+
message: "\u8BF7\u7C98\u8D34\u4F60\u7684 CLI API Key",
|
|
2822
|
+
validate: (value) => validateApiKey(value) || 'API Key \u5FC5\u987B\u4EE5 "ta_" \u5F00\u5934\u3002'
|
|
2823
|
+
});
|
|
2824
|
+
logger.info(formatSection("\u7B2C 2 \u6B65\uFF1A\u9A8C\u8BC1 API Key"));
|
|
2825
|
+
logger.info(formatKeyValue("\u5F85\u9A8C\u8BC1 Key", maskSecret(apiKey)));
|
|
2234
2826
|
try {
|
|
2235
2827
|
const client = new ApiClient(apiUrl, apiKey);
|
|
2236
2828
|
const settings = await client.fetchSettings();
|
|
2237
2829
|
if (!settings) {
|
|
2238
2830
|
logger.info(
|
|
2239
|
-
|
|
2831
|
+
formatBullet(
|
|
2832
|
+
"\u65E0\u6CD5\u5728\u7EBF\u9A8C\u8BC1 Key\uFF08\u53EF\u80FD\u662F\u7F51\u7EDC\u539F\u56E0\uFF09\uFF0C\u5C06\u7EE7\u7EED\u4FDD\u5B58\u3002",
|
|
2833
|
+
"warning"
|
|
2834
|
+
)
|
|
2240
2835
|
);
|
|
2241
2836
|
} else {
|
|
2242
|
-
logger.info("Key
|
|
2837
|
+
logger.info(formatBullet("API Key \u9A8C\u8BC1\u6210\u529F\u3002", "success"));
|
|
2243
2838
|
}
|
|
2244
2839
|
} catch (err) {
|
|
2245
2840
|
if (err.message === "UNAUTHORIZED") {
|
|
2246
2841
|
logger.error("Invalid API key. Please check and try again.");
|
|
2247
2842
|
process.exit(1);
|
|
2248
2843
|
}
|
|
2249
|
-
logger.info(
|
|
2844
|
+
logger.info(
|
|
2845
|
+
formatBullet(
|
|
2846
|
+
"\u65E0\u6CD5\u5B8C\u6210\u5728\u7EBF\u9A8C\u8BC1\uFF08\u53EF\u80FD\u662F\u7F51\u7EDC\u539F\u56E0\uFF09\uFF0C\u5C06\u7EE7\u7EED\u4FDD\u5B58\u3002",
|
|
2847
|
+
"warning"
|
|
2848
|
+
)
|
|
2849
|
+
);
|
|
2250
2850
|
}
|
|
2251
2851
|
const config = {
|
|
2252
2852
|
apiKey,
|
|
@@ -2256,17 +2856,28 @@ Verifying key ${apiKey.slice(0, 8)}...`);
|
|
|
2256
2856
|
saveConfig(config);
|
|
2257
2857
|
const deviceId = getOrCreateDeviceId(config);
|
|
2258
2858
|
config.deviceId = deviceId;
|
|
2259
|
-
logger.info(
|
|
2859
|
+
logger.info(formatSection("\u7B2C 3 \u6B65\uFF1A\u5B8C\u6210\u672C\u5730\u6CE8\u518C"));
|
|
2260
2860
|
const tools = getDetectedTools();
|
|
2261
2861
|
if (tools.length > 0) {
|
|
2262
|
-
logger.info(
|
|
2862
|
+
logger.info(formatSection("\u68C0\u6D4B\u5230\u7684 AI CLI"));
|
|
2863
|
+
for (const tool of tools) {
|
|
2864
|
+
logger.info(formatBullet(tool.name, "success"));
|
|
2865
|
+
}
|
|
2263
2866
|
} else {
|
|
2264
|
-
logger.info("
|
|
2867
|
+
logger.info(formatSection("\u68C0\u6D4B\u5230\u7684 AI CLI"));
|
|
2868
|
+
logger.info(
|
|
2869
|
+
formatBullet(
|
|
2870
|
+
"\u5F53\u524D\u672A\u68C0\u6D4B\u5230\u5DF2\u5B89\u88C5\u5DE5\u5177\uFF0C\u7A0D\u540E\u5B89\u88C5\u540E\u4E5F\u53EF\u4EE5\u76F4\u63A5\u6267\u884C sync\u3002",
|
|
2871
|
+
"warning"
|
|
2872
|
+
)
|
|
2873
|
+
);
|
|
2265
2874
|
}
|
|
2266
|
-
logger.info("\
|
|
2875
|
+
logger.info(formatSection("\u9996\u6B21\u540C\u6B65"));
|
|
2876
|
+
logger.info(formatBullet("\u6B63\u5728\u4E0A\u4F20\u672C\u5730\u5DF2\u6709\u7684\u4F7F\u7528\u6570\u636E\u3002"));
|
|
2267
2877
|
await runSync(config, { source: "init" });
|
|
2268
|
-
logger.info(
|
|
2269
|
-
|
|
2878
|
+
logger.info(formatSection("\u521D\u59CB\u5316\u5B8C\u6210"));
|
|
2879
|
+
logger.info(formatBullet("TokenArena \u5DF2\u51C6\u5907\u5C31\u7EEA\u3002", "success"));
|
|
2880
|
+
logger.info(formatKeyValue("\u63A7\u5236\u53F0", `${apiUrl}/usage`));
|
|
2270
2881
|
await setupShellAlias();
|
|
2271
2882
|
}
|
|
2272
2883
|
async function setupShellAlias() {
|
|
@@ -2274,17 +2885,18 @@ async function setupShellAlias() {
|
|
|
2274
2885
|
if (!setup) {
|
|
2275
2886
|
return;
|
|
2276
2887
|
}
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
);
|
|
2281
|
-
if (
|
|
2888
|
+
const shouldCreateAlias = await promptConfirm({
|
|
2889
|
+
message: `\u662F\u5426\u4E3A ${setup.shellLabel} \u81EA\u52A8\u6DFB\u52A0 ta \u522B\u540D\uFF1F`,
|
|
2890
|
+
defaultValue: true
|
|
2891
|
+
});
|
|
2892
|
+
if (!shouldCreateAlias) {
|
|
2893
|
+
logger.info(formatBullet("\u5DF2\u8DF3\u8FC7 shell alias \u8BBE\u7F6E\u3002"));
|
|
2282
2894
|
return;
|
|
2283
2895
|
}
|
|
2284
2896
|
try {
|
|
2285
2897
|
await mkdir(dirname2(setup.configFile), { recursive: true });
|
|
2286
2898
|
let existingContent = "";
|
|
2287
|
-
if (
|
|
2899
|
+
if (existsSync12(setup.configFile)) {
|
|
2288
2900
|
existingContent = await readFile(setup.configFile, "utf-8");
|
|
2289
2901
|
}
|
|
2290
2902
|
const normalizedContent = existingContent.toLowerCase();
|
|
@@ -2292,10 +2904,7 @@ Set up ${setup.shellLabel} alias 'ta' for 'tokenarena'? (Y/n) `
|
|
|
2292
2904
|
(pattern) => normalizedContent.includes(pattern.toLowerCase())
|
|
2293
2905
|
);
|
|
2294
2906
|
if (aliasExists) {
|
|
2295
|
-
logger.info(
|
|
2296
|
-
`
|
|
2297
|
-
Alias 'ta' already exists in ${setup.configFile}. Skipping.`
|
|
2298
|
-
);
|
|
2907
|
+
logger.info(formatBullet(`\u522B\u540D ta \u5DF2\u5B58\u5728\uFF1A${setup.configFile}`));
|
|
2299
2908
|
return;
|
|
2300
2909
|
}
|
|
2301
2910
|
const aliasWithComment = `
|
|
@@ -2303,18 +2912,76 @@ Alias 'ta' already exists in ${setup.configFile}. Skipping.`
|
|
|
2303
2912
|
${setup.aliasLine}
|
|
2304
2913
|
`;
|
|
2305
2914
|
await appendFile(setup.configFile, aliasWithComment, "utf-8");
|
|
2306
|
-
logger.info(
|
|
2307
|
-
|
|
2915
|
+
logger.info(formatSection("Shell alias"));
|
|
2916
|
+
logger.info(formatBullet(`\u5DF2\u5199\u5165 ${setup.configFile}`, "success"));
|
|
2308
2917
|
logger.info(
|
|
2309
|
-
|
|
2918
|
+
formatKeyValue("\u751F\u6548\u65B9\u5F0F", `\u6267\u884C '${setup.sourceHint}' \u6216\u91CD\u542F\u7EC8\u7AEF`)
|
|
2310
2919
|
);
|
|
2311
|
-
logger.info("
|
|
2920
|
+
logger.info(formatKeyValue("\u4E4B\u540E\u53EF\u7528", "ta sync"));
|
|
2312
2921
|
} catch (err) {
|
|
2922
|
+
logger.info(formatSection("Shell alias"));
|
|
2313
2923
|
logger.info(
|
|
2314
|
-
|
|
2315
|
-
|
|
2924
|
+
formatBullet(
|
|
2925
|
+
`\u65E0\u6CD5\u5199\u5165 ${setup.configFile}: ${err.message}`,
|
|
2926
|
+
"warning"
|
|
2927
|
+
)
|
|
2316
2928
|
);
|
|
2317
|
-
logger.info(
|
|
2929
|
+
logger.info(formatKeyValue("\u8BF7\u624B\u52A8\u6DFB\u52A0", setup.aliasLine));
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/commands/daemon.ts
|
|
2934
|
+
var DEFAULT_INTERVAL = 5 * 6e4;
|
|
2935
|
+
function log(msg) {
|
|
2936
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
2937
|
+
process.stdout.write(`[${ts}] ${msg}
|
|
2938
|
+
`);
|
|
2939
|
+
}
|
|
2940
|
+
function sleep(ms) {
|
|
2941
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2942
|
+
}
|
|
2943
|
+
async function runDaemon(opts = {}) {
|
|
2944
|
+
const config = loadConfig();
|
|
2945
|
+
if (!config?.apiKey) {
|
|
2946
|
+
if (isInteractiveTerminal()) {
|
|
2947
|
+
logger.info(
|
|
2948
|
+
formatHeader(
|
|
2949
|
+
"\u5C1A\u672A\u5B8C\u6210\u521D\u59CB\u5316",
|
|
2950
|
+
"\u542F\u52A8 daemon \u524D\u9700\u8981\u5148\u914D\u7F6E\u6709\u6548\u7684 API Key\u3002"
|
|
2951
|
+
)
|
|
2952
|
+
);
|
|
2953
|
+
const shouldInit = await promptConfirm({
|
|
2954
|
+
message: "\u662F\u5426\u5148\u8FDB\u5165\u521D\u59CB\u5316\u6D41\u7A0B\uFF1F",
|
|
2955
|
+
defaultValue: true
|
|
2956
|
+
});
|
|
2957
|
+
if (shouldInit) {
|
|
2958
|
+
await runInit();
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
logger.info(formatBullet("\u5DF2\u53D6\u6D88\u542F\u52A8 daemon\u3002", "warning"));
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
logger.error("Not configured. Run `tokenarena init` first.");
|
|
2965
|
+
process.exit(1);
|
|
2966
|
+
}
|
|
2967
|
+
const interval = opts.interval || config.syncInterval || DEFAULT_INTERVAL;
|
|
2968
|
+
const intervalMin = Math.round(interval / 6e4);
|
|
2969
|
+
log(`Daemon started (sync every ${intervalMin}m, Ctrl+C to stop)`);
|
|
2970
|
+
while (true) {
|
|
2971
|
+
try {
|
|
2972
|
+
await runSync(config, {
|
|
2973
|
+
quiet: true,
|
|
2974
|
+
source: "daemon",
|
|
2975
|
+
throws: true
|
|
2976
|
+
});
|
|
2977
|
+
} catch (err) {
|
|
2978
|
+
if (err.message === "UNAUTHORIZED") {
|
|
2979
|
+
log("API key invalid. Exiting.");
|
|
2980
|
+
process.exit(1);
|
|
2981
|
+
}
|
|
2982
|
+
log(`Sync error: ${err.message}`);
|
|
2983
|
+
}
|
|
2984
|
+
await sleep(interval);
|
|
2318
2985
|
}
|
|
2319
2986
|
}
|
|
2320
2987
|
|
|
@@ -2324,58 +2991,94 @@ function formatMaybe(value) {
|
|
|
2324
2991
|
}
|
|
2325
2992
|
async function runStatus() {
|
|
2326
2993
|
const config = loadConfig();
|
|
2327
|
-
logger.info(
|
|
2994
|
+
logger.info(
|
|
2995
|
+
formatHeader(
|
|
2996
|
+
"TokenArena \u72B6\u6001",
|
|
2997
|
+
"\u67E5\u770B\u5F53\u524D\u914D\u7F6E\u3001\u5DF2\u68C0\u6D4B\u5DE5\u5177\u4EE5\u53CA\u6700\u8FD1\u4E00\u6B21\u540C\u6B65\u60C5\u51B5\u3002"
|
|
2998
|
+
)
|
|
2999
|
+
);
|
|
3000
|
+
logger.info(formatSection("\u914D\u7F6E"));
|
|
2328
3001
|
if (!config?.apiKey) {
|
|
2329
|
-
logger.info("
|
|
2330
|
-
logger.info("
|
|
3002
|
+
logger.info(formatKeyValue("\u72B6\u6001", formatStatusBadge("\u672A\u914D\u7F6E", "warning")));
|
|
3003
|
+
logger.info(formatBullet("\u8FD0\u884C tokenarena init \u5B8C\u6210\u9996\u6B21\u8BBE\u7F6E\u3002", "warning"));
|
|
2331
3004
|
} else {
|
|
2332
|
-
logger.info(
|
|
2333
|
-
logger.info(
|
|
2334
|
-
logger.info(
|
|
3005
|
+
logger.info(formatKeyValue("\u72B6\u6001", formatStatusBadge("\u5DF2\u914D\u7F6E", "success")));
|
|
3006
|
+
logger.info(formatKeyValue("\u914D\u7F6E\u6587\u4EF6", getConfigPath()));
|
|
3007
|
+
logger.info(formatKeyValue("API Key", maskSecret(config.apiKey)));
|
|
3008
|
+
logger.info(
|
|
3009
|
+
formatKeyValue("API URL", config.apiUrl || "https://token.poco-ai.com")
|
|
3010
|
+
);
|
|
2335
3011
|
if (config.syncInterval) {
|
|
2336
3012
|
logger.info(
|
|
2337
|
-
|
|
3013
|
+
formatKeyValue(
|
|
3014
|
+
"\u540C\u6B65\u95F4\u9694",
|
|
3015
|
+
`${Math.round(config.syncInterval / 6e4)} \u5206\u949F`
|
|
3016
|
+
)
|
|
2338
3017
|
);
|
|
2339
3018
|
}
|
|
2340
3019
|
}
|
|
2341
|
-
logger.info("\
|
|
3020
|
+
logger.info(formatSection("\u5DF2\u68C0\u6D4B\u5DE5\u5177"));
|
|
2342
3021
|
const detected = detectInstalledTools();
|
|
2343
3022
|
if (detected.length === 0) {
|
|
2344
|
-
logger.info("
|
|
3023
|
+
logger.info(formatBullet("\u672A\u68C0\u6D4B\u5230\u5DF2\u5B89\u88C5\u7684 AI CLI\u3002", "warning"));
|
|
2345
3024
|
} else {
|
|
2346
3025
|
for (const tool of detected) {
|
|
2347
|
-
logger.info(
|
|
3026
|
+
logger.info(formatBullet(tool.name, "success"));
|
|
2348
3027
|
}
|
|
2349
|
-
logger.info("");
|
|
2350
3028
|
}
|
|
2351
|
-
logger.info("
|
|
3029
|
+
logger.info(formatSection("\u652F\u6301\u7684\u5DE5\u5177"));
|
|
2352
3030
|
for (const tool of getAllTools()) {
|
|
2353
|
-
const installed = isToolInstalled(tool.id)
|
|
2354
|
-
logger.info(
|
|
3031
|
+
const installed = isToolInstalled(tool.id);
|
|
3032
|
+
logger.info(
|
|
3033
|
+
formatBullet(
|
|
3034
|
+
`${tool.name} \xB7 ${installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u53D1\u73B0"}`,
|
|
3035
|
+
installed ? "success" : "neutral"
|
|
3036
|
+
)
|
|
3037
|
+
);
|
|
2355
3038
|
}
|
|
2356
3039
|
const syncState = loadSyncState();
|
|
2357
|
-
logger.info("\
|
|
2358
|
-
|
|
2359
|
-
logger.info(
|
|
2360
|
-
|
|
3040
|
+
logger.info(formatSection("\u540C\u6B65\u72B6\u6001"));
|
|
3041
|
+
const statusTone = syncState.status === "idle" ? "success" : syncState.status === "syncing" ? "warning" : "danger";
|
|
3042
|
+
logger.info(
|
|
3043
|
+
formatKeyValue("\u72B6\u6001", formatStatusBadge(syncState.status, statusTone))
|
|
3044
|
+
);
|
|
3045
|
+
logger.info(formatKeyValue("\u4E0A\u6B21\u5C1D\u8BD5", formatMaybe(syncState.lastAttemptAt)));
|
|
3046
|
+
logger.info(formatKeyValue("\u4E0A\u6B21\u6210\u529F", formatMaybe(syncState.lastSuccessAt)));
|
|
2361
3047
|
if (syncState.lastSource) {
|
|
2362
|
-
logger.info(
|
|
3048
|
+
logger.info(formatKeyValue("\u89E6\u53D1\u6765\u6E90", syncState.lastSource));
|
|
2363
3049
|
}
|
|
2364
3050
|
if (syncState.lastError) {
|
|
2365
|
-
logger.info(
|
|
3051
|
+
logger.info(formatKeyValue("\u9519\u8BEF\u4FE1\u606F", syncState.lastError));
|
|
2366
3052
|
}
|
|
2367
3053
|
if (syncState.lastResult) {
|
|
2368
3054
|
logger.info(
|
|
2369
|
-
|
|
3055
|
+
formatKeyValue(
|
|
3056
|
+
"\u6700\u8FD1\u7ED3\u679C",
|
|
3057
|
+
`${syncState.lastResult.buckets} buckets, ${syncState.lastResult.sessions} sessions`
|
|
3058
|
+
)
|
|
2370
3059
|
);
|
|
2371
3060
|
}
|
|
2372
|
-
logger.info("");
|
|
2373
3061
|
}
|
|
2374
3062
|
|
|
2375
3063
|
// src/commands/sync.ts
|
|
2376
3064
|
async function runSyncCommand(opts = {}) {
|
|
2377
3065
|
const config = loadConfig();
|
|
2378
3066
|
if (!config?.apiKey) {
|
|
3067
|
+
if (isInteractiveTerminal()) {
|
|
3068
|
+
logger.info(
|
|
3069
|
+
formatHeader("\u5C1A\u672A\u5B8C\u6210\u521D\u59CB\u5316", "\u540C\u6B65\u524D\u9700\u8981\u5148\u914D\u7F6E\u6709\u6548\u7684 API Key\u3002")
|
|
3070
|
+
);
|
|
3071
|
+
const shouldInit = await promptConfirm({
|
|
3072
|
+
message: "\u662F\u5426\u73B0\u5728\u8FDB\u5165\u521D\u59CB\u5316\u6D41\u7A0B\uFF1F",
|
|
3073
|
+
defaultValue: true
|
|
3074
|
+
});
|
|
3075
|
+
if (shouldInit) {
|
|
3076
|
+
await runInit();
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
logger.info(formatBullet("\u5DF2\u53D6\u6D88\u540C\u6B65\u3002", "warning"));
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
2379
3082
|
logger.error("Not configured. Run `tokenarena init` first.");
|
|
2380
3083
|
process.exit(1);
|
|
2381
3084
|
}
|
|
@@ -2386,18 +3089,8 @@ async function runSyncCommand(opts = {}) {
|
|
|
2386
3089
|
}
|
|
2387
3090
|
|
|
2388
3091
|
// src/commands/uninstall.ts
|
|
2389
|
-
import { existsSync as
|
|
3092
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
2390
3093
|
import { homedir as homedir9, platform as platform2 } from "os";
|
|
2391
|
-
import { createInterface as createInterface2 } from "readline";
|
|
2392
|
-
function prompt2(question) {
|
|
2393
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2394
|
-
return new Promise((resolve2) => {
|
|
2395
|
-
rl.question(question, (answer) => {
|
|
2396
|
-
rl.close();
|
|
2397
|
-
resolve2(answer.trim());
|
|
2398
|
-
});
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
3094
|
function removeShellAlias() {
|
|
2402
3095
|
const shell = process.env.SHELL;
|
|
2403
3096
|
if (!shell) return;
|
|
@@ -2409,7 +3102,7 @@ function removeShellAlias() {
|
|
|
2409
3102
|
configFile = `${homedir9()}/.zshrc`;
|
|
2410
3103
|
break;
|
|
2411
3104
|
case "bash":
|
|
2412
|
-
if (platform2() === "darwin" &&
|
|
3105
|
+
if (platform2() === "darwin" && existsSync13(`${homedir9()}/.bash_profile`)) {
|
|
2413
3106
|
configFile = `${homedir9()}/.bash_profile`;
|
|
2414
3107
|
} else {
|
|
2415
3108
|
configFile = `${homedir9()}/.bashrc`;
|
|
@@ -2421,9 +3114,9 @@ function removeShellAlias() {
|
|
|
2421
3114
|
default:
|
|
2422
3115
|
return;
|
|
2423
3116
|
}
|
|
2424
|
-
if (!
|
|
3117
|
+
if (!existsSync13(configFile)) return;
|
|
2425
3118
|
try {
|
|
2426
|
-
let content =
|
|
3119
|
+
let content = readFileSync10(configFile, "utf-8");
|
|
2427
3120
|
const aliasPatterns = [
|
|
2428
3121
|
// zsh / bash format: alias ta="tokenarena"
|
|
2429
3122
|
new RegExp(
|
|
@@ -2445,7 +3138,7 @@ function removeShellAlias() {
|
|
|
2445
3138
|
content = next;
|
|
2446
3139
|
}
|
|
2447
3140
|
}
|
|
2448
|
-
|
|
3141
|
+
writeFileSync5(configFile, content, "utf-8");
|
|
2449
3142
|
logger.info(`Removed shell alias from ${configFile}`);
|
|
2450
3143
|
} catch (err) {
|
|
2451
3144
|
logger.warn(
|
|
@@ -2456,47 +3149,173 @@ function removeShellAlias() {
|
|
|
2456
3149
|
async function runUninstall() {
|
|
2457
3150
|
const configPath = getConfigPath();
|
|
2458
3151
|
const configDir = getConfigDir();
|
|
2459
|
-
if (!
|
|
2460
|
-
logger.info("
|
|
3152
|
+
if (!existsSync13(configPath)) {
|
|
3153
|
+
logger.info(formatHeader("\u5378\u8F7D TokenArena"));
|
|
3154
|
+
logger.info(formatBullet("\u672A\u53D1\u73B0\u672C\u5730\u914D\u7F6E\uFF0C\u65E0\u9700\u5378\u8F7D\u3002"));
|
|
2461
3155
|
return;
|
|
2462
3156
|
}
|
|
2463
3157
|
const config = loadConfig();
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
"\nAre you sure you want to uninstall? This will delete all local data. (y/N) "
|
|
3158
|
+
logger.info(
|
|
3159
|
+
formatHeader(
|
|
3160
|
+
"\u5378\u8F7D TokenArena",
|
|
3161
|
+
"\u8BE5\u64CD\u4F5C\u4F1A\u5220\u9664\u672C\u5730\u914D\u7F6E\u3001\u540C\u6B65\u72B6\u6001\u4E0E\u8FD0\u884C\u65F6\u6587\u4EF6\u3002"
|
|
3162
|
+
)
|
|
2470
3163
|
);
|
|
2471
|
-
if (
|
|
2472
|
-
logger.info("
|
|
3164
|
+
if (config?.apiKey) {
|
|
3165
|
+
logger.info(formatKeyValue("API Key", maskSecret(config.apiKey)));
|
|
3166
|
+
}
|
|
3167
|
+
logger.info(formatKeyValue("\u914D\u7F6E\u76EE\u5F55", configDir));
|
|
3168
|
+
logger.info(formatKeyValue("\u72B6\u6001\u76EE\u5F55", getStateDir()));
|
|
3169
|
+
logger.info(formatKeyValue("\u8FD0\u884C\u76EE\u5F55", getRuntimeDirPath()));
|
|
3170
|
+
const shouldUninstall = await promptConfirm({
|
|
3171
|
+
message: "\u786E\u8BA4\u7EE7\u7EED\u5378\u8F7D\u672C\u5730 TokenArena \u6570\u636E\uFF1F",
|
|
3172
|
+
defaultValue: false
|
|
3173
|
+
});
|
|
3174
|
+
if (!shouldUninstall) {
|
|
3175
|
+
logger.info(formatBullet("\u5DF2\u53D6\u6D88\u5378\u8F7D\u3002", "warning"));
|
|
2473
3176
|
return;
|
|
2474
3177
|
}
|
|
2475
3178
|
deleteConfig();
|
|
2476
|
-
logger.info("
|
|
2477
|
-
|
|
3179
|
+
logger.info(formatSection("\u6267\u884C\u7ED3\u679C"));
|
|
3180
|
+
logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u6587\u4EF6\u3002", "success"));
|
|
3181
|
+
if (existsSync13(configDir)) {
|
|
2478
3182
|
try {
|
|
2479
3183
|
rmSync2(configDir, { recursive: false, force: true });
|
|
2480
|
-
logger.info("
|
|
3184
|
+
logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u76EE\u5F55\u3002", "success"));
|
|
2481
3185
|
} catch {
|
|
2482
3186
|
}
|
|
2483
3187
|
}
|
|
2484
3188
|
const stateDir = getStateDir();
|
|
2485
|
-
if (
|
|
3189
|
+
if (existsSync13(stateDir)) {
|
|
2486
3190
|
rmSync2(stateDir, { recursive: true, force: true });
|
|
2487
|
-
logger.info("
|
|
3191
|
+
logger.info(formatBullet("\u5DF2\u5220\u9664\u72B6\u6001\u6570\u636E\u3002", "success"));
|
|
2488
3192
|
}
|
|
2489
3193
|
const runtimeDir = getRuntimeDirPath();
|
|
2490
|
-
if (
|
|
3194
|
+
if (existsSync13(runtimeDir)) {
|
|
2491
3195
|
rmSync2(runtimeDir, { recursive: true, force: true });
|
|
2492
|
-
logger.info("
|
|
3196
|
+
logger.info(formatBullet("\u5DF2\u5220\u9664\u8FD0\u884C\u65F6\u6570\u636E\u3002", "success"));
|
|
2493
3197
|
}
|
|
2494
3198
|
removeShellAlias();
|
|
2495
|
-
logger.info("\
|
|
3199
|
+
logger.info(formatSection("\u5B8C\u6210"));
|
|
3200
|
+
logger.info(formatBullet("TokenArena \u5DF2\u4ECE\u672C\u5730\u5378\u8F7D\u5B8C\u6210\u3002", "success"));
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/commands/home.ts
|
|
3204
|
+
function logHomeSummary() {
|
|
3205
|
+
const config = loadConfig();
|
|
3206
|
+
const configured = Boolean(config?.apiKey);
|
|
3207
|
+
logger.info(
|
|
3208
|
+
formatHeader(
|
|
3209
|
+
"TokenArena CLI",
|
|
3210
|
+
"\u901A\u8FC7\u66F4\u53CB\u597D\u7684\u4EA4\u4E92\u5B8C\u6210\u521D\u59CB\u5316\u3001\u540C\u6B65\u3001\u914D\u7F6E\u4E0E\u6E05\u7406\u3002"
|
|
3211
|
+
)
|
|
3212
|
+
);
|
|
3213
|
+
logger.info(
|
|
3214
|
+
formatKeyValue(
|
|
3215
|
+
"\u914D\u7F6E\u72B6\u6001",
|
|
3216
|
+
configured ? formatStatusBadge("\u5DF2\u914D\u7F6E", "success") : formatStatusBadge("\u672A\u914D\u7F6E", "warning")
|
|
3217
|
+
)
|
|
3218
|
+
);
|
|
3219
|
+
if (configured && config) {
|
|
3220
|
+
logger.info(formatKeyValue("API Key", maskSecret(config.apiKey)));
|
|
3221
|
+
logger.info(
|
|
3222
|
+
formatKeyValue("API \u5730\u5740", config.apiUrl || "https://token.poco-ai.com")
|
|
3223
|
+
);
|
|
3224
|
+
} else {
|
|
3225
|
+
logger.info(formatBullet("\u5EFA\u8BAE\u5148\u8FD0\u884C\u521D\u59CB\u5316\u6D41\u7A0B\u7ED1\u5B9A API Key\u3002", "warning"));
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
async function pickHomeAction() {
|
|
3229
|
+
return promptSelect({
|
|
3230
|
+
message: "\u8BF7\u9009\u62E9\u8981\u6267\u884C\u7684\u64CD\u4F5C",
|
|
3231
|
+
choices: [
|
|
3232
|
+
{
|
|
3233
|
+
name: "\u521D\u59CB\u5316 TokenArena",
|
|
3234
|
+
value: "init",
|
|
3235
|
+
description: "\u914D\u7F6E API Key\u3001\u68C0\u6D4B\u5DE5\u5177\u5E76\u6267\u884C\u9996\u6B21\u540C\u6B65"
|
|
3236
|
+
},
|
|
3237
|
+
{
|
|
3238
|
+
name: "\u67E5\u770B\u5F53\u524D\u72B6\u6001",
|
|
3239
|
+
value: "status",
|
|
3240
|
+
description: "\u67E5\u770B\u914D\u7F6E\u3001\u5DE5\u5177\u68C0\u6D4B\u7ED3\u679C\u4E0E\u6700\u8FD1\u540C\u6B65\u72B6\u6001"
|
|
3241
|
+
},
|
|
3242
|
+
{
|
|
3243
|
+
name: "\u7ACB\u5373\u540C\u6B65",
|
|
3244
|
+
value: "sync",
|
|
3245
|
+
description: "\u624B\u52A8\u4E0A\u4F20\u672C\u5730\u6700\u65B0 token \u4F7F\u7528\u6570\u636E"
|
|
3246
|
+
},
|
|
3247
|
+
{
|
|
3248
|
+
name: "\u7BA1\u7406\u914D\u7F6E",
|
|
3249
|
+
value: "config",
|
|
3250
|
+
description: "\u67E5\u770B\u6216\u4FEE\u6539 API Key\u3001API \u5730\u5740\u3001\u540C\u6B65\u95F4\u9694\u7B49\u914D\u7F6E"
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
name: "\u542F\u52A8\u5B88\u62A4\u540C\u6B65",
|
|
3254
|
+
value: "daemon",
|
|
3255
|
+
description: "\u6301\u7EED\u540E\u53F0\u540C\u6B65\uFF0C\u9002\u5408\u957F\u671F\u8FD0\u884C"
|
|
3256
|
+
},
|
|
3257
|
+
{
|
|
3258
|
+
name: "\u5378\u8F7D\u672C\u5730\u914D\u7F6E",
|
|
3259
|
+
value: "uninstall",
|
|
3260
|
+
description: "\u5220\u9664\u672C\u5730\u914D\u7F6E\u3001\u72B6\u6001\u4E0E\u8FD0\u884C\u65F6\u6587\u4EF6"
|
|
3261
|
+
},
|
|
3262
|
+
{
|
|
3263
|
+
name: "\u67E5\u770B\u5E2E\u52A9",
|
|
3264
|
+
value: "help",
|
|
3265
|
+
description: "\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E2E\u52A9"
|
|
3266
|
+
},
|
|
3267
|
+
{
|
|
3268
|
+
name: "\u9000\u51FA",
|
|
3269
|
+
value: "exit",
|
|
3270
|
+
description: "\u7ED3\u675F\u5F53\u524D\u4EA4\u4E92"
|
|
3271
|
+
}
|
|
3272
|
+
]
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
async function runHome(program) {
|
|
3276
|
+
while (true) {
|
|
3277
|
+
logHomeSummary();
|
|
3278
|
+
const action = await pickHomeAction();
|
|
3279
|
+
logger.info("");
|
|
3280
|
+
switch (action) {
|
|
3281
|
+
case "init":
|
|
3282
|
+
await runInit();
|
|
3283
|
+
break;
|
|
3284
|
+
case "status":
|
|
3285
|
+
await runStatus();
|
|
3286
|
+
break;
|
|
3287
|
+
case "sync":
|
|
3288
|
+
await runSyncCommand();
|
|
3289
|
+
break;
|
|
3290
|
+
case "config":
|
|
3291
|
+
await handleConfig([]);
|
|
3292
|
+
break;
|
|
3293
|
+
case "daemon":
|
|
3294
|
+
await runDaemon();
|
|
3295
|
+
return;
|
|
3296
|
+
case "uninstall":
|
|
3297
|
+
await runUninstall();
|
|
3298
|
+
break;
|
|
3299
|
+
case "help":
|
|
3300
|
+
program.outputHelp();
|
|
3301
|
+
break;
|
|
3302
|
+
case "exit":
|
|
3303
|
+
logger.info(formatBullet("\u5DF2\u9000\u51FA\u4EA4\u4E92\u5F0F\u4E3B\u9875\u3002", "neutral"));
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
const continueAnswer = await promptConfirm({
|
|
3307
|
+
message: "\u662F\u5426\u7EE7\u7EED\u6267\u884C\u5176\u4ED6\u64CD\u4F5C\uFF1F",
|
|
3308
|
+
defaultValue: true
|
|
3309
|
+
});
|
|
3310
|
+
if (!continueAnswer) {
|
|
3311
|
+
logger.info(formatBullet("\u4E0B\u6B21\u53EF\u76F4\u63A5\u8FD0\u884C tokenarena \u7EE7\u7EED\u3002", "neutral"));
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
2496
3315
|
}
|
|
2497
3316
|
|
|
2498
3317
|
// src/infrastructure/runtime/cli-version.ts
|
|
2499
|
-
import { readFileSync as
|
|
3318
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2500
3319
|
import { dirname as dirname3, join as join12 } from "path";
|
|
2501
3320
|
import { fileURLToPath } from "url";
|
|
2502
3321
|
var FALLBACK_VERSION = "0.0.0";
|
|
@@ -2511,7 +3330,7 @@ function getCliVersion(metaUrl = import.meta.url) {
|
|
|
2511
3330
|
"package.json"
|
|
2512
3331
|
);
|
|
2513
3332
|
try {
|
|
2514
|
-
const packageJson = JSON.parse(
|
|
3333
|
+
const packageJson = JSON.parse(readFileSync11(packageJsonPath, "utf-8"));
|
|
2515
3334
|
cachedVersion = typeof packageJson.version === "string" ? packageJson.version : FALLBACK_VERSION;
|
|
2516
3335
|
} catch {
|
|
2517
3336
|
cachedVersion = FALLBACK_VERSION;
|
|
@@ -2524,11 +3343,15 @@ var CLI_VERSION = getCliVersion();
|
|
|
2524
3343
|
function createCli() {
|
|
2525
3344
|
const program = new Command();
|
|
2526
3345
|
program.name("tokenarena").description("Track token burn across AI coding tools").version(CLI_VERSION).showHelpAfterError().showSuggestionAfterError().helpCommand("help [command]", "Display help for command");
|
|
2527
|
-
program.action(() => {
|
|
3346
|
+
program.action(async () => {
|
|
2528
3347
|
const userArgs = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
2529
3348
|
if (userArgs.length > 0) {
|
|
2530
3349
|
program.error(`unknown command '${userArgs[0]}'`);
|
|
2531
3350
|
}
|
|
3351
|
+
if (isInteractiveTerminal()) {
|
|
3352
|
+
await runHome(program);
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
2532
3355
|
program.help();
|
|
2533
3356
|
});
|
|
2534
3357
|
program.command("init").description("Initialize configuration with API key").option("--api-url <url>", "Custom API server URL").action(async (opts) => {
|
|
@@ -2543,9 +3366,12 @@ function createCli() {
|
|
|
2543
3366
|
program.command("status").description("Show configuration and detected tools").action(async () => {
|
|
2544
3367
|
await runStatus();
|
|
2545
3368
|
});
|
|
2546
|
-
program.command("config").description("Manage configuration").argument("
|
|
2547
|
-
|
|
2548
|
-
|
|
3369
|
+
program.command("config").description("Manage configuration").argument("[subcommand]", "get|set|show").argument("[key]", "Config key").argument("[value]", "Config value").allowUnknownOption(true).action(async (subcommand, key, value) => {
|
|
3370
|
+
await handleConfig(
|
|
3371
|
+
[subcommand, key, value].filter(
|
|
3372
|
+
(item) => typeof item === "string"
|
|
3373
|
+
)
|
|
3374
|
+
);
|
|
2549
3375
|
});
|
|
2550
3376
|
program.command("uninstall").description("Remove all local configuration and data").action(async () => {
|
|
2551
3377
|
await runUninstall();
|
|
@@ -2554,7 +3380,7 @@ function createCli() {
|
|
|
2554
3380
|
}
|
|
2555
3381
|
|
|
2556
3382
|
// src/infrastructure/runtime/main-module.ts
|
|
2557
|
-
import { existsSync as
|
|
3383
|
+
import { existsSync as existsSync14, realpathSync } from "fs";
|
|
2558
3384
|
import { resolve } from "path";
|
|
2559
3385
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2560
3386
|
function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
|
|
@@ -2565,7 +3391,7 @@ function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
|
|
|
2565
3391
|
try {
|
|
2566
3392
|
return realpathSync(argvEntry) === realpathSync(currentModulePath);
|
|
2567
3393
|
} catch {
|
|
2568
|
-
if (!
|
|
3394
|
+
if (!existsSync14(argvEntry)) {
|
|
2569
3395
|
return false;
|
|
2570
3396
|
}
|
|
2571
3397
|
return resolve(argvEntry) === resolve(currentModulePath);
|
|
@@ -2576,12 +3402,12 @@ function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
|
|
|
2576
3402
|
function normalizeArgv(argv) {
|
|
2577
3403
|
return argv.filter((arg, index) => index < 2 || arg !== "--");
|
|
2578
3404
|
}
|
|
2579
|
-
function run(argv = process.argv) {
|
|
3405
|
+
async function run(argv = process.argv) {
|
|
2580
3406
|
const program = createCli();
|
|
2581
|
-
program.
|
|
3407
|
+
await program.parseAsync(normalizeArgv(argv));
|
|
2582
3408
|
}
|
|
2583
3409
|
if (isMainModule()) {
|
|
2584
|
-
run();
|
|
3410
|
+
void run();
|
|
2585
3411
|
}
|
|
2586
3412
|
export {
|
|
2587
3413
|
normalizeArgv,
|