coding-friend-cli 1.21.0 → 1.22.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/dist/{chunk-3KXYEC2T.js → chunk-LCABODSZ.js} +80 -1
- package/dist/{memory-PDENAODU.js → chunk-TPLH46BH.js} +152 -76
- package/dist/{config-5I3XJEEL.js → config-K2S23A5R.js} +2 -1
- package/dist/index.js +15 -15
- package/dist/{init-3XMC6YJI.js → init-FMPOE2MN.js} +8 -22
- package/dist/{mcp-U34LFD34.js → mcp-SQWZXF5Q.js} +30 -3
- package/dist/memory-7WLVIFAZ.js +37 -0
- package/lib/cf-memory/CHANGELOG.md +6 -0
- package/lib/cf-memory/README.md +3 -0
- package/lib/cf-memory/package.json +1 -1
- package/lib/cf-memory/src/__tests__/daemon-client.test.ts +146 -0
- package/lib/cf-memory/src/daemon/process.ts +7 -0
- package/lib/cf-memory/src/index.ts +13 -1
- package/lib/cf-memory/src/lib/daemon-client.ts +54 -3
- package/lib/cf-memory/src/lib/tier.ts +27 -3
- package/package.json +1 -1
- package/dist/{host-V5O3PDCF.js → host-E644BDZK.js} +3 -3
- package/dist/{status-F6WASK7N.js → status-CGYNNMYF.js} +3 -3
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getLibPath
|
|
3
3
|
} from "./chunk-RZRT7NGT.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveMemoryDir
|
|
6
|
+
} from "./chunk-SIQJPT47.js";
|
|
4
7
|
import {
|
|
5
8
|
BACK,
|
|
6
9
|
askScope,
|
|
@@ -9,7 +12,8 @@ import {
|
|
|
9
12
|
} from "./chunk-BIX7AJU3.js";
|
|
10
13
|
import {
|
|
11
14
|
mergeJson,
|
|
12
|
-
readJson
|
|
15
|
+
readJson,
|
|
16
|
+
writeJson
|
|
13
17
|
} from "./chunk-5UVDWG5L.js";
|
|
14
18
|
import {
|
|
15
19
|
globalConfigPath,
|
|
@@ -20,7 +24,10 @@ import {
|
|
|
20
24
|
} from "./chunk-W5CD7WTX.js";
|
|
21
25
|
|
|
22
26
|
// src/lib/memory-prompts.ts
|
|
27
|
+
import { existsSync } from "fs";
|
|
28
|
+
import { homedir } from "os";
|
|
23
29
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
30
|
+
import chalk from "chalk";
|
|
24
31
|
import { join } from "path";
|
|
25
32
|
function getMemoryFieldScope(field, globalCfg, localCfg) {
|
|
26
33
|
const globalSection = globalCfg?.memory;
|
|
@@ -235,6 +242,67 @@ async function editMemoryDaemonTimeout(globalCfg, localCfg) {
|
|
|
235
242
|
idleTimeout: Number(value) * 6e4
|
|
236
243
|
});
|
|
237
244
|
}
|
|
245
|
+
function writeMemoryMcpEntry(serverPath, memoryDir) {
|
|
246
|
+
const mcpPath = join(process.cwd(), ".mcp.json");
|
|
247
|
+
const existing = readJson(mcpPath) ?? {};
|
|
248
|
+
const servers = existing.mcpServers ?? {};
|
|
249
|
+
writeJson(mcpPath, {
|
|
250
|
+
...existing,
|
|
251
|
+
mcpServers: {
|
|
252
|
+
...servers,
|
|
253
|
+
"coding-friend-memory": {
|
|
254
|
+
command: "node",
|
|
255
|
+
args: [serverPath, memoryDir]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
log.success("Added coding-friend-memory to .mcp.json");
|
|
260
|
+
}
|
|
261
|
+
function getMemoryMcpStatus() {
|
|
262
|
+
const localMcpPath = join(process.cwd(), ".mcp.json");
|
|
263
|
+
const localMcp = readJson(localMcpPath);
|
|
264
|
+
const localServers = localMcp?.mcpServers;
|
|
265
|
+
if (localServers != null && "coding-friend-memory" in localServers) {
|
|
266
|
+
return { configured: true, scope: "local" };
|
|
267
|
+
}
|
|
268
|
+
const globalMcpPath = join(homedir(), ".claude", ".mcp.json");
|
|
269
|
+
const globalMcp = readJson(globalMcpPath);
|
|
270
|
+
const globalServers = globalMcp?.mcpServers;
|
|
271
|
+
if (globalServers != null && "coding-friend-memory" in globalServers) {
|
|
272
|
+
return { configured: true, scope: "global" };
|
|
273
|
+
}
|
|
274
|
+
return { configured: false, scope: null };
|
|
275
|
+
}
|
|
276
|
+
async function editMemoryMcp() {
|
|
277
|
+
const status = getMemoryMcpStatus();
|
|
278
|
+
if (status.configured) {
|
|
279
|
+
const label = status.scope === "local" ? chalk.green("configured") + chalk.dim(" (local .mcp.json)") : chalk.green("configured") + chalk.dim(" (global ~/.claude/.mcp.json)") + " " + chalk.yellow("\u26A0 only works for one project");
|
|
280
|
+
log.info(`MCP: ${label}`);
|
|
281
|
+
const reconfigure = await confirm({
|
|
282
|
+
message: "Reconfigure Memory MCP in local .mcp.json?",
|
|
283
|
+
default: false
|
|
284
|
+
});
|
|
285
|
+
if (!reconfigure) return;
|
|
286
|
+
}
|
|
287
|
+
let mcpDir;
|
|
288
|
+
try {
|
|
289
|
+
mcpDir = getLibPath("cf-memory");
|
|
290
|
+
} catch {
|
|
291
|
+
log.warn(
|
|
292
|
+
"cf-memory package not found. Install the CLI first: npm i -g coding-friend-cli"
|
|
293
|
+
);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const serverPath = join(mcpDir, "dist", "index.js");
|
|
297
|
+
if (!existsSync(serverPath)) {
|
|
298
|
+
log.warn(
|
|
299
|
+
'cf-memory not built yet. Run "cf memory mcp" after building to get the config.'
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const memoryDir = resolveMemoryDir();
|
|
304
|
+
writeMemoryMcpEntry(serverPath, memoryDir);
|
|
305
|
+
}
|
|
238
306
|
async function memoryConfigMenu(opts) {
|
|
239
307
|
while (true) {
|
|
240
308
|
const globalCfg = readJson(globalConfigPath());
|
|
@@ -274,6 +342,8 @@ async function memoryConfigMenu(opts) {
|
|
|
274
342
|
const daemonScope = getMemoryFieldScope("daemon", globalCfg, localCfg);
|
|
275
343
|
const daemonVal = getMergedMemoryValue("daemon", globalCfg, localCfg);
|
|
276
344
|
const embeddingLabel = embeddingVal?.provider ? embeddingVal.model ? `${embeddingVal.model} (${embeddingVal.provider})` : embeddingVal.provider : "";
|
|
345
|
+
const mcpStatus = getMemoryMcpStatus();
|
|
346
|
+
const mcpLabel = mcpStatus.configured ? mcpStatus.scope === "local" ? chalk.green("configured") + chalk.dim(" (.mcp.json)") : chalk.green("configured") + chalk.dim(" (global)") + " " + chalk.yellow("\u26A0") : chalk.dim("not configured");
|
|
277
347
|
const choice = await select({
|
|
278
348
|
message: "Memory settings:",
|
|
279
349
|
choices: injectBackChoice(
|
|
@@ -297,6 +367,10 @@ async function memoryConfigMenu(opts) {
|
|
|
297
367
|
{
|
|
298
368
|
name: `Daemon timeout ${formatScopeLabel(daemonScope)}${daemonVal?.idleTimeout ? ` (${daemonVal.idleTimeout / 6e4}min)` : ""}`,
|
|
299
369
|
value: "daemon"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: `MCP setup (${mcpLabel})`,
|
|
373
|
+
value: "mcp"
|
|
300
374
|
}
|
|
301
375
|
],
|
|
302
376
|
opts?.exitLabel ?? "Back"
|
|
@@ -319,6 +393,9 @@ async function memoryConfigMenu(opts) {
|
|
|
319
393
|
case "daemon":
|
|
320
394
|
await editMemoryDaemonTimeout(globalCfg, localCfg);
|
|
321
395
|
break;
|
|
396
|
+
case "mcp":
|
|
397
|
+
await editMemoryMcp();
|
|
398
|
+
break;
|
|
322
399
|
}
|
|
323
400
|
}
|
|
324
401
|
}
|
|
@@ -329,5 +406,7 @@ export {
|
|
|
329
406
|
editMemoryAutoStart,
|
|
330
407
|
editMemoryEmbedding,
|
|
331
408
|
editMemoryDaemonTimeout,
|
|
409
|
+
writeMemoryMcpEntry,
|
|
410
|
+
getMemoryMcpStatus,
|
|
332
411
|
memoryConfigMenu
|
|
333
412
|
};
|
|
@@ -4,16 +4,17 @@ import {
|
|
|
4
4
|
editMemoryDaemonTimeout,
|
|
5
5
|
editMemoryEmbedding,
|
|
6
6
|
editMemoryTier,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
getMemoryMcpStatus,
|
|
8
|
+
memoryConfigMenu,
|
|
9
|
+
writeMemoryMcpEntry
|
|
10
|
+
} from "./chunk-LCABODSZ.js";
|
|
11
|
+
import {
|
|
12
|
+
getLibPath
|
|
13
|
+
} from "./chunk-RZRT7NGT.js";
|
|
9
14
|
import {
|
|
10
15
|
loadConfig,
|
|
11
16
|
resolveMemoryDir
|
|
12
17
|
} from "./chunk-SIQJPT47.js";
|
|
13
|
-
import {
|
|
14
|
-
getLibPath
|
|
15
|
-
} from "./chunk-RZRT7NGT.js";
|
|
16
|
-
import "./chunk-PRIH34UB.js";
|
|
17
18
|
import {
|
|
18
19
|
showConfigHint
|
|
19
20
|
} from "./chunk-BIX7AJU3.js";
|
|
@@ -62,7 +63,7 @@ function truncateError(text) {
|
|
|
62
63
|
const skipped = lines.length - 28;
|
|
63
64
|
return [...head, ` ... (${skipped} lines omitted) ...`, ...tail].join("\n");
|
|
64
65
|
}
|
|
65
|
-
function
|
|
66
|
+
function ensureMemoryBuilt(mcpDir) {
|
|
66
67
|
if (!existsSync(join(mcpDir, "node_modules"))) {
|
|
67
68
|
log.step("Installing memory server dependencies (one-time setup)...");
|
|
68
69
|
const result = runWithStderr("npm", ["install"], { cwd: mcpDir });
|
|
@@ -84,6 +85,55 @@ function ensureBuilt(mcpDir) {
|
|
|
84
85
|
log.success("Done.");
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
function printMemoryMcpConfig(serverPath, memoryDir) {
|
|
89
|
+
console.log(`Add this to your MCP client config:
|
|
90
|
+
|
|
91
|
+
--- Claude Code (.mcp.json in project root) ---
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"coding-friend-memory": {
|
|
96
|
+
"command": "node",
|
|
97
|
+
"args": ["${serverPath}", "${memoryDir}"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
--- Claude Desktop / Claude Chat (claude_desktop_config.json) ---
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"coding-friend-memory": {
|
|
107
|
+
"command": "node",
|
|
108
|
+
"args": ["${serverPath}", "${memoryDir}"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
--- Generic MCP client ---
|
|
114
|
+
|
|
115
|
+
Server command: node ${serverPath} ${memoryDir}
|
|
116
|
+
Transport: stdio
|
|
117
|
+
|
|
118
|
+
--- Available tools ---
|
|
119
|
+
|
|
120
|
+
memory_store Store a new memory
|
|
121
|
+
memory_search Search memories (keyword match)
|
|
122
|
+
memory_retrieve Get a specific memory by ID
|
|
123
|
+
memory_list List memories with filtering
|
|
124
|
+
memory_update Update existing memory
|
|
125
|
+
memory_delete Delete a memory
|
|
126
|
+
|
|
127
|
+
--- Resources ---
|
|
128
|
+
|
|
129
|
+
memory://index Browse all memories
|
|
130
|
+
memory://stats Storage statistics
|
|
131
|
+
`);
|
|
132
|
+
log.warn(
|
|
133
|
+
"Note: The memory path is project-specific. Use local .mcp.json (per project), not global ~/.claude/.mcp.json."
|
|
134
|
+
);
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
87
137
|
function formatUptime(seconds) {
|
|
88
138
|
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
89
139
|
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
@@ -93,7 +143,7 @@ async function memoryStatusCommand() {
|
|
|
93
143
|
const memoryDir = getMemoryDir();
|
|
94
144
|
const docCount = countMdFiles(memoryDir);
|
|
95
145
|
const mcpDir = getLibPath("cf-memory");
|
|
96
|
-
|
|
146
|
+
ensureMemoryBuilt(mcpDir);
|
|
97
147
|
const { isDaemonRunning, getDaemonInfo } = await import(join(mcpDir, "dist/daemon/process.js"));
|
|
98
148
|
const { areSqliteDepsAvailable } = await import(join(mcpDir, "dist/lib/lazy-install.js"));
|
|
99
149
|
const sqliteAvailable = areSqliteDepsAvailable();
|
|
@@ -140,6 +190,20 @@ async function memoryStatusCommand() {
|
|
|
140
190
|
const model = embeddingConfig.model ?? (provider === "ollama" ? "all-minilm:l6-v2" : "Xenova/all-MiniLM-L6-v2");
|
|
141
191
|
log.info(`Embedding: ${chalk.cyan(model)} ${chalk.dim(`(${provider})`)}`);
|
|
142
192
|
}
|
|
193
|
+
const mcpStatus = getMemoryMcpStatus();
|
|
194
|
+
if (mcpStatus.configured && mcpStatus.scope === "local") {
|
|
195
|
+
log.info(
|
|
196
|
+
`MCP: ${chalk.green("configured")} ${chalk.dim("(local .mcp.json)")}`
|
|
197
|
+
);
|
|
198
|
+
} else if (mcpStatus.configured && mcpStatus.scope === "global") {
|
|
199
|
+
log.info(
|
|
200
|
+
`MCP: ${chalk.green("configured")} ${chalk.dim("(global ~/.claude/.mcp.json)")} ${chalk.yellow("\u26A0 global config uses a fixed path \u2014 only works for one project")}`
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
log.info(
|
|
204
|
+
`MCP: ${chalk.dim("not configured")} ${chalk.dim('(run "cf memory init" or add manually via "cf memory mcp")')}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
143
207
|
const autoCapture = config.memory?.autoCapture ?? false;
|
|
144
208
|
log.info(
|
|
145
209
|
`Auto-capture: ${autoCapture ? chalk.green("on") : chalk.dim("off")}`
|
|
@@ -165,7 +229,7 @@ async function memorySearchCommand(query) {
|
|
|
165
229
|
process.exit(1);
|
|
166
230
|
}
|
|
167
231
|
const mcpDir = getLibPath("cf-memory");
|
|
168
|
-
|
|
232
|
+
ensureMemoryBuilt(mcpDir);
|
|
169
233
|
const result = run(
|
|
170
234
|
"node",
|
|
171
235
|
[
|
|
@@ -205,7 +269,7 @@ async function memoryListCommand(opts) {
|
|
|
205
269
|
return;
|
|
206
270
|
}
|
|
207
271
|
const mcpDir = getLibPath("cf-memory");
|
|
208
|
-
|
|
272
|
+
ensureMemoryBuilt(mcpDir);
|
|
209
273
|
const result = run(
|
|
210
274
|
"node",
|
|
211
275
|
[
|
|
@@ -246,7 +310,7 @@ async function memoryListCommand(opts) {
|
|
|
246
310
|
async function memoryStartDaemonCommand() {
|
|
247
311
|
const memoryDir = getMemoryDir();
|
|
248
312
|
const mcpDir = getLibPath("cf-memory");
|
|
249
|
-
|
|
313
|
+
ensureMemoryBuilt(mcpDir);
|
|
250
314
|
const { isDaemonRunning, getDaemonInfo, spawnDaemon } = await import(join(mcpDir, "dist/daemon/process.js"));
|
|
251
315
|
if (await isDaemonRunning()) {
|
|
252
316
|
const info = getDaemonInfo();
|
|
@@ -256,7 +320,8 @@ async function memoryStartDaemonCommand() {
|
|
|
256
320
|
log.step("Starting memory daemon...");
|
|
257
321
|
const config = loadConfig();
|
|
258
322
|
const embedding = config.memory?.embedding;
|
|
259
|
-
const
|
|
323
|
+
const idleTimeoutMs = config.memory?.daemon?.idleTimeout;
|
|
324
|
+
const result = await spawnDaemon(memoryDir, embedding, { idleTimeoutMs });
|
|
260
325
|
if (result) {
|
|
261
326
|
log.success(`Daemon started (PID ${result.pid})`);
|
|
262
327
|
log.info(`Watching ${chalk.cyan(memoryDir)} for changes`);
|
|
@@ -267,7 +332,7 @@ async function memoryStartDaemonCommand() {
|
|
|
267
332
|
}
|
|
268
333
|
async function memoryStopDaemonCommand() {
|
|
269
334
|
const mcpDir = getLibPath("cf-memory");
|
|
270
|
-
|
|
335
|
+
ensureMemoryBuilt(mcpDir);
|
|
271
336
|
const { stopDaemon, isDaemonRunning } = await import(join(mcpDir, "dist/daemon/process.js"));
|
|
272
337
|
if (!await isDaemonRunning()) {
|
|
273
338
|
log.info("Daemon is not running.");
|
|
@@ -284,7 +349,7 @@ async function memoryStopDaemonCommand() {
|
|
|
284
349
|
async function memoryRebuildCommand() {
|
|
285
350
|
const memoryDir = getMemoryDir();
|
|
286
351
|
const mcpDir = getLibPath("cf-memory");
|
|
287
|
-
|
|
352
|
+
ensureMemoryBuilt(mcpDir);
|
|
288
353
|
const { areSqliteDepsAvailable } = await import(join(mcpDir, "dist/lib/lazy-install.js"));
|
|
289
354
|
if (areSqliteDepsAvailable()) {
|
|
290
355
|
log.step("Rebuilding SQLite index + embeddings...");
|
|
@@ -346,10 +411,61 @@ function getDbPath(memoryDir) {
|
|
|
346
411
|
}
|
|
347
412
|
}
|
|
348
413
|
var em = chalk.hex("#10b981");
|
|
414
|
+
async function ensureSqliteDepsIfNeeded(mcpDir) {
|
|
415
|
+
const config = loadConfig();
|
|
416
|
+
const tier = config.memory?.tier ?? "auto";
|
|
417
|
+
if (tier === "markdown" || tier === "lite") return true;
|
|
418
|
+
const { ensureDeps, areSqliteDepsAvailable } = await import(join(mcpDir, "dist/lib/lazy-install.js"));
|
|
419
|
+
if (areSqliteDepsAvailable()) return true;
|
|
420
|
+
log.step("Installing SQLite dependencies...");
|
|
421
|
+
const installed = await ensureDeps({
|
|
422
|
+
onProgress: (msg) => log.step(msg)
|
|
423
|
+
});
|
|
424
|
+
if (!installed) {
|
|
425
|
+
log.error("Failed to install SQLite dependencies.");
|
|
426
|
+
log.dim(
|
|
427
|
+
"Ensure you have a C++ compiler installed (Xcode CLT on macOS, build-essential on Linux)."
|
|
428
|
+
);
|
|
429
|
+
log.dim(
|
|
430
|
+
'Memory will fall back to a lower tier. You can retry later with "cf memory init".'
|
|
431
|
+
);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
log.success("Dependencies installed.");
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
async function setupMemoryMcp(memoryDir, mcpDir) {
|
|
438
|
+
const mcpStatus = getMemoryMcpStatus();
|
|
439
|
+
if (mcpStatus.configured && mcpStatus.scope === "local") {
|
|
440
|
+
log.info(`MCP: ${chalk.green("already configured")} in .mcp.json`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
console.log();
|
|
444
|
+
log.step("MCP setup");
|
|
445
|
+
log.dim(
|
|
446
|
+
"The Memory MCP connects Claude Code to the memory system so skills can store and search memories."
|
|
447
|
+
);
|
|
448
|
+
const addMcp = await confirm({
|
|
449
|
+
message: "Add coding-friend-memory to .mcp.json?",
|
|
450
|
+
default: true
|
|
451
|
+
});
|
|
452
|
+
if (!addMcp) {
|
|
453
|
+
log.dim('Skipped. Run "cf memory mcp" anytime to get the config.');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const serverPath = join(mcpDir, "dist", "index.js");
|
|
457
|
+
if (!existsSync(serverPath)) {
|
|
458
|
+
log.warn(
|
|
459
|
+
"cf-memory not built yet. Run `cf memory mcp` after building to get the config."
|
|
460
|
+
);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
writeMemoryMcpEntry(serverPath, memoryDir);
|
|
464
|
+
}
|
|
349
465
|
async function memoryInitCommand() {
|
|
350
466
|
const memoryDir = getMemoryDir();
|
|
351
467
|
const mcpDir = getLibPath("cf-memory");
|
|
352
|
-
|
|
468
|
+
ensureMemoryBuilt(mcpDir);
|
|
353
469
|
const dbExists = getDbPath(memoryDir) !== null;
|
|
354
470
|
if (dbExists) {
|
|
355
471
|
console.log();
|
|
@@ -359,6 +475,7 @@ async function memoryInitCommand() {
|
|
|
359
475
|
log.dim('To re-import memories, run "cf memory rebuild".');
|
|
360
476
|
console.log();
|
|
361
477
|
await memoryConfigMenu({ exitLabel: "Done" });
|
|
478
|
+
await ensureSqliteDepsIfNeeded(mcpDir);
|
|
362
479
|
return;
|
|
363
480
|
}
|
|
364
481
|
console.log();
|
|
@@ -403,47 +520,33 @@ async function memoryInitCommand() {
|
|
|
403
520
|
const config = loadConfig();
|
|
404
521
|
const tier = config.memory?.tier ?? "auto";
|
|
405
522
|
if (tier === "markdown") {
|
|
523
|
+
await setupMemoryMcp(memoryDir, mcpDir);
|
|
406
524
|
log.success(
|
|
407
525
|
'Memory initialized with Tier 3 (markdown). Run "cf memory status" to verify.'
|
|
408
526
|
);
|
|
409
527
|
return;
|
|
410
528
|
}
|
|
411
529
|
if (tier === "lite") {
|
|
530
|
+
await setupMemoryMcp(memoryDir, mcpDir);
|
|
412
531
|
log.success(
|
|
413
532
|
'Memory initialized. Run "cf memory start-daemon" to enable Tier 2 search.'
|
|
414
533
|
);
|
|
415
534
|
return;
|
|
416
535
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (areSqliteDepsAvailable()) {
|
|
420
|
-
log.info("SQLite dependencies already installed.");
|
|
421
|
-
} else {
|
|
422
|
-
const installed = await ensureDeps({
|
|
423
|
-
onProgress: (msg) => log.step(msg)
|
|
424
|
-
});
|
|
425
|
-
if (!installed) {
|
|
426
|
-
log.error("Failed to install SQLite dependencies.");
|
|
427
|
-
log.dim(
|
|
428
|
-
"Ensure you have a C++ compiler installed (Xcode CLT on macOS, build-essential on Linux)."
|
|
429
|
-
);
|
|
430
|
-
log.dim(
|
|
431
|
-
'Memory will fall back to a lower tier. You can retry later with "cf memory init".'
|
|
432
|
-
);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
log.success("Dependencies installed.");
|
|
436
|
-
}
|
|
536
|
+
const depsOk = await ensureSqliteDepsIfNeeded(mcpDir);
|
|
537
|
+
if (!depsOk) return;
|
|
437
538
|
if (!existsSync(memoryDir)) {
|
|
438
539
|
log.info(
|
|
439
540
|
"No memory directory found. Memories will be indexed as they're created."
|
|
440
541
|
);
|
|
542
|
+
await setupMemoryMcp(memoryDir, mcpDir);
|
|
441
543
|
log.success('Memory initialized. Run "cf memory status" to verify.');
|
|
442
544
|
return;
|
|
443
545
|
}
|
|
444
546
|
const docCount = countMdFiles(memoryDir);
|
|
445
547
|
if (docCount === 0) {
|
|
446
548
|
log.info("No existing memories to import.");
|
|
549
|
+
await setupMemoryMcp(memoryDir, mcpDir);
|
|
447
550
|
log.success('Memory initialized. Run "cf memory status" to verify.');
|
|
448
551
|
return;
|
|
449
552
|
}
|
|
@@ -464,6 +567,7 @@ async function memoryInitCommand() {
|
|
|
464
567
|
} finally {
|
|
465
568
|
await backend.close();
|
|
466
569
|
}
|
|
570
|
+
await setupMemoryMcp(memoryDir, mcpDir);
|
|
467
571
|
console.log();
|
|
468
572
|
log.success('Memory initialized! Run "cf memory status" to verify.');
|
|
469
573
|
log.info(
|
|
@@ -563,7 +667,7 @@ function getProjectInfo(projectDir, projectId, mcpDir, knownSourceDir) {
|
|
|
563
667
|
async function memoryListProjectsCommand() {
|
|
564
668
|
const baseDir = getProjectsBaseDir();
|
|
565
669
|
const mcpDir = getLibPath("cf-memory");
|
|
566
|
-
|
|
670
|
+
ensureMemoryBuilt(mcpDir);
|
|
567
671
|
if (!existsSync(baseDir)) {
|
|
568
672
|
log.info("No memory projects found.");
|
|
569
673
|
log.dim('Run "cf memory init" in a project to create one.');
|
|
@@ -617,7 +721,7 @@ async function memoryRmCommand(opts) {
|
|
|
617
721
|
}
|
|
618
722
|
if (opts.prune) {
|
|
619
723
|
const mcpDir = getLibPath("cf-memory");
|
|
620
|
-
|
|
724
|
+
ensureMemoryBuilt(mcpDir);
|
|
621
725
|
const dirs = readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name);
|
|
622
726
|
const orphaned = [];
|
|
623
727
|
for (const id of dirs) {
|
|
@@ -734,50 +838,22 @@ async function memoryRmCommand(opts) {
|
|
|
734
838
|
async function memoryMcpCommand() {
|
|
735
839
|
const memoryDir = getMemoryDir();
|
|
736
840
|
const mcpDir = getLibPath("cf-memory");
|
|
737
|
-
|
|
841
|
+
ensureMemoryBuilt(mcpDir);
|
|
738
842
|
const serverPath = join(mcpDir, "dist", "index.js");
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
--- Claude Desktop / Claude Chat (claude_desktop_config.json) ---
|
|
742
|
-
|
|
743
|
-
{
|
|
744
|
-
"mcpServers": {
|
|
745
|
-
"coding-friend-memory": {
|
|
746
|
-
"command": "node",
|
|
747
|
-
"args": ["${serverPath}", "${memoryDir}"]
|
|
748
|
-
}
|
|
749
|
-
}
|
|
843
|
+
printMemoryMcpConfig(serverPath, memoryDir);
|
|
750
844
|
}
|
|
751
845
|
|
|
752
|
-
--- Generic MCP client ---
|
|
753
|
-
|
|
754
|
-
Server command: node ${serverPath} ${memoryDir}
|
|
755
|
-
Transport: stdio
|
|
756
|
-
|
|
757
|
-
--- Available tools ---
|
|
758
|
-
|
|
759
|
-
memory_store Store a new memory
|
|
760
|
-
memory_search Search memories (keyword match)
|
|
761
|
-
memory_retrieve Get a specific memory by ID
|
|
762
|
-
memory_list List memories with filtering
|
|
763
|
-
memory_update Update existing memory
|
|
764
|
-
memory_delete Delete a memory
|
|
765
|
-
|
|
766
|
-
--- Resources ---
|
|
767
|
-
|
|
768
|
-
memory://index Browse all memories
|
|
769
|
-
memory://stats Storage statistics
|
|
770
|
-
`);
|
|
771
|
-
}
|
|
772
846
|
export {
|
|
773
|
-
|
|
774
|
-
|
|
847
|
+
ensureMemoryBuilt,
|
|
848
|
+
printMemoryMcpConfig,
|
|
849
|
+
memoryStatusCommand,
|
|
850
|
+
memorySearchCommand,
|
|
775
851
|
memoryListCommand,
|
|
776
|
-
|
|
852
|
+
memoryStartDaemonCommand,
|
|
853
|
+
memoryStopDaemonCommand,
|
|
777
854
|
memoryRebuildCommand,
|
|
855
|
+
memoryInitCommand,
|
|
856
|
+
memoryConfigCommand,
|
|
778
857
|
memoryRmCommand,
|
|
779
|
-
|
|
780
|
-
memoryStartDaemonCommand,
|
|
781
|
-
memoryStatusCommand,
|
|
782
|
-
memoryStopDaemonCommand
|
|
858
|
+
memoryMcpCommand
|
|
783
859
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
memoryConfigMenu
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LCABODSZ.js";
|
|
4
4
|
import {
|
|
5
5
|
getAllRules,
|
|
6
6
|
getExistingRules
|
|
7
7
|
} from "./chunk-HEBLANJO.js";
|
|
8
8
|
import "./chunk-RZRT7NGT.js";
|
|
9
|
+
import "./chunk-SIQJPT47.js";
|
|
9
10
|
import {
|
|
10
11
|
findStatuslineHookPath,
|
|
11
12
|
isStatuslineConfigured,
|
package/dist/index.js
CHANGED
|
@@ -30,19 +30,19 @@ program.command("enable").description("Re-enable the Coding Friend plugin").opti
|
|
|
30
30
|
await enableCommand(opts);
|
|
31
31
|
});
|
|
32
32
|
program.command("init").description("Initialize coding-friend in current project").action(async () => {
|
|
33
|
-
const { initCommand } = await import("./init-
|
|
33
|
+
const { initCommand } = await import("./init-FMPOE2MN.js");
|
|
34
34
|
await initCommand();
|
|
35
35
|
});
|
|
36
36
|
program.command("config").description("Manage Coding Friend configuration").action(async () => {
|
|
37
|
-
const { configCommand } = await import("./config-
|
|
37
|
+
const { configCommand } = await import("./config-K2S23A5R.js");
|
|
38
38
|
await configCommand();
|
|
39
39
|
});
|
|
40
40
|
program.command("host").description("Build and serve learning docs as a static website").argument("[path]", "path to docs folder").option("-p, --port <port>", "port number", "3333").action(async (path, opts) => {
|
|
41
|
-
const { hostCommand } = await import("./host-
|
|
41
|
+
const { hostCommand } = await import("./host-E644BDZK.js");
|
|
42
42
|
await hostCommand(path, opts);
|
|
43
43
|
});
|
|
44
44
|
program.command("mcp").description("Setup MCP server for learning docs").argument("[path]", "path to docs folder").action(async (path) => {
|
|
45
|
-
const { mcpCommand } = await import("./mcp-
|
|
45
|
+
const { mcpCommand } = await import("./mcp-SQWZXF5Q.js");
|
|
46
46
|
await mcpCommand(path);
|
|
47
47
|
});
|
|
48
48
|
program.command("permission").description("Manage Claude Code permission rules for Coding Friend").option("--all", "Apply all recommended permissions without prompts").option("--user", "Save to user-level settings (~/.claude/settings.json)").option(
|
|
@@ -61,7 +61,7 @@ program.command("update").description("Update coding-friend plugin, CLI, and sta
|
|
|
61
61
|
await updateCommand(opts);
|
|
62
62
|
});
|
|
63
63
|
program.command("status").description("Show comprehensive Coding Friend status").action(async () => {
|
|
64
|
-
const { statusCommand } = await import("./status-
|
|
64
|
+
const { statusCommand } = await import("./status-CGYNNMYF.js");
|
|
65
65
|
await statusCommand();
|
|
66
66
|
});
|
|
67
67
|
var session = program.command("session").description("Save and load Claude Code sessions across machines");
|
|
@@ -100,43 +100,43 @@ Memory subcommands:
|
|
|
100
100
|
memory mcp Show MCP server setup instructions`
|
|
101
101
|
);
|
|
102
102
|
memory.command("status").description("Show memory system status").action(async () => {
|
|
103
|
-
const { memoryStatusCommand } = await import("./memory-
|
|
103
|
+
const { memoryStatusCommand } = await import("./memory-7WLVIFAZ.js");
|
|
104
104
|
await memoryStatusCommand();
|
|
105
105
|
});
|
|
106
106
|
memory.command("search").description("Search memories by query").argument("<query>", "search query").action(async (query) => {
|
|
107
|
-
const { memorySearchCommand } = await import("./memory-
|
|
107
|
+
const { memorySearchCommand } = await import("./memory-7WLVIFAZ.js");
|
|
108
108
|
await memorySearchCommand(query);
|
|
109
109
|
});
|
|
110
110
|
memory.command("list").description(
|
|
111
111
|
"List memories in current project, or all projects with --projects"
|
|
112
112
|
).option("--projects", "List all project databases with size and metadata").action(async (opts) => {
|
|
113
|
-
const { memoryListCommand } = await import("./memory-
|
|
113
|
+
const { memoryListCommand } = await import("./memory-7WLVIFAZ.js");
|
|
114
114
|
await memoryListCommand(opts);
|
|
115
115
|
});
|
|
116
116
|
memory.command("init").description(
|
|
117
117
|
"Initialize memory system \u2014 interactive wizard (first time) or config menu"
|
|
118
118
|
).action(async () => {
|
|
119
|
-
const { memoryInitCommand } = await import("./memory-
|
|
119
|
+
const { memoryInitCommand } = await import("./memory-7WLVIFAZ.js");
|
|
120
120
|
await memoryInitCommand();
|
|
121
121
|
});
|
|
122
122
|
memory.command("config").description("Configure memory system settings").action(async () => {
|
|
123
|
-
const { memoryConfigCommand } = await import("./memory-
|
|
123
|
+
const { memoryConfigCommand } = await import("./memory-7WLVIFAZ.js");
|
|
124
124
|
await memoryConfigCommand();
|
|
125
125
|
});
|
|
126
126
|
memory.command("start-daemon").description("Start the memory daemon (Tier 2 \u2014 MiniSearch)").action(async () => {
|
|
127
|
-
const { memoryStartDaemonCommand } = await import("./memory-
|
|
127
|
+
const { memoryStartDaemonCommand } = await import("./memory-7WLVIFAZ.js");
|
|
128
128
|
await memoryStartDaemonCommand();
|
|
129
129
|
});
|
|
130
130
|
memory.command("stop-daemon").description("Stop the memory daemon").action(async () => {
|
|
131
|
-
const { memoryStopDaemonCommand } = await import("./memory-
|
|
131
|
+
const { memoryStopDaemonCommand } = await import("./memory-7WLVIFAZ.js");
|
|
132
132
|
await memoryStopDaemonCommand();
|
|
133
133
|
});
|
|
134
134
|
memory.command("rebuild").description("Rebuild the daemon search index").action(async () => {
|
|
135
|
-
const { memoryRebuildCommand } = await import("./memory-
|
|
135
|
+
const { memoryRebuildCommand } = await import("./memory-7WLVIFAZ.js");
|
|
136
136
|
await memoryRebuildCommand();
|
|
137
137
|
});
|
|
138
138
|
memory.command("mcp").description("Show MCP server setup instructions").action(async () => {
|
|
139
|
-
const { memoryMcpCommand } = await import("./memory-
|
|
139
|
+
const { memoryMcpCommand } = await import("./memory-7WLVIFAZ.js");
|
|
140
140
|
await memoryMcpCommand();
|
|
141
141
|
});
|
|
142
142
|
memory.command("rm").description("Remove a project database").option("--project-id <id>", "Project ID to remove").option("--all", "Remove all project databases").option(
|
|
@@ -144,7 +144,7 @@ memory.command("rm").description("Remove a project database").option("--project-
|
|
|
144
144
|
"Remove orphaned projects (source dir missing or 0 memories)"
|
|
145
145
|
).action(
|
|
146
146
|
async (opts) => {
|
|
147
|
-
const { memoryRmCommand } = await import("./memory-
|
|
147
|
+
const { memoryRmCommand } = await import("./memory-7WLVIFAZ.js");
|
|
148
148
|
await memoryRmCommand(opts);
|
|
149
149
|
}
|
|
150
150
|
);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getMemoryMcpStatus,
|
|
3
|
+
writeMemoryMcpEntry
|
|
4
|
+
} from "./chunk-LCABODSZ.js";
|
|
1
5
|
import {
|
|
2
6
|
applyPermissions,
|
|
3
7
|
buildLearnDirRules,
|
|
@@ -10,6 +14,7 @@ import {
|
|
|
10
14
|
import {
|
|
11
15
|
getLibPath
|
|
12
16
|
} from "./chunk-RZRT7NGT.js";
|
|
17
|
+
import "./chunk-SIQJPT47.js";
|
|
13
18
|
import {
|
|
14
19
|
findStatuslineHookPath,
|
|
15
20
|
isStatuslineConfigured,
|
|
@@ -587,15 +592,9 @@ async function stepStatusline() {
|
|
|
587
592
|
writeStatuslineSettings(hookResult.hookPath);
|
|
588
593
|
log.success("Statusline configured!");
|
|
589
594
|
}
|
|
590
|
-
function isMemoryMcpConfigured() {
|
|
591
|
-
const mcpPath = join(process.cwd(), ".mcp.json");
|
|
592
|
-
const config = readJson(mcpPath);
|
|
593
|
-
if (!config) return false;
|
|
594
|
-
const servers = config.mcpServers;
|
|
595
|
-
return servers != null && "coding-friend-memory" in servers;
|
|
596
|
-
}
|
|
597
595
|
async function stepMemory(docsDir) {
|
|
598
|
-
|
|
596
|
+
const mcpStatus = getMemoryMcpStatus();
|
|
597
|
+
if (mcpStatus.configured) {
|
|
599
598
|
printStepHeader(
|
|
600
599
|
`CF Memory MCP ${chalk.green("[done]")}`,
|
|
601
600
|
"Connects the memory system to Claude Code via MCP."
|
|
@@ -642,20 +641,7 @@ async function stepMemory(docsDir) {
|
|
|
642
641
|
return;
|
|
643
642
|
}
|
|
644
643
|
const memoryDir = join(process.cwd(), docsDir, "memory");
|
|
645
|
-
|
|
646
|
-
const existing = readJson(mcpPath) ?? {};
|
|
647
|
-
const servers = existing.mcpServers ?? {};
|
|
648
|
-
writeJson(mcpPath, {
|
|
649
|
-
...existing,
|
|
650
|
-
mcpServers: {
|
|
651
|
-
...servers,
|
|
652
|
-
"coding-friend-memory": {
|
|
653
|
-
command: "node",
|
|
654
|
-
args: [serverPath, memoryDir]
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
log.success(`Added coding-friend-memory to .mcp.json`);
|
|
644
|
+
writeMemoryMcpEntry(serverPath, memoryDir);
|
|
659
645
|
const autoCapture = await confirm({
|
|
660
646
|
message: "Enable auto-capture? (saves session summaries to memory before context compaction)",
|
|
661
647
|
default: false
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
ensureMemoryBuilt,
|
|
3
|
+
printMemoryMcpConfig
|
|
4
|
+
} from "./chunk-TPLH46BH.js";
|
|
5
|
+
import "./chunk-LCABODSZ.js";
|
|
4
6
|
import {
|
|
5
7
|
getLibPath
|
|
6
8
|
} from "./chunk-RZRT7NGT.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveDocsDir,
|
|
11
|
+
resolveMemoryDir
|
|
12
|
+
} from "./chunk-SIQJPT47.js";
|
|
7
13
|
import "./chunk-PRIH34UB.js";
|
|
14
|
+
import "./chunk-BIX7AJU3.js";
|
|
8
15
|
import {
|
|
9
16
|
run
|
|
10
17
|
} from "./chunk-EVGXUDX4.js";
|
|
@@ -43,7 +50,7 @@ async function mcpCommand(path) {
|
|
|
43
50
|
log.dim("Run /cf-learn first to generate some docs.");
|
|
44
51
|
process.exit(1);
|
|
45
52
|
}
|
|
46
|
-
console.log("=== \u{
|
|
53
|
+
console.log(chalk.green.bold("=== \u{1F4DA} Learn MCP ==="));
|
|
47
54
|
log.info(`Docs folder: ${chalk.cyan(docsDir)}`);
|
|
48
55
|
log.info(`Found: ${chalk.green(docCount)} docs`);
|
|
49
56
|
console.log();
|
|
@@ -100,6 +107,26 @@ Write:
|
|
|
100
107
|
improve-doc Get improvement suggestions
|
|
101
108
|
track-knowledge Record understanding level (remembered/needs-review/new)
|
|
102
109
|
`);
|
|
110
|
+
printMemoryMcp();
|
|
111
|
+
}
|
|
112
|
+
function printMemoryMcp() {
|
|
113
|
+
const memoryDir = resolveMemoryDir();
|
|
114
|
+
let mcpDir;
|
|
115
|
+
try {
|
|
116
|
+
mcpDir = getLibPath("cf-memory");
|
|
117
|
+
} catch {
|
|
118
|
+
log.dim(
|
|
119
|
+
'Memory MCP: cf-memory package not found. Run "cf memory init" to set it up.'
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
ensureMemoryBuilt(mcpDir);
|
|
124
|
+
const serverPath = join(mcpDir, "dist", "index.js");
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(chalk.magenta.bold("=== \u{1F9E0} Memory MCP ==="));
|
|
127
|
+
log.info(`Memory dir: ${chalk.cyan(memoryDir)}`);
|
|
128
|
+
console.log();
|
|
129
|
+
printMemoryMcpConfig(serverPath, memoryDir);
|
|
103
130
|
}
|
|
104
131
|
export {
|
|
105
132
|
mcpCommand
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureMemoryBuilt,
|
|
3
|
+
memoryConfigCommand,
|
|
4
|
+
memoryInitCommand,
|
|
5
|
+
memoryListCommand,
|
|
6
|
+
memoryMcpCommand,
|
|
7
|
+
memoryRebuildCommand,
|
|
8
|
+
memoryRmCommand,
|
|
9
|
+
memorySearchCommand,
|
|
10
|
+
memoryStartDaemonCommand,
|
|
11
|
+
memoryStatusCommand,
|
|
12
|
+
memoryStopDaemonCommand,
|
|
13
|
+
printMemoryMcpConfig
|
|
14
|
+
} from "./chunk-TPLH46BH.js";
|
|
15
|
+
import "./chunk-LCABODSZ.js";
|
|
16
|
+
import "./chunk-RZRT7NGT.js";
|
|
17
|
+
import "./chunk-SIQJPT47.js";
|
|
18
|
+
import "./chunk-PRIH34UB.js";
|
|
19
|
+
import "./chunk-BIX7AJU3.js";
|
|
20
|
+
import "./chunk-EVGXUDX4.js";
|
|
21
|
+
import "./chunk-5UVDWG5L.js";
|
|
22
|
+
import "./chunk-DO6FV6BL.js";
|
|
23
|
+
import "./chunk-W5CD7WTX.js";
|
|
24
|
+
export {
|
|
25
|
+
ensureMemoryBuilt,
|
|
26
|
+
memoryConfigCommand,
|
|
27
|
+
memoryInitCommand,
|
|
28
|
+
memoryListCommand,
|
|
29
|
+
memoryMcpCommand,
|
|
30
|
+
memoryRebuildCommand,
|
|
31
|
+
memoryRmCommand,
|
|
32
|
+
memorySearchCommand,
|
|
33
|
+
memoryStartDaemonCommand,
|
|
34
|
+
memoryStatusCommand,
|
|
35
|
+
memoryStopDaemonCommand,
|
|
36
|
+
printMemoryMcpConfig
|
|
37
|
+
};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# CF Memory Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.2 (2026-03-25)
|
|
4
|
+
|
|
5
|
+
- Add NaN guard for `MEMORY_DAEMON_IDLE_TIMEOUT` env var to prevent invalid timeout values [#d4401fa](https://github.com/dinhanhthi/coding-friend/commit/d4401fa)
|
|
6
|
+
- Fix `ping()` to use raw request instead of triggering daemon respawn during tier detection [#d4401fa](https://github.com/dinhanhthi/coding-friend/commit/d4401fa)
|
|
7
|
+
- Pass `daemonOptions` consistently in index.ts instead of inline object [#d4401fa](https://github.com/dinhanhthi/coding-friend/commit/d4401fa)
|
|
8
|
+
|
|
3
9
|
## v0.2.1 (2026-03-22)
|
|
4
10
|
|
|
5
11
|
- Fix flaky tier detection tests — mock `isDaemonRunning` to prevent local daemon from affecting test results [#d786f08](https://github.com/dinhanhthi/coding-friend/commit/d786f08)
|
package/lib/cf-memory/README.md
CHANGED
|
@@ -229,6 +229,8 @@ node dist/daemon/entry.js ./docs/memory 1800000 --tier=lite
|
|
|
229
229
|
|
|
230
230
|
The daemon runs a Hono HTTP server on a Unix Domain Socket at `~/.coding-friend/memory/daemon.sock`, with PID tracking at `~/.coding-friend/memory/daemon.pid`.
|
|
231
231
|
|
|
232
|
+
**Auto-reconnect:** If the daemon dies (e.g., idle timeout expires), the `DaemonClient` automatically respawns it on the next request. This means mid-session daemon restarts are transparent — no manual intervention needed.
|
|
233
|
+
|
|
232
234
|
### Lazy dependencies (Tier 1)
|
|
233
235
|
|
|
234
236
|
Heavy dependencies for Tier 1 (SQLite + embeddings) are installed on-demand into `~/.coding-friend/memory/node_modules/`, not in this package's `node_modules/`. These include:
|
|
@@ -302,6 +304,7 @@ If these are missing, `cf memory init` will fail at the "Installing SQLite depen
|
|
|
302
304
|
| ----------------------------- | ------------------------ | ------------------------------------------------- |
|
|
303
305
|
| `MEMORY_DOCS_DIR` | `./docs/memory` | Path to memory storage directory |
|
|
304
306
|
| `MEMORY_TIER` | `auto` | Force a tier: `auto`, `full`, `lite`, `markdown` |
|
|
307
|
+
| `MEMORY_DAEMON_IDLE_TIMEOUT` | `1800000` (30 min) | Daemon idle timeout in ms (`0` = never auto-stop) |
|
|
305
308
|
| `MEMORY_EMBEDDING_PROVIDER` | `transformers` | Embedding provider: `transformers` or `ollama` |
|
|
306
309
|
| `MEMORY_EMBEDDING_MODEL` | (provider default) | Embedding model name (e.g., `nomic-embed-text`) |
|
|
307
310
|
| `MEMORY_EMBEDDING_OLLAMA_URL` | `http://localhost:11434` | Ollama server URL |
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { DaemonClient } from "../lib/daemon-client.js";
|
|
6
|
+
import { startDaemonServer, type DaemonPaths } from "../daemon/process.js";
|
|
7
|
+
import { MiniSearchBackend } from "../backends/minisearch.js";
|
|
8
|
+
|
|
9
|
+
let testDir: string;
|
|
10
|
+
let testPaths: DaemonPaths;
|
|
11
|
+
let counter = 0;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
testDir = join(tmpdir(), `cf-daemon-client-test-${Date.now()}-${++counter}`);
|
|
15
|
+
mkdirSync(testDir, { recursive: true });
|
|
16
|
+
|
|
17
|
+
const daemonDir = join(testDir, "daemon");
|
|
18
|
+
mkdirSync(daemonDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
testPaths = {
|
|
21
|
+
socketPath: join(daemonDir, "daemon.sock"),
|
|
22
|
+
pidFile: join(daemonDir, "daemon.pid"),
|
|
23
|
+
logFile: join(daemonDir, "daemon.log"),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("DaemonClient auto-reconnect", () => {
|
|
32
|
+
it("calls respawn and retries on connection error", async () => {
|
|
33
|
+
const docsDir = join(testDir, "docs");
|
|
34
|
+
mkdirSync(docsDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Start daemon
|
|
37
|
+
const backend = new MiniSearchBackend(docsDir);
|
|
38
|
+
let handle = startDaemonServer(backend, {
|
|
39
|
+
paths: testPaths,
|
|
40
|
+
idleTimeoutMs: 0,
|
|
41
|
+
});
|
|
42
|
+
await new Promise<void>((resolve) => {
|
|
43
|
+
handle.server.once("listening", resolve);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const respawn = vi.fn(async () => {
|
|
47
|
+
// Re-create a fresh backend and server on same paths
|
|
48
|
+
const newBackend = new MiniSearchBackend(docsDir);
|
|
49
|
+
handle = startDaemonServer(newBackend, {
|
|
50
|
+
paths: testPaths,
|
|
51
|
+
idleTimeoutMs: 0,
|
|
52
|
+
});
|
|
53
|
+
await new Promise<void>((resolve) => {
|
|
54
|
+
handle.server.once("listening", resolve);
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const client = new DaemonClient(testPaths.socketPath, { respawn });
|
|
60
|
+
|
|
61
|
+
// Verify initial connection works
|
|
62
|
+
const alive = await client.ping();
|
|
63
|
+
expect(alive).toBe(true);
|
|
64
|
+
|
|
65
|
+
// Kill daemon and wait for socket cleanup
|
|
66
|
+
await new Promise<void>((resolve) => {
|
|
67
|
+
handle.server.close(() => resolve());
|
|
68
|
+
});
|
|
69
|
+
rmSync(testPaths.socketPath, { force: true });
|
|
70
|
+
rmSync(testPaths.pidFile, { force: true });
|
|
71
|
+
// Small delay to ensure OS fully releases the socket
|
|
72
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
73
|
+
|
|
74
|
+
// Next request should trigger respawn and succeed
|
|
75
|
+
const stats = await client.stats();
|
|
76
|
+
expect(stats.total).toBe(0);
|
|
77
|
+
expect(respawn).toHaveBeenCalledOnce();
|
|
78
|
+
|
|
79
|
+
// Cleanup
|
|
80
|
+
await new Promise<void>((resolve) => {
|
|
81
|
+
handle.server.close(() => resolve());
|
|
82
|
+
});
|
|
83
|
+
rmSync(testPaths.socketPath, { force: true });
|
|
84
|
+
rmSync(testPaths.pidFile, { force: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("throws original error when respawn fails", async () => {
|
|
88
|
+
const respawn = vi.fn(async () => false);
|
|
89
|
+
const client = new DaemonClient(
|
|
90
|
+
"/tmp/nonexistent-cf-test-sock-" + Date.now(),
|
|
91
|
+
{ respawn },
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await expect(client.stats()).rejects.toThrow();
|
|
95
|
+
expect(respawn).toHaveBeenCalledOnce();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("throws directly without respawn callback", async () => {
|
|
99
|
+
const client = new DaemonClient(
|
|
100
|
+
"/tmp/nonexistent-cf-test-sock-" + Date.now(),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await expect(client.stats()).rejects.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("ping does not trigger respawn", async () => {
|
|
107
|
+
const respawn = vi.fn(async () => true);
|
|
108
|
+
const client = new DaemonClient(
|
|
109
|
+
"/tmp/nonexistent-cf-test-sock-" + Date.now(),
|
|
110
|
+
{ respawn },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// ping() should return false without attempting respawn
|
|
114
|
+
const alive = await client.ping();
|
|
115
|
+
expect(alive).toBe(false);
|
|
116
|
+
expect(respawn).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("does not call respawn for HTTP 4xx errors", async () => {
|
|
120
|
+
const docsDir = join(testDir, "docs");
|
|
121
|
+
mkdirSync(docsDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
const backend = new MiniSearchBackend(docsDir);
|
|
124
|
+
const handle = startDaemonServer(backend, {
|
|
125
|
+
paths: testPaths,
|
|
126
|
+
idleTimeoutMs: 0,
|
|
127
|
+
});
|
|
128
|
+
await new Promise<void>((resolve) => {
|
|
129
|
+
handle.server.once("listening", resolve);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const respawn = vi.fn(async () => true);
|
|
133
|
+
const client = new DaemonClient(testPaths.socketPath, { respawn });
|
|
134
|
+
|
|
135
|
+
// 404 is not a connection error — should not trigger respawn
|
|
136
|
+
const result = await client.retrieve("nonexistent/id");
|
|
137
|
+
expect(result).toBeNull();
|
|
138
|
+
expect(respawn).not.toHaveBeenCalled();
|
|
139
|
+
|
|
140
|
+
await new Promise<void>((resolve) => {
|
|
141
|
+
handle.server.close(() => resolve());
|
|
142
|
+
});
|
|
143
|
+
rmSync(testPaths.socketPath, { force: true });
|
|
144
|
+
rmSync(testPaths.pidFile, { force: true });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -189,6 +189,9 @@ export async function spawnDaemon(
|
|
|
189
189
|
model?: string;
|
|
190
190
|
ollamaUrl?: string;
|
|
191
191
|
},
|
|
192
|
+
opts?: {
|
|
193
|
+
idleTimeoutMs?: number;
|
|
194
|
+
},
|
|
192
195
|
): Promise<{ pid: number } | null> {
|
|
193
196
|
if (await isDaemonRunning()) return null;
|
|
194
197
|
|
|
@@ -201,6 +204,10 @@ export async function spawnDaemon(
|
|
|
201
204
|
if (!fs.existsSync(entryPath)) return null;
|
|
202
205
|
|
|
203
206
|
const args = [entryPath, docsDir];
|
|
207
|
+
// Pass idle timeout as positional arg (entry.js expects it as argv[3])
|
|
208
|
+
if (opts?.idleTimeoutMs !== undefined) {
|
|
209
|
+
args.push(String(opts.idleTimeoutMs));
|
|
210
|
+
}
|
|
204
211
|
if (embeddingConfig?.provider)
|
|
205
212
|
args.push(`--embedding-provider=${embeddingConfig.provider}`);
|
|
206
213
|
if (embeddingConfig?.model)
|
|
@@ -12,6 +12,13 @@ const rawDir =
|
|
|
12
12
|
const docsDir = path.resolve(rawDir);
|
|
13
13
|
const tierConfig = (process.env.MEMORY_TIER ?? "auto") as TierConfig;
|
|
14
14
|
|
|
15
|
+
// Daemon idle timeout from env (in milliseconds, 0 = disable)
|
|
16
|
+
const idleTimeoutMs = (() => {
|
|
17
|
+
if (!process.env.MEMORY_DAEMON_IDLE_TIMEOUT) return undefined;
|
|
18
|
+
const parsed = parseInt(process.env.MEMORY_DAEMON_IDLE_TIMEOUT, 10);
|
|
19
|
+
return isNaN(parsed) ? undefined : parsed;
|
|
20
|
+
})();
|
|
21
|
+
|
|
15
22
|
// Embedding config from environment variables
|
|
16
23
|
const embeddingConfig: Partial<EmbeddingConfig> | undefined = (() => {
|
|
17
24
|
const provider = process.env.MEMORY_EMBEDDING_PROVIDER as
|
|
@@ -27,10 +34,15 @@ const embeddingConfig: Partial<EmbeddingConfig> | undefined = (() => {
|
|
|
27
34
|
};
|
|
28
35
|
})();
|
|
29
36
|
|
|
37
|
+
const daemonOptions =
|
|
38
|
+
idleTimeoutMs !== undefined ? { idleTimeoutMs } : undefined;
|
|
39
|
+
|
|
30
40
|
const { backend, tier } = await createBackendForTier(
|
|
31
41
|
docsDir,
|
|
32
42
|
tierConfig,
|
|
33
43
|
embeddingConfig,
|
|
44
|
+
undefined,
|
|
45
|
+
daemonOptions,
|
|
34
46
|
);
|
|
35
47
|
|
|
36
48
|
// Auto-start daemon for file watching (Tier 1 & 2)
|
|
@@ -38,7 +50,7 @@ const { backend, tier } = await createBackendForTier(
|
|
|
38
50
|
// and rebuilds the search index automatically.
|
|
39
51
|
if (tier.name !== "markdown") {
|
|
40
52
|
const { spawnDaemon } = await import("./daemon/process.js");
|
|
41
|
-
spawnDaemon(docsDir, embeddingConfig).catch(() => {});
|
|
53
|
+
spawnDaemon(docsDir, embeddingConfig, daemonOptions).catch(() => {});
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
const server = new McpServer({
|
|
@@ -11,13 +11,32 @@ import type {
|
|
|
11
11
|
UpdateInput,
|
|
12
12
|
} from "./types.js";
|
|
13
13
|
|
|
14
|
+
export interface DaemonClientOptions {
|
|
15
|
+
/** Called to respawn the daemon when a connection fails. */
|
|
16
|
+
respawn?: () => Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
/**
|
|
15
20
|
* MemoryBackend implementation that talks to the daemon over UDS.
|
|
21
|
+
*
|
|
22
|
+
* When a `respawn` callback is provided, connection errors automatically
|
|
23
|
+
* trigger a single respawn attempt followed by a retry of the original request.
|
|
16
24
|
*/
|
|
17
25
|
export class DaemonClient implements MemoryBackend {
|
|
18
|
-
|
|
26
|
+
private respawn?: () => Promise<boolean>;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private socketPath: string,
|
|
30
|
+
opts?: DaemonClientOptions,
|
|
31
|
+
) {
|
|
32
|
+
this.respawn = opts?.respawn;
|
|
33
|
+
}
|
|
19
34
|
|
|
20
|
-
private
|
|
35
|
+
private rawRequest<T>(
|
|
36
|
+
method: string,
|
|
37
|
+
path: string,
|
|
38
|
+
body?: unknown,
|
|
39
|
+
): Promise<T> {
|
|
21
40
|
return new Promise((resolve, reject) => {
|
|
22
41
|
const options: http.RequestOptions = {
|
|
23
42
|
socketPath: this.socketPath,
|
|
@@ -68,6 +87,27 @@ export class DaemonClient implements MemoryBackend {
|
|
|
68
87
|
});
|
|
69
88
|
}
|
|
70
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Send a request to the daemon. On connection error (ECONNREFUSED, ENOENT),
|
|
92
|
+
* attempt to respawn the daemon once and retry.
|
|
93
|
+
*/
|
|
94
|
+
private async request<T>(
|
|
95
|
+
method: string,
|
|
96
|
+
path: string,
|
|
97
|
+
body?: unknown,
|
|
98
|
+
): Promise<T> {
|
|
99
|
+
try {
|
|
100
|
+
return await this.rawRequest<T>(method, path, body);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (!this.respawn || !isConnectionError(err)) throw err;
|
|
103
|
+
|
|
104
|
+
const ok = await this.respawn();
|
|
105
|
+
if (!ok) throw err;
|
|
106
|
+
|
|
107
|
+
return this.rawRequest<T>(method, path, body);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
71
111
|
async store(input: StoreInput): Promise<Memory> {
|
|
72
112
|
const result = await this.request<{
|
|
73
113
|
id: string;
|
|
@@ -143,7 +183,11 @@ export class DaemonClient implements MemoryBackend {
|
|
|
143
183
|
*/
|
|
144
184
|
async ping(): Promise<boolean> {
|
|
145
185
|
try {
|
|
146
|
-
|
|
186
|
+
// Use rawRequest — ping checks liveness, must not trigger respawn
|
|
187
|
+
const result = await this.rawRequest<{ status: string }>(
|
|
188
|
+
"GET",
|
|
189
|
+
"/health",
|
|
190
|
+
);
|
|
147
191
|
return result.status === "ok";
|
|
148
192
|
} catch {
|
|
149
193
|
return false;
|
|
@@ -161,3 +205,10 @@ export class DaemonClient implements MemoryBackend {
|
|
|
161
205
|
}
|
|
162
206
|
}
|
|
163
207
|
}
|
|
208
|
+
|
|
209
|
+
/** Connection-level errors that indicate the daemon process is gone. */
|
|
210
|
+
function isConnectionError(err: unknown): boolean {
|
|
211
|
+
if (!(err instanceof Error)) return false;
|
|
212
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
213
|
+
return code === "ECONNREFUSED" || code === "ENOENT" || code === "ECONNRESET";
|
|
214
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { MemoryBackend } from "./backend.js";
|
|
2
2
|
import { DaemonClient } from "./daemon-client.js";
|
|
3
3
|
import { MarkdownBackend } from "../backends/markdown.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getDaemonPaths,
|
|
6
|
+
isDaemonRunning,
|
|
7
|
+
spawnDaemon,
|
|
8
|
+
} from "../daemon/process.js";
|
|
5
9
|
import { areSqliteDepsAvailable } from "./lazy-install.js";
|
|
6
10
|
import type { EmbeddingConfig } from "../backends/sqlite/embeddings.js";
|
|
7
11
|
import type { SqliteBackendOptions } from "../backends/sqlite/index.js";
|
|
@@ -46,6 +50,20 @@ export async function detectTier(configTier?: TierConfig): Promise<TierInfo> {
|
|
|
46
50
|
return TIERS.markdown;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
/** Build a respawn callback that DaemonClient can call when the daemon is gone. */
|
|
54
|
+
function makeRespawn(
|
|
55
|
+
docsDir: string,
|
|
56
|
+
embeddingConfig?: Partial<EmbeddingConfig>,
|
|
57
|
+
idleTimeoutMs?: number,
|
|
58
|
+
): () => Promise<boolean> {
|
|
59
|
+
return async () => {
|
|
60
|
+
const result = await spawnDaemon(docsDir, embeddingConfig, {
|
|
61
|
+
idleTimeoutMs,
|
|
62
|
+
});
|
|
63
|
+
return result !== null;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
/**
|
|
50
68
|
* Create the appropriate backend for the detected tier.
|
|
51
69
|
*/
|
|
@@ -54,8 +72,14 @@ export async function createBackendForTier(
|
|
|
54
72
|
configTier?: TierConfig,
|
|
55
73
|
embeddingConfig?: Partial<EmbeddingConfig>,
|
|
56
74
|
sqliteOptions?: Pick<SqliteBackendOptions, "dbPath">,
|
|
75
|
+
daemonOptions?: { idleTimeoutMs?: number },
|
|
57
76
|
): Promise<{ backend: MemoryBackend; tier: TierInfo }> {
|
|
58
77
|
const tier = await detectTier(configTier);
|
|
78
|
+
const respawn = makeRespawn(
|
|
79
|
+
docsDir,
|
|
80
|
+
embeddingConfig,
|
|
81
|
+
daemonOptions?.idleTimeoutMs,
|
|
82
|
+
);
|
|
59
83
|
|
|
60
84
|
switch (tier.name) {
|
|
61
85
|
case "full": {
|
|
@@ -71,7 +95,7 @@ export async function createBackendForTier(
|
|
|
71
95
|
// SQLite backend failed — fall through to daemon or markdown
|
|
72
96
|
if (await isDaemonRunning()) {
|
|
73
97
|
const paths = getDaemonPaths();
|
|
74
|
-
const client = new DaemonClient(paths.socketPath);
|
|
98
|
+
const client = new DaemonClient(paths.socketPath, { respawn });
|
|
75
99
|
const alive = await client.ping();
|
|
76
100
|
if (alive) {
|
|
77
101
|
return { backend: client, tier: TIERS.lite };
|
|
@@ -86,7 +110,7 @@ export async function createBackendForTier(
|
|
|
86
110
|
case "lite": {
|
|
87
111
|
// Use daemon client
|
|
88
112
|
const paths = getDaemonPaths();
|
|
89
|
-
const client = new DaemonClient(paths.socketPath);
|
|
113
|
+
const client = new DaemonClient(paths.socketPath, { respawn });
|
|
90
114
|
const alive = await client.ping();
|
|
91
115
|
if (alive) {
|
|
92
116
|
return { backend: client, tier };
|
package/package.json
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
resolveMemoryDir
|
|
3
|
-
} from "./chunk-SIQJPT47.js";
|
|
4
1
|
import {
|
|
5
2
|
getCliVersion,
|
|
6
3
|
getLatestCliVersion,
|
|
@@ -17,6 +14,9 @@ import {
|
|
|
17
14
|
import {
|
|
18
15
|
getLibPath
|
|
19
16
|
} from "./chunk-RZRT7NGT.js";
|
|
17
|
+
import {
|
|
18
|
+
resolveMemoryDir
|
|
19
|
+
} from "./chunk-SIQJPT47.js";
|
|
20
20
|
import {
|
|
21
21
|
getInstalledVersion
|
|
22
22
|
} from "./chunk-TRIALFOQ.js";
|