chainlesschain 0.37.8 → 0.37.10

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.
Files changed (59) hide show
  1. package/README.md +403 -8
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +7 -2
  4. package/src/commands/agent.js +30 -0
  5. package/src/commands/ask.js +114 -0
  6. package/src/commands/audit.js +286 -0
  7. package/src/commands/auth.js +387 -0
  8. package/src/commands/browse.js +184 -0
  9. package/src/commands/chat.js +35 -0
  10. package/src/commands/db.js +152 -0
  11. package/src/commands/did.js +376 -0
  12. package/src/commands/encrypt.js +233 -0
  13. package/src/commands/export.js +125 -0
  14. package/src/commands/git.js +215 -0
  15. package/src/commands/import.js +259 -0
  16. package/src/commands/instinct.js +202 -0
  17. package/src/commands/llm.js +288 -0
  18. package/src/commands/mcp.js +302 -0
  19. package/src/commands/memory.js +282 -0
  20. package/src/commands/note.js +489 -0
  21. package/src/commands/org.js +505 -0
  22. package/src/commands/p2p.js +274 -0
  23. package/src/commands/plugin.js +398 -0
  24. package/src/commands/search.js +237 -0
  25. package/src/commands/session.js +238 -0
  26. package/src/commands/skill.js +479 -0
  27. package/src/commands/sync.js +249 -0
  28. package/src/commands/tokens.js +214 -0
  29. package/src/commands/wallet.js +416 -0
  30. package/src/index.js +65 -0
  31. package/src/lib/audit-logger.js +364 -0
  32. package/src/lib/bm25-search.js +322 -0
  33. package/src/lib/browser-automation.js +216 -0
  34. package/src/lib/crypto-manager.js +246 -0
  35. package/src/lib/did-manager.js +270 -0
  36. package/src/lib/ensure-utf8.js +59 -0
  37. package/src/lib/git-integration.js +220 -0
  38. package/src/lib/instinct-manager.js +190 -0
  39. package/src/lib/knowledge-exporter.js +302 -0
  40. package/src/lib/knowledge-importer.js +293 -0
  41. package/src/lib/llm-providers.js +325 -0
  42. package/src/lib/mcp-client.js +413 -0
  43. package/src/lib/memory-manager.js +211 -0
  44. package/src/lib/note-versioning.js +244 -0
  45. package/src/lib/org-manager.js +424 -0
  46. package/src/lib/p2p-manager.js +317 -0
  47. package/src/lib/pdf-parser.js +96 -0
  48. package/src/lib/permission-engine.js +374 -0
  49. package/src/lib/plan-mode.js +333 -0
  50. package/src/lib/platform.js +15 -0
  51. package/src/lib/plugin-manager.js +312 -0
  52. package/src/lib/response-cache.js +156 -0
  53. package/src/lib/session-manager.js +189 -0
  54. package/src/lib/sync-manager.js +347 -0
  55. package/src/lib/token-tracker.js +200 -0
  56. package/src/lib/wallet-manager.js +348 -0
  57. package/src/repl/agent-repl.js +912 -0
  58. package/src/repl/chat-repl.js +262 -0
  59. package/src/runtime/bootstrap.js +159 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Lightweight MCP (Model Context Protocol) client.
