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.
- package/README.md +403 -8
- package/bin/chainlesschain.js +4 -0
- package/package.json +7 -2
- package/src/commands/agent.js +30 -0
- package/src/commands/ask.js +114 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/chat.js +35 -0
- package/src/commands/db.js +152 -0
- package/src/commands/did.js +376 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/import.js +259 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +288 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +489 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +398 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +479 -0
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/index.js +65 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/platform.js +15 -0
- package/src/lib/plugin-manager.js +312 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/repl/agent-repl.js +912 -0
- package/src/repl/chat-repl.js +262 -0
- 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
|
+
}
|