@hasna/conversations 0.2.24 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/mcp.js CHANGED
@@ -6537,6 +6537,75 @@ var require_dist = __commonJS((exports, module) => {
6537
6537
  });
6538
6538
 
6539
6539
  // node_modules/@hasna/cloud/dist/index.js
6540
+ var exports_dist = {};
6541
+ __export(exports_dist, {
6542
+ translateSql: () => translateSql,
6543
+ translateParams: () => translateParams,
6544
+ translateDdl: () => translateDdl,
6545
+ syncPush: () => syncPush,
6546
+ syncPull: () => syncPull,
6547
+ storeConflicts: () => storeConflicts,
6548
+ setupAutoSync: () => setupAutoSync,
6549
+ sendFeedback: () => sendFeedback,
6550
+ saveFeedback: () => saveFeedback,
6551
+ saveCloudConfig: () => saveCloudConfig,
6552
+ runScheduledSync: () => runScheduledSync,
6553
+ resolveConflicts: () => resolveConflicts,
6554
+ resolveConflict: () => resolveConflict,
6555
+ resetSyncMeta: () => resetSyncMeta,
6556
+ resetAllSyncMeta: () => resetAllSyncMeta,
6557
+ removeSyncSchedule: () => removeSyncSchedule,
6558
+ registerSyncSchedule: () => registerSyncSchedule,
6559
+ registerCloudTools: () => registerCloudTools,
6560
+ registerCloudCommands: () => registerCloudCommands,
6561
+ purgeResolvedConflicts: () => purgeResolvedConflicts,
6562
+ parseInterval: () => parseInterval,
6563
+ minutesToCron: () => minutesToCron,
6564
+ migrateService: () => migrateService,
6565
+ migrateDotfile: () => migrateDotfile,
6566
+ migrateAllServices: () => migrateAllServices,
6567
+ listSqliteTables: () => listSqliteTables,
6568
+ listPgTables: () => listPgTables,
6569
+ listFeedback: () => listFeedback,
6570
+ listConflicts: () => listConflicts,
6571
+ isSyncExcludedTable: () => isSyncExcludedTable,
6572
+ incrementalSyncPush: () => incrementalSyncPush,
6573
+ incrementalSyncPull: () => incrementalSyncPull,
6574
+ hasLegacyDotfile: () => hasLegacyDotfile,
6575
+ getWinningData: () => getWinningData,
6576
+ getSyncScheduleStatus: () => getSyncScheduleStatus,
6577
+ getSyncMetaForTable: () => getSyncMetaForTable,
6578
+ getSyncMetaAll: () => getSyncMetaAll,
6579
+ getServiceDbPath: () => getServiceDbPath,
6580
+ getHasnaDir: () => getHasnaDir,
6581
+ getDbPath: () => getDbPath,
6582
+ getDataDir: () => getDataDir,
6583
+ getConnectionString: () => getConnectionString,
6584
+ getConflict: () => getConflict,
6585
+ getConfigPath: () => getConfigPath,
6586
+ getConfigDir: () => getConfigDir,
6587
+ getCloudConfig: () => getCloudConfig,
6588
+ getAutoSyncConfig: () => getAutoSyncConfig,
6589
+ ensureSyncMetaTable: () => ensureSyncMetaTable,
6590
+ ensurePgDatabase: () => ensurePgDatabase,
6591
+ ensureFeedbackTable: () => ensureFeedbackTable,
6592
+ ensureConflictsTable: () => ensureConflictsTable,
6593
+ ensureAllPgDatabases: () => ensureAllPgDatabases,
6594
+ enableAutoSync: () => enableAutoSync,
6595
+ discoverSyncableServicesV2: () => discoverSyncableServices,
6596
+ discoverSyncableServices: () => discoverSyncableServices2,
6597
+ discoverServices: () => discoverServices,
6598
+ detectConflicts: () => detectConflicts,
6599
+ createDatabase: () => createDatabase,
6600
+ applyPgMigrations: () => applyPgMigrations,
6601
+ SyncProgressTracker: () => SyncProgressTracker,
6602
+ SqliteAdapter: () => SqliteAdapter,
6603
+ SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
6604
+ PgAdapterAsync: () => PgAdapterAsync,
6605
+ PgAdapter: () => PgAdapter,
6606
+ KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES,
6607
+ CloudConfigSchema: () => CloudConfigSchema
6608
+ });
6540
6609
  import { createRequire } from "module";
6541
6610
  import { Database } from "bun:sqlite";
6542
6611
  import {
@@ -6554,9 +6623,13 @@ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
6554
6623
  import { join as join3 } from "path";
6555
6624
  import { homedir as homedir3 } from "os";
6556
6625
  import { hostname as hostname3 } from "os";
6626
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
6557
6627
  import { homedir as homedir4 } from "os";
6558
6628
  import { join as join4 } from "path";
6629
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
6630
+ import { join as join5 } from "path";
6559
6631
  import { join as join6, dirname } from "path";
6632
+ import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
6560
6633
  import { homedir as homedir5, platform } from "os";
6561
6634
  function __accessProp2(key) {
6562
6635
  return this[key];
@@ -6611,6 +6684,17 @@ function sqliteToPostgres(sql) {
6611
6684
  }
6612
6685
  return out;
6613
6686
  }
6687
+ function translateDdl(ddl, dialect) {
6688
+ if (dialect === "sqlite")
6689
+ return ddl;
6690
+ let out = ddl;
6691
+ out = out.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/gi, "BIGSERIAL PRIMARY KEY");
6692
+ out = out.replace(/\bAUTOINCREMENT\b/gi, "");
6693
+ out = out.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
6694
+ out = out.replace(/\bBLOB\b/gi, "BYTEA");
6695
+ out = sqliteToPostgres(out);
6696
+ return out;
6697
+ }
6614
6698
 
6615
6699
  class SqliteAdapter {
6616
6700
  db;
@@ -7436,6 +7520,39 @@ function getDbPath(serviceName) {
7436
7520
  const dir = getDataDir(serviceName);
7437
7521
  return join(dir, `${serviceName}.db`);
7438
7522
  }
7523
+ function migrateDotfile(serviceName) {
7524
+ const legacyDir = join(homedir(), `.${serviceName}`);
7525
+ const newDir = join(HASNA_DIR, serviceName);
7526
+ if (!existsSync(legacyDir))
7527
+ return [];
7528
+ if (existsSync(newDir))
7529
+ return [];
7530
+ mkdirSync(newDir, { recursive: true });
7531
+ const migrated = [];
7532
+ copyDirRecursive(legacyDir, newDir, legacyDir, migrated);
7533
+ return migrated;
7534
+ }
7535
+ function copyDirRecursive(src, dest, root, migrated) {
7536
+ const entries = readdirSync(src, { withFileTypes: true });
7537
+ for (const entry of entries) {
7538
+ const srcPath = join(src, entry.name);
7539
+ const destPath = join(dest, entry.name);
7540
+ if (entry.isDirectory()) {
7541
+ mkdirSync(destPath, { recursive: true });
7542
+ copyDirRecursive(srcPath, destPath, root, migrated);
7543
+ } else {
7544
+ copyFileSync(srcPath, destPath);
7545
+ migrated.push(relative(root, srcPath));
7546
+ }
7547
+ }
7548
+ }
7549
+ function hasLegacyDotfile(serviceName) {
7550
+ return existsSync(join(homedir(), `.${serviceName}`));
7551
+ }
7552
+ function getHasnaDir() {
7553
+ mkdirSync(HASNA_DIR, { recursive: true });
7554
+ return HASNA_DIR;
7555
+ }
7439
7556
  function getConfigDir() {
7440
7557
  return CONFIG_DIR;
7441
7558
  }
@@ -8008,6 +8125,10 @@ async function sendFeedback(feedback, db) {
8008
8125
  return { sent: false, id, error: errorMsg };
8009
8126
  }
8010
8127
  }
8128
+ function listFeedback(db) {
8129
+ ensureFeedbackTable(db);
8130
+ return db.all(`SELECT id, service, version, message, email, machine_id, created_at FROM feedback ORDER BY created_at DESC`);
8131
+ }
8011
8132
 
8012
8133
  class SyncProgressTracker {
8013
8134
  db;
@@ -8128,6 +8249,847 @@ class SyncProgressTracker {
8128
8249
  }
8129
8250
  }
8130
8251
  }
