@hasna/invoices 0.1.0 → 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +109 -62
  3. package/dashboard/dist/assets/index-BhR1BMmj.js +139 -0
  4. package/dashboard/dist/assets/index-khzrvnCp.css +1 -0
  5. package/dashboard/dist/index.html +13 -0
  6. package/dashboard/dist/logo.jpg +0 -0
  7. package/dist/cli/index.js +10111 -9633
  8. package/dist/cli/tui.d.ts +2 -0
  9. package/dist/cli/tui.d.ts.map +1 -0
  10. package/dist/db/agents.d.ts +19 -0
  11. package/dist/db/agents.d.ts.map +1 -0
  12. package/dist/db/cloud.d.ts +20 -0
  13. package/dist/db/cloud.d.ts.map +1 -0
  14. package/dist/db/core.test.d.ts +2 -0
  15. package/dist/db/core.test.d.ts.map +1 -0
  16. package/dist/db/database.d.ts +7 -8
  17. package/dist/db/database.d.ts.map +1 -1
  18. package/dist/db/invoices.d.ts +77 -90
  19. package/dist/db/invoices.d.ts.map +1 -1
  20. package/dist/db/migrate.d.ts +4 -0
  21. package/dist/db/migrate.d.ts.map +1 -0
  22. package/dist/db/pg.d.ts +3 -0
  23. package/dist/db/pg.d.ts.map +1 -0
  24. package/dist/db/schema.d.ts +6 -0
  25. package/dist/db/schema.d.ts.map +1 -0
  26. package/dist/index.d.ts +8 -7
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1061 -510
  29. package/dist/lib/version.d.ts +2 -0
  30. package/dist/lib/version.d.ts.map +1 -0
  31. package/dist/mcp/index.d.ts +1 -1
  32. package/dist/mcp/index.js +1320 -766
  33. package/dist/server/index.d.ts +3 -0
  34. package/dist/server/index.d.ts.map +1 -0
  35. package/dist/server/index.js +14919 -0
  36. package/dist/types/index.d.ts +18 -0
  37. package/dist/types/index.d.ts.map +1 -0
  38. package/package.json +21 -14
  39. package/dist/db/business.d.ts +0 -93
  40. package/dist/db/business.d.ts.map +0 -1
  41. package/dist/db/clients.d.ts +0 -45
  42. package/dist/db/clients.d.ts.map +0 -1
  43. package/dist/db/migrations.d.ts +0 -7
  44. package/dist/db/migrations.d.ts.map +0 -1
package/dist/mcp/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
4
  var __returnValue = (v) => v;
@@ -16,6 +16,9 @@ var __export = (target, all) => {
16
16
  };
17
17
 
18
18
  // src/mcp/index.ts
19
+ import { readFileSync as readFileSync2 } from "fs";
20
+ import { dirname as dirname3, join as join7 } from "path";
21
+ import { fileURLToPath } from "url";
19
22
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
24
 
@@ -3992,6 +3995,10 @@ var coerce = {
3992
3995
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
3993
3996
  };
3994
3997
  var NEVER = INVALID;
3998
+ // src/db/database.ts
3999
+ import { mkdirSync as mkdirSync3 } from "fs";
4000
+ import { dirname as dirname2, join as join5 } from "path";
4001
+
3995
4002
  // node_modules/@hasna/cloud/dist/index.js
3996
4003
  import { createRequire } from "module";
3997
4004
  import { Database } from "bun:sqlite";
@@ -4009,6 +4016,7 @@ import { join as join2 } from "path";
4009
4016
  import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
4010
4017
  import { join as join3 } from "path";
4011
4018
  import { homedir as homedir3 } from "os";
4019
+ import { hostname } from "os";
4012
4020
  import { homedir as homedir4 } from "os";
4013
4021
  import { join as join4 } from "path";
4014
4022
  import { join as join6, dirname } from "path";
@@ -5352,7 +5360,7 @@ var require_cert_signatures = __commonJS((exports, module) => {
5352
5360
  module.exports = { signatureAlgorithmHashFromCertificate };
5353
5361
  });
