uloop-cli 0.48.3 → 0.49.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/dist/cli.bundle.cjs +211 -22
- package/dist/cli.bundle.cjs.map +3 -3
- package/jest.config.cjs +2 -1
- package/md-transformer.cjs +11 -0
- package/package.json +1 -1
- package/scripts/generate-bundled-skills.ts +87 -31
- package/src/__tests__/cli-e2e.test.ts +76 -10
- package/src/__tests__/skills-manager.test.ts +158 -0
- package/src/cli.ts +44 -2
- package/src/default-tools.json +1 -1
- package/src/skills/bundled-skills.ts +19 -16
- package/src/skills/skill-definitions/cli-only/uloop-get-version/SKILL.md +37 -0
- package/src/skills/skills-manager.ts +232 -9
- package/src/version.ts +1 -1
- package/CLAUDE.md +0 -61
- package/src/skills/skill-definitions/uloop-capture-unity-window/SKILL.md +0 -81
- package/src/skills/skill-definitions/uloop-clear-console/SKILL.md +0 -34
- package/src/skills/skill-definitions/uloop-compile/SKILL.md +0 -47
- package/src/skills/skill-definitions/uloop-control-play-mode/SKILL.md +0 -48
- package/src/skills/skill-definitions/uloop-execute-dynamic-code/SKILL.md +0 -79
- package/src/skills/skill-definitions/uloop-execute-menu-item/SKILL.md +0 -43
- package/src/skills/skill-definitions/uloop-find-game-objects/SKILL.md +0 -46
- package/src/skills/skill-definitions/uloop-focus-window/SKILL.md +0 -34
- package/src/skills/skill-definitions/uloop-get-hierarchy/SKILL.md +0 -44
- package/src/skills/skill-definitions/uloop-get-logs/SKILL.md +0 -45
- package/src/skills/skill-definitions/uloop-get-menu-items/SKILL.md +0 -44
- package/src/skills/skill-definitions/uloop-get-provider-details/SKILL.md +0 -45
- package/src/skills/skill-definitions/uloop-get-version/SKILL.md +0 -32
- package/src/skills/skill-definitions/uloop-run-tests/SKILL.md +0 -43
- package/src/skills/skill-definitions/uloop-unity-search/SKILL.md +0 -44
- /package/src/skills/skill-definitions/{uloop-get-project-info → cli-only/uloop-get-project-info}/SKILL.md +0 -0
package/jest.config.cjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
module.exports = {
|
|
3
3
|
preset: 'ts-jest',
|
|
4
4
|
testEnvironment: 'node',
|
|
5
|
-
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
5
|
+
moduleFileExtensions: ['ts', 'js', 'json', 'md'],
|
|
6
6
|
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
7
7
|
transform: {
|
|
8
8
|
'^.+\\.ts$': 'ts-jest',
|
|
9
|
+
'^.+\\.md$': '<rootDir>/md-transformer.cjs',
|
|
9
10
|
},
|
|
10
11
|
moduleNameMapper: {
|
|
11
12
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest transformer for .md files
|
|
3
|
+
* Converts markdown files to string exports (similar to esbuild's --loader:.md=text)
|
|
4
|
+
*/
|
|
5
|
+
module.exports = {
|
|
6
|
+
process(sourceText) {
|
|
7
|
+
return {
|
|
8
|
+
code: `module.exports = ${JSON.stringify(sourceText)};`,
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
};
|
package/package.json
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-generates bundled-skills.ts from
|
|
3
|
-
* Scans
|
|
2
|
+
* Auto-generates bundled-skills.ts from multiple source directories.
|
|
3
|
+
* Scans SKILL.md files from:
|
|
4
|
+
* 1. Editor/Api/McpTools/SKILL.md (C# tool folders)
|
|
5
|
+
* 2. skill-definitions/cli-only/SKILL.md (CLI-only skills)
|
|
6
|
+
*
|
|
4
7
|
* Skills with `internal: true` in frontmatter are excluded from generation.
|
|
5
8
|
*
|
|
6
9
|
* Usage: npx tsx scripts/generate-bundled-skills.ts
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
10
|
-
import { join, dirname } from 'path';
|
|
13
|
+
import { join, dirname, relative } from 'path';
|
|
11
14
|
import { fileURLToPath } from 'url';
|
|
12
15
|
|
|
13
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
17
|
const __dirname = dirname(__filename);
|
|
15
18
|
|
|
16
|
-
const
|
|
19
|
+
const MCPTOOLS_DIR = join(__dirname, '../../Editor/Api/McpTools');
|
|
20
|
+
const CLI_ONLY_DIR = join(__dirname, '../src/skills/skill-definitions/cli-only');
|
|
17
21
|
const OUTPUT_FILE = join(__dirname, '../src/skills/bundled-skills.ts');
|
|
22
|
+
const OUTPUT_DIR = dirname(OUTPUT_FILE);
|
|
18
23
|
|
|
19
24
|
interface SkillMetadata {
|
|
20
25
|
dirName: string;
|
|
21
26
|
name: string;
|
|
22
27
|
isInternal: boolean;
|
|
28
|
+
importPath: string;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
function parseFrontmatter(content: string): Record<string, string | boolean> {
|
|
@@ -52,26 +58,79 @@ function parseFrontmatter(content: string): Record<string, string | boolean> {
|
|
|
52
58
|
return frontmatter;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
function getSkillMetadataFromPath(
|
|
62
|
+
skillMdPath: string,
|
|
63
|
+
folderName: string
|
|
64
|
+
): SkillMetadata | null {
|
|
65
|
+
if (!existsSync(skillMdPath)) {
|
|
59
66
|
return null;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
const content = readFileSync(
|
|
69
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
63
70
|
const frontmatter = parseFrontmatter(content);
|
|
64
71
|
|
|
65
|
-
const name = typeof frontmatter.name === 'string' ? frontmatter.name :
|
|
72
|
+
const name = typeof frontmatter.name === 'string' ? frontmatter.name : folderName;
|
|
66
73
|
const isInternal = frontmatter.internal === true;
|
|
74
|
+
const dirName = name;
|
|
75
|
+
|
|
76
|
+
const relativePath = relative(OUTPUT_DIR, skillMdPath).replace(/\\/g, '/');
|
|
77
|
+
const importPath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
|
78
|
+
|
|
79
|
+
return { dirName, name, isInternal, importPath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function collectSkillsFromMcpTools(): SkillMetadata[] {
|
|
83
|
+
if (!existsSync(MCPTOOLS_DIR)) {
|
|
84
|
+
console.warn(`Warning: McpTools directory not found: ${MCPTOOLS_DIR}`);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const dirs = readdirSync(MCPTOOLS_DIR, { withFileTypes: true })
|
|
89
|
+
.filter((dirent) => dirent.isDirectory())
|
|
90
|
+
.map((dirent) => dirent.name)
|
|
91
|
+
.sort();
|
|
92
|
+
|
|
93
|
+
const skills: SkillMetadata[] = [];
|
|
94
|
+
|
|
95
|
+
for (const dirName of dirs) {
|
|
96
|
+
const skillPath = join(MCPTOOLS_DIR, dirName, 'SKILL.md');
|
|
97
|
+
const metadata = getSkillMetadataFromPath(skillPath, dirName);
|
|
98
|
+
if (metadata !== null) {
|
|
99
|
+
skills.push(metadata);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return skills;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collectSkillsFromCliOnly(): SkillMetadata[] {
|
|
107
|
+
if (!existsSync(CLI_ONLY_DIR)) {
|
|
108
|
+
console.warn(`Warning: CLI-only directory not found: ${CLI_ONLY_DIR}`);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
67
111
|
|
|
68
|
-
|
|
112
|
+
const dirs = readdirSync(CLI_ONLY_DIR, { withFileTypes: true })
|
|
113
|
+
.filter((dirent) => dirent.isDirectory())
|
|
114
|
+
.map((dirent) => dirent.name)
|
|
115
|
+
.sort();
|
|
116
|
+
|
|
117
|
+
const skills: SkillMetadata[] = [];
|
|
118
|
+
|
|
119
|
+
for (const dirName of dirs) {
|
|
120
|
+
const skillPath = join(CLI_ONLY_DIR, dirName, 'SKILL.md');
|
|
121
|
+
const metadata = getSkillMetadataFromPath(skillPath, dirName);
|
|
122
|
+
if (metadata !== null) {
|
|
123
|
+
skills.push(metadata);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return skills;
|
|
69
128
|
}
|
|
70
129
|
|
|
71
130
|
function toVariableName(dirName: string): string {
|
|
72
131
|
return dirName
|
|
73
132
|
.replace(/^uloop-/, '')
|
|
74
|
-
.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
|
133
|
+
.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase())
|
|
75
134
|
.concat('Skill');
|
|
76
135
|
}
|
|
77
136
|
|
|
@@ -79,7 +138,7 @@ function generateBundledSkillsFile(skills: SkillMetadata[]): string {
|
|
|
79
138
|
const imports = skills
|
|
80
139
|
.map((skill) => {
|
|
81
140
|
const varName = toVariableName(skill.dirName);
|
|
82
|
-
return `import ${varName} from '
|
|
141
|
+
return `import ${varName} from '${skill.importPath}';`;
|
|
83
142
|
})
|
|
84
143
|
.join('\n');
|
|
85
144
|
|
|
@@ -98,8 +157,11 @@ function generateBundledSkillsFile(skills: SkillMetadata[]): string {
|
|
|
98
157
|
* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
|
|
99
158
|
* Generated by: scripts/generate-bundled-skills.ts
|
|
100
159
|
*
|
|
101
|
-
* This file is automatically generated from
|
|
102
|
-
*
|
|
160
|
+
* This file is automatically generated from:
|
|
161
|
+
* - Editor/Api/McpTools/<ToolFolder>/SKILL.md
|
|
162
|
+
* - skill-definitions/cli-only/<SkillFolder>/SKILL.md
|
|
163
|
+
*
|
|
164
|
+
* To add a new skill, create a SKILL.md file in the appropriate location.
|
|
103
165
|
* To exclude a skill from bundling, add \`internal: true\` to its frontmatter.
|
|
104
166
|
*/
|
|
105
167
|
|
|
@@ -122,33 +184,28 @@ export function getBundledSkillByName(name: string): BundledSkill | undefined {
|
|
|
122
184
|
}
|
|
123
185
|
|
|
124
186
|
function main(): void {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
.map((dirent) => dirent.name)
|
|
128
|
-
.sort();
|
|
187
|
+
const mcpToolsSkills = collectSkillsFromMcpTools();
|
|
188
|
+
const cliOnlySkills = collectSkillsFromCliOnly();
|
|
129
189
|
|
|
130
190
|
const allSkills: SkillMetadata[] = [];
|
|
131
191
|
const internalSkills: string[] = [];
|
|
132
192
|
|
|
133
|
-
for (const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.warn(`Warning: No SKILL.md found in ${dirName}, skipping`);
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (metadata.isInternal) {
|
|
141
|
-
internalSkills.push(metadata.name);
|
|
193
|
+
for (const skill of [...mcpToolsSkills, ...cliOnlySkills]) {
|
|
194
|
+
if (skill.isInternal) {
|
|
195
|
+
internalSkills.push(skill.name);
|
|
142
196
|
continue;
|
|
143
197
|
}
|
|
144
|
-
|
|
145
|
-
allSkills.push(metadata);
|
|
198
|
+
allSkills.push(skill);
|
|
146
199
|
}
|
|
147
200
|
|
|
201
|
+
allSkills.sort((a, b) => a.name.localeCompare(b.name));
|
|
202
|
+
|
|
148
203
|
const output = generateBundledSkillsFile(allSkills);
|
|
149
204
|
writeFileSync(OUTPUT_FILE, output, 'utf-8');
|
|
150
205
|
|
|
151
206
|
console.log(`Generated ${OUTPUT_FILE}`);
|
|
207
|
+
console.log(` - From McpTools: ${mcpToolsSkills.length} skills found`);
|
|
208
|
+
console.log(` - From cli-only: ${cliOnlySkills.length} skills found`);
|
|
152
209
|
console.log(` - Included: ${allSkills.length} skills`);
|
|
153
210
|
if (internalSkills.length > 0) {
|
|
154
211
|
console.log(` - Excluded (internal): ${internalSkills.join(', ')}`);
|
|
@@ -156,4 +213,3 @@ function main(): void {
|
|
|
156
213
|
}
|
|
157
214
|
|
|
158
215
|
main();
|
|
159
|
-
|
|
@@ -92,22 +92,19 @@ describe('CLI E2E Tests (requires running Unity)', () => {
|
|
|
92
92
|
expect(result.Success).toBe(true);
|
|
93
93
|
expect(result.ErrorCount).toBe(0);
|
|
94
94
|
});
|
|
95
|
-
|
|
96
|
-
it('should support --force-recompile option', () => {
|
|
97
|
-
const { exitCode } = runCli('compile --force-recompile');
|
|
98
|
-
|
|
99
|
-
// Domain Reload causes connection to be lost, so we just verify the command runs
|
|
100
|
-
// The exit code may be non-zero due to connection being dropped during reload
|
|
101
|
-
expect(typeof exitCode).toBe('number');
|
|
102
|
-
});
|
|
103
95
|
});
|
|
104
96
|
|
|
105
97
|
describe('get-logs', () => {
|
|
106
98
|
const TEST_LOG_MENU_PATH = 'uLoopMCP/Debug/LogGetter Tests/Output Test Logs';
|
|
99
|
+
const MENU_ITEM_WAIT_MS = 1000;
|
|
107
100
|
|
|
108
101
|
function setupTestLogs(): void {
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
runCliWithRetry('clear-console');
|
|
103
|
+
const result = runCliWithRetry(`execute-menu-item --menu-item-path "${TEST_LOG_MENU_PATH}"`);
|
|
104
|
+
if (result.exitCode !== 0) {
|
|
105
|
+
throw new Error(`execute-menu-item failed: ${result.stderr || result.stdout}`);
|
|
106
|
+
}
|
|
107
|
+
sleepSync(MENU_ITEM_WAIT_MS);
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
it('should retrieve test logs after executing Output Test Logs menu item', () => {
|
|
@@ -339,6 +336,64 @@ describe('CLI E2E Tests (requires running Unity)', () => {
|
|
|
339
336
|
});
|
|
340
337
|
});
|
|
341
338
|
|
|
339
|
+
describe('skills', () => {
|
|
340
|
+
it('should list skills for claude target', () => {
|
|
341
|
+
const { stdout, exitCode } = runCli('skills list --claude');
|
|
342
|
+
|
|
343
|
+
expect(exitCode).toBe(0);
|
|
344
|
+
expect(stdout).toContain('uloop-compile');
|
|
345
|
+
expect(stdout).toContain('uloop-get-logs');
|
|
346
|
+
expect(stdout).toContain('uloop-run-tests');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should show bundled and project skills count', () => {
|
|
350
|
+
const { stdout, exitCode } = runCli('skills list --claude');
|
|
351
|
+
|
|
352
|
+
expect(exitCode).toBe(0);
|
|
353
|
+
// Should show total skills count
|
|
354
|
+
expect(stdout).toMatch(/total:\s*\d+/i);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should install skills for claude target', () => {
|
|
358
|
+
// First uninstall to ensure clean state
|
|
359
|
+
runCli('skills uninstall --claude');
|
|
360
|
+
|
|
361
|
+
const { stdout, exitCode } = runCli('skills install --claude');
|
|
362
|
+
|
|
363
|
+
expect(exitCode).toBe(0);
|
|
364
|
+
expect(stdout).toMatch(/installed|updated|skipped/i);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should uninstall skills for claude target', () => {
|
|
368
|
+
// First install to ensure there are skills to uninstall
|
|
369
|
+
runCli('skills install --claude');
|
|
370
|
+
|
|
371
|
+
const { stdout, exitCode } = runCli('skills uninstall --claude');
|
|
372
|
+
|
|
373
|
+
expect(exitCode).toBe(0);
|
|
374
|
+
expect(stdout).toMatch(/removed|not found/i);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should include project skills in list when available', () => {
|
|
378
|
+
const { stdout, exitCode } = runCli('skills list --claude');
|
|
379
|
+
|
|
380
|
+
expect(exitCode).toBe(0);
|
|
381
|
+
// HelloWorld sample should be detected as a project skill
|
|
382
|
+
expect(stdout).toContain('uloop-hello-world');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should install project skills along with bundled skills', () => {
|
|
386
|
+
// First uninstall
|
|
387
|
+
runCli('skills uninstall --claude');
|
|
388
|
+
|
|
389
|
+
const { stdout, exitCode } = runCli('skills install --claude');
|
|
390
|
+
|
|
391
|
+
expect(exitCode).toBe(0);
|
|
392
|
+
// Should mention project skills were installed
|
|
393
|
+
expect(stdout).toMatch(/project|installed/i);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
342
397
|
describe('error handling', () => {
|
|
343
398
|
it('should handle unknown commands gracefully', () => {
|
|
344
399
|
const { exitCode } = runCli('unknown-command');
|
|
@@ -346,4 +401,15 @@ describe('CLI E2E Tests (requires running Unity)', () => {
|
|
|
346
401
|
expect(exitCode).not.toBe(0);
|
|
347
402
|
});
|
|
348
403
|
});
|
|
404
|
+
|
|
405
|
+
// Domain Reload tests must run last to avoid affecting other tests
|
|
406
|
+
describe('compile --force-recompile (Domain Reload)', () => {
|
|
407
|
+
it('should support --force-recompile option', () => {
|
|
408
|
+
const { exitCode } = runCli('compile --force-recompile');
|
|
409
|
+
|
|
410
|
+
// Domain Reload causes connection to be lost, so we just verify the command runs
|
|
411
|
+
// The exit code may be non-zero due to connection being dropped during reload
|
|
412
|
+
expect(typeof exitCode).toBe('number');
|
|
413
|
+
});
|
|
414
|
+
});
|
|
349
415
|
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for skills-manager.ts
|
|
3
|
+
*
|
|
4
|
+
* Tests pure functions that don't require Unity connection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseFrontmatter } from '../skills/skills-manager.js';
|
|
8
|
+
|
|
9
|
+
describe('parseFrontmatter', () => {
|
|
10
|
+
it('should parse basic frontmatter with string values', () => {
|
|
11
|
+
const content = `---
|
|
12
|
+
name: uloop-test-skill
|
|
13
|
+
description: A test skill for testing
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Test Skill
|
|
17
|
+
|
|
18
|
+
Some content here.
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const result = parseFrontmatter(content);
|
|
22
|
+
|
|
23
|
+
expect(result.name).toBe('uloop-test-skill');
|
|
24
|
+
expect(result.description).toBe('A test skill for testing');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should parse boolean true value', () => {
|
|
28
|
+
const content = `---
|
|
29
|
+
name: internal-skill
|
|
30
|
+
internal: true
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# Internal Skill
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const result = parseFrontmatter(content);
|
|
37
|
+
|
|
38
|
+
expect(result.name).toBe('internal-skill');
|
|
39
|
+
expect(result.internal).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should parse boolean false value', () => {
|
|
43
|
+
const content = `---
|
|
44
|
+
name: public-skill
|
|
45
|
+
internal: false
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
# Public Skill
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const result = parseFrontmatter(content);
|
|
52
|
+
|
|
53
|
+
expect(result.name).toBe('public-skill');
|
|
54
|
+
expect(result.internal).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return empty object for content without frontmatter', () => {
|
|
58
|
+
const content = `# No Frontmatter
|
|
59
|
+
|
|
60
|
+
Just some markdown content.
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const result = parseFrontmatter(content);
|
|
64
|
+
|
|
65
|
+
expect(result).toEqual({});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return empty object for empty content', () => {
|
|
69
|
+
const result = parseFrontmatter('');
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual({});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle frontmatter with colons in value', () => {
|
|
75
|
+
const content = `---
|
|
76
|
+
name: uloop-test
|
|
77
|
+
description: Use when: (1) first case, (2) second case
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# Test
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const result = parseFrontmatter(content);
|
|
84
|
+
|
|
85
|
+
expect(result.name).toBe('uloop-test');
|
|
86
|
+
expect(result.description).toBe('Use when: (1) first case, (2) second case');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle frontmatter with empty values', () => {
|
|
90
|
+
const content = `---
|
|
91
|
+
name: uloop-test
|
|
92
|
+
description:
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
# Test
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const result = parseFrontmatter(content);
|
|
99
|
+
|
|
100
|
+
expect(result.name).toBe('uloop-test');
|
|
101
|
+
expect(result.description).toBe('');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should skip lines without colons', () => {
|
|
105
|
+
const content = `---
|
|
106
|
+
name: uloop-test
|
|
107
|
+
this line has no colon
|
|
108
|
+
description: valid description
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
# Test
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
const result = parseFrontmatter(content);
|
|
115
|
+
|
|
116
|
+
expect(result.name).toBe('uloop-test');
|
|
117
|
+
expect(result.description).toBe('valid description');
|
|
118
|
+
expect(Object.keys(result)).toHaveLength(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should trim whitespace from keys and values', () => {
|
|
122
|
+
const content = `---
|
|
123
|
+
name : uloop-test
|
|
124
|
+
description : spaced description
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
# Test
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
const result = parseFrontmatter(content);
|
|
131
|
+
|
|
132
|
+
expect(result.name).toBe('uloop-test');
|
|
133
|
+
expect(result.description).toBe('spaced description');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle incomplete frontmatter (missing closing)', () => {
|
|
137
|
+
const content = `---
|
|
138
|
+
name: uloop-test
|
|
139
|
+
description: incomplete
|
|
140
|
+
|
|
141
|
+
# No closing delimiter
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const result = parseFrontmatter(content);
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual({});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle frontmatter-only content (no body)', () => {
|
|
150
|
+
const content = `---
|
|
151
|
+
name: minimal-skill
|
|
152
|
+
---`;
|
|
153
|
+
|
|
154
|
+
const result = parseFrontmatter(content);
|
|
155
|
+
|
|
156
|
+
expect(result.name).toBe('minimal-skill');
|
|
157
|
+
});
|
|
158
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -595,7 +595,49 @@ function listOptionsForCommand(cmdName: string): void {
|
|
|
595
595
|
console.log(options.join('\n'));
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
-
|
|
599
|
-
if
|
|
598
|
+
/**
|
|
599
|
+
* Check if a command exists in the current program.
|
|
600
|
+
*/
|
|
601
|
+
function commandExists(cmdName: string): boolean {
|
|
602
|
+
if (BUILTIN_COMMANDS.includes(cmdName as (typeof BUILTIN_COMMANDS)[number])) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
const tools = loadToolsCache();
|
|
606
|
+
return tools.tools.some((t) => t.name === cmdName);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Main entry point with auto-sync for unknown commands.
|
|
611
|
+
*/
|
|
612
|
+
async function main(): Promise<void> {
|
|
613
|
+
if (handleCompletionOptions()) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const args = process.argv.slice(2);
|
|
618
|
+
const cmdName = args.find((arg) => !arg.startsWith('-'));
|
|
619
|
+
|
|
620
|
+
if (cmdName && !commandExists(cmdName)) {
|
|
621
|
+
console.log(`\x1b[33mUnknown command '${cmdName}'. Syncing tools from Unity...\x1b[0m`);
|
|
622
|
+
try {
|
|
623
|
+
await syncTools({});
|
|
624
|
+
const newCache = loadToolsCache();
|
|
625
|
+
const tool = newCache.tools.find((t) => t.name === cmdName);
|
|
626
|
+
if (tool) {
|
|
627
|
+
registerToolCommand(tool);
|
|
628
|
+
console.log(`\x1b[32m✓ Found '${cmdName}' after sync.\x1b[0m\n`);
|
|
629
|
+
} else {
|
|
630
|
+
console.error(`\x1b[31mError: Command '${cmdName}' not found even after sync.\x1b[0m`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
} catch (error) {
|
|
634
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
635
|
+
console.error(`\x1b[31mError: Failed to sync tools: ${message}\x1b[0m`);
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
600
640
|
program.parse();
|
|
601
641
|
}
|
|
642
|
+
|
|
643
|
+
void main();
|
package/src/default-tools.json
CHANGED
|
@@ -2,25 +2,28 @@
|
|
|
2
2
|
* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
|
|
3
3
|
* Generated by: scripts/generate-bundled-skills.ts
|
|
4
4
|
*
|
|
5
|
-
* This file is automatically generated from
|
|
6
|
-
*
|
|
5
|
+
* This file is automatically generated from:
|
|
6
|
+
* - Editor/Api/McpTools/<ToolFolder>/SKILL.md
|
|
7
|
+
* - skill-definitions/cli-only/<SkillFolder>/SKILL.md
|
|
8
|
+
*
|
|
9
|
+
* To add a new skill, create a SKILL.md file in the appropriate location.
|
|
7
10
|
* To exclude a skill from bundling, add `internal: true` to its frontmatter.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
import captureUnityWindowSkill from '
|
|
11
|
-
import clearConsoleSkill from '
|
|
12
|
-
import compileSkill from '
|
|
13
|
-
import controlPlayModeSkill from '
|
|
14
|
-
import executeDynamicCodeSkill from '
|
|
15
|
-
import executeMenuItemSkill from '
|
|
16
|
-
import findGameObjectsSkill from '
|
|
17
|
-
import focusWindowSkill from '
|
|
18
|
-
import getHierarchySkill from '
|
|
19
|
-
import getLogsSkill from '
|
|
20
|
-
import getMenuItemsSkill from '
|
|
21
|
-
import getProviderDetailsSkill from '
|
|
22
|
-
import runTestsSkill from '
|
|
23
|
-
import unitySearchSkill from '
|
|
13
|
+
import captureUnityWindowSkill from '../../../Editor/Api/McpTools/CaptureUnityWindow/SKILL.md';
|
|
14
|
+
import clearConsoleSkill from '../../../Editor/Api/McpTools/ClearConsole/SKILL.md';
|
|
15
|
+
import compileSkill from '../../../Editor/Api/McpTools/Compile/SKILL.md';
|
|
16
|
+
import controlPlayModeSkill from '../../../Editor/Api/McpTools/ControlPlayMode/SKILL.md';
|
|
17
|
+
import executeDynamicCodeSkill from '../../../Editor/Api/McpTools/ExecuteDynamicCode/SKILL.md';
|
|
18
|
+
import executeMenuItemSkill from '../../../Editor/Api/McpTools/ExecuteMenuItem/SKILL.md';
|
|
19
|
+
import findGameObjectsSkill from '../../../Editor/Api/McpTools/FindGameObjects/SKILL.md';
|
|
20
|
+
import focusWindowSkill from '../../../Editor/Api/McpTools/FocusUnityWindow/SKILL.md';
|
|
21
|
+
import getHierarchySkill from '../../../Editor/Api/McpTools/GetHierarchy/SKILL.md';
|
|
22
|
+
import getLogsSkill from '../../../Editor/Api/McpTools/GetLogs/SKILL.md';
|
|
23
|
+
import getMenuItemsSkill from '../../../Editor/Api/McpTools/GetMenuItems/SKILL.md';
|
|
24
|
+
import getProviderDetailsSkill from '../../../Editor/Api/McpTools/UnitySearchProviderDetails/SKILL.md';
|
|
25
|
+
import runTestsSkill from '../../../Editor/Api/McpTools/RunTests/SKILL.md';
|
|
26
|
+
import unitySearchSkill from '../../../Editor/Api/McpTools/UnitySearch/SKILL.md';
|
|
24
27
|
|
|
25
28
|
export interface BundledSkill {
|
|
26
29
|
name: string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: uloop-get-version
|
|
3
|
+
description: Get Unity and project information via uloop CLI. Use when you need to verify Unity version, check project settings (ProductName, CompanyName, Version), or troubleshoot environment issues.
|
|
4
|
+
internal: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# uloop get-version
|
|
8
|
+
|
|
9
|
+
Get Unity version and project information.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uloop get-version
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Parameters
|
|
18
|
+
|
|
19
|
+
None.
|
|
20
|
+
|
|
21
|
+
## Output
|
|
22
|
+
|
|
23
|
+
Returns JSON with:
|
|
24
|
+
- `UnityVersion`: Unity Editor version
|
|
25
|
+
- `Platform`: Current platform
|
|
26
|
+
- `DataPath`: Assets folder path
|
|
27
|
+
- `PersistentDataPath`: Persistent data path
|
|
28
|
+
- `TemporaryCachePath`: Temporary cache path
|
|
29
|
+
- `IsEditor`: Whether running in editor
|
|
30
|
+
- `ProductName`: Application product name
|
|
31
|
+
- `CompanyName`: Company name
|
|
32
|
+
- `Version`: Application version
|
|
33
|
+
- `Ver`: uLoopMCP package version
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
This is a sample custom tool demonstrating how to create MCP tools.
|