@hasna/economy 0.5.9 → 0.5.11
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 +12 -0
- package/README.md +22 -0
- package/dist/chunks/{index-twkdyrw4.js → index-akxt4n8x.js} +1 -1
- package/dist/chunks/{index-spwpgdbb.js → index-b36bq51x.js} +1 -1
- package/dist/chunks/{index-970htsxe.js → index-mgnzgf1m.js} +1 -1
- package/dist/chunks/{index-92x56q4h.js → index-v35tm3xg.js} +1 -1
- package/dist/chunks/index-yv28hyk7.js +1064 -0
- package/dist/chunks/{menubar-6xtkt17h.js → menubar-zgazgppv.js} +1 -1
- package/dist/chunks/{serve-tmv8zgvh.js → serve-kk3n0p7j.js} +2 -2
- package/dist/chunks/{tui-j8brj3wf.js → tui-hxjczmv5.js} +3 -3
- package/dist/chunks/watch-ewed2srf.js +307 -0
- package/dist/cli/commands/watch.d.ts +2 -0
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +42 -1035
- package/dist/index.js +50 -9
- package/dist/lib/ingest-journal.d.ts.map +1 -1
- package/dist/lib/watch-collector.d.ts +11 -0
- package/dist/lib/watch-collector.d.ts.map +1 -0
- package/dist/lib/watch-paths.d.ts +2 -1
- package/dist/lib/watch-paths.d.ts.map +1 -1
- package/dist/mcp/index.js +53 -12
- package/dist/otel/index.js +53 -12
- package/dist/server/index.js +1 -1
- package/dist/server/service-worker.js +1 -1
- package/docs/cli.md +1 -1
- package/package.json +1 -1
- package/dist/chunks/watch-ksn2mezm.js +0 -196
package/dist/index.js
CHANGED
|
@@ -111,7 +111,7 @@ import { randomUUID } from "crypto";
|
|
|
111
111
|
// package.json
|
|
112
112
|
var package_default = {
|
|
113
113
|
name: "@hasna/economy",
|
|
114
|
-
version: "0.5.
|
|
114
|
+
version: "0.5.11",
|
|
115
115
|
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",
|
|
116
116
|
repository: {
|
|
117
117
|
type: "git",
|
|
@@ -1760,6 +1760,7 @@ import { z as z2 } from "zod";
|
|
|
1760
1760
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
1761
1761
|
import { createHash } from "crypto";
|
|
1762
1762
|
import { join, resolve } from "path";
|
|
1763
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
1763
1764
|
var MAX_COMMANDS = 1000;
|
|
1764
1765
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
1765
1766
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -1871,15 +1872,55 @@ function sameReceipt(left, right) {
|
|
|
1871
1872
|
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
1872
1873
|
}
|
|
1873
1874
|
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
1875
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
1876
|
+
throw new RangeError("Receipt lookup requires a positive finite timeout");
|
|
1874
1877
|
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1878
|
+
const deadline = performance.now() + timeoutMs;
|
|
1879
|
+
let failures = 0, lastError;
|
|
1880
|
+
for (;; ) {
|
|
1881
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
1882
|
+
if (remaining <= 0)
|
|
1883
|
+
throw lastError ?? new Error("Receipt lookup deadline expired");
|
|
1884
|
+
let response;
|
|
1885
|
+
try {
|
|
1886
|
+
const abort = new AbortController;
|
|
1887
|
+
let timer;
|
|
1888
|
+
try {
|
|
1889
|
+
const expired = new Promise((_, reject) => {
|
|
1890
|
+
timer = setTimeout(() => {
|
|
1891
|
+
abort.abort();
|
|
1892
|
+
reject(new Error("Receipt lookup deadline expired"));
|
|
1893
|
+
}, remaining);
|
|
1894
|
+
});
|
|
1895
|
+
response = await Promise.race([
|
|
1896
|
+
cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs: remaining, signal: abort.signal }),
|
|
1897
|
+
expired
|
|
1898
|
+
]);
|
|
1899
|
+
} finally {
|
|
1900
|
+
clearTimeout(timer);
|
|
1901
|
+
}
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
if (!(error instanceof HasnaHttpError) || error.status !== 503)
|
|
1904
|
+
throw error;
|
|
1905
|
+
lastError = error;
|
|
1906
|
+
failures++;
|
|
1907
|
+
const delay = 100 * failures;
|
|
1908
|
+
if (failures >= 3 || deadline - performance.now() <= delay)
|
|
1909
|
+
throw error;
|
|
1910
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (performance.now() >= deadline)
|
|
1914
|
+
throw new Error("Receipt lookup deadline expired");
|
|
1915
|
+
const raw = unwrap(response);
|
|
1916
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
1917
|
+
throw new Error("Invalid reconciliation response");
|
|
1918
|
+
if (raw.status === "absent")
|
|
1919
|
+
return null;
|
|
1920
|
+
if (raw.status !== "committed")
|
|
1921
|
+
throw new Error("Invalid reconciliation response");
|
|
1922
|
+
return raw.receipt;
|
|
1923
|
+
}
|
|
1883
1924
|
}
|
|
1884
1925
|
|
|
1885
1926
|
class IngestJournal {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest-journal.d.ts","sourceRoot":"","sources":["../../src/lib/ingest-journal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ingest-journal.d.ts","sourceRoot":"","sources":["../../src/lib/ingest-journal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** One collector owns its timers and never overlaps durable ingestion calls. */
|
|
2
|
+
export interface WatchCollectorOptions {
|
|
3
|
+
intervalMs: number;
|
|
4
|
+
collect: () => Promise<void>;
|
|
5
|
+
onError: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createWatchCollector(options: WatchCollectorOptions): {
|
|
8
|
+
request: () => void;
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=watch-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-collector.d.ts","sourceRoot":"","sources":["../../src/lib/watch-collector.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB;mBA6B7C,IAAI;YAaR,OAAO,CAAC,IAAI,CAAC;EAW9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch-paths.d.ts","sourceRoot":"","sources":["../../src/lib/watch-paths.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"watch-paths.d.ts","sourceRoot":"","sources":["../../src/lib/watch-paths.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAExC,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,CASrD"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -86,7 +86,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
86
86
|
// package.json
|
|
87
87
|
var package_default = {
|
|
88
88
|
name: "@hasna/economy",
|
|
89
|
-
version: "0.5.
|
|
89
|
+
version: "0.5.11",
|
|
90
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",
|
|
91
91
|
repository: {
|
|
92
92
|
type: "git",
|
|
@@ -758,6 +758,7 @@ import { z as z3 } from "zod";
|
|
|
758
758
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
759
759
|
import { createHash } from "crypto";
|
|
760
760
|
import { join as join2, resolve } from "path";
|
|
761
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
761
762
|
var MAX_COMMANDS = 1000;
|
|
762
763
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
763
764
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -869,15 +870,55 @@ function sameReceipt(left, right) {
|
|
|
869
870
|
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
870
871
|
}
|
|
871
872
|
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
873
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
874
|
+
throw new RangeError("Receipt lookup requires a positive finite timeout");
|
|
872
875
|
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
876
|
+
const deadline = performance.now() + timeoutMs;
|
|
877
|
+
let failures = 0, lastError;
|
|
878
|
+
for (;; ) {
|
|
879
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
880
|
+
if (remaining <= 0)
|
|
881
|
+
throw lastError ?? new Error("Receipt lookup deadline expired");
|
|
882
|
+
let response;
|
|
883
|
+
try {
|
|
884
|
+
const abort = new AbortController;
|
|
885
|
+
let timer;
|
|
886
|
+
try {
|
|
887
|
+
const expired = new Promise((_, reject) => {
|
|
888
|
+
timer = setTimeout(() => {
|
|
889
|
+
abort.abort();
|
|
890
|
+
reject(new Error("Receipt lookup deadline expired"));
|
|
891
|
+
}, remaining);
|
|
892
|
+
});
|
|
893
|
+
response = await Promise.race([
|
|
894
|
+
cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs: remaining, signal: abort.signal }),
|
|
895
|
+
expired
|
|
896
|
+
]);
|
|
897
|
+
} finally {
|
|
898
|
+
clearTimeout(timer);
|
|
899
|
+
}
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (!(error instanceof HasnaHttpError) || error.status !== 503)
|
|
902
|
+
throw error;
|
|
903
|
+
lastError = error;
|
|
904
|
+
failures++;
|
|
905
|
+
const delay = 100 * failures;
|
|
906
|
+
if (failures >= 3 || deadline - performance.now() <= delay)
|
|
907
|
+
throw error;
|
|
908
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (performance.now() >= deadline)
|
|
912
|
+
throw new Error("Receipt lookup deadline expired");
|
|
913
|
+
const raw = unwrap(response);
|
|
914
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
915
|
+
throw new Error("Invalid reconciliation response");
|
|
916
|
+
if (raw.status === "absent")
|
|
917
|
+
return null;
|
|
918
|
+
if (raw.status !== "committed")
|
|
919
|
+
throw new Error("Invalid reconciliation response");
|
|
920
|
+
return raw.receipt;
|
|
921
|
+
}
|
|
881
922
|
}
|
|
882
923
|
|
|
883
924
|
class IngestJournal {
|
|
@@ -3784,7 +3825,7 @@ class IngestBatchFailure extends Error {
|
|
|
3784
3825
|
}
|
|
3785
3826
|
|
|
3786
3827
|
// src/lib/cloud-ingest.ts
|
|
3787
|
-
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
3828
|
+
import { HasnaHttpError as HasnaHttpError2 } from "@hasna/contracts/client";
|
|
3788
3829
|
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
|
|
3789
3830
|
function getIngestCachePath() {
|
|
3790
3831
|
if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
|
|
@@ -3867,7 +3908,7 @@ async function postIngest(cloud, body) {
|
|
|
3867
3908
|
try {
|
|
3868
3909
|
response = await cloud.client.transport.post("/ingest", batch, { retry: false });
|
|
3869
3910
|
} catch (error) {
|
|
3870
|
-
const refusal = error instanceof
|
|
3911
|
+
const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
|
|
3871
3912
|
if (refusal) {
|
|
3872
3913
|
journal.recordRefusal(batch, refusal);
|
|
3873
3914
|
throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
|
|
@@ -3905,7 +3946,7 @@ async function postIngest(cloud, body) {
|
|
|
3905
3946
|
} catch (error) {
|
|
3906
3947
|
if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
|
|
3907
3948
|
throw error;
|
|
3908
|
-
if (error instanceof
|
|
3949
|
+
if (error instanceof HasnaHttpError2 && error.status >= 400 && error.status < 500 && error.status !== 408) {
|
|
3909
3950
|
const body2 = error.body;
|
|
3910
3951
|
throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
|
|
3911
3952
|
}
|
package/dist/otel/index.js
CHANGED
|
@@ -2696,6 +2696,7 @@ init_database();
|
|
|
2696
2696
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync as mkdirSync3, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
2697
2697
|
import { createHash as createHash4 } from "crypto";
|
|
2698
2698
|
import { join as join8, resolve as resolve2 } from "path";
|
|
2699
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
2699
2700
|
var MAX_COMMANDS = 1000;
|
|
2700
2701
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
2701
2702
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -2807,15 +2808,55 @@ function sameReceipt(left, right) {
|
|
|
2807
2808
|
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
2808
2809
|
}
|
|
2809
2810
|
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
2811
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
2812
|
+
throw new RangeError("Receipt lookup requires a positive finite timeout");
|
|
2810
2813
|
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2814
|
+
const deadline = performance.now() + timeoutMs;
|
|
2815
|
+
let failures = 0, lastError;
|
|
2816
|
+
for (;; ) {
|
|
2817
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
2818
|
+
if (remaining <= 0)
|
|
2819
|
+
throw lastError ?? new Error("Receipt lookup deadline expired");
|
|
2820
|
+
let response;
|
|
2821
|
+
try {
|
|
2822
|
+
const abort = new AbortController;
|
|
2823
|
+
let timer;
|
|
2824
|
+
try {
|
|
2825
|
+
const expired = new Promise((_, reject) => {
|
|
2826
|
+
timer = setTimeout(() => {
|
|
2827
|
+
abort.abort();
|
|
2828
|
+
reject(new Error("Receipt lookup deadline expired"));
|
|
2829
|
+
}, remaining);
|
|
2830
|
+
});
|
|
2831
|
+
response = await Promise.race([
|
|
2832
|
+
cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs: remaining, signal: abort.signal }),
|
|
2833
|
+
expired
|
|
2834
|
+
]);
|
|
2835
|
+
} finally {
|
|
2836
|
+
clearTimeout(timer);
|
|
2837
|
+
}
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
if (!(error instanceof HasnaHttpError) || error.status !== 503)
|
|
2840
|
+
throw error;
|
|
2841
|
+
lastError = error;
|
|
2842
|
+
failures++;
|
|
2843
|
+
const delay = 100 * failures;
|
|
2844
|
+
if (failures >= 3 || deadline - performance.now() <= delay)
|
|
2845
|
+
throw error;
|
|
2846
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
if (performance.now() >= deadline)
|
|
2850
|
+
throw new Error("Receipt lookup deadline expired");
|
|
2851
|
+
const raw = unwrap(response);
|
|
2852
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
2853
|
+
throw new Error("Invalid reconciliation response");
|
|
2854
|
+
if (raw.status === "absent")
|
|
2855
|
+
return null;
|
|
2856
|
+
if (raw.status !== "committed")
|
|
2857
|
+
throw new Error("Invalid reconciliation response");
|
|
2858
|
+
return raw.receipt;
|
|
2859
|
+
}
|
|
2819
2860
|
}
|
|
2820
2861
|
|
|
2821
2862
|
class IngestJournal {
|
|
@@ -3377,7 +3418,7 @@ var ingestJournalOperationSchema = z3.discriminatedUnion("action", [
|
|
|
3377
3418
|
]);
|
|
3378
3419
|
|
|
3379
3420
|
// src/lib/cloud-ingest.ts
|
|
3380
|
-
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
3421
|
+
import { HasnaHttpError as HasnaHttpError2 } from "@hasna/contracts/client";
|
|
3381
3422
|
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
|
|
3382
3423
|
function exportIngestRows(db) {
|
|
3383
3424
|
const body = {};
|
|
@@ -3424,7 +3465,7 @@ async function postIngest(cloud, body) {
|
|
|
3424
3465
|
try {
|
|
3425
3466
|
response = await cloud.client.transport.post("/ingest", batch, { retry: false });
|
|
3426
3467
|
} catch (error) {
|
|
3427
|
-
const refusal = error instanceof
|
|
3468
|
+
const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
|
|
3428
3469
|
if (refusal) {
|
|
3429
3470
|
journal.recordRefusal(batch, refusal);
|
|
3430
3471
|
throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
|
|
@@ -3462,7 +3503,7 @@ async function postIngest(cloud, body) {
|
|
|
3462
3503
|
} catch (error) {
|
|
3463
3504
|
if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
|
|
3464
3505
|
throw error;
|
|
3465
|
-
if (error instanceof
|
|
3506
|
+
if (error instanceof HasnaHttpError2 && error.status >= 400 && error.status < 500 && error.status !== 408) {
|
|
3466
3507
|
const body2 = error.body;
|
|
3467
3508
|
throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
|
|
3468
3509
|
}
|
|
@@ -3482,7 +3523,7 @@ async function pushIngestRows(cloud, scratch) {
|
|
|
3482
3523
|
// package.json
|
|
3483
3524
|
var package_default = {
|
|
3484
3525
|
name: "@hasna/economy",
|
|
3485
|
-
version: "0.5.
|
|
3526
|
+
version: "0.5.11",
|
|
3486
3527
|
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",
|
|
3487
3528
|
repository: {
|
|
3488
3529
|
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.11",
|
|
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",
|
|
@@ -7454,7 +7454,7 @@ import { randomUUID } from "crypto";
|
|
|
7454
7454
|
// package.json
|
|
7455
7455
|
var package_default = {
|
|
7456
7456
|
name: "@hasna/economy",
|
|
7457
|
-
version: "0.5.
|
|
7457
|
+
version: "0.5.11",
|
|
7458
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",
|
|
7459
7459
|
repository: {
|
|
7460
7460
|
type: "git",
|
package/docs/cli.md
CHANGED
|
@@ -29,7 +29,7 @@ The supported coding-agent values are `claude`, `takumi`, `codex`, `gemini`, `op
|
|
|
29
29
|
| `economy brief` | Fleet brief with `--since <24h|7d|ISO-date>`, `--machine <id|all>`, or `--json`. |
|
|
30
30
|
| `economy usage [period]` | Quota/usage snapshots with `--agent`, `--limit`, `--verbose`, or `--json`. |
|
|
31
31
|
| `economy savings [period]` | Subscription-versus-API-equivalent savings with `--agent` or `--json`. |
|
|
32
|
-
| `economy watch` |
|
|
32
|
+
| `economy watch` | Stream stored costs. Explicit `--daemon` also collects local provider files through the configured store, including the hosted API. `--sync-interval` bounds collection frequency and periodic fallback; `--interval` controls report refresh. Also accepts `--agent`, `--notify`, and `--no-notifications`. Ordinary hosted watch remains read-only. This foreground command does not install a startup service. |
|
|
33
33
|
| `economy status` | Print one-line spend, fleet, storage, top-agent, and available quota status. |
|
|
34
34
|
| `economy doctor` | Check source paths/token availability, storage mode, pricing gaps, deduplication, and billing drift. |
|
|
35
35
|
| `economy transport` | Report the resolved client transport and credential SOURCE (never the key): the `/v1` authority, the URL/key source names, and the credential tier from the `@hasna/contracts` chain; `--json` for the full report. Exits 0 even when unconfigured — the refusal is the report. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
agentPaths,
|
|
4
|
-
syncAll
|
|
5
|
-
} from "./index-970htsxe.js";
|
|
6
|
-
import {
|
|
7
|
-
getStore,
|
|
8
|
-
isCloudStore
|
|
9
|
-
} from "./index-92x56q4h.js";
|
|
10
|
-
import"./index-spwpgdbb.js";
|
|
11
|
-
import {
|
|
12
|
-
formatSummaryValue
|
|
13
|
-
} from "./index-zgvmrqyc.js";
|
|
14
|
-
import {
|
|
15
|
-
__require
|
|
16
|
-
} from "./index-p35cyhc4.js";
|
|
17
|
-
|
|
18
|
-
// src/cli/commands/watch.ts
|
|
19
|
-
import chalk from "chalk";
|
|
20
|
-
import { watch } from "fs";
|
|
21
|
-
|
|
22
|
-
// src/lib/watch-paths.ts
|
|
23
|
-
import { existsSync } from "fs";
|
|
24
|
-
function getWatchPaths() {
|
|
25
|
-
const p = agentPaths();
|
|
26
|
-
const candidates = [
|
|
27
|
-
p.claudeProjects,
|
|
28
|
-
p.takumiProjects,
|
|
29
|
-
p.codexDir,
|
|
30
|
-
p.geminiTmp,
|
|
31
|
-
p.geminiHistory,
|
|
32
|
-
p.opencodeMessages,
|
|
33
|
-
p.piSessions,
|
|
34
|
-
p.hermesDir
|
|
35
|
-
];
|
|
36
|
-
return candidates.filter((path) => existsSync(path));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// src/cli/commands/notification.ts
|
|
40
|
-
import { execFileSync } from "child_process";
|
|
41
|
-
function appleScriptString(value) {
|
|
42
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
43
|
-
}
|
|
44
|
-
function notificationScript(title, body) {
|
|
45
|
-
return `display notification ${appleScriptString(body)} with title ${appleScriptString(title)}`;
|
|
46
|
-
}
|
|
47
|
-
function sendNotification(title, body, execFile = execFileSync) {
|
|
48
|
-
try {
|
|
49
|
-
execFile("osascript", ["-e", notificationScript(title, body)], { stdio: "ignore" });
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// src/cli/commands/watch.ts
|
|
54
|
-
function fmt(usd) {
|
|
55
|
-
return chalk.green(`$${usd.toFixed(4)}`);
|
|
56
|
-
}
|
|
57
|
-
function renderHeader(today, week, mode) {
|
|
58
|
-
process.stdout.write("\x1B[H\x1B[2J");
|
|
59
|
-
console.log(chalk.bold.cyan(" economy watch") + chalk.dim(` \u2014 stored usage stream (${mode})`));
|
|
60
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
61
|
-
console.log(` Today: ${formatSummaryValue(today)} Week: ${formatSummaryValue(week)}`);
|
|
62
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
63
|
-
console.log(" UTC; selected stored records only; source coverage unverified");
|
|
64
|
-
console.log(chalk.dim(" [agent] cost model tokens project"));
|
|
65
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
66
|
-
}
|
|
67
|
-
function agentLabel(agent) {
|
|
68
|
-
if (agent === "claude")
|
|
69
|
-
return chalk.blue("[claude]");
|
|
70
|
-
if (agent === "codex")
|
|
71
|
-
return chalk.yellow("[codex] ");
|
|
72
|
-
if (agent === "gemini")
|
|
73
|
-
return chalk.green("[gemini]");
|
|
74
|
-
if (agent === "takumi")
|
|
75
|
-
return chalk.magenta("[takumi]");
|
|
76
|
-
if (agent === "opencode")
|
|
77
|
-
return chalk.cyan("[opncde]");
|
|
78
|
-
if (agent === "cursor")
|
|
79
|
-
return chalk.white("[cursor]");
|
|
80
|
-
if (agent === "pi")
|
|
81
|
-
return chalk.white("[pi ]");
|
|
82
|
-
if (agent === "hermes")
|
|
83
|
-
return chalk.white("[hermes]");
|
|
84
|
-
return chalk.gray(`[${agent.slice(0, 6).padEnd(6)}]`);
|
|
85
|
-
}
|
|
86
|
-
async function watchCosts(opts) {
|
|
87
|
-
const store = getStore();
|
|
88
|
-
const cloud = isCloudStore();
|
|
89
|
-
const db = cloud ? null : (await import("./sqlite-store-r219qrjt.js")).openDatabase();
|
|
90
|
-
let lastCheck = new Date(Date.now() - opts.interval * 1000).toISOString();
|
|
91
|
-
const lines = [];
|
|
92
|
-
const MAX_LINES = 20;
|
|
93
|
-
let sessionCumulativeCost = 0;
|
|
94
|
-
let notifyThresholdFired = 0;
|
|
95
|
-
let ingestPending = false;
|
|
96
|
-
let ingestTimer = null;
|
|
97
|
-
const daemon = opts.daemon && !cloud;
|
|
98
|
-
const mode = cloud ? "cloud" : daemon ? "daemon" : "poll";
|
|
99
|
-
const initialSummaryToday = await store.summary("today");
|
|
100
|
-
const initialSummaryWeek = await store.summary("week");
|
|
101
|
-
renderHeader(initialSummaryToday, initialSummaryWeek, mode);
|
|
102
|
-
if (daemon) {
|
|
103
|
-
const paths = getWatchPaths();
|
|
104
|
-
console.log(chalk.dim(`
|
|
105
|
-
Watching ${paths.length} paths \u2014 sync on change, refresh every ${opts.interval}s
|
|
106
|
-
`));
|
|
107
|
-
for (const p of paths) {
|
|
108
|
-
try {
|
|
109
|
-
watch(p, { recursive: true }, () => scheduleIngest());
|
|
110
|
-
} catch {
|
|
111
|
-
try {
|
|
112
|
-
watch(p, () => scheduleIngest());
|
|
113
|
-
} catch {}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} else if (cloud) {
|
|
117
|
-
console.log(chalk.dim(`
|
|
118
|
-
Streaming from cloud API every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
119
|
-
`));
|
|
120
|
-
} else {
|
|
121
|
-
console.log(chalk.dim(`
|
|
122
|
-
Polling every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
123
|
-
`));
|
|
124
|
-
}
|
|
125
|
-
function scheduleIngest() {
|
|
126
|
-
ingestPending = true;
|
|
127
|
-
if (ingestTimer)
|
|
128
|
-
return;
|
|
129
|
-
ingestTimer = setTimeout(() => {
|
|
130
|
-
ingestTimer = null;
|
|
131
|
-
poll();
|
|
132
|
-
}, 500);
|
|
133
|
-
}
|
|
134
|
-
async function poll() {
|
|
135
|
-
const now = new Date().toISOString();
|
|
136
|
-
const shouldSync = !daemon || ingestPending;
|
|
137
|
-
ingestPending = false;
|
|
138
|
-
if (shouldSync && db)
|
|
139
|
-
await syncAll(db);
|
|
140
|
-
const newRequests = await store.recentRequests(lastCheck);
|
|
141
|
-
lastCheck = now;
|
|
142
|
-
for (const req of newRequests) {
|
|
143
|
-
if (opts.agent && req.agent !== opts.agent)
|
|
144
|
-
continue;
|
|
145
|
-
const tokens = req.input_tokens + req.output_tokens;
|
|
146
|
-
const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
147
|
-
const line = ` ${agentLabel(req.agent)} ${fmt(req.cost_usd).padEnd(14)}${req.model.substring(0, 24).padEnd(26)}${tokStr.padEnd(10)}${req.session_id.substring(0, 12)}`;
|
|
148
|
-
lines.push(line);
|
|
149
|
-
if (lines.length > MAX_LINES)
|
|
150
|
-
lines.shift();
|
|
151
|
-
if (req.cost_usd > 1) {
|
|
152
|
-
sendNotification("economy: high cost", `$${req.cost_usd.toFixed(2)} on ${req.model}`);
|
|
153
|
-
}
|
|
154
|
-
if (opts.notify && opts.notify > 0) {
|
|
155
|
-
sessionCumulativeCost += req.cost_usd;
|
|
156
|
-
const crossedThresholds = Math.floor(sessionCumulativeCost / opts.notify);
|
|
157
|
-
if (crossedThresholds > notifyThresholdFired) {
|
|
158
|
-
notifyThresholdFired = crossedThresholds;
|
|
159
|
-
sendNotification("Cost Alert", `Economy: $${sessionCumulativeCost.toFixed(2)} spent this session`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const today = await store.summary("today");
|
|
164
|
-
const week = await store.summary("week");
|
|
165
|
-
renderHeader(today, week, mode);
|
|
166
|
-
for (const line of lines)
|
|
167
|
-
console.log(line);
|
|
168
|
-
if (lines.length === 0)
|
|
169
|
-
console.log(chalk.dim(" Waiting for new requests..."));
|
|
170
|
-
const suffix = daemon ? `file watch + ${opts.interval}s refresh` : cloud ? `cloud stream every ${opts.interval}s` : `polling every ${opts.interval}s`;
|
|
171
|
-
console.log(chalk.dim(`
|
|
172
|
-
Last updated: ${new Date().toLocaleTimeString()} \u2014 ${suffix} \u2014 Ctrl+C to exit`));
|
|
173
|
-
}
|
|
174
|
-
if (daemon)
|
|
175
|
-
ingestPending = true;
|
|
176
|
-
await poll();
|
|
177
|
-
const timer = setInterval(() => {
|
|
178
|
-
poll().catch((e) => {
|
|
179
|
-
console.log(chalk.dim(`
|
|
180
|
-
Poll error: ${e instanceof Error ? e.message : String(e)} \u2014 retrying next tick`));
|
|
181
|
-
});
|
|
182
|
-
}, opts.interval * 1000);
|
|
183
|
-
process.on("SIGINT", () => {
|
|
184
|
-
clearInterval(timer);
|
|
185
|
-
if (ingestTimer)
|
|
186
|
-
clearTimeout(ingestTimer);
|
|
187
|
-
console.log(chalk.dim(`
|
|
188
|
-
|
|
189
|
-
Stopped watching.`));
|
|
190
|
-
process.exit(0);
|
|
191
|
-
});
|
|
192
|
-
await new Promise(() => {});
|
|
193
|
-
}
|
|
194
|
-
export {
|
|
195
|
-
watchCosts
|
|
196
|
-
};
|