8252
+ function detectConflicts(local, remote, table, primaryKey = "id", conflictColumn = "updated_at") {
8253
+ const conflicts = [];
8254
+ const remoteMap = new Map;
8255
+ for (const row of remote) {
8256
+ const key = String(row[primaryKey]);
8257
+ remoteMap.set(key, row);
8258
+ }
8259
+ for (const localRow of local) {
8260
+ const key = String(localRow[primaryKey]);
8261
+ const remoteRow = remoteMap.get(key);
8262
+ if (!remoteRow)
8263
+ continue;
8264
+ const localTs = localRow[conflictColumn];
8265
+ const remoteTs = remoteRow[conflictColumn];
8266
+ if (localTs !== remoteTs) {
8267
+ conflicts.push({
8268
+ table,
8269
+ row_id: key,
8270
+ local_updated_at: String(localTs ?? ""),
8271
+ remote_updated_at: String(remoteTs ?? ""),
8272
+ local_data: { ...localRow },
8273
+ remote_data: { ...remoteRow },
8274
+ resolved: false
8275
+ });
8276
+ }
8277
+ }
8278
+ return conflicts;
8279
+ }
8280
+ function resolveConflicts(conflicts, strategy = "newest-wins") {
8281
+ return conflicts.map((conflict) => {
8282
+ const resolved = { ...conflict, resolved: true, resolution: strategy };
8283
+ switch (strategy) {
8284
+ case "local-wins":
8285
+ break;
8286
+ case "remote-wins":
8287
+ break;
8288
+ case "newest-wins": {
8289
+ const localTime = new Date(conflict.local_updated_at).getTime();
8290
+ const remoteTime = new Date(conflict.remote_updated_at).getTime();
8291
+ if (remoteTime > localTime) {
8292
+ resolved.resolution = "newest-wins";
8293
+ } else {
8294
+ resolved.resolution = "newest-wins";
8295
+ }
8296
+ break;
8297
+ }
8298
+ }
8299
+ return resolved;
8300
+ });
8301
+ }
8302
+ function getWinningData(conflict) {
8303
+ if (!conflict.resolved || !conflict.resolution) {
8304
+ throw new Error(`Conflict for row ${conflict.row_id} is not resolved`);
8305
+ }
8306
+ switch (conflict.resolution) {
8307
+ case "local-wins":
8308
+ return conflict.local_data;
8309
+ case "remote-wins":
8310
+ return conflict.remote_data;
8311
+ case "newest-wins": {
8312
+ const localTime = new Date(conflict.local_updated_at).getTime();
8313
+ const remoteTime = new Date(conflict.remote_updated_at).getTime();
8314
+ return remoteTime >= localTime ? conflict.remote_data : conflict.local_data;
8315
+ }
8316
+ case "manual":
8317
+ return conflict.local_data;
8318
+ default:
8319
+ return conflict.local_data;
8320
+ }
8321
+ }
8322
+ function ensureConflictsTable(db) {
8323
+ db.exec(`
8324
+ CREATE TABLE IF NOT EXISTS _sync_conflicts (
8325
+ id TEXT PRIMARY KEY,
8326
+ table_name TEXT,
8327
+ row_id TEXT,
8328
+ local_data TEXT,
8329
+ remote_data TEXT,
8330
+ local_updated_at TEXT,
8331
+ remote_updated_at TEXT,
8332
+ resolution TEXT,
8333
+ resolved_at TEXT,
8334
+ created_at TEXT DEFAULT (datetime('now'))
8335
+ )
8336
+ `);
8337
+ }
8338
+ function storeConflicts(db, conflicts) {
8339
+ ensureConflictsTable(db);
8340
+ for (const conflict of conflicts) {
8341
+ const id = `${conflict.table}:${conflict.row_id}:${Date.now()}`;
8342
+ db.run(`INSERT INTO _sync_conflicts (id, table_name, row_id, local_data, remote_data, local_updated_at, remote_updated_at, resolution, resolved_at)
8343
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, conflict.table, conflict.row_id, JSON.stringify(conflict.local_data), JSON.stringify(conflict.remote_data), conflict.local_updated_at, conflict.remote_updated_at, conflict.resolution ?? null, conflict.resolved ? new Date().toISOString() : null);
8344
+ }
8345
+ }
8346
+ function listConflicts(db, opts) {
8347
+ ensureConflictsTable(db);
8348
+ let sql = `SELECT * FROM _sync_conflicts WHERE 1=1`;
8349
+ const params = [];
8350
+ if (opts?.resolved !== undefined) {
8351
+ if (opts.resolved) {
8352
+ sql += ` AND resolution IS NOT NULL AND resolved_at IS NOT NULL`;
8353
+ } else {
8354
+ sql += ` AND (resolution IS NULL OR resolved_at IS NULL)`;
8355
+ }
8356
+ }
8357
+ if (opts?.table) {
8358
+ sql += ` AND table_name = ?`;
8359
+ params.push(opts.table);
8360
+ }
8361
+ sql += ` ORDER BY created_at DESC`;
8362
+ return db.all(sql, ...params);
8363
+ }
8364
+ function resolveConflict(db, conflictId, strategy) {
8365
+ ensureConflictsTable(db);
8366
+ const row = db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
8367
+ if (!row)
8368
+ return null;
8369
+ db.run(`UPDATE _sync_conflicts SET resolution = ?, resolved_at = datetime('now') WHERE id = ?`, strategy, conflictId);
8370
+ return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
8371
+ }
8372
+ function getConflict(db, conflictId) {
8373
+ ensureConflictsTable(db);
8374
+ return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
8375
+ }
8376
+ function purgeResolvedConflicts(db) {
8377
+ ensureConflictsTable(db);
8378
+ const result = db.run(`DELETE FROM _sync_conflicts WHERE resolution IS NOT NULL AND resolved_at IS NOT NULL`);
8379
+ return result.changes;
8380
+ }
8381
+ function ensureSyncMetaTable(db) {
8382
+ db.exec(SYNC_META_TABLE_SQL);
8383
+ }
8384
+ function getSyncMeta(db, table) {
8385
+ ensureSyncMetaTable(db);
8386
+ return db.get(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?`, table) ?? null;
8387
+ }
8388
+ function upsertSyncMeta(db, meta3) {
8389
+ ensureSyncMetaTable(db);
8390
+ const existing = db.get(`SELECT table_name FROM _sync_meta WHERE table_name = ?`, meta3.table_name);
8391
+ if (existing) {
8392
+ db.run(`UPDATE _sync_meta SET last_synced_at = ?, last_synced_row_count = ?, direction = ? WHERE table_name = ?`, meta3.last_synced_at, meta3.last_synced_row_count, meta3.direction, meta3.table_name);
8393
+ } else {
8394
+ db.run(`INSERT INTO _sync_meta (table_name, last_synced_at, last_synced_row_count, direction) VALUES (?, ?, ?, ?)`, meta3.table_name, meta3.last_synced_at, meta3.last_synced_row_count, meta3.direction);
8395
+ }
8396
+ }
8397
+ function transferRows(source, target, table, rows, options) {
8398
+ const { primaryKey = "id", conflictColumn = "updated_at" } = options;
8399
+ let written = 0;
8400
+ let skipped = 0;
8401
+ const errors22 = [];
8402
+ if (rows.length === 0)
8403
+ return { written, skipped, errors: errors22 };
8404
+ const columns = Object.keys(rows[0]);
8405
+ const hasConflictCol = columns.includes(conflictColumn);
8406
+ const hasPrimaryKey = columns.includes(primaryKey);
8407
+ if (!hasPrimaryKey) {
8408
+ errors22.push(`Table "${table}" has no "${primaryKey}" column -- skipping`);
8409
+ return { written, skipped, errors: errors22 };
8410
+ }
8411
+ for (const row of rows) {
8412
+ try {
8413
+ const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
8414
+ if (existing) {
8415
+ if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
8416
+ const existingTime = new Date(existing[conflictColumn]).getTime();
8417
+ const incomingTime = new Date(row[conflictColumn]).getTime();
8418
+ if (existingTime >= incomingTime) {
8419
+ skipped++;
8420
+ continue;
8421
+ }
8422
+ }
8423
+ const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
8424
+ const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
8425
+ values.push(row[primaryKey]);
8426
+ target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
8427
+ } else {
8428
+ const placeholders = columns.map(() => "?").join(", ");
8429
+ const colList = columns.map((c) => `"${c}"`).join(", ");
8430
+ const values = columns.map((c) => row[c]);
8431
+ target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
8432
+ }
8433
+ written++;
8434
+ } catch (err) {
8435
+ errors22.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
8436
+ }
8437
+ }
8438
+ return { written, skipped, errors: errors22 };
8439
+ }
8440
+ function incrementalSyncPush(local, remote, tables, options = {}) {
8441
+ const { conflictColumn = "updated_at", batchSize = 500 } = options;
8442
+ const results = [];
8443
+ ensureSyncMetaTable(local);
8444
+ for (const table of tables) {
8445
+ const stat = {
8446
+ table,
8447
+ total_rows: 0,
8448
+ synced_rows: 0,
8449
+ skipped_rows: 0,
8450
+ errors: [],
8451
+ first_sync: false
8452
+ };
8453
+ try {
8454
+ const countResult = local.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
8455
+ stat.total_rows = countResult?.cnt ?? 0;
8456
+ const meta3 = getSyncMeta(local, table);
8457
+ let rows;
8458
+ if (meta3?.last_synced_at) {
8459
+ try {
8460
+ rows = local.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta3.last_synced_at);
8461
+ } catch {
8462
+ rows = local.all(`SELECT * FROM "${table}"`);
8463
+ stat.first_sync = true;
8464
+ }
8465
+ } else {
8466
+ rows = local.all(`SELECT * FROM "${table}"`);
8467
+ stat.first_sync = true;
8468
+ }
8469
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
8470
+ const batch = rows.slice(offset, offset + batchSize);
8471
+ const result = transferRows(local, remote, table, batch, options);
8472
+ stat.synced_rows += result.written;
8473
+ stat.skipped_rows += result.skipped;
8474
+ stat.errors.push(...result.errors);
8475
+ }
8476
+ if (rows.length === 0) {
8477
+ stat.skipped_rows = stat.total_rows;
8478
+ }
8479
+ const now = new Date().toISOString();
8480
+ upsertSyncMeta(local, {
8481
+ table_name: table,
8482
+ last_synced_at: now,
8483
+ last_synced_row_count: stat.synced_rows,
8484
+ direction: "push"
8485
+ });
8486
+ } catch (err) {
8487
+ stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
8488
+ }
8489
+ results.push(stat);
8490
+ }
8491
+ return results;
8492
+ }
8493
+ function incrementalSyncPull(remote, local, tables, options = {}) {
8494
+ const { conflictColumn = "updated_at", batchSize = 500 } = options;
8495
+ const results = [];
8496
+ ensureSyncMetaTable(local);
8497
+ for (const table of tables) {
8498
+ const stat = {
8499
+ table,
8500
+ total_rows: 0,
8501
+ synced_rows: 0,
8502
+ skipped_rows: 0,
8503
+ errors: [],
8504
+ first_sync: false
8505
+ };
8506
+ try {
8507
+ const countResult = remote.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
8508
+ stat.total_rows = countResult?.cnt ?? 0;
8509
+ const meta3 = getSyncMeta(local, table);
8510
+ let rows;
8511
+ if (meta3?.last_synced_at) {
8512
+ try {
8513
+ rows = remote.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta3.last_synced_at);
8514
+ } catch {
8515
+ rows = remote.all(`SELECT * FROM "${table}"`);
8516
+ stat.first_sync = true;
8517
+ }
8518
+ } else {
8519
+ rows = remote.all(`SELECT * FROM "${table}"`);
8520
+ stat.first_sync = true;
8521
+ }
8522
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
8523
+ const batch = rows.slice(offset, offset + batchSize);
8524
+ const result = transferRows(remote, local, table, batch, options);
8525
+ stat.synced_rows += result.written;
8526
+ stat.skipped_rows += result.skipped;
8527
+ stat.errors.push(...result.errors);
8528
+ }
8529
+ if (rows.length === 0) {
8530
+ stat.skipped_rows = stat.total_rows;
8531
+ }
8532
+ const now = new Date().toISOString();
8533
+ upsertSyncMeta(local, {
8534
+ table_name: table,
8535
+ last_synced_at: now,
8536
+ last_synced_row_count: stat.synced_rows,
8537
+ direction: "pull"
8538
+ });
8539
+ } catch (err) {
8540
+ stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
8541
+ }
8542
+ results.push(stat);
8543
+ }
8544
+ return results;
8545
+ }
8546
+ function getSyncMetaAll(db) {
8547
+ ensureSyncMetaTable(db);
8548
+ return db.all(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta ORDER BY table_name`);
8549
+ }
8550
+ function getSyncMetaForTable(db, table) {
8551
+ return getSyncMeta(db, table);
8552
+ }
8553
+ function resetSyncMeta(db, table) {
8554
+ ensureSyncMetaTable(db);
8555
+ db.run(`DELETE FROM _sync_meta WHERE table_name = ?`, table);
8556
+ }
8557
+ function resetAllSyncMeta(db) {
8558
+ ensureSyncMetaTable(db);
8559
+ db.run(`DELETE FROM _sync_meta`);
8560
+ }
8561
+ function getAutoSyncConfig() {
8562
+ try {
8563
+ if (!existsSync4(AUTO_SYNC_CONFIG_PATH)) {
8564
+ return { ...DEFAULT_AUTO_SYNC_CONFIG };
8565
+ }
8566
+ const raw = JSON.parse(readFileSync2(AUTO_SYNC_CONFIG_PATH, "utf-8"));
8567
+ return {
8568
+ auto_sync_on_start: typeof raw.auto_sync_on_start === "boolean" ? raw.auto_sync_on_start : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_start,
8569
+ auto_sync_on_stop: typeof raw.auto_sync_on_stop === "boolean" ? raw.auto_sync_on_stop : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_stop
8570
+ };
8571
+ } catch {
8572
+ return { ...DEFAULT_AUTO_SYNC_CONFIG };
8573
+ }
8574
+ }
8575
+ async function executeAutoSync(event, serviceName, local, tables) {
8576
+ const direction = event === "start" ? "pull" : "push";
8577
+ const result = {
8578
+ event,
8579
+ direction,
8580
+ success: false,
8581
+ tables_synced: 0,
8582
+ total_rows_synced: 0,
8583
+ errors: []
8584
+ };
8585
+ let remote = null;
8586
+ try {
8587
+ const connStr = getConnectionString(serviceName);
8588
+ remote = new PgAdapterAsync(connStr);
8589
+ const syncTables = tables.length > 0 ? tables.filter((t) => !isSyncExcludedTable(t)) : direction === "push" ? listSqliteTables(local).filter((t) => !isSyncExcludedTable(t)) : (await listPgTables(remote)).filter((t) => !isSyncExcludedTable(t));
8590
+ if (syncTables.length === 0) {
8591
+ result.success = true;
8592
+ return result;
8593
+ }
8594
+ const results = direction === "pull" ? await syncPull(remote, local, { tables: syncTables }) : await syncPush(local, remote, { tables: syncTables });
8595
+ for (const r of results) {
8596
+ if (r.errors.length === 0)
8597
+ result.tables_synced++;
8598
+ result.total_rows_synced += r.rowsWritten;
8599
+ result.errors.push(...r.errors);
8600
+ }
8601
+ result.success = result.errors.length === 0;
8602
+ } catch (err) {
8603
+ result.errors.push(err?.message ?? String(err));
8604
+ } finally {
8605
+ if (remote) {
8606
+ try {
8607
+ await remote.close();
8608
+ } catch {}
8609
+ }
8610
+ }
8611
+ return result;
8612
+ }
8613
+ function installSignalHandlers() {
8614
+ if (signalHandlersInstalled)
8615
+ return;
8616
+ signalHandlersInstalled = true;
8617
+ const handleExit = async () => {
8618
+ for (const fn of cleanupHandlers) {
8619
+ try {
8620
+ await fn();
8621
+ } catch {}
8622
+ }
8623
+ };
8624
+ process.on("SIGTERM", async () => {
8625
+ await handleExit();
8626
+ process.exit(0);
8627
+ });
8628
+ process.on("SIGINT", async () => {
8629
+ await handleExit();
8630
+ process.exit(0);
8631
+ });
8632
+ process.on("beforeExit", async () => {
8633
+ await handleExit();
8634
+ });
8635
+ }
8636
+ function setupAutoSync(serviceName, server, local, remote, tables) {
8637
+ const config2 = getAutoSyncConfig();
8638
+ const cloudConfig = getCloudConfig();
8639
+ const isSyncEnabled = cloudConfig.mode === "hybrid" || cloudConfig.mode === "cloud";
8640
+ const syncOnStart = async () => {
8641
+ if (!config2.auto_sync_on_start || !isSyncEnabled)
8642
+ return null;
8643
+ return executeAutoSync("start", serviceName, local, tables);
8644
+ };
8645
+ const syncOnStop = async () => {
8646
+ if (!config2.auto_sync_on_stop || !isSyncEnabled)
8647
+ return null;
8648
+ return executeAutoSync("stop", serviceName, local, tables);
8649
+ };
8650
+ if (server && typeof server.onconnect === "function") {
8651
+ const origOnConnect = server.onconnect;
8652
+ server.onconnect = async (...args) => {
8653
+ await syncOnStart();
8654
+ return origOnConnect.apply(server, args);
8655
+ };
8656
+ } else if (server && typeof server.on === "function") {
8657
+ server.on("connect", () => syncOnStart());
8658
+ }
8659
+ if (server && typeof server.ondisconnect === "function") {
8660
+ const origOnDisconnect = server.ondisconnect;
8661
+ server.ondisconnect = async (...args) => {
8662
+ await syncOnStop();
8663
+ return origOnDisconnect.apply(server, args);
8664
+ };
8665
+ } else if (server && typeof server.on === "function") {
8666
+ server.on("disconnect", () => syncOnStop());
8667
+ }
8668
+ installSignalHandlers();
8669
+ cleanupHandlers.push(async () => {
8670
+ await syncOnStop();
8671
+ });
8672
+ return { syncOnStart, syncOnStop, config: config2 };
8673
+ }
8674
+ function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
8675
+ setupAutoSync(serviceName, mcpServer, local, remote, tables);
8676
+ }
8677
+ function discoverSyncableServices2() {
8678
+ const hasnaDir = getHasnaDir();
8679
+ const services = [];
8680
+ try {
8681
+ const entries = readdirSync3(hasnaDir, { withFileTypes: true });
8682
+ for (const entry of entries) {
8683
+ if (!entry.isDirectory())
8684
+ continue;
8685
+ const dbPath = join5(hasnaDir, entry.name, `${entry.name}.db`);
8686
+ if (existsSync5(dbPath)) {
8687
+ services.push(entry.name);
8688
+ }
8689
+ }
8690
+ } catch {}
8691
+ return services;
8692
+ }
8693
+ async function runScheduledSync() {
8694
+ const config2 = getCloudConfig();
8695
+ if (config2.mode === "local")
8696
+ return [];
8697
+ const services = discoverSyncableServices2();
8698
+ const results = [];
8699
+ let remote = null;
8700
+ for (const service of services) {
8701
+ const result = {
8702
+ service,
8703
+ tables_synced: 0,
8704
+ total_rows_synced: 0,
8705
+ errors: []
8706
+ };
8707
+ try {
8708
+ const dbPath = join5(getDataDir(service), `${service}.db`);
8709
+ if (!existsSync5(dbPath)) {
8710
+ continue;
8711
+ }
8712
+ const local = new SqliteAdapter(dbPath);
8713
+ const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
8714
+ if (tables.length === 0) {
8715
+ local.close();
8716
+ continue;
8717
+ }
8718
+ try {
8719
+ const connStr = getConnectionString(service);
8720
+ remote = new PgAdapterAsync(connStr);
8721
+ } catch (err) {
8722
+ result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
8723
+ local.close();
8724
+ results.push(result);
8725
+ continue;
8726
+ }
8727
+ const stats = incrementalSyncPush(local, remote, tables);
8728
+ for (const s of stats) {
8729
+ if (s.errors.length === 0) {
8730
+ result.tables_synced++;
8731
+ }
8732
+ result.total_rows_synced += s.synced_rows;
8733
+ result.errors.push(...s.errors);
8734
+ }
8735
+ local.close();
8736
+ await remote.close();
8737
+ remote = null;
8738
+ } catch (err) {
8739
+ result.errors.push(err?.message ?? String(err));
8740
+ }
8741
+ results.push(result);
8742
+ }
8743
+ if (remote) {
8744
+ try {
8745
+ await remote.close();
8746
+ } catch {}
8747
+ }
8748
+ return results;
8749
+ }
8750
+ function parseInterval(input) {
8751
+ const trimmed = input.trim().toLowerCase();
8752
+ const hourMatch = trimmed.match(/^(\d+)\s*h$/);
8753
+ if (hourMatch) {
8754
+ const hours = parseInt(hourMatch[1], 10);
8755
+ if (hours <= 0) {
8756
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
8757
+ }
8758
+ return hours * 60;
8759
+ }
8760
+ const minMatch = trimmed.match(/^(\d+)\s*m$/);
8761
+ if (minMatch) {
8762
+ const mins = parseInt(minMatch[1], 10);
8763
+ if (mins <= 0) {
8764
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
8765
+ }
8766
+ return mins;
8767
+ }
8768
+ const plain = parseInt(trimmed, 10);
8769
+ if (!isNaN(plain) && plain > 0) {
8770
+ return plain;
8771
+ }
8772
+ throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
8773
+ }
8774
+ function minutesToCron(minutes) {
8775
+ if (minutes <= 0) {
8776
+ throw new Error("Interval must be greater than 0 minutes.");
8777
+ }
8778
+ if (minutes < 60) {
8779
+ return `*/${minutes} * * * *`;
8780
+ }
8781
+ const hours = Math.floor(minutes / 60);
8782
+ const remainderMins = minutes % 60;
8783
+ if (remainderMins === 0 && hours <= 24) {
8784
+ return `0 */${hours} * * *`;
8785
+ }
8786
+ return `*/${minutes} * * * *`;
8787
+ }
8788
+ function getWorkerPath() {
8789
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
8790
+ const tsPath = join6(dir, "scheduled-sync.ts");
8791
+ const jsPath = join6(dir, "scheduled-sync.js");
8792
+ try {
8793
+ if (existsSync6(tsPath))
8794
+ return tsPath;
8795
+ } catch {}
8796
+ return jsPath;
8797
+ }
8798
+ function getBunPath() {
8799
+ const candidates = [
8800
+ join6(homedir5(), ".bun", "bin", "bun"),
8801
+ "/usr/local/bin/bun",
8802
+ "/usr/bin/bun"
8803
+ ];
8804
+ for (const p of candidates) {
8805
+ if (existsSync6(p))
8806
+ return p;
8807
+ }
8808
+ return "bun";
8809
+ }
8810
+ function getLaunchdPlistPath() {
8811
+ return join6(homedir5(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
8812
+ }
8813
+ function createLaunchdPlist(intervalMinutes) {
8814
+ const workerPath = getWorkerPath();
8815
+ const bunPath = getBunPath();
8816
+ const logPath = join6(CONFIG_DIR2, "sync.log");
8817
+ const errorLogPath = join6(CONFIG_DIR2, "sync-error.log");
8818
+ return `<?xml version="1.0" encoding="UTF-8"?>
8819
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
8820
+ <plist version="1.0">
8821
+ <dict>
8822
+ <key>Label</key>
8823
+ <string>com.hasna.cloud-sync</string>
8824
+ <key>ProgramArguments</key>
8825
+ <array>
8826
+ <string>${bunPath}</string>
8827
+ <string>run</string>
8828
+ <string>${workerPath}</string>
8829
+ </array>
8830
+ <key>StartInterval</key>
8831
+ <integer>${intervalMinutes * 60}</integer>
8832
+ <key>RunAtLoad</key>
8833
+ <true/>
8834
+ <key>StandardOutPath</key>
8835
+ <string>${logPath}</string>
8836
+ <key>StandardErrorPath</key>
8837
+ <string>${errorLogPath}</string>
8838
+ <key>EnvironmentVariables</key>
8839
+ <dict>
8840
+ <key>PATH</key>
8841
+ <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
8842
+ <key>HOME</key>
8843
+ <string>${homedir5()}</string>
8844
+ </dict>
8845
+ </dict>
8846
+ </plist>`;
8847
+ }
8848
+ async function registerLaunchd(intervalMinutes) {
8849
+ const plistPath = getLaunchdPlistPath();
8850
+ const plistDir = dirname(plistPath);
8851
+ mkdirSync3(plistDir, { recursive: true });
8852
+ try {
8853
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
8854
+ } catch {}
8855
+ writeFileSync2(plistPath, createLaunchdPlist(intervalMinutes));
8856
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
8857
+ }
8858
+ async function removeLaunchd() {
8859
+ const plistPath = getLaunchdPlistPath();
8860
+ try {
8861
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
8862
+ } catch {}
8863
+ try {
8864
+ unlinkSync(plistPath);
8865
+ } catch {}
8866
+ }
8867
+ function getSystemdDir() {
8868
+ return join6(homedir5(), ".config", "systemd", "user");
8869
+ }
8870
+ function createSystemdService() {
8871
+ const workerPath = getWorkerPath();
8872
+ const bunPath = getBunPath();
8873
+ return `[Unit]
8874
+ Description=Hasna Cloud Sync
8875
+ After=network.target
8876
+
8877
+ [Service]
8878
+ Type=oneshot
8879
+ ExecStart=${bunPath} run ${workerPath}
8880
+ Environment=HOME=${homedir5()}
8881
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
8882
+
8883
+ [Install]
8884
+ WantedBy=default.target
8885
+ `;
8886
+ }
8887
+ function createSystemdTimer(intervalMinutes) {
8888
+ return `[Unit]
8889
+ Description=Hasna Cloud Sync Timer
8890
+
8891
+ [Timer]
8892
+ OnBootSec=${intervalMinutes}min
8893
+ OnUnitActiveSec=${intervalMinutes}min
8894
+ Persistent=true
8895
+
8896
+ [Install]
8897
+ WantedBy=timers.target
8898
+ `;
8899
+ }
8900
+ async function registerSystemd(intervalMinutes) {
8901
+ const dir = getSystemdDir();
8902
+ mkdirSync3(dir, { recursive: true });
8903
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.service`), createSystemdService());
8904
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
8905
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
8906
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
8907
+ }
8908
+ async function removeSystemd() {
8909
+ try {
8910
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SERVICE_NAME}.timer`]).exited;
8911
+ } catch {}
8912
+ const dir = getSystemdDir();
8913
+ try {
8914
+ unlinkSync(join6(dir, `${SERVICE_NAME}.service`));
8915
+ } catch {}
8916
+ try {
8917
+ unlinkSync(join6(dir, `${SERVICE_NAME}.timer`));
8918
+ } catch {}
8919
+ try {
8920
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
8921
+ } catch {}
8922
+ }
8923
+ async function registerSyncSchedule(intervalMinutes) {
8924
+ if (intervalMinutes <= 0) {
8925
+ throw new Error("Interval must be a positive number of minutes.");
8926
+ }
8927
+ mkdirSync3(CONFIG_DIR2, { recursive: true });
8928
+ if (platform() === "darwin") {
8929
+ await registerLaunchd(intervalMinutes);
8930
+ } else {
8931
+ await registerSystemd(intervalMinutes);
8932
+ }
8933
+ const config2 = getCloudConfig();
8934
+ config2.sync.schedule_minutes = intervalMinutes;
8935
+ saveCloudConfig(config2);
8936
+ }
8937
+ async function removeSyncSchedule() {
8938
+ if (platform() === "darwin") {
8939
+ await removeLaunchd();
8940
+ } else {
8941
+ await removeSystemd();
8942
+ }
8943
+ const config2 = getCloudConfig();
8944
+ config2.sync.schedule_minutes = 0;
8945
+ saveCloudConfig(config2);
8946
+ }
8947
+ function getSyncScheduleStatus() {
8948
+ const config2 = getCloudConfig();
8949
+ const minutes = config2.sync.schedule_minutes;
8950
+ const registered = minutes > 0;
8951
+ let mechanism = "none";
8952
+ if (registered) {
8953
+ if (platform() === "darwin") {
8954
+ mechanism = existsSync6(getLaunchdPlistPath()) ? "launchd" : "none";
8955
+ } else {
8956
+ mechanism = existsSync6(join6(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
8957
+ }
8958
+ }
8959
+ return {
8960
+ registered,
8961
+ schedule_minutes: minutes,
8962
+ cron_expression: registered ? minutesToCron(minutes) : null,
8963
+ mechanism
8964
+ };
8965
+ }
8966
+ async function applyPgMigrations(connectionString, migrations, service = "unknown") {
8967
+ const pg2 = new PgAdapterAsync(connectionString);
8968
+ const result = {
8969
+ service,
8970
+ applied: [],
8971
+ alreadyApplied: [],
8972
+ errors: [],
8973
+ totalMigrations: migrations.length
8974
+ };
8975
+ try {
8976
+ await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
8977
+ id SERIAL PRIMARY KEY,
8978
+ version INT UNIQUE NOT NULL,
8979
+ applied_at TIMESTAMPTZ DEFAULT NOW()
8980
+ )`);
8981
+ const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
8982
+ const appliedSet = new Set(applied.map((r) => r.version));
8983
+ for (let i = 0;i < migrations.length; i++) {
8984
+ if (appliedSet.has(i)) {
8985
+ result.alreadyApplied.push(i);
8986
+ continue;
8987
+ }
8988
+ try {
8989
+ await pg2.exec(migrations[i]);
8990
+ await pg2.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
8991
+ result.applied.push(i);
8992
+ } catch (err) {
8993
+ result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
8994
+ break;
8995
+ }
8996
+ }
8997
+ } finally {
8998
+ await pg2.close();
8999
+ }
9000
+ return result;
9001
+ }
9002
+ function getServicePackage(service) {
9003
+ return `@hasna/${service}`;
9004
+ }
9005
+ async function loadServiceMigrations(service) {
9006
+ const pkg = getServicePackage(service);
9007
+ const paths = [
9008
+ `${pkg}/pg-migrations`,
9009
+ `${pkg}/dist/db/pg-migrations.js`,
9010
+ `${pkg}/dist/db/pg-migrations`
9011
+ ];
9012
+ for (const path of paths) {
9013
+ try {
9014
+ const mod = await import(path);
9015
+ if (Array.isArray(mod.PG_MIGRATIONS)) {
9016
+ return mod.PG_MIGRATIONS;
9017
+ }
9018
+ if (mod.default && Array.isArray(mod.default.PG_MIGRATIONS)) {
9019
+ return mod.default.PG_MIGRATIONS;
9020
+ }
9021
+ } catch {}
9022
+ }
9023
+ return null;
9024
+ }
9025
+ async function migrateService(service, connectionString) {
9026
+ const connStr = connectionString ?? getConnectionString(service);
9027
+ const migrations = await loadServiceMigrations(service);
9028
+ if (!migrations) {
9029
+ return {
9030
+ service,
9031
+ applied: [],
9032
+ alreadyApplied: [],
9033
+ errors: [`No PG migrations found for service "${service}"`],
9034
+ totalMigrations: 0
9035
+ };
9036
+ }
9037
+ return applyPgMigrations(connStr, migrations, service);
9038
+ }
9039
+ async function migrateAllServices() {
9040
+ const { discoverServices: discoverServices2 } = await Promise.resolve().then(() => (init_discover(), exports_discover));
9041
+ const services = discoverServices2();
9042
+ const results = [];
9043
+ for (const service of services) {
9044
+ try {
9045
+ const result = await migrateService(service);
9046
+ results.push(result);
9047
+ } catch (err) {
9048
+ results.push({
9049
+ service,
9050
+ applied: [],
9051
+ alreadyApplied: [],
9052
+ errors: [err?.message ?? String(err)],
9053
+ totalMigrations: 0
9054
+ });
9055
+ }
9056
+ }
9057
+ return results;
9058
+ }
9059
+ async function ensurePgDatabase(service) {
9060
+ const config2 = (await Promise.resolve().then(() => (init_config(), exports_config))).getCloudConfig();
9061
+ const { host, port, username, password_env, ssl } = config2.rds;
9062
+ if (!host || !username)
9063
+ return false;
9064
+ const password = process.env[password_env] ?? "";
9065
+ const sslParam = ssl ? "?sslmode=require" : "";
9066
+ const adminConnStr = `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/postgres${sslParam}`;
9067
+ const pg2 = new PgAdapterAsync(adminConnStr);
9068
+ try {
9069
+ const existing = await pg2.all(`SELECT 1 FROM pg_database WHERE datname = $1`, service);
9070
+ if (existing.length === 0) {
9071
+ await pg2.exec(`CREATE DATABASE "${service}"`);
9072
+ return true;
9073
+ }
9074
+ return false;
9075
+ } finally {
9076
+ await pg2.close();
9077
+ }
9078
+ }
9079
+ async function ensureAllPgDatabases() {
9080
+ const { discoverServices: discoverServices2 } = await Promise.resolve().then(() => (init_discover(), exports_discover));
9081
+ const services = discoverServices2();
9082
+ const results = [];
9083
+ for (const service of services) {
9084
+ try {
9085
+ const created = await ensurePgDatabase(service);
9086
+ results.push({ service, created });
9087
+ } catch (err) {
9088
+ results.push({ service, created: false, error: err?.message ?? String(err) });
9089
+ }
9090
+ }
9091
+ return results;
9092
+ }
8131
9093
  function registerCloudTools(server, serviceName) {
8132
9094
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
8133
9095
  const config2 = getCloudConfig();
@@ -8228,6 +9190,85 @@ function registerCloudTools(server, serviceName) {
8228
9190
  };
8229
9191
  });
