vigthoria-cli 1.8.7 → 1.8.9

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.
@@ -49,9 +49,28 @@ const session_js_1 = require("../utils/session.js");
49
49
  const bridge_client_js_1 = require("../utils/bridge-client.js");
50
50
  const workspace_stream_js_1 = require("../utils/workspace-stream.js");
51
51
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
52
- const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '3900000';
52
+ const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
53
+ if (!rawValue) {
54
+ return 0;
55
+ }
56
+ const parsed = Number.parseInt(rawValue, 10);
57
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
58
+ })();
59
+ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
60
+ const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
61
+ if (!rawValue) {
62
+ return 0;
63
+ }
64
+ const parsed = Number.parseInt(rawValue, 10);
65
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
66
+ })();
67
+ const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
68
+ const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS;
69
+ if (!rawValue) {
70
+ return 0;
71
+ }
53
72
  const parsed = Number.parseInt(rawValue, 10);
54
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 3900000;
73
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
55
74
  })();
56
75
  class ChatCommand {
57
76
  config;
@@ -122,13 +141,9 @@ class ChatCommand {
122
141
  return this.getDefaultChatModel();
123
142
  }
124
143
  isLegacyAgentFallbackAllowed() {
125
- // CLI always has local file access, so fallback to local agent loop
126
- // is safe and should be the default when V3 agent is unreachable or
127
- // rejects the request (e.g. context too large).
128
- if (process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '0') {
129
- return false;
130
- }
131
- return true;
144
+ // Hosted V3 Agent Mode must fail loudly by default so clients do not silently drop into legacy mode.
145
+ // Operators can still opt in during emergency local debugging.
146
+ return process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '1';
132
147
  }
133
148
  resolveAgentExecutionPolicy(prompt) {
134
149
  const explicitModel = this.modelExplicitlySelected;
@@ -1624,7 +1639,7 @@ class ChatCommand {
1624
1639
  clientSurface: 'cli',
1625
1640
  localMachineCapable: true,
1626
1641
  agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
1627
- agentIdleTimeoutMs: 90000,
1642
+ agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
1628
1643
  model: routingPolicy.selectedModel,
1629
1644
  requestedModel: this.currentModel,
1630
1645
  agentExecutionPolicy: routingPolicy,
@@ -1634,11 +1649,11 @@ class ChatCommand {
1634
1649
  ...runtimeContext,
1635
1650
  onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
1636
1651
  });
1637
- const response = await (rescueEligible
1652
+ const response = await (rescueEligible && DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS > 0
1638
1653
  ? Promise.race([
1639
1654
  workflowPromise,
1640
1655
  new Promise((_, reject) => {
1641
- setTimeout(() => reject(new Error('V3_SAAS_SOFT_TIMEOUT')), 240000);
1656
+ setTimeout(() => reject(new Error('V3_SAAS_SOFT_TIMEOUT')), DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS);
1642
1657
  }),
1643
1658
  ])
1644
1659
  : workflowPromise);
@@ -1758,13 +1773,16 @@ class ChatCommand {
1758
1773
  spinner.stop();
1759
1774
  }
1760
1775
  this.logger.warn('Falling back to legacy CLI agent loop');
1761
- this.logger.debug(`V3 agent workflow unavailable: ${error.message}`);
1776
+ this.logger.debug(`V3 agent workflow unavailable: ${(0, api_js_1.sanitizeUserFacingErrorText)(error.message || '')}`);
1762
1777
  return false;
1763
1778
  }
1764
1779
  if (spinner) {
1765
1780
  spinner.stop();
1766
1781
  }
1767
- const errorMessage = `Agent mode requires the V3 workflow and will not fall back to the legacy CLI loop. ${error.message}`;
1782
+ const safeDetail = (0, api_js_1.sanitizeUserFacingErrorText)(error.message || '');
1783
+ const errorMessage = safeDetail
1784
+ ? `Agent mode is unavailable right now. ${safeDetail}`
1785
+ : 'Agent mode is unavailable right now. Please retry shortly or run vigthoria login if the issue persists.';
1768
1786
  this.logger.error(errorMessage);
1769
1787
  this.messages.push({ role: 'assistant', content: errorMessage });
1770
1788
  if (this.jsonOutput) {
@@ -186,6 +186,8 @@ export interface VigthoriUser {
186
186
  adminAccess: boolean;
187
187
  };
188
188
  }
189
+ export declare function sanitizeUserFacingErrorText(input: string): string;
190
+ export declare function describeUpstreamStatus(status: number): string;
189
191
  export declare class APIClient {
190
192
  private client;
191
193
  private modelRouterClient;
package/dist/utils/api.js CHANGED
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.APIClient = exports.CLIError = void 0;
11
11
  exports.classifyError = classifyError;
12
12
  exports.formatCLIError = formatCLIError;
13
+ exports.sanitizeUserFacingErrorText = sanitizeUserFacingErrorText;
14
+ exports.describeUpstreamStatus = describeUpstreamStatus;
13
15
  const axios_1 = __importDefault(require("axios"));
14
16
  const crypto_1 = require("crypto");
15
17
  const fs_1 = __importDefault(require("fs"));
@@ -90,20 +92,65 @@ function formatCLIError(err) {
90
92
  }
91
93
  }
92
94
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
93
- const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '1200000';
95
+ const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
96
+ if (!rawValue) {
97
+ // No total timeout by default for long-running SSE agent workflows.
98
+ return 0;
99
+ }
94
100
  const parsed = Number.parseInt(rawValue, 10);
95
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 1200000;
101
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
96
102
  })();
