@hasna/conversations 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hook.js +18 -8
- package/bin/index.js +416 -23
- package/bin/mcp.js +306 -22
- package/dist/index.d.ts +2 -2
- package/dist/index.js +122 -9
- package/dist/lib/locks.d.ts +33 -0
- package/dist/lib/messages.d.ts +38 -0
- package/package.json +1 -1
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: (
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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: (
|
|
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 "";
|
|
@@ -29068,6 +29141,39 @@ function searchMessages(opts) {
|
|
|
29068
29141
|
return { ...msg, snippet: null, relevance_score: 0 };
|
|
29069
29142
|
});
|
|
29070
29143
|
}
|
|
29144
|
+
function listUnreadCounts(agent) {
|
|
29145
|
+
const db2 = getDb();
|
|
29146
|
+
if (agent) {
|
|
29147
|
+
const rows2 = db2.prepare(`
|
|
29148
|
+
SELECT
|
|
29149
|
+
space,
|
|
29150
|
+
COUNT(CASE WHEN read_at IS NULL AND (to_agent = ? OR to_agent IS NULL OR to_agent = '') THEN 1 END) AS unread_count,
|
|
29151
|
+
MAX(created_at) AS latest_message_at
|
|
29152
|
+
FROM messages
|
|
29153
|
+
WHERE space IN (
|
|
29154
|
+
SELECT DISTINCT space FROM space_members WHERE agent = ?
|
|
29155
|
+
UNION
|
|
29156
|
+
SELECT DISTINCT space FROM messages WHERE to_agent = ? AND space IS NOT NULL
|
|
29157
|
+
)
|
|
29158
|
+
GROUP BY space
|
|
29159
|
+
HAVING COUNT(*) > 0
|
|
29160
|
+
ORDER BY unread_count DESC, latest_message_at DESC
|
|
29161
|
+
`).all(agent, agent, agent);
|
|
29162
|
+
return rows2;
|
|
29163
|
+
}
|
|
29164
|
+
const rows = db2.prepare(`
|
|
29165
|
+
SELECT
|
|
29166
|
+
space,
|
|
29167
|
+
COUNT(CASE WHEN read_at IS NULL THEN 1 END) AS unread_count,
|
|
29168
|
+
MAX(created_at) AS latest_message_at
|
|
29169
|
+
FROM messages
|
|
29170
|
+
WHERE space IS NOT NULL
|
|
29171
|
+
GROUP BY space
|
|
29172
|
+
HAVING COUNT(*) > 0
|
|
29173
|
+
ORDER BY unread_count DESC, latest_message_at DESC
|
|
29174
|
+
`).all();
|
|
29175
|
+
return rows;
|
|
29176
|
+
}
|
|
29071
29177
|
|
|
29072
29178
|
// src/lib/sessions.ts
|
|
29073
29179
|
init_db();
|
|
@@ -30050,10 +30156,12 @@ function getReactionSummary(messageId) {
|
|
|
30050
30156
|
// src/lib/locks.ts
|
|
30051
30157
|
init_db();
|
|
30052
30158
|
var DEFAULT_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
30159
|
+
var STALE_HEARTBEAT_SECONDS = 30 * 60;
|
|
30053
30160
|
function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", expiryMs = DEFAULT_LOCK_EXPIRY_MS) {
|
|
30054
30161
|
const db2 = getDb();
|
|
30055
30162
|
return db2.transaction(() => {
|
|
30056
30163
|
cleanExpiredLocks();
|
|
30164
|
+
releaseStaleAgentLocks();
|
|
30057
30165
|
const existing = db2.prepare(`
|
|
30058
30166
|
SELECT * FROM resource_locks
|
|
30059
30167
|
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
@@ -30080,6 +30188,55 @@ function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", e
|
|
|
30080
30188
|
return { acquired: true, lock };
|
|
30081
30189
|
}).immediate();
|
|
30082
30190
|
}
|
|
30191
|
+
function bulkAcquireLock(resources, agentId) {
|
|
30192
|
+
const db2 = getDb();
|
|
30193
|
+
return db2.transaction(() => {
|
|
30194
|
+
cleanExpiredLocks();
|
|
30195
|
+
releaseStaleAgentLocks();
|
|
30196
|
+
const acquired = [];
|
|
30197
|
+
for (const { resource_type, resource_id, lock_type = "advisory", expiry_ms = DEFAULT_LOCK_EXPIRY_MS } of resources) {
|
|
30198
|
+
const existing = db2.prepare(`
|
|
30199
|
+
SELECT * FROM resource_locks
|
|
30200
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
30201
|
+
`).get(resource_type, resource_id, lock_type);
|
|
30202
|
+
if (existing && existing.agent_id !== agentId) {
|
|
30203
|
+
throw { _bulkConflict: true, resource_type, resource_id, held_by: existing.agent_id };
|
|
30204
|
+
}
|
|
30205
|
+
const expiresAt = new Date(Date.now() + expiry_ms).toISOString().slice(0, -1);
|
|
30206
|
+
if (existing) {
|
|
30207
|
+
db2.prepare(`
|
|
30208
|
+
UPDATE resource_locks SET expires_at = ?, locked_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
30209
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
30210
|
+
`).run(expiresAt, resource_type, resource_id, lock_type);
|
|
30211
|
+
} else {
|
|
30212
|
+
db2.prepare(`
|
|
30213
|
+
INSERT INTO resource_locks (resource_type, resource_id, agent_id, lock_type, locked_at, expires_at)
|
|
30214
|
+
VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
30215
|
+
`).run(resource_type, resource_id, agentId, lock_type, expiresAt);
|
|
30216
|
+
}
|
|
30217
|
+
const lock = db2.prepare(`
|
|
30218
|
+
SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
30219
|
+
`).get(resource_type, resource_id, lock_type);
|
|
30220
|
+
acquired.push(lock);
|
|
30221
|
+
}
|
|
30222
|
+
return { acquired: true, locks: acquired };
|
|
30223
|
+
}).immediate();
|
|
30224
|
+
}
|
|
30225
|
+
function tryBulkAcquireLock(resources, agentId) {
|
|
30226
|
+
try {
|
|
30227
|
+
return bulkAcquireLock(resources, agentId);
|
|
30228
|
+
} catch (err) {
|
|
30229
|
+
const e = err;
|
|
30230
|
+
if (e?._bulkConflict) {
|
|
30231
|
+
return {
|
|
30232
|
+
acquired: false,
|
|
30233
|
+
locks: [],
|
|
30234
|
+
blocked_by: { resource_type: e.resource_type, resource_id: e.resource_id, held_by: e.held_by }
|
|
30235
|
+
};
|
|
30236
|
+
}
|
|
30237
|
+
throw err;
|
|
30238
|
+
}
|
|
30239
|
+
}
|
|
30083
30240
|
function releaseLock(resourceType, resourceId, agentId) {
|
|
30084
30241
|
const db2 = getDb();
|
|
30085
30242
|
const result = db2.prepare(`
|
|
@@ -30091,6 +30248,7 @@ function releaseLock(resourceType, resourceId, agentId) {
|
|
|
30091
30248
|
function checkLock(resourceType, resourceId) {
|
|
30092
30249
|
const db2 = getDb();
|
|
30093
30250
|
cleanExpiredLocks();
|
|
30251
|
+
releaseStaleAgentLocks();
|
|
30094
30252
|
return db2.prepare(`
|
|
30095
30253
|
SELECT * FROM resource_locks
|
|
30096
30254
|
WHERE resource_type = ? AND resource_id = ?
|
|
@@ -30098,6 +30256,17 @@ function checkLock(resourceType, resourceId) {
|
|
|
30098
30256
|
LIMIT 1
|
|
30099
30257
|
`).get(resourceType, resourceId);
|
|
30100
30258
|
}
|
|
30259
|
+
function releaseStaleAgentLocks() {
|
|
30260
|
+
const db2 = getDb();
|
|
30261
|
+
const result = db2.prepare(`
|
|
30262
|
+
DELETE FROM resource_locks
|
|
30263
|
+
WHERE LOWER(agent_id) IN (
|
|
30264
|
+
SELECT LOWER(agent) FROM agent_presence
|
|
30265
|
+
WHERE last_seen_at < strftime('%Y-%m-%dT%H:%M:%f', 'now', '-${STALE_HEARTBEAT_SECONDS} seconds')
|
|
30266
|
+
)
|
|
30267
|
+
`).run();
|
|
30268
|
+
return result.changes;
|
|
30269
|
+
}
|
|
30101
30270
|
function cleanExpiredLocks() {
|
|
30102
30271
|
const db2 = getDb();
|
|
30103
30272
|
const result = db2.prepare(`
|
|
@@ -30108,6 +30277,7 @@ function cleanExpiredLocks() {
|
|
|
30108
30277
|
function listLocks(opts) {
|
|
30109
30278
|
const db2 = getDb();
|
|
30110
30279
|
cleanExpiredLocks();
|
|
30280
|
+
releaseStaleAgentLocks();
|
|
30111
30281
|
let query = "SELECT * FROM resource_locks WHERE 1=1";
|
|
30112
30282
|
const params = [];
|
|
30113
30283
|
if (opts?.resource_type) {
|
|
@@ -30121,6 +30291,31 @@ function listLocks(opts) {
|
|
|
30121
30291
|
query += " ORDER BY locked_at ASC";
|
|
30122
30292
|
return db2.prepare(query).all(...params);
|
|
30123
30293
|
}
|
|
30294
|
+
function listLocksEnriched(opts) {
|
|
30295
|
+
const locks = listLocks(opts);
|
|
30296
|
+
const db2 = getDb();
|
|
30297
|
+
const nowMs = Date.now();
|
|
30298
|
+
return locks.map((lock) => {
|
|
30299
|
+
const lockedMs = new Date(lock.locked_at + "Z").getTime();
|
|
30300
|
+
const expiresMs = new Date(lock.expires_at + "Z").getTime();
|
|
30301
|
+
const presenceRow = db2.prepare(`
|
|
30302
|
+
SELECT role, status, last_seen_at, project_id FROM agent_presence WHERE LOWER(agent) = LOWER(?)
|
|
30303
|
+
`).get(lock.agent_id);
|
|
30304
|
+
const agent = presenceRow ? {
|
|
30305
|
+
role: presenceRow.role ?? null,
|
|
30306
|
+
status: presenceRow.status ?? null,
|
|
30307
|
+
online: presenceRow.last_seen_at ? nowMs - new Date(presenceRow.last_seen_at + "Z").getTime() < 60000 : false,
|
|
30308
|
+
last_seen_at: presenceRow.last_seen_at ?? null,
|
|
30309
|
+
project_id: presenceRow.project_id ?? null
|
|
30310
|
+
} : null;
|
|
30311
|
+
return {
|
|
30312
|
+
...lock,
|
|
30313
|
+
locked_seconds_ago: Math.round((nowMs - lockedMs) / 1000),
|
|
30314
|
+
expires_in_seconds: Math.round((expiresMs - nowMs) / 1000),
|
|
30315
|
+
agent
|
|
30316
|
+
};
|
|
30317
|
+
});
|
|
30318
|
+
}
|
|
30124
30319
|
|
|
30125
30320
|
// src/lib/hot.ts
|
|
30126
30321
|
init_db();
|
|
@@ -30605,7 +30800,7 @@ function getGraphStats() {
|
|
|
30605
30800
|
// package.json
|
|
30606
30801
|
var package_default = {
|
|
30607
30802
|
name: "@hasna/conversations",
|
|
30608
|
-
version: "0.2.
|
|
30803
|
+
version: "0.2.3",
|
|
30609
30804
|
description: "Real-time CLI messaging for AI agents",
|
|
30610
30805
|
type: "module",
|
|
30611
30806
|
bin: {
|
|
@@ -30735,7 +30930,8 @@ server.registerTool("read_messages", {
|
|
|
30735
30930
|
project_id: exports_external.string().optional(),
|
|
30736
30931
|
since: exports_external.string().optional(),
|
|
30737
30932
|
limit: exports_external.coerce.number().optional(),
|
|
30738
|
-
unread_only: exports_external.coerce.boolean().optional()
|
|
30933
|
+
unread_only: exports_external.coerce.boolean().optional(),
|
|
30934
|
+
mark_read: exports_external.coerce.boolean().optional()
|
|
30739
30935
|
}
|
|
30740
30936
|
}, async (args) => {
|
|
30741
30937
|
const agent = resolveIdentity(args.from);
|
|
@@ -30743,6 +30939,9 @@ server.registerTool("read_messages", {
|
|
|
30743
30939
|
...args,
|
|
30744
30940
|
project_id: args.project_id ?? resolveProjectId(undefined, agent)
|
|
30745
30941
|
});
|
|
30942
|
+
if (args.mark_read !== false && messages.length > 0) {
|
|
30943
|
+
markReadByIds(messages.map((m) => m.id));
|
|
30944
|
+
}
|
|
30746
30945
|
return {
|
|
30747
30946
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
30748
30947
|
};
|
|
@@ -30901,6 +31100,17 @@ server.registerTool("list_spaces", {
|
|
|
30901
31100
|
content: [{ type: "text", text: JSON.stringify(spaces) }]
|
|
30902
31101
|
};
|
|
30903
31102
|
});
|
|
31103
|
+
server.registerTool("list_unread_counts", {
|
|
31104
|
+
description: "Get unread message counts per space without fetching message content. Use this at session start to triage which spaces need attention before calling read_messages.",
|
|
31105
|
+
inputSchema: {
|
|
31106
|
+
agent: exports_external.string().optional().describe("Filter to spaces the agent is a member of or has received messages in. Omit for global unread counts.")
|
|
31107
|
+
}
|
|
31108
|
+
}, async (args) => {
|
|
31109
|
+
const counts = listUnreadCounts(args.agent);
|
|
31110
|
+
return {
|
|
31111
|
+
content: [{ type: "text", text: JSON.stringify(counts) }]
|
|
31112
|
+
};
|
|
31113
|
+
});
|
|
30904
31114
|
server.registerTool("send_to_space", {
|
|
30905
31115
|
description: "Post a message to a space.",
|
|
30906
31116
|
inputSchema: {
|
|
@@ -30938,15 +31148,36 @@ server.registerTool("read_space", {
|
|
|
30938
31148
|
inputSchema: {
|
|
30939
31149
|
space: exports_external.string(),
|
|
30940
31150
|
since: exports_external.string().optional(),
|
|
30941
|
-
limit: exports_external.coerce.number().optional()
|
|
31151
|
+
limit: exports_external.coerce.number().optional(),
|
|
31152
|
+
mark_read: exports_external.coerce.boolean().optional()
|
|
30942
31153
|
}
|
|
30943
31154
|
}, async (args) => {
|
|
30944
|
-
const { space, since, limit } = args;
|
|
31155
|
+
const { space, since, limit, mark_read } = args;
|
|
30945
31156
|
const messages = readMessages({ space, since, limit });
|
|
31157
|
+
if (mark_read !== false && messages.length > 0) {
|
|
31158
|
+
markReadByIds(messages.map((m) => m.id));
|
|
31159
|
+
}
|
|
30946
31160
|
return {
|
|
30947
31161
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
30948
31162
|
};
|
|
30949
31163
|
});
|
|
31164
|
+
server.registerTool("read_digest", {
|
|
31165
|
+
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.",
|
|
31166
|
+
inputSchema: {
|
|
31167
|
+
space: exports_external.string().optional(),
|
|
31168
|
+
session_id: exports_external.string().optional(),
|
|
31169
|
+
to: exports_external.string().optional(),
|
|
31170
|
+
since: exports_external.string().optional(),
|
|
31171
|
+
limit: exports_external.coerce.number().optional(),
|
|
31172
|
+
project_id: exports_external.string().optional()
|
|
31173
|
+
}
|
|
31174
|
+
}, async (args) => {
|
|
31175
|
+
const { space, session_id, to, since, limit, project_id } = args;
|
|
31176
|
+
const result = readDigest({ space, session_id, to, since, limit, project_id });
|
|
31177
|
+
return {
|
|
31178
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
31179
|
+
};
|
|
31180
|
+
});
|
|
30950
31181
|
server.registerTool("join_space", {
|
|
30951
31182
|
description: "Join a space as a member.",
|
|
30952
31183
|
inputSchema: {
|
|
@@ -31479,18 +31710,29 @@ server.registerTool("get_reaction_summary", {
|
|
|
31479
31710
|
return { content: [{ type: "text", text: JSON.stringify(summary) }] };
|
|
31480
31711
|
});
|
|
31481
31712
|
server.registerTool("acquire_lock", {
|
|
31482
|
-
description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock.",
|
|
31713
|
+
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
31714
|
inputSchema: {
|
|
31484
31715
|
resource_type: exports_external.string(),
|
|
31485
31716
|
resource_id: exports_external.string(),
|
|
31486
31717
|
lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
|
|
31487
31718
|
expiry_ms: exports_external.coerce.number().optional(),
|
|
31488
|
-
from: exports_external.string().optional()
|
|
31719
|
+
from: exports_external.string().optional(),
|
|
31720
|
+
auto_dm: exports_external.coerce.boolean().optional()
|
|
31489
31721
|
}
|
|
31490
31722
|
}, async (args) => {
|
|
31491
|
-
const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam } = args;
|
|
31723
|
+
const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam, auto_dm } = args;
|
|
31492
31724
|
const agent = resolveIdentity(fromParam);
|
|
31493
31725
|
const result = acquireLock(resource_type, resource_id, agent, lock_type ?? "advisory", expiry_ms);
|
|
31726
|
+
if (!result.acquired && result.held_by && auto_dm !== false) {
|
|
31727
|
+
try {
|
|
31728
|
+
sendMessage({
|
|
31729
|
+
from: agent,
|
|
31730
|
+
to: result.held_by,
|
|
31731
|
+
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\`.`,
|
|
31732
|
+
priority: "high"
|
|
31733
|
+
});
|
|
31734
|
+
} catch {}
|
|
31735
|
+
}
|
|
31494
31736
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
31495
31737
|
});
|
|
31496
31738
|
server.registerTool("release_lock", {
|
|
@@ -31517,15 +31759,50 @@ server.registerTool("check_lock", {
|
|
|
31517
31759
|
return { content: [{ type: "text", text: JSON.stringify(lock ?? { locked: false }) }] };
|
|
31518
31760
|
});
|
|
31519
31761
|
server.registerTool("list_locks", {
|
|
31520
|
-
description: "List all active (non-expired) locks. Filter by resource_type or agent.",
|
|
31762
|
+
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
31763
|
inputSchema: {
|
|
31522
31764
|
resource_type: exports_external.string().optional(),
|
|
31523
31765
|
agent_id: exports_external.string().optional()
|
|
31524
31766
|
}
|
|
31525
31767
|
}, async (args) => {
|
|
31526
|
-
const locks =
|
|
31768
|
+
const locks = listLocksEnriched({ resource_type: args.resource_type, agent_id: args.agent_id });
|
|
31527
31769
|
return { content: [{ type: "text", text: JSON.stringify(locks) }] };
|
|
31528
31770
|
});
|
|
31771
|
+
server.registerTool("bulk_acquire_lock", {
|
|
31772
|
+
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.",
|
|
31773
|
+
inputSchema: {
|
|
31774
|
+
resources: exports_external.array(exports_external.object({
|
|
31775
|
+
resource_type: exports_external.string(),
|
|
31776
|
+
resource_id: exports_external.string(),
|
|
31777
|
+
lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
|
|
31778
|
+
expiry_ms: exports_external.coerce.number().optional()
|
|
31779
|
+
})),
|
|
31780
|
+
from: exports_external.string().optional(),
|
|
31781
|
+
auto_dm: exports_external.coerce.boolean().optional()
|
|
31782
|
+
}
|
|
31783
|
+
}, async (args) => {
|
|
31784
|
+
const agent = resolveIdentity(args.from);
|
|
31785
|
+
const result = tryBulkAcquireLock(args.resources, agent);
|
|
31786
|
+
if (!result.acquired && result.blocked_by && args.auto_dm !== false) {
|
|
31787
|
+
try {
|
|
31788
|
+
sendMessage({
|
|
31789
|
+
from: agent,
|
|
31790
|
+
to: result.blocked_by.held_by,
|
|
31791
|
+
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.`,
|
|
31792
|
+
priority: "high"
|
|
31793
|
+
});
|
|
31794
|
+
} catch {}
|
|
31795
|
+
}
|
|
31796
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
31797
|
+
});
|
|
31798
|
+
server.registerTool("clean_expired_locks", {
|
|
31799
|
+
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.",
|
|
31800
|
+
inputSchema: {}
|
|
31801
|
+
}, async () => {
|
|
31802
|
+
const stale = releaseStaleAgentLocks();
|
|
31803
|
+
const expired = cleanExpiredLocks();
|
|
31804
|
+
return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
|
|
31805
|
+
});
|
|
31529
31806
|
server.registerTool("get_thread_replies", {
|
|
31530
31807
|
description: "Get all replies in a thread for a given parent message ID.",
|
|
31531
31808
|
inputSchema: {
|
|
@@ -31709,6 +31986,7 @@ server.registerTool("search_tools", {
|
|
|
31709
31986
|
const all = [
|
|
31710
31987
|
"send_message",
|
|
31711
31988
|
"read_messages",
|
|
31989
|
+
"read_digest",
|
|
31712
31990
|
"list_sessions",
|
|
31713
31991
|
"reply",
|
|
31714
31992
|
"mark_read",
|
|
@@ -31747,9 +32025,11 @@ server.registerTool("search_tools", {
|
|
|
31747
32025
|
"get_reactions",
|
|
31748
32026
|
"get_reaction_summary",
|
|
31749
32027
|
"acquire_lock",
|
|
32028
|
+
"bulk_acquire_lock",
|
|
31750
32029
|
"release_lock",
|
|
31751
32030
|
"check_lock",
|
|
31752
32031
|
"list_locks",
|
|
32032
|
+
"clean_expired_locks",
|
|
31753
32033
|
"get_thread_replies",
|
|
31754
32034
|
"set_focus",
|
|
31755
32035
|
"get_focus",
|
|
@@ -31775,16 +32055,18 @@ server.registerTool("describe_tools", {
|
|
|
31775
32055
|
}, async ({ names }) => {
|
|
31776
32056
|
const descriptions = {
|
|
31777
32057
|
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?",
|
|
32058
|
+
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)",
|
|
32059
|
+
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
32060
|
list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
|
|
31780
32061
|
reply: "Reply to a message in same session. Required: message_id, content. Optional: from?",
|
|
31781
32062
|
mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
|
|
31782
32063
|
search_messages: "Full-text search messages. Required: query. Optional: space?, from?, to?, limit?",
|
|
31783
32064
|
export_messages: "Export messages as JSON or CSV. Optional: space?, session_id?, from?, since?, until?, format?(json|csv)",
|
|
31784
32065
|
create_space: "Create space and auto-join. Required: name. Optional: from?, description?, parent_id?(max 3 levels), project_id?",
|
|
32066
|
+
list_unread_counts: "Get unread message counts per space (no content). Ideal for session start triage. Optional: agent?(filter to agent's spaces)",
|
|
31785
32067
|
list_spaces: "List spaces with member/message counts. Optional: project_id?, parent_id?(use 'null' for top-level), include_archived?",
|
|
31786
32068
|
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?",
|
|
32069
|
+
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
32070
|
join_space: "Join a space. Required: space. Optional: from?",
|
|
31789
32071
|
leave_space: "Leave a space. Required: space. Optional: from?",
|
|
31790
32072
|
update_space: "Update space fields. Required: name. Optional: description?, parent_id?(use 'null' to remove), project_id?(use 'null' to remove)",
|
|
@@ -31813,10 +32095,12 @@ server.registerTool("describe_tools", {
|
|
|
31813
32095
|
remove_reaction: "Remove emoji reaction from a message. Required: message_id, emoji. Optional: from?",
|
|
31814
32096
|
get_reactions: "Get all reactions for a message. Required: message_id",
|
|
31815
32097
|
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?",
|
|
32098
|
+
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)",
|
|
32099
|
+
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
32100
|
release_lock: "Release lock held by agent. Required: resource_type, resource_id. Optional: from?",
|
|
31818
32101
|
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?",
|
|
32102
|
+
list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
|
|
32103
|
+
clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
|
|
31820
32104
|
get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
|
|
31821
32105
|
set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
|
|
31822
32106
|
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";
|