@hasna/economy 0.5.0 → 0.5.1

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Update the hosted container to the pinned Alpine runtime and age-eligible security fixes, with exact standalone dependency overrides and reproducible lockfile resolution.
8
+ - Repair missing legacy PostgreSQL cost-center schema during explicit migration, preserving original rows and migration history with an atomic, validated completion receipt.
9
+
3
10
  ## 0.5.0
4
11
 
5
12
  ### Minor Changes
@@ -4,7 +4,7 @@ import {
4
4
  } from "./index-w3bvv0ns.js";
5
5
  import {
6
6
  getStore
7
- } from "./index-kgy9b6bf.js";
7
+ } from "./index-r1jv3kpy.js";
8
8
  import {
9
9
  formatReportedUsd,
10
10
  formatSummaryCoverage,
@@ -19,7 +19,7 @@ import {
19
19
  // package.json
20
20
  var package_default = {
21
21
  name: "@hasna/economy",
22
- version: "0.5.0",
22
+ version: "0.5.1",
23
23
  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",
24
24
  repository: {
25
25
  type: "git",
@@ -114,7 +114,16 @@ var package_default = {
114
114
  },
115
115
  trustedDependencies: [
116
116
  "@hasna/projects"
117
- ]
117
+ ],
118
+ overrides: {
119
+ "fast-uri": "3.1.8",
120
+ hono: "4.13.7",
121
+ "ip-address": "10.7.1",
122
+ nanoid: "5.1.16",
123
+ "path-to-regexp": "8.4.0",
124
+ undici: "6.28.1",
125
+ "express-rate-limit": "8.7.0"
126
+ }
118
127
  };
119
128
 
120
129
  // src/lib/package-metadata.ts
@@ -14,7 +14,7 @@ import {
14
14
  querySavingsSummary,
15
15
  requireCurrentBillingDiffSummary,
16
16
  requireCurrentSavingsReport
17
- } from "./index-axh32d30.js";
17
+ } from "./index-d480p2wh.js";
18
18
  import {
19
19
  deleteBudget,
20
20
  deleteGoal,
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  LOCAL_STORAGE_OPT_IN_KEYS
4
- } from "./index-axh32d30.js";
4
+ } from "./index-d480p2wh.js";
5
5
  import {
6
6
  assessCostFromDb,
7
7
  getIngestState,
@@ -5,7 +5,7 @@ import {
5
5
  } from "./index-w3bvv0ns.js";
6
6
  import {
7
7
  syncAll
8
- } from "./index-emvs5ph2.js";
8
+ } from "./index-xsah93s2.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-axh32d30.js";
19
+ } from "./index-d480p2wh.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-kn5572kd.js";
9
+ } from "./index-49f12css.js";
10
10
  import"./index-w3bvv0ns.js";
11
- import"./index-kgy9b6bf.js";
12
- import"./index-axh32d30.js";
11
+ import"./index-r1jv3kpy.js";
12
+ import"./index-d480p2wh.js";
13
13
  import"./index-ez9thsr7.js";
14
14
  import"./index-p35cyhc4.js";
15
15
  export {
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  agentPaths,
4
4
  syncAll
5
- } from "./index-emvs5ph2.js";
5
+ } from "./index-xsah93s2.js";
6
6
  import {
7
7
  getStore,
8
8
  isCloudStore
9
- } from "./index-kgy9b6bf.js";
10
- import"./index-axh32d30.js";
9
+ } from "./index-r1jv3kpy.js";
10
+ import"./index-d480p2wh.js";
11
11
  import {
12
12
  formatSummaryValue
13
13
  } from "./index-ez9thsr7.js";
package/dist/cli/index.js CHANGED
@@ -8,22 +8,22 @@ import {
8
8
  import {
9
9
  buildStatusLine,
10
10
  gatherStatusData
11
- } from "../chunks/index-kn5572kd.js";
11
+ } from "../chunks/index-49f12css.js";
12
12
  import"../chunks/index-w3bvv0ns.js";
13
13
  import {
14
14
  agentPaths,
15
15
  syncAll
16
- } from "../chunks/index-emvs5ph2.js";
16
+ } from "../chunks/index-xsah93s2.js";
17
17
  import {
18
18
  getStore,
19
19
  isCloudStore
20
- } from "../chunks/index-kgy9b6bf.js";
20
+ } from "../chunks/index-r1jv3kpy.js";
21
21
  import {
22
22
  billingDriftCheck,
23
23
  economyCloudStorage,
24
24
  economyTransportReport,
25
25
  packageMetadata
26
- } from "../chunks/index-axh32d30.js";
26
+ } from "../chunks/index-d480p2wh.js";
27
27
  import {
28
28
  AGENTS,
29
29
  MAX_INGEST_BYTES,
@@ -1357,23 +1357,23 @@ function registerExtendedCommands(program) {
1357
1357
  console.log();
1358
1358
  });
