total-recall-brain 3.6.5 → 3.6.7
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.
- package/extension/background.js +4 -0
- package/extension/content-script.js +9 -1
- package/extension/manifest.json +0 -1
- package/package.json +1 -1
- package/src/core/daemon-loop.mjs +153 -62
- package/src/core/fact-seeker.mjs +6 -3
- package/src/core/research-queue.mjs +8 -1
- package/src/core/scheduler.mjs +23 -5
- package/src/core/schema.mjs +23 -0
- package/src/core/surface.mjs +6 -8
- package/src/server/rest.mjs +43 -189
- package/src/server/route-manifest.json +4 -0
- package/src/server/routes/auth.mjs +32 -0
- package/src/server/routes/research.mjs +55 -0
- package/src/server/routes/sandbox.mjs +55 -0
- package/src/server/routes/skills.mjs +118 -0
package/extension/background.js
CHANGED
|
@@ -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
|
-
|
|
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');
|
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
package/src/core/daemon-loop.mjs
CHANGED
|
@@ -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
|
-
// ───
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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')
|
|
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
|
-
|
|
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 &&
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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) {
|
package/src/core/fact-seeker.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/src/core/scheduler.mjs
CHANGED
|
@@ -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-${
|
|
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:
|
|
262
|
-
body:
|
|
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) {
|
package/src/core/schema.mjs
CHANGED
|
@@ -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,
|
package/src/core/surface.mjs
CHANGED
|
@@ -395,8 +395,9 @@ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir
|
|
|
395
395
|
if (fs.existsSync(shimPath)) {
|
|
396
396
|
const stat = fs.lstatSync(shimPath);
|
|
397
397
|
if (stat.isSymbolicLink()) {
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
// If it's a symlink, DO NOT destroy it. It likely points to INSTRUCTIONS.md natively,
|
|
399
|
+
// and its content will update automatically when the target updates.
|
|
400
|
+
return;
|
|
400
401
|
} else {
|
|
401
402
|
const raw = fs.readFileSync(shimPath, 'utf8');
|
|
402
403
|
let cleaned = raw;
|
|
@@ -465,12 +466,9 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
|
|
|
465
466
|
const connectedClients = readConnectedClients(clientsPath);
|
|
466
467
|
|
|
467
468
|
if (connectedClients === null) {
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
await writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir, derivedDir });
|
|
472
|
-
}
|
|
473
|
-
}
|
|
469
|
+
// Backward compat loop removed to prevent context exhaustion.
|
|
470
|
+
// By default, we only write INSTRUCTIONS.md.
|
|
471
|
+
// Users must explicitly use 'npx total-recall connect' to register client IDEs.
|
|
474
472
|
} else {
|
|
475
473
|
// Only write shims for connected clients
|
|
476
474
|
for (const client of connectedClients) {
|
package/src/server/rest.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
+
});
|