coding-friend-cli 1.16.0 → 1.17.1

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 (73) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
  5. package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
  6. package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
  7. package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
  11. package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
  12. package/dist/index.js +78 -18
  13. package/dist/{init-FZ3GG53E.js → init-MF7ISADJ.js} +102 -6
  14. package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
  15. package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
  16. package/dist/memory-RGLM35HC.js +647 -0
  17. package/dist/postinstall.js +1 -1
  18. package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
  19. package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
  20. package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
  22. package/lib/cf-memory/CHANGELOG.md +25 -0
  23. package/lib/cf-memory/README.md +284 -0
  24. package/lib/cf-memory/package-lock.json +2790 -0
  25. package/lib/cf-memory/package.json +31 -0
  26. package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
  27. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
  28. package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
  29. package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
  30. package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
  31. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
  32. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
  33. package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
  34. package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
  35. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
  36. package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
  37. package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
  38. package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
  39. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
  40. package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
  41. package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
  42. package/lib/cf-memory/src/backends/markdown.ts +318 -0
  43. package/lib/cf-memory/src/backends/minisearch.ts +203 -0
  44. package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
  45. package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
  46. package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
  47. package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
  48. package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
  49. package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
  50. package/lib/cf-memory/src/daemon/entry.ts +99 -0
  51. package/lib/cf-memory/src/daemon/process.ts +271 -0
  52. package/lib/cf-memory/src/daemon/server.ts +166 -0
  53. package/lib/cf-memory/src/daemon/watcher.ts +90 -0
  54. package/lib/cf-memory/src/index.ts +53 -0
  55. package/lib/cf-memory/src/lib/backend.ts +23 -0
  56. package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
  57. package/lib/cf-memory/src/lib/dedup.ts +80 -0
  58. package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
  59. package/lib/cf-memory/src/lib/ollama.ts +76 -0
  60. package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
  61. package/lib/cf-memory/src/lib/tier.ts +107 -0
  62. package/lib/cf-memory/src/lib/types.ts +109 -0
  63. package/lib/cf-memory/src/resources/index.ts +62 -0
  64. package/lib/cf-memory/src/server.ts +20 -0
  65. package/lib/cf-memory/src/tools/delete.ts +38 -0
  66. package/lib/cf-memory/src/tools/list.ts +38 -0
  67. package/lib/cf-memory/src/tools/retrieve.ts +52 -0
  68. package/lib/cf-memory/src/tools/search.ts +47 -0
  69. package/lib/cf-memory/src/tools/store.ts +70 -0
  70. package/lib/cf-memory/src/tools/update.ts +62 -0
  71. package/lib/cf-memory/tsconfig.json +15 -0
  72. package/lib/cf-memory/vitest.config.ts +7 -0
  73. package/package.json +1 -1
