uloop-cli 0.68.1 → 0.68.3

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.68.1",
3
+ "version": "0.68.3",
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",
@@ -13,6 +13,7 @@ import {
13
13
  spawnSync,
14
14
  SpawnSyncOptionsWithStringEncoding,
15
15
  } from 'child_process';
16
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
16
17
  import { join } from 'path';
17
18
 
18
19
  const CLI_PATH = join(__dirname, '../..', 'dist/cli.bundle.cjs');
@@ -459,16 +460,16 @@ describe('CLI E2E Tests (requires running Unity)', () => {
459
460
  });
460
461
  });
461
462
 
462
- describe('get-provider-details', () => {
463
+ describe('get-unity-search-providers', () => {
463
464
  it('should retrieve search providers', () => {
464
- const result = runCliJson<{ Providers: unknown[] }>('get-provider-details');
465
+ const result = runCliJson<{ Providers: unknown[] }>('get-unity-search-providers');
465
466
 
466
467
  expect(Array.isArray(result.Providers)).toBe(true);
467
468
  });
468
469
 
469
470
  it('should support --include-descriptions false to exclude descriptions', () => {
470
471
  const result = runCliJson<{ Providers: unknown[] }>(
471
- 'get-provider-details --include-descriptions false',
472
+ 'get-unity-search-providers --include-descriptions false',
472
473
  );
473
474
 
474
475
  expect(Array.isArray(result.Providers)).toBe(true);
@@ -476,7 +477,7 @@ describe('CLI E2E Tests (requires running Unity)', () => {
476
477
 
477
478
  it('should support --sort-by-priority false to disable priority sorting', () => {
478
479
  const result = runCliJson<{ Providers: unknown[] }>(
479
- 'get-provider-details --sort-by-priority false',
480
+ 'get-unity-search-providers --sort-by-priority false',
480
481
  );
481
482
 
482
483
  expect(Array.isArray(result.Providers)).toBe(true);
@@ -500,6 +501,48 @@ describe('CLI E2E Tests (requires running Unity)', () => {
500
501
  expect(stdout).toContain('--force-recompile');
501
502
  });
502
503
 
504
+ it('should display grouped help with category headings', () => {
505
+ const { stdout, exitCode } = runCli('--help');
506
+
507
+ expect(exitCode).toBe(0);
508
+ expect(stdout).toContain('Built-in Tools:');
509
+ expect(stdout).toContain('CLI Commands:');
510
+ // CLI Commands should appear before Built-in Tools
511
+ const cliIndex: number = stdout.indexOf('CLI Commands:');
512
+ const builtInIndex: number = stdout.indexOf('Built-in Tools:');
513
+ expect(cliIndex).toBeLessThan(builtInIndex);
514
+ });
515
+
516
+ it('should display Third-party Tools section when cache contains third-party tools', () => {
517
+ const { stdout, exitCode } = runCli('--help');
518
+
519
+ expect(exitCode).toBe(0);
520
+ // hello-world is a third-party tool present in the local cache but not in default-tools.json
521
+ if (stdout.includes('hello-world')) {
522
+ expect(stdout).toContain('Third-party Tools:');
523
+ const builtInIndex: number = stdout.indexOf('Built-in Tools:');
524
+ const thirdPartyIndex: number = stdout.indexOf('Third-party Tools:');
525
+ expect(builtInIndex).toBeLessThan(thirdPartyIndex);
526
+ }
527
+ });
528
+
529
+ it('should resolve tool cache via --project-path', () => {
530
+ const withProjectPath = runCli(`--help --project-path "${UNITY_PROJECT_ROOT}"`);
531
+ const withoutProjectPath = runCli('--help');
532
+
533
+ expect(withProjectPath.exitCode).toBe(0);
534
+ expect(withoutProjectPath.exitCode).toBe(0);
535
+
536
+ // Both should show the same category headings
537
+ expect(withProjectPath.stdout).toContain('Built-in Tools:');
538
+ expect(withProjectPath.stdout).toContain('CLI Commands:');
539
+
540
+ // Third-party tools visible in normal help should also appear with --project-path
541
+ if (withoutProjectPath.stdout.includes('Third-party Tools:')) {
542
+ expect(withProjectPath.stdout).toContain('Third-party Tools:');
543
+ }
544
+ });
545
+
503
546
  it('should display boolean options with value format in get-hierarchy help', () => {
504
547
  const { stdout, exitCode } = runCli('get-hierarchy --help');
505
548
 
@@ -666,6 +709,56 @@ describe('CLI E2E Tests (requires running Unity)', () => {
666
709
  });
667
710
  });
668
711
 
712
+ describe('tool-settings', () => {
713
+ const settingsPath: string = join(UNITY_PROJECT_ROOT, '.uloop', 'settings.tools.json');
714
+ let originalSettings: string | null;
715
+
716
+ beforeAll(() => {
717
+ try {
718
+ originalSettings = readFileSync(settingsPath, 'utf-8');
719
+ } catch (error) {
720
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
721
+ originalSettings = null;
722
+ } else {
723
+ throw error;
724
+ }
725
+ }
726
+ writeFileSync(settingsPath, JSON.stringify({ disabledTools: ['get-logs'] }));
727
+ });
728
+
729
+ afterAll(() => {
730
+ if (originalSettings !== null) {
731
+ writeFileSync(settingsPath, originalSettings);
732
+ } else {
733
+ unlinkSync(settingsPath);
734
+ }
735
+ });
736
+
737
+ it('should not display disabled tools in --help', () => {
738
+ const { stdout, exitCode } = runCli('--help');
739
+
740
+ expect(exitCode).toBe(0);
741
+ expect(stdout).toContain('compile');
742
+ expect(stdout).not.toContain('get-logs');
743
+ });
744
+
745
+ it('should not include disabled tools in --list-commands', () => {
746
+ const { stdout, exitCode } = runCli('--list-commands');
747
+
748
+ expect(exitCode).toBe(0);
749
+ const commands: string[] = stdout.trim().split('\n').filter(Boolean);
750
+ expect(commands).toContain('compile');
751
+ expect(commands).not.toContain('get-logs');
752
+ });
753
+
754
+ it('should output nothing for --list-options on disabled tool', () => {
755
+ const { stdout, exitCode } = runCli('--list-options get-logs');
756
+
757
+ expect(exitCode).toBe(0);
758
+ expect(stdout.trim()).toBe('');
759
+ });
760
+ });
761
+
669
762
  // Domain Reload tests must run last to avoid affecting other tests
670
763
  describe('compile --force-recompile (Domain Reload)', () => {
671
764
  it('should support --force-recompile option', () => {
package/src/cli.ts CHANGED
@@ -12,7 +12,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from '
12
12
  import { join, basename, dirname } from 'path';
13
13
  import { homedir } from 'os';
14
14
  import { spawn } from 'child_process';
15
- import { Command } from 'commander';
15
+ import { Command, Option } from 'commander';
16
16
  import {
17
17
  executeToolCommand,
18
18
  listAvailableTools,
@@ -24,6 +24,7 @@ import {
24
24
  loadToolsCache,
25
25
  hasCacheFile,
26
26
  getDefaultTools,
27
+ getDefaultToolNames,
27
28
  ToolDefinition,
28
29
  ToolProperty,
29
30
  getCachedServerVersion,
@@ -41,9 +42,20 @@ interface CliOptions extends GlobalOptions {
41
42
  [key: string]: unknown;
42
43
  }
43
44
 
45
+ const FOCUS_WINDOW_COMMAND = 'focus-window' as const;
44
46
  const LAUNCH_COMMAND = 'launch' as const;
45
47
  const UPDATE_COMMAND = 'update' as const;
46
48
 
49
+ const HELP_GROUP_BUILTIN_TOOLS = 'Built-in Tools:' as const;
50
+ const HELP_GROUP_THIRD_PARTY_TOOLS = 'Third-party Tools:' as const;
51
+ const HELP_GROUP_CLI_COMMANDS = 'CLI Commands:' as const;
52
+
53
+ const HELP_GROUP_ORDER = [
54
+ HELP_GROUP_CLI_COMMANDS,
55
+ HELP_GROUP_BUILTIN_TOOLS,
56
+ HELP_GROUP_THIRD_PARTY_TOOLS,
57
+ ] as const;
58
+
47
59
  // commander.js built-in flags that exit immediately without needing Unity
48
60
  const NO_SYNC_FLAGS = ['-v', '--version', '-h', '--help'] as const;
49
61
 
@@ -55,7 +67,7 @@ const BUILTIN_COMMANDS = [
55
67
  'fix',
56
68
  'skills',
57
69
  LAUNCH_COMMAND,
58
- 'focus-window',
70
+ FOCUS_WINDOW_COMMAND,
59
71
  ] as const;
60
72
 
61
73
  const program = new Command();
@@ -64,7 +76,45 @@ program
64
76
  .name('uloop')
65
77
  .description('Unity MCP CLI - Direct communication with Unity Editor')
66
78
  .version(VERSION, '-v, --version', 'Output the version number')
67
- .showHelpAfterError('(run with -h for available options)');
79
+ .showHelpAfterError('(run with -h for available options)')
80
+ .configureHelp({
81
+ sortSubcommands: true,
82
+ // commander.js default groupItems determines group display order by registration order,
83
+ // but CLI commands are registered at module level (before tools), so the default order
84
+ // would be wrong. We re-implement to enforce HELP_GROUP_ORDER.
85
+ groupItems<T extends Command | Option>(
86
+ unsortedItems: T[],
87
+ visibleItems: T[],
88
+ getGroup: (item: T) => string,
89
+ ): Map<string, T[]> {
90
+ const groupMap = new Map<string, T[]>();
91
+ for (const item of unsortedItems) {
92
+ const group: string = getGroup(item);
93
+ if (!groupMap.has(group)) {
94
+ groupMap.set(group, []);
95
+ }
96
+ }
97
+ for (const item of visibleItems) {
98
+ const group: string = getGroup(item);
99
+ if (!groupMap.has(group)) {
100
+ groupMap.set(group, []);
101
+ }
102
+ groupMap.get(group)!.push(item);
103
+ }
104
+ const ordered = new Map<string, T[]>();
105
+ for (const key of HELP_GROUP_ORDER) {
106
+ const items: T[] | undefined = groupMap.get(key);
107
+ if (items !== undefined) {
108
+ ordered.set(key, items);
109
+ groupMap.delete(key);
110
+ }
111
+ }
112
+ for (const [key, value] of groupMap) {
113
+ ordered.set(key, value);
114
+ }
115
+ return ordered;
116
+ },
117
+ });
68
118
 
69
119
  // --list-commands: Output command names for shell completion
70
120
  program.option('--list-commands', 'List all command names (for shell completion)');
@@ -72,6 +122,12 @@ program.option('--list-commands', 'List all command names (for shell completion)
72
122
  // --list-options <cmd>: Output options for a specific command (for shell completion)
73
123
  program.option('--list-options <cmd>', 'List options for a command (for shell completion)');
74
124
 
125
+ // Set default help group for CLI commands registered at module level
126
+ program.commandsGroup(HELP_GROUP_CLI_COMMANDS);
127
+ // Eagerly initialize the implicit help command so it inherits the CLI Commands group.
128
+ // Without this, the lazy-created help command skips _initCommandGroup and falls into "Commands:" group.
129
+ program.helpCommand(true);
130
+
75
131
  // Built-in commands (not from tools.json)
76
132
  program
77
133
  .command('list')
@@ -124,18 +180,19 @@ registerSkillsCommand(program);
124
180
  // Register launch subcommand
125
181
  registerLaunchCommand(program);
126
182
 
127
- // Register focus-window subcommand
128
- registerFocusWindowCommand(program);
183
+ // focus-window is registered conditionally in main() based on tool settings,
184
+ // since it corresponds to an MCP tool that can be disabled via Tool Settings UI
129
185
 
130
186
  /**
131
187
  * Register a tool as a CLI command dynamically.
132
188
  */
133
- function registerToolCommand(tool: ToolDefinition): void {
189
+ function registerToolCommand(tool: ToolDefinition, helpGroup: string): void {
134
190
  // Skip if already registered as a built-in command
135
191
  if (BUILTIN_COMMANDS.includes(tool.name as (typeof BUILTIN_COMMANDS)[number])) {
136
192
  return;
137
193
  }
138
- const cmd = program.command(tool.name).description(tool.description);
194
+ const firstLine: string = tool.description.split('\n')[0];
195
+ const cmd = program.command(tool.name).description(firstLine).helpGroup(helpGroup);
139
196
 
140
197
  // Add options from inputSchema.properties
141
198
  const properties = tool.inputSchema.properties;
@@ -302,6 +359,10 @@ function convertValue(value: unknown, propInfo: ToolProperty): unknown {
302
359
  return value;
303
360
  }
304
361
 
362
+ function getToolHelpGroup(toolName: string, defaultToolNames: ReadonlySet<string>): string {
363
+ return defaultToolNames.has(toolName) ? HELP_GROUP_BUILTIN_TOOLS : HELP_GROUP_THIRD_PARTY_TOOLS;
364
+ }
365
+
305
366
  function extractGlobalOptions(options: Record<string, unknown>): GlobalOptions {
306
367
  return {
307
368
  port: options['port'] as string | undefined,
@@ -717,12 +778,14 @@ function handleCompletionOptions(): boolean {
717
778
 
718
779
  if (args.includes('--list-commands')) {
719
780
  const tools = loadToolsCache();
720
- // Only filter disabled tools when --project-path is explicitly provided;
721
- // without it, loadDisabledTools would trigger findUnityProjectRoot() scanning
722
- const enabledTools: ToolDefinition[] =
723
- projectPath !== undefined ? filterEnabledTools(tools.tools, projectPath) : tools.tools;
724
- const allCommands = [...BUILTIN_COMMANDS, ...enabledTools.map((t) => t.name)];
725
- console.log(allCommands.join('\n'));
781
+ const enabledTools: ToolDefinition[] = filterEnabledTools(tools.tools, projectPath);
782
+ const allCommands = [
783
+ ...BUILTIN_COMMANDS.filter(
784
+ (cmd) => cmd !== FOCUS_WINDOW_COMMAND || isToolEnabled(cmd, projectPath),
785
+ ),
786
+ ...enabledTools.map((t) => t.name),
787
+ ];
788
+ console.log(allCommands.sort().join('\n'));
726
789
  return true;
727
790
  }
728
791
 
@@ -746,13 +809,10 @@ function listOptionsForCommand(cmdName: string, projectPath?: string): void {
746
809
  }
747
810
 
748
811
  // Tool commands - only output tool-specific options
749
- // Only filter disabled tools when --project-path is explicitly provided;
750
- // without it, loadDisabledTools would trigger findUnityProjectRoot() scanning
751
812
  const tools = loadToolsCache();
752
- const tool: ToolDefinition | undefined =
753
- projectPath !== undefined
754
- ? filterEnabledTools(tools.tools, projectPath).find((t) => t.name === cmdName)
755
- : tools.tools.find((t) => t.name === cmdName);
813
+ const tool: ToolDefinition | undefined = filterEnabledTools(tools.tools, projectPath).find(
814
+ (t) => t.name === cmdName,
815
+ );
756
816
  if (!tool) {
757
817
  return;
758
818
  }
@@ -770,6 +830,9 @@ function listOptionsForCommand(cmdName: string, projectPath?: string): void {
770
830
  * Check if a command exists in the current program.
771
831
  */
772
832
  function commandExists(cmdName: string, projectPath?: string): boolean {
833
+ if (cmdName === FOCUS_WINDOW_COMMAND) {
834
+ return isToolEnabled(FOCUS_WINDOW_COMMAND, projectPath);
835
+ }
773
836
  if (BUILTIN_COMMANDS.includes(cmdName as (typeof BUILTIN_COMMANDS)[number])) {
774
837
  return true;
775
838
  }
@@ -784,6 +847,26 @@ function shouldSkipAutoSync(cmdName: string | undefined, args: string[]): boolea
784
847
  return args.some((arg) => (NO_SYNC_FLAGS as readonly string[]).includes(arg));
785
848
  }
786
849
 
850
+ // Options that consume the next argument as a value
851
+ const OPTIONS_WITH_VALUE: ReadonlySet<string> = new Set(['--port', '-p', '--project-path']);
852
+
853
+ /**
854
+ * Find the first non-option argument that is not a value of a known option.
855
+ */
856
+ function findCommandName(args: readonly string[]): string | undefined {
857
+ for (let i = 0; i < args.length; i++) {
858
+ const arg: string = args[i];
859
+ if (arg.startsWith('-')) {
860
+ if (OPTIONS_WITH_VALUE.has(arg)) {
861
+ i++; // skip the next arg (option value)
862
+ }
863
+ continue;
864
+ }
865
+ return arg;
866
+ }
867
+ return undefined;
868
+ }
869
+
787
870
  function extractSyncGlobalOptions(args: string[]): GlobalOptions {
788
871
  const options: GlobalOptions = {};
789
872
 
@@ -828,8 +911,8 @@ async function main(): Promise<void> {
828
911
  }
829
912
 
830
913
  const args = process.argv.slice(2);
831
- const cmdName = args.find((arg) => !arg.startsWith('-'));
832
914
  const syncGlobalOptions = extractSyncGlobalOptions(args);
915
+ const cmdName = findCommandName(args);
833
916
 
834
917
  // No command name = no Unity operation; skip project detection
835
918
  const NO_PROJECT_COMMANDS = [UPDATE_COMMAND, 'completion'] as const;
@@ -837,15 +920,24 @@ async function main(): Promise<void> {
837
920
  cmdName === undefined || (NO_PROJECT_COMMANDS as readonly string[]).includes(cmdName);
838
921
 
839
922
  if (skipProjectDetection) {
840
- const defaultTools = getDefaultTools();
841
- // Only filter disabled tools when --project-path is explicitly provided;
842
- // without it, filterEnabledTools would trigger findUnityProjectRoot() scanning
843
- const tools: ToolDefinition[] =
844
- syncGlobalOptions.projectPath !== undefined
845
- ? filterEnabledTools(defaultTools.tools, syncGlobalOptions.projectPath)
846
- : defaultTools.tools;
923
+ const defaultToolNames: ReadonlySet<string> = getDefaultToolNames();
924
+ // Only filter disabled tools for top-level help (uloop --help); subcommand help
925
+ // (e.g. uloop completion --help) does not list dynamic tools, so scanning is unnecessary
926
+ const isTopLevelHelp: boolean =
927
+ cmdName === undefined && (args.includes('-h') || args.includes('--help'));
928
+ const shouldFilter: boolean = syncGlobalOptions.projectPath !== undefined || isTopLevelHelp;
929
+ // Use cache to include third-party tools in help output; falls back to defaults when no cache exists
930
+ const sourceTools: ToolDefinition[] = shouldFilter
931
+ ? loadToolsCache(syncGlobalOptions.projectPath).tools
932
+ : getDefaultTools().tools;
933
+ const tools: ToolDefinition[] = shouldFilter
934
+ ? filterEnabledTools(sourceTools, syncGlobalOptions.projectPath)
935
+ : sourceTools;
936
+ if (!shouldFilter || isToolEnabled(FOCUS_WINDOW_COMMAND, syncGlobalOptions.projectPath)) {
937
+ registerFocusWindowCommand(program, HELP_GROUP_BUILTIN_TOOLS);
938
+ }
847
939
  for (const tool of tools) {
848
- registerToolCommand(tool);
940
+ registerToolCommand(tool, getToolHelpGroup(tool.name, defaultToolNames));
849
941
  }
850
942
  program.parse();
851
943
  return;
@@ -878,8 +970,12 @@ async function main(): Promise<void> {
878
970
  // Register tool commands from cache (after potential auto-sync)
879
971
  const toolsCache = loadToolsCache();
880
972
  const projectPath: string | undefined = syncGlobalOptions.projectPath;
973
+ const defaultToolNames: ReadonlySet<string> = getDefaultToolNames();
974
+ if (isToolEnabled(FOCUS_WINDOW_COMMAND, projectPath)) {
975
+ registerFocusWindowCommand(program, HELP_GROUP_BUILTIN_TOOLS);
976
+ }
881
977
  for (const tool of filterEnabledTools(toolsCache.tools, projectPath)) {
882
- registerToolCommand(tool);
978
+ registerToolCommand(tool, getToolHelpGroup(tool.name, defaultToolNames));
883
979
  }
884
980
 
885
981
  if (cmdName && !commandExists(cmdName, projectPath)) {
@@ -894,7 +990,7 @@ async function main(): Promise<void> {
894
990
  const newCache = loadToolsCache();
895
991
  const tool = filterEnabledTools(newCache.tools, projectPath).find((t) => t.name === cmdName);
896
992
  if (tool) {
897
- registerToolCommand(tool);
993
+ registerToolCommand(tool, getToolHelpGroup(tool.name, defaultToolNames));
898
994
  console.log(`\x1b[32m✓ Found '${cmdName}' after sync.\x1b[0m\n`);
899
995
  } else {
900
996
  console.error(`\x1b[31mError: Command '${cmdName}' not found even after sync.\x1b[0m`);
@@ -12,66 +12,71 @@ import { findRunningUnityProcess, focusUnityProcess } from 'launch-unity';
12
12
  import { findUnityProjectRoot } from '../project-root.js';
13
13
  import { validateProjectPath } from '../port-resolver.js';
14
14
 
15
- export function registerFocusWindowCommand(program: Command): void {
16
- program
15
+ export function registerFocusWindowCommand(program: Command, helpGroup?: string): void {
16
+ const cmd = program
17
17
  .command('focus-window')
18
18
  .description('Bring Unity Editor window to front using OS-level commands')
19
- .option('--project-path <path>', 'Unity project path')
20
- .action(async (options: { projectPath?: string }) => {
21
- let projectRoot: string | null;
22
- if (options.projectPath !== undefined) {
23
- try {
24
- projectRoot = validateProjectPath(options.projectPath);
25
- } catch (error) {
26
- console.error(
27
- JSON.stringify({
28
- Success: false,
29
- Message: error instanceof Error ? error.message : String(error),
30
- }),
31
- );
32
- process.exit(1);
33
- return;
34
- }
35
- } else {
36
- projectRoot = findUnityProjectRoot();
37
- }
38
- if (projectRoot === null) {
39
- console.error(
40
- JSON.stringify({
41
- Success: false,
42
- Message: 'Unity project not found',
43
- }),
44
- );
45
- process.exit(1);
46
- }
19
+ .option('--project-path <path>', 'Unity project path');
47
20
 
48
- const runningProcess = await findRunningUnityProcess(projectRoot);
49
- if (!runningProcess) {
50
- console.error(
51
- JSON.stringify({
52
- Success: false,
53
- Message: 'No running Unity process found for this project',
54
- }),
55
- );
56
- process.exit(1);
57
- }
21
+ if (helpGroup !== undefined) {
22
+ cmd.helpGroup(helpGroup);
23
+ }
58
24
 
25
+ cmd.action(async (options: { projectPath?: string }) => {
26
+ let projectRoot: string | null;
27
+ if (options.projectPath !== undefined) {
59
28
  try {
60
- await focusUnityProcess(runningProcess.pid);
61
- console.log(
62
- JSON.stringify({
63
- Success: true,
64
- Message: `Unity Editor window focused (PID: ${runningProcess.pid})`,
65
- }),
66
- );
29
+ projectRoot = validateProjectPath(options.projectPath);
67
30
  } catch (error) {
68
31
  console.error(
69
32
  JSON.stringify({
70
33
  Success: false,
71
- Message: `Failed to focus Unity window: ${error instanceof Error ? error.message : String(error)}`,
34
+ Message: error instanceof Error ? error.message : String(error),
72
35
  }),
73
36
  );
74
37
  process.exit(1);
38
+ return;
75
39
  }
76
- });
40
+ } else {
41
+ projectRoot = findUnityProjectRoot();
42
+ }
43
+ if (projectRoot === null) {
44
+ console.error(
45
+ JSON.stringify({
46
+ Success: false,
47
+ Message: 'Unity project not found',
48
+ }),
49
+ );
50
+ process.exit(1);
51
+ }
52
+
53
+ const runningProcess = await findRunningUnityProcess(projectRoot);
54
+ if (!runningProcess) {
55
+ console.error(
56
+ JSON.stringify({
57
+ Success: false,
58
+ Message: 'No running Unity process found for this project',
59
+ }),
60
+ );
61
+ process.exit(1);
62
+ }
63
+
64
+ try {
65
+ await focusUnityProcess(runningProcess.pid);
66
+ console.log(
67
+ JSON.stringify({
68
+ Success: true,
69
+ Message: `Unity Editor window focused (PID: ${runningProcess.pid})`,
70
+ }),
71
+ );
72
+ } catch (error) {
73
+ console.error(
74
+ JSON.stringify({
75
+ Success: false,
76
+ Message: `Failed to focus Unity window: ${error instanceof Error ? error.message : String(error)}`,
77
+ }),
78
+ );
79
+ process.exit(1);
80
+ }
81
+ });
77
82
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.68.1",
2
+ "version": "0.68.3",
3
3
  "tools": [
4
4
  {
5
5
  "name": "compile",
@@ -239,6 +239,34 @@
239
239
  }
240
240
  }
241
241
  },
242
+ {
243
+ "name": "get-unity-search-providers",
244
+ "description": "Get detailed information about Unity Search providers including display names, descriptions, active status, and capabilities",
245
+ "inputSchema": {
246
+ "type": "object",
247
+ "properties": {
248
+ "ProviderId": {
249
+ "type": "string",
250
+ "description": "Specific provider ID to get details for (empty = all providers). Examples: 'asset', 'scene', 'menu', 'settings'"
251
+ },
252
+ "ActiveOnly": {
253
+ "type": "boolean",
254
+ "description": "Whether to include only active providers",
255
+ "default": false
256
+ },
257
+ "SortByPriority": {
258
+ "type": "boolean",
259
+ "description": "Sort providers by priority (lower number = higher priority)",
260
+ "default": true
261
+ },
262
+ "IncludeDescriptions": {
263
+ "type": "boolean",
264
+ "description": "Include detailed descriptions for each provider",
265
+ "default": true
266
+ }
267
+ }
268
+ }
269
+ },
242
270
  {
243
271
  "name": "get-menu-items",
244
272
  "description": "Retrieve Unity MenuItems",
@@ -6,5 +6,5 @@
6
6
  */
7
7
  export const DEPRECATED_SKILLS: string[] = [
8
8
  'uloop-capture-window', // renamed to uloop-screenshot in v0.54.0
9
- 'uloop-get-provider-details', // internal development-only tool, not for end users
9
+ 'uloop-get-provider-details', // renamed to uloop-get-unity-search-providers
10
10
  ];
package/src/tool-cache.ts CHANGED
@@ -41,7 +41,10 @@ export interface ToolsCache {
41
41
  const CACHE_DIR = '.uloop';
42
42
  const CACHE_FILE = 'tools.json';
43
43
 
44
- function getCacheDir(): string {
44
+ function getCacheDir(projectPath?: string): string {
45
+ if (projectPath !== undefined) {
46
+ return join(projectPath, CACHE_DIR);
47
+ }
45
48
  const projectRoot = findUnityProjectRoot();
46
49
  if (projectRoot === null) {
47
50
  return join(process.cwd(), CACHE_DIR);
@@ -49,8 +52,8 @@ function getCacheDir(): string {
49
52
  return join(projectRoot, CACHE_DIR);
50
53
  }
51
54
 
52
- function getCachePath(): string {
53
- return join(getCacheDir(), CACHE_FILE);
55
+ function getCachePath(projectPath?: string): string {
56
+ return join(getCacheDir(projectPath), CACHE_FILE);
54
57
  }
55
58
 
56
59
  /**
@@ -62,9 +65,10 @@ export function getDefaultTools(): ToolsCache {
62
65
 
63
66
  /**
64
67
  * Load tools from cache file, falling back to default tools if cache doesn't exist.
68
+ * When projectPath is specified, reads cache from that project directory.
65
69
  */
66
- export function loadToolsCache(): ToolsCache {
67
- const cachePath = getCachePath();
70
+ export function loadToolsCache(projectPath?: string): ToolsCache {
71
+ const cachePath = getCachePath(projectPath);
68
72
 
69
73
  if (existsSync(cachePath)) {
70
74
  try {
@@ -107,6 +111,15 @@ export function getCacheFilePath(): string {
107
111
  return getCachePath();
108
112
  }
109
113
 
114
+ /**
115
+ * Get the set of default tool names bundled with npm package.
116
+ * Used to distinguish built-in tools from third-party tools.
117
+ */
118
+ export function getDefaultToolNames(): ReadonlySet<string> {
119
+ const defaultTools: ToolsCache = getDefaultTools();
120
+ return new Set(defaultTools.tools.map((tool: ToolDefinition) => tool.name));
121
+ }
122
+
110
123
  /**
111
124
  * Get the Unity server version from cache file.
112
125
  * Returns undefined if cache doesn't exist, is corrupted, or serverVersion is missing.
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.68.1'; // x-release-please-version
7
+ export const VERSION = '0.68.3'; // x-release-please-version