vectorforce 0.0.0-stage → 0.2.0
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/.agents/agents/AGENTS.md +12 -0
- package/.agents/agents/vectorforce-architect.md +35 -0
- package/.agents/agents/vectorforce-auditor.md +25 -0
- package/.agents/agents/vectorforce-samples-builder.md +30 -0
- package/.agents/mcp_config.json +11 -0
- package/.codex/config.toml +5 -0
- package/AGENTS.md +115 -0
- package/LICENSE +21 -0
- package/README.md +198 -2
- package/bin/vf.js +1159 -0
- package/codex.json +14 -0
- package/commandcode.json +11 -0
- package/musecode.json +14 -0
- package/package.json +40 -4
- package/src/core/errors.js +45 -0
- package/src/core/hash.js +56 -0
- package/src/core/types.js +194 -0
- package/src/index.js +17 -0
- package/src/mcp/harness-config.js +546 -0
- package/src/mcp/mcp-server.js +507 -0
- package/src/operations/briefing-service.js +313 -0
- package/src/operations/checkpoint-service.js +120 -0
- package/src/operations/handoff-service.js +343 -0
- package/src/operations/project-service.js +132 -0
- package/src/operations/reasoning-service.js +356 -0
- package/src/policy/authorizer.js +147 -0
- package/src/providers/code-index-provider.js +285 -0
- package/src/providers/document-provider.js +105 -0
- package/src/providers/git-provider.js +98 -0
- package/src/providers/llm-provider.js +425 -0
- package/src/storage/sqlite-store.js +387 -0
- package/src/studio/server.js +764 -0
- package/studio/dist/assets/index-C0_eVpV1.js +9 -0
- package/studio/dist/assets/index-CxTK-2ty.css +2 -0
- package/studio/dist/favicon.svg +1 -0
- package/studio/dist/icons.svg +24 -0
- package/studio/dist/index.html +17 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join, extname } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { SqliteStore } from '../storage/sqlite-store.js';
|
|
8
|
+
import { ProjectService } from '../operations/project-service.js';
|
|
9
|
+
import { CheckpointService } from '../operations/checkpoint-service.js';
|
|
10
|
+
import { BriefingService } from '../operations/briefing-service.js';
|
|
11
|
+
import { HandoffService } from '../operations/handoff-service.js';
|
|
12
|
+
import { ReasoningService } from '../operations/reasoning-service.js';
|
|
13
|
+
import { CodeIndexProvider } from '../providers/code-index-provider.js';
|
|
14
|
+
import { LlmProvider, resolveLlmConfig } from '../providers/llm-provider.js';
|
|
15
|
+
import { GitProvider } from '../providers/git-provider.js';
|
|
16
|
+
import { HarnessConfig } from '../mcp/harness-config.js';
|
|
17
|
+
|
|
18
|
+
export function createStudioServer({
|
|
19
|
+
dbPath,
|
|
20
|
+
workspacePath = process.cwd(),
|
|
21
|
+
port = 3210
|
|
22
|
+
} = {}) {
|
|
23
|
+
const store = new SqliteStore(dbPath);
|
|
24
|
+
const projectService = new ProjectService(store);
|
|
25
|
+
const checkpointService = new CheckpointService(store, projectService);
|
|
26
|
+
const briefingService = new BriefingService(store, projectService);
|
|
27
|
+
const handoffService = new HandoffService(store, projectService, checkpointService);
|
|
28
|
+
|
|
29
|
+
let llmProvider = null;
|
|
30
|
+
let llmError = null;
|
|
31
|
+
try {
|
|
32
|
+
const config = resolveLlmConfig(process.env, {}, workspacePath);
|
|
33
|
+
llmProvider = new LlmProvider(config);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
llmError = err.message;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const reasoningService = new ReasoningService(store, projectService, briefingService, {
|
|
39
|
+
llmProvider,
|
|
40
|
+
codeIndexProvider: CodeIndexProvider
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function setCorsHeaders(res) {
|
|
44
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
45
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
46
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const server = createServer(async (req, res) => {
|
|
50
|
+
setCorsHeaders(res);
|
|
51
|
+
if (req.method === 'OPTIONS') {
|
|
52
|
+
res.writeHead(204);
|
|
53
|
+
res.end();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
58
|
+
const pathname = url.pathname;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (pathname === '/api/status' && req.method === 'GET') {
|
|
62
|
+
const project = projectService.initProject(workspacePath);
|
|
63
|
+
const tasks = store.listTasks(project.project_id, 'all');
|
|
64
|
+
const activeTasks = tasks.filter(t => t.status === 'active');
|
|
65
|
+
const checkpoints = store.getCheckpoints(project.project_id, { limit: 100 });
|
|
66
|
+
const git = GitProvider.collect(workspacePath).git;
|
|
67
|
+
|
|
68
|
+
let driftCount = 0;
|
|
69
|
+
let driftDetails = [];
|
|
70
|
+
for (const task of activeTasks) {
|
|
71
|
+
try {
|
|
72
|
+
const briefing = briefingService.generateBriefing({
|
|
73
|
+
workspacePath,
|
|
74
|
+
taskId: task.task_id,
|
|
75
|
+
destination: 'cli'
|
|
76
|
+
});
|
|
77
|
+
if (briefing.driftDetected) {
|
|
78
|
+
driftCount++;
|
|
79
|
+
driftDetails.push({ taskId: task.task_id, warnings: briefing.warnings });
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Task might have 0 checkpoints
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let activeLlmConfig = null;
|
|
87
|
+
let activeLlmError = null;
|
|
88
|
+
try {
|
|
89
|
+
activeLlmConfig = resolveLlmConfig(process.env, {}, workspacePath);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
activeLlmError = err.message;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const healthScore = Math.max(70, 100 - (driftCount * 12) - (activeTasks.length > 5 ? 5 : 0));
|
|
95
|
+
|
|
96
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({
|
|
98
|
+
project: {
|
|
99
|
+
id: project.project_id,
|
|
100
|
+
name: project.name,
|
|
101
|
+
path: project.canonical_path
|
|
102
|
+
},
|
|
103
|
+
ai: {
|
|
104
|
+
connected: !activeLlmError && !!activeLlmConfig,
|
|
105
|
+
provider: activeLlmConfig ? activeLlmConfig.provider : 'ollama',
|
|
106
|
+
model: activeLlmConfig ? activeLlmConfig.model : 'llama3.1:8b',
|
|
107
|
+
baseUrl: activeLlmConfig ? activeLlmConfig.baseUrl : null,
|
|
108
|
+
error: activeLlmError
|
|
109
|
+
},
|
|
110
|
+
tasks: {
|
|
111
|
+
total: tasks.length,
|
|
112
|
+
active: activeTasks.length,
|
|
113
|
+
list: tasks
|
|
114
|
+
},
|
|
115
|
+
checkpointsCount: checkpoints.length,
|
|
116
|
+
lastCheckpoint: checkpoints[checkpoints.length - 1] || null,
|
|
117
|
+
git: git ? { commit: git.commit_hash, branch: git.branch, isDirty: git.is_dirty } : null,
|
|
118
|
+
health: {
|
|
119
|
+
score: healthScore,
|
|
120
|
+
driftDetected: driftCount > 0,
|
|
121
|
+
driftCount,
|
|
122
|
+
driftDetails
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (pathname === '/api/memory' && req.method === 'GET') {
|
|
129
|
+
const project = projectService.initProject(workspacePath);
|
|
130
|
+
const checkpoints = store.getCheckpoints(project.project_id);
|
|
131
|
+
const allowedDocs = store.getDocumentPolicy(project.project_id);
|
|
132
|
+
const codeIndex = CodeIndexProvider.build(workspacePath);
|
|
133
|
+
|
|
134
|
+
const allDecisions = [];
|
|
135
|
+
const seenDecisions = new Set();
|
|
136
|
+
const allConstraints = new Set();
|
|
137
|
+
|
|
138
|
+
for (const cp of checkpoints) {
|
|
139
|
+
if (cp.claims.decisions) {
|
|
140
|
+
for (const d of cp.claims.decisions) {
|
|
141
|
+
if (!seenDecisions.has(d.id)) {
|
|
142
|
+
seenDecisions.add(d.id);
|
|
143
|
+
allDecisions.push({ ...d, taskId: cp.task_id, timestamp: cp.timestamp });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (cp.claims.constraints) {
|
|
148
|
+
for (const c of cp.claims.constraints) {
|
|
149
|
+
allConstraints.add(c);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
155
|
+
res.end(JSON.stringify({
|
|
156
|
+
filesIndexed: codeIndex.files.length,
|
|
157
|
+
symbolsIndexed: codeIndex.files.reduce((acc, f) => acc + ((f.symbols?.functions?.length || 0) + (f.symbols?.classes?.length || 0) + (f.symbols?.exports?.length || 0)), 0),
|
|
158
|
+
allowedDocuments: allowedDocs,
|
|
159
|
+
decisions: allDecisions,
|
|
160
|
+
constraints: Array.from(allConstraints),
|
|
161
|
+
totalCheckpoints: checkpoints.length
|
|
162
|
+
}));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (pathname === '/api/timeline' && req.method === 'GET') {
|
|
167
|
+
const project = projectService.initProject(workspacePath);
|
|
168
|
+
const days = parseInt(url.searchParams.get('days') || '7', 10);
|
|
169
|
+
const taskId = url.searchParams.get('taskId') || undefined;
|
|
170
|
+
const recap = briefingService.generateRecap({
|
|
171
|
+
workspacePath,
|
|
172
|
+
days,
|
|
173
|
+
taskId,
|
|
174
|
+
destination: 'cli'
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
178
|
+
res.end(JSON.stringify({
|
|
179
|
+
recapText: recap.recapText,
|
|
180
|
+
checkpoints: recap.checkpoints,
|
|
181
|
+
taskCount: recap.taskCount
|
|
182
|
+
}));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (pathname === '/api/graph' && req.method === 'GET') {
|
|
187
|
+
const project = projectService.initProject(workspacePath);
|
|
188
|
+
const codeIndex = CodeIndexProvider.build(workspacePath);
|
|
189
|
+
const checkpoints = store.getCheckpoints(project.project_id);
|
|
190
|
+
const changedFiles = new Set(GitProvider.collectChangedFiles(workspacePath).files || []);
|
|
191
|
+
|
|
192
|
+
// Dynamically cluster code files by architectural module
|
|
193
|
+
const moduleMap = new Map();
|
|
194
|
+
for (const file of codeIndex.files) {
|
|
195
|
+
const parts = file.path.split('/');
|
|
196
|
+
let modId = parts[0];
|
|
197
|
+
if (file.path === 'src/index.js') {
|
|
198
|
+
modId = 'core';
|
|
199
|
+
} else if (file.path.startsWith('src/')) {
|
|
200
|
+
modId = parts[1] || 'core';
|
|
201
|
+
} else if (file.path.startsWith('test/')) {
|
|
202
|
+
modId = 'tests';
|
|
203
|
+
} else if (file.path.startsWith('wiki/')) {
|
|
204
|
+
modId = 'wiki';
|
|
205
|
+
} else if (file.path.startsWith('studio/')) {
|
|
206
|
+
modId = 'studio-ui';
|
|
207
|
+
} else if (file.path.startsWith('bin/')) {
|
|
208
|
+
modId = 'cli';
|
|
209
|
+
} else {
|
|
210
|
+
modId = 'project-root';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!moduleMap.has(modId)) {
|
|
214
|
+
const prettyNames = {
|
|
215
|
+
'policy': 'Security & Boundaries',
|
|
216
|
+
'storage': 'SQLite Context Store',
|
|
217
|
+
'operations': 'Continuity & Handoff Services',
|
|
218
|
+
'providers': 'AI LLM & AST Code Index',
|
|
219
|
+
'mcp': 'Harness MCP Bridge',
|
|
220
|
+
'studio': 'Studio API Server',
|
|
221
|
+
'core': 'Core Hashes & Errors',
|
|
222
|
+
'cli': 'Unified CLI Interface',
|
|
223
|
+
'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'
|
|
227
|
+
};
|
|
228
|
+
moduleMap.set(modId, {
|
|
229
|
+
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',
|
|
233
|
+
files: [],
|
|
234
|
+
drift: false,
|
|
235
|
+
dependencies: new Set(),
|
|
236
|
+
symbolsCount: 0
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const m = moduleMap.get(modId);
|
|
240
|
+
m.files.push(file.path);
|
|
241
|
+
if (changedFiles.has(file.path)) {
|
|
242
|
+
m.drift = true;
|
|
243
|
+
}
|
|
244
|
+
if (file.symbols) {
|
|
245
|
+
m.symbolsCount += (file.symbols.functions?.length || 0) + (file.symbols.classes?.length || 0) + (file.symbols.exports?.length || 0);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Map codeIndex.edges to module dependencies
|
|
250
|
+
for (const edge of codeIndex.edges) {
|
|
251
|
+
const fromParts = edge.from.split('/');
|
|
252
|
+
const toParts = edge.to.split('/');
|
|
253
|
+
const fromMod = fromParts[0] === 'src' ? (fromParts[1] || 'core') : fromParts[0];
|
|
254
|
+
const toMod = toParts[0] === 'src' ? (toParts[1] || 'core') : toParts[0];
|
|
255
|
+
if (fromMod !== toMod && moduleMap.has(fromMod) && moduleMap.has(toMod)) {
|
|
256
|
+
moduleMap.get(fromMod).dependencies.add(toMod);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Link real ADR decisions from checkpoints
|
|
261
|
+
const allDecisions = [];
|
|
262
|
+
for (const cp of checkpoints) {
|
|
263
|
+
if (cp.claims?.decisions) {
|
|
264
|
+
for (const d of cp.claims.decisions) {
|
|
265
|
+
allDecisions.push(`${d.id}: ${d.title}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
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
|
+
}));
|
|
276
|
+
|
|
277
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
278
|
+
res.end(JSON.stringify({ modules }));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (pathname === '/api/team' && req.method === 'GET') {
|
|
283
|
+
const project = projectService.initProject(workspacePath);
|
|
284
|
+
const checkpoints = store.getCheckpoints(project.project_id);
|
|
285
|
+
|
|
286
|
+
const authorMap = new Map();
|
|
287
|
+
for (const cp of checkpoints) {
|
|
288
|
+
const name = cp.author?.name || 'developer';
|
|
289
|
+
if (!authorMap.has(name)) {
|
|
290
|
+
authorMap.set(name, {
|
|
291
|
+
id: name.toLowerCase().replace(/[^a-z0-9]/g, '-'),
|
|
292
|
+
name,
|
|
293
|
+
role: name === 'omprakashprajapat' ? 'Lead Architect / Owner' : 'Collaborator',
|
|
294
|
+
harness: cp.author?.harness || 'cli',
|
|
295
|
+
tasks: new Set(),
|
|
296
|
+
checkpointsCount: 0,
|
|
297
|
+
lastActive: cp.timestamp
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const a = authorMap.get(name);
|
|
301
|
+
a.tasks.add(cp.task_id);
|
|
302
|
+
a.checkpointsCount++;
|
|
303
|
+
if (new Date(cp.timestamp) > new Date(a.lastActive)) {
|
|
304
|
+
a.lastActive = cp.timestamp;
|
|
305
|
+
a.harness = cp.author?.harness || a.harness;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const team = Array.from(authorMap.values()).map(a => ({
|
|
310
|
+
...a,
|
|
311
|
+
tasks: Array.from(a.tasks)
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
315
|
+
res.end(JSON.stringify({ team, totalCheckpoints: checkpoints.length }));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (pathname === '/api/handoff/export' && req.method === 'POST') {
|
|
320
|
+
let body = '';
|
|
321
|
+
req.on('data', chunk => { body += chunk; });
|
|
322
|
+
req.on('end', async () => {
|
|
323
|
+
try {
|
|
324
|
+
const data = JSON.parse(body || '{}');
|
|
325
|
+
const project = projectService.getProjectForWorkspace(workspacePath);
|
|
326
|
+
const allTasks = store.listTasks(project.project_id);
|
|
327
|
+
|
|
328
|
+
let resolvedTaskId = data.taskId;
|
|
329
|
+
if (!resolvedTaskId || !allTasks.some(t => t.task_id === resolvedTaskId)) {
|
|
330
|
+
const activeTask = allTasks.find(t => t.status === 'active') || allTasks[0];
|
|
331
|
+
if (activeTask) {
|
|
332
|
+
resolvedTaskId = activeTask.task_id;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let resolvedCheckpointId = data.checkpointId || undefined;
|
|
337
|
+
if (!resolvedCheckpointId && resolvedTaskId) {
|
|
338
|
+
const leaves = store.getLeafCheckpoints(project.project_id, resolvedTaskId);
|
|
339
|
+
if (leaves.length > 1) {
|
|
340
|
+
const sorted = [...leaves].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
341
|
+
resolvedCheckpointId = sorted[0].checkpoint_id;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const result = handoffService.exportHandoff({
|
|
346
|
+
workspacePath,
|
|
347
|
+
taskId: resolvedTaskId || 'phase-5-pilot',
|
|
348
|
+
checkpointId: resolvedCheckpointId,
|
|
349
|
+
recipient: data.recipient || 'Collaborator',
|
|
350
|
+
destinationHarness: data.destinationHarness || 'any'
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const bundle = {
|
|
354
|
+
schema_version: 'vectorforce/handoff-bundle/v1',
|
|
355
|
+
package_id: result.package_id,
|
|
356
|
+
created_at: result.handoff?.created_at || new Date().toISOString(),
|
|
357
|
+
manifest: result.manifest,
|
|
358
|
+
handoff: result.handoff
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
362
|
+
res.end(JSON.stringify({ ...result, bundle }));
|
|
363
|
+
} catch (err) {
|
|
364
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
365
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (pathname === '/api/handoff/import' && req.method === 'POST') {
|
|
372
|
+
let body = '';
|
|
373
|
+
req.on('data', chunk => { body += chunk; });
|
|
374
|
+
req.on('end', async () => {
|
|
375
|
+
try {
|
|
376
|
+
const data = JSON.parse(body || '{}');
|
|
377
|
+
let targetBundlePath = data.bundlePath;
|
|
378
|
+
|
|
379
|
+
if (!targetBundlePath && data.bundleContent) {
|
|
380
|
+
const handoffsDir = join(homedir(), '.vectorforce', 'handoffs');
|
|
381
|
+
mkdirSync(handoffsDir, { recursive: true, mode: 0o700 });
|
|
382
|
+
const rawStr = typeof data.bundleContent === 'string'
|
|
383
|
+
? data.bundleContent
|
|
384
|
+
: JSON.stringify(data.bundleContent, null, 2);
|
|
385
|
+
targetBundlePath = join(handoffsDir, `import-${randomUUID().slice(0, 8)}.vfhandoff.json`);
|
|
386
|
+
writeFileSync(targetBundlePath, rawStr, 'utf8');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const result = handoffService.importHandoff({
|
|
390
|
+
workspacePath,
|
|
391
|
+
bundlePath: targetBundlePath,
|
|
392
|
+
targetTaskId: data.targetTaskId || undefined
|
|
393
|
+
});
|
|
394
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
395
|
+
res.end(JSON.stringify(result));
|
|
396
|
+
} catch (err) {
|
|
397
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
398
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (pathname === '/api/reason' && req.method === 'POST') {
|
|
405
|
+
let body = '';
|
|
406
|
+
req.on('data', chunk => { body += chunk; });
|
|
407
|
+
req.on('end', async () => {
|
|
408
|
+
try {
|
|
409
|
+
const data = JSON.parse(body || '{}');
|
|
410
|
+
const result = await reasoningService.reasonAboutTask({
|
|
411
|
+
workspacePath,
|
|
412
|
+
taskId: data.taskId,
|
|
413
|
+
checkpointId: data.checkpointId,
|
|
414
|
+
focus: data.focus || undefined,
|
|
415
|
+
model: data.model || undefined,
|
|
416
|
+
destination: 'cli',
|
|
417
|
+
maxTokens: 4000,
|
|
418
|
+
includeCode: data.includeCode !== false,
|
|
419
|
+
_viaStudio: true
|
|
420
|
+
});
|
|
421
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
422
|
+
res.end(JSON.stringify(result));
|
|
423
|
+
} catch (err) {
|
|
424
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
425
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (pathname === '/api/checkpoint' && req.method === 'POST') {
|
|
432
|
+
let body = '';
|
|
433
|
+
req.on('data', chunk => { body += chunk; });
|
|
434
|
+
req.on('end', async () => {
|
|
435
|
+
try {
|
|
436
|
+
const data = JSON.parse(body || '{}');
|
|
437
|
+
const result = checkpointService.captureCheckpoint({
|
|
438
|
+
workspacePath,
|
|
439
|
+
taskId: data.taskId || 'main',
|
|
440
|
+
summary: data.summary,
|
|
441
|
+
goals: data.goals || [],
|
|
442
|
+
constraints: data.constraints || [],
|
|
443
|
+
decisions: data.decisions || [],
|
|
444
|
+
nextSteps: data.nextSteps || [],
|
|
445
|
+
requestedDocuments: data.requestedDocuments || [],
|
|
446
|
+
includeGit: data.includeGit !== false,
|
|
447
|
+
authorName: data.authorName || 'developer',
|
|
448
|
+
authorHarness: data.authorHarness || 'studio'
|
|
449
|
+
});
|
|
450
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
451
|
+
res.end(JSON.stringify(result));
|
|
452
|
+
} catch (err) {
|
|
453
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
454
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Interactive Studio Configuration & Task/To-Do Endpoints ---
|
|
461
|
+
|
|
462
|
+
if (pathname === '/api/config/llm' && req.method === 'GET') {
|
|
463
|
+
let activeCfg = null;
|
|
464
|
+
let error = null;
|
|
465
|
+
try {
|
|
466
|
+
activeCfg = resolveLlmConfig(process.env, {}, workspacePath);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
error = err.message;
|
|
469
|
+
}
|
|
470
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
471
|
+
res.end(JSON.stringify({
|
|
472
|
+
provider: activeCfg?.provider || 'ollama',
|
|
473
|
+
model: activeCfg?.model || 'llama3.3',
|
|
474
|
+
baseUrl: activeCfg?.baseUrl || 'http://127.0.0.1:11434',
|
|
475
|
+
hasApiKey: Boolean(activeCfg?.apiKey),
|
|
476
|
+
error
|
|
477
|
+
}));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (pathname === '/api/config/llm' && req.method === 'POST') {
|
|
482
|
+
let body = '';
|
|
483
|
+
req.on('data', chunk => { body += chunk; });
|
|
484
|
+
req.on('end', () => {
|
|
485
|
+
try {
|
|
486
|
+
const data = JSON.parse(body || '{}');
|
|
487
|
+
const provider = String(data.provider || 'ollama').toLowerCase().trim();
|
|
488
|
+
const model = String(data.model || '').trim();
|
|
489
|
+
const baseUrl = String(data.baseUrl || '').trim();
|
|
490
|
+
const apiKey = String(data.apiKey || '').trim();
|
|
491
|
+
|
|
492
|
+
const vfDir = join(workspacePath, '.vectorforce');
|
|
493
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
494
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
495
|
+
let existing = {};
|
|
496
|
+
if (existsSync(cfgPath)) {
|
|
497
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
existing.VF_LLM_PROVIDER = provider;
|
|
501
|
+
if (provider === 'fireworks') {
|
|
502
|
+
if (apiKey) existing.FIREWORKS_API_KEY = apiKey;
|
|
503
|
+
existing.VF_FIREWORKS_MODEL = model || 'accounts/fireworks/models/muse-spark-open';
|
|
504
|
+
if (baseUrl) existing.VF_FIREWORKS_BASE_URL = baseUrl;
|
|
505
|
+
} else if (provider === 'muse') {
|
|
506
|
+
if (apiKey) existing.MODEL_API_KEY = apiKey;
|
|
507
|
+
existing.VF_MUSE_MODEL = model || 'muse-spark-1.3';
|
|
508
|
+
if (baseUrl) existing.VF_MUSE_BASE_URL = baseUrl;
|
|
509
|
+
} else if (provider === 'openrouter') {
|
|
510
|
+
if (apiKey) existing.OPENROUTER_API_KEY = apiKey;
|
|
511
|
+
existing.VF_OPENROUTER_MODEL = model || 'meta/muse-spark-open';
|
|
512
|
+
} else if (provider === 'ollama') {
|
|
513
|
+
existing.VF_OLLAMA_MODEL = model || 'llama3.3';
|
|
514
|
+
existing.VF_OLLAMA_BASE_URL = baseUrl || 'http://127.0.0.1:11434';
|
|
515
|
+
} else if (provider === 'openai' || provider === 'codex') {
|
|
516
|
+
if (apiKey) existing.VF_LLM_API_KEY = apiKey;
|
|
517
|
+
existing.VF_LLM_MODEL = model || 'gpt-4.1';
|
|
518
|
+
existing.VF_LLM_BASE_URL = baseUrl || 'https://api.openai.com/v1';
|
|
519
|
+
} else if (provider === 'anthropic') {
|
|
520
|
+
if (apiKey) existing.ANTHROPIC_API_KEY = apiKey;
|
|
521
|
+
existing.VF_ANTHROPIC_MODEL = model || 'claude-sonnet-4-5';
|
|
522
|
+
if (baseUrl) existing.VF_ANTHROPIC_BASE_URL = baseUrl;
|
|
523
|
+
} else if (provider === 'byok') {
|
|
524
|
+
if (apiKey) existing.VF_LLM_API_KEY = apiKey;
|
|
525
|
+
if (model) existing.VF_LLM_MODEL = model;
|
|
526
|
+
if (baseUrl) existing.VF_LLM_BASE_URL = baseUrl;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
530
|
+
|
|
531
|
+
// Hot-reload reasoningService LLM provider
|
|
532
|
+
let resolved = null;
|
|
533
|
+
let resolveErr = null;
|
|
534
|
+
try {
|
|
535
|
+
resolved = resolveLlmConfig(process.env, {}, workspacePath);
|
|
536
|
+
reasoningService.llmProvider = new LlmProvider(resolved);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
resolveErr = err.message;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
542
|
+
res.end(JSON.stringify({
|
|
543
|
+
ok: true,
|
|
544
|
+
provider: resolved?.provider || provider,
|
|
545
|
+
model: resolved?.model || model,
|
|
546
|
+
baseUrl: resolved?.baseUrl || baseUrl,
|
|
547
|
+
hasApiKey: Boolean(resolved?.apiKey || apiKey),
|
|
548
|
+
error: resolveErr
|
|
549
|
+
}));
|
|
550
|
+
} catch (err) {
|
|
551
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
552
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (pathname === '/api/policy' && req.method === 'POST') {
|
|
559
|
+
let body = '';
|
|
560
|
+
req.on('data', chunk => { body += chunk; });
|
|
561
|
+
req.on('end', () => {
|
|
562
|
+
try {
|
|
563
|
+
const data = JSON.parse(body || '{}');
|
|
564
|
+
const project = projectService.initProject(workspacePath);
|
|
565
|
+
const current = store.getDocumentPolicy(project.project_id);
|
|
566
|
+
let nextList = [...current];
|
|
567
|
+
if (Array.isArray(data.allowedDocuments)) {
|
|
568
|
+
nextList = data.allowedDocuments;
|
|
569
|
+
} else if (data.action === 'remove' && data.path) {
|
|
570
|
+
nextList = current.filter(p => p !== data.path);
|
|
571
|
+
} else if (data.path) {
|
|
572
|
+
if (!nextList.includes(data.path)) nextList.push(data.path);
|
|
573
|
+
}
|
|
574
|
+
const updated = store.setDocumentPolicy(project.project_id, nextList);
|
|
575
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
576
|
+
res.end(JSON.stringify({ ok: true, allowedDocuments: updated }));
|
|
577
|
+
} catch (err) {
|
|
578
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
579
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (pathname === '/api/config/harnesses' && req.method === 'POST') {
|
|
586
|
+
try {
|
|
587
|
+
const agy = HarnessConfig.installAntigravity(workspacePath);
|
|
588
|
+
const cmd = HarnessConfig.installCommandCode(workspacePath);
|
|
589
|
+
const muse = HarnessConfig.installMuseCode(workspacePath);
|
|
590
|
+
const codex = HarnessConfig.installCodex(workspacePath);
|
|
591
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
592
|
+
res.end(JSON.stringify({
|
|
593
|
+
ok: true,
|
|
594
|
+
installed: [
|
|
595
|
+
{ harness: 'Google Antigravity', path: agy },
|
|
596
|
+
{ harness: 'Claude Code & Command Code', path: cmd },
|
|
597
|
+
{ harness: 'Meta Muse', path: muse },
|
|
598
|
+
{ harness: 'OpenAI Codex CLI', path: codex }
|
|
599
|
+
]
|
|
600
|
+
}));
|
|
601
|
+
} catch (err) {
|
|
602
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
603
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (pathname === '/api/tasks' && req.method === 'POST') {
|
|
609
|
+
let body = '';
|
|
610
|
+
req.on('data', chunk => { body += chunk; });
|
|
611
|
+
req.on('end', () => {
|
|
612
|
+
try {
|
|
613
|
+
const data = JSON.parse(body || '{}');
|
|
614
|
+
if (data.action === 'status' && data.taskId && data.status) {
|
|
615
|
+
const updated = projectService.updateTaskStatus(workspacePath, data.taskId, data.status);
|
|
616
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
617
|
+
res.end(JSON.stringify({ ok: true, task: updated }));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const taskId = String(data.taskId || '').trim();
|
|
621
|
+
const title = String(data.title || taskId).trim();
|
|
622
|
+
if (!taskId) throw new Error('taskId is required');
|
|
623
|
+
const created = projectService.createTask(workspacePath, taskId, title);
|
|
624
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
625
|
+
res.end(JSON.stringify({ ok: true, task: created }));
|
|
626
|
+
} catch (err) {
|
|
627
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
628
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (pathname === '/api/todos' && (req.method === 'GET' || req.method === 'POST')) {
|
|
635
|
+
const vfDir = join(workspacePath, '.vectorforce');
|
|
636
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
637
|
+
const todosPath = join(vfDir, 'todos.json');
|
|
638
|
+
|
|
639
|
+
const loadTodos = () => {
|
|
640
|
+
if (existsSync(todosPath)) {
|
|
641
|
+
try { return JSON.parse(readFileSync(todosPath, 'utf8')); } catch {}
|
|
642
|
+
}
|
|
643
|
+
// Auto-seed from latest checkpoint next_steps & goals
|
|
644
|
+
const project = projectService.initProject(workspacePath);
|
|
645
|
+
const cps = store.getCheckpoints(project.project_id, { limit: 10 });
|
|
646
|
+
const latest = cps[cps.length - 1];
|
|
647
|
+
const seeded = [];
|
|
648
|
+
if (latest?.claims?.next_steps) {
|
|
649
|
+
for (const step of latest.claims.next_steps) {
|
|
650
|
+
seeded.push({
|
|
651
|
+
id: `todo-${Math.random().toString(36).slice(2, 9)}`,
|
|
652
|
+
text: step,
|
|
653
|
+
taskId: latest.task_id,
|
|
654
|
+
done: false,
|
|
655
|
+
createdAt: latest.timestamp
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (seeded.length > 0) {
|
|
660
|
+
writeFileSync(todosPath, JSON.stringify(seeded, null, 2) + '\n', 'utf8');
|
|
661
|
+
}
|
|
662
|
+
return seeded;
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
if (req.method === 'GET') {
|
|
666
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
667
|
+
res.end(JSON.stringify({ todos: loadTodos() }));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let body = '';
|
|
672
|
+
req.on('data', chunk => { body += chunk; });
|
|
673
|
+
req.on('end', () => {
|
|
674
|
+
try {
|
|
675
|
+
const data = JSON.parse(body || '{}');
|
|
676
|
+
let todos = loadTodos();
|
|
677
|
+
if (data.action === 'add' && data.text) {
|
|
678
|
+
todos.unshift({
|
|
679
|
+
id: `todo-${Date.now().toString(36)}`,
|
|
680
|
+
text: String(data.text).trim(),
|
|
681
|
+
taskId: data.taskId || 'main',
|
|
682
|
+
done: false,
|
|
683
|
+
createdAt: new Date().toISOString()
|
|
684
|
+
});
|
|
685
|
+
} else if (data.action === 'toggle' && data.id) {
|
|
686
|
+
todos = todos.map(t => t.id === data.id ? { ...t, done: !t.done } : t);
|
|
687
|
+
} else if (data.action === 'delete' && data.id) {
|
|
688
|
+
todos = todos.filter(t => t.id !== data.id);
|
|
689
|
+
} else if (Array.isArray(data.todos)) {
|
|
690
|
+
todos = data.todos;
|
|
691
|
+
}
|
|
692
|
+
writeFileSync(todosPath, JSON.stringify(todos, null, 2) + '\n', 'utf8');
|
|
693
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
694
|
+
res.end(JSON.stringify({ ok: true, todos }));
|
|
695
|
+
} catch (err) {
|
|
696
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
697
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Serve static assets from studio/dist for standalone Studio UI
|
|
704
|
+
const bundledDist = fileURLToPath(new URL('../../studio/dist', import.meta.url));
|
|
705
|
+
const workspaceDist = join(workspacePath, 'studio', 'dist');
|
|
706
|
+
const distDir = existsSync(workspaceDist) ? workspaceDist : bundledDist;
|
|
707
|
+
if (existsSync(distDir)) {
|
|
708
|
+
let filePath = join(distDir, pathname === '/' ? 'index.html' : pathname.slice(1));
|
|
709
|
+
if (!existsSync(filePath)) {
|
|
710
|
+
filePath = join(distDir, 'index.html');
|
|
711
|
+
}
|
|
712
|
+
if (existsSync(filePath)) {
|
|
713
|
+
const ext = extname(filePath).toLowerCase();
|
|
714
|
+
const mimeTypes = {
|
|
715
|
+
'.html': 'text/html; charset=utf-8',
|
|
716
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
717
|
+
'.css': 'text/css; charset=utf-8',
|
|
718
|
+
'.json': 'application/json',
|
|
719
|
+
'.png': 'image/png',
|
|
720
|
+
'.svg': 'image/svg+xml'
|
|
721
|
+
};
|
|
722
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
723
|
+
const content = readFileSync(filePath);
|
|
724
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
725
|
+
res.end(content);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
730
|
+
res.end(`<!DOCTYPE html><html><body style="font-family:system-ui,-apple-system,sans-serif;background:#090d16;color:#e2e8f0;display:flex;flex-direction:column;align-items:center;justify-content:center;height:90vh;text-align:center;">
|
|
731
|
+
<h1 style="color:#60a5fa;margin-bottom:8px;">VectorForce Studio API is Active</h1>
|
|
732
|
+
<p style="color:#94a3b8;max-width:500px;line-height:1.6;">The backend is connected to SQLite and serving APIs at <a href="/api/status" style="color:#38bdf8;">/api/status</a>. To build the UI bundle, run:<br/><code style="background:#1e293b;padding:4px 8px;border-radius:4px;color:#a5b4fc;display:inline-block;margin-top:12px;">cd studio && npm install && npm run build</code></p>
|
|
733
|
+
</body></html>`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
738
|
+
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.error('SERVER CATCH ERROR:', err);
|
|
741
|
+
if (!res.headersSent) {
|
|
742
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
743
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
server,
|
|
750
|
+
start() {
|
|
751
|
+
return new Promise((resolve) => {
|
|
752
|
+
server.listen(port, () => {
|
|
753
|
+
resolve(`http://localhost:${port}`);
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
},
|
|
757
|
+
stop() {
|
|
758
|
+
return new Promise((resolve) => {
|
|
759
|
+
store.close();
|
|
760
|
+
server.close(resolve);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|