@poco-ai/tokenarena 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,14 +1631,14 @@ 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)}`
@@ -1303,10 +1648,19 @@ function toProjectIdentity(input) {
1303
1648
  // src/infrastructure/api/client.ts
1304
1649
  import http from "http";
1305
1650
  import https from "https";
1306
- import { URL } from "url";
1651
+ import { URL as URL2 } from "url";
1307
1652
  var MAX_RETRIES = 3;
1308
1653
  var INITIAL_DELAY = 1e3;
1309
1654
  var TIMEOUT_MS = 6e4;
1655
+ function getIngestPayloadSize(device, buckets, sessions) {
1656
+ const payload = {
1657
+ schemaVersion: 2,
1658
+ device,
1659
+ buckets,
1660
+ sessions: sessions ?? []
1661
+ };
1662
+ return Buffer.byteLength(JSON.stringify(payload));
1663
+ }
1310
1664
  var ApiClient = class {
1311
1665
  constructor(apiUrl, apiKey) {
1312
1666
  this.apiUrl = apiUrl;
@@ -1336,14 +1690,15 @@ var ApiClient = class {
1336
1690
  }
1337
1691
  sendIngest(device, buckets, sessions, onProgress) {
1338
1692
  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));
1693
+ const url = new URL2("/api/usage/ingest", this.apiUrl);
1694
+ const body = Buffer.from(
1695
+ JSON.stringify({
1696
+ schemaVersion: 2,
1697
+ device,
1698
+ buckets,
1699
+ sessions: sessions ?? []
1700
+ })
1701
+ );
1347
1702
  const totalBytes = body.length;
1348
1703
  const mod = url.protocol === "https:" ? https : http;
1349
1704
  const req = mod.request(
@@ -1416,7 +1771,7 @@ var ApiClient = class {
1416
1771
  */
1417
1772
  async fetchSettings() {
1418
1773
  return new Promise((resolve2, reject) => {
1419
- const url = new URL("/api/usage/settings", this.apiUrl);
1774
+ const url = new URL2("/api/usage/settings", this.apiUrl);
1420
1775
  const mod = url.protocol === "https:" ? https : http;
1421
1776
  const req = mod.request(
1422
1777
  url,
@@ -1467,7 +1822,7 @@ var ApiClient = class {
1467
1822
  */
1468
1823
  async deleteAllData(opts) {
1469
1824
  return new Promise((resolve2, reject) => {
1470
- const url = new URL("/api/usage/ingest", this.apiUrl);
1825
+ const url = new URL2("/api/usage/ingest", this.apiUrl);
1471
1826
  if (opts?.hostname) {
1472
1827
  url.searchParams.set("hostname", opts.hostname);
1473
1828
  }
@@ -1731,16 +2086,24 @@ function getDetectedTools() {
1731
2086
  // src/services/sync-service.ts
1732
2087
  var BATCH_SIZE = 100;
1733
2088
  var SESSION_BATCH_SIZE = 500;
2089
+ var PROGRESS_BAR_WIDTH = 28;
1734
2090
  function formatBytes(bytes) {
1735
2091
  if (bytes < 1024) return `${bytes}B`;
1736
2092
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1737
2093
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1738
2094
  }
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`;
2095
+ function renderProgressBar(progress) {
2096
+ const safeProgress = Math.max(0, Math.min(progress, 1));
2097
+ const filled = Math.round(safeProgress * PROGRESS_BAR_WIDTH);
2098
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(PROGRESS_BAR_WIDTH - filled)}`;
2099
+ }
2100
+ function writeUploadProgress(sent, total, batchNum, totalBatches) {
2101
+ const pct = total > 0 ? Math.round(sent / total * 100) : 100;
2102
+ const progressBar = renderProgressBar(total > 0 ? sent / total : 1);
2103
+ const batchLabel = totalBatches > 1 ? ` \xB7 batch ${batchNum}/${totalBatches}` : "";
2104
+ process.stdout.write(
2105
+ `\r Uploading ${progressBar} ${String(pct).padStart(3, " ")}% \xB7 ${formatBytes(sent)}/${formatBytes(total)}${batchLabel}\x1B[K`
2106
+ );
1744
2107
  }
1745
2108
  function toDeviceMetadata(config) {
1746
2109
  return {
@@ -1889,19 +2252,30 @@ async function runSync(config, opts = {}) {
1889
2252
  const device = toDeviceMetadata(config);
1890
2253
  const uploadBuckets = toUploadBuckets(allBuckets, settings, device);
1891
2254
  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]}`);
1899
- }
1900
2255
  const bucketBatches = Math.ceil(uploadBuckets.length / BATCH_SIZE);
1901
2256
  const sessionBatches = Math.ceil(
1902
2257
  uploadSessions.length / SESSION_BATCH_SIZE
1903
2258
  );
1904
2259
  const totalBatches = Math.max(bucketBatches, sessionBatches, 1);
