total-recall-brain 3.12.2 → 3.13.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 (154) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +3 -1
  9. package/frontend/src/App.tsx +112 -111
  10. package/frontend/src/api.ts +700 -18
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +78 -0
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +614 -0
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +583 -0
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/ModelsPage.tsx +280 -23
  30. package/frontend/src/pages/OkfPage.tsx +535 -0
  31. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  32. package/frontend/src/pages/OpenWikiPage.tsx +561 -0
  33. package/frontend/src/pages/SettingsPage.tsx +826 -740
  34. package/frontend/src/pages/SkillsPage.tsx +1259 -117
  35. package/frontend/src/pages/UsagePage.tsx +256 -11
  36. package/frontend/src/pages/VaultPage.tsx +223 -0
  37. package/frontend/src/types.ts +173 -0
  38. package/package.json +4 -4
  39. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  40. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  41. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  42. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  43. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  48. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  49. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  50. package/src/cli/brain.mjs +63 -5
  51. package/src/cli/connect.mjs +11 -19
  52. package/src/cli/daemon.mjs +5 -4
  53. package/src/cli/deploy-ui.mjs +8 -3
  54. package/src/cli/deploy.mjs +84 -27
  55. package/src/cli/ingest/index.mjs +1 -1
  56. package/src/cli/init.mjs +63 -34
  57. package/src/cli/remember.mjs +3 -3
  58. package/src/cli/secret.mjs +812 -0
  59. package/src/cli/setup.mjs +34 -7
  60. package/src/cli/share.mjs +2 -2
  61. package/src/cli/skill.mjs +396 -35
  62. package/src/cli/sync.mjs +4 -4
  63. package/src/cli/task.mjs +287 -0
  64. package/src/core/clarity-rewriter.mjs +5 -5
  65. package/src/core/config.mjs +44 -2
  66. package/src/core/conflict-detector.mjs +5 -5
  67. package/src/core/crons.mjs +54 -0
  68. package/src/core/daemon-loop.mjs +87 -235
  69. package/src/core/dream.mjs +170 -10
  70. package/src/core/env-import.mjs +456 -0
  71. package/src/core/fact-seeker.mjs +14 -14
  72. package/src/core/import-rules.mjs +20 -7
  73. package/src/core/inference-engine.mjs +3 -3
  74. package/src/core/migrate.mjs +14 -8
  75. package/src/core/okf-adapter.mjs +2 -2
  76. package/src/core/operation-validator.mjs +39 -331
  77. package/src/core/optimizer.mjs +7 -0
  78. package/src/core/post-mortem.mjs +2 -2
  79. package/src/core/project-brain.mjs +349 -0
  80. package/src/core/provider-catalog.mjs +296 -0
  81. package/src/core/quick-capture.mjs +1 -1
  82. package/src/core/remote-vault-sync.mjs +194 -0
  83. package/src/core/research-queue.mjs +7 -11
  84. package/src/core/runtime.mjs +33 -9
  85. package/src/core/scheduler.mjs +59 -5
  86. package/src/core/schema.mjs +14 -0
  87. package/src/core/search.mjs +1 -1
  88. package/src/core/secrets-env-export.mjs +323 -0
  89. package/src/core/secrets-rotate.mjs +245 -0
  90. package/src/core/secrets-store.mjs +645 -0
  91. package/src/core/semantic-index.mjs +3 -3
  92. package/src/core/skills-registry.mjs +1062 -0
  93. package/src/core/source-adapters.mjs +4 -1
  94. package/src/core/ssss-host-extension.mjs +94 -0
  95. package/src/core/ssss-kernel-bridge.mjs +530 -0
  96. package/src/core/surface.mjs +14 -0
  97. package/src/core/task-envelope.mjs +298 -0
  98. package/src/core/task-executors.mjs +600 -0
  99. package/src/core/validated-write.mjs +124 -56
  100. package/src/core/vault.mjs +19 -34
  101. package/src/core/webauthn-store.mjs +272 -0
  102. package/src/server/api.mjs +28 -16
  103. package/src/server/auth.mjs +53 -2
  104. package/src/server/index.mjs +3 -3
  105. package/src/server/keys.mjs +30 -4
  106. package/src/server/rest.mjs +405 -68
  107. package/src/server/route-manifest.json +168 -0
  108. package/src/server/routes/_shared.mjs +40 -1
  109. package/src/server/routes/docs.mjs +260 -0
  110. package/src/server/routes/memory.mjs +145 -32
  111. package/src/server/routes/research.mjs +30 -0
  112. package/src/server/routes/secrets.mjs +470 -0
  113. package/src/server/routes/share.mjs +2 -2
  114. package/src/server/routes/skills.mjs +452 -54
  115. package/src/server/routes/sync.mjs +48 -0
  116. package/src/server/routes/webauthn.mjs +184 -0
  117. package/src/server/update-route-manifest.mjs +39 -0
  118. package/templates/onboarding-interview.md +32 -22
  119. package/templates/openwiki/README.md +10 -0
  120. package/templates/openwiki/architecture.md +5 -0
  121. package/templates/openwiki/ide.md +10 -0
  122. package/templates/openwiki/memory.md +11 -0
  123. package/templates/openwiki/secrets.md +7 -0
  124. package/templates/openwiki/skills.md +7 -0
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  130. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  131. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  132. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  143. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  144. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  149. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  150. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  153. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  154. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -14,12 +14,13 @@ import express from 'express';
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { createMemoryNode, walkMd } from '../../core/vault.mjs';
17
- import { writeNodeValidated } from '../../core/validated-write.mjs';
17
+ import { writeNodeValidatedAsync } from '../../core/validated-write.mjs';
18
18
  import { getNodes, invalidate } from '../../core/vault-cache.mjs';
