total-recall-brain 3.30.2 → 3.31.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.
@@ -58,6 +58,7 @@ const COMMANDS = {
58
58
  status: 'status.mjs',
59
59
  remember: 'remember.mjs',
60
60
  forget: 'forget.mjs',
61
+ edit: 'edit.mjs',
61
62
  recall: 'recall.mjs',
62
63
  search: 'recall.mjs',
63
64
  research: 'research.mjs',
@@ -111,7 +112,7 @@ function printHelp() {
111
112
  Core (default product path):
112
113
  init [--project] Bootstrap global or project brain + openwiki
113
114
  connect <client> Wire IDE / Obsidian / http-api host
114
- remember / forget Write-path memory (SSSS vault)
115
+ remember / forget / edit Write-path memory (SSSS vault)
115
116
  recall Read-path hybrid search
116
117
  compile Rebuild instruction surfaces
117
118
  dream Memory consolidation cycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.30.2",
3
+ "version": "3.31.0",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -71,7 +71,9 @@ Use the **MESH / CONTROL SERVER** section. Node details are entity fields on
71
71
  SSSS `mesh_node` documents — never hardcoded hostnames.
72
72
 
73
73
  ### 5d. When running tests, typechecks, or builds
74
- See **TESTS**. Heavy runs go to the Mac Mini or the droplet, never the laptop.
74
+ See **TESTS**. The full suite and the quality gates run only on the Mac Mini —
75
+ never on a laptop or the cloud droplet. A single spec file may run on any mesh node. Builds and installs are fine on any machine; build large apps
76
+ on the Mac Mini.
75
77
 
76
78
  ### 6. When troubleshooting connections, port blocks, or sync errors
77
79
  * **Trigger**: If the REST server is unreachable, ports are blocked, or the upstream sync tool encounters errors.
@@ -602,9 +604,11 @@ npx vitest run <path> # one spec file
602
604
 
603
605
  Current baseline (3.30.1): **349 spec files / 2024 tests, ~160 s** on the Mac Mini.
604
606
 
605
- > **Never run the full suite, a full typecheck, or a production build on the
606
- > laptop.** These go to the Mac Mini or the droplet — local runs cause slowdowns
607
- > and OOM kills. Sync and run remotely:
607
+ > **The full suite and the quality gates run only on the Mac Mini** — never on a
608
+ > laptop or the cloud droplet; they cause slowdowns and OOM kills. A single spec
609
+ > file may run on any mesh node. Builds
610
+ > and installs are fine anywhere; build large apps on the Mac Mini. Sync and run
611
+ > remotely:
608
612
  >
609
613
  > ```bash
610
614
  > REMOTE=build-host; REMOTE_DIR=code/total-recall # your own host and path
@@ -0,0 +1,206 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { resolveBrainDir, parseLayerFlag, getBothBrains } from './agent-dir.mjs';
5
+ import { writeNodeValidatedAsync } from '../core/validated-write.mjs';
6
+ import { isSafeVaultName } from '../core/vault.mjs';
7
+
8
+ const PRIORITIES = ['absolute', 'high', 'normal', 'low'];
9
+ const MODALITIES = ['must', 'must_not', 'should', 'should_not', 'descriptive', 'preference'];
10
+ const STATUSES = ['active', 'archived', 'deprecated'];
11
+
12
+ function printHelp() {
13
+ console.log(`
14
+ total-recall edit — Change an existing memory node in place (same slug)
15
+
16
+ Usage: total-recall edit <slug> ["<new content>"] [options]
17
+
18
+ Options:
19
+ --title <text> Replace the title
20
+ --importance, -i <1-5> Replace the importance
21
+ --priority, -p <level> absolute | high | normal | low
22
+ --modality, -m <type> must | must_not | should | should_not | descriptive | preference
23
+ --tags, -t <list> Replace the tags (comma-separated)
24
+ --status <status> active | archived | deprecated
25
+ --global Edit the node in the global brain
26
+ --project Edit the node in the project brain
27
+ --no-compile Skip auto-recompilation after the edit
28
+ --help, -h Show this help
29
+
30
+ Pass "-" as the content to read the new body from stdin. Omit the content to
31
+ keep the body and change only the options given.
32
+
33
+ If neither --global nor --project is specified, the node is looked up in the
34
+ project brain first (if one exists), then in the global brain.
35
+
36
+ Examples:
37
+ npx total-recall edit anti-patterns-7a7fb6a5 "Never run tests on the MacBook."
38
+ npx total-recall edit my-rule --priority absolute --modality must --global
39
+ cat new-body.md | npx total-recall edit my-fact -
40
+ `);
41
+ }
42
+
43
+ /** Path of `<slug>.md` inside any category directory of `vaultDir`, or null. */
44
+ export function findNodeFile(slug, vaultDir) {
45
+ if (!isSafeVaultName(slug) || !fs.existsSync(vaultDir)) return null;
46
+ for (const entry of fs.readdirSync(vaultDir, { withFileTypes: true })) {
47
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
48
+ const filePath = path.join(vaultDir, entry.name, `${slug}.md`);
49
+ if (fs.existsSync(filePath)) return filePath;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Parse edit arguments into { slug, body, changes, noCompile }.
56
+ * Throws with a user-facing message on invalid input.
57
+ */
58
+ export function parseEditArgs(args) {
59
+ const result = { slug: null, body: undefined, changes: {}, noCompile: false };
60
+ const positional = [];
61
+ for (let i = 0; i < args.length; i++) {
62
+ const arg = args[i];
63
+ const next = () => {
64
+ const value = args[++i];
65
+ if (value === undefined) throw new Error(`${arg} requires a value.`);
66
+ return value;
67
+ };
68
+ if (arg === '--no-compile') result.noCompile = true;
69
+ else if (arg === '--title') result.changes.title = next();
70
+ else if (arg === '--importance' || arg === '-i') {
71
+ const value = Number(next());
72
+ if (!Number.isInteger(value) || value < 1 || value > 5) {
73
+ throw new Error('--importance must be an integer from 1 to 5.');
74
+ }
75
+ result.changes.importance = value;
76
+ } else if (arg === '--priority' || arg === '-p') {
77
+ const value = next().toLowerCase();
78
+ if (!PRIORITIES.includes(value)) throw new Error(`--priority must be one of: ${PRIORITIES.join(', ')}.`);
79
+ result.changes.priority = value;
80
+ } else if (arg === '--modality' || arg === '-m') {
81
+ const value = next().toLowerCase();
82
+ if (!MODALITIES.includes(value)) throw new Error(`--modality must be one of: ${MODALITIES.join(', ')}.`);
83
+ result.changes.modality = value;
84
+ } else if (arg === '--tags' || arg === '-t') {
85
+ result.changes.tags = next().split(',').map((t) => t.trim()).filter(Boolean);
86
+ } else if (arg === '--status') {
87
+ const value = next().toLowerCase();
88
+ if (!STATUSES.includes(value)) throw new Error(`--status must be one of: ${STATUSES.join(', ')}.`);
89
+ result.changes.status = value;
90
+ } else if (/^--?[a-z]/i.test(arg)) {
91
+ throw new Error(`Unknown option: ${arg}`);
92
+ } else positional.push(arg);
93
+ }
94
+ if (positional.length > 2) throw new Error('Quote the new content as a single argument.');
95
+ [result.slug, result.body] = positional;
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Apply an edit to a parsed node. Returns the updated node and the list of
101
+ * changed field names. `immutable` follows `priority`, as in `remember`.
102
+ */
103
+ export function applyEdit(node, { body, changes }, now = new Date().toISOString()) {
104
+ const updated = { ...node };
105
+ const changed = [];
106
+ if (body !== undefined && body.trim() !== (node.body || '').trim()) {
107
+ updated.body = body.trim();
108
+ changed.push('body');
109
+ }
110
+ for (const [key, value] of Object.entries(changes)) {
111
+ if (JSON.stringify(node[key]) === JSON.stringify(value)) continue;
112
+ updated[key] = value;
113
+ changed.push(key);
114
+ }
115
+ if (changed.includes('title') && node.description === node.title) {
116
+ updated.description = updated.title;
117
+ }
118
+ if (changed.includes('priority')) {
119
+ if (updated.priority === 'absolute') updated.immutable = true;
120
+ else delete updated.immutable;
121
+ }
122
+ if (changed.length) updated.updated = now;
123
+ return { node: updated, changed };
124
+ }
125
+
126
+ function locate(slug, explicitLayer) {
127
+ const layers = explicitLayer === 'auto' ? ['project', 'global'] : [explicitLayer];
128
+ const brains = getBothBrains();
129
+ for (const layer of layers) {
130
+ if (layer === 'project' && !brains.project) continue;
131
+ const vaultDir = path.join(resolveBrainDir(layer), 'memory-vault');
132
+ const filePath = findNodeFile(slug, vaultDir);
133
+ if (filePath) return { layer, vaultDir, filePath };
134
+ }
135
+ return null;
136
+ }
137
+
138
+ async function readStdin() {
139
+ const chunks = [];
140
+ for await (const chunk of process.stdin) chunks.push(chunk);
141
+ return Buffer.concat(chunks).toString('utf8');
142
+ }
143
+
144
+ export default async function edit(args) {
145
+ const { layer: explicitLayer, remainingArgs } = parseLayerFlag(args);
146
+ if (!remainingArgs.length || remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
147
+ printHelp();
148
+ return;
149
+ }
150
+
151
+ let parsed;
152
+ try {
153
+ parsed = parseEditArgs(remainingArgs);
154
+ } catch (err) {
155
+ console.error(` ❌ ${err.message}`);
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+ if (parsed.body === '-') parsed.body = await readStdin();
160
+ if (parsed.body !== undefined && !parsed.body.trim()) {
161
+ console.error(' ❌ The new content is empty. Use `total-recall forget` to delete a node.');
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+
166
+ const found = locate(parsed.slug, explicitLayer);
167
+ if (!found) {
168
+ const where = explicitLayer === 'auto' ? 'project or global' : explicitLayer;
169
+ console.error(` ❌ Node "${parsed.slug}" not found in the ${where} vault.`);
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+
174
+ const { data, content } = matter(fs.readFileSync(found.filePath, 'utf8'));
175
+ const { node, changed } = applyEdit({ ...data, body: content.trim() }, parsed);
176
+ const layerLabel = `[${found.layer}]`;
177
+ const relPath = path.relative(path.dirname(found.vaultDir), found.filePath);
178
+
179
+ if (!changed.length) {
180
+ console.log(` ℹ️ Nothing to change in ${layerLabel} ${relPath}.`);
181
+ return;
182
+ }
183
+
184
+ const result = await writeNodeValidatedAsync(node, found.vaultDir);
185
+ if (!result.success) {
186
+ const errors = result.validation?.errors || [result.error || 'unknown validation failure'];
187
+ console.error(` ❌ Validation failed: ${errors.join('; ')}`);
188
+ process.exitCode = 1;
189
+ return;
190
+ }
191
+ console.log(` ✅ Updated ${layerLabel} ${relPath} (${changed.join(', ')})`);
192
+
193
+ if (parsed.noCompile) return;
194
+ console.log(' ⏳ Recompiling active memory surfaces and indexes in the background...');
195
+ try {
196
+ const { spawn } = await import('node:child_process');
197
+ const child = spawn(process.argv[0], [process.argv[1], 'compile', `--${found.layer}`], {
198
+ detached: true,
199
+ stdio: 'ignore',
200
+ });
201
+ child.unref();
202
+ console.log(' ✅ Background compilation started.');
203
+ } catch (err) {
204
+ console.warn(` ⚠️ Node updated, but background recompilation spawn failed: ${err.message}`);
205
+ }
206
+ }
package/src/cli/help.mjs CHANGED
@@ -146,7 +146,7 @@ function printGeneralHelp() {
146
146
  npx total-recall help architecture
147
147
 
148
148
  \x1b[1mCore Memory Operations:\x1b[0m
149
- \x1b[33mremember\x1b[0m, \x1b[33mrecall\x1b[0m, \x1b[33mforget\x1b[0m, \x1b[33mbrain\x1b[0m, \x1b[33mmap\x1b[0m
149
+ \x1b[33mremember\x1b[0m, \x1b[33mrecall\x1b[0m, \x1b[33mforget\x1b[0m, \x1b[33medit\x1b[0m, \x1b[33mbrain\x1b[0m, \x1b[33mmap\x1b[0m
150
150
 
151
151
  \x1b[1mCompilation & System Rules:\x1b[0m
152
152
  \x1b[33mcompile\x1b[0m, \x1b[33mrebuild\x1b[0m, \x1b[33mconnect\x1b[0m, \x1b[33mskill\x1b[0m, \x1b[33mimport-rules\x1b[0m