amalfa 1.0.40 โ†’ 1.2.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to AMALFA will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-01-13
9
+
10
+ ### Added
11
+ - **Scratchpad Protocol (Phase 7)**: Intercepts large MCP tool outputs (>4KB) and caches them to `.amalfa/cache/scratchpad/`, returning a reference with preview instead of full content. Reduces context window usage for verbose responses.
12
+ - New `scratchpad_read` and `scratchpad_list` MCP tools for retrieving cached content.
13
+ - Content-addressable storage with SHA256 deduplication.
14
+ - Configurable threshold, max age (24h), and cache size limit (50MB).
15
+
16
+ ## [1.1.0] - 2026-01-13
17
+
18
+ ### Added
19
+ - **Graphology Workflows (Phase 6)**:
20
+ - Added Adamic-Adar ("Friend-of-a-Friend") link prediction strategy.
21
+ - Added PageRank ("Pillar Content") identification strategy.
22
+ - Added Louvain ("Global Context") community detection strategy.
23
+ - Added `amalfa enhance --strategy=<strategy>` CLI command to expose these analyses.
24
+ - **Autonomous Research (Phase 5)**:
25
+ - **LouvainGate**: Configurable thresholds for super-node detection.
26
+ - **Pipeline History**: Added `history` table to track graph mutations.
27
+ - **CLI Promotion**: `amalfa stats --orphans` and `amalfa validate --graph` for advanced diagnostics.
28
+ - **Graph Features**: Exposed `GraphEngine.traverse()` (BFS) and `validateIntegrity()`.
29
+
30
+ ### Changed
31
+ - **CLI Architecture**: Refactored monolithic `src/cli.ts` into modular command files (`src/cli/commands/*.ts`) for maintainability.
32
+ - **Cleanup**: Deprecated legacy `tag-slug` syntax in EdgeWeaver.
33
+
8
34
  ## [1.0.40] - 2026-01-09
9
35
 
10
36
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalfa",
3
- "version": "1.0.40",
3
+ "version": "1.2.0",
4
4
  "description": "Local-first knowledge graph engine for AI agents. Transforms markdown into searchable memory with MCP protocol.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/pjsvis/amalfa#readme",
@@ -44,7 +44,7 @@
44
44
  "bun": ">=1.0.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@biomejs/biome": "2.3.8",
47
+ "@biomejs/biome": "^2.3.11",
48
48
  "@types/bun": "1.3.4",
49
49
  "only-allow": "1.2.2",
50
50
  "pino-pretty": "13.1.3",
