@shahmilsaari/memory-core 0.2.16 → 0.2.17
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 +79 -5
- package/dist/chunk-T4WJR6L6.js +1287 -0
- package/dist/cli.js +341 -1463
- package/dist/dashboard/assets/index-BCu-gBna.js +2 -0
- package/dist/dashboard/assets/index-BxS_xPdw.css +1 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/dashboard-server-EVN4FL4L.js +547 -0
- package/package.json +13 -3
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
detectProject,
|
|
4
|
+
getChatProviderLabel,
|
|
5
|
+
startWatch
|
|
6
|
+
} from "./chunk-T4WJR6L6.js";
|
|
7
|
+
import {
|
|
8
|
+
embed
|
|
9
|
+
} from "./chunk-HAGRPKR3.js";
|
|
10
|
+
import {
|
|
11
|
+
closePool,
|
|
12
|
+
deleteMemory,
|
|
13
|
+
getPool,
|
|
14
|
+
listMemories,
|
|
15
|
+
saveMemory,
|
|
16
|
+
updateMemory
|
|
17
|
+
} from "./chunk-WUL7HLAA.js";
|
|
18
|
+
import {
|
|
19
|
+
Config
|
|
20
|
+
} from "./chunk-KSLFLWB4.js";
|
|
21
|
+
|
|
22
|
+
// src/dashboard-server.ts
|
|
23
|
+
import { createHash } from "crypto";
|
|
24
|
+
import { createReadStream, existsSync, readFileSync, watch } from "fs";
|
|
25
|
+
import { createServer } from "http";
|
|
26
|
+
import { extname, join, normalize, relative } from "path";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
import chalk from "chalk";
|
|
29
|
+
var clients = /* @__PURE__ */ new Set();
|
|
30
|
+
var fileStatuses = /* @__PURE__ */ new Map();
|
|
31
|
+
var recentEvents = [];
|
|
32
|
+
var RUNTIME_ENV_KEYS = [
|
|
33
|
+
"DATABASE_URL",
|
|
34
|
+
"OLLAMA_URL",
|
|
35
|
+
"OLLAMA_MODEL",
|
|
36
|
+
"CHAT_PROVIDER",
|
|
37
|
+
"CHAT_MODEL",
|
|
38
|
+
"OLLAMA_CHAT_MODEL",
|
|
39
|
+
"CHAT_API_KEY"
|
|
40
|
+
];
|
|
41
|
+
function readJsonFile(path, fallback) {
|
|
42
|
+
if (!existsSync(path)) return fallback;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function readProjectConfig() {
|
|
50
|
+
return readJsonFile(join(process.cwd(), ".memory-core.json"), null);
|
|
51
|
+
}
|
|
52
|
+
function parseEnvFile(raw) {
|
|
53
|
+
const values = {};
|
|
54
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
57
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
58
|
+
if (separatorIndex === -1) continue;
|
|
59
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
60
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
61
|
+
if (key) values[key] = value;
|
|
62
|
+
}
|
|
63
|
+
return values;
|
|
64
|
+
}
|
|
65
|
+
function getRuntimeEnvPath() {
|
|
66
|
+
const memoryEnv = join(process.cwd(), ".memory-core.env");
|
|
67
|
+
return existsSync(memoryEnv) ? memoryEnv : join(process.cwd(), ".env");
|
|
68
|
+
}
|
|
69
|
+
function reloadRuntimeEnv() {
|
|
70
|
+
const envPath = getRuntimeEnvPath();
|
|
71
|
+
for (const key of RUNTIME_ENV_KEYS) {
|
|
72
|
+
delete process.env[key];
|
|
73
|
+
}
|
|
74
|
+
if (!existsSync(envPath)) return;
|
|
75
|
+
const values = parseEnvFile(readFileSync(envPath, "utf-8"));
|
|
76
|
+
for (const [key, value] of Object.entries(values)) {
|
|
77
|
+
if (value !== "") process.env[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function readStats() {
|
|
81
|
+
return readJsonFile(join(process.cwd(), ".memory-core-stats.json"), { rules: {}, files: {} });
|
|
82
|
+
}
|
|
83
|
+
function topEntries(values = {}, limit = 8) {
|
|
84
|
+
return Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([name, count]) => ({ name, count }));
|
|
85
|
+
}
|
|
86
|
+
function redactDatabaseUrl(url) {
|
|
87
|
+
if (!url) return "(not set)";
|
|
88
|
+
return url.replace(/:\/\/([^:@/]+)(?::[^@/]+)?@/, "://$1:***@");
|
|
89
|
+
}
|
|
90
|
+
function parseDatabaseUrl(url) {
|
|
91
|
+
if (!url) {
|
|
92
|
+
return {
|
|
93
|
+
configured: false,
|
|
94
|
+
url: "(not set)",
|
|
95
|
+
host: "(not set)",
|
|
96
|
+
port: "",
|
|
97
|
+
database: "",
|
|
98
|
+
user: ""
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const parsed = new URL(url);
|
|
103
|
+
return {
|
|
104
|
+
configured: true,
|
|
105
|
+
url: redactDatabaseUrl(url),
|
|
106
|
+
host: parsed.hostname,
|
|
107
|
+
port: parsed.port,
|
|
108
|
+
database: parsed.pathname.replace(/^\//, ""),
|
|
109
|
+
user: decodeURIComponent(parsed.username)
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return {
|
|
113
|
+
configured: true,
|
|
114
|
+
url: redactDatabaseUrl(url),
|
|
115
|
+
host: "invalid URL",
|
|
116
|
+
port: "",
|
|
117
|
+
database: "",
|
|
118
|
+
user: ""
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function resolveOllamaModel(ollamaUrl, model) {
|
|
123
|
+
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
124
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
125
|
+
const data = await res.json();
|
|
126
|
+
const models = data.models ?? [];
|
|
127
|
+
const exact = models.find((entry) => entry.name === model);
|
|
128
|
+
const prefixed = models.find((entry) => entry.name.startsWith(`${model}:`));
|
|
129
|
+
return (exact ?? prefixed)?.name ?? null;
|
|
130
|
+
}
|
|
131
|
+
async function getModelStatus() {
|
|
132
|
+
const provider = Config.chatProvider;
|
|
133
|
+
const checkModel = Config.chatModel;
|
|
134
|
+
const embeddingModel = Config.ollamaModel;
|
|
135
|
+
const ollamaUrl = Config.ollamaUrl;
|
|
136
|
+
const status = {
|
|
137
|
+
provider,
|
|
138
|
+
checkModel,
|
|
139
|
+
chatModel: checkModel,
|
|
140
|
+
embeddingModel,
|
|
141
|
+
ollamaUrl,
|
|
142
|
+
label: getChatProviderLabel(),
|
|
143
|
+
ollamaReachable: false,
|
|
144
|
+
checkModelInstalled: provider !== "ollama" ? void 0 : false,
|
|
145
|
+
checkModelResolved: void 0,
|
|
146
|
+
embeddingModelInstalled: false,
|
|
147
|
+
embeddingModelResolved: void 0,
|
|
148
|
+
apiKeyConfigured: provider === "ollama" ? void 0 : Boolean(Config.chatApiKey),
|
|
149
|
+
error: void 0
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
const embedding = await resolveOllamaModel(ollamaUrl, embeddingModel);
|
|
153
|
+
status.ollamaReachable = true;
|
|
154
|
+
status.embeddingModelInstalled = Boolean(embedding);
|
|
155
|
+
status.embeddingModelResolved = embedding ?? void 0;
|
|
156
|
+
if (provider === "ollama") {
|
|
157
|
+
const chat = checkModel === embeddingModel ? embedding : await resolveOllamaModel(ollamaUrl, checkModel);
|
|
158
|
+
status.checkModelInstalled = Boolean(chat);
|
|
159
|
+
status.checkModelResolved = chat ?? void 0;
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
status.error = err.message;
|
|
163
|
+
}
|
|
164
|
+
return status;
|
|
165
|
+
}
|
|
166
|
+
async function getRuntimeStatus(config) {
|
|
167
|
+
const detected = detectProject(process.cwd());
|
|
168
|
+
const declaredArchitectures = [
|
|
169
|
+
config?.backendArchitecture,
|
|
170
|
+
config?.frontendFramework
|
|
171
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
172
|
+
const activeArchitectures = declaredArchitectures.length > 0 ? declaredArchitectures : config?.projectType === "backend" ? ["clean-architecture"] : [];
|
|
173
|
+
const database = parseDatabaseUrl(Config.databaseUrl);
|
|
174
|
+
let postgres = {
|
|
175
|
+
...database,
|
|
176
|
+
connected: false,
|
|
177
|
+
error: database.configured ? void 0 : "DATABASE_URL is not set"
|
|
178
|
+
};
|
|
179
|
+
if (database.configured) {
|
|
180
|
+
try {
|
|
181
|
+
const result = await getPool().query(
|
|
182
|
+
"SELECT current_database() AS database, current_user AS user"
|
|
183
|
+
);
|
|
184
|
+
postgres = {
|
|
185
|
+
...postgres,
|
|
186
|
+
connected: true,
|
|
187
|
+
error: void 0,
|
|
188
|
+
serverDatabase: result.rows[0]?.database,
|
|
189
|
+
serverUser: result.rows[0]?.user
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
postgres = {
|
|
193
|
+
...postgres,
|
|
194
|
+
connected: false,
|
|
195
|
+
error: err.message
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const model = await getModelStatus();
|
|
200
|
+
return {
|
|
201
|
+
project: {
|
|
202
|
+
name: config?.projectName ?? process.cwd().split("/").pop() ?? "project",
|
|
203
|
+
type: config?.projectType ?? "unknown",
|
|
204
|
+
language: config?.language ?? detected.language,
|
|
205
|
+
initialized: config !== null,
|
|
206
|
+
declaredArchitectures,
|
|
207
|
+
activeArchitectures,
|
|
208
|
+
detected
|
|
209
|
+
},
|
|
210
|
+
postgres,
|
|
211
|
+
model
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function isRelevantMemory(memory, config, activeArchitectures) {
|
|
215
|
+
const currentProject = config?.projectName;
|
|
216
|
+
if (memory.project_name && memory.project_name !== currentProject) return false;
|
|
217
|
+
if (!["rule", "pattern", "decision", "ignore"].includes(memory.type)) return false;
|
|
218
|
+
if (memory.architecture && memory.architecture !== "global") {
|
|
219
|
+
return activeArchitectures.includes(memory.architecture);
|
|
220
|
+
}
|
|
221
|
+
const tags = new Set(memory.tags ?? []);
|
|
222
|
+
const knownArchitectureTags = /* @__PURE__ */ new Set([
|
|
223
|
+
"angular",
|
|
224
|
+
"clean-architecture",
|
|
225
|
+
"express",
|
|
226
|
+
"fastify",
|
|
227
|
+
"go-api",
|
|
228
|
+
"hexagonal",
|
|
229
|
+
"laravel-service-repository",
|
|
230
|
+
"modular-monolith",
|
|
231
|
+
"mvc",
|
|
232
|
+
"nestjs",
|
|
233
|
+
"nextjs",
|
|
234
|
+
"nuxt",
|
|
235
|
+
"react",
|
|
236
|
+
"react-native",
|
|
237
|
+
"svelte",
|
|
238
|
+
"vue"
|
|
239
|
+
]);
|
|
240
|
+
const architectureTags = [...tags].filter((tag) => knownArchitectureTags.has(tag));
|
|
241
|
+
if (architectureTags.length === 0) return true;
|
|
242
|
+
return architectureTags.some((tag) => activeArchitectures.includes(tag));
|
|
243
|
+
}
|
|
244
|
+
function filterMemoriesForProject(memories, config, activeArchitectures) {
|
|
245
|
+
return memories.filter((memory) => isRelevantMemory(memory, config, activeArchitectures));
|
|
246
|
+
}
|
|
247
|
+
async function getSnapshot() {
|
|
248
|
+
const config = readProjectConfig();
|
|
249
|
+
const runtime = await getRuntimeStatus(config);
|
|
250
|
+
let memories = [];
|
|
251
|
+
let dbError;
|
|
252
|
+
try {
|
|
253
|
+
memories = await listMemories({ limit: 1e4 });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
dbError = err.message;
|
|
256
|
+
}
|
|
257
|
+
const relevantMemories = filterMemoriesForProject(memories, config, runtime.project.activeArchitectures);
|
|
258
|
+
const stats = readStats();
|
|
259
|
+
return {
|
|
260
|
+
config,
|
|
261
|
+
runtime,
|
|
262
|
+
stats: {
|
|
263
|
+
rules: stats.rules ?? {},
|
|
264
|
+
files: stats.files ?? {},
|
|
265
|
+
topRules: topEntries(stats.rules),
|
|
266
|
+
topFiles: topEntries(stats.files)
|
|
267
|
+
},
|
|
268
|
+
files: Array.from(fileStatuses.values()).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen)),
|
|
269
|
+
events: recentEvents,
|
|
270
|
+
memories: relevantMemories,
|
|
271
|
+
memoryCount: {
|
|
272
|
+
total: memories.length,
|
|
273
|
+
relevant: relevantMemories.length
|
|
274
|
+
},
|
|
275
|
+
dbError
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function sendJson(res, status, payload) {
|
|
279
|
+
const body = JSON.stringify(payload);
|
|
280
|
+
res.writeHead(status, {
|
|
281
|
+
"content-type": "application/json; charset=utf-8",
|
|
282
|
+
"content-length": Buffer.byteLength(body)
|
|
283
|
+
});
|
|
284
|
+
res.end(body);
|
|
285
|
+
}
|
|
286
|
+
async function readBody(req) {
|
|
287
|
+
const chunks = [];
|
|
288
|
+
for await (const chunk of req) {
|
|
289
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
290
|
+
}
|
|
291
|
+
if (chunks.length === 0) return {};
|
|
292
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
293
|
+
}
|
|
294
|
+
function parseTags(value) {
|
|
295
|
+
if (Array.isArray(value)) {
|
|
296
|
+
return value.map(String).map((tag) => tag.trim()).filter(Boolean);
|
|
297
|
+
}
|
|
298
|
+
if (typeof value === "string") {
|
|
299
|
+
return value.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
300
|
+
}
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
async function handleApi(req, res, url) {
|
|
304
|
+
try {
|
|
305
|
+
if (req.method === "GET" && url.pathname === "/api/snapshot") {
|
|
306
|
+
sendJson(res, 200, await getSnapshot());
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (req.method === "POST" && url.pathname === "/api/memories") {
|
|
310
|
+
const body = await readBody(req);
|
|
311
|
+
const content = typeof body.content === "string" ? body.content.trim() : "";
|
|
312
|
+
if (!content) {
|
|
313
|
+
sendJson(res, 400, { error: "content is required" });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const config = readProjectConfig();
|
|
317
|
+
await saveMemory({
|
|
318
|
+
type: typeof body.type === "string" ? body.type : "rule",
|
|
319
|
+
scope: typeof body.scope === "string" ? body.scope : "project",
|
|
320
|
+
architecture: typeof config?.backendArchitecture === "string" ? config.backendArchitecture : typeof config?.frontendFramework === "string" ? config.frontendFramework : void 0,
|
|
321
|
+
projectName: typeof config?.projectName === "string" ? config.projectName : void 0,
|
|
322
|
+
title: typeof body.title === "string" ? body.title : void 0,
|
|
323
|
+
content,
|
|
324
|
+
reason: typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : void 0,
|
|
325
|
+
context: {},
|
|
326
|
+
tags: parseTags(body.tags),
|
|
327
|
+
embedding: await embed(content)
|
|
328
|
+
});
|
|
329
|
+
await broadcastSnapshot();
|
|
330
|
+
sendJson(res, 201, { ok: true });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const memoryMatch = url.pathname.match(/^\/api\/memories\/(\d+)$/);
|
|
334
|
+
if (memoryMatch && req.method === "PATCH") {
|
|
335
|
+
const id = Number(memoryMatch[1]);
|
|
336
|
+
const body = await readBody(req);
|
|
337
|
+
const content = typeof body.content === "string" ? body.content.trim() : void 0;
|
|
338
|
+
const updated = await updateMemory(id, {
|
|
339
|
+
type: typeof body.type === "string" ? body.type : void 0,
|
|
340
|
+
scope: typeof body.scope === "string" ? body.scope : void 0,
|
|
341
|
+
title: typeof body.title === "string" ? body.title : void 0,
|
|
342
|
+
content,
|
|
343
|
+
reason: typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : void 0,
|
|
344
|
+
tags: parseTags(body.tags),
|
|
345
|
+
embedding: content ? await embed(content) : void 0
|
|
346
|
+
});
|
|
347
|
+
if (!updated) {
|
|
348
|
+
sendJson(res, 404, { error: `No memory found with ID ${id}` });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
await broadcastSnapshot();
|
|
352
|
+
sendJson(res, 200, updated);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (memoryMatch && req.method === "DELETE") {
|
|
356
|
+
const id = Number(memoryMatch[1]);
|
|
357
|
+
const deleted = await deleteMemory(id);
|
|
358
|
+
if (!deleted) {
|
|
359
|
+
sendJson(res, 404, { error: `No memory found with ID ${id}` });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
await broadcastSnapshot();
|
|
363
|
+
sendJson(res, 200, { ok: true });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
sendJson(res, 404, { error: "Not found" });
|
|
367
|
+
} catch (err) {
|
|
368
|
+
sendJson(res, 500, { error: err.message });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function contentType(path) {
|
|
372
|
+
switch (extname(path)) {
|
|
373
|
+
case ".html":
|
|
374
|
+
return "text/html; charset=utf-8";
|
|
375
|
+
case ".js":
|
|
376
|
+
return "text/javascript; charset=utf-8";
|
|
377
|
+
case ".css":
|
|
378
|
+
return "text/css; charset=utf-8";
|
|
379
|
+
case ".svg":
|
|
380
|
+
return "image/svg+xml";
|
|
381
|
+
case ".json":
|
|
382
|
+
return "application/json; charset=utf-8";
|
|
383
|
+
default:
|
|
384
|
+
return "application/octet-stream";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function serveStatic(req, res, url) {
|
|
388
|
+
const dashboardDir = join(fileURLToPath(new URL(".", import.meta.url)), "dashboard");
|
|
389
|
+
const requested = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
390
|
+
const filePath = normalize(join(dashboardDir, requested));
|
|
391
|
+
if (!filePath.startsWith(dashboardDir) || relative(dashboardDir, filePath).startsWith("..")) {
|
|
392
|
+
res.writeHead(403);
|
|
393
|
+
res.end("Forbidden");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!existsSync(filePath)) {
|
|
397
|
+
if (!existsSync(join(dashboardDir, "index.html"))) {
|
|
398
|
+
res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
|
|
399
|
+
res.end("Dashboard assets are missing. Run `npm run build` before `memory-core dashboard`.");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
createReadStream(join(dashboardDir, "index.html")).pipe(res.writeHead(200, { "content-type": contentType("index.html") }));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
createReadStream(filePath).pipe(res.writeHead(200, { "content-type": contentType(filePath) }));
|
|
406
|
+
}
|
|
407
|
+
function encodeFrame(payload) {
|
|
408
|
+
const body = Buffer.from(payload);
|
|
409
|
+
if (body.length <= 125) {
|
|
410
|
+
return Buffer.concat([Buffer.from([129, body.length]), body]);
|
|
411
|
+
}
|
|
412
|
+
if (body.length <= 65535) {
|
|
413
|
+
const header2 = Buffer.alloc(4);
|
|
414
|
+
header2[0] = 129;
|
|
415
|
+
header2[1] = 126;
|
|
416
|
+
header2.writeUInt16BE(body.length, 2);
|
|
417
|
+
return Buffer.concat([header2, body]);
|
|
418
|
+
}
|
|
419
|
+
const header = Buffer.alloc(10);
|
|
420
|
+
header[0] = 129;
|
|
421
|
+
header[1] = 127;
|
|
422
|
+
header.writeBigUInt64BE(BigInt(body.length), 2);
|
|
423
|
+
return Buffer.concat([header, body]);
|
|
424
|
+
}
|
|
425
|
+
function sendWs(socket, payload) {
|
|
426
|
+
if (socket.destroyed) return;
|
|
427
|
+
socket.write(encodeFrame(JSON.stringify(payload)));
|
|
428
|
+
}
|
|
429
|
+
function broadcast(payload) {
|
|
430
|
+
for (const client of clients) sendWs(client, payload);
|
|
431
|
+
}
|
|
432
|
+
async function broadcastSnapshot() {
|
|
433
|
+
broadcast({ type: "snapshot", timestamp: (/* @__PURE__ */ new Date()).toISOString(), snapshot: await getSnapshot() });
|
|
434
|
+
}
|
|
435
|
+
function handleWatchEvent(event) {
|
|
436
|
+
recentEvents.push(event);
|
|
437
|
+
if (recentEvents.length > 100) recentEvents.shift();
|
|
438
|
+
if (event.type === "saved") {
|
|
439
|
+
fileStatuses.set(event.file, {
|
|
440
|
+
file: event.file,
|
|
441
|
+
status: "checking",
|
|
442
|
+
lastSeen: event.timestamp,
|
|
443
|
+
violations: []
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (event.type === "clean") {
|
|
447
|
+
fileStatuses.set(event.file, {
|
|
448
|
+
file: event.file,
|
|
449
|
+
status: "clean",
|
|
450
|
+
lastSeen: event.timestamp,
|
|
451
|
+
violations: []
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (event.type === "violations") {
|
|
455
|
+
fileStatuses.set(event.file, {
|
|
456
|
+
file: event.file,
|
|
457
|
+
status: "violations",
|
|
458
|
+
lastSeen: event.timestamp,
|
|
459
|
+
violations: event.violations
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
broadcast({ type: "watch", event });
|
|
463
|
+
void broadcastSnapshot();
|
|
464
|
+
}
|
|
465
|
+
function acceptWebSocket(req, socket) {
|
|
466
|
+
const key = req.headers["sec-websocket-key"];
|
|
467
|
+
if (typeof key !== "string") {
|
|
468
|
+
socket.destroy();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
472
|
+
socket.write([
|
|
473
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
474
|
+
"Upgrade: websocket",
|
|
475
|
+
"Connection: Upgrade",
|
|
476
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
477
|
+
"",
|
|
478
|
+
""
|
|
479
|
+
].join("\r\n"));
|
|
480
|
+
clients.add(socket);
|
|
481
|
+
socket.on("close", () => clients.delete(socket));
|
|
482
|
+
socket.on("error", () => clients.delete(socket));
|
|
483
|
+
sendWs(socket, { type: "connected", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
484
|
+
void broadcastSnapshot();
|
|
485
|
+
}
|
|
486
|
+
function startConfigWatch() {
|
|
487
|
+
let timer;
|
|
488
|
+
const watchedFiles = [".env", ".memory-core.env", ".memory-core.json", ".memory-core-stats.json"];
|
|
489
|
+
const watchers = watchedFiles.map((file) => join(process.cwd(), file)).filter((filePath) => existsSync(filePath)).map((filePath) => watch(filePath, () => {
|
|
490
|
+
if (timer) clearTimeout(timer);
|
|
491
|
+
timer = setTimeout(() => {
|
|
492
|
+
reloadRuntimeEnv();
|
|
493
|
+
recentEvents.push({
|
|
494
|
+
type: "error",
|
|
495
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
496
|
+
message: `Configuration reloaded from ${filePath.split("/").pop()}`
|
|
497
|
+
});
|
|
498
|
+
if (recentEvents.length > 100) recentEvents.shift();
|
|
499
|
+
void broadcastSnapshot();
|
|
500
|
+
}, 150);
|
|
501
|
+
}));
|
|
502
|
+
return () => {
|
|
503
|
+
if (timer) clearTimeout(timer);
|
|
504
|
+
for (const watcher of watchers) watcher.close();
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function startDashboard(options = {}) {
|
|
508
|
+
reloadRuntimeEnv();
|
|
509
|
+
const port = options.port ?? 5178;
|
|
510
|
+
const stopConfigWatch = startConfigWatch();
|
|
511
|
+
const server = createServer((req, res) => {
|
|
512
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
513
|
+
if (url.pathname.startsWith("/api/")) {
|
|
514
|
+
void handleApi(req, res, url);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
serveStatic(req, res, url);
|
|
518
|
+
});
|
|
519
|
+
server.on("upgrade", (req, socket) => {
|
|
520
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
521
|
+
if (url.pathname !== "/ws") {
|
|
522
|
+
socket.destroy();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
acceptWebSocket(req, socket);
|
|
526
|
+
});
|
|
527
|
+
server.on("close", () => {
|
|
528
|
+
stopConfigWatch();
|
|
529
|
+
void closePool();
|
|
530
|
+
});
|
|
531
|
+
await new Promise((resolve) => {
|
|
532
|
+
server.listen(port, resolve);
|
|
533
|
+
});
|
|
534
|
+
console.log(chalk.green(`
|
|
535
|
+
Dashboard: http://localhost:${port}
|
|
536
|
+
`));
|
|
537
|
+
if (options.watch ?? true) {
|
|
538
|
+
void startWatch({
|
|
539
|
+
path: options.path,
|
|
540
|
+
onEvent: handleWatchEvent,
|
|
541
|
+
exitOnSetupFailure: false
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
export {
|
|
546
|
+
startDashboard
|
|
547
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shahmilsaari/memory-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "Universal AI memory core — generate AI context files from architecture profiles with RAG support",
|
|
5
|
+
"homepage": "https://memory-core.shahmilsaari.my/",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
7
8
|
"memory-core": "dist/cli.js"
|
|
@@ -12,7 +13,13 @@
|
|
|
12
13
|
"profiles"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
|
-
"build": "tsup",
|
|
16
|
+
"build": "tsup && vite build --config dashboard/vite.config.ts",
|
|
17
|
+
"docs:start": "npm run site:start",
|
|
18
|
+
"docs:build": "node scripts/build-static-docs.mjs",
|
|
19
|
+
"docs:serve": "npm run site:serve",
|
|
20
|
+
"site:build": "npm run docs:build && node scripts/assemble-static-site.mjs",
|
|
21
|
+
"site:serve": "node scripts/serve-static-site.mjs --dir site-build --host 127.0.0.1 --port 3010",
|
|
22
|
+
"site:start": "npm run site:build && npm run site:serve",
|
|
16
23
|
"typecheck": "tsc --noEmit",
|
|
17
24
|
"lint": "node scripts/lint.mjs",
|
|
18
25
|
"smoke:pack": "node scripts/pack-smoke.mjs",
|
|
@@ -33,12 +40,15 @@
|
|
|
33
40
|
"pg": "^8.11.0"
|
|
34
41
|
},
|
|
35
42
|
"devDependencies": {
|
|
43
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
36
44
|
"@types/js-yaml": "^4.0.9",
|
|
37
45
|
"@types/node": "^20.0.0",
|
|
38
46
|
"@types/pg": "^8.11.0",
|
|
47
|
+
"svelte": "^5.55.5",
|
|
39
48
|
"tsup": "^8.0.0",
|
|
40
49
|
"tsx": "^4.7.0",
|
|
41
|
-
"typescript": "^5.4.0"
|
|
50
|
+
"typescript": "^5.4.0",
|
|
51
|
+
"vite": "^5.4.21"
|
|
42
52
|
},
|
|
43
53
|
"engines": {
|
|
44
54
|
"node": ">=18.0.0"
|