vigthoria-cli 1.13.26 → 1.13.30

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.
Files changed (59) hide show
  1. package/README.md +12 -0
  2. package/completions/_vigthoria +1 -0
  3. package/completions/vigthoria.bash +1 -1
  4. package/completions/vigthoria.fish +1 -0
  5. package/dist/commands/chat.js +73 -26
  6. package/dist/commands/config.js +4 -4
  7. package/dist/commands/creative-registration.d.ts +13 -0
  8. package/dist/commands/creative-registration.js +88 -0
  9. package/dist/commands/fork.d.ts +3 -2
  10. package/dist/commands/fork.js +124 -123
  11. package/dist/commands/game.d.ts +8 -0
  12. package/dist/commands/game.js +113 -9
  13. package/dist/commands/history.d.ts +0 -1
  14. package/dist/commands/history.js +8 -22
  15. package/dist/commands/hub.d.ts +20 -0
  16. package/dist/commands/hub.js +17 -3
  17. package/dist/commands/preview.js +7 -2
  18. package/dist/commands/product-run-registration.js +1 -1
  19. package/dist/commands/replay.d.ts +0 -1
  20. package/dist/commands/replay.js +10 -19
  21. package/dist/commands/repo.js +16 -4
  22. package/dist/commands/update-registration.js +2 -2
  23. package/dist/commands/workflow.d.ts +4 -0
  24. package/dist/commands/workflow.js +27 -0
  25. package/dist/index.js +8 -4
  26. package/dist/utils/agentRunOutcome.d.ts +7 -0
  27. package/dist/utils/agentRunOutcome.js +13 -0
  28. package/dist/utils/api.d.ts +20 -5
  29. package/dist/utils/api.js +428 -43
  30. package/dist/utils/command-policy.js +3 -1
  31. package/dist/utils/config.d.ts +2 -0
  32. package/dist/utils/config.js +22 -9
  33. package/dist/utils/frontend-preview-service.d.ts +1 -0
  34. package/dist/utils/frontend-preview-service.js +54 -5
  35. package/dist/utils/model-governance.js +23 -14
  36. package/dist/utils/model-transport-service.js +1 -1
  37. package/dist/utils/network-policy.js +15 -3
  38. package/dist/utils/operator-client.js +23 -4
  39. package/dist/utils/post-write-validator.js +7 -3
  40. package/dist/utils/preview-screenshot-adapter.d.ts +16 -41
  41. package/dist/utils/preview-screenshot-adapter.js +273 -64
  42. package/dist/utils/runtime-capability.d.ts +7 -0
  43. package/dist/utils/runtime-capability.js +11 -0
  44. package/dist/utils/runtime-temp.d.ts +5 -2
  45. package/dist/utils/runtime-temp.js +131 -30
  46. package/dist/utils/tools.js +1 -1
  47. package/dist/utils/v3-stream-events.js +10 -2
  48. package/dist/utils/v3-workspace-service.d.ts +1 -0
  49. package/dist/utils/v3-workspace-service.js +38 -1
  50. package/dist/utils/vigflow-client.d.ts +9 -0
  51. package/dist/utils/vigflow-client.js +48 -2
  52. package/dist/utils/workspace-reference.d.ts +8 -0
  53. package/dist/utils/workspace-reference.js +21 -0
  54. package/install.ps1 +2 -2
  55. package/install.sh +2 -2
  56. package/package.json +4 -6
  57. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
  58. package/scripts/release/validate-live-service-gates.sh +3 -3
  59. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -3,14 +3,17 @@ import { hasLocalV3AgentCapability } from '../utils/runtime-capability.js';
3
3
  * fork.ts — Fork from an existing V3 agent run and stream the result.
4
4
  */
5
5
  import chalk from 'chalk';
6
- import { createRequire } from 'node:module';
7
6
  import { createSpinner, CH } from '../utils/logger.js';
8
7
  import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
