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 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.6+ (uses built-in `node:sqlite`). That's all an end user needs — `npx`
21
- fetches the rest. State (identity, contacts, inbox cache) lives in `~/.cli-chat`,
22
- not next to the code, so it survives across `npx` runs.
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
 
@@ -52,9 +52,10 @@ function displayNameByKey(book, signPub) {
52
52
  }
53
53
 
54
54
  // src/db.ts
55
- import { DatabaseSync } from "node:sqlite";
55
+ import sqlite from "node-sqlite3-wasm";
56
+ var { Database } = sqlite;
56
57
  function openMailbox(path) {
57
- const db = new DatabaseSync(path);
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.prepare(
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
- ).run(
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.prepare(
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
- ).all(me);
91
+ ORDER BY created_at ASC`,
92
+ [me]
93
+ );
101
94
  }
102
95
  function getMessage(db, id) {
103
- return db.prepare(`SELECT * FROM messages WHERE id = ?`).get(id);
96
+ return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
104
97
  }
105
98
  function markRead(db, id, now) {
106
- db.prepare(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`).run(
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
@@ -89,9 +89,10 @@ function contactByKey(book, signPub) {
89
89
  }
90
90
 
91
91
  // src/db.ts
92
- import { DatabaseSync } from "node:sqlite";
92
+ import sqlite from "node-sqlite3-wasm";
93
+ var { Database } = sqlite;
93
94
  function openMailbox(path) {
94
- const db = new DatabaseSync(path);
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.prepare(
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
- ).run(
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.prepare(
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
- ).all(me);
128
+ ORDER BY created_at ASC`,
129
+ [me]
130
+ );
138
131
  }
139
132
  function getMessage(db, id) {
140
- return db.prepare(`SELECT * FROM messages WHERE id = ?`).get(id);
133
+ return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
141
134
  }
142
135
  function markRead(db, id, now2) {
143
- db.prepare(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`).run(
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.2.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.6.0"
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": {