@pcircle/memesh 4.5.1 → 4.6.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 (202) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +6 -4
  3. package/AGENTS.md +116 -0
  4. package/README.de.md +141 -48
  5. package/README.md +173 -48
  6. package/README.zh-TW.md +142 -48
  7. package/dashboard/dist/index.html +15 -14
  8. package/dist/cli/view-live.js +3 -3
  9. package/dist/core/analytics.d.ts +9 -0
  10. package/dist/core/analytics.d.ts.map +1 -1
  11. package/dist/core/analytics.js +36 -18
  12. package/dist/core/analytics.js.map +1 -1
  13. package/dist/core/auto-tagger.d.ts.map +1 -1
  14. package/dist/core/auto-tagger.js +4 -9
  15. package/dist/core/auto-tagger.js.map +1 -1
  16. package/dist/core/briefing.d.ts +8 -0
  17. package/dist/core/briefing.d.ts.map +1 -0
  18. package/dist/core/briefing.js +92 -0
  19. package/dist/core/briefing.js.map +1 -0
  20. package/dist/core/capture-flag.d.ts +5 -0
  21. package/dist/core/capture-flag.d.ts.map +1 -0
  22. package/dist/core/capture-flag.js +10 -0
  23. package/dist/core/capture-flag.js.map +1 -0
  24. package/dist/core/config.d.ts +0 -1
  25. package/dist/core/config.d.ts.map +1 -1
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/conflict-candidates.d.ts +20 -0
  28. package/dist/core/conflict-candidates.d.ts.map +1 -0
  29. package/dist/core/conflict-candidates.js +79 -0
  30. package/dist/core/conflict-candidates.js.map +1 -0
  31. package/dist/core/conflict-judge.d.ts +47 -0
  32. package/dist/core/conflict-judge.d.ts.map +1 -0
  33. package/dist/core/conflict-judge.js +189 -0
  34. package/dist/core/conflict-judge.js.map +1 -0
  35. package/dist/core/demo.d.ts.map +1 -1
  36. package/dist/core/demo.js +1 -1
  37. package/dist/core/demo.js.map +1 -1
  38. package/dist/core/digest-validator.d.ts.map +1 -1
  39. package/dist/core/digest-validator.js +3 -5
  40. package/dist/core/digest-validator.js.map +1 -1
  41. package/dist/core/doctor.d.ts +2 -0
  42. package/dist/core/doctor.d.ts.map +1 -1
  43. package/dist/core/doctor.js +59 -62
  44. package/dist/core/doctor.js.map +1 -1
  45. package/dist/core/dreamer.d.ts +5 -2
  46. package/dist/core/dreamer.d.ts.map +1 -1
  47. package/dist/core/dreamer.js +329 -25
  48. package/dist/core/dreamer.js.map +1 -1
  49. package/dist/core/embedder.d.ts +8 -4
  50. package/dist/core/embedder.d.ts.map +1 -1
  51. package/dist/core/embedder.js +82 -24
  52. package/dist/core/embedder.js.map +1 -1
  53. package/dist/core/failure-analyzer.d.ts.map +1 -1
  54. package/dist/core/failure-analyzer.js +7 -12
  55. package/dist/core/failure-analyzer.js.map +1 -1
  56. package/dist/core/graph.d.ts +12 -0
  57. package/dist/core/graph.d.ts.map +1 -1
  58. package/dist/core/graph.js +56 -1
  59. package/dist/core/graph.js.map +1 -1
  60. package/dist/core/guards.d.ts +20 -0
  61. package/dist/core/guards.d.ts.map +1 -0
  62. package/dist/core/guards.js +103 -0
  63. package/dist/core/guards.js.map +1 -0
  64. package/dist/core/install-channel.d.ts +1 -1
  65. package/dist/core/install-channel.d.ts.map +1 -1
  66. package/dist/core/install-channel.js +16 -5
  67. package/dist/core/install-channel.js.map +1 -1
  68. package/dist/core/install-hooks.d.ts +5 -0
  69. package/dist/core/install-hooks.d.ts.map +1 -1
  70. package/dist/core/install-hooks.js +0 -0
  71. package/dist/core/install-hooks.js.map +1 -1
  72. package/dist/core/json-utils.d.ts +1 -0
  73. package/dist/core/json-utils.d.ts.map +1 -1
  74. package/dist/core/json-utils.js +19 -10
  75. package/dist/core/json-utils.js.map +1 -1
  76. package/dist/core/kg-backfill.d.ts +5 -2
  77. package/dist/core/kg-backfill.d.ts.map +1 -1
  78. package/dist/core/kg-backfill.js +155 -5
  79. package/dist/core/kg-backfill.js.map +1 -1
  80. package/dist/core/lifecycle.d.ts.map +1 -1
  81. package/dist/core/lifecycle.js +14 -21
  82. package/dist/core/lifecycle.js.map +1 -1
  83. package/dist/core/memory-tool.d.ts.map +1 -1
  84. package/dist/core/memory-tool.js +4 -4
  85. package/dist/core/memory-tool.js.map +1 -1
  86. package/dist/core/operations.d.ts +13 -2
  87. package/dist/core/operations.d.ts.map +1 -1
  88. package/dist/core/operations.js +115 -28
  89. package/dist/core/operations.js.map +1 -1
  90. package/dist/core/prompt-safety.d.ts +1 -0
  91. package/dist/core/prompt-safety.d.ts.map +1 -1
  92. package/dist/core/prompt-safety.js +7 -0
  93. package/dist/core/prompt-safety.js.map +1 -1
  94. package/dist/core/schema-export.d.ts.map +1 -1
  95. package/dist/core/schema-export.js +31 -0
  96. package/dist/core/schema-export.js.map +1 -1
  97. package/dist/core/serializer.d.ts.map +1 -1
  98. package/dist/core/serializer.js +8 -0
  99. package/dist/core/serializer.js.map +1 -1
  100. package/dist/core/setup.d.ts +29 -0
  101. package/dist/core/setup.d.ts.map +1 -0
  102. package/dist/core/setup.js +127 -0
  103. package/dist/core/setup.js.map +1 -0
  104. package/dist/core/task-state-store.d.ts +17 -0
  105. package/dist/core/task-state-store.d.ts.map +1 -0
  106. package/dist/core/task-state-store.js +45 -0
  107. package/dist/core/task-state-store.js.map +1 -0
  108. package/dist/core/task-state.d.ts +19 -0
  109. package/dist/core/task-state.d.ts.map +1 -0
  110. package/dist/core/task-state.js +91 -0
  111. package/dist/core/task-state.js.map +1 -0
  112. package/dist/core/time-utils.d.ts +2 -0
  113. package/dist/core/time-utils.d.ts.map +1 -0
  114. package/dist/core/time-utils.js +14 -0
  115. package/dist/core/time-utils.js.map +1 -0
  116. package/dist/core/title.d.ts +5 -0
  117. package/dist/core/title.d.ts.map +1 -0
  118. package/dist/core/title.js +14 -0
  119. package/dist/core/title.js.map +1 -0
  120. package/dist/core/transcript-source.d.ts.map +1 -1
  121. package/dist/core/transcript-source.js +2 -3
  122. package/dist/core/transcript-source.js.map +1 -1
  123. package/dist/core/types.d.ts +5 -0
  124. package/dist/core/types.d.ts.map +1 -1
  125. package/dist/core/why.d.ts +54 -0
  126. package/dist/core/why.d.ts.map +1 -0
  127. package/dist/core/why.js +168 -0
  128. package/dist/core/why.js.map +1 -0
  129. package/dist/core/work-topology.d.ts +36 -0
  130. package/dist/core/work-topology.d.ts.map +1 -0
  131. package/dist/core/work-topology.js +192 -0
  132. package/dist/core/work-topology.js.map +1 -0
  133. package/dist/db.d.ts +33 -11
  134. package/dist/db.d.ts.map +1 -1
  135. package/dist/db.js +307 -315
  136. package/dist/db.js.map +1 -1
  137. package/dist/knowledge-graph.d.ts +1 -0
  138. package/dist/knowledge-graph.d.ts.map +1 -1
  139. package/dist/knowledge-graph.js +50 -40
  140. package/dist/knowledge-graph.js.map +1 -1
  141. package/dist/skills-manifest.json +62 -22
  142. package/dist/storage/conflicts.d.ts.map +1 -1
  143. package/dist/storage/conflicts.js +2 -7
  144. package/dist/storage/conflicts.js.map +1 -1
  145. package/dist/storage/fts-index.d.ts +4 -2
  146. package/dist/storage/fts-index.d.ts.map +1 -1
  147. package/dist/storage/fts-index.js +16 -4
  148. package/dist/storage/fts-index.js.map +1 -1
  149. package/dist/storage/schema.d.ts +20 -0
  150. package/dist/storage/schema.d.ts.map +1 -0
  151. package/dist/storage/schema.js +274 -0
  152. package/dist/storage/schema.js.map +1 -0
  153. package/dist/storage/sqlite.d.ts.map +1 -1
  154. package/dist/storage/sqlite.js +1 -1
  155. package/dist/storage/sqlite.js.map +1 -1
  156. package/dist/transports/cli/cli.d.ts +1 -4
  157. package/dist/transports/cli/cli.d.ts.map +1 -1
  158. package/dist/transports/cli/cli.js +579 -66
  159. package/dist/transports/cli/cli.js.map +1 -1
  160. package/dist/transports/http/server.d.ts.map +1 -1
  161. package/dist/transports/http/server.js +242 -303
  162. package/dist/transports/http/server.js.map +1 -1
  163. package/dist/transports/mcp/handlers.d.ts +46 -0
  164. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  165. package/dist/transports/mcp/handlers.js +59 -4
  166. package/dist/transports/mcp/handlers.js.map +1 -1
  167. package/dist/transports/schemas.d.ts +29 -10
  168. package/dist/transports/schemas.d.ts.map +1 -1
  169. package/dist/transports/schemas.js +33 -8
  170. package/dist/transports/schemas.js.map +1 -1
  171. package/hooks/hooks.json +10 -0
  172. package/llms-install.md +138 -0
  173. package/package.json +14 -9
  174. package/scripts/hooks/_generated/capture-flag.js +17 -0
  175. package/scripts/hooks/_generated/fts-index.js +16 -4
  176. package/scripts/hooks/_generated/guards.js +110 -0
  177. package/scripts/hooks/_generated/schema.js +281 -0
  178. package/scripts/hooks/_generated/sqlite.js +1 -1
  179. package/scripts/hooks/_generated/task-state.js +98 -0
  180. package/scripts/hooks/_generated/time-utils.js +21 -0
  181. package/scripts/hooks/_generated/title.js +21 -0
  182. package/scripts/hooks/_generated/work-topology.js +199 -0
  183. package/scripts/hooks/_shared.js +197 -480
  184. package/scripts/hooks/guard-check.js +76 -0
  185. package/scripts/hooks/post-commit.js +31 -1
  186. package/scripts/hooks/pre-compact.js +13 -1
  187. package/scripts/hooks/pre-edit-recall.js +158 -120
  188. package/scripts/hooks/session-start.js +169 -82
  189. package/scripts/hooks/session-summary.js +78 -90
  190. package/skills/memesh/SKILL.md +108 -76
  191. package/README.es.md +0 -467
  192. package/README.fr.md +0 -459
  193. package/README.ja.md +0 -467
  194. package/README.ko.md +0 -467
  195. package/README.pt.md +0 -459
  196. package/README.th.md +0 -460
  197. package/README.vi.md +0 -459
  198. package/README.zh-CN.md +0 -466
  199. package/dist/cli/view.d.ts +0 -3
  200. package/dist/cli/view.d.ts.map +0 -1
  201. package/dist/cli/view.js +0 -523
  202. package/dist/cli/view.js.map +0 -1
@@ -12,8 +12,8 @@ import { computePatterns } from '../../core/patterns.js';
12
12
  import { computeAnalytics, computePmAnalytics } from '../../core/analytics.js';
13
13
  import { computeStats } from '../../core/stats.js';
14
14
  import { computeProjects } from '../../core/projects.js';
15
- import { computeGraph } from '../../core/graph.js';
16
- import { RememberSchema as RememberBody, RecallSchema as RecallBody, ForgetSchema as ForgetBody, ExportSchema as ExportBody, ImportSchema as ImportBody, LearnSchema as LearnBody, } from '../schemas.js';
15
+ import { computeGraph, computeWorkGraph, computeNodeEvidence } from '../../core/graph.js';
16
+ import { RememberSchema as RememberBody, RecallSchema as RecallBody, ForgetSchema as ForgetBody, ExportSchema as ExportBody, ImportSchema as ImportBody, LearnSchema as LearnBody, WhySchema as WhyBody, } from '../schemas.js';
17
17
  import { checkForUpdate, getLastUpdateCheck, getUpdateCheck } from '../../core/version-check.js';
18
18
  import { getCurrentInstallChannel, getInstallChannelSupport } from '../../core/install-channel.js';
19
19
  import { getDbPath, getMemeshDirFromDbPath, redactSecrets, redactUserPaths } from '../../core/paths.js';
@@ -203,20 +203,14 @@ app.get('/v1/health', (_req, res) => {
203
203
  }
204
204
  }
205
205
  });
206
- app.get('/v1/doctor', async (_req, res) => {
207
- try {
208
- const { runDoctor } = await import('../../core/doctor.js');
209
- const result = await runDoctor({
210
- packageRoot,
211
- packageVersion,
212
- });
213
- const safe = JSON.parse(redactUserPaths(redactSecrets(JSON.stringify(result))));
214
- res.json({ success: true, data: safe });
215
- }
216
- catch (err) {
217
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
218
- }
219
- });
206
+ app.get('/v1/doctor', (_req, res) => handleGet(res, async () => {
207
+ const { runDoctor } = await import('../../core/doctor.js');
208
+ const result = await runDoctor({
209
+ packageRoot,
210
+ packageVersion,
211
+ });
212
+ return JSON.parse(redactUserPaths(redactSecrets(JSON.stringify(result))));
213
+ }));
220
214
  function requireJsonBody(req, res) {
221
215
  if (req.body !== undefined)
222
216
  return true;
@@ -228,46 +222,73 @@ function requireJsonBody(req, res) {
228
222
  });
229
223
  return false;
230
224
  }
231
- function handlePost(schema, req, res, handler) {
232
- if (!requireJsonBody(req, res))
225
+ class HttpError extends Error {
226
+ status;
227
+ code;
228
+ constructor(status, code, message) {
229
+ super(message);
230
+ this.status = status;
231
+ this.code = code;
232
+ }
233
+ }
234
+ function sendError(res, err, fallbackStatus, fallbackCode) {
235
+ if (err instanceof HttpError) {
236
+ res.status(err.status).json({ success: false, errorCode: err.code, error: err.message });
233
237
  return;
234
- const parsed = schema.safeParse(req.body);
238
+ }
239
+ res.status(fallbackStatus).json({
240
+ success: false,
241
+ errorCode: fallbackCode,
242
+ error: err instanceof Error ? err.message : String(err),
243
+ });
244
+ }
245
+ function zodErrorText(error) {
246
+ return error.issues.map(i => (i.path.length ? `${i.path.join('.')}: ${i.message}` : i.message)).join('; ');
247
+ }
248
+ function parseQuery(schema, req, res) {
249
+ const parsed = schema.safeParse(req.query);
250
+ if (!parsed.success) {
251
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: zodErrorText(parsed.error) });
252
+ return null;
253
+ }
254
+ return parsed.data;
255
+ }
256
+ function requireIdParam(req, res) {
257
+ const id = parseInt(String(req.params.id), 10);
258
+ if (!Number.isInteger(id) || id < 1) {
259
+ res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
260
+ return null;
261
+ }
262
+ return id;
263
+ }
264
+ function handlePost(schema, req, res, handler, opts) {
265
+ if (!opts?.allowEmptyBody && !requireJsonBody(req, res))
266
+ return;
267
+ const parsed = schema.safeParse(req.body ?? (opts?.allowEmptyBody ? {} : undefined));
235
268
  if (!parsed.success) {
236
269
  res.status(400).json({
237
270
  success: false,
238
271
  errorCode: 'validation.bad-body',
239
- error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
272
+ error: zodErrorText(parsed.error),
240
273
  });
241
274
  return;
242
275
  }
243
276
  Promise.resolve()
244
277
  .then(() => handler(parsed.data))
245
278
  .then((data) => res.json({ success: true, data }))
246
- .catch((err) => res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) }));
279
+ .catch((err) => sendError(res, err, opts?.errorStatus ?? 400, opts?.errorCode ?? 'operation.failed'));
247
280
  }
