@hasna/conversations 0.2.25 → 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 +352 -6
- package/bin/mcp.js +319 -5
- 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.test.d.ts +1 -0
- package/package.json +2 -2
package/bin/hook.js
CHANGED
|
@@ -9467,6 +9467,7 @@ function getDb() {
|
|
|
9467
9467
|
db.exec(`
|
|
9468
9468
|
CREATE TABLE IF NOT EXISTS messages (
|
|
9469
9469
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9470
|
+
uuid TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
|
|
9470
9471
|
session_id TEXT NOT NULL,
|
|
9471
9472
|
from_agent TEXT NOT NULL,
|
|
9472
9473
|
to_agent TEXT NOT NULL,
|
|
@@ -9486,6 +9487,7 @@ function getDb() {
|
|
|
9486
9487
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
|
|
9487
9488
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
|
|
9488
9489
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
|
|
9490
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid)");
|
|
9489
9491
|
db.exec(`
|
|
9490
9492
|
CREATE TABLE IF NOT EXISTS projects (
|
|
9491
9493
|
id TEXT PRIMARY KEY,
|
|
@@ -9633,6 +9635,11 @@ function getDb() {
|
|
|
9633
9635
|
db.exec("ALTER TABLE messages ADD COLUMN project_id TEXT");
|
|
9634
9636
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(project_id)");
|
|
9635
9637
|
}
|
|
9638
|
+
if (!colNames2.includes("uuid")) {
|
|
9639
|
+
db.exec("ALTER TABLE messages ADD COLUMN uuid TEXT");
|
|
9640
|
+
db.exec("UPDATE messages SET uuid = lower(hex(randomblob(16))) WHERE uuid IS NULL");
|
|
9641
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid)");
|
|
9642
|
+
}
|
|
9636
9643
|
const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
|
|
9637
9644
|
const presenceColNames = presenceCols.map((c) => c.name);
|
|
9638
9645
|
if (!presenceColNames.includes("id")) {
|
package/bin/index.js
CHANGED
|
@@ -13382,6 +13382,7 @@ function getDb() {
|
|
|
13382
13382
|
db.exec(`
|
|
13383
13383
|
CREATE TABLE IF NOT EXISTS messages (
|
|
13384
13384
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13385
|
+
uuid TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
|
|
13385
13386
|
session_id TEXT NOT NULL,
|
|
13386
13387
|
from_agent TEXT NOT NULL,
|
|
13387
13388
|
to_agent TEXT NOT NULL,
|
|
@@ -13401,6 +13402,7 @@ function getDb() {
|
|
|
13401
13402
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
|
|
13402
13403
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
|
|
13403
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)");
|
|
13404
13406
|
db.exec(`
|
|
13405
13407
|
CREATE TABLE IF NOT EXISTS projects (
|
|
13406
13408
|
id TEXT PRIMARY KEY,
|
|
@@ -13548,6 +13550,11 @@ function getDb() {
|
|
|
13548
13550
|
db.exec("ALTER TABLE messages ADD COLUMN project_id TEXT");
|
|
13549
13551
|
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_project ON messages(project_id)");
|
|
13550
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
|
+
}
|
|
13551
13558
|
const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
|
|
13552
13559
|
const presenceColNames = presenceCols.map((c) => c.name);
|
|
13553
13560
|
if (!presenceColNames.includes("id")) {
|
|
@@ -14089,7 +14096,26 @@ function guessMimeType(name) {
|
|
|
14089
14096
|
};
|
|
14090
14097
|
return mimeMap[ext || ""] || "application/octet-stream";
|
|
14091
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
|
+
}
|
|
14092
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);
|
|
14093
14119
|
const db2 = getDb();
|
|
14094
14120
|
const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
|
|
14095
14121
|
const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`);
|
|
@@ -14617,9 +14643,11 @@ function getMessageReadStatus(messageId, space) {
|
|
|
14617
14643
|
const unread_by = members.map((m) => m.agent).filter((a) => !readers.has(a));
|
|
14618
14644
|
return { receipts, unread_by };
|
|
14619
14645
|
}
|
|
14646
|
+
var MAX_MESSAGE_BYTES = 65536, RATE_LIMIT_MAX = 60, RATE_LIMIT_WINDOW_MS = 60000, _rateLimitCounters;
|
|
14620
14647
|
var init_messages = __esm(() => {
|
|
14621
14648
|
init_db();
|
|
14622
14649
|
init_webhooks();
|
|
14650
|
+
_rateLimitCounters = new Map;
|
|
14623
14651
|
});
|
|
14624
14652
|
|
|
14625
14653
|
// src/lib/poll.ts
|
|
@@ -14900,7 +14928,7 @@ var init_presence = __esm(() => {
|
|
|
14900
14928
|
var require_package = __commonJS((exports, module) => {
|
|
14901
14929
|
module.exports = {
|
|
14902
14930
|
name: "@hasna/conversations",
|
|
14903
|
-
version: "0.2.
|
|
14931
|
+
version: "0.2.26",
|
|
14904
14932
|
description: "Real-time CLI messaging for AI agents",
|
|
14905
14933
|
type: "module",
|
|
14906
14934
|
bin: {
|
|
@@ -14929,7 +14957,7 @@ var require_package = __commonJS((exports, module) => {
|
|
|
14929
14957
|
test: "bun test",
|
|
14930
14958
|
dev: "bun run ./src/cli/index.tsx",
|
|
14931
14959
|
typecheck: "tsc --noEmit",
|
|
14932
|
-
prepublishOnly: "bun run build",
|
|
14960
|
+
prepublishOnly: "bun run build:dashboard && bun run build",
|
|
14933
14961
|
postinstall: "mkdir -p $HOME/.hasna/conversations $HOME/.hasna/conversations/training 2>/dev/null || true"
|
|
14934
14962
|
},
|
|
14935
14963
|
keywords: [
|
|
@@ -46272,6 +46300,187 @@ var init_advanced = __esm(() => {
|
|
|
46272
46300
|
init_graph();
|
|
46273
46301
|
});
|
|
46274
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
|
+
|
|
46275
46484
|
// src/mcp/tools/cloud.ts
|
|
46276
46485
|
async function detectAndLogConflicts(local, cloud, table) {
|
|
46277
46486
|
if (!CONFLICT_TABLES.has(table))
|
|
@@ -46355,7 +46564,7 @@ function registerCloudSyncTools(server) {
|
|
|
46355
46564
|
const localPath = cloudGetDbPath("conversations");
|
|
46356
46565
|
const local = new SqliteAdapter2(localPath);
|
|
46357
46566
|
const cloud = new PgAdapterAsync2(getConnectionString2("conversations"));
|
|
46358
|
-
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !
|
|
46567
|
+
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !SYNC_EXCLUDED.has(t));
|
|
46359
46568
|
let totalConflicts = 0;
|
|
46360
46569
|
for (const table of tableList) {
|
|
46361
46570
|
totalConflicts += await detectAndLogConflicts(local, cloud, table);
|
|
@@ -46400,7 +46609,7 @@ function registerCloudSyncTools(server) {
|
|
|
46400
46609
|
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
46401
46610
|
} else {
|
|
46402
46611
|
try {
|
|
46403
|
-
tableList = (await listPgTables2(cloud)).filter((t) => !
|
|
46612
|
+
tableList = (await listPgTables2(cloud)).filter((t) => !SYNC_EXCLUDED.has(t));
|
|
46404
46613
|
} catch {
|
|
46405
46614
|
local.close();
|
|
46406
46615
|
await cloud.close();
|
|
@@ -46422,6 +46631,100 @@ function registerCloudSyncTools(server) {
|
|
|
46422
46631
|
if (errors4.length > 0)
|
|
46423
46632
|
lines.push(`Errors: ${errors4.join("; ")}`);
|
|
46424
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(`
|
|
46425
46728
|
`) }] };
|
|
46426
46729
|
} catch (e) {
|
|
46427
46730
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
@@ -46452,10 +46755,19 @@ function formatError2(e) {
|
|
|
46452
46755
|
return e.message;
|
|
46453
46756
|
return String(e);
|
|
46454
46757
|
}
|
|
46455
|
-
var CONFLICT_TABLES;
|
|
46758
|
+
var SYNC_EXCLUDED, CONFLICT_TABLES;
|
|
46456
46759
|
var init_cloud = __esm(() => {
|
|
46457
46760
|
init_zod2();
|
|
46458
|
-
|
|
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"]);
|
|
46459
46771
|
});
|
|
46460
46772
|
|
|
46461
46773
|
// src/mcp/index.ts
|
|
@@ -49719,6 +50031,40 @@ program2.command("dashboard").description("Start web dashboard").option("--port
|
|
|
49719
50031
|
});
|
|
49720
50032
|
registerBrainsCommand(program2);
|
|
49721
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
|
+
}
|
|
49722
50068
|
program2.action(() => {
|
|
49723
50069
|
if (!process.stdin.isTTY) {
|
|
49724
50070
|
console.error(chalk9.red("Interactive mode requires a TTY terminal."));
|