coding-friend-cli 1.21.1 → 1.23.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.
Files changed (40) hide show
  1. package/dist/{chunk-PDVSGGH6.js → chunk-2ETE7NEL.js} +32 -28
  2. package/dist/{chunk-PYTEAC6K.js → chunk-DMJCO6LJ.js} +1 -1
  3. package/dist/{chunk-3KXYEC2T.js → chunk-LTHJTWJ6.js} +82 -3
  4. package/dist/chunk-NREZK463.js +41 -0
  5. package/dist/{chunk-BIX7AJU3.js → chunk-O4CNZB5H.js} +1 -1
  6. package/dist/{memory-27ZSRPM7.js → chunk-WCSNAEOF.js} +155 -73
  7. package/dist/{config-5I3XJEEL.js → config-5DKOGWWJ.js} +9 -14
  8. package/dist/{dev-4A5CTBGK.js → dev-S7S3SFKI.js} +19 -18
  9. package/dist/{disable-FNMRPUO7.js → disable-V7OLXEUH.js} +2 -2
  10. package/dist/{enable-HUKTMK5U.js → enable-OLHRKZCV.js} +2 -2
  11. package/dist/{host-V5O3PDCF.js → host-RR6FT7AX.js} +7 -6
  12. package/dist/index.js +31 -31
  13. package/dist/{init-3XMC6YJI.js → init-6B2OHKFX.js} +22 -40
  14. package/dist/{install-5ZBHH2OM.js → install-BURZ5QEM.js} +7 -6
  15. package/dist/mcp-CB4PUUES.js +156 -0
  16. package/dist/memory-TJZNGTOA.js +37 -0
  17. package/dist/{permission-BEC6YZQA.js → permission-QIJLEMJ6.js} +4 -3
  18. package/dist/postinstall.js +2 -2
  19. package/dist/{session-ZDJ2APGL.js → session-IECOM63R.js} +1 -1
  20. package/dist/{status-F6WASK7N.js → status-ZZRJ3VGF.js} +7 -7
  21. package/dist/{statusline-AISH77XP.js → statusline-YL44XYGF.js} +4 -3
  22. package/dist/{uninstall-RJ6SBB76.js → uninstall-OXWRPWYF.js} +7 -6
  23. package/dist/{update-536C3YG7.js → update-6N7EASFT.js} +4 -4
  24. package/dist/{update-check-L5TNWT2J.js → update-check-XSETLRVR.js} +1 -1
  25. package/lib/cf-memory/README.md +4 -1
  26. package/lib/cf-memory/package.json +0 -1
  27. package/lib/cf-memory/src/__tests__/daemon-client.test.ts +146 -0
  28. package/lib/cf-memory/src/daemon/process.ts +7 -0
  29. package/lib/cf-memory/src/index.ts +21 -2
  30. package/lib/cf-memory/src/lib/daemon-client.ts +54 -3
  31. package/lib/cf-memory/src/lib/tier.ts +27 -3
  32. package/lib/learn-host/package.json +0 -1
  33. package/lib/learn-mcp/package.json +0 -1
  34. package/lib/learn-mcp/src/index.ts +8 -1
  35. package/package.json +1 -1
  36. package/dist/chunk-W5CD7WTX.js +0 -15
  37. package/dist/mcp-U34LFD34.js +0 -106
  38. package/lib/cf-memory/CHANGELOG.md +0 -42
  39. package/lib/learn-host/CHANGELOG.md +0 -35
  40. package/lib/learn-mcp/CHANGELOG.md +0 -16
@@ -5,10 +5,10 @@ import {
5
5
  import {
6
6
  hasShellCompletion,
7
7
  removeShellCompletion
8
- } from "./chunk-PYTEAC6K.js";
8
+ } from "./chunk-DMJCO6LJ.js";
9
9
  import {
10
10
  resolveScope
11
- } from "./chunk-BIX7AJU3.js";
11
+ } from "./chunk-O4CNZB5H.js";
12
12
  import {
13
13
  commandExists,
14
14
  run
@@ -25,8 +25,9 @@ import {
25
25
  marketplaceClonePath
26
26
  } from "./chunk-DO6FV6BL.js";
27
27
  import {
28
- log
29
- } from "./chunk-W5CD7WTX.js";
28
+ log,
29
+ printBanner
30
+ } from "./chunk-NREZK463.js";
30
31
 