248
281
  function handleGet(res, produce) {
249
282
  Promise.resolve()
250
283
  .then(produce)
251
284
  .then((data) => res.json({ success: true, data }))
252
- .catch((err) => res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) }));
285
+ .catch((err) => sendError(res, err, 500, 'server.internal'));
253
286
  }
254
287
  app.post('/v1/remember', (req, res) => handlePost(RememberBody, req, res, (data) => remember({ ...data, sourceHost: 'http' })));
255
- app.post('/v1/recall', async (req, res) => {
256
- if (!requireJsonBody(req, res))
257
- return;
258
- const parsed = RecallBody.safeParse(req.body);
259
- if (!parsed.success) {
260
- res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
261
- return;
262
- }
263
- try {
264
- const { entities, conflicts } = await recallWithConflicts(parsed.data);
265
- res.json({ success: true, data: conflicts.length > 0 ? { entities, conflicts } : entities });
266
- }
267
- catch (err) {
268
- res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) });
269
- }
270
- });
288
+ app.post('/v1/recall', (req, res) => handlePost(RecallBody, req, res, async (data) => {
289
+ const { entities, conflicts, retrieval } = await recallWithConflicts(data);
290
+ return conflicts.length > 0 ? { entities, retrieval, conflicts } : { entities, retrieval };
291
+ }));
271
292
  app.post('/v1/forget', (req, res) => handlePost(ForgetBody, req, res, forget));
