vectorforce 0.3.3 → 0.3.7
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/CHANGELOG.md +27 -0
- package/README.md +90 -8
- package/bin/vf.js +140 -93
- package/package.json +1 -1
- package/src/mcp/harness-config.js +346 -75
- package/src/mcp/mcp-server.js +81 -9
- package/src/operations/project-service.js +18 -1
- package/src/studio/server.js +104 -18
- package/studio/dist/assets/{index-C0_eVpV1.js → index-Cizk5468.js} +1 -1
- package/studio/dist/assets/index-w62XkQKM.css +2 -0
- package/studio/dist/index.html +2 -2
- package/studio/dist/assets/index-CxTK-2ty.css +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, resolve } from 'node:path';
|
|
1
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
2
|
import { homedir, tmpdir } from 'node:os';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, realpathSync } from 'node:fs';
|
|
@@ -19,7 +19,177 @@ export class HarnessConfig {
|
|
|
19
19
|
* Get the absolute path to bin/vf.js (cross-platform: macOS, Linux, Windows)
|
|
20
20
|
*/
|
|
21
21
|
static getBinPath() {
|
|
22
|
-
|
|
22
|
+
const raw = fileURLToPath(new URL('../../bin/vf.js', import.meta.url));
|
|
23
|
+
return process.platform === 'win32' ? raw.replace(/\\/g, '/') : raw;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Absolute path of the Node.js runtime used to spawn `vf mcp`.
|
|
28
|
+
* GUI-spawned MCP clients (VS Code, Cursor, ...) on Windows often lack
|
|
29
|
+
* `node` on PATH, so generated configs must never rely on bare `node`.
|
|
30
|
+
*/
|
|
31
|
+
static getNodeCommand() {
|
|
32
|
+
return process.execPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Escape a value for embedding in a TOML basic string.
|
|
37
|
+
*/
|
|
38
|
+
static _tomlString(value) {
|
|
39
|
+
return '"' + String(value).split('\\').join('\\\\').split('"').join('\\"') + '"';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Canonical registry of supported harnesses.
|
|
44
|
+
* markers: workspace-relative files/dirs whose presence indicates the harness is in use.
|
|
45
|
+
*/
|
|
46
|
+
static getSupportedHarnesses() {
|
|
47
|
+
return [
|
|
48
|
+
{ id: 'antigravity', label: 'Google Antigravity', markers: ['.agents', '.gemini'], files: ['.agents/mcp_config.json'] },
|
|
49
|
+
{ id: 'command-code', label: 'Claude Code / Command Code', aliases: ['claude-code', 'claude', 'cmd'], markers: ['.mcp.json', 'commandcode.json', '.claude', 'CLAUDE.md'], files: ['.mcp.json', 'commandcode.json'] },
|
|
50
|
+
{ id: 'muse', label: 'Meta Muse', aliases: ['muse-code'], markers: ['.muse', 'musecode.json'], files: ['.muse/mcp_config.json', 'musecode.json'] },
|
|
51
|
+
{ id: 'codex', label: 'OpenAI Codex', markers: ['.codex', 'codex.json'], files: ['.codex/config.toml', 'codex.json'] },
|
|
52
|
+
{ id: 'cursor', label: 'Cursor', markers: ['.cursor', '.cursorrules'], files: ['.cursor/mcp.json'] },
|
|
53
|
+
{ id: 'windsurf', label: 'Windsurf', markers: ['.windsurf', '.windsurfrules'], files: ['.windsurf/mcp_config.json'] },
|
|
54
|
+
{ id: 'copilot', label: 'GitHub Copilot / VS Code', aliases: ['github-copilot', 'vscode'], markers: ['.vscode', '.github/copilot-instructions.md', '.github/muse-instructions.md'], files: ['.vscode/mcp.json'] },
|
|
55
|
+
{ id: 'kiro', label: 'Kiro IDE', aliases: ['kiro-ide'], markers: ['.kiro', 'kiro.json'], files: ['.kiro/settings/mcp.json'] }
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a user-supplied harness name or alias to its canonical id.
|
|
61
|
+
* @returns {string|null}
|
|
62
|
+
*/
|
|
63
|
+
static resolveHarnessId(input) {
|
|
64
|
+
const normalized = String(input || '').trim().toLowerCase();
|
|
65
|
+
if (!normalized) return null;
|
|
66
|
+
for (const harness of HarnessConfig.getSupportedHarnesses()) {
|
|
67
|
+
if (harness.id === normalized || (harness.aliases || []).includes(normalized)) {
|
|
68
|
+
return harness.id;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Detect harnesses already in use by the workspace (marker files/dirs).
|
|
76
|
+
* @returns {{ id: string, label: string, matched: string[] }[]}
|
|
77
|
+
*/
|
|
78
|
+
static detectHarnesses(workspacePath) {
|
|
79
|
+
const detected = [];
|
|
80
|
+
for (const harness of HarnessConfig.getSupportedHarnesses()) {
|
|
81
|
+
const matched = (harness.markers || []).filter((marker) => {
|
|
82
|
+
try {
|
|
83
|
+
return existsSync(join(workspacePath, marker));
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (matched.length > 0) {
|
|
89
|
+
detected.push({ id: harness.id, label: harness.label, matched });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return detected;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Install (or merge) the VectorForce entry for one canonical harness id.
|
|
97
|
+
* @returns {string|null} primary config file path, or null for unknown ids
|
|
98
|
+
*/
|
|
99
|
+
static installHarness(workspacePath, id) {
|
|
100
|
+
switch (id) {
|
|
101
|
+
case 'antigravity': return HarnessConfig.installAntigravity(workspacePath);
|
|
102
|
+
case 'command-code': return HarnessConfig.installCommandCode(workspacePath);
|
|
103
|
+
case 'muse': return HarnessConfig.installMuseCode(workspacePath);
|
|
104
|
+
case 'codex': return HarnessConfig.installCodex(workspacePath);
|
|
105
|
+
case 'cursor': return HarnessConfig.installCursor(workspacePath);
|
|
106
|
+
case 'windsurf': return HarnessConfig.installWindsurf(workspacePath);
|
|
107
|
+
case 'copilot': return HarnessConfig.installCopilot(workspacePath);
|
|
108
|
+
case 'kiro': return HarnessConfig.installKiro(workspacePath);
|
|
109
|
+
default: return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Remove the VectorForce entry for one canonical harness id.
|
|
115
|
+
*/
|
|
116
|
+
static uninstallHarness(workspacePath, id) {
|
|
117
|
+
switch (id) {
|
|
118
|
+
case 'antigravity': return HarnessConfig.uninstallAntigravity(workspacePath);
|
|
119
|
+
case 'command-code': return HarnessConfig.uninstallCommandCode(workspacePath);
|
|
120
|
+
case 'muse': return HarnessConfig.uninstallMuseCode(workspacePath);
|
|
121
|
+
case 'codex': return HarnessConfig.uninstallCodex(workspacePath);
|
|
122
|
+
case 'cursor': return HarnessConfig.uninstallCursor(workspacePath);
|
|
123
|
+
case 'windsurf': return HarnessConfig.uninstallWindsurf(workspacePath);
|
|
124
|
+
case 'copilot': return HarnessConfig.uninstallCopilot(workspacePath);
|
|
125
|
+
case 'kiro': return HarnessConfig.uninstallKiro(workspacePath);
|
|
126
|
+
default: return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether the workspace currently carries a VectorForce entry the harness will read.
|
|
132
|
+
*/
|
|
133
|
+
static isHarnessConfigured(workspacePath, id) {
|
|
134
|
+
const readJson = (relPath) => {
|
|
135
|
+
try {
|
|
136
|
+
const full = join(workspacePath, relPath);
|
|
137
|
+
if (!existsSync(full)) return null;
|
|
138
|
+
return JSON.parse(readFileSync(full, 'utf8'));
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const hasEntry = (config) => Boolean(config?.mcpServers?.vectorforce);
|
|
144
|
+
switch (id) {
|
|
145
|
+
case 'antigravity':
|
|
146
|
+
return hasEntry(readJson(join('.agents', 'mcp_config.json')));
|
|
147
|
+
case 'command-code':
|
|
148
|
+
return hasEntry(readJson('.mcp.json')) || hasEntry(readJson('commandcode.json'));
|
|
149
|
+
case 'muse':
|
|
150
|
+
return hasEntry(readJson(join('.muse', 'mcp_config.json')))
|
|
151
|
+
|| hasEntry(readJson('musecode.json'))
|
|
152
|
+
|| HarnessConfig._isMuseGlobalConfigured(workspacePath);
|
|
153
|
+
case 'codex': {
|
|
154
|
+
try {
|
|
155
|
+
const tomlPath = join(workspacePath, '.codex', 'config.toml');
|
|
156
|
+
if (existsSync(tomlPath) && readFileSync(tomlPath, 'utf8').includes('[mcp_servers.vectorforce]')) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
return hasEntry(readJson('codex.json'));
|
|
161
|
+
}
|
|
162
|
+
case 'cursor':
|
|
163
|
+
return hasEntry(readJson(join('.cursor', 'mcp.json')));
|
|
164
|
+
case 'windsurf':
|
|
165
|
+
return hasEntry(readJson(join('.windsurf', 'mcp_config.json')));
|
|
166
|
+
case 'copilot': {
|
|
167
|
+
const config = readJson(join('.vscode', 'mcp.json'));
|
|
168
|
+
return Boolean(config?.servers?.vectorforce || config?.mcpServers?.vectorforce);
|
|
169
|
+
}
|
|
170
|
+
case 'kiro':
|
|
171
|
+
return hasEntry(readJson(join('.kiro', 'settings', 'mcp.json')))
|
|
172
|
+
|| hasEntry(readJson(join('.kiro', 'mcp.json')))
|
|
173
|
+
|| hasEntry(readJson('kiro.json'));
|
|
174
|
+
default:
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Whether the global Muse Code settings already register VectorForce.
|
|
181
|
+
* Temp workspaces never consult global state (tests stay hermetic).
|
|
182
|
+
*/
|
|
183
|
+
static _isMuseGlobalConfigured(workspacePath) {
|
|
184
|
+
if (isTempWorkspace(workspacePath)) return false;
|
|
185
|
+
try {
|
|
186
|
+
const settingsPath = join(homedir(), '.config', 'muse', 'settings.json');
|
|
187
|
+
if (!existsSync(settingsPath)) return false;
|
|
188
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
189
|
+
return Boolean(settings?.mcpServers?.vectorforce);
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
23
193
|
}
|
|
24
194
|
|
|
25
195
|
/**
|
|
@@ -27,7 +197,8 @@ export class HarnessConfig {
|
|
|
27
197
|
*/
|
|
28
198
|
static getAntigravityServerEntry() {
|
|
29
199
|
return {
|
|
30
|
-
|
|
200
|
+
type: 'stdio',
|
|
201
|
+
command: HarnessConfig.getNodeCommand(),
|
|
31
202
|
args: [HarnessConfig.getBinPath(), 'mcp']
|
|
32
203
|
};
|
|
33
204
|
}
|
|
@@ -81,9 +252,12 @@ export class HarnessConfig {
|
|
|
81
252
|
|
|
82
253
|
if (!isTempWorkspace(workspacePath)) {
|
|
83
254
|
try {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
255
|
+
const globalCandidates = [
|
|
256
|
+
join(homedir(), '.gemini', 'config', 'mcp_config.json'),
|
|
257
|
+
join(homedir(), '.gemini', 'antigravity', 'mcp_config.json')
|
|
258
|
+
];
|
|
259
|
+
const globalMcpPath = globalCandidates.find((candidate) => existsSync(dirname(candidate)));
|
|
260
|
+
if (globalMcpPath) {
|
|
87
261
|
let globalConfig = {};
|
|
88
262
|
if (existsSync(globalMcpPath)) {
|
|
89
263
|
try {
|
|
@@ -95,7 +269,8 @@ export class HarnessConfig {
|
|
|
95
269
|
globalConfig.mcpServers = {
|
|
96
270
|
...(globalConfig.mcpServers || {}),
|
|
97
271
|
vectorforce: {
|
|
98
|
-
|
|
272
|
+
type: 'stdio',
|
|
273
|
+
command: HarnessConfig.getNodeCommand(),
|
|
99
274
|
args: [HarnessConfig.getBinPath(), 'mcp', '--workspace', workspacePath],
|
|
100
275
|
env: {
|
|
101
276
|
VF_WORKSPACE: workspacePath,
|
|
@@ -141,7 +316,7 @@ export class HarnessConfig {
|
|
|
141
316
|
static getCommandCodeServerEntry() {
|
|
142
317
|
return {
|
|
143
318
|
type: 'stdio',
|
|
144
|
-
command:
|
|
319
|
+
command: HarnessConfig.getNodeCommand(),
|
|
145
320
|
args: [HarnessConfig.getBinPath(), 'mcp']
|
|
146
321
|
};
|
|
147
322
|
}
|
|
@@ -179,12 +354,19 @@ export class HarnessConfig {
|
|
|
179
354
|
}
|
|
180
355
|
}
|
|
181
356
|
|
|
357
|
+
const mcpServers = { ...(mcpCurrent?.mcpServers || {}) };
|
|
358
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
359
|
+
if (server && typeof server === 'object' && server.serverUrl && !server.url) {
|
|
360
|
+
server.url = server.serverUrl;
|
|
361
|
+
delete server.serverUrl;
|
|
362
|
+
if (!server.type) server.type = 'sse';
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
mcpServers.vectorforce = HarnessConfig.getCommandCodeServerEntry();
|
|
366
|
+
|
|
182
367
|
const mcpMerged = {
|
|
183
368
|
...mcpCurrent,
|
|
184
|
-
mcpServers
|
|
185
|
-
...(mcpCurrent?.mcpServers || {}),
|
|
186
|
-
vectorforce: HarnessConfig.getCommandCodeServerEntry()
|
|
187
|
-
}
|
|
369
|
+
mcpServers
|
|
188
370
|
};
|
|
189
371
|
|
|
190
372
|
const cliCommand = `cmd mcp add --transport stdio vectorforce -- node ${HarnessConfig.getBinPath()} mcp`;
|
|
@@ -251,7 +433,8 @@ export class HarnessConfig {
|
|
|
251
433
|
*/
|
|
252
434
|
static getMuseCodeServerEntry() {
|
|
253
435
|
return {
|
|
254
|
-
|
|
436
|
+
type: 'stdio',
|
|
437
|
+
command: HarnessConfig.getNodeCommand(),
|
|
255
438
|
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
256
439
|
env: {
|
|
257
440
|
VF_HARNESS: 'muse-code'
|
|
@@ -294,6 +477,42 @@ export class HarnessConfig {
|
|
|
294
477
|
};
|
|
295
478
|
}
|
|
296
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Merge the VectorForce entry into global Muse Code settings (pure, no I/O).
|
|
482
|
+
* Muse Code reads MCP servers from the camelCase `mcpServers` block of the
|
|
483
|
+
* user settings file. A stale snake_case `mcp_servers.vectorforce` entry
|
|
484
|
+
* (written by older VectorForce releases) is migrated, never duplicated.
|
|
485
|
+
* The global entry carries no workspace pin, so one entry serves every
|
|
486
|
+
* project: the server resolves its workspace from its launch directory.
|
|
487
|
+
*/
|
|
488
|
+
static _mergeMuseGlobalSettings(settings) {
|
|
489
|
+
const merged = { ...(settings || {}) };
|
|
490
|
+
if (merged.schema_version === undefined) {
|
|
491
|
+
merged.schema_version = 1;
|
|
492
|
+
}
|
|
493
|
+
const servers = { ...(merged.mcpServers || {}) };
|
|
494
|
+
servers.vectorforce = {
|
|
495
|
+
mode: 'optional',
|
|
496
|
+
transport: 'stdio',
|
|
497
|
+
command: HarnessConfig.getNodeCommand(),
|
|
498
|
+
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
499
|
+
env: {
|
|
500
|
+
VF_HARNESS: 'muse-code'
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
merged.mcpServers = servers;
|
|
504
|
+
if (merged.mcp_servers?.vectorforce) {
|
|
505
|
+
const remaining = { ...merged.mcp_servers };
|
|
506
|
+
delete remaining.vectorforce;
|
|
507
|
+
if (Object.keys(remaining).length === 0) {
|
|
508
|
+
delete merged.mcp_servers;
|
|
509
|
+
} else {
|
|
510
|
+
merged.mcp_servers = remaining;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return merged;
|
|
514
|
+
}
|
|
515
|
+
|
|
297
516
|
/**
|
|
298
517
|
* Install Muse Code MCP configuration reversibly.
|
|
299
518
|
* @param {string} workspacePath
|
|
@@ -309,25 +528,28 @@ export class HarnessConfig {
|
|
|
309
528
|
|
|
310
529
|
if (!isTempWorkspace(workspacePath)) {
|
|
311
530
|
try {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
531
|
+
const globalCandidates = [
|
|
532
|
+
join(homedir(), '.config', 'muse', 'settings.json'),
|
|
533
|
+
...(process.platform === 'win32' ? [
|
|
534
|
+
join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'muse', 'settings.json'),
|
|
535
|
+
join(homedir(), '.muse', 'settings.json')
|
|
536
|
+
] : [])
|
|
537
|
+
];
|
|
538
|
+
for (const globalSettingsPath of globalCandidates) {
|
|
539
|
+
const globalMuseDir = dirname(globalSettingsPath);
|
|
540
|
+
if (globalSettingsPath.includes('.config') || existsSync(globalMuseDir)) {
|
|
541
|
+
mkdirSync(globalMuseDir, { recursive: true });
|
|
542
|
+
let settings = {};
|
|
543
|
+
if (existsSync(globalSettingsPath)) {
|
|
544
|
+
try {
|
|
545
|
+
settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
|
|
546
|
+
} catch {
|
|
547
|
+
settings = {};
|
|
548
|
+
}
|
|
327
549
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
550
|
+
settings = HarnessConfig._mergeMuseGlobalSettings(settings);
|
|
551
|
+
writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
552
|
+
}
|
|
331
553
|
}
|
|
332
554
|
} catch {}
|
|
333
555
|
}
|
|
@@ -368,12 +590,26 @@ export class HarnessConfig {
|
|
|
368
590
|
|
|
369
591
|
if (!isTempWorkspace(workspacePath)) {
|
|
370
592
|
try {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
593
|
+
const globalCandidates = [
|
|
594
|
+
join(homedir(), '.config', 'muse', 'settings.json'),
|
|
595
|
+
...(process.platform === 'win32' ? [
|
|
596
|
+
join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'muse', 'settings.json'),
|
|
597
|
+
join(homedir(), '.muse', 'settings.json')
|
|
598
|
+
] : [])
|
|
599
|
+
];
|
|
600
|
+
for (const globalSettingsPath of globalCandidates) {
|
|
601
|
+
if (existsSync(globalSettingsPath)) {
|
|
602
|
+
const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
|
|
603
|
+
let changed = false;
|
|
604
|
+
for (const key of ['mcpServers', 'mcp_servers']) {
|
|
605
|
+
if (settings?.[key]?.vectorforce) {
|
|
606
|
+
delete settings[key].vectorforce;
|
|
607
|
+
changed = true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (changed) {
|
|
611
|
+
writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
612
|
+
}
|
|
377
613
|
}
|
|
378
614
|
}
|
|
379
615
|
} catch {}
|
|
@@ -387,7 +623,8 @@ export class HarnessConfig {
|
|
|
387
623
|
*/
|
|
388
624
|
static getCodexServerEntry() {
|
|
389
625
|
return {
|
|
390
|
-
|
|
626
|
+
type: 'stdio',
|
|
627
|
+
command: HarnessConfig.getNodeCommand(),
|
|
391
628
|
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
392
629
|
env: {
|
|
393
630
|
VF_HARNESS: 'codex'
|
|
@@ -401,8 +638,9 @@ export class HarnessConfig {
|
|
|
401
638
|
* @returns {string} merged TOML
|
|
402
639
|
*/
|
|
403
640
|
static _mergeCodexToml(existingToml) {
|
|
404
|
-
const
|
|
405
|
-
const
|
|
641
|
+
const nodeCommand = HarnessConfig._tomlString(HarnessConfig.getNodeCommand());
|
|
642
|
+
const binPath = HarnessConfig._tomlString(HarnessConfig.getBinPath());
|
|
643
|
+
const serverBlock = `[mcp_servers.vectorforce]\ncommand = ${nodeCommand}\nargs = [${binPath}, "mcp"]\n[mcp_servers.vectorforce.env]\nVF_HARNESS = "codex"\n`;
|
|
406
644
|
|
|
407
645
|
if (!existingToml || !existingToml.trim()) {
|
|
408
646
|
return serverBlock;
|
|
@@ -553,7 +791,8 @@ export class HarnessConfig {
|
|
|
553
791
|
*/
|
|
554
792
|
static getCursorServerEntry() {
|
|
555
793
|
return {
|
|
556
|
-
|
|
794
|
+
type: 'stdio',
|
|
795
|
+
command: HarnessConfig.getNodeCommand(),
|
|
557
796
|
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
558
797
|
env: {
|
|
559
798
|
VF_HARNESS: 'cursor'
|
|
@@ -630,7 +869,8 @@ export class HarnessConfig {
|
|
|
630
869
|
globalConfig.mcpServers = {
|
|
631
870
|
...(globalConfig.mcpServers || {}),
|
|
632
871
|
vectorforce: {
|
|
633
|
-
|
|
872
|
+
type: 'stdio',
|
|
873
|
+
command: HarnessConfig.getNodeCommand(),
|
|
634
874
|
args: [HarnessConfig.getBinPath(), 'mcp', '--workspace', workspacePath],
|
|
635
875
|
env: {
|
|
636
876
|
VF_WORKSPACE: workspacePath,
|
|
@@ -676,7 +916,8 @@ export class HarnessConfig {
|
|
|
676
916
|
*/
|
|
677
917
|
static getWindsurfServerEntry() {
|
|
678
918
|
return {
|
|
679
|
-
|
|
919
|
+
type: 'stdio',
|
|
920
|
+
command: HarnessConfig.getNodeCommand(),
|
|
680
921
|
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
681
922
|
env: {
|
|
682
923
|
VF_HARNESS: 'windsurf'
|
|
@@ -753,7 +994,8 @@ export class HarnessConfig {
|
|
|
753
994
|
globalConfig.mcpServers = {
|
|
754
995
|
...(globalConfig.mcpServers || {}),
|
|
755
996
|
vectorforce: {
|
|
756
|
-
|
|
997
|
+
type: 'stdio',
|
|
998
|
+
command: HarnessConfig.getNodeCommand(),
|
|
757
999
|
args: [HarnessConfig.getBinPath(), 'mcp', '--workspace', workspacePath],
|
|
758
1000
|
env: {
|
|
759
1001
|
VF_WORKSPACE: workspacePath,
|
|
@@ -799,7 +1041,8 @@ export class HarnessConfig {
|
|
|
799
1041
|
*/
|
|
800
1042
|
static getCopilotServerEntry() {
|
|
801
1043
|
return {
|
|
802
|
-
|
|
1044
|
+
type: 'stdio',
|
|
1045
|
+
command: HarnessConfig.getNodeCommand(),
|
|
803
1046
|
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
804
1047
|
env: {
|
|
805
1048
|
VF_HARNESS: 'copilot'
|
|
@@ -822,18 +1065,31 @@ export class HarnessConfig {
|
|
|
822
1065
|
current = {};
|
|
823
1066
|
}
|
|
824
1067
|
}
|
|
1068
|
+
// VS Code reads only the top-level `servers` key; migrate any legacy
|
|
1069
|
+
// `mcpServers.vectorforce` entry this tool wrote previously.
|
|
1070
|
+
const migratedLegacy = Boolean(current?.mcpServers?.vectorforce);
|
|
825
1071
|
const mergedContent = {
|
|
826
1072
|
...(current || {}),
|
|
827
|
-
|
|
828
|
-
...(current?.
|
|
1073
|
+
servers: {
|
|
1074
|
+
...(current?.servers || {}),
|
|
829
1075
|
vectorforce: HarnessConfig.getCopilotServerEntry()
|
|
830
1076
|
}
|
|
831
1077
|
};
|
|
1078
|
+
if (mergedContent.mcpServers?.vectorforce) {
|
|
1079
|
+
const remaining = { ...mergedContent.mcpServers };
|
|
1080
|
+
delete remaining.vectorforce;
|
|
1081
|
+
if (Object.keys(remaining).length === 0) {
|
|
1082
|
+
delete mergedContent.mcpServers;
|
|
1083
|
+
} else {
|
|
1084
|
+
mergedContent.mcpServers = remaining;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
832
1087
|
return {
|
|
833
1088
|
filePath,
|
|
834
1089
|
exists: existsSync(filePath),
|
|
835
1090
|
currentContent: current,
|
|
836
|
-
mergedContent
|
|
1091
|
+
mergedContent,
|
|
1092
|
+
migratedLegacy
|
|
837
1093
|
};
|
|
838
1094
|
}
|
|
839
1095
|
|
|
@@ -878,10 +1134,14 @@ export class HarnessConfig {
|
|
|
878
1134
|
if (existsSync(filePath)) {
|
|
879
1135
|
try {
|
|
880
1136
|
const config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
881
|
-
|
|
882
|
-
|
|
1137
|
+
for (const key of ['servers', 'mcpServers']) {
|
|
1138
|
+
if (config?.[key]?.vectorforce) {
|
|
1139
|
+
delete config[key].vectorforce;
|
|
1140
|
+
removed = true;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (removed) {
|
|
883
1144
|
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
884
|
-
removed = true;
|
|
885
1145
|
}
|
|
886
1146
|
} catch {}
|
|
887
1147
|
}
|
|
@@ -897,7 +1157,8 @@ export class HarnessConfig {
|
|
|
897
1157
|
*/
|
|
898
1158
|
static getKiroServerEntry() {
|
|
899
1159
|
return {
|
|
900
|
-
|
|
1160
|
+
type: 'stdio',
|
|
1161
|
+
command: HarnessConfig.getNodeCommand(),
|
|
901
1162
|
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
902
1163
|
env: {
|
|
903
1164
|
VF_HARNESS: 'kiro'
|
|
@@ -910,9 +1171,10 @@ export class HarnessConfig {
|
|
|
910
1171
|
* @param {string} workspacePath
|
|
911
1172
|
*/
|
|
912
1173
|
static previewKiro(workspacePath) {
|
|
913
|
-
const dir = join(workspacePath, '.kiro');
|
|
1174
|
+
const dir = join(workspacePath, '.kiro', 'settings');
|
|
914
1175
|
const filePath = join(dir, 'mcp.json');
|
|
915
1176
|
const kiroJsonPath = join(workspacePath, 'kiro.json');
|
|
1177
|
+
const legacyPath = join(workspacePath, '.kiro', 'mcp.json');
|
|
916
1178
|
|
|
917
1179
|
let current = null;
|
|
918
1180
|
if (existsSync(filePath)) {
|
|
@@ -949,6 +1211,7 @@ export class HarnessConfig {
|
|
|
949
1211
|
return {
|
|
950
1212
|
filePath,
|
|
951
1213
|
kiroJsonPath,
|
|
1214
|
+
legacyPath,
|
|
952
1215
|
exists: existsSync(filePath) || existsSync(kiroJsonPath),
|
|
953
1216
|
currentContent: current,
|
|
954
1217
|
mergedContent,
|
|
@@ -962,19 +1225,34 @@ export class HarnessConfig {
|
|
|
962
1225
|
* @returns {string} filePath
|
|
963
1226
|
*/
|
|
964
1227
|
static installKiro(workspacePath) {
|
|
965
|
-
const { filePath, kiroJsonPath, mergedContent, mergedKiroJson } = HarnessConfig.previewKiro(workspacePath);
|
|
966
|
-
const dir = join(workspacePath, '.kiro');
|
|
1228
|
+
const { filePath, kiroJsonPath, legacyPath, mergedContent, mergedKiroJson } = HarnessConfig.previewKiro(workspacePath);
|
|
1229
|
+
const dir = join(workspacePath, '.kiro', 'settings');
|
|
967
1230
|
if (!existsSync(dir)) {
|
|
968
1231
|
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
969
1232
|
}
|
|
970
1233
|
writeFileSync(filePath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
971
1234
|
writeFileSync(kiroJsonPath, JSON.stringify(mergedKiroJson, null, 2) + '\n', 'utf8');
|
|
972
1235
|
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
1236
|
+
// Migrate the legacy .kiro/mcp.json entry (Kiro only reads .kiro/settings/mcp.json).
|
|
1237
|
+
if (legacyPath && existsSync(legacyPath)) {
|
|
1238
|
+
try {
|
|
1239
|
+
const legacy = JSON.parse(readFileSync(legacyPath, 'utf8'));
|
|
1240
|
+
if (legacy?.mcpServers?.vectorforce) {
|
|
1241
|
+
delete legacy.mcpServers.vectorforce;
|
|
1242
|
+
writeFileSync(legacyPath, JSON.stringify(legacy, null, 2) + '\n', 'utf8');
|
|
1243
|
+
}
|
|
1244
|
+
} catch {}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Create the Kiro steering file if missing.
|
|
1248
|
+
const steeringDir = join(workspacePath, '.kiro', 'steering');
|
|
1249
|
+
if (!existsSync(steeringDir)) {
|
|
1250
|
+
mkdirSync(steeringDir, { recursive: true, mode: 0o755 });
|
|
1251
|
+
}
|
|
1252
|
+
const steeringPath = join(steeringDir, 'vectorforce.md');
|
|
1253
|
+
if (!existsSync(steeringPath)) {
|
|
976
1254
|
writeFileSync(
|
|
977
|
-
|
|
1255
|
+
steeringPath,
|
|
978
1256
|
`# VectorForce Agent Steering (Kiro IDE)\n\nThis workspace uses VectorForce for architectural continuity and drift prevention.\n- Use VectorForce MCP tools: 'list_tasks', 'check_workspace_drift', 'get_resume_briefing', 'save_checkpoint'.\n- Strictly adhere to invariant constraints and architectural decisions.\n`,
|
|
979
1257
|
'utf8'
|
|
980
1258
|
);
|
|
@@ -989,27 +1267,20 @@ export class HarnessConfig {
|
|
|
989
1267
|
* @returns {boolean} whether removed
|
|
990
1268
|
*/
|
|
991
1269
|
static uninstallKiro(workspacePath) {
|
|
992
|
-
const
|
|
993
|
-
|
|
1270
|
+
const candidates = [
|
|
1271
|
+
join(workspacePath, '.kiro', 'settings', 'mcp.json'),
|
|
1272
|
+
join(workspacePath, '.kiro', 'mcp.json'),
|
|
1273
|
+
join(workspacePath, 'kiro.json')
|
|
1274
|
+
];
|
|
994
1275
|
let removed = false;
|
|
995
1276
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
999
|
-
if (config?.mcpServers?.vectorforce) {
|
|
1000
|
-
delete config.mcpServers.vectorforce;
|
|
1001
|
-
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
1002
|
-
removed = true;
|
|
1003
|
-
}
|
|
1004
|
-
} catch {}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (existsSync(kiroJsonPath)) {
|
|
1277
|
+
for (const candidate of candidates) {
|
|
1278
|
+
if (!existsSync(candidate)) continue;
|
|
1008
1279
|
try {
|
|
1009
|
-
const config = JSON.parse(readFileSync(
|
|
1280
|
+
const config = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
1010
1281
|
if (config?.mcpServers?.vectorforce) {
|
|
1011
1282
|
delete config.mcpServers.vectorforce;
|
|
1012
|
-
writeFileSync(
|
|
1283
|
+
writeFileSync(candidate, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
1013
1284
|
removed = true;
|
|
1014
1285
|
}
|
|
1015
1286
|
} catch {}
|