amalfa 1.0.36 → 1.0.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalfa",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
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",
@@ -45,9 +45,9 @@
45
45
  "devDependencies": {
46
46
  "@biomejs/biome": "2.3.8",
47
47
  "@types/bun": "1.3.4",
48
- "only-allow": "^1.2.2",
49
- "pino-pretty": "^13.1.3",
50
- "typescript": "^5.9.3"
48
+ "only-allow": "1.2.2",
49
+ "pino-pretty": "13.1.3",
50
+ "typescript": "5.9.3"
51
51
  },
52
52
  "scripts": {
53
53
  "precommit": "bun run scripts/maintenance/pre-commit.ts",
@@ -64,9 +64,12 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@modelcontextprotocol/sdk": "1.25.2",
67
+ "drizzle-kit": "0.31.8",
68
+ "drizzle-orm": "0.45.1",
67
69
  "fastembed": "2.0.0",
68
- "graphology": "^0.26.0",
69
- "graphology-library": "^0.8.0",
70
- "pino": "^10.1.0"
70
+ "graphology": "0.26.0",
71
+ "graphology-library": "0.8.0",
72
+ "hono": "4.11.3",
73
+ "pino": "10.1.0"
71
74
  }
72
75
  }
@@ -24,27 +24,14 @@ import { ServiceLifecycle } from "@src/utils/ServiceLifecycle";
24
24
  import { inferenceState } from "./sonar-inference";
25
25
  import {
26
26
  handleBatchEnhancement,
27
- handleChat,
28
- handleContextExtraction,
29
27
  handleGardenTask,
30
- handleMetadataEnhancement,
31
28
  handleResearchTask,
32
- handleResultReranking,
33
- handleSearchAnalysis,
34
29
  handleSynthesisTask,
35
30
  handleTimelineTask,
36
31
  type SonarContext,
37
32
  } from "./sonar-logic";
38
33
  import { getTaskModel } from "./sonar-strategies";
39
- import type {
40
- ChatRequest,
41
- ChatSession,
42
- MetadataEnhanceRequest,
43
- SearchAnalyzeRequest,
44
- SearchContextRequest,
45
- SearchRerankRequest,
46
- SonarTask,
47
- } from "./sonar-types";
34
+ import type { ChatSession, SonarTask } from "./sonar-types";
48
35
 
49
36
  const args = process.argv.slice(2);
50
37
  const command = args[0] || "serve";
@@ -128,123 +115,18 @@ async function main() {
128
115
  }
129
116
  }
130
117
 
118
+ import { createSonarApp } from "./sonar-server";
119
+
131
120
  /**
132
- * Start Bun HTTP Server
121
+ * Start Bun HTTP Server via Hono
133
122
  */
