vectorforce 0.3.3 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +90 -8
- package/bin/vf.js +140 -93
- package/package.json +1 -1
- package/src/mcp/harness-config.js +346 -75
- package/src/mcp/mcp-server.js +81 -9
- package/src/operations/project-service.js +18 -1
- package/src/studio/server.js +104 -18
- package/studio/dist/assets/{index-C0_eVpV1.js → index-Cizk5468.js} +1 -1
- package/studio/dist/assets/index-w62XkQKM.css +2 -0
- package/studio/dist/index.html +2 -2
- package/studio/dist/assets/index-CxTK-2ty.css +0 -2
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);
|
package/src/studio/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import { ReasoningService } from '../operations/reasoning-service.js';
|
|
|
13
13
|
import { CodeIndexProvider } from '../providers/code-index-provider.js';
|
|
14
14
|
import { LlmProvider, resolveLlmConfig } from '../providers/llm-provider.js';
|
|
15
15
|
import { GitProvider } from '../providers/git-provider.js';
|
|
16
|
+
import { DocumentProvider } from '../providers/document-provider.js';
|
|
16
17
|
import { HarnessConfig } from '../mcp/harness-config.js';
|
|
17
18
|
|
|
18
19
|
export function createStudioServer({
|
|
@@ -136,7 +137,7 @@ export function createStudioServer({
|
|
|
136
137
|
const allConstraints = new Set();
|
|
137
138
|
|
|
138
139
|
for (const cp of checkpoints) {
|
|
139
|
-
if (cp.claims
|
|
140
|
+
if (cp.claims?.decisions) {
|
|
140
141
|
for (const d of cp.claims.decisions) {
|
|
141
142
|
if (!seenDecisions.has(d.id)) {
|
|
142
143
|
seenDecisions.add(d.id);
|
|
@@ -144,18 +145,58 @@ export function createStudioServer({
|
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
|
-
if (cp.claims
|
|
148
|
+
if (cp.claims?.constraints) {
|
|
148
149
|
for (const c of cp.claims.constraints) {
|
|
149
150
|
allConstraints.add(c);
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
// Collect allowlisted docs details (sizes, hashes) safely
|
|
156
|
+
let docDetails = [];
|
|
157
|
+
try {
|
|
158
|
+
const collected = DocumentProvider.collect(workspacePath, allowedDocs, allowedDocs);
|
|
159
|
+
docDetails = (collected.documents || []).map(d => ({
|
|
160
|
+
path: d.relative_path,
|
|
161
|
+
size: d.size_bytes ? (d.size_bytes < 1024 ? `${d.size_bytes} B` : `${(d.size_bytes / 1024).toFixed(1)} kB`) : '0 B',
|
|
162
|
+
hash: d.sha256_hash ? d.sha256_hash.slice(0, 10) + '...' : 'tracked',
|
|
163
|
+
status: 'Verified'
|
|
164
|
+
}));
|
|
165
|
+
for (const g of (collected.gaps || [])) {
|
|
166
|
+
docDetails.push({
|
|
167
|
+
path: g.relative_path,
|
|
168
|
+
size: 'Missing',
|
|
169
|
+
hash: 'N/A',
|
|
170
|
+
status: 'File Missing'
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
docDetails = (allowedDocs || []).map(p => ({ path: p, size: 'Tracked', hash: 'allowlisted', status: 'Verified' }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Extract real top AST symbols from code index
|
|
178
|
+
const topSymbols = [];
|
|
179
|
+
for (const f of codeIndex.files) {
|
|
180
|
+
if (f.symbols) {
|
|
181
|
+
for (const c of (f.symbols.classes || [])) {
|
|
182
|
+
topSymbols.push({ name: c, type: 'Class', file: f.path });
|
|
183
|
+
}
|
|
184
|
+
for (const fn of (f.symbols.functions || [])) {
|
|
185
|
+
topSymbols.push({ name: fn, type: 'Function', file: f.path });
|
|
186
|
+
}
|
|
187
|
+
for (const exp of (f.symbols.exports || [])) {
|
|
188
|
+
topSymbols.push({ name: exp, type: 'Export', file: f.path });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
154
193
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
155
194
|
res.end(JSON.stringify({
|
|
156
195
|
filesIndexed: codeIndex.files.length,
|
|
157
196
|
symbolsIndexed: codeIndex.files.reduce((acc, f) => acc + ((f.symbols?.functions?.length || 0) + (f.symbols?.classes?.length || 0) + (f.symbols?.exports?.length || 0)), 0),
|
|
158
197
|
allowedDocuments: allowedDocs,
|
|
198
|
+
allowedDocsList: docDetails,
|
|
199
|
+
topSymbols: topSymbols.slice(0, 30),
|
|
159
200
|
decisions: allDecisions,
|
|
160
201
|
constraints: Array.from(allConstraints),
|
|
161
202
|
totalCheckpoints: checkpoints.length
|
|
@@ -188,6 +229,12 @@ export function createStudioServer({
|
|
|
188
229
|
const codeIndex = CodeIndexProvider.build(workspacePath);
|
|
189
230
|
const checkpoints = store.getCheckpoints(project.project_id);
|
|
190
231
|
const changedFiles = new Set(GitProvider.collectChangedFiles(workspacePath).files || []);
|
|
232
|
+
const requestedTaskId = url.searchParams.get('taskId') || null;
|
|
233
|
+
|
|
234
|
+
// If a task is selected, get its allowlisted docs or touched files
|
|
235
|
+
const taskCheckpoints = requestedTaskId
|
|
236
|
+
? checkpoints.filter(cp => cp.task_id === requestedTaskId)
|
|
237
|
+
: checkpoints;
|
|
191
238
|
|
|
192
239
|
// Dynamically cluster code files by architectural module
|
|
193
240
|
const moduleMap = new Map();
|
|
@@ -207,7 +254,7 @@ export function createStudioServer({
|
|
|
207
254
|
} else if (file.path.startsWith('bin/')) {
|
|
208
255
|
modId = 'cli';
|
|
209
256
|
} else {
|
|
210
|
-
modId = '
|
|
257
|
+
modId = parts.length > 1 ? parts[0] : 'root';
|
|
211
258
|
}
|
|
212
259
|
|
|
213
260
|
if (!moduleMap.has(modId)) {
|
|
@@ -221,15 +268,26 @@ export function createStudioServer({
|
|
|
221
268
|
'core': 'Core Hashes & Errors',
|
|
222
269
|
'cli': 'Unified CLI Interface',
|
|
223
270
|
'studio-ui': 'React Studio Frontend',
|
|
224
|
-
'tests': 'Automated Test Suite
|
|
225
|
-
'wiki': 'Architecture Wiki
|
|
226
|
-
'
|
|
271
|
+
'tests': 'Automated Test Suite',
|
|
272
|
+
'wiki': 'Architecture Wiki',
|
|
273
|
+
'root': 'Project Root & Config',
|
|
274
|
+
'backend': 'Backend Services',
|
|
275
|
+
'frontend': 'Frontend Application',
|
|
276
|
+
'api': 'API Routes & Handlers',
|
|
277
|
+
'models': 'Data Models & Schemas',
|
|
278
|
+
'controllers': 'Controllers',
|
|
279
|
+
'routes': 'Router Layer',
|
|
280
|
+
'services': 'Domain Services',
|
|
281
|
+
'utils': 'Utility Helpers'
|
|
227
282
|
};
|
|
283
|
+
const capitalizedMod = modId.charAt(0).toUpperCase() + modId.slice(1);
|
|
228
284
|
moduleMap.set(modId, {
|
|
229
285
|
id: modId,
|
|
230
|
-
name: prettyNames[modId] ||
|
|
231
|
-
category: (modId === 'policy' || modId === 'storage'
|
|
232
|
-
|
|
286
|
+
name: prettyNames[modId] || capitalizedMod,
|
|
287
|
+
category: (modId === 'policy' || modId === 'storage' || modId === 'auth' || modId === 'security') ? 'Security' :
|
|
288
|
+
(modId === 'providers' || modId === 'ai' || modId === 'llm') ? 'AI / Copilot' :
|
|
289
|
+
(modId === 'mcp' || modId === 'api' || modId === 'routes') ? 'Integration' : 'Core',
|
|
290
|
+
owner: project.name || 'Workspace',
|
|
233
291
|
files: [],
|
|
234
292
|
drift: false,
|
|
235
293
|
dependencies: new Set(),
|
|
@@ -259,23 +317,51 @@ export function createStudioServer({
|
|
|
259
317
|
|
|
260
318
|
// Link real ADR decisions from checkpoints
|
|
261
319
|
const allDecisions = [];
|
|
262
|
-
for (const cp of
|
|
320
|
+
for (const cp of taskCheckpoints) {
|
|
263
321
|
if (cp.claims?.decisions) {
|
|
264
322
|
for (const d of cp.claims.decisions) {
|
|
265
|
-
allDecisions.push(`${d.id}: ${d.title}`);
|
|
323
|
+
allDecisions.push(`${d.id}: ${d.title || d.rationale || ''}`);
|
|
266
324
|
}
|
|
267
325
|
}
|
|
268
326
|
}
|
|
269
327
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
328
|
+
// Compute clean 2D grid coordinates for modules
|
|
329
|
+
const modArray = Array.from(moduleMap.values());
|
|
330
|
+
const cols = Math.min(3, Math.max(2, Math.ceil(Math.sqrt(modArray.length))));
|
|
331
|
+
const colWidth = 270;
|
|
332
|
+
const rowHeight = 160;
|
|
333
|
+
const startX = 40;
|
|
334
|
+
const startY = 40;
|
|
335
|
+
|
|
336
|
+
const connections = [];
|
|
337
|
+
for (const [fromMod, mod] of moduleMap.entries()) {
|
|
338
|
+
for (const toMod of mod.dependencies) {
|
|
339
|
+
connections.push({ from: fromMod, to: toMod });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const modules = modArray.map((m, idx) => {
|
|
344
|
+
const col = idx % cols;
|
|
345
|
+
const row = Math.floor(idx / cols);
|
|
346
|
+
return {
|
|
347
|
+
...m,
|
|
348
|
+
x: startX + col * colWidth,
|
|
349
|
+
y: startY + row * rowHeight,
|
|
350
|
+
status: m.drift ? 'Drift Detected' : 'Verified',
|
|
351
|
+
dependencies: Array.from(m.dependencies),
|
|
352
|
+
decisions: allDecisions.slice(0, 3),
|
|
353
|
+
files: m.files.length,
|
|
354
|
+
description: `${m.files.length} file${m.files.length === 1 ? '' : 's'} · ${m.symbolsCount} symbols indexed.`
|
|
355
|
+
};
|
|
356
|
+
});
|
|
276
357
|
|
|
277
358
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
278
|
-
res.end(JSON.stringify({
|
|
359
|
+
res.end(JSON.stringify({
|
|
360
|
+
modules,
|
|
361
|
+
connections,
|
|
362
|
+
activeTask: requestedTaskId,
|
|
363
|
+
totalFiles: codeIndex.files.length
|
|
364
|
+
}));
|
|
279
365
|
return;
|
|
280
366
|
}
|
|
281
367
|
|