@hasna/conversations 0.0.8 → 0.1.1

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/mcp.js CHANGED
@@ -6286,7 +6286,7 @@ var require_formats = __commonJS((exports) => {
6286
6286
  }
6287
6287
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6288
6288
  function getTime(strictTimeZone) {
6289
- return function time3(str) {
6289
+ return function time(str) {
6290
6290
  const matches = TIME.exec(str);
6291
6291
  if (!matches)
6292
6292
  return false;
@@ -28341,7 +28341,7 @@ function getDb() {
28341
28341
  session_id TEXT NOT NULL,
28342
28342
  from_agent TEXT NOT NULL,
28343
28343
  to_agent TEXT NOT NULL,
28344
- channel TEXT,
28344
+ space TEXT,
28345
28345
  content TEXT NOT NULL,
28346
28346
  priority TEXT NOT NULL DEFAULT 'normal',
28347
28347
  working_dir TEXT,
@@ -28355,48 +28355,137 @@ function getDb() {
28355
28355
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)");
28356
28356
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent)");
28357
28357
  db.exec("CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at)");
28358
- db.exec("CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel)");
28359
- const cols = db.prepare("PRAGMA table_info(messages)").all();
28360
- if (!cols.some((c) => c.name === "channel")) {
28361
- db.exec("ALTER TABLE messages ADD COLUMN channel TEXT");
28362
- }
28358
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
28359
+ db.exec(`
28360
+ CREATE TABLE IF NOT EXISTS projects (
28361
+ id TEXT PRIMARY KEY,
28362
+ name TEXT NOT NULL UNIQUE,
28363
+ description TEXT,
28364
+ path TEXT,
28365
+ created_by TEXT NOT NULL,
28366
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
28367
+ metadata TEXT,
28368
+ tags TEXT,
28369
+ status TEXT NOT NULL DEFAULT 'active',
28370
+ repository TEXT,
28371
+ settings TEXT
28372
+ )
28373
+ `);
28374
+ db.exec("CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name)");
28375
+ db.exec("CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status)");
28363
28376
  db.exec(`
28364
- CREATE TABLE IF NOT EXISTS channels (
28377
+ CREATE TABLE IF NOT EXISTS spaces (
28365
28378
  name TEXT PRIMARY KEY,
28366
28379
  description TEXT,
28380
+ parent_id TEXT REFERENCES spaces(name),
28381
+ project_id TEXT REFERENCES projects(id),
28367
28382
  created_by TEXT NOT NULL,
28368
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
28383
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
28384
+ archived_at TEXT
28369
28385
  )
28370
28386
  `);
28387
+ db.exec("CREATE INDEX IF NOT EXISTS idx_spaces_parent ON spaces(parent_id)");
28388
+ db.exec("CREATE INDEX IF NOT EXISTS idx_spaces_project ON spaces(project_id)");
28371
28389
  db.exec(`
28372
- CREATE TABLE IF NOT EXISTS channel_members (
28373
- channel TEXT NOT NULL REFERENCES channels(name),
28390
+ CREATE TABLE IF NOT EXISTS space_members (
28391
+ space TEXT NOT NULL REFERENCES spaces(name),
28374
28392
  agent TEXT NOT NULL,
28375
28393
  joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
28376
- PRIMARY KEY (channel, agent)
28394
+ PRIMARY KEY (space, agent)
28377
28395
  )
28378
28396
  `);
28397
+ db.exec(`
28398
+ CREATE TABLE IF NOT EXISTS agent_presence (
28399
+ agent TEXT PRIMARY KEY,
28400
+ status TEXT NOT NULL DEFAULT 'online',
28401
+ last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
28402
+ metadata TEXT
28403
+ )
28404
+ `);
28405
+ const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
28406
+ const tableNames = existingTables.map((t) => t.name);
28407
+ if (tableNames.includes("channels") && tableNames.includes("spaces")) {
28408
+ const spaceCount = db.prepare("SELECT COUNT(*) as c FROM spaces").get().c;
28409
+ const channelCount = db.prepare("SELECT COUNT(*) as c FROM channels").get().c;
28410
+ if (channelCount > 0 && spaceCount === 0) {
28411
+ db.exec("BEGIN");
28412
+ try {
28413
+ db.exec(`
28414
+ INSERT OR IGNORE INTO spaces (name, description, created_by, created_at)
28415
+ SELECT name, description, created_by, created_at FROM channels
28416
+ `);
28417
+ if (tableNames.includes("channel_members")) {
28418
+ db.exec(`
28419
+ INSERT OR IGNORE INTO space_members (space, agent, joined_at)
28420
+ SELECT channel, agent, joined_at FROM channel_members
28421
+ `);
28422
+ }
28423
+ db.exec("COMMIT");
28424
+ } catch (e) {
28425
+ db.exec("ROLLBACK");
28426
+ throw e;
28427
+ }
28428
+ }
28429
+ db.exec("DROP TABLE IF EXISTS channel_members");
28430
+ db.exec("DROP TABLE IF EXISTS channels");
28431
+ }
28432
+ const msgCols = db.prepare("PRAGMA table_info(messages)").all();
28433
+ const colNames = msgCols.map((c) => c.name);
28434
+ if (colNames.includes("channel") && !colNames.includes("space")) {
28435
+ db.exec("ALTER TABLE messages ADD COLUMN space TEXT");
28436
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_space ON messages(space)");
28437
+ db.exec("UPDATE messages SET space = channel WHERE channel IS NOT NULL");
28438
+ db.exec(`
28439
+ UPDATE messages
28440
+ SET session_id = 'space:' || substr(session_id, 9)
28441
+ WHERE session_id LIKE 'channel:%'
28442
+ `);
28443
+ }
28444
+ const spaceCols = db.prepare("PRAGMA table_info(spaces)").all();
28445
+ const spaceColNames = spaceCols.map((c) => c.name);
28446
+ if (!spaceColNames.includes("archived_at")) {
28447
+ db.exec("ALTER TABLE spaces ADD COLUMN archived_at TEXT");
28448
+ }
28449
+ const msgCols2 = db.prepare("PRAGMA table_info(messages)").all();
28450
+ const colNames2 = msgCols2.map((c) => c.name);
28451
+ if (!colNames2.includes("edited_at")) {
28452
+ db.exec("ALTER TABLE messages ADD COLUMN edited_at TEXT");
28453
+ }
28454
+ if (!colNames2.includes("pinned_at")) {
28455
+ db.exec("ALTER TABLE messages ADD COLUMN pinned_at TEXT");
28456
+ db.exec("CREATE INDEX IF NOT EXISTS idx_messages_pinned ON messages(pinned_at)");
28457
+ }
28379
28458
  return db;
28380
28459
  }
28381
28460
 
28382
28461
  // src/lib/messages.ts
28383
28462
  import { randomUUID } from "crypto";
28384
28463
  function parseMessage(row) {
28464
+ let metadata = null;
28465
+ if (row.metadata) {
28466
+ try {
28467
+ metadata = JSON.parse(row.metadata);
28468
+ } catch {
28469
+ metadata = null;
28470
+ }
28471
+ }
28385
28472
  return {
28386
28473
  ...row,
28387
- metadata: row.metadata ? JSON.parse(row.metadata) : null
28474
+ metadata
28388
28475
  };
28389
28476
  }
28390
28477
  function sendMessage(opts) {
28391
28478
  const db2 = getDb();
28392
- const sessionId = opts.session_id || `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`;
28479
+ const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
28480
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID().slice(0, 8)}`);
28393
28481
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
28482
+ const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
28394
28483
  const stmt = db2.prepare(`
28395
- INSERT INTO messages (session_id, from_agent, to_agent, channel, content, priority, working_dir, repository, branch, metadata)
28484
+ INSERT INTO messages (session_id, from_agent, to_agent, space, content, priority, working_dir, repository, branch, metadata)
28396
28485
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
28397
28486
  RETURNING *
28398
28487
  `);
28399
- const row = stmt.get(sessionId, opts.from, opts.to, opts.channel || null, opts.content, opts.priority || "normal", opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
28488
+ const row = stmt.get(sessionId, opts.from, opts.to, opts.space || null, opts.content, normalizedPriority, opts.working_dir || null, opts.repository || null, opts.branch || null, metadata);
28400
28489
  return parseMessage(row);
28401
28490
  }
28402
28491
  function readMessages(opts = {}) {
@@ -28415,20 +28504,25 @@ function readMessages(opts = {}) {
28415
28504
  conditions.push("to_agent = ?");
28416
28505
  params.push(opts.to);
28417
28506
  }
28418
- if (opts.channel) {
28419
- conditions.push("channel = ?");
28420
- params.push(opts.channel);
28507
+ if (opts.space) {
28508
+ conditions.push("space = ?");
28509
+ params.push(opts.space);
28421
28510
  }
28422
28511
  if (opts.since) {
28423
28512
  conditions.push("created_at > ?");
28424
28513
  params.push(opts.since);
28425
28514
  }
28515
+ if (opts.since_id !== undefined) {
28516
+ conditions.push("id > ?");
28517
+ params.push(opts.since_id);
28518
+ }
28426
28519
  if (opts.unread_only) {
28427
28520
  conditions.push("read_at IS NULL");
28428
28521
  }
28429
28522
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
28430
- const limit = opts.limit ? `LIMIT ${opts.limit}` : "";
28431
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ASC ${limit}`).all(...params);
28523
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
28524
+ const order = opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
28525
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} ${limit}`).all(...params);
28432
28526
  return rows.map(parseMessage);
28433
28527
  }
28434
28528
  function markRead(ids, reader) {
@@ -28445,6 +28539,130 @@ function getMessageById(id) {
28445
28539
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
28446
28540
  return row ? parseMessage(row) : null;
28447
28541
  }
28542
+ function markAllRead(agent) {
28543
+ const db2 = getDb();
28544
+ const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE to_agent = ? AND read_at IS NULL`);
28545
+ const result = stmt.run(agent);
28546
+ return result.changes;
28547
+ }
28548
+ function escapeCsvField(value) {
28549
+ if (value === null || value === undefined)
28550
+ return "";
28551
+ const str = String(value);
28552
+ if (str.includes(",") || str.includes('"') || str.includes(`
28553
+ `) || str.includes("\r")) {
28554
+ return `"${str.replace(/"/g, '""')}"`;
28555
+ }
28556
+ return str;
28557
+ }
28558
+ function exportMessages(opts) {
28559
+ const db2 = getDb();
28560
+ const conditions = [];
28561
+ const params = [];
28562
+ if (opts?.space) {
28563
+ conditions.push("space = ?");
28564
+ params.push(opts.space);
28565
+ }
28566
+ if (opts?.session_id) {
28567
+ conditions.push("session_id = ?");
28568
+ params.push(opts.session_id);
28569
+ }
28570
+ if (opts?.from) {
28571
+ conditions.push("from_agent = ?");
28572
+ params.push(opts.from);
28573
+ }
28574
+ if (opts?.since) {
28575
+ conditions.push("created_at >= ?");
28576
+ params.push(opts.since);
28577
+ }
28578
+ if (opts?.until) {
28579
+ conditions.push("created_at <= ?");
28580
+ params.push(opts.until);
28581
+ }
28582
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
28583
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ASC, id ASC`).all(...params);
28584
+ const messages = rows.map(parseMessage);
28585
+ const format = opts?.format ?? "json";
28586
+ if (format === "csv") {
28587
+ const headers = "id,session_id,from_agent,to_agent,space,content,priority,created_at,read_at";
28588
+ const lines = messages.map((m) => [
28589
+ String(m.id),
28590
+ escapeCsvField(m.session_id),
28591
+ escapeCsvField(m.from_agent),
28592
+ escapeCsvField(m.to_agent),
28593
+ escapeCsvField(m.space),
28594
+ escapeCsvField(m.content),
28595
+ escapeCsvField(m.priority),
28596
+ escapeCsvField(m.created_at),
28597
+ escapeCsvField(m.read_at)
28598
+ ].join(","));
28599
+ return [headers, ...lines].join(`
28600
+ `);
28601
+ }
28602
+ return JSON.stringify(messages, null, 2);
28603
+ }
28604
+ function deleteMessage(id, agent) {
28605
+ const db2 = getDb();
28606
+ const stmt = db2.prepare("DELETE FROM messages WHERE id = ? AND from_agent = ?");
28607
+ const result = stmt.run(id, agent);
28608
+ return result.changes > 0;
28609
+ }
28610
+ function editMessage(id, agent, newContent) {
28611
+ const db2 = getDb();
28612
+ const stmt = db2.prepare(`UPDATE messages SET content = ?, edited_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id = ? AND from_agent = ? RETURNING *`);
28613
+ const row = stmt.get(newContent, id, agent);
28614
+ return row ? parseMessage(row) : null;
28615
+ }
28616
+ function pinMessage(id) {
28617
+ const db2 = getDb();
28618
+ const stmt = db2.prepare(`UPDATE messages SET pinned_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id = ? RETURNING *`);
28619
+ const row = stmt.get(id);
28620
+ return row ? parseMessage(row) : null;
28621
+ }
28622
+ function unpinMessage(id) {
28623
+ const db2 = getDb();
28624
+ const stmt = db2.prepare(`UPDATE messages SET pinned_at = NULL WHERE id = ? RETURNING *`);
28625
+ const row = stmt.get(id);
28626
+ return row ? parseMessage(row) : null;
28627
+ }
28628
+ function getPinnedMessages(opts) {
28629
+ const db2 = getDb();
28630
+ const conditions = ["pinned_at IS NOT NULL"];
28631
+ const params = [];
28632
+ if (opts?.space) {
28633
+ conditions.push("space = ?");
28634
+ params.push(opts.space);
28635
+ }
28636
+ if (opts?.session_id) {
28637
+ conditions.push("session_id = ?");
28638
+ params.push(opts.session_id);
28639
+ }
28640
+ const where = `WHERE ${conditions.join(" AND ")}`;
28641
+ const limit = Number.isFinite(opts?.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
28642
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY pinned_at DESC, id DESC ${limit}`).all(...params);
28643
+ return rows.map(parseMessage);
28644
+ }
28645
+ function searchMessages(opts) {
28646
+ const db2 = getDb();
28647
+ const conditions = ["content LIKE ?"];
28648
+ const params = [`%${opts.query}%`];
28649
+ if (opts.space) {
28650
+ conditions.push("space = ?");
28651
+ params.push(opts.space);
28652
+ }
28653
+ if (opts.from) {
28654
+ conditions.push("from_agent = ?");
28655
+ params.push(opts.from);
28656
+ }
28657
+ if (opts.to) {
28658
+ conditions.push("to_agent = ?");
28659
+ params.push(opts.to);
28660
+ }
28661
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 50;
28662
+ const where = `WHERE ${conditions.join(" AND ")}`;
28663
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at DESC, id DESC LIMIT ${limit}`).all(...params);
28664
+ return rows.map(parseMessage);
28665
+ }
28448
28666
 
28449
28667
  // src/lib/sessions.ts
28450
28668
  function listSessions(agent) {
@@ -28476,70 +28694,407 @@ function listSessions(agent) {
28476
28694
  });
28477
28695
  }
28478
28696
 
28479
- // src/lib/channels.ts
28480
- function createChannel(name, createdBy, description) {
28697
+ // src/lib/spaces.ts
28698
+ function getSpaceDepth(spaceName) {
28699
+ const db2 = getDb();
28700
+ let depth = 0;
28701
+ let current = spaceName;
28702
+ for (let i = 0;i < 10; i++) {
28703
+ const row = db2.prepare("SELECT parent_id FROM spaces WHERE name = ?").get(current);
28704
+ if (!row || !row.parent_id)
28705
+ break;
28706
+ depth++;
28707
+ current = row.parent_id;
28708
+ }
28709
+ return depth;
28710
+ }
28711
+ function createSpace(name, createdBy, options) {
28481
28712
  const db2 = getDb();
28482
- const row = db2.prepare("INSERT INTO channels (name, description, created_by) VALUES (?, ?, ?) RETURNING *").get(name, description || null, createdBy);
28483
- db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(name, createdBy);
28713
+ if (options?.parent_id) {
28714
+ const parentExists = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(options.parent_id);
28715
+ if (!parentExists) {
28716
+ throw new Error(`Parent space not found: ${options.parent_id}`);
28717
+ }
28718
+ const parentDepth = getSpaceDepth(options.parent_id);
28719
+ if (parentDepth >= 2) {
28720
+ throw new Error("Maximum space nesting depth is 3 levels");
28721
+ }
28722
+ }
28723
+ if (options?.project_id) {
28724
+ const projectExists = db2.prepare("SELECT id FROM projects WHERE id = ?").get(options.project_id);
28725
+ if (!projectExists) {
28726
+ throw new Error(`Project not found: ${options.project_id}`);
28727
+ }
28728
+ }
28729
+ const row = db2.prepare("INSERT INTO spaces (name, description, parent_id, project_id, created_by) VALUES (?, ?, ?, ?, ?) RETURNING *").get(name, options?.description || null, options?.parent_id || null, options?.project_id || null, createdBy);
28730
+ db2.prepare("INSERT OR IGNORE INTO space_members (space, agent) VALUES (?, ?)").run(name, createdBy);
28484
28731
  return row;
28485
28732
  }
28486
- function listChannels() {
28733
+ function listSpaces(options) {
28487
28734
  const db2 = getDb();
28735
+ const conditions = [];
28736
+ const params = [];
28737
+ if (options?.project_id) {
28738
+ conditions.push("s.project_id = ?");
28739
+ params.push(options.project_id);
28740
+ }
28741
+ if (options?.parent_id !== undefined) {
28742
+ if (options.parent_id === null) {
28743
+ conditions.push("s.parent_id IS NULL");
28744
+ } else {
28745
+ conditions.push("s.parent_id = ?");
28746
+ params.push(options.parent_id);
28747
+ }
28748
+ }
28749
+ if (!options?.include_archived) {
28750
+ conditions.push("s.archived_at IS NULL");
28751
+ }
28752
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
28488
28753
  const rows = db2.prepare(`
28489
28754
  SELECT
28490
- c.name,
28491
- c.description,
28492
- c.created_by,
28493
- c.created_at,
28494
- (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
28495
- (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
28496
- FROM channels c
28497
- ORDER BY c.name ASC
28498
- `).all();
28755
+ s.name,
28756
+ s.description,
28757
+ s.parent_id,
28758
+ s.project_id,
28759
+ s.created_by,
28760
+ s.created_at,
28761
+ s.archived_at,
28762
+ (SELECT COUNT(*) FROM space_members WHERE space = s.name) AS member_count,
28763
+ (SELECT COUNT(*) FROM messages WHERE space = s.name) AS message_count
28764
+ FROM spaces s
28765
+ ${where}
28766
+ ORDER BY s.name ASC
28767
+ `).all(...params);
28499
28768
  return rows;
28500
28769
  }
28501
- function getChannel(name) {
28770
+ function getSpace(name) {
28502
28771
  const db2 = getDb();
28503
28772
  const row = db2.prepare(`
28504
28773
  SELECT
28505
- c.name,
28506
- c.description,
28507
- c.created_by,
28508
- c.created_at,
28509
- (SELECT COUNT(*) FROM channel_members WHERE channel = c.name) AS member_count,
28510
- (SELECT COUNT(*) FROM messages WHERE channel = c.name) AS message_count
28511
- FROM channels c
28512
- WHERE c.name = ?
28774
+ s.name,
28775
+ s.description,
28776
+ s.parent_id,
28777
+ s.project_id,
28778
+ s.created_by,
28779
+ s.created_at,
28780
+ s.archived_at,
28781
+ (SELECT COUNT(*) FROM space_members WHERE space = s.name) AS member_count,
28782
+ (SELECT COUNT(*) FROM messages WHERE space = s.name) AS message_count
28783
+ FROM spaces s
28784
+ WHERE s.name = ?
28513
28785
  `).get(name);
28514
28786
  return row;
28515
28787
  }
28516
- function joinChannel(channelName, agent) {
28788
+ function joinSpace(spaceName, agent) {
28517
28789
  const db2 = getDb();
28518
- const channel = db2.prepare("SELECT name FROM channels WHERE name = ?").get(channelName);
28519
- if (!channel)
28790
+ const space = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(spaceName);
28791
+ if (!space)
28520
28792
  return false;
28521
- db2.prepare("INSERT OR IGNORE INTO channel_members (channel, agent) VALUES (?, ?)").run(channelName, agent);
28793
+ db2.prepare("INSERT OR IGNORE INTO space_members (space, agent) VALUES (?, ?)").run(spaceName, agent);
28522
28794
  return true;
28523
28795
  }
28524
- function leaveChannel(channelName, agent) {
28796
+ function leaveSpace(spaceName, agent) {
28525
28797
  const db2 = getDb();
28526
- const result = db2.prepare("DELETE FROM channel_members WHERE channel = ? AND agent = ?").run(channelName, agent);
28798
+ const result = db2.prepare("DELETE FROM space_members WHERE space = ? AND agent = ?").run(spaceName, agent);
28799
+ return result.changes > 0;
28800
+ }
28801
+ function updateSpace(name, updates) {
28802
+ const db2 = getDb();
28803
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
28804
+ if (!existing) {
28805
+ throw new Error(`Space not found: ${name}`);
28806
+ }
28807
+ if (updates.parent_id !== undefined && updates.parent_id !== existing.parent_id) {
28808
+ if (updates.parent_id !== null) {
28809
+ const parentExists = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(updates.parent_id);
28810
+ if (!parentExists) {
28811
+ throw new Error(`Parent space not found: ${updates.parent_id}`);
28812
+ }
28813
+ const parentDepth = getSpaceDepth(updates.parent_id);
28814
+ if (parentDepth >= 2) {
28815
+ throw new Error("Maximum space nesting depth is 3 levels");
28816
+ }
28817
+ if (updates.parent_id === name) {
28818
+ throw new Error("A space cannot be its own parent");
28819
+ }
28820
+ }
28821
+ }
28822
+ if (updates.project_id !== undefined && updates.project_id !== existing.project_id) {
28823
+ if (updates.project_id !== null) {
28824
+ const projectExists = db2.prepare("SELECT id FROM projects WHERE id = ?").get(updates.project_id);
28825
+ if (!projectExists) {
28826
+ throw new Error(`Project not found: ${updates.project_id}`);
28827
+ }
28828
+ }
28829
+ }
28830
+ const sets = [];
28831
+ const params = [];
28832
+ if (updates.description !== undefined) {
28833
+ sets.push("description = ?");
28834
+ params.push(updates.description);
28835
+ }
28836
+ if (updates.parent_id !== undefined) {
28837
+ sets.push("parent_id = ?");
28838
+ params.push(updates.parent_id);
28839
+ }
28840
+ if (updates.project_id !== undefined) {
28841
+ sets.push("project_id = ?");
28842
+ params.push(updates.project_id);
28843
+ }
28844
+ if (sets.length === 0) {
28845
+ return existing;
28846
+ }
28847
+ params.push(name);
28848
+ const row = db2.prepare(`UPDATE spaces SET ${sets.join(", ")} WHERE name = ? RETURNING *`).get(...params);
28849
+ return row;
28850
+ }
28851
+ function archiveSpace(name) {
28852
+ const db2 = getDb();
28853
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
28854
+ if (!existing) {
28855
+ throw new Error(`Space not found: ${name}`);
28856
+ }
28857
+ const row = db2.prepare("UPDATE spaces SET archived_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE name = ? RETURNING *").get(name);
28858
+ return row;
28859
+ }
28860
+ function unarchiveSpace(name) {
28861
+ const db2 = getDb();
28862
+ const existing = db2.prepare("SELECT * FROM spaces WHERE name = ?").get(name);
28863
+ if (!existing) {
28864
+ throw new Error(`Space not found: ${name}`);
28865
+ }
28866
+ const row = db2.prepare("UPDATE spaces SET archived_at = NULL WHERE name = ? RETURNING *").get(name);
28867
+ return row;
28868
+ }
28869
+
28870
+ // src/lib/projects.ts
28871
+ import { randomUUID as randomUUID2 } from "crypto";
28872
+ function parseProject(row) {
28873
+ let metadata = null;
28874
+ if (row.metadata) {
28875
+ try {
28876
+ metadata = JSON.parse(row.metadata);
28877
+ } catch {
28878
+ metadata = null;
28879
+ }
28880
+ }
28881
+ let tags = [];
28882
+ if (row.tags) {
28883
+ try {
28884
+ tags = JSON.parse(row.tags);
28885
+ } catch {
28886
+ tags = [];
28887
+ }
28888
+ }
28889
+ let settings = null;
28890
+ if (row.settings) {
28891
+ try {
28892
+ settings = JSON.parse(row.settings);
28893
+ } catch {
28894
+ settings = null;
28895
+ }
28896
+ }
28897
+ return {
28898
+ id: row.id,
28899
+ name: row.name,
28900
+ description: row.description || null,
28901
+ path: row.path || null,
28902
+ created_by: row.created_by,
28903
+ created_at: row.created_at,
28904
+ metadata,
28905
+ tags,
28906
+ status: row.status || "active",
28907
+ repository: row.repository || null,
28908
+ settings
28909
+ };
28910
+ }
28911
+ function createProject(opts) {
28912
+ const db2 = getDb();
28913
+ const id = randomUUID2();
28914
+ const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
28915
+ const tags = opts.tags ? JSON.stringify(opts.tags) : null;
28916
+ const settings = opts.settings ? JSON.stringify(opts.settings) : null;
28917
+ const row = db2.prepare(`
28918
+ INSERT INTO projects (id, name, description, path, created_by, metadata, tags, repository, settings)
28919
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
28920
+ RETURNING *
28921
+ `).get(id, opts.name, opts.description || null, opts.path || null, opts.created_by, metadata, tags, opts.repository || null, settings);
28922
+ return parseProject(row);
28923
+ }
28924
+ function listProjects(opts) {
28925
+ const db2 = getDb();
28926
+ const conditions = [];
28927
+ const params = [];
28928
+ if (opts?.status) {
28929
+ conditions.push("p.status = ?");
28930
+ params.push(opts.status);
28931
+ }
28932
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
28933
+ const rows = db2.prepare(`
28934
+ SELECT
28935
+ p.*,
28936
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
28937
+ FROM projects p
28938
+ ${where}
28939
+ ORDER BY p.name ASC
28940
+ `).all(...params);
28941
+ return rows.map((row) => ({
28942
+ ...parseProject(row),
28943
+ space_count: row.space_count
28944
+ }));
28945
+ }
28946
+ function getProject(id) {
28947
+ const db2 = getDb();
28948
+ const row = db2.prepare(`
28949
+ SELECT
28950
+ p.*,
28951
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
28952
+ FROM projects p
28953
+ WHERE p.id = ?
28954
+ `).get(id);
28955
+ if (!row)
28956
+ return null;
28957
+ return {
28958
+ ...parseProject(row),
28959
+ space_count: row.space_count
28960
+ };
28961
+ }
28962
+ function getProjectByName(name) {
28963
+ const db2 = getDb();
28964
+ const row = db2.prepare(`
28965
+ SELECT
28966
+ p.*,
28967
+ (SELECT COUNT(*) FROM spaces WHERE project_id = p.id) AS space_count
28968
+ FROM projects p
28969
+ WHERE p.name = ?
28970
+ `).get(name);
28971
+ if (!row)
28972
+ return null;
28973
+ return {
28974
+ ...parseProject(row),
28975
+ space_count: row.space_count
28976
+ };
28977
+ }
28978
+ function updateProject(id, updates) {
28979
+ const db2 = getDb();
28980
+ const existing = db2.prepare("SELECT * FROM projects WHERE id = ?").get(id);
28981
+ if (!existing) {
28982
+ throw new Error(`Project not found: ${id}`);
28983
+ }
28984
+ const sets = [];
28985
+ const params = [];
28986
+ if (updates.name !== undefined) {
28987
+ sets.push("name = ?");
28988
+ params.push(updates.name);
28989
+ }
28990
+ if (updates.description !== undefined) {
28991
+ sets.push("description = ?");
28992
+ params.push(updates.description);
28993
+ }
28994
+ if (updates.path !== undefined) {
28995
+ sets.push("path = ?");
28996
+ params.push(updates.path);
28997
+ }
28998
+ if (updates.metadata !== undefined) {
28999
+ sets.push("metadata = ?");
29000
+ params.push(JSON.stringify(updates.metadata));
29001
+ }
29002
+ if (updates.tags !== undefined) {
29003
+ sets.push("tags = ?");
29004
+ params.push(JSON.stringify(updates.tags));
29005
+ }
29006
+ if (updates.status !== undefined) {
29007
+ sets.push("status = ?");
29008
+ params.push(updates.status);
29009
+ }
29010
+ if (updates.repository !== undefined) {
29011
+ sets.push("repository = ?");
29012
+ params.push(updates.repository);
29013
+ }
29014
+ if (updates.settings !== undefined) {
29015
+ sets.push("settings = ?");
29016
+ params.push(JSON.stringify(updates.settings));
29017
+ }
29018
+ if (sets.length === 0) {
29019
+ return parseProject(existing);
29020
+ }
29021
+ params.push(id);
29022
+ const row = db2.prepare(`UPDATE projects SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...params);
29023
+ return parseProject(row);
29024
+ }
29025
+ function deleteProject(id) {
29026
+ const db2 = getDb();
29027
+ const spaceCount = db2.prepare("SELECT COUNT(*) as c FROM spaces WHERE project_id = ?").get(id).c;
29028
+ if (spaceCount > 0) {
29029
+ throw new Error(`Cannot delete project: ${spaceCount} space(s) still reference it`);
29030
+ }
29031
+ const result = db2.prepare("DELETE FROM projects WHERE id = ?").run(id);
28527
29032
  return result.changes > 0;
28528
29033
  }
28529
29034
 
28530
29035
  // src/lib/identity.ts
28531
29036
  function resolveIdentity(explicit) {
28532
- if (explicit)
28533
- return explicit;
28534
- if (process.env.CONVERSATIONS_AGENT_ID)
28535
- return process.env.CONVERSATIONS_AGENT_ID;
29037
+ const explicitValue = explicit?.trim();
29038
+ if (explicitValue)
29039
+ return explicitValue;
29040
+ const envValue = process.env.CONVERSATIONS_AGENT_ID?.trim();
29041
+ if (envValue)
29042
+ return envValue;
28536
29043
  return "user";
28537
29044
  }
28538
29045
 
29046
+ // src/lib/presence.ts
29047
+ var ONLINE_THRESHOLD_SECONDS = 60;
29048
+ function parsePresence(row) {
29049
+ let metadata = null;
29050
+ if (row.metadata) {
29051
+ try {
29052
+ metadata = JSON.parse(row.metadata);
29053
+ } catch {
29054
+ metadata = null;
29055
+ }
29056
+ }
29057
+ const lastSeenAt = row.last_seen_at;
29058
+ const lastSeenMs = new Date(lastSeenAt + "Z").getTime();
29059
+ const nowMs = Date.now();
29060
+ const online = nowMs - lastSeenMs < ONLINE_THRESHOLD_SECONDS * 1000;
29061
+ return {
29062
+ agent: row.agent,
29063
+ status: row.status,
29064
+ last_seen_at: lastSeenAt,
29065
+ online,
29066
+ metadata
29067
+ };
29068
+ }
29069
+ function heartbeat(agent, status, metadata) {
29070
+ const db2 = getDb();
29071
+ const metadataJson = metadata ? JSON.stringify(metadata) : null;
29072
+ const resolvedStatus = status || "online";
29073
+ db2.prepare(`
29074
+ INSERT INTO agent_presence (agent, status, last_seen_at, metadata)
29075
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
29076
+ ON CONFLICT(agent) DO UPDATE SET
29077
+ status = excluded.status,
29078
+ last_seen_at = excluded.last_seen_at,
29079
+ metadata = excluded.metadata
29080
+ `).run(agent, resolvedStatus, metadataJson);
29081
+ }
29082
+ function listAgents(opts) {
29083
+ const db2 = getDb();
29084
+ let query = "SELECT * FROM agent_presence";
29085
+ const params = [];
29086
+ if (opts?.online_only) {
29087
+ query += " WHERE last_seen_at > strftime('%Y-%m-%dT%H:%M:%f', 'now', '-60 seconds')";
29088
+ }
29089
+ query += " ORDER BY last_seen_at DESC";
29090
+ const rows = db2.prepare(query).all(...params);
29091
+ return rows.map(parsePresence);
29092
+ }
29093
+
28539
29094
  // src/mcp/index.ts
28540
29095
  var server = new McpServer({
28541
29096
  name: "conversations",
28542
- version: "0.0.8"
29097
+ version: "0.1.0"
28543
29098
  });
28544
29099
  server.registerTool("send_message", {
28545
29100
  title: "Send Message",
@@ -28556,7 +29111,17 @@ server.registerTool("send_message", {
28556
29111
  }
28557
29112
  }, async ({ to, content, session_id, priority, working_dir, repository, branch, metadata }) => {
28558
29113
  const from = resolveIdentity();
28559
- const parsedMetadata = metadata ? JSON.parse(metadata) : undefined;
29114
+ let parsedMetadata;
29115
+ if (metadata) {
29116
+ try {
29117
+ parsedMetadata = JSON.parse(metadata);
29118
+ } catch {
29119
+ return {
29120
+ content: [{ type: "text", text: "Invalid metadata JSON." }],
29121
+ isError: true
29122
+ };
29123
+ }
29124
+ }
28560
29125
  const msg = sendMessage({
28561
29126
  from,
28562
29127
  to,
@@ -28579,7 +29144,7 @@ server.registerTool("read_messages", {
28579
29144
  session_id: exports_external.string().optional().describe("Filter by session ID"),
28580
29145
  from: exports_external.string().optional().describe("Filter by sender agent ID"),
28581
29146
  to: exports_external.string().optional().describe("Filter by recipient agent ID"),
28582
- channel: exports_external.string().optional().describe("Filter by channel name"),
29147
+ space: exports_external.string().optional().describe("Filter by space name"),
28583
29148
  since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
28584
29149
  limit: exports_external.number().optional().describe("Max messages to return"),
28585
29150
  unread_only: exports_external.boolean().optional().describe("Only return unread messages")
@@ -28619,12 +29184,15 @@ server.registerTool("reply", {
28619
29184
  };
28620
29185
  }
28621
29186
  const from = resolveIdentity();
29187
+ const space = original.space || (original.session_id?.startsWith("space:") ? original.session_id.slice(6) : undefined);
29188
+ const to = space ? space : original.from_agent === from ? original.to_agent : original.from_agent;
28622
29189
  const msg = sendMessage({
28623
29190
  from,
28624
- to: original.from_agent,
29191
+ to,
28625
29192
  content,
28626
29193
  session_id: original.session_id,
28627
- priority
29194
+ priority,
29195
+ space
28628
29196
  });
28629
29197
  return {
28630
29198
  content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
@@ -28632,123 +29200,568 @@ server.registerTool("reply", {
28632
29200
  });
28633
29201
  server.registerTool("mark_read", {
28634
29202
  title: "Mark Read",
28635
- description: "Mark message IDs as read for the current agent.",
29203
+ description: "Mark message IDs as read for the current agent. Set 'all' to true to mark all unread messages as read.",
28636
29204
  inputSchema: {
28637
- ids: exports_external.array(exports_external.number()).describe("Message IDs to mark as read")
29205
+ ids: exports_external.array(exports_external.number()).optional().describe("Message IDs to mark as read"),
29206
+ all: exports_external.boolean().optional().describe("Mark all unread messages as read")
28638
29207
  }
28639
- }, async ({ ids }) => {
29208
+ }, async ({ ids, all }) => {
28640
29209
  const agent = resolveIdentity();
28641
- const count = markRead(ids, agent);
29210
+ let count;
29211
+ if (all) {
29212
+ count = markAllRead(agent);
29213
+ } else if (ids && ids.length > 0) {
29214
+ count = markRead(ids, agent);
29215
+ } else {
29216
+ return {
29217
+ content: [{ type: "text", text: "Provide message IDs or set 'all' to true." }],
29218
+ isError: true
29219
+ };
29220
+ }
28642
29221
  return {
28643
29222
  content: [{ type: "text", text: JSON.stringify({ marked_read: count }, null, 2) }]
28644
29223
  };
28645
29224
  });
28646
- server.registerTool("create_channel", {
28647
- title: "Create Channel",
28648
- description: "Create a new channel. The creator is auto-joined.",
29225
+ server.registerTool("search_messages", {
29226
+ title: "Search Messages",
29227
+ description: "Full-text search across message content. Returns matching messages ordered by newest first.",
28649
29228
  inputSchema: {
28650
- name: exports_external.string().describe("Channel name (e.g. 'deployments', 'code-review')"),
28651
- description: exports_external.string().optional().describe("Channel description")
29229
+ query: exports_external.string().describe("Search query string"),
29230
+ space: exports_external.string().optional().describe("Filter by space name"),
29231
+ from: exports_external.string().optional().describe("Filter by sender agent ID"),
29232
+ to: exports_external.string().optional().describe("Filter by recipient agent ID"),
29233
+ limit: exports_external.number().optional().describe("Max results to return (default 50)")
28652
29234
  }
28653
- }, async ({ name, description }) => {
29235
+ }, async ({ query, space, from, to, limit }) => {
29236
+ const messages = searchMessages({ query, space, from, to, limit });
29237
+ return {
29238
+ content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
29239
+ };
29240
+ });
29241
+ server.registerTool("export_messages", {
29242
+ title: "Export Messages",
29243
+ description: "Export messages as JSON or CSV with optional filters.",
29244
+ inputSchema: {
29245
+ space: exports_external.string().optional().describe("Filter by space name"),
29246
+ session_id: exports_external.string().optional().describe("Filter by session ID"),
29247
+ from: exports_external.string().optional().describe("Filter by sender agent ID"),
29248
+ since: exports_external.string().optional().describe("Messages after this ISO date"),
29249
+ until: exports_external.string().optional().describe("Messages before this ISO date"),
29250
+ format: exports_external.enum(["json", "csv"]).optional().describe("Output format (default: json)")
29251
+ }
29252
+ }, async ({ space, session_id, from, since, until, format }) => {
29253
+ const result = exportMessages({ space, session_id, from, since, until, format });
29254
+ return {
29255
+ content: [{ type: "text", text: result }]
29256
+ };
29257
+ });
29258
+ server.registerTool("create_space", {
29259
+ title: "Create Space",
29260
+ description: "Create a new space. The creator is auto-joined. Spaces can be nested (max 3 levels) and associated with a project.",
29261
+ inputSchema: {
29262
+ name: exports_external.string().describe("Space name (e.g. 'deployments', 'code-review')"),
29263
+ description: exports_external.string().optional().describe("Space description"),
29264
+ parent_id: exports_external.string().optional().describe("Parent space name for nesting (max 3 levels deep)"),
29265
+ project_id: exports_external.string().optional().describe("Project ID to associate this space with")
29266
+ }
29267
+ }, async ({ name, description, parent_id, project_id }) => {
28654
29268
  const agent = resolveIdentity();
28655
29269
  try {
28656
- const ch = createChannel(name, agent, description);
29270
+ const sp = createSpace(name, agent, { description, parent_id, project_id });
28657
29271
  return {
28658
- content: [{ type: "text", text: JSON.stringify(ch, null, 2) }]
29272
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
28659
29273
  };
28660
29274
  } catch (e) {
28661
29275
  if (e.message?.includes("UNIQUE constraint")) {
28662
29276
  return {
28663
- content: [{ type: "text", text: `Channel #${name} already exists` }],
29277
+ content: [{ type: "text", text: `Space #${name} already exists` }],
28664
29278
  isError: true
28665
29279
  };
28666
29280
  }
28667
- throw e;
29281
+ return {
29282
+ content: [{ type: "text", text: e.message }],
29283
+ isError: true
29284
+ };
28668
29285
  }
28669
29286
  });
28670
- server.registerTool("list_channels", {
28671
- title: "List Channels",
28672
- description: "List all available channels with member and message counts."
28673
- }, async () => {
28674
- const channels = listChannels();
29287
+ server.registerTool("list_spaces", {
29288
+ title: "List Spaces",
29289
+ description: "List all available spaces with member and message counts. Can filter by project or parent. Archived spaces are excluded by default.",
29290
+ inputSchema: {
29291
+ project_id: exports_external.string().optional().describe("Filter by project ID"),
29292
+ parent_id: exports_external.string().optional().describe("Filter by parent space name. Use 'null' for top-level only."),
29293
+ include_archived: exports_external.boolean().optional().describe("Include archived spaces (default: false)")
29294
+ }
29295
+ }, async ({ project_id, parent_id, include_archived }) => {
29296
+ const opts = {};
29297
+ if (project_id)
29298
+ opts.project_id = project_id;
29299
+ if (parent_id === "null") {
29300
+ opts.parent_id = null;
29301
+ } else if (parent_id) {
29302
+ opts.parent_id = parent_id;
29303
+ }
29304
+ if (include_archived)
29305
+ opts.include_archived = true;
29306
+ const spaces = listSpaces(opts);
28675
29307
  return {
28676
- content: [{ type: "text", text: JSON.stringify(channels, null, 2) }]
29308
+ content: [{ type: "text", text: JSON.stringify(spaces, null, 2) }]
28677
29309
  };
28678
29310
  });
28679
- server.registerTool("send_to_channel", {
28680
- title: "Send to Channel",
28681
- description: "Send a message to a channel. All members can see it.",
29311
+ server.registerTool("send_to_space", {
29312
+ title: "Send to Space",
29313
+ description: "Send a message to a space. All members can see it.",
28682
29314
  inputSchema: {
28683
- channel: exports_external.string().describe("Channel name"),
29315
+ space: exports_external.string().describe("Space name"),
28684
29316
  content: exports_external.string().describe("Message content"),
28685
29317
  priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional().describe("Message priority")
28686
29318
  }
28687
- }, async ({ channel, content, priority }) => {
29319
+ }, async ({ space, content, priority }) => {
28688
29320
  const from = resolveIdentity();
28689
- const ch = getChannel(channel);
28690
- if (!ch) {
29321
+ const sp = getSpace(space);
29322
+ if (!sp) {
28691
29323
  return {
28692
- content: [{ type: "text", text: `Channel #${channel} not found` }],
29324
+ content: [{ type: "text", text: `Space #${space} not found` }],
28693
29325
  isError: true
28694
29326
  };
28695
29327
  }
28696
29328
  const msg = sendMessage({
28697
29329
  from,
28698
- to: channel,
29330
+ to: space,
28699
29331
  content,
28700
- channel,
28701
- session_id: `channel:${channel}`,
29332
+ space,
29333
+ session_id: `space:${space}`,
28702
29334
  priority
28703
29335
  });
28704
29336
  return {
28705
29337
  content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
28706
29338
  };
28707
29339
  });
28708
- server.registerTool("read_channel", {
28709
- title: "Read Channel",
28710
- description: "Read messages from a channel.",
29340
+ server.registerTool("read_space", {
29341
+ title: "Read Space",
29342
+ description: "Read messages from a space.",
28711
29343
  inputSchema: {
28712
- channel: exports_external.string().describe("Channel name"),
29344
+ space: exports_external.string().describe("Space name"),
28713
29345
  since: exports_external.string().optional().describe("Messages after this ISO timestamp"),
28714
29346
  limit: exports_external.number().optional().describe("Max messages to return")
28715
29347
  }
28716
- }, async ({ channel, since, limit }) => {
28717
- const messages = readMessages({ channel, since, limit });
29348
+ }, async ({ space, since, limit }) => {
29349
+ const messages = readMessages({ space, since, limit });
28718
29350
  return {
28719
29351
  content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
28720
29352
  };
28721
29353
  });
28722
- server.registerTool("join_channel", {
28723
- title: "Join Channel",
28724
- description: "Join a channel to receive messages.",
29354
+ server.registerTool("join_space", {
29355
+ title: "Join Space",
29356
+ description: "Join a space to receive messages.",
28725
29357
  inputSchema: {
28726
- channel: exports_external.string().describe("Channel name to join")
29358
+ space: exports_external.string().describe("Space name to join")
28727
29359
  }
28728
- }, async ({ channel }) => {
29360
+ }, async ({ space }) => {
28729
29361
  const agent = resolveIdentity();
28730
- const ok = joinChannel(channel, agent);
29362
+ const ok = joinSpace(space, agent);
28731
29363
  if (!ok) {
28732
29364
  return {
28733
- content: [{ type: "text", text: `Channel #${channel} not found` }],
29365
+ content: [{ type: "text", text: `Space #${space} not found` }],
29366
+ isError: true
29367
+ };
29368
+ }
29369
+ return {
29370
+ content: [{ type: "text", text: JSON.stringify({ space, agent, joined: true }, null, 2) }]
29371
+ };
29372
+ });
29373
+ server.registerTool("leave_space", {
29374
+ title: "Leave Space",
29375
+ description: "Leave a space.",
29376
+ inputSchema: {
29377
+ space: exports_external.string().describe("Space name to leave")
29378
+ }
29379
+ }, async ({ space }) => {
29380
+ const agent = resolveIdentity();
29381
+ const left = leaveSpace(space, agent);
29382
+ return {
29383
+ content: [{ type: "text", text: JSON.stringify({ space, agent, left }, null, 2) }]
29384
+ };
29385
+ });
29386
+ server.registerTool("update_space", {
29387
+ title: "Update Space",
29388
+ description: "Update a space's description, parent, or project association.",
29389
+ inputSchema: {
29390
+ name: exports_external.string().describe("Space name to update"),
29391
+ description: exports_external.string().optional().describe("New description"),
29392
+ parent_id: exports_external.string().optional().describe("New parent space name (use 'null' to remove parent)"),
29393
+ project_id: exports_external.string().optional().describe("New project ID (use 'null' to remove project)")
29394
+ }
29395
+ }, async ({ name, description, parent_id, project_id }) => {
29396
+ const updates = {};
29397
+ if (description !== undefined)
29398
+ updates.description = description;
29399
+ if (parent_id !== undefined)
29400
+ updates.parent_id = parent_id === "null" ? null : parent_id;
29401
+ if (project_id !== undefined)
29402
+ updates.project_id = project_id === "null" ? null : project_id;
29403
+ try {
29404
+ const sp = updateSpace(name, updates);
29405
+ return {
29406
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
29407
+ };
29408
+ } catch (e) {
29409
+ return {
29410
+ content: [{ type: "text", text: e.message }],
29411
+ isError: true
29412
+ };
29413
+ }
29414
+ });
29415
+ server.registerTool("archive_space", {
29416
+ title: "Archive Space",
29417
+ description: "Archive a space. Archived spaces are hidden from list by default.",
29418
+ inputSchema: {
29419
+ name: exports_external.string().describe("Space name to archive")
29420
+ }
29421
+ }, async ({ name }) => {
29422
+ try {
29423
+ const sp = archiveSpace(name);
29424
+ return {
29425
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
29426
+ };
29427
+ } catch (e) {
29428
+ return {
29429
+ content: [{ type: "text", text: e.message }],
29430
+ isError: true
29431
+ };
29432
+ }
29433
+ });
29434
+ server.registerTool("unarchive_space", {
29435
+ title: "Unarchive Space",
29436
+ description: "Unarchive a previously archived space.",
29437
+ inputSchema: {
29438
+ name: exports_external.string().describe("Space name to unarchive")
29439
+ }
29440
+ }, async ({ name }) => {
29441
+ try {
29442
+ const sp = unarchiveSpace(name);
29443
+ return {
29444
+ content: [{ type: "text", text: JSON.stringify(sp, null, 2) }]
29445
+ };
29446
+ } catch (e) {
29447
+ return {
29448
+ content: [{ type: "text", text: e.message }],
29449
+ isError: true
29450
+ };
29451
+ }
29452
+ });
29453
+ server.registerTool("create_project", {
29454
+ title: "Create Project",
29455
+ description: "Create a new project. Projects organize spaces and provide context for agent collaboration.",
29456
+ inputSchema: {
29457
+ name: exports_external.string().describe("Project name (unique)"),
29458
+ description: exports_external.string().optional().describe("Project description"),
29459
+ path: exports_external.string().optional().describe("Absolute path to project on disk"),
29460
+ repository: exports_external.string().optional().describe("Repository URL"),
29461
+ tags: exports_external.string().optional().describe(`JSON array of tags (e.g. '["backend", "api"]')`),
29462
+ metadata: exports_external.string().optional().describe("JSON metadata string"),
29463
+ settings: exports_external.string().optional().describe("JSON settings string")
29464
+ }
29465
+ }, async ({ name, description, path, repository, tags, metadata, settings }) => {
29466
+ const agent = resolveIdentity();
29467
+ let parsedTags;
29468
+ if (tags) {
29469
+ try {
29470
+ parsedTags = JSON.parse(tags);
29471
+ } catch {
29472
+ return {
29473
+ content: [{ type: "text", text: "Invalid tags JSON. Expected array of strings." }],
29474
+ isError: true
29475
+ };
29476
+ }
29477
+ }
29478
+ let parsedMetadata;
29479
+ if (metadata) {
29480
+ try {
29481
+ parsedMetadata = JSON.parse(metadata);
29482
+ } catch {
29483
+ return {
29484
+ content: [{ type: "text", text: "Invalid metadata JSON." }],
29485
+ isError: true
29486
+ };
29487
+ }
29488
+ }
29489
+ let parsedSettings;
29490
+ if (settings) {
29491
+ try {
29492
+ parsedSettings = JSON.parse(settings);
29493
+ } catch {
29494
+ return {
29495
+ content: [{ type: "text", text: "Invalid settings JSON." }],
29496
+ isError: true
29497
+ };
29498
+ }
29499
+ }
29500
+ try {
29501
+ const project = createProject({
29502
+ name,
29503
+ created_by: agent,
29504
+ description,
29505
+ path,
29506
+ repository,
29507
+ tags: parsedTags,
29508
+ metadata: parsedMetadata,
29509
+ settings: parsedSettings
29510
+ });
29511
+ return {
29512
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
29513
+ };
29514
+ } catch (e) {
29515
+ if (e.message?.includes("UNIQUE constraint")) {
29516
+ return {
29517
+ content: [{ type: "text", text: `Project "${name}" already exists` }],
29518
+ isError: true
29519
+ };
29520
+ }
29521
+ return {
29522
+ content: [{ type: "text", text: e.message }],
29523
+ isError: true
29524
+ };
29525
+ }
29526
+ });
29527
+ server.registerTool("list_projects", {
29528
+ title: "List Projects",
29529
+ description: "List all registered projects.",
29530
+ inputSchema: {
29531
+ status: exports_external.enum(["active", "archived"]).optional().describe("Filter by project status")
29532
+ }
29533
+ }, async ({ status }) => {
29534
+ const projects = listProjects(status ? { status } : undefined);
29535
+ return {
29536
+ content: [{ type: "text", text: JSON.stringify(projects, null, 2) }]
29537
+ };
29538
+ });
29539
+ server.registerTool("get_project", {
29540
+ title: "Get Project",
29541
+ description: "Get full details of a project by ID or name.",
29542
+ inputSchema: {
29543
+ id: exports_external.string().describe("Project ID (UUID) or name")
29544
+ }
29545
+ }, async ({ id }) => {
29546
+ let project = getProject(id);
29547
+ if (!project) {
29548
+ project = getProjectByName(id);
29549
+ }
29550
+ if (!project) {
29551
+ return {
29552
+ content: [{ type: "text", text: `Project "${id}" not found` }],
29553
+ isError: true
29554
+ };
29555
+ }
29556
+ return {
29557
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
29558
+ };
29559
+ });
29560
+ server.registerTool("update_project", {
29561
+ title: "Update Project",
29562
+ description: "Update a project's fields.",
29563
+ inputSchema: {
29564
+ id: exports_external.string().describe("Project ID (UUID)"),
29565
+ name: exports_external.string().optional().describe("New project name"),
29566
+ description: exports_external.string().optional().describe("New description"),
29567
+ path: exports_external.string().optional().describe("New path"),
29568
+ status: exports_external.enum(["active", "archived"]).optional().describe("New status"),
29569
+ repository: exports_external.string().optional().describe("New repository URL"),
29570
+ tags: exports_external.string().optional().describe("JSON array of tags"),
29571
+ metadata: exports_external.string().optional().describe("JSON metadata string"),
29572
+ settings: exports_external.string().optional().describe("JSON settings string")
29573
+ }
29574
+ }, async ({ id, name, description, path, status, repository, tags, metadata, settings }) => {
29575
+ const updates = {};
29576
+ if (name !== undefined)
29577
+ updates.name = name;
29578
+ if (description !== undefined)
29579
+ updates.description = description;
29580
+ if (path !== undefined)
29581
+ updates.path = path;
29582
+ if (status !== undefined)
29583
+ updates.status = status;
29584
+ if (repository !== undefined)
29585
+ updates.repository = repository;
29586
+ if (tags) {
29587
+ try {
29588
+ updates.tags = JSON.parse(tags);
29589
+ } catch {
29590
+ return {
29591
+ content: [{ type: "text", text: "Invalid tags JSON." }],
29592
+ isError: true
29593
+ };
29594
+ }
29595
+ }
29596
+ if (metadata) {
29597
+ try {
29598
+ updates.metadata = JSON.parse(metadata);
29599
+ } catch {
29600
+ return {
29601
+ content: [{ type: "text", text: "Invalid metadata JSON." }],
29602
+ isError: true
29603
+ };
29604
+ }
29605
+ }
29606
+ if (settings) {
29607
+ try {
29608
+ updates.settings = JSON.parse(settings);
29609
+ } catch {
29610
+ return {
29611
+ content: [{ type: "text", text: "Invalid settings JSON." }],
29612
+ isError: true
29613
+ };
29614
+ }
29615
+ }
29616
+ try {
29617
+ const project = updateProject(id, updates);
29618
+ return {
29619
+ content: [{ type: "text", text: JSON.stringify(project, null, 2) }]
29620
+ };
29621
+ } catch (e) {
29622
+ return {
29623
+ content: [{ type: "text", text: e.message }],
29624
+ isError: true
29625
+ };
29626
+ }
29627
+ });
29628
+ server.registerTool("delete_project", {
29629
+ title: "Delete Project",
29630
+ description: "Delete a project permanently. Fails if spaces still reference it.",
29631
+ inputSchema: {
29632
+ id: exports_external.string().describe("Project ID (UUID)")
29633
+ }
29634
+ }, async ({ id }) => {
29635
+ try {
29636
+ const deleted = deleteProject(id);
29637
+ if (!deleted) {
29638
+ return {
29639
+ content: [{ type: "text", text: `Project "${id}" not found` }],
29640
+ isError: true
29641
+ };
29642
+ }
29643
+ return {
29644
+ content: [{ type: "text", text: JSON.stringify({ id, deleted: true }, null, 2) }]
29645
+ };
29646
+ } catch (e) {
29647
+ return {
29648
+ content: [{ type: "text", text: e.message }],
28734
29649
  isError: true
28735
29650
  };
28736
29651
  }
29652
+ });
29653
+ server.registerTool("delete_message", {
29654
+ title: "Delete Message",
29655
+ description: "Delete a message. Only the sender can delete their own messages. The agent is auto-resolved.",
29656
+ inputSchema: {
29657
+ id: exports_external.number().describe("Message ID to delete")
29658
+ }
29659
+ }, async ({ id }) => {
29660
+ const agent = resolveIdentity();
29661
+ const deleted = deleteMessage(id, agent);
29662
+ if (!deleted) {
29663
+ return {
29664
+ content: [{ type: "text", text: `Message #${id} not found or not your message` }],
29665
+ isError: true
29666
+ };
29667
+ }
29668
+ return {
29669
+ content: [{ type: "text", text: JSON.stringify({ deleted: true }, null, 2) }]
29670
+ };
29671
+ });
29672
+ server.registerTool("edit_message", {
29673
+ title: "Edit Message",
29674
+ description: "Edit a message's content. Only the sender can edit their own messages. The agent is auto-resolved.",
29675
+ inputSchema: {
29676
+ id: exports_external.number().describe("Message ID to edit"),
29677
+ content: exports_external.string().describe("New message content")
29678
+ }
29679
+ }, async ({ id, content }) => {
29680
+ const agent = resolveIdentity();
29681
+ const msg = editMessage(id, agent, content);
29682
+ if (!msg) {
29683
+ return {
29684
+ content: [{ type: "text", text: `Message #${id} not found or not your message` }],
29685
+ isError: true
29686
+ };
29687
+ }
29688
+ return {
29689
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
29690
+ };
29691
+ });
29692
+ server.registerTool("pin_message", {
29693
+ title: "Pin Message",
29694
+ description: "Pin a message. Pinned messages can be retrieved with get_pinned_messages.",
29695
+ inputSchema: {
29696
+ id: exports_external.number().describe("Message ID to pin")
29697
+ }
29698
+ }, async ({ id }) => {
29699
+ const msg = pinMessage(id);
29700
+ if (!msg) {
29701
+ return {
29702
+ content: [{ type: "text", text: `Message #${id} not found` }],
29703
+ isError: true
29704
+ };
29705
+ }
29706
+ return {
29707
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
29708
+ };
29709
+ });
29710
+ server.registerTool("unpin_message", {
29711
+ title: "Unpin Message",
29712
+ description: "Unpin a previously pinned message.",
29713
+ inputSchema: {
29714
+ id: exports_external.number().describe("Message ID to unpin")
29715
+ }
29716
+ }, async ({ id }) => {
29717
+ const msg = unpinMessage(id);
29718
+ if (!msg) {
29719
+ return {
29720
+ content: [{ type: "text", text: `Message #${id} not found` }],
29721
+ isError: true
29722
+ };
29723
+ }
29724
+ return {
29725
+ content: [{ type: "text", text: JSON.stringify(msg, null, 2) }]
29726
+ };
29727
+ });
29728
+ server.registerTool("get_pinned_messages", {
29729
+ title: "Get Pinned Messages",
29730
+ description: "Retrieve pinned messages, optionally filtered by space or session.",
29731
+ inputSchema: {
29732
+ space: exports_external.string().optional().describe("Filter by space name"),
29733
+ session_id: exports_external.string().optional().describe("Filter by session ID"),
29734
+ limit: exports_external.number().optional().describe("Max messages to return")
29735
+ }
29736
+ }, async ({ space, session_id, limit }) => {
29737
+ const messages = getPinnedMessages({ space, session_id, limit });
28737
29738
  return {
28738
- content: [{ type: "text", text: JSON.stringify({ channel, agent, joined: true }, null, 2) }]
29739
+ content: [{ type: "text", text: JSON.stringify(messages, null, 2) }]
28739
29740
  };
28740
29741
  });
28741
- server.registerTool("leave_channel", {
28742
- title: "Leave Channel",
28743
- description: "Leave a channel.",
29742
+ server.registerTool("heartbeat", {
29743
+ title: "Heartbeat",
29744
+ description: "Send a heartbeat to indicate agent is alive. Auto-resolves agent from CONVERSATIONS_AGENT_ID env var. Optionally set a status.",
28744
29745
  inputSchema: {
28745
- channel: exports_external.string().describe("Channel name to leave")
29746
+ status: exports_external.string().optional().describe("Agent status (e.g. 'online', 'busy', 'idle'). Defaults to 'online'.")
28746
29747
  }
28747
- }, async ({ channel }) => {
29748
+ }, async ({ status }) => {
28748
29749
  const agent = resolveIdentity();
28749
- const left = leaveChannel(channel, agent);
29750
+ heartbeat(agent, status);
29751
+ return {
29752
+ content: [{ type: "text", text: JSON.stringify({ agent, status: status || "online", heartbeat: true }, null, 2) }]
29753
+ };
29754
+ });
29755
+ server.registerTool("list_agents", {
29756
+ title: "List Agents",
29757
+ description: "List all agents with their presence status. Returns agent name, status, last seen time, and whether they are online.",
29758
+ inputSchema: {
29759
+ online_only: exports_external.boolean().optional().describe("Only return agents that are currently online (seen within last 60 seconds)")
29760
+ }
29761
+ }, async ({ online_only }) => {
29762
+ const agents = listAgents({ online_only });
28750
29763
  return {
28751
- content: [{ type: "text", text: JSON.stringify({ channel, agent, left }, null, 2) }]
29764
+ content: [{ type: "text", text: JSON.stringify(agents, null, 2) }]
28752
29765
  };
28753
29766
  });
28754
29767
  async function startMcpServer() {