total-recall-brain 3.9.0 → 3.11.0
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/bin/antigravity.mjs +62 -183
- package/package.json +6 -5
- package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
- package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/SKILL.md +3 -2
- package/scaffold/.agent/skills/total-recall/skills/{research → tr-research}/SKILL.md +2 -1
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/SKILL.md +3 -2
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/evals/evals.json +3 -3
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-add.sh +1 -1
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-delete.sh +1 -1
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/create-skill.sh +3 -3
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/find-skills.mjs +25 -1
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/install-skill.mjs +117 -0
- package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/scan-skill.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/SKILL.md +120 -19
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +124 -0
- package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/references/ssss-spec.md +474 -43
- package/src/cli/connect.mjs +6 -59
- package/src/cli/ingest-openwiki.mjs +122 -0
- package/src/cli/ingest.mjs +9 -0
- package/src/cli/init.mjs +42 -2
- package/src/cli/skill-projection.mjs +214 -0
- package/src/cli/skill.mjs +44 -18
- package/src/core/daemon-loop.mjs +13 -2
- package/src/core/operation-validator.mjs +85 -9
- package/src/core/research.mjs +47 -9
- package/src/core/runtime.mjs +14 -7
- package/src/core/schema.mjs +85 -13
- package/src/server/api.mjs +2 -2
- package/src/server/rest.mjs +3 -3
- package/src/server/routes/skills.mjs +25 -4
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/CHANGELOG.md +0 -9
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/agents.yml +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/evals/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/antigravity.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/claude.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/codex.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/gemini.txt +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/scripts/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/subagents/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/assets/skill-template/SKILL.md +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/learnings/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/references/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/enforce-skill-optimization.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/skill-watcher.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/subagents/audit-all-skills.md +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/evals/evals.json +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/scripts/validate-schema.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/subagents/audit-ssss-node.md +0 -0
package/src/cli/connect.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { agentDir as configAgentDir, brainDir as configBrainDir, xdgConfigHome, trBrain, trPat } from '../core/config.mjs';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { resolveAgentDir, getBothBrains, detectProjectBrain } from './agent-dir.mjs';
|
|
7
|
+
import { discoverRepoSkills, projectSkillsAsCommands } from './skill-projection.mjs';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
// Resolve templates dir relative to this file (src/cli/ → ../../templates/)
|
|
@@ -78,7 +79,11 @@ const CLIENTS = {
|
|
|
78
79
|
label: 'Gemini',
|
|
79
80
|
mode: 'symlink',
|
|
80
81
|
target: '.agents/rules/GEMINI.md',
|
|
81
|
-
writeSlashCommands: true
|
|
82
|
+
writeSlashCommands: true,
|
|
83
|
+
// Gemini CLI reads project workspace skills from <project>/.agents/skills/
|
|
84
|
+
// (the same Agent Skills location Antigravity uses) and turns each into a
|
|
85
|
+
// /name slash command.
|
|
86
|
+
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
|
|
82
87
|
},
|
|
83
88
|
aider: {
|
|
84
89
|
label: 'Aider',
|
|
@@ -628,64 +633,6 @@ Instructions:
|
|
|
628
633
|
}
|
|
629
634
|
|
|
630
635
|
|
|
631
|
-
/**
|
|
632
|
-
* Discover repo skills to expose as slash commands.
|
|
633
|
-
* Every <agentDir>/skills/<name>/ directory containing a SKILL.md is a skill.
|
|
634
|
-
* The SKILL.md (Agent Skills standard) is what each IDE turns into /<name>.
|
|
635
|
-
*/
|
|
636
|
-
function discoverRepoSkills(agentDir) {
|
|
637
|
-
const skillsRoot = path.join(agentDir, 'skills');
|
|
638
|
-
if (!fs.existsSync(skillsRoot)) return [];
|
|
639
|
-
let entries;
|
|
640
|
-
try {
|
|
641
|
-
entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
|
642
|
-
} catch {
|
|
643
|
-
return [];
|
|
644
|
-
}
|
|
645
|
-
const skills = [];
|
|
646
|
-
for (const entry of entries) {
|
|
647
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
648
|
-
const skillDir = path.join(skillsRoot, entry.name);
|
|
649
|
-
if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) continue;
|
|
650
|
-
skills.push({ name: entry.name, skillDir });
|
|
651
|
-
}
|
|
652
|
-
return skills;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Symlink each discovered skill dir into an IDE's Agent-Skills directory.
|
|
657
|
-
* Idempotent and self-healing: a symlink that is broken or points at a stale
|
|
658
|
-
* location (e.g. after the repo moved) is refreshed even without --force; a
|
|
659
|
-
* real (non-symlink) entry is only replaced with --force.
|
|
660
|
-
*/
|
|
661
|
-
function projectSkillsAsCommands(destDir, skills, opts) {
|
|
662
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
663
|
-
const written = [];
|
|
664
|
-
for (const skill of skills) {
|
|
665
|
-
const linkPath = path.join(destDir, skill.name);
|
|
666
|
-
const wantTarget = path.resolve(skill.skillDir);
|
|
667
|
-
if (pathExists(linkPath)) {
|
|
668
|
-
const stat = fs.lstatSync(linkPath);
|
|
669
|
-
if (stat.isSymbolicLink()) {
|
|
670
|
-
const current = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
|
|
671
|
-
if (current === wantTarget && fs.existsSync(linkPath) && !opts.force) {
|
|
672
|
-
written.push({ name: skill.name, action: 'exists' });
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
fs.unlinkSync(linkPath); // stale, broken, or --force → refresh
|
|
676
|
-
} else if (!opts.force) {
|
|
677
|
-
written.push({ name: skill.name, action: 'skipped' }); // real file/dir in the way
|
|
678
|
-
continue;
|
|
679
|
-
} else {
|
|
680
|
-
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
fs.symlinkSync(skill.skillDir, linkPath);
|
|
684
|
-
written.push({ name: skill.name, action: 'linked' });
|
|
685
|
-
}
|
|
686
|
-
return written;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
636
|
export default async function connect(args) {
|
|
690
637
|
const opts = parseArgs(args);
|
|
691
638
|
if (opts.help || !opts.client) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
function printHelp() {
|
|
6
|
+
console.log(`
|
|
7
|
+
Usage:
|
|
8
|
+
total-recall ingest openwiki <openwiki-path> [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--dry-run Preview import without writing
|
|
12
|
+
--global Target the global brain
|
|
13
|
+
--project Target the project brain (default)
|
|
14
|
+
--help, -h Show this help
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runOpenWikiIngest(args) {
|
|
19
|
+
let openwikiPath = null;
|
|
20
|
+
let dryRun = false;
|
|
21
|
+
let layerFlag = '--project';
|
|
22
|
+
let help = false;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
const arg = args[i];
|
|
26
|
+
switch (arg) {
|
|
27
|
+
case '--dry-run': dryRun = true; break;
|
|
28
|
+
case '--global': layerFlag = '--global'; break;
|
|
29
|
+
case '--project': layerFlag = '--project'; break;
|
|
30
|
+
case '--help': case '-h': help = true; break;
|
|
31
|
+
default:
|
|
32
|
+
if (!arg.startsWith('-') && !openwikiPath) {
|
|
33
|
+
openwikiPath = arg;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (help || !openwikiPath) {
|
|
39
|
+
printHelp();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(openwikiPath)) {
|
|
44
|
+
console.error(`❌ Error: OpenWiki directory not found at ${openwikiPath}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`\n 📥 Importing LangChain OpenWiki from: ${openwikiPath}`);
|
|
49
|
+
console.log(` 🧠 Target Brain: ${layerFlag.replace('--', '')}`);
|
|
50
|
+
if (dryRun) console.log(' ⚠️ DRY RUN: No actual writes will be committed.\n');
|
|
51
|
+
|
|
52
|
+
// Find all markdown files recursively
|
|
53
|
+
const mdFiles = [];
|
|
54
|
+
function scanDir(dir) {
|
|
55
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
scanDir(fullPath);
|
|
60
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
61
|
+
mdFiles.push(fullPath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
scanDir(openwikiPath);
|
|
67
|
+
|
|
68
|
+
let imported = 0;
|
|
69
|
+
let errors = 0;
|
|
70
|
+
|
|
71
|
+
for (const file of mdFiles) {
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
74
|
+
|
|
75
|
+
// Try to extract title from the first heading, else use filename
|
|
76
|
+
let title = path.basename(file, '.md').replace(/[-_]/g, ' ');
|
|
77
|
+
const titleMatch = content.match(/^#\\s+(.+)$/m);
|
|
78
|
+
if (titleMatch) {
|
|
79
|
+
title = titleMatch[1].trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const slug = 'openwiki-' + path.basename(file, '.md').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
83
|
+
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
console.log(` 🟢 [DRY RUN] Would import: ${slug} ("${title}")`);
|
|
86
|
+
imported++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const cliArgs = [
|
|
91
|
+
process.argv[1],
|
|
92
|
+
'remember', 'fact',
|
|
93
|
+
content,
|
|
94
|
+
'--title', title,
|
|
95
|
+
'--slug', slug,
|
|
96
|
+
'--tags', 'openwiki,architecture,auto-generated',
|
|
97
|
+
layerFlag
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const result = spawnSync(process.argv[0], cliArgs, { encoding: 'utf8' });
|
|
101
|
+
|
|
102
|
+
if (result.status === 0) {
|
|
103
|
+
console.log(` 🟢 Imported: ${slug}`);
|
|
104
|
+
imported++;
|
|
105
|
+
} else {
|
|
106
|
+
console.error(` 🔴 Error importing ${slug}:\\n${result.stderr}`);
|
|
107
|
+
errors++;
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(` 🔴 Exception on ${file}: ${err.message}`);
|
|
111
|
+
errors++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`\n Import Summary:`);
|
|
116
|
+
console.log(` 🟢 Imported: ${imported} files`);
|
|
117
|
+
console.log(` 🔴 Errors: ${errors} files`);
|
|
118
|
+
|
|
119
|
+
if (!dryRun && imported > 0) {
|
|
120
|
+
console.log(`\n ✅ OpenWiki successfully integrated into Total Recall.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/cli/ingest.mjs
CHANGED
|
@@ -40,6 +40,8 @@ function printHelp() {
|
|
|
40
40
|
Use 'total-recall ingest google-takeout --help' for details
|
|
41
41
|
okf <path> Ingest an Open Knowledge Format (OKF) bundle directory
|
|
42
42
|
Use 'total-recall ingest okf --help' for details
|
|
43
|
+
openwiki <path> Ingest a LangChain OpenWiki directory
|
|
44
|
+
Use 'total-recall ingest openwiki --help' for details
|
|
43
45
|
|
|
44
46
|
IDE Session Ingest:
|
|
45
47
|
Scans known local directories for conversation history from:
|
|
@@ -55,6 +57,7 @@ function printHelp() {
|
|
|
55
57
|
total-recall ingest [options]
|
|
56
58
|
total-recall ingest google-takeout <path> [options]
|
|
57
59
|
total-recall ingest okf <path> [options]
|
|
60
|
+
total-recall ingest openwiki <path> [options]
|
|
58
61
|
|
|
59
62
|
Options:
|
|
60
63
|
--sources <list> Comma-separated source filter (e.g. claude-code,codex)
|
|
@@ -77,6 +80,12 @@ export default async function ingest(args) {
|
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
if (args[0] === 'openwiki') {
|
|
84
|
+
const { runOpenWikiIngest } = await import('./ingest-openwiki.mjs');
|
|
85
|
+
await runOpenWikiIngest(args.slice(1));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
const opts = parseArgs(args);
|
|
81
90
|
if (opts.help) { printHelp(); return; }
|
|
82
91
|
|
package/src/cli/init.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import path from 'node:path';
|
|
|
30
30
|
import os from 'node:os';
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { exec, spawn, spawnSync } from 'node:child_process';
|
|
33
|
+
import { projectSkillsForScope, detectActiveSkillTargets } from './skill-projection.mjs';
|
|
33
34
|
|
|
34
35
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
35
36
|
const ROOT = path.join(__dirname, '..', '..');
|
|
@@ -83,7 +84,8 @@ function parseArgs(args) {
|
|
|
83
84
|
domain: null,
|
|
84
85
|
tunnelName: null,
|
|
85
86
|
tunnelCredentials: null,
|
|
86
|
-
yes: false
|
|
87
|
+
yes: false,
|
|
88
|
+
force: false
|
|
87
89
|
};
|
|
88
90
|
for (let i = 0; i < args.length; i++) {
|
|
89
91
|
const arg = args[i];
|
|
@@ -97,6 +99,7 @@ function parseArgs(args) {
|
|
|
97
99
|
else if (arg === '--tunnel-name') opts.tunnelName = args[++i];
|
|
98
100
|
else if (arg === '--tunnel-credentials') opts.tunnelCredentials = args[++i];
|
|
99
101
|
else if (arg === '--yes' || arg === '-y') opts.yes = true;
|
|
102
|
+
else if (arg === '--force') opts.force = true;
|
|
100
103
|
}
|
|
101
104
|
return opts;
|
|
102
105
|
}
|
|
@@ -474,7 +477,7 @@ export default async function init(args) {
|
|
|
474
477
|
}
|
|
475
478
|
|
|
476
479
|
// ── Symlink nested brain skills to top-level .agent/skills/ ──
|
|
477
|
-
// Skills inside the brain (e.g. .agent/skills/total-recall/skills/ssss/) are
|
|
480
|
+
// Skills inside the brain (e.g. .agent/skills/total-recall/skills/tr-ssss/) are
|
|
478
481
|
// invisible to IDEs because they only scan .agent/skills/<name>/ one level
|
|
479
482
|
// deep. Create symlinks so they appear as slash commands.
|
|
480
483
|
const nestedSkillsDir = path.join(brainDir, 'skills');
|
|
@@ -498,6 +501,43 @@ export default async function init(args) {
|
|
|
498
501
|
}
|
|
499
502
|
}
|
|
500
503
|
|
|
504
|
+
// ── Step 3.7: Project skills as slash commands into the IDEs in use ──
|
|
505
|
+
// Scope matches the brain: `init --project` projects PROJECT skills into the
|
|
506
|
+
// repo's IDE skill dirs for IDEs actually used here (.claude/, .agents/, or
|
|
507
|
+
// the current process); `init` projects GLOBAL skills into installed IDEs'
|
|
508
|
+
// global skill dirs. Codex (global-only) is opt-in for project skills.
|
|
509
|
+
const skillScope = isProject ? 'project' : 'global';
|
|
510
|
+
logStep('3.7/4', `Projecting ${skillScope} skills as slash commands (IDEs in use)`);
|
|
511
|
+
if (opts.dryRun) {
|
|
512
|
+
const targets = detectActiveSkillTargets({ scope: skillScope, cwd });
|
|
513
|
+
for (const t of targets.filter(t => t.supported)) {
|
|
514
|
+
const destLabel = t.destDir ? path.relative(cwd, t.destDir) || t.destDir : '(n/a)';
|
|
515
|
+
log(` ${t.active ? 'would wire' : 'available (opt-in)'}: ${t.label} → ${destLabel}/`);
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
try {
|
|
519
|
+
const { skills, wired, available } = projectSkillsForScope({
|
|
520
|
+
scope: skillScope, cwd, agentDir, opts: { force: opts.force }
|
|
521
|
+
});
|
|
522
|
+
if (skills.length === 0) {
|
|
523
|
+
log(' No skills found yet — author one under .agent/skills/<name>/SKILL.md');
|
|
524
|
+
} else if (wired.length === 0) {
|
|
525
|
+
log(` No in-use IDE detected for ${skillScope} skills — run \`npx total-recall connect <ide>\` to wire them.`);
|
|
526
|
+
} else {
|
|
527
|
+
for (const t of wired) {
|
|
528
|
+
const n = t.results.filter(r => ['linked', 'exists', 'source'].includes(r.action)).length;
|
|
529
|
+
logOk(`${t.label}: ${n} skill(s) → ${path.relative(cwd, t.destDir) || t.destDir}/`);
|
|
530
|
+
}
|
|
531
|
+
log(` Slash commands: ${skills.map(s => '/' + s.name).join(', ')}`);
|
|
532
|
+
if (available.length > 0) {
|
|
533
|
+
log(` Also available (opt-in): ${available.map(t => t.clients.join('/')).join(', ')} — \`npx total-recall connect <ide>\``);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} catch (err) {
|
|
537
|
+
logWarn(`Could not project skills into IDEs: ${err.message}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
501
541
|
// ── Step 4: Run compile to inject memory block into existing IDE files ──
|
|
502
542
|
logStep('4/4', 'Compiling vault and injecting into IDE instruction files');
|
|
503
543
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope-aware skill → slash-command projection.
|
|
3
|
+
*
|
|
4
|
+
* A "repo skill" is any <skills-root>/<name>/SKILL.md directory (the Agent
|
|
5
|
+
* Skills standard). Each IDE turns such a directory into a /<name> slash
|
|
6
|
+
* command, but every IDE looks in a *different* place, and the right place
|
|
7
|
+
* depends on the *scope* of the skill:
|
|
8
|
+
*
|
|
9
|
+
* PROJECT skills (authored in <repo>/.agent/skills or <repo>/.agents/skills)
|
|
10
|
+
* → project into the *repo's* IDE skill dirs, and only for IDEs actually
|
|
11
|
+
* in use in this repo (project marker dir present, or current process is
|
|
12
|
+
* that IDE). They belong to this repo, not the whole machine.
|
|
13
|
+
*
|
|
14
|
+
* GLOBAL skills (authored in ~/.agent/skills)
|
|
15
|
+
* → project into each IDE's *global* skill dir, for IDEs installed on this
|
|
16
|
+
* machine. They are available in every repo.
|
|
17
|
+
*
|
|
18
|
+
* This mirrors the brain model: `init --project` ⇒ project skills; `init`
|
|
19
|
+
* (global) ⇒ global skills. Codex has no project-local skills dir (it only
|
|
20
|
+
* reads ~/.codex/skills globally), so it participates in GLOBAL projection only;
|
|
21
|
+
* pushing project skills to Codex is opt-in via `connect codex`.
|
|
22
|
+
*/
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import os from 'node:os';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* IDE skill descriptors. Each IDE declares how it is detected and where its
|
|
29
|
+
* skills live, per scope:
|
|
30
|
+
* project: { dir, markers, env } — repo-relative dest; detected by repo
|
|
31
|
+
* marker dirs or current-process env vars. null ⇒ no project skills.
|
|
32
|
+
* global: { dir, markers } — home-relative dest; detected by home
|
|
33
|
+
* marker dirs (i.e. the IDE is installed). null ⇒ no global skills.
|
|
34
|
+
*/
|
|
35
|
+
export const IDE_SKILLS = [
|
|
36
|
+
{
|
|
37
|
+
id: 'claude-code',
|
|
38
|
+
label: 'Claude Code',
|
|
39
|
+
clients: ['claude-code'],
|
|
40
|
+
project: { dir: ['.claude', 'skills'], markers: ['.claude'], env: ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'] },
|
|
41
|
+
global: { dir: ['.claude', 'skills'], markers: ['.claude'] }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'agents',
|
|
45
|
+
label: 'Antigravity / Gemini',
|
|
46
|
+
clients: ['antigravity', 'gemini'],
|
|
47
|
+
// Antigravity & Gemini both read <repo>/.agents/skills as the workspace
|
|
48
|
+
// Agent-Skills location. Their *global* surface is ~/.gemini/commands
|
|
49
|
+
// (markdown commands), wired by the slash-command writer, not here.
|
|
50
|
+
project: { dir: ['.agents', 'skills'], markers: ['.agents', '.gemini'], env: ['ANTIGRAVITY', 'GEMINI_CLI'] },
|
|
51
|
+
global: null
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'codex',
|
|
55
|
+
label: 'Codex',
|
|
56
|
+
clients: ['codex'],
|
|
57
|
+
// Codex only discovers skills globally under ~/.codex/skills. There is no
|
|
58
|
+
// project-local dir, so project skills are opt-in (namespaced) via connect.
|
|
59
|
+
project: null,
|
|
60
|
+
global: { dir: ['.codex', 'skills'], markers: ['.codex'] }
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function pathExists(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
fs.lstatSync(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Skill source roots, in priority order. The canonical Total Recall location
|
|
75
|
+
* (.agent/skills, singular) wins, but we also scan the Antigravity-native
|
|
76
|
+
* .agents/skills (plural) so a skill authored in either place is discovered.
|
|
77
|
+
*/
|
|
78
|
+
export function skillSourceRoots(agentDir) {
|
|
79
|
+
const roots = [path.join(agentDir, 'skills')];
|
|
80
|
+
const parent = path.dirname(agentDir);
|
|
81
|
+
if (path.basename(agentDir) === '.agent') {
|
|
82
|
+
roots.push(path.join(parent, '.agents', 'skills'));
|
|
83
|
+
}
|
|
84
|
+
return roots;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Discover repo skills to expose as slash commands.
|
|
89
|
+
* Every <root>/<name>/ directory containing a SKILL.md is a skill. When the
|
|
90
|
+
* same name appears in more than one source root, the canonical root wins.
|
|
91
|
+
*/
|
|
92
|
+
export function discoverRepoSkills(agentDir) {
|
|
93
|
+
const seen = new Map();
|
|
94
|
+
for (const skillsRoot of skillSourceRoots(agentDir)) {
|
|
95
|
+
if (!fs.existsSync(skillsRoot)) continue;
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
104
|
+
if (seen.has(entry.name)) continue;
|
|
105
|
+
const skillDir = path.join(skillsRoot, entry.name);
|
|
106
|
+
if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) continue;
|
|
107
|
+
seen.set(entry.name, { name: entry.name, skillDir });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...seen.values()];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Symlink each discovered skill dir into one IDE's Agent-Skills directory.
|
|
115
|
+
* Idempotent, self-healing, and strictly additive: a symlink that is broken or
|
|
116
|
+
* stale is refreshed even without --force; a *real* (non-symlink) entry is
|
|
117
|
+
* never clobbered unless --force (so a user's own skill always wins a name
|
|
118
|
+
* collision); a skill whose source already *is* the destination is left alone.
|
|
119
|
+
*/
|
|
120
|
+
export function projectSkillsAsCommands(destDir, skills, opts = {}) {
|
|
121
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
122
|
+
const written = [];
|
|
123
|
+
for (const skill of skills) {
|
|
124
|
+
const linkPath = path.join(destDir, skill.name);
|
|
125
|
+
const wantTarget = path.resolve(skill.skillDir);
|
|
126
|
+
|
|
127
|
+
if (path.resolve(linkPath) === wantTarget) {
|
|
128
|
+
written.push({ name: skill.name, action: 'source' });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (pathExists(linkPath)) {
|
|
133
|
+
const stat = fs.lstatSync(linkPath);
|
|
134
|
+
if (stat.isSymbolicLink()) {
|
|
135
|
+
const current = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
|
|
136
|
+
if (current === wantTarget && fs.existsSync(linkPath) && !opts.force) {
|
|
137
|
+
written.push({ name: skill.name, action: 'exists' });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
fs.unlinkSync(linkPath); // stale, broken, or --force → refresh
|
|
141
|
+
} else if (!opts.force) {
|
|
142
|
+
written.push({ name: skill.name, action: 'skipped' }); // user's real file/dir wins
|
|
143
|
+
continue;
|
|
144
|
+
} else {
|
|
145
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
fs.symlinkSync(skill.skillDir, linkPath);
|
|
149
|
+
written.push({ name: skill.name, action: 'linked' });
|
|
150
|
+
}
|
|
151
|
+
return written;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Is an IDE target active for the given scope?
|
|
156
|
+
* project → repo has a marker dir, or the current process is this IDE (env)
|
|
157
|
+
* global → the IDE is installed (home marker dir present)
|
|
158
|
+
*/
|
|
159
|
+
function isTargetActive(target, scope, cwd, env) {
|
|
160
|
+
const spec = target[scope];
|
|
161
|
+
if (!spec) return false;
|
|
162
|
+
if (scope === 'project') {
|
|
163
|
+
if ((spec.markers || []).some(m => pathExists(path.join(cwd, m)))) return true;
|
|
164
|
+
if ((spec.env || []).some(v => env[v] != null && env[v] !== '')) return true;
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
// global
|
|
168
|
+
return (spec.markers || []).some(m => pathExists(path.join(os.homedir(), m)));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve which IDE targets are active (and where they project to) for a scope.
|
|
173
|
+
* Returns every target annotated with { active, destDir } so callers can both
|
|
174
|
+
* wire the active ones and advertise the inactive ones as opt-in.
|
|
175
|
+
*/
|
|
176
|
+
export function detectActiveSkillTargets({ scope, cwd = process.cwd(), env = process.env }) {
|
|
177
|
+
return IDE_SKILLS.map(target => {
|
|
178
|
+
const spec = target[scope];
|
|
179
|
+
const active = isTargetActive(target, scope, cwd, env);
|
|
180
|
+
let destDir = null;
|
|
181
|
+
if (spec) {
|
|
182
|
+
destDir = scope === 'global'
|
|
183
|
+
? path.join(os.homedir(), ...spec.dir)
|
|
184
|
+
: path.join(cwd, ...spec.dir);
|
|
185
|
+
}
|
|
186
|
+
return { id: target.id, label: target.label, clients: target.clients, scope, supported: !!spec, active, destDir };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Project discovered skills into every *active* IDE skill dir for one scope.
|
|
192
|
+
* Used by `init`: project skills on `init --project`, global skills on `init`.
|
|
193
|
+
*
|
|
194
|
+
* @returns {{ scope, skills, wired: Array, available: Array }}
|
|
195
|
+
* wired — targets we projected into (active + supported)
|
|
196
|
+
* available — supported-but-inactive targets, surfaced as opt-in `connect`
|
|
197
|
+
*/
|
|
198
|
+
export function projectSkillsForScope({ scope, cwd = process.cwd(), agentDir, opts = {}, env = process.env }) {
|
|
199
|
+
const skills = discoverRepoSkills(agentDir);
|
|
200
|
+
const targets = detectActiveSkillTargets({ scope, cwd, env });
|
|
201
|
+
const wired = [];
|
|
202
|
+
const available = [];
|
|
203
|
+
|
|
204
|
+
for (const target of targets) {
|
|
205
|
+
if (!target.supported) continue;
|
|
206
|
+
if (target.active && skills.length > 0) {
|
|
207
|
+
const results = projectSkillsAsCommands(target.destDir, skills, opts);
|
|
208
|
+
wired.push({ ...target, results });
|
|
209
|
+
} else if (!target.active) {
|
|
210
|
+
available.push(target);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { scope, skills, wired, available };
|
|
214
|
+
}
|
package/src/cli/skill.mjs
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { resolveAgentDir, parseLayerFlag } from './agent-dir.mjs';
|
|
5
6
|
|
|
6
7
|
// Helper to resolve agent directories dynamically
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const ROOT = path.resolve(__dirname, '../../');
|
|
9
|
-
const agentDir = path.join(ROOT, '.agent');
|
|
10
|
-
const skillsDir = path.join(agentDir, 'skills');
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const
|
|
11
|
+
export function resolveSkillScriptPath(scriptName, { agentDir = resolveAgentDir(), root = ROOT } = {}) {
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.join(agentDir, 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
|
|
14
|
+
path.join(agentDir, 'skills', 'total-recall', 'skills', 'skill', 'scripts', scriptName),
|
|
15
|
+
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
|
+
];
|
|
18
|
+
const found = candidates.find(candidate => fs.existsSync(candidate));
|
|
19
|
+
if (!found) {
|
|
20
|
+
throw new Error(`Could not find bundled skill helper "${scriptName}". Checked: ${candidates.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function importSkillScript(scriptName, options) {
|
|
26
|
+
return import(pathToFileURL(resolveSkillScriptPath(scriptName, options)).href);
|
|
27
|
+
}
|
|
14
28
|
|
|
15
29
|
function printHelp() {
|
|
16
30
|
console.log(`
|
|
@@ -28,6 +42,8 @@ function printHelp() {
|
|
|
28
42
|
remove <name> Safely delete a skill package and hot-recompile shims (alias: rm)
|
|
29
43
|
|
|
30
44
|
Options:
|
|
45
|
+
--global Target the global brain (~/.agent)
|
|
46
|
+
--project Target the current repo's project brain
|
|
31
47
|
--help, -h Show this help menu
|
|
32
48
|
|
|
33
49
|
Examples:
|
|
@@ -40,15 +56,24 @@ function printHelp() {
|
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
export default async function skillCli(args) {
|
|
43
|
-
|
|
59
|
+
const { layer, remainingArgs } = parseLayerFlag(args);
|
|
60
|
+
if (remainingArgs.length === 0 || remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
|
|
44
61
|
printHelp();
|
|
45
62
|
return;
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
let agentDir;
|
|
66
|
+
try {
|
|
67
|
+
agentDir = resolveAgentDir(layer);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`❌ ${err.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const skillsDir = path.join(agentDir, 'skills');
|
|
73
|
+
const command = remainingArgs[0];
|
|
49
74
|
|
|
50
75
|
if (command === 'find') {
|
|
51
|
-
const query =
|
|
76
|
+
const query = remainingArgs[1];
|
|
52
77
|
if (!query) {
|
|
53
78
|
console.error('❌ You must provide a search query.');
|
|
54
79
|
console.error('Usage: npx total-recall skill find <query>');
|
|
@@ -56,7 +81,7 @@ export default async function skillCli(args) {
|
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
try {
|
|
59
|
-
const { searchAndSort } = await
|
|
84
|
+
const { searchAndSort } = await importSkillScript('find-skills.mjs', { agentDir });
|
|
60
85
|
const results = searchAndSort(query);
|
|
61
86
|
|
|
62
87
|
if (results.length === 0) {
|
|
@@ -100,7 +125,7 @@ export default async function skillCli(args) {
|
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
else if (command === 'install' || command === 'add' || command === 'load') {
|
|
103
|
-
const pkg =
|
|
128
|
+
const pkg = remainingArgs[1];
|
|
104
129
|
if (!pkg) {
|
|
105
130
|
console.error('❌ You must provide a skill package to load.');
|
|
106
131
|
console.error('Usage: npx total-recall skill load <owner/repo@skill>');
|
|
@@ -108,8 +133,8 @@ export default async function skillCli(args) {
|
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
try {
|
|
111
|
-
const { installSkill } = await
|
|
112
|
-
const res = installSkill(pkg);
|
|
136
|
+
const { installSkill } = await importSkillScript('install-skill.mjs', { agentDir });
|
|
137
|
+
const res = installSkill(pkg, { cwd: process.cwd(), agentDir });
|
|
113
138
|
if (!res.success) {
|
|
114
139
|
process.exit(1);
|
|
115
140
|
}
|
|
@@ -120,7 +145,7 @@ export default async function skillCli(args) {
|
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
else if (command === 'scan') {
|
|
123
|
-
const target =
|
|
148
|
+
const target = remainingArgs[1];
|
|
124
149
|
if (!target) {
|
|
125
150
|
console.error('❌ You must specify a local skill directory to scan.');
|
|
126
151
|
console.error('Usage: npx total-recall skill scan <skill-name>');
|
|
@@ -130,7 +155,7 @@ export default async function skillCli(args) {
|
|
|
130
155
|
const targetDir = path.resolve(skillsDir, target);
|
|
131
156
|
|
|
132
157
|
try {
|
|
133
|
-
const { runScan } = await
|
|
158
|
+
const { runScan } = await importSkillScript('scan-skill.mjs', { agentDir });
|
|
134
159
|
const result = runScan(targetDir);
|
|
135
160
|
process.exit(result.success ? 0 : 1);
|
|
136
161
|
} catch (err) {
|
|
@@ -176,7 +201,7 @@ export default async function skillCli(args) {
|
|
|
176
201
|
}
|
|
177
202
|
|
|
178
203
|
else if (command === 'remove' || command === 'rm') {
|
|
179
|
-
const target =
|
|
204
|
+
const target = remainingArgs[1];
|
|
180
205
|
if (!target) {
|
|
181
206
|
console.error('❌ You must specify a skill name to remove.');
|
|
182
207
|
console.error('Usage: npx total-recall skill remove <skill-name>');
|
|
@@ -197,7 +222,8 @@ export default async function skillCli(args) {
|
|
|
197
222
|
console.log('🔄 Hot-recompiling active shims to reflect changes...');
|
|
198
223
|
const child = spawnSync('npx', ['total-recall', 'compile'], {
|
|
199
224
|
encoding: 'utf8',
|
|
200
|
-
cwd:
|
|
225
|
+
cwd: process.cwd(),
|
|
226
|
+
env: { ...process.env, AGENT_DIR: agentDir },
|
|
201
227
|
});
|
|
202
228
|
|
|
203
229
|
if (child.error || child.status !== 0) {
|
|
@@ -208,7 +234,7 @@ export default async function skillCli(args) {
|
|
|
208
234
|
}
|
|
209
235
|
|
|
210
236
|
else if (command === 'create' || command === 'new') {
|
|
211
|
-
const target =
|
|
237
|
+
const target = remainingArgs[1];
|
|
212
238
|
if (!target) {
|
|
213
239
|
console.error('❌ You must specify a skill name to create.');
|
|
214
240
|
console.error('Usage: npx total-recall skill create <skill-name>');
|
|
@@ -285,7 +311,7 @@ Provide a high-level explanation of the skill's capabilities and context.
|
|
|
285
311
|
}
|
|
286
312
|
|
|
287
313
|
else if (command === 'edit') {
|
|
288
|
-
const target =
|
|
314
|
+
const target = remainingArgs[1];
|
|
289
315
|
if (!target) {
|
|
290
316
|
console.error('❌ You must specify a skill name to edit.');
|
|
291
317
|
console.error('Usage: npx total-recall skill edit <skill-name>');
|