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.
@@ -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
- memoryConfigMenu
8
- } from "./chunk-3KXYEC2T.js";
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 ensureBuilt(mcpDir) {
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
- ensureBuilt(mcpDir);
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
- ensureBuilt(mcpDir);
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
- ensureBuilt(mcpDir);
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
- ensureBuilt(mcpDir);
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 result = await spawnDaemon(memoryDir, embedding);
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
- ensureBuilt(mcpDir);
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
- ensureBuilt(mcpDir);
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
- ensureBuilt(mcpDir);
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
- log.step("Installing SQLite dependencies...");
418
- const { ensureDeps, areSqliteDepsAvailable } = await import(join(mcpDir, "dist/lib/lazy-install.js"));
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
- ensureBuilt(mcpDir);
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
- ensureBuilt(mcpDir);
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
- ensureBuilt(mcpDir);
841
+ ensureMemoryBuilt(mcpDir);
738
842
  const serverPath = join(mcpDir, "dist", "index.js");
739
- console.log(`Add this to your MCP client config:
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
- memoryConfigCommand,
774
- memoryInitCommand,
847
+ ensureMemoryBuilt,
848
+ printMemoryMcpConfig,
849
+ memoryStatusCommand,
850
+ memorySearchCommand,
775
851
  memoryListCommand,
776
- memoryMcpCommand,
852
+ memoryStartDaemonCommand,
853
+ memoryStopDaemonCommand,
777
854
  memoryRebuildCommand,
855
+ memoryInitCommand,
856
+ memoryConfigCommand,
778
857
  memoryRmCommand,
779
- memorySearchCommand,
780
- memoryStartDaemonCommand,
781
- memoryStatusCommand,
782
- memoryStopDaemonCommand
858
+ memoryMcpCommand
783
859
  };
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  memoryConfigMenu
3
- } from "./chunk-3KXYEC2T.js";
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-3XMC6YJI.js");
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-5I3XJEEL.js");
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-V5O3PDCF.js");
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-U34LFD34.js");
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-F6WASK7N.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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-PDENAODU.js");
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
- if (isMemoryMcpConfigured()) {
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
- const mcpPath = join(process.cwd(), ".mcp.json");
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
- resolveDocsDir
3
- } from "./chunk-SIQJPT47.js";
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{1F33F} Coding Friend MCP \u{1F33F} ===");
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)
@@ -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 |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cf-memory",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- constructor(private socketPath: string) {}
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 request<T>(method: string, path: string, body?: unknown): Promise<T> {
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
- const result = await this.request<{ status: string }>("GET", "/health");
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 { getDaemonPaths, isDaemonRunning } from "../daemon/process.js";
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,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,9 @@
1
- import {
2
- resolveDocsDir
3
- } from "./chunk-SIQJPT47.js";
4
1
  import {
5
2
  getLibPath
6
3
  } from "./chunk-RZRT7NGT.js";
4
+ import {
5
+ resolveDocsDir
6
+ } from "./chunk-SIQJPT47.js";
7
7
  import "./chunk-PRIH34UB.js";
8
8
  import {
9
9
  run,
@@ -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";