@hasna/economy 0.3.5 → 0.3.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAqpBjC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAg0BjC"}
@@ -668,6 +668,16 @@ function addColumnIfMissing(db, table, column, definition) {
668
668
  }
669
669
  function initSchema(db) {
670
670
  db.exec(`
671
+ CREATE TABLE IF NOT EXISTS cost_centers (
672
+ id TEXT PRIMARY KEY,
673
+ kind TEXT NOT NULL,
674
+ name TEXT NOT NULL,
675
+ repo_path TEXT,
676
+ labels_json TEXT DEFAULT '{}',
677
+ created_at TEXT NOT NULL,
678
+ updated_at TEXT NOT NULL
679
+ );
680
+
671
681
  CREATE TABLE IF NOT EXISTS requests (
672
682
  id TEXT PRIMARY KEY,
673
683
  agent TEXT NOT NULL,
@@ -684,6 +694,7 @@ function initSchema(db) {
684
694
  timestamp TEXT NOT NULL,
685
695
  source_request_id TEXT,
686
696
  machine_id TEXT DEFAULT '',
697
+ cost_center_id TEXT,
687
698
  account_key TEXT DEFAULT '',
688
699
  account_tool TEXT DEFAULT '',
689
700
  account_name TEXT DEFAULT '',
@@ -702,6 +713,7 @@ function initSchema(db) {
702
713
  total_tokens INTEGER DEFAULT 0,
703
714
  request_count INTEGER DEFAULT 0,
704
715
  machine_id TEXT DEFAULT '',
716
+ cost_center_id TEXT,
705
717
  account_key TEXT DEFAULT '',
706
718
  account_tool TEXT DEFAULT '',
707
719
  account_name TEXT DEFAULT '',
@@ -722,6 +734,7 @@ function initSchema(db) {
722
734
  id TEXT PRIMARY KEY,
723
735
  project_path TEXT,
724
736
  agent TEXT,
737
+ cost_center_id TEXT,
725
738
  period TEXT NOT NULL,
726
739
  limit_usd REAL NOT NULL,
727
740
  alert_at_percent INTEGER DEFAULT 80,
@@ -752,6 +765,7 @@ function initSchema(db) {
752
765
  CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
753
766
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
754
767
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
768
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
755
769
 
756
770
  CREATE TABLE IF NOT EXISTS model_pricing (
757
771
  model TEXT PRIMARY KEY,
@@ -838,6 +852,12 @@ function initSchema(db) {
838
852
  `);
839
853
  addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
840
854
  addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
855
+ if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
856
+ db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
857
+ }
858
+ addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
859
+ addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
860
+ addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
841
861
  if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
842
862
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
843
863
  }
