vectorforce 0.3.2 → 0.3.4
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 +38 -0
- package/README.md +185 -9
- package/bin/vf.js +142 -55
- package/package.json +1 -1
- package/src/mcp/harness-config.js +785 -40
- package/src/mcp/mcp-server.js +85 -13
- package/src/operations/project-service.js +18 -1
- package/src/policy/authorizer.js +15 -1
|
@@ -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;
|
|
@@ -543,4 +781,511 @@ export class HarnessConfig {
|
|
|
543
781
|
|
|
544
782
|
return removed;
|
|
545
783
|
}
|
|
784
|
+
|
|
785
|
+
// ==========================================
|
|
786
|
+
// Cursor IDE Integration
|
|
787
|
+
// ==========================================
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Generate configuration object for Cursor (.cursor/mcp.json).
|
|
791
|
+
*/
|
|
792
|
+
static getCursorServerEntry() {
|
|
793
|
+
return {
|
|
794
|
+
type: 'stdio',
|
|
795
|
+
command: HarnessConfig.getNodeCommand(),
|
|
796
|
+
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
797
|
+
env: {
|
|
798
|
+
VF_HARNESS: 'cursor'
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Preview changes to Cursor configuration.
|
|
805
|
+
* @param {string} workspacePath
|
|
806
|
+
*/
|
|
807
|
+
static previewCursor(workspacePath) {
|
|
808
|
+
const dir = join(workspacePath, '.cursor');
|
|
809
|
+
const filePath = join(dir, 'mcp.json');
|
|
810
|
+
let current = null;
|
|
811
|
+
if (existsSync(filePath)) {
|
|
812
|
+
try {
|
|
813
|
+
current = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
814
|
+
} catch {
|
|
815
|
+
current = {};
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const mergedContent = {
|
|
819
|
+
...(current || {}),
|
|
820
|
+
mcpServers: {
|
|
821
|
+
...(current?.mcpServers || {}),
|
|
822
|
+
vectorforce: HarnessConfig.getCursorServerEntry()
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
return {
|
|
826
|
+
filePath,
|
|
827
|
+
exists: existsSync(filePath),
|
|
828
|
+
currentContent: current,
|
|
829
|
+
mergedContent
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Install Cursor MCP configuration and rules reversibly.
|
|
835
|
+
* @param {string} workspacePath
|
|
836
|
+
* @returns {string} filePath
|
|
837
|
+
*/
|
|
838
|
+
static installCursor(workspacePath) {
|
|
839
|
+
const { filePath, mergedContent } = HarnessConfig.previewCursor(workspacePath);
|
|
840
|
+
const dir = join(workspacePath, '.cursor');
|
|
841
|
+
if (!existsSync(dir)) {
|
|
842
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
843
|
+
}
|
|
844
|
+
writeFileSync(filePath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
845
|
+
|
|
846
|
+
// Create .cursorrules if missing
|
|
847
|
+
const cursorRulesPath = join(workspacePath, '.cursorrules');
|
|
848
|
+
if (!existsSync(cursorRulesPath)) {
|
|
849
|
+
writeFileSync(
|
|
850
|
+
cursorRulesPath,
|
|
851
|
+
`# VectorForce Agent Protocol (Cursor)\n\nThis workspace uses VectorForce for architectural continuity and drift verification.\n- Check active tasks & drift: Use MCP tools 'list_tasks' & 'check_workspace_drift'\n- Retrieve briefing: Use MCP tool 'get_resume_briefing'\n- Save checkpoint: Use MCP tool 'save_checkpoint'\n`,
|
|
852
|
+
'utf8'
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!isTempWorkspace(workspacePath)) {
|
|
857
|
+
try {
|
|
858
|
+
const globalCursorDir = join(homedir(), '.cursor');
|
|
859
|
+
if (existsSync(globalCursorDir)) {
|
|
860
|
+
const globalMcpPath = join(globalCursorDir, 'mcp.json');
|
|
861
|
+
let globalConfig = {};
|
|
862
|
+
if (existsSync(globalMcpPath)) {
|
|
863
|
+
try {
|
|
864
|
+
globalConfig = JSON.parse(readFileSync(globalMcpPath, 'utf8'));
|
|
865
|
+
} catch {
|
|
866
|
+
globalConfig = {};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
globalConfig.mcpServers = {
|
|
870
|
+
...(globalConfig.mcpServers || {}),
|
|
871
|
+
vectorforce: {
|
|
872
|
+
type: 'stdio',
|
|
873
|
+
command: HarnessConfig.getNodeCommand(),
|
|
874
|
+
args: [HarnessConfig.getBinPath(), 'mcp', '--workspace', workspacePath],
|
|
875
|
+
env: {
|
|
876
|
+
VF_WORKSPACE: workspacePath,
|
|
877
|
+
VF_HARNESS: 'cursor'
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
writeFileSync(globalMcpPath, JSON.stringify(globalConfig, null, 2) + '\n', 'utf8');
|
|
882
|
+
}
|
|
883
|
+
} catch {}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return filePath;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Reversibly uninstall VectorForce from Cursor configuration.
|
|
891
|
+
* @param {string} workspacePath
|
|
892
|
+
* @returns {boolean} whether removed
|
|
893
|
+
*/
|
|
894
|
+
static uninstallCursor(workspacePath) {
|
|
895
|
+
const filePath = join(workspacePath, '.cursor', 'mcp.json');
|
|
896
|
+
let removed = false;
|
|
897
|
+
if (existsSync(filePath)) {
|
|
898
|
+
try {
|
|
899
|
+
const config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
900
|
+
if (config?.mcpServers?.vectorforce) {
|
|
901
|
+
delete config.mcpServers.vectorforce;
|
|
902
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
903
|
+
removed = true;
|
|
904
|
+
}
|
|
905
|
+
} catch {}
|
|
906
|
+
}
|
|
907
|
+
return removed;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ==========================================
|
|
911
|
+
// Windsurf Cascade Integration
|
|
912
|
+
// ==========================================
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Generate configuration object for Windsurf (.windsurf/mcp_config.json).
|
|
916
|
+
*/
|
|
917
|
+
static getWindsurfServerEntry() {
|
|
918
|
+
return {
|
|
919
|
+
type: 'stdio',
|
|
920
|
+
command: HarnessConfig.getNodeCommand(),
|
|
921
|
+
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
922
|
+
env: {
|
|
923
|
+
VF_HARNESS: 'windsurf'
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Preview changes to Windsurf configuration.
|
|
930
|
+
* @param {string} workspacePath
|
|
931
|
+
*/
|
|
932
|
+
static previewWindsurf(workspacePath) {
|
|
933
|
+
const dir = join(workspacePath, '.windsurf');
|
|
934
|
+
const filePath = join(dir, 'mcp_config.json');
|
|
935
|
+
let current = null;
|
|
936
|
+
if (existsSync(filePath)) {
|
|
937
|
+
try {
|
|
938
|
+
current = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
939
|
+
} catch {
|
|
940
|
+
current = {};
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const mergedContent = {
|
|
944
|
+
...(current || {}),
|
|
945
|
+
mcpServers: {
|
|
946
|
+
...(current?.mcpServers || {}),
|
|
947
|
+
vectorforce: HarnessConfig.getWindsurfServerEntry()
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
return {
|
|
951
|
+
filePath,
|
|
952
|
+
exists: existsSync(filePath),
|
|
953
|
+
currentContent: current,
|
|
954
|
+
mergedContent
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Install Windsurf MCP configuration and rules reversibly.
|
|
960
|
+
* @param {string} workspacePath
|
|
961
|
+
* @returns {string} filePath
|
|
962
|
+
*/
|
|
963
|
+
static installWindsurf(workspacePath) {
|
|
964
|
+
const { filePath, mergedContent } = HarnessConfig.previewWindsurf(workspacePath);
|
|
965
|
+
const dir = join(workspacePath, '.windsurf');
|
|
966
|
+
if (!existsSync(dir)) {
|
|
967
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
968
|
+
}
|
|
969
|
+
writeFileSync(filePath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
970
|
+
|
|
971
|
+
// Create .windsurfrules if missing
|
|
972
|
+
const windsurfRulesPath = join(workspacePath, '.windsurfrules');
|
|
973
|
+
if (!existsSync(windsurfRulesPath)) {
|
|
974
|
+
writeFileSync(
|
|
975
|
+
windsurfRulesPath,
|
|
976
|
+
`# VectorForce Agent Protocol (Windsurf Cascade)\n\nThis workspace uses VectorForce for architectural continuity and drift verification.\n- Check tasks & drift: Call MCP tools 'list_tasks' and 'check_workspace_drift'\n- Resume briefing: Call MCP tool 'get_resume_briefing'\n- Save checkpoint: Call MCP tool 'save_checkpoint'\n`,
|
|
977
|
+
'utf8'
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (!isTempWorkspace(workspacePath)) {
|
|
982
|
+
try {
|
|
983
|
+
const globalWindsurfDir = join(homedir(), '.codeium', 'windsurf');
|
|
984
|
+
if (existsSync(globalWindsurfDir)) {
|
|
985
|
+
const globalMcpPath = join(globalWindsurfDir, 'mcp_config.json');
|
|
986
|
+
let globalConfig = {};
|
|
987
|
+
if (existsSync(globalMcpPath)) {
|
|
988
|
+
try {
|
|
989
|
+
globalConfig = JSON.parse(readFileSync(globalMcpPath, 'utf8'));
|
|
990
|
+
} catch {
|
|
991
|
+
globalConfig = {};
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
globalConfig.mcpServers = {
|
|
995
|
+
...(globalConfig.mcpServers || {}),
|
|
996
|
+
vectorforce: {
|
|
997
|
+
type: 'stdio',
|
|
998
|
+
command: HarnessConfig.getNodeCommand(),
|
|
999
|
+
args: [HarnessConfig.getBinPath(), 'mcp', '--workspace', workspacePath],
|
|
1000
|
+
env: {
|
|
1001
|
+
VF_WORKSPACE: workspacePath,
|
|
1002
|
+
VF_HARNESS: 'windsurf'
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
writeFileSync(globalMcpPath, JSON.stringify(globalConfig, null, 2) + '\n', 'utf8');
|
|
1007
|
+
}
|
|
1008
|
+
} catch {}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return filePath;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Reversibly uninstall VectorForce from Windsurf configuration.
|
|
1016
|
+
* @param {string} workspacePath
|
|
1017
|
+
* @returns {boolean} whether removed
|
|
1018
|
+
*/
|
|
1019
|
+
static uninstallWindsurf(workspacePath) {
|
|
1020
|
+
const filePath = join(workspacePath, '.windsurf', 'mcp_config.json');
|
|
1021
|
+
let removed = false;
|
|
1022
|
+
if (existsSync(filePath)) {
|
|
1023
|
+
try {
|
|
1024
|
+
const config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1025
|
+
if (config?.mcpServers?.vectorforce) {
|
|
1026
|
+
delete config.mcpServers.vectorforce;
|
|
1027
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
1028
|
+
removed = true;
|
|
1029
|
+
}
|
|
1030
|
+
} catch {}
|
|
1031
|
+
}
|
|
1032
|
+
return removed;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ==========================================
|
|
1036
|
+
// GitHub Copilot & VS Code Integration
|
|
1037
|
+
// ==========================================
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Generate configuration object for VS Code / GitHub Copilot (.vscode/mcp.json).
|
|
1041
|
+
*/
|
|
1042
|
+
static getCopilotServerEntry() {
|
|
1043
|
+
return {
|
|
1044
|
+
type: 'stdio',
|
|
1045
|
+
command: HarnessConfig.getNodeCommand(),
|
|
1046
|
+
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
1047
|
+
env: {
|
|
1048
|
+
VF_HARNESS: 'copilot'
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Preview changes to Copilot / VS Code configuration.
|
|
1055
|
+
* @param {string} workspacePath
|
|
1056
|
+
*/
|
|
1057
|
+
static previewCopilot(workspacePath) {
|
|
1058
|
+
const dir = join(workspacePath, '.vscode');
|
|
1059
|
+
const filePath = join(dir, 'mcp.json');
|
|
1060
|
+
let current = null;
|
|
1061
|
+
if (existsSync(filePath)) {
|
|
1062
|
+
try {
|
|
1063
|
+
current = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1064
|
+
} catch {
|
|
1065
|
+
current = {};
|
|
1066
|
+
}
|
|
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);
|
|
1071
|
+
const mergedContent = {
|
|
1072
|
+
...(current || {}),
|
|
1073
|
+
servers: {
|
|
1074
|
+
...(current?.servers || {}),
|
|
1075
|
+
vectorforce: HarnessConfig.getCopilotServerEntry()
|
|
1076
|
+
}
|
|
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
|
+
}
|
|
1087
|
+
return {
|
|
1088
|
+
filePath,
|
|
1089
|
+
exists: existsSync(filePath),
|
|
1090
|
+
currentContent: current,
|
|
1091
|
+
mergedContent,
|
|
1092
|
+
migratedLegacy
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Install GitHub Copilot workspace instructions & VS Code MCP configuration.
|
|
1098
|
+
* @param {string} workspacePath
|
|
1099
|
+
* @returns {string} filePath
|
|
1100
|
+
*/
|
|
1101
|
+
static installCopilot(workspacePath) {
|
|
1102
|
+
const { filePath, mergedContent } = HarnessConfig.previewCopilot(workspacePath);
|
|
1103
|
+
const vscodeDir = join(workspacePath, '.vscode');
|
|
1104
|
+
if (!existsSync(vscodeDir)) {
|
|
1105
|
+
mkdirSync(vscodeDir, { recursive: true, mode: 0o755 });
|
|
1106
|
+
}
|
|
1107
|
+
writeFileSync(filePath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
1108
|
+
|
|
1109
|
+
// Create .github/copilot-instructions.md if missing
|
|
1110
|
+
const githubDir = join(workspacePath, '.github');
|
|
1111
|
+
if (!existsSync(githubDir)) {
|
|
1112
|
+
mkdirSync(githubDir, { recursive: true, mode: 0o755 });
|
|
1113
|
+
}
|
|
1114
|
+
const copilotMdPath = join(githubDir, 'copilot-instructions.md');
|
|
1115
|
+
if (!existsSync(copilotMdPath)) {
|
|
1116
|
+
writeFileSync(
|
|
1117
|
+
copilotMdPath,
|
|
1118
|
+
`# VectorForce Context Protocol (GitHub Copilot)\n\nThis workspace uses VectorForce for architectural continuity and drift protection.\n- Architectural decisions (ADRs) and invariant constraints are tracked in VectorForce.\n- Check tasks and drift via VectorForce MCP or \`node bin/vf.js drift\`.\n- Always capture progress with \`node bin/vf.js checkpoint\`.\n`,
|
|
1119
|
+
'utf8'
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return filePath;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Reversibly uninstall VectorForce from Copilot / VS Code configuration.
|
|
1128
|
+
* @param {string} workspacePath
|
|
1129
|
+
* @returns {boolean} whether removed
|
|
1130
|
+
*/
|
|
1131
|
+
static uninstallCopilot(workspacePath) {
|
|
1132
|
+
const filePath = join(workspacePath, '.vscode', 'mcp.json');
|
|
1133
|
+
let removed = false;
|
|
1134
|
+
if (existsSync(filePath)) {
|
|
1135
|
+
try {
|
|
1136
|
+
const config = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
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) {
|
|
1144
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
1145
|
+
}
|
|
1146
|
+
} catch {}
|
|
1147
|
+
}
|
|
1148
|
+
return removed;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ==========================================
|
|
1152
|
+
// Kiro IDE Integration
|
|
1153
|
+
// ==========================================
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Generate configuration object for Kiro IDE (.kiro/mcp.json).
|
|
1157
|
+
*/
|
|
1158
|
+
static getKiroServerEntry() {
|
|
1159
|
+
return {
|
|
1160
|
+
type: 'stdio',
|
|
1161
|
+
command: HarnessConfig.getNodeCommand(),
|
|
1162
|
+
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
1163
|
+
env: {
|
|
1164
|
+
VF_HARNESS: 'kiro'
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Preview changes to Kiro IDE configuration.
|
|
1171
|
+
* @param {string} workspacePath
|
|
1172
|
+
*/
|
|
1173
|
+
static previewKiro(workspacePath) {
|
|
1174
|
+
const dir = join(workspacePath, '.kiro', 'settings');
|
|
1175
|
+
const filePath = join(dir, 'mcp.json');
|
|
1176
|
+
const kiroJsonPath = join(workspacePath, 'kiro.json');
|
|
1177
|
+
const legacyPath = join(workspacePath, '.kiro', 'mcp.json');
|
|
1178
|
+
|
|
1179
|
+
let current = null;
|
|
1180
|
+
if (existsSync(filePath)) {
|
|
1181
|
+
try {
|
|
1182
|
+
current = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1183
|
+
} catch {
|
|
1184
|
+
current = {};
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const mergedContent = {
|
|
1188
|
+
...(current || {}),
|
|
1189
|
+
mcpServers: {
|
|
1190
|
+
...(current?.mcpServers || {}),
|
|
1191
|
+
vectorforce: HarnessConfig.getKiroServerEntry()
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
let currentKiroJson = null;
|
|
1196
|
+
if (existsSync(kiroJsonPath)) {
|
|
1197
|
+
try {
|
|
1198
|
+
currentKiroJson = JSON.parse(readFileSync(kiroJsonPath, 'utf8'));
|
|
1199
|
+
} catch {
|
|
1200
|
+
currentKiroJson = {};
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const mergedKiroJson = {
|
|
1204
|
+
...(currentKiroJson || {}),
|
|
1205
|
+
mcpServers: {
|
|
1206
|
+
...(currentKiroJson?.mcpServers || {}),
|
|
1207
|
+
vectorforce: HarnessConfig.getKiroServerEntry()
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
return {
|
|
1212
|
+
filePath,
|
|
1213
|
+
kiroJsonPath,
|
|
1214
|
+
legacyPath,
|
|
1215
|
+
exists: existsSync(filePath) || existsSync(kiroJsonPath),
|
|
1216
|
+
currentContent: current,
|
|
1217
|
+
mergedContent,
|
|
1218
|
+
mergedKiroJson
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Install Kiro IDE MCP configuration and rules reversibly.
|
|
1224
|
+
* @param {string} workspacePath
|
|
1225
|
+
* @returns {string} filePath
|
|
1226
|
+
*/
|
|
1227
|
+
static installKiro(workspacePath) {
|
|
1228
|
+
const { filePath, kiroJsonPath, legacyPath, mergedContent, mergedKiroJson } = HarnessConfig.previewKiro(workspacePath);
|
|
1229
|
+
const dir = join(workspacePath, '.kiro', 'settings');
|
|
1230
|
+
if (!existsSync(dir)) {
|
|
1231
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
1232
|
+
}
|
|
1233
|
+
writeFileSync(filePath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
1234
|
+
writeFileSync(kiroJsonPath, JSON.stringify(mergedKiroJson, null, 2) + '\n', 'utf8');
|
|
1235
|
+
|
|
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)) {
|
|
1254
|
+
writeFileSync(
|
|
1255
|
+
steeringPath,
|
|
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`,
|
|
1257
|
+
'utf8'
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
return filePath;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Reversibly uninstall VectorForce from Kiro IDE configuration.
|
|
1266
|
+
* @param {string} workspacePath
|
|
1267
|
+
* @returns {boolean} whether removed
|
|
1268
|
+
*/
|
|
1269
|
+
static uninstallKiro(workspacePath) {
|
|
1270
|
+
const candidates = [
|
|
1271
|
+
join(workspacePath, '.kiro', 'settings', 'mcp.json'),
|
|
1272
|
+
join(workspacePath, '.kiro', 'mcp.json'),
|
|
1273
|
+
join(workspacePath, 'kiro.json')
|
|
1274
|
+
];
|
|
1275
|
+
let removed = false;
|
|
1276
|
+
|
|
1277
|
+
for (const candidate of candidates) {
|
|
1278
|
+
if (!existsSync(candidate)) continue;
|
|
1279
|
+
try {
|
|
1280
|
+
const config = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
1281
|
+
if (config?.mcpServers?.vectorforce) {
|
|
1282
|
+
delete config.mcpServers.vectorforce;
|
|
1283
|
+
writeFileSync(candidate, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
1284
|
+
removed = true;
|
|
1285
|
+
}
|
|
1286
|
+
} catch {}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
return removed;
|
|
1290
|
+
}
|
|
546
1291
|
}
|