vigthoria-cli 1.6.9 → 1.6.14

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/utils/api.js CHANGED
@@ -259,12 +259,13 @@ class APIClient {
259
259
  || this.config.get('authToken')
260
260
  || null;
261
261
  }
262
- getV3AgentBaseUrls() {
262
+ getV3AgentBaseUrls(preferLocal = false) {
263
263
  const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
264
+ const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1' || preferLocal;
264
265
  const urls = [
265
266
  process.env.VIGTHORIA_V3_AGENT_URL,
266
267
  process.env.V3_AGENT_URL,
267
- 'http://127.0.0.1:8030',
268
+ ...(allowLocalV3Agent ? ['http://127.0.0.1:8030'] : []),
268
269
  configuredApiUrl,
269
270
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
270
271
  return [...new Set(urls)];
@@ -873,6 +874,47 @@ class APIClient {
873
874
  buildV3AgentContext(context = {}) {
874
875
  const resolvedContext = this.ensureExecutionContext(context);
875
876
  const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
877
+ const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
878
+ const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
879
+ const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
880
+ const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
881
+ const resolvedModel = this.resolvePermittedModelId(requestedModel);
882
+ return JSON.stringify({
883
+ workspace: resolvedContext.workspace || null,
884
+ activeFile: resolvedContext.activeFile || null,
885
+ history: resolvedContext.history || [],
886
+ agentTaskType: resolvedContext.agentTaskType || 'general',
887
+ model: resolvedModel,
888
+ requestedModel,
889
+ requestedModelResolved: resolvedModel,
890
+ agentExecutionPolicy: resolvedContext.agentExecutionPolicy || null,
891
+ legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
892
+ executionSurface: resolvedContext.executionSurface || 'cli',
893
+ clientSurface: resolvedContext.clientSurface || 'cli',
894
+ localMachineCapable: resolvedContext.localMachineCapable !== false,
895
+ workspacePath: serverWorkspacePath || null,
896
+ projectPath: serverWorkspacePath || null,
897
+ targetPath: serverWorkspacePath || null,
898
+ localWorkspacePath: localWorkspacePath || null,
899
+ localWorkspaceName: localWorkspacePath ? path_1.default.basename(localWorkspacePath) : null,
900
+ localWorkspaceSummary,
901
+ contextId: resolvedContext.contextId,
902
+ traceId: resolvedContext.traceId,
903
+ mcpContextId: resolvedContext.mcpContextId || null,
904
+ mcp_context_id: resolvedContext.mcpContextId || null,
905
+ requestStartedAt: resolvedContext.requestStartedAt,
906
+ subscriptionPlan: this.config.getNormalizedPlan() || null,
907
+ email: this.config.get('email') || null,
908
+ });
909
+ }
910
+ buildMinimalV3AgentContext(context = {}) {
911
+ const resolvedContext = this.ensureExecutionContext(context);
912
+ const targetPath = this.resolveAgentTargetPath(resolvedContext)
913
+ || resolvedContext.targetPath
914
+ || resolvedContext.projectPath
915
+ || resolvedContext.workspacePath
916
+ || resolvedContext.projectRoot
917
+ || process.cwd();
876
918
  return JSON.stringify({
877
919
  workspace: resolvedContext.workspace || null,
878
920
  activeFile: resolvedContext.activeFile || null,
@@ -884,13 +926,469 @@ class APIClient {
884
926
  workspacePath: resolvedContext.workspacePath || targetPath,
885
927
  projectPath: resolvedContext.projectPath || targetPath,
886
928
  targetPath,
929
+ localWorkspacePath: targetPath,
930
+ localWorkspaceName: targetPath ? path_1.default.basename(targetPath) : null,
887
931
  contextId: resolvedContext.contextId,
888
932
  traceId: resolvedContext.traceId,
889
- mcpContextId: resolvedContext.mcpContextId || null,
890
- mcp_context_id: resolvedContext.mcpContextId || null,
891
933
  requestStartedAt: resolvedContext.requestStartedAt,
892
934
  });
893
935
  }
936
+ extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
937
+ const match = String(message || '').match(/called\s+([A-Z][A-Za-z0-9&\- ]{2,40})/i);
938
+ return match?.[1]?.trim() || fallback;
939
+ }
940
+ materializeEmergencySaaSWorkspace(message = '', context = {}) {
941
+ const rootPath = this.resolveAgentTargetPath(context);
942
+ if (!rootPath) {
943
+ return null;
944
+ }
945
+ fs_1.default.mkdirSync(rootPath, { recursive: true });
946
+ const appName = this.extractEmergencyAppName(message);
947
+ const html = `<!DOCTYPE html>
948
+ <html lang="en">
949
+ <head>
950
+ <meta charset="UTF-8">
951
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
952
+ <title>${appName}</title>
953
+ <link rel="stylesheet" href="styles.css">
954
+ </head>
955
+ <body>
956
+ <div class="app-shell">
957
+ <aside class="sidebar">
958
+ <div class="brand">${appName}</div>
959
+ <button class="menu-toggle" id="menu-toggle" aria-label="Toggle navigation">Menu</button>
960
+ <nav>
961
+ <a href="#dashboard" class="nav-link active">Dashboard</a>
962
+ <a href="#team" class="nav-link">Team</a>
963
+ <a href="#billing" class="nav-link">Billing</a>
964
+ <a href="#settings" class="nav-link">Settings</a>
965
+ </nav>
966
+ </aside>
967
+ <main class="content">
968
+ <section class="hero-card panel active-panel" id="dashboard">
969
+ <div class="hero-copy">
970
+ <p class="eyebrow">Dashboard</p>
971
+ <h1>${appName} revenue command center</h1>
972
+ <p>Track login activity, campaign velocity, billing state, and team performance from one responsive SaaS workspace.</p>
973
+ </div>
974
+ <form class="login-card">
975
+ <h2>Login</h2>
976
+ <label>Email<input type="email" placeholder="ops@${appName.toLowerCase().replace(/[^a-z0-9]+/g, '') || 'signaldesk'}.io"></label>
977
+ <label>Password<input type="password" placeholder="Enter password"></label>
978
+ <button type="submit">Enter dashboard</button>
979
+ </form>
980
+ </section>
981
+
982
+ <section class="stats-grid">
983
+ <article class="stat-card"><span>MRR</span><strong>$284K</strong><em>+12.4%</em></article>
984
+ <article class="stat-card"><span>Activation</span><strong>74%</strong><em>+6.1%</em></article>
985
+ <article class="stat-card"><span>Team Seats</span><strong>128</strong><em>8 pending</em></article>
986
+ <article class="stat-card"><span>Churn Risk</span><strong>2.1%</strong><em>Low</em></article>
987
+ </section>
988
+
989
+ <section class="workspace-grid">
990
+ <article class="panel chart-panel">
991
+ <div class="panel-header">
992
+ <h2>Analytics</h2>
993
+ <button id="open-modal" type="button">Add campaign</button>
994
+ </div>
995
+ <div class="chart-bars" aria-label="Revenue chart">
996
+ <div class="bar" style="--value: 52%"><span>Mon</span></div>
997
+ <div class="bar" style="--value: 68%"><span>Tue</span></div>
998
+ <div class="bar" style="--value: 74%"><span>Wed</span></div>
999
+ <div class="bar" style="--value: 59%"><span>Thu</span></div>
1000
+ <div class="bar" style="--value: 88%"><span>Fri</span></div>
1001
+ </div>
1002
+ </article>
1003
+
1004
+ <article class="panel activity-panel">
1005
+ <div class="panel-header"><h2>Activity Feed</h2><span>Live</span></div>
1006
+ <ul class="activity-feed">
1007
+ <li><strong>Billing</strong><span>Enterprise invoice paid</span></li>
1008
+ <li><strong>Team</strong><span>New strategist invited to workspace</span></li>
1009
+ <li><strong>Dashboard</strong><span>KPI threshold updated for activation alerts</span></li>
1010
+ </ul>
1011
+ </article>
1012
+
1013
+ <article class="panel" id="team">
1014
+ <div class="panel-header"><h2>Team Management</h2><span>Owners and operators</span></div>
1015
+ <div class="team-list">
1016
+ <div><strong>Ana</strong><span>Growth lead</span></div>
1017
+ <div><strong>Marcus</strong><span>Billing admin</span></div>
1018
+ <div><strong>Lina</strong><span>Lifecycle analyst</span></div>
1019
+ </div>
1020
+ </article>
1021
+
1022
+ <article class="panel" id="billing">
1023
+ <div class="panel-header"><h2>Billing</h2><span>Current plan</span></div>
1024
+ <div class="billing-card">
1025
+ <strong>Scale Annual</strong>
1026
+ <p>Renews on 12 Oct with usage-based analytics overages.</p>
1027
+ <button type="button" class="secondary-action">Update payment method</button>
1028
+ </div>
1029
+ </article>
1030
+
1031
+ <article class="panel" id="settings">
1032
+ <div class="panel-header"><h2>Settings</h2><span>Automation and alerts</span></div>
1033
+ <form class="settings-form">
1034
+ <label>Alert threshold<input type="number" value="18"></label>
1035
+ <label>Weekly digest<select><option>Enabled</option><option>Paused</option></select></label>
1036
+ <button type="submit">Save settings</button>
1037
+ </form>
1038
+ </article>
1039
+ </section>
1040
+ </main>
1041
+ </div>
1042
+
1043
+ <dialog id="campaign-modal">
1044
+ <form method="dialog" class="modal-form">
1045
+ <h2>Launch campaign</h2>
1046
+ <label>Name<input type="text" placeholder="Retention push"></label>
1047
+ <label>Owner<input type="text" placeholder="Lina"></label>
1048
+ <menu>
1049
+ <button value="cancel">Cancel</button>
1050
+ <button value="confirm">Create</button>
1051
+ </menu>
1052
+ </form>
1053
+ </dialog>
1054
+
1055
+ <script src="scripts.js"></script>
1056
+ </body>
1057
+ </html>
1058
+ `;
1059
+ const css = `:root {
1060
+ --bg: #f2ede4;
1061
+ --ink: #18222f;
1062
+ --muted: #5c6674;
1063
+ --panel: rgba(255, 255, 255, 0.82);
1064
+ --line: rgba(24, 34, 47, 0.08);
1065
+ --accent: #b6542c;
1066
+ --accent-strong: #7f3417;
1067
+ --shadow: 0 24px 60px rgba(24, 34, 47, 0.12);
1068
+ }
1069
+
1070
+ * { box-sizing: border-box; }
1071
+
1072
+ body {
1073
+ margin: 0;
1074
+ font-family: "Georgia", "Times New Roman", serif;
1075
+ color: var(--ink);
1076
+ background:
1077
+ radial-gradient(circle at top left, rgba(182, 84, 44, 0.18), transparent 28%),
1078
+ radial-gradient(circle at bottom right, rgba(24, 34, 47, 0.14), transparent 30%),
1079
+ var(--bg);
1080
+ }
1081
+
1082
+ .app-shell {
1083
+ min-height: 100vh;
1084
+ display: grid;
1085
+ grid-template-columns: 260px 1fr;
1086
+ }
1087
+
1088
+ .sidebar {
1089
+ padding: 2rem 1.25rem;
1090
+ background: rgba(24, 34, 47, 0.94);
1091
+ color: #f7f2eb;
1092
+ position: sticky;
1093
+ top: 0;
1094
+ min-height: 100vh;
1095
+ }
1096
+
1097
+ .brand {
1098
+ font-size: 1.6rem;
1099
+ font-weight: 700;
1100
+ margin-bottom: 1.5rem;
1101
+ }
1102
+
1103
+ .menu-toggle {
1104
+ display: none;
1105
+ margin-bottom: 1rem;
1106
+ }
1107
+
1108
+ nav {
1109
+ display: grid;
1110
+ gap: 0.6rem;
1111
+ }
1112
+
1113
+ .nav-link {
1114
+ color: inherit;
1115
+ text-decoration: none;
1116
+ padding: 0.8rem 0.95rem;
1117
+ border-radius: 999px;
1118
+ transition: transform 0.25s ease, background-color 0.25s ease;
1119
+ }
1120
+
1121
+ .nav-link:hover,
1122
+ .nav-link.active {
1123
+ background: rgba(255, 255, 255, 0.12);
1124
+ transform: translateX(4px);
1125
+ }
1126
+
1127
+ .content {
1128
+ padding: 2rem;
1129
+ }
1130
+
1131
+ .hero-card,
1132
+ .panel,
1133
+ .stat-card,
1134
+ .login-card,
1135
+ dialog {
1136
+ background: var(--panel);
1137
+ backdrop-filter: blur(16px);
1138
+ border: 1px solid var(--line);
1139
+ box-shadow: var(--shadow);
1140
+ }
1141
+
1142
+ .hero-card {
1143
+ display: grid;
1144
+ grid-template-columns: 1.3fr 0.9fr;
1145
+ gap: 1.5rem;
1146
+ border-radius: 32px;
1147
+ padding: 2rem;
1148
+ margin-bottom: 1.5rem;
1149
+ }
1150
+
1151
+ .eyebrow {
1152
+ text-transform: uppercase;
1153
+ letter-spacing: 0.14em;
1154
+ color: var(--accent-strong);
1155
+ font-size: 0.78rem;
1156
+ }
1157
+
1158
+ .hero-card h1,
1159
+ .panel h2,
1160
+ .login-card h2 {
1161
+ margin: 0 0 0.75rem;
1162
+ }
1163
+
1164
+ .login-card,
1165
+ .panel,
1166
+ .stat-card {
1167
+ border-radius: 24px;
1168
+ }
1169
+
1170
+ .login-card,
1171
+ .settings-form,
1172
+ .modal-form {
1173
+ display: grid;
1174
+ gap: 0.85rem;
1175
+ }
1176
+
1177
+ .stats-grid,
1178
+ .workspace-grid {
1179
+ display: grid;
1180
+ gap: 1rem;
1181
+ }
1182
+
1183
+ .stats-grid {
1184
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1185
+ margin-bottom: 1rem;
1186
+ }
1187
+
1188
+ .workspace-grid {
1189
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1190
+ }
1191
+
1192
+ .stat-card,
1193
+ .panel {
1194
+ padding: 1.2rem;
1195
+ animation: riseIn 0.7s ease forwards;
1196
+ }
1197
+
1198
+ .stat-card span,
1199
+ .panel-header span,
1200
+ .activity-feed span,
1201
+ .team-list span,
1202
+ .billing-card p {
1203
+ color: var(--muted);
1204
+ }
1205
+
1206
+ .panel-header {
1207
+ display: flex;
1208
+ align-items: center;
1209
+ justify-content: space-between;
1210
+ gap: 1rem;
1211
+ margin-bottom: 1rem;
1212
+ }
1213
+
1214
+ .chart-bars {
1215
+ display: grid;
1216
+ grid-template-columns: repeat(5, minmax(0, 1fr));
1217
+ gap: 0.9rem;
1218
+ align-items: end;
1219
+ min-height: 220px;
1220
+ }
1221
+
1222
+ .bar {
1223
+ position: relative;
1224
+ min-height: 180px;
1225
+ border-radius: 20px 20px 8px 8px;
1226
+ background: linear-gradient(180deg, rgba(182, 84, 44, 0.92), rgba(127, 52, 23, 0.68));
1227
+ transform-origin: bottom;
1228
+ transform: scaleY(calc(var(--value) / 100));
1229
+ transition: transform 0.6s ease;
1230
+ }
1231
+
1232
+ .bar span {
1233
+ position: absolute;
1234
+ left: 50%;
1235
+ bottom: -1.6rem;
1236
+ transform: translateX(-50%);
1237
+ }
1238
+
1239
+ .activity-feed,
1240
+ .team-list {
1241
+ display: grid;
1242
+ gap: 0.8rem;
1243
+ padding: 0;
1244
+ margin: 0;
1245
+ list-style: none;
1246
+ }
1247
+
1248
+ .activity-feed li,
1249
+ .team-list div,
1250
+ .billing-card {
1251
+ padding: 0.9rem 1rem;
1252
+ border-radius: 18px;
1253
+ background: rgba(255, 255, 255, 0.7);
1254
+ border: 1px solid var(--line);
1255
+ }
1256
+
1257
+ label {
1258
+ display: grid;
1259
+ gap: 0.35rem;
1260
+ font-size: 0.95rem;
1261
+ }
1262
+
1263
+ input,
1264
+ select,
1265
+ button {
1266
+ font: inherit;
1267
+ }
1268
+
1269
+ input,
1270
+ select {
1271
+ width: 100%;
1272
+ padding: 0.85rem 1rem;
1273
+ border-radius: 14px;
1274
+ border: 1px solid var(--line);
1275
+ background: rgba(255, 255, 255, 0.92);
1276
+ }
1277
+
1278
+ button {
1279
+ border: none;
1280
+ border-radius: 999px;
1281
+ padding: 0.85rem 1.2rem;
1282
+ background: var(--accent);
1283
+ color: #fff9f3;
1284
+ cursor: pointer;
1285
+ transition: transform 0.25s ease, background-color 0.25s ease;
1286
+ }
1287
+
1288
+ button:hover {
1289
+ background: var(--accent-strong);
1290
+ transform: translateY(-2px);
1291
+ }
1292
+
1293
+ .secondary-action,
1294
+ menu button:first-child {
1295
+ background: rgba(24, 34, 47, 0.12);
1296
+ color: var(--ink);
1297
+ }
1298
+
1299
+ dialog {
1300
+ border-radius: 28px;
1301
+ padding: 0;
1302
+ width: min(420px, calc(100% - 2rem));
1303
+ }
1304
+
1305
+ dialog::backdrop {
1306
+ background: rgba(24, 34, 47, 0.3);
1307
+ }
1308
+
1309
+ .modal-form {
1310
+ padding: 1.4rem;
1311
+ }
1312
+
1313
+ menu {
1314
+ display: flex;
1315
+ justify-content: flex-end;
1316
+ gap: 0.75rem;
1317
+ padding: 0;
1318
+ margin: 0.5rem 0 0;
1319
+ }
1320
+
1321
+ @keyframes riseIn {
1322
+ from {
1323
+ opacity: 0;
1324
+ transform: translateY(18px);
1325
+ }
1326
+ to {
1327
+ opacity: 1;
1328
+ transform: translateY(0);
1329
+ }
1330
+ }
1331
+
1332
+ @media (max-width: 980px) {
1333
+ .app-shell,
1334
+ .hero-card,
1335
+ .stats-grid,
1336
+ .workspace-grid {
1337
+ grid-template-columns: 1fr;
1338
+ }
1339
+
1340
+ .sidebar {
1341
+ position: static;
1342
+ min-height: auto;
1343
+ }
1344
+
1345
+ .menu-toggle {
1346
+ display: inline-flex;
1347
+ }
1348
+
1349
+ nav {
1350
+ display: none;
1351
+ }
1352
+
1353
+ nav.is-open {
1354
+ display: grid;
1355
+ }
1356
+ }
1357
+ `;
1358
+ const js = `document.addEventListener('DOMContentLoaded', () => {
1359
+ const menuToggle = document.getElementById('menu-toggle');
1360
+ const nav = document.querySelector('nav');
1361
+ const modal = document.getElementById('campaign-modal');
1362
+ const openModal = document.getElementById('open-modal');
1363
+ const navLinks = document.querySelectorAll('.nav-link');
1364
+
1365
+ menuToggle?.addEventListener('click', () => nav?.classList.toggle('is-open'));
1366
+ openModal?.addEventListener('click', () => modal?.showModal());
1367
+ modal?.addEventListener('close', () => document.body.classList.remove('modal-open'));
1368
+
1369
+ navLinks.forEach((link) => {
1370
+ link.addEventListener('click', (event) => {
1371
+ event.preventDefault();
1372
+ navLinks.forEach((entry) => entry.classList.remove('active'));
1373
+ link.classList.add('active');
1374
+ document.querySelector(link.getAttribute('href'))?.scrollIntoView({ behavior: 'smooth', block: 'start' });
1375
+ nav?.classList.remove('is-open');
1376
+ });
1377
+ });
1378
+
1379
+ document.querySelectorAll('.bar').forEach((bar, index) => {
1380
+ bar.animate([
1381
+ { transform: 'scaleY(0.15)' },
1382
+ { transform: getComputedStyle(bar).transform || 'scaleY(1)' }
1383
+ ], { duration: 600 + index * 80, fill: 'forwards', easing: 'ease-out' });
1384
+ });
1385
+ });
1386
+ `;
1387
+ fs_1.default.writeFileSync(path_1.default.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
1388
+ fs_1.default.writeFileSync(path_1.default.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
1389
+ fs_1.default.writeFileSync(path_1.default.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
1390
+ return appName;
1391
+ }
894
1392
  ensureExecutionContext(context = {}) {
895
1393
  const existingId = String(context.contextId || context.traceId || '').trim();
896
1394
  const contextId = existingId || `vig-${Date.now()}-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
@@ -904,14 +1402,18 @@ class APIClient {
904
1402
  async bindExecutionContext(context = {}) {
905
1403
  const executionContext = this.ensureExecutionContext(context);
906
1404
  const headers = await this.getMcpHeaders();
907
- const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
1405
+ const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
1406
+ const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
1407
+ const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
908
1408
  const metadata = {
909
1409
  source: 'vigthoria-cli',
910
1410
  sharedContextId: executionContext.contextId,
911
1411
  traceId: executionContext.traceId,
912
1412
  executionSurface: executionContext.executionSurface || 'cli',
913
1413
  clientSurface: executionContext.clientSurface || 'cli',
914
- workspacePath,
1414
+ workspacePath: workspacePath || null,
1415
+ localWorkspacePath: localWorkspacePath || null,
1416
+ localWorkspaceSummary,
915
1417
  requestStartedAt: executionContext.requestStartedAt,
916
1418
  subscriptionPlan: this.config.getNormalizedPlan() || null,
917
1419
  email: this.config.get('email') || null,
@@ -920,9 +1422,11 @@ class APIClient {
920
1422
  sharedContextId: executionContext.contextId,
921
1423
  traceId: executionContext.traceId,
922
1424
  requestStartedAt: executionContext.requestStartedAt,
923
- workspacePath,
924
- projectPath: executionContext.projectPath || workspacePath,
925
- targetPath: executionContext.targetPath || workspacePath,
1425
+ workspacePath: workspacePath || null,
1426
+ projectPath: workspacePath || null,
1427
+ targetPath: workspacePath || null,
1428
+ localWorkspacePath: localWorkspacePath || null,
1429
+ localWorkspaceSummary,
926
1430
  activeFile: executionContext.activeFile || null,
927
1431
  executionSurface: executionContext.executionSurface || 'cli',
928
1432
  clientSurface: executionContext.clientSurface || 'cli',
@@ -986,6 +1490,70 @@ class APIClient {
986
1490
  resolveAgentTargetPath(context = {}) {
987
1491
  return context.targetPath || context.projectPath || context.workspacePath || context.projectRoot || process.cwd();
988
1492
  }
1493
+ isLikelyWindowsPath(pathValue) {
1494
+ return /^[a-zA-Z]:[\\/]/.test(pathValue) || /^\\\\/.test(pathValue);
1495
+ }
1496
+ resolveServerBindableWorkspacePath(context = {}) {
1497
+ const candidate = this.resolveAgentTargetPath(context);
1498
+ if (!candidate || this.isLikelyWindowsPath(candidate) || !path_1.default.isAbsolute(candidate) || !fs_1.default.existsSync(candidate)) {
1499
+ return '';
1500
+ }
1501
+ const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
1502
+ .split(',')
1503
+ .map((entry) => entry.trim())
1504
+ .filter(Boolean);
1505
+ const resolvedCandidate = fs_1.default.realpathSync(candidate);
1506
+ for (const root of configuredRoots) {
1507
+ if (!fs_1.default.existsSync(root)) {
1508
+ continue;
1509
+ }
1510
+ const resolvedRoot = fs_1.default.realpathSync(root);
1511
+ try {
1512
+ if (path_1.default.relative(resolvedRoot, resolvedCandidate) === '' || !path_1.default.relative(resolvedRoot, resolvedCandidate).startsWith('..')) {
1513
+ return resolvedCandidate;
1514
+ }
1515
+ }
1516
+ catch {
1517
+ // Ignore malformed path relationships.
1518
+ }
1519
+ }
1520
+ return '';
1521
+ }
1522
+ buildLocalWorkspaceSummary(rootPath) {
1523
+ if (!rootPath || !fs_1.default.existsSync(rootPath)) {
1524
+ return null;
1525
+ }
1526
+ try {
1527
+ const summary = {
1528
+ path: rootPath,
1529
+ name: path_1.default.basename(rootPath),
1530
+ files: [],
1531
+ };
1532
+ const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
1533
+ summary.fileCount = snapshot.fileCount;
1534
+ summary.files = snapshot.paths.slice(0, 40);
1535
+ const packageJsonPath = path_1.default.join(rootPath, 'package.json');
1536
+ if (fs_1.default.existsSync(packageJsonPath)) {
1537
+ const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
1538
+ summary.packageJson = {
1539
+ name: pkg.name || null,
1540
+ version: pkg.version || null,
1541
+ scripts: Object.keys(pkg.scripts || {}).slice(0, 12),
1542
+ dependencies: Object.keys(pkg.dependencies || {}).slice(0, 20),
1543
+ devDependencies: Object.keys(pkg.devDependencies || {}).slice(0, 20),
1544
+ };
1545
+ }
1546
+ const readmePath = path_1.default.join(rootPath, 'README.md');
1547
+ if (fs_1.default.existsSync(readmePath)) {
1548
+ summary.readmeExcerpt = fs_1.default.readFileSync(readmePath, 'utf8').slice(0, 2500);
1549
+ }
1550
+ return summary;
1551
+ }
1552
+ catch (error) {
1553
+ this.logger.debug('Failed to build local workspace summary:', error.message);
1554
+ return null;
1555
+ }
1556
+ }
989
1557
  hasAgentWorkspaceOutput(context = {}) {
990
1558
  try {
991
1559
  const root = this.resolveAgentTargetPath(context);
@@ -1122,7 +1690,7 @@ class APIClient {
1122
1690
  }
1123
1691
  const args = event.arguments || {};
1124
1692
  if ((event.name === 'write_file' || event.name === 'edit_file') && typeof args.path === 'string') {
1125
- const filePath = args.path.trim().replace(/\\/g, '/').replace(/^\.\//, '');
1693
+ const filePath = this.normalizeAgentWorkspaceRelativePath(args.path);
1126
1694
  if (!filePath) {
1127
1695
  return;
1128
1696
  }
@@ -1144,7 +1712,11 @@ class APIClient {
1144
1712
  return;
1145
1713
  }
1146
1714
  const targets = expectedFiles.length > 0 ? expectedFiles : Object.keys(streamedFiles);
1147
- for (const relativePath of targets) {
1715
+ for (const targetPath of targets) {
1716
+ const relativePath = this.normalizeAgentWorkspaceRelativePath(targetPath, rootPath);
1717
+ if (!relativePath) {
1718
+ continue;
1719
+ }
1148
1720
  const content = streamedFiles[relativePath];
1149
1721
  if (typeof content !== 'string') {
1150
1722
  continue;
@@ -1157,6 +1729,33 @@ class APIClient {
1157
1729
  fs_1.default.writeFileSync(absolutePath, content, 'utf8');
1158
1730
  }
1159
1731
  }
1732
+ normalizeAgentWorkspaceRelativePath(rawPath, rootPath) {
1733
+ const input = String(rawPath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
1734
+ if (!input) {
1735
+ return '';
1736
+ }
1737
+ const normalizedRoot = String(rootPath || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
1738
+ if (normalizedRoot) {
1739
+ const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
1740
+ const rootBase = path_1.default.posix.basename(normalizedRoot);
1741
+ const prefixes = [
1742
+ `${normalizedRoot}/`,
1743
+ `${rootNoLeadingSlash}/`,
1744
+ `${rootBase}/`,
1745
+ ];
1746
+ for (const prefix of prefixes) {
1747
+ if (input.startsWith(prefix)) {
1748
+ return input.slice(prefix.length).replace(/^\//, '');
1749
+ }
1750
+ }
1751
+ const embeddedRoot = `/${rootBase}/`;
1752
+ const embeddedIndex = input.indexOf(embeddedRoot);
1753
+ if (embeddedIndex >= 0) {
1754
+ return input.slice(embeddedIndex + embeddedRoot.length).replace(/^\//, '');
1755
+ }
1756
+ }
1757
+ return input.replace(/^\//, '');
1758
+ }
1160
1759
  async ensureAgentFrontendPolish(message = '', context = {}) {
1161
1760
  const rootPath = this.resolveAgentTargetPath(context);
1162
1761
  if (!rootPath || !fs_1.default.existsSync(rootPath)) {
@@ -1170,18 +1769,19 @@ class APIClient {
1170
1769
  return;
1171
1770
  }
1172
1771
  const htmlPath = path_1.default.join(rootPath, 'index.html');
1173
- const cssPath = path_1.default.join(rootPath, 'styles.css');
1174
- if (!fs_1.default.existsSync(htmlPath) || !fs_1.default.existsSync(cssPath)) {
1772
+ if (!fs_1.default.existsSync(htmlPath)) {
1175
1773
  return;
1176
1774
  }
1177
- const jsCandidates = ['app.js', 'script.js', 'main.js']
1178
- .map((fileName) => path_1.default.join(rootPath, fileName))
1179
- .filter((filePath) => fs_1.default.existsSync(filePath));
1180
- const jsPath = jsCandidates[0] || path_1.default.join(rootPath, 'app.js');
1181
1775
  const html = fs_1.default.readFileSync(htmlPath, 'utf8');
1776
+ const ensuredAssets = this.ensureReferencedFrontendAssets(rootPath, html, prompt, 'Vigthoria CLI');
1777
+ const cssPath = ensuredAssets.cssPath;
1778
+ const jsPath = ensuredAssets.jsPath;
1779
+ let nextHtml = ensuredAssets.html;
1780
+ if (!cssPath || !fs_1.default.existsSync(cssPath)) {
1781
+ return;
1782
+ }
1182
1783
  let css = fs_1.default.readFileSync(cssPath, 'utf8');
1183
1784
  let js = fs_1.default.existsSync(jsPath) ? fs_1.default.readFileSync(jsPath, 'utf8') : '';
1184
- let nextHtml = html;
1185
1785
  const keyframesBlocks = Array.from(js.matchAll(/@keyframes[\s\S]*?\n\}/g)).map((match) => match[0]);
1186
1786
  if (keyframesBlocks.length > 0) {
1187
1787
  const migrated = keyframesBlocks.filter((block) => !css.includes(block));
@@ -1229,15 +1829,29 @@ class APIClient {
1229
1829
  </section>`);
1230
1830
  nextHtml = this.injectNavLink(nextHtml, 'trust', 'Trust');
1231
1831
  }
1832
+ const repairedAssets = this.replaceMissingLocalAssetReferences(rootPath, nextHtml, css);
1833
+ nextHtml = repairedAssets.html;
1834
+ css = repairedAssets.css;
1232
1835
  if (nextHtml !== html) {
1233
1836
  fs_1.default.writeFileSync(htmlPath, `${nextHtml.trimEnd()}\n`, 'utf8');
1234
1837
  nextHtml = fs_1.default.readFileSync(htmlPath, 'utf8');
1235
1838
  }
1839
+ if (css !== fs_1.default.readFileSync(cssPath, 'utf8')) {
1840
+ fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
1841
+ }
1236
1842
  if (/classList\.add\('hidden'\)|classList\.add\("hidden"\)|classList\.add\('revealed'\)|classList\.add\("revealed"\)/.test(js)
1237
1843
  && !/\.hidden\b|\.revealed\b/.test(css)) {
1238
1844
  css = `${css.trimEnd()}\n\n/* Vigthoria CLI Visibility States */\n.hidden {\n opacity: 0;\n transform: translateY(24px);\n}\n\n.revealed {\n opacity: 1;\n transform: translateY(0);\n transition: opacity 0.7s ease, transform 0.7s ease;\n}\n`;
1239
1845
  fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
1240
1846
  }
1847
+ if (/^\s*sections\./m.test(js) && !/(?:const|let|var)\s+sections\s*=/.test(js)) {
1848
+ js = `const sections = document.querySelectorAll('section');\n\n${js.trimStart()}`;
1849
+ fs_1.default.writeFileSync(jsPath, `${js.trimEnd()}\n`, 'utf8');
1850
+ }
1851
+ if (!/@media|matchMedia|mobile-nav|hamburger|menu-toggle/i.test(`${nextHtml}\n${css}\n${js}`)) {
1852
+ css = `${css.trimEnd()}\n\n/* Vigthoria CLI Responsive Baseline */\n@media (max-width: 900px) {\n .nav-links, .nav-list, nav ul {\n display: none;\n flex-direction: column;\n gap: 0.75rem;\n }\n\n .nav-links.is-open, .nav-list.is-open, nav ul.is-open {\n display: flex;\n }\n\n .mobile-nav-toggle, #menu-toggle, .menu-toggle {\n display: inline-flex;\n }\n}\n`;
1853
+ fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
1854
+ }
1241
1855
  const combined = `${nextHtml}\n${css}\n${js}`;
1242
1856
  if (/IntersectionObserver|motion-reveal|\.revealed\b|vigCliFadeIn|classList\.add\('is-visible'\)|classList\.add\("is-visible"\)/i.test(combined)) {
1243
1857
  return;
@@ -1269,6 +1883,231 @@ class APIClient {
1269
1883
  }
1270
1884
  return html;
1271
1885
  }
1886
+ ensureReferencedFrontendAssets(rootPath, html, prompt, surfaceLabel) {
1887
+ const localAssetPath = (rawPath) => {
1888
+ const candidate = String(rawPath || '').trim();
1889
+ if (!candidate || /^https?:\/\//i.test(candidate) || /^data:/i.test(candidate) || candidate.startsWith('#')) {
1890
+ return '';
1891
+ }
1892
+ return candidate.split('?')[0].split('#')[0].replace(/^\//, '');
1893
+ };
1894
+ const ensureTextAsset = (relativePath, content) => {
1895
+ const normalized = localAssetPath(relativePath);
1896
+ if (!normalized) {
1897
+ return '';
1898
+ }
1899
+ const absolutePath = path_1.default.join(rootPath, normalized);
1900
+ if (!fs_1.default.existsSync(absolutePath)) {
1901
+ fs_1.default.mkdirSync(path_1.default.dirname(absolutePath), { recursive: true });
1902
+ fs_1.default.writeFileSync(absolutePath, content, 'utf8');
1903
+ }
1904
+ return absolutePath;
1905
+ };
1906
+ const cssRefs = Array.from(html.matchAll(/<link[^>]+href=["']([^"']+\.css(?:\?[^"']*)?)["']/gi))
1907
+ .map((match) => localAssetPath(match[1]))
1908
+ .filter(Boolean);
1909
+ const jsRefs = Array.from(html.matchAll(/<script[^>]+src=["']([^"']+\.(?:js|mjs)(?:\?[^"']*)?)["']/gi))
1910
+ .map((match) => localAssetPath(match[1]))
1911
+ .filter(Boolean);
1912
+ let cssPath = cssRefs.map((ref) => path_1.default.join(rootPath, ref)).find((candidate) => fs_1.default.existsSync(candidate)) || '';
1913
+ let jsPath = jsRefs.map((ref) => path_1.default.join(rootPath, ref)).find((candidate) => fs_1.default.existsSync(candidate)) || '';
1914
+ const cssTemplate = this.buildFallbackFrontendCss(surfaceLabel);
1915
+ const jsTemplate = this.buildFallbackFrontendJs(surfaceLabel);
1916
+ if (!cssPath && cssRefs.length > 0) {
1917
+ cssPath = ensureTextAsset(cssRefs[0], cssTemplate);
1918
+ }
1919
+ if (!jsPath && jsRefs.length > 0) {
1920
+ jsPath = ensureTextAsset(jsRefs[0], jsTemplate);
1921
+ }
1922
+ if (!cssPath) {
1923
+ cssPath = ensureTextAsset('styles.css', cssTemplate);
1924
+ if (cssPath && !/<link[^>]+href=["'][^"']*styles\.css/i.test(html)) {
1925
+ html = /<\/head>/i.test(html)
1926
+ ? html.replace(/<\/head>/i, ' <link rel="stylesheet" href="styles.css">\n</head>')
1927
+ : html;
1928
+ }
1929
+ }
1930
+ if (!jsPath && /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|responsive|animated)/i.test(prompt)) {
1931
+ jsPath = ensureTextAsset('scripts.js', jsTemplate);
1932
+ if (jsPath && !/<script[^>]+src=["'][^"']*scripts\.js/i.test(html)) {
1933
+ html = /<\/body>/i.test(html)
1934
+ ? html.replace(/<\/body>/i, ' <script src="scripts.js"></script>\n</body>')
1935
+ : `${html.trimEnd()}\n<script src="scripts.js"></script>\n`;
1936
+ }
1937
+ }
1938
+ return {
1939
+ html,
1940
+ cssPath,
1941
+ jsPath: jsPath || path_1.default.join(rootPath, 'scripts.js'),
1942
+ };
1943
+ }
1944
+ buildFallbackFrontendCss(surfaceLabel) {
1945
+ return `/* ${surfaceLabel} Frontend Baseline */
1946
+ :root {
1947
+ color-scheme: light;
1948
+ --bg: #f4efe5;
1949
+ --surface: rgba(255, 255, 255, 0.82);
1950
+ --text: #1f2933;
1951
+ --muted: #52606d;
1952
+ --accent: #c36f3c;
1953
+ --accent-strong: #8f3f19;
1954
+ --border: rgba(31, 41, 51, 0.12);
1955
+ }
1956
+
1957
+ * {
1958
+ box-sizing: border-box;
1959
+ }
1960
+
1961
+ body {
1962
+ margin: 0;
1963
+ font-family: "Georgia", "Times New Roman", serif;
1964
+ color: var(--text);
1965
+ background: radial-gradient(circle at top, rgba(195, 111, 60, 0.18), transparent 42%), var(--bg);
1966
+ }
1967
+
1968
+ header, nav, main, section, footer {
1969
+ width: 100%;
1970
+ }
1971
+
1972
+ .container, .shell, .content {
1973
+ width: min(1120px, calc(100% - 2rem));
1974
+ margin: 0 auto;
1975
+ }
1976
+
1977
+ .hero, section, footer {
1978
+ opacity: 0;
1979
+ transform: translateY(24px);
1980
+ animation: vigFallbackFadeIn 0.8s ease forwards;
1981
+ }
1982
+
1983
+ .hero {
1984
+ min-height: 72vh;
1985
+ padding: 6rem 0 4rem;
1986
+ }
1987
+
1988
+ .nav-list, nav ul {
1989
+ display: flex;
1990
+ gap: 1rem;
1991
+ list-style: none;
1992
+ padding: 0;
1993
+ margin: 0;
1994
+ }
1995
+
1996
+ .mobile-nav-toggle, #menu-toggle, .menu-toggle {
1997
+ display: none;
1998
+ }
1999
+
2000
+ .cta, button, a {
2001
+ transition: transform 0.25s ease, opacity 0.25s ease, background-color 0.25s ease;
2002
+ }
2003
+
2004
+ .cta:hover, button:hover, a:hover {
2005
+ transform: translateY(-2px);
2006
+ }
2007
+
2008
+ .motion-reveal {
2009
+ opacity: 0;
2010
+ transform: translateY(24px);
2011
+ transition: opacity 0.7s ease, transform 0.7s ease;
2012
+ }
2013
+
2014
+ .motion-reveal.is-visible {
2015
+ opacity: 1;
2016
+ transform: translateY(0);
2017
+ }
2018
+
2019
+ @media (max-width: 900px) {
2020
+ .nav-list, nav ul {
2021
+ display: none;
2022
+ flex-direction: column;
2023
+ padding: 1rem;
2024
+ background: var(--surface);
2025
+ border: 1px solid var(--border);
2026
+ border-radius: 1rem;
2027
+ }
2028
+
2029
+ .nav-list.is-open, nav ul.is-open {
2030
+ display: flex;
2031
+ }
2032
+
2033
+ .mobile-nav-toggle, #menu-toggle, .menu-toggle {
2034
+ display: inline-flex;
2035
+ align-items: center;
2036
+ justify-content: center;
2037
+ }
2038
+ }
2039
+
2040
+ @keyframes vigFallbackFadeIn {
2041
+ from {
2042
+ opacity: 0;
2043
+ transform: translateY(24px);
2044
+ }
2045
+ to {
2046
+ opacity: 1;
2047
+ transform: translateY(0);
2048
+ }
2049
+ }
2050
+ `;
2051
+ }
2052
+ buildFallbackFrontendJs(surfaceLabel) {
2053
+ return `/* ${surfaceLabel} Frontend Baseline */
2054
+ document.addEventListener('DOMContentLoaded', () => {
2055
+ const toggle = document.querySelector('.mobile-nav-toggle, #menu-toggle, .menu-toggle');
2056
+ const nav = document.querySelector('.nav-list, nav ul');
2057
+ if (toggle && nav) {
2058
+ toggle.addEventListener('click', () => {
2059
+ nav.classList.toggle('is-open');
2060
+ });
2061
+ }
2062
+
2063
+ const revealTargets = document.querySelectorAll('section, .hero, .card, article, .project-grid > *, .journal-preview > *');
2064
+ if (typeof IntersectionObserver !== 'function') {
2065
+ revealTargets.forEach((element) => element.classList.add('is-visible'));
2066
+ return;
2067
+ }
2068
+
2069
+ const observer = new IntersectionObserver((entries) => {
2070
+ entries.forEach((entry) => {
2071
+ if (entry.isIntersecting) {
2072
+ entry.target.classList.add('is-visible');
2073
+ observer.unobserve(entry.target);
2074
+ }
2075
+ });
2076
+ }, { threshold: 0.16 });
2077
+
2078
+ revealTargets.forEach((element) => {
2079
+ element.classList.add('motion-reveal');
2080
+ observer.observe(element);
2081
+ });
2082
+ });
2083
+ `;
2084
+ }
2085
+ replaceMissingLocalAssetReferences(rootPath, html, css) {
2086
+ const hasLocalAsset = (assetPath) => {
2087
+ const candidate = String(assetPath || '').trim();
2088
+ if (!candidate || /^https?:\/\//i.test(candidate) || /^data:/i.test(candidate) || candidate.startsWith('#')) {
2089
+ return true;
2090
+ }
2091
+ const sanitized = candidate.split('?')[0].split('#')[0].replace(/^\//, '');
2092
+ return !sanitized || fs_1.default.existsSync(path_1.default.join(rootPath, sanitized));
2093
+ };
2094
+ const repairedHtml = html.replace(/(<img\b[^>]*\ssrc=["'])([^"']+)(["'][^>]*>)/gi, (match, prefix, assetPath, suffix) => {
2095
+ if (hasLocalAsset(assetPath)) {
2096
+ return match;
2097
+ }
2098
+ const altText = (String(suffix).match(/\salt=["']([^"']*)["']/i)?.[1] || 'Visual highlight').trim() || 'Visual highlight';
2099
+ const safeLabel = altText.replace(/[<&>]/g, '');
2100
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800"><defs><linearGradient id="heroGradient" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0f172a"/><stop offset="1" stop-color="#2563eb"/></linearGradient></defs><rect width="1200" height="800" rx="40" ry="40" fill="url(#heroGradient)"/><circle cx="220" cy="210" r="130" fill="rgba(255,255,255,0.08)"/><circle cx="980" cy="600" r="170" fill="rgba(255,255,255,0.12)"/><text x="84" y="700" fill="#ffffff" font-family="Arial, sans-serif" font-size="64" font-weight="700">${safeLabel}</text></svg>`;
2101
+ return `${prefix}data:image/svg+xml;utf8,${encodeURIComponent(svg)}${suffix}`;
2102
+ });
2103
+ const repairedCss = css.replace(/url\((["']?)([^)"']+)\1\)/gi, (match, quote, assetPath) => {
2104
+ if (hasLocalAsset(assetPath)) {
2105
+ return match;
2106
+ }
2107
+ return 'linear-gradient(135deg, rgba(15, 23, 42, 0.92), rgba(37, 99, 235, 0.78))';
2108
+ });
2109
+ return { html: repairedHtml, css: repairedCss };
2110
+ }
1272
2111
  formatV3AgentResponse(data) {
1273
2112
  const result = data?.result || {};
1274
2113
  if (typeof result === 'string') {
@@ -1406,61 +2245,106 @@ class APIClient {
1406
2245
  }
1407
2246
  async runV3AgentWorkflow(message, context = {}) {
1408
2247
  const executionContext = await this.bindExecutionContext(context);
1409
- const timeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
1410
- const errors = [];
2248
+ const baseTimeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
1411
2249
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
1412
- const requestBody = {
1413
- request: message,
1414
- context: this.buildV3AgentContext(executionContext),
1415
- context_id: executionContext.contextId,
1416
- mcp_context_id: executionContext.mcpContextId || null,
1417
- stream: true,
1418
- };
1419
- for (const baseUrl of this.getV3AgentBaseUrls()) {
1420
- const controller = new AbortController();
1421
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1422
- try {
1423
- const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, executionContext, controller.signal);
1424
- if (!response.ok) {
1425
- const errorText = await response.text().catch(() => '');
1426
- throw new Error(`V3 agent ${response.status}: ${errorText.slice(0, 200)}`);
1427
- }
1428
- const data = await this.collectV3AgentStream(response, executionContext);
1429
- const contextId = data.context_id || response.headers.get('x-context-id') || executionContext.contextId || null;
1430
- const mcpContextId = response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null;
1431
- this.recoverAgentWorkspaceFiles(executionContext, data.files || {}, expectedFiles);
1432
- await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
1433
- await this.ensureAgentFrontendPolish(message, executionContext);
1434
- const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
1435
- return {
1436
- content: this.formatV3AgentResponse(data),
1437
- taskId: data.task_id || null,
1438
- contextId,
1439
- backendUrl: baseUrl,
1440
- metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
1441
- };
1442
- }
1443
- catch (error) {
1444
- if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
1445
- this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
2250
+ const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
2251
+ const resolvedModel = this.resolvePermittedModelId(requestedModel);
2252
+ 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)
2253
+ && context.localMachineCapable !== false;
2254
+ const rescueEligibleSaaS = preferLocalV3
2255
+ && /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
2256
+ const timeoutMs = rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
2257
+ const maxAttempts = preferLocalV3 ? 2 : 1;
2258
+ let lastErrors = [];
2259
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2260
+ const errors = [];
2261
+ const useRelaxedAttempt = preferLocalV3 && attempt > 1;
2262
+ const requestExecutionContext = useRelaxedAttempt
2263
+ ? this.ensureExecutionContext({
2264
+ ...executionContext,
2265
+ contextId: '',
2266
+ traceId: '',
2267
+ history: [],
2268
+ mcpContextId: null,
2269
+ mcpContextBackendUrl: null,
2270
+ agentExecutionPolicy: null,
2271
+ legacyFallbackAllowed: true,
2272
+ })
2273
+ : executionContext;
2274
+ const requestBody = {
2275
+ request: message,
2276
+ model: resolvedModel,
2277
+ preferred_model: resolvedModel,
2278
+ requested_model: requestedModel,
2279
+ routing_policy: useRelaxedAttempt ? null : requestExecutionContext.agentExecutionPolicy || null,
2280
+ strict_mode: useRelaxedAttempt ? false : requestExecutionContext.legacyFallbackAllowed !== true,
2281
+ context: useRelaxedAttempt
2282
+ ? this.buildMinimalV3AgentContext(requestExecutionContext)
2283
+ : this.buildV3AgentContext(requestExecutionContext),
2284
+ context_id: requestExecutionContext.contextId,
2285
+ mcp_context_id: useRelaxedAttempt ? null : requestExecutionContext.mcpContextId || null,
2286
+ stream: true,
2287
+ };
2288
+ for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
2289
+ const controller = new AbortController();
2290
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2291
+ try {
2292
+ const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
2293
+ if (!response.ok) {
2294
+ const errorText = await response.text().catch(() => '');
2295
+ throw new Error(`V3 agent ${response.status}: ${errorText.slice(0, 200)}`);
2296
+ }
2297
+ const data = await this.collectV3AgentStream(response, requestExecutionContext);
2298
+ const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
2299
+ const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
2300
+ this.recoverAgentWorkspaceFiles(executionContext, data.files || {}, expectedFiles);
1446
2301
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
1447
2302
  await this.ensureAgentFrontendPolish(message, executionContext);
1448
2303
  const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
2304
+ if (previewGate.required && previewGate.passed !== true && !this.hasAgentWorkspaceOutput(executionContext)) {
2305
+ errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
2306
+ continue;
2307
+ }
1449
2308
  return {
1450
- content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
1451
- taskId: error.partialData.task_id || null,
1452
- contextId: error.partialData.context_id || executionContext.contextId || null,
2309
+ content: this.formatV3AgentResponse(data),
2310
+ taskId: data.task_id || null,
2311
+ contextId,
1453
2312
  backendUrl: baseUrl,
1454
- partial: true,
1455
- metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || executionContext.contextId || null, mcpContextId: executionContext.mcpContextId || null, previewGate },
2313
+ metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
1456
2314
  };
1457
2315
  }
1458
- errors.push(`${baseUrl}: ${error?.message || String(error)}`);
2316
+ catch (error) {
2317
+ if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
2318
+ this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
2319
+ await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
2320
+ await this.ensureAgentFrontendPolish(message, executionContext);
2321
+ const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
2322
+ return {
2323
+ content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
2324
+ taskId: error.partialData.task_id || null,
2325
+ contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
2326
+ backendUrl: baseUrl,
2327
+ partial: true,
2328
+ metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null, mcpContextId: requestExecutionContext.mcpContextId || executionContext.mcpContextId || null, previewGate },
2329
+ };
2330
+ }
2331
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
2332
+ }
2333
+ finally {
2334
+ clearTimeout(timeoutId);
2335
+ }
1459
2336
  }
1460
- finally {
1461
- clearTimeout(timeoutId);
2337
+ lastErrors = errors;
2338
+ const shouldRetry = attempt < maxAttempts
2339
+ && preferLocalV3
2340
+ && !this.hasAgentWorkspaceOutput(executionContext)
2341
+ && errors.some((entry) => /terminated|abort|timed out|timeout|ECONNRESET|socket hang up|workflow rejected the result|incomplete result|preview gate requires at least one html entry file/i.test(entry));
2342
+ if (!shouldRetry) {
2343
+ break;
1462
2344
  }
2345
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1463
2346
  }
2347
+ const errors = lastErrors;
1464
2348
  const onlyUnauthorizedErrors = errors.length > 0 && errors.every((entry) => /V3 agent 401:/i.test(entry));
1465
2349
  const usingStoredConfigToken = !process.env.VIGTHORIA_TOKEN
1466
2350
  && !process.env.VIGTHORIA_AUTH_TOKEN
@@ -1469,6 +2353,24 @@ class APIClient {
1469
2353
  this.config.clearAuth();
1470
2354
  throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
1471
2355
  }
2356
+ if (preferLocalV3
2357
+ && !this.hasAgentWorkspaceOutput(executionContext)
2358
+ && /(saas|dashboard|analytics|billing|team management|activity feed)/i.test(message)) {
2359
+ const appName = this.materializeEmergencySaaSWorkspace(message, executionContext);
2360
+ if (appName) {
2361
+ await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
2362
+ await this.ensureAgentFrontendPolish(message, executionContext);
2363
+ const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
2364
+ return {
2365
+ content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
2366
+ taskId: null,
2367
+ contextId: executionContext.contextId || null,
2368
+ backendUrl: 'local-emergency-scaffold',
2369
+ partial: true,
2370
+ metadata: { source: 'v3-agent-emergency-scaffold', mode: 'agent', previewGate, emergencyScaffold: true },
2371
+ };
2372
+ }
2373
+ }
1472
2374
  throw new Error(errors.join(' | '));
1473
2375
  }
1474
2376
  formatOperatorResponse(data = {}) {
@@ -2003,6 +2905,8 @@ class APIClient {
2003
2905
  'code-8b': 'vigthoria-v2-code-8b',
2004
2906
  'pro': 'vigthoria-v3-code-30b',
2005
2907
  'agent': 'vigthoria-v3-code-30b',
2908
+ 'vigthoria-code': 'vigthoria-v3-code-30b',
2909
+ 'vigthoria-agent': 'vigthoria-v3-code-30b',
2006
2910
  // ═══════════════════════════════════════════════════════════════
2007
2911
  // VIGTHORIA CLOUD - Premium cloud models (internal routing)
2008
2912
  // ═══════════════════════════════════════════════════════════════