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.
- package/README.md +12 -0
- package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
- package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
- package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
- package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
- package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
- package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
- package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
- package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
- package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
- package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
- package/dist/index.js +78 -18
- package/dist/{init-FZ3GG53E.js → init-MF7ISADJ.js} +102 -6
- package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
- package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
- package/dist/memory-RGLM35HC.js +647 -0
- package/dist/postinstall.js +1 -1
- package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
- package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
- package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
- package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
- package/lib/cf-memory/CHANGELOG.md +25 -0
- package/lib/cf-memory/README.md +284 -0
- package/lib/cf-memory/package-lock.json +2790 -0
- package/lib/cf-memory/package.json +31 -0
- package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
- package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
- package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
- package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
- package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
- package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
- package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
- package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
- package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
- package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
- package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
- package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
- package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
- package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
- package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
- package/lib/cf-memory/src/backends/markdown.ts +318 -0
- package/lib/cf-memory/src/backends/minisearch.ts +203 -0
- package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
- package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
- package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
- package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
- package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
- package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
- package/lib/cf-memory/src/daemon/entry.ts +99 -0
- package/lib/cf-memory/src/daemon/process.ts +271 -0
- package/lib/cf-memory/src/daemon/server.ts +166 -0
- package/lib/cf-memory/src/daemon/watcher.ts +90 -0
- package/lib/cf-memory/src/index.ts +53 -0
- package/lib/cf-memory/src/lib/backend.ts +23 -0
- package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
- package/lib/cf-memory/src/lib/dedup.ts +80 -0
- package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
- package/lib/cf-memory/src/lib/ollama.ts +76 -0
- package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
- package/lib/cf-memory/src/lib/tier.ts +107 -0
- package/lib/cf-memory/src/lib/types.ts +109 -0
- package/lib/cf-memory/src/resources/index.ts +62 -0
- package/lib/cf-memory/src/server.ts +20 -0
- package/lib/cf-memory/src/tools/delete.ts +38 -0
- package/lib/cf-memory/src/tools/list.ts +38 -0
- package/lib/cf-memory/src/tools/retrieve.ts +52 -0
- package/lib/cf-memory/src/tools/search.ts +47 -0
- package/lib/cf-memory/src/tools/store.ts +70 -0
- package/lib/cf-memory/src/tools/update.ts +62 -0
- package/lib/cf-memory/tsconfig.json +15 -0
- package/lib/cf-memory/vitest.config.ts +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import type { MemoryBackend } from "./backend.js";
|
|
3
|
+
import type {
|
|
4
|
+
ListInput,
|
|
5
|
+
Memory,
|
|
6
|
+
MemoryMeta,
|
|
7
|
+
MemoryStats,
|
|
8
|
+
SearchInput,
|
|
9
|
+
SearchResult,
|
|
10
|
+
StoreInput,
|
|
11
|
+
UpdateInput,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MemoryBackend implementation that talks to the daemon over UDS.
|
|
16
|
+
*/
|
|
17
|
+
export class DaemonClient implements MemoryBackend {
|
|
18
|
+
constructor(private socketPath: string) {}
|
|
19
|
+
|
|
20
|
+
private request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const options: http.RequestOptions = {
|
|
23
|
+
socketPath: this.socketPath,
|
|
24
|
+
path,
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const req = http.request(options, (res) => {
|
|
32
|
+
let data = "";
|
|
33
|
+
res.on("data", (chunk: Buffer) => {
|
|
34
|
+
data += chunk.toString();
|
|
35
|
+
});
|
|
36
|
+
res.on("end", () => {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(data) as T;
|
|
39
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
40
|
+
reject(
|
|
41
|
+
new Error(
|
|
42
|
+
(parsed as Record<string, string>).error ??
|
|
43
|
+
`HTTP ${res.statusCode}`,
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
resolve(parsed);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
reject(new Error(`Invalid JSON response: ${data}`));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
req.on("error", (err) => {
|
|
56
|
+
reject(err);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
req.setTimeout(10000, () => {
|
|
60
|
+
req.destroy();
|
|
61
|
+
reject(new Error("Request timeout"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (body) {
|
|
65
|
+
req.write(JSON.stringify(body));
|
|
66
|
+
}
|
|
67
|
+
req.end();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async store(input: StoreInput): Promise<Memory> {
|
|
72
|
+
const result = await this.request<{
|
|
73
|
+
id: string;
|
|
74
|
+
title: string;
|
|
75
|
+
stored: boolean;
|
|
76
|
+
}>("POST", "/memory", input);
|
|
77
|
+
// Retrieve full memory after store
|
|
78
|
+
const memory = await this.retrieve(result.id);
|
|
79
|
+
if (!memory) throw new Error("Store succeeded but retrieve failed");
|
|
80
|
+
return memory;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async search(input: SearchInput): Promise<SearchResult[]> {
|
|
84
|
+
const params = new URLSearchParams();
|
|
85
|
+
params.set("query", input.query);
|
|
86
|
+
if (input.type) params.set("type", input.type);
|
|
87
|
+
if (input.tags) params.set("tags", input.tags.join(","));
|
|
88
|
+
if (input.limit) params.set("limit", String(input.limit));
|
|
89
|
+
|
|
90
|
+
return this.request<SearchResult[]>(
|
|
91
|
+
"GET",
|
|
92
|
+
`/memory/search?${params.toString()}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async retrieve(id: string): Promise<Memory | null> {
|
|
97
|
+
try {
|
|
98
|
+
return await this.request<Memory>("GET", `/memory/${id}`);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async list(input: ListInput): Promise<MemoryMeta[]> {
|
|
105
|
+
const params = new URLSearchParams();
|
|
106
|
+
if (input.type) params.set("type", input.type);
|
|
107
|
+
if (input.category) params.set("category", input.category);
|
|
108
|
+
if (input.limit) params.set("limit", String(input.limit));
|
|
109
|
+
|
|
110
|
+
const qs = params.toString();
|
|
111
|
+
return this.request<MemoryMeta[]>("GET", qs ? `/memory?${qs}` : "/memory");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async update(input: UpdateInput): Promise<Memory | null> {
|
|
115
|
+
const { id, ...body } = input;
|
|
116
|
+
try {
|
|
117
|
+
await this.request("PATCH", `/memory/${id}`, body);
|
|
118
|
+
return this.retrieve(id);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async delete(id: string): Promise<boolean> {
|
|
125
|
+
try {
|
|
126
|
+
await this.request("DELETE", `/memory/${id}`);
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async stats(): Promise<MemoryStats> {
|
|
134
|
+
return this.request<MemoryStats>("GET", "/stats");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async close(): Promise<void> {
|
|
138
|
+
// No-op — daemon keeps running
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if the daemon is reachable.
|
|
143
|
+
*/
|
|
144
|
+
async ping(): Promise<boolean> {
|
|
145
|
+
try {
|
|
146
|
+
const result = await this.request<{ status: string }>("GET", "/health");
|
|
147
|
+
return result.status === "ok";
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Trigger index rebuild on the daemon.
|
|
155
|
+
*/
|
|
156
|
+
async rebuild(): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
await this.request<{ rebuilt: boolean }>("POST", "/rebuild");
|
|
159
|
+
} catch {
|
|
160
|
+
// Rebuild failed or not supported
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { MemoryBackend } from "./backend.js";
|
|
2
|
+
import type { StoreInput } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface DedupResult {
|
|
5
|
+
isDuplicate: boolean;
|
|
6
|
+
similarId?: string;
|
|
7
|
+
similarity: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface DedupOptions {
|
|
11
|
+
threshold?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function textSimilarity(a: string, b: string): number {
|
|
15
|
+
const tokenize = (s: string): Set<string> =>
|
|
16
|
+
new Set(
|
|
17
|
+
s
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.split(/\s+/)
|
|
20
|
+
.filter((t) => t.length > 0),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const setA = tokenize(a);
|
|
24
|
+
const setB = tokenize(b);
|
|
25
|
+
|
|
26
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
27
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
28
|
+
|
|
29
|
+
let intersection = 0;
|
|
30
|
+
for (const token of setA) {
|
|
31
|
+
if (setB.has(token)) intersection++;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const union = new Set([...setA, ...setB]).size;
|
|
35
|
+
return intersection / union;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function checkDuplicate(
|
|
39
|
+
backend: MemoryBackend,
|
|
40
|
+
input: StoreInput,
|
|
41
|
+
options?: DedupOptions,
|
|
42
|
+
): Promise<DedupResult> {
|
|
43
|
+
const searchScoreThreshold = options?.threshold ?? 0.8;
|
|
44
|
+
const titleThreshold = 0.85;
|
|
45
|
+
|
|
46
|
+
// Search by title only — more reliable across all backend tiers
|
|
47
|
+
// (MarkdownBackend uses substring matching, composite queries miss)
|
|
48
|
+
const results = await backend.search({ query: input.title, limit: 5 });
|
|
49
|
+
|
|
50
|
+
let maxSimilarity = 0;
|
|
51
|
+
const inputText = input.title + " " + input.description;
|
|
52
|
+
|
|
53
|
+
for (const result of results) {
|
|
54
|
+
const resultText =
|
|
55
|
+
result.memory.frontmatter.title +
|
|
56
|
+
" " +
|
|
57
|
+
result.memory.frontmatter.description;
|
|
58
|
+
|
|
59
|
+
const titleSim = textSimilarity(
|
|
60
|
+
input.title,
|
|
61
|
+
result.memory.frontmatter.title,
|
|
62
|
+
);
|
|
63
|
+
const overallSim = textSimilarity(inputText, resultText);
|
|
64
|
+
|
|
65
|
+
const combinedSim = Math.max(titleSim, overallSim);
|
|
66
|
+
if (combinedSim > maxSimilarity) {
|
|
67
|
+
maxSimilarity = combinedSim;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (result.score > searchScoreThreshold && titleSim > titleThreshold) {
|
|
71
|
+
return {
|
|
72
|
+
isDuplicate: true,
|
|
73
|
+
similarId: result.memory.id,
|
|
74
|
+
similarity: combinedSim,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { isDuplicate: false, similarity: maxSimilarity };
|
|
80
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy dependency installer for Phase 3 (SQLite + Hybrid Search).
|
|
3
|
+
*
|
|
4
|
+
* Heavy deps (better-sqlite3, sqlite-vec, @huggingface/transformers) are installed
|
|
5
|
+
* into ~/.coding-friend/memory/node_modules/ — NOT bundled with the CLI package.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
|
|
12
|
+
/** Dependencies required for Tier 1 (SQLite + Hybrid Search) */
|
|
13
|
+
export const SQLITE_DEPS: Record<string, string> = {
|
|
14
|
+
"better-sqlite3": "^11.0.0",
|
|
15
|
+
"sqlite-vec": "^0.1.6",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const EMBEDDING_DEPS: Record<string, string> = {
|
|
19
|
+
"@huggingface/transformers": "^3.4.0",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const ALL_DEPS = { ...SQLITE_DEPS, ...EMBEDDING_DEPS };
|
|
23
|
+
|
|
24
|
+
export interface DepsManifest {
|
|
25
|
+
version: number;
|
|
26
|
+
installed: Record<string, string>;
|
|
27
|
+
installedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MANIFEST_VERSION = 1;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the base directory for lazy-installed deps.
|
|
34
|
+
*/
|
|
35
|
+
export function getDepsDir(): string {
|
|
36
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
37
|
+
return path.join(home, ".coding-friend", "memory");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the path to the deps manifest file.
|
|
42
|
+
*/
|
|
43
|
+
export function getManifestPath(depsDir?: string): string {
|
|
44
|
+
return path.join(depsDir ?? getDepsDir(), "deps.json");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read the current deps manifest.
|
|
49
|
+
*/
|
|
50
|
+
export function readManifest(depsDir?: string): DepsManifest | null {
|
|
51
|
+
const manifestPath = getManifestPath(depsDir);
|
|
52
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write the deps manifest.
|
|
62
|
+
*/
|
|
63
|
+
function writeManifest(manifest: DepsManifest, depsDir?: string): void {
|
|
64
|
+
const dir = depsDir ?? getDepsDir();
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
fs.writeFileSync(getManifestPath(dir), JSON.stringify(manifest, null, 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a specific set of deps are installed and up-to-date.
|
|
71
|
+
*/
|
|
72
|
+
export function areDepsInstalled(
|
|
73
|
+
deps: Record<string, string>,
|
|
74
|
+
depsDir?: string,
|
|
75
|
+
): boolean {
|
|
76
|
+
const dir = depsDir ?? getDepsDir();
|
|
77
|
+
const manifest = readManifest(dir);
|
|
78
|
+
if (!manifest || manifest.version !== MANIFEST_VERSION) return false;
|
|
79
|
+
|
|
80
|
+
for (const [pkg, version] of Object.entries(deps)) {
|
|
81
|
+
const installedVersion = manifest.installed[pkg];
|
|
82
|
+
if (!installedVersion) return false;
|
|
83
|
+
|
|
84
|
+
// Semver caret range check:
|
|
85
|
+
// ^x.y.z (x>0): same major → ^11.0.0 accepts 11.x.x
|
|
86
|
+
// ^0.y.z (y>0): same major+minor → ^0.1.6 accepts 0.1.x
|
|
87
|
+
// ^0.0.z: exact → ^0.0.3 accepts only 0.0.3
|
|
88
|
+
const requiredParts = version.replace(/^\^/, "").split(".");
|
|
89
|
+
const installedParts = installedVersion.split(".");
|
|
90
|
+
|
|
91
|
+
if (requiredParts[0] !== "0") {
|
|
92
|
+
// Major > 0: check major version matches
|
|
93
|
+
if (requiredParts[0] !== installedParts[0]) return false;
|
|
94
|
+
} else if (requiredParts[1] !== "0") {
|
|
95
|
+
// ^0.y.z: check major + minor match
|
|
96
|
+
if (requiredParts[0] !== installedParts[0]) return false;
|
|
97
|
+
if (requiredParts[1] !== installedParts[1]) return false;
|
|
98
|
+
} else {
|
|
99
|
+
// ^0.0.z: exact match required
|
|
100
|
+
if (installedVersion !== version.replace(/^\^/, "")) return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Also check that the module actually exists on disk
|
|
104
|
+
const modulePath = path.join(dir, "node_modules", pkg);
|
|
105
|
+
if (!fs.existsSync(modulePath)) return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if SQLite deps are available (quick check for tier detection).
|
|
113
|
+
*/
|
|
114
|
+
export function areSqliteDepsAvailable(depsDir?: string): boolean {
|
|
115
|
+
return areDepsInstalled(SQLITE_DEPS, depsDir);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if embedding deps are available.
|
|
120
|
+
*/
|
|
121
|
+
export function areEmbeddingDepsAvailable(depsDir?: string): boolean {
|
|
122
|
+
return areDepsInstalled(EMBEDDING_DEPS, depsDir);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface InstallOptions {
|
|
126
|
+
onProgress?: (message: string) => void;
|
|
127
|
+
depsDir?: string;
|
|
128
|
+
sqliteOnly?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ensure all required deps are installed. Installs missing ones.
|
|
133
|
+
*
|
|
134
|
+
* Returns true if all deps are available after this call.
|
|
135
|
+
*/
|
|
136
|
+
export async function ensureDeps(opts?: InstallOptions): Promise<boolean> {
|
|
137
|
+
const dir = opts?.depsDir ?? getDepsDir();
|
|
138
|
+
const deps = opts?.sqliteOnly ? SQLITE_DEPS : ALL_DEPS;
|
|
139
|
+
const progress = opts?.onProgress ?? (() => {});
|
|
140
|
+
|
|
141
|
+
if (areDepsInstalled(deps, dir)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
progress("Checking build tools...");
|
|
146
|
+
|
|
147
|
+
// Verify npm is available
|
|
148
|
+
try {
|
|
149
|
+
execFileSync("npm", ["--version"], { stdio: "ignore" });
|
|
150
|
+
} catch {
|
|
151
|
+
progress("Error: npm not found. Please install Node.js >= 18.");
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create directory structure
|
|
156
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
157
|
+
|
|
158
|
+
// Create a minimal package.json if it doesn't exist
|
|
159
|
+
const pkgPath = path.join(dir, "package.json");
|
|
160
|
+
if (!fs.existsSync(pkgPath)) {
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
pkgPath,
|
|
163
|
+
JSON.stringify(
|
|
164
|
+
{
|
|
165
|
+
name: "cf-memory-deps",
|
|
166
|
+
version: "1.0.0",
|
|
167
|
+
private: true,
|
|
168
|
+
description: "Lazy-installed dependencies for cf-memory Tier 1",
|
|
169
|
+
},
|
|
170
|
+
null,
|
|
171
|
+
2,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Build npm install args: ["install", "--save", "pkg1@ver1", "pkg2@ver2"]
|
|
177
|
+
const installArgs = ["install", "--save"];
|
|
178
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
179
|
+
installArgs.push(`${name}@${version}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
progress(`Installing dependencies: ${Object.keys(deps).join(", ")}...`);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
execFileSync("npm", installArgs, {
|
|
186
|
+
cwd: dir,
|
|
187
|
+
stdio: "pipe",
|
|
188
|
+
timeout: 300_000, // 5 minutes
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const message =
|
|
192
|
+
err instanceof Error ? err.message : "Unknown installation error";
|
|
193
|
+
progress(`Installation failed: ${message}`);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Read actual installed versions from node_modules
|
|
198
|
+
const installed: Record<string, string> = {};
|
|
199
|
+
for (const pkg of Object.keys(deps)) {
|
|
200
|
+
try {
|
|
201
|
+
const pkgJsonPath = path.join(dir, "node_modules", pkg, "package.json");
|
|
202
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
203
|
+
installed[pkg] = pkgJson.version;
|
|
204
|
+
} catch {
|
|
205
|
+
progress(`Warning: Could not read installed version for ${pkg}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Write manifest
|
|
210
|
+
const manifest: DepsManifest = {
|
|
211
|
+
version: MANIFEST_VERSION,
|
|
212
|
+
installed,
|
|
213
|
+
installedAt: new Date().toISOString(),
|
|
214
|
+
};
|
|
215
|
+
writeManifest(manifest, dir);
|
|
216
|
+
|
|
217
|
+
progress("Dependencies installed successfully.");
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Synchronously load a lazily-installed CJS module (e.g., better-sqlite3).
|
|
223
|
+
*/
|
|
224
|
+
export function loadDepSync<T = unknown>(
|
|
225
|
+
moduleName: string,
|
|
226
|
+
depsDir?: string,
|
|
227
|
+
): T {
|
|
228
|
+
const dir = depsDir ?? getDepsDir();
|
|
229
|
+
const modulePath = path.join(dir, "node_modules", moduleName);
|
|
230
|
+
|
|
231
|
+
if (!fs.existsSync(modulePath)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Dependency "${moduleName}" not installed. Run "cf memory init" first.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const require = createRequire(path.join(dir, "package.json"));
|
|
238
|
+
return require(moduleName) as T;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Dynamically import a lazily-installed ESM module (e.g., @huggingface/transformers).
|
|
243
|
+
*/
|
|
244
|
+
export async function loadDepAsync<T = unknown>(
|
|
245
|
+
moduleName: string,
|
|
246
|
+
depsDir?: string,
|
|
247
|
+
): Promise<T> {
|
|
248
|
+
const dir = depsDir ?? getDepsDir();
|
|
249
|
+
const modulePath = path.join(dir, "node_modules", moduleName);
|
|
250
|
+
|
|
251
|
+
if (!fs.existsSync(modulePath)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Dependency "${moduleName}" not installed. Run "cf memory init" first.`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Find the main entry point from package.json
|
|
258
|
+
const pkgJsonPath = path.join(modulePath, "package.json");
|
|
259
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
260
|
+
|
|
261
|
+
// Try: exports → module → main
|
|
262
|
+
const entry =
|
|
263
|
+
typeof pkgJson.exports === "string"
|
|
264
|
+
? pkgJson.exports
|
|
265
|
+
: (pkgJson.exports?.["."]?.import ??
|
|
266
|
+
pkgJson.exports?.["."]?.default ??
|
|
267
|
+
pkgJson.exports?.["."] ??
|
|
268
|
+
pkgJson.module ??
|
|
269
|
+
pkgJson.main ??
|
|
270
|
+
"index.js");
|
|
271
|
+
|
|
272
|
+
const entryPath = path.join(modulePath, entry);
|
|
273
|
+
return (await import(entryPath)) as T;
|
|
274
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama integration for embedding generation.
|
|
3
|
+
*
|
|
4
|
+
* Provides auto-detection of Ollama availability and model listing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DEFAULT_OLLAMA_URL = process.env.OLLAMA_HOST ?? "http://localhost:11434";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if Ollama is running at the given URL.
|
|
11
|
+
*/
|
|
12
|
+
export async function isOllamaRunning(
|
|
13
|
+
url: string = DEFAULT_OLLAMA_URL,
|
|
14
|
+
): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${url}/api/tags`, {
|
|
17
|
+
signal: AbortSignal.timeout(2000),
|
|
18
|
+
});
|
|
19
|
+
return response.ok;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* List available Ollama models.
|
|
27
|
+
*/
|
|
28
|
+
export async function listOllamaModels(
|
|
29
|
+
url: string = DEFAULT_OLLAMA_URL,
|
|
30
|
+
): Promise<string[]> {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`${url}/api/tags`, {
|
|
33
|
+
signal: AbortSignal.timeout(5000),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) return [];
|
|
36
|
+
|
|
37
|
+
const data = (await response.json()) as {
|
|
38
|
+
models?: Array<{ name: string }>;
|
|
39
|
+
};
|
|
40
|
+
return data.models?.map((m) => m.name) ?? [];
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a specific embedding model is available in Ollama.
|
|
48
|
+
*/
|
|
49
|
+
export async function hasOllamaEmbeddingModel(
|
|
50
|
+
model: string = "all-minilm:l6-v2",
|
|
51
|
+
url: string = DEFAULT_OLLAMA_URL,
|
|
52
|
+
): Promise<boolean> {
|
|
53
|
+
const models = await listOllamaModels(url);
|
|
54
|
+
return models.some((m) => m.startsWith(model));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Auto-detect the best embedding provider.
|
|
59
|
+
*
|
|
60
|
+
* Prefers Ollama if available and has an embedding model,
|
|
61
|
+
* otherwise falls back to Transformers.js.
|
|
62
|
+
*/
|
|
63
|
+
export async function detectEmbeddingProvider(
|
|
64
|
+
ollamaUrl?: string,
|
|
65
|
+
): Promise<{ provider: "ollama" | "transformers"; model?: string }> {
|
|
66
|
+
const url = ollamaUrl ?? DEFAULT_OLLAMA_URL;
|
|
67
|
+
|
|
68
|
+
if (await isOllamaRunning(url)) {
|
|
69
|
+
if (await hasOllamaEmbeddingModel("all-minilm:l6-v2", url)) {
|
|
70
|
+
return { provider: "ollama", model: "all-minilm:l6-v2" };
|
|
71
|
+
}
|
|
72
|
+
// Ollama running but no embedding model — use transformers
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { provider: "transformers" };
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function applyTemporalDecay(
|
|
2
|
+
score: number,
|
|
3
|
+
updatedDate: string,
|
|
4
|
+
accessCount?: number,
|
|
5
|
+
): number {
|
|
6
|
+
const updated = new Date(updatedDate);
|
|
7
|
+
if (isNaN(updated.getTime())) return score;
|
|
8
|
+
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const ageDays = (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24);
|
|
11
|
+
|
|
12
|
+
score *= 0.7 + 0.3 * Math.exp(-ageDays / 90);
|
|
13
|
+
|
|
14
|
+
if (accessCount !== undefined) {
|
|
15
|
+
score *= 1 + 0.05 * Math.min(accessCount, 10);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return score;
|
|
19
|
+
}
|