kiro-memory 1.6.0 → 1.7.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 +105 -99
- package/package.json +14 -7
- package/plugin/dist/cli/contextkit.js +2661 -497
- package/plugin/dist/hooks/agentSpawn.js +1455 -189
- package/plugin/dist/hooks/kiro-hooks.js +1389 -156
- package/plugin/dist/hooks/postToolUse.js +1451 -174
- package/plugin/dist/hooks/stop.js +1426 -170
- package/plugin/dist/hooks/userPromptSubmit.js +1418 -170
- package/plugin/dist/index.js +1406 -172
- package/plugin/dist/sdk/index.js +1389 -155
- package/plugin/dist/servers/mcp-server.js +203 -2
- package/plugin/dist/services/search/EmbeddingService.js +363 -0
- package/plugin/dist/services/search/HybridSearch.js +703 -151
- package/plugin/dist/services/search/ScoringEngine.js +75 -0
- package/plugin/dist/services/search/VectorSearch.js +512 -0
- package/plugin/dist/services/search/index.js +776 -64
- package/plugin/dist/services/sqlite/Database.js +49 -0
- package/plugin/dist/services/sqlite/Observations.js +70 -6
- package/plugin/dist/services/sqlite/Search.js +92 -8
- package/plugin/dist/services/sqlite/Summaries.js +8 -5
- package/plugin/dist/services/sqlite/index.js +384 -18
- package/plugin/dist/types/worker-types.js +6 -0
- package/plugin/dist/viewer.js +369 -69
- package/plugin/dist/worker-service.js +1496 -148
|
@@ -41,7 +41,7 @@ var TOOLS = [
|
|
|
41
41
|
properties: {
|
|
42
42
|
query: { type: "string", description: "Text to search in observations and summaries" },
|
|
43
43
|
project: { type: "string", description: "Filter by project name (optional)" },
|
|
44
|
-
type: { type: "string", description: "Filter by observation type: file-write, command, research, tool-use (optional)" },
|
|
44
|
+
type: { type: "string", description: "Filter by observation type: file-write, command, research, tool-use, constraint, decision, heuristic, rejected (optional)" },
|
|
45
45
|
limit: { type: "number", description: "Max number of results (default: 20)" }
|
|
46
46
|
},
|
|
47
47
|
required: ["query"]
|
|
@@ -85,6 +85,77 @@ var TOOLS = [
|
|
|
85
85
|
},
|
|
86
86
|
required: ["project"]
|
|
87
87
|
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "semantic_search",
|
|
91
|
+
description: 'Semantic search using vector embeddings. Finds observations by meaning, not just keywords. E.g. searching "authentication fix" also finds "OAuth token refresh". Falls back to keyword search if embeddings are unavailable.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
query: { type: "string", description: "Natural language query for semantic search" },
|
|
96
|
+
project: { type: "string", description: "Filter by project name (optional)" },
|
|
97
|
+
limit: { type: "number", description: "Max number of results (default: 10)" }
|
|
98
|
+
},
|
|
99
|
+
required: ["query"]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "embedding_stats",
|
|
104
|
+
description: "Show embedding statistics: total observations, how many have embeddings, embedding provider info.",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {},
|
|
108
|
+
required: []
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "store_knowledge",
|
|
113
|
+
description: 'Store structured knowledge: constraints (rules), decisions (architectural choices), heuristics (soft preferences), or rejected solutions. This knowledge is boosted in search rankings and helps remember the "why" behind code decisions.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
knowledge_type: {
|
|
118
|
+
type: "string",
|
|
119
|
+
enum: ["constraint", "decision", "heuristic", "rejected"],
|
|
120
|
+
description: "Type of knowledge: constraint (hard/soft rules), decision (architectural choices with alternatives), heuristic (soft preferences), rejected (discarded solutions with reason)"
|
|
121
|
+
},
|
|
122
|
+
title: { type: "string", description: "Short descriptive title for the knowledge entry" },
|
|
123
|
+
content: { type: "string", description: "Detailed content explaining the knowledge" },
|
|
124
|
+
project: { type: "string", description: "Project name (required)" },
|
|
125
|
+
severity: { type: "string", enum: ["hard", "soft"], description: "For constraints: hard (must never violate) or soft (prefer to follow)" },
|
|
126
|
+
alternatives: { type: "array", items: { type: "string" }, description: "For decisions/rejected: alternative options considered" },
|
|
127
|
+
reason: { type: "string", description: "For decisions/rejected: why this choice was made or rejected" },
|
|
128
|
+
context: { type: "string", description: "For heuristics: when this preference applies" },
|
|
129
|
+
confidence: { type: "string", enum: ["high", "medium", "low"], description: "For heuristics: confidence level" },
|
|
130
|
+
concepts: { type: "array", items: { type: "string" }, description: "Related concepts/tags (optional)" },
|
|
131
|
+
files: { type: "array", items: { type: "string" }, description: "Related files (optional)" }
|
|
132
|
+
},
|
|
133
|
+
required: ["knowledge_type", "title", "content", "project"]
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "resume_session",
|
|
138
|
+
description: "Resume a previous coding session. Returns the checkpoint with task, progress, next steps, and relevant files from the last session on this project. Use when starting a new session to continue previous work.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
project: { type: "string", description: "Project name (optional, uses auto-detected project from environment)" },
|
|
143
|
+
session_id: { type: "number", description: "Specific session ID to resume (optional, uses latest checkpoint for the project)" }
|
|
144
|
+
},
|
|
145
|
+
required: []
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "generate_report",
|
|
150
|
+
description: "Generate an activity report for a project. Returns a markdown summary with observations, sessions, learnings, completed tasks, and file hotspots for the specified time period.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
project: { type: "string", description: "Project name (optional, uses auto-detected project)" },
|
|
155
|
+
period: { type: "string", description: 'Time period: "weekly" (default) or "monthly"' }
|
|
156
|
+
},
|
|
157
|
+
required: []
|
|
158
|
+
}
|
|
88
159
|
}
|
|
89
160
|
];
|
|
90
161
|
var handlers = {
|
|
@@ -215,6 +286,135 @@ var handlers = {
|
|
|
215
286
|
});
|
|
216
287
|
}
|
|
217
288
|
return output;
|
|
289
|
+
},
|
|
290
|
+
async semantic_search(args) {
|
|
291
|
+
const result = await callWorkerGET("/api/hybrid-search", {
|
|
292
|
+
q: args.query,
|
|
293
|
+
project: args.project || "",
|
|
294
|
+
limit: String(args.limit || 10)
|
|
295
|
+
});
|
|
296
|
+
const hits = result.results || [];
|
|
297
|
+
if (hits.length === 0) {
|
|
298
|
+
return "No semantic results found for the query.";
|
|
299
|
+
}
|
|
300
|
+
let output = `## Semantic Search: "${args.query}"
|
|
301
|
+
|
|
302
|
+
`;
|
|
303
|
+
output += `Found ${hits.length} results:
|
|
304
|
+
|
|
305
|
+
`;
|
|
306
|
+
hits.forEach((h) => {
|
|
307
|
+
const scorePercent = Math.round((h.score || 0) * 100);
|
|
308
|
+
const source = h.source || "unknown";
|
|
309
|
+
output += `- **#${h.id}** [${h.type}] ${h.title} (score: ${scorePercent}%, source: ${source})
|
|
310
|
+
`;
|
|
311
|
+
if (h.content) output += ` ${h.content.substring(0, 150)}
|
|
312
|
+
`;
|
|
313
|
+
output += "\n";
|
|
314
|
+
});
|
|
315
|
+
return output;
|
|
316
|
+
},
|
|
317
|
+
async embedding_stats() {
|
|
318
|
+
const result = await callWorkerGET("/api/embeddings/stats");
|
|
319
|
+
let output = `## Embedding Statistics
|
|
320
|
+
|
|
321
|
+
`;
|
|
322
|
+
output += `- **Total observations**: ${result.total}
|
|
323
|
+
`;
|
|
324
|
+
output += `- **With embeddings**: ${result.embedded}
|
|
325
|
+
`;
|
|
326
|
+
output += `- **Coverage**: ${result.percentage}%
|
|
327
|
+
`;
|
|
328
|
+
output += `- **Provider**: ${result.provider || "none"}
|
|
329
|
+
`;
|
|
330
|
+
output += `- **Dimensions**: ${result.dimensions}
|
|
331
|
+
`;
|
|
332
|
+
output += `- **Available**: ${result.available ? "yes" : "no"}
|
|
333
|
+
`;
|
|
334
|
+
if (result.percentage < 100 && result.total > 0) {
|
|
335
|
+
output += `
|
|
336
|
+
_Tip: Run \`kiro-memory embeddings backfill\` to generate missing embeddings._
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
return output;
|
|
340
|
+
},
|
|
341
|
+
async store_knowledge(args) {
|
|
342
|
+
const result = await callWorkerPOST("/api/knowledge", args);
|
|
343
|
+
return `Knowledge stored successfully.
|
|
344
|
+
- **ID**: ${result.id}
|
|
345
|
+
- **Type**: ${result.knowledge_type}
|
|
346
|
+
- **Title**: ${args.title}`;
|
|
347
|
+
},
|
|
348
|
+
async resume_session(args) {
|
|
349
|
+
let checkpoint;
|
|
350
|
+
if (args.session_id) {
|
|
351
|
+
checkpoint = await callWorkerGET(`/api/sessions/${args.session_id}/checkpoint`);
|
|
352
|
+
} else {
|
|
353
|
+
const project = args.project || process.env.KIRO_MEMORY_PROJECT || "";
|
|
354
|
+
if (!project) {
|
|
355
|
+
return "No project specified and unable to auto-detect. Provide a project name or session_id.";
|
|
356
|
+
}
|
|
357
|
+
checkpoint = await callWorkerGET("/api/checkpoint", { project });
|
|
358
|
+
}
|
|
359
|
+
if (!checkpoint || checkpoint.error) {
|
|
360
|
+
return "No checkpoint found. There is no previous session to resume for this project.";
|
|
361
|
+
}
|
|
362
|
+
const parts = [
|
|
363
|
+
`## Session Checkpoint \u2014 ${checkpoint.project}`,
|
|
364
|
+
`**Task**: ${checkpoint.task}`
|
|
365
|
+
];
|
|
366
|
+
if (checkpoint.progress) parts.push(`**Progress**: ${checkpoint.progress}`);
|
|
367
|
+
if (checkpoint.next_steps) parts.push(`**Next Steps**: ${checkpoint.next_steps}`);
|
|
368
|
+
if (checkpoint.open_questions) parts.push(`**Open Questions**: ${checkpoint.open_questions}`);
|
|
369
|
+
if (checkpoint.relevant_files) parts.push(`**Relevant Files**: ${checkpoint.relevant_files}`);
|
|
370
|
+
if (checkpoint.created_at) parts.push(`
|
|
371
|
+
_Checkpoint created: ${checkpoint.created_at}_`);
|
|
372
|
+
return parts.join("\n");
|
|
373
|
+
},
|
|
374
|
+
async generate_report(args) {
|
|
375
|
+
const project = args.project || process.env.KIRO_MEMORY_PROJECT || "";
|
|
376
|
+
const period = args.period === "monthly" ? "monthly" : "weekly";
|
|
377
|
+
const params = { period, format: "markdown" };
|
|
378
|
+
if (project) params.project = project;
|
|
379
|
+
const result = await callWorkerGET("/api/report", params);
|
|
380
|
+
if (typeof result === "string") return result;
|
|
381
|
+
if (result?.error) return `Report generation failed: ${result.error}`;
|
|
382
|
+
const d = result;
|
|
383
|
+
const parts = [
|
|
384
|
+
`# Activity Report \u2014 ${d.period?.label || period}`,
|
|
385
|
+
`**Period**: ${d.period?.start} \u2192 ${d.period?.end} (${d.period?.days} days)`,
|
|
386
|
+
"",
|
|
387
|
+
"## Overview",
|
|
388
|
+
`- Observations: ${d.overview?.observations || 0}`,
|
|
389
|
+
`- Summaries: ${d.overview?.summaries || 0}`,
|
|
390
|
+
`- Sessions: ${d.overview?.sessions || 0}`,
|
|
391
|
+
`- Knowledge items: ${d.overview?.knowledgeCount || 0}`
|
|
392
|
+
];
|
|
393
|
+
if (d.sessionStats?.total > 0) {
|
|
394
|
+
const pct = Math.round(d.sessionStats.completed / d.sessionStats.total * 100);
|
|
395
|
+
parts.push("", "## Sessions");
|
|
396
|
+
parts.push(`- Total: ${d.sessionStats.total} | Completed: ${d.sessionStats.completed} (${pct}%)`);
|
|
397
|
+
if (d.sessionStats.avgDurationMinutes > 0) {
|
|
398
|
+
parts.push(`- Avg duration: ${d.sessionStats.avgDurationMinutes} min`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (d.topLearnings?.length > 0) {
|
|
402
|
+
parts.push("", "## Key Learnings");
|
|
403
|
+
d.topLearnings.forEach((l) => parts.push(`- ${l}`));
|
|
404
|
+
}
|
|
405
|
+
if (d.completedTasks?.length > 0) {
|
|
406
|
+
parts.push("", "## Completed");
|
|
407
|
+
d.completedTasks.forEach((t) => parts.push(`- ${t}`));
|
|
408
|
+
}
|
|
409
|
+
if (d.nextSteps?.length > 0) {
|
|
410
|
+
parts.push("", "## Next Steps");
|
|
411
|
+
d.nextSteps.forEach((s) => parts.push(`- ${s}`));
|
|
412
|
+
}
|
|
413
|
+
if (d.fileHotspots?.length > 0) {
|
|
414
|
+
parts.push("", "## File Hotspots");
|
|
415
|
+
d.fileHotspots.slice(0, 10).forEach((f) => parts.push(`- \`${f.file}\` (${f.count}x)`));
|
|
416
|
+
}
|
|
417
|
+
return parts.join("\n");
|
|
218
418
|
}
|
|
219
419
|
};
|
|
220
420
|
async function main() {
|
|
@@ -251,8 +451,9 @@ Start the worker with: cd <kiro-memory-dir> && npm run worker:start`
|
|
|
251
451
|
isError: true
|
|
252
452
|
};
|
|
253
453
|
}
|
|
454
|
+
const safeMsg = msg.includes("Worker") ? "Worker communication error" : "Internal error processing request";
|
|
254
455
|
return {
|
|
255
|
-
content: [{ type: "text", text: `Error: ${
|
|
456
|
+
content: [{ type: "text", text: `Error: ${safeMsg}` }],
|
|
256
457
|
isError: true
|
|
257
458
|
};
|
|
258
459
|
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { createRequire } from 'module';const require = createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/utils/logger.ts
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
8
|
+
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
|
|
9
|
+
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
|
|
10
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
11
|
+
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
|
|
12
|
+
LogLevel2[LogLevel2["SILENT"] = 4] = "SILENT";
|
|
13
|
+
return LogLevel2;
|
|
14
|
+
})(LogLevel || {});
|
|
15
|
+
var DEFAULT_DATA_DIR = join(homedir(), ".contextkit");
|
|
16
|
+
var Logger = class {
|
|
17
|
+
level = null;
|
|
18
|
+
useColor;
|
|
19
|
+
logFilePath = null;
|
|
20
|
+
logFileInitialized = false;
|
|
21
|
+
constructor() {
|
|
22
|
+
this.useColor = process.stdout.isTTY ?? false;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize log file path and ensure directory exists (lazy initialization)
|
|
26
|
+
*/
|
|
27
|
+
ensureLogFileInitialized() {
|
|
28
|
+
if (this.logFileInitialized) return;
|
|
29
|
+
this.logFileInitialized = true;
|
|
30
|
+
try {
|
|
31
|
+
const logsDir = join(DEFAULT_DATA_DIR, "logs");
|
|
32
|
+
if (!existsSync(logsDir)) {
|
|
33
|
+
mkdirSync(logsDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
36
|
+
this.logFilePath = join(logsDir, `kiro-memory-${date}.log`);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("[LOGGER] Failed to initialize log file:", error);
|
|
39
|
+
this.logFilePath = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Lazy-load log level from settings file
|
|
44
|
+
*/
|
|
45
|
+
getLevel() {
|
|
46
|
+
if (this.level === null) {
|
|
47
|
+
try {
|
|
48
|
+
const settingsPath = join(DEFAULT_DATA_DIR, "settings.json");
|
|
49
|
+
if (existsSync(settingsPath)) {
|
|
50
|
+
const settingsData = readFileSync(settingsPath, "utf-8");
|
|
51
|
+
const settings = JSON.parse(settingsData);
|
|
52
|
+
const envLevel = (settings.KIRO_MEMORY_LOG_LEVEL || settings.CONTEXTKIT_LOG_LEVEL || "INFO").toUpperCase();
|
|
53
|
+
this.level = LogLevel[envLevel] ?? 1 /* INFO */;
|
|
54
|
+
} else {
|
|
55
|
+
this.level = 1 /* INFO */;
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.level = 1 /* INFO */;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return this.level;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create correlation ID for tracking an observation through the pipeline
|
|
65
|
+
*/
|
|
66
|
+
correlationId(sessionId, observationNum) {
|
|
67
|
+
return `obs-${sessionId}-${observationNum}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create session correlation ID
|
|
71
|
+
*/
|
|
72
|
+
sessionId(sessionId) {
|
|
73
|
+
return `session-${sessionId}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format data for logging - create compact summaries instead of full dumps
|
|
77
|
+
*/
|
|
78
|
+
formatData(data) {
|
|
79
|
+
if (data === null || data === void 0) return "";
|
|
80
|
+
if (typeof data === "string") return data;
|
|
81
|
+
if (typeof data === "number") return data.toString();
|
|
82
|
+
if (typeof data === "boolean") return data.toString();
|
|
83
|
+
if (typeof data === "object") {
|
|
84
|
+
if (data instanceof Error) {
|
|
85
|
+
return this.getLevel() === 0 /* DEBUG */ ? `${data.message}
|
|
86
|
+
${data.stack}` : data.message;
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(data)) {
|
|
89
|
+
return `[${data.length} items]`;
|
|
90
|
+
}
|
|
91
|
+
const keys = Object.keys(data);
|
|
92
|
+
if (keys.length === 0) return "{}";
|
|
93
|
+
if (keys.length <= 3) {
|
|
94
|
+
return JSON.stringify(data);
|
|
95
|
+
}
|
|
96
|
+
return `{${keys.length} keys: ${keys.slice(0, 3).join(", ")}...}`;
|
|
97
|
+
}
|
|
98
|
+
return String(data);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Format timestamp in local timezone (YYYY-MM-DD HH:MM:SS.mmm)
|
|
102
|
+
*/
|
|
103
|
+
formatTimestamp(date) {
|
|
104
|
+
const year = date.getFullYear();
|
|
105
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
106
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
107
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
108
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
109
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
110
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
111
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Core logging method
|
|
115
|
+
*/
|
|
116
|
+
log(level, component, message, context, data) {
|
|
117
|
+
if (level < this.getLevel()) return;
|
|
118
|
+
this.ensureLogFileInitialized();
|
|
119
|
+
const timestamp = this.formatTimestamp(/* @__PURE__ */ new Date());
|
|
120
|
+
const levelStr = LogLevel[level].padEnd(5);
|
|
121
|
+
const componentStr = component.padEnd(6);
|
|
122
|
+
let correlationStr = "";
|
|
123
|
+
if (context?.correlationId) {
|
|
124
|
+
correlationStr = `[${context.correlationId}] `;
|
|
125
|
+
} else if (context?.sessionId) {
|
|
126
|
+
correlationStr = `[session-${context.sessionId}] `;
|
|
127
|
+
}
|
|
128
|
+
let dataStr = "";
|
|
129
|
+
if (data !== void 0 && data !== null) {
|
|
130
|
+
if (data instanceof Error) {
|
|
131
|
+
dataStr = this.getLevel() === 0 /* DEBUG */ ? `
|
|
132
|
+
${data.message}
|
|
133
|
+
${data.stack}` : ` ${data.message}`;
|
|
134
|
+
} else if (this.getLevel() === 0 /* DEBUG */ && typeof data === "object") {
|
|
135
|
+
dataStr = "\n" + JSON.stringify(data, null, 2);
|
|
136
|
+
} else {
|
|
137
|
+
dataStr = " " + this.formatData(data);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let contextStr = "";
|
|
141
|
+
if (context) {
|
|
142
|
+
const { sessionId, memorySessionId, correlationId, ...rest } = context;
|
|
143
|
+
if (Object.keys(rest).length > 0) {
|
|
144
|
+
const pairs = Object.entries(rest).map(([k, v]) => `${k}=${v}`);
|
|
145
|
+
contextStr = ` {${pairs.join(", ")}}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const logLine = `[${timestamp}] [${levelStr}] [${componentStr}] ${correlationStr}${message}${contextStr}${dataStr}`;
|
|
149
|
+
if (this.logFilePath) {
|
|
150
|
+
try {
|
|
151
|
+
appendFileSync(this.logFilePath, logLine + "\n", "utf8");
|
|
152
|
+
} catch (error) {
|
|
153
|
+
process.stderr.write(`[LOGGER] Failed to write to log file: ${error}
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
process.stderr.write(logLine + "\n");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Public logging methods
|
|
161
|
+
debug(component, message, context, data) {
|
|
162
|
+
this.log(0 /* DEBUG */, component, message, context, data);
|
|
163
|
+
}
|
|
164
|
+
info(component, message, context, data) {
|
|
165
|
+
this.log(1 /* INFO */, component, message, context, data);
|
|
166
|
+
}
|
|
167
|
+
warn(component, message, context, data) {
|
|
168
|
+
this.log(2 /* WARN */, component, message, context, data);
|
|
169
|
+
}
|
|
170
|
+
error(component, message, context, data) {
|
|
171
|
+
this.log(3 /* ERROR */, component, message, context, data);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Log data flow: input → processing
|
|
175
|
+
*/
|
|
176
|
+
dataIn(component, message, context, data) {
|
|
177
|
+
this.info(component, `\u2192 ${message}`, context, data);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Log data flow: processing → output
|
|
181
|
+
*/
|
|
182
|
+
dataOut(component, message, context, data) {
|
|
183
|
+
this.info(component, `\u2190 ${message}`, context, data);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Log successful completion
|
|
187
|
+
*/
|
|
188
|
+
success(component, message, context, data) {
|
|
189
|
+
this.info(component, `\u2713 ${message}`, context, data);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Log failure
|
|
193
|
+
*/
|
|
194
|
+
failure(component, message, context, data) {
|
|
195
|
+
this.error(component, `\u2717 ${message}`, context, data);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Log timing information
|
|
199
|
+
*/
|
|
200
|
+
timing(component, message, durationMs, context) {
|
|
201
|
+
this.info(component, `\u23F1 ${message}`, context, { duration: `${durationMs}ms` });
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Happy Path Error - logs when the expected "happy path" fails but we have a fallback
|
|
205
|
+
*/
|
|
206
|
+
happyPathError(component, message, context, data, fallback = "") {
|
|
207
|
+
const stack = new Error().stack || "";
|
|
208
|
+
const stackLines = stack.split("\n");
|
|
209
|
+
const callerLine = stackLines[2] || "";
|
|
210
|
+
const callerMatch = callerLine.match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/);
|
|
211
|
+
const location = callerMatch ? `${callerMatch[1].split("/").pop()}:${callerMatch[2]}` : "unknown";
|
|
212
|
+
const enhancedContext = {
|
|
213
|
+
...context,
|
|
214
|
+
location
|
|
215
|
+
};
|
|
216
|
+
this.warn(component, `[HAPPY-PATH] ${message}`, enhancedContext, data);
|
|
217
|
+
return fallback;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var logger = new Logger();
|
|
221
|
+
|
|
222
|
+
// src/services/search/EmbeddingService.ts
|
|
223
|
+
var EmbeddingService = class {
|
|
224
|
+
provider = null;
|
|
225
|
+
model = null;
|
|
226
|
+
initialized = false;
|
|
227
|
+
initializing = null;
|
|
228
|
+
/**
|
|
229
|
+
* Inizializza il servizio di embedding.
|
|
230
|
+
* Tenta fastembed, poi @huggingface/transformers, poi fallback a null.
|
|
231
|
+
*/
|
|
232
|
+
async initialize() {
|
|
233
|
+
if (this.initialized) return this.provider !== null;
|
|
234
|
+
if (this.initializing) return this.initializing;
|
|
235
|
+
this.initializing = this._doInitialize();
|
|
236
|
+
const result = await this.initializing;
|
|
237
|
+
this.initializing = null;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
async _doInitialize() {
|
|
241
|
+
try {
|
|
242
|
+
const fastembed = await import("fastembed");
|
|
243
|
+
const EmbeddingModel = fastembed.EmbeddingModel || fastembed.default?.EmbeddingModel;
|
|
244
|
+
const FlagEmbedding = fastembed.FlagEmbedding || fastembed.default?.FlagEmbedding;
|
|
245
|
+
if (FlagEmbedding && EmbeddingModel) {
|
|
246
|
+
this.model = await FlagEmbedding.init({
|
|
247
|
+
model: EmbeddingModel.BGESmallENV15
|
|
248
|
+
});
|
|
249
|
+
this.provider = "fastembed";
|
|
250
|
+
this.initialized = true;
|
|
251
|
+
logger.info("EMBEDDING", "Inizializzato con fastembed (BGE-small-en-v1.5)");
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger.debug("EMBEDDING", `fastembed non disponibile: ${error}`);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const transformers = await import("@huggingface/transformers");
|
|
259
|
+
const pipeline = transformers.pipeline || transformers.default?.pipeline;
|
|
260
|
+
if (pipeline) {
|
|
261
|
+
this.model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
|
|
262
|
+
quantized: true
|
|
263
|
+
});
|
|
264
|
+
this.provider = "transformers";
|
|
265
|
+
this.initialized = true;
|
|
266
|
+
logger.info("EMBEDDING", "Inizializzato con @huggingface/transformers (all-MiniLM-L6-v2)");
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
logger.debug("EMBEDDING", `@huggingface/transformers non disponibile: ${error}`);
|
|
271
|
+
}
|
|
272
|
+
this.provider = null;
|
|
273
|
+
this.initialized = true;
|
|
274
|
+
logger.warn("EMBEDDING", "Nessun provider embedding disponibile, ricerca semantica disabilitata");
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Genera embedding per un singolo testo.
|
|
279
|
+
* Ritorna Float32Array con 384 dimensioni, o null se non disponibile.
|
|
280
|
+
*/
|
|
281
|
+
async embed(text) {
|
|
282
|
+
if (!this.initialized) await this.initialize();
|
|
283
|
+
if (!this.provider || !this.model) return null;
|
|
284
|
+
try {
|
|
285
|
+
const truncated = text.substring(0, 2e3);
|
|
286
|
+
if (this.provider === "fastembed") {
|
|
287
|
+
return await this._embedFastembed(truncated);
|
|
288
|
+
} else if (this.provider === "transformers") {
|
|
289
|
+
return await this._embedTransformers(truncated);
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error("EMBEDDING", `Errore generazione embedding: ${error}`);
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Genera embeddings in batch.
|
|
298
|
+
*/
|
|
299
|
+
async embedBatch(texts) {
|
|
300
|
+
if (!this.initialized) await this.initialize();
|
|
301
|
+
if (!this.provider || !this.model) return texts.map(() => null);
|
|
302
|
+
const results = [];
|
|
303
|
+
for (const text of texts) {
|
|
304
|
+
try {
|
|
305
|
+
const embedding = await this.embed(text);
|
|
306
|
+
results.push(embedding);
|
|
307
|
+
} catch {
|
|
308
|
+
results.push(null);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Verifica se il servizio è disponibile.
|
|
315
|
+
*/
|
|
316
|
+
isAvailable() {
|
|
317
|
+
return this.initialized && this.provider !== null;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Nome del provider attivo.
|
|
321
|
+
*/
|
|
322
|
+
getProvider() {
|
|
323
|
+
return this.provider;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Dimensioni del vettore embedding.
|
|
327
|
+
*/
|
|
328
|
+
getDimensions() {
|
|
329
|
+
return 384;
|
|
330
|
+
}
|
|
331
|
+
// --- Provider specifici ---
|
|
332
|
+
async _embedFastembed(text) {
|
|
333
|
+
const embeddings = this.model.embed([text], 1);
|
|
334
|
+
for await (const batch of embeddings) {
|
|
335
|
+
if (batch && batch.length > 0) {
|
|
336
|
+
const vec = batch[0];
|
|
337
|
+
return vec instanceof Float32Array ? vec : new Float32Array(vec);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
async _embedTransformers(text) {
|
|
343
|
+
const output = await this.model(text, {
|
|
344
|
+
pooling: "mean",
|
|
345
|
+
normalize: true
|
|
346
|
+
});
|
|
347
|
+
if (output?.data) {
|
|
348
|
+
return output.data instanceof Float32Array ? output.data : new Float32Array(output.data);
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
var embeddingService = null;
|
|
354
|
+
function getEmbeddingService() {
|
|
355
|
+
if (!embeddingService) {
|
|
356
|
+
embeddingService = new EmbeddingService();
|
|
357
|
+
}
|
|
358
|
+
return embeddingService;
|
|
359
|
+
}
|
|
360
|
+
export {
|
|
361
|
+
EmbeddingService,
|
|
362
|
+
getEmbeddingService
|
|
363
|
+
};
|