total-recall-brain 3.6.13 → 3.8.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.
@@ -80,6 +80,7 @@ const COMMANDS = {
80
80
  help: 'help.mjs',
81
81
  collab: 'collab.mjs',
82
82
  command: 'command.mjs',
83
+ export: 'export.mjs',
83
84
  };
84
85
  function printHelp() {
85
86
  console.log(`
@@ -122,6 +123,7 @@ function printHelp() {
122
123
  into the vault (run before compile on first install)
123
124
  ingest [--watch] Ingest IDE conversation logs (Claude Code, Codex, Gemini, etc.)
124
125
  ingest google-takeout <path> Ingest Google Takeout export data
126
+ export <path> --okf Export memory vault as an OKF-compliant bundle
125
127
  connect <client> Configure Cursor, Claude Code, Codex, UltraChat, Gemini, etc.
126
128
  sync [--watch] Pull remote brain instructions into the current workspace
127
129
  relay <cmd> Local background relay: ship IDE sessions to remote brain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.6.13",
3
+ "version": "3.8.0",
4
4
  "description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,4 @@
1
1
  ---
2
- type: skill
3
2
  name: total-recall
4
3
  description: "Use this skill as the master guide to understand the entire Total Recall Sovereign AI OS setup, VFS topologies, SSSS protocol, CLI parameter reference, troubleshooting, and automated upstream repository sync. MANDATORY: Read this file before attempting major setup modifications or diagnoses."
5
4
  ---
@@ -145,6 +144,38 @@ npx total-recall compile
145
144
  ```
146
145
  Synchronously rebuild the entire memory index (derived embeddings) and regenerate prompt instruction shims (such as `INSTRUCTIONS.md`, `GEMINI.md`, `AGENTS.md`, etc.).
147
146
 
147
+ ### 5. Ingest OKF Bundle
148
+ ```bash
149
+ npx total-recall ingest okf <path> [options]
150
+ ```
151
+ Recursively parse and ingest an Open Knowledge Format (OKF v0.1 Draft) bundle directory.
152
+ * **Options**:
153
+ * `--dry-run`: Parse and validate nodes without writing files.
154
+ * `--category <name>`: Override and force all imported nodes to a specific category.
155
+ * `--importance <1-5>`: Override importance for imported nodes.
156
+ * `--on-conflict <strategy>`: Slug conflict behavior (`overwrite`, `skip`, `warn` - default is `warn`).
157
+ * `--type-map <json>`: Custom type-to-category mapping JSON.
158
+ * *Note*: Spawns an asynchronous background surface compile process immediately on successful ingest.
159
+
160
+ ### 6. Export OKF Bundle
161
+ ```bash
162
+ npx total-recall export <path> [options]
163
+ ```
164
+ Export the memory vault into an OKF-compliant directory structure.
165
+ * **Options**:
166
+ * `--okf`: Run in OKF export mode.
167
+ * `--strip-ssss`: Strip all SSSS-specific metadata parameters from output frontmatter.
168
+ * `--format <type>`: Package output as archive (`tar.gz`).
169
+ * `--global` / `--project`: Target scope layer.
170
+
171
+ ### 7. Lint OKF Compliance
172
+ ```bash
173
+ npx total-recall lint --okf [options]
174
+ ```
175
+ Scan vault memory nodes and check for OKF metadata compliance (presence of title, description, tags, and updated timestamp).
176
+ * **Options**:
177
+ * `--strict`: Promote compliance warnings to errors and exit with code 1.
178
+
148
179
  ---
149
180
 
150
181
  ## 🌐 DIRECT REST API REFERENCE
@@ -1,8 +1,6 @@
1
1
  ---
2
- type: skill
3
2
  name: cli-agents
4
3
  description: "Orchestrate headlessly spawned CLI agents from the central registry."
5
- schema_version: 2
6
4
  ---
7
5
 
8
6
  # CLI Agents — Headless Multi-Agent Orchestration & Dispatch Engine
@@ -1,8 +1,6 @@
1
1
  ---
2
- type: skill
3
2
  name: ssss
4
3
  description: "Use this skill to inspect, validate, write, and manage SSSS primitives, memory nodes, operation envelopes, scope overlays, projections, and VFS specifications in the Total Recall reference kernel. MANDATORY: Read this file before editing SSSS files or code."
5
- schema_version: 2
6
4
  ---
7
5
 
8
6
  # SSSS — Structured Semantic Syntax System Manager
@@ -127,6 +125,9 @@ SSSS schema v2. Required schema v2 knowledge-graph fields are:
127
125
 
128
126
  Invariants additionally use `priority: absolute` and `immutable: true`.
129
127
 
128
+ ### OKF Compatibility Note
129
+ SSSS v0.2 is a strict superset of the Open Knowledge Format (OKF v0.1 Draft). SSSS memory nodes are fully OKF-compatible, as they are Markdown files containing a YAML frontmatter block with a `type` field (which maps to SSSS `category` via the OKF adapter). To bridge the format seamlessly, SSSS v0.2 extends `MemoryNodeSchema` with two optional OKF-standard fields: `description` and `resource` (supporting non-HTTP URIs such as `gs://` or `s3://`).
130
+
130
131
  Memory validation is owned by `TotalRecallMemoryValidator`, not by generic loose
131
132
  frontmatter checks. No memory path may be committed with zero validation.
132
133
 
@@ -0,0 +1,85 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseLayerFlag, resolveBrainDir } from './agent-dir.mjs';
4
+ import { exportBundle } from '../core/okf-adapter.mjs';
5
+
6
+ export default async function exportCommand(args) {
7
+ const { layer, remainingArgs } = parseLayerFlag(args);
8
+
9
+ const opts = {
10
+ outputPath: null,
11
+ okf: false,
12
+ format: 'dir',
13
+ stripSsss: false,
14
+ help: false
15
+ };
16
+
17
+ for (let i = 0; i < remainingArgs.length; i++) {
18
+ const arg = remainingArgs[i];
19
+ const val = remainingArgs[i + 1];
20
+
21
+ switch (arg) {
22
+ case '--okf':
23
+ opts.okf = true;
24
+ break;
25
+ case '--format':
26
+ if (val) { opts.format = val; i++; }
27
+ break;
28
+ case '--strip-ssss':
29
+ opts.stripSsss = true;
30
+ break;
31
+ case '--help': case '-h':
32
+ opts.help = true;
33
+ break;
34
+ default:
35
+ if (!arg.startsWith('-') && !opts.outputPath) {
36
+ opts.outputPath = arg;
37
+ }
38
+ }
39
+ }
40
+
41
+ if (opts.help || !opts.outputPath || !opts.okf) {
42
+ printExportHelp();
43
+ return;
44
+ }
45
+
46
+ const brainDir = resolveBrainDir(layer);
47
+ const vaultDir = path.join(brainDir, 'memory-vault');
48
+
49
+ console.log(`\n 📤 Exporting Memory Vault to OKF Bundle...`);
50
+ console.log(` 🧠 Source Brain: ${layer} (${vaultDir})`);
51
+ console.log(` 📂 Destination: ${opts.outputPath} (Format: ${opts.format})`);
52
+
53
+ try {
54
+ const report = await exportBundle(vaultDir, opts.outputPath, {
55
+ format: opts.format,
56
+ stripSsss: opts.stripSsss
57
+ });
58
+
59
+ console.log(`\n Export Summary:`);
60
+ console.log(` 🟢 Exported: ${report.exported.length} concepts`);
61
+ console.log(` 📄 generated index.md`);
62
+ console.log(` 📄 generated log.md`);
63
+ console.log(` ✅ Export completed successfully.\n`);
64
+ } catch (err) {
65
+ console.error(`\n ❌ Export failed: ${err.message}\n`);
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ function printExportHelp() {
71
+ console.log(`
72
+ total-recall export — Export Total Recall knowledge bases
73
+
74
+ Usage:
75
+ total-recall export <output-path> --okf [options]
76
+
77
+ Options:
78
+ --okf Export as an Open Knowledge Format (OKF) bundle (required)
79
+ --format <dir|tar.gz> Output format: directory or compressed tarball (default: dir)
80
+ --strip-ssss Remove all SSSS-specific metadata fields, leaving only pure OKF
81
+ --global Source from the global brain
82
+ --project Source from the project brain (default)
83
+ --help, -h Show this help
84
+ `);
85
+ }
@@ -0,0 +1,132 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseLayerFlag, resolveBrainDir } from './agent-dir.mjs';
4
+ import { importBundle } from '../core/okf-adapter.mjs';
5
+
6
+ export async function runOkfIngest(args) {
7
+ const { layer, remainingArgs } = parseLayerFlag(args);
8
+
9
+ const opts = {
10
+ bundlePath: null,
11
+ dryRun: false,
12
+ category: null,
13
+ importance: null,
14
+ onConflict: 'warn',
15
+ typeMap: null,
16
+ help: false
17
+ };
18
+
19
+ for (let i = 0; i < remainingArgs.length; i++) {
20
+ const arg = remainingArgs[i];
21
+ const val = remainingArgs[i + 1];
22
+
23
+ switch (arg) {
24
+ case '--dry-run':
25
+ opts.dryRun = true;
26
+ break;
27
+ case '--category':
28
+ if (val) { opts.category = val; i++; }
29
+ break;
30
+ case '--importance':
31
+ if (val) { opts.importance = parseInt(val, 10); i++; }
32
+ break;
33
+ case '--on-conflict':
34
+ if (val) { opts.onConflict = val; i++; }
35
+ break;
36
+ case '--type-map':
37
+ if (val) {
38
+ const map = {};
39
+ const pairs = val.split(',');
40
+ for (const pair of pairs) {
41
+ const parts = pair.split('=');
42
+ if (parts.length === 2) {
43
+ map[parts[0].trim()] = parts[1].trim();
44
+ }
45
+ }
46
+ opts.typeMap = map;
47
+ i++;
48
+ }
49
+ break;
50
+ case '--help': case '-h':
51
+ opts.help = true;
52
+ break;
53
+ default:
54
+ if (!arg.startsWith('-') && !opts.bundlePath) {
55
+ opts.bundlePath = arg;
56
+ }
57
+ }
58
+ }
59
+
60
+ if (opts.help || !opts.bundlePath) {
61
+ printIngestOkfHelp();
62
+ return;
63
+ }
64
+
65
+ const brainDir = resolveBrainDir(layer);
66
+ const vaultDir = path.join(brainDir, 'memory-vault');
67
+
68
+ console.log(`\n 📥 Importing OKF Bundle from: ${opts.bundlePath}`);
69
+ console.log(` 🧠 Target Brain: ${layer} (${vaultDir})`);
70
+ if (opts.dryRun) console.log(' ⚠️ DRY RUN: No actual writes will be committed.');
71
+
72
+ const report = await importBundle(opts.bundlePath, vaultDir, {
73
+ dryRun: opts.dryRun,
74
+ category: opts.category,
75
+ importance: opts.importance,
76
+ onConflict: opts.onConflict,
77
+ typeMap: opts.typeMap
78
+ });
79
+
80
+ console.log(`\n Import Summary:`);
81
+ console.log(` 🟢 Imported: ${report.imported.length} concepts`);
82
+ console.log(` 🟡 Skipped: ${report.skipped.length} concepts`);
83
+ console.log(` 🔴 Errors: ${report.errors.length} concepts`);
84
+
85
+ if (report.skipped.length > 0) {
86
+ console.log(`\n Skipped detail:`);
87
+ for (const item of report.skipped) {
88
+ console.log(` • ${item.file}: ${item.reason}`);
89
+ }
90
+ }
91
+
92
+ if (report.errors.length > 0) {
93
+ console.log(`\n Errors detail:`);
94
+ for (const item of report.errors) {
95
+ console.log(` • ${item.file}: ${item.error}`);
96
+ }
97
+ process.exit(1);
98
+ }
99
+
100
+ if (report.imported.length > 0 && !opts.dryRun) {
101
+ // Recompile active memory surfaces and indexes in the background to avoid blocking the CLI call.
102
+ console.log('\n ⏳ Recompiling active memory surfaces and indexes in the background...');
103
+ try {
104
+ const { spawn } = await import('node:child_process');
105
+ const child = spawn(process.argv[0], [process.argv[1], 'compile'], {
106
+ detached: true,
107
+ stdio: 'ignore'
108
+ });
109
+ child.unref();
110
+ console.log(' ✅ Background compilation started.');
111
+ } catch (err) {
112
+ console.warn(` ⚠️ Background recompilation spawn failed: ${err.message}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ function printIngestOkfHelp() {
118
+ console.log(`
119
+ Usage:
120
+ total-recall ingest okf <bundle-path> [options]
121
+
122
+ Options:
123
+ --dry-run Validate concepts and preview import without writing
124
+ --category <name> Override and force category for all imported concepts
125
+ --importance <1-5> Override default importance (default: 3)
126
+ --on-conflict <strategy> Strategy for duplicate slugs: 'skip' | 'warn' | 'overwrite' (default: warn)
127
+ --type-map <mapping> Custom type mappings. Format: "Type A=facts,Type B=concepts"
128
+ --global Target the global brain
129
+ --project Target the project brain (default)
130
+ --help, -h Show this help
131
+ `);
132
+ }
@@ -38,6 +38,8 @@ function printHelp() {
38
38
  Subcommands:
39
39
  google-takeout <path> Ingest Google Takeout export data
40
40
  Use 'total-recall ingest google-takeout --help' for details
41
+ okf <path> Ingest an Open Knowledge Format (OKF) bundle directory
42
+ Use 'total-recall ingest okf --help' for details
41
43
 
42
44
  IDE Session Ingest:
43
45
  Scans known local directories for conversation history from:
@@ -52,6 +54,7 @@ function printHelp() {
52
54
  Usage:
53
55
  total-recall ingest [options]
54
56
  total-recall ingest google-takeout <path> [options]
57
+ total-recall ingest okf <path> [options]
55
58
 
56
59
  Options:
57
60
  --sources <list> Comma-separated source filter (e.g. claude-code,codex)
@@ -68,6 +71,12 @@ export default async function ingest(args) {
68
71
  return;
69
72
  }
70
73
 
74
+ if (args[0] === 'okf') {
75
+ const { runOkfIngest } = await import('./ingest-okf.mjs');
76
+ await runOkfIngest(args.slice(1));
77
+ return;
78
+ }
79
+
71
80
  const opts = parseArgs(args);
72
81
  if (opts.help) { printHelp(); return; }
73
82
 
package/src/cli/lint.mjs CHANGED
@@ -24,12 +24,13 @@ const AGENT_DIR = resolveAgentDir();
24
24
  const BRAIN_DIR = resolveBrainDir();
25
25
 
26
26
  function parseArgs(args) {
27
- const opts = { vault: null, strict: false, json: false, help: false };
27
+ const opts = { vault: null, strict: false, json: false, help: false, okf: false };
28
28
  for (let i = 0; i < args.length; i++) {
29
29
  switch (args[i]) {
30
30
  case '--vault': opts.vault = args[++i]; break;
31
31
  case '--strict': opts.strict = true; break;
32
32
  case '--json': opts.json = true; break;
33
+ case '--okf': opts.okf = true; break;
33
34
  case '--help': case '-h': opts.help = true; break;
34
35
  }
35
36
  }
@@ -38,16 +39,17 @@ function parseArgs(args) {
38
39
 
39
40
  function printHelp() {
40
41
  console.log(`
41
- total-recall lint — Validate vault nodes against SSSS schema v2
42
+ total-recall lint — Validate vault nodes against SSSS schema v2 or OKF spec
42
43
 
43
44
  Scans all .md files in the memory vault and validates their YAML
44
- frontmatter against the Zod schemas defined in src/core/schema.mjs.
45
+ frontmatter against the Zod schemas or checks for OKF compliance.
45
46
 
46
47
  Usage: total-recall lint [options]
47
48
 
48
49
  Options:
49
50
  --vault <path> Override vault directory (default: ~/.agent/memory-vault)
50
51
  --strict Treat warnings as errors (exit 1 on any issue)
52
+ --okf Verify OKF v0.1 draft metadata compliance
51
53
  --json Output results as JSONL
52
54
  --help, -h Show this help
53
55
 
@@ -63,6 +65,40 @@ export default async function lint(args) {
63
65
 
64
66
  const vaultDir = opts.vault || path.join(BRAIN_DIR, 'memory-vault');
65
67
 
68
+ if (opts.okf) {
69
+ const { lintOkfCompliance } = await import('../core/okf-adapter.mjs');
70
+ const report = lintOkfCompliance(vaultDir, { strict: opts.strict });
71
+
72
+ if (opts.json) {
73
+ console.log(JSON.stringify(report));
74
+ } else {
75
+ console.error(`\n 🔍 OKF Compliance Report for: ${vaultDir}\n`);
76
+ for (const warn of report.warnings) {
77
+ console.error(` ⚠️ ${warn.file}: ${warn.message}`);
78
+ }
79
+ for (const err of report.errors) {
80
+ console.error(` ❌ ${err.file}: ${err.message}`);
81
+ }
82
+
83
+ console.error(`\n ── Results ──`);
84
+ console.error(` Total scanned: ${report.total}`);
85
+ console.error(` Pass: ${report.pass ? 'Yes' : 'No'}`);
86
+ console.error(` Warnings: ${report.warnings.length}`);
87
+ console.error(` Errors: ${report.errors.length}`);
88
+
89
+ if (report.pass) {
90
+ console.error(`\n ✅ OKF Compliance passed\n`);
91
+ } else {
92
+ console.error(`\n ❌ OKF Compliance failed\n`);
93
+ }
94
+ }
95
+
96
+ if (!report.pass) {
97
+ process.exit(1);
98
+ }
99
+ return;
100
+ }
101
+
66
102
  if (!opts.json) {
67
103
  console.error(`\n 🔍 Linting vault: ${vaultDir}\n`);
68
104
  }
@@ -0,0 +1,543 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import matter from 'gray-matter';
5
+ import { safeStringify } from './vault.mjs';
6
+ import { writeNodeValidated } from './validated-write.mjs';
7
+ import { getNodes } from './vault-cache.mjs';
8
+ import { validateMemoryNode } from './total-recall-memory-validator.mjs';
9
+
10
+ // ─── DEFAULT OKF TYPE MAP ──────────────────────────────────────────────────
11
+ export const DEFAULT_OKF_TYPE_MAP = {
12
+ 'BigQuery Table': 'facts',
13
+ 'BigQuery Dataset': 'facts',
14
+ 'API Endpoint': 'facts',
15
+ 'Metric': 'concepts',
16
+ 'Playbook': 'patterns',
17
+ 'Runbook': 'patterns',
18
+ 'Reference': 'facts',
19
+ 'Architecture': 'concepts',
20
+ 'Decision': 'decisions',
21
+ 'Policy': 'invariants',
22
+ 'Best Practice': 'patterns',
23
+ 'Anti-Pattern': 'anti-patterns',
24
+ '*': 'facts',
25
+ };
26
+
27
+ // ─── Pure Mapping Functions ────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Derives a valid SSSS subject from a title.
31
+ */
32
+ function deriveSubject(title) {
33
+ let subject = 'okf.concept';
34
+ if (title) {
35
+ const cleaned = title.replace(/[^a-zA-Z0-9_\s.-]/g, '').trim();
36
+ if (cleaned) {
37
+ const words = cleaned.split(/\s+/);
38
+ if (words.length > 0 && words[0]) {
39
+ subject = words[0].toLowerCase();
40
+ }
41
+ }
42
+ }
43
+ return /^[a-zA-Z0-9_\s.-]+$/.test(subject) ? subject : 'okf.concept';
44
+ }
45
+
46
+ /**
47
+ * Derives a description from a Markdown body if absent.
48
+ */
49
+ function deriveDescription(body) {
50
+ if (!body) return undefined;
51
+ const match = body.trim().match(/^[^.!?\n]+[.!?]/);
52
+ return match ? match[0].trim() : body.trim().split('\n')[0].trim();
53
+ }
54
+
55
+ /**
56
+ * Capitalizes SSSS category to OKF type.
57
+ */
58
+ function categoryToOkfType(category) {
59
+ const reverseMap = {
60
+ 'facts': 'Fact',
61
+ 'concepts': 'Concept',
62
+ 'patterns': 'Pattern',
63
+ 'invariants': 'Invariant',
64
+ 'decisions': 'Decision',
65
+ 'anti-patterns': 'Anti-Pattern',
66
+ 'preferences': 'Preference',
67
+ 'corrections': 'Correction',
68
+ 'lore': 'Lore'
69
+ };
70
+ return reverseMap[category] || (category.charAt(0).toUpperCase() + category.slice(1));
71
+ }
72
+
73
+ /**
74
+ * Convert an OKF concept (frontmatter + body) into an SSSS memory node.
75
+ */
76
+ export function okfConceptToSsssNode(okfFrontmatter, okfBody, conceptId, options = {}) {
77
+ if (!okfFrontmatter || typeof okfFrontmatter !== 'object') {
78
+ return null;
79
+ }
80
+
81
+ const actualTypeMap = { ...DEFAULT_OKF_TYPE_MAP, ...options.typeMap };
82
+ const okfType = okfFrontmatter.type;
83
+
84
+ let category = options.category;
85
+ if (!category) {
86
+ category = actualTypeMap[okfType] || actualTypeMap['*'] || 'facts';
87
+ }
88
+
89
+ // Derive slug
90
+ let slug = conceptId || 'unnamed-concept';
91
+ if (slug.endsWith('.md')) {
92
+ slug = slug.slice(0, -3);
93
+ }
94
+ slug = slug.replace(/^(\.\.?[\/\\])+/, '').replace(/^[\/\\]+/, '');
95
+ slug = slug.replace(/[\/\\]/g, '-').toLowerCase();
96
+
97
+ const title = okfFrontmatter.title || conceptId || 'Untitled Concept';
98
+ const subject = okfFrontmatter.title ? deriveSubject(okfFrontmatter.title) : 'okf.concept';
99
+ const object = okfFrontmatter.type || 'concept';
100
+
101
+ // Parse ISO 8601 timestamp
102
+ let timestamp = new Date().toISOString();
103
+ if (okfFrontmatter.timestamp) {
104
+ if (okfFrontmatter.timestamp instanceof Date) {
105
+ timestamp = okfFrontmatter.timestamp.toISOString();
106
+ } else if (typeof okfFrontmatter.timestamp === 'string') {
107
+ timestamp = okfFrontmatter.timestamp;
108
+ }
109
+ }
110
+
111
+ const importance = (options.importance !== undefined && options.importance !== null)
112
+ ? options.importance
113
+ : ((options.defaultImportance !== undefined && options.defaultImportance !== null)
114
+ ? options.defaultImportance
115
+ : 3);
116
+
117
+ const node = {
118
+ type: 'memory',
119
+ slug,
120
+ category,
121
+ title,
122
+ description: okfFrontmatter.description || undefined,
123
+ resource: okfFrontmatter.resource || undefined,
124
+ status: 'active',
125
+ confidence: 0.8,
126
+ importance,
127
+ created: timestamp,
128
+ updated: timestamp,
129
+ last_accessed: new Date().toISOString(),
130
+ source: {
131
+ type: 'okf-import',
132
+ session_id: crypto.randomUUID(),
133
+ agent: 'total-recall-cli',
134
+ evidence_count: 1,
135
+ },
136
+ supersedes: [],
137
+ superseded_by: null,
138
+ contradicts: [],
139
+ tags: Array.isArray(okfFrontmatter.tags) ? okfFrontmatter.tags : (okfFrontmatter.tags ? [okfFrontmatter.tags] : []),
140
+ related: [],
141
+ routes_to_skills: [],
142
+ sentiment_polarity: 'descriptive',
143
+ sentiment_target: subject,
144
+ modality: 'descriptive',
145
+ subject,
146
+ predicate: 'describes',
147
+ object,
148
+ decay: {
149
+ half_life_days: 30,
150
+ access_count: 0,
151
+ },
152
+ schema_version: 2,
153
+ body: okfBody || '',
154
+ };
155
+
156
+ return node;
157
+ }
158
+
159
+ /**
160
+ * Convert an SSSS memory node into an OKF-compliant concept.
161
+ */
162
+ export function ssssNodeToOkfConcept(ssssNode) {
163
+ if (!ssssNode || typeof ssssNode !== 'object') {
164
+ return null;
165
+ }
166
+
167
+ const desc = ssssNode.description || deriveDescription(ssssNode.body);
168
+
169
+ const frontmatter = {
170
+ ...ssssNode,
171
+ type: categoryToOkfType(ssssNode.category),
172
+ title: ssssNode.title,
173
+ description: desc || undefined,
174
+ resource: ssssNode.resource || undefined,
175
+ tags: ssssNode.tags || [],
176
+ timestamp: ssssNode.updated || ssssNode.created || new Date().toISOString(),
177
+ };
178
+
179
+ // Clean up SSSS node-specific keys we don't want as top-level frontmatter if they match the body/filepath fields
180
+ delete frontmatter.body;
181
+ delete frontmatter._filePath;
182
+ delete frontmatter._filepath;
183
+ delete frontmatter._file_path;
184
+
185
+ return {
186
+ frontmatter,
187
+ body: ssssNode.body || '',
188
+ };
189
+ }
190
+
191
+ // ─── Bundle Operations (Placeholders to be fully implemented) ────────────────
192
+
193
+ export async function importBundle(bundlePath, vaultDir, options = {}) {
194
+ const imported = [];
195
+ const skipped = [];
196
+ const errors = [];
197
+
198
+ if (!fs.existsSync(bundlePath)) {
199
+ errors.push({ file: bundlePath, error: 'Bundle directory does not exist' });
200
+ return { imported, skipped, errors };
201
+ }
202
+
203
+ // Helper to walk directory recursively, skipping index.md and log.md
204
+ function walk(dir) {
205
+ let files = [];
206
+ const list = fs.readdirSync(dir, { withFileTypes: true });
207
+ for (const entry of list) {
208
+ const name = entry.name;
209
+ // Skip reserved filenames at any directory level
210
+ if (name === 'index.md' || name === 'log.md') {
211
+ continue;
212
+ }
213
+ const fullPath = path.join(dir, name);
214
+ if (entry.isDirectory()) {
215
+ files = files.concat(walk(fullPath));
216
+ } else if (name.endsWith('.md')) {
217
+ files.push(fullPath);
218
+ }
219
+ }
220
+ return files;
221
+ }
222
+
223
+ let mdFiles = [];
224
+ try {
225
+ mdFiles = walk(bundlePath);
226
+ } catch (err) {
227
+ errors.push({ file: bundlePath, error: err.message });
228
+ return { imported, skipped, errors };
229
+ }
230
+
231
+ // Get existing slugs to handle conflicts
232
+ let existingSlugs = new Set();
233
+ try {
234
+ const existing = getNodes(vaultDir);
235
+ existingSlugs = new Set(existing.map(n => n.slug));
236
+ } catch (err) {
237
+ // If vault dir doesn't exist yet, it's fine, we'll create it during writes
238
+ }
239
+
240
+ const onConflict = options.onConflict || 'warn';
241
+
242
+ for (const filePath of mdFiles) {
243
+ const relPath = path.relative(bundlePath, filePath);
244
+ let raw;
245
+ try {
246
+ raw = fs.readFileSync(filePath, 'utf8');
247
+ } catch (err) {
248
+ errors.push({ file: relPath, error: `Failed to read file: ${err.message}` });
249
+ continue;
250
+ }
251
+
252
+ let parsed;
253
+ try {
254
+ parsed = matter(raw);
255
+ } catch (err) {
256
+ errors.push({ file: relPath, error: `Failed to parse YAML frontmatter: ${err.message}` });
257
+ continue;
258
+ }
259
+
260
+ // Skip if there's no frontmatter data or if the required OKF 'type' field is missing
261
+ if (!parsed || !parsed.data || Object.keys(parsed.data).length === 0 || !parsed.data.type) {
262
+ skipped.push({ file: relPath, reason: 'missing frontmatter or type field' });
263
+ continue;
264
+ }
265
+
266
+ const node = okfConceptToSsssNode(parsed.data, parsed.content, relPath, options);
267
+ if (!node) {
268
+ skipped.push({ file: relPath, reason: 'failed to map to SSSS node' });
269
+ continue;
270
+ }
271
+
272
+ // Handle slug conflict
273
+ if (existingSlugs.has(node.slug)) {
274
+ if (onConflict === 'skip') {
275
+ skipped.push({ file: relPath, reason: `conflict (slug ${node.slug} exists)` });
276
+ continue;
277
+ } else if (onConflict === 'warn') {
278
+ console.warn(`[OKF Import Warning] Slug collision: '${node.slug}' already exists. Overwriting.`);
279
+ }
280
+ }
281
+
282
+ try {
283
+ const writeResult = writeNodeValidated(node, vaultDir, {
284
+ dryRun: options.dryRun || false,
285
+ agentRole: options.agentRole || 'admin'
286
+ });
287
+
288
+ if (!writeResult.success) {
289
+ let errMsg = writeResult.error || 'Validation failed';
290
+ if (writeResult.repair?.field_errors) {
291
+ const details = writeResult.repair.field_errors.map(fe => `${fe.field}: ${fe.issue}`).join(', ');
292
+ errMsg += ` (${details})`;
293
+ }
294
+ errors.push({ file: relPath, error: errMsg });
295
+ } else {
296
+ imported.push({ slug: node.slug, file: relPath });
297
+ if (!options.dryRun) {
298
+ existingSlugs.add(node.slug);
299
+ }
300
+ }
301
+ } catch (err) {
302
+ errors.push({ file: relPath, error: `Write error: ${err.message}` });
303
+ }
304
+ }
305
+
306
+ return { imported, skipped, errors };
307
+ }
308
+
309
+ export async function exportBundle(vaultDir, outputDir, options = {}) {
310
+ const { execSync } = await import('node:child_process');
311
+ const os = await import('node:os');
312
+
313
+ const format = options.format || 'dir';
314
+ const stripSsss = options.stripSsss || false;
315
+
316
+ let targetDir = outputDir;
317
+ let isTar = format === 'tar.gz';
318
+ let tempDir = null;
319
+
320
+ if (isTar) {
321
+ tempDir = path.join(os.tmpdir(), `okf-export-${crypto.randomUUID()}`);
322
+ targetDir = tempDir;
323
+ }
324
+
325
+ fs.mkdirSync(targetDir, { recursive: true });
326
+
327
+ const nodes = getNodes(vaultDir);
328
+ const exported = [];
329
+
330
+ const conceptsByCategory = {};
331
+
332
+ for (const node of nodes) {
333
+ const concept = ssssNodeToOkfConcept(node);
334
+ if (!concept) continue;
335
+
336
+ if (stripSsss) {
337
+ const allowed = ['type', 'title', 'description', 'resource', 'tags', 'timestamp'];
338
+ const stripped = {};
339
+ for (const key of allowed) {
340
+ if (concept.frontmatter[key] !== undefined) {
341
+ stripped[key] = concept.frontmatter[key];
342
+ }
343
+ }
344
+ concept.frontmatter = stripped;
345
+ }
346
+
347
+ const category = node.category || 'facts';
348
+ const catDir = path.join(targetDir, category);
349
+ fs.mkdirSync(catDir, { recursive: true });
350
+
351
+ const filename = `${node.slug}.md`;
352
+ const destPath = path.join(catDir, filename);
353
+
354
+ const fileContent = safeStringify(concept.body, concept.frontmatter);
355
+ fs.writeFileSync(destPath, fileContent, 'utf8');
356
+
357
+ exported.push({ slug: node.slug, file: path.join(category, filename) });
358
+
359
+ if (!conceptsByCategory[category]) {
360
+ conceptsByCategory[category] = [];
361
+ }
362
+ conceptsByCategory[category].push({
363
+ title: node.title,
364
+ relPath: `./${category}/${filename}`,
365
+ description: concept.frontmatter.description || ''
366
+ });
367
+ }
368
+
369
+ // Generate index.md
370
+ let indexContent = '# Knowledge Bundle Index\n\n';
371
+ for (const [category, list] of Object.entries(conceptsByCategory)) {
372
+ const catHeader = category.charAt(0).toUpperCase() + category.slice(1);
373
+ indexContent += `## ${catHeader}\n`;
374
+ for (const item of list) {
375
+ const descSuffix = item.description ? ` - ${item.description}` : '';
376
+ indexContent += `- [${item.title}](${item.relPath})${descSuffix}\n`;
377
+ }
378
+ indexContent += '\n';
379
+ }
380
+ fs.writeFileSync(path.join(targetDir, 'index.md'), indexContent, 'utf8');
381
+
382
+ // Generate log.md
383
+ let logContent = '# Bundle Update History\n\n';
384
+ const auditPath = path.join(vaultDir, '.events', 'audit.jsonl');
385
+ if (fs.existsSync(auditPath)) {
386
+ try {
387
+ const lines = fs.readFileSync(auditPath, 'utf8').trim().split('\n');
388
+ for (const line of lines) {
389
+ if (!line) continue;
390
+ const event = JSON.parse(line);
391
+ const date = event.ts ? new Date(event.ts).toISOString().replace(/\.\d+Z$/, 'Z') : 'Unknown Date';
392
+ const operation = event.payload?.resolved_type || 'mutation';
393
+ logContent += `- **${date}**: Operation '${operation}' committed at \`${event.subject || event.path || ''}\`\n`;
394
+ }
395
+ } catch (err) {
396
+ logContent += `Failed to parse audit history: ${err.message}\n`;
397
+ }
398
+ } else {
399
+ logContent += 'No update history available.\n';
400
+ }
401
+ fs.writeFileSync(path.join(targetDir, 'log.md'), logContent, 'utf8');
402
+
403
+ if (isTar) {
404
+ const tarFile = outputDir.endsWith('.tar.gz') ? outputDir : `${outputDir}.tar.gz`;
405
+ fs.mkdirSync(path.dirname(tarFile), { recursive: true });
406
+ execSync(`tar -czf "${tarFile}" -C "${targetDir}" .`);
407
+ fs.rmSync(tempDir, { recursive: true, force: true });
408
+ }
409
+
410
+ return {
411
+ exported,
412
+ indexGenerated: true,
413
+ logGenerated: true
414
+ };
415
+ }
416
+
417
+ export function lintOkfCompliance(vaultDir, options = {}) {
418
+ const strict = options.strict || false;
419
+ const nodes = getNodes(vaultDir);
420
+
421
+ const warnings = [];
422
+ const errors = [];
423
+
424
+ for (const node of nodes) {
425
+ const relPath = `${node.category}/${node.slug}.md`;
426
+
427
+ // Check title
428
+ if (!node.title) {
429
+ const msg = `Missing title in ${relPath}`;
430
+ if (strict) errors.push({ file: relPath, message: msg });
431
+ else warnings.push({ file: relPath, message: msg });
432
+ }
433
+
434
+ // Check description
435
+ if (!node.description) {
436
+ const msg = `Missing description in ${relPath}`;
437
+ if (strict) errors.push({ file: relPath, message: msg });
438
+ else warnings.push({ file: relPath, message: msg });
439
+ }
440
+
441
+ // Check tags
442
+ if (!node.tags || !Array.isArray(node.tags) || node.tags.length === 0) {
443
+ const msg = `Missing or empty tags in ${relPath}`;
444
+ if (strict) errors.push({ file: relPath, message: msg });
445
+ else warnings.push({ file: relPath, message: msg });
446
+ }
447
+
448
+ // Check updated/created timestamp
449
+ if (!node.updated && !node.created) {
450
+ const msg = `Missing updated/created timestamp in ${relPath}`;
451
+ if (strict) errors.push({ file: relPath, message: msg });
452
+ else warnings.push({ file: relPath, message: msg });
453
+ }
454
+ }
455
+
456
+ const pass = errors.length === 0;
457
+
458
+ return {
459
+ total: nodes.length,
460
+ pass,
461
+ warnings,
462
+ errors
463
+ };
464
+ }
465
+
466
+ export function generateLiveIndex(vaultDir) {
467
+ const nodes = getNodes(vaultDir);
468
+ if (nodes.length === 0) return;
469
+
470
+ const conceptsByType = {};
471
+
472
+ for (const node of nodes) {
473
+ const concept = ssssNodeToOkfConcept(node);
474
+ if (!concept) continue;
475
+
476
+ const type = concept.frontmatter.type || 'Generic Concept';
477
+ const category = node.category || 'facts';
478
+ const filename = `${node.slug}.md`;
479
+
480
+ if (!conceptsByType[type]) {
481
+ conceptsByType[type] = [];
482
+ }
483
+ conceptsByType[type].push({
484
+ title: node.title || node.slug,
485
+ relPath: `./${category}/${filename}`,
486
+ description: concept.frontmatter.description || ''
487
+ });
488
+ }
489
+
490
+ let indexContent = '# Knowledge Bundle Index\n\n';
491
+ const sortedTypes = Object.keys(conceptsByType).sort();
492
+ for (const type of sortedTypes) {
493
+ indexContent += `# ${type}\n\n`;
494
+ const list = conceptsByType[type].sort((a, b) => a.title.localeCompare(b.title));
495
+ for (const item of list) {
496
+ const descSuffix = item.description ? ` - ${item.description}` : '';
497
+ indexContent += `* [${item.title}](${item.relPath})${descSuffix}\n`;
498
+ }
499
+ indexContent += '\n';
500
+ }
501
+ fs.writeFileSync(path.join(vaultDir, 'index.md'), indexContent, 'utf8');
502
+ }
503
+
504
+ export function generateLiveLog(vaultDir) {
505
+ let logContent = '# Bundle Update History\n\n';
506
+ const auditPath = path.join(vaultDir, '.events', 'audit.jsonl');
507
+ if (fs.existsSync(auditPath)) {
508
+ try {
509
+ const lines = fs.readFileSync(auditPath, 'utf8').trim().split('\n').filter(Boolean);
510
+ const eventsByDate = {};
511
+
512
+ for (const line of lines) {
513
+ const event = JSON.parse(line);
514
+ if (!event.ts) continue;
515
+ const dateStr = new Date(event.ts).toISOString().split('T')[0];
516
+ const operation = event.payload?.resolved_type || 'mutation';
517
+ const formattedOp = operation.charAt(0).toUpperCase() + operation.slice(1);
518
+ const subject = event.subject || event.path || 'unknown';
519
+
520
+ if (!eventsByDate[dateStr]) {
521
+ eventsByDate[dateStr] = [];
522
+ }
523
+ eventsByDate[dateStr].push(`* **${formattedOp}**: Committed at \`${subject}\``);
524
+ }
525
+
526
+ const sortedDates = Object.keys(eventsByDate).sort().reverse();
527
+ for (const date of sortedDates) {
528
+ logContent += `## ${date}\n`;
529
+ const logs = eventsByDate[date].reverse();
530
+ for (const log of logs) {
531
+ logContent += `${log}\n`;
532
+ }
533
+ logContent += '\n';
534
+ }
535
+ } catch (err) {
536
+ logContent += `Failed to parse audit history: ${err.message}\n`;
537
+ }
538
+ } else {
539
+ logContent += 'No update history available.\n';
540
+ }
541
+ fs.writeFileSync(path.join(vaultDir, 'log.md'), logContent, 'utf8');
542
+ }
543
+
@@ -23,6 +23,8 @@ export const MemoryNodeSchema = z.object({
23
23
  slug: z.string(),
24
24
  category: z.string(),
25
25
  title: z.string(),
26
+ description: z.string().optional(),
27
+ resource: z.string().optional(),
26
28
  status: z.enum(['active', 'superseded', 'deprecated', 'draft']),
27
29
  confidence: z.number().min(0).max(1).optional(),
28
30
  importance: z.number().int().min(1).max(5).optional(),
@@ -10,13 +10,29 @@ import {
10
10
  } from './memory-layers.mjs';
11
11
 
12
12
  /**
13
- * Extract [[slug]] wikilink references from body text.
13
+ * Extract [[slug]] wikilink references and relative Markdown link targets from body text.
14
14
  * Native TR link resolution; Obsidian renders them as graph edges.
15
15
  */
16
16
  export function extractWikilinks(body) {
17
17
  if (!body) return [];
18
18
  const matches = body.match(/\[\[([^\]]+)\]\]/g) || [];
19
- return [...new Set(matches.map(m => m.slice(2, -2).split('|')[0].trim()))];
19
+ const wikilinks = matches.map(m => m.slice(2, -2).split('|')[0].trim());
20
+
21
+ const mdLinkMatches = body.matchAll(/\[([^\]]*)\]\(([^)]+)\)/g);
22
+ const mdLinks = [];
23
+ for (const match of mdLinkMatches) {
24
+ const url = match[2].trim();
25
+ if (/^https?:\/\//i.test(url)) {
26
+ continue;
27
+ }
28
+ const base = path.basename(url);
29
+ const targetSlug = base.endsWith('.md') ? base.slice(0, -3) : base;
30
+ if (targetSlug) {
31
+ mdLinks.push(targetSlug);
32
+ }
33
+ }
34
+
35
+ return [...new Set([...wikilinks, ...mdLinks])];
20
36
  }
21
37
 
22
38
  /**
@@ -145,15 +161,56 @@ function saveCompactedRulesCache(derivedDir, cache) {
145
161
  }
146
162
  }
147
163
 
164
+ /**
165
+ * Map a node's modality field to a compact marker for compiled shims.
166
+ * Gives agents instant priority signal per OKF §4.1 typed-concept pattern.
167
+ */
168
+ function modalityMarker(node) {
169
+ const m = (node.modality || '').toLowerCase();
170
+ if (m === 'must') return '[MUST]';
171
+ if (m === 'must_not') return '[MUST NOT]';
172
+ if (m === 'should') return '[SHOULD]';
173
+ if (m === 'should_not') return '[SHOULD NOT]';
174
+ // Fallback: infer from category
175
+ if (node.category === 'invariants') return '[MUST]';
176
+ if (node.category === 'anti-patterns') return '[CORRECTION]';
177
+ if (node.category === 'preferences') return '[PREF]';
178
+ return '';
179
+ }
180
+
148
181
  /**
149
182
  * Heuristically summarize a memory node.
183
+ * OKF-aligned: modality markers, no title/body duplication, sentence-boundary truncation.
150
184
  */
151
185
  export function heuristicCompact(node) {
152
186
  const title = (node.title || '').trim();
153
187
  const text = (node.body || node.content || '').trim();
188
+ const marker = modalityMarker(node);
189
+ const prefix = marker ? `${marker} ` : '';
190
+
191
+ // Determine the best display text, avoiding title/body duplication.
192
+ // When title is auto-generated ("Self-captured memory: ...") it echoes the body —
193
+ // use the body directly to avoid doubling the same content.
194
+ const titleIsEcho = title.startsWith('Self-captured memory:') ||
195
+ (text && title.length > 20 && text.toLowerCase().startsWith(title.toLowerCase().slice(0, 20)));
196
+
197
+ if (node.category && ['invariants', 'preferences', 'anti-patterns'].includes(node.category)) {
198
+ if (text) {
199
+ if (titleIsEcho) {
200
+ // Use body directly — title would duplicate it
201
+ return `${prefix}${_truncateAtSentence(text, 180)}`;
202
+ }
203
+ const lines = text.split('\n');
204
+ if (lines.length > 1) {
205
+ return `${prefix}${title}:\n ${lines.map(l => l.trim()).join('\n ')}`;
206
+ }
207
+ return `${prefix}${title}: ${text}`;
208
+ }
209
+ return `${prefix}${title}`;
210
+ }
154
211
 
155
- let summary = title;
156
- if (text) {
212
+ let summary = titleIsEcho && text ? text : title;
213
+ if (text && !titleIsEcho) {
157
214
  const firstLine = text.split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
158
215
  if (firstLine &&
159
216
  !title.toLowerCase().includes(firstLine.toLowerCase()) &&
@@ -164,24 +221,42 @@ export function heuristicCompact(node) {
164
221
  }
165
222
 
166
223
  summary = summary.replace(/\s+/g, ' ');
167
- if (summary.length > 180) {
168
- summary = summary.substring(0, 180).trim() + '... (use recall to read more)';
224
+ return `${prefix}${_truncateAtSentence(summary, 180)}`;
225
+ }
226
+
227
+ /**
228
+ * Truncate text at the nearest sentence boundary before maxLen,
229
+ * or at maxLen if no sentence boundary is found.
230
+ */
231
+ function _truncateAtSentence(text, maxLen) {
232
+ if (text.length <= maxLen) return text;
233
+ // Look for sentence-ending punctuation before maxLen
234
+ const slice = text.substring(0, maxLen);
235
+ const lastSentence = Math.max(
236
+ slice.lastIndexOf('. '),
237
+ slice.lastIndexOf('! '),
238
+ slice.lastIndexOf('? ')
239
+ );
240
+ if (lastSentence > maxLen * 0.4) {
241
+ return text.substring(0, lastSentence + 1).trim() + ' (use recall to read more)';
169
242
  }
170
- return summary;
243
+ return slice.trim() + '... (use recall to read more)';
171
244
  }
172
245
 
173
246
  /**
174
247
  * Compact a single memory node using LLM or heuristic fallback.
248
+ * When force=true, bypass cache read but still write back (OKF augmentation pattern).
175
249
  */
176
- async function compactNode(node, derivedDir) {
250
+ async function compactNode(node, derivedDir, force = false) {
177
251
  const title = (node.title || '').trim();
178
252
  const body = (node.body || node.content || '').trim();
179
253
  const fullText = `${title}\n\n${body}`;
180
254
  const contentHash = crypto.createHash('sha256').update(fullText).digest('hex');
181
255
 
182
- // Load from cache if possible
256
+ // Load from cache if possible — skip when force=true to recompute from scratch.
257
+ // Per OKF augmentation pattern: force bypasses cache *read* but preserves cache *write*.
183
258
  let cache = {};
184
- if (derivedDir) {
259
+ if (derivedDir && !force) {
185
260
  cache = loadCompactedRulesCache(derivedDir);
186
261
  if (cache[node.slug] && cache[node.slug].content_sha256 === contentHash) {
187
262
  return cache[node.slug].compacted;
@@ -210,8 +285,13 @@ async function compactNode(node, derivedDir) {
210
285
  }
211
286
  }
212
287
 
213
- // Save to cache
288
+ // Save to cache — always merge into existing cache (OKF non-destructive augmentation).
289
+ // When force=true we skipped cache loading above, so reload it now before writing
290
+ // to avoid blowing away other nodes' cached compactions.
214
291
  if (derivedDir) {
292
+ if (force) {
293
+ cache = loadCompactedRulesCache(derivedDir);
294
+ }
215
295
  cache[node.slug] = {
216
296
  compacted,
217
297
  content_sha256: contentHash,
@@ -223,87 +303,23 @@ async function compactNode(node, derivedDir) {
223
303
  return compacted;
224
304
  }
225
305
 
226
- export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide', derivedDir } = {}) {
306
+ export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide', derivedDir, force = false } = {}) {
227
307
  let combined;
228
308
 
229
309
  if (consumer === 'api') {
230
310
  // API consumers don't need CLI quickstart docs
231
311
  combined = `## Total Recall — Active Memory Context\n\nYour memories and rules are loaded from the active brain vault.\n`;
232
312
  } else {
233
- // CLI quickstart is always included so agents know TR is installed
313
+ // Compact CLI reference (OKF §6 progressive disclosure — summary, not full manual)
234
314
  combined = `## Total Recall — Sovereign Memory System (Installed)
235
315
 
236
- You have Total Recall installed. Use these CLI commands to remember and recall information.
237
-
238
- ### npx total-recall remember <category> "<content>" [options]
239
- Save rules, preferences, corrections, and facts to permanent memory.
240
-
241
- **Categories:** invariant, preference, correction, fact, concept, pattern, anti-pattern, decision, lore
242
-
243
- **Options:**
244
- --tags, -t <list> Comma-separated tags (e.g. "config,server")
245
- --importance, -i <1-5> Importance level (default: 3)
246
- --priority, -p <level> absolute | high | normal | low (default: normal)
247
- --modality, -m <type> must | must_not | should | should_not | descriptive | preference
248
- --confidence, -c <0-1> Confidence level (default: 1.0)
249
- --slug <custom-slug> Custom kebab-case slug
250
- --title <custom-title> Custom human-readable title
251
- --status <state> active | draft | archived (default: active)
252
- --related <list> Comma-separated related slugs
253
- --global Save to global brain (identity layer)
254
- --project Save to project brain (context layer)
255
-
256
- **Examples:**
257
- npx total-recall remember invariant "Never run tsc directly." --importance 5 --priority absolute
258
- npx total-recall remember preference "Always use single quotes." --tags "style,js"
259
- npx total-recall remember fact "The server runs on port 3000." --importance 4
260
- npx total-recall remember fact "Uses Drizzle ORM" --project
261
-
262
- ### npx total-recall recall "<query>" [options]
263
- Semantic search across rules, facts, and session history.
264
-
265
- **Options:**
266
- --top-k, -k <number> Results to return (default: 5, max: 20)
267
- --no-sessions, -ns Exclude session chunks, vault only
268
- --format, -f <type> text (default) or json
269
- --category, -cat <name> Filter by SSSS category
270
- --tags, -t <list> Filter by tags
271
- --modality, -m <type> Filter by modality
272
- --importance, -i <1-5> Filter by minimum importance
273
- --global Search global brain only
274
- --project Search project brain only
275
-
276
- **Examples:**
277
- npx total-recall recall "Never run tsc directly"
278
- npx total-recall recall "Express server port" --top-k 3
279
- npx total-recall recall "tsc" --category invariants --modality must
280
-
281
- ### npx total-recall forget <slug> [options]
282
- Delete a memory node by slug. Auto-recompiles surfaces and propagates to project brains.
283
-
284
- **Options:**
285
- --global Delete from global brain
286
- --project Delete from project brain
287
- --no-compile Skip auto-recompilation after deletion
288
-
289
- **Examples:**
290
- npx total-recall forget old-invariant-slug
291
- npx total-recall forget stale-fact --global
292
- npx total-recall forget project-specific-node --project
293
-
294
- ### npx total-recall help <topic>
295
- Query interactive local documentation, VFS specifications, and command references.
296
-
297
- **Options:**
298
- --json, -j Emit machine-readable JSON (ideal for programmatic retrieval)
299
-
300
- **Examples:**
301
- npx total-recall help connect
302
- npx total-recall help architecture
303
- npx total-recall help ssss
304
-
305
- ### npx total-recall --help
306
- Show all available commands.
316
+ **Quick Reference:**
317
+ - \`npx total-recall remember <category> "<content>" [options]\` — Save to memory (categories: invariant, preference, correction, fact, concept, pattern, anti-pattern, decision, lore; key flags: --importance, --priority, --modality, --tags, --global, --project)
318
+ - \`npx total-recall recall "<query>" [options]\` — Search memory (--top-k, --category, --tags, --modality)
319
+ - \`npx total-recall forget <slug> [options]\` — Delete a memory node (--global, --project)
320
+ - \`npx total-recall compile\` — Rebuild instruction surfaces
321
+ - \`npx total-recall help <topic>\` — Query local documentation
322
+ - \`npx total-recall --help\` — Full CLI reference
307
323
  `;
308
324
  }
309
325
 
@@ -324,10 +340,35 @@ Show all available commands.
324
340
  const preferences = nodes.filter(n => n.category === 'preferences' && n.status === 'active' && !isExpired(n));
325
341
  const corrections = nodes.filter(n => n.category === 'anti-patterns' && n.status === 'active' && !isExpired(n));
326
342
 
343
+ // Deduplicate nodes by compacted content hash (OKF non-destructive merge pattern).
344
+ // When two nodes produce identical compacted text, keep the one with higher importance.
345
+ const deduplicateNodes = (list) => {
346
+ const seen = new Map();
347
+ const deduped = [];
348
+ for (const n of list) {
349
+ const key = (n.body || n.content || '').trim().toLowerCase().substring(0, 200);
350
+ if (seen.has(key)) {
351
+ const existing = seen.get(key);
352
+ if ((n.importance || 3) > (existing.importance || 3)) {
353
+ // Replace with higher-importance node
354
+ const idx = deduped.indexOf(existing);
355
+ if (idx !== -1) deduped[idx] = n;
356
+ seen.set(key, n);
357
+ }
358
+ // else skip the lower-importance duplicate
359
+ } else {
360
+ seen.set(key, n);
361
+ deduped.push(n);
362
+ }
363
+ }
364
+ return deduped;
365
+ };
366
+
327
367
  const formatNodes = async (list) => {
368
+ const unique = deduplicateNodes(list);
328
369
  const formatted = [];
329
- for (const n of list) {
330
- const snippet = await compactNode(n, derivedDir);
370
+ for (const n of unique) {
371
+ const snippet = await compactNode(n, derivedDir, force);
331
372
  formatted.push(snippet.startsWith('-') ? snippet : `- ${snippet}`);
332
373
  }
333
374
  return formatted.join('\n');
@@ -412,9 +453,9 @@ function injectDirectives(fileContent, rulesBlock) {
412
453
  /**
413
454
  * Write or update a platform instruction shim with the pointer and active rules.
414
455
  */
415
- async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir } = {}) {
456
+ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir, force = false } = {}) {
416
457
  const shimDir = path.dirname(shimPath);
417
- const rulesBlock = await buildRulesBlock(skillsDir, nodes, { vaultDir, derivedDir });
458
+ const rulesBlock = await buildRulesBlock(skillsDir, nodes, { vaultDir, derivedDir, force });
418
459
  const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.\n';
419
460
  const fullContent = `${baseline}\n${DIRECTIVES_BEGIN}\n${rulesBlock}\n${DIRECTIVES_END}\n`;
420
461
 
@@ -496,13 +537,13 @@ function readConnectedClients(clientsPath) {
496
537
  /**
497
538
  * Compile shims asynchronously.
498
539
  */
499
- async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir, derivedDir } = {}) {
540
+ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir, derivedDir, force = false } = {}) {
500
541
  const agentDir = path.dirname(instructionsFile);
501
542
  const baseDir = path.basename(agentDir) === '.agent' ? path.dirname(agentDir) : agentDir;
502
543
  let injectedCount = 0;
503
544
 
504
545
  // Always write the canonical INSTRUCTIONS.md
505
- if (await writeShim(path.join(baseDir, 'INSTRUCTIONS.md'), skillsDir, nodes, { vaultDir, derivedDir })) {
546
+ if (await writeShim(path.join(baseDir, 'INSTRUCTIONS.md'), skillsDir, nodes, { vaultDir, derivedDir, force })) {
506
547
  injectedCount++;
507
548
  }
508
549
 
@@ -516,7 +557,7 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
516
557
  const files = CLIENT_SHIMS[client];
517
558
  if (!files) continue;
518
559
  for (const file of files) {
519
- if (await writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir, derivedDir })) {
560
+ if (await writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir, derivedDir, force })) {
520
561
  injectedCount++;
521
562
  }
522
563
  }
@@ -551,7 +592,7 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
551
592
  }
552
593
 
553
594
  // 1. Write pointer and active rules to all instruction shims
554
- const skillsInjected = await compilePointers(instructionsFile, skillsDir, nodes, { vaultDir, derivedDir });
595
+ const skillsInjected = await compilePointers(instructionsFile, skillsDir, nodes, { vaultDir, derivedDir, force });
555
596
 
556
597
  // 2. Build derived indexes (powers semantic search API)
557
598
  if (!fs.existsSync(derivedDir)) {
@@ -564,7 +605,8 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
564
605
  category: n.category,
565
606
  status: n.status,
566
607
  confidence: n.confidence,
567
- memory_layer: inferMemoryLayer(n)
608
+ memory_layer: inferMemoryLayer(n),
609
+ links: extractWikilinks(n.body || '')
568
610
  }));
569
611
  atomicWrite(path.join(derivedDir, 'graph-index.jsonl'), graphIndex.map(n => JSON.stringify(n)).join('\n'));
570
612
  atomicWrite(
@@ -589,6 +631,15 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
589
631
  atomicWrite(hashFile, currentHash);
590
632
  writeProjectionManifest(derivedDir, currentHash);
591
633
 
634
+ // 6. Generate live OKF Index and Log
635
+ try {
636
+ const { generateLiveIndex, generateLiveLog } = await import('./okf-adapter.mjs');
637
+ generateLiveIndex(vaultDir);
638
+ generateLiveLog(vaultDir);
639
+ } catch (err) {
640
+ logger.warn('surface', `Failed to generate live OKF index/log: ${err.message}`);
641
+ }
642
+
592
643
  return {
593
644
  nodesProcessed: nodes.length,
594
645
  skillsInjected,