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.
@@ -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 = '2024-11-05';
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: MCP_PROTOCOL_VERSION,
277
+ protocolVersion: negotiateProtocolVersion(params?.protocolVersion),
233
278
  serverInfo: {
234
279
  name: 'vectorforce',
235
- version: '0.1.0'
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
- const idHash = sha256(canonical).slice(0, 16);
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);
@@ -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.decisions) {
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.constraints) {
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 = 'project-root';
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 (62 Tests)',
225
- 'wiki': 'Architecture Wiki (ADR 001-014)',
226
- 'project-root': 'Project Root & Config'
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] || modId.toUpperCase(),
231
- category: (modId === 'policy' || modId === 'storage') ? 'Security & Data' : modId === 'providers' ? 'AI / LLM' : modId === 'mcp' ? 'Integration' : modId === 'wiki' ? 'Governance' : 'Core',
232
- owner: 'VectorForce',
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 checkpoints) {
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
- const modules = Array.from(moduleMap.values()).map(m => ({
271
- ...m,
272
- status: m.drift ? 'Drift Detected' : 'Verified',
273
- dependencies: Array.from(m.dependencies),
274
- decisions: allDecisions.slice(0, 3)
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({ modules }));
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