134
123
  function startServer(port: number) {
135
- const corsHeaders = {
136
- "Access-Control-Allow-Origin": "*",
137
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
138
- "Access-Control-Allow-Headers": "Content-Type",
139
- };
140
-
141
124
  const context: SonarContext = { db, graphEngine, gardener, chatSessions };
125
+ const app = createSonarApp(context);
142
126
 
143
127
  Bun.serve({
144
128
  port,
145
- async fetch(req) {
146
- if (req.method === "OPTIONS")
147
- return new Response(null, { headers: corsHeaders });
148
- const url = new URL(req.url);
149
-
150
- // Health check
151
- if (url.pathname === "/health") {
152
- const cfg = await loadConfig();
153
- const provider = cfg.sonar.cloud?.enabled ? "cloud" : "local";
154
- const model = cfg.sonar.cloud?.enabled
155
- ? cfg.sonar.cloud.model
156
- : inferenceState.ollamaModel || cfg.sonar.model;
157
- return Response.json(
158
- {
159
- status: "ok",
160
- ollama: inferenceState.ollamaAvailable,
161
- provider,
162
- model,
163
- },
164
- { headers: corsHeaders },
165
- );
166
- }
167
-
168
- // Chat endpoint
169
- if (url.pathname === "/chat" && req.method === "POST") {
170
- try {
171
- const body = (await req.json()) as ChatRequest;
172
- const { sessionId, message, model } = body;
173
- const result = await handleChat(sessionId, message, context, model);
174
- return Response.json(result, { headers: corsHeaders });
175
- } catch (error) {
176
- return Response.json(
177
- { error: String(error) },
178
- { status: 500, headers: corsHeaders },
179
- );
180
- }
181
- }
182
-
183
- // Metadata enhancement endpoint
184
- if (url.pathname === "/metadata/enhance" && req.method === "POST") {
185
- try {
186
- const body = (await req.json()) as MetadataEnhanceRequest;
187
- const { docId } = body;
188
- await handleMetadataEnhancement(docId, context);
189
- return Response.json({ status: "success" }, { headers: corsHeaders });
190
- } catch (error) {
191
- return Response.json(
192
- { error: String(error) },
193
- { status: 500, headers: corsHeaders },
194
- );
195
- }
196
- }
197
-
198
- // Graph Stats endpoint
199
- if (url.pathname === "/graph/stats" && req.method === "GET") {
200
- return Response.json(graphEngine.getStats(), { headers: corsHeaders });
201
- }
202
-
203
- // Search endpoints (analysis, rerank, context)
204
- if (url.pathname === "/search/analyze" && req.method === "POST") {
205
- try {
206
- const body = (await req.json()) as SearchAnalyzeRequest;
207
- const { query } = body;
208
- const result = await handleSearchAnalysis(query, context);
209
- return Response.json(result, { headers: corsHeaders });
210
- } catch (error) {
211
- return Response.json(
212
- { error: String(error) },
213
- { status: 500, headers: corsHeaders },
214
- );
215
- }
216
- }
217
-
218
- if (url.pathname === "/search/rerank" && req.method === "POST") {
219
- try {
220
- const body = (await req.json()) as SearchRerankRequest;
221
- const { results, query, intent } = body;
222
- const result = await handleResultReranking(results, query, intent);
223
- return Response.json(result, { headers: corsHeaders });
224
- } catch (error) {
225
- return Response.json(
226
- { error: String(error) },
227
- { status: 500, headers: corsHeaders },
228
- );
229
- }
230
- }
231
-
232
- if (url.pathname === "/search/context" && req.method === "POST") {
233
- try {
234
- const body = (await req.json()) as SearchContextRequest;
235
- const { result, query } = body;
236
- const contextResult = await handleContextExtraction(result, query);
237
- return Response.json(contextResult, { headers: corsHeaders });
238
- } catch (error) {
239
- return Response.json(
240
- { error: String(error) },
241
- { status: 500, headers: corsHeaders },
242
- );
243
- }
244
- }
245
-
246
- return new Response("Not Found", { status: 404, headers: corsHeaders });
247
- },
129
+ fetch: app.fetch,
248
130
  });
249
131
 
250
132
  log.info(`Server started on port ${port}`);
@@ -0,0 +1,133 @@
1
+ import { loadConfig } from "@src/config/defaults";
2
+ import { Hono } from "hono";
3
+ import { cors } from "hono/cors";
4
+ import { inferenceState } from "./sonar-inference";
5
+ import {
6
+ handleChat,
7
+ handleContextExtraction,
8
+ handleMetadataEnhancement,
9
+ handleResultReranking,
10
+ handleSearchAnalysis,
11
+ type SonarContext,
12
+ } from "./sonar-logic";
13
+ import type {
14
+ ChatRequest,
15
+ MetadataEnhanceRequest,
16
+ SearchAnalyzeRequest,
17
+ SearchContextRequest,
18
+ SearchRerankRequest,
19
+ } from "./sonar-types";
20
+
21
+ /**
22
+ * Creates the Hono application for the Sonar Agent
23
+ */
24
+ export function createSonarApp(context: SonarContext) {
25
+ const app = new Hono();
26
+
27
+ // Global Middleware
28
+ app.use(
29
+ "*",
30
+ cors({
31
+ origin: "*",
32
+ allowMethods: ["GET", "POST", "OPTIONS"],
33
+ allowHeaders: ["Content-Type"],
34
+ }),
35
+ );
36
+
37
+ /**
38
+ * Health Check
39
+ */
40
+ app.get("/health", async (c) => {
41
+ const cfg = await loadConfig();
42
+ const provider = cfg.sonar.cloud?.enabled ? "cloud" : "local";
43
+ const model = cfg.sonar.cloud?.enabled
44
+ ? cfg.sonar.cloud.model
45
+ : inferenceState.ollamaModel || cfg.sonar.model;
46
+
47
+ return c.json({
48
+ status: "ok",
49
+ ollama: inferenceState.ollamaAvailable,
50
+ provider,
51
+ model,
52
+ });
53
+ });
54
+
55
+ /**
56
+ * Chat Interface
57
+ */
58
+ app.post("/chat", async (c) => {
59
+ try {
60
+ const body = await c.req.json<ChatRequest>();
61
+ const { sessionId, message, model } = body;
62
+ const result = await handleChat(sessionId, message, context, model);
63
+ return c.json(result);
64
+ } catch (error) {
65
+ return c.json({ error: String(error) }, 500);
66
+ }
67
+ });
68
+
69
+ /**
70
+ * Metadata Enhancement
71
+ */
72
+ app.post("/metadata/enhance", async (c) => {
73
+ try {
74
+ const body = await c.req.json<MetadataEnhanceRequest>();
75
+ const { docId } = body;
76
+ await handleMetadataEnhancement(docId, context);
77
+ return c.json({ status: "success" });
78
+ } catch (error) {
79
+ return c.json({ error: String(error) }, 500);
80
+ }
81
+ });
82
+
83
+ /**
84
+ * Graph Stats
85
+ */
86
+ app.get("/graph/stats", (c) => {
87
+ return c.json(context.graphEngine.getStats());
88
+ });
89
+
90
+ /**
91
+ * Search: Query Analysis
92
+ */
93
+ app.post("/search/analyze", async (c) => {
94
+ try {
95
+ const body = await c.req.json<SearchAnalyzeRequest>();
96
+ const { query } = body;
97
+ const result = await handleSearchAnalysis(query, context);
98
+ return c.json(result);
99
+ } catch (error) {
100
+ return c.json({ error: String(error) }, 500);
101
+ }
102
+ });
103
+
104
+ /**
105
+ * Search: Reranking
106
+ */
107
+ app.post("/search/rerank", async (c) => {
108
+ try {
109
+ const body = await c.req.json<SearchRerankRequest>();
110
+ const { results, query, intent } = body;
111
+ const result = await handleResultReranking(results, query, intent);
112
+ return c.json(result);
113
+ } catch (error) {
114
+ return c.json({ error: String(error) }, 500);
115
+ }
116
+ });
117
+
118
+ /**
119
+ * Search: Context Extraction
120
+ */
121
+ app.post("/search/context", async (c) => {
122
+ try {
123
+ const body = await c.req.json<SearchContextRequest>();
124
+ const { result, query } = body;
125
+ const contextResult = await handleContextExtraction(result, query);
126
+ return c.json(contextResult);
127
+ } catch (error) {
128
+ return c.json({ error: String(error) }, 500);
129
+ }
130
+ });
131
+
132
+ return app;
133
+ }
@@ -13,7 +13,7 @@ import { Database } from "bun:sqlite";
13
13
  export const DatabaseFactory = {
14
14
  /**
15
15
  * Connects specifically to the main Resonance Graph database.
16
- * @deprecated Use connect() with explicit path from config instead.
16
+ * Kept for convenience and backward compatibility.
17
17
  */
18
18
  connectToResonance(
19
19
  dbPath: string = ".amalfa/resonance.db",
@@ -0,0 +1,25 @@
1
+ # Drizzle Schema Management
2
+
3
+ ## ⚠️ CRITICAL ARCHITECTURE RULE
4
+
5
+ **Drizzle ORM is used HERE strictly for:**
6
+ 1. **Schema Definition:** Single source of truth for table structures (`schema.ts`).
7
+ 2. **Migrations:** Generating and running SQL migrations (`drizzle-kit`).
8
+
9
+ **Drizzle ORM is NOT used for:**
10
+ - **Runtime Queries:** Do NOT use Drizzle `db.select()` query builders in critical paths.
11
+ - **Data Access:** All runtime data access uses `bun:sqlite` with raw parameterized SQL (`db.prepare()`).
12
+
13
+ ## Why?
14
+ Our **FAFCAS** (Fast, Autonomous, Fractionally-Connected Agent System) architecture demands raw IO performance. We treat SQLite as a raw storage engine. The overhead of an ORM query builder—even a light one—is unnecessary.
15
+
16
+ ## ⛔ Agent Guardrail
17
+ If you are an AI Agent reading this:
18
+ - **DO NOT** refactor the existing `ResonanceDB` SQL queries to use Drizzle.
19
+ - **DO NOT** introduce Drizzle usage into `GraphEngine` or `VectorEngine`.
20
+ - **ONLY** modify this folder if the Database Schema (tables/columns) needs to change.
21
+
22
+ ## Workflow for Schema Changes
23
+ 1. Edit `schema.ts`
24
+ 2. Run `bunx drizzle-kit generate`
25
+ 3. Run migrations (automated via scripts)
@@ -0,0 +1,30 @@
1
+ CREATE TABLE `edges` (
2
+ `source` text NOT NULL,
3
+ `target` text NOT NULL,
4
+ `type` text NOT NULL,
5
+ `confidence` real DEFAULT 1,
6
+ `veracity` real DEFAULT 1,
7
+ `context_source` text,
8
+ PRIMARY KEY(`source`, `target`, `type`)
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE INDEX `idx_edges_source` ON `edges` (`source`);--> statement-breakpoint
12
+ CREATE INDEX `idx_edges_target` ON `edges` (`target`);--> statement-breakpoint
13
+ CREATE TABLE `ember_state` (
14
+ `file_path` text PRIMARY KEY NOT NULL,
15
+ `last_analyzed` text,
16
+ `sidecar_created` integer,
17
+ `confidence` real
18
+ );
19
+ --> statement-breakpoint
20
+ CREATE TABLE `nodes` (
21
+ `id` text PRIMARY KEY NOT NULL,
22
+ `type` text,
23
+ `title` text,
24
+ `domain` text,
25
+ `layer` text,
26
+ `embedding` blob,
27
+ `hash` text,
28
+ `meta` text,
29
+ `date` text
30
+ );
@@ -0,0 +1,199 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "577c9f07-f198-49d2-9fa3-f222a6ecee23",
5
+ "prevId": "00000000-0000-0000-0000-000000000000",
6
+ "tables": {
7
+ "edges": {
8
+ "name": "edges",
9
+ "columns": {
10
+ "source": {
11
+ "name": "source",
12
+ "type": "text",
13
+ "primaryKey": false,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "target": {
18
+ "name": "target",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "type": {
25
+ "name": "type",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ },
31
+ "confidence": {
32
+ "name": "confidence",
33
+ "type": "real",
34
+ "primaryKey": false,
35
+ "notNull": false,
36
+ "autoincrement": false,
37
+ "default": 1
38
+ },
39
+ "veracity": {
40
+ "name": "veracity",
41
+ "type": "real",
42
+ "primaryKey": false,
43
+ "notNull": false,
44
+ "autoincrement": false,
45
+ "default": 1
46
+ },
47
+ "context_source": {
48
+ "name": "context_source",
49
+ "type": "text",
50
+ "primaryKey": false,
51
+ "notNull": false,
52
+ "autoincrement": false
53
+ }
54
+ },
55
+ "indexes": {
56
+ "idx_edges_source": {
57
+ "name": "idx_edges_source",
58
+ "columns": ["source"],
59
+ "isUnique": false
60
+ },
61
+ "idx_edges_target": {
62
+ "name": "idx_edges_target",
63
+ "columns": ["target"],
64
+ "isUnique": false
65
+ }
66
+ },
67
+ "foreignKeys": {},
68
+ "compositePrimaryKeys": {
69
+ "edges_source_target_type_pk": {
70
+ "columns": ["source", "target", "type"],
71
+ "name": "edges_source_target_type_pk"
72
+ }
73
+ },
74
+ "uniqueConstraints": {},
75
+ "checkConstraints": {}
76
+ },
77
+ "ember_state": {
78
+ "name": "ember_state",
79
+ "columns": {
80
+ "file_path": {
81
+ "name": "file_path",
82
+ "type": "text",
83
+ "primaryKey": true,
84
+ "notNull": true,
85
+ "autoincrement": false
86
+ },
87
+ "last_analyzed": {
88
+ "name": "last_analyzed",
89
+ "type": "text",
90
+ "primaryKey": false,
91
+ "notNull": false,
92
+ "autoincrement": false
93
+ },
94
+ "sidecar_created": {
95
+ "name": "sidecar_created",
96
+ "type": "integer",
97
+ "primaryKey": false,
98
+ "notNull": false,
99
+ "autoincrement": false
100
+ },
101
+ "confidence": {
102
+ "name": "confidence",
103
+ "type": "real",
104
+ "primaryKey": false,
105
+ "notNull": false,
106
+ "autoincrement": false
107
+ }
108
+ },
109
+ "indexes": {},
110
+ "foreignKeys": {},
111
+ "compositePrimaryKeys": {},
112
+ "uniqueConstraints": {},
113
+ "checkConstraints": {}
114
+ },
115
+ "nodes": {
116
+ "name": "nodes",
117
+ "columns": {
118
+ "id": {
119
+ "name": "id",
120
+ "type": "text",
121
+ "primaryKey": true,
122
+ "notNull": true,
123
+ "autoincrement": false
124
+ },
125
+ "type": {
126
+ "name": "type",
127
+ "type": "text",
128
+ "primaryKey": false,
129
+ "notNull": false,
130
+ "autoincrement": false
131
+ },
132
+ "title": {
133
+ "name": "title",
134
+ "type": "text",
135
+ "primaryKey": false,
136
+ "notNull": false,
137
+ "autoincrement": false
138
+ },
139
+ "domain": {
140
+ "name": "domain",
141
+ "type": "text",
142
+ "primaryKey": false,
143
+ "notNull": false,
144
+ "autoincrement": false
145
+ },
146
+ "layer": {
147
+ "name": "layer",
148
+ "type": "text",
149
+ "primaryKey": false,
150
+ "notNull": false,
151
+ "autoincrement": false
152
+ },
153
+ "embedding": {
154
+ "name": "embedding",
155
+ "type": "blob",
156
+ "primaryKey": false,
157
+ "notNull": false,
158
+ "autoincrement": false
159
+ },
160
+ "hash": {
161
+ "name": "hash",
162
+ "type": "text",
163
+ "primaryKey": false,
164
+ "notNull": false,
165
+ "autoincrement": false
166
+ },
167
+ "meta": {
168
+ "name": "meta",
169
+ "type": "text",
170
+ "primaryKey": false,
171
+ "notNull": false,
172
+ "autoincrement": false
173
+ },
174
+ "date": {
175
+ "name": "date",
176
+ "type": "text",
177
+ "primaryKey": false,
178
+ "notNull": false,
179
+ "autoincrement": false
180
+ }
181
+ },
182
+ "indexes": {},
183
+ "foreignKeys": {},
184
+ "compositePrimaryKeys": {},
185
+ "uniqueConstraints": {},
186
+ "checkConstraints": {}
187
+ }
188
+ },
189
+ "views": {},
190
+ "enums": {},
191
+ "_meta": {
192
+ "schemas": {},
193
+ "tables": {},
194
+ "columns": {}
195
+ },
196
+ "internal": {
197
+ "indexes": {}
198
+ }
199
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1767981411501,
9
+ "tag": "0000_happy_thaddeus_ross",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ blob,
3
+ index,
4
+ integer,
5
+ primaryKey,
6
+ real,
7
+ sqliteTable,
8
+ text,
9
+ } from "drizzle-orm/sqlite-core";
10
+
11
+ /**
12
+ * NODES Table
13
+ * Core entity storage. Now "Hollow" (no content).
14
+ */
15
+ export const nodes = sqliteTable("nodes", {
16
+ id: text("id").primaryKey(),
17
+ type: text("type"),
18
+ title: text("title"),
19
+ domain: text("domain"),
20
+ layer: text("layer"),
21
+ // Embeddings are stored as raw BLOBs (Float32Array bytes)
22
+ embedding: blob("embedding"),
23
+ hash: text("hash"),
24
+ meta: text("meta"), // JSON string
25
+ date: text("date"), // ISO string or YYYY-MM-DD
26
+ });
27
+
28
+ /**
29
+ * EDGES Table
30
+ * Defines relationships between nodes.
31
+ */
32
+ export const edges = sqliteTable(
33
+ "edges",
34
+ {
35
+ source: text("source").notNull(),
36
+ target: text("target").notNull(),
37
+ type: text("type").notNull(),
38
+ confidence: real("confidence").default(1.0),
39
+ veracity: real("veracity").default(1.0),
40
+ contextSource: text("context_source"),
41
+ },
42
+ (table) => ({
43
+ // Composite Primary Key
44
+ pk: primaryKey({ columns: [table.source, table.target, table.type] }),
45
+ // Indices for traversal speed
46
+ sourceIdx: index("idx_edges_source").on(table.source),
47
+ targetIdx: index("idx_edges_target").on(table.target),
48
+ }),
49
+ );
50
+
51
+ /**
52
+ * EMBER STATE Table (Pilot)
53
+ * Tracks the state of the Ember Service (automated enrichment).
54
+ */
55
+ export const emberState = sqliteTable("ember_state", {
56
+ filePath: text("file_path").primaryKey(),
57
+ lastAnalyzed: text("last_analyzed"),
58
+ sidecarCreated: integer("sidecar_created", { mode: "boolean" }),
59
+ confidence: real("confidence"),
60
+ });