vectorforce 0.3.3 → 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 +16 -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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Cross-platform MCP availability: generated harness configs now use the absolute Node.js runtime (`process.execPath`) with `"type": "stdio"` instead of bare `node`, so GUI-launched clients on Windows, macOS, and Linux can spawn `vf mcp` even when `node` is not on their PATH.
|
|
14
|
+
- MCP protocol compatibility: the stdio server negotiates `protocolVersion` (`2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`), reports the package version, answers `resources/list`, `resources/templates/list`, `prompts/list`, and JSON-RPC batches, and keeps `stdout` reserved for JSON-RPC in `vf mcp` mode.
|
|
15
|
+
- VS Code / Copilot: `.vscode/mcp.json` is now written under the top-level `servers` key (the only shape VS Code reads); legacy `mcpServers.vectorforce` entries are migrated on install and removed on uninstall.
|
|
16
|
+
- Kiro IDE: workspace config is now written to `.kiro/settings/mcp.json` (the path Kiro reads) with steering in `.kiro/steering/vectorforce.md`; the legacy `.kiro/mcp.json` entry is migrated.
|
|
17
|
+
- Meta Muse: `installMuseCode` now always ensures the global `~/.config/muse/settings.json` entry under the `mcpServers` key (the block Muse Code actually reads; a stale `mcp_servers.vectorforce` entry is migrated) with no workspace pin so one entry serves every project, and no longer deletes unrelated user keys.
|
|
18
|
+
- Antigravity: global config prefers `~/.gemini/config/mcp_config.json` with fallback to `~/.gemini/antigravity/mcp_config.json`.
|
|
19
|
+
- Codex TOML now embeds quoted absolute paths for both the Node runtime and `bin/vf.js`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `vf init` no longer creates every harness config at once: it auto-detects harnesses already in use (marker files/dirs), installs only those, and otherwise creates none while printing the exact follow-up commands. Override with `vf init --harness <id[,...]|all|none>` or `vf init --all`.
|
|
23
|
+
- Added `vf config list` showing supported harnesses with detected/configured status; `vf config install`/`uninstall` reject unknown harness names with that hint.
|
|
24
|
+
- `vf doctor`, `vf status`, and `vf config doctor` now verify the VectorForce entry exists instead of only the file.
|
|
25
|
+
|
|
10
26
|
## [0.3.2] - 2026-11-01
|
|
11
27
|
|
|
12
28
|
### Added
|
package/README.md
CHANGED
|
@@ -27,6 +27,85 @@ VectorForce provides an immutable, cryptographic context layer that tracks:
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
+
## Architecture & System Topology
|
|
31
|
+
|
|
32
|
+
VectorForce is engineered with a strict **Local-First & Zero Runtime Dependency** design. It operates entirely through Node.js built-ins (`node:sqlite`, `node:crypto`, `node:http`, `node:fs`) and integrates seamlessly across local IDEs, multi-agent MCP bridges, and zero-knowledge cloud sync.
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
flowchart TD
|
|
36
|
+
subgraph AI_Harnesses["AI Coding Agent Ecosystem (MCP Protocol)"]
|
|
37
|
+
A1["⚡ Google Antigravity"]
|
|
38
|
+
A2["🦙 Meta Muse"]
|
|
39
|
+
A3["⌘ Claude / Command Code"]
|
|
40
|
+
A4["◈ OpenAI Codex"]
|
|
41
|
+
A5["🖱️ Cursor IDE"]
|
|
42
|
+
A6["🌊 Windsurf Cascade"]
|
|
43
|
+
A7["🐙 GitHub Copilot"]
|
|
44
|
+
A8["⚡ Kiro IDE"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
subgraph VectorForce_Core["VectorForce Core Engine (Zero Runtime Dependencies)"]
|
|
48
|
+
direction TB
|
|
49
|
+
MCP["MCP Server & JSON-RPC Bridge\n(src/mcp/mcp-server.js • stdio)"]
|
|
50
|
+
CLI["CLI Control Interface\n(bin/vf.js • init, status, drift, resume)"]
|
|
51
|
+
|
|
52
|
+
AUTH["Zero-Leak Policy Authorizer\n(src/policy/authorizer.js • Symlink & Containment Guard)"]
|
|
53
|
+
|
|
54
|
+
subgraph Operations["Operations & Synthesis Services"]
|
|
55
|
+
CHK["Checkpoint Service\n(Claims vs Evidence Fencing)"]
|
|
56
|
+
BRF["Briefing & Drift Service\n(Git HEAD & SHA-256 Hash Matching)"]
|
|
57
|
+
RSN["AI Reasoning Service\n(Meta Muse • Fireworks • Llama 3.3)"]
|
|
58
|
+
HDF["Handoff Service\n(HMAC-SHA256 Manifest Export/Import)"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
subgraph Providers["Zero-Dep Workspace Observers"]
|
|
62
|
+
GIT["Git Provider\n(Branch, Commit SHA, Diff, Dirty Tree)"]
|
|
63
|
+
DOC["Document Provider\n(Allowlisted Spec Reader & SHA-256 Hashes)"]
|
|
64
|
+
AST["Code Index Provider\n(AST Symbols & Import Dependency Graph)"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
DB[("Local SQLite WAL Store\n(.vectorforce/vectorforce.db\nnode:sqlite WAL mode)")]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
subgraph Studio_UI["Local VectorForce Studio (Web UI)"]
|
|
71
|
+
WEB["Studio HTTP Server (127.0.0.1:3210)\nReact + Vite + Tailwind + XYFlow"]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
subgraph Cloud_Layer["VectorForce Cloud (Modal.com / E2EE Sync)"]
|
|
75
|
+
REMOTE["Zero-Knowledge Wire & Hub\nX25519 ECDH + AES-256-GCM + Ed25519 Signature\n(Multi-Project Hub • PM Interpreter Portal)"]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
%% Wiring
|
|
79
|
+
A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 <-->|"stdio JSON-RPC 2.0"| MCP
|
|
80
|
+
CLI --> AUTH
|
|
81
|
+
MCP --> AUTH
|
|
82
|
+
AUTH --> Operations
|
|
83
|
+
Operations <--> Providers
|
|
84
|
+
Operations <--> DB
|
|
85
|
+
DB <--> WEB
|
|
86
|
+
Operations <-->|"vf push / vf pull\n(Encrypted Ciphertext)"| REMOTE
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Tech Stack Overview
|
|
92
|
+
|
|
93
|
+
VectorForce adheres to strict architectural purity, ensuring rock-solid stability, privacy, and speed without bloated dependency chains.
|
|
94
|
+
|
|
95
|
+
| Layer | Technology | Rationale / Key Capabilities |
|
|
96
|
+
| :--- | :--- | :--- |
|
|
97
|
+
| **Runtime & Core** | **Node.js (>= 22.5.0 ESM)** | Pure ES Modules, fast startup, native cross-platform support (macOS, Linux, Windows). |
|
|
98
|
+
| **Backend Dependencies** | **Zero External Runtime Dependencies** | Powered 100% by Node.js built-ins: `node:sqlite`, `node:crypto`, `node:http`, `node:fs`, `node:child_process`, `node:os`. |
|
|
99
|
+
| **Storage Engine** | **`node:sqlite` (WAL Mode)** | Local-first, zero-configuration database (`.vectorforce/vectorforce.db`) with Write-Ahead Logging for atomic concurrent reads/writes. |
|
|
100
|
+
| **Cryptographic Security** | **`node:crypto` (HMAC-SHA256, AES-256-GCM, X25519, Ed25519)** | Tamper-evident handoffs, verifiable file digests, and end-to-end zero-knowledge multi-machine sync. |
|
|
101
|
+
| **Protocol Integration** | **Model Context Protocol (MCP)** | JSON-RPC 2.0 over standard I/O (`stdio`), 9 deterministic tools, and auto-configuration for 8+ AI harnesses. |
|
|
102
|
+
| **AI Reasoning Layer** | **Meta Muse & Llama 3.3** | Native adapters for **Meta Muse (`muse-spark-1.3`)**, **Meta Muse Spark Open** (Fireworks AI / OpenRouter), Local **Ollama**, **OpenAI**, and **Anthropic** with graceful offline briefing fallback. |
|
|
103
|
+
| **AST & Code Indexer** | **Custom Zero-Dependency Regex/Tokenizer Indexer** | Extracts symbols (classes, functions, interfaces), imports, and module dependency graphs without heavy external parser bloat. |
|
|
104
|
+
| **Local Studio Frontend** | **React 18 + Vite + Tailwind CSS + XYFlow** | Interactive architecture control center, live blast radius inspector, Claims vs Evidence timeline, and memory console. |
|
|
105
|
+
| **Cloud Remote Sync** | **Modal.com Serverless Container (Debian + Node 22)** | Zero-Knowledge E2EE relay hub, scale-to-zero serverless deployment, PM Interpreter, and Multi-Project switcher. |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
30
109
|
## VectorForce Studio (Web UI)
|
|
31
110
|
|
|
32
111
|
VectorForce includes a local-first engineering control center:
|
|
@@ -47,9 +126,12 @@ node bin/vf.js studio
|
|
|
47
126
|
|
|
48
127
|
## Quickstart
|
|
49
128
|
|
|
50
|
-
### 1. Initialize a Project
|
|
129
|
+
### 1. Initialize a Project (auto-detects your AI harness)
|
|
51
130
|
```bash
|
|
52
131
|
node bin/vf.js init
|
|
132
|
+
# Only detected harnesses are configured; repos with no harness get no MCP files yet.
|
|
133
|
+
node bin/vf.js config list # supported harnesses + detected/configured status
|
|
134
|
+
node bin/vf.js config install cursor # or: --harness on init, or install all
|
|
53
135
|
```
|
|
54
136
|
|
|
55
137
|
### 2. Create and Select a Task
|
|
@@ -139,19 +221,19 @@ node bin/vf.js studio
|
|
|
139
221
|
|
|
140
222
|
VectorForce exposes a standard stdio JSON-RPC MCP server for direct integration into **Google Antigravity**, **Claude Code**, **Command Code**, **Meta Muse**, **OpenAI Codex**, **Cursor**, **Windsurf**, **GitHub Copilot**, and **Kiro IDE**.
|
|
141
223
|
|
|
142
|
-
> **How MCP Starts:** You **never** need to run `vf mcp` manually in a terminal. Each AI coding agent reads its configuration file below and automatically spawns VectorForce in the background over `stdio`.
|
|
224
|
+
> **How MCP Starts:** You **never** need to run `vf mcp` manually in a terminal. Each AI coding agent reads its configuration file below and automatically spawns VectorForce in the background over `stdio`. Generated entries use the absolute Node.js runtime path, so GUI-launched clients find it even when `node` is not on their PATH (common on Windows).
|
|
143
225
|
|
|
144
226
|
| AI Coding Client | Config File Read by Client | 1-Command Auto-Setup |
|
|
145
227
|
|---|---|---|
|
|
146
|
-
| **Google Antigravity** | `.agents/mcp_config.json` & `~/.gemini/antigravity/mcp_config.json` | `node bin/vf.js config install antigravity` |
|
|
228
|
+
| **Google Antigravity** | `.agents/mcp_config.json` & `~/.gemini/config/mcp_config.json` (or `~/.gemini/antigravity/mcp_config.json`) | `node bin/vf.js config install antigravity` |
|
|
147
229
|
| **Claude Code (`claude`)** | `.mcp.json` (project root) | `node bin/vf.js config install claude-code` |
|
|
148
230
|
| **Command Code (`cmd`)** | `.mcp.json` & `commandcode.json` | `node bin/vf.js config install command-code` |
|
|
149
|
-
| **Meta Muse (`muse`)** |
|
|
231
|
+
| **Meta Muse (`muse`)** | `~/.config/muse/settings.json` (global `mcpServers`; `.muse/mcp_config.json` kept as mirror) | `node bin/vf.js config install muse` |
|
|
150
232
|
| **OpenAI Codex (`codex`)** | `.codex/config.toml` & `~/.codex/config.toml` | `node bin/vf.js config install codex` |
|
|
151
233
|
| **Cursor IDE** | `.cursor/mcp.json` & `.cursorrules` | `node bin/vf.js config install cursor` |
|
|
152
234
|
| **Windsurf (Codeium)** | `.windsurf/mcp_config.json` & `.windsurfrules` | `node bin/vf.js config install windsurf` |
|
|
153
|
-
| **GitHub Copilot / VS Code** | `.vscode/mcp.json` & `.github/copilot-instructions.md` | `node bin/vf.js config install copilot` |
|
|
154
|
-
| **Kiro IDE** | `.kiro/mcp.json` & `kiro.json` | `node bin/vf.js config install kiro` |
|
|
235
|
+
| **GitHub Copilot / VS Code** | `.vscode/mcp.json` (top-level `servers`) & `.github/copilot-instructions.md` | `node bin/vf.js config install copilot` |
|
|
236
|
+
| **Kiro IDE** | `.kiro/settings/mcp.json` & `kiro.json` | `node bin/vf.js config install kiro` |
|
|
155
237
|
|
|
156
238
|
### Automated Configuration & Health Check
|
|
157
239
|
```bash
|
|
@@ -167,7 +249,7 @@ node bin/vf.js status
|
|
|
167
249
|
{
|
|
168
250
|
"mcpServers": {
|
|
169
251
|
"vectorforce": {
|
|
170
|
-
"command": "node",
|
|
252
|
+
"command": "/absolute/path/to/node",
|
|
171
253
|
"args": ["/absolute/path/to/VectorForce/bin/vf.js", "mcp"]
|
|
172
254
|
}
|
|
173
255
|
}
|
|
@@ -177,7 +259,7 @@ node bin/vf.js status
|
|
|
177
259
|
### TOML Configuration (OpenAI Codex CLI: `.codex/config.toml`)
|
|
178
260
|
```toml
|
|
179
261
|
[mcp_servers.vectorforce]
|
|
180
|
-
command = "node"
|
|
262
|
+
command = "/absolute/path/to/node"
|
|
181
263
|
args = ["/absolute/path/to/VectorForce/bin/vf.js", "mcp"]
|
|
182
264
|
[mcp_servers.vectorforce.env]
|
|
183
265
|
VF_HARNESS = "codex"
|
package/bin/vf.js
CHANGED
|
@@ -146,7 +146,7 @@ function printUsage() {
|
|
|
146
146
|
VectorForce (vf) - Git for AI Engineering Context
|
|
147
147
|
|
|
148
148
|
Usage:
|
|
149
|
-
vf init [project_name]
|
|
149
|
+
vf init [project_name] [--harness <id|all|none>] Init workspace (auto-detects harnesses)
|
|
150
150
|
vf task <id> [title] Create or register a task
|
|
151
151
|
vf tasks [active|completed|all] List tasks in workspace
|
|
152
152
|
vf policy list List allowed document paths
|
|
@@ -162,7 +162,7 @@ Usage:
|
|
|
162
162
|
vf studio [options] Launch VectorForce Studio web control center
|
|
163
163
|
vf ui [options] Alias for vf studio
|
|
164
164
|
vf mcp Start stdio MCP server for Antigravity / Command Code
|
|
165
|
-
vf config
|
|
165
|
+
vf config list|install|doctor List, install, or check harness MCP integrations
|
|
166
166
|
vf help Show this help
|
|
167
167
|
|
|
168
168
|
Checkpoint options:
|
|
@@ -235,7 +235,15 @@ function parseFlags(args) {
|
|
|
235
235
|
rationale: parts.slice(2).join(':') || 'Approved architecture decision'
|
|
236
236
|
});
|
|
237
237
|
} else if (arg === '--harness' && args[i + 1]) {
|
|
238
|
-
|
|
238
|
+
const harnessValue = args[++i];
|
|
239
|
+
flags.harness = harnessValue;
|
|
240
|
+
flags.harnessList = (flags.harnessList || []).concat(
|
|
241
|
+
harnessValue.split(',').map((value) => value.trim()).filter(Boolean)
|
|
242
|
+
);
|
|
243
|
+
} else if (arg === '--all') {
|
|
244
|
+
flags.all = true;
|
|
245
|
+
} else if (arg === '--none') {
|
|
246
|
+
flags.none = true;
|
|
239
247
|
} else if (arg === '--author' && args[i + 1]) {
|
|
240
248
|
flags.author = args[++i];
|
|
241
249
|
} else if (arg === '--next' && args[i + 1]) {
|
|
@@ -330,14 +338,34 @@ async function main() {
|
|
|
330
338
|
case 'init': {
|
|
331
339
|
const name = args[1] && !args[1].startsWith('--') ? args[1] : null;
|
|
332
340
|
const project = projectService.initProject(cwd, name);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
const harnessRequest = flags.harnessList && flags.harnessList.length > 0 ? flags.harnessList : null;
|
|
342
|
+
let harnessTargets = [];
|
|
343
|
+
if (flags.all || (harnessRequest && harnessRequest.some((h) => h.toLowerCase() === 'all'))) {
|
|
344
|
+
harnessTargets = HarnessConfig.getSupportedHarnesses().map((h) => h.id);
|
|
345
|
+
} else if (flags.none || (harnessRequest && harnessRequest.some((h) => h.toLowerCase() === 'none'))) {
|
|
346
|
+
harnessTargets = [];
|
|
347
|
+
} else if (harnessRequest) {
|
|
348
|
+
const unknown = [];
|
|
349
|
+
for (const requested of harnessRequest) {
|
|
350
|
+
const resolved = HarnessConfig.resolveHarnessId(requested);
|
|
351
|
+
if (resolved && !harnessTargets.includes(resolved)) harnessTargets.push(resolved);
|
|
352
|
+
else if (!resolved) unknown.push(requested);
|
|
353
|
+
}
|
|
354
|
+
if (unknown.length > 0) {
|
|
355
|
+
console.error(`Unknown harness: ${unknown.join(', ')}. Run "vf config list" for supported values.`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
harnessTargets = HarnessConfig.detectHarnesses(cwd).map((h) => h.id);
|
|
360
|
+
}
|
|
361
|
+
const installedHarnesses = [];
|
|
362
|
+
for (const id of harnessTargets) {
|
|
363
|
+
try {
|
|
364
|
+
installedHarnesses.push({ id, path: HarnessConfig.installHarness(cwd, id) });
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.warn(`Warning: failed to configure ${id}: ${err.message}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
341
369
|
const agentsMdPath = join(cwd, 'AGENTS.md');
|
|
342
370
|
if (!existsSync(agentsMdPath)) {
|
|
343
371
|
writeFileSync(
|
|
@@ -347,6 +375,13 @@ async function main() {
|
|
|
347
375
|
);
|
|
348
376
|
}
|
|
349
377
|
console.log(`Initialized VectorForce project: ${project.name} (${project.project_id})`);
|
|
378
|
+
if (installedHarnesses.length > 0) {
|
|
379
|
+
console.log(`Configured harness integration(s): ${installedHarnesses.map((h) => h.id).join(', ')}`);
|
|
380
|
+
console.log('For other harnesses, run "vf config list" or "vf config install <harness|all>".');
|
|
381
|
+
} else {
|
|
382
|
+
console.log('No harness detected, so no MCP configs were created.');
|
|
383
|
+
console.log('Run "vf config list" to see supported harnesses, then "vf config install <harness>".');
|
|
384
|
+
}
|
|
350
385
|
break;
|
|
351
386
|
}
|
|
352
387
|
|
|
@@ -551,6 +586,8 @@ async function main() {
|
|
|
551
586
|
}
|
|
552
587
|
|
|
553
588
|
case 'mcp': {
|
|
589
|
+
console.log = (...args) => console.error(...args);
|
|
590
|
+
console.info = (...args) => console.error(...args);
|
|
554
591
|
const server = new McpServer({ store, projectService, checkpointService, briefingService, handoffService, reasoningService }, cwd);
|
|
555
592
|
server.connect(process.stdin, process.stdout);
|
|
556
593
|
process.on('SIGINT', () => {
|
|
@@ -568,57 +605,63 @@ async function main() {
|
|
|
568
605
|
case 'config': {
|
|
569
606
|
const sub = args[1];
|
|
570
607
|
if (sub === 'install' || sub === 'setup') {
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
if (target === 'windsurf' || target === 'all') {
|
|
594
|
-
const p = HarnessConfig.installWindsurf(cwd);
|
|
595
|
-
installed.push(`Windsurf -> ${p} (+ .windsurfrules)`);
|
|
608
|
+
const raw = (args[2] || 'all').toLowerCase();
|
|
609
|
+
const installNotes = {
|
|
610
|
+
antigravity: (p) => `Google Antigravity -> ${p} (+ global ~/.gemini MCP config when present)`,
|
|
611
|
+
'command-code': (p) => `Command Code / Claude Code -> .mcp.json & ${p}`,
|
|
612
|
+
muse: (p) => `Meta Muse -> ${p} (+ ~/.config/muse/settings.json)`,
|
|
613
|
+
codex: (p) => `Codex -> ${p} (+ ~/.codex/config.toml)`,
|
|
614
|
+
cursor: (p) => `Cursor -> ${p} (+ .cursorrules)`,
|
|
615
|
+
windsurf: (p) => `Windsurf -> ${p} (+ .windsurfrules)`,
|
|
616
|
+
copilot: (p) => `GitHub Copilot / VS Code -> ${p} (+ .github/copilot-instructions.md)`,
|
|
617
|
+
kiro: (p) => `Kiro IDE -> ${p} (+ kiro.json)`
|
|
618
|
+
};
|
|
619
|
+
let targets = [];
|
|
620
|
+
if (raw === 'all') {
|
|
621
|
+
targets = HarnessConfig.getSupportedHarnesses().map((h) => h.id);
|
|
622
|
+
} else {
|
|
623
|
+
const resolved = HarnessConfig.resolveHarnessId(raw);
|
|
624
|
+
if (!resolved) {
|
|
625
|
+
console.error(`Unknown harness "${args[2]}". Run "vf config list" for supported values.`);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
targets = [resolved];
|
|
596
629
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
installed.push(
|
|
600
|
-
}
|
|
601
|
-
if (target === 'kiro' || target === 'kiro-ide' || target === 'all') {
|
|
602
|
-
const p = HarnessConfig.installKiro(cwd);
|
|
603
|
-
installed.push(`Kiro IDE -> ${p} (+ kiro.json)`);
|
|
630
|
+
const installed = [];
|
|
631
|
+
for (const id of targets) {
|
|
632
|
+
installed.push(installNotes[id](HarnessConfig.installHarness(cwd, id)));
|
|
604
633
|
}
|
|
605
634
|
console.log(`\n✔ Configured harness integration(s):\n - ${installed.join('\n - ')}`);
|
|
606
635
|
console.log(`\n💡 Next step: Restart or reload your AI agent, then ask in chat:\n "List active VectorForce tasks and check workspace drift"`);
|
|
607
636
|
break;
|
|
608
637
|
}
|
|
609
638
|
|
|
639
|
+
if (sub === 'list' || sub === 'harnesses') {
|
|
640
|
+
const detected = new Set(HarnessConfig.detectHarnesses(cwd).map((h) => h.id));
|
|
641
|
+
console.log(`\nSupported AI harnesses (${cwd}):\n`);
|
|
642
|
+
for (const harness of HarnessConfig.getSupportedHarnesses()) {
|
|
643
|
+
const aliases = harness.aliases ? ` (aliases: ${harness.aliases.join(', ')})` : '';
|
|
644
|
+
const detectedMark = detected.has(harness.id) ? 'detected' : 'not detected';
|
|
645
|
+
const configuredMark = HarnessConfig.isHarnessConfigured(cwd, harness.id) ? 'configured' : 'not configured';
|
|
646
|
+
console.log(` ${harness.id}${aliases}: ${harness.label} [${detectedMark}] [${configuredMark}]`);
|
|
647
|
+
}
|
|
648
|
+
console.log('\nSetup one harness: vf config install <harness> (e.g. vf config install cursor)');
|
|
649
|
+
console.log('Setup all harnesses: vf config install all\n');
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
|
|
610
653
|
if (sub === 'doctor' || sub === 'check') {
|
|
611
654
|
const checks = [
|
|
612
655
|
{ name: 'SQLite Database (.vectorforce/vectorforce.db)', ok: existsSync(getDbPath(cwd)), path: getDbPath(cwd) },
|
|
613
656
|
{ name: 'Agent Protocol Rules (AGENTS.md)', ok: existsSync(join(cwd, 'AGENTS.md')), path: join(cwd, 'AGENTS.md') },
|
|
614
|
-
{ name: 'Google Antigravity (.agents/mcp_config.json)', ok:
|
|
615
|
-
{ name: 'Claude Code & Command Code (.mcp.json)', ok:
|
|
616
|
-
{ name: 'Meta Muse (.muse/mcp_config.json)', ok:
|
|
617
|
-
{ name: 'OpenAI Codex (.codex/config.toml)', ok:
|
|
618
|
-
{ name: 'Cursor (.cursor/mcp.json)', ok:
|
|
619
|
-
{ name: 'Windsurf (.windsurf/mcp_config.json)', ok:
|
|
620
|
-
{ name: 'GitHub Copilot / VS Code (.vscode/mcp.json)', ok:
|
|
621
|
-
{ name: 'Kiro IDE (.kiro/mcp.json)', ok:
|
|
657
|
+
{ name: 'Google Antigravity (.agents/mcp_config.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'antigravity'), path: join(cwd, '.agents', 'mcp_config.json') },
|
|
658
|
+
{ name: 'Claude Code & Command Code (.mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'command-code'), path: join(cwd, '.mcp.json') },
|
|
659
|
+
{ name: 'Meta Muse (.muse/mcp_config.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'muse'), path: join(cwd, '.muse', 'mcp_config.json') },
|
|
660
|
+
{ name: 'OpenAI Codex (.codex/config.toml)', ok: HarnessConfig.isHarnessConfigured(cwd, 'codex'), path: join(cwd, '.codex', 'config.toml') },
|
|
661
|
+
{ name: 'Cursor (.cursor/mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'cursor'), path: join(cwd, '.cursor', 'mcp.json') },
|
|
662
|
+
{ name: 'Windsurf (.windsurf/mcp_config.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'windsurf'), path: join(cwd, '.windsurf', 'mcp_config.json') },
|
|
663
|
+
{ name: 'GitHub Copilot / VS Code (.vscode/mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'copilot'), path: join(cwd, '.vscode', 'mcp.json') },
|
|
664
|
+
{ name: 'Kiro IDE (.kiro/settings/mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'kiro'), path: join(cwd, '.kiro', 'settings', 'mcp.json') }
|
|
622
665
|
];
|
|
623
666
|
console.log(`\nVectorForce Workspace & MCP Health Check (${cwd}):\n`);
|
|
624
667
|
for (const c of checks) {
|
|
@@ -631,31 +674,22 @@ async function main() {
|
|
|
631
674
|
}
|
|
632
675
|
|
|
633
676
|
if (sub === 'uninstall' || sub === 'remove') {
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
677
|
+
const raw = (args[2] || 'all').toLowerCase();
|
|
678
|
+
const displayNames = { antigravity: 'Antigravity', 'command-code': 'Command Code', muse: 'Muse Code', codex: 'Codex', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot / VS Code', kiro: 'Kiro IDE' };
|
|
679
|
+
let targets = [];
|
|
680
|
+
if (raw === 'all') {
|
|
681
|
+
targets = HarnessConfig.getSupportedHarnesses().map((h) => h.id);
|
|
682
|
+
} else {
|
|
683
|
+
const resolved = HarnessConfig.resolveHarnessId(raw);
|
|
684
|
+
if (!resolved) {
|
|
685
|
+
console.error(`Unknown harness "${args[2]}". Run "vf config list" for supported values.`);
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
targets = [resolved];
|
|
641
689
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (target === 'codex' || target === 'all') {
|
|
646
|
-
if (HarnessConfig.uninstallCodex(cwd)) uninstalled.push('Codex');
|
|
647
|
-
}
|
|
648
|
-
if (target === 'cursor' || target === 'all') {
|
|
649
|
-
if (HarnessConfig.uninstallCursor(cwd)) uninstalled.push('Cursor');
|
|
650
|
-
}
|
|
651
|
-
if (target === 'windsurf' || target === 'all') {
|
|
652
|
-
if (HarnessConfig.uninstallWindsurf(cwd)) uninstalled.push('Windsurf');
|
|
653
|
-
}
|
|
654
|
-
if (target === 'copilot' || target === 'github-copilot' || target === 'vscode' || target === 'all') {
|
|
655
|
-
if (HarnessConfig.uninstallCopilot(cwd)) uninstalled.push('GitHub Copilot / VS Code');
|
|
656
|
-
}
|
|
657
|
-
if (target === 'kiro' || target === 'kiro-ide' || target === 'all') {
|
|
658
|
-
if (HarnessConfig.uninstallKiro(cwd)) uninstalled.push('Kiro IDE');
|
|
690
|
+
const uninstalled = [];
|
|
691
|
+
for (const id of targets) {
|
|
692
|
+
if (HarnessConfig.uninstallHarness(cwd, id)) uninstalled.push(displayNames[id]);
|
|
659
693
|
}
|
|
660
694
|
console.log(`\n✔ Removed harness integration(s): ${uninstalled.length ? uninstalled.join(', ') : 'none found'}`);
|
|
661
695
|
break;
|
|
@@ -879,7 +913,7 @@ VectorForce Harness Configuration:
|
|
|
879
913
|
{
|
|
880
914
|
"mcpServers": {
|
|
881
915
|
"vectorforce": {
|
|
882
|
-
"command": "
|
|
916
|
+
"command": "${process.execPath}",
|
|
883
917
|
"args": ["${binPath}", "mcp"]
|
|
884
918
|
}
|
|
885
919
|
}
|
|
@@ -891,7 +925,7 @@ VectorForce Harness Configuration:
|
|
|
891
925
|
{
|
|
892
926
|
"mcpServers": {
|
|
893
927
|
"vectorforce": {
|
|
894
|
-
"command": "
|
|
928
|
+
"command": "${process.execPath}",
|
|
895
929
|
"args": ["${binPath}", "mcp"],
|
|
896
930
|
"env": { "VF_HARNESS": "muse-code" }
|
|
897
931
|
}
|
|
@@ -906,7 +940,7 @@ VectorForce Harness Configuration:
|
|
|
906
940
|
File: .codex/config.toml (or codex.json)
|
|
907
941
|
Content:
|
|
908
942
|
[mcp_servers.vectorforce]
|
|
909
|
-
command = "
|
|
943
|
+
command = "${process.execPath}"
|
|
910
944
|
args = ["${binPath}", "mcp"]
|
|
911
945
|
[mcp_servers.vectorforce.env]
|
|
912
946
|
VF_HARNESS = "codex"
|
|
@@ -943,6 +977,9 @@ VectorForce Harness Configuration:
|
|
|
943
977
|
VF_OPENROUTER_MODEL=deepseek/deepseek-v4.1-flash-1017 # default
|
|
944
978
|
|
|
945
979
|
Privacy: set VF_LLM_ALLOW_CODE=false (or run with --no-code) to keep source out of the prompt.
|
|
980
|
+
|
|
981
|
+
Harnesses: run "vf config list" to see detected/configured harnesses,
|
|
982
|
+
then "vf config install <harness|all>" to configure one or all of them.
|
|
946
983
|
`);
|
|
947
984
|
break;
|
|
948
985
|
}
|
|
@@ -983,9 +1020,15 @@ VectorForce Harness Configuration:
|
|
|
983
1020
|
const remoteProjectId = flags.project || remote.remoteProjectId || 'default';
|
|
984
1021
|
const project = projectService.getProjectForWorkspace(cwd);
|
|
985
1022
|
const tasks = store.listTasks(project.project_id);
|
|
986
|
-
const
|
|
1023
|
+
const rawHost = hostname();
|
|
1024
|
+
const machineId = flags.machine || (
|
|
1025
|
+
rawHost.toLowerCase().includes('lenovo') ? `Lenovo Laptop (${rawHost})` :
|
|
1026
|
+
rawHost.toLowerCase().includes('macbook') || process.platform === 'darwin' ? `MacBook Pro (${rawHost})` :
|
|
1027
|
+
`${rawHost} (${process.platform})`
|
|
1028
|
+
);
|
|
1029
|
+
const devIdentity = flags.dev || `${process.env.USER || 'developer'}`;
|
|
987
1030
|
|
|
988
|
-
console.log(`🔄 Pushing local context (${project.name}) -> ${targetUrl} ...`);
|
|
1031
|
+
console.log(`🔄 Pushing local context (${project.name}) -> ${targetUrl} [Machine: ${machineId}] ...`);
|
|
989
1032
|
|
|
990
1033
|
for (const t of tasks) {
|
|
991
1034
|
if (flags.task && t.task_id !== flags.task) continue;
|
|
@@ -999,8 +1042,8 @@ VectorForce Harness Configuration:
|
|
|
999
1042
|
projectId: remoteProjectId,
|
|
1000
1043
|
taskId: t.task_id,
|
|
1001
1044
|
title: t.title || t.task_id,
|
|
1002
|
-
assignee:
|
|
1003
|
-
role: `
|
|
1045
|
+
assignee: devIdentity,
|
|
1046
|
+
role: `Workspace (${basename(cwd)})`,
|
|
1004
1047
|
priority: 'P0',
|
|
1005
1048
|
machineId,
|
|
1006
1049
|
harness: flags.harness || 'antigravity'
|
|
@@ -1025,13 +1068,17 @@ VectorForce Harness Configuration:
|
|
|
1025
1068
|
taskId: cp.task_id,
|
|
1026
1069
|
senderMachineId: machineId,
|
|
1027
1070
|
recipientMachineId: 'computer-2-meta-muse-cloud',
|
|
1028
|
-
authorName: `${cp.author?.name ||
|
|
1029
|
-
authorHarness: cp.author?.harness || 'antigravity',
|
|
1071
|
+
authorName: `${cp.author?.name || devIdentity} (${basename(cwd)})`,
|
|
1072
|
+
authorHarness: cp.author?.harness || flags.harness || 'antigravity',
|
|
1030
1073
|
summary: cp.summary,
|
|
1031
1074
|
goals: cp.claims?.goals || [],
|
|
1032
1075
|
constraints: cp.claims?.constraints || [],
|
|
1033
1076
|
decisions: cp.claims?.decisions || [],
|
|
1034
|
-
nextSteps: cp.claims?.next_steps || []
|
|
1077
|
+
nextSteps: cp.claims?.next_steps || [],
|
|
1078
|
+
gitCommit: cp.evidence?.git?.commit || null,
|
|
1079
|
+
gitBranch: cp.evidence?.git?.branch || 'main',
|
|
1080
|
+
gitDirty: cp.evidence?.git?.dirty !== undefined ? Boolean(cp.evidence.git.dirty) : false,
|
|
1081
|
+
documents: (cp.evidence?.documents || []).map((d) => d.path)
|
|
1035
1082
|
})
|
|
1036
1083
|
});
|
|
1037
1084
|
lastResp = await res.json();
|
|
@@ -1134,14 +1181,14 @@ VectorForce Harness Configuration:
|
|
|
1134
1181
|
const checks = [
|
|
1135
1182
|
{ name: 'SQLite Database (.vectorforce/vectorforce.db)', ok: existsSync(getDbPath(cwd)) },
|
|
1136
1183
|
{ name: 'Agent Protocol Rules (AGENTS.md)', ok: existsSync(join(cwd, 'AGENTS.md')) },
|
|
1137
|
-
{ name: 'Google Antigravity (.agents/mcp_config.json)', ok:
|
|
1138
|
-
{ name: 'Claude Code & Command Code (.mcp.json)', ok:
|
|
1139
|
-
{ name: 'Meta Muse (.muse/mcp_config.json or musecode.json)', ok:
|
|
1140
|
-
{ name: 'OpenAI Codex (.codex/config.toml)', ok:
|
|
1141
|
-
{ name: 'Cursor (.cursor/mcp.json)', ok:
|
|
1142
|
-
{ name: 'Windsurf (.windsurf/mcp_config.json)', ok:
|
|
1143
|
-
{ name: 'GitHub Copilot / VS Code (.vscode/mcp.json)', ok:
|
|
1144
|
-
{ name: 'Kiro IDE (.kiro/mcp.json)', ok:
|
|
1184
|
+
{ name: 'Google Antigravity (.agents/mcp_config.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'antigravity') },
|
|
1185
|
+
{ name: 'Claude Code & Command Code (.mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'command-code') },
|
|
1186
|
+
{ name: 'Meta Muse (.muse/mcp_config.json or musecode.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'muse') },
|
|
1187
|
+
{ name: 'OpenAI Codex (.codex/config.toml)', ok: HarnessConfig.isHarnessConfigured(cwd, 'codex') },
|
|
1188
|
+
{ name: 'Cursor (.cursor/mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'cursor') },
|
|
1189
|
+
{ name: 'Windsurf (.windsurf/mcp_config.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'windsurf') },
|
|
1190
|
+
{ name: 'GitHub Copilot / VS Code (.vscode/mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'copilot') },
|
|
1191
|
+
{ name: 'Kiro IDE (.kiro/settings/mcp.json)', ok: HarnessConfig.isHarnessConfigured(cwd, 'kiro') }
|
|
1145
1192
|
];
|
|
1146
1193
|
console.log(`\nVectorForce Workspace & MCP Status (${cwd}):\n`);
|
|
1147
1194
|
for (const c of checks) {
|
package/package.json
CHANGED
|
@@ -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 {}
|
package/src/mcp/mcp-server.js
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
1
4
|
import { ErrorCode, VectorForceError } from '../core/errors.js';
|
|
2
5
|
|
|
3
|
-
export const MCP_PROTOCOL_VERSION = '
|
|
6
|
+
export const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
7
|
+
|
|
8
|
+
export const MCP_SUPPORTED_PROTOCOL_VERSIONS = [
|
|
9
|
+
'2025-11-25',
|
|
10
|
+
'2025-06-18',
|
|
11
|
+
'2025-03-26',
|
|
12
|
+
'2024-11-05'
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function readServerVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
18
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version || '0.3.2';
|
|
19
|
+
} catch {
|
|
20
|
+
return '0.3.2';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const MCP_SERVER_VERSION = readServerVersion();
|
|
25
|
+
|
|
26
|
+
export function negotiateProtocolVersion(clientVersion) {
|
|
27
|
+
if (clientVersion && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)) {
|
|
28
|
+
return clientVersion;
|
|
29
|
+
}
|
|
30
|
+
return MCP_PROTOCOL_VERSION;
|
|
31
|
+
}
|
|
4
32
|
|
|
5
33
|
export const MCP_TOOLS = [
|
|
6
34
|
{
|
|
@@ -214,14 +242,31 @@ export class McpServer {
|
|
|
214
242
|
return;
|
|
215
243
|
}
|
|
216
244
|
|
|
245
|
+
// JSON-RPC batch: dispatch each entry; entries without an id are notifications.
|
|
246
|
+
if (Array.isArray(req)) {
|
|
247
|
+
if (req.length === 0) {
|
|
248
|
+
this._sendError(null, -32600, 'Invalid Request: empty batch');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (const sub of req) {
|
|
252
|
+
this._dispatch(sub);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this._dispatch(req);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_dispatch(req) {
|
|
261
|
+
if (!req || typeof req !== 'object' || Array.isArray(req)) {
|
|
262
|
+
this._sendError(null, -32600, 'Invalid Request');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
217
266
|
const { id, method, params } = req;
|
|
218
267
|
|
|
219
|
-
// Notifications (no id)
|
|
268
|
+
// Notifications (no id) are acknowledged silently, never answered.
|
|
220
269
|
if (id === undefined || id === null) {
|
|
221
|
-
if (method === 'notifications/initialized') {
|
|
222
|
-
// Client handshake complete
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
270
|
return;
|
|
226
271
|
}
|
|
227
272
|
|
|
@@ -229,15 +274,17 @@ export class McpServer {
|
|
|
229
274
|
switch (method) {
|
|
230
275
|
case 'initialize': {
|
|
231
276
|
this._sendResult(id, {
|
|
232
|
-
protocolVersion:
|
|
277
|
+
protocolVersion: negotiateProtocolVersion(params?.protocolVersion),
|
|
233
278
|
serverInfo: {
|
|
234
279
|
name: 'vectorforce',
|
|
235
|
-
version:
|
|
280
|
+
version: MCP_SERVER_VERSION
|
|
236
281
|
},
|
|
237
282
|
capabilities: {
|
|
238
283
|
tools: {
|
|
239
284
|
listChanged: false
|
|
240
|
-
}
|
|
285
|
+
},
|
|
286
|
+
resources: {},
|
|
287
|
+
prompts: {}
|
|
241
288
|
}
|
|
242
289
|
});
|
|
243
290
|
break;
|
|
@@ -255,6 +302,31 @@ export class McpServer {
|
|
|
255
302
|
break;
|
|
256
303
|
}
|
|
257
304
|
|
|
305
|
+
case 'resources/list': {
|
|
306
|
+
this._sendResult(id, { resources: [] });
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'resources/templates/list': {
|
|
311
|
+
this._sendResult(id, { resourceTemplates: [] });
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case 'resources/read': {
|
|
316
|
+
this._sendError(id, -32602, `Unknown resource: ${params?.uri || '(missing uri)'}`);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case 'prompts/list': {
|
|
321
|
+
this._sendResult(id, { prompts: [] });
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case 'prompts/get': {
|
|
326
|
+
this._sendError(id, -32602, `Unknown prompt: ${params?.name || '(missing name)'}`);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
|
|
258
330
|
case 'tools/call': {
|
|
259
331
|
this._handleToolCall(id, params);
|
|
260
332
|
break;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Authorizer } from '../policy/authorizer.js';
|
|
2
2
|
import { sha256 } from '../core/hash.js';
|
|
3
3
|
import { basename, dirname } from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import { ErrorCode, VectorForceError } from '../core/errors.js';
|
|
5
6
|
|
|
6
7
|
export class ProjectService {
|
|
@@ -13,6 +14,8 @@ export class ProjectService {
|
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Initialize or retrieve a project for a workspace directory.
|
|
17
|
+
* Derives a deterministic project identity from Git remote URL if present,
|
|
18
|
+
* ensuring that multiple developers cloning the same repo share the identical project ID.
|
|
16
19
|
* @param {string} workspacePath
|
|
17
20
|
* @param {string} [customName]
|
|
18
21
|
* @returns {{ project_id: string, canonical_path: string, name: string, created_at: string }}
|
|
@@ -23,7 +26,21 @@ export class ProjectService {
|
|
|
23
26
|
if (existing) {
|
|
24
27
|
return existing;
|
|
25
28
|
}
|
|
26
|
-
|
|
29
|
+
|
|
30
|
+
let identitySeed = canonical;
|
|
31
|
+
try {
|
|
32
|
+
const gitRemote = execFileSync('git', ['config', '--get', 'remote.origin.url'], {
|
|
33
|
+
cwd: canonical,
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
36
|
+
timeout: 1000
|
|
37
|
+
}).trim();
|
|
38
|
+
if (gitRemote) {
|
|
39
|
+
identitySeed = gitRemote.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
|
|
43
|
+
const idHash = sha256(identitySeed).slice(0, 16);
|
|
27
44
|
const projectId = `proj-${idHash}`;
|
|
28
45
|
const name = customName || basename(canonical);
|
|
29
46
|
return this.store.registerProject(projectId, canonical, name);
|