272
293
  app.post('/v1/consolidate', (_req, res) => {
273
294
  res.status(410).json({ success: false, errorCode: 'route.retired', error: RETIRED_ROUTES['/v1/consolidate'] });
@@ -275,6 +296,15 @@ app.post('/v1/consolidate', (_req, res) => {
275
296
  app.post('/v1/export', (req, res) => handlePost(ExportBody, req, res, exportMemories));
276
297
  app.post('/v1/import', (req, res) => handlePost(ImportBody, req, res, importMemories));
277
298
  app.post('/v1/learn', (req, res) => handlePost(LearnBody, req, res, (data) => learn({ ...data, sourceHost: 'http' })));
299
+ app.post('/v1/why', (req, res) => handlePost(WhyBody, req, res, async (data) => {
300
+ const { explainCommits } = await import('../../core/why.js');
301
+ return explainCommits(getDatabase(), {
302
+ file: data.file,
303
+ commits: data.commits?.map((hash) => ({ hash })),
304
+ project: data.project ?? null,
305
+ limit: data.limit,
306
+ });
307
+ }));
278
308
  app.post('/v1/verify', (_req, res) => {
279
309
  res.status(410).json({ success: false, errorCode: 'route.retired', error: RETIRED_ROUTES['/v1/verify'] });
280
310
  });
@@ -305,16 +335,11 @@ function preserveFallbackApiKeys(incoming, stored) {
305
335
  return clean;
306
336
  });
307
337
  }
308
- app.get('/v1/config', (_req, res) => {
309
- try {
310
- const config = readConfig();
311
- const caps = detectCapabilities(config);
312
- res.json({ success: true, data: { config: maskLlmSecrets(config), capabilities: maskLlmSecrets(caps) } });
313
- }
314
- catch (err) {
315
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
316
- }
317
- });
338
+ app.get('/v1/config', (_req, res) => handleGet(res, () => {
339
+ const config = readConfig();
340
+ const caps = detectCapabilities(config);
341
+ return { config: maskLlmSecrets(config), capabilities: maskLlmSecrets(caps) };
342
+ }));
318
343
  const ConfigBody = z.object({
319
344
  llm: z.union([
320
345
  z.object({
@@ -340,109 +365,101 @@ const ConfigBody = z.object({
340
365
  .optional(),
341
366
  setupCompleted: z.boolean().optional(),
342
367
  }).strip();
343
- app.post('/v1/config', async (req, res) => {
344
- if (!requireJsonBody(req, res))
345
- return;
346
- const parsed = ConfigBody.safeParse(req.body);
347
- if (!parsed.success) {
348
- res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ') });
349
- return;
350
- }
351
- try {
352
- const before = readConfig();
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
- }
368
+ app.post('/v1/config', (req, res) => handlePost(ConfigBody, req, res, (data) => {
369
+ const before = readConfig();
370
+ if (data.llm && data.llm.apiKey === API_KEY_MASK) {
371
+ if (before.llm && before.llm.provider === data.llm.provider && before.llm.apiKey) {
372
+ data.llm.apiKey = before.llm.apiKey;
360
373
  }
361
- if (parsed.data.llmFallbacks) {
362
- parsed.data.llmFallbacks = preserveFallbackApiKeys(parsed.data.llmFallbacks, before.llmFallbacks);
374
+ else {
375
+ delete data.llm.apiKey;
363
376
  }
364
- const updated = updateConfig(parsed.data);
365
- res.json({ success: true, data: maskLlmSecrets(updated) });
366
377
  }
367
- catch (err) {
368
- res.status(400).json({ success: false, errorCode: 'operation.failed', error: err instanceof Error ? err.message : String(err) });
378
+ if (data.llmFallbacks) {
379
+ data.llmFallbacks = preserveFallbackApiKeys(data.llmFallbacks, before.llmFallbacks);
369
380
  }
370
- });
381
+ const updated = updateConfig(data);
382
+ return maskLlmSecrets(updated);
383
+ }));
371
384
  const ConfigTestBody = z.object({
372
385
  provider: z.enum(['anthropic', 'openai', 'ollama']),
373
386
  apiKey: z.string().max(500).optional(),
374
387
  host: z.string().max(500).optional(),
375
388
  fallbackIndex: z.number().int().nonnegative().optional(),
376
389
  });
377
- app.post('/v1/config/test', async (req, res) => {
378
- if (!requireJsonBody(req, res))
379
- return;
380
- const parsed = ConfigTestBody.safeParse(req.body);
381
- if (!parsed.success) {
390
+ app.post('/v1/config/test', (req, res) => handlePost(ConfigTestBody, req, res, async (data) => {
391
+ const { probeProvider } = await import('../../core/llm-validator.js');
392
+ const { provider, host, fallbackIndex } = data;
393
+ let { apiKey } = data;
394
+ if (!apiKey && (provider === 'anthropic' || provider === 'openai')) {
395
+ const existing = readConfig();
396
+ if (typeof fallbackIndex === 'number') {
397
+ const fb = existing.llmFallbacks?.[fallbackIndex];
398
+ if (fb && fb.provider === provider && fb.apiKey)
399
+ apiKey = fb.apiKey;
400
+ }
401
+ else if (existing.llm?.provider === provider && existing.llm.apiKey) {
402
+ apiKey = existing.llm.apiKey;
403
+ }
404
+ }
405
+ return probeProvider(provider, apiKey, host);
406
+ }, { errorStatus: 500, errorCode: 'server.internal' }));
407
+ app.get('/v1/update-status', (req, res) => handleGet(res, async () => {
408
+ const cached = req.query.cached === '1' || req.query.cached === 'true';
409
+ const install = getCurrentInstallChannel({ packageRoot });
410
+ const installSupport = getInstallChannelSupport(install);
411
+ const update = await getUpdateCheck(packageVersion, { preferFresh: !cached });
412
+ return {
413
+ currentVersion: packageVersion,
414
+ latestVersion: update?.latestVersion ?? null,
415
+ checkedAt: update?.checkedAt ?? null,
416
+ lastAttemptAt: update?.lastAttemptAt ?? null,
417
+ lastSuccessfulCheckAt: update?.lastSuccessfulCheckAt ?? null,
418
+ lastError: update?.lastError ?? null,
419
+ updateAvailable: update?.updateAvailable ?? false,
420
+ checkSucceeded: update?.checkSucceeded ?? false,
421
+ source: update?.source ?? null,
422
+ freshness: update?.freshness ?? 'unavailable',
423
+ installChannel: installSupport.channel,
424
+ canSelfUpdate: installSupport.canSelfUpdate,
425
+ recommendedCommand: (update?.currentVersionDeprecated
426
+ && update.latestVersion
427
+ && update.latestVersion === update.currentVersion
428
+ && update.freshness === 'fresh') ? null : installSupport.recommendedCommand,
429
+ currentVersionDeprecated: update?.currentVersionDeprecated ?? false,
430
+ deprecationMessage: update?.deprecationMessage ?? null,
431
+ };
432
+ }));
433
+ app.get('/v1/graph', (req, res) => {
434
+ const layer = req.query.layer;
435
+ if (layer !== undefined && layer !== 'work') {
382
436
  res.status(400).json({
383
437
  success: false,
384
- errorCode: 'validation.bad-body',
385
- error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
438
+ errorCode: 'validation.bad-param',
439
+ error: "layer must be 'work' (omit the parameter for the full graph)",
386
440
  });
387
441
  return;
388
442
  }
389
- try {
390
- const { probeProvider } = await import('../../core/llm-validator.js');
391
- const { provider, host, fallbackIndex } = parsed.data;
392
- let { apiKey } = parsed.data;
393
- if (!apiKey && (provider === 'anthropic' || provider === 'openai')) {
394
- const existing = readConfig();
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) {
401
- apiKey = existing.llm.apiKey;
402
- }
403
- }
404
- const result = await probeProvider(provider, apiKey, host);
405
- res.json({ success: true, data: result });
406
- }
407
- catch (err) {
408
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
409
- }
443
+ handleGet(res, () => (layer === 'work' ? computeWorkGraph(getDatabase()) : computeGraph(getDatabase())));
410
444
  });
411
- app.get('/v1/update-status', async (req, res) => {
412
- try {
413
- const cached = req.query.cached === '1' || req.query.cached === 'true';
414
- const install = getCurrentInstallChannel({ packageRoot });
415
- const installSupport = getInstallChannelSupport(install);
416
- const update = await getUpdateCheck(packageVersion, { preferFresh: !cached });
417
- res.json({
418
- success: true,
419
- data: {
420
- currentVersion: packageVersion,
421
- latestVersion: update?.latestVersion ?? null,
422
- checkedAt: update?.checkedAt ?? null,
423
- lastAttemptAt: update?.lastAttemptAt ?? null,
424
- lastSuccessfulCheckAt: update?.lastSuccessfulCheckAt ?? null,
425
- lastError: update?.lastError ?? null,
426
- updateAvailable: update?.updateAvailable ?? false,
427
- checkSucceeded: update?.checkSucceeded ?? false,
428
- source: update?.source ?? null,
429
- freshness: update?.freshness ?? 'unavailable',
430
- installChannel: installSupport.channel,
431
- canSelfUpdate: installSupport.canSelfUpdate,
432
- recommendedCommand: (update?.currentVersionDeprecated
433
- && update.latestVersion
434
- && update.latestVersion === update.currentVersion
435
- && update.freshness === 'fresh') ? null : installSupport.recommendedCommand,
436
- currentVersionDeprecated: update?.currentVersionDeprecated ?? false,
437
- deprecationMessage: update?.deprecationMessage ?? null,
438
- },
445
+ app.get('/v1/graph/evidence', (req, res) => {
446
+ const node = req.query.node;
447
+ if (typeof node !== 'string' || node.length === 0) {
448
+ res.status(400).json({
449
+ success: false,
450
+ errorCode: 'validation.bad-param',
451
+ error: 'node query parameter is required (the work-node entity name)',
439
452
  });
453
+ return;
440
454
  }
441
- catch (err) {
442
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
443
- }
455
+ handleGet(res, () => {
456
+ const result = computeNodeEvidence(getDatabase(), node);
457
+ if (result === null) {
458
+ throw new HttpError(404, 'resource.not-found', `Entity "${node}" not found`);
459
+ }
460
+ return result;
461
+ });
444
462
  });
445
- app.get('/v1/graph', (_req, res) => handleGet(res, () => computeGraph(getDatabase())));
446
463
  app.get('/v1/stats', (_req, res) => handleGet(res, () => computeStats(getDatabase())));
447
464
  app.get('/v1/analytics', (_req, res) => handleGet(res, () => computeAnalytics(getDatabase())));
448
465
  app.get('/v1/analytics/pm', (req, res) => {
@@ -451,80 +468,51 @@ app.get('/v1/analytics/pm', (req, res) => {
451
468
  const windowDays = Number.isFinite(window) && window > 0 ? window : 30;
452
469
  handleGet(res, () => computePmAnalytics(getDatabase(), windowDays));
453
470
  });
454
- app.post('/v1/demo/seed', async (_req, res) => {
455
- try {
456
- const { seedDemo } = await import('../../core/demo.js');
457
- const data = seedDemo(getDatabase());
458
- res.json({ success: true, data });
459
- }
460
- catch (err) {
461
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
462
- }
463
- });
464
- app.post('/v1/demo/reset', async (_req, res) => {
465
- try {
466
- const { seedDemo } = await import('../../core/demo.js');
467
- const data = seedDemo(getDatabase(), { reset: true });
468
- res.json({ success: true, data });
469
- }
470
- catch (err) {
471
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
472
- }
473
- });
471
+ app.post('/v1/demo/seed', (_req, res) => handleGet(res, async () => {
472
+ const { seedDemo } = await import('../../core/demo.js');
473
+ return seedDemo(getDatabase());
474
+ }));
475
+ app.post('/v1/demo/reset', (_req, res) => handleGet(res, async () => {
476
+ const { seedDemo } = await import('../../core/demo.js');
477
+ return seedDemo(getDatabase(), { reset: true });
478
+ }));
474
479
  app.get('/v1/projects', (_req, res) => handleGet(res, () => computeProjects(getDatabase())));
475
480
  app.get('/v1/patterns', (_req, res) => handleGet(res, () => computePatterns(getDatabase())));
476
481
  const TelemetryQuerySchema = z.object({
477
482
  window: z.coerce.number().int().min(1).max(365).default(30),
478
483
  });
479
- app.get('/v1/telemetry', async (req, res) => {
480
- try {
481
- const parsed = TelemetryQuerySchema.safeParse(req.query);
482
- if (!parsed.success) {
483
- res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: parsed.error.issues.map(i => i.message).join('; ') });
484
- return;
485
- }
484
+ app.get('/v1/telemetry', (req, res) => {
485
+ const query = parseQuery(TelemetryQuerySchema, req, res);
486
+ if (!query)
487
+ return;
488
+ handleGet(res, async () => {
486
489
  const { summariseTelemetry } = await import('../../core/llm-telemetry.js');
487
- const summaries = summariseTelemetry(parsed.data.window);
488
- res.json({ success: true, data: { window_days: parsed.data.window, summaries } });
489
- }
490
- catch (err) {
491
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
492
- }
490
+ return { window_days: query.window, summaries: summariseTelemetry(query.window) };
491
+ });
493
492
  });
494
493
  const DreamProposalsQuerySchema = z.object({
495
494
  status: z.enum(['pending', 'applied', 'rejected', 'all']).default('pending'),
496
495
  });
497
496
  app.get('/v1/dream/proposals', (req, res) => {
498
- try {
499
- const parsed = DreamProposalsQuerySchema.safeParse(req.query);
500
- if (!parsed.success) {
501
- res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: parsed.error.issues.map(i => i.message).join('; ') });
502
- return;
503
- }
504
- const status = parsed.data.status;
505
- import('../../core/dreamer.js').then(({ listProposals }) => {
506
- const db = getDatabase();
507
- const rows = status === 'all'
508
- ? [...listProposals(db, 'pending'), ...listProposals(db, 'applied'), ...listProposals(db, 'rejected')]
509
- : listProposals(db, status);
510
- res.json({ success: true, data: rows });
511
- }).catch((err) => res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) }));
512
- }
513
- catch (err) {
514
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
515
- }
497
+ const query = parseQuery(DreamProposalsQuerySchema, req, res);
498
+ if (!query)
499
+ return;
500
+ handleGet(res, async () => {
501
+ const { listProposals } = await import('../../core/dreamer.js');
502
+ const db = getDatabase();
503
+ return query.status === 'all'
504
+ ? [...listProposals(db, 'pending'), ...listProposals(db, 'applied'), ...listProposals(db, 'rejected')]
505
+ : listProposals(db, query.status);
506
+ });
516
507
  });
517
508
  app.get('/v1/dream/proposals/:id', (req, res) => {
518
- const id = parseInt(req.params.id, 10);
519
- if (!Number.isInteger(id) || id < 1) {
520
- res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
509
+ const id = requireIdParam(req, res);
510
+ if (id === null)
521
511
  return;
522
- }
523
- try {
512
+ handleGet(res, () => {
524
513
  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);
525
514
  if (!row) {
526
- res.status(404).json({ success: false, errorCode: 'resource.not-found', error: `proposal #${id} not found` });
527
- return;
515
+ throw new HttpError(404, 'resource.not-found', `proposal #${id} not found`);
528
516
  }
529
517
  let digest = null;
530
518
  let sourceIds = [];
@@ -536,11 +524,8 @@ app.get('/v1/dream/proposals/:id', (req, res) => {
536
524
  sourceIds = JSON.parse(row.source_ids);
537
525
  }
538
526
  catch { }
539
- res.json({ success: true, data: { ...row, proposed_digest: digest, source_ids: sourceIds } });
540
- }
541
- catch (err) {
542
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
543
- }
527
+ return { ...row, proposed_digest: digest, source_ids: sourceIds };
528
+ });
544
529
  });
545
530
  const DreamRunBody = z.object({
546
531
  project: z.string().min(1).max(100).optional(),
@@ -548,96 +533,64 @@ const DreamRunBody = z.object({
548
533
  maxLlmCalls: z.number().int().min(1).max(20).default(5),
549
534
  validate: z.boolean().default(false),
550
535
  });
551
- app.post('/v1/dream/run', async (req, res) => {
552
- const parsed = DreamRunBody.safeParse(req.body ?? {});
553
- if (!parsed.success) {
554
- res.status(400).json({
555
- success: false,
556
- errorCode: 'validation.bad-body',
557
- error: parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; '),
558
- });
559
- return;
560
- }
561
- try {
562
- const { runDreamer } = await import('../../core/dreamer.js');
563
- const caps = detectCapabilities();
564
- const llm = caps.llm;
565
- if (!llm) {
566
- res.status(400).json({
567
- success: false,
568
- errorCode: 'llm.not-configured',
569
- error: 'No LLM configured — dream run requires Smart Mode. Configure a provider in Settings.',
570
- });
571
- return;
572
- }
573
- const result = await runDreamer(getDatabase(), llm, {
574
- project: parsed.data.project,
575
- windowDays: parsed.data.windowDays,
576
- maxLlmCalls: parsed.data.maxLlmCalls,
577
- fallbacks: caps.llmFallbacks,
578
- validateBeforeStage: parsed.data.validate,
579
- });
580
- res.json({ success: true, data: result });
581
- }
582
- catch (err) {
583
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
584
- }
585
- });
586
- app.post('/v1/dream/proposals/:id/accept', async (req, res) => {
587
- const id = parseInt(req.params.id, 10);
588
- if (!Number.isInteger(id) || id < 1) {
589
- res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
536
+ app.post('/v1/dream/run', (req, res) => handlePost(DreamRunBody, req, res, async (data) => {
537
+ const { runDreamer } = await import('../../core/dreamer.js');
538
+ const caps = detectCapabilities();
539
+ const llm = caps.llm;
540
+ if (!llm) {
541
+ throw new HttpError(400, 'llm.not-configured', 'No LLM configured — dream run requires Smart Mode. Configure a provider in Settings.');
542
+ }
543
+ return runDreamer(getDatabase(), llm, {
544
+ project: data.project,
545
+ windowDays: data.windowDays,
546
+ maxLlmCalls: data.maxLlmCalls,
547
+ fallbacks: caps.llmFallbacks,
548
+ validateBeforeStage: data.validate,
549
+ });
550
+ }, { allowEmptyBody: true, errorStatus: 500, errorCode: 'server.internal' }));
551
+ app.post('/v1/dream/proposals/:id/accept', (req, res) => {
552
+ const id = requireIdParam(req, res);
553
+ if (id === null)
590
554
  return;
591
- }
592
- let NothingToClaim;
593
- try {
555
+ handleGet(res, async () => {
594
556
  const dreamer = await import('../../core/dreamer.js');
595
- NothingToClaim = dreamer.NothingToClaimError;
596
- const kg = new KnowledgeGraph(getDatabase());
597
- const result = dreamer.applyProposal(getDatabase(), id, kg);
598
- res.json({ success: true, data: result });
599
- }
600
- catch (err) {
601
- const msg = err instanceof Error ? err.message : String(err);
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 });
557
+ try {
558
+ const kg = new KnowledgeGraph(getDatabase());
559
+ return dreamer.applyProposal(getDatabase(), id, kg);
607
560
  }
608
- else {
609
- res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
561
+ catch (err) {
562
+ const msg = err instanceof Error ? err.message : String(err);
563
+ if (err instanceof dreamer.NothingToClaimError) {
564
+ throw new HttpError(400, 'operation.failed', msg);
565
+ }
566
+ if (/not found or not pending/.test(msg)) {
567
+ throw new HttpError(404, 'resource.not-found', msg);
568
+ }
569
+ throw err;
610
570
  }
611
- }
571
+ });
612
572
  });
613
573
  const RejectBodySchema = z.object({
614
574
  reason: z.string().max(500).optional(),
615
575
  });
616
- app.post('/v1/dream/proposals/:id/reject', async (req, res) => {
617
- const id = parseInt(req.params.id, 10);
618
- if (!Number.isInteger(id) || id < 1) {
619
- res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: 'invalid id' });
620
- return;
621
- }
622
- const parsed = RejectBodySchema.safeParse(req.body ?? {});
623
- if (!parsed.success) {
624
- res.status(400).json({ success: false, errorCode: 'validation.bad-body', error: parsed.error.issues.map(i => i.message).join('; ') });
576
+ app.post('/v1/dream/proposals/:id/reject', (req, res) => {
577
+ const id = requireIdParam(req, res);
578
+ if (id === null)
625
579
  return;
626
- }
627
- try {
580
+ handlePost(RejectBodySchema, req, res, async (data) => {
628
581
  const { rejectProposal } = await import('../../core/dreamer.js');
629
- rejectProposal(getDatabase(), id, parsed.data.reason);
630
- res.json({ success: true, data: { id, status: 'rejected' } });
631
- }
632
- catch (err) {
633
- const msg = err instanceof Error ? err.message : String(err);
634
- if (/not found or not pending/.test(msg)) {
635
- res.status(404).json({ success: false, errorCode: 'resource.not-found', error: msg });
582
+ try {
583
+ rejectProposal(getDatabase(), id, data.reason);
636
584
  }
637
- else {
638
- res.status(500).json({ success: false, errorCode: 'server.internal', error: msg });
585
+ catch (err) {
586
+ const msg = err instanceof Error ? err.message : String(err);
587
+ if (/not found or not pending/.test(msg)) {
588
+ throw new HttpError(404, 'resource.not-found', msg);
589
+ }
590
+ throw err;
639
591
  }
640
- }
592
+ return { id, status: 'rejected' };
593
+ }, { allowEmptyBody: true, errorStatus: 500, errorCode: 'server.internal' });
641
594
  });
642
595
  const EntitiesQuerySchema = z.object({
643
596
  type: z.string().min(1).max(100).optional(),
@@ -645,40 +598,26 @@ const EntitiesQuerySchema = z.object({
645
598
  status: z.enum(['all', 'active']).optional(),
646
599
  });
647
600
  app.get('/v1/entities', (req, res) => {
648
- try {
649
- const parsed = EntitiesQuerySchema.safeParse(req.query);
650
- if (!parsed.success) {
651
- res.status(400).json({ success: false, errorCode: 'validation.bad-param', error: `Invalid query: ${parsed.error.message}` });
652
- return;
653
- }
654
- const { type: typeFilter, limit, status } = parsed.data;
601
+ const query = parseQuery(EntitiesQuerySchema, req, res);
602
+ if (!query)
603
+ return;
604
+ handleGet(res, () => {
605
+ const { type: typeFilter, limit, status } = query;
655
606
  const includeArchived = status === 'all';
656
- const db = getDatabase();
657
- const kg = new KnowledgeGraph(db);
658
- const entities = typeFilter
607
+ const kg = new KnowledgeGraph(getDatabase());
608
+ return typeFilter
659
609
  ? kg.listByType(typeFilter, limit, includeArchived)
660
610
  : kg.listRecent(limit, includeArchived);
661
- res.json({ success: true, data: entities });
662
- }
663
- catch (err) {
664
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
665
- }
666
- });
667
- app.get('/v1/entities/:name', (req, res) => {
668
- try {
669
- const db = getDatabase();
670
- const kg = new KnowledgeGraph(db);
671
- const entity = kg.getEntity(req.params.name);
672
- if (!entity) {
673
- res.status(404).json({ success: false, errorCode: 'resource.not-found', error: `Entity "${req.params.name}" not found` });
674
- return;
675
- }
676
- res.json({ success: true, data: entity });
677
- }
678
- catch (err) {
679
- res.status(500).json({ success: false, errorCode: 'server.internal', error: err instanceof Error ? err.message : String(err) });
680
- }
611
+ });
681
612
  });
613
+ app.get('/v1/entities/:name', (req, res) => handleGet(res, () => {
614
+ const kg = new KnowledgeGraph(getDatabase());
615
+ const entity = kg.getEntity(String(req.params.name));
616
+ if (!entity) {
617
+ throw new HttpError(404, 'resource.not-found', `Entity "${String(req.params.name)}" not found`);
618
+ }
619
+ return entity;
620
+ }));
682
621
  const HOST = process.env.MEMESH_HTTP_HOST || '127.0.0.1';
683
622
  const PORT = parseInt(process.env.MEMESH_HTTP_PORT || '3737');
684
623
  const ALLOW_REMOTE_BY_ENV = /^(1|true|yes)$/i.test(process.env.MEMESH_HTTP_ALLOW_REMOTE || '');