uloop-cli 0.67.5 → 0.68.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.67.5",
3
+ "version": "0.68.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",
@@ -48,7 +48,7 @@
48
48
  "devDependencies": {
49
49
  "@eslint/js": "10.0.1",
50
50
  "@types/jest": "30.0.0",
51
- "@types/node": "25.3.1",
51
+ "@types/node": "25.3.2",
52
52
  "@types/semver": "7.7.1",
53
53
  "esbuild": "0.27.3",
54
54
  "eslint": "10.0.2",
@@ -63,6 +63,6 @@
63
63
  "typescript-eslint": "8.56.1"
64
64
  },
65
65
  "overrides": {
66
- "minimatch": "10.2.2"
66
+ "minimatch": "10.2.4"
67
67
  }
68
68
  }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Unit tests for tool-settings-loader.ts
3
+ *
4
+ * Tests pure functions for loading and filtering disabled tools.
5
+ * Uses temporary directories to avoid affecting real project settings.
6
+ */
7
+
8
+ import { mkdirSync, rmSync, writeFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { tmpdir } from 'os';
11
+
12
+ // Mock findUnityProjectRoot before importing the module
13
+ let mockProjectRoot: string | null = null;
14
+
15
+ jest.mock('../project-root.js', () => ({
16
+ findUnityProjectRoot: (): string | null => mockProjectRoot,
17
+ }));
18
+
19
+ // Import after mocking
20
+ import { loadDisabledTools, isToolEnabled, filterEnabledTools } from '../tool-settings-loader.js';
21
+ import type { ToolDefinition } from '../tool-cache.js';
22
+
23
+ describe('tool-settings-loader', () => {
24
+ let testDir: string;
25
+
26
+ beforeEach(() => {
27
+ testDir = join(tmpdir(), `uloop-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
28
+ mkdirSync(join(testDir, '.uloop'), { recursive: true });
29
+ mockProjectRoot = testDir;
30
+ });
31
+
32
+ afterEach(() => {
33
+ rmSync(testDir, { recursive: true, force: true });
34
+ mockProjectRoot = null;
35
+ });
36
+
37
+ // ── loadDisabledTools ──────────────────────────────────────────
38
+
39
+ describe('loadDisabledTools', () => {
40
+ it('should return disabled tools from valid settings file', () => {
41
+ writeFileSync(
42
+ join(testDir, '.uloop', 'settings.tools.json'),
43
+ JSON.stringify({ disabledTools: ['compile', 'get-logs'] }),
44
+ );
45
+
46
+ const result: string[] = loadDisabledTools();
47
+
48
+ expect(result).toEqual(['compile', 'get-logs']);
49
+ });
50
+
51
+ it('should return empty array when file does not exist', () => {
52
+ const result: string[] = loadDisabledTools();
53
+
54
+ expect(result).toEqual([]);
55
+ });
56
+
57
+ it('should return empty array when project root is null', () => {
58
+ mockProjectRoot = null;
59
+
60
+ const result: string[] = loadDisabledTools();
61
+
62
+ expect(result).toEqual([]);
63
+ });
64
+
65
+ it('should return empty array for invalid JSON', () => {
66
+ writeFileSync(join(testDir, '.uloop', 'settings.tools.json'), '{invalid json}');
67
+
68
+ const result: string[] = loadDisabledTools();
69
+
70
+ expect(result).toEqual([]);
71
+ });
72
+
73
+ it('should return empty array for empty file', () => {
74
+ writeFileSync(join(testDir, '.uloop', 'settings.tools.json'), '');
75
+
76
+ const result: string[] = loadDisabledTools();
77
+
78
+ expect(result).toEqual([]);
79
+ });
80
+
81
+ it('should return empty array when disabledTools is not an array', () => {
82
+ writeFileSync(
83
+ join(testDir, '.uloop', 'settings.tools.json'),
84
+ JSON.stringify({ disabledTools: 'not-an-array' }),
85
+ );
86
+
87
+ const result: string[] = loadDisabledTools();
88
+
89
+ expect(result).toEqual([]);
90
+ });
91
+
92
+ it('should return empty array when disabledTools key is missing', () => {
93
+ writeFileSync(
94
+ join(testDir, '.uloop', 'settings.tools.json'),
95
+ JSON.stringify({ other: 'data' }),
96
+ );
97
+
98
+ const result: string[] = loadDisabledTools();
99
+
100
+ expect(result).toEqual([]);
101
+ });
102
+ });
103
+
104
+ // ── isToolEnabled ──────────────────────────────────────────────
105
+
106
+ describe('isToolEnabled', () => {
107
+ it('should return false for a disabled tool', () => {
108
+ writeFileSync(
109
+ join(testDir, '.uloop', 'settings.tools.json'),
110
+ JSON.stringify({ disabledTools: ['compile'] }),
111
+ );
112
+
113
+ expect(isToolEnabled('compile')).toBe(false);
114
+ });
115
+
116
+ it('should return true for an enabled tool', () => {
117
+ writeFileSync(
118
+ join(testDir, '.uloop', 'settings.tools.json'),
119
+ JSON.stringify({ disabledTools: ['compile'] }),
120
+ );
121
+
122
+ expect(isToolEnabled('get-logs')).toBe(true);
123
+ });
124
+
125
+ it('should return true when no settings file exists', () => {
126
+ expect(isToolEnabled('compile')).toBe(true);
127
+ });
128
+ });
129
+
130
+ // ── filterEnabledTools ─────────────────────────────────────────
131
+
132
+ describe('filterEnabledTools', () => {
133
+ const mockTools: ToolDefinition[] = [
134
+ {
135
+ name: 'compile',
136
+ description: 'Compile',
137
+ inputSchema: { type: 'object', properties: {} },
138
+ },
139
+ {
140
+ name: 'get-logs',
141
+ description: 'Get logs',
142
+ inputSchema: { type: 'object', properties: {} },
143
+ },
144
+ {
145
+ name: 'clear-console',
146
+ description: 'Clear',
147
+ inputSchema: { type: 'object', properties: {} },
148
+ },
149
+ ];
150
+
151
+ it('should filter out disabled tools', () => {
152
+ writeFileSync(
153
+ join(testDir, '.uloop', 'settings.tools.json'),
154
+ JSON.stringify({ disabledTools: ['compile', 'clear-console'] }),
155
+ );
156
+
157
+ const result: ToolDefinition[] = filterEnabledTools(mockTools);
158
+
159
+ expect(result).toHaveLength(1);
160
+ expect(result[0].name).toBe('get-logs');
161
+ });
162
+
163
+ it('should return all tools when none are disabled', () => {
164
+ writeFileSync(
165
+ join(testDir, '.uloop', 'settings.tools.json'),
166
+ JSON.stringify({ disabledTools: [] }),
167
+ );
168
+
169
+ const result: ToolDefinition[] = filterEnabledTools(mockTools);
170
+
171
+ expect(result).toHaveLength(3);
172
+ });
173
+
174
+ it('should return all tools when settings file does not exist', () => {
175
+ const result: ToolDefinition[] = filterEnabledTools(mockTools);
176
+
177
+ expect(result).toHaveLength(3);
178
+ });
179
+ });
180
+
181
+ // ── projectPath override ────────────────────────────────────────
182
+
183
+ describe('projectPath override', () => {
184
+ let otherDir: string;
185
+
186
+ beforeEach(() => {
187
+ otherDir = join(tmpdir(), `uloop-other-${Date.now()}-${Math.random().toString(36).slice(2)}`);
188
+ mkdirSync(join(otherDir, '.uloop'), { recursive: true });
189
+ });
190
+
191
+ afterEach(() => {
192
+ rmSync(otherDir, { recursive: true, force: true });
193
+ });
194
+
195
+ it('should load settings from projectPath instead of cwd project root', () => {
196
+ writeFileSync(
197
+ join(otherDir, '.uloop', 'settings.tools.json'),
198
+ JSON.stringify({ disabledTools: ['get-logs'] }),
199
+ );
200
+
201
+ const result: string[] = loadDisabledTools(otherDir);
202
+
203
+ expect(result).toEqual(['get-logs']);
204
+ });
205
+
206
+ it('should ignore cwd project root when projectPath is provided', () => {
207
+ writeFileSync(
208
+ join(testDir, '.uloop', 'settings.tools.json'),
209
+ JSON.stringify({ disabledTools: ['compile'] }),
210
+ );
211
+ writeFileSync(
212
+ join(otherDir, '.uloop', 'settings.tools.json'),
213
+ JSON.stringify({ disabledTools: ['get-logs'] }),
214
+ );
215
+
216
+ expect(loadDisabledTools(otherDir)).toEqual(['get-logs']);
217
+ expect(loadDisabledTools()).toEqual(['compile']);
218
+ });
219
+
220
+ it('should pass projectPath through isToolEnabled', () => {
221
+ writeFileSync(
222
+ join(otherDir, '.uloop', 'settings.tools.json'),
223
+ JSON.stringify({ disabledTools: ['compile'] }),
224
+ );
225
+
226
+ expect(isToolEnabled('compile', otherDir)).toBe(false);
227
+ expect(isToolEnabled('get-logs', otherDir)).toBe(true);
228
+ });
229
+
230
+ it('should pass projectPath through filterEnabledTools', () => {
231
+ const mockTools: ToolDefinition[] = [
232
+ {
233
+ name: 'compile',
234
+ description: 'Compile',
235
+ inputSchema: { type: 'object', properties: {} },
236
+ },
237
+ {
238
+ name: 'get-logs',
239
+ description: 'Get logs',
240
+ inputSchema: { type: 'object', properties: {} },
241
+ },
242
+ ];
243
+ writeFileSync(
244
+ join(otherDir, '.uloop', 'settings.tools.json'),
245
+ JSON.stringify({ disabledTools: ['compile'] }),
246
+ );
247
+
248
+ const result: ToolDefinition[] = filterEnabledTools(mockTools, otherDir);
249
+
250
+ expect(result).toHaveLength(1);
251
+ expect(result[0].name).toBe('get-logs');
252
+ });
253
+
254
+ it('should return empty array when projectPath has no settings file', () => {
255
+ const result: string[] = loadDisabledTools(otherDir);
256
+
257
+ expect(result).toEqual([]);
258
+ });
259
+ });
260
+ });
package/src/cli.ts CHANGED
@@ -35,6 +35,7 @@ import { registerFocusWindowCommand } from './commands/focus-window.js';
35
35
  import { VERSION } from './version.js';
36
36
  import { findUnityProjectRoot } from './project-root.js';
37
37
  import { validateProjectPath } from './port-resolver.js';
38
+ import { filterEnabledTools, isToolEnabled } from './tool-settings-loader.js';
38
39
 
39
40
  interface CliOptions extends GlobalOptions {
40
41
  [key: string]: unknown;
@@ -312,6 +313,11 @@ function isConnectionError(message: string): boolean {
312
313
  return message.includes('ECONNREFUSED') || message.includes('EADDRNOTAVAIL');
313
314
  }
314
315
 
316
+ function printToolDisabledError(cmdName: string): void {
317
+ console.error(`\x1b[33mTool '${cmdName}' is disabled.\x1b[0m`);
318
+ console.error('You can enable it in Unity: Window > uLoop > Tool Settings');
319
+ }
320
+
315
321
  function printConnectionError(): void {
316
322
  console.error('\x1b[31mError: Cannot connect to Unity.\x1b[0m');
317
323
  console.error('Make sure Unity Editor is open and uLoopMCP server is running.');
@@ -707,10 +713,15 @@ function handleCompletion(install: boolean, shellOverride?: string): void {
707
713
  */
708
714
  function handleCompletionOptions(): boolean {
709
715
  const args = process.argv.slice(2);
716
+ const projectPath: string | undefined = extractSyncGlobalOptions(args).projectPath;
710
717
 
711
718
  if (args.includes('--list-commands')) {
712
719
  const tools = loadToolsCache();
713
- const allCommands = [...BUILTIN_COMMANDS, ...tools.tools.map((t) => t.name)];
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)];
714
725
  console.log(allCommands.join('\n'));
715
726
  return true;
716
727
  }
@@ -718,7 +729,7 @@ function handleCompletionOptions(): boolean {
718
729
  const listOptionsIdx = args.indexOf('--list-options');
719
730
  if (listOptionsIdx !== -1 && args[listOptionsIdx + 1]) {
720
731
  const cmdName = args[listOptionsIdx + 1];
721
- listOptionsForCommand(cmdName);
732
+ listOptionsForCommand(cmdName, projectPath);
722
733
  return true;
723
734
  }
724
735
 
@@ -728,15 +739,20 @@ function handleCompletionOptions(): boolean {
728
739
  /**
729
740
  * List options for a specific command.
730
741
  */
731
- function listOptionsForCommand(cmdName: string): void {
742
+ function listOptionsForCommand(cmdName: string, projectPath?: string): void {
732
743
  // Built-in commands have no tool-specific options
733
744
  if (BUILTIN_COMMANDS.includes(cmdName as (typeof BUILTIN_COMMANDS)[number])) {
734
745
  return;
735
746
  }
736
747
 
737
748
  // 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
738
751
  const tools = loadToolsCache();
739
- const tool = tools.tools.find((t) => t.name === cmdName);
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);
740
756
  if (!tool) {
741
757
  return;
742
758
  }
@@ -753,12 +769,12 @@ function listOptionsForCommand(cmdName: string): void {
753
769
  /**
754
770
  * Check if a command exists in the current program.
755
771
  */
756
- function commandExists(cmdName: string): boolean {
772
+ function commandExists(cmdName: string, projectPath?: string): boolean {
757
773
  if (BUILTIN_COMMANDS.includes(cmdName as (typeof BUILTIN_COMMANDS)[number])) {
758
774
  return true;
759
775
  }
760
776
  const tools = loadToolsCache();
761
- return tools.tools.some((t) => t.name === cmdName);
777
+ return filterEnabledTools(tools.tools, projectPath).some((t) => t.name === cmdName);
762
778
  }
763
779
 
764
780
  function shouldSkipAutoSync(cmdName: string | undefined, args: string[]): boolean {
@@ -822,7 +838,13 @@ async function main(): Promise<void> {
822
838
 
823
839
  if (skipProjectDetection) {
824
840
  const defaultTools = getDefaultTools();
825
- for (const tool of defaultTools.tools) {
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;
847
+ for (const tool of tools) {
826
848
  registerToolCommand(tool);
827
849
  }
828
850
  program.parse();
@@ -855,16 +877,22 @@ async function main(): Promise<void> {
855
877
 
856
878
  // Register tool commands from cache (after potential auto-sync)
857
879
  const toolsCache = loadToolsCache();
858
- for (const tool of toolsCache.tools) {
880
+ const projectPath: string | undefined = syncGlobalOptions.projectPath;
881
+ for (const tool of filterEnabledTools(toolsCache.tools, projectPath)) {
859
882
  registerToolCommand(tool);
860
883
  }
861
884
 
862
- if (cmdName && !commandExists(cmdName)) {
885
+ if (cmdName && !commandExists(cmdName, projectPath)) {
886
+ if (!isToolEnabled(cmdName, projectPath)) {
887
+ printToolDisabledError(cmdName);
888
+ process.exit(1);
889
+ }
890
+
863
891
  console.log(`\x1b[33mUnknown command '${cmdName}'. Syncing tools from Unity...\x1b[0m`);
864
892
  try {
865
893
  await syncTools(syncGlobalOptions);
866
894
  const newCache = loadToolsCache();
867
- const tool = newCache.tools.find((t) => t.name === cmdName);
895
+ const tool = filterEnabledTools(newCache.tools, projectPath).find((t) => t.name === cmdName);
868
896
  if (tool) {
869
897
  registerToolCommand(tool);
870
898
  console.log(`\x1b[32m✓ Found '${cmdName}' after sync.\x1b[0m\n`);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.67.5",
2
+ "version": "0.68.0",
3
3
  "tools": [
4
4
  {
5
5
  "name": "compile",
@@ -343,8 +343,8 @@
343
343
  }
344
344
  },
345
345
  {
346
- "name": "capture-window",
347
- "description": "Capture Unity EditorWindow and save as PNG",
346
+ "name": "screenshot",
347
+ "description": "Take a screenshot of Unity EditorWindow and save as PNG",
348
348
  "inputSchema": {
349
349
  "type": "object",
350
350
  "properties": {
@@ -367,6 +367,11 @@
367
367
  "contains"
368
368
  ],
369
369
  "default": "exact"
370
+ },
371
+ "OutputDirectory": {
372
+ "type": "string",
373
+ "description": "Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths.",
374
+ "default": ""
370
375
  }
371
376
  }
372
377
  }
@@ -392,33 +397,6 @@
392
397
  }
393
398
  }
394
399
  },
395
- {
396
- "name": "get-provider-details",
397
- "description": "Get Unity Search provider details",
398
- "inputSchema": {
399
- "type": "object",
400
- "properties": {
401
- "ProviderId": {
402
- "type": "string",
403
- "description": "Specific provider ID"
404
- },
405
- "ActiveOnly": {
406
- "type": "boolean",
407
- "description": "Only active providers"
408
- },
409
- "IncludeDescriptions": {
410
- "type": "boolean",
411
- "description": "Include descriptions",
412
- "default": true
413
- },
414
- "SortByPriority": {
415
- "type": "boolean",
416
- "description": "Sort by priority",
417
- "default": true
418
- }
419
- }
420
- }
421
- },
422
400
  {
423
401
  "name": "get-project-info",
424
402
  "description": "Get Unity project information",
@@ -6,4 +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
10
  ];
@@ -13,6 +13,7 @@ import { homedir } from 'os';
13
13
  import { TargetConfig } from './target-config.js';
14
14
  import { findUnityProjectRoot, getUnityProjectStatus } from '../project-root.js';
15
15
  import { DEPRECATED_SKILLS } from './deprecated-skills.js';
16
+ import { loadDisabledTools } from '../tool-settings-loader.js';
16
17
 
17
18
  export type SkillStatus = 'installed' | 'not_installed' | 'outdated';
18
19
 
@@ -25,6 +26,7 @@ export interface SkillInfo {
25
26
 
26
27
  export interface SkillDefinition {
27
28
  name: string;
29
+ toolName?: string;
28
30
  dirName: string;
29
31
  content: string;
30
32
  sourcePath: string;
@@ -145,13 +147,13 @@ export function getSkillStatus(
145
147
  }
146
148
 
147
149
  export function parseFrontmatter(content: string): Record<string, string | boolean> {
148
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
150
+ const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
149
151
  if (!frontmatterMatch) {
150
152
  return {};
151
153
  }
152
154
 
153
155
  const frontmatterMap = new Map<string, string | boolean>();
154
- const lines = frontmatterMatch[1].split('\n');
156
+ const lines = frontmatterMatch[1].split(/\r?\n/);
155
157
 
156
158
  for (const line of lines) {
157
159
  const colonIndex = line.indexOf(':');
@@ -239,10 +241,13 @@ function scanEditorFolderForSkills(
239
241
  }
240
242
 
241
243
  const name = typeof frontmatter.name === 'string' ? frontmatter.name : entry.name;
244
+ const toolName =
245
+ typeof frontmatter.toolName === 'string' ? frontmatter.toolName : undefined;
242
246
  const additionalFiles = collectSkillFolderFiles(skillDir);
243
247
 
244
248
  skills.push({
245
249
  name,
250
+ toolName,
246
251
  dirName: name,
247
252
  content,
248
253
  sourcePath: skillMdPath,
@@ -399,11 +404,19 @@ export function installAllSkills(target: TargetConfig, global: boolean): Install
399
404
  }
400
405
  }
401
406
 
407
+ // Global installs ignore project-local tool settings
408
+ const disabledTools: string[] = global ? [] : loadDisabledTools();
409
+
402
410
  const allSkills = collectAllSkills();
403
411
  const projectSkills = allSkills.filter((skill) => skill.sourceType === 'project');
404
412
  const nonProjectSkills = allSkills.filter((skill) => skill.sourceType !== 'project');
405
413
 
406
414
  for (const skill of allSkills) {
415
+ if (isSkillDisabledByToolSettings(skill, disabledTools)) {
416
+ uninstallSkill(skill, target, global);
417
+ continue;
418
+ }
419
+
407
420
  const status = getSkillStatus(skill, target, global);
408
421
 
409
422
  if (status === 'not_installed') {
@@ -422,6 +435,19 @@ export function installAllSkills(target: TargetConfig, global: boolean): Install
422
435
  return result;
423
436
  }
424
437
 
438
+ function isSkillDisabledByToolSettings(skill: SkillDefinition, disabledTools: string[]): boolean {
439
+ if (disabledTools.length === 0) {
440
+ return false;
441
+ }
442
+
443
+ const toolName: string | null =
444
+ skill.toolName ?? (skill.name.startsWith('uloop-') ? skill.name.slice('uloop-'.length) : null);
445
+ if (toolName === null) {
446
+ return false;
447
+ }
448
+ return disabledTools.includes(toolName);
449
+ }
450
+
425
451
  export interface UninstallResult {
426
452
  removed: number;
427
453
  notFound: number;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Loads tool toggle settings from .uloop/settings.tools.json.
3
+ * Used by CLI to filter out disabled tools from help and command registration.
4
+ */
5
+
6
+ // File paths are constructed from Unity project root, not from untrusted user input
7
+ /* eslint-disable security/detect-non-literal-fs-filename */
8
+
9
+ import { readFileSync } from 'fs';
10
+ import { join, resolve } from 'path';
11
+ import { findUnityProjectRoot } from './project-root.js';
12
+ import type { ToolDefinition } from './tool-cache.js';
13
+
14
+ const ULOOP_DIR = '.uloop';
15
+ const TOOL_SETTINGS_FILE = 'settings.tools.json';
16
+
17
+ interface ToolSettingsData {
18
+ disabledTools: string[];
19
+ }
20
+
21
+ export function loadDisabledTools(projectPath?: string): string[] {
22
+ const projectRoot: string | null =
23
+ projectPath !== undefined ? resolve(projectPath) : findUnityProjectRoot();
24
+ if (projectRoot === null) {
25
+ return [];
26
+ }
27
+
28
+ const settingsPath: string = join(projectRoot, ULOOP_DIR, TOOL_SETTINGS_FILE);
29
+
30
+ let content: string;
31
+ try {
32
+ content = readFileSync(settingsPath, 'utf-8');
33
+ } catch {
34
+ return [];
35
+ }
36
+
37
+ if (!content.trim()) {
38
+ return [];
39
+ }
40
+
41
+ let parsed: unknown;
42
+ try {
43
+ parsed = JSON.parse(content);
44
+ } catch {
45
+ return [];
46
+ }
47
+
48
+ if (typeof parsed !== 'object' || parsed === null) {
49
+ return [];
50
+ }
51
+
52
+ const data = parsed as ToolSettingsData;
53
+ if (!Array.isArray(data.disabledTools)) {
54
+ return [];
55
+ }
56
+
57
+ return data.disabledTools;
58
+ }
59
+
60
+ export function isToolEnabled(toolName: string, projectPath?: string): boolean {
61
+ const disabledTools: string[] = loadDisabledTools(projectPath);
62
+ return !disabledTools.includes(toolName);
63
+ }
64
+
65
+ export function filterEnabledTools(
66
+ tools: ToolDefinition[],
67
+ projectPath?: string,
68
+ ): ToolDefinition[] {
69
+ const disabledTools: string[] = loadDisabledTools(projectPath);
70
+ if (disabledTools.length === 0) {
71
+ return tools;
72
+ }
73
+ return tools.filter((tool) => !disabledTools.includes(tool.name));
74
+ }
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.67.5'; // x-release-please-version
7
+ export const VERSION = '0.68.0'; // x-release-please-version