@hasna/cloud 0.1.6 → 0.1.8

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");
@@ -10247,6 +10250,161 @@ function setupAutoSync(serviceName, server, local, remote, tables) {
10247
10250
  function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
10248
10251
  setupAutoSync(serviceName, mcpServer, local, remote, tables);
10249
10252
  }
10253
+ // src/scheduled-sync.ts
10254
+ import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
10255
+ import { join as join4 } from "path";
10256
+ function discoverSyncableServices() {
10257
+ const hasnaDir = getHasnaDir();
10258
+ const services = [];
10259
+ try {
10260
+ const entries = readdirSync2(hasnaDir, { withFileTypes: true });
10261
+ for (const entry of entries) {
10262
+ if (!entry.isDirectory())
10263
+ continue;
10264
+ const dbPath = join4(hasnaDir, entry.name, `${entry.name}.db`);
10265
+ if (existsSync4(dbPath)) {
10266
+ services.push(entry.name);
10267
+ }
10268
+ }
10269
+ } catch {}
10270
+ return services;
10271
+ }
10272
+ async function runScheduledSync() {
10273
+ const config = getCloudConfig();
10274
+ if (config.mode === "local")
10275
+ return [];
10276
+ const services = discoverSyncableServices();
10277
+ const results = [];
10278
+ let remote = null;
10279
+ for (const service of services) {
10280
+ const result = {
10281
+ service,
10282
+ tables_synced: 0,
10283
+ total_rows_synced: 0,
10284
+ errors: []
10285
+ };
10286
+ try {
10287
+ const dbPath = join4(getDataDir(service), `${service}.db`);
10288
+ if (!existsSync4(dbPath)) {
10289
+ continue;
10290
+ }
10291
+ const local = new SqliteAdapter(dbPath);
10292
+ const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
10293
+ if (tables.length === 0) {
10294
+ local.close();
10295
+ continue;
10296
+ }
10297
+ try {
10298
+ const connStr = getConnectionString(service);
10299
+ remote = new PgAdapterAsync(connStr);
10300
+ } catch (err) {
10301
+ result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
10302
+ local.close();
10303
+ results.push(result);
10304
+ continue;
10305
+ }
10306
+ const stats = incrementalSyncPush(local, remote, tables);
10307
+ for (const s of stats) {
10308
+ if (s.errors.length === 0) {
10309
+ result.tables_synced++;
10310
+ }
10311
+ result.total_rows_synced += s.synced_rows;
10312
+ result.errors.push(...s.errors);
10313
+ }
10314
+ local.close();
10315
+ await remote.close();
10316
+ remote = null;
10317
+ } catch (err) {
10318
+ result.errors.push(err?.message ?? String(err));
10319
+ }
10320
+ results.push(result);
10321
+ }
10322
+ if (remote) {
10323
+ try {
10324
+ await remote.close();
10325
+ } catch {}
10326
+ }
10327
+ return results;
10328
+ }
10329
+ // src/sync-schedule.ts
10330
+ import { join as join5, dirname } from "path";
10331
+ var CRON_TITLE = "hasna-cloud-sync";
10332
+ function getWorkerPath() {
10333
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
10334
+ const tsPath = join5(dir, "scheduled-sync.ts");
10335
+ const jsPath = join5(dir, "scheduled-sync.js");
10336
+ try {
10337
+ const { existsSync: existsSync5 } = __require("fs");
10338
+ if (existsSync5(tsPath))
10339
+ return tsPath;
10340
+ } catch {}
10341
+ return jsPath;
10342
+ }
10343
+ function parseInterval(input) {
10344
+ const trimmed = input.trim().toLowerCase();
10345
+ const hourMatch = trimmed.match(/^(\d+)\s*h$/);
10346
+ if (hourMatch) {
10347
+ const hours = parseInt(hourMatch[1], 10);
10348
+ if (hours <= 0) {
10349
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
10350
+ }
10351
+ return hours * 60;
10352
+ }
10353
+ const minMatch = trimmed.match(/^(\d+)\s*m$/);
10354
+ if (minMatch) {
10355
+ const mins = parseInt(minMatch[1], 10);
10356
+ if (mins <= 0) {
10357
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
10358
+ }
10359
+ return mins;
10360
+ }
10361
+ const plain = parseInt(trimmed, 10);
10362
+ if (!isNaN(plain) && plain > 0) {
10363
+ return plain;
10364
+ }
10365
+ throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
10366
+ }
10367
+ function minutesToCron(minutes) {
10368
+ if (minutes <= 0) {
10369
+ throw new Error("Interval must be greater than 0 minutes.");
10370
+ }
10371
+ if (minutes < 60) {
10372
+ return `*/${minutes} * * * *`;
10373
+ }
10374
+ const hours = Math.floor(minutes / 60);
10375
+ const remainderMins = minutes % 60;
10376
+ if (remainderMins === 0 && hours <= 24) {
10377
+ return `0 */${hours} * * *`;
10378
+ }
10379
+ return `*/${minutes} * * * *`;
10380
+ }
10381
+ async function registerSyncSchedule(intervalMinutes) {
10382
+ if (intervalMinutes <= 0) {
10383
+ throw new Error("Interval must be a positive number of minutes.");
10384
+ }
10385
+ const cronExpr = minutesToCron(intervalMinutes);
10386
+ const workerPath = getWorkerPath();
10387
+ await Bun.cron(workerPath, cronExpr, CRON_TITLE);
10388
+ const config = getCloudConfig();
10389
+ config.sync.schedule_minutes = intervalMinutes;
10390
+ saveCloudConfig(config);
10391
+ }
10392
+ async function removeSyncSchedule() {
10393
+ await Bun.cron.remove(CRON_TITLE);
10394
+ const config = getCloudConfig();
10395
+ config.sync.schedule_minutes = 0;
10396
+ saveCloudConfig(config);
10397
+ }
10398
+ function getSyncScheduleStatus() {
10399
+ const config = getCloudConfig();
10400
+ const minutes = config.sync.schedule_minutes;
10401
+ const registered = minutes > 0;
10402
+ return {
10403
+ registered,
10404
+ schedule_minutes: minutes,
10405
+ cron_expression: registered ? minutesToCron(minutes) : null
10406
+ };
10407
+ }
10250
10408
  // src/mcp-helpers.ts
