@pcircle/memesh 4.4.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.de.md +31 -8
  4. package/README.es.md +9 -5
  5. package/README.fr.md +29 -6
  6. package/README.ja.md +9 -5
  7. package/README.ko.md +10 -6
  8. package/README.md +14 -7
  9. package/README.pt.md +28 -5
  10. package/README.th.md +14 -4
  11. package/README.vi.md +30 -7
  12. package/README.zh-CN.md +10 -6
  13. package/README.zh-TW.md +10 -6
  14. package/dashboard/dist/index.html +10 -10
  15. package/dist/cli/view-live.js +1 -1
  16. package/dist/core/analytics.d.ts +4 -0
  17. package/dist/core/analytics.d.ts.map +1 -1
  18. package/dist/core/analytics.js +8 -8
  19. package/dist/core/analytics.js.map +1 -1
  20. package/dist/core/auto-tagger.d.ts.map +1 -1
  21. package/dist/core/auto-tagger.js.map +1 -1
  22. package/dist/core/config.d.ts +5 -2
  23. package/dist/core/config.d.ts.map +1 -1
  24. package/dist/core/config.js +28 -13
  25. package/dist/core/config.js.map +1 -1
  26. package/dist/core/digest-validator.d.ts.map +1 -1
  27. package/dist/core/digest-validator.js +3 -1
  28. package/dist/core/digest-validator.js.map +1 -1
  29. package/dist/core/doctor.d.ts +2 -0
  30. package/dist/core/doctor.d.ts.map +1 -1
  31. package/dist/core/doctor.js +70 -60
  32. package/dist/core/doctor.js.map +1 -1
  33. package/dist/core/dreamer.d.ts +21 -1
  34. package/dist/core/dreamer.d.ts.map +1 -1
  35. package/dist/core/dreamer.js +86 -8
  36. package/dist/core/dreamer.js.map +1 -1
  37. package/dist/core/embedder.d.ts +1 -4
  38. package/dist/core/embedder.d.ts.map +1 -1
  39. package/dist/core/embedder.js +5 -95
  40. package/dist/core/embedder.js.map +1 -1
  41. package/dist/core/failure-analyzer.d.ts.map +1 -1
  42. package/dist/core/failure-analyzer.js +2 -1
  43. package/dist/core/failure-analyzer.js.map +1 -1
  44. package/dist/core/llm-client.d.ts.map +1 -1
  45. package/dist/core/llm-client.js.map +1 -1
  46. package/dist/core/llm-validator.d.ts +1 -0
  47. package/dist/core/llm-validator.d.ts.map +1 -1
  48. package/dist/core/llm-validator.js +33 -10
  49. package/dist/core/llm-validator.js.map +1 -1
  50. package/dist/core/operations.js +1 -1
  51. package/dist/core/operations.js.map +1 -1
  52. package/dist/core/output-language.d.ts +6 -0
  53. package/dist/core/output-language.d.ts.map +1 -0
  54. package/dist/core/output-language.js +25 -0
  55. package/dist/core/output-language.js.map +1 -0
  56. package/dist/core/patterns.d.ts +0 -1
  57. package/dist/core/patterns.d.ts.map +1 -1
  58. package/dist/core/patterns.js +1 -5
  59. package/dist/core/patterns.js.map +1 -1
  60. package/dist/core/transcript-extractor.d.ts +89 -0
  61. package/dist/core/transcript-extractor.d.ts.map +1 -0
  62. package/dist/core/transcript-extractor.js +437 -0
  63. package/dist/core/transcript-extractor.js.map +1 -0
  64. package/dist/core/transcript-source.d.ts +21 -0
  65. package/dist/core/transcript-source.d.ts.map +1 -0
  66. package/dist/core/transcript-source.js +142 -0
  67. package/dist/core/transcript-source.js.map +1 -0
  68. package/dist/core/types.d.ts.map +1 -1
  69. package/dist/db.d.ts.map +1 -1
  70. package/dist/db.js +10 -0
  71. package/dist/db.js.map +1 -1
  72. package/dist/skills-manifest.json +4 -4
  73. package/dist/transports/cli/cli.d.ts.map +1 -1
  74. package/dist/transports/cli/cli.js +153 -8
  75. package/dist/transports/cli/cli.js.map +1 -1
  76. package/dist/transports/http/server.d.ts +7 -0
  77. package/dist/transports/http/server.d.ts.map +1 -1
  78. package/dist/transports/http/server.js +117 -50
  79. package/dist/transports/http/server.js.map +1 -1
  80. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  81. package/dist/transports/mcp/handlers.js +2 -1
  82. package/dist/transports/mcp/handlers.js.map +1 -1
  83. package/package.json +1 -10
  84. package/skills/agentic-orchestration/SKILL.md +1 -1
  85. 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();
