cwtools-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,506 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createNodeHostServices = createNodeHostServices;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const fssync = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const cwtools_shared_1 = require("cwtools-shared");
41
+ const lspProcessHost_1 = require("./lspProcessHost");
42
+ const projectDetect_1 = require("./projectDetect");
43
+ const projectSettings_1 = require("./projectSettings");
44
+ const revalidation_1 = require("./revalidation");
45
+ const vscodeCache_1 = require("./vscodeCache");
46
+ function createNodeHostServices(config) {
47
+ const workspaceRoot = path.resolve(config.workspaceRoot);
48
+ const allowlist = config.allowedTools.length > 0
49
+ ? new Set(config.allowedTools)
50
+ : new Set(cwtools_shared_1.MCP_WRITE_TOOL_NAMES);
51
+ const filesystem = new NodeFilesystemHost(workspaceRoot, config.enableWrites);
52
+ const support = (0, projectDetect_1.detectProjectSupport)(workspaceRoot);
53
+ const enabled = support.supported || config.forceStart;
54
+ // Fall back to the VS Code cwtools extension's globalStorage cache when --cache
55
+ // is omitted, so the MCP reuses the vanilla cache the extension already built.
56
+ const autoCache = config.cachePath ?? (0, vscodeCache_1.detectExtensionCacheDir)(config.game);
57
+ const detectedRulesPath = (0, vscodeCache_1.detectExtensionRulesDir)(autoCache, config.game);
58
+ const rulesConfigDirs = uniqueStrings([config.rulesPath, detectedRulesPath].filter((item) => !!item));
59
+ const lspProcess = enabled
60
+ ? (0, lspProcessHost_1.createLspProcessHost)({
61
+ workspaceRoot,
62
+ game: config.game,
63
+ serverPath: config.serverPath,
64
+ cachePath: autoCache,
65
+ gamePath: config.gamePath,
66
+ bundledRulesPath: config.rulesPath,
67
+ })
68
+ : undefined;
69
+ const lsp = lspProcess
70
+ ?? (0, cwtools_shared_1.createUnavailableLspHost)('Workspace is not a recognised Paradox mod; CWTools is disabled here.');
71
+ if (!enabled) {
72
+ console.error(`[cwtools-mcp] info: tool calls will be rejected (no language server) — ${support.reason} (pass --force-start to override)`);
73
+ }
74
+ else if (!support.supported) {
75
+ console.error(`[cwtools-mcp] info: --force-start set; starting despite: ${support.reason}`);
76
+ }
77
+ else if (support.matchedAt && support.matchedAt !== workspaceRoot) {
78
+ console.error(`[cwtools-mcp] info: mod detected at ${support.matchedAt} (workspace = ${workspaceRoot})`);
79
+ }
80
+ if (enabled && !config.cachePath && autoCache) {
81
+ console.error(`[cwtools-mcp] info: auto-detected VS Code extension cache at ${autoCache}`);
82
+ }
83
+ // Surface the resolved workspace so it's clear which mod is analysed — when
84
+ // --workspace is omitted this is the process cwd Codex launched the server in.
85
+ console.error(`[cwtools-mcp] info: workspace = ${workspaceRoot}${config.workspaceRoot === process.cwd() ? ' (inherited cwd)' : ''}`);
86
+ return {
87
+ workspaceRoot,
88
+ readonlyMode: !config.enableWrites,
89
+ writesEnabled: config.enableWrites,
90
+ allowedWriteTools: allowlist,
91
+ projectSupported: enabled,
92
+ projectSupportReason: support.reason,
93
+ lsp,
94
+ diagnostics: new LspDiagnosticsHost(lsp, workspaceRoot, lspProcess ? new revalidation_1.RevalidationCoordinator(lsp, workspaceRoot, () => lspProcess.readyAtMs) : undefined),
95
+ filesystem,
96
+ indexing: new ThinNodeIndexHost(workspaceRoot),
97
+ rules: {
98
+ gameId: config.game,
99
+ configDirs: rulesConfigDirs,
100
+ async readTextFile(filePath) {
101
+ if (!fssync.existsSync(filePath))
102
+ return { content: '', hasBom: false, exists: false };
103
+ const content = await fs.readFile(filePath, 'utf8');
104
+ return { content, hasBom: content.charCodeAt(0) === 0xfeff, exists: true };
105
+ },
106
+ async listCwtFiles(root, options) {
107
+ const limit = Math.max(1, Math.min(options?.limit ?? 1000, 5000));
108
+ const results = [];
109
+ await walk(root, async (filePath) => {
110
+ if (results.length >= limit)
111
+ return;
112
+ if (filePath.toLowerCase().endsWith('.cwt'))
113
+ results.push(filePath);
114
+ });
115
+ return results;
116
+ },
117
+ },
118
+ vanillaCache: probeVanillaCache(workspaceRoot, { ...config, cachePath: autoCache }),
119
+ now: () => Date.now(),
120
+ log: (level, message, data) => {
121
+ if (level === 'debug')
122
+ return;
123
+ const suffix = data === undefined ? '' : ` ${JSON.stringify(data)}`;
124
+ console.error(`[cwtools-mcp] ${level}: ${message}${suffix}`);
125
+ },
126
+ dispose: () => lspProcess?.dispose(),
127
+ };
128
+ }
129
+ function uniqueStrings(values) {
130
+ const seen = new Set();
131
+ const results = [];
132
+ for (const value of values) {
133
+ const key = process.platform === 'win32' ? value.toLowerCase() : value;
134
+ if (seen.has(key))
135
+ continue;
136
+ seen.add(key);
137
+ results.push(value);
138
+ }
139
+ return results;
140
+ }
141
+ // Resolve vanilla-cache availability up front: a pre-built <game>.cwb under the
142
+ // rules-cache root means vanilla data loads; a valid --game-path means the server
143
+ // can build it; otherwise results are mod-only. Mirrors GameLoader.getCachedFiles
144
+ // (cache file lives at the rules-cache root) and Program.fs cache.<game> handling.
145
+ function probeVanillaCache(workspaceRoot, config) {
146
+ const cacheFileName = (0, cwtools_shared_1.vanillaCacheFileName)(config.game);
147
+ const rulesCacheRoot = (0, lspProcessHost_1.resolveRulesCacheRoot)({ cachePath: config.cachePath, workspaceRoot });
148
+ const cacheFile = cacheFileName ? path.join(rulesCacheRoot, cacheFileName) : undefined;
149
+ const cacheExists = !!cacheFile && fssync.existsSync(cacheFile);
150
+ const gamePathValid = !!config.gamePath && fssync.existsSync(config.gamePath);
151
+ if (cacheExists) {
152
+ return { available: true, source: 'mod_plus_vanilla', cacheFile, reason: 'Loaded a pre-built vanilla cache.' };
153
+ }
154
+ if (gamePathValid) {
155
+ return {
156
+ available: true,
157
+ source: 'mod_plus_vanilla',
158
+ cacheFile,
159
+ gamePath: config.gamePath,
160
+ reason: 'No pre-built cache; the server will build one from --game-path on first load (slow).',
161
+ };
162
+ }
163
+ return {
164
+ available: false,
165
+ source: 'mod_only',
166
+ cacheFile,
167
+ reason: 'No vanilla cache and no --game-path/--cache; results reflect mod files only.',
168
+ };
169
+ }
170
+ class LspDiagnosticsHost {
171
+ constructor(lsp, workspaceRoot, revalidation) {
172
+ this.lsp = lsp;
173
+ this.workspaceRoot = workspaceRoot;
174
+ this.revalidation = revalidation;
175
+ }
176
+ async getDiagnostics(filter = {}) {
177
+ if (this.revalidation) {
178
+ const absFile = filter.file
179
+ ? (0, cwtools_shared_1.resolveWorkspacePath)(this.workspaceRoot, filter.file).resolvedPath ?? undefined
180
+ : undefined;
181
+ await this.revalidation.ensureFresh(absFile);
182
+ }
183
+ return this.applyIgnoreList(await this.queryDiagnostics(filter));
184
+ }
185
+ applyIgnoreList(result) {
186
+ if (!result.ok || result.diagnostics.length === 0)
187
+ return result;
188
+ return (0, projectSettings_1.applyDiagnosticIgnoreList)(result, (0, projectSettings_1.readIgnoredDiagnostics)(this.workspaceRoot));
189
+ }
190
+ async queryDiagnostics(filter) {
191
+ const file = filter.file;
192
+ if (file) {
193
+ const resolution = (0, cwtools_shared_1.resolveWorkspacePath)(this.workspaceRoot, file);
194
+ const filePath = resolution.resolvedPath ?? file;
195
+ const raw = asRecord(await this.lsp.executeCommand('cwtools.ai.getDiagnosticsFresh', [(0, lspProcessHost_1.pathToFileUri)(filePath)], { timeoutMs: 20000 }));
196
+ return normalizeDiagnosticsFresh(raw);
197
+ }
198
+ // Whole-workspace: aggregate cached per-file diagnostics from the server. The
199
+ // server populated these for every file during load; getValidationStatus alone
200
+ // only reports freshness, never the diagnostics themselves.
201
+ const severity = filter.severity === 'information' ? 'info' : filter.severity ?? 'all';
202
+ const limit = typeof filter.limit === 'number' ? filter.limit : 1000;
203
+ const raw = asRecord(await this.lsp.executeCommand('cwtools.ai.getAllDiagnostics', [severity, limit], { timeoutMs: 30000 }));
204
+ if (raw.ok === false || raw.status === 'unavailable') {
205
+ return unavailableDiagnostics(raw);
206
+ }
207
+ return {
208
+ ok: true,
209
+ status: 'fresh',
210
+ diagnostics: Array.isArray(raw.diagnostics) ? raw.diagnostics.map(normalizeDiagnosticRecord) : [],
211
+ totalCount: numberOrUndefined(raw.totalCount),
212
+ truncated: raw.truncated === true,
213
+ freshness: {
214
+ value: 'fresh',
215
+ pendingKinds: [],
216
+ epoch: numberOrUndefined(raw.epoch),
217
+ },
218
+ };
219
+ }
220
+ }
221
+ function normalizeDiagnosticsFresh(raw) {
222
+ if (raw.ok === false || raw.status === 'unavailable') {
223
+ return unavailableDiagnostics(raw);
224
+ }
225
+ const freshness = String(raw.freshness ?? 'unavailable');
226
+ return {
227
+ ok: true,
228
+ status: freshness,
229
+ diagnostics: Array.isArray(raw.diagnostics) ? raw.diagnostics.map(normalizeDiagnosticRecord) : [],
230
+ freshness: {
231
+ value: freshness,
232
+ pendingKinds: asStringArray(raw.pendingGlobalKinds),
233
+ validatedVersion: numberOrUndefined(raw.validatedVersion),
234
+ epoch: numberOrUndefined(raw.epoch),
235
+ updatedAt: numberOrUndefined(raw.updatedAtUnixMs),
236
+ },
237
+ };
238
+ }
239
+ function asRecord(value) {
240
+ return value && typeof value === 'object' && !Array.isArray(value)
241
+ ? value
242
+ : {
243
+ ok: false,
244
+ status: 'unavailable',
245
+ error: {
246
+ code: 'lsp_no_response',
247
+ message: 'LSP returned no diagnostics response.',
248
+ },
249
+ };
250
+ }
251
+ function unavailableDiagnostics(raw) {
252
+ const error = raw.error && typeof raw.error === 'object' ? raw.error : {};
253
+ return {
254
+ ok: false,
255
+ status: 'unavailable',
256
+ diagnostics: [],
257
+ error: {
258
+ code: String(error.code ?? 'lsp_unavailable'),
259
+ message: String(error.message ?? 'Diagnostics are unavailable.'),
260
+ },
261
+ };
262
+ }
263
+ function normalizeDiagnosticRecord(value) {
264
+ const record = value && typeof value === 'object' ? value : {};
265
+ const severity = String(record.severity ?? 'information');
266
+ return {
267
+ file: typeof record.file === 'string' ? record.file : undefined,
268
+ line: numberOrUndefined(record.line),
269
+ column: numberOrUndefined(record.column),
270
+ severity: severity === 'error' || severity === 'warning' || severity === 'hint' ? severity : 'information',
271
+ code: typeof record.code === 'string' ? record.code : undefined,
272
+ message: String(record.message ?? ''),
273
+ source: typeof record.source === 'string' ? record.source : 'cwtools',
274
+ };
275
+ }
276
+ function asStringArray(value) {
277
+ return Array.isArray(value) ? value.map(item => String(item)) : [];
278
+ }
279
+ function numberOrUndefined(value) {
280
+ if (typeof value === 'number')
281
+ return value;
282
+ if (typeof value === 'string' && value.trim()) {
283
+ const parsed = Number(value);
284
+ return Number.isFinite(parsed) ? parsed : undefined;
285
+ }
286
+ return undefined;
287
+ }
288
+ class NodeFilesystemHost {
289
+ constructor(workspaceRoot, writesEnabled) {
290
+ this.workspaceRoot = workspaceRoot;
291
+ this.writesEnabled = writesEnabled;
292
+ }
293
+ async readTextFile(filePath) {
294
+ const resolved = this.resolve(filePath);
295
+ if (!fssync.existsSync(resolved)) {
296
+ return { content: '', hasBom: false, exists: false };
297
+ }
298
+ const content = await fs.readFile(resolved, 'utf8');
299
+ return {
300
+ content,
301
+ hasBom: content.charCodeAt(0) === 0xfeff,
302
+ exists: true,
303
+ };
304
+ }
305
+ async writeTextFile(filePath, content) {
306
+ if (!this.writesEnabled) {
307
+ throw new Error('writes_disabled');
308
+ }
309
+ const resolved = this.resolve(filePath);
310
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
311
+ await fs.writeFile(resolved, content, 'utf8');
312
+ }
313
+ async list(dirPath) {
314
+ const resolved = this.resolve(dirPath);
315
+ const entries = await fs.readdir(resolved, { withFileTypes: true });
316
+ return entries.map(entry => {
317
+ const fullPath = path.join(resolved, entry.name);
318
+ const stat = fssync.existsSync(fullPath) ? fssync.statSync(fullPath) : undefined;
319
+ return {
320
+ name: entry.name,
321
+ type: entry.isDirectory() ? 'directory' : 'file',
322
+ size: stat?.isFile() ? stat.size : undefined,
323
+ };
324
+ });
325
+ }
326
+ async glob(pattern, options) {
327
+ const limit = Math.max(1, Math.min(options?.limit ?? 500, 5000));
328
+ const suffix = pattern.startsWith('**/*') ? pattern.slice(4) : pattern;
329
+ const results = [];
330
+ await walk(this.workspaceRoot, async (filePath) => {
331
+ if (results.length >= limit)
332
+ return;
333
+ if (!suffix || filePath.replace(/\\/g, '/').endsWith(suffix.replace(/\\/g, '/'))) {
334
+ results.push(path.relative(this.workspaceRoot, filePath).replace(/\\/g, '/'));
335
+ }
336
+ });
337
+ return results;
338
+ }
339
+ resolve(filePath) {
340
+ const resolution = (0, cwtools_shared_1.resolveWorkspacePath)(this.workspaceRoot, filePath);
341
+ if (!resolution.ok || !resolution.resolvedPath) {
342
+ throw new Error(`Path '${filePath}' is outside the workspace root.`);
343
+ }
344
+ return resolution.resolvedPath;
345
+ }
346
+ }
347
+ class ThinNodeIndexHost {
348
+ constructor(workspaceRoot) {
349
+ this.workspaceRoot = workspaceRoot;
350
+ }
351
+ async queryWorkspace(query) {
352
+ const limit = Math.max(1, Math.min(query.limit ?? 50, 200));
353
+ const entries = [];
354
+ await walk(this.workspaceRoot, async (filePath) => {
355
+ if (entries.length >= limit)
356
+ return;
357
+ const rel = path.relative(this.workspaceRoot, filePath).replace(/\\/g, '/');
358
+ if (!isWorkspaceIndexFile(rel))
359
+ return;
360
+ if (query.directory && !rel.toLowerCase().includes(query.directory.toLowerCase()))
361
+ return;
362
+ const content = await fs.readFile(filePath, 'utf8').catch(() => '');
363
+ const lines = content.split(/\r?\n/);
364
+ for (let index = 0; index < lines.length && entries.length < limit; index++) {
365
+ const symbol = parseSymbol(lines[index] ?? '', rel);
366
+ if (!symbol)
367
+ continue;
368
+ if (!matchesName(symbol.name, query.name, query))
369
+ continue;
370
+ if (query.kind && symbol.kind !== query.kind)
371
+ continue;
372
+ if (query.category && symbol.category !== query.category)
373
+ continue;
374
+ if (query.source && symbol.source !== query.source)
375
+ continue;
376
+ entries.push({
377
+ ...symbol,
378
+ file: rel,
379
+ line: index,
380
+ origin: 'workspace',
381
+ updatedAt: fssync.statSync(filePath).mtimeMs,
382
+ });
383
+ }
384
+ });
385
+ return {
386
+ status: 'ready',
387
+ totalCount: entries.length,
388
+ entries,
389
+ indexedSymbolNames: entries.length,
390
+ indexUpdatedAt: Date.now(),
391
+ _hint: 'Phase 0 thin Node index only returns lightweight workspace symbols; LSP/index commands remain the Phase 1 source of truth.',
392
+ };
393
+ }
394
+ async queryLocalisation(query) {
395
+ const limit = Math.max(1, Math.min(query.limit ?? 20, 100));
396
+ const entries = [];
397
+ await walk(this.workspaceRoot, async (filePath) => {
398
+ if (entries.length >= limit)
399
+ return;
400
+ const rel = path.relative(this.workspaceRoot, filePath).replace(/\\/g, '/');
401
+ if (!isLocalisationFile(rel))
402
+ return;
403
+ const raw = await fs.readFile(filePath, 'utf8').catch(() => '');
404
+ const content = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
405
+ const lines = content.split(/\r?\n/);
406
+ let language = '';
407
+ for (let index = 0; index < lines.length && entries.length < limit; index++) {
408
+ const line = lines[index] ?? '';
409
+ const header = line.match(/^\s*(l_[a-z_]+):/i);
410
+ if (header?.[1])
411
+ language = header[1];
412
+ const match = line.match(/^\s*([\w.-]+):\d*\s*"([^"]*)"/);
413
+ if (!match?.[1])
414
+ continue;
415
+ if (query.language && query.language !== language)
416
+ continue;
417
+ if (!matchesLocalisationKey(match[1], query))
418
+ continue;
419
+ entries.push({
420
+ key: match[1],
421
+ value: match[2] ?? '',
422
+ file: rel,
423
+ line: index,
424
+ language,
425
+ });
426
+ }
427
+ });
428
+ return {
429
+ status: 'ready',
430
+ totalCount: entries.length,
431
+ entries,
432
+ indexUpdatedAt: Date.now(),
433
+ _hint: 'Phase 0 thin Node localisation index scans workspace YML files only.',
434
+ };
435
+ }
436
+ }
437
+ async function walk(root, visit) {
438
+ if (!fssync.existsSync(root))
439
+ return;
440
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
441
+ for (const entry of entries) {
442
+ if (entry.name.startsWith('.') && entry.name !== '.cwtools' && entry.name !== '.cwtools-ai')
443
+ continue;
444
+ if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'coverage')
445
+ continue;
446
+ const fullPath = path.join(root, entry.name);
447
+ if (entry.isDirectory()) {
448
+ await walk(fullPath, visit);
449
+ }
450
+ else {
451
+ await visit(fullPath);
452
+ }
453
+ }
454
+ }
455
+ function isWorkspaceIndexFile(relativePath) {
456
+ return /\.(txt|gfx|asset|gui)$/i.test(relativePath);
457
+ }
458
+ function isLocalisationFile(relativePath) {
459
+ const normalized = relativePath.toLowerCase();
460
+ return /\.yml$/.test(normalized)
461
+ && (normalized.startsWith('localisation/')
462
+ || normalized.startsWith('localisation_synced/')
463
+ || normalized.startsWith('localization/'));
464
+ }
465
+ function parseSymbol(line, relativePath) {
466
+ const namespace = line.match(/^\s*namespace\s*=\s*"?([\w.:-]+)"?/);
467
+ if (namespace?.[1]) {
468
+ return { name: namespace[1], kind: 'namespace', source: 'script', category: 'event' };
469
+ }
470
+ const id = line.match(/^\s*id\s*=\s*"?([\w.:-]+)"?/);
471
+ if (id?.[1]) {
472
+ return { name: id[1], kind: 'event', source: 'script', category: 'event' };
473
+ }
474
+ const topLevel = line.match(/^([@\w][\w.:-]*)\s*=/);
475
+ if (topLevel?.[1]) {
476
+ const source = relativePath.endsWith('.gui') ? 'gui' : relativePath.endsWith('.gfx') || relativePath.endsWith('.asset') ? 'asset' : 'script';
477
+ return { name: topLevel[1], kind: 'symbol', source, category: source === 'script' ? 'script' : source };
478
+ }
479
+ const sprite = line.match(/\bname\s*=\s*"?(GFX_[\w.:-]+)"?/);
480
+ if (sprite?.[1]) {
481
+ return { name: sprite[1], kind: 'sprite', source: 'asset', category: 'asset' };
482
+ }
483
+ return null;
484
+ }
485
+ function matchesName(name, queryName, query) {
486
+ if (!queryName)
487
+ return true;
488
+ const needle = queryName.toLowerCase();
489
+ const haystack = name.toLowerCase();
490
+ if (query.exact)
491
+ return haystack === needle;
492
+ if (query.prefix)
493
+ return haystack.startsWith(needle);
494
+ return haystack.includes(needle);
495
+ }
496
+ function matchesLocalisationKey(key, query) {
497
+ if (!query.key)
498
+ return true;
499
+ const haystack = query.caseSensitive ? key : key.toLowerCase();
500
+ const needle = query.caseSensitive ? query.key : query.key.toLowerCase();
501
+ if (query.prefix)
502
+ return haystack.startsWith(needle);
503
+ if (query.contains)
504
+ return haystack.includes(needle);
505
+ return haystack === needle || haystack.includes(needle);
506
+ }
@@ -0,0 +1,7 @@
1
+ export interface ProjectSupport {
2
+ supported: boolean;
3
+ reason: string;
4
+ markers: string[];
5
+ matchedAt?: string;
6
+ }
7
+ export declare function detectProjectSupport(workspaceRoot: string): ProjectSupport;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.detectProjectSupport = detectProjectSupport;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ // Strong, unambiguous "this directory is a mod root" signals.
40
+ const ROOT_FILE_MARKERS = ['descriptor.mod'];
41
+ const ROOT_NESTED_FILE_MARKERS = [path.join('.metadata', 'metadata.json')];
42
+ const ROOT_DIR_MARKERS = ['.cwtools', '.cwtools-ai'];
43
+ // PDX content dirs — specific enough at a mod root, but too generic to trust in a
44
+ // far-off ancestor. Generic graphics dirs (gfx/, interface/) are excluded.
45
+ const CONTENT_DIR_MARKERS = ['common', 'events', 'localisation', 'localization'];
46
+ const MAX_ANCESTORS = 6; // how far up to look for a mod root above the cwd
47
+ const MAX_CHILDREN = 64; // how many immediate subdirs to probe when cwd is a parent
48
+ function isFile(p) {
49
+ try {
50
+ return fs.statSync(p).isFile();
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ function isDir(p) {
57
+ try {
58
+ return fs.statSync(p).isDirectory();
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ // Markers present directly in `dir`. `includeContent` adds the PDX content dirs,
65
+ // which we only trust at the workspace root / immediate children, not ancestors.
66
+ function markersAt(dir, includeContent) {
67
+ const found = [];
68
+ for (const f of ROOT_FILE_MARKERS) {
69
+ if (isFile(path.join(dir, f)))
70
+ found.push(f);
71
+ }
72
+ for (const f of ROOT_NESTED_FILE_MARKERS) {
73
+ if (isFile(path.join(dir, f)))
74
+ found.push(f.replace(/\\/g, '/'));
75
+ }
76
+ for (const d of ROOT_DIR_MARKERS) {
77
+ if (isDir(path.join(dir, d)))
78
+ found.push(`${d}/`);
79
+ }
80
+ if (includeContent) {
81
+ for (const d of CONTENT_DIR_MARKERS) {
82
+ if (isDir(path.join(dir, d)))
83
+ found.push(`${d}/`);
84
+ }
85
+ }
86
+ return found;
87
+ }
88
+ // Decide whether `workspaceRoot` is (or contains, or sits inside) a Paradox mod the
89
+ // CWTools tools can serve. Cheap synchronous stat checks only — run once at startup
90
+ // before anything heavy (the language server) is spawned. Codex usually launches
91
+ // the MCP without --workspace, so the cwd may be the mod root, an ancestor, or a
92
+ // parent holding the mod in a subfolder; all three are accepted.
93
+ function detectProjectSupport(workspaceRoot) {
94
+ // 1. The workspace root itself (the common, correct case).
95
+ const atRoot = markersAt(workspaceRoot, true);
96
+ if (atRoot.length > 0) {
97
+ return {
98
+ supported: true,
99
+ markers: atRoot,
100
+ matchedAt: workspaceRoot,
101
+ reason: `Detected Paradox mod markers at the workspace root: ${atRoot.join(', ')}.`,
102
+ };
103
+ }
104
+ // 2. An ancestor mod root (cwd is a subfolder of the mod). Strong markers only.
105
+ let dir = workspaceRoot;
106
+ for (let i = 0; i < MAX_ANCESTORS; i++) {
107
+ const parent = path.dirname(dir);
108
+ if (!parent || parent === dir)
109
+ break;
110
+ dir = parent;
111
+ const atAncestor = markersAt(dir, false);
112
+ if (atAncestor.length > 0) {
113
+ return {
114
+ supported: true,
115
+ markers: atAncestor,
116
+ matchedAt: dir,
117
+ reason: `Detected a Paradox mod root above the workspace (${dir}): ${atAncestor.join(', ')}.`,
118
+ };
119
+ }
120
+ }
121
+ // 3. An immediate child that is a mod root (cwd is a parent holding the mod).
122
+ let children = [];
123
+ try {
124
+ children = fs.readdirSync(workspaceRoot, { withFileTypes: true });
125
+ }
126
+ catch {
127
+ children = [];
128
+ }
129
+ let probed = 0;
130
+ for (const child of children) {
131
+ if (!child.isDirectory() || child.name === 'node_modules')
132
+ continue;
133
+ if (probed++ >= MAX_CHILDREN)
134
+ break;
135
+ const childDir = path.join(workspaceRoot, child.name);
136
+ const atChild = markersAt(childDir, true);
137
+ if (atChild.length > 0) {
138
+ return {
139
+ supported: true,
140
+ markers: atChild.map(m => `${child.name}/${m}`),
141
+ matchedAt: childDir,
142
+ reason: `Detected a Paradox mod in a subfolder (${child.name}/): ${atChild.join(', ')}.`,
143
+ };
144
+ }
145
+ }
146
+ return {
147
+ supported: false,
148
+ markers: [],
149
+ reason: `No Paradox mod markers (descriptor.mod, common/, events/, localisation/, .cwtools/) at, above, or directly under '${workspaceRoot}'.`,
150
+ };
151
+ }
@@ -0,0 +1,11 @@
1
+ import type { DiagnosticsQueryResult } from 'cwtools-shared';
2
+ export declare function getExtensionSetting(workspaceRoot: string, subKey: string): unknown;
3
+ export declare function readIgnoredDiagnostics(workspaceRoot: string): string[];
4
+ export interface LocalisationConfig {
5
+ languages: string[];
6
+ source: 'settings' | 'detected' | 'default';
7
+ }
8
+ export declare function resolveLocalisationLanguages(workspaceRoot: string): LocalisationConfig;
9
+ export declare function resolveGeneratedStrings(workspaceRoot: string): string;
10
+ export declare function resolveExperimental(workspaceRoot: string): boolean;
11
+ export declare function applyDiagnosticIgnoreList(result: DiagnosticsQueryResult, ignored: readonly string[]): DiagnosticsQueryResult;