@pcircle/memesh 4.4.0 → 4.5.1

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 (198) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +1 -1
  4. package/README.de.md +32 -12
  5. package/README.es.md +13 -12
  6. package/README.fr.md +31 -11
  7. package/README.ja.md +13 -12
  8. package/README.ko.md +14 -13
  9. package/README.md +41 -16
  10. package/README.pt.md +30 -10
  11. package/README.th.md +16 -9
  12. package/README.vi.md +32 -12
  13. package/README.zh-CN.md +14 -13
  14. package/README.zh-TW.md +14 -13
  15. package/dashboard/dist/index.html +10 -10
  16. package/dist/cli/view-live.js +1 -1
  17. package/dist/cli/view.d.ts.map +1 -1
  18. package/dist/cli/view.js +2 -2
  19. package/dist/cli/view.js.map +1 -1
  20. package/dist/core/analytics.d.ts +7 -3
  21. package/dist/core/analytics.d.ts.map +1 -1
  22. package/dist/core/analytics.js +8 -8
  23. package/dist/core/analytics.js.map +1 -1
  24. package/dist/core/auto-tagger.d.ts.map +1 -1
  25. package/dist/core/auto-tagger.js.map +1 -1
  26. package/dist/core/config.d.ts +5 -3
  27. package/dist/core/config.d.ts.map +1 -1
  28. package/dist/core/config.js +30 -14
  29. package/dist/core/config.js.map +1 -1
  30. package/dist/core/demo.d.ts +2 -2
  31. package/dist/core/demo.d.ts.map +1 -1
  32. package/dist/core/demo.js.map +1 -1
  33. package/dist/core/digest-validator.d.ts.map +1 -1
  34. package/dist/core/digest-validator.js +3 -1
  35. package/dist/core/digest-validator.js.map +1 -1
  36. package/dist/core/doctor.d.ts +3 -0
  37. package/dist/core/doctor.d.ts.map +1 -1
  38. package/dist/core/doctor.js +255 -101
  39. package/dist/core/doctor.js.map +1 -1
  40. package/dist/core/dreamer.d.ts +35 -7
  41. package/dist/core/dreamer.d.ts.map +1 -1
  42. package/dist/core/dreamer.js +394 -41
  43. package/dist/core/dreamer.js.map +1 -1
  44. package/dist/core/embedder.d.ts +3 -5
  45. package/dist/core/embedder.d.ts.map +1 -1
  46. package/dist/core/embedder.js +26 -95
  47. package/dist/core/embedder.js.map +1 -1
  48. package/dist/core/extractor.d.ts.map +1 -1
  49. package/dist/core/extractor.js +2 -1
  50. package/dist/core/extractor.js.map +1 -1
  51. package/dist/core/failure-analyzer.d.ts.map +1 -1
  52. package/dist/core/failure-analyzer.js +2 -1
  53. package/dist/core/failure-analyzer.js.map +1 -1
  54. package/dist/core/graph.d.ts +4 -4
  55. package/dist/core/graph.d.ts.map +1 -1
  56. package/dist/core/graph.js.map +1 -1
  57. package/dist/core/install-hooks.d.ts +1 -0
  58. package/dist/core/install-hooks.d.ts.map +1 -1
  59. package/dist/core/install-hooks.js +0 -0
  60. package/dist/core/install-hooks.js.map +1 -1
  61. package/dist/core/kg-backfill.d.ts +4 -4
  62. package/dist/core/kg-backfill.d.ts.map +1 -1
  63. package/dist/core/kg-backfill.js +1 -1
  64. package/dist/core/kg-backfill.js.map +1 -1
  65. package/dist/core/lesson-engine.d.ts +1 -0
  66. package/dist/core/lesson-engine.d.ts.map +1 -1
  67. package/dist/core/lesson-engine.js +1 -0
  68. package/dist/core/lesson-engine.js.map +1 -1
  69. package/dist/core/lifecycle.d.ts +4 -4
  70. package/dist/core/lifecycle.d.ts.map +1 -1
  71. package/dist/core/lifecycle.js +1 -1
  72. package/dist/core/lifecycle.js.map +1 -1
  73. package/dist/core/llm-client.d.ts.map +1 -1
  74. package/dist/core/llm-client.js +3 -6
  75. package/dist/core/llm-client.js.map +1 -1
  76. package/dist/core/llm-telemetry.d.ts +4 -4
  77. package/dist/core/llm-telemetry.d.ts.map +1 -1
  78. package/dist/core/llm-telemetry.js +1 -1
  79. package/dist/core/llm-telemetry.js.map +1 -1
  80. package/dist/core/llm-validator.d.ts +1 -0
  81. package/dist/core/llm-validator.d.ts.map +1 -1
  82. package/dist/core/llm-validator.js +33 -10
  83. package/dist/core/llm-validator.js.map +1 -1
  84. package/dist/core/memory-tool.d.ts.map +1 -1
  85. package/dist/core/memory-tool.js +4 -0
  86. package/dist/core/memory-tool.js.map +1 -1
  87. package/dist/core/operations.d.ts.map +1 -1
  88. package/dist/core/operations.js +23 -8
  89. package/dist/core/operations.js.map +1 -1
  90. package/dist/core/output-language.d.ts +6 -0
  91. package/dist/core/output-language.d.ts.map +1 -0
  92. package/dist/core/output-language.js +25 -0
  93. package/dist/core/output-language.js.map +1 -0
  94. package/dist/core/paths.d.ts +3 -0
  95. package/dist/core/paths.d.ts.map +1 -1
  96. package/dist/core/paths.js +67 -1
  97. package/dist/core/paths.js.map +1 -1
  98. package/dist/core/patterns.d.ts +2 -3
  99. package/dist/core/patterns.d.ts.map +1 -1
  100. package/dist/core/patterns.js +1 -5
  101. package/dist/core/patterns.js.map +1 -1
  102. package/dist/core/project-tags.d.ts +3 -3
  103. package/dist/core/project-tags.d.ts.map +1 -1
  104. package/dist/core/project-tags.js.map +1 -1
  105. package/dist/core/projects.d.ts +2 -2
  106. package/dist/core/projects.d.ts.map +1 -1
  107. package/dist/core/projects.js.map +1 -1
  108. package/dist/core/schema-export.d.ts.map +1 -1
  109. package/dist/core/schema-export.js +0 -34
  110. package/dist/core/schema-export.js.map +1 -1
  111. package/dist/core/serializer.d.ts.map +1 -1
  112. package/dist/core/serializer.js +45 -4
  113. package/dist/core/serializer.js.map +1 -1
  114. package/dist/core/stats.d.ts +2 -2
  115. package/dist/core/stats.d.ts.map +1 -1
  116. package/dist/core/stats.js.map +1 -1
  117. package/dist/core/transcript-extractor.d.ts +88 -0
  118. package/dist/core/transcript-extractor.d.ts.map +1 -0
  119. package/dist/core/transcript-extractor.js +417 -0
  120. package/dist/core/transcript-extractor.js.map +1 -0
  121. package/dist/core/transcript-source.d.ts +21 -0
  122. package/dist/core/transcript-source.d.ts.map +1 -0
  123. package/dist/core/transcript-source.js +142 -0
  124. package/dist/core/transcript-source.js.map +1 -0
  125. package/dist/core/types.d.ts +17 -7
  126. package/dist/core/types.d.ts.map +1 -1
  127. package/dist/core/types.js +2 -0
  128. package/dist/core/types.js.map +1 -1
  129. package/dist/db.d.ts +5 -5
  130. package/dist/db.d.ts.map +1 -1
  131. package/dist/db.js +165 -17
  132. package/dist/db.js.map +1 -1
  133. package/dist/knowledge-graph.d.ts +3 -2
  134. package/dist/knowledge-graph.d.ts.map +1 -1
  135. package/dist/knowledge-graph.js +19 -10
  136. package/dist/knowledge-graph.js.map +1 -1
  137. package/dist/mcp/server.js +2 -1
  138. package/dist/mcp/server.js.map +1 -1
  139. package/dist/skills-manifest.json +28 -33
  140. package/dist/storage/conflicts.d.ts +3 -3
  141. package/dist/storage/conflicts.d.ts.map +1 -1
  142. package/dist/storage/conflicts.js.map +1 -1
  143. package/dist/storage/fts-index.d.ts +4 -4
  144. package/dist/storage/fts-index.d.ts.map +1 -1
  145. package/dist/storage/fts-index.js.map +1 -1
  146. package/dist/storage/sqlite.d.ts +20 -0
  147. package/dist/storage/sqlite.d.ts.map +1 -0
  148. package/dist/storage/sqlite.js +64 -0
  149. package/dist/storage/sqlite.js.map +1 -0
  150. package/dist/storage/vector-index.d.ts +3 -0
  151. package/dist/storage/vector-index.d.ts.map +1 -0
  152. package/dist/storage/vector-index.js +7 -0
  153. package/dist/storage/vector-index.js.map +1 -0
  154. package/dist/transports/cli/cli.d.ts.map +1 -1
  155. package/dist/transports/cli/cli.js +278 -91
  156. package/dist/transports/cli/cli.js.map +1 -1
  157. package/dist/transports/http/retired-routes.d.ts.map +1 -1
  158. package/dist/transports/http/retired-routes.js +1 -0
  159. package/dist/transports/http/retired-routes.js.map +1 -1
  160. package/dist/transports/http/server.d.ts +7 -0
  161. package/dist/transports/http/server.d.ts.map +1 -1
  162. package/dist/transports/http/server.js +165 -79
  163. package/dist/transports/http/server.js.map +1 -1
  164. package/dist/transports/mcp/handlers.d.ts +4 -96
  165. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  166. package/dist/transports/mcp/handlers.js +26 -59
  167. package/dist/transports/mcp/handlers.js.map +1 -1
  168. package/dist/transports/schemas.d.ts +10 -30
  169. package/dist/transports/schemas.d.ts.map +1 -1
  170. package/dist/transports/schemas.js +5 -29
  171. package/dist/transports/schemas.js.map +1 -1
  172. package/hooks/hooks.json +0 -10
  173. package/package.json +6 -19
  174. package/scripts/hooks/_generated/core-paths.js +67 -1
  175. package/scripts/hooks/_generated/sqlite.js +71 -0
  176. package/scripts/hooks/_shared.js +286 -195
  177. package/scripts/hooks/post-commit.js +52 -10
  178. package/scripts/hooks/pre-compact.js +10 -7
  179. package/scripts/hooks/pre-edit-recall.js +4 -8
  180. package/scripts/hooks/session-start.js +95 -47
  181. package/scripts/hooks/session-summary.js +120 -52
  182. package/scripts/hooks/user-prompt-intent.js +3 -2
  183. package/skills/memesh/SKILL.md +3 -2
  184. package/dist/core/skill-usage-log.d.ts +0 -11
  185. package/dist/core/skill-usage-log.d.ts.map +0 -1
  186. package/dist/core/skill-usage-log.js +0 -125
  187. package/dist/core/skill-usage-log.js.map +0 -1
  188. package/dist/core/verifier.d.ts +0 -40
  189. package/dist/core/verifier.d.ts.map +0 -1
  190. package/dist/core/verifier.js +0 -206
  191. package/dist/core/verifier.js.map +0 -1
  192. package/dist/mcp/launcher.d.ts +0 -3
  193. package/dist/mcp/launcher.d.ts.map +0 -1
  194. package/dist/mcp/launcher.js +0 -37
  195. package/dist/mcp/launcher.js.map +0 -1
  196. package/scripts/hooks/pre-bash-orchestration-nudge.js +0 -155
  197. package/scripts/postinstall-rebuild.mjs +0 -41
  198. package/skills/agentic-orchestration/SKILL.md +0 -399
