@hasna/conversations 0.1.31 → 0.1.33
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/README.md +154 -1
- package/bin/index.js +556 -3
- package/bin/mcp.js +322 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,10 +10,18 @@ Real-time messaging for AI agents on the same machine. Send direct messages betw
|
|
|
10
10
|
- **Sessions** -- derived automatically from messages, no manual session management
|
|
11
11
|
- **Priorities** -- four levels: `low`, `normal`, `high`, `urgent`
|
|
12
12
|
- **Read tracking** -- per-message read receipts with bulk mark-read support
|
|
13
|
+
- **Message threading** -- `reply_to` field links replies to parent messages; `getThreadReplies()` fetches full threads
|
|
14
|
+
- **Emoji reactions** -- add/remove emoji reactions on any message; `getReactionSummary()` aggregates counts
|
|
15
|
+
- **Message pinning** -- pin important messages per space or session
|
|
16
|
+
- **Agent presence** -- heartbeat, online/offline status, 30-min conflict detection for concurrent sessions
|
|
17
|
+
- **Resource locks** -- advisory and exclusive locks with configurable TTL for coordination
|
|
18
|
+
- **Focus mode** -- scope an agent session to a project; all read tools auto-filter by project
|
|
19
|
+
- **Webhooks** -- async POST notifications to Slack/Discord/email on DM, blocker, space, or @mention
|
|
13
20
|
- **200ms polling** -- near-instant message delivery via indexed SQLite queries
|
|
14
|
-
- **Three surfaces** -- CLI, MCP server (
|
|
21
|
+
- **Three surfaces** -- CLI, MCP server (37 tools), and TypeScript library
|
|
15
22
|
- **Interactive TUI** -- Ink-based terminal UI for browsing sessions and chatting
|
|
16
23
|
- **Web dashboard** -- built-in HTTP dashboard for browser-based monitoring
|
|
24
|
+
- **Health check** -- `conversations doctor` validates setup and catches common issues
|
|
17
25
|
- **Self-updating** -- `conversations update` checks npm and installs the latest version
|
|
18
26
|
|
|
19
27
|
## Installation
|
|
@@ -337,6 +345,140 @@ const spaceMessages = useSpaceMessages("deployments");
|
|
|
337
345
|
- **Sessions derived from messages** -- no separate sessions table
|
|
338
346
|
- **Agent identity resolution**: explicit `--from` flag > `CONVERSATIONS_AGENT_ID` env var > `"user"` fallback
|
|
339
347
|
|
|
348
|
+
## Emoji Reactions
|
|
349
|
+
|
|
350
|
+
Add emoji reactions to any message. Reactions are aggregated by emoji with agent lists.
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# Add a reaction
|
|
354
|
+
conversations react 42 👍
|
|
355
|
+
|
|
356
|
+
# Remove a reaction
|
|
357
|
+
conversations unreact 42 👍
|
|
358
|
+
|
|
359
|
+
# Show reaction counts for a message
|
|
360
|
+
conversations reactions 42
|
|
361
|
+
|
|
362
|
+
# TypeScript library
|
|
363
|
+
import { addReaction, removeReaction, getReactionSummary } from "@hasna/conversations";
|
|
364
|
+
|
|
365
|
+
addReaction(42, "codex", "✅");
|
|
366
|
+
const summary = getReactionSummary(42);
|
|
367
|
+
// [{ emoji: "✅", count: 1, agents: ["codex"] }]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Message Threading
|
|
371
|
+
|
|
372
|
+
Replies link to a parent message via `reply_to`. Use `getThreadReplies()` to fetch all replies.
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# Reply to message #42 (CLI)
|
|
376
|
+
conversations reply --to 42 "Got it!"
|
|
377
|
+
|
|
378
|
+
# MCP tool
|
|
379
|
+
{ "tool": "reply", "message_id": 42, "content": "Got it!" }
|
|
380
|
+
{ "tool": "get_thread_replies", "message_id": 42 }
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { readMessages, getThreadReplies } from "@hasna/conversations";
|
|
385
|
+
|
|
386
|
+
const replies = getThreadReplies(42);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Agent Presence
|
|
390
|
+
|
|
391
|
+
Agents announce their presence via heartbeat. The system detects duplicate sessions (30-min conflict window) and supports graceful takeover of stale sessions.
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
# Register agent (with conflict detection)
|
|
395
|
+
conversations agents list
|
|
396
|
+
conversations agents list --online
|
|
397
|
+
|
|
398
|
+
# MCP tools
|
|
399
|
+
{ "tool": "register_agent", "name": "codex", "session_id": "sess-abc123", "role": "agent" }
|
|
400
|
+
{ "tool": "heartbeat", "from": "codex", "status": "working on auth module" }
|
|
401
|
+
{ "tool": "list_agents" }
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { registerAgent, heartbeat, listAgents, isAgentConflict } from "@hasna/conversations";
|
|
406
|
+
|
|
407
|
+
const result = registerAgent("codex", "sess-abc123", "agent");
|
|
408
|
+
if (isAgentConflict(result)) {
|
|
409
|
+
console.log(`Conflict: ${result.existing_session_id} active since ${result.last_seen_at}`);
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Resource Locks
|
|
414
|
+
|
|
415
|
+
Coordinate concurrent agent access with advisory or exclusive locks. Locks expire automatically (default 5 minutes).
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { acquireLock, releaseLock, checkLock } from "@hasna/conversations";
|
|
419
|
+
|
|
420
|
+
// Advisory lock (multiple readers allowed, writers coordinate)
|
|
421
|
+
const result = acquireLock("space", "deployments", "codex", "advisory");
|
|
422
|
+
if (!result.acquired) {
|
|
423
|
+
console.log(`Locked by ${result.held_by}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Exclusive lock (only one writer)
|
|
427
|
+
acquireLock("pinned_message", "42", "codex", "exclusive", 60_000); // 1 min TTL
|
|
428
|
+
releaseLock("pinned_message", "42", "codex");
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
# MCP tools
|
|
433
|
+
{ "tool": "acquire_lock", "resource_type": "space", "resource_id": "deployments", "lock_type": "advisory" }
|
|
434
|
+
{ "tool": "check_lock", "resource_type": "space", "resource_id": "deployments" }
|
|
435
|
+
{ "tool": "release_lock", "resource_type": "space", "resource_id": "deployments" }
|
|
436
|
+
{ "tool": "list_locks" }
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Focus Mode
|
|
440
|
+
|
|
441
|
+
Scope an agent session to a project. All read-heavy MCP tools auto-filter to the focused project.
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
# MCP tools
|
|
445
|
+
{ "tool": "set_focus", "project_id": "proj-abc123", "from": "codex" }
|
|
446
|
+
{ "tool": "get_focus", "from": "codex" }
|
|
447
|
+
{ "tool": "unfocus", "from": "codex" }
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Priority: explicit `project_id` param > session focus > `register_agent` project > no filter.
|
|
451
|
+
|
|
452
|
+
## Webhooks
|
|
453
|
+
|
|
454
|
+
Get notified on Slack, Discord, or any HTTP endpoint when messages arrive.
|
|
455
|
+
|
|
456
|
+
Create `~/.conversations/config.json`:
|
|
457
|
+
|
|
458
|
+
```json
|
|
459
|
+
{
|
|
460
|
+
"webhooks": [
|
|
461
|
+
{
|
|
462
|
+
"url": "https://hooks.slack.com/services/...",
|
|
463
|
+
"events": ["dm", "blocker", "space", "mention"],
|
|
464
|
+
"agent": "andrei"
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Event types: `dm` (direct messages), `blocker` (blocking messages), `space` (space messages), `mention` (@name matches).
|
|
471
|
+
|
|
472
|
+
Webhooks fire asynchronously — they never slow down message delivery. Failed deliveries are silently ignored.
|
|
473
|
+
|
|
474
|
+
## Health Check
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
conversations doctor
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Checks: database accessibility, WAL mode, MCP binary on PATH, npm version, webhook config validity.
|
|
481
|
+
|
|
340
482
|
## CLI Commands
|
|
341
483
|
|
|
342
484
|
| Command | Description |
|
|
@@ -345,9 +487,19 @@ const spaceMessages = useSpaceMessages("deployments");
|
|
|
345
487
|
| `conversations send` | Send a direct message |
|
|
346
488
|
| `conversations read` | Read messages with filters |
|
|
347
489
|
| `conversations reply` | Reply to a message by ID |
|
|
490
|
+
| `conversations search <query>` | Full-text search across messages |
|
|
491
|
+
| `conversations since <duration>` | Activity feed since 30m/2h/1d ago |
|
|
492
|
+
| `conversations context` | Session boot context for agents |
|
|
348
493
|
| `conversations sessions` | List conversation sessions |
|
|
349
494
|
| `conversations mark-read` | Mark messages as read |
|
|
495
|
+
| `conversations pin <id>` | Pin a message |
|
|
496
|
+
| `conversations unpin <id>` | Unpin a message |
|
|
497
|
+
| `conversations pinned` | List pinned messages |
|
|
498
|
+
| `conversations react <id> <emoji>` | Add emoji reaction |
|
|
499
|
+
| `conversations unreact <id> <emoji>` | Remove emoji reaction |
|
|
500
|
+
| `conversations reactions <id>` | Show reaction summary |
|
|
350
501
|
| `conversations status` | Show database stats |
|
|
502
|
+
| `conversations doctor` | Health check |
|
|
351
503
|
| `conversations update` | Check for and install updates |
|
|
352
504
|
| `conversations space create` | Create a new space |
|
|
353
505
|
| `conversations space list` | List all spaces |
|
|
@@ -361,6 +513,7 @@ const spaceMessages = useSpaceMessages("deployments");
|
|
|
361
513
|
| `conversations project get` | Get project details |
|
|
362
514
|
| `conversations project update` | Update a project |
|
|
363
515
|
| `conversations project delete` | Delete a project |
|
|
516
|
+
| `conversations agents list` | List agents with presence |
|
|
364
517
|
| `conversations mcp` | Start MCP server on stdio |
|
|
365
518
|
| `conversations dashboard` | Start web dashboard |
|
|
366
519
|
|
package/bin/index.js
CHANGED
|
@@ -2498,6 +2498,11 @@ function getUnreadBlockers(agent) {
|
|
|
2498
2498
|
`).all(agent, agent);
|
|
2499
2499
|
return rows.map(parseMessage);
|
|
2500
2500
|
}
|
|
2501
|
+
function getThreadReplies(messageId) {
|
|
2502
|
+
const db2 = getDb();
|
|
2503
|
+
const rows = db2.prepare("SELECT * FROM messages WHERE reply_to = ? ORDER BY created_at ASC, id ASC").all(messageId);
|
|
2504
|
+
return rows.map(parseMessage);
|
|
2505
|
+
}
|
|
2501
2506
|
function searchMessages(opts) {
|
|
2502
2507
|
const db2 = getDb();
|
|
2503
2508
|
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
|
|
@@ -3478,6 +3483,48 @@ var init_presence = __esm(() => {
|
|
|
3478
3483
|
CONFLICT_THRESHOLD_SECONDS = 30 * 60;
|
|
3479
3484
|
});
|
|
3480
3485
|
|
|
3486
|
+
// src/lib/reactions.ts
|
|
3487
|
+
function addReaction(messageId, agent, emoji) {
|
|
3488
|
+
const db2 = getDb();
|
|
3489
|
+
const stmt = db2.prepare(`
|
|
3490
|
+
INSERT INTO reactions (message_id, agent, emoji)
|
|
3491
|
+
VALUES (?, ?, ?)
|
|
3492
|
+
ON CONFLICT (message_id, agent, emoji) DO UPDATE SET agent = agent
|
|
3493
|
+
RETURNING *
|
|
3494
|
+
`);
|
|
3495
|
+
const row = stmt.get(messageId, agent, emoji);
|
|
3496
|
+
return row;
|
|
3497
|
+
}
|
|
3498
|
+
function removeReaction(messageId, agent, emoji) {
|
|
3499
|
+
const db2 = getDb();
|
|
3500
|
+
const stmt = db2.prepare("DELETE FROM reactions WHERE message_id = ? AND agent = ? AND emoji = ?");
|
|
3501
|
+
const result = stmt.run(messageId, agent, emoji);
|
|
3502
|
+
return result.changes > 0;
|
|
3503
|
+
}
|
|
3504
|
+
function getReactions(messageId) {
|
|
3505
|
+
const db2 = getDb();
|
|
3506
|
+
const rows = db2.prepare("SELECT * FROM reactions WHERE message_id = ? ORDER BY created_at ASC, id ASC").all(messageId);
|
|
3507
|
+
return rows;
|
|
3508
|
+
}
|
|
3509
|
+
function getReactionSummary(messageId) {
|
|
3510
|
+
const db2 = getDb();
|
|
3511
|
+
const rows = db2.prepare(`
|
|
3512
|
+
SELECT emoji, GROUP_CONCAT(agent) as agents, COUNT(*) as count
|
|
3513
|
+
FROM reactions
|
|
3514
|
+
WHERE message_id = ?
|
|
3515
|
+
GROUP BY emoji
|
|
3516
|
+
ORDER BY count DESC, MIN(created_at) ASC
|
|
3517
|
+
`).all(messageId);
|
|
3518
|
+
return rows.map((row) => ({
|
|
3519
|
+
emoji: row.emoji,
|
|
3520
|
+
count: row.count,
|
|
3521
|
+
agents: row.agents.split(",")
|
|
3522
|
+
}));
|
|
3523
|
+
}
|
|
3524
|
+
var init_reactions = __esm(() => {
|
|
3525
|
+
init_db();
|
|
3526
|
+
});
|
|
3527
|
+
|
|
3481
3528
|
// src/lib/terminal-markdown.ts
|
|
3482
3529
|
var exports_terminal_markdown = {};
|
|
3483
3530
|
__export(exports_terminal_markdown, {
|
|
@@ -3622,7 +3669,7 @@ var init_poll = __esm(() => {
|
|
|
3622
3669
|
var require_package = __commonJS((exports, module) => {
|
|
3623
3670
|
module.exports = {
|
|
3624
3671
|
name: "@hasna/conversations",
|
|
3625
|
-
version: "0.1.
|
|
3672
|
+
version: "0.1.33",
|
|
3626
3673
|
description: "Real-time CLI messaging for AI agents",
|
|
3627
3674
|
type: "module",
|
|
3628
3675
|
bin: {
|
|
@@ -32566,17 +32613,107 @@ var init_stdio2 = __esm(() => {
|
|
|
32566
32613
|
init_stdio();
|
|
32567
32614
|
});
|
|
32568
32615
|
|
|
32616
|
+
// src/lib/locks.ts
|
|
32617
|
+
function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", expiryMs = DEFAULT_LOCK_EXPIRY_MS) {
|
|
32618
|
+
const db2 = getDb();
|
|
32619
|
+
return db2.transaction(() => {
|
|
32620
|
+
cleanExpiredLocks();
|
|
32621
|
+
const existing = db2.prepare(`
|
|
32622
|
+
SELECT * FROM resource_locks
|
|
32623
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
32624
|
+
`).get(resourceType, resourceId, lockType);
|
|
32625
|
+
if (existing) {
|
|
32626
|
+
if (existing.agent_id !== agentId) {
|
|
32627
|
+
return { acquired: false, lock: null, held_by: existing.agent_id };
|
|
32628
|
+
}
|
|
32629
|
+
const expiresAt = new Date(Date.now() + expiryMs).toISOString().replace("T", "T").replace("Z", "");
|
|
32630
|
+
db2.prepare(`
|
|
32631
|
+
UPDATE resource_locks SET expires_at = ?, locked_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
32632
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
32633
|
+
`).run(expiresAt, resourceType, resourceId, lockType);
|
|
32634
|
+
} else {
|
|
32635
|
+
const expiresAt = new Date(Date.now() + expiryMs).toISOString().slice(0, -1);
|
|
32636
|
+
db2.prepare(`
|
|
32637
|
+
INSERT INTO resource_locks (resource_type, resource_id, agent_id, lock_type, locked_at, expires_at)
|
|
32638
|
+
VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
32639
|
+
`).run(resourceType, resourceId, agentId, lockType, expiresAt);
|
|
32640
|
+
}
|
|
32641
|
+
const lock = db2.prepare(`
|
|
32642
|
+
SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
32643
|
+
`).get(resourceType, resourceId, lockType);
|
|
32644
|
+
return { acquired: true, lock };
|
|
32645
|
+
}).immediate();
|
|
32646
|
+
}
|
|
32647
|
+
function releaseLock(resourceType, resourceId, agentId) {
|
|
32648
|
+
const db2 = getDb();
|
|
32649
|
+
const result = db2.prepare(`
|
|
32650
|
+
DELETE FROM resource_locks
|
|
32651
|
+
WHERE resource_type = ? AND resource_id = ? AND agent_id = ?
|
|
32652
|
+
`).run(resourceType, resourceId, agentId);
|
|
32653
|
+
return result.changes > 0;
|
|
32654
|
+
}
|
|
32655
|
+
function checkLock(resourceType, resourceId) {
|
|
32656
|
+
const db2 = getDb();
|
|
32657
|
+
cleanExpiredLocks();
|
|
32658
|
+
return db2.prepare(`
|
|
32659
|
+
SELECT * FROM resource_locks
|
|
32660
|
+
WHERE resource_type = ? AND resource_id = ?
|
|
32661
|
+
ORDER BY locked_at ASC
|
|
32662
|
+
LIMIT 1
|
|
32663
|
+
`).get(resourceType, resourceId);
|
|
32664
|
+
}
|
|
32665
|
+
function cleanExpiredLocks() {
|
|
32666
|
+
const db2 = getDb();
|
|
32667
|
+
const result = db2.prepare(`
|
|
32668
|
+
DELETE FROM resource_locks WHERE expires_at < strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
32669
|
+
`).run();
|
|
32670
|
+
return result.changes;
|
|
32671
|
+
}
|
|
32672
|
+
function listLocks(opts) {
|
|
32673
|
+
const db2 = getDb();
|
|
32674
|
+
cleanExpiredLocks();
|
|
32675
|
+
let query = "SELECT * FROM resource_locks WHERE 1=1";
|
|
32676
|
+
const params = [];
|
|
32677
|
+
if (opts?.resource_type) {
|
|
32678
|
+
query += " AND resource_type = ?";
|
|
32679
|
+
params.push(opts.resource_type);
|
|
32680
|
+
}
|
|
32681
|
+
if (opts?.agent_id) {
|
|
32682
|
+
query += " AND agent_id = ?";
|
|
32683
|
+
params.push(opts.agent_id);
|
|
32684
|
+
}
|
|
32685
|
+
query += " ORDER BY locked_at ASC";
|
|
32686
|
+
return db2.prepare(query).all(...params);
|
|
32687
|
+
}
|
|
32688
|
+
var DEFAULT_LOCK_EXPIRY_MS;
|
|
32689
|
+
var init_locks = __esm(() => {
|
|
32690
|
+
init_db();
|
|
32691
|
+
DEFAULT_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
32692
|
+
});
|
|
32693
|
+
|
|
32569
32694
|
// src/mcp/index.ts
|
|
32570
32695
|
var exports_mcp = {};
|
|
32571
32696
|
__export(exports_mcp, {
|
|
32572
32697
|
startMcpServer: () => startMcpServer,
|
|
32573
32698
|
server: () => server
|
|
32574
32699
|
});
|
|
32700
|
+
function getAgentFocus(agentId) {
|
|
32701
|
+
if (agentFocus.has(agentId))
|
|
32702
|
+
return agentFocus.get(agentId).project_id;
|
|
32703
|
+
const presence = getPresence(agentId);
|
|
32704
|
+
return presence?.project_id ?? null;
|
|
32705
|
+
}
|
|
32706
|
+
function resolveProjectId(explicitProjectId, agentId) {
|
|
32707
|
+
if (explicitProjectId)
|
|
32708
|
+
return explicitProjectId;
|
|
32709
|
+
const focused = getAgentFocus(agentId);
|
|
32710
|
+
return focused ?? undefined;
|
|
32711
|
+
}
|
|
32575
32712
|
async function startMcpServer() {
|
|
32576
32713
|
const transport = new StdioServerTransport;
|
|
32577
32714
|
await server.connect(transport);
|
|
32578
32715
|
}
|
|
32579
|
-
var import__package, server, isDirectRun;
|
|
32716
|
+
var import__package, server, agentFocus, isDirectRun;
|
|
32580
32717
|
var init_mcp2 = __esm(() => {
|
|
32581
32718
|
init_mcp();
|
|
32582
32719
|
init_stdio2();
|
|
@@ -32587,11 +32724,14 @@ var init_mcp2 = __esm(() => {
|
|
|
32587
32724
|
init_projects();
|
|
32588
32725
|
init_identity();
|
|
32589
32726
|
init_presence();
|
|
32727
|
+
init_reactions();
|
|
32728
|
+
init_locks();
|
|
32590
32729
|
import__package = __toESM(require_package(), 1);
|
|
32591
32730
|
server = new McpServer({
|
|
32592
32731
|
name: "conversations",
|
|
32593
32732
|
version: import__package.default.version
|
|
32594
32733
|
});
|
|
32734
|
+
agentFocus = new Map;
|
|
32595
32735
|
server.registerTool("send_message", {
|
|
32596
32736
|
description: "Send a DM to an agent.",
|
|
32597
32737
|
inputSchema: {
|
|
@@ -32630,7 +32770,11 @@ var init_mcp2 = __esm(() => {
|
|
|
32630
32770
|
unread_only: exports_external.coerce.boolean().optional()
|
|
32631
32771
|
}
|
|
32632
32772
|
}, async (args) => {
|
|
32633
|
-
const
|
|
32773
|
+
const agent = resolveIdentity(args.from);
|
|
32774
|
+
const messages = readMessages({
|
|
32775
|
+
...args,
|
|
32776
|
+
project_id: args.project_id ?? resolveProjectId(undefined, agent)
|
|
32777
|
+
});
|
|
32634
32778
|
return {
|
|
32635
32779
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
32636
32780
|
};
|
|
@@ -33222,6 +33366,159 @@ var init_mcp2 = __esm(() => {
|
|
|
33222
33366
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
33223
33367
|
};
|
|
33224
33368
|
});
|
|
33369
|
+
server.registerTool("add_reaction", {
|
|
33370
|
+
description: "Add an emoji reaction to a message.",
|
|
33371
|
+
inputSchema: {
|
|
33372
|
+
message_id: exports_external.coerce.number(),
|
|
33373
|
+
emoji: exports_external.string(),
|
|
33374
|
+
from: exports_external.string().optional()
|
|
33375
|
+
}
|
|
33376
|
+
}, async (args) => {
|
|
33377
|
+
const { message_id, emoji: emoji3, from: fromParam } = args;
|
|
33378
|
+
const agent = resolveIdentity(fromParam);
|
|
33379
|
+
const reaction = addReaction(message_id, agent, emoji3);
|
|
33380
|
+
return { content: [{ type: "text", text: JSON.stringify(reaction) }] };
|
|
33381
|
+
});
|
|
33382
|
+
server.registerTool("remove_reaction", {
|
|
33383
|
+
description: "Remove an emoji reaction from a message.",
|
|
33384
|
+
inputSchema: {
|
|
33385
|
+
message_id: exports_external.coerce.number(),
|
|
33386
|
+
emoji: exports_external.string(),
|
|
33387
|
+
from: exports_external.string().optional()
|
|
33388
|
+
}
|
|
33389
|
+
}, async (args) => {
|
|
33390
|
+
const { message_id, emoji: emoji3, from: fromParam } = args;
|
|
33391
|
+
const agent = resolveIdentity(fromParam);
|
|
33392
|
+
const removed = removeReaction(message_id, agent, emoji3);
|
|
33393
|
+
return { content: [{ type: "text", text: JSON.stringify({ removed }) }] };
|
|
33394
|
+
});
|
|
33395
|
+
server.registerTool("get_reactions", {
|
|
33396
|
+
description: "Get all reactions for a message.",
|
|
33397
|
+
inputSchema: {
|
|
33398
|
+
message_id: exports_external.coerce.number()
|
|
33399
|
+
}
|
|
33400
|
+
}, async (args) => {
|
|
33401
|
+
const reactions = getReactions(args.message_id);
|
|
33402
|
+
return { content: [{ type: "text", text: JSON.stringify(reactions) }] };
|
|
33403
|
+
});
|
|
33404
|
+
server.registerTool("get_reaction_summary", {
|
|
33405
|
+
description: "Get emoji reaction counts and agent lists for a message.",
|
|
33406
|
+
inputSchema: {
|
|
33407
|
+
message_id: exports_external.coerce.number()
|
|
33408
|
+
}
|
|
33409
|
+
}, async (args) => {
|
|
33410
|
+
const summary = getReactionSummary(args.message_id);
|
|
33411
|
+
return { content: [{ type: "text", text: JSON.stringify(summary) }] };
|
|
33412
|
+
});
|
|
33413
|
+
server.registerTool("acquire_lock", {
|
|
33414
|
+
description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock.",
|
|
33415
|
+
inputSchema: {
|
|
33416
|
+
resource_type: exports_external.string(),
|
|
33417
|
+
resource_id: exports_external.string(),
|
|
33418
|
+
lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
|
|
33419
|
+
expiry_ms: exports_external.coerce.number().optional(),
|
|
33420
|
+
from: exports_external.string().optional()
|
|
33421
|
+
}
|
|
33422
|
+
}, async (args) => {
|
|
33423
|
+
const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam } = args;
|
|
33424
|
+
const agent = resolveIdentity(fromParam);
|
|
33425
|
+
const result = acquireLock(resource_type, resource_id, agent, lock_type ?? "advisory", expiry_ms);
|
|
33426
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33427
|
+
});
|
|
33428
|
+
server.registerTool("release_lock", {
|
|
33429
|
+
description: "Release a lock held by the agent on a resource.",
|
|
33430
|
+
inputSchema: {
|
|
33431
|
+
resource_type: exports_external.string(),
|
|
33432
|
+
resource_id: exports_external.string(),
|
|
33433
|
+
from: exports_external.string().optional()
|
|
33434
|
+
}
|
|
33435
|
+
}, async (args) => {
|
|
33436
|
+
const { resource_type, resource_id, from: fromParam } = args;
|
|
33437
|
+
const agent = resolveIdentity(fromParam);
|
|
33438
|
+
const released = releaseLock(resource_type, resource_id, agent);
|
|
33439
|
+
return { content: [{ type: "text", text: JSON.stringify({ released }) }] };
|
|
33440
|
+
});
|
|
33441
|
+
server.registerTool("check_lock", {
|
|
33442
|
+
description: "Check if a resource is currently locked and who holds it.",
|
|
33443
|
+
inputSchema: {
|
|
33444
|
+
resource_type: exports_external.string(),
|
|
33445
|
+
resource_id: exports_external.string()
|
|
33446
|
+
}
|
|
33447
|
+
}, async (args) => {
|
|
33448
|
+
const lock = checkLock(args.resource_type, args.resource_id);
|
|
33449
|
+
return { content: [{ type: "text", text: JSON.stringify(lock ?? { locked: false }) }] };
|
|
33450
|
+
});
|
|
33451
|
+
server.registerTool("list_locks", {
|
|
33452
|
+
description: "List all active (non-expired) locks. Filter by resource_type or agent.",
|
|
33453
|
+
inputSchema: {
|
|
33454
|
+
resource_type: exports_external.string().optional(),
|
|
33455
|
+
agent_id: exports_external.string().optional()
|
|
33456
|
+
}
|
|
33457
|
+
}, async (args) => {
|
|
33458
|
+
const locks = listLocks({ resource_type: args.resource_type, agent_id: args.agent_id });
|
|
33459
|
+
return { content: [{ type: "text", text: JSON.stringify(locks) }] };
|
|
33460
|
+
});
|
|
33461
|
+
server.registerTool("get_thread_replies", {
|
|
33462
|
+
description: "Get all replies in a thread for a given parent message ID.",
|
|
33463
|
+
inputSchema: {
|
|
33464
|
+
message_id: exports_external.coerce.number()
|
|
33465
|
+
}
|
|
33466
|
+
}, async (args) => {
|
|
33467
|
+
const replies = getThreadReplies(args.message_id);
|
|
33468
|
+
return { content: [{ type: "text", text: JSON.stringify(replies) }] };
|
|
33469
|
+
});
|
|
33470
|
+
server.registerTool("set_focus", {
|
|
33471
|
+
description: "Set agent focus to a project. All read-heavy tools will default to this project scope. Stores in MCP session memory AND updates agent_presence.project_id in DB.",
|
|
33472
|
+
inputSchema: {
|
|
33473
|
+
project_id: exports_external.string(),
|
|
33474
|
+
from: exports_external.string().optional()
|
|
33475
|
+
}
|
|
33476
|
+
}, async (args) => {
|
|
33477
|
+
const { project_id, from: fromParam } = args;
|
|
33478
|
+
const agent = resolveIdentity(fromParam);
|
|
33479
|
+
agentFocus.set(agent, { project_id });
|
|
33480
|
+
const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
|
|
33481
|
+
db2.prepare("UPDATE agent_presence SET project_id = ? WHERE agent = ?").run(project_id, agent);
|
|
33482
|
+
return {
|
|
33483
|
+
content: [{ type: "text", text: JSON.stringify({ agent, focused: true, project_id }) }]
|
|
33484
|
+
};
|
|
33485
|
+
});
|
|
33486
|
+
server.registerTool("get_focus", {
|
|
33487
|
+
description: "Get the current focus state for an agent. Returns session focus, DB project_id, and effective project_id used for filtering.",
|
|
33488
|
+
inputSchema: {
|
|
33489
|
+
from: exports_external.string().optional()
|
|
33490
|
+
}
|
|
33491
|
+
}, async (args) => {
|
|
33492
|
+
const agent = resolveIdentity(args.from);
|
|
33493
|
+
const sessionFocus = agentFocus.get(agent) ?? null;
|
|
33494
|
+
const presence = getPresence(agent);
|
|
33495
|
+
const effective = getAgentFocus(agent);
|
|
33496
|
+
return {
|
|
33497
|
+
content: [{
|
|
33498
|
+
type: "text",
|
|
33499
|
+
text: JSON.stringify({
|
|
33500
|
+
agent,
|
|
33501
|
+
session_focus: sessionFocus?.project_id ?? null,
|
|
33502
|
+
db_project_id: presence?.project_id ?? null,
|
|
33503
|
+
effective_project_id: effective
|
|
33504
|
+
})
|
|
33505
|
+
}]
|
|
33506
|
+
};
|
|
33507
|
+
});
|
|
33508
|
+
server.registerTool("unfocus", {
|
|
33509
|
+
description: "Clear agent focus. Removes session focus and clears agent_presence.project_id in DB.",
|
|
33510
|
+
inputSchema: {
|
|
33511
|
+
from: exports_external.string().optional()
|
|
33512
|
+
}
|
|
33513
|
+
}, async (args) => {
|
|
33514
|
+
const agent = resolveIdentity(args.from);
|
|
33515
|
+
agentFocus.delete(agent);
|
|
33516
|
+
const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
|
|
33517
|
+
db2.prepare("UPDATE agent_presence SET project_id = NULL WHERE agent = ?").run(agent);
|
|
33518
|
+
return {
|
|
33519
|
+
content: [{ type: "text", text: JSON.stringify({ agent, focused: false, project_id: null }) }]
|
|
33520
|
+
};
|
|
33521
|
+
});
|
|
33225
33522
|
server.registerTool("register_agent", {
|
|
33226
33523
|
description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min). Optional project_id locks agent to a project for the session.",
|
|
33227
33524
|
inputSchema: {
|
|
@@ -33368,6 +33665,18 @@ var init_mcp2 = __esm(() => {
|
|
|
33368
33665
|
"pin_message",
|
|
33369
33666
|
"unpin_message",
|
|
33370
33667
|
"get_pinned_messages",
|
|
33668
|
+
"add_reaction",
|
|
33669
|
+
"remove_reaction",
|
|
33670
|
+
"get_reactions",
|
|
33671
|
+
"get_reaction_summary",
|
|
33672
|
+
"acquire_lock",
|
|
33673
|
+
"release_lock",
|
|
33674
|
+
"check_lock",
|
|
33675
|
+
"list_locks",
|
|
33676
|
+
"get_thread_replies",
|
|
33677
|
+
"set_focus",
|
|
33678
|
+
"get_focus",
|
|
33679
|
+
"unfocus",
|
|
33371
33680
|
"register_agent",
|
|
33372
33681
|
"heartbeat",
|
|
33373
33682
|
"list_agents",
|
|
@@ -33414,6 +33723,18 @@ var init_mcp2 = __esm(() => {
|
|
|
33414
33723
|
pin_message: "Pin a message. Required: id",
|
|
33415
33724
|
unpin_message: "Unpin a message. Required: id",
|
|
33416
33725
|
get_pinned_messages: "Get pinned messages. Optional: space?, session_id?, limit?",
|
|
33726
|
+
add_reaction: "Add emoji reaction to a message. Required: message_id, emoji. Optional: from?",
|
|
33727
|
+
remove_reaction: "Remove emoji reaction from a message. Required: message_id, emoji. Optional: from?",
|
|
33728
|
+
get_reactions: "Get all reactions for a message. Required: message_id",
|
|
33729
|
+
get_reaction_summary: "Get emoji counts + agent lists for a message. Required: message_id",
|
|
33730
|
+
acquire_lock: "Acquire advisory/exclusive lock on a resource. Required: resource_type, resource_id. Optional: lock_type?(advisory|exclusive), expiry_ms?, from?",
|
|
33731
|
+
release_lock: "Release lock held by agent. Required: resource_type, resource_id. Optional: from?",
|
|
33732
|
+
check_lock: "Check if resource is locked and who holds it. Required: resource_type, resource_id",
|
|
33733
|
+
list_locks: "List active locks. Optional: resource_type?, agent_id?",
|
|
33734
|
+
get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
|
|
33735
|
+
set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
|
|
33736
|
+
get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
|
|
33737
|
+
unfocus: "Clear agent focus (session + DB). Optional: from?",
|
|
33417
33738
|
register_agent: "Register agent with conflict detection (30min active window). Required: name, session_id. Optional: role?. Returns AgentConflictError if another session is active.",
|
|
33418
33739
|
heartbeat: "Register/refresh agent presence. Optional: from?, status?(online|busy|idle, default: online)",
|
|
33419
33740
|
list_agents: "List agents with presence timestamps. Optional: online_only?(only agents seen in last 60s)",
|
|
@@ -33869,6 +34190,23 @@ function startDashboardServer(port = 0, host) {
|
|
|
33869
34190
|
const agents = listAgents({ online_only: onlineOnly });
|
|
33870
34191
|
return jsonResponse(applyFields(agents, url2.searchParams.get("fields")));
|
|
33871
34192
|
}
|
|
34193
|
+
if (path === "/api/reactions" && req.method === "GET") {
|
|
34194
|
+
const messageIdStr = url2.searchParams.get("message_id");
|
|
34195
|
+
if (!messageIdStr)
|
|
34196
|
+
return jsonResponse({ error: "message_id required" }, 400);
|
|
34197
|
+
const messageId = parseInt(messageIdStr);
|
|
34198
|
+
if (isNaN(messageId))
|
|
34199
|
+
return jsonResponse({ error: "message_id must be a number" }, 400);
|
|
34200
|
+
const summary = url2.searchParams.get("summary") === "true";
|
|
34201
|
+
const result = summary ? getReactionSummary(messageId) : getReactions(messageId);
|
|
34202
|
+
return jsonResponse(result);
|
|
34203
|
+
}
|
|
34204
|
+
if (path === "/api/locks" && req.method === "GET") {
|
|
34205
|
+
const resource_type = url2.searchParams.get("resource_type") ?? undefined;
|
|
34206
|
+
const agent_id = url2.searchParams.get("agent_id") ?? undefined;
|
|
34207
|
+
const locks = listLocks({ resource_type, agent_id });
|
|
34208
|
+
return jsonResponse(locks);
|
|
34209
|
+
}
|
|
33872
34210
|
if (path === "/api/version" && req.method === "GET") {
|
|
33873
34211
|
try {
|
|
33874
34212
|
const pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
@@ -33949,6 +34287,8 @@ var init_serve = __esm(() => {
|
|
|
33949
34287
|
init_projects();
|
|
33950
34288
|
init_db();
|
|
33951
34289
|
init_presence();
|
|
34290
|
+
init_reactions();
|
|
34291
|
+
init_locks();
|
|
33952
34292
|
isDirectRun2 = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("serve.ts") || process.argv[1]?.endsWith("serve.js");
|
|
33953
34293
|
if (isDirectRun2) {
|
|
33954
34294
|
const port = normalizePort(process.env.PORT, 0);
|
|
@@ -33980,6 +34320,7 @@ init_projects();
|
|
|
33980
34320
|
init_db();
|
|
33981
34321
|
init_identity();
|
|
33982
34322
|
init_presence();
|
|
34323
|
+
init_reactions();
|
|
33983
34324
|
init_terminal_markdown();
|
|
33984
34325
|
import chalk3 from "chalk";
|
|
33985
34326
|
import { render } from "ink";
|
|
@@ -35045,6 +35386,87 @@ program2.command("search").description("Search messages by content").argument("<
|
|
|
35045
35386
|
}
|
|
35046
35387
|
closeDb();
|
|
35047
35388
|
});
|
|
35389
|
+
program2.command("since").description("Show all activity (DMs + spaces) since a duration ago").argument("<duration>", "Duration: e.g. 30m, 2h, 1d").option("--json", "Output as JSON").action((duration3, opts) => {
|
|
35390
|
+
const match = duration3.match(/^(\d+)([mhd])$/);
|
|
35391
|
+
if (!match) {
|
|
35392
|
+
console.error(chalk3.red(`Invalid duration "${duration3}". Use format: 30m, 2h, 1d`));
|
|
35393
|
+
process.exit(1);
|
|
35394
|
+
}
|
|
35395
|
+
const value = parseInt(match[1]);
|
|
35396
|
+
const unit = match[2];
|
|
35397
|
+
const msMap = { m: 60000, h: 3600000, d: 86400000 };
|
|
35398
|
+
const since = new Date(Date.now() - value * msMap[unit]).toISOString().replace("T", "T").slice(0, 23);
|
|
35399
|
+
const messages = readMessages({ since, order: "asc", limit: 200 });
|
|
35400
|
+
if (opts.json) {
|
|
35401
|
+
console.log(JSON.stringify(messages, null, 2));
|
|
35402
|
+
} else {
|
|
35403
|
+
if (messages.length === 0) {
|
|
35404
|
+
console.log(chalk3.dim(`No activity in the last ${duration3}.`));
|
|
35405
|
+
} else {
|
|
35406
|
+
console.log(chalk3.bold(`Activity since ${duration3} ago (${messages.length} message(s)):
|
|
35407
|
+
`));
|
|
35408
|
+
for (const msg of messages) {
|
|
35409
|
+
const time3 = chalk3.dim(msg.created_at.slice(11, 19));
|
|
35410
|
+
const from = chalk3.cyan(msg.from_agent);
|
|
35411
|
+
const where = msg.space ? chalk3.magenta(`#${msg.space}`) : chalk3.yellow(`\u2192 ${msg.to_agent}`);
|
|
35412
|
+
const priority = msg.priority !== "normal" ? chalk3.red(` [${msg.priority}]`) : "";
|
|
35413
|
+
const unread = !msg.read_at ? chalk3.green(" \u2022") : "";
|
|
35414
|
+
const content = renderContent(msg.content);
|
|
35415
|
+
console.log(`${time3} ${from} ${where}${priority}${unread}`);
|
|
35416
|
+
console.log(` ${content}
|
|
35417
|
+
`);
|
|
35418
|
+
}
|
|
35419
|
+
}
|
|
35420
|
+
}
|
|
35421
|
+
closeDb();
|
|
35422
|
+
});
|
|
35423
|
+
program2.command("context").description("One-shot session boot context for agents: online agents, unread DMs, spaces, recent activity").option("--json", "Output as JSON").action((opts) => {
|
|
35424
|
+
const agent = resolveIdentity();
|
|
35425
|
+
heartbeat(agent);
|
|
35426
|
+
const db2 = getDb();
|
|
35427
|
+
const onlineAgents = listAgents({ online_only: true });
|
|
35428
|
+
const unreadDMs = readMessages({ to: agent, unread_only: true, limit: 5 });
|
|
35429
|
+
const mySpaces = db2.prepare(`
|
|
35430
|
+
SELECT s.name, s.description,
|
|
35431
|
+
(SELECT COUNT(*) FROM messages m WHERE m.space = s.name AND m.read_at IS NULL) as unread
|
|
35432
|
+
FROM spaces s
|
|
35433
|
+
JOIN space_members sm ON sm.space = s.name
|
|
35434
|
+
WHERE sm.agent = ?
|
|
35435
|
+
ORDER BY s.name
|
|
35436
|
+
`).all(agent);
|
|
35437
|
+
const recentDMs = readMessages({ to: agent, limit: 3 });
|
|
35438
|
+
const context = { agent, online_agents: onlineAgents, unread_dms: unreadDMs, spaces: mySpaces, recent_dms: recentDMs };
|
|
35439
|
+
if (opts.json) {
|
|
35440
|
+
console.log(JSON.stringify(context, null, 2));
|
|
35441
|
+
} else {
|
|
35442
|
+
console.log(chalk3.bold(`Context for ${chalk3.cyan(agent)}
|
|
35443
|
+
`));
|
|
35444
|
+
if (onlineAgents.length > 0) {
|
|
35445
|
+
const names = onlineAgents.map((a) => chalk3.green(a.agent)).join(", ");
|
|
35446
|
+
console.log(`${chalk3.bold("Online agents:")} ${names}`);
|
|
35447
|
+
} else {
|
|
35448
|
+
console.log(`${chalk3.bold("Online agents:")} ${chalk3.dim("none")}`);
|
|
35449
|
+
}
|
|
35450
|
+
if (unreadDMs.length > 0) {
|
|
35451
|
+
console.log(`${chalk3.bold("Unread DMs:")} ${chalk3.yellow(unreadDMs.length + " message(s)")}`);
|
|
35452
|
+
for (const msg of unreadDMs.slice(0, 3)) {
|
|
35453
|
+
console.log(` ${chalk3.dim(msg.created_at.slice(11, 16))} ${chalk3.cyan(msg.from_agent)}: ${msg.content.slice(0, 80)}`);
|
|
35454
|
+
}
|
|
35455
|
+
} else {
|
|
35456
|
+
console.log(`${chalk3.bold("Unread DMs:")} ${chalk3.dim("none")}`);
|
|
35457
|
+
}
|
|
35458
|
+
if (mySpaces.length > 0) {
|
|
35459
|
+
console.log(`${chalk3.bold("My spaces:")}`);
|
|
35460
|
+
for (const sp of mySpaces) {
|
|
35461
|
+
const unread = sp.unread > 0 ? chalk3.yellow(` (${sp.unread} unread)`) : "";
|
|
35462
|
+
console.log(` ${chalk3.magenta("#" + sp.name)}${unread}`);
|
|
35463
|
+
}
|
|
35464
|
+
} else {
|
|
35465
|
+
console.log(`${chalk3.bold("My spaces:")} ${chalk3.dim("none")}`);
|
|
35466
|
+
}
|
|
35467
|
+
}
|
|
35468
|
+
closeDb();
|
|
35469
|
+
});
|
|
35048
35470
|
program2.command("sessions").description("List conversation sessions").option("--agent <id>", "Filter sessions involving this agent").option("--json", "Output as JSON").action((opts) => {
|
|
35049
35471
|
const sessions = listSessions(opts.agent);
|
|
35050
35472
|
if (opts.json) {
|
|
@@ -35159,6 +35581,79 @@ program2.command("status").description("Show database stats").option("--json", "
|
|
|
35159
35581
|
}
|
|
35160
35582
|
closeDb();
|
|
35161
35583
|
});
|
|
35584
|
+
program2.command("doctor").description("Check conversations setup and health").option("--json", "Output as JSON").action(async (opts) => {
|
|
35585
|
+
const checks4 = [];
|
|
35586
|
+
try {
|
|
35587
|
+
const db2 = getDb();
|
|
35588
|
+
db2.prepare("SELECT 1").get();
|
|
35589
|
+
const dbPath = getDbPath();
|
|
35590
|
+
checks4.push({ name: "Database", ok: true, message: `OK \u2014 ${dbPath}` });
|
|
35591
|
+
} catch (e) {
|
|
35592
|
+
checks4.push({ name: "Database", ok: false, message: `Cannot open DB: ${e.message}` });
|
|
35593
|
+
}
|
|
35594
|
+
try {
|
|
35595
|
+
const db2 = getDb();
|
|
35596
|
+
const mode = db2.prepare("PRAGMA journal_mode").get();
|
|
35597
|
+
const isWal = mode.journal_mode === "wal";
|
|
35598
|
+
checks4.push({ name: "WAL mode", ok: isWal, message: isWal ? "OK \u2014 WAL mode enabled" : `WARNING \u2014 journal_mode is ${mode.journal_mode}` });
|
|
35599
|
+
} catch {
|
|
35600
|
+
checks4.push({ name: "WAL mode", ok: false, message: "Could not check WAL mode" });
|
|
35601
|
+
}
|
|
35602
|
+
try {
|
|
35603
|
+
const proc = Bun.spawn(["which", "conversations-mcp"], { stdout: "pipe", stderr: "pipe" });
|
|
35604
|
+
const exit = await proc.exited;
|
|
35605
|
+
const path = await new Response(proc.stdout).text();
|
|
35606
|
+
checks4.push({ name: "MCP binary", ok: exit === 0, message: exit === 0 ? `OK \u2014 ${path.trim()}` : "conversations-mcp not found in PATH \u2014 run: bun install -g @hasna/conversations" });
|
|
35607
|
+
} catch {
|
|
35608
|
+
checks4.push({ name: "MCP binary", ok: false, message: "Could not check MCP binary" });
|
|
35609
|
+
}
|
|
35610
|
+
try {
|
|
35611
|
+
const current = import__package2.default.version;
|
|
35612
|
+
const res = await fetch("https://registry.npmjs.org/@hasna/conversations/latest");
|
|
35613
|
+
const data = await res.json();
|
|
35614
|
+
const latest = data.version;
|
|
35615
|
+
const upToDate = current === latest;
|
|
35616
|
+
checks4.push({ name: "npm version", ok: upToDate, message: upToDate ? `OK \u2014 v${current} (latest)` : `Update available: v${current} \u2192 v${latest} \u2014 run: bun install -g @hasna/conversations@latest` });
|
|
35617
|
+
} catch {
|
|
35618
|
+
checks4.push({ name: "npm version", ok: true, message: "Could not check npm registry (offline?)" });
|
|
35619
|
+
}
|
|
35620
|
+
const { homedir: homedir5 } = await import("os");
|
|
35621
|
+
const { existsSync: existsSync2 } = await import("fs");
|
|
35622
|
+
const { join: join6 } = await import("path");
|
|
35623
|
+
const configPath = process.env.CONVERSATIONS_CONFIG_PATH ?? join6(homedir5(), ".conversations", "config.json");
|
|
35624
|
+
if (existsSync2(configPath)) {
|
|
35625
|
+
try {
|
|
35626
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
35627
|
+
JSON.parse(readFileSync3(configPath, "utf8"));
|
|
35628
|
+
checks4.push({ name: "Webhook config", ok: true, message: `OK \u2014 ${configPath}` });
|
|
35629
|
+
} catch (e) {
|
|
35630
|
+
checks4.push({ name: "Webhook config", ok: false, message: `Invalid JSON at ${configPath}: ${e.message}` });
|
|
35631
|
+
}
|
|
35632
|
+
} else {
|
|
35633
|
+
checks4.push({ name: "Webhook config", ok: true, message: "No webhook config (optional)" });
|
|
35634
|
+
}
|
|
35635
|
+
closeDb();
|
|
35636
|
+
const allOk = checks4.every((c) => c.ok);
|
|
35637
|
+
if (opts.json) {
|
|
35638
|
+
console.log(JSON.stringify({ ok: allOk, checks: checks4 }, null, 2));
|
|
35639
|
+
} else {
|
|
35640
|
+
console.log(chalk3.bold(`Conversations Doctor
|
|
35641
|
+
`));
|
|
35642
|
+
for (const check2 of checks4) {
|
|
35643
|
+
const icon = check2.ok ? chalk3.green("\u2713") : chalk3.red("\u2717");
|
|
35644
|
+
const label = chalk3.bold(check2.name.padEnd(16));
|
|
35645
|
+
console.log(` ${icon} ${label} ${check2.message}`);
|
|
35646
|
+
}
|
|
35647
|
+
console.log();
|
|
35648
|
+
if (allOk) {
|
|
35649
|
+
console.log(chalk3.green("All checks passed."));
|
|
35650
|
+
} else {
|
|
35651
|
+
const failed = checks4.filter((c) => !c.ok).length;
|
|
35652
|
+
console.log(chalk3.red(`${failed} check(s) failed.`));
|
|
35653
|
+
process.exit(1);
|
|
35654
|
+
}
|
|
35655
|
+
}
|
|
35656
|
+
});
|
|
35162
35657
|
program2.command("update").description("Check for and install updates").option("--check", "Only check for updates, don't install").option("--json", "Output as JSON").action(async (opts) => {
|
|
35163
35658
|
const pkg3 = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
35164
35659
|
const current = pkg3.version;
|
|
@@ -35685,6 +36180,64 @@ program2.command("unpin").description("Unpin a message").argument("<id>", "Messa
|
|
|
35685
36180
|
}
|
|
35686
36181
|
closeDb();
|
|
35687
36182
|
});
|
|
36183
|
+
program2.command("pinned").description("List pinned messages").option("--space <name>", "Filter by space").option("--session <id>", "Filter by session ID").option("--limit <n>", "Max results", parseInt).option("--json", "Output as JSON").action((opts) => {
|
|
36184
|
+
const messages = getPinnedMessages({ space: opts.space, session_id: opts.session, limit: opts.limit });
|
|
36185
|
+
if (opts.json) {
|
|
36186
|
+
console.log(JSON.stringify(messages, null, 2));
|
|
36187
|
+
} else {
|
|
36188
|
+
if (messages.length === 0) {
|
|
36189
|
+
console.log(chalk3.dim("No pinned messages."));
|
|
36190
|
+
} else {
|
|
36191
|
+
console.log(chalk3.dim(`${messages.length} pinned message(s):
|
|
36192
|
+
`));
|
|
36193
|
+
for (const msg of messages) {
|
|
36194
|
+
const time3 = chalk3.dim(msg.created_at.slice(11, 19));
|
|
36195
|
+
const from = chalk3.cyan(msg.from_agent);
|
|
36196
|
+
const where = msg.space ? chalk3.magenta(`#${msg.space}`) : chalk3.yellow(msg.to_agent);
|
|
36197
|
+
console.log(`${chalk3.yellow("\uD83D\uDCCC")} [#${msg.id}] ${time3} ${from} \u2192 ${where}: ${msg.content}`);
|
|
36198
|
+
}
|
|
36199
|
+
}
|
|
36200
|
+
}
|
|
36201
|
+
closeDb();
|
|
36202
|
+
});
|
|
36203
|
+
program2.command("react").description("Add an emoji reaction to a message").argument("<id>", "Message ID", parseInt).argument("<emoji>", "Emoji to react with").option("--from <agent>", "Agent identity override").option("--json", "Output as JSON").action((id, emoji3, opts) => {
|
|
36204
|
+
const agent = resolveIdentity(opts.from);
|
|
36205
|
+
const reaction = addReaction(id, agent, emoji3);
|
|
36206
|
+
if (opts.json) {
|
|
36207
|
+
console.log(JSON.stringify(reaction, null, 2));
|
|
36208
|
+
} else {
|
|
36209
|
+
console.log(chalk3.green(`${emoji3} reaction added to message #${id}`));
|
|
36210
|
+
}
|
|
36211
|
+
closeDb();
|
|
36212
|
+
});
|
|
36213
|
+
program2.command("unreact").description("Remove an emoji reaction from a message").argument("<id>", "Message ID", parseInt).argument("<emoji>", "Emoji to remove").option("--from <agent>", "Agent identity override").option("--json", "Output as JSON").action((id, emoji3, opts) => {
|
|
36214
|
+
const agent = resolveIdentity(opts.from);
|
|
36215
|
+
const removed = removeReaction(id, agent, emoji3);
|
|
36216
|
+
if (opts.json) {
|
|
36217
|
+
console.log(JSON.stringify({ removed }, null, 2));
|
|
36218
|
+
} else {
|
|
36219
|
+
if (removed) {
|
|
36220
|
+
console.log(chalk3.green(`${emoji3} reaction removed from message #${id}`));
|
|
36221
|
+
} else {
|
|
36222
|
+
console.log(chalk3.dim(`No ${emoji3} reaction found on message #${id}`));
|
|
36223
|
+
}
|
|
36224
|
+
}
|
|
36225
|
+
closeDb();
|
|
36226
|
+
});
|
|
36227
|
+
program2.command("reactions").description("Show emoji reactions on a message").argument("<id>", "Message ID", parseInt).option("--json", "Output as JSON").action((id, opts) => {
|
|
36228
|
+
const summary = getReactionSummary(id);
|
|
36229
|
+
if (opts.json) {
|
|
36230
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
36231
|
+
} else {
|
|
36232
|
+
if (summary.length === 0) {
|
|
36233
|
+
console.log(chalk3.dim(`No reactions on message #${id}`));
|
|
36234
|
+
} else {
|
|
36235
|
+
const parts = summary.map((r) => `${r.emoji} ${r.count}`).join(" ");
|
|
36236
|
+
console.log(`Message #${id}: ${parts}`);
|
|
36237
|
+
}
|
|
36238
|
+
}
|
|
36239
|
+
closeDb();
|
|
36240
|
+
});
|
|
35688
36241
|
var agents = program2.command("agents").description("Manage agents");
|
|
35689
36242
|
agents.command("list").description("List all agents with their presence status").option("--online", "Only show online agents").option("--json", "Output as JSON").action((opts) => {
|
|
35690
36243
|
const agent = resolveIdentity();
|
package/bin/mcp.js
CHANGED
|
@@ -28939,6 +28939,11 @@ function getUnreadBlockers(agent) {
|
|
|
28939
28939
|
`).all(agent, agent);
|
|
28940
28940
|
return rows.map(parseMessage);
|
|
28941
28941
|
}
|
|
28942
|
+
function getThreadReplies(messageId) {
|
|
28943
|
+
const db2 = getDb();
|
|
28944
|
+
const rows = db2.prepare("SELECT * FROM messages WHERE reply_to = ? ORDER BY created_at ASC, id ASC").all(messageId);
|
|
28945
|
+
return rows.map(parseMessage);
|
|
28946
|
+
}
|
|
28942
28947
|
function searchMessages(opts) {
|
|
28943
28948
|
const db2 = getDb();
|
|
28944
28949
|
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
|
|
@@ -29863,6 +29868,12 @@ function heartbeat(agent, status, metadata, sessionId) {
|
|
|
29863
29868
|
metadata = excluded.metadata
|
|
29864
29869
|
`).run(id, normalizedAgent, sessionId ?? null, resolvedStatus, metadataJson);
|
|
29865
29870
|
}
|
|
29871
|
+
function getPresence(agent) {
|
|
29872
|
+
const db2 = getDb();
|
|
29873
|
+
const normalizedAgent = agent.trim().toLowerCase();
|
|
29874
|
+
const row = db2.prepare("SELECT * FROM agent_presence WHERE LOWER(agent) = ?").get(normalizedAgent);
|
|
29875
|
+
return row ? parsePresence(row) : null;
|
|
29876
|
+
}
|
|
29866
29877
|
function listAgents(opts) {
|
|
29867
29878
|
const db2 = getDb();
|
|
29868
29879
|
let query = "SELECT * FROM agent_presence";
|
|
@@ -29892,10 +29903,125 @@ function renameAgent(oldName, newName) {
|
|
|
29892
29903
|
db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
|
|
29893
29904
|
return true;
|
|
29894
29905
|
}
|
|
29906
|
+
|
|
29907
|
+
// src/lib/reactions.ts
|
|
29908
|
+
init_db();
|
|
29909
|
+
function addReaction(messageId, agent, emoji3) {
|
|
29910
|
+
const db2 = getDb();
|
|
29911
|
+
const stmt = db2.prepare(`
|
|
29912
|
+
INSERT INTO reactions (message_id, agent, emoji)
|
|
29913
|
+
VALUES (?, ?, ?)
|
|
29914
|
+
ON CONFLICT (message_id, agent, emoji) DO UPDATE SET agent = agent
|
|
29915
|
+
RETURNING *
|
|
29916
|
+
`);
|
|
29917
|
+
const row = stmt.get(messageId, agent, emoji3);
|
|
29918
|
+
return row;
|
|
29919
|
+
}
|
|
29920
|
+
function removeReaction(messageId, agent, emoji3) {
|
|
29921
|
+
const db2 = getDb();
|
|
29922
|
+
const stmt = db2.prepare("DELETE FROM reactions WHERE message_id = ? AND agent = ? AND emoji = ?");
|
|
29923
|
+
const result = stmt.run(messageId, agent, emoji3);
|
|
29924
|
+
return result.changes > 0;
|
|
29925
|
+
}
|
|
29926
|
+
function getReactions(messageId) {
|
|
29927
|
+
const db2 = getDb();
|
|
29928
|
+
const rows = db2.prepare("SELECT * FROM reactions WHERE message_id = ? ORDER BY created_at ASC, id ASC").all(messageId);
|
|
29929
|
+
return rows;
|
|
29930
|
+
}
|
|
29931
|
+
function getReactionSummary(messageId) {
|
|
29932
|
+
const db2 = getDb();
|
|
29933
|
+
const rows = db2.prepare(`
|
|
29934
|
+
SELECT emoji, GROUP_CONCAT(agent) as agents, COUNT(*) as count
|
|
29935
|
+
FROM reactions
|
|
29936
|
+
WHERE message_id = ?
|
|
29937
|
+
GROUP BY emoji
|
|
29938
|
+
ORDER BY count DESC, MIN(created_at) ASC
|
|
29939
|
+
`).all(messageId);
|
|
29940
|
+
return rows.map((row) => ({
|
|
29941
|
+
emoji: row.emoji,
|
|
29942
|
+
count: row.count,
|
|
29943
|
+
agents: row.agents.split(",")
|
|
29944
|
+
}));
|
|
29945
|
+
}
|
|
29946
|
+
|
|
29947
|
+
// src/lib/locks.ts
|
|
29948
|
+
init_db();
|
|
29949
|
+
var DEFAULT_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
29950
|
+
function acquireLock(resourceType, resourceId, agentId, lockType = "advisory", expiryMs = DEFAULT_LOCK_EXPIRY_MS) {
|
|
29951
|
+
const db2 = getDb();
|
|
29952
|
+
return db2.transaction(() => {
|
|
29953
|
+
cleanExpiredLocks();
|
|
29954
|
+
const existing = db2.prepare(`
|
|
29955
|
+
SELECT * FROM resource_locks
|
|
29956
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
29957
|
+
`).get(resourceType, resourceId, lockType);
|
|
29958
|
+
if (existing) {
|
|
29959
|
+
if (existing.agent_id !== agentId) {
|
|
29960
|
+
return { acquired: false, lock: null, held_by: existing.agent_id };
|
|
29961
|
+
}
|
|
29962
|
+
const expiresAt = new Date(Date.now() + expiryMs).toISOString().replace("T", "T").replace("Z", "");
|
|
29963
|
+
db2.prepare(`
|
|
29964
|
+
UPDATE resource_locks SET expires_at = ?, locked_at = strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
29965
|
+
WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
29966
|
+
`).run(expiresAt, resourceType, resourceId, lockType);
|
|
29967
|
+
} else {
|
|
29968
|
+
const expiresAt = new Date(Date.now() + expiryMs).toISOString().slice(0, -1);
|
|
29969
|
+
db2.prepare(`
|
|
29970
|
+
INSERT INTO resource_locks (resource_type, resource_id, agent_id, lock_type, locked_at, expires_at)
|
|
29971
|
+
VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'), ?)
|
|
29972
|
+
`).run(resourceType, resourceId, agentId, lockType, expiresAt);
|
|
29973
|
+
}
|
|
29974
|
+
const lock = db2.prepare(`
|
|
29975
|
+
SELECT * FROM resource_locks WHERE resource_type = ? AND resource_id = ? AND lock_type = ?
|
|
29976
|
+
`).get(resourceType, resourceId, lockType);
|
|
29977
|
+
return { acquired: true, lock };
|
|
29978
|
+
}).immediate();
|
|
29979
|
+
}
|
|
29980
|
+
function releaseLock(resourceType, resourceId, agentId) {
|
|
29981
|
+
const db2 = getDb();
|
|
29982
|
+
const result = db2.prepare(`
|
|
29983
|
+
DELETE FROM resource_locks
|
|
29984
|
+
WHERE resource_type = ? AND resource_id = ? AND agent_id = ?
|
|
29985
|
+
`).run(resourceType, resourceId, agentId);
|
|
29986
|
+
return result.changes > 0;
|
|
29987
|
+
}
|
|
29988
|
+
function checkLock(resourceType, resourceId) {
|
|
29989
|
+
const db2 = getDb();
|
|
29990
|
+
cleanExpiredLocks();
|
|
29991
|
+
return db2.prepare(`
|
|
29992
|
+
SELECT * FROM resource_locks
|
|
29993
|
+
WHERE resource_type = ? AND resource_id = ?
|
|
29994
|
+
ORDER BY locked_at ASC
|
|
29995
|
+
LIMIT 1
|
|
29996
|
+
`).get(resourceType, resourceId);
|
|
29997
|
+
}
|
|
29998
|
+
function cleanExpiredLocks() {
|
|
29999
|
+
const db2 = getDb();
|
|
30000
|
+
const result = db2.prepare(`
|
|
30001
|
+
DELETE FROM resource_locks WHERE expires_at < strftime('%Y-%m-%dT%H:%M:%f', 'now')
|
|
30002
|
+
`).run();
|
|
30003
|
+
return result.changes;
|
|
30004
|
+
}
|
|
30005
|
+
function listLocks(opts) {
|
|
30006
|
+
const db2 = getDb();
|
|
30007
|
+
cleanExpiredLocks();
|
|
30008
|
+
let query = "SELECT * FROM resource_locks WHERE 1=1";
|
|
30009
|
+
const params = [];
|
|
30010
|
+
if (opts?.resource_type) {
|
|
30011
|
+
query += " AND resource_type = ?";
|
|
30012
|
+
params.push(opts.resource_type);
|
|
30013
|
+
}
|
|
30014
|
+
if (opts?.agent_id) {
|
|
30015
|
+
query += " AND agent_id = ?";
|
|
30016
|
+
params.push(opts.agent_id);
|
|
30017
|
+
}
|
|
30018
|
+
query += " ORDER BY locked_at ASC";
|
|
30019
|
+
return db2.prepare(query).all(...params);
|
|
30020
|
+
}
|
|
29895
30021
|
// package.json
|
|
29896
30022
|
var package_default = {
|
|
29897
30023
|
name: "@hasna/conversations",
|
|
29898
|
-
version: "0.1.
|
|
30024
|
+
version: "0.1.33",
|
|
29899
30025
|
description: "Real-time CLI messaging for AI agents",
|
|
29900
30026
|
type: "module",
|
|
29901
30027
|
bin: {
|
|
@@ -29977,6 +30103,19 @@ var server = new McpServer({
|
|
|
29977
30103
|
name: "conversations",
|
|
29978
30104
|
version: package_default.version
|
|
29979
30105
|
});
|
|
30106
|
+
var agentFocus = new Map;
|
|
30107
|
+
function getAgentFocus(agentId) {
|
|
30108
|
+
if (agentFocus.has(agentId))
|
|
30109
|
+
return agentFocus.get(agentId).project_id;
|
|
30110
|
+
const presence = getPresence(agentId);
|
|
30111
|
+
return presence?.project_id ?? null;
|
|
30112
|
+
}
|
|
30113
|
+
function resolveProjectId(explicitProjectId, agentId) {
|
|
30114
|
+
if (explicitProjectId)
|
|
30115
|
+
return explicitProjectId;
|
|
30116
|
+
const focused = getAgentFocus(agentId);
|
|
30117
|
+
return focused ?? undefined;
|
|
30118
|
+
}
|
|
29980
30119
|
server.registerTool("send_message", {
|
|
29981
30120
|
description: "Send a DM to an agent.",
|
|
29982
30121
|
inputSchema: {
|
|
@@ -30015,7 +30154,11 @@ server.registerTool("read_messages", {
|
|
|
30015
30154
|
unread_only: exports_external.coerce.boolean().optional()
|
|
30016
30155
|
}
|
|
30017
30156
|
}, async (args) => {
|
|
30018
|
-
const
|
|
30157
|
+
const agent = resolveIdentity(args.from);
|
|
30158
|
+
const messages = readMessages({
|
|
30159
|
+
...args,
|
|
30160
|
+
project_id: args.project_id ?? resolveProjectId(undefined, agent)
|
|
30161
|
+
});
|
|
30019
30162
|
return {
|
|
30020
30163
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
30021
30164
|
};
|
|
@@ -30607,6 +30750,159 @@ server.registerTool("get_pinned_messages", {
|
|
|
30607
30750
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
30608
30751
|
};
|
|
30609
30752
|
});
|
|
30753
|
+
server.registerTool("add_reaction", {
|
|
30754
|
+
description: "Add an emoji reaction to a message.",
|
|
30755
|
+
inputSchema: {
|
|
30756
|
+
message_id: exports_external.coerce.number(),
|
|
30757
|
+
emoji: exports_external.string(),
|
|
30758
|
+
from: exports_external.string().optional()
|
|
30759
|
+
}
|
|
30760
|
+
}, async (args) => {
|
|
30761
|
+
const { message_id, emoji: emoji3, from: fromParam } = args;
|
|
30762
|
+
const agent = resolveIdentity(fromParam);
|
|
30763
|
+
const reaction = addReaction(message_id, agent, emoji3);
|
|
30764
|
+
return { content: [{ type: "text", text: JSON.stringify(reaction) }] };
|
|
30765
|
+
});
|
|
30766
|
+
server.registerTool("remove_reaction", {
|
|
30767
|
+
description: "Remove an emoji reaction from a message.",
|
|
30768
|
+
inputSchema: {
|
|
30769
|
+
message_id: exports_external.coerce.number(),
|
|
30770
|
+
emoji: exports_external.string(),
|
|
30771
|
+
from: exports_external.string().optional()
|
|
30772
|
+
}
|
|
30773
|
+
}, async (args) => {
|
|
30774
|
+
const { message_id, emoji: emoji3, from: fromParam } = args;
|
|
30775
|
+
const agent = resolveIdentity(fromParam);
|
|
30776
|
+
const removed = removeReaction(message_id, agent, emoji3);
|
|
30777
|
+
return { content: [{ type: "text", text: JSON.stringify({ removed }) }] };
|
|
30778
|
+
});
|
|
30779
|
+
server.registerTool("get_reactions", {
|
|
30780
|
+
description: "Get all reactions for a message.",
|
|
30781
|
+
inputSchema: {
|
|
30782
|
+
message_id: exports_external.coerce.number()
|
|
30783
|
+
}
|
|
30784
|
+
}, async (args) => {
|
|
30785
|
+
const reactions = getReactions(args.message_id);
|
|
30786
|
+
return { content: [{ type: "text", text: JSON.stringify(reactions) }] };
|
|
30787
|
+
});
|
|
30788
|
+
server.registerTool("get_reaction_summary", {
|
|
30789
|
+
description: "Get emoji reaction counts and agent lists for a message.",
|
|
30790
|
+
inputSchema: {
|
|
30791
|
+
message_id: exports_external.coerce.number()
|
|
30792
|
+
}
|
|
30793
|
+
}, async (args) => {
|
|
30794
|
+
const summary = getReactionSummary(args.message_id);
|
|
30795
|
+
return { content: [{ type: "text", text: JSON.stringify(summary) }] };
|
|
30796
|
+
});
|
|
30797
|
+
server.registerTool("acquire_lock", {
|
|
30798
|
+
description: "Acquire an advisory or exclusive lock on a resource. Returns conflict info if another agent holds the lock.",
|
|
30799
|
+
inputSchema: {
|
|
30800
|
+
resource_type: exports_external.string(),
|
|
30801
|
+
resource_id: exports_external.string(),
|
|
30802
|
+
lock_type: exports_external.enum(["advisory", "exclusive"]).optional(),
|
|
30803
|
+
expiry_ms: exports_external.coerce.number().optional(),
|
|
30804
|
+
from: exports_external.string().optional()
|
|
30805
|
+
}
|
|
30806
|
+
}, async (args) => {
|
|
30807
|
+
const { resource_type, resource_id, lock_type, expiry_ms, from: fromParam } = args;
|
|
30808
|
+
const agent = resolveIdentity(fromParam);
|
|
30809
|
+
const result = acquireLock(resource_type, resource_id, agent, lock_type ?? "advisory", expiry_ms);
|
|
30810
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
30811
|
+
});
|
|
30812
|
+
server.registerTool("release_lock", {
|
|
30813
|
+
description: "Release a lock held by the agent on a resource.",
|
|
30814
|
+
inputSchema: {
|
|
30815
|
+
resource_type: exports_external.string(),
|
|
30816
|
+
resource_id: exports_external.string(),
|
|
30817
|
+
from: exports_external.string().optional()
|
|
30818
|
+
}
|
|
30819
|
+
}, async (args) => {
|
|
30820
|
+
const { resource_type, resource_id, from: fromParam } = args;
|
|
30821
|
+
const agent = resolveIdentity(fromParam);
|
|
30822
|
+
const released = releaseLock(resource_type, resource_id, agent);
|
|
30823
|
+
return { content: [{ type: "text", text: JSON.stringify({ released }) }] };
|
|
30824
|
+
});
|
|
30825
|
+
server.registerTool("check_lock", {
|
|
30826
|
+
description: "Check if a resource is currently locked and who holds it.",
|
|
30827
|
+
inputSchema: {
|
|
30828
|
+
resource_type: exports_external.string(),
|
|
30829
|
+
resource_id: exports_external.string()
|
|
30830
|
+
}
|
|
30831
|
+
}, async (args) => {
|
|
30832
|
+
const lock = checkLock(args.resource_type, args.resource_id);
|
|
30833
|
+
return { content: [{ type: "text", text: JSON.stringify(lock ?? { locked: false }) }] };
|
|
30834
|
+
});
|
|
30835
|
+
server.registerTool("list_locks", {
|
|
30836
|
+
description: "List all active (non-expired) locks. Filter by resource_type or agent.",
|
|
30837
|
+
inputSchema: {
|
|
30838
|
+
resource_type: exports_external.string().optional(),
|
|
30839
|
+
agent_id: exports_external.string().optional()
|
|
30840
|
+
}
|
|
30841
|
+
}, async (args) => {
|
|
30842
|
+
const locks = listLocks({ resource_type: args.resource_type, agent_id: args.agent_id });
|
|
30843
|
+
return { content: [{ type: "text", text: JSON.stringify(locks) }] };
|
|
30844
|
+
});
|
|
30845
|
+
server.registerTool("get_thread_replies", {
|
|
30846
|
+
description: "Get all replies in a thread for a given parent message ID.",
|
|
30847
|
+
inputSchema: {
|
|
30848
|
+
message_id: exports_external.coerce.number()
|
|
30849
|
+
}
|
|
30850
|
+
}, async (args) => {
|
|
30851
|
+
const replies = getThreadReplies(args.message_id);
|
|
30852
|
+
return { content: [{ type: "text", text: JSON.stringify(replies) }] };
|
|
30853
|
+
});
|
|
30854
|
+
server.registerTool("set_focus", {
|
|
30855
|
+
description: "Set agent focus to a project. All read-heavy tools will default to this project scope. Stores in MCP session memory AND updates agent_presence.project_id in DB.",
|
|
30856
|
+
inputSchema: {
|
|
30857
|
+
project_id: exports_external.string(),
|
|
30858
|
+
from: exports_external.string().optional()
|
|
30859
|
+
}
|
|
30860
|
+
}, async (args) => {
|
|
30861
|
+
const { project_id, from: fromParam } = args;
|
|
30862
|
+
const agent = resolveIdentity(fromParam);
|
|
30863
|
+
agentFocus.set(agent, { project_id });
|
|
30864
|
+
const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
|
|
30865
|
+
db2.prepare("UPDATE agent_presence SET project_id = ? WHERE agent = ?").run(project_id, agent);
|
|
30866
|
+
return {
|
|
30867
|
+
content: [{ type: "text", text: JSON.stringify({ agent, focused: true, project_id }) }]
|
|
30868
|
+
};
|
|
30869
|
+
});
|
|
30870
|
+
server.registerTool("get_focus", {
|
|
30871
|
+
description: "Get the current focus state for an agent. Returns session focus, DB project_id, and effective project_id used for filtering.",
|
|
30872
|
+
inputSchema: {
|
|
30873
|
+
from: exports_external.string().optional()
|
|
30874
|
+
}
|
|
30875
|
+
}, async (args) => {
|
|
30876
|
+
const agent = resolveIdentity(args.from);
|
|
30877
|
+
const sessionFocus = agentFocus.get(agent) ?? null;
|
|
30878
|
+
const presence = getPresence(agent);
|
|
30879
|
+
const effective = getAgentFocus(agent);
|
|
30880
|
+
return {
|
|
30881
|
+
content: [{
|
|
30882
|
+
type: "text",
|
|
30883
|
+
text: JSON.stringify({
|
|
30884
|
+
agent,
|
|
30885
|
+
session_focus: sessionFocus?.project_id ?? null,
|
|
30886
|
+
db_project_id: presence?.project_id ?? null,
|
|
30887
|
+
effective_project_id: effective
|
|
30888
|
+
})
|
|
30889
|
+
}]
|
|
30890
|
+
};
|
|
30891
|
+
});
|
|
30892
|
+
server.registerTool("unfocus", {
|
|
30893
|
+
description: "Clear agent focus. Removes session focus and clears agent_presence.project_id in DB.",
|
|
30894
|
+
inputSchema: {
|
|
30895
|
+
from: exports_external.string().optional()
|
|
30896
|
+
}
|
|
30897
|
+
}, async (args) => {
|
|
30898
|
+
const agent = resolveIdentity(args.from);
|
|
30899
|
+
agentFocus.delete(agent);
|
|
30900
|
+
const db2 = (await Promise.resolve().then(() => (init_db(), exports_db))).getDb();
|
|
30901
|
+
db2.prepare("UPDATE agent_presence SET project_id = NULL WHERE agent = ?").run(agent);
|
|
30902
|
+
return {
|
|
30903
|
+
content: [{ type: "text", text: JSON.stringify({ agent, focused: false, project_id: null }) }]
|
|
30904
|
+
};
|
|
30905
|
+
});
|
|
30610
30906
|
server.registerTool("register_agent", {
|
|
30611
30907
|
description: "Register an agent with conflict detection. Returns AgentConflictError if another active session exists (active = heartbeat within last 30 min). Optional project_id locks agent to a project for the session.",
|
|
30612
30908
|
inputSchema: {
|
|
@@ -30753,6 +31049,18 @@ server.registerTool("search_tools", {
|
|
|
30753
31049
|
"pin_message",
|
|
30754
31050
|
"unpin_message",
|
|
30755
31051
|
"get_pinned_messages",
|
|
31052
|
+
"add_reaction",
|
|
31053
|
+
"remove_reaction",
|
|
31054
|
+
"get_reactions",
|
|
31055
|
+
"get_reaction_summary",
|
|
31056
|
+
"acquire_lock",
|
|
31057
|
+
"release_lock",
|
|
31058
|
+
"check_lock",
|
|
31059
|
+
"list_locks",
|
|
31060
|
+
"get_thread_replies",
|
|
31061
|
+
"set_focus",
|
|
31062
|
+
"get_focus",
|
|
31063
|
+
"unfocus",
|
|
30756
31064
|
"register_agent",
|
|
30757
31065
|
"heartbeat",
|
|
30758
31066
|
"list_agents",
|
|
@@ -30799,6 +31107,18 @@ server.registerTool("describe_tools", {
|
|
|
30799
31107
|
pin_message: "Pin a message. Required: id",
|
|
30800
31108
|
unpin_message: "Unpin a message. Required: id",
|
|
30801
31109
|
get_pinned_messages: "Get pinned messages. Optional: space?, session_id?, limit?",
|
|
31110
|
+
add_reaction: "Add emoji reaction to a message. Required: message_id, emoji. Optional: from?",
|
|
31111
|
+
remove_reaction: "Remove emoji reaction from a message. Required: message_id, emoji. Optional: from?",
|
|
31112
|
+
get_reactions: "Get all reactions for a message. Required: message_id",
|
|
31113
|
+
get_reaction_summary: "Get emoji counts + agent lists for a message. Required: message_id",
|
|
31114
|
+
acquire_lock: "Acquire advisory/exclusive lock on a resource. Required: resource_type, resource_id. Optional: lock_type?(advisory|exclusive), expiry_ms?, from?",
|
|
31115
|
+
release_lock: "Release lock held by agent. Required: resource_type, resource_id. Optional: from?",
|
|
31116
|
+
check_lock: "Check if resource is locked and who holds it. Required: resource_type, resource_id",
|
|
31117
|
+
list_locks: "List active locks. Optional: resource_type?, agent_id?",
|
|
31118
|
+
get_thread_replies: "Get all replies in a thread. Required: message_id. Optional: limit?",
|
|
31119
|
+
set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
|
|
31120
|
+
get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
|
|
31121
|
+
unfocus: "Clear agent focus (session + DB). Optional: from?",
|
|
30802
31122
|
register_agent: "Register agent with conflict detection (30min active window). Required: name, session_id. Optional: role?. Returns AgentConflictError if another session is active.",
|
|
30803
31123
|
heartbeat: "Register/refresh agent presence. Optional: from?, status?(online|busy|idle, default: online)",
|
|
30804
31124
|
list_agents: "List agents with presence timestamps. Optional: online_only?(only agents seen in last 60s)",
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,10 @@ export { listSessions, getSession, } from "./lib/sessions.js";
|
|
|
14
14
|
export { createSpace, updateSpace, archiveSpace, unarchiveSpace, listSpaces, getSpace, joinSpace, leaveSpace, getSpaceMembers, isSpaceMember, getSpaceDepth, } from "./lib/spaces.js";
|
|
15
15
|
export { createProject, listProjects, getProject, getProjectByName, updateProject, deleteProject, } from "./lib/projects.js";
|
|
16
16
|
export { getDb, getDbPath, closeDb, } from "./lib/db.js";
|
|
17
|
-
export { startPolling, useSpaceMessages, } from "./lib/poll.js";
|
|
17
|
+
export { startPolling, useMessages, useSpaceMessages, } from "./lib/poll.js";
|
|
18
18
|
export { resolveIdentity, requireIdentity, } from "./lib/identity.js";
|
|
19
19
|
export { addReaction, removeReaction, getReactions, getReactionSummary, } from "./lib/reactions.js";
|
|
20
|
+
export type { ReactionSummary } from "./lib/reactions.js";
|
|
20
21
|
export { fireWebhooks, } from "./lib/webhooks.js";
|
|
21
22
|
export { heartbeat, registerAgent, isAgentConflict, getPresence, listAgents, removePresence, renameAgent, } from "./lib/presence.js";
|
|
22
23
|
export { acquireLock, releaseLock, checkLock, cleanExpiredLocks, listLocks, } from "./lib/locks.js";
|
package/dist/index.js
CHANGED
|
@@ -2983,6 +2983,22 @@ function startPolling(opts) {
|
|
|
2983
2983
|
}
|
|
2984
2984
|
};
|
|
2985
2985
|
}
|
|
2986
|
+
function useMessages(sessionId, agent) {
|
|
2987
|
+
const [messages, setMessages] = import_react.useState([]);
|
|
2988
|
+
import_react.useEffect(() => {
|
|
2989
|
+
const existing = readMessages({ session_id: sessionId });
|
|
2990
|
+
setMessages(existing);
|
|
2991
|
+
const { stop } = startPolling({
|
|
2992
|
+
session_id: sessionId,
|
|
2993
|
+
interval_ms: 200,
|
|
2994
|
+
on_messages: (newMessages) => {
|
|
2995
|
+
setMessages((prev) => [...prev, ...newMessages]);
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
return stop;
|
|
2999
|
+
}, [sessionId, agent]);
|
|
3000
|
+
return messages;
|
|
3001
|
+
}
|
|
2986
3002
|
function useSpaceMessages(spaceName) {
|
|
2987
3003
|
const [messages, setMessages] = import_react.useState([]);
|
|
2988
3004
|
import_react.useEffect(() => {
|
|
@@ -3656,6 +3672,7 @@ function listLocks(opts) {
|
|
|
3656
3672
|
}
|
|
3657
3673
|
export {
|
|
3658
3674
|
useSpaceMessages,
|
|
3675
|
+
useMessages,
|
|
3659
3676
|
updateSpace,
|
|
3660
3677
|
updateProject,
|
|
3661
3678
|
unpinMessage,
|