@poco-ai/tokenarena 0.2.1 → 0.2.3

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
@@ -1,4 +1,21 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ formatBullet,
4
+ formatHeader,
5
+ formatKeyValue,
6
+ formatMutedPath,
7
+ formatSection,
8
+ formatStatusBadge,
9
+ isCommandAvailable,
10
+ isInteractiveTerminal,
11
+ logger,
12
+ maskSecret,
13
+ promptConfirm,
14
+ promptPassword,
15
+ promptSelect,
16
+ promptText,
17
+ runInstallService
18
+ } from "./chunk-XJKRJ3K2.js";
2
19
 
3
20
  // src/parsers/claude-code.ts
4
21
  import { existsSync as existsSync3 } from "fs";
@@ -211,6 +228,17 @@ function readFileSafe(filePath) {
211
228
  return null;
212
229
  }
213
230
  }
231
+ function parseJsonl(content) {
232
+ const results = [];
233
+ for (const line of content.split("\n")) {
234
+ if (!line.trim()) continue;
235
+ try {
236
+ results.push(JSON.parse(line));
237
+ } catch {
238
+ }
239
+ }
240
+ return results;
241
+ }
214
242
  function extractSessionId(filePath) {
215
243
  return basename(filePath, ".jsonl");
216
244
  }
@@ -1098,37 +1126,657 @@ var OpenClawParser = class {
1098
1126
  };
1099
1127
  registerParser(new OpenClawParser());
1100
1128
 
