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 +6 -4
- package/dist/check-inbox.js +126 -18
- package/dist/server-net.js +156 -20
- package/package.json +2 -1
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
package/dist/check-inbox.js
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
+
return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
|
|
61
63
|
} catch {
|
|
64
|
+
return void 0;
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
package/dist/server-net.js
CHANGED
|
@@ -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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
+
return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
|
|
98
100
|
} catch {
|
|
101
|
+
return void 0;
|
|
99
102
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|