97
103
  const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
98
- const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
104
+ const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
105
+ if (!rawValue) {
106
+ // Keep stream open indefinitely unless user configures an idle limit.
107
+ return 0;
108
+ }
99
109
  const parsed = Number.parseInt(rawValue, 10);
100
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
110
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
101
111
  })();
102
112
  const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
103
- const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '300000';
113
+ const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
114
+ if (!rawValue) {
115
+ // BMAD/operator flows can be long-running; do not cap by default.
116
+ return 0;
117
+ }
104
118
  const parsed = Number.parseInt(rawValue, 10);
105
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
119
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
106
120
  })();
121
+ // Sanitize an upstream error string before exposing it to the end user.
122
+ // Strips URLs, IPs:ports, absolute server paths, and bare hostnames so the
123
+ // CLI never reveals internal infrastructure to remote users.
124
+ function sanitizeUserFacingErrorText(input) {
125
+ if (!input)
126
+ return '';
127
+ let out = String(input);
128
+ out = out.replace(/https?:\/\/[^\s'"<>)]+/gi, '[redacted-url]');
129
+ out = out.replace(/\b\d{1,3}(?:\.\d{1,3}){3}(?::\d+)?\b/g, '[redacted-host]');
130
+ out = out.replace(/\b(?:localhost|127\.0\.0\.1)(?::\d+)?\b/gi, '[redacted-host]');
131
+ out = out.replace(/\b[a-z0-9.-]+\.vigthoria\.io\b/gi, '[redacted-host]');
132
+ out = out.replace(/(?:[A-Za-z]:)?[\\/](?:var|opt|tmp|home|root|etc|usr)[\\/][^\s'"<>)]*/gi, '[redacted-path]');
133
+ out = out.replace(/\{\s*"detail"\s*:\s*"[^"]*"\s*\}/g, '');
134
+ out = out.replace(/\s+/g, ' ').trim();
135
+ if (out.length > 160)
136
+ out = out.slice(0, 160) + '...';
137
+ return out;
138
+ }
139
+ function describeUpstreamStatus(status) {
140
+ if (status === 401 || status === 403)
141
+ return 'Authentication failed. Please run vigthoria login.';
142
+ if (status === 404)
143
+ return 'Requested service endpoint was not found.';
144
+ if (status === 408 || status === 504)
145
+ return 'Upstream service timed out.';
146
+ if (status === 429)
147
+ return 'Rate limit reached. Please retry shortly.';
148
+ if (status >= 500)
149
+ return 'Upstream service is temporarily unavailable.';
150
+ if (status >= 400)
151
+ return 'Request was rejected by the service.';
152
+ return 'Unexpected response from service.';
153
+ }
107
154
  class APIClient {
108
155
  client;
109
156
  modelRouterClient;
@@ -793,7 +840,7 @@ class APIClient {
793
840
  });
794
841
  if (!response.ok) {
795
842
  const errorText = await response.text().catch(() => '');
796
- throw new Error(`Template preview proof ${response.status}: ${errorText.slice(0, 200)}`);
843
+ throw new Error(`Template preview proof ${response.status}: ${describeUpstreamStatus(response.status)}`);
797
844
  }
798
845
  const payload = await response.json();
799
846
  const modes = payload?.modes || {};
@@ -1734,7 +1781,7 @@ menu {
1734
1781
  });
1735
1782
  if (!response.ok) {
1736
1783
  const errorText = await response.text().catch(() => '');
1737
- throw new Error(`MCP context update ${response.status}: ${errorText.slice(0, 200)}`);
1784
+ throw new Error(`MCP context update ${response.status}: ${describeUpstreamStatus(response.status)}`);
1738
1785
  }
1739
1786
  return {
1740
1787
  ...executionContext,
@@ -1765,7 +1812,7 @@ menu {
1765
1812
  });
1766
1813
  if (!createResponse.ok) {
1767
1814
  const errorText = await createResponse.text().catch(() => '');
1768
- throw new Error(`MCP context create ${createResponse.status}: ${errorText.slice(0, 200)}`);
1815
+ throw new Error(`MCP context create ${createResponse.status}: ${describeUpstreamStatus(createResponse.status)}`);
1769
1816
  }
1770
1817
  const payload = await createResponse.json();
1771
1818
  const mcpContextId = String(payload.contextId || '').trim();
@@ -2122,6 +2169,11 @@ menu {
2122
2169
  if (!looksLikeFrontendTask) {
2123
2170
  return;
2124
2171
  }
2172
+ // Skip motion/scroll enhancements for games — they use canvas, not section-based layouts
2173
+ const looksLikeGame = /\bgame\b|arcade|pac.?man|tetris|platformer|roguelike|breakout|pong|snake\s+game|tower\s+defense|playable/i.test(prompt);
2174
+ if (looksLikeGame) {
2175
+ return;
2176
+ }
2125
2177
  const htmlPath = path_1.default.join(rootPath, 'index.html');
2126
2178
  if (!fs_1.default.existsSync(htmlPath)) {
2127
2179
  return;
@@ -2568,7 +2620,7 @@ document.addEventListener('DOMContentLoaded', () => {
2568
2620
  let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
2569
2621
  let serverWorkspaceRoot = null;
2570
2622
  const streamedFiles = {};
2571
- const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
2623
+ const idleTimeoutMs = context.agentIdleTimeoutMs ?? DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
2572
2624
  while (true) {
2573
2625
  let chunk;
2574
2626
  try {
@@ -2706,7 +2758,7 @@ document.addEventListener('DOMContentLoaded', () => {
2706
2758
  partial: true,
2707
2759
  };
2708
2760
  }
2709
- throw new Error(event.message || 'V3 agent returned an error');
2761
+ throw new Error(`V3 agent: ${sanitizeUserFacingErrorText(event.message || '') || 'returned an error'}`);
2710
2762
  }
2711
2763
  if (event.type === 'complete' || event.type === 'message') {
2712
2764
  final = event;
@@ -2724,15 +2776,13 @@ document.addEventListener('DOMContentLoaded', () => {
2724
2776
  }
2725
2777
  async runV3AgentWorkflow(message, context = {}) {
2726
2778
  const executionContext = await this.bindExecutionContext(context);
2727
- const baseTimeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
2779
+ const baseTimeoutMs = executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS;
2728
2780
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
2729
2781
  const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
2730
2782
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
2731
2783
  const preferLocalV3 = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|responsive|animated|create the required project files and write them to the workspace)/i.test(message)
2732
2784
  && context.localMachineCapable !== false;
2733
- const rescueEligibleSaaS = preferLocalV3
2734
- && /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
2735
- const timeoutMs = rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
2785
+ const timeoutMs = baseTimeoutMs;
2736
2786
  const maxAttempts = preferLocalV3 ? 2 : 1;
2737
2787
  let lastErrors = [];
2738
2788
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
@@ -2766,12 +2816,12 @@ document.addEventListener('DOMContentLoaded', () => {
2766
2816
  };
2767
2817
  for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
2768
2818
  const controller = new AbortController();
2769
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2819
+ const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
2770
2820
  try {
2771
2821
  const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
2772
2822
  if (!response.ok) {
2773
2823
  const errorText = await response.text().catch(() => '');
2774
- throw new Error(`V3 agent ${response.status}: ${errorText.slice(0, 200)}`);
2824
+ throw new Error(`V3 agent ${response.status}: ${describeUpstreamStatus(response.status)}`);
2775
2825
  }
2776
2826
  const data = await this.collectV3AgentStream(response, requestExecutionContext);
2777
2827
  // Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
@@ -2798,7 +2848,7 @@ document.addEventListener('DOMContentLoaded', () => {
2798
2848
  stream: true,
2799
2849
  };
2800
2850
  const continueController = new AbortController();
2801
- const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
2851
+ const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
2802
2852
  try {
2803
2853
  const continueHeaders = await this.getV3AgentHeaders();
2804
2854
  const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
@@ -2816,7 +2866,9 @@ document.addEventListener('DOMContentLoaded', () => {
2816
2866
  break; // Fall through to normal completion with partial data
2817
2867
  }
2818
2868
  finally {
2819
- clearTimeout(continueTimeoutId);
2869
+ if (continueTimeoutId) {
2870
+ clearTimeout(continueTimeoutId);
2871
+ }
2820
2872
  }
2821
2873
  }
2822
2874
  // Use the final continuation data for workspace recovery
@@ -2873,7 +2925,9 @@ document.addEventListener('DOMContentLoaded', () => {
2873
2925
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
2874
2926
  }
2875
2927
  finally {
2876
- clearTimeout(timeoutId);
2928
+ if (timeoutId) {
2929
+ clearTimeout(timeoutId);
2930
+ }
2877
2931
  }
2878
2932
  }
2879
2933
  lastErrors = errors;
@@ -2940,7 +2994,7 @@ document.addEventListener('DOMContentLoaded', () => {
2940
2994
  }
2941
2995
  async runOperatorWorkflow(message, context = {}) {
2942
2996
  const executionContext = await this.bindExecutionContext(context);
2943
- const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
2997
+ const timeoutMs = context.operatorTimeoutMs ?? DEFAULT_OPERATOR_TIMEOUT_MS;
2944
2998
  const errors = [];
2945
2999
  const authToken = this.config.get('authToken');
2946
3000
  // Collect a lightweight workspace file listing so the operator can
@@ -2949,7 +3003,7 @@ document.addEventListener('DOMContentLoaded', () => {
2949
3003
  const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
2950
3004
  for (const baseUrl of this.getOperatorBaseUrls()) {
2951
3005
  const controller = new AbortController();
2952
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
3006
+ const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
2953
3007
  try {
2954
3008
  const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
2955
3009
  method: 'POST',
@@ -2994,7 +3048,7 @@ document.addEventListener('DOMContentLoaded', () => {
2994
3048
  });
2995
3049
  if (!response.ok) {
2996
3050
  const errorText = await response.text().catch(() => '');
2997
- throw new Error(`Operator stream ${response.status}: ${errorText.slice(0, 200)}`);
3051
+ throw new Error(`Operator stream ${response.status}: ${describeUpstreamStatus(response.status)}`);
2998
3052
  }
2999
3053
  if (!response.body || typeof response.body.getReader !== 'function') {
3000
3054
  const fallbackData = await response.json();
@@ -3096,7 +3150,9 @@ document.addEventListener('DOMContentLoaded', () => {
3096
3150
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3097
3151
  }
3098
3152
  finally {
3099
- clearTimeout(timeoutId);
3153
+ if (timeoutId) {
3154
+ clearTimeout(timeoutId);
3155
+ }
3100
3156
  }
3101
3157
  }
3102
3158
  throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.8.7",
3
+ "version": "1.8.9",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [