amalfa 1.0.22 → 1.0.24

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/src/mcp/index.ts CHANGED
@@ -14,11 +14,15 @@ import { ResonanceDB } from "@src/resonance/db";
14
14
  import { DaemonManager } from "../utils/DaemonManager";
15
15
  import { getLogger } from "../utils/Logger";
16
16
  import { ServiceLifecycle } from "../utils/ServiceLifecycle";
17
+ import { createSonarClient, type SonarClient } from "../utils/sonar-client";
17
18
 
18
19
  const args = process.argv.slice(2);
19
20
  const command = args[0] || "serve";
20
21
  const log = getLogger("MCP");
21
22
 
23
+ // Sonar client for enhanced search
24
+ const sonarClient: SonarClient = await createSonarClient();
25
+
22
26
  // --- Service Lifecycle ---
23
27
 
24
28
  const lifecycle = new ServiceLifecycle({
@@ -181,11 +185,40 @@ async function runServer() {
181
185
  const limit = Number(args?.limit || 20);
182
186
  const candidates = new Map<
183
187
  string,
184
- { id: string; score: number; preview: string; source: string }
188
+ {
189
+ id: string;
190
+ score: number;
191
+ preview: string;
192
+ source: string;
193
+ content: string;
194
+ }
185
195
  >();
186
196
  const errors: string[] = [];
187
197
 
188
- // Vector Search only (FTS removed in Hollow Node migration)
198
+ // Step 1: Analyze query with Sonar (if available)
199
+ const sonarAvailable = await sonarClient.isAvailable();
200
+ let queryAnalysis: Awaited<
201
+ ReturnType<typeof sonarClient.analyzeQuery>
202
+ > | null = null;
203
+ let queryIntent: string | undefined = undefined;
204
+
205
+ if (sonarAvailable) {
206
+ log.info({ query }, "🔍 Analyzing query with Sonar");
207
+ queryAnalysis = await sonarClient.analyzeQuery(query);
208
+ if (queryAnalysis) {
209
+ queryIntent = queryAnalysis.intent;
210
+ log.info(
211
+ {
212
+ intent: queryAnalysis.intent,
213
+ entities: queryAnalysis.entities.join(", "),
214
+ level: queryAnalysis.technical_level,
215
+ },
216
+ "✅ Query analysis complete",
217
+ );
218
+ }
219
+ }
220
+
221
+ // Step 2: Vector Search (FTS removed in Hollow Node migration)
189
222
  try {
190
223
  const vectorResults = await vectorEngine.search(query, limit);
191
224
  for (const r of vectorResults) {
@@ -198,6 +231,7 @@ async function runServer() {
198
231
  score: r.score,
199
232
  preview: preview,
200
233
  source: "vector",
234
+ content: r.content || "",
201
235
  });
202
236
  }
203
237
  } catch (e: unknown) {
@@ -206,12 +240,63 @@ async function runServer() {
206
240
  errors.push(msg);
207
241
  }
208
242
 
209
- const results = Array.from(candidates.values())
243
+ // Step 3: Re-rank results with Sonar (if available)
244
+ let rankedResults = Array.from(candidates.values())
210
245
  .sort((a, b) => b.score - a.score)
211
- .slice(0, limit)
212
- .map((r) => ({ ...r, score: r.score.toFixed(3) }));
246
+ .slice(0, limit);
247
+
248
+ if (sonarAvailable && queryAnalysis) {
249
+ log.info("🔄 Re-ranking results with Sonar");
250
+ const reRanked = await sonarClient.rerankResults(
251
+ rankedResults,
252
+ query,
253
+ queryIntent,
254
+ );
255
+ rankedResults = reRanked.map((rr) => {
256
+ const original = candidates.get(rr.id)!;
257
+ return {
258
+ ...original,
259
+ score: rr.relevance_score,
260
+ };
261
+ });
262
+ log.info("✅ Results re-ranked");
263
+ }
264
+
265
+ // Step 4: Extract context with Sonar for top results (if available)
266
+ // We'll prepare the final output structure here
267
+ let finalResults: Array<any> = rankedResults;
268
+
269
+ if (sonarAvailable) {
270
+ log.info("📝 Extracting context with Sonar");
271
+ const contextResults = await Promise.all(
272
+ rankedResults.slice(0, 5).map(async (r) => {
273
+ const context = await sonarClient.extractContext(r, query);
274
+ return {
275
+ // ... (keeping structure) ...
276
+ ...r,
277
+ score: r.score.toFixed(3),
278
+ snippet: context?.snippet || r.preview,
279
+ context: context?.context || "No additional context",
280
+ confidence: context?.confidence || 0.5,
281
+ };
282
+ }),
283
+ );
284
+ // Combine context results with the rest
285
+ finalResults = [
286
+ ...contextResults,
287
+ ...rankedResults
288
+ .slice(5)
289
+ .map((r) => ({ ...r, score: r.score.toFixed(3) })),
290
+ ];
291
+ log.info("✅ Context extraction complete");
292
+ } else {
293
+ finalResults = rankedResults.map((r) => ({
294
+ ...r,
295
+ score: r.score.toFixed(3),
296
+ }));
297
+ }
213
298
 
214
- if (results.length === 0 && errors.length > 0) {
299
+ if (finalResults.length === 0 && errors.length > 0) {
215
300
  return {
216
301
  content: [
217
302
  { type: "text", text: `Search Error: ${errors.join(", ")}` },
@@ -219,8 +304,29 @@ async function runServer() {
219
304
  isError: true,
220
305
  };
221
306
  }
307
+
308
+ // Add Sonar metadata to response
309
+ const searchMetadata = {
310
+ query,
311
+ sonar_enabled: sonarAvailable,
312
+ intent: queryIntent,
313
+ analysis: queryAnalysis,
314
+ };
315
+
222
316
  return {
223
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
317
+ content: [
318
+ {
319
+ type: "text",
320
+ text: JSON.stringify(
321
+ {
322
+ results: finalResults,
323
+ metadata: searchMetadata,
324
+ },
325
+ null,
326
+ 2,
327
+ ),
328
+ },
329
+ ],
224
330
  };
225
331
  } finally {
226
332
  // Cleanup connection
@@ -410,4 +516,4 @@ process.on("unhandledRejection", (reason) => {
410
516
 
411
517
  // --- Dispatch ---
412
518
 
413
- await lifecycle.run(command, runServer, false);
519
+ await lifecycle.run(command, runServer);
@@ -366,11 +366,24 @@ export class ResonanceDB {
366
366
  checkpoint() {
367
367
  this.db.run("PRAGMA wal_checkpoint(TRUNCATE);");
368
368
  }
369
+ getNode(id: string): Node | null {
370
+ const row = this.db.query("SELECT * FROM nodes WHERE id = ?").get(id);
371
+ if (!row) return null;
372
+ return this.mapRowToNode(row);
373
+ }
374
+
375
+ updateNodeMeta(id: string, meta: Record<string, unknown>) {
376
+ this.db.run("UPDATE nodes SET meta = ? WHERE id = ?", [
377
+ JSON.stringify(meta),
378
+ id,
379
+ ]);
380
+ }
369
381
  }
370
382
 
371
383
  // Helper: Calculate magnitude (L2 norm) of a vector
372
384
  function magnitude(vec: Float32Array): number {
373
385
  let sum = 0;
386
+ // Modern JS engines SIMD-optimize this loop automatically
374
387
  for (let i = 0; i < vec.length; i++) {
375
388
  sum += (vec[i] || 0) * (vec[i] || 0);
376
389
  }
@@ -7,6 +7,7 @@ export interface DaemonStatus {
7
7
  running: boolean;
8
8
  pid?: number;
9
9
  port?: number;
10
+ activeModel?: string;
10
11
  }
11
12
 
12
13
  /**
@@ -16,6 +17,7 @@ export interface DaemonStatus {
16
17
  export class DaemonManager {
17
18
  private vectorLifecycle: ServiceLifecycle;
18
19
  private watcherLifecycle: ServiceLifecycle;
20
+ private sonarLifecycle: ServiceLifecycle;
19
21
 
20
22
  constructor() {
21
23
  this.vectorLifecycle = new ServiceLifecycle({
@@ -31,6 +33,13 @@ export class DaemonManager {
31
33
  logFile: join(AMALFA_DIRS.logs, "daemon.log"),
32
34
  entryPoint: "src/daemon/index.ts",
33
35
  });
36
+
37
+ this.sonarLifecycle = new ServiceLifecycle({
38
+ name: "SonarAgent",
39
+ pidFile: join(AMALFA_DIRS.runtime, "sonar.pid"),
40
+ logFile: join(AMALFA_DIRS.logs, "sonar.log"),
41
+ entryPoint: "src/daemon/sonar-agent.ts",
42
+ });
34
43
  }
35
44
 
36
45
  /**
@@ -65,7 +74,9 @@ export class DaemonManager {
65
74
  * Check if vector daemon is running
66
75
  */
67
76
  async checkVectorDaemon(): Promise<DaemonStatus> {
68
- const pid = await this.readPid(join(AMALFA_DIRS.runtime, "vector-daemon.pid"));
77
+ const pid = await this.readPid(
78
+ join(AMALFA_DIRS.runtime, "vector-daemon.pid"),
79
+ );
69
80
  if (!pid) {
70
81
  return { running: false };
71
82
  }
@@ -126,24 +137,77 @@ export class DaemonManager {
126
137
  await this.watcherLifecycle.stop();
127
138
  }
128
139
 
140
+ /**
141
+ * Check if Sonar Agent is running
142
+ */
143
+ async checkSonarAgent(): Promise<DaemonStatus> {
144
+ const pid = await this.readPid(join(AMALFA_DIRS.runtime, "sonar.pid"));
145
+ if (!pid) {
146
+ return { running: false };
147
+ }
148
+
149
+ const running = await this.isProcessRunning(pid);
150
+ let activeModel: string | undefined;
151
+
152
+ if (running) {
153
+ try {
154
+ const health = (await fetch("http://localhost:3012/health").then((r) =>
155
+ r.json(),
156
+ )) as { model?: string };
157
+ activeModel = health.model;
158
+ } catch {
159
+ // disregard
160
+ }
161
+ }
162
+
163
+ return {
164
+ running,
165
+ pid: running ? pid : undefined,
166
+ port: running ? 3012 : undefined,
167
+ activeModel,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Start Sonar Agent
173
+ */
174
+ async startSonarAgent(): Promise<void> {
175
+ await this.sonarLifecycle.start();
176
+ // Wait a moment for daemon to initialize
177
+ await new Promise((resolve) => setTimeout(resolve, 1000));
178
+ }
179
+
180
+ /**
181
+ * Stop Sonar Agent
182
+ */
183
+ async stopSonarAgent(): Promise<void> {
184
+ await this.sonarLifecycle.stop();
185
+ }
186
+
129
187
  /**
130
188
  * Check status of all daemons
131
189
  */
132
190
  async checkAll(): Promise<{
133
191
  vector: DaemonStatus;
134
192
  watcher: DaemonStatus;
193
+ sonar: DaemonStatus;
135
194
  }> {
136
- const [vector, watcher] = await Promise.all([
195
+ const [vector, watcher, sonar] = await Promise.all([
137
196
  this.checkVectorDaemon(),
138
197
  this.checkFileWatcher(),
198
+ this.checkSonarAgent(),
139
199
  ]);
140
- return { vector, watcher };
200
+ return { vector, watcher, sonar };
141
201
  }
142
202
 
143
203
  /**
144
204
  * Stop all daemons
145
205
  */
146
206
  async stopAll(): Promise<void> {
147
- await Promise.all([this.stopVectorDaemon(), this.stopFileWatcher()]);
207
+ await Promise.all([
208
+ this.stopVectorDaemon(),
209
+ this.stopFileWatcher(),
210
+ this.stopSonarAgent(),
211
+ ]);
148
212
  }
149
213
  }