2260
+ const batchPayloadSizes = Array.from(
2261
+ { length: totalBatches },
2262
+ (_, batchIdx) => getIngestPayloadSize(
2263
+ device,
2264
+ uploadBuckets.slice(
2265
+ batchIdx * BATCH_SIZE,
2266
+ (batchIdx + 1) * BATCH_SIZE
2267
+ ),
2268
+ uploadSessions.slice(
2269
+ batchIdx * SESSION_BATCH_SIZE,
2270
+ (batchIdx + 1) * SESSION_BATCH_SIZE
2271
+ )
2272
+ )
2273
+ );
2274
+ const totalPayloadBytes = batchPayloadSizes.reduce(
2275
+ (sum, size) => sum + size,
2276
+ 0
2277
+ );
2278
+ let uploadedBytesBeforeBatch = 0;
1905
2279
  if (!quiet) {
1906
2280
  const parts = [];
1907
2281
  if (uploadBuckets.length > 0) {
@@ -1924,22 +2298,30 @@ async function runSync(config, opts = {}) {
1924
2298
  (batchIdx + 1) * SESSION_BATCH_SIZE
1925
2299
  );
1926
2300
  const batchNum = batchIdx + 1;
1927
- const prefix = totalBatches > 1 ? ` [${batchNum}/${totalBatches}] ` : " ";
1928
2301
  const result = await apiClient.ingest(
1929
2302
  device,
1930
2303
  batch,
1931
2304
  batchSessions.length > 0 ? batchSessions : void 0,
1932
2305
  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`
2306
+ writeUploadProgress(
2307
+ uploadedBytesBeforeBatch + sent,
2308
+ totalPayloadBytes || total,
2309
+ batchNum,
2310
+ totalBatches
1936
2311
  );
1937
2312
  }
1938
2313
  );
1939
2314
  totalIngested += result.ingested ?? batch.length;
1940
2315
  totalSessionsSynced += result.sessions ?? batchSessions.length;
2316
+ uploadedBytesBeforeBatch += batchPayloadSizes[batchIdx] ?? 0;
1941
2317
  }
1942
2318
  if (!quiet && (totalBatches > 1 || uploadBuckets.length > 0)) {
2319
+ writeUploadProgress(
2320
+ totalPayloadBytes,
2321
+ totalPayloadBytes,
2322
+ totalBatches,
2323
+ totalBatches
2324
+ );
1943
2325
  process.stdout.write("\n");
1944
2326
  }
1945
2327
  const syncParts = [`${totalIngested} buckets`];
@@ -1947,23 +2329,6 @@ async function runSync(config, opts = {}) {
1947
2329
  syncParts.push(`${totalSessionsSynced} sessions`);
1948
2330
  }
1949
2331
  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
2332
  if (!quiet) {
1968
2333
  logger.info(`
1969
2334
  View your dashboard at: ${apiUrl}/usage`);
@@ -2016,64 +2381,17 @@ View your dashboard at: ${apiUrl}/usage`);
2016
2381
  process.exit(1);
2017
2382
  }
2018
2383
 
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
2384
  // src/commands/init.ts
2057
2385
  import { execFileSync as execFileSync2, spawn } from "child_process";
2058
2386
  import { existsSync as existsSync11 } from "fs";
2059
2387
  import { appendFile, mkdir, readFile } from "fs/promises";
2060
2388
  import { homedir as homedir8, platform } from "os";
2061
2389
  import { dirname as dirname2, join as join11, posix, win32 } from "path";
2062
- import { createInterface } from "readline";
2063
2390
  function joinForPlatform(currentPlatform, ...parts) {
2064
2391
  return currentPlatform === "win32" ? win32.join(...parts) : posix.join(...parts);
2065
2392
  }
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;
2393
+ function basenameLikeShell(input2) {
2394
+ return input2.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input2;
2077
2395
  }
2078
2396
  function getBrowserLaunchCommand(url, currentPlatform = platform()) {
2079
2397
  switch (currentPlatform) {
@@ -2210,43 +2528,62 @@ function resolveShellAliasSetup(options = {}) {
2210
2528
  }
2211
2529
  }
2212
2530
  async function runInit(opts = {}) {
2213
- logger.info("\n tokenarena - Token Usage Tracker\n");
2531
+ logger.info(formatHeader("TokenArena \u521D\u59CB\u5316"));
2214
2532
  const existing = loadConfig();
2215
2533
  if (existing?.apiKey) {
2216
- const answer = await prompt("Config already exists. Overwrite? (y/N) ");
2217
- if (answer.toLowerCase() !== "y") {
2218
- logger.info("Cancelled.");
2534
+ logger.info(formatSection("\u68C0\u6D4B\u5230\u5DF2\u6709\u914D\u7F6E"));
2535
+ logger.info(formatKeyValue("\u5F53\u524D API Key", maskSecret(existing.apiKey)));
2536
+ logger.info(
2537
+ formatKeyValue(
2538
+ "\u5F53\u524D API \u5730\u5740",
2539
+ existing.apiUrl || "https://token.poco-ai.com"
2540
+ )
2541
+ );
2542
+ const shouldOverwrite = await promptConfirm({
2543
+ message: "\u5DF2\u7ECF\u5B58\u5728\u672C\u5730\u914D\u7F6E\uFF0C\u662F\u5426\u8986\u76D6\u5E76\u91CD\u65B0\u521D\u59CB\u5316\uFF1F",
2544
+ defaultValue: false
2545
+ });
2546
+ if (!shouldOverwrite) {
2547
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u521D\u59CB\u5316\u3002", "warning"));
2219
2548
  return;
2220
2549
  }
2221
2550
  }
2222
2551
  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)}...`);
2552
+ const cliKeysUrl = `${apiUrl}/zh/settings/cli-keys`;
2553
+ logger.info(formatSection("\u7B2C 1 \u6B65\uFF1A\u51C6\u5907 API Key"));
2554
+ logger.info(formatBullet("\u6D4F\u89C8\u5668\u5C06\u5C1D\u8BD5\u81EA\u52A8\u6253\u5F00 CLI Key \u9875\u9762\u3002"));
2555
+ logger.info(formatKeyValue("Key \u9875\u9762", formatMutedPath(cliKeysUrl)));
2556
+ openBrowser(cliKeysUrl);
2557
+ const apiKey = await promptPassword({
2558
+ message: "\u8BF7\u7C98\u8D34\u4F60\u7684 CLI API Key",
2559
+ validate: (value) => validateApiKey(value) || 'API Key \u5FC5\u987B\u4EE5 "ta_" \u5F00\u5934\u3002'
2560
+ });
2561
+ logger.info(formatSection("\u7B2C 2 \u6B65\uFF1A\u9A8C\u8BC1 API Key"));
2562
+ logger.info(formatKeyValue("\u5F85\u9A8C\u8BC1 Key", maskSecret(apiKey)));
2234
2563
  try {
2235
2564
  const client = new ApiClient(apiUrl, apiKey);
2236
2565
  const settings = await client.fetchSettings();
2237
2566
  if (!settings) {
2238
2567
  logger.info(
2239
- "Could not verify key settings (network error). Saving anyway.\n"
2568
+ formatBullet(
2569
+ "\u65E0\u6CD5\u5728\u7EBF\u9A8C\u8BC1 Key\uFF08\u53EF\u80FD\u662F\u7F51\u7EDC\u539F\u56E0\uFF09\uFF0C\u5C06\u7EE7\u7EED\u4FDD\u5B58\u3002",
2570
+ "warning"
2571
+ )
2240
2572
  );
2241
2573
  } else {
2242
- logger.info("Key verified.\n");
2574
+ logger.info(formatBullet("API Key \u9A8C\u8BC1\u6210\u529F\u3002", "success"));
2243
2575
  }
2244
2576
  } catch (err) {
2245
2577
  if (err.message === "UNAUTHORIZED") {
2246
2578
  logger.error("Invalid API key. Please check and try again.");
2247
2579
  process.exit(1);
2248
2580
  }
2249
- logger.info("Could not verify key (network error). Saving anyway.\n");
2581
+ logger.info(
2582
+ formatBullet(
2583
+ "\u65E0\u6CD5\u5B8C\u6210\u5728\u7EBF\u9A8C\u8BC1\uFF08\u53EF\u80FD\u662F\u7F51\u7EDC\u539F\u56E0\uFF09\uFF0C\u5C06\u7EE7\u7EED\u4FDD\u5B58\u3002",
2584
+ "warning"
2585
+ )
2586
+ );
2250
2587
  }
2251
2588
  const config = {
2252
2589
  apiKey,
@@ -2256,17 +2593,28 @@ Verifying key ${apiKey.slice(0, 8)}...`);
2256
2593
  saveConfig(config);
2257
2594
  const deviceId = getOrCreateDeviceId(config);
2258
2595
  config.deviceId = deviceId;
2259
- logger.info(`Device registered: ${deviceId.slice(0, 8)}...`);
2596
+ logger.info(formatSection("\u7B2C 3 \u6B65\uFF1A\u5B8C\u6210\u672C\u5730\u6CE8\u518C"));
2260
2597
  const tools = getDetectedTools();
2261
2598
  if (tools.length > 0) {
2262
- logger.info(`Detected tools: ${tools.map((tool) => tool.name).join(", ")}`);
2599
+ logger.info(formatSection("\u68C0\u6D4B\u5230\u7684 AI CLI"));
2600
+ for (const tool of tools) {
2601
+ logger.info(formatBullet(tool.name, "success"));
2602
+ }
2263
2603
  } else {
2264
- logger.info("No AI coding tools detected. Install one and re-run init.");
2604
+ logger.info(formatSection("\u68C0\u6D4B\u5230\u7684 AI CLI"));
2605
+ logger.info(
2606
+ formatBullet(
2607
+ "\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",
2608
+ "warning"
2609
+ )
2610
+ );
2265
2611
  }
2266
- logger.info("\nRunning initial sync...");
2612
+ logger.info(formatSection("\u9996\u6B21\u540C\u6B65"));
2613
+ logger.info(formatBullet("\u6B63\u5728\u4E0A\u4F20\u672C\u5730\u5DF2\u6709\u7684\u4F7F\u7528\u6570\u636E\u3002"));
2267
2614
  await runSync(config, { source: "init" });
2268
- logger.info(`
2269
- Setup complete! View your dashboard at: ${apiUrl}/usage`);
2615
+ logger.info(formatSection("\u521D\u59CB\u5316\u5B8C\u6210"));
2616
+ logger.info(formatBullet("TokenArena \u5DF2\u51C6\u5907\u5C31\u7EEA\u3002", "success"));
2617
+ logger.info(formatKeyValue("\u63A7\u5236\u53F0", `${apiUrl}/usage`));
2270
2618
  await setupShellAlias();
2271
2619
  }
