@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.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 = "http://localhost:3000";
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 handleConfig(args) {
1223
- const sub = args[0];
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
- const key = args[1];
1534
+ let key = args[1];
1227
1535
  if (!key) {
1228
- logger.error("Usage: tokenarena config get <key>");
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
- const key = args[1];
1241
- let value = args[2];
1242
- if (!key || value === void 0) {
1243
- logger.error("Usage: tokenarena config set <key> <value>");
1244
- process.exit(1);
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 (!VALID_KEYS.includes(key)) {
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: "http://localhost:3000"
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
- value = parseInt(value, 10);
1257
- if (Number.isNaN(value)) {
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
- const record = config;
1263
- record[key] = value;
1603
+ record[key] = normalized;
1264
1604
  saveConfig(config);
1265
- logger.info(`Set ${key} = ${value}`);
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
- const config = loadConfig();
1270
- if (!config) {
1271
- console.log("{}");
1614
+ if (interactive) {
1615
+ printConfigShow();
1272
1616
  } else {
1273
- console.log(JSON.stringify(config, null, 2));
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(input) {
1290
- if (input.mode === "disabled") {
1634
+ function toProjectIdentity(input2) {
1635
+ if (input2.mode === "disabled") {
1291
1636
  return { projectKey: "unknown", projectLabel: "Unknown Project" };
1292
1637
  }
1293
- if (input.mode === "raw") {
1294
- return { projectKey: input.project, projectLabel: input.project };
1638
+ if (input2.mode === "raw") {
1639
+ return { projectKey: input2.project, projectLabel: input2.project };
1295
1640
  }
1296
- const projectKey = createHmac("sha256", input.salt).update(input.project).digest("hex").slice(0, 16);
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 URL("/api/usage/ingest", this.apiUrl);
1340
- const payload = {
1341
- schemaVersion: 2,
1342
- device,
1343
- buckets,
1344
- sessions: sessions ?? []
1345
- };
1346
- const body = Buffer.from(JSON.stringify(payload));
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 URL("/api/usage/settings", this.apiUrl);
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 URL("/api/usage/ingest", this.apiUrl);
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 formatTime(secs) {
1740
- if (secs < 60) return `${secs}s`;
1741
- const h = Math.floor(secs / 3600);
1742
- const m = Math.floor(secs % 3600 / 60);
1743
- return h > 0 ? m > 0 ? `${h}h ${m}m` : `${h}h` : `${m}m`;
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
- if (!quiet) {
1893
- const projectModeLabel = {
1894
- hashed: "\u54C8\u5E0C\u5316",
1895
- raw: "\u539F\u59CB\u540D\u79F0",
1896
- disabled: "\u5DF2\u9690\u85CF"
1897
- };
1898
- logger.info(`\u{1F4C2} \u9879\u76EE\u6A21\u5F0F: ${projectModeLabel[settings.projectMode]}`);
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
- const bucketBatches = Math.ceil(uploadBuckets.length / BATCH_SIZE);
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
- uploadSessions.length / SESSION_BATCH_SIZE
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 (uploadBuckets.length > 0) {
1908
- parts.push(`${uploadBuckets.length} buckets`);
2536
+ if (changedBuckets.length > 0) {
2537
+ parts.push(`${changedBuckets.length} buckets`);
1909
2538
  }
1910
- if (uploadSessions.length > 0) {
1911
- parts.push(`${uploadSessions.length} sessions`);
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 = uploadBuckets.slice(
2554
+ const batch = changedBuckets.slice(
1919
2555
  batchIdx * BATCH_SIZE,
1920
2556
  (batchIdx + 1) * BATCH_SIZE
1921
2557
  );
1922
- const batchSessions = uploadSessions.slice(
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
- const pct = Math.round(sent / total * 100);
1934
- process.stdout.write(
1935
- `\r${prefix}${formatBytes(sent)}/${formatBytes(total)} (${pct}%)\x1B[K`
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
- if (!quiet && (totalBatches > 1 || uploadBuckets.length > 0)) {
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 existsSync11 } from "fs";
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 prompt(question) {
2067
- const rl = createInterface({ input: process.stdin, output: process.stdout });
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 ?? existsSync11;
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("\n tokenarena - Token Usage Tracker\n");
2794
+ logger.info(formatHeader("TokenArena \u521D\u59CB\u5316"));
2214
2795
  const existing = loadConfig();
2215
2796
  if (existing?.apiKey) {
2216
- const answer = await prompt("Config already exists. Overwrite? (y/N) ");
2217
- if (answer.toLowerCase() !== "y") {
2218
- logger.info("Cancelled.");
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
- logger.info(`Open ${apiUrl}/usage and create your API key from Settings.
2224
- `);
2225
- openBrowser(`${apiUrl}/usage`);
2226
- let apiKey;
2227
- while (true) {
2228
- apiKey = await prompt("Paste your API key: ");
2229
- if (validateApiKey(apiKey)) break;
2230
- logger.info('Invalid key - must start with "ta_". Try again.');
2231
- }
2232
- logger.info(`
2233
- Verifying key ${apiKey.slice(0, 8)}...`);
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
- "Could not verify key settings (network error). Saving anyway.\n"
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 verified.\n");
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("Could not verify key (network error). Saving anyway.\n");
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(`Device registered: ${deviceId.slice(0, 8)}...`);
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(`Detected tools: ${tools.map((tool) => tool.name).join(", ")}`);
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("No AI coding tools detected. Install one and re-run init.");
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("\nRunning initial sync...");
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
- Setup complete! View your dashboard at: ${apiUrl}/usage`);
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 answer = await prompt(
2278
- `
2279
- Set up ${setup.shellLabel} alias 'ta' for 'tokenarena'? (Y/n) `
2280
- );
2281
- if (answer.toLowerCase() === "n") {
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 (existsSync11(setup.configFile)) {
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
- Added alias to ${setup.configFile}`);
2915
+ logger.info(formatSection("Shell alias"));
2916
+ logger.info(formatBullet(`\u5DF2\u5199\u5165 ${setup.configFile}`, "success"));
2308
2917
  logger.info(
2309
- ` Run '${setup.sourceHint}' or restart your terminal to use it.`
2918
+ formatKeyValue("\u751F\u6548\u65B9\u5F0F", `\u6267\u884C '${setup.sourceHint}' \u6216\u91CD\u542F\u7EC8\u7AEF`)
2310
2919
  );
2311
- logger.info(" Then you can use: ta sync");
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
- Could not write to ${setup.configFile}: ${err.message}`
2924
+ formatBullet(
2925
+ `\u65E0\u6CD5\u5199\u5165 ${setup.configFile}: ${err.message}`,
2926
+ "warning"
2927
+ )
2316
2928
  );
2317
- logger.info(` Add this line manually: ${setup.aliasLine}`);
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("\ntokenarena status\n");
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(" Config: not configured");
2330
- logger.info(" Run `tokenarena init` to set up.\n");
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(` Config: ${getConfigPath()}`);
2333
- logger.info(` API key: ${config.apiKey.slice(0, 8)}...`);
2334
- logger.info(` API URL: ${config.apiUrl || "http://localhost:3000"}`);
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
- ` Sync interval: ${Math.round(config.syncInterval / 6e4)}m`
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("\n Detected tools:");
3020
+ logger.info(formatSection("\u5DF2\u68C0\u6D4B\u5DE5\u5177"));
2342
3021
  const detected = detectInstalledTools();
2343
3022
  if (detected.length === 0) {
2344
- logger.info(" (none)\n");
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(` ${tool.name}`);
3026
+ logger.info(formatBullet(tool.name, "success"));
2348
3027
  }
2349
- logger.info("");
2350
3028
  }
2351
- logger.info(" All supported tools:");
3029
+ logger.info(formatSection("\u652F\u6301\u7684\u5DE5\u5177"));
2352
3030
  for (const tool of getAllTools()) {
2353
- const installed = isToolInstalled(tool.id) ? "installed" : "not found";
2354
- logger.info(` ${tool.name}: ${installed}`);
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("\n Sync state:");
2358
- logger.info(` Status: ${syncState.status}`);
2359
- logger.info(` Last attempt: ${formatMaybe(syncState.lastAttemptAt)}`);
2360
- logger.info(` Last success: ${formatMaybe(syncState.lastSuccessAt)}`);
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(` Last source: ${syncState.lastSource}`);
3048
+ logger.info(formatKeyValue("\u89E6\u53D1\u6765\u6E90", syncState.lastSource));
2363
3049
  }
2364
3050
  if (syncState.lastError) {
2365
- logger.info(` Last error: ${syncState.lastError}`);
3051
+ logger.info(formatKeyValue("\u9519\u8BEF\u4FE1\u606F", syncState.lastError));
2366
3052
  }
2367
3053
  if (syncState.lastResult) {
2368
3054
  logger.info(
2369
- ` Last result: ${syncState.lastResult.buckets} buckets, ${syncState.lastResult.sessions} sessions`
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 existsSync12, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
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" && existsSync12(`${homedir9()}/.bash_profile`)) {
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 (!existsSync12(configFile)) return;
3117
+ if (!existsSync13(configFile)) return;
2425
3118
  try {
2426
- let content = readFileSync9(configFile, "utf-8");
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
- writeFileSync4(configFile, content, "utf-8");
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 (!existsSync12(configPath)) {
2460
- logger.info("No configuration found. Nothing to uninstall.");
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
- if (config?.apiKey) {
2465
- logger.info(`API key: ${config.apiKey.slice(0, 8)}...`);
2466
- }
2467
- logger.info(`Config directory: ${configDir}`);
2468
- const answer = await prompt2(
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 (answer.toLowerCase() !== "y") {
2472
- logger.info("Cancelled.");
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("Deleted configuration file.");
2477
- if (existsSync12(configDir)) {
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("Deleted config directory.");
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 (existsSync12(stateDir)) {
3189
+ if (existsSync13(stateDir)) {
2486
3190
  rmSync2(stateDir, { recursive: true, force: true });
2487
- logger.info("Deleted state data.");
3191
+ logger.info(formatBullet("\u5DF2\u5220\u9664\u72B6\u6001\u6570\u636E\u3002", "success"));
2488
3192
  }
2489
3193
  const runtimeDir = getRuntimeDirPath();
2490
- if (existsSync12(runtimeDir)) {
3194
+ if (existsSync13(runtimeDir)) {
2491
3195
  rmSync2(runtimeDir, { recursive: true, force: true });
2492
- logger.info("Deleted runtime data.");
3196
+ logger.info(formatBullet("\u5DF2\u5220\u9664\u8FD0\u884C\u65F6\u6570\u636E\u3002", "success"));
2493
3197
  }
2494
3198
  removeShellAlias();
2495
- logger.info("\nTokenArena has been uninstalled successfully.");
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 readFileSync10 } from "fs";
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(readFileSync10(packageJsonPath, "utf-8"));
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("<subcommand>", "get|set|show").argument("[key]", "Config key").argument("[value]", "Config value").allowUnknownOption(true).action((_subcommand, _key, _value, cmd) => {
2547
- const args = cmd.args.slice(1);
2548
- handleConfig(args);
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 existsSync13, realpathSync } from "fs";
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 (!existsSync13(argvEntry)) {
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.parse(normalizeArgv(argv));
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,