@hasna/cloud 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9265,7 +9265,10 @@ var CloudConfigSchema = exports_external.object({
9265
9265
  }).default({}),
9266
9266
  mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
9267
9267
  auto_sync_interval_minutes: exports_external.number().default(0),
9268
- feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
9268
+ feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
9269
+ sync: exports_external.object({
9270
+ schedule_minutes: exports_external.number().default(0)
9271
+ }).default({})
9269
9272
  });
9270
9273
  var CONFIG_DIR = join2(homedir2(), ".hasna", "cloud");
9271
9274
  var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
@@ -9441,6 +9444,11 @@ async function syncTransfer(source, target, options, _direction) {
9441
9444
  primaryKey: pkOption
9442
9445
  } = options;
9443
9446
  const results = [];
9447
+ if (!isAsyncAdapter(target)) {
9448
+ try {
9449
+ target.run("PRAGMA foreign_keys = OFF");
9450
+ } catch {}
9451
+ }
9444
9452
  for (let i = 0;i < tables.length; i++) {
9445
9453
  const table = tables[i];
9446
9454
  const result = {
@@ -9558,6 +9566,11 @@ async function syncTransfer(source, target, options, _direction) {
9558
9566
  }
9559
9567
  results.push(result);
9560
9568
  }
9569
+ if (!isAsyncAdapter(target)) {
9570
+ try {
9571
+ target.run("PRAGMA foreign_keys = ON");
9572
+ } catch {}
9573
+ }
9561
9574
  return results;
9562
9575
  }
9563
9576
  async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
@@ -10247,6 +10260,161 @@ function setupAutoSync(serviceName, server, local, remote, tables) {
10247
10260
  function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
10248
10261
  setupAutoSync(serviceName, mcpServer, local, remote, tables);
10249
10262
  }
