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,259 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { callLocalRuntime } from './runtime.mjs';
|
|
5
|
+
import { atomicWrite, safeStringify } from './vault.mjs';
|
|
6
|
+
import { logger } from './logger.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Total Recall Conclusion Writer — Research → Active Validation Gate
|
|
10
|
+
*
|
|
11
|
+
* All research-derived facts arrive as status: draft in .agent/memory-inbox/pending/.
|
|
12
|
+
* Before promoting any draft to status: active, the Conclusion Writer asks the local LLM:
|
|
13
|
+
* 1. Is this fact self-consistent?
|
|
14
|
+
* 2. Does it contradict any existing active node?
|
|
15
|
+
* 3. Is the confidence appropriate given the source?
|
|
16
|
+
*
|
|
17
|
+
* If it passes, the node is promoted (status: active, moved to vault).
|
|
18
|
+
* If it fails, it gets quarantined with a reason record.
|
|
19
|
+
*
|
|
20
|
+
* This prevents research hallucinations from polluting working memory.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const VALIDATION_SYSTEM = `You are a Fact Validation Gate for a sovereign AI memory system called Total Recall.
|
|
24
|
+
|
|
25
|
+
You will be given a draft memory node and a list of related active nodes. Determine if the draft should be promoted to active memory.
|
|
26
|
+
|
|
27
|
+
Output valid JSON:
|
|
28
|
+
{
|
|
29
|
+
"verdict": "APPROVE" | "REJECT" | "NEEDS_REVISION",
|
|
30
|
+
"confidence_adjustment": -0.3 to +0.1,
|
|
31
|
+
"reasoning": "string",
|
|
32
|
+
"revision_instructions": "string (only if NEEDS_REVISION)"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Criteria for REJECT:
|
|
36
|
+
- Factual claim that contradicts an active node with higher confidence
|
|
37
|
+
- Circular or empty content (no actionable information)
|
|
38
|
+
- Obvious hallucination (specific version numbers without any evidence)
|
|
39
|
+
- Duplicate of an existing node
|
|
40
|
+
|
|
41
|
+
Criteria for NEEDS_REVISION:
|
|
42
|
+
- Directionally correct but confidence is too high for the evidence
|
|
43
|
+
- Title is misleading
|
|
44
|
+
- Body needs tightening
|
|
45
|
+
|
|
46
|
+
Criteria for APPROVE:
|
|
47
|
+
- Adds genuine new knowledge
|
|
48
|
+
- Does not contradict existing high-confidence nodes
|
|
49
|
+
- Confidence is appropriate for the evidence
|
|
50
|
+
|
|
51
|
+
Output ONLY valid JSON.`;
|
|
52
|
+
|
|
53
|
+
function buildValidationPrompt(draftNode, relatedActiveNodes) {
|
|
54
|
+
const draftText = [
|
|
55
|
+
`## Draft Node: ${draftNode.slug}`,
|
|
56
|
+
`Title: "${draftNode.title}"`,
|
|
57
|
+
`Category: ${draftNode.category} | Confidence: ${draftNode.confidence}`,
|
|
58
|
+
`Source: ${draftNode.source?.type || 'unknown'} / ${draftNode.source?.agent || 'unknown'}`,
|
|
59
|
+
'',
|
|
60
|
+
'## Body',
|
|
61
|
+
(draftNode.body || '').slice(0, 1200),
|
|
62
|
+
].join('\n');
|
|
63
|
+
|
|
64
|
+
const relatedText = relatedActiveNodes.length > 0
|
|
65
|
+
? '\n\n## Related Active Nodes\n' + relatedActiveNodes.map(n => [
|
|
66
|
+
`### [${n.slug}] ${n.title} (confidence: ${n.confidence})`,
|
|
67
|
+
(n.body || '').slice(0, 400),
|
|
68
|
+
].join('\n')).join('\n\n')
|
|
69
|
+
: '\n\n## Related Active Nodes\n(none)';
|
|
70
|
+
|
|
71
|
+
return draftText + relatedText + '\n\nShould this draft be promoted to active memory?';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find active nodes related to a draft (same category or shared tags).
|
|
76
|
+
*/
|
|
77
|
+
function findRelatedNodes(draftNode, allActiveNodes) {
|
|
78
|
+
return allActiveNodes
|
|
79
|
+
.filter(n => {
|
|
80
|
+
if (n.category !== draftNode.category) return false;
|
|
81
|
+
const draftTags = new Set(draftNode.tags || []);
|
|
82
|
+
const sharedTags = (n.tags || []).filter(t => draftTags.has(t));
|
|
83
|
+
return sharedTags.length > 0;
|
|
84
|
+
})
|
|
85
|
+
.slice(0, 5); // Cap at 5 to keep prompt size manageable
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run the validation gate on a single draft node.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} draftNode Parsed draft node object (includes _filepath)
|
|
92
|
+
* @param {object[]} allActiveNodes All currently active nodes from vault
|
|
93
|
+
* @param {object} opts
|
|
94
|
+
* @param {string} opts.vaultDir Path to memory vault
|
|
95
|
+
* @param {string} opts.quarantineDir Path to quarantine directory
|
|
96
|
+
* @param {object} opts.runtimeConfig Local LLM config
|
|
97
|
+
* @returns {{ verdict: string, promoted: boolean, error?: string }}
|
|
98
|
+
*/
|
|
99
|
+
export async function validateDraftNode(draftNode, allActiveNodes, { vaultDir, quarantineDir, runtimeConfig }) {
|
|
100
|
+
const relatedNodes = findRelatedNodes(draftNode, allActiveNodes);
|
|
101
|
+
const prompt = buildValidationPrompt(draftNode, relatedNodes);
|
|
102
|
+
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
const rawResponse = await callLocalRuntime(prompt, VALIDATION_SYSTEM, runtimeConfig);
|
|
106
|
+
const jsonMatch = rawResponse.match(/\{[\s\S]*\}/);
|
|
107
|
+
if (!jsonMatch) throw new Error('No JSON in response');
|
|
108
|
+
result = JSON.parse(jsonMatch[0]);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.info({
|
|
111
|
+
subsystem: 'conclusion-writer',
|
|
112
|
+
message: `Validation failed for ${draftNode.slug}: ${err.message}`,
|
|
113
|
+
});
|
|
114
|
+
return { verdict: 'SKIP', promoted: false, error: err.message };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const verdict = result.verdict || 'REJECT';
|
|
118
|
+
const confidenceAdj = Number(result.confidence_adjustment) || 0;
|
|
119
|
+
|
|
120
|
+
if (verdict === 'APPROVE') {
|
|
121
|
+
await promoteToVault(draftNode, confidenceAdj, vaultDir);
|
|
122
|
+
logger.info({
|
|
123
|
+
subsystem: 'conclusion-writer',
|
|
124
|
+
message: `APPROVED: ${draftNode.slug} → ${draftNode.category}/`,
|
|
125
|
+
});
|
|
126
|
+
return { verdict: 'APPROVE', promoted: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (verdict === 'NEEDS_REVISION') {
|
|
130
|
+
await applyRevision(draftNode, confidenceAdj, result.revision_instructions);
|
|
131
|
+
logger.info({
|
|
132
|
+
subsystem: 'conclusion-writer',
|
|
133
|
+
message: `NEEDS_REVISION: ${draftNode.slug} — revised in-place`,
|
|
134
|
+
});
|
|
135
|
+
return { verdict: 'NEEDS_REVISION', promoted: false };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// REJECT → quarantine
|
|
139
|
+
await quarantineDraft(draftNode, result.reasoning || 'Rejected by validation gate', quarantineDir);
|
|
140
|
+
logger.info({
|
|
141
|
+
subsystem: 'conclusion-writer',
|
|
142
|
+
message: `REJECTED: ${draftNode.slug} — ${result.reasoning}`,
|
|
143
|
+
});
|
|
144
|
+
return { verdict: 'REJECT', promoted: false };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function promoteToVault(draftNode, confidenceAdj, vaultDir) {
|
|
148
|
+
if (!draftNode._filepath) return;
|
|
149
|
+
|
|
150
|
+
const raw = fs.readFileSync(draftNode._filepath, 'utf8');
|
|
151
|
+
const { data, content } = matter(raw);
|
|
152
|
+
|
|
153
|
+
// Promote
|
|
154
|
+
data.status = 'active';
|
|
155
|
+
data.confidence = Math.min(1.0, Math.max(0.1, (data.confidence || 0.5) + confidenceAdj));
|
|
156
|
+
data.updated = new Date().toISOString();
|
|
157
|
+
data.x_promoted_at = new Date().toISOString();
|
|
158
|
+
|
|
159
|
+
// Compute target path in vault
|
|
160
|
+
const targetDir = path.join(vaultDir, data.category || 'concepts');
|
|
161
|
+
if (!fs.existsSync(targetDir)) {
|
|
162
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const targetPath = path.join(targetDir, `${data.slug}.md`);
|
|
166
|
+
atomicWrite(targetPath, safeStringify(content, data));
|
|
167
|
+
|
|
168
|
+
// Remove from inbox
|
|
169
|
+
try { fs.unlinkSync(draftNode._filepath); } catch { /* ignore */ }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function applyRevision(draftNode, confidenceAdj, revisionInstructions) {
|
|
173
|
+
if (!draftNode._filepath || !revisionInstructions) return;
|
|
174
|
+
|
|
175
|
+
const raw = fs.readFileSync(draftNode._filepath, 'utf8');
|
|
176
|
+
const { data, content } = matter(raw);
|
|
177
|
+
|
|
178
|
+
data.confidence = Math.min(1.0, Math.max(0.1, (data.confidence || 0.5) + confidenceAdj));
|
|
179
|
+
data.x_revision_requested = revisionInstructions;
|
|
180
|
+
data.updated = new Date().toISOString();
|
|
181
|
+
|
|
182
|
+
atomicWrite(draftNode._filepath, safeStringify(content, data));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function quarantineDraft(draftNode, reason, quarantineDir) {
|
|
186
|
+
if (!fs.existsSync(quarantineDir)) {
|
|
187
|
+
fs.mkdirSync(quarantineDir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!draftNode._filepath) return;
|
|
191
|
+
|
|
192
|
+
const raw = fs.readFileSync(draftNode._filepath, 'utf8');
|
|
193
|
+
const { data, content } = matter(raw);
|
|
194
|
+
data.status = 'quarantined';
|
|
195
|
+
data.x_quarantine_reason = reason;
|
|
196
|
+
data.x_quarantined_at = new Date().toISOString();
|
|
197
|
+
|
|
198
|
+
const targetPath = path.join(quarantineDir, `${data.slug}.md`);
|
|
199
|
+
atomicWrite(targetPath, safeStringify(content, data));
|
|
200
|
+
|
|
201
|
+
try { fs.unlinkSync(draftNode._filepath); } catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run the validation gate over all draft nodes in the inbox.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} opts
|
|
208
|
+
* @param {string} opts.inboxDir Path to pending inbox
|
|
209
|
+
* @param {string} opts.vaultDir Path to memory vault
|
|
210
|
+
* @param {string} opts.quarantineDir Path to quarantine
|
|
211
|
+
* @param {object} opts.runtimeConfig
|
|
212
|
+
* @returns {{ processed: number, approved: number, rejected: number }}
|
|
213
|
+
*/
|
|
214
|
+
export async function runConclusionWriter({ inboxDir, vaultDir, quarantineDir, runtimeConfig }) {
|
|
215
|
+
if (!fs.existsSync(inboxDir)) {
|
|
216
|
+
return { processed: 0, approved: 0, rejected: 0, skipped: 'no-inbox' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.md'));
|
|
220
|
+
if (files.length === 0) {
|
|
221
|
+
return { processed: 0, approved: 0, rejected: 0, skipped: 'empty-inbox' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Load all active vault nodes for comparison
|
|
225
|
+
const { loadNodes } = await import('./vault.mjs');
|
|
226
|
+
const allActiveNodes = loadNodes(vaultDir).filter(n => n.status === 'active');
|
|
227
|
+
|
|
228
|
+
let approved = 0;
|
|
229
|
+
let rejected = 0;
|
|
230
|
+
|
|
231
|
+
// Process up to 10 drafts per run to avoid excessive LLM calls
|
|
232
|
+
const batch = files.slice(0, 10);
|
|
233
|
+
|
|
234
|
+
for (const file of batch) {
|
|
235
|
+
const filepath = path.join(inboxDir, file);
|
|
236
|
+
try {
|
|
237
|
+
const raw = fs.readFileSync(filepath, 'utf8');
|
|
238
|
+
const { data, content } = matter(raw);
|
|
239
|
+
if (!data.slug || data.status !== 'draft') continue;
|
|
240
|
+
|
|
241
|
+
const draftNode = { ...data, body: content.trim(), _filepath: filepath };
|
|
242
|
+
const result = await validateDraftNode(draftNode, allActiveNodes, {
|
|
243
|
+
vaultDir,
|
|
244
|
+
quarantineDir,
|
|
245
|
+
runtimeConfig,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (result.promoted) approved++;
|
|
249
|
+
else if (result.verdict === 'REJECT') rejected++;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
logger.info({
|
|
252
|
+
subsystem: 'conclusion-writer',
|
|
253
|
+
message: `Error processing ${file}: ${err.message}`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { processed: batch.length, approved, rejected };
|
|
259
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
// ─── Robust PATH-Expansion for LaunchAgents & background processes ─────────────
|
|
7
|
+
try {
|
|
8
|
+
const HOME = os.homedir();
|
|
9
|
+
const commonPaths = [
|
|
10
|
+
'/opt/homebrew/bin',
|
|
11
|
+
'/usr/local/bin',
|
|
12
|
+
path.join(HOME, '.local/bin'),
|
|
13
|
+
path.join(HOME, '.nvm/versions/node/v24.12.0/bin'),
|
|
14
|
+
path.join(HOME, '.npm-global/bin'),
|
|
15
|
+
path.join(HOME, 'bin'),
|
|
16
|
+
'/usr/bin',
|
|
17
|
+
'/bin',
|
|
18
|
+
'/usr/sbin',
|
|
19
|
+
'/sbin'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const nvmVersionsDir = path.join(HOME, '.nvm', 'versions', 'node');
|
|
23
|
+
if (fs.existsSync(nvmVersionsDir)) {
|
|
24
|
+
const versions = fs.readdirSync(nvmVersionsDir);
|
|
25
|
+
for (const v of versions) {
|
|
26
|
+
commonPaths.unshift(path.join(nvmVersionsDir, v, 'bin'));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const activePaths = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
|
|
31
|
+
const uniquePaths = Array.from(new Set([...activePaths, ...commonPaths])).filter(p => fs.existsSync(p));
|
|
32
|
+
process.env.PATH = uniquePaths.join(path.delimiter);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Silent fallback
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Central Configuration Schema
|
|
40
|
+
* Uses Zod to validate and parse environment variables into a strongly typed system.
|
|
41
|
+
*/
|
|
42
|
+
const configSchema = z.object({
|
|
43
|
+
agentDir: z.preprocess(
|
|
44
|
+
(val) => val || process.env._TR_TEST_AGENT_DIR || path.join(os.homedir(), '.agent'),
|
|
45
|
+
z.string()
|
|
46
|
+
),
|
|
47
|
+
cliAgent: z.string().optional(),
|
|
48
|
+
cliModel: z.string().optional(),
|
|
49
|
+
cliTimeout: z.preprocess((val) => val === undefined || val === '' ? 300 : parseInt(String(val), 10), z.number().int().default(300)),
|
|
50
|
+
googleApiKey: z.string().optional(),
|
|
51
|
+
embedModel: z.string().default('gemini-embedding-2'),
|
|
52
|
+
searxngBaseUrl: z.string().optional(),
|
|
53
|
+
braveApiKey: z.string().optional(),
|
|
54
|
+
braveSearchApiKey: z.string().optional(),
|
|
55
|
+
exaApiKey: z.string().optional(),
|
|
56
|
+
githubToken: z.string().optional(),
|
|
57
|
+
serperApiKey: z.string().optional(),
|
|
58
|
+
tavilyApiKey: z.string().optional(),
|
|
59
|
+
dailySearchLimit: z.preprocess((val) => val === undefined || val === '' ? 50 : parseInt(String(val), 10), z.number().int().default(50)),
|
|
60
|
+
researchCooldownMs: z.preprocess((val) => val === undefined || val === '' ? 3600000 : parseInt(String(val), 10), z.number().int().default(3600000)),
|
|
61
|
+
sessionSecret: z.string().optional(),
|
|
62
|
+
nodeEnv: z.string().default('production'),
|
|
63
|
+
port: z.preprocess((val) => val === undefined || val === '' ? 3000 : parseInt(String(val), 10), z.number().int().default(3000)),
|
|
64
|
+
host: z.string().default('127.0.0.1'),
|
|
65
|
+
display: z.string().optional(),
|
|
66
|
+
totalRecallToken: z.string().optional(),
|
|
67
|
+
trBrain: z.string().optional(),
|
|
68
|
+
trPat: z.string().optional(),
|
|
69
|
+
xdgConfigHome: z.preprocess(
|
|
70
|
+
(val) => val || path.join(os.homedir(), '.config'),
|
|
71
|
+
z.string()
|
|
72
|
+
)
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Resolve brainDir paths
|
|
76
|
+
const resolvedAgentDir = process.env.AGENT_DIR || process.env._TR_TEST_AGENT_DIR || path.join(os.homedir(), '.agent');
|
|
77
|
+
const tempGlobalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
|
|
78
|
+
|
|
79
|
+
// Auto-detect project brain if any
|
|
80
|
+
let projectBrainDir = null;
|
|
81
|
+
if (!process.env._TR_TEST_AGENT_DIR) {
|
|
82
|
+
let dir = process.cwd();
|
|
83
|
+
const homeDir = os.homedir();
|
|
84
|
+
while (dir !== path.dirname(dir)) {
|
|
85
|
+
if (dir === homeDir) break;
|
|
86
|
+
const candidate = path.join(dir, '.agent', 'skills', 'total-recall');
|
|
87
|
+
if (fs.existsSync(path.join(candidate, 'SKILL.md'))) {
|
|
88
|
+
projectBrainDir = candidate;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
dir = path.dirname(dir);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Load secrets from project or global
|
|
96
|
+
let secrets = {};
|
|
97
|
+
try {
|
|
98
|
+
const pathsToCheck = [];
|
|
99
|
+
if (projectBrainDir) {
|
|
100
|
+
pathsToCheck.push(path.join(projectBrainDir, 'config', 'secrets.enc'));
|
|
101
|
+
}
|
|
102
|
+
pathsToCheck.push(path.join(tempGlobalBrainDir, 'config', 'secrets.enc'));
|
|
103
|
+
pathsToCheck.push(path.join(resolvedAgentDir, 'skills', 'total-recall', 'config', 'secrets.enc'));
|
|
104
|
+
|
|
105
|
+
for (const p of pathsToCheck) {
|
|
106
|
+
if (fs.existsSync(p)) {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8') || '{}');
|
|
109
|
+
if (Object.keys(parsed).length > 0) {
|
|
110
|
+
secrets = parsed;
|
|
111
|
+
break; // Found valid secrets!
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Ignore
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Capture raw configuration values from process.env and secrets.enc
|
|
121
|
+
const rawConfig = {
|
|
122
|
+
agentDir: process.env.AGENT_DIR,
|
|
123
|
+
cliAgent: process.env.TR_CLI_AGENT,
|
|
124
|
+
cliModel: process.env.TR_CLI_MODEL,
|
|
125
|
+
cliTimeout: process.env.TR_CLI_TIMEOUT,
|
|
126
|
+
googleApiKey: process.env.GOOGLE_API_KEY || secrets.google_api_key,
|
|
127
|
+
embedModel: process.env.TR_EMBED_MODEL,
|
|
128
|
+
searxngBaseUrl: process.env.SEARXNG_BASE_URL,
|
|
129
|
+
braveApiKey: process.env.BRAVE_SEARCH_API_KEY || process.env.BRAVE_API_KEY || secrets.brave_api_key,
|
|
130
|
+
braveSearchApiKey: process.env.BRAVE_SEARCH_API_KEY || secrets.brave_api_key,
|
|
131
|
+
exaApiKey: process.env.EXA_API_KEY || secrets.exa_api_key,
|
|
132
|
+
githubToken: process.env.GITHUB_TOKEN || secrets.github_token,
|
|
133
|
+
serperApiKey: process.env.SERPER_API_KEY || secrets.serper_api_key,
|
|
134
|
+
tavilyApiKey: process.env.TAVILY_API_KEY || secrets.tavily_api_key,
|
|
135
|
+
dailySearchLimit: process.env.TR_DAILY_SEARCH_LIMIT,
|
|
136
|
+
researchCooldownMs: process.env.RESEARCH_COOLDOWN_MS,
|
|
137
|
+
sessionSecret: process.env.SESSION_SECRET,
|
|
138
|
+
nodeEnv: process.env.NODE_ENV,
|
|
139
|
+
port: process.env.PORT,
|
|
140
|
+
host: process.env.HOST,
|
|
141
|
+
display: process.env.DISPLAY,
|
|
142
|
+
totalRecallToken: process.env.TOTAL_RECALL_TOKEN,
|
|
143
|
+
trBrain: process.env.TR_BRAIN,
|
|
144
|
+
trPat: process.env.TR_PAT,
|
|
145
|
+
xdgConfigHome: process.env.XDG_CONFIG_HOME
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let validated;
|
|
149
|
+
try {
|
|
150
|
+
validated = configSchema.parse(rawConfig);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const issues = err.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
|
|
153
|
+
throw new Error(`Configuration validation failed: ${issues}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const config = Object.freeze(validated);
|
|
157
|
+
|
|
158
|
+
export default config;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* brainDir — The Total Recall meta-skill directory.
|
|
162
|
+
* This IS the user's brain. ALL user data (memory-vault, config, sessions,
|
|
163
|
+
* scheduler, memory-derived, memory-inbox, logs, .backups) lives inside here.
|
|
164
|
+
* Backup backs up this one directory and gets everything.
|
|
165
|
+
*/
|
|
166
|
+
export const brainDir = path.join(config.agentDir, 'skills', 'total-recall');
|
|
167
|
+
|
|
168
|
+
export const {
|
|
169
|
+
agentDir,
|
|
170
|
+
cliAgent,
|
|
171
|
+
cliModel,
|
|
172
|
+
cliTimeout,
|
|
173
|
+
googleApiKey,
|
|
174
|
+
embedModel,
|
|
175
|
+
searxngBaseUrl,
|
|
176
|
+
braveApiKey,
|
|
177
|
+
braveSearchApiKey,
|
|
178
|
+
exaApiKey,
|
|
179
|
+
githubToken,
|
|
180
|
+
serperApiKey,
|
|
181
|
+
tavilyApiKey,
|
|
182
|
+
dailySearchLimit,
|
|
183
|
+
researchCooldownMs,
|
|
184
|
+
sessionSecret,
|
|
185
|
+
nodeEnv,
|
|
186
|
+
port,
|
|
187
|
+
host,
|
|
188
|
+
display,
|
|
189
|
+
totalRecallToken,
|
|
190
|
+
trBrain,
|
|
191
|
+
trPat,
|
|
192
|
+
xdgConfigHome
|
|
193
|
+
} = config;
|
|
194
|
+
|
|
195
|
+
export function getEnvVar(name) {
|
|
196
|
+
return process.env[name];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Layered Brain Resolution ───────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Global brain — always at ~/.agent/skills/total-recall/
|
|
203
|
+
* Holds identity: universal preferences, invariants, corrections, coding principles.
|
|
204
|
+
*/
|
|
205
|
+
export const globalAgentDir = path.join(os.homedir(), '.agent');
|
|
206
|
+
export const globalBrainDir = path.join(globalAgentDir, 'skills', 'total-recall');
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Detect a project-level brain by walking up from startDir looking for
|
|
210
|
+
* .agent/skills/total-recall/. Skips the home directory (that's the global brain).
|
|
211
|
+
*
|
|
212
|
+
* @param {string} [startDir=process.cwd()] - Directory to start searching from
|
|
213
|
+
* @returns {{ agentDir: string, brainDir: string, projectRoot: string } | null}
|
|
214
|
+
*/
|
|
215
|
+
export function detectProjectBrain(startDir = process.cwd()) {
|
|
216
|
+
// In test mode, skip project detection
|
|
217
|
+
if (process.env._TR_TEST_AGENT_DIR) return null;
|
|
218
|
+
|
|
219
|
+
let dir = startDir;
|
|
220
|
+
const homeDir = os.homedir();
|
|
221
|
+
while (dir !== path.dirname(dir)) {
|
|
222
|
+
if (dir === homeDir) break; // don't detect global as project
|
|
223
|
+
const candidate = path.join(dir, '.agent', 'skills', 'total-recall');
|
|
224
|
+
if (fs.existsSync(path.join(candidate, 'SKILL.md'))) {
|
|
225
|
+
return {
|
|
226
|
+
agentDir: path.join(dir, '.agent'),
|
|
227
|
+
brainDir: candidate,
|
|
228
|
+
projectRoot: dir,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
dir = path.dirname(dir);
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get both brain layers for the current context.
|
|
238
|
+
*
|
|
239
|
+
* @returns {{ global: { agentDir: string, brainDir: string, layer: 'global' },
|
|
240
|
+
* project: { agentDir: string, brainDir: string, projectRoot: string, layer: 'project' } | null }}
|
|
241
|
+
*/
|
|
242
|
+
export function getActiveBrains() {
|
|
243
|
+
const global = { agentDir: globalAgentDir, brainDir: globalBrainDir, layer: 'global' };
|
|
244
|
+
const project = detectProjectBrain();
|
|
245
|
+
return {
|
|
246
|
+
global,
|
|
247
|
+
project: project ? { ...project, layer: 'project' } : null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolve which brain to use for a given operation.
|
|
253
|
+
*
|
|
254
|
+
* @param {'global' | 'project' | 'auto'} layer - Explicit layer choice or auto-detect
|
|
255
|
+
* @param {string} [category] - Memory category (used for auto-detect heuristic)
|
|
256
|
+
* @returns {{ agentDir: string, brainDir: string, layer: 'global' | 'project' }}
|
|
257
|
+
*/
|
|
258
|
+
export function resolveBrainLayer(layer = 'auto', category) {
|
|
259
|
+
const { global, project } = getActiveBrains();
|
|
260
|
+
|
|
261
|
+
if (layer === 'global') return global;
|
|
262
|
+
if (layer === 'project') {
|
|
263
|
+
if (!project) {
|
|
264
|
+
throw new Error('No project brain found. Run `npx total-recall init --project` to create one.');
|
|
265
|
+
}
|
|
266
|
+
return project;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Auto-detect by category heuristic
|
|
270
|
+
if (project && category) {
|
|
271
|
+
const projectCategories = new Set(['fact', 'concept', 'pattern', 'anti-pattern', 'decision']);
|
|
272
|
+
if (projectCategories.has(category)) return project;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Default: project if exists, else global
|
|
276
|
+
return project || global;
|
|
277
|
+
}
|