9
- const require = createRequire(import.meta.url);
8
+ import { createAPIClient } from '../utils/api-client-factory.js';
9
+ import { buildLocalWorkspaceReference } from '../utils/workspace-reference.js';
10
+ import { guardedFetch } from '../utils/network-policy.js';
10
11
  export class ForkCommand {
11
12
  config;
12
- constructor(config, _logger) {
13
+ logger;
14
+ constructor(config, logger) {
13
15
  this.config = config;
16
+ this.logger = logger;
14
17
  }
15
18
  getHeaders() {
16
19
  const headers = { 'Content-Type': 'application/json' };
@@ -34,50 +37,69 @@ export class ForkCommand {
34
37
  (allowLocal ? 'http://127.0.0.1:8030' : null) ||
35
38
  configuredApiUrl);
36
39
  }
37
- resolveWorkspaceRoot(project) {
38
- if (/^[a-zA-Z]:[\\/]/.test(project) || /^\\\\/.test(project))
39
- return '';
40
- if (typeof require !== 'undefined') {
41
- try {
42
- const path = require('path');
43
- if (!path.isAbsolute(project))
44
- return '';
40
+ buildForkHistory(events, eventIndex) {
41
+ const selected = eventIndex > 0 ? events.slice(0, eventIndex) : events;
42
+ const lines = [
43
+ `Fork point: ${eventIndex > 0 ? eventIndex : events.length} of ${events.length} recorded events.`,
44
+ 'Prior run evidence (metadata only; re-read current files through client tools):',
45
+ ];
46
+ let toolCalls = 0;
47
+ for (const event of selected.slice(-120)) {
48
+ const type = String(event?.type || 'unknown');
49
+ if (type === 'plan') {
50
+ const count = Array.isArray(event.tasks) ? event.tasks.length : 0;
51
+ lines.push(`- plan: ${count} task(s)`);
45
52
  }
46
- catch { }
53
+ else if (type === 'tool_call') {
54
+ toolCalls += 1;
55
+ lines.push(`- tool_call ${toolCalls}: ${String(event.name || 'unknown').slice(0, 80)}`);
56
+ }
57
+ else if (type === 'tool_result') {
58
+ lines.push(`- tool_result: ${String(event.name || 'unknown').slice(0, 80)} success=${event.success !== false}`);
59
+ }
60
+ else if (type === 'file_mutation') {
61
+ lines.push(`- file_mutation: ${String(event.kind || 'change').slice(0, 32)} (path intentionally omitted)`);
62
+ }
63
+ else if (type === 'complete') {
64
+ lines.push('- prior run reached its completion event');
65
+ }
66
+ else if (type === 'error') {
67
+ lines.push(`- prior run error code: ${String(event.code || event.error_code || 'unspecified').slice(0, 80)}`);
68
+ }
69
+ if (lines.join('\n').length > 12_000)
70
+ break;
47
71
  }
48
- return project;
72
+ return lines.join('\n').slice(0, 12_000);
49
73
  }
50
74
  async run(runId, message, options) {
51
75
  const project = options.project || process.cwd();
52
- const workspace = this.resolveWorkspaceRoot(project);
76
+ const workspaceRef = buildLocalWorkspaceReference(project, String(this.config.get('userId') || this.config.get('email') || ''));
53
77
  const eventIndex = options.eventIndex || 0;
54
- const spinner = createSpinner(`Forking run ${runId}...`).start();
78
+ const spinner = createSpinner(`Loading run ${runId} for a local fork...`).start();
55
79
  const streamController = new AbortController();
80
+ let api = null;
56
81
  let interrupted = false;
57
82
  const onInterrupt = () => {
58
83
  interrupted = true;
59
84
  streamController.abort();
85
+ api?.destroy();
60
86
  };
61
87
  process.once('SIGINT', onInterrupt);
62
88
  try {
63
89
  const baseUrl = this.getBaseUrl();
64
- const body = {
65
- workspace_root: workspace,
66
- from_event_index: eventIndex,
67
- new_request: message || '',
68
- stream: true,
69
- context: '',
70
- };
71
- const resp = await fetch(`${baseUrl}/api/runs/${encodeURIComponent(runId)}/fork`, {
72
- method: 'POST',
90
+ const params = new URLSearchParams({
91
+ workspace_root: '',
92
+ local_workspace_path: workspaceRef,
93
+ project_path: workspaceRef,
94
+ });
95
+ const resp = await guardedFetch(`${baseUrl}/api/runs/${encodeURIComponent(runId)}/events?${params}`, {
73
96
  headers: this.getHeaders(),
74
- body: JSON.stringify(body),
75
97
  signal: streamController.signal,
76
- });
98
+ }, { audience: 'v3' });
77
99
  if (!resp.ok) {
78
100
  spinner.stop();
79
101
  if (resp.status === 404) {
80
- throw new CliCommandError(`Run ${runId} not found or has no event log.`, {
102
+ throw new CliCommandError(`Run ${runId} was not found or has no event log.`, {
81
103
  code: 'RUN_NOT_FOUND', status: resp.status,
82
104
  });
83
105
  }
@@ -87,116 +109,94 @@ export class ForkCommand {
87
109
  });
88
110
  }
89
111
  else {
90
- throw new CliCommandError(`Fork failed: ${resp.status} ${resp.statusText}`, {
112
+ throw new CliCommandError(`Could not load the fork source: ${resp.status} ${resp.statusText}`, {
91
113
  code: 'RUN_FORK_FAILED', status: resp.status,
92
114
  });
93
115
  }
94
116
  }
117
+ const source = (await resp.json());
118
+ const sourceEvents = Array.isArray(source.events) ? source.events : [];
119
+ if (eventIndex < 0 || eventIndex > sourceEvents.length) {
120
+ throw new CliCommandError(`Fork event index ${eventIndex} is outside 0..${sourceEvents.length}.`, {
121
+ code: 'RUN_FORK_EVENT_INDEX_INVALID',
122
+ });
123
+ }
124
+ const forkHistory = this.buildForkHistory(sourceEvents, eventIndex);
95
125
  spinner.stop();
96
- if (!options.json)
97
- console.log(chalk.bold(`\n${CH.success} Forked from ${chalk.cyan(runId)} at event ${eventIndex}\n`));
98
- if (!resp.body) {
99
- throw new CliCommandError('Fork response did not contain an event stream', { code: 'RUN_FORK_STREAM_MISSING' });
126
+ if (!options.json) {
127
+ console.log(chalk.bold(`\n${CH.success} Starting local fork from ${chalk.cyan(runId)} at event ${eventIndex || sourceEvents.length}\n`));
100
128
  }
101
- // Stream SSE events
102
- const reader = resp.body.getReader();
103
- const decoder = new TextDecoder();
104
- let buffer = '';
129
+ const streamEvents = [];
105
130
  let toolCallNum = 0;
106
- let completed = false;
107
- const events = [];
108
- const configuredIdle = Number.parseInt(process.env.VIGTHORIA_FORK_IDLE_TIMEOUT_MS || '', 10);
109
- const idleTimeoutMs = Number.isFinite(configuredIdle) && configuredIdle > 0 ? configuredIdle : 60_000;
110
- stream: while (true) {
111
- let timer;
112
- const { done, value } = await Promise.race([
113
- reader.read(),
114
- new Promise((_resolve, reject) => {
115
- timer = setTimeout(() => {
116
- streamController.abort();
117
- reject(new CliCommandError(`Fork event stream was idle for ${idleTimeoutMs}ms.`, {
118
- code: 'RUN_FORK_IDLE_TIMEOUT', category: 'network',
119
- }));
120
- }, idleTimeoutMs);
121
- }),
122
- ]).finally(() => {
123
- if (timer)
124
- clearTimeout(timer);
125
- });
126
- if (done)
127
- break;
128
- buffer += decoder.decode(value, { stream: true });
129
- const lines = buffer.split('\n');
130
- buffer = lines.pop() || '';
131
- for (const line of lines) {
132
- if (!line.startsWith('data: '))
133
- continue;
134
- const payload = line.slice(6).trim();
135
- if (payload === '[DONE]') {
136
- completed = true;
137
- break stream;
131
+ api = createAPIClient(this.config, this.logger);
132
+ const request = [
133
+ message.trim() || 'Continue the previous run from the selected fork point.',
134
+ '',
135
+ forkHistory,
136
+ '',
137
+ 'Use the current client-authoritative workspace as truth. Re-read every needed file through client tools; do not assume prior file contents.',
138
+ ].join('\n');
139
+ const result = await api.runV3AgentWorkflow(request, {
140
+ projectPath: project,
141
+ targetPath: project,
142
+ workspacePath: project,
143
+ localWorkspacePath: project,
144
+ localMachineCapable: true,
145
+ clientToolExecution: true,
146
+ executionSurface: 'fork',
147
+ clientSurface: 'cli',
148
+ rawPrompt: message || request,
149
+ contextualPrompt: forkHistory,
150
+ forkedFrom: runId,
151
+ forkEventIndex: eventIndex,
152
+ onStreamEvent: (event) => {
153
+ streamEvents.push({
154
+ type: String(event.type || 'unknown'),
155
+ name: typeof event.name === 'string' ? event.name : undefined,
156
+ success: typeof event.success === 'boolean' ? event.success : undefined,
157
+ code: typeof event.code === 'string' ? event.code : undefined,
158
+ });
159
+ if (options.json)
160
+ return;
161
+ const type = String(event.type || 'unknown');
162
+ if (type === 'plan') {
163
+ console.log(chalk.magenta(` 📋 PLAN`) + ` ${event.tasks?.length || 0} tasks`);
138
164
  }
139
- try {
140
- const evt = JSON.parse(payload);
141
- const type = evt.type || 'unknown';
142
- events.push(evt);
143
- if (options.json) {
144
- continue;
145
- }
146
- switch (type) {
147
- case 'context':
148
- console.log(chalk.blue(` context: ${evt.context_id || '?'}`) +
149
- (evt.forked_from ? chalk.dim(` (forked from ${evt.forked_from})`) : ''));
150
- break;
151
- case 'start':
152
- console.log(chalk.green(` ▶ START`) + ` task=${evt.task_id || '?'}` +
153
- (evt.forked_from ? chalk.dim(` forked_from=${evt.forked_from}`) : ''));
154
- break;
155
- case 'plan':
156
- console.log(chalk.magenta(` 📋 PLAN`) + ` ${evt.tasks?.length || 0} tasks`);
157
- break;
158
- case 'tool_call':
159
- toolCallNum++;
160
- console.log(chalk.yellow(` 🔧 #${toolCallNum}`) + ` ${evt.name || '?'}(${JSON.stringify(evt.arguments || {}).substring(0, 60)})`);
161
- break;
162
- case 'tool_result': {
163
- const icon = evt.success !== false ? chalk.green('✓') : chalk.red('✗');
164
- console.log(` ${icon} ${evt.name || '?'}: ${chalk.dim((evt.output || '').substring(0, 100))}`);
165
- break;
166
- }
167
- case 'message':
168
- console.log(chalk.cyan(` 💬 `) + (evt.content || '').substring(0, 150));
169
- break;
170
- case 'complete': {
171
- const seal = evt.seal_score ? `[${evt.seal_score.tier} ${evt.seal_score.overall}]` : '';
172
- console.log(chalk.green(` ✅ COMPLETE `) + chalk.yellow(seal) +
173
- ` ${evt.iterations || '?'} iterations, ${evt.tool_calls || '?'} tool calls`);
174
- break;
175
- }
176
- case 'error':
177
- throw new CliCommandError(String(evt.message || 'Fork stream reported an error.'), {
178
- code: 'RUN_FORK_STREAM_ERROR', details: evt,
179
- });
180
- default:
181
- console.log(chalk.dim(` ${type}: ${JSON.stringify(evt).substring(0, 80)}`));
182
- }
165
+ else if (type === 'tool_call') {
166
+ toolCallNum += 1;
167
+ console.log(chalk.yellow(` 🔧 #${toolCallNum}`) + ` ${event.name || '?'}`);
183
168
  }
184
- catch (error) {
185
- if (error instanceof CliCommandError)
186
- throw error;
187
- throw new CliCommandError('Fork event stream contained malformed JSON.', {
188
- code: 'RUN_FORK_STREAM_MALFORMED', details: { payload: payload.slice(0, 200) }, cause: error,
189
- });
169
+ else if (type === 'tool_result') {
170
+ const icon = event.success !== false ? chalk.green('✓') : chalk.red('✗');
171
+ console.log(` ${icon} ${event.name || '?'}`);
190
172
  }
191
- }
192
- }
193
- if (!completed) {
194
- throw new CliCommandError('Fork event stream ended without a completion marker.', { code: 'RUN_FORK_STREAM_INCOMPLETE' });
173
+ else if (type === 'message' && event.content) {
174
+ console.log(chalk.cyan(' 💬 ') + String(event.content).slice(0, 180));
175
+ }
176
+ else if (type === 'complete') {
177
+ console.log(chalk.green(' ✅ COMPLETE'));
178
+ }
179
+ },
180
+ });
181
+ if (result.partial) {
182
+ throw new CliCommandError('Fork execution ended with a partial result.', {
183
+ code: 'RUN_FORK_PARTIAL', details: { contextId: result.contextId, taskId: result.taskId },
184
+ });
195
185
  }
196
186
  if (options.json) {
197
- console.log(formatSuccessJson('fork', { runId, eventIndex, events }, { stream: true, eventCount: events.length }));
187
+ console.log(formatSuccessJson('fork', {
188
+ runId,
189
+ eventIndex: eventIndex || sourceEvents.length,
190
+ taskId: result.taskId,
191
+ contextId: result.contextId,
192
+ content: result.content,
193
+ changedFiles: Object.keys(result.changedFiles || {}),
194
+ events: streamEvents,
195
+ }, { stream: true, eventCount: streamEvents.length, workspaceAuthority: 'client-tool-bridge' }));
198
196
  }
199
197
  else {
198
+ if (result.content)
199
+ console.log(result.content);
200
200
  console.log(chalk.bold(`\n${CH.success} Fork run complete\n`));
201
201
  }
202
202
  }
@@ -208,6 +208,7 @@ export class ForkCommand {
208
208
  throw commandFailure(err, { code: 'RUN_FORK_FAILED' });
209
209
  }
210
210
  finally {
211
+ api?.destroy();
211
212
  process.removeListener('SIGINT', onInterrupt);
212
213
  }
213
214
  }
@@ -8,6 +8,14 @@ export declare function gameProcessInvocation(pm: Manager, args: readonly string
8
8
  executable: string;
9
9
  args: string[];
10
10
  };
11
+ export declare function windowsProcessTreeTerminationInvocation(pid: number, systemRoot?: string): {
12
+ executable: string;
13
+ args: string[];
14
+ };
15
+ export declare const runGameProcess: (pm: Manager, args: string[], cwd: string, timeoutMs?: number, outputMode?: "inherit" | "capture") => Promise<{
16
+ stdout: string;
17
+ stderr: string;
18
+ }>;
11
19
  export declare class GameCommand {
12
20
  private logger;
13
21
  constructor(logger: Logger);
@@ -46,11 +46,114 @@ export function gameProcessInvocation(pm, args, platform = process.platform, com
46
46
  const command = [executable, ...args].join(' ');
47
47
  return { executable: comSpec, args: ['/d', '/s', '/c', command] };
48
48
  }
49
- const run = (pm, args, cwd, timeoutMs = 10 * 60_000) => new Promise((resolve, reject) => { const invocation = gameProcessInvocation(pm, args); const child = spawn(invocation.executable, invocation.args, { cwd, stdio: 'inherit', windowsHide: true, env: safeChildProcessEnv() }); let interrupted = false; const onInterrupt = () => { interrupted = true; child.kill('SIGINT'); }; process.once('SIGINT', onInterrupt); const timer = timeoutMs > 0 ? setTimeout(() => { child.kill('SIGTERM'); reject(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' })); }, timeoutMs) : null; const finish = () => { if (timer)
50
- clearTimeout(timer); process.removeListener('SIGINT', onInterrupt); }; child.once('error', error => { finish(); reject(error); }); child.once('exit', (code, signal) => { finish(); if (interrupted)
51
- reject(new CliCommandError(`${pm} command cancelled by user`, { code: 'GAME_PROCESS_CANCELLED', category: 'cancelled' }));
52
- else
53
- code === 0 ? resolve() : reject(new Error(`${pm} exited with ${signal || code}`)); }); });
49
+ export function windowsProcessTreeTerminationInvocation(pid, systemRoot = process.env.SystemRoot || 'C:\\Windows') {
50
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
51
+ throw new CliCommandError('Cannot terminate a game process without a valid owned PID.', {
52
+ code: 'GAME_PROCESS_ID_INVALID',
53
+ });
54
+ }
55
+ return {
56
+ executable: path.win32.join(systemRoot, 'System32', 'taskkill.exe'),
57
+ args: ['/PID', String(pid), '/T', '/F'],
58
+ };
59
+ }
60
+ async function terminateOwnedProcessTree(child, platform) {
61
+ const pid = child.pid;
62
+ if (!pid)
63
+ return;
64
+ if (platform === 'win32') {
65
+ const invocation = windowsProcessTreeTerminationInvocation(pid);
66
+ await new Promise((resolve) => {
67
+ const killer = spawn(invocation.executable, invocation.args, {
68
+ stdio: 'ignore',
69
+ windowsHide: true,
70
+ env: safeChildProcessEnv(),
71
+ });
72
+ const timer = setTimeout(() => { try {
73
+ killer.kill();
74
+ }
75
+ catch { /* exact helper only */ } resolve(); }, 5_000);
76
+ killer.once('error', () => { clearTimeout(timer); try {
77
+ child.kill();
78
+ }
79
+ catch { /* already gone */ } resolve(); });
80
+ killer.once('exit', () => { clearTimeout(timer); resolve(); });
81
+ });
82
+ return;
83
+ }
84
+ try {
85
+ process.kill(-pid, 'SIGTERM');
86
+ }
87
+ catch {
88
+ try {
89
+ child.kill('SIGTERM');
90
+ }
91
+ catch { /* already gone */ }
92
+ }
93
+ await new Promise((resolve) => setTimeout(resolve, 250));
94
+ try {
95
+ process.kill(-pid, 0);
96
+ process.kill(-pid, 'SIGKILL');
97
+ }
98
+ catch { /* process group exited */ }
99
+ }
100
+ export const runGameProcess = (pm, args, cwd, timeoutMs = 10 * 60_000, outputMode = 'inherit') => new Promise((resolve, reject) => {
101
+ const invocation = gameProcessInvocation(pm, args);
102
+ const platform = process.platform;
103
+ const child = spawn(invocation.executable, invocation.args, {
104
+ cwd,
105
+ stdio: outputMode === 'capture' ? ['ignore', 'pipe', 'pipe'] : 'inherit',
106
+ windowsHide: true,
107
+ env: safeChildProcessEnv(),
108
+ detached: platform !== 'win32',
109
+ });
110
+ let interrupted = false;
111
+ let timedOut = false;
112
+ let settled = false;
113
+ let stdout = '';
114
+ let stderr = '';
115
+ const appendBounded = (current, chunk) => `${current}${String(chunk)}`.slice(-64 * 1024);
116
+ if (outputMode === 'capture') {
117
+ child.stdout?.setEncoding('utf8');
118
+ child.stderr?.setEncoding('utf8');
119
+ child.stdout?.on('data', (chunk) => { stdout = appendBounded(stdout, chunk); });
120
+ child.stderr?.on('data', (chunk) => { stderr = appendBounded(stderr, chunk); });
121
+ }
122
+ const settle = (error) => {
123
+ if (settled)
124
+ return;
125
+ settled = true;
126
+ if (timer)
127
+ clearTimeout(timer);
128
+ process.removeListener('SIGINT', onInterrupt);
129
+ error ? reject(error) : resolve({ stdout, stderr });
130
+ };
131
+ const onInterrupt = () => {
132
+ interrupted = true;
133
+ void terminateOwnedProcessTree(child, platform);
134
+ };
135
+ process.once('SIGINT', onInterrupt);
136
+ const timer = timeoutMs > 0 ? setTimeout(() => {
137
+ timedOut = true;
138
+ void terminateOwnedProcessTree(child, platform).finally(() => settle(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' })));
139
+ }, timeoutMs) : null;
140
+ child.once('error', (error) => settle(error));
141
+ child.once('exit', (code, signal) => {
142
+ if (timedOut) {
143
+ settle(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' }));
144
+ }
145
+ else if (interrupted) {
146
+ settle(new CliCommandError(`${pm} command cancelled by user`, { code: 'GAME_PROCESS_CANCELLED', category: 'cancelled' }));
147
+ }
148
+ else if (code === 0) {
149
+ settle();
150
+ }
151
+ else {
152
+ const detail = outputMode === 'capture' ? `: ${(stderr || stdout).trim().slice(-4_000)}` : '';
153
+ settle(new Error(`${pm} exited with ${signal || code}${detail}`));
154
+ }
155
+ });
156
+ });
54
157
  const html = `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>VGE Game</title></head><body><canvas id="game"></canvas><div id="hud"><strong>Vigthoria Gaming Engine</strong><span>Ready</span></div><script type="module" src="/src/main.ts"></script></body></html>\n`;
55
158
  const style = `*{box-sizing:border-box}html,body,#game{width:100%;height:100%;margin:0;overflow:hidden}body{background:#030510;color:#e8f7ff;font-family:Inter,system-ui,sans-serif}#game{display:block;touch-action:none}#hud{position:fixed;left:24px;top:24px;display:flex;gap:16px;padding:12px 16px;border:1px solid #00d4ff66;border-radius:12px;background:#070a18cc;backdrop-filter:blur(12px);box-shadow:0 0 32px #7c3aed33}#hud span{color:#5eead4}\n`;
56
159
  const main = `import { ArcRotateCamera, Color3, Color4, DirectionalLight, Engine, HemisphericLight, MeshBuilder, PBRMaterial, Scene, ShadowGenerator, Vector3 } from '@babylonjs/core';
@@ -80,21 +183,22 @@ export class GameCommand {
80
183
  if (fs.existsSync(root) && fs.readdirSync(root).length)
81
184
  throw new Error(`Target directory is not empty: ${root}`);
82
185
  fs.mkdirSync(path.join(root, 'src'), { recursive: true });
83
- const pkg = { name: slug, version: '0.1.0', private: true, type: 'module', vigthoria: { kind: 'game', engine: 'babylon', renderTransport: 'client-state', schemaVersion: 1 }, scripts: { dev: 'vite', build: 'tsc --noEmit && vite build', preview: 'vite preview' }, dependencies: { '@babylonjs/core': '7.54.3', '@babylonjs/loaders': '7.54.3' }, devDependencies: { typescript: '5.8.3', vite: '6.1.0' } };
186
+ const pkg = { name: slug, version: '0.1.0', private: true, type: 'module', engines: { node: '>=20.19.0' }, vigthoria: { kind: 'game', engine: 'babylon', renderTransport: 'client-state', schemaVersion: 1 }, scripts: { dev: 'node ./node_modules/vite/bin/vite.js', build: 'node ./node_modules/typescript/bin/tsc --noEmit && node ./node_modules/vite/bin/vite.js build', preview: 'node ./node_modules/vite/bin/vite.js preview' }, dependencies: { '@babylonjs/core': '9.20.0', '@babylonjs/loaders': '9.20.0' }, devDependencies: { typescript: '5.9.3', vite: '7.3.6' } };
84
187
  fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
85
188
  fs.writeFileSync(path.join(root, 'index.html'), html);
86
189
  fs.writeFileSync(path.join(root, 'src/main.ts'), main);
87
190
  fs.writeFileSync(path.join(root, 'src/style.css'), style);
191
+ fs.writeFileSync(path.join(root, 'src/vite-env.d.ts'), '/// <reference types="vite/client" />\n');
88
192
  fs.writeFileSync(path.join(root, '.gitignore'), 'node_modules\ndist\n.vigthoria\n');
89
193
  fs.writeFileSync(path.join(root, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext', lib: ['ES2022', 'DOM'], skipLibCheck: true, moduleResolution: 'Bundler', isolatedModules: true, noEmit: true, strict: true }, include: ['src'] }, null, 2) + '\n');
90
194
  if (o.install !== false)
91
- await run(manager(root, o.packageManager), ['install'], root);
195
+ await runGameProcess(manager(root, o.packageManager), ['install'], root);
92
196
  this.logger.success(`Babylon/VGE game created: ${root}`);
93
197
  this.logger.info(`Next: cd ${slug} && vigthoria game run`);
94
198
  }
95
199
  async run(o) { const root = path.resolve(o.project || process.cwd()); this.project(root); const args = ['run', 'dev', '--', '--host', '127.0.0.1']; if (o.port)
96
200
  args.push('--port', String(o.port)); if (o.open !== false)
97
- args.push('--open'); await run(manager(root, o.packageManager), args, root, 0); }
201
+ args.push('--open'); await runGameProcess(manager(root, o.packageManager), args, root, 0); }
98
202
  async validate(o) {
99
203
  const root = path.resolve(o.project || process.cwd());
100
204
  const checks = [];
@@ -110,7 +214,7 @@ export class GameCommand {
110
214
  const source = this.sources(root);
111
215
  checks.push({ check: 'Babylon client-state metadata', passed: pkg.vigthoria?.engine === 'babylon' && pkg.vigthoria?.renderTransport === 'client-state' }, { check: 'Pinned local Babylon dependencies', passed: /^\d/.test(pkg.dependencies?.['@babylonjs/core'] || '') && /^\d/.test(pkg.dependencies?.['@babylonjs/loaders'] || '') }, { check: 'No CDN engine imports', passed: !/https?:\/\/(cdn|unpkg|jsdelivr)[^\s"']*(babylon|three)/i.test(source) }, { check: 'Runtime diagnostics', passed: source.includes('__vgeDiagnostics') }, { check: 'Client source entry', passed: fs.existsSync(path.join(root, 'src/main.ts')) || fs.existsSync(path.join(root, 'src/main.js')) });
112
216
  try {
113
- await run(manager(root, o.packageManager), ['run', 'build'], root);
217
+ await runGameProcess(manager(root, o.packageManager), ['run', 'build'], root, 10 * 60_000, o.json ? 'capture' : 'inherit');
114
218
  checks.push({ check: 'Production build', passed: true });
115
219
  }
116
220
  catch (e) {
@@ -11,7 +11,6 @@ export declare class HistoryCommand {
11
11
  constructor(config: Config, logger: Logger);
12
12
  private getHeaders;
13
13
  private getBaseUrl;
14
- private resolveWorkspaceRoot;
15
14
  run(options: HistoryOptions): Promise<void>;
16
15
  }
17
16
  export {};
@@ -3,10 +3,10 @@ import { hasLocalV3AgentCapability } from '../utils/runtime-capability.js';
3
3
  * history.ts — List recent V3 agent runs with summaries.
4
4
  */
5
5
  import chalk from 'chalk';
6
- import { createRequire } from 'node:module';
7
6
  import { createSpinner, CH } from '../utils/logger.js';
8
7
  import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
9
- const require = createRequire(import.meta.url);
8
+ import { buildLocalWorkspaceReference } from '../utils/workspace-reference.js';
9
+ import { guardedFetch } from '../utils/network-policy.js';
10
10
  export class HistoryCommand {
11
11
  config;
12
12
  logger;
@@ -36,36 +36,22 @@ export class HistoryCommand {
36
36
  (allowLocal ? 'http://127.0.0.1:8030' : null) ||
37
37
  configuredApiUrl);
38
38
  }
39
- resolveWorkspaceRoot(project) {
40
- // On Windows or non-absolute paths, send empty so the server uses its fallback root
41
- if (/^[a-zA-Z]:[\\/]/.test(project) || /^\\\\/.test(project))
42
- return '';
43
- if (typeof require !== 'undefined') {
44
- try {
45
- const path = require('path');
46
- if (!path.isAbsolute(project))
47
- return '';
48
- }
49
- catch { }
50
- }
51
- return project;
52
- }
53
39
  async run(options) {
54
40
  const limit = options.limit || 20;
55
41
  const project = options.project || process.cwd();
56
- const workspace = this.resolveWorkspaceRoot(project);
42
+ const workspaceRef = buildLocalWorkspaceReference(project, String(this.config.get('userId') || this.config.get('email') || ''));
57
43
  const spinner = createSpinner('Loading run history...').start();
58
44
  try {
59
45
  const baseUrl = this.getBaseUrl();
60
46
  const params = new URLSearchParams({
61
47
  limit: String(limit),
62
- workspace_root: workspace,
63
- local_workspace_path: project,
64
- project_path: project,
48
+ workspace_root: '',
49
+ local_workspace_path: workspaceRef,
50
+ project_path: workspaceRef,
65
51
  });
66
- const resp = await fetch(`${baseUrl}/api/runs?${params}`, {
52
+ const resp = await guardedFetch(`${baseUrl}/api/runs?${params}`, {
67
53
  headers: this.getHeaders(),
68
- });
54
+ }, { audience: 'v3' });
69
55
  if (!resp.ok) {
70
56
  spinner.stop();
71
57
  if (resp.status === 404 || resp.status === 502 || resp.status === 503) {
@@ -6,6 +6,25 @@
6
6
  */
7
7
  import { Config } from '../utils/config.js';
8
8
  import { Logger } from '../utils/logger.js';
9
+ interface Module {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ endpoint: string;
14
+ documentation: string;
15
+ pricing: {
16
+ unit: string;
17
+ cost: number;
18
+ example: string;
19
+ };
20
+ status: string;
21
+ category: string;
22
+ tags: string[];
23
+ unit?: string;
24
+ creditsPerUnit?: number | string;
25
+ euroPerUnit?: number | string;
26
+ }
27
+ export declare function moduleMatchesIdentifier(module: Pick<Module, 'id' | 'name'>, requested: string): boolean;
9
28
  export declare class HubCommand {
10
29
  private config;
11
30
  constructor(config: Config, _logger: Logger);
@@ -39,3 +58,4 @@ export declare class HubCommand {
39
58
  */
40
59
  discover(): Promise<void>;
41
60
  }
61
+ export {};
@@ -8,6 +8,19 @@ import chalk from 'chalk';
8
8
  import { guardedFetch } from '../utils/network-policy.js';
9
9
  import { CliCommandError, commandFailure } from '../utils/command-contract.js';
10
10
  const API_BASE = 'https://hub.vigthoria.io';
11
+ function normalizeModuleIdentifier(value) {
12
+ return String(value || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
13
+ }
14
+ export function moduleMatchesIdentifier(module, requested) {
15
+ const needle = normalizeModuleIdentifier(requested);
16
+ if (!needle)
17
+ return false;
18
+ const id = normalizeModuleIdentifier(module.id);
19
+ const name = normalizeModuleIdentifier(module.name);
20
+ return needle === id
21
+ || needle === name
22
+ || needle === id.replace(/-api$/, '');
23
+ }
11
24
  export class HubCommand {
12
25
  config;
13
26
  constructor(config, _logger) {
@@ -76,6 +89,7 @@ export class HubCommand {
76
89
  data.results.forEach((module, index) => {
77
90
  const statusColor = module.status === 'available' ? chalk.green : chalk.gray;
78
91
  console.log(chalk.bold.white(` ${index + 1}. ${module.name}`));
92
+ console.log(chalk.gray(` Module ID: ${module.id}`));
79
93
  console.log(chalk.gray(` ${module.description.substring(0, 80)}...`));
80
94
  console.log(chalk.cyan(` 💰 ${module.pricing.example}`));
81
95
  console.log(statusColor(` 📊 Status: ${module.status.toUpperCase()}`));
@@ -86,7 +100,7 @@ export class HubCommand {
86
100
  console.log(chalk.cyan(`💡 ${data.suggestion}`));
87
101
  }
88
102
  console.log(chalk.gray('\nTo activate a module: vigthoria hub activate <module-name>'));
89
- console.log(chalk.gray('Example: vigthoria hub activate music\n'));
103
+ console.log(chalk.gray(`Example: vigthoria hub activate ${data.results[0].id}\n`));
90
104
  }
91
105
  catch (error) {
92
106
  throw commandFailure(error, { code: 'HUB_SEARCH_FAILED' });
@@ -144,7 +158,7 @@ export class HubCommand {
144
158
  this.getAuthToken();
145
159
  console.log(chalk.cyan(`\n🔌 Activating module: ${moduleId}...\n`));
146
160
  try {
147
- const module = (await this.moduleCatalog()).find((entry) => entry.id === moduleId);
161
+ const module = (await this.moduleCatalog()).find((entry) => moduleMatchesIdentifier(entry, moduleId));
148
162
  if (!module)
149
163
  throw new CliCommandError(`Unknown Hub module: ${moduleId}`, { code: 'HUB_MODULE_NOT_FOUND', category: 'usage' });
150
164
  console.log(chalk.green(`✅ ${module.name} is published in the Vigthoria module catalog.`));
@@ -186,7 +200,7 @@ export class HubCommand {
186
200
  async info(moduleId) {
187
201
  console.log(chalk.cyan(`\n📖 Module Info: ${moduleId}\n`));
188
202
  try {
189
- const module = (await this.moduleCatalog()).find((entry) => entry.id === moduleId);
203
+ const module = (await this.moduleCatalog()).find((entry) => moduleMatchesIdentifier(entry, moduleId));
190
204
  if (!module)
191
205
  throw new CliCommandError(`Unknown Hub module: ${moduleId}`, { code: 'HUB_MODULE_NOT_FOUND', category: 'usage' });
192
206
  console.log(chalk.bold.white(`${module.name}`));