31
32
  // src/commands/uninstall.ts
32
33
  import { existsSync, rmSync } from "fs";
@@ -118,8 +119,8 @@ async function uninstallScoped(scope) {
118
119
  log.dim("Restart Claude Code to complete the uninstall.");
119
120
  }
120
121
  async function uninstallCommand(opts = {}) {
121
- console.log(`
122
- === \u{1F44B} ${chalk.red("Coding Friend Uninstall")} \u{1F44B} ===`);
122
+ console.log();
123
+ printBanner("\u{1F44B} Coding Friend Uninstall \u{1F44B}", { color: chalk.red });
123
124
  if (!commandExists("claude")) {
124
125
  log.error("Claude CLI not found. Cannot uninstall plugin without it.");
125
126
  log.dim(
@@ -4,15 +4,15 @@ import {
4
4
  getLatestVersion,
5
5
  semverCompare,
6
6
  updateCommand
7
- } from "./chunk-PDVSGGH6.js";
7
+ } from "./chunk-2ETE7NEL.js";
8
8
  import "./chunk-TRIALFOQ.js";
9
9
  import "./chunk-PRIH34UB.js";
10
- import "./chunk-PYTEAC6K.js";
11
- import "./chunk-BIX7AJU3.js";
10
+ import "./chunk-DMJCO6LJ.js";
11
+ import "./chunk-O4CNZB5H.js";
12
12
  import "./chunk-EVGXUDX4.js";
13
13
  import "./chunk-5UVDWG5L.js";
14
14
  import "./chunk-DO6FV6BL.js";
15
- import "./chunk-W5CD7WTX.js";
15
+ import "./chunk-NREZK463.js";
16
16
  export {
17
17
  getCliVersion,
18
18
  getLatestCliVersion,
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-DO6FV6BL.js";
7
7
  import {
8
8
  log
9
- } from "./chunk-W5CD7WTX.js";
9
+ } from "./chunk-NREZK463.js";
10
10
 
11
11
  // src/lib/update-check.ts
12
12
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
@@ -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 |
@@ -309,4 +312,4 @@ If these are missing, `cf memory init` will fail at the "Installing SQLite depen
309
312
 
310
313
  ## Versioning
311
314
 
312
- Part of the [coding-friend](https://github.com/dinhanhthi/coding-friend) monorepo. Tagged independently as `cf-memory-v*`. Changelog: [CHANGELOG.md](./CHANGELOG.md).
315
+ Part of the [coding-friend](https://github.com/dinhanhthi/coding-friend) monorepo. Versioned and released as part of the CLI package. Changelog: [`cli/CHANGELOG.md`](../../CHANGELOG.md).
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "name": "coding-friend-cf-memory",
3
- "version": "0.2.1",
4
3
  "private": true,
5
4
  "type": "module",
6
5
  "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)
@@ -1,4 +1,6 @@
1
1
  import path from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
6
  import { registerAllTools } from "./server.js";
@@ -7,11 +9,23 @@ import { createBackendForTier } from "./lib/tier.js";
7
9
  import type { TierConfig } from "./lib/tier.js";
8
10
  import type { EmbeddingConfig } from "./backends/sqlite/embeddings.js";
9
11
 
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const cliPkg = JSON.parse(
14
+ readFileSync(path.resolve(__dirname, "../../../package.json"), "utf-8"),
15
+ );
16
+
10
17
  const rawDir =
11
18
  process.argv[2] ?? process.env.MEMORY_DOCS_DIR ?? "./docs/memory";
12
19
  const docsDir = path.resolve(rawDir);
13
20
  const tierConfig = (process.env.MEMORY_TIER ?? "auto") as TierConfig;
14
21
 
22
+ // Daemon idle timeout from env (in milliseconds, 0 = disable)
23
+ const idleTimeoutMs = (() => {
24
+ if (!process.env.MEMORY_DAEMON_IDLE_TIMEOUT) return undefined;
25
+ const parsed = parseInt(process.env.MEMORY_DAEMON_IDLE_TIMEOUT, 10);
26
+ return isNaN(parsed) ? undefined : parsed;
27
+ })();
28
+
15
29
  // Embedding config from environment variables
16
30
  const embeddingConfig: Partial<EmbeddingConfig> | undefined = (() => {
17
31
  const provider = process.env.MEMORY_EMBEDDING_PROVIDER as
@@ -27,10 +41,15 @@ const embeddingConfig: Partial<EmbeddingConfig> | undefined = (() => {
27
41
  };
28
42
  })();
29
43
 
44
+ const daemonOptions =
45
+ idleTimeoutMs !== undefined ? { idleTimeoutMs } : undefined;
46
+
30
47
  const { backend, tier } = await createBackendForTier(
31
48
  docsDir,
32
49
  tierConfig,
33
50
  embeddingConfig,
51
+ undefined,
52
+ daemonOptions,
34
53
  );
35
54
 
36
55
  // Auto-start daemon for file watching (Tier 1 & 2)
@@ -38,12 +57,12 @@ const { backend, tier } = await createBackendForTier(
38
57
  // and rebuilds the search index automatically.
39
58
  if (tier.name !== "markdown") {
40
59
  const { spawnDaemon } = await import("./daemon/process.js");
41
- spawnDaemon(docsDir, embeddingConfig).catch(() => {});
60
+ spawnDaemon(docsDir, embeddingConfig, daemonOptions).catch(() => {});
42
61
  }
43
62
 
44
63
  const server = new McpServer({
45
64
  name: "coding-friend-memory",
46
- version: "0.0.1",
65
+ version: cliPkg.version,
47
66
  });
48
67
 
49
68
  registerAllTools(server, backend);
@@ -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 };
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "name": "coding-friend-learn-host",
3
- "version": "0.3.0",
4
3
  "private": true,
5
4
  "scripts": {
6
5
  "dev": "next dev -p 3333",
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "name": "coding-friend-learn-mcp",
3
- "version": "0.0.3",
4
3
  "private": true,
5
4
  "type": "module",
6
5
  "bin": {
@@ -1,4 +1,6 @@
1
1
  import path from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
6
  import { registerAllTools } from "./server.js";
@@ -6,9 +8,14 @@ import { registerAllTools } from "./server.js";
6
8
  const rawDir = process.argv[2] ?? process.env.LEARN_DOCS_DIR ?? "./docs/learn";
7
9
  const docsDir = path.resolve(rawDir);
8
10
 
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const cliPkg = JSON.parse(
13
+ readFileSync(path.resolve(__dirname, "../../../package.json"), "utf-8"),
14
+ );
15
+
9
16
  const server = new McpServer({
10
17
  name: "coding-friend-learn",
11
- version: "0.0.3",
18
+ version: cliPkg.version,
12
19
  });
13
20
 
14
21
  registerAllTools(server, docsDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-friend-cli",
3
- "version": "1.21.1",
3
+ "version": "1.23.0",
4
4
  "description": "CLI for coding-friend — host learning docs, setup MCP server, initialize projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,15 +0,0 @@
1
- // src/lib/log.ts
2
- import chalk from "chalk";
3
- var log = {
4
- info: (msg) => console.log(chalk.blue("\u2139"), msg),
5
- success: (msg) => console.log(chalk.green("\u2714"), msg),
6
- congrats: (msg) => console.log(chalk.green("\u{1F389}"), msg),
7
- warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
8
- error: (msg) => console.log(chalk.red("\u2716"), msg),
9
- step: (msg) => console.log(chalk.cyan("\u2192"), msg),
10
- dim: (msg) => console.log(chalk.dim(msg))
11
- };
12
-
13
- export {
14
- log
15
- };
@@ -1,106 +0,0 @@
1
- import {
2
- resolveDocsDir
3
- } from "./chunk-SIQJPT47.js";
4
- import {
5
- getLibPath
6
- } from "./chunk-RZRT7NGT.js";
7
- import "./chunk-PRIH34UB.js";
8
- import {
9
- run
10
- } from "./chunk-EVGXUDX4.js";
11
- import "./chunk-5UVDWG5L.js";
12
- import "./chunk-DO6FV6BL.js";
13
- import {
14
- log
15
- } from "./chunk-W5CD7WTX.js";
16
-
17
- // src/commands/mcp.ts
18
- import { existsSync, readdirSync } from "fs";
19
- import { join } from "path";
20
- import chalk from "chalk";
21
- function countMdFiles(dir) {
22
- let count = 0;
23
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
24
- if (entry.isDirectory()) {
25
- count += countMdFiles(join(dir, entry.name));
26
- } else if (entry.name.endsWith(".md") && entry.name !== "README.md") {
27
- count++;
28
- }
29
- }
30
- return count;
31
- }
32
- async function mcpCommand(path) {
33
- const docsDir = resolveDocsDir(path);
34
- const mcpDir = getLibPath("learn-mcp");
35
- if (!existsSync(docsDir)) {
36
- log.error(`Docs folder not found: ${docsDir}`);
37
- log.dim("Run /cf-learn first to generate some docs.");
38
- process.exit(1);
39
- }
40
- const docCount = countMdFiles(docsDir);
41
- if (docCount === 0) {
42
- log.error(`No .md files found in ${docsDir}`);
43
- log.dim("Run /cf-learn first to generate some docs.");
44
- process.exit(1);
45
- }
46
- console.log("=== \u{1F33F} Coding Friend MCP \u{1F33F} ===");
47
- log.info(`Docs folder: ${chalk.cyan(docsDir)}`);
48
- log.info(`Found: ${chalk.green(docCount)} docs`);
49
- console.log();
50
- if (!existsSync(join(mcpDir, "node_modules"))) {
51
- log.step("Installing MCP server dependencies (one-time setup)...");
52
- const result = run("npm", ["install", "--silent"], { cwd: mcpDir });
53
- if (result === null) {
54
- log.error("Failed to install dependencies");
55
- process.exit(1);
56
- }
57
- log.success("Done.");
58
- }
59
- if (!existsSync(join(mcpDir, "dist"))) {
60
- log.step("Building MCP server...");
61
- const result = run("npm", ["run", "build", "--silent"], { cwd: mcpDir });
62
- if (result === null) {
63
- log.error("Failed to build MCP server");
64
- process.exit(1);
65
- }
66
- log.success("Done.");
67
- }
68
- console.log();
69
- const serverPath = join(mcpDir, "dist", "index.js");
70
- console.log(`Add this to your MCP client config:
71
-
72
- --- Claude Desktop / Claude Chat (claude_desktop_config.json) ---
73
-
74
- {
75
- "mcpServers": {
76
- "coding-friend-learn": {
77
- "command": "node",
78
- "args": ["${serverPath}", "${docsDir}"]
79
- }
80
- }
81
- }
82
-
83
- --- Generic MCP client ---
84
-
85
- Server command: node ${serverPath} ${docsDir}
86
- Transport: stdio
87
-
88
- --- Available tools ---
89
-
90
- Read:
91
- list-categories List all categories with doc counts
92
- list-docs List docs, filter by category/tag
93
- read-doc Read full content of a doc
94
- search-docs Full-text search across all docs
95
- get-review-list Docs that need review
96
-
97
- Write:
98
- create-doc Create new learning doc
99
- update-doc Append content or update tags
100
- improve-doc Get improvement suggestions
101
- track-knowledge Record understanding level (remembered/needs-review/new)
102
- `);
103
- }
104
- export {
105
- mcpCommand
106
- };