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,286 @@
1
+ /**
2
+ * Embedding pipeline for semantic search.
3
+ *
4
+ * Supports two providers:
5
+ * - Transformers.js (local, default): all-MiniLM-L6-v2, 384 dims
6
+ * - Ollama (optional): configurable model, auto-detect availability
7
+ *
8
+ * Embeddings are cached by content hash in the SQLite embedding_cache table.
9
+ */
10
+ import crypto from "node:crypto";
11
+ import type { DatabaseLike } from "./migrations.js";
12
+
13
+ export type EmbeddingProvider = "transformers" | "ollama";
14
+
15
+ export interface EmbeddingConfig {
16
+ provider: EmbeddingProvider;
17
+ model?: string;
18
+ ollamaUrl?: string;
19
+ depsDir?: string;
20
+ }
21
+
22
+ export const DEFAULT_TRANSFORMERS_MODEL = "Xenova/all-MiniLM-L6-v2";
23
+ export const DEFAULT_OLLAMA_MODEL = "all-minilm:l6-v2";
24
+ const DEFAULT_OLLAMA_URL = "http://localhost:11434";
25
+
26
+ /** Default embedding dimensions (all-MiniLM-L6-v2) */
27
+ export const DEFAULT_EMBEDDING_DIMS = 384;
28
+
29
+ /** Known model name to embedding dimension mapping */
30
+ export const MODEL_DIMS: Record<string, number> = {
31
+ // Transformers.js models
32
+ "Xenova/all-MiniLM-L6-v2": 384,
33
+ "Xenova/all-MiniLM-L12-v2": 384,
34
+ // Ollama models
35
+ "all-minilm:l6-v2": 384,
36
+ "all-minilm": 384,
37
+ "nomic-embed-text": 768,
38
+ "mxbai-embed-large": 1024,
39
+ "snowflake-arctic-embed:s": 384,
40
+ "snowflake-arctic-embed:m": 768,
41
+ "snowflake-arctic-embed:l": 1024,
42
+ "bge-small-en-v1.5": 384,
43
+ "bge-base-en-v1.5": 768,
44
+ "bge-large-en-v1.5": 1024,
45
+ };
46
+
47
+ /**
48
+ * Resolve embedding dimensions for a model.
49
+ * Returns known dims for recognized models, DEFAULT_EMBEDDING_DIMS otherwise.
50
+ */
51
+ export function resolveModelDims(
52
+ model: string | undefined,
53
+ _provider: EmbeddingProvider,
54
+ ): number {
55
+ if (model === undefined) return DEFAULT_EMBEDDING_DIMS;
56
+ if (model in MODEL_DIMS) return MODEL_DIMS[model];
57
+ process.stderr.write(
58
+ `[cf-memory] Unknown embedding model "${model}" — assuming ${DEFAULT_EMBEDDING_DIMS} dims\n`,
59
+ );
60
+ return DEFAULT_EMBEDDING_DIMS;
61
+ }
62
+
63
+ /**
64
+ * Compute a content hash for embedding cache lookups.
65
+ */
66
+ export function contentHash(text: string): string {
67
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
68
+ }
69
+
70
+ /**
71
+ * Prepare text for embedding: combine title + description + tags + content.
72
+ */
73
+ export function prepareEmbeddingText(fields: {
74
+ title: string;
75
+ description: string;
76
+ tags: string[];
77
+ content: string;
78
+ }): string {
79
+ return [
80
+ fields.title,
81
+ fields.description,
82
+ fields.tags.join(" "),
83
+ fields.content.slice(0, 2000), // Truncate long content
84
+ ]
85
+ .filter(Boolean)
86
+ .join("\n");
87
+ }
88
+
89
+ /**
90
+ * EmbeddingPipeline — lazy-loads the model and generates embeddings.
91
+ */
92
+ export class EmbeddingPipeline {
93
+ private config: EmbeddingConfig;
94
+ private pipeline: unknown = null;
95
+ private loading: Promise<void> | null = null;
96
+
97
+ constructor(config?: Partial<EmbeddingConfig>) {
98
+ this.config = {
99
+ provider: config?.provider ?? "transformers",
100
+ model: config?.model,
101
+ ollamaUrl: config?.ollamaUrl,
102
+ depsDir: config?.depsDir,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Get the expected embedding dimensions based on the configured model.
108
+ */
109
+ get dims(): number {
110
+ const model =
111
+ this.config.model ??
112
+ (this.config.provider === "ollama"
113
+ ? DEFAULT_OLLAMA_MODEL
114
+ : DEFAULT_TRANSFORMERS_MODEL);
115
+ return resolveModelDims(model, this.config.provider);
116
+ }
117
+
118
+ /**
119
+ * Get the resolved model name (configured or default for provider).
120
+ */
121
+ get modelName(): string {
122
+ if (this.config.provider === "ollama") {
123
+ return this.config.model ?? DEFAULT_OLLAMA_MODEL;
124
+ }
125
+ return this.config.model ?? DEFAULT_TRANSFORMERS_MODEL;
126
+ }
127
+
128
+ /**
129
+ * Lazy-load the embedding model.
130
+ */
131
+ private async ensureModel(): Promise<void> {
132
+ if (this.pipeline) return;
133
+ if (this.loading) {
134
+ await this.loading;
135
+ return;
136
+ }
137
+
138
+ this.loading = this.loadModel();
139
+ await this.loading;
140
+ }
141
+
142
+ private async loadModel(): Promise<void> {
143
+ if (this.config.provider === "ollama") {
144
+ // Ollama doesn't need model preloading — it's server-side
145
+ this.pipeline = "ollama";
146
+ return;
147
+ }
148
+
149
+ // Load Transformers.js from lazy-installed deps
150
+ const { loadDepAsync } = await import("../../lib/lazy-install.js");
151
+ const transformers = await loadDepAsync<{
152
+ pipeline: (
153
+ task: string,
154
+ model: string,
155
+ options?: Record<string, unknown>,
156
+ ) => Promise<unknown>;
157
+ }>("@huggingface/transformers", this.config.depsDir);
158
+
159
+ const model = this.config.model ?? DEFAULT_TRANSFORMERS_MODEL;
160
+
161
+ this.pipeline = await transformers.pipeline("feature-extraction", model, {
162
+ dtype: "fp32",
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Generate an embedding for the given text.
168
+ *
169
+ * Returns a Float32Array with dimensions matching the configured model.
170
+ */
171
+ async embed(text: string): Promise<Float32Array> {
172
+ await this.ensureModel();
173
+
174
+ if (this.config.provider === "ollama") {
175
+ return this.embedWithOllama(text);
176
+ }
177
+
178
+ return this.embedWithTransformers(text);
179
+ }
180
+
181
+ private async embedWithTransformers(text: string): Promise<Float32Array> {
182
+ const pipe = this.pipeline as (
183
+ text: string,
184
+ options: { pooling: string; normalize: boolean },
185
+ ) => Promise<{ data: Float32Array }>;
186
+
187
+ const result = await pipe(text, {
188
+ pooling: "mean",
189
+ normalize: true,
190
+ });
191
+
192
+ return new Float32Array(result.data);
193
+ }
194
+
195
+ private async embedWithOllama(text: string): Promise<Float32Array> {
196
+ const url = this.config.ollamaUrl ?? DEFAULT_OLLAMA_URL;
197
+ const model = this.config.model ?? DEFAULT_OLLAMA_MODEL;
198
+
199
+ const response = await fetch(`${url}/api/embeddings`, {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/json" },
202
+ body: JSON.stringify({ model, prompt: text }),
203
+ });
204
+
205
+ if (!response.ok) {
206
+ throw new Error(`Ollama embedding failed: ${response.statusText}`);
207
+ }
208
+
209
+ const data = (await response.json()) as { embedding: number[] };
210
+ return new Float32Array(data.embedding);
211
+ }
212
+
213
+ /**
214
+ * Check if Ollama is available at the configured URL.
215
+ */
216
+ async isOllamaAvailable(): Promise<boolean> {
217
+ const url = this.config.ollamaUrl ?? DEFAULT_OLLAMA_URL;
218
+ try {
219
+ const response = await fetch(`${url}/api/tags`, {
220
+ signal: AbortSignal.timeout(2000),
221
+ });
222
+ return response.ok;
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Embedding cache operations — backed by SQLite.
231
+ */
232
+ export class EmbeddingCache {
233
+ constructor(private db: DatabaseLike) {}
234
+
235
+ /**
236
+ * Get a cached embedding by content hash.
237
+ */
238
+ get(hash: string, dims: number): Float32Array | null {
239
+ const row = this.db
240
+ .prepare("SELECT embedding FROM embedding_cache WHERE content_hash = ?")
241
+ .get(hash) as { embedding: Buffer } | undefined;
242
+
243
+ if (!row) return null;
244
+ // Copy to a fresh Buffer to avoid alignment issues with pooled ArrayBuffers
245
+ const buf = Buffer.from(row.embedding);
246
+ // Validate buffer size matches requested dims (stale cache from model change)
247
+ if (buf.byteLength < dims * Float32Array.BYTES_PER_ELEMENT) return null;
248
+ return new Float32Array(buf.buffer, buf.byteOffset, dims);
249
+ }
250
+
251
+ /**
252
+ * Store an embedding in the cache.
253
+ */
254
+ set(hash: string, embedding: Float32Array, model: string): void {
255
+ // Use byteOffset + byteLength to handle Float32Array views correctly
256
+ const buffer = Buffer.from(
257
+ embedding.buffer,
258
+ embedding.byteOffset,
259
+ embedding.byteLength,
260
+ );
261
+ this.db
262
+ .prepare(
263
+ "INSERT OR REPLACE INTO embedding_cache (content_hash, embedding, model, created) VALUES (?, ?, ?, ?)",
264
+ )
265
+ .run(hash, buffer, model, new Date().toISOString());
266
+ }
267
+
268
+ /**
269
+ * Check if a hash exists in the cache.
270
+ */
271
+ has(hash: string): boolean {
272
+ const row = this.db
273
+ .prepare("SELECT 1 FROM embedding_cache WHERE content_hash = ? LIMIT 1")
274
+ .get(hash);
275
+ return !!row;
276
+ }
277
+
278
+ /**
279
+ * Remove an entry from the cache.
280
+ */
281
+ delete(hash: string): void {
282
+ this.db
283
+ .prepare("DELETE FROM embedding_cache WHERE content_hash = ?")
284
+ .run(hash);
285
+ }
286
+ }