1359
1359
  program.command("tui").description("Terminal status dashboard (Linux/SSH friendly)").option("--watch", "Live refresh").option("--interval <seconds>", "Refresh interval", "30").action(async (opts) => {
1360
- const { runTui } = await import("../chunks/tui-7f8f951w.js");
1360
+ const { runTui } = await import("../chunks/tui-khmnkat2.js");
1361
1361
  await runTui({
1362
1362
  watch: Boolean(opts.watch),
1363
1363
  interval: Number(opts.interval ?? 30)
1364
1364
  });
1365
1365
  });
1366
1366
  program.command("waybar").description("Print waybar-compatible JSON status line").action(async () => {
1367
- const { printWaybarJson } = await import("../chunks/tui-7f8f951w.js");
1367
+ const { printWaybarJson } = await import("../chunks/tui-khmnkat2.js");
1368
1368
  await printWaybarJson();
1369
1369
  });
1370
1370
  const barCmd = program.command("bar").description("Status bar helpers (Linux)");
1371
1371
  barCmd.command("tui").description("Alias for economy tui").option("--watch", "Live refresh").action(async (opts) => {
1372
- const { runTui } = await import("../chunks/tui-7f8f951w.js");
1372
+ const { runTui } = await import("../chunks/tui-khmnkat2.js");
1373
1373
  await runTui({ watch: Boolean(opts.watch) });
1374
1374
  });
1375
1375
  barCmd.command("waybar").description("Alias for economy waybar").action(async () => {
1376
- const { printWaybarJson } = await import("../chunks/tui-7f8f951w.js");
1376
+ const { printWaybarJson } = await import("../chunks/tui-khmnkat2.js");
1377
1377
  await printWaybarJson();
1378
1378
  });
1379
1379
  program.command("estimate").description("Estimate cost for token counts").requiredOption("--model <model>", "Model name").option("--input <n>", "Input tokens", "0").option("--output <n>", "Output tokens", "0").action(async (opts) => {
@@ -3198,7 +3198,7 @@ program.command("accounts [period]").description("List account usage by email ad
3198
3198
  console.log();
3199
3199
  });
3200
3200
  program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
3201
- const { watchCosts } = await import("../chunks/watch-mm1j39vy.js");
3201
+ const { watchCosts } = await import("../chunks/watch-4y064yys.js");
3202
3202
  await watchCosts({
3203
3203
  interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
3204
3204
  daemon: Boolean(opts.daemon),
@@ -3434,7 +3434,7 @@ pricingCmd.command("remove <model>").description("Remove pricing for a model").a
3434
3434
  });
3435
3435
  program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
3436
3436
  const port = parseCliPort(opts.port ?? "3456", "--port");
3437
- const { startServer } = await import("../chunks/serve-0k69e4h9.js");
3437
+ const { startServer } = await import("../chunks/serve-2knem98x.js");
3438
3438
  startServer(port);
3439
3439
  });
3440
3440
  program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--gemini", "Install into Gemini CLI").option("--all", "Install into all agents").action(async (opts) => {
@@ -0,0 +1,12 @@
1
+ export interface LegacySchemaRepairResult {
2
+ id: string;
3
+ status: 'not_needed' | 'applied' | 'already_applied';
4
+ added: string[];
5
+ }
6
+ /**
7
+ * Repair one known additive schema gap before the existing numbered migration
8
+ * sequence. Only the explicit migration command calls this; server startup
9
+ * never repairs a store. Originals and their migration markers are untouched.
10
+ */
11
+ export declare function repairLegacyCostCenterSchema(dsn: string): Promise<LegacySchemaRepairResult>;
12
+ //# sourceMappingURL=pg-legacy-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-legacy-schema.d.ts","sourceRoot":"","sources":["../../src/db/pg-legacy-schema.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,iBAAiB,CAAA;IACpD,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AA6HD;;;;GAIG;AACH,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA8DjG"}
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ import { randomUUID } from "crypto";
61
61
  // package.json
