@hasna/economy 0.2.24 → 0.2.26
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/README.md +9 -3
- package/dist/cli/index.js +24 -2
- package/dist/index.js +22 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/mcp/index.js +27 -4
- package/dist/server/index.js +22 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,12 @@ Full sync also imports active project metadata from `@hasna/projects` when the r
|
|
|
106
106
|
|
|
107
107
|
Account attribution is automatic when `@hasna/accounts` has a matching active, applied, or env-dir profile for the agent. You can also force attribution for a process with `ECONOMY_ACCOUNT=tool:name` or agent-specific overrides such as `ECONOMY_CODEX_ACCOUNT=codex:work`.
|
|
108
108
|
|
|
109
|
+
Session drilldown can be scoped to an account key, account name, or email:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
economy sessions --account work@example.com
|
|
113
|
+
```
|
|
114
|
+
|
|
109
115
|
Account breakdowns report `api_equivalent_usd` for the API list-price value of the usage, plus `billable_usd`/`metered_api_usd` for known direct API spend and `subscription_included_usd` for usage covered by a subscription.
|
|
110
116
|
|
|
111
117
|
Subscription plans can be configured locally and are used by savings calculations:
|
|
@@ -180,7 +186,7 @@ Common endpoints:
|
|
|
180
186
|
|
|
181
187
|
- `GET /health`
|
|
182
188
|
- `GET /api/summary?period=today`
|
|
183
|
-
- `GET /api/sessions?agent=codex&limit=20`
|
|
189
|
+
- `GET /api/sessions?agent=codex&account=work@example.com&limit=20`
|
|
184
190
|
- `GET /api/sessions/:id/requests`
|
|
185
191
|
- `GET /api/models`
|
|
186
192
|
- `GET /api/projects?period=month`
|
|
@@ -206,11 +212,11 @@ Common endpoints:
|
|
|
206
212
|
|
|
207
213
|
Budget, goal, and subscription mutation endpoints validate agent scopes against `claude`, `takumi`, `codex`, `gemini`, `opencode`, `cursor`, `pi`, and `hermes`.
|
|
208
214
|
|
|
209
|
-
The server also serves the built dashboard when `dashboard/dist` is present.
|
|
215
|
+
The server also serves the built dashboard when `dashboard/dist` is present. The dashboard includes account-scoped session filtering, subscription plan create/update/delete controls in Savings, and savings/usage/account tables for subscription-aware cost analysis.
|
|
210
216
|
|
|
211
217
|
## Native macOS Menubar
|
|
212
218
|
|
|
213
|
-
The `menubar/` app is a native SwiftUI `MenuBarExtra` app, not Electron. It targets Swift 5.9+ and macOS 14+, and talks to the REST API exposed by `economy-serve`. It shows today/week/month spend, token and request counts, top agents, top accounts, top projects, subscription savings, usage snapshots, recent sessions, and fleet status. The default server URL is `http://127.0.0.1:3456`.
|
|
219
|
+
The `menubar/` app is a native SwiftUI `MenuBarExtra` app, not Electron. It targets Swift 5.9+ and macOS 14+, and talks to the REST API exposed by `economy-serve`. It shows today/week/month spend, token and request counts, top agents, top accounts, top projects, active subscription plans, subscription savings, multi-agent usage snapshots, recent sessions, and fleet status. The default server URL is `http://127.0.0.1:3456`.
|
|
214
220
|
|
|
215
221
|
Build it on macOS:
|
|
216
222
|
|
package/dist/cli/index.js
CHANGED
|
@@ -2642,6 +2642,25 @@ function buildThreadQuery(codexDb) {
|
|
|
2642
2642
|
FROM threads WHERE tokens_used > 0
|
|
2643
2643
|
`;
|
|
2644
2644
|
}
|
|
2645
|
+
function openCodexDb(dbPath, verbose) {
|
|
2646
|
+
let lastError;
|
|
2647
|
+
for (const readonly of [true, false]) {
|
|
2648
|
+
let codexDb = null;
|
|
2649
|
+
try {
|
|
2650
|
+
codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
|
|
2651
|
+
codexDb.prepare("PRAGMA schema_version").get();
|
|
2652
|
+
return codexDb;
|
|
2653
|
+
} catch (error) {
|
|
2654
|
+
lastError = error;
|
|
2655
|
+
codexDb?.close();
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (verbose) {
|
|
2659
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2660
|
+
console.log("Codex DB unreadable:", dbPath, message);
|
|
2661
|
+
}
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2645
2664
|
function readTokenEvents(rolloutPath) {
|
|
2646
2665
|
if (!rolloutPath || !existsSync5(rolloutPath))
|
|
2647
2666
|
return [];
|
|
@@ -2731,7 +2750,9 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2731
2750
|
let requests = 0;
|
|
2732
2751
|
const account = await resolveAccountForAgent("codex");
|
|
2733
2752
|
try {
|
|
2734
|
-
codexDb =
|
|
2753
|
+
codexDb = openCodexDb(dbPath, verbose);
|
|
2754
|
+
if (!codexDb)
|
|
2755
|
+
return { sessions: 0, requests: 0 };
|
|
2735
2756
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2736
2757
|
for (const thread of threads) {
|
|
2737
2758
|
const model = thread.model ?? readCodexModel();
|
|
@@ -6790,7 +6811,7 @@ program.command("month").description("Cost summary for this month").action(async
|
|
|
6790
6811
|
await autoSync();
|
|
6791
6812
|
printSummary("This Month", "month");
|
|
6792
6813
|
});
|
|
6793
|
-
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|takumi|codex|gemini)").option("--project <path>", "Filter by project path").option("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
|
|
6814
|
+
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|takumi|codex|gemini)").option("--project <path>", "Filter by project path").option("--account <query>", "Filter by account key, name, or email").option("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
|
|
6794
6815
|
const limit = parsePositiveCliInteger(opts.limit ?? "20", "--limit");
|
|
6795
6816
|
const agent = parseOptionalCliAgent(opts.agent);
|
|
6796
6817
|
await autoSync();
|
|
@@ -6799,6 +6820,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
6799
6820
|
let sessions = querySessions(db, {
|
|
6800
6821
|
agent,
|
|
6801
6822
|
project: opts.project,
|
|
6823
|
+
account: opts.account,
|
|
6802
6824
|
machine: opts.machine,
|
|
6803
6825
|
limit,
|
|
6804
6826
|
since: sinceDate,
|
package/dist/index.js
CHANGED
|
@@ -2087,6 +2087,25 @@ function buildThreadQuery(codexDb) {
|
|
|
2087
2087
|
FROM threads WHERE tokens_used > 0
|
|
2088
2088
|
`;
|
|
2089
2089
|
}
|
|
2090
|
+
function openCodexDb(dbPath, verbose) {
|
|
2091
|
+
let lastError;
|
|
2092
|
+
for (const readonly of [true, false]) {
|
|
2093
|
+
let codexDb = null;
|
|
2094
|
+
try {
|
|
2095
|
+
codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
|
|
2096
|
+
codexDb.prepare("PRAGMA schema_version").get();
|
|
2097
|
+
return codexDb;
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
lastError = error;
|
|
2100
|
+
codexDb?.close();
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
if (verbose) {
|
|
2104
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2105
|
+
console.log("Codex DB unreadable:", dbPath, message);
|
|
2106
|
+
}
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2090
2109
|
function readTokenEvents(rolloutPath) {
|
|
2091
2110
|
if (!rolloutPath || !existsSync4(rolloutPath))
|
|
2092
2111
|
return [];
|
|
@@ -2176,7 +2195,9 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2176
2195
|
let requests = 0;
|
|
2177
2196
|
const account = await resolveAccountForAgent("codex");
|
|
2178
2197
|
try {
|
|
2179
|
-
codexDb =
|
|
2198
|
+
codexDb = openCodexDb(dbPath, verbose);
|
|
2199
|
+
if (!codexDb)
|
|
2200
|
+
return { sessions: 0, requests: 0 };
|
|
2180
2201
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2181
2202
|
for (const thread of threads) {
|
|
2182
2203
|
const model = thread.model ?? readCodexModel();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA6C7D,iBAAS,cAAc,IAAI,MAAM,CAUhC;
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA6C7D,iBAAS,cAAc,IAAI,MAAM,CAUhC;AAwGD,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8FhH;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -2122,6 +2122,25 @@ function buildThreadQuery(codexDb) {
|
|
|
2122
2122
|
FROM threads WHERE tokens_used > 0
|
|
2123
2123
|
`;
|
|
2124
2124
|
}
|
|
2125
|
+
function openCodexDb(dbPath, verbose) {
|
|
2126
|
+
let lastError;
|
|
2127
|
+
for (const readonly of [true, false]) {
|
|
2128
|
+
let codexDb = null;
|
|
2129
|
+
try {
|
|
2130
|
+
codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
|
|
2131
|
+
codexDb.prepare("PRAGMA schema_version").get();
|
|
2132
|
+
return codexDb;
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
lastError = error;
|
|
2135
|
+
codexDb?.close();
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (verbose) {
|
|
2139
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2140
|
+
console.log("Codex DB unreadable:", dbPath, message);
|
|
2141
|
+
}
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2125
2144
|
function readTokenEvents(rolloutPath) {
|
|
2126
2145
|
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
2127
2146
|
return [];
|
|
@@ -2211,7 +2230,9 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2211
2230
|
let requests = 0;
|
|
2212
2231
|
const account = await resolveAccountForAgent("codex");
|
|
2213
2232
|
try {
|
|
2214
|
-
codexDb =
|
|
2233
|
+
codexDb = openCodexDb(dbPath, verbose);
|
|
2234
|
+
if (!codexDb)
|
|
2235
|
+
return { sessions: 0, requests: 0 };
|
|
2215
2236
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2216
2237
|
for (const thread of threads) {
|
|
2217
2238
|
const model = thread.model ?? readCodexModel();
|
|
@@ -3450,7 +3471,7 @@ var TOOL_NAMES = [
|
|
|
3450
3471
|
];
|
|
3451
3472
|
var TOOL_DESCRIPTIONS = {
|
|
3452
3473
|
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
3453
|
-
get_sessions: `agent(${AGENTS.join("|")}), project(partial), machine?(hostname), limit(20) -> compact session table`,
|
|
3474
|
+
get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
|
|
3454
3475
|
get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
|
|
3455
3476
|
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
3456
3477
|
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
@@ -3524,15 +3545,17 @@ server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, to
|
|
|
3524
3545
|
].join(`
|
|
3525
3546
|
`));
|
|
3526
3547
|
});
|
|
3527
|
-
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
|
|
3548
|
+
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, account, machine, limit(20)", {
|
|
3528
3549
|
agent: z.enum(AGENTS).optional(),
|
|
3529
3550
|
project: z.string().optional(),
|
|
3551
|
+
account: z.string().optional(),
|
|
3530
3552
|
machine: z.string().optional(),
|
|
3531
3553
|
limit: z.number().int().positive().max(100).optional()
|
|
3532
|
-
}, async ({ agent, project, machine, limit }) => {
|
|
3554
|
+
}, async ({ agent, project, account, machine, limit }) => {
|
|
3533
3555
|
const sessions = querySessions(db, {
|
|
3534
3556
|
agent,
|
|
3535
3557
|
project,
|
|
3558
|
+
account,
|
|
3536
3559
|
machine,
|
|
3537
3560
|
limit: limit ?? 20
|
|
3538
3561
|
});
|
package/dist/server/index.js
CHANGED
|
@@ -2549,6 +2549,25 @@ function buildThreadQuery(codexDb) {
|
|
|
2549
2549
|
FROM threads WHERE tokens_used > 0
|
|
2550
2550
|
`;
|
|
2551
2551
|
}
|
|
2552
|
+
function openCodexDb(dbPath, verbose) {
|
|
2553
|
+
let lastError;
|
|
2554
|
+
for (const readonly of [true, false]) {
|
|
2555
|
+
let codexDb = null;
|
|
2556
|
+
try {
|
|
2557
|
+
codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
|
|
2558
|
+
codexDb.prepare("PRAGMA schema_version").get();
|
|
2559
|
+
return codexDb;
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
lastError = error;
|
|
2562
|
+
codexDb?.close();
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
if (verbose) {
|
|
2566
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2567
|
+
console.log("Codex DB unreadable:", dbPath, message);
|
|
2568
|
+
}
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2552
2571
|
function readTokenEvents(rolloutPath) {
|
|
2553
2572
|
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
2554
2573
|
return [];
|
|
@@ -2638,7 +2657,9 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2638
2657
|
let requests = 0;
|
|
2639
2658
|
const account = await resolveAccountForAgent("codex");
|
|
2640
2659
|
try {
|
|
2641
|
-
codexDb =
|
|
2660
|
+
codexDb = openCodexDb(dbPath, verbose);
|
|
2661
|
+
if (!codexDb)
|
|
2662
|
+
return { sessions: 0, requests: 0 };
|
|
2642
2663
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2643
2664
|
for (const thread of threads) {
|
|
2644
2665
|
const model = thread.model ?? readCodexModel();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
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",
|