@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/index.js CHANGED
@@ -2257,6 +2257,75 @@ var init_names = __esm(() => {
2257
2257
  });
2258
2258
 
2259
2259
  // node_modules/@hasna/cloud/dist/index.js
2260
+ var exports_dist = {};
2261
+ __export(exports_dist, {
2262
+ translateSql: () => translateSql,
2263
+ translateParams: () => translateParams,
2264
+ translateDdl: () => translateDdl,
2265
+ syncPush: () => syncPush,
2266
+ syncPull: () => syncPull,
2267
+ storeConflicts: () => storeConflicts,
2268
+ setupAutoSync: () => setupAutoSync,
2269
+ sendFeedback: () => sendFeedback,
2270
+ saveFeedback: () => saveFeedback,
2271
+ saveCloudConfig: () => saveCloudConfig,
2272
+ runScheduledSync: () => runScheduledSync,
2273
+ resolveConflicts: () => resolveConflicts,
2274
+ resolveConflict: () => resolveConflict,
2275
+ resetSyncMeta: () => resetSyncMeta,
2276
+ resetAllSyncMeta: () => resetAllSyncMeta,
2277
+ removeSyncSchedule: () => removeSyncSchedule,
2278
+ registerSyncSchedule: () => registerSyncSchedule,
2279
+ registerCloudTools: () => registerCloudTools,
2280
+ registerCloudCommands: () => registerCloudCommands,
2281
+ purgeResolvedConflicts: () => purgeResolvedConflicts,
2282
+ parseInterval: () => parseInterval,
2283
+ minutesToCron: () => minutesToCron,
2284
+ migrateService: () => migrateService,
2285
+ migrateDotfile: () => migrateDotfile,
2286
+ migrateAllServices: () => migrateAllServices,
2287
+ listSqliteTables: () => listSqliteTables,
2288
+ listPgTables: () => listPgTables,
2289
+ listFeedback: () => listFeedback,
2290
+ listConflicts: () => listConflicts,
2291
+ isSyncExcludedTable: () => isSyncExcludedTable,
2292
+ incrementalSyncPush: () => incrementalSyncPush,
2293
+ incrementalSyncPull: () => incrementalSyncPull,
2294
+ hasLegacyDotfile: () => hasLegacyDotfile,
2295
+ getWinningData: () => getWinningData,
2296
+ getSyncScheduleStatus: () => getSyncScheduleStatus,
2297
+ getSyncMetaForTable: () => getSyncMetaForTable,
2298
+ getSyncMetaAll: () => getSyncMetaAll,
2299
+ getServiceDbPath: () => getServiceDbPath,
2300
+ getHasnaDir: () => getHasnaDir,
2301
+ getDbPath: () => getDbPath,
2302
+ getDataDir: () => getDataDir,
2303
+ getConnectionString: () => getConnectionString,
2304
+ getConflict: () => getConflict,
2305
+ getConfigPath: () => getConfigPath,
2306
+ getConfigDir: () => getConfigDir,
2307
+ getCloudConfig: () => getCloudConfig,
2308
+ getAutoSyncConfig: () => getAutoSyncConfig,
2309
+ ensureSyncMetaTable: () => ensureSyncMetaTable,
2310
+ ensurePgDatabase: () => ensurePgDatabase,
2311
+ ensureFeedbackTable: () => ensureFeedbackTable,
2312
+ ensureConflictsTable: () => ensureConflictsTable,
2313
+ ensureAllPgDatabases: () => ensureAllPgDatabases,
2314
+ enableAutoSync: () => enableAutoSync,
2315
+ discoverSyncableServicesV2: () => discoverSyncableServices,
2316
+ discoverSyncableServices: () => discoverSyncableServices2,
2317
+ discoverServices: () => discoverServices,
2318
+ detectConflicts: () => detectConflicts,
2319
+ createDatabase: () => createDatabase,
2320
+ applyPgMigrations: () => applyPgMigrations,
2321
+ SyncProgressTracker: () => SyncProgressTracker,
2322
+ SqliteAdapter: () => SqliteAdapter,
2323
+ SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
2324
+ PgAdapterAsync: () => PgAdapterAsync,
2325
+ PgAdapter: () => PgAdapter,
2326
+ KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES,
2327
+ CloudConfigSchema: () => CloudConfigSchema
2328
+ });
2260
2329
  import { createRequire } from "module";
2261
2330
  import { Database } from "bun:sqlite";
2262
2331
  import {
@@ -2274,9 +2343,13 @@ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
2274
2343
  import { join as join3 } from "path";
2275
2344
  import { homedir as homedir3 } from "os";
2276
2345
  import { hostname } from "os";
2346
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
2277
2347
  import { homedir as homedir4 } from "os";
2278
2348
  import { join as join4 } from "path";
2349
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
2350
+ import { join as join5 } from "path";
2279
2351
  import { join as join6, dirname } from "path";
2352
+ import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
2280
2353
  import { homedir as homedir5, platform } from "os";
2281
2354
  function __accessProp2(key) {
2282
2355
  return this[key];
@@ -2331,6 +2404,17 @@ function sqliteToPostgres(sql) {
2331
2404
  }
2332
2405
  return out;
2333
2406
  }
2407
+ function translateDdl(ddl, dialect) {
2408
+ if (dialect === "sqlite")
2409
+ return ddl;
2410
+ let out = ddl;
2411
+ out = out.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/gi, "BIGSERIAL PRIMARY KEY");
2412
+ out = out.replace(/\bAUTOINCREMENT\b/gi, "");
2413
+ out = out.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
2414
+ out = out.replace(/\bBLOB\b/gi, "BYTEA");
2415
+ out = sqliteToPostgres(out);
2416
+ return out;
2417
+ }
2334
2418
 
2335
2419
  class SqliteAdapter {
2336
2420
  db;
@@ -3156,6 +3240,39 @@ function getDbPath(serviceName) {
3156
3240
  const dir = getDataDir(serviceName);
3157
3241
  return join(dir, `${serviceName}.db`);
3158
3242
  }
3243
+ function migrateDotfile(serviceName) {
3244
+ const legacyDir = join(homedir(), `.${serviceName}`);
3245
+ const newDir = join(HASNA_DIR, serviceName);
3246
+ if (!existsSync(legacyDir))
3247
+ return [];
3248
+ if (existsSync(newDir))
3249
+ return [];
3250
+ mkdirSync(newDir, { recursive: true });
3251
+ const migrated = [];
3252
+ copyDirRecursive(legacyDir, newDir, legacyDir, migrated);
3253
+ return migrated;
3254
+ }
3255
+ function copyDirRecursive(src, dest, root, migrated) {
3256
+ const entries = readdirSync(src, { withFileTypes: true });
3257
+ for (const entry of entries) {
3258
+ const srcPath = join(src, entry.name);
3259
+ const destPath = join(dest, entry.name);
3260
+ if (entry.isDirectory()) {
3261
+ mkdirSync(destPath, { recursive: true });
3262
+ copyDirRecursive(srcPath, destPath, root, migrated);
3263
+ } else {
3264
+ copyFileSync(srcPath, destPath);
3265
+ migrated.push(relative(root, srcPath));
3266
+ }
3267
+ }
3268
+ }
3269
+ function hasLegacyDotfile(serviceName) {
3270
+ return existsSync(join(homedir(), `.${serviceName}`));
3271
+ }
3272
+ function getHasnaDir() {
3273
+ mkdirSync(HASNA_DIR, { recursive: true });
3274
+ return HASNA_DIR;
3275
+ }
3159
3276
  function getConfigDir() {
3160
3277
  return CONFIG_DIR;
3161
3278
  }
@@ -3728,6 +3845,10 @@ async function sendFeedback(feedback, db) {
3728
3845
  return { sent: false, id, error: errorMsg };
3729
3846
  }
3730
3847
  }
3848
+ function listFeedback(db) {
3849
+ ensureFeedbackTable(db);
3850
+ return db.all(`SELECT id, service, version, message, email, machine_id, created_at FROM feedback ORDER BY created_at DESC`);
3851
+ }
3731
3852
 
3732
3853
  class SyncProgressTracker {
3733
3854
  db;
@@ -3848,6 +3969,847 @@ class SyncProgressTracker {
3848
3969
  }
3849
3970
  }
3850
3971
  }
