amalfa 1.0.19 → 1.0.23
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/package.json +10 -3
- 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 +349 -95
- package/src/config/defaults.ts +137 -28
- package/src/config/scripts-registry.json +72 -0
- package/src/core/MarkdownMasker.ts +1 -1
- package/src/daemon/index.ts +44 -27
- package/src/daemon/sonar-agent.ts +774 -0
- package/src/mcp/index.ts +145 -23
- package/src/pipeline/AmalfaIngestor.ts +40 -17
- package/src/pipeline/PreFlightAnalyzer.ts +2 -2
- package/src/pipeline/SemanticHarvester.ts +2 -2
- package/src/resonance/db.ts +13 -0
- package/src/resonance/services/embedder.ts +1 -1
- package/src/resonance/services/vector-daemon.ts +1 -1
- package/src/utils/DaemonManager.ts +70 -6
- package/src/utils/Notifications.ts +2 -2
- package/src/utils/ServiceLifecycle.ts +198 -211
- package/src/utils/StatsTracker.ts +10 -6
- package/src/utils/ollama-discovery.ts +190 -0
- package/src/utils/sonar-client.ts +294 -0
- package/.biomeignore +0 -19
- package/amalfa.config.example.ts +0 -113
- package/biome.json +0 -49
- package/bun.lock +0 -369
- package/src/utils/ZombieDefense.ts +0 -258
- package/tsconfig.json +0 -46
package/src/mcp/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { appendFileSync } from "
|
|
2
|
-
import { join } from "
|
|
1
|
+
import { appendFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import {
|
|
@@ -8,18 +8,21 @@ import {
|
|
|
8
8
|
ListToolsRequestSchema,
|
|
9
9
|
ReadResourceRequestSchema,
|
|
10
10
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
import { loadConfig } from "@src/config/defaults";
|
|
11
|
+
import { AMALFA_DIRS, loadConfig } from "@src/config/defaults";
|
|
12
12
|
import { VectorEngine } from "@src/core/VectorEngine";
|
|
13
13
|
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 {
|
|
17
|
+
import { createSonarClient, type SonarClient } from "../utils/sonar-client";
|
|
18
18
|
|
|
19
19
|
const args = process.argv.slice(2);
|
|
20
20
|
const command = args[0] || "serve";
|
|
21
21
|
const log = getLogger("MCP");
|
|
22
22
|
|
|
23
|
+
// Sonar client for enhanced search
|
|
24
|
+
const sonarClient: SonarClient = await createSonarClient();
|
|
25
|
+
|
|
23
26
|
// --- Service Lifecycle ---
|
|
24
27
|
|
|
25
28
|
const lifecycle = new ServiceLifecycle({
|
|
@@ -182,11 +185,40 @@ async function runServer() {
|
|
|
182
185
|
const limit = Number(args?.limit || 20);
|
|
183
186
|
const candidates = new Map<
|
|
184
187
|
string,
|
|
185
|
-
{
|
|
188
|
+
{
|
|
189
|
+
id: string;
|
|
190
|
+
score: number;
|
|
191
|
+
preview: string;
|
|
192
|
+
source: string;
|
|
193
|
+
content: string;
|
|
194
|
+
}
|
|
186
195
|
>();
|
|
187
196
|
const errors: string[] = [];
|
|
188
197
|
|
|
189
|
-
//
|
|
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)
|
|
190
222
|
try {
|
|
191
223
|
const vectorResults = await vectorEngine.search(query, limit);
|
|
192
224
|
for (const r of vectorResults) {
|
|
@@ -199,6 +231,7 @@ async function runServer() {
|
|
|
199
231
|
score: r.score,
|
|
200
232
|
preview: preview,
|
|
201
233
|
source: "vector",
|
|
234
|
+
content: r.content || "",
|
|
202
235
|
});
|
|
203
236
|
}
|
|
204
237
|
} catch (e: unknown) {
|
|
@@ -207,12 +240,63 @@ async function runServer() {
|
|
|
207
240
|
errors.push(msg);
|
|
208
241
|
}
|
|
209
242
|
|
|
210
|
-
|
|
243
|
+
// Step 3: Re-rank results with Sonar (if available)
|
|
244
|
+
let rankedResults = Array.from(candidates.values())
|
|
211
245
|
.sort((a, b) => b.score - a.score)
|
|
212
|
-
.slice(0, limit)
|
|
213
|
-
|
|
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
|
+
}
|
|
214
264
|
|
|
215
|
-
|
|
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
|
+
}
|
|
298
|
+
|
|
299
|
+
if (finalResults.length === 0 && errors.length > 0) {
|
|
216
300
|
return {
|
|
217
301
|
content: [
|
|
218
302
|
{ type: "text", text: `Search Error: ${errors.join(", ")}` },
|
|
@@ -220,8 +304,29 @@ async function runServer() {
|
|
|
220
304
|
isError: true,
|
|
221
305
|
};
|
|
222
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
|
+
|
|
223
316
|
return {
|
|
224
|
-
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
|
+
],
|
|
225
330
|
};
|
|
226
331
|
} finally {
|
|
227
332
|
// Cleanup connection
|
|
@@ -296,10 +401,7 @@ async function runServer() {
|
|
|
296
401
|
|
|
297
402
|
if (name === TOOLS.LIST) {
|
|
298
403
|
// TODO: Make this configurable via amalfa.config.ts
|
|
299
|
-
const structure = [
|
|
300
|
-
"docs/",
|
|
301
|
-
"notes/",
|
|
302
|
-
];
|
|
404
|
+
const structure = ["docs/", "notes/"];
|
|
303
405
|
return {
|
|
304
406
|
content: [{ type: "text", text: JSON.stringify(structure, null, 2) }],
|
|
305
407
|
};
|
|
@@ -308,17 +410,37 @@ async function runServer() {
|
|
|
308
410
|
if (name === TOOLS.GARDEN) {
|
|
309
411
|
const filePath = String(args?.file_path);
|
|
310
412
|
const tags = args?.tags as string[];
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
413
|
+
let content = await Bun.file(filePath).text();
|
|
414
|
+
|
|
415
|
+
// Check for existing tag block and merge/replace
|
|
416
|
+
const tagPattern = /<!-- tags: ([^>]+) -->\s*$/;
|
|
417
|
+
const match = content.match(tagPattern);
|
|
418
|
+
|
|
419
|
+
let operation = "injected";
|
|
420
|
+
if (match?.[1]) {
|
|
421
|
+
// Merge with existing tags
|
|
422
|
+
const existingTags = match[1]
|
|
423
|
+
.split(",")
|
|
424
|
+
.map((t) => t.trim())
|
|
425
|
+
.filter(Boolean);
|
|
426
|
+
const mergedTags = [...new Set([...existingTags, ...tags])]; // deduplicate
|
|
427
|
+
const tagBlock = `<!-- tags: ${mergedTags.join(", ")} -->`;
|
|
428
|
+
content = content.replace(tagPattern, `${tagBlock}\n`);
|
|
429
|
+
operation = "merged";
|
|
430
|
+
} else {
|
|
431
|
+
// Append new tag block
|
|
432
|
+
const tagBlock = `<!-- tags: ${tags.join(", ")} -->`;
|
|
433
|
+
content = content.endsWith("\n")
|
|
434
|
+
? `${content}\n${tagBlock}\n`
|
|
435
|
+
: `${content}\n\n${tagBlock}\n`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await Bun.write(filePath, content);
|
|
317
439
|
return {
|
|
318
440
|
content: [
|
|
319
441
|
{
|
|
320
442
|
type: "text",
|
|
321
|
-
text: `
|
|
443
|
+
text: `Successfully ${operation} ${tags.length} tags into ${filePath}`,
|
|
322
444
|
},
|
|
323
445
|
],
|
|
324
446
|
};
|
|
@@ -394,4 +516,4 @@ process.on("unhandledRejection", (reason) => {
|
|
|
394
516
|
|
|
395
517
|
// --- Dispatch ---
|
|
396
518
|
|
|
397
|
-
await lifecycle.run(command, runServer
|
|
519
|
+
await lifecycle.run(command, runServer);
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* No Persona/CDA complexity - just pure markdown → knowledge graph
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { join } from "
|
|
8
|
-
import { Glob } from "bun";
|
|
7
|
+
import { join } from "path";
|
|
9
8
|
import type { AmalfaConfig } from "@src/config/defaults";
|
|
10
9
|
import { EdgeWeaver } from "@src/core/EdgeWeaver";
|
|
11
|
-
import {
|
|
10
|
+
import type { Node, ResonanceDB } from "@src/resonance/db";
|
|
12
11
|
import { Embedder } from "@src/resonance/services/embedder";
|
|
13
12
|
import { SimpleTokenizerService as TokenizerService } from "@src/resonance/services/simpleTokenizer";
|
|
14
13
|
import { getLogger } from "@src/utils/Logger";
|
|
14
|
+
import { Glob } from "bun";
|
|
15
15
|
|
|
16
16
|
export interface IngestionResult {
|
|
17
17
|
success: boolean;
|
|
@@ -37,7 +37,7 @@ export class AmalfaIngestor {
|
|
|
37
37
|
*/
|
|
38
38
|
async ingest(): Promise<IngestionResult> {
|
|
39
39
|
const startTime = performance.now();
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
const sources = this.config.sources || ["./docs"];
|
|
42
42
|
this.log.info(`📚 Starting ingestion from: ${sources.join(", ")}`);
|
|
43
43
|
|
|
@@ -47,7 +47,7 @@ export class AmalfaIngestor {
|
|
|
47
47
|
await embedder.embed("init"); // Warm up
|
|
48
48
|
|
|
49
49
|
const tokenizer = TokenizerService.getInstance();
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
// Discover markdown files
|
|
52
52
|
const files = await this.discoverFiles();
|
|
53
53
|
this.log.info(`📁 Found ${files.length} markdown files`);
|
|
@@ -108,7 +108,10 @@ export class AmalfaIngestor {
|
|
|
108
108
|
if (!filePath) continue;
|
|
109
109
|
const content = await Bun.file(filePath).text();
|
|
110
110
|
const filename = filePath.split("/").pop() || "unknown";
|
|
111
|
-
const id = filename
|
|
111
|
+
const id = filename
|
|
112
|
+
.replace(".md", "")
|
|
113
|
+
.toLowerCase()
|
|
114
|
+
.replace(/[^a-z0-9-]/g, "-");
|
|
112
115
|
weaver.weave(id, content);
|
|
113
116
|
}
|
|
114
117
|
this.db.commit();
|
|
@@ -117,17 +120,36 @@ export class AmalfaIngestor {
|
|
|
117
120
|
this.log.info("💾 Forcing WAL checkpoint...");
|
|
118
121
|
this.db.getRawDb().run("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
119
122
|
|
|
123
|
+
// OH-104: The Pinch Check (verify physical commit)
|
|
124
|
+
const dbPath = this.db.getRawDb().filename;
|
|
125
|
+
const dbFile = Bun.file(dbPath);
|
|
126
|
+
if (!(await dbFile.exists())) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"OH-104 VIOLATION: Database file missing after checkpoint",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const finalSize = dbFile.size;
|
|
132
|
+
if (finalSize === 0) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"OH-104 VIOLATION: Database file is empty after checkpoint",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
this.log.info(`✅ Pinch Check: db=${(finalSize / 1024).toFixed(1)}KB`);
|
|
138
|
+
|
|
120
139
|
const endTime = performance.now();
|
|
121
140
|
const durationSec = (endTime - startTime) / 1000;
|
|
122
141
|
|
|
123
142
|
const stats = this.db.getStats();
|
|
124
|
-
this.log.info(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
this.log.info(
|
|
144
|
+
{
|
|
145
|
+
files: processedCount,
|
|
146
|
+
nodes: stats.nodes,
|
|
147
|
+
edges: stats.edges,
|
|
148
|
+
vectors: stats.vectors,
|
|
149
|
+
durationSec: durationSec.toFixed(2),
|
|
150
|
+
},
|
|
151
|
+
"✅ Ingestion complete",
|
|
152
|
+
);
|
|
131
153
|
|
|
132
154
|
return {
|
|
133
155
|
success: true,
|
|
@@ -178,7 +200,10 @@ export class AmalfaIngestor {
|
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
} catch (e) {
|
|
181
|
-
this.log.warn(
|
|
203
|
+
this.log.warn(
|
|
204
|
+
{ source: sourcePath, err: e },
|
|
205
|
+
"⚠️ Failed to scan directory",
|
|
206
|
+
);
|
|
182
207
|
}
|
|
183
208
|
}
|
|
184
209
|
|
|
@@ -215,9 +240,7 @@ export class AmalfaIngestor {
|
|
|
215
240
|
|
|
216
241
|
// Parse frontmatter
|
|
217
242
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
218
|
-
const frontmatter = fmMatch?.[1]
|
|
219
|
-
? this.parseFrontmatter(fmMatch[1])
|
|
220
|
-
: {};
|
|
243
|
+
const frontmatter = fmMatch?.[1] ? this.parseFrontmatter(fmMatch[1]) : {};
|
|
221
244
|
|
|
222
245
|
// Generate ID from filename
|
|
223
246
|
const filename = filePath.split("/").pop() || "unknown";
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* Generates .amalfa/logs/pre-flight.log with recommendations.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, lstatSync, readdirSync, realpathSync, statSync, writeFileSync } from "
|
|
14
|
-
import { join, relative } from "
|
|
13
|
+
import { existsSync, lstatSync, readdirSync, realpathSync, statSync, writeFileSync } from "fs";
|
|
14
|
+
import { join, relative } from "path";
|
|
15
15
|
import { getLogger } from "@src/utils/Logger";
|
|
16
16
|
import type { AmalfaConfig } from "@src/config/defaults";
|
|
17
17
|
import { AMALFA_DIRS, initAmalfaDirs } from "@src/config/defaults";
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* await harvester.loadIntoResonance(graph);
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync } from "
|
|
14
|
-
import { join } from "
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
15
|
import { getLogger } from "@src/utils/Logger";
|
|
16
16
|
import { $ } from "bun";
|
|
17
17
|
|
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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync } from "
|
|
2
|
-
import { join } from "
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
3
|
import { ServiceLifecycle } from "./ServiceLifecycle";
|
|
4
4
|
import { AMALFA_DIRS } from "@src/config/defaults";
|
|
5
5
|
|
|
@@ -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
|
}
|