2272
2620
  async function setupShellAlias() {
@@ -2274,11 +2622,12 @@ async function setupShellAlias() {
2274
2622
  if (!setup) {
2275
2623
  return;
2276
2624
  }
2277
- const answer = await prompt(
2278
- `
2279
- Set up ${setup.shellLabel} alias 'ta' for 'tokenarena'? (Y/n) `
2280
- );
2281
- if (answer.toLowerCase() === "n") {
2625
+ const shouldCreateAlias = await promptConfirm({
2626
+ message: `\u662F\u5426\u4E3A ${setup.shellLabel} \u81EA\u52A8\u6DFB\u52A0 ta \u522B\u540D\uFF1F`,
2627
+ defaultValue: true
2628
+ });
2629
+ if (!shouldCreateAlias) {
2630
+ logger.info(formatBullet("\u5DF2\u8DF3\u8FC7 shell alias \u8BBE\u7F6E\u3002"));
2282
2631
  return;
2283
2632
  }
2284
2633
  try {
@@ -2292,10 +2641,7 @@ Set up ${setup.shellLabel} alias 'ta' for 'tokenarena'? (Y/n) `
2292
2641
  (pattern) => normalizedContent.includes(pattern.toLowerCase())
2293
2642
  );
2294
2643
  if (aliasExists) {
2295
- logger.info(
2296
- `
2297
- Alias 'ta' already exists in ${setup.configFile}. Skipping.`
2298
- );
2644
+ logger.info(formatBullet(`\u522B\u540D ta \u5DF2\u5B58\u5728\uFF1A${setup.configFile}`));
2299
2645
  return;
2300
2646
  }
2301
2647
  const aliasWithComment = `
@@ -2303,18 +2649,76 @@ Alias 'ta' already exists in ${setup.configFile}. Skipping.`
2303
2649
  ${setup.aliasLine}
2304
2650
  `;
2305
2651
  await appendFile(setup.configFile, aliasWithComment, "utf-8");
2306
- logger.info(`
2307
- Added alias to ${setup.configFile}`);
2652
+ logger.info(formatSection("Shell alias"));
2653
+ logger.info(formatBullet(`\u5DF2\u5199\u5165 ${setup.configFile}`, "success"));
2308
2654
  logger.info(
2309
- ` Run '${setup.sourceHint}' or restart your terminal to use it.`
2655
+ formatKeyValue("\u751F\u6548\u65B9\u5F0F", `\u6267\u884C '${setup.sourceHint}' \u6216\u91CD\u542F\u7EC8\u7AEF`)
2310
2656
  );
2311
- logger.info(" Then you can use: ta sync");
2657
+ logger.info(formatKeyValue("\u4E4B\u540E\u53EF\u7528", "ta sync"));
2312
2658
  } catch (err) {
2659
+ logger.info(formatSection("Shell alias"));
2313
2660
  logger.info(
2314
- `
2315
- Could not write to ${setup.configFile}: ${err.message}`
2661
+ formatBullet(
2662
+ `\u65E0\u6CD5\u5199\u5165 ${setup.configFile}: ${err.message}`,
2663
+ "warning"
2664
+ )
2316
2665
  );
2317
- logger.info(` Add this line manually: ${setup.aliasLine}`);
2666
+ logger.info(formatKeyValue("\u8BF7\u624B\u52A8\u6DFB\u52A0", setup.aliasLine));
2667
+ }
2668
+ }
2669
+
2670
+ // src/commands/daemon.ts
2671
+ var DEFAULT_INTERVAL = 5 * 6e4;
2672
+ function log(msg) {
2673
+ const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
2674
+ process.stdout.write(`[${ts}] ${msg}
2675
+ `);
2676
+ }
2677
+ function sleep(ms) {
2678
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2679
+ }
2680
+ async function runDaemon(opts = {}) {
2681
+ const config = loadConfig();
2682
+ if (!config?.apiKey) {
2683
+ if (isInteractiveTerminal()) {
2684
+ logger.info(
2685
+ formatHeader(
2686
+ "\u5C1A\u672A\u5B8C\u6210\u521D\u59CB\u5316",
2687
+ "\u542F\u52A8 daemon \u524D\u9700\u8981\u5148\u914D\u7F6E\u6709\u6548\u7684 API Key\u3002"
2688
+ )
2689
+ );
2690
+ const shouldInit = await promptConfirm({
2691
+ message: "\u662F\u5426\u5148\u8FDB\u5165\u521D\u59CB\u5316\u6D41\u7A0B\uFF1F",
2692
+ defaultValue: true
2693
+ });
2694
+ if (shouldInit) {
2695
+ await runInit();
2696
+ return;
2697
+ }
2698
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u542F\u52A8 daemon\u3002", "warning"));
2699
+ return;
2700
+ }
2701
+ logger.error("Not configured. Run `tokenarena init` first.");
2702
+ process.exit(1);
2703
+ }
2704
+ const interval = opts.interval || config.syncInterval || DEFAULT_INTERVAL;
2705
+ const intervalMin = Math.round(interval / 6e4);
2706
+ log(`Daemon started (sync every ${intervalMin}m, Ctrl+C to stop)`);
2707
+ while (true) {
2708
+ try {
2709
+ await runSync(config, {
2710
+ quiet: true,
2711
+ source: "daemon",
2712
+ throws: true
2713
+ });
2714
+ } catch (err) {
2715
+ if (err.message === "UNAUTHORIZED") {
2716
+ log("API key invalid. Exiting.");
2717
+ process.exit(1);
2718
+ }
2719
+ log(`Sync error: ${err.message}`);
2720
+ }
2721
+ await sleep(interval);
2318
2722
  }
2319
2723
  }
2320
2724
 
@@ -2324,58 +2728,94 @@ function formatMaybe(value) {
2324
2728
  }
2325
2729
  async function runStatus() {
2326
2730
  const config = loadConfig();
2327
- logger.info("\ntokenarena status\n");
2731
+ logger.info(
2732
+ formatHeader(
2733
+ "TokenArena \u72B6\u6001",
2734
+ "\u67E5\u770B\u5F53\u524D\u914D\u7F6E\u3001\u5DF2\u68C0\u6D4B\u5DE5\u5177\u4EE5\u53CA\u6700\u8FD1\u4E00\u6B21\u540C\u6B65\u60C5\u51B5\u3002"
2735
+ )
2736
+ );
2737
+ logger.info(formatSection("\u914D\u7F6E"));
2328
2738
  if (!config?.apiKey) {
2329
- logger.info(" Config: not configured");
2330
- logger.info(" Run `tokenarena init` to set up.\n");
2739
+ logger.info(formatKeyValue("\u72B6\u6001", formatStatusBadge("\u672A\u914D\u7F6E", "warning")));
2740
+ logger.info(formatBullet("\u8FD0\u884C tokenarena init \u5B8C\u6210\u9996\u6B21\u8BBE\u7F6E\u3002", "warning"));
2331
2741
  } 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"}`);
2742
+ logger.info(formatKeyValue("\u72B6\u6001", formatStatusBadge("\u5DF2\u914D\u7F6E", "success")));
2743
+ logger.info(formatKeyValue("\u914D\u7F6E\u6587\u4EF6", getConfigPath()));
2744
+ logger.info(formatKeyValue("API Key", maskSecret(config.apiKey)));
2745
+ logger.info(
2746
+ formatKeyValue("API URL", config.apiUrl || "https://token.poco-ai.com")
2747
+ );
2335
2748
  if (config.syncInterval) {
2336
2749
  logger.info(
2337
- ` Sync interval: ${Math.round(config.syncInterval / 6e4)}m`
2750
+ formatKeyValue(
2751
+ "\u540C\u6B65\u95F4\u9694",
2752
+ `${Math.round(config.syncInterval / 6e4)} \u5206\u949F`
2753
+ )
2338
2754
  );
2339
2755
  }
2340
2756
  }
2341
- logger.info("\n Detected tools:");
2757
+ logger.info(formatSection("\u5DF2\u68C0\u6D4B\u5DE5\u5177"));
2342
2758
  const detected = detectInstalledTools();
2343
2759
  if (detected.length === 0) {
2344
- logger.info(" (none)\n");
2760
+ logger.info(formatBullet("\u672A\u68C0\u6D4B\u5230\u5DF2\u5B89\u88C5\u7684 AI CLI\u3002", "warning"));
2345
2761
  } else {
2346
2762
  for (const tool of detected) {
2347
- logger.info(` ${tool.name}`);
2763
+ logger.info(formatBullet(tool.name, "success"));
2348
2764
  }
2349
- logger.info("");
2350
2765
  }
2351
- logger.info(" All supported tools:");
2766
+ logger.info(formatSection("\u652F\u6301\u7684\u5DE5\u5177"));
2352
2767
  for (const tool of getAllTools()) {
2353
- const installed = isToolInstalled(tool.id) ? "installed" : "not found";
2354
- logger.info(` ${tool.name}: ${installed}`);
2768
+ const installed = isToolInstalled(tool.id);
2769
+ logger.info(
2770
+ formatBullet(
2771
+ `${tool.name} \xB7 ${installed ? "\u5DF2\u5B89\u88C5" : "\u672A\u53D1\u73B0"}`,
2772
+ installed ? "success" : "neutral"
2773
+ )
2774
+ );
2355
2775
  }
2356
2776
  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)}`);
2777
+ logger.info(formatSection("\u540C\u6B65\u72B6\u6001"));
2778
+ const statusTone = syncState.status === "idle" ? "success" : syncState.status === "syncing" ? "warning" : "danger";
2779
+ logger.info(
2780
+ formatKeyValue("\u72B6\u6001", formatStatusBadge(syncState.status, statusTone))
2781
+ );
2782
+ logger.info(formatKeyValue("\u4E0A\u6B21\u5C1D\u8BD5", formatMaybe(syncState.lastAttemptAt)));
2783
+ logger.info(formatKeyValue("\u4E0A\u6B21\u6210\u529F", formatMaybe(syncState.lastSuccessAt)));
2361
2784
  if (syncState.lastSource) {
2362
- logger.info(` Last source: ${syncState.lastSource}`);
2785
+ logger.info(formatKeyValue("\u89E6\u53D1\u6765\u6E90", syncState.lastSource));
2363
2786
  }
2364
2787
  if (syncState.lastError) {
2365
- logger.info(` Last error: ${syncState.lastError}`);
2788
+ logger.info(formatKeyValue("\u9519\u8BEF\u4FE1\u606F", syncState.lastError));
2366
2789
  }
2367
2790
  if (syncState.lastResult) {
2368
2791
  logger.info(
2369
- ` Last result: ${syncState.lastResult.buckets} buckets, ${syncState.lastResult.sessions} sessions`
2792
+ formatKeyValue(
2793
+ "\u6700\u8FD1\u7ED3\u679C",
2794
+ `${syncState.lastResult.buckets} buckets, ${syncState.lastResult.sessions} sessions`
2795
+ )
2370
2796
  );
2371
2797
  }
2372
- logger.info("");
2373
2798
  }
2374
2799
 
2375
2800
  // src/commands/sync.ts
2376
2801
  async function runSyncCommand(opts = {}) {
2377
2802
  const config = loadConfig();
2378
2803
  if (!config?.apiKey) {
2804
+ if (isInteractiveTerminal()) {
2805
+ logger.info(
2806
+ formatHeader("\u5C1A\u672A\u5B8C\u6210\u521D\u59CB\u5316", "\u540C\u6B65\u524D\u9700\u8981\u5148\u914D\u7F6E\u6709\u6548\u7684 API Key\u3002")
2807
+ );
2808
+ const shouldInit = await promptConfirm({
2809
+ message: "\u662F\u5426\u73B0\u5728\u8FDB\u5165\u521D\u59CB\u5316\u6D41\u7A0B\uFF1F",
2810
+ defaultValue: true
2811
+ });
2812
+ if (shouldInit) {
2813
+ await runInit();
2814
+ return;
2815
+ }
2816
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u540C\u6B65\u3002", "warning"));
2817
+ return;
2818
+ }
2379
2819
  logger.error("Not configured. Run `tokenarena init` first.");
2380
2820
  process.exit(1);
2381
2821
  }
@@ -2388,16 +2828,6 @@ async function runSyncCommand(opts = {}) {
2388
2828
  // src/commands/uninstall.ts
2389
2829
  import { existsSync as existsSync12, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
2390
2830
  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
2831
  function removeShellAlias() {
2402
2832
  const shell = process.env.SHELL;
2403
2833
  if (!shell) return;
@@ -2457,42 +2887,168 @@ async function runUninstall() {
2457
2887
  const configPath = getConfigPath();
2458
2888
  const configDir = getConfigDir();
2459
2889
  if (!existsSync12(configPath)) {
2460
- logger.info("No configuration found. Nothing to uninstall.");
2890
+ logger.info(formatHeader("\u5378\u8F7D TokenArena"));
2891
+ logger.info(formatBullet("\u672A\u53D1\u73B0\u672C\u5730\u914D\u7F6E\uFF0C\u65E0\u9700\u5378\u8F7D\u3002"));
2461
2892
  return;
2462
2893
  }
2463
2894
  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) "
2895
+ logger.info(
2896
+ formatHeader(
2897
+ "\u5378\u8F7D TokenArena",
2898
+ "\u8BE5\u64CD\u4F5C\u4F1A\u5220\u9664\u672C\u5730\u914D\u7F6E\u3001\u540C\u6B65\u72B6\u6001\u4E0E\u8FD0\u884C\u65F6\u6587\u4EF6\u3002"
2899
+ )
2470
2900
  );
2471
- if (answer.toLowerCase() !== "y") {
2472
- logger.info("Cancelled.");
2901
+ if (config?.apiKey) {
2902
+ logger.info(formatKeyValue("API Key", maskSecret(config.apiKey)));
2903
+ }
2904
+ logger.info(formatKeyValue("\u914D\u7F6E\u76EE\u5F55", configDir));
2905
+ logger.info(formatKeyValue("\u72B6\u6001\u76EE\u5F55", getStateDir()));
2906
+ logger.info(formatKeyValue("\u8FD0\u884C\u76EE\u5F55", getRuntimeDirPath()));
2907
+ const shouldUninstall = await promptConfirm({
2908
+ message: "\u786E\u8BA4\u7EE7\u7EED\u5378\u8F7D\u672C\u5730 TokenArena \u6570\u636E\uFF1F",
2909
+ defaultValue: false
2910
+ });
2911
+ if (!shouldUninstall) {
2912
+ logger.info(formatBullet("\u5DF2\u53D6\u6D88\u5378\u8F7D\u3002", "warning"));
2473
2913
  return;
2474
2914
  }
2475
2915
  deleteConfig();
2476
- logger.info("Deleted configuration file.");
2916
+ logger.info(formatSection("\u6267\u884C\u7ED3\u679C"));
2917
+ logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u6587\u4EF6\u3002", "success"));
2477
2918
  if (existsSync12(configDir)) {
2478
2919
  try {
2479
2920
  rmSync2(configDir, { recursive: false, force: true });
2480
- logger.info("Deleted config directory.");
2921
+ logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u76EE\u5F55\u3002", "success"));
2481
2922
  } catch {
2482
2923
  }
2483
2924
  }
2484
2925
  const stateDir = getStateDir();
2485
2926
  if (existsSync12(stateDir)) {
2486
2927
  rmSync2(stateDir, { recursive: true, force: true });
2487
- logger.info("Deleted state data.");
2928
+ logger.info(formatBullet("\u5DF2\u5220\u9664\u72B6\u6001\u6570\u636E\u3002", "success"));
2488
2929
  }
2489
2930
  const runtimeDir = getRuntimeDirPath();
2490
2931
  if (existsSync12(runtimeDir)) {
2491
2932
  rmSync2(runtimeDir, { recursive: true, force: true });
2492
- logger.info("Deleted runtime data.");
2933
+ logger.info(formatBullet("\u5DF2\u5220\u9664\u8FD0\u884C\u65F6\u6570\u636E\u3002", "success"));
2493
2934
  }
2494
2935
  removeShellAlias();
2495
- logger.info("\nTokenArena has been uninstalled successfully.");
2936
+ logger.info(formatSection("\u5B8C\u6210"));
2937
+ logger.info(formatBullet("TokenArena \u5DF2\u4ECE\u672C\u5730\u5378\u8F7D\u5B8C\u6210\u3002", "success"));
2938
+ }
2939
+
2940
+ // src/commands/home.ts
2941
+ function logHomeSummary() {
2942
+ const config = loadConfig();
2943
+ const configured = Boolean(config?.apiKey);
2944
+ logger.info(
2945
+ formatHeader(
2946
+ "TokenArena CLI",
2947
+ "\u901A\u8FC7\u66F4\u53CB\u597D\u7684\u4EA4\u4E92\u5B8C\u6210\u521D\u59CB\u5316\u3001\u540C\u6B65\u3001\u914D\u7F6E\u4E0E\u6E05\u7406\u3002"
2948
+ )
2949
+ );
2950
+ logger.info(
2951
+ formatKeyValue(
2952
+ "\u914D\u7F6E\u72B6\u6001",
2953
+ configured ? formatStatusBadge("\u5DF2\u914D\u7F6E", "success") : formatStatusBadge("\u672A\u914D\u7F6E", "warning")
2954
+ )
2955
+ );
2956
+ if (configured && config) {
2957
+ logger.info(formatKeyValue("API Key", maskSecret(config.apiKey)));
2958
+ logger.info(
2959
+ formatKeyValue("API \u5730\u5740", config.apiUrl || "https://token.poco-ai.com")
2960
+ );
2961
+ } else {
2962
+ logger.info(formatBullet("\u5EFA\u8BAE\u5148\u8FD0\u884C\u521D\u59CB\u5316\u6D41\u7A0B\u7ED1\u5B9A API Key\u3002", "warning"));
2963
+ }
2964
+ }
2965
+ async function pickHomeAction() {
2966
+ return promptSelect({
2967
+ message: "\u8BF7\u9009\u62E9\u8981\u6267\u884C\u7684\u64CD\u4F5C",
2968
+ choices: [
2969
+ {
2970
+ name: "\u521D\u59CB\u5316 TokenArena",
2971
+ value: "init",
2972
+ description: "\u914D\u7F6E API Key\u3001\u68C0\u6D4B\u5DE5\u5177\u5E76\u6267\u884C\u9996\u6B21\u540C\u6B65"
2973
+ },
2974
+ {
2975
+ name: "\u67E5\u770B\u5F53\u524D\u72B6\u6001",
2976
+ value: "status",
2977
+ description: "\u67E5\u770B\u914D\u7F6E\u3001\u5DE5\u5177\u68C0\u6D4B\u7ED3\u679C\u4E0E\u6700\u8FD1\u540C\u6B65\u72B6\u6001"
2978
+ },
2979
+ {
2980
+ name: "\u7ACB\u5373\u540C\u6B65",
2981
+ value: "sync",
2982
+ description: "\u624B\u52A8\u4E0A\u4F20\u672C\u5730\u6700\u65B0 token \u4F7F\u7528\u6570\u636E"
2983
+ },
2984
+ {
2985
+ name: "\u7BA1\u7406\u914D\u7F6E",
2986
+ value: "config",
2987
+ description: "\u67E5\u770B\u6216\u4FEE\u6539 API Key\u3001API \u5730\u5740\u3001\u540C\u6B65\u95F4\u9694\u7B49\u914D\u7F6E"
2988
+ },
2989
+ {
2990
+ name: "\u542F\u52A8\u5B88\u62A4\u540C\u6B65",
2991
+ value: "daemon",
2992
+ description: "\u6301\u7EED\u540E\u53F0\u540C\u6B65\uFF0C\u9002\u5408\u957F\u671F\u8FD0\u884C"
2993
+ },
2994
+ {
2995
+ name: "\u5378\u8F7D\u672C\u5730\u914D\u7F6E",
2996
+ value: "uninstall",
2997
+ description: "\u5220\u9664\u672C\u5730\u914D\u7F6E\u3001\u72B6\u6001\u4E0E\u8FD0\u884C\u65F6\u6587\u4EF6"
2998
+ },
2999
+ {
3000
+ name: "\u67E5\u770B\u5E2E\u52A9",
3001
+ value: "help",
3002
+ description: "\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E2E\u52A9"
3003
+ },
3004
+ {
3005
+ name: "\u9000\u51FA",
3006
+ value: "exit",
3007
+ description: "\u7ED3\u675F\u5F53\u524D\u4EA4\u4E92"
3008
+ }
3009
+ ]
3010
+ });
3011
+ }
3012
+ async function runHome(program) {
3013
+ while (true) {
3014
+ logHomeSummary();
3015
+ const action = await pickHomeAction();
3016
+ logger.info("");
3017
+ switch (action) {
3018
+ case "init":
3019
+ await runInit();
3020
+ break;
3021
+ case "status":
3022
+ await runStatus();
3023
+ break;
3024
+ case "sync":
3025
+ await runSyncCommand();
3026
+ break;
3027
+ case "config":
3028
+ await handleConfig([]);
3029
+ break;
3030
+ case "daemon":
3031
+ await runDaemon();
3032
+ return;
3033
+ case "uninstall":
3034
+ await runUninstall();
3035
+ break;
3036
+ case "help":
3037
+ program.outputHelp();
3038
+ break;
3039
+ case "exit":
3040
+ logger.info(formatBullet("\u5DF2\u9000\u51FA\u4EA4\u4E92\u5F0F\u4E3B\u9875\u3002", "neutral"));
3041
+ return;
3042
+ }
3043
+ const continueAnswer = await promptConfirm({
3044
+ message: "\u662F\u5426\u7EE7\u7EED\u6267\u884C\u5176\u4ED6\u64CD\u4F5C\uFF1F",
3045
+ defaultValue: true
3046
+ });
3047
+ if (!continueAnswer) {
3048
+ logger.info(formatBullet("\u4E0B\u6B21\u53EF\u76F4\u63A5\u8FD0\u884C tokenarena \u7EE7\u7EED\u3002", "neutral"));
3049
+ return;
3050
+ }
3051
+ }
2496
3052
  }
2497
3053
 
2498
3054
  // src/infrastructure/runtime/cli-version.ts
@@ -2524,11 +3080,15 @@ var CLI_VERSION = getCliVersion();
2524
3080
  function createCli() {
2525
3081
  const program = new Command();
2526
3082
  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(() => {
3083
+ program.action(async () => {
2528
3084
  const userArgs = process.argv.slice(2).filter((a) => !a.startsWith("-"));
2529
3085
  if (userArgs.length > 0) {
2530
3086
  program.error(`unknown command '${userArgs[0]}'`);
2531
3087
  }
3088
+ if (isInteractiveTerminal()) {
3089
+ await runHome(program);
3090
+ return;
3091
+ }
2532
3092
  program.help();
2533
3093
  });
2534
3094
  program.command("init").description("Initialize configuration with API key").option("--api-url <url>", "Custom API server URL").action(async (opts) => {
@@ -2543,9 +3103,12 @@ function createCli() {
2543
3103
  program.command("status").description("Show configuration and detected tools").action(async () => {
2544
3104
  await runStatus();
2545
3105
  });
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);
3106
+ 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) => {
3107
+ await handleConfig(
3108
+ [subcommand, key, value].filter(
3109
+ (item) => typeof item === "string"
3110
+ )
3111
+ );
2549
3112
  });
2550
3113
  program.command("uninstall").description("Remove all local configuration and data").action(async () => {
2551
3114
  await runUninstall();
@@ -2576,12 +3139,12 @@ function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
2576
3139
  function normalizeArgv(argv) {
2577
3140
  return argv.filter((arg, index) => index < 2 || arg !== "--");
2578
3141
  }
2579
- function run(argv = process.argv) {
3142
+ async function run(argv = process.argv) {
2580
3143
  const program = createCli();
2581
- program.parse(normalizeArgv(argv));
3144
+ await program.parseAsync(normalizeArgv(argv));
2582
3145
  }
2583
3146
  if (isMainModule()) {
2584
- run();
3147
+ void run();
2585
3148
  }
2586
3149
  export {
2587
3150
  normalizeArgv,