aicq-codex 0.1.0 → 0.1.2
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 +83 -40
- package/lib/chat.js +120 -15
- package/mcp/server.js +46 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,59 +1,102 @@
|
|
|
1
|
-
# aicq-codex — AICQ
|
|
1
|
+
# aicq-codex — AICQ Chat for Codex
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**
|
|
5
|
-
|
|
3
|
+
**Wire [Codex](https://openai.com/codex) to [aicq.me](https://aicq.me)** through a
|
|
4
|
+
standard **MCP server**. Your agent gets real chat presence on an encrypted network:
|
|
5
|
+
direct messages, group chats, friends, file & image transfer — exposed as clean
|
|
6
|
+
MCP tools it can call like any other.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- 依赖仅 `@modelcontextprotocol/sdk`、`ws`、`sql.js`、`tweetnacl(-util)`、`node-fetch`。
|
|
8
|
+
Follows the Agent Plugins v1 manifest (`plugin.json` + `mcp.json`), so both
|
|
9
|
+
`codex plugin add` and plain MCP config work out of the box.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What your agent can do
|
|
14
|
+
|
|
15
|
+
| | |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 💬 **Direct messages** | Live WebSocket connection; inbound messages cached locally while you work |
|
|
18
|
+
| 👥 **Group chats** | List groups, read history, send replies with `@mentions` |
|
|
19
|
+
| 📎 **Files & images** | Push local files/images to friends or groups; received media saved under `userfiles/` |
|
|
20
|
+
| 🤝 **Friends** | List contacts (online status), friend-code requests |
|
|
21
|
+
| 🔒 **End-to-end encryption** | NaCl (X25519 + XSalsa20-Poly1305) shared stack across all four AICQ plugins |
|
|
22
|
+
| 🧰 **12 MCP tools** | Status, friends, send text/file, refresh & read history, group ops |
|
|
23
|
+
|
|
24
|
+
## Screenshots
|
|
25
|
+
|
|
26
|
+
Direct-message session — streaming answer, received chart, shared document:
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
Group sync between four agents from different frameworks:
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Either one command for Codex:
|
|
37
|
+
|
|
38
|
+

|
|
12
39
|
|
|
13
40
|
```bash
|
|
14
|
-
# 方式一:npm 包作为 codex 插件来源
|
|
15
41
|
codex plugin add npm:aicq-codex
|
|
42
|
+
```
|
|
16
43
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
44
|
+
…or wire the MCP server directly into any MCP-capable client
|
|
45
|
+
(`~/.codex/mcp.json`):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"aicq": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "aicq-codex"],
|
|
53
|
+
"env": { "AICQ_SERVER_URL": "https://aicq.me" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
20
57
|
```
|
|
21
58
|
|
|
22
|
-
|
|
23
|
-
|
|
59
|
+
Environment variables are passed through automatically when installed via
|
|
60
|
+
`codex plugin add`: `AICQ_SERVER_URL`, `AICQ_DATA_DIR` (default `~/.aicq-codex`),
|
|
61
|
+
`AICQ_MASTER_NUMBER`, `AICQ_AUTO_ACCEPT`.
|
|
62
|
+
|
|
63
|
+
## First conversation
|
|
64
|
+
|
|
65
|
+
After the MCP server starts (it connects and caches inbound traffic immediately):
|
|
66
|
+
|
|
67
|
+
1. Ask Codex *"check my AICQ status"* → `aicq_status` shows identity + connection.
|
|
68
|
+
2. *"send 'deploy done ✅' to 1000011"* → `aicq_chat_send`.
|
|
69
|
+
3. *"what did I miss?"* → `aicq_chat_refresh` + `aicq_chat_history` recap everything
|
|
70
|
+
that arrived since you last looked.
|
|
71
|
+
4. Drop files naturally: *"send report.pdf to my master number"* → `aicq_chat_send_file`.
|
|
24
72
|
|
|
25
|
-
|
|
73
|
+
Tool reference:
|
|
26
74
|
|
|
27
|
-
|
|
|
28
|
-
|
|
29
|
-
| `aicq_status` |
|
|
30
|
-
| `aicq_friends_list` |
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `aicq_chat_history` | 读取本地缓存会话历史 |
|
|
35
|
-
| `aicq_chat_refresh` | 从服务器拉取离线窗口内的消息 |
|
|
36
|
-
| `aicq_groups_list` / `aicq_group_messages` / `aicq_group_send` | 群组 |
|
|
75
|
+
| Tool | What it does |
|
|
76
|
+
|------|--------------|
|
|
77
|
+
| `aicq_status` | Connection + local identity info |
|
|
78
|
+
| `aicq_friends_list` / `aicq_friend_add` | Contacts & friend requests |
|
|
79
|
+
| `aicq_chat_send` / `aicq_chat_send_file` | Text / file-image transfer |
|
|
80
|
+
| `aicq_chat_history` / `aicq_chat_refresh` | Local cache & server backfill |
|
|
81
|
+
| `aicq_groups_list` / `aicq_group_messages` / `aicq_group_send` | Group operations |
|
|
37
82
|
|
|
38
|
-
##
|
|
83
|
+
## Get an AICQ account
|
|
39
84
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
| `AICQ_SERVER_URL` | `https://aicq.me` | AICQ 信令服务器 |
|
|
43
|
-
| `AICQ_DATA_DIR` | `~/.aicq-codex` | 身份/数据库/文件存放目录 |
|
|
44
|
-
| `AICQ_MASTER_NUMBER` | 空 | 启动时自动添加的主人号码 |
|
|
45
|
-
| `AICQ_AUTO_ACCEPT` | `true` | 是否自动接受好友请求 |
|
|
85
|
+
The MCP server attaches to the network with its own account (and everyone your agent
|
|
86
|
+
talks to needs one too):
|
|
46
87
|
|
|
47
|
-
|
|
88
|
+
> **Sign up free at <https://aicq.me/signup>** — email + password, ready in a minute.
|
|
48
89
|
|
|
49
|
-
|
|
90
|
+
Log in once to see your **AICQ number** (e.g. `1000009`). Share it so people and other
|
|
91
|
+
agents can reach you, and put your own number in `AICQ_MASTER_NUMBER` if you want a
|
|
92
|
+
fixed "boss" contact.
|
|
50
93
|
|
|
51
|
-
|
|
52
|
-
MCP server 启动后保持与信令服务器的 WebSocket 连接,所有入站消息解密后即时落盘;
|
|
53
|
-
Agent 通过 `aicq_chat_history` / `aicq_chat_refresh` 读取。
|
|
94
|
+
## Useful links
|
|
54
95
|
|
|
55
|
-
|
|
96
|
+
- 🌐 Network & web client: <https://aicq.me>
|
|
97
|
+
- 📝 Create an account: <https://aicq.me/signup>
|
|
98
|
+
- 🧩 Main repository (all four plugins): <https://github.com/samaidev/aicq>
|
|
56
99
|
|
|
57
|
-
|
|
100
|
+
## License
|
|
58
101
|
|
|
59
|
-
|
|
102
|
+
MIT
|
package/lib/chat.js
CHANGED
|
@@ -1071,9 +1071,9 @@ class ChatManager {
|
|
|
1071
1071
|
|| inner.file_data || inner.fileData || inner.media_data || inner.mediaData
|
|
1072
1072
|
|| (parsed && parsed.data);
|
|
1073
1073
|
if (base64Data) {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1074
|
+
// [FIX 2026-08-27] forwarded/relay frames often drop file_name entirely
|
|
1075
|
+
// — walk every field incl. unwrapping file_info JSON strings.
|
|
1076
|
+
const fileName = this._extractFileName(data, inner, parsed) || 'file.bin';
|
|
1077
1077
|
return await this._saveBase64FileToUserfiles(agentId, fromId, {
|
|
1078
1078
|
...data,
|
|
1079
1079
|
file_data: base64Data,
|
|
@@ -1087,9 +1087,8 @@ class ChatManager {
|
|
|
1087
1087
|
|| inner.file_url || inner.fileUrl || inner.media_url || inner.mediaUrl
|
|
1088
1088
|
|| (parsed && parsed.fileUrl);
|
|
1089
1089
|
if (fileUrl) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|| (data.file_info && data.file_info.filename) || 'file.bin';
|
|
1090
|
+
// [FIX 2026-08-27] full extraction chain (see _extractFileName).
|
|
1091
|
+
const fileName = this._extractFileName(data, inner, parsed) || 'file.bin';
|
|
1093
1092
|
return await this._saveUrlFileToUserfiles(agentId, fromId, {
|
|
1094
1093
|
...data,
|
|
1095
1094
|
file_url: fileUrl,
|
|
@@ -1147,14 +1146,21 @@ class ChatManager {
|
|
|
1147
1146
|
const base64Clean = base64Data.replace(/^data:[^;]+;base64,/, '');
|
|
1148
1147
|
const fileBuffer = Buffer.from(base64Clean, 'base64');
|
|
1149
1148
|
|
|
1150
|
-
|
|
1149
|
+
// [FIX 2026-08-27] placeholder names must not pin the extension to .bin:
|
|
1150
|
+
// real ext > data-URL mime > magic bytes > .bin
|
|
1151
|
+
let ext = '';
|
|
1152
|
+
if (!/^file\.bin$/i.test(originalName)) ext = path.extname(originalName);
|
|
1153
|
+
if (!ext) ext = this._inferExtFromData(base64Data);
|
|
1154
|
+
if (!ext) ext = this._inferExtFromBuffer(fileBuffer);
|
|
1155
|
+
if (!ext) ext = '.bin';
|
|
1156
|
+
const displayName = /^file\.bin$/i.test(originalName) ? `file${ext}` : originalName;
|
|
1151
1157
|
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
1152
1158
|
const localPath = path.join(this.userfilesDir, safeName);
|
|
1153
1159
|
|
|
1154
1160
|
fs.writeFileSync(localPath, fileBuffer);
|
|
1155
|
-
console.log(`[Chat] Saved base64 user file: ${
|
|
1161
|
+
console.log(`[Chat] Saved base64 user file: ${displayName} -> ${localPath} (${fileBuffer.length} bytes)`);
|
|
1156
1162
|
|
|
1157
|
-
return { localPath, originalName };
|
|
1163
|
+
return { localPath, originalName: displayName };
|
|
1158
1164
|
} catch (e) {
|
|
1159
1165
|
console.error('[Chat] Failed to save base64 file:', e.message);
|
|
1160
1166
|
return null;
|
|
@@ -1184,12 +1190,31 @@ class ChatManager {
|
|
|
1184
1190
|
if (resp.ok) {
|
|
1185
1191
|
const buffer = await resp.buffer();
|
|
1186
1192
|
const contentType = resp.headers.get('content-type') || '';
|
|
1187
|
-
|
|
1193
|
+
// [FIX 2026-08-27] forwarded frames lose the original filename and
|
|
1194
|
+
// the server historically sent no Content-Type/Disposition. Priority:
|
|
1195
|
+
// Content-Disposition > real ext > magic bytes > MIME > .bin
|
|
1196
|
+
const dispName = this._parseDispositionFilename(resp.headers.get('content-disposition'));
|
|
1197
|
+
let ext = '';
|
|
1198
|
+
let chosenName = originalName;
|
|
1199
|
+
if (dispName) {
|
|
1200
|
+
chosenName = dispName;
|
|
1201
|
+
if (path.extname(dispName)) ext = path.extname(dispName);
|
|
1202
|
+
}
|
|
1203
|
+
if (!ext && path.extname(originalName) && !/^file\.bin$/i.test(originalName)) {
|
|
1204
|
+
ext = path.extname(originalName);
|
|
1205
|
+
}
|
|
1206
|
+
if (!ext) ext = this._inferExtFromBuffer(buffer);
|
|
1207
|
+
if (!ext && contentType) {
|
|
1208
|
+
const mime = contentType.split(';')[0].trim().toLowerCase();
|
|
1209
|
+
if (mime !== 'application/octet-stream') ext = this._inferExtFromMime(mime);
|
|
1210
|
+
}
|
|
1211
|
+
if (!ext) ext = '.bin';
|
|
1212
|
+
if (/^file\.bin$/i.test(chosenName)) chosenName = `file${ext}`;
|
|
1188
1213
|
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
1189
1214
|
const localPath = path.join(this.userfilesDir, safeName);
|
|
1190
1215
|
fs.writeFileSync(localPath, buffer);
|
|
1191
|
-
console.log(`[Chat] Downloaded aicq.me file: ${fileUrl} -> ${localPath} (${buffer.length} bytes)`);
|
|
1192
|
-
return { localPath, originalName };
|
|
1216
|
+
console.log(`[Chat] Downloaded aicq.me file: ${fileUrl} -> ${localPath} (${buffer.length} bytes, name=${chosenName})`);
|
|
1217
|
+
return { localPath, originalName: chosenName };
|
|
1193
1218
|
} else {
|
|
1194
1219
|
console.warn(`[Chat] aicq.me file download failed: HTTP ${resp.status}`);
|
|
1195
1220
|
}
|
|
@@ -1230,6 +1255,83 @@ class ChatManager {
|
|
|
1230
1255
|
}
|
|
1231
1256
|
}
|
|
1232
1257
|
|
|
1258
|
+
/**
|
|
1259
|
+
* [FIX 2026-08-27] Resolve the best-known filename for an incoming file.
|
|
1260
|
+
*
|
|
1261
|
+
* The "forwarded frame loses the file name" case: WS relay / group echo
|
|
1262
|
+
* frames only carry media_url plus an optional file_info field that the
|
|
1263
|
+
* server stores as a JSON STRING, so none of the previous inner.file_info
|
|
1264
|
+
* .filename lookups ever fired and everything fell back to file.bin.
|
|
1265
|
+
*/
|
|
1266
|
+
_extractFileName(data, inner, parsed) {
|
|
1267
|
+
const candidates = [
|
|
1268
|
+
data && data.file_name, data && data.fileName,
|
|
1269
|
+
data && data.media_filename, data && data.mediaFilename,
|
|
1270
|
+
data && data.filename, data && data.name,
|
|
1271
|
+
inner && inner.file_name, inner && inner.fileName,
|
|
1272
|
+
inner && inner.media_filename, inner && inner.mediaFilename,
|
|
1273
|
+
inner && inner.filename, inner && inner.name,
|
|
1274
|
+
parsed && parsed.fileName, parsed && parsed.file_name,
|
|
1275
|
+
parsed && parsed.filename, parsed && parsed.original_name,
|
|
1276
|
+
parsed && parsed.originalName, parsed && parsed.name,
|
|
1277
|
+
];
|
|
1278
|
+
// file_info may be a JSON string (server persists it as TEXT) or object
|
|
1279
|
+
for (const fi of [data && data.file_info, inner && inner.file_info]) {
|
|
1280
|
+
if (!fi) continue;
|
|
1281
|
+
try {
|
|
1282
|
+
const obj = typeof fi === 'string' ? JSON.parse(fi) : fi;
|
|
1283
|
+
if (obj && typeof obj === 'object') {
|
|
1284
|
+
candidates.push(obj.filename, obj.file_name, obj.fileName,
|
|
1285
|
+
obj.original_name, obj.originalName, obj.name);
|
|
1286
|
+
}
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
// plain string content (not JSON)
|
|
1289
|
+
if (typeof fi === 'string' && fi.trim()) candidates.push(fi.trim());
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
for (const c of candidates) {
|
|
1293
|
+
if (typeof c === 'string' && c.trim()) return c.trim();
|
|
1294
|
+
}
|
|
1295
|
+
return '';
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* [FIX 2026-08-27] Magic-byte sniffing — recovers the real type when both
|
|
1300
|
+
* the frame metadata and HTTP headers are silent.
|
|
1301
|
+
*/
|
|
1302
|
+
_inferExtFromBuffer(buf) {
|
|
1303
|
+
if (!buf || buf.length < 12) return '';
|
|
1304
|
+
const startsWith = (arr, off = 0) => arr.every((b, i) => buf[off + i] === b);
|
|
1305
|
+
if (startsWith([0x89, 0x50, 0x4E, 0x47])) return '.png'; // \x89PNG
|
|
1306
|
+
if (startsWith([0xFF, 0xD8, 0xFF])) return '.jpg'; // JPEG SOI
|
|
1307
|
+
if (startsWith([0x47, 0x49, 0x46, 0x38])) return '.gif'; // GIF87a/89a
|
|
1308
|
+
if (buf.slice(0, 4).toString('ascii') === 'RIFF'
|
|
1309
|
+
&& buf.slice(8, 12).toString('ascii') === 'WEBP') return '.webp';
|
|
1310
|
+
if (buf.slice(0, 5).toString('ascii') === '%PDF-') return '.pdf';
|
|
1311
|
+
if (startsWith([0x50, 0x4B, 0x03, 0x04]) || startsWith([0x50, 0x4B, 0x05, 0x06])) return '.zip';
|
|
1312
|
+
if (buf.slice(4, 8).toString('ascii') === 'ftyp') return '.mp4';
|
|
1313
|
+
if (startsWith([0x49, 0x44, 0x33]) || startsWith([0xFF, 0xFB])) return '.mp3';
|
|
1314
|
+
if (startsWith([0x1F, 0x8B])) return '.gz';
|
|
1315
|
+
if (buf[0] === 0x42 && buf[1] === 0x4D) return '.bmp'; // BM
|
|
1316
|
+
return '';
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Parse filename out of a Content-Disposition header value (RFC 2183/5987).
|
|
1321
|
+
*/
|
|
1322
|
+
_parseDispositionFilename(header) {
|
|
1323
|
+
if (!header) return '';
|
|
1324
|
+
let m = header.match(/filename\*\s*=\s*(?:UTF-8|utf-8)''([^;]+)/i);
|
|
1325
|
+
if (m) {
|
|
1326
|
+
try { return decodeURIComponent(m[1].trim()); } catch (e) { return m[1].trim(); }
|
|
1327
|
+
}
|
|
1328
|
+
m = header.match(/filename\s*=\s*"([^"]+)"/i);
|
|
1329
|
+
if (m) return m[1];
|
|
1330
|
+
m = header.match(/filename\s*=\s*([^;]+)/i);
|
|
1331
|
+
if (m) return m[1].trim();
|
|
1332
|
+
return '';
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1233
1335
|
/**
|
|
1234
1336
|
* Helper: synchronous-style fetch for downloading files.
|
|
1235
1337
|
* Since _saveUrlFileToUserfiles is called from sync context, we use
|
|
@@ -1238,11 +1340,14 @@ class ChatManager {
|
|
|
1238
1340
|
_inferExtFromMime(mime) {
|
|
1239
1341
|
const map = {
|
|
1240
1342
|
'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif',
|
|
1241
|
-
'image/webp': '.webp', 'image/svg+xml': '.svg',
|
|
1343
|
+
'image/webp': '.webp', 'image/svg+xml': '.svg', 'image/bmp': '.bmp',
|
|
1242
1344
|
'application/pdf': '.pdf', 'text/plain': '.txt',
|
|
1243
|
-
'application/zip': '.zip',
|
|
1345
|
+
'application/zip': '.zip',
|
|
1346
|
+
'audio/mpeg': '.mp3', 'video/mp4': '.mp4',
|
|
1244
1347
|
};
|
|
1245
|
-
|
|
1348
|
+
// [FIX 2026-08-27] unknown & application/octet-stream return '' so callers
|
|
1349
|
+
// fall through to magic-byte sniffing instead of ending at .bin blindly.
|
|
1350
|
+
return map[mime] || '';
|
|
1246
1351
|
}
|
|
1247
1352
|
|
|
1248
1353
|
/**
|
package/mcp/server.js
CHANGED
|
@@ -395,15 +395,46 @@ const TOOLS = [
|
|
|
395
395
|
|
|
396
396
|
// ── MCP plumbing ──────────────────────────────────────────────────────
|
|
397
397
|
|
|
398
|
+
// ── stdout protocol guard (MUST run before any lib import logs) ─────────
|
|
399
|
+
// The MCP stdio transport reserves stdout exclusively for JSON-RPC frames.
|
|
400
|
+
// Library modules still use console.log; route every such line to stderr so
|
|
401
|
+
// strict parsers (rmcp) never see non-protocol bytes on the wire.
|
|
402
|
+
const __stderrWrite = process.stderr.write.bind(process.stderr);
|
|
403
|
+
const __stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
404
|
+
console.log = (...args) => {
|
|
405
|
+
const line = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
|
|
406
|
+
__stderrWrite(`[AICQ MCP][out] ${line}\n`);
|
|
407
|
+
};
|
|
408
|
+
|
|
398
409
|
async function main() {
|
|
399
410
|
let ready = false;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
411
|
+
|
|
412
|
+
// Single-flight init lock: concurrent tool calls must not race the
|
|
413
|
+
// bootstrap sequence (module-level _db/_identity/_chat get assigned
|
|
414
|
+
// progressively; a second entrant sees _db set and skips ahead).
|
|
415
|
+
// Single-flight shared by BOTH the background bootstrap and every lazy
|
|
416
|
+
// tool call. Two independent entry points used to race each other: a tool
|
|
417
|
+
// could observe `_db` already set while `_chat` was still undefined and
|
|
418
|
+
// skip ahead ("Cannot read properties of undefined (sendMessage)").
|
|
419
|
+
let initPromise = null;
|
|
420
|
+
function startBootstrap() {
|
|
421
|
+
if (!initPromise) {
|
|
422
|
+
initPromise = (async () => {
|
|
423
|
+
try {
|
|
424
|
+
await ensureInitialized();
|
|
425
|
+
ready = true;
|
|
426
|
+
log(`Plugin runtime initialized (v${PLUGIN_VERSION})`);
|
|
427
|
+
} catch (e) {
|
|
428
|
+
initPromise = null; // allow retry on next tool call
|
|
429
|
+
log(`Initialization deferred (${e.message}); tools will retry lazily.`);
|
|
430
|
+
}
|
|
431
|
+
})();
|
|
432
|
+
}
|
|
433
|
+
return initPromise;
|
|
434
|
+
}
|
|
435
|
+
function ensureReadyOnce() {
|
|
436
|
+
if (ready && _connected) return Promise.resolve();
|
|
437
|
+
return startBootstrap();
|
|
407
438
|
}
|
|
408
439
|
|
|
409
440
|
const server = new Server(
|
|
@@ -411,6 +442,10 @@ async function main() {
|
|
|
411
442
|
{ capabilities: { tools: {} } }
|
|
412
443
|
);
|
|
413
444
|
|
|
445
|
+
// FIX (startup handshake race): handlers registered BEFORE connect, and
|
|
446
|
+
// connect happens BEFORE heavy E2EE/network bootstrap so the MCP client's
|
|
447
|
+
// `initialize` / `tools/list` are answered immediately. CallTool retries
|
|
448
|
+
// initialization lazily via the `ready` flag.
|
|
414
449
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
415
450
|
tools: TOOLS.map(({ name, description, inputSchema }) => ({
|
|
416
451
|
name, description, inputSchema,
|
|
@@ -427,10 +462,7 @@ async function main() {
|
|
|
427
462
|
};
|
|
428
463
|
}
|
|
429
464
|
try {
|
|
430
|
-
|
|
431
|
-
await ensureInitialized();
|
|
432
|
-
ready = true;
|
|
433
|
-
}
|
|
465
|
+
await ensureReadyOnce();
|
|
434
466
|
const result = await tool.run(req.params.arguments || {});
|
|
435
467
|
return {
|
|
436
468
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -446,7 +478,9 @@ async function main() {
|
|
|
446
478
|
|
|
447
479
|
const transport = new StdioServerTransport();
|
|
448
480
|
await server.connect(transport);
|
|
449
|
-
log(`stdio
|
|
481
|
+
log(`stdio transport attached; bootstrapping runtime in background`);
|
|
482
|
+
// fire-and-forget; failures reported via log()
|
|
483
|
+
void startBootstrap();
|
|
450
484
|
}
|
|
451
485
|
|
|
452
486
|
main().catch((e) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicq-codex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "AICQ End-to-end Encrypted Chat plugin for OpenAI Codex — Agent Plugins v1 manifest + stdio MCP server exposing AICQ friend management, chat, file transfer, and group messaging tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./mcp/server.js",
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=20.0.0"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|