@hasna/economy 0.3.3 → 0.3.5

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":"watch.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/watch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAGjD,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;AA4BD,wBAAsB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0GlE"}
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;AAGjD,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;AA4BD,wBAAsB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAgHlE"}
package/dist/cli/index.js CHANGED
@@ -6330,7 +6330,12 @@ async function watchCosts(opts) {
6330
6330
  if (daemon)
6331
6331
  ingestPending = true;
6332
6332
  await poll();
6333
- const timer = setInterval(poll, opts.interval * 1000);
6333
+ const timer = setInterval(() => {
6334
+ poll().catch((e) => {
6335
+ console.log(chalk5.dim(`
6336
+ Poll error: ${e instanceof Error ? e.message : String(e)} \u2014 retrying next tick`));
6337
+ });
6338
+ }, opts.interval * 1000);
6334
6339
  process.on("SIGINT", () => {
6335
6340
  clearInterval(timer);
6336
6341
  if (ingestTimer)
@@ -6389,11 +6394,16 @@ function resolveWorkerPath() {
6389
6394
  return candidate;
6390
6395
  return candidates[0];
6391
6396
  }
6397
+ function isTransientConnError(message) {
6398
+ const m = message.toLowerCase();
6399
+ return m.includes("econnreset") || m.includes("econnrefused") || m.includes("etimedout") || m.includes("connection terminated") || m.includes("connection ended") || m.includes("timeout exceeded when trying to connect") || m.includes("terminating connection") || m.includes("server closed the connection");
6400
+ }
6392
6401
 
6393
6402
  class SyncPgAdapter {
6394
6403
  worker;
6395
6404
  port;
6396
6405
  counter = 0;
6406
+ sleeper = new Int32Array(new SharedArrayBuffer(4));
6397
6407
  constructor(dsn) {
6398
6408
  const workerPath = resolveWorkerPath();
6399
6409
  this.worker = new Worker(workerPath, { workerData: { dsn } });
@@ -6402,25 +6412,45 @@ class SyncPgAdapter {
6402
6412
  this.port = channel.port1;
6403
6413
  this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
6404
6414
  }
6405
- query(sql, params) {
6406
- const pgSql = translateSqliteDates(translateSql(sql, "pg"));
6407
- const pgParams = translateParams(params);
6415
+ drainReply(expectedId) {
6416
+ const deadline = Date.now() + 30000;
6417
+ for (;; ) {
6418
+ const received = receiveMessageOnPort(this.port);
6419
+ if (received) {
6420
+ const reply = received.message;
6421
+ if (reply.id === expectedId)
6422
+ return reply;
6423
+ continue;
6424
+ }
6425
+ if (Date.now() > deadline)
6426
+ throw new Error("SyncPgAdapter: no reply from worker (timeout)");
6427
+ Atomics.wait(this.sleeper, 0, 0, 1);
6428
+ }
6429
+ }
6430
+ queryOnce(pgSql, pgParams) {
6408
6431
  const id = ++this.counter;
6409
6432
  const signal = new SharedArrayBuffer(4);
6410
6433
  const sig = new Int32Array(signal);
6411
6434
  Atomics.store(sig, 0, 0);
6412
6435
  this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
6413
6436
  Atomics.wait(sig, 0, 0);
6414
- let received = receiveMessageOnPort(this.port);
6415
- for (let attempt = 0;!received && attempt < 1e6; attempt++)
6416
- received = receiveMessageOnPort(this.port);
6417
- if (!received)
6418
- throw new Error("SyncPgAdapter: no reply from worker");
6419
- const reply = received.message;
6437
+ const reply = this.drainReply(id);
6420
6438
  if (!reply.ok)
6421
6439
  throw new Error(reply.error);
6422
6440
  return { rows: reply.rows, rowCount: reply.rowCount };
6423
6441
  }
6442
+ query(sql, params) {
6443
+ const pgSql = translateSqliteDates(translateSql(sql, "pg"));
6444
+ const pgParams = translateParams(params);
6445
+ try {
6446
+ return this.queryOnce(pgSql, pgParams);
6447
+ } catch (err) {
6448
+ if (err instanceof Error && isTransientConnError(err.message)) {
6449
+ return this.queryOnce(pgSql, pgParams);
6450
+ }
6451
+ throw err;
6452
+ }
6453
+ }
6424
6454
  run(sql, ...params) {
6425
6455
  const result = this.query(sql, params);
6426
6456
  return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
@@ -10581,6 +10611,7 @@ init_billing();
10581
10611
  init_package_metadata();
10582
10612
  init_pricing();
10583
10613
  init_store();
10614
+ init_transport();
10584
10615
  import { execSync } from "child_process";
10585
10616
  var program = new Command;
10586
10617
  program.name("economy").description("AI coding cost tracker \u2014 Claude, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
@@ -11650,4 +11681,16 @@ registerBriefCommand(program, { beforeRead: () => autoSync({ dedupe: false }) })
11650
11681
  registerExtendedCommands(program);
11651
11682
  registerFleetCommands(program);
11652
11683
  registerEventsCommands(program, { source: "economy" });
11653
- program.parseAsync();
11684
+ function reportCliError(err2) {
11685
+ if (err2 instanceof HasnaHttpError) {
11686
+ if (err2.status === 404) {
11687
+ fail(`economy: the cloud API has no endpoint for this command (${err2.method} ${err2.path} -> 404). ` + `The self-hosted server is likely running an older build \u2014 it needs an ECS redeploy of the current version.`);
11688
+ }
11689
+ if (err2.status === 401 || err2.status === 403) {
11690
+ fail(`economy: cloud API rejected the request (${err2.status}). Check HASNA_ECONOMY_API_KEY.`);
11691
+ }
11692
+ fail(`economy: cloud API request failed (${err2.method} ${err2.path} -> ${err2.status}).`);
11693
+ }
11694
+ fail(`economy: ${err2 instanceof Error ? err2.message : String(err2)}`);
11695
+ }
11696
+ program.parseAsync().catch(reportCliError);
@@ -6,11 +6,31 @@ import { type DbAdapter, type PreparedStatement, type RunResult } from '@hasna/c
6
6
  * `DATE(col)` casts through `timestamptz`. Ordered most-specific first.
7
7
  */
8
8
  export declare function translateSqliteDates(sql: string): string;
9
+ /**
10
+ * Connection-level failures where the query provably never executed (the pool
11
+ * could not hand out or keep a live backend). These are safe to retry once —
12
+ * the pg pool discards the dead client and dials a fresh one on the next query,
13
+ * so a single retry transparently absorbs RDS failover / idle-drop / connect
14
+ * timeout blips instead of surfacing a 500 the caller has to retry by hand.
15
+ */
16
+ export declare function isTransientConnError(message: string): boolean;
9
17
  export declare class SyncPgAdapter implements DbAdapter {
10
18
  private readonly worker;
11
19
  private readonly port;
12
20
  private counter;
21
+ private readonly sleeper;
13
22
  constructor(dsn: string);
23
+ /**
24
+ * Drain the reply matching `expectedId`. The reply is enqueued on the port
25
+ * before the worker notifies, but cross-thread delivery can lag under CPU
26
+ * pressure — so poll on a wall-clock deadline (yielding via a 1ms Atomics.wait
27
+ * between empty reads) rather than a fixed iteration count, which could give
28
+ * up prematurely on a busy box and throw a spurious 500. Replies whose id does
29
+ * not match are stale results of a previously abandoned (timed-out) query;
30
+ * discard them so the port never cross-talks between requests.
31
+ */
32
+ private drainReply;
33
+ private queryOnce;
14
34
  private query;
15
35
  run(sql: string, ...params: unknown[]): RunResult;
16
36
  get(sql: string, ...params: unknown[]): unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"sync-pg.d.ts","sourceRoot":"","sources":["../../src/db/sync-pg.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiC,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAIpH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiCxD;AAaD,qBAAa,aAAc,YAAW,SAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,OAAO,CAAI;gBAEP,GAAG,EAAE,MAAM;IAYvB,OAAO,CAAC,KAAK;IAsBb,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS;IAIjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;IAG/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE;IAGjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAGvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB;IASvC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAI9B,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"sync-pg.d.ts","sourceRoot":"","sources":["../../src/db/sync-pg.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiC,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAIpH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiCxD;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAY7D;AAED,qBAAa,aAAc,YAAW,SAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,OAAO,CAAI;IAInB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;gBAEvD,GAAG,EAAE,MAAM;IAYvB;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,KAAK;IAcb,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS;IAIjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;IAG/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE;IAGjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAGvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB;IASvC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAI9B,KAAK,IAAI,IAAI;CAGd"}
@@ -1988,11 +1988,16 @@ function resolveWorkerPath() {
1988
1988
  return candidate;
1989
1989
  return candidates[0];
1990
1990
  }
1991
+ function isTransientConnError(message) {
1992
+ const m = message.toLowerCase();
1993
+ return m.includes("econnreset") || m.includes("econnrefused") || m.includes("etimedout") || m.includes("connection terminated") || m.includes("connection ended") || m.includes("timeout exceeded when trying to connect") || m.includes("terminating connection") || m.includes("server closed the connection");
1994
+ }
1991
1995
 
1992
1996
  class SyncPgAdapter {
1993
1997
  worker;
1994
1998
  port;
1995
1999
  counter = 0;
2000
+ sleeper = new Int32Array(new SharedArrayBuffer(4));
1996
2001
  constructor(dsn) {
1997
2002
  const workerPath = resolveWorkerPath();
1998
2003
  this.worker = new Worker(workerPath, { workerData: { dsn } });
@@ -2001,25 +2006,45 @@ class SyncPgAdapter {
2001
2006
  this.port = channel.port1;
2002
2007
  this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
2003
2008
  }
2004
- query(sql, params) {
2005
- const pgSql = translateSqliteDates(translateSql(sql, "pg"));
2006
- const pgParams = translateParams(params);
2009
+ drainReply(expectedId) {
2010
+ const deadline = Date.now() + 30000;
2011
+ for (;; ) {
2012
+ const received = receiveMessageOnPort(this.port);
2013
+ if (received) {
2014
+ const reply = received.message;
2015
+ if (reply.id === expectedId)
2016
+ return reply;
2017
+ continue;
2018
+ }
2019
+ if (Date.now() > deadline)
2020
+ throw new Error("SyncPgAdapter: no reply from worker (timeout)");
2021
+ Atomics.wait(this.sleeper, 0, 0, 1);
2022
+ }
2023
+ }
2024
+ queryOnce(pgSql, pgParams) {
2007
2025
  const id = ++this.counter;
2008
2026
  const signal = new SharedArrayBuffer(4);
2009
2027
  const sig = new Int32Array(signal);
2010
2028
  Atomics.store(sig, 0, 0);
2011
2029
  this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
2012
2030
  Atomics.wait(sig, 0, 0);
2013
- let received = receiveMessageOnPort(this.port);
2014
- for (let attempt = 0;!received && attempt < 1e6; attempt++)
2015
- received = receiveMessageOnPort(this.port);
2016
- if (!received)
2017
- throw new Error("SyncPgAdapter: no reply from worker");
2018
- const reply = received.message;
2031
+ const reply = this.drainReply(id);
2019
2032
  if (!reply.ok)
2020
2033
  throw new Error(reply.error);
2021
2034
  return { rows: reply.rows, rowCount: reply.rowCount };
2022
2035
  }
2036
+ query(sql, params) {
2037
+ const pgSql = translateSqliteDates(translateSql(sql, "pg"));
2038
+ const pgParams = translateParams(params);
2039
+ try {
2040
+ return this.queryOnce(pgSql, pgParams);
2041
+ } catch (err) {
2042
+ if (err instanceof Error && isTransientConnError(err.message)) {
2043
+ return this.queryOnce(pgSql, pgParams);
2044
+ }
2045
+ throw err;
2046
+ }
2047
+ }
2023
2048
  run(sql, ...params) {
2024
2049
  const result = this.query(sql, params);
2025
2050
  return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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
  "repository": {
6
6
  "type": "git",