@hasna/economy 0.5.10 → 0.5.12

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.
@@ -86,7 +86,7 @@ function comparisonValueChange(left, right) {
86
86
  // package.json
87
87
  var package_default = {
88
88
  name: "@hasna/economy",
89
- version: "0.5.10",
89
+ version: "0.5.12",
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",
@@ -5,7 +5,7 @@ import {
5
5
  } from "./index-w3bvv0ns.js";
6
6
  import {
7
7
  syncAll
8
- } from "./index-t777mc83.js";
8
+ } from "./index-a5gdrfby.js";
9
9
  import {
10
10
  buildBrief,
11
11
  packageMetadata,
@@ -16,7 +16,7 @@ import {
16
16
  queryProjectDetail,
17
17
  queryRangeStats,
18
18
  querySavingsSummary
19
- } from "./index-4v5r3c22.js";
19
+ } from "./index-vavbzb1s.js";
20
20
  import {
21
21
  AGENTS,
22
22
  INGEST_SCHEMA_SQL,
@@ -6,10 +6,10 @@ import {
6
6
  printStatusLine,
7
7
  printWaybarJson,
8
8
  runTui
9
- } from "./index-w4r6xs02.js";
9
+ } from "./index-bx0wbqyr.js";
10
10
  import"./index-w3bvv0ns.js";
11
- import"./index-twn8ncdp.js";
12
- import"./index-4v5r3c22.js";
11
+ import"./index-mn8ybzv7.js";
12
+ import"./index-vavbzb1s.js";
13
13
  import"./index-zgvmrqyc.js";
14
14
  import"./index-p35cyhc4.js";
15
15
  export {
@@ -0,0 +1,307 @@
1
+ // @bun
2
+ import {
3
+ syncAllToCloud,
4
+ syncHasFailures
5
+ } from "./index-pgt6fhkb.js";
6
+ import"./index-gtjqy0kd.js";
7
+ import {
8
+ agentPaths,
9
+ syncAll
10
+ } from "./index-a5gdrfby.js";
11
+ import {
12
+ getStore,
13
+ isCloudStore
14
+ } from "./index-mn8ybzv7.js";
15
+ import {
16
+ economyCloudStorage
17
+ } from "./index-vavbzb1s.js";
18
+ import {
19
+ formatSummaryValue
20
+ } from "./index-zgvmrqyc.js";
21
+ import {
22
+ __require
23
+ } from "./index-p35cyhc4.js";
24
+
25
+ // src/cli/commands/watch.ts
26
+ import chalk from "chalk";
27
+ import { watch } from "fs";
28
+
29
+ // src/lib/watch-collector.ts
30
+ function createWatchCollector(options) {
31
+ if (!Number.isSafeInteger(options.intervalMs) || options.intervalMs < 1 || options.intervalMs > 2147483647) {
32
+ throw new Error("Collection interval must be a positive integer");
33
+ }
34
+ let stopped = false;
35
+ let requested = false;
36
+ let lastStarted = Number.NEGATIVE_INFINITY;
37
+ let running;
38
+ let scheduled;
39
+ function schedule() {
40
+ if (stopped || running || scheduled || !requested)
41
+ return;
42
+ const delay = Math.max(0, lastStarted + options.intervalMs - performance.now());
43
+ scheduled = setTimeout(() => {
44
+ scheduled = undefined;
45
+ if (stopped || !requested)
46
+ return;
47
+ requested = false;
48
+ lastStarted = performance.now();
49
+ running = Promise.resolve().then(options.collect).catch(() => {
50
+ try {
51
+ options.onError();
52
+ } catch {}
53
+ }).finally(() => {
54
+ running = undefined;
55
+ schedule();
56
+ });
57
+ }, Math.ceil(delay));
58
+ }
59
+ function request() {
60
+ if (stopped)
61
+ return;
62
+ requested = true;
63
+ schedule();
64
+ }
65
+ const fallback = setInterval(request, options.intervalMs);
66
+ request();
67
+ return {
68
+ request,
69
+ async stop() {
70
+ stopped = true;
71
+ requested = false;
72
+ clearInterval(fallback);
73
+ if (scheduled)
74
+ clearTimeout(scheduled);
75
+ scheduled = undefined;
76
+ await running;
77
+ }
78
+ };
79
+ }
80
+
81
+ // src/lib/watch-paths.ts
82
+ import { existsSync } from "fs";
83
+ function getWatchPaths(agent) {
84
+ const p = agentPaths();
85
+ const paths = {
86
+ claude: [p.claudeProjects],
87
+ takumi: [p.takumiProjects],
88
+ codex: [p.codexDir],
89
+ gemini: [p.geminiTmp, p.geminiHistory],
90
+ opencode: [p.opencodeMessages],
91
+ pi: [p.piSessions],
92
+ hermes: [p.hermesDir]
93
+ };
94
+ const candidates = agent ? paths[agent] ?? [] : Object.values(paths).flat();
95
+ return [...new Set(candidates)].filter((path) => existsSync(path));
96
+ }
97
+
98
+ // src/cli/commands/notification.ts
99
+ import { execFileSync } from "child_process";
100
+ function appleScriptString(value) {
101
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
102
+ }
103
+ function notificationScript(title, body) {
104
+ return `display notification ${appleScriptString(body)} with title ${appleScriptString(title)}`;
105
+ }
106
+ function sendNotification(title, body, execFile = execFileSync) {
107
+ try {
108
+ execFile("osascript", ["-e", notificationScript(title, body)], { stdio: "ignore" });
109
+ } catch {}
110
+ }
111
+
112
+ // src/cli/commands/watch.ts
113
+ function fmt(usd) {
114
+ return chalk.green(`$${usd.toFixed(4)}`);
115
+ }
116
+ function renderHeader(today, week, mode) {
117
+ process.stdout.write("\x1B[H\x1B[2J");
118
+ console.log(chalk.bold.cyan(" economy watch") + chalk.dim(` \u2014 stored usage stream (${mode})`));
119
+ 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"));
120
+ console.log(` Today: ${formatSummaryValue(today)} Week: ${formatSummaryValue(week)}`);
121
+ 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"));
122
+ console.log(" UTC; selected stored records only; source coverage unverified");
123
+ console.log(chalk.dim(" [agent] cost model tokens project"));
124
+ 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"));
125
+ }
126
+ function agentLabel(agent) {
127
+ if (agent === "claude")
128
+ return chalk.blue("[claude]");
129
+ if (agent === "codex")
130
+ return chalk.yellow("[codex] ");
131
+ if (agent === "gemini")
132
+ return chalk.green("[gemini]");
133
+ if (agent === "takumi")
134
+ return chalk.magenta("[takumi]");
135
+ if (agent === "opencode")
136
+ return chalk.cyan("[opncde]");
137
+ if (agent === "cursor")
138
+ return chalk.white("[cursor]");
139
+ if (agent === "pi")
140
+ return chalk.white("[pi ]");
141
+ if (agent === "hermes")
142
+ return chalk.white("[hermes]");
143
+ return chalk.gray(`[${agent.slice(0, 6).padEnd(6)}]`);
144
+ }
145
+ async function watchCosts(opts) {
146
+ const syncInterval = opts.syncInterval ?? 60;
147
+ for (const seconds of [opts.interval, syncInterval]) {
148
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds * 1000 > 2147483647) {
149
+ throw new Error("Watch intervals must be integer seconds between 1 and 2147483");
150
+ }
151
+ }
152
+ const store = getStore();
153
+ const cloud = isCloudStore();
154
+ const db = cloud ? null : (await import("./sqlite-store-r219qrjt.js")).openDatabase();
155
+ let lastCheck = new Date(Date.now() - opts.interval * 1000).toISOString();
156
+ const lines = [];
157
+ const MAX_LINES = 20;
158
+ let sessionCumulativeCost = 0;
159
+ let notifyThresholdFired = 0;
160
+ const daemon = Boolean(opts.daemon);
161
+ const selectedSource = opts.agent ? { [opts.agent]: true } : {};
162
+ const mode = cloud ? daemon ? "cloud collector" : "cloud" : daemon ? "daemon" : "poll";
163
+ let collectionMessage = "Waiting for initial collection";
164
+ let polling = false;
165
+ let stopping = false;
166
+ const watchers = [];
167
+ const initialSummaryToday = await store.summary("today");
168
+ const initialSummaryWeek = await store.summary("week");
169
+ renderHeader(initialSummaryToday, initialSummaryWeek, mode);
170
+ const collector = daemon ? createWatchCollector({
171
+ intervalMs: syncInterval * 1000,
172
+ collect: async () => {
173
+ collectionMessage = "Collecting provider usage; stored reports remain available";
174
+ const result = cloud ? await (async () => {
175
+ const connection = economyCloudStorage();
176
+ if (!connection.active)
177
+ throw new Error("HOSTED_COLLECTION_UNAVAILABLE");
178
+ return syncAllToCloud(connection, selectedSource);
179
+ })() : await syncAll(db, selectedSource);
180
+ collectionMessage = syncHasFailures(result) ? "Collection incomplete; source coverage remains unverified" : `Collection completed at ${new Date().toISOString()}; source coverage remains unverified`;
181
+ },
182
+ onError: () => {
183
+ collectionMessage = "Collection failed; recovery state retained. The next scan reconciles before collecting.";
184
+ console.error("economy: daemon collection failed; check authentication, source access and ingest journal status.");
185
+ }
186
+ }) : undefined;
187
+ if (collector) {
188
+ const paths = getWatchPaths(opts.agent);
189
+ console.log(chalk.dim(`
190
+ Watching ${paths.length} paths \u2014 collection at most once every ${syncInterval}s, refresh every ${opts.interval}s
191
+ `));
192
+ for (const p of paths) {
193
+ try {
194
+ watchers.push(watch(p, { recursive: true }, collector.request));
195
+ } catch {
196
+ try {
197
+ watchers.push(watch(p, collector.request));
198
+ } catch {}
199
+ }
200
+ }
201
+ } else if (cloud) {
202
+ console.log(chalk.dim(`
203
+ Streaming from cloud API every ${opts.interval}s \u2014 Ctrl+C to exit
204
+ `));
205
+ } else {
206
+ console.log(chalk.dim(`
207
+ Polling every ${opts.interval}s \u2014 Ctrl+C to exit
208
+ `));
209
+ }
210
+ async function poll() {
211
+ if (polling || stopping)
212
+ return;
213
+ polling = true;
214
+ try {
215
+ const now = new Date().toISOString();
216
+ if (!daemon && db)
217
+ await syncAll(db, selectedSource);
218
+ const newRequests = await store.recentRequests(lastCheck);
219
+ lastCheck = now;
220
+ for (const req of newRequests) {
221
+ if (opts.agent && req.agent !== opts.agent)
222
+ continue;
223
+ const tokens = req.input_tokens + req.output_tokens;
224
+ const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
225
+ 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)}`;
226
+ lines.push(line);
227
+ if (lines.length > MAX_LINES)
228
+ lines.shift();
229
+ if (opts.notifications !== false && req.cost_usd > 1) {
230
+ sendNotification("economy: high cost", `$${req.cost_usd.toFixed(2)} on ${req.model}`);
231
+ }
232
+ if (opts.notifications !== false && opts.notify && opts.notify > 0) {
233
+ sessionCumulativeCost += req.cost_usd;
234
+ const crossedThresholds = Math.floor(sessionCumulativeCost / opts.notify);
235
+ if (crossedThresholds > notifyThresholdFired) {
236
+ notifyThresholdFired = crossedThresholds;
237
+ sendNotification("Cost Alert", `Economy: $${sessionCumulativeCost.toFixed(2)} spent this session`);
238
+ }
239
+ }
240
+ }
241
+ const today = await store.summary("today");
242
+ const week = await store.summary("week");
243
+ renderHeader(today, week, mode);
244
+ for (const line of lines)
245
+ console.log(line);
246
+ if (lines.length === 0)
247
+ console.log(chalk.dim(" Waiting for new requests..."));
248
+ const suffix = daemon ? `collection interval ${syncInterval}s + ${opts.interval}s refresh` : cloud ? `cloud stream every ${opts.interval}s` : `polling every ${opts.interval}s`;
249
+ console.log(chalk.dim(`
250
+ Last updated: ${new Date().toLocaleTimeString()} \u2014 ${suffix} \u2014 Ctrl+C to exit`));
251
+ if (daemon)
252
+ console.log(chalk.dim(` ${collectionMessage}`));
253
+ } finally {
254
+ polling = false;
255
+ }
256
+ }
257
+ let activePoll;
258
+ const safePoll = () => {
259
+ if (stopping)
260
+ return Promise.resolve();
261
+ if (activePoll)
262
+ return activePoll;
263
+ activePoll = poll().catch(() => {
264
+ console.error("economy: stored report refresh failed; retrying next interval.");
265
+ }).finally(() => {
266
+ activePoll = undefined;
267
+ });
268
+ return activePoll;
269
+ };
270
+ let finish;
271
+ const stopped = new Promise((resolve) => {
272
+ finish = resolve;
273
+ });
274
+ const timer = setInterval(() => {
275
+ safePoll();
276
+ }, opts.interval * 1000);
277
+ async function shutdown() {
278
+ if (stopping)
279
+ return;
280
+ stopping = true;
281
+ clearInterval(timer);
282
+ for (const watcher of watchers)
283
+ watcher.close();
284
+ if (collector)
285
+ console.log(chalk.dim(`
286
+ Finishing the current collection before stopping...`));
287
+ await collector?.stop();
288
+ await activePoll;
289
+ db?.close();
290
+ console.log(chalk.dim(`
291
+
292
+ Stopped watching.`));
293
+ finish();
294
+ }
295
+ const onSignal = () => {
296
+ shutdown();
297
+ };
298
+ process.on("SIGINT", onSignal);
299
+ process.on("SIGTERM", onSignal);
300
+ await safePoll();
301
+ await stopped;
302
+ process.removeListener("SIGINT", onSignal);
303
+ process.removeListener("SIGTERM", onSignal);
304
+ }
305
+ export {
306
+ watchCosts
307
+ };
@@ -4,6 +4,8 @@ interface WatchOptions {
4
4
  agent?: Agent;
5
5
  notify?: number;
6
6
  daemon?: boolean;
7
+ syncInterval?: number;
8
+ notifications?: boolean;
7
9
  }
8
10
  export declare function watchCosts(opts: WatchOptions): Promise<void>;
9
11
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/watch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,sBAAsB,CAAA;AAG9D,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AA6BD,wBAAsB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAkHlE"}
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/watch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,sBAAsB,CAAA;AAG9D,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AA6BD,wBAAsB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoJlE"}