@hasna/conversations 0.1.32 → 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 +479 -1
- package/bin/mcp.js +240 -1
- 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,6 +32613,84 @@ 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, {
|
|
@@ -32599,6 +32724,8 @@ var init_mcp2 = __esm(() => {
|
|
|
32599
32724
|
init_projects();
|
|
32600
32725
|
init_identity();
|
|
32601
32726
|
init_presence();
|
|
32727
|
+
init_reactions();
|
|
32728
|
+
init_locks();
|
|
32602
32729
|
import__package = __toESM(require_package(), 1);
|
|
32603
32730
|
server = new McpServer({
|
|
32604
32731
|
name: "conversations",
|
|
@@ -33239,6 +33366,107 @@ var init_mcp2 = __esm(() => {
|
|
|
33239
33366
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
33240
33367
|
};
|
|
33241
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
|
+
});
|
|
33242
33470
|
server.registerTool("set_focus", {
|
|
33243
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.",
|
|
33244
33472
|
inputSchema: {
|
|
@@ -33437,6 +33665,15 @@ var init_mcp2 = __esm(() => {
|
|
|
33437
33665
|
"pin_message",
|
|
33438
33666
|
"unpin_message",
|
|
33439
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",
|
|
33440
33677
|
"set_focus",
|
|
33441
33678
|
"get_focus",
|
|
33442
33679
|
"unfocus",
|
|
@@ -33486,6 +33723,15 @@ var init_mcp2 = __esm(() => {
|
|
|
33486
33723
|
pin_message: "Pin a message. Required: id",
|
|
33487
33724
|
unpin_message: "Unpin a message. Required: id",
|
|
33488
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?",
|
|
33489
33735
|
set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
|
|
33490
33736
|
get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
|
|
33491
33737
|
unfocus: "Clear agent focus (session + DB). Optional: from?",
|
|
@@ -33944,6 +34190,23 @@ function startDashboardServer(port = 0, host) {
|
|
|
33944
34190
|
const agents = listAgents({ online_only: onlineOnly });
|
|
33945
34191
|
return jsonResponse(applyFields(agents, url2.searchParams.get("fields")));
|
|
33946
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
|
+
}
|
|
33947
34210
|
if (path === "/api/version" && req.method === "GET") {
|
|
33948
34211
|
try {
|
|
33949
34212
|
const pkg2 = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
@@ -34024,6 +34287,8 @@ var init_serve = __esm(() => {
|
|
|
34024
34287
|
init_projects();
|
|
34025
34288
|
init_db();
|
|
34026
34289
|
init_presence();
|
|
34290
|
+
init_reactions();
|
|
34291
|
+
init_locks();
|
|
34027
34292
|
isDirectRun2 = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("serve.ts") || process.argv[1]?.endsWith("serve.js");
|
|
34028
34293
|
if (isDirectRun2) {
|
|
34029
34294
|
const port = normalizePort(process.env.PORT, 0);
|
|
@@ -34055,6 +34320,7 @@ init_projects();
|
|
|
34055
34320
|
init_db();
|
|
34056
34321
|
init_identity();
|
|
34057
34322
|
init_presence();
|
|
34323
|
+
init_reactions();
|
|
34058
34324
|
init_terminal_markdown();
|
|
34059
34325
|
import chalk3 from "chalk";
|
|
34060
34326
|
import { render } from "ink";
|
|
@@ -35120,6 +35386,87 @@ program2.command("search").description("Search messages by content").argument("<
|
|
|
35120
35386
|
}
|
|
35121
35387
|
closeDb();
|
|
35122
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
|
+
});
|
|
35123
35470
|
program2.command("sessions").description("List conversation sessions").option("--agent <id>", "Filter sessions involving this agent").option("--json", "Output as JSON").action((opts) => {
|
|
35124
35471
|
const sessions = listSessions(opts.agent);
|
|
35125
35472
|
if (opts.json) {
|
|
@@ -35234,6 +35581,79 @@ program2.command("status").description("Show database stats").option("--json", "
|
|
|
35234
35581
|
}
|
|
35235
35582
|
closeDb();
|
|
35236
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
|
+
});
|
|
35237
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) => {
|
|
35238
35658
|
const pkg3 = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
35239
35659
|
const current = pkg3.version;
|
|
@@ -35760,6 +36180,64 @@ program2.command("unpin").description("Unpin a message").argument("<id>", "Messa
|
|
|
35760
36180
|
}
|
|
35761
36181
|
closeDb();
|
|
35762
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
|
+
});
|
|
35763
36241
|
var agents = program2.command("agents").description("Manage agents");
|
|
35764
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) => {
|
|
35765
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;
|
|
@@ -29898,10 +29903,125 @@ function renameAgent(oldName, newName) {
|
|
|
29898
29903
|
db2.prepare("UPDATE agent_presence SET agent = ? WHERE LOWER(agent) = ?").run(normalizedNew, normalizedOld);
|
|
29899
29904
|
return true;
|
|
29900
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
|
+
}
|
|
29901
30021
|
// package.json
|
|
29902
30022
|
var package_default = {
|
|
29903
30023
|
name: "@hasna/conversations",
|
|
29904
|
-
version: "0.1.
|
|
30024
|
+
version: "0.1.33",
|
|
29905
30025
|
description: "Real-time CLI messaging for AI agents",
|
|
29906
30026
|
type: "module",
|
|
29907
30027
|
bin: {
|
|
@@ -30630,6 +30750,107 @@ server.registerTool("get_pinned_messages", {
|
|
|
30630
30750
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
30631
30751
|
};
|
|
30632
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
|
+
});
|
|
30633
30854
|
server.registerTool("set_focus", {
|
|
30634
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.",
|
|
30635
30856
|
inputSchema: {
|
|
@@ -30828,6 +31049,15 @@ server.registerTool("search_tools", {
|
|
|
30828
31049
|
"pin_message",
|
|
30829
31050
|
"unpin_message",
|
|
30830
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",
|
|
30831
31061
|
"set_focus",
|
|
30832
31062
|
"get_focus",
|
|
30833
31063
|
"unfocus",
|
|
@@ -30877,6 +31107,15 @@ server.registerTool("describe_tools", {
|
|
|
30877
31107
|
pin_message: "Pin a message. Required: id",
|
|
30878
31108
|
unpin_message: "Unpin a message. Required: id",
|
|
30879
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?",
|
|
30880
31119
|
set_focus: "Set agent focus to a project. All read tools default to this scope. Required: project_id. Optional: from?",
|
|
30881
31120
|
get_focus: "Get current focus: session focus, DB project_id, effective project_id. Optional: from?",
|
|
30882
31121
|
unfocus: "Clear agent focus (session + DB). Optional: from?",
|
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,
|