3972
+ function detectConflicts(local, remote, table, primaryKey = "id", conflictColumn = "updated_at") {
3973
+ const conflicts = [];
3974
+ const remoteMap = new Map;
3975
+ for (const row of remote) {
3976
+ const key = String(row[primaryKey]);
3977
+ remoteMap.set(key, row);
3978
+ }
3979
+ for (const localRow of local) {
3980
+ const key = String(localRow[primaryKey]);
3981
+ const remoteRow = remoteMap.get(key);
3982
+ if (!remoteRow)
3983
+ continue;
3984
+ const localTs = localRow[conflictColumn];
3985
+ const remoteTs = remoteRow[conflictColumn];
3986
+ if (localTs !== remoteTs) {
3987
+ conflicts.push({
3988
+ table,
3989
+ row_id: key,
3990
+ local_updated_at: String(localTs ?? ""),
3991
+ remote_updated_at: String(remoteTs ?? ""),
3992
+ local_data: { ...localRow },
3993
+ remote_data: { ...remoteRow },
3994
+ resolved: false
3995
+ });
3996
+ }
3997
+ }
3998
+ return conflicts;
3999
+ }
4000
+ function resolveConflicts(conflicts, strategy = "newest-wins") {
4001
+ return conflicts.map((conflict) => {
4002
+ const resolved = { ...conflict, resolved: true, resolution: strategy };
4003
+ switch (strategy) {
4004
+ case "local-wins":
4005
+ break;
4006
+ case "remote-wins":
4007
+ break;
4008
+ case "newest-wins": {
4009
+ const localTime = new Date(conflict.local_updated_at).getTime();
4010
+ const remoteTime = new Date(conflict.remote_updated_at).getTime();
4011
+ if (remoteTime > localTime) {
4012
+ resolved.resolution = "newest-wins";
4013
+ } else {
4014
+ resolved.resolution = "newest-wins";
4015
+ }
4016
+ break;
4017
+ }
4018
+ }
4019
+ return resolved;
4020
+ });
4021
+ }
4022
+ function getWinningData(conflict) {
4023
+ if (!conflict.resolved || !conflict.resolution) {
4024
+ throw new Error(`Conflict for row ${conflict.row_id} is not resolved`);
4025
+ }
4026
+ switch (conflict.resolution) {
4027
+ case "local-wins":
4028
+ return conflict.local_data;
4029
+ case "remote-wins":
4030
+ return conflict.remote_data;
4031
+ case "newest-wins": {
4032
+ const localTime = new Date(conflict.local_updated_at).getTime();
4033
+ const remoteTime = new Date(conflict.remote_updated_at).getTime();
4034
+ return remoteTime >= localTime ? conflict.remote_data : conflict.local_data;
4035
+ }
4036
+ case "manual":
4037
+ return conflict.local_data;
4038
+ default:
4039
+ return conflict.local_data;
4040
+ }
4041
+ }
4042
+ function ensureConflictsTable(db) {
4043
+ db.exec(`
4044
+ CREATE TABLE IF NOT EXISTS _sync_conflicts (
4045
+ id TEXT PRIMARY KEY,
4046
+ table_name TEXT,
4047
+ row_id TEXT,
4048
+ local_data TEXT,
4049
+ remote_data TEXT,
4050
+ local_updated_at TEXT,
4051
+ remote_updated_at TEXT,
4052
+ resolution TEXT,
4053
+ resolved_at TEXT,
4054
+ created_at TEXT DEFAULT (datetime('now'))
4055
+ )
4056
+ `);
4057
+ }
4058
+ function storeConflicts(db, conflicts) {
4059
+ ensureConflictsTable(db);
4060
+ for (const conflict of conflicts) {
4061
+ const id = `${conflict.table}:${conflict.row_id}:${Date.now()}`;
4062
+ db.run(`INSERT INTO _sync_conflicts (id, table_name, row_id, local_data, remote_data, local_updated_at, remote_updated_at, resolution, resolved_at)
4063
+ 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);
4064
+ }
4065
+ }
4066
+ function listConflicts(db, opts) {
4067
+ ensureConflictsTable(db);
4068
+ let sql = `SELECT * FROM _sync_conflicts WHERE 1=1`;
4069
+ const params = [];
4070
+ if (opts?.resolved !== undefined) {
4071
+ if (opts.resolved) {
4072
+ sql += ` AND resolution IS NOT NULL AND resolved_at IS NOT NULL`;
4073
+ } else {
4074
+ sql += ` AND (resolution IS NULL OR resolved_at IS NULL)`;
4075
+ }
4076
+ }
4077
+ if (opts?.table) {
4078
+ sql += ` AND table_name = ?`;
4079
+ params.push(opts.table);
4080
+ }
4081
+ sql += ` ORDER BY created_at DESC`;
4082
+ return db.all(sql, ...params);
4083
+ }
4084
+ function resolveConflict(db, conflictId, strategy) {
4085
+ ensureConflictsTable(db);
4086
+ const row = db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
4087
+ if (!row)
4088
+ return null;
4089
+ db.run(`UPDATE _sync_conflicts SET resolution = ?, resolved_at = datetime('now') WHERE id = ?`, strategy, conflictId);
4090
+ return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
4091
+ }
4092
+ function getConflict(db, conflictId) {
4093
+ ensureConflictsTable(db);
4094
+ return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
4095
+ }
4096
+ function purgeResolvedConflicts(db) {
4097
+ ensureConflictsTable(db);
4098
+ const result = db.run(`DELETE FROM _sync_conflicts WHERE resolution IS NOT NULL AND resolved_at IS NOT NULL`);
4099
+ return result.changes;
4100
+ }
4101
+ function ensureSyncMetaTable(db) {
4102
+ db.exec(SYNC_META_TABLE_SQL);
4103
+ }
4104
+ function getSyncMeta(db, table) {
4105
+ ensureSyncMetaTable(db);
4106
+ return db.get(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?`, table) ?? null;
4107
+ }
4108
+ function upsertSyncMeta(db, meta) {
4109
+ ensureSyncMetaTable(db);
4110
+ const existing = db.get(`SELECT table_name FROM _sync_meta WHERE table_name = ?`, meta.table_name);
4111
+ if (existing) {
4112
+ db.run(`UPDATE _sync_meta SET last_synced_at = ?, last_synced_row_count = ?, direction = ? WHERE table_name = ?`, meta.last_synced_at, meta.last_synced_row_count, meta.direction, meta.table_name);
4113
+ } else {
4114
+ db.run(`INSERT INTO _sync_meta (table_name, last_synced_at, last_synced_row_count, direction) VALUES (?, ?, ?, ?)`, meta.table_name, meta.last_synced_at, meta.last_synced_row_count, meta.direction);
4115
+ }
4116
+ }
4117
+ function transferRows(source, target, table, rows, options) {
4118
+ const { primaryKey = "id", conflictColumn = "updated_at" } = options;
4119
+ let written = 0;
4120
+ let skipped = 0;
4121
+ const errors2 = [];
4122
+ if (rows.length === 0)
4123
+ return { written, skipped, errors: errors2 };
4124
+ const columns = Object.keys(rows[0]);
4125
+ const hasConflictCol = columns.includes(conflictColumn);
4126
+ const hasPrimaryKey = columns.includes(primaryKey);
4127
+ if (!hasPrimaryKey) {
4128
+ errors2.push(`Table "${table}" has no "${primaryKey}" column -- skipping`);
4129
+ return { written, skipped, errors: errors2 };
4130
+ }
4131
+ for (const row of rows) {
4132
+ try {
4133
+ const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
4134
+ if (existing) {
4135
+ if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
4136
+ const existingTime = new Date(existing[conflictColumn]).getTime();
4137
+ const incomingTime = new Date(row[conflictColumn]).getTime();
4138
+ if (existingTime >= incomingTime) {
4139
+ skipped++;
4140
+ continue;
4141
+ }
4142
+ }
4143
+ const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
4144
+ const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
4145
+ values.push(row[primaryKey]);
4146
+ target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
4147
+ } else {
4148
+ const placeholders = columns.map(() => "?").join(", ");
4149
+ const colList = columns.map((c) => `"${c}"`).join(", ");
4150
+ const values = columns.map((c) => row[c]);
4151
+ target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
4152
+ }
4153
+ written++;
4154
+ } catch (err) {
4155
+ errors2.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
4156
+ }
4157
+ }
4158
+ return { written, skipped, errors: errors2 };
4159
+ }
4160
+ function incrementalSyncPush(local, remote, tables, options = {}) {
4161
+ const { conflictColumn = "updated_at", batchSize = 500 } = options;
4162
+ const results = [];
4163
+ ensureSyncMetaTable(local);
4164
+ for (const table of tables) {
4165
+ const stat = {
4166
+ table,
4167
+ total_rows: 0,
4168
+ synced_rows: 0,
4169
+ skipped_rows: 0,
4170
+ errors: [],
4171
+ first_sync: false
4172
+ };
4173
+ try {
4174
+ const countResult = local.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
4175
+ stat.total_rows = countResult?.cnt ?? 0;
4176
+ const meta = getSyncMeta(local, table);
4177
+ let rows;
4178
+ if (meta?.last_synced_at) {
4179
+ try {
4180
+ rows = local.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
4181
+ } catch {
4182
+ rows = local.all(`SELECT * FROM "${table}"`);
4183
+ stat.first_sync = true;
4184
+ }
4185
+ } else {
4186
+ rows = local.all(`SELECT * FROM "${table}"`);
4187
+ stat.first_sync = true;
4188
+ }
4189
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
4190
+ const batch = rows.slice(offset, offset + batchSize);
4191
+ const result = transferRows(local, remote, table, batch, options);
4192
+ stat.synced_rows += result.written;
4193
+ stat.skipped_rows += result.skipped;
4194
+ stat.errors.push(...result.errors);
4195
+ }
4196
+ if (rows.length === 0) {
4197
+ stat.skipped_rows = stat.total_rows;
4198
+ }
4199
+ const now = new Date().toISOString();
4200
+ upsertSyncMeta(local, {
4201
+ table_name: table,
4202
+ last_synced_at: now,
4203
+ last_synced_row_count: stat.synced_rows,
4204
+ direction: "push"
4205
+ });
4206
+ } catch (err) {
4207
+ stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
4208
+ }
4209
+ results.push(stat);
4210
+ }
4211
+ return results;
4212
+ }
4213
+ function incrementalSyncPull(remote, local, tables, options = {}) {
4214
+ const { conflictColumn = "updated_at", batchSize = 500 } = options;
4215
+ const results = [];
4216
+ ensureSyncMetaTable(local);
4217
+ for (const table of tables) {
4218
+ const stat = {
4219
+ table,
4220
+ total_rows: 0,
4221
+ synced_rows: 0,
4222
+ skipped_rows: 0,
4223
+ errors: [],
4224
+ first_sync: false
4225
+ };
4226
+ try {
4227
+ const countResult = remote.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
4228
+ stat.total_rows = countResult?.cnt ?? 0;
4229
+ const meta = getSyncMeta(local, table);
4230
+ let rows;
4231
+ if (meta?.last_synced_at) {
4232
+ try {
4233
+ rows = remote.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
4234
+ } catch {
4235
+ rows = remote.all(`SELECT * FROM "${table}"`);
4236
+ stat.first_sync = true;
4237
+ }
4238
+ } else {
4239
+ rows = remote.all(`SELECT * FROM "${table}"`);
4240
+ stat.first_sync = true;
4241
+ }
4242
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
4243
+ const batch = rows.slice(offset, offset + batchSize);
4244
+ const result = transferRows(remote, local, table, batch, options);
4245
+ stat.synced_rows += result.written;
4246
+ stat.skipped_rows += result.skipped;
4247
+ stat.errors.push(...result.errors);
4248
+ }
4249
+ if (rows.length === 0) {
4250
+ stat.skipped_rows = stat.total_rows;
4251
+ }
4252
+ const now = new Date().toISOString();
4253
+ upsertSyncMeta(local, {
4254
+ table_name: table,
4255
+ last_synced_at: now,
4256
+ last_synced_row_count: stat.synced_rows,
4257
+ direction: "pull"
4258
+ });
4259
+ } catch (err) {
4260
+ stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
4261
+ }
4262
+ results.push(stat);
4263
+ }
4264
+ return results;
4265
+ }
4266
+ function getSyncMetaAll(db) {
4267
+ ensureSyncMetaTable(db);
4268
+ return db.all(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta ORDER BY table_name`);
4269
+ }
4270
+ function getSyncMetaForTable(db, table) {
4271
+ return getSyncMeta(db, table);
4272
+ }
4273
+ function resetSyncMeta(db, table) {
4274
+ ensureSyncMetaTable(db);
4275
+ db.run(`DELETE FROM _sync_meta WHERE table_name = ?`, table);
4276
+ }
4277
+ function resetAllSyncMeta(db) {
4278
+ ensureSyncMetaTable(db);
4279
+ db.run(`DELETE FROM _sync_meta`);
4280
+ }
4281
+ function getAutoSyncConfig() {
4282
+ try {
4283
+ if (!existsSync4(AUTO_SYNC_CONFIG_PATH)) {
4284
+ return { ...DEFAULT_AUTO_SYNC_CONFIG };
4285
+ }
4286
+ const raw = JSON.parse(readFileSync2(AUTO_SYNC_CONFIG_PATH, "utf-8"));
4287
+ return {
4288
+ auto_sync_on_start: typeof raw.auto_sync_on_start === "boolean" ? raw.auto_sync_on_start : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_start,
4289
+ auto_sync_on_stop: typeof raw.auto_sync_on_stop === "boolean" ? raw.auto_sync_on_stop : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_stop
4290
+ };
4291
+ } catch {
4292
+ return { ...DEFAULT_AUTO_SYNC_CONFIG };
4293
+ }
4294
+ }
4295
+ async function executeAutoSync(event, serviceName, local, tables) {
4296
+ const direction = event === "start" ? "pull" : "push";
4297
+ const result = {
4298
+ event,
4299
+ direction,
4300
+ success: false,
4301
+ tables_synced: 0,
4302
+ total_rows_synced: 0,
4303
+ errors: []
4304
+ };
4305
+ let remote = null;
4306
+ try {
4307
+ const connStr = getConnectionString(serviceName);
4308
+ remote = new PgAdapterAsync(connStr);
4309
+ 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));
4310
+ if (syncTables.length === 0) {
4311
+ result.success = true;
4312
+ return result;
4313
+ }
4314
+ const results = direction === "pull" ? await syncPull(remote, local, { tables: syncTables }) : await syncPush(local, remote, { tables: syncTables });
4315
+ for (const r of results) {
4316
+ if (r.errors.length === 0)
4317
+ result.tables_synced++;
4318
+ result.total_rows_synced += r.rowsWritten;
4319
+ result.errors.push(...r.errors);
4320
+ }
4321
+ result.success = result.errors.length === 0;
4322
+ } catch (err) {
4323
+ result.errors.push(err?.message ?? String(err));
4324
+ } finally {
4325
+ if (remote) {
4326
+ try {
4327
+ await remote.close();
4328
+ } catch {}
4329
+ }
4330
+ }
4331
+ return result;
4332
+ }
4333
+ function installSignalHandlers() {
4334
+ if (signalHandlersInstalled)
4335
+ return;
4336
+ signalHandlersInstalled = true;
4337
+ const handleExit = async () => {
4338
+ for (const fn of cleanupHandlers) {
4339
+ try {
4340
+ await fn();
4341
+ } catch {}
4342
+ }
4343
+ };
4344
+ process.on("SIGTERM", async () => {
4345
+ await handleExit();
4346
+ process.exit(0);
4347
+ });
4348
+ process.on("SIGINT", async () => {
4349
+ await handleExit();
4350
+ process.exit(0);
4351
+ });
4352
+ process.on("beforeExit", async () => {
4353
+ await handleExit();
4354
+ });
4355
+ }
4356
+ function setupAutoSync(serviceName, server, local, remote, tables) {
4357
+ const config = getAutoSyncConfig();
4358
+ const cloudConfig = getCloudConfig();
4359
+ const isSyncEnabled = cloudConfig.mode === "hybrid" || cloudConfig.mode === "cloud";
4360
+ const syncOnStart = async () => {
4361
+ if (!config.auto_sync_on_start || !isSyncEnabled)
4362
+ return null;
4363
+ return executeAutoSync("start", serviceName, local, tables);
4364
+ };
4365
+ const syncOnStop = async () => {
4366
+ if (!config.auto_sync_on_stop || !isSyncEnabled)
4367
+ return null;
4368
+ return executeAutoSync("stop", serviceName, local, tables);
4369
+ };
4370
+ if (server && typeof server.onconnect === "function") {
4371
+ const origOnConnect = server.onconnect;
4372
+ server.onconnect = async (...args) => {
4373
+ await syncOnStart();
4374
+ return origOnConnect.apply(server, args);
4375
+ };
4376
+ } else if (server && typeof server.on === "function") {
4377
+ server.on("connect", () => syncOnStart());
4378
+ }
4379
+ if (server && typeof server.ondisconnect === "function") {
4380
+ const origOnDisconnect = server.ondisconnect;
4381
+ server.ondisconnect = async (...args) => {
4382
+ await syncOnStop();
4383
+ return origOnDisconnect.apply(server, args);
4384
+ };
4385
+ } else if (server && typeof server.on === "function") {
4386
+ server.on("disconnect", () => syncOnStop());
4387
+ }
4388
+ installSignalHandlers();
4389
+ cleanupHandlers.push(async () => {
4390
+ await syncOnStop();
4391
+ });
4392
+ return { syncOnStart, syncOnStop, config };
4393
+ }
4394
+ function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
4395
+ setupAutoSync(serviceName, mcpServer, local, remote, tables);
4396
+ }
4397
+ function discoverSyncableServices2() {
4398
+ const hasnaDir = getHasnaDir();
4399
+ const services = [];
4400
+ try {
4401
+ const entries = readdirSync3(hasnaDir, { withFileTypes: true });
4402
+ for (const entry of entries) {
4403
+ if (!entry.isDirectory())
4404
+ continue;
4405
+ const dbPath = join5(hasnaDir, entry.name, `${entry.name}.db`);
4406
+ if (existsSync5(dbPath)) {
4407
+ services.push(entry.name);
4408
+ }
4409
+ }
4410
+ } catch {}
4411
+ return services;
4412
+ }
4413
+ async function runScheduledSync() {
4414
+ const config = getCloudConfig();
4415
+ if (config.mode === "local")
4416
+ return [];
4417
+ const services = discoverSyncableServices2();
4418
+ const results = [];
4419
+ let remote = null;
4420
+ for (const service of services) {
4421
+ const result = {
4422
+ service,
4423
+ tables_synced: 0,
4424
+ total_rows_synced: 0,
4425
+ errors: []
4426
+ };
4427
+ try {
4428
+ const dbPath = join5(getDataDir(service), `${service}.db`);
4429
+ if (!existsSync5(dbPath)) {
4430
+ continue;
4431
+ }
4432
+ const local = new SqliteAdapter(dbPath);
4433
+ const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
4434
+ if (tables.length === 0) {
4435
+ local.close();
4436
+ continue;
4437
+ }
4438
+ try {
4439
+ const connStr = getConnectionString(service);
4440
+ remote = new PgAdapterAsync(connStr);
4441
+ } catch (err) {
4442
+ result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
4443
+ local.close();
4444
+ results.push(result);
4445
+ continue;
4446
+ }
4447
+ const stats = incrementalSyncPush(local, remote, tables);
4448
+ for (const s of stats) {
4449
+ if (s.errors.length === 0) {
4450
+ result.tables_synced++;
4451
+ }
4452
+ result.total_rows_synced += s.synced_rows;
4453
+ result.errors.push(...s.errors);
4454
+ }
4455
+ local.close();
4456
+ await remote.close();
4457
+ remote = null;
4458
+ } catch (err) {
4459
+ result.errors.push(err?.message ?? String(err));
4460
+ }
4461
+ results.push(result);
4462
+ }
4463
+ if (remote) {
4464
+ try {
4465
+ await remote.close();
4466
+ } catch {}
4467
+ }
4468
+ return results;
4469
+ }
4470
+ function parseInterval(input) {
4471
+ const trimmed = input.trim().toLowerCase();
4472
+ const hourMatch = trimmed.match(/^(\d+)\s*h$/);
4473
+ if (hourMatch) {
4474
+ const hours = parseInt(hourMatch[1], 10);
4475
+ if (hours <= 0) {
4476
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
4477
+ }
4478
+ return hours * 60;
4479
+ }
4480
+ const minMatch = trimmed.match(/^(\d+)\s*m$/);
4481
+ if (minMatch) {
4482
+ const mins = parseInt(minMatch[1], 10);
4483
+ if (mins <= 0) {
4484
+ throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
4485
+ }
4486
+ return mins;
4487
+ }
4488
+ const plain = parseInt(trimmed, 10);
4489
+ if (!isNaN(plain) && plain > 0) {
4490
+ return plain;
4491
+ }
4492
+ throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
4493
+ }
4494
+ function minutesToCron(minutes) {
4495
+ if (minutes <= 0) {
4496
+ throw new Error("Interval must be greater than 0 minutes.");
4497
+ }
4498
+ if (minutes < 60) {
4499
+ return `*/${minutes} * * * *`;
4500
+ }
4501
+ const hours = Math.floor(minutes / 60);
4502
+ const remainderMins = minutes % 60;
4503
+ if (remainderMins === 0 && hours <= 24) {
4504
+ return `0 */${hours} * * *`;
4505
+ }
4506
+ return `*/${minutes} * * * *`;
4507
+ }
4508
+ function getWorkerPath() {
4509
+ const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
4510
+ const tsPath = join6(dir, "scheduled-sync.ts");
4511
+ const jsPath = join6(dir, "scheduled-sync.js");
4512
+ try {
4513
+ if (existsSync6(tsPath))
4514
+ return tsPath;
4515
+ } catch {}
4516
+ return jsPath;
4517
+ }
4518
+ function getBunPath() {
4519
+ const candidates = [
4520
+ join6(homedir5(), ".bun", "bin", "bun"),
4521
+ "/usr/local/bin/bun",
4522
+ "/usr/bin/bun"
4523
+ ];
4524
+ for (const p of candidates) {
4525
+ if (existsSync6(p))
4526
+ return p;
4527
+ }
4528
+ return "bun";
4529
+ }
4530
+ function getLaunchdPlistPath() {
4531
+ return join6(homedir5(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
4532
+ }
4533
+ function createLaunchdPlist(intervalMinutes) {
4534
+ const workerPath = getWorkerPath();
4535
+ const bunPath = getBunPath();
4536
+ const logPath = join6(CONFIG_DIR2, "sync.log");
4537
+ const errorLogPath = join6(CONFIG_DIR2, "sync-error.log");
4538
+ return `<?xml version="1.0" encoding="UTF-8"?>
4539
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4540
+ <plist version="1.0">
4541
+ <dict>
4542
+ <key>Label</key>
4543
+ <string>com.hasna.cloud-sync</string>
4544
+ <key>ProgramArguments</key>
4545
+ <array>
4546
+ <string>${bunPath}</string>
4547
+ <string>run</string>
4548
+ <string>${workerPath}</string>
4549
+ </array>
4550
+ <key>StartInterval</key>
4551
+ <integer>${intervalMinutes * 60}</integer>
4552
+ <key>RunAtLoad</key>
4553
+ <true/>
4554
+ <key>StandardOutPath</key>
4555
+ <string>${logPath}</string>
4556
+ <key>StandardErrorPath</key>
4557
+ <string>${errorLogPath}</string>
4558
+ <key>EnvironmentVariables</key>
4559
+ <dict>
4560
+ <key>PATH</key>
4561
+ <string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
4562
+ <key>HOME</key>
4563
+ <string>${homedir5()}</string>
4564
+ </dict>
4565
+ </dict>
4566
+ </plist>`;
4567
+ }
4568
+ async function registerLaunchd(intervalMinutes) {
4569
+ const plistPath = getLaunchdPlistPath();
4570
+ const plistDir = dirname(plistPath);
4571
+ mkdirSync3(plistDir, { recursive: true });
4572
+ try {
4573
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
4574
+ } catch {}
4575
+ writeFileSync2(plistPath, createLaunchdPlist(intervalMinutes));
4576
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
4577
+ }
4578
+ async function removeLaunchd() {
4579
+ const plistPath = getLaunchdPlistPath();
4580
+ try {
4581
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
4582
+ } catch {}
4583
+ try {
4584
+ unlinkSync(plistPath);
4585
+ } catch {}
4586
+ }
4587
+ function getSystemdDir() {
4588
+ return join6(homedir5(), ".config", "systemd", "user");
4589
+ }
4590
+ function createSystemdService() {
4591
+ const workerPath = getWorkerPath();
4592
+ const bunPath = getBunPath();
4593
+ return `[Unit]
4594
+ Description=Hasna Cloud Sync
4595
+ After=network.target
4596
+
4597
+ [Service]
4598
+ Type=oneshot
4599
+ ExecStart=${bunPath} run ${workerPath}
4600
+ Environment=HOME=${homedir5()}
4601
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
4602
+
4603
+ [Install]
4604
+ WantedBy=default.target
4605
+ `;
4606
+ }
4607
+ function createSystemdTimer(intervalMinutes) {
4608
+ return `[Unit]
4609
+ Description=Hasna Cloud Sync Timer
4610
+
4611
+ [Timer]
4612
+ OnBootSec=${intervalMinutes}min
4613
+ OnUnitActiveSec=${intervalMinutes}min
4614
+ Persistent=true
4615
+
4616
+ [Install]
4617
+ WantedBy=timers.target
4618
+ `;
4619
+ }
4620
+ async function registerSystemd(intervalMinutes) {
4621
+ const dir = getSystemdDir();
4622
+ mkdirSync3(dir, { recursive: true });
4623
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.service`), createSystemdService());
4624
+ writeFileSync2(join6(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
4625
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
4626
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
4627
+ }
4628
+ async function removeSystemd() {
4629
+ try {
4630
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SERVICE_NAME}.timer`]).exited;
4631
+ } catch {}
4632
+ const dir = getSystemdDir();
4633
+ try {
4634
+ unlinkSync(join6(dir, `${SERVICE_NAME}.service`));
4635
+ } catch {}
4636
+ try {
4637
+ unlinkSync(join6(dir, `${SERVICE_NAME}.timer`));
4638
+ } catch {}
4639
+ try {
4640
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
4641
+ } catch {}
4642
+ }
4643
+ async function registerSyncSchedule(intervalMinutes) {
4644
+ if (intervalMinutes <= 0) {
4645
+ throw new Error("Interval must be a positive number of minutes.");
4646
+ }
4647
+ mkdirSync3(CONFIG_DIR2, { recursive: true });
4648
+ if (platform() === "darwin") {
4649
+ await registerLaunchd(intervalMinutes);
4650
+ } else {
4651
+ await registerSystemd(intervalMinutes);
4652
+ }
4653
+ const config = getCloudConfig();
4654
+ config.sync.schedule_minutes = intervalMinutes;
4655
+ saveCloudConfig(config);
4656
+ }
4657
+ async function removeSyncSchedule() {
4658
+ if (platform() === "darwin") {
4659
+ await removeLaunchd();
4660
+ } else {
4661
+ await removeSystemd();
4662
+ }
4663
+ const config = getCloudConfig();
4664
+ config.sync.schedule_minutes = 0;
4665
+ saveCloudConfig(config);
4666
+ }
4667
+ function getSyncScheduleStatus() {
4668
+ const config = getCloudConfig();
4669
+ const minutes = config.sync.schedule_minutes;
4670
+ const registered = minutes > 0;
4671
+ let mechanism = "none";
4672
+ if (registered) {
4673
+ if (platform() === "darwin") {
4674
+ mechanism = existsSync6(getLaunchdPlistPath()) ? "launchd" : "none";
4675
+ } else {
4676
+ mechanism = existsSync6(join6(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
4677
+ }
4678
+ }
4679
+ return {
4680
+ registered,
4681
+ schedule_minutes: minutes,
4682
+ cron_expression: registered ? minutesToCron(minutes) : null,
4683
+ mechanism
4684
+ };
4685
+ }
4686
+ async function applyPgMigrations(connectionString, migrations, service = "unknown") {
4687
+ const pg2 = new PgAdapterAsync(connectionString);
4688
+ const result = {
4689
+ service,
4690
+ applied: [],
4691
+ alreadyApplied: [],
4692
+ errors: [],
4693
+ totalMigrations: migrations.length
4694
+ };
4695
+ try {
4696
+ await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
4697
+ id SERIAL PRIMARY KEY,
4698
+ version INT UNIQUE NOT NULL,
4699
+ applied_at TIMESTAMPTZ DEFAULT NOW()
4700
+ )`);
4701
+ const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
4702
+ const appliedSet = new Set(applied.map((r) => r.version));
4703
+ for (let i = 0;i < migrations.length; i++) {
4704
+ if (appliedSet.has(i)) {
4705
+ result.alreadyApplied.push(i);
4706
+ continue;
4707
+ }
4708
+ try {
4709
+ await pg2.exec(migrations[i]);
4710
+ await pg2.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
4711
+ result.applied.push(i);
4712
+ } catch (err) {
4713
+ result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
4714
+ break;
4715
+ }
4716
+ }
4717
+ } finally {
4718
+ await pg2.close();
4719
+ }
4720
+ return result;
4721
+ }
4722
+ function getServicePackage(service) {
4723
+ return `@hasna/${service}`;
4724
+ }
4725
+ async function loadServiceMigrations(service) {
4726
+ const pkg = getServicePackage(service);
4727
+ const paths = [
4728
+ `${pkg}/pg-migrations`,
4729
+ `${pkg}/dist/db/pg-migrations.js`,
4730
+ `${pkg}/dist/db/pg-migrations`
4731
+ ];
4732
+ for (const path of paths) {
4733
+ try {
4734
+ const mod = await import(path);
4735
+ if (Array.isArray(mod.PG_MIGRATIONS)) {
4736
+ return mod.PG_MIGRATIONS;
4737
+ }
4738
+ if (mod.default && Array.isArray(mod.default.PG_MIGRATIONS)) {
4739
+ return mod.default.PG_MIGRATIONS;
4740
+ }
4741
+ } catch {}
4742
+ }
4743
+ return null;
4744
+ }
4745
+ async function migrateService(service, connectionString) {
4746
+ const connStr = connectionString ?? getConnectionString(service);
4747
+ const migrations = await loadServiceMigrations(service);
4748
+ if (!migrations) {
4749
+ return {
4750
+ service,
4751
+ applied: [],
4752
+ alreadyApplied: [],
4753
+ errors: [`No PG migrations found for service "${service}"`],
4754
+ totalMigrations: 0
4755
+ };
4756
+ }
4757
+ return applyPgMigrations(connStr, migrations, service);
4758
+ }
4759
+ async function migrateAllServices() {
4760
+ const { discoverServices: discoverServices2 } = await Promise.resolve().then(() => (init_discover(), exports_discover));
4761
+ const services = discoverServices2();
4762
+ const results = [];
4763
+ for (const service of services) {
4764
+ try {
4765
+ const result = await migrateService(service);
4766
+ results.push(result);
4767
+ } catch (err) {
4768
+ results.push({
4769
+ service,
4770
+ applied: [],
4771
+ alreadyApplied: [],
4772
+ errors: [err?.message ?? String(err)],
4773
+ totalMigrations: 0
4774
+ });
4775
+ }
4776
+ }
4777
+ return results;
4778
+ }
4779
+ async function ensurePgDatabase(service) {
4780
+ const config = (await Promise.resolve().then(() => (init_config(), exports_config))).getCloudConfig();
4781
+ const { host, port, username, password_env, ssl } = config.rds;
4782
+ if (!host || !username)
4783
+ return false;
4784
+ const password = process.env[password_env] ?? "";
4785
+ const sslParam = ssl ? "?sslmode=require" : "";
4786
+ const adminConnStr = `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/postgres${sslParam}`;
4787
+ const pg2 = new PgAdapterAsync(adminConnStr);
4788
+ try {
4789
+ const existing = await pg2.all(`SELECT 1 FROM pg_database WHERE datname = $1`, service);
4790
+ if (existing.length === 0) {
4791
+ await pg2.exec(`CREATE DATABASE "${service}"`);
4792
+ return true;
4793
+ }
4794
+ return false;
4795
+ } finally {
4796
+ await pg2.close();
4797
+ }
4798
+ }
4799
+ async function ensureAllPgDatabases() {
4800
+ const { discoverServices: discoverServices2 } = await Promise.resolve().then(() => (init_discover(), exports_discover));
4801
+ const services = discoverServices2();
4802
+ const results = [];
4803
+ for (const service of services) {
4804
+ try {
4805
+ const created = await ensurePgDatabase(service);
4806
+ results.push({ service, created });
4807
+ } catch (err) {
4808
+ results.push({ service, created: false, error: err?.message ?? String(err) });
4809
+ }
4810
+ }
4811
+ return results;
4812
+ }
3851
4813
  function registerCloudTools(server, serviceName) {
3852
4814
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
3853
4815
  const config = getCloudConfig();
@@ -4279,7 +5241,13 @@ CREATE TABLE IF NOT EXISTS feedback (
4279
5241
  email TEXT DEFAULT '',
4280
5242
  machine_id TEXT DEFAULT '',
4281
5243
  created_at TEXT DEFAULT (datetime('now'))
4282
- )`, AUTO_SYNC_CONFIG_PATH, CONFIG_DIR2;
5244
+ )`, SYNC_META_TABLE_SQL = `
5245
+ CREATE TABLE IF NOT EXISTS _sync_meta (
5246
+ table_name TEXT PRIMARY KEY,
5247
+ last_synced_at TEXT,
5248
+ last_synced_row_count INTEGER DEFAULT 0,
5249
+ direction TEXT DEFAULT 'push'
5250
+ )`, AUTO_SYNC_CONFIG_PATH, DEFAULT_AUTO_SYNC_CONFIG, cleanupHandlers, signalHandlersInstalled = false, SERVICE_NAME = "hasna-cloud-sync", CONFIG_DIR2;
4283
5251
  var init_dist = __esm(() => {
4284
5252
  __create2 = Object.create;
4285
5253
  __getProtoOf2 = Object.getPrototypeOf;
@@ -12347,6 +13315,11 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
12347
13315
  init_config();
12348
13316
  init_discover();
12349
13317
  AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
13318
+ DEFAULT_AUTO_SYNC_CONFIG = {
13319
+ auto_sync_on_start: true,
13320
+ auto_sync_on_stop: true
13321
+ };
13322
+ cleanupHandlers = [];
12350
13323
  init_config();
12351
13324
  init_adapter();
12352
13325
  init_dotfile();
@@ -12372,23 +13345,23 @@ __export(exports_db, {
12372
13345
  getDataDir: () => getDataDir2,
12373
13346
  closeDb: () => closeDb
12374
13347
  });
12375
- import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync } from "fs";
12376
- import { join as join5, dirname as dirname2 } from "path";
13348
+ import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync4, statSync } from "fs";
13349
+ import { join as join7, dirname as dirname2 } from "path";
12377
13350
  import { homedir as homedir6 } from "os";
12378
13351
  function getDataDir2() {
12379
13352
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir6();
12380
- const newDir = join5(home, ".hasna", "conversations");
12381
- const oldDir = join5(home, ".conversations");
12382
- if (existsSync4(oldDir) && !existsSync4(newDir)) {
12383
- mkdirSync3(newDir, { recursive: true });
12384
- for (const file of readdirSync3(oldDir)) {
12385
- const oldPath = join5(oldDir, file);
13353
+ const newDir = join7(home, ".hasna", "conversations");
13354
+ const oldDir = join7(home, ".conversations");
13355
+ if (existsSync7(oldDir) && !existsSync7(newDir)) {
13356
+ mkdirSync4(newDir, { recursive: true });
13357
+ for (const file of readdirSync4(oldDir)) {
13358
+ const oldPath = join7(oldDir, file);
12386
13359
  if (statSync(oldPath).isFile()) {
12387
- copyFileSync2(oldPath, join5(newDir, file));
13360
+ copyFileSync2(oldPath, join7(newDir, file));
12388
13361
  }
12389
13362
  }
12390
13363
  }
12391
- mkdirSync3(newDir, { recursive: true });
13364
+ mkdirSync4(newDir, { recursive: true });
12392
13365
  return newDir;
12393
13366
  }
12394
13367
  function getDbPath2() {
@@ -12396,13 +13369,13 @@ function getDbPath2() {
12396
13369
  return process.env.HASNA_CONVERSATIONS_DB_PATH;
12397
13370
  if (process.env.CONVERSATIONS_DB_PATH)
12398
13371
  return process.env.CONVERSATIONS_DB_PATH;
12399
- return join5(getDataDir2(), "messages.db");
13372
+ return join7(getDataDir2(), "messages.db");
12400
13373
  }
12401
13374
  function getDb() {
12402
13375
  if (db)
12403
13376
  return db;
12404
13377
  const dbPath = getDbPath2();
12405
- mkdirSync3(dirname2(dbPath), { recursive: true });
13378
+ mkdirSync4(dirname2(dbPath), { recursive: true });
12406
13379
  db = new SqliteAdapter(dbPath);
12407
13380
  db.exec("PRAGMA journal_mode = WAL");
12408
13381
  db.exec("PRAGMA busy_timeout = 5000");
@@ -12680,8 +13653,8 @@ var init_db = __esm(() => {
12680
13653
  });
12681
13654
 
12682
13655
  // src/lib/identity.ts
12683
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5 } from "fs";
12684
- import { join as join7, dirname as dirname3 } from "path";
13656
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync6 } from "fs";
13657
+ import { join as join9, dirname as dirname3 } from "path";
12685
13658
  function isNameTaken(name) {
12686
13659
  try {
12687
13660
  const { getDb: getDb2 } = (init_db(), __toCommonJS(exports_db));
@@ -12696,7 +13669,7 @@ function getAutoName() {
12696
13669
  if (cachedAutoName)
12697
13670
  return cachedAutoName;
12698
13671
  try {
12699
- const name2 = readFileSync2(AGENT_ID_FILE, "utf-8").trim();
13672
+ const name2 = readFileSync3(AGENT_ID_FILE, "utf-8").trim();
12700
13673
  if (name2) {
12701
13674
  cachedAutoName = name2;
12702
13675
  return name2;
@@ -12712,8 +13685,8 @@ function getAutoName() {
12712
13685
  }
12713
13686
  cachedAutoName = name;
12714
13687
  try {
12715
- mkdirSync5(dirname3(AGENT_ID_FILE), { recursive: true });
12716
- writeFileSync2(AGENT_ID_FILE, name + `
13688
+ mkdirSync6(dirname3(AGENT_ID_FILE), { recursive: true });
13689
+ writeFileSync3(AGENT_ID_FILE, name + `
12717
13690
  `, "utf-8");
12718
13691
  } catch {}
12719
13692
  return name;
@@ -12730,8 +13703,8 @@ function resolveIdentity(explicit) {
12730
13703
  function updateCachedAutoName(newName) {
12731
13704
  cachedAutoName = newName;
12732
13705
  try {
12733
- mkdirSync5(dirname3(AGENT_ID_FILE), { recursive: true });
12734
- writeFileSync2(AGENT_ID_FILE, newName + `
13706
+ mkdirSync6(dirname3(AGENT_ID_FILE), { recursive: true });
13707
+ writeFileSync3(AGENT_ID_FILE, newName + `
12735
13708
  `, "utf-8");
12736
13709
  } catch {}
12737
13710
  }
@@ -12739,7 +13712,7 @@ var AGENT_ID_FILE, cachedAutoName = null;
12739
13712
  var init_identity = __esm(() => {
12740
13713
  init_names();
12741
13714
  init_db();
12742
- AGENT_ID_FILE = join7(getDataDir2(), "agent-id");
13715
+ AGENT_ID_FILE = join9(getDataDir2(), "agent-id");
12743
13716
  });
12744
13717
 
12745
13718
  // src/lib/sessions.ts
@@ -12982,17 +13955,17 @@ var init_spaces = __esm(() => {
12982
13955
  });
12983
13956
 
12984
13957
  // src/lib/webhooks.ts
12985
- import { readFileSync as readFileSync3 } from "fs";
12986
- import { join as join8 } from "path";
13958
+ import { readFileSync as readFileSync5 } from "fs";
13959
+ import { join as join10 } from "path";
12987
13960
  function getConfigPath2() {
12988
- return process.env.CONVERSATIONS_CONFIG_PATH || join8(getDataDir2(), "config.json");
13961
+ return process.env.CONVERSATIONS_CONFIG_PATH || join10(getDataDir2(), "config.json");
12989
13962
  }
12990
13963
  function loadConfig() {
12991
13964
  const now = Date.now();
12992
13965
  if (cachedConfig && now - configLoadedAt < CONFIG_CACHE_MS)
12993
13966
  return cachedConfig;
12994
13967
  try {
12995
- const raw = readFileSync3(getConfigPath2(), "utf-8");
13968
+ const raw = readFileSync5(getConfigPath2(), "utf-8");
12996
13969
  cachedConfig = JSON.parse(raw);
12997
13970
  configLoadedAt = now;
12998
13971
  return cachedConfig;
@@ -13047,8 +14020,8 @@ var init_webhooks = __esm(() => {
13047
14020
 
13048
14021
  // src/lib/messages.ts
13049
14022
  import { randomUUID } from "crypto";
13050
- import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
13051
- import { join as join9 } from "path";
14023
+ import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
14024
+ import { join as join11 } from "path";
13052
14025
  function compactMessage(msg) {
13053
14026
  const result = {};
13054
14027
  for (const key of Object.keys(msg)) {
@@ -13087,7 +14060,7 @@ function parseMessage(row) {
13087
14060
  function getAttachmentsDir() {
13088
14061
  if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
13089
14062
  return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
13090
- return join9(getDataDir2(), "attachments");
14063
+ return join11(getDataDir2(), "attachments");
13091
14064
  }
13092
14065
  function guessMimeType(name) {
13093
14066
  const ext = name.split(".").pop()?.toLowerCase();
@@ -13132,11 +14105,11 @@ function sendMessage(opts) {
13132
14105
  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);
13133
14106
  const message = parseMessage(row);
13134
14107
  if (opts.attachments && opts.attachments.length > 0) {
13135
- const attachmentsDir = join9(getAttachmentsDir(), String(message.id));
13136
- mkdirSync6(attachmentsDir, { recursive: true });
14108
+ const attachmentsDir = join11(getAttachmentsDir(), String(message.id));
14109
+ mkdirSync7(attachmentsDir, { recursive: true });
13137
14110
  const attachmentInfos = [];
13138
14111
  for (const att of opts.attachments) {
13139
- const destPath = join9(attachmentsDir, att.name);
14112
+ const destPath = join11(attachmentsDir, att.name);
13140
14113
  copyFileSync3(att.source_path, destPath);
13141
14114
  const stat = statSync2(destPath);
13142
14115
  attachmentInfos.push({
@@ -13927,7 +14900,7 @@ var init_presence = __esm(() => {
13927
14900
  var require_package = __commonJS((exports, module) => {
13928
14901
  module.exports = {
13929
14902
  name: "@hasna/conversations",
13930
- version: "0.2.24",
14903
+ version: "0.2.25",
13931
14904
  description: "Real-time CLI messaging for AI agents",
13932
14905
  type: "module",
13933
14906
  bin: {
@@ -45299,6 +46272,192 @@ var init_advanced = __esm(() => {
45299
46272
  init_graph();
45300
46273
  });
45301
46274
 
46275
+ // src/mcp/tools/cloud.ts
46276
+ async function detectAndLogConflicts(local, cloud, table) {
46277
+ if (!CONFLICT_TABLES.has(table))
46278
+ return 0;
46279
+ try {
46280
+ const { detectConflicts: detectConflicts2, storeConflicts: storeConflicts2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
46281
+ const pk = table === "messages" ? "id" : table === "spaces" ? "name" : table === "space_members" ? "space" : "id";
46282
+ const tsCol = "created_at";
46283
+ const localRows = local.all(`SELECT * FROM "${table}"`);
46284
+ const remoteRows = await cloud.all(`SELECT * FROM "${table}"`);
46285
+ if (localRows.length === 0 || remoteRows.length === 0)
46286
+ return 0;
46287
+ const conflicts = detectConflicts2(localRows, remoteRows, table, pk, tsCol);
46288
+ if (conflicts.length > 0) {
46289
+ storeConflicts2(local, conflicts);
46290
+ }
46291
+ return conflicts.length;
46292
+ } catch {
46293
+ return 0;
46294
+ }
46295
+ }
46296
+ function registerCloudSyncTools(server) {
46297
+ server.tool("conversations_cloud_status", "Show cloud configuration, connection health, and sync status", {}, async () => {
46298
+ try {
46299
+ const {
46300
+ getCloudConfig: getCloudConfig2,
46301
+ getConnectionString: getConnectionString2,
46302
+ PgAdapterAsync: PgAdapterAsync2,
46303
+ listConflicts: listConflicts2,
46304
+ ensureConflictsTable: ensureConflictsTable2,
46305
+ SqliteAdapter: SqliteAdapter2,
46306
+ getDbPath: cloudGetDbPath
46307
+ } = await Promise.resolve().then(() => (init_dist(), exports_dist));
46308
+ const config2 = getCloudConfig2();
46309
+ const lines = [
46310
+ `Mode: ${config2.mode}`,
46311
+ `Service: conversations`,
46312
+ `RDS Host: ${config2.rds.host || "(not configured)"}`
46313
+ ];
46314
+ if (config2.rds.host && config2.rds.username) {
46315
+ try {
46316
+ const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
46317
+ await pg.get("SELECT 1 as ok");
46318
+ lines.push("PostgreSQL: connected");
46319
+ await pg.close();
46320
+ } catch (err) {
46321
+ lines.push(`PostgreSQL: failed \u2014 ${err?.message}`);
46322
+ }
46323
+ }
46324
+ try {
46325
+ const local = new SqliteAdapter2(cloudGetDbPath("conversations"));
46326
+ ensureConflictsTable2(local);
46327
+ const unresolved = listConflicts2(local, { resolved: false });
46328
+ const resolved = listConflicts2(local, { resolved: true });
46329
+ lines.push(`Sync conflicts: ${unresolved.length} unresolved, ${resolved.length} resolved`);
46330
+ local.close();
46331
+ } catch {}
46332
+ return { content: [{ type: "text", text: lines.join(`
46333
+ `) }] };
46334
+ } catch (e) {
46335
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
46336
+ }
46337
+ });
46338
+ server.tool("conversations_cloud_push", "Push local conversations data to cloud PostgreSQL. Detects conflicts and syncs all tables.", {
46339
+ tables: exports_external2.string().optional().describe("Comma-separated table names (default: all)")
46340
+ }, async ({ tables: tablesStr }) => {
46341
+ try {
46342
+ const {
46343
+ getCloudConfig: getCloudConfig2,
46344
+ getConnectionString: getConnectionString2,
46345
+ syncPush: syncPush2,
46346
+ listSqliteTables: listSqliteTables2,
46347
+ SqliteAdapter: SqliteAdapter2,
46348
+ PgAdapterAsync: PgAdapterAsync2,
46349
+ getDbPath: cloudGetDbPath
46350
+ } = await Promise.resolve().then(() => (init_dist(), exports_dist));
46351
+ const config2 = getCloudConfig2();
46352
+ if (config2.mode === "local") {
46353
+ return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
46354
+ }
46355
+ const localPath = cloudGetDbPath("conversations");
46356
+ const local = new SqliteAdapter2(localPath);
46357
+ const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
46358
+ const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !t.startsWith("_") && !t.endsWith("_fts") && !t.startsWith("messages_fts"));
46359
+ let totalConflicts = 0;
46360
+ for (const table of tableList) {
46361
+ totalConflicts += await detectAndLogConflicts(local, cloud, table);
46362
+ }
46363
+ const results = await syncPush2(local, cloud, { tables: tableList });
46364
+ local.close();
46365
+ await cloud.close();
46366
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
46367
+ const errors4 = results.flatMap((r) => r.errors);
46368
+ const lines = [`Pushed ${total} rows across ${tableList.length} table(s).`];
46369
+ if (totalConflicts > 0)
46370
+ lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
46371
+ if (errors4.length > 0)
46372
+ lines.push(`Errors: ${errors4.join("; ")}`);
46373
+ return { content: [{ type: "text", text: lines.join(`
46374
+ `) }] };
46375
+ } catch (e) {
46376
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
46377
+ }
46378
+ });
46379
+ server.tool("conversations_cloud_pull", "Pull cloud PostgreSQL data to local. Detects conflicts and merges by primary key with UPSERT.", {
46380
+ tables: exports_external2.string().optional().describe("Comma-separated table names (default: all)")
46381
+ }, async ({ tables: tablesStr }) => {
46382
+ try {
46383
+ const {
46384
+ getCloudConfig: getCloudConfig2,
46385
+ getConnectionString: getConnectionString2,
46386
+ syncPull: syncPull2,
46387
+ listPgTables: listPgTables2,
46388
+ SqliteAdapter: SqliteAdapter2,
46389
+ PgAdapterAsync: PgAdapterAsync2,
46390
+ getDbPath: cloudGetDbPath
46391
+ } = await Promise.resolve().then(() => (init_dist(), exports_dist));
46392
+ const config2 = getCloudConfig2();
46393
+ if (config2.mode === "local") {
46394
+ return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
46395
+ }
46396
+ const local = new SqliteAdapter2(cloudGetDbPath("conversations"));
46397
+ const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
46398
+ let tableList;
46399
+ if (tablesStr) {
46400
+ tableList = tablesStr.split(",").map((t) => t.trim());
46401
+ } else {
46402
+ try {
46403
+ tableList = (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
46404
+ } catch {
46405
+ local.close();
46406
+ await cloud.close();
46407
+ return { content: [{ type: "text", text: "Error: failed to list cloud tables." }], isError: true };
46408
+ }
46409
+ }
46410
+ let totalConflicts = 0;
46411
+ for (const table of tableList) {
46412
+ totalConflicts += await detectAndLogConflicts(local, cloud, table);
46413
+ }
46414
+ const results = await syncPull2(cloud, local, { tables: tableList });
46415
+ local.close();
46416
+ await cloud.close();
46417
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
46418
+ const errors4 = results.flatMap((r) => r.errors);
46419
+ const lines = [`Pulled ${total} rows across ${tableList.length} table(s).`];
46420
+ if (totalConflicts > 0)
46421
+ lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
46422
+ if (errors4.length > 0)
46423
+ lines.push(`Errors: ${errors4.join("; ")}`);
46424
+ return { content: [{ type: "text", text: lines.join(`
46425
+ `) }] };
46426
+ } catch (e) {
46427
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
46428
+ }
46429
+ });
46430
+ server.tool("conversations_cloud_feedback", "Send feedback for the conversations service", {
46431
+ message: exports_external2.string().describe("Feedback message"),
46432
+ email: exports_external2.string().optional().describe("Contact email")
46433
+ }, async ({ message, email: email3 }) => {
46434
+ try {
46435
+ const { sendFeedback: sendFeedback2, createDatabase: createDatabase2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
46436
+ const db2 = createDatabase2({ service: "cloud" });
46437
+ const result = await sendFeedback2({ service: "conversations", message, email: email3 }, db2);
46438
+ db2.close();
46439
+ return {
46440
+ content: [{
46441
+ type: "text",
46442
+ text: result.sent ? `Feedback sent (id: ${result.id})` : `Saved locally (id: ${result.id}): ${result.error}`
46443
+ }]
46444
+ };
46445
+ } catch (e) {
46446
+ return { content: [{ type: "text", text: formatError2(e) }], isError: true };
46447
+ }
46448
+ });
46449
+ }
46450
+ function formatError2(e) {
46451
+ if (e instanceof Error)
46452
+ return e.message;
46453
+ return String(e);
46454
+ }
46455
+ var CONFLICT_TABLES;
46456
+ var init_cloud = __esm(() => {
46457
+ init_zod2();
46458
+ CONFLICT_TABLES = new Set(["messages", "spaces", "projects", "agent_presence"]);
46459
+ });
46460
+
45302
46461
  // src/mcp/index.ts
45303
46462
  var exports_mcp = {};
45304
46463
  __export(exports_mcp, {
@@ -45319,20 +46478,20 @@ function resolveProjectId(explicitProjectId, agentId) {
45319
46478
  }
45320
46479
  async function startMcpServer() {
45321
46480
  const transport = new StdioServerTransport;
45322
- registerCloudTools(server, "conversations");
46481
+ registerCloudSyncTools(server);
45323
46482
  await server.connect(transport);
45324
46483
  }
45325
46484
  var import__package2, server, agentFocus, isDirectRun;
45326
46485
  var init_mcp2 = __esm(() => {
45327
46486
  init_mcp();
45328
46487
  init_stdio2();
45329
- init_dist();
45330
46488
  init_presence();
45331
46489
  init_messaging();
45332
46490
  init_spaces2();
45333
46491
  init_projects2();
45334
46492
  init_agents();
45335
46493
  init_advanced();
46494
+ init_cloud();
45336
46495
  import__package2 = __toESM(require_package(), 1);
45337
46496
  server = new McpServer({
45338
46497
  name: "conversations",
@@ -45358,8 +46517,8 @@ var exports_serve = {};
45358
46517
  __export(exports_serve, {
45359
46518
  startDashboardServer: () => startDashboardServer
45360
46519
  });
45361
- import { join as join12, resolve, sep } from "path";
45362
- import { existsSync as existsSync7 } from "fs";
46520
+ import { join as join14, resolve, sep } from "path";
46521
+ import { existsSync as existsSync10 } from "fs";
45363
46522
  function securityHeaders(base) {
45364
46523
  const headers = new Headers(base);
45365
46524
  if (!headers.has("X-Content-Type-Options"))
@@ -45456,8 +46615,8 @@ function isSameOrigin(req) {
45456
46615
  function startDashboardServer(port = 0, host) {
45457
46616
  const resolvedPort = normalizePort(port, 0);
45458
46617
  const resolvedHost = normalizeHost(host ?? process.env.CONVERSATIONS_DASHBOARD_HOST);
45459
- const dashboardDist = join12(import.meta.dir, "../../dashboard/dist");
45460
- const hasDist = existsSync7(dashboardDist);
46618
+ const dashboardDist = join14(import.meta.dir, "../../dashboard/dist");
46619
+ const hasDist = existsSync10(dashboardDist);
45461
46620
  const server2 = Bun.serve({
45462
46621
  port: resolvedPort,
45463
46622
  hostname: resolvedHost,
@@ -45878,7 +47037,7 @@ function startDashboardServer(port = 0, host) {
45878
47037
  headers.set("Content-Type", file2.type);
45879
47038
  return new Response(file2, { headers });
45880
47039
  }
45881
- file2 = Bun.file(join12(dashboardDist, "index.html"));
47040
+ file2 = Bun.file(join14(dashboardDist, "index.html"));
45882
47041
  if (await file2.exists()) {
45883
47042
  const headers = securityHeaders();
45884
47043
  if (file2.type)
@@ -46871,8 +48030,8 @@ function App({ agent }) {
46871
48030
 
46872
48031
  // src/cli/brains.ts
46873
48032
  import chalk2 from "chalk";
46874
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync4 } from "fs";
46875
- import { join as join11 } from "path";
48033
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
48034
+ import { join as join13 } from "path";
46876
48035
  import { spawnSync } from "child_process";
46877
48036
 
46878
48037
  // src/lib/gatherer.ts
@@ -46936,25 +48095,25 @@ var gatherTrainingData = async (options = {}) => {
46936
48095
 
46937
48096
  // src/lib/model-config.ts
46938
48097
  init_db();
46939
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7, existsSync as existsSync6 } from "fs";
46940
- import { join as join10 } from "path";
48098
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync8, existsSync as existsSync9 } from "fs";
48099
+ import { join as join12 } from "path";
46941
48100
  var DEFAULT_MODEL = "gpt-4o-mini";
46942
48101
  var CONFIG_DIR3 = getDataDir2();
46943
- var CONFIG_PATH2 = join10(CONFIG_DIR3, "config.json");
48102
+ var CONFIG_PATH2 = join12(CONFIG_DIR3, "config.json");
46944
48103
  function readConfig() {
46945
- if (!existsSync6(CONFIG_PATH2))
48104
+ if (!existsSync9(CONFIG_PATH2))
46946
48105
  return {};
46947
48106
  try {
46948
- return JSON.parse(readFileSync4(CONFIG_PATH2, "utf-8"));
48107
+ return JSON.parse(readFileSync6(CONFIG_PATH2, "utf-8"));
46949
48108
  } catch {
46950
48109
  return {};
46951
48110
  }
46952
48111
  }
46953
48112
  function writeConfig(config) {
46954
- if (!existsSync6(CONFIG_DIR3)) {
46955
- mkdirSync7(CONFIG_DIR3, { recursive: true });
48113
+ if (!existsSync9(CONFIG_DIR3)) {
48114
+ mkdirSync8(CONFIG_DIR3, { recursive: true });
46956
48115
  }
46957
- writeFileSync3(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
48116
+ writeFileSync5(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
46958
48117
  }
46959
48118
  function getActiveModel() {
46960
48119
  const config = readConfig();
@@ -46979,13 +48138,13 @@ function registerBrainsCommand(program2) {
46979
48138
  try {
46980
48139
  const since = opts.since ? new Date(opts.since) : undefined;
46981
48140
  const result = await gatherTrainingData({ limit: opts.limit, since });
46982
- const outputDir = join11(getDataDir2(), "training");
46983
- mkdirSync8(outputDir, { recursive: true });
48141
+ const outputDir = join13(getDataDir2(), "training");
48142
+ mkdirSync9(outputDir, { recursive: true });
46984
48143
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
46985
- const outputPath = opts.output ?? join11(outputDir, `training-${timestamp}.jsonl`);
48144
+ const outputPath = opts.output ?? join13(outputDir, `training-${timestamp}.jsonl`);
46986
48145
  const jsonl = result.examples.map((ex) => JSON.stringify(ex)).join(`
46987
48146
  `);
46988
- writeFileSync4(outputPath, jsonl, "utf-8");
48147
+ writeFileSync6(outputPath, jsonl, "utf-8");
46989
48148
  if (opts.json) {
46990
48149
  console.log(JSON.stringify({ path: outputPath, count: result.count, source: result.source }));
46991
48150
  } else {
@@ -48153,10 +49312,10 @@ function registerAgentCommands(program2) {
48153
49312
  } else if (envValue) {
48154
49313
  source = "env var (CONVERSATIONS_AGENT_ID)";
48155
49314
  } else {
48156
- const { join: join12 } = __require("path");
49315
+ const { join: join8 } = __require("path");
48157
49316
  const { homedir: homedir7 } = __require("os");
48158
49317
  const { getDataDir: getDataDir3 } = (init_db(), __toCommonJS(exports_db));
48159
- const agentIdFile = join12(getDataDir3(), "agent-id");
49318
+ const agentIdFile = join8(getDataDir3(), "agent-id");
48160
49319
  source = `auto-generated (${agentIdFile})`;
48161
49320
  }
48162
49321
  const presence = getPresence(agent);
@@ -48459,14 +49618,14 @@ function registerAnalyticsCommands(program2) {
48459
49618
  checks.push({ name: "npm version", ok: true, message: "Could not check npm registry (offline?)" });
48460
49619
  }
48461
49620
  const { homedir: homedir7 } = await import("os");
48462
- const { existsSync: existsSync5 } = await import("fs");
48463
- const { join: join12 } = await import("path");
49621
+ const { existsSync: existsSync8 } = await import("fs");
49622
+ const { join: join8 } = await import("path");
48464
49623
  const { getDataDir: getDataDir3 } = await Promise.resolve().then(() => (init_db(), exports_db));
48465
- const configPath = process.env.CONVERSATIONS_CONFIG_PATH ?? join12(getDataDir3(), "config.json");
48466
- if (existsSync5(configPath)) {
49624
+ const configPath = process.env.CONVERSATIONS_CONFIG_PATH ?? join8(getDataDir3(), "config.json");
49625
+ if (existsSync8(configPath)) {
48467
49626
  try {
48468
- const { readFileSync: readFileSync5 } = await import("fs");
48469
- JSON.parse(readFileSync5(configPath, "utf8"));
49627
+ const { readFileSync: readFileSync4 } = await import("fs");
49628
+ JSON.parse(readFileSync4(configPath, "utf8"));
48470
49629
  checks.push({ name: "Webhook config", ok: true, message: `OK \u2014 ${configPath}` });
48471
49630
  } catch (e) {
48472
49631
  checks.push({ name: "Webhook config", ok: false, message: `Invalid JSON at ${configPath}: ${e.message}` });