cli-chat-mcp 0.2.0 → 0.3.0
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 +4 -3
- package/dist/check-inbox.js +13 -20
- package/dist/server-net.js +136 -21
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -17,9 +17,10 @@ agent surfaces the message and helps them reply.
|
|
|
17
17
|
See [PLAN.md](./PLAN.md) for the full concept and roadmap.
|
|
18
18
|
|
|
19
19
|
## Requirements
|
|
20
|
-
Node 22
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
Node 22+ (uses the native global `WebSocket` for push; the inbox cache uses
|
|
21
|
+
`node-sqlite3-wasm` — WebAssembly SQLite, no native build). That's all an end user
|
|
22
|
+
needs — `npx` fetches the rest. State (identity, contacts, inbox cache) lives in
|
|
23
|
+
`~/.cli-chat`, not next to the code, so it survives across `npx` runs.
|
|
23
24
|
|
|
24
25
|
## Set up to message someone (no clone)
|
|
25
26
|
|
package/dist/check-inbox.js
CHANGED
|
@@ -52,9 +52,10 @@ function displayNameByKey(book, signPub) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// src/db.ts
|
|
55
|
-
import
|
|
55
|
+
import sqlite from "node-sqlite3-wasm";
|
|
56
|
+
var { Database } = sqlite;
|
|
56
57
|
function openMailbox(path) {
|
|
57
|
-
const db = new
|
|
58
|
+
const db = new Database(path);
|
|
58
59
|
try {
|
|
59
60
|
db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000;`);
|
|
60
61
|
} catch {
|
|
@@ -76,38 +77,30 @@ function openMailbox(path) {
|
|
|
76
77
|
return db;
|
|
77
78
|
}
|
|
78
79
|
function insertMessage(db, m) {
|
|
79
|
-
db.
|
|
80
|
+
db.run(
|
|
80
81
|
`INSERT INTO messages
|
|
81
82
|
(id, recipient, sender, body, tags, created_at, fetched_at, read_at, in_reply_to)
|
|
82
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
83
|
-
|
|
84
|
-
m.id,
|
|
85
|
-
m.recipient,
|
|
86
|
-
m.sender,
|
|
87
|
-
m.body,
|
|
88
|
-
m.tags,
|
|
89
|
-
m.created_at,
|
|
90
|
-
m.fetched_at,
|
|
91
|
-
m.read_at,
|
|
92
|
-
m.in_reply_to
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
84
|
+
[m.id, m.recipient, m.sender, m.body, m.tags, m.created_at, m.fetched_at, m.read_at, m.in_reply_to]
|
|
93
85
|
);
|
|
94
86
|
}
|
|
95
87
|
function unreadFor(db, me) {
|
|
96
|
-
return db.
|
|
88
|
+
return db.all(
|
|
97
89
|
`SELECT * FROM messages
|
|
98
90
|
WHERE recipient = ? AND read_at IS NULL
|
|
99
|
-
ORDER BY created_at ASC
|
|
100
|
-
|
|
91
|
+
ORDER BY created_at ASC`,
|
|
92
|
+
[me]
|
|
93
|
+
);
|
|
101
94
|
}
|
|
102
95
|
function getMessage(db, id) {
|
|
103
|
-
return db.
|
|
96
|
+
return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
|
|
104
97
|
}
|
|
105
98
|
function markRead(db, id, now) {
|
|
106
|
-
db.
|
|
99
|
+
db.run(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`, [
|
|
107
100
|
now,
|
|
108
101
|
now,
|
|
109
102
|
id
|
|
110
|
-
);
|
|
103
|
+
]);
|
|
111
104
|
}
|
|
112
105
|
|
|
113
106
|
// src/canonical.ts
|
package/dist/server-net.js
CHANGED
|
@@ -89,9 +89,10 @@ function contactByKey(book, signPub) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// src/db.ts
|
|
92
|
-
import
|
|
92
|
+
import sqlite from "node-sqlite3-wasm";
|
|
93
|
+
var { Database } = sqlite;
|
|
93
94
|
function openMailbox(path) {
|
|
94
|
-
const db = new
|
|
95
|
+
const db = new Database(path);
|
|
95
96
|
try {
|
|
96
97
|
db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000;`);
|
|
97
98
|
} catch {
|
|
@@ -113,38 +114,30 @@ function openMailbox(path) {
|
|
|
113
114
|
return db;
|
|
114
115
|
}
|
|
115
116
|
function insertMessage(db, m) {
|
|
116
|
-
db.
|
|
117
|
+
db.run(
|
|
117
118
|
`INSERT INTO messages
|
|
118
119
|
(id, recipient, sender, body, tags, created_at, fetched_at, read_at, in_reply_to)
|
|
119
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
120
|
-
|
|
121
|
-
m.id,
|
|
122
|
-
m.recipient,
|
|
123
|
-
m.sender,
|
|
124
|
-
m.body,
|
|
125
|
-
m.tags,
|
|
126
|
-
m.created_at,
|
|
127
|
-
m.fetched_at,
|
|
128
|
-
m.read_at,
|
|
129
|
-
m.in_reply_to
|
|
120
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
121
|
+
[m.id, m.recipient, m.sender, m.body, m.tags, m.created_at, m.fetched_at, m.read_at, m.in_reply_to]
|
|
130
122
|
);
|
|
131
123
|
}
|
|
132
124
|
function unreadFor(db, me) {
|
|
133
|
-
return db.
|
|
125
|
+
return db.all(
|
|
134
126
|
`SELECT * FROM messages
|
|
135
127
|
WHERE recipient = ? AND read_at IS NULL
|
|
136
|
-
ORDER BY created_at ASC
|
|
137
|
-
|
|
128
|
+
ORDER BY created_at ASC`,
|
|
129
|
+
[me]
|
|
130
|
+
);
|
|
138
131
|
}
|
|
139
132
|
function getMessage(db, id) {
|
|
140
|
-
return db.
|
|
133
|
+
return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
|
|
141
134
|
}
|
|
142
135
|
function markRead(db, id, now2) {
|
|
143
|
-
db.
|
|
136
|
+
db.run(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`, [
|
|
144
137
|
now2,
|
|
145
138
|
now2,
|
|
146
139
|
id
|
|
147
|
-
);
|
|
140
|
+
]);
|
|
148
141
|
}
|
|
149
142
|
|
|
150
143
|
// src/canonical.ts
|
|
@@ -375,6 +368,9 @@ function resolveMailboxUrl() {
|
|
|
375
368
|
return process.env.MESSENGER_MAILBOX_URL?.trim() || DEFAULT_MAILBOX_URL;
|
|
376
369
|
}
|
|
377
370
|
|
|
371
|
+
// src/warmer.ts
|
|
372
|
+
import { execFile } from "node:child_process";
|
|
373
|
+
|
|
378
374
|
// src/core-net.ts
|
|
379
375
|
import { randomUUID } from "node:crypto";
|
|
380
376
|
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -510,6 +506,113 @@ async function draftReply(ctx, args) {
|
|
|
510
506
|
return { ok: true, id, to: { name: c.name, signPub: c.signPub } };
|
|
511
507
|
}
|
|
512
508
|
|
|
509
|
+
// src/warmer.ts
|
|
510
|
+
var PING_MS = 3e4;
|
|
511
|
+
var POLL_MS = 6e4;
|
|
512
|
+
var BACKOFF_START_MS = 1e3;
|
|
513
|
+
var BACKOFF_MAX_MS = 3e4;
|
|
514
|
+
function startWarmer(ctx, opts) {
|
|
515
|
+
const wsUrl = opts.mailboxUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/connect";
|
|
516
|
+
let ws = null;
|
|
517
|
+
let stopped = false;
|
|
518
|
+
let backoff = BACKOFF_START_MS;
|
|
519
|
+
let pingTimer = null;
|
|
520
|
+
let reconnectTimer = null;
|
|
521
|
+
let draining = false;
|
|
522
|
+
async function drain() {
|
|
523
|
+
if (draining || stopped) return;
|
|
524
|
+
draining = true;
|
|
525
|
+
try {
|
|
526
|
+
const added = await sync(ctx);
|
|
527
|
+
if (added > 0) notifyNewMail(ctx);
|
|
528
|
+
} catch {
|
|
529
|
+
} finally {
|
|
530
|
+
draining = false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function clearPing() {
|
|
534
|
+
if (pingTimer) {
|
|
535
|
+
clearInterval(pingTimer);
|
|
536
|
+
pingTimer = null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function connect() {
|
|
540
|
+
if (stopped) return;
|
|
541
|
+
const h = makeAuthHeaders(ctx.me.signPub, ctx.me.signSec, "GET", "/connect", "", opts.now());
|
|
542
|
+
const u = new URL(wsUrl);
|
|
543
|
+
u.searchParams.set("x-pubkey", h["x-pubkey"]);
|
|
544
|
+
u.searchParams.set("x-timestamp", h["x-timestamp"]);
|
|
545
|
+
u.searchParams.set("x-signature", h["x-signature"]);
|
|
546
|
+
ws = new WebSocket(u.toString());
|
|
547
|
+
ws.addEventListener("open", () => {
|
|
548
|
+
backoff = BACKOFF_START_MS;
|
|
549
|
+
void drain();
|
|
550
|
+
clearPing();
|
|
551
|
+
pingTimer = setInterval(() => {
|
|
552
|
+
try {
|
|
553
|
+
ws?.send("ping");
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
}, PING_MS);
|
|
557
|
+
});
|
|
558
|
+
ws.addEventListener("message", (ev) => {
|
|
559
|
+
if (String(ev.data) === "pong") return;
|
|
560
|
+
void drain();
|
|
561
|
+
});
|
|
562
|
+
ws.addEventListener("close", scheduleReconnect);
|
|
563
|
+
ws.addEventListener("error", () => {
|
|
564
|
+
try {
|
|
565
|
+
ws?.close();
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function scheduleReconnect() {
|
|
571
|
+
clearPing();
|
|
572
|
+
if (stopped) return;
|
|
573
|
+
const jitter = Math.floor(backoff * 0.3 * Math.random());
|
|
574
|
+
reconnectTimer = setTimeout(connect, backoff + jitter);
|
|
575
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
|
|
576
|
+
}
|
|
577
|
+
connect();
|
|
578
|
+
const pollTimer = setInterval(() => void drain(), POLL_MS);
|
|
579
|
+
return function stop() {
|
|
580
|
+
stopped = true;
|
|
581
|
+
clearInterval(pollTimer);
|
|
582
|
+
clearPing();
|
|
583
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
584
|
+
try {
|
|
585
|
+
ws?.close();
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function notifyNewMail(ctx) {
|
|
591
|
+
if (process.env.MESSENGER_NOTIFY !== "1") return;
|
|
592
|
+
const unread = unreadFor(ctx.cache, ctx.me.signPub);
|
|
593
|
+
const latest = unread[unread.length - 1];
|
|
594
|
+
if (!latest) return;
|
|
595
|
+
const from = displayNameByKey(ctx.book, latest.sender);
|
|
596
|
+
const title = "New message";
|
|
597
|
+
const body = `${from}: ${latest.body.slice(0, 80)}`;
|
|
598
|
+
try {
|
|
599
|
+
process.stdout.write("\x07");
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
if (process.platform === "darwin") {
|
|
603
|
+
const safe = (s2) => s2.replace(/["\\]/g, " ");
|
|
604
|
+
execFile(
|
|
605
|
+
"osascript",
|
|
606
|
+
["-e", `display notification "${safe(body)}" with title "${safe(title)}"`],
|
|
607
|
+
() => {
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
} else if (process.platform === "linux") {
|
|
611
|
+
execFile("notify-send", [title, body], () => {
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
513
616
|
// src/server-net.ts
|
|
514
617
|
var mailboxUrl = resolveMailboxUrl();
|
|
515
618
|
var now = () => Date.now();
|
|
@@ -540,6 +643,16 @@ if (existing) {
|
|
|
540
643
|
console.error(`Found user "${existing}" but couldn't load identity: ${e.message}`);
|
|
541
644
|
}
|
|
542
645
|
}
|
|
646
|
+
var stopWarmer = null;
|
|
647
|
+
function ensureWarmer() {
|
|
648
|
+
if (process.env.MESSENGER_PUSH === "0") return;
|
|
649
|
+
if (stopWarmer) {
|
|
650
|
+
stopWarmer();
|
|
651
|
+
stopWarmer = null;
|
|
652
|
+
}
|
|
653
|
+
if (S) stopWarmer = startWarmer(S.ctx, { mailboxUrl, now });
|
|
654
|
+
}
|
|
655
|
+
ensureWarmer();
|
|
543
656
|
async function claimHandle(client) {
|
|
544
657
|
for (let i = 0; i < 8; i++) {
|
|
545
658
|
const candidate = randomHandle(randomBytes(8));
|
|
@@ -666,6 +779,7 @@ server.registerTool(
|
|
|
666
779
|
);
|
|
667
780
|
setCurrentUser(id.handle);
|
|
668
781
|
S = buildSession(id.handle);
|
|
782
|
+
ensureWarmer();
|
|
669
783
|
return ok({
|
|
670
784
|
ok: true,
|
|
671
785
|
created: true,
|
|
@@ -752,9 +866,10 @@ server.registerTool(
|
|
|
752
866
|
const deadline = now() + WATCH_MS;
|
|
753
867
|
const progressToken = extra?._meta?.progressToken;
|
|
754
868
|
let ticks = 0;
|
|
869
|
+
await sync(S.ctx);
|
|
870
|
+
let sinceSync = 0;
|
|
755
871
|
for (; ; ) {
|
|
756
872
|
if (extra?.signal?.aborted) return ok({ status: "idle", count: 0, note: "Watch cancelled." });
|
|
757
|
-
await sync(S.ctx);
|
|
758
873
|
const rows = unreadFor(S.cache, S.me.signPub);
|
|
759
874
|
if (rows.length > 0) {
|
|
760
875
|
const messages = rows.map((m) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-chat-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Headless cross-CLI messenger for coding agents (MCP)",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"engines": {
|
|
14
|
-
"node": ">=22
|
|
14
|
+
"node": ">=22"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "node build.mjs",
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"add-contact": "node src/add-contact.ts",
|
|
29
29
|
"mailbox": "node server-mailbox/node.ts",
|
|
30
30
|
"server": "node src/server-net.ts",
|
|
31
|
-
"deploy": "wrangler deploy"
|
|
31
|
+
"deploy": "npx wrangler deploy"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@hono/node-server": "^2.0.5",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
36
|
"hono": "^4.12.25",
|
|
37
37
|
"libsodium-wrappers": "^0.8.4",
|
|
38
|
+
"node-sqlite3-wasm": "^0.8.58",
|
|
38
39
|
"zod": "^3.23.8"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|