cli-chat-mcp 0.2.0 → 0.3.1

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,12 @@ 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 picks its
21
+ SQLite driver automatically: native `node:sqlite` on Node 24+ (one shared WAL
22
+ handle), falling back to `node-sqlite3-wasm` WebAssembly SQLite, no native build
23
+ — on Node 22/23. That's all an end user needs — `npx` fetches the rest. State
24
+ (identity, contacts, inbox cache) lives in `~/.cli-chat`, not next to the code, so
25
+ it survives across `npx` runs.
23
26
 
24
27
  ## Set up to message someone (no clone)
25
28
 
@@ -52,62 +52,163 @@ function displayNameByKey(book, signPub) {
52
52
  }
53
53
 
54
54
  // src/db.ts
55
- import { DatabaseSync } from "node:sqlite";
56
- function openMailbox(path) {
57
- const db = new DatabaseSync(path);
55
+ import { createRequire } from "node:module";
56
+ import sqlite from "node-sqlite3-wasm";
57
+ var { Database: WasmDatabase } = sqlite;
58
+ var nodeMajor = Number(process.versions.node.split(".")[0]);
59
+ var forcedDriver = process.env.MESSENGER_DB_DRIVER?.trim().toLowerCase();
60
+ function loadNative() {
58
61
  try {
59
- db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000;`);
62
+ return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
60
63
  } catch {
64
+ return void 0;
61
65
  }
62
- db.exec(`
63
- CREATE TABLE IF NOT EXISTS messages (
64
- id TEXT PRIMARY KEY,
65
- recipient TEXT NOT NULL,
66
- sender TEXT NOT NULL,
67
- body TEXT NOT NULL,
68
- tags TEXT,
69
- created_at INTEGER NOT NULL,
70
- fetched_at INTEGER,
71
- read_at INTEGER,
72
- in_reply_to TEXT
73
- );
74
- CREATE INDEX IF NOT EXISTS idx_recipient ON messages (recipient, created_at);
75
- `);
66
+ }
67
+ var NativeDatabase = nodeMajor >= 24 || forcedDriver === "native" ? loadNative() : void 0;
68
+ function chooseDriver() {
69
+ if (forcedDriver === "wasm") return "wasm";
70
+ if (forcedDriver === "native") return NativeDatabase ? "native" : "wasm";
71
+ return NativeDatabase ? "native" : "wasm";
72
+ }
73
+ var SCHEMA = `
74
+ CREATE TABLE IF NOT EXISTS messages (
75
+ id TEXT PRIMARY KEY,
76
+ recipient TEXT NOT NULL,
77
+ sender TEXT NOT NULL,
78
+ body TEXT NOT NULL,
79
+ tags TEXT,
80
+ created_at INTEGER NOT NULL,
81
+ fetched_at INTEGER,
82
+ read_at INTEGER,
83
+ in_reply_to TEXT
84
+ );
85
+ CREATE INDEX IF NOT EXISTS idx_recipient ON messages (recipient, created_at);
86
+ `;
87
+ var NativeStore = class {
88
+ #db;
89
+ constructor(path) {
90
+ const Db = NativeDatabase;
91
+ this.#db = new Db(path);
92
+ try {
93
+ this.#db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
94
+ } catch {
95
+ }
96
+ this.#db.exec(SCHEMA);
97
+ }
98
+ run(sql, params = []) {
99
+ this.#db.prepare(sql).run(...params);
100
+ }
101
+ all(sql, params = []) {
102
+ return this.#db.prepare(sql).all(...params);
103
+ }
104
+ get(sql, params = []) {
105
+ return this.#db.prepare(sql).get(...params) ?? void 0;
106
+ }
107
+ close() {
108
+ this.#db.close();
109
+ }
110
+ };
111
+ function sleepSync(ms) {
112
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
113
+ }
114
+ function withRetry(fn, attempts = 6) {
115
+ let delay = 15;
116
+ for (let i = 0; ; i++) {
117
+ try {
118
+ return fn();
119
+ } catch (e) {
120
+ const msg = String(e?.message ?? e);
121
+ const retriable = /unable to open|database is locked|database table is locked|is busy/i.test(msg);
122
+ if (!retriable || i >= attempts) throw e;
123
+ sleepSync(delay);
124
+ delay = Math.min(delay * 2, 200);
125
+ }
126
+ }
127
+ }
128
+ function openWasm(path) {
129
+ const db = new WasmDatabase(path);
130
+ try {
131
+ db.exec("PRAGMA busy_timeout = 4000;");
132
+ } catch {
133
+ }
134
+ db.exec(SCHEMA);
76
135
  return db;
77
136
  }
137
+ var WasmMemoryStore = class {
138
+ #db;
139
+ constructor(path) {
140
+ this.#db = openWasm(path);
141
+ }
142
+ run(sql, params = []) {
143
+ this.#db.run(sql, params);
144
+ }
145
+ all(sql, params = []) {
146
+ return this.#db.all(sql, params);
147
+ }
148
+ get(sql, params = []) {
149
+ return this.#db.get(sql, params) ?? void 0;
150
+ }
151
+ close() {
152
+ this.#db.close();
153
+ }
154
+ };
155
+ var WasmPerOpStore = class {
156
+ #path;
157
+ constructor(path) {
158
+ this.#path = path;
159
+ }
160
+ #with(fn) {
161
+ return withRetry(() => {
162
+ const db = openWasm(this.#path);
163
+ try {
164
+ return fn(db);
165
+ } finally {
166
+ db.close();
167
+ }
168
+ });
169
+ }
170
+ run(sql, params = []) {
171
+ this.#with((db) => db.run(sql, params));
172
+ }
173
+ all(sql, params = []) {
174
+ return this.#with((db) => db.all(sql, params));
175
+ }
176
+ get(sql, params = []) {
177
+ return this.#with((db) => db.get(sql, params)) ?? void 0;
178
+ }
179
+ close() {
180
+ }
181
+ };
182
+ function openMailbox(path) {
183
+ if (chooseDriver() === "native") return new NativeStore(path);
184
+ if (path === ":memory:") return new WasmMemoryStore(path);
185
+ return new WasmPerOpStore(path);
186
+ }
78
187
  function insertMessage(db, m) {
79
- db.prepare(
188
+ db.run(
80
189
  `INSERT INTO messages
81
190
  (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
191
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
192
+ [m.id, m.recipient, m.sender, m.body, m.tags, m.created_at, m.fetched_at, m.read_at, m.in_reply_to]
93
193
  );
94
194
  }
95
195
  function unreadFor(db, me) {
96
- return db.prepare(
196
+ return db.all(
97
197
  `SELECT * FROM messages
98
198
  WHERE recipient = ? AND read_at IS NULL
99
- ORDER BY created_at ASC`
100
- ).all(me);
199
+ ORDER BY created_at ASC`,
200
+ [me]
201
+ );
101
202
  }
102
203
  function getMessage(db, id) {
103
- return db.prepare(`SELECT * FROM messages WHERE id = ?`).get(id);
204
+ return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
104
205
  }
105
206
  function markRead(db, id, now) {
106
- db.prepare(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`).run(
207
+ db.run(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`, [
107
208
  now,
108
209
  now,
109
210
  id
110
- );
211
+ ]);
111
212
  }
112
213
 
113
214
  // src/canonical.ts
@@ -89,62 +89,163 @@ function contactByKey(book, signPub) {
89
89
  }
90
90
 
91
91
  // src/db.ts
92
- import { DatabaseSync } from "node:sqlite";
93
- function openMailbox(path) {
94
- const db = new DatabaseSync(path);
92
+ import { createRequire } from "node:module";
93
+ import sqlite from "node-sqlite3-wasm";
94
+ var { Database: WasmDatabase } = sqlite;
95
+ var nodeMajor = Number(process.versions.node.split(".")[0]);
96
+ var forcedDriver = process.env.MESSENGER_DB_DRIVER?.trim().toLowerCase();
97
+ function loadNative() {
95
98
  try {
96
- db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000;`);
99
+ return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
97
100
  } catch {
101
+ return void 0;
98
102
  }
99
- db.exec(`
100
- CREATE TABLE IF NOT EXISTS messages (
101
- id TEXT PRIMARY KEY,
102
- recipient TEXT NOT NULL,
103
- sender TEXT NOT NULL,
104
- body TEXT NOT NULL,
105
- tags TEXT,
106
- created_at INTEGER NOT NULL,
107
- fetched_at INTEGER,
108
- read_at INTEGER,
109
- in_reply_to TEXT
110
- );
111
- CREATE INDEX IF NOT EXISTS idx_recipient ON messages (recipient, created_at);
112
- `);
103
+ }
104
+ var NativeDatabase = nodeMajor >= 24 || forcedDriver === "native" ? loadNative() : void 0;
105
+ function chooseDriver() {
106
+ if (forcedDriver === "wasm") return "wasm";
107
+ if (forcedDriver === "native") return NativeDatabase ? "native" : "wasm";
108
+ return NativeDatabase ? "native" : "wasm";
109
+ }
110
+ var SCHEMA = `
111
+ CREATE TABLE IF NOT EXISTS messages (
112
+ id TEXT PRIMARY KEY,
113
+ recipient TEXT NOT NULL,
114
+ sender TEXT NOT NULL,
115
+ body TEXT NOT NULL,
116
+ tags TEXT,
117
+ created_at INTEGER NOT NULL,
118
+ fetched_at INTEGER,
119
+ read_at INTEGER,
120
+ in_reply_to TEXT
121
+ );
122
+ CREATE INDEX IF NOT EXISTS idx_recipient ON messages (recipient, created_at);
123
+ `;
124
+ var NativeStore = class {
125
+ #db;
126
+ constructor(path) {
127
+ const Db = NativeDatabase;
128
+ this.#db = new Db(path);
129
+ try {
130
+ this.#db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
131
+ } catch {
132
+ }
133
+ this.#db.exec(SCHEMA);
134
+ }
135
+ run(sql, params = []) {
136
+ this.#db.prepare(sql).run(...params);
137
+ }
138
+ all(sql, params = []) {
139
+ return this.#db.prepare(sql).all(...params);
140
+ }
141
+ get(sql, params = []) {
142
+ return this.#db.prepare(sql).get(...params) ?? void 0;
143
+ }
144
+ close() {
145
+ this.#db.close();
146
+ }
147
+ };
148
+ function sleepSync(ms) {
149
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
150
+ }
151
+ function withRetry(fn, attempts = 6) {
152
+ let delay = 15;
153
+ for (let i = 0; ; i++) {
154
+ try {
155
+ return fn();
156
+ } catch (e) {
157
+ const msg = String(e?.message ?? e);
158
+ const retriable = /unable to open|database is locked|database table is locked|is busy/i.test(msg);
159
+ if (!retriable || i >= attempts) throw e;
160
+ sleepSync(delay);
161
+ delay = Math.min(delay * 2, 200);
162
+ }
163
+ }
164
+ }
165
+ function openWasm(path) {
166
+ const db = new WasmDatabase(path);
167
+ try {
168
+ db.exec("PRAGMA busy_timeout = 4000;");
169
+ } catch {
170
+ }
171
+ db.exec(SCHEMA);
113
172
  return db;
114
173
  }
174
+ var WasmMemoryStore = class {
175
+ #db;
176
+ constructor(path) {
177
+ this.#db = openWasm(path);
178
+ }
179
+ run(sql, params = []) {
180
+ this.#db.run(sql, params);
181
+ }
182
+ all(sql, params = []) {
183
+ return this.#db.all(sql, params);
184
+ }
185
+ get(sql, params = []) {
186
+ return this.#db.get(sql, params) ?? void 0;
187
+ }
188
+ close() {
189
+ this.#db.close();
190
+ }
191
+ };
192
+ var WasmPerOpStore = class {
193
+ #path;
194
+ constructor(path) {
195
+ this.#path = path;
196
+ }
197
+ #with(fn) {
198
+ return withRetry(() => {
199
+ const db = openWasm(this.#path);
200
+ try {
201
+ return fn(db);
202
+ } finally {
203
+ db.close();
204
+ }
205
+ });
206
+ }
207
+ run(sql, params = []) {
208
+ this.#with((db) => db.run(sql, params));
209
+ }
210
+ all(sql, params = []) {
211
+ return this.#with((db) => db.all(sql, params));
212
+ }
213
+ get(sql, params = []) {
214
+ return this.#with((db) => db.get(sql, params)) ?? void 0;
215
+ }
216
+ close() {
217
+ }
218
+ };
219
+ function openMailbox(path) {
220
+ if (chooseDriver() === "native") return new NativeStore(path);
221
+ if (path === ":memory:") return new WasmMemoryStore(path);
222
+ return new WasmPerOpStore(path);
223
+ }
115
224
  function insertMessage(db, m) {
116
- db.prepare(
225
+ db.run(
117
226
  `INSERT INTO messages
118
227
  (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
228
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
229
+ [m.id, m.recipient, m.sender, m.body, m.tags, m.created_at, m.fetched_at, m.read_at, m.in_reply_to]
130
230
  );
131
231
  }
132
232
  function unreadFor(db, me) {
133
- return db.prepare(
233
+ return db.all(
134
234
  `SELECT * FROM messages
135
235
  WHERE recipient = ? AND read_at IS NULL
136
- ORDER BY created_at ASC`
137
- ).all(me);
236
+ ORDER BY created_at ASC`,
237
+ [me]
238
+ );
138
239
  }
139
240
  function getMessage(db, id) {
140
- return db.prepare(`SELECT * FROM messages WHERE id = ?`).get(id);
241
+ return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
141
242
  }
142
243
  function markRead(db, id, now2) {
143
- db.prepare(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`).run(
244
+ db.run(`UPDATE messages SET read_at = ?, fetched_at = COALESCE(fetched_at, ?) WHERE id = ?`, [
144
245
  now2,
145
246
  now2,
146
247
  id
147
- );
248
+ ]);
148
249
  }
149
250
 
150
251
  // src/canonical.ts
@@ -353,6 +454,9 @@ function resolveDir(sel) {
353
454
  }
354
455
  return null;
355
456
  }
457
+ function resolveIdentity(sel) {
458
+ return resolveDir(sel);
459
+ }
356
460
  function currentUser() {
357
461
  const env = process.env.MESSENGER_USER?.trim();
358
462
  if (env) return resolveDir(env) ?? env;
@@ -375,6 +479,9 @@ function resolveMailboxUrl() {
375
479
  return process.env.MESSENGER_MAILBOX_URL?.trim() || DEFAULT_MAILBOX_URL;
376
480
  }
377
481
 
482
+ // src/warmer.ts
483
+ import { execFile } from "node:child_process";
484
+
378
485
  // src/core-net.ts
379
486
  import { randomUUID } from "node:crypto";
380
487
  import { writeFileSync as writeFileSync2 } from "node:fs";
@@ -510,6 +617,113 @@ async function draftReply(ctx, args) {
510
617
  return { ok: true, id, to: { name: c.name, signPub: c.signPub } };
511
618
  }
512
619
 
620
+ // src/warmer.ts
621
+ var PING_MS = 3e4;
622
+ var POLL_MS = 6e4;
623
+ var BACKOFF_START_MS = 1e3;
624
+ var BACKOFF_MAX_MS = 3e4;
625
+ function startWarmer(ctx, opts) {
626
+ const wsUrl = opts.mailboxUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/connect";
627
+ let ws = null;
628
+ let stopped = false;
629
+ let backoff = BACKOFF_START_MS;
630
+ let pingTimer = null;
631
+ let reconnectTimer = null;
632
+ let draining = false;
633
+ async function drain() {
634
+ if (draining || stopped) return;
635
+ draining = true;
636
+ try {
637
+ const added = await sync(ctx);
638
+ if (added > 0) notifyNewMail(ctx);
639
+ } catch {
640
+ } finally {
641
+ draining = false;
642
+ }
643
+ }
644
+ function clearPing() {
645
+ if (pingTimer) {
646
+ clearInterval(pingTimer);
647
+ pingTimer = null;
648
+ }
649
+ }
650
+ function connect() {
651
+ if (stopped) return;
652
+ const h = makeAuthHeaders(ctx.me.signPub, ctx.me.signSec, "GET", "/connect", "", opts.now());
653
+ const u = new URL(wsUrl);
654
+ u.searchParams.set("x-pubkey", h["x-pubkey"]);
655
+ u.searchParams.set("x-timestamp", h["x-timestamp"]);
656
+ u.searchParams.set("x-signature", h["x-signature"]);
657
+ ws = new WebSocket(u.toString());
658
+ ws.addEventListener("open", () => {
659
+ backoff = BACKOFF_START_MS;
660
+ void drain();
661
+ clearPing();
662
+ pingTimer = setInterval(() => {
663
+ try {
664
+ ws?.send("ping");
665
+ } catch {
666
+ }
667
+ }, PING_MS);
668
+ });
669
+ ws.addEventListener("message", (ev) => {
670
+ if (String(ev.data) === "pong") return;
671
+ void drain();
672
+ });
673
+ ws.addEventListener("close", scheduleReconnect);
674
+ ws.addEventListener("error", () => {
675
+ try {
676
+ ws?.close();
677
+ } catch {
678
+ }
679
+ });
680
+ }
681
+ function scheduleReconnect() {
682
+ clearPing();
683
+ if (stopped) return;
684
+ const jitter = Math.floor(backoff * 0.3 * Math.random());
685
+ reconnectTimer = setTimeout(connect, backoff + jitter);
686
+ backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
687
+ }
688
+ connect();
689
+ const pollTimer = setInterval(() => void drain(), POLL_MS);
690
+ return function stop() {
691
+ stopped = true;
692
+ clearInterval(pollTimer);
693
+ clearPing();
694
+ if (reconnectTimer) clearTimeout(reconnectTimer);
695
+ try {
696
+ ws?.close();
697
+ } catch {
698
+ }
699
+ };
700
+ }
701
+ function notifyNewMail(ctx) {
702
+ if (process.env.MESSENGER_NOTIFY !== "1") return;
703
+ const unread = unreadFor(ctx.cache, ctx.me.signPub);
704
+ const latest = unread[unread.length - 1];
705
+ if (!latest) return;
706
+ const from = displayNameByKey(ctx.book, latest.sender);
707
+ const title = "New message";
708
+ const body = `${from}: ${latest.body.slice(0, 80)}`;
709
+ try {
710
+ process.stdout.write("\x07");
711
+ } catch {
712
+ }
713
+ if (process.platform === "darwin") {
714
+ const safe = (s2) => s2.replace(/["\\]/g, " ");
715
+ execFile(
716
+ "osascript",
717
+ ["-e", `display notification "${safe(body)}" with title "${safe(title)}"`],
718
+ () => {
719
+ }
720
+ );
721
+ } else if (process.platform === "linux") {
722
+ execFile("notify-send", [title, body], () => {
723
+ });
724
+ }
725
+ }
726
+
513
727
  // src/server-net.ts
514
728
  var mailboxUrl = resolveMailboxUrl();
515
729
  var now = () => Date.now();
@@ -520,7 +734,15 @@ function buildSession(user) {
520
734
  const contactsPath = contactsFile(user);
521
735
  const me = loadIdentity(identityFile(user));
522
736
  const book = loadContacts(contactsPath);
523
- const cache = openMailbox(inboxFile(user));
737
+ let cache;
738
+ try {
739
+ cache = openMailbox(inboxFile(user));
740
+ } catch (e) {
741
+ console.error(
742
+ `Inbox cache for "${user}" is unavailable (${e.message}); using a temporary in-memory cache for this session.`
743
+ );
744
+ cache = openMailbox(":memory:");
745
+ }
524
746
  const ctx = {
525
747
  me,
526
748
  book,
@@ -540,6 +762,16 @@ if (existing) {
540
762
  console.error(`Found user "${existing}" but couldn't load identity: ${e.message}`);
541
763
  }
542
764
  }
765
+ var stopWarmer = null;
766
+ function ensureWarmer() {
767
+ if (process.env.MESSENGER_PUSH === "0") return;
768
+ if (stopWarmer) {
769
+ stopWarmer();
770
+ stopWarmer = null;
771
+ }
772
+ if (S) stopWarmer = startWarmer(S.ctx, { mailboxUrl, now });
773
+ }
774
+ ensureWarmer();
543
775
  async function claimHandle(client) {
544
776
  for (let i = 0; i < 8; i++) {
545
777
  const candidate = randomHandle(randomBytes(8));
@@ -655,6 +887,23 @@ server.registerTool(
655
887
  });
656
888
  }
657
889
  const display = (name ?? process.env.MESSENGER_USER ?? userInfo().username ?? "me").trim() || "me";
890
+ const pin = process.env.MESSENGER_USER?.trim();
891
+ for (const sel of [name?.trim(), pin].filter((s2) => !!s2)) {
892
+ const dir = resolveIdentity(sel);
893
+ if (dir) {
894
+ S = buildSession(dir);
895
+ if (!pin) setCurrentUser(dir);
896
+ ensureWarmer();
897
+ return ok({
898
+ ok: true,
899
+ created: false,
900
+ name: S.me.name,
901
+ handle: S.me.handle,
902
+ fullKey: encodeKey(S.me.signPub, S.me.boxPub),
903
+ note: "You already have an account \u2014 this is your code to share."
904
+ });
905
+ }
906
+ }
658
907
  const id = generateIdentity();
659
908
  id.name = display;
660
909
  id.handle = await claimHandle(createMailboxClient(mailboxUrl, id, now));
@@ -664,8 +913,9 @@ server.registerTool(
664
913
  contactsFile(id.handle),
665
914
  JSON.stringify({ me: id.signPub, contacts: [] }, null, 2) + "\n"
666
915
  );
667
- setCurrentUser(id.handle);
916
+ if (!process.env.MESSENGER_USER?.trim()) setCurrentUser(id.handle);
668
917
  S = buildSession(id.handle);
918
+ ensureWarmer();
669
919
  return ok({
670
920
  ok: true,
671
921
  created: true,
@@ -752,9 +1002,10 @@ server.registerTool(
752
1002
  const deadline = now() + WATCH_MS;
753
1003
  const progressToken = extra?._meta?.progressToken;
754
1004
  let ticks = 0;
1005
+ await sync(S.ctx);
1006
+ let sinceSync = 0;
755
1007
  for (; ; ) {
756
1008
  if (extra?.signal?.aborted) return ok({ status: "idle", count: 0, note: "Watch cancelled." });
757
- await sync(S.ctx);
758
1009
  const rows = unreadFor(S.cache, S.me.signPub);
759
1010
  if (rows.length > 0) {
760
1011
  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.1",
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",
@@ -22,19 +22,21 @@
22
22
  "test:coverage": "node --test --experimental-test-coverage \"test/unit/*.test.ts\" \"test/integration/*.test.ts\"",
23
23
  "test:e2e": "node test/e2e/live-net.ts",
24
24
  "test:mcp": "node test/e2e/live-net-mcp.ts",
25
+ "test:push": "node test/e2e/live-push.ts",
25
26
  "init": "node src/init-identity.ts",
26
27
  "migrate": "node src/migrate.ts",
27
28
  "install-clis": "node src/install.ts",
28
29
  "add-contact": "node src/add-contact.ts",
29
30
  "mailbox": "node server-mailbox/node.ts",
30
31
  "server": "node src/server-net.ts",
31
- "deploy": "wrangler deploy"
32
+ "deploy": "npx wrangler deploy"
32
33
  },
33
34
  "dependencies": {
34
35
  "@hono/node-server": "^2.0.5",
35
36
  "@modelcontextprotocol/sdk": "^1.0.0",
36
37
  "hono": "^4.12.25",
37
38
  "libsodium-wrappers": "^0.8.4",
39
+ "node-sqlite3-wasm": "^0.8.58",
38
40
  "zod": "^3.23.8"
39
41
  },
40
42
  "devDependencies": {