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.
- package/README.md +279 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +76 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +212 -0
- package/dist/cwtools-mcp.cjs +49480 -0
- package/dist/hosts/lspProcessHost.d.ts +49 -0
- package/dist/hosts/lspProcessHost.js +412 -0
- package/dist/hosts/nodeHostServices.d.ts +3 -0
- package/dist/hosts/nodeHostServices.js +506 -0
- package/dist/hosts/projectDetect.d.ts +7 -0
- package/dist/hosts/projectDetect.js +151 -0
- package/dist/hosts/projectSettings.d.ts +11 -0
- package/dist/hosts/projectSettings.js +265 -0
- package/dist/hosts/revalidation.d.ts +12 -0
- package/dist/hosts/revalidation.js +157 -0
- package/dist/hosts/vscodeCache.d.ts +5 -0
- package/dist/hosts/vscodeCache.js +153 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +25 -0
- package/dist/mcp/bridgeProxy.d.ts +4 -0
- package/dist/mcp/bridgeProxy.js +281 -0
- package/dist/mcp/resources.d.ts +15 -0
- package/dist/mcp/resources.js +151 -0
- package/dist/mcp/toolHandlers.d.ts +9 -0
- package/dist/mcp/toolHandlers.js +58 -0
- package/dist/mcp/toolRegistrar.d.ts +2 -0
- package/dist/mcp/toolRegistrar.js +7 -0
- package/dist/mcp/transportHttp.d.ts +7 -0
- package/dist/mcp/transportHttp.js +101 -0
- package/dist/mcp/transportStdio.d.ts +2 -0
- package/dist/mcp/transportStdio.js +8 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +85 -0
- package/package.json +46 -0
|
@@ -0,0 +1,265 @@
|
|
|
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.getExtensionSetting = getExtensionSetting;
|
|
37
|
+
exports.readIgnoredDiagnostics = readIgnoredDiagnostics;
|
|
38
|
+
exports.resolveLocalisationLanguages = resolveLocalisationLanguages;
|
|
39
|
+
exports.resolveGeneratedStrings = resolveGeneratedStrings;
|
|
40
|
+
exports.resolveExperimental = resolveExperimental;
|
|
41
|
+
exports.applyDiagnosticIgnoreList = applyDiagnosticIgnoreList;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
function parseJsonc(text) {
|
|
45
|
+
const s = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
46
|
+
let out = '';
|
|
47
|
+
let i = 0;
|
|
48
|
+
const n = s.length;
|
|
49
|
+
let pendingCommaAt = -1; // index in `out` of a comma that may be trailing
|
|
50
|
+
const dropPendingIfClosing = (ch) => {
|
|
51
|
+
if (pendingCommaAt >= 0) {
|
|
52
|
+
if (ch === '}' || ch === ']')
|
|
53
|
+
out = out.slice(0, pendingCommaAt) + out.slice(pendingCommaAt + 1);
|
|
54
|
+
pendingCommaAt = -1;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
while (i < n) {
|
|
58
|
+
const c = s[i];
|
|
59
|
+
if (c === '"') {
|
|
60
|
+
dropPendingIfClosing(c);
|
|
61
|
+
out += c;
|
|
62
|
+
i++;
|
|
63
|
+
while (i < n) {
|
|
64
|
+
const d = s[i];
|
|
65
|
+
out += d;
|
|
66
|
+
i++;
|
|
67
|
+
if (d === '\\') {
|
|
68
|
+
out += s[i] ?? '';
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (d === '"')
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (c === '/' && s[i + 1] === '/') {
|
|
78
|
+
i += 2;
|
|
79
|
+
while (i < n && s[i] !== '\n')
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === '/' && s[i + 1] === '*') {
|
|
84
|
+
i += 2;
|
|
85
|
+
while (i < n && !(s[i] === '*' && s[i + 1] === '/'))
|
|
86
|
+
i++;
|
|
87
|
+
i += 2;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === ' ' || c === '\t' || c === '\r' || c === '\n') {
|
|
91
|
+
out += c;
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (c === ',') {
|
|
96
|
+
pendingCommaAt = out.length;
|
|
97
|
+
out += c;
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
dropPendingIfClosing(c);
|
|
102
|
+
out += c;
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
return JSON.parse(out);
|
|
106
|
+
}
|
|
107
|
+
const cache = new Map();
|
|
108
|
+
const SETTINGS_NAMESPACE = 'stellarisLanguageServices';
|
|
109
|
+
const LEGACY_SETTINGS_NAMESPACE = 'cwtools';
|
|
110
|
+
function readSettingsObject(workspaceRoot) {
|
|
111
|
+
const file = path.join(workspaceRoot, '.vscode', 'settings.json');
|
|
112
|
+
let mtimeMs;
|
|
113
|
+
try {
|
|
114
|
+
mtimeMs = fs.statSync(file).mtimeMs;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
cache.delete(file);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const cached = cache.get(file);
|
|
121
|
+
if (cached && cached.mtimeMs === mtimeMs)
|
|
122
|
+
return cached.settings;
|
|
123
|
+
let settings;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = parseJsonc(fs.readFileSync(file, 'utf8'));
|
|
126
|
+
settings = parsed && typeof parsed === 'object' ? parsed : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
settings = undefined;
|
|
130
|
+
}
|
|
131
|
+
cache.set(file, { mtimeMs, settings });
|
|
132
|
+
return settings;
|
|
133
|
+
}
|
|
134
|
+
function resolveDotted(obj, fullKey) {
|
|
135
|
+
if (fullKey in obj)
|
|
136
|
+
return obj[fullKey];
|
|
137
|
+
const segs = fullKey.split('.');
|
|
138
|
+
for (let i = 1; i < segs.length; i++) {
|
|
139
|
+
const head = segs.slice(0, i).join('.');
|
|
140
|
+
if (head in obj) {
|
|
141
|
+
const child = obj[head];
|
|
142
|
+
if (child && typeof child === 'object') {
|
|
143
|
+
const r = resolveDotted(child, segs.slice(i).join('.'));
|
|
144
|
+
if (r !== undefined)
|
|
145
|
+
return r;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
function getExtensionSetting(workspaceRoot, subKey) {
|
|
152
|
+
const settings = readSettingsObject(workspaceRoot);
|
|
153
|
+
if (!settings)
|
|
154
|
+
return undefined;
|
|
155
|
+
const current = resolveDotted(settings, `${SETTINGS_NAMESPACE}.${subKey}`);
|
|
156
|
+
return current !== undefined
|
|
157
|
+
? current
|
|
158
|
+
: resolveDotted(settings, `${LEGACY_SETTINGS_NAMESPACE}.${subKey}`);
|
|
159
|
+
}
|
|
160
|
+
function asNonEmptyStringArray(value) {
|
|
161
|
+
return Array.isArray(value)
|
|
162
|
+
? value.filter((v) => typeof v === 'string' && v.length > 0)
|
|
163
|
+
: [];
|
|
164
|
+
}
|
|
165
|
+
function readIgnoredDiagnostics(workspaceRoot) {
|
|
166
|
+
return asNonEmptyStringArray(getExtensionSetting(workspaceRoot, 'ai.ignoredDiagnostics'));
|
|
167
|
+
}
|
|
168
|
+
const LANG_TAG_MAP = [
|
|
169
|
+
['English', 'english'],
|
|
170
|
+
['French', 'french'],
|
|
171
|
+
['German', 'german'],
|
|
172
|
+
['Spanish', 'spanish'],
|
|
173
|
+
['Russian', 'russian'],
|
|
174
|
+
['Braz_Por', 'braz_por'],
|
|
175
|
+
['Polish', 'polish'],
|
|
176
|
+
['Chinese', 'simp_chinese'],
|
|
177
|
+
['Korean', 'korean'],
|
|
178
|
+
['Japanese', 'japanese'],
|
|
179
|
+
['Turkish', 'turkish'],
|
|
180
|
+
];
|
|
181
|
+
const LOC_DIRS = ['localisation', 'localisation_synced', 'localization'];
|
|
182
|
+
const MAX_LOC_FILES = 5000;
|
|
183
|
+
// Count localisation files by language tag and return the dominant non-English
|
|
184
|
+
// language, or undefined (English is the server default). Bounded recursive scan.
|
|
185
|
+
function detectLocalisationLanguage(workspaceRoot) {
|
|
186
|
+
const counts = new Map();
|
|
187
|
+
let scanned = 0;
|
|
188
|
+
const visit = (dir) => {
|
|
189
|
+
if (scanned >= MAX_LOC_FILES)
|
|
190
|
+
return;
|
|
191
|
+
let entries;
|
|
192
|
+
try {
|
|
193
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const e of entries) {
|
|
199
|
+
if (scanned >= MAX_LOC_FILES)
|
|
200
|
+
return;
|
|
201
|
+
const full = path.join(dir, e.name);
|
|
202
|
+
if (e.isDirectory()) {
|
|
203
|
+
visit(full);
|
|
204
|
+
}
|
|
205
|
+
else if (e.isFile() && e.name.toLowerCase().endsWith('.yml')) {
|
|
206
|
+
scanned++;
|
|
207
|
+
const base = e.name.toLowerCase();
|
|
208
|
+
for (const [name, tag] of LANG_TAG_MAP) {
|
|
209
|
+
if (base.endsWith(`l_${tag}.yml`)) {
|
|
210
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
for (const d of LOC_DIRS)
|
|
218
|
+
visit(path.join(workspaceRoot, d));
|
|
219
|
+
let best;
|
|
220
|
+
let bestCount = 0;
|
|
221
|
+
for (const [name, count] of counts) {
|
|
222
|
+
if (count > bestCount) {
|
|
223
|
+
bestCount = count;
|
|
224
|
+
best = name;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return best && best !== 'English' ? best : undefined;
|
|
228
|
+
}
|
|
229
|
+
function resolveLocalisationLanguages(workspaceRoot) {
|
|
230
|
+
const fromSettings = asNonEmptyStringArray(getExtensionSetting(workspaceRoot, 'localisation.languages'));
|
|
231
|
+
if (fromSettings.length > 0)
|
|
232
|
+
return { languages: fromSettings, source: 'settings' };
|
|
233
|
+
const detected = detectLocalisationLanguage(workspaceRoot);
|
|
234
|
+
if (detected)
|
|
235
|
+
return { languages: [detected], source: 'detected' };
|
|
236
|
+
return { languages: ['English'], source: 'default' };
|
|
237
|
+
}
|
|
238
|
+
function resolveGeneratedStrings(workspaceRoot) {
|
|
239
|
+
const v = getExtensionSetting(workspaceRoot, 'localisation.generated_strings');
|
|
240
|
+
return typeof v === 'string' && v.length > 0 ? v : 'replace';
|
|
241
|
+
}
|
|
242
|
+
// `stellarisLanguageServices.experimental` setting. Defaults ON for the MCP: it gates the incremental
|
|
243
|
+
// scripted-type refresh, which lets revalidation of scripted_triggers/effects/values
|
|
244
|
+
// patch the type index in milliseconds instead of triggering a full reload — so
|
|
245
|
+
// definition-file edits don't mis-report references. Honor an explicit opt-out.
|
|
246
|
+
function resolveExperimental(workspaceRoot) {
|
|
247
|
+
const v = getExtensionSetting(workspaceRoot, 'experimental');
|
|
248
|
+
return typeof v === 'boolean' ? v : true;
|
|
249
|
+
}
|
|
250
|
+
function applyDiagnosticIgnoreList(result, ignored) {
|
|
251
|
+
if (!result.ok || result.diagnostics.length === 0 || ignored.length === 0)
|
|
252
|
+
return result;
|
|
253
|
+
const kept = result.diagnostics.filter(d => !ignored.some(key => d.message.includes(key)));
|
|
254
|
+
const suppressed = result.diagnostics.length - kept.length;
|
|
255
|
+
if (suppressed === 0)
|
|
256
|
+
return result;
|
|
257
|
+
return {
|
|
258
|
+
...result,
|
|
259
|
+
diagnostics: kept,
|
|
260
|
+
suppressedCount: (result.suppressedCount ?? 0) + suppressed,
|
|
261
|
+
totalCount: typeof result.totalCount === 'number'
|
|
262
|
+
? Math.max(0, result.totalCount - suppressed)
|
|
263
|
+
: result.totalCount,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LspHost } from 'cwtools-shared';
|
|
2
|
+
export declare class RevalidationCoordinator {
|
|
3
|
+
private readonly lsp;
|
|
4
|
+
private readonly workspaceRoot;
|
|
5
|
+
private readonly readyAt;
|
|
6
|
+
private readonly lastReval;
|
|
7
|
+
constructor(lsp: LspHost, workspaceRoot: string, readyAt: () => number | undefined);
|
|
8
|
+
ensureFresh(absFile?: string): Promise<void>;
|
|
9
|
+
private changedSingle;
|
|
10
|
+
private changedAll;
|
|
11
|
+
private waitDrained;
|
|
12
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
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.RevalidationCoordinator = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const lspProcessHost_1 = require("./lspProcessHost");
|
|
40
|
+
// File extensions the language server validates — only these are worth revalidating.
|
|
41
|
+
const REVALIDATE_EXTS = new Set(['.txt', '.yml', '.gui', '.gfx', '.asset', '.shader', '.fxh']);
|
|
42
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage', '.git', '.cwtools', '.cwtools-ai', '.vscode']);
|
|
43
|
+
const MAX_WALK_FILES = 20000;
|
|
44
|
+
const MAX_BATCH = 200;
|
|
45
|
+
function numberOr(value, fallback) {
|
|
46
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
47
|
+
}
|
|
48
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
49
|
+
class RevalidationCoordinator {
|
|
50
|
+
constructor(lsp, workspaceRoot, readyAt) {
|
|
51
|
+
this.lsp = lsp;
|
|
52
|
+
this.workspaceRoot = workspaceRoot;
|
|
53
|
+
this.readyAt = readyAt;
|
|
54
|
+
this.lastReval = new Map();
|
|
55
|
+
}
|
|
56
|
+
// absFile: absolute path for a single-file query, or undefined for whole-project.
|
|
57
|
+
async ensureFresh(absFile) {
|
|
58
|
+
const baseline = this.readyAt();
|
|
59
|
+
if (baseline === undefined)
|
|
60
|
+
return; // server still loading; its load is the freshest state we have
|
|
61
|
+
const changed = absFile ? this.changedSingle(absFile, baseline) : this.changedAll(baseline);
|
|
62
|
+
if (changed.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
const batch = changed.slice(0, MAX_BATCH);
|
|
65
|
+
const uris = batch.map(c => (0, lspProcessHost_1.pathToFileUri)(c.path));
|
|
66
|
+
let baselineEpoch = 0;
|
|
67
|
+
try {
|
|
68
|
+
const res = await this.lsp.executeCommand('cwtools.ai.revalidateFiles', [uris], { timeoutMs: 10000 });
|
|
69
|
+
if (!res || res.ok !== true)
|
|
70
|
+
return; // older server without the command → plain read
|
|
71
|
+
baselineEpoch = numberOr(res.baselineEpoch, 0);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Mark seen so unchanged files are skipped next time.
|
|
77
|
+
for (const c of batch)
|
|
78
|
+
this.lastReval.set(c.path, c.mtimeMs);
|
|
79
|
+
await this.waitDrained(baselineEpoch);
|
|
80
|
+
}
|
|
81
|
+
changedSingle(absFile, baseline) {
|
|
82
|
+
let mtimeMs;
|
|
83
|
+
try {
|
|
84
|
+
const st = fs.statSync(absFile);
|
|
85
|
+
if (!st.isFile())
|
|
86
|
+
return [];
|
|
87
|
+
mtimeMs = st.mtimeMs;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const threshold = Math.max(baseline, this.lastReval.get(absFile) ?? 0);
|
|
93
|
+
return mtimeMs > threshold ? [{ path: absFile, mtimeMs }] : [];
|
|
94
|
+
}
|
|
95
|
+
changedAll(baseline) {
|
|
96
|
+
const out = [];
|
|
97
|
+
let scanned = 0;
|
|
98
|
+
const walk = (dir) => {
|
|
99
|
+
if (out.length >= MAX_BATCH || scanned >= MAX_WALK_FILES)
|
|
100
|
+
return;
|
|
101
|
+
let entries;
|
|
102
|
+
try {
|
|
103
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const e of entries) {
|
|
109
|
+
if (out.length >= MAX_BATCH || scanned >= MAX_WALK_FILES)
|
|
110
|
+
return;
|
|
111
|
+
if (e.isDirectory()) {
|
|
112
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.'))
|
|
113
|
+
walk(path.join(dir, e.name));
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!e.isFile())
|
|
117
|
+
continue;
|
|
118
|
+
if (!REVALIDATE_EXTS.has(path.extname(e.name).toLowerCase()))
|
|
119
|
+
continue;
|
|
120
|
+
scanned++;
|
|
121
|
+
const abs = path.join(dir, e.name);
|
|
122
|
+
let mtimeMs;
|
|
123
|
+
try {
|
|
124
|
+
mtimeMs = fs.statSync(abs).mtimeMs;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (mtimeMs > Math.max(baseline, this.lastReval.get(abs) ?? 0))
|
|
130
|
+
out.push({ path: abs, mtimeMs });
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
walk(this.workspaceRoot);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
// Wait until the lint queue drains and the epoch advanced past the revalidation
|
|
137
|
+
// baseline (so the posted re-lints have completed). Bounded; best-effort.
|
|
138
|
+
async waitDrained(baselineEpoch) {
|
|
139
|
+
const timeoutMs = 5000;
|
|
140
|
+
const intervalMs = 50;
|
|
141
|
+
let elapsed = 0;
|
|
142
|
+
await sleep(30); // head start so the lint agent picks up the posted requests
|
|
143
|
+
while (elapsed < timeoutMs) {
|
|
144
|
+
const s = await this.lsp.executeCommand('cwtools.ai.getValidationStatus', [], { timeoutMs: 5000 }).catch(() => null);
|
|
145
|
+
if (!s || typeof s !== 'object')
|
|
146
|
+
return;
|
|
147
|
+
const epoch = numberOr(s.epoch, 0);
|
|
148
|
+
const inProgress = s.inProgress === true;
|
|
149
|
+
const queue = numberOr(s.queueDepth, 0) + numberOr(s.debounceQueueDepth, 0);
|
|
150
|
+
if (epoch > baselineEpoch && !inProgress && queue === 0)
|
|
151
|
+
return;
|
|
152
|
+
await sleep(intervalMs);
|
|
153
|
+
elapsed += intervalMs;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.RevalidationCoordinator = RevalidationCoordinator;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function detectExtensionCacheDir(game: string | undefined): string | undefined;
|
|
2
|
+
export declare function detectInstalledExtensionDir(): string | undefined;
|
|
3
|
+
export declare function detectExtensionServerPath(): string | undefined;
|
|
4
|
+
export declare function detectExtensionBridgeManifestPath(): string | undefined;
|
|
5
|
+
export declare function detectExtensionRulesDir(cacheDir: string | undefined, game: string | undefined): string | undefined;
|
|
@@ -0,0 +1,153 @@
|
|
|
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.detectExtensionCacheDir = detectExtensionCacheDir;
|
|
37
|
+
exports.detectInstalledExtensionDir = detectInstalledExtensionDir;
|
|
38
|
+
exports.detectExtensionServerPath = detectExtensionServerPath;
|
|
39
|
+
exports.detectExtensionBridgeManifestPath = detectExtensionBridgeManifestPath;
|
|
40
|
+
exports.detectExtensionRulesDir = detectExtensionRulesDir;
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const cwtools_shared_1 = require("cwtools-shared");
|
|
45
|
+
// VS Code-compatible hosts store the extension's built vanilla cache + extracted
|
|
46
|
+
// rules under globalStorage. In standalone mode, when the user doesn't pass
|
|
47
|
+
// --cache, reuse that dir so the MCP rides on a cache the extension already built.
|
|
48
|
+
const PRIMARY_EXTENSION_ID = 'ForeverSkywalker.foreverskywalker-stellaris-cwtools';
|
|
49
|
+
const PRIMARY_EXTENSION_DIR = PRIMARY_EXTENSION_ID.toLowerCase();
|
|
50
|
+
const VSCODE_APP_DIRS = ['Code', 'Code - Insiders', 'VSCodium', 'Cursor', 'Antigravity'];
|
|
51
|
+
function globalStorageBases() {
|
|
52
|
+
const home = os.homedir();
|
|
53
|
+
const platform = os.platform();
|
|
54
|
+
return VSCODE_APP_DIRS.map(app => {
|
|
55
|
+
if (platform === 'win32') {
|
|
56
|
+
const appData = process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming');
|
|
57
|
+
return path.join(appData, app, 'User', 'globalStorage');
|
|
58
|
+
}
|
|
59
|
+
if (platform === 'darwin') {
|
|
60
|
+
return path.join(home, 'Library', 'Application Support', app, 'User', 'globalStorage');
|
|
61
|
+
}
|
|
62
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path.join(home, '.config');
|
|
63
|
+
return path.join(xdg, app, 'User', 'globalStorage');
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// The extension's `.cwtools` dir if it holds the built <game>.cwb, else undefined.
|
|
67
|
+
function detectExtensionCacheDir(game) {
|
|
68
|
+
const cacheFile = (0, cwtools_shared_1.vanillaCacheFileName)(game);
|
|
69
|
+
if (!cacheFile)
|
|
70
|
+
return undefined;
|
|
71
|
+
for (const base of globalStorageBases()) {
|
|
72
|
+
const dir = path.join(base, PRIMARY_EXTENSION_DIR, '.cwtools');
|
|
73
|
+
if (fs.existsSync(path.join(dir, cacheFile)))
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
// Installed-extension roots (`~/.vscode/extensions` and variants), where the
|
|
79
|
+
// packaged CWTools server binary lives — so the MCP rides on the same server the
|
|
80
|
+
// user already installed, with no dev checkout required.
|
|
81
|
+
function extensionInstallRoots() {
|
|
82
|
+
const home = os.homedir();
|
|
83
|
+
return ['.vscode', '.vscode-insiders', '.vscode-oss', '.cursor', '.vscode-server', '.antigravity'].map(d => path.join(home, d, 'extensions'));
|
|
84
|
+
}
|
|
85
|
+
// Newest installed main extension dir, else undefined.
|
|
86
|
+
function detectInstalledExtensionDir() {
|
|
87
|
+
let best;
|
|
88
|
+
for (const root of extensionInstallRoots()) {
|
|
89
|
+
if (!fs.existsSync(root))
|
|
90
|
+
continue;
|
|
91
|
+
for (const name of fs.readdirSync(root)) {
|
|
92
|
+
if (!name.toLowerCase().startsWith(`${PRIMARY_EXTENSION_DIR}-`))
|
|
93
|
+
continue;
|
|
94
|
+
const version = name.slice(PRIMARY_EXTENSION_DIR.length + 1);
|
|
95
|
+
// String compare is enough to pick the highest semver-ish folder name.
|
|
96
|
+
if (!best || version > best.version) {
|
|
97
|
+
best = { dir: path.join(root, name), version };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return best?.dir;
|
|
102
|
+
}
|
|
103
|
+
// The packaged server binary inside the installed extension, else undefined.
|
|
104
|
+
function detectExtensionServerPath() {
|
|
105
|
+
const ext = detectInstalledExtensionDir();
|
|
106
|
+
if (!ext)
|
|
107
|
+
return undefined;
|
|
108
|
+
const platform = os.platform();
|
|
109
|
+
const exe = platform === 'win32'
|
|
110
|
+
? path.join('win-x64', 'CWTools Server.exe')
|
|
111
|
+
: platform === 'darwin'
|
|
112
|
+
? path.join('osx-x64', 'CWTools Server')
|
|
113
|
+
: path.join('linux-x64', 'CWTools Server');
|
|
114
|
+
const candidate = path.join(ext, 'bin', 'server', exe);
|
|
115
|
+
return fs.existsSync(candidate) ? candidate : undefined;
|
|
116
|
+
}
|
|
117
|
+
// The bridge manifest the active extension host writes under
|
|
118
|
+
// globalStorage/<extension>/mcp/bridge-manifest.json. Lets an independently
|
|
119
|
+
// installed (npx / global npm) cwtools-mcp discover the running extension
|
|
120
|
+
// without a --bridge-manifest override.
|
|
121
|
+
function detectExtensionBridgeManifestPath() {
|
|
122
|
+
for (const base of globalStorageBases()) {
|
|
123
|
+
const candidate = path.join(base, PRIMARY_EXTENSION_DIR, 'mcp', 'bridge-manifest.json');
|
|
124
|
+
if (fs.existsSync(candidate))
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
// True when dir holds at least one .cwt rule file at its top level.
|
|
130
|
+
function containsRuleFiles(dir) {
|
|
131
|
+
try {
|
|
132
|
+
return fs.readdirSync(dir).some(name => name.toLowerCase().endsWith('.cwt'));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function detectExtensionRulesDir(cacheDir, game) {
|
|
139
|
+
const g = (game ?? 'stellaris').toLowerCase();
|
|
140
|
+
const roots = [
|
|
141
|
+
...(cacheDir ? [cacheDir] : []),
|
|
142
|
+
...globalStorageBases().map(base => path.join(base, PRIMARY_EXTENSION_DIR, '.cwtools')),
|
|
143
|
+
];
|
|
144
|
+
for (const root of roots) {
|
|
145
|
+
const config = path.join(root, g, 'config');
|
|
146
|
+
if (containsRuleFiles(config))
|
|
147
|
+
return config;
|
|
148
|
+
const bare = path.join(root, g);
|
|
149
|
+
if (containsRuleFiles(bare))
|
|
150
|
+
return bare;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './config';
|
|
2
|
+
export * from './hosts/lspProcessHost';
|
|
3
|
+
export * from './hosts/nodeHostServices';
|
|
4
|
+
export * from './mcp/toolHandlers';
|
|
5
|
+
export * from './mcp/toolRegistrar';
|
|
6
|
+
export * from './mcp/resources';
|
|
7
|
+
export * from './mcp/transportHttp';
|
|
8
|
+
export * from './mcp/transportStdio';
|
|
9
|
+
export * from './server';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./config"), exports);
|
|
18
|
+
__exportStar(require("./hosts/lspProcessHost"), exports);
|
|
19
|
+
__exportStar(require("./hosts/nodeHostServices"), exports);
|
|
20
|
+
__exportStar(require("./mcp/toolHandlers"), exports);
|
|
21
|
+
__exportStar(require("./mcp/toolRegistrar"), exports);
|
|
22
|
+
__exportStar(require("./mcp/resources"), exports);
|
|
23
|
+
__exportStar(require("./mcp/transportHttp"), exports);
|
|
24
|
+
__exportStar(require("./mcp/transportStdio"), exports);
|
|
25
|
+
__exportStar(require("./server"), exports);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { CwtoolsMcpConfig } from '../config';
|
|
3
|
+
export declare function createBridgeProxyMcpServer(config: CwtoolsMcpConfig): Server;
|
|
4
|
+
export declare function assertBridgeWorkspaceMatches(expectedWorkspaceRoot: string | string[], servedWorkspaceRoot: string, source?: string): void;
|