claude-alfred 0.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.
Files changed (44) hide show
  1. package/README.ja.md +283 -0
  2. package/README.md +283 -0
  3. package/content/hooks/hooks.json +41 -0
  4. package/content/mcp/.mcp.json +12 -0
  5. package/dist/audit-DujZ6YAy.mjs +18 -0
  6. package/dist/cli.mjs +509 -0
  7. package/dist/dispatcher-BzOdcjaa.mjs +93 -0
  8. package/dist/embedder-BshPIMrW.mjs +215 -0
  9. package/dist/epic-CdRKNGvP.mjs +227 -0
  10. package/dist/fts-BDdUbNfM.mjs +195 -0
  11. package/dist/helpers-BsdW4kgn.mjs +94 -0
  12. package/dist/knowledge-CCCixwb8.mjs +156 -0
  13. package/dist/post-tool-qemgso2b.mjs +88 -0
  14. package/dist/postinstall.mjs +49 -0
  15. package/dist/pre-compact-Cmg9kprV.mjs +181 -0
  16. package/dist/project-CpgK3fwQ.mjs +79 -0
  17. package/dist/schema-CcIFwr_0.mjs +289 -0
  18. package/dist/server-DF7CXxKi.mjs +2635 -0
  19. package/dist/server-Dsf47Pd4.mjs +19220 -0
  20. package/dist/session-start-DUYF6E0V.mjs +209 -0
  21. package/dist/store-Clcihees.mjs +338 -0
  22. package/dist/types-C3butmI8.mjs +6823 -0
  23. package/dist/user-prompt-BDeST0mR.mjs +144 -0
  24. package/dist/vectors-DvuAqDeO.mjs +83 -0
  25. package/package.json +46 -0
  26. package/web/dist/assets/activity-UyW12k7Z.js +1 -0
  27. package/web/dist/assets/api-BI8AW-mC.js +1 -0
  28. package/web/dist/assets/dist-BHj_gZG8.js +1 -0
  29. package/web/dist/assets/dist-DDZSXOC-.js +1 -0
  30. package/web/dist/assets/index-B9C85vN2.js +10 -0
  31. package/web/dist/assets/index-bIyYMf1a.css +1 -0
  32. package/web/dist/assets/knowledge-DmvXTX67.js +5 -0
  33. package/web/dist/assets/link-BSgD_zxQ.js +1 -0
  34. package/web/dist/assets/matchContext-CO01nzZ3.js +1 -0
  35. package/web/dist/assets/progress-DBmt_Ww6.js +6 -0
  36. package/web/dist/assets/routes-zEN1XNFl.js +1 -0
  37. package/web/dist/assets/scroll-area-DPCDB42s.js +45 -0
  38. package/web/dist/assets/separator-5sy8HYz5.js +1 -0
  39. package/web/dist/assets/skeleton-D7GRd6oJ.js +1 -0
  40. package/web/dist/assets/tabs-VSkG1f0-.js +1 -0
  41. package/web/dist/assets/tasks-CKNc1U7M.js +1 -0
  42. package/web/dist/assets/tasks._slug-DPzi78wf.js +8 -0
  43. package/web/dist/assets/utils-Dw49HYRP.js +1 -0
  44. package/web/dist/index.html +17 -0
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ //#region src/embedder/voyage.ts
3
+ const DEFAULT_API_URL = "https://api.voyageai.com/v1/embeddings";
4
+ const DEFAULT_RERANK_URL = "https://api.voyageai.com/v1/rerank";
5
+ const DEFAULT_MODEL = "voyage-4-large";
6
+ const DEFAULT_RERANK_MODEL = "rerank-2.5";
7
+ const DEFAULT_DIMS = 2048;
8
+ function envOr(key, fallback) {
9
+ return process.env[key] || fallback;
10
+ }
11
+ function envIntOr(key, fallback) {
12
+ const v = process.env[key];
13
+ if (v) {
14
+ const n = parseInt(v, 10);
15
+ if (!isNaN(n) && n > 0) return n;
16
+ }
17
+ return fallback;
18
+ }
19
+ var VoyageError = class extends Error {
20
+ status;
21
+ detail;
22
+ raw;
23
+ errResp;
24
+ constructor(status, detail, raw, errResp) {
25
+ super(status === 401 || status === 403 ? `embedder: voyage returned ${status}: authentication failed (check VOYAGE_API_KEY)` : `embedder: voyage returned ${status}: ${detail}`);
26
+ this.status = status;
27
+ this.detail = detail;
28
+ this.raw = raw;
29
+ this.errResp = errResp;
30
+ }
31
+ };
32
+ const TRANSIENT_ERROR_TYPES = new Set([
33
+ "temporary_error",
34
+ "transient_error",
35
+ "overloaded",
36
+ "rate_limited",
37
+ "capacity_exceeded",
38
+ "service_unavailable"
39
+ ]);
40
+ const TRANSIENT_MESSAGE_PATTERNS = [
41
+ "request to model",
42
+ "model is overloaded",
43
+ "temporarily",
44
+ "try again",
45
+ "service unavailable",
46
+ "internal server error",
47
+ "over capacity"
48
+ ];
49
+ function isVoyageTransient(detail) {
50
+ const lower = detail.toLowerCase();
51
+ return TRANSIENT_MESSAGE_PATTERNS.some((p) => lower.includes(p));
52
+ }
53
+ function isVoyageTransientStructured(errResp) {
54
+ if (!errResp) return false;
55
+ if (errResp.type && TRANSIENT_ERROR_TYPES.has(errResp.type.toLowerCase())) return true;
56
+ if (errResp.code && TRANSIENT_ERROR_TYPES.has(errResp.code.toLowerCase())) return true;
57
+ return false;
58
+ }
59
+ async function retryVoyage(fn, signal) {
60
+ let lastErr;
61
+ for (let attempt = 0; attempt < 3; attempt++) {
62
+ if (attempt > 0) {
63
+ const delay = (1 << attempt) * 1e3;
64
+ await new Promise((resolve, reject) => {
65
+ const onAbort = () => {
66
+ clearTimeout(timer);
67
+ reject(signal.reason);
68
+ };
69
+ const timer = setTimeout(() => {
70
+ signal?.removeEventListener("abort", onAbort);
71
+ resolve();
72
+ }, delay);
73
+ signal?.addEventListener("abort", onAbort, { once: true });
74
+ });
75
+ }
76
+ try {
77
+ return await fn();
78
+ } catch (err) {
79
+ lastErr = err;
80
+ if (err instanceof VoyageError) {
81
+ if (err.status === 429 || err.status >= 500) continue;
82
+ if (err.status === 400 && (isVoyageTransient(err.detail) || isVoyageTransientStructured(err.errResp))) continue;
83
+ throw err;
84
+ }
85
+ continue;
86
+ }
87
+ }
88
+ throw lastErr;
89
+ }
90
+ var VoyageClient = class {
91
+ apiKey;
92
+ apiURL;
93
+ rerankURL;
94
+ model;
95
+ rerankModel;
96
+ dims;
97
+ constructor(apiKey) {
98
+ this.apiKey = apiKey;
99
+ this.apiURL = envOr("VOYAGE_API_URL", DEFAULT_API_URL);
100
+ this.rerankURL = envOr("VOYAGE_RERANK_API_URL", DEFAULT_RERANK_URL);
101
+ this.model = envOr("VOYAGE_MODEL", DEFAULT_MODEL);
102
+ this.rerankModel = envOr("VOYAGE_RERANK_MODEL", DEFAULT_RERANK_MODEL);
103
+ this.dims = envIntOr("VOYAGE_DIMS", DEFAULT_DIMS);
104
+ }
105
+ async embed(texts, inputType, signal) {
106
+ const payload = {
107
+ input: texts,
108
+ model: this.model,
109
+ input_type: inputType,
110
+ output_dimension: this.dims
111
+ };
112
+ return retryVoyage(() => this.doEmbed(payload, signal), signal);
113
+ }
114
+ async doEmbed(payload, signal) {
115
+ const resp = await fetch(this.apiURL, {
116
+ method: "POST",
117
+ headers: {
118
+ "Content-Type": "application/json",
119
+ "Authorization": `Bearer ${this.apiKey}`
120
+ },
121
+ body: JSON.stringify(payload),
122
+ signal: signal ?? AbortSignal.timeout(3e4)
123
+ });
124
+ if (!resp.ok) {
125
+ const raw = await resp.text().catch(() => "");
126
+ let errResp;
127
+ try {
128
+ const parsed = JSON.parse(raw);
129
+ if (parsed.detail) errResp = parsed;
130
+ } catch {}
131
+ throw new VoyageError(resp.status, errResp?.detail ?? raw, raw, errResp);
132
+ }
133
+ return (await resp.json()).data.map((d) => d.embedding);
134
+ }
135
+ async embedForSearch(query, signal) {
136
+ const vecs = await this.embed([query], "query", signal);
137
+ if (vecs.length === 0) throw new Error("embedder: no embeddings returned");
138
+ return vecs[0];
139
+ }
140
+ async embedForStorage(text, signal) {
141
+ const vecs = await this.embed([text], "document", signal);
142
+ if (vecs.length === 0) throw new Error("embedder: no embeddings returned");
143
+ return vecs[0];
144
+ }
145
+ async rerank(query, documents, topK, signal) {
146
+ const payload = {
147
+ query,
148
+ documents,
149
+ model: this.rerankModel,
150
+ top_k: topK,
151
+ return_documents: false
152
+ };
153
+ return retryVoyage(() => this.doRerank(payload, signal), signal);
154
+ }
155
+ async doRerank(payload, signal) {
156
+ const resp = await fetch(this.rerankURL, {
157
+ method: "POST",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ "Authorization": `Bearer ${this.apiKey}`
161
+ },
162
+ body: JSON.stringify(payload),
163
+ signal: signal ?? AbortSignal.timeout(3e4)
164
+ });
165
+ if (!resp.ok) {
166
+ const raw = await resp.text().catch(() => "");
167
+ let errResp;
168
+ try {
169
+ const parsed = JSON.parse(raw);
170
+ if (parsed.detail) errResp = parsed;
171
+ } catch {}
172
+ throw new VoyageError(resp.status, errResp?.detail ?? raw, raw, errResp);
173
+ }
174
+ return (await resp.json()).data.map((d) => ({
175
+ index: d.index,
176
+ relevanceScore: d.relevance_score
177
+ }));
178
+ }
179
+ };
180
+ //#endregion
181
+ //#region src/embedder/index.ts
182
+ var Embedder = class Embedder {
183
+ client;
184
+ constructor(client) {
185
+ this.client = client;
186
+ }
187
+ static create() {
188
+ const apiKey = process.env["VOYAGE_API_KEY"];
189
+ if (!apiKey) throw new Error("VOYAGE_API_KEY is required but not set (get a key at https://dash.voyageai.com/)");
190
+ return new Embedder(new VoyageClient(apiKey));
191
+ }
192
+ get dims() {
193
+ return this.client.dims;
194
+ }
195
+ get model() {
196
+ return this.client.model;
197
+ }
198
+ async embedForSearch(query, signal) {
199
+ return this.client.embedForSearch(query, signal);
200
+ }
201
+ async embedForStorage(text, signal) {
202
+ return this.client.embedForStorage(text, signal);
203
+ }
204
+ async embedBatchForStorage(texts, signal) {
205
+ return this.client.embed(texts, "document", signal);
206
+ }
207
+ async validate(signal) {
208
+ await this.client.embed(["test"], "query", signal);
209
+ }
210
+ async rerank(query, documents, topK, signal) {
211
+ return this.client.rerank(query, documents, topK, signal);
212
+ }
213
+ };
214
+ //#endregion
215
+ export { Embedder };
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { n as VALID_SLUG, p as require_dist } from "./types-C3butmI8.mjs";
3
+ import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ //#region src/epic/index.ts
6
+ var import_dist = require_dist();
7
+ function epicsDir(projectPath) {
8
+ return join(projectPath, ".alfred", "epics");
9
+ }
10
+ function epicActivePath(projectPath) {
11
+ return join(epicsDir(projectPath), "_active.yaml");
12
+ }
13
+ const STATUS_DRAFT = "draft";
14
+ const STATUS_IN_PROGRESS = "in-progress";
15
+ const STATUS_COMPLETED = "completed";
16
+ const STATUS_NOT_STARTED = "not-started";
17
+ var EpicDir = class {
18
+ projectPath;
19
+ slug;
20
+ constructor(projectPath, slug) {
21
+ this.projectPath = projectPath;
22
+ this.slug = slug;
23
+ }
24
+ dir() {
25
+ return join(epicsDir(this.projectPath), this.slug);
26
+ }
27
+ epicPath() {
28
+ return join(this.dir(), "epic.yaml");
29
+ }
30
+ exists() {
31
+ try {
32
+ return statSync(this.dir()).isDirectory();
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ read() {
38
+ return (0, import_dist.parse)(readFileSync(this.epicPath(), "utf-8"));
39
+ }
40
+ save(ep) {
41
+ const data = (0, import_dist.stringify)(ep);
42
+ const tmp = this.epicPath() + ".tmp";
43
+ writeFileSync(tmp, data);
44
+ renameSync(tmp, this.epicPath());
45
+ }
46
+ link(taskSlug, dependsOn) {
47
+ const ep = this.read();
48
+ const tasks = ep.tasks ?? [];
49
+ if (tasks.some((t) => t.slug === taskSlug)) throw new Error(`task "${taskSlug}" already linked to epic "${this.slug}"`);
50
+ const taskSet = new Set(tasks.map((t) => t.slug));
51
+ for (const dep of dependsOn) if (!taskSet.has(dep)) throw new Error(`dependency "${dep}" not found in epic "${this.slug}"`);
52
+ tasks.push({
53
+ slug: taskSlug,
54
+ status: STATUS_NOT_STARTED,
55
+ depends_on: dependsOn.length > 0 ? dependsOn : void 0
56
+ });
57
+ ep.tasks = tasks;
58
+ if (ep.status === "draft") ep.status = STATUS_IN_PROGRESS;
59
+ this.save(ep);
60
+ }
61
+ unlink(taskSlug) {
62
+ const ep = this.read();
63
+ const tasks = ep.tasks ?? [];
64
+ const idx = tasks.findIndex((t) => t.slug === taskSlug);
65
+ if (idx === -1) throw new Error(`task "${taskSlug}" not linked to epic "${this.slug}"`);
66
+ tasks.splice(idx, 1);
67
+ for (const t of tasks) if (t.depends_on) {
68
+ t.depends_on = t.depends_on.filter((d) => d !== taskSlug);
69
+ if (t.depends_on.length === 0) t.depends_on = void 0;
70
+ }
71
+ ep.tasks = tasks;
72
+ this.save(ep);
73
+ }
74
+ progress() {
75
+ const tasks = this.read().tasks ?? [];
76
+ return {
77
+ completed: tasks.filter((t) => t.status === STATUS_COMPLETED).length,
78
+ total: tasks.length
79
+ };
80
+ }
81
+ };
82
+ function initEpic(projectPath, slug, name) {
83
+ if (!VALID_SLUG.test(slug)) throw new Error(`invalid epic_slug "${slug}": must be lowercase alphanumeric with hyphens`);
84
+ const ed = new EpicDir(projectPath, slug);
85
+ if (ed.exists()) throw new Error(`epic already exists: ${slug}`);
86
+ mkdirSync(ed.dir(), { recursive: true });
87
+ const ep = {
88
+ name,
89
+ status: STATUS_DRAFT,
90
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
91
+ };
92
+ ed.save(ep);
93
+ let state;
94
+ try {
95
+ state = readActiveEpics(projectPath);
96
+ } catch {
97
+ state = {};
98
+ }
99
+ const epics = state.epics ?? [];
100
+ if (!epics.includes(slug)) epics.push(slug);
101
+ state.epics = epics;
102
+ if (!state.primary) state.primary = slug;
103
+ writeActiveEpics(projectPath, state);
104
+ return ed;
105
+ }
106
+ function topologicalOrder(tasks) {
107
+ const inDeg = /* @__PURE__ */ new Map();
108
+ const adj = /* @__PURE__ */ new Map();
109
+ for (const t of tasks) {
110
+ if (!inDeg.has(t.slug)) inDeg.set(t.slug, 0);
111
+ for (const dep of t.depends_on ?? []) {
112
+ adj.set(dep, [...adj.get(dep) ?? [], t.slug]);
113
+ inDeg.set(t.slug, (inDeg.get(t.slug) ?? 0) + 1);
114
+ }
115
+ }
116
+ const queue = tasks.filter((t) => (inDeg.get(t.slug) ?? 0) === 0).map((t) => t.slug).sort();
117
+ const order = [];
118
+ while (queue.length > 0) {
119
+ const cur = queue.shift();
120
+ order.push(cur);
121
+ const neighbors = (adj.get(cur) ?? []).sort();
122
+ for (const next of neighbors) {
123
+ const deg = (inDeg.get(next) ?? 1) - 1;
124
+ inDeg.set(next, deg);
125
+ if (deg === 0) queue.push(next);
126
+ }
127
+ }
128
+ if (order.length !== tasks.length) throw new Error("dependency cycle detected");
129
+ return order;
130
+ }
131
+ function nextActionable(tasks) {
132
+ const statusMap = new Map(tasks.map((t) => [t.slug, t.status]));
133
+ return tasks.filter((t) => t.status === "not-started" && (t.depends_on ?? []).every((d) => statusMap.get(d) === "completed")).map((t) => t.slug);
134
+ }
135
+ function listAllEpics(projectPath) {
136
+ const dir = epicsDir(projectPath);
137
+ let entries;
138
+ try {
139
+ entries = readdirSync(dir);
140
+ } catch {
141
+ return [];
142
+ }
143
+ const summaries = [];
144
+ for (const entry of entries) {
145
+ if (entry.startsWith("_") || entry.startsWith(".")) continue;
146
+ try {
147
+ if (!statSync(join(dir, entry)).isDirectory()) continue;
148
+ } catch {
149
+ continue;
150
+ }
151
+ const ed = new EpicDir(projectPath, entry);
152
+ try {
153
+ const ep = ed.read();
154
+ const tasks = ep.tasks ?? [];
155
+ const completed = tasks.filter((t) => t.status === STATUS_COMPLETED).length;
156
+ summaries.push({
157
+ slug: entry,
158
+ name: ep.name,
159
+ status: ep.status,
160
+ completed,
161
+ total: tasks.length,
162
+ tasks
163
+ });
164
+ } catch {
165
+ continue;
166
+ }
167
+ }
168
+ return summaries;
169
+ }
170
+ function removeEpic(projectPath, slug) {
171
+ const ed = new EpicDir(projectPath, slug);
172
+ if (!ed.exists()) throw new Error(`epic "${slug}" not found`);
173
+ rmSync(ed.dir(), {
174
+ recursive: true,
175
+ force: true
176
+ });
177
+ try {
178
+ const state = readActiveEpics(projectPath);
179
+ state.epics = (state.epics ?? []).filter((e) => e !== slug);
180
+ if (state.primary === slug) state.primary = state.epics[0] ?? "";
181
+ writeActiveEpics(projectPath, state);
182
+ } catch {}
183
+ }
184
+ function unlinkTaskFromAllEpics(projectPath, taskSlug) {
185
+ for (const s of listAllEpics(projectPath)) if (s.tasks.some((t) => t.slug === taskSlug)) {
186
+ const ed = new EpicDir(projectPath, s.slug);
187
+ try {
188
+ ed.unlink(taskSlug);
189
+ } catch {}
190
+ }
191
+ }
192
+ function syncTaskStatus(projectPath, taskSlug, newStatus) {
193
+ for (const s of listAllEpics(projectPath)) {
194
+ const task = s.tasks.find((t) => t.slug === taskSlug);
195
+ if (!task) continue;
196
+ if (task.status === newStatus) return false;
197
+ const ed = new EpicDir(projectPath, s.slug);
198
+ try {
199
+ const ep = ed.read();
200
+ const tasks = ep.tasks ?? [];
201
+ const target = tasks.find((t) => t.slug === taskSlug);
202
+ if (target) target.status = newStatus;
203
+ const allCompleted = tasks.length > 0 && tasks.every((t) => t.status === "completed");
204
+ const anyInProgress = tasks.some((t) => t.status === STATUS_IN_PROGRESS);
205
+ if (allCompleted) ep.status = STATUS_COMPLETED;
206
+ else if (anyInProgress) ep.status = STATUS_IN_PROGRESS;
207
+ ed.save(ep);
208
+ return true;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+ return false;
214
+ }
215
+ function readActiveEpics(projectPath) {
216
+ return (0, import_dist.parse)(readFileSync(epicActivePath(projectPath), "utf-8"));
217
+ }
218
+ function writeActiveEpics(projectPath, state) {
219
+ mkdirSync(epicsDir(projectPath), { recursive: true });
220
+ const data = (0, import_dist.stringify)(state);
221
+ const path = epicActivePath(projectPath);
222
+ const tmp = path + ".tmp";
223
+ writeFileSync(tmp, data);
224
+ renameSync(tmp, path);
225
+ }
226
+ //#endregion
227
+ export { removeEpic as a, unlinkTaskFromAllEpics as c, nextActionable as i, initEpic as n, syncTaskStatus as o, listAllEpics as r, topologicalOrder as s, EpicDir as t };
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import { c as mapRow, r as getKnowledgeByIDs, u as searchKnowledgeKeyword } from "./knowledge-CCCixwb8.mjs";
3
+ import { n as deserializeFloat32, t as cosineSimilarity } from "./vectors-DvuAqDeO.mjs";
4
+ //#region src/store/fts.ts
5
+ function subTypeHalfLife(subType) {
6
+ switch (subType) {
7
+ case "assumption": return 30;
8
+ case "inference": return 45;
9
+ case "general": return 60;
10
+ case "pattern": return 90;
11
+ case "decision": return 90;
12
+ case "rule": return 120;
13
+ default: return 60;
14
+ }
15
+ }
16
+ function subTypeBoost(subType) {
17
+ switch (subType) {
18
+ case "rule": return 2;
19
+ case "decision": return 1.5;
20
+ case "pattern": return 1.3;
21
+ default: return 1;
22
+ }
23
+ }
24
+ function searchKnowledgeFTS(store, query, limit) {
25
+ if (limit <= 0) limit = 10;
26
+ query = query.trim();
27
+ if (!query) return searchKnowledgeKeyword(store, "", limit);
28
+ const words = query.split(/\s+/);
29
+ let expanded;
30
+ try {
31
+ expanded = expandAliases(store, words);
32
+ } catch {
33
+ expanded = words;
34
+ }
35
+ const ftsTerms = [];
36
+ for (const w of expanded) {
37
+ const sanitized = sanitizeFTSTerm(w);
38
+ if (sanitized) ftsTerms.push(`"${sanitized}"`);
39
+ }
40
+ if (ftsTerms.length === 0) return searchKnowledgeKeyword(store, query, limit);
41
+ const ftsQuery = ftsTerms.join(" OR ");
42
+ let docs;
43
+ try {
44
+ docs = searchFTSKnowledge(store, ftsQuery, limit);
45
+ } catch {
46
+ return searchKnowledgeKeyword(store, query, limit);
47
+ }
48
+ if (docs.length < limit) {
49
+ const fuzzyDocs = fuzzySearchKnowledge(store, words, limit - docs.length, docs);
50
+ docs.push(...fuzzyDocs);
51
+ }
52
+ return docs;
53
+ }
54
+ function searchFTSKnowledge(store, ftsQuery, limit) {
55
+ return store.db.prepare(`
56
+ SELECT k.id, k.file_path, k.content_hash, k.title, k.content, k.sub_type,
57
+ k.project_remote, k.project_path, k.project_name, k.branch,
58
+ k.created_at, k.updated_at, k.hit_count, k.last_accessed, k.enabled,
59
+ bm25(knowledge_fts, 3.0, 1.0, 1.0) AS rank
60
+ FROM knowledge_fts f
61
+ JOIN knowledge_index k ON k.id = f.rowid
62
+ WHERE knowledge_fts MATCH ? AND k.enabled = 1
63
+ ORDER BY rank
64
+ LIMIT ?
65
+ `).all(ftsQuery, limit).map(mapRow);
66
+ }
67
+ function fuzzySearchKnowledge(store, queryWords, limit, exclude) {
68
+ if (limit <= 0) return [];
69
+ const excludeIds = new Set(exclude.map((d) => d.id));
70
+ const rows = store.db.prepare(`
71
+ SELECT id, file_path, content_hash, title, content, sub_type,
72
+ project_remote, project_path, project_name, branch,
73
+ created_at, updated_at, hit_count, last_accessed, enabled
74
+ FROM knowledge_index WHERE enabled = 1 LIMIT 500
75
+ `).all();
76
+ const docs = [];
77
+ for (const r of rows) {
78
+ if (excludeIds.has(r.id)) continue;
79
+ const targetWords = r.title.toLowerCase().split(/\s+/);
80
+ for (const qw of queryWords) {
81
+ let matched = false;
82
+ for (const tw of targetWords) if (fuzzyMatch(qw, tw)) {
83
+ matched = true;
84
+ break;
85
+ }
86
+ if (matched) {
87
+ docs.push(mapRow(r));
88
+ if (docs.length >= limit) return docs;
89
+ break;
90
+ }
91
+ }
92
+ }
93
+ return docs;
94
+ }
95
+ function detectKnowledgeConflicts(store, threshold = .7) {
96
+ const docs = store.db.prepare(`
97
+ SELECT e.source_id, e.vector FROM embeddings e
98
+ JOIN knowledge_index k ON k.id = e.source_id
99
+ WHERE e.source = 'knowledge' AND k.enabled = 1
100
+ LIMIT 1000
101
+ `).all().map((r) => ({
102
+ id: r.source_id,
103
+ vec: deserializeFloat32(r.vector)
104
+ }));
105
+ const conflicts = [];
106
+ for (let i = 0; i < docs.length; i++) for (let j = i + 1; j < docs.length; j++) {
107
+ if (docs[i].vec.length !== docs[j].vec.length) continue;
108
+ const sim = cosineSimilarity(docs[i].vec, docs[j].vec);
109
+ if (sim >= threshold) conflicts.push({
110
+ a: { id: docs[i].id },
111
+ b: { id: docs[j].id },
112
+ similarity: sim,
113
+ type: "potential_duplicate"
114
+ });
115
+ }
116
+ if (conflicts.length > 0) {
117
+ const hydrated = getKnowledgeByIDs(store, conflicts.flatMap((c) => [c.a.id, c.b.id]));
118
+ const docMap = new Map(hydrated.map((d) => [d.id, d]));
119
+ for (const conflict of conflicts) {
120
+ const a = docMap.get(conflict.a.id);
121
+ const b = docMap.get(conflict.b.id);
122
+ if (a) conflict.a = a;
123
+ if (b) conflict.b = b;
124
+ conflict.type = classifyConflict(conflict.a.content, conflict.b.content);
125
+ }
126
+ }
127
+ conflicts.sort((a, b) => b.similarity - a.similarity);
128
+ return conflicts;
129
+ }
130
+ function expandAliases(store, terms) {
131
+ if (terms.length === 0) return [];
132
+ const seen = new Set(terms.map((t) => t.toLowerCase()));
133
+ for (const t of terms) {
134
+ const lower = t.toLowerCase();
135
+ const aliasRows = store.db.prepare("SELECT alias FROM tag_aliases WHERE LOWER(tag) = ?").all(lower);
136
+ for (const r of aliasRows) seen.add(r.alias.toLowerCase());
137
+ const tagRows = store.db.prepare("SELECT tag FROM tag_aliases WHERE LOWER(alias) = ?").all(lower);
138
+ for (const r of tagRows) seen.add(r.tag.toLowerCase());
139
+ }
140
+ return [...seen];
141
+ }
142
+ function sanitizeFTSTerm(term) {
143
+ return term.replace(/["*^{}]/g, "");
144
+ }
145
+ const CONTRADICTION_PAIRS = [
146
+ ["always", "never"],
147
+ ["must", "must not"],
148
+ ["use", "avoid"],
149
+ ["enable", "disable"],
150
+ ["allow", "deny"],
151
+ ["required", "optional"],
152
+ ["do", "don't"],
153
+ ["add", "remove"],
154
+ ["include", "exclude"]
155
+ ];
156
+ function classifyConflict(contentA, contentB) {
157
+ const lowerA = contentA.toLowerCase();
158
+ const lowerB = contentB.toLowerCase();
159
+ for (const [w0, w1] of CONTRADICTION_PAIRS) {
160
+ const aHas0 = lowerA.includes(w0);
161
+ const aHas1 = lowerA.includes(w1);
162
+ const bHas0 = lowerB.includes(w0);
163
+ const bHas1 = lowerB.includes(w1);
164
+ if (aHas0 && bHas1 && !aHas1 || aHas1 && bHas0 && !bHas1) return "potential_contradiction";
165
+ }
166
+ return "potential_duplicate";
167
+ }
168
+ function levenshtein(a, b) {
169
+ const ra = [...a];
170
+ const rb = [...b];
171
+ const la = ra.length;
172
+ const lb = rb.length;
173
+ if (la === 0) return lb;
174
+ if (lb === 0) return la;
175
+ let prev = Array.from({ length: lb + 1 }, (_, j) => j);
176
+ for (let i = 1; i <= la; i++) {
177
+ const curr = new Array(lb + 1);
178
+ curr[0] = i;
179
+ for (let j = 1; j <= lb; j++) {
180
+ const cost = ra[i - 1] === rb[j - 1] ? 0 : 1;
181
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
182
+ }
183
+ prev = curr;
184
+ }
185
+ return prev[lb];
186
+ }
187
+ function fuzzyMatch(query, target) {
188
+ const qLen = [...query].length;
189
+ if (qLen < 3) return false;
190
+ let maxDist = Math.min(2, Math.floor(qLen / 3));
191
+ if (maxDist === 0) maxDist = 1;
192
+ return levenshtein(query.toLowerCase(), target.toLowerCase()) <= maxDist;
193
+ }
194
+ //#endregion
195
+ export { subTypeHalfLife as i, searchKnowledgeFTS as n, subTypeBoost as r, detectKnowledgeConflicts as t };