62
62
  var package_default = {
63
63
  name: "@hasna/economy",
64
- version: "0.5.0",
64
+ version: "0.5.1",
65
65
  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",
66
66
  repository: {
67
67
  type: "git",
@@ -156,7 +156,16 @@ var package_default = {
156
156
  },
157
157
  trustedDependencies: [
158
158
  "@hasna/projects"
159
- ]
159
+ ],
160
+ overrides: {
161
+ "fast-uri": "3.1.8",
162
+ hono: "4.13.7",
163
+ "ip-address": "10.7.1",
164
+ nanoid: "5.1.16",
165
+ "path-to-regexp": "8.4.0",
166
+ undici: "6.28.1",
167
+ "express-rate-limit": "8.7.0"
168
+ }
160
169
  };
161
170
 
162
171
  // src/lib/package-metadata.ts
package/dist/mcp/index.js CHANGED
@@ -84,7 +84,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
84
84
  // package.json
85
85
  var package_default = {
86
86
  name: "@hasna/economy",
87
- version: "0.5.0",
87
+ version: "0.5.1",
88
88
  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",
89
89
  repository: {
90
90
  type: "git",
@@ -179,7 +179,16 @@ var package_default = {
179
179
  },
180
180
  trustedDependencies: [
181
181
  "@hasna/projects"
182
- ]
182
+ ],
183
+ overrides: {
184
+ "fast-uri": "3.1.8",
185
+ hono: "4.13.7",
186
+ "ip-address": "10.7.1",
187
+ nanoid: "5.1.16",
188
+ "path-to-regexp": "8.4.0",
189
+ undici: "6.28.1",
190
+ "express-rate-limit": "8.7.0"
191
+ }
183
192
  };
184
193
 
185
194
  // src/lib/package-metadata.ts