8230
9192
  }
9193
+ function registerCloudCommands(program, serviceName) {
9194
+ const cloudCmd = program.command("cloud").description("Cloud sync and feedback commands");
9195
+ cloudCmd.command("status").description("Show cloud config and connection health").action(async () => {
9196
+ const config2 = getCloudConfig();
9197
+ console.log("Mode:", config2.mode);
9198
+ console.log("RDS Host:", config2.rds.host || "(not configured)");
9199
+ console.log("Service:", serviceName);
9200
+ if (config2.rds.host && config2.rds.username) {
9201
+ try {
9202
+ const connStr = getConnectionString("postgres");
9203
+ const pg2 = new PgAdapterAsync(connStr);
9204
+ await pg2.get("SELECT 1 as ok");
9205
+ console.log("PostgreSQL: connected");
9206
+ await pg2.close();
9207
+ } catch (err) {
9208
+ console.log("PostgreSQL: connection failed \u2014", err?.message);
9209
+ }
9210
+ }
9211
+ });
9212
+ cloudCmd.command("push").description("Push local data to cloud").option("--tables <tables>", "Comma-separated table names").action(async (opts) => {
9213
+ const config2 = getCloudConfig();
9214
+ if (config2.mode === "local") {
9215
+ console.error("Error: mode is 'local'. Run `cloud setup` first.");
9216
+ process.exit(1);
9217
+ }
9218
+ const local = new SqliteAdapter(getDbPath(serviceName));
9219
+ const cloud = new PgAdapterAsync(getConnectionString(serviceName));
9220
+ const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables(local);
9221
+ const results = await syncPush(local, cloud, {
9222
+ tables,
9223
+ onProgress: (p) => {
9224
+ if (p.phase === "done") {
9225
+ console.log(` ${p.table}: ${p.rowsWritten} rows pushed`);
9226
+ }
9227
+ }
9228
+ });
9229
+ local.close();
9230
+ await cloud.close();
9231
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
9232
+ console.log(`Done. ${total} rows pushed.`);
9233
+ });
9234
+ cloudCmd.command("pull").description("Pull cloud data to local").option("--tables <tables>", "Comma-separated table names").action(async (opts) => {
9235
+ const config2 = getCloudConfig();
9236
+ if (config2.mode === "local") {
9237
+ console.error("Error: mode is 'local'. Run `cloud setup` first.");
9238
+ process.exit(1);
9239
+ }
9240
+ const local = new SqliteAdapter(getDbPath(serviceName));
9241
+ const cloud = new PgAdapterAsync(getConnectionString(serviceName));
9242
+ let tables;
9243
+ if (opts.tables) {
9244
+ tables = opts.tables.split(",").map((t) => t.trim());
9245
+ } else {
9246
+ tables = await listPgTables(cloud);
9247
+ }
9248
+ const results = await syncPull(cloud, local, {
9249
+ tables,
9250
+ onProgress: (p) => {
9251
+ if (p.phase === "done") {
9252
+ console.log(` ${p.table}: ${p.rowsWritten} rows pulled`);
9253
+ }
9254
+ }
9255
+ });
9256
+ local.close();
9257
+ await cloud.close();
9258
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
9259
+ console.log(`Done. ${total} rows pulled.`);
9260
+ });
9261
+ cloudCmd.command("feedback").description("Send feedback").requiredOption("--message <msg>", "Feedback message").option("--email <email>", "Contact email").action(async (opts) => {
9262
+ const db = createDatabase({ service: "cloud" });
9263
+ const result = await sendFeedback({ service: serviceName, message: opts.message, email: opts.email }, db);
9264
+ db.close();
9265
+ if (result.sent) {
9266
+ console.log(`Feedback sent (id: ${result.id})`);
9267
+ } else {
9268
+ console.log(`Feedback saved locally (id: ${result.id}): ${result.error}`);
9269
+ }
9270
+ });
9271
+ }
8231
9272
  var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node2, __toESMCache_esm2, __toESM2 = (mod, isNodeMode, target) => {
8232
9273
  var canCache = mod != null && typeof mod === "object";
8233
9274
  if (canCache) {
@@ -8480,7 +9521,13 @@ CREATE TABLE IF NOT EXISTS feedback (
8480
9521
  email TEXT DEFAULT '',
8481
9522
  machine_id TEXT DEFAULT '',
8482
9523
  created_at TEXT DEFAULT (datetime('now'))
8483
- )`, AUTO_SYNC_CONFIG_PATH, CONFIG_DIR2;
9524
+ )`, SYNC_META_TABLE_SQL = `
9525
+ CREATE TABLE IF NOT EXISTS _sync_meta (
9526
+ table_name TEXT PRIMARY KEY,
9527
+ last_synced_at TEXT,
9528
+ last_synced_row_count INTEGER DEFAULT 0,
9529
+ direction TEXT DEFAULT 'push'
9530
+ )`, AUTO_SYNC_CONFIG_PATH, DEFAULT_AUTO_SYNC_CONFIG, cleanupHandlers, signalHandlersInstalled = false, SERVICE_NAME = "hasna-cloud-sync", CONFIG_DIR2;
8484
9531
  var init_dist = __esm(() => {
8485
9532
  __create2 = Object.create;
8486
9533
  __getProtoOf2 = Object.getPrototypeOf;
@@ -16548,6 +17595,11 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
16548
17595
  init_config();
16549
17596
  init_discover();
16550
17597
  AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
17598
+ DEFAULT_AUTO_SYNC_CONFIG = {
17599
+ auto_sync_on_start: true,
17600
+ auto_sync_on_stop: true
17601
+ };
17602
+ cleanupHandlers = [];
16551
17603
  init_config();
16552
17604
  init_adapter();
16553
17605
  init_dotfile();
@@ -16573,23 +17625,23 @@ __export(exports_db, {
16573
17625
  getDataDir: () => getDataDir2,
16574
17626
  closeDb: () => closeDb
16575
17627
  });
16576
- import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync } from "fs";
16577
- import { join as join5, dirname as dirname2 } from "path";
17628
+ import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync4, statSync } from "fs";
17629
+ import { join as join7, dirname as dirname2 } from "path";
16578
17630
  import { homedir as homedir6 } from "os";
16579
17631
  function getDataDir2() {
16580
17632
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir6();
16581
- const newDir = join5(home, ".hasna", "conversations");
16582
- const oldDir = join5(home, ".conversations");
16583
- if (existsSync4(oldDir) && !existsSync4(newDir)) {
16584
- mkdirSync3(newDir, { recursive: true });
16585
- for (const file2 of readdirSync3(oldDir)) {
16586
- const oldPath = join5(oldDir, file2);
17633
+ const newDir = join7(home, ".hasna", "conversations");
17634
+ const oldDir = join7(home, ".conversations");
17635
+ if (existsSync7(oldDir) && !existsSync7(newDir)) {
17636
+ mkdirSync4(newDir, { recursive: true });
17637
+ for (const file2 of readdirSync4(oldDir)) {
17638
+ const oldPath = join7(oldDir, file2);
16587
17639
  if (statSync(oldPath).isFile()) {
16588
- copyFileSync2(oldPath, join5(newDir, file2));
17640
+ copyFileSync2(oldPath, join7(newDir, file2));
16589
17641
  }
16590
17642
  }
16591
17643
  }
16592
- mkdirSync3(newDir, { recursive: true });
17644
+ mkdirSync4(newDir, { recursive: true });
16593
17645
  return newDir;
16594
17646
  }
16595
17647
  function getDbPath2() {
@@ -16597,13 +17649,13 @@ function getDbPath2() {
16597
17649
  return process.env.HASNA_CONVERSATIONS_DB_PATH;
16598
17650
  if (process.env.CONVERSATIONS_DB_PATH)
16599
17651
  return process.env.CONVERSATIONS_DB_PATH;
16600
- return join5(getDataDir2(), "messages.db");
17652
+ return join7(getDataDir2(), "messages.db");
16601
17653
  }
16602
17654
  function getDb() {
16603
17655
  if (db)
16604
17656
  return db;
16605
17657
  const dbPath = getDbPath2();
16606
- mkdirSync3(dirname2(dbPath), { recursive: true });
17658
+ mkdirSync4(dirname2(dbPath), { recursive: true });
16607
17659
  db = new SqliteAdapter(dbPath);
16608
17660
  db.exec("PRAGMA journal_mode = WAL");
16609
17661
  db.exec("PRAGMA busy_timeout = 5000");
@@ -38697,9 +39749,6 @@ class StdioServerTransport {
38697
39749
  }
38698
39750
  }
38699
39751
 
38700
- // src/mcp/index.ts
38701
- init_dist();
38702
-
38703
39752
  // src/lib/presence.ts
38704
39753
  init_db();
38705
39754
  var ONLINE_THRESHOLD_SECONDS = 60;
@@ -38832,25 +39881,25 @@ function renameAgent(oldName, newName) {
38832
39881
  // src/lib/messages.ts
38833
39882
  init_db();
38834
39883
  import { randomUUID } from "crypto";
38835
- import { mkdirSync as mkdirSync5, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
38836
- import { join as join8 } from "path";
39884
+ import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
39885
+ import { join as join10 } from "path";
38837
39886
 
38838
39887
  // src/lib/webhooks.ts
38839
39888
  init_db();
38840
- import { readFileSync as readFileSync2 } from "fs";
38841
- import { join as join7 } from "path";
39889
+ import { readFileSync as readFileSync3 } from "fs";
39890
+ import { join as join9 } from "path";
38842
39891
  var cachedConfig = null;
38843
39892
  var configLoadedAt = 0;
38844
39893
  var CONFIG_CACHE_MS = 1e4;
38845
39894
  function getConfigPath2() {
38846
- return process.env.CONVERSATIONS_CONFIG_PATH || join7(getDataDir2(), "config.json");
39895
+ return process.env.CONVERSATIONS_CONFIG_PATH || join9(getDataDir2(), "config.json");
38847
39896
  }
38848
39897
  function loadConfig() {
38849
39898
  const now = Date.now();
38850
39899
  if (cachedConfig && now - configLoadedAt < CONFIG_CACHE_MS)
38851
39900
  return cachedConfig;
38852
39901
  try {
38853
- const raw = readFileSync2(getConfigPath2(), "utf-8");
39902
+ const raw = readFileSync3(getConfigPath2(), "utf-8");
38854
39903
  cachedConfig = JSON.parse(raw);
38855
39904
  configLoadedAt = now;
38856
39905
  return cachedConfig;
@@ -38938,7 +39987,7 @@ function parseMessage(row) {
38938
39987
  function getAttachmentsDir() {
38939
39988
  if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
38940
39989
  return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
38941
- return join8(getDataDir2(), "attachments");
39990
+ return join10(getDataDir2(), "attachments");
38942
39991
  }
38943
39992
  function guessMimeType(name) {
38944
39993
  const ext = name.split(".").pop()?.toLowerCase();
@@ -38983,11 +40032,11 @@ function sendMessage(opts) {
38983
40032
  const row = stmt.get(sessionId, opts.from, opts.to, opts.space || null, opts.project_id || null, opts.content, normalizedPriority, opts.working_dir || null, opts.repository || null, opts.branch || null, metadata, blocking, replyTo);
38984
40033
  const message = parseMessage(row);
38985
40034
  if (opts.attachments && opts.attachments.length > 0) {
38986
- const attachmentsDir = join8(getAttachmentsDir(), String(message.id));
38987
- mkdirSync5(attachmentsDir, { recursive: true });
40035
+ const attachmentsDir = join10(getAttachmentsDir(), String(message.id));
40036
+ mkdirSync6(attachmentsDir, { recursive: true });
38988
40037
  const attachmentInfos = [];
38989
40038
  for (const att of opts.attachments) {
38990
- const destPath = join8(attachmentsDir, att.name);
40039
+ const destPath = join10(attachmentsDir, att.name);
38991
40040
  copyFileSync3(att.source_path, destPath);
38992
40041
  const stat = statSync2(destPath);
38993
40042
  attachmentInfos.push({
@@ -39548,8 +40597,8 @@ function getSessionActivity(sessionId) {
39548
40597
  }
39549
40598
 
39550
40599
  // src/lib/identity.ts
39551
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync6 } from "fs";
39552
- import { join as join9, dirname as dirname3 } from "path";
40600
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
40601
+ import { join as join11, dirname as dirname3 } from "path";
39553
40602
 
39554
40603
  // src/lib/names.ts
39555
40604
  var AGENT_NAMES = [
@@ -39902,7 +40951,7 @@ var AGENT_NAMES = [
39902
40951
 
39903
40952
  // src/lib/identity.ts
39904
40953
  init_db();
39905
- var AGENT_ID_FILE = join9(getDataDir2(), "agent-id");
40954
+ var AGENT_ID_FILE = join11(getDataDir2(), "agent-id");
39906
40955
  var cachedAutoName = null;
39907
40956
  function isNameTaken(name) {
39908
40957
  try {
@@ -39918,7 +40967,7 @@ function getAutoName() {
39918
40967
  if (cachedAutoName)
39919
40968
  return cachedAutoName;
39920
40969
  try {
39921
- const name2 = readFileSync3(AGENT_ID_FILE, "utf-8").trim();
40970
+ const name2 = readFileSync5(AGENT_ID_FILE, "utf-8").trim();
39922
40971
  if (name2) {
39923
40972
  cachedAutoName = name2;
39924
40973
  return name2;
@@ -39934,8 +40983,8 @@ function getAutoName() {
39934
40983
  }
39935
40984
  cachedAutoName = name;
39936
40985
  try {
39937
- mkdirSync6(dirname3(AGENT_ID_FILE), { recursive: true });
39938
- writeFileSync2(AGENT_ID_FILE, name + `
40986
+ mkdirSync7(dirname3(AGENT_ID_FILE), { recursive: true });
40987
+ writeFileSync3(AGENT_ID_FILE, name + `
39939
40988
  `, "utf-8");
39940
40989
  } catch {}
39941
40990
  return name;
@@ -39952,8 +41001,8 @@ function resolveIdentity(explicit) {
39952
41001
  function updateCachedAutoName(newName) {
39953
41002
  cachedAutoName = newName;
39954
41003
  try {
39955
- mkdirSync6(dirname3(AGENT_ID_FILE), { recursive: true });
39956
- writeFileSync2(AGENT_ID_FILE, newName + `
41004
+ mkdirSync7(dirname3(AGENT_ID_FILE), { recursive: true });
41005
+ writeFileSync3(AGENT_ID_FILE, newName + `
39957
41006
  `, "utf-8");
39958
41007
  } catch {}
39959
41008
  }
@@ -42504,10 +43553,192 @@ function registerAdvancedTools(server, pkgVersion) {
42504
43553
  }
42505
43554
  });
42506
43555
  }
43556
+
43557
+ // src/mcp/tools/cloud.ts
43558
+ var CONFLICT_TABLES = new Set(["messages", "spaces", "projects", "agent_presence"]);
43559
+ async function detectAndLogConflicts(local, cloud, table) {
43560
+ if (!CONFLICT_TABLES.has(table))
43561
+ return 0;
43562
+ try {
43563
+ const { detectConflicts: detectConflicts2, storeConflicts: storeConflicts2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
43564
+ const pk = table === "messages" ? "id" : table === "spaces" ? "name" : table === "space_members" ? "space" : "id";
43565
+ const tsCol = "created_at";
43566
+ const localRows = local.all(`SELECT * FROM "${table}"`);
43567
+ const remoteRows = await cloud.all(`SELECT * FROM "${table}"`);
43568
+ if (localRows.length === 0 || remoteRows.length === 0)
43569
+ return 0;
43570
+ const conflicts = detectConflicts2(localRows, remoteRows, table, pk, tsCol);
43571
+ if (conflicts.length > 0) {
43572
+ storeConflicts2(local, conflicts);
43573
+ }
43574
+ return conflicts.length;
43575
+ } catch {
43576
+ return 0;
43577
+ }
43578
+ }
43579
+ function registerCloudSyncTools(server) {
43580
+ server.tool("conversations_cloud_status", "Show cloud configuration, connection health, and sync status", {}, async () => {
43581
+ try {
43582
+ const {
43583
+ getCloudConfig: getCloudConfig2,
43584
+ getConnectionString: getConnectionString2,
43585
+ PgAdapterAsync: PgAdapterAsync2,
43586
+ listConflicts: listConflicts2,
43587
+ ensureConflictsTable: ensureConflictsTable2,
43588
+ SqliteAdapter: SqliteAdapter2,
43589
+ getDbPath: cloudGetDbPath
43590
+ } = await Promise.resolve().then(() => (init_dist(), exports_dist));
43591
+ const config2 = getCloudConfig2();
43592
+ const lines = [
43593
+ `Mode: ${config2.mode}`,
43594
+ `Service: conversations`,
43595
+ `RDS Host: ${config2.rds.host || "(not configured)"}`
43596
+ ];
43597
+ if (config2.rds.host && config2.rds.username) {
43598
+ try {
43599
+ const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
43600
+ await pg.get("SELECT 1 as ok");
43601
+ lines.push("PostgreSQL: connected");
43602
+ await pg.close();
43603
+ } catch (err) {
43604
+ lines.push(`PostgreSQL: failed \u2014 ${err?.message}`);
43605
+ }
43606
+ }
43607
+ try {
43608
+ const local = new SqliteAdapter2(cloudGetDbPath("conversations"));
43609
+ ensureConflictsTable2(local);
43610
+ const unresolved = listConflicts2(local, { resolved: false });
43611
+ const resolved = listConflicts2(local, { resolved: true });
43612
+ lines.push(`Sync conflicts: ${unresolved.length} unresolved, ${resolved.length} resolved`);
43613
+ local.close();
43614
+ } catch {}
43615
+ return { content: [{ type: "text", text: lines.join(`
43616
+ `) }] };
43617
+ } catch (e) {
43618
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
43619
+ }
43620
+ });
43621
+ server.tool("conversations_cloud_push", "Push local conversations data to cloud PostgreSQL. Detects conflicts and syncs all tables.", {
43622
+ tables: exports_external.string().optional().describe("Comma-separated table names (default: all)")
43623
+ }, async ({ tables: tablesStr }) => {
43624
+ try {
43625
+ const {
43626
+ getCloudConfig: getCloudConfig2,
43627
+ getConnectionString: getConnectionString2,
43628
+ syncPush: syncPush2,
43629
+ listSqliteTables: listSqliteTables2,
43630
+ SqliteAdapter: SqliteAdapter2,
43631
+ PgAdapterAsync: PgAdapterAsync2,
43632
+ getDbPath: cloudGetDbPath
43633
+ } = await Promise.resolve().then(() => (init_dist(), exports_dist));
43634
+ const config2 = getCloudConfig2();
43635
+ if (config2.mode === "local") {
43636
+ return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
43637
+ }
43638
+ const localPath = cloudGetDbPath("conversations");
43639
+ const local = new SqliteAdapter2(localPath);
43640
+ const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
43641
+ const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !t.startsWith("_") && !t.endsWith("_fts") && !t.startsWith("messages_fts"));
43642
+ let totalConflicts = 0;
43643
+ for (const table of tableList) {
43644
+ totalConflicts += await detectAndLogConflicts(local, cloud, table);
43645
+ }
43646
+ const results = await syncPush2(local, cloud, { tables: tableList });
43647
+ local.close();
43648
+ await cloud.close();
43649
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
43650
+ const errors3 = results.flatMap((r) => r.errors);
43651
+ const lines = [`Pushed ${total} rows across ${tableList.length} table(s).`];
43652
+ if (totalConflicts > 0)
43653
+ lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
43654
+ if (errors3.length > 0)
43655
+ lines.push(`Errors: ${errors3.join("; ")}`);
43656
+ return { content: [{ type: "text", text: lines.join(`
43657
+ `) }] };
43658
+ } catch (e) {
43659
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
43660
+ }
43661
+ });
43662
+ server.tool("conversations_cloud_pull", "Pull cloud PostgreSQL data to local. Detects conflicts and merges by primary key with UPSERT.", {
43663
+ tables: exports_external.string().optional().describe("Comma-separated table names (default: all)")
43664
+ }, async ({ tables: tablesStr }) => {
43665
+ try {
43666
+ const {
43667
+ getCloudConfig: getCloudConfig2,
43668
+ getConnectionString: getConnectionString2,
43669
+ syncPull: syncPull2,
43670
+ listPgTables: listPgTables2,
43671
+ SqliteAdapter: SqliteAdapter2,
43672
+ PgAdapterAsync: PgAdapterAsync2,
43673
+ getDbPath: cloudGetDbPath
43674
+ } = await Promise.resolve().then(() => (init_dist(), exports_dist));
43675
+ const config2 = getCloudConfig2();
43676
+ if (config2.mode === "local") {
43677
+ return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
43678
+ }
43679
+ const local = new SqliteAdapter2(cloudGetDbPath("conversations"));
43680
+ const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
43681
+ let tableList;
43682
+ if (tablesStr) {
43683
+ tableList = tablesStr.split(",").map((t) => t.trim());
43684
+ } else {
43685
+ try {
43686
+ tableList = (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
43687
+ } catch {
43688
+ local.close();
43689
+ await cloud.close();
43690
+ return { content: [{ type: "text", text: "Error: failed to list cloud tables." }], isError: true };
43691
+ }
43692
+ }
43693
+ let totalConflicts = 0;
43694
+ for (const table of tableList) {
43695
+ totalConflicts += await detectAndLogConflicts(local, cloud, table);
43696
+ }
43697
+ const results = await syncPull2(cloud, local, { tables: tableList });
43698
+ local.close();
43699
+ await cloud.close();
43700
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
43701
+ const errors3 = results.flatMap((r) => r.errors);
43702
+ const lines = [`Pulled ${total} rows across ${tableList.length} table(s).`];
43703
+ if (totalConflicts > 0)
43704
+ lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
43705
+ if (errors3.length > 0)
43706
+ lines.push(`Errors: ${errors3.join("; ")}`);
43707
+ return { content: [{ type: "text", text: lines.join(`
43708
+ `) }] };
43709
+ } catch (e) {
43710
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
43711
+ }
43712
+ });
43713
+ server.tool("conversations_cloud_feedback", "Send feedback for the conversations service", {
43714
+ message: exports_external.string().describe("Feedback message"),
43715
+ email: exports_external.string().optional().describe("Contact email")
43716
+ }, async ({ message, email: email3 }) => {
43717
+ try {
43718
+ const { sendFeedback: sendFeedback2, createDatabase: createDatabase2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
43719
+ const db2 = createDatabase2({ service: "cloud" });
43720
+ const result = await sendFeedback2({ service: "conversations", message, email: email3 }, db2);
43721
+ db2.close();
43722
+ return {
43723
+ content: [{
43724
+ type: "text",
43725
+ text: result.sent ? `Feedback sent (id: ${result.id})` : `Saved locally (id: ${result.id}): ${result.error}`
43726
+ }]
43727
+ };
43728
+ } catch (e) {
43729
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
43730
+ }
43731
+ });
43732
+ }
43733
+ function formatError2(e) {
43734
+ if (e instanceof Error)
43735
+ return e.message;
43736
+ return String(e);
43737
+ }
42507
43738
  // package.json
42508
43739
  var package_default = {
42509
43740
  name: "@hasna/conversations",
42510
- version: "0.2.24",
43741
+ version: "0.2.25",
42511
43742
  description: "Real-time CLI messaging for AI agents",
42512
43743
  type: "module",
42513
43744
  bin: {
@@ -42611,7 +43842,7 @@ registerAgentTools(server, agentFocus, getAgentFocus);
42611
43842
  registerAdvancedTools(server, package_default.version);
42612
43843
  async function startMcpServer() {
42613
43844
  const transport = new StdioServerTransport;
42614
- registerCloudTools(server, "conversations");
43845
+ registerCloudSyncTools(server);
42615
43846
  await server.connect(transport);
42616
43847
  }
42617
43848
  var isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("mcp.js") || process.argv[1]?.endsWith("mcp.ts");