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.
- package/bin/total-recall.mjs +2 -0
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/SKILL.md +32 -1
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +0 -2
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/evals/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/subagents/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +3 -2
- package/src/cli/export.mjs +85 -0
- package/src/cli/ingest-okf.mjs +132 -0
- package/src/cli/ingest.mjs +9 -0
- package/src/cli/lint.mjs +39 -3
- package/src/core/okf-adapter.mjs +543 -0
- package/src/core/schema.mjs +2 -0
- package/src/core/surface.mjs +144 -93
package/bin/total-recall.mjs
CHANGED
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
+
}
|
package/src/cli/ingest.mjs
CHANGED
|
@@ -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
|
|
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
|
+
|
package/src/core/schema.mjs
CHANGED
|
@@ -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(),
|
package/src/core/surface.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
|
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
|
|
313
|
+
// Compact CLI reference (OKF §6 progressive disclosure — summary, not full manual)
|
|
234
314
|
combined = `## Total Recall — Sovereign Memory System (Installed)
|
|
235
315
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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,
|