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 +26 -0
- package/package.json +2 -2
- package/src/cli/commands/doctor.ts +83 -0
- package/src/cli/commands/enhance.ts +100 -0
- package/src/cli/commands/init.ts +130 -0
- package/src/cli/commands/server.ts +290 -0
- package/src/cli/commands/services.ts +247 -0
- package/src/cli/commands/setup.ts +50 -0
- package/src/cli/commands/stats.ts +110 -0
- package/src/cli/commands/validate.ts +150 -0
- package/src/cli/utils.ts +31 -0
- package/src/cli.ts +26 -952
- package/src/config/defaults.ts +25 -0
- package/src/core/EdgeWeaver.ts +26 -3
- package/src/core/GraphEngine.ts +62 -1
- package/src/daemon/sonar-logic.ts +4 -4
- package/src/mcp/index.ts +83 -11
- package/src/pipeline/AmalfaIngestor.ts +11 -1
- package/src/resonance/db.ts +35 -0
- package/src/resonance/drizzle/migrations/0001_happy_serpent_society.sql +9 -0
- package/src/resonance/drizzle/migrations/meta/0001_snapshot.json +259 -0
- package/src/resonance/drizzle/migrations/meta/_journal.json +7 -0
- package/src/resonance/drizzle/schema.ts +15 -0
- package/src/resonance/schema.ts +22 -1
- package/src/utils/Scratchpad.ts +427 -0
- package/src/utils/StatsTracker.ts +1 -0
- package/src/utils/TagInjector.ts +63 -69
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
|
|
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.
|
|
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
|
+
}
|