total-recall-brain 3.1.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/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/antigravity.mjs +212 -0
- package/bin/init.mjs +36 -0
- package/bin/oci-sniper.sh +69 -0
- package/bin/total-recall.mjs +229 -0
- package/docs/ARCHITECTURE.md +251 -0
- package/docs/IDE_INTEGRATION.md +112 -0
- package/package.json +71 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
- package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
- package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
- package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
- package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
- package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
- package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
- package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
- package/src/cli/agent-dir.mjs +105 -0
- package/src/cli/backup.mjs +313 -0
- package/src/cli/brain.mjs +224 -0
- package/src/cli/chat.mjs +74 -0
- package/src/cli/config.mjs +247 -0
- package/src/cli/connect.mjs +872 -0
- package/src/cli/daemon.mjs +228 -0
- package/src/cli/deploy-ui.mjs +1793 -0
- package/src/cli/deploy.mjs +1726 -0
- package/src/cli/doctor.mjs +254 -0
- package/src/cli/dream.mjs +80 -0
- package/src/cli/forget.mjs +92 -0
- package/src/cli/friction.mjs +21 -0
- package/src/cli/generate-pat.mjs +65 -0
- package/src/cli/hash-password.mjs +22 -0
- package/src/cli/help.mjs +153 -0
- package/src/cli/import-rules.mjs +102 -0
- package/src/cli/ingest/index.mjs +318 -0
- package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
- package/src/cli/ingest/parsers/google-keep.mjs +147 -0
- package/src/cli/ingest/parsers/search-history.mjs +142 -0
- package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
- package/src/cli/ingest/utils/dedup.mjs +69 -0
- package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
- package/src/cli/ingest.mjs +102 -0
- package/src/cli/init.mjs +736 -0
- package/src/cli/integration-dispatcher.mjs +128 -0
- package/src/cli/lint.mjs +166 -0
- package/src/cli/map.mjs +194 -0
- package/src/cli/migrate.mjs +87 -0
- package/src/cli/rebuild.mjs +88 -0
- package/src/cli/recall.mjs +219 -0
- package/src/cli/relay.mjs +518 -0
- package/src/cli/remember.mjs +306 -0
- package/src/cli/research.mjs +381 -0
- package/src/cli/reset-password.mjs +75 -0
- package/src/cli/restore.mjs +135 -0
- package/src/cli/setup.mjs +616 -0
- package/src/cli/share.mjs +180 -0
- package/src/cli/skill.mjs +313 -0
- package/src/cli/snapshot.mjs +108 -0
- package/src/cli/start.mjs +63 -0
- package/src/cli/status.mjs +296 -0
- package/src/cli/sync.mjs +383 -0
- package/src/cli/uninstall.mjs +613 -0
- package/src/cli/upgrade.mjs +141 -0
- package/src/cli/wizard.html +3682 -0
- package/src/core/blackboard.mjs +54 -0
- package/src/core/clarity-rewriter.mjs +770 -0
- package/src/core/conclusion-writer.mjs +259 -0
- package/src/core/config.mjs +277 -0
- package/src/core/conflict-detector.mjs +536 -0
- package/src/core/crypto.mjs +107 -0
- package/src/core/daemon-control.mjs +158 -0
- package/src/core/daemon-loop.mjs +364 -0
- package/src/core/dream.mjs +347 -0
- package/src/core/drift-detector.mjs +143 -0
- package/src/core/embeddings.mjs +632 -0
- package/src/core/emergency-alerts.mjs +137 -0
- package/src/core/evolution.mjs +107 -0
- package/src/core/fact-seeker.mjs +1602 -0
- package/src/core/friction.mjs +95 -0
- package/src/core/import-rules.mjs +308 -0
- package/src/core/inference-engine.mjs +367 -0
- package/src/core/logger.mjs +75 -0
- package/src/core/memory-layers.mjs +96 -0
- package/src/core/migrate.mjs +116 -0
- package/src/core/notifications.mjs +77 -0
- package/src/core/operation-validator.mjs +272 -0
- package/src/core/optimizer.mjs +330 -0
- package/src/core/pattern_detector.mjs +113 -0
- package/src/core/post-mortem.mjs +413 -0
- package/src/core/protect-instructions.mjs +148 -0
- package/src/core/quick-capture.mjs +145 -0
- package/src/core/research-queue.mjs +431 -0
- package/src/core/research.mjs +461 -0
- package/src/core/runtime.mjs +515 -0
- package/src/core/sandbox.mjs +163 -0
- package/src/core/scheduler.mjs +569 -0
- package/src/core/schema.mjs +333 -0
- package/src/core/search.mjs +219 -0
- package/src/core/semantic-index.mjs +276 -0
- package/src/core/session-watcher.mjs +850 -0
- package/src/core/snapshot.mjs +133 -0
- package/src/core/source-adapters.mjs +1038 -0
- package/src/core/steering.mjs +137 -0
- package/src/core/surface.mjs +444 -0
- package/src/core/task_runner.mjs +125 -0
- package/src/core/tts.mjs +142 -0
- package/src/core/usage-tracker.mjs +349 -0
- package/src/core/vault-cache.mjs +76 -0
- package/src/core/vault.mjs +273 -0
- package/src/core/watchdog.mjs +236 -0
- package/src/server/api.mjs +944 -0
- package/src/server/auth.mjs +410 -0
- package/src/server/index.mjs +868 -0
- package/src/server/keys.mjs +181 -0
- package/src/server/rest.mjs +1652 -0
- package/src/server/route-manifest.json +77 -0
- package/src/server/routes/_shared.mjs +93 -0
- package/src/server/routes/keys.mjs +55 -0
- package/src/server/routes/memory.mjs +249 -0
- package/src/server/routes/sessions.mjs +336 -0
- package/src/server/routes/share.mjs +138 -0
- package/src/server/tools.mjs +618 -0
- package/templates/Caddyfile +37 -0
- package/templates/com.totalrecall.backup.plist +42 -0
- package/templates/com.totalrecall.daemon.plist +40 -0
- package/templates/com.totalrecall.duckdns.plist +23 -0
- package/templates/com.totalrecall.server.plist +46 -0
- package/templates/default-config/budget.yml +6 -0
- package/templates/default-config/runtime.yml +11 -0
- package/templates/default-config/security.yml +53 -0
- package/templates/default-config/voice.yml +34 -0
- package/templates/obsidian-queries/conflicts.md +26 -0
- package/templates/obsidian-queries/daily.md +17 -0
- package/templates/obsidian-queries/index.md +38 -0
- package/templates/obsidian-queries/skills.md +14 -0
- package/templates/onboarding-interview.md +72 -0
- package/templates/skill_engineering.md +40 -0
- package/templates/total-recall-daemon.service +21 -0
- package/templates/total-recall-server.service +20 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Index — file-native vector search alongside tf-idf
|
|
3
|
+
*
|
|
4
|
+
* Generates embeddings via a local Ollama embedding model and persists them
|
|
5
|
+
* in memory-derived/embeddings.jsonl — a plain JSONL file, no database.
|
|
6
|
+
*
|
|
7
|
+
* Embedding model: nomic-embed-text (default) or any Ollama embedding model.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* - `buildSemanticIndex(nodes, derivedDir, opts)` — called by compileSurface
|
|
11
|
+
* - `semanticSearch(query, derivedDir, opts)` — called alongside tf-idf recall
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import { getEmbedding } from './embeddings.mjs';
|
|
18
|
+
|
|
19
|
+
const EMBEDDINGS_FILE = 'embeddings.jsonl';
|
|
20
|
+
|
|
21
|
+
// ─── Embedding generation ────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate an embedding vector for `text` via Google embeddings (with OpenAI fallback).
|
|
25
|
+
* Returns null if the embeddings API is unreachable or fails.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} text
|
|
28
|
+
* @param {object} [opts]
|
|
29
|
+
* @returns {Promise<number[]|null>}
|
|
30
|
+
*/
|
|
31
|
+
export async function generateEmbedding(text, opts = {}) {
|
|
32
|
+
try {
|
|
33
|
+
return await getEmbedding(text);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Cosine similarity ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export function cosineSimilarity(a, b) {
|
|
42
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
43
|
+
let dot = 0, magA = 0, magB = 0;
|
|
44
|
+
for (let i = 0; i < a.length; i++) {
|
|
45
|
+
dot += a[i] * b[i];
|
|
46
|
+
magA += a[i] * a[i];
|
|
47
|
+
magB += b[i] * b[i];
|
|
48
|
+
}
|
|
49
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
50
|
+
return denom === 0 ? 0 : dot / denom;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Index I/O ───────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load the embeddings index from disk.
|
|
57
|
+
* Returns Map<slug, number[] & { content_sha256: string }>.
|
|
58
|
+
*/
|
|
59
|
+
export function loadEmbeddingsIndex(derivedDir) {
|
|
60
|
+
const file = path.join(derivedDir, EMBEDDINGS_FILE);
|
|
61
|
+
const index = new Map();
|
|
62
|
+
if (!fs.existsSync(file)) return index;
|
|
63
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
64
|
+
if (!line.trim()) continue;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(line);
|
|
67
|
+
const { slug, embedding, content_sha256 } = parsed;
|
|
68
|
+
if (slug && Array.isArray(embedding)) {
|
|
69
|
+
const vec = embedding;
|
|
70
|
+
Object.defineProperty(vec, 'embedding', {
|
|
71
|
+
get() { return this; },
|
|
72
|
+
enumerable: false,
|
|
73
|
+
configurable: true
|
|
74
|
+
});
|
|
75
|
+
Object.defineProperty(vec, 'content_sha256', {
|
|
76
|
+
value: content_sha256 || '',
|
|
77
|
+
writable: true,
|
|
78
|
+
enumerable: false,
|
|
79
|
+
configurable: true
|
|
80
|
+
});
|
|
81
|
+
index.set(slug, vec);
|
|
82
|
+
} else if (slug && parsed.embedding === undefined && Array.isArray(parsed)) {
|
|
83
|
+
// Fallback for very old formats if any
|
|
84
|
+
const vec = parsed;
|
|
85
|
+
Object.defineProperty(vec, 'embedding', {
|
|
86
|
+
get() { return this; },
|
|
87
|
+
enumerable: false,
|
|
88
|
+
configurable: true
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(vec, 'content_sha256', {
|
|
91
|
+
value: '',
|
|
92
|
+
writable: true,
|
|
93
|
+
enumerable: false,
|
|
94
|
+
configurable: true
|
|
95
|
+
});
|
|
96
|
+
index.set(slug, vec);
|
|
97
|
+
}
|
|
98
|
+
} catch { /* skip corrupt lines */ }
|
|
99
|
+
}
|
|
100
|
+
return index;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeEmbeddingsIndex(derivedDir, index) {
|
|
104
|
+
if (!fs.existsSync(derivedDir)) fs.mkdirSync(derivedDir, { recursive: true });
|
|
105
|
+
const lines = [];
|
|
106
|
+
for (const [slug, val] of index) {
|
|
107
|
+
if (val && Array.isArray(val.embedding)) {
|
|
108
|
+
lines.push(JSON.stringify({ slug, embedding: val.embedding, content_sha256: val.content_sha256 || '' }));
|
|
109
|
+
} else if (Array.isArray(val)) {
|
|
110
|
+
lines.push(JSON.stringify({ slug, embedding: val, content_sha256: val.content_sha256 || '' }));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
fs.writeFileSync(path.join(derivedDir, EMBEDDINGS_FILE), lines.join('\n') + '\n', 'utf8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function throttledMap(items, limit, fn) {
|
|
117
|
+
const results = [];
|
|
118
|
+
const executing = new Set();
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
const p = Promise.resolve().then(() => fn(item));
|
|
121
|
+
results.push(p);
|
|
122
|
+
executing.add(p);
|
|
123
|
+
const clean = () => executing.delete(p);
|
|
124
|
+
p.then(clean, clean);
|
|
125
|
+
if (executing.size >= limit) {
|
|
126
|
+
await Promise.race(executing);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return Promise.all(results);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Index builder ───────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build (or incrementally update) the embeddings index from SSSS nodes.
|
|
136
|
+
* Re-embeds nodes if they are missing from the index OR if their content hash has changed.
|
|
137
|
+
* Skips silently if Ollama is unreachable.
|
|
138
|
+
*
|
|
139
|
+
* @param {object[]} nodes SSSS nodes with slug, title, body/content fields
|
|
140
|
+
* @param {string} derivedDir
|
|
141
|
+
* @param {object} [opts]
|
|
142
|
+
* @param {string} [opts.endpoint]
|
|
143
|
+
* @param {string} [opts.model]
|
|
144
|
+
* @returns {Promise<{ indexed: number, skipped: number, unavailable: boolean }>}
|
|
145
|
+
*/
|
|
146
|
+
export async function buildSemanticIndex(nodes, derivedDir, opts = {}) {
|
|
147
|
+
if (nodes.length === 0) {
|
|
148
|
+
return { indexed: 0, skipped: 0, unavailable: false };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const existing = loadEmbeddingsIndex(derivedDir);
|
|
152
|
+
|
|
153
|
+
// Probe availability and get current model dimension using the first node
|
|
154
|
+
const firstText = [nodes[0].title, nodes[0].body || nodes[0].content].filter(Boolean).join('\n').slice(0, 4096);
|
|
155
|
+
const probe = await generateEmbedding(firstText, opts);
|
|
156
|
+
if (!probe) {
|
|
157
|
+
return { indexed: 0, skipped: nodes.length, unavailable: true };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const newDim = probe.length;
|
|
161
|
+
|
|
162
|
+
// Check for dimension mismatch against existing cached embeddings
|
|
163
|
+
if (existing.size > 0) {
|
|
164
|
+
const firstVal = existing.values().next().value;
|
|
165
|
+
const existingDim = Array.isArray(firstVal)
|
|
166
|
+
? firstVal.length
|
|
167
|
+
: (firstVal && Array.isArray(firstVal.embedding) ? firstVal.embedding.length : 0);
|
|
168
|
+
|
|
169
|
+
if (existingDim > 0 && existingDim !== newDim) {
|
|
170
|
+
// Dimension mismatch detected! Wipe the old index to re-embed everything
|
|
171
|
+
existing.clear();
|
|
172
|
+
try {
|
|
173
|
+
const file = path.join(derivedDir, EMBEDDINGS_FILE);
|
|
174
|
+
if (fs.existsSync(file)) {
|
|
175
|
+
fs.unlinkSync(file);
|
|
176
|
+
}
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Filter nodes that need embedding
|
|
182
|
+
const toEmbed = nodes.filter(n => {
|
|
183
|
+
const cached = existing.get(n.slug);
|
|
184
|
+
if (!cached) return true;
|
|
185
|
+
|
|
186
|
+
const cachedHash = cached.content_sha256 || '';
|
|
187
|
+
const text = [n.title, n.body || n.content].filter(Boolean).join('\n').slice(0, 4096);
|
|
188
|
+
const hash = crypto.createHash('sha256').update(text).digest('hex');
|
|
189
|
+
return !cachedHash || cachedHash !== hash;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (toEmbed.length === 0) {
|
|
193
|
+
return { indexed: 0, skipped: nodes.length, unavailable: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Save the probe result we already generated for the first node if it is in toEmbed
|
|
197
|
+
const firstNode = toEmbed.find(n => n.slug === nodes[0].slug);
|
|
198
|
+
if (firstNode) {
|
|
199
|
+
const firstHash = crypto.createHash('sha256').update(firstText).digest('hex');
|
|
200
|
+
const firstVec = probe;
|
|
201
|
+
Object.defineProperty(firstVec, 'embedding', {
|
|
202
|
+
get() { return this; },
|
|
203
|
+
enumerable: false,
|
|
204
|
+
configurable: true
|
|
205
|
+
});
|
|
206
|
+
Object.defineProperty(firstVec, 'content_sha256', {
|
|
207
|
+
value: firstHash,
|
|
208
|
+
writable: true,
|
|
209
|
+
enumerable: false,
|
|
210
|
+
configurable: true
|
|
211
|
+
});
|
|
212
|
+
existing.set(firstNode.slug, firstVec);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Embed the remaining nodes concurrently
|
|
216
|
+
const remainingToEmbed = toEmbed.filter(n => n.slug !== nodes[0].slug);
|
|
217
|
+
await throttledMap(remainingToEmbed, 6, async (node) => {
|
|
218
|
+
const text = [node.title, node.body || node.content].filter(Boolean).join('\n').slice(0, 4096);
|
|
219
|
+
const vec = await generateEmbedding(text, opts);
|
|
220
|
+
if (vec) {
|
|
221
|
+
const hash = crypto.createHash('sha256').update(text).digest('hex');
|
|
222
|
+
const vecWithHash = vec;
|
|
223
|
+
Object.defineProperty(vecWithHash, 'embedding', {
|
|
224
|
+
get() { return this; },
|
|
225
|
+
enumerable: false,
|
|
226
|
+
configurable: true
|
|
227
|
+
});
|
|
228
|
+
Object.defineProperty(vecWithHash, 'content_sha256', {
|
|
229
|
+
value: hash,
|
|
230
|
+
writable: true,
|
|
231
|
+
enumerable: false,
|
|
232
|
+
configurable: true
|
|
233
|
+
});
|
|
234
|
+
existing.set(node.slug, vecWithHash);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
writeEmbeddingsIndex(derivedDir, existing);
|
|
239
|
+
const indexed = toEmbed.filter(n => existing.has(n.slug)).length;
|
|
240
|
+
return { indexed, skipped: nodes.length - toEmbed.length, unavailable: false };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Semantic search ─────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Search the embeddings index for nodes similar to `query`.
|
|
247
|
+
* Returns an array of { slug, score } sorted by descending cosine similarity.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} query
|
|
250
|
+
* @param {string} derivedDir
|
|
251
|
+
* @param {object} [opts]
|
|
252
|
+
* @param {string} [opts.endpoint]
|
|
253
|
+
* @param {string} [opts.model]
|
|
254
|
+
* @param {number} [opts.topK] Max results (default: 10)
|
|
255
|
+
* @param {number} [opts.threshold] Min cosine similarity (default: 0.5)
|
|
256
|
+
* @returns {Promise<Array<{ slug: string, score: number }>>}
|
|
257
|
+
*/
|
|
258
|
+
export async function semanticSearch(query, derivedDir, opts = {}) {
|
|
259
|
+
const topK = opts.topK ?? 10;
|
|
260
|
+
const threshold = opts.threshold ?? 0.5;
|
|
261
|
+
const index = loadEmbeddingsIndex(derivedDir);
|
|
262
|
+
if (index.size === 0) return [];
|
|
263
|
+
|
|
264
|
+
const queryVec = await generateEmbedding(query, opts);
|
|
265
|
+
if (!queryVec) return [];
|
|
266
|
+
|
|
267
|
+
const scored = [];
|
|
268
|
+
for (const [slug, val] of index) {
|
|
269
|
+
const vec = val && Array.isArray(val.embedding) ? val.embedding : (Array.isArray(val) ? val : null);
|
|
270
|
+
if (!vec) continue;
|
|
271
|
+
const score = cosineSimilarity(queryVec, vec);
|
|
272
|
+
if (score >= threshold) scored.push({ slug, score });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, topK);
|
|
276
|
+
}
|