1129
+ // src/parsers/qwen-code.ts
1130
+ import { existsSync as existsSync8, readdirSync as readdirSync6 } from "fs";
1131
+ import { homedir as homedir7 } from "os";
1132
+ import { join as join8 } from "path";
1133
+ var TOOL_ID = "qwen-code";
1134
+ var TOOL_NAME = "Qwen Code";
1135
+ var DEFAULT_DATA_DIR2 = join8(homedir7(), ".qwen", "tmp");
1136
+ function createToolDefinition(dataDir) {
1137
+ return {
1138
+ id: TOOL_ID,
1139
+ name: TOOL_NAME,
1140
+ dataDir
1141
+ };
1142
+ }
1143
+ function toSafeNumber(value) {
1144
+ const numberValue = Number(value);
1145
+ return Number.isFinite(numberValue) ? numberValue : 0;
1146
+ }
1147
+ function getPathLeaf2(value) {
1148
+ const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
1149
+ const leaf = normalized.split("/").filter(Boolean).pop();
1150
+ return leaf || "unknown";
1151
+ }
1152
+ function normalizeForPrefix(value) {
1153
+ return value.replace(/\\/g, "/").replace(/\/+$/, "");
1154
+ }
1155
+ function findSessionFiles2(baseDir) {
1156
+ const results = [];
1157
+ if (!existsSync8(baseDir)) return results;
1158
+ try {
1159
+ for (const entry of readdirSync6(baseDir, { withFileTypes: true })) {
1160
+ if (!entry.isDirectory()) continue;
1161
+ const chatsDir = join8(baseDir, entry.name, "chats");
1162
+ if (!existsSync8(chatsDir)) continue;
1163
+ try {
1164
+ for (const file of readdirSync6(chatsDir)) {
1165
+ if (file.endsWith(".jsonl")) {
1166
+ results.push(join8(chatsDir, file));
1167
+ }
1168
+ }
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ } catch {
1173
+ return results;
1174
+ }
1175
+ return results;
1176
+ }
1177
+ function resolveQwenProject(cwd, filePath, dataDir = DEFAULT_DATA_DIR2) {
1178
+ if (cwd) {
1179
+ return getPathLeaf2(cwd);
1180
+ }
1181
+ const normalizedFilePath = normalizeForPrefix(filePath);
1182
+ const normalizedDataDir = normalizeForPrefix(dataDir);
1183
+ const prefix = `${normalizedDataDir}/`;
1184
+ if (!normalizedFilePath.startsWith(prefix)) {
1185
+ return "unknown";
1186
+ }
1187
+ const relativePath = normalizedFilePath.slice(prefix.length);
1188
+ const projectId = relativePath.split("/")[0];
1189
+ return projectId || "unknown";
1190
+ }
1191
+ var QwenCodeParser = class {
1192
+ constructor(dataDir = DEFAULT_DATA_DIR2) {
1193
+ this.dataDir = dataDir;
1194
+ this.tool = createToolDefinition(dataDir);
1195
+ }
1196
+ tool;
1197
+ async parse() {
1198
+ const sessionFiles = findSessionFiles2(this.dataDir);
1199
+ if (sessionFiles.length === 0) {
1200
+ return { buckets: [], sessions: [] };
1201
+ }
1202
+ const entries = [];
1203
+ const sessionEvents = [];
1204
+ const seenUuids = /* @__PURE__ */ new Set();
1205
+ for (const filePath of sessionFiles) {
1206
+ const content = readFileSafe(filePath);
1207
+ if (!content) continue;
1208
+ const sessionId = filePath;
1209
+ for (const line of content.split("\n")) {
1210
+ if (!line.trim()) continue;
1211
+ try {
1212
+ const obj = JSON.parse(line);
1213
+ if (!obj.timestamp) continue;
1214
+ const timestamp = new Date(obj.timestamp);
1215
+ if (Number.isNaN(timestamp.getTime())) continue;
1216
+ const project = resolveQwenProject(obj.cwd, filePath, this.dataDir);
1217
+ if (obj.type === "user" || obj.type === "assistant") {
1218
+ sessionEvents.push({
1219
+ sessionId,
1220
+ source: TOOL_ID,
1221
+ project,
1222
+ timestamp,
1223
+ role: obj.type
1224
+ });
1225
+ }
1226
+ if (obj.type !== "assistant") continue;
1227
+ const usage = obj.usageMetadata || obj.usage;
1228
+ if (!usage) continue;
1229
+ const totalInput = toSafeNumber(usage.promptTokenCount) || toSafeNumber(usage.input_tokens);
1230
+ const totalOutput = toSafeNumber(usage.candidatesTokenCount) || toSafeNumber(usage.output_tokens);
1231
+ const cachedTokens = toSafeNumber(usage.cachedContentTokenCount);
1232
+ const reasoningTokens = toSafeNumber(usage.thoughtsTokenCount);
1233
+ if (totalInput === 0 && totalOutput === 0 && cachedTokens === 0 && reasoningTokens === 0) {
1234
+ continue;
1235
+ }
1236
+ if (obj.uuid) {
1237
+ if (seenUuids.has(obj.uuid)) continue;
1238
+ seenUuids.add(obj.uuid);
1239
+ }
1240
+ entries.push({
1241
+ sessionId,
1242
+ source: TOOL_ID,
1243
+ model: obj.model || "unknown",
1244
+ project,
1245
+ timestamp,
1246
+ inputTokens: Math.max(0, totalInput - cachedTokens),
1247
+ outputTokens: Math.max(0, totalOutput - reasoningTokens),
1248
+ reasoningTokens,
1249
+ cachedTokens
1250
+ });
1251
+ } catch {
1252
+ }
1253
+ }
1254
+ }
1255
+ return {
1256
+ buckets: aggregateToBuckets(entries),
1257
+ sessions: extractSessions(sessionEvents, entries)
1258
+ };
1259
+ }
1260
+ isInstalled() {
1261
+ return existsSync8(this.dataDir);
1262
+ }
1263
+ };
1264
+ registerParser(new QwenCodeParser());
1265
+
1266
+ // src/parsers/kimi-code.ts
1267
+ import { existsSync as existsSync9, readdirSync as readdirSync7 } from "fs";
1268
+ import { homedir as homedir8 } from "os";
1269
+ import { join as join9 } from "path";
1270
+ var TOOL_ID2 = "kimi-code";
1271
+ var TOOL_NAME2 = "Kimi Code";
1272
+ var DEFAULT_SESSIONS_DIR = join9(homedir8(), ".kimi", "sessions");
1273
+ var DEFAULT_CONFIG_PATH = join9(homedir8(), ".kimi", "kimi.json");
1274
+ var USER_EVENT_TYPES = /* @__PURE__ */ new Set(["UserMessage", "user_message", "Input"]);
1275
+ var ASSISTANT_EVENT_TYPES = /* @__PURE__ */ new Set([
1276
+ "AssistantMessage",
1277
+ "assistant_message",
1278
+ "Output",
1279
+ "ModelOutput",
1280
+ "AssistantOutput"
1281
+ ]);
1282
+ function createToolDefinition2(dataDir) {
1283
+ return {
1284
+ id: TOOL_ID2,
1285
+ name: TOOL_NAME2,
1286
+ dataDir
1287
+ };
1288
+ }
1289
+ function toSafeNumber2(value) {
1290
+ const numberValue = Number(value);
1291
+ return Number.isFinite(numberValue) ? numberValue : 0;
1292
+ }
1293
+ function getPathLeaf3(value) {
1294
+ const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
1295
+ const leaf = normalized.split("/").filter(Boolean).pop();
1296
+ return leaf || "unknown";
1297
+ }
1298
+ function findWireFiles(baseDir) {
1299
+ const results = [];
1300
+ if (!existsSync9(baseDir)) return results;
1301
+ try {
1302
+ for (const workDir of readdirSync7(baseDir, { withFileTypes: true })) {
1303
+ if (!workDir.isDirectory()) continue;
1304
+ const workDirPath = join9(baseDir, workDir.name);
1305
+ try {
1306
+ for (const session of readdirSync7(workDirPath, {
1307
+ withFileTypes: true
1308
+ })) {
1309
+ if (!session.isDirectory()) continue;
1310
+ const wireFile = join9(workDirPath, session.name, "wire.jsonl");
1311
+ if (existsSync9(wireFile)) {
1312
+ results.push({ filePath: wireFile, workDirHash: workDir.name });
1313
+ }
1314
+ }
1315
+ } catch {
1316
+ }
1317
+ }
1318
+ } catch {
1319
+ return results;
1320
+ }
1321
+ return results;
1322
+ }
1323
+ function parseTimestamp(value) {
1324
+ if (value == null) return null;
1325
+ const timestamp = new Date(value);
1326
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp;
1327
+ }
1328
+ function classifyKimiRole(type, payload) {
1329
+ if (payload?.role === "user" || payload?.role === "assistant") {
1330
+ return payload.role;
1331
+ }
1332
+ if (type && USER_EVENT_TYPES.has(type)) {
1333
+ return "user";
1334
+ }
1335
+ if (type && ASSISTANT_EVENT_TYPES.has(type)) {
1336
+ return "assistant";
1337
+ }
1338
+ if (type?.toLowerCase().includes("assistant")) {
1339
+ return "assistant";
1340
+ }
1341
+ return null;
1342
+ }
1343
+ function loadProjectMap(configPath) {
1344
+ const projectMap = /* @__PURE__ */ new Map();
1345
+ const content = readFileSafe(configPath);
1346
+ if (!content) return projectMap;
1347
+ try {
1348
+ const config = JSON.parse(content);
1349
+ const workspaces = config.workspaces || config.projects || {};
1350
+ for (const [hash, info] of Object.entries(workspaces)) {
1351
+ const pathValue = typeof info === "string" ? info : info.path || info.dir || void 0;
1352
+ if (!pathValue) continue;
1353
+ projectMap.set(hash, getPathLeaf3(pathValue));
1354
+ }
1355
+ } catch {
1356
+ }
1357
+ return projectMap;
1358
+ }
1359
+ var KimiCodeParser = class {
1360
+ tool;
1361
+ sessionsDir;
1362
+ configPath;
1363
+ constructor(options = {}) {
1364
+ this.sessionsDir = options.sessionsDir || DEFAULT_SESSIONS_DIR;
1365
+ this.configPath = options.configPath || DEFAULT_CONFIG_PATH;
1366
+ this.tool = createToolDefinition2(this.sessionsDir);
1367
+ }
1368
+ async parse() {
1369
+ const wireFiles = findWireFiles(this.sessionsDir);
1370
+ if (wireFiles.length === 0) {
1371
+ return { buckets: [], sessions: [] };
1372
+ }
1373
+ const projectMap = loadProjectMap(this.configPath);
1374
+ const entries = [];
1375
+ const sessionEvents = [];
1376
+ const seenMessageIds = /* @__PURE__ */ new Set();
1377
+ for (const { filePath, workDirHash } of wireFiles) {
1378
+ const content = readFileSafe(filePath);
1379
+ if (!content) continue;
1380
+ const sessionId = filePath;
1381
+ const project = projectMap.get(workDirHash) || workDirHash;
1382
+ let currentModel = "unknown";
1383
+ let lastTimestampRaw;
1384
+ for (const line of content.split("\n")) {
1385
+ if (!line.trim()) continue;
1386
+ let obj;
1387
+ try {
1388
+ obj = JSON.parse(line);
1389
+ } catch {
1390
+ continue;
1391
+ }
1392
+ const payload = obj.payload;
1393
+ if (!payload) continue;
1394
+ if (payload.model) {
1395
+ currentModel = payload.model;
1396
+ }
1397
+ const timestampValue = payload.timestamp ?? obj.timestamp ?? lastTimestampRaw;
1398
+ const timestamp = parseTimestamp(timestampValue);
1399
+ if (payload.timestamp != null) {
1400
+ lastTimestampRaw = payload.timestamp;
1401
+ } else if (obj.timestamp != null) {
1402
+ lastTimestampRaw = obj.timestamp;
1403
+ }
1404
+ const role = classifyKimiRole(obj.type, payload);
1405
+ if (role && timestamp) {
1406
+ sessionEvents.push({
1407
+ sessionId,
1408
+ source: TOOL_ID2,
1409
+ project,
1410
+ timestamp,
1411
+ role
1412
+ });
1413
+ }
1414
+ if (obj.type !== "StatusUpdate") continue;
1415
+ const tokenUsage = payload.token_usage;
1416
+ if (!tokenUsage || !timestamp) continue;
1417
+ const inputTokens = toSafeNumber2(tokenUsage.input_other);
1418
+ const outputTokens = toSafeNumber2(tokenUsage.output);
1419
+ const cachedTokens = toSafeNumber2(tokenUsage.input_cache_read);
1420
+ const cacheCreateTokens = toSafeNumber2(tokenUsage.input_cache_creation);
1421
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && cacheCreateTokens === 0) {
1422
+ continue;
1423
+ }
1424
+ if (payload.message_id) {
1425
+ if (seenMessageIds.has(payload.message_id)) continue;
1426
+ seenMessageIds.add(payload.message_id);
1427
+ }
1428
+ if (!role) {
1429
+ sessionEvents.push({
1430
+ sessionId,
1431
+ source: TOOL_ID2,
1432
+ project,
1433
+ timestamp,
1434
+ role: "assistant"
1435
+ });
1436
+ }
1437
+ entries.push({
1438
+ sessionId,
1439
+ source: TOOL_ID2,
1440
+ model: currentModel,
1441
+ project,
1442
+ timestamp,
1443
+ inputTokens,
1444
+ outputTokens,
1445
+ reasoningTokens: 0,
1446
+ cachedTokens
1447
+ });
1448
+ }
1449
+ }
1450
+ return {
1451
+ buckets: aggregateToBuckets(entries),
1452
+ sessions: extractSessions(sessionEvents, entries)
1453
+ };
1454
+ }
1455
+ isInstalled() {
1456
+ return existsSync9(this.sessionsDir);
1457
+ }
1458
+ };
1459
+ registerParser(new KimiCodeParser());
1460
+
1461
+ // src/parsers/droid.ts
1462
+ import { existsSync as existsSync10, readdirSync as readdirSync8 } from "fs";
1463
+ import { homedir as homedir9 } from "os";
1464
+ import { basename as basename4, dirname as dirname2, join as join10 } from "path";
1465
+ var TOOL_ID3 = "droid";
1466
+ var TOOL_NAME3 = "Droid";
1467
+ var DEFAULT_DATA_DIR3 = join10(homedir9(), ".factory", "sessions");
1468
+ function createToolDefinition3(dataDir) {
1469
+ return {
1470
+ id: TOOL_ID3,
1471
+ name: TOOL_NAME3,
1472
+ dataDir
1473
+ };
1474
+ }
1475
+ function findSessionFiles3(dir) {
1476
+ const results = [];
1477
+ if (!existsSync10(dir)) return results;
1478
+ try {
1479
+ for (const entry of readdirSync8(dir, { withFileTypes: true })) {
1480
+ const fullPath = join10(dir, entry.name);
1481
+ if (entry.isDirectory()) {
1482
+ results.push(...findSessionFiles3(fullPath));
1483
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl") && !entry.name.endsWith(".settings.json")) {
1484
+ results.push(fullPath);
1485
+ }
1486
+ }
1487
+ } catch {
1488
+ }
1489
+ return results;
1490
+ }
1491
+ function extractDroidProject(slug) {
1492
+ const parts = slug.split("-").filter(Boolean);
1493
+ return parts.length > 0 ? parts[parts.length - 1] : "unknown";
1494
+ }
1495
+ function toSafeNumber3(value) {
1496
+ const numberValue = Number(value);
1497
+ return Number.isFinite(numberValue) ? numberValue : 0;
1498
+ }
1499
+ var DroidParser = class {
1500
+ constructor(dataDir = DEFAULT_DATA_DIR3) {
1501
+ this.dataDir = dataDir;
1502
+ this.tool = createToolDefinition3(dataDir);
1503
+ }
1504
+ tool;
1505
+ async parse() {
1506
+ const sessionFiles = findSessionFiles3(this.dataDir);
1507
+ if (sessionFiles.length === 0) {
1508
+ return { buckets: [], sessions: [] };
1509
+ }
1510
+ const entries = [];
1511
+ const sessionEvents = [];
1512
+ for (const filePath of sessionFiles) {
1513
+ const sessionId = filePath;
1514
+ const project = extractDroidProject(basename4(dirname2(filePath)));
1515
+ let firstMessageTimestamp = null;
1516
+ const content = readFileSafe(filePath);
1517
+ if (!content) continue;
1518
+ for (const line of content.split("\n")) {
1519
+ if (!line.trim()) continue;
1520
+ let obj;
1521
+ try {
1522
+ obj = JSON.parse(line);
1523
+ } catch {
1524
+ continue;
1525
+ }
1526
+ if (obj.type !== "message" || !obj.timestamp) continue;
1527
+ const timestamp = new Date(obj.timestamp);
1528
+ if (Number.isNaN(timestamp.getTime())) continue;
1529
+ const role = obj.message?.role;
1530
+ if (role !== "user" && role !== "assistant") continue;
1531
+ if (firstMessageTimestamp === null) {
1532
+ firstMessageTimestamp = timestamp;
1533
+ }
1534
+ sessionEvents.push({
1535
+ sessionId,
1536
+ source: TOOL_ID3,
1537
+ project,
1538
+ timestamp,
1539
+ role
1540
+ });
1541
+ }
1542
+ const settingsPath = join10(
1543
+ dirname2(filePath),
1544
+ `${basename4(filePath, ".jsonl")}.settings.json`
1545
+ );
1546
+ const settingsContent = readFileSafe(settingsPath);
1547
+ if (!settingsContent || firstMessageTimestamp === null) continue;
1548
+ let settings;
1549
+ try {
1550
+ settings = JSON.parse(settingsContent);
1551
+ } catch {
1552
+ continue;
1553
+ }
1554
+ const tokenUsage = settings.tokenUsage;
1555
+ if (!tokenUsage) continue;
1556
+ const cachedTokens = toSafeNumber3(tokenUsage.cacheReadTokens);
1557
+ const reasoningTokens = toSafeNumber3(tokenUsage.thinkingTokens);
1558
+ const inputTokens = Math.max(
1559
+ 0,
1560
+ toSafeNumber3(tokenUsage.inputTokens) - cachedTokens
1561
+ );
1562
+ const outputTokens = Math.max(
1563
+ 0,
1564
+ toSafeNumber3(tokenUsage.outputTokens) - reasoningTokens
1565
+ );
1566
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && reasoningTokens === 0) {
1567
+ continue;
1568
+ }
1569
+ entries.push({
1570
+ sessionId,
1571
+ source: TOOL_ID3,
1572
+ model: settings.model || "unknown",
1573
+ project,
1574
+ timestamp: firstMessageTimestamp,
1575
+ inputTokens,
1576
+ outputTokens,
1577
+ reasoningTokens,
1578
+ cachedTokens
1579
+ });
1580
+ }
1581
+ return {
1582
+ buckets: aggregateToBuckets(entries),
1583
+ sessions: extractSessions(sessionEvents, entries)
1584
+ };
1585
+ }
1586
+ isInstalled() {
1587
+ return existsSync10(this.dataDir);
1588
+ }
1589
+ };
1590
+ registerParser(new DroidParser());
1591
+
1592
+ // src/parsers/pi-coding-agent.ts
1593
+ import { existsSync as existsSync11 } from "fs";
1594
+ import { homedir as homedir10 } from "os";
1595
+ import { join as join11 } from "path";
1596
+ var TOOL_ID4 = "pi-coding-agent";
1597
+ var TOOL_NAME4 = "pi";
1598
+ var DEFAULT_SESSIONS_DIR2 = join11(homedir10(), ".pi", "agent", "sessions");
1599
+ function createToolDefinition4(dataDir) {
1600
+ return {
1601
+ id: TOOL_ID4,
1602
+ name: TOOL_NAME4,
1603
+ dataDir
1604
+ };
1605
+ }
1606
+ function toSafeNumber4(value) {
1607
+ const numberValue = Number(value);
1608
+ return Number.isFinite(numberValue) ? numberValue : 0;
1609
+ }
1610
+ function getPathLeaf4(value) {
1611
+ const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
1612
+ const leaf = normalized.split("/").filter(Boolean).pop();
1613
+ return leaf || "unknown";
1614
+ }
1615
+ function normalizeForPrefix2(value) {
1616
+ return value.replace(/\\/g, "/").replace(/\/+$/, "");
1617
+ }
1618
+ function getUsageNumber(usage, ...keys) {
1619
+ for (const key of keys) {
1620
+ const value = usage[key];
1621
+ const numberValue = toSafeNumber4(value);
1622
+ if (numberValue > 0) {
1623
+ return numberValue;
1624
+ }
1625
+ }
1626
+ return 0;
1627
+ }
1628
+ function extractPiProjectFromCwd(cwd) {
1629
+ return getPathLeaf4(cwd);
1630
+ }
1631
+ function extractPiProjectFromDir(filePath, sessionsDir = DEFAULT_SESSIONS_DIR2) {
1632
+ const normalizedFilePath = normalizeForPrefix2(filePath);
1633
+ const normalizedSessionsDir = normalizeForPrefix2(sessionsDir);
1634
+ const prefix = `${normalizedSessionsDir}/`;
1635
+ if (!normalizedFilePath.startsWith(prefix)) {
1636
+ return "unknown";
1637
+ }
1638
+ const relativePath = normalizedFilePath.slice(prefix.length);
1639
+ const firstSegment = relativePath.split("/")[0];
1640
+ if (!firstSegment) {
1641
+ return "unknown";
1642
+ }
1643
+ try {
1644
+ const decoded = decodeURIComponent(firstSegment);
1645
+ if (decoded.includes("/") || decoded.includes("\\")) {
1646
+ return getPathLeaf4(decoded);
1647
+ }
1648
+ } catch {
1649
+ }
1650
+ const slugParts = firstSegment.split("-").filter(Boolean);
1651
+ return slugParts.length > 0 ? slugParts[slugParts.length - 1] : "unknown";
1652
+ }
1653
+ var PiCodingAgentParser = class {
1654
+ constructor(sessionsDir = DEFAULT_SESSIONS_DIR2) {
1655
+ this.sessionsDir = sessionsDir;
1656
+ this.tool = createToolDefinition4(sessionsDir);
1657
+ }
1658
+ tool;
1659
+ async parse() {
1660
+ const sessionFiles = findJsonlFiles(this.sessionsDir);
1661
+ if (sessionFiles.length === 0) {
1662
+ return { buckets: [], sessions: [] };
1663
+ }
1664
+ const entries = [];
1665
+ const sessionEvents = [];
1666
+ const seenEntryIds = /* @__PURE__ */ new Set();
1667
+ for (const filePath of sessionFiles) {
1668
+ const content = readFileSafe(filePath);
1669
+ if (!content) continue;
1670
+ const rows = parseJsonl(content);
1671
+ if (rows.length === 0) continue;
1672
+ let sessionId = filePath;
1673
+ let project = extractPiProjectFromDir(filePath, this.sessionsDir);
1674
+ for (const row of rows) {
1675
+ if (row.type !== "session") continue;
1676
+ if (row.id) {
1677
+ sessionId = row.id;
1678
+ }
1679
+ if (row.cwd) {
1680
+ project = extractPiProjectFromCwd(row.cwd);
1681
+ }
1682
+ break;
1683
+ }
1684
+ for (const row of rows) {
1685
+ if (row.type !== "message") continue;
1686
+ const message = row.message;
1687
+ if (!message) continue;
1688
+ const rawTimestamp = row.timestamp || message.timestamp;
1689
+ if (!rawTimestamp) continue;
1690
+ const timestamp = new Date(rawTimestamp);
1691
+ if (Number.isNaN(timestamp.getTime())) continue;
1692
+ if (message.role === "user" || message.role === "assistant") {
1693
+ sessionEvents.push({
1694
+ sessionId,
1695
+ source: TOOL_ID4,
1696
+ project,
1697
+ timestamp,
1698
+ role: message.role
1699
+ });
1700
+ }
1701
+ if (message.role !== "assistant") continue;
1702
+ const usage = message.usage;
1703
+ if (!usage) continue;
1704
+ const inputTokens = getUsageNumber(usage, "input", "inputTokens");
1705
+ const outputTokens = getUsageNumber(usage, "output", "outputTokens");
1706
+ const cachedTokens = getUsageNumber(
1707
+ usage,
1708
+ "cacheRead",
1709
+ "cacheReadTokens",
1710
+ "cache_read"
1711
+ );
1712
+ const reasoningTokens = getUsageNumber(
1713
+ usage,
1714
+ "reasoningOutputTokens",
1715
+ "thinkingTokens",
1716
+ "thoughts"
1717
+ );
1718
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && reasoningTokens === 0) {
1719
+ continue;
1720
+ }
1721
+ if (row.id) {
1722
+ if (seenEntryIds.has(row.id)) continue;
1723
+ seenEntryIds.add(row.id);
1724
+ }
1725
+ entries.push({
1726
+ sessionId,
1727
+ source: TOOL_ID4,
1728
+ model: message.model || "unknown",
1729
+ project,
1730
+ timestamp,
1731
+ inputTokens,
1732
+ outputTokens,
1733
+ reasoningTokens,
1734
+ cachedTokens
1735
+ });
1736
+ }
1737
+ }
1738
+ return {
1739
+ buckets: aggregateToBuckets(entries),
1740
+ sessions: extractSessions(sessionEvents, entries)
1741
+ };
1742
+ }
1743
+ isInstalled() {
1744
+ return existsSync11(this.sessionsDir);
1745
+ }
1746
+ };
1747
+ registerParser(new PiCodingAgentParser());
1748
+
1101
1749
  // src/cli.ts