@@ -7,16 +7,16 @@ 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';
13
14
  import { computeProjects } from '../../core/projects.js';
14
15
  import { computeGraph } from '../../core/graph.js';
15
- import { verifyAgentWork } from '../../core/verifier.js';
16
- 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';
16
+ import { RememberSchema as RememberBody, RecallSchema as RecallBody, ForgetSchema as ForgetBody, ExportSchema as ExportBody, ImportSchema as ImportBody, LearnSchema as LearnBody, } from '../schemas.js';
17
+ import { checkForUpdate, getLastUpdateCheck, getUpdateCheck } from '../../core/version-check.js';
18
18
  import { getCurrentInstallChannel, getInstallChannelSupport } from '../../core/install-channel.js';
19
- import { getDbPath, getMemeshDirFromDbPath } from '../../core/paths.js';
19
+ import { getDbPath, getMemeshDirFromDbPath, redactSecrets, redactUserPaths } from '../../core/paths.js';
20
20
  import { RETIRED_ROUTES } from './retired-routes.js';
21
21
  import fs from 'fs';
22
22
  import path from 'path';
@@ -25,12 +25,23 @@ const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)
25
25
  const packageRoot = path.dirname(packageJsonPath);
26
26
  const packageVersion = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version ?? '0.0.0';