19
19
  import { requireAuth, requireScope } from '../auth.mjs';
20
20
  import { compileSurface } from '../../core/surface.mjs';
21
21
  import { buildEmbeddingsIndex, buildSessionEmbeddingsIndex } from '../../core/embeddings.mjs';
22
22
  import { detectAndResolve } from '../../core/conflict-detector.mjs';
23
+ import { listQueue, updateQueueItem } from '../../core/research-queue.mjs';
23
24
  import {
24
25
  BRAIN_DIR,
25
26
  VAULT_DIR,
@@ -32,6 +33,7 @@ import {
32
33
  serverError,
33
34
  sanitizeNode,
34
35
  resolveVaultFromQuery,
36
+ resolveAllVaultsFromQuery,
35
37
  } from './_shared.mjs';
36
38
 
37
39
  const router = express.Router();
@@ -109,10 +111,23 @@ const PASSTHROUGH_FIELDS = [
109
111
 
110
112
  router.get('/api/memory', requireAuth, requireScope('memory:read'), (req, res) => {
111
113
  try {
112
- const vaultDir = resolveVaultFromQuery(req);
113
- let list = nodes(vaultDir);
114
+ const vaultDirs = resolveAllVaultsFromQuery(req);
115
+ let list = [];
114
116
 
115
- const { q, category, tag, status, limit = '200', offset = '0' } = req.query;
117
+ // Track unique slugs so we don't count shadowing duplicates twice
118
+ const seenSlugs = new Set();
119
+
120
+ // Iterate backwards so most specific (last in list) is seen first
121
+ for (let i = vaultDirs.length - 1; i >= 0; i--) {
122
+ const vaultList = nodes(vaultDirs[i]);
123
+ for (const n of vaultList) {
124
+ if (!seenSlugs.has(n.slug)) {
125
+ seenSlugs.add(n.slug);
126
+ list.push(n);
127
+ }
128
+ }
129
+ }
130
+ const { q, category, tag, tags, status, limit = '200', offset = '0' } = req.query;
116
131
 
117
132
  if (q) {
118
133
  const query = String(q).toLowerCase();
@@ -123,7 +138,20 @@ router.get('/api/memory', requireAuth, requireScope('memory:read'), (req, res) =
123
138
  }
124
139
  if (category) list = list.filter(n => n.category === category);
125
140
  if (status) list = list.filter(n => n.status === status);
126
- if (tag) list = list.filter(n => (n.tags || []).includes(tag));
141
+ // Accept tag= or tags= (comma-separated). OpenWiki UI uses tags=openwiki.
142
+ const tagFilter = tag || tags;
143
+ if (tagFilter) {
144
+ const wanted = String(tagFilter)
145
+ .split(',')
146
+ .map((t) => t.trim().toLowerCase())
147
+ .filter(Boolean);
148
+ if (wanted.length) {
149
+ list = list.filter((n) => {
150
+ const nodeTags = (n.tags || []).map((t) => String(t).toLowerCase());
151
+ return wanted.every((t) => nodeTags.includes(t));
152
+ });
153
+ }
154
+ }
127
155
 
128
156
  const total = list.length;
129
157
  const off = Math.max(0, parseInt(offset, 10) || 0);
@@ -138,30 +166,87 @@ router.get('/api/memory', requireAuth, requireScope('memory:read'), (req, res) =
138
166
 
139
167
  router.get('/api/memory/stats', requireAuth, requireScope('memory:read'), (req, res) => {
140
168
  try {
141
- const vaultDir = resolveVaultFromQuery(req);
142
- const list = nodes(vaultDir);
169
+ const vaultDirs = resolveAllVaultsFromQuery(req);
143
170
  const byCategory = {};
144
- for (const n of list) {
145
- byCategory[n.category] = (byCategory[n.category] || 0) + 1;
171
+ let total = 0;
172
+
173
+ // Track unique slugs so we don't count shadowing duplicates twice
174
+ const seenSlugs = new Set();
175
+
176
+ // Iterate backwards so most specific (last in list) is seen first
177
+ for (let i = vaultDirs.length - 1; i >= 0; i--) {
178
+ const list = nodes(vaultDirs[i]);
179
+ for (const n of list) {
180
+ if (!seenSlugs.has(n.slug)) {
181
+ seenSlugs.add(n.slug);
182
+ byCategory[n.category] = (byCategory[n.category] || 0) + 1;
183
+ total++;
184
+ }
185
+ }
146
186
  }
147
- res.json({ total: list.length, by_category: byCategory });
187
+
188
+ res.json({ total, by_category: byCategory });
148
189
  } catch (err) {
149
190
  serverError(res, err);
150
191
  }
151
192
  });
152
193
 
194
+ /**
195
+ * Expand vault search: query brain(s) first, then global, then every registered
196
+ * project brain. Fixes dashboard 404s when the list came from a project vault
197
+ * but GET omitted ?brain=.
198
+ */
199
+ function vaultDirsForSlugLookup(req) {
200
+ const dirs = [...resolveAllVaultsFromQuery(req)];
201
+ const seen = new Set(dirs);
202
+
203
+ const push = (dir) => {
204
+ if (dir && !seen.has(dir) && fs.existsSync(dir)) {
205
+ seen.add(dir);
206
+ dirs.push(dir);
207
+ }
208
+ };
209
+
210
+ push(VAULT_DIR);
211
+
212
+ try {
213
+ const home = process.env.HOME || process.env.USERPROFILE || '';
214
+ const registryPath = path.join(home, '.agent', 'skills', 'total-recall', 'config', 'project-registry.json');
215
+ if (fs.existsSync(registryPath)) {
216
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
217
+ if (Array.isArray(registry)) {
218
+ for (const project of registry) {
219
+ if (project?.brainDir) {
220
+ push(path.join(project.brainDir, 'memory-vault'));
221
+ }
222
+ }
223
+ }
224
+ }
225
+ } catch {
226
+ // non-fatal
227
+ }
228
+
229
+ return dirs;
230
+ }
231
+
153
232
  router.get('/api/memory/:slug', requireAuth, requireScope('memory:read'), (req, res) => {
154
233
  try {
155
- const vaultDir = resolveVaultFromQuery(req);
156
- const node = nodes(vaultDir).find(n => n.slug === req.params.slug);
157
- if (!node) return notFound(res, `Memory node not found: ${req.params.slug}`);
158
- res.json(sanitizeNode(node));
234
+ const vaultDirs = vaultDirsForSlugLookup(req);
235
+
236
+ for (const vaultDir of vaultDirs) {
237
+ const node = nodes(vaultDir).find(n => n.slug === req.params.slug);
238
+ if (node) {
239
+ return res.json(sanitizeNode(node));
240
+ }
241
+ }
242
+
243
+ return notFound(res, `Memory node not found: ${req.params.slug}`);
159
244
  } catch (err) {
160
245
  serverError(res, err);
161
246
  }
162
247
  });
163
248
 
164
- router.post('/api/memory', requireAuth, requireScope('memory:write'), (req, res) => {
249
+ router.post('/api/memory', requireAuth, requireScope('memory:write'), async (req, res) => {
165
250
  try {
166
251
  const vaultDir = resolveVaultFromQuery(req);
167
252
  const { slug, title, category, content, body, tags } = req.body || {};
@@ -179,7 +264,7 @@ router.post('/api/memory', requireAuth, requireScope('memory:write'), (req, res)
179
264
  if (req.body[key] !== undefined) node[key] = req.body[key];
180
265
  }
181
266
 
182
- const vaultResult = writeNodeValidated(node, vaultDir);
267
+ const vaultResult = await writeNodeValidatedAsync(node, vaultDir);
183
268
  if (!vaultResult.success) {
184
269
  return res.status(422).json({
185
270
  error: 'Validation failed',
@@ -195,7 +280,7 @@ router.post('/api/memory', requireAuth, requireScope('memory:write'), (req, res)
195
280
  }
196
281
  });
197
282
 
198
- router.put('/api/memory/:slug', requireAuth, requireScope('memory:write'), (req, res) => {
283
+ router.put('/api/memory/:slug', requireAuth, requireScope('memory:write'), async (req, res) => {
199
284
  try {
200
285
  const vaultDir = resolveVaultFromQuery(req);
201
286
  const { title, category, content, body, tags } = req.body || {};
@@ -210,7 +295,7 @@ router.put('/api/memory/:slug', requireAuth, requireScope('memory:write'), (req,
210
295
  if (req.body[key] !== undefined) node[key] = req.body[key];
211
296
  }
212
297
 
213
- const vaultResult = writeNodeValidated(node, vaultDir);
298
+ const vaultResult = await writeNodeValidatedAsync(node, vaultDir);
214
299
  if (!vaultResult.success) {
215
300
  return res.status(422).json({
216
301
  error: 'Validation failed',
@@ -218,18 +303,28 @@ router.put('/api/memory/:slug', requireAuth, requireScope('memory:write'), (req,
218
303
  repair: vaultResult.repair,
219
304
  });
220
305
  }
221
- invalidate(vaultDir);
222
- triggerMutation(node, vaultDir);
306
+ invalidate(targetVaultDir);
307
+ triggerMutation(node, targetVaultDir);
223
308
  res.json(sanitizeNode(node));
224
309
  } catch (err) {
225
310
  serverError(res, err);
226
311
  }
227
312
  });
228
313
 
229
- router.patch('/api/memory/:slug', requireAuth, requireScope('memory:write'), (req, res) => {
314
+ router.patch('/api/memory/:slug', requireAuth, requireScope('memory:write'), async (req, res) => {
230
315
  try {
231
- const vaultDir = resolveVaultFromQuery(req);
232
- const existing = nodes(vaultDir).find(n => n.slug === req.params.slug);
316
+ const vaultDirs = resolveAllVaultsFromQuery(req);
317
+ let existing;
318
+ let targetVaultDir = vaultDirs[vaultDirs.length - 1]; // Default to most specific
319
+
320
+ for (const vaultDir of vaultDirs) {
321
+ existing = nodes(vaultDir).find(n => n.slug === req.params.slug);
322
+ if (existing) {
323
+ targetVaultDir = vaultDir;
324
+ break;
325
+ }
326
+ }
327
+
233
328
  if (!existing) return notFound(res, `Memory node not found: ${req.params.slug}`);
234
329
 
235
330
  const { title, category, content, body, tags } = req.body || {};
@@ -250,7 +345,7 @@ router.patch('/api/memory/:slug', requireAuth, requireScope('memory:write'), (re
250
345
  if (req.body[key] !== undefined) updated[key] = req.body[key];
251
346
  }
252
347
 
253
- const vaultResult = writeNodeValidated(updated, vaultDir);
348
+ const vaultResult = await writeNodeValidatedAsync(updated, targetVaultDir);
254
349
  if (!vaultResult.success) {
255
350
  return res.status(422).json({
256
351
  error: 'Validation failed',
@@ -258,8 +353,8 @@ router.patch('/api/memory/:slug', requireAuth, requireScope('memory:write'), (re
258
353
  repair: vaultResult.repair,
259
354
  });
260
355
  }
261
- invalidate(vaultDir);
262
- triggerMutation(updated, vaultDir);
356
+ invalidate(targetVaultDir);
357
+ triggerMutation(updated, targetVaultDir);
263
358
  res.json(sanitizeNode(updated));
264
359
  } catch (err) {
265
360
  serverError(res, err);
@@ -268,15 +363,24 @@ router.patch('/api/memory/:slug', requireAuth, requireScope('memory:write'), (re
268
363
 
269
364
  router.delete('/api/memory/:slug', requireAuth, requireScope('memory:write'), (req, res) => {
270
365
  try {
271
- const vaultDir = resolveVaultFromQuery(req);
272
- const list = nodes(vaultDir);
273
- const node = list.find(n => n.slug === req.params.slug);
366
+ const vaultDirs = resolveAllVaultsFromQuery(req);
367
+ let node;
368
+ let targetVaultDir;
369
+
370
+ for (const vaultDir of vaultDirs) {
371
+ node = nodes(vaultDir).find(n => n.slug === req.params.slug);
372
+ if (node) {
373
+ targetVaultDir = vaultDir;
374
+ break;
375
+ }
376
+ }
377
+
274
378
  if (!node) return notFound(res, `Memory node not found: ${req.params.slug}`);
275
379
 
276
380
  if (node._filePath && fs.existsSync(node._filePath)) {
277
381
  fs.unlinkSync(node._filePath);
278
382
  } else {
279
- for (const file of walkMd(vaultDir)) {
383
+ for (const file of walkMd(targetVaultDir)) {
280
384
  const raw = fs.readFileSync(file, 'utf8');
281
385
  if (raw.includes(`slug: ${req.params.slug}`)) {
282
386
  fs.unlinkSync(file);
@@ -284,8 +388,17 @@ router.delete('/api/memory/:slug', requireAuth, requireScope('memory:write'), (r
284
388
  }
285
389
  }
286
390
  }
287
- invalidate(vaultDir);
288
- triggerMutation(null, vaultDir);
391
+ invalidate(targetVaultDir);
392
+ triggerMutation(null, targetVaultDir);
393
+
394
+ // Keep interface and queue in sync: clear this node_slug from any research agenda items
395
+ const queue = listQueue({});
396
+ for (const item of queue.items) {
397
+ if (item.node_slug === req.params.slug) {
398
+ updateQueueItem(item.id, { node_slug: null });
399
+ }
400
+ }
401
+
289
402
  res.json({ deleted: true, slug: req.params.slug });
290
403
  } catch (err) {
291
404
  serverError(res, err);
@@ -53,3 +53,33 @@ researchRouter.delete('/api/research/:id', requireAuth, requireScope('memory:wri
53
53
  serverError(res, err);
54
54
  }
55
55
  });
56
+
57
+ // ─── Optional remote vault sync ───────────────────────────────────────────────
58
+
59
+ researchRouter.post('/api/sync/remote-vault/run', requireAuth, requireScope('memory:write'), async (req, res) => {
60
+ try {
61
+ const { runSync } = await import('../../core/remote-vault-sync.mjs');
62
+ runSync().catch((err) =>
63
+ logger.error('remote-vault-sync', 'Background sync failed', { error: err.message }),
64
+ );
65
+ res.json({ status: 'started' });
66
+ } catch (err) {
67
+ serverError(res, err);
68
+ }
69
+ });
70
+
71
+ researchRouter.get('/api/sync/remote-vault/status', requireAuth, requireScope('memory:read'), async (req, res) => {
72
+ try {
73
+ const fs = await import('node:fs');
74
+ const path = await import('node:path');
75
+ const { remoteVaultSync } = await import('../../core/config.mjs');
76
+ const statusFile = path.join(path.dirname(remoteVaultSync.vaultDir), 'sync-status.json');
77
+ if (!fs.existsSync(statusFile)) {
78
+ return res.json({ ok: false, error: 'Never run' });
79
+ }
80
+ const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
81
+ res.json(status);
82
+ } catch (err) {
83
+ serverError(res, err);
84
+ }
85
+ });