cli-chat-mcp 0.3.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,10 +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+ (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.
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.
24
26
 
25
27
  ## Set up to message someone (no clone)
26
28
 
@@ -52,30 +52,138 @@ function displayNameByKey(book, signPub) {
52
52
  }
53
53
 
54
54
  // src/db.ts
55
+ import { createRequire } from "node:module";
55
56
  import sqlite from "node-sqlite3-wasm";
56
- var { Database } = sqlite;
57
- function openMailbox(path) {
58
- const db = new Database(path);
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() {
59
61
  try {
60
- db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000;`);
62
+ return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
61
63
  } catch {
64
+ return void 0;
62
65
  }
63
- db.exec(`
64
- CREATE TABLE IF NOT EXISTS messages (
65
- id TEXT PRIMARY KEY,
66
- recipient TEXT NOT NULL,
67
- sender TEXT NOT NULL,
68
- body TEXT NOT NULL,
69
- tags TEXT,
70
- created_at INTEGER NOT NULL,
71
- fetched_at INTEGER,
72
- read_at INTEGER,
73
- in_reply_to TEXT
74
- );
75
- CREATE INDEX IF NOT EXISTS idx_recipient ON messages (recipient, created_at);
76
- `);
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);
77
135
  return db;
78
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
+ }
79
187
  function insertMessage(db, m) {
80
188
  db.run(
81
189
  `INSERT INTO messages
@@ -89,30 +89,138 @@ function contactByKey(book, signPub) {
89
89
  }
90
90
 
91
91
  // src/db.ts
92
+ import { createRequire } from "node:module";
92
93
  import sqlite from "node-sqlite3-wasm";
93
- var { Database } = sqlite;
94
- function openMailbox(path) {
95
- const db = new Database(path);
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() {
96
98
  try {
97
- db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000;`);
99
+ return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
98
100
  } catch {
101
+ return void 0;
99
102
  }
100
- db.exec(`
101
- CREATE TABLE IF NOT EXISTS messages (
102
- id TEXT PRIMARY KEY,
103
- recipient TEXT NOT NULL,
104
- sender TEXT NOT NULL,
105
- body TEXT NOT NULL,
106
- tags TEXT,
107
- created_at INTEGER NOT NULL,
108
- fetched_at INTEGER,
109
- read_at INTEGER,
110
- in_reply_to TEXT
111
- );
112
- CREATE INDEX IF NOT EXISTS idx_recipient ON messages (recipient, created_at);
113
- `);
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);
114
172
  return db;
115
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
+ }
116
224
  function insertMessage(db, m) {
117
225
  db.run(
118
226
  `INSERT INTO messages
@@ -346,6 +454,9 @@ function resolveDir(sel) {
346
454
  }
347
455
  return null;
348
456
  }
457
+ function resolveIdentity(sel) {
458
+ return resolveDir(sel);
459
+ }
349
460
  function currentUser() {
350
461
  const env = process.env.MESSENGER_USER?.trim();
351
462
  if (env) return resolveDir(env) ?? env;
@@ -623,7 +734,15 @@ function buildSession(user) {
623
734
  const contactsPath = contactsFile(user);
624
735
  const me = loadIdentity(identityFile(user));
625
736
  const book = loadContacts(contactsPath);
626
- 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
+ }
627
746
  const ctx = {
628
747
  me,
629
748
  book,
@@ -768,6 +887,23 @@ server.registerTool(
768
887
  });
769
888
  }
770
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
+ }
771
907
  const id = generateIdentity();
772
908
  id.name = display;
773
909
  id.handle = await claimHandle(createMailboxClient(mailboxUrl, id, now));
@@ -777,7 +913,7 @@ server.registerTool(
777
913
  contactsFile(id.handle),
778
914
  JSON.stringify({ me: id.signPub, contacts: [] }, null, 2) + "\n"
779
915
  );
780
- setCurrentUser(id.handle);
916
+ if (!process.env.MESSENGER_USER?.trim()) setCurrentUser(id.handle);
781
917
  S = buildSession(id.handle);
782
918
  ensureWarmer();
783
919
  return ok({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-chat-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Headless cross-CLI messenger for coding agents (MCP)",
6
6
  "bin": {
@@ -22,6 +22,7 @@
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",