@hasna/economy 0.2.13 → 0.2.14

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +49 -91
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2858,24 +2858,34 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
2858
2858
  process.exit(1);
2859
2859
  }
2860
2860
  });
2861
+ var CLOUD_RDS_HOST = "hasnaxyz-prod-opensource.c4limg0qgqvk.us-east-1.rds.amazonaws.com";
2862
+ var CLOUD_RDS_USER = "hasna_admin";
2863
+ var CLOUD_RDS_DB = "economy";
2864
+ var CLOUD_TABLES = ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
2865
+ async function getCloudPassword() {
2866
+ if (process.env["ECONOMY_PG_PASSWORD"])
2867
+ return process.env["ECONOMY_PG_PASSWORD"];
2868
+ const { execSync: exec } = await import("child_process");
2869
+ const secretJson = exec(`aws --profile hasna-xyz-hq secretsmanager get-secret-value --secret-id 'rds!db-7a451ce6-83a9-40fa-b24a-81e5d5943511' --query SecretString --output text`, { timeout: 1e4, encoding: "utf-8" });
2870
+ return JSON.parse(secretJson).password;
2871
+ }
2872
+ async function getCloudPg() {
2873
+ const { PgAdapterAsync } = await import("@hasna/cloud");
2874
+ const pw = encodeURIComponent(await getCloudPassword());
2875
+ return new PgAdapterAsync(`postgresql://${CLOUD_RDS_USER}:${pw}@${CLOUD_RDS_HOST}:5432/${CLOUD_RDS_DB}?sslmode=require`);
2876
+ }
2861
2877
  var cloudCmd = program.command("cloud").description("Cross-machine sync via cloud PostgreSQL");
2862
2878
  cloudCmd.command("push").description("Push local economy data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
2863
- const { syncPush, PgAdapterAsync, getCloudConfig, SqliteAdapter } = await import("@hasna/cloud");
2879
+ const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
2864
2880
  const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2865
- const config = getCloudConfig();
2866
- if (!config.rds?.host) {
2867
- console.error(chalk4.red("Cloud not configured. Set RDS host in ~/.hasna/cloud.json"));
2868
- process.exit(1);
2869
- }
2870
- const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
2881
+ const cloud = await getCloudPg();
2871
2882
  const local = new SqliteAdapter(getDbPath());
2872
- const cloud = new PgAdapterAsync(connStr);
2873
2883
  process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
2874
2884
  for (const sql of PG_MIGRATIONS2) {
2875
2885
  await cloud.run(sql);
2876
2886
  }
2877
2887
  console.log(chalk4.green("\u2713"));
2878
- const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
2888
+ const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
2879
2889
  process.stdout.write(chalk4.cyan(`\u2192 Pushing ${tableList.join(", ")}... `));
2880
2890
  const results = await syncPush(local, cloud, { tables: tableList });
2881
2891
  const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
@@ -2886,22 +2896,16 @@ cloudCmd.command("push").description("Push local economy data to cloud PostgreSQ
2886
2896
  \u2713 Push complete from ${getMachineId()}`));
2887
2897
  });
2888
2898
  cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
2889
- const { syncPull, PgAdapterAsync, getCloudConfig, SqliteAdapter } = await import("@hasna/cloud");
2899
+ const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
2890
2900
  const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2891
- const config = getCloudConfig();
2892
- if (!config.rds?.host) {
2893
- console.error(chalk4.red("Cloud not configured. Set RDS host in ~/.hasna/cloud.json"));
2894
- process.exit(1);
2895
- }
2896
- const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
2901
+ const cloud = await getCloudPg();
2897
2902
  const local = new SqliteAdapter(getDbPath());
2898
- const cloud = new PgAdapterAsync(connStr);
2899
2903
  process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
2900
2904
  for (const sql of PG_MIGRATIONS2) {
2901
2905
  await cloud.run(sql);
2902
2906
  }
2903
2907
  console.log(chalk4.green("\u2713"));
2904
- const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
2908
+ const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
2905
2909
  process.stdout.write(chalk4.cyan(`\u2192 Pulling ${tableList.join(", ")}... `));
2906
2910
  const results = await syncPull(cloud, local, { tables: tableList });
2907
2911
  const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
@@ -2911,90 +2915,44 @@ cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").opti
2911
2915
  console.log(chalk4.bold.green(`
2912
2916
  \u2713 Pull complete to ${getMachineId()}`));
2913
2917
  });
2914
- cloudCmd.command("sync").description("Full sync: ingest local, then merge data from all reachable machines via SSH").option("--machines <list>", "Comma-separated machine hostnames (default: spark01,apple01,apple03)").action(async (opts) => {
2915
- const thisMachine = getMachineId();
2916
- console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${thisMachine}
2918
+ cloudCmd.command("sync").description("Full sync: ingest local \u2192 push to cloud \u2192 pull from cloud").action(async () => {
2919
+ console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${getMachineId()}
2917
2920
  `));
