@pcircle/memesh 4.3.0 → 4.5.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.de.md +35 -12
- package/README.es.md +13 -9
- package/README.fr.md +33 -10
- package/README.ja.md +13 -9
- package/README.ko.md +14 -10
- package/README.md +18 -11
- package/README.pt.md +32 -9
- package/README.th.md +18 -8
- package/README.vi.md +34 -11
- package/README.zh-CN.md +14 -10
- package/README.zh-TW.md +14 -10
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.js +2 -2
- package/dist/cli/view.js +1 -1
- package/dist/core/analytics.d.ts +4 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +8 -8
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/config.d.ts +5 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +28 -13
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo.d.ts +1 -0
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +24 -0
- package/dist/core/demo.js.map +1 -1
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +3 -1
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +70 -60
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +21 -1
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +86 -8
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +1 -4
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +5 -92
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +2 -1
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/llm-client.d.ts.map +1 -1
- package/dist/core/llm-client.js.map +1 -1
- package/dist/core/llm-validator.d.ts +1 -0
- package/dist/core/llm-validator.d.ts.map +1 -1
- package/dist/core/llm-validator.js +33 -10
- package/dist/core/llm-validator.js.map +1 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +10 -3
- package/dist/core/operations.js.map +1 -1
- package/dist/core/output-language.d.ts +6 -0
- package/dist/core/output-language.d.ts.map +1 -0
- package/dist/core/output-language.js +25 -0
- package/dist/core/output-language.js.map +1 -0
- package/dist/core/patterns.d.ts +0 -1
- package/dist/core/patterns.d.ts.map +1 -1
- package/dist/core/patterns.js +1 -5
- package/dist/core/patterns.js.map +1 -1
- package/dist/core/transcript-extractor.d.ts +89 -0
- package/dist/core/transcript-extractor.d.ts.map +1 -0
- package/dist/core/transcript-extractor.js +437 -0
- package/dist/core/transcript-extractor.js.map +1 -0
- package/dist/core/transcript-source.d.ts +21 -0
- package/dist/core/transcript-source.d.ts.map +1 -0
- package/dist/core/transcript-source.js +142 -0
- package/dist/core/transcript-source.js.map +1 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/verifier.js +1 -1
- package/dist/core/verifier.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +10 -0
- package/dist/db.js.map +1 -1
- package/dist/skills-manifest.json +11 -11
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +229 -65
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts +7 -0
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +145 -50
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +1 -1
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +3 -2
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js.map +1 -1
- package/package.json +2 -11
- package/scripts/hooks/post-commit.js +8 -2
- package/scripts/hooks/pre-compact.js +9 -0
- package/scripts/hooks/session-start.js +1 -1
- package/skills/agentic-orchestration/SKILL.md +1 -1
- package/skills/memesh/SKILL.md +2 -0
|
@@ -7,6 +7,7 @@ import { openDatabase, closeDatabase, getDatabase } from '../../db.js';
|
|
|
7
7
|
import { remember, recallWithConflicts, forget, exportMemories, importMemories, learn } from '../../core/operations.js';
|
|
8
8
|
import { KnowledgeGraph } from '../../knowledge-graph.js';
|
|
9
9
|
import { logCapabilities, readConfig, updateConfig, detectCapabilities } from '../../core/config.js';
|
|
10
|
+
import { languageValueError } from '../../core/output-language.js';
|
|
10
11
|
import { computePatterns } from '../../core/patterns.js';
|
|
11
12
|
import { computeAnalytics, computePmAnalytics } from '../../core/analytics.js';
|
|
12
13
|
import { computeStats } from '../../core/stats.js';
|
|
@@ -14,7 +15,7 @@ import { computeProjects } from '../../core/projects.js';
|
|
|
14
15
|
import { computeGraph } from '../../core/graph.js';
|
|
15
16
|
import { verifyAgentWork } from '../../core/verifier.js';
|
|
16
17
|
import { RememberSchema as RememberBody, RecallSchema as RecallBody, ForgetSchema as ForgetBody, ExportSchema as ExportBody, ImportSchema as ImportBody, LearnSchema as LearnBody, VerifyAgentWorkSchema as VerifyBody, } from '../schemas.js';
|
|
17
|
-
import { getUpdateCheck } from '../../core/version-check.js';
|
|
18
|
+
import { checkForUpdate, getLastUpdateCheck, getUpdateCheck } from '../../core/version-check.js';
|
|
18
19
|
import { getCurrentInstallChannel, getInstallChannelSupport } from '../../core/install-channel.js';
|
|
19
20
|
import { getDbPath, getMemeshDirFromDbPath } from '../../core/paths.js';
|
|
20
21
|
import { RETIRED_ROUTES } from './retired-routes.js';
|
|
@@ -25,12 +26,23 @@ const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)
|
|
|
25
26
|
const packageRoot = path.dirname(packageJsonPath);
|
|
26
27
|
const packageVersion = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version ?? '0.0.0';
|
|
27
28
|
const app = express();
|
|
29
|
+
export function isLoopbackRequest(req) {
|
|
30
|
+
const ip = req.ip ?? '';
|
|
31
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
32
|
+
}
|
|
28
33
|
const apiLimiter = rateLimit({
|
|
29
34
|
windowMs: 15 * 60 * 1000,
|
|
30
35
|
max: 100,
|
|
31
36
|
standardHeaders: true,
|
|
32
37
|
legacyHeaders: false,
|
|
33
|
-
|
|
38
|
+
skip: (req) => isLoopbackRequest(req),
|
|
39
|
+
handler: (_req, res) => {
|
|
40
|
+
res.status(429).json({
|
|
41
|
+
success: false,
|
|
42
|
+
errorCode: 'rate.limited',
|
|
43
|
+
error: 'Too many requests in a short time. Wait a moment and try again.',
|
|
44
|
+
});
|
|
45
|
+
},
|
|
34
46
|
});
|
|
35
47
|
let remoteToken = null;
|
|
36
48
|
const serverAuthRequired = new WeakMap();
|
|
@@ -97,6 +109,7 @@ function bearerAuth(req, res, next) {
|
|
|
97
109
|
if (!remoteToken) {
|
|
98
110
|
res.status(503).json({
|
|
99
111
|
success: false,
|
|
112
|
+
errorCode: 'auth.not-configured',
|
|
100
113
|
error: 'remote bearer auth not configured on this server',
|
|
101
114
|
});
|
|
102
115
|
return;
|
|
@@ -105,17 +118,17 @@ function bearerAuth(req, res, next) {
|
|
|
105
118
|
const trimmed = header.trim();
|
|
106
119
|
const wsIndex = trimmed.search(/\s/);
|
|
107
120
|
if (wsIndex < 0 || trimmed.slice(0, wsIndex).toLowerCase() !== 'bearer') {
|
|
108
|
-
res.status(401).json({ success: false, error: 'Missing Authorization: Bearer <token>' });
|
|
121
|
+
res.status(401).json({ success: false, errorCode: 'auth.missing-bearer', error: 'Missing Authorization: Bearer <token>' });
|
|
109
122
|
return;
|
|
110
123
|
}
|
|
111
124
|
const tokenPart = trimmed.slice(wsIndex + 1).trim();
|
|
112
125
|
if (!tokenPart) {
|
|
113
|
-
res.status(401).json({ success: false, error: 'Missing Authorization: Bearer <token>' });
|
|
126
|
+
res.status(401).json({ success: false, errorCode: 'auth.missing-bearer', error: 'Missing Authorization: Bearer <token>' });
|
|
114
127
|
return;
|
|
115
128
|
}
|
|
116
129
|
const presented = Buffer.from(tokenPart, 'utf8');
|
|
117
130
|
if (!constantTimeEquals(presented, remoteToken)) {
|
|
118
|
-
res.status(401).json({ success: false, error: 'Invalid bearer token' });
|
|
131
|
+
res.status(401).json({ success: false, errorCode: 'auth.invalid-token', error: 'Invalid bearer token' });
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
121
134
|
next();
|
|
@@ -127,11 +140,21 @@ function payloadTooLargeHandler(err, _req, res, next) {
|
|
|
127
140
|
if (!err || typeof err !== 'object')
|
|
128
141
|
return next(err);
|
|
129
142
|
const e = err;
|
|
143
|
+
if (e.type === 'entity.parse.failed' || (err instanceof SyntaxError && (e.status === 400 || e.statusCode === 400))) {
|
|
144
|
+
res.status(400).json({
|
|
145
|
+
success: false,
|
|
146
|
+
errorCode: 'validation.bad-body',
|
|
147
|
+
error: 'Request body is not valid JSON.',
|
|
148
|
+
hint: 'Send a JSON object with Content-Type: application/json.',
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
130
152
|
const isTooLarge = e.type === 'entity.too.large' || e.status === 413 || e.statusCode === 413;
|
|
131
153
|
if (!isTooLarge)
|
|
132
154
|
return next(err);
|
|
133
155
|
res.status(413).json({
|
|
134
156
|
success: false,
|
|
157
|
+
errorCode: 'payload.too-large',
|
|
135
158
|
error: 'Request body exceeds the 1MB limit',
|
|
136
159
|
code: 'PAYLOAD_TOO_LARGE',
|
|
137
160
|
limit: '1mb',
|
|
@@ -171,12 +194,13 @@ app.get('/v1/health', (_req, res) => {
|
|
|
171
194
|
if (message === 'Database not opened') {
|
|
172
195
|
res.status(503).json({
|
|
173
196
|
success: false,
|
|
197
|
+
errorCode: 'server.internal',
|
|
174
198
|
error: 'Database not initialized',
|
|
175
199
|
details: 'MeMesh database failed to open at startup. Check server logs for details, or run "memesh doctor" to diagnose.',
|
|
176
200
|
});
|
|
177
201
|
}
|
|
178
202
|
else {
|
|
179
|
-
res.status(500).json({ success: false, error: message });
|
|
203
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: message });
|
|
180
204
|
}
|
|
181
205
|
}
|
|
182
206
|
});
|
|
@@ -201,33 +225,49 @@ app.get('/v1/doctor', async (_req, res) => {
|
|
|
201
225
|
res.json({ success: true, data: safe });
|
|
202
226
|
}
|
|
203
227
|
catch (err) {
|
|
204
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
228
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
205
229
|
}
|
|
206
230
|
});
|
|
231
|
+
function requireJsonBody(req, res) {
|
|
232
|
+
if (req.body !== undefined)
|
|
233
|
+
return true;
|
|
234
|
+
res.status(400).json({
|
|
235
|
+
success: false,
|
|
236
|
+
errorCode: 'validation.bad-body',
|
|
237
|
+
error: 'No JSON body was parsed from this request.',
|
|
238
|
+
hint: 'Send the payload with Content-Type: application/json.',
|
|
239
|
+
});
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
207
242
|
function handlePost(schema, req, res, handler) {
|
|
243
|
+
if (!requireJsonBody(req, res))
|
|
244
|
+
return;
|
|
208
245
|
const parsed = schema.safeParse(req.body);
|
|
209
246
|
if (!parsed.success) {
|
|
210
247
|
res.status(400).json({
|
|
211
248
|
success: false,
|
|
249
|
+
errorCode: 'validation.bad-body',
|
|
212
250
|
error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
|
|
213
251
|
});
|
|
214
252
|
return;
|
|
215
253
|
}
|
|
216
254
|
Promise.resolve(handler(parsed.data))
|
|
217
255
|
.then((data) => res.json({ success: true, data }))
|
|
218
|
-
.catch((err) => res.status(400).json({ success: false, error: err instanceof Error ? err.message : String(err) }));
|
|
256
|
+
.catch((err) => res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) }));
|
|
219
257
|
}
|
|
220
258
|
function handleGet(res, produce) {
|
|
221
259
|
Promise.resolve()
|
|
222
260
|
.then(produce)
|
|
223
261
|
.then((data) => res.json({ success: true, data }))
|
|
224
|
-
.catch((err) => res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) }));
|
|
262
|
+
.catch((err) => res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) }));
|
|
225
263
|
}
|
|
226
264
|
app.post('/v1/remember', (req, res) => handlePost(RememberBody, req, res, remember));
|
|
227
265
|
app.post('/v1/recall', async (req, res) => {
|
|
266
|
+
if (!requireJsonBody(req, res))
|
|
267
|
+
return;
|
|
228
268
|
const parsed = RecallBody.safeParse(req.body);
|
|
229
269
|
if (!parsed.success) {
|
|
230
|
-
res.status(400).json({ success: false, error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
|
|
270
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
|
|
231
271
|
return;
|
|
232
272
|
}
|
|
233
273
|
try {
|
|
@@ -235,27 +275,44 @@ app.post('/v1/recall', async (req, res) => {
|
|
|
235
275
|
res.json({ success: true, data: conflicts.length > 0 ? { entities, conflicts } : entities });
|
|
236
276
|
}
|
|
237
277
|
catch (err) {
|
|
238
|
-
res.status(400).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
278
|
+
res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) });
|
|
239
279
|
}
|
|
240
280
|
});
|
|
241
281
|
app.post('/v1/forget', (req, res) => handlePost(ForgetBody, req, res, forget));
|
|
242
282
|
app.post('/v1/consolidate', (_req, res) => {
|
|
243
|
-
res.status(410).json({ success: false, error: RETIRED_ROUTES['/v1/consolidate'] });
|
|
283
|
+
res.status(410).json({ success: false, errorCode: 'route.retired', error: RETIRED_ROUTES['/v1/consolidate'] });
|
|
244
284
|
});
|
|
245
285
|
app.post('/v1/export', (req, res) => handlePost(ExportBody, req, res, exportMemories));
|
|
246
286
|
app.post('/v1/import', (req, res) => handlePost(ImportBody, req, res, importMemories));
|
|
247
287
|
app.post('/v1/learn', (req, res) => handlePost(LearnBody, req, res, learn));
|
|
248
288
|
app.post('/v1/verify', (req, res) => handlePost(VerifyBody, req, res, verifyAgentWork));
|
|
289
|
+
const API_KEY_MASK = '***';
|
|
249
290
|
function maskLlmSecrets(obj) {
|
|
250
291
|
const masked = { ...obj };
|
|
251
292
|
if (masked.llm?.apiKey) {
|
|
252
|
-
masked.llm = { ...masked.llm, apiKey:
|
|
293
|
+
masked.llm = { ...masked.llm, apiKey: API_KEY_MASK };
|
|
253
294
|
}
|
|
254
295
|
if (Array.isArray(masked.llmFallbacks) && masked.llmFallbacks.length > 0) {
|
|
255
|
-
masked.llmFallbacks = masked.llmFallbacks.map(fb => fb?.apiKey ? { ...fb, apiKey:
|
|
296
|
+
masked.llmFallbacks = masked.llmFallbacks.map(fb => fb?.apiKey ? { ...fb, apiKey: API_KEY_MASK } : fb);
|
|
256
297
|
}
|
|
257
298
|
return masked;
|
|
258
299
|
}
|
|
300
|
+
function preserveFallbackApiKeys(incoming, stored) {
|
|
301
|
+
return incoming.map((entry) => {
|
|
302
|
+
const { keepKeyFrom, ...clean } = entry;
|
|
303
|
+
if (clean.apiKey === API_KEY_MASK)
|
|
304
|
+
delete clean.apiKey;
|
|
305
|
+
if (clean.apiKey)
|
|
306
|
+
return clean;
|
|
307
|
+
if (typeof keepKeyFrom === 'number' && stored && keepKeyFrom >= 0 && keepKeyFrom < stored.length) {
|
|
308
|
+
const src = stored[keepKeyFrom];
|
|
309
|
+
if (src && src.provider === clean.provider && src.apiKey) {
|
|
310
|
+
return { ...clean, apiKey: src.apiKey };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return clean;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
259
316
|
app.get('/v1/config', (_req, res) => {
|
|
260
317
|
try {
|
|
261
318
|
const config = readConfig();
|
|
@@ -263,7 +320,7 @@ app.get('/v1/config', (_req, res) => {
|
|
|
263
320
|
res.json({ success: true, data: { config: maskLlmSecrets(config), capabilities: maskLlmSecrets(caps) } });
|
|
264
321
|
}
|
|
265
322
|
catch (err) {
|
|
266
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
323
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
267
324
|
}
|
|
268
325
|
});
|
|
269
326
|
const ConfigBody = z.object({
|
|
@@ -279,56 +336,77 @@ const ConfigBody = z.object({
|
|
|
279
336
|
provider: z.enum(['anthropic', 'openai', 'ollama']),
|
|
280
337
|
model: z.string().optional(),
|
|
281
338
|
apiKey: z.string().optional(),
|
|
339
|
+
keepKeyFrom: z.number().int().nonnegative().nullable().optional(),
|
|
282
340
|
})).optional(),
|
|
283
341
|
autoCapture: z.boolean().optional(),
|
|
284
342
|
sessionLimit: z.number().int().min(1).max(100).optional(),
|
|
285
343
|
enableAgenticOrchestration: z.boolean().optional(),
|
|
286
344
|
autoUpdate: z.enum(['off', 'patch', 'minor', 'major']).optional(),
|
|
345
|
+
language: z.string().trim().min(1).max(60)
|
|
346
|
+
.refine((v) => languageValueError(v) === null, {
|
|
347
|
+
message: 'language must not contain line breaks or other control characters',
|
|
348
|
+
})
|
|
349
|
+
.optional(),
|
|
287
350
|
setupCompleted: z.boolean().optional(),
|
|
288
351
|
}).strip();
|
|
289
352
|
app.post('/v1/config', async (req, res) => {
|
|
353
|
+
if (!requireJsonBody(req, res))
|
|
354
|
+
return;
|
|
290
355
|
const parsed = ConfigBody.safeParse(req.body);
|
|
291
356
|
if (!parsed.success) {
|
|
292
|
-
res.status(400).json({ success: false, error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
|
|
357
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
|
|
293
358
|
return;
|
|
294
359
|
}
|
|
295
360
|
try {
|
|
296
361
|
const before = readConfig();
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
362
|
+
if (parsed.data.llm && parsed.data.llm.apiKey === API_KEY_MASK) {
|
|
363
|
+
if (before.llm && before.llm.provider === parsed.data.llm.provider && before.llm.apiKey) {
|
|
364
|
+
parsed.data.llm.apiKey = before.llm.apiKey;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
delete parsed.data.llm.apiKey;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (parsed.data.llmFallbacks) {
|
|
371
|
+
parsed.data.llmFallbacks = preserveFallbackApiKeys(parsed.data.llmFallbacks, before.llmFallbacks);
|
|
304
372
|
}
|
|
373
|
+
const updated = updateConfig(parsed.data);
|
|
305
374
|
res.json({ success: true, data: maskLlmSecrets(updated) });
|
|
306
375
|
}
|
|
307
376
|
catch (err) {
|
|
308
|
-
res.status(400).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
377
|
+
res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) });
|
|
309
378
|
}
|
|
310
379
|
});
|
|
311
380
|
const ConfigTestBody = z.object({
|
|
312
381
|
provider: z.enum(['anthropic', 'openai', 'ollama']),
|
|
313
382
|
apiKey: z.string().max(500).optional(),
|
|
314
383
|
host: z.string().max(500).optional(),
|
|
384
|
+
fallbackIndex: z.number().int().nonnegative().optional(),
|
|
315
385
|
});
|
|
316
386
|
app.post('/v1/config/test', async (req, res) => {
|
|
387
|
+
if (!requireJsonBody(req, res))
|
|
388
|
+
return;
|
|
317
389
|
const parsed = ConfigTestBody.safeParse(req.body);
|
|
318
390
|
if (!parsed.success) {
|
|
319
391
|
res.status(400).json({
|
|
320
392
|
success: false,
|
|
393
|
+
errorCode: 'validation.bad-body',
|
|
321
394
|
error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
|
|
322
395
|
});
|
|
323
396
|
return;
|
|
324
397
|
}
|
|
325
398
|
try {
|
|
326
399
|
const { probeProvider } = await import('../../core/llm-validator.js');
|
|
327
|
-
const { provider, host } = parsed.data;
|
|
400
|
+
const { provider, host, fallbackIndex } = parsed.data;
|
|
328
401
|
let { apiKey } = parsed.data;
|
|
329
402
|
if (!apiKey && (provider === 'anthropic' || provider === 'openai')) {
|
|
330
403
|
const existing = readConfig();
|
|
331
|
-
if (
|
|
404
|
+
if (typeof fallbackIndex === 'number') {
|
|
405
|
+
const fb = existing.llmFallbacks?.[fallbackIndex];
|
|
406
|
+
if (fb && fb.provider === provider && fb.apiKey)
|
|
407
|
+
apiKey = fb.apiKey;
|
|
408
|
+
}
|
|
409
|
+
else if (existing.llm?.provider === provider && existing.llm.apiKey) {
|
|
332
410
|
apiKey = existing.llm.apiKey;
|
|
333
411
|
}
|
|
334
412
|
}
|
|
@@ -336,7 +414,7 @@ app.post('/v1/config/test', async (req, res) => {
|
|
|
336
414
|
res.json({ success: true, data: result });
|
|
337
415
|
}
|
|
338
416
|
catch (err) {
|
|
339
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
417
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
340
418
|
}
|
|
341
419
|
});
|
|
342
420
|
app.get('/v1/update-status', async (req, res) => {
|
|
@@ -370,7 +448,7 @@ app.get('/v1/update-status', async (req, res) => {
|
|
|
370
448
|
});
|
|
371
449
|
}
|
|
372
450
|
catch (err) {
|
|
373
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
451
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
374
452
|
}
|
|
375
453
|
});
|
|
376
454
|
app.get('/v1/graph', (_req, res) => handleGet(res, () => computeGraph(getDatabase())));
|
|
@@ -389,7 +467,7 @@ app.post('/v1/demo/seed', async (_req, res) => {
|
|
|
389
467
|
res.json({ success: true, data });
|
|
390
468
|
}
|
|
391
469
|
catch (err) {
|
|
392
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
470
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
393
471
|
}
|
|
394
472
|
});
|
|
395
473
|
app.post('/v1/demo/reset', async (_req, res) => {
|
|
@@ -399,7 +477,7 @@ app.post('/v1/demo/reset', async (_req, res) => {
|
|
|
399
477
|
res.json({ success: true, data });
|
|
400
478
|
}
|
|
401
479
|
catch (err) {
|
|
402
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
480
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
403
481
|
}
|
|
404
482
|
});
|
|
405
483
|
app.get('/v1/projects', (_req, res) => handleGet(res, () => computeProjects(getDatabase())));
|
|
@@ -411,7 +489,7 @@ app.get('/v1/telemetry', async (req, res) => {
|
|
|
411
489
|
try {
|
|
412
490
|
const parsed = TelemetryQuerySchema.safeParse(req.query);
|
|
413
491
|
if (!parsed.success) {
|
|
414
|
-
res.status(400).json({ success: false, error: parsed.error.issues.map(i => i.message).join('; ') });
|
|
492
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: parsed.error.issues.map(i => i.message).join('; ') });
|
|
415
493
|
return;
|
|
416
494
|
}
|
|
417
495
|
const { summariseTelemetry } = await import('../../core/llm-telemetry.js');
|
|
@@ -419,7 +497,7 @@ app.get('/v1/telemetry', async (req, res) => {
|
|
|
419
497
|
res.json({ success: true, data: { window_days: parsed.data.window, summaries } });
|
|
420
498
|
}
|
|
421
499
|
catch (err) {
|
|
422
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
500
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
423
501
|
}
|
|
424
502
|
});
|
|
425
503
|
const DreamProposalsQuerySchema = z.object({
|
|
@@ -429,7 +507,7 @@ app.get('/v1/dream/proposals', (req, res) => {
|
|
|
429
507
|
try {
|
|
430
508
|
const parsed = DreamProposalsQuerySchema.safeParse(req.query);
|
|
431
509
|
if (!parsed.success) {
|
|
432
|
-
res.status(400).json({ success: false, error: parsed.error.issues.map(i => i.message).join('; ') });
|
|
510
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: parsed.error.issues.map(i => i.message).join('; ') });
|
|
433
511
|
return;
|
|
434
512
|
}
|
|
435
513
|
const status = parsed.data.status;
|
|
@@ -439,22 +517,22 @@ app.get('/v1/dream/proposals', (req, res) => {
|
|
|
439
517
|
? [...listProposals(db, 'pending'), ...listProposals(db, 'applied'), ...listProposals(db, 'rejected')]
|
|
440
518
|
: listProposals(db, status);
|
|
441
519
|
res.json({ success: true, data: rows });
|
|
442
|
-
}).catch((err) => res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) }));
|
|
520
|
+
}).catch((err) => res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) }));
|
|
443
521
|
}
|
|
444
522
|
catch (err) {
|
|
445
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
523
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
446
524
|
}
|
|
447
525
|
});
|
|
448
526
|
app.get('/v1/dream/proposals/:id', (req, res) => {
|
|
449
527
|
const id = parseInt(req.params.id, 10);
|
|
450
528
|
if (!Number.isInteger(id) || id < 1) {
|
|
451
|
-
res.status(400).json({ success: false, error: 'invalid id' });
|
|
529
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
|
|
452
530
|
return;
|
|
453
531
|
}
|
|
454
532
|
try {
|
|
455
533
|
const row = getDatabase().prepare('SELECT id, project, cluster_key, source_ids, proposed_digest, llm_model, prompt_version, status, reason, created_at, reviewed_at FROM dream_proposals WHERE id = ?').get(id);
|
|
456
534
|
if (!row) {
|
|
457
|
-
res.status(404).json({ success: false, error: `proposal #${id} not found` });
|
|
535
|
+
res.status(404).json({ success: false, errorCode: 'resource.not-found', error: `proposal #${id} not found` });
|
|
458
536
|
return;
|
|
459
537
|
}
|
|
460
538
|
let digest = null;
|
|
@@ -470,7 +548,7 @@ app.get('/v1/dream/proposals/:id', (req, res) => {
|
|
|
470
548
|
res.json({ success: true, data: { ...row, proposed_digest: digest, source_ids: sourceIds } });
|
|
471
549
|
}
|
|
472
550
|
catch (err) {
|
|
473
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
551
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
474
552
|
}
|
|
475
553
|
});
|
|
476
554
|
const DreamRunBody = z.object({
|
|
@@ -484,6 +562,7 @@ app.post('/v1/dream/run', async (req, res) => {
|
|
|
484
562
|
if (!parsed.success) {
|
|
485
563
|
res.status(400).json({
|
|
486
564
|
success: false,
|
|
565
|
+
errorCode: 'validation.bad-body',
|
|
487
566
|
error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
|
|
488
567
|
});
|
|
489
568
|
return;
|
|
@@ -494,6 +573,7 @@ app.post('/v1/dream/run', async (req, res) => {
|
|
|
494
573
|
if (!cfg.llm) {
|
|
495
574
|
res.status(400).json({
|
|
496
575
|
success: false,
|
|
576
|
+
errorCode: 'llm.not-configured',
|
|
497
577
|
error: 'No LLM configured — dream run requires Smart Mode. Configure a provider in Settings.',
|
|
498
578
|
});
|
|
499
579
|
return;
|
|
@@ -508,13 +588,13 @@ app.post('/v1/dream/run', async (req, res) => {
|
|
|
508
588
|
res.json({ success: true, data: result });
|
|
509
589
|
}
|
|
510
590
|
catch (err) {
|
|
511
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
591
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
512
592
|
}
|
|
513
593
|
});
|
|
514
594
|
app.post('/v1/dream/proposals/:id/accept', async (req, res) => {
|
|
515
595
|
const id = parseInt(req.params.id, 10);
|
|
516
596
|
if (!Number.isInteger(id) || id < 1) {
|
|
517
|
-
res.status(400).json({ success: false, error: 'invalid id' });
|
|
597
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
|
|
518
598
|
return;
|
|
519
599
|
}
|
|
520
600
|
try {
|
|
@@ -526,10 +606,10 @@ app.post('/v1/dream/proposals/:id/accept', async (req, res) => {
|
|
|
526
606
|
catch (err) {
|
|
527
607
|
const msg = err instanceof Error ? err.message : String(err);
|
|
528
608
|
if (/not found or not pending/.test(msg)) {
|
|
529
|
-
res.status(404).json({ success: false, error: msg });
|
|
609
|
+
res.status(404).json({ success: false, errorCode: 'resource.not-found', error: msg });
|
|
530
610
|
}
|
|
531
611
|
else {
|
|
532
|
-
res.status(500).json({ success: false, error: msg });
|
|
612
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
|
|
533
613
|
}
|
|
534
614
|
}
|
|
535
615
|
});
|
|
@@ -539,12 +619,12 @@ const RejectBodySchema = z.object({
|
|
|
539
619
|
app.post('/v1/dream/proposals/:id/reject', async (req, res) => {
|
|
540
620
|
const id = parseInt(req.params.id, 10);
|
|
541
621
|
if (!Number.isInteger(id) || id < 1) {
|
|
542
|
-
res.status(400).json({ success: false, error: 'invalid id' });
|
|
622
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
|
|
543
623
|
return;
|
|
544
624
|
}
|
|
545
625
|
const parsed = RejectBodySchema.safeParse(req.body ?? {});
|
|
546
626
|
if (!parsed.success) {
|
|
547
|
-
res.status(400).json({ success: false, error: parsed.error.issues.map(i => i.message).join('; ') });
|
|
627
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => i.message).join('; ') });
|
|
548
628
|
return;
|
|
549
629
|
}
|
|
550
630
|
try {
|
|
@@ -555,10 +635,10 @@ app.post('/v1/dream/proposals/:id/reject', async (req, res) => {
|
|
|
555
635
|
catch (err) {
|
|
556
636
|
const msg = err instanceof Error ? err.message : String(err);
|
|
557
637
|
if (/not found or not pending/.test(msg)) {
|
|
558
|
-
res.status(404).json({ success: false, error: msg });
|
|
638
|
+
res.status(404).json({ success: false, errorCode: 'resource.not-found', error: msg });
|
|
559
639
|
}
|
|
560
640
|
else {
|
|
561
|
-
res.status(500).json({ success: false, error: msg });
|
|
641
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
|
|
562
642
|
}
|
|
563
643
|
}
|
|
564
644
|
});
|
|
@@ -571,7 +651,7 @@ app.get('/v1/entities', (req, res) => {
|
|
|
571
651
|
try {
|
|
572
652
|
const parsed = EntitiesQuerySchema.safeParse(req.query);
|
|
573
653
|
if (!parsed.success) {
|
|
574
|
-
res.status(400).json({ success: false, error: `Invalid query: ${parsed.error.message}` });
|
|
654
|
+
res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: `Invalid query: ${parsed.error.message}` });
|
|
575
655
|
return;
|
|
576
656
|
}
|
|
577
657
|
const { type: typeFilter, limit, status } = parsed.data;
|
|
@@ -584,7 +664,7 @@ app.get('/v1/entities', (req, res) => {
|
|
|
584
664
|
res.json({ success: true, data: entities });
|
|
585
665
|
}
|
|
586
666
|
catch (err) {
|
|
587
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
667
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
588
668
|
}
|
|
589
669
|
});
|
|
590
670
|
app.get('/v1/entities/:name', (req, res) => {
|
|
@@ -593,13 +673,13 @@ app.get('/v1/entities/:name', (req, res) => {
|
|
|
593
673
|
const kg = new KnowledgeGraph(db);
|
|
594
674
|
const entity = kg.getEntity(req.params.name);
|
|
595
675
|
if (!entity) {
|
|
596
|
-
res.status(404).json({ success: false, error: `Entity "${req.params.name}" not found` });
|
|
676
|
+
res.status(404).json({ success: false, errorCode: 'resource.not-found', error: `Entity "${req.params.name}" not found` });
|
|
597
677
|
return;
|
|
598
678
|
}
|
|
599
679
|
res.json({ success: true, data: entity });
|
|
600
680
|
}
|
|
601
681
|
catch (err) {
|
|
602
|
-
res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
682
|
+
res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
|
|
603
683
|
}
|
|
604
684
|
});
|
|
605
685
|
const HOST = process.env.MEMESH_HTTP_HOST || '127.0.0.1';
|
|
@@ -619,6 +699,7 @@ function isLoopbackHost(host) {
|
|
|
619
699
|
app.use((req, res) => {
|
|
620
700
|
res.status(404).json({
|
|
621
701
|
success: false,
|
|
702
|
+
errorCode: 'route.not-found',
|
|
622
703
|
code: 'NOT_FOUND',
|
|
623
704
|
error: `No route for ${req.method} ${req.path}`,
|
|
624
705
|
});
|
|
@@ -668,13 +749,27 @@ export function startServer(host = HOST, port = PORT, opts) {
|
|
|
668
749
|
throw new Error(`Database initialization failed: ${message}`, { cause: err });
|
|
669
750
|
}
|
|
670
751
|
logCapabilities();
|
|
752
|
+
const injectedUpdateSeam = Boolean(opts?.updateCheckImpl || opts?.lastUpdateCheckImpl);
|
|
753
|
+
const updateCheckWanted = opts?.autoUpdateCheck === true || injectedUpdateSeam;
|
|
754
|
+
if (updateCheckWanted && !process.env.MEMESH_SKIP_UPDATE_CHECK) {
|
|
755
|
+
void (async () => {
|
|
756
|
+
const readLast = opts?.lastUpdateCheckImpl ?? getLastUpdateCheck;
|
|
757
|
+
const refresh = opts?.updateCheckImpl ?? checkForUpdate;
|
|
758
|
+
const cached = readLast(packageVersion);
|
|
759
|
+
if (cached && (cached.freshness === 'fresh' || cached.freshness === 'cached'))
|
|
760
|
+
return;
|
|
761
|
+
await refresh(packageVersion);
|
|
762
|
+
})().catch(() => { });
|
|
763
|
+
}
|
|
671
764
|
const server = app.listen(port, host, () => {
|
|
672
765
|
const addr = server.address();
|
|
673
766
|
if (addr && typeof addr === 'object') {
|
|
674
767
|
console.log(`MeMesh HTTP server running at http://${addr.address}:${addr.port}`);
|
|
768
|
+
console.log(`MeMesh dashboard: http://${addr.address}:${addr.port}/dashboard`);
|
|
675
769
|
}
|
|
676
770
|
else {
|
|
677
771
|
console.log(`MeMesh HTTP server running at http://${host}:${port}`);
|
|
772
|
+
console.log(`MeMesh dashboard: http://${host}:${port}/dashboard`);
|
|
678
773
|
}
|
|
679
774
|
});
|
|
680
775
|
serverAuthRequired.set(server, isRemote);
|