27
27
  const app = express();
28
+ export function isLoopbackRequest(req) {
29
+ const ip = req.ip ?? '';
30
+ return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
31
+ }
28
32
  const apiLimiter = rateLimit({
29
33
  windowMs: 15 * 60 * 1000,
30
34
  max: 100,
31
35
  standardHeaders: true,
32
36
  legacyHeaders: false,
33
- message: 'Too many requests from this IP, please try again later.',
37
+ skip: (req) => isLoopbackRequest(req),
38
+ handler: (_req, res) => {
39
+ res.status(429).json({
40
+ success: false,
41
+ errorCode: 'rate.limited',
42
+ error: 'Too many requests in a short time. Wait a moment and try again.',
43
+ });
44
+ },
34
45
  });
35
46
  let remoteToken = null;
36
47
  const serverAuthRequired = new WeakMap();
@@ -97,6 +108,7 @@ function bearerAuth(req, res, next) {
97
108
  if (!remoteToken) {
98
109
  res.status(503).json({
99
110
  success: false,
111
+ errorCode: 'auth.not-configured',
100
112
  error: 'remote bearer auth not configured on this server',
101
113
  });
102
114
  return;
@@ -105,17 +117,17 @@ function bearerAuth(req, res, next) {
105
117
  const trimmed = header.trim();
106
118
  const wsIndex = trimmed.search(/\s/);
107
119
  if (wsIndex < 0 || trimmed.slice(0, wsIndex).toLowerCase() !== 'bearer') {
108
- res.status(401).json({ success: false, error: 'Missing Authorization: Bearer <token>' });
120
+ res.status(401).json({ success: false, errorCode: 'auth.missing-bearer', error: 'Missing Authorization: Bearer <token>' });
109
121
  return;
110
122
  }
111
123
  const tokenPart = trimmed.slice(wsIndex + 1).trim();
112
124
  if (!tokenPart) {
113
- res.status(401).json({ success: false, error: 'Missing Authorization: Bearer <token>' });
125
+ res.status(401).json({ success: false, errorCode: 'auth.missing-bearer', error: 'Missing Authorization: Bearer <token>' });
114
126
  return;
115
127
  }
116
128
  const presented = Buffer.from(tokenPart, 'utf8');
117
129
  if (!constantTimeEquals(presented, remoteToken)) {
118
- res.status(401).json({ success: false, error: 'Invalid bearer token' });
130
+ res.status(401).json({ success: false, errorCode: 'auth.invalid-token', error: 'Invalid bearer token' });
119
131
  return;
120
132
  }
121
133
  next();
@@ -130,6 +142,7 @@ function payloadTooLargeHandler(err, _req, res, next) {
130
142
  if (e.type === 'entity.parse.failed' || (err instanceof SyntaxError && (e.status === 400 || e.statusCode === 400))) {
131
143
  res.status(400).json({
132
144
  success: false,
145
+ errorCode: 'validation.bad-body',
133
146
  error: 'Request body is not valid JSON.',
134
147
  hint: 'Send a JSON object with Content-Type: application/json.',
135
148
  });
@@ -140,6 +153,7 @@ function payloadTooLargeHandler(err, _req, res, next) {
140
153
  return next(err);
141
154
  res.status(413).json({
142
155
  success: false,
156
+ errorCode: 'payload.too-large',
143
157
  error: 'Request body exceeds the 1MB limit',
144
158
  code: 'PAYLOAD_TOO_LARGE',
145
159
  limit: '1mb',
@@ -179,25 +193,16 @@ app.get('/v1/health', (_req, res) => {
179
193
  if (message === 'Database not opened') {
180
194
  res.status(503).json({
181
195
  success: false,
196
+ errorCode: 'server.internal',
182
197
  error: 'Database not initialized',
183
198
  details: 'MeMesh database failed to open at startup. Check server logs for details, or run "memesh doctor" to diagnose.',
184
199
  });
185
200
  }
186
201
  else {
187
- res.status(500).json({ success: false, error: message });
202
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: message });
188
203
  }
189
204
  }
190
205
  });
191
- function redactSecrets(input) {
192
- return input
193
- .replace(/sk-ant-[A-Za-z0-9_-]{20,}/g, 'sk-ant-***REDACTED***')
194
- .replace(/sk-proj-[A-Za-z0-9_-]{20,}/g, 'sk-proj-***REDACTED***')
195
- .replace(/sk-[A-Za-z0-9]{32,}/g, 'sk-***REDACTED***')
196
- .replace(/ghp_[A-Za-z0-9]{30,}/g, 'ghp_***REDACTED***')
197
- .replace(/gho_[A-Za-z0-9]{30,}/g, 'gho_***REDACTED***')
198
- .replace(/AKIA[A-Z0-9]{16}/g, 'AKIA***REDACTED***')
199
- .replace(/Bearer\s+[A-Za-z0-9._-]{20,}/gi, 'Bearer ***REDACTED***');
200
- }
201
206
  app.get('/v1/doctor', async (_req, res) => {
202
207
  try {
203
208
  const { runDoctor } = await import('../../core/doctor.js');
@@ -205,11 +210,11 @@ app.get('/v1/doctor', async (_req, res) => {
205
210
  packageRoot,
206
211
  packageVersion,
207
212
  });
208
- const safe = JSON.parse(redactSecrets(JSON.stringify(result)));
213
+ const safe = JSON.parse(redactUserPaths(redactSecrets(JSON.stringify(result))));
209
214
  res.json({ success: true, data: safe });
210
215
  }
211
216
  catch (err) {
212
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
217
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
213
218
  }
214
219
  });
215
220
  function requireJsonBody(req, res) {
@@ -217,6 +222,7 @@ function requireJsonBody(req, res) {
217
222
  return true;
218
223
  res.status(400).json({
219
224
  success: false,
225
+ errorCode: 'validation.bad-body',
220
226
  error: 'No JSON body was parsed from this request.',
221
227
  hint: 'Send the payload with Content-Type: application/json.',
222
228
  });
@@ -229,27 +235,29 @@ function handlePost(schema, req, res, handler) {
229
235
  if (!parsed.success) {
230
236
  res.status(400).json({
231
237
  success: false,
238
+ errorCode: 'validation.bad-body',
232
239
  error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
233
240
  });
234
241
  return;
235
242
  }
236
- Promise.resolve(handler(parsed.data))
243
+ Promise.resolve()
244
+ .then(() => handler(parsed.data))
237
245
  .then((data) => res.json({ success: true, data }))
238
- .catch((err) => res.status(400).json({ success: false, error: err instanceof Error ? err.message : String(err) }));
246
+ .catch((err) => res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) }));
239
247
  }
240
248
  function handleGet(res, produce) {
241
249
  Promise.resolve()
242
250
  .then(produce)
243
251
  .then((data) => res.json({ success: true, data }))
244
- .catch((err) => res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) }));
252
+ .catch((err) => res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) }));
245
253
  }