@@ -130,6 +143,7 @@ function payloadTooLargeHandler(err, _req, res, next) {
130
143
  if (e.type === 'entity.parse.failed' || (err instanceof SyntaxError && (e.status === 400 || e.statusCode === 400))) {
131
144
  res.status(400).json({
132
145
  success: false,
146
+ errorCode: 'validation.bad-body',
133
147
  error: 'Request body is not valid JSON.',
134
148
  hint: 'Send a JSON object with Content-Type: application/json.',
135
149
  });
@@ -140,6 +154,7 @@ function payloadTooLargeHandler(err, _req, res, next) {
140
154
  return next(err);
141
155
  res.status(413).json({
142
156
  success: false,
157
+ errorCode: 'payload.too-large',
143
158
  error: 'Request body exceeds the 1MB limit',
144
159
  code: 'PAYLOAD_TOO_LARGE',
145
160
  limit: '1mb',
@@ -179,12 +194,13 @@ app.get('/v1/health', (_req, res) => {
179
194
  if (message === 'Database not opened') {
180
195
  res.status(503).json({
181
196
  success: false,
197
+ errorCode: 'server.internal',
182
198
  error: 'Database not initialized',
183
199
  details: 'MeMesh database failed to open at startup. Check server logs for details, or run "memesh doctor" to diagnose.',
184
200
  });
185
201
  }
186
202
  else {
187
- res.status(500).json({ success: false, error: message });
203
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: message });
188
204
  }
189
205
  }
190
206
  });
@@ -209,7 +225,7 @@ app.get('/v1/doctor', async (_req, res) => {
209
225
  res.json({ success: true, data: safe });
210
226
  }
211
227
  catch (err) {
212
- 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) });
213
229
  }
214
230
  });
215
231
  function requireJsonBody(req, res) {
@@ -217,6 +233,7 @@ function requireJsonBody(req, res) {
217
233
  return true;
218
234
  res.status(400).json({
219
235
  success: false,
236
+ errorCode: 'validation.bad-body',
220
237
  error: 'No JSON body was parsed from this request.',
221
238
  hint: 'Send the payload with Content-Type: application/json.',
222
239
  });
@@ -229,19 +246,20 @@ function handlePost(schema, req, res, handler) {
229
246
  if (!parsed.success) {
230
247
  res.status(400).json({
231
248
  success: false,
249
+ errorCode: 'validation.bad-body',
232
250
  error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
233
251
  });
234
252
  return;
235
253
  }
236
254
  Promise.resolve(handler(parsed.data))
237
255
  .then((data) => res.json({ success: true, data }))
238
- .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) }));
239
257
  }
240
258
  function handleGet(res, produce) {
241
259
  Promise.resolve()
242
260
  .then(produce)
243
261
  .then((data) => res.json({ success: true, data }))
244
- .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) }));
245
263
  }
246
264
  app.post('/v1/remember', (req, res) => handlePost(RememberBody, req, res, remember));
247
265
  app.post('/v1/recall', async (req, res) => {
@@ -249,7 +267,7 @@ app.post('/v1/recall', async (req, res) => {
249
267
  return;
250
268
  const parsed = RecallBody.safeParse(req.body);
251
269
  if (!parsed.success) {
252
- 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('; ') });
253
271
  return;
254
272
  }
255
273
  try {
@@ -257,27 +275,44 @@ app.post('/v1/recall', async (req, res) => {
257
275
  res.json({ success: true, data: conflicts.length > 0 ? { entities, conflicts } : entities });
258
276
  }
259
277
  catch (err) {
260
- 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) });
261
279
  }
262
280
  });
263
281
  app.post('/v1/forget', (req, res) => handlePost(ForgetBody, req, res, forget));
264
282
  app.post('/v1/consolidate', (_req, res) => {
265
- 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'] });
266
284
  });
267
285
  app.post('/v1/export', (req, res) => handlePost(ExportBody, req, res, exportMemories));
