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
package/bin/vf.js
ADDED
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { homedir, hostname } from 'node:os';
|
|
4
|
+
import { resolve, join, dirname, basename } from 'node:path';
|
|
5
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import {
|
|
7
|
+
SqliteStore,
|
|
8
|
+
ProjectService,
|
|
9
|
+
CheckpointService,
|
|
10
|
+
BriefingService,
|
|
11
|
+
HandoffService,
|
|
12
|
+
ReasoningService,
|
|
13
|
+
LlmProvider,
|
|
14
|
+
CodeIndexProvider,
|
|
15
|
+
resolveLlmConfig,
|
|
16
|
+
McpServer,
|
|
17
|
+
HarnessConfig,
|
|
18
|
+
createStudioServer,
|
|
19
|
+
VectorForceError
|
|
20
|
+
} from '../src/index.js';
|
|
21
|
+
|
|
22
|
+
function getScriptWorkspace() {
|
|
23
|
+
try {
|
|
24
|
+
const scriptDir = dirname(resolve(process.argv[1] || ''));
|
|
25
|
+
let curr = scriptDir;
|
|
26
|
+
while (curr && curr !== '/' && curr !== homedir()) {
|
|
27
|
+
if (existsSync(join(curr, '.vectorforce', 'vectorforce.db')) || existsSync(join(curr, '.git'))) {
|
|
28
|
+
return curr;
|
|
29
|
+
}
|
|
30
|
+
const parent = dirname(curr);
|
|
31
|
+
if (parent === curr) break;
|
|
32
|
+
curr = parent;
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getWorkspacePath(startDir = process.cwd()) {
|
|
39
|
+
if (process.env.VF_WORKSPACE) return resolve(process.env.VF_WORKSPACE);
|
|
40
|
+
|
|
41
|
+
// If startDir is root or homedir, check if the script location belongs to a VectorForce workspace first
|
|
42
|
+
if (startDir === '/' || startDir === homedir()) {
|
|
43
|
+
const scriptWs = getScriptWorkspace();
|
|
44
|
+
if (scriptWs) return scriptWs;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let curr = resolve(startDir);
|
|
48
|
+
while (true) {
|
|
49
|
+
// homedir can contain ~/.vectorforce as a global fallback directory; do not treat homedir as a project workspace unless it is a git repo
|
|
50
|
+
if (curr !== homedir() && curr !== '/') {
|
|
51
|
+
if (existsSync(join(curr, '.vectorforce')) || existsSync(join(curr, '.git'))) {
|
|
52
|
+
return curr;
|
|
53
|
+
}
|
|
54
|
+
} else if (existsSync(join(curr, '.git'))) {
|
|
55
|
+
return curr;
|
|
56
|
+
}
|
|
57
|
+
const parent = dirname(curr);
|
|
58
|
+
if (parent === curr) break;
|
|
59
|
+
curr = parent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return resolve(startDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getDbPath(startDir = process.cwd()) {
|
|
66
|
+
if (process.env.VF_DB_PATH) return process.env.VF_DB_PATH;
|
|
67
|
+
|
|
68
|
+
// If startDir is root or homedir, check if the script location belongs to a VectorForce workspace first
|
|
69
|
+
if (startDir === '/' || startDir === homedir()) {
|
|
70
|
+
const scriptWs = getScriptWorkspace();
|
|
71
|
+
if (scriptWs) {
|
|
72
|
+
const dbCandidate = join(scriptWs, '.vectorforce', 'vectorforce.db');
|
|
73
|
+
if (existsSync(dbCandidate)) return dbCandidate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 1. Search up from current directory to root: look for .vectorforce/vectorforce.db or .git
|
|
78
|
+
let curr = resolve(startDir);
|
|
79
|
+
while (true) {
|
|
80
|
+
if (curr !== homedir() && curr !== '/') {
|
|
81
|
+
const localDb = join(curr, '.vectorforce', 'vectorforce.db');
|
|
82
|
+
if (existsSync(localDb)) {
|
|
83
|
+
return localDb;
|
|
84
|
+
}
|
|
85
|
+
const localDir = join(curr, '.vectorforce');
|
|
86
|
+
if (existsSync(localDir)) {
|
|
87
|
+
return localDb;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (existsSync(join(curr, '.git'))) {
|
|
91
|
+
const gitLocalDir = join(curr, '.vectorforce');
|
|
92
|
+
mkdirSync(gitLocalDir, { recursive: true, mode: 0o700 });
|
|
93
|
+
return join(gitLocalDir, 'vectorforce.db');
|
|
94
|
+
}
|
|
95
|
+
const parent = dirname(curr);
|
|
96
|
+
if (parent === curr) break;
|
|
97
|
+
curr = parent;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. Global fallback in ~/.vectorforce/vectorforce.db
|
|
101
|
+
try {
|
|
102
|
+
const homeDir = join(homedir(), '.vectorforce');
|
|
103
|
+
mkdirSync(homeDir, { recursive: true, mode: 0o700 });
|
|
104
|
+
return join(homeDir, 'vectorforce.db');
|
|
105
|
+
} catch {
|
|
106
|
+
const fallbackDir = join(startDir, '.vectorforce');
|
|
107
|
+
mkdirSync(fallbackDir, { recursive: true, mode: 0o700 });
|
|
108
|
+
return join(fallbackDir, 'vectorforce.db');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createLlmProvider(workspacePath) {
|
|
113
|
+
try {
|
|
114
|
+
return { provider: new LlmProvider(resolveLlmConfig(process.env, {}, workspacePath)), error: null };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { provider: null, error: err.message };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getServices(explicitDbPath, workspacePath) {
|
|
121
|
+
const cwd = workspacePath || getWorkspacePath(process.cwd());
|
|
122
|
+
const dbPath = explicitDbPath || getDbPath(cwd);
|
|
123
|
+
const store = new SqliteStore(dbPath);
|
|
124
|
+
const projectService = new ProjectService(store);
|
|
125
|
+
const checkpointService = new CheckpointService(store, projectService);
|
|
126
|
+
const briefingService = new BriefingService(store, projectService);
|
|
127
|
+
const handoffService = new HandoffService(store, projectService, checkpointService);
|
|
128
|
+
const llm = createLlmProvider(cwd);
|
|
129
|
+
const reasoningService = new ReasoningService(store, projectService, briefingService, {
|
|
130
|
+
llmProvider: llm.provider,
|
|
131
|
+
codeIndexProvider: CodeIndexProvider
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
store,
|
|
135
|
+
projectService,
|
|
136
|
+
checkpointService,
|
|
137
|
+
briefingService,
|
|
138
|
+
handoffService,
|
|
139
|
+
reasoningService,
|
|
140
|
+
llmProviderError: llm.error
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function printUsage() {
|
|
145
|
+
console.log(`
|
|
146
|
+
VectorForce (vf) - Git for AI Engineering Context
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
vf init [project_name] Initialize current workspace
|
|
150
|
+
vf task <id> [title] Create or register a task
|
|
151
|
+
vf tasks [active|completed|all] List tasks in workspace
|
|
152
|
+
vf policy list List allowed document paths
|
|
153
|
+
vf policy add <relative_path> Allow document for checkpointing
|
|
154
|
+
vf checkpoint [options] Capture an immutable checkpoint
|
|
155
|
+
vf resume [options] Generate authorized resume briefing
|
|
156
|
+
vf reason [options] AI reasoning over memory + codebase (alias: continue)
|
|
157
|
+
vf drift [options] Check for workspace and document drift
|
|
158
|
+
vf export [options] Export selective handoff package
|
|
159
|
+
vf import <package_path> [options] Import handoff package into workspace
|
|
160
|
+
vf recap [options] Show weekly timeline & engineering recap
|
|
161
|
+
vf timeline [options] Alias for vf recap
|
|
162
|
+
vf studio [options] Launch VectorForce Studio web control center
|
|
163
|
+
vf ui [options] Alias for vf studio
|
|
164
|
+
vf mcp Start stdio MCP server for Antigravity / Command Code
|
|
165
|
+
vf config Display MCP configuration snippet for harnesses
|
|
166
|
+
vf help Show this help
|
|
167
|
+
|
|
168
|
+
Checkpoint options:
|
|
169
|
+
--task <id> Task identifier
|
|
170
|
+
--summary <text> Concise summary
|
|
171
|
+
--goal <text> Add a goal (can repeat)
|
|
172
|
+
--constraint <text> Add a constraint (can repeat)
|
|
173
|
+
--decision <text> Add decision format "ID:Title:Rationale" (can repeat)
|
|
174
|
+
--next <text> Add next step (can repeat)
|
|
175
|
+
--doc <path> Request allowlisted document (can repeat)
|
|
176
|
+
--no-git Skip Git status collection
|
|
177
|
+
|
|
178
|
+
Resume options:
|
|
179
|
+
--task <id> Task identifier
|
|
180
|
+
--checkpoint <id> Specific checkpoint ID (defaults to latest head)
|
|
181
|
+
--dest <harness> Destination harness: antigravity, command-code, muse, codex, pi, cli
|
|
182
|
+
|
|
183
|
+
Reason options:
|
|
184
|
+
--task <id> Task identifier
|
|
185
|
+
--checkpoint <id> Specific checkpoint ID (defaults to latest head)
|
|
186
|
+
--focus <text> Optional question or focus for the reasoning pass
|
|
187
|
+
--model <name> Override the model id (e.g. a Meta Llama or Muse model)
|
|
188
|
+
--no-code Reason over memory only (do not send code context)
|
|
189
|
+
--raw Print the deterministic briefing without calling the model
|
|
190
|
+
--dest <harness> Destination harness: antigravity, command-code, muse, codex, pi, cli
|
|
191
|
+
|
|
192
|
+
Recap / Timeline options:
|
|
193
|
+
--days <number> Number of days to include (default: 7)
|
|
194
|
+
--task <id> Filter by specific task
|
|
195
|
+
--dest <harness> Destination harness: antigravity, command-code, muse, codex, pi, cli
|
|
196
|
+
|
|
197
|
+
Handoff options:
|
|
198
|
+
--task <id> Task identifier
|
|
199
|
+
--recipient <name> Target recipient/audience
|
|
200
|
+
--checkpoint <id> Specific checkpoint ID (defaults to latest head)
|
|
201
|
+
--dest <harness> Destination harness: antigravity, command-code, muse, codex, pi, cli, any
|
|
202
|
+
--preview Preview export bundle without writing file
|
|
203
|
+
--out <path> Target output path for handoff package
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function parseFlags(args) {
|
|
208
|
+
const flags = {
|
|
209
|
+
goals: [],
|
|
210
|
+
constraints: [],
|
|
211
|
+
decisions: [],
|
|
212
|
+
nextSteps: [],
|
|
213
|
+
docs: [],
|
|
214
|
+
includeGit: true
|
|
215
|
+
};
|
|
216
|
+
for (let i = 0; i < args.length; i++) {
|
|
217
|
+
const arg = args[i];
|
|
218
|
+
if (arg === '--task' && args[i + 1]) {
|
|
219
|
+
flags.task = args[++i];
|
|
220
|
+
} else if (arg === '--summary' && args[i + 1]) {
|
|
221
|
+
flags.summary = args[++i];
|
|
222
|
+
} else if (arg === '--checkpoint' && args[i + 1]) {
|
|
223
|
+
flags.checkpoint = args[++i];
|
|
224
|
+
} else if (arg === '--dest' && args[i + 1]) {
|
|
225
|
+
flags.dest = args[++i];
|
|
226
|
+
} else if (arg === '--goal' && args[i + 1]) {
|
|
227
|
+
flags.goals.push(args[++i]);
|
|
228
|
+
} else if (arg === '--constraint' && args[i + 1]) {
|
|
229
|
+
flags.constraints.push(args[++i]);
|
|
230
|
+
} else if (arg === '--decision' && args[i + 1]) {
|
|
231
|
+
const parts = args[++i].split(':');
|
|
232
|
+
flags.decisions.push({
|
|
233
|
+
id: parts[0] || 'DEC',
|
|
234
|
+
title: parts[1] || parts[0],
|
|
235
|
+
rationale: parts.slice(2).join(':') || 'Approved architecture decision'
|
|
236
|
+
});
|
|
237
|
+
} else if (arg === '--harness' && args[i + 1]) {
|
|
238
|
+
flags.harness = args[++i];
|
|
239
|
+
} else if (arg === '--author' && args[i + 1]) {
|
|
240
|
+
flags.author = args[++i];
|
|
241
|
+
} else if (arg === '--next' && args[i + 1]) {
|
|
242
|
+
flags.nextSteps.push(args[++i]);
|
|
243
|
+
} else if (arg === '--doc' && args[i + 1]) {
|
|
244
|
+
flags.docs.push(args[++i]);
|
|
245
|
+
} else if (arg === '--model' && args[i + 1]) {
|
|
246
|
+
flags.model = args[++i];
|
|
247
|
+
} else if (arg === '--focus' && args[i + 1]) {
|
|
248
|
+
flags.focus = args[++i];
|
|
249
|
+
} else if (arg === '--no-code') {
|
|
250
|
+
flags.includeCode = false;
|
|
251
|
+
} else if (arg === '--raw') {
|
|
252
|
+
flags.raw = true;
|
|
253
|
+
} else if (arg === '--no-git') {
|
|
254
|
+
flags.includeGit = false;
|
|
255
|
+
} else if (arg === '--recipient' && args[i + 1]) {
|
|
256
|
+
flags.recipient = args[++i];
|
|
257
|
+
} else if (arg === '--preview') {
|
|
258
|
+
flags.preview = true;
|
|
259
|
+
} else if (arg === '--out' && args[i + 1]) {
|
|
260
|
+
flags.outPath = args[++i];
|
|
261
|
+
} else if (arg === '--port' && args[i + 1]) {
|
|
262
|
+
flags.port = parseInt(args[++i], 10);
|
|
263
|
+
} else if (arg === '--workspace' && args[i + 1]) {
|
|
264
|
+
flags.workspace = resolve(args[++i]);
|
|
265
|
+
} else if (arg === '--db' && args[i + 1]) {
|
|
266
|
+
flags.db = resolve(args[++i]);
|
|
267
|
+
} else if ((arg === '--url' || arg === '--remote') && args[i + 1]) {
|
|
268
|
+
flags.url = args[++i];
|
|
269
|
+
} else if (arg === '--key' && args[i + 1]) {
|
|
270
|
+
flags.key = args[++i];
|
|
271
|
+
} else if (arg === '--project' && args[i + 1]) {
|
|
272
|
+
flags.project = args[++i];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return flags;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getRemoteConfig(cwd) {
|
|
279
|
+
const defaultUrl = process.env.VF_CLOUD_URL || 'https://omprakash-pce2011--vectorforce-cloud-vectorforce-cloud-web.modal.run';
|
|
280
|
+
const defaultKey = process.env.VF_SUBSCRIPTION_KEY || 'vf_live_sub_98a7bc65f012e4d9';
|
|
281
|
+
const cfgPath = join(cwd, '.vectorforce', 'config.json');
|
|
282
|
+
try {
|
|
283
|
+
if (existsSync(cfgPath)) {
|
|
284
|
+
const parsed = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
285
|
+
return {
|
|
286
|
+
remoteUrl: (parsed.remoteUrl || defaultUrl).replace(/\/+$/, ''),
|
|
287
|
+
subscriptionKey: parsed.subscriptionKey || defaultKey,
|
|
288
|
+
remoteProjectId: parsed.remoteProjectId || 'proj-bistro-pos'
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
} catch {}
|
|
292
|
+
return {
|
|
293
|
+
remoteUrl: defaultUrl.replace(/\/+$/, ''),
|
|
294
|
+
subscriptionKey: defaultKey,
|
|
295
|
+
remoteProjectId: 'proj-bistro-pos'
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function saveRemoteConfig(cwd, updates) {
|
|
300
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
301
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
302
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
303
|
+
let existing = {};
|
|
304
|
+
try {
|
|
305
|
+
if (existsSync(cfgPath)) {
|
|
306
|
+
existing = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
307
|
+
}
|
|
308
|
+
} catch {}
|
|
309
|
+
const merged = { ...existing, ...updates };
|
|
310
|
+
writeFileSync(cfgPath, JSON.stringify(merged, null, 2), 'utf8');
|
|
311
|
+
return merged;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function main() {
|
|
315
|
+
const args = process.argv.slice(2);
|
|
316
|
+
const command = args[0] || 'help';
|
|
317
|
+
|
|
318
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
319
|
+
printUsage();
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const flags = parseFlags(args.slice(1));
|
|
324
|
+
const cwd = flags.workspace || getWorkspacePath(process.cwd());
|
|
325
|
+
const dbPath = flags.db || getDbPath(cwd);
|
|
326
|
+
const { store, projectService, checkpointService, briefingService, handoffService, reasoningService, llmProviderError } = getServices(dbPath, cwd);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
switch (command) {
|
|
330
|
+
case 'init': {
|
|
331
|
+
const name = args[1] && !args[1].startsWith('--') ? args[1] : null;
|
|
332
|
+
const project = projectService.initProject(cwd, name);
|
|
333
|
+
HarnessConfig.installAntigravity(cwd);
|
|
334
|
+
HarnessConfig.installCommandCode(cwd);
|
|
335
|
+
HarnessConfig.installMuseCode(cwd);
|
|
336
|
+
HarnessConfig.installCodex(cwd);
|
|
337
|
+
const agentsMdPath = join(cwd, 'AGENTS.md');
|
|
338
|
+
if (!existsSync(agentsMdPath)) {
|
|
339
|
+
writeFileSync(
|
|
340
|
+
agentsMdPath,
|
|
341
|
+
`# Agent Continuity Protocol ā ${project.name}\n\nThis repository uses **VectorForce** ("Git for AI Context") across Google Antigravity, Meta Muse, Claude Code, Command Code, and OpenAI Codex.\n\n## Workflow\n1. **Start/Resume Task**: Run \`vf tasks active\`, \`vf drift --task <id>\`, and \`vf resume --task <id> --dest <harness>\` (or MCP tools \`list_tasks\`, \`check_workspace_drift\`, \`get_resume_briefing\`, \`reason_about_task\`).\n2. **Save Checkpoint Before Switching IDE**: Run \`vf checkpoint --task <id> --summary "<summary>" --goal "<goal>" --constraint "<constraint>" --decision "<ID>:<Title>:<Rationale>" --next "<next>" --doc "AGENTS.md"\` (or MCP tool \`save_checkpoint\`).\n`,
|
|
342
|
+
'utf8'
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
console.log(`Initialized VectorForce project: ${project.name} (${project.project_id})`);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case 'task': {
|
|
350
|
+
const sub = args[1];
|
|
351
|
+
if (sub === 'complete' || sub === 'done') {
|
|
352
|
+
const taskId = args[2];
|
|
353
|
+
if (!taskId) {
|
|
354
|
+
console.error('Error: task id required (e.g. vf task complete phase-5-pilot)');
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
const task = projectService.updateTaskStatus(cwd, taskId, 'completed');
|
|
358
|
+
console.log(`Task marked as completed: ${task.title} (${task.task_id}) [${task.status}]`);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
const isCreateSub = sub === 'create' || sub === 'add';
|
|
362
|
+
const rawArgs = isCreateSub ? args.slice(2) : args.slice(1);
|
|
363
|
+
const taskId = rawArgs[0];
|
|
364
|
+
if (!taskId) {
|
|
365
|
+
console.error('Error: task id required (e.g. vf task feat-sqlite "Implement SQLite store")');
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
const title = rawArgs.slice(1).join(' ') || taskId;
|
|
369
|
+
const task = projectService.createTask(cwd, taskId, title);
|
|
370
|
+
console.log(`Task registered: ${task.title} (${task.task_id}) [${task.status}]`);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case 'tasks': {
|
|
375
|
+
const filter = args[1] || 'active';
|
|
376
|
+
const project = projectService.getProjectForWorkspace(cwd);
|
|
377
|
+
const tasks = store.listTasks(project.project_id, filter);
|
|
378
|
+
console.log(`Tasks for ${project.name} [${filter}]:`);
|
|
379
|
+
for (const t of tasks) {
|
|
380
|
+
console.log(`- [${t.status}] ${t.task_id}: ${t.title}`);
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
case 'policy': {
|
|
386
|
+
const sub = args[1];
|
|
387
|
+
const project = projectService.getProjectForWorkspace(cwd);
|
|
388
|
+
if (sub === 'list') {
|
|
389
|
+
const list = store.getDocumentPolicy(project.project_id);
|
|
390
|
+
console.log(`Document allowlist for ${project.name}:`);
|
|
391
|
+
if (list.length === 0) console.log(' (no documents allowlisted)');
|
|
392
|
+
for (const p of list) console.log(` - ${p}`);
|
|
393
|
+
} else if (sub === 'add') {
|
|
394
|
+
const docPath = args[2];
|
|
395
|
+
if (!docPath) {
|
|
396
|
+
console.error('Error: relative document path required');
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
const current = store.getDocumentPolicy(project.project_id);
|
|
400
|
+
const updated = store.setDocumentPolicy(project.project_id, [...current, docPath]);
|
|
401
|
+
console.log(`Updated allowlist: ${updated.join(', ')}`);
|
|
402
|
+
} else {
|
|
403
|
+
console.error('Unknown policy subcommand. Use "vf policy list" or "vf policy add <path>"');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'checkpoint': {
|
|
410
|
+
const flags = parseFlags(args.slice(1));
|
|
411
|
+
if (!flags.summary) {
|
|
412
|
+
console.error('Error: --summary <text> is required');
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
const receipt = checkpointService.captureCheckpoint({
|
|
416
|
+
workspacePath: cwd,
|
|
417
|
+
taskId: flags.task,
|
|
418
|
+
summary: flags.summary,
|
|
419
|
+
goals: flags.goals,
|
|
420
|
+
constraints: flags.constraints,
|
|
421
|
+
decisions: flags.decisions,
|
|
422
|
+
nextSteps: flags.nextSteps,
|
|
423
|
+
requestedDocuments: flags.docs,
|
|
424
|
+
includeGit: flags.includeGit,
|
|
425
|
+
authorName: flags.author || process.env.VF_AUTHOR || process.env.USER || 'developer',
|
|
426
|
+
authorHarness: flags.harness || process.env.VF_HARNESS || 'cli'
|
|
427
|
+
});
|
|
428
|
+
console.log(`Checkpoint saved: ${receipt.reference_id} [${receipt.status}]`);
|
|
429
|
+
if (receipt.warnings.length > 0) {
|
|
430
|
+
for (const w of receipt.warnings) console.warn(`Warning: ${w}`);
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case 'resume': {
|
|
436
|
+
const flags = parseFlags(args.slice(1));
|
|
437
|
+
const result = briefingService.generateBriefing({
|
|
438
|
+
workspacePath: cwd,
|
|
439
|
+
taskId: flags.task,
|
|
440
|
+
checkpointId: flags.checkpoint,
|
|
441
|
+
destination: flags.dest || 'cli'
|
|
442
|
+
});
|
|
443
|
+
console.log(result.briefingText);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
case 'drift': {
|
|
448
|
+
const flags = parseFlags(args.slice(1));
|
|
449
|
+
const result = briefingService.generateBriefing({
|
|
450
|
+
workspacePath: cwd,
|
|
451
|
+
taskId: flags.task,
|
|
452
|
+
checkpointId: flags.checkpoint,
|
|
453
|
+
destination: 'cli'
|
|
454
|
+
});
|
|
455
|
+
if (result.driftDetected) {
|
|
456
|
+
console.log('ā ļø DRIFT DETECTED:');
|
|
457
|
+
for (const w of result.warnings) console.log(`- ${w}`);
|
|
458
|
+
} else {
|
|
459
|
+
console.log('ā No drift detected: workspace and document state match recorded checkpoint.');
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
case 'reason':
|
|
465
|
+
case 'continue': {
|
|
466
|
+
const flags = parseFlags(args.slice(1));
|
|
467
|
+
if (flags.raw) {
|
|
468
|
+
const raw = briefingService.generateBriefing({
|
|
469
|
+
workspacePath: cwd,
|
|
470
|
+
taskId: flags.task,
|
|
471
|
+
checkpointId: flags.checkpoint,
|
|
472
|
+
destination: flags.dest || 'cli'
|
|
473
|
+
});
|
|
474
|
+
console.log(raw.briefingText);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (llmProviderError) {
|
|
478
|
+
console.warn(`Warning: LLM configuration issue: ${llmProviderError}`);
|
|
479
|
+
}
|
|
480
|
+
const result = await reasoningService.reasonAboutTask({
|
|
481
|
+
workspacePath: cwd,
|
|
482
|
+
taskId: flags.task,
|
|
483
|
+
checkpointId: flags.checkpoint,
|
|
484
|
+
destination: flags.dest || 'cli',
|
|
485
|
+
focus: flags.focus,
|
|
486
|
+
model: flags.model,
|
|
487
|
+
maxTokens: flags.maxTokens ? parseInt(flags.maxTokens, 10) : 4000,
|
|
488
|
+
includeCode: flags.includeCode !== false
|
|
489
|
+
});
|
|
490
|
+
console.log(result.narrativeText);
|
|
491
|
+
if (result.degraded) {
|
|
492
|
+
console.error(`\nā AI reasoning unavailable${result.error ? ` (${result.error})` : ''} ā showing deterministic briefing.`);
|
|
493
|
+
} else {
|
|
494
|
+
console.error(`\nā reasoned by ${result.provider}/${result.model} Ā· ${result.codeFilesConsidered} code file(s) considered`);
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
case 'export':
|
|
500
|
+
case 'export-handoff': {
|
|
501
|
+
const flags = parseFlags(args.slice(1));
|
|
502
|
+
if (!flags.task) {
|
|
503
|
+
console.error('Error: --task <id> is required for handoff export.');
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
if (!flags.recipient) {
|
|
507
|
+
console.error('Error: --recipient <name> is required for handoff export.');
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
const result = handoffService.exportHandoff({
|
|
511
|
+
workspacePath: cwd,
|
|
512
|
+
taskId: flags.task,
|
|
513
|
+
checkpointId: flags.checkpoint,
|
|
514
|
+
recipient: flags.recipient,
|
|
515
|
+
destinationHarness: flags.dest || 'any',
|
|
516
|
+
preview: flags.preview === true,
|
|
517
|
+
outPath: flags.outPath
|
|
518
|
+
});
|
|
519
|
+
if (result.preview) {
|
|
520
|
+
console.log(`\nā Handoff Preview for [${flags.task}] to ${flags.recipient}:`);
|
|
521
|
+
console.log(JSON.stringify(result.handoff, null, 2));
|
|
522
|
+
} else {
|
|
523
|
+
console.log(`\nā Handoff exported successfully: ${result.package_id}`);
|
|
524
|
+
console.log(` Bundle Path: ${result.bundlePath}`);
|
|
525
|
+
console.log(` Receipt: ${result.receipt.operation_id} [${result.receipt.status}]`);
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
case 'import':
|
|
531
|
+
case 'import-handoff': {
|
|
532
|
+
const bundlePath = args[1];
|
|
533
|
+
if (!bundlePath || bundlePath.startsWith('--')) {
|
|
534
|
+
console.error('Error: Package path required: vf import <package_path> [--task <id>]');
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
const flags = parseFlags(args.slice(2));
|
|
538
|
+
const result = handoffService.importHandoff({
|
|
539
|
+
workspacePath: cwd,
|
|
540
|
+
bundlePath,
|
|
541
|
+
targetTaskId: flags.task
|
|
542
|
+
});
|
|
543
|
+
console.log(`\nā Handoff imported successfully into task [${result.task.task_id}]`);
|
|
544
|
+
console.log(` Imported Checkpoint: ${result.importedCheckpointId}`);
|
|
545
|
+
console.log(` Receipt: ${result.receipt.operation_id} [${result.receipt.status}]`);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
case 'mcp': {
|
|
550
|
+
const server = new McpServer({ store, projectService, checkpointService, briefingService, handoffService, reasoningService }, cwd);
|
|
551
|
+
server.connect(process.stdin, process.stdout);
|
|
552
|
+
process.on('SIGINT', () => {
|
|
553
|
+
store.close();
|
|
554
|
+
process.exit(0);
|
|
555
|
+
});
|
|
556
|
+
process.on('SIGTERM', () => {
|
|
557
|
+
store.close();
|
|
558
|
+
process.exit(0);
|
|
559
|
+
});
|
|
560
|
+
// Keep process alive for stdio stream; do not execute finally store.close()
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
case 'config': {
|
|
565
|
+
const sub = args[1];
|
|
566
|
+
if (sub === 'install' || sub === 'setup') {
|
|
567
|
+
const target = (args[2] || 'all').toLowerCase();
|
|
568
|
+
const installed = [];
|
|
569
|
+
if (target === 'antigravity' || target === 'all') {
|
|
570
|
+
const p = HarnessConfig.installAntigravity(cwd);
|
|
571
|
+
installed.push(`Google Antigravity -> ${p} (+ ~/.gemini/antigravity/mcp_config.json)`);
|
|
572
|
+
}
|
|
573
|
+
if (target === 'command-code' || target === 'cmd' || target === 'claude' || target === 'claude-code' || target === 'all') {
|
|
574
|
+
const p = HarnessConfig.installCommandCode(cwd);
|
|
575
|
+
installed.push(`Command Code / Claude Code -> .mcp.json & ${p}`);
|
|
576
|
+
}
|
|
577
|
+
if (target === 'muse' || target === 'muse-code' || target === 'all') {
|
|
578
|
+
const p = HarnessConfig.installMuseCode(cwd);
|
|
579
|
+
installed.push(`Meta Muse -> ${p} (+ ~/.config/muse/settings.json)`);
|
|
580
|
+
}
|
|
581
|
+
if (target === 'codex' || target === 'all') {
|
|
582
|
+
const p = HarnessConfig.installCodex(cwd);
|
|
583
|
+
installed.push(`Codex -> ${p} (+ ~/.codex/config.toml)`);
|
|
584
|
+
}
|
|
585
|
+
console.log(`\nā Configured harness integration(s):\n - ${installed.join('\n - ')}`);
|
|
586
|
+
console.log(`\nš” Next step: Restart or reload your AI agent, then ask in chat:\n "List active VectorForce tasks and check workspace drift"`);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (sub === 'doctor' || sub === 'check') {
|
|
591
|
+
const checks = [
|
|
592
|
+
{ name: 'SQLite Database (.vectorforce/vectorforce.db)', ok: existsSync(getDbPath(cwd)), path: getDbPath(cwd) },
|
|
593
|
+
{ name: 'Agent Protocol Rules (AGENTS.md)', ok: existsSync(join(cwd, 'AGENTS.md')), path: join(cwd, 'AGENTS.md') },
|
|
594
|
+
{ name: 'Google Antigravity (.agents/mcp_config.json)', ok: existsSync(join(cwd, '.agents', 'mcp_config.json')), path: join(cwd, '.agents', 'mcp_config.json') },
|
|
595
|
+
{ name: 'Claude Code & Command Code (.mcp.json)', ok: existsSync(join(cwd, '.mcp.json')), path: join(cwd, '.mcp.json') },
|
|
596
|
+
{ name: 'Meta Muse (.muse/mcp_config.json)', ok: existsSync(join(cwd, '.muse', 'mcp_config.json')), path: join(cwd, '.muse', 'mcp_config.json') },
|
|
597
|
+
{ name: 'OpenAI Codex (.codex/config.toml)', ok: existsSync(join(cwd, '.codex', 'config.toml')), path: join(cwd, '.codex', 'config.toml') }
|
|
598
|
+
];
|
|
599
|
+
console.log(`\nVectorForce Workspace & MCP Health Check (${cwd}):\n`);
|
|
600
|
+
for (const c of checks) {
|
|
601
|
+
console.log(` ${c.ok ? 'ā [READY] ' : 'ā [MISSING]'} ${c.name}`);
|
|
602
|
+
}
|
|
603
|
+
console.log(`\nHow to verify inside your AI agent chat:`);
|
|
604
|
+
console.log(` Ask: "Check active VectorForce tasks and workspace drift"`);
|
|
605
|
+
console.log(` Your agent will automatically call MCP tools: list_tasks & check_workspace_drift\n`);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (sub === 'uninstall' || sub === 'remove') {
|
|
610
|
+
const target = (args[2] || 'all').toLowerCase();
|
|
611
|
+
const uninstalled = [];
|
|
612
|
+
if (target === 'antigravity' || target === 'all') {
|
|
613
|
+
if (HarnessConfig.uninstallAntigravity(cwd)) uninstalled.push('Antigravity');
|
|
614
|
+
}
|
|
615
|
+
if (target === 'command-code' || target === 'cmd' || target === 'all') {
|
|
616
|
+
if (HarnessConfig.uninstallCommandCode(cwd)) uninstalled.push('Command Code');
|
|
617
|
+
}
|
|
618
|
+
if (target === 'muse' || target === 'muse-code' || target === 'all') {
|
|
619
|
+
if (HarnessConfig.uninstallMuseCode(cwd)) uninstalled.push('Muse Code');
|
|
620
|
+
}
|
|
621
|
+
if (target === 'codex' || target === 'all') {
|
|
622
|
+
if (HarnessConfig.uninstallCodex(cwd)) uninstalled.push('Codex');
|
|
623
|
+
}
|
|
624
|
+
console.log(`\nā Removed harness integration(s): ${uninstalled.length ? uninstalled.join(', ') : 'none found'}`);
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (sub === 'set') {
|
|
629
|
+
const key = args[2];
|
|
630
|
+
const val = args[3];
|
|
631
|
+
if (!key || val === undefined) {
|
|
632
|
+
console.error('Usage: node bin/vf.js config set <KEY> <VALUE>');
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
636
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
637
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
638
|
+
let existing = {};
|
|
639
|
+
if (existsSync(cfgPath)) {
|
|
640
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
641
|
+
}
|
|
642
|
+
existing[key] = val;
|
|
643
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
644
|
+
console.log(`\nā Saved configuration [${key}] in ${cfgPath}`);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (sub === 'openrouter') {
|
|
649
|
+
const key = args[2];
|
|
650
|
+
const model = args[3] || 'deepseek/deepseek-v4.1-flash-1017';
|
|
651
|
+
if (!key) {
|
|
652
|
+
console.error('Usage: node bin/vf.js config openrouter <API_KEY> [MODEL]');
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
656
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
657
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
658
|
+
let existing = {};
|
|
659
|
+
if (existsSync(cfgPath)) {
|
|
660
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
661
|
+
}
|
|
662
|
+
existing.VF_LLM_PROVIDER = 'openrouter';
|
|
663
|
+
existing.OPENROUTER_API_KEY = key;
|
|
664
|
+
existing.VF_OPENROUTER_MODEL = model;
|
|
665
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
666
|
+
console.log(`\nā Configured OpenRouter in ${cfgPath}`);
|
|
667
|
+
console.log(` Provider: openrouter`);
|
|
668
|
+
console.log(` Model: ${model}`);
|
|
669
|
+
console.log(` Base URL: https://openrouter.ai/api/v1`);
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (sub === 'fireworks') {
|
|
674
|
+
const key = args[2];
|
|
675
|
+
const model = args[3] || 'accounts/fireworks/models/muse-spark-open';
|
|
676
|
+
if (!key) {
|
|
677
|
+
console.error('Usage: node bin/vf.js config fireworks <API_KEY> [MODEL]');
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
681
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
682
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
683
|
+
let existing = {};
|
|
684
|
+
if (existsSync(cfgPath)) {
|
|
685
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
686
|
+
}
|
|
687
|
+
existing.VF_LLM_PROVIDER = 'fireworks';
|
|
688
|
+
existing.FIREWORKS_API_KEY = key;
|
|
689
|
+
existing.VF_FIREWORKS_MODEL = model;
|
|
690
|
+
existing.VF_FIREWORKS_BASE_URL = 'https://api.fireworks.ai/inference/v1';
|
|
691
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
692
|
+
console.log(`\nā Configured Fireworks AI (Meta Muse Spark Open) in ${cfgPath}`);
|
|
693
|
+
console.log(` Provider: fireworks`);
|
|
694
|
+
console.log(` Model: ${model}`);
|
|
695
|
+
console.log(` Base URL: https://api.fireworks.ai/inference/v1`);
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (sub === 'muse') {
|
|
700
|
+
const key = args[2];
|
|
701
|
+
const model = args[3] || 'muse-spark-1.3';
|
|
702
|
+
if (!key) {
|
|
703
|
+
console.error('Usage: node bin/vf.js config muse <API_KEY> [MODEL]');
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
707
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
708
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
709
|
+
let existing = {};
|
|
710
|
+
if (existsSync(cfgPath)) {
|
|
711
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
712
|
+
}
|
|
713
|
+
existing.VF_LLM_PROVIDER = 'muse';
|
|
714
|
+
existing.MODEL_API_KEY = key;
|
|
715
|
+
existing.VF_MUSE_MODEL = model;
|
|
716
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
717
|
+
console.log(`\nā Configured Muse in ${cfgPath}`);
|
|
718
|
+
console.log(` Provider: muse`);
|
|
719
|
+
console.log(` Model: ${model}`);
|
|
720
|
+
console.log(` Base URL: https://api.meta.ai/v1`);
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (sub === 'ollama') {
|
|
725
|
+
const model = args[2] || 'llama3.1:8b';
|
|
726
|
+
const baseUrl = args[3] || 'http://127.0.0.1:11434';
|
|
727
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
728
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
729
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
730
|
+
let existing = {};
|
|
731
|
+
if (existsSync(cfgPath)) {
|
|
732
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
733
|
+
}
|
|
734
|
+
existing.VF_LLM_PROVIDER = 'ollama';
|
|
735
|
+
existing.VF_OLLAMA_MODEL = model;
|
|
736
|
+
existing.VF_OLLAMA_BASE_URL = baseUrl;
|
|
737
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
738
|
+
console.log(`\nā Configured Local Ollama in ${cfgPath}`);
|
|
739
|
+
console.log(` Provider: ollama (100% Local & Private)`);
|
|
740
|
+
console.log(` Model: ${model}`);
|
|
741
|
+
console.log(` Base URL: ${baseUrl}`);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (sub === 'openai' || sub === 'codex') {
|
|
746
|
+
const key = args[2];
|
|
747
|
+
const model = args[3] || 'o3-mini';
|
|
748
|
+
const baseUrl = args[4] || 'https://api.openai.com/v1';
|
|
749
|
+
if (!key) {
|
|
750
|
+
console.error('Usage: node bin/vf.js config openai <API_KEY> [MODEL] [BASE_URL]');
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
754
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
755
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
756
|
+
let existing = {};
|
|
757
|
+
if (existsSync(cfgPath)) {
|
|
758
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
759
|
+
}
|
|
760
|
+
existing.VF_LLM_PROVIDER = 'openai';
|
|
761
|
+
existing.VF_LLM_API_KEY = key;
|
|
762
|
+
existing.VF_LLM_MODEL = model;
|
|
763
|
+
existing.VF_LLM_BASE_URL = baseUrl;
|
|
764
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
765
|
+
console.log(`\nā Configured OpenAI in ${cfgPath}`);
|
|
766
|
+
console.log(` Provider: openai`);
|
|
767
|
+
console.log(` Model: ${model}`);
|
|
768
|
+
console.log(` Base URL: ${baseUrl}`);
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (sub === 'anthropic') {
|
|
773
|
+
const key = args[2];
|
|
774
|
+
const model = args[3] || 'claude-sonnet-4-20250514';
|
|
775
|
+
const baseUrl = args[4] || 'https://api.anthropic.com/v1';
|
|
776
|
+
if (!key) {
|
|
777
|
+
console.error('Usage: node bin/vf.js config anthropic <API_KEY> [MODEL] [BASE_URL]');
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
781
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
782
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
783
|
+
let existing = {};
|
|
784
|
+
if (existsSync(cfgPath)) {
|
|
785
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
786
|
+
}
|
|
787
|
+
existing.VF_LLM_PROVIDER = 'anthropic';
|
|
788
|
+
existing.VF_LLM_API_KEY = key;
|
|
789
|
+
existing.VF_LLM_MODEL = model;
|
|
790
|
+
existing.VF_LLM_BASE_URL = baseUrl;
|
|
791
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
792
|
+
console.log(`\nā Configured Anthropic in ${cfgPath}`);
|
|
793
|
+
console.log(` Provider: anthropic`);
|
|
794
|
+
console.log(` Model: ${model}`);
|
|
795
|
+
console.log(` Base URL: ${baseUrl}`);
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (sub === 'byok') {
|
|
800
|
+
const baseUrl = args[2];
|
|
801
|
+
const key = args[3];
|
|
802
|
+
const model = args[4] || 'llama-3.3-70b-versatile';
|
|
803
|
+
if (!baseUrl || !key) {
|
|
804
|
+
console.error('Usage: node bin/vf.js config byok <BASE_URL> <API_KEY> [MODEL]');
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
const vfDir = join(cwd, '.vectorforce');
|
|
808
|
+
mkdirSync(vfDir, { recursive: true, mode: 0o700 });
|
|
809
|
+
const cfgPath = join(vfDir, 'config.json');
|
|
810
|
+
let existing = {};
|
|
811
|
+
if (existsSync(cfgPath)) {
|
|
812
|
+
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch {}
|
|
813
|
+
}
|
|
814
|
+
existing.VF_LLM_PROVIDER = 'byok';
|
|
815
|
+
existing.VF_LLM_BASE_URL = baseUrl;
|
|
816
|
+
existing.VF_LLM_API_KEY = key;
|
|
817
|
+
existing.VF_LLM_MODEL = model;
|
|
818
|
+
writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
819
|
+
console.log(`\nā Configured Custom BYOK Endpoint in ${cfgPath}`);
|
|
820
|
+
console.log(` Provider: byok`);
|
|
821
|
+
console.log(` Model: ${model}`);
|
|
822
|
+
console.log(` Base URL: ${baseUrl}`);
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (sub === 'show' || sub === 'status') {
|
|
827
|
+
const cfg = resolveLlmConfig(process.env, {}, cwd);
|
|
828
|
+
console.log('\nVectorForce AI Reasoning Configuration:');
|
|
829
|
+
console.log(` Provider: ${cfg.provider}`);
|
|
830
|
+
console.log(` Model: ${cfg.model}`);
|
|
831
|
+
console.log(` Base URL: ${cfg.baseUrl}`);
|
|
832
|
+
console.log(` API Key: ${cfg.apiKey ? 'configured (hidden)' : 'none (falling back to local)'}`);
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const binPath = resolve(process.argv[1]);
|
|
837
|
+
console.log(`
|
|
838
|
+
VectorForce Harness Configuration:
|
|
839
|
+
|
|
840
|
+
1. Google Antigravity Configuration:
|
|
841
|
+
File: .agents/mcp_config.json (or ~/.gemini/config/mcp_config.json)
|
|
842
|
+
Content:
|
|
843
|
+
{
|
|
844
|
+
"mcpServers": {
|
|
845
|
+
"vectorforce": {
|
|
846
|
+
"command": "node",
|
|
847
|
+
"args": ["${binPath}", "mcp"]
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
2. Meta Muse Code (muse-code) Configuration:
|
|
853
|
+
File: .muse/mcp_config.json (or musecode.json)
|
|
854
|
+
Content:
|
|
855
|
+
{
|
|
856
|
+
"mcpServers": {
|
|
857
|
+
"vectorforce": {
|
|
858
|
+
"command": "node",
|
|
859
|
+
"args": ["${binPath}", "mcp"],
|
|
860
|
+
"env": { "VF_HARNESS": "muse-code" }
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
3. Command Code (command-code) Configuration:
|
|
866
|
+
Run command:
|
|
867
|
+
cmd mcp add --transport stdio vectorforce -- node ${binPath} mcp
|
|
868
|
+
|
|
869
|
+
4. OpenAI Codex (codex) Configuration:
|
|
870
|
+
File: .codex/config.toml (or codex.json)
|
|
871
|
+
Content:
|
|
872
|
+
[mcp_servers.vectorforce]
|
|
873
|
+
command = "node"
|
|
874
|
+
args = ["${binPath}", "mcp"]
|
|
875
|
+
[mcp_servers.vectorforce.env]
|
|
876
|
+
VF_HARNESS = "codex"
|
|
877
|
+
|
|
878
|
+
Or run command:
|
|
879
|
+
codex mcp add vectorforce -- node ${binPath} mcp
|
|
880
|
+
|
|
881
|
+
Automated Setup:
|
|
882
|
+
node bin/vf.js config install codex
|
|
883
|
+
node bin/vf.js config install all
|
|
884
|
+
|
|
885
|
+
5. AI Reasoning (vf reason / reason_about_task):
|
|
886
|
+
Local Ollama (default, 100% private):
|
|
887
|
+
VF_LLM_PROVIDER=ollama
|
|
888
|
+
VF_OLLAMA_BASE_URL=http://127.0.0.1:11434
|
|
889
|
+
VF_OLLAMA_MODEL=llama3.1:8b
|
|
890
|
+
|
|
891
|
+
Cloud Meta Muse (Responses API):
|
|
892
|
+
VF_LLM_PROVIDER=muse
|
|
893
|
+
MODEL_API_KEY=<your-key>
|
|
894
|
+
VF_MUSE_MODEL=muse-spark-1.3 # default
|
|
895
|
+
VF_MUSE_BASE_URL=https://api.meta.ai/v1 # default
|
|
896
|
+
# Or: node bin/vf.js config muse <API_KEY> [MODEL]
|
|
897
|
+
|
|
898
|
+
Cloud OpenAI Codex / GPT:
|
|
899
|
+
VF_LLM_PROVIDER=codex
|
|
900
|
+
VF_CODEX_BASE_URL=https://api.openai.com/v1
|
|
901
|
+
VF_CODEX_API_KEY=<your-key>
|
|
902
|
+
VF_CODEX_MODEL=o3-mini
|
|
903
|
+
|
|
904
|
+
Cloud OpenRouter (DeepSeek v4.1 Flash / Multi-Model):
|
|
905
|
+
VF_LLM_PROVIDER=openrouter
|
|
906
|
+
OPENROUTER_API_KEY=<your-key> # or VF_OPENROUTER_API_KEY
|
|
907
|
+
VF_OPENROUTER_MODEL=deepseek/deepseek-v4.1-flash-1017 # default
|
|
908
|
+
|
|
909
|
+
Privacy: set VF_LLM_ALLOW_CODE=false (or run with --no-code) to keep source out of the prompt.
|
|
910
|
+
`);
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
case 'remote': {
|
|
915
|
+
const sub = args[1] || 'show';
|
|
916
|
+
if (sub === 'set' || sub === 'add') {
|
|
917
|
+
const urlArg = args[2] && !args[2].startsWith('--') ? args[2] : flags.url;
|
|
918
|
+
if (!urlArg) {
|
|
919
|
+
console.error('Usage: vf remote set <CLOUD_URL> [--key <SUBSCRIPTION_KEY>] [--project <PROJECT_ID>]');
|
|
920
|
+
process.exit(1);
|
|
921
|
+
}
|
|
922
|
+
const saved = saveRemoteConfig(cwd, {
|
|
923
|
+
remoteUrl: urlArg.replace(/\/+$/, ''),
|
|
924
|
+
...(flags.key ? { subscriptionKey: flags.key } : {}),
|
|
925
|
+
...(flags.project ? { remoteProjectId: flags.project } : {})
|
|
926
|
+
});
|
|
927
|
+
console.log(`ā Remote VectorForce Cloud configured in .vectorforce/config.json:`);
|
|
928
|
+
console.log(` Remote URL: ${saved.remoteUrl}`);
|
|
929
|
+
console.log(` Project ID: ${saved.remoteProjectId || 'proj-bistro-pos'}`);
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
const remote = getRemoteConfig(cwd);
|
|
933
|
+
console.log(`\nVectorForce Cloud Remote (${cwd}):`);
|
|
934
|
+
console.log(` Remote URL: ${remote.remoteUrl}`);
|
|
935
|
+
console.log(` Remote Project: ${remote.remoteProjectId}`);
|
|
936
|
+
console.log(` Subscription Key: ${remote.subscriptionKey.slice(0, 12)}...`);
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
case 'push': {
|
|
941
|
+
const remote = getRemoteConfig(cwd);
|
|
942
|
+
const targetUrl = (flags.url || remote.remoteUrl).replace(/\/+$/, '');
|
|
943
|
+
const remoteProjectId = flags.project || remote.remoteProjectId || 'proj-bistro-pos';
|
|
944
|
+
const project = projectService.getProjectForWorkspace(cwd);
|
|
945
|
+
const tasks = store.listTasks(project.project_id);
|
|
946
|
+
const machineId = `${hostname()}-local`;
|
|
947
|
+
|
|
948
|
+
console.log(`š Pushing local context (${project.name}) -> ${targetUrl} ...`);
|
|
949
|
+
|
|
950
|
+
for (const t of tasks) {
|
|
951
|
+
if (flags.task && t.task_id !== flags.task) continue;
|
|
952
|
+
await fetch(`${targetUrl}/api/cloud/tasks`, {
|
|
953
|
+
method: 'POST',
|
|
954
|
+
headers: {
|
|
955
|
+
'Content-Type': 'application/json',
|
|
956
|
+
Authorization: `Bearer ${remote.subscriptionKey}`
|
|
957
|
+
},
|
|
958
|
+
body: JSON.stringify({
|
|
959
|
+
projectId: remoteProjectId,
|
|
960
|
+
taskId: t.task_id,
|
|
961
|
+
title: t.title || t.task_id,
|
|
962
|
+
assignee: `${process.env.USER || 'developer'}@local-mac`,
|
|
963
|
+
role: `Local Workspace (${basename(cwd)})`,
|
|
964
|
+
priority: 'P0',
|
|
965
|
+
machineId,
|
|
966
|
+
harness: flags.harness || 'antigravity'
|
|
967
|
+
})
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const checkpoints = store.getCheckpoints(project.project_id, { limit: 50 });
|
|
972
|
+
let pushedCount = 0;
|
|
973
|
+
let lastResp = null;
|
|
974
|
+
|
|
975
|
+
for (const cp of checkpoints) {
|
|
976
|
+
if (flags.task && cp.task_id !== flags.task) continue;
|
|
977
|
+
const res = await fetch(`${targetUrl}/api/cloud/sync/push`, {
|
|
978
|
+
method: 'POST',
|
|
979
|
+
headers: {
|
|
980
|
+
'Content-Type': 'application/json',
|
|
981
|
+
Authorization: `Bearer ${remote.subscriptionKey}`
|
|
982
|
+
},
|
|
983
|
+
body: JSON.stringify({
|
|
984
|
+
projectId: remoteProjectId,
|
|
985
|
+
taskId: cp.task_id,
|
|
986
|
+
senderMachineId: machineId,
|
|
987
|
+
recipientMachineId: 'computer-2-meta-muse-cloud',
|
|
988
|
+
authorName: `${cp.author?.name || process.env.USER || 'developer'} (${basename(cwd)})`,
|
|
989
|
+
authorHarness: cp.author?.harness || 'antigravity',
|
|
990
|
+
summary: cp.summary,
|
|
991
|
+
goals: cp.claims?.goals || [],
|
|
992
|
+
constraints: cp.claims?.constraints || [],
|
|
993
|
+
decisions: cp.claims?.decisions || [],
|
|
994
|
+
nextSteps: cp.claims?.next_steps || []
|
|
995
|
+
})
|
|
996
|
+
});
|
|
997
|
+
lastResp = await res.json();
|
|
998
|
+
pushedCount++;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
console.log(`ā Pushed ${pushedCount} checkpoint(s) [E2EE AES-256-GCM + X25519 + Ed25519] to ${targetUrl}`);
|
|
1002
|
+
if (lastResp?.checkpoint_id) {
|
|
1003
|
+
console.log(` Latest Cloud Checkpoint: ${lastResp.checkpoint_id}`);
|
|
1004
|
+
}
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
case 'pull': {
|
|
1009
|
+
const remote = getRemoteConfig(cwd);
|
|
1010
|
+
const targetUrl = (flags.url || remote.remoteUrl).replace(/\/+$/, '');
|
|
1011
|
+
const remoteProjectId = flags.project || remote.remoteProjectId || 'proj-bistro-pos';
|
|
1012
|
+
|
|
1013
|
+
console.log(`š Pulling & Decrypting latest context from ${targetUrl} ...`);
|
|
1014
|
+
const res = await fetch(`${targetUrl}/api/cloud/sync/pull`, {
|
|
1015
|
+
method: 'POST',
|
|
1016
|
+
headers: {
|
|
1017
|
+
'Content-Type': 'application/json',
|
|
1018
|
+
Authorization: `Bearer ${remote.subscriptionKey}`
|
|
1019
|
+
},
|
|
1020
|
+
body: JSON.stringify({
|
|
1021
|
+
projectId: remoteProjectId,
|
|
1022
|
+
taskId: flags.task || undefined
|
|
1023
|
+
})
|
|
1024
|
+
});
|
|
1025
|
+
const data = await res.json();
|
|
1026
|
+
if (!res.ok || data.verified === false) {
|
|
1027
|
+
console.error(`Error pulling from remote: ${data.error || res.statusText}`);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const payload = data.decrypted_on_computer_2?.payload;
|
|
1032
|
+
if (payload) {
|
|
1033
|
+
let project;
|
|
1034
|
+
try {
|
|
1035
|
+
project = projectService.getProjectForWorkspace(cwd);
|
|
1036
|
+
} catch {
|
|
1037
|
+
project = projectService.initProject(cwd, basename(cwd));
|
|
1038
|
+
}
|
|
1039
|
+
const targetTaskId = flags.task || payload.task_id || 'cloud-sync-task';
|
|
1040
|
+
if (!store.getTask(project.project_id, targetTaskId)) {
|
|
1041
|
+
store.createTask(project.project_id, targetTaskId, `Synced Task: ${targetTaskId}`);
|
|
1042
|
+
}
|
|
1043
|
+
const leaves = store.getLeafCheckpoints(project.project_id, targetTaskId);
|
|
1044
|
+
const parentId = leaves.length > 0 ? leaves[0].checkpoint_id : undefined;
|
|
1045
|
+
|
|
1046
|
+
const receipt = checkpointService.captureCheckpoint({
|
|
1047
|
+
workspacePath: cwd,
|
|
1048
|
+
taskId: targetTaskId,
|
|
1049
|
+
parentId,
|
|
1050
|
+
authorName: payload.author?.name || 'cloud-sync',
|
|
1051
|
+
authorHarness: payload.author?.harness || 'muse-code',
|
|
1052
|
+
summary: `[Pulled from Cloud] ${payload.summary}`,
|
|
1053
|
+
goals: payload.claims?.goals?.length ? payload.claims.goals : ['Continue cloud-synced task'],
|
|
1054
|
+
constraints: payload.claims?.constraints || [],
|
|
1055
|
+
decisions: payload.claims?.decisions || [],
|
|
1056
|
+
nextSteps: payload.claims?.next_steps?.length ? payload.claims.next_steps : ['Inspect pulled context'],
|
|
1057
|
+
requestedDocuments: ['AGENTS.md'],
|
|
1058
|
+
includeGit: true
|
|
1059
|
+
});
|
|
1060
|
+
console.log(`ā Verified Ed25519 signature, decrypted (X25519 + AES-256-GCM), and reconciled into local SQLite!`);
|
|
1061
|
+
console.log(` Local Checkpoint Committed: ${receipt.reference_id} [${receipt.status}]`);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (data.muse_reasoning) {
|
|
1065
|
+
console.log(`\nš§ Cloud AI Briefing (${data.muse_reasoning.model}):`);
|
|
1066
|
+
console.log(` Summary: ${data.muse_reasoning.where_computer_1_left_off}`);
|
|
1067
|
+
for (const step of data.muse_reasoning.recommended_next_actions_for_computer_2 || []) {
|
|
1068
|
+
console.log(` Next -> ${step}`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
case 'recap':
|
|
1075
|
+
case 'timeline': {
|
|
1076
|
+
const flags = parseFlags(args.slice(1));
|
|
1077
|
+
const days = flags.days ? parseInt(flags.days, 10) : 7;
|
|
1078
|
+
const recap = briefingService.generateRecap({
|
|
1079
|
+
workspacePath: cwd,
|
|
1080
|
+
days,
|
|
1081
|
+
taskId: flags.task,
|
|
1082
|
+
destination: flags.dest || 'cli'
|
|
1083
|
+
});
|
|
1084
|
+
console.log(recap.recapText);
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
case 'doctor':
|
|
1089
|
+
case 'status': {
|
|
1090
|
+
const checks = [
|
|
1091
|
+
{ name: 'SQLite Database (.vectorforce/vectorforce.db)', ok: existsSync(getDbPath(cwd)) },
|
|
1092
|
+
{ name: 'Agent Protocol Rules (AGENTS.md)', ok: existsSync(join(cwd, 'AGENTS.md')) },
|
|
1093
|
+
{ name: 'Google Antigravity (.agents/mcp_config.json)', ok: existsSync(join(cwd, '.agents', 'mcp_config.json')) },
|
|
1094
|
+
{ name: 'Claude Code & Command Code (.mcp.json)', ok: existsSync(join(cwd, '.mcp.json')) },
|
|
1095
|
+
{ name: 'Meta Muse (.muse/mcp_config.json or musecode.json)', ok: existsSync(join(cwd, '.muse', 'mcp_config.json')) || existsSync(join(cwd, 'musecode.json')) },
|
|
1096
|
+
{ name: 'OpenAI Codex (.codex/config.toml)', ok: existsSync(join(cwd, '.codex', 'config.toml')) }
|
|
1097
|
+
];
|
|
1098
|
+
console.log(`\nVectorForce Workspace & MCP Status (${cwd}):\n`);
|
|
1099
|
+
for (const c of checks) {
|
|
1100
|
+
console.log(` ${c.ok ? 'ā [READY] ' : 'ā [MISSING]'} ${c.name}`);
|
|
1101
|
+
}
|
|
1102
|
+
console.log(`\nš” How it works:`);
|
|
1103
|
+
console.log(` 1. You do NOT need to run "vf mcp" manually.`);
|
|
1104
|
+
console.log(` 2. Your AI agent (Antigravity, Claude Code, Command Code, Muse, Codex) reads its config file above and automatically spawns "vf mcp" over stdio.`);
|
|
1105
|
+
console.log(` 3. To verify in your AI chat, ask: "List active VectorForce tasks and check workspace drift"\n`);
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
case 'studio':
|
|
1110
|
+
case 'ui': {
|
|
1111
|
+
const flags = parseFlags(args.slice(1));
|
|
1112
|
+
const port = flags.port || 3210;
|
|
1113
|
+
const studio = createStudioServer({
|
|
1114
|
+
dbPath: getDbPath(cwd),
|
|
1115
|
+
workspacePath: cwd,
|
|
1116
|
+
port
|
|
1117
|
+
});
|
|
1118
|
+
const url = await studio.start();
|
|
1119
|
+
console.log(`\nš VectorForce Studio is running at: ${url}`);
|
|
1120
|
+
console.log(` Connected to SQLite: ${getDbPath(cwd)}`);
|
|
1121
|
+
console.log(` Interactive Topology & AI Memory: ${url}`);
|
|
1122
|
+
console.log(` Press Ctrl+C to stop.\n`);
|
|
1123
|
+
|
|
1124
|
+
try {
|
|
1125
|
+
const { exec } = await import('node:child_process');
|
|
1126
|
+
exec(`open ${url}`);
|
|
1127
|
+
} catch {}
|
|
1128
|
+
|
|
1129
|
+
process.on('SIGINT', async () => {
|
|
1130
|
+
await studio.stop();
|
|
1131
|
+
process.exit(0);
|
|
1132
|
+
});
|
|
1133
|
+
process.on('SIGTERM', async () => {
|
|
1134
|
+
await studio.stop();
|
|
1135
|
+
process.exit(0);
|
|
1136
|
+
});
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
default:
|
|
1141
|
+
console.error(`Unknown command: ${command}`);
|
|
1142
|
+
printUsage();
|
|
1143
|
+
process.exit(1);
|
|
1144
|
+
}
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
if (err instanceof VectorForceError) {
|
|
1147
|
+
console.error(`VectorForce Error [${err.code}]: ${err.message}`);
|
|
1148
|
+
} else {
|
|
1149
|
+
console.error(`Error: ${err.message}`);
|
|
1150
|
+
}
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
} finally {
|
|
1153
|
+
if (command !== 'mcp' && command !== 'studio' && command !== 'ui') {
|
|
1154
|
+
store.close();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
main();
|