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.
- package/dist/{chunk-PDVSGGH6.js → chunk-2ETE7NEL.js} +32 -28
- package/dist/{chunk-PYTEAC6K.js → chunk-DMJCO6LJ.js} +1 -1
- package/dist/{chunk-3KXYEC2T.js → chunk-LTHJTWJ6.js} +82 -3
- package/dist/chunk-NREZK463.js +41 -0
- package/dist/{chunk-BIX7AJU3.js → chunk-O4CNZB5H.js} +1 -1
- package/dist/{memory-27ZSRPM7.js → chunk-WCSNAEOF.js} +155 -73
- package/dist/{config-5I3XJEEL.js → config-5DKOGWWJ.js} +9 -14
- package/dist/{dev-4A5CTBGK.js → dev-S7S3SFKI.js} +19 -18
- package/dist/{disable-FNMRPUO7.js → disable-V7OLXEUH.js} +2 -2
- package/dist/{enable-HUKTMK5U.js → enable-OLHRKZCV.js} +2 -2
- package/dist/{host-V5O3PDCF.js → host-RR6FT7AX.js} +7 -6
- package/dist/index.js +31 -31
- package/dist/{init-3XMC6YJI.js → init-6B2OHKFX.js} +22 -40
- package/dist/{install-5ZBHH2OM.js → install-BURZ5QEM.js} +7 -6
- package/dist/mcp-CB4PUUES.js +156 -0
- package/dist/memory-TJZNGTOA.js +37 -0
- package/dist/{permission-BEC6YZQA.js → permission-QIJLEMJ6.js} +4 -3
- package/dist/postinstall.js +2 -2
- package/dist/{session-ZDJ2APGL.js → session-IECOM63R.js} +1 -1
- package/dist/{status-F6WASK7N.js → status-ZZRJ3VGF.js} +7 -7
- package/dist/{statusline-AISH77XP.js → statusline-YL44XYGF.js} +4 -3
- package/dist/{uninstall-RJ6SBB76.js → uninstall-OXWRPWYF.js} +7 -6
- package/dist/{update-536C3YG7.js → update-6N7EASFT.js} +4 -4
- package/dist/{update-check-L5TNWT2J.js → update-check-XSETLRVR.js} +1 -1
- package/lib/cf-memory/README.md +4 -1
- package/lib/cf-memory/package.json +0 -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 +21 -2
- package/lib/cf-memory/src/lib/daemon-client.ts +54 -3
- package/lib/cf-memory/src/lib/tier.ts +27 -3
- package/lib/learn-host/package.json +0 -1
- package/lib/learn-mcp/package.json +0 -1
- package/lib/learn-mcp/src/index.ts +8 -1
- package/package.json +1 -1
- package/dist/chunk-W5CD7WTX.js +0 -15
- package/dist/mcp-U34LFD34.js +0 -106
- package/lib/cf-memory/CHANGELOG.md +0 -42
- package/lib/learn-host/CHANGELOG.md +0 -35
- 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-
|
|
8
|
+
} from "./chunk-DMJCO6LJ.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveScope
|
|
11
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
7
|
+
} from "./chunk-2ETE7NEL.js";
|
|
8
8
|
import "./chunk-TRIALFOQ.js";
|
|
9
9
|
import "./chunk-PRIH34UB.js";
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
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-
|
|
15
|
+
import "./chunk-NREZK463.js";
|
|
16
16
|
export {
|
|
17
17
|
getCliVersion,
|
|
18
18
|
getLatestCliVersion,
|
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 |
|
|
@@ -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.
|
|
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).
|
|
@@ -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:
|
|
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
|
-
|
|
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 };
|
|
@@ -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:
|
|
18
|
+
version: cliPkg.version,
|
|
12
19
|
});
|
|
13
20
|
|
|
14
21
|
registerAllTools(server, docsDir);
|
package/package.json
CHANGED
package/dist/chunk-W5CD7WTX.js
DELETED
|
@@ -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
|
-
};
|
package/dist/mcp-U34LFD34.js
DELETED
|
@@ -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
|
-
};
|