268
286
  app.post('/v1/import', (req, res) => handlePost(ImportBody, req, res, importMemories));
269
287
  app.post('/v1/learn', (req, res) => handlePost(LearnBody, req, res, learn));
270
288
  app.post('/v1/verify', (req, res) => handlePost(VerifyBody, req, res, verifyAgentWork));
289
+ const API_KEY_MASK = '***';
271
290
  function maskLlmSecrets(obj) {
272
291
  const masked = { ...obj };
273
292
  if (masked.llm?.apiKey) {
274
- masked.llm = { ...masked.llm, apiKey: '***' };
293
+ masked.llm = { ...masked.llm, apiKey: API_KEY_MASK };
275
294
  }
276
295
  if (Array.isArray(masked.llmFallbacks) && masked.llmFallbacks.length > 0) {
277
- 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);
278
297
  }
279
298
  return masked;
280
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
+ }
281
316
  app.get('/v1/config', (_req, res) => {
282
317
  try {
283
318
  const config = readConfig();
@@ -285,7 +320,7 @@ app.get('/v1/config', (_req, res) => {
285
320
  res.json({ success: true, data: { config: maskLlmSecrets(config), capabilities: maskLlmSecrets(caps) } });
286
321
  }
287
322
  catch (err) {
288
- 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) });
289
324
  }
290
325
  });
291
326
  const ConfigBody = z.object({
@@ -301,11 +336,17 @@ const ConfigBody = z.object({
301
336
  provider: z.enum(['anthropic', 'openai', 'ollama']),
302
337
  model: z.string().optional(),
303
338
  apiKey: z.string().optional(),
339
+ keepKeyFrom: z.number().int().nonnegative().nullable().optional(),
304
340
  })).optional(),
305
341
  autoCapture: z.boolean().optional(),
306
342
  sessionLimit: z.number().int().min(1).max(100).optional(),
307
343
  enableAgenticOrchestration: z.boolean().optional(),
308
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(),
309
350
  setupCompleted: z.boolean().optional(),
310
351
  }).strip();
311
352
  app.post('/v1/config', async (req, res) => {
@@ -313,29 +354,34 @@ app.post('/v1/config', async (req, res) => {
313
354
  return;
314
355
  const parsed = ConfigBody.safeParse(req.body);
315
356
  if (!parsed.success) {
316
- 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('; ') });
317
358
  return;
318
359
  }
319
360
  try {
320
361
  const before = readConfig();
321
- const updated = updateConfig(parsed.data);
322
- const llmChanged = parsed.data.llm !== undefined &&
323
- (before.llm?.provider !== updated.llm?.provider ||
324
- before.llm?.apiKey !== updated.llm?.apiKey);
325
- if (llmChanged) {
326
- const { resetEmbeddingState } = await import('../../core/embedder.js');
327
- 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);
328
372
  }
373
+ const updated = updateConfig(parsed.data);
329
374
  res.json({ success: true, data: maskLlmSecrets(updated) });
330
375
  }
331
376
  catch (err) {
332
- 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) });
333
378
  }
334
379
  });
335
380
  const ConfigTestBody = z.object({
336
381
  provider: z.enum(['anthropic', 'openai', 'ollama']),
337
382
  apiKey: z.string().max(500).optional(),
338
383
  host: z.string().max(500).optional(),
384
+ fallbackIndex: z.number().int().nonnegative().optional(),
339
385
  });
340
386
  app.post('/v1/config/test', async (req, res) => {
341
387
  if (!requireJsonBody(req, res))
@@ -344,17 +390,23 @@ app.post('/v1/config/test', async (req, res) => {
344
390
  if (!parsed.success) {
345
391
  res.status(400).json({
346
392
  success: false,
393
+ errorCode: 'validation.bad-body',
347
394
  error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
348
395
  });
349
396
  return;
350
397
  }
351
398
  try {
352
399
  const { probeProvider } = await import('../../core/llm-validator.js');
353
- const { provider, host } = parsed.data;
400
+ const { provider, host, fallbackIndex } = parsed.data;
354
401
  let { apiKey } = parsed.data;
355
402
  if (!apiKey && (provider === 'anthropic' || provider === 'openai')) {
356
403
  const existing = readConfig();
357
- 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) {
358
410
  apiKey = existing.llm.apiKey;
359
411
  }
360
412
  }
@@ -362,7 +414,7 @@ app.post('/v1/config/test', async (req, res) => {
362
414
  res.json({ success: true, data: result });
363
415
  }
364
416
  catch (err) {
365
- 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) });
366
418
  }
367
419
  });
368
420
  app.get('/v1/update-status', async (req, res) => {
@@ -396,7 +448,7 @@ app.get('/v1/update-status', async (req, res) => {
396
448
  });
397
449
  }
398
450
  catch (err) {
399
- 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) });
400
452
  }
401
453
  });
402
454
  app.get('/v1/graph', (_req, res) => handleGet(res, () => computeGraph(getDatabase())));
@@ -415,7 +467,7 @@ app.post('/v1/demo/seed', async (_req, res) => {
415
467
  res.json({ success: true, data });
416
468
  }
417
469
  catch (err) {
418
- 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) });
419
471
  }
420
472
  });
421
473
  app.post('/v1/demo/reset', async (_req, res) => {
@@ -425,7 +477,7 @@ app.post('/v1/demo/reset', async (_req, res) => {
425
477
  res.json({ success: true, data });
426
478
  }
427
479
  catch (err) {
428
- 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) });
429
481
  }
430
482
  });
431
483
  app.get('/v1/projects', (_req, res) => handleGet(res, () => computeProjects(getDatabase())));
@@ -437,7 +489,7 @@ app.get('/v1/telemetry', async (req, res) => {
437
489
  try {
438
490
  const parsed = TelemetryQuerySchema.safeParse(req.query);
439
491
  if (!parsed.success) {
440
- 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('; ') });
441
493
  return;
442
494
  }
443
495
  const { summariseTelemetry } = await import('../../core/llm-telemetry.js');
@@ -445,7 +497,7 @@ app.get('/v1/telemetry', async (req, res) => {
445
497
  res.json({ success: true, data: { window_days: parsed.data.window, summaries } });
446
498
  }
447
499
  catch (err) {
448
- 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) });
449
501
  }
450
502
  });
451
503
  const DreamProposalsQuerySchema = z.object({
@@ -455,7 +507,7 @@ app.get('/v1/dream/proposals', (req, res) => {
455
507
  try {
456
508
  const parsed = DreamProposalsQuerySchema.safeParse(req.query);
457
509
  if (!parsed.success) {
458
- 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('; ') });
459
511
  return;
460
512
  }
461
513
  const status = parsed.data.status;
@@ -465,22 +517,22 @@ app.get('/v1/dream/proposals', (req, res) => {
465
517
  ? [...listProposals(db, 'pending'), ...listProposals(db, 'applied'), ...listProposals(db, 'rejected')]
466
518
  : listProposals(db, status);
467
519
  res.json({ success: true, data: rows });
468
- }).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) }));
469
521
  }
470
522
  catch (err) {
471
- 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) });
472
524
  }
473
525
  });
474
526
  app.get('/v1/dream/proposals/:id', (req, res) => {
475
527
  const id = parseInt(req.params.id, 10);
476
528
  if (!Number.isInteger(id) || id < 1) {
477
- res.status(400).json({ success: false, error: 'invalid id' });
529
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
478
530
  return;
479
531
  }
480
532
  try {
481
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);
482
534
  if (!row) {
483
- 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` });
484
536
  return;
485
537
  }
486
538
  let digest = null;
@@ -496,7 +548,7 @@ app.get('/v1/dream/proposals/:id', (req, res) => {
496
548
  res.json({ success: true, data: { ...row, proposed_digest: digest, source_ids: sourceIds } });
497
549
  }
498
550
  catch (err) {
499
- 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) });
500
552
  }
501
553
  });
502
554
  const DreamRunBody = z.object({
@@ -510,6 +562,7 @@ app.post('/v1/dream/run', async (req, res) => {
510
562
  if (!parsed.success) {
511
563
  res.status(400).json({
512
564
  success: false,
565
+ errorCode: 'validation.bad-body',
513
566
  error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
514
567
  });
515
568
  return;
@@ -520,6 +573,7 @@ app.post('/v1/dream/run', async (req, res) => {
520
573
  if (!cfg.llm) {
521
574
  res.status(400).json({
522
575
  success: false,
576
+ errorCode: 'llm.not-configured',
523
577
  error: 'No LLM configured — dream run requires Smart Mode. Configure a provider in Settings.',
524
578
  });
525
579
  return;
@@ -534,13 +588,13 @@ app.post('/v1/dream/run', async (req, res) => {
534
588
  res.json({ success: true, data: result });
535
589
  }
536
590
  catch (err) {
537
- 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) });
538
592
  }
539
593
  });
540
594
  app.post('/v1/dream/proposals/:id/accept', async (req, res) => {
541
595
  const id = parseInt(req.params.id, 10);
542
596
  if (!Number.isInteger(id) || id < 1) {
543
- res.status(400).json({ success: false, error: 'invalid id' });
597
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
544
598
  return;
545
599
  }
546
600
  try {
@@ -552,10 +606,10 @@ app.post('/v1/dream/proposals/:id/accept', async (req, res) => {
552
606
  catch (err) {
553
607
  const msg = err instanceof Error ? err.message : String(err);
554
608
  if (/not found or not pending/.test(msg)) {
555
- res.status(404).json({ success: false, error: msg });
609
+ res.status(404).json({ success: false, errorCode: 'resource.not-found', error: msg });
556
610
  }
557
611
  else {
558
- res.status(500).json({ success: false, error: msg });
612
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
559
613
  }
560
614
  }
561
615
  });
@@ -565,12 +619,12 @@ const RejectBodySchema = z.object({
565
619
  app.post('/v1/dream/proposals/:id/reject', async (req, res) => {
566
620
  const id = parseInt(req.params.id, 10);
567
621
  if (!Number.isInteger(id) || id < 1) {
568
- res.status(400).json({ success: false, error: 'invalid id' });
622
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
569
623
  return;
570
624
  }
571
625
  const parsed = RejectBodySchema.safeParse(req.body ?? {});
572
626
  if (!parsed.success) {
573
- 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('; ') });
574
628
  return;
575
629
  }
576
630
  try {
@@ -581,10 +635,10 @@ app.post('/v1/dream/proposals/:id/reject', async (req, res) => {
581
635
  catch (err) {
582
636
  const msg = err instanceof Error ? err.message : String(err);
583
637
  if (/not found or not pending/.test(msg)) {
584
- res.status(404).json({ success: false, error: msg });
638
+ res.status(404).json({ success: false, errorCode: 'resource.not-found', error: msg });
585
639
  }
586
640
  else {
587
- res.status(500).json({ success: false, error: msg });
641
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
588
642
  }
589
643
  }
590
644
  });
@@ -597,7 +651,7 @@ app.get('/v1/entities', (req, res) => {
597
651
  try {
598
652
  const parsed = EntitiesQuerySchema.safeParse(req.query);
599
653
  if (!parsed.success) {
600
- 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}` });
601
655
  return;
602
656
  }
603
657
  const { type: typeFilter, limit, status } = parsed.data;
@@ -610,7 +664,7 @@ app.get('/v1/entities', (req, res) => {
610
664
  res.json({ success: true, data: entities });
611
665
  }
612
666
  catch (err) {
613
- 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) });
614
668
  }
615
669
  });
616
670
  app.get('/v1/entities/:name', (req, res) => {
@@ -619,13 +673,13 @@ app.get('/v1/entities/:name', (req, res) => {
619
673
  const kg = new KnowledgeGraph(db);
620
674
  const entity = kg.getEntity(req.params.name);
621
675
  if (!entity) {
622
- 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` });
623
677
  return;
624
678
  }
625
679
  res.json({ success: true, data: entity });
626
680
  }
627
681
  catch (err) {
628
- 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) });
629
683
  }
630
684
  });
631
685
  const HOST = process.env.MEMESH_HTTP_HOST || '127.0.0.1';
@@ -645,6 +699,7 @@ function isLoopbackHost(host) {
645
699
  app.use((req, res) => {
646
700
  res.status(404).json({
647
701
  success: false,
702
+ errorCode: 'route.not-found',
648
703
  code: 'NOT_FOUND',
649
704
  error: `No route for ${req.method} ${req.path}`,
650
705
  });
@@ -694,6 +749,18 @@ export function startServer(host = HOST, port = PORT, opts) {
694
749
  throw new Error(`Database initialization failed: ${message}`, { cause: err });
695
750
  }
696
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
+ }
697
764
  const server = app.listen(port, host, () => {
698
765
  const addr = server.address();
699
766
  if (addr && typeof addr === 'object') {