total-recall-brain 3.1.0
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/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/antigravity.mjs +212 -0
- package/bin/init.mjs +36 -0
- package/bin/oci-sniper.sh +69 -0
- package/bin/total-recall.mjs +229 -0
- package/docs/ARCHITECTURE.md +251 -0
- package/docs/IDE_INTEGRATION.md +112 -0
- package/package.json +71 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
- package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
- package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
- package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
- package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
- package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
- package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
- package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
- package/src/cli/agent-dir.mjs +105 -0
- package/src/cli/backup.mjs +313 -0
- package/src/cli/brain.mjs +224 -0
- package/src/cli/chat.mjs +74 -0
- package/src/cli/config.mjs +247 -0
- package/src/cli/connect.mjs +872 -0
- package/src/cli/daemon.mjs +228 -0
- package/src/cli/deploy-ui.mjs +1793 -0
- package/src/cli/deploy.mjs +1726 -0
- package/src/cli/doctor.mjs +254 -0
- package/src/cli/dream.mjs +80 -0
- package/src/cli/forget.mjs +92 -0
- package/src/cli/friction.mjs +21 -0
- package/src/cli/generate-pat.mjs +65 -0
- package/src/cli/hash-password.mjs +22 -0
- package/src/cli/help.mjs +153 -0
- package/src/cli/import-rules.mjs +102 -0
- package/src/cli/ingest/index.mjs +318 -0
- package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
- package/src/cli/ingest/parsers/google-keep.mjs +147 -0
- package/src/cli/ingest/parsers/search-history.mjs +142 -0
- package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
- package/src/cli/ingest/utils/dedup.mjs +69 -0
- package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
- package/src/cli/ingest.mjs +102 -0
- package/src/cli/init.mjs +736 -0
- package/src/cli/integration-dispatcher.mjs +128 -0
- package/src/cli/lint.mjs +166 -0
- package/src/cli/map.mjs +194 -0
- package/src/cli/migrate.mjs +87 -0
- package/src/cli/rebuild.mjs +88 -0
- package/src/cli/recall.mjs +219 -0
- package/src/cli/relay.mjs +518 -0
- package/src/cli/remember.mjs +306 -0
- package/src/cli/research.mjs +381 -0
- package/src/cli/reset-password.mjs +75 -0
- package/src/cli/restore.mjs +135 -0
- package/src/cli/setup.mjs +616 -0
- package/src/cli/share.mjs +180 -0
- package/src/cli/skill.mjs +313 -0
- package/src/cli/snapshot.mjs +108 -0
- package/src/cli/start.mjs +63 -0
- package/src/cli/status.mjs +296 -0
- package/src/cli/sync.mjs +383 -0
- package/src/cli/uninstall.mjs +613 -0
- package/src/cli/upgrade.mjs +141 -0
- package/src/cli/wizard.html +3682 -0
- package/src/core/blackboard.mjs +54 -0
- package/src/core/clarity-rewriter.mjs +770 -0
- package/src/core/conclusion-writer.mjs +259 -0
- package/src/core/config.mjs +277 -0
- package/src/core/conflict-detector.mjs +536 -0
- package/src/core/crypto.mjs +107 -0
- package/src/core/daemon-control.mjs +158 -0
- package/src/core/daemon-loop.mjs +364 -0
- package/src/core/dream.mjs +347 -0
- package/src/core/drift-detector.mjs +143 -0
- package/src/core/embeddings.mjs +632 -0
- package/src/core/emergency-alerts.mjs +137 -0
- package/src/core/evolution.mjs +107 -0
- package/src/core/fact-seeker.mjs +1602 -0
- package/src/core/friction.mjs +95 -0
- package/src/core/import-rules.mjs +308 -0
- package/src/core/inference-engine.mjs +367 -0
- package/src/core/logger.mjs +75 -0
- package/src/core/memory-layers.mjs +96 -0
- package/src/core/migrate.mjs +116 -0
- package/src/core/notifications.mjs +77 -0
- package/src/core/operation-validator.mjs +272 -0
- package/src/core/optimizer.mjs +330 -0
- package/src/core/pattern_detector.mjs +113 -0
- package/src/core/post-mortem.mjs +413 -0
- package/src/core/protect-instructions.mjs +148 -0
- package/src/core/quick-capture.mjs +145 -0
- package/src/core/research-queue.mjs +431 -0
- package/src/core/research.mjs +461 -0
- package/src/core/runtime.mjs +515 -0
- package/src/core/sandbox.mjs +163 -0
- package/src/core/scheduler.mjs +569 -0
- package/src/core/schema.mjs +333 -0
- package/src/core/search.mjs +219 -0
- package/src/core/semantic-index.mjs +276 -0
- package/src/core/session-watcher.mjs +850 -0
- package/src/core/snapshot.mjs +133 -0
- package/src/core/source-adapters.mjs +1038 -0
- package/src/core/steering.mjs +137 -0
- package/src/core/surface.mjs +444 -0
- package/src/core/task_runner.mjs +125 -0
- package/src/core/tts.mjs +142 -0
- package/src/core/usage-tracker.mjs +349 -0
- package/src/core/vault-cache.mjs +76 -0
- package/src/core/vault.mjs +273 -0
- package/src/core/watchdog.mjs +236 -0
- package/src/server/api.mjs +944 -0
- package/src/server/auth.mjs +410 -0
- package/src/server/index.mjs +868 -0
- package/src/server/keys.mjs +181 -0
- package/src/server/rest.mjs +1652 -0
- package/src/server/route-manifest.json +77 -0
- package/src/server/routes/_shared.mjs +93 -0
- package/src/server/routes/keys.mjs +55 -0
- package/src/server/routes/memory.mjs +249 -0
- package/src/server/routes/sessions.mjs +336 -0
- package/src/server/routes/share.mjs +138 -0
- package/src/server/tools.mjs +618 -0
- package/templates/Caddyfile +37 -0
- package/templates/com.totalrecall.backup.plist +42 -0
- package/templates/com.totalrecall.daemon.plist +40 -0
- package/templates/com.totalrecall.duckdns.plist +23 -0
- package/templates/com.totalrecall.server.plist +46 -0
- package/templates/default-config/budget.yml +6 -0
- package/templates/default-config/runtime.yml +11 -0
- package/templates/default-config/security.yml +53 -0
- package/templates/default-config/voice.yml +34 -0
- package/templates/obsidian-queries/conflicts.md +26 -0
- package/templates/obsidian-queries/daily.md +17 -0
- package/templates/obsidian-queries/index.md +38 -0
- package/templates/obsidian-queries/skills.md +14 -0
- package/templates/onboarding-interview.md +72 -0
- package/templates/skill_engineering.md +40 -0
- package/templates/total-recall-daemon.service +21 -0
- package/templates/total-recall-server.service +20 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick Capture β ingest messages from external sources as SSSS inbox nodes
|
|
3
|
+
*
|
|
4
|
+
* Classifies incoming message text and writes a draft SSSS memory node
|
|
5
|
+
* to ~/.agent/memory-inbox/ for Dream Cycle synthesis on the next pass.
|
|
6
|
+
* File-native; no external database.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import { brainDir, agentDir } from './config.mjs';
|
|
13
|
+
|
|
14
|
+
function getBrainDir() {
|
|
15
|
+
// Support test overrides: if _TR_TEST_AGENT_DIR is set, derive brainDir from it
|
|
16
|
+
const testDir = process.env._TR_TEST_AGENT_DIR;
|
|
17
|
+
if (testDir && testDir !== agentDir) {
|
|
18
|
+
return path.join(testDir, 'skills', 'total-recall');
|
|
19
|
+
}
|
|
20
|
+
return brainDir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function inboxDir() {
|
|
24
|
+
return path.join(getBrainDir(), 'memory-inbox', 'capture');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// βββ Heuristic classifier ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
28
|
+
|
|
29
|
+
const TASK_PATTERNS = [/\bTODO\b/i, /\bremind me\b/i, /\btask:/i, /^(do|fix|add|remove|update|create)\b/i];
|
|
30
|
+
const QUESTION_PATTERNS = [/\?$/, /\bhow\b/i, /\bwhat\b/i, /\bwhy\b/i, /\bwhen\b/i];
|
|
31
|
+
|
|
32
|
+
function classifyMessage(text) {
|
|
33
|
+
if (TASK_PATTERNS.some(p => p.test(text))) return 'task';
|
|
34
|
+
if (QUESTION_PATTERNS.some(p => p.test(text))) return 'observation';
|
|
35
|
+
return 'observation';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// βββ Node draft builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
39
|
+
|
|
40
|
+
function buildInboxNode({ text, author, channel, source, ts }) {
|
|
41
|
+
const now = ts || new Date().toISOString();
|
|
42
|
+
const slug = `capture-${source}-${crypto.randomBytes(4).toString('hex')}`;
|
|
43
|
+
const nodeType = classifyMessage(text);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
slug,
|
|
47
|
+
type: 'memory',
|
|
48
|
+
category: 'capture',
|
|
49
|
+
title: text.slice(0, 80).replace(/\n/g, ' '),
|
|
50
|
+
status: 'draft',
|
|
51
|
+
confidence: 0.5,
|
|
52
|
+
importance: 2,
|
|
53
|
+
created: now,
|
|
54
|
+
updated: now,
|
|
55
|
+
last_accessed: now,
|
|
56
|
+
source: {
|
|
57
|
+
type: source,
|
|
58
|
+
session_id: slug,
|
|
59
|
+
agent: author || 'unknown',
|
|
60
|
+
evidence_count: 1
|
|
61
|
+
},
|
|
62
|
+
supersedes: [],
|
|
63
|
+
superseded_by: null,
|
|
64
|
+
contradicts: [],
|
|
65
|
+
tags: ['capture', source, channel || 'direct'].filter(Boolean),
|
|
66
|
+
related: [],
|
|
67
|
+
routes_to_skills: [],
|
|
68
|
+
sentiment_polarity: 'descriptive',
|
|
69
|
+
sentiment_target: text.slice(0, 40),
|
|
70
|
+
modality: 'should',
|
|
71
|
+
subject: author || source,
|
|
72
|
+
predicate: nodeType === 'task' ? 'needs_to' : 'observed',
|
|
73
|
+
object: text.slice(0, 120),
|
|
74
|
+
decay: { half_life_days: 30, access_count: 0 },
|
|
75
|
+
schema_version: 2,
|
|
76
|
+
x_memory_layer: 'system1',
|
|
77
|
+
raw_content: text,
|
|
78
|
+
x_temporal_context: now,
|
|
79
|
+
x_citations: [{
|
|
80
|
+
source: source || 'chat',
|
|
81
|
+
title: `Captured message from channel ${channel || 'direct'}`,
|
|
82
|
+
url: `chat://${source}/${channel || 'direct'}`,
|
|
83
|
+
published: now,
|
|
84
|
+
relevance: 1.0,
|
|
85
|
+
accessed: now
|
|
86
|
+
}]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// βββ Source validation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a capture source identifier.
|
|
94
|
+
* Accepts any lowercase alphanumeric kebab-case string up to 50 characters.
|
|
95
|
+
* @param {string} source
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
export function isValidSource(source) {
|
|
99
|
+
return typeof source === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(source) && source.length <= 50;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// βββ Capture entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Write an inbox node from an inbound message.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} opts
|
|
108
|
+
* @param {string} opts.text Message text content
|
|
109
|
+
* @param {string} [opts.author] Author username / user id
|
|
110
|
+
* @param {string} [opts.channel] Channel name or id
|
|
111
|
+
* @param {string} opts.source Kebab-case source identifier (e.g. 'slack', 'chrome-extension', 'google-takeout')
|
|
112
|
+
* @param {string} [opts.ts] ISO timestamp (defaults to now)
|
|
113
|
+
* @returns {{ slug: string, file: string }}
|
|
114
|
+
*/
|
|
115
|
+
export function captureMessage({ text, author, channel, source, ts }) {
|
|
116
|
+
if (!text || !text.trim()) throw new Error('text is required and must not be empty');
|
|
117
|
+
if (!isValidSource(source)) throw new Error('source must be a lowercase alphanumeric kebab-case string (max 50 chars)');
|
|
118
|
+
|
|
119
|
+
const node = buildInboxNode({ text: text.trim(), author, channel, source, ts });
|
|
120
|
+
|
|
121
|
+
const dir = inboxDir();
|
|
122
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
123
|
+
|
|
124
|
+
const file = path.join(dir, `${node.slug}.json`);
|
|
125
|
+
fs.writeFileSync(file, JSON.stringify(node, null, 2), 'utf8');
|
|
126
|
+
|
|
127
|
+
return { slug: node.slug, file };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* List inbox capture nodes, most recent first.
|
|
132
|
+
* @returns {object[]}
|
|
133
|
+
*/
|
|
134
|
+
export function listCaptureInbox() {
|
|
135
|
+
const dir = inboxDir();
|
|
136
|
+
if (!fs.existsSync(dir)) return [];
|
|
137
|
+
return fs.readdirSync(dir)
|
|
138
|
+
.filter(f => f.endsWith('.json'))
|
|
139
|
+
.map(f => {
|
|
140
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); }
|
|
141
|
+
catch { return null; }
|
|
142
|
+
})
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.sort((a, b) => b.created.localeCompare(a.created));
|
|
145
|
+
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/research-queue.mjs
|
|
3
|
+
*
|
|
4
|
+
* Canonical research queue CRUD. All queue logic lives here.
|
|
5
|
+
* MCP tools and REST endpoints are thin callers.
|
|
6
|
+
*
|
|
7
|
+
* Storage: .agent/research-queue.jsonl (one JSON object per line)
|
|
8
|
+
* Each item: { id, topic, status, priority, notes, node_slug, research_phase, created_at, updated_at, completed_at, summary }
|
|
9
|
+
* Status values: 'pending' | 'in_progress' | 'done' | 'failed'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import matter from 'gray-matter';
|
|
16
|
+
import { loadNodes, writeNode, deleteNode } from './vault.mjs';
|
|
17
|
+
import { brainDir } from './config.mjs';
|
|
18
|
+
|
|
19
|
+
const getBrainDir = () => process.env._TR_TEST_AGENT_DIR || brainDir;
|
|
20
|
+
|
|
21
|
+
function getQueueFile(overrideBrainDir) {
|
|
22
|
+
const dir = overrideBrainDir || getBrainDir();
|
|
23
|
+
return path.join(dir, 'research-queue.jsonl');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STATUS_RANK = { pending: 0, in_progress: 1, done: 2, failed: 3 };
|
|
27
|
+
|
|
28
|
+
// βββ Summary Compiler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compile a gorgeous, detailed research project summary from the topic state and vault node.
|
|
32
|
+
*/
|
|
33
|
+
export function compileResearchProjectSummary(item, node) {
|
|
34
|
+
if (!node) {
|
|
35
|
+
return {
|
|
36
|
+
id: item.id,
|
|
37
|
+
topic: item.topic,
|
|
38
|
+
status: item.status,
|
|
39
|
+
phase: item.research_phase || 'acquisition',
|
|
40
|
+
last_updated: item.updated_at,
|
|
41
|
+
conclusions: [],
|
|
42
|
+
ongoing_directions: ['Research project is enqueued / in progress in phase: ' + (item.research_phase || 'acquisition')],
|
|
43
|
+
sources_count: 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const body = node.body || '';
|
|
48
|
+
const sections = {};
|
|
49
|
+
let currentHeader = null;
|
|
50
|
+
|
|
51
|
+
// Split body by lines to group into sections
|
|
52
|
+
const lines = body.split('\n');
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const headerMatch = line.match(/^##\s+(.+)$/);
|
|
55
|
+
if (headerMatch) {
|
|
56
|
+
currentHeader = headerMatch[1].trim().toLowerCase();
|
|
57
|
+
sections[currentHeader] = [];
|
|
58
|
+
} else if (currentHeader) {
|
|
59
|
+
sections[currentHeader].push(line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Helper to extract bullet points from a section
|
|
64
|
+
const extractBullets = (sectionNames) => {
|
|
65
|
+
const bullets = [];
|
|
66
|
+
for (const name of sectionNames) {
|
|
67
|
+
const secLines = sections[name];
|
|
68
|
+
if (secLines) {
|
|
69
|
+
for (const line of secLines) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (trimmed.startsWith('-') || trimmed.startsWith('*')) {
|
|
72
|
+
const cleaned = trimmed.replace(/^[-*]\s+/, '');
|
|
73
|
+
bullets.push(cleaned);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return bullets;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Find conclusions & key facts
|
|
82
|
+
const keyFacts = extractBullets(['key facts', 'verified facts', 'facts']);
|
|
83
|
+
const cognitiveDelib = extractBullets(['cognitive deliberation', 'insights', 'deliberation']);
|
|
84
|
+
const conclusions = [...keyFacts, ...cognitiveDelib];
|
|
85
|
+
|
|
86
|
+
// Find ongoing directions, gaps, and monitoring
|
|
87
|
+
const furtherResearch = extractBullets(['further research needed', 'gaps', 'tangents']);
|
|
88
|
+
const monitoring = extractBullets(['ongoing monitoring & sources', 'monitored sources', 'monitoring']);
|
|
89
|
+
const expansion = extractBullets(['spawned research expansion avenues', 'expansion avenues', 'expansion']);
|
|
90
|
+
const ongoingDirections = [...furtherResearch, ...monitoring, ...expansion];
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: item.id,
|
|
94
|
+
topic: item.topic,
|
|
95
|
+
status: item.status,
|
|
96
|
+
phase: item.research_phase || 'acquisition',
|
|
97
|
+
node_slug: item.node_slug,
|
|
98
|
+
node_status: node.status,
|
|
99
|
+
last_updated: item.updated_at,
|
|
100
|
+
conclusions: conclusions.length > 0 ? conclusions : ['Initial facts gathered and indexed in the knowledge base.'],
|
|
101
|
+
ongoing_directions: ongoingDirections.length > 0 ? ongoingDirections : ['Next phases: deliberation, layout refinement, ongoing monitoring, and tangential expansion.'],
|
|
102
|
+
sources_count: node.source?.evidence_count || keyFacts.length || 0
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sync a research project summary node to the SSSS memory vault.
|
|
108
|
+
* Creates/updates a .md node in memory-vault/facts/ named research-project-<id>.md.
|
|
109
|
+
*/
|
|
110
|
+
export function syncResearchProjectNode(item) {
|
|
111
|
+
if (!item) return;
|
|
112
|
+
const vaultDir = path.join(getBrainDir(), 'memory-vault');
|
|
113
|
+
|
|
114
|
+
// ONLY create/sync the vault memory node if the project's status is 'done'!
|
|
115
|
+
// If it is pending, in_progress, or failed, we do NOT want it in our memory-vault/facts directory
|
|
116
|
+
// as it does not contain real completed memory yet.
|
|
117
|
+
if (item.status !== 'done') {
|
|
118
|
+
try {
|
|
119
|
+
deleteNode(`research-project-${item.id}`, vaultDir);
|
|
120
|
+
} catch {}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Build a beautiful markdown body for the summary node
|
|
125
|
+
const lines = [];
|
|
126
|
+
lines.push(`# Research Project Summary: ${item.topic}`);
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(`> [!NOTE]`);
|
|
129
|
+
lines.push(`> **Project Status**: \`${item.status}\` | **Research Phase**: \`${item.research_phase || 'acquisition'}\``);
|
|
130
|
+
lines.push(`> **Priority**: \`${item.priority || 'medium'}\``);
|
|
131
|
+
lines.push(`> **Created**: \`${item.created_at}\` | **Last Updated**: \`${item.updated_at}\``);
|
|
132
|
+
lines.push('');
|
|
133
|
+
|
|
134
|
+
lines.push('## Conclusions');
|
|
135
|
+
if (item.summary?.conclusions && item.summary.conclusions.length > 0) {
|
|
136
|
+
for (const c of item.summary.conclusions) {
|
|
137
|
+
lines.push(`- ${c}`);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
lines.push('- No conclusions recorded yet.');
|
|
141
|
+
}
|
|
142
|
+
lines.push('');
|
|
143
|
+
|
|
144
|
+
lines.push('## Ongoing Directions');
|
|
145
|
+
if (item.summary?.ongoing_directions && item.summary.ongoing_directions.length > 0) {
|
|
146
|
+
for (const d of item.summary.ongoing_directions) {
|
|
147
|
+
lines.push(`- ${d}`);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
lines.push('- No ongoing directions recorded yet.');
|
|
151
|
+
}
|
|
152
|
+
lines.push('');
|
|
153
|
+
|
|
154
|
+
if (item.node_slug) {
|
|
155
|
+
lines.push('## Full Report');
|
|
156
|
+
lines.push(`- [[${item.node_slug}]]`);
|
|
157
|
+
lines.push('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const body = lines.join('\n');
|
|
161
|
+
|
|
162
|
+
const node = {
|
|
163
|
+
type: 'memory',
|
|
164
|
+
slug: `research-project-${item.id}`,
|
|
165
|
+
category: 'facts',
|
|
166
|
+
title: `Research Project Summary: ${item.topic}`,
|
|
167
|
+
body,
|
|
168
|
+
status: 'active',
|
|
169
|
+
confidence: 1.0,
|
|
170
|
+
importance: 3,
|
|
171
|
+
created: item.created_at,
|
|
172
|
+
updated: item.updated_at,
|
|
173
|
+
last_accessed: new Date().toISOString(),
|
|
174
|
+
source: {
|
|
175
|
+
type: 'research-engine',
|
|
176
|
+
session_id: item.id,
|
|
177
|
+
evidence_count: item.summary?.sources_count || 0
|
|
178
|
+
},
|
|
179
|
+
supersedes: [],
|
|
180
|
+
superseded_by: null,
|
|
181
|
+
contradicts: [],
|
|
182
|
+
tags: ['research-project', 'summary', 'status'],
|
|
183
|
+
related: item.node_slug ? [item.node_slug] : [],
|
|
184
|
+
routes_to_skills: [],
|
|
185
|
+
sentiment_polarity: 'descriptive',
|
|
186
|
+
sentiment_target: 'research-project',
|
|
187
|
+
modality: 'should',
|
|
188
|
+
subject: 'agent',
|
|
189
|
+
predicate: 'tracked_research_project',
|
|
190
|
+
object: 'knowledge_vault',
|
|
191
|
+
decay: {
|
|
192
|
+
half_life_days: 90,
|
|
193
|
+
access_count: 1
|
|
194
|
+
},
|
|
195
|
+
schema_version: 2,
|
|
196
|
+
x_temporal_context: item.updated_at,
|
|
197
|
+
x_citations: [{
|
|
198
|
+
source: 'research-engine',
|
|
199
|
+
title: item.topic,
|
|
200
|
+
url: `research://${item.id}`,
|
|
201
|
+
published: item.created_at,
|
|
202
|
+
relevance: 1.0,
|
|
203
|
+
accessed: new Date().toISOString()
|
|
204
|
+
}]
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
writeNode(node, vaultDir);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Log or handle error gracefully
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// βββ I/O βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
215
|
+
|
|
216
|
+
export function loadQueue(overrideBrainDir) {
|
|
217
|
+
const queueFile = getQueueFile(overrideBrainDir);
|
|
218
|
+
const effectiveBrainDir = overrideBrainDir || getBrainDir();
|
|
219
|
+
if (!fs.existsSync(queueFile)) return [];
|
|
220
|
+
const items = fs.readFileSync(queueFile, 'utf8')
|
|
221
|
+
.split('\n').filter(Boolean)
|
|
222
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
|
|
225
|
+
// Dynamic self-healing: ensure every research item has a summary enqueued
|
|
226
|
+
let changed = false;
|
|
227
|
+
const vaultDir = path.join(effectiveBrainDir, 'memory-vault');
|
|
228
|
+
|
|
229
|
+
for (const item of items) {
|
|
230
|
+
if (!item.summary) {
|
|
231
|
+
let node = null;
|
|
232
|
+
if (item.node_slug) {
|
|
233
|
+
try {
|
|
234
|
+
const nodes = loadNodes(vaultDir);
|
|
235
|
+
node = nodes.find(n => n.slug === item.node_slug);
|
|
236
|
+
if (!node) {
|
|
237
|
+
const inboxFile = path.join(effectiveBrainDir, 'memory-inbox', 'pending', `${item.node_slug}.md`);
|
|
238
|
+
if (fs.existsSync(inboxFile)) {
|
|
239
|
+
const raw = fs.readFileSync(inboxFile, 'utf8');
|
|
240
|
+
const { data, content } = matter(raw);
|
|
241
|
+
node = { ...data, body: content.trim() };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {}
|
|
245
|
+
}
|
|
246
|
+
item.summary = compileResearchProjectSummary(item, node);
|
|
247
|
+
changed = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Dynamic self-healing: ensure a corresponding markdown summary node is in the vault
|
|
251
|
+
if (item.status === 'done') {
|
|
252
|
+
const summaryPath = path.join(vaultDir, 'facts', `research-project-${item.id}.md`);
|
|
253
|
+
if (!fs.existsSync(summaryPath)) {
|
|
254
|
+
try {
|
|
255
|
+
syncResearchProjectNode(item);
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
try {
|
|
260
|
+
deleteNode(`research-project-${item.id}`, vaultDir);
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (changed) {
|
|
266
|
+
try {
|
|
267
|
+
saveQueue(items, overrideBrainDir);
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return items;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function saveQueue(items, overrideBrainDir) {
|
|
275
|
+
const queueFile = getQueueFile(overrideBrainDir);
|
|
276
|
+
fs.mkdirSync(path.dirname(queueFile), { recursive: true });
|
|
277
|
+
fs.writeFileSync(queueFile, items.map(i => JSON.stringify(i)).join('\n') + '\n', 'utf8');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// βββ CRUD ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* List all research projects, optionally filtered by status or search query.
|
|
284
|
+
* Sorted: pending β in_progress β done β failed, then by updated_at desc.
|
|
285
|
+
*
|
|
286
|
+
* @param {{ status?: string, query?: string, limit?: number, offset?: number }} [opts]
|
|
287
|
+
* @returns {{ counts: object, total: number, items: object[] }}
|
|
288
|
+
*/
|
|
289
|
+
export function listQueue({ status, query, limit = 100, offset = 0, brainDir: overrideBrainDir } = {}) {
|
|
290
|
+
const all = loadQueue(overrideBrainDir);
|
|
291
|
+
const counts = { pending: 0, in_progress: 0, done: 0, failed: 0 };
|
|
292
|
+
all.forEach(i => { if (counts[i.status] !== undefined) counts[i.status]++; });
|
|
293
|
+
|
|
294
|
+
let items = status && status !== 'all' ? all.filter(i => i.status === status) : [...all];
|
|
295
|
+
|
|
296
|
+
if (query) {
|
|
297
|
+
const q = String(query).toLowerCase();
|
|
298
|
+
items = items.filter(i =>
|
|
299
|
+
(i.topic && i.topic.toLowerCase().includes(q)) ||
|
|
300
|
+
(i.notes && i.notes.toLowerCase().includes(q))
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
items.sort((a, b) => {
|
|
305
|
+
const ra = STATUS_RANK[a.status] ?? 4, rb = STATUS_RANK[b.status] ?? 4;
|
|
306
|
+
if (ra !== rb) return ra - rb;
|
|
307
|
+
return (b.updated_at || b.created_at).localeCompare(a.updated_at || a.created_at);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const lim = Math.min(500, Number(limit) || 100);
|
|
311
|
+
const off = Number(offset) || 0;
|
|
312
|
+
return { counts, total: items.length, items: items.slice(off, off + lim) };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Add a new research topic (status: pending).
|
|
317
|
+
*
|
|
318
|
+
* @param {{ topic: string, priority?: string, notes?: string }} opts
|
|
319
|
+
* @returns {object} The created item
|
|
320
|
+
*/
|
|
321
|
+
export function addToQueue({ topic, priority = 'medium', notes, brainDir: overrideBrainDir } = {}) {
|
|
322
|
+
if (!topic) throw new Error('topic is required');
|
|
323
|
+
const item = {
|
|
324
|
+
id: crypto.randomUUID(),
|
|
325
|
+
topic: String(topic),
|
|
326
|
+
status: 'pending',
|
|
327
|
+
priority: priority || 'medium',
|
|
328
|
+
notes: notes || null,
|
|
329
|
+
node_slug: null,
|
|
330
|
+
research_phase: 'acquisition',
|
|
331
|
+
created_at: new Date().toISOString(),
|
|
332
|
+
updated_at: new Date().toISOString(),
|
|
333
|
+
completed_at: null,
|
|
334
|
+
};
|
|
335
|
+
item.summary = compileResearchProjectSummary(item, null);
|
|
336
|
+
const items = loadQueue(overrideBrainDir);
|
|
337
|
+
items.unshift(item);
|
|
338
|
+
saveQueue(items, overrideBrainDir);
|
|
339
|
+
|
|
340
|
+
// Sync initial pending project summary node to the vault
|
|
341
|
+
try {
|
|
342
|
+
syncResearchProjectNode(item);
|
|
343
|
+
} catch {}
|
|
344
|
+
|
|
345
|
+
return item;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Update an existing item by id.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} id
|
|
352
|
+
* @param {{ status?: string, notes?: string, node_slug?: string, priority?: string, research_phase?: string }} patch
|
|
353
|
+
* @returns {object} The updated item
|
|
354
|
+
*/
|
|
355
|
+
export function updateQueueItem(id, patch = {}, overrideBrainDir) {
|
|
356
|
+
const items = loadQueue(overrideBrainDir);
|
|
357
|
+
const idx = items.findIndex(i => i.id === id);
|
|
358
|
+
if (idx === -1) throw Object.assign(new Error(`Research project not found: ${id}`), { status: 404 });
|
|
359
|
+
|
|
360
|
+
const item = { ...items[idx] };
|
|
361
|
+
if (patch.status !== undefined) {
|
|
362
|
+
item.status = patch.status;
|
|
363
|
+
if (patch.status === 'pending') {
|
|
364
|
+
item.completed_at = null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (patch.notes !== undefined) item.notes = patch.notes;
|
|
368
|
+
if (patch.node_slug !== undefined) item.node_slug = patch.node_slug;
|
|
369
|
+
if (patch.priority !== undefined) item.priority = patch.priority;
|
|
370
|
+
if (patch.research_phase !== undefined) item.research_phase = patch.research_phase;
|
|
371
|
+
|
|
372
|
+
item.updated_at = new Date().toISOString();
|
|
373
|
+
if ((item.status === 'done' || item.status === 'failed') && !item.completed_at) {
|
|
374
|
+
item.completed_at = new Date().toISOString();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Load the latest node (from vault or inbox pending) to compile summary
|
|
378
|
+
const localBrainDir = overrideBrainDir || getBrainDir();
|
|
379
|
+
const vaultDir = path.join(localBrainDir, 'memory-vault');
|
|
380
|
+
let node = null;
|
|
381
|
+
if (item.node_slug) {
|
|
382
|
+
try {
|
|
383
|
+
const nodes = loadNodes(vaultDir);
|
|
384
|
+
node = nodes.find(n => n.slug === item.node_slug);
|
|
385
|
+
|
|
386
|
+
if (!node) {
|
|
387
|
+
const inboxFile = path.join(localBrainDir, 'memory-inbox', 'pending', `${item.node_slug}.md`);
|
|
388
|
+
if (fs.existsSync(inboxFile)) {
|
|
389
|
+
const raw = fs.readFileSync(inboxFile, 'utf8');
|
|
390
|
+
const { data, content } = matter(raw);
|
|
391
|
+
node = { ...data, body: content.trim() };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} catch (e) {
|
|
395
|
+
// Ignore non-fatal vault lookup error
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
item.summary = compileResearchProjectSummary(item, node);
|
|
399
|
+
|
|
400
|
+
items[idx] = item;
|
|
401
|
+
saveQueue(items, overrideBrainDir);
|
|
402
|
+
|
|
403
|
+
// Sync updated research project summary node to the vault
|
|
404
|
+
try {
|
|
405
|
+
syncResearchProjectNode(item);
|
|
406
|
+
} catch {}
|
|
407
|
+
|
|
408
|
+
return item;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Remove a research project by id.
|
|
413
|
+
*
|
|
414
|
+
* @param {string} id
|
|
415
|
+
* @returns {{ deleted: boolean, id: string, topic: string }}
|
|
416
|
+
*/
|
|
417
|
+
export function removeFromQueue(id, overrideBrainDir) {
|
|
418
|
+
const items = loadQueue(overrideBrainDir);
|
|
419
|
+
const idx = items.findIndex(i => i.id === id);
|
|
420
|
+
if (idx === -1) throw Object.assign(new Error(`Research project not found: ${id}`), { status: 404 });
|
|
421
|
+
const [removed] = items.splice(idx, 1);
|
|
422
|
+
saveQueue(items, overrideBrainDir);
|
|
423
|
+
|
|
424
|
+
// Delete the corresponding summary node from the vault
|
|
425
|
+
try {
|
|
426
|
+
const vaultDir = path.join(overrideBrainDir || getBrainDir(), 'memory-vault');
|
|
427
|
+
deleteNode(`research-project-${id}`, vaultDir);
|
|
428
|
+
} catch {}
|
|
429
|
+
|
|
430
|
+
return { deleted: true, id: removed.id, topic: removed.topic };
|
|
431
|
+
}
|