uloop-cli 0.45.2 → 0.47.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uloop-cli",
3
- "version": "0.45.2",
3
+ "version": "0.47.0",
4
4
  "//version": "x-release-please-version",
5
5
  "description": "CLI tool for Unity Editor communication via uLoopMCP",
6
6
  "main": "dist/cli.bundle.cjs",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "type": "module",
11
11
  "scripts": {
12
- "build": "esbuild src/cli.ts --bundle --platform=node --format=cjs --outfile=dist/cli.bundle.cjs --sourcemap --banner:js='#!/usr/bin/env node' --loader:.md=text",
12
+ "generate-skills": "npx tsx scripts/generate-bundled-skills.ts",
13
+ "build": "npm run generate-skills && esbuild src/cli.ts --bundle --platform=node --format=cjs --outfile=dist/cli.bundle.cjs --sourcemap --banner:js='#!/usr/bin/env node' --loader:.md=text",
13
14
  "lint": "eslint src",
14
15
  "lint:fix": "eslint src --fix",
15
16
  "format": "prettier --write src/**/*.ts",
@@ -41,12 +42,15 @@
41
42
  "provenance": true
42
43
  },
43
44
  "dependencies": {
44
- "commander": "^14.0.2"
45
+ "commander": "^14.0.2",
46
+ "semver": "^7.7.3"
45
47
  },
