@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/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: (
|
|
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);
|
|
@@ -2382,12 +2403,64 @@ function getMessageById(id) {
|
|
|
2382
2403
|
const row = db2.prepare("SELECT * FROM messages WHERE id = ?").get(id);
|
|
2383
2404
|
return row ? parseMessage(row) : null;
|
|
2384
2405
|
}
|
|
2406
|
+
function markReadByIds(ids) {
|
|
2407
|
+
const db2 = getDb();
|
|
2408
|
+
if (ids.length === 0)
|
|
2409
|
+
return 0;
|
|
2410
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
2411
|
+
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`);
|
|
2412
|
+
const result = stmt.run(...ids);
|
|
2413
|
+
return result.changes;
|
|
2414
|
+
}
|
|
2385
2415
|
function markAllRead(agent) {
|
|
2386
2416
|
const db2 = getDb();
|
|
2387
2417
|
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`);
|
|
2388
2418
|
const result = stmt.run(agent);
|
|
2389
2419
|
return result.changes;
|
|
2390
2420
|
}
|
|
2421
|
+
function readDigest(opts = {}) {
|
|
2422
|
+
const db2 = getDb();
|
|
2423
|
+
const countConditions = ["read_at IS NULL"];
|
|
2424
|
+
const countParams = [];
|
|
2425
|
+
if (opts.space) {
|
|
2426
|
+
countConditions.push("space = ?");
|
|
2427
|
+
countParams.push(opts.space);
|
|
2428
|
+
}
|
|
2429
|
+
if (opts.session_id) {
|
|
2430
|
+
countConditions.push("session_id = ?");
|
|
2431
|
+
countParams.push(opts.session_id);
|
|
2432
|
+
}
|
|
2433
|
+
if (opts.to) {
|
|
2434
|
+
countConditions.push("to_agent = ?");
|
|
2435
|
+
countParams.push(opts.to);
|
|
2436
|
+
}
|
|
2437
|
+
if (opts.since) {
|
|
2438
|
+
countConditions.push("created_at > ?");
|
|
2439
|
+
countParams.push(opts.since);
|
|
2440
|
+
}
|
|
2441
|
+
if (opts.project_id) {
|
|
2442
|
+
countConditions.push("project_id = ?");
|
|
2443
|
+
countParams.push(opts.project_id);
|
|
2444
|
+
}
|
|
2445
|
+
const countWhere = `WHERE ${countConditions.join(" AND ")}`;
|
|
2446
|
+
const totalUnread = db2.prepare(`SELECT COUNT(*) as n FROM messages ${countWhere}`).get(...countParams).n;
|
|
2447
|
+
const messages = readMessages({ ...opts, unread_only: opts.unread_only ?? true });
|
|
2448
|
+
if (messages.length > 0) {
|
|
2449
|
+
markReadByIds(messages.map((m) => m.id));
|
|
2450
|
+
}
|
|
2451
|
+
const digest = messages.map((m) => ({
|
|
2452
|
+
id: m.id,
|
|
2453
|
+
from: m.from_agent,
|
|
2454
|
+
created_at: m.created_at,
|
|
2455
|
+
preview: m.content.slice(0, 100) + (m.content.length > 100 ? "\u2026" : ""),
|
|
2456
|
+
priority: m.priority,
|
|
2457
|
+
has_attachments: Array.isArray(m.attachments) && m.attachments.length > 0,
|
|
2458
|
+
space: m.space,
|
|
2459
|
+
to: m.to_agent,
|
|
2460
|
+
unread: !m.read_at
|
|
2461
|
+
}));
|
|
2462
|
+
return { messages: digest, total_unread: totalUnread, shown: digest.length };
|
|
2463
|
+
}
|
|
2391
2464
|
function escapeCsvField(value) {
|
|
2392
2465
|
if (value === null || value === undefined)
|
|
2393
2466
|
return "";
|
|
@@ -2571,6 +2644,39 @@ function searchMessages(opts) {
|
|
|
2571
2644
|
return { ...msg, snippet: null, relevance_score: 0 };
|
|
2572
2645
|
});
|
|
2573
2646
|
}
|
|
2647
|
+
function listUnreadCounts(agent) {
|
|
2648
|
+
const db2 = getDb();
|
|
2649
|
+
if (agent) {
|
|
2650
|
+
const rows2 = db2.prepare(`
|
|
2651
|
+
SELECT
|
|
2652
|
+
space,
|
|
2653
|
+
COUNT(CASE WHEN read_at IS NULL AND (to_agent = ? OR to_agent IS NULL OR to_agent = '') THEN 1 END) AS unread_count,
|
|
2654
|
+
MAX(created_at) AS latest_message_at
|
|
2655
|
+
FROM messages
|
|
2656
|
+
WHERE space IN (
|
|
2657
|
+
SELECT DISTINCT space FROM space_members WHERE agent = ?
|
|
2658
|
+
UNION
|
|
2659
|
+
SELECT DISTINCT space FROM messages WHERE to_agent = ? AND space IS NOT NULL
|
|
2660
|
+
)
|
|
2661
|
+
GROUP BY space
|
|
2662
|
+
HAVING COUNT(*) > 0
|
|
2663
|
+
ORDER BY unread_count DESC, latest_message_at DESC
|
|
2664
|
+
`).all(agent, agent, agent);
|
|
2665
|
+
return rows2;
|
|
2666
|
+
}
|
|
2667
|
+
const rows = db2.prepare(`
|
|
2668
|
+
SELECT
|
|
2669
|
+
space,
|
|
2670
|
+
COUNT(CASE WHEN read_at IS NULL THEN 1 END) AS unread_count,
|
|
2671
|
+
MAX(created_at) AS latest_message_at
|
|
2672
|
+
FROM messages
|
|
2673
|
+
WHERE space IS NOT NULL
|
|
2674
|
+
GROUP BY space
|
|
2675
|
+
HAVING COUNT(*) > 0
|
|
2676
|
+
ORDER BY unread_count DESC, latest_message_at DESC
|
|
2677
|
+
`).all();
|
|
2678
|
+
return rows;
|
|
2679
|
+
}
|
|
2574
2680
|
var init_messages = __esm(() => {
|
|
2575
2681
|
init_db();
|
|
2576
2682
|
init_webhooks();
|
|
@@ -3431,6 +3537,9 @@ function isActiveSession(lastSeenAt) {
|
|
|
3431
3537
|
const nowMs = Date.now();
|
|
3432
3538
|
return nowMs - lastSeenMs < CONFLICT_THRESHOLD_SECONDS * 1000;
|
|
3433
3539
|
}
|
|
3540
|
+
function isAgentConflict(result) {
|
|
3541
|
+
return result.conflict === true;
|
|
3542
|
+
}
|
|
3434
3543
|
function registerAgent(name, sessionId, role, projectId) {
|
|
3435
3544
|
const db2 = getDb();
|
|
3436
3545
|
const normalizedName = name.trim().toLowerCase();
|
|
@@ -4207,7 +4316,7 @@ var init_poll = __esm(() => {
|
|
|
4207
4316
|
var require_package = __commonJS((exports, module) => {
|
|
4208
4317
|
module.exports = {
|
|
4209
4318
|
name: "@hasna/conversations",
|
|
4210
|
-
version: "0.2.
|
|
4319
|
+
version: "0.2.3",
|
|
4211
4320
|
description: "Real-time CLI messaging for AI agents",
|
|
4212
4321
|
type: "module",
|
|
4213
4322
|
bin: {
|
|
@@ -33216,6 +33325,7 @@ function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", e
|
|
|
33216
33325
|
const db2 = getDb();
|
|
33217
33326
|
return db2.transaction(() => {
|
|
33218
33327
|
cleanExpiredLocks();
|
|
33328
|
+
releaseStaleAgentLocks();
|
|
33219
33329
|
const existing = db2.prepare(`
|
|
33220
33330
|
SELECT * FROM resource_locks
|
|
33221
33331
|
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
@@ -33242,6 +33352,55 @@ function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", e
|
|
|
33242
33352
|
return { acquired: true, lock };
|
|
33243
33353
|
}).immediate();
|
|
33244
33354
|
}
|
|
33355
|
+
function bulkAcquireLock(resources, agentId) {
|
|
33356
|
+
const db2 = getDb();
|
|
33357
|
+
return db2.transaction(() => {
|
|
33358
|
+
cleanExpiredLocks();
|
|
33359
|
+
releaseStaleAgentLocks();
|
|
33360
|
+
const acquired = [];
|
|
33361
|
+
for (const { resource_type, resource_id, lock_type = "advisory", expiry_ms = DEFAULT_LOCK_EXPIRY_MS } of resources) {
|
|
33362
|
+
const existing = db2.prepare(`
|
|
33363
|
+
SELECT * FROM resource_locks
|
|
33364
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
33365
|
+
`).get(resource_type, resource_id, lock_type);
|
|
33366
|
+
if (existing && existing.agent_id !== agentId) {
|
|
33367
|
+
throw { _bulkConflict: true, resource_type, resource_id, held_by: existing.agent_id };
|
|
33368
|
+
}
|
|
33369
|
+
const expiresAt = new Date(Date.now() + expiry_ms).toISOString().slice(0, -1);
|
|
33370
|
+
if (existing) {
|
|
33371
|
+
db2.prepare(`
|
|
33372
|
+
UPDATE resource_locks SET expires_at = ?, locked_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
33373
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
33374
|
+
`).run(expiresAt, resource_type, resource_id, lock_type);
|
|
33375
|
+
} else {
|
|
33376
|
+
db2.prepare(`
|
|
33377
|
+
INSERT INTO resource_locks (resource_type, resource_id, agent_id, lock_type, locked_at, expires_at)
|
|
33378
|
+
VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
33379
|
+
`).run(resource_type, resource_id, agentId, lock_type, expiresAt);
|
|
33380
|
+
}
|
|
33381
|
+
const lock = db2.prepare(`
|
|
33382
|
+
SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
33383
|
+
`).get(resource_type, resource_id, lock_type);
|
|
33384
|
+
acquired.push(lock);
|
|
33385
|
+
}
|
|
33386
|
+
return { acquired: true, locks: acquired };
|
|
33387
|
+
}).immediate();
|
|
33388
|
+
}
|
|
33389
|
+
function tryBulkAcquireLock(resources, agentId) {
|
|
33390
|
+
try {
|
|
33391
|
+
return bulkAcquireLock(resources, agentId);
|
|
33392
|
+
} catch (err) {
|
|
33393
|
+
const e = err;
|
|
33394
|
+
if (e?._bulkConflict) {
|
|
33395
|
+
return {
|
|
33396
|
+
acquired: false,
|
|
33397
|
+
locks: [],
|
|
33398
|
+
blocked_by: { resource_type: e.resource_type, resource_id: e.resource_id, held_by: e.held_by }
|
|
33399
|
+
};
|
|
33400
|
+
}
|
|
33401
|
+
throw err;
|
|
33402
|
+
}
|
|
33403
|
+
}
|
|
33245
33404
|
function releaseLock(resourceType, resourceId, agentId) {
|
|
33246
33405
|
const db2 = getDb();
|
|
33247
33406
|
const result = db2.prepare(`
|
|
@@ -33253,6 +33412,7 @@ function releaseLock(resourceType, resourceId, agentId) {
|
|
|
33253
33412
|
function checkLock(resourceType, resourceId) {
|
|
33254
33413
|
const db2 = getDb();
|
|
33255
33414
|
cleanExpiredLocks();
|
|
33415
|
+
releaseStaleAgentLocks();
|
|
33256
33416
|
return db2.prepare(`
|
|
33257
33417
|
SELECT * FROM resource_locks
|
|
33258
33418
|
WHERE resource_type = ? AND resource_id = ?
|
|
@@ -33260,6 +33420,17 @@ function checkLock(resourceType, resourceId) {
|
|
|
33260
33420
|
LIMIT 1
|
|
33261
33421
|
`).get(resourceType, resourceId);
|
|
33262
33422
|
}
|
|
33423
|
+
function releaseStaleAgentLocks() {
|
|
33424
|
+
const db2 = getDb();
|
|
33425
|
+
const result = db2.prepare(`
|
|
33426
|
+
DELETE FROM resource_locks
|
|
33427
|
+
WHERE LOWER(agent_id) IN (
|
|
33428
|
+
SELECT LOWER(agent) FROM agent_presence
|
|
33429
|
+
WHERE last_seen_at < strftime('%Y-%m-%dT%H:%M:%f', 'now', '-${STALE_HEARTBEAT_SECONDS} seconds')
|
|
33430
|
+
)
|
|
33431
|
+
`).run();
|
|
33432
|
+
return result.changes;
|
|
33433
|
+
}
|
|
33263
33434
|
function cleanExpiredLocks() {
|
|
33264
33435
|
const db2 = getDb();
|
|
33265
33436
|
const result = db2.prepare(`
|
|
@@ -33270,6 +33441,7 @@ function cleanExpiredLocks() {
|
|
|
33270
33441
|
function listLocks(opts) {
|
|
33271
33442
|
const db2 = getDb();
|
|
33272
33443
|
cleanExpiredLocks();
|
|
33444
|
+
releaseStaleAgentLocks();
|
|
33273
33445
|
let query = "SELECT * FROM resource_locks WHERE 1=1";
|
|
33274
33446
|
const params = [];
|
|
33275
33447
|
if (opts?.resource_type) {
|
|
@@ -33283,10 +33455,36 @@ function listLocks(opts) {
|
|
|
33283
33455
|
query += " ORDER BY locked_at ASC";
|
|
33284
33456
|
return db2.prepare(query).all(...params);
|
|
33285
33457
|
}
|
|
33286
|
-
|
|
33458
|
+
function listLocksEnriched(opts) {
|
|
33459
|
+
const locks = listLocks(opts);
|
|
33460
|
+
const db2 = getDb();
|
|
33461
|
+
const nowMs = Date.now();
|
|
33462
|
+
return locks.map((lock) => {
|
|
33463
|
+
const lockedMs = new Date(lock.locked_at + "Z").getTime();
|
|
33464
|
+
const expiresMs = new Date(lock.expires_at + "Z").getTime();
|
|
33465
|
+
const presenceRow = db2.prepare(`
|
|
33466
|
+
SELECT role, status, last_seen_at, project_id FROM agent_presence WHERE LOWER(agent) = LOWER(?)
|
|
33467
|
+
`).get(lock.agent_id);
|
|
33468
|
+
const agent = presenceRow ? {
|
|
33469
|
+
role: presenceRow.role ?? null,
|
|
33470
|
+
status: presenceRow.status ?? null,
|
|
33471
|
+
online: presenceRow.last_seen_at ? nowMs - new Date(presenceRow.last_seen_at + "Z").getTime() < 60000 : false,
|
|
33472
|
+
last_seen_at: presenceRow.last_seen_at ?? null,
|
|
33473
|
+
project_id: presenceRow.project_id ?? null
|
|
33474
|
+
} : null;
|
|
33475
|
+
return {
|
|
33476
|
+
...lock,
|
|
33477
|
+
locked_seconds_ago: Math.round((nowMs - lockedMs) / 1000),
|
|
33478
|
+
expires_in_seconds: Math.round((expiresMs - nowMs) / 1000),
|
|
33479
|
+
agent
|
|
33480
|
+
};
|
|
33481
|
+
});
|
|
33482
|
+
}
|
|
33483
|
+
var DEFAULT_LOCK_EXPIRY_MS, STALE_HEARTBEAT_SECONDS;
|
|
33287
33484
|
var init_locks = __esm(() => {
|
|
33288
33485
|
init_db();
|
|
33289
33486
|
DEFAULT_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
33487
|
+
STALE_HEARTBEAT_SECONDS = 30 * 60;
|
|
33290
33488
|
});
|
|
33291
33489
|
|
|
33292
33490
|
// src/mcp/index.ts
|
|
@@ -33369,7 +33567,8 @@ var init_mcp2 = __esm(() => {
|
|
|
33369
33567
|
project_id: exports_external.string().optional(),
|
|
33370
33568
|
since: exports_external.string().optional(),
|
|
33371
33569
|
limit: exports_external.coerce.number().optional(),
|
|
33372
|
-
unread_only: exports_external.coerce.boolean().optional()
|
|
33570
|
+
unread_only: exports_external.coerce.boolean().optional(),
|
|
33571
|
+
mark_read: exports_external.coerce.boolean().optional()
|
|
33373
33572
|
}
|
|
33374
33573
|
}, async (args) => {
|
|
33375
33574
|
const agent = resolveIdentity(args.from);
|
|
@@ -33377,6 +33576,9 @@ var init_mcp2 = __esm(() => {
|
|
|
33377
33576
|
...args,
|
|
33378
33577
|
project_id: args.project_id ?? resolveProjectId(undefined, agent)
|
|
33379
33578
|
});
|
|
33579
|
+
if (args.mark_read !== false && messages.length > 0) {
|
|
33580
|
+
markReadByIds(messages.map((m) => m.id));
|
|
33581
|
+
}
|
|
33380
33582
|
return {
|
|
33381
33583
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
33382
33584
|
};
|
|
@@ -33535,6 +33737,17 @@ var init_mcp2 = __esm(() => {
|
|
|
33535
33737
|
content: [{ type: "text", text: JSON.stringify(spaces) }]
|
|
33536
33738
|
};
|
|
33537
33739
|
});
|
|
33740
|
+
server.registerTool("list_unread_counts", {
|
|
33741
|
+
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.",
|
|
33742
|
+
inputSchema: {
|
|
33743
|
+
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.")
|
|
33744
|
+
}
|
|
33745
|
+
}, async (args) => {
|
|
33746
|
+
const counts = listUnreadCounts(args.agent);
|
|
33747
|
+
return {
|
|
33748
|
+
content: [{ type: "text", text: JSON.stringify(counts) }]
|
|
33749
|
+
};
|
|
33750
|
+
});
|
|
33538
33751
|
server.registerTool("send_to_space", {
|
|
33539
33752
|
description: "Post a message to a space.",
|
|
33540
33753
|
inputSchema: {
|
|
@@ -33572,15 +33785,36 @@ var init_mcp2 = __esm(() => {
|
|
|
33572
33785
|
inputSchema: {
|
|
33573
33786
|
space: exports_external.string(),
|
|
33574
33787
|
since: exports_external.string().optional(),
|
|
33575
|
-
limit: exports_external.coerce.number().optional()
|
|
33788
|
+
limit: exports_external.coerce.number().optional(),
|
|
33789
|
+
mark_read: exports_external.coerce.boolean().optional()
|
|
33576
33790
|
}
|
|
33577
33791
|
}, async (args) => {
|
|
33578
|
-
const { space, since, limit } = args;
|
|
33792
|
+
const { space, since, limit, mark_read } = args;
|
|
33579
33793
|
const messages = readMessages({ space, since, limit });
|
|
33794
|
+
if (mark_read !== false && messages.length > 0) {
|
|
33795
|
+
markReadByIds(messages.map((m) => m.id));
|
|
33796
|
+
}
|
|
33580
33797
|
return {
|
|
33581
33798
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
33582
33799
|
};
|
|
33583
33800
|
});
|
|
33801
|
+
server.registerTool("read_digest", {
|
|
33802
|
+
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.",
|
|
33803
|
+
inputSchema: {
|
|
33804
|
+
space: exports_external.string().optional(),
|
|
33805
|
+
session_id: exports_external.string().optional(),
|
|
33806
|
+
to: exports_external.string().optional(),
|
|
33807
|
+
since: exports_external.string().optional(),
|
|
33808
|
+
limit: exports_external.coerce.number().optional(),
|
|
33809
|
+
project_id: exports_external.string().optional()
|
|
33810
|
+
}
|
|
33811
|
+
}, async (args) => {
|
|
33812
|
+
const { space, session_id, to, since, limit, project_id } = args;
|
|
33813
|
+
const result = readDigest({ space, session_id, to, since, limit, project_id });
|
|
33814
|
+
return {
|
|
33815
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
33816
|
+
};
|
|
33817
|
+
});
|
|
33584
33818
|
server.registerTool("join_space", {
|
|
33585
33819
|
description: "Join a space as a member.",
|
|
33586
33820
|
inputSchema: {
|
|
@@ -34113,18 +34347,29 @@ var init_mcp2 = __esm(() => {
|
|
|
34113
34347
|
return { content: [{ type: "text", text: JSON.stringify(summary) }] };
|
|
34114
34348
|
});
|
|
34115
34349
|
server.registerTool("acquire_lock", {
|
|
34116
|
-
description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock.",
|
|
34350
|
+
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.",
|
|
34117
34351
|
inputSchema: {
|
|
34118
34352
|
resource_type: exports_external.string(),
|
|
34119
34353
|
resource_id: exports_external.string(),
|
|
34120
34354
|
lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
|
|
34121
34355
|
expiry_ms: exports_external.coerce.number().optional(),
|
|
34122
|
-
from: exports_external.string().optional()
|
|
34356
|
+
from: exports_external.string().optional(),
|
|
34357
|
+
auto_dm: exports_external.coerce.boolean().optional()
|
|
34123
34358
|
}
|
|
34124
34359
|
}, async (args) => {
|
|
34125
|
-
const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam } = args;
|
|
34360
|
+
const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam, auto_dm } = args;
|
|
34126
34361
|
const agent = resolveIdentity(fromParam);
|
|
34127
34362
|
const result = acquireLock(resource_type, resource_id, agent, lock_type ?? "advisory", expiry_ms);
|
|
34363
|
+
if (!result.acquired && result.held_by && auto_dm !== false) {
|
|
34364
|
+
try {
|
|
34365
|
+
sendMessage({
|
|
34366
|
+
from: agent,
|
|
34367
|
+
to: result.held_by,
|
|
34368
|
+
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\`.`,
|
|
34369
|
+
priority: "high"
|
|
34370
|
+
});
|
|
34371
|
+
} catch {}
|
|
34372
|
+
}
|
|
34128
34373
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
34129
34374
|
});
|
|
34130
34375
|
server.registerTool("release_lock", {
|
|
@@ -34151,15 +34396,50 @@ var init_mcp2 = __esm(() => {
|
|
|
34151
34396
|
return { content: [{ type: "text", text: JSON.stringify(lock ?? { locked: false }) }] };
|
|
34152
34397
|
});
|
|
34153
34398
|
server.registerTool("list_locks", {
|
|
34154
|
-
description: "List all active (non-expired) locks. Filter by resource_type or agent.",
|
|
34399
|
+
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.",
|
|
34155
34400
|
inputSchema: {
|
|
34156
34401
|
resource_type: exports_external.string().optional(),
|
|
34157
34402
|
agent_id: exports_external.string().optional()
|
|
34158
34403
|
}
|
|
34159
34404
|
}, async (args) => {
|
|
34160
|
-
const locks =
|
|
34405
|
+
const locks = listLocksEnriched({ resource_type: args.resource_type, agent_id: args.agent_id });
|
|
34161
34406
|
return { content: [{ type: "text", text: JSON.stringify(locks) }] };
|
|
34162
34407
|
});
|
|
34408
|
+
server.registerTool("bulk_acquire_lock", {
|
|
34409
|
+
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.",
|
|
34410
|
+
inputSchema: {
|
|
34411
|
+
resources: exports_external.array(exports_external.object({
|
|
34412
|
+
resource_type: exports_external.string(),
|
|
34413
|
+
resource_id: exports_external.string(),
|
|
34414
|
+
lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
|
|
34415
|
+
expiry_ms: exports_external.coerce.number().optional()
|
|
34416
|
+
})),
|
|
34417
|
+
from: exports_external.string().optional(),
|
|
34418
|
+
auto_dm: exports_external.coerce.boolean().optional()
|
|
34419
|
+
}
|
|
34420
|
+
}, async (args) => {
|
|
34421
|
+
const agent = resolveIdentity(args.from);
|
|
34422
|
+
const result = tryBulkAcquireLock(args.resources, agent);
|
|
34423
|
+
if (!result.acquired && result.blocked_by && args.auto_dm !== false) {
|
|
34424
|
+
try {
|
|
34425
|
+
sendMessage({
|
|
34426
|
+
from: agent,
|
|
34427
|
+
to: result.blocked_by.held_by,
|
|
34428
|
+
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.`,
|
|
34429
|
+
priority: "high"
|
|
34430
|
+
});
|
|
34431
|
+
} catch {}
|
|
34432
|
+
}
|
|
34433
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
34434
|
+
});
|
|
34435
|
+
server.registerTool("clean_expired_locks", {
|
|
34436
|
+
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.",
|
|
34437
|
+
inputSchema: {}
|
|
34438
|
+
}, async () => {
|
|
34439
|
+
const stale = releaseStaleAgentLocks();
|
|
34440
|
+
const expired = cleanExpiredLocks();
|
|
34441
|
+
return { content: [{ type: "text", text: JSON.stringify({ released_stale_agent: stale, released_expired: expired, total: stale + expired }) }] };
|
|
34442
|
+
});
|
|
34163
34443
|
server.registerTool("get_thread_replies", {
|
|
34164
34444
|
description: "Get all replies in a thread for a given parent message ID.",
|
|
34165
34445
|
inputSchema: {
|
|
@@ -34343,6 +34623,7 @@ var init_mcp2 = __esm(() => {
|
|
|
34343
34623
|
const all = [
|
|
34344
34624
|
"send_message",
|
|
34345
34625
|
"read_messages",
|
|
34626
|
+
"read_digest",
|
|
34346
34627
|
"list_sessions",
|
|
34347
34628
|
"reply",
|
|
34348
34629
|
"mark_read",
|
|
@@ -34381,9 +34662,11 @@ var init_mcp2 = __esm(() => {
|
|
|
34381
34662
|
"get_reactions",
|
|
34382
34663
|
"get_reaction_summary",
|
|
34383
34664
|
"acquire_lock",
|
|
34665
|
+
"bulk_acquire_lock",
|
|
34384
34666
|
"release_lock",
|
|
34385
34667
|
"check_lock",
|
|
34386
34668
|
"list_locks",
|
|
34669
|
+
"clean_expired_locks",
|
|
34387
34670
|
"get_thread_replies",
|
|
34388
34671
|
"set_focus",
|
|
34389
34672
|
"get_focus",
|
|
@@ -34409,16 +34692,18 @@ var init_mcp2 = __esm(() => {
|
|
|
34409
34692
|
}, async ({ names }) => {
|
|
34410
34693
|
const descriptions = {
|
|
34411
34694
|
send_message: "Send DM to agent. Required: to, content. Optional: from?, priority?(low|normal|high|urgent), blocking?",
|
|
34412
|
-
read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?",
|
|
34695
|
+
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)",
|
|
34696
|
+
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?",
|
|
34413
34697
|
list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
|
|
34414
34698
|
reply: "Reply to a message in same session. Required: message_id, content. Optional: from?",
|
|
34415
34699
|
mark_read: "Mark messages as read. Optional: from?, ids?(array), all?(bool \u2014 mark all unread)",
|
|
34416
34700
|
search_messages: "Full-text search messages. Required: query. Optional: space?, from?, to?, limit?",
|
|
34417
34701
|
export_messages: "Export messages as JSON or CSV. Optional: space?, session_id?, from?, since?, until?, format?(json|csv)",
|
|
34418
34702
|
create_space: "Create space and auto-join. Required: name. Optional: from?, description?, parent_id?(max 3 levels), project_id?",
|
|
34703
|
+
list_unread_counts: "Get unread message counts per space (no content). Ideal for session start triage. Optional: agent?(filter to agent's spaces)",
|
|
34419
34704
|
list_spaces: "List spaces with member/message counts. Optional: project_id?, parent_id?(use 'null' for top-level), include_archived?",
|
|
34420
34705
|
send_to_space: "Post message to space. Required: space, content. Optional: from?, priority?(low|normal|high|urgent), blocking?",
|
|
34421
|
-
read_space: "Read messages in a space. Required: space. Optional: since?(ISO), limit?",
|
|
34706
|
+
read_space: "Read messages in a space. Required: space. Optional: since?(ISO), limit?, mark_read?(default true \u2014 auto-marks returned messages as read)",
|
|
34422
34707
|
join_space: "Join a space. Required: space. Optional: from?",
|
|
34423
34708
|
leave_space: "Leave a space. Required: space. Optional: from?",
|
|
34424
34709
|
update_space: "Update space fields. Required: name. Optional: description?, parent_id?(use 'null' to remove), project_id?(use 'null' to remove)",
|
|
@@ -34447,10 +34732,12 @@ var init_mcp2 = __esm(() => {
|
|
|
34447
34732
|
remove_reaction: "Remove emoji reaction from a message. Required: message_id, emoji. Optional: from?",
|
|
34448
34733
|
get_reactions: "Get all reactions for a message. Required: message_id",
|
|
34449
34734
|
get_reaction_summary: "Get emoji counts + agent lists for a message. Required: message_id",
|
|
34450
|
-
acquire_lock: "Acquire advisory/exclusive lock on a resource. Required: resource_type, resource_id. Optional: lock_type?(advisory|exclusive), expiry_ms?, from?",
|
|
34735
|
+
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)",
|
|
34736
|
+
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.",
|
|
34451
34737
|
release_lock: "Release lock held by agent. Required: resource_type, resource_id. Optional: from?",
|
|
34452
34738
|
check_lock: "Check if resource is locked and who holds it. Required: resource_type, resource_id",
|
|
34453
|
-
list_locks: "List active locks. Optional: resource_type?, agent_id?",
|
|
34739
|
+
list_locks: "List active locks enriched with agent presence + time context. Optional: resource_type?, agent_id?",
|
|
34740
|
+
clean_expired_locks: "Release expired locks + locks held by agents with stale heartbeat (>30 min). Returns {released_stale_agent, released_expired, total}",
|
|
34454
34741
|
get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
|
|
34455
34742
|
set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
|
|
34456
34743
|
get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
|
|
@@ -36099,6 +36386,33 @@ program2.command("read").description("Read messages").option("--session <id>", "
|
|
|
36099
36386
|
}
|
|
36100
36387
|
closeDb();
|
|
36101
36388
|
});
|
|
36389
|
+
program2.command("digest").description("Show unread message digest (preview only, auto-marks read)").argument("[space]", "Space name to digest (omit for DMs)").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to show", parseInt).option("--to <agent>", "Filter by recipient (for DMs)").option("--json", "Output as JSON").action((spaceArg, opts) => {
|
|
36390
|
+
const result = readDigest({
|
|
36391
|
+
space: spaceArg || undefined,
|
|
36392
|
+
since: opts.since,
|
|
36393
|
+
limit: opts.limit,
|
|
36394
|
+
to: opts.to
|
|
36395
|
+
});
|
|
36396
|
+
if (opts.json) {
|
|
36397
|
+
console.log(JSON.stringify(result, null, 2));
|
|
36398
|
+
} else {
|
|
36399
|
+
console.log(chalk3.bold(`Unread: ${result.total_unread} total, showing ${result.shown}`));
|
|
36400
|
+
if (result.messages.length === 0) {
|
|
36401
|
+
console.log(chalk3.dim(" No unread messages."));
|
|
36402
|
+
} else {
|
|
36403
|
+
for (const msg of result.messages) {
|
|
36404
|
+
const time3 = chalk3.dim(msg.created_at.slice(11, 19));
|
|
36405
|
+
const from = chalk3.cyan(msg.from);
|
|
36406
|
+
const dest = msg.space ? chalk3.magenta(`#${msg.space}`) : chalk3.yellow(msg.to ?? "?");
|
|
36407
|
+
const priority = msg.priority !== "normal" ? chalk3.red(` [${msg.priority}]`) : "";
|
|
36408
|
+
const att = msg.has_attachments ? chalk3.dim(" \uD83D\uDCCE") : "";
|
|
36409
|
+
console.log(`${time3} ${from} \u2192 ${dest}${priority}${att}`);
|
|
36410
|
+
console.log(` ${chalk3.dim(msg.preview)}`);
|
|
36411
|
+
}
|
|
36412
|
+
}
|
|
36413
|
+
}
|
|
36414
|
+
closeDb();
|
|
36415
|
+
});
|
|
36102
36416
|
program2.command("search").description("Search messages by content").argument("<query>", "Search query string").option("--space <name>", "Filter by space").option("--from <agent>", "Filter by sender").option("--to <agent>", "Filter by recipient").option("--limit <n>", "Max results to return", parseInt).option("--json", "Output as JSON").action((query, opts) => {
|
|
36103
36417
|
const q = typeof query === "string" ? query.trim() : "";
|
|
36104
36418
|
if (!q) {
|
|
@@ -37184,6 +37498,85 @@ agents.command("rename").description("Rename an agent in the presence list").arg
|
|
|
37184
37498
|
}
|
|
37185
37499
|
closeDb();
|
|
37186
37500
|
});
|
|
37501
|
+
agents.command("register").description("Register an agent with conflict detection (30 min active window)").argument("<name>", "Agent name to register").option("--session <id>", "Session ID (default: random UUID)").option("--role <role>", "Agent role (default: agent)").option("--project <id>", "Project ID to lock agent to").option("--force", "Force takeover even if another session is active").option("--json", "Output as JSON").action((name, opts) => {
|
|
37502
|
+
const agentName = (typeof name === "string" ? name : "").trim();
|
|
37503
|
+
if (!agentName) {
|
|
37504
|
+
console.error(chalk3.red("Agent name is required."));
|
|
37505
|
+
process.exit(1);
|
|
37506
|
+
}
|
|
37507
|
+
const sessionId = opts.session || crypto.randomUUID();
|
|
37508
|
+
const result = registerAgent(agentName, sessionId, opts.role, opts.project);
|
|
37509
|
+
if (isAgentConflict(result)) {
|
|
37510
|
+
if (opts.json) {
|
|
37511
|
+
console.log(JSON.stringify(result));
|
|
37512
|
+
} else {
|
|
37513
|
+
console.error(chalk3.red(`Conflict: agent "${agentName}" is already active (last seen: ${result.last_seen_at}).`));
|
|
37514
|
+
console.error(chalk3.dim("Use --force or wait 30 minutes for the session to expire."));
|
|
37515
|
+
}
|
|
37516
|
+
process.exit(1);
|
|
37517
|
+
}
|
|
37518
|
+
if (opts.json) {
|
|
37519
|
+
console.log(JSON.stringify(result));
|
|
37520
|
+
} else {
|
|
37521
|
+
const action = result.took_over ? chalk3.yellow("took over") : result.created ? chalk3.green("registered") : chalk3.cyan("updated");
|
|
37522
|
+
console.log(` ${action} ${chalk3.bold(result.agent.agent)} session: ${chalk3.dim(sessionId)}`);
|
|
37523
|
+
}
|
|
37524
|
+
closeDb();
|
|
37525
|
+
});
|
|
37526
|
+
agents.command("heartbeat").description("Send a presence heartbeat to mark yourself as active").option("--from <agent>", "Agent identity (default: CONVERSATIONS_AGENT_ID or auto)").option("--status <status>", "Status: online, busy, idle (default: online)").option("--json", "Output as JSON").action((opts) => {
|
|
37527
|
+
const agent = resolveIdentity(opts.from);
|
|
37528
|
+
const status = opts.status || "online";
|
|
37529
|
+
heartbeat(agent, status);
|
|
37530
|
+
if (opts.json) {
|
|
37531
|
+
console.log(JSON.stringify({ agent, status, heartbeat: true }));
|
|
37532
|
+
} else {
|
|
37533
|
+
console.log(` ${chalk3.green("\u2665")} ${chalk3.cyan(agent)} ${chalk3.dim(status)}`);
|
|
37534
|
+
}
|
|
37535
|
+
closeDb();
|
|
37536
|
+
});
|
|
37537
|
+
var focus = program2.command("focus").description("Manage agent project focus");
|
|
37538
|
+
focus.command("set").description("Set your project focus \u2014 scopes read operations to this project").argument("<project>", "Project ID or name").option("--from <agent>", "Agent identity").option("--json", "Output as JSON").action((projectArg, opts) => {
|
|
37539
|
+
const agent = resolveIdentity(opts.from);
|
|
37540
|
+
const project2 = getProject(projectArg) || getProjectByName(projectArg);
|
|
37541
|
+
if (!project2) {
|
|
37542
|
+
console.error(chalk3.red(`Project "${projectArg}" not found.`));
|
|
37543
|
+
process.exit(1);
|
|
37544
|
+
}
|
|
37545
|
+
getDb().prepare("UPDATE agent_presence SET project_id = ? WHERE agent = ?").run(project2.id, agent);
|
|
37546
|
+
if (opts.json) {
|
|
37547
|
+
console.log(JSON.stringify({ agent, project_id: project2.id, project_name: project2.name, focused: true }));
|
|
37548
|
+
} else {
|
|
37549
|
+
console.log(` ${chalk3.green("focused")} ${chalk3.cyan(agent)} \u2192 ${chalk3.bold(project2.name)} ${chalk3.dim(`(${project2.id})`)}`);
|
|
37550
|
+
}
|
|
37551
|
+
closeDb();
|
|
37552
|
+
});
|
|
37553
|
+
focus.command("clear").description("Clear your project focus").option("--from <agent>", "Agent identity").option("--json", "Output as JSON").action((opts) => {
|
|
37554
|
+
const agent = resolveIdentity(opts.from);
|
|
37555
|
+
getDb().prepare("UPDATE agent_presence SET project_id = NULL WHERE agent = ?").run(agent);
|
|
37556
|
+
if (opts.json) {
|
|
37557
|
+
console.log(JSON.stringify({ agent, project_id: null, focused: false }));
|
|
37558
|
+
} else {
|
|
37559
|
+
console.log(` ${chalk3.yellow("unfocused")} ${chalk3.cyan(agent)}`);
|
|
37560
|
+
}
|
|
37561
|
+
closeDb();
|
|
37562
|
+
});
|
|
37563
|
+
focus.command("get").description("Show current project focus").option("--from <agent>", "Agent identity").option("--json", "Output as JSON").action((opts) => {
|
|
37564
|
+
const agent = resolveIdentity(opts.from);
|
|
37565
|
+
const presence = getPresence(agent);
|
|
37566
|
+
const projectId = presence?.project_id ?? null;
|
|
37567
|
+
const project2 = projectId ? getProject(projectId) || null : null;
|
|
37568
|
+
if (opts.json) {
|
|
37569
|
+
console.log(JSON.stringify({ agent, project_id: projectId, project_name: project2?.name ?? null }));
|
|
37570
|
+
} else {
|
|
37571
|
+
if (projectId) {
|
|
37572
|
+
const name = project2?.name ?? chalk3.dim("(unknown)");
|
|
37573
|
+
console.log(` ${chalk3.cyan(agent)} focused on ${chalk3.bold(name)} ${chalk3.dim(`(${projectId})`)}`);
|
|
37574
|
+
} else {
|
|
37575
|
+
console.log(` ${chalk3.cyan(agent)} ${chalk3.dim("no focus set")}`);
|
|
37576
|
+
}
|
|
37577
|
+
}
|
|
37578
|
+
closeDb();
|
|
37579
|
+
});
|
|
37187
37580
|
program2.command("whoami").description("Show current agent identity and online status").option("--from <agent>", "Explicit agent identity").action((opts) => {
|
|
37188
37581
|
const envValue = process.env.CONVERSATIONS_AGENT_ID?.trim();
|
|
37189
37582
|
const agent = resolveIdentity(opts.from);
|