5354
5362
  var require_sasl = __commonJS((exports, module) => {
5355
- var crypto2 = require_utils2();
5363
+ var crypto = require_utils2();
5356
5364
  var { signatureAlgorithmHashFromCertificate } = require_cert_signatures();
5357
5365
  function startSession(mechanisms, stream) {
5358
5366
  const candidates = ["SCRAM-SHA-256"];
@@ -5365,7 +5373,7 @@ var require_sasl = __commonJS((exports, module) => {
5365
5373
  if (mechanism === "SCRAM-SHA-256-PLUS" && typeof stream.getPeerCertificate !== "function") {
5366
5374
  throw new Error("SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate");
5367
5375
  }
5368
- const clientNonce = crypto2.randomBytes(18).toString("base64");
5376
+ const clientNonce = crypto.randomBytes(18).toString("base64");
5369
5377
  const gs2Header = mechanism === "SCRAM-SHA-256-PLUS" ? "p=tls-server-end-point" : stream ? "y" : "n";
5370
5378
  return {
5371
5379
  mechanism,
@@ -5401,20 +5409,20 @@ var require_sasl = __commonJS((exports, module) => {
5401
5409
  let hashName = signatureAlgorithmHashFromCertificate(peerCert);
5402
5410
  if (hashName === "MD5" || hashName === "SHA-1")
5403
5411
  hashName = "SHA-256";
5404
- const certHash = await crypto2.hashByName(hashName, peerCert);
5412
+ const certHash = await crypto.hashByName(hashName, peerCert);
5405
5413
  const bindingData = Buffer.concat([Buffer.from("p=tls-server-end-point,,"), Buffer.from(certHash)]);
5406
5414
  channelBinding = bindingData.toString("base64");
5407
5415
  }
5408
5416
  const clientFinalMessageWithoutProof = "c=" + channelBinding + ",r=" + sv.nonce;
5409
5417
  const authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
5410
5418
  const saltBytes = Buffer.from(sv.salt, "base64");
5411
- const saltedPassword = await crypto2.deriveKey(password, saltBytes, sv.iteration);
5412
- const clientKey = await crypto2.hmacSha256(saltedPassword, "Client Key");
5413
- const storedKey = await crypto2.sha256(clientKey);
5414
- const clientSignature = await crypto2.hmacSha256(storedKey, authMessage);
5419
+ const saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration);
5420
+ const clientKey = await crypto.hmacSha256(saltedPassword, "Client Key");
5421
+ const storedKey = await crypto.sha256(clientKey);
5422
+ const clientSignature = await crypto.hmacSha256(storedKey, authMessage);
5415
5423
  const clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString("base64");
5416
- const serverKey = await crypto2.hmacSha256(saltedPassword, "Server Key");
5417
- const serverSignatureBytes = await crypto2.hmacSha256(serverKey, authMessage);
5424
+ const serverKey = await crypto.hmacSha256(saltedPassword, "Server Key");
5425
+ const serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage);
5418
5426
  session.message = "SASLResponse";
5419
5427
  session.serverSignature = Buffer.from(serverSignatureBytes).toString("base64");
5420
5428
  session.response = clientFinalMessageWithoutProof + ",p=" + clientProof;
@@ -5577,11 +5585,11 @@ var require_pg_connection_string = __commonJS((exports, module) => {
5577
5585
  config.client_encoding = result.searchParams.get("encoding");
5578
5586
  return config;
5579
5587
  }
5580
- const hostname = dummyHost ? "" : result.hostname;
5588
+ const hostname2 = dummyHost ? "" : result.hostname;
5581
5589
  if (!config.host) {
5582
- config.host = decodeURIComponent(hostname);
5583
- } else if (hostname && /^%2f/i.test(hostname)) {
5584
- result.pathname = hostname + result.pathname;
5590
+ config.host = decodeURIComponent(hostname2);
5591
+ } else if (hostname2 && /^%2f/i.test(hostname2)) {
5592
+ result.pathname = hostname2 + result.pathname;
5585
5593
  }
5586
5594
  if (!config.port) {
5587
5595
  config.port = result.port;
@@ -7432,7 +7440,7 @@ var require_client = __commonJS((exports, module) => {
7432
7440
  var Query = require_query();
7433
7441
  var defaults = require_defaults();
7434
7442
  var Connection = require_connection();
7435
- var crypto2 = require_utils2();
7443
+ var crypto = require_utils2();
7436
7444
  var activeQueryDeprecationNotice = nodeUtils.deprecate(() => {}, "Client.activeQuery is deprecated and will be removed in pg@9.0");
7437
7445
  var queryQueueDeprecationNotice = nodeUtils.deprecate(() => {}, "Client.queryQueue is deprecated and will be removed in pg@9.0.");
7438
7446
  var pgPassDeprecationNotice = nodeUtils.deprecate(() => {}, "pgpass support is deprecated and will be removed in pg@9.0. " + "You can provide an async function as the password property to the Client/Pool constructor that returns a password instead. Within this function you can call the pgpass module in your own code.");
@@ -7648,7 +7656,7 @@ var require_client = __commonJS((exports, module) => {
7648
7656
  _handleAuthMD5Password(msg) {
7649
7657
  this._getPassword(async () => {
7650
7658
  try {
7651
- const hashedPassword = await crypto2.postgresMd5PasswordHash(this.user, this.password, msg.salt);
7659
+ const hashedPassword = await crypto.postgresMd5PasswordHash(this.user, this.password, msg.salt);
7652
7660
  this.connection.password(hashedPassword);
7653
7661
  } catch (e) {
7654
7662
  this.emit("error", e);
@@ -8926,6 +8934,18 @@ function sqliteToPostgres(sql) {
8926
8934
  }
8927
8935
  return out;
8928
8936
  }
8937
+ function translateDdl(ddl, dialect) {
8938
+ if (dialect === "sqlite")
8939
+ return ddl;
8940
+ let out = ddl;
8941
+ out = out.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/gi, "BIGSERIAL PRIMARY KEY");
8942
+ out = out.replace(/\bAUTOINCREMENT\b/gi, "");
8943
+ out = out.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
8944
+ out = out.replace(/\bBLOB\b/gi, "BYTEA");
8945
+ out = sqliteToPostgres(out);
8946
+ return out;
8947
+ }
8948
+
8929
8949
  class SqliteAdapter {
8930
8950
  db;
8931
8951
  constructor(path) {
@@ -9113,6 +9133,63 @@ class PgAdapter {
9113
9133
  return this.pool;
9114
9134
  }
9115
9135
  }
9136
+
9137
+ class PgAdapterAsync {
9138
+ pool;
9139
+ constructor(arg) {
9140
+ if (typeof arg === "string") {
9141
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
9142
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
9143
+ } else {
9144
+ this.pool = arg;
9145
+ }
9146
+ }
9147
+ async run(sql, ...params) {
9148
+ const pgSql = translateSql(sql, "pg");
9149
+ const pgParams = translateParams(params);
9150
+ const res = await this.pool.query(pgSql, pgParams);
9151
+ return {
9152
+ changes: res.rowCount ?? 0,
9153
+ lastInsertRowid: res.rows?.[0]?.id ?? 0
9154
+ };
9155
+ }
9156
+ async get(sql, ...params) {
9157
+ const pgSql = translateSql(sql, "pg");
9158
+ const pgParams = translateParams(params);
9159
+ const res = await this.pool.query(pgSql, pgParams);
9160
+ return res.rows[0] ?? null;
9161
+ }
9162
+ async all(sql, ...params) {
9163
+ const pgSql = translateSql(sql, "pg");
9164
+ const pgParams = translateParams(params);
9165
+ const res = await this.pool.query(pgSql, pgParams);
9166
+ return res.rows;
9167
+ }
9168
+ async exec(sql) {
9169
+ const pgSql = translateSql(sql, "pg");
9170
+ await this.pool.query(pgSql);
9171
+ }
9172
+ async close() {
9173
+ await this.pool.end();
9174
+ }
9175
+ async transaction(fn) {
9176
+ const client = await this.pool.connect();
9177
+ try {
9178
+ await client.query("BEGIN");
9179
+ const result = await fn(client);
9180
+ await client.query("COMMIT");
9181
+ return result;
9182
+ } catch (err) {
9183
+ await client.query("ROLLBACK");
9184
+ throw err;
9185
+ } finally {
9186
+ client.release();
9187
+ }
9188
+ }
9189
+ get raw() {
9190
+ return this.pool;
9191
+ }
9192
+ }
9116
9193
  var init_adapter = __esm(() => {
9117
9194
  init_esm();
9118
9195
  });
@@ -13375,7 +13452,494 @@ var init_discover = __esm(() => {
13375
13452
  });
13376
13453
  init_adapter();
13377
13454
  init_config();
13455
+ async function syncPush(local, remote, options) {
13456
+ const orderedTables = await getTableOrder(remote, options.tables);
13457
+ return syncTransfer(local, remote, { ...options, tables: orderedTables }, "push");
13458
+ }
13459
+ async function syncPull(remote, local, options) {
13460
+ const orderedTables = await getTableOrder(remote, options.tables);
13461
+ return syncTransfer(remote, local, { ...options, tables: orderedTables }, "pull");
13462
+ }
13463
+ async function getTableOrder(remote, tables) {
13464
+ if (tables.length <= 1)
13465
+ return tables;
13466
+ try {
13467
+ const fks = await remote.all(`
13468
+ SELECT DISTINCT
13469
+ tc.table_name AS source_table,
13470
+ ccu.table_name AS referenced_table
13471
+ FROM information_schema.table_constraints tc
13472
+ JOIN information_schema.constraint_column_usage ccu
13473
+ ON tc.constraint_name = ccu.constraint_name
13474
+ AND tc.table_schema = ccu.table_schema
13475
+ WHERE tc.constraint_type = 'FOREIGN KEY'
13476
+ AND tc.table_schema = 'public'
13477
+ `);
13478
+ if (fks.length > 0) {
13479
+ return topoSort(tables, fks);
13480
+ }
13481
+ } catch {}
13482
+ return heuristicOrder(tables);
13483
+ }
13484
+ function topoSort(tables, fks) {
13485
+ const tableSet = new Set(tables);
13486
+ const deps = new Map;
13487
+ for (const t of tables) {
13488
+ deps.set(t, new Set);
13489
+ }
13490
+ for (const fk of fks) {
13491
+ if (tableSet.has(fk.source_table) && tableSet.has(fk.referenced_table)) {
13492
+ deps.get(fk.source_table).add(fk.referenced_table);
13493
+ }
13494
+ }
13495
+ const sorted = [];
13496
+ const visited = new Set;
13497
+ const visiting = new Set;
13498
+ function visit(table) {
13499
+ if (visited.has(table))
13500
+ return;
13501
+ if (visiting.has(table)) {
13502
+ sorted.push(table);
13503
+ visited.add(table);
13504
+ return;
13505
+ }
13506
+ visiting.add(table);
13507
+ const tableDeps = deps.get(table) ?? new Set;
13508
+ for (const dep of tableDeps) {
13509
+ visit(dep);
13510
+ }
13511
+ visiting.delete(table);
13512
+ visited.add(table);
13513
+ sorted.push(table);
13514
+ }
13515
+ for (const t of tables) {
13516
+ visit(t);
13517
+ }
13518
+ return sorted;
13519
+ }
13520
+ function heuristicOrder(tables) {
13521
+ const sorted = [...tables].sort((a, b) => {
13522
+ const aIsChild = a.includes("_") && tables.some((t) => a.startsWith(t + "_") || a.endsWith("_" + t));
13523
+ const bIsChild = b.includes("_") && tables.some((t) => b.startsWith(t + "_") || b.endsWith("_" + t));
13524
+ if (aIsChild && !bIsChild)
13525
+ return 1;
13526
+ if (!aIsChild && bIsChild)
13527
+ return -1;
13528
+ return a.localeCompare(b);
13529
+ });
13530
+ return sorted;
13531
+ }
13532
+ function getSqlitePrimaryKeys(adapter, table) {
13533
+ try {
13534
+ const cols = adapter.all(`PRAGMA table_info("${table}")`);
13535
+ const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
13536
+ return pkCols;
13537
+ } catch {
13538
+ return [];
13539
+ }
13540
+ }
13541
+ async function getPgPrimaryKeys(adapter, table) {
13542
+ try {
13543
+ const rows = await adapter.all(`
13544
+ SELECT kcu.column_name, kcu.ordinal_position
13545
+ FROM information_schema.table_constraints tc
13546
+ JOIN information_schema.key_column_usage kcu
13547
+ ON tc.constraint_name = kcu.constraint_name
13548
+ AND tc.table_schema = kcu.table_schema
13549
+ WHERE tc.constraint_type = 'PRIMARY KEY'
13550
+ AND tc.table_schema = 'public'
13551
+ AND tc.table_name = '${table}'
13552
+ ORDER BY kcu.ordinal_position
13553
+ `);
13554
+ return rows.map((r) => r.column_name);
13555
+ } catch {
13556
+ return [];
13557
+ }
13558
+ }
13559
+ async function detectPrimaryKeys(adapter, table) {
13560
+ if (isAsyncAdapter(adapter)) {
13561
+ return getPgPrimaryKeys(adapter, table);
13562
+ }
13563
+ return getSqlitePrimaryKeys(adapter, table);
13564
+ }
13565
+ async function resolvePrimaryKeys(source, target, table, pkOption) {
13566
+ if (pkOption) {
13567
+ return Array.isArray(pkOption) ? pkOption : [pkOption];
13568
+ }
13569
+ let pks = await detectPrimaryKeys(source, table);
13570
+ if (pks.length === 0) {
13571
+ pks = await detectPrimaryKeys(target, table);
13572
+ }
13573
+ return pks;
13574
+ }
13575
+ function pgTypeToSqlite(pgType) {
13576
+ const t = pgType.toLowerCase();
13577
+ if (t.includes("int") || t === "bigint" || t === "smallint" || t === "serial" || t === "bigserial")
13578
+ return "INTEGER";
13579
+ if (t.includes("bool"))
13580
+ return "INTEGER";
13581
+ if (t.includes("float") || t.includes("double") || t === "real" || t === "numeric" || t === "decimal")
13582
+ return "REAL";
13583
+ if (t === "bytea")
13584
+ return "BLOB";
13585
+ return "TEXT";
13586
+ }
13587
+ async function ensureTableInSqliteFromPg(target, source, table) {
13588
+ const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
13589
+ if (existing.length > 0)
13590
+ return false;
13591
+ const cols = await source.all(`SELECT column_name, data_type, is_nullable, column_default
13592
+ FROM information_schema.columns
13593
+ WHERE table_schema = 'public' AND table_name = '${table}'
13594
+ ORDER BY ordinal_position`);
13595
+ if (cols.length === 0)
13596
+ return false;
13597
+ const pkCols = await source.all(`SELECT kcu.column_name
13598
+ FROM information_schema.table_constraints tc
13599
+ JOIN information_schema.key_column_usage kcu
13600
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
13601
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = '${table}'
13602
+ ORDER BY kcu.ordinal_position`);
13603
+ const pkSet = new Set(pkCols.map((c) => c.column_name));
13604
+ const skipTypes = new Set(["tsvector", "tsquery", "user-defined"]);
13605
+ const filteredCols = cols.filter((c) => !skipTypes.has(c.data_type));
13606
+ const colDefs = filteredCols.map((c) => {
13607
+ const sqliteType = pgTypeToSqlite(c.data_type);
13608
+ const notNull = c.is_nullable === "NO" && !pkSet.has(c.column_name) ? " NOT NULL" : "";
13609
+ return `"${c.column_name}" ${sqliteType}${notNull}`;
13610
+ });
13611
+ if (pkSet.size > 0) {
13612
+ const pkList = [...pkSet].map((c) => `"${c}"`).join(", ");
13613
+ colDefs.push(`PRIMARY KEY (${pkList})`);
13614
+ }
13615
+ const sql = `CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`;
13616
+ target.exec(sql);
13617
+ process.stderr.write(` [sync] ${table}: auto-created in SQLite from PG schema
13618
+ `);
13619
+ return true;
13620
+ }
13621
+ async function ensureTablesExist(source, target, tables) {
13622
+ for (const table of tables) {
13623
+ if (!isAsyncAdapter(target) && isAsyncAdapter(source)) {
13624
+ await ensureTableInSqliteFromPg(target, source, table);
13625
+ }
13626
+ }
13627
+ }
13628
+ async function filterColumnsForTarget(target, table, sourceColumns) {
13629
+ try {
13630
+ if (!isAsyncAdapter(target)) {
13631
+ const colInfo = target.all(`PRAGMA table_info("${table}")`);
13632
+ if (Array.isArray(colInfo) && colInfo.length > 0) {
13633
+ const targetCols = new Set(colInfo.map((c) => c.name));
13634
+ const filtered = sourceColumns.filter((c) => targetCols.has(c));
13635
+ if (filtered.length < sourceColumns.length) {
13636
+ const dropped = sourceColumns.filter((c) => !targetCols.has(c));
13637
+ process.stderr.write(` [sync] ${table}: dropping ${dropped.length} columns not in target: ${dropped.join(", ")}
13638
+ `);
13639
+ }
13640
+ return filtered;
13641
+ }
13642
+ } else {
13643
+ const colInfo = await target.all(`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${table}'`);
13644
+ if (colInfo.length > 0) {
13645
+ const targetCols = new Set(colInfo.map((c) => c.column_name));
13646
+ return sourceColumns.filter((c) => targetCols.has(c));
13647
+ }
13648
+ }
13649
+ } catch {}
13650
+ return sourceColumns;
13651
+ }
13652
+ async function syncTransfer(source, target, options, _direction) {
13653
+ const {
13654
+ tables,
13655
+ onProgress,
13656
+ batchSize = 100,
13657
+ conflictColumn = "updated_at",
13658
+ primaryKey: pkOption
13659
+ } = options;
13660
+ const results = [];
13661
+ const sqliteTarget = !isAsyncAdapter(target) ? target : null;
13662
+ await ensureTablesExist(source, target, tables);
13663
+ if (sqliteTarget) {
13664
+ try {
13665
+ sqliteTarget.exec("PRAGMA foreign_keys = OFF");
13666
+ } catch {}
13667
+ }
13668
+ try {
13669
+ for (let i = 0;i < tables.length; i++) {
13670
+ const table = tables[i];
13671
+ const result = {
13672
+ table,
13673
+ rowsRead: 0,
13674
+ rowsWritten: 0,
13675
+ rowsSkipped: 0,
13676
+ errors: []
13677
+ };
13678
+ try {
13679
+ onProgress?.({
13680
+ table,
13681
+ phase: "reading",
13682
+ rowsRead: 0,
13683
+ rowsWritten: 0,
13684
+ totalTables: tables.length,
13685
+ currentTableIndex: i
13686
+ });
13687
+ const rows = await readAll(source, `SELECT * FROM "${table}"`);
13688
+ result.rowsRead = rows.length;
13689
+ if (rows.length === 0) {
13690
+ onProgress?.({
13691
+ table,
13692
+ phase: "done",
13693
+ rowsRead: 0,
13694
+ rowsWritten: 0,
13695
+ totalTables: tables.length,
13696
+ currentTableIndex: i
13697
+ });
13698
+ results.push(result);
13699
+ continue;
13700
+ }
13701
+ const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
13702
+ const sourceColumns = Object.keys(rows[0]);
13703
+ const columns = await filterColumnsForTarget(target, table, sourceColumns);
13704
+ if (pkColumns.length === 0) {
13705
+ result.errors.push(`Table "${table}" has no primary key \u2014 inserting without conflict handling`);
13706
+ onProgress?.({
13707
+ table,
13708
+ phase: "writing",
13709
+ rowsRead: result.rowsRead,
13710
+ rowsWritten: 0,
13711
+ totalTables: tables.length,
13712
+ currentTableIndex: i
13713
+ });
13714
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
13715
+ const batch = rows.slice(offset, offset + batchSize);
13716
+ try {
13717
+ if (isAsyncAdapter(target)) {
13718
+ await batchInsertPg(target, table, columns, batch);
13719
+ } else {
13720
+ batchInsertSqlite(target, table, columns, batch);
13721
+ }
13722
+ result.rowsWritten += batch.length;
13723
+ } catch (err) {
13724
+ result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
13725
+ }
13726
+ }
13727
+ onProgress?.({
13728
+ table,
13729
+ phase: "done",
13730
+ rowsRead: result.rowsRead,
13731
+ rowsWritten: result.rowsWritten,
13732
+ totalTables: tables.length,
13733
+ currentTableIndex: i
13734
+ });
13735
+ results.push(result);
13736
+ continue;
13737
+ }
13738
+ const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
13739
+ if (missingPks.length > 0) {
13740
+ result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} \u2014 skipping`);
13741
+ results.push(result);
13742
+ continue;
13743
+ }
13744
+ onProgress?.({
13745
+ table,
13746
+ phase: "writing",
13747
+ rowsRead: result.rowsRead,
13748
+ rowsWritten: 0,
13749
+ totalTables: tables.length,
13750
+ currentTableIndex: i
13751
+ });
13752
+ const updateCols = columns.filter((c) => !pkColumns.includes(c));
13753
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
13754
+ const batch = rows.slice(offset, offset + batchSize);
13755
+ try {
13756
+ if (isAsyncAdapter(target)) {
13757
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
13758
+ } else {
13759
+ batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
13760
+ }
13761
+ result.rowsWritten += batch.length;
13762
+ } catch (err) {
13763
+ result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
13764
+ }
13765
+ onProgress?.({
13766
+ table,
13767
+ phase: "writing",
13768
+ rowsRead: result.rowsRead,
13769
+ rowsWritten: result.rowsWritten,
13770
+ totalTables: tables.length,
13771
+ currentTableIndex: i
13772
+ });
13773
+ }
13774
+ onProgress?.({
13775
+ table,
13776
+ phase: "done",
13777
+ rowsRead: result.rowsRead,
13778
+ rowsWritten: result.rowsWritten,
13779
+ totalTables: tables.length,
13780
+ currentTableIndex: i
13781
+ });
13782
+ } catch (err) {
13783
+ result.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
13784
+ }
13785
+ results.push(result);
13786
+ }
13787
+ } finally {
13788
+ if (sqliteTarget) {
13789
+ try {
13790
+ sqliteTarget.exec("PRAGMA foreign_keys = ON");
13791
+ } catch {}
13792
+ try {
13793
+ const violations = sqliteTarget.all("PRAGMA foreign_key_check");
13794
+ if (violations.length > 0) {
13795
+ const tables2 = [...new Set(violations.map((v) => v.table))];
13796
+ const msg = `FK integrity check: ${violations.length} violation(s) in table(s): ${tables2.join(", ")}`;
13797
+ if (results.length > 0) {
13798
+ results[results.length - 1].errors.push(msg);
13799
+ }
13800
+ }
13801
+ } catch {}
13802
+ }
13803
+ }
13804
+ return results;
13805
+ }
13806
+ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
13807
+ if (batch.length === 0)
13808
+ return;
13809
+ const colList = columns.map((c) => `"${c}"`).join(", ");
13810
+ const valuePlaceholders = batch.map((_, rowIdx) => {
13811
+ const offset = rowIdx * columns.length;
13812
+ return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
13813
+ }).join(", ");
13814
+ const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
13815
+ const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
13816
+ const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
13817
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
13818
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
13819
+ const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
13820
+ await target.run(sql, ...params);
13821
+ }
13822
+ function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
13823
+ if (batch.length === 0)
13824
+ return;
13825
+ const colList = columns.map((c) => `"${c}"`).join(", ");
13826
+ const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
13827
+ const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
13828
+ const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
13829
+ const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
13830
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
13831
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
13832
+ const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
13833
+ target.run(sql, ...params);
13834
+ }
13835
+ async function batchInsertPg(target, table, columns, batch) {
13836
+ if (batch.length === 0)
13837
+ return;
13838
+ const colList = columns.map((c) => `"${c}"`).join(", ");
13839
+ const valuePlaceholders = batch.map((_, rowIdx) => {
13840
+ const offset = rowIdx * columns.length;
13841
+ return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
13842
+ }).join(", ");
13843
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
13844
+ const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
13845
+ await target.run(sql, ...params);
13846
+ }
13847
+ function batchInsertSqlite(target, table, columns, batch) {
13848
+ if (batch.length === 0)
13849
+ return;
13850
+ const colList = columns.map((c) => `"${c}"`).join(", ");
13851
+ const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
13852
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
13853
+ const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
13854
+ target.run(sql, ...params);
13855
+ }
13856
+ function coerceForSqlite(value) {
13857
+ if (value === null || value === undefined)
13858
+ return null;
13859
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
13860
+ return value;
13861
+ if (value instanceof Date)
13862
+ return value.toISOString();
13863
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
13864
+ return value;
13865
+ if (typeof value === "object")
13866
+ return JSON.stringify(value);
13867
+ return String(value);
13868
+ }
13869
+ function isAsyncAdapter(adapter) {
13870
+ return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
13871
+ }
13872
+ async function readAll(adapter, sql) {
13873
+ const result = adapter.all(sql);
13874
+ return result instanceof Promise ? await result : result;
13875
+ }
13876
+ function listSqliteTables(db) {
13877
+ const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
13878
+ return rows.map((r) => r.name);
13879
+ }
13378
13880
  init_config();
13881
+ var FEEDBACK_TABLE_SQL = `
13882
+ CREATE TABLE IF NOT EXISTS feedback (
13883
+ id TEXT PRIMARY KEY,
13884
+ service TEXT NOT NULL,
13885
+ version TEXT DEFAULT '',
13886
+ message TEXT NOT NULL,
13887
+ email TEXT DEFAULT '',
13888
+ machine_id TEXT DEFAULT '',
13889
+ created_at TEXT DEFAULT (datetime('now'))
13890
+ )`;
13891
+ function ensureFeedbackTable(db) {
13892
+ db.exec(FEEDBACK_TABLE_SQL);
13893
+ }
13894
+ function saveFeedback(db, feedback) {
13895
+ ensureFeedbackTable(db);
13896
+ const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
13897
+ const now = new Date().toISOString();
13898
+ const machineId = feedback.machine_id ?? hostname();
13899
+ db.run(`INSERT INTO feedback (id, service, version, message, email, machine_id, created_at)
13900
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now);
13901
+ return id;
13902
+ }
13903
+ async function sendFeedback(feedback, db) {
13904
+ const config = getCloudConfig();
13905
+ const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
13906
+ const machineId = feedback.machine_id ?? hostname();
13907
+ const now = new Date().toISOString();
13908
+ const payload = {
13909
+ id,
13910
+ service: feedback.service,
13911
+ version: feedback.version ?? "",
13912
+ message: feedback.message,
13913
+ email: feedback.email ?? "",
13914
+ machine_id: machineId,
13915
+ created_at: feedback.created_at ?? now
13916
+ };
13917
+ try {
13918
+ const res = await fetch(config.feedback_endpoint, {
13919
+ method: "POST",
13920
+ headers: { "Content-Type": "application/json" },
13921
+ body: JSON.stringify(payload),
13922
+ signal: AbortSignal.timeout(1e4)
13923
+ });
13924
+ if (!res.ok) {
13925
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
13926
+ }
13927
+ if (db) {
13928
+ try {
13929
+ saveFeedback(db, { ...feedback, id });
13930
+ } catch {}
13931
+ }
13932
+ return { sent: true, id };
13933
+ } catch (err) {
13934
+ const errorMsg = err?.message ?? String(err);
13935
+ if (db) {
13936
+ try {
13937
+ saveFeedback(db, { ...feedback, id });
13938
+ } catch {}
13939
+ }
13940
+ return { sent: false, id, error: errorMsg };
13941
+ }
13942
+ }
13379
13943
  init_dotfile();
13380
13944
 
13381
13945
  class SyncProgressTracker {
@@ -13508,6 +14072,42 @@ init_config();
13508
14072
  var CONFIG_DIR2 = join6(homedir5(), ".hasna", "cloud");
13509
14073
  init_adapter();
13510
14074
  init_config();
14075
+ async function applyPgMigrations(connectionString, migrations, service = "unknown") {
14076
+ const pg2 = new PgAdapterAsync(connectionString);
14077
+ const result = {
14078
+ service,
14079
+ applied: [],
14080
+ alreadyApplied: [],
14081
+ errors: [],
14082
+ totalMigrations: migrations.length
14083
+ };
14084
+ try {
14085
+ await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
14086
+ id SERIAL PRIMARY KEY,
14087
+ version INT UNIQUE NOT NULL,
14088
+ applied_at TIMESTAMPTZ DEFAULT NOW()
14089
+ )`);
14090
+ const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
14091
+ const appliedSet = new Set(applied.map((r) => r.version));
14092
+ for (let i = 0;i < migrations.length; i++) {
14093
+ if (appliedSet.has(i)) {
14094
+ result.alreadyApplied.push(i);
14095
+ continue;
14096
+ }
14097
+ try {
14098
+ await pg2.exec(migrations[i]);
14099
+ await pg2.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
14100
+ result.applied.push(i);
14101
+ } catch (err) {
14102
+ result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
14103
+ break;
14104
+ }
14105
+ }
14106
+ } finally {
14107
+ await pg2.close();
14108
+ }
14109
+ return result;
14110
+ }
13511
14111
  init_discover();
13512
14112
  init_zod();
13513
14113
  init_config();
@@ -13518,847 +14118,801 @@ init_dotfile();
13518
14118
  init_adapter();
13519
14119
 
13520
14120
  // src/db/database.ts
13521
- import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
13522
- import { dirname as dirname2, join as join5 } from "path";
14121
+ function defaultDatabasePath() {
14122
+ const baseDir = join5(process.env.HOME ?? ".", ".hasna", "invoices");
14123
+ mkdirSync3(baseDir, { recursive: true });
14124
+ return join5(baseDir, "invoices.db");
14125
+ }
14126
+ function openInvoiceDatabase(options = {}) {
14127
+ const dbPath = options.dbPath ?? defaultDatabasePath();
14128
+ mkdirSync3(dirname2(dbPath), { recursive: true });
14129
+ const db = new SqliteAdapter(dbPath);
14130
+ db.exec("PRAGMA journal_mode = WAL;");
14131
+ db.exec("PRAGMA foreign_keys = ON;");
14132
+ db.exec("PRAGMA busy_timeout = 5000;");
14133
+ return db;
14134
+ }
13523
14135
 
13524
- // src/db/migrations.ts
14136
+ // src/db/schema.ts
13525
14137
  var MIGRATIONS = [
13526
14138
  {
13527
- id: 1,
13528
- name: "initial_schema",
14139
+ id: "0001_init",
13529
14140
  sql: `
13530
- CREATE TABLE IF NOT EXISTS clients (
13531
- id TEXT PRIMARY KEY,
13532
- name TEXT NOT NULL,
13533
- email TEXT,
13534
- phone TEXT,
13535
- address TEXT,
13536
- tax_id TEXT,
13537
- notes TEXT,
13538
- metadata TEXT NOT NULL DEFAULT '{}',
13539
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
13540
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
13541
- );
13542
-
13543
- CREATE TABLE IF NOT EXISTS invoices (
13544
- id TEXT PRIMARY KEY,
13545
- invoice_number TEXT NOT NULL UNIQUE,
13546
- client_id TEXT REFERENCES clients(id) ON DELETE SET NULL,
13547
- status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'sent', 'paid', 'overdue', 'cancelled', 'refunded')),
13548
- issue_date TEXT NOT NULL DEFAULT (date('now')),
13549
- due_date TEXT,
13550
- currency TEXT NOT NULL DEFAULT 'USD',
13551
- subtotal REAL NOT NULL DEFAULT 0,
13552
- tax_rate REAL NOT NULL DEFAULT 0,
13553
- tax_amount REAL NOT NULL DEFAULT 0,
13554
- discount REAL NOT NULL DEFAULT 0,
13555
- total REAL NOT NULL DEFAULT 0,
13556
- notes TEXT,
13557
- metadata TEXT NOT NULL DEFAULT '{}',
13558
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
13559
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
13560
- paid_at TEXT
13561
- );
14141
+ CREATE TABLE IF NOT EXISTS migrations (
14142
+ id TEXT PRIMARY KEY,
14143
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
14144
+ );
13562
14145
 
13563
- CREATE TABLE IF NOT EXISTS line_items (
13564
- id TEXT PRIMARY KEY,
13565
- invoice_id TEXT NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
13566
- description TEXT NOT NULL,
13567
- quantity REAL NOT NULL DEFAULT 1,
13568
- unit_price REAL NOT NULL DEFAULT 0,
13569
- amount REAL NOT NULL DEFAULT 0,
13570
- sort_order INTEGER NOT NULL DEFAULT 0,
13571
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
13572
- );
14146
+ CREATE TABLE IF NOT EXISTS parties (
14147
+ id TEXT PRIMARY KEY,
14148
+ kind TEXT NOT NULL CHECK(kind IN ('issuer', 'customer')),
14149
+ legal_name TEXT NOT NULL,
14150
+ email TEXT,
14151
+ tax_id TEXT,
14152
+ country TEXT,
14153
+ address TEXT,
14154
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
14155
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
14156
+ );
13573
14157
 
13574
- CREATE TABLE IF NOT EXISTS payments (
13575
- id TEXT PRIMARY KEY,
13576
- invoice_id TEXT NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
13577
- amount REAL NOT NULL,
13578
- method TEXT,
13579
- reference TEXT,
13580
- notes TEXT,
13581
- paid_at TEXT NOT NULL DEFAULT (datetime('now')),
13582
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
13583
- );
14158
+ CREATE TABLE IF NOT EXISTS invoices (
14159
+ id TEXT PRIMARY KEY,
14160
+ number TEXT NOT NULL UNIQUE,
14161
+ issuer_id TEXT NOT NULL REFERENCES parties(id),
14162
+ customer_id TEXT NOT NULL REFERENCES parties(id),
14163
+ status TEXT NOT NULL CHECK(status IN ('draft', 'sent', 'partially_paid', 'paid', 'void', 'overdue')),
14164
+ currency TEXT NOT NULL,
14165
+ notes TEXT,
14166
+ issued_at TEXT NOT NULL,
14167
+ due_at TEXT,
14168
+ subtotal_cents INTEGER NOT NULL DEFAULT 0,
14169
+ tax_cents INTEGER NOT NULL DEFAULT 0,
14170
+ total_cents INTEGER NOT NULL DEFAULT 0,
14171
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
14172
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
14173
+ );
13584
14174
 
13585
- CREATE TABLE IF NOT EXISTS invoice_counter (
13586
- id INTEGER PRIMARY KEY CHECK (id = 1),
13587
- prefix TEXT NOT NULL DEFAULT 'INV',
13588
- next_number INTEGER NOT NULL DEFAULT 1
13589
- );
14175
+ CREATE TABLE IF NOT EXISTS invoice_lines (
14176
+ id TEXT PRIMARY KEY,
14177
+ invoice_id TEXT NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
14178
+ position INTEGER NOT NULL,
14179
+ description TEXT NOT NULL,
14180
+ quantity REAL NOT NULL DEFAULT 1,
14181
+ unit_price_cents INTEGER NOT NULL DEFAULT 0,
14182
+ tax_rate_basis_points INTEGER NOT NULL DEFAULT 0,
14183
+ line_total_cents INTEGER NOT NULL DEFAULT 0,
14184
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
14185
+ );
13590
14186
 
13591
- INSERT OR IGNORE INTO invoice_counter (id, prefix, next_number) VALUES (1, 'INV', 1);
14187
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_invoice_lines_invoice_position
14188
+ ON invoice_lines(invoice_id, position);
13592
14189
 
13593
- CREATE INDEX IF NOT EXISTS idx_invoices_client ON invoices(client_id);
13594
- CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
13595
- CREATE INDEX IF NOT EXISTS idx_invoices_number ON invoices(invoice_number);
13596
- CREATE INDEX IF NOT EXISTS idx_invoices_due ON invoices(due_date);
13597
- CREATE INDEX IF NOT EXISTS idx_line_items_invoice ON line_items(invoice_id);
13598
- CREATE INDEX IF NOT EXISTS idx_payments_invoice ON payments(invoice_id);
13599
- CREATE INDEX IF NOT EXISTS idx_clients_name ON clients(name);
13600
- CREATE INDEX IF NOT EXISTS idx_clients_email ON clients(email);
13601
- `
13602
- },
13603
- {
13604
- id: 2,
13605
- name: "multi_country_support",
13606
- sql: `
13607
- CREATE TABLE IF NOT EXISTS business_profiles (
13608
- id TEXT PRIMARY KEY,
13609
- name TEXT NOT NULL,
13610
- address_line1 TEXT,
13611
- address_line2 TEXT,
13612
- city TEXT,
13613
- state TEXT,
13614
- postal_code TEXT,
13615
- country TEXT NOT NULL DEFAULT 'US',
13616
- tax_id TEXT,
13617
- vat_number TEXT,
13618
- registration_number TEXT,
13619
- email TEXT,
13620
- phone TEXT,
13621
- website TEXT,
13622
- bank_name TEXT,
13623
- bank_iban TEXT,
13624
- bank_swift TEXT,
13625
- bank_account TEXT,
13626
- logo_url TEXT,
13627
- is_default INTEGER NOT NULL DEFAULT 0,
13628
- metadata TEXT NOT NULL DEFAULT '{}',
13629
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
13630
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
13631
- );
14190
+ CREATE INDEX IF NOT EXISTS idx_invoices_number ON invoices(number);
14191
+ CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
14192
+ CREATE INDEX IF NOT EXISTS idx_invoices_issued_at ON invoices(issued_at);
14193
+ CREATE INDEX IF NOT EXISTS idx_invoices_due_at ON invoices(due_at);
13632
14194
 
13633
- CREATE TABLE IF NOT EXISTS tax_rules (
13634
- id TEXT PRIMARY KEY,
13635
- country TEXT NOT NULL,
13636
- region TEXT,
13637
- tax_name TEXT NOT NULL,
13638
- rate REAL NOT NULL,
13639
- type TEXT NOT NULL DEFAULT 'vat' CHECK(type IN ('vat', 'sales_tax', 'gst', 'other')),
13640
- is_default INTEGER NOT NULL DEFAULT 0,
13641
- reverse_charge INTEGER NOT NULL DEFAULT 0,
13642
- description TEXT,
13643
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
13644
- );
14195
+ CREATE VIRTUAL TABLE IF NOT EXISTS invoices_fts USING fts5(
14196
+ invoice_id UNINDEXED,
14197
+ number,
14198
+ issuer_name,
14199
+ customer_name,
14200
+ notes
14201
+ );
13645
14202
 
13646
- -- Add new columns to invoices
13647
- ALTER TABLE invoices ADD COLUMN business_profile_id TEXT REFERENCES business_profiles(id) ON DELETE SET NULL;
13648
- ALTER TABLE invoices ADD COLUMN tax_name TEXT DEFAULT 'Tax';
13649
- ALTER TABLE invoices ADD COLUMN reverse_charge INTEGER NOT NULL DEFAULT 0;
13650
- ALTER TABLE invoices ADD COLUMN language TEXT NOT NULL DEFAULT 'en';
13651
- ALTER TABLE invoices ADD COLUMN footer_text TEXT;
14203
+ CREATE TRIGGER IF NOT EXISTS invoices_ai_fts
14204
+ AFTER INSERT ON invoices
14205
+ BEGIN
14206
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
14207
+ VALUES (
14208
+ NEW.id,
14209
+ NEW.number,
14210
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.issuer_id), ''),
14211
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.customer_id), ''),
14212
+ COALESCE(NEW.notes, '')
14213
+ );
14214
+ END;
13652
14215
 
13653
- -- Add new columns to clients
13654
- ALTER TABLE clients ADD COLUMN address_line1 TEXT;
13655
- ALTER TABLE clients ADD COLUMN address_line2 TEXT;
13656
- ALTER TABLE clients ADD COLUMN city TEXT;
13657
- ALTER TABLE clients ADD COLUMN state TEXT;
13658
- ALTER TABLE clients ADD COLUMN postal_code TEXT;
13659
- ALTER TABLE clients ADD COLUMN country TEXT DEFAULT 'US';
13660
- ALTER TABLE clients ADD COLUMN vat_number TEXT;
13661
- ALTER TABLE clients ADD COLUMN language TEXT DEFAULT 'en';
14216
+ CREATE TRIGGER IF NOT EXISTS invoices_au_fts
14217
+ AFTER UPDATE ON invoices
14218
+ BEGIN
14219
+ DELETE FROM invoices_fts WHERE invoice_id = OLD.id;
14220
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
14221
+ VALUES (
14222
+ NEW.id,
14223
+ NEW.number,
14224
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.issuer_id), ''),
14225
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.customer_id), ''),
14226
+ COALESCE(NEW.notes, '')
14227
+ );
14228
+ END;
13662
14229
 
13663
- -- Add per-line-item tax support
13664
- ALTER TABLE line_items ADD COLUMN tax_rate REAL;
13665
- ALTER TABLE line_items ADD COLUMN tax_amount REAL DEFAULT 0;
14230
+ CREATE TRIGGER IF NOT EXISTS invoices_ad_fts
14231
+ AFTER DELETE ON invoices
14232
+ BEGIN
14233
+ DELETE FROM invoices_fts WHERE invoice_id = OLD.id;
14234
+ END;
14235
+ `
14236
+ },
14237
+ {
14238
+ id: "0002_party_fts_sync",
14239
+ sql: `
14240
+ CREATE TRIGGER IF NOT EXISTS parties_au_invoice_fts
14241
+ AFTER UPDATE ON parties
14242
+ BEGIN
14243
+ DELETE FROM invoices_fts
14244
+ WHERE invoice_id IN (
14245
+ SELECT id FROM invoices WHERE issuer_id = NEW.id OR customer_id = NEW.id
14246
+ );
13666
14247
 
13667
- -- Seed default tax rules
13668
- INSERT INTO tax_rules (id, country, tax_name, rate, type, is_default, description) VALUES
13669
- ('tax-ro-vat-19', 'RO', 'TVA', 19, 'vat', 1, 'Romania standard VAT 19%'),
13670
- ('tax-ro-vat-9', 'RO', 'TVA', 9, 'vat', 0, 'Romania reduced VAT 9% (food, hotels)'),
13671
- ('tax-ro-vat-5', 'RO', 'TVA', 5, 'vat', 0, 'Romania reduced VAT 5% (housing)'),
13672
- ('tax-us-none', 'US', 'Sales Tax', 0, 'sales_tax', 1, 'US federal (no federal sales tax)'),
13673
- ('tax-us-ca', 'US', 'Sales Tax', 7.25, 'sales_tax', 0, 'California base sales tax'),
13674
- ('tax-us-ny', 'US', 'Sales Tax', 8, 'sales_tax', 0, 'New York sales tax'),
13675
- ('tax-us-tx', 'US', 'Sales Tax', 6.25, 'sales_tax', 0, 'Texas sales tax'),
13676
- ('tax-uk-vat-20', 'GB', 'VAT', 20, 'vat', 1, 'UK standard VAT 20%'),
13677
- ('tax-uk-vat-5', 'GB', 'VAT', 5, 'vat', 0, 'UK reduced VAT 5%'),
13678
- ('tax-uk-vat-0', 'GB', 'VAT', 0, 'vat', 0, 'UK zero-rated VAT'),
13679
- ('tax-de-vat-19', 'DE', 'MwSt', 19, 'vat', 1, 'Germany standard VAT 19%'),
13680
- ('tax-de-vat-7', 'DE', 'MwSt', 7, 'vat', 0, 'Germany reduced VAT 7%'),
13681
- ('tax-fr-vat-20', 'FR', 'TVA', 20, 'vat', 1, 'France standard VAT 20%'),
13682
- ('tax-fr-vat-10', 'FR', 'TVA', 10, 'vat', 0, 'France reduced VAT 10%'),
13683
- ('tax-fr-vat-55', 'FR', 'TVA', 5.5, 'vat', 0, 'France reduced VAT 5.5%'),
13684
- ('tax-nl-vat-21', 'NL', 'BTW', 21, 'vat', 1, 'Netherlands standard VAT 21%'),
13685
- ('tax-it-vat-22', 'IT', 'IVA', 22, 'vat', 1, 'Italy standard VAT 22%'),
13686
- ('tax-es-vat-21', 'ES', 'IVA', 21, 'vat', 1, 'Spain standard VAT 21%'),
13687
- ('tax-at-vat-20', 'AT', 'USt', 20, 'vat', 1, 'Austria standard VAT 20%'),
13688
- ('tax-be-vat-21', 'BE', 'BTW', 21, 'vat', 1, 'Belgium standard VAT 21%'),
13689
- ('tax-pl-vat-23', 'PL', 'VAT', 23, 'vat', 1, 'Poland standard VAT 23%'),
13690
- ('tax-ie-vat-23', 'IE', 'VAT', 23, 'vat', 1, 'Ireland standard VAT 23%'),
13691
- ('tax-se-vat-25', 'SE', 'Moms', 25, 'vat', 1, 'Sweden standard VAT 25%'),
13692
- ('tax-dk-vat-25', 'DK', 'Moms', 25, 'vat', 1, 'Denmark standard VAT 25%'),
13693
- ('tax-hu-vat-27', 'HU', 'AFA', 27, 'vat', 1, 'Hungary standard VAT 27%'),
13694
- ('tax-bg-vat-20', 'BG', 'DDC', 20, 'vat', 1, 'Bulgaria standard VAT 20%'),
13695
- ('tax-eu-rc', 'EU', 'Reverse Charge', 0, 'vat', 0, 'EU B2B reverse charge mechanism');
14248
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
14249
+ SELECT
14250
+ i.id,
14251
+ i.number,
14252
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.issuer_id), ''),
14253
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.customer_id), ''),
14254
+ COALESCE(i.notes, '')
14255
+ FROM invoices i
14256
+ WHERE i.issuer_id = NEW.id OR i.customer_id = NEW.id;
14257
+ END;
13696
14258
 
13697
- CREATE INDEX IF NOT EXISTS idx_business_profiles_default ON business_profiles(is_default);
13698
- CREATE INDEX IF NOT EXISTS idx_tax_rules_country ON tax_rules(country);
13699
- CREATE INDEX IF NOT EXISTS idx_invoices_business ON invoices(business_profile_id);
13700
- `
14259
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
14260
+ SELECT
14261
+ i.id,
14262
+ i.number,
14263
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.issuer_id), ''),
14264
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.customer_id), ''),
14265
+ COALESCE(i.notes, '')
14266
+ FROM invoices i
14267
+ WHERE i.id NOT IN (SELECT invoice_id FROM invoices_fts);
14268
+ `
14269
+ },
14270
+ {
14271
+ id: "0003_agents",
14272
+ sql: `
14273
+ CREATE TABLE IF NOT EXISTS agents (
14274
+ id TEXT PRIMARY KEY,
14275
+ name TEXT NOT NULL UNIQUE,
14276
+ description TEXT,
14277
+ focus TEXT,
14278
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
14279
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
14280
+ );
14281
+ `
13701
14282
  }
13702
14283
  ];
13703
14284
 
13704
- // src/db/database.ts
13705
- var _db = null;
13706
- function getDbPath2() {
13707
- const explicit = process.env["HASNA_INVOICES_DIR"] ?? process.env["INVOICES_DIR"];
13708
- if (explicit) {
13709
- return join5(explicit, "invoices.db");
13710
- }
13711
- const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "~";
13712
- return join5(home, ".hasna", "invoices", "invoices.db");
13713
- }
13714
- function getDatabase() {
13715
- if (_db)
13716
- return _db;
13717
- const dbPath = getDbPath2();
13718
- const dir = dirname2(dbPath);
13719
- if (!existsSync4(dir)) {
13720
- mkdirSync3(dir, { recursive: true });
13721
- }
13722
- const adapter = new SqliteAdapter(dbPath);
13723
- _db = adapter.raw;
13724
- _db.exec(`
13725
- CREATE TABLE IF NOT EXISTS _migrations (
13726
- id INTEGER PRIMARY KEY,
13727
- name TEXT NOT NULL,
13728
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
13729
- )
13730
- `);
13731
- const applied = _db.query("SELECT id FROM _migrations ORDER BY id").all();
13732
- const appliedIds = new Set(applied.map((r) => r.id));
14285
+ // src/db/migrate.ts
14286
+ function ensureMigrationsTable(db) {
14287
+ db.exec(`
14288
+ CREATE TABLE IF NOT EXISTS migrations (
14289
+ id TEXT PRIMARY KEY,
14290
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
14291
+ );
14292
+ `);
14293
+ }
14294
+ function applyMigrations(db) {
14295
+ ensureMigrationsTable(db);
14296
+ const getMigration = db.query("SELECT id FROM migrations WHERE id = ?1");
14297
+ const markMigration = db.query("INSERT INTO migrations (id) VALUES (?1)");
14298
+ const applied = [];
13733
14299
  for (const migration of MIGRATIONS) {
13734
- if (appliedIds.has(migration.id))
14300
+ const row = getMigration.get(migration.id);
14301
+ if (row) {
13735
14302
  continue;
13736
- _db.exec("BEGIN");
13737
- try {
13738
- _db.exec(migration.sql);
13739
- _db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(migration.id, migration.name);
13740
- _db.exec("COMMIT");
13741
- } catch (error) {
13742
- _db.exec("ROLLBACK");
13743
- throw new Error(`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`);
13744
14303
  }
14304
+ db.transaction(() => {
14305
+ db.exec(migration.sql);
14306
+ markMigration.run(migration.id);
14307
+ });
14308
+ applied.push(migration.id);
14309
+ }
14310
+ return applied;
14311
+ }
14312
+ function migrateDatabase(options = {}) {
14313
+ const db = openInvoiceDatabase(options);
14314
+ try {
14315
+ return applyMigrations(db);
14316
+ } finally {
14317
+ db.close();
13745
14318
  }
13746
- return _db;
13747
14319
  }
13748
14320
 
13749
- // src/db/invoices.ts
13750
- function rowToInvoice(row) {
13751
- return {
13752
- ...row,
13753
- reverse_charge: row.reverse_charge === 1,
13754
- tax_name: row["tax_name"] || "Tax",
13755
- language: row["language"] || "en",
13756
- footer_text: row["footer_text"] ?? null,
13757
- business_profile_id: row["business_profile_id"] ?? null,
13758
- metadata: JSON.parse(row.metadata || "{}")
13759
- };
13760
- }
13761
- function nextInvoiceNumber() {
13762
- const db = getDatabase();
13763
- const counter = db.prepare("SELECT prefix, next_number FROM invoice_counter WHERE id = 1").get();
13764
- const number = `${counter.prefix}-${String(counter.next_number).padStart(5, "0")}`;
13765
- db.prepare("UPDATE invoice_counter SET next_number = next_number + 1 WHERE id = 1").run();
13766
- return number;
13767
- }
13768
- function recalculateInvoice(invoiceId) {
13769
- const db = getDatabase();
13770
- const row = db.prepare("SELECT COALESCE(SUM(amount), 0) as subtotal FROM line_items WHERE invoice_id = ?").get(invoiceId);
13771
- const invoice = db.prepare("SELECT tax_rate, discount FROM invoices WHERE id = ?").get(invoiceId);
13772
- const subtotal = row.subtotal;
13773
- const discounted = subtotal - (invoice.discount || 0);
13774
- const taxAmount = discounted * ((invoice.tax_rate || 0) / 100);
13775
- const total = discounted + taxAmount;
13776
- db.prepare("UPDATE invoices SET subtotal = ?, tax_amount = ?, total = ?, updated_at = datetime('now') WHERE id = ?").run(subtotal, taxAmount, total, invoiceId);
13777
- }
13778
- function createInvoice(input = {}) {
13779
- const db = getDatabase();
13780
- const id = crypto.randomUUID();
13781
- const invoiceNumber = input.invoice_number || nextInvoiceNumber();
13782
- db.prepare(`INSERT INTO invoices (id, invoice_number, client_id, business_profile_id, issue_date, due_date, currency, tax_rate, tax_name, discount, reverse_charge, language, notes, footer_text)
13783
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, invoiceNumber, input.client_id || null, input.business_profile_id || null, input.issue_date ?? new Date().toISOString().split("T")[0], input.due_date ?? null, input.currency || "USD", input.tax_rate || 0, input.tax_name || "Tax", input.discount || 0, input.reverse_charge ? 1 : 0, input.language || "en", input.notes || null, input.footer_text || null);
13784
- return getInvoice(id);
13785
- }
13786
- function getInvoice(id) {
13787
- const db = getDatabase();
13788
- const row = db.prepare("SELECT * FROM invoices WHERE id = ? OR invoice_number = ?").get(id, id);
13789
- return row ? rowToInvoice(row) : null;
13790
- }
13791
- function getInvoiceWithItems(id) {
13792
- const invoice = getInvoice(id);
13793
- if (!invoice)
13794
- return null;
13795
- const db = getDatabase();
13796
- const line_items = db.prepare("SELECT * FROM line_items WHERE invoice_id = ? ORDER BY sort_order").all(invoice.id);
13797
- const payments = db.prepare("SELECT * FROM payments WHERE invoice_id = ? ORDER BY paid_at").all(invoice.id);
13798
- return { ...invoice, line_items, payments };
13799
- }
13800
- function listInvoices(options = {}) {
13801
- const db = getDatabase();
13802
- const conditions = [];
13803
- const params = [];
13804
- if (options.status) {
13805
- conditions.push("status = ?");
13806
- params.push(options.status);
13807
- }
13808
- if (options.client_id) {
13809
- conditions.push("client_id = ?");
13810
- params.push(options.client_id);
13811
- }
13812
- if (options.from_date) {
13813
- conditions.push("issue_date >= ?");
13814
- params.push(options.from_date);
13815
- }
13816
- if (options.to_date) {
13817
- conditions.push("issue_date <= ?");
13818
- params.push(options.to_date);
13819
- }
13820
- let sql = "SELECT * FROM invoices";
13821
- if (conditions.length > 0) {
13822
- sql += " WHERE " + conditions.join(" AND ");
13823
- }
13824
- sql += " ORDER BY created_at DESC";
13825
- if (options.limit) {
13826
- sql += " LIMIT ?";
13827
- params.push(options.limit);
13828
- }
13829
- const rows = db.prepare(sql).all(...params);
13830
- return rows.map(rowToInvoice);
13831
- }
13832
- function updateInvoiceStatus(id, status) {
13833
- const db = getDatabase();
13834
- const paidAt = status === "paid" ? "datetime('now')" : "NULL";
13835
- db.prepare(`UPDATE invoices SET status = ?, paid_at = ${paidAt}, updated_at = datetime('now') WHERE id = ?`).run(status, id);
13836
- return getInvoice(id);
13837
- }
13838
- function deleteInvoice(id) {
13839
- const db = getDatabase();
13840
- return db.prepare("DELETE FROM invoices WHERE id = ?").run(id).changes > 0;
13841
- }
13842
- function addLineItem(input) {
13843
- const db = getDatabase();
13844
- const id = crypto.randomUUID();
13845
- const quantity = input.quantity || 1;
13846
- const amount = quantity * input.unit_price;
13847
- const maxOrder = db.prepare("SELECT COALESCE(MAX(sort_order), -1) as max_order FROM line_items WHERE invoice_id = ?").get(input.invoice_id);
13848
- db.prepare(`INSERT INTO line_items (id, invoice_id, description, quantity, unit_price, amount, sort_order)
13849
- VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, input.invoice_id, input.description, quantity, input.unit_price, amount, maxOrder.max_order + 1);
13850
- recalculateInvoice(input.invoice_id);
13851
- return db.prepare("SELECT * FROM line_items WHERE id = ?").get(id);
13852
- }
13853
- function removeLineItem(id) {
13854
- const db = getDatabase();
13855
- const item = db.prepare("SELECT invoice_id FROM line_items WHERE id = ?").get(id);
13856
- if (!item)
13857
- return false;
13858
- db.prepare("DELETE FROM line_items WHERE id = ?").run(id);
13859
- recalculateInvoice(item.invoice_id);
13860
- return true;
14321
+ // src/db/cloud.ts
14322
+ function defaultConnectionString() {
14323
+ return process.env.INVOICES_DATABASE_URL ?? getConnectionString("invoices");
13861
14324
  }
13862
- function recordPayment(input) {
13863
- const db = getDatabase();
13864
- const id = crypto.randomUUID();
13865
- db.prepare(`INSERT INTO payments (id, invoice_id, amount, method, reference, notes)
13866
- VALUES (?, ?, ?, ?, ?, ?)`).run(id, input.invoice_id, input.amount, input.method || null, input.reference || null, input.notes || null);
13867
- const invoice = getInvoice(input.invoice_id);
13868
- if (invoice) {
13869
- const totalPaid = db.prepare("SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE invoice_id = ?").get(input.invoice_id);
13870
- if (totalPaid.total >= invoice.total) {
13871
- updateInvoiceStatus(input.invoice_id, "paid");
13872
- }
13873
- }
13874
- return db.prepare("SELECT * FROM payments WHERE id = ?").get(id);
13875
- }
13876
- function getInvoiceSummary() {
13877
- const db = getDatabase();
13878
- const counts = db.prepare(`SELECT
13879
- COUNT(*) as total_invoices,
13880
- SUM(CASE WHEN status = 'draft' THEN 1 ELSE 0 END) as draft,
13881
- SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END) as sent,
13882
- SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid,
13883
- SUM(CASE WHEN status = 'overdue' THEN 1 ELSE 0 END) as overdue,
13884
- COALESCE(SUM(CASE WHEN status IN ('sent', 'overdue') THEN total ELSE 0 END), 0) as total_outstanding,
13885
- COALESCE(SUM(CASE WHEN status = 'paid' THEN total ELSE 0 END), 0) as total_paid
13886
- FROM invoices`).get();
13887
- return counts;
14325
+ function parseTablesArg(tables) {
14326
+ if (!tables)
14327
+ return [];
14328
+ return tables.split(",").map((table) => table.trim()).filter(Boolean);
13888
14329
  }
13889
-
13890
- // src/db/clients.ts
13891
- function rowToClient(row) {
13892
- return { ...row, metadata: JSON.parse(row.metadata || "{}") };
13893
- }
13894
- function createClient(input) {
13895
- const db = getDatabase();
13896
- const id = crypto.randomUUID();
13897
- db.prepare(`INSERT INTO clients (id, name, email, phone, address, address_line1, address_line2, city, state, postal_code, country, tax_id, vat_number, language, notes)
13898
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.name, input.email || null, input.phone || null, input.address || null, input.address_line1 || null, input.address_line2 || null, input.city || null, input.state || null, input.postal_code || null, input.country || null, input.tax_id || null, input.vat_number || null, input.language || null, input.notes || null);
13899
- return getClient(id);
13900
- }
13901
- function getClient(id) {
13902
- const db = getDatabase();
13903
- const row = db.prepare("SELECT * FROM clients WHERE id = ?").get(id);
13904
- return row ? rowToClient(row) : null;
13905
- }
13906
- function listClients(search) {
13907
- const db = getDatabase();
13908
- if (search) {
13909
- const q = `%${search}%`;
13910
- const rows = db.prepare("SELECT * FROM clients WHERE name LIKE ? OR email LIKE ? ORDER BY name").all(q, q);
13911
- return rows.map(rowToClient);
13912
- }
13913
- return db.prepare("SELECT * FROM clients ORDER BY name").all().map(rowToClient);
13914
- }
13915
- function updateClient(id, input) {
13916
- const db = getDatabase();
13917
- const existing = getClient(id);
13918
- if (!existing)
13919
- return null;
13920
- const sets = [];
13921
- const params = [];
13922
- for (const [key, value] of Object.entries(input)) {
13923
- if (value !== undefined) {
13924
- sets.push(`${key} = ?`);
13925
- params.push(value);
13926
- }
14330
+ function resolveTables(local, tablesCsv) {
14331
+ const configured = parseTablesArg(tablesCsv);
14332
+ return configured.length > 0 ? configured : listSqliteTables(local);
14333
+ }
14334
+ async function cloudPush(local, tablesCsv, connectionString = defaultConnectionString()) {
14335
+ const remote = new PgAdapterAsync(connectionString);
14336
+ try {
14337
+ const tables = resolveTables(local, tablesCsv);
14338
+ const result = await syncPush(local, remote, { tables });
14339
+ return { direction: "push", tables, result };
14340
+ } finally {
14341
+ await remote.close();
13927
14342
  }
13928
- if (sets.length === 0)
13929
- return existing;
13930
- sets.push("updated_at = datetime('now')");
13931
- params.push(id);
13932
- db.prepare(`UPDATE clients SET ${sets.join(", ")} WHERE id = ?`).run(...params);
13933
- return getClient(id);
13934
14343
  }
13935
- function deleteClient(id) {
13936
- const db = getDatabase();
13937
- return db.prepare("DELETE FROM clients WHERE id = ?").run(id).changes > 0;
14344
+ async function cloudPull(local, tablesCsv, connectionString = defaultConnectionString()) {
14345
+ const remote = new PgAdapterAsync(connectionString);
14346
+ try {
14347
+ const tables = resolveTables(local, tablesCsv);
14348
+ const result = await syncPull(remote, local, { tables });
14349
+ return { direction: "pull", tables, result };
14350
+ } finally {
14351
+ await remote.close();
14352
+ }
14353
+ }
14354
+ async function submitFeedback(local, feedback) {
14355
+ const payload = {
14356
+ service: "invoices",
14357
+ version: feedback.version,
14358
+ message: feedback.message,
14359
+ email: feedback.email,
14360
+ machine_id: feedback.machine_id
14361
+ };
14362
+ const delivery = await sendFeedback(payload, local);
14363
+ if (!delivery.sent) {
14364
+ const id = saveFeedback(local, payload);
14365
+ return { ...delivery, id, stored_locally: true };
14366
+ }
14367
+ return { ...delivery, stored_locally: false };
13938
14368
  }
13939
14369
 
13940
- // src/db/business.ts
13941
- function rowToBusiness(row) {
14370
+ // src/db/pg.ts
14371
+ function defaultPgConnectionString() {
14372
+ return process.env.INVOICES_DATABASE_URL ?? getConnectionString("invoices");
14373
+ }
14374
+ async function migratePgDatabase(connectionString = defaultPgConnectionString()) {
14375
+ const pgMigrations = MIGRATIONS.map((migration) => translateDdl(migration.sql, "pg"));
14376
+ return applyPgMigrations(connectionString, pgMigrations, "invoices");
14377
+ }
14378
+
14379
+ // src/db/invoices.ts
14380
+ import { randomUUID } from "crypto";
14381
+ function toLineTotalCents(line) {
14382
+ const subtotal = Math.round(line.quantity * line.unitPriceCents);
14383
+ const taxRate = line.taxRateBasisPoints ?? 0;
14384
+ const tax = Math.round(subtotal * taxRate / 1e4);
14385
+ return { lineTotalCents: subtotal, lineTaxCents: tax };
14386
+ }
14387
+ function mapPartyRow(row) {
13942
14388
  return {
13943
- ...row,
13944
- is_default: row.is_default === 1,
13945
- metadata: JSON.parse(row.metadata || "{}")
13946
- };
13947
- }
13948
- function createBusinessProfile(input) {
13949
- const db = getDatabase();
13950
- const id = crypto.randomUUID();
13951
- if (input.is_default) {
13952
- db.prepare("UPDATE business_profiles SET is_default = 0").run();
13953
- }
13954
- db.prepare(`INSERT INTO business_profiles (id, name, address_line1, address_line2, city, state, postal_code, country, tax_id, vat_number, registration_number, email, phone, website, bank_name, bank_iban, bank_swift, bank_account, logo_url, is_default)
13955
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.name, input.address_line1 || null, input.address_line2 || null, input.city || null, input.state || null, input.postal_code || null, input.country || "US", input.tax_id || null, input.vat_number || null, input.registration_number || null, input.email || null, input.phone || null, input.website || null, input.bank_name || null, input.bank_iban || null, input.bank_swift || null, input.bank_account || null, input.logo_url || null, input.is_default ? 1 : 0);
13956
- return getBusinessProfile(id);
13957
- }
13958
- function getBusinessProfile(id) {
13959
- const db = getDatabase();
13960
- const row = db.prepare("SELECT * FROM business_profiles WHERE id = ?").get(id);
13961
- return row ? rowToBusiness(row) : null;
13962
- }
13963
- function getDefaultBusinessProfile() {
13964
- const db = getDatabase();
13965
- const row = db.prepare("SELECT * FROM business_profiles WHERE is_default = 1").get();
13966
- return row ? rowToBusiness(row) : null;
13967
- }
13968
- function listBusinessProfiles() {
13969
- const db = getDatabase();
13970
- return db.prepare("SELECT * FROM business_profiles ORDER BY is_default DESC, name").all().map(rowToBusiness);
13971
- }
13972
- function updateBusinessProfile(id, input) {
13973
- const db = getDatabase();
13974
- if (!getBusinessProfile(id))
13975
- return null;
13976
- const sets = [];
13977
- const params = [];
13978
- for (const [key, value] of Object.entries(input)) {
13979
- if (value === undefined)
13980
- continue;
13981
- if (key === "is_default") {
13982
- if (value)
13983
- db.prepare("UPDATE business_profiles SET is_default = 0").run();
13984
- sets.push("is_default = ?");
13985
- params.push(value ? 1 : 0);
13986
- } else {
13987
- sets.push(`${key} = ?`);
13988
- params.push(value);
14389
+ id: row.id,
14390
+ kind: row.kind,
14391
+ legalName: row.legal_name,
14392
+ email: row.email ?? undefined,
14393
+ taxId: row.tax_id ?? undefined,
14394
+ country: row.country ?? undefined,
14395
+ address: row.address ?? undefined,
14396
+ createdAt: row.created_at,
14397
+ updatedAt: row.updated_at
14398
+ };
14399
+ }
14400
+ function mapInvoiceRow(row) {
14401
+ return {
14402
+ id: row.id,
14403
+ number: row.number,
14404
+ issuerId: row.issuer_id,
14405
+ customerId: row.customer_id,
14406
+ status: row.status,
14407
+ currency: row.currency,
14408
+ notes: row.notes ?? undefined,
14409
+ issuedAt: row.issued_at,
14410
+ dueAt: row.due_at ?? undefined,
14411
+ subtotalCents: row.subtotal_cents,
14412
+ taxCents: row.tax_cents,
14413
+ totalCents: row.total_cents,
14414
+ createdAt: row.created_at,
14415
+ updatedAt: row.updated_at
14416
+ };
14417
+ }
14418
+ function mapInvoiceLineRow(row) {
14419
+ return {
14420
+ id: row.id,
14421
+ invoiceId: row.invoice_id,
14422
+ position: row.position,
14423
+ description: row.description,
14424
+ quantity: row.quantity,
14425
+ unitPriceCents: row.unit_price_cents,
14426
+ taxRateBasisPoints: row.tax_rate_basis_points,
14427
+ lineTotalCents: row.line_total_cents,
14428
+ createdAt: row.created_at
14429
+ };
14430
+ }
14431
+ function createParty(db, input) {
14432
+ const id = input.id ?? randomUUID();
14433
+ db.query(`
14434
+ INSERT INTO parties (id, kind, legal_name, email, tax_id, country, address)
14435
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
14436
+ `).run(id, input.kind, input.legalName, input.email ?? null, input.taxId ?? null, input.country ?? null, input.address ?? null);
14437
+ return getPartyById(db, id);
14438
+ }
14439
+ function getPartyById(db, id) {
14440
+ const row = db.query("SELECT * FROM parties WHERE id = ?1").get(id);
14441
+ return row ? mapPartyRow(row) : null;
14442
+ }
14443
+ function listParties(db, kind) {
14444
+ if (kind) {
14445
+ return db.query("SELECT * FROM parties WHERE kind = ?1 ORDER BY legal_name ASC").all(kind).map(mapPartyRow);
14446
+ }
14447
+ return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
14448
+ }
14449
+ function createInvoice(db, input) {
14450
+ const invoiceId = input.id ?? randomUUID();
14451
+ const preparedLines = input.lines.map((line, index) => {
14452
+ const { lineTotalCents, lineTaxCents } = toLineTotalCents(line);
14453
+ return {
14454
+ id: line.id ?? randomUUID(),
14455
+ position: index,
14456
+ description: line.description,
14457
+ quantity: line.quantity,
14458
+ unitPriceCents: line.unitPriceCents,
14459
+ taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
14460
+ lineTotalCents,
14461
+ lineTaxCents
14462
+ };
14463
+ });
14464
+ const subtotalCents = preparedLines.reduce((sum, line) => sum + line.lineTotalCents, 0);
14465
+ const taxCents = preparedLines.reduce((sum, line) => sum + line.lineTaxCents, 0);
14466
+ const totalCents = subtotalCents + taxCents;
14467
+ db.transaction(() => {
14468
+ db.query(`
14469
+ INSERT INTO invoices (
14470
+ id, number, issuer_id, customer_id, status, currency, notes, issued_at, due_at, subtotal_cents, tax_cents, total_cents
14471
+ )
14472
+ VALUES (?1, ?2, ?3, ?4, 'draft', ?5, ?6, ?7, ?8, ?9, ?10, ?11)
14473
+ `).run(invoiceId, input.number, input.issuerId, input.customerId, input.currency, input.notes ?? null, input.issuedAt, input.dueAt ?? null, subtotalCents, taxCents, totalCents);
14474
+ const insertLine = db.query(`
14475
+ INSERT INTO invoice_lines (
14476
+ id, invoice_id, position, description, quantity, unit_price_cents, tax_rate_basis_points, line_total_cents
14477
+ )
14478
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
14479
+ `);
14480
+ for (const line of preparedLines) {
14481
+ insertLine.run(line.id, invoiceId, line.position, line.description, line.quantity, line.unitPriceCents, line.taxRateBasisPoints, line.lineTotalCents);
13989
14482
  }
14483
+ });
14484
+ return getInvoiceById(db, invoiceId);
14485
+ }
14486
+ function getInvoiceById(db, id) {
14487
+ const invoiceRow = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
14488
+ if (!invoiceRow) {
14489
+ return null;
14490
+ }
14491
+ const lines = db.query("SELECT * FROM invoice_lines WHERE invoice_id = ?1 ORDER BY position ASC").all(id).map(mapInvoiceLineRow);
14492
+ return {
14493
+ ...mapInvoiceRow(invoiceRow),
14494
+ lines
14495
+ };
14496
+ }
14497
+ function listInvoices(db, options = {}) {
14498
+ const limit = options.limit ?? 50;
14499
+ const offset = options.offset ?? 0;
14500
+ if (options.status) {
14501
+ return db.query("SELECT * FROM invoices WHERE status = ?1 ORDER BY issued_at DESC, created_at DESC LIMIT ?2 OFFSET ?3").all(options.status, limit, offset).map(mapInvoiceRow);
13990
14502
  }
13991
- if (sets.length === 0)
13992
- return getBusinessProfile(id);
13993
- sets.push("updated_at = datetime('now')");
13994
- params.push(id);
13995
- db.prepare(`UPDATE business_profiles SET ${sets.join(", ")} WHERE id = ?`).run(...params);
13996
- return getBusinessProfile(id);
14503
+ return db.query("SELECT * FROM invoices ORDER BY issued_at DESC, created_at DESC LIMIT ?1 OFFSET ?2").all(limit, offset).map(mapInvoiceRow);
13997
14504
  }
13998
- function deleteBusinessProfile(id) {
13999
- return getDatabase().prepare("DELETE FROM business_profiles WHERE id = ?").run(id).changes > 0;
14505
+ function updateInvoiceStatus(db, id, status) {
14506
+ db.query("UPDATE invoices SET status = ?2, updated_at = datetime('now') WHERE id = ?1").run(id, status);
14507
+ const row = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
14508
+ return row ? mapInvoiceRow(row) : null;
14000
14509
  }
14001
- function rowToTaxRule(row) {
14002
- return { ...row, is_default: row.is_default === 1, reverse_charge: row.reverse_charge === 1 };
14510
+ function deleteInvoice(db, id) {
14511
+ const result = db.query("DELETE FROM invoices WHERE id = ?1").run(id);
14512
+ return result.changes > 0;
14003
14513
  }
14004
- function getTaxRulesForCountry(country) {
14005
- const db = getDatabase();
14006
- return db.prepare("SELECT * FROM tax_rules WHERE country = ? ORDER BY is_default DESC, rate DESC").all(country).map(rowToTaxRule);
14514
+ function searchInvoices(db, query, options = {}) {
14515
+ const limit = options.limit ?? 20;
14516
+ const offset = options.offset ?? 0;
14517
+ const escapedPhrase = `"${query.replace(/"/g, '""')}"`;
14518
+ if (options.status) {
14519
+ return db.query(`
14520
+ SELECT i.*
14521
+ FROM invoices_fts f
14522
+ JOIN invoices i ON i.id = f.invoice_id
14523
+ WHERE invoices_fts MATCH ?1
14524
+ AND i.status = ?2
14525
+ ORDER BY bm25(invoices_fts)
14526
+ LIMIT ?3
14527
+ OFFSET ?4
14528
+ `).all(escapedPhrase, options.status, limit, offset).map(mapInvoiceRow);
14529
+ }
14530
+ return db.query(`
14531
+ SELECT i.*
14532
+ FROM invoices_fts f
14533
+ JOIN invoices i ON i.id = f.invoice_id
14534
+ WHERE invoices_fts MATCH ?1
14535
+ ORDER BY bm25(invoices_fts)
14536
+ LIMIT ?2
14537
+ OFFSET ?3
14538
+ `).all(escapedPhrase, limit, offset).map(mapInvoiceRow);
14539
+ }
14540
+
14541
+ // src/db/agents.ts
14542
+ import { randomUUID as randomUUID2 } from "crypto";
14543
+ function mapAgent(row) {
14544
+ return {
14545
+ id: row.id,
14546
+ name: row.name,
14547
+ description: row.description ?? undefined,
14548
+ focus: row.focus ?? undefined,
14549
+ createdAt: row.created_at,
14550
+ lastSeenAt: row.last_seen_at
14551
+ };
14007
14552
  }
14008
- function getDefaultTaxRule(country) {
14009
- const db = getDatabase();
14010
- const row = db.prepare("SELECT * FROM tax_rules WHERE country = ? AND is_default = 1").get(country);
14011
- return row ? rowToTaxRule(row) : null;
14553
+ function registerAgent(db, input) {
14554
+ const existing = db.query("SELECT * FROM agents WHERE name = ?1").get(input.name);
14555
+ if (existing) {
14556
+ db.query("UPDATE agents SET description = COALESCE(?2, description), last_seen_at = datetime('now') WHERE name = ?1").run(input.name, input.description ?? null);
14557
+ return getAgentByName(db, input.name);
14558
+ }
14559
+ const id = randomUUID2();
14560
+ db.query("INSERT INTO agents (id, name, description) VALUES (?1, ?2, ?3)").run(id, input.name, input.description ?? null);
14561
+ return getAgentById(db, id);
14012
14562
  }
14013
- function getTaxRule(id) {
14014
- const db = getDatabase();
14015
- const row = db.prepare("SELECT * FROM tax_rules WHERE id = ?").get(id);
14016
- return row ? rowToTaxRule(row) : null;
14563
+ function getAgentById(db, id) {
14564
+ const row = db.query("SELECT * FROM agents WHERE id = ?1").get(id);
14565
+ return row ? mapAgent(row) : null;
14017
14566
  }
14018
- function listAllTaxRules() {
14019
- const db = getDatabase();
14020
- return db.prepare("SELECT * FROM tax_rules ORDER BY country, is_default DESC, rate DESC").all().map(rowToTaxRule);
14567
+ function getAgentByName(db, name) {
14568
+ const row = db.query("SELECT * FROM agents WHERE name = ?1").get(name);
14569
+ return row ? mapAgent(row) : null;
14021
14570
  }
14022
- function createTaxRule(input) {
14023
- const db = getDatabase();
14024
- const id = crypto.randomUUID();
14025
- db.prepare(`INSERT INTO tax_rules (id, country, region, tax_name, rate, type, is_default, reverse_charge, description)
14026
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.country, input.region || null, input.tax_name, input.rate, input.type || "vat", input.is_default ? 1 : 0, input.reverse_charge ? 1 : 0, input.description || null);
14027
- return getTaxRule(id);
14571
+ function heartbeatAgent(db, agentId) {
14572
+ db.query("UPDATE agents SET last_seen_at = datetime('now') WHERE id = ?1").run(agentId);
14573
+ return getAgentById(db, agentId);
14028
14574
  }
14029
- function deleteTaxRule(id) {
14030
- return getDatabase().prepare("DELETE FROM tax_rules WHERE id = ?").run(id).changes > 0;
14575
+ function setAgentFocus(db, agentId, focus) {
14576
+ db.query("UPDATE agents SET focus = ?2, last_seen_at = datetime('now') WHERE id = ?1").run(agentId, focus ?? null);
14577
+ return getAgentById(db, agentId);
14031
14578
  }
14032
- function determineTax(issuerCountry, clientCountry, clientVatNumber) {
14033
- const EU_COUNTRIES = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"];
14034
- const issuerInEU = EU_COUNTRIES.includes(issuerCountry);
14035
- const clientInEU = EU_COUNTRIES.includes(clientCountry);
14036
- if (issuerInEU && clientInEU && issuerCountry !== clientCountry && clientVatNumber) {
14037
- return { tax_rate: 0, tax_name: "Reverse Charge", reverse_charge: true };
14038
- }
14039
- const defaultRule = getDefaultTaxRule(issuerCountry);
14040
- if (defaultRule) {
14041
- return { tax_rate: defaultRule.rate, tax_name: defaultRule.tax_name, reverse_charge: false };
14042
- }
14043
- return { tax_rate: 0, tax_name: "Tax", reverse_charge: false };
14579
+ function listAgents(db) {
14580
+ return db.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all().map(mapAgent);
14044
14581
  }
14045
14582
 
14046
14583
  // src/mcp/index.ts
14584
+ function getMcpVersion() {
14585
+ try {
14586
+ const __dir = dirname3(fileURLToPath(import.meta.url));
14587
+ const pkgPath = join7(__dir, "..", "..", "package.json");
14588
+ return JSON.parse(readFileSync2(pkgPath, "utf-8")).version || "0.0.0";
14589
+ } catch {
14590
+ return "0.0.0";
14591
+ }
14592
+ }
14047
14593
  var server = new McpServer({
14048
14594
  name: "invoices",
14049
- version: "0.1.0"
14595
+ version: getMcpVersion()
14596
+ });
14597
+ var statusSchema = exports_external.enum(["draft", "sent", "partially_paid", "paid", "void", "overdue"]);
14598
+ server.registerTool("health", {
14599
+ title: "Health",
14600
+ description: "Health check and current SQLite database path.",
14601
+ inputSchema: {}
14602
+ }, async () => {
14603
+ const db = openInvoiceDatabase();
14604
+ const path = db.filename;
14605
+ db.close();
14606
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, dbPath: path }) }] };
14607
+ });
14608
+ server.registerTool("migrate_database", {
14609
+ title: "Migrate Database",
14610
+ description: "Apply pending SQLite schema migrations.",
14611
+ inputSchema: {}
14612
+ }, async () => {
14613
+ const applied = migrateDatabase();
14614
+ return { content: [{ type: "text", text: JSON.stringify({ applied }) }] };
14615
+ });
14616
+ server.registerTool("create_party", {
14617
+ title: "Create Party",
14618
+ description: "Create invoice issuer/customer party.",
14619
+ inputSchema: {
14620
+ kind: exports_external.enum(["issuer", "customer"]),
14621
+ legal_name: exports_external.string().min(1),
14622
+ email: exports_external.string().email().optional(),
14623
+ tax_id: exports_external.string().optional(),
14624
+ country: exports_external.string().optional(),
14625
+ address: exports_external.string().optional()
14626
+ }
14627
+ }, async (input) => {
14628
+ const db = openInvoiceDatabase();
14629
+ try {
14630
+ const party = createParty(db, {
14631
+ kind: input.kind,
14632
+ legalName: input.legal_name,
14633
+ email: input.email,
14634
+ taxId: input.tax_id,
14635
+ country: input.country,
14636
+ address: input.address
14637
+ });
14638
+ return { content: [{ type: "text", text: JSON.stringify(party) }] };
14639
+ } finally {
14640
+ db.close();
14641
+ }
14642
+ });
14643
+ server.registerTool("list_parties", {
14644
+ title: "List Parties",
14645
+ description: "List registered invoice parties.",
14646
+ inputSchema: {
14647
+ kind: exports_external.enum(["issuer", "customer"]).optional()
14648
+ }
14649
+ }, async (input) => {
14650
+ const db = openInvoiceDatabase();
14651
+ try {
14652
+ const parties = listParties(db, input.kind);
14653
+ return { content: [{ type: "text", text: JSON.stringify(parties) }] };
14654
+ } finally {
14655
+ db.close();
14656
+ }
14050
14657
  });
14051
14658
  server.registerTool("create_invoice", {
14052
14659
  title: "Create Invoice",
14053
- description: "Create a new invoice. Auto-generates an invoice number (INV-00001, etc.).",
14660
+ description: "Create invoice with lines and calculated totals.",
14054
14661
  inputSchema: {
14055
- client_id: exports_external.string().optional(),
14056
- business_profile_id: exports_external.string().optional(),
14057
- due_date: exports_external.string().optional(),
14058
- currency: exports_external.string().optional(),
14059
- tax_rate: exports_external.number().optional(),
14060
- tax_name: exports_external.string().optional(),
14061
- discount: exports_external.number().optional(),
14062
- reverse_charge: exports_external.boolean().optional(),
14063
- language: exports_external.string().optional(),
14662
+ number: exports_external.string().min(1),
14663
+ issuer_id: exports_external.string().min(1),
14664
+ customer_id: exports_external.string().min(1),
14665
+ currency: exports_external.string().min(3),
14064
14666
  notes: exports_external.string().optional(),
14065
- footer_text: exports_external.string().optional()
14667
+ issued_at: exports_external.string().min(1),
14668
+ due_at: exports_external.string().optional(),
14669
+ lines: exports_external.array(exports_external.object({
14670
+ description: exports_external.string().min(1),
14671
+ quantity: exports_external.number().positive(),
14672
+ unit_price_cents: exports_external.number().int().nonnegative(),
14673
+ tax_rate_basis_points: exports_external.number().int().nonnegative().optional()
14674
+ }))
14675
+ }
14676
+ }, async (input) => {
14677
+ const db = openInvoiceDatabase();
14678
+ try {
14679
+ const invoice = createInvoice(db, {
14680
+ number: input.number,
14681
+ issuerId: input.issuer_id,
14682
+ customerId: input.customer_id,
14683
+ currency: input.currency,
14684
+ notes: input.notes,
14685
+ issuedAt: input.issued_at,
14686
+ dueAt: input.due_at,
14687
+ lines: input.lines.map((line) => ({
14688
+ description: line.description,
14689
+ quantity: line.quantity,
14690
+ unitPriceCents: line.unit_price_cents,
14691
+ taxRateBasisPoints: line.tax_rate_basis_points
14692
+ }))
14693
+ });
14694
+ return { content: [{ type: "text", text: JSON.stringify(invoice) }] };
14695
+ } finally {
14696
+ db.close();
14066
14697
  }
14067
- }, async (params) => {
14068
- const invoice = createInvoice(params);
14069
- return { content: [{ type: "text", text: JSON.stringify(invoice, null, 2) }] };
14070
14698
  });
14071
14699
  server.registerTool("get_invoice", {
14072
14700
  title: "Get Invoice",
14073
- description: "Get an invoice with line items and payments by ID or invoice number.",
14074
- inputSchema: { id: exports_external.string() }
14075
- }, async ({ id }) => {
14076
- const invoice = getInvoiceWithItems(id);
14077
- if (!invoice) {
14078
- return { content: [{ type: "text", text: `Invoice '${id}' not found.` }], isError: true };
14079
- }
14080
- return { content: [{ type: "text", text: JSON.stringify(invoice, null, 2) }] };
14701
+ description: "Fetch invoice details including lines.",
14702
+ inputSchema: {
14703
+ id: exports_external.string().min(1)
14704
+ }
14705
+ }, async (input) => {
14706
+ const db = openInvoiceDatabase();
14707
+ try {
14708
+ const invoice = getInvoiceById(db, input.id);
14709
+ return { content: [{ type: "text", text: JSON.stringify(invoice) }] };
14710
+ } finally {
14711
+ db.close();
14712
+ }
14081
14713
  });
14082
14714
  server.registerTool("list_invoices", {
14083
14715
  title: "List Invoices",
14084
- description: "List invoices with optional filters.",
14716
+ description: "List invoices with optional status filter.",
14085
14717
  inputSchema: {
14086
- status: exports_external.string().optional(),
14087
- client_id: exports_external.string().optional(),
14088
- from_date: exports_external.string().optional(),
14089
- to_date: exports_external.string().optional(),
14090
- limit: exports_external.number().optional()
14091
- }
14092
- }, async (params) => {
14093
- const invoices = listInvoices(params);
14094
- return {
14095
- content: [{ type: "text", text: JSON.stringify({ invoices, count: invoices.length }, null, 2) }]
14096
- };
14718
+ status: statusSchema.optional(),
14719
+ limit: exports_external.number().int().positive().max(200).optional(),
14720
+ offset: exports_external.number().int().nonnegative().optional()
14721
+ }
14722
+ }, async (input) => {
14723
+ const db = openInvoiceDatabase();
14724
+ try {
14725
+ const invoices = listInvoices(db, {
14726
+ status: input.status,
14727
+ limit: input.limit,
14728
+ offset: input.offset
14729
+ });
14730
+ return { content: [{ type: "text", text: JSON.stringify(invoices) }] };
14731
+ } finally {
14732
+ db.close();
14733
+ }
14734
+ });
14735
+ server.registerTool("search_invoices", {
14736
+ title: "Search Invoices",
14737
+ description: "Full-text search over invoice number, issuer/customer names, and notes.",
14738
+ inputSchema: {
14739
+ query: exports_external.string().min(1),
14740
+ status: statusSchema.optional(),
14741
+ limit: exports_external.number().int().positive().max(200).optional(),
14742
+ offset: exports_external.number().int().nonnegative().optional()
14743
+ }
14744
+ }, async (input) => {
14745
+ const db = openInvoiceDatabase();
14746
+ try {
14747
+ const invoices = searchInvoices(db, input.query, {
14748
+ status: input.status,
14749
+ limit: input.limit,
14750
+ offset: input.offset
14751
+ });
14752
+ return { content: [{ type: "text", text: JSON.stringify(invoices) }] };
14753
+ } finally {
14754
+ db.close();
14755
+ }
14097
14756
  });
14098
14757
  server.registerTool("update_invoice_status", {
14099
14758
  title: "Update Invoice Status",
14100
- description: "Change invoice status: draft, sent, paid, overdue, cancelled, refunded.",
14759
+ description: "Update invoice lifecycle status.",
14101
14760
  inputSchema: {
14102
- id: exports_external.string(),
14103
- status: exports_external.enum(["draft", "sent", "paid", "overdue", "cancelled", "refunded"])
14761
+ id: exports_external.string().min(1),
14762
+ status: statusSchema
14104
14763
  }
14105
- }, async ({ id, status }) => {
14106
- const invoice = updateInvoiceStatus(id, status);
14107
- if (!invoice) {
14108
- return { content: [{ type: "text", text: `Invoice '${id}' not found.` }], isError: true };
14764
+ }, async (input) => {
14765
+ const db = openInvoiceDatabase();
14766
+ try {
14767
+ const invoice = updateInvoiceStatus(db, input.id, input.status);
14768
+ return { content: [{ type: "text", text: JSON.stringify(invoice) }] };
14769
+ } finally {
14770
+ db.close();
14109
14771
  }
14110
- return { content: [{ type: "text", text: JSON.stringify(invoice, null, 2) }] };
14111
14772
  });
14112
14773
  server.registerTool("delete_invoice", {
14113
14774
  title: "Delete Invoice",
14114
- description: "Delete an invoice.",
14115
- inputSchema: { id: exports_external.string() }
14116
- }, async ({ id }) => {
14117
- const deleted = deleteInvoice(id);
14118
- return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
14119
- });
14120
- server.registerTool("add_line_item", {
14121
- title: "Add Line Item",
14122
- description: "Add a line item to an invoice. Automatically recalculates subtotal, tax, and total.",
14775
+ description: "Delete invoice and lines by id.",
14123
14776
  inputSchema: {
14124
- invoice_id: exports_external.string(),
14125
- description: exports_external.string(),
14126
- unit_price: exports_external.number(),
14127
- quantity: exports_external.number().optional()
14128
- }
14129
- }, async (params) => {
14130
- const item = addLineItem(params);
14131
- return { content: [{ type: "text", text: JSON.stringify(item, null, 2) }] };
14132
- });
14133
- server.registerTool("remove_line_item", {
14134
- title: "Remove Line Item",
14135
- description: "Remove a line item from an invoice. Recalculates totals.",
14136
- inputSchema: { id: exports_external.string() }
14137
- }, async ({ id }) => {
14138
- const removed = removeLineItem(id);
14139
- return { content: [{ type: "text", text: JSON.stringify({ id, removed }) }] };
14140
- });
14141
- server.registerTool("record_payment", {
14142
- title: "Record Payment",
14143
- description: "Record a payment against an invoice. Auto-marks invoice as paid when fully covered.",
14144
- inputSchema: {
14145
- invoice_id: exports_external.string(),
14146
- amount: exports_external.number(),
14147
- method: exports_external.string().optional(),
14148
- reference: exports_external.string().optional(),
14149
- notes: exports_external.string().optional()
14150
- }
14151
- }, async (params) => {
14152
- const payment = recordPayment(params);
14153
- return { content: [{ type: "text", text: JSON.stringify(payment, null, 2) }] };
14154
- });
14155
- server.registerTool("invoice_summary", {
14156
- title: "Invoice Summary",
14157
- description: "Get summary statistics: total invoices by status, outstanding and collected amounts.",
14158
- inputSchema: {}
14159
- }, async () => {
14160
- const summary = getInvoiceSummary();
14161
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
14777
+ id: exports_external.string().min(1)
14778
+ }
14779
+ }, async (input) => {
14780
+ const db = openInvoiceDatabase();
14781
+ try {
14782
+ const deleted = deleteInvoice(db, input.id);
14783
+ return { content: [{ type: "text", text: JSON.stringify({ deleted }) }] };
14784
+ } finally {
14785
+ db.close();
14786
+ }
14162
14787
  });
14163
- server.registerTool("create_client", {
14164
- title: "Create Client",
14165
- description: "Create a new client for invoicing.",
14788
+ server.registerTool("register_agent", {
14789
+ title: "Register Agent",
14790
+ description: "Register or refresh an agent identity.",
14166
14791
  inputSchema: {
14167
- name: exports_external.string(),
14168
- email: exports_external.string().optional(),
14169
- phone: exports_external.string().optional(),
14170
- address: exports_external.string().optional(),
14171
- address_line1: exports_external.string().optional(),
14172
- city: exports_external.string().optional(),
14173
- state: exports_external.string().optional(),
14174
- postal_code: exports_external.string().optional(),
14175
- country: exports_external.string().optional(),
14176
- tax_id: exports_external.string().optional(),
14177
- vat_number: exports_external.string().optional(),
14178
- language: exports_external.string().optional(),
14179
- notes: exports_external.string().optional()
14792
+ name: exports_external.string().min(1),
14793
+ description: exports_external.string().optional()
14794
+ }
14795
+ }, async (input) => {
14796
+ const db = openInvoiceDatabase();
14797
+ try {
14798
+ const agent = registerAgent(db, {
14799
+ name: input.name,
14800
+ description: input.description
14801
+ });
14802
+ return { content: [{ type: "text", text: JSON.stringify(agent) }] };
14803
+ } finally {
14804
+ db.close();
14180
14805
  }
14181
- }, async (params) => {
14182
- const client = createClient(params);
14183
- return { content: [{ type: "text", text: JSON.stringify(client, null, 2) }] };
14184
- });
14185
- server.registerTool("list_clients", {
14186
- title: "List Clients",
14187
- description: "List all clients. Optionally search by name or email.",
14188
- inputSchema: { search: exports_external.string().optional() }
14189
- }, async ({ search }) => {
14190
- const clients = listClients(search);
14191
- return {
14192
- content: [{ type: "text", text: JSON.stringify({ clients, count: clients.length }, null, 2) }]
14193
- };
14194
14806
  });
14195
- server.registerTool("update_client", {
14196
- title: "Update Client",
14197
- description: "Update client details.",
14807
+ server.registerTool("heartbeat", {
14808
+ title: "Heartbeat",
14809
+ description: "Update agent last_seen_at.",
14198
14810
  inputSchema: {
14199
- id: exports_external.string(),
14200
- name: exports_external.string().optional(),
14201
- email: exports_external.string().optional(),
14202
- phone: exports_external.string().optional(),
14203
- country: exports_external.string().optional(),
14204
- vat_number: exports_external.string().optional(),
14205
- notes: exports_external.string().optional()
14811
+ agent_id: exports_external.string().min(1)
14206
14812
  }
14207
- }, async ({ id, ...input }) => {
14208
- const client = updateClient(id, input);
14209
- if (!client) {
14210
- return { content: [{ type: "text", text: `Client '${id}' not found.` }], isError: true };
14813
+ }, async (input) => {
14814
+ const db = openInvoiceDatabase();
14815
+ try {
14816
+ const agent = heartbeatAgent(db, input.agent_id);
14817
+ return { content: [{ type: "text", text: JSON.stringify(agent) }] };
14818
+ } finally {
14819
+ db.close();
14211
14820
  }
14212
- return { content: [{ type: "text", text: JSON.stringify(client, null, 2) }] };
14213
- });
14214
- server.registerTool("delete_client", {
14215
- title: "Delete Client",
14216
- description: "Delete a client.",
14217
- inputSchema: { id: exports_external.string() }
14218
- }, async ({ id }) => {
14219
- const deleted = deleteClient(id);
14220
- return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
14221
14821
  });
14222
- server.registerTool("create_business_profile", {
14223
- title: "Create Business Profile",
14224
- description: "Create a business profile (issuer details for invoices).",
14822
+ server.registerTool("set_focus", {
14823
+ title: "Set Focus",
14824
+ description: "Set or clear current agent focus.",
14225
14825
  inputSchema: {
14226
- name: exports_external.string(),
14227
- country: exports_external.string().optional(),
14228
- address_line1: exports_external.string().optional(),
14229
- city: exports_external.string().optional(),
14230
- state: exports_external.string().optional(),
14231
- postal_code: exports_external.string().optional(),
14232
- tax_id: exports_external.string().optional(),
14233
- vat_number: exports_external.string().optional(),
14234
- registration_number: exports_external.string().optional(),
14235
- email: exports_external.string().optional(),
14236
- phone: exports_external.string().optional(),
14237
- website: exports_external.string().optional(),
14238
- bank_name: exports_external.string().optional(),
14239
- bank_iban: exports_external.string().optional(),
14240
- bank_swift: exports_external.string().optional(),
14241
- bank_account: exports_external.string().optional(),
14242
- logo_url: exports_external.string().optional(),
14243
- is_default: exports_external.boolean().optional()
14244
- }
14245
- }, async (params) => {
14246
- const biz = createBusinessProfile(params);
14247
- return { content: [{ type: "text", text: JSON.stringify(biz, null, 2) }] };
14248
- });
14249
- server.registerTool("get_business_profile", {
14250
- title: "Get Business Profile",
14251
- description: "Get a business profile by ID.",
14252
- inputSchema: { id: exports_external.string() }
14253
- }, async ({ id }) => {
14254
- const biz = getBusinessProfile(id);
14255
- if (!biz) {
14256
- return { content: [{ type: "text", text: `Business profile '${id}' not found.` }], isError: true };
14257
- }
14258
- return { content: [{ type: "text", text: JSON.stringify(biz, null, 2) }] };
14826
+ agent_id: exports_external.string().min(1),
14827
+ focus: exports_external.string().optional()
14828
+ }
14829
+ }, async (input) => {
14830
+ const db = openInvoiceDatabase();
14831
+ try {
14832
+ const agent = setAgentFocus(db, input.agent_id, input.focus);
14833
+ return { content: [{ type: "text", text: JSON.stringify(agent) }] };
14834
+ } finally {
14835
+ db.close();
14836
+ }
14259
14837
  });
14260
- server.registerTool("get_default_business_profile", {
14261
- title: "Get Default Business Profile",
14262
- description: "Get the default business profile.",
14838
+ server.registerTool("list_agents", {
14839
+ title: "List Agents",
14840
+ description: "List registered agents and their metadata.",
14263
14841
  inputSchema: {}
14264
14842
  }, async () => {
14265
- const biz = getDefaultBusinessProfile();
14266
- return { content: [{ type: "text", text: JSON.stringify(biz, null, 2) }] };
14843
+ const db = openInvoiceDatabase();
14844
+ try {
14845
+ const agents = listAgents(db);
14846
+ return { content: [{ type: "text", text: JSON.stringify(agents) }] };
14847
+ } finally {
14848
+ db.close();
14849
+ }
14267
14850
  });
14268
- server.registerTool("list_business_profiles", {
14269
- title: "List Business Profiles",
14270
- description: "List all business profiles.",
14271
- inputSchema: {}
14272
- }, async () => {
14273
- const profiles = listBusinessProfiles();
14274
- return { content: [{ type: "text", text: JSON.stringify({ profiles, count: profiles.length }, null, 2) }] };
14851
+ server.registerTool("migrate_postgres", {
14852
+ title: "Migrate PostgreSQL",
14853
+ description: "Apply pending PostgreSQL migrations translated from local schema.",
14854
+ inputSchema: {
14855
+ connection_string: exports_external.string().optional()
14856
+ }
14857
+ }, async (input) => {
14858
+ const result = await migratePgDatabase(input.connection_string);
14859
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
14275
14860
  });
14276
- server.registerTool("update_business_profile", {
14277
- title: "Update Business Profile",
14278
- description: "Update a business profile.",
14861
+ server.registerTool("cloud_push", {
14862
+ title: "Cloud Push",
14863
+ description: "Push local SQLite tables to cloud PostgreSQL.",
14279
14864
  inputSchema: {
14280
- id: exports_external.string(),
14281
- name: exports_external.string().optional(),
14282
- country: exports_external.string().optional(),
14283
- email: exports_external.string().optional(),
14284
- phone: exports_external.string().optional(),
14285
- bank_iban: exports_external.string().optional(),
14286
- is_default: exports_external.boolean().optional()
14865
+ tables: exports_external.string().optional(),
14866
+ connection_string: exports_external.string().optional()
14287
14867
  }
14288
- }, async ({ id, ...input }) => {
14289
- const biz = updateBusinessProfile(id, input);
14290
- if (!biz) {
14291
- return { content: [{ type: "text", text: `Business profile '${id}' not found.` }], isError: true };
14868
+ }, async (input) => {
14869
+ const db = openInvoiceDatabase();
14870
+ try {
14871
+ const result = await cloudPush(db, input.tables, input.connection_string);
14872
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
14873
+ } finally {
14874
+ db.close();
14292
14875
  }
14293
- return { content: [{ type: "text", text: JSON.stringify(biz, null, 2) }] };
14294
- });
14295
- server.registerTool("delete_business_profile", {
14296
- title: "Delete Business Profile",
14297
- description: "Delete a business profile.",
14298
- inputSchema: { id: exports_external.string() }
14299
- }, async ({ id }) => {
14300
- const deleted = deleteBusinessProfile(id);
14301
- return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
14302
- });
14303
- server.registerTool("list_tax_rules", {
14304
- title: "List Tax Rules",
14305
- description: "List tax rules. Optionally filter by country code.",
14306
- inputSchema: { country: exports_external.string().optional() }
14307
- }, async ({ country }) => {
14308
- const rules = country ? getTaxRulesForCountry(country) : listAllTaxRules();
14309
- return { content: [{ type: "text", text: JSON.stringify({ rules, count: rules.length }, null, 2) }] };
14310
14876
  });
14311
- server.registerTool("get_default_tax_rule", {
14312
- title: "Get Default Tax Rule",
14313
- description: "Get the default tax rule for a country.",
14314
- inputSchema: { country: exports_external.string() }
14315
- }, async ({ country }) => {
14316
- const rule = getDefaultTaxRule(country);
14317
- return { content: [{ type: "text", text: JSON.stringify(rule, null, 2) }] };
14318
- });
14319
- server.registerTool("create_tax_rule", {
14320
- title: "Create Tax Rule",
14321
- description: "Create a custom tax rule for a country or region.",
14877
+ server.registerTool("cloud_pull", {
14878
+ title: "Cloud Pull",
14879
+ description: "Pull cloud PostgreSQL tables into local SQLite.",
14322
14880
  inputSchema: {
14323
- country: exports_external.string(),
14324
- region: exports_external.string().optional(),
14325
- tax_name: exports_external.string(),
14326
- rate: exports_external.number(),
14327
- type: exports_external.enum(["vat", "sales_tax", "gst", "other"]).optional(),
14328
- is_default: exports_external.boolean().optional(),
14329
- reverse_charge: exports_external.boolean().optional(),
14330
- description: exports_external.string().optional()
14881
+ tables: exports_external.string().optional(),
14882
+ connection_string: exports_external.string().optional()
14883
+ }
14884
+ }, async (input) => {
14885
+ const db = openInvoiceDatabase();
14886
+ try {
14887
+ const result = await cloudPull(db, input.tables, input.connection_string);
14888
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
14889
+ } finally {
14890
+ db.close();
14331
14891
  }
14332
- }, async (params) => {
14333
- const rule = createTaxRule(params);
14334
- return { content: [{ type: "text", text: JSON.stringify(rule, null, 2) }] };
14335
- });
14336
- server.registerTool("delete_tax_rule", {
14337
- title: "Delete Tax Rule",
14338
- description: "Delete a tax rule.",
14339
- inputSchema: { id: exports_external.string() }
14340
- }, async ({ id }) => {
14341
- const deleted = deleteTaxRule(id);
14342
- return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
14343
14892
  });
14344
- server.registerTool("determine_tax", {
14345
- title: "Determine Tax",
14346
- description: "Determine the applicable tax rate for a transaction between issuer and client countries. Handles EU reverse charge automatically.",
14893
+ server.registerTool("send_feedback", {
14894
+ title: "Send Feedback",
14895
+ description: "Send feedback to remote endpoint with local persistence fallback.",
14347
14896
  inputSchema: {
14348
- issuer_country: exports_external.string(),
14349
- client_country: exports_external.string(),
14350
- client_vat_number: exports_external.string().optional()
14897
+ message: exports_external.string().min(1),
14898
+ email: exports_external.string().email().optional(),
14899
+ version: exports_external.string().optional(),
14900
+ machine_id: exports_external.string().optional()
14901
+ }
14902
+ }, async (input) => {
14903
+ const db = openInvoiceDatabase();
14904
+ try {
14905
+ const result = await submitFeedback(db, {
14906
+ service: "invoices",
14907
+ message: input.message,
14908
+ email: input.email,
14909
+ version: input.version,
14910
+ machine_id: input.machine_id
14911
+ });
14912
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
14913
+ } finally {
14914
+ db.close();
14351
14915
  }
14352
- }, async ({ issuer_country, client_country, client_vat_number }) => {
14353
- const result = determineTax(issuer_country, client_country, client_vat_number);
14354
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
14355
- });
14356
- async function main() {
14357
- const transport = new StdioServerTransport;
14358
- await server.connect(transport);
14359
- console.error("invoices MCP server running on stdio");
14360
- }
14361
- main().catch((error) => {
14362
- console.error("Fatal error:", error);
14363
- process.exit(1);
14364
14916
  });
14917
+ var transport = new StdioServerTransport;
14918
+ await server.connect(transport);