10251
10409
  function registerCloudTools(server, serviceName) {
10252
10410
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
@@ -10439,13 +10597,18 @@ export {
10439
10597
  sendFeedback,
10440
10598
  saveFeedback,
10441
10599
  saveCloudConfig,
10600
+ runScheduledSync,
10442
10601
  resolveConflicts,
10443
10602
  resolveConflict,
10444
10603
  resetSyncMeta,
10445
10604
  resetAllSyncMeta,
10605
+ removeSyncSchedule,
10606
+ registerSyncSchedule,
10446
10607
  registerCloudTools,
10447
10608
  registerCloudCommands,
10448
10609
  purgeResolvedConflicts,
10610
+ parseInterval,
10611
+ minutesToCron,
10449
10612
  migrateDotfile,
10450
10613
  listSqliteTables,
10451
10614
  listPgTables,
@@ -10455,6 +10618,7 @@ export {
10455
10618
  incrementalSyncPull,
10456
10619
  hasLegacyDotfile,
10457
10620
  getWinningData,
10621
+ getSyncScheduleStatus,
10458
10622
  getSyncMetaForTable,
10459
10623
  getSyncMetaAll,
10460
10624
  getHasnaDir,
@@ -10470,6 +10634,7 @@ export {
10470
10634
  ensureFeedbackTable,
10471
10635
  ensureConflictsTable,
10472
10636
  enableAutoSync,
10637
+ discoverSyncableServices,
10473
10638
  detectConflicts,
10474
10639
  createDatabase,
10475
10640
  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");
@@ -24988,7 +24991,10 @@ var CloudConfigSchema2 = exports_external.object({
24988
24991
  }).default({}),
24989
24992
  mode: exports_external.enum(["local", "cloud", "hybrid"]).default("local"),
24990
24993
  auto_sync_interval_minutes: exports_external.number().default(0),
24991
- feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback")
24994
+ feedback_endpoint: exports_external.string().default("https://feedback.hasna.com/api/v1/feedback"),
24995
+ sync: exports_external.object({
24996
+ schedule_minutes: exports_external.number().default(0)
24997
+ }).default({})
24992
24998
  });
24993
24999
  var CONFIG_DIR2 = join3(homedir3(), ".hasna", "cloud");
24994
25000
  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"}