total-recall-brain 3.24.0 → 3.24.2
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-BQL-opnK.js +5742 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/favicon.png +0 -0
- package/frontend/dist/favicon.svg +3 -6
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/api.spec.ts +2 -1
- package/frontend/src/api/system.ts +6 -0
- package/frontend/src/api/update.ts +1 -4
- package/frontend/src/components/MobilePairing.tsx +25 -18
- package/frontend/src/pages/ChatPage.tsx +25 -1
- package/frontend/src/pages/HealthPage.tsx +52 -3
- package/frontend/src/pages/SettingsPage.tsx +35 -8
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/SKILL.md +13 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +2 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +2 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +166 -61
- package/src/cli/backup.mjs +63 -0
- package/src/core/package-auto-update.mjs +13 -9
- package/src/core/surface.mjs +51 -22
- package/src/server/route-manifest.json +4 -0
- package/src/server/routes/system.mjs +15 -0
- package/src/server/routes/update.mjs +1 -1
- package/frontend/dist/assets/index-CtCDaZYZ.js +0 -5742
|
@@ -1,83 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-repo — refresh this brain's skill definitions from the upstream templates.
|
|
4
|
+
*
|
|
5
|
+
* Replaces a stub that printed "Synced and verified skill: <name>", "Merging core
|
|
6
|
+
* invariants non-destructively" and "Sync Completed Successfully!" without
|
|
7
|
+
* fetching, merging or writing anything — while the skill's Core Directive #4
|
|
8
|
+
* told agents to run it to keep skills current. It also probed
|
|
9
|
+
* <agent>/memory-vault, which is not where a brain lives, so on a correct
|
|
10
|
+
* install it aborted with a misleading "run init first".
|
|
11
|
+
*
|
|
12
|
+
* What it actually does now:
|
|
13
|
+
* - resolves a real template source (env override, installed package,
|
|
14
|
+
* running-from-source checkout, or a shallow clone of upstream)
|
|
15
|
+
* - copies skill files, PRESERVING each destination's compiled
|
|
16
|
+
* "<!-- BEGIN INJECTED MEMORY -->" block, which is per-repo and must not be
|
|
17
|
+
* overwritten by a template
|
|
18
|
+
* - never touches memory-vault/ — that is user memory, not a template
|
|
19
|
+
* - reports the files it really changed, and exits non-zero if it changed nothing
|
|
20
|
+
* because it could not find a source
|
|
21
|
+
*
|
|
22
|
+
* Usage: node sync-repo.mjs [--dry-run] [--source <dir>]
|
|
23
|
+
*/
|
|
1
24
|
import fs from 'node:fs';
|
|
2
25
|
import path from 'node:path';
|
|
3
26
|
import os from 'node:os';
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
4
28
|
import { fileURLToPath } from 'node:url';
|
|
5
29
|
|
|
6
|
-
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const DRY = process.argv.includes('--dry-run');
|
|
32
|
+
const srcFlag = process.argv.indexOf('--source');
|
|
33
|
+
const SRC_OVERRIDE = srcFlag !== -1 ? process.argv[srcFlag + 1] : process.env.TR_SCAFFOLD_DIR;
|
|
34
|
+
|
|
7
35
|
const home = os.homedir();
|
|
8
36
|
const AGENT_DIR = process.env.AGENT_DIR || path.join(home, '.agent');
|
|
9
|
-
|
|
37
|
+
// A brain is <agent>/skills/total-recall — memory-vault, config and the rest all
|
|
38
|
+
// live INSIDE it. The old code looked for <agent>/memory-vault and never found one.
|
|
39
|
+
const BRAIN_DIR = path.join(AGENT_DIR, 'skills', 'total-recall');
|
|
10
40
|
const SKILLS_DIR = path.join(AGENT_DIR, 'skills');
|
|
11
41
|
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
const INJECT_START = '<!-- BEGIN INJECTED MEMORY';
|
|
43
|
+
const INJECT_END = '<!-- END INJECTED MEMORY -->';
|
|
44
|
+
|
|
45
|
+
function log(m) { console.log(m); }
|
|
46
|
+
function fail(m) { console.error(`❌ ${m}`); process.exit(1); }
|
|
47
|
+
|
|
48
|
+
/** Locate real template files. Returns { dir, origin } or null. */
|
|
49
|
+
function resolveTemplateSource() {
|
|
50
|
+
if (SRC_OVERRIDE) {
|
|
51
|
+
if (!fs.existsSync(SRC_OVERRIDE)) fail(`--source path does not exist: ${SRC_OVERRIDE}`);
|
|
52
|
+
return { dir: SRC_OVERRIDE, origin: `explicit: ${SRC_OVERRIDE}` };
|
|
53
|
+
}
|
|
54
|
+
// Walk up looking for the package root rather than counting '..' segments:
|
|
55
|
+
// this script legitimately runs from two different depths (a brain at
|
|
56
|
+
// <agent>/skills/total-recall/scripts, and the template copy at
|
|
57
|
+
// <repo>/scaffold/.agent/skills/total-recall/scripts). A fixed relative path
|
|
58
|
+
// is right for one and silently wrong for the other — which resolved to the
|
|
59
|
+
// INSTALLED (older) package and would have reverted newer local templates.
|
|
60
|
+
const fromCheckout = (() => {
|
|
61
|
+
let dir = __dirname;
|
|
62
|
+
for (let i = 0; i < 8; i += 1) {
|
|
63
|
+
const pkg = path.join(dir, 'package.json');
|
|
64
|
+
const scaffold = path.join(dir, 'scaffold', '.agent', 'skills');
|
|
65
|
+
if (fs.existsSync(pkg) && fs.existsSync(scaffold)) {
|
|
66
|
+
try {
|
|
67
|
+
if (JSON.parse(fs.readFileSync(pkg, 'utf8')).name === 'total-recall-brain') return scaffold;
|
|
68
|
+
} catch { /* keep walking */ }
|
|
69
|
+
}
|
|
70
|
+
const up = path.dirname(dir);
|
|
71
|
+
if (up === dir) break;
|
|
72
|
+
dir = up;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
})();
|
|
14
76
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
77
|
+
const candidates = [
|
|
78
|
+
[fromCheckout, 'source checkout'],
|
|
79
|
+
[path.join(process.cwd(), 'node_modules', 'total-recall-brain', 'scaffold', '.agent', 'skills'), 'installed package'],
|
|
80
|
+
[path.join(home, 'node_modules', 'total-recall-brain', 'scaffold', '.agent', 'skills'), 'installed package (home)'],
|
|
81
|
+
];
|
|
82
|
+
for (const [dir, origin] of candidates) {
|
|
83
|
+
if (dir && fs.existsSync(dir) && fs.statSync(dir).isDirectory()) return { dir, origin };
|
|
22
84
|
}
|
|
23
|
-
|
|
24
|
-
// Use default
|
|
85
|
+
return null;
|
|
25
86
|
}
|
|
26
|
-
console.log(`🌐 Upstream Repository URL: ${upstreamUrl}`);
|
|
27
87
|
|
|
28
|
-
|
|
29
|
-
|
|
88
|
+
/** Last resort: shallow-clone upstream into a temp dir. */
|
|
89
|
+
function cloneUpstream() {
|
|
90
|
+
let upstream = 'https://github.com/gregiteen/total-recall';
|
|
30
91
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error('Total Recall directories are missing. Run "npx total-recall init" first.');
|
|
92
|
+
const cfg = path.join(BRAIN_DIR, 'config', 'brain.json');
|
|
93
|
+
if (fs.existsSync(cfg)) {
|
|
94
|
+
const parsed = JSON.parse(fs.readFileSync(cfg, 'utf8'));
|
|
95
|
+
if (parsed.upstream) upstream = parsed.upstream;
|
|
36
96
|
}
|
|
97
|
+
} catch { /* default */ }
|
|
98
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tr-sync-'));
|
|
99
|
+
log(`🌐 No local templates — shallow-cloning ${upstream}`);
|
|
100
|
+
try {
|
|
101
|
+
execFileSync('git', ['clone', '--depth', '1', '--quiet', upstream, tmp], { stdio: 'pipe' });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
104
|
+
fail(`could not clone upstream (${err.message.split('\n')[0]}). Pass --source <dir> to sync offline.`);
|
|
105
|
+
}
|
|
106
|
+
const dir = path.join(tmp, 'scaffold', '.agent', 'skills');
|
|
107
|
+
if (!fs.existsSync(dir)) {
|
|
108
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
109
|
+
fail('upstream clone has no scaffold/.agent/skills — cannot sync.');
|
|
110
|
+
}
|
|
111
|
+
return { dir, origin: `clone of ${upstream}`, cleanup: tmp };
|
|
112
|
+
}
|
|
37
113
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
114
|
+
/** Copy src over dst, carrying dst's compiled injected-memory block across. */
|
|
115
|
+
function writePreservingInjectedBlock(srcFile, dstFile) {
|
|
116
|
+
let next = fs.readFileSync(srcFile, 'utf8');
|
|
117
|
+
if (fs.existsSync(dstFile)) {
|
|
118
|
+
const cur = fs.readFileSync(dstFile, 'utf8');
|
|
119
|
+
const s = cur.indexOf(INJECT_START);
|
|
120
|
+
const e = cur.indexOf(INJECT_END);
|
|
121
|
+
if (s !== -1 && e !== -1 && e > s) {
|
|
122
|
+
const block = cur.slice(s, e + INJECT_END.length);
|
|
123
|
+
const ns = next.indexOf(INJECT_START);
|
|
124
|
+
const ne = next.indexOf(INJECT_END);
|
|
125
|
+
next = (ns !== -1 && ne !== -1 && ne > ns)
|
|
126
|
+
? next.slice(0, ns) + block + next.slice(ne + INJECT_END.length)
|
|
127
|
+
: `${next.replace(/\s*$/, '')}\n\n${block}\n`;
|
|
52
128
|
}
|
|
129
|
+
if (cur === next) return 'identical';
|
|
130
|
+
}
|
|
131
|
+
if (!DRY) {
|
|
132
|
+
fs.mkdirSync(path.dirname(dstFile), { recursive: true });
|
|
133
|
+
fs.writeFileSync(dstFile, next);
|
|
134
|
+
}
|
|
135
|
+
return 'updated';
|
|
136
|
+
}
|
|
53
137
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
derivedDir,
|
|
68
|
-
instructionsFile
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
console.log(`🚀 Compilation Complete: Processed ${result.nodesProcessed} nodes, injected ${result.skillsInjected} skills.`);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.warn(`⚠️ Compilation skipped or ran into error: ${err.message}`);
|
|
74
|
-
}
|
|
138
|
+
function walk(dir) {
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
141
|
+
const p = path.join(dir, e.name);
|
|
142
|
+
if (e.isDirectory()) {
|
|
143
|
+
// memory-vault is the user's own memory. A template must never overwrite it.
|
|
144
|
+
if (e.name === 'memory-vault' || e.name === 'memory-derived' || e.name === 'sessions') continue;
|
|
145
|
+
if (e.name === '.git' || e.name === 'node_modules') continue;
|
|
146
|
+
out.push(...walk(p));
|
|
147
|
+
} else out.push(p);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
75
151
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
152
|
+
log('🔄 Total Recall — syncing skill definitions from upstream templates');
|
|
153
|
+
log(`📂 Brain: ${BRAIN_DIR}`);
|
|
154
|
+
if (DRY) log('🔍 DRY RUN — nothing will be written');
|
|
155
|
+
|
|
156
|
+
if (!fs.existsSync(BRAIN_DIR)) fail(`no brain at ${BRAIN_DIR}. Run "npx total-recall init" first.`);
|
|
157
|
+
|
|
158
|
+
let source = resolveTemplateSource();
|
|
159
|
+
if (!source) source = cloneUpstream();
|
|
160
|
+
log(`📦 Template source: ${source.origin}`);
|
|
161
|
+
|
|
162
|
+
const skills = fs.readdirSync(source.dir, { withFileTypes: true })
|
|
163
|
+
.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
164
|
+
if (skills.length === 0) fail(`template source has no skills: ${source.dir}`);
|
|
165
|
+
|
|
166
|
+
let updated = 0, identical = 0, created = 0;
|
|
167
|
+
const touched = [];
|
|
168
|
+
for (const skill of skills) {
|
|
169
|
+
const from = path.join(source.dir, skill);
|
|
170
|
+
const to = path.join(SKILLS_DIR, skill);
|
|
171
|
+
const isNew = !fs.existsSync(to);
|
|
172
|
+
for (const f of walk(from)) {
|
|
173
|
+
const rel = path.relative(from, f);
|
|
174
|
+
const res = writePreservingInjectedBlock(f, path.join(to, rel));
|
|
175
|
+
if (res === 'updated') { updated++; touched.push(`${skill}/${rel}`); } else identical++;
|
|
80
176
|
}
|
|
177
|
+
if (isNew) created++;
|
|
81
178
|
}
|
|
82
179
|
|
|
83
|
-
|
|
180
|
+
if (source.cleanup) fs.rmSync(source.cleanup, { recursive: true, force: true });
|
|
181
|
+
|
|
182
|
+
log('');
|
|
183
|
+
log(`${DRY ? '🔍' : '✅'} ${updated} file(s) ${DRY ? 'would change' : 'updated'}, ${identical} already current, ${created} new skill(s)`);
|
|
184
|
+
for (const t of touched.slice(0, 20)) log(` ${DRY ? '~' : '↳'} ${t}`);
|
|
185
|
+
if (touched.length > 20) log(` … and ${touched.length - 20} more`);
|
|
186
|
+
if (updated === 0) log(' Nothing to do — every skill file already matches the templates.');
|
|
187
|
+
log('');
|
|
188
|
+
log(DRY ? 'Re-run without --dry-run to apply.' : 'Run "npx total-recall compile" to rebuild the instruction surfaces.');
|
package/src/cli/backup.mjs
CHANGED
|
@@ -116,6 +116,33 @@ function printHelp() {
|
|
|
116
116
|
`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function ensureBrainGitignore(SKILL_DIR) {
|
|
120
|
+
const gitignorePath = path.join(SKILL_DIR, '.gitignore');
|
|
121
|
+
const entries = [
|
|
122
|
+
'memory-derived/', 'sessions/', '*.backup', 'logs/', 'node_modules/',
|
|
123
|
+
'memory-vault/.events/', 'browser-profile/', '.snapshots/', 'daemon.pid',
|
|
124
|
+
];
|
|
125
|
+
let existing = '';
|
|
126
|
+
if (fs.existsSync(gitignorePath)) existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
127
|
+
const missing = entries.filter((e) => !existing.split('\n').some((l) => l.trim() === e));
|
|
128
|
+
if (missing.length === 0) return 0;
|
|
129
|
+
fs.appendFileSync(
|
|
130
|
+
gitignorePath,
|
|
131
|
+
'\n# Auto-added by total-recall backup — runtime/derived/sensitive, never versioned\n'
|
|
132
|
+
+ missing.join('\n') + '\n',
|
|
133
|
+
);
|
|
134
|
+
console.error(` \u2705 .gitignore: added ${missing.length} protected entr${missing.length === 1 ? 'y' : 'ies'}`);
|
|
135
|
+
// Anything already tracked must also be untracked, or the ignore is inert.
|
|
136
|
+
for (const e of missing) {
|
|
137
|
+
const tracked = git(SKILL_DIR, 'ls-files', e.replace(/\/$/, ''));
|
|
138
|
+
if (tracked.ok && tracked.stdout) {
|
|
139
|
+
git(SKILL_DIR, 'rm', '-r', '--cached', '-f', '-q', e.replace(/\/$/, ''));
|
|
140
|
+
console.error(` \u2705 untracked ${e} (kept on disk)`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return missing.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
119
146
|
async function pushGitBackup(remote, layer = 'auto') {
|
|
120
147
|
const SKILL_DIR = getSkillDir(layer);
|
|
121
148
|
if (!commandExists('git')) {
|
|
@@ -129,6 +156,19 @@ async function pushGitBackup(remote, layer = 'auto') {
|
|
|
129
156
|
process.exit(1);
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
// A brain is memory, not runtime state. Three classes of file have each
|
|
160
|
+
// broken a real backup by being versioned:
|
|
161
|
+
// memory-vault/.events/ append-only logs; default.jsonl reached 306MB and
|
|
162
|
+
// GitHub hard-rejects >100MB, silently killing every
|
|
163
|
+
// global backup for 5 weeks (2026-07-14 -> 08-19).
|
|
164
|
+
// browser-profile/ the rotation Chromium profile — live Stripe/GitHub
|
|
165
|
+
// cookies and Login Data. Was tracked and one
|
|
166
|
+
// successful push from landing on GitHub.
|
|
167
|
+
// .snapshots/ ~46MB vault tarballs stored inside the vault.
|
|
168
|
+
// Enforced on every push, not just --install, so brains created before this
|
|
169
|
+
// rule are corrected on their next backup.
|
|
170
|
+
ensureBrainGitignore(SKILL_DIR);
|
|
171
|
+
|
|
132
172
|
// Initialise the skill folder as a git repo if needed
|
|
133
173
|
const gitDir = path.join(SKILL_DIR, '.git');
|
|
134
174
|
if (!fs.existsSync(gitDir)) {
|
|
@@ -170,6 +210,29 @@ async function pushGitBackup(remote, layer = 'auto') {
|
|
|
170
210
|
const stamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
171
211
|
|
|
172
212
|
if (!status.stdout) {
|
|
213
|
+
// A clean working tree does NOT mean the remote is current. Two scheduled
|
|
214
|
+
// jobs pointing the shared "backup" remote at different URLs is enough to
|
|
215
|
+
// break this: whichever ran second re-pointed the remote, found the tree
|
|
216
|
+
// clean, and returned "nothing to push" — so that destination silently
|
|
217
|
+
// froze for months while every run reported success. Only skip when the
|
|
218
|
+
// remote actually has our HEAD.
|
|
219
|
+
const head = git(SKILL_DIR, 'rev-parse', 'HEAD');
|
|
220
|
+
const localSha = head.ok ? (head.stdout || '').trim() : '';
|
|
221
|
+
const remoteHead = git(SKILL_DIR, 'ls-remote', remote, 'main');
|
|
222
|
+
const remoteSha = remoteHead.ok ? (remoteHead.stdout || '').split(/\s+/)[0] : '';
|
|
223
|
+
// No local commit to compare, or the remote already has it: genuinely
|
|
224
|
+
// nothing to do. Only a KNOWN mismatch justifies pushing a clean tree.
|
|
225
|
+
if (!localSha || remoteSha === localSha) {
|
|
226
|
+
console.error(` ✅ Brain unchanged — nothing to push (${stamp})`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (remoteSha) {
|
|
230
|
+
console.error(` 📤 Working tree clean but ${remote} is behind — pushing existing commits...`);
|
|
231
|
+
const p = git(SKILL_DIR, 'push', remote, 'HEAD:main');
|
|
232
|
+
if (p.ok) console.error(` ✅ Brain pushed to backup (${stamp})`);
|
|
233
|
+
else console.error(` ❌ Push failed: ${p.stderr || p.stdout}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
173
236
|
console.error(` ✅ Brain unchanged — nothing to push (${stamp})`);
|
|
174
237
|
return;
|
|
175
238
|
}
|
|
@@ -169,20 +169,24 @@ export function inspectProjectPackage(projectRoot) {
|
|
|
169
169
|
} catch {
|
|
170
170
|
return { declared: null, installed: null, isSourceTree: false, packageName: null };
|
|
171
171
|
}
|
|
172
|
+
const isSourceTree =
|
|
173
|
+
pkg.name === PACKAGE_NAME &&
|
|
174
|
+
fs.existsSync(path.join(projectRoot, 'src', 'server', 'index.mjs'));
|
|
172
175
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
173
176
|
const declared = deps[PACKAGE_NAME] || null;
|
|
174
177
|
let installed = null;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
if (isSourceTree) {
|
|
179
|
+
installed = pkg.version || null;
|
|
180
|
+
} else {
|
|
181
|
+
const nm = path.join(projectRoot, 'node_modules', PACKAGE_NAME, 'package.json');
|
|
182
|
+
if (fs.existsSync(nm)) {
|
|
183
|
+
try {
|
|
184
|
+
installed = JSON.parse(fs.readFileSync(nm, 'utf8')).version || null;
|
|
185
|
+
} catch {
|
|
186
|
+
installed = null;
|
|
187
|
+
}
|
|
181
188
|
}
|
|
182
189
|
}
|
|
183
|
-
const isSourceTree =
|
|
184
|
-
pkg.name === PACKAGE_NAME &&
|
|
185
|
-
fs.existsSync(path.join(projectRoot, 'src', 'server', 'index.mjs'));
|
|
186
190
|
return { declared, installed, isSourceTree, packageName: pkg.name || null };
|
|
187
191
|
}
|
|
188
192
|
|
package/src/core/surface.mjs
CHANGED
|
@@ -321,38 +321,67 @@ export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide',
|
|
|
321
321
|
// Note: Only invariants, preferences, and anti-patterns are included in instructions,
|
|
322
322
|
// enforcing Category Partitioning (concepts, decisions, facts are search-only).
|
|
323
323
|
const isImportant = (n) => n.importance === undefined || n.importance >= 3;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
324
|
+
|
|
325
|
+
// Determine current project/repo context to prevent cross-repo pollution
|
|
326
|
+
const currentRepoName = path.basename(process.cwd()).toLowerCase();
|
|
327
|
+
|
|
328
|
+
const isRelevantToRepo = (n) => {
|
|
329
|
+
// If explicitly marked global or no repo restriction, it applies everywhere
|
|
330
|
+
if (n.scope === 'global' || n._layer === 'global') {
|
|
331
|
+
// If the node text explicitly binds to specific other repos, skip it
|
|
332
|
+
if (Array.isArray(n.repos) && n.repos.length > 0) {
|
|
333
|
+
return n.repos.some(r => r.toLowerCase() === currentRepoName);
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
// If tagged with specific repos or project name
|
|
338
|
+
if (Array.isArray(n.repos) && n.repos.length > 0) {
|
|
339
|
+
return n.repos.some(r => r.toLowerCase() === currentRepoName);
|
|
340
|
+
}
|
|
341
|
+
if (n.project && typeof n.project === 'string') {
|
|
342
|
+
return n.project.toLowerCase() === currentRepoName;
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
};
|
|
327
346
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
347
|
+
const rawInvariants = nodes.filter(n => n.category === 'invariants' && n.status === 'active' && !isExpired(n) && isImportant(n) && isRelevantToRepo(n));
|
|
348
|
+
const rawPreferences = nodes.filter(n => n.category === 'preferences' && n.status === 'active' && !isExpired(n) && isImportant(n) && isRelevantToRepo(n));
|
|
349
|
+
const rawCorrections = nodes.filter(n => n.category === 'anti-patterns' && n.status === 'active' && !isExpired(n) && isImportant(n) && isRelevantToRepo(n));
|
|
350
|
+
|
|
351
|
+
// Deduplicate nodes by normalized content key and rank by priority/importance
|
|
352
|
+
const deduplicateAndRankNodes = (list, maxCount = 15) => {
|
|
331
353
|
const seen = new Map();
|
|
332
354
|
const deduped = [];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
355
|
+
|
|
356
|
+
// Sort by importance (descending) and priority (absolute first)
|
|
357
|
+
const sorted = [...list].sort((a, b) => {
|
|
358
|
+
const aAbs = a.priority === 'absolute' ? 10 : 0;
|
|
359
|
+
const bAbs = b.priority === 'absolute' ? 10 : 0;
|
|
360
|
+
const aScore = (a.importance || 3) + aAbs;
|
|
361
|
+
const bScore = (b.importance || 3) + bAbs;
|
|
362
|
+
return bScore - aScore;
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
for (const n of sorted) {
|
|
366
|
+
// Normalize content to catch paraphrased/duplicate directives
|
|
367
|
+
const rawText = (n.body || n.content || n.title || '').trim().toLowerCase();
|
|
368
|
+
const normKey = rawText.replace(/[^a-z0-9]/g, '').substring(0, 120);
|
|
369
|
+
|
|
370
|
+
if (!seen.has(normKey)) {
|
|
371
|
+
seen.set(normKey, n);
|
|
346
372
|
deduped.push(n);
|
|
347
373
|
}
|
|
348
374
|
}
|
|
349
|
-
return deduped;
|
|
375
|
+
return deduped.slice(0, maxCount);
|
|
350
376
|
};
|
|
351
377
|
|
|
378
|
+
const invariants = deduplicateAndRankNodes(rawInvariants, 15);
|
|
379
|
+
const preferences = deduplicateAndRankNodes(rawPreferences, 10);
|
|
380
|
+
const corrections = deduplicateAndRankNodes(rawCorrections, 15);
|
|
381
|
+
|
|
352
382
|
const formatNodes = async (list) => {
|
|
353
|
-
const unique = deduplicateNodes(list);
|
|
354
383
|
const formatted = [];
|
|
355
|
-
for (const n of
|
|
384
|
+
for (const n of list) {
|
|
356
385
|
const snippet = await compactNode(n, derivedDir, force);
|
|
357
386
|
formatted.push(snippet.startsWith('-') ? snippet : `- ${snippet}`);
|
|
358
387
|
}
|
|
@@ -202,5 +202,20 @@ router.get('/api/usage/providers', requireAuth, async (req, res) => {
|
|
|
202
202
|
} catch (err) { serverError(res, err); }
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* POST /api/daemon/restart
|
|
207
|
+
* Restarts the Active Intelligence background daemon process.
|
|
208
|
+
*/
|
|
209
|
+
router.post('/api/daemon/restart', requireAuth, requireScope('config:write'), async (_req, res) => {
|
|
210
|
+
try {
|
|
211
|
+
const { stopDaemon, startDaemon } = await import('../../core/daemon-control.mjs');
|
|
212
|
+
stopDaemon();
|
|
213
|
+
const pid = startDaemon();
|
|
214
|
+
res.json({ success: true, message: `Daemon restarted successfully (PID ${pid})`, pid });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
serverError(res, err);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
205
220
|
export default router;
|
|
206
221
|
export { router as systemRouter };
|
|
@@ -45,7 +45,7 @@ router.get('/api/update/check', requireAuth, async (_req, res) => {
|
|
|
45
45
|
|
|
46
46
|
const consumersBehind = projects.filter((p) => p.update_available).length;
|
|
47
47
|
const updateAvailable =
|
|
48
|
-
Boolean(latest && current && current
|
|
48
|
+
Boolean(latest && current && needsUpdate(current, latest)) || consumersBehind > 0;
|
|
49
49
|
|
|
50
50
|
res.json({
|
|
51
51
|
package: PACKAGE_NAME,
|