@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.
- package/dist/cli/index.js +49 -91
- 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,
|
|
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
|
|
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()) :
|
|
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,
|
|
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
|
|
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()) :
|
|
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
|
|
2915
|
-
|
|
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
|
|
2922
|
-
const
|
|
2923
|
-
const
|
|
2924
|
-
const
|
|
2925
|
-
const
|
|
2926
|
-
|
|
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(
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
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