10263
+ // src/scheduled-sync.ts
10264
+ import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
10265
+ import { join as join4 } from "path";
10266
+ function discoverSyncableServices() {
10267
+ const hasnaDir = getHasnaDir();
10268
+ const services = [];
10269
+ try {
10270
+ const entries = readdirSync2(hasnaDir, { withFileTypes: true });
10271
+ for (const entry of entries) {
10272
+ if (!entry.isDirectory())
10273
+ continue;
10274
+ const dbPath = join4(hasnaDir, entry.name, `${entry.name}.db`);
10275
+ if (existsSync4(dbPath)) {
10276
+ services.push(entry.name);
10277
+ }
10278
+ }
10279
+ } catch {}
10280
+ return services;
10281
+ }
10282
+ async function runScheduledSync() {
10283
+ const config = getCloudConfig();
10284
+ if (config.mode === "local")
10285
+ return [];
10286
+ const services = discoverSyncableServices();
10287
+ const results = [];
10288
+ let remote = null;
10289
+ for (const service of services) {
10290
+ const result = {
10291
+ service,
10292
+ tables_synced: 0,
10293
+ total_rows_synced: 0,
10294
+ errors: []
10295
+ };
10296
+ try {
10297
+ const dbPath = join4(getDataDir(service), `${service}.db`);
10298
+ if (!existsSync4(dbPath)) {
10299
+ continue;
10300
+ }
10301
+ const local = new SqliteAdapter(dbPath);
10302
+ const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
10303
+ if (tables.length === 0) {
10304
+ local.close();
10305
+ continue;
10306
+ }
10307
+ try {
10308
+ const connStr = getConnectionString(service);
10309
+ remote = new PgAdapterAsync(connStr);
10310
+ } catch (err) {
10311
+ result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
10312
+ local.close();
10313
+ results.push(result);
10314
+ continue;
10315
+ }
10316
+ const stats = incrementalSyncPush(local, remote, tables);
10317
+ for (const s of stats) {
10318
+ if (s.errors.length === 0) {
10319
+ result.tables_synced++;
10320
+ }
10321
+ result.total_rows_synced += s.synced_rows;
10322
+ result.errors.push(...s.errors);
10323
+ }
10324
+ local.close();
10325
+ await remote.close();
10326
+ remote = null;
10327
+ } catch (err) {
10328
+ result.errors.push(err?.message ?? String(err));
10329
+ }
10330
+ results.push(result);
10331
+ }
10332
+ if (remote) {
10333
+ try {
10334
+ await remote.close();
10335
+ } catch {}
10336
+ }
10337
+ return results;
10338
+ }
10339
+ // src/sync-schedule.ts
10340
+ import { join as join5, dirname } from "path";
10341
+ var CRON_TITLE = "hasna-cloud-sync";
10342
+ function getWorkerPath() {
10343
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
10344
+ const tsPath = join5(dir, "scheduled-sync.ts");
10345
+ const jsPath = join5(dir, "scheduled-sync.js");
10346
+ try {
10347
+ const { existsSync: existsSync5 } = __require("fs");
10348
+ if (existsSync5(tsPath))
10349
+ return tsPath;
10350
+ } catch {}
10351
+ return jsPath;
10352
+ }
10353
+ function parseInterval(input) {
10354
+ const trimmed = input.trim().toLowerCase();
10355
+ const hourMatch = trimmed.match(/^(\d+)\s*h$/);
10356
+ if (hourMatch) {
10357
+ const hours = parseInt(hourMatch[1], 10);
10358
+ if (hours <= 0) {
10359
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
10360
+ }
10361
+ return hours * 60;
10362
+ }
10363
+ const minMatch = trimmed.match(/^(\d+)\s*m$/);
10364
+ if (minMatch) {
10365
+ const mins = parseInt(minMatch[1], 10);
10366
+ if (mins <= 0) {
10367
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
10368
+ }
10369
+ return mins;
10370
+ }
10371
+ const plain = parseInt(trimmed, 10);
10372
+ if (!isNaN(plain) && plain > 0) {
10373
+ return plain;
10374
+ }
10375
+ throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
10376
+ }
10377
+ function minutesToCron(minutes) {
10378
+ if (minutes <= 0) {
10379
+ throw new Error("Interval must be greater than 0 minutes.");
10380
+ }
10381
+ if (minutes < 60) {
10382
+ return `*/${minutes} * * * *`;
10383
+ }
10384
+ const hours = Math.floor(minutes / 60);
10385
+ const remainderMins = minutes % 60;
10386
+ if (remainderMins === 0 && hours <= 24) {
10387
+ return `0 */${hours} * * *`;
10388
+ }
10389
+ return `*/${minutes} * * * *`;
10390
+ }
10391
+ async function registerSyncSchedule(intervalMinutes) {
10392
+ if (intervalMinutes <= 0) {
10393
+ throw new Error("Interval must be a positive number of minutes.");
10394
+ }
10395
+ const cronExpr = minutesToCron(intervalMinutes);
10396
+ const workerPath = getWorkerPath();
10397
+ await Bun.cron(workerPath, cronExpr, CRON_TITLE);
10398
+ const config = getCloudConfig();
10399
+ config.sync.schedule_minutes = intervalMinutes;
10400
+ saveCloudConfig(config);
10401
+ }
10402
+ async function removeSyncSchedule() {
10403
+ await Bun.cron.remove(CRON_TITLE);
10404
+ const config = getCloudConfig();
10405
+ config.sync.schedule_minutes = 0;
10406
+ saveCloudConfig(config);
10407
+ }
10408
+ function getSyncScheduleStatus() {
10409
+ const config = getCloudConfig();
10410
+ const minutes = config.sync.schedule_minutes;
10411
+ const registered = minutes > 0;
10412
+ return {
10413
+ registered,
10414
+ schedule_minutes: minutes,
10415
+ cron_expression: registered ? minutesToCron(minutes) : null
10416
+ };
10417
+ }
10250
10418
  // src/mcp-helpers.ts
