total-recall-brain 3.13.0 → 3.13.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/README.md +91 -108
- package/bin/antigravity.mjs +81 -52
- package/bin/total-recall.mjs +32 -57
- package/extension/background.js +1 -1
- package/extension/options/options.js +9 -3
- package/extension/sidepanel/sidepanel.js +0 -2
- package/frontend/index.html +4 -3
- package/frontend/package.json +1 -0
- package/frontend/src/App.tsx +109 -152
- package/frontend/src/api.ts +605 -23
- package/frontend/src/components/BrainSelector.tsx +211 -199
- package/frontend/src/components/DocumentEditorModal.tsx +57 -0
- package/frontend/src/components/DocumentTable.tsx +47 -0
- package/frontend/src/components/Graph3D.tsx +120 -71
- package/frontend/src/components/UsageChart.tsx +3 -3
- package/frontend/src/components/brand/BrandMark.tsx +95 -0
- package/frontend/src/index.css +286 -86
- package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
- package/frontend/src/pages/ChatPage.tsx +103 -45
- package/frontend/src/pages/DesignDocsPage.tsx +5 -5
- package/frontend/src/pages/FilesPage.tsx +10 -6
- package/frontend/src/pages/GraphPage.tsx +5 -3
- package/frontend/src/pages/HelpPage.tsx +2 -2
- package/frontend/src/pages/InboxPage.tsx +82 -0
- package/frontend/src/pages/InstructionsPage.tsx +1 -1
- package/frontend/src/pages/IntegrationsPage.tsx +21 -10
- package/frontend/src/pages/LoginPage.tsx +95 -77
- package/frontend/src/pages/MemoryPage.tsx +31 -8
- package/frontend/src/pages/OnboardingPage.tsx +690 -0
- package/frontend/src/pages/OpenWikiPage.tsx +412 -323
- package/frontend/src/pages/SettingsPage.tsx +825 -922
- package/frontend/src/pages/SkillsPage.tsx +272 -147
- package/frontend/src/pages/UsagePage.tsx +23 -31
- package/frontend/src/pages/VaultPage.tsx +223 -0
- package/frontend/src/types.ts +13 -0
- package/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
- package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
- package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
- package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
- package/src/cli/brain.mjs +63 -5
- package/src/cli/connect.mjs +11 -19
- package/src/cli/daemon.mjs +5 -4
- package/src/cli/deploy-ui.mjs +8 -3
- package/src/cli/deploy.mjs +84 -27
- package/src/cli/ingest/index.mjs +1 -1
- package/src/cli/init.mjs +63 -34
- package/src/cli/remember.mjs +3 -3
- package/src/cli/secret.mjs +812 -0
- package/src/cli/setup.mjs +34 -7
- package/src/cli/share.mjs +2 -2
- package/src/cli/skill.mjs +396 -35
- package/src/cli/sync.mjs +4 -4
- package/src/cli/task.mjs +287 -0
- package/src/core/clarity-rewriter.mjs +5 -5
- package/src/core/config.mjs +44 -2
- package/src/core/conflict-detector.mjs +5 -5
- package/src/core/crons.mjs +54 -0
- package/src/core/daemon-loop.mjs +87 -235
- package/src/core/dream.mjs +170 -10
- package/src/core/env-import.mjs +456 -0
- package/src/core/fact-seeker.mjs +14 -14
- package/src/core/import-rules.mjs +20 -7
- package/src/core/inference-engine.mjs +3 -3
- package/src/core/migrate.mjs +14 -8
- package/src/core/okf-adapter.mjs +2 -2
- package/src/core/operation-validator.mjs +39 -331
- package/src/core/optimizer.mjs +7 -0
- package/src/core/post-mortem.mjs +2 -2
- package/src/core/project-brain.mjs +349 -0
- package/src/core/provider-catalog.mjs +296 -0
- package/src/core/quick-capture.mjs +1 -1
- package/src/core/remote-vault-sync.mjs +194 -0
- package/src/core/research-queue.mjs +7 -11
- package/src/core/runtime.mjs +33 -9
- package/src/core/scheduler.mjs +59 -5
- package/src/core/schema.mjs +14 -0
- package/src/core/search.mjs +1 -1
- package/src/core/secrets-env-export.mjs +323 -0
- package/src/core/secrets-rotate.mjs +245 -0
- package/src/core/secrets-store.mjs +645 -0
- package/src/core/semantic-index.mjs +3 -3
- package/src/core/skills-registry.mjs +1062 -0
- package/src/core/source-adapters.mjs +4 -1
- package/src/core/ssss-host-extension.mjs +94 -0
- package/src/core/ssss-kernel-bridge.mjs +530 -0
- package/src/core/surface.mjs +14 -0
- package/src/core/task-envelope.mjs +298 -0
- package/src/core/task-executors.mjs +600 -0
- package/src/core/validated-write.mjs +124 -56
- package/src/core/vault.mjs +21 -34
- package/src/core/webauthn-store.mjs +272 -0
- package/src/server/api.mjs +26 -16
- package/src/server/auth.mjs +51 -0
- package/src/server/index.mjs +3 -3
- package/src/server/keys.mjs +30 -4
- package/src/server/rest.mjs +198 -25
- package/src/server/route-manifest.json +156 -0
- package/src/server/routes/_shared.mjs +40 -1
- package/src/server/routes/docs.mjs +260 -0
- package/src/server/routes/memory.mjs +145 -32
- package/src/server/routes/research.mjs +30 -0
- package/src/server/routes/secrets.mjs +470 -0
- package/src/server/routes/share.mjs +2 -2
- package/src/server/routes/skills.mjs +452 -54
- package/src/server/routes/sync.mjs +48 -0
- package/src/server/routes/webauthn.mjs +184 -0
- package/src/server/update-route-manifest.mjs +39 -0
- package/templates/onboarding-interview.md +32 -22
- package/templates/openwiki/README.md +10 -0
- package/templates/openwiki/architecture.md +5 -0
- package/templates/openwiki/ide.md +10 -0
- package/templates/openwiki/memory.md +11 -0
- package/templates/openwiki/secrets.md +7 -0
- package/templates/openwiki/skills.md +7 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
- /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
package/src/server/auth.mjs
CHANGED
|
@@ -404,6 +404,57 @@ export async function changePasswordHandler(req, res) {
|
|
|
404
404
|
res.json({ success: true });
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Short-lived step-up token for high-risk actions (secret reveal / copy).
|
|
409
|
+
* Requires a fresh passkey assertion or password re-entry to mint.
|
|
410
|
+
*
|
|
411
|
+
* @param {{ purpose?: string, ttlSeconds?: number, actor?: string }} opts
|
|
412
|
+
*/
|
|
413
|
+
export function mintStepUpToken(opts = {}) {
|
|
414
|
+
const purpose = opts.purpose || 'secrets:reveal';
|
|
415
|
+
const ttl = Math.min(300, Math.max(15, opts.ttlSeconds || 60));
|
|
416
|
+
return jwt.sign(
|
|
417
|
+
{
|
|
418
|
+
purpose,
|
|
419
|
+
step_up: true,
|
|
420
|
+
actor: opts.actor || 'dashboard',
|
|
421
|
+
},
|
|
422
|
+
JWT_SECRET,
|
|
423
|
+
{ expiresIn: ttl },
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @param {string} token
|
|
429
|
+
* @param {string} expectedPurpose
|
|
430
|
+
* @returns {{ ok: true, payload: object } | { ok: false, error: string }}
|
|
431
|
+
*/
|
|
432
|
+
export function verifyStepUpToken(token, expectedPurpose = 'secrets:reveal') {
|
|
433
|
+
if (!token || typeof token !== 'string') {
|
|
434
|
+
return { ok: false, error: 'Step-up token required' };
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
const payload = jwt.verify(token, JWT_SECRET);
|
|
438
|
+
if (!payload?.step_up || payload.purpose !== expectedPurpose) {
|
|
439
|
+
return { ok: false, error: 'Invalid step-up purpose' };
|
|
440
|
+
}
|
|
441
|
+
return { ok: true, payload };
|
|
442
|
+
} catch {
|
|
443
|
+
return { ok: false, error: 'Step-up token invalid or expired — re-authenticate with passkey' };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Re-check dashboard password (step-up when no passkey registered).
|
|
449
|
+
*/
|
|
450
|
+
export async function verifyDashboardPassword(password) {
|
|
451
|
+
if (!password) return false;
|
|
452
|
+
const config = loadSecurityConfig();
|
|
453
|
+
const passwordHash = config.dashboard?.password_hash;
|
|
454
|
+
if (!passwordHash) return false;
|
|
455
|
+
return bcrypt.compare(String(password), passwordHash);
|
|
456
|
+
}
|
|
457
|
+
|
|
407
458
|
export function logoutHandler(req, res) {
|
|
408
459
|
res.clearCookie('session');
|
|
409
460
|
res.json({ success: true });
|
package/src/server/index.mjs
CHANGED
|
@@ -114,7 +114,7 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
|
|
|
114
114
|
// CLI agent availability
|
|
115
115
|
const { findBinaryInPath } = await import('../core/runtime.mjs');
|
|
116
116
|
const cliAgents = [];
|
|
117
|
-
for (const bin of ['antigravity', 'gemini', 'claude', 'codex']) {
|
|
117
|
+
for (const bin of ['antigravity', 'grok', 'gemini', 'claude', 'codex']) {
|
|
118
118
|
if (findBinaryInPath(bin)) cliAgents.push(bin);
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -179,7 +179,7 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
|
|
|
179
179
|
app.get('/api/health', requireAuth, requireScope('health:read'), async (req, res) => {
|
|
180
180
|
const { findBinaryInPath } = await import('../core/runtime.mjs');
|
|
181
181
|
const agents = [];
|
|
182
|
-
for (const bin of ['antigravity', 'gemini', 'claude', 'codex']) {
|
|
182
|
+
for (const bin of ['antigravity', 'grok', 'gemini', 'claude', 'codex']) {
|
|
183
183
|
if (findBinaryInPath(bin)) agents.push(bin);
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -189,7 +189,7 @@ app.get('/api/health', requireAuth, requireScope('health:read'), async (req, res
|
|
|
189
189
|
runtime: 'cli-agents',
|
|
190
190
|
uptime_seconds: Math.floor(process.uptime()),
|
|
191
191
|
capabilities: [],
|
|
192
|
-
reason: 'No CLI agents found. Install antigravity, claude, or codex.',
|
|
192
|
+
reason: 'No CLI agents found. Install antigravity, grok, claude, or codex.',
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
195
|
|
package/src/server/keys.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export const KNOWN_SCOPES = [
|
|
|
25
25
|
'models:read',
|
|
26
26
|
'instructions:read',
|
|
27
27
|
'ssss:read',
|
|
28
|
+
'ssss:write',
|
|
28
29
|
'memory:read',
|
|
29
30
|
'memory:write',
|
|
30
31
|
'memory:recompile',
|
|
@@ -52,6 +53,12 @@ function safeEqualHex(a, b) {
|
|
|
52
53
|
|
|
53
54
|
function normalizeKey(raw) {
|
|
54
55
|
const key = { ...raw };
|
|
56
|
+
// name must always be a string — guard against bad issueKey({ name }) calls
|
|
57
|
+
if (key.name && typeof key.name === 'object') {
|
|
58
|
+
key.name = String(key.name.name || key.name.label || 'Unnamed Key');
|
|
59
|
+
} else if (typeof key.name !== 'string' || !key.name.trim()) {
|
|
60
|
+
key.name = key.name == null ? 'Unnamed Key' : String(key.name);
|
|
61
|
+
}
|
|
55
62
|
if (key.token && typeof key.token === 'string') {
|
|
56
63
|
if (key.token === 'local') {
|
|
57
64
|
key.revoked = true;
|
|
@@ -129,16 +136,35 @@ function saveKeys(keys) {
|
|
|
129
136
|
fs.writeFileSync(KEYS_FILE, sanitized.map(k => JSON.stringify(k)).join('\n') + (sanitized.length ? '\n' : ''), { encoding: 'utf8', mode: 0o600 });
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Issue a PAT.
|
|
141
|
+
* @param {string|object} nameOrOpts - display name, or { name, scopes, expires_at }
|
|
142
|
+
* @param {object} [options]
|
|
143
|
+
*/
|
|
144
|
+
export function issueKey(nameOrOpts, options = {}) {
|
|
145
|
+
let name = nameOrOpts;
|
|
146
|
+
let opts = options;
|
|
147
|
+
// Accept issueKey({ name, scopes }) form safely
|
|
148
|
+
if (nameOrOpts && typeof nameOrOpts === 'object' && !Array.isArray(nameOrOpts)) {
|
|
149
|
+
name = nameOrOpts.name || nameOrOpts.label || 'Unnamed Key';
|
|
150
|
+
opts = {
|
|
151
|
+
scopes: nameOrOpts.scopes ?? options.scopes,
|
|
152
|
+
expires_at: nameOrOpts.expires_at ?? nameOrOpts.expiresAt ?? options.expires_at ?? options.expiresAt,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
156
|
+
name = 'Unnamed Key';
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
const keys = loadKeys();
|
|
134
160
|
const token = `tr_${crypto.randomBytes(32).toString('base64url')}`;
|
|
135
161
|
const key = {
|
|
136
162
|
id: crypto.randomUUID(),
|
|
137
|
-
name: name
|
|
163
|
+
name: name.trim(),
|
|
138
164
|
token_hash: hashToken(token),
|
|
139
165
|
token_prefix: token.slice(0, 8),
|
|
140
|
-
scopes: normalizeScopes(
|
|
141
|
-
expires_at:
|
|
166
|
+
scopes: normalizeScopes(opts.scopes),
|
|
167
|
+
expires_at: opts.expires_at || opts.expiresAt || null,
|
|
142
168
|
created_at: new Date().toISOString(),
|
|
143
169
|
last_used_at: null,
|
|
144
170
|
hit_count: 0,
|
package/src/server/rest.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* GET /api/config get sanitized runtime + security config
|
|
34
34
|
*
|
|
35
35
|
* Models (OpenAI-compatible extension)
|
|
36
|
-
* GET /v1/models list available
|
|
36
|
+
* GET /v1/models list available models (OpenAI-compatible)
|
|
37
37
|
*
|
|
38
38
|
* Vault (admin operations)
|
|
39
39
|
* POST /api/vault/compile trigger full surface compile (re-generates compile.json)
|
|
@@ -81,12 +81,16 @@ import { synthesize as synthesizeTts, isTtsEnabled, TtsNotConfiguredError } from
|
|
|
81
81
|
import { logger } from '../core/logger.mjs';
|
|
82
82
|
import { memoryRouter } from './routes/memory.mjs';
|
|
83
83
|
import { keysRouter } from './routes/keys.mjs';
|
|
84
|
+
import { secretsRouter } from './routes/secrets.mjs';
|
|
84
85
|
import { sessionsRouter } from './routes/sessions.mjs';
|
|
85
86
|
import { shareRouter } from './routes/share.mjs';
|
|
86
87
|
import { authRouter } from './routes/auth.mjs';
|
|
88
|
+
import { webauthnRouter } from './routes/webauthn.mjs';
|
|
87
89
|
import { sandboxRouter } from './routes/sandbox.mjs';
|
|
88
90
|
import { researchRouter } from './routes/research.mjs';
|
|
89
91
|
import { skillsRouter } from './routes/skills.mjs';
|
|
92
|
+
import { docsRouter } from './routes/docs.mjs';
|
|
93
|
+
import syncRouter from './routes/sync.mjs';
|
|
90
94
|
// ollamaUrl removed — CLI agents replace Ollama
|
|
91
95
|
import {
|
|
92
96
|
AGENT_DIR,
|
|
@@ -101,7 +105,7 @@ import {
|
|
|
101
105
|
CONFIG_DIR
|
|
102
106
|
} from './routes/_shared.mjs';
|
|
103
107
|
|
|
104
|
-
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
108
|
+
const ROOT = process.env.TR_ROOT || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
105
109
|
const MODEL_CATALOG_DIR = path.join(ROOT, 'models', 'catalog', 'total-recall');
|
|
106
110
|
|
|
107
111
|
function listFilesRecursive(root, predicate) {
|
|
@@ -161,14 +165,18 @@ const router = express.Router();
|
|
|
161
165
|
// handlers below so URL precedence stays identical to the pre-refactor file.
|
|
162
166
|
router.use(memoryRouter);
|
|
163
167
|
router.use(keysRouter);
|
|
168
|
+
router.use(secretsRouter);
|
|
164
169
|
router.use(sessionsRouter);
|
|
165
170
|
router.use(shareRouter);
|
|
166
171
|
router.use(authRouter);
|
|
172
|
+
router.use(webauthnRouter);
|
|
167
173
|
router.use(sandboxRouter);
|
|
168
174
|
router.use(researchRouter);
|
|
169
175
|
router.use(skillsRouter);
|
|
176
|
+
router.use(docsRouter);
|
|
177
|
+
router.use(syncRouter);
|
|
170
178
|
|
|
171
|
-
// ───
|
|
179
|
+
// ─── Brain Routing Middleware ──────────────────────────────────────────────────────────────────
|
|
172
180
|
|
|
173
181
|
function notFound(res, msg) {
|
|
174
182
|
return res.status(404).json({ error: msg || 'Not found' });
|
|
@@ -198,7 +206,6 @@ function sanitizeNode({ body, ...rest }) {
|
|
|
198
206
|
* POST /api/memory/search/semantic
|
|
199
207
|
* Body: { query: string, top_k?: number }
|
|
200
208
|
* Returns top-k vault nodes ranked by vector similarity to the query.
|
|
201
|
-
* Requires Ollama with nomic-embed-text running.
|
|
202
209
|
*/
|
|
203
210
|
router.post('/api/memory/search/semantic', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
204
211
|
try {
|
|
@@ -208,7 +215,6 @@ router.post('/api/memory/search/semantic', requireAuth, requireScope('memory:rea
|
|
|
208
215
|
if (results.length === 0) return res.status(503).json({ error: 'Embeddings index is empty. Run POST /api/vault/compile to build it.' });
|
|
209
216
|
res.json({ query, top_k: Math.min(Number(top_k) || 5, 20), results });
|
|
210
217
|
} catch (err) {
|
|
211
|
-
if (err.message?.includes('Ollama')) return res.status(503).json({ error: err.message });
|
|
212
218
|
serverError(res, err);
|
|
213
219
|
}
|
|
214
220
|
});
|
|
@@ -233,7 +239,7 @@ router.post('/api/vault/compile', requireAuth, requireScope('memory:recompile'),
|
|
|
233
239
|
const vaultNodes = getNodes(VAULT_DIR);
|
|
234
240
|
vaultEmbed = await buildEmbeddingsIndex(vaultNodes, DERIVED_DIR);
|
|
235
241
|
sessionEmbed = await buildSessionEmbeddingsIndex(SESSIONS_DIR, DERIVED_DIR);
|
|
236
|
-
} catch { /*
|
|
242
|
+
} catch { /* embedding service may be unavailable — non-fatal */ }
|
|
237
243
|
res.json({ compiled: true, elapsed_ms: Date.now() - start, vault_embeddings: vaultEmbed, session_embeddings: sessionEmbed });
|
|
238
244
|
} catch (err) {
|
|
239
245
|
serverError(res, err);
|
|
@@ -633,7 +639,7 @@ router.get('/api', (req, res) => {
|
|
|
633
639
|
'GET /api/memory/stats': 'Node counts by category',
|
|
634
640
|
'GET /api/memory/:slug': 'Get node by slug',
|
|
635
641
|
'POST /api/memory': 'Create node (slug, title, category, content)',
|
|
636
|
-
'POST /api/memory/search/semantic': 'Semantic search by meaning (query, top_k)
|
|
642
|
+
'POST /api/memory/search/semantic': 'Semantic search by meaning (query, top_k)',
|
|
637
643
|
'PUT /api/memory/:slug': 'Replace node',
|
|
638
644
|
'PATCH /api/memory/:slug': 'Partial update',
|
|
639
645
|
'DELETE /api/memory/:slug': 'Delete node',
|
|
@@ -666,7 +672,7 @@ router.get('/api', (req, res) => {
|
|
|
666
672
|
},
|
|
667
673
|
discovery: {
|
|
668
674
|
'GET /.well-known/total-recall.json': 'Client auto-config manifest',
|
|
669
|
-
'GET /health': 'System health (disk,
|
|
675
|
+
'GET /health': 'System health (disk, embedding service, vault)',
|
|
670
676
|
},
|
|
671
677
|
},
|
|
672
678
|
scopes: {
|
|
@@ -751,7 +757,7 @@ router.post('/api/integrations/connect', requireAuth, (req, res) => {
|
|
|
751
757
|
|
|
752
758
|
const validClients = [
|
|
753
759
|
'vscode', 'pi', 'hermes', 'openclaw', 'cursor', 'claude-code',
|
|
754
|
-
'codex', 'gemini', 'aider', '
|
|
760
|
+
'codex', 'gemini', 'aider', 'http-api', 'obsidian',
|
|
755
761
|
'generic', 'antigravity'
|
|
756
762
|
];
|
|
757
763
|
|
|
@@ -815,7 +821,7 @@ router.get('/api/import/rules', requireAuth, requireScope('memory:read'), (req,
|
|
|
815
821
|
* Import rule files into the vault.
|
|
816
822
|
* Body: { dirs?: string[], files?: string[], force?: boolean, dryRun?: boolean }
|
|
817
823
|
*/
|
|
818
|
-
router.post('/api/import/rules', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
824
|
+
router.post('/api/import/rules', requireAuth, requireScope('memory:write'), async (req, res) => {
|
|
819
825
|
try {
|
|
820
826
|
const { dirs, force = false, dryRun = false } = req.body || {};
|
|
821
827
|
const searchDirs = dirs?.length ? dirs : [process.cwd(), os.homedir()];
|
|
@@ -824,7 +830,7 @@ router.post('/api/import/rules', requireAuth, requireScope('memory:write'), (req
|
|
|
824
830
|
? detected.filter(f => req.body.files.includes(f.absolutePath))
|
|
825
831
|
: detected.filter(f => !f.alreadyImported || force);
|
|
826
832
|
if (dryRun) return res.json({ dryRun: true, detected, toImport, imported: [], skipped: [], failed: [] });
|
|
827
|
-
const result = importRuleFiles(toImport, { force, vaultDir: VAULT_DIR });
|
|
833
|
+
const result = await importRuleFiles(toImport, { force, vaultDir: VAULT_DIR });
|
|
828
834
|
res.json({ detected, ...result });
|
|
829
835
|
|
|
830
836
|
} catch (err) { serverError(res, err); }
|
|
@@ -1519,8 +1525,50 @@ router.post('/api/tts', requireAuth, requireScope('tts:use'), async (req, res) =
|
|
|
1519
1525
|
|
|
1520
1526
|
// ─── Instructions (sync consumers) ─────────────────────────────────────────────
|
|
1521
1527
|
|
|
1528
|
+
router.get('/api/dashboard/instructions', requireAuth, requireScope('instructions:read'), (req, res) => {
|
|
1529
|
+
const surfaces = [];
|
|
1530
|
+
const surfaceFiles = ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md', 'INSTRUCTIONS.md'];
|
|
1531
|
+
|
|
1532
|
+
for (const name of surfaceFiles) {
|
|
1533
|
+
let filePath;
|
|
1534
|
+
if (name === 'INSTRUCTIONS.md') {
|
|
1535
|
+
filePath = path.join(ROOT, name);
|
|
1536
|
+
} else {
|
|
1537
|
+
filePath = path.join(ROOT, name);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
let size = 0;
|
|
1541
|
+
let lastCompiled = '';
|
|
1542
|
+
let active = false;
|
|
1543
|
+
|
|
1544
|
+
if (fs.existsSync(filePath)) {
|
|
1545
|
+
const stat = fs.statSync(filePath);
|
|
1546
|
+
size = stat.size;
|
|
1547
|
+
lastCompiled = stat.mtime.toISOString();
|
|
1548
|
+
active = true;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
surfaces.push({
|
|
1552
|
+
name,
|
|
1553
|
+
filename: name,
|
|
1554
|
+
size,
|
|
1555
|
+
lastCompiled,
|
|
1556
|
+
active
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const lastCompileTimestamp = fs.existsSync(INSTRUCTIONS) ? fs.statSync(INSTRUCTIONS).mtime.toISOString() : '';
|
|
1561
|
+
const totalNodes = fs.existsSync(VAULT_DIR) ? getNodes(VAULT_DIR).length : 0;
|
|
1562
|
+
|
|
1563
|
+
res.json({
|
|
1564
|
+
surfaces,
|
|
1565
|
+
lastCompileTimestamp,
|
|
1566
|
+
totalNodes
|
|
1567
|
+
});
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1522
1570
|
router.get('/api/instructions', requireAuth, requireScope('instructions:read'), (req, res) => {
|
|
1523
|
-
return sendTextResource(res, INSTRUCTIONS, 'instructions');
|
|
1571
|
+
return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
|
|
1524
1572
|
});
|
|
1525
1573
|
|
|
1526
1574
|
router.put('/api/instructions', requireAuth, requireScope('instructions:write'), (req, res) => {
|
|
@@ -1574,7 +1622,23 @@ function absoluteUrl(req, routePath) {
|
|
|
1574
1622
|
}
|
|
1575
1623
|
|
|
1576
1624
|
function ssssReferenceDir() {
|
|
1577
|
-
|
|
1625
|
+
// Prefer compact TR references; fall back to legacy nested module paths.
|
|
1626
|
+
const candidates = [
|
|
1627
|
+
path.join(SKILLS_DIR, 'total-recall', 'references'),
|
|
1628
|
+
path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'references'),
|
|
1629
|
+
path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references'),
|
|
1630
|
+
];
|
|
1631
|
+
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function ssssSkillDocPath() {
|
|
1635
|
+
const candidates = [
|
|
1636
|
+
path.join(SKILLS_DIR, 'total-recall', 'references', 'ssss-reference.md'),
|
|
1637
|
+
path.join(SKILLS_DIR, 'total-recall', 'SKILL.md'),
|
|
1638
|
+
path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'MODULE.md'),
|
|
1639
|
+
path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'SKILL.md'),
|
|
1640
|
+
];
|
|
1641
|
+
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
1578
1642
|
}
|
|
1579
1643
|
|
|
1580
1644
|
function listSsssReferences(req) {
|
|
@@ -1615,7 +1679,7 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
|
|
|
1615
1679
|
name: 'ssss-skill',
|
|
1616
1680
|
url: absoluteUrl(req, '/api/ssss/skill/ssss'),
|
|
1617
1681
|
...(() => {
|
|
1618
|
-
const r = readTextResource(
|
|
1682
|
+
const r = readTextResource(ssssSkillDocPath(), 'ssss-skill');
|
|
1619
1683
|
return r ? { sha256: r.sha256, bytes: r.bytes, modified: r.modified } : { sha256: null, bytes: 0, modified: null };
|
|
1620
1684
|
})()
|
|
1621
1685
|
},
|
|
@@ -1623,7 +1687,11 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
|
|
|
1623
1687
|
name: 'ssss-spec',
|
|
1624
1688
|
url: absoluteUrl(req, '/api/ssss/spec'),
|
|
1625
1689
|
...(() => {
|
|
1626
|
-
const
|
|
1690
|
+
const refs = ssssReferenceDir();
|
|
1691
|
+
const specPath = ['ssss-reference.md', 'ssss-spec.md']
|
|
1692
|
+
.map((n) => path.join(refs, n))
|
|
1693
|
+
.find((p) => fs.existsSync(p));
|
|
1694
|
+
const r = specPath ? readTextResource(specPath, 'ssss-spec') : null;
|
|
1627
1695
|
return r ? { sha256: r.sha256, bytes: r.bytes, modified: r.modified } : { sha256: null, bytes: 0, modified: null };
|
|
1628
1696
|
})()
|
|
1629
1697
|
},
|
|
@@ -1637,16 +1705,29 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
|
|
|
1637
1705
|
});
|
|
1638
1706
|
});
|
|
1639
1707
|
|
|
1640
|
-
router.get('/api/ssss/instructions', requireAuth, requireScope('ssss:read', 'instructions:read'), (
|
|
1641
|
-
|
|
1708
|
+
router.get('/api/ssss/instructions', requireAuth, requireScope('ssss:read', 'instructions:read'), (req, res) => {
|
|
1709
|
+
const surface = req.query.surface;
|
|
1710
|
+
if (surface) {
|
|
1711
|
+
if (surface === 'INSTRUCTIONS.md') {
|
|
1712
|
+
return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
|
|
1713
|
+
}
|
|
1714
|
+
const safeSurface = path.basename(surface);
|
|
1715
|
+
const surfacePath = path.join(ROOT, safeSurface);
|
|
1716
|
+
return sendTextResource(res, surfacePath, safeSurface);
|
|
1717
|
+
}
|
|
1718
|
+
return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
|
|
1642
1719
|
});
|
|
1643
1720
|
|
|
1644
1721
|
router.get('/api/ssss/skill/ssss', requireAuth, requireScope('ssss:read'), (_req, res) => {
|
|
1645
|
-
return sendTextResource(res,
|
|
1722
|
+
return sendTextResource(res, ssssSkillDocPath(), 'ssss-skill');
|
|
1646
1723
|
});
|
|
1647
1724
|
|
|
1648
1725
|
router.get('/api/ssss/spec', requireAuth, requireScope('ssss:read'), (_req, res) => {
|
|
1649
|
-
|
|
1726
|
+
const refs = ssssReferenceDir();
|
|
1727
|
+
const specPath = ['ssss-reference.md', 'ssss-spec.md']
|
|
1728
|
+
.map((n) => path.join(refs, n))
|
|
1729
|
+
.find((p) => fs.existsSync(p));
|
|
1730
|
+
return sendTextResource(res, specPath || path.join(refs, 'ssss-reference.md'), 'ssss-spec');
|
|
1650
1731
|
});
|
|
1651
1732
|
|
|
1652
1733
|
router.get('/api/ssss/references', requireAuth, requireScope('ssss:read'), (req, res) => {
|
|
@@ -1666,7 +1747,7 @@ router.get('/api/ssss/references/:name', requireAuth, requireScope('ssss:read'),
|
|
|
1666
1747
|
* List all known brains (global + registered projects) with state metadata.
|
|
1667
1748
|
* The global brain reads project brain frontmatter for state display.
|
|
1668
1749
|
*/
|
|
1669
|
-
router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) => {
|
|
1750
|
+
router.get('/api/brains', requireAuth, requireScope('ssss:read'), async (req, res) => {
|
|
1670
1751
|
try {
|
|
1671
1752
|
const globalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
|
|
1672
1753
|
const brains = [];
|
|
@@ -1737,7 +1818,47 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) =>
|
|
|
1737
1818
|
last_compiled: project.last_compiled || getLastModified(path.join(project.brainDir, 'memory-derived', 'graph-index.jsonl')),
|
|
1738
1819
|
});
|
|
1739
1820
|
}
|
|
1740
|
-
} catch {
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
logger.error('server', 'Failed to read project registry', { error: err.message });
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Optional remote/tenant vault from config (path-agnostic — user sets vaultDir)
|
|
1827
|
+
try {
|
|
1828
|
+
const config = await import('../core/config.mjs');
|
|
1829
|
+
if (config.remoteVaultSync?.enabled && config.remoteVaultSync.vaultDir) {
|
|
1830
|
+
const tenantVaultDir = config.remoteVaultSync.vaultDir;
|
|
1831
|
+
const tenantId =
|
|
1832
|
+
process.env.TR_TENANT_ID ||
|
|
1833
|
+
path.basename(path.dirname(tenantVaultDir)) ||
|
|
1834
|
+
'remote';
|
|
1835
|
+
const exists = fs.existsSync(tenantVaultDir);
|
|
1836
|
+
let nodeCount = 0;
|
|
1837
|
+
if (exists) {
|
|
1838
|
+
const countMd = (dir) => {
|
|
1839
|
+
if (!fs.existsSync(dir)) return 0;
|
|
1840
|
+
let count = 0;
|
|
1841
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1842
|
+
if (entry.isDirectory()) count += countMd(path.join(dir, entry.name));
|
|
1843
|
+
else if (entry.name.endsWith('.md')) count++;
|
|
1844
|
+
}
|
|
1845
|
+
return count;
|
|
1846
|
+
};
|
|
1847
|
+
nodeCount = countMd(tenantVaultDir);
|
|
1848
|
+
}
|
|
1849
|
+
brains.push({
|
|
1850
|
+
id: `tenant:${tenantId}`,
|
|
1851
|
+
name: process.env.TR_TENANT_NAME || `Tenant (${tenantId})`,
|
|
1852
|
+
layer: 'tenant',
|
|
1853
|
+
path: tenantVaultDir,
|
|
1854
|
+
project_root: path.dirname(tenantVaultDir),
|
|
1855
|
+
exists,
|
|
1856
|
+
node_count: nodeCount,
|
|
1857
|
+
last_compiled: null,
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
} catch {
|
|
1861
|
+
// ignore
|
|
1741
1862
|
}
|
|
1742
1863
|
|
|
1743
1864
|
res.json({ brains });
|
|
@@ -1749,13 +1870,15 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) =>
|
|
|
1749
1870
|
* GET /api/brains/:id/nodes
|
|
1750
1871
|
* List all memory nodes for a specific brain.
|
|
1751
1872
|
*/
|
|
1752
|
-
router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memory:read'), (req, res) => {
|
|
1873
|
+
router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memory:read'), async (req, res) => {
|
|
1753
1874
|
try {
|
|
1754
1875
|
const brainId = req.params.id;
|
|
1755
1876
|
let brainDir;
|
|
1877
|
+
let vaultDir;
|
|
1756
1878
|
|
|
1757
1879
|
if (brainId === 'global') {
|
|
1758
1880
|
brainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
|
|
1881
|
+
vaultDir = path.join(brainDir, 'memory-vault');
|
|
1759
1882
|
} else if (brainId.startsWith('project:')) {
|
|
1760
1883
|
const projectName = brainId.slice('project:'.length);
|
|
1761
1884
|
const globalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
|
|
@@ -1766,14 +1889,35 @@ router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memo
|
|
|
1766
1889
|
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
1767
1890
|
const project = registry.find(p => p.name === projectName);
|
|
1768
1891
|
if (!project) {
|
|
1769
|
-
return res.status(404).json({ error: `Project
|
|
1892
|
+
return res.status(404).json({ error: `Project "${projectName}" not found in registry` });
|
|
1770
1893
|
}
|
|
1771
1894
|
brainDir = project.brainDir;
|
|
1895
|
+
vaultDir = path.join(brainDir, 'memory-vault');
|
|
1896
|
+
} else if (brainId.startsWith('tenant:')) {
|
|
1897
|
+
const tenantName = brainId.slice('tenant:'.length).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1898
|
+
const config = await import('../core/config.mjs');
|
|
1899
|
+
// Prefer configured remote vault when id matches configured tenant; else ~/.agent/tenants/<name>/vault
|
|
1900
|
+
const configuredId =
|
|
1901
|
+
process.env.TR_TENANT_ID ||
|
|
1902
|
+
(config.remoteVaultSync?.vaultDir
|
|
1903
|
+
? path.basename(path.dirname(config.remoteVaultSync.vaultDir))
|
|
1904
|
+
: null);
|
|
1905
|
+
if (
|
|
1906
|
+
config.remoteVaultSync?.enabled &&
|
|
1907
|
+
config.remoteVaultSync.vaultDir &&
|
|
1908
|
+
(tenantName === configuredId || tenantName === 'remote' || tenantName === 'default')
|
|
1909
|
+
) {
|
|
1910
|
+
vaultDir = config.remoteVaultSync.vaultDir;
|
|
1911
|
+
} else {
|
|
1912
|
+
vaultDir = path.join(os.homedir(), '.agent', 'tenants', tenantName, 'vault');
|
|
1913
|
+
if (!fs.existsSync(vaultDir)) {
|
|
1914
|
+
return res.status(404).json({ error: `Tenant "${tenantName}" not found` });
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1772
1917
|
} else {
|
|
1773
|
-
return res.status(400).json({ error: 'Invalid brain ID. Use
|
|
1918
|
+
return res.status(400).json({ error: 'Invalid brain ID. Use "global", "project:<name>", or "tenant:<name>"' });
|
|
1774
1919
|
}
|
|
1775
1920
|
|
|
1776
|
-
const vaultDir = path.join(brainDir, 'memory-vault');
|
|
1777
1921
|
if (!fs.existsSync(vaultDir)) {
|
|
1778
1922
|
return res.json({ nodes: [], brain_id: brainId });
|
|
1779
1923
|
}
|
|
@@ -2111,6 +2255,35 @@ router.post('/api/update/run', requireAuth, async (req, res) => {
|
|
|
2111
2255
|
}
|
|
2112
2256
|
});
|
|
2113
2257
|
|
|
2258
|
+
// ─── DLQ Admin Routes ────────────────────────────────────────────────────────────
|
|
2259
|
+
const QUEUE_DIR = path.join(BRAIN_DIR, 'scheduler', 'queue');
|
|
2260
|
+
|
|
2261
|
+
router.get('/api/tasks/failed', requireAuth, async (req, res) => {
|
|
2262
|
+
try {
|
|
2263
|
+
const { loadPendingTasks } = await import('../core/scheduler.mjs');
|
|
2264
|
+
const allTasks = loadPendingTasks(QUEUE_DIR);
|
|
2265
|
+
const failed = allTasks.filter(t => t.status === 'failed');
|
|
2266
|
+
res.json({ total: failed.length, tasks: failed });
|
|
2267
|
+
} catch (err) {
|
|
2268
|
+
serverError(res, err);
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
router.post('/api/tasks/:id/retry', requireAuth, async (req, res) => {
|
|
2273
|
+
try {
|
|
2274
|
+
const taskId = req.params.id;
|
|
2275
|
+
const { loadPendingTasks, updateTaskStatus } = await import('../core/scheduler.mjs');
|
|
2276
|
+
const allTasks = loadPendingTasks(QUEUE_DIR);
|
|
2277
|
+
const task = allTasks.find(t => t.slug === taskId);
|
|
2278
|
+
if (!task) return res.status(404).json({ error: 'Task not found in queue' });
|
|
2279
|
+
if (task.status !== 'failed') return res.status(400).json({ error: 'Task is not in failed state' });
|
|
2280
|
+
updateTaskStatus(task, 'pending', QUEUE_DIR);
|
|
2281
|
+
res.json({ success: true, message: `Task ${taskId} re-queued` });
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
serverError(res, err);
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2114
2287
|
router.get('/api/help', requireAuth, (req, res) => {
|
|
2115
2288
|
const { topic } = req.query;
|
|
2116
2289
|
const docsDir = path.join(ROOT, 'docs');
|