total-recall-brain 3.18.3 → 3.19.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/frontend/dist/assets/{index-CGqvdwHT.js → index-B6AfuKv9.js} +115 -115
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/update.ts +29 -4
- package/frontend/src/pages/HealthPage.tsx +12 -16
- package/frontend/src/pages/SettingsPage.tsx +22 -1
- package/package.json +1 -1
- package/src/cli/agent-dir.mjs +15 -1
- package/src/cli/chat.mjs +1 -1
- package/src/cli/daemon.mjs +2 -1
- package/src/cli/dream.mjs +14 -10
- package/src/cli/forget.mjs +14 -2
- package/src/cli/friction.mjs +1 -1
- package/src/cli/import-rules.mjs +8 -3
- package/src/cli/ingest.mjs +3 -2
- package/src/cli/lint.mjs +9 -6
- package/src/cli/rebuild.mjs +9 -4
- package/src/cli/remember.mjs +21 -15
- package/src/cli/secret.mjs +4 -1
- package/src/cli/share.mjs +5 -3
- package/src/cli/skill-projection.mjs +21 -2
- package/src/cli/snapshot.mjs +10 -8
- package/src/cli/update.mjs +2 -1
- package/src/core/clarity-rewriter.mjs +31 -12
- package/src/core/config.mjs +10 -4
- package/src/core/daemon-loop.mjs +2 -1
- package/src/core/embeddings.mjs +51 -3
- package/src/core/memory-title.mjs +70 -0
- package/src/core/runtime.mjs +11 -1
- package/src/core/secrets-store.mjs +7 -1
- package/src/core/skills-registry.mjs +12 -0
- package/src/core/surface.mjs +4 -4
- package/src/core/throttled-fetch.mjs +5 -0
- package/src/core/vault.mjs +7 -1
- package/src/server/index.mjs +56 -0
- package/src/server/route-manifest.json +16 -0
- package/src/server/routes/_shared.mjs +33 -1
- package/src/server/routes/context.mjs +14 -9
- package/src/server/routes/dashboard.mjs +39 -18
- package/src/server/routes/docs.mjs +27 -13
- package/src/server/routes/dream.mjs +10 -11
- package/src/server/routes/export.mjs +43 -12
- package/src/server/routes/field.mjs +11 -9
- package/src/server/routes/graph.mjs +31 -13
- package/src/server/routes/import.mjs +35 -16
- package/src/server/routes/instructions.mjs +41 -5
- package/src/server/routes/memory.mjs +61 -21
- package/src/server/routes/share.mjs +2 -1
- package/src/server/routes/ssss.mjs +11 -3
- package/src/server/routes/update.mjs +22 -3
- package/src/server/routes/vault.mjs +53 -28
|
@@ -1,52 +1,73 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { requireAuth, requireScope } from '../auth.mjs';
|
|
5
6
|
import { getNodes } from '../../core/vault-cache.mjs';
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
VAULT_DIR,
|
|
9
|
+
INSTRUCTIONS,
|
|
10
|
+
ROOT as SHARED_ROOT,
|
|
11
|
+
resolveVaultFromQuery,
|
|
12
|
+
pathsForVault,
|
|
13
|
+
} from './_shared.mjs';
|
|
7
14
|
|
|
8
15
|
const router = Router();
|
|
9
|
-
|
|
16
|
+
// Prefer shared ROOT (fileURLToPath-safe). Fall back with fileURLToPath — never URL.pathname
|
|
17
|
+
// (breaks on Windows and on paths with encoded characters).
|
|
18
|
+
const ROOT = process.env.TR_ROOT || SHARED_ROOT || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
10
19
|
|
|
11
20
|
router.get('/api/dashboard/instructions', requireAuth, requireScope('instructions:read'), (req, res) => {
|
|
21
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
22
|
+
const paths = pathsForVault(vaultDir);
|
|
23
|
+
// skillsDir = …/.agent/skills → agentDir = …/.agent; surfaces often at repo root (parent of .agent)
|
|
24
|
+
const agentDir = path.dirname(paths.skillsDir);
|
|
25
|
+
const surfaceBase =
|
|
26
|
+
path.basename(agentDir) === '.agent' ? path.dirname(agentDir) : agentDir;
|
|
27
|
+
|
|
12
28
|
const surfaces = [];
|
|
13
29
|
const surfaceFiles = ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md', 'INSTRUCTIONS.md'];
|
|
14
|
-
|
|
30
|
+
|
|
15
31
|
for (const name of surfaceFiles) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
// Prefer agent-adjacent surfaces; fall back to package ROOT for monorepo dashboard
|
|
33
|
+
const candidates = [
|
|
34
|
+
path.join(surfaceBase, name),
|
|
35
|
+
path.join(agentDir, name),
|
|
36
|
+
path.join(ROOT, name),
|
|
37
|
+
];
|
|
38
|
+
let filePath = candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
39
|
+
|
|
23
40
|
let size = 0;
|
|
24
41
|
let lastCompiled = '';
|
|
25
42
|
let active = false;
|
|
26
|
-
|
|
43
|
+
|
|
27
44
|
if (fs.existsSync(filePath)) {
|
|
28
45
|
const stat = fs.statSync(filePath);
|
|
29
46
|
size = stat.size;
|
|
30
47
|
lastCompiled = stat.mtime.toISOString();
|
|
31
48
|
active = true;
|
|
32
49
|
}
|
|
33
|
-
|
|
50
|
+
|
|
34
51
|
surfaces.push({
|
|
35
52
|
name,
|
|
36
53
|
filename: name,
|
|
37
54
|
size,
|
|
38
55
|
lastCompiled,
|
|
39
|
-
active
|
|
56
|
+
active,
|
|
40
57
|
});
|
|
41
58
|
}
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
59
|
+
|
|
60
|
+
const instructionsFile = paths.instructionsFile || INSTRUCTIONS;
|
|
61
|
+
const lastCompileTimestamp = fs.existsSync(instructionsFile)
|
|
62
|
+
? fs.statSync(instructionsFile).mtime.toISOString()
|
|
63
|
+
: '';
|
|
64
|
+
const totalNodes = fs.existsSync(vaultDir) ? getNodes(vaultDir).length : 0;
|
|
65
|
+
|
|
46
66
|
res.json({
|
|
47
67
|
surfaces,
|
|
48
68
|
lastCompileTimestamp,
|
|
49
|
-
totalNodes
|
|
69
|
+
totalNodes,
|
|
70
|
+
vault_dir: vaultDir,
|
|
50
71
|
});
|
|
51
72
|
});
|
|
52
73
|
|
|
@@ -27,10 +27,26 @@ function getRegistryTypes(brainDir) {
|
|
|
27
27
|
return {};
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// Ensure the path is safe
|
|
30
|
+
// Ensure the path is safe (relative, no traversal)
|
|
31
31
|
function isSafePath(p) {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
if (!p || typeof p !== 'string' || p.includes('\0')) return false;
|
|
33
|
+
if (path.isAbsolute(p)) return false;
|
|
34
|
+
const normalized = path.normalize(p);
|
|
35
|
+
// Reject any path that still escapes after normalize
|
|
36
|
+
if (normalized.startsWith('..') || normalized.includes(`${path.sep}..${path.sep}`) || normalized.endsWith(`${path.sep}..`)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (normalized.includes('..')) return false;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolve relPath under vaultDir; returns null if it escapes the vault. */
|
|
44
|
+
function resolveSafeVaultPath(vaultDir, relPath) {
|
|
45
|
+
if (!isSafePath(relPath)) return null;
|
|
46
|
+
const root = path.resolve(vaultDir);
|
|
47
|
+
const abs = path.resolve(vaultDir, relPath);
|
|
48
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) return null;
|
|
49
|
+
return abs;
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
/**
|
|
@@ -108,9 +124,8 @@ router.get('/api/docs/read', requireAuth, requireScope('ssss:read'), (req, res)
|
|
|
108
124
|
try {
|
|
109
125
|
const vaultDir = resolveVaultFromQuery(req);
|
|
110
126
|
const relPath = req.query.path;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const absPath = path.join(vaultDir, relPath);
|
|
127
|
+
const absPath = resolveSafeVaultPath(vaultDir, relPath);
|
|
128
|
+
if (!absPath) return badRequest(res, 'Invalid path');
|
|
114
129
|
if (!fs.existsSync(absPath)) return notFound(res, 'Document not found');
|
|
115
130
|
|
|
116
131
|
const raw = fs.readFileSync(absPath, 'utf8');
|
|
@@ -134,8 +149,8 @@ router.post('/api/docs', requireAuth, requireScope('ssss:write'), async (req, re
|
|
|
134
149
|
const vaultDir = resolveVaultFromQuery(req);
|
|
135
150
|
const { path: relPath, content } = req.body;
|
|
136
151
|
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
const absPath = resolveSafeVaultPath(vaultDir, relPath);
|
|
153
|
+
if (!absPath) return badRequest(res, 'Invalid path');
|
|
139
154
|
if (fs.existsSync(absPath)) return res.status(409).json({ error: 'Document already exists' });
|
|
140
155
|
|
|
141
156
|
const envelope = {
|
|
@@ -164,8 +179,8 @@ router.put('/api/docs', requireAuth, requireScope('ssss:write'), async (req, res
|
|
|
164
179
|
const vaultDir = resolveVaultFromQuery(req);
|
|
165
180
|
const { path: relPath, content } = req.body;
|
|
166
181
|
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
const absPath = resolveSafeVaultPath(vaultDir, relPath);
|
|
183
|
+
if (!absPath) return badRequest(res, 'Invalid path');
|
|
169
184
|
if (!fs.existsSync(absPath)) return notFound(res, 'Document not found');
|
|
170
185
|
|
|
171
186
|
const envelope = {
|
|
@@ -193,9 +208,8 @@ router.delete('/api/docs', requireAuth, requireScope('ssss:write'), async (req,
|
|
|
193
208
|
try {
|
|
194
209
|
const vaultDir = resolveVaultFromQuery(req);
|
|
195
210
|
const relPath = req.query.path;
|
|
196
|
-
|
|
197
|
-
if (!
|
|
198
|
-
const absPath = path.join(vaultDir, relPath);
|
|
211
|
+
const absPath = resolveSafeVaultPath(vaultDir, relPath);
|
|
212
|
+
if (!absPath) return badRequest(res, 'Invalid path');
|
|
199
213
|
if (!fs.existsSync(absPath)) return notFound(res, 'Document not found');
|
|
200
214
|
|
|
201
215
|
const envelope = {
|
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
3
|
import { requireAuth, requireScope } from '../auth.mjs';
|
|
6
|
-
import { serverError,
|
|
7
|
-
import { logger } from '../../core/logger.mjs';
|
|
4
|
+
import { serverError, resolveVaultFromQuery, pathsForVault } from './_shared.mjs';
|
|
8
5
|
|
|
9
6
|
const router = Router();
|
|
10
7
|
|
|
11
8
|
router.post('/api/dream', requireAuth, requireScope('memory:recompile'), async (req, res) => {
|
|
12
9
|
try {
|
|
13
10
|
const { runDreamCycle } = await import('../../core/dream.mjs');
|
|
14
|
-
const
|
|
15
|
-
|
|
11
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
12
|
+
const paths = pathsForVault(vaultDir);
|
|
13
|
+
const conflictsDir = path.join(paths.brainDir, 'memory-inbox', 'conflicts');
|
|
14
|
+
|
|
16
15
|
const result = await runDreamCycle({
|
|
17
|
-
vaultDir
|
|
18
|
-
skillsDir:
|
|
19
|
-
derivedDir:
|
|
16
|
+
vaultDir,
|
|
17
|
+
skillsDir: paths.skillsDir,
|
|
18
|
+
derivedDir: paths.derivedDir,
|
|
20
19
|
conflictsDir,
|
|
21
|
-
instructionsFile:
|
|
20
|
+
instructionsFile: paths.instructionsFile,
|
|
22
21
|
});
|
|
23
22
|
|
|
24
|
-
res.json({ success: true, status: result.status });
|
|
23
|
+
res.json({ success: true, status: result.status, vault_dir: vaultDir });
|
|
25
24
|
} catch (err) {
|
|
26
25
|
serverError(res, err);
|
|
27
26
|
}
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
4
|
import { spawn } from 'node:child_process';
|
|
6
5
|
import { requireAuth, requireScope } from '../auth.mjs';
|
|
7
|
-
import {
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
serverError,
|
|
8
|
+
resolveVaultFromQuery,
|
|
9
|
+
pathsForVault,
|
|
10
|
+
} from './_shared.mjs';
|
|
9
11
|
|
|
10
12
|
const router = Router();
|
|
11
13
|
|
|
12
14
|
router.get('/api/brain/export', requireAuth, requireScope('brain:export'), async (req, res) => {
|
|
13
15
|
try {
|
|
16
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
17
|
+
const paths = pathsForVault(vaultDir);
|
|
18
|
+
const brainDir = paths.brainDir;
|
|
19
|
+
|
|
14
20
|
const ALL_PARTS = {
|
|
15
|
-
vault:
|
|
16
|
-
derived:
|
|
17
|
-
sessions:
|
|
18
|
-
config: path.join(
|
|
19
|
-
skills:
|
|
21
|
+
vault: vaultDir,
|
|
22
|
+
derived: paths.derivedDir,
|
|
23
|
+
sessions: paths.sessionsDir,
|
|
24
|
+
config: path.join(brainDir, 'config'),
|
|
25
|
+
skills: paths.skillsDir,
|
|
20
26
|
};
|
|
21
27
|
const requested = req.query.include
|
|
22
28
|
? String(req.query.include).split(',').map(s => s.trim())
|
|
@@ -27,14 +33,39 @@ router.get('/api/brain/export', requireAuth, requireScope('brain:export'), async
|
|
|
27
33
|
const date = new Date().toISOString().slice(0, 10);
|
|
28
34
|
res.setHeader('Content-Type', 'application/gzip');
|
|
29
35
|
res.setHeader('Content-Disposition', `attachment; filename="total-recall-brain-${date}.tar.gz"`);
|
|
36
|
+
res.setHeader('X-Total-Recall-Vault', vaultDir);
|
|
37
|
+
|
|
38
|
+
// Only pack paths under brainDir (avoid `..` parent escapes for skills)
|
|
39
|
+
const relativeDirs = [];
|
|
40
|
+
for (const k of dirs) {
|
|
41
|
+
const abs = ALL_PARTS[k];
|
|
42
|
+
const rel = path.relative(brainDir, abs);
|
|
43
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
44
|
+
// skills lives as sibling of total-recall under .agent/skills — skip unsafe parent
|
|
45
|
+
if (k === 'skills') continue;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
relativeDirs.push(rel || '.');
|
|
49
|
+
}
|
|
50
|
+
if (relativeDirs.length === 0) {
|
|
51
|
+
// Always can pack vault at least
|
|
52
|
+
relativeDirs.push(path.relative(brainDir, vaultDir) || 'memory-vault');
|
|
53
|
+
}
|
|
30
54
|
|
|
31
|
-
const
|
|
32
|
-
|
|
55
|
+
const tar = spawn('tar', [
|
|
56
|
+
'czf', '-',
|
|
57
|
+
'-C', brainDir,
|
|
58
|
+
'--exclude=security.yml',
|
|
59
|
+
'--exclude=keys.jsonl',
|
|
60
|
+
'--exclude=session-secret',
|
|
61
|
+
...relativeDirs,
|
|
62
|
+
], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
33
63
|
tar.stdout.pipe(res);
|
|
34
64
|
tar.on('error', err => { if (!res.headersSent) serverError(res, err); });
|
|
35
65
|
tar.on('close', code => { if (code !== 0 && !res.writableEnded) res.end(); });
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
} catch (err) {
|
|
67
|
+
serverError(res, err);
|
|
68
|
+
}
|
|
38
69
|
});
|
|
39
70
|
|
|
40
71
|
export default router;
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
2
|
import { requireAuth, requireScope } from '../auth.mjs';
|
|
6
|
-
import { serverError,
|
|
7
|
-
import { logger } from '../../core/logger.mjs';
|
|
3
|
+
import { serverError, badRequest, resolveVaultFromQuery, pathsForVault } from './_shared.mjs';
|
|
8
4
|
|
|
9
5
|
const router = Router();
|
|
10
6
|
|
|
11
7
|
router.post('/api/field/compile', requireAuth, requireScope('memory:recompile'), async (req, res) => {
|
|
12
8
|
try {
|
|
13
9
|
const { compileField } = await import('../../core/vector-field.mjs');
|
|
14
|
-
const
|
|
15
|
-
|
|
10
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
11
|
+
const { derivedDir } = pathsForVault(vaultDir);
|
|
12
|
+
const result = await compileField({ vaultDir, derivedDir });
|
|
13
|
+
res.json({ compiled: true, vault_dir: vaultDir, ...result.meta });
|
|
16
14
|
} catch (err) {
|
|
17
15
|
serverError(res, err);
|
|
18
16
|
}
|
|
@@ -23,12 +21,14 @@ router.post('/api/field/sample', requireAuth, requireScope('memory:read'), async
|
|
|
23
21
|
const { sampleField } = await import('../../core/vector-field.mjs');
|
|
24
22
|
const { query, top_k, entanglement_boost, velocity_weight } = req.body || {};
|
|
25
23
|
if (!query) return badRequest(res, 'query is required');
|
|
24
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
25
|
+
const { derivedDir } = pathsForVault(vaultDir);
|
|
26
26
|
const result = await sampleField({
|
|
27
27
|
query,
|
|
28
28
|
topK: top_k,
|
|
29
29
|
entanglementBoost: entanglement_boost,
|
|
30
30
|
velocityWeight: velocity_weight,
|
|
31
|
-
derivedDir
|
|
31
|
+
derivedDir,
|
|
32
32
|
});
|
|
33
33
|
res.json(result);
|
|
34
34
|
} catch (err) {
|
|
@@ -39,7 +39,9 @@ router.post('/api/field/sample', requireAuth, requireScope('memory:read'), async
|
|
|
39
39
|
router.get('/api/field/stats', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
40
40
|
try {
|
|
41
41
|
const { fieldStats } = await import('../../core/vector-field.mjs');
|
|
42
|
-
|
|
42
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
43
|
+
const { derivedDir } = pathsForVault(vaultDir);
|
|
44
|
+
res.json({ vault_dir: vaultDir, ...fieldStats(derivedDir) });
|
|
43
45
|
} catch (err) {
|
|
44
46
|
serverError(res, err);
|
|
45
47
|
}
|
|
@@ -30,14 +30,24 @@ import {
|
|
|
30
30
|
ROOT,
|
|
31
31
|
badRequest,
|
|
32
32
|
serverError,
|
|
33
|
+
resolveVaultFromQuery,
|
|
34
|
+
pathsForVault,
|
|
33
35
|
} from './_shared.mjs';
|
|
34
36
|
|
|
35
37
|
const router = Router();
|
|
36
38
|
|
|
37
39
|
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Feature flag file locations:
|
|
43
|
+
* - Production SSSS layout: <vault>/preferences/dashboard-enhanced.md
|
|
44
|
+
* - Legacy/test layout: <vault>/../preferences/dashboard-enhanced.md
|
|
45
|
+
*/
|
|
46
|
+
function isDashboardEnhanced(vaultDir = VAULT_DIR) {
|
|
47
|
+
return (
|
|
48
|
+
fs.existsSync(path.join(vaultDir, 'preferences', 'dashboard-enhanced.md')) ||
|
|
49
|
+
fs.existsSync(path.join(vaultDir, '..', 'preferences', 'dashboard-enhanced.md'))
|
|
50
|
+
);
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function sha256(content) {
|
|
@@ -124,19 +134,23 @@ function safeReferencePath(name) {
|
|
|
124
134
|
* Requires dashboard-enhanced feature flag.
|
|
125
135
|
*/
|
|
126
136
|
router.get('/api/graph', requireAuth, requireScope('ssss:read'), (req, res) => {
|
|
127
|
-
|
|
137
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
138
|
+
if (!isDashboardEnhanced(vaultDir)) {
|
|
128
139
|
return res.status(404).json({ error: 'dashboard-enhanced feature flag not enabled' });
|
|
129
140
|
}
|
|
130
141
|
try {
|
|
131
|
-
const
|
|
132
|
-
|
|
142
|
+
const { derivedDir } = pathsForVault(vaultDir);
|
|
143
|
+
// Prefer brain-local derived; fall back to shared DERIVED_DIR (tests/mocks)
|
|
144
|
+
const derived = fs.existsSync(derivedDir) ? derivedDir : DERIVED_DIR;
|
|
145
|
+
const graphFile = path.join(derived, 'graph-index.jsonl');
|
|
146
|
+
const routesFile = path.join(derived, 'skill-routes.jsonl');
|
|
133
147
|
const nodes = fs.existsSync(graphFile)
|
|
134
148
|
? fs.readFileSync(graphFile, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l))
|
|
135
149
|
: [];
|
|
136
150
|
const routes = fs.existsSync(routesFile)
|
|
137
151
|
? fs.readFileSync(routesFile, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l))
|
|
138
152
|
: [];
|
|
139
|
-
res.json({ nodes, routes });
|
|
153
|
+
res.json({ nodes, routes, vault_dir: vaultDir });
|
|
140
154
|
} catch (err) { serverError(res, err); }
|
|
141
155
|
});
|
|
142
156
|
|
|
@@ -146,11 +160,13 @@ router.get('/api/graph', requireAuth, requireScope('ssss:read'), (req, res) => {
|
|
|
146
160
|
* Requires dashboard-enhanced feature flag.
|
|
147
161
|
*/
|
|
148
162
|
router.get('/api/conflicts', requireAuth, requireScope('ssss:read'), async (req, res) => {
|
|
149
|
-
|
|
163
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
164
|
+
if (!isDashboardEnhanced(vaultDir)) {
|
|
150
165
|
return res.status(404).json({ error: 'dashboard-enhanced feature flag not enabled' });
|
|
151
166
|
}
|
|
152
167
|
try {
|
|
153
|
-
const
|
|
168
|
+
const paths = pathsForVault(vaultDir);
|
|
169
|
+
const conflictsDir = path.join(paths.brainDir, 'memory-inbox', 'conflicts');
|
|
154
170
|
const conflicts = [];
|
|
155
171
|
|
|
156
172
|
if (fs.existsSync(conflictsDir)) {
|
|
@@ -166,9 +182,9 @@ router.get('/api/conflicts', requireAuth, requireScope('ssss:read'), async (req,
|
|
|
166
182
|
}
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
// Dynamic scan
|
|
185
|
+
// Dynamic scan for the requested brain (not always global)
|
|
170
186
|
const { detectSemanticConflicts } = await import('../../core/conflict-detector.mjs');
|
|
171
|
-
const list = getNodes(
|
|
187
|
+
const list = getNodes(vaultDir);
|
|
172
188
|
const dynamicConflicts = [];
|
|
173
189
|
for (let i = 0; i < list.length; i++) {
|
|
174
190
|
const found = detectSemanticConflicts(list[i], list.slice(0, i));
|
|
@@ -207,15 +223,17 @@ router.post('/api/conflicts/resolve', requireAuth, requireScope('memory:write'),
|
|
|
207
223
|
return badRequest(res, "action must be either 'keep' or 'supersede'");
|
|
208
224
|
}
|
|
209
225
|
|
|
210
|
-
const
|
|
226
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
227
|
+
const paths = pathsForVault(vaultDir);
|
|
228
|
+
const inboxDir = path.join(paths.brainDir, 'memory-inbox');
|
|
211
229
|
const { resolveConflict } = await import('../../core/conflict-detector.mjs');
|
|
212
230
|
const result = resolveConflict(conflict_id, inboxDir, action, winner_slug);
|
|
213
231
|
if (!result.resolved) {
|
|
214
232
|
return badRequest(res, result.error || 'Failed to resolve conflict');
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
invalidate();
|
|
218
|
-
res.json({ success: true, conflict_id });
|
|
235
|
+
invalidate(vaultDir);
|
|
236
|
+
res.json({ success: true, conflict_id, vault_dir: vaultDir });
|
|
219
237
|
} catch (err) {
|
|
220
238
|
serverError(res, err);
|
|
221
239
|
}
|
|
@@ -1,39 +1,58 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
3
|
import { requireAuth, requireScope } from '../auth.mjs';
|
|
6
|
-
import {
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
serverError,
|
|
6
|
+
ROOT,
|
|
7
|
+
VAULT_DIR,
|
|
8
|
+
badRequest,
|
|
9
|
+
resolveVaultFromQuery,
|
|
10
|
+
} from './_shared.mjs';
|
|
8
11
|
import { detectRuleFiles, importRuleFiles } from '../../core/import-rules.mjs';
|
|
9
12
|
|
|
10
13
|
const router = Router();
|
|
11
14
|
|
|
12
15
|
router.get('/api/import/rules', requireAuth, requireScope('memory:read'), (req, res) => {
|
|
13
16
|
try {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
18
|
+
const allowedRoots = [ROOT, VAULT_DIR, vaultDir, path.dirname(ROOT)];
|
|
19
|
+
const rawDirs = req.query.dir ? (Array.isArray(req.query.dir) ? req.query.dir : [req.query.dir]) : [ROOT];
|
|
20
|
+
const dirs = rawDirs.filter(d =>
|
|
21
|
+
allowedRoots.some(root => path.resolve(d).startsWith(path.resolve(root))),
|
|
22
|
+
);
|
|
17
23
|
if (dirs.length === 0) return badRequest(res, 'No permitted directories specified');
|
|
18
24
|
const detected = detectRuleFiles(dirs);
|
|
19
|
-
res.json({ dirs, detected });
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
res.json({ dirs, detected, vault_dir: vaultDir });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
serverError(res, err);
|
|
28
|
+
}
|
|
22
29
|
});
|
|
23
30
|
|
|
24
31
|
router.post('/api/import/rules', requireAuth, requireScope('memory:write'), async (req, res) => {
|
|
25
32
|
try {
|
|
26
33
|
const { dirs, force = false, dryRun = false } = req.body || {};
|
|
27
|
-
const
|
|
34
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
35
|
+
const searchDirs = dirs?.length ? dirs : [ROOT];
|
|
28
36
|
const detected = detectRuleFiles(searchDirs);
|
|
29
37
|
const toImport = req.body?.files?.length
|
|
30
38
|
? detected.filter(f => req.body.files.includes(f.absolutePath))
|
|
31
39
|
: detected.filter(f => !f.alreadyImported || force);
|
|
32
|
-
if (dryRun)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
if (dryRun) {
|
|
41
|
+
return res.json({
|
|
42
|
+
dryRun: true,
|
|
43
|
+
detected,
|
|
44
|
+
toImport,
|
|
45
|
+
imported: [],
|
|
46
|
+
skipped: [],
|
|
47
|
+
failed: [],
|
|
48
|
+
vault_dir: vaultDir,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const result = await importRuleFiles(toImport, { force, vaultDir });
|
|
52
|
+
res.json({ detected, vault_dir: vaultDir, ...result });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
serverError(res, err);
|
|
55
|
+
}
|
|
37
56
|
});
|
|
38
57
|
|
|
39
58
|
export default router;
|
|
@@ -1,15 +1,49 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
4
|
import { requireAuth, requireScope } from '../auth.mjs';
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
serverError,
|
|
7
|
+
INSTRUCTIONS,
|
|
8
|
+
ROOT,
|
|
9
|
+
resolveVaultFromQuery,
|
|
10
|
+
pathsForVault,
|
|
11
|
+
} from './_shared.mjs';
|
|
7
12
|
import { logger } from '../../core/logger.mjs';
|
|
8
13
|
|
|
9
14
|
const router = Router();
|
|
10
15
|
|
|
16
|
+
function resolveInstructionsPath(req) {
|
|
17
|
+
const vaultDir = resolveVaultFromQuery(req);
|
|
18
|
+
const paths = pathsForVault(vaultDir);
|
|
19
|
+
// Prefer brain-local INSTRUCTIONS, then shared constant, then package ROOT
|
|
20
|
+
const candidates = [
|
|
21
|
+
paths.instructionsFile,
|
|
22
|
+
// surface compile often writes to parent of .agent (repo root)
|
|
23
|
+
path.join(path.dirname(path.dirname(paths.skillsDir)), 'INSTRUCTIONS.md'),
|
|
24
|
+
INSTRUCTIONS,
|
|
25
|
+
path.join(ROOT, 'INSTRUCTIONS.md'),
|
|
26
|
+
];
|
|
27
|
+
for (const p of candidates) {
|
|
28
|
+
if (p && fs.existsSync(p)) return p;
|
|
29
|
+
}
|
|
30
|
+
return paths.instructionsFile || INSTRUCTIONS;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sendTextResource(res, filePath, label) {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(filePath)) {
|
|
36
|
+
return res.status(404).json({ error: `${label} not found`, path: filePath });
|
|
37
|
+
}
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
39
|
+
res.type('text/markdown').send(content);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
serverError(res, err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
11
45
|
router.get('/api/instructions', requireAuth, requireScope('instructions:read'), (req, res) => {
|
|
12
|
-
return sendTextResource(res,
|
|
46
|
+
return sendTextResource(res, resolveInstructionsPath(req), 'instructions');
|
|
13
47
|
});
|
|
14
48
|
|
|
15
49
|
router.put('/api/instructions', requireAuth, requireScope('instructions:write'), (req, res) => {
|
|
@@ -18,8 +52,10 @@ router.put('/api/instructions', requireAuth, requireScope('instructions:write'),
|
|
|
18
52
|
return res.status(400).json({ error: 'Missing or invalid `content` field.' });
|
|
19
53
|
}
|
|
20
54
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
55
|
+
const target = resolveInstructionsPath(req);
|
|
56
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
57
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
58
|
+
return res.json({ success: true, message: 'Instructions updated successfully', path: target });
|
|
23
59
|
} catch (err) {
|
|
24
60
|
logger.error('api', `Failed to write instructions: ${err.message}`);
|
|
25
61
|
return res.status(500).json({ error: err.message });
|