@hasna/economy 0.2.26 → 0.2.27

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.
@@ -1038,9 +1038,7 @@ function queryAgentBreakdown(db, period = "all") {
1038
1038
  }
1039
1039
  return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1040
1040
  }
1041
- function labelForPath(projectPath, projectName) {
1042
- if (projectName && projectName.trim() !== "")
1043
- return projectName;
1041
+ function pathProjectLabel(projectPath) {
1044
1042
  if (!projectPath)
1045
1043
  return "";
1046
1044
  const segments = projectPath.split("/").filter(Boolean);
@@ -1049,12 +1047,45 @@ function labelForPath(projectPath, projectName) {
1049
1047
  if (projectPrefix.test(seg))
1050
1048
  return seg;
1051
1049
  }
1052
- const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
1050
+ const generic = new Set([
1051
+ "web",
1052
+ "app",
1053
+ "apps",
1054
+ "packages",
1055
+ "src",
1056
+ "lib",
1057
+ "server",
1058
+ "client",
1059
+ "api",
1060
+ "frontend",
1061
+ "backend",
1062
+ "home",
1063
+ "users",
1064
+ "workspace",
1065
+ "workspaces",
1066
+ "hasna"
1067
+ ]);
1053
1068
  for (let i = segments.length - 1;i >= 0; i--) {
1054
1069
  if (!generic.has(segments[i].toLowerCase()))
1055
1070
  return segments[i];
1056
1071
  }
1057
- return segments[segments.length - 1] ?? projectPath;
1072
+ return null;
1073
+ }
1074
+ function isRepoLikeLabel(label) {
1075
+ return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
1076
+ }
1077
+ function labelForPath(projectPath, projectName) {
1078
+ const pathLabel = pathProjectLabel(projectPath);
1079
+ if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
1080
+ return pathLabel;
1081
+ if (projectName && projectName.trim() !== "")
1082
+ return projectName;
1083
+ if (pathLabel)
1084
+ return pathLabel;
1085
+ return projectPath;
1086
+ }
1087
+ function groupKeyForPath(projectPath, projectName) {
1088
+ return labelForPath(projectPath, projectName).trim().toLowerCase();
1058
1089
  }
1059
1090
  function queryProjectBreakdown(db, period = "all") {
1060
1091
  const requestWhere = requestPeriodWhere(period);
@@ -1069,14 +1100,15 @@ function queryProjectBreakdown(db, period = "all") {
1069
1100
  const label = labelForPath(s.project_path, s.project_name);
1070
1101
  if (!label)
1071
1102
  continue;
1072
- const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path };
1103
+ const key = groupKeyForPath(s.project_path, s.project_name);
1104
+ const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
1073
1105
  g.sessionIds.push(s.id);
1074
1106
  if (!g.samplePath)
1075
1107
  g.samplePath = s.project_path;
1076
- groups.set(label, g);
1108
+ groups.set(key, g);
1077
1109
  }
1078
1110
  const result = [];
1079
- for (const [label, g] of groups.entries()) {
1111
+ for (const g of groups.values()) {
1080
1112
  const placeholders = g.sessionIds.map(() => "?").join(",");
1081
1113
  const reqStats = placeholders.length ? db.prepare(`
1082
1114
  SELECT
@@ -1107,7 +1139,7 @@ function queryProjectBreakdown(db, period = "all") {
1107
1139
  const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1108
1140
  result.push({
1109
1141
  project_path: g.samplePath,
1110
- project_name: label,
1142
+ project_name: g.label,
1111
1143
  sessions: totalSessions,
1112
1144
  requests: reqStats.requests + sessionOnlyStats.requests,
1113
1145
  total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
@@ -1430,17 +1462,21 @@ function listMachineRegistry(db) {
1430
1462
  }
1431
1463
  function dedupeRequests(db) {
1432
1464
  const dupes = db.prepare(`
1433
- SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
1465
+ SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
1434
1466
  FROM requests
1435
1467
  WHERE source_request_id != '' AND source_request_id IS NOT NULL
1436
- GROUP BY source_request_id, agent
1468
+ GROUP BY source_request_id, agent, COALESCE(machine_id, '')
1437
1469
  HAVING cnt > 1
1438
1470
  `).all();
1439
1471
  let removed = 0;
1440
1472
  for (const row of dupes) {
1441
1473
  const result = db.prepare(`
1442
- DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
1443
- `).run(row.source_request_id, row.agent, row.keep_id);
1474
+ DELETE FROM requests
1475
+ WHERE source_request_id = ?
1476
+ AND agent = ?
1477
+ AND COALESCE(machine_id, '') = ?
1478
+ AND id != ?
1479
+ `).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
1444
1480
  removed += result.changes;
1445
1481
  }
1446
1482
  return removed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",