46
48
  "devDependencies": {
47
49
  "@eslint/js": "9.39.2",
50
+ "tsx": "4.19.0",
48
51
  "@types/jest": "30.0.0",
49
52
  "@types/node": "25.0.2",
53
+ "@types/semver": "^7.7.1",
50
54
  "esbuild": "0.27.1",
51
55
  "eslint": "9.39.2",
52
56
  "eslint-config-prettier": "^10.1.8",
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Auto-generates bundled-skills.ts from skill-definitions directory.
3
+ * Scans all SKILL.md files and generates import statements and BUNDLED_SKILLS array.
4
+ * Skills with `internal: true` in frontmatter are excluded from generation.
5
+ *
6
+ * Usage: npx tsx scripts/generate-bundled-skills.ts
7
+ */
8
+
9
+ import { readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ const SKILL_DEFINITIONS_DIR = join(__dirname, '../src/skills/skill-definitions');
17
+ const OUTPUT_FILE = join(__dirname, '../src/skills/bundled-skills.ts');
18
+
19
+ interface SkillMetadata {
20
+ dirName: string;
21
+ name: string;
22
+ isInternal: boolean;
23
+ }
24
+
25
+ function parseFrontmatter(content: string): Record<string, string | boolean> {
26
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
27
+ if (!frontmatterMatch) {
28
+ return {};
29
+ }
30
+
31
+ const frontmatter: Record<string, string | boolean> = {};
32
+ const lines = frontmatterMatch[1].split('\n');
33
+
34
+ for (const line of lines) {
35
+ const colonIndex = line.indexOf(':');
36
+ if (colonIndex === -1) {
37
+ continue;
38
+ }
39
+
40
+ const key = line.slice(0, colonIndex).trim();
41
+ const value = line.slice(colonIndex + 1).trim();
42
+
43
+ if (value === 'true') {
44
+ frontmatter[key] = true;
45
+ } else if (value === 'false') {
46
+ frontmatter[key] = false;
47
+ } else {
48
+ frontmatter[key] = value;
49
+ }
50
+ }
51
+
52
+ return frontmatter;
53
+ }
54
+
55
+ function getSkillMetadata(dirName: string): SkillMetadata | null {
56
+ const skillPath = join(SKILL_DEFINITIONS_DIR, dirName, 'SKILL.md');
57
+
58
+ if (!existsSync(skillPath)) {
59
+ return null;
60
+ }
61
+
62
+ const content = readFileSync(skillPath, 'utf-8');
63
+ const frontmatter = parseFrontmatter(content);
64
+
65
+ const name = typeof frontmatter.name === 'string' ? frontmatter.name : dirName;
66
+ const isInternal = frontmatter.internal === true;
67
+
68
+ return { dirName, name, isInternal };
69
+ }
70
+
71
+ function toVariableName(dirName: string): string {
72
+ return dirName
73
+ .replace(/^uloop-/, '')
74
+ .replace(/-([a-z])/g, (_, char) => char.toUpperCase())
75
+ .concat('Skill');
76
+ }
77
+
78
+ function generateBundledSkillsFile(skills: SkillMetadata[]): string {
79
+ const imports = skills
80
+ .map((skill) => {
81
+ const varName = toVariableName(skill.dirName);
82
+ return `import ${varName} from './skill-definitions/${skill.dirName}/SKILL.md';`;
83
+ })
84
+ .join('\n');
85
+
86
+ const entries = skills
87
+ .map((skill) => {
88
+ const varName = toVariableName(skill.dirName);
89
+ return ` {
90
+ name: '${skill.name}',
91
+ dirName: '${skill.dirName}',
92
+ content: ${varName},
93
+ },`;
94
+ })
95
+ .join('\n');
96
+
97
+ return `/**
98
+ * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
99
+ * Generated by: scripts/generate-bundled-skills.ts
100
+ *
101
+ * This file is automatically generated from skill-definitions directory.
102
+ * To add a new skill, create a new directory in skill-definitions with a SKILL.md file.
103
+ * To exclude a skill from bundling, add \`internal: true\` to its frontmatter.
104
+ */
105
+
106
+ ${imports}
107
+
108
+ export interface BundledSkill {
109
+ name: string;
110
+ dirName: string;
111
+ content: string;
112
+ }
113
+
114
+ export const BUNDLED_SKILLS: BundledSkill[] = [
115
+ ${entries}
116
+ ];
117
+
118
+ export function getBundledSkillByName(name: string): BundledSkill | undefined {
119
+ return BUNDLED_SKILLS.find((skill) => skill.name === name);
120
+ }
121
+ `;
122
+ }
123
+
124
+ function main(): void {
125
+ const dirs = readdirSync(SKILL_DEFINITIONS_DIR, { withFileTypes: true })
126
+ .filter((dirent) => dirent.isDirectory())
127
+ .map((dirent) => dirent.name)
128
+ .sort();
129
+
130
+ const allSkills: SkillMetadata[] = [];
131
+ const internalSkills: string[] = [];
132
+
133
+ for (const dirName of dirs) {
134
+ const metadata = getSkillMetadata(dirName);
135
+ if (metadata === null) {
136
+ console.warn(`Warning: No SKILL.md found in ${dirName}, skipping`);
137
+ continue;
138
+ }
139
+
140
+ if (metadata.isInternal) {
141
+ internalSkills.push(metadata.name);
142
+ continue;
143
+ }
144
+
145
+ allSkills.push(metadata);
146
+ }
147
+
148
+ const output = generateBundledSkillsFile(allSkills);
149
+ writeFileSync(OUTPUT_FILE, output, 'utf-8');
150
+
151
+ console.log(`Generated ${OUTPUT_FILE}`);
152
+ console.log(` - Included: ${allSkills.length} skills`);
153
+ if (internalSkills.length > 0) {
154
+ console.log(` - Excluded (internal): ${internalSkills.join(', ')}`);
155
+ }
156
+ }
157
+
158
+ main();
159
+
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.45.2",
2
+ "version": "0.47.0",
3
3
  "tools": [
4
4
  {
5
5
  "name": "compile",
@@ -10,6 +10,7 @@
10
10
  import * as readline from 'readline';
11
11
  import { existsSync } from 'fs';
12
12
  import { join } from 'path';
13
+ import * as semver from 'semver';
13
14
  import { DirectUnityClient } from './direct-unity-client.js';
14
15
  import { resolveUnityPort } from './port-resolver.js';
15
16
  import { saveToolsCache, getCacheFilePath, ToolsCache, ToolDefinition } from './tool-cache.js';
@@ -70,6 +71,52 @@ function isRetryableError(error: unknown): boolean {
70
71
  return message.includes('ECONNREFUSED') || message === 'UNITY_NO_RESPONSE';
71
72
  }
72
73
 
74
+ /**
75
+ * Compare two semantic versions safely.
76
+ * Returns true if v1 < v2, false otherwise.
77
+ * Falls back to string comparison if versions are invalid.
78
+ */
79
+ function isVersionOlder(v1: string, v2: string): boolean {
80
+ const parsed1 = semver.valid(v1);
81
+ const parsed2 = semver.valid(v2);
82
+
83
+ if (parsed1 && parsed2) {
84
+ return semver.lt(parsed1, parsed2);
85
+ }
86
+
87
+ return v1 < v2;
88
+ }
89
+
90
+ /**
91
+ * Print version mismatch warning to stderr.
92
+ * Does not block execution - just warns the user.
93
+ */
94
+ function printVersionWarning(cliVersion: string, serverVersion: string): void {
95
+ const isCliOlder = isVersionOlder(cliVersion, serverVersion);
96
+ const updateCommand = isCliOlder
97
+ ? `npm install -g uloop-cli@${serverVersion}`
98
+ : `Update uLoopMCP package to ${cliVersion} via Unity Package Manager`;
99
+
100
+ console.error('\x1b[33m⚠️ Version mismatch detected!\x1b[0m');
101
+ console.error(` uloop-cli version: ${cliVersion}`);
102
+ console.error(` uloop server version: ${serverVersion}`);
103
+ console.error('');
104
+ console.error(' This may cause unexpected behavior or errors.');
105
+ console.error('');
106
+ console.error(` ${isCliOlder ? 'To update CLI:' : 'To update server:'} ${updateCommand}`);
107
+ console.error('');
108
+ }
109
+
110
+ /**
111
+ * Check server version from response and print warning if mismatched.
112
+ */
113
+ function checkServerVersion(result: Record<string, unknown>): void {
114
+ const serverVersion = result['ULoopServerVersion'] as string | undefined;
115
+ if (serverVersion && serverVersion !== VERSION) {
116
+ printVersionWarning(VERSION, serverVersion);
117
+ }
118
+ }
119
+
73
120
  /**
74
121
  * Check if Unity is in a busy state (compiling, reloading, or server starting).
75
122
  * Throws an error with appropriate message if busy.
@@ -123,7 +170,7 @@ export async function executeToolCommand(
123
170
  await client.connect();
124
171
 
125
172
  spinner.update(`Executing ${toolName}...`);
126
- const result = await client.sendRequest(toolName, params);
173
+ const result = await client.sendRequest<Record<string, unknown>>(toolName, params);
127
174
 
128
175
  if (result === undefined || result === null) {
129
176
  throw new Error('UNITY_NO_RESPONSE');
@@ -132,6 +179,10 @@ export async function executeToolCommand(
132
179
  // Success - stop spinner and output result
133
180
  spinner.stop();
134
181
  restoreStdin();
182
+
183
+ // Check server version and warn if mismatched
184
+ checkServerVersion(result);
185
+
135
186
  console.log(JSON.stringify(result, null, 2));
136
187
  return;
137
188
  } catch (error) {
@@ -1,21 +1,26 @@
1
1
  /**
2
- * Bundled skill definitions for uloop CLI.
3
- * These skills are embedded at build time via esbuild --loader:.md=text
2
+ * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
3
+ * Generated by: scripts/generate-bundled-skills.ts
4
+ *
5
+ * This file is automatically generated from skill-definitions directory.
6
+ * To add a new skill, create a new directory in skill-definitions with a SKILL.md file.
7
+ * To exclude a skill from bundling, add `internal: true` to its frontmatter.
4
8
  */
5
9
 
6
- import compileSkill from './skill-definitions/uloop-compile/SKILL.md';
7
- import getLogsSkill from './skill-definitions/uloop-get-logs/SKILL.md';
8
- import runTestsSkill from './skill-definitions/uloop-run-tests/SKILL.md';
10
+ import captureGameviewSkill from './skill-definitions/uloop-capture-gameview/SKILL.md';
9
11
  import clearConsoleSkill from './skill-definitions/uloop-clear-console/SKILL.md';
12
+ import compileSkill from './skill-definitions/uloop-compile/SKILL.md';
13
+ import controlPlayModeSkill from './skill-definitions/uloop-control-play-mode/SKILL.md';
14
+ import executeDynamicCodeSkill from './skill-definitions/uloop-execute-dynamic-code/SKILL.md';
15
+ import executeMenuItemSkill from './skill-definitions/uloop-execute-menu-item/SKILL.md';
16
+ import findGameObjectsSkill from './skill-definitions/uloop-find-game-objects/SKILL.md';
10
17
  import focusWindowSkill from './skill-definitions/uloop-focus-window/SKILL.md';
11
18
  import getHierarchySkill from './skill-definitions/uloop-get-hierarchy/SKILL.md';
12
- import unitySearchSkill from './skill-definitions/uloop-unity-search/SKILL.md';
19
+ import getLogsSkill from './skill-definitions/uloop-get-logs/SKILL.md';
13
20
  import getMenuItemsSkill from './skill-definitions/uloop-get-menu-items/SKILL.md';
14
- import executeMenuItemSkill from './skill-definitions/uloop-execute-menu-item/SKILL.md';
15
- import findGameObjectsSkill from './skill-definitions/uloop-find-game-objects/SKILL.md';
16
- import captureGameviewSkill from './skill-definitions/uloop-capture-gameview/SKILL.md';
17
- import executeDynamicCodeSkill from './skill-definitions/uloop-execute-dynamic-code/SKILL.md';
18
21
  import getProviderDetailsSkill from './skill-definitions/uloop-get-provider-details/SKILL.md';
22
+ import runTestsSkill from './skill-definitions/uloop-run-tests/SKILL.md';
23
+ import unitySearchSkill from './skill-definitions/uloop-unity-search/SKILL.md';
19
24
 
20
25
  export interface BundledSkill {
21
26
  name: string;
@@ -24,14 +29,31 @@ export interface BundledSkill {
24
29
  }
25
30
 
26
31
  export const BUNDLED_SKILLS: BundledSkill[] = [
27
- { name: 'uloop-compile', dirName: 'uloop-compile', content: compileSkill },
28
- { name: 'uloop-get-logs', dirName: 'uloop-get-logs', content: getLogsSkill },
29
- { name: 'uloop-run-tests', dirName: 'uloop-run-tests', content: runTestsSkill },
30
- { name: 'uloop-clear-console', dirName: 'uloop-clear-console', content: clearConsoleSkill },
31
- { name: 'uloop-focus-window', dirName: 'uloop-focus-window', content: focusWindowSkill },
32
- { name: 'uloop-get-hierarchy', dirName: 'uloop-get-hierarchy', content: getHierarchySkill },
33
- { name: 'uloop-unity-search', dirName: 'uloop-unity-search', content: unitySearchSkill },
34
- { name: 'uloop-get-menu-items', dirName: 'uloop-get-menu-items', content: getMenuItemsSkill },
32
+ {
33
+ name: 'uloop-capture-gameview',
34
+ dirName: 'uloop-capture-gameview',
35
+ content: captureGameviewSkill,
36
+ },
37
+ {
38
+ name: 'uloop-clear-console',
39
+ dirName: 'uloop-clear-console',
40
+ content: clearConsoleSkill,
41
+ },
42
+ {
43
+ name: 'uloop-compile',
44
+ dirName: 'uloop-compile',
45
+ content: compileSkill,
46
+ },
47
+ {
48
+ name: 'uloop-control-play-mode',
49
+ dirName: 'uloop-control-play-mode',
50
+ content: controlPlayModeSkill,
51
+ },
52
+ {
53
+ name: 'uloop-execute-dynamic-code',
54
+ dirName: 'uloop-execute-dynamic-code',
55
+ content: executeDynamicCodeSkill,
56
+ },
35
57
  {
36
58
  name: 'uloop-execute-menu-item',
37
59
  dirName: 'uloop-execute-menu-item',
@@ -43,20 +65,40 @@ export const BUNDLED_SKILLS: BundledSkill[] = [
43
65
  content: findGameObjectsSkill,
44
66
  },
45
67
  {
46
- name: 'uloop-capture-gameview',
47
- dirName: 'uloop-capture-gameview',
48
- content: captureGameviewSkill,
68
+ name: 'uloop-focus-window',
69
+ dirName: 'uloop-focus-window',
70
+ content: focusWindowSkill,
49
71
  },
50
72
  {
51
- name: 'uloop-execute-dynamic-code',
52
- dirName: 'uloop-execute-dynamic-code',
53
- content: executeDynamicCodeSkill,
73
+ name: 'uloop-get-hierarchy',
74
+ dirName: 'uloop-get-hierarchy',
75
+ content: getHierarchySkill,
76
+ },
77
+ {
78
+ name: 'uloop-get-logs',
79
+ dirName: 'uloop-get-logs',
80
+ content: getLogsSkill,
81
+ },
82
+ {
83
+ name: 'uloop-get-menu-items',
84
+ dirName: 'uloop-get-menu-items',
85
+ content: getMenuItemsSkill,
54
86
  },
55
87
  {
56
88
  name: 'uloop-get-provider-details',
57
89
  dirName: 'uloop-get-provider-details',
58
90
  content: getProviderDetailsSkill,
59
91
  },
92
+ {
93
+ name: 'uloop-run-tests',
94
+ dirName: 'uloop-run-tests',
95
+ content: runTestsSkill,
96
+ },
97
+ {
98
+ name: 'uloop-unity-search',
99
+ dirName: 'uloop-unity-search',
100
+ content: unitySearchSkill,
101
+ },
60
102
  ];
61
103
 
62
104
  export function getBundledSkillByName(name: string): BundledSkill | undefined {
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: uloop-control-play-mode
3
+ description: Control Unity Editor play mode via uloop CLI. Use when you need to: (1) Start play mode for testing, (2) Stop play mode after testing, (3) Pause play mode for debugging.
4
+ ---
5
+
6
+ # uloop control-play-mode
7
+
8
+ Control Unity Editor play mode (play/stop/pause).
9
+
10
+ ## Usage
11
+
12
+ ```bash
13
+ uloop control-play-mode [options]
14
+ ```
15
+
16
+ ## Parameters
17
+
18
+ | Parameter | Type | Default | Description |
19
+ |-----------|------|---------|-------------|
20
+ | `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` |
21
+
22
+ ## Examples
23
+
24
+ ```bash
25
+ # Start play mode
26
+ uloop control-play-mode --action Play
27
+
28
+ # Stop play mode
29
+ uloop control-play-mode --action Stop
30
+
31
+ # Pause play mode
32
+ uloop control-play-mode --action Pause
33
+ ```
34
+
35
+ ## Output
36
+
37
+ Returns JSON with the current play mode state:
38
+ - `IsPlaying`: Whether Unity is currently in play mode
39
+ - `IsPaused`: Whether play mode is paused
40
+ - `Message`: Description of the action performed
41
+
42
+ ## Notes
43
+
44
+ - Play action starts the game in the Unity Editor (also resumes from pause)
45
+ - Stop action exits play mode and returns to edit mode
46
+ - Pause action pauses the game while remaining in play mode
47
+ - Useful for automated testing workflows
48
+
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: uloop-get-project-info
3
3
  description: Get Unity project information via uloop CLI. Use when you need to: (1) Check Unity Editor version, (2) Get project settings and platform info, (3) Retrieve project metadata for diagnostics.
4
+ internal: true
4
5
  ---
5
6
 
6
7
  # uloop get-project-info
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: uloop-get-version
3
3
  description: Get uLoopMCP version information via uloop CLI. Use when you need to: (1) Check uLoopMCP package version, (2) Verify installation is working correctly, (3) Troubleshoot version compatibility issues.
4
+ internal: true
4
5
  ---
5
6
 
6
7
  # uloop get-version
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * This file exists to avoid bundling the entire package.json into the CLI bundle.
5
5
  * This version is automatically updated by release-please.
6
6
  */
7
- export const VERSION = '0.45.2'; // x-release-please-version
7
+ export const VERSION = '0.47.0'; // x-release-please-version