@@ -0,0 +1,271 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import net from "node:net";
4
+ import http from "node:http";
5
+ import { getRequestListener } from "@hono/node-server";
6
+ import { createDaemonApp } from "./server.js";
7
+ import type { MemoryBackend } from "../lib/backend.js";
8
+
9
+ const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
10
+
11
+ export interface DaemonPaths {
12
+ socketPath: string;
13
+ pidFile: string;
14
+ logFile: string;
15
+ }
16
+
17
+ export function getDaemonPaths(): DaemonPaths {
18
+ const baseDir = path.join(
19
+ process.env.HOME ?? process.env.USERPROFILE ?? "/tmp",
20
+ ".coding-friend",
21
+ "memory",
22
+ );
23
+ fs.mkdirSync(baseDir, { recursive: true });
24
+ return {
25
+ socketPath: path.join(baseDir, "daemon.sock"),
26
+ pidFile: path.join(baseDir, "daemon.pid"),
27
+ logFile: path.join(baseDir, "daemon.log"),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Check if the daemon is running by trying to connect to the socket.
33
+ */
34
+ export async function isDaemonRunning(paths?: DaemonPaths): Promise<boolean> {
35
+ const { socketPath, pidFile } = paths ?? getDaemonPaths();
36
+
37
+ // Check PID file first
38
+ if (!fs.existsSync(pidFile)) return false;
39
+
40
+ const pid = parseInt(
41
+ fs.readFileSync(pidFile, "utf-8").trim().split("\n")[0],
42
+ 10,
43
+ );
44
+ if (isNaN(pid)) return false;
45
+
46
+ // Check if process is alive
47
+ try {
48
+ process.kill(pid, 0);
49
+ } catch {
50
+ // Process not running, clean up stale files
51
+ cleanupDaemonFiles(paths);
52
+ return false;
53
+ }
54
+
55
+ // Try to connect to socket
56
+ return new Promise((resolve) => {
57
+ const client = net.createConnection({ path: socketPath }, () => {
58
+ client.end();
59
+ resolve(true);
60
+ });
61
+ client.on("error", () => {
62
+ resolve(false);
63
+ });
64
+ client.setTimeout(1000, () => {
65
+ client.destroy();
66
+ resolve(false);
67
+ });
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Get daemon info from PID file.
73
+ */
74
+ export function getDaemonInfo(
75
+ paths?: DaemonPaths,
76
+ ): { pid: number; startedAt: number } | null {
77
+ const { pidFile } = paths ?? getDaemonPaths();
78
+ if (!fs.existsSync(pidFile)) return null;
79
+
80
+ const content = fs.readFileSync(pidFile, "utf-8").trim();
81
+ const lines = content.split("\n");
82
+ const pid = parseInt(lines[0], 10);
83
+ if (isNaN(pid)) return null;
84
+
85
+ const startedAt = lines[1] ? parseInt(lines[1], 10) : Date.now();
86
+ return { pid, startedAt };
87
+ }
88
+
89
+ function cleanupDaemonFiles(paths?: DaemonPaths): void {
90
+ const { socketPath, pidFile } = paths ?? getDaemonPaths();
91
+ try {
92
+ fs.unlinkSync(socketPath);
93
+ } catch {
94
+ // Ignore
95
+ }
96
+ try {
97
+ fs.unlinkSync(pidFile);
98
+ } catch {
99
+ // Ignore
100
+ }
101
+ }
102
+
103
+ export interface DaemonHandle {
104
+ close: () => void;
105
+ server: http.Server;
106
+ }
107
+
108
+ /**
109
+ * Start the daemon HTTP server on a Unix Domain Socket.
110
+ *
111
+ * Signal handling is NOT registered here — the caller owns that.
112
+ * Use the returned `close()` to trigger graceful shutdown.
113
+ */
114
+ export function startDaemonServer(
115
+ backend: MemoryBackend,
116
+ opts?: {
117
+ idleTimeoutMs?: number;
118
+ paths?: DaemonPaths;
119
+ onShutdown?: () => void;
120
+ },
121
+ ): DaemonHandle {
122
+ const paths = opts?.paths ?? getDaemonPaths();
123
+ const idleTimeoutMs = opts?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
124
+ const { socketPath, pidFile } = paths;
125
+
126
+ // Clean up stale socket (catch ENOENT instead of TOCTOU check-then-act)
127
+ try {
128
+ fs.unlinkSync(socketPath);
129
+ } catch {
130
+ // No stale socket — fine
131
+ }
132
+
133
+ const app = createDaemonApp(backend);
134
+ const listener = getRequestListener(app.fetch);
135
+
136
+ const server = http.createServer(listener);
137
+
138
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
139
+ let shuttingDown = false;
140
+
141
+ function resetIdleTimer() {
142
+ if (idleTimer) clearTimeout(idleTimer);
143
+ if (idleTimeoutMs > 0) {
144
+ idleTimer = setTimeout(() => {
145
+ shutdown();
146
+ }, idleTimeoutMs);
147
+ }
148
+ }
149
+
150
+ // Reset idle timer on each request
151
+ server.on("request", () => {
152
+ resetIdleTimer();
153
+ });
154
+
155
+ server.listen(socketPath, () => {
156
+ // Write PID file
157
+ fs.writeFileSync(pidFile, `${process.pid}\n${Date.now()}`, "utf-8");
158
+ resetIdleTimer();
159
+ });
160
+
161
+ function shutdown() {
162
+ if (shuttingDown) return;
163
+ shuttingDown = true;
164
+
165
+ if (idleTimer) clearTimeout(idleTimer);
166
+ opts?.onShutdown?.();
167
+ backend.close().catch(() => {});
168
+ server.close(() => {
169
+ cleanupDaemonFiles(paths);
170
+ process.exit(0);
171
+ });
172
+ // Force exit after 5 seconds
173
+ setTimeout(() => process.exit(1), 5000).unref();
174
+ }
175
+
176
+ return { close: shutdown, server };
177
+ }
178
+
179
+ /**
180
+ * Spawn the daemon as a detached background process (if not already running).
181
+ *
182
+ * Resolves `daemon/entry.js` relative to this file so it works from both
183
+ * the MCP server and the CLI without hardcoding paths.
184
+ */
185
+ export async function spawnDaemon(
186
+ docsDir: string,
187
+ embeddingConfig?: {
188
+ provider?: string;
189
+ model?: string;
190
+ ollamaUrl?: string;
191
+ },
192
+ ): Promise<{ pid: number } | null> {
193
+ if (await isDaemonRunning()) return null;
194
+
195
+ const { spawn } = await import("node:child_process");
196
+ const { fileURLToPath } = await import("node:url");
197
+
198
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
199
+ const entryPath = path.join(thisDir, "entry.js");
200
+
201
+ if (!fs.existsSync(entryPath)) return null;
202
+
203
+ const args = [entryPath, docsDir];
204
+ if (embeddingConfig?.provider)
205
+ args.push(`--embedding-provider=${embeddingConfig.provider}`);
206
+ if (embeddingConfig?.model)
207
+ args.push(`--embedding-model=${embeddingConfig.model}`);
208
+ if (embeddingConfig?.ollamaUrl)
209
+ args.push(`--embedding-ollama-url=${embeddingConfig.ollamaUrl}`);
210
+
211
+ const child = spawn("node", args, {
212
+ detached: true,
213
+ stdio: "ignore",
214
+ env: { ...process.env },
215
+ });
216
+ child.unref();
217
+
218
+ // Wait for daemon to be ready (max 3 seconds)
219
+ for (let i = 0; i < 30; i++) {
220
+ await new Promise((r) => setTimeout(r, 100));
221
+ if (await isDaemonRunning()) {
222
+ const info = getDaemonInfo();
223
+ return info ? { pid: info.pid } : null;
224
+ }
225
+ }
226
+
227
+ return null;
228
+ }
229
+
230
+ /**
231
+ * Stop the daemon by sending SIGTERM.
232
+ */
233
+ export async function stopDaemon(paths?: DaemonPaths): Promise<boolean> {
234
+ const { pidFile } = paths ?? getDaemonPaths();
235
+ if (!fs.existsSync(pidFile)) return false;
236
+
237
+ const pid = parseInt(
238
+ fs.readFileSync(pidFile, "utf-8").trim().split("\n")[0],
239
+ 10,
240
+ );
241
+ if (isNaN(pid)) return false;
242
+
243
+ try {
244
+ process.kill(pid, "SIGTERM");
245
+ } catch {
246
+ // Already dead
247
+ cleanupDaemonFiles(paths);
248
+ return false;
249
+ }
250
+
251
+ // Wait for process to exit (max 5 seconds)
252
+ for (let i = 0; i < 50; i++) {
253
+ await new Promise((r) => setTimeout(r, 100));
254
+ try {
255
+ process.kill(pid, 0);
256
+ } catch {
257
+ // Process exited
258
+ cleanupDaemonFiles(paths);
259
+ return true;
260
+ }
261
+ }
262
+
263
+ // Force kill
264
+ try {
265
+ process.kill(pid, "SIGKILL");
266
+ } catch {
267
+ // Already dead
268
+ }
269
+ cleanupDaemonFiles(paths);
270
+ return true;
271
+ }
@@ -0,0 +1,166 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import type { MemoryBackend } from "../lib/backend.js";
4
+ import {
5
+ MEMORY_TYPES,
6
+ type MemoryType,
7
+ type SearchInput,
8
+ } from "../lib/types.js";
9
+
10
+ const memoryTypeSchema = z.enum(MEMORY_TYPES);
11
+
12
+ const storeSchema = z.object({
13
+ title: z.string().min(1),
14
+ description: z.string(),
15
+ type: memoryTypeSchema,
16
+ tags: z.array(z.string()),
17
+ content: z.string(),
18
+ importance: z.number().min(1).max(5).optional(),
19
+ source: z.string().optional(),
20
+ });
21
+
22
+ const updateSchema = z.object({
23
+ title: z.string().min(1).optional(),
24
+ description: z.string().optional(),
25
+ tags: z.array(z.string()).optional(),
26
+ content: z.string().optional(),
27
+ importance: z.number().min(1).max(5).optional(),
28
+ });
29
+
30
+ function parseType(raw: string | undefined): MemoryType | undefined {
31
+ if (!raw) return undefined;
32
+ const result = memoryTypeSchema.safeParse(raw);
33
+ return result.success ? result.data : undefined;
34
+ }
35
+
36
+ export function createDaemonApp(backend: MemoryBackend): Hono {
37
+ const app = new Hono();
38
+
39
+ // Global error handler
40
+ app.onError((err, c) => {
41
+ return c.json(
42
+ { error: err instanceof Error ? err.message : "Internal server error" },
43
+ 500,
44
+ );
45
+ });
46
+
47
+ // Health check
48
+ app.get("/health", (c) => {
49
+ return c.json({
50
+ status: "ok",
51
+ uptime: process.uptime(),
52
+ pid: process.pid,
53
+ });
54
+ });
55
+
56
+ // Stats
57
+ app.get("/stats", async (c) => {
58
+ const stats = await backend.stats();
59
+ return c.json(stats);
60
+ });
61
+
62
+ // Store a new memory
63
+ app.post("/memory", async (c) => {
64
+ const raw = await c.req.json();
65
+ const parsed = storeSchema.safeParse(raw);
66
+ if (!parsed.success) {
67
+ return c.json(
68
+ { error: "Validation failed", details: parsed.error.issues },
69
+ 400,
70
+ );
71
+ }
72
+ const memory = await backend.store(parsed.data);
73
+ return c.json(
74
+ { id: memory.id, title: memory.frontmatter.title, stored: true },
75
+ 201,
76
+ );
77
+ });
78
+
79
+ // Search memories
80
+ app.get("/memory/search", async (c) => {
81
+ const query = c.req.query("query") ?? "";
82
+ const type = parseType(c.req.query("type"));
83
+ const tagsRaw = c.req.query("tags");
84
+ const limitRaw = c.req.query("limit");
85
+
86
+ const input: SearchInput = {
87
+ query,
88
+ type,
89
+ tags: tagsRaw ? tagsRaw.split(",") : undefined,
90
+ limit: limitRaw ? parseInt(limitRaw, 10) : undefined,
91
+ };
92
+
93
+ const results = await backend.search(input);
94
+ return c.json(results);
95
+ });
96
+
97
+ // Retrieve a memory by ID (category/slug)
98
+ app.get("/memory/:category/:slug", async (c) => {
99
+ const id = `${c.req.param("category")}/${c.req.param("slug")}`;
100
+ const memory = await backend.retrieve(id);
101
+ if (!memory) {
102
+ return c.json({ error: "Not found" }, 404);
103
+ }
104
+ return c.json(memory);
105
+ });
106
+
107
+ // List memories
108
+ app.get("/memory", async (c) => {
109
+ const type = parseType(c.req.query("type"));
110
+ const category = c.req.query("category");
111
+ const limitRaw = c.req.query("limit");
112
+
113
+ const metas = await backend.list({
114
+ type,
115
+ category: category || undefined,
116
+ limit: limitRaw ? parseInt(limitRaw, 10) : undefined,
117
+ });
118
+ return c.json(metas);
119
+ });
120
+
121
+ // Update a memory
122
+ app.patch("/memory/:category/:slug", async (c) => {
123
+ const id = `${c.req.param("category")}/${c.req.param("slug")}`;
124
+ const raw = await c.req.json();
125
+ const parsed = updateSchema.safeParse(raw);
126
+ if (!parsed.success) {
127
+ return c.json(
128
+ { error: "Validation failed", details: parsed.error.issues },
129
+ 400,
130
+ );
131
+ }
132
+ const memory = await backend.update({ id, ...parsed.data });
133
+ if (!memory) {
134
+ return c.json({ error: "Not found" }, 404);
135
+ }
136
+ return c.json({
137
+ id: memory.id,
138
+ title: memory.frontmatter.title,
139
+ updated: true,
140
+ });
141
+ });
142
+
143
+ // Delete a memory
144
+ app.delete("/memory/:category/:slug", async (c) => {
145
+ const id = `${c.req.param("category")}/${c.req.param("slug")}`;
146
+ const deleted = await backend.delete(id);
147
+ if (!deleted) {
148
+ return c.json({ error: "Not found" }, 404);
149
+ }
150
+ return c.json({ id, deleted: true });
151
+ });
152
+
153
+ // Rebuild index (for MiniSearchBackend)
154
+ app.post("/rebuild", async (c) => {
155
+ if ("rebuild" in backend && typeof backend.rebuild === "function") {
156
+ await (backend as MemoryBackend & { rebuild(): Promise<void> }).rebuild();
157
+ return c.json({ rebuilt: true });
158
+ }
159
+ return c.json({
160
+ rebuilt: false,
161
+ reason: "Backend does not support rebuild",
162
+ });
163
+ });
164
+
165
+ return app;
166
+ }
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { MemoryBackend } from "../lib/backend.js";
4
+
5
+ export interface WatcherHandle {
6
+ close: () => void;
7
+ }
8
+
9
+ /**
10
+ * Watch docs/memory/ for changes and trigger index rebuild (debounced).
11
+ */
12
+ export function setupWatcher(
13
+ docsDir: string,
14
+ backend: Required<Pick<MemoryBackend, "rebuild">>,
15
+ debounceMs = 500,
16
+ ): WatcherHandle {
17
+ let timer: ReturnType<typeof setTimeout> | null = null;
18
+ const watchers: fs.FSWatcher[] = [];
19
+ const watchedDirs = new Set<string>();
20
+
21
+ function scheduleRebuild() {
22
+ if (timer) clearTimeout(timer);
23
+ timer = setTimeout(() => {
24
+ backend.rebuild().catch(() => {});
25
+ }, debounceMs);
26
+ }
27
+
28
+ function watchSubdir(dirPath: string) {
29
+ if (watchedDirs.has(dirPath)) return;
30
+ watchedDirs.add(dirPath);
31
+ try {
32
+ const w = fs.watch(dirPath, { persistent: false }, (_, filename) => {
33
+ if (filename && filename.endsWith(".md")) {
34
+ scheduleRebuild();
35
+ }
36
+ });
37
+ watchers.push(w);
38
+ } catch {
39
+ watchedDirs.delete(dirPath);
40
+ }
41
+ }
42
+
43
+ // Watch the root docs dir
44
+ if (fs.existsSync(docsDir)) {
45
+ try {
46
+ const rootWatcher = fs.watch(
47
+ docsDir,
48
+ { persistent: false },
49
+ (_, filename) => {
50
+ if (filename && !filename.startsWith(".")) {
51
+ scheduleRebuild();
52
+ // If a new directory appeared, watch it too
53
+ const subPath = path.join(docsDir, filename);
54
+ try {
55
+ if (fs.statSync(subPath).isDirectory()) {
56
+ watchSubdir(subPath);
57
+ }
58
+ } catch {
59
+ // File may have been deleted between event and stat
60
+ }
61
+ }
62
+ },
63
+ );
64
+ watchers.push(rootWatcher);
65
+ } catch {
66
+ // Ignore watch errors (e.g., directory doesn't exist)
67
+ }
68
+
69
+ // Watch existing subdirectories
70
+ try {
71
+ const entries = fs.readdirSync(docsDir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
74
+ watchSubdir(path.join(docsDir, entry.name));
75
+ }
76
+ }
77
+ } catch {
78
+ // Ignore
79
+ }
80
+ }
81
+
82
+ return {
83
+ close() {
84
+ if (timer) clearTimeout(timer);
85
+ for (const w of watchers) {
86
+ w.close();
87
+ }
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,53 @@
1
+ import path from "node:path";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerAllTools } from "./server.js";
5
+ import { registerAllResources } from "./resources/index.js";
6
+ import { createBackendForTier } from "./lib/tier.js";
7
+ import type { TierConfig } from "./lib/tier.js";
8
+ import type { EmbeddingConfig } from "./backends/sqlite/embeddings.js";
9
+
10
+ const rawDir =
11
+ process.argv[2] ?? process.env.MEMORY_DOCS_DIR ?? "./docs/memory";
12
+ const docsDir = path.resolve(rawDir);
13
+ const tierConfig = (process.env.MEMORY_TIER ?? "auto") as TierConfig;
14
+
15
+ // Embedding config from environment variables
16
+ const embeddingConfig: Partial<EmbeddingConfig> | undefined = (() => {
17
+ const provider = process.env.MEMORY_EMBEDDING_PROVIDER as
18
+ | EmbeddingConfig["provider"]
19
+ | undefined;
20
+ const model = process.env.MEMORY_EMBEDDING_MODEL;
21
+ const ollamaUrl = process.env.MEMORY_EMBEDDING_OLLAMA_URL;
22
+ if (!provider && !model && !ollamaUrl) return undefined;
23
+ return {
24
+ ...(provider && { provider }),
25
+ ...(model && { model }),
26
+ ...(ollamaUrl && { ollamaUrl }),
27
+ };
28
+ })();
29
+
30
+ const { backend, tier } = await createBackendForTier(
31
+ docsDir,
32
+ tierConfig,
33
+ embeddingConfig,
34
+ );
35
+
36
+ // Auto-start daemon for file watching (Tier 1 & 2)
37
+ // Daemon watches docs/memory/ for external changes (git pull, manual edits)
38
+ // and rebuilds the search index automatically.
39
+ if (tier.name !== "markdown") {
40
+ const { spawnDaemon } = await import("./daemon/process.js");
41
+ spawnDaemon(docsDir, embeddingConfig).catch(() => {});
42
+ }
43
+
44
+ const server = new McpServer({
45
+ name: "coding-friend-memory",
46
+ version: "0.0.1",
47
+ });
48
+
49
+ registerAllTools(server, backend);
50
+ registerAllResources(server, backend);
51
+
52
+ const transport = new StdioServerTransport();
53
+ await server.connect(transport);
@@ -0,0 +1,23 @@
1
+ import type {
2
+ ListInput,
3
+ Memory,
4
+ MemoryMeta,
5
+ MemoryStats,
6
+ SearchInput,
7
+ SearchResult,
8
+ StoreInput,
9
+ UpdateInput,
10
+ } from "./types.js";
11
+
12
+ export interface MemoryBackend {
13
+ store(input: StoreInput): Promise<Memory>;
14
+ search(input: SearchInput): Promise<SearchResult[]>;
15
+ retrieve(id: string): Promise<Memory | null>;
16
+ list(input: ListInput): Promise<MemoryMeta[]>;
17
+ update(input: UpdateInput): Promise<Memory | null>;
18
+ delete(id: string): Promise<boolean>;
19
+ stats(): Promise<MemoryStats>;
20
+ close(): Promise<void>;
21
+ /** Rebuild the search index from source files. Optional — not all backends support it. */
22
+ rebuild?(): Promise<void>;
23
+ }