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 +6 -3
- package/dist/check-inbox.js +137 -36
- package/dist/server-net.js +290 -39
- package/package.json +5 -3
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
package/dist/check-inbox.js
CHANGED
|
@@ -52,62 +52,163 @@ function displayNameByKey(book, signPub) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// src/db.ts
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
+
return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
|
|
60
63
|
} catch {
|
|
64
|
+
return void 0;
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
199
|
+
ORDER BY created_at ASC`,
|
|
200
|
+
[me]
|
|
201
|
+
);
|
|
101
202
|
}
|
|
102
203
|
function getMessage(db, id) {
|
|
103
|
-
return db.
|
|
204
|
+
return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
|
|
104
205
|
}
|
|
105
206
|
function markRead(db, id, now) {
|
|
106
|
-
db.
|
|
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
|
package/dist/server-net.js
CHANGED
|
@@ -89,62 +89,163 @@ function contactByKey(book, signPub) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// src/db.ts
|
|
92
|
-
import {
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
+
return createRequire(import.meta.url)("node:sqlite").DatabaseSync;
|
|
97
100
|
} catch {
|
|
101
|
+
return void 0;
|
|
98
102
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
236
|
+
ORDER BY created_at ASC`,
|
|
237
|
+
[me]
|
|
238
|
+
);
|
|
138
239
|
}
|
|
139
240
|
function getMessage(db, id) {
|
|
140
|
-
return db.
|
|
241
|
+
return db.get(`SELECT * FROM messages WHERE id = ?`, [id]) ?? void 0;
|
|
141
242
|
}
|
|
142
243
|
function markRead(db, id, now2) {
|
|
143
|
-
db.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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": {
|