@hasna/conversations 0.2.1 → 0.2.2

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);
@@ -28879,12 +28900,64 @@ function getMessageById(id) {
28879
28900
  const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
28880
28901
  return row ? parseMessage(row) : null;
28881
28902
  }
28903
+ function markReadByIds(ids) {
28904
+ const db2 = getDb();
28905
+ if (ids.length === 0)
28906
+ return 0;
28907
+ const placeholders = ids.map(() => "?").join(", ");
28908
+ 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`);
28909
+ const result = stmt.run(...ids);
28910
+ return result.changes;
28911
+ }
28882
28912
  function markAllRead(agent) {
28883
28913
  const db2 = getDb();
28884
28914
  const stmt = db2.prepare(`UPDATE messages SET read_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') WHERE to_agent = ? AND read_at IS NULL`);
28885
28915
  const result = stmt.run(agent);
28886
28916
  return result.changes;
28887
28917
  }
28918
+ function readDigest(opts = {}) {
28919
+ const db2 = getDb();
28920
+ const countConditions = ["read_at IS NULL"];
28921
+ const countParams = [];
28922
+ if (opts.space) {
28923
+ countConditions.push("space = ?");
28924
+ countParams.push(opts.space);
28925
+ }
28926
+ if (opts.session_id) {
28927
+ countConditions.push("session_id = ?");
28928
+ countParams.push(opts.session_id);
28929
+ }
28930
+ if (opts.to) {
28931
+ countConditions.push("to_agent = ?");
28932
+ countParams.push(opts.to);
28933
+ }
28934
+ if (opts.since) {
28935
+ countConditions.push("created_at > ?");
28936
+ countParams.push(opts.since);
28937
+ }
28938
+ if (opts.project_id) {
28939
+ countConditions.push("project_id = ?");
28940
+ countParams.push(opts.project_id);
28941
+ }
28942
+ const countWhere = `WHERE ${countConditions.join(" AND ")}`;
28943
+ const totalUnread = db2.prepare(`SELECT COUNT(*) as n FROM messages ${countWhere}`).get(...countParams).n;
28944
+ const messages = readMessages({ ...opts, unread_only: opts.unread_only ?? true });
28945
+ if (messages.length > 0) {
28946
+ markReadByIds(messages.map((m) => m.id));
28947
+ }
28948
+ const digest = messages.map((m) => ({
28949
+ id: m.id,
28950
+ from: m.from_agent,
28951
+ created_at: m.created_at,
28952
+ preview: m.content.slice(0, 100) + (m.content.length > 100 ? "\u2026" : ""),
28953
+ priority: m.priority,
28954
+ has_attachments: Array.isArray(m.attachments) && m.attachments.length > 0,
28955
+ space: m.space,
28956
+ to: m.to_agent,
28957
+ unread: !m.read_at
28958
+ }));
28959
+ return { messages: digest, total_unread: totalUnread, shown: digest.length };
28960
+ }
28888
28961
  function escapeCsvField(value) {
28889
28962
  if (value === null || value === undefined)
28890
28963
  return "";
@@ -30050,10 +30123,12 @@ function getReactionSummary(messageId) {
30050
30123
  // src/lib/locks.ts
30051
30124
  init_db();
30052
30125
  var DEFAULT_LOCK_EXPIRY_MS = 5 * 60 * 1000;
30126
+ var STALE_HEARTBEAT_SECONDS = 30 * 60;
30053
30127
  function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", expiryMs = DEFAULT_LOCK_EXPIRY_MS) {
30054
30128
  const db2 = getDb();
30055
30129
  return db2.transaction(() => {
30056
30130
  cleanExpiredLocks();
30131
+ releaseStaleAgentLocks();
30057
30132
  const existing = db2.prepare(`
30058
30133
  SELECT * FROM resource_locks
30059
30134
  WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
@@ -30080,6 +30155,55 @@ function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", e
30080
30155
  return { acquired: true, lock };
30081
30156
  }).immediate();
30082
30157
  }
30158
+ function bulkAcquireLock(resources, agentId) {
30159
+ const db2 = getDb();
30160
+ return db2.transaction(() => {
30161
+ cleanExpiredLocks();
30162
+ releaseStaleAgentLocks();
30163
+ const acquired = [];
30164
+ for (const { resource_type, resource_id, lock_type = "advisory", expiry_ms = DEFAULT_LOCK_EXPIRY_MS } of resources) {
30165
+ const existing = db2.prepare(`
30166
+ SELECT * FROM resource_locks
30167
+ WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
30168
+ `).get(resource_type, resource_id, lock_type);
30169
+ if (existing && existing.agent_id !== agentId) {
30170
+ throw { _bulkConflict: true, resource_type, resource_id, held_by: existing.agent_id };
30171
+ }
30172
+ const expiresAt = new Date(Date.now() + expiry_ms).toISOString().slice(0, -1);
30173
+ if (existing) {
30174
+ db2.prepare(`
30175
+ UPDATE resource_locks SET expires_at = ?, locked_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
30176
+ WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
30177
+ `).run(expiresAt, resource_type, resource_id, lock_type);
30178
+ } else {
30179
+ db2.prepare(`
30180
+ INSERT INTO resource_locks (resource_type, resource_id, agent_id, lock_type, locked_at, expires_at)
30181
+ VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
30182
+ `).run(resource_type, resource_id, agentId, lock_type, expiresAt);
30183
+ }
30184
+ const lock = db2.prepare(`
30185
+ SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
30186
+ `).get(resource_type, resource_id, lock_type);
30187
+ acquired.push(lock);
30188
+ }
30189
+ return { acquired: true, locks: acquired };
30190
+ }).immediate();
30191
+ }
30192
+ function tryBulkAcquireLock(resources, agentId) {
30193
+ try {
30194
+ return bulkAcquireLock(resources, agentId);
30195
+ } catch (err) {
30196
+ const e = err;
30197
+ if (e?._bulkConflict) {
30198
+ return {
30199
+ acquired: false,
30200
+ locks: [],
30201
+ blocked_by: { resource_type: e.resource_type, resource_id: e.resource_id, held_by: e.held_by }
30202
+ };
30203
+ }
30204
+ throw err;
30205
+ }
30206
+ }
30083
30207
  function releaseLock(resourceType, resourceId, agentId) {
30084
30208
  const db2 = getDb();
30085
30209
  const result = db2.prepare(`
@@ -30091,6 +30215,7 @@ function releaseLock(resourceType, resourceId, agentId) {
30091
30215
  function checkLock(resourceType, resourceId) {
30092
30216
  const db2 = getDb();
30093
30217
  cleanExpiredLocks();
30218
+ releaseStaleAgentLocks();
30094
30219
  return db2.prepare(`
30095
30220
  SELECT * FROM resource_locks
30096
30221
  WHERE resource_type = ? AND resource_id = ?
@@ -30098,6 +30223,17 @@ function checkLock(resourceType, resourceId) {
30098
30223
  LIMIT 1
30099
30224
  `).get(resourceType, resourceId);
30100
30225
  }
30226
+ function releaseStaleAgentLocks() {
30227
+ const db2 = getDb();
30228
+ const result = db2.prepare(`
30229
+ DELETE FROM resource_locks
30230
+ WHERE LOWER(agent_id) IN (
30231
+ SELECT LOWER(agent) FROM agent_presence
30232
+ WHERE last_seen_at < strftime('%Y-%m-%dT%H:%M:%f', 'now', '-${STALE_HEARTBEAT_SECONDS} seconds')
30233
+ )
30234
+ `).run();
30235
+ return result.changes;
30236
+ }
30101
30237
  function cleanExpiredLocks() {
30102
30238
  const db2 = getDb();
30103
30239
  const result = db2.prepare(`
@@ -30108,6 +30244,7 @@ function cleanExpiredLocks() {
30108
30244
  function listLocks(opts) {
30109
30245
  const db2 = getDb();
30110
30246
  cleanExpiredLocks();
30247
+ releaseStaleAgentLocks();
30111
30248
  let query = "SELECT * FROM resource_locks WHERE 1=1";
30112
30249
  const params = [];
30113
30250
  if (opts?.resource_type) {
@@ -30121,6 +30258,31 @@ function listLocks(opts) {
30121
30258
  query += " ORDER BY locked_at ASC";
30122
30259
  return db2.prepare(query).all(...params);
30123
30260
  }
30261
+ function listLocksEnriched(opts) {
30262
+ const locks = listLocks(opts);
30263
+ const db2 = getDb();
30264
+ const nowMs = Date.now();
30265
+ return locks.map((lock) => {
30266
+ const lockedMs = new Date(lock.locked_at + "Z").getTime();
30267
+ const expiresMs = new Date(lock.expires_at + "Z").getTime();
30268
+ const presenceRow = db2.prepare(`
30269
+ SELECT role, status, last_seen_at, project_id FROM agent_presence WHERE LOWER(agent) = LOWER(?)
30270
+ `).get(lock.agent_id);
30271
+ const agent = presenceRow ? {
30272
+ role: presenceRow.role ?? null,
30273
+ status: presenceRow.status ?? null,
30274
+ online: presenceRow.last_seen_at ? nowMs - new Date(presenceRow.last_seen_at + "Z").getTime() < 60000 : false,
30275
+ last_seen_at: presenceRow.last_seen_at ?? null,
30276
+ project_id: presenceRow.project_id ?? null
30277
+ } : null;
30278
+ return {
30279
+ ...lock,
30280
+ locked_seconds_ago: Math.round((nowMs - lockedMs) / 1000),
30281
+ expires_in_seconds: Math.round((expiresMs - nowMs) / 1000),
30282
+ agent
30283
+ };
30284
+ });
30285
+ }
30124
30286
 
30125
30287
  // src/lib/hot.ts
30126
30288
  init_db();
@@ -30605,7 +30767,7 @@ function getGraphStats() {
30605
30767
  // package.json
30606
30768
  var package_default = {
30607
30769
  name: "@hasna/conversations",
30608
- version: "0.2.1",
30770
+ version: "0.2.2",
30609
30771
  description: "Real-time CLI messaging for AI agents",
30610
30772
  type: "module",
30611
30773
  bin: {
@@ -30735,7 +30897,8 @@ server.registerTool("read_messages", {
30735
30897
  project_id: exports_external.string().optional(),
30736
30898
  since: exports_external.string().optional(),
30737
30899
  limit: exports_external.coerce.number().optional(),
30738
- unread_only: exports_external.coerce.boolean().optional()
30900
+ unread_only: exports_external.coerce.boolean().optional(),
30901
+ mark_read: exports_external.coerce.boolean().optional()
30739
30902
  }
30740
30903
  }, async (args) => {
30741
30904
  const agent = resolveIdentity(args.from);
@@ -30743,6 +30906,9 @@ server.registerTool("read_messages", {
30743
30906
  ...args,
30744
30907
  project_id: args.project_id ?? resolveProjectId(undefined, agent)
30745
30908
  });
30909
+ if (args.mark_read !== false && messages.length > 0) {
30910
+ markReadByIds(messages.map((m) => m.id));
30911
+ }
30746
30912
  return {
30747
30913
  content: [{ type: "text", text: JSON.stringify(messages) }]
30748
30914
  };
@@ -30938,15 +31104,36 @@ server.registerTool("read_space", {
30938
31104
  inputSchema: {
30939
31105
  space: exports_external.string(),
30940
31106
  since: exports_external.string().optional(),
30941
- limit: exports_external.coerce.number().optional()
31107
+ limit: exports_external.coerce.number().optional(),
31108
+ mark_read: exports_external.coerce.boolean().optional()
30942
31109
  }
30943
31110
  }, async (args) => {
30944
- const { space, since, limit } = args;
31111
+ const { space, since, limit, mark_read } = args;
30945
31112
  const messages = readMessages({ space, since, limit });
31113
+ if (mark_read !== false && messages.length > 0) {
31114
+ markReadByIds(messages.map((m) => m.id));
31115
+ }
30946
31116
  return {
30947
31117
  content: [{ type: "text", text: JSON.stringify(messages) }]
30948
31118
  };
30949
31119
  });
31120
+ server.registerTool("read_digest", {
31121
+ description: "Lightweight unread message digest \u2014 returns preview-only summaries, auto-marks as read. Use instead of read_messages on busy spaces to avoid token overflow.",
31122
+ inputSchema: {
31123
+ space: exports_external.string().optional(),
31124
+ session_id: exports_external.string().optional(),
31125
+ to: exports_external.string().optional(),
31126
+ since: exports_external.string().optional(),
31127
+ limit: exports_external.coerce.number().optional(),
31128
+ project_id: exports_external.string().optional()
31129
+ }
31130
+ }, async (args) => {
31131
+ const { space, session_id, to, since, limit, project_id } = args;
31132
+ const result = readDigest({ space, session_id, to, since, limit, project_id });
31133
+ return {
31134
+ content: [{ type: "text", text: JSON.stringify(result) }]
31135
+ };
31136
+ });
30950
31137
  server.registerTool("join_space", {
30951
31138
  description: "Join a space as a member.",
30952
31139
  inputSchema: {
@@ -31479,18 +31666,29 @@ server.registerTool("get_reaction_summary", {
31479
31666
  return { content: [{ type: "text", text: JSON.stringify(summary) }] };
31480
31667
  });
31481
31668
  server.registerTool("acquire_lock", {
31482
- description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock.",
31669
+ description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock. On conflict, auto-DMs the holding agent.",
31483
31670
  inputSchema: {
31484
31671
  resource_type: exports_external.string(),
31485
31672
  resource_id: exports_external.string(),
31486
31673
  lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
31487
31674
  expiry_ms: exports_external.coerce.number().optional(),
31488
- from: exports_external.string().optional()
31675
+ from: exports_external.string().optional(),
31676
+ auto_dm: exports_external.coerce.boolean().optional()
31489
31677
  }
31490
31678
  }, async (args) => {
31491
- const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam } = args;
31679
+ const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam, auto_dm } = args;
31492
31680
  const agent = resolveIdentity(fromParam);
31493
31681
  const result = acquireLock(resource_type, resource_id, agent, lock_type ?? "advisory", expiry_ms);
31682
+ if (!result.acquired && result.held_by && auto_dm !== false) {
31683
+ try {
31684
+ sendMessage({
31685
+ from: agent,
31686
+ to: result.held_by,
31687
+ content: `Lock conflict: I (@${agent}) tried to acquire ${lock_type ?? "advisory"} lock on \`${resource_type}/${resource_id}\` but you hold it. If you no longer need it, release it with \`release_lock\`.`,
31688
+ priority: "high"
31689
+ });
31690
+ } catch {}
31691
+ }
31494
31692
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
31495
31693
  });
31496
31694
  server.registerTool("release_lock", {
@@ -31517,15 +31715,50 @@ server.registerTool("check_lock", {
31517
31715
  return { content: [{ type: "text", text: JSON.stringify(lock ?? { locked: false }) }] };
31518
31716
  });
31519
31717
  server.registerTool("list_locks", {
31520
- description: "List all active (non-expired) locks. Filter by resource_type or agent.",
31718
+ description: "List all active (non-expired) locks enriched with agent presence details (status, online, last_seen_at) and time context (locked_seconds_ago, expires_in_seconds). Filter by resource_type or agent.",
31521
31719
  inputSchema: {
31522
31720
  resource_type: exports_external.string().optional(),
31523
31721
  agent_id: exports_external.string().optional()
31524
31722
  }
31525
31723
  }, async (args) => {
31526
- const locks = listLocks({ resource_type: args.resource_type, agent_id: args.agent_id });
31724
+ const locks = listLocksEnriched({ resource_type: args.resource_type, agent_id: args.agent_id });
31527
31725
  return { content: [{ type: "text", text: JSON.stringify(locks) }] };
31528
31726
  });
31727
+ server.registerTool("bulk_acquire_lock", {
31728
+ description: "Atomically acquire multiple locks at once. All-or-nothing: if any lock is held by another agent, none are acquired. Returns blocked_by info on conflict.",
31729
+ inputSchema: {
31730
+ resources: exports_external.array(exports_external.object({
31731
+ resource_type: exports_external.string(),
31732
+ resource_id: exports_external.string(),
31733
+ lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
31734
+ expiry_ms: exports_external.coerce.number().optional()
31735
+ })),
31736
+ from: exports_external.string().optional(),
31737
+ auto_dm: exports_external.coerce.boolean().optional()
31738
+ }
31739
+ }, async (args) => {
31740
+ const agent = resolveIdentity(args.from);
31741
+ const result = tryBulkAcquireLock(args.resources, agent);
31742
+ if (!result.acquired && result.blocked_by && args.auto_dm !== false) {
31743
+ try {
31744
+ sendMessage({
31745
+ from: agent,
31746
+ to: result.blocked_by.held_by,
31747
+ content: `Bulk lock conflict: I (@${agent}) tried to atomically acquire ${args.resources.length} locks but you hold \`${result.blocked_by.resource_type}/${result.blocked_by.resource_id}\`. Release it when done.`,
31748
+ priority: "high"
31749
+ });
31750
+ } catch {}
31751
+ }
31752
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
31753
+ });
31754
+ server.registerTool("clean_expired_locks", {
31755
+ description: "Clean up expired locks and auto-release locks held by agents whose heartbeat has been stale for >30 minutes. Returns counts of removed locks.",
31756
+ inputSchema: {}
31757
+ }, async () => {
31758
+ const stale = releaseStaleAgentLocks();
31759
+ const expired = cleanExpiredLocks();
31760
+ return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
31761
+ });
31529
31762
  server.registerTool("get_thread_replies", {
31530
31763
  description: "Get all replies in a thread for a given parent message ID.",
31531
31764
  inputSchema: {
@@ -31709,6 +31942,7 @@ server.registerTool("search_tools", {
31709
31942
  const all = [
31710
31943
  "send_message",
31711
31944
  "read_messages",
31945
+ "read_digest",
31712
31946
  "list_sessions",
31713
31947
  "reply",
31714
31948
  "mark_read",
@@ -31747,9 +31981,11 @@ server.registerTool("search_tools", {
31747
31981
  "get_reactions",
31748
31982
  "get_reaction_summary",
31749
31983
  "acquire_lock",
31984
+ "bulk_acquire_lock",
31750
31985
  "release_lock",
31751
31986
  "check_lock",
31752
31987
  "list_locks",
31988
+ "clean_expired_locks",
31753
31989
  "get_thread_replies",
31754
31990
  "set_focus",
31755
31991
  "get_focus",
@@ -31775,7 +32011,8 @@ server.registerTool("describe_tools", {
31775
32011
  }, async ({ names }) => {
31776
32012
  const descriptions = {
31777
32013
  send_message: "Send DM to agent. Required: to, content. Optional: from?, priority?(low|normal|high|urgent), blocking?",
31778
- read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?",
32014
+ read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?, mark_read?(default true \u2014 auto-marks returned messages as read, pass false to peek without consuming)",
32015
+ read_digest: "Lightweight unread digest \u2014 preview only (no full bodies), auto-marks read, never overflows tokens. Returns { messages, total_unread, shown }. Optional: space?, session_id?, to?, since?(ISO), limit?, project_id?",
31779
32016
  list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
31780
32017
  reply: "Reply to a message in same session. Required: message_id, content. Optional: from?",
31781
32018
  mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
@@ -31784,7 +32021,7 @@ server.registerTool("describe_tools", {
31784
32021
  create_space: "Create space and auto-join. Required: name. Optional: from?, description?, parent_id?(max 3 levels), project_id?",
31785
32022
  list_spaces: "List spaces with member/message counts. Optional: project_id?, parent_id?(use 'null' for top-level), include_archived?",
31786
32023
  send_to_space: "Post message to space. Required: space, content. Optional: from?, priority?(low|normal|high|urgent), blocking?",
31787
- read_space: "Read messages in a space. Required: space. Optional: since?(ISO), limit?",
32024
+ read_space: "Read messages in a space. Required: space. Optional: since?(ISO), limit?, mark_read?(default true \u2014 auto-marks returned messages as read)",
31788
32025
  join_space: "Join a space. Required: space. Optional: from?",
31789
32026
  leave_space: "Leave a space. Required: space. Optional: from?",
31790
32027
  update_space: "Update space fields. Required: name. Optional: description?, parent_id?(use 'null' to remove), project_id?(use 'null' to remove)",
@@ -31813,10 +32050,12 @@ server.registerTool("describe_tools", {
31813
32050
  remove_reaction: "Remove emoji reaction from a message. Required: message_id, emoji. Optional: from?",
31814
32051
  get_reactions: "Get all reactions for a message. Required: message_id",
31815
32052
  get_reaction_summary: "Get emoji counts + agent lists for a message. Required: message_id",
31816
- acquire_lock: "Acquire advisory/exclusive lock on a resource. Required: resource_type, resource_id. Optional: lock_type?(advisory|exclusive), expiry_ms?, from?",
32053
+ acquire_lock: "Acquire advisory/exclusive lock on a resource. On conflict, auto-DMs the holding agent. Required: resource_type, resource_id. Optional: lock_type?(advisory|exclusive), expiry_ms?, from?, auto_dm?(default true)",
32054
+ bulk_acquire_lock: "Atomically acquire multiple locks (all-or-nothing). Required: resources[]{resource_type,resource_id,lock_type?,expiry_ms?}. Optional: from?, auto_dm?(default true). Returns blocked_by on conflict.",
31817
32055
  release_lock: "Release lock held by agent. Required: resource_type, resource_id. Optional: from?",
31818
32056
  check_lock: "Check if resource is locked and who holds it. Required: resource_type, resource_id",
31819
- list_locks: "List active locks. Optional: resource_type?, agent_id?",
32057
+ list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
32058
+ clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
31820
32059
  get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
31821
32060
  set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
31822
32061
  get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
package/dist/index.d.ts CHANGED
@@ -21,8 +21,8 @@ export { addReaction, removeReaction, getReactions, getReactionSummary, } from "
21
21
  export type { ReactionSummary } from "./lib/reactions.js";
22
22
  export { fireWebhooks, } from "./lib/webhooks.js";
23
23
  export { heartbeat, registerAgent, isAgentConflict, getPresence, listAgents, removePresence, renameAgent, } from "./lib/presence.js";
24
- export { acquireLock, releaseLock, checkLock, cleanExpiredLocks, listLocks, } from "./lib/locks.js";
25
- export type { ResourceLock } from "./lib/locks.js";
24
+ export { acquireLock, tryBulkAcquireLock, releaseLock, checkLock, cleanExpiredLocks, releaseStaleAgentLocks, listLocks, listLocksEnriched, } from "./lib/locks.js";
25
+ export type { ResourceLock, EnrichedLock, BulkLockRequest, BulkAcquireResult } from "./lib/locks.js";
26
26
  export { computeHotness, listHotSessions, } from "./lib/hot.js";
27
27
  export type { HotSession, HotSessionsOptions } from "./lib/hot.js";
28
28
  export { extractTopics, getSpaceTopics, getSessionTopics, getTrendingTopics, } from "./lib/topics.js";