246
- app.post('/v1/remember', (req, res) => handlePost(RememberBody, req, res, remember));
254
+ app.post('/v1/remember', (req, res) => handlePost(RememberBody, req, res, (data) => remember({ ...data, sourceHost: 'http' })));
247
255
  app.post('/v1/recall', async (req, res) => {
248
256
  if (!requireJsonBody(req, res))
249
257
  return;
250
258
  const parsed = RecallBody.safeParse(req.body);
251
259
  if (!parsed.success) {
252
- res.status(400).json({ success: false, error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
260
+ res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
253
261
  return;
254
262
  }
255
263
  try {
@@ -257,27 +265,46 @@ app.post('/v1/recall', async (req, res) => {
257
265
  res.json({ success: true, data: conflicts.length > 0 ? { entities, conflicts } : entities });
258
266
  }
259
267
  catch (err) {
260
- res.status(400).json({ success: false, error: err instanceof Error ? err.message : String(err) });
268
+ res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) });
261
269
  }
262
270
  });
263
271
  app.post('/v1/forget', (req, res) => handlePost(ForgetBody, req, res, forget));
264
272
  app.post('/v1/consolidate', (_req, res) => {
265
- res.status(410).json({ success: false, error: RETIRED_ROUTES['/v1/consolidate'] });
273
+ res.status(410).json({ success: false, errorCode: 'route.retired', error: RETIRED_ROUTES['/v1/consolidate'] });
266
274
  });
267
275
  app.post('/v1/export', (req, res) => handlePost(ExportBody, req, res, exportMemories));
268
276
  app.post('/v1/import', (req, res) => handlePost(ImportBody, req, res, importMemories));
269
- app.post('/v1/learn', (req, res) => handlePost(LearnBody, req, res, learn));
270
- app.post('/v1/verify', (req, res) => handlePost(VerifyBody, req, res, verifyAgentWork));
277
+ app.post('/v1/learn', (req, res) => handlePost(LearnBody, req, res, (data) => learn({ ...data, sourceHost: 'http' })));
278
+ app.post('/v1/verify', (_req, res) => {
279
+ res.status(410).json({ success: false, errorCode: 'route.retired', error: RETIRED_ROUTES['/v1/verify'] });
280
+ });
281
+ const API_KEY_MASK = '***';
271
282
  function maskLlmSecrets(obj) {
272
283
  const masked = { ...obj };
273
284
  if (masked.llm?.apiKey) {
274
- masked.llm = { ...masked.llm, apiKey: '***' };
285
+ masked.llm = { ...masked.llm, apiKey: API_KEY_MASK };
275
286
  }
276
287
  if (Array.isArray(masked.llmFallbacks) && masked.llmFallbacks.length > 0) {
277
- masked.llmFallbacks = masked.llmFallbacks.map(fb => fb?.apiKey ? { ...fb, apiKey: '***' } : fb);
288
+ masked.llmFallbacks = masked.llmFallbacks.map(fb => fb?.apiKey ? { ...fb, apiKey: API_KEY_MASK } : fb);
278
289
  }
279
290
  return masked;
280
291
  }
292
+ function preserveFallbackApiKeys(incoming, stored) {
293
+ return incoming.map((entry) => {
294
+ const { keepKeyFrom, ...clean } = entry;
295
+ if (clean.apiKey === API_KEY_MASK)
296
+ delete clean.apiKey;
297
+ if (clean.apiKey)
298
+ return clean;
299
+ if (typeof keepKeyFrom === 'number' && stored && keepKeyFrom >= 0 && keepKeyFrom < stored.length) {
300
+ const src = stored[keepKeyFrom];
301
+ if (src && src.provider === clean.provider && src.apiKey) {
302
+ return { ...clean, apiKey: src.apiKey };
303
+ }
304
+ }
305
+ return clean;
306
+ });
307
+ }
281
308
  app.get('/v1/config', (_req, res) => {
282
309
  try {
283
310
  const config = readConfig();
@@ -285,7 +312,7 @@ app.get('/v1/config', (_req, res) => {
285
312
  res.json({ success: true, data: { config: maskLlmSecrets(config), capabilities: maskLlmSecrets(caps) } });
286
313
  }
287
314
  catch (err) {
288
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
315
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
289
316
  }
290
317
  });
291
318
  const ConfigBody = z.object({
@@ -301,11 +328,16 @@ const ConfigBody = z.object({
301
328
  provider: z.enum(['anthropic', 'openai', 'ollama']),
302
329
  model: z.string().optional(),
303
330
  apiKey: z.string().optional(),
331
+ keepKeyFrom: z.number().int().nonnegative().nullable().optional(),
304
332
  })).optional(),
305
333
  autoCapture: z.boolean().optional(),
306
334
  sessionLimit: z.number().int().min(1).max(100).optional(),
307
- enableAgenticOrchestration: z.boolean().optional(),
308
335
  autoUpdate: z.enum(['off', 'patch', 'minor', 'major']).optional(),
336
+ language: z.string().trim().min(1).max(60)
337
+ .refine((v) => languageValueError(v) === null, {
338
+ message: 'language must not contain line breaks or other control characters',
339
+ })
340
+ .optional(),
309
341
  setupCompleted: z.boolean().optional(),
310
342
  }).strip();
311
343
  app.post('/v1/config', async (req, res) => {
@@ -313,29 +345,34 @@ app.post('/v1/config', async (req, res) => {
313
345
  return;
314
346
  const parsed = ConfigBody.safeParse(req.body);
315
347
  if (!parsed.success) {
316
- res.status(400).json({ success: false, error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
348
+ res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
317
349
  return;
318
350
  }
319
351
  try {
320
352
  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();
353
+ if (parsed.data.llm && parsed.data.llm.apiKey === API_KEY_MASK) {
354
+ if (before.llm && before.llm.provider === parsed.data.llm.provider && before.llm.apiKey) {
355
+ parsed.data.llm.apiKey = before.llm.apiKey;
356
+ }
357
+ else {
358
+ delete parsed.data.llm.apiKey;
359
+ }
360
+ }
361
+ if (parsed.data.llmFallbacks) {
362
+ parsed.data.llmFallbacks = preserveFallbackApiKeys(parsed.data.llmFallbacks, before.llmFallbacks);
328
363
  }
364
+ const updated = updateConfig(parsed.data);
329
365
  res.json({ success: true, data: maskLlmSecrets(updated) });
330
366
  }
331
367
  catch (err) {
332
- res.status(400).json({ success: false, error: err instanceof Error ? err.message : String(err) });
368
+ res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) });
333
369
  }
334
370
  });
335
371
  const ConfigTestBody = z.object({
336
372
  provider: z.enum(['anthropic', 'openai', 'ollama']),
337
373
  apiKey: z.string().max(500).optional(),
338
374
  host: z.string().max(500).optional(),
375
+ fallbackIndex: z.number().int().nonnegative().optional(),
339
376
  });
340
377
  app.post('/v1/config/test', async (req, res) => {
341
378
  if (!requireJsonBody(req, res))
@@ -344,17 +381,23 @@ app.post('/v1/config/test', async (req, res) => {
344
381
  if (!parsed.success) {
345
382
  res.status(400).json({
346
383
  success: false,
384
+ errorCode: 'validation.bad-body',
347
385
  error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
348
386
  });
349
387
  return;
350
388
  }
351
389
  try {
352
390
  const { probeProvider } = await import('../../core/llm-validator.js');
353
- const { provider, host } = parsed.data;
391
+ const { provider, host, fallbackIndex } = parsed.data;
354
392
  let { apiKey } = parsed.data;
355
393
  if (!apiKey && (provider === 'anthropic' || provider === 'openai')) {
356
394
  const existing = readConfig();
357
- if (existing.llm?.provider === provider && existing.llm.apiKey) {
395
+ if (typeof fallbackIndex === 'number') {
396
+ const fb = existing.llmFallbacks?.[fallbackIndex];
397
+ if (fb && fb.provider === provider && fb.apiKey)
398
+ apiKey = fb.apiKey;
399
+ }
400
+ else if (existing.llm?.provider === provider && existing.llm.apiKey) {
358
401
  apiKey = existing.llm.apiKey;
359
402
  }
360
403
  }
@@ -362,7 +405,7 @@ app.post('/v1/config/test', async (req, res) => {
362
405
  res.json({ success: true, data: result });
363
406
  }
364
407
  catch (err) {
365
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
408
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
366
409
  }
367
410
  });
368
411
  app.get('/v1/update-status', async (req, res) => {
@@ -396,7 +439,7 @@ app.get('/v1/update-status', async (req, res) => {
396
439
  });
397
440
  }
398
441
  catch (err) {
399
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
442
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
400
443
  }
401
444
  });
402
445
  app.get('/v1/graph', (_req, res) => handleGet(res, () => computeGraph(getDatabase())));
@@ -415,7 +458,7 @@ app.post('/v1/demo/seed', async (_req, res) => {
415
458
  res.json({ success: true, data });
416
459
  }
417
460
  catch (err) {
418
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
461
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
419
462
  }
420
463
  });
421
464
  app.post('/v1/demo/reset', async (_req, res) => {
@@ -425,7 +468,7 @@ app.post('/v1/demo/reset', async (_req, res) => {
425
468
  res.json({ success: true, data });
426
469
  }
427
470
  catch (err) {
428
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
471
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
429
472
  }
430
473
  });
431
474
  app.get('/v1/projects', (_req, res) => handleGet(res, () => computeProjects(getDatabase())));
@@ -437,7 +480,7 @@ app.get('/v1/telemetry', async (req, res) => {
437
480
  try {
438
481
  const parsed = TelemetryQuerySchema.safeParse(req.query);
439
482
  if (!parsed.success) {
440
- res.status(400).json({ success: false, error: parsed.error.issues.map(i => i.message).join('; ') });
483
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: parsed.error.issues.map(i => i.message).join('; ') });
441
484
  return;
442
485
  }
443
486
  const { summariseTelemetry } = await import('../../core/llm-telemetry.js');
@@ -445,7 +488,7 @@ app.get('/v1/telemetry', async (req, res) => {
445
488
  res.json({ success: true, data: { window_days: parsed.data.window, summaries } });
446
489
  }
447
490
  catch (err) {
448
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
491
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
449
492
  }
450
493
  });
451
494
  const DreamProposalsQuerySchema = z.object({
@@ -455,7 +498,7 @@ app.get('/v1/dream/proposals', (req, res) => {
455
498
  try {
456
499
  const parsed = DreamProposalsQuerySchema.safeParse(req.query);
457
500
  if (!parsed.success) {
458
- res.status(400).json({ success: false, error: parsed.error.issues.map(i => i.message).join('; ') });
501
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: parsed.error.issues.map(i => i.message).join('; ') });
459
502
  return;
460
503
  }
461
504
  const status = parsed.data.status;
@@ -465,22 +508,22 @@ app.get('/v1/dream/proposals', (req, res) => {
465
508
  ? [...listProposals(db, 'pending'), ...listProposals(db, 'applied'), ...listProposals(db, 'rejected')]
466
509
  : listProposals(db, status);
467
510
  res.json({ success: true, data: rows });
468
- }).catch((err) => res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) }));
511
+ }).catch((err) => res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) }));
469
512
  }
470
513
  catch (err) {
471
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
514
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
472
515
  }
473
516
  });
