@hasna/economy 0.5.6 → 0.5.8
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/CHANGELOG.md +15 -0
- package/dist/chunks/{billing-22az60hy.js → billing-7twygmtj.js} +2 -2
- package/dist/chunks/{config-hsp7ndhj.js → config-mjgbekxj.js} +2 -2
- package/dist/chunks/{index-9wa1f2kv.js → index-7ek4r0ww.js} +1 -1
- package/dist/chunks/{index-qvcafabj.js → index-9za4cmq2.js} +2 -2
- package/dist/chunks/{index-zs5dba3t.js → index-a1gh3d6t.js} +5 -4
- package/dist/chunks/{index-vajzfzgv.js → index-ep1pkyr3.js} +1 -1
- package/dist/chunks/{index-zqsx461z.js → index-gf3kan31.js} +2 -2
- package/dist/chunks/{index-dk5rq1rq.js → index-gtjqy0kd.js} +1 -1
- package/dist/chunks/{index-m12xttw1.js → index-h2mcj7gv.js} +82 -7
- package/dist/chunks/{index-c7h2rxx1.js → index-t0n32g8r.js} +1 -1
- package/dist/chunks/{index-f5jv6q5q.js → index-zgvmrqyc.js} +1 -1
- package/dist/chunks/{open-projects-8qm46m3d.js → open-projects-bj561k5j.js} +1 -1
- package/dist/chunks/{pricing-k9wsjwbz.js → pricing-bkgvzpe9.js} +1 -1
- package/dist/chunks/{pricing-462f8xkm.js → pricing-bn88mhqk.js} +1 -1
- package/dist/chunks/{pricing-vxejj1qa.js → pricing-nt31hf53.js} +1 -1
- package/dist/chunks/{serve-ept6cfbe.js → serve-14cgmx02.js} +14 -8
- package/dist/chunks/{sqlite-store-ht6frztj.js → sqlite-store-8t4anw98.js} +2 -2
- package/dist/chunks/{sqlite-store-13nhr1z0.js → sqlite-store-jfy0e8b6.js} +2 -2
- package/dist/chunks/{sqlite-store-gamn65xp.js → sqlite-store-r219qrjt.js} +2 -2
- package/dist/chunks/{tui-4bh067a3.js → tui-s6tpce3h.js} +4 -4
- package/dist/chunks/{watch-227kxmra.js → watch-cv5va8x6.js} +5 -5
- package/dist/chunks/{webhooks-nw4daw9y.js → webhooks-td46p6gk.js} +2 -2
- package/dist/cli/index.js +32 -28
- package/dist/cost-imports/contract.d.ts +84 -0
- package/dist/cost-imports/contract.d.ts.map +1 -0
- package/dist/cost-imports/decimal.d.ts +6 -0
- package/dist/cost-imports/decimal.d.ts.map +1 -0
- package/dist/cost-imports/providers.d.ts +4 -0
- package/dist/cost-imports/providers.d.ts.map +1 -0
- package/dist/index.js +66 -8
- package/dist/lib/analytics.d.ts +3 -0
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/comparison-contract.d.ts +8 -0
- package/dist/lib/comparison-contract.d.ts.map +1 -0
- package/dist/lib/comparison-test-contract.d.ts +3 -0
- package/dist/lib/comparison-test-contract.d.ts.map +1 -0
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/mcp/index.js +69 -11
- package/dist/otel/index.js +1 -1
- package/dist/server/index.js +40 -6
- package/dist/server/service-operations.d.ts.map +1 -1
- package/dist/server/service-worker.js +40 -6
- package/docs/comparison-coverage.md +30 -0
- package/native/README.md +70 -2
- package/native/Tools/pair-qr.py +130 -34
- package/native/Tools/pair-qr.swift +19 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
MAX_INGEST_BYTES,
|
|
6
6
|
MAX_INGEST_ROWS,
|
|
7
7
|
assessCostFromDb,
|
|
8
|
+
buildCostSummary,
|
|
8
9
|
cacheDir,
|
|
9
10
|
calendarPeriodBounds,
|
|
10
11
|
calendarPeriodWhere,
|
|
@@ -75,7 +76,7 @@ import {
|
|
|
75
76
|
upsertUsageSnapshot,
|
|
76
77
|
usageSnapshotFilterForPeriod,
|
|
77
78
|
validateIngest
|
|
78
|
-
} from "../chunks/index-
|
|
79
|
+
} from "../chunks/index-ep1pkyr3.js";
|
|
79
80
|
import {
|
|
80
81
|
__require
|
|
81
82
|
} from "../chunks/index-p35cyhc4.js";
|
|
@@ -85,7 +86,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
85
86
|
// package.json
|
|
86
87
|
var package_default = {
|
|
87
88
|
name: "@hasna/economy",
|
|
88
|
-
version: "0.5.
|
|
89
|
+
version: "0.5.8",
|
|
89
90
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
90
91
|
repository: {
|
|
91
92
|
type: "git",
|
|
@@ -3705,7 +3706,7 @@ function backfillMachineId(db) {
|
|
|
3705
3706
|
return { requests: Number(reqCount.changes), sessions: Number(sessCount.changes) };
|
|
3706
3707
|
}
|
|
3707
3708
|
async function recalculateZeroCostRequests(db) {
|
|
3708
|
-
const { assessCostFromDb: assessCostFromDb2 } = await import("../chunks/pricing-
|
|
3709
|
+
const { assessCostFromDb: assessCostFromDb2 } = await import("../chunks/pricing-nt31hf53.js");
|
|
3709
3710
|
const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens
|
|
3710
3711
|
FROM requests
|
|
3711
3712
|
WHERE cost_usd = 0
|
|
@@ -3914,7 +3915,7 @@ async function postIngest(cloud, body) {
|
|
|
3914
3915
|
}
|
|
3915
3916
|
async function syncAllToCloud(cloud, opts = {}) {
|
|
3916
3917
|
await runIngestJournalOperation(cloud, { action: "reconcile" });
|
|
3917
|
-
const { openDatabase } = await import("../chunks/sqlite-store-
|
|
3918
|
+
const { openDatabase } = await import("../chunks/sqlite-store-jfy0e8b6.js");
|
|
3918
3919
|
const scratch = openDatabase(":memory:", true);
|
|
3919
3920
|
ensurePricingSeeded(scratch);
|
|
3920
3921
|
const cachePath = opts.cachePath ?? getIngestCachePath();
|
|
@@ -3948,6 +3949,54 @@ function syncOutcomeLines(result) {
|
|
|
3948
3949
|
return (result.collections ?? []).map((row) => `${row.source}: ${row.status}${row.error_code ? ` (${row.error_code})` : ""}`);
|
|
3949
3950
|
}
|
|
3950
3951
|
|
|
3952
|
+
// src/lib/comparison-contract.ts
|
|
3953
|
+
function comparisonDateBounds(from, to) {
|
|
3954
|
+
const valid = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value + "T00:00:00Z")) && new Date(value + "T00:00:00Z").toISOString().slice(0, 10) === value;
|
|
3955
|
+
if (!valid(from) || !valid(to) || from > to) {
|
|
3956
|
+
throw new RangeError("from and to must be real YYYY-MM-DD UTC dates with from <= to");
|
|
3957
|
+
}
|
|
3958
|
+
const end = new Date(to + "T00:00:00Z");
|
|
3959
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
3960
|
+
const exclusive = end.toISOString().slice(0, 10);
|
|
3961
|
+
if (!valid(exclusive))
|
|
3962
|
+
throw new RangeError("to exceeds the supported UTC date range");
|
|
3963
|
+
return { time_zone: "UTC", start_date: from, end_date_exclusive: exclusive };
|
|
3964
|
+
}
|
|
3965
|
+
function requireCurrentRangeStats(value, from, to) {
|
|
3966
|
+
if (!value || typeof value !== "object")
|
|
3967
|
+
throw new Error("COMPARISON_REPORT_UNAVAILABLE: Invalid response");
|
|
3968
|
+
const report = value;
|
|
3969
|
+
let money;
|
|
3970
|
+
try {
|
|
3971
|
+
money = requireCurrentCostSummary(report.money);
|
|
3972
|
+
} catch {
|
|
3973
|
+
throw new Error("COMPARISON_REPORT_UNAVAILABLE: The service must support comparison coverage");
|
|
3974
|
+
}
|
|
3975
|
+
for (const key of ["sessions", "requests", "tokens"]) {
|
|
3976
|
+
if (!Number.isSafeInteger(report[key]) || report[key] < 0)
|
|
3977
|
+
throw new Error("COMPARISON_REPORT_UNAVAILABLE: Invalid " + key);
|
|
3978
|
+
}
|
|
3979
|
+
if (report.cost !== money.total_usd || report.requests !== money.request_rows || report.tokens !== money.tokens || money.session_only_sessions !== 0 || money.session_only_value_usd !== 0 || money.requests !== money.request_rows) {
|
|
3980
|
+
throw new Error("COMPARISON_REPORT_UNAVAILABLE: Inconsistent monetary coverage");
|
|
3981
|
+
}
|
|
3982
|
+
const knownRows = money.request_rows - money.unknown_price_requests;
|
|
3983
|
+
const completeness = money.request_rows === 0 ? "no_records" : money.unknown_price_requests === 0 ? "complete" : knownRows > 0 ? "partial" : "unavailable";
|
|
3984
|
+
if (money.completeness !== completeness || knownRows === 0 && money.total_usd !== 0 || money.sessions > money.request_rows || money.request_rows === 0 && money.tokens !== 0) {
|
|
3985
|
+
throw new Error("COMPARISON_REPORT_UNAVAILABLE: Inconsistent request coverage");
|
|
3986
|
+
}
|
|
3987
|
+
const bounds = money.period_bounds;
|
|
3988
|
+
if (!bounds.start_date || !bounds.end_date_exclusive || bounds.start_date >= bounds.end_date_exclusive) {
|
|
3989
|
+
throw new Error("COMPARISON_REPORT_UNAVAILABLE: Missing comparison dates");
|
|
3990
|
+
}
|
|
3991
|
+
if (from !== undefined || to !== undefined) {
|
|
3992
|
+
const expected = comparisonDateBounds(from, to);
|
|
3993
|
+
if (bounds.start_date !== expected.start_date || bounds.end_date_exclusive !== expected.end_date_exclusive) {
|
|
3994
|
+
throw new Error("COMPARISON_REPORT_UNAVAILABLE: Response dates do not match the requested range");
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
return report;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
3951
4000
|
// src/lib/store/index.ts
|
|
3952
4001
|
import { randomUUID } from "crypto";
|
|
3953
4002
|
|
|
@@ -4888,11 +4937,20 @@ function queryExportRows(db, type, period) {
|
|
|
4888
4937
|
return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
4889
4938
|
}
|
|
4890
4939
|
function queryRangeStats(db, from, to) {
|
|
4891
|
-
const
|
|
4892
|
-
|
|
4893
|
-
|
|
4940
|
+
const bounds = comparisonDateBounds(from, to);
|
|
4941
|
+
const groups = db.prepare(`
|
|
4942
|
+
SELECT cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
|
|
4943
|
+
SUM(cost_usd) AS cost_usd, COUNT(*) AS requests,
|
|
4944
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
4945
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
|
|
4946
|
+
COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
|
|
4947
|
+
FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?
|
|
4948
|
+
GROUP BY cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
|
|
4949
|
+
`).all(from, to);
|
|
4950
|
+
const requestSessions = db.prepare("SELECT COUNT(DISTINCT session_id) AS sessions FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?").get(from, to);
|
|
4951
|
+
const money = buildCostSummary("all", bounds, groups, { sessions: 0, requests: 0, tokens: 0, cost_usd: 0 }, requestSessions.sessions, listModelPricing(db));
|
|
4894
4952
|
const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
4895
|
-
return { cost:
|
|
4953
|
+
return { cost: money.total_usd, money, requests: money.request_rows, tokens: money.tokens, sessions: s.sessions };
|
|
4896
4954
|
}
|
|
4897
4955
|
function queryForecast(db, now2 = new Date) {
|
|
4898
4956
|
const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now2);
|
|
@@ -4956,7 +5014,7 @@ class LocalStore2 {
|
|
|
4956
5014
|
_db;
|
|
4957
5015
|
async db() {
|
|
4958
5016
|
if (!this._db) {
|
|
4959
|
-
const { openDatabase } = await import("../chunks/sqlite-store-
|
|
5017
|
+
const { openDatabase } = await import("../chunks/sqlite-store-jfy0e8b6.js");
|
|
4960
5018
|
this._db = openDatabase();
|
|
4961
5019
|
ensurePricingSeeded(this._db);
|
|
4962
5020
|
}
|
|
@@ -5233,7 +5291,7 @@ class ApiStore2 {
|
|
|
5233
5291
|
return cloudListItems(this.cloud, "export", q({ type, period }));
|
|
5234
5292
|
}
|
|
5235
5293
|
async rangeStats(from, to) {
|
|
5236
|
-
return cloudObject(this.cloud, "/compare", q({ from, to }));
|
|
5294
|
+
return requireCurrentRangeStats(await cloudObject(this.cloud, "/compare", q({ from, to })), from, to);
|
|
5237
5295
|
}
|
|
5238
5296
|
async forecast() {
|
|
5239
5297
|
return cloudObject(this.cloud, "/forecast");
|
|
@@ -5319,7 +5377,7 @@ function buildServer() {
|
|
|
5319
5377
|
let _db;
|
|
5320
5378
|
const localDb = async () => {
|
|
5321
5379
|
if (!_db) {
|
|
5322
|
-
const { openDatabase } = await import("../chunks/sqlite-store-
|
|
5380
|
+
const { openDatabase } = await import("../chunks/sqlite-store-jfy0e8b6.js");
|
|
5323
5381
|
_db = openDatabase();
|
|
5324
5382
|
ensurePricingSeeded(_db);
|
|
5325
5383
|
}
|
package/dist/otel/index.js
CHANGED
|
@@ -3480,7 +3480,7 @@ async function pushIngestRows(cloud, scratch) {
|
|
|
3480
3480
|
// package.json
|
|
3481
3481
|
var package_default = {
|
|
3482
3482
|
name: "@hasna/economy",
|
|
3483
|
-
version: "0.5.
|
|
3483
|
+
version: "0.5.8",
|
|
3484
3484
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
3485
3485
|
repository: {
|
|
3486
3486
|
type: "git",
|
package/dist/server/index.js
CHANGED
|
@@ -5578,7 +5578,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5578
5578
|
// package.json
|
|
5579
5579
|
var package_default = {
|
|
5580
5580
|
name: "@hasna/economy",
|
|
5581
|
-
version: "0.5.
|
|
5581
|
+
version: "0.5.8",
|
|
5582
5582
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5583
5583
|
repository: {
|
|
5584
5584
|
type: "git",
|
|
@@ -11839,6 +11839,25 @@ function buildBrief(db, opts = {}) {
|
|
|
11839
11839
|
};
|
|
11840
11840
|
}
|
|
11841
11841
|
|
|
11842
|
+
// src/lib/analytics.ts
|
|
11843
|
+
init_database();
|
|
11844
|
+
init_summary_contract();
|
|
11845
|
+
|
|
11846
|
+
// src/lib/comparison-contract.ts
|
|
11847
|
+
init_summary_contract();
|
|
11848
|
+
function comparisonDateBounds(from, to) {
|
|
11849
|
+
const valid = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value + "T00:00:00Z")) && new Date(value + "T00:00:00Z").toISOString().slice(0, 10) === value;
|
|
11850
|
+
if (!valid(from) || !valid(to) || from > to) {
|
|
11851
|
+
throw new RangeError("from and to must be real YYYY-MM-DD UTC dates with from <= to");
|
|
11852
|
+
}
|
|
11853
|
+
const end = new Date(to + "T00:00:00Z");
|
|
11854
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
11855
|
+
const exclusive = end.toISOString().slice(0, 10);
|
|
11856
|
+
if (!valid(exclusive))
|
|
11857
|
+
throw new RangeError("to exceeds the supported UTC date range");
|
|
11858
|
+
return { time_zone: "UTC", start_date: from, end_date_exclusive: exclusive };
|
|
11859
|
+
}
|
|
11860
|
+
|
|
11842
11861
|
// src/lib/analytics.ts
|
|
11843
11862
|
function queryProjectDetail(db, nameOrPath, now = new Date) {
|
|
11844
11863
|
const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
|
|
@@ -11888,11 +11907,20 @@ function queryExportRows(db, type, period) {
|
|
|
11888
11907
|
return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
11889
11908
|
}
|
|
11890
11909
|
function queryRangeStats(db, from, to) {
|
|
11891
|
-
const
|
|
11892
|
-
|
|
11893
|
-
|
|
11910
|
+
const bounds = comparisonDateBounds(from, to);
|
|
11911
|
+
const groups = db.prepare(`
|
|
11912
|
+
SELECT cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
|
|
11913
|
+
SUM(cost_usd) AS cost_usd, COUNT(*) AS requests,
|
|
11914
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
11915
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
|
|
11916
|
+
COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
|
|
11917
|
+
FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?
|
|
11918
|
+
GROUP BY cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
|
|
11919
|
+
`).all(from, to);
|
|
11920
|
+
const requestSessions = db.prepare("SELECT COUNT(DISTINCT session_id) AS sessions FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?").get(from, to);
|
|
11921
|
+
const money = buildCostSummary("all", bounds, groups, { sessions: 0, requests: 0, tokens: 0, cost_usd: 0 }, requestSessions.sessions, listModelPricing(db));
|
|
11894
11922
|
const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
11895
|
-
return { cost:
|
|
11923
|
+
return { cost: money.total_usd, money, requests: money.request_rows, tokens: money.tokens, sessions: s.sessions };
|
|
11896
11924
|
}
|
|
11897
11925
|
function queryForecast(db, now = new Date) {
|
|
11898
11926
|
const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
|
|
@@ -12394,7 +12422,13 @@ function createDataHandler(db, actorId) {
|
|
|
12394
12422
|
const to = url.searchParams.get("to")?.trim();
|
|
12395
12423
|
if (!from || !to)
|
|
12396
12424
|
return err("from and to are required (YYYY-MM-DD)");
|
|
12397
|
-
|
|
12425
|
+
try {
|
|
12426
|
+
return ok(queryRangeStats(db, from, to));
|
|
12427
|
+
} catch (error) {
|
|
12428
|
+
if (error instanceof RangeError)
|
|
12429
|
+
return err(error.message);
|
|
12430
|
+
throw error;
|
|
12431
|
+
}
|
|
12398
12432
|
}
|
|
12399
12433
|
if (path === "/api/forecast" && method === "GET") {
|
|
12400
12434
|
return ok(queryForecast(db));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-operations.d.ts","sourceRoot":"","sources":["../../src/server/service-operations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAuCxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"service-operations.d.ts","sourceRoot":"","sources":["../../src/server/service-operations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAuCxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAokB5E,gHAAgH;AAChH,wBAAsB,uBAAuB,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAe/G"}
|
|
@@ -7264,6 +7264,25 @@ function buildBrief(db, opts = {}) {
|
|
|
7264
7264
|
};
|
|
7265
7265
|
}
|
|
7266
7266
|
|
|
7267
|
+
// src/lib/analytics.ts
|
|
7268
|
+
init_database();
|
|
7269
|
+
init_summary_contract();
|
|
7270
|
+
|
|
7271
|
+
// src/lib/comparison-contract.ts
|
|
7272
|
+
init_summary_contract();
|
|
7273
|
+
function comparisonDateBounds(from, to) {
|
|
7274
|
+
const valid = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value + "T00:00:00Z")) && new Date(value + "T00:00:00Z").toISOString().slice(0, 10) === value;
|
|
7275
|
+
if (!valid(from) || !valid(to) || from > to) {
|
|
7276
|
+
throw new RangeError("from and to must be real YYYY-MM-DD UTC dates with from <= to");
|
|
7277
|
+
}
|
|
7278
|
+
const end = new Date(to + "T00:00:00Z");
|
|
7279
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
7280
|
+
const exclusive = end.toISOString().slice(0, 10);
|
|
7281
|
+
if (!valid(exclusive))
|
|
7282
|
+
throw new RangeError("to exceeds the supported UTC date range");
|
|
7283
|
+
return { time_zone: "UTC", start_date: from, end_date_exclusive: exclusive };
|
|
7284
|
+
}
|
|
7285
|
+
|
|
7267
7286
|
// src/lib/analytics.ts
|
|
7268
7287
|
function queryProjectDetail(db, nameOrPath, now = new Date) {
|
|
7269
7288
|
const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
|
|
@@ -7313,11 +7332,20 @@ function queryExportRows(db, type, period) {
|
|
|
7313
7332
|
return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
7314
7333
|
}
|
|
7315
7334
|
function queryRangeStats(db, from, to) {
|
|
7316
|
-
const
|
|
7317
|
-
|
|
7318
|
-
|
|
7335
|
+
const bounds = comparisonDateBounds(from, to);
|
|
7336
|
+
const groups = db.prepare(`
|
|
7337
|
+
SELECT cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
|
|
7338
|
+
SUM(cost_usd) AS cost_usd, COUNT(*) AS requests,
|
|
7339
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
7340
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
|
|
7341
|
+
COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
|
|
7342
|
+
FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?
|
|
7343
|
+
GROUP BY cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
|
|
7344
|
+
`).all(from, to);
|
|
7345
|
+
const requestSessions = db.prepare("SELECT COUNT(DISTINCT session_id) AS sessions FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?").get(from, to);
|
|
7346
|
+
const money = buildCostSummary("all", bounds, groups, { sessions: 0, requests: 0, tokens: 0, cost_usd: 0 }, requestSessions.sessions, listModelPricing(db));
|
|
7319
7347
|
const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
7320
|
-
return { cost:
|
|
7348
|
+
return { cost: money.total_usd, money, requests: money.request_rows, tokens: money.tokens, sessions: s.sessions };
|
|
7321
7349
|
}
|
|
7322
7350
|
function queryForecast(db, now = new Date) {
|
|
7323
7351
|
const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
|
|
@@ -7426,7 +7454,7 @@ import { randomUUID } from "crypto";
|
|
|
7426
7454
|
// package.json
|
|
7427
7455
|
var package_default = {
|
|
7428
7456
|
name: "@hasna/economy",
|
|
7429
|
-
version: "0.5.
|
|
7457
|
+
version: "0.5.8",
|
|
7430
7458
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
7431
7459
|
repository: {
|
|
7432
7460
|
type: "git",
|
|
@@ -7945,7 +7973,13 @@ function createDataHandler(db, actorId) {
|
|
|
7945
7973
|
const to = url.searchParams.get("to")?.trim();
|
|
7946
7974
|
if (!from || !to)
|
|
7947
7975
|
return err("from and to are required (YYYY-MM-DD)");
|
|
7948
|
-
|
|
7976
|
+
try {
|
|
7977
|
+
return ok(queryRangeStats(db, from, to));
|
|
7978
|
+
} catch (error) {
|
|
7979
|
+
if (error instanceof RangeError)
|
|
7980
|
+
return err(error.message);
|
|
7981
|
+
throw error;
|
|
7982
|
+
}
|
|
7949
7983
|
}
|
|
7950
7984
|
if (path === "/api/forecast" && method === "GET") {
|
|
7951
7985
|
return ok(queryForecast(db));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Historical comparison coverage
|
|
2
|
+
|
|
3
|
+
The comparison endpoint returns request/token counts and session counts as before.
|
|
4
|
+
Its numeric `cost` field is a **known usage value subtotal**, following the same
|
|
5
|
+
contract as the summary report's `total_usd`. It is not a billed or paid amount.
|
|
6
|
+
|
|
7
|
+
`money` carries the full monetary evidence:
|
|
8
|
+
|
|
9
|
+
- `complete_total_usd` is null for incomplete or empty selected request data.
|
|
10
|
+
- Recorded metered amounts, estimated usage value and subscription-included
|
|
11
|
+
value are separate subtotals.
|
|
12
|
+
- Unknown billing basis and unavailable pricing remain explicit counts.
|
|
13
|
+
- `source_coverage: "not_established"` makes clear that completeness of the
|
|
14
|
+
selected stored records does not establish complete provider collection.
|
|
15
|
+
- UTC bounds identify the selected inclusive date range through an exclusive
|
|
16
|
+
ending date.
|
|
17
|
+
|
|
18
|
+
The CLI displays usage value as unavailable when the complete total is null,
|
|
19
|
+
alongside the known subtotal and coverage. It calculates a monetary percentage
|
|
20
|
+
only when both selected sets are complete and the baseline is nonzero.
|
|
21
|
+
Genuinely recorded free usage can be complete zero; no records cannot.
|
|
22
|
+
|
|
23
|
+
The hosted SDK refuses older responses without coverage and responses for a
|
|
24
|
+
different date range. Deploy the service support before relying on a new
|
|
25
|
+
comparison client. No stored original amount or billing classification is
|
|
26
|
+
rewritten by this read-only reporting correction.
|
|
27
|
+
|
|
28
|
+
Comparison usage values cover request rows. Lifetime-only session amounts are
|
|
29
|
+
not allocated to dates by this endpoint. Session counts retain the existing
|
|
30
|
+
started-within-range definition.
|
package/native/README.md
CHANGED
|
@@ -30,8 +30,28 @@ Never run real codes in a captured agent terminal. The helper obtains a
|
|
|
30
30
|
single-use code, hands only that temporary code to the CoreImage renderer and
|
|
31
31
|
clears the display within five minutes. `--self-test` decodes a synthetic QR;
|
|
32
32
|
`test_pair_qr.py` proves transcript/agent-key refusal and credential confinement.
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
Without a saved human session, use
|
|
34
|
+
`python3 native/Tools/pair-qr.py --tenant <workspace-id> --login-email <human-email>`.
|
|
35
|
+
The helper opens the owner's Terminal, asks for confirmation before requesting
|
|
36
|
+
one sign-in email, and reads the verification code there with echo disabled.
|
|
37
|
+
It verifies a human principal and the selected workspace before displaying the
|
|
38
|
+
one-use QR. The new setup session stays in memory. On renderer exit or catchable
|
|
39
|
+
Terminal shutdown (HUP, TERM or Ctrl-C), setup makes one timeout-limited logout
|
|
40
|
+
attempt and reports an unconfirmed result as a failure. Force-kill, a reboot,
|
|
41
|
+
network failure or an interrupted verification response can prevent confirmation;
|
|
42
|
+
check the account's sessions before retrying in those cases. An existing session
|
|
43
|
+
supplied with `--session-ref` is retained.
|
|
44
|
+
Only the temporary QR code reaches the renderer, and no response or verification
|
|
45
|
+
code is returned to the agent terminal.
|
|
46
|
+
|
|
47
|
+
For a first account, `--create-account` offers signup through the existing
|
|
48
|
+
Identities flow instead of login. It requires direct confirmation in the owner
|
|
49
|
+
Terminal before account creation or email delivery. It does not create an
|
|
50
|
+
organization, grant an owner role, alter memberships or configure Economy's
|
|
51
|
+
store binding. An existing-account refusal is not retried as login, and failed
|
|
52
|
+
delivery/verification is not retried automatically. Never type the owner's code
|
|
53
|
+
for them or copy it into an agent session. A source/fixture test does not prove
|
|
54
|
+
real email delivery, account authority or a successful phone connection.
|
|
35
55
|
|
|
36
56
|
The adapter verifies Economy's `/auth/session` subject, tenant, effective scopes
|
|
37
57
|
and integer expiry. Viewer sign-in never requests write access. Refresh and period
|
|
@@ -129,3 +149,51 @@ fixture checks. Owner-facing screen captures go through Designs.
|
|
|
129
149
|
Apple references: [MenuBarExtra](https://developer.apple.com/documentation/swiftui/menubarextra),
|
|
130
150
|
[native design adoption](https://developer.apple.com/videos/play/wwdc2025/323/),
|
|
131
151
|
[application delegate adaptor](https://developer.apple.com/documentation/swiftui/nsapplicationdelegateadaptor).
|
|
152
|
+
|
|
153
|
+
## Vault-backed Mac release signing
|
|
154
|
+
|
|
155
|
+
When the exact approved Developer ID certificate is not in the login Keychain,
|
|
156
|
+
`Tools/prepare-signing.py` prepares a fresh private release Keychain. It does not
|
|
157
|
+
unlock the login Keychain, sign the application, submit it to Apple or install it.
|
|
158
|
+
Use the supported Secrets consumer to project these inputs directly to this
|
|
159
|
+
process: `ECONOMY_SIGNING_CERTIFICATE_B64` (DER certificate),
|
|
160
|
+
`ECONOMY_SIGNING_PRIVATE_KEY` (PEM), `ECONOMY_NOTARY_PRIVATE_KEY` (PEM),
|
|
161
|
+
`ECONOMY_NOTARY_KEY_ID` and `ECONOMY_NOTARY_ISSUER`. Resolve the approved
|
|
162
|
+
references and existing Apple team before execution. Never put their values in
|
|
163
|
+
arguments, output or repository files.
|
|
164
|
+
|
|
165
|
+
Run `python3 Tools/prepare-signing.py --output FRESH_SCRATCH_PATH --certificate EXACT_SHA1`.
|
|
166
|
+
The output must be a new directory under the session Scratchpad. The helper
|
|
167
|
+
imports credentials in memory, verifies the exact certificate, preserves the
|
|
168
|
+
existing ordered search-list entries and default Keychain, and binds access to codesign. Its
|
|
169
|
+
random password enters Apple's non-echoing prompt through its own terminal,
|
|
170
|
+
never the command arguments. Apple's notarytool requires a regular key file and
|
|
171
|
+
refuses stdin and FIFOs. The consumer writes that one input exclusively at mode
|
|
172
|
+
0600 inside its fresh mode 0700 directory and passes only its path. It stores the
|
|
173
|
+
profile in the encrypted Keychain; credential-consumer output is suppressed.
|
|
174
|
+
The receipt explicitly names the sensitive input awaiting recoverable cleanup.
|
|
175
|
+
If macOS registers the new Keychain, that owned addition is retained and reported
|
|
176
|
+
in the receipt. The helper never replaces the shared search list; concurrent
|
|
177
|
+
additions are retained. An explicit Keychain path also works without registration.
|
|
178
|
+
|
|
179
|
+
Pass the resulting absolute Keychain path with `--keychain` to the existing
|
|
180
|
+
`release.py sign-mac`, `submit-mac` and `check-mac` stages. Their separate
|
|
181
|
+
signature, exact artifact, submission and acceptance checks still apply.
|
|
182
|
+
Omitting the option retains the existing login-Keychain route. Preparation is
|
|
183
|
+
not signing or notarization proof.
|
|
184
|
+
|
|
185
|
+
Keep the preparation receipt and any failed artifact. Do not replay an uncertain
|
|
186
|
+
submission. On failure, timeout, Ctrl-C, HUP or TERM, preparation settles its own
|
|
187
|
+
child, locks the owned Keychain and inspects the original entries and default.
|
|
188
|
+
An incomplete lock or inspection is explicitly recovery-unverified. SIGKILL,
|
|
189
|
+
power loss and a killed parent cannot guarantee cleanup; inspect the retained
|
|
190
|
+
artifact before retrying. After confirmed profile storage, preserve/read back
|
|
191
|
+
the private input and remove that exact owned file through Trash; report pending
|
|
192
|
+
cleanup on failure and retain its restricted permissions. Never treat preparation
|
|
193
|
+
as complete release acceptance while sensitive cleanup remains. After the release,
|
|
194
|
+
lock the exact release Keychain,
|
|
195
|
+
preserve and read back its encrypted bytes through Trash, then retire only its
|
|
196
|
+
owned registration through the supported Keychain operation. Verify other
|
|
197
|
+
entries and the default remain; never replace the complete list during cleanup.
|
|
198
|
+
The Keychain has a 15-minute idle lock; an expired lock requires a fresh
|
|
199
|
+
preparation, not a password copied into a command or a login-Keychain change.
|