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/README.md +0 -3
- package/package.json +1 -1
- package/src/cli/enhance-commands.ts +81 -0
- package/src/cli/list-scripts.ts +67 -0
- package/src/cli/sonar-chat.ts +95 -0
- package/src/cli.ts +346 -92
- package/src/config/defaults.ts +135 -26
- package/src/config/scripts-registry.json +72 -0
- package/src/daemon/index.ts +41 -24
- package/src/daemon/sonar-agent.ts +774 -0
- package/src/mcp/index.ts +114 -8
- package/src/resonance/db.ts +13 -0
- package/src/utils/DaemonManager.ts +68 -4
- package/src/utils/ServiceLifecycle.ts +195 -208
- package/src/utils/StatsTracker.ts +8 -4
- package/src/utils/ollama-discovery.ts +190 -0
- package/src/utils/sonar-client.ts +294 -0
- package/src/utils/ZombieDefense.ts +0 -258
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
|
-
{
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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: [
|
|
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
|
|
519
|
+
await lifecycle.run(command, runServer);
|
package/src/resonance/db.ts
CHANGED
|
@@ -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(
|
|
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([
|
|
207
|
+
await Promise.all([
|
|
208
|
+
this.stopVectorDaemon(),
|
|
209
|
+
this.stopFileWatcher(),
|
|
210
|
+
this.stopSonarAgent(),
|
|
211
|
+
]);
|
|
148
212
|
}
|
|
149
213
|
}
|