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
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills API — project skills by repo (excludes Total Recall / tr-* system skills).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/skills list skills grouped by registered project
|
|
5
|
+
* GET /api/skills/:name?repo=… read SKILL.md
|
|
6
|
+
* PUT /api/skills/:name?repo=… write SKILL.md
|
|
7
|
+
* DELETE /api/skills/:name?repo=… remove skill dir
|
|
8
|
+
* GET /api/skills/:name/files?repo=…&dir=…
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
import express from 'express';
|
|
2
12
|
import fs from 'node:fs';
|
|
3
13
|
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
4
15
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
16
|
import { requireAuth, requireScope } from '../auth.mjs';
|
|
6
17
|
import { logger } from '../../core/logger.mjs';
|
|
@@ -8,14 +19,325 @@ import { AGENT_DIR, SKILLS_DIR } from './_shared.mjs';
|
|
|
8
19
|
|
|
9
20
|
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
10
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Project-level skill roots mapped to IDEs that load them.
|
|
24
|
+
* Sources: Claude Code docs (.claude/skills), Cursor docs (.cursor/skills + .agents/skills + compat),
|
|
25
|
+
* Gemini CLI (.gemini/skills + .agents/skills), Codex (~/.codex/skills primarily; project .codex/skills),
|
|
26
|
+
* Total Recall / agent (.agent/skills).
|
|
27
|
+
*/
|
|
28
|
+
const SKILL_ROOTS = [
|
|
29
|
+
{
|
|
30
|
+
rel: '.claude/skills',
|
|
31
|
+
ide: 'claude-code',
|
|
32
|
+
ide_label: 'Claude Code',
|
|
33
|
+
docs: 'https://code.claude.com/docs/en/skills',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
rel: '.cursor/skills',
|
|
37
|
+
ide: 'cursor',
|
|
38
|
+
ide_label: 'Cursor',
|
|
39
|
+
docs: 'https://cursor.com/docs/skills',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
rel: '.agents/skills',
|
|
43
|
+
ide: 'agents-standard',
|
|
44
|
+
ide_label: 'Agents standard (Cursor / Gemini)',
|
|
45
|
+
docs: 'https://cursor.com/docs/skills',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
rel: '.gemini/skills',
|
|
49
|
+
ide: 'gemini',
|
|
50
|
+
ide_label: 'Gemini CLI',
|
|
51
|
+
docs: 'https://geminicli.com/docs/cli/skills/',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
rel: '.codex/skills',
|
|
55
|
+
ide: 'codex',
|
|
56
|
+
ide_label: 'Codex',
|
|
57
|
+
docs: 'https://developers.openai.com/codex/skills',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
rel: '.agent/skills',
|
|
61
|
+
ide: 'total-recall',
|
|
62
|
+
ide_label: 'Total Recall / .agent',
|
|
63
|
+
docs: null,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Back-compat alias for older callers
|
|
68
|
+
const SKILL_DIR_CANDIDATES = SKILL_ROOTS.map((r) => r.rel);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Exclude Total Recall core / product skills from the UI list.
|
|
72
|
+
* User-facing Skills page should only show project/app skills.
|
|
73
|
+
*/
|
|
74
|
+
const TR_SKILL_EXCLUDE = new Set([
|
|
75
|
+
'total-recall',
|
|
76
|
+
'tr-skill',
|
|
77
|
+
'tr-ssss',
|
|
78
|
+
'tr-research',
|
|
79
|
+
'tr-cli-agents',
|
|
80
|
+
'tr-cli-agent',
|
|
81
|
+
'cli-agents', // TR CLI agent registry package, not an app skill
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
function isTrSkillName(name) {
|
|
85
|
+
if (!name || typeof name !== 'string') return true;
|
|
86
|
+
const n = name.toLowerCase();
|
|
87
|
+
if (TR_SKILL_EXCLUDE.has(n)) return true;
|
|
88
|
+
if (n.startsWith('tr-')) return true;
|
|
89
|
+
if (n.startsWith('total-recall')) return true;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rootMeta(rel) {
|
|
94
|
+
return SKILL_ROOTS.find((r) => r.rel === rel) || {
|
|
95
|
+
rel,
|
|
96
|
+
ide: 'unknown',
|
|
97
|
+
ide_label: rel,
|
|
98
|
+
docs: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadProjectRegistry() {
|
|
103
|
+
const registryPath = path.join(
|
|
104
|
+
os.homedir(),
|
|
105
|
+
'.agent',
|
|
106
|
+
'skills',
|
|
107
|
+
'total-recall',
|
|
108
|
+
'config',
|
|
109
|
+
'project-registry.json',
|
|
110
|
+
);
|
|
111
|
+
if (!fs.existsSync(registryPath)) return [];
|
|
112
|
+
try {
|
|
113
|
+
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
114
|
+
return Array.isArray(data) ? data : [];
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function listSubSkills(skillDir) {
|
|
121
|
+
const subSkills = [];
|
|
122
|
+
try {
|
|
123
|
+
for (const entry of fs.readdirSync(skillDir, { withFileTypes: true })) {
|
|
124
|
+
if (!entry.isDirectory()) continue;
|
|
125
|
+
// nested skills/ folder OR common skill subdirs
|
|
126
|
+
if (entry.name === 'skills') {
|
|
127
|
+
const nested = path.join(skillDir, 'skills');
|
|
128
|
+
try {
|
|
129
|
+
for (const sd of fs.readdirSync(nested, { withFileTypes: true })) {
|
|
130
|
+
if (sd.isDirectory()) subSkills.push(sd.name);
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
/* ignore */
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
subSkills.push(entry.name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
/* ignore */
|
|
141
|
+
}
|
|
142
|
+
return subSkills;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scanSkillRoot(skillsRoot, repo, projectPath, rootInfo) {
|
|
146
|
+
if (!fs.existsSync(skillsRoot) || !fs.statSync(skillsRoot).isDirectory()) return [];
|
|
147
|
+
const out = [];
|
|
148
|
+
let entries;
|
|
149
|
+
try {
|
|
150
|
+
entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const sourceRel = rootInfo.rel;
|
|
155
|
+
for (const ent of entries) {
|
|
156
|
+
if (!ent.isDirectory()) continue;
|
|
157
|
+
if (isTrSkillName(ent.name)) continue;
|
|
158
|
+
const skillDir = path.join(skillsRoot, ent.name);
|
|
159
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
160
|
+
const skillMdAlt = path.join(skillDir, 'skill.md');
|
|
161
|
+
const hasSkillMd = fs.existsSync(skillMd) || fs.existsSync(skillMdAlt);
|
|
162
|
+
let stats;
|
|
163
|
+
try {
|
|
164
|
+
stats = fs.statSync(skillDir);
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
out.push({
|
|
169
|
+
// Unique per repo + IDE path + name (same name can exist under multiple IDEs)
|
|
170
|
+
id: `${repo}::${rootInfo.ide}::${ent.name}`,
|
|
171
|
+
name: ent.name,
|
|
172
|
+
repo,
|
|
173
|
+
project_path: projectPath,
|
|
174
|
+
source: sourceRel,
|
|
175
|
+
ide: rootInfo.ide,
|
|
176
|
+
ide_label: rootInfo.ide_label,
|
|
177
|
+
skill_dir: skillDir,
|
|
178
|
+
has_skill_md: hasSkillMd,
|
|
179
|
+
size: stats.size,
|
|
180
|
+
modified: stats.mtime,
|
|
181
|
+
isDirectory: true,
|
|
182
|
+
subSkills: listSubSkills(skillDir),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Collect skills for all registered projects, grouped by repo → IDE.
|
|
190
|
+
* Excludes total-recall / tr-* system skills.
|
|
191
|
+
*/
|
|
192
|
+
export function collectSkillsByRepo() {
|
|
193
|
+
const registry = loadProjectRegistry();
|
|
194
|
+
const byRepo = new Map();
|
|
195
|
+
|
|
196
|
+
for (const entry of registry) {
|
|
197
|
+
const projectPath = entry.path || entry.project_root;
|
|
198
|
+
const repo = entry.name || (projectPath ? path.basename(projectPath) : null);
|
|
199
|
+
if (!repo || !projectPath || !fs.existsSync(projectPath)) {
|
|
200
|
+
if (repo) {
|
|
201
|
+
byRepo.set(repo, {
|
|
202
|
+
repo,
|
|
203
|
+
path: projectPath || null,
|
|
204
|
+
skills: [],
|
|
205
|
+
ides: [],
|
|
206
|
+
ok: false,
|
|
207
|
+
error: 'path missing',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const skills = [];
|
|
214
|
+
const byIde = new Map();
|
|
215
|
+
|
|
216
|
+
for (const rootInfo of SKILL_ROOTS) {
|
|
217
|
+
const root = path.join(projectPath, rootInfo.rel);
|
|
218
|
+
const found = scanSkillRoot(root, repo, projectPath, rootInfo);
|
|
219
|
+
if (!found.length) continue;
|
|
220
|
+
found.sort((a, b) => a.name.localeCompare(b.name));
|
|
221
|
+
skills.push(...found);
|
|
222
|
+
byIde.set(rootInfo.ide, {
|
|
223
|
+
ide: rootInfo.ide,
|
|
224
|
+
ide_label: rootInfo.ide_label,
|
|
225
|
+
source: rootInfo.rel,
|
|
226
|
+
docs: rootInfo.docs,
|
|
227
|
+
skills: found,
|
|
228
|
+
count: found.length,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Stable IDE order matching SKILL_ROOTS
|
|
233
|
+
const ides = SKILL_ROOTS.map((r) => byIde.get(r.ide)).filter(Boolean);
|
|
234
|
+
|
|
235
|
+
skills.sort((a, b) => {
|
|
236
|
+
const ideCmp = (a.ide_label || '').localeCompare(b.ide_label || '');
|
|
237
|
+
if (ideCmp !== 0) return ideCmp;
|
|
238
|
+
return a.name.localeCompare(b.name);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
byRepo.set(repo, {
|
|
242
|
+
repo,
|
|
243
|
+
path: projectPath,
|
|
244
|
+
skills,
|
|
245
|
+
ides,
|
|
246
|
+
ok: true,
|
|
247
|
+
count: skills.length,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const repos = [...byRepo.values()].sort((a, b) => a.repo.localeCompare(b.repo));
|
|
252
|
+
const flat = repos.flatMap((r) => r.skills);
|
|
253
|
+
return {
|
|
254
|
+
repos,
|
|
255
|
+
skills: flat,
|
|
256
|
+
total: flat.length,
|
|
257
|
+
ide_roots: SKILL_ROOTS.map(({ rel, ide, ide_label, docs }) => ({
|
|
258
|
+
rel,
|
|
259
|
+
ide,
|
|
260
|
+
ide_label,
|
|
261
|
+
docs,
|
|
262
|
+
})),
|
|
263
|
+
excluded_note: 'total-recall and tr-* skills are hidden from this catalog',
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveRegisteredProject(repoName) {
|
|
268
|
+
if (!repoName) return null;
|
|
269
|
+
const registry = loadProjectRegistry();
|
|
270
|
+
const entry = registry.find(
|
|
271
|
+
(p) => p.name === repoName || path.basename(p.path || p.project_root || '') === repoName,
|
|
272
|
+
);
|
|
273
|
+
if (!entry) return null;
|
|
274
|
+
const projectPath = entry.path || entry.project_root;
|
|
275
|
+
if (!projectPath || !fs.existsSync(projectPath)) return null;
|
|
276
|
+
return { name: entry.name || repoName, path: path.resolve(projectPath) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Resolve skill directory under a registered project only.
|
|
281
|
+
* @param {string} repoName
|
|
282
|
+
* @param {string} skillName
|
|
283
|
+
* @param {{ source?: string, ide?: string }} [opts]
|
|
284
|
+
*/
|
|
285
|
+
function resolveSkillDir(repoName, skillName, opts = {}) {
|
|
286
|
+
if (!skillName || skillName.includes('..') || skillName.includes('/') || skillName.includes('\\')) {
|
|
287
|
+
throw new Error('Invalid skill name');
|
|
288
|
+
}
|
|
289
|
+
if (isTrSkillName(skillName)) {
|
|
290
|
+
throw new Error('Total Recall system skills are not managed on this page');
|
|
291
|
+
}
|
|
292
|
+
const project = resolveRegisteredProject(repoName);
|
|
293
|
+
if (!project) {
|
|
294
|
+
// Fallback: global SKILLS_DIR for backward compat (non-TR only)
|
|
295
|
+
if (!repoName || repoName === 'global' || repoName === '_') {
|
|
296
|
+
const globalDir = path.join(SKILLS_DIR, skillName);
|
|
297
|
+
if (fs.existsSync(globalDir) && !isTrSkillName(skillName)) {
|
|
298
|
+
return { skillDir: globalDir, repo: 'global', projectPath: AGENT_DIR };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
throw new Error(`Unknown repo: ${repoName || '(none)'}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const preferredSource =
|
|
305
|
+
opts.source ||
|
|
306
|
+
(opts.ide ? SKILL_ROOTS.find((r) => r.ide === opts.ide)?.rel : null);
|
|
307
|
+
const roots = preferredSource
|
|
308
|
+
? [
|
|
309
|
+
...SKILL_ROOTS.filter((r) => r.rel === preferredSource),
|
|
310
|
+
...SKILL_ROOTS.filter((r) => r.rel !== preferredSource),
|
|
311
|
+
]
|
|
312
|
+
: SKILL_ROOTS;
|
|
313
|
+
|
|
314
|
+
for (const rootInfo of roots) {
|
|
315
|
+
const skillDir = path.join(project.path, rootInfo.rel, skillName);
|
|
316
|
+
if (fs.existsSync(skillDir) && fs.statSync(skillDir).isDirectory()) {
|
|
317
|
+
const resolved = path.resolve(skillDir);
|
|
318
|
+
if (!resolved.startsWith(project.path + path.sep) && resolved !== project.path) {
|
|
319
|
+
throw new Error('Skill path escapes project root');
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
skillDir: resolved,
|
|
323
|
+
repo: project.name,
|
|
324
|
+
projectPath: project.path,
|
|
325
|
+
source: rootInfo.rel,
|
|
326
|
+
ide: rootInfo.ide,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`Skill "${skillName}" not found in repo ${project.name}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
11
333
|
function resolveTrSkillScript(scriptName) {
|
|
12
334
|
const candidates = [
|
|
335
|
+
path.join(AGENT_DIR, 'skills', 'total-recall', 'modules', 'skill-deploy', 'scripts', scriptName),
|
|
336
|
+
path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'modules', 'skill-deploy', 'scripts', scriptName),
|
|
13
337
|
path.join(AGENT_DIR, 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
|
|
14
|
-
path.join(AGENT_DIR, 'skills', 'total-recall', 'skills', 'skill', 'scripts', scriptName),
|
|
15
338
|
path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
|
|
16
|
-
path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'skills', 'skill', 'scripts', scriptName),
|
|
17
339
|
];
|
|
18
|
-
const found = candidates.find(candidate => fs.existsSync(candidate));
|
|
340
|
+
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
|
19
341
|
if (!found) {
|
|
20
342
|
throw new Error(`Could not find bundled skill helper "${scriptName}".`);
|
|
21
343
|
}
|
|
@@ -33,35 +355,13 @@ function serverError(res, err) {
|
|
|
33
355
|
|
|
34
356
|
export const skillsRouter = express.Router();
|
|
35
357
|
|
|
36
|
-
skillsRouter.get('/api/skills', requireAuth, requireScope('files:read', 'ssss:read'), (
|
|
358
|
+
skillsRouter.get('/api/skills', requireAuth, requireScope('files:read', 'ssss:read'), (_req, res) => {
|
|
37
359
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const stats = fs.statSync(dirPath);
|
|
44
|
-
|
|
45
|
-
let subSkills = [];
|
|
46
|
-
const subSkillsPath = path.join(dirPath, 'skills');
|
|
47
|
-
if (fs.existsSync(subSkillsPath) && fs.statSync(subSkillsPath).isDirectory()) {
|
|
48
|
-
try {
|
|
49
|
-
subSkills = fs.readdirSync(subSkillsPath).filter(sd =>
|
|
50
|
-
fs.statSync(path.join(subSkillsPath, sd)).isDirectory()
|
|
51
|
-
);
|
|
52
|
-
} catch (e) {}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
name: dir,
|
|
57
|
-
size: stats.size,
|
|
58
|
-
modified: stats.mtime,
|
|
59
|
-
isDirectory: stats.isDirectory(),
|
|
60
|
-
subSkills
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
res.json(skills);
|
|
64
|
-
} catch (err) { serverError(res, err); }
|
|
360
|
+
const catalog = collectSkillsByRepo();
|
|
361
|
+
res.json(catalog);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
serverError(res, err);
|
|
364
|
+
}
|
|
65
365
|
});
|
|
66
366
|
|
|
67
367
|
skillsRouter.get('/api/skills/search', requireAuth, requireScope('files:read', 'ssss:read'), async (req, res) => {
|
|
@@ -73,7 +373,9 @@ skillsRouter.get('/api/skills/search', requireAuth, requireScope('files:read', '
|
|
|
73
373
|
const { searchAndSort } = await importTrSkillScript('find-skills.mjs');
|
|
74
374
|
const results = searchAndSort(q);
|
|
75
375
|
res.json(results);
|
|
76
|
-
} catch (err) {
|
|
376
|
+
} catch (err) {
|
|
377
|
+
serverError(res, err);
|
|
378
|
+
}
|
|
77
379
|
});
|
|
78
380
|
|
|
79
381
|
skillsRouter.post('/api/skills/install', requireAuth, requireScope('files:write', 'ssss:write'), async (req, res) => {
|
|
@@ -85,55 +387,151 @@ skillsRouter.post('/api/skills/install', requireAuth, requireScope('files:write'
|
|
|
85
387
|
const { installSkill } = await importTrSkillScript('install-skill.mjs');
|
|
86
388
|
const result = installSkill(pkg, { agentDir: AGENT_DIR });
|
|
87
389
|
res.json(result);
|
|
88
|
-
} catch (err) {
|
|
390
|
+
} catch (err) {
|
|
391
|
+
serverError(res, err);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
skillsRouter.get('/api/skills/:name/files', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
|
|
396
|
+
try {
|
|
397
|
+
const { name } = req.params;
|
|
398
|
+
const repo = req.query.repo || req.query.project || '';
|
|
399
|
+
const source = req.query.source || '';
|
|
400
|
+
const ide = req.query.ide || '';
|
|
401
|
+
let skillDir;
|
|
402
|
+
try {
|
|
403
|
+
({ skillDir } = resolveSkillDir(String(repo), name, { source: source || undefined, ide: ide || undefined }));
|
|
404
|
+
} catch (e) {
|
|
405
|
+
return res.status(404).json({ error: e.message });
|
|
406
|
+
}
|
|
407
|
+
const dirParam = req.query.dir;
|
|
408
|
+
let targetDir = skillDir;
|
|
409
|
+
if (dirParam) {
|
|
410
|
+
if (dirParam.includes('..') || dirParam.includes('/') || dirParam.includes('\\')) {
|
|
411
|
+
return res.status(400).json({ error: 'Invalid dir parameter' });
|
|
412
|
+
}
|
|
413
|
+
targetDir = path.join(skillDir, dirParam);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
|
|
417
|
+
return res.json([]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const files = fs
|
|
421
|
+
.readdirSync(targetDir)
|
|
422
|
+
.map((file) => {
|
|
423
|
+
const stats = fs.statSync(path.join(targetDir, file));
|
|
424
|
+
return {
|
|
425
|
+
name: file,
|
|
426
|
+
size: stats.isDirectory()
|
|
427
|
+
? 'DIR'
|
|
428
|
+
: stats.size < 1024
|
|
429
|
+
? `${stats.size} B`
|
|
430
|
+
: `${(stats.size / 1024).toFixed(1)} KB`,
|
|
431
|
+
isDirectory: stats.isDirectory(),
|
|
432
|
+
};
|
|
433
|
+
})
|
|
434
|
+
.filter((f) => !f.isDirectory);
|
|
435
|
+
|
|
436
|
+
res.json(files);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
serverError(res, err);
|
|
439
|
+
}
|
|
89
440
|
});
|
|
90
441
|
|
|
91
442
|
skillsRouter.get('/api/skills/:name', requireAuth, requireScope('files:read', 'ssss:read'), (req, res) => {
|
|
92
443
|
try {
|
|
93
444
|
const { name } = req.params;
|
|
94
|
-
|
|
95
|
-
|
|
445
|
+
const repo = req.query.repo || req.query.project || '';
|
|
446
|
+
const source = req.query.source || '';
|
|
447
|
+
const ide = req.query.ide || '';
|
|
448
|
+
let skillDir;
|
|
449
|
+
let resolved;
|
|
450
|
+
try {
|
|
451
|
+
resolved = resolveSkillDir(String(repo), name, {
|
|
452
|
+
source: source || undefined,
|
|
453
|
+
ide: ide || undefined,
|
|
454
|
+
});
|
|
455
|
+
skillDir = resolved.skillDir;
|
|
456
|
+
} catch (e) {
|
|
457
|
+
return res.status(404).json({ error: e.message });
|
|
96
458
|
}
|
|
97
|
-
const skillPath = path.join(
|
|
459
|
+
const skillPath = fs.existsSync(path.join(skillDir, 'SKILL.md'))
|
|
460
|
+
? path.join(skillDir, 'SKILL.md')
|
|
461
|
+
: path.join(skillDir, 'skill.md');
|
|
98
462
|
if (!fs.existsSync(skillPath)) {
|
|
99
|
-
return res.status(404).json({ error: `
|
|
463
|
+
return res.status(404).json({ error: `SKILL.md not found for "${name}"` });
|
|
100
464
|
}
|
|
101
465
|
const content = fs.readFileSync(skillPath, 'utf8');
|
|
102
|
-
res.json({
|
|
103
|
-
|
|
466
|
+
res.json({
|
|
467
|
+
name,
|
|
468
|
+
repo: String(repo),
|
|
469
|
+
source: resolved.source,
|
|
470
|
+
ide: resolved.ide,
|
|
471
|
+
content,
|
|
472
|
+
skill_dir: skillDir,
|
|
473
|
+
});
|
|
474
|
+
} catch (err) {
|
|
475
|
+
serverError(res, err);
|
|
476
|
+
}
|
|
104
477
|
});
|
|
105
478
|
|
|
106
479
|
skillsRouter.put('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
|
|
107
480
|
try {
|
|
108
481
|
const { name } = req.params;
|
|
109
|
-
const { content } = req.body;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
482
|
+
const { content, repo: bodyRepo, source: bodySource, ide: bodyIde } = req.body || {};
|
|
483
|
+
const repo = bodyRepo || req.query.repo || req.query.project || '';
|
|
484
|
+
const source = bodySource || req.query.source || '';
|
|
485
|
+
const ide = bodyIde || req.query.ide || '';
|
|
113
486
|
if (typeof content !== 'string') {
|
|
114
487
|
return res.status(400).json({ error: 'Missing or invalid `content` field.' });
|
|
115
488
|
}
|
|
116
|
-
|
|
117
|
-
|
|
489
|
+
let skillDir;
|
|
490
|
+
try {
|
|
491
|
+
({ skillDir } = resolveSkillDir(String(repo), name, {
|
|
492
|
+
source: source || undefined,
|
|
493
|
+
ide: ide || undefined,
|
|
494
|
+
}));
|
|
495
|
+
} catch (e) {
|
|
496
|
+
// create under preferred IDE path (default Claude Code or .agent)
|
|
497
|
+
const project = resolveRegisteredProject(String(repo));
|
|
498
|
+
if (!project) return res.status(404).json({ error: e.message });
|
|
499
|
+
if (isTrSkillName(name)) {
|
|
500
|
+
return res.status(400).json({ error: 'Cannot create Total Recall system skills here' });
|
|
501
|
+
}
|
|
502
|
+
const createRel =
|
|
503
|
+
source ||
|
|
504
|
+
(ide ? SKILL_ROOTS.find((r) => r.ide === ide)?.rel : null) ||
|
|
505
|
+
'.claude/skills';
|
|
506
|
+
skillDir = path.join(project.path, createRel, name);
|
|
118
507
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
119
508
|
}
|
|
120
509
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
121
510
|
fs.writeFileSync(skillPath, content, 'utf8');
|
|
122
|
-
res.json({ success: true, message: `Skill "${name}" updated successfully
|
|
123
|
-
} catch (err) {
|
|
511
|
+
res.json({ success: true, message: `Skill "${name}" updated successfully`, repo: String(repo) });
|
|
512
|
+
} catch (err) {
|
|
513
|
+
serverError(res, err);
|
|
514
|
+
}
|
|
124
515
|
});
|
|
125
516
|
|
|
126
517
|
skillsRouter.delete('/api/skills/:name', requireAuth, requireScope('files:write', 'ssss:write'), (req, res) => {
|
|
127
518
|
try {
|
|
128
519
|
const { name } = req.params;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
520
|
+
const repo = req.query.repo || req.query.project || '';
|
|
521
|
+
const source = req.query.source || '';
|
|
522
|
+
const ide = req.query.ide || '';
|
|
523
|
+
let skillDir;
|
|
524
|
+
try {
|
|
525
|
+
({ skillDir } = resolveSkillDir(String(repo), name, {
|
|
526
|
+
source: source || undefined,
|
|
527
|
+
ide: ide || undefined,
|
|
528
|
+
}));
|
|
529
|
+
} catch (e) {
|
|
530
|
+
return res.status(404).json({ error: e.message });
|
|
135
531
|
}
|
|
136
532
|
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
137
533
|
res.json({ success: true, message: `Skill "${name}" deleted successfully` });
|
|
138
|
-
} catch (err) {
|
|
534
|
+
} catch (err) {
|
|
535
|
+
serverError(res, err);
|
|
536
|
+
}
|
|
139
537
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { runSync } from '../../core/remote-vault-sync.mjs';
|
|
3
|
+
import config from '../../core/config.mjs';
|
|
4
|
+
import { requireAuth } from '../auth.mjs';
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
/** Generic remote-vault proposal decision proxy (optional feature). */
|
|
9
|
+
router.post('/api/sync/remote-vault/proposals/:id/decision', requireAuth, async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const { id } = req.params;
|
|
12
|
+
const { action, notes } = req.body;
|
|
13
|
+
|
|
14
|
+
const token = process.env[config.remoteVaultSync.tokenRef];
|
|
15
|
+
if (!token) {
|
|
16
|
+
return res.status(500).json({ error: 'Missing remote vault sync token' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!config.remoteVaultSync.baseUrl) {
|
|
20
|
+
return res.status(500).json({ error: 'TR_REMOTE_VAULT_URL not configured' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const baseUrl = config.remoteVaultSync.baseUrl.replace(/\/+$/, '');
|
|
24
|
+
|
|
25
|
+
const remoteRes = await fetch(`${baseUrl}/api/admin/proposals/${id}/decision`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ action, notes }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!remoteRes.ok) {
|
|
35
|
+
const err = await remoteRes.text();
|
|
36
|
+
return res.status(remoteRes.status).json({ error: `Remote rejected decision: ${err}` });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await remoteRes.json();
|
|
40
|
+
await runSync();
|
|
41
|
+
|
|
42
|
+
res.json({ success: true, remote_response: data });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
res.status(500).json({ error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export default router;
|