@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.
Files changed (100) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.de.md +35 -12
  4. package/README.es.md +13 -9
  5. package/README.fr.md +33 -10
  6. package/README.ja.md +13 -9
  7. package/README.ko.md +14 -10
  8. package/README.md +18 -11
  9. package/README.pt.md +32 -9
  10. package/README.th.md +18 -8
  11. package/README.vi.md +34 -11
  12. package/README.zh-CN.md +14 -10
  13. package/README.zh-TW.md +14 -10
  14. package/dashboard/dist/index.html +10 -10
  15. package/dist/cli/view-live.js +2 -2
  16. package/dist/cli/view.js +1 -1
  17. package/dist/core/analytics.d.ts +4 -0
  18. package/dist/core/analytics.d.ts.map +1 -1
  19. package/dist/core/analytics.js +8 -8
  20. package/dist/core/analytics.js.map +1 -1
  21. package/dist/core/auto-tagger.d.ts.map +1 -1
  22. package/dist/core/auto-tagger.js.map +1 -1
  23. package/dist/core/config.d.ts +5 -2
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +28 -13
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/demo.d.ts +1 -0
  28. package/dist/core/demo.d.ts.map +1 -1
  29. package/dist/core/demo.js +24 -0
  30. package/dist/core/demo.js.map +1 -1
  31. package/dist/core/digest-validator.d.ts.map +1 -1
  32. package/dist/core/digest-validator.js +3 -1
  33. package/dist/core/digest-validator.js.map +1 -1
  34. package/dist/core/doctor.d.ts +2 -0
  35. package/dist/core/doctor.d.ts.map +1 -1
  36. package/dist/core/doctor.js +70 -60
  37. package/dist/core/doctor.js.map +1 -1
  38. package/dist/core/dreamer.d.ts +21 -1
  39. package/dist/core/dreamer.d.ts.map +1 -1
  40. package/dist/core/dreamer.js +86 -8
  41. package/dist/core/dreamer.js.map +1 -1
  42. package/dist/core/embedder.d.ts +1 -4
  43. package/dist/core/embedder.d.ts.map +1 -1
  44. package/dist/core/embedder.js +5 -92
  45. package/dist/core/embedder.js.map +1 -1
  46. package/dist/core/failure-analyzer.d.ts.map +1 -1
  47. package/dist/core/failure-analyzer.js +2 -1
  48. package/dist/core/failure-analyzer.js.map +1 -1
  49. package/dist/core/llm-client.d.ts.map +1 -1
  50. package/dist/core/llm-client.js.map +1 -1
  51. package/dist/core/llm-validator.d.ts +1 -0
  52. package/dist/core/llm-validator.d.ts.map +1 -1
  53. package/dist/core/llm-validator.js +33 -10
  54. package/dist/core/llm-validator.js.map +1 -1
  55. package/dist/core/operations.d.ts.map +1 -1
  56. package/dist/core/operations.js +10 -3
  57. package/dist/core/operations.js.map +1 -1
  58. package/dist/core/output-language.d.ts +6 -0
  59. package/dist/core/output-language.d.ts.map +1 -0
  60. package/dist/core/output-language.js +25 -0
  61. package/dist/core/output-language.js.map +1 -0
  62. package/dist/core/patterns.d.ts +0 -1
  63. package/dist/core/patterns.d.ts.map +1 -1
  64. package/dist/core/patterns.js +1 -5
  65. package/dist/core/patterns.js.map +1 -1
  66. package/dist/core/transcript-extractor.d.ts +89 -0
  67. package/dist/core/transcript-extractor.d.ts.map +1 -0
  68. package/dist/core/transcript-extractor.js +437 -0
  69. package/dist/core/transcript-extractor.js.map +1 -0
  70. package/dist/core/transcript-source.d.ts +21 -0
  71. package/dist/core/transcript-source.d.ts.map +1 -0
  72. package/dist/core/transcript-source.js +142 -0
  73. package/dist/core/transcript-source.js.map +1 -0
  74. package/dist/core/types.d.ts +4 -0
  75. package/dist/core/types.d.ts.map +1 -1
  76. package/dist/core/verifier.js +1 -1
  77. package/dist/core/verifier.js.map +1 -1
  78. package/dist/db.d.ts.map +1 -1
  79. package/dist/db.js +10 -0
  80. package/dist/db.js.map +1 -1
  81. package/dist/skills-manifest.json +11 -11
  82. package/dist/transports/cli/cli.d.ts.map +1 -1
  83. package/dist/transports/cli/cli.js +229 -65
  84. package/dist/transports/cli/cli.js.map +1 -1
  85. package/dist/transports/http/server.d.ts +7 -0
  86. package/dist/transports/http/server.d.ts.map +1 -1
  87. package/dist/transports/http/server.js +145 -50
  88. package/dist/transports/http/server.js.map +1 -1
  89. package/dist/transports/mcp/handlers.d.ts +1 -1
  90. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  91. package/dist/transports/mcp/handlers.js +3 -2
  92. package/dist/transports/mcp/handlers.js.map +1 -1
  93. package/dist/transports/schemas.d.ts.map +1 -1
  94. package/dist/transports/schemas.js.map +1 -1
  95. package/package.json +2 -11
  96. package/scripts/hooks/post-commit.js +8 -2
  97. package/scripts/hooks/pre-compact.js +9 -0
  98. package/scripts/hooks/session-start.js +1 -1
  99. package/skills/agentic-orchestration/SKILL.md +1 -1
  100. 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
- message: 'Too many requests from this IP, please try again later.',
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: '***' } : fb);
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
- const updated = updateConfig(parsed.data);
298
- const llmChanged = parsed.data.llm !== undefined &&
299
- (before.llm?.provider !== updated.llm?.provider ||
300
- before.llm?.apiKey !== updated.llm?.apiKey);
301
- if (llmChanged) {
302
- const { resetEmbeddingState } = await import('../../core/embedder.js');
303
- resetEmbeddingState();
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 (existing.llm?.provider === provider && existing.llm.apiKey) {
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);