@babylonjs-toolkit/agent 1.1.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/README.md +344 -0
- package/bin/bt-agent.js +266 -0
- package/lib/doctor.js +59 -0
- package/lib/install.js +145 -0
- package/lib/manifest.js +46 -0
- package/lib/paths.js +66 -0
- package/lib/payload.js +56 -0
- package/lib/persona.js +177 -0
- package/lib/targets.js +105 -0
- package/package.json +43 -0
- package/persona.md +5 -0
- package/scripts/postinstall.js +49 -0
- package/skills/bt-atlas/SKILL.md +192 -0
- package/skills/bt-atlas/scripts/composite_skin.py +58 -0
- package/skills/bt-atlas/scripts/preview.py +70 -0
- package/skills/bt-atlas/scripts/requirements.txt +2 -0
- package/skills/bt-atlas/scripts/uv_island_mask.py +87 -0
- package/skills/bt-convert/SKILL.md +32 -0
- package/skills/bt-copycat/SKILL.md +184 -0
- package/skills/bt-design/SKILL.md +187 -0
- package/skills/bt-design/references/3d-hero-docs.md +976 -0
- package/skills/bt-design/references/3d-hero-scroll.md +269 -0
- package/skills/bt-design/templates/3d-hero-scroll/HeroScroll.tsx +167 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.css +268 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.d.ts +67 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.html +78 -0
- package/skills/bt-design/templates/3d-hero-scroll/hero-scroll.js +559 -0
- package/skills/bt-execute/SKILL.md +130 -0
- package/skills/bt-gauntlet/SKILL.md +335 -0
- package/skills/bt-hero/SKILL.md +158 -0
- package/skills/bt-landing/SKILL.md +126 -0
- package/skills/bt-plan/SKILL.md +172 -0
- package/skills/bt-prototype/SKILL.md +161 -0
- package/skills/bt-spec/SKILL.md +328 -0
package/lib/install.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { resolveTargets } = require('./targets');
|
|
7
|
+
const { listSkills, copyDir } = require('./payload');
|
|
8
|
+
const { applyPersona, removePersona, personaText } = require('./persona');
|
|
9
|
+
const manifest = require('./manifest');
|
|
10
|
+
const { manifestPath } = require('./paths');
|
|
11
|
+
|
|
12
|
+
const { version } = require('../package.json');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Copy every shipped skill into every target skills directory, refresh the Agent
|
|
16
|
+
* Persona in every target instruction file, and record the result.
|
|
17
|
+
*
|
|
18
|
+
* Each skill folder is replaced wholesale (delete-then-copy) so files removed
|
|
19
|
+
* upstream do not linger, but only folder names this package ships are ever
|
|
20
|
+
* deleted — a user's own `bt-*` skill is untouched.
|
|
21
|
+
*/
|
|
22
|
+
function install(opts = {}) {
|
|
23
|
+
const {
|
|
24
|
+
mode = 'global',
|
|
25
|
+
projectRoot = process.cwd(),
|
|
26
|
+
ids = null,
|
|
27
|
+
legacyCodex = false,
|
|
28
|
+
persona = true,
|
|
29
|
+
skills: installSkills = true,
|
|
30
|
+
migrate = true,
|
|
31
|
+
dryRun = false,
|
|
32
|
+
prune = true,
|
|
33
|
+
} = opts;
|
|
34
|
+
|
|
35
|
+
if (!installSkills && !persona) {
|
|
36
|
+
throw new Error('--persona-only and --no-persona cannot be combined — nothing would be installed.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resolved = resolveTargets({ mode, projectRoot, ids, legacyCodex });
|
|
40
|
+
const skills = installSkills ? listSkills() : [];
|
|
41
|
+
const previous = manifest.read(mode, projectRoot);
|
|
42
|
+
|
|
43
|
+
const report = {
|
|
44
|
+
version,
|
|
45
|
+
mode,
|
|
46
|
+
dryRun,
|
|
47
|
+
skillsInstalled: installSkills,
|
|
48
|
+
skills: skills.map((s) => s.name),
|
|
49
|
+
skillDirs: [],
|
|
50
|
+
persona: [],
|
|
51
|
+
pruned: [],
|
|
52
|
+
manifest: manifestPath(mode, projectRoot),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const { dir, targets } of installSkills ? resolved.skillDirs : []) {
|
|
56
|
+
if (!dryRun) fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
for (const skill of skills) {
|
|
59
|
+
const dest = path.join(dir, skill.name);
|
|
60
|
+
if (!dryRun) {
|
|
61
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
62
|
+
copyDir(skill.src, dest);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remove skills we installed previously that this version no longer ships.
|
|
67
|
+
if (prune) {
|
|
68
|
+
const shipped = new Set(skills.map((s) => s.name));
|
|
69
|
+
for (const stale of manifest.trackedSkills(previous, dir).filter((n) => !shipped.has(n))) {
|
|
70
|
+
const dest = path.join(dir, stale);
|
|
71
|
+
if (fs.existsSync(dest)) {
|
|
72
|
+
if (!dryRun) fs.rmSync(dest, { recursive: true, force: true });
|
|
73
|
+
report.pruned.push(dest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
report.skillDirs.push({ dir, targets, skills: skills.map((s) => s.name) });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (persona) {
|
|
82
|
+
const text = personaText();
|
|
83
|
+
for (const { file, targets } of resolved.instructionFiles) {
|
|
84
|
+
const result = applyPersona(file, { text, dryRun, migrate });
|
|
85
|
+
report.persona.push({ ...result, targets });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
// A persona-only run must not erase the record of previously installed skills,
|
|
91
|
+
// or a later uninstall would leave them orphaned on disk.
|
|
92
|
+
const skillDirs = installSkills ? report.skillDirs : (previous && previous.skillDirs) || [];
|
|
93
|
+
|
|
94
|
+
manifest.write(mode, projectRoot, {
|
|
95
|
+
version,
|
|
96
|
+
installedAt: new Date().toISOString(),
|
|
97
|
+
mode,
|
|
98
|
+
projectRoot: mode === 'project' ? projectRoot : undefined,
|
|
99
|
+
skillDirs,
|
|
100
|
+
persona: report.persona.map(({ file, action }) => ({ file, action })),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return report;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove the skills this package installed, plus the managed persona block.
|
|
109
|
+
* Manifest-scoped: anything the manifest does not list is left alone.
|
|
110
|
+
*/
|
|
111
|
+
function uninstall(opts = {}) {
|
|
112
|
+
const { mode = 'global', projectRoot = process.cwd(), dryRun = false, persona = true } = opts;
|
|
113
|
+
|
|
114
|
+
const previous = manifest.read(mode, projectRoot);
|
|
115
|
+
const report = { mode, dryRun, removed: [], persona: [], manifestFound: Boolean(previous) };
|
|
116
|
+
|
|
117
|
+
if (!previous) return report;
|
|
118
|
+
|
|
119
|
+
for (const { dir, skills } of previous.skillDirs || []) {
|
|
120
|
+
for (const name of skills) {
|
|
121
|
+
const dest = path.join(dir, name);
|
|
122
|
+
if (fs.existsSync(dest)) {
|
|
123
|
+
if (!dryRun) fs.rmSync(dest, { recursive: true, force: true });
|
|
124
|
+
report.removed.push(dest);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Drop the skills directory only if we emptied it.
|
|
128
|
+
try {
|
|
129
|
+
if (!dryRun && fs.readdirSync(dir).filter((n) => n !== '.DS_Store').length === 0) fs.rmdirSync(dir);
|
|
130
|
+
} catch {
|
|
131
|
+
/* directory not empty or not ours to remove */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (persona) {
|
|
136
|
+
for (const entry of previous.persona || []) {
|
|
137
|
+
report.persona.push(removePersona(entry.file, { dryRun }));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!dryRun) manifest.remove(mode, projectRoot);
|
|
142
|
+
return report;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { install, uninstall };
|
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { manifestPath } = require('./paths');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Record of what this tool wrote, so update and uninstall can touch only its own
|
|
10
|
+
* files. Without it the only way to clean up is a blanket `rm -rf bt-*`, which
|
|
11
|
+
* also deletes skills the user authored themselves.
|
|
12
|
+
*/
|
|
13
|
+
function read(mode, projectRoot) {
|
|
14
|
+
const file = manifestPath(mode, projectRoot);
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function write(mode, projectRoot, data) {
|
|
23
|
+
const file = manifestPath(mode, projectRoot);
|
|
24
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
25
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
26
|
+
return file;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function remove(mode, projectRoot) {
|
|
30
|
+
const file = manifestPath(mode, projectRoot);
|
|
31
|
+
try {
|
|
32
|
+
fs.unlinkSync(file);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Skill folder names previously installed into `dir`, per the manifest. */
|
|
40
|
+
function trackedSkills(manifest, dir) {
|
|
41
|
+
if (!manifest || !manifest.skillDirs) return [];
|
|
42
|
+
const entry = manifest.skillDirs.find((d) => d.dir === dir);
|
|
43
|
+
return entry ? entry.skills.slice() : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { read, write, remove, trackedSkills };
|
package/lib/paths.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
function homeDir() {
|
|
9
|
+
return os.homedir();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Expand a leading `~` to the user's home directory. */
|
|
13
|
+
function expandHome(p) {
|
|
14
|
+
if (!p) return null;
|
|
15
|
+
if (p === '~') return homeDir();
|
|
16
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(homeDir(), p.slice(2));
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a target-matrix path.
|
|
22
|
+
* Global paths are `~`-prefixed; project paths are relative to the project root.
|
|
23
|
+
*/
|
|
24
|
+
function resolveTargetPath(p, mode, projectRoot) {
|
|
25
|
+
if (!p) return null;
|
|
26
|
+
return mode === 'project' ? path.resolve(projectRoot, p) : path.resolve(expandHome(p));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Directory inside this package holding the shipped skill folders. */
|
|
30
|
+
function payloadDir() {
|
|
31
|
+
return path.join(PACKAGE_ROOT, 'skills');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Canonical Agent Persona source file shipped with this package. */
|
|
35
|
+
function personaFile() {
|
|
36
|
+
return path.join(PACKAGE_ROOT, 'persona.md');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stateDir(mode, projectRoot) {
|
|
40
|
+
return mode === 'project'
|
|
41
|
+
? path.join(projectRoot, '.babylon-toolkit')
|
|
42
|
+
: path.join(homeDir(), '.babylon-toolkit');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function manifestPath(mode, projectRoot) {
|
|
46
|
+
return path.join(stateDir(mode, projectRoot), 'install-manifest.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Shorten an absolute path back to `~/...` for readable output. */
|
|
50
|
+
function prettyPath(p) {
|
|
51
|
+
if (!p) return '';
|
|
52
|
+
const home = homeDir();
|
|
53
|
+
return p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
PACKAGE_ROOT,
|
|
58
|
+
homeDir,
|
|
59
|
+
expandHome,
|
|
60
|
+
resolveTargetPath,
|
|
61
|
+
payloadDir,
|
|
62
|
+
personaFile,
|
|
63
|
+
stateDir,
|
|
64
|
+
manifestPath,
|
|
65
|
+
prettyPath,
|
|
66
|
+
};
|
package/lib/payload.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { payloadDir } = require('./paths');
|
|
7
|
+
|
|
8
|
+
const IGNORED = new Set(['.DS_Store', 'Thumbs.db']);
|
|
9
|
+
|
|
10
|
+
function walk(dir, prefix = '') {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
13
|
+
if (IGNORED.has(entry.name)) continue;
|
|
14
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
15
|
+
if (entry.isDirectory()) out.push(...walk(path.join(dir, entry.name), rel));
|
|
16
|
+
else if (entry.isFile()) out.push(rel);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enumerate the skills this package actually ships.
|
|
23
|
+
*
|
|
24
|
+
* Everything downstream — install, stale cleanup, and verification — derives from
|
|
25
|
+
* this. Nothing hardcodes a skill list, so a newly added skill is installed and
|
|
26
|
+
* verified without touching any code.
|
|
27
|
+
*/
|
|
28
|
+
function listSkills() {
|
|
29
|
+
const root = payloadDir();
|
|
30
|
+
if (!fs.existsSync(root)) throw new Error(`Skills payload missing from package: ${root}`);
|
|
31
|
+
|
|
32
|
+
return fs
|
|
33
|
+
.readdirSync(root, { withFileTypes: true })
|
|
34
|
+
.filter((e) => e.isDirectory() && !IGNORED.has(e.name))
|
|
35
|
+
.map((e) => e.name)
|
|
36
|
+
.sort()
|
|
37
|
+
.map((name) => ({
|
|
38
|
+
name,
|
|
39
|
+
src: path.join(root, name),
|
|
40
|
+
files: walk(path.join(root, name)),
|
|
41
|
+
}))
|
|
42
|
+
.filter((s) => s.files.includes('SKILL.md'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copyDir(src, dest) {
|
|
46
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
47
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
48
|
+
if (IGNORED.has(entry.name)) continue;
|
|
49
|
+
const from = path.join(src, entry.name);
|
|
50
|
+
const to = path.join(dest, entry.name);
|
|
51
|
+
if (entry.isDirectory()) copyDir(from, to);
|
|
52
|
+
else if (entry.isFile()) fs.copyFileSync(from, to);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { listSkills, copyDir };
|
package/lib/persona.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const { personaFile } = require('./paths');
|
|
7
|
+
|
|
8
|
+
const BEGIN = '<!-- BEGIN BABYLON TOOLKIT PERSONA v1 -->';
|
|
9
|
+
const END = '<!-- END BABYLON TOOLKIT PERSONA -->';
|
|
10
|
+
|
|
11
|
+
/** Present in every persona ever installed, including pre-marker (legacy) ones. */
|
|
12
|
+
const LEGACY_SENTINEL = 'babylontoolkit/agent/main/reference.md';
|
|
13
|
+
const LEGACY_HEADING = '# Babylon Toolkit Agent Persona';
|
|
14
|
+
|
|
15
|
+
function personaText() {
|
|
16
|
+
return fs.readFileSync(personaFile(), 'utf8').trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderBlock(text, eol) {
|
|
20
|
+
return [BEGIN, text, END, ''].join('\n').replace(/\n/g, eol);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function detectEol(content) {
|
|
24
|
+
return content && content.includes('\r\n') ? '\r\n' : '\n';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Join the block to whatever follows it, keeping exactly one blank line between. */
|
|
28
|
+
function joinAfterBlock(block, rest, eol) {
|
|
29
|
+
if (!rest || rest.trim() === '') return block;
|
|
30
|
+
return block + eol + rest.replace(/^[\r\n]+/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Locate a legacy (pre-marker) persona: the heading through the `---` rule that
|
|
35
|
+
* closes it. Returns null when the region cannot be identified confidently — a
|
|
36
|
+
* user who reworded the block gets left alone rather than mangled.
|
|
37
|
+
*/
|
|
38
|
+
function findLegacyRegion(content) {
|
|
39
|
+
const start = content.indexOf(LEGACY_HEADING);
|
|
40
|
+
if (start === -1) return null;
|
|
41
|
+
|
|
42
|
+
const rule = /^-{3,}[ \t]*$/m;
|
|
43
|
+
const after = content.slice(start + LEGACY_HEADING.length);
|
|
44
|
+
const m = rule.exec(after);
|
|
45
|
+
if (!m) return null;
|
|
46
|
+
|
|
47
|
+
let end = start + LEGACY_HEADING.length + m.index + m[0].length;
|
|
48
|
+
// Swallow the newline(s) that terminate the rule so we don't leave a blank gap.
|
|
49
|
+
while (end < content.length && (content[end] === '\r' || content[end] === '\n')) end += 1;
|
|
50
|
+
|
|
51
|
+
// Sanity check: the sentinel must live inside the region we are about to replace.
|
|
52
|
+
if (!content.slice(start, end).includes(LEGACY_SENTINEL)) return null;
|
|
53
|
+
return { start, end };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function backup(file) {
|
|
57
|
+
const dest = `${file}.bak`;
|
|
58
|
+
fs.copyFileSync(file, dest);
|
|
59
|
+
return dest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Install / refresh the Agent Persona in one instruction file.
|
|
64
|
+
*
|
|
65
|
+
* Exactly one outcome per file:
|
|
66
|
+
* created — file did not exist
|
|
67
|
+
* updated — managed block found, contents refreshed in place
|
|
68
|
+
* unchanged — managed block already matches
|
|
69
|
+
* migrated — legacy unmarked persona converted to a managed block
|
|
70
|
+
* prepended — no persona present, block added at the top
|
|
71
|
+
* legacy-unrecognized — persona present but reworded; left untouched
|
|
72
|
+
*
|
|
73
|
+
* Existing content is never truncated, and any modified file is backed up first.
|
|
74
|
+
*/
|
|
75
|
+
function applyPersona(file, { text = personaText(), dryRun = false, migrate = true } = {}) {
|
|
76
|
+
const exists = fs.existsSync(file);
|
|
77
|
+
|
|
78
|
+
if (!exists) {
|
|
79
|
+
const block = renderBlock(text, '\n');
|
|
80
|
+
if (!dryRun) {
|
|
81
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
82
|
+
fs.writeFileSync(file, block, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
return { file, action: 'created' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
88
|
+
const eol = detectEol(content);
|
|
89
|
+
const block = renderBlock(text, eol);
|
|
90
|
+
|
|
91
|
+
const b = content.indexOf(BEGIN);
|
|
92
|
+
const e = content.indexOf(END);
|
|
93
|
+
|
|
94
|
+
if (b !== -1 && e !== -1 && e > b) {
|
|
95
|
+
const endOfBlock = e + END.length;
|
|
96
|
+
const current = content.slice(b, endOfBlock);
|
|
97
|
+
const desired = [BEGIN, text, END].join('\n').replace(/\n/g, eol);
|
|
98
|
+
if (current === desired) return { file, action: 'unchanged' };
|
|
99
|
+
|
|
100
|
+
const next = content.slice(0, b) + desired + content.slice(endOfBlock);
|
|
101
|
+
let bak = null;
|
|
102
|
+
if (!dryRun) {
|
|
103
|
+
bak = backup(file);
|
|
104
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
return { file, action: 'updated', backup: bak };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (content.includes(LEGACY_SENTINEL)) {
|
|
110
|
+
if (!migrate) return { file, action: 'unchanged', note: 'legacy persona left in place (--no-migrate)' };
|
|
111
|
+
|
|
112
|
+
const region = findLegacyRegion(content);
|
|
113
|
+
if (!region) {
|
|
114
|
+
return {
|
|
115
|
+
file,
|
|
116
|
+
action: 'legacy-unrecognized',
|
|
117
|
+
note: 'persona present but reworded — left untouched to avoid clobbering your edits',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const next = content.slice(0, region.start) + joinAfterBlock(block, content.slice(region.end), eol);
|
|
122
|
+
let bak = null;
|
|
123
|
+
if (!dryRun) {
|
|
124
|
+
bak = backup(file);
|
|
125
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
126
|
+
}
|
|
127
|
+
return { file, action: 'migrated', backup: bak };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const next = joinAfterBlock(block, content, eol);
|
|
131
|
+
let bak = null;
|
|
132
|
+
if (!dryRun) {
|
|
133
|
+
bak = backup(file);
|
|
134
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
135
|
+
}
|
|
136
|
+
return { file, action: 'prepended', backup: bak };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Strip the managed block (used by uninstall). Legacy blocks are left alone. */
|
|
140
|
+
function removePersona(file, { dryRun = false } = {}) {
|
|
141
|
+
if (!fs.existsSync(file)) return { file, action: 'absent' };
|
|
142
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
143
|
+
const b = content.indexOf(BEGIN);
|
|
144
|
+
const e = content.indexOf(END);
|
|
145
|
+
if (b === -1 || e === -1 || e < b) return { file, action: 'absent' };
|
|
146
|
+
|
|
147
|
+
let endOfBlock = e + END.length;
|
|
148
|
+
while (endOfBlock < content.length && (content[endOfBlock] === '\r' || content[endOfBlock] === '\n')) endOfBlock += 1;
|
|
149
|
+
|
|
150
|
+
const next = (content.slice(0, b) + content.slice(endOfBlock)).replace(/^\s+/, '');
|
|
151
|
+
let bak = null;
|
|
152
|
+
if (!dryRun) {
|
|
153
|
+
bak = backup(file);
|
|
154
|
+
if (next.trim() === '') fs.unlinkSync(file);
|
|
155
|
+
else fs.writeFileSync(file, next, 'utf8');
|
|
156
|
+
}
|
|
157
|
+
return { file, action: next.trim() === '' ? 'file-removed' : 'removed', backup: bak };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** True when the file carries the persona in any form (managed or legacy). */
|
|
161
|
+
function hasPersona(file) {
|
|
162
|
+
try {
|
|
163
|
+
return fs.readFileSync(file, 'utf8').includes(LEGACY_SENTINEL);
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
BEGIN,
|
|
171
|
+
END,
|
|
172
|
+
LEGACY_SENTINEL,
|
|
173
|
+
personaText,
|
|
174
|
+
applyPersona,
|
|
175
|
+
removePersona,
|
|
176
|
+
hasPersona,
|
|
177
|
+
};
|
package/lib/targets.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveTargetPath } = require('./paths');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The install target matrix.
|
|
7
|
+
*
|
|
8
|
+
* Adding support for another AI client is one entry here — no other file changes.
|
|
9
|
+
* `skills` is the directory that receives the skill folders (null = this client
|
|
10
|
+
* reads no skills of its own). `instructions` is the global instruction file that
|
|
11
|
+
* receives the Agent Persona (null = this client has no instruction file).
|
|
12
|
+
*
|
|
13
|
+
* Only paths that are documented by the client vendor belong here. Guessing a path
|
|
14
|
+
* writes files somewhere the client will never read.
|
|
15
|
+
*/
|
|
16
|
+
const TARGETS = [
|
|
17
|
+
{
|
|
18
|
+
id: 'claude',
|
|
19
|
+
label: 'Claude Code',
|
|
20
|
+
default: true,
|
|
21
|
+
global: { skills: '~/.claude/skills', instructions: '~/.claude/CLAUDE.md' },
|
|
22
|
+
project: { skills: '.claude/skills', instructions: 'CLAUDE.md' },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'agents',
|
|
26
|
+
label: 'Cross-agent standard (Codex, GitHub Copilot, Antigravity)',
|
|
27
|
+
default: true,
|
|
28
|
+
global: { skills: '~/.agents/skills', instructions: '~/.agents/AGENTS.md' },
|
|
29
|
+
project: { skills: '.agents/skills', instructions: 'AGENTS.md' },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'codex',
|
|
33
|
+
label: 'Codex chat clients (Codex CLI / Codex IDE)',
|
|
34
|
+
default: true,
|
|
35
|
+
global: { skills: null, instructions: '~/.codex/AGENTS.md' },
|
|
36
|
+
project: { skills: null, instructions: 'AGENTS.md' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'gemini',
|
|
40
|
+
// Gemini CLI does NOT read AGENTS.md by default — its global context file is
|
|
41
|
+
// GEMINI.md, so ~/.agents/AGENTS.md alone never reaches it.
|
|
42
|
+
label: 'Gemini CLI',
|
|
43
|
+
default: true,
|
|
44
|
+
global: { skills: null, instructions: '~/.gemini/GEMINI.md' },
|
|
45
|
+
project: { skills: null, instructions: 'GEMINI.md' },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'codex-legacy',
|
|
49
|
+
// Pre-`.agents` Codex CLI builds read ~/.codex/skills. Opt in with --legacy-codex.
|
|
50
|
+
label: 'Legacy Codex CLI skills directory',
|
|
51
|
+
default: false,
|
|
52
|
+
global: { skills: '~/.codex/skills', instructions: null },
|
|
53
|
+
project: { skills: '.codex/skills', instructions: null },
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function targetIds() {
|
|
58
|
+
return TARGETS.map((t) => t.id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the matrix into concrete absolute paths for this run.
|
|
63
|
+
*
|
|
64
|
+
* Returns de-duplicated lists: in project mode several clients share ./AGENTS.md,
|
|
65
|
+
* and writing the persona to it once per client would stack duplicate blocks.
|
|
66
|
+
*/
|
|
67
|
+
function resolveTargets({ mode = 'global', projectRoot = process.cwd(), ids = null, legacyCodex = false } = {}) {
|
|
68
|
+
const wanted = new Set(ids && ids.length ? ids : TARGETS.filter((t) => t.default).map((t) => t.id));
|
|
69
|
+
if (legacyCodex) wanted.add('codex-legacy');
|
|
70
|
+
|
|
71
|
+
const unknown = [...wanted].filter((id) => !TARGETS.some((t) => t.id === id));
|
|
72
|
+
if (unknown.length) {
|
|
73
|
+
throw new Error(`Unknown target(s): ${unknown.join(', ')}. Known targets: ${targetIds().join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const selected = TARGETS.filter((t) => wanted.has(t.id));
|
|
77
|
+
const skillDirs = new Map();
|
|
78
|
+
const instructionFiles = new Map();
|
|
79
|
+
|
|
80
|
+
for (const t of selected) {
|
|
81
|
+
const spec = mode === 'project' ? t.project : t.global;
|
|
82
|
+
|
|
83
|
+
const skillsDir = resolveTargetPath(spec.skills, mode, projectRoot);
|
|
84
|
+
if (skillsDir) {
|
|
85
|
+
if (!skillDirs.has(skillsDir)) skillDirs.set(skillsDir, { dir: skillsDir, targets: [] });
|
|
86
|
+
skillDirs.get(skillsDir).targets.push(t.id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const instructions = resolveTargetPath(spec.instructions, mode, projectRoot);
|
|
90
|
+
if (instructions) {
|
|
91
|
+
if (!instructionFiles.has(instructions)) instructionFiles.set(instructions, { file: instructions, targets: [] });
|
|
92
|
+
instructionFiles.get(instructions).targets.push(t.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
mode,
|
|
98
|
+
projectRoot,
|
|
99
|
+
selected,
|
|
100
|
+
skillDirs: [...skillDirs.values()],
|
|
101
|
+
instructionFiles: [...instructionFiles.values()],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { TARGETS, targetIds, resolveTargets };
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@babylonjs-toolkit/agent",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Installs the Babylon Toolkit agent skills and Agent Persona for Claude Code, Codex, GitHub Copilot, Gemini CLI and Antigravity.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"babylonjs",
|
|
7
|
+
"babylon-toolkit",
|
|
8
|
+
"agent-skills",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"codex",
|
|
11
|
+
"copilot"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/babylontoolkit/skills#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/babylontoolkit/skills/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/babylontoolkit/skills.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Mackey Kinard",
|
|
23
|
+
"bin": {
|
|
24
|
+
"bt-agent": "bin/bt-agent.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"skills",
|
|
28
|
+
"bin",
|
|
29
|
+
"lib",
|
|
30
|
+
"scripts",
|
|
31
|
+
"persona.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"postinstall": "node scripts/postinstall.js",
|
|
35
|
+
"deploy-all": "npm publish --access public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/persona.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Babylon Toolkit Agent Persona
|
|
2
|
+
|
|
3
|
+
You are an expert web game developer using BabylonJS and the Babylon Toolkit. Whenever the user's request involves Babylon, BabylonJS, or the Babylon Toolkit, you must always fetch and read the `Agent Reference` at https://raw.githubusercontent.com/babylontoolkit/agent/main/reference.md before doing anything else. Treat that document as your source of truth for conventions, api, patterns and training examples. If the fetch fails, stop immediately and tell the user. This applies even on the very first turn of a new or empty project, before any scaffolding.
|
|
4
|
+
|
|
5
|
+
---
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convenience auto-install for `npm install -g @babylonjs-toolkit/agent`.
|
|
5
|
+
*
|
|
6
|
+
* Two rules:
|
|
7
|
+
* 1. Only run for a global install. If this package is ever pulled in as a
|
|
8
|
+
* project dependency, writing to the user's home directory would be rude.
|
|
9
|
+
* 2. Never fail. A skills-copy problem must not fail someone's npm install —
|
|
10
|
+
* it prints guidance and exits 0.
|
|
11
|
+
*
|
|
12
|
+
* npm's --ignore-scripts, pnpm, and some corporate configs skip this hook
|
|
13
|
+
* entirely, which is why `bt-agent install` is the documented, guaranteed path.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function isGlobalInstall() {
|
|
17
|
+
const flag = process.env.npm_config_global;
|
|
18
|
+
return flag === 'true' || flag === '1';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function main() {
|
|
22
|
+
if (!isGlobalInstall()) {
|
|
23
|
+
console.log('\n@babylonjs-toolkit/agent installed locally — run `npx bt-agent install` to set up your skills.\n');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { install } = require('../lib/install');
|
|
28
|
+
const { doctor } = require('../lib/doctor');
|
|
29
|
+
|
|
30
|
+
const report = install({ mode: 'global' });
|
|
31
|
+
const check = doctor({ mode: 'global' });
|
|
32
|
+
|
|
33
|
+
console.log(`\nBabylon Toolkit — installed ${report.skills.length} agent skills.`);
|
|
34
|
+
for (const entry of report.skillDirs) console.log(` skills ${entry.dir}`);
|
|
35
|
+
for (const p of report.persona) console.log(` persona ${p.file} — ${p.action}`);
|
|
36
|
+
|
|
37
|
+
console.log(check.ok ? '\nINSTALL OK' : '\nINSTALL INCOMPLETE — run `bt-agent doctor` for details');
|
|
38
|
+
console.log('\nRestart your agent session so the skills and persona are picked up.\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
main();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.log('\n@babylonjs-toolkit/agent: automatic setup did not complete.');
|
|
45
|
+
console.log(` reason: ${err && err.message ? err.message : err}`);
|
|
46
|
+
console.log(' run `bt-agent install` to finish setting up.\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process.exit(0);
|