aicq-codex 0.1.0

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/mcp/server.js ADDED
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AICQ Codex Plugin — stdio MCP server
4
+ * =====================================
5
+ * Exposes the AICQ end-to-end encrypted chat network to OpenAI Codex as
6
+ * Model Context Protocol tools. Shipped as an Agent Plugins v1 bundle:
7
+ *
8
+ * plugin.json (https://agent-plugins.org/schemas/1.0.0)
9
+ * mcp.json ({"mcpServers": {"aicq": {"type": "stdio", ...}}})
10
+ * lib/ CommonJS protocol stack reused verbatim from aicq-openclaw
11
+ * (identity / sql.js database / NaCl E2EE crypto / HTTP+WS
12
+ * signaling client / handshake / chat manager)
13
+ *
14
+ * Runtime model (Codex has no push channel):
15
+ * - The server keeps a WebSocket connection to the AICQ signaling server.
16
+ * - Every inbound message is decrypted and persisted into the local SQLite
17
+ * store by ChatManager regardless of any subscriber, so Codex pulls
18
+ * conversations with `aicq_chat_history` / `aicq_chat_read_unread`.
19
+ *
20
+ * Environment variables (forwarded by mcp.json env_vars):
21
+ * AICQ_SERVER_URL default https://aicq.me
22
+ * AICQ_DATA_DIR default ~/.aicq-codex
23
+ * AICQ_MASTER_NUMBER auto-add this AICQ number as friend on startup
24
+ * AICQ_AUTO_ACCEPT auto-accept friend requests, "true"/"false"
25
+ */
26
+
27
+ import { createRequire } from "module";
28
+ import path from "path";
29
+ import os from "os";
30
+ import fs from "fs";
31
+ import url from "url";
32
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
33
+ import {
34
+ ListToolsRequestSchema,
35
+ CallToolRequestSchema,
36
+ } from "@modelcontextprotocol/sdk/types.js";
37
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
38
+
39
+ const require = createRequire(import.meta.url);
40
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
41
+
42
+ // ── CJS protocol stack (lib/ declares {"type":"commonjs"}) ────────────
43
+ const PluginDatabase = require(path.join(__dirname, "..", "lib", "database"));
44
+ const IdentityManager = require(path.join(__dirname, "..", "lib", "identity"));
45
+ const ServerClient = require(path.join(__dirname, "..", "lib", "server-client"));
46
+ const HandshakeManager = require(path.join(__dirname, "..", "lib", "handshake"));
47
+ const ChatManager = require(path.join(__dirname, "..", "lib", "chat"));
48
+
49
+ const SERVER_URL = process.env.AICQ_SERVER_URL || "https://aicq.me";
50
+ const DATA_DIR =
51
+ process.env.AICQ_DATA_DIR || path.join(os.homedir(), ".aicq-codex");
52
+ const MASTER_NUMBER = process.env.AICQ_MASTER_NUMBER || "";
53
+ const AUTO_ACCEPT = (process.env.AICQ_AUTO_ACCEPT || "true").toLowerCase() === "true";
54
+ const AGENT_ID = "codex-default";
55
+
56
+ function log(msg) {
57
+ // stderr only — stdout belongs to the MCP JSON-RPC transport.
58
+ console.error(`[AICQ MCP] ${msg}`);
59
+ }
60
+
61
+ let _db, _identity, _serverClient, _handshake, _chat;
62
+ let _connected = false;
63
+
64
+ async function ensureInitialized() {
65
+ if (_db) return;
66
+
67
+ fs.mkdirSync(DATA_DIR, { recursive: true });
68
+ const uploadsDir = path.join(DATA_DIR, "uploads");
69
+ const userfilesDir = path.join(DATA_DIR, "userfiles");
70
+ fs.mkdirSync(uploadsDir, { recursive: true });
71
+ fs.mkdirSync(userfilesDir, { recursive: true });
72
+
73
+ _db = new PluginDatabase(DATA_DIR);
74
+ await _db.init();
75
+ log("SQLite database initialized");
76
+
77
+ _identity = new IdentityManager(_db);
78
+ if (_identity.listAgents().length === 0) {
79
+ _identity.createAgent(AGENT_ID, "AICQ Codex");
80
+ log(`Created agent identity: ${AGENT_ID}`);
81
+ }
82
+
83
+ _serverClient = new ServerClient(_identity, _db, SERVER_URL);
84
+ _handshake = new HandshakeManager(_identity, _serverClient, _db);
85
+ _chat = new ChatManager(_identity, _serverClient, _db, uploadsDir, userfilesDir);
86
+
87
+ await _serverClient.ensureAuth(AGENT_ID);
88
+ log(`Authenticated as ${AGENT_ID} against ${SERVER_URL}`);
89
+
90
+ if (typeof _serverClient.start === "function") {
91
+ await _serverClient.start(AGENT_ID);
92
+ } else {
93
+ _serverClient.connectWS();
94
+ }
95
+ log("WebSocket signaling connected");
96
+
97
+ // Keep local history warm so pull-based tools see messages promptly.
98
+ _chat.setOnNewMessage(async () => {});
99
+
100
+ try {
101
+ await syncFriendsFromServer();
102
+ } catch (e) {
103
+ log(`Initial friend sync failed: ${e.message}`);
104
+ }
105
+
106
+ if (MASTER_NUMBER) await addFriendByNumber(MASTER_NUMBER).catch((e) =>
107
+ log(`Auto-add master ${MASTER_NUMBER} failed: ${e.message}`)
108
+ );
109
+
110
+ _connected = true;
111
+ log(`Plugin runtime initialized (v${PLUGIN_VERSION})`);
112
+ }
113
+
114
+ async function syncFriendsFromServer() {
115
+ await _serverClient.ensureAuth(AGENT_ID);
116
+ const result = await _serverClient.listFriends();
117
+ if (!result || !Array.isArray(result.friends)) return;
118
+ for (const f of result.friends) {
119
+ const existing = _db.getFriend(AGENT_ID, f.id);
120
+ if (!existing) {
121
+ _db.addFriend({
122
+ agent_id: AGENT_ID,
123
+ id: f.id,
124
+ public_key: f.public_key || f.publicKey || "",
125
+ fingerprint: f.fingerprint || "",
126
+ friend_type: f.type || f.friend_type || "ai",
127
+ ai_name: f.agent_name || f.ai_name || f.displayName || "",
128
+ });
129
+ } else {
130
+ _db.updateFriendOnline(AGENT_ID, f.id, !!f.is_online);
131
+ }
132
+ }
133
+ }
134
+
135
+ async function addFriendByNumber(aicqNumber, message) {
136
+ await _serverClient.ensureAuth(AGENT_ID);
137
+ const result = await _serverClient.sendFriendRequest(
138
+ String(aicqNumber),
139
+ message || "Hi, I'd like to add you as a friend!"
140
+ );
141
+ if (result && result.status === "accepted" && result.to_id) {
142
+ _db.addFriend({
143
+ agent_id: AGENT_ID,
144
+ id: result.to_id,
145
+ public_key: "",
146
+ fingerprint: "",
147
+ friend_type: "human",
148
+ ai_name: "",
149
+ });
150
+ }
151
+ return {
152
+ success: true,
153
+ request_id: result && (result.id || result.request_id),
154
+ status: result ? result.status : undefined,
155
+ to_id: result ? result.to_id : undefined,
156
+ };
157
+ }
158
+
159
+ // ── Tool registry ─────────────────────────────────────────────────────
160
+
161
+ const PLUGIN_VERSION = (() => {
162
+ try {
163
+ return JSON.parse(
164
+ fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
165
+ ).version || "unknown";
166
+ } catch {
167
+ return "unknown";
168
+ }
169
+ })();
170
+
171
+ const TOOLS = [
172
+ {
173
+ name: "aicq_status",
174
+ description:
175
+ "Get the current AICQ encrypted-chat connection status and identity info. Call before other aicq_* tools if unsure.",
176
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
177
+ async run() {
178
+ const info = _identity.getInfo(AGENT_ID) || {};
179
+ const agents = _identity.listAgents();
180
+ return {
181
+ connected: _connected,
182
+ server_url: SERVER_URL,
183
+ data_dir: DATA_DIR,
184
+ agent_id: agents.length > 0 ? agents[0].agent_id : AGENT_ID,
185
+ account_id: info.account_id || info.aicq_number || null,
186
+ public_key: info.publicKey || info.public_key || null,
187
+ fingerprint: info.fingerprint || null,
188
+ version: PLUGIN_VERSION,
189
+ };
190
+ },
191
+ },
192
+ {
193
+ name: "aicq_friends_list",
194
+ description: "List all AICQ friends (humans and AI agents) with online status.",
195
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
196
+ async run() {
197
+ await syncFriendsFromServer().catch(() => {});
198
+ const friends = _db.listFriends(AGENT_ID);
199
+ return {
200
+ friends: friends.map((f) => ({
201
+ id: f.id,
202
+ name: f.ai_name || f.name || f.id,
203
+ type: f.friend_type || f.type || "unknown",
204
+ online: !!f.is_online,
205
+ fingerprint: f.fingerprint || null,
206
+ })),
207
+ };
208
+ },
209
+ },
210
+ {
211
+ name: "aicq_friend_add",
212
+ description:
213
+ "Send an AICQ friend request by AICQ number (e.g. '1000008'). Optionally attach a greeting message.",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ aicq_number: { type: "string", description: "Target AICQ number" },
218
+ message: { type: "string", description: "Optional friend request text" },
219
+ },
220
+ required: ["aicq_number"],
221
+ additionalProperties: false,
222
+ },
223
+ async run(args) {
224
+ return await addFriendByNumber(args.aicq_number, args.message);
225
+ },
226
+ },
227
+ {
228
+ name: "aicq_chat_send",
229
+ description:
230
+ "Send a text message to an AICQ friend by ID. Content supports plain text; markdown renders on recipients' clients.",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {
234
+ target_id: { type: "string", description: "Recipient AICQ account ID" },
235
+ content: { type: "string", description: "Message body to send" },
236
+ },
237
+ required: ["target_id", "content"],
238
+ additionalProperties: false,
239
+ },
240
+ async run(args) {
241
+ const result = await _chat.sendMessage(AGENT_ID, args.target_id, args.content, {
242
+ isGroup: false,
243
+ });
244
+ return { success: true, message_id: result?.message_id ?? null };
245
+ },
246
+ },
247
+ {
248
+ name: "aicq_chat_send_file",
249
+ description:
250
+ "Send a local file or image to an AICQ friend. Reads bytes from file_path and uploads through the AICQ server.",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ target_id: { type: "string", description: "Recipient AICQ account ID" },
255
+ file_path: { type: "string", description: "Absolute path of the local file" },
256
+ },
257
+ required: ["target_id", "file_path"],
258
+ additionalProperties: false,
259
+ },
260
+ async run(args) {
261
+ if (!fs.existsSync(args.file_path)) {
262
+ throw new Error(`file not found: ${args.file_path}`);
263
+ }
264
+ const buf = fs.readFileSync(args.file_path);
265
+ const result = await _chat.handleFileUpload(AGENT_ID, args.target_id, {
266
+ buffer: buf,
267
+ originalname: path.basename(args.file_path),
268
+ size: buf.length,
269
+ }, false);
270
+ return { success: true, ...(result || {}) };
271
+ },
272
+ },
273
+ {
274
+ name: "aicq_chat_history",
275
+ description:
276
+ "Read chat history with a friend from the local encrypted store. Omit friend_id to get the most recent messages across all chats.",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ friend_id: { type: "string", description: "AICQ account ID of the friend" },
281
+ limit: { type: "integer", description: "Max messages (default 50)" },
282
+ },
283
+ additionalProperties: false,
284
+ },
285
+ async run(args) {
286
+ const limit = Number.isInteger(args.limit) && args.limit > 0 ? args.limit : 50;
287
+ if (args.friend_id) {
288
+ return { messages: _db.getChatHistory(AGENT_ID, args.friend_id, { limit }) };
289
+ }
290
+ return { messages: _db.getRecentMessages(AGENT_ID, { limit }) };
291
+ },
292
+ },
293
+ {
294
+ name: "aicq_chat_refresh",
295
+ description:
296
+ "Pull each friend's recent conversation from the AICQ server so aicq_chat_history sees anything sent while this server was disconnected. Run before listing history after startup or long idle periods. Optionally mark a friend's conversation as read afterwards.",
297
+ inputSchema: {
298
+ type: "object",
299
+ properties: {
300
+ friend_id: { type: "string", description: "Sync only this friend; omit to sync all friends" },
301
+ mark_read: { type: "boolean", description: "Mark synced conversations as read on the server (default false)" },
302
+ },
303
+ additionalProperties: false,
304
+ },
305
+ async run(args) {
306
+ await _serverClient.ensureAuth(AGENT_ID);
307
+ const friends = _db.listFriends(AGENT_ID);
308
+ const targets = args.friend_id
309
+ ? [args.friend_id]
310
+ : friends.map((f) => f.id);
311
+ let synced = 0;
312
+ const failures = [];
313
+ for (const id of targets) {
314
+ try {
315
+ const conv = await _serverClient.getConversation(id, 50);
316
+ const msgs =
317
+ conv && Array.isArray(conv.messages)
318
+ ? conv.messages
319
+ : conv && Array.isArray(conv.data)
320
+ ? conv.data
321
+ : [];
322
+ for (const m of msgs) {
323
+ if (m._outbound || m.from_id === AGENT_ID) continue;
324
+ const existing = typeof _db.getMessage === "function"
325
+ ? _db.getMessage(m.id || m.message_id)
326
+ : null;
327
+ if (existing) continue;
328
+ _db.saveMessage({
329
+ agent_id: AGENT_ID,
330
+ target_id: id,
331
+ from_id: m.from_id || id,
332
+ to_id: m.to_id || AGENT_ID,
333
+ type: m.type || "text",
334
+ content:
335
+ typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
336
+ file_url: m.file_url || null,
337
+ file_name: m.file_name || null,
338
+ is_group: 0,
339
+ status: "delivered",
340
+ });
341
+ }
342
+ if (args.mark_read) await _serverClient.markRead(id).catch(() => {});
343
+ synced++;
344
+ } catch (e) {
345
+ failures.push({ friend_id: id, error: e.message });
346
+ }
347
+ }
348
+ return { synced_friends: synced, failures };
349
+ },
350
+ },
351
+ {
352
+ name: "aicq_groups_list",
353
+ description: "List AICQ group chats the agent belongs to.",
354
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
355
+ async run() {
356
+ await _serverClient.ensureAuth(AGENT_ID);
357
+ return await _serverClient.listGroups();
358
+ },
359
+ },
360
+ {
361
+ name: "aicq_group_messages",
362
+ description: "Fetch recent messages from an AICQ group chat.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ group_id: { type: "string" },
367
+ limit: { type: "integer", description: "Default 50" },
368
+ },
369
+ required: ["group_id"],
370
+ additionalProperties: false,
371
+ },
372
+ async run(args) {
373
+ await _serverClient.ensureAuth(AGENT_ID);
374
+ return await _serverClient.getGroupMessages(args.group_id, args.limit || 50);
375
+ },
376
+ },
377
+ {
378
+ name: "aicq_group_send",
379
+ description: "Send a text message to an AICQ group chat.",
380
+ inputSchema: {
381
+ type: "object",
382
+ properties: {
383
+ group_id: { type: "string" },
384
+ content: { type: "string" },
385
+ },
386
+ required: ["group_id", "content"],
387
+ additionalProperties: false,
388
+ },
389
+ async run(args) {
390
+ await _chat.sendMessage(AGENT_ID, args.group_id, args.content, { isGroup: true });
391
+ return { success: true };
392
+ },
393
+ },
394
+ ];
395
+
396
+ // ── MCP plumbing ──────────────────────────────────────────────────────
397
+
398
+ async function main() {
399
+ let ready = false;
400
+ try {
401
+ await ensureInitialized();
402
+ ready = true;
403
+ } catch (e) {
404
+ // Don't crash codex startup just because the network is down: expose
405
+ // tools anyway and let handlers surface actionable errors.
406
+ log(`Initialization deferred (${e.message}); tools will retry lazily.`);
407
+ }
408
+
409
+ const server = new Server(
410
+ { name: "aicq-codex", version: PLUGIN_VERSION },
411
+ { capabilities: { tools: {} } }
412
+ );
413
+
414
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
415
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({
416
+ name, description, inputSchema,
417
+ })),
418
+ }));
419
+
420
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
421
+ const name = req.params.name;
422
+ const tool = TOOLS.find((t) => t.name === name);
423
+ if (!tool) {
424
+ return {
425
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
426
+ isError: true,
427
+ };
428
+ }
429
+ try {
430
+ if (!ready) {
431
+ await ensureInitialized();
432
+ ready = true;
433
+ }
434
+ const result = await tool.run(req.params.arguments || {});
435
+ return {
436
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
437
+ };
438
+ } catch (e) {
439
+ log(`tool ${name} failed: ${e.message}`);
440
+ return {
441
+ content: [{ type: "text", text: `Error: ${e.message}` }],
442
+ isError: true,
443
+ };
444
+ }
445
+ });
446
+
447
+ const transport = new StdioServerTransport();
448
+ await server.connect(transport);
449
+ log(`stdio MCP server ready (plugin v${PLUGIN_VERSION})`);
450
+ }
451
+
452
+ main().catch((e) => {
453
+ console.error("[AICQ MCP] fatal:", e);
454
+ process.exit(1);
455
+ });
package/mcp.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "aicq": {
5
+ "type": "stdio",
6
+ "command": "node",
7
+ "args": ["./mcp/server.js"],
8
+ "env_vars": [
9
+ "AICQ_SERVER_URL",
10
+ "AICQ_DATA_DIR",
11
+ "AICQ_MASTER_NUMBER",
12
+ "AICQ_AUTO_ADD_FRIENDS"
13
+ ]
14
+ }
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "aicq-codex",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "main": "./mcp/server.js",
7
+ "bin": {
8
+ "aicq-codex-mcp": "./mcp/server.js"
9
+ },
10
+ "files": [
11
+ "plugin.json",
12
+ "mcp.json",
13
+ "mcp/",
14
+ "lib/",
15
+ "skills/",
16
+ ".codex-plugin/plugin.json",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "aicq",
22
+ "codex",
23
+ "openai-codex",
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "chat",
27
+ "encrypted",
28
+ "e2ee",
29
+ "agent-plugins"
30
+ ],
31
+ "author": {
32
+ "name": "samaidev",
33
+ "url": "https://aicq.me"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/samaidev/pluginAICQ.git",
39
+ "directory": "codex-plugin"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/samaidev/aicq/issues"
43
+ },
44
+ "homepage": "https://aicq.me",
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.12.1",
47
+ "node-fetch": "^2.7.0",
48
+ "sql.js": "^1.14.1",
49
+ "tweetnacl": "^1.0.3",
50
+ "tweetnacl-util": "^0.15.1",
51
+ "ws": "^8.16.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=20.0.0"
55
+ }
56
+ }
package/plugin.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "aicq-codex",
4
+ "version": "0.1.0",
5
+ "description": "AICQ End-to-end Encrypted Chat — exposes AICQ friend management, chat, file transfer, and group messaging to OpenAI Codex via a stdio MCP server.",
6
+ "author": {
7
+ "name": "samaidev",
8
+ "url": "https://aicq.me"
9
+ },
10
+ "homepage": "https://aicq.me",
11
+ "repository": "https://github.com/samaidev/pluginAICQ",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "aicq",
15
+ "codex",
16
+ "mcp",
17
+ "chat",
18
+ "encrypted",
19
+ "e2ee"
20
+ ]
21
+ }