1102
1750
  import { Command, Option } from "commander";
1103
1751
 
1104
1752
  // src/infrastructure/config/manager.ts
1105
1753
  import { randomUUID } from "crypto";
1106
1754
  import {
1107
- existsSync as existsSync8,
1755
+ existsSync as existsSync12,
1108
1756
  mkdirSync,
1109
1757
  readFileSync as readFileSync6,
1110
1758
  unlinkSync,
1111
1759
  writeFileSync
1112
1760
  } from "fs";
1113
- import { join as join9 } from "path";
1761
+ import { join as join13 } from "path";
1114
1762
 
1115
1763
  // src/infrastructure/xdg.ts
1116
- import { homedir as homedir7 } from "os";
1117
- import { join as join8 } from "path";
1764
+ import { homedir as homedir11 } from "os";
1765
+ import { join as join12 } from "path";
1118
1766
  function getConfigHome() {
1119
- return process.env.XDG_CONFIG_HOME || join8(homedir7(), ".config");
1767
+ return process.env.XDG_CONFIG_HOME || join12(homedir11(), ".config");
1120
1768
  }
1121
1769
  function getStateHome() {
1122
- return process.env.XDG_STATE_HOME || join8(homedir7(), ".local", "state");
1770
+ return process.env.XDG_STATE_HOME || join12(homedir11(), ".local", "state");
1123
1771
  }
