total-recall-brain 3.8.1 → 3.9.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/package.json +1 -1
- package/src/cli/connect.mjs +99 -4
package/package.json
CHANGED
package/src/cli/connect.mjs
CHANGED
|
@@ -52,18 +52,27 @@ const CLIENTS = {
|
|
|
52
52
|
label: 'Claude Code',
|
|
53
53
|
mode: 'symlink',
|
|
54
54
|
target: 'CLAUDE.md',
|
|
55
|
-
writeSlashCommands: true
|
|
55
|
+
writeSlashCommands: true,
|
|
56
|
+
// Project-scoped: repo skills are repo-specific. Claude Code discovers
|
|
57
|
+
// these as native skills / slash commands under <project>/.claude/skills/.
|
|
58
|
+
skillsProjection: { scope: 'project', dir: path.join('.claude', 'skills') }
|
|
56
59
|
},
|
|
57
60
|
codex: {
|
|
58
61
|
label: 'Codex',
|
|
59
62
|
mode: 'symlink',
|
|
60
|
-
target: 'AGENTS.md'
|
|
63
|
+
target: 'AGENTS.md',
|
|
64
|
+
// Codex only discovers skills globally under $CODEX_HOME/skills (~/.codex/skills).
|
|
65
|
+
// There is no project-local skills dir, so this projection is global by necessity.
|
|
66
|
+
skillsProjection: { scope: 'home', dir: path.join('.codex', 'skills') }
|
|
61
67
|
},
|
|
62
68
|
antigravity: {
|
|
63
69
|
label: 'Antigravity',
|
|
64
70
|
mode: 'symlink',
|
|
65
71
|
target: '.agents/rules/AGENTS.md',
|
|
66
|
-
writeSlashCommands: true
|
|
72
|
+
writeSlashCommands: true,
|
|
73
|
+
// Antigravity CLI (the Agent Skills standard) reads project workspace skills
|
|
74
|
+
// from <project>/.agents/skills/ and turns each into a /name slash command.
|
|
75
|
+
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
|
|
67
76
|
},
|
|
68
77
|
gemini: {
|
|
69
78
|
label: 'Gemini',
|
|
@@ -293,7 +302,12 @@ function writeSymlinkProjection(cwd, preset, opts) {
|
|
|
293
302
|
fs.rmSync(targetPath, { force: true, recursive: true });
|
|
294
303
|
}
|
|
295
304
|
}
|
|
296
|
-
|
|
305
|
+
// Ensure the parent dir exists (nested targets like .agents/rules/AGENTS.md)
|
|
306
|
+
// and point the symlink at INSTRUCTIONS.md using a path relative to the
|
|
307
|
+
// link's own directory, so nested projections don't dangle.
|
|
308
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
309
|
+
const linkTarget = path.relative(path.dirname(targetPath), instructionsPath);
|
|
310
|
+
fs.symlinkSync(linkTarget, targetPath);
|
|
297
311
|
return { targetPath, action: 'symlinked' };
|
|
298
312
|
}
|
|
299
313
|
|
|
@@ -614,6 +628,64 @@ Instructions:
|
|
|
614
628
|
}
|
|
615
629
|
|
|
616
630
|
|
|
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
|
+
|
|
617
689
|
export default async function connect(args) {
|
|
618
690
|
const opts = parseArgs(args);
|
|
619
691
|
if (opts.help || !opts.client) {
|
|
@@ -844,6 +916,29 @@ export default async function connect(args) {
|
|
|
844
916
|
}
|
|
845
917
|
}
|
|
846
918
|
|
|
919
|
+
// Project repo skills as native slash commands via the Agent Skills standard.
|
|
920
|
+
// Each <agentDir>/skills/<name>/SKILL.md becomes /<name> in the target IDE.
|
|
921
|
+
if (preset.skillsProjection) {
|
|
922
|
+
const skills = discoverRepoSkills(agentDir);
|
|
923
|
+
if (skills.length > 0) {
|
|
924
|
+
const destDir = preset.skillsProjection.scope === 'home'
|
|
925
|
+
? path.join(os.homedir(), preset.skillsProjection.dir)
|
|
926
|
+
: path.join(cwd, preset.skillsProjection.dir);
|
|
927
|
+
const skillResults = projectSkillsAsCommands(destDir, skills, opts);
|
|
928
|
+
result.skill_commands = skillResults;
|
|
929
|
+
const fresh = skillResults.filter(r => r.action === 'linked');
|
|
930
|
+
if (fresh.length > 0) {
|
|
931
|
+
const destLabel = preset.skillsProjection.scope === 'home'
|
|
932
|
+
? destDir
|
|
933
|
+
: (path.relative(cwd, destDir) || destDir);
|
|
934
|
+
result.notes.push(
|
|
935
|
+
` Repo skills projected as slash commands → ${destLabel}/:\n` +
|
|
936
|
+
fresh.map(r => ` /${r.name}`).join('\n')
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
847
942
|
// Record this client in the registry so `status` can report freshness
|
|
848
943
|
registerClient(brainDir, opts.client, preset, result.projection?.targetPath || null);
|
|
849
944
|
|