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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/antigravity.mjs +212 -0
  4. package/bin/init.mjs +36 -0
  5. package/bin/oci-sniper.sh +69 -0
  6. package/bin/total-recall.mjs +229 -0
  7. package/docs/ARCHITECTURE.md +251 -0
  8. package/docs/IDE_INTEGRATION.md +112 -0
  9. package/package.json +71 -0
  10. package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
  11. package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
  12. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
  13. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
  14. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
  15. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
  16. package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
  17. package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
  18. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
  19. package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
  20. package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
  21. package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
  22. package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
  23. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
  24. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
  25. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
  26. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
  27. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
  28. package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
  29. package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
  30. package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
  31. package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
  32. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
  33. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
  34. package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
  35. package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
  36. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
  37. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
  38. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
  39. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
  40. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
  41. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
  42. package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
  43. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
  44. package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
  45. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
  46. package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
  47. package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
  48. package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
  49. package/src/cli/agent-dir.mjs +105 -0
  50. package/src/cli/backup.mjs +313 -0
  51. package/src/cli/brain.mjs +224 -0
  52. package/src/cli/chat.mjs +74 -0
  53. package/src/cli/config.mjs +247 -0
  54. package/src/cli/connect.mjs +872 -0
  55. package/src/cli/daemon.mjs +228 -0
  56. package/src/cli/deploy-ui.mjs +1793 -0
  57. package/src/cli/deploy.mjs +1726 -0
  58. package/src/cli/doctor.mjs +254 -0
  59. package/src/cli/dream.mjs +80 -0
  60. package/src/cli/forget.mjs +92 -0
  61. package/src/cli/friction.mjs +21 -0
  62. package/src/cli/generate-pat.mjs +65 -0
  63. package/src/cli/hash-password.mjs +22 -0
  64. package/src/cli/help.mjs +153 -0
  65. package/src/cli/import-rules.mjs +102 -0
  66. package/src/cli/ingest/index.mjs +318 -0
  67. package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
  68. package/src/cli/ingest/parsers/google-keep.mjs +147 -0
  69. package/src/cli/ingest/parsers/search-history.mjs +142 -0
  70. package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
  71. package/src/cli/ingest/utils/dedup.mjs +69 -0
  72. package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
  73. package/src/cli/ingest.mjs +102 -0
  74. package/src/cli/init.mjs +736 -0
  75. package/src/cli/integration-dispatcher.mjs +128 -0
  76. package/src/cli/lint.mjs +166 -0
  77. package/src/cli/map.mjs +194 -0
  78. package/src/cli/migrate.mjs +87 -0
  79. package/src/cli/rebuild.mjs +88 -0
  80. package/src/cli/recall.mjs +219 -0
  81. package/src/cli/relay.mjs +518 -0
  82. package/src/cli/remember.mjs +306 -0
  83. package/src/cli/research.mjs +381 -0
  84. package/src/cli/reset-password.mjs +75 -0
  85. package/src/cli/restore.mjs +135 -0
  86. package/src/cli/setup.mjs +616 -0
  87. package/src/cli/share.mjs +180 -0
  88. package/src/cli/skill.mjs +313 -0
  89. package/src/cli/snapshot.mjs +108 -0
  90. package/src/cli/start.mjs +63 -0
  91. package/src/cli/status.mjs +296 -0
  92. package/src/cli/sync.mjs +383 -0
  93. package/src/cli/uninstall.mjs +613 -0
  94. package/src/cli/upgrade.mjs +141 -0
  95. package/src/cli/wizard.html +3682 -0
  96. package/src/core/blackboard.mjs +54 -0
  97. package/src/core/clarity-rewriter.mjs +770 -0
  98. package/src/core/conclusion-writer.mjs +259 -0
  99. package/src/core/config.mjs +277 -0
  100. package/src/core/conflict-detector.mjs +536 -0
  101. package/src/core/crypto.mjs +107 -0
  102. package/src/core/daemon-control.mjs +158 -0
  103. package/src/core/daemon-loop.mjs +364 -0
  104. package/src/core/dream.mjs +347 -0
  105. package/src/core/drift-detector.mjs +143 -0
  106. package/src/core/embeddings.mjs +632 -0
  107. package/src/core/emergency-alerts.mjs +137 -0
  108. package/src/core/evolution.mjs +107 -0
  109. package/src/core/fact-seeker.mjs +1602 -0
  110. package/src/core/friction.mjs +95 -0
  111. package/src/core/import-rules.mjs +308 -0
  112. package/src/core/inference-engine.mjs +367 -0
  113. package/src/core/logger.mjs +75 -0
  114. package/src/core/memory-layers.mjs +96 -0
  115. package/src/core/migrate.mjs +116 -0
  116. package/src/core/notifications.mjs +77 -0
  117. package/src/core/operation-validator.mjs +272 -0
  118. package/src/core/optimizer.mjs +330 -0
  119. package/src/core/pattern_detector.mjs +113 -0
  120. package/src/core/post-mortem.mjs +413 -0
  121. package/src/core/protect-instructions.mjs +148 -0
  122. package/src/core/quick-capture.mjs +145 -0
  123. package/src/core/research-queue.mjs +431 -0
  124. package/src/core/research.mjs +461 -0
  125. package/src/core/runtime.mjs +515 -0
  126. package/src/core/sandbox.mjs +163 -0
  127. package/src/core/scheduler.mjs +569 -0
  128. package/src/core/schema.mjs +333 -0
  129. package/src/core/search.mjs +219 -0
  130. package/src/core/semantic-index.mjs +276 -0
  131. package/src/core/session-watcher.mjs +850 -0
  132. package/src/core/snapshot.mjs +133 -0
  133. package/src/core/source-adapters.mjs +1038 -0
  134. package/src/core/steering.mjs +137 -0
  135. package/src/core/surface.mjs +444 -0
  136. package/src/core/task_runner.mjs +125 -0
  137. package/src/core/tts.mjs +142 -0
  138. package/src/core/usage-tracker.mjs +349 -0
  139. package/src/core/vault-cache.mjs +76 -0
  140. package/src/core/vault.mjs +273 -0
  141. package/src/core/watchdog.mjs +236 -0
  142. package/src/server/api.mjs +944 -0
  143. package/src/server/auth.mjs +410 -0
  144. package/src/server/index.mjs +868 -0
  145. package/src/server/keys.mjs +181 -0
  146. package/src/server/rest.mjs +1652 -0
  147. package/src/server/route-manifest.json +77 -0
  148. package/src/server/routes/_shared.mjs +93 -0
  149. package/src/server/routes/keys.mjs +55 -0
  150. package/src/server/routes/memory.mjs +249 -0
  151. package/src/server/routes/sessions.mjs +336 -0
  152. package/src/server/routes/share.mjs +138 -0
  153. package/src/server/tools.mjs +618 -0
  154. package/templates/Caddyfile +37 -0
  155. package/templates/com.totalrecall.backup.plist +42 -0
  156. package/templates/com.totalrecall.daemon.plist +40 -0
  157. package/templates/com.totalrecall.duckdns.plist +23 -0
  158. package/templates/com.totalrecall.server.plist +46 -0
  159. package/templates/default-config/budget.yml +6 -0
  160. package/templates/default-config/runtime.yml +11 -0
  161. package/templates/default-config/security.yml +53 -0
  162. package/templates/default-config/voice.yml +34 -0
  163. package/templates/obsidian-queries/conflicts.md +26 -0
  164. package/templates/obsidian-queries/daily.md +17 -0
  165. package/templates/obsidian-queries/index.md +38 -0
  166. package/templates/obsidian-queries/skills.md +14 -0
  167. package/templates/onboarding-interview.md +72 -0
  168. package/templates/skill_engineering.md +40 -0
  169. package/templates/total-recall-daemon.service +21 -0
  170. package/templates/total-recall-server.service +20 -0
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'node:crypto';
4
+ import natural from 'natural';
5
+ import { atomicWrite } from './vault.mjs';
6
+
7
+ const JACCARD_THRESHOLD = 0.75;
8
+
9
+ /**
10
+ * Jaccard similarity for two sets of tokens.
11
+ */
12
+ function jaccard(setA, setB) {
13
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
14
+ const union = new Set([...setA, ...setB]);
15
+ if (union.size === 0) return 0;
16
+ return intersection.size / union.size;
17
+ }
18
+
19
+ /**
20
+ * Layer 1: Ontology SPO collision check.
21
+ * Checks if Subject, Predicate, and Object match but modality/sentiment flip.
22
+ */
23
+ export function checkLayer1(candidate, existing) {
24
+ if (
25
+ candidate.subject === existing.subject &&
26
+ candidate.predicate === existing.predicate &&
27
+ candidate.object === existing.object
28
+ ) {
29
+ const polarityFlip = candidate.sentiment_polarity !== existing.sentiment_polarity ||
30
+ candidate.modality !== existing.modality;
31
+ if (polarityFlip) {
32
+ return {
33
+ collision: true,
34
+ reason: `Layer 1: Polarity flip on SPO (${candidate.subject} -> ${candidate.predicate} -> ${candidate.object})`
35
+ };
36
+ }
37
+ }
38
+ return { collision: false };
39
+ }
40
+
41
+ /**
42
+ * Layer 2: Jaccard + Cosine collision check on node body.
43
+ */
44
+ export function checkLayer2(candidate, existing) {
45
+ const tokenizer = new natural.WordTokenizer();
46
+ const tokensA = new Set(tokenizer.tokenize((candidate.body || '').toLowerCase()));
47
+ const tokensB = new Set(tokenizer.tokenize((existing.body || '').toLowerCase()));
48
+
49
+ const score = jaccard(tokensA, tokensB);
50
+
51
+ if (score >= JACCARD_THRESHOLD) {
52
+ const polarityFlip = candidate.sentiment_polarity !== existing.sentiment_polarity;
53
+ if (polarityFlip) {
54
+ return {
55
+ collision: true,
56
+ similarity: score,
57
+ reason: `Layer 2: Jaccard similarity ${score.toFixed(2)} >= ${JACCARD_THRESHOLD} with polarity flip`
58
+ };
59
+ }
60
+ }
61
+ return { collision: false };
62
+ }
63
+
64
+ /**
65
+ * Run all collision checks for a candidate against the vault.
66
+ */
67
+ export function detectConflicts(candidate, existingNodes) {
68
+ const conflicts = [];
69
+
70
+ for (const existing of existingNodes) {
71
+ if (existing.status !== 'active') continue;
72
+
73
+ const l1 = checkLayer1(candidate, existing);
74
+ if (l1.collision) {
75
+ conflicts.push({
76
+ type: 'conflict',
77
+ conflict_id: `conflict-${Date.now()}-${crypto.randomInt(0, 1000)}`,
78
+ status: 'pending',
79
+ new_slug: candidate.slug,
80
+ existing_slug: existing.slug,
81
+ similarity: 1.0,
82
+ polarity_flip: true,
83
+ detected_at: new Date().toISOString(),
84
+ reason: l1.reason,
85
+ resolution: null,
86
+ resolved_at: null
87
+ });
88
+ continue;
89
+ }
90
+
91
+ const l2 = checkLayer2(candidate, existing);
92
+ if (l2.collision) {
93
+ conflicts.push({
94
+ type: 'conflict',
95
+ conflict_id: `conflict-${Date.now()}-${crypto.randomInt(0, 1000)}`,
96
+ status: 'pending',
97
+ new_slug: candidate.slug,
98
+ existing_slug: existing.slug,
99
+ similarity: l2.similarity,
100
+ polarity_flip: true,
101
+ detected_at: new Date().toISOString(),
102
+ reason: l2.reason,
103
+ resolution: null,
104
+ resolved_at: null
105
+ });
106
+ }
107
+ }
108
+
109
+ return conflicts;
110
+ }
111
+
112
+ /**
113
+ * Write conflict record to the memory-inbox/conflicts/ quarantine directory.
114
+ */
115
+ export function quarantineConflict(conflict, conflictsDir) {
116
+ if (!fs.existsSync(conflictsDir)) {
117
+ fs.mkdirSync(conflictsDir, { recursive: true });
118
+ }
119
+ const filePath = path.join(conflictsDir, `${conflict.conflict_id}.json`);
120
+ atomicWrite(filePath, JSON.stringify(conflict, null, 2));
121
+ }
122
+
123
+ /**
124
+ * Resolve a conflict.
125
+ */
126
+ export function resolveConflict(conflictId, resolution, conflictsDir) {
127
+ const filePath = path.join(conflictsDir, `${conflictId}.json`);
128
+ if (!fs.existsSync(filePath)) throw new Error('Conflict not found');
129
+
130
+ const conflict = JSON.parse(fs.readFileSync(filePath, 'utf8'));
131
+ conflict.status = 'resolved';
132
+ conflict.resolution = resolution;
133
+ conflict.resolved_at = new Date().toISOString();
134
+
135
+ atomicWrite(filePath, JSON.stringify(conflict, null, 2));
136
+ return conflict;
137
+ }
@@ -0,0 +1,444 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { loadNodes, loadSkills, atomicWrite } from './vault.mjs';
4
+ import {
5
+ buildMemoryLayerIndex,
6
+ inferMemoryLayer,
7
+ } from './memory-layers.mjs';
8
+
9
+ /**
10
+ * Extract [[slug]] wikilink references from body text.
11
+ * Native TR link resolution; Obsidian renders them as graph edges.
12
+ */
13
+ export function extractWikilinks(body) {
14
+ if (!body) return [];
15
+ const matches = body.match(/\[\[([^\]]+)\]\]/g) || [];
16
+ return [...new Set(matches.map(m => m.slice(2, -2).split('|')[0].trim()))];
17
+ }
18
+
19
+ /**
20
+ * Generate an Obsidian Canvas JSON file from active vault nodes.
21
+ * Written to memory-vault/graph.canvas; also readable by /api/graph.
22
+ */
23
+ function generateCanvas(nodes, vaultDir) {
24
+ const active = nodes.filter(n => n.status === 'active');
25
+ if (active.length === 0) return;
26
+
27
+ const CARD_W = 240, CARD_H = 60, GAP_X = 60, GAP_Y = 30;
28
+ const cols = Math.max(1, Math.ceil(Math.sqrt(active.length)));
29
+
30
+ const canvasNodes = active.map((n, i) => ({
31
+ id: n.slug,
32
+ x: (i % cols) * (CARD_W + GAP_X),
33
+ y: Math.floor(i / cols) * (CARD_H + GAP_Y),
34
+ width: CARD_W,
35
+ height: CARD_H,
36
+ type: 'text',
37
+ text: `**${n.title}**\n_${n.category}_`
38
+ }));
39
+
40
+ const slugSet = new Set(canvasNodes.map(n => n.id));
41
+ const seen = new Set();
42
+ const canvasEdges = [];
43
+
44
+ for (const n of active) {
45
+ const targets = [...(n.related || []), ...extractWikilinks(n.body || '')];
46
+ for (const target of targets) {
47
+ if (!slugSet.has(target) || target === n.slug) continue;
48
+ const key = `${n.slug}→${target}`;
49
+ if (seen.has(key)) continue;
50
+ seen.add(key);
51
+ canvasEdges.push({
52
+ id: `e${canvasEdges.length}`,
53
+ fromNode: n.slug,
54
+ toNode: target,
55
+ fromSide: 'right',
56
+ toSide: 'left'
57
+ });
58
+ }
59
+ }
60
+
61
+ const canvasPath = path.join(vaultDir, 'graph.canvas');
62
+ atomicWrite(canvasPath, JSON.stringify({ nodes: canvasNodes, edges: canvasEdges }, null, 2));
63
+ }
64
+
65
+ // ─── Legacy injection markers (kept for cleanup of old files) ───
66
+ const INJECTION_BEGIN = '<!-- BEGIN INJECTED MEMORY: do not edit by hand; rebuilt by total-recall surface -->';
67
+ const INJECTION_END = '<!-- END INJECTED MEMORY -->';
68
+
69
+ export function replaceFirstManagedInjectionBlock(raw, injectionBlock) {
70
+ const chunks = raw.match(/[^\n]*(?:\n|$)/g) || [];
71
+ let inFence = false;
72
+ let offset = 0;
73
+
74
+ for (let i = 0; i < chunks.length; i++) {
75
+ const chunk = chunks[i];
76
+ if (chunk === '') continue;
77
+ const line = chunk.endsWith('\n') ? chunk.slice(0, -1) : chunk;
78
+ const trimmed = line.trim();
79
+
80
+ if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
81
+ inFence = !inFence;
82
+ offset += chunk.length;
83
+ continue;
84
+ }
85
+
86
+ if (!inFence && line === INJECTION_BEGIN) {
87
+ let endOffset = offset + chunk.length;
88
+ for (let j = i + 1; j < chunks.length; j++) {
89
+ endOffset += chunks[j].length;
90
+ const endLine = chunks[j].endsWith('\n') ? chunks[j].slice(0, -1) : chunks[j];
91
+ if (endLine === INJECTION_END) {
92
+ return `${raw.slice(0, offset)}${injectionBlock}\n${raw.slice(endOffset)}`;
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+
98
+ offset += chunk.length;
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ const DIRECTIVES_BEGIN = '<!-- BEGIN INJECTED ACTIVE DIRECTIVES: do not edit by hand; rebuilt by total-recall surface -->';
105
+ const DIRECTIVES_END = '<!-- END INJECTED ACTIVE DIRECTIVES -->';
106
+
107
+ function extractRuleContent(filePath) {
108
+ if (!fs.existsSync(filePath)) return '';
109
+ const content = fs.readFileSync(filePath, 'utf8');
110
+ const parts = content.split('---');
111
+ if (parts.length >= 3) {
112
+ return parts.slice(2).join('---').trim();
113
+ }
114
+ return content.trim();
115
+ }
116
+
117
+ export function buildRulesBlock(skillsDir, nodes = [], { vaultDir, consumer = 'ide' } = {}) {
118
+ let combined;
119
+
120
+ if (consumer === 'api') {
121
+ // API consumers don't need CLI quickstart docs
122
+ combined = `## Total Recall — Active Memory Context\n\nYour memories and rules are loaded from the active brain vault.\n`;
123
+ } else {
124
+ // CLI quickstart is always included so agents know TR is installed
125
+ combined = `## Total Recall — Sovereign Memory System (Installed)
126
+
127
+ You have Total Recall installed. Use these CLI commands to remember and recall information.
128
+
129
+ ### npx total-recall remember <category> "<content>" [options]
130
+ Save rules, preferences, corrections, and facts to permanent memory.
131
+
132
+ **Categories:** invariant, preference, correction, fact, concept, pattern, anti-pattern, decision, lore
133
+
134
+ **Options:**
135
+ --tags, -t <list> Comma-separated tags (e.g. "config,server")
136
+ --importance, -i <1-5> Importance level (default: 3)
137
+ --priority, -p <level> absolute | high | normal | low (default: normal)
138
+ --modality, -m <type> must | must_not | should | should_not | descriptive | preference
139
+ --confidence, -c <0-1> Confidence level (default: 1.0)
140
+ --slug <custom-slug> Custom kebab-case slug
141
+ --title <custom-title> Custom human-readable title
142
+ --status <state> active | draft | archived (default: active)
143
+ --related <list> Comma-separated related slugs
144
+ --global Save to global brain (identity layer)
145
+ --project Save to project brain (context layer)
146
+
147
+ **Examples:**
148
+ npx total-recall remember invariant "Never run tsc directly." --importance 5 --priority absolute
149
+ npx total-recall remember preference "Always use single quotes." --tags "style,js"
150
+ npx total-recall remember fact "The server runs on port 3000." --importance 4
151
+ npx total-recall remember fact "Uses Drizzle ORM" --project
152
+
153
+ ### npx total-recall recall "<query>" [options]
154
+ Semantic search across rules, facts, and session history.
155
+
156
+ **Options:**
157
+ --top-k, -k <number> Results to return (default: 5, max: 20)
158
+ --no-sessions, -ns Exclude session chunks, vault only
159
+ --format, -f <type> text (default) or json
160
+ --category, -cat <name> Filter by SSSS category
161
+ --tags, -t <list> Filter by tags
162
+ --modality, -m <type> Filter by modality
163
+ --importance, -i <1-5> Filter by minimum importance
164
+ --global Search global brain only
165
+ --project Search project brain only
166
+
167
+ **Examples:**
168
+ npx total-recall recall "Never run tsc directly"
169
+ npx total-recall recall "Express server port" --top-k 3
170
+ npx total-recall recall "tsc" --category invariants --modality must
171
+
172
+ ### npx total-recall forget <slug> [options]
173
+ Delete a memory node by slug. Auto-recompiles surfaces and propagates to project brains.
174
+
175
+ **Options:**
176
+ --global Delete from global brain
177
+ --project Delete from project brain
178
+ --no-compile Skip auto-recompilation after deletion
179
+
180
+ **Examples:**
181
+ npx total-recall forget old-invariant-slug
182
+ npx total-recall forget stale-fact --global
183
+ npx total-recall forget project-specific-node --project
184
+
185
+ ### npx total-recall help <topic>
186
+ Query interactive local documentation, VFS specifications, and command references.
187
+
188
+ **Options:**
189
+ --json, -j Emit machine-readable JSON (ideal for programmatic retrieval)
190
+
191
+ **Examples:**
192
+ npx total-recall help connect
193
+ npx total-recall help architecture
194
+ npx total-recall help ssss
195
+
196
+ ### npx total-recall --help
197
+ Show all available commands.
198
+ `;
199
+ }
200
+
201
+ // 1. Filter expired rules and auto-archive them
202
+ const now = new Date();
203
+ const isExpired = (n) => n.expires_at && new Date(n.expires_at) <= now;
204
+
205
+ const expiredNodes = nodes.filter(n => n.status === 'active' && isExpired(n));
206
+ for (const expired of expiredNodes) {
207
+ expired.status = 'deprecated';
208
+ console.error(`⏰ Auto-archived expired rule: ${expired.slug}`);
209
+ if (vaultDir) {
210
+ try {
211
+ const nodePath = path.join(vaultDir, expired.category, `${expired.slug}.md`);
212
+ if (fs.existsSync(nodePath)) {
213
+ let content = fs.readFileSync(nodePath, 'utf8');
214
+ content = content.replace(/^status:\s*active$/m, 'status: deprecated');
215
+ atomicWrite(nodePath, content);
216
+ }
217
+ } catch { /* non-fatal: archive is best-effort */ }
218
+ }
219
+ }
220
+
221
+ // 2. Group active (non-expired) rules from SSSS vault nodes
222
+ const invariants = nodes.filter(n => n.category === 'invariants' && n.status === 'active' && !isExpired(n));
223
+ const preferences = nodes.filter(n => n.category === 'preferences' && n.status === 'active' && !isExpired(n));
224
+ const corrections = nodes.filter(n => n.category === 'anti-patterns' && n.status === 'active' && !isExpired(n));
225
+
226
+ const formatNodes = (list) => {
227
+ return list.map(n => {
228
+ const text = n.body || n.content || '';
229
+ return text.startsWith('-') ? text : `- ${text}`;
230
+ }).join('\n');
231
+ };
232
+
233
+ if (invariants.length > 0) {
234
+ combined += `\n\n## Invariant Rules\n\n${formatNodes(invariants)}`;
235
+ }
236
+
237
+ if (preferences.length > 0) {
238
+ combined += `\n\n## User Preferences\n\n${formatNodes(preferences)}`;
239
+ }
240
+
241
+ if (corrections.length > 0) {
242
+ combined += `\n\n## Corrections\n\n${formatNodes(corrections)}`;
243
+ }
244
+
245
+ // 2. Append legacy rule sheet files if they exist
246
+ if (skillsDir) {
247
+ const rulesDir = path.join(skillsDir, 'total-recall', 'rules');
248
+ const legacyInvariants = extractRuleContent(path.join(rulesDir, 'invariants.md'));
249
+ const legacyPreferences = extractRuleContent(path.join(rulesDir, 'preferences.md'));
250
+ const legacyCorrections = extractRuleContent(path.join(rulesDir, 'corrections.md'));
251
+
252
+ if (legacyInvariants) {
253
+ combined += `\n\n${legacyInvariants}`;
254
+ }
255
+ if (legacyPreferences) {
256
+ combined += `\n\n${legacyPreferences}`;
257
+ }
258
+ if (legacyCorrections) {
259
+ combined += `\n\n${legacyCorrections}`;
260
+ }
261
+ }
262
+
263
+ return combined.trim();
264
+ }
265
+
266
+ function injectDirectives(fileContent, rulesBlock) {
267
+ const beginIdx = fileContent.indexOf(DIRECTIVES_BEGIN);
268
+ const endIdx = fileContent.indexOf(DIRECTIVES_END);
269
+
270
+ if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
271
+ return fileContent.slice(0, beginIdx) + DIRECTIVES_BEGIN + '\n' + rulesBlock + '\n' + DIRECTIVES_END + fileContent.slice(endIdx + DIRECTIVES_END.length);
272
+ }
273
+
274
+ let content = fileContent.trimEnd();
275
+ const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.';
276
+ if (!content.includes('SKILL.md')) {
277
+ content = baseline + '\n\n' + content;
278
+ }
279
+
280
+ return content.trimEnd() + '\n\n' + DIRECTIVES_BEGIN + '\n' + rulesBlock + '\n' + DIRECTIVES_END + '\n';
281
+ }
282
+
283
+ /**
284
+ * Write or update a platform instruction shim with the pointer and active rules.
285
+ */
286
+ function writeShim(shimPath, skillsDir, nodes = [], { vaultDir } = {}) {
287
+ const shimDir = path.dirname(shimPath);
288
+ const rulesBlock = buildRulesBlock(skillsDir, nodes, { vaultDir });
289
+ const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.\n';
290
+ const fullContent = `${baseline}\n${DIRECTIVES_BEGIN}\n${rulesBlock}\n${DIRECTIVES_END}\n`;
291
+
292
+ try {
293
+ if (fs.existsSync(shimPath)) {
294
+ const stat = fs.lstatSync(shimPath);
295
+ if (stat.isSymbolicLink()) {
296
+ fs.unlinkSync(shimPath);
297
+ atomicWrite(shimPath, fullContent);
298
+ } else {
299
+ const raw = fs.readFileSync(shimPath, 'utf8');
300
+ let cleaned = raw;
301
+ if (raw.includes(INJECTION_BEGIN)) {
302
+ const replaced = replaceFirstManagedInjectionBlock(raw, '');
303
+ if (replaced !== null) cleaned = replaced;
304
+ }
305
+
306
+ const updated = injectDirectives(cleaned, rulesBlock);
307
+ atomicWrite(shimPath, updated);
308
+ }
309
+ } else {
310
+ if (!fs.existsSync(shimDir)) {
311
+ fs.mkdirSync(shimDir, { recursive: true });
312
+ }
313
+ atomicWrite(shimPath, fullContent);
314
+ }
315
+ } catch (err) {
316
+ // Ignore permission errors
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Map of client names → shim file paths they require.
322
+ */
323
+ const CLIENT_SHIMS = {
324
+ cursor: ['.cursorrules'],
325
+ 'claude-code': ['CLAUDE.md', '.clauderules'],
326
+ antigravity: ['AGENTS.md'],
327
+ gemini: ['GEMINI.md'],
328
+ codex: ['CODEX.md', '.codexrules'],
329
+ vscode: ['.github/copilot-instructions.md', '.vscode/copilot-instructions.md'],
330
+ pi: ['AGENTS.md'],
331
+ aider: ['.aider.rules.md'],
332
+ windsurf: ['.windsurfrules', 'WINDSURF.md'],
333
+ };
334
+
335
+ /**
336
+ * Read the set of connected IDE clients from config/clients.json.
337
+ * Returns a Set of client name strings, or null if the file is
338
+ * missing / empty / malformed (null signals "write all shims").
339
+ */
340
+ function readConnectedClients(clientsPath) {
341
+ try {
342
+ if (!fs.existsSync(clientsPath)) return null;
343
+ const raw = fs.readFileSync(clientsPath, 'utf8').trim();
344
+ if (!raw) return null;
345
+ const parsed = JSON.parse(raw);
346
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
347
+ return new Set(parsed.map(String));
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Compile pointers with active rules directly into root instruction shims.
355
+ *
356
+ * ALL files (including INSTRUCTIONS.md) are user-owned. TR only injects
357
+ * content between the <!-- BEGIN/END INJECTED ACTIVE DIRECTIVES --> markers,
358
+ * preserving any user-written content outside the markers.
359
+ *
360
+ * Only shims for connected clients (per config/clients.json) are written.
361
+ * If clients.json is absent or unreadable, ALL shims are written (backward compat).
362
+ * INSTRUCTIONS.md is always written as the canonical source.
363
+ */
364
+ function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir } = {}) {
365
+ const agentDir = path.dirname(instructionsFile);
366
+ const baseDir = path.basename(agentDir) === '.agent' ? path.dirname(agentDir) : agentDir;
367
+
368
+ // Always write the canonical INSTRUCTIONS.md
369
+ writeShim(path.join(baseDir, 'INSTRUCTIONS.md'), skillsDir, nodes, { vaultDir });
370
+
371
+ // Determine which client shims to write
372
+ const clientsPath = path.join(baseDir, '.agent', 'config', 'clients.json');
373
+ const connectedClients = readConnectedClients(clientsPath);
374
+
375
+ if (connectedClients === null) {
376
+ // No clients.json → backward compat: write ALL client shims
377
+ for (const files of Object.values(CLIENT_SHIMS)) {
378
+ for (const file of files) {
379
+ writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir });
380
+ }
381
+ }
382
+ } else {
383
+ // Only write shims for connected clients
384
+ for (const client of connectedClients) {
385
+ const files = CLIENT_SHIMS[client];
386
+ if (!files) continue;
387
+ for (const file of files) {
388
+ writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir });
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Main surface compilation entry point.
396
+ */
397
+ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile }) {
398
+ const nodes = loadNodes(vaultDir);
399
+
400
+ // 1. Write pointer and active rules to all instruction shims
401
+ compilePointers(instructionsFile, skillsDir, nodes, { vaultDir });
402
+
403
+ // 2. Build derived indexes (powers semantic search API)
404
+ if (!fs.existsSync(derivedDir)) {
405
+ fs.mkdirSync(derivedDir, { recursive: true });
406
+ }
407
+
408
+ const graphIndex = nodes.map(n => ({
409
+ slug: n.slug,
410
+ title: n.title,
411
+ category: n.category,
412
+ status: n.status,
413
+ confidence: n.confidence,
414
+ memory_layer: inferMemoryLayer(n)
415
+ }));
416
+ atomicWrite(path.join(derivedDir, 'graph-index.jsonl'), graphIndex.map(n => JSON.stringify(n)).join('\n'));
417
+ atomicWrite(
418
+ path.join(derivedDir, 'memory-layers.jsonl'),
419
+ buildMemoryLayerIndex(nodes).map(n => JSON.stringify(n)).join('\n')
420
+ );
421
+
422
+ // 3. Build semantic embeddings index if available (fire-and-forget)
423
+ let semanticResult = { indexed: 0, skipped: nodes.length, unavailable: true };
424
+ try {
425
+ const { buildSemanticIndex } = await import('./semantic-index.mjs');
426
+ semanticResult = await buildSemanticIndex(nodes, derivedDir);
427
+ } catch { /* semantic index is optional — never block compile */ }
428
+
429
+ // 4. Generate Obsidian Canvas (fire-and-forget)
430
+ try {
431
+ generateCanvas(nodes, vaultDir);
432
+ } catch { /* non-fatal */ }
433
+
434
+ return {
435
+ nodesProcessed: nodes.length,
436
+ skillsInjected: 0, // v2: no more skill injection
437
+ semanticIndexed: semanticResult.indexed,
438
+ semanticUnavailable: semanticResult.unavailable
439
+ };
440
+ }
441
+ // ─── Legacy exports (kept for backward compatibility) ───
442
+ export function routeNodesToSkills() { return []; }
443
+ export function injectSkills() {}
444
+ export function compileTier1(nodes, instructionsFile) { compilePointers(instructionsFile); }
@@ -0,0 +1,125 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { atomicWrite, safeStringify } from './vault.mjs';
5
+ import { logger } from './logger.mjs';
6
+
7
+ /**
8
+ * Task Runner for Total Recall
9
+ * Reads pending tasks from .agent/scheduler/queue/, sorts by priority, and executes them.
10
+ */
11
+
12
+ export function loadPendingTasks(queueDir) {
13
+ if (!fs.existsSync(queueDir)) return [];
14
+
15
+ const tasks = [];
16
+ const files = fs.readdirSync(queueDir);
17
+
18
+ for (const file of files) {
19
+ if (!file.endsWith('.md')) continue;
20
+ const filePath = path.join(queueDir, file);
21
+ try {
22
+ const raw = fs.readFileSync(filePath, 'utf8');
23
+ const { data, content } = matter(raw);
24
+ if (data.type === 'task' && data.status === 'pending') {
25
+ tasks.push({ ...data, body: content.trim(), filepath: filePath });
26
+ }
27
+ } catch (err) {
28
+ logger.warn('task-runner', `Failed to parse task ${file}`, { err: err.message });
29
+ }
30
+ }
31
+
32
+ // Sort by priority (higher priority = lower number)
33
+ return tasks.sort((a, b) => a.priority - b.priority);
34
+ }
35
+
36
+ export function updateTaskStatus(task, newStatus, error = null) {
37
+ const { filepath, body, ...frontmatter } = task;
38
+ frontmatter.status = newStatus;
39
+
40
+ if (error) {
41
+ frontmatter.error = error;
42
+ }
43
+
44
+ const raw = safeStringify(body || '', frontmatter);
45
+ atomicWrite(filepath, raw);
46
+ return { ...task, status: newStatus };
47
+ }
48
+
49
+ /**
50
+ * Executes a single task.
51
+ */
52
+ export async function executeTask(task, context) {
53
+ logger.info('task-runner', `Executing Task [P${task.priority}]: ${task.target}`);
54
+ updateTaskStatus(task, 'in_progress');
55
+
56
+ try {
57
+ // In a real implementation, this would route to specific subagents/workflows
58
+ // based on task.category. For now, we simulate execution.
59
+ switch (task.category) {
60
+ case 'skill-engineering':
61
+ await handleSkillEngineering(task, context);
62
+ break;
63
+ case 'memory-maintenance':
64
+ await handleMemoryMaintenance(task, context);
65
+ break;
66
+ case 'system2-deliberation':
67
+ await handleSystem2Deliberation(task, context);
68
+ break;
69
+ case 'proactive-research':
70
+ logger.info('task-runner', 'Handling Deep Research...');
71
+ const { handleProactiveResearch } = await import('./research.mjs');
72
+ const report = await handleProactiveResearch(task, context);
73
+ task.body = `# Deep Research Report\n\n${report}`;
74
+ break;
75
+ case 'self-evaluation':
76
+ case 'exploration':
77
+ logger.info('task-runner', `Handling ${task.category}...`);
78
+ // Simulated work
79
+ await new Promise(resolve => setTimeout(resolve, 500));
80
+ break;
81
+ default:
82
+ throw new Error(`Unknown task category: ${task.category}`);
83
+ }
84
+
85
+ updateTaskStatus(task, 'done');
86
+ logger.info('task-runner', `Completed Task: ${task.target}`);
87
+ } catch (err) {
88
+ logger.error('task-runner', `Failed Task: ${task.target}`, { err: err.message });
89
+ updateTaskStatus(task, 'failed', err.message);
90
+ }
91
+ }
92
+
93
+ async function handleSkillEngineering(task, context) {
94
+ // E.g., spawn an agent with the template to create a skill
95
+ logger.info('task-runner', `Engineering skill: ${task.target}`);
96
+ }
97
+
98
+ async function handleMemoryMaintenance(task, context) {
99
+ logger.info('task-runner', `Running memory maintenance for: ${task.target}`);
100
+ }
101
+
102
+ async function handleSystem2Deliberation(task, context) {
103
+ logger.info('task-runner', `Running System 2 deliberation for: ${task.target}`);
104
+ }
105
+
106
+ /**
107
+ * Main loop for the task runner.
108
+ */
109
+ export async function runTaskQueue(queueDir, context) {
110
+ logger.info('task-runner', 'Starting Task Runner...');
111
+ const pending = loadPendingTasks(queueDir);
112
+
113
+ if (pending.length === 0) {
114
+ logger.info('task-runner', 'No pending tasks in queue.');
115
+ return;
116
+ }
117
+
118
+ logger.info('task-runner', `Found ${pending.length} pending tasks.`);
119
+
120
+ for (const task of pending) {
121
+ await executeTask(task, context);
122
+ }
123
+
124
+ logger.info('task-runner', 'Task Runner queue empty.');
125
+ }