10251
10419
  function registerCloudTools(server, serviceName) {
10252
10420
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
@@ -10439,13 +10607,18 @@ export {
10439
10607
  sendFeedback,
10440
10608
  saveFeedback,
10441
10609
  saveCloudConfig,
10610
+ runScheduledSync,
10442
10611
  resolveConflicts,
10443
10612
  resolveConflict,
10444
10613
  resetSyncMeta,
10445
10614
  resetAllSyncMeta,
10615
+ removeSyncSchedule,
10616
+ registerSyncSchedule,
10446
10617
  registerCloudTools,
10447
10618
  registerCloudCommands,
10448
10619
  purgeResolvedConflicts,
10620
+ parseInterval,
10621
+ minutesToCron,
10449
10622
  migrateDotfile,
10450
10623
  listSqliteTables,
10451
10624
  listPgTables,
@@ -10455,6 +10628,7 @@ export {
10455
10628
  incrementalSyncPull,
10456
10629
  hasLegacyDotfile,
10457
10630
  getWinningData,
10631
+ getSyncScheduleStatus,
10458
10632
  getSyncMetaForTable,
10459
10633
  getSyncMetaAll,
10460
10634
  getHasnaDir,
@@ -10470,6 +10644,7 @@ export {
10470
10644
  ensureFeedbackTable,
10471
10645
  ensureConflictsTable,
10472
10646
  enableAutoSync,
10647
+ discoverSyncableServices,
10473
10648
  detectConflicts,
10474
10649
  createDatabase,
10475
10650
  SyncProgressTracker,
package/dist/mcp/index.js CHANGED
@@ -24622,7 +24622,10 @@ var CloudConfigSchema = exports_external.object({
24622
24622
  }).default({}),
24623
24623
  mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
24624
24624
  auto_sync_interval_minutes: exports_external.number().default(0),
24625
- feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
24625
+ feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
24626
+ sync: exports_external.object({
24627
+ schedule_minutes: exports_external.number().default(0)
24628
+ }).default({})
24626
24629
  });
24627
24630
  var CONFIG_DIR = join2(homedir2(), ".hasna", "cloud");
24628
24631
  var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
@@ -24788,6 +24791,11 @@ async function syncTransfer(source, target, options, _direction) {
24788
24791
  primaryKey: pkOption
24789
24792
  } = options;
24790
24793
  const results = [];
24794
+ if (!isAsyncAdapter(target)) {
24795
+ try {
24796
+ target.run("PRAGMA foreign_keys = OFF");
24797
+ } catch {}
24798
+ }
24791
24799
  for (let i = 0;i < tables.length; i++) {
24792
24800
  const table = tables[i];
24793
24801
  const result = {
@@ -24905,6 +24913,11 @@ async function syncTransfer(source, target, options, _direction) {
24905
24913
  }
24906
24914
  results.push(result);
24907
24915
  }
24916
+ if (!isAsyncAdapter(target)) {
24917
+ try {
24918
+ target.run("PRAGMA foreign_keys = ON");
24919
+ } catch {}
24920
+ }
24908
24921
  return results;
24909
24922
  }
24910
24923
  async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
@@ -24988,7 +25001,10 @@ var CloudConfigSchema2 = exports_external.object({
24988
25001
  }).default({}),
24989
25002
  mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
24990
25003
  auto_sync_interval_minutes: exports_external.number().default(0),
24991
- feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
25004
+ feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
25005
+ sync: exports_external.object({
25006
+ schedule_minutes: exports_external.number().default(0)
25007
+ }).default({})
24992
25008
  });
24993
25009
  var CONFIG_DIR2 = join3(homedir3(), ".hasna", "cloud");
24994
25010
  var CONFIG_PATH2 = join3(CONFIG_DIR2, "config.json");
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Discover services under `~/.hasna/` that have a `<service>.db` SQLite file.
3
+ * Returns an array of service names.
4
+ */
5
+ export declare function discoverSyncableServices(): string[];
6
+ export interface ScheduledSyncResult {
7
+ service: string;
8
+ tables_synced: number;
9
+ total_rows_synced: number;
10
+ errors: string[];
11
+ }
12
+ /**
13
+ * Run a scheduled sync push for all discovered services.
14
+ *
15
+ * - Skips if mode is `local`.
16
+ * - Opens each service's SQLite DB, discovers tables, and pushes to PG.
17
+ * - Returns per-service results.
18
+ */
19
+ export declare function runScheduledSync(): Promise<ScheduledSyncResult[]>;
20
+ declare const _default: {
21
+ scheduled(): Promise<void>;
22
+ };
23
+ export default _default;
24
+ //# sourceMappingURL=scheduled-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduled-sync.d.ts","sourceRoot":"","sources":["../src/scheduled-sync.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAkBnD;AAMD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA2EvE;;;;AAUD,wBAIE"}