2918
2921
  process.stdout.write(chalk4.cyan("\u2192 Ingesting local data... "));
2919
2922
  await autoSync();
2920
2923
  console.log(chalk4.green("\u2713"));
2921
- const allMachines = (opts.machines ?? "spark01,apple01,apple03").split(",").map((m) => m.trim());
2922
- const remoteMachines = allMachines.filter((m) => m !== thisMachine);
2923
- const db = openDatabase();
2924
- const { existsSync: existsSync8, mkdirSync: mkdirSync4, unlinkSync } = await import("fs");
2925
- const { join: join9 } = await import("path");
2926
- const { execSync: exec } = await import("child_process");
2927
- const tmpDir = join9(process.env["TMPDIR"] ?? "/tmp", "economy-sync");
2928
- mkdirSync4(tmpDir, { recursive: true });
2929
- const isLinux = process.platform === "linux";
2930
- const remoteDbPath = isLinux ? ".hasna/economy/economy.db" : ".hasna/economy/economy.db";
2931
- for (const machine of remoteMachines) {
2932
- const localCopy = join9(tmpDir, `${machine}.db`);
2933
- process.stdout.write(chalk4.cyan(`\u2192 Fetching from ${machine}... `));
2934
- try {
2935
- exec(`scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${machine}:~/${remoteDbPath} "${localCopy}" 2>/dev/null`, { timeout: 30000 });
2936
- if (!existsSync8(localCopy)) {
2937
- console.log(chalk4.yellow("skipped (no file)"));
2938
- continue;
2939
- }
2940
- const { SqliteAdapter } = await import("@hasna/cloud");
2941
- const remoteDb = new SqliteAdapter(localCopy);
2942
- remoteDb.exec("PRAGMA busy_timeout = 5000");
2943
- const sessions = remoteDb.prepare("SELECT * FROM sessions").all();
2944
- let sCount = 0;
2945
- for (const s of sessions) {
2946
- const existing = db.prepare("SELECT id FROM sessions WHERE id = ?").get(s["id"]);
2947
- if (!existing) {
2948
- db.prepare(`INSERT OR IGNORE INTO sessions (id, agent, project_path, project_name, started_at, ended_at, total_cost_usd, total_tokens, request_count, machine_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(s["id"], s["agent"], s["project_path"], s["project_name"], s["started_at"], s["ended_at"], s["total_cost_usd"], s["total_tokens"], s["request_count"], s["machine_id"] || machine);
2949
- sCount++;
2950
- }
2951
- }
2952
- let rCount = 0;
2953
- try {
2954
- const cols = remoteDb.prepare("PRAGMA table_info(requests)").all();
2955
- const hasMachineCol = cols.some((c) => c.name === "machine_id");
2956
- const requests = remoteDb.prepare("SELECT * FROM requests").all();
2957
- for (const r of requests) {
2958
- const existing = db.prepare("SELECT id FROM requests WHERE id = ?").get(r["id"]);
2959
- if (!existing) {
2960
- db.prepare(`INSERT OR IGNORE INTO requests (id, agent, session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cost_usd, duration_ms, timestamp, source_request_id, machine_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(r["id"], r["agent"], r["session_id"], r["model"], r["input_tokens"], r["output_tokens"], r["cache_read_tokens"], r["cache_create_tokens"], r["cost_usd"], r["duration_ms"], r["timestamp"], r["source_request_id"], hasMachineCol ? r["machine_id"] || machine : machine);
2961
- rCount++;
2962
- }
2963
- }
2964
- } catch {}
2965
- remoteDb.close();
2966
- try {
2967
- unlinkSync(localCopy);
2968
- } catch {}
2969
- console.log(chalk4.green(`\u2713 ${sCount} sessions, ${rCount} requests`));
2970
- } catch (e) {
2971
- console.log(chalk4.yellow(`skipped (${e instanceof Error ? e.message.split(`
2972
- `)[0] : "unreachable"})`));
2973
- try {
2974
- unlinkSync(localCopy);
2975
- } catch {}
2976
- }
2924
+ const { syncPush, syncPull, SqliteAdapter } = await import("@hasna/cloud");
2925
+ const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2926
+ const cloud = await getCloudPg();
2927
+ const local = new SqliteAdapter(getDbPath());
2928
+ for (const sql of PG_MIGRATIONS2) {
2929
+ await cloud.run(sql);
2977
2930
  }
2931
+ process.stdout.write(chalk4.cyan("\u2192 Pushing local \u2192 cloud... "));
2932
+ const pushResults = await syncPush(local, cloud, { tables: CLOUD_TABLES });
2933
+ console.log(chalk4.green(`\u2713 ${pushResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
2934
+ process.stdout.write(chalk4.cyan("\u2192 Pulling cloud \u2192 local... "));
2935
+ const pullResults = await syncPull(cloud, local, { tables: CLOUD_TABLES });
2936
+ console.log(chalk4.green(`\u2713 ${pullResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
2937
+ local.close();
2938
+ await cloud.close();
2978
2939
  console.log(chalk4.bold.green(`
2979
2940
  \u2713 Cloud sync complete`));
2980
2941
  });
2981
2942
  cloudCmd.command("status").description("Check cloud connection status").action(async () => {
2982
- const { PgAdapterAsync, getCloudConfig } = await import("@hasna/cloud");
2983
- const config = getCloudConfig();
2984
2943
  console.log();
2985
- console.log(` Mode: ${chalk4.white(config.mode)}`);
2986
2944
  console.log(` Machine: ${chalk4.white(getMachineId())}`);
2987
- console.log(` RDS Host: ${chalk4.white(config.rds?.host || "(not configured)")}`);
2988
- if (config.rds?.host && config.rds?.username) {
2989
- try {
2990
- const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
2991
- const pg = new PgAdapterAsync(connStr);
2992
- await pg.get("SELECT 1 as ok");
2993
- console.log(` PostgreSQL: ${chalk4.green("connected")}`);
2994
- await pg.close();
2995
- } catch (err2) {
2996
- console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
2997
- }
2945
+ console.log(` RDS Host: ${chalk4.white(CLOUD_RDS_HOST)}`);
2946
+ console.log(` Database: ${chalk4.white(CLOUD_RDS_DB)}`);
2947
+ try {
2948
+ const cloud = await getCloudPg();
2949
+ await cloud.get("SELECT 1 as ok");
2950
+ const tables = await cloud.all("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
2951
+ console.log(` PostgreSQL: ${chalk4.green("connected")}`);
2952
+ console.log(` Tables: ${chalk4.white(tables.map((t) => t.tablename).join(", ") || "(none)")}`);
2953
+ await cloud.close();
2954
+ } catch (err2) {
2955
+ console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
2998
2956
  }
2999
2957
  console.log();
3000
2958
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",