vigthoria-cli 1.12.3 → 1.13.6
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/dist/commands/chat.d.ts +3 -1
- package/dist/commands/chat.js +36 -42
- package/dist/commands/game.d.ts +26 -0
- package/dist/commands/game.js +113 -0
- package/dist/commands/repo.d.ts +9 -1
- package/dist/commands/repo.js +178 -29
- package/dist/commands/v4.d.ts +3 -0
- package/dist/commands/v4.js +12 -0
- package/dist/index.js +117 -58
- package/dist/utils/api.d.ts +1 -0
- package/dist/utils/api.js +83 -27
- package/dist/utils/brain-hub-client.js +42 -14
- package/dist/utils/codebase-indexer.js +29 -6
- package/dist/utils/command-menu.d.ts +11 -0
- package/dist/utils/command-menu.js +97 -0
- package/dist/utils/deckEvents.js +7 -1
- package/dist/utils/requestIntent.d.ts +4 -0
- package/dist/utils/requestIntent.js +16 -1
- package/dist/utils/tools.js +18 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { BridgeCommand } from './commands/bridge.js';
|
|
|
31
31
|
import { DeviceCommand } from './commands/device.js';
|
|
32
32
|
import { WorkflowCommand } from './commands/workflow.js';
|
|
33
33
|
import { PreviewCommand } from './commands/preview.js';
|
|
34
|
+
import { GameCommand } from './commands/game.js';
|
|
34
35
|
import { LegionCommand } from './commands/legion.js';
|
|
35
36
|
import { HistoryCommand } from './commands/history.js';
|
|
36
37
|
import { ReplayCommand } from './commands/replay.js';
|
|
@@ -38,7 +39,7 @@ import { ForkCommand } from './commands/fork.js';
|
|
|
38
39
|
import { CancelCommand } from './commands/cancel.js';
|
|
39
40
|
import { BackgroundCommand } from './commands/background.js';
|
|
40
41
|
import { SecurityCommand } from './commands/security.js';
|
|
41
|
-
import { V4Command
|
|
42
|
+
import { V4Command } from './commands/v4.js';
|
|
42
43
|
import { runV4Menu } from './commands/v4-menu.js';
|
|
43
44
|
import { Config } from './utils/config.js';
|
|
44
45
|
import { Logger, CH } from './utils/logger.js';
|
|
@@ -49,6 +50,7 @@ import * as os from 'os';
|
|
|
49
50
|
import { fileURLToPath } from 'url';
|
|
50
51
|
import { createHash } from 'crypto';
|
|
51
52
|
import axios from 'axios';
|
|
53
|
+
import { renderDynamicHelp, selectInteractiveCommand } from './utils/command-menu.js';
|
|
52
54
|
// ESM shim — TypeScript emits ESM under "type": "module", so __dirname
|
|
53
55
|
// is not available natively. Restore the CommonJS-style helper so the
|
|
54
56
|
// pre-existing package-discovery logic continues to work unchanged.
|
|
@@ -58,6 +60,44 @@ import { APIClient, sanitizeUserFacingErrorText } from './utils/api.js';
|
|
|
58
60
|
import { getCliStateFile, isOfflineMode, isSafeNpmPackageSpec, isUpdateCheckSuppressed, readCachedLatestVersion, readGatewayPreflightCache, writeCachedLatestVersion, writeGatewayPreflightCache, } from './utils/cli-state.js';
|
|
59
61
|
import { applyLocalTestfarmDefaults, isLocalTestfarmMode } from './utils/localTestMode.js';
|
|
60
62
|
applyLocalTestfarmDefaults();
|
|
63
|
+
// --- Terminal column-width safety clamp -----------------------------------
|
|
64
|
+
// Node's `readline` module (used for the interactive chat `> ` prompt and a
|
|
65
|
+
// few other prompts) redraws the current input line on every keystroke using
|
|
66
|
+
// cursor-position math derived from `process.stdout.columns`. When this CLI
|
|
67
|
+
// runs inside a freshly-spawned ConPTY child (e.g. the Vigthoria Workbench's
|
|
68
|
+
// embedded terminal pane on Windows), there is a brief window right after
|
|
69
|
+
// process start where the console width hasn't been reported yet (or is
|
|
70
|
+
// reported as an implausibly small value like 0/1) before the real terminal
|
|
71
|
+
// size propagates down to the child process. Because `readline` reads
|
|
72
|
+
// `columns` fresh on every redraw (not just once at startup), an early or
|
|
73
|
+
// transient bad reading makes it believe the terminal is only 1 column wide,
|
|
74
|
+
// so it moves the cursor to a new row (and re-indents) after every single
|
|
75
|
+
// typed character -- producing a "staircase" of one-character lines that
|
|
76
|
+
// looks exactly like what's reported. This has nothing to do with the host
|
|
77
|
+
// terminal's real (correct) width; it's purely readline believing a bad
|
|
78
|
+
// number. Clamp `columns`/`rows` to sane floors up front and keep them
|
|
79
|
+
// clamped on every real resize event so readline never sees an implausible
|
|
80
|
+
// value, regardless of what the host terminal actually reports.
|
|
81
|
+
const MIN_TTY_COLUMNS = 30;
|
|
82
|
+
const MIN_TTY_ROWS = 4;
|
|
83
|
+
function clampStreamSize(stream) {
|
|
84
|
+
if (!stream || !stream.isTTY)
|
|
85
|
+
return;
|
|
86
|
+
if (!Number.isFinite(stream.columns) || (stream.columns ?? 0) < MIN_TTY_COLUMNS) {
|
|
87
|
+
stream.columns = 80;
|
|
88
|
+
}
|
|
89
|
+
if (!Number.isFinite(stream.rows) || (stream.rows ?? 0) < MIN_TTY_ROWS) {
|
|
90
|
+
stream.rows = 24;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
clampStreamSize(process.stdout);
|
|
94
|
+
clampStreamSize(process.stderr);
|
|
95
|
+
if (process.stdout.isTTY) {
|
|
96
|
+
process.stdout.on('resize', () => clampStreamSize(process.stdout));
|
|
97
|
+
}
|
|
98
|
+
if (process.stderr.isTTY) {
|
|
99
|
+
process.stderr.on('resize', () => clampStreamSize(process.stderr));
|
|
100
|
+
}
|
|
61
101
|
function isApiError(error) {
|
|
62
102
|
return Boolean(error &&
|
|
63
103
|
typeof error === 'object' &&
|
|
@@ -200,10 +240,14 @@ function maxVersion(...versions) {
|
|
|
200
240
|
async function fetchNpmLatestVersion() {
|
|
201
241
|
const { execSync } = await import('child_process');
|
|
202
242
|
try {
|
|
243
|
+
// Hard 5s timeout: a slow/blocked path to the public npm registry must
|
|
244
|
+
// never hang the caller (e.g. the Workbench's blocking `vigthoria update`
|
|
245
|
+
// invocation) indefinitely.
|
|
203
246
|
const latest = execSync('npm view vigthoria-cli version', {
|
|
204
247
|
encoding: 'utf8',
|
|
205
248
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
206
249
|
windowsHide: true,
|
|
250
|
+
timeout: 5000,
|
|
207
251
|
}).trim();
|
|
208
252
|
return latest || null;
|
|
209
253
|
}
|
|
@@ -212,7 +256,7 @@ async function fetchNpmLatestVersion() {
|
|
|
212
256
|
}
|
|
213
257
|
}
|
|
214
258
|
const VERSION = getVersion();
|
|
215
|
-
const VIGTHORIA_DEFAULT_MANIFEST_URL = process.env.VIGTHORIA_UPDATE_MANIFEST_URL || "https://
|
|
259
|
+
const VIGTHORIA_DEFAULT_MANIFEST_URL = process.env.VIGTHORIA_UPDATE_MANIFEST_URL || "https://coder.vigthoria.io/releases/manifest.json";
|
|
216
260
|
function resolveManifestEntry(manifest, channel) {
|
|
217
261
|
const normalized = String(channel || 'stable').trim() || 'stable';
|
|
218
262
|
if (manifest.channels && manifest.channels[normalized]) {
|
|
@@ -321,6 +365,10 @@ async function installGlobalPackageWithNpm(packageSpec) {
|
|
|
321
365
|
stdio: 'inherit',
|
|
322
366
|
windowsHide: true,
|
|
323
367
|
shell: attempt.shell === true,
|
|
368
|
+
// Hard cap so a stalled/hung npm install (registry outage, network
|
|
369
|
+
// issue, etc.) can never block the caller indefinitely - especially
|
|
370
|
+
// important since the Workbench invokes this synchronously.
|
|
371
|
+
timeout: 180000,
|
|
324
372
|
});
|
|
325
373
|
return;
|
|
326
374
|
}
|
|
@@ -646,7 +694,7 @@ export async function main(args) {
|
|
|
646
694
|
.option('--auto-approve', 'Auto-approve agent actions (dangerous!)', false)
|
|
647
695
|
.option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
|
|
648
696
|
.action(async (options) => {
|
|
649
|
-
const chat = new ChatCommand(config, logger);
|
|
697
|
+
const chat = new ChatCommand(config, logger, program);
|
|
650
698
|
await chat.run({
|
|
651
699
|
model: options.model,
|
|
652
700
|
project: options.project,
|
|
@@ -675,7 +723,7 @@ export async function main(args) {
|
|
|
675
723
|
.option('--auto-approve', 'Auto-approve agent actions (dangerous!)', false)
|
|
676
724
|
.option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
|
|
677
725
|
.action(async (options) => {
|
|
678
|
-
const chat = new ChatCommand(config, logger);
|
|
726
|
+
const chat = new ChatCommand(config, logger, program);
|
|
679
727
|
await chat.run({
|
|
680
728
|
model: options.model,
|
|
681
729
|
project: options.project,
|
|
@@ -707,7 +755,7 @@ export async function main(args) {
|
|
|
707
755
|
.option('--auto-approve', 'Auto-approve all actions (dangerous!)', false)
|
|
708
756
|
.option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
|
|
709
757
|
.action(async (options) => {
|
|
710
|
-
const chat = new ChatCommand(config, logger);
|
|
758
|
+
const chat = new ChatCommand(config, logger, program);
|
|
711
759
|
await chat.run({
|
|
712
760
|
model: options.model,
|
|
713
761
|
project: options.project,
|
|
@@ -737,7 +785,7 @@ export async function main(args) {
|
|
|
737
785
|
.option('--json', 'Emit machine-readable JSON output for direct prompt runs', false)
|
|
738
786
|
.option('--bridge <url>', 'Connect to Vigthoria Commando Bridge for remote admin observability')
|
|
739
787
|
.action(async (options) => {
|
|
740
|
-
const chat = new ChatCommand(config, logger);
|
|
788
|
+
const chat = new ChatCommand(config, logger, program);
|
|
741
789
|
await chat.run({
|
|
742
790
|
model: options.model,
|
|
743
791
|
project: options.project,
|
|
@@ -752,37 +800,7 @@ export async function main(args) {
|
|
|
752
800
|
bridge: options.bridge,
|
|
753
801
|
});
|
|
754
802
|
});
|
|
755
|
-
program
|
|
756
|
-
.command('v4')
|
|
757
|
-
.alias('v4-agent')
|
|
758
|
-
.description('Launch Vigthoria V4 Operating Agent (DeerFlow 2.0 harness)')
|
|
759
|
-
.option('--provider <provider>', 'Provider: cloud | v3_local | byok')
|
|
760
|
-
.option('--api-key <key>', 'BYOK API key for non-interactive mode')
|
|
761
|
-
.option('--base-url <url>', 'Provider base URL override')
|
|
762
|
-
.option('--model <name>', 'Provider model override')
|
|
763
|
-
.option('--transport <mode>', 'Bridge transport: auto | ws | sse', 'auto')
|
|
764
|
-
.option('--harness-stream-url <url>', 'Override harness stream URL')
|
|
765
|
-
.option('--harness-control-url <url>', 'Override harness control URL')
|
|
766
|
-
.option('--session-id <id>', 'Explicit V4 session id')
|
|
767
|
-
.option('--non-interactive', 'Disable interactive menu and use CLI options', false)
|
|
768
|
-
.option('--python <bin>', 'Python binary to use', defaultV4PythonBin())
|
|
769
|
-
.option('--v4-path <path>', 'V4 agent root path (or set VIGTHORIA_V4_AGENT_DIR)', defaultV4AgentDir())
|
|
770
|
-
.action(async (options) => {
|
|
771
|
-
const v4 = new V4Command(config, logger);
|
|
772
|
-
await v4.run({
|
|
773
|
-
provider: options.provider,
|
|
774
|
-
apiKey: options.apiKey,
|
|
775
|
-
baseUrl: options.baseUrl,
|
|
776
|
-
model: options.model,
|
|
777
|
-
transport: options.transport,
|
|
778
|
-
harnessStreamUrl: options.harnessStreamUrl,
|
|
779
|
-
harnessControlUrl: options.harnessControlUrl,
|
|
780
|
-
sessionId: options.sessionId,
|
|
781
|
-
nonInteractive: options.nonInteractive,
|
|
782
|
-
python: options.python,
|
|
783
|
-
v4Path: options.v4Path,
|
|
784
|
-
});
|
|
785
|
-
});
|
|
803
|
+
new V4Command(config, logger).register(program);
|
|
786
804
|
program
|
|
787
805
|
.command('v4-menu')
|
|
788
806
|
.description('Internal: interactive V3/V4 agent + provider picker used by the V4 Operating Agent launcher')
|
|
@@ -1143,20 +1161,21 @@ export async function main(args) {
|
|
|
1143
1161
|
await music.status(taskId, options);
|
|
1144
1162
|
});
|
|
1145
1163
|
// ==================== REPO COMMANDS ====================
|
|
1146
|
-
// Repo command - Push/Pull projects to/from Vigthoria Repository
|
|
1164
|
+
// Repo command - Push/Pull projects to/from Vigthoria Community Repository
|
|
1147
1165
|
const repoCommand = program
|
|
1148
1166
|
.command('repo')
|
|
1149
1167
|
.alias('repository')
|
|
1150
|
-
.description('Push and pull projects to/from your Vigthoria Repository');
|
|
1168
|
+
.description('Push and pull projects to/from your Vigthoria Community Repository');
|
|
1151
1169
|
repoCommand
|
|
1152
1170
|
.command('push [path]')
|
|
1153
1171
|
.alias('upload')
|
|
1154
|
-
.description('Push current or specified project to Vigthoria
|
|
1172
|
+
.description('Push current or specified project to Vigthoria Community Repository')
|
|
1155
1173
|
.option('-n, --name <projectName>', 'Project name override for the remote repo')
|
|
1156
1174
|
.option('-v, --visibility <type>', 'Set visibility (private, restricted, public)', 'private')
|
|
1157
1175
|
.option('-d, --description <text>', 'Project description')
|
|
1158
1176
|
.option('-f, --force', 'Overwrite existing project', false)
|
|
1159
1177
|
.option('-y, --yes', 'Run non-interactively with provided/default values', false)
|
|
1178
|
+
.option('--retry <reviewId>', 'Link this corrected push to a repository security review')
|
|
1160
1179
|
.option('--open-in <engine>', 'After push, open project in engine: shop, visual, game')
|
|
1161
1180
|
.option('--browser', 'Open result URL in default browser', false)
|
|
1162
1181
|
.action(async (pathArg, options) => {
|
|
@@ -1167,7 +1186,8 @@ export async function main(args) {
|
|
|
1167
1186
|
visibility: options.visibility,
|
|
1168
1187
|
description: options.description,
|
|
1169
1188
|
force: options.force,
|
|
1170
|
-
yes: options.yes
|
|
1189
|
+
yes: options.yes,
|
|
1190
|
+
retry: options.retry
|
|
1171
1191
|
});
|
|
1172
1192
|
if (options.openIn) {
|
|
1173
1193
|
const engine = options.openIn;
|
|
@@ -1179,10 +1199,18 @@ export async function main(args) {
|
|
|
1179
1199
|
}
|
|
1180
1200
|
}
|
|
1181
1201
|
});
|
|
1202
|
+
repoCommand
|
|
1203
|
+
.command('review [reviewId]')
|
|
1204
|
+
.description('Show repository security review status, findings, and retry guidance')
|
|
1205
|
+
.option('--json', 'Emit machine-readable JSON output', false)
|
|
1206
|
+
.action(async (reviewId, options) => {
|
|
1207
|
+
const repo = new RepoCommand(config, logger);
|
|
1208
|
+
await repo.review(reviewId, { json: options.json });
|
|
1209
|
+
});
|
|
1182
1210
|
repoCommand
|
|
1183
1211
|
.command('pull <name>')
|
|
1184
1212
|
.alias('download')
|
|
1185
|
-
.description('Pull a project from your Vigthoria
|
|
1213
|
+
.description('Pull a project from your Vigthoria Community Repository')
|
|
1186
1214
|
.option('-o, --output <path>', 'Output directory path')
|
|
1187
1215
|
.option('-f, --force', 'Overwrite existing directory', false)
|
|
1188
1216
|
.action(async (name, options) => {
|
|
@@ -1195,7 +1223,7 @@ export async function main(args) {
|
|
|
1195
1223
|
repoCommand
|
|
1196
1224
|
.command('list')
|
|
1197
1225
|
.alias('ls')
|
|
1198
|
-
.description('List all your projects in Vigthoria
|
|
1226
|
+
.description('List all your projects in Vigthoria Community Repository')
|
|
1199
1227
|
.option('-v, --visibility <type>', 'Filter by visibility (private, restricted, public)')
|
|
1200
1228
|
.action(async (options) => {
|
|
1201
1229
|
const repo = new RepoCommand(config, logger);
|
|
@@ -1219,14 +1247,14 @@ export async function main(args) {
|
|
|
1219
1247
|
repoCommand
|
|
1220
1248
|
.command('delete <name>')
|
|
1221
1249
|
.alias('rm')
|
|
1222
|
-
.description('Remove a project from your Vigthoria
|
|
1250
|
+
.description('Remove a project from your Vigthoria Community Repository')
|
|
1223
1251
|
.action(async (name) => {
|
|
1224
1252
|
const repo = new RepoCommand(config, logger);
|
|
1225
1253
|
await repo.delete(name);
|
|
1226
1254
|
});
|
|
1227
1255
|
repoCommand
|
|
1228
1256
|
.command('clone <url>')
|
|
1229
|
-
.description('Clone a public project from Vigthoria
|
|
1257
|
+
.description('Clone a public project from Vigthoria Community')
|
|
1230
1258
|
.option('-o, --output <path>', 'Output directory path')
|
|
1231
1259
|
.option('-f, --force', 'Overwrite existing directory', false)
|
|
1232
1260
|
.action(async (url, options) => {
|
|
@@ -1238,7 +1266,7 @@ export async function main(args) {
|
|
|
1238
1266
|
});
|
|
1239
1267
|
repoCommand
|
|
1240
1268
|
.command('open-in <engine> [projectName]')
|
|
1241
|
-
.description('Open a Vigthoria
|
|
1269
|
+
.description('Open a Vigthoria Community Repository project in the Shop Engine, Visual Editor, or Gaming Engine')
|
|
1242
1270
|
.option('--browser', 'Open result URL in default browser', false)
|
|
1243
1271
|
.option('--shop-id <id>', 'Target shop ID (for shop engine)', 'default')
|
|
1244
1272
|
.addHelpText('after', `
|
|
@@ -1416,6 +1444,12 @@ Examples:
|
|
|
1416
1444
|
screenshot: options.screenshot,
|
|
1417
1445
|
});
|
|
1418
1446
|
});
|
|
1447
|
+
// ==================== GAME COMMAND ====================
|
|
1448
|
+
const gameCommand = program.command('game').description('Create, run, and validate Babylon/VGE game projects');
|
|
1449
|
+
gameCommand.command('init [name]').description('Create a production-ready client-rendered Babylon/VGE game').option('--engine <engine>', 'Game engine', 'babylon').option('--no-install', 'Create files without installing dependencies').option('--package-manager <manager>', 'npm, pnpm, yarn, or bun').action(async (name, options) => { await new GameCommand(logger).init(name, options); });
|
|
1450
|
+
gameCommand.command('run').description('Run the local game development server').option('-p, --project <path>', 'Game project directory', process.cwd()).option('--port <number>', 'Development server port', parseInt).option('--no-open', 'Do not open a browser').option('--package-manager <manager>', 'npm, pnpm, yarn, or bun').action(async (options) => { await new GameCommand(logger).run(options); });
|
|
1451
|
+
gameCommand.command('validate').description('Build and validate a Babylon/VGE game project').option('-p, --project <path>', 'Game project directory', process.cwd()).option('--json', 'Print machine-readable validation results').option('--package-manager <manager>', 'npm, pnpm, yarn, or bun').action(async (options) => { await new GameCommand(logger).validate(options); });
|
|
1452
|
+
gameCommand.action(() => gameCommand.outputHelp());
|
|
1419
1453
|
// ==================== LEGION COMMAND ====================
|
|
1420
1454
|
program
|
|
1421
1455
|
.command('legion [request]')
|
|
@@ -1676,10 +1710,23 @@ Examples:
|
|
|
1676
1710
|
console.log(chalk.gray('Continuing with npm registry check...'));
|
|
1677
1711
|
}
|
|
1678
1712
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1713
|
+
// The release manifest (server-curated, sha256-verified) is the
|
|
1714
|
+
// authoritative source of truth for this private CLI. Only fall back
|
|
1715
|
+
// to probing the public npm registry when the manifest itself has no
|
|
1716
|
+
// usable entry - never let an npm version number override a valid,
|
|
1717
|
+
// verified manifest entry (npm install -g against the public registry
|
|
1718
|
+
// is slower, unverified, and has previously caused the Workbench's
|
|
1719
|
+
// update button to hang for minutes).
|
|
1720
|
+
let npmVersion = null;
|
|
1721
|
+
if (manifestEntry?.version && manifestEntry?.url) {
|
|
1722
|
+
console.log(chalk.gray('Release manifest has a valid entry; skipping public npm registry lookup.'));
|
|
1723
|
+
}
|
|
1724
|
+
else {
|
|
1725
|
+
console.log(chalk.cyan('Checking npm registry...'));
|
|
1726
|
+
npmVersion = await fetchNpmLatestVersion();
|
|
1727
|
+
if (!npmVersion) {
|
|
1728
|
+
console.log(chalk.yellow('Could not read latest version from npm registry.'));
|
|
1729
|
+
}
|
|
1683
1730
|
}
|
|
1684
1731
|
const effectiveLatest = maxVersion(manifestEntry?.version, npmVersion);
|
|
1685
1732
|
if (!effectiveLatest) {
|
|
@@ -1687,11 +1734,6 @@ Examples:
|
|
|
1687
1734
|
process.exitCode = 1;
|
|
1688
1735
|
return;
|
|
1689
1736
|
}
|
|
1690
|
-
if (manifestEntry?.version
|
|
1691
|
-
&& npmVersion
|
|
1692
|
-
&& compareVersions(npmVersion, manifestEntry.version) > 0) {
|
|
1693
|
-
console.log(chalk.yellow(`Release manifest (${manifestEntry.version}) is behind npm (${npmVersion}); npm will be used when needed.`));
|
|
1694
|
-
}
|
|
1695
1737
|
if (!allowDowngrade && compareVersions(effectiveLatest, currentVersion) <= 0) {
|
|
1696
1738
|
console.log(chalk.green(`You are running the latest version (${currentVersion})`));
|
|
1697
1739
|
return;
|
|
@@ -1807,6 +1849,21 @@ Examples:
|
|
|
1807
1849
|
const configCmd = new ConfigCommand(config, logger);
|
|
1808
1850
|
await configCmd.init();
|
|
1809
1851
|
});
|
|
1852
|
+
program
|
|
1853
|
+
.command('menu')
|
|
1854
|
+
.description('Open the interactive command-family menu')
|
|
1855
|
+
.action(async () => {
|
|
1856
|
+
const selectedArgs = await selectInteractiveCommand(program);
|
|
1857
|
+
if (selectedArgs) {
|
|
1858
|
+
await program.parseAsync([args[0], args[1], ...selectedArgs]);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
program
|
|
1862
|
+
.command('commands')
|
|
1863
|
+
.description('List every CLI command grouped by command family')
|
|
1864
|
+
.action(() => {
|
|
1865
|
+
console.log(renderDynamicHelp(program));
|
|
1866
|
+
});
|
|
1810
1867
|
const codingCommandDefinitions = [
|
|
1811
1868
|
{ name: 'edit', description: 'Edit code by describing the desired change', instruction: 'Edit the project according to this request' },
|
|
1812
1869
|
{ name: 'generate', description: 'Generate code from a prompt', instruction: 'Generate code for this request' },
|
|
@@ -1823,7 +1880,7 @@ Examples:
|
|
|
1823
1880
|
.option('-p, --project <path>', 'Project directory', process.cwd())
|
|
1824
1881
|
.action(async (requestParts = [], options) => {
|
|
1825
1882
|
const requestText = requestParts.join(' ').trim();
|
|
1826
|
-
const chat = new ChatCommand(config, logger);
|
|
1883
|
+
const chat = new ChatCommand(config, logger, program);
|
|
1827
1884
|
await chat.run({
|
|
1828
1885
|
model: options.model || 'code',
|
|
1829
1886
|
project: options.project || process.cwd(),
|
|
@@ -1834,8 +1891,10 @@ Examples:
|
|
|
1834
1891
|
try {
|
|
1835
1892
|
// Default to chat if no command
|
|
1836
1893
|
if (args.length === 2) {
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1894
|
+
const selectedArgs = await selectInteractiveCommand(program);
|
|
1895
|
+
if (selectedArgs) {
|
|
1896
|
+
await program.parseAsync([args[0], args[1], ...selectedArgs]);
|
|
1897
|
+
}
|
|
1839
1898
|
process.exitCode = 0;
|
|
1840
1899
|
return;
|
|
1841
1900
|
}
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -277,6 +277,7 @@ export declare class APIClient {
|
|
|
277
277
|
private listFrontendWorkspaceFiles;
|
|
278
278
|
private chooseFrontendPreviewEntry;
|
|
279
279
|
private extractLinkedFrontendAssets;
|
|
280
|
+
private buildFrontendForPreview;
|
|
280
281
|
private gatherFrontendPreviewArtifacts;
|
|
281
282
|
private captureFrontendPreviewScreenshot;
|
|
282
283
|
private evaluateFrontendVisualProof;
|
package/dist/utils/api.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Connects to coder.vigthoria.io API endpoints
|
|
4
4
|
*/
|
|
5
5
|
import axios from 'axios';
|
|
6
|
+
import { execFile } from 'child_process';
|
|
6
7
|
import { randomUUID } from 'crypto';
|
|
7
8
|
import fs from 'fs';
|
|
8
9
|
import https from 'https';
|
|
@@ -743,8 +744,8 @@ export class APIClient {
|
|
|
743
744
|
const urls = [
|
|
744
745
|
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
|
|
745
746
|
process.env.TEMPLATE_SERVICE_URL,
|
|
746
|
-
'http://127.0.0.1:4011',
|
|
747
747
|
`${configuredApiUrl}/api/template-service`,
|
|
748
|
+
'http://127.0.0.1:4011',
|
|
748
749
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
749
750
|
return [...new Set(urls)];
|
|
750
751
|
}
|
|
@@ -842,7 +843,15 @@ export class APIClient {
|
|
|
842
843
|
const baseName = path.posix.basename(relativePath).toLowerCase();
|
|
843
844
|
const depth = relativePath.split('/').length - 1;
|
|
844
845
|
let score = 0;
|
|
845
|
-
|
|
846
|
+
const normalizedPath = relativePath.toLowerCase();
|
|
847
|
+
const topLevelDir = normalizedPath.split('/')[0];
|
|
848
|
+
if (['dist', 'build', 'out'].includes(topLevelDir)) {
|
|
849
|
+
score += 320;
|
|
850
|
+
}
|
|
851
|
+
else if (topLevelDir === 'public') {
|
|
852
|
+
score += 180;
|
|
853
|
+
}
|
|
854
|
+
if (normalizedPath === 'index.html') {
|
|
846
855
|
score += 200;
|
|
847
856
|
}
|
|
848
857
|
if (baseName === 'index.html') {
|
|
@@ -896,6 +905,34 @@ export class APIClient {
|
|
|
896
905
|
js: Array.from(js),
|
|
897
906
|
};
|
|
898
907
|
}
|
|
908
|
+
async buildFrontendForPreview(rootPath) {
|
|
909
|
+
const packagePath = path.join(rootPath, 'package.json');
|
|
910
|
+
if (!fs.existsSync(packagePath)) {
|
|
911
|
+
return { attempted: false, succeeded: false };
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
915
|
+
if (!packageJson.scripts?.build) {
|
|
916
|
+
return { attempted: false, succeeded: false };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
return { attempted: false, succeeded: false, error: `Invalid package.json: ${error?.message || String(error)}` };
|
|
921
|
+
}
|
|
922
|
+
const npmCommand = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : 'npm';
|
|
923
|
+
const npmArgs = process.platform === 'win32'
|
|
924
|
+
? ['/d', '/s', '/c', 'npm run build']
|
|
925
|
+
: ['run', 'build'];
|
|
926
|
+
return await new Promise((resolve) => {
|
|
927
|
+
execFile(npmCommand, npmArgs, { cwd: rootPath, timeout: 150_000, windowsHide: true, maxBuffer: 2 * 1024 * 1024 }, (error) => {
|
|
928
|
+
if (error) {
|
|
929
|
+
resolve({ attempted: true, succeeded: false, error: sanitizeUserFacingErrorText(error.message).slice(0, 300) });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
resolve({ attempted: true, succeeded: true });
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
}
|
|
899
936
|
gatherFrontendPreviewArtifacts(message = '', context = {}) {
|
|
900
937
|
const rootPath = this.resolveAgentTargetPath(context);
|
|
901
938
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
@@ -1051,12 +1088,22 @@ export class APIClient {
|
|
|
1051
1088
|
return { required: false, passed: true };
|
|
1052
1089
|
}
|
|
1053
1090
|
const rootPath = this.resolveAgentTargetPath(context);
|
|
1054
|
-
|
|
1091
|
+
let artifacts = this.gatherFrontendPreviewArtifacts(message, context);
|
|
1092
|
+
let buildResult = null;
|
|
1093
|
+
if (rootPath && artifacts.error?.includes('at least one HTML entry file')) {
|
|
1094
|
+
buildResult = await this.buildFrontendForPreview(rootPath);
|
|
1095
|
+
if (buildResult.attempted) {
|
|
1096
|
+
artifacts = this.gatherFrontendPreviewArtifacts(message, context);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1055
1099
|
if (artifacts.error || !artifacts.htmlPath || typeof artifacts.html !== 'string') {
|
|
1100
|
+
const buildDetail = buildResult?.attempted
|
|
1101
|
+
? ` Build retry ${buildResult.succeeded ? 'completed but produced no HTML entry' : `failed: ${buildResult.error || 'unknown build error'}`}.`
|
|
1102
|
+
: '';
|
|
1056
1103
|
return {
|
|
1057
1104
|
required: true,
|
|
1058
1105
|
passed: false,
|
|
1059
|
-
error: artifacts.error || 'Frontend preview gate could not collect frontend artifacts.'
|
|
1106
|
+
error: `${artifacts.error || 'Frontend preview gate could not collect frontend artifacts.'}${buildDetail}`,
|
|
1060
1107
|
};
|
|
1061
1108
|
}
|
|
1062
1109
|
// Cap artifact sizes to prevent 413 Payload Too Large
|
|
@@ -1083,11 +1130,19 @@ export class APIClient {
|
|
|
1083
1130
|
error: 'Preview proof skipped: payload exceeds size limit.',
|
|
1084
1131
|
};
|
|
1085
1132
|
}
|
|
1133
|
+
const internalServiceKey = process.env.VIGTHORIA_TEMPLATE_SERVICE_KEY
|
|
1134
|
+
|| process.env.V3_SERVICE_KEY;
|
|
1135
|
+
const authToken = this.getAccessToken();
|
|
1086
1136
|
const response = await fetch(`${baseUrl}/preview-proof`, {
|
|
1087
1137
|
method: 'POST',
|
|
1088
1138
|
headers: {
|
|
1089
1139
|
'Content-Type': 'application/json',
|
|
1090
1140
|
Accept: 'application/json',
|
|
1141
|
+
...(authToken ? {
|
|
1142
|
+
Authorization: `Bearer ${authToken}`,
|
|
1143
|
+
Cookie: `vigthoria-auth-token=${authToken}`,
|
|
1144
|
+
} : {}),
|
|
1145
|
+
...(internalServiceKey ? { 'x-vigthoria-service-key': internalServiceKey } : {}),
|
|
1091
1146
|
},
|
|
1092
1147
|
body: proofPayload,
|
|
1093
1148
|
});
|
|
@@ -1772,10 +1827,14 @@ export class APIClient {
|
|
|
1772
1827
|
return finish();
|
|
1773
1828
|
}
|
|
1774
1829
|
/** Normalize common Unix-only commands before Windows cmd/powershell execution. */
|
|
1775
|
-
normalizeWindowsRunCommand(command) {
|
|
1830
|
+
normalizeWindowsRunCommand(command, serverRoot = '') {
|
|
1776
1831
|
let cmd = String(command || '').trim();
|
|
1777
1832
|
if (!cmd)
|
|
1778
1833
|
return cmd;
|
|
1834
|
+
if (serverRoot) {
|
|
1835
|
+
const escapedRoot = serverRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1836
|
+
cmd = cmd.replace(new RegExp(`^\\s*cd\\s+(?:[\"']?${escapedRoot}[\"']?)\\s*(?:&&|;)\\s*`, 'i'), '');
|
|
1837
|
+
}
|
|
1779
1838
|
if (/^\/[^\s/]/.test(cmd) && !/^\/[a-z]:/i.test(cmd)) {
|
|
1780
1839
|
throw new Error('Command starts with "/" which Windows interprets as an invalid switch. Use workspace-relative paths without a leading slash.');
|
|
1781
1840
|
}
|
|
@@ -3082,38 +3141,35 @@ menu {
|
|
|
3082
3141
|
return { success: false, output: '', error: 'run_command requires command.' };
|
|
3083
3142
|
try {
|
|
3084
3143
|
if (process.platform === 'win32') {
|
|
3085
|
-
command = this.normalizeWindowsRunCommand(command);
|
|
3144
|
+
command = this.normalizeWindowsRunCommand(command, serverRoot);
|
|
3086
3145
|
}
|
|
3087
3146
|
}
|
|
3088
3147
|
catch (error) {
|
|
3089
3148
|
return { success: false, output: '', error: error.message };
|
|
3090
3149
|
}
|
|
3091
3150
|
const cwd = this.resolveRunCommandCwd(rootPath, args, serverRoot);
|
|
3092
|
-
const
|
|
3093
|
-
&& /^(npm|npx|node|yarn|pnpm|git|python|pip|powershell)\b/i.test(command);
|
|
3094
|
-
const { exec } = await import('child_process');
|
|
3151
|
+
const { exec, execFile } = await import('child_process');
|
|
3095
3152
|
return await new Promise((resolve) => {
|
|
3096
|
-
const
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
};
|
|
3102
|
-
if (usePowerShell) {
|
|
3103
|
-
execOptions.shell = 'powershell.exe';
|
|
3104
|
-
execOptions.windowsHide = true;
|
|
3105
|
-
command = `-NoProfile -ExecutionPolicy Bypass -Command ${JSON.stringify(command)}`;
|
|
3106
|
-
}
|
|
3107
|
-
else if (process.platform === 'win32') {
|
|
3108
|
-
execOptions.shell = 'cmd.exe';
|
|
3109
|
-
}
|
|
3110
|
-
exec(command, execOptions, (error, stdout, stderr) => {
|
|
3153
|
+
const timeout = Number(args.timeout || 30000);
|
|
3154
|
+
const finish = (error, stdout, stderr) => {
|
|
3155
|
+
const stdoutText = String(stdout || '');
|
|
3156
|
+
const stderrText = String(stderr || '');
|
|
3157
|
+
const combined = [stdoutText, stderrText].filter(Boolean).join('\n').slice(0, 12000);
|
|
3111
3158
|
resolve({
|
|
3112
3159
|
success: !error,
|
|
3113
|
-
output:
|
|
3114
|
-
error: error ? String(
|
|
3160
|
+
output: combined,
|
|
3161
|
+
error: error ? String(error.message || error) : '',
|
|
3115
3162
|
});
|
|
3116
|
-
}
|
|
3163
|
+
};
|
|
3164
|
+
if (process.platform === 'win32') {
|
|
3165
|
+
const commandProcessor = process.env.ComSpec
|
|
3166
|
+
|| path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe');
|
|
3167
|
+
execFile(commandProcessor, ['/d', '/s', '/c', command], {
|
|
3168
|
+
cwd, timeout, maxBuffer: 1024 * 1024, env: process.env, windowsHide: true,
|
|
3169
|
+
}, finish);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
exec(command, { cwd, timeout, maxBuffer: 1024 * 1024, env: process.env }, finish);
|
|
3117
3173
|
});
|
|
3118
3174
|
}
|
|
3119
3175
|
return { success: false, output: '', error: `Unsupported client tool: ${name}` };
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
* Brain Hub client — sync workspace index + fetch account brain context
|
|
3
3
|
* via coder.vigthoria.io (JWT-authenticated).
|
|
4
4
|
*/
|
|
5
|
+
// Brain Hub calls are an optional personalization enhancement to an agent
|
|
6
|
+
// turn, not on the critical path. Native `fetch()` has NO default timeout,
|
|
7
|
+
// so a slow/unresponsive coder.vigthoria.io backend previously caused the
|
|
8
|
+
// ENTIRE agent turn to hang indefinitely with zero console output, well
|
|
9
|
+
// before any "ROUTING DECISION" text is printed (2026-07-10 root-cause).
|
|
10
|
+
const BRAIN_HUB_TIMEOUT_MS = 3500;
|
|
11
|
+
async function fetchWithTimeout(url, init = {}, timeoutMs = BRAIN_HUB_TIMEOUT_MS) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
14
|
+
try {
|
|
15
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
5
21
|
export class BrainHubClient {
|
|
6
22
|
apiBase;
|
|
7
23
|
getAuthToken;
|
|
@@ -22,27 +38,39 @@ export class BrainHubClient {
|
|
|
22
38
|
if (!headers.Authorization) {
|
|
23
39
|
return { ok: false, skipped: true, reason: 'not_authenticated' };
|
|
24
40
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
try {
|
|
42
|
+
const resp = await fetchWithTimeout(`${this.apiBase}/api/brain/sync-index`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers,
|
|
45
|
+
body: JSON.stringify(payload),
|
|
46
|
+
});
|
|
47
|
+
const data = await resp.json().catch(() => ({}));
|
|
48
|
+
if (!resp.ok) {
|
|
49
|
+
return { ok: false, error: data.error || `HTTP ${resp.status}` };
|
|
50
|
+
}
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const timedOut = error instanceof Error && error.name === 'AbortError';
|
|
55
|
+
return { ok: false, error: timedOut ? `timeout after ${BRAIN_HUB_TIMEOUT_MS}ms` : String(error) };
|
|
33
56
|
}
|
|
34
|
-
return data;
|
|
35
57
|
}
|
|
36
58
|
async fetchAccountContext(limit = 25) {
|
|
37
59
|
const headers = await this.headers();
|
|
38
60
|
if (!headers.Authorization) {
|
|
39
61
|
return { ok: false, formattedText: '', memories: [] };
|
|
40
62
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
try {
|
|
64
|
+
const resp = await fetchWithTimeout(`${this.apiBase}/api/brain/context?limit=${limit}`, { headers });
|
|
65
|
+
const data = await resp.json().catch(() => ({}));
|
|
66
|
+
if (!resp.ok) {
|
|
67
|
+
return { ok: false, formattedText: '', error: data.error || `HTTP ${resp.status}` };
|
|
68
|
+
}
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const timedOut = error instanceof Error && error.name === 'AbortError';
|
|
73
|
+
return { ok: false, formattedText: '', error: timedOut ? `timeout after ${BRAIN_HUB_TIMEOUT_MS}ms` : String(error) };
|
|
45
74
|
}
|
|
46
|
-
return data;
|
|
47
75
|
}
|
|
48
76
|
}
|