@@ -0,0 +1,83 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadConfig } from "@src/config/defaults";
4
+ import { getDbPath } from "../utils";
5
+
6
+ export async function cmdDoctor(_args: string[]) {
7
+ console.log("๐Ÿฉบ AMALFA Health Check\n");
8
+
9
+ let issues = 0;
10
+
11
+ // Check Bun runtime
12
+ console.log("โœ“ Bun runtime: OK");
13
+
14
+ // Check config and database
15
+ const dbPath = await getDbPath();
16
+ if (existsSync(dbPath)) {
17
+ const fileSizeMB = (statSync(dbPath).size / 1024 / 1024).toFixed(2);
18
+ console.log(`โœ“ Database found: ${dbPath} (${fileSizeMB} MB)`);
19
+ } else {
20
+ console.log(`โœ— Database not found: ${dbPath}`);
21
+ console.log(` Run: amalfa init`);
22
+ issues++;
23
+ }
24
+
25
+ // Check .amalfa directory
26
+ const amalfaDir = join(process.cwd(), ".amalfa");
27
+ if (existsSync(amalfaDir)) {
28
+ console.log(`โœ“ AMALFA directory: ${amalfaDir}`);
29
+ } else {
30
+ console.log(`โœ— AMALFA directory not found: ${amalfaDir}`);
31
+ issues++;
32
+ }
33
+
34
+ // Check source directories from config
35
+ const config = await loadConfig();
36
+ const sources = config.sources || ["./docs"];
37
+ let sourcesFound = 0;
38
+ for (const source of sources) {
39
+ const sourcePath = join(process.cwd(), source);
40
+ if (existsSync(sourcePath)) {
41
+ console.log(`โœ“ Source directory: ${sourcePath}`);
42
+ sourcesFound++;
43
+ } else {
44
+ console.log(`โœ— Source directory not found: ${sourcePath}`);
45
+ issues++;
46
+ }
47
+ }
48
+ if (sourcesFound === 0) {
49
+ console.log(` Configure sources in amalfa.config.json`);
50
+ }
51
+
52
+ // Check dependencies (FastEmbed)
53
+ try {
54
+ await import("fastembed");
55
+ console.log("โœ“ FastEmbed: OK");
56
+ } catch {
57
+ console.log("โœ— FastEmbed not found (run: bun install)");
58
+ issues++;
59
+ }
60
+
61
+ // Check MCP SDK
62
+ try {
63
+ await import("@modelcontextprotocol/sdk/server/index.js");
64
+ console.log("โœ“ MCP SDK: OK");
65
+ } catch {
66
+ console.log("โœ— MCP SDK not found (run: bun install)");
67
+ issues++;
68
+ }
69
+
70
+ console.log("");
71
+
72
+ if (issues === 0) {
73
+ console.log("โœ… All checks passed! AMALFA is ready to use.");
74
+ console.log("\nNext steps:");
75
+ console.log(" amalfa serve # Start MCP server");
76
+ console.log(" amalfa stats # View database statistics");
77
+ } else {
78
+ console.log(
79
+ `โŒ Found ${issues} issue(s). Please resolve them and try again.`,
80
+ );
81
+ process.exit(1);
82
+ }
83
+ }
@@ -0,0 +1,100 @@
1
+ import { existsSync } from "node:fs";
2
+ import { GraphEngine } from "@src/core/GraphEngine";
3
+ import { ResonanceDB } from "@src/resonance/db";
4
+ import { getDbPath } from "../utils";
5
+
6
+ export async function cmdEnhance(args: string[]) {
7
+ // Parse args
8
+ const strategyArg = args.find((a) => a.startsWith("--strategy="));
9
+ const strategy = strategyArg ? strategyArg.split("=")[1] : "help";
10
+ const limitArg = args.find((a) => a.startsWith("--limit="));
11
+ const limitStr = limitArg ? limitArg.split("=")[1] : "10";
12
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : 10;
13
+
14
+ if (strategy === "help" || !strategy) {
15
+ console.log(`
16
+ Enhance your knowledge graph with structural analysis strategies.
17
+
18
+ Usage:
19
+ amalfa enhance --strategy=[strategy] [--limit=N]
20
+
21
+ Strategies:
22
+ adamic-adar Find "Friend-of-a-Friend" connections (missing links)
23
+ pagerank Identify "Pillar Content" (high authority nodes)
24
+ communities List "Global Context" clusters (Louvain communities)
25
+
26
+ Examples:
27
+ amalfa enhance --strategy=adamic-adar --limit=5
28
+ amalfa enhance --strategy=pagerank
29
+ `);
30
+ return;
31
+ }
32
+
33
+ // Load DB
34
+ const dbPath = await getDbPath();
35
+ if (!existsSync(dbPath)) {
36
+ console.error("โŒ Database not found. Run 'amalfa init' first.");
37
+ process.exit(1);
38
+ }
39
+
40
+ const db = new ResonanceDB(dbPath);
41
+ const graph = new GraphEngine();
42
+
43
+ try {
44
+ await graph.load(db.getRawDb());
45
+
46
+ if (strategy === "adamic-adar") {
47
+ console.log(`๐Ÿ” Finding structural gaps (Adamic-Adar)...`);
48
+ const candidates = graph.findStructuralCandidates(limit);
49
+
50
+ if (candidates.length === 0) {
51
+ console.log("โœ… No significant structural gaps found.");
52
+ } else {
53
+ console.log("\nProposed Edges (based on shared neighbors):");
54
+ console.log("Score | Source <-> Target");
55
+ console.log("-".repeat(40));
56
+ for (const c of candidates) {
57
+ console.log(`${c.score.toFixed(2)} | ${c.source} <-> ${c.target}`);
58
+ }
59
+ }
60
+ } else if (strategy === "pagerank") {
61
+ console.log(`๐Ÿ›๏ธ Identifying Pillar Content (PageRank)...`);
62
+ const pillars = graph.findPillars(limit);
63
+
64
+ console.log("\nTop Authority Nodes:");
65
+ console.log("Score | Degree | ID");
66
+ console.log("-".repeat(40));
67
+ for (const p of pillars) {
68
+ console.log(
69
+ `${p.score.toFixed(4)} | ${p.degree.toString().padEnd(6)} | ${p.id}`,
70
+ );
71
+ }
72
+ } else if (strategy === "communities") {
73
+ console.log(`๐ŸŒ Identifying Global Context (Louvain)...`);
74
+ const communities = graph.getCommunities();
75
+ const clusterIds = Object.keys(communities)
76
+ .sort(
77
+ (a, b) =>
78
+ (communities[b]?.length || 0) - (communities[a]?.length || 0),
79
+ )
80
+ .slice(0, limit);
81
+
82
+ console.log("\nTop Communities:");
83
+ for (const id of clusterIds) {
84
+ const members = communities[id] || [];
85
+ console.log(`\nCluster ${id} (${members.length} nodes):`);
86
+ console.log(
87
+ members.slice(0, 5).join(", ") + (members.length > 5 ? "..." : ""),
88
+ );
89
+ }
90
+ } else {
91
+ console.error(`โŒ Unknown strategy: ${strategy}`);
92
+ process.exit(1);
93
+ }
94
+ } catch (error) {
95
+ console.error("โŒ Enhancement failed:", error);
96
+ process.exit(1);
97
+ } finally {
98
+ db.close();
99
+ }
100
+ }
@@ -0,0 +1,130 @@
1
+ import { existsSync, mkdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadConfig } from "@src/config/defaults";
4
+ import { AmalfaIngestor } from "@src/pipeline/AmalfaIngestor";
5
+ import { PreFlightAnalyzer } from "@src/pipeline/PreFlightAnalyzer";
6
+ import { ResonanceDB } from "@src/resonance/db";
7
+ import { StatsTracker } from "@src/utils/StatsTracker";
8
+ import pkg from "../../../package.json" with { type: "json" };
9
+
10
+ const VERSION = pkg.version;
11
+
12
+ export async function cmdInit(args: string[]) {
13
+ console.log("๐Ÿš€ AMALFA Initialization\n");
14
+
15
+ // Check for --force flag
16
+ const forceMode = args.includes("--force");
17
+
18
+ // Load configuration
19
+ const config = await loadConfig();
20
+
21
+ const sources = config.sources || ["./docs"];
22
+ console.log(`๐Ÿ“ Sources: ${sources.join(", ")}`);
23
+ console.log(`๐Ÿ’พ Database: ${config.database}`);
24
+ console.log(`๐Ÿง  Model: ${config.embeddings.model}\n`);
25
+
26
+ // Run pre-flight analysis
27
+ console.log("๐Ÿ” Running pre-flight analysis...\n");
28
+ const analyzer = new PreFlightAnalyzer(config);
29
+ const report = await analyzer.analyze();
30
+
31
+ // Display summary
32
+ console.log("๐Ÿ“Š Pre-Flight Summary:");
33
+ console.log(` Total files: ${report.totalFiles}`);
34
+ console.log(` Valid files: ${report.validFiles}`);
35
+ console.log(` Skipped files: ${report.skippedFiles}`);
36
+ console.log(
37
+ ` Total size: ${(report.totalSizeBytes / 1024 / 1024).toFixed(2)} MB`,
38
+ );
39
+ console.log(` Estimated nodes: ${report.estimatedNodes}\n`);
40
+
41
+ if (report.hasErrors) {
42
+ console.error("โŒ Pre-flight check failed with errors\n");
43
+ console.error("Errors detected:");
44
+ for (const issue of report.issues.filter((i) => i.severity === "error")) {
45
+ console.error(` - ${issue.path}: ${issue.details}`);
46
+ }
47
+ console.error(
48
+ "\nSee .amalfa/logs/pre-flight.log for details and recommendations",
49
+ );
50
+ console.error("\nFix these issues and try again.");
51
+ process.exit(1);
52
+ }
53
+
54
+ if (report.hasWarnings && !forceMode) {
55
+ console.warn("โš ๏ธ Pre-flight check completed with warnings\n");
56
+ console.warn("Warnings detected:");
57
+ for (const issue of report.issues.filter((i) => i.severity === "warning")) {
58
+ console.warn(` - ${issue.path}: ${issue.details}`);
59
+ }
60
+ console.warn("\nSee .amalfa/logs/pre-flight.log for recommendations");
61
+ console.warn("\nTo proceed anyway, use: amalfa init --force");
62
+ process.exit(1);
63
+ }
64
+
65
+ if (report.validFiles === 0) {
66
+ console.error("\nโŒ No valid markdown files found");
67
+ console.error("See .amalfa/logs/pre-flight.log for details");
68
+ process.exit(1);
69
+ }
70
+
71
+ if (forceMode && report.hasWarnings) {
72
+ console.warn("โš ๏ธ Proceeding with --force despite warnings\n");
73
+ }
74
+
75
+ // Create .amalfa directory
76
+ const amalfaDir = join(process.cwd(), ".amalfa");
77
+ if (!existsSync(amalfaDir)) {
78
+ console.log(`๐Ÿ“‚ Creating directory: ${amalfaDir}`);
79
+ mkdirSync(amalfaDir, { recursive: true });
80
+ }
81
+
82
+ // Initialize database
83
+ const dbPath = join(process.cwd(), config.database);
84
+ console.log(`๐Ÿ—„๏ธ Initializing database: ${dbPath}\n`);
85
+
86
+ try {
87
+ // Create/open database
88
+ const db = new ResonanceDB(dbPath);
89
+
90
+ // Run ingestion
91
+ const ingestor = new AmalfaIngestor(config, db);
92
+ const result = await ingestor.ingest();
93
+
94
+ db.close();
95
+
96
+ if (result.success) {
97
+ // Record database snapshot for tracking
98
+ const tracker = new StatsTracker();
99
+ const fileSize = statSync(dbPath).size;
100
+ const dbSizeMB = fileSize / 1024 / 1024;
101
+
102
+ await tracker.recordSnapshot({
103
+ timestamp: new Date().toISOString(),
104
+ nodes: result.stats.nodes,
105
+ edges: result.stats.edges,
106
+ embeddings: result.stats.vectors,
107
+ dbSizeMB,
108
+ version: VERSION,
109
+ });
110
+
111
+ console.log("\nโœ… Initialization complete!");
112
+ console.log("\n๐Ÿ“Š Summary:");
113
+ console.log(` Files processed: ${result.stats.files}`);
114
+ console.log(` Nodes created: ${result.stats.nodes}`);
115
+ console.log(` Edges created: ${result.stats.edges}`);
116
+ console.log(` Embeddings: ${result.stats.vectors}`);
117
+ console.log(` Duration: ${result.stats.durationSec.toFixed(2)}s\n`);
118
+ console.log("๐Ÿ“Š Snapshot saved to: .amalfa/stats-history.json\n");
119
+ console.log("Next steps:");
120
+ console.log(" amalfa serve # Start MCP server");
121
+ console.log(" amalfa daemon # Watch for file changes (coming soon)");
122
+ } else {
123
+ console.error("\nโŒ Initialization failed");
124
+ process.exit(1);
125
+ }
126
+ } catch (error) {
127
+ console.error("\nโŒ Initialization failed:", error);
128
+ process.exit(1);
129
+ }
130
+ }
@@ -0,0 +1,290 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { AMALFA_DIRS } from "@src/config/defaults";
5
+ import { checkDatabase, getDbPath } from "../utils";
6
+
7
+ export async function cmdServe(_args: string[]) {
8
+ // Check database exists
9
+ if (!(await checkDatabase())) {
10
+ process.exit(1);
11
+ }
12
+
13
+ const dbPath = await getDbPath();
14
+ console.error("๐Ÿš€ Starting AMALFA MCP Server...");
15
+ console.error(`๐Ÿ“Š Database: ${dbPath}`);
16
+ console.error("");
17
+
18
+ const serverPath = join(process.cwd(), "src/mcp/index.ts");
19
+ const proc = spawn("bun", ["run", serverPath, "serve"], {
20
+ stdio: "inherit",
21
+ cwd: process.cwd(),
22
+ });
23
+
24
+ proc.on("exit", (code) => {
25
+ process.exit(code ?? 0);
26
+ });
27
+ }
28
+
29
+ export async function cmdServers(args: string[]) {
30
+ const action = args[1];
31
+ // If action is provided and isn't a flag (like --dot), treat it as a lifecycle command
32
+ if (
33
+ action &&
34
+ !action.startsWith("-") &&
35
+ ["start", "stop", "restart", "status"].includes(action)
36
+ ) {
37
+ if (action === "status") {
38
+ // Just fall through to normal status display
39
+ } else {
40
+ await manageAllServers(action as "start" | "stop" | "restart");
41
+ return;
42
+ }
43
+ }
44
+
45
+ const showDot = args.includes("--dot");
46
+
47
+ const SERVICES = [
48
+ {
49
+ name: "MCP Server",
50
+ pidFile: join(AMALFA_DIRS.runtime, "mcp.pid"),
51
+ port: "stdio",
52
+ id: "mcp",
53
+ cmd: "amalfa serve",
54
+ },
55
+ {
56
+ name: "Vector Daemon",
57
+ pidFile: join(AMALFA_DIRS.runtime, "vector-daemon.pid"),
58
+ port: "3010",
59
+ id: "vector",
60
+ cmd: "amalfa vector start",
61
+ },
62
+ {
63
+ name: "File Watcher",
64
+ pidFile: join(AMALFA_DIRS.runtime, "daemon.pid"),
65
+ port: "-",
66
+ id: "watcher",
67
+ cmd: "amalfa daemon start",
68
+ },
69
+ {
70
+ name: "Sonar Agent",
71
+ pidFile: join(AMALFA_DIRS.runtime, "sonar.pid"),
72
+ port: "3012",
73
+ id: "sonar",
74
+ cmd: "amalfa sonar start",
75
+ },
76
+ ];
77
+
78
+ async function isRunning(pid: number): Promise<boolean> {
79
+ try {
80
+ process.kill(pid, 0);
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ if (showDot) {
88
+ // Generate DOT diagram
89
+ const statuses = new Map<string, { status: string; pid: string }>();
90
+
91
+ for (const svc of SERVICES) {
92
+ let status = "stopped";
93
+ let pidStr = "-";
94
+
95
+ if (existsSync(svc.pidFile)) {
96
+ try {
97
+ const text = readFileSync(svc.pidFile, "utf-8");
98
+ const pid = Number.parseInt(text.trim(), 10);
99
+
100
+ if (!Number.isNaN(pid) && (await isRunning(pid))) {
101
+ status = "running";
102
+ pidStr = pid.toString();
103
+ } else {
104
+ status = "stale";
105
+ pidStr = `${pid}`;
106
+ }
107
+ } catch {
108
+ // Ignore
109
+ }
110
+ }
111
+
112
+ statuses.set(svc.id, { status, pid: pidStr });
113
+ }
114
+
115
+ console.log("digraph AMALFA {");
116
+ console.log(" rankdir=LR;");
117
+ console.log(" node [shape=box, style=filled];");
118
+ console.log("");
119
+ console.log(" // Nodes");
120
+
121
+ for (const svc of SERVICES) {
122
+ const st = statuses.get(svc.id);
123
+ const color =
124
+ st?.status === "running"
125
+ ? "lightgreen"
126
+ : st?.status === "stale"
127
+ ? "orange"
128
+ : "lightgray";
129
+ const label = `${svc.name}\\nPort: ${svc.port}\\nPID: ${st?.pid || "-"}`;
130
+ console.log(` ${svc.id} [label="${label}", fillcolor=${color}];`);
131
+ }
132
+
133
+ console.log("");
134
+ console.log(" // Database");
135
+ console.log(
136
+ ' db [label="SQLite\\n.amalfa/resonance.db", shape=cylinder, fillcolor=lightyellow];',
137
+ );
138
+ console.log("");
139
+ console.log(" // Connections");
140
+ console.log(' mcp -> db [label="read/write"];');
141
+ console.log(' vector -> db [label="embeddings"];');
142
+ console.log(' watcher -> db [label="updates"];');
143
+ console.log(' mcp -> vector [label="query", style=dashed];');
144
+ console.log("}");
145
+ console.log("");
146
+ console.log("# Save to file: amalfa servers --dot > amalfa.dot");
147
+ console.log("# Render: dot -Tpng amalfa.dot -o amalfa.png");
148
+ return;
149
+ }
150
+
151
+ console.log("\n๐Ÿ“ก AMALFA Service Status\n");
152
+ console.log("โ”€".repeat(95));
153
+ console.log(
154
+ "SERVICE".padEnd(18) +
155
+ "COMMAND".padEnd(25) +
156
+ "PORT".padEnd(12) +
157
+ "STATUS".padEnd(15) +
158
+ "PID".padEnd(10),
159
+ );
160
+ console.log("โ”€".repeat(95));
161
+
162
+ for (const svc of SERVICES) {
163
+ let status = "โšช๏ธ STOPPED";
164
+ let pidStr = "-";
165
+
166
+ if (existsSync(svc.pidFile)) {
167
+ try {
168
+ const text = readFileSync(svc.pidFile, "utf-8");
169
+ const pid = Number.parseInt(text.trim(), 10);
170
+
171
+ if (!Number.isNaN(pid) && (await isRunning(pid))) {
172
+ status = "๐ŸŸข RUNNING";
173
+ pidStr = pid.toString();
174
+ } else {
175
+ status = "๐Ÿ”ด STALE";
176
+ pidStr = `${pid} (?)`;
177
+ }
178
+ } catch {
179
+ // Ignore read errors
180
+ }
181
+ }
182
+
183
+ console.log(
184
+ svc.name.padEnd(18) +
185
+ svc.cmd.padEnd(25) +
186
+ svc.port.padEnd(12) +
187
+ status.padEnd(15) +
188
+ pidStr.padEnd(10),
189
+ );
190
+ }
191
+
192
+ console.log("โ”€".repeat(95));
193
+ console.log(
194
+ "\n๐Ÿ’ก Commands: amalfa servers [start|stop|restart] | amalfa vector start | amalfa daemon start\n",
195
+ );
196
+ }
197
+
198
+ // Background services to manage via 'amalfa servers start/restart'
199
+ const BACKGROUND_SERVICES = [
200
+ {
201
+ name: "Vector Daemon",
202
+ cmd: "amalfa",
203
+ args: ["vector", "start"],
204
+ },
205
+ {
206
+ name: "File Watcher",
207
+ cmd: "amalfa",
208
+ args: ["daemon", "start"],
209
+ },
210
+ {
211
+ name: "Sonar Agent",
212
+ cmd: "amalfa",
213
+ args: ["sonar", "start"],
214
+ },
215
+ ];
216
+
217
+ async function manageAllServers(action: "start" | "stop" | "restart") {
218
+ if (action === "stop" || action === "restart") {
219
+ await cmdStopAll([]);
220
+ }
221
+
222
+ if (action === "start" || action === "restart") {
223
+ console.log("๐Ÿš€ Starting background services...\n");
224
+
225
+ for (const svc of BACKGROUND_SERVICES) {
226
+ console.log(`โ–ถ๏ธ Starting ${svc.name}...`);
227
+ const child = spawn(svc.cmd, svc.args, {
228
+ detached: true,
229
+ stdio: "ignore", // Daemons manage their own logs
230
+ cwd: process.cwd(),
231
+ });
232
+ child.unref();
233
+ // Brief pause to allow pid file creation / logging
234
+ await new Promise((resolve) => setTimeout(resolve, 500));
235
+ }
236
+ console.log("\nโœ… All background services triggered.");
237
+ console.log("Run 'amalfa servers' to check status.");
238
+ }
239
+ }
240
+
241
+ export async function cmdStopAll(_args: string[]) {
242
+ console.log("๐Ÿ›‘ Stopping ALL Amalfa Services...\n");
243
+
244
+ const SERVICES = [
245
+ {
246
+ name: "Vector Daemon",
247
+ pidFile: join(AMALFA_DIRS.runtime, "vector-daemon.pid"),
248
+ },
249
+ { name: "File Watcher", pidFile: join(AMALFA_DIRS.runtime, "daemon.pid") },
250
+ { name: "Sonar Agent", pidFile: join(AMALFA_DIRS.runtime, "sonar.pid") },
251
+ // MCP usually runs as stdio, but if we track a PID file for it:
252
+ { name: "MCP Server", pidFile: join(AMALFA_DIRS.runtime, "mcp.pid") },
253
+ ];
254
+
255
+ let stoppedCount = 0;
256
+
257
+ for (const svc of SERVICES) {
258
+ if (existsSync(svc.pidFile)) {
259
+ try {
260
+ const pidStr = readFileSync(svc.pidFile, "utf-8").trim();
261
+ const pid = Number.parseInt(pidStr, 10);
262
+
263
+ if (!Number.isNaN(pid)) {
264
+ // Check if running
265
+ try {
266
+ process.kill(pid, 0); // Check existence
267
+ process.kill(pid, "SIGTERM");
268
+ console.log(`โœ… Sent SIGTERM to ${svc.name} (PID: ${pid})`);
269
+ stoppedCount++;
270
+ } catch {
271
+ // Not running, just stale
272
+ console.log(`๐Ÿงน Cleaning stale PID file for ${svc.name}`);
273
+ }
274
+ }
275
+ } catch (e) {
276
+ console.warn(`โš ๏ธ Failed to stop ${svc.name}:`, e);
277
+ }
278
+ // Always clean up PID file
279
+ try {
280
+ unlinkSync(svc.pidFile);
281
+ } catch {}
282
+ }
283
+ }
284
+
285
+ if (stoppedCount === 0) {
286
+ console.log("โœจ No active services found.");
287
+ } else {
288
+ console.log(`\nโœ… Stopped ${stoppedCount} service(s).`);
289
+ }
290
+ }