@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/index.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);
@@ -2235,7 +2256,7 @@ var init_names = __esm(() => {
2235
2256
  ];
2236
2257
  });
2237
2258
 
2238
- // ../../../../node_modules/@hasna/cloud/dist/index.js
2259
+ // node_modules/@hasna/cloud/dist/index.js
2239
2260
  var exports_dist = {};
2240
2261
  __export(exports_dist, {
2241
2262
  translateSql: () => translateSql,
@@ -2330,11 +2351,11 @@ import { join as join5 } from "path";
2330
2351
  import { join as join6, dirname } from "path";
2331
2352
  import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
2332
2353
  import { homedir as homedir5, platform } from "os";
2333
- function __accessProp(key) {
2354
+ function __accessProp2(key) {
2334
2355
  return this[key];
2335
2356
  }
2336
- function __exportSetter(name, newValue) {
2337
- this[name] = __returnValue.bind(null, newValue);
2357
+ function __exportSetter2(name, newValue) {
2358
+ this[name] = __returnValue2.bind(null, newValue);
2338
2359
  }
2339
2360
  function translateSql(sql, dialect) {
2340
2361
  if (dialect === "sqlite")
@@ -3647,9 +3668,9 @@ async function syncTransfer(source, target, options, _direction) {
3647
3668
  const batch = rows.slice(offset, offset + batchSize);
3648
3669
  try {
3649
3670
  if (isAsyncAdapter(target)) {
3650
- await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
3671
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
3651
3672
  } else {
3652
- batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
3673
+ batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
3653
3674
  }
3654
3675
  result.rowsWritten += batch.length;
3655
3676
  } catch (err) {
@@ -3696,7 +3717,7 @@ async function syncTransfer(source, target, options, _direction) {
3696
3717
  }
3697
3718
  return results;
3698
3719
  }
3699
- async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
3720
+ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
3700
3721
  if (batch.length === 0)
3701
3722
  return;
3702
3723
  const colList = columns.map((c) => `"${c}"`).join(", ");
@@ -3706,22 +3727,20 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, ba
3706
3727
  }).join(", ");
3707
3728
  const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
3708
3729
  const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
3709
- const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
3710
3730
  const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
3711
- ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
3731
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
3712
3732
  const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
3713
3733
  await target.run(sql, ...params);
3714
3734
  }
3715
- function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
3735
+ function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
3716
3736
  if (batch.length === 0)
3717
3737
  return;
3718
3738
  const colList = columns.map((c) => `"${c}"`).join(", ");
3719
3739
  const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
3720
3740
  const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
3721
3741
  const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
3722
- const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
3723
3742
  const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
3724
- ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
3743
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
3725
3744
  const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
3726
3745
  target.run(sql, ...params);
3727
3746
  }
@@ -4791,7 +4810,7 @@ async function ensureAllPgDatabases() {
4791
4810
  }
4792
4811
  return results;
4793
4812
  }
4794
- function registerCloudTools(server, serviceName, opts = {}) {
4813
+ function registerCloudTools(server, serviceName) {
4795
4814
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
4796
4815
  const config = getCloudConfig();
4797
4816
  const lines = [
@@ -4824,13 +4843,8 @@ function registerCloudTools(server, serviceName, opts = {}) {
4824
4843
  isError: true
4825
4844
  };
4826
4845
  }
4827
- const local = new SqliteAdapter(opts.dbPath ?? getDbPath(serviceName));
4846
+ const local = new SqliteAdapter(getDbPath(serviceName));
4828
4847
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
4829
- if (opts.migrations?.length) {
4830
- for (const sql of opts.migrations) {
4831
- await cloud.run(sql);
4832
- }
4833
- }
4834
4848
  const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
4835
4849
  const results = await syncPush(local, cloud, { tables: tableList });
4836
4850
  local.close();
@@ -4852,7 +4866,7 @@ function registerCloudTools(server, serviceName, opts = {}) {
4852
4866
  isError: true
4853
4867
  };
4854
4868
  }
4855
- const local = new SqliteAdapter(opts.dbPath ?? getDbPath(serviceName));
4869
+ const local = new SqliteAdapter(getDbPath(serviceName));
4856
4870
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
4857
4871
  let tableList;
4858
4872
  if (tablesStr) {
@@ -4975,10 +4989,10 @@ function registerCloudCommands(program2, serviceName) {
4975
4989
  }
4976
4990
  });
4977
4991
  }
