total-recall-brain 3.6.4 → 3.6.6
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/extension/background.js +4 -0
- package/extension/content-script.js +9 -1
- package/extension/manifest.json +0 -1
- package/frontend/src/App.tsx +5 -8
- package/frontend/src/components/BrainSelector.tsx +10 -4
- package/frontend/src/index.css +1 -0
- package/frontend/src/pages/ChatPage.tsx +2 -0
- package/frontend/src/pages/{DeploymentsPage.tsx → ModelsPage.tsx} +183 -87
- package/frontend/src/types.ts +3 -0
- package/frontend/vite.config.ts +3 -3
- package/package.json +1 -1
- package/src/core/daemon-loop.mjs +162 -58
- package/src/core/fact-seeker.mjs +6 -3
- package/src/core/research-queue.mjs +8 -1
- package/src/core/scheduler.mjs +23 -5
- package/src/core/schema.mjs +23 -0
- package/src/server/index.mjs +19 -0
- package/src/server/rest.mjs +43 -189
- package/src/server/route-manifest.json +4 -0
- package/src/server/routes/auth.mjs +32 -0
- package/src/server/routes/research.mjs +55 -0
- package/src/server/routes/sandbox.mjs +55 -0
- package/src/server/routes/skills.mjs +118 -0
package/src/server/rest.mjs
CHANGED
|
@@ -83,6 +83,10 @@ import { memoryRouter } from './routes/memory.mjs';
|
|
|
83
83
|
import { keysRouter } from './routes/keys.mjs';
|
|
84
84
|
import { sessionsRouter } from './routes/sessions.mjs';
|
|
85
85
|
import { shareRouter } from './routes/share.mjs';
|
|
86
|
+
import { authRouter } from './routes/auth.mjs';
|
|
87
|
+
import { sandboxRouter } from './routes/sandbox.mjs';
|
|
88
|
+
import { researchRouter } from './routes/research.mjs';
|
|
89
|
+
import { skillsRouter } from './routes/skills.mjs';
|
|
86
90
|
// ollamaUrl removed — CLI agents replace Ollama
|
|
87
91
|
import {
|
|
88
92
|
AGENT_DIR,
|
|
@@ -159,6 +163,10 @@ router.use(memoryRouter);
|
|
|
159
163
|
router.use(keysRouter);
|
|
160
164
|
router.use(sessionsRouter);
|
|
161
165
|
router.use(shareRouter);
|
|
166
|
+
router.use(authRouter);
|
|
167
|
+
router.use(sandboxRouter);
|
|
168
|
+
router.use(researchRouter);
|
|
169
|
+
router.use(skillsRouter);
|
|
162
170
|
|
|
163
171
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
164
172
|
|
|
@@ -490,36 +498,7 @@ router.post('/api/diagnostics/agents', requireAuth, requireScope('health:read'),
|
|
|
490
498
|
// ─── Keys ─────────── (moved to ./routes/keys.mjs)
|
|
491
499
|
// ─── Sessions ─────── (moved to ./routes/sessions.mjs)
|
|
492
500
|
// ─── Sandbox ──────────────────────────────────────────────────────────────────
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* POST /api/sandbox
|
|
496
|
-
* Body: { code }
|
|
497
|
-
*/
|
|
498
|
-
// Sandbox is rate-limited *before* requireAuth on purpose — limiter keys on
|
|
499
|
-
// the authenticated principal when present, so it's IP-bucketed for
|
|
500
|
-
// pre-auth misuse and key-bucketed once a PAT is attached.
|
|
501
|
-
router.post('/api/sandbox', sandboxRateLimiter(), requireAuth, requireScope('sandbox:run'), requireSandboxEnabled, async (req, res) => {
|
|
502
|
-
try {
|
|
503
|
-
const { code } = req.body || {};
|
|
504
|
-
if (!code) return badRequest(res, 'code is required');
|
|
505
|
-
|
|
506
|
-
const tmpDir = path.join(os.tmpdir(), 'total-recall-sandbox');
|
|
507
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
508
|
-
const script = path.join(tmpDir, `rest-${Date.now()}.mjs`);
|
|
509
|
-
fs.writeFileSync(script, code);
|
|
510
|
-
|
|
511
|
-
const result = await runInSandbox(script, 15000);
|
|
512
|
-
try { fs.unlinkSync(script); } catch {}
|
|
513
|
-
|
|
514
|
-
res.json({
|
|
515
|
-
success: result.success,
|
|
516
|
-
exit_code: result.code,
|
|
517
|
-
output: result.output,
|
|
518
|
-
});
|
|
519
|
-
} catch (err) {
|
|
520
|
-
serverError(res, err);
|
|
521
|
-
}
|
|
522
|
-
});
|
|
501
|
+
// Moved to routes/sandbox.mjs
|
|
523
502
|
|
|
524
503
|
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
525
504
|
|
|
@@ -707,41 +686,7 @@ router.get('/api', (req, res) => {
|
|
|
707
686
|
});
|
|
708
687
|
|
|
709
688
|
// ─── Research Queue ───────────────────────────────────────────────────────────
|
|
710
|
-
//
|
|
711
|
-
|
|
712
|
-
router.get('/api/research', requireAuth, requireScope('memory:read'), (req, res) => {
|
|
713
|
-
try {
|
|
714
|
-
const { status, query, limit, offset } = req.query;
|
|
715
|
-
res.json(listQueue({ status, query, limit, offset }));
|
|
716
|
-
} catch (err) { serverError(res, err); }
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
router.post('/api/research', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
720
|
-
try {
|
|
721
|
-
const { topic, priority, notes } = req.body || {};
|
|
722
|
-
if (!topic) return badRequest(res, 'topic is required');
|
|
723
|
-
res.status(201).json(addToQueue({ topic, priority, notes }));
|
|
724
|
-
} catch (err) { serverError(res, err); }
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
router.patch('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
728
|
-
try {
|
|
729
|
-
res.json(updateQueueItem(req.params.id, req.body || {}));
|
|
730
|
-
} catch (err) {
|
|
731
|
-
if (err.status === 404) return notFound(res, err.message);
|
|
732
|
-
serverError(res, err);
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
router.delete('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
737
|
-
try {
|
|
738
|
-
res.json(removeFromQueue(req.params.id));
|
|
739
|
-
} catch (err) {
|
|
740
|
-
if (err.status === 404) return notFound(res, err.message);
|
|
741
|
-
serverError(res, err);
|
|
742
|
-
}
|
|
743
|
-
});
|
|
744
|
-
|
|
689
|
+
// Moved to routes/research.mjs
|
|
745
690
|
// ─── Active Integrations ──────────────────────────────────────────────────────
|
|
746
691
|
|
|
747
692
|
/**
|
|
@@ -1098,110 +1043,7 @@ router.get('/api/files', requireAuth, requireScope('files:read'), (req, res) =>
|
|
|
1098
1043
|
} catch (err) { serverError(res, err); }
|
|
1099
1044
|
});
|
|
1100
1045
|
|
|
1101
|
-
|
|
1102
|
-
try {
|
|
1103
|
-
if (!fs.existsSync(SKILLS_DIR)) {
|
|
1104
|
-
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
1105
|
-
}
|
|
1106
|
-
const skills = fs.readdirSync(SKILLS_DIR).map(dir => {
|
|
1107
|
-
const dirPath = path.join(SKILLS_DIR, dir);
|
|
1108
|
-
const stats = fs.statSync(dirPath);
|
|
1109
|
-
|
|
1110
|
-
let subSkills = [];
|
|
1111
|
-
const subSkillsPath = path.join(dirPath, 'skills');
|
|
1112
|
-
if (fs.existsSync(subSkillsPath) && fs.statSync(subSkillsPath).isDirectory()) {
|
|
1113
|
-
try {
|
|
1114
|
-
subSkills = fs.readdirSync(subSkillsPath).filter(sd =>
|
|
1115
|
-
fs.statSync(path.join(subSkillsPath, sd)).isDirectory()
|
|
1116
|
-
);
|
|
1117
|
-
} catch (e) {}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
return {
|
|
1121
|
-
name: dir,
|
|
1122
|
-
size: stats.size,
|
|
1123
|
-
modified: stats.mtime,
|
|
1124
|
-
isDirectory: stats.isDirectory(),
|
|
1125
|
-
subSkills
|
|
1126
|
-
};
|
|
1127
|
-
});
|
|
1128
|
-
res.json(skills);
|
|
1129
|
-
} catch (err) { serverError(res, err); }
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
router.get('/api/skills/search', requireAuth, requireScope('files:read', 'ssss:read'), async (req, res) => {
|
|
1133
|
-
try {
|
|
1134
|
-
const q = req.query.q || '';
|
|
1135
|
-
if (!q) {
|
|
1136
|
-
return res.status(400).json({ error: 'Missing search query parameter `q`.' });
|
|
1137
|
-
}
|
|
1138
|
-
const { searchAndSort } = await import('../../.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs');
|
|
1139
|
-
const results = searchAndSort(q);
|
|
1140
|
-
res.json(results);
|
|
1141
|
-
} catch (err) { serverError(res, err); }
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
router.post('/api/skills/install', requireAuth, requireScope('files:write', 'ssss:write'), async (req, res) => {
|
|
1145
|
-
try {
|
|
1146
|
-
const { pkg } = req.body;
|
|
1147
|
-
if (!pkg) {
|
|
1148
|
-
return res.status(400).json({ error: 'Missing required `pkg` body parameter.' });
|
|
1149
|
-
}
|
|
1150
|
-
const { installSkill } = await import('../../.agent/skills/total-recall/skills/skill/scripts/install-skill.mjs');
|
|
1151
|
-
const result = installSkill(pkg);
|
|
1152
|
-
res.json(result);
|
|
1153
|
-
} catch (err) { serverError(res, err); }
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
router.get('/api/skills/:name', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
|
|
1157
|
-
try {
|
|
1158
|
-
const { name } = req.params;
|
|
1159
|
-
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
1160
|
-
return res.status(400).json({ error: 'Invalid skill name' });
|
|
1161
|
-
}
|
|
1162
|
-
const skillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
1163
|
-
if (!fs.existsSync(skillPath)) {
|
|
1164
|
-
return res.status(404).json({ error: `Skill "${name}" not found` });
|
|
1165
|
-
}
|
|
1166
|
-
const content = fs.readFileSync(skillPath, 'utf8');
|
|
1167
|
-
res.json({ name, content });
|
|
1168
|
-
} catch (err) { serverError(res, err); }
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
router.put('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
|
|
1172
|
-
try {
|
|
1173
|
-
const { name } = req.params;
|
|
1174
|
-
const { content } = req.body;
|
|
1175
|
-
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
1176
|
-
return res.status(400).json({ error: 'Invalid skill name' });
|
|
1177
|
-
}
|
|
1178
|
-
if (typeof content !== 'string') {
|
|
1179
|
-
return res.status(400).json({ error: 'Missing or invalid `content` field.' });
|
|
1180
|
-
}
|
|
1181
|
-
const skillDir = path.join(SKILLS_DIR, name);
|
|
1182
|
-
if (!fs.existsSync(skillDir)) {
|
|
1183
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
1184
|
-
}
|
|
1185
|
-
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
1186
|
-
fs.writeFileSync(skillPath, content, 'utf8');
|
|
1187
|
-
res.json({ success: true, message: `Skill "${name}" updated successfully` });
|
|
1188
|
-
} catch (err) { serverError(res, err); }
|
|
1189
|
-
});
|
|
1190
|
-
|
|
1191
|
-
router.delete('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
|
|
1192
|
-
try {
|
|
1193
|
-
const { name } = req.params;
|
|
1194
|
-
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
1195
|
-
return res.status(400).json({ error: 'Invalid skill name' });
|
|
1196
|
-
}
|
|
1197
|
-
const skillDir = path.join(SKILLS_DIR, name);
|
|
1198
|
-
if (!fs.existsSync(skillDir)) {
|
|
1199
|
-
return res.status(404).json({ error: `Skill "${name}" not found` });
|
|
1200
|
-
}
|
|
1201
|
-
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
1202
|
-
res.json({ success: true, message: `Skill "${name}" deleted successfully` });
|
|
1203
|
-
} catch (err) { serverError(res, err); }
|
|
1204
|
-
});
|
|
1046
|
+
// Moved to routes/skills.mjs
|
|
1205
1047
|
|
|
1206
1048
|
// ─── Scripts Editor & Execution ───────────────────────────────────────────────
|
|
1207
1049
|
|
|
@@ -1345,6 +1187,12 @@ router.get('/api/tasks', requireAuth, requireScope('tasks:read'), (req, res) =>
|
|
|
1345
1187
|
try {
|
|
1346
1188
|
const raw = fs.readFileSync(path.join(TASKS_DIR, file), 'utf8');
|
|
1347
1189
|
const { data, content } = matter(raw);
|
|
1190
|
+
|
|
1191
|
+
// By default, filter out completed tasks
|
|
1192
|
+
if (req.query.status !== 'all' && data.status === 'completed') {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1348
1196
|
tasks.push({ ...data, body: content.trim(), slug: file.replace('.md', '') });
|
|
1349
1197
|
} catch (e) {
|
|
1350
1198
|
// skip
|
|
@@ -1354,6 +1202,31 @@ router.get('/api/tasks', requireAuth, requireScope('tasks:read'), (req, res) =>
|
|
|
1354
1202
|
} catch (err) { serverError(res, err); }
|
|
1355
1203
|
});
|
|
1356
1204
|
|
|
1205
|
+
router.delete('/api/tasks/cleanup', requireAuth, requireScope('tasks:write'), (req, res) => {
|
|
1206
|
+
try {
|
|
1207
|
+
if (!fs.existsSync(TASKS_DIR)) {
|
|
1208
|
+
return res.json({ deleted: 0 });
|
|
1209
|
+
}
|
|
1210
|
+
let deleted = 0;
|
|
1211
|
+
const files = fs.readdirSync(TASKS_DIR);
|
|
1212
|
+
for (const file of files) {
|
|
1213
|
+
if (!file.endsWith('.md')) continue;
|
|
1214
|
+
const filePath = path.join(TASKS_DIR, file);
|
|
1215
|
+
try {
|
|
1216
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
1217
|
+
const { data } = matter(raw);
|
|
1218
|
+
if (data.status === 'completed') {
|
|
1219
|
+
fs.unlinkSync(filePath);
|
|
1220
|
+
deleted++;
|
|
1221
|
+
}
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
// skip
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
res.json({ deleted });
|
|
1227
|
+
} catch (err) { serverError(res, err); }
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1357
1230
|
router.post('/api/tasks', requireAuth, requireScope('tasks:write'), (req, res) => {
|
|
1358
1231
|
try {
|
|
1359
1232
|
const { category, target, body, priority = 5 } = req.body || {};
|
|
@@ -1559,26 +1432,7 @@ router.post('/api/capture/:source', requireAuth, requireScope('memory:write'), a
|
|
|
1559
1432
|
});
|
|
1560
1433
|
|
|
1561
1434
|
// ─── Consolidated Auth Routes ──────────────────────────────────────────────────
|
|
1562
|
-
|
|
1563
|
-
router.post('/auth/login', loginHandler);
|
|
1564
|
-
router.post('/auth/logout', logoutHandler);
|
|
1565
|
-
router.post('/auth/change-password', requireAuth, changePasswordHandler);
|
|
1566
|
-
router.get('/auth/me', requireAuth, (req, res) => res.json({ authenticated: true }));
|
|
1567
|
-
|
|
1568
|
-
router.get("/auth/status", (req, res) => {
|
|
1569
|
-
const config = loadSecurityConfig();
|
|
1570
|
-
const configured = !!config.dashboard?.password_hash;
|
|
1571
|
-
res.json({ configured });
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
router.post("/auth/setup", async (req, res) => {
|
|
1575
|
-
const config = loadSecurityConfig();
|
|
1576
|
-
const alreadyConfigured = !!config.dashboard?.password_hash;
|
|
1577
|
-
if (alreadyConfigured) {
|
|
1578
|
-
return res.status(400).json({ error: "Dashboard password is already configured" });
|
|
1579
|
-
}
|
|
1580
|
-
return changePasswordHandler(req, res);
|
|
1581
|
-
});
|
|
1435
|
+
// Moved to routes/auth.mjs
|
|
1582
1436
|
|
|
1583
1437
|
// ─── Voice / TTS (Kokoro / System) ───────────────────────────────────────────────
|
|
1584
1438
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import {
|
|
3
|
+
requireAuth,
|
|
4
|
+
loadSecurityConfig,
|
|
5
|
+
loginHandler,
|
|
6
|
+
logoutHandler,
|
|
7
|
+
changePasswordHandler,
|
|
8
|
+
} from '../auth.mjs';
|
|
9
|
+
|
|
10
|
+
export const authRouter = express.Router();
|
|
11
|
+
|
|
12
|
+
// ─── Consolidated Auth Routes ──────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
authRouter.post('/auth/login', loginHandler);
|
|
15
|
+
authRouter.post('/auth/logout', logoutHandler);
|
|
16
|
+
authRouter.post('/auth/change-password', requireAuth, changePasswordHandler);
|
|
17
|
+
authRouter.get('/auth/me', requireAuth, (req, res) => res.json({ authenticated: true }));
|
|
18
|
+
|
|
19
|
+
authRouter.get("/auth/status", (req, res) => {
|
|
20
|
+
const config = loadSecurityConfig();
|
|
21
|
+
const configured = !!config.dashboard?.password_hash;
|
|
22
|
+
res.json({ configured });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
authRouter.post("/auth/setup", async (req, res) => {
|
|
26
|
+
const config = loadSecurityConfig();
|
|
27
|
+
const alreadyConfigured = !!config.dashboard?.password_hash;
|
|
28
|
+
if (alreadyConfigured) {
|
|
29
|
+
return res.status(400).json({ error: "Dashboard password is already configured" });
|
|
30
|
+
}
|
|
31
|
+
return changePasswordHandler(req, res);
|
|
32
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { requireAuth, requireScope } from '../auth.mjs';
|
|
3
|
+
import { listQueue, addToQueue, updateQueueItem, removeFromQueue } from '../../core/research-queue.mjs';
|
|
4
|
+
import { logger } from '../../core/logger.mjs';
|
|
5
|
+
|
|
6
|
+
function badRequest(res, msg) {
|
|
7
|
+
return res.status(400).json({ error: msg });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function notFound(res, msg) {
|
|
11
|
+
return res.status(404).json({ error: msg || 'Not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function serverError(res, err) {
|
|
15
|
+
logger.error('research', 'Internal server error', { error: err.message, stack: err.stack });
|
|
16
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const researchRouter = express.Router();
|
|
20
|
+
|
|
21
|
+
// ─── Research Queue ───────────────────────────────────────────────────────────
|
|
22
|
+
// Thin REST wrappers over src/core/research-queue.mjs
|
|
23
|
+
|
|
24
|
+
researchRouter.get('/api/research', requireAuth, requireScope('memory:read'), (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const { status, query, limit, offset } = req.query;
|
|
27
|
+
res.json(listQueue({ status, query, limit, offset }));
|
|
28
|
+
} catch (err) { serverError(res, err); }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
researchRouter.post('/api/research', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const { topic, priority, notes } = req.body || {};
|
|
34
|
+
if (!topic) return badRequest(res, 'topic is required');
|
|
35
|
+
res.status(201).json(addToQueue({ topic, priority, notes }));
|
|
36
|
+
} catch (err) { serverError(res, err); }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
researchRouter.patch('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
res.json(updateQueueItem(req.params.id, req.body || {}));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.status === 404) return notFound(res, err.message);
|
|
44
|
+
serverError(res, err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
researchRouter.delete('/api/research/:id', requireAuth, requireScope('memory:write'), (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
res.json(removeFromQueue(req.params.id));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.status === 404) return notFound(res, err.message);
|
|
53
|
+
serverError(res, err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { runInSandbox } from '../../core/sandbox.mjs';
|
|
6
|
+
import {
|
|
7
|
+
requireAuth,
|
|
8
|
+
requireScope,
|
|
9
|
+
sandboxRateLimiter,
|
|
10
|
+
requireSandboxEnabled,
|
|
11
|
+
} from '../auth.mjs';
|
|
12
|
+
import { logger } from '../../core/logger.mjs';
|
|
13
|
+
|
|
14
|
+
function badRequest(res, msg) {
|
|
15
|
+
return res.status(400).json({ error: msg });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function serverError(res, err) {
|
|
19
|
+
logger.error('sandbox', 'Internal server error', { error: err.message, stack: err.stack });
|
|
20
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const sandboxRouter = express.Router();
|
|
24
|
+
|
|
25
|
+
// ─── Sandbox ──────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* POST /api/sandbox
|
|
29
|
+
* Body: { code }
|
|
30
|
+
*/
|
|
31
|
+
// Sandbox is rate-limited *before* requireAuth on purpose — limiter keys on
|
|
32
|
+
// the authenticated principal when present, so it's IP-bucketed for
|
|
33
|
+
// pre-auth misuse and key-bucketed once a PAT is attached.
|
|
34
|
+
sandboxRouter.post('/api/sandbox', sandboxRateLimiter(), requireAuth, requireScope('sandbox:run'), requireSandboxEnabled, async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const { code } = req.body || {};
|
|
37
|
+
if (!code) return badRequest(res, 'code is required');
|
|
38
|
+
|
|
39
|
+
const tmpDir = path.join(os.tmpdir(), 'total-recall-sandbox');
|
|
40
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
41
|
+
const script = path.join(tmpDir, `rest-${Date.now()}.mjs`);
|
|
42
|
+
fs.writeFileSync(script, code);
|
|
43
|
+
|
|
44
|
+
const result = await runInSandbox(script, 15000);
|
|
45
|
+
try { fs.unlinkSync(script); } catch {}
|
|
46
|
+
|
|
47
|
+
res.json({
|
|
48
|
+
success: result.success,
|
|
49
|
+
exit_code: result.code,
|
|
50
|
+
output: result.output,
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
serverError(res, err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { requireAuth, requireScope } from '../auth.mjs';
|
|
5
|
+
import { logger } from '../../core/logger.mjs';
|
|
6
|
+
import { SKILLS_DIR } from './_shared.mjs';
|
|
7
|
+
|
|
8
|
+
function serverError(res, err) {
|
|
9
|
+
logger.error('skills', 'Internal server error', { error: err.message, stack: err.stack });
|
|
10
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const skillsRouter = express.Router();
|
|
14
|
+
|
|
15
|
+
skillsRouter.get('/api/skills', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
18
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const skills = fs.readdirSync(SKILLS_DIR).map(dir => {
|
|
21
|
+
const dirPath = path.join(SKILLS_DIR, dir);
|
|
22
|
+
const stats = fs.statSync(dirPath);
|
|
23
|
+
|
|
24
|
+
let subSkills = [];
|
|
25
|
+
const subSkillsPath = path.join(dirPath, 'skills');
|
|
26
|
+
if (fs.existsSync(subSkillsPath) && fs.statSync(subSkillsPath).isDirectory()) {
|
|
27
|
+
try {
|
|
28
|
+
subSkills = fs.readdirSync(subSkillsPath).filter(sd =>
|
|
29
|
+
fs.statSync(path.join(subSkillsPath, sd)).isDirectory()
|
|
30
|
+
);
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
name: dir,
|
|
36
|
+
size: stats.size,
|
|
37
|
+
modified: stats.mtime,
|
|
38
|
+
isDirectory: stats.isDirectory(),
|
|
39
|
+
subSkills
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
res.json(skills);
|
|
43
|
+
} catch (err) { serverError(res, err); }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
skillsRouter.get('/api/skills/search', requireAuth, requireScope('files:read', 'ssss:read'), async (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const q = req.query.q || '';
|
|
49
|
+
if (!q) {
|
|
50
|
+
return res.status(400).json({ error: 'Missing search query parameter `q`.' });
|
|
51
|
+
}
|
|
52
|
+
const { searchAndSort } = await import('../../../.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs');
|
|
53
|
+
const results = searchAndSort(q);
|
|
54
|
+
res.json(results);
|
|
55
|
+
} catch (err) { serverError(res, err); }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
skillsRouter.post('/api/skills/install', requireAuth, requireScope('files:write', 'ssss:write'), async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const { pkg } = req.body;
|
|
61
|
+
if (!pkg) {
|
|
62
|
+
return res.status(400).json({ error: 'Missing required `pkg` body parameter.' });
|
|
63
|
+
}
|
|
64
|
+
const { installSkill } = await import('../../../.agent/skills/total-recall/skills/skill/scripts/install-skill.mjs');
|
|
65
|
+
const result = installSkill(pkg);
|
|
66
|
+
res.json(result);
|
|
67
|
+
} catch (err) { serverError(res, err); }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
skillsRouter.get('/api/skills/:name', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const { name } = req.params;
|
|
73
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
74
|
+
return res.status(400).json({ error: 'Invalid skill name' });
|
|
75
|
+
}
|
|
76
|
+
const skillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
77
|
+
if (!fs.existsSync(skillPath)) {
|
|
78
|
+
return res.status(404).json({ error: `Skill "${name}" not found` });
|
|
79
|
+
}
|
|
80
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
81
|
+
res.json({ name, content });
|
|
82
|
+
} catch (err) { serverError(res, err); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
skillsRouter.put('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { name } = req.params;
|
|
88
|
+
const { content } = req.body;
|
|
89
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
90
|
+
return res.status(400).json({ error: 'Invalid skill name' });
|
|
91
|
+
}
|
|
92
|
+
if (typeof content !== 'string') {
|
|
93
|
+
return res.status(400).json({ error: 'Missing or invalid `content` field.' });
|
|
94
|
+
}
|
|
95
|
+
const skillDir = path.join(SKILLS_DIR, name);
|
|
96
|
+
if (!fs.existsSync(skillDir)) {
|
|
97
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
100
|
+
fs.writeFileSync(skillPath, content, 'utf8');
|
|
101
|
+
res.json({ success: true, message: `Skill "${name}" updated successfully` });
|
|
102
|
+
} catch (err) { serverError(res, err); }
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
skillsRouter.delete('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const { name } = req.params;
|
|
108
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
109
|
+
return res.status(400).json({ error: 'Invalid skill name' });
|
|
110
|
+
}
|
|
111
|
+
const skillDir = path.join(SKILLS_DIR, name);
|
|
112
|
+
if (!fs.existsSync(skillDir)) {
|
|
113
|
+
return res.status(404).json({ error: `Skill "${name}" not found` });
|
|
114
|
+
}
|
|
115
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
116
|
+
res.json({ success: true, message: `Skill "${name}" deleted successfully` });
|
|
117
|
+
} catch (err) { serverError(res, err); }
|
|
118
|
+
});
|