vectorforce 0.1.0 → 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 +3 -9
- package/.codex/config.toml +0 -3
- package/AGENTS.md +93 -21
- package/README.md +65 -40
- package/bin/vf.js +418 -15
- package/musecode.json +2 -6
- package/package.json +3 -2
- package/src/mcp/harness-config.js +93 -45
- package/src/providers/llm-provider.js +21 -6
- package/src/studio/server.js +312 -4
- package/studio/dist/assets/index-C0_eVpV1.js +9 -0
- package/studio/dist/assets/index-CxTK-2ty.css +2 -0
- package/studio/dist/index.html +2 -2
- package/studio/dist/assets/index-DTKNBk3V.css +0 -2
- package/studio/dist/assets/index-X-wHcJ6r.js +0 -9
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import {
|
|
2
|
+
import { homedir, tmpdir } from 'node:os';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, realpathSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
function isTempWorkspace(workspacePath) {
|
|
7
|
+
if (process.env.VF_NO_GLOBAL_CONFIG === '1' || process.env.NODE_TEST_CONTEXT) return true;
|
|
8
|
+
try {
|
|
9
|
+
const realWs = realpathSync(workspacePath);
|
|
10
|
+
const realTmp = realpathSync(tmpdir());
|
|
11
|
+
return realWs.startsWith(realTmp);
|
|
12
|
+
} catch {
|
|
13
|
+
return workspacePath.startsWith(tmpdir()) || workspacePath.includes('vf-test-') || workspacePath.includes('vf-practical-');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
export class HarnessConfig {
|
|
6
18
|
/**
|
|
7
|
-
* Get the absolute path to bin/vf.js
|
|
19
|
+
* Get the absolute path to bin/vf.js (cross-platform: macOS, Linux, Windows)
|
|
8
20
|
*/
|
|
9
21
|
static getBinPath() {
|
|
10
|
-
return
|
|
22
|
+
return fileURLToPath(new URL('../../bin/vf.js', import.meta.url));
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
/**
|
|
@@ -66,6 +78,36 @@ export class HarnessConfig {
|
|
|
66
78
|
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
67
79
|
}
|
|
68
80
|
writeFileSync(filePath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
81
|
+
|
|
82
|
+
if (!isTempWorkspace(workspacePath)) {
|
|
83
|
+
try {
|
|
84
|
+
const globalAgyDir = join(homedir(), '.gemini', 'antigravity');
|
|
85
|
+
if (existsSync(globalAgyDir)) {
|
|
86
|
+
const globalMcpPath = join(globalAgyDir, 'mcp_config.json');
|
|
87
|
+
let globalConfig = {};
|
|
88
|
+
if (existsSync(globalMcpPath)) {
|
|
89
|
+
try {
|
|
90
|
+
globalConfig = JSON.parse(readFileSync(globalMcpPath, 'utf8'));
|
|
91
|
+
} catch {
|
|
92
|
+
globalConfig = {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
globalConfig.mcpServers = {
|
|
96
|
+
...(globalConfig.mcpServers || {}),
|
|
97
|
+
vectorforce: {
|
|
98
|
+
command: 'node',
|
|
99
|
+
args: [HarnessConfig.getBinPath(), 'mcp', '--workspace', workspacePath],
|
|
100
|
+
env: {
|
|
101
|
+
VF_WORKSPACE: workspacePath,
|
|
102
|
+
VF_DB_PATH: join(workspacePath, '.vectorforce', 'vectorforce.db')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
writeFileSync(globalMcpPath, JSON.stringify(globalConfig, null, 2) + '\n', 'utf8');
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
69
111
|
return filePath;
|
|
70
112
|
}
|
|
71
113
|
|
|
@@ -265,28 +307,30 @@ export class HarnessConfig {
|
|
|
265
307
|
writeFileSync(filePath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
266
308
|
writeFileSync(museJsonPath, JSON.stringify(mergedContent, null, 2) + '\n', 'utf8');
|
|
267
309
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
310
|
+
if (!isTempWorkspace(workspacePath)) {
|
|
311
|
+
try {
|
|
312
|
+
const globalMuseDir = join(homedir(), '.config', 'muse');
|
|
313
|
+
const globalSettingsPath = join(globalMuseDir, 'settings.json');
|
|
314
|
+
if (existsSync(globalSettingsPath)) {
|
|
315
|
+
const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
|
|
316
|
+
settings.mcp_servers = {
|
|
317
|
+
...(settings.mcp_servers || {}),
|
|
318
|
+
vectorforce: {
|
|
319
|
+
transport: 'stdio',
|
|
320
|
+
command: 'node',
|
|
321
|
+
args: [HarnessConfig.getBinPath(), 'mcp'],
|
|
322
|
+
env: {
|
|
323
|
+
VF_HARNESS: 'muse-code',
|
|
324
|
+
VF_WORKSPACE: workspacePath
|
|
325
|
+
},
|
|
326
|
+
mode: 'optional'
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
delete settings.mcpServers;
|
|
330
|
+
writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
331
|
+
}
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
290
334
|
|
|
291
335
|
return filePath;
|
|
292
336
|
}
|
|
@@ -322,16 +366,18 @@ export class HarnessConfig {
|
|
|
322
366
|
} catch {}
|
|
323
367
|
}
|
|
324
368
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
369
|
+
if (!isTempWorkspace(workspacePath)) {
|
|
370
|
+
try {
|
|
371
|
+
const globalSettingsPath = join(homedir(), '.config', 'muse', 'settings.json');
|
|
372
|
+
if (existsSync(globalSettingsPath)) {
|
|
373
|
+
const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
|
|
374
|
+
if (settings?.mcp_servers?.vectorforce) {
|
|
375
|
+
delete settings.mcp_servers.vectorforce;
|
|
376
|
+
writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
377
|
+
}
|
|
332
378
|
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
379
|
+
} catch {}
|
|
380
|
+
}
|
|
335
381
|
|
|
336
382
|
return removed;
|
|
337
383
|
}
|
|
@@ -355,7 +401,7 @@ export class HarnessConfig {
|
|
|
355
401
|
* @returns {string} merged TOML
|
|
356
402
|
*/
|
|
357
403
|
static _mergeCodexToml(existingToml) {
|
|
358
|
-
const binPath = HarnessConfig.getBinPath();
|
|
404
|
+
const binPath = HarnessConfig.getBinPath().replace(/\\/g, '\\\\');
|
|
359
405
|
const serverBlock = `[mcp_servers.vectorforce]\ncommand = "node"\nargs = ["${binPath}", "mcp"]\n[mcp_servers.vectorforce.env]\nVF_HARNESS = "codex"\n`;
|
|
360
406
|
|
|
361
407
|
if (!existingToml || !existingToml.trim()) {
|
|
@@ -434,15 +480,17 @@ export class HarnessConfig {
|
|
|
434
480
|
writeFileSync(filePath, mergedContent, 'utf8');
|
|
435
481
|
writeFileSync(codexJsonPath, JSON.stringify(mergedJson, null, 2) + '\n', 'utf8');
|
|
436
482
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
483
|
+
if (!isTempWorkspace(workspacePath)) {
|
|
484
|
+
try {
|
|
485
|
+
const globalCodex = join(homedir(), '.codex');
|
|
486
|
+
if (existsSync(globalCodex)) {
|
|
487
|
+
const globalTomlPath = join(globalCodex, 'config.toml');
|
|
488
|
+
const existingGlobal = existsSync(globalTomlPath) ? readFileSync(globalTomlPath, 'utf8') : '';
|
|
489
|
+
const updatedGlobal = HarnessConfig._mergeCodexToml(existingGlobal);
|
|
490
|
+
writeFileSync(globalTomlPath, updatedGlobal, 'utf8');
|
|
491
|
+
}
|
|
492
|
+
} catch {}
|
|
493
|
+
}
|
|
446
494
|
|
|
447
495
|
return filePath;
|
|
448
496
|
}
|
|
@@ -9,6 +9,8 @@ export const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
|
9
9
|
export const DEFAULT_OPENROUTER_MODEL = 'deepseek/deepseek-v4.1-flash-1017';
|
|
10
10
|
export const DEFAULT_MUSE_BASE_URL = 'https://api.meta.ai/v1';
|
|
11
11
|
export const DEFAULT_MUSE_MODEL = 'muse-spark-1.3';
|
|
12
|
+
export const DEFAULT_FIREWORKS_BASE_URL = 'https://api.fireworks.ai/inference/v1';
|
|
13
|
+
export const DEFAULT_FIREWORKS_MODEL = 'accounts/fireworks/models/muse-spark-open';
|
|
12
14
|
export const DEFAULT_TIMEOUT_MS = 60000;
|
|
13
15
|
|
|
14
16
|
function trimTrailingSlash(url) {
|
|
@@ -102,10 +104,11 @@ export function resolveLlmConfig(env = process.env, overrides = {}, workspacePat
|
|
|
102
104
|
const provider = (
|
|
103
105
|
overrides.provider ||
|
|
104
106
|
mergedEnv.VF_LLM_PROVIDER ||
|
|
107
|
+
(mergedEnv.FIREWORKS_API_KEY || mergedEnv.VF_FIREWORKS_API_KEY ? 'fireworks' :
|
|
105
108
|
(mergedEnv.OPENROUTER_API_KEY || mergedEnv.VF_OPENROUTER_API_KEY ? 'openrouter' :
|
|
106
109
|
(mergedEnv.VF_CODEX_API_KEY ? 'codex' :
|
|
107
110
|
(mergedEnv.MODEL_API_KEY || mergedEnv.MUSE_API_KEY || mergedEnv.VF_MUSE_API_KEY ? 'muse' :
|
|
108
|
-
(mergedEnv.VF_LLM_API_KEY ? 'openai' : 'ollama'))))
|
|
111
|
+
(mergedEnv.VF_LLM_API_KEY ? 'openai' : 'ollama')))))
|
|
109
112
|
).toLowerCase();
|
|
110
113
|
const timeoutMs = overrides.timeoutMs ?? (mergedEnv.VF_LLM_TIMEOUT_MS ? Number(mergedEnv.VF_LLM_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS);
|
|
111
114
|
|
|
@@ -120,6 +123,16 @@ export function resolveLlmConfig(env = process.env, overrides = {}, workspacePat
|
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
if (provider === 'fireworks') {
|
|
127
|
+
const baseUrl = trimTrailingSlash(overrides.baseUrl || mergedEnv.VF_FIREWORKS_BASE_URL || mergedEnv.FIREWORKS_BASE_URL || mergedEnv.VF_LLM_BASE_URL || DEFAULT_FIREWORKS_BASE_URL);
|
|
128
|
+
const apiKey = overrides.apiKey ?? mergedEnv.FIREWORKS_API_KEY ?? mergedEnv.VF_FIREWORKS_API_KEY ?? mergedEnv.VF_LLM_API_KEY ?? null;
|
|
129
|
+
const model = overrides.model || mergedEnv.VF_FIREWORKS_MODEL || mergedEnv.FIREWORKS_MODEL || mergedEnv.VF_LLM_MODEL || DEFAULT_FIREWORKS_MODEL;
|
|
130
|
+
if (!apiKey) {
|
|
131
|
+
throw new VectorForceError(ErrorCode.LLM_CONFIG_INVALID, 'Fireworks provider requires FIREWORKS_API_KEY or VF_FIREWORKS_API_KEY');
|
|
132
|
+
}
|
|
133
|
+
return { provider: 'fireworks', baseUrl, apiKey, model, timeoutMs };
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
if (provider === 'openrouter') {
|
|
124
137
|
const baseUrl = trimTrailingSlash(overrides.baseUrl || mergedEnv.VF_OPENROUTER_BASE_URL || mergedEnv.OPENROUTER_BASE_URL || mergedEnv.VF_LLM_BASE_URL || DEFAULT_OPENROUTER_BASE_URL);
|
|
125
138
|
const apiKey = overrides.apiKey ?? mergedEnv.OPENROUTER_API_KEY ?? mergedEnv.VF_OPENROUTER_API_KEY ?? mergedEnv.VF_LLM_API_KEY ?? null;
|
|
@@ -140,10 +153,12 @@ export function resolveLlmConfig(env = process.env, overrides = {}, workspacePat
|
|
|
140
153
|
return { provider: 'muse', baseUrl, apiKey, model, timeoutMs };
|
|
141
154
|
}
|
|
142
155
|
|
|
143
|
-
if (provider === 'openai' || provider === 'codex') {
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
156
|
+
if (provider === 'openai' || provider === 'codex' || provider === 'anthropic' || provider === 'byok') {
|
|
157
|
+
const defaultBase = provider === 'anthropic' ? 'https://api.anthropic.com/v1' : '';
|
|
158
|
+
const baseUrl = trimTrailingSlash(overrides.baseUrl || mergedEnv.VF_CODEX_BASE_URL || mergedEnv.OPENAI_BASE_URL || mergedEnv.ANTHROPIC_BASE_URL || mergedEnv.VF_LLM_BASE_URL || defaultBase);
|
|
159
|
+
const apiKey = overrides.apiKey ?? mergedEnv.VF_CODEX_API_KEY ?? mergedEnv.OPENAI_API_KEY ?? mergedEnv.ANTHROPIC_API_KEY ?? mergedEnv.VF_LLM_API_KEY ?? null;
|
|
160
|
+
const defaultModel = provider === 'anthropic' ? 'claude-sonnet-4-20250514' : DEFAULT_CLOUD_MODEL;
|
|
161
|
+
const model = overrides.model || mergedEnv.VF_CODEX_MODEL || mergedEnv.VF_LLM_MODEL || defaultModel;
|
|
147
162
|
if (!baseUrl) {
|
|
148
163
|
throw new VectorForceError(
|
|
149
164
|
ErrorCode.LLM_CONFIG_INVALID,
|
|
@@ -158,7 +173,7 @@ export function resolveLlmConfig(env = process.env, overrides = {}, workspacePat
|
|
|
158
173
|
|
|
159
174
|
throw new VectorForceError(
|
|
160
175
|
ErrorCode.LLM_CONFIG_INVALID,
|
|
161
|
-
`Unknown LLM provider '${provider}'. Use 'openrouter', 'muse', 'codex', 'openai' (cloud) or 'ollama' (local).`
|
|
176
|
+
`Unknown LLM provider '${provider}'. Use 'openrouter', 'muse', 'codex', 'openai', 'anthropic', 'byok' (cloud) or 'ollama' (local).`
|
|
162
177
|
);
|
|
163
178
|
}
|
|
164
179
|
|
package/src/studio/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join, extname } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
4
6
|
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { SqliteStore } from '../storage/sqlite-store.js';
|
|
6
8
|
import { ProjectService } from '../operations/project-service.js';
|
|
@@ -11,6 +13,7 @@ import { ReasoningService } from '../operations/reasoning-service.js';
|
|
|
11
13
|
import { CodeIndexProvider } from '../providers/code-index-provider.js';
|
|
12
14
|
import { LlmProvider, resolveLlmConfig } from '../providers/llm-provider.js';
|
|
13
15
|
import { GitProvider } from '../providers/git-provider.js';
|
|
16
|
+
import { HarnessConfig } from '../mcp/harness-config.js';
|
|
14
17
|
|
|
15
18
|
export function createStudioServer({
|
|
16
19
|
dbPath,
|
|
@@ -319,13 +322,75 @@ export function createStudioServer({
|
|
|
319
322
|
req.on('end', async () => {
|
|
320
323
|
try {
|
|
321
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
|
+
|
|
322
345
|
const result = handoffService.exportHandoff({
|
|
323
346
|
workspacePath,
|
|
324
|
-
taskId:
|
|
325
|
-
checkpointId:
|
|
347
|
+
taskId: resolvedTaskId || 'phase-5-pilot',
|
|
348
|
+
checkpointId: resolvedCheckpointId,
|
|
326
349
|
recipient: data.recipient || 'Collaborator',
|
|
327
350
|
destinationHarness: data.destinationHarness || 'any'
|
|
328
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
|
+
});
|
|
329
394
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
330
395
|
res.end(JSON.stringify(result));
|
|
331
396
|
} catch (err) {
|
|
@@ -380,7 +445,7 @@ export function createStudioServer({
|
|
|
380
445
|
requestedDocuments: data.requestedDocuments || [],
|
|
381
446
|
includeGit: data.includeGit !== false,
|
|
382
447
|
authorName: data.authorName || 'developer',
|
|
383
|
-
authorHarness: 'studio'
|
|
448
|
+
authorHarness: data.authorHarness || 'studio'
|
|
384
449
|
});
|
|
385
450
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
386
451
|
res.end(JSON.stringify(result));
|
|
@@ -392,6 +457,249 @@ export function createStudioServer({
|
|
|
392
457
|
return;
|
|
393
458
|
}
|
|
394
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
|
+
|
|
395
703
|
// Serve static assets from studio/dist for standalone Studio UI
|
|
396
704
|
const bundledDist = fileURLToPath(new URL('../../studio/dist', import.meta.url));
|
|
397
705
|
const workspaceDist = join(workspacePath, 'studio', 'dist');
|