@hasna/conversations 0.2.42 → 0.2.44

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
@@ -6,39 +6,60 @@ var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ function __accessProp(key) {
10
+ return this[key];
11
+ }
12
+ var __toESMCache_node;
13
+ var __toESMCache_esm;
9
14
  var __toESM = (mod, isNodeMode, target) => {
15
+ var canCache = mod != null && typeof mod === "object";
16
+ if (canCache) {
17
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
18
+ var cached = cache.get(mod);
19
+ if (cached)
20
+ return cached;
21
+ }
10
22
  target = mod != null ? __create(__getProtoOf(mod)) : {};
11
23
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
24
  for (let key of __getOwnPropNames(mod))
13
25
  if (!__hasOwnProp.call(to, key))
14
26
  __defProp(to, key, {
15
- get: () => mod[key],
27
+ get: __accessProp.bind(mod, key),
16
28
  enumerable: true
17
29
  });
30
+ if (canCache)
31
+ cache.set(mod, to);
18
32
  return to;
19
33
  };
20
- var __moduleCache = /* @__PURE__ */ new WeakMap;
21
34
  var __toCommonJS = (from) => {
22
- var entry = __moduleCache.get(from), desc;
35
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
23
36
  if (entry)
24
37
  return entry;
25
38
  entry = __defProp({}, "__esModule", { value: true });
26
- if (from && typeof from === "object" || typeof from === "function")
27
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
- get: () => from[key],
29
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
- }));
39
+ if (from && typeof from === "object" || typeof from === "function") {
40
+ for (var key of __getOwnPropNames(from))
41
+ if (!__hasOwnProp.call(entry, key))
42
+ __defProp(entry, key, {
43
+ get: __accessProp.bind(from, key),
44
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
45
+ });
46
+ }
31
47
  __moduleCache.set(from, entry);
32
48
  return entry;
33
49
  };
50
+ var __moduleCache;
34
51
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
52
+ var __returnValue = (v) => v;
53
+ function __exportSetter(name, newValue) {
54
+ this[name] = __returnValue.bind(null, newValue);
55
+ }
35
56
  var __export = (target, all) => {
36
57
  for (var name in all)
37
58
  __defProp(target, name, {
38
59
  get: all[name],
39
60
  enumerable: true,
40
61
  configurable: true,
41
- set: (newValue) => all[name] = () => newValue
62
+ set: __exportSetter.bind(all, name)
42
63
  });
43
64
  };
