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.
@@ -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
- return fileURLToPath(new URL('../../bin/vf.js', import.meta.url));
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
- command: 'node',
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 globalAgyDir = join(homedir(), '.gemini', 'antigravity');
85
- if (existsSync(globalAgyDir)) {
86
- const globalMcpPath = join(globalAgyDir, 'mcp_config.json');
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
- command: 'node',
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: 'node',
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
- command: 'node',
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 globalMuseDir = join(homedir(), '.config', 'muse');
313
- const globalSettingsPath = join(globalMuseDir, 'settings.json');
314
- if (existsSync(globalSettingsPath)) {
315
- const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
316
- settings.mcp_servers = {
317
- ...(settings.mcp_servers || {}),
318
- vectorforce: {
319
- transport: 'stdio',
320
- command: 'node',
321
- args: [HarnessConfig.getBinPath(), 'mcp'],
322
- env: {
323
- VF_HARNESS: 'muse-code',
324
- VF_WORKSPACE: workspacePath
325
- },
326
- mode: 'optional'
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
- delete settings.mcpServers;
330
- writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
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 globalSettingsPath = join(homedir(), '.config', 'muse', 'settings.json');
372
- if (existsSync(globalSettingsPath)) {
373
- const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
374
- if (settings?.mcp_servers?.vectorforce) {
375
- delete settings.mcp_servers.vectorforce;
376
- writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
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
- command: 'node',
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 binPath = HarnessConfig.getBinPath().replace(/\\/g, '\\\\');
405
- const serverBlock = `[mcp_servers.vectorforce]\ncommand = "node"\nargs = ["${binPath}", "mcp"]\n[mcp_servers.vectorforce.env]\nVF_HARNESS = "codex"\n`;
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
  }