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.
Files changed (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /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'), (req, res) => {
358
+ skillsRouter.get('/api/skills', requireAuth, requireScope('files:read', 'ssss:read'), (_req, res) => {
37
359
  try {
38
- if (!fs.existsSync(SKILLS_DIR)) {
39
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
40
- }
41
- const skills = fs.readdirSync(SKILLS_DIR).map(dir => {
42
- const dirPath = path.join(SKILLS_DIR, dir);
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) { serverError(res, 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) { serverError(res, 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
- if (name.includes('..') || name.includes('/') || name.includes('\\')) {
95
- return res.status(400).json({ error: 'Invalid skill name' });
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(SKILLS_DIR, name, 'SKILL.md');
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: `Skill "${name}" not found` });
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({ name, content });
103
- } catch (err) { serverError(res, err); }
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
- if (name.includes('..') || name.includes('/') || name.includes('\\')) {
111
- return res.status(400).json({ error: 'Invalid skill name' });
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
- const skillDir = path.join(SKILLS_DIR, name);
117
- if (!fs.existsSync(skillDir)) {
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) { serverError(res, 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
- if (name.includes('..') || name.includes('/') || name.includes('\\')) {
130
- return res.status(400).json({ error: 'Invalid skill name' });
131
- }
132
- const skillDir = path.join(SKILLS_DIR, name);
133
- if (!fs.existsSync(skillDir)) {
134
- return res.status(404).json({ error: `Skill "${name}" not found` });
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) { serverError(res, 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;