1124
1772
  function getRuntimeDir() {
1125
1773
  return process.env.XDG_RUNTIME_DIR || getStateHome();
1126
1774
  }
1127
1775
 
1128
1776
  // src/infrastructure/config/manager.ts
1129
- var CONFIG_DIR = join9(getConfigHome(), "tokenarena");
1777
+ var CONFIG_DIR = join13(getConfigHome(), "tokenarena");
1130
1778
  var isDev = process.env.TOKEN_ARENA_DEV === "1";
1131
- var CONFIG_FILE = join9(CONFIG_DIR, isDev ? "config.dev.json" : "config.json");
1779
+ var CONFIG_FILE = join13(CONFIG_DIR, isDev ? "config.dev.json" : "config.json");
1132
1780
  var DEFAULT_API_URL = "https://token.poco-ai.com";
1133
1781
  var VALID_CONFIG_KEYS = [
1134
1782
  "apiKey",
@@ -1144,7 +1792,7 @@ function getConfigDir() {
1144
1792
  return CONFIG_DIR;
1145
1793
  }
1146
1794
  function loadConfig() {
1147
- if (!existsSync8(CONFIG_FILE)) return null;
1795
+ if (!existsSync12(CONFIG_FILE)) return null;
1148
1796
  try {
1149
1797
  const raw = readFileSync6(CONFIG_FILE, "utf-8");
1150
1798
  const config = JSON.parse(raw);
@@ -1162,7 +1810,7 @@ function saveConfig(config) {
1162
1810
  `, "utf-8");
1163
1811
  }
1164
1812
  function deleteConfig() {
1165
- if (existsSync8(CONFIG_FILE)) {
1813
+ if (existsSync12(CONFIG_FILE)) {
1166
1814
  unlinkSync(CONFIG_FILE);
1167
1815
  }
1168
1816
  }
@@ -1182,150 +1830,6 @@ function getDefaultApiUrl() {
1182
1830
  return process.env.TOKEN_ARENA_API_URL || DEFAULT_API_URL;
1183
1831
  }
1184
1832
 
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
-
1284
- // src/utils/logger.ts
1285
- var LOG_LEVELS = {
1286
- debug: 0,
1287
- info: 1,
1288
- warn: 2,
1289
- error: 3
1290
- };
1291
- var Logger = class {
1292
- level;
1293
- constructor(level = "info") {
1294
- this.level = level;
1295
- }
1296
- setLevel(level) {
1297
- this.level = level;
1298
- }
1299
- debug(msg) {
1300
- if (LOG_LEVELS[this.level] <= LOG_LEVELS.debug) {
1301
- process.stderr.write(`[debug] ${msg}
1302
- `);
1303
- }
1304
- }
1305
- info(msg) {
1306
- if (LOG_LEVELS[this.level] <= LOG_LEVELS.info) {
1307
- process.stdout.write(`${msg}
1308
- `);
1309
- }
1310
- }
1311
- warn(msg) {
1312
- if (LOG_LEVELS[this.level] <= LOG_LEVELS.warn) {
1313
- process.stderr.write(`warn: ${msg}
1314
- `);
1315
- }
1316
- }
1317
- error(msg) {
1318
- if (LOG_LEVELS[this.level] <= LOG_LEVELS.error) {
1319
- process.stderr.write(`error: ${msg}
1320
- `);
1321
- }
1322
- }
1323
- log(msg) {
1324
- this.info(msg);
1325
- }
1326
- };
1327
- var logger = new Logger();
1328
-
1329
1833
  // src/commands/config.ts
1330
1834
  var VALID_KEYS = ["apiKey", "apiUrl", "syncInterval", "logLevel"];
1331
1835
  function isConfigKey(value) {
@@ -1631,14 +2135,14 @@ import { hostname as hostname3 } from "os";
1631
2135
 
1632
2136
  // src/domain/project-identity.ts
1633
2137
  import { createHmac } from "crypto";
1634
- function toProjectIdentity(input2) {
1635
- if (input2.mode === "disabled") {
2138
+ function toProjectIdentity(input) {
2139
+ if (input.mode === "disabled") {
1636
2140
  return { projectKey: "unknown", projectLabel: "Unknown Project" };
1637
2141
  }
1638
- if (input2.mode === "raw") {
1639
- return { projectKey: input2.project, projectLabel: input2.project };
2142
+ if (input.mode === "raw") {
2143
+ return { projectKey: input.project, projectLabel: input.project };
1640
2144
  }
1641
- const projectKey = createHmac("sha256", input2.salt).update(input2.project).digest("hex").slice(0, 16);
2145
+ const projectKey = createHmac("sha256", input.salt).update(input.project).digest("hex").slice(0, 16);
1642
2146
  return {
1643
2147
  projectKey,
1644
2148
  projectLabel: `Project ${projectKey.slice(0, 6)}`
@@ -1654,13 +2158,13 @@ function shortHash(value) {
1654
2158
  function normalizeApiUrl(apiUrl) {
1655
2159
  return apiUrl.replace(/\/+$/, "");
1656
2160
  }
1657
- function buildUploadManifestScope(input2) {
2161
+ function buildUploadManifestScope(input) {
1658
2162
  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
2163
+ apiKeyHash: shortHash(input.apiKey),
2164
+ apiUrl: normalizeApiUrl(input.apiUrl),
2165
+ deviceId: input.deviceId,
2166
+ projectHashSaltHash: shortHash(input.settings.projectHashSalt),
2167
+ projectMode: input.settings.projectMode
1664
2168
  };
1665
2169
  }
1666
2170
  function describeUploadManifestScopeChanges(previous, current) {
@@ -1730,20 +2234,20 @@ function buildRecordHashes(items, getKey, getHash) {
1730
2234
  }
1731
2235
  return hashes;
1732
2236
  }
1733
- function createUploadManifest(input2) {
2237
+ function createUploadManifest(input) {
1734
2238
  return {
1735
2239
  buckets: buildRecordHashes(
1736
- input2.buckets,
2240
+ input.buckets,
1737
2241
  getUploadBucketManifestKey,
1738
2242
  getUploadBucketContentHash
1739
2243
  ),
1740
- scope: input2.scope,
2244
+ scope: input.scope,
1741
2245
  sessions: buildRecordHashes(
1742
- input2.sessions,
2246
+ input.sessions,
1743
2247
  getUploadSessionManifestKey,
1744
2248
  getUploadSessionContentHash
1745
2249
  ),
1746
- updatedAt: input2.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2250
+ updatedAt: input.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1747
2251
  version: MANIFEST_VERSION
1748
2252
  };
1749
2253
  }
@@ -1756,21 +2260,21 @@ function countRemovedRecords(previous, current) {
1756
2260
  }
1757
2261
  return removed;
1758
2262
  }
1759
- function diffUploadManifest(input2) {
2263
+ function diffUploadManifest(input) {
1760
2264
  const nextManifest = createUploadManifest({
1761
- buckets: input2.buckets,
1762
- scope: input2.scope,
1763
- sessions: input2.sessions,
1764
- updatedAt: input2.updatedAt
2265
+ buckets: input.buckets,
2266
+ scope: input.scope,
2267
+ sessions: input.sessions,
2268
+ updatedAt: input.updatedAt
1765
2269
  });
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) => {
2270
+ const scopeChangedReasons = input.previous ? describeUploadManifestScopeChanges(input.previous.scope, input.scope) : [];
2271
+ const previousBuckets = input.previous && scopeChangedReasons.length === 0 ? input.previous.buckets : {};
2272
+ const previousSessions = input.previous && scopeChangedReasons.length === 0 ? input.previous.sessions : {};
2273
+ const bucketsToUpload = input.buckets.filter((bucket) => {
1770
2274
  const key = getUploadBucketManifestKey(bucket);
1771
2275
  return previousBuckets[key] !== nextManifest.buckets[key];
1772
2276
  });
1773
- const sessionsToUpload = input2.sessions.filter((session) => {
2277
+ const sessionsToUpload = input.sessions.filter((session) => {
1774
2278
  const key = getUploadSessionManifestKey(session);
1775
2279
  return previousSessions[key] !== nextManifest.sessions[key];
1776
2280
  });
@@ -1784,8 +2288,8 @@ function diffUploadManifest(input2) {
1784
2288
  ),
1785
2289
  scopeChangedReasons,
1786
2290
  sessionsToUpload,
1787
- unchangedBuckets: input2.buckets.length - bucketsToUpload.length,
1788
- unchangedSessions: input2.sessions.length - sessionsToUpload.length
2291
+ unchangedBuckets: input.buckets.length - bucketsToUpload.length,
2292
+ unchangedSessions: input.sessions.length - sessionsToUpload.length
1789
2293
  };
1790
2294
  }
1791
2295
 
@@ -2019,7 +2523,7 @@ var ApiClient = class {
2019
2523
  // src/infrastructure/runtime/lock.ts
2020
2524
  import {
2021
2525
  closeSync,
2022
- existsSync as existsSync9,
2526
+ existsSync as existsSync13,
2023
2527
  openSync,
2024
2528
  readFileSync as readFileSync7,
2025
2529
  rmSync,
@@ -2028,22 +2532,22 @@ import {
2028
2532
 
2029
2533
  // src/infrastructure/runtime/paths.ts
2030
2534
  import { mkdirSync as mkdirSync2 } from "fs";
2031
- import { join as join10 } from "path";
2535
+ import { join as join14 } from "path";
2032
2536
  var APP_NAME = "tokenarena";
2033
2537
  function getRuntimeDirPath() {
2034
- return join10(getRuntimeDir(), APP_NAME);
2538
+ return join14(getRuntimeDir(), APP_NAME);
2035
2539
  }
2036
2540
  function getStateDir() {
2037
- return join10(getStateHome(), APP_NAME);
2541
+ return join14(getStateHome(), APP_NAME);
2038
2542
  }
2039
2543
  function getSyncLockPath() {
2040
- return join10(getRuntimeDirPath(), "sync.lock");
2544
+ return join14(getRuntimeDirPath(), "sync.lock");
2041
2545
  }
2042
2546
  function getSyncStatePath() {
2043
- return join10(getStateDir(), "status.json");
2547
+ return join14(getStateDir(), "status.json");
2044
2548
  }
2045
2549
  function getUploadManifestPath() {
2046
- return join10(getStateDir(), "upload-manifest.json");
2550
+ return join14(getStateDir(), "upload-manifest.json");
2047
2551
  }
2048
2552
  function ensureAppDirs() {
2049
2553
  mkdirSync2(getRuntimeDirPath(), { recursive: true });
@@ -2061,7 +2565,7 @@ function isProcessAlive(pid) {
2061
2565
  }
2062
2566
  }
2063
2567
  function readLockMetadata(lockPath) {
2064
- if (!existsSync9(lockPath)) {
2568
+ if (!existsSync13(lockPath)) {
2065
2569
  return null;
2066
2570
  }
2067
2571
  try {
@@ -2135,13 +2639,13 @@ function describeExistingSyncLock() {
2135
2639
  }
2136
2640
 
2137
2641
  // src/infrastructure/runtime/state.ts
2138
- import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
2642
+ import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
2139
2643
  function getDefaultState() {
2140
2644
  return { status: "idle" };
2141
2645
  }
2142
2646
  function loadSyncState() {
2143
2647
  const path = getSyncStatePath();
2144
- if (!existsSync10(path)) {
2648
+ if (!existsSync14(path)) {
2145
2649
  return getDefaultState();
2146
2650
  }
2147
2651
  try {
@@ -2202,7 +2706,7 @@ function markSyncFailed(source, error, status) {
2202
2706
  }
2203
2707
 
2204
2708
  // src/infrastructure/runtime/upload-manifest.ts
2205
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2709
+ import { existsSync as existsSync15, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2206
2710
  function isRecordOfStrings(value) {
2207
2711
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2208
2712
  return false;
@@ -2218,7 +2722,7 @@ function isUploadManifest(value) {
2218
2722
  }
2219
2723
  function loadUploadManifest() {
2220
2724
  const path = getUploadManifestPath();
2221
- if (!existsSync11(path)) {
2725
+ if (!existsSync15(path)) {
2222
2726
  return null;
2223
2727
  }
2224
2728
  try {
@@ -2646,15 +3150,15 @@ View your dashboard at: ${apiUrl}/usage`);
2646
3150
 
2647
3151
  // src/commands/init.ts
2648
3152
  import { execFileSync as execFileSync2, spawn } from "child_process";
2649
- import { existsSync as existsSync12 } from "fs";
3153
+ import { existsSync as existsSync16 } from "fs";
2650
3154
  import { appendFile, mkdir, readFile } from "fs/promises";
2651
- import { homedir as homedir8, platform } from "os";
2652
- import { dirname as dirname2, join as join11, posix, win32 } from "path";
3155
+ import { homedir as homedir12, platform } from "os";
3156
+ import { dirname as dirname3, join as join15, posix, win32 } from "path";
2653
3157
  function joinForPlatform(currentPlatform, ...parts) {
2654
3158
  return currentPlatform === "win32" ? win32.join(...parts) : posix.join(...parts);
2655
3159
  }
2656
- function basenameLikeShell(input2) {
2657
- return input2.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input2;
3160
+ function basenameLikeShell(input) {
3161
+ return input.split(/[\\/]+/).pop()?.replace(/\.exe$/i, "") ?? input;
2658
3162
  }
2659
3163
  function getBrowserLaunchCommand(url, currentPlatform = platform()) {
2660
3164
  switch (currentPlatform) {
@@ -2693,7 +3197,7 @@ function resolvePowerShellProfilePath() {
2693
3197
  const systemRoot = process.env.SYSTEMROOT || "C:\\Windows";
2694
3198
  const candidates = [
2695
3199
  "pwsh.exe",
2696
- join11(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
3200
+ join15(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
2697
3201
  ];
2698
3202
  for (const command of candidates) {
2699
3203
  try {
@@ -2722,8 +3226,8 @@ function resolvePowerShellProfilePath() {
2722
3226
  function resolveShellAliasSetup(options = {}) {
2723
3227
  const currentPlatform = options.currentPlatform ?? platform();
2724
3228
  const env = options.env ?? process.env;
2725
- const homeDir = options.homeDir ?? homedir8();
2726
- const pathExists = options.exists ?? existsSync12;
3229
+ const homeDir = options.homeDir ?? homedir12();
3230
+ const pathExists = options.exists ?? existsSync16;
2727
3231
  const shellFromEnv = env.SHELL ? basenameLikeShell(env.SHELL).toLowerCase() : "";
2728
3232
  const shellName = shellFromEnv || (currentPlatform === "win32" ? "powershell" : "");
2729
3233
  const aliasName = "ta";
@@ -2879,6 +3383,30 @@ async function runInit(opts = {}) {
2879
3383
  logger.info(formatBullet("TokenArena \u5DF2\u51C6\u5907\u5C31\u7EEA\u3002", "success"));
2880
3384
  logger.info(formatKeyValue("\u63A7\u5236\u53F0", `${apiUrl}/usage`));
2881
3385
  await setupShellAlias();
3386
+ await setupSystemdService();
3387
+ }
3388
+ async function setupSystemdService() {
3389
+ const currentPlatform = platform();
3390
+ if (currentPlatform !== "linux") {
3391
+ return;
3392
+ }
3393
+ if (!isCommandAvailable("systemctl")) {
3394
+ return;
3395
+ }
3396
+ const shouldSetup = await promptConfirm({
3397
+ message: "\u662F\u5426\u8BBE\u7F6E systemd \u670D\u52A1\u4EE5\u81EA\u52A8\u8FD0\u884C daemon\uFF1F",
3398
+ defaultValue: true
3399
+ });
3400
+ if (!shouldSetup) {
3401
+ logger.info(formatBullet("\u5DF2\u8DF3\u8FC7 systemd \u670D\u52A1\u8BBE\u7F6E\u3002"));
3402
+ return;
3403
+ }
3404
+ try {
3405
+ const { runInstallService: runInstallService2 } = await import("./service-4U7K4DKW.js");
3406
+ await runInstallService2({ action: "setup", skipPrompt: true });
3407
+ } catch (err) {
3408
+ logger.warn(`systemd \u670D\u52A1\u8BBE\u7F6E\u5931\u8D25: ${err.message}`);
3409
+ }
2882
3410
  }
2883
3411
  async function setupShellAlias() {
2884
3412
  const setup = resolveShellAliasSetup();
@@ -2894,9 +3422,9 @@ async function setupShellAlias() {
2894
3422
  return;
2895
3423
  }
2896
3424
  try {
2897
- await mkdir(dirname2(setup.configFile), { recursive: true });
3425
+ await mkdir(dirname3(setup.configFile), { recursive: true });
2898
3426
  let existingContent = "";
2899
- if (existsSync12(setup.configFile)) {
3427
+ if (existsSync16(setup.configFile)) {
2900
3428
  existingContent = await readFile(setup.configFile, "utf-8");
2901
3429
  }
2902
3430
  const normalizedContent = existingContent.toLowerCase();
@@ -3089,8 +3617,8 @@ async function runSyncCommand(opts = {}) {
3089
3617
  }
3090
3618
 
3091
3619
  // src/commands/uninstall.ts
3092
- import { existsSync as existsSync13, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3093
- import { homedir as homedir9, platform as platform2 } from "os";
3620
+ import { existsSync as existsSync17, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3621
+ import { homedir as homedir13, platform as platform2 } from "os";
3094
3622
  function removeShellAlias() {
3095
3623
  const shell = process.env.SHELL;
3096
3624
  if (!shell) return;
@@ -3099,22 +3627,22 @@ function removeShellAlias() {
3099
3627
  let configFile;
3100
3628
  switch (shellName) {
3101
3629
  case "zsh":
3102
- configFile = `${homedir9()}/.zshrc`;
3630
+ configFile = `${homedir13()}/.zshrc`;
3103
3631
  break;
3104
3632
  case "bash":
3105
- if (platform2() === "darwin" && existsSync13(`${homedir9()}/.bash_profile`)) {
3106
- configFile = `${homedir9()}/.bash_profile`;
3633
+ if (platform2() === "darwin" && existsSync17(`${homedir13()}/.bash_profile`)) {
3634
+ configFile = `${homedir13()}/.bash_profile`;
3107
3635
  } else {
3108
- configFile = `${homedir9()}/.bashrc`;
3636
+ configFile = `${homedir13()}/.bashrc`;
3109
3637
  }
3110
3638
  break;
3111
3639
  case "fish":
3112
- configFile = `${homedir9()}/.config/fish/config.fish`;
3640
+ configFile = `${homedir13()}/.config/fish/config.fish`;
3113
3641
  break;
3114
3642
  default:
3115
3643
  return;
3116
3644
  }
3117
- if (!existsSync13(configFile)) return;
3645
+ if (!existsSync17(configFile)) return;
3118
3646
  try {
3119
3647
  let content = readFileSync10(configFile, "utf-8");
3120
3648
  const aliasPatterns = [
@@ -3149,7 +3677,7 @@ function removeShellAlias() {
3149
3677
  async function runUninstall() {
3150
3678
  const configPath = getConfigPath();
3151
3679
  const configDir = getConfigDir();
3152
- if (!existsSync13(configPath)) {
3680
+ if (!existsSync17(configPath)) {
3153
3681
  logger.info(formatHeader("\u5378\u8F7D TokenArena"));
3154
3682
  logger.info(formatBullet("\u672A\u53D1\u73B0\u672C\u5730\u914D\u7F6E\uFF0C\u65E0\u9700\u5378\u8F7D\u3002"));
3155
3683
  return;
@@ -3178,7 +3706,7 @@ async function runUninstall() {
3178
3706
  deleteConfig();
3179
3707
  logger.info(formatSection("\u6267\u884C\u7ED3\u679C"));
3180
3708
  logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u6587\u4EF6\u3002", "success"));
3181
- if (existsSync13(configDir)) {
3709
+ if (existsSync17(configDir)) {
3182
3710
  try {
3183
3711
  rmSync2(configDir, { recursive: false, force: true });
3184
3712
  logger.info(formatBullet("\u5DF2\u5220\u9664\u914D\u7F6E\u76EE\u5F55\u3002", "success"));
@@ -3186,12 +3714,12 @@ async function runUninstall() {
3186
3714
  }
3187
3715
  }
3188
3716
  const stateDir = getStateDir();
3189
- if (existsSync13(stateDir)) {
3717
+ if (existsSync17(stateDir)) {
3190
3718
  rmSync2(stateDir, { recursive: true, force: true });
3191
3719
  logger.info(formatBullet("\u5DF2\u5220\u9664\u72B6\u6001\u6570\u636E\u3002", "success"));
3192
3720
  }
3193
3721
  const runtimeDir = getRuntimeDirPath();
3194
- if (existsSync13(runtimeDir)) {
3722
+ if (existsSync17(runtimeDir)) {
3195
3723
  rmSync2(runtimeDir, { recursive: true, force: true });
3196
3724
  logger.info(formatBullet("\u5DF2\u5220\u9664\u8FD0\u884C\u65F6\u6570\u636E\u3002", "success"));
3197
3725
  }
@@ -3316,7 +3844,7 @@ async function runHome(program) {
3316
3844
 
3317
3845
  // src/infrastructure/runtime/cli-version.ts
3318
3846
  import { readFileSync as readFileSync11 } from "fs";
3319
- import { dirname as dirname3, join as join12 } from "path";
3847
+ import { dirname as dirname4, join as join16 } from "path";
3320
3848
  import { fileURLToPath } from "url";
3321
3849
  var FALLBACK_VERSION = "0.0.0";
3322
3850
  var cachedVersion;
@@ -3324,8 +3852,8 @@ function getCliVersion(metaUrl = import.meta.url) {
3324
3852
  if (cachedVersion) {
3325
3853
  return cachedVersion;
3326
3854
  }
3327
- const packageJsonPath = join12(
3328
- dirname3(fileURLToPath(metaUrl)),
3855
+ const packageJsonPath = join16(
3856
+ dirname4(fileURLToPath(metaUrl)),
3329
3857
  "..",
3330
3858
  "package.json"
3331
3859
  );
@@ -3363,6 +3891,11 @@ function createCli() {
3363
3891
  program.command("daemon").description("Run continuous sync (every 5 minutes by default)").option("--interval <ms>", "Sync interval in milliseconds", parseInt).action(async (opts) => {
3364
3892
  await runDaemon(opts);
3365
3893
  });
3894
+ program.command("service [action]").description(
3895
+ "Manage systemd user service (setup|start|stop|restart|status|uninstall)"
3896
+ ).action(async (action) => {
3897
+ await runInstallService({ action });
3898
+ });
3366
3899
  program.command("status").description("Show configuration and detected tools").action(async () => {
3367
3900
  await runStatus();
3368
3901
  });
@@ -3380,7 +3913,7 @@ function createCli() {
3380
3913
  }
3381
3914
 
3382
3915
  // src/infrastructure/runtime/main-module.ts
3383
- import { existsSync as existsSync14, realpathSync } from "fs";
3916
+ import { existsSync as existsSync18, realpathSync } from "fs";
3384
3917
  import { resolve } from "path";
3385
3918
  import { fileURLToPath as fileURLToPath2 } from "url";
3386
3919
  function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
@@ -3391,7 +3924,7 @@ function isMainModule(argvEntry = process.argv[1], metaUrl = import.meta.url) {
3391
3924
  try {
3392
3925
  return realpathSync(argvEntry) === realpathSync(currentModulePath);
3393
3926
  } catch {
3394
- if (!existsSync14(argvEntry)) {
3927
+ if (!existsSync18(argvEntry)) {
3395
3928
  return false;
3396
3929
  }
3397
3930
  return resolve(argvEntry) === resolve(currentModulePath);