3
+ * Implements JSON-RPC 2.0 over stdio transport without external SDK dependency.
4
+ */
5
+
6
+ import { spawn } from "child_process";
7
+ import { EventEmitter } from "events";
8
+
9
+ /**
10
+ * MCP Server connection states.
11
+ */
12
+ export const ServerState = {
13
+ DISCONNECTED: "disconnected",
14
+ CONNECTING: "connecting",
15
+ CONNECTED: "connected",
16
+ ERROR: "error",
17
+ };
18
+
19
+ /**
20
+ * MCP Client — manages connections to MCP servers.
21
+ */
22
+ export class MCPClient extends EventEmitter {
23
+ constructor() {
24
+ super();
25
+ this.servers = new Map(); // name → { process, state, tools, resources, config }
26
+ this._nextId = 1;
27
+ }
28
+
29
+ /**
30
+ * Connect to an MCP server via stdio transport.
31
+ * @param {string} name - Server name
32
+ * @param {object} config - { command, args?, env? }
33
+ */
34
+ async connect(name, config) {
35
+ if (this.servers.has(name)) {
36
+ throw new Error(`Server "${name}" already connected`);
37
+ }
38
+
39
+ const entry = {
40
+ config,
41
+ state: ServerState.CONNECTING,
42
+ process: null,
43
+ tools: [],
44
+ resources: [],
45
+ prompts: [],
46
+ _pending: new Map(),
47
+ _buffer: "",
48
+ };
49
+
50
+ this.servers.set(name, entry);
51
+
52
+ try {
53
+ const proc = spawn(config.command, config.args || [], {
54
+ stdio: ["pipe", "pipe", "pipe"],
55
+ env: { ...process.env, ...(config.env || {}) },
56
+ });
57
+
58
+ entry.process = proc;
59
+
60
+ proc.stdout.on("data", (data) => {
61
+ this._handleData(name, data.toString("utf8"));
62
+ });
63
+
64
+ proc.stderr.on("data", (data) => {
65
+ this.emit("server-error", { name, error: data.toString("utf8") });
66
+ });
67
+
68
+ proc.on("close", (code) => {
69
+ entry.state = ServerState.DISCONNECTED;
70
+ this.emit("server-disconnected", { name, code });
71
+ });
72
+
73
+ proc.on("error", (err) => {
74
+ entry.state = ServerState.ERROR;
75
+ this.emit("server-error", { name, error: err.message });
76
+ });
77
+
78
+ // Initialize MCP protocol
79
+ const initResult = await this._sendRequest(name, "initialize", {
80
+ protocolVersion: "2024-11-05",
81
+ capabilities: { tools: {}, resources: {} },
82
+ clientInfo: { name: "chainlesschain-cli", version: "0.37.9" },
83
+ });
84
+
85
+ // Send initialized notification
86
+ this._sendNotification(name, "notifications/initialized", {});
87
+
88
+ entry.state = ServerState.CONNECTED;
89
+ entry.serverInfo = initResult?.serverInfo || {};
90
+ entry.capabilities = initResult?.capabilities || {};
91
+
92
+ // Fetch available tools
93
+ try {
94
+ const toolsResult = await this._sendRequest(name, "tools/list", {});
95
+ entry.tools = toolsResult?.tools || [];
96
+ } catch {
97
+ // Server may not support tools
98
+ }
99
+
100
+ // Fetch available resources
101
+ try {
102
+ const resourcesResult = await this._sendRequest(
103
+ name,
104
+ "resources/list",
105
+ {},
106
+ );
107
+ entry.resources = resourcesResult?.resources || [];
108
+ } catch {
109
+ // Server may not support resources
110
+ }
111
+
112
+ this.emit("server-connected", { name, tools: entry.tools.length });
113
+ return {
114
+ name,
115
+ state: entry.state,
116
+ tools: entry.tools,
117
+ resources: entry.resources,
118
+ serverInfo: entry.serverInfo,
119
+ };
120
+ } catch (err) {
121
+ entry.state = ServerState.ERROR;
122
+ this.servers.delete(name);
123
+ throw err;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Disconnect from an MCP server.
129
+ */
130
+ async disconnect(name) {
131
+ const entry = this.servers.get(name);
132
+ if (!entry) return false;
133
+
134
+ if (entry.process) {
135
+ entry.process.kill();
136
+ }
137
+
138
+ entry.state = ServerState.DISCONNECTED;
139
+ this.servers.delete(name);
140
+ return true;
141
+ }
142
+
143
+ /**
144
+ * Disconnect from all servers.
145
+ */
146
+ async disconnectAll() {
147
+ const names = [...this.servers.keys()];
148
+ for (const name of names) {
149
+ await this.disconnect(name);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * List all connected servers.
155
+ */
156
+ listServers() {
157
+ const result = [];
158
+ for (const [name, entry] of this.servers) {
159
+ result.push({
160
+ name,
161
+ state: entry.state,
162
+ tools: entry.tools.length,
163
+ resources: entry.resources.length,
164
+ serverInfo: entry.serverInfo || {},
165
+ });
166
+ }
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * List tools from a specific server or all servers.
172
+ */
173
+ listTools(serverName) {
174
+ if (serverName) {
175
+ const entry = this.servers.get(serverName);
176
+ if (!entry) throw new Error(`Server "${serverName}" not found`);
177
+ return entry.tools.map((t) => ({ ...t, server: serverName }));
178
+ }
179
+
180
+ const allTools = [];
181
+ for (const [name, entry] of this.servers) {
182
+ for (const tool of entry.tools) {
183
+ allTools.push({ ...tool, server: name });
184
+ }
185
+ }
186
+ return allTools;
187
+ }
188
+
189
+ /**
190
+ * Call a tool on a specific server.
191
+ * @param {string} serverName - Server name
192
+ * @param {string} toolName - Tool name
193
+ * @param {object} args - Tool arguments
194
+ */
195
+ async callTool(serverName, toolName, args = {}) {
196
+ const entry = this.servers.get(serverName);
197
+ if (!entry) throw new Error(`Server "${serverName}" not found`);
198
+ if (entry.state !== ServerState.CONNECTED) {
199
+ throw new Error(`Server "${serverName}" is not connected`);
200
+ }
201
+
202
+ const result = await this._sendRequest(serverName, "tools/call", {
203
+ name: toolName,
204
+ arguments: args,
205
+ });
206
+
207
+ return result;
208
+ }
209
+
210
+ /**
211
+ * Read a resource from a server.
212
+ */
213
+ async readResource(serverName, uri) {
214
+ const entry = this.servers.get(serverName);
215
+ if (!entry) throw new Error(`Server "${serverName}" not found`);
216
+
217
+ const result = await this._sendRequest(serverName, "resources/read", {
218
+ uri,
219
+ });
220
+ return result;
221
+ }
222
+
223
+ // ─── Internal JSON-RPC transport ──────────────────────────────
224
+
225
+ _sendRequest(serverName, method, params) {
226
+ return new Promise((resolve, reject) => {
227
+ const entry = this.servers.get(serverName);
228
+ if (!entry || !entry.process) {
229
+ return reject(new Error("Server not available"));
230
+ }
231
+
232
+ const id = this._nextId++;
233
+ const message = JSON.stringify({
234
+ jsonrpc: "2.0",
235
+ id,
236
+ method,
237
+ params,
238
+ });
239
+
240
+ entry._pending.set(id, { resolve, reject });
241
+
242
+ // Set timeout
243
+ const timeout = setTimeout(() => {
244
+ entry._pending.delete(id);
245
+ reject(new Error(`Request timeout: ${method}`));
246
+ }, 30000);
247
+
248
+ entry._pending.get(id).timeout = timeout;
249
+
250
+ try {
251
+ entry.process.stdin.write(message + "\n");
252
+ } catch (err) {
253
+ clearTimeout(timeout);
254
+ entry._pending.delete(id);
255
+ reject(err);
256
+ }
257
+ });
258
+ }
259
+
260
+ _sendNotification(serverName, method, params) {
261
+ const entry = this.servers.get(serverName);
262
+ if (!entry || !entry.process) return;
263
+
264
+ const message = JSON.stringify({
265
+ jsonrpc: "2.0",
266
+ method,
267
+ params,
268
+ });
269
+
270
+ try {
271
+ entry.process.stdin.write(message + "\n");
272
+ } catch {
273
+ // Ignore notification errors
274
+ }
275
+ }
276
+
277
+ _handleData(serverName, data) {
278
+ const entry = this.servers.get(serverName);
279
+ if (!entry) return;
280
+
281
+ entry._buffer += data;
282
+
283
+ // Process complete JSON lines
284
+ const lines = entry._buffer.split("\n");
285
+ entry._buffer = lines.pop() || "";
286
+
287
+ for (const line of lines) {
288
+ const trimmed = line.trim();
289
+ if (!trimmed) continue;
290
+
291
+ try {
292
+ const msg = JSON.parse(trimmed);
293
+ this._handleMessage(serverName, msg);
294
+ } catch {
295
+ // Skip malformed lines
296
+ }
297
+ }
298
+ }
299
+
300
+ _handleMessage(serverName, msg) {
301
+ const entry = this.servers.get(serverName);
302
+ if (!entry) return;
303
+
304
+ // Response to a request
305
+ if (msg.id !== undefined && entry._pending.has(msg.id)) {
306
+ const { resolve, reject, timeout } = entry._pending.get(msg.id);
307
+ clearTimeout(timeout);
308
+ entry._pending.delete(msg.id);
309
+
310
+ if (msg.error) {
311
+ reject(new Error(msg.error.message || "Unknown error"));
312
+ } else {
313
+ resolve(msg.result);
314
+ }
315
+ return;
316
+ }
317
+
318
+ // Server notification
319
+ if (msg.method) {
320
+ this.emit("notification", {
321
+ server: serverName,
322
+ method: msg.method,
323
+ params: msg.params,
324
+ });
325
+ }
326
+ }
327
+ }
328
+
329
+ /**
330
+ * MCP server configuration storage.
331
+ * Persists server configs in the database.
332
+ */
333
+ export class MCPServerConfig {
334
+ constructor(db) {
335
+ this.db = db;
336
+ this._ensureTable();
337
+ }
338
+
339
+ _ensureTable() {
340
+ this.db.exec(`
341
+ CREATE TABLE IF NOT EXISTS mcp_servers (
342
+ name TEXT PRIMARY KEY,
343
+ command TEXT NOT NULL,
344
+ args TEXT DEFAULT '[]',
345
+ env TEXT DEFAULT '{}',
346
+ auto_connect INTEGER DEFAULT 0,
347
+ created_at TEXT DEFAULT (datetime('now')),
348
+ updated_at TEXT DEFAULT (datetime('now'))
349
+ )
350
+ `);
351
+ }
352
+
353
+ add(name, config) {
354
+ this.db
355
+ .prepare(
356
+ "INSERT OR REPLACE INTO mcp_servers (name, command, args, env, auto_connect, updated_at) VALUES (?, ?, ?, ?, ?, datetime('now'))",
357
+ )
358
+ .run(
359
+ name,
360
+ config.command,
361
+ JSON.stringify(config.args || []),
362
+ JSON.stringify(config.env || {}),
363
+ config.autoConnect ? 1 : 0,
364
+ );
365
+ }
366
+
367
+ remove(name) {
368
+ const result = this.db
369
+ .prepare("DELETE FROM mcp_servers WHERE name = ?")
370
+ .run(name);
371
+ return result.changes > 0;
372
+ }
373
+
374
+ get(name) {
375
+ const row = this.db
376
+ .prepare("SELECT * FROM mcp_servers WHERE name = ?")
377
+ .get(name);
378
+ if (!row) return null;
379
+ return {
380
+ name: row.name,
381
+ command: row.command,
382
+ args: JSON.parse(row.args || "[]"),
383
+ env: JSON.parse(row.env || "{}"),
384
+ autoConnect: row.auto_connect === 1,
385
+ };
386
+ }
387
+
388
+ list() {
389
+ const rows = this.db
390
+ .prepare("SELECT * FROM mcp_servers ORDER BY name")
391
+ .all();
392
+ return rows.map((row) => ({
393
+ name: row.name,
394
+ command: row.command,
395
+ args: JSON.parse(row.args || "[]"),
396
+ env: JSON.parse(row.env || "{}"),
397
+ autoConnect: row.auto_connect === 1,
398
+ }));
399
+ }
400
+
401
+ getAutoConnect() {
402
+ const rows = this.db
403
+ .prepare("SELECT * FROM mcp_servers WHERE auto_connect = ? ORDER BY name")
404
+ .all(1);
405
+ return rows.map((row) => ({
406
+ name: row.name,
407
+ command: row.command,
408
+ args: JSON.parse(row.args || "[]"),
409
+ env: JSON.parse(row.env || "{}"),
410
+ autoConnect: true,
411
+ }));
412
+ }
413
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Persistent memory manager for CLI
3
+ *
4
+ * Manages daily notes and long-term memory files.
5
+ * Lightweight port of desktop-app-vue/src/main/llm/permanent-memory-manager.js
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+
11
+ /**
12
+ * Ensure the memory directory structure exists
13
+ */
14
+ function ensureMemoryDirs(memoryDir) {
15
+ try {
16
+ const dailyDir = path.join(memoryDir, "daily");
17
+ if (!fs.existsSync(memoryDir)) fs.mkdirSync(memoryDir, { recursive: true });
18
+ if (!fs.existsSync(dailyDir)) fs.mkdirSync(dailyDir, { recursive: true });
19
+ } catch (err) {
20
+ throw new Error(`Failed to create memory directory: ${err.message}`);
21
+ }
22
+ }
23
+
24
+ function ensureMemoryTable(db) {
25
+ db.exec(`
26
+ CREATE TABLE IF NOT EXISTS memory_entries (
27
+ id TEXT PRIMARY KEY,
28
+ content TEXT NOT NULL,
29
+ category TEXT DEFAULT 'general',
30
+ importance INTEGER DEFAULT 3,
31
+ source TEXT DEFAULT 'user',
32
+ created_at TEXT DEFAULT (datetime('now')),
33
+ updated_at TEXT DEFAULT (datetime('now'))
34
+ )
35
+ `);
36
+ }
37
+
38
+ /**
39
+ * Get the memory directory path
40
+ */
41
+ export function getMemoryDir(dataDir) {
42
+ return path.join(dataDir, "memory");
43
+ }
44
+
45
+ /**
46
+ * Add an entry to memory (stored in DB)
47
+ */
48
+ export function addMemory(db, content, options = {}) {
49
+ ensureMemoryTable(db);
50
+
51
+ if (!content || !content.trim()) {
52
+ throw new Error("Memory content cannot be empty");
53
+ }
54
+
55
+ const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
56
+ const category = options.category || "general";
57
+ const importance = Math.max(
58
+ 1,
59
+ Math.min(5, parseInt(options.importance) || 3),
60
+ );
61
+ const source = options.source || "user";
62
+
63
+ db.prepare(
64
+ `INSERT INTO memory_entries (id, content, category, importance, source) VALUES (?, ?, ?, ?, ?)`,
65
+ ).run(id, content, category, importance, source);
66
+
67
+ return { id, content, category, importance };
68
+ }
69
+
70
+ /**
71
+ * Search memory entries
72
+ */
73
+ export function searchMemory(db, query, options = {}) {
74
+ ensureMemoryTable(db);
75
+
76
+ if (!query || !query.trim()) return [];
77
+
78
+ const limit = Math.max(1, parseInt(options.limit) || 20);
79
+ const pattern = `%${query}%`;
80
+
81
+ return db
82
+ .prepare(
83
+ `SELECT * FROM memory_entries
84
+ WHERE content LIKE ?
85
+ ORDER BY importance DESC, created_at DESC
86
+ LIMIT ?`,
87
+ )
88
+ .all(pattern, limit);
89
+ }
90
+
91
+ /**
92
+ * List all memory entries
93
+ */
94
+ export function listMemory(db, options = {}) {
95
+ ensureMemoryTable(db);
96
+
97
+ const limit = Math.max(1, parseInt(options.limit) || 50);
98
+ const category = options.category;
99
+
100
+ if (category) {
101
+ return db
102
+ .prepare(
103
+ `SELECT * FROM memory_entries WHERE category = ? ORDER BY importance DESC, created_at DESC LIMIT ?`,
104
+ )
105
+ .all(category, limit);
106
+ }
107
+
108
+ return db
109
+ .prepare(
110
+ `SELECT * FROM memory_entries ORDER BY importance DESC, created_at DESC LIMIT ?`,
111
+ )
112
+ .all(limit);
113
+ }
114
+
115
+ /**
116
+ * Delete a memory entry
117
+ */
118
+ export function deleteMemory(db, id) {
119
+ ensureMemoryTable(db);
120
+
121
+ // Try exact match first, then prefix match
122
+ let result = db.prepare("DELETE FROM memory_entries WHERE id = ?").run(id);
123
+
124
+ if (result.changes === 0 && id.length >= 4) {
125
+ result = db
126
+ .prepare("DELETE FROM memory_entries WHERE id LIKE ? LIMIT 1")
127
+ .run(`${id}%`);
128
+ }
129
+
130
+ return result.changes > 0;
131
+ }
132
+
133
+ /**
134
+ * Append to today's daily note
135
+ */
136
+ export function appendDailyNote(memoryDir, content) {
137
+ ensureMemoryDirs(memoryDir);
138
+
139
+ const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
140
+ const filePath = path.join(memoryDir, "daily", `${today}.md`);
141
+
142
+ const timestamp = new Date().toISOString().slice(11, 19); // HH:MM:SS
143
+ const entry = `\n## ${timestamp}\n\n${content}\n`;
144
+
145
+ if (fs.existsSync(filePath)) {
146
+ fs.appendFileSync(filePath, entry, "utf8");
147
+ } else {
148
+ const header = `# Daily Note: ${today}\n${entry}`;
149
+ fs.writeFileSync(filePath, header, "utf8");
150
+ }
151
+
152
+ return { date: today, path: filePath };
153
+ }
154
+
155
+ /**
156
+ * Read a daily note
157
+ */
158
+ export function getDailyNote(memoryDir, date) {
159
+ if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) return null;
160
+ const filePath = path.join(memoryDir, "daily", `${date}.md`);
161
+ if (!fs.existsSync(filePath)) return null;
162
+ return fs.readFileSync(filePath, "utf8");
163
+ }
164
+
165
+ /**
166
+ * List available daily notes
167
+ */
168
+ export function listDailyNotes(memoryDir, options = {}) {
169
+ ensureMemoryDirs(memoryDir);
170
+
171
+ const dailyDir = path.join(memoryDir, "daily");
172
+ const limit = Math.max(1, parseInt(options.limit) || 30);
173
+
174
+ try {
175
+ const files = fs
176
+ .readdirSync(dailyDir)
177
+ .filter((f) => f.endsWith(".md"))
178
+ .sort()
179
+ .reverse()
180
+ .slice(0, limit);
181
+
182
+ return files.map((f) => {
183
+ const filePath = path.join(dailyDir, f);
184
+ const stat = fs.statSync(filePath);
185
+ return {
186
+ date: f.replace(".md", ""),
187
+ path: filePath,
188
+ size: stat.size,
189
+ };
190
+ });
191
+ } catch {
192
+ return [];
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Read or update the MEMORY.md file (long-term knowledge)
198
+ */
199
+ export function getMemoryFile(memoryDir) {
200
+ ensureMemoryDirs(memoryDir);
201
+ const filePath = path.join(memoryDir, "MEMORY.md");
202
+ if (!fs.existsSync(filePath)) return "";
203
+ return fs.readFileSync(filePath, "utf8");
204
+ }
205
+
206
+ export function updateMemoryFile(memoryDir, content) {
207
+ ensureMemoryDirs(memoryDir);
208
+ const filePath = path.join(memoryDir, "MEMORY.md");
209
+ fs.writeFileSync(filePath, content, "utf8");
210
+ return { path: filePath };
211
+ }