total-recall-brain 3.6.5 → 3.6.6

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.
@@ -10,6 +10,10 @@ const { share, search, healthCheck } = self.BrainClient;
10
10
  // Context menu registration
11
11
  // ---------------------------------------------------------------------------
12
12
  chrome.runtime.onInstalled.addListener(() => {
13
+ if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
14
+ chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error);
15
+ }
16
+
13
17
  chrome.contextMenus.create({
14
18
  id: 'send-to-brain',
15
19
  title: 'Send to Brain',
@@ -398,7 +398,15 @@
398
398
  toast.classList.add('visible');
399
399
  setTimeout(() => toast.classList.remove('visible'), 2500);
400
400
  } else {
401
- toast.textContent = '❌ Action failed';
401
+ let errText = '❌ Action failed';
402
+ if (res && res.error) {
403
+ if (res.error.includes('Authentication failed')) {
404
+ errText = '❌ Auth Error - Check PAT';
405
+ } else {
406
+ errText = `❌ ${res.error}`;
407
+ }
408
+ }
409
+ toast.textContent = errText;
402
410
  toast.style.background = '#e74c3c';
403
411
  toast.style.boxShadow = '0 4px 12px rgba(231, 76, 60, 0.3)';
404
412
  toast.classList.add('visible');
@@ -10,7 +10,6 @@
10
10
  "service_worker": "background.js"
11
11
  },
12
12
  "action": {
13
- "default_popup": "popup/popup.html",
14
13
  "default_icon": {
15
14
  "16": "icons/icon-16.png",
16
15
  "48": "icons/icon-48.png",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.6.5",
3
+ "version": "3.6.6",
4
4
  "description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,63 +38,81 @@ const RECOMPILE_EVERY_N_TASKS = 20;
38
38
  // Pause between task iterations (ms)
39
39
  const TASK_SLEEP_MS = 10000;
40
40
 
41
- // ─── Categories that require LLM (skipped in deterministic mode) ────────────────
42
-
43
- const LLM_REQUIRED_CATEGORIES = new Set([
44
- 'system2-deliberation',
45
- 'conscious-enforcement',
46
- 'cutoff-audit',
47
- ]);
48
-
49
- /**
50
- * Check if a task requires an LLM to execute.
51
- * These tasks are skipped — the daemon is deterministic.
52
- */
53
- function requiresLLM(task) {
54
- if (LLM_REQUIRED_CATEGORIES.has(task.category)) return true;
55
-
56
- const slug = task.slug;
57
- if (slug.startsWith('research-deliberation-')) return true;
58
- if (slug.startsWith('research-improvement-')) return true;
59
- if (slug.startsWith('research-monitoring-')) return true;
60
- if (slug.startsWith('research-expansion-')) return true;
61
- if (slug.includes('self-diagnosis')) return true;
62
- if (slug.includes('clarity-review')) return true;
63
- if (slug.includes('staleness-check')) return true;
64
- if (slug.includes('inference')) return true;
65
- if (slug.includes('post-mortem')) return true;
66
- if (slug.includes('compliance')) return true;
67
-
68
- return false;
69
- }
41
+ // ─── Tasks are no longer skipped to conserve resources. We use CLI agents! ───
70
42
 
71
43
  async function dispatchTask(task) {
72
- if (requiresLLM(task)) {
73
- logger.info({
74
- subsystem: 'daemon-loop',
75
- message: `Skipping task (requires LLM): ${task.slug} [${task.category}]`,
76
- });
77
- return { success: true, skippedLLM: true, output: 'Skipped — handled by IDE agents' };
78
- }
79
-
80
44
  const category = task.category;
81
45
 
82
46
  try {
83
- switch (category) {
84
- case 'memory-maintenance':
85
- return await runMaintenanceTask(task);
86
-
87
- case 'proactive-research':
88
- case 'research-acquisition':
89
- return await runResearchTask(task);
90
-
91
- default:
92
- logger.info({
93
- subsystem: 'daemon-loop',
94
- message: `Skipping task (not deterministic): ${task.slug} [${category}]`,
95
- });
96
- return { success: true, skippedLLM: true, output: 'Skipped — handled by IDE agents' };
47
+ const { loadRuntimeConfig } = await import('./runtime.mjs');
48
+ const runtimeConfig = await loadRuntimeConfig();
49
+
50
+ // ─── Research & Proactive Tasks (LLM + Fetching) ───
51
+ if (task.slug.startsWith('research-') || category === 'proactive-research' || category === 'research-acquisition') {
52
+ if (task.slug.startsWith('staleness-check-')) {
53
+ const { runStalenessCheck } = await import('./clarity-rewriter.mjs');
54
+ const result = await runStalenessCheck(task.target, { vaultDir: VAULT_DIR, queueDir: QUEUE_DIR, runtimeConfig });
55
+ return { success: true, output: `Staleness check complete: ${result.verdict} (conf: ${result.confidence})` };
56
+ }
57
+ return await runResearchTask(task, runtimeConfig);
58
+ }
59
+
60
+ // ─── System 2 Inference ───
61
+ if (category === 'system2-deliberation') {
62
+ const { runInferenceTask } = await import('./inference-engine.mjs');
63
+ const slugs = task.target ? task.target.split(',') : [];
64
+ const result = await runInferenceTask(slugs, {
65
+ vaultDir: VAULT_DIR,
66
+ inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'),
67
+ runtimeConfig
68
+ });
69
+ return { success: true, output: `Inference complete: ${result.conclusions?.length || 0} conclusions` };
70
+ }
71
+
72
+ // ─── Cutoff Audit ───
73
+ if (category === 'cutoff-audit' || task.slug.startsWith('cutoff-audit-')) {
74
+ const { runCutoffAudit } = await import('./clarity-rewriter.mjs');
75
+ const result = await runCutoffAudit({ vaultDir: VAULT_DIR, queueDir: QUEUE_DIR, runtimeConfig });
76
+ return { success: true, output: `Cutoff audit complete: ${result.audited} audited, ${result.flagged} flagged, ${result.critical} critical` };
77
+ }
78
+
79
+ // ─── Self Diagnosis & Conscious Enforcement ───
80
+ if (category === 'conscious-enforcement' || task.slug.includes('self-diagnosis')) {
81
+ if (task.slug.startsWith('post-mortem-')) {
82
+ const { runPostMortem } = await import('./post-mortem.mjs');
83
+ const sessionPath = path.join(brainDir, 'sessions', task.target);
84
+ const result = await runPostMortem(sessionPath, { vaultDir: VAULT_DIR, inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'), runtimeConfig });
85
+ return { success: true, output: `Post-mortem complete: ${result.nodesCreated || 0} nodes extracted` };
86
+ }
87
+
88
+ if (task.slug.includes('self-diagnosis')) {
89
+ const { runSelfDiagnosis } = await import('./fact-seeker.mjs');
90
+ await runSelfDiagnosis({ vaultDir: VAULT_DIR, runtimeConfig });
91
+ return { success: true, output: `Self-diagnosis complete` };
92
+ }
93
+ return { success: true, output: `Conscious enforcement handled` };
94
+ }
95
+
96
+ // ─── Memory Maintenance ───
97
+ if (category === 'memory-maintenance') {
98
+ if (task.slug.startsWith('clarity-review-')) {
99
+ const { runClarityReview } = await import('./clarity-rewriter.mjs');
100
+ const result = await runClarityReview(task.target, { vaultDir: VAULT_DIR, inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'), runtimeConfig });
101
+ return { success: true, output: `Clarity review complete: rewrote=${result.rewrote}` };
102
+ }
103
+ if (task.slug.startsWith('memory-compaction-')) {
104
+ const { runMemoryCompaction } = await import('./fact-seeker.mjs');
105
+ const result = await runMemoryCompaction({ vaultDir: VAULT_DIR, inboxDir: path.join(BRAIN_DIR, 'memory-inbox', 'pending'), runtimeConfig });
106
+ return { success: true, output: `Memory compaction complete. Consolidated nodes: ${result.consolidatedCount || 0}` };
107
+ }
108
+ return await runMaintenanceTask(task);
97
109
  }
110
+
111
+ logger.info({
112
+ subsystem: 'daemon-loop',
113
+ message: `Skipping unhandled task category: ${task.slug} [${category}]`,
114
+ });
115
+ return { success: true, output: 'Skipped unhandled task' };
98
116
  } catch (err) {
99
117
  logger.info({
100
118
  subsystem: 'daemon-loop',
@@ -106,10 +124,10 @@ async function dispatchTask(task) {
106
124
 
107
125
  // ─── Research Engine (Web Fetching Only — No LLM Synthesis) ─────────────────────
108
126
 
109
- async function runResearchTask(task) {
127
+ async function runResearchTask(task, runtimeConfig) {
110
128
  const inboxDir = path.join(BRAIN_DIR, 'memory-inbox', 'pending');
111
129
 
112
- // Knowledge acquisition — fetch from web, save raw results
130
+ // Phase 1: Knowledge acquisition
113
131
  if (task.slug.startsWith('research-acquisition-') || task.category === 'proactive-research' || task.slug.includes('fact-seeker') || task.slug.includes('knowledge-acquisition')) {
114
132
  const { runKnowledgeAcquisitionCycle } = await import('./fact-seeker.mjs');
115
133
  const forceTopic = task.target || null;
@@ -121,6 +139,7 @@ async function runResearchTask(task) {
121
139
  skillsDir: SKILLS_DIR,
122
140
  derivedDir: DERIVED_DIR,
123
141
  instructionsFile: INSTRUCTIONS_FILE,
142
+ runtimeConfig
124
143
  });
125
144
  if (result.skipped) return { success: true, output: `Knowledge acquisition: ${result.skipped}` };
126
145
  const surfaceNote = result.surfaced ? ' [SURFACED]' : ' [staged]';
@@ -131,8 +150,59 @@ async function runResearchTask(task) {
131
150
  };
132
151
  }
133
152
 
153
+ // Phase 2: Deliberation
154
+ if (task.slug.startsWith('research-deliberation-')) {
155
+ const { runResearchDeliberationCycle } = await import('./fact-seeker.mjs');
156
+ const result = await runResearchDeliberationCycle({
157
+ vaultDir: VAULT_DIR,
158
+ nodeSlug: task._node_slug || 'pending',
159
+ topic: task.title || task.target || 'Unknown Topic',
160
+ runtimeConfig
161
+ });
162
+ return { success: true, output: `Deliberation complete: ${result.slug || 'done'}` };
163
+ }
164
+
165
+ // Phase 3: Improvement
166
+ if (task.slug.startsWith('research-improvement-')) {
167
+ const { runResearchImprovementCycle } = await import('./fact-seeker.mjs');
168
+ const result = await runResearchImprovementCycle({
169
+ vaultDir: VAULT_DIR,
170
+ nodeSlug: task._node_slug || 'pending',
171
+ topic: task.title || task.target || 'Unknown Topic',
172
+ runtimeConfig
173
+ });
174
+ return { success: true, output: `Improvement complete` };
175
+ }
176
+
177
+ // Phase 4: Monitoring
178
+ if (task.slug.startsWith('research-monitoring-')) {
179
+ const { runResearchMonitoringCycle } = await import('./fact-seeker.mjs');
180
+ const result = await runResearchMonitoringCycle({
181
+ vaultDir: VAULT_DIR,
182
+ nodeSlug: task._node_slug || 'pending',
183
+ topic: task.title || task.target || 'Unknown Topic',
184
+ runtimeConfig,
185
+ skillsDir: SKILLS_DIR,
186
+ derivedDir: DERIVED_DIR,
187
+ instructionsFile: INSTRUCTIONS_FILE
188
+ });
189
+ return { success: true, output: `Monitoring complete` };
190
+ }
191
+
192
+ // Phase 5: Expansion
193
+ if (task.slug.startsWith('research-expansion-')) {
194
+ const { runResearchExpansionCycle } = await import('./fact-seeker.mjs');
195
+ const result = await runResearchExpansionCycle({
196
+ vaultDir: VAULT_DIR,
197
+ nodeSlug: task._node_slug || 'pending',
198
+ topic: task.title || task.target || 'Unknown Topic',
199
+ runtimeConfig
200
+ });
201
+ return { success: true, output: `Expansion complete` };
202
+ }
203
+
134
204
  // Conclusion writer — validate pending drafts (deterministic checks)
135
- if (task.slug.includes('validate') || task.category === 'research-acquisition') {
205
+ if (task.slug.includes('validate')) {
136
206
  const { runConclusionWriter } = await import('./conclusion-writer.mjs');
137
207
  const result = await runConclusionWriter({
138
208
  inboxDir,
@@ -313,8 +383,18 @@ async function main() {
313
383
  try {
314
384
  if (result.skippedLLM) {
315
385
  updateTaskStatus(task, 'pending', QUEUE_DIR);
386
+ } else if (result.success) {
387
+ updateTaskStatus(task, 'completed', QUEUE_DIR);
316
388
  } else {
317
- updateTaskStatus(task, result.success ? 'completed' : 'failed', QUEUE_DIR);
389
+ // Dead Letter Queue / Retry Logic
390
+ task.retry_count = (task.retry_count || 0) + 1;
391
+ if (task.retry_count >= 3) {
392
+ logger.info({ subsystem: 'daemon-loop', message: `Task ${task.slug} failed 3 times. Moving to DLQ (failed).` });
393
+ updateTaskStatus(task, 'failed', QUEUE_DIR);
394
+ } else {
395
+ logger.info({ subsystem: 'daemon-loop', message: `Task ${task.slug} failed. Retry ${task.retry_count}/3. Requeueing.` });
396
+ updateTaskStatus(task, 'pending', QUEUE_DIR);
397
+ }
318
398
  }
319
399
  } catch (statusErr) {
320
400
  logger.info({
@@ -334,15 +414,26 @@ async function main() {
334
414
  patch.node_slug = result.factSlug;
335
415
  }
336
416
  // If the task was skipped because it requires an LLM, DO NOT complete the research project.
337
- // Wait for an IDE agent to run it.
338
417
  if (result.skippedLLM) {
339
418
  patch.status = 'pending';
340
- } else if (result.success && task._research_phase === 'acquisition' && result.factSlug) {
341
- // Acquisition is done, advance to deliberation phase for LLM processing!
342
- patch.status = 'pending';
343
- patch.research_phase = 'deliberation';
344
- // Also notify the user/UI that phase 1 is complete!
345
- patch.completed_at = new Date().toISOString();
419
+ } else if (result.success && result.factSlug) {
420
+ if (task._research_phase === 'acquisition') {
421
+ patch.status = 'pending';
422
+ patch.research_phase = 'deliberation';
423
+ patch.completed_at = new Date().toISOString();
424
+ } else if (task._research_phase === 'deliberation') {
425
+ patch.status = 'pending';
426
+ patch.research_phase = 'improvement';
427
+ } else if (task._research_phase === 'improvement') {
428
+ patch.status = 'pending';
429
+ patch.research_phase = 'monitoring';
430
+ } else if (task._research_phase === 'monitoring') {
431
+ patch.status = 'pending';
432
+ patch.research_phase = 'expansion';
433
+ } else if (task._research_phase === 'expansion') {
434
+ patch.status = 'done';
435
+ patch.completed_at = new Date().toISOString();
436
+ }
346
437
  }
347
438
  updateQueueItem(task._research_id, patch);
348
439
  } catch (err) {
@@ -529,7 +529,8 @@ export function buildMultiNoteGraph(topic, synthesis, parsedSupportingNotes, sta
529
529
  return { slug, body: bodyLines.join('\n'), frontmatter };
530
530
  });
531
531
 
532
- const masterSlug = `fact-master-${crypto.randomBytes(5).toString('hex')}`;
532
+ const topicHash = crypto.createHash('md5').update(topic).digest('hex').slice(0, 10);
533
+ const masterSlug = `fact-master-${topicHash}`;
533
534
 
534
535
  // Bidirectionally link the master node and supporting notes
535
536
  const masterRelated = supportingNodes.map(n => n.slug);
@@ -658,7 +659,8 @@ async function writeAndSurfaceImmediately(topic, synthesis, sourceResults, {
658
659
  const topResults = sourceResults.slice(0, 4);
659
660
  for (const r of topResults) {
660
661
  const summary = await synthesizeSupportingNote(topic, r, runtimeConfig);
661
- const slug = `fact-note-${crypto.randomBytes(5).toString('hex')}`;
662
+ const noteHash = crypto.createHash('md5').update(r.url || r.title || r.snippet).digest('hex').slice(0, 10);
663
+ const slug = `fact-note-${noteHash}`;
662
664
  parsedSupportingNotes.push({ result: r, summary, slug });
663
665
  }
664
666
 
@@ -700,7 +702,8 @@ async function writeDraftToInbox(topic, synthesis, sourceResults, inboxDir, runt
700
702
  const topResults = sourceResults.slice(0, 4);
701
703
  for (const r of topResults) {
702
704
  const summary = await synthesizeSupportingNote(topic, r, runtimeConfig);
703
- const slug = `fact-note-${crypto.randomBytes(5).toString('hex')}`;
705
+ const noteHash = crypto.createHash('md5').update(r.url || r.title || r.snippet).digest('hex').slice(0, 10);
706
+ const slug = `fact-note-${noteHash}`;
704
707
  parsedSupportingNotes.push({ result: r, summary, slug });
705
708
  }
706
709
 
@@ -325,6 +325,14 @@ export function listQueue({ status, query, limit = 100, offset = 0, brainDir: ov
325
325
  */
326
326
  export function addToQueue({ topic, priority = 'medium', notes, brainDir: overrideBrainDir } = {}) {
327
327
  if (!topic) throw new Error('topic is required');
328
+
329
+ const items = loadQueue(overrideBrainDir);
330
+ const normalizedTopic = String(topic).trim().toLowerCase();
331
+ const existing = items.find(i => i.topic.trim().toLowerCase() === normalizedTopic);
332
+ if (existing) {
333
+ return existing;
334
+ }
335
+
328
336
  const item = {
329
337
  id: crypto.randomUUID(),
330
338
  topic: String(topic),
@@ -338,7 +346,6 @@ export function addToQueue({ topic, priority = 'medium', notes, brainDir: overri
338
346
  completed_at: null,
339
347
  };
340
348
  item.summary = compileResearchProjectSummary(item, null);
341
- const items = loadQueue(overrideBrainDir);
342
349
  items.unshift(item);
343
350
  saveQueue(items, overrideBrainDir);
344
351
 
@@ -210,6 +210,7 @@ export function generateIdleTask({ vaultDir, sessionsDir }) {
210
210
  () => generateInferenceTask(vaultDir),
211
211
  () => generatePostMortemTask(sessionsDir),
212
212
  () => generateClarityReviewTask(vaultDir),
213
+ () => generateMemoryCompactionTask(vaultDir),
213
214
  ];
214
215
 
215
216
  const strategy = cleanStrategies[_idleCycleCounter % cleanStrategies.length];
@@ -248,18 +249,34 @@ function generateClarityReviewTask(vaultDir) {
248
249
  if (nodes.length === 0) {
249
250
  return makeFallbackTask('memory-maintenance', 'No active nodes to review');
250
251
  }
251
-
252
- const target = nodes[crypto.randomInt(0, nodes.length)];
252
+ const target = nodes[Math.floor(Math.random() * nodes.length)];
253
253
  return {
254
254
  type: 'task',
255
- slug: `clarity-review-${target.slug}-${Date.now().toString(36)}`,
255
+ slug: `clarity-review-${crypto.randomBytes(4).toString('hex')}`,
256
256
  priority: 30,
257
257
  category: 'memory-maintenance',
258
258
  target: target.slug,
259
259
  status: 'pending',
260
260
  created_by: 'scheduler-idle',
261
- reason: `Idle task: review "${target.title}" for clarity and actionability.`,
262
- body: `## Objective\nReview memory node "${target.slug}" and evaluate:\n1. Is the title descriptive enough for a 7-item skill injection?\n2. Is the body actionable by an AI agent?\n3. Does the frontmatter accurately reflect the content?\n\nIf improvements are needed, generate a rewrite proposal.`,
261
+ reason: `Idle task: selected active node ${target.slug} for clarity review.`,
262
+ body: `Analyze node ${target.slug} for clarity, deduplication, formatting, and structural integrity. Rewrite via SSSS strict compliance if needed.`,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Generate a memory compaction task to fuse highly overlapping fragmented nodes.
268
+ */
269
+ function generateMemoryCompactionTask(vaultDir) {
270
+ return {
271
+ type: 'task',
272
+ slug: `memory-compaction-${crypto.randomBytes(4).toString('hex')}`,
273
+ priority: 35,
274
+ category: 'memory-maintenance',
275
+ target: 'global',
276
+ status: 'pending',
277
+ created_by: 'scheduler-idle',
278
+ reason: `Idle task: scan vault for fragmented nodes to merge into comprehensive master documents.`,
279
+ body: `Scan for highly overlapping or fragmented memory nodes and fuse them into comprehensive master nodes, archiving the fragments.`,
263
280
  };
264
281
  }
265
282
 
@@ -523,6 +540,7 @@ export function createScheduler({ queueDir, vaultDir, sessionsDir }) {
523
540
  body,
524
541
  _research_id: item.id,
525
542
  _research_phase: phase,
543
+ _node_slug: item.node_slug,
526
544
  });
527
545
  }
528
546
  if (researchItems.length > 0) {
@@ -644,6 +644,28 @@ export const PhoneNumberSchema = z.object({
644
644
  user_id: z.string().optional().nullable(),
645
645
  });
646
646
 
647
+ export const DomainSchema = z.object({
648
+ type: z.literal('domain'),
649
+ domain_name: z.string(),
650
+ registrar: z.string(),
651
+ status: z.string(),
652
+ payment_status: z.string(),
653
+ workspace_id: z.string().optional().nullable(),
654
+ user_id: z.string().optional().nullable(),
655
+ base_price_usd: z.number().optional().nullable(),
656
+ purchase_price_usd: z.number().optional().nullable(),
657
+ annual_renewal_usd: z.number().optional().nullable(),
658
+ markup_amount_usd: z.number().optional().nullable(),
659
+ markup_rate: z.number().optional().nullable(),
660
+ expires_at: z.string().optional().nullable(),
661
+ payment_method: z.string().optional().nullable(),
662
+ vercel_domain_id: z.string().optional().nullable(),
663
+ stripe_payment_intent_id: z.string().optional().nullable(),
664
+ stripe_checkout_session_id: z.string().optional().nullable(),
665
+ credits_charged: z.number().optional().nullable(),
666
+ credit_transaction_id: z.string().optional().nullable(),
667
+ });
668
+
647
669
  export const WorkspaceSchema = z.object({
648
670
  type: z.literal('workspace'),
649
671
  name: z.string(),
@@ -804,6 +826,7 @@ export const SSSS_SCHEMAS = {
804
826
  workspace_transfer: WorkspaceTransferSchema,
805
827
  extension: ExtensionSchema,
806
828
  phone_number: PhoneNumberSchema,
829
+ domain: DomainSchema,
807
830
  workspace: WorkspaceSchema,
808
831
  commerce_catalog: CommerceCatalogSchema,
809
832
  product: ProductSchema,
@@ -83,6 +83,10 @@ import { memoryRouter } from './routes/memory.mjs';
83
83
  import { keysRouter } from './routes/keys.mjs';
84
84
  import { sessionsRouter } from './routes/sessions.mjs';
85
85
  import { shareRouter } from './routes/share.mjs';
86
+ import { authRouter } from './routes/auth.mjs';
87
+ import { sandboxRouter } from './routes/sandbox.mjs';
88
+ import { researchRouter } from './routes/research.mjs';
89
+ import { skillsRouter } from './routes/skills.mjs';
86
90
  // ollamaUrl removed — CLI agents replace Ollama
87
91
  import {
88
92
  AGENT_DIR,
@@ -159,6 +163,10 @@ router.use(memoryRouter);
159
163
  router.use(keysRouter);
160
164
  router.use(sessionsRouter);
161
165
  router.use(shareRouter);
166
+ router.use(authRouter);
167
+ router.use(sandboxRouter);
168
+ router.use(researchRouter);
169
+ router.use(skillsRouter);
162
170
 
163
171
  // ─── Helpers ──────────────────────────────────────────────────────────────────
164
172
 
@@ -490,36 +498,7 @@ router.post('/api/diagnostics/agents', requireAuth, requireScope('health:read'),
490
498
  // ─── Keys ─────────── (moved to ./routes/keys.mjs)
491
499
  // ─── Sessions ─────── (moved to ./routes/sessions.mjs)
492
500
  // ─── Sandbox ──────────────────────────────────────────────────────────────────
493
-
494
- /**
495
- * POST /api/sandbox
496
- * Body: { code }
497
- */
498
- // Sandbox is rate-limited *before* requireAuth on purpose — limiter keys on
499
- // the authenticated principal when present, so it's IP-bucketed for
500
- // pre-auth misuse and key-bucketed once a PAT is attached.
501
- router.post('/api/sandbox', sandboxRateLimiter(), requireAuth, requireScope('sandbox:run'), requireSandboxEnabled, async (req, res) => {
502
- try {
503
- const { code } = req.body || {};
504
- if (!code) return badRequest(res, 'code is required');
505
-
506
- const tmpDir = path.join(os.tmpdir(), 'total-recall-sandbox');
507
- fs.mkdirSync(tmpDir, { recursive: true });
508
- const script = path.join(tmpDir, `rest-${Date.now()}.mjs`);
509
- fs.writeFileSync(script, code);
510
-
511
- const result = await runInSandbox(script, 15000);
512
- try { fs.unlinkSync(script); } catch {}
513
-
514
- res.json({
515
- success: result.success,
516
- exit_code: result.code,
517
- output: result.output,
518
- });
519
- } catch (err) {
520
- serverError(res, err);
521
- }
522
- });
501
+ // Moved to routes/sandbox.mjs
523
502
 
524
503
  // ─── Config ───────────────────────────────────────────────────────────────────
525
504
 
@@ -707,41 +686,7 @@ router.get('/api', (req, res) => {
707
686
  });
708
687
 
709
688
  // ─── Research Queue ───────────────────────────────────────────────────────────
710
- // Thin REST wrappers over src/core/research-queue.mjs
711
-
712
- router.get('/api/research', requireAuth, requireScope('memory:read'), (req, res) => {
713
- try {
714
- const { status, query, limit, offset } = req.query;
715
- res.json(listQueue({ status, query, limit, offset }));
716
- } catch (err) { serverError(res, err); }
717
- });
718
-
719
- router.post('/api/research', requireAuth, requireScope('memory:write'), (req, res) => {
720
- try {
721
- const { topic, priority, notes } = req.body || {};
722
- if (!topic) return badRequest(res, 'topic is required');
723
- res.status(201).json(addToQueue({ topic, priority, notes }));
724
- } catch (err) { serverError(res, err); }
725
- });
726
-
727
- router.patch('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
728
- try {
729
- res.json(updateQueueItem(req.params.id, req.body || {}));
730
- } catch (err) {
731
- if (err.status === 404) return notFound(res, err.message);
732
- serverError(res, err);
733
- }
734
- });
735
-
736
- router.delete('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
737
- try {
738
- res.json(removeFromQueue(req.params.id));
739
- } catch (err) {
740
- if (err.status === 404) return notFound(res, err.message);
741
- serverError(res, err);
742
- }
743
- });
744
-
689
+ // Moved to routes/research.mjs
745
690
  // ─── Active Integrations ──────────────────────────────────────────────────────
746
691
 
747
692
  /**
@@ -1098,110 +1043,7 @@ router.get('/api/files', requireAuth, requireScope('files:read'), (req, res) =>
1098
1043
  } catch (err) { serverError(res, err); }
1099
1044
  });
1100
1045
 
1101
- router.get('/api/skills', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
1102
- try {
1103
- if (!fs.existsSync(SKILLS_DIR)) {
1104
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
1105
- }
1106
- const skills = fs.readdirSync(SKILLS_DIR).map(dir => {
1107
- const dirPath = path.join(SKILLS_DIR, dir);
1108
- const stats = fs.statSync(dirPath);
1109
-
1110
- let subSkills = [];
1111
- const subSkillsPath = path.join(dirPath, 'skills');
1112
- if (fs.existsSync(subSkillsPath) && fs.statSync(subSkillsPath).isDirectory()) {
1113
- try {
1114
- subSkills = fs.readdirSync(subSkillsPath).filter(sd =>
1115
- fs.statSync(path.join(subSkillsPath, sd)).isDirectory()
1116
- );
1117
- } catch (e) {}
1118
- }
1119
-
1120
- return {
1121
- name: dir,
1122
- size: stats.size,
1123
- modified: stats.mtime,
1124
- isDirectory: stats.isDirectory(),
1125
- subSkills
1126
- };
1127
- });
1128
- res.json(skills);
1129
- } catch (err) { serverError(res, err); }
1130
- });
1131
-
1132
- router.get('/api/skills/search', requireAuth, requireScope('files:read', 'ssss:read'), async (req, res) => {
1133
- try {
1134
- const q = req.query.q || '';
1135
- if (!q) {
1136
- return res.status(400).json({ error: 'Missing search query parameter `q`.' });
1137
- }
1138
- const { searchAndSort } = await import('../../.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs');
1139
- const results = searchAndSort(q);
1140
- res.json(results);
1141
- } catch (err) { serverError(res, err); }
1142
- });
1143
-
1144
- router.post('/api/skills/install', requireAuth, requireScope('files:write', 'ssss:write'), async (req, res) => {
1145
- try {
1146
- const { pkg } = req.body;
1147
- if (!pkg) {
1148
- return res.status(400).json({ error: 'Missing required `pkg` body parameter.' });
1149
- }
1150
- const { installSkill } = await import('../../.agent/skills/total-recall/skills/skill/scripts/install-skill.mjs');
1151
- const result = installSkill(pkg);
1152
- res.json(result);
1153
- } catch (err) { serverError(res, err); }
1154
- });
1155
-
1156
- router.get('/api/skills/:name', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
1157
- try {
1158
- const { name } = req.params;
1159
- if (name.includes('..') || name.includes('/') || name.includes('\\')) {
1160
- return res.status(400).json({ error: 'Invalid skill name' });
1161
- }
1162
- const skillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
1163
- if (!fs.existsSync(skillPath)) {
1164
- return res.status(404).json({ error: `Skill "${name}" not found` });
1165
- }
1166
- const content = fs.readFileSync(skillPath, 'utf8');
1167
- res.json({ name, content });
1168
- } catch (err) { serverError(res, err); }
1169
- });
1170
-
1171
- router.put('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
1172
- try {
1173
- const { name } = req.params;
1174
- const { content } = req.body;
1175
- if (name.includes('..') || name.includes('/') || name.includes('\\')) {
1176
- return res.status(400).json({ error: 'Invalid skill name' });
1177
- }
1178
- if (typeof content !== 'string') {
1179
- return res.status(400).json({ error: 'Missing or invalid `content` field.' });
1180
- }
1181
- const skillDir = path.join(SKILLS_DIR, name);
1182
- if (!fs.existsSync(skillDir)) {
1183
- fs.mkdirSync(skillDir, { recursive: true });
1184
- }
1185
- const skillPath = path.join(skillDir, 'SKILL.md');
1186
- fs.writeFileSync(skillPath, content, 'utf8');
1187
- res.json({ success: true, message: `Skill "${name}" updated successfully` });
1188
- } catch (err) { serverError(res, err); }
1189
- });
1190
-
1191
- router.delete('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
1192
- try {
1193
- const { name } = req.params;
1194
- if (name.includes('..') || name.includes('/') || name.includes('\\')) {
1195
- return res.status(400).json({ error: 'Invalid skill name' });
1196
- }
1197
- const skillDir = path.join(SKILLS_DIR, name);
1198
- if (!fs.existsSync(skillDir)) {
1199
- return res.status(404).json({ error: `Skill "${name}" not found` });
1200
- }
1201
- fs.rmSync(skillDir, { recursive: true, force: true });
1202
- res.json({ success: true, message: `Skill "${name}" deleted successfully` });
1203
- } catch (err) { serverError(res, err); }
1204
- });
1046
+ // Moved to routes/skills.mjs
1205
1047
 
1206
1048
  // ─── Scripts Editor & Execution ───────────────────────────────────────────────
1207
1049
 
@@ -1345,6 +1187,12 @@ router.get('/api/tasks', requireAuth, requireScope('tasks:read'), (req, res) =>
1345
1187
  try {
1346
1188
  const raw = fs.readFileSync(path.join(TASKS_DIR, file), 'utf8');
1347
1189
  const { data, content } = matter(raw);
1190
+
1191
+ // By default, filter out completed tasks
1192
+ if (req.query.status !== 'all' && data.status === 'completed') {
1193
+ continue;
1194
+ }
1195
+
1348
1196
  tasks.push({ ...data, body: content.trim(), slug: file.replace('.md', '') });
1349
1197
  } catch (e) {
1350
1198
  // skip
@@ -1354,6 +1202,31 @@ router.get('/api/tasks', requireAuth, requireScope('tasks:read'), (req, res) =>
1354
1202
  } catch (err) { serverError(res, err); }
1355
1203
  });
1356
1204
 
1205
+ router.delete('/api/tasks/cleanup', requireAuth, requireScope('tasks:write'), (req, res) => {
1206
+ try {
1207
+ if (!fs.existsSync(TASKS_DIR)) {
1208
+ return res.json({ deleted: 0 });
1209
+ }
1210
+ let deleted = 0;
1211
+ const files = fs.readdirSync(TASKS_DIR);
1212
+ for (const file of files) {
1213
+ if (!file.endsWith('.md')) continue;
1214
+ const filePath = path.join(TASKS_DIR, file);
1215
+ try {
1216
+ const raw = fs.readFileSync(filePath, 'utf8');
1217
+ const { data } = matter(raw);
1218
+ if (data.status === 'completed') {
1219
+ fs.unlinkSync(filePath);
1220
+ deleted++;
1221
+ }
1222
+ } catch (e) {
1223
+ // skip
1224
+ }
1225
+ }
1226
+ res.json({ deleted });
1227
+ } catch (err) { serverError(res, err); }
1228
+ });
1229
+
1357
1230
  router.post('/api/tasks', requireAuth, requireScope('tasks:write'), (req, res) => {
1358
1231
  try {
1359
1232
  const { category, target, body, priority = 5 } = req.body || {};
@@ -1559,26 +1432,7 @@ router.post('/api/capture/:source', requireAuth, requireScope('memory:write'), a
1559
1432
  });
1560
1433
 
1561
1434
  // ─── Consolidated Auth Routes ──────────────────────────────────────────────────
1562
-
1563
- router.post('/auth/login', loginHandler);
1564
- router.post('/auth/logout', logoutHandler);
1565
- router.post('/auth/change-password', requireAuth, changePasswordHandler);
1566
- router.get('/auth/me', requireAuth, (req, res) => res.json({ authenticated: true }));
1567
-
1568
- router.get("/auth/status", (req, res) => {
1569
- const config = loadSecurityConfig();
1570
- const configured = !!config.dashboard?.password_hash;
1571
- res.json({ configured });
1572
- });
1573
-
1574
- router.post("/auth/setup", async (req, res) => {
1575
- const config = loadSecurityConfig();
1576
- const alreadyConfigured = !!config.dashboard?.password_hash;
1577
- if (alreadyConfigured) {
1578
- return res.status(400).json({ error: "Dashboard password is already configured" });
1579
- }
1580
- return changePasswordHandler(req, res);
1581
- });
1435
+ // Moved to routes/auth.mjs
1582
1436
 
1583
1437
  // ─── Voice / TTS (Kokoro / System) ───────────────────────────────────────────────
1584
1438
 
@@ -295,6 +295,10 @@
295
295
  "method": "POST",
296
296
  "path": "/api/tasks"
297
297
  },
298
+ {
299
+ "method": "DELETE",
300
+ "path": "/api/tasks/cleanup"
301
+ },
298
302
  {
299
303
  "method": "POST",
300
304
  "path": "/api/tts"
@@ -0,0 +1,32 @@
1
+ import express from 'express';
2
+ import {
3
+ requireAuth,
4
+ loadSecurityConfig,
5
+ loginHandler,
6
+ logoutHandler,
7
+ changePasswordHandler,
8
+ } from '../auth.mjs';
9
+
10
+ export const authRouter = express.Router();
11
+
12
+ // ─── Consolidated Auth Routes ──────────────────────────────────────────────────
13
+
14
+ authRouter.post('/auth/login', loginHandler);
15
+ authRouter.post('/auth/logout', logoutHandler);
16
+ authRouter.post('/auth/change-password', requireAuth, changePasswordHandler);
17
+ authRouter.get('/auth/me', requireAuth, (req, res) => res.json({ authenticated: true }));
18
+
19
+ authRouter.get("/auth/status", (req, res) => {
20
+ const config = loadSecurityConfig();
21
+ const configured = !!config.dashboard?.password_hash;
22
+ res.json({ configured });
23
+ });
24
+
25
+ authRouter.post("/auth/setup", async (req, res) => {
26
+ const config = loadSecurityConfig();
27
+ const alreadyConfigured = !!config.dashboard?.password_hash;
28
+ if (alreadyConfigured) {
29
+ return res.status(400).json({ error: "Dashboard password is already configured" });
30
+ }
31
+ return changePasswordHandler(req, res);
32
+ });
@@ -0,0 +1,55 @@
1
+ import express from 'express';
2
+ import { requireAuth, requireScope } from '../auth.mjs';
3
+ import { listQueue, addToQueue, updateQueueItem, removeFromQueue } from '../../core/research-queue.mjs';
4
+ import { logger } from '../../core/logger.mjs';
5
+
6
+ function badRequest(res, msg) {
7
+ return res.status(400).json({ error: msg });
8
+ }
9
+
10
+ function notFound(res, msg) {
11
+ return res.status(404).json({ error: msg || 'Not found' });
12
+ }
13
+
14
+ function serverError(res, err) {
15
+ logger.error('research', 'Internal server error', { error: err.message, stack: err.stack });
16
+ return res.status(500).json({ error: 'Internal server error' });
17
+ }
18
+
19
+ export const researchRouter = express.Router();
20
+
21
+ // ─── Research Queue ───────────────────────────────────────────────────────────
22
+ // Thin REST wrappers over src/core/research-queue.mjs
23
+
24
+ researchRouter.get('/api/research', requireAuth, requireScope('memory:read'), (req, res) => {
25
+ try {
26
+ const { status, query, limit, offset } = req.query;
27
+ res.json(listQueue({ status, query, limit, offset }));
28
+ } catch (err) { serverError(res, err); }
29
+ });
30
+
31
+ researchRouter.post('/api/research', requireAuth, requireScope('memory:write'), (req, res) => {
32
+ try {
33
+ const { topic, priority, notes } = req.body || {};
34
+ if (!topic) return badRequest(res, 'topic is required');
35
+ res.status(201).json(addToQueue({ topic, priority, notes }));
36
+ } catch (err) { serverError(res, err); }
37
+ });
38
+
39
+ researchRouter.patch('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
40
+ try {
41
+ res.json(updateQueueItem(req.params.id, req.body || {}));
42
+ } catch (err) {
43
+ if (err.status === 404) return notFound(res, err.message);
44
+ serverError(res, err);
45
+ }
46
+ });
47
+
48
+ researchRouter.delete('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
49
+ try {
50
+ res.json(removeFromQueue(req.params.id));
51
+ } catch (err) {
52
+ if (err.status === 404) return notFound(res, err.message);
53
+ serverError(res, err);
54
+ }
55
+ });
@@ -0,0 +1,55 @@
1
+ import express from 'express';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { runInSandbox } from '../../core/sandbox.mjs';
6
+ import {
7
+ requireAuth,
8
+ requireScope,
9
+ sandboxRateLimiter,
10
+ requireSandboxEnabled,
11
+ } from '../auth.mjs';
12
+ import { logger } from '../../core/logger.mjs';
13
+
14
+ function badRequest(res, msg) {
15
+ return res.status(400).json({ error: msg });
16
+ }
17
+
18
+ function serverError(res, err) {
19
+ logger.error('sandbox', 'Internal server error', { error: err.message, stack: err.stack });
20
+ return res.status(500).json({ error: 'Internal server error' });
21
+ }
22
+
23
+ export const sandboxRouter = express.Router();
24
+
25
+ // ─── Sandbox ──────────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * POST /api/sandbox
29
+ * Body: { code }
30
+ */
31
+ // Sandbox is rate-limited *before* requireAuth on purpose — limiter keys on
32
+ // the authenticated principal when present, so it's IP-bucketed for
33
+ // pre-auth misuse and key-bucketed once a PAT is attached.
34
+ sandboxRouter.post('/api/sandbox', sandboxRateLimiter(), requireAuth, requireScope('sandbox:run'), requireSandboxEnabled, async (req, res) => {
35
+ try {
36
+ const { code } = req.body || {};
37
+ if (!code) return badRequest(res, 'code is required');
38
+
39
+ const tmpDir = path.join(os.tmpdir(), 'total-recall-sandbox');
40
+ fs.mkdirSync(tmpDir, { recursive: true });
41
+ const script = path.join(tmpDir, `rest-${Date.now()}.mjs`);
42
+ fs.writeFileSync(script, code);
43
+
44
+ const result = await runInSandbox(script, 15000);
45
+ try { fs.unlinkSync(script); } catch {}
46
+
47
+ res.json({
48
+ success: result.success,
49
+ exit_code: result.code,
50
+ output: result.output,
51
+ });
52
+ } catch (err) {
53
+ serverError(res, err);
54
+ }
55
+ });
@@ -0,0 +1,118 @@
1
+ import express from 'express';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { requireAuth, requireScope } from '../auth.mjs';
5
+ import { logger } from '../../core/logger.mjs';
6
+ import { SKILLS_DIR } from './_shared.mjs';
7
+
8
+ function serverError(res, err) {
9
+ logger.error('skills', 'Internal server error', { error: err.message, stack: err.stack });
10
+ return res.status(500).json({ error: 'Internal server error' });
11
+ }
12
+
13
+ export const skillsRouter = express.Router();
14
+
15
+ skillsRouter.get('/api/skills', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
16
+ try {
17
+ if (!fs.existsSync(SKILLS_DIR)) {
18
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
19
+ }
20
+ const skills = fs.readdirSync(SKILLS_DIR).map(dir => {
21
+ const dirPath = path.join(SKILLS_DIR, dir);
22
+ const stats = fs.statSync(dirPath);
23
+
24
+ let subSkills = [];
25
+ const subSkillsPath = path.join(dirPath, 'skills');
26
+ if (fs.existsSync(subSkillsPath) && fs.statSync(subSkillsPath).isDirectory()) {
27
+ try {
28
+ subSkills = fs.readdirSync(subSkillsPath).filter(sd =>
29
+ fs.statSync(path.join(subSkillsPath, sd)).isDirectory()
30
+ );
31
+ } catch (e) {}
32
+ }
33
+
34
+ return {
35
+ name: dir,
36
+ size: stats.size,
37
+ modified: stats.mtime,
38
+ isDirectory: stats.isDirectory(),
39
+ subSkills
40
+ };
41
+ });
42
+ res.json(skills);
43
+ } catch (err) { serverError(res, err); }
44
+ });
45
+
46
+ skillsRouter.get('/api/skills/search', requireAuth, requireScope('files:read', 'ssss:read'), async (req, res) => {
47
+ try {
48
+ const q = req.query.q || '';
49
+ if (!q) {
50
+ return res.status(400).json({ error: 'Missing search query parameter `q`.' });
51
+ }
52
+ const { searchAndSort } = await import('../../../.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs');
53
+ const results = searchAndSort(q);
54
+ res.json(results);
55
+ } catch (err) { serverError(res, err); }
56
+ });
57
+
58
+ skillsRouter.post('/api/skills/install', requireAuth, requireScope('files:write', 'ssss:write'), async (req, res) => {
59
+ try {
60
+ const { pkg } = req.body;
61
+ if (!pkg) {
62
+ return res.status(400).json({ error: 'Missing required `pkg` body parameter.' });
63
+ }
64
+ const { installSkill } = await import('../../../.agent/skills/total-recall/skills/skill/scripts/install-skill.mjs');
65
+ const result = installSkill(pkg);
66
+ res.json(result);
67
+ } catch (err) { serverError(res, err); }
68
+ });
69
+
70
+ skillsRouter.get('/api/skills/:name', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
71
+ try {
72
+ const { name } = req.params;
73
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) {
74
+ return res.status(400).json({ error: 'Invalid skill name' });
75
+ }
76
+ const skillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
77
+ if (!fs.existsSync(skillPath)) {
78
+ return res.status(404).json({ error: `Skill "${name}" not found` });
79
+ }
80
+ const content = fs.readFileSync(skillPath, 'utf8');
81
+ res.json({ name, content });
82
+ } catch (err) { serverError(res, err); }
83
+ });
84
+
85
+ skillsRouter.put('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
86
+ try {
87
+ const { name } = req.params;
88
+ const { content } = req.body;
89
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) {
90
+ return res.status(400).json({ error: 'Invalid skill name' });
91
+ }
92
+ if (typeof content !== 'string') {
93
+ return res.status(400).json({ error: 'Missing or invalid `content` field.' });
94
+ }
95
+ const skillDir = path.join(SKILLS_DIR, name);
96
+ if (!fs.existsSync(skillDir)) {
97
+ fs.mkdirSync(skillDir, { recursive: true });
98
+ }
99
+ const skillPath = path.join(skillDir, 'SKILL.md');
100
+ fs.writeFileSync(skillPath, content, 'utf8');
101
+ res.json({ success: true, message: `Skill "${name}" updated successfully` });
102
+ } catch (err) { serverError(res, err); }
103
+ });
104
+
105
+ skillsRouter.delete('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
106
+ try {
107
+ const { name } = req.params;
108
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) {
109
+ return res.status(400).json({ error: 'Invalid skill name' });
110
+ }
111
+ const skillDir = path.join(SKILLS_DIR, name);
112
+ if (!fs.existsSync(skillDir)) {
113
+ return res.status(404).json({ error: `Skill "${name}" not found` });
114
+ }
115
+ fs.rmSync(skillDir, { recursive: true, force: true });
116
+ res.json({ success: true, message: `Skill "${name}" deleted successfully` });
117
+ } catch (err) { serverError(res, err); }
118
+ });