@@ -3458,7 +3458,7 @@ async function pushIngestRows(cloud, scratch) {
3458
3458
  // package.json
3459
3459
  var package_default = {
3460
3460
  name: "@hasna/economy",
3461
- version: "0.5.0",
3461
+ version: "0.5.1",
3462
3462
  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",
3463
3463
  repository: {
3464
3464
  type: "git",
@@ -3553,7 +3553,16 @@ var package_default = {
3553
3553
  },
3554
3554
  trustedDependencies: [
3555
3555
  "@hasna/projects"
3556
- ]
3556
+ ],
3557
+ overrides: {
3558
+ "fast-uri": "3.1.8",
3559
+ hono: "4.13.7",
3560
+ "ip-address": "10.7.1",
3561
+ nanoid: "5.1.16",
3562
+ "path-to-regexp": "8.4.0",
3563
+ undici: "6.28.1",
3564
+ "express-rate-limit": "8.7.0"
3565
+ }
3557
3566
  };
3558
3567
 
3559
3568
  // src/lib/package-metadata.ts
@@ -4164,6 +4164,208 @@ var init_pg_migrate = __esm(() => {
4164
4164
  init_pg_connection();
4165
4165
  });
4166
4166
 
4167
+ // src/db/pg-legacy-schema.ts
4168
+ var exports_pg_legacy_schema = {};
4169
+ __export(exports_pg_legacy_schema, {
4170
+ repairLegacyCostCenterSchema: () => repairLegacyCostCenterSchema
4171
+ });
4172
+ import { createHash as createHash5 } from "crypto";
4173
+ import pg4 from "pg";
4174
+ async function assertReceiptTable(client, schema) {
4175
+ await client.query("LOCK TABLE " + quote2(schema) + ".economy_schema_repairs IN SHARE ROW EXCLUSIVE MODE");
4176
+ const shape = await relation(client, schema, "economy_schema_repairs");
4177
+ if (!shape || shape.columns.length !== 3 || !checkColumn("economy_schema_repairs", shape.columns.find((c) => c.name === "id"), true) || !checkColumn("economy_schema_repairs", shape.columns.find((c) => c.name === "sql_sha256"), true)) {
4178
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger columns");
4179
+ }
4180
+ const completed = shape.columns.find((c) => c.name === "completed_at");
4181
+ if (!completed || completed.type !== "timestamp with time zone" || !completed.required || completed.generated || completed.identity) {
4182
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger timestamp");
4183
+ }
4184
+ const meta = await client.query(`
4185
+ SELECT c.relpersistence='p' AND NOT c.relrowsecurity AND NOT c.relforcerowsecurity
4186
+ AND NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid=c.oid AND NOT tgisinternal)
4187
+ AND NOT EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class=c.oid)
4188
+ AND NOT EXISTS (SELECT 1 FROM pg_inherits WHERE inhrelid=c.oid OR inhparent=c.oid)
4189
+ AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid=c.oid AND (contype<>'p' OR condeferrable))
4190
+ AS ordinary,
4191
+ ARRAY(SELECT a.attname::text FROM pg_index i
4192
+ CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
4193
+ JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum=k.attnum
4194
+ WHERE i.indrelid=c.oid AND i.indisprimary AND i.indisvalid AND i.indisready
4195
+ ORDER BY k.ordinality) AS primary_columns,
4196
+ (SELECT pg_get_expr(d.adbin,d.adrelid) FROM pg_attrdef d JOIN pg_attribute a
4197
+ ON a.attrelid=d.adrelid AND a.attnum=d.adnum
4198
+ WHERE d.adrelid=c.oid AND a.attname='completed_at') AS clock_default
4199
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
4200
+ WHERE n.nspname=$1 AND c.relname='economy_schema_repairs'`, [schema]);
4201
+ const row = meta.rows[0];
4202
+ if (!row?.ordinary || JSON.stringify(row.primary_columns) !== '["id"]' || row.clock_default !== "clock_timestamp()") {
4203
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger behavior");
4204
+ }
4205
+ }
4206
+ async function relation(client, schema, name) {
4207
+ const result = await client.query(`
4208
+ SELECT c.relkind AS kind, a.attname AS name, a.atttypid::regtype::text AS type,
4209
+ a.attnotnull AS required, a.attgenerated AS generated, a.attidentity AS identity
4210
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
4211
+ LEFT JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum>0 AND NOT a.attisdropped
4212
+ WHERE n.nspname=$1 AND c.relname=$2 ORDER BY a.attnum`, [schema, name]);
4213
+ if (!result.rows.length)
4214
+ return null;
4215
+ if (result.rows[0].kind !== "r")
4216
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: expected ordinary table " + name);
4217
+ return { kind: result.rows[0].kind, columns: result.rows.filter((row) => row.name) };
4218
+ }
4219
+ function checkColumn(table, actual, required3) {
4220
+ if (!actual)
4221
+ return false;
4222
+ if (actual.type !== "text" || actual.required !== required3 || actual.generated || actual.identity) {
4223
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected column " + table + "." + actual.name);
4224
+ }
4225
+ return true;
4226
+ }
4227
+ async function missingStructure(client, schema) {
4228
+ const missing = [];
4229
+ for (const table of ["requests", "sessions", "budgets"]) {
4230
+ const found = await relation(client, schema, table);
4231
+ if (!found)
4232
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: missing core table " + table);
4233
+ if (!checkColumn(table, found.columns.find((column) => column.name === "cost_center_id"), false)) {
4234
+ missing.push(table + ".cost_center_id");
4235
+ }
4236
+ }
4237
+ const centers = await relation(client, schema, "cost_centers");
4238
+ if (!centers)
4239
+ missing.push("cost_centers");
4240
+ else {
4241
+ for (const [name, required3] of [
4242
+ ["id", true],
4243
+ ["kind", true],
4244
+ ["name", true],
4245
+ ["repo_path", false],
4246
+ ["labels_json", false],
4247
+ ["created_at", true],
4248
+ ["updated_at", true]
4249
+ ]) {
4250
+ if (!checkColumn("cost_centers", centers.columns.find((column) => column.name === name), required3)) {
4251
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: incomplete cost_centers table");
4252
+ }
4253
+ }
4254
+ const primary = await client.query(`
4255
+ SELECT array_agg(a.attname::text ORDER BY k.ordinality) AS columns
4256
+ FROM pg_index i JOIN pg_class c ON c.oid=i.indrelid JOIN pg_namespace n ON n.oid=c.relnamespace
4257
+ CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
4258
+ JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum=k.attnum
4259
+ WHERE n.nspname=$1 AND c.relname='cost_centers' AND i.indisprimary
4260
+ GROUP BY i.indexrelid`, [schema]);
4261
+ if (JSON.stringify(primary.rows.map((row) => row.columns)) !== '[["id"]]') {
4262
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected cost_centers primary key");
4263
+ }
4264
+ }
4265
+ for (const [name, table, column] of INDEXES) {
4266
+ const result = await client.query(`
4267
+ SELECT t.relname AS table_name, am.amname AS method,
4268
+ i.indisunique AS unique, i.indisvalid AS valid, i.indisready AS ready,
4269
+ i.indpred IS NULL AS unfiltered, i.indexprs IS NULL AS simple,
4270
+ i.indnatts=i.indnkeyatts AS no_includes,
4271
+ ARRAY(SELECT a.attname::text FROM unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
4272
+ JOIN pg_attribute a ON a.attrelid=t.oid AND a.attnum=k.attnum
4273
+ ORDER BY k.ordinality) AS columns
4274
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
4275
+ LEFT JOIN pg_index i ON i.indexrelid=c.oid LEFT JOIN pg_class t ON t.oid=i.indrelid
4276
+ LEFT JOIN pg_am am ON am.oid=c.relam WHERE n.nspname=$1 AND c.relname=$2`, [schema, name]);
4277
+ if (!result.rows.length) {
4278
+ missing.push(name);
4279
+ continue;
4280
+ }
4281
+ const actual = result.rows[0];
4282
+ if (actual.table_name !== table || actual.method !== "btree" || actual.unique || !actual.valid || !actual.ready || !actual.unfiltered || !actual.simple || !actual.no_includes || JSON.stringify(actual.columns) !== JSON.stringify([column])) {
4283
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected index " + name);
4284
+ }
4285
+ }
4286
+ return missing;
4287
+ }
4288
+ async function repairLegacyCostCenterSchema(dsn) {
4289
+ const result = { id: REPAIR_ID, status: "not_needed", added: [] };
4290
+ const pool = new pg4.Pool({ ...resolvePgConnection(dsn), max: 1, connectionTimeoutMillis: 1e4 });
4291
+ pool.on("error", () => {});
4292
+ let client;
4293
+ try {
4294
+ client = await pool.connect();
4295
+ client.on("error", () => {});
4296
+ await client.query("SET statement_timeout='15min'; SET lock_timeout='15s'");
4297
+ await client.query("SELECT pg_advisory_lock(hashtext('economy.pg-migrations.v1'), hashtext(current_database() || ':' || current_schema()))");
4298
+ const schema = (await client.query("SELECT current_schema() AS name")).rows[0]?.name;
4299
+ if (typeof schema !== "string" || !schema)
4300
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: missing migration schema");
4301
+ const ledger = await relation(client, schema, "_pg_migrations");
4302
+ if (!ledger)
4303
+ return result;
4304
+ const history = await client.query("SELECT version FROM " + quote2(schema) + "._pg_migrations WHERE version>=0 AND version<85 ORDER BY version");
4305
+ if (history.rows.length !== 85 || history.rows.some((row, index) => row.version !== index)) {
4306
+ return result;
4307
+ }
4308
+ await client.query("BEGIN");
4309
+ try {
4310
+ await client.query("SELECT set_config('search_path', $1, true)", [quote2(schema)]);
4311
+ const missing = await missingStructure(client, schema);
4312
+ const receiptTable = await relation(client, schema, "economy_schema_repairs");
4313
+ if (receiptTable) {
4314
+ await assertReceiptTable(client, schema);
4315
+ const receipt = await client.query("SELECT sql_sha256 FROM economy_schema_repairs WHERE id=$1", [REPAIR_ID]);
4316
+ if (receipt.rows.length) {
4317
+ if (receipt.rows.length !== 1 || receipt.rows[0].sql_sha256 !== REPAIR_HASH || missing.length) {
4318
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: completed repair receipt or schema has changed");
4319
+ }
4320
+ await client.query("COMMIT");
4321
+ return { ...result, status: "already_applied" };
4322
+ }
4323
+ }
4324
+ if (!missing.length) {
4325
+ await client.query("COMMIT");
4326
+ return result;
4327
+ }
4328
+ await client.query(`CREATE TABLE IF NOT EXISTS economy_schema_repairs (
4329
+ id TEXT PRIMARY KEY, sql_sha256 TEXT NOT NULL,
4330
+ completed_at TIMESTAMPTZ NOT NULL DEFAULT pg_catalog.clock_timestamp()
4331
+ )`);
4332
+ await assertReceiptTable(client, schema);
4333
+ for (const sql of REPAIR_SQL)
4334
+ await client.query(sql);
4335
+ if ((await missingStructure(client, schema)).length)
4336
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: repair postcondition failed");
4337
+ const inserted = await client.query("INSERT INTO economy_schema_repairs (id,sql_sha256) VALUES ($1,$2) RETURNING id,sql_sha256", [REPAIR_ID, REPAIR_HASH]);
4338
+ const readback = await client.query("SELECT id,sql_sha256 FROM economy_schema_repairs WHERE id=$1", [REPAIR_ID]);
4339
+ for (const receipt of [inserted, readback]) {
4340
+ if (receipt.rowCount !== 1 || receipt.rows[0]?.id !== REPAIR_ID || receipt.rows[0]?.sql_sha256 !== REPAIR_HASH) {
4341
+ throw Error("LEGACY_SCHEMA_INCOMPATIBLE: repair receipt readback failed");
4342
+ }
4343
+ }
4344
+ await client.query("COMMIT");
4345
+ return { id: REPAIR_ID, status: "applied", added: missing };
4346
+ } catch (error) {
4347
+ await client.query("ROLLBACK").catch(() => {});
4348
+ throw error;
4349
+ }
4350
+ } finally {
4351
+ client?.release();
4352
+ await pool.end().catch(() => {});
4353
+ }
4354
+ }
4355
+ var REPAIR_ID = "legacy-cost-centers-v1", REPAIR_SQL, REPAIR_HASH, INDEXES, quote2 = (name) => '"' + name.replaceAll('"', '""') + '"';
4356
+ var init_pg_legacy_schema = __esm(() => {
4357
+ init_pg_connection();
4358
+ init_pg_migrations();
4359
+ REPAIR_SQL = [0, 38, 47, 48, 15, 54, 55, 56].map((index) => PG_MIGRATIONS[index]);
4360
+ REPAIR_HASH = createHash5("sha256").update(JSON.stringify(REPAIR_SQL)).digest("hex");
4361
+ INDEXES = [
4362
+ ["idx_cost_centers_kind", "cost_centers", "kind"],
4363
+ ["idx_requests_cost_center", "requests", "cost_center_id"],
4364
+ ["idx_sessions_cost_center", "sessions", "cost_center_id"],
4365
+ ["idx_budgets_cost_center", "budgets", "cost_center_id"]
4366
+ ];
4367
+ });
4368
+
4167
4369
  // src/server/legacy-inventory.ts
4168
4370
  var exports_legacy_inventory = {};
4169
4371
  __export(exports_legacy_inventory, {
@@ -4316,7 +4518,7 @@ __export(exports_legacy_plan, {
4316
4518
  parseLegacySelection: () => parseLegacySelection,
4317
4519
  parseLegacyPlanOptions: () => parseLegacyPlanOptions
4318
4520
  });
4319
- import { createHash as createHash5 } from "crypto";
4521
+ import { createHash as createHash6 } from "crypto";
4320
4522
  import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
4321
4523
  import { isAbsolute } from "path";
4322
4524
  function object(value, fields, label) {
@@ -4505,7 +4707,7 @@ async function runLegacyPlan(selection) {
4505
4707
  await pool.end();
4506
4708
  }
4507
4709
  }
4508
- var INPUT_MAX, MEMBERS_MAX = 1e4, sha256 = (text) => createHash5("sha256").update(text).digest("hex");
4710
+ var INPUT_MAX, MEMBERS_MAX = 1e4, sha256 = (text) => createHash6("sha256").update(text).digest("hex");
4509
4711
  var init_legacy_plan = __esm(() => {
4510
4712
  init_legacy_inventory();
4511
4713
  INPUT_MAX = 256 * 1024;
@@ -4524,7 +4726,7 @@ __export(exports_legacy_preservation, {
4524
4726
  parseLegacyCapsule: () => parseLegacyCapsule,
4525
4727
  assertLegacyPlanCurrent: () => assertLegacyPlanCurrent
4526
4728
  });
4527
- import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
4729
+ import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
4528
4730
  import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync, openSync as openSync2, readSync as readSync2, realpathSync, writeSync } from "fs";
4529
4731
  import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
4530
4732
  function parseLegacyPlan(value) {
@@ -4774,8 +4976,8 @@ async function rehearseLegacyRestore(client, input, scope = "transaction") {
4774
4976
  const table = tables[i], expected = capsule.plan.snapshot.tables[i];
4775
4977
  if (table.table_name !== expected.table_name || table.columns_sha256 !== expected.columns_sha256)
4776
4978
  fail("Restore target schema differs from the preserved schema");
4777
- const source = quote2(table.schema_name) + "." + quote2(table.table_name);
4778
- const temp = quote2("economy_restore_" + randomUUID3().replaceAll("-", ""));
4979
+ const source = quote3(table.schema_name) + "." + quote3(table.table_name);
4980
+ const temp = quote3("economy_restore_" + randomUUID3().replaceAll("-", ""));
4779
4981
  await client.query("CREATE TEMP TABLE " + temp + " (LIKE " + source + " INCLUDING ALL) ON COMMIT DROP");
4780
4982
  const originals = table.table_name === "requests" ? capsule.requests : capsule.sessions;
4781
4983
  restoreTables.push({ source, temp, originals });
@@ -4835,12 +5037,12 @@ async function runLegacyPreservation(command, options) {
4835
5037
  await pool.end();
4836
5038
  }
4837
5039
  }
4838
- var MAX_BYTES, MAX_MEMBERS = 1e4, sha2 = (s) => createHash6("sha256").update(s).digest("hex"), fail = (message) => {
5040
+ var MAX_BYTES, MAX_MEMBERS = 1e4, sha2 = (s) => createHash7("sha256").update(s).digest("hex"), fail = (message) => {
4839
5041
  throw Error(message);
4840
5042
  }, record = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : fail("Expected a JSON object"), hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v) ? v : fail("Expected a lowercase SHA-256 digest"), identifier3 = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024 && !v.includes("\x00") ? v : fail("Expected a nonempty identifier of at most 1024 characters without NUL"), fields = (o, names) => {
4841
5043
  if (Object.keys(o).sort().join(",") !== [...names].sort().join(","))
4842
5044
  fail("Unexpected or missing preservation fields");
4843
- }, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b), quote2 = (s) => '"' + s.replaceAll('"', '""') + '"';
5045
+ }, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b), quote3 = (s) => '"' + s.replaceAll('"', '""') + '"';
4844
5046
  var init_legacy_preservation = __esm(() => {
4845
5047
  init_legacy_plan();
4846
5048
  MAX_BYTES = 32 * 1024 * 1024;
@@ -4858,7 +5060,7 @@ __export(exports_legacy_operations, {
4858
5060
  applyLegacyReconciliation: () => applyLegacyReconciliation,
4859
5061
  LegacyOperationInputError: () => LegacyOperationInputError
4860
5062
  });
4861
- import { createHash as createHash7 } from "crypto";
5063
+ import { createHash as createHash8 } from "crypto";
4862
5064
  import { isAbsolute as isAbsolute3 } from "path";
4863
5065
  function parseLegacyIntent(action, value) {
4864
5066
  try {
@@ -5150,7 +5352,7 @@ async function runLegacyOperation(command, options) {
5150
5352
  await pool.end();
5151
5353
  }
5152
5354
  }
5153
- var sha3 = (s) => createHash7("sha256").update(s).digest("hex"), fail2 = (s) => {
5355
+ var sha3 = (s) => createHash8("sha256").update(s).digest("hex"), fail2 = (s) => {
5154
5356
  throw Error(s);
5155
5357
  }, digest3 = (v, label = "digest") => typeof v === "string" && /^[a-f0-9]{64}$/.test(v) ? v : fail2(label + " must be a lowercase SHA-256 digest"), key = (v, label = "operation_key") => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : fail2(label + " must contain 1-200 letters, digits, dot, underscore, colon or hyphen"), reference = (v, label) => typeof v === "string" && /^[A-Za-z0-9:/._-]{1,512}$/.test(v) ? v : fail2(label + " must contain 1-512 letters, digits, colon, slash, dot, underscore or hyphen"), LegacyOperationInputError, TABLES_SQL = `SELECT c.relname AS table_name,n.nspname AS schema_name,c.oid::text AS relation_oid,
5156
5358
  encode(sha256(convert_to(json_agg(json_build_array(a.attnum,a.attname,a.atttypid::text,a.atttypmod,a.attnotnull,a.attcollation::text) ORDER BY a.attnum)::text,'UTF8')),'hex') AS columns_sha256
@@ -5194,7 +5396,7 @@ function isAuthorizedRequest(req, path) {
5194
5396
  // package.json
5195
5397
  var package_default = {
5196
5398
  name: "@hasna/economy",
5197
- version: "0.5.0",
5399
+ version: "0.5.1",
5198
5400
  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",
5199
5401
  repository: {
5200
5402
  type: "git",
@@ -5289,7 +5491,16 @@ var package_default = {
5289
5491
  },
5290
5492
  trustedDependencies: [
5291
5493
  "@hasna/projects"
5292
- ]
5494
+ ],
5495
+ overrides: {
5496
+ "fast-uri": "3.1.8",
5497
+ hono: "4.13.7",
5498
+ "ip-address": "10.7.1",
5499
+ nanoid: "5.1.16",
5500
+ "path-to-regexp": "8.4.0",
5501
+ undici: "6.28.1",
5502
+ "express-rate-limit": "8.7.0"
5503
+ }
5293
5504
  };
5294
5505
 
5295
5506
  // src/lib/package-metadata.ts
@@ -12457,10 +12668,12 @@ function parsePort(raw, label) {
12457
12668
  async function runMigrate() {
12458
12669
  const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
12459
12670
  const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
12671
+ const { repairLegacyCostCenterSchema: repairLegacyCostCenterSchema2 } = await Promise.resolve().then(() => (init_pg_legacy_schema(), exports_pg_legacy_schema));
12460
12672
  const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
12461
12673
  const dsn = getCloudDatabaseUrl2();
12462
12674
  if (!dsn)
12463
12675
  throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
12676
+ const repair = await repairLegacyCostCenterSchema2(dsn);
12464
12677
  const result = await applyPgMigrations2(dsn, cloudMigrations2(), "economy");
12465
12678
  const pool = createCloudPool2(dsn);
12466
12679
  try {
@@ -12468,7 +12681,7 @@ async function runMigrate() {
12468
12681
  } finally {
12469
12682
  await pool.end().catch(() => {});
12470
12683
  }
12471
- console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, api_keys: "ensured" }));
12684
+ console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, repair, api_keys: "ensured" }));
12472
12685
  if (result.errors.length)
12473
12686
  process.exit(1);
12474
12687
  }
@@ -7266,7 +7266,7 @@ import { randomUUID } from "crypto";
7266
7266
  // package.json
7267
7267
  var package_default = {
7268
7268
  name: "@hasna/economy",
7269
- version: "0.5.0",
7269
+ version: "0.5.1",
7270
7270
  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",
7271
7271
  repository: {
7272
7272
  type: "git",
@@ -7361,7 +7361,16 @@ var package_default = {
7361
7361
  },
7362
7362
  trustedDependencies: [
7363
7363
  "@hasna/projects"
7364
- ]
7364
+ ],
7365
+ overrides: {
7366
+ "fast-uri": "3.1.8",
7367
+ hono: "4.13.7",
7368
+ "ip-address": "10.7.1",
7369
+ nanoid: "5.1.16",
7370
+ "path-to-regexp": "8.4.0",
7371
+ undici: "6.28.1",
7372
+ "express-rate-limit": "8.7.0"
7373
+ }
7365
7374
  };
7366
7375
 
7367
7376
  // src/lib/package-metadata.ts
@@ -0,0 +1,30 @@
1
+ # Legacy PostgreSQL cost-center schema repair
2
+
3
+ Some existing databases record migrations 0–84 while missing `cost_centers`
4
+ and the nullable `cost_center_id` columns on `requests`, `sessions` and
5
+ `budgets`. Migration 102 requires those columns.
6
+
7
+ The explicit `economy-serve migrate` command checks this known layout before
8
+ running the numbered migrations. It adds the missing table, columns and four
9
+ indexes using their unchanged original definitions. It does not infer cost
10
+ center assignments, change existing row values or rewrite migration history.
11
+ Fresh and already complete databases need no compatibility repair.
12
+
13
+ The repair serializes with the existing migration runner. Its DDL and one
14
+ `legacy-cost-centers-v1` receipt commit in one transaction. The receipt records
15
+ the exact repair SQL hash. An existing receipt ledger must have the canonical
16
+ permanent shape, with no triggers, rewrite rules or row security. A completed
17
+ receipt with changed schema or hash is refused. Incompatible existing tables,
18
+ columns or indexes are also refused without replacing them.
19
+
20
+ Preserve and restore-test the database before using the normal deployment
21
+ migration operation. Server startup does not run this repair. If a connection
22
+ is lost during commit, keep the outcome uncertain and inspect the database
23
+ before an explicit retry; the command never retries automatically. A subsequent
24
+ invocation recognizes a committed matching receipt as `already_applied`.
25
+ Numbered migrations retain their separate atomic completion markers.
26
+
27
+ The migration JSON includes `repair.id`, `repair.status` and `repair.added`.
28
+ Possible statuses are `not_needed`, `applied` and `already_applied`. Retain that
29
+ result with the deployment and preservation records. Rolling application code
30
+ back does not remove the added schema or repair receipt.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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",
@@ -95,5 +95,14 @@
95
95
  },
96
96
  "trustedDependencies": [
97
97
  "@hasna/projects"
98
- ]
98
+ ],
99
+ "overrides": {
100
+ "fast-uri": "3.1.8",
101
+ "hono": "4.13.7",
102
+ "ip-address": "10.7.1",
103
+ "nanoid": "5.1.16",
104
+ "path-to-regexp": "8.4.0",
105
+ "undici": "6.28.1",
106
+ "express-rate-limit": "8.7.0"
107
+ }
99
108
  }