44
65
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -6302,7 +6323,7 @@ var require_formats = __commonJS((exports) => {
6302
6323
  }
6303
6324
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6304
6325
  function getTime(strictTimeZone) {
6305
- return function time(str) {
6326
+ return function time3(str) {
6306
6327
  const matches = TIME.exec(str);
6307
6328
  if (!matches)
6308
6329
  return false;
@@ -6515,7 +6536,7 @@ var require_dist = __commonJS((exports, module) => {
6515
6536
  exports.default = formatsPlugin;
6516
6537
  });
6517
6538
 
6518
- // ../../../../node_modules/@hasna/cloud/dist/index.js
6539
+ // node_modules/@hasna/cloud/dist/index.js
6519
6540
  var exports_dist = {};
6520
6541
  __export(exports_dist, {
6521
6542
  translateSql: () => translateSql,
@@ -6610,11 +6631,11 @@ import { join as join5 } from "path";
6610
6631
  import { join as join6, dirname } from "path";
6611
6632
  import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
6612
6633
  import { homedir as homedir5, platform } from "os";
6613
- function __accessProp(key) {
6634
+ function __accessProp2(key) {
6614
6635
  return this[key];
6615
6636
  }
6616
- function __exportSetter(name, newValue) {
6617
- this[name] = __returnValue.bind(null, newValue);
6637
+ function __exportSetter2(name, newValue) {
6638
+ this[name] = __returnValue2.bind(null, newValue);
6618
6639
  }
6619
6640
  function translateSql(sql, dialect) {
6620
6641
  if (dialect === "sqlite")
@@ -7927,9 +7948,9 @@ async function syncTransfer(source, target, options, _direction) {
7927
7948
  const batch = rows.slice(offset, offset + batchSize);
7928
7949
  try {
7929
7950
  if (isAsyncAdapter(target)) {
7930
- await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
7951
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
7931
7952
  } else {
7932
- batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
7953
+ batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
7933
7954
  }
7934
7955
  result.rowsWritten += batch.length;
7935
7956
  } catch (err) {
@@ -7976,7 +7997,7 @@ async function syncTransfer(source, target, options, _direction) {
7976
7997
  }
7977
7998
  return results;
7978
7999
  }
7979
- async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
8000
+ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
7980
8001
  if (batch.length === 0)
7981
8002
  return;
7982
8003
  const colList = columns.map((c) => `"${c}"`).join(", ");
@@ -7986,22 +8007,20 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, ba
7986
8007
  }).join(", ");
7987
8008
  const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
7988
8009
  const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
7989
- const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
7990
8010
  const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
7991
- ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
8011
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
7992
8012
  const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
7993
8013
  await target.run(sql, ...params);
7994
8014
  }
7995
- function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
8015
+ function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
7996
8016
  if (batch.length === 0)
7997
8017
  return;
7998
8018
  const colList = columns.map((c) => `"${c}"`).join(", ");
7999
8019
  const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
8000
8020
  const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
8001
8021
  const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
8002
- const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
8003
8022
  const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
8004
- ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
8023
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
8005
8024
  const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
8006
8025
  target.run(sql, ...params);
8007
8026
  }
@@ -9071,7 +9090,7 @@ async function ensureAllPgDatabases() {
9071
9090
  }
9072
9091
  return results;
9073
9092
  }
9074
- function registerCloudTools(server, serviceName, opts = {}) {
9093
+ function registerCloudTools(server, serviceName) {
9075
9094
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
9076
9095
  const config2 = getCloudConfig();
9077
9096
  const lines = [
@@ -9104,13 +9123,8 @@ function registerCloudTools(server, serviceName, opts = {}) {
9104
9123
  isError: true
9105
9124
  };
9106
9125
  }
9107
- const local = new SqliteAdapter(opts.dbPath ?? getDbPath(serviceName));
9126
+ const local = new SqliteAdapter(getDbPath(serviceName));
9108
9127
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
9109
- if (opts.migrations?.length) {
9110
- for (const sql of opts.migrations) {
9111
- await cloud.run(sql);
9112
- }
9113
- }
9114
9128
  const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
9115
9129
  const results = await syncPush(local, cloud, { tables: tableList });
9116
9130
  local.close();
@@ -9132,7 +9146,7 @@ function registerCloudTools(server, serviceName, opts = {}) {
9132
9146
  isError: true
9133
9147
  };
9134
9148
  }
9135
- const local = new SqliteAdapter(opts.dbPath ?? getDbPath(serviceName));
9149
+ const local = new SqliteAdapter(getDbPath(serviceName));
9136
9150
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
9137
9151
  let tableList;
9138
9152
  if (tablesStr) {
@@ -9255,10 +9269,10 @@ function registerCloudCommands(program, serviceName) {
9255
9269
  }
9256
9270
  });
9257
9271
  }
9258
- var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node, __toESMCache_esm, __toESM2 = (mod, isNodeMode, target) => {
9272
+ var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node2, __toESMCache_esm2, __toESM2 = (mod, isNodeMode, target) => {
9259
9273
  var canCache = mod != null && typeof mod === "object";
9260
9274
  if (canCache) {
9261
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
9275
+ var cache = isNodeMode ? __toESMCache_node2 ??= new WeakMap : __toESMCache_esm2 ??= new WeakMap;
9262
9276
  var cached2 = cache.get(mod);
9263
9277
  if (cached2)
9264
9278
  return cached2;
@@ -9268,19 +9282,19 @@ var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __t
9268
9282
  for (let key of __getOwnPropNames2(mod))
9269
9283
  if (!__hasOwnProp2.call(to, key))
9270
9284
  __defProp2(to, key, {
9271
- get: __accessProp.bind(mod, key),
9285
+ get: __accessProp2.bind(mod, key),
9272
9286
  enumerable: true
9273
9287
  });
9274
9288
  if (canCache)
9275
9289
  cache.set(mod, to);
9276
9290
  return to;
9277
- }, __commonJS2 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue = (v) => v, __export2 = (target, all) => {
9291
+ }, __commonJS2 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue2 = (v) => v, __export2 = (target, all) => {
9278
9292
  for (var name in all)
9279
9293
  __defProp2(target, name, {
9280
9294
  get: all[name],
9281
9295
  enumerable: true,
9282
9296
  configurable: true,
9283
- set: __exportSetter.bind(all, name)
9297
+ set: __exportSetter2.bind(all, name)
9284
9298
  });
9285
9299
  }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), __require, require_postgres_array, require_arrayParser, require_postgres_date, require_mutable, require_postgres_interval, require_postgres_bytea, require_textParsers, require_pg_int8, require_binaryParsers, require_builtins, require_pg_types, require_defaults2, require_utils2, require_utils_legacy, require_utils_webcrypto, require_utils22, require_cert_signatures, require_sasl, require_type_overrides, require_pg_connection_string, require_connection_parameters, require_result, require_query, require_messages, require_buffer_writer, require_serializer, require_buffer_reader, require_parser, require_dist2, require_empty, require_stream, require_connection, require_split2, require_helper, require_lib, require_client, require_pg_pool, require_query2, require_client2, require_lib2, import_lib, Client, Pool, Connection, types, Query, DatabaseError, escapeIdentifier, escapeLiteral, Result, TypeOverrides, defaults, esm_default, init_esm, init_adapter, util2, objectUtil2, ZodParsedType2, getParsedType3 = (data) => {
9286
9300
  const t = typeof data;
@@ -17637,6 +17651,137 @@ function getDbPath2() {
17637
17651
  return process.env.CONVERSATIONS_DB_PATH;
17638
17652
  return join7(getDataDir2(), "messages.db");
17639
17653
  }
17654
+ function parsePresenceTimestamp(value) {
17655
+ if (typeof value !== "string" || !value)
17656
+ return 0;
17657
+ return new Date(`${value}Z`).getTime() || 0;
17658
+ }
17659
+ function normalizePresenceText(value) {
17660
+ if (typeof value !== "string")
17661
+ return null;
17662
+ const normalized = value.trim();
17663
+ return normalized ? normalized : null;
17664
+ }
17665
+ function shouldRebuildAgentPresenceTable(columns) {
17666
+ const byName = new Map(columns.map((column) => [column.name, column]));
17667
+ const agentCol = byName.get("agent");
17668
+ const projectCol = byName.get("project_id");
17669
+ if (!agentCol)
17670
+ return false;
17671
+ if (!projectCol)
17672
+ return true;
17673
+ return agentCol.pk !== 1 || projectCol.pk !== 2 || projectCol.notnull !== 1 || byName.has("pid");
17674
+ }
17675
+ function rebuildLegacyAgentPresenceTable(db2) {
17676
+ const fallbackNow = db2.prepare("SELECT strftime('%Y-%m-%dT%H:%M:%f', 'now') AS now").get().now;
17677
+ const legacyRows = db2.prepare("SELECT rowid AS _rowid, * FROM agent_presence").all();
17678
+ legacyRows.sort((left, right) => {
17679
+ const lastSeenDelta = parsePresenceTimestamp(right.last_seen_at) - parsePresenceTimestamp(left.last_seen_at);
17680
+ if (lastSeenDelta !== 0)
17681
+ return lastSeenDelta;
17682
+ const createdDelta = parsePresenceTimestamp(right.created_at) - parsePresenceTimestamp(left.created_at);
17683
+ if (createdDelta !== 0)
17684
+ return createdDelta;
17685
+ const projectDelta = Number(Boolean(normalizePresenceText(right.project_id))) - Number(Boolean(normalizePresenceText(left.project_id)));
17686
+ if (projectDelta !== 0)
17687
+ return projectDelta;
17688
+ return right._rowid - left._rowid;
17689
+ });
17690
+ const dedupedRows = new Map;
17691
+ for (const row of legacyRows) {
17692
+ const normalizedAgent = normalizePresenceText(row.agent)?.toLowerCase();
17693
+ if (!normalizedAgent)
17694
+ continue;
17695
+ const storedProjectId = normalizePresenceText(row.project_id) ?? "";
17696
+ const dedupeKey = `${normalizedAgent}\x00${storedProjectId}`;
17697
+ if (dedupedRows.has(dedupeKey))
17698
+ continue;
17699
+ dedupedRows.set(dedupeKey, row);
17700
+ }
17701
+ db2.exec("BEGIN");
17702
+ try {
17703
+ db2.exec(`
17704
+ CREATE TABLE agent_presence_new (
17705
+ id TEXT NOT NULL,
17706
+ agent TEXT NOT NULL,
17707
+ session_id TEXT,
17708
+ role TEXT NOT NULL DEFAULT 'agent',
17709
+ project_id TEXT NOT NULL DEFAULT '',
17710
+ status TEXT NOT NULL DEFAULT 'online',
17711
+ last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
17712
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
17713
+ metadata TEXT,
17714
+ PRIMARY KEY (agent, project_id)
17715
+ )
17716
+ `);
17717
+ const insertPresence = db2.prepare(`
17718
+ INSERT INTO agent_presence_new (id, agent, session_id, role, project_id, status, last_seen_at, created_at, metadata)
17719
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
17720
+ `);
17721
+ for (const [dedupeKey, row] of dedupedRows) {
17722
+ const [agent, projectKey] = dedupeKey.split("\x00");
17723
+ const id = normalizePresenceText(row.id) ?? crypto.randomUUID().slice(0, 8);
17724
+ const sessionId = normalizePresenceText(row.session_id);
17725
+ const role = normalizePresenceText(row.role) ?? "agent";
17726
+ const projectId = projectKey;
17727
+ const status = normalizePresenceText(row.status) ?? "online";
17728
+ const lastSeenAt = normalizePresenceText(row.last_seen_at) ?? fallbackNow;
17729
+ const createdAt = normalizePresenceText(row.created_at) ?? lastSeenAt;
17730
+ const metadata = typeof row.metadata === "string" ? row.metadata : row.metadata == null ? null : JSON.stringify(row.metadata);
17731
+ insertPresence.run(id, agent, sessionId, role, projectId, status, lastSeenAt, createdAt, metadata);
17732
+ }
17733
+ db2.exec("DROP TABLE agent_presence");
17734
+ db2.exec("ALTER TABLE agent_presence_new RENAME TO agent_presence");
17735
+ db2.exec("COMMIT");
17736
+ } catch (error48) {
17737
+ db2.exec("ROLLBACK");
17738
+ throw error48;
17739
+ }
17740
+ }
17741
+ function collapseDuplicateAgentPresenceRows(db2) {
17742
+ const rows = db2.prepare("SELECT rowid AS _rowid, * FROM agent_presence").all();
17743
+ rows.sort((left, right) => {
17744
+ const lastSeenDelta = parsePresenceTimestamp(right.last_seen_at) - parsePresenceTimestamp(left.last_seen_at);
17745
+ if (lastSeenDelta !== 0)
17746
+ return lastSeenDelta;
17747
+ const createdDelta = parsePresenceTimestamp(right.created_at) - parsePresenceTimestamp(left.created_at);
17748
+ if (createdDelta !== 0)
17749
+ return createdDelta;
17750
+ const projectDelta = Number(Boolean(normalizePresenceText(right.project_id))) - Number(Boolean(normalizePresenceText(left.project_id)));
17751
+ if (projectDelta !== 0)
17752
+ return projectDelta;
17753
+ return right._rowid - left._rowid;
17754
+ });
17755
+ const rowIdsToDelete = [];
17756
+ const seenAgents = new Set;
17757
+ for (const row of rows) {
17758
+ const normalizedAgent = normalizePresenceText(row.agent)?.toLowerCase();
17759
+ if (!normalizedAgent)
17760
+ continue;
17761
+ if (seenAgents.has(normalizedAgent)) {
17762
+ rowIdsToDelete.push(row._rowid);
17763
+ continue;
17764
+ }
17765
+ seenAgents.add(normalizedAgent);
17766
+ }
17767
+ if (rowIdsToDelete.length === 0)
17768
+ return;
17769
+ db2.exec("BEGIN");
17770
+ try {
17771
+ const deleteRow = db2.prepare("DELETE FROM agent_presence WHERE rowid = ?");
17772
+ for (const rowId of rowIdsToDelete) {
17773
+ deleteRow.run(rowId);
17774
+ }
17775
+ db2.exec("COMMIT");
17776
+ } catch (error48) {
17777
+ db2.exec("ROLLBACK");
17778
+ throw error48;
17779
+ }
17780
+ }
17781
+ function ensureAgentPresenceAgentUniqueIndex(db2) {
17782
+ collapseDuplicateAgentPresenceRows(db2);
17783
+ db2.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_presence_agent_unique ON agent_presence(agent)");
17784
+ }
17640
17785
  function getDb() {
17641
17786
  if (db)
17642
17787
  return db;
@@ -17709,16 +17854,18 @@ function getDb() {
17709
17854
  db.exec(`
17710
17855
  CREATE TABLE IF NOT EXISTS agent_presence (
17711
17856
  id TEXT NOT NULL,
17712
- agent TEXT PRIMARY KEY,
17857
+ agent TEXT NOT NULL,
17713
17858
  session_id TEXT,
17714
17859
  role TEXT NOT NULL DEFAULT 'agent',
17715
- project_id TEXT,
17860
+ project_id TEXT NOT NULL DEFAULT '',
17716
17861
  status TEXT NOT NULL DEFAULT 'online',
17717
17862
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
17718
17863
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
17719
- metadata TEXT
17864
+ metadata TEXT,
17865
+ PRIMARY KEY (agent, project_id)
17720
17866
  )
17721
17867
  `);
17868
+ ensureAgentPresenceAgentUniqueIndex(db);
17722
17869
  db.exec(`
17723
17870
  CREATE TABLE IF NOT EXISTS resource_locks (
17724
17871
  resource_type TEXT NOT NULL,
@@ -17820,8 +17967,13 @@ function getDb() {
17820
17967
  db.exec("UPDATE messages SET uuid = lower(hex(randomblob(16))) WHERE uuid IS NULL");
17821
17968
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid)");
17822
17969
  }
17823
- const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
17824
- const presenceColNames = presenceCols.map((c) => c.name);
17970
+ let presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
17971
+ let presenceColNames = presenceCols.map((c) => c.name);
17972
+ if (shouldRebuildAgentPresenceTable(presenceCols)) {
17973
+ rebuildLegacyAgentPresenceTable(db);
17974
+ presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
17975
+ presenceColNames = presenceCols.map((c) => c.name);
17976
+ }
17825
17977
  if (!presenceColNames.includes("id")) {
17826
17978
  db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
17827
17979
  const rows = db.prepare("SELECT agent FROM agent_presence").all();
@@ -17842,7 +17994,9 @@ function getDb() {
17842
17994
  }
17843
17995
  if (!presenceColNames.includes("project_id")) {
17844
17996
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
17997
+ db.exec("UPDATE agent_presence SET project_id = '' WHERE project_id IS NULL");
17845
17998
  }
17999
+ ensureAgentPresenceAgentUniqueIndex(db);
17846
18000
  db.exec(`
17847
18001
  CREATE TABLE IF NOT EXISTS message_read_receipts (
17848
18002
  message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
@@ -18437,15 +18591,17 @@ var init_pg_migrations = __esm(() => {
18437
18591
 
18438
18592
  CREATE TABLE IF NOT EXISTS agent_presence (
18439
18593
  id TEXT NOT NULL DEFAULT '',
18440
- agent TEXT PRIMARY KEY,
18594
+ agent TEXT NOT NULL,
18441
18595
  session_id TEXT,
18442
18596
  role TEXT NOT NULL DEFAULT 'agent',
18443
- project_id TEXT,
18597
+ project_id TEXT NOT NULL DEFAULT '',
18444
18598
  status TEXT NOT NULL DEFAULT 'online',
18445
18599
  last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18446
18600
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18447
- metadata TEXT
18601
+ metadata TEXT,
18602
+ PRIMARY KEY (agent, project_id)
18448
18603
  );
18604
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_presence_agent_unique ON agent_presence(agent);
18449
18605
 
18450
18606
  CREATE TABLE IF NOT EXISTS resource_locks (
18451
18607
  resource_type TEXT NOT NULL,
@@ -40361,6 +40517,33 @@ class StdioServerTransport {
40361
40517
  init_db();
40362
40518
  var ONLINE_THRESHOLD_SECONDS = 60;
40363
40519
  var CONFLICT_THRESHOLD_SECONDS = 30 * 60;
40520
+ function normalizeAgentName(name) {
40521
+ return name.trim().toLowerCase();
40522
+ }
40523
+ function toStoredProjectId(projectId) {
40524
+ const normalized = projectId?.trim() ?? "";
40525
+ return normalized || "";
40526
+ }
40527
+ function fromStoredProjectId(projectId) {
40528
+ const normalized = typeof projectId === "string" ? projectId.trim() : "";
40529
+ return normalized || null;
40530
+ }
40531
+ function getPresenceByAgent(db2, agent) {
40532
+ return db2.prepare(`
40533
+ SELECT * FROM agent_presence
40534
+ WHERE LOWER(agent) = ?
40535
+ ORDER BY last_seen_at DESC
40536
+ LIMIT 1
40537
+ `).get(agent);
40538
+ }
40539
+ function getPresenceByAgentAndProject(db2, agent, projectId) {
40540
+ return db2.prepare(`
40541
+ SELECT * FROM agent_presence
40542
+ WHERE LOWER(agent) = ? AND COALESCE(project_id, '') = ?
40543
+ ORDER BY last_seen_at DESC
40544
+ LIMIT 1
40545
+ `).get(agent, projectId);
40546
+ }
40364
40547
  function parsePresence(row) {
40365
40548
  let metadata = null;
40366
40549
  if (row.metadata) {
@@ -40379,7 +40562,7 @@ function parsePresence(row) {
40379
40562
  agent: row.agent,
40380
40563
  session_id: row.session_id ?? null,
40381
40564
  role: row.role || "agent",
40382
- project_id: row.project_id ?? null,
40565
+ project_id: fromStoredProjectId(row.project_id),
40383
40566
  status: row.status,
40384
40567
  last_seen_at: lastSeenAt,
40385
40568
  created_at: row.created_at || lastSeenAt,
@@ -40394,9 +40577,10 @@ function isActiveSession(lastSeenAt) {
40394
40577
  }
40395
40578
  function registerAgent(name, sessionId, role, projectId) {
40396
40579
  const db2 = getDb();
40397
- const normalizedName = name.trim().toLowerCase();
40580
+ const normalizedName = normalizeAgentName(name);
40398
40581
  const result = db2.transaction(() => {
40399
- const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
40582
+ const existing = getPresenceByAgent(db2, normalizedName);
40583
+ const storedProjectId = toStoredProjectId(projectId ?? existing?.project_id);
40400
40584
  if (existing) {
40401
40585
  const lastSeenAt = existing.last_seen_at;
40402
40586
  const existingSessionId = existing.session_id;
@@ -40414,12 +40598,26 @@ function registerAgent(name, sessionId, role, projectId) {
40414
40598
  };
40415
40599
  }
40416
40600
  const tookOver = existingSessionId !== sessionId;
40417
- db2.prepare(`
40418
- UPDATE agent_presence
40419
- SET session_id = ?, role = ?, project_id = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
40420
- WHERE agent = ?
40421
- `).run(sessionId, role || existing.role || "agent", projectId ?? existing.project_id ?? null, normalizedName);
40422
- const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
40601
+ const existingId = existing.id;
40602
+ const target = getPresenceByAgentAndProject(db2, normalizedName, storedProjectId);
40603
+ if (target && target.id !== existingId) {
40604
+ db2.prepare(`
40605
+ UPDATE agent_presence
40606
+ SET session_id = ?, role = ?, status = 'online', last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
40607
+ WHERE id = ?
40608
+ `).run(sessionId, role || existing.role || "agent", target.id);
40609
+ db2.prepare("DELETE FROM agent_presence WHERE id = ?").run(existingId);
40610
+ } else {
40611
+ db2.prepare(`
40612
+ UPDATE agent_presence
40613
+ SET session_id = ?, role = ?, project_id = ?, status = 'online', last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
40614
+ WHERE id = ?
40615
+ `).run(sessionId, role || existing.role || "agent", storedProjectId, existingId);
40616
+ }
40617
+ const updated = getPresenceByAgentAndProject(db2, normalizedName, storedProjectId) ?? getPresenceByAgent(db2, normalizedName);
40618
+ if (!updated) {
40619
+ throw new Error(`Failed to update presence for agent "${normalizedName}"`);
40620
+ }
40423
40621
  return { agent: parsePresence(updated), created: false, took_over: tookOver };
40424
40622
  }
40425
40623
  const id = crypto.randomUUID().slice(0, 8);
@@ -40427,33 +40625,60 @@ function registerAgent(name, sessionId, role, projectId) {
40427
40625
  db2.prepare(`
40428
40626
  INSERT INTO agent_presence (id, agent, session_id, role, project_id, status, last_seen_at, created_at)
40429
40627
  VALUES (?, ?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
40430
- `).run(id, normalizedName, sessionId, resolvedRole, projectId ?? null);
40431
- const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
40628
+ `).run(id, normalizedName, sessionId, resolvedRole, storedProjectId);
40629
+ const created = getPresenceByAgentAndProject(db2, normalizedName, storedProjectId) ?? getPresenceByAgent(db2, normalizedName);
40630
+ if (!created) {
40631
+ throw new Error(`Failed to create presence for agent "${normalizedName}"`);
40632
+ }
40432
40633
  return { agent: parsePresence(created), created: true, took_over: false };
40433
40634
  });
40434
40635
  return result;
40435
40636
  }
40436
- function heartbeat(agent, status, metadata, sessionId) {
40637
+ function heartbeat(agent, status, metadata, sessionId, projectId) {
40437
40638
  const db2 = getDb();
40438
40639
  const metadataJson = metadata ? JSON.stringify(metadata) : null;
40439
40640
  const resolvedStatus = status || "online";
40440
- const normalizedAgent = agent.trim().toLowerCase();
40441
- const existing = db2.prepare("SELECT id FROM agent_presence WHERE agent = ?").get(agent);
40442
- const id = existing?.id || crypto.randomUUID().slice(0, 8);
40443
- db2.prepare(`
40444
- INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at, metadata)
40445
- VALUES (?, ?, ?, 'agent', ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
40446
- ON CONFLICT(agent) DO UPDATE SET
40447
- status = excluded.status,
40448
- last_seen_at = excluded.last_seen_at,
40449
- session_id = COALESCE(excluded.session_id, agent_presence.session_id),
40450
- metadata = excluded.metadata
40451
- `).run(id, normalizedAgent, sessionId ?? null, resolvedStatus, metadataJson);
40641
+ const normalizedAgent = normalizeAgentName(agent);
40642
+ db2.transaction(() => {
40643
+ const existing = getPresenceByAgent(db2, normalizedAgent);
40644
+ const storedProjectId = toStoredProjectId(projectId ?? existing?.project_id);
40645
+ const id = existing?.id || crypto.randomUUID().slice(0, 8);
40646
+ if (existing) {
40647
+ const existingId = existing.id;
40648
+ const target = getPresenceByAgentAndProject(db2, normalizedAgent, storedProjectId);
40649
+ if (target && target.id !== existingId) {
40650
+ db2.prepare(`
40651
+ UPDATE agent_presence
40652
+ SET status = ?,
40653
+ last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now'),
40654
+ session_id = COALESCE(?, session_id),
40655
+ metadata = ?
40656
+ WHERE id = ?
40657
+ `).run(resolvedStatus, sessionId ?? null, metadataJson, target.id);
40658
+ db2.prepare("DELETE FROM agent_presence WHERE id = ?").run(existingId);
40659
+ return;
40660
+ }
40661
+ db2.prepare(`
40662
+ UPDATE agent_presence
40663
+ SET status = ?,
40664
+ last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now'),
40665
+ session_id = COALESCE(?, session_id),
40666
+ metadata = ?,
40667
+ project_id = ?
40668
+ WHERE id = ?
40669
+ `).run(resolvedStatus, sessionId ?? null, metadataJson, storedProjectId, existingId);
40670
+ return;
40671
+ }
40672
+ db2.prepare(`
40673
+ INSERT INTO agent_presence (id, agent, session_id, role, project_id, status, last_seen_at, created_at, metadata)
40674
+ VALUES (?, ?, ?, 'agent', ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
40675
+ `).run(id, normalizedAgent, sessionId ?? null, storedProjectId, resolvedStatus, metadataJson);
40676
+ });
40452
40677
  }
40453
40678
  function getPresence(agent) {
40454
40679
  const db2 = getDb();
40455
- const normalizedAgent = agent.trim().toLowerCase();
40456
- const row = db2.prepare("SELECT * FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedAgent);
40680
+ const normalizedAgent = normalizeAgentName(agent);
40681
+ const row = getPresenceByAgent(db2, normalizedAgent);
40457
40682
  return row ? parsePresence(row) : null;
40458
40683
  }
40459
40684
  function listAgents(opts) {
@@ -40468,14 +40693,14 @@ function listAgents(opts) {
40468
40693
  }
40469
40694
  function removePresence(agent) {
40470
40695
  const db2 = getDb();
40471
- const normalizedAgent = agent.trim().toLowerCase();
40696
+ const normalizedAgent = normalizeAgentName(agent);
40472
40697
  const result = db2.prepare("DELETE FROM agent_presence WHERE LOWER(agent) = ?").run(normalizedAgent);
40473
40698
  return result.changes > 0;
40474
40699
  }
40475
40700
  function renameAgent(oldName, newName) {
40476
40701
  const db2 = getDb();
40477
- const normalizedOld = oldName.trim().toLowerCase();
40478
- const normalizedNew = newName.trim().toLowerCase();
40702
+ const normalizedOld = normalizeAgentName(oldName);
40703
+ const normalizedNew = normalizeAgentName(newName);
40479
40704
  const existing = db2.prepare("SELECT agent FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedOld);
40480
40705
  if (!existing)
40481
40706
  return false;
@@ -40485,17 +40710,53 @@ function renameAgent(oldName, newName) {
40485
40710
  db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
40486
40711
  return true;
40487
40712
  }
40713
+ function setPresenceProject(agent, projectId) {
40714
+ const db2 = getDb();
40715
+ const normalizedAgent = normalizeAgentName(agent);
40716
+ const desiredProjectId = toStoredProjectId(projectId);
40717
+ const latest = getPresenceByAgent(db2, normalizedAgent);
40718
+ if (!latest) {
40719
+ heartbeat(normalizedAgent, "online", undefined, undefined, projectId);
40720
+ return;
40721
+ }
40722
+ const currentProjectId = toStoredProjectId(latest.project_id);
40723
+ if (currentProjectId === desiredProjectId)
40724
+ return;
40725
+ db2.transaction(() => {
40726
+ const latestId = latest.id;
40727
+ const target = getPresenceByAgentAndProject(db2, normalizedAgent, desiredProjectId);
40728
+ if (target && target.id !== latestId) {
40729
+ db2.prepare(`
40730
+ UPDATE agent_presence
40731
+ SET status = ?,
40732
+ last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now'),
40733
+ session_id = COALESCE(?, session_id),
40734
+ metadata = COALESCE(?, metadata)
40735
+ WHERE LOWER(agent) = ? AND COALESCE(project_id, '') = ?
40736
+ `).run(latest.status || target.status || "online", latest.session_id ?? null, latest.metadata ?? null, normalizedAgent, desiredProjectId);
40737
+ db2.prepare("DELETE FROM agent_presence WHERE id = ?").run(latestId);
40738
+ return;
40739
+ }
40740
+ db2.prepare(`
40741
+ UPDATE agent_presence
40742
+ SET project_id = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
40743
+ WHERE id = ?
40744
+ `).run(desiredProjectId, latestId);
40745
+ });
40746
+ }
40488
40747
 
40489
40748
  // src/lib/messages.ts
40490
40749
  init_db();
40491
40750
  import { randomUUID } from "crypto";
40492
- import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
40493
- import { join as join10 } from "path";
40751
+ import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync3, statSync as statSync2, existsSync as existsSync9, realpathSync } from "fs";
40752
+ import { join as join10, basename, resolve } from "path";
40494
40753
 
40495
40754
  // src/lib/webhooks.ts
40496
40755
  init_db();
40497
40756
  import { readFileSync as readFileSync3 } from "fs";
40498
40757
  import { join as join9 } from "path";
40758
+ import dns from "dns";
40759
+ import net from "net";
40499
40760
  var cachedConfig = null;
40500
40761
  var configLoadedAt = 0;
40501
40762
  var CONFIG_CACHE_MS = 1e4;
@@ -40530,6 +40791,44 @@ function matchesEvent(webhook, msg) {
40530
40791
  }
40531
40792
  return false;
40532
40793
  }
40794
+ function isPrivateIP(ip) {
40795
+ if (net.isIPv4(ip)) {
40796
+ const parts = ip.split(".").map(Number);
40797
+ if (parts[0] === 10)
40798
+ return true;
40799
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
40800
+ return true;
40801
+ if (parts[0] === 192 && parts[1] === 168)
40802
+ return true;
40803
+ if (parts[0] === 127)
40804
+ return true;
40805
+ if (parts[0] === 169 && parts[1] === 254)
40806
+ return true;
40807
+ if (parts[0] === 0)
40808
+ return true;
40809
+ } else if (net.isIPv6(ip)) {
40810
+ const lower = ip.toLowerCase();
40811
+ if (lower === "::1" || lower.startsWith("::ffff:") || lower.startsWith("fc") || lower.startsWith("fd"))
40812
+ return true;
40813
+ }
40814
+ return false;
40815
+ }
40816
+ async function validateWebhookUrl(urlStr) {
40817
+ try {
40818
+ const url2 = new URL(urlStr);
40819
+ if (url2.protocol !== "https:")
40820
+ return false;
40821
+ const hostname4 = url2.hostname;
40822
+ if (hostname4 === "localhost" || hostname4 === "0.0.0.0" || hostname4 === "127.0.0.1" || hostname4 === "::1")
40823
+ return false;
40824
+ const addresses = await dns.promises.lookup(hostname4, { all: true });
40825
+ if (!Array.isArray(addresses))
40826
+ return false;
40827
+ return !addresses.some((a) => isPrivateIP(a.address));
40828
+ } catch {
40829
+ return false;
40830
+ }
40831
+ }
40533
40832
  function fireWebhooks(msg) {
40534
40833
  const config2 = loadConfig();
40535
40834
  if (!config2.webhooks || config2.webhooks.length === 0)
@@ -40539,20 +40838,24 @@ function fireWebhooks(msg) {
40539
40838
  continue;
40540
40839
  if (!matchesEvent(webhook, msg))
40541
40840
  continue;
40542
- fetch(webhook.url, {
40543
- method: "POST",
40544
- headers: { "Content-Type": "application/json" },
40545
- body: JSON.stringify({
40546
- id: msg.id,
40547
- from: msg.from_agent,
40548
- to: msg.to_agent,
40549
- space: msg.space,
40550
- content: msg.content,
40551
- priority: msg.priority,
40552
- blocking: msg.blocking,
40553
- created_at: msg.created_at
40554
- })
40555
- }).catch(() => {});
40841
+ validateWebhookUrl(webhook.url).then((valid) => {
40842
+ if (!valid)
40843
+ return;
40844
+ fetch(webhook.url, {
40845
+ method: "POST",
40846
+ headers: { "Content-Type": "application/json" },
40847
+ body: JSON.stringify({
40848
+ id: msg.id,
40849
+ from: msg.from_agent,
40850
+ to: msg.to_agent,
40851
+ space: msg.space,
40852
+ content: msg.content,
40853
+ priority: msg.priority,
40854
+ blocking: msg.blocking,
40855
+ created_at: msg.created_at
40856
+ })
40857
+ }).catch(() => {});
40858
+ });
40556
40859
  }
40557
40860
  }
40558
40861
 
@@ -40597,6 +40900,22 @@ function getAttachmentsDir() {
40597
40900
  return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
40598
40901
  return join10(getDataDir2(), "attachments");
40599
40902
  }
40903
+ function validateAttachment(sourcePath, name) {
40904
+ const absolute = resolve(sourcePath);
40905
+ if (!existsSync9(absolute)) {
40906
+ throw new Error(`Attachment source not found: ${sourcePath}`);
40907
+ }
40908
+ const real = realpathSync(absolute);
40909
+ const stat = statSync2(real);
40910
+ if (!stat.isFile()) {
40911
+ throw new Error(`Attachment source must be a regular file: ${sourcePath}`);
40912
+ }
40913
+ const safeName = basename(name.replace(/\0/g, ""));
40914
+ if (!safeName || safeName.startsWith(".")) {
40915
+ throw new Error(`Invalid attachment name: ${name}`);
40916
+ }
40917
+ return { safeSource: real, safeName };
40918
+ }
40600
40919
  function guessMimeType(name) {
40601
40920
  const ext = name.split(".").pop()?.toLowerCase();
40602
40921
  const mimeMap = {
@@ -40668,14 +40987,15 @@ function sendMessage(opts) {
40668
40987
  mkdirSync6(attachmentsDir, { recursive: true });
40669
40988
  const attachmentInfos = [];
40670
40989
  for (const att of opts.attachments) {
40671
- const destPath = join10(attachmentsDir, att.name);
40672
- copyFileSync3(att.source_path, destPath);
40990
+ const { safeSource, safeName } = validateAttachment(att.source_path, att.name);
40991
+ const destPath = join10(attachmentsDir, safeName);
40992
+ copyFileSync3(safeSource, destPath);
40673
40993
  const stat = statSync2(destPath);
40674
40994
  attachmentInfos.push({
40675
- name: att.name,
40995
+ name: safeName,
40676
40996
  path: destPath,
40677
40997
  size: stat.size,
40678
- mime_type: guessMimeType(att.name)
40998
+ mime_type: guessMimeType(safeName)
40679
40999
  });
40680
41000
  }
40681
41001
  const attachmentsJson = JSON.stringify(attachmentInfos);
@@ -40737,8 +41057,10 @@ function readMessages(opts = {}) {
40737
41057
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
40738
41058
  const resolvedLimit = isLatest ? Math.floor(opts.latest) : Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
40739
41059
  const order = isLatest ? "DESC" : opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
40740
- const resolvedOffset = opts.offset && opts.offset > 0 ? Math.floor(opts.offset) : 0;
40741
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${resolvedLimit} OFFSET ${resolvedOffset}`).all(...params);
41060
+ const resolvedOffset = Number.isFinite(opts.offset) ? Math.floor(opts.offset) : 0;
41061
+ const safeLimit = Math.max(1, Math.min(resolvedLimit, 1e4));
41062
+ const safeOffset = Math.max(0, Math.floor(resolvedOffset));
41063
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${safeLimit} OFFSET ${safeOffset}`).all(...params);
40742
41064
  let messages = rows.map(parseMessage);
40743
41065
  if (opts.include_reply_counts && messages.length > 0) {
40744
41066
  const db22 = getDb();
@@ -40778,10 +41100,20 @@ function getMessageById(id) {
40778
41100
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
40779
41101
  return row ? parseMessage(row) : null;
40780
41102
  }
40781
- function markReadByIds(ids) {
41103
+ function markReadByIds(ids, agent) {
40782
41104
  const db2 = getDb();
40783
41105
  if (ids.length === 0)
40784
41106
  return 0;
41107
+ if (agent) {
41108
+ const stmt2 = db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
41109
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`);
41110
+ const normalized = agent.toLowerCase();
41111
+ for (const id of ids)
41112
+ stmt2.run(id, normalized);
41113
+ const placeholders2 = ids.map(() => "?").join(", ");
41114
+ const update = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id IN (${placeholders2}) AND read_at IS NULL`);
41115
+ return update.run(...ids).changes;
41116
+ }
40785
41117
  const placeholders = ids.map(() => "?").join(", ");
40786
41118
  const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE id IN (${placeholders}) AND read_at IS NULL`);
40787
41119
  const result = stmt.run(...ids);
@@ -40929,8 +41261,9 @@ function getPinnedMessages(opts) {
40929
41261
  params.push(opts.session_id);
40930
41262
  }
40931
41263
  const where = `WHERE ${conditions.join(" AND ")}`;
40932
- const limit = Number.isFinite(opts?.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
40933
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY pinned_at DESC, id DESC ${limit}`).all(...params);
41264
+ const safeLimit = Number.isFinite(opts?.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 0;
41265
+ const limitClause = safeLimit > 0 ? `LIMIT ${safeLimit}` : "";
41266
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY pinned_at DESC, id DESC ${limitClause}`).all(...params);
40934
41267
  return rows.map(parseMessage);
40935
41268
  }
40936
41269
  function getUnreadBlockers(agent) {
@@ -41119,11 +41452,11 @@ function getMessagesForAgent(agent, opts) {
41119
41452
  if (opts?.unread_only) {
41120
41453
  conditions.push("mm.notified_at IS NULL");
41121
41454
  }
41122
- const limit = opts?.limit ?? 50;
41455
+ const safeLimit = Math.max(1, Math.min(Math.floor(opts?.limit ?? 50), 1000));
41123
41456
  const rows = db2.prepare(`SELECT m.*, mm.id AS mention_id FROM messages m
41124
41457
  JOIN message_mentions mm ON mm.message_id = m.id
41125
41458
  WHERE ${conditions.join(" AND ")}
41126
- ORDER BY m.created_at DESC LIMIT ${limit}`).all(...params);
41459
+ ORDER BY m.created_at DESC LIMIT ${safeLimit}`).all(...params);
41127
41460
  return rows.map(({ mention_id, ...row }) => ({ message: parseMessage(row), mention_id }));
41128
41461
  }
41129
41462
  function markMentionsRead(agent, space) {
@@ -41231,6 +41564,7 @@ function getSessionActivity(sessionId) {
41231
41564
  // src/mcp/tools/messaging.ts
41232
41565
  init_identity();
41233
41566
  function registerMessagingTools(server, resolveProjectId) {
41567
+ const _sessionInjectRate = new Map;
41234
41568
  server.registerTool("send_message", {
41235
41569
  description: "Send a DM to an agent by name, or to a specific agent-claude session by ID. When target_session_id is provided, the message is routed to that exact session and auto-injected into its conversation.",
41236
41570
  inputSchema: {
@@ -41270,6 +41604,22 @@ function registerMessagingTools(server, resolveProjectId) {
41270
41604
  }, async (args) => {
41271
41605
  const { target_session_id, content, from: fromParam, priority } = args;
41272
41606
  const from = resolveIdentity(fromParam);
41607
+ const rateKey = `session:${from}:${target_session_id}`;
41608
+ const now = Date.now();
41609
+ const windowMs = 60000;
41610
+ const maxPerWindow = 10;
41611
+ const rateEntry = _sessionInjectRate.get(rateKey);
41612
+ if (rateEntry && now - rateEntry.start < windowMs && rateEntry.count >= maxPerWindow) {
41613
+ return {
41614
+ content: [{ type: "text", text: `Rate limit: max ${maxPerWindow} session injections per minute. Try again soon.` }],
41615
+ isError: true
41616
+ };
41617
+ }
41618
+ if (!rateEntry || now - rateEntry.start >= windowMs) {
41619
+ _sessionInjectRate.set(rateKey, { count: 1, start: now });
41620
+ } else {
41621
+ rateEntry.count++;
41622
+ }
41273
41623
  const msg = sendMessage({
41274
41624
  from,
41275
41625
  to: `session:${target_session_id}`,
@@ -41307,7 +41657,7 @@ function registerMessagingTools(server, resolveProjectId) {
41307
41657
  project_id: args.project_id ?? resolveProjectId(undefined, agent)
41308
41658
  });
41309
41659
  if (args.mark_read !== false && messages.length > 0) {
41310
- markReadByIds(messages.map((m) => m.id));
41660
+ markReadByIds(messages.map((m) => m.id), agent);
41311
41661
  }
41312
41662
  return {
41313
41663
  content: [{ type: "text", text: JSON.stringify({ messages, count: messages.length, offset: args.offset ?? 0 }) }]
@@ -42691,8 +43041,7 @@ function registerAgentTools(server, agentFocus, getAgentFocus) {
42691
43041
  const { project_id, from: fromParam } = args;
42692
43042
  const agent = resolveIdentity(fromParam);
42693
43043
  agentFocus.set(agent, { project_id });
42694
- const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
42695
- db2.prepare("UPDATE agent_presence SET project_id = ? WHERE agent = ?").run(project_id, agent);
43044
+ setPresenceProject(agent, project_id);
42696
43045
  return {
42697
43046
  content: [{ type: "text", text: JSON.stringify({ agent, focused: true, project_id }) }]
42698
43047
  };
@@ -42727,8 +43076,7 @@ function registerAgentTools(server, agentFocus, getAgentFocus) {
42727
43076
  }, async (args) => {
42728
43077
  const agent = resolveIdentity(args.from);
42729
43078
  agentFocus.delete(agent);
42730
- const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
42731
- db2.prepare("UPDATE agent_presence SET project_id = NULL WHERE agent = ?").run(agent);
43079
+ setPresenceProject(agent, null);
42732
43080
  return {
42733
43081
  content: [{ type: "text", text: JSON.stringify({ agent, focused: false, project_id: null }) }]
42734
43082
  };
@@ -44315,7 +44663,7 @@ ${msg.text}`;
44315
44663
  // src/cli/commands/tmux.ts
44316
44664
  import { execSync } from "child_process";
44317
44665
  function sleep(ms) {
44318
- return new Promise((resolve) => setTimeout(resolve, ms));
44666
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
44319
44667
  }
44320
44668
  function countLines(message) {
44321
44669
  const matches = message.match(/\r?\n/g);
@@ -44359,7 +44707,7 @@ async function tmuxSend(target, message, opts = {}) {
44359
44707
 
44360
44708
  // src/mcp/tools/tmux.ts
44361
44709
  function sleep2(ms) {
44362
- return new Promise((resolve) => setTimeout(resolve, ms));
44710
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
44363
44711
  }
44364
44712
  function parseOptionalDelayMs(value) {
44365
44713
  return typeof value === "number" && value >= 0 ? value : undefined;
@@ -44460,7 +44808,7 @@ function registerTmuxTools(server) {
44460
44808
  // package.json
44461
44809
  var package_default = {
44462
44810
  name: "@hasna/conversations",
44463
- version: "0.2.42",
44811
+ version: "0.2.44",
44464
44812
  description: "Real-time CLI messaging for AI agents",
44465
44813
  type: "module",
44466
44814
  bin: {