4978
- var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node, __toESMCache_esm, __toESM2 = (mod, isNodeMode, target) => {
4992
+ var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node2, __toESMCache_esm2, __toESM2 = (mod, isNodeMode, target) => {
4979
4993
  var canCache = mod != null && typeof mod === "object";
4980
4994
  if (canCache) {
4981
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
4995
+ var cache = isNodeMode ? __toESMCache_node2 ??= new WeakMap : __toESMCache_esm2 ??= new WeakMap;
4982
4996
  var cached = cache.get(mod);
4983
4997
  if (cached)
4984
4998
  return cached;
@@ -4988,19 +5002,19 @@ var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __t
4988
5002
  for (let key of __getOwnPropNames2(mod))
4989
5003
  if (!__hasOwnProp2.call(to, key))
4990
5004
  __defProp2(to, key, {
4991
- get: __accessProp.bind(mod, key),
5005
+ get: __accessProp2.bind(mod, key),
4992
5006
  enumerable: true
4993
5007
  });
4994
5008
  if (canCache)
4995
5009
  cache.set(mod, to);
4996
5010
  return to;
4997
- }, __commonJS2 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue = (v) => v, __export2 = (target, all) => {
5011
+ }, __commonJS2 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue2 = (v) => v, __export2 = (target, all) => {
4998
5012
  for (var name in all)
4999
5013
  __defProp2(target, name, {
5000
5014
  get: all[name],
5001
5015
  enumerable: true,
5002
5016
  configurable: true,
5003
- set: __exportSetter.bind(all, name)
5017
+ set: __exportSetter2.bind(all, name)
5004
5018
  });
5005
5019
  }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), __require2, 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_defaults, require_utils, require_utils_legacy, require_utils_webcrypto, require_utils2, 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_dist, 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, util, objectUtil, ZodParsedType, getParsedType = (data) => {
5006
5020
  const t = typeof data;
@@ -13357,6 +13371,137 @@ function getDbPath2() {
13357
13371
  return process.env.CONVERSATIONS_DB_PATH;
13358
13372
  return join7(getDataDir2(), "messages.db");
13359
13373
  }
13374
+ function parsePresenceTimestamp(value) {
13375
+ if (typeof value !== "string" || !value)
13376
+ return 0;
13377
+ return new Date(`${value}Z`).getTime() || 0;
13378
+ }
13379
+ function normalizePresenceText(value) {
13380
+ if (typeof value !== "string")
13381
+ return null;
13382
+ const normalized = value.trim();
13383
+ return normalized ? normalized : null;
13384
+ }
13385
+ function shouldRebuildAgentPresenceTable(columns) {
13386
+ const byName = new Map(columns.map((column) => [column.name, column]));
13387
+ const agentCol = byName.get("agent");
13388
+ const projectCol = byName.get("project_id");
13389
+ if (!agentCol)
13390
+ return false;
13391
+ if (!projectCol)
13392
+ return true;
13393
+ return agentCol.pk !== 1 || projectCol.pk !== 2 || projectCol.notnull !== 1 || byName.has("pid");
13394
+ }
13395
+ function rebuildLegacyAgentPresenceTable(db2) {
13396
+ const fallbackNow = db2.prepare("SELECT strftime('%Y-%m-%dT%H:%M:%f', 'now') AS now").get().now;
13397
+ const legacyRows = db2.prepare("SELECT rowid AS _rowid, * FROM agent_presence").all();
13398
+ legacyRows.sort((left, right) => {
13399
+ const lastSeenDelta = parsePresenceTimestamp(right.last_seen_at) - parsePresenceTimestamp(left.last_seen_at);
13400
+ if (lastSeenDelta !== 0)
13401
+ return lastSeenDelta;
13402
+ const createdDelta = parsePresenceTimestamp(right.created_at) - parsePresenceTimestamp(left.created_at);
13403
+ if (createdDelta !== 0)
13404
+ return createdDelta;
13405
+ const projectDelta = Number(Boolean(normalizePresenceText(right.project_id))) - Number(Boolean(normalizePresenceText(left.project_id)));
13406
+ if (projectDelta !== 0)
13407
+ return projectDelta;
13408
+ return right._rowid - left._rowid;
13409
+ });
13410
+ const dedupedRows = new Map;
13411
+ for (const row of legacyRows) {
13412
+ const normalizedAgent = normalizePresenceText(row.agent)?.toLowerCase();
13413
+ if (!normalizedAgent)
13414
+ continue;
13415
+ const storedProjectId = normalizePresenceText(row.project_id) ?? "";
13416
+ const dedupeKey = `${normalizedAgent}\x00${storedProjectId}`;
13417
+ if (dedupedRows.has(dedupeKey))
13418
+ continue;
13419
+ dedupedRows.set(dedupeKey, row);
13420
+ }
13421
+ db2.exec("BEGIN");
13422
+ try {
13423
+ db2.exec(`
13424
+ CREATE TABLE agent_presence_new (
13425
+ id TEXT NOT NULL,
13426
+ agent TEXT NOT NULL,
13427
+ session_id TEXT,
13428
+ role TEXT NOT NULL DEFAULT 'agent',
13429
+ project_id TEXT NOT NULL DEFAULT '',
13430
+ status TEXT NOT NULL DEFAULT 'online',
13431
+ last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
13432
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
13433
+ metadata TEXT,
13434
+ PRIMARY KEY (agent, project_id)
13435
+ )
13436
+ `);
13437
+ const insertPresence = db2.prepare(`
13438
+ INSERT INTO agent_presence_new (id, agent, session_id, role, project_id, status, last_seen_at, created_at, metadata)
13439
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
13440
+ `);
13441
+ for (const [dedupeKey, row] of dedupedRows) {
13442
+ const [agent, projectKey] = dedupeKey.split("\x00");
13443
+ const id = normalizePresenceText(row.id) ?? crypto.randomUUID().slice(0, 8);
13444
+ const sessionId = normalizePresenceText(row.session_id);
13445
+ const role = normalizePresenceText(row.role) ?? "agent";
13446
+ const projectId = projectKey;
13447
+ const status = normalizePresenceText(row.status) ?? "online";
13448
+ const lastSeenAt = normalizePresenceText(row.last_seen_at) ?? fallbackNow;
13449
+ const createdAt = normalizePresenceText(row.created_at) ?? lastSeenAt;
13450
+ const metadata = typeof row.metadata === "string" ? row.metadata : row.metadata == null ? null : JSON.stringify(row.metadata);
13451
+ insertPresence.run(id, agent, sessionId, role, projectId, status, lastSeenAt, createdAt, metadata);
13452
+ }
13453
+ db2.exec("DROP TABLE agent_presence");
13454
+ db2.exec("ALTER TABLE agent_presence_new RENAME TO agent_presence");
13455
+ db2.exec("COMMIT");
13456
+ } catch (error) {
13457
+ db2.exec("ROLLBACK");
13458
+ throw error;
13459
+ }
13460
+ }
13461
+ function collapseDuplicateAgentPresenceRows(db2) {
13462
+ const rows = db2.prepare("SELECT rowid AS _rowid, * FROM agent_presence").all();
13463
+ rows.sort((left, right) => {
13464
+ const lastSeenDelta = parsePresenceTimestamp(right.last_seen_at) - parsePresenceTimestamp(left.last_seen_at);
13465
+ if (lastSeenDelta !== 0)
13466
+ return lastSeenDelta;
13467
+ const createdDelta = parsePresenceTimestamp(right.created_at) - parsePresenceTimestamp(left.created_at);
13468
+ if (createdDelta !== 0)
13469
+ return createdDelta;
13470
+ const projectDelta = Number(Boolean(normalizePresenceText(right.project_id))) - Number(Boolean(normalizePresenceText(left.project_id)));
13471
+ if (projectDelta !== 0)
13472
+ return projectDelta;
13473
+ return right._rowid - left._rowid;
13474
+ });
13475
+ const rowIdsToDelete = [];
13476
+ const seenAgents = new Set;
13477
+ for (const row of rows) {
13478
+ const normalizedAgent = normalizePresenceText(row.agent)?.toLowerCase();
13479
+ if (!normalizedAgent)
13480
+ continue;
13481
+ if (seenAgents.has(normalizedAgent)) {
13482
+ rowIdsToDelete.push(row._rowid);
13483
+ continue;
13484
+ }
13485
+ seenAgents.add(normalizedAgent);
13486
+ }
13487
+ if (rowIdsToDelete.length === 0)
13488
+ return;
13489
+ db2.exec("BEGIN");
13490
+ try {
13491
+ const deleteRow = db2.prepare("DELETE FROM agent_presence WHERE rowid = ?");
13492
+ for (const rowId of rowIdsToDelete) {
13493
+ deleteRow.run(rowId);
13494
+ }
13495
+ db2.exec("COMMIT");
13496
+ } catch (error) {
13497
+ db2.exec("ROLLBACK");
13498
+ throw error;
13499
+ }
13500
+ }
13501
+ function ensureAgentPresenceAgentUniqueIndex(db2) {
13502
+ collapseDuplicateAgentPresenceRows(db2);
13503
+ db2.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_presence_agent_unique ON agent_presence(agent)");
13504
+ }
13360
13505
  function getDb() {
13361
13506
  if (db)
13362
13507
  return db;
@@ -13429,16 +13574,18 @@ function getDb() {
13429
13574
  db.exec(`
13430
13575
  CREATE TABLE IF NOT EXISTS agent_presence (
13431
13576
  id TEXT NOT NULL,
13432
- agent TEXT PRIMARY KEY,
13577
+ agent TEXT NOT NULL,
13433
13578
  session_id TEXT,
13434
13579
  role TEXT NOT NULL DEFAULT 'agent',
13435
- project_id TEXT,
13580
+ project_id TEXT NOT NULL DEFAULT '',
13436
13581
  status TEXT NOT NULL DEFAULT 'online',
13437
13582
  last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
13438
13583
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
13439
- metadata TEXT
13584
+ metadata TEXT,
13585
+ PRIMARY KEY (agent, project_id)
13440
13586
  )
13441
13587
  `);
13588
+ ensureAgentPresenceAgentUniqueIndex(db);
13442
13589
  db.exec(`
13443
13590
  CREATE TABLE IF NOT EXISTS resource_locks (
13444
13591
  resource_type TEXT NOT NULL,
@@ -13540,8 +13687,13 @@ function getDb() {
13540
13687
  db.exec("UPDATE messages SET uuid = lower(hex(randomblob(16))) WHERE uuid IS NULL");
13541
13688
  db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uuid ON messages(uuid)");
13542
13689
  }
13543
- const presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
13544
- const presenceColNames = presenceCols.map((c) => c.name);
13690
+ let presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
13691
+ let presenceColNames = presenceCols.map((c) => c.name);
13692
+ if (shouldRebuildAgentPresenceTable(presenceCols)) {
13693
+ rebuildLegacyAgentPresenceTable(db);
13694
+ presenceCols = db.prepare("PRAGMA table_info(agent_presence)").all();
13695
+ presenceColNames = presenceCols.map((c) => c.name);
13696
+ }
13545
13697
  if (!presenceColNames.includes("id")) {
13546
13698
  db.exec("ALTER TABLE agent_presence ADD COLUMN id TEXT NOT NULL DEFAULT ''");
13547
13699
  const rows = db.prepare("SELECT agent FROM agent_presence").all();
@@ -13562,7 +13714,9 @@ function getDb() {
13562
13714
  }
13563
13715
  if (!presenceColNames.includes("project_id")) {
13564
13716
  db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
13717
+ db.exec("UPDATE agent_presence SET project_id = '' WHERE project_id IS NULL");
13565
13718
  }
13719
+ ensureAgentPresenceAgentUniqueIndex(db);
13566
13720
  db.exec(`
13567
13721
  CREATE TABLE IF NOT EXISTS message_read_receipts (
13568
13722
  message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
@@ -13969,6 +14123,8 @@ var init_spaces = __esm(() => {
13969
14123
  // src/lib/webhooks.ts
13970
14124
  import { readFileSync as readFileSync5 } from "fs";
13971
14125
  import { join as join10 } from "path";
14126
+ import dns from "dns";
14127
+ import net from "net";
13972
14128
  function getConfigPath2() {
13973
14129
  return process.env.CONVERSATIONS_CONFIG_PATH || join10(getDataDir2(), "config.json");
13974
14130
  }
@@ -14000,6 +14156,44 @@ function matchesEvent(webhook, msg) {
14000
14156
  }
14001
14157
  return false;
14002
14158
  }
14159
+ function isPrivateIP(ip) {
14160
+ if (net.isIPv4(ip)) {
14161
+ const parts = ip.split(".").map(Number);
14162
+ if (parts[0] === 10)
14163
+ return true;
14164
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
14165
+ return true;
14166
+ if (parts[0] === 192 && parts[1] === 168)
14167
+ return true;
14168
+ if (parts[0] === 127)
14169
+ return true;
14170
+ if (parts[0] === 169 && parts[1] === 254)
14171
+ return true;
14172
+ if (parts[0] === 0)
14173
+ return true;
14174
+ } else if (net.isIPv6(ip)) {
14175
+ const lower = ip.toLowerCase();
14176
+ if (lower === "::1" || lower.startsWith("::ffff:") || lower.startsWith("fc") || lower.startsWith("fd"))
14177
+ return true;
14178
+ }
14179
+ return false;
14180
+ }
14181
+ async function validateWebhookUrl(urlStr) {
14182
+ try {
14183
+ const url = new URL(urlStr);
14184
+ if (url.protocol !== "https:")
14185
+ return false;
14186
+ const hostname2 = url.hostname;
14187
+ if (hostname2 === "localhost" || hostname2 === "0.0.0.0" || hostname2 === "127.0.0.1" || hostname2 === "::1")
14188
+ return false;
14189
+ const addresses = await dns.promises.lookup(hostname2, { all: true });
14190
+ if (!Array.isArray(addresses))
14191
+ return false;
14192
+ return !addresses.some((a) => isPrivateIP(a.address));
14193
+ } catch {
14194
+ return false;
14195
+ }
14196
+ }
14003
14197
  function fireWebhooks(msg) {
14004
14198
  const config = loadConfig();
14005
14199
  if (!config.webhooks || config.webhooks.length === 0)
@@ -14009,20 +14203,24 @@ function fireWebhooks(msg) {
14009
14203
  continue;
14010
14204
  if (!matchesEvent(webhook, msg))
14011
14205
  continue;
14012
- fetch(webhook.url, {
14013
- method: "POST",
14014
- headers: { "Content-Type": "application/json" },
14015
- body: JSON.stringify({
14016
- id: msg.id,
14017
- from: msg.from_agent,
14018
- to: msg.to_agent,
14019
- space: msg.space,
14020
- content: msg.content,
14021
- priority: msg.priority,
14022
- blocking: msg.blocking,
14023
- created_at: msg.created_at
14024
- })
14025
- }).catch(() => {});
14206
+ validateWebhookUrl(webhook.url).then((valid) => {
14207
+ if (!valid)
14208
+ return;
14209
+ fetch(webhook.url, {
14210
+ method: "POST",
14211
+ headers: { "Content-Type": "application/json" },
14212
+ body: JSON.stringify({
14213
+ id: msg.id,
14214
+ from: msg.from_agent,
14215
+ to: msg.to_agent,
14216
+ space: msg.space,
14217
+ content: msg.content,
14218
+ priority: msg.priority,
14219
+ blocking: msg.blocking,
14220
+ created_at: msg.created_at
14221
+ })
14222
+ }).catch(() => {});
14223
+ });
14026
14224
  }
14027
14225
  }
14028
14226
  var cachedConfig = null, configLoadedAt = 0, CONFIG_CACHE_MS = 1e4;
@@ -14032,8 +14230,8 @@ var init_webhooks = __esm(() => {
14032
14230
 
14033
14231
  // src/lib/messages.ts
14034
14232
  import { randomUUID } from "crypto";
14035
- import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
14036
- import { join as join11 } from "path";
14233
+ import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync3, statSync as statSync2, existsSync as existsSync9, realpathSync } from "fs";
14234
+ import { join as join11, basename, resolve } from "path";
14037
14235
  function compactMessage(msg) {
14038
14236
  const result = {};
14039
14237
  for (const key of Object.keys(msg)) {
@@ -14074,6 +14272,22 @@ function getAttachmentsDir() {
14074
14272
  return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
14075
14273
  return join11(getDataDir2(), "attachments");
14076
14274
  }
14275
+ function validateAttachment(sourcePath, name) {
14276
+ const absolute = resolve(sourcePath);
14277
+ if (!existsSync9(absolute)) {
14278
+ throw new Error(`Attachment source not found: ${sourcePath}`);
14279
+ }
14280
+ const real = realpathSync(absolute);
14281
+ const stat = statSync2(real);
14282
+ if (!stat.isFile()) {
14283
+ throw new Error(`Attachment source must be a regular file: ${sourcePath}`);
14284
+ }
14285
+ const safeName = basename(name.replace(/\0/g, ""));
14286
+ if (!safeName || safeName.startsWith(".")) {
14287
+ throw new Error(`Invalid attachment name: ${name}`);
14288
+ }
14289
+ return { safeSource: real, safeName };
14290
+ }
14077
14291
  function guessMimeType(name) {
14078
14292
  const ext = name.split(".").pop()?.toLowerCase();
14079
14293
  const mimeMap = {
@@ -14141,14 +14355,15 @@ function sendMessage(opts) {
14141
14355
  mkdirSync7(attachmentsDir, { recursive: true });
14142
14356
  const attachmentInfos = [];
14143
14357
  for (const att of opts.attachments) {
14144
- const destPath = join11(attachmentsDir, att.name);
14145
- copyFileSync3(att.source_path, destPath);
14358
+ const { safeSource, safeName } = validateAttachment(att.source_path, att.name);
14359
+ const destPath = join11(attachmentsDir, safeName);
14360
+ copyFileSync3(safeSource, destPath);
14146
14361
  const stat = statSync2(destPath);
14147
14362
  attachmentInfos.push({
14148
- name: att.name,
14363
+ name: safeName,
14149
14364
  path: destPath,
14150
14365
  size: stat.size,
14151
- mime_type: guessMimeType(att.name)
14366
+ mime_type: guessMimeType(safeName)
14152
14367
  });
14153
14368
  }
14154
14369
  const attachmentsJson = JSON.stringify(attachmentInfos);
@@ -14210,8 +14425,10 @@ function readMessages(opts = {}) {
14210
14425
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
14211
14426
  const resolvedLimit = isLatest ? Math.floor(opts.latest) : Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
14212
14427
  const order = isLatest ? "DESC" : opts.order?.toLowerCase() === "desc" ? "DESC" : "ASC";
14213
- const resolvedOffset = opts.offset && opts.offset > 0 ? Math.floor(opts.offset) : 0;
14214
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${resolvedLimit} OFFSET ${resolvedOffset}`).all(...params);
14428
+ const resolvedOffset = Number.isFinite(opts.offset) ? Math.floor(opts.offset) : 0;
14429
+ const safeLimit = Math.max(1, Math.min(resolvedLimit, 1e4));
14430
+ const safeOffset = Math.max(0, Math.floor(resolvedOffset));
14431
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY created_at ${order}, id ${order} LIMIT ${safeLimit} OFFSET ${safeOffset}`).all(...params);
14215
14432
  let messages = rows.map(parseMessage);
14216
14433
  if (opts.include_reply_counts && messages.length > 0) {
14217
14434
  const db22 = getDb();
@@ -14257,10 +14474,20 @@ function getMessageById(id) {
14257
14474
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
14258
14475
  return row ? parseMessage(row) : null;
14259
14476
  }
14260
- function markReadByIds(ids) {
14477
+ function markReadByIds(ids, agent) {
14261
14478
  const db2 = getDb();
14262
14479
  if (ids.length === 0)
14263
14480
  return 0;
14481
+ if (agent) {
14482
+ const stmt2 = db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
14483
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`);
14484
+ const normalized = agent.toLowerCase();
14485
+ for (const id of ids)
14486
+ stmt2.run(id, normalized);
14487
+ const placeholders2 = ids.map(() => "?").join(", ");
14488
+ 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`);
14489
+ return update.run(...ids).changes;
14490
+ }
14264
14491
  const placeholders = ids.map(() => "?").join(", ");
14265
14492
  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`);
14266
14493
  const result = stmt.run(...ids);
@@ -14408,8 +14635,9 @@ function getPinnedMessages(opts) {
14408
14635
  params.push(opts.session_id);
14409
14636
  }
14410
14637
  const where = `WHERE ${conditions.join(" AND ")}`;
14411
- const limit = Number.isFinite(opts?.limit) && opts.limit > 0 ? `LIMIT ${Math.floor(opts.limit)}` : "";
14412
- const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY pinned_at DESC, id DESC ${limit}`).all(...params);
14638
+ const safeLimit = Number.isFinite(opts?.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 0;
14639
+ const limitClause = safeLimit > 0 ? `LIMIT ${safeLimit}` : "";
14640
+ const rows = db2.prepare(`SELECT * FROM messages ${where} ORDER BY pinned_at DESC, id DESC ${limitClause}`).all(...params);
14413
14641
  return rows.map(parseMessage);
14414
14642
  }
14415
14643
  function getUnreadBlockers(agent) {
@@ -14598,11 +14826,11 @@ function getMessagesForAgent(agent, opts) {
14598
14826
  if (opts?.unread_only) {
14599
14827
  conditions.push("mm.notified_at IS NULL");
14600
14828
  }
14601
- const limit = opts?.limit ?? 50;
14829
+ const safeLimit = Math.max(1, Math.min(Math.floor(opts?.limit ?? 50), 1000));
14602
14830
  const rows = db2.prepare(`SELECT m.*, mm.id AS mention_id FROM messages m
14603
14831
  JOIN message_mentions mm ON mm.message_id = m.id
14604
14832
  WHERE ${conditions.join(" AND ")}
14605
- ORDER BY m.created_at DESC LIMIT ${limit}`).all(...params);
14833
+ ORDER BY m.created_at DESC LIMIT ${safeLimit}`).all(...params);
14606
14834
  return rows.map(({ mention_id, ...row }) => ({ message: parseMessage(row), mention_id }));
14607
14835
  }
14608
14836
  function markMentionsRead(agent, space) {
@@ -14797,6 +15025,33 @@ var renderInline = (text) => {
14797
15025
  var init_terminal_markdown = () => {};
14798
15026
 
14799
15027
  // src/lib/presence.ts
15028
+ function normalizeAgentName(name) {
15029
+ return name.trim().toLowerCase();
15030
+ }
15031
+ function toStoredProjectId(projectId) {
15032
+ const normalized = projectId?.trim() ?? "";
15033
+ return normalized || "";
15034
+ }
15035
+ function fromStoredProjectId(projectId) {
15036
+ const normalized = typeof projectId === "string" ? projectId.trim() : "";
15037
+ return normalized || null;
15038
+ }
15039
+ function getPresenceByAgent(db2, agent) {
15040
+ return db2.prepare(`
15041
+ SELECT * FROM agent_presence
15042
+ WHERE LOWER(agent) = ?
15043
+ ORDER BY last_seen_at DESC
15044
+ LIMIT 1
15045
+ `).get(agent);
15046
+ }
15047
+ function getPresenceByAgentAndProject(db2, agent, projectId) {
15048
+ return db2.prepare(`
15049
+ SELECT * FROM agent_presence
15050
+ WHERE LOWER(agent) = ? AND COALESCE(project_id, '') = ?
15051
+ ORDER BY last_seen_at DESC
15052
+ LIMIT 1
15053
+ `).get(agent, projectId);
15054
+ }
14800
15055
  function parsePresence(row) {
14801
15056
  let metadata = null;
14802
15057
  if (row.metadata) {
@@ -14815,7 +15070,7 @@ function parsePresence(row) {
14815
15070
  agent: row.agent,
14816
15071
  session_id: row.session_id ?? null,
14817
15072
  role: row.role || "agent",
14818
- project_id: row.project_id ?? null,
15073
+ project_id: fromStoredProjectId(row.project_id),
14819
15074
  status: row.status,
14820
15075
  last_seen_at: lastSeenAt,
14821
15076
  created_at: row.created_at || lastSeenAt,
@@ -14833,9 +15088,10 @@ function isAgentConflict(result) {
14833
15088
  }
14834
15089
  function registerAgent(name, sessionId, role, projectId) {
14835
15090
  const db2 = getDb();
14836
- const normalizedName = name.trim().toLowerCase();
15091
+ const normalizedName = normalizeAgentName(name);
14837
15092
  const result = db2.transaction(() => {
14838
- const existing = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
15093
+ const existing = getPresenceByAgent(db2, normalizedName);
15094
+ const storedProjectId = toStoredProjectId(projectId ?? existing?.project_id);
14839
15095
  if (existing) {
14840
15096
  const lastSeenAt = existing.last_seen_at;
14841
15097
  const existingSessionId = existing.session_id;
@@ -14853,12 +15109,26 @@ function registerAgent(name, sessionId, role, projectId) {
14853
15109
  };
14854
15110
  }
14855
15111
  const tookOver = existingSessionId !== sessionId;
14856
- db2.prepare(`
14857
- UPDATE agent_presence
14858
- SET session_id = ?, role = ?, project_id = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
14859
- WHERE agent = ?
14860
- `).run(sessionId, role || existing.role || "agent", projectId ?? existing.project_id ?? null, normalizedName);
14861
- const updated = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
15112
+ const existingId = existing.id;
15113
+ const target = getPresenceByAgentAndProject(db2, normalizedName, storedProjectId);
15114
+ if (target && target.id !== existingId) {
15115
+ db2.prepare(`
15116
+ UPDATE agent_presence
15117
+ SET session_id = ?, role = ?, status = 'online', last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
15118
+ WHERE id = ?
15119
+ `).run(sessionId, role || existing.role || "agent", target.id);
15120
+ db2.prepare("DELETE FROM agent_presence WHERE id = ?").run(existingId);
15121
+ } else {
15122
+ db2.prepare(`
15123
+ UPDATE agent_presence
15124
+ SET session_id = ?, role = ?, project_id = ?, status = 'online', last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
15125
+ WHERE id = ?
15126
+ `).run(sessionId, role || existing.role || "agent", storedProjectId, existingId);
15127
+ }
15128
+ const updated = getPresenceByAgentAndProject(db2, normalizedName, storedProjectId) ?? getPresenceByAgent(db2, normalizedName);
15129
+ if (!updated) {
15130
+ throw new Error(`Failed to update presence for agent "${normalizedName}"`);
15131
+ }
14862
15132
  return { agent: parsePresence(updated), created: false, took_over: tookOver };
14863
15133
  }
14864
15134
  const id = crypto.randomUUID().slice(0, 8);
@@ -14866,33 +15136,60 @@ function registerAgent(name, sessionId, role, projectId) {
14866
15136
  db2.prepare(`
14867
15137
  INSERT INTO agent_presence (id, agent, session_id, role, project_id, status, last_seen_at, created_at)
14868
15138
  VALUES (?, ?, ?, ?, ?, 'online', strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'))
14869
- `).run(id, normalizedName, sessionId, resolvedRole, projectId ?? null);
14870
- const created = db2.prepare("SELECT * FROM agent_presence WHERE agent = ?").get(normalizedName);
15139
+ `).run(id, normalizedName, sessionId, resolvedRole, storedProjectId);
15140
+ const created = getPresenceByAgentAndProject(db2, normalizedName, storedProjectId) ?? getPresenceByAgent(db2, normalizedName);
15141
+ if (!created) {
15142
+ throw new Error(`Failed to create presence for agent "${normalizedName}"`);
15143
+ }
14871
15144
  return { agent: parsePresence(created), created: true, took_over: false };
14872
15145
  });
14873
15146
  return result;
14874
15147
  }
14875
- function heartbeat(agent, status, metadata, sessionId) {
15148
+ function heartbeat(agent, status, metadata, sessionId, projectId) {
14876
15149
  const db2 = getDb();
14877
15150
  const metadataJson = metadata ? JSON.stringify(metadata) : null;
14878
15151
  const resolvedStatus = status || "online";
14879
- const normalizedAgent = agent.trim().toLowerCase();
14880
- const existing = db2.prepare("SELECT id FROM agent_presence WHERE agent = ?").get(agent);
14881
- const id = existing?.id || crypto.randomUUID().slice(0, 8);
14882
- db2.prepare(`
14883
- INSERT INTO agent_presence (id, agent, session_id, role, status, last_seen_at, created_at, metadata)
14884
- VALUES (?, ?, ?, 'agent', ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
14885
- ON CONFLICT(agent) DO UPDATE SET
14886
- status = excluded.status,
14887
- last_seen_at = excluded.last_seen_at,
14888
- session_id = COALESCE(excluded.session_id, agent_presence.session_id),
14889
- metadata = excluded.metadata
14890
- `).run(id, normalizedAgent, sessionId ?? null, resolvedStatus, metadataJson);
15152
+ const normalizedAgent = normalizeAgentName(agent);
15153
+ db2.transaction(() => {
15154
+ const existing = getPresenceByAgent(db2, normalizedAgent);
15155
+ const storedProjectId = toStoredProjectId(projectId ?? existing?.project_id);
15156
+ const id = existing?.id || crypto.randomUUID().slice(0, 8);
15157
+ if (existing) {
15158
+ const existingId = existing.id;
15159
+ const target = getPresenceByAgentAndProject(db2, normalizedAgent, storedProjectId);
15160
+ if (target && target.id !== existingId) {
15161
+ db2.prepare(`
15162
+ UPDATE agent_presence
15163
+ SET status = ?,
15164
+ last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now'),
15165
+ session_id = COALESCE(?, session_id),
15166
+ metadata = ?
15167
+ WHERE id = ?
15168
+ `).run(resolvedStatus, sessionId ?? null, metadataJson, target.id);
15169
+ db2.prepare("DELETE FROM agent_presence WHERE id = ?").run(existingId);
15170
+ return;
15171
+ }
15172
+ db2.prepare(`
15173
+ UPDATE agent_presence
15174
+ SET status = ?,
15175
+ last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now'),
15176
+ session_id = COALESCE(?, session_id),
15177
+ metadata = ?,
15178
+ project_id = ?
15179
+ WHERE id = ?
15180
+ `).run(resolvedStatus, sessionId ?? null, metadataJson, storedProjectId, existingId);
15181
+ return;
15182
+ }
15183
+ db2.prepare(`
15184
+ INSERT INTO agent_presence (id, agent, session_id, role, project_id, status, last_seen_at, created_at, metadata)
15185
+ VALUES (?, ?, ?, 'agent', ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
15186
+ `).run(id, normalizedAgent, sessionId ?? null, storedProjectId, resolvedStatus, metadataJson);
15187
+ });
14891
15188
  }
14892
15189
  function getPresence(agent) {
14893
15190
  const db2 = getDb();
14894
- const normalizedAgent = agent.trim().toLowerCase();
14895
- const row = db2.prepare("SELECT * FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedAgent);
15191
+ const normalizedAgent = normalizeAgentName(agent);
15192
+ const row = getPresenceByAgent(db2, normalizedAgent);
14896
15193
  return row ? parsePresence(row) : null;
14897
15194
  }
14898
15195
  function listAgents(opts) {
@@ -14907,14 +15204,14 @@ function listAgents(opts) {
14907
15204
  }
14908
15205
  function removePresence(agent) {
14909
15206
  const db2 = getDb();
14910
- const normalizedAgent = agent.trim().toLowerCase();
15207
+ const normalizedAgent = normalizeAgentName(agent);
14911
15208
  const result = db2.prepare("DELETE FROM agent_presence WHERE LOWER(agent) = ?").run(normalizedAgent);
14912
15209
  return result.changes > 0;
14913
15210
  }
14914
15211
  function renameAgent(oldName, newName) {
14915
15212
  const db2 = getDb();
14916
- const normalizedOld = oldName.trim().toLowerCase();
14917
- const normalizedNew = newName.trim().toLowerCase();
15213
+ const normalizedOld = normalizeAgentName(oldName);
15214
+ const normalizedNew = normalizeAgentName(newName);
14918
15215
  const existing = db2.prepare("SELECT agent FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedOld);
14919
15216
  if (!existing)
14920
15217
  return false;
@@ -14924,6 +15221,40 @@ function renameAgent(oldName, newName) {
14924
15221
  db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
14925
15222
  return true;
14926
15223
  }
15224
+ function setPresenceProject(agent, projectId) {
15225
+ const db2 = getDb();
15226
+ const normalizedAgent = normalizeAgentName(agent);
15227
+ const desiredProjectId = toStoredProjectId(projectId);
15228
+ const latest = getPresenceByAgent(db2, normalizedAgent);
15229
+ if (!latest) {
15230
+ heartbeat(normalizedAgent, "online", undefined, undefined, projectId);
15231
+ return;
15232
+ }
15233
+ const currentProjectId = toStoredProjectId(latest.project_id);
15234
+ if (currentProjectId === desiredProjectId)
15235
+ return;
15236
+ db2.transaction(() => {
15237
+ const latestId = latest.id;
15238
+ const target = getPresenceByAgentAndProject(db2, normalizedAgent, desiredProjectId);
15239
+ if (target && target.id !== latestId) {
15240
+ db2.prepare(`
15241
+ UPDATE agent_presence
15242
+ SET status = ?,
15243
+ last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now'),
15244
+ session_id = COALESCE(?, session_id),
15245
+ metadata = COALESCE(?, metadata)
15246
+ WHERE LOWER(agent) = ? AND COALESCE(project_id, '') = ?
15247
+ `).run(latest.status || target.status || "online", latest.session_id ?? null, latest.metadata ?? null, normalizedAgent, desiredProjectId);
15248
+ db2.prepare("DELETE FROM agent_presence WHERE id = ?").run(latestId);
15249
+ return;
15250
+ }
15251
+ db2.prepare(`
15252
+ UPDATE agent_presence
15253
+ SET project_id = ?, last_seen_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
15254
+ WHERE id = ?
15255
+ `).run(desiredProjectId, latestId);
15256
+ });
15257
+ }
14927
15258
  var ONLINE_THRESHOLD_SECONDS = 60, CONFLICT_THRESHOLD_SECONDS;
14928
15259
  var init_presence = __esm(() => {
14929
15260
  init_db();
@@ -14934,7 +15265,7 @@ var init_presence = __esm(() => {
14934
15265
  var require_package = __commonJS((exports, module) => {
14935
15266
  module.exports = {
14936
15267
  name: "@hasna/conversations",
14937
- version: "0.2.42",
15268
+ version: "0.2.44",
14938
15269
  description: "Real-time CLI messaging for AI agents",
14939
15270
  type: "module",
14940
15271
  bin: {
@@ -15732,7 +16063,7 @@ var init_graph = __esm(() => {
15732
16063
  import chalk9 from "chalk";
15733
16064
  import { execSync } from "child_process";
15734
16065
  function sleep(ms) {
15735
- return new Promise((resolve) => setTimeout(resolve, ms));
16066
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
15736
16067
  }
15737
16068
  function countLines(message) {
15738
16069
  const matches = message.match(/\r?\n/g);
@@ -36508,7 +36839,7 @@ class Protocol {
36508
36839
  return;
36509
36840
  }
36510
36841
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
36511
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
36842
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
36512
36843
  options?.signal?.throwIfAborted();
36513
36844
  }
36514
36845
  } catch (error48) {
@@ -36520,7 +36851,7 @@ class Protocol {
36520
36851
  }
36521
36852
  request(request, resultSchema, options) {
36522
36853
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
36523
- return new Promise((resolve, reject) => {
36854
+ return new Promise((resolve2, reject) => {
36524
36855
  const earlyReject = (error48) => {
36525
36856
  reject(error48);
36526
36857
  };
@@ -36598,7 +36929,7 @@ class Protocol {
36598
36929
  if (!parseResult.success) {
36599
36930
  reject(parseResult.error);
36600
36931
  } else {
36601
- resolve(parseResult.data);
36932
+ resolve2(parseResult.data);
36602
36933
  }
36603
36934
  } catch (error48) {
36604
36935
  reject(error48);
@@ -36789,12 +37120,12 @@ class Protocol {
36789
37120
  interval = task.pollInterval;
36790
37121
  }
36791
37122
  } catch {}
36792
- return new Promise((resolve, reject) => {
37123
+ return new Promise((resolve2, reject) => {
36793
37124
  if (signal.aborted) {
36794
37125
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
36795
37126
  return;
36796
37127
  }
36797
- const timeoutId = setTimeout(resolve, interval);
37128
+ const timeoutId = setTimeout(resolve2, interval);
36798
37129
  signal.addEventListener("abort", () => {
36799
37130
  clearTimeout(timeoutId);
36800
37131
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -39779,7 +40110,7 @@ var require_compile = __commonJS((exports) => {
39779
40110
  const schOrFunc = root.refs[ref];
39780
40111
  if (schOrFunc)
39781
40112
  return schOrFunc;
39782
- let _sch = resolve.call(this, root, ref);
40113
+ let _sch = resolve2.call(this, root, ref);
39783
40114
  if (_sch === undefined) {
39784
40115
  const schema = (_a2 = root.localRefs) === null || _a2 === undefined ? undefined : _a2[ref];
39785
40116
  const { schemaId } = this.opts;
@@ -39806,7 +40137,7 @@ var require_compile = __commonJS((exports) => {
39806
40137
  function sameSchemaEnv(s1, s2) {
39807
40138
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
39808
40139
  }
39809
- function resolve(root, ref) {
40140
+ function resolve2(root, ref) {
39810
40141
  let sch;
39811
40142
  while (typeof (sch = this.refs[ref]) == "string")
39812
40143
  ref = sch;
@@ -40336,7 +40667,7 @@ var require_fast_uri = __commonJS((exports, module) => {
40336
40667
  }
40337
40668
  return uri;
40338
40669
  }
40339
- function resolve(baseURI, relativeURI, options) {
40670
+ function resolve2(baseURI, relativeURI, options) {
40340
40671
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
40341
40672
  const resolved = resolveComponent(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
40342
40673
  schemelessOptions.skipEscape = true;
@@ -40564,7 +40895,7 @@ var require_fast_uri = __commonJS((exports, module) => {
40564
40895
  var fastUri = {
40565
40896
  SCHEMES,
40566
40897
  normalize,
40567
- resolve,
40898
+ resolve: resolve2,
40568
40899
  resolveComponent,
40569
40900
  equal,
40570
40901
  serialize,
@@ -43152,7 +43483,7 @@ var require_formats = __commonJS((exports) => {
43152
43483
  }
43153
43484
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
43154
43485
  function getTime(strictTimeZone) {
43155
- return function time(str) {
43486
+ return function time3(str) {
43156
43487
  const matches = TIME.exec(str);
43157
43488
  if (!matches)
43158
43489
  return false;
@@ -44090,7 +44421,7 @@ class McpServer {
44090
44421
  let task = createTaskResult.task;
44091
44422
  const pollInterval = task.pollInterval ?? 5000;
44092
44423
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
44093
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
44424
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
44094
44425
  const updatedTask = await extra.taskStore.getTask(taskId);
44095
44426
  if (!updatedTask) {
44096
44427
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -44714,12 +45045,12 @@ class StdioServerTransport {
44714
45045
  this.onclose?.();
44715
45046
  }
44716
45047
  send(message) {
44717
- return new Promise((resolve) => {
45048
+ return new Promise((resolve2) => {
44718
45049
  const json2 = serializeMessage(message);
44719
45050
  if (this._stdout.write(json2)) {
44720
- resolve();
45051
+ resolve2();
44721
45052
  } else {
44722
- this._stdout.once("drain", resolve);
45053
+ this._stdout.once("drain", resolve2);
44723
45054
  }
44724
45055
  });
44725
45056
  }
@@ -44730,6 +45061,7 @@ var init_stdio2 = __esm(() => {
44730
45061
 
44731
45062
  // src/mcp/tools/messaging.ts
44732
45063
  function registerMessagingTools(server, resolveProjectId) {
45064
+ const _sessionInjectRate = new Map;
44733
45065
  server.registerTool("send_message", {
44734
45066
  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.",
44735
45067
  inputSchema: {
@@ -44769,6 +45101,22 @@ function registerMessagingTools(server, resolveProjectId) {
44769
45101
  }, async (args) => {
44770
45102
  const { target_session_id, content, from: fromParam, priority } = args;
44771
45103
  const from = resolveIdentity(fromParam);
45104
+ const rateKey = `session:${from}:${target_session_id}`;
45105
+ const now = Date.now();
45106
+ const windowMs = 60000;
45107
+ const maxPerWindow = 10;
45108
+ const rateEntry = _sessionInjectRate.get(rateKey);
45109
+ if (rateEntry && now - rateEntry.start < windowMs && rateEntry.count >= maxPerWindow) {
45110
+ return {
45111
+ content: [{ type: "text", text: `Rate limit: max ${maxPerWindow} session injections per minute. Try again soon.` }],
45112
+ isError: true
45113
+ };
45114
+ }
45115
+ if (!rateEntry || now - rateEntry.start >= windowMs) {
45116
+ _sessionInjectRate.set(rateKey, { count: 1, start: now });
45117
+ } else {
45118
+ rateEntry.count++;
45119
+ }
44772
45120
  const msg = sendMessage({
44773
45121
  from,
44774
45122
  to: `session:${target_session_id}`,
@@ -44806,7 +45154,7 @@ function registerMessagingTools(server, resolveProjectId) {
44806
45154
  project_id: args.project_id ?? resolveProjectId(undefined, agent)
44807
45155
  });
44808
45156
  if (args.mark_read !== false && messages.length > 0) {
44809
- markReadByIds(messages.map((m) => m.id));
45157
+ markReadByIds(messages.map((m) => m.id), agent);
44810
45158
  }
44811
45159
  return {
44812
45160
  content: [{ type: "text", text: JSON.stringify({ messages, count: messages.length, offset: args.offset ?? 0 }) }]
@@ -45850,8 +46198,7 @@ function registerAgentTools(server, agentFocus, getAgentFocus) {
45850
46198
  const { project_id, from: fromParam } = args;
45851
46199
  const agent = resolveIdentity(fromParam);
45852
46200
  agentFocus.set(agent, { project_id });
45853
- const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
45854
- db2.prepare("UPDATE agent_presence SET project_id = ? WHERE agent = ?").run(project_id, agent);
46201
+ setPresenceProject(agent, project_id);
45855
46202
  return {
45856
46203
  content: [{ type: "text", text: JSON.stringify({ agent, focused: true, project_id }) }]
45857
46204
  };
@@ -45886,8 +46233,7 @@ function registerAgentTools(server, agentFocus, getAgentFocus) {
45886
46233
  }, async (args) => {
45887
46234
  const agent = resolveIdentity(args.from);
45888
46235
  agentFocus.delete(agent);
45889
- const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
45890
- db2.prepare("UPDATE agent_presence SET project_id = NULL WHERE agent = ?").run(agent);
46236
+ setPresenceProject(agent, null);
45891
46237
  return {
45892
46238
  content: [{ type: "text", text: JSON.stringify({ agent, focused: false, project_id: null }) }]
45893
46239
  };
@@ -46674,15 +47020,17 @@ var init_pg_migrations = __esm(() => {
46674
47020
 
46675
47021
  CREATE TABLE IF NOT EXISTS agent_presence (
46676
47022
  id TEXT NOT NULL DEFAULT '',
46677
- agent TEXT PRIMARY KEY,
47023
+ agent TEXT NOT NULL,
46678
47024
  session_id TEXT,
46679
47025
  role TEXT NOT NULL DEFAULT 'agent',
46680
- project_id TEXT,
47026
+ project_id TEXT NOT NULL DEFAULT '',
46681
47027
  status TEXT NOT NULL DEFAULT 'online',
46682
47028
  last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
46683
47029
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
46684
- metadata TEXT
47030
+ metadata TEXT,
47031
+ PRIMARY KEY (agent, project_id)
46685
47032
  );
47033
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_presence_agent_unique ON agent_presence(agent);
46686
47034
 
46687
47035
  CREATE TABLE IF NOT EXISTS resource_locks (
46688
47036
  resource_type TEXT NOT NULL,
@@ -47160,7 +47508,7 @@ var init_telegram_channel = __esm(() => {
47160
47508
 
47161
47509
  // src/mcp/tools/tmux.ts
47162
47510
  function sleep2(ms) {
47163
- return new Promise((resolve) => setTimeout(resolve, ms));
47511
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
47164
47512
  }
47165
47513
  function parseOptionalDelayMs(value) {
47166
47514
  return typeof value === "number" && value >= 0 ? value : undefined;
@@ -47328,8 +47676,8 @@ var exports_serve = {};
47328
47676
  __export(exports_serve, {
47329
47677
  startDashboardServer: () => startDashboardServer
47330
47678
  });
47331
- import { join as join14, resolve, sep } from "path";
47332
- import { existsSync as existsSync10 } from "fs";
47679
+ import { join as join14, resolve as resolve2, sep } from "path";
47680
+ import { existsSync as existsSync11 } from "fs";
47333
47681
  function securityHeaders(base) {
47334
47682
  const headers = new Headers(base);
47335
47683
  if (!headers.has("X-Content-Type-Options"))
@@ -47427,7 +47775,7 @@ function startDashboardServer(port = 0, host) {
47427
47775
  const resolvedPort = normalizePort(port, 0);
47428
47776
  const resolvedHost = normalizeHost(host ?? process.env.CONVERSATIONS_DASHBOARD_HOST);
47429
47777
  const dashboardDist = join14(import.meta.dir, "../../dashboard/dist");
47430
- const hasDist = existsSync10(dashboardDist);
47778
+ const hasDist = existsSync11(dashboardDist);
47431
47779
  const server2 = Bun.serve({
47432
47780
  port: resolvedPort,
47433
47781
  hostname: resolvedHost,
@@ -47835,9 +48183,9 @@ function startDashboardServer(port = 0, host) {
47835
48183
  }
47836
48184
  }
47837
48185
  if (hasDist) {
47838
- const baseDir = resolve(dashboardDist);
48186
+ const baseDir = resolve2(dashboardDist);
47839
48187
  const safePath = path === "/" ? "index.html" : path.replace(/^\/+/, "");
47840
- const filePath = resolve(baseDir, safePath);
48188
+ const filePath = resolve2(baseDir, safePath);
47841
48189
  if (!filePath.startsWith(baseDir + sep)) {
47842
48190
  return new Response("Not Found", { status: 404 });
47843
48191
  }
@@ -48906,13 +49254,13 @@ var gatherTrainingData = async (options = {}) => {
48906
49254
 
48907
49255
  // src/lib/model-config.ts
48908
49256
  init_db();
48909
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync8, existsSync as existsSync9 } from "fs";
49257
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync8, existsSync as existsSync10 } from "fs";
48910
49258
  import { join as join12 } from "path";
48911
49259
  var DEFAULT_MODEL = "gpt-4o-mini";
48912
49260
  var CONFIG_DIR3 = getDataDir2();
48913
49261
  var CONFIG_PATH2 = join12(CONFIG_DIR3, "config.json");
48914
49262
  function readConfig() {
48915
- if (!existsSync9(CONFIG_PATH2))
49263
+ if (!existsSync10(CONFIG_PATH2))
48916
49264
  return {};
48917
49265
  try {
48918
49266
  return JSON.parse(readFileSync6(CONFIG_PATH2, "utf-8"));
@@ -48921,7 +49269,7 @@ function readConfig() {
48921
49269
  }
48922
49270
  }
48923
49271
  function writeConfig(config) {
48924
- if (!existsSync9(CONFIG_DIR3)) {
49272
+ if (!existsSync10(CONFIG_DIR3)) {
48925
49273
  mkdirSync8(CONFIG_DIR3, { recursive: true });
48926
49274
  }
48927
49275
  writeFileSync5(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
@@ -50140,7 +50488,7 @@ function registerAgentCommands(program2) {
50140
50488
  console.error(chalk7.red(`Project "${projectArg}" not found.`));
50141
50489
  process.exit(1);
50142
50490
  }
50143
- getDb().prepare("UPDATE agent_presence SET project_id = ? WHERE agent = ?").run(project.id, agent);
50491
+ setPresenceProject(agent, project.id);
50144
50492
  if (opts.json) {
50145
50493
  console.log(JSON.stringify({ agent, project_id: project.id, project_name: project.name, focused: true }));
50146
50494
  } else {
@@ -50150,7 +50498,7 @@ function registerAgentCommands(program2) {
50150
50498
  });
50151
50499
  focus.command("clear").description("Clear your project focus").option("--from <agent>", "Agent identity").option("-j, --json", "Output as JSON").action((opts) => {
50152
50500
  const agent = resolveIdentity(opts.from);
50153
- getDb().prepare("UPDATE agent_presence SET project_id = NULL WHERE agent = ?").run(agent);
50501
+ setPresenceProject(agent, null);
50154
50502
  if (opts.json) {
50155
50503
  console.log(JSON.stringify({ agent, project_id: null, focused: false }));
50156
50504
  } else {