@hasna/conversations 0.2.24 → 0.2.26
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/hook.js +7 -0
- package/bin/index.js +1567 -62
- package/bin/mcp.js +1582 -37
- package/dashboard/dist/assets/index-Bw0wMcXE.js +186 -0
- package/dashboard/dist/assets/index-CF_GDtNp.css +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/index.js +30 -0
- package/dist/lib/messages.d.ts +2 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/tools/cloud.d.ts +7 -0
- package/dist/mcp/tools/cloud.test.d.ts +1 -0
- package/package.json +2 -2
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
|
-
)`,
|
|
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
|
|
12376
|
-
import { join as
|
|
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 =
|
|
12381
|
-
const oldDir =
|
|
12382
|
-
if (
|
|
12383
|
-
|
|
12384
|
-
for (const file of
|
|
12385
|
-
const oldPath =
|
|
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,
|
|
13360
|
+
copyFileSync2(oldPath, join7(newDir, file));
|
|
12388
13361
|
}
|
|
12389
13362
|
}
|
|
12390
13363
|
}
|
|
12391
|
-
|
|
13364
|
+
mkdirSync4(newDir, { recursive: true });
|
|
12392
13365
|
return newDir;
|
|
12393
13366
|
}
|
|
12394
13367
|
function getDbPath2() {
|
|
@@ -12396,19 +13369,20 @@ 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
|
|
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
|
-
|
|
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");
|
|
12409
13382
|
db.exec(`
|
|
12410
13383
|
CREATE TABLE IF NOT EXISTS messages (
|
|
12411
13384
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13385
|
+
uuid TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
|
|
12412
13386
|
session_id TEXT NOT NULL,
|
|
12413
13387
|
from_agent TEXT NOT NULL,
|
|
12414
13388
|
to_agent TEXT NOT NULL,
|
|
@@ -12428,6 +13402,7 @@ function getDb() {
|
|
|
12428
13402
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
|
|
12429
13403
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
|
|
12430
13404
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
|
|
13405
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid)");
|
|
12431
13406
|
db.exec(`
|
|
12432
13407
|
CREATE TABLE IF NOT EXISTS projects (
|
|
12433
13408
|
id TEXT PRIMARY KEY,
|
|
@@ -12575,6 +13550,11 @@ function getDb() {
|
|
|
12575
13550
|
db.exec("ALTER TABLE messages ADD COLUMN project_id TEXT");
|
|
12576
13551
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(project_id)");
|
|
12577
13552
|
}
|
|
13553
|
+
if (!colNames2.includes("uuid")) {
|
|
13554
|
+
db.exec("ALTER TABLE messages ADD COLUMN uuid TEXT");
|
|
13555
|
+
db.exec("UPDATE messages SET uuid = lower(hex(randomblob(16))) WHERE uuid IS NULL");
|
|
13556
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid)");
|
|
13557
|
+
}
|
|
12578
13558
|
const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
|
|
12579
13559
|
const presenceColNames = presenceCols.map((c) => c.name);
|
|
12580
13560
|
if (!presenceColNames.includes("id")) {
|
|
@@ -12680,8 +13660,8 @@ var init_db = __esm(() => {
|
|
|
12680
13660
|
});
|
|
12681
13661
|
|
|
12682
13662
|
// src/lib/identity.ts
|
|
12683
|
-
import { readFileSync as
|
|
12684
|
-
import { join as
|
|
13663
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync6 } from "fs";
|
|
13664
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
12685
13665
|
function isNameTaken(name) {
|
|
12686
13666
|
try {
|
|
12687
13667
|
const { getDb: getDb2 } = (init_db(), __toCommonJS(exports_db));
|
|
@@ -12696,7 +13676,7 @@ function getAutoName() {
|
|
|
12696
13676
|
if (cachedAutoName)
|
|
12697
13677
|
return cachedAutoName;
|
|
12698
13678
|
try {
|
|
12699
|
-
const name2 =
|
|
13679
|
+
const name2 = readFileSync3(AGENT_ID_FILE, "utf-8").trim();
|
|
12700
13680
|
if (name2) {
|
|
12701
13681
|
cachedAutoName = name2;
|
|
12702
13682
|
return name2;
|
|
@@ -12712,8 +13692,8 @@ function getAutoName() {
|
|
|
12712
13692
|
}
|
|
12713
13693
|
cachedAutoName = name;
|
|
12714
13694
|
try {
|
|
12715
|
-
|
|
12716
|
-
|
|
13695
|
+
mkdirSync6(dirname3(AGENT_ID_FILE), { recursive: true });
|
|
13696
|
+
writeFileSync3(AGENT_ID_FILE, name + `
|
|
12717
13697
|
`, "utf-8");
|
|
12718
13698
|
} catch {}
|
|
12719
13699
|
return name;
|
|
@@ -12730,8 +13710,8 @@ function resolveIdentity(explicit) {
|
|
|
12730
13710
|
function updateCachedAutoName(newName) {
|
|
12731
13711
|
cachedAutoName = newName;
|
|
12732
13712
|
try {
|
|
12733
|
-
|
|
12734
|
-
|
|
13713
|
+
mkdirSync6(dirname3(AGENT_ID_FILE), { recursive: true });
|
|
13714
|
+
writeFileSync3(AGENT_ID_FILE, newName + `
|
|
12735
13715
|
`, "utf-8");
|
|
12736
13716
|
} catch {}
|
|
12737
13717
|
}
|
|
@@ -12739,7 +13719,7 @@ var AGENT_ID_FILE, cachedAutoName = null;
|
|
|
12739
13719
|
var init_identity = __esm(() => {
|
|
12740
13720
|
init_names();
|
|
12741
13721
|
init_db();
|
|
12742
|
-
AGENT_ID_FILE =
|
|
13722
|
+
AGENT_ID_FILE = join9(getDataDir2(), "agent-id");
|
|
12743
13723
|
});
|
|
12744
13724
|
|
|
12745
13725
|
// src/lib/sessions.ts
|
|
@@ -12982,17 +13962,17 @@ var init_spaces = __esm(() => {
|
|
|
12982
13962
|
});
|
|
12983
13963
|
|
|
12984
13964
|
// src/lib/webhooks.ts
|
|
12985
|
-
import { readFileSync as
|
|
12986
|
-
import { join as
|
|
13965
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
13966
|
+
import { join as join10 } from "path";
|
|
12987
13967
|
function getConfigPath2() {
|
|
12988
|
-
return process.env.CONVERSATIONS_CONFIG_PATH ||
|
|
13968
|
+
return process.env.CONVERSATIONS_CONFIG_PATH || join10(getDataDir2(), "config.json");
|
|
12989
13969
|
}
|
|
12990
13970
|
function loadConfig() {
|
|
12991
13971
|
const now = Date.now();
|
|
12992
13972
|
if (cachedConfig && now - configLoadedAt < CONFIG_CACHE_MS)
|
|
12993
13973
|
return cachedConfig;
|
|
12994
13974
|
try {
|
|
12995
|
-
const raw =
|
|
13975
|
+
const raw = readFileSync5(getConfigPath2(), "utf-8");
|
|
12996
13976
|
cachedConfig = JSON.parse(raw);
|
|
12997
13977
|
configLoadedAt = now;
|
|
12998
13978
|
return cachedConfig;
|
|
@@ -13047,8 +14027,8 @@ var init_webhooks = __esm(() => {
|
|
|
13047
14027
|
|
|
13048
14028
|
// src/lib/messages.ts
|
|
13049
14029
|
import { randomUUID } from "crypto";
|
|
13050
|
-
import { mkdirSync as
|
|
13051
|
-
import { join as
|
|
14030
|
+
import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
|
|
14031
|
+
import { join as join11 } from "path";
|
|
13052
14032
|
function compactMessage(msg) {
|
|
13053
14033
|
const result = {};
|
|
13054
14034
|
for (const key of Object.keys(msg)) {
|
|
@@ -13087,7 +14067,7 @@ function parseMessage(row) {
|
|
|
13087
14067
|
function getAttachmentsDir() {
|
|
13088
14068
|
if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
|
|
13089
14069
|
return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
|
|
13090
|
-
return
|
|
14070
|
+
return join11(getDataDir2(), "attachments");
|
|
13091
14071
|
}
|
|
13092
14072
|
function guessMimeType(name) {
|
|
13093
14073
|
const ext = name.split(".").pop()?.toLowerCase();
|
|
@@ -13116,7 +14096,26 @@ function guessMimeType(name) {
|
|
|
13116
14096
|
};
|
|
13117
14097
|
return mimeMap[ext || ""] || "application/octet-stream";
|
|
13118
14098
|
}
|
|
14099
|
+
function checkRateLimit(agentId) {
|
|
14100
|
+
const dbPath = process.env.CONVERSATIONS_DB_PATH ?? process.env.HASNA_CONVERSATIONS_DB_PATH ?? "";
|
|
14101
|
+
if (dbPath === ":memory:" || dbPath.includes("test") || dbPath.includes("tmp"))
|
|
14102
|
+
return;
|
|
14103
|
+
const now = Date.now();
|
|
14104
|
+
const entry = _rateLimitCounters.get(agentId);
|
|
14105
|
+
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
|
14106
|
+
_rateLimitCounters.set(agentId, { count: 1, windowStart: now });
|
|
14107
|
+
return;
|
|
14108
|
+
}
|
|
14109
|
+
entry.count++;
|
|
14110
|
+
if (entry.count > RATE_LIMIT_MAX) {
|
|
14111
|
+
throw new Error(`Rate limit exceeded: ${agentId} may send at most ${RATE_LIMIT_MAX} messages per minute.`);
|
|
14112
|
+
}
|
|
14113
|
+
}
|
|
13119
14114
|
function sendMessage(opts) {
|
|
14115
|
+
if (Buffer.byteLength(opts.content, "utf8") > MAX_MESSAGE_BYTES) {
|
|
14116
|
+
throw new Error(`Message content exceeds maximum size of ${MAX_MESSAGE_BYTES} bytes (64 KB).`);
|
|
14117
|
+
}
|
|
14118
|
+
checkRateLimit(opts.from);
|
|
13120
14119
|
const db2 = getDb();
|
|
13121
14120
|
const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
|
|
13122
14121
|
const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`);
|
|
@@ -13132,11 +14131,11 @@ function sendMessage(opts) {
|
|
|
13132
14131
|
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
14132
|
const message = parseMessage(row);
|
|
13134
14133
|
if (opts.attachments && opts.attachments.length > 0) {
|
|
13135
|
-
const attachmentsDir =
|
|
13136
|
-
|
|
14134
|
+
const attachmentsDir = join11(getAttachmentsDir(), String(message.id));
|
|
14135
|
+
mkdirSync7(attachmentsDir, { recursive: true });
|
|
13137
14136
|
const attachmentInfos = [];
|
|
13138
14137
|
for (const att of opts.attachments) {
|
|
13139
|
-
const destPath =
|
|
14138
|
+
const destPath = join11(attachmentsDir, att.name);
|
|
13140
14139
|
copyFileSync3(att.source_path, destPath);
|
|
13141
14140
|
const stat = statSync2(destPath);
|
|
13142
14141
|
attachmentInfos.push({
|
|
@@ -13644,9 +14643,11 @@ function getMessageReadStatus(messageId, space) {
|
|
|
13644
14643
|
const unread_by = members.map((m) => m.agent).filter((a) => !readers.has(a));
|
|
13645
14644
|
return { receipts, unread_by };
|
|
13646
14645
|
}
|
|
14646
|
+
var MAX_MESSAGE_BYTES = 65536, RATE_LIMIT_MAX = 60, RATE_LIMIT_WINDOW_MS = 60000, _rateLimitCounters;
|
|
13647
14647
|
var init_messages = __esm(() => {
|
|
13648
14648
|
init_db();
|
|
13649
14649
|
init_webhooks();
|
|
14650
|
+
_rateLimitCounters = new Map;
|
|
13650
14651
|
});
|
|
13651
14652
|
|
|
13652
14653
|
// src/lib/poll.ts
|
|
@@ -13927,7 +14928,7 @@ var init_presence = __esm(() => {
|
|
|
13927
14928
|
var require_package = __commonJS((exports, module) => {
|
|
13928
14929
|
module.exports = {
|
|
13929
14930
|
name: "@hasna/conversations",
|
|
13930
|
-
version: "0.2.
|
|
14931
|
+
version: "0.2.26",
|
|
13931
14932
|
description: "Real-time CLI messaging for AI agents",
|
|
13932
14933
|
type: "module",
|
|
13933
14934
|
bin: {
|
|
@@ -13956,7 +14957,7 @@ var require_package = __commonJS((exports, module) => {
|
|
|
13956
14957
|
test: "bun test",
|
|
13957
14958
|
dev: "bun run ./src/cli/index.tsx",
|
|
13958
14959
|
typecheck: "tsc --noEmit",
|
|
13959
|
-
prepublishOnly: "bun run build",
|
|
14960
|
+
prepublishOnly: "bun run build:dashboard && bun run build",
|
|
13960
14961
|
postinstall: "mkdir -p $HOME/.hasna/conversations $HOME/.hasna/conversations/training 2>/dev/null || true"
|
|
13961
14962
|
},
|
|
13962
14963
|
keywords: [
|
|
@@ -45299,6 +46300,476 @@ var init_advanced = __esm(() => {
|
|
|
45299
46300
|
init_graph();
|
|
45300
46301
|
});
|
|
45301
46302
|
|
|
46303
|
+
// src/lib/pg-migrations.ts
|
|
46304
|
+
var exports_pg_migrations = {};
|
|
46305
|
+
__export(exports_pg_migrations, {
|
|
46306
|
+
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
46307
|
+
});
|
|
46308
|
+
var PG_MIGRATIONS;
|
|
46309
|
+
var init_pg_migrations = __esm(() => {
|
|
46310
|
+
PG_MIGRATIONS = [
|
|
46311
|
+
`
|
|
46312
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
46313
|
+
id TEXT PRIMARY KEY,
|
|
46314
|
+
name TEXT NOT NULL UNIQUE,
|
|
46315
|
+
description TEXT,
|
|
46316
|
+
path TEXT,
|
|
46317
|
+
created_by TEXT NOT NULL,
|
|
46318
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46319
|
+
metadata TEXT,
|
|
46320
|
+
tags TEXT,
|
|
46321
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
46322
|
+
repository TEXT,
|
|
46323
|
+
settings TEXT
|
|
46324
|
+
);
|
|
46325
|
+
CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
|
|
46326
|
+
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
|
|
46327
|
+
|
|
46328
|
+
CREATE TABLE IF NOT EXISTS spaces (
|
|
46329
|
+
name TEXT PRIMARY KEY,
|
|
46330
|
+
description TEXT,
|
|
46331
|
+
parent_id TEXT REFERENCES spaces(name),
|
|
46332
|
+
project_id TEXT REFERENCES projects(id),
|
|
46333
|
+
created_by TEXT NOT NULL,
|
|
46334
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46335
|
+
archived_at TEXT,
|
|
46336
|
+
topic TEXT
|
|
46337
|
+
);
|
|
46338
|
+
CREATE INDEX IF NOT EXISTS idx_spaces_parent ON spaces(parent_id);
|
|
46339
|
+
CREATE INDEX IF NOT EXISTS idx_spaces_project ON spaces(project_id);
|
|
46340
|
+
|
|
46341
|
+
CREATE TABLE IF NOT EXISTS space_members (
|
|
46342
|
+
space TEXT NOT NULL REFERENCES spaces(name),
|
|
46343
|
+
agent TEXT NOT NULL,
|
|
46344
|
+
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46345
|
+
PRIMARY KEY (space, agent)
|
|
46346
|
+
);
|
|
46347
|
+
|
|
46348
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
46349
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
46350
|
+
uuid TEXT NOT NULL DEFAULT gen_random_uuid()::text UNIQUE,
|
|
46351
|
+
session_id TEXT NOT NULL,
|
|
46352
|
+
from_agent TEXT NOT NULL,
|
|
46353
|
+
to_agent TEXT NOT NULL,
|
|
46354
|
+
space TEXT,
|
|
46355
|
+
project_id TEXT,
|
|
46356
|
+
content TEXT NOT NULL,
|
|
46357
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
46358
|
+
working_dir TEXT,
|
|
46359
|
+
repository TEXT,
|
|
46360
|
+
branch TEXT,
|
|
46361
|
+
metadata TEXT,
|
|
46362
|
+
edited_at TEXT,
|
|
46363
|
+
pinned_at TEXT,
|
|
46364
|
+
blocking BOOLEAN NOT NULL DEFAULT FALSE,
|
|
46365
|
+
attachments TEXT,
|
|
46366
|
+
reply_to BIGINT,
|
|
46367
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46368
|
+
read_at TEXT
|
|
46369
|
+
);
|
|
46370
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
46371
|
+
CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent);
|
|
46372
|
+
CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
|
|
46373
|
+
CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space);
|
|
46374
|
+
CREATE INDEX IF NOT EXISTS idx_messages_pinned ON messages(pinned_at);
|
|
46375
|
+
CREATE INDEX IF NOT EXISTS idx_messages_blocking ON messages(blocking);
|
|
46376
|
+
CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to);
|
|
46377
|
+
CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(project_id);
|
|
46378
|
+
|
|
46379
|
+
CREATE TABLE IF NOT EXISTS agent_presence (
|
|
46380
|
+
id TEXT NOT NULL DEFAULT '',
|
|
46381
|
+
agent TEXT PRIMARY KEY,
|
|
46382
|
+
session_id TEXT,
|
|
46383
|
+
role TEXT NOT NULL DEFAULT 'agent',
|
|
46384
|
+
project_id TEXT,
|
|
46385
|
+
status TEXT NOT NULL DEFAULT 'online',
|
|
46386
|
+
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46387
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46388
|
+
metadata TEXT
|
|
46389
|
+
);
|
|
46390
|
+
|
|
46391
|
+
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
46392
|
+
resource_type TEXT NOT NULL,
|
|
46393
|
+
resource_id TEXT NOT NULL,
|
|
46394
|
+
agent_id TEXT NOT NULL,
|
|
46395
|
+
lock_type TEXT NOT NULL DEFAULT 'advisory',
|
|
46396
|
+
locked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46397
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
46398
|
+
UNIQUE(resource_type, resource_id, lock_type)
|
|
46399
|
+
);
|
|
46400
|
+
CREATE INDEX IF NOT EXISTS idx_locks_resource ON resource_locks(resource_type, resource_id);
|
|
46401
|
+
CREATE INDEX IF NOT EXISTS idx_locks_agent ON resource_locks(agent_id);
|
|
46402
|
+
CREATE INDEX IF NOT EXISTS idx_locks_expires ON resource_locks(expires_at);
|
|
46403
|
+
|
|
46404
|
+
CREATE TABLE IF NOT EXISTS reactions (
|
|
46405
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
46406
|
+
message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
46407
|
+
agent TEXT NOT NULL,
|
|
46408
|
+
emoji TEXT NOT NULL,
|
|
46409
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46410
|
+
UNIQUE(message_id, agent, emoji)
|
|
46411
|
+
);
|
|
46412
|
+
CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id);
|
|
46413
|
+
|
|
46414
|
+
CREATE TABLE IF NOT EXISTS message_read_receipts (
|
|
46415
|
+
message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
46416
|
+
agent TEXT NOT NULL,
|
|
46417
|
+
read_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46418
|
+
PRIMARY KEY (message_id, agent)
|
|
46419
|
+
);
|
|
46420
|
+
CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id);
|
|
46421
|
+
CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent);
|
|
46422
|
+
|
|
46423
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
46424
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
46425
|
+
message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
46426
|
+
mentioned_agent TEXT NOT NULL,
|
|
46427
|
+
from_agent TEXT NOT NULL,
|
|
46428
|
+
space TEXT,
|
|
46429
|
+
notified_at TEXT,
|
|
46430
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
46431
|
+
);
|
|
46432
|
+
CREATE INDEX IF NOT EXISTS idx_mentions_agent ON message_mentions(mentioned_agent);
|
|
46433
|
+
CREATE INDEX IF NOT EXISTS idx_mentions_message ON message_mentions(message_id);
|
|
46434
|
+
CREATE INDEX IF NOT EXISTS idx_mentions_notified ON message_mentions(notified_at);
|
|
46435
|
+
|
|
46436
|
+
-- Full-text search using PostgreSQL tsvector
|
|
46437
|
+
ALTER TABLE messages ADD COLUMN IF NOT EXISTS search_vector tsvector;
|
|
46438
|
+
CREATE INDEX IF NOT EXISTS idx_messages_search ON messages USING GIN(search_vector);
|
|
46439
|
+
|
|
46440
|
+
CREATE OR REPLACE FUNCTION messages_search_vector_update() RETURNS trigger AS $$
|
|
46441
|
+
BEGIN
|
|
46442
|
+
NEW.search_vector :=
|
|
46443
|
+
setweight(to_tsvector('english', COALESCE(NEW.content, '')), 'A') ||
|
|
46444
|
+
setweight(to_tsvector('english', COALESCE(NEW.from_agent, '')), 'B') ||
|
|
46445
|
+
setweight(to_tsvector('english', COALESCE(NEW.to_agent, '')), 'B') ||
|
|
46446
|
+
setweight(to_tsvector('english', COALESCE(NEW.space, '')), 'C');
|
|
46447
|
+
RETURN NEW;
|
|
46448
|
+
END;
|
|
46449
|
+
$$ LANGUAGE plpgsql;
|
|
46450
|
+
|
|
46451
|
+
DROP TRIGGER IF EXISTS messages_search_vector_trigger ON messages;
|
|
46452
|
+
CREATE TRIGGER messages_search_vector_trigger
|
|
46453
|
+
BEFORE INSERT OR UPDATE OF content, from_agent, to_agent, space ON messages
|
|
46454
|
+
FOR EACH ROW EXECUTE FUNCTION messages_search_vector_update();
|
|
46455
|
+
|
|
46456
|
+
-- Backfill existing rows
|
|
46457
|
+
UPDATE messages SET search_vector =
|
|
46458
|
+
setweight(to_tsvector('english', COALESCE(content, '')), 'A') ||
|
|
46459
|
+
setweight(to_tsvector('english', COALESCE(from_agent, '')), 'B') ||
|
|
46460
|
+
setweight(to_tsvector('english', COALESCE(to_agent, '')), 'B') ||
|
|
46461
|
+
setweight(to_tsvector('english', COALESCE(space, '')), 'C')
|
|
46462
|
+
WHERE search_vector IS NULL;
|
|
46463
|
+
|
|
46464
|
+
-- Feedback table
|
|
46465
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
46466
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
46467
|
+
message TEXT NOT NULL,
|
|
46468
|
+
email TEXT,
|
|
46469
|
+
category TEXT DEFAULT 'general',
|
|
46470
|
+
version TEXT,
|
|
46471
|
+
machine_id TEXT,
|
|
46472
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
46473
|
+
);
|
|
46474
|
+
|
|
46475
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
46476
|
+
id INTEGER PRIMARY KEY,
|
|
46477
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
46478
|
+
);
|
|
46479
|
+
INSERT INTO _migrations (id) VALUES (1) ON CONFLICT DO NOTHING;
|
|
46480
|
+
`
|
|
46481
|
+
];
|
|
46482
|
+
});
|
|
46483
|
+
|
|
46484
|
+
// src/mcp/tools/cloud.ts
|
|
46485
|
+
async function detectAndLogConflicts(local, cloud, table) {
|
|
46486
|
+
if (!CONFLICT_TABLES.has(table))
|
|
46487
|
+
return 0;
|
|
46488
|
+
try {
|
|
46489
|
+
const { detectConflicts: detectConflicts2, storeConflicts: storeConflicts2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
46490
|
+
const pk = table === "messages" ? "id" : table === "spaces" ? "name" : table === "space_members" ? "space" : "id";
|
|
46491
|
+
const tsCol = "created_at";
|
|
46492
|
+
const localRows = local.all(`SELECT * FROM "${table}"`);
|
|
46493
|
+
const remoteRows = await cloud.all(`SELECT * FROM "${table}"`);
|
|
46494
|
+
if (localRows.length === 0 || remoteRows.length === 0)
|
|
46495
|
+
return 0;
|
|
46496
|
+
const conflicts = detectConflicts2(localRows, remoteRows, table, pk, tsCol);
|
|
46497
|
+
if (conflicts.length > 0) {
|
|
46498
|
+
storeConflicts2(local, conflicts);
|
|
46499
|
+
}
|
|
46500
|
+
return conflicts.length;
|
|
46501
|
+
} catch {
|
|
46502
|
+
return 0;
|
|
46503
|
+
}
|
|
46504
|
+
}
|
|
46505
|
+
function registerCloudSyncTools(server) {
|
|
46506
|
+
server.tool("conversations_cloud_status", "Show cloud configuration, connection health, and sync status", {}, async () => {
|
|
46507
|
+
try {
|
|
46508
|
+
const {
|
|
46509
|
+
getCloudConfig: getCloudConfig2,
|
|
46510
|
+
getConnectionString: getConnectionString2,
|
|
46511
|
+
PgAdapterAsync: PgAdapterAsync2,
|
|
46512
|
+
listConflicts: listConflicts2,
|
|
46513
|
+
ensureConflictsTable: ensureConflictsTable2,
|
|
46514
|
+
SqliteAdapter: SqliteAdapter2,
|
|
46515
|
+
getDbPath: cloudGetDbPath
|
|
46516
|
+
} = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
46517
|
+
const config2 = getCloudConfig2();
|
|
46518
|
+
const lines = [
|
|
46519
|
+
`Mode: ${config2.mode}`,
|
|
46520
|
+
`Service: conversations`,
|
|
46521
|
+
`RDS Host: ${config2.rds.host || "(not configured)"}`
|
|
46522
|
+
];
|
|
46523
|
+
if (config2.rds.host && config2.rds.username) {
|
|
46524
|
+
try {
|
|
46525
|
+
const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
|
|
46526
|
+
await pg.get("SELECT 1 as ok");
|
|
46527
|
+
lines.push("PostgreSQL: connected");
|
|
46528
|
+
await pg.close();
|
|
46529
|
+
} catch (err) {
|
|
46530
|
+
lines.push(`PostgreSQL: failed \u2014 ${err?.message}`);
|
|
46531
|
+
}
|
|
46532
|
+
}
|
|
46533
|
+
try {
|
|
46534
|
+
const local = new SqliteAdapter2(cloudGetDbPath("conversations"));
|
|
46535
|
+
ensureConflictsTable2(local);
|
|
46536
|
+
const unresolved = listConflicts2(local, { resolved: false });
|
|
46537
|
+
const resolved = listConflicts2(local, { resolved: true });
|
|
46538
|
+
lines.push(`Sync conflicts: ${unresolved.length} unresolved, ${resolved.length} resolved`);
|
|
46539
|
+
local.close();
|
|
46540
|
+
} catch {}
|
|
46541
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
46542
|
+
`) }] };
|
|
46543
|
+
} catch (e) {
|
|
46544
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
46545
|
+
}
|
|
46546
|
+
});
|
|
46547
|
+
server.tool("conversations_cloud_push", "Push local conversations data to cloud PostgreSQL. Detects conflicts and syncs all tables.", {
|
|
46548
|
+
tables: exports_external2.string().optional().describe("Comma-separated table names (default: all)")
|
|
46549
|
+
}, async ({ tables: tablesStr }) => {
|
|
46550
|
+
try {
|
|
46551
|
+
const {
|
|
46552
|
+
getCloudConfig: getCloudConfig2,
|
|
46553
|
+
getConnectionString: getConnectionString2,
|
|
46554
|
+
syncPush: syncPush2,
|
|
46555
|
+
listSqliteTables: listSqliteTables2,
|
|
46556
|
+
SqliteAdapter: SqliteAdapter2,
|
|
46557
|
+
PgAdapterAsync: PgAdapterAsync2,
|
|
46558
|
+
getDbPath: cloudGetDbPath
|
|
46559
|
+
} = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
46560
|
+
const config2 = getCloudConfig2();
|
|
46561
|
+
if (config2.mode === "local") {
|
|
46562
|
+
return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
|
|
46563
|
+
}
|
|
46564
|
+
const localPath = cloudGetDbPath("conversations");
|
|
46565
|
+
const local = new SqliteAdapter2(localPath);
|
|
46566
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
46567
|
+
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !SYNC_EXCLUDED.has(t));
|
|
46568
|
+
let totalConflicts = 0;
|
|
46569
|
+
for (const table of tableList) {
|
|
46570
|
+
totalConflicts += await detectAndLogConflicts(local, cloud, table);
|
|
46571
|
+
}
|
|
46572
|
+
const results = await syncPush2(local, cloud, { tables: tableList });
|
|
46573
|
+
local.close();
|
|
46574
|
+
await cloud.close();
|
|
46575
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
46576
|
+
const errors4 = results.flatMap((r) => r.errors);
|
|
46577
|
+
const lines = [`Pushed ${total} rows across ${tableList.length} table(s).`];
|
|
46578
|
+
if (totalConflicts > 0)
|
|
46579
|
+
lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
|
|
46580
|
+
if (errors4.length > 0)
|
|
46581
|
+
lines.push(`Errors: ${errors4.join("; ")}`);
|
|
46582
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
46583
|
+
`) }] };
|
|
46584
|
+
} catch (e) {
|
|
46585
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
46586
|
+
}
|
|
46587
|
+
});
|
|
46588
|
+
server.tool("conversations_cloud_pull", "Pull cloud PostgreSQL data to local. Detects conflicts and merges by primary key with UPSERT.", {
|
|
46589
|
+
tables: exports_external2.string().optional().describe("Comma-separated table names (default: all)")
|
|
46590
|
+
}, async ({ tables: tablesStr }) => {
|
|
46591
|
+
try {
|
|
46592
|
+
const {
|
|
46593
|
+
getCloudConfig: getCloudConfig2,
|
|
46594
|
+
getConnectionString: getConnectionString2,
|
|
46595
|
+
syncPull: syncPull2,
|
|
46596
|
+
listPgTables: listPgTables2,
|
|
46597
|
+
SqliteAdapter: SqliteAdapter2,
|
|
46598
|
+
PgAdapterAsync: PgAdapterAsync2,
|
|
46599
|
+
getDbPath: cloudGetDbPath
|
|
46600
|
+
} = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
46601
|
+
const config2 = getCloudConfig2();
|
|
46602
|
+
if (config2.mode === "local") {
|
|
46603
|
+
return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
|
|
46604
|
+
}
|
|
46605
|
+
const local = new SqliteAdapter2(cloudGetDbPath("conversations"));
|
|
46606
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
46607
|
+
let tableList;
|
|
46608
|
+
if (tablesStr) {
|
|
46609
|
+
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
46610
|
+
} else {
|
|
46611
|
+
try {
|
|
46612
|
+
tableList = (await listPgTables2(cloud)).filter((t) => !SYNC_EXCLUDED.has(t));
|
|
46613
|
+
} catch {
|
|
46614
|
+
local.close();
|
|
46615
|
+
await cloud.close();
|
|
46616
|
+
return { content: [{ type: "text", text: "Error: failed to list cloud tables." }], isError: true };
|
|
46617
|
+
}
|
|
46618
|
+
}
|
|
46619
|
+
let totalConflicts = 0;
|
|
46620
|
+
for (const table of tableList) {
|
|
46621
|
+
totalConflicts += await detectAndLogConflicts(local, cloud, table);
|
|
46622
|
+
}
|
|
46623
|
+
const results = await syncPull2(cloud, local, { tables: tableList });
|
|
46624
|
+
local.close();
|
|
46625
|
+
await cloud.close();
|
|
46626
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
46627
|
+
const errors4 = results.flatMap((r) => r.errors);
|
|
46628
|
+
const lines = [`Pulled ${total} rows across ${tableList.length} table(s).`];
|
|
46629
|
+
if (totalConflicts > 0)
|
|
46630
|
+
lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
|
|
46631
|
+
if (errors4.length > 0)
|
|
46632
|
+
lines.push(`Errors: ${errors4.join("; ")}`);
|
|
46633
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
46634
|
+
`) }] };
|
|
46635
|
+
} catch (e) {
|
|
46636
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
46637
|
+
}
|
|
46638
|
+
});
|
|
46639
|
+
server.tool("conversations_cloud_sync", "Bidirectional cloud sync \u2014 pull remote changes then push local changes. Detects and logs conflicts.", {
|
|
46640
|
+
tables: exports_external2.string().optional().describe("Comma-separated table names (default: all syncable tables)")
|
|
46641
|
+
}, async ({ tables: tablesStr }) => {
|
|
46642
|
+
try {
|
|
46643
|
+
const {
|
|
46644
|
+
getCloudConfig: getCloudConfig2,
|
|
46645
|
+
getConnectionString: getConnectionString2,
|
|
46646
|
+
syncPush: syncPush2,
|
|
46647
|
+
syncPull: syncPull2,
|
|
46648
|
+
listSqliteTables: listSqliteTables2,
|
|
46649
|
+
listPgTables: listPgTables2,
|
|
46650
|
+
SqliteAdapter: SqliteAdapter2,
|
|
46651
|
+
PgAdapterAsync: PgAdapterAsync2,
|
|
46652
|
+
getDbPath: cloudGetDbPath
|
|
46653
|
+
} = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
46654
|
+
const config2 = getCloudConfig2();
|
|
46655
|
+
if (config2.mode === "local") {
|
|
46656
|
+
return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
|
|
46657
|
+
}
|
|
46658
|
+
const local = new SqliteAdapter2(cloudGetDbPath("conversations"));
|
|
46659
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
46660
|
+
let tableList;
|
|
46661
|
+
if (tablesStr) {
|
|
46662
|
+
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
46663
|
+
} else {
|
|
46664
|
+
const localTables = new Set(listSqliteTables2(local).filter((t) => !SYNC_EXCLUDED.has(t)));
|
|
46665
|
+
let remoteTables;
|
|
46666
|
+
try {
|
|
46667
|
+
remoteTables = new Set((await listPgTables2(cloud)).filter((t) => !SYNC_EXCLUDED.has(t)));
|
|
46668
|
+
} catch {
|
|
46669
|
+
local.close();
|
|
46670
|
+
await cloud.close();
|
|
46671
|
+
return { content: [{ type: "text", text: "Error: failed to list cloud tables." }], isError: true };
|
|
46672
|
+
}
|
|
46673
|
+
tableList = [...new Set([...localTables, ...remoteTables])];
|
|
46674
|
+
}
|
|
46675
|
+
let totalConflicts = 0;
|
|
46676
|
+
for (const table of tableList) {
|
|
46677
|
+
totalConflicts += await detectAndLogConflicts(local, cloud, table);
|
|
46678
|
+
}
|
|
46679
|
+
const pullResults = await syncPull2(cloud, local, { tables: tableList });
|
|
46680
|
+
const pullTotal = pullResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
46681
|
+
const pushResults = await syncPush2(local, cloud, { tables: tableList });
|
|
46682
|
+
const pushTotal = pushResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
46683
|
+
local.close();
|
|
46684
|
+
await cloud.close();
|
|
46685
|
+
const allErrors = [
|
|
46686
|
+
...pullResults.flatMap((r) => r.errors.map((e) => `pull: ${e}`)),
|
|
46687
|
+
...pushResults.flatMap((r) => r.errors.map((e) => `push: ${e}`))
|
|
46688
|
+
];
|
|
46689
|
+
const lines = [
|
|
46690
|
+
`Sync complete: pulled ${pullTotal} rows, pushed ${pushTotal} rows across ${tableList.length} table(s).`
|
|
46691
|
+
];
|
|
46692
|
+
if (totalConflicts > 0)
|
|
46693
|
+
lines.push(`Conflicts detected: ${totalConflicts} (logged to _sync_conflicts)`);
|
|
46694
|
+
if (allErrors.length > 0)
|
|
46695
|
+
lines.push(`Errors: ${allErrors.join("; ")}`);
|
|
46696
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
46697
|
+
`) }] };
|
|
46698
|
+
} catch (e) {
|
|
46699
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
46700
|
+
}
|
|
46701
|
+
});
|
|
46702
|
+
server.tool("conversations_cloud_migrate", "Run PostgreSQL migrations against the configured RDS instance to initialize the cloud schema", {
|
|
46703
|
+
dry_run: exports_external2.boolean().optional().describe("Print SQL without executing")
|
|
46704
|
+
}, async ({ dry_run }) => {
|
|
46705
|
+
try {
|
|
46706
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, PgAdapterAsync: PgAdapterAsync2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
46707
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
46708
|
+
const config2 = getCloudConfig2();
|
|
46709
|
+
if (config2.mode === "local") {
|
|
46710
|
+
return { content: [{ type: "text", text: "Error: cloud mode not configured." }], isError: true };
|
|
46711
|
+
}
|
|
46712
|
+
if (dry_run) {
|
|
46713
|
+
return { content: [{ type: "text", text: PG_MIGRATIONS2.join(`
|
|
46714
|
+
|
|
46715
|
+
---
|
|
46716
|
+
|
|
46717
|
+
`) }] };
|
|
46718
|
+
}
|
|
46719
|
+
const pg = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
46720
|
+
const lines = [];
|
|
46721
|
+
for (let i = 0;i < PG_MIGRATIONS2.length; i++) {
|
|
46722
|
+
await pg.run(PG_MIGRATIONS2[i]);
|
|
46723
|
+
lines.push(`Migration ${i + 1}/${PG_MIGRATIONS2.length}: applied`);
|
|
46724
|
+
}
|
|
46725
|
+
await pg.close();
|
|
46726
|
+
lines.push("All migrations applied successfully.");
|
|
46727
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
46728
|
+
`) }] };
|
|
46729
|
+
} catch (e) {
|
|
46730
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
46731
|
+
}
|
|
46732
|
+
});
|
|
46733
|
+
server.tool("conversations_cloud_feedback", "Send feedback for the conversations service", {
|
|
46734
|
+
message: exports_external2.string().describe("Feedback message"),
|
|
46735
|
+
email: exports_external2.string().optional().describe("Contact email")
|
|
46736
|
+
}, async ({ message, email: email3 }) => {
|
|
46737
|
+
try {
|
|
46738
|
+
const { sendFeedback: sendFeedback2, createDatabase: createDatabase2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
46739
|
+
const db2 = createDatabase2({ service: "cloud" });
|
|
46740
|
+
const result = await sendFeedback2({ service: "conversations", message, email: email3 }, db2);
|
|
46741
|
+
db2.close();
|
|
46742
|
+
return {
|
|
46743
|
+
content: [{
|
|
46744
|
+
type: "text",
|
|
46745
|
+
text: result.sent ? `Feedback sent (id: ${result.id})` : `Saved locally (id: ${result.id}): ${result.error}`
|
|
46746
|
+
}]
|
|
46747
|
+
};
|
|
46748
|
+
} catch (e) {
|
|
46749
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
46750
|
+
}
|
|
46751
|
+
});
|
|
46752
|
+
}
|
|
46753
|
+
function formatError2(e) {
|
|
46754
|
+
if (e instanceof Error)
|
|
46755
|
+
return e.message;
|
|
46756
|
+
return String(e);
|
|
46757
|
+
}
|
|
46758
|
+
var SYNC_EXCLUDED, CONFLICT_TABLES;
|
|
46759
|
+
var init_cloud = __esm(() => {
|
|
46760
|
+
init_zod2();
|
|
46761
|
+
SYNC_EXCLUDED = new Set([
|
|
46762
|
+
"messages",
|
|
46763
|
+
"reactions",
|
|
46764
|
+
"message_read_receipts",
|
|
46765
|
+
"message_mentions",
|
|
46766
|
+
"messages_fts",
|
|
46767
|
+
"_sync_conflicts",
|
|
46768
|
+
"_migrations"
|
|
46769
|
+
]);
|
|
46770
|
+
CONFLICT_TABLES = new Set(["spaces", "projects", "agent_presence"]);
|
|
46771
|
+
});
|
|
46772
|
+
|
|
45302
46773
|
// src/mcp/index.ts
|
|
45303
46774
|
var exports_mcp = {};
|
|
45304
46775
|
__export(exports_mcp, {
|
|
@@ -45319,20 +46790,20 @@ function resolveProjectId(explicitProjectId, agentId) {
|
|
|
45319
46790
|
}
|
|
45320
46791
|
async function startMcpServer() {
|
|
45321
46792
|
const transport = new StdioServerTransport;
|
|
45322
|
-
|
|
46793
|
+
registerCloudSyncTools(server);
|
|
45323
46794
|
await server.connect(transport);
|
|
45324
46795
|
}
|
|
45325
46796
|
var import__package2, server, agentFocus, isDirectRun;
|
|
45326
46797
|
var init_mcp2 = __esm(() => {
|
|
45327
46798
|
init_mcp();
|
|
45328
46799
|
init_stdio2();
|
|
45329
|
-
init_dist();
|
|
45330
46800
|
init_presence();
|
|
45331
46801
|
init_messaging();
|
|
45332
46802
|
init_spaces2();
|
|
45333
46803
|
init_projects2();
|
|
45334
46804
|
init_agents();
|
|
45335
46805
|
init_advanced();
|
|
46806
|
+
init_cloud();
|
|
45336
46807
|
import__package2 = __toESM(require_package(), 1);
|
|
45337
46808
|
server = new McpServer({
|
|
45338
46809
|
name: "conversations",
|
|
@@ -45358,8 +46829,8 @@ var exports_serve = {};
|
|
|
45358
46829
|
__export(exports_serve, {
|
|
45359
46830
|
startDashboardServer: () => startDashboardServer
|
|
45360
46831
|
});
|
|
45361
|
-
import { join as
|
|
45362
|
-
import { existsSync as
|
|
46832
|
+
import { join as join14, resolve, sep } from "path";
|
|
46833
|
+
import { existsSync as existsSync10 } from "fs";
|
|
45363
46834
|
function securityHeaders(base) {
|
|
45364
46835
|
const headers = new Headers(base);
|
|
45365
46836
|
if (!headers.has("X-Content-Type-Options"))
|
|
@@ -45456,8 +46927,8 @@ function isSameOrigin(req) {
|
|
|
45456
46927
|
function startDashboardServer(port = 0, host) {
|
|
45457
46928
|
const resolvedPort = normalizePort(port, 0);
|
|
45458
46929
|
const resolvedHost = normalizeHost(host ?? process.env.CONVERSATIONS_DASHBOARD_HOST);
|
|
45459
|
-
const dashboardDist =
|
|
45460
|
-
const hasDist =
|
|
46930
|
+
const dashboardDist = join14(import.meta.dir, "../../dashboard/dist");
|
|
46931
|
+
const hasDist = existsSync10(dashboardDist);
|
|
45461
46932
|
const server2 = Bun.serve({
|
|
45462
46933
|
port: resolvedPort,
|
|
45463
46934
|
hostname: resolvedHost,
|
|
@@ -45878,7 +47349,7 @@ function startDashboardServer(port = 0, host) {
|
|
|
45878
47349
|
headers.set("Content-Type", file2.type);
|
|
45879
47350
|
return new Response(file2, { headers });
|
|
45880
47351
|
}
|
|
45881
|
-
file2 = Bun.file(
|
|
47352
|
+
file2 = Bun.file(join14(dashboardDist, "index.html"));
|
|
45882
47353
|
if (await file2.exists()) {
|
|
45883
47354
|
const headers = securityHeaders();
|
|
45884
47355
|
if (file2.type)
|
|
@@ -46871,8 +48342,8 @@ function App({ agent }) {
|
|
|
46871
48342
|
|
|
46872
48343
|
// src/cli/brains.ts
|
|
46873
48344
|
import chalk2 from "chalk";
|
|
46874
|
-
import { mkdirSync as
|
|
46875
|
-
import { join as
|
|
48345
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
48346
|
+
import { join as join13 } from "path";
|
|
46876
48347
|
import { spawnSync } from "child_process";
|
|
46877
48348
|
|
|
46878
48349
|
// src/lib/gatherer.ts
|
|
@@ -46936,25 +48407,25 @@ var gatherTrainingData = async (options = {}) => {
|
|
|
46936
48407
|
|
|
46937
48408
|
// src/lib/model-config.ts
|
|
46938
48409
|
init_db();
|
|
46939
|
-
import { readFileSync as
|
|
46940
|
-
import { join as
|
|
48410
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync8, existsSync as existsSync9 } from "fs";
|
|
48411
|
+
import { join as join12 } from "path";
|
|
46941
48412
|
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
46942
48413
|
var CONFIG_DIR3 = getDataDir2();
|
|
46943
|
-
var CONFIG_PATH2 =
|
|
48414
|
+
var CONFIG_PATH2 = join12(CONFIG_DIR3, "config.json");
|
|
46944
48415
|
function readConfig() {
|
|
46945
|
-
if (!
|
|
48416
|
+
if (!existsSync9(CONFIG_PATH2))
|
|
46946
48417
|
return {};
|
|
46947
48418
|
try {
|
|
46948
|
-
return JSON.parse(
|
|
48419
|
+
return JSON.parse(readFileSync6(CONFIG_PATH2, "utf-8"));
|
|
46949
48420
|
} catch {
|
|
46950
48421
|
return {};
|
|
46951
48422
|
}
|
|
46952
48423
|
}
|
|
46953
48424
|
function writeConfig(config) {
|
|
46954
|
-
if (!
|
|
46955
|
-
|
|
48425
|
+
if (!existsSync9(CONFIG_DIR3)) {
|
|
48426
|
+
mkdirSync8(CONFIG_DIR3, { recursive: true });
|
|
46956
48427
|
}
|
|
46957
|
-
|
|
48428
|
+
writeFileSync5(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
46958
48429
|
}
|
|
46959
48430
|
function getActiveModel() {
|
|
46960
48431
|
const config = readConfig();
|
|
@@ -46979,13 +48450,13 @@ function registerBrainsCommand(program2) {
|
|
|
46979
48450
|
try {
|
|
46980
48451
|
const since = opts.since ? new Date(opts.since) : undefined;
|
|
46981
48452
|
const result = await gatherTrainingData({ limit: opts.limit, since });
|
|
46982
|
-
const outputDir =
|
|
46983
|
-
|
|
48453
|
+
const outputDir = join13(getDataDir2(), "training");
|
|
48454
|
+
mkdirSync9(outputDir, { recursive: true });
|
|
46984
48455
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
46985
|
-
const outputPath = opts.output ??
|
|
48456
|
+
const outputPath = opts.output ?? join13(outputDir, `training-${timestamp}.jsonl`);
|
|
46986
48457
|
const jsonl = result.examples.map((ex) => JSON.stringify(ex)).join(`
|
|
46987
48458
|
`);
|
|
46988
|
-
|
|
48459
|
+
writeFileSync6(outputPath, jsonl, "utf-8");
|
|
46989
48460
|
if (opts.json) {
|
|
46990
48461
|
console.log(JSON.stringify({ path: outputPath, count: result.count, source: result.source }));
|
|
46991
48462
|
} else {
|
|
@@ -48153,10 +49624,10 @@ function registerAgentCommands(program2) {
|
|
|
48153
49624
|
} else if (envValue) {
|
|
48154
49625
|
source = "env var (CONVERSATIONS_AGENT_ID)";
|
|
48155
49626
|
} else {
|
|
48156
|
-
const { join:
|
|
49627
|
+
const { join: join8 } = __require("path");
|
|
48157
49628
|
const { homedir: homedir7 } = __require("os");
|
|
48158
49629
|
const { getDataDir: getDataDir3 } = (init_db(), __toCommonJS(exports_db));
|
|
48159
|
-
const agentIdFile =
|
|
49630
|
+
const agentIdFile = join8(getDataDir3(), "agent-id");
|
|
48160
49631
|
source = `auto-generated (${agentIdFile})`;
|
|
48161
49632
|
}
|
|
48162
49633
|
const presence = getPresence(agent);
|
|
@@ -48459,14 +49930,14 @@ function registerAnalyticsCommands(program2) {
|
|
|
48459
49930
|
checks.push({ name: "npm version", ok: true, message: "Could not check npm registry (offline?)" });
|
|
48460
49931
|
}
|
|
48461
49932
|
const { homedir: homedir7 } = await import("os");
|
|
48462
|
-
const { existsSync:
|
|
48463
|
-
const { join:
|
|
49933
|
+
const { existsSync: existsSync8 } = await import("fs");
|
|
49934
|
+
const { join: join8 } = await import("path");
|
|
48464
49935
|
const { getDataDir: getDataDir3 } = await Promise.resolve().then(() => (init_db(), exports_db));
|
|
48465
|
-
const configPath = process.env.CONVERSATIONS_CONFIG_PATH ??
|
|
48466
|
-
if (
|
|
49936
|
+
const configPath = process.env.CONVERSATIONS_CONFIG_PATH ?? join8(getDataDir3(), "config.json");
|
|
49937
|
+
if (existsSync8(configPath)) {
|
|
48467
49938
|
try {
|
|
48468
|
-
const { readFileSync:
|
|
48469
|
-
JSON.parse(
|
|
49939
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
49940
|
+
JSON.parse(readFileSync4(configPath, "utf8"));
|
|
48470
49941
|
checks.push({ name: "Webhook config", ok: true, message: `OK \u2014 ${configPath}` });
|
|
48471
49942
|
} catch (e) {
|
|
48472
49943
|
checks.push({ name: "Webhook config", ok: false, message: `Invalid JSON at ${configPath}: ${e.message}` });
|
|
@@ -48560,6 +50031,40 @@ program2.command("dashboard").description("Start web dashboard").option("--port
|
|
|
48560
50031
|
});
|
|
48561
50032
|
registerBrainsCommand(program2);
|
|
48562
50033
|
registerCloudCommands(program2, "conversations");
|
|
50034
|
+
{
|
|
50035
|
+
const cloudCmd = program2.commands.find((c) => c.name() === "cloud");
|
|
50036
|
+
if (cloudCmd) {
|
|
50037
|
+
cloudCmd.command("migrate").description("Run PostgreSQL migrations against the configured RDS instance").option("--dry-run", "Print SQL without executing").action(async (opts) => {
|
|
50038
|
+
try {
|
|
50039
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, PgAdapterAsync: PgAdapterAsync2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
50040
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
50041
|
+
const config2 = getCloudConfig2();
|
|
50042
|
+
if (config2.mode === "local") {
|
|
50043
|
+
console.error(chalk9.red("Error: cloud mode not configured. Set RDS credentials first."));
|
|
50044
|
+
process.exit(1);
|
|
50045
|
+
}
|
|
50046
|
+
if (opts.dryRun) {
|
|
50047
|
+
console.log(chalk9.dim(`-- Dry run: SQL that would be executed --
|
|
50048
|
+
`));
|
|
50049
|
+
for (const sql of PG_MIGRATIONS2)
|
|
50050
|
+
console.log(sql);
|
|
50051
|
+
return;
|
|
50052
|
+
}
|
|
50053
|
+
const pg = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
50054
|
+
for (let i = 0;i < PG_MIGRATIONS2.length; i++) {
|
|
50055
|
+
process.stdout.write(chalk9.dim(`Running migration ${i + 1}/${PG_MIGRATIONS2.length}...`));
|
|
50056
|
+
await pg.run(PG_MIGRATIONS2[i]);
|
|
50057
|
+
console.log(chalk9.green(" done"));
|
|
50058
|
+
}
|
|
50059
|
+
await pg.close();
|
|
50060
|
+
console.log(chalk9.green("\u2713 All migrations applied."));
|
|
50061
|
+
} catch (e) {
|
|
50062
|
+
console.error(chalk9.red(`Migration failed: ${e?.message ?? e}`));
|
|
50063
|
+
process.exit(1);
|
|
50064
|
+
}
|
|
50065
|
+
});
|
|
50066
|
+
}
|
|
50067
|
+
}
|
|
48563
50068
|
program2.action(() => {
|
|
48564
50069
|
if (!process.stdin.isTTY) {
|
|
48565
50070
|
console.error(chalk9.red("Interactive mode requires a TTY terminal."));
|