@@ -866,8 +886,20 @@ function initSchema(db) {
866
886
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
867
887
  CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
868
888
  CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
889
+ CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
890
+ CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
891
+ CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
892
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
869
893
  `);
870
894
  }
895
+ function upsertCostCenter(db, center) {
896
+ const updatedAt = center.updated_at ?? center.created_at;
897
+ db.prepare(`
898
+ INSERT OR REPLACE INTO cost_centers
899
+ (id, kind, name, repo_path, labels_json, created_at, updated_at)
900
+ VALUES (?, ?, ?, ?, ?, ?, ?)
901
+ `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
902
+ }
871
903
  function upsertRequest(db, req) {
872
904
  const now = req.updated_at ?? new Date().toISOString();
873
905
  db.prepare(`
@@ -875,10 +907,10 @@ function upsertRequest(db, req) {
875
907
  (id, agent, session_id, model, input_tokens, output_tokens,
876
908
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
877
909
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
878
- source_request_id, machine_id, attribution_tag, account_key, account_tool,
910
+ source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
879
911
  account_name, account_email, account_source, updated_at, synced_at)
880
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
881
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
912
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
913
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
882
914
  }
883
915
  function upsertSession(db, session) {
884
916
  const now = session.updated_at ?? new Date().toISOString();
@@ -886,9 +918,9 @@ function upsertSession(db, session) {
886
918
  INSERT OR REPLACE INTO sessions
887
919
  (id, agent, project_path, project_name, started_at, ended_at,
888
920
  total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
889
- account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
890
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
891
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
921
+ cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
922
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
923
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.cost_center_id ?? null, session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
892
924
  }
893
925
  function rollupSession(db, sessionId) {
894
926
  db.prepare(`
@@ -914,9 +946,12 @@ function rollupSession(db, sessionId) {
914
946
  ELSE account_email END,
915
947
  account_source = CASE WHEN account_source = '' OR account_source IS NULL
916
948
  THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
917
- ELSE account_source END
949
+ ELSE account_source END,
950
+ cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
951
+ THEN COALESCE((SELECT cost_center_id FROM requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
952
+ ELSE cost_center_id END
918
953
  WHERE id = ?
919
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
954
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
920
955
  }
921
956
  function upsertModelPricing(db, p) {
922
957
  db.prepare(`
@@ -995,6 +1030,97 @@ function attrsMap(attributes) {
995
1030
  }
996
1031
  return out;
997
1032
  }
1033
+ var COST_CENTER_KINDS = ["loop", "app", "repo", "service", "team"];
1034
+ var PSEUDO_AGENTS = ["app", "service", "repo", "loop"];
1035
+ function isCostCenterKind(value) {
1036
+ return COST_CENTER_KINDS.includes(value);
1037
+ }
1038
+ function asString(value) {
1039
+ if (value == null)
1040
+ return;
1041
+ const text = String(value).trim();
1042
+ return text ? text : undefined;
1043
+ }
1044
+ function firstString(source, keys) {
1045
+ for (const key of keys) {
1046
+ const value = asString(source[key]);
1047
+ if (value)
1048
+ return value;
1049
+ }
1050
+ return;
1051
+ }
1052
+ function normalizeCostCenterKind(raw) {
1053
+ if (!raw)
1054
+ return;
1055
+ const value = raw.toLowerCase();
1056
+ return isCostCenterKind(value) ? value : undefined;
1057
+ }
1058
+ function normalizeAgent(raw, kind) {
1059
+ if (raw && isAgent(raw))
1060
+ return raw;
1061
+ if (raw && PSEUDO_AGENTS.includes(raw))
1062
+ return raw;
1063
+ if (kind && PSEUDO_AGENTS.includes(kind))
1064
+ return kind;
1065
+ return "service";
1066
+ }
1067
+ function slug(value) {
1068
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
1069
+ }
1070
+ function costCenterId(kind, name, explicitId) {
1071
+ if (explicitId)
1072
+ return explicitId;
1073
+ if (!kind || !name)
1074
+ return;
1075
+ return `${kind}:${slug(name)}`;
1076
+ }
1077
+ function projectNameFromPath(projectPath) {
1078
+ if (!projectPath)
1079
+ return;
1080
+ return projectPath.split("/").filter(Boolean).at(-1);
1081
+ }
1082
+ function extractAttribution(source) {
1083
+ const sourceKind = firstString(source, ["source", "source_kind", "ai.source"]);
1084
+ const rawKind = firstString(source, ["cost_center_kind", "cost_center.kind", "economy.cost_center.kind"]) ?? sourceKind;
1085
+ const costCenterKind = normalizeCostCenterKind(rawKind);
1086
+ const costCenter = firstString(source, [
1087
+ "cost_center",
1088
+ "cost_center_name",
1089
+ "cost_center.name",
1090
+ "economy.cost_center",
1091
+ "service.name",
1092
+ "app",
1093
+ "app.name",
1094
+ "repo",
1095
+ "repository"
1096
+ ]);
1097
+ const projectPath = firstString(source, ["project_path", "project.path", "repo_path", "repository.path"]);
1098
+ const explicitId = firstString(source, ["cost_center_id", "cost_center.id", "economy.cost_center.id"]);
1099
+ const costBasisRaw = firstString(source, ["cost_basis", "economy.cost_basis"]);
1100
+ const costBasis = costBasisRaw && ["metered_api", "subscription_included", "estimated", "unknown"].includes(costBasisRaw) ? costBasisRaw : undefined;
1101
+ return {
1102
+ agent: normalizeAgent(firstString(source, ["agent", "ai.agent"]) ?? sourceKind, costCenterKind),
1103
+ cost_center: costCenter,
1104
+ cost_center_kind: costCenterKind,
1105
+ cost_center_id: costCenterId(costCenterKind, costCenter, explicitId),
1106
+ attribution_tag: firstString(source, ["attribution_tag", "attribution.tag", "economy.attribution_tag"]),
1107
+ project_path: projectPath,
1108
+ project_name: firstString(source, ["project_name", "project.name"]) ?? projectNameFromPath(projectPath) ?? costCenter,
1109
+ repo: firstString(source, ["repo", "repository", "repository.name"]),
1110
+ account_key: firstString(source, ["account_key", "account.key", "economy.account_key"]),
1111
+ account_tool: firstString(source, ["account_tool", "account.tool", "economy.account_tool"]),
1112
+ account_name: firstString(source, ["account_name", "account.name", "economy.account_name"]),
1113
+ account_email: firstString(source, ["account_email", "account.email", "economy.account_email"]),
1114
+ account_source: firstString(source, ["account_source", "account.source", "economy.account_source"]),
1115
+ cost_basis: costBasis
1116
+ };
1117
+ }
1118
+ function mergeAttribution(row, attribution) {
1119
+ return {
1120
+ ...row,
1121
+ ...Object.fromEntries(Object.entries(attribution).filter(([, value]) => value !== undefined && value !== ""))
1122
+ };
1123
+ }
998
1124
  function metricKind(body) {
999
1125
  const name = String(body["name"] ?? "").toLowerCase();
1000
1126
  if (name.includes("cost") || name.includes(".usd"))
@@ -1043,13 +1169,13 @@ function parseOtlpMetrics(body) {
1043
1169
  continue;
1044
1170
  const pointAttrs = attrsMap(dp.attributes);
1045
1171
  const merged = { ...resourceAttrs, ...pointAttrs };
1046
- const agentRaw = String(merged["agent"] ?? merged["ai.agent"] ?? "unknown");
1047
- const agent = isAgent(agentRaw) ? agentRaw : "opencode";
1172
+ const attribution = extractAttribution(merged);
1173
+ const agent = attribution.agent;
1048
1174
  const sessionId = String(merged["session_id"] ?? merged["session.id"] ?? "otel-session");
1049
1175
  const model = String(merged["model"] ?? merged["ai.model"] ?? "unknown");
1050
1176
  const sourceId = String(merged["request_id"] ?? merged["event.id"] ?? `${sessionId}-${kind}`);
1051
1177
  const key = `${agent}:${sourceId}`;
1052
- const row = partial.get(key) ?? {
1178
+ const row = mergeAttribution(partial.get(key) ?? {
1053
1179
  key,
1054
1180
  agent,
1055
1181
  session_id: sessionId,
@@ -1059,7 +1185,7 @@ function parseOtlpMetrics(body) {
1059
1185
  output_tokens: 0,
1060
1186
  timestamp: new Date().toISOString(),
1061
1187
  source_request_id: sourceId
1062
- };
1188
+ }, attribution);
1063
1189
  const asDouble = dp.asDouble;
1064
1190
  const asInt = dp.asInt;
1065
1191
  const value = asDouble ?? (asInt != null ? Number(asInt) : 0);
@@ -1086,16 +1212,27 @@ function parseOtlpMetrics(body) {
1086
1212
  input_tokens: r.input_tokens ?? 0,
1087
1213
  output_tokens: r.output_tokens ?? 0,
1088
1214
  timestamp: r.timestamp,
1089
- source_request_id: r.source_request_id
1215
+ source_request_id: r.source_request_id,
1216
+ cost_basis: r.cost_basis,
1217
+ cost_center: r.cost_center,
1218
+ cost_center_kind: r.cost_center_kind,
1219
+ cost_center_id: r.cost_center_id,
1220
+ attribution_tag: r.attribution_tag,
1221
+ project_path: r.project_path,
1222
+ project_name: r.project_name,
1223
+ repo: r.repo,
1224
+ account_key: r.account_key,
1225
+ account_tool: r.account_tool,
1226
+ account_name: r.account_name,
1227
+ account_email: r.account_email,
1228
+ account_source: r.account_source
1090
1229
  }));
1091
1230
  }
1092
1231
  function parseSimpleIngest(body) {
1093
1232
  if (!body || typeof body !== "object")
1094
1233
  return null;
1095
1234
  const b = body;
1096
- const agentRaw = String(b["agent"] ?? "");
1097
- if (!isAgent(agentRaw))
1098
- return null;
1235
+ const attribution = extractAttribution(b);
1099
1236
  const cost = Number(b["cost_usd"] ?? 0);
1100
1237
  const input = Number(b["input_tokens"] ?? 0);
1101
1238
  const output = Number(b["output_tokens"] ?? 0);
@@ -1104,7 +1241,8 @@ function parseSimpleIngest(body) {
1104
1241
  const sessionId = String(b["session_id"] ?? "otel-session");
1105
1242
  const sourceId = String(b["request_id"] ?? b["source_request_id"] ?? `${sessionId}-${Date.now()}`);
1106
1243
  return {
1107
- agent: agentRaw,
1244
+ ...attribution,
1245
+ agent: attribution.agent,
1108
1246
  session_id: sessionId,
1109
1247
  model: String(b["model"] ?? "unknown"),
1110
1248
  cost_usd: cost,
@@ -1114,12 +1252,31 @@ function parseSimpleIngest(body) {
1114
1252
  source_request_id: sourceId
1115
1253
  };
1116
1254
  }
1255
+ function ensureCostCenter(db, row) {
1256
+ const id = row.cost_center_id ?? costCenterId(row.cost_center_kind, row.cost_center);
1257
+ if (!id || !row.cost_center_kind || !row.cost_center)
1258
+ return id;
1259
+ const labels = { source: "otel" };
1260
+ if (row.repo)
1261
+ labels["repo"] = row.repo;
1262
+ upsertCostCenter(db, {
1263
+ id,
1264
+ kind: row.cost_center_kind,
1265
+ name: row.cost_center,
1266
+ repo_path: row.project_path ?? null,
1267
+ labels_json: JSON.stringify(labels),
1268
+ created_at: row.timestamp
1269
+ });
1270
+ return id;
1271
+ }
1117
1272
  async function ingestOtelRows(db, rows) {
1118
1273
  const machineId = getMachineId();
1119
1274
  const sessions = new Set;
1120
1275
  let requests = 0;
1121
1276
  for (const row of rows) {
1122
- const reqId = `otel-${row.agent}-${row.source_request_id}`;
1277
+ const costCenterId2 = ensureCostCenter(db, row);
1278
+ const requestScope = slug(costCenterId2 ?? row.cost_center ?? row.project_path ?? row.session_id);
1279
+ const reqId = `otel-${row.agent}-${requestScope}-${row.source_request_id}`;
1123
1280
  upsertRequest(db, {
1124
1281
  id: reqId,
1125
1282
  agent: row.agent,
@@ -1130,25 +1287,38 @@ async function ingestOtelRows(db, rows) {
1130
1287
  cache_read_tokens: 0,
1131
1288
  cache_create_tokens: 0,
1132
1289
  cost_usd: row.cost_usd,
1133
- cost_basis: "estimated",
1290
+ cost_basis: row.cost_basis ?? "estimated",
1134
1291
  duration_ms: 0,
1135
1292
  timestamp: row.timestamp,
1136
1293
  source_request_id: row.source_request_id,
1137
1294
  machine_id: machineId,
1138
- attribution_tag: "otel"
1295
+ cost_center_id: costCenterId2 ?? null,
1296
+ attribution_tag: row.attribution_tag ?? "otel",
1297
+ account_key: row.account_key ?? "",
1298
+ account_tool: row.account_tool ?? "",
1299
+ account_name: row.account_name ?? "",
1300
+ account_email: row.account_email ?? "",
1301
+ account_source: row.account_source ?? ""
1139
1302
  });
1140
1303
  if (!sessions.has(row.session_id)) {
1141
1304
  upsertSession(db, {
1142
1305
  id: row.session_id,
1143
1306
  agent: row.agent,
1144
- project_path: "",
1145
- project_name: "",
1307
+ project_path: row.project_path ?? "",
1308
+ project_name: row.project_name ?? row.repo ?? row.cost_center ?? "",
1146
1309
  started_at: row.timestamp,
1147
1310
  ended_at: null,
1148
1311
  total_cost_usd: 0,
1149
1312
  total_tokens: 0,
1150
1313
  request_count: 0,
1151
- machine_id: machineId
1314
+ machine_id: machineId,
1315
+ cost_center_id: costCenterId2 ?? null,
1316
+ attribution_tag: row.attribution_tag ?? "otel",
1317
+ account_key: row.account_key ?? "",
1318
+ account_tool: row.account_tool ?? "",
1319
+ account_name: row.account_name ?? "",
1320
+ account_email: row.account_email ?? "",
1321
+ account_source: row.account_source ?? ""
1152
1322
  });
1153
1323
  sessions.add(row.session_id);
1154
1324
  }