474
517
  app.get('/v1/dream/proposals/:id', (req, res) => {
475
518
  const id = parseInt(req.params.id, 10);
476
519
  if (!Number.isInteger(id) || id < 1) {
477
- res.status(400).json({ success: false, error: 'invalid id' });
520
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
478
521
  return;
479
522
  }
480
523
  try {
481
524
  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
525
  if (!row) {
483
- res.status(404).json({ success: false, error: `proposal #${id} not found` });
526
+ res.status(404).json({ success: false, errorCode: 'resource.not-found', error: `proposal #${id} not found` });
484
527
  return;
485
528
  }
486
529
  let digest = null;
@@ -496,7 +539,7 @@ app.get('/v1/dream/proposals/:id', (req, res) => {
496
539
  res.json({ success: true, data: { ...row, proposed_digest: digest, source_ids: sourceIds } });
497
540
  }
498
541
  catch (err) {
499
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
542
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
500
543
  }
501
544
  });
502
545
  const DreamRunBody = z.object({
@@ -510,52 +553,60 @@ app.post('/v1/dream/run', async (req, res) => {
510
553
  if (!parsed.success) {
511
554
  res.status(400).json({
512
555
  success: false,
556
+ errorCode: 'validation.bad-body',
513
557
  error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
514
558
  });
515
559
  return;
516
560
  }
517
561
  try {
518
562
  const { runDreamer } = await import('../../core/dreamer.js');
519
- const cfg = readConfig();
520
- if (!cfg.llm) {
563
+ const caps = detectCapabilities();
564
+ const llm = caps.llm;
565
+ if (!llm) {
521
566
  res.status(400).json({
522
567
  success: false,
568
+ errorCode: 'llm.not-configured',
523
569
  error: 'No LLM configured — dream run requires Smart Mode. Configure a provider in Settings.',
524
570
  });
525
571
  return;
526
572
  }
527
- const result = await runDreamer(getDatabase(), cfg.llm, {
573
+ const result = await runDreamer(getDatabase(), llm, {
528
574
  project: parsed.data.project,
529
575
  windowDays: parsed.data.windowDays,
530
576
  maxLlmCalls: parsed.data.maxLlmCalls,
531
- fallbacks: cfg.llmFallbacks,
577
+ fallbacks: caps.llmFallbacks,
532
578
  validateBeforeStage: parsed.data.validate,
533
579
  });
534
580
  res.json({ success: true, data: result });
535
581
  }
536
582
  catch (err) {
537
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
583
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
538
584
  }
539
585
  });
540
586
  app.post('/v1/dream/proposals/:id/accept', async (req, res) => {
541
587
  const id = parseInt(req.params.id, 10);
542
588
  if (!Number.isInteger(id) || id < 1) {
543
- res.status(400).json({ success: false, error: 'invalid id' });
589
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
544
590
  return;
545
591
  }
592
+ let NothingToClaim;
546
593
  try {
547
- const { applyProposal } = await import('../../core/dreamer.js');
594
+ const dreamer = await import('../../core/dreamer.js');
595
+ NothingToClaim = dreamer.NothingToClaimError;
548
596
  const kg = new KnowledgeGraph(getDatabase());
549
- const result = applyProposal(getDatabase(), id, kg);
597
+ const result = dreamer.applyProposal(getDatabase(), id, kg);
550
598
  res.json({ success: true, data: result });
551
599
  }
552
600
  catch (err) {
553
601
  const msg = err instanceof Error ? err.message : String(err);
554
- if (/not found or not pending/.test(msg)) {
555
- res.status(404).json({ success: false, error: msg });
602
+ if (NothingToClaim && err instanceof NothingToClaim) {
603
+ res.status(400).json({ success: false, errorCode: 'operation.failed', error: msg });
604
+ }
605
+ else if (/not found or not pending/.test(msg)) {
606
+ res.status(404).json({ success: false, errorCode: 'resource.not-found', error: msg });
556
607
  }
557
608
  else {
558
- res.status(500).json({ success: false, error: msg });
609
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
559
610
  }
560
611
  }
561
612
  });
@@ -565,12 +616,12 @@ const RejectBodySchema = z.object({
565
616
  app.post('/v1/dream/proposals/:id/reject', async (req, res) => {
566
617
  const id = parseInt(req.params.id, 10);
567
618
  if (!Number.isInteger(id) || id < 1) {
568
- res.status(400).json({ success: false, error: 'invalid id' });
619
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
569
620
  return;
570
621
  }
571
622
  const parsed = RejectBodySchema.safeParse(req.body ?? {});
572
623
  if (!parsed.success) {
573
- res.status(400).json({ success: false, error: parsed.error.issues.map(i => i.message).join('; ') });
624
+ res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => i.message).join('; ') });
574
625
  return;
575
626
  }
576
627
  try {
@@ -581,10 +632,10 @@ app.post('/v1/dream/proposals/:id/reject', async (req, res) => {
581
632
  catch (err) {
582
633
  const msg = err instanceof Error ? err.message : String(err);
583
634
  if (/not found or not pending/.test(msg)) {
584
- res.status(404).json({ success: false, error: msg });
635
+ res.status(404).json({ success: false, errorCode: 'resource.not-found', error: msg });
585
636
  }
586
637
  else {
587
- res.status(500).json({ success: false, error: msg });
638
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
588
639
  }
589
640
  }
590
641
  });
@@ -597,7 +648,7 @@ app.get('/v1/entities', (req, res) => {
597
648
  try {
598
649
  const parsed = EntitiesQuerySchema.safeParse(req.query);
599
650
  if (!parsed.success) {
600
- res.status(400).json({ success: false, error: `Invalid query: ${parsed.error.message}` });
651
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: `Invalid query: ${parsed.error.message}` });
601
652
  return;
602
653
  }
603
654
  const { type: typeFilter, limit, status } = parsed.data;
@@ -610,7 +661,7 @@ app.get('/v1/entities', (req, res) => {
610
661
  res.json({ success: true, data: entities });
611
662
  }
612
663
  catch (err) {
613
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
664
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
614
665
  }
615
666
  });
616
667
  app.get('/v1/entities/:name', (req, res) => {
@@ -619,13 +670,13 @@ app.get('/v1/entities/:name', (req, res) => {
619
670
  const kg = new KnowledgeGraph(db);
620
671
  const entity = kg.getEntity(req.params.name);
621
672
  if (!entity) {
622
- res.status(404).json({ success: false, error: `Entity "${req.params.name}" not found` });
673
+ res.status(404).json({ success: false, errorCode: 'resource.not-found', error: `Entity "${req.params.name}" not found` });
623
674
  return;
624
675
  }
625
676
  res.json({ success: true, data: entity });
626
677
  }
627
678
  catch (err) {
628
- res.status(500).json({ success: false, error: err instanceof Error ? err.message : String(err) });
679
+ res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
629
680
  }
630
681
  });
631
682
  const HOST = process.env.MEMESH_HTTP_HOST || '127.0.0.1';
@@ -645,6 +696,7 @@ function isLoopbackHost(host) {
645
696
  app.use((req, res) => {
646
697
  res.status(404).json({
647
698
  success: false,
699
+ errorCode: 'route.not-found',
648
700
  code: 'NOT_FOUND',
649
701
  error: `No route for ${req.method} ${req.path}`,
650
702
  });
@@ -655,6 +707,10 @@ export function startServer(host = HOST, port = PORT, opts) {
655
707
  if (!allowRemote && isRemote) {
656
708
  throw new Error(`Refusing to bind MeMesh HTTP server to non-loopback host "${host}" without explicit remote access opt-in. Use --allow-remote or MEMESH_HTTP_ALLOW_REMOTE=true.`);
657
709
  }
710
+ if (allowRemote && !isRemote) {
711
+ process.stderr.write(`MeMesh HTTP: --allow-remote has no effect on loopback host "${host}" — the server stays local ` +
712
+ 'and no bearer token is generated. Add --host <address> to bind somewhere reachable.\n');
713
+ }
658
714
  if (isRemote) {
659
715
  const { token, freshlyCreated } = loadOrCreateRemoteToken();
660
716
  remoteToken = token;
@@ -694,16 +750,39 @@ export function startServer(host = HOST, port = PORT, opts) {
694
750
  throw new Error(`Database initialization failed: ${message}`, { cause: err });
695
751
  }
696
752
  logCapabilities();
753
+ const injectedUpdateSeam = Boolean(opts?.updateCheckImpl || opts?.lastUpdateCheckImpl);
754
+ const updateCheckWanted = opts?.autoUpdateCheck === true || injectedUpdateSeam;
755
+ if (updateCheckWanted && !process.env.MEMESH_SKIP_UPDATE_CHECK) {
756
+ void (async () => {
757
+ const readLast = opts?.lastUpdateCheckImpl ?? getLastUpdateCheck;
758
+ const refresh = opts?.updateCheckImpl ?? checkForUpdate;
759
+ const cached = readLast(packageVersion);
760
+ if (cached && (cached.freshness === 'fresh' || cached.freshness === 'cached'))
761
+ return;
762
+ await refresh(packageVersion);
763
+ })().catch(() => { });
764
+ }
697
765
  const server = app.listen(port, host, () => {
698
766
  const addr = server.address();
699
767
  if (addr && typeof addr === 'object') {
700
768
  console.log(`MeMesh HTTP server running at http://${addr.address}:${addr.port}`);
701
769
  console.log(`MeMesh dashboard: http://${addr.address}:${addr.port}/dashboard`);
702
770
  }
771
+ });
772
+ server.on('error', (err) => {
773
+ const where = `${host}:${port}`;
774
+ if (err.code === 'EADDRINUSE') {
775
+ console.error(`MeMesh: cannot start — ${where} is already in use.`);
776
+ console.error(`MeMesh: stop whatever is listening there, or pick another port with --port <n>.`);
777
+ }
778
+ else if (err.code === 'EACCES') {
779
+ console.error(`MeMesh: cannot start — not permitted to bind ${where}.`);
780
+ console.error(`MeMesh: ports below 1024 need elevated privileges; pick a port above 1024 with --port <n>.`);
781
+ }
703
782
  else {
704
- console.log(`MeMesh HTTP server running at http://${host}:${port}`);
705
- console.log(`MeMesh dashboard: http://${host}:${port}/dashboard`);
783
+ console.error(`MeMesh: cannot start on ${where} — ${err.message}`);
706
784
  }
785
+ process.exit(1);
707
786
  });
708
787
  serverAuthRequired.set(server, isRemote);
709
788
  return server;
@@ -713,7 +792,14 @@ export function __setRemoteTokenForTest(value) {
713
792
  }
714
793
  const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
715
794
  if (isMain || process.argv[1]?.endsWith('memesh-http')) {
716
- const server = startServer();
795
+ let server;
796
+ try {
797
+ server = startServer();
798
+ }
799
+ catch (err) {
800
+ console.error(`MeMesh: ${err instanceof Error ? err.message : String(err)}`);
801
+ process.exit(1);
802
+ }
717
803
  function shutdown() {
718
804
  server.close();
719
805
  try {