total-recall-brain 3.6.13 → 3.7.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.7.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": {
@@ -145,6 +145,38 @@ npx total-recall compile
145
145
  ```
146
146
  Synchronously rebuild the entire memory index (derived embeddings) and regenerate prompt instruction shims (such as `INSTRUCTIONS.md`, `GEMINI.md`, `AGENTS.md`, etc.).
147
147
 
148
+ ### 5. Ingest OKF Bundle
149
+ ```bash
150
+ npx total-recall ingest okf <path> [options]
151
+ ```
152
+ Recursively parse and ingest an Open Knowledge Format (OKF v0.1 Draft) bundle directory.
153
+ * **Options**:
154
+ * `--dry-run`: Parse and validate nodes without writing files.
155
+ * `--category <name>`: Override and force all imported nodes to a specific category.
156
+ * `--importance <1-5>`: Override importance for imported nodes.
157
+ * `--on-conflict <strategy>`: Slug conflict behavior (`overwrite`, `skip`, `warn` - default is `warn`).
158
+ * `--type-map <json>`: Custom type-to-category mapping JSON.
159
+ * *Note*: Spawns an asynchronous background surface compile process immediately on successful ingest.
160
+
161
+ ### 6. Export OKF Bundle
162
+ ```bash
163
+ npx total-recall export <path> [options]
164
+ ```
165
+ Export the memory vault into an OKF-compliant directory structure.
166
+ * **Options**:
167
+ * `--okf`: Run in OKF export mode.
168
+ * `--strip-ssss`: Strip all SSSS-specific metadata parameters from output frontmatter.
169
+ * `--format <type>`: Package output as archive (`tar.gz`).
170
+ * `--global` / `--project`: Target scope layer.
171
+
172
+ ### 7. Lint OKF Compliance
173
+ ```bash
174
+ npx total-recall lint --okf [options]
175
+ ```
176
+ Scan vault memory nodes and check for OKF metadata compliance (presence of title, description, tags, and updated timestamp).
177
+ * **Options**:
178
+ * `--strict`: Promote compliance warnings to errors and exit with code 1.
179
+
148
180
  ---
149
181
 
150
182
  ## 🌐 DIRECT REST API REFERENCE
@@ -127,6 +127,9 @@ SSSS schema v2. Required schema v2 knowledge-graph fields are:
127
127
 
128
128
  Invariants additionally use `priority: absolute` and `immutable: true`.
129
129
 
130
+ ### OKF Compatibility Note
131
+ 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://`).
132
+
130
133
  Memory validation is owned by `TotalRecallMemoryValidator`, not by generic loose
131
134
  frontmatter checks. No memory path may be committed with zero validation.
132
135
 
@@ -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,464 @@
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
+ }
@@ -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
  /**
@@ -564,7 +580,8 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
564
580
  category: n.category,
565
581
  status: n.status,
566
582
  confidence: n.confidence,
567
- memory_layer: inferMemoryLayer(n)
583
+ memory_layer: inferMemoryLayer(n),
584
+ links: extractWikilinks(n.body || '')
568
585
  }));
569
586
  atomicWrite(path.join(derivedDir, 'graph-index.jsonl'), graphIndex.map(n => JSON.stringify(n)).join('\n'));
570
587
  atomicWrite(