vigthoria-cli 1.10.48 → 1.10.49
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/agent-session-menu.js +2 -8
- package/dist/commands/bridge.js +36 -9
- package/dist/commands/chat.d.ts +3 -28
- package/dist/commands/chat.js +326 -295
- package/dist/commands/fork.js +1 -1
- package/dist/commands/history.js +1 -1
- package/dist/commands/replay.js +1 -1
- package/dist/index.js +40 -214
- package/dist/utils/api.d.ts +25 -53
- package/dist/utils/api.js +300 -1443
- package/dist/utils/config.d.ts +0 -3
- package/dist/utils/config.js +2 -0
- package/dist/utils/desktop-bridge-client.d.ts +12 -0
- package/dist/utils/desktop-bridge-client.js +30 -0
- package/dist/utils/post-write-validator.js +5 -5
- package/dist/utils/tools.d.ts +0 -7
- package/dist/utils/tools.js +15 -87
- package/package.json +2 -1
- package/scripts/release/validate-no-go-gates.sh +7 -4
package/dist/utils/api.js
CHANGED
|
@@ -254,33 +254,15 @@ export function propagateError(err) {
|
|
|
254
254
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
255
255
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
256
256
|
if (!rawValue) {
|
|
257
|
-
return
|
|
257
|
+
return 300000;
|
|
258
258
|
}
|
|
259
259
|
const parsed = Number.parseInt(rawValue, 10);
|
|
260
|
-
return Number.isFinite(parsed) && parsed
|
|
260
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
|
|
261
261
|
})();
|
|
262
262
|
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
263
|
-
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
|
|
264
|
-
if (!rawValue) {
|
|
265
|
-
return 0;
|
|
266
|
-
}
|
|
267
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
268
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
269
|
-
})();
|
|
270
|
-
const DEFAULT_MCP_BIND_TIMEOUT_MS = (() => {
|
|
271
|
-
const rawValue = process.env.VIGTHORIA_MCP_BIND_TIMEOUT_MS || process.env.MCP_BIND_TIMEOUT_MS || '5000';
|
|
263
|
+
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
|
|
272
264
|
const parsed = Number.parseInt(rawValue, 10);
|
|
273
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed :
|
|
274
|
-
})();
|
|
275
|
-
const DEFAULT_WORKSPACE_SCAN_TIMEOUT_MS = (() => {
|
|
276
|
-
const rawValue = process.env.VIGTHORIA_WORKSPACE_SCAN_TIMEOUT_MS || '2500';
|
|
277
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
278
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 2500;
|
|
279
|
-
})();
|
|
280
|
-
const DEFAULT_WORKSPACE_SCAN_MAX_FILES = (() => {
|
|
281
|
-
const rawValue = process.env.VIGTHORIA_WORKSPACE_SCAN_MAX_FILES || '1800';
|
|
282
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
283
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1800;
|
|
265
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
284
266
|
})();
|
|
285
267
|
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
286
268
|
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
|
|
@@ -290,16 +272,6 @@ const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
|
290
272
|
const parsed = Number.parseInt(rawValue, 10);
|
|
291
273
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
292
274
|
})();
|
|
293
|
-
const DEFAULT_CHAT_CONNECT_TIMEOUT_MS = (() => {
|
|
294
|
-
const rawValue = process.env.VIGTHORIA_CHAT_CONNECT_TIMEOUT_MS || '20000';
|
|
295
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
296
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 20000;
|
|
297
|
-
})();
|
|
298
|
-
const DEFAULT_CHAT_IDLE_TIMEOUT_MS = (() => {
|
|
299
|
-
const rawValue = process.env.VIGTHORIA_CHAT_IDLE_TIMEOUT_MS || '120000';
|
|
300
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
301
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 120000;
|
|
302
|
-
})();
|
|
303
275
|
export class APIClient {
|
|
304
276
|
client;
|
|
305
277
|
modelRouterClient;
|
|
@@ -1143,76 +1115,13 @@ export class APIClient {
|
|
|
1143
1115
|
// Non-fatal: fall through; the health check will surface the auth error.
|
|
1144
1116
|
}
|
|
1145
1117
|
}
|
|
1146
|
-
|
|
1147
|
-
* Fast preflight for local agent loop — probes model backends in parallel
|
|
1148
|
-
* so users see a clear pass/fail before "Planning..." hangs on slow routes.
|
|
1149
|
-
*/
|
|
1150
|
-
async runChatModelPreflight(requestedModel = 'agent') {
|
|
1151
|
-
const routes = [];
|
|
1152
|
-
const probes = [
|
|
1153
|
-
{
|
|
1154
|
-
name: 'Vigthoria Coder API',
|
|
1155
|
-
run: async () => {
|
|
1156
|
-
const health = await this.getCoderHealth();
|
|
1157
|
-
return { ok: health.ok, error: health.error };
|
|
1158
|
-
},
|
|
1159
|
-
},
|
|
1160
|
-
{
|
|
1161
|
-
name: 'Vigthoria Models API',
|
|
1162
|
-
run: async () => {
|
|
1163
|
-
const health = await this.getModelsHealth();
|
|
1164
|
-
return { ok: health.ok, error: health.error };
|
|
1165
|
-
},
|
|
1166
|
-
},
|
|
1167
|
-
];
|
|
1168
|
-
const selfHostedHealth = await this.getSelfHostedHealth();
|
|
1169
|
-
if (selfHostedHealth) {
|
|
1170
|
-
probes.unshift({
|
|
1171
|
-
name: selfHostedHealth.name || 'Dedicated Models API',
|
|
1172
|
-
run: async () => ({ ok: selfHostedHealth.ok, error: selfHostedHealth.error }),
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
if (this.isSelfHostedPreferredModel(this.resolveModelId(requestedModel), requestedModel)) {
|
|
1176
|
-
// Agent/code models should also accept V3 health as a usable inference path.
|
|
1177
|
-
probes.push({
|
|
1178
|
-
name: 'V3 Agent API',
|
|
1179
|
-
run: async () => {
|
|
1180
|
-
const v3 = await this.runV3HealthCheck();
|
|
1181
|
-
return { ok: v3.healthy, error: v3.error };
|
|
1182
|
-
},
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
const results = await Promise.all(probes.map(async (probe) => {
|
|
1186
|
-
try {
|
|
1187
|
-
const result = await probe.run();
|
|
1188
|
-
routes.push({ name: probe.name, ok: result.ok, error: result.error });
|
|
1189
|
-
return result.ok;
|
|
1190
|
-
}
|
|
1191
|
-
catch (error) {
|
|
1192
|
-
const message = error.message || String(error);
|
|
1193
|
-
routes.push({ name: probe.name, ok: false, error: message });
|
|
1194
|
-
return false;
|
|
1195
|
-
}
|
|
1196
|
-
}));
|
|
1197
|
-
const healthy = results.some(Boolean);
|
|
1198
|
-
const inferenceRoutes = routes.filter((route) => route.name !== 'V3 Agent API');
|
|
1199
|
-
const firstHealthy = inferenceRoutes.find((route) => route.ok) || routes.find((route) => route.ok);
|
|
1200
|
-
return {
|
|
1201
|
-
healthy,
|
|
1202
|
-
endpoint: firstHealthy?.name || 'api.vigthoria.io',
|
|
1203
|
-
error: healthy
|
|
1204
|
-
? undefined
|
|
1205
|
-
: 'No Vigthoria model backend responded during preflight. Run `vigthoria login` and check your internet connection.',
|
|
1206
|
-
routes,
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
async runV3HealthCheck(options = {}) {
|
|
1210
|
-
await this.ensureV3ServiceKey();
|
|
1118
|
+
async runV3HealthCheck() {
|
|
1211
1119
|
const endpoints = this.getV3AgentBaseUrls(false);
|
|
1212
1120
|
const headers = await this.getV3AgentHeaders();
|
|
1213
|
-
const
|
|
1214
|
-
const probe = async (baseUrl) => {
|
|
1121
|
+
for (const baseUrl of endpoints) {
|
|
1215
1122
|
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
1123
|
+
// Use the lightweight GET /health endpoint — no LLM invocation, responds instantly.
|
|
1124
|
+
// For local 8030 the path is /health directly; for remote it's /api/v3-agent/health via proxy.
|
|
1216
1125
|
const healthUrl = /127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)
|
|
1217
1126
|
? `${baseUrl}/health`
|
|
1218
1127
|
: `${baseUrl}/api/v3-agent/health`;
|
|
@@ -1220,198 +1129,29 @@ export class APIClient {
|
|
|
1220
1129
|
const response = await fetch(healthUrl, {
|
|
1221
1130
|
method: 'GET',
|
|
1222
1131
|
headers,
|
|
1223
|
-
signal: AbortSignal.timeout(
|
|
1132
|
+
signal: AbortSignal.timeout(8000),
|
|
1224
1133
|
});
|
|
1225
1134
|
if (response.status === 401 || response.status === 403) {
|
|
1226
1135
|
return {
|
|
1227
1136
|
healthy: false,
|
|
1228
1137
|
endpoint: runUrl,
|
|
1229
|
-
error: 'V3 service rejected authentication.
|
|
1138
|
+
error: 'V3 service rejected authentication. Please re-login to refresh your token.',
|
|
1230
1139
|
};
|
|
1231
1140
|
}
|
|
1141
|
+
// 200 OK or 405 Method Not Allowed both mean V3 is reachable
|
|
1232
1142
|
if (response.ok || response.status === 405) {
|
|
1233
1143
|
return { healthy: true, endpoint: runUrl };
|
|
1234
1144
|
}
|
|
1235
|
-
if (response.status === 502 || response.status === 503 || response.status === 504) {
|
|
1236
|
-
return {
|
|
1237
|
-
healthy: false,
|
|
1238
|
-
endpoint: runUrl,
|
|
1239
|
-
error: `V3 agent proxy returned ${response.status}. The V3 Code Agent service may be restarting or overloaded.`,
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
return {
|
|
1243
|
-
healthy: false,
|
|
1244
|
-
endpoint: runUrl,
|
|
1245
|
-
error: `V3 agent health returned ${response.status}`,
|
|
1246
|
-
};
|
|
1247
1145
|
}
|
|
1248
|
-
catch
|
|
1249
|
-
|
|
1250
|
-
if (/timeout|aborted|timed out/i.test(reason)) {
|
|
1251
|
-
return {
|
|
1252
|
-
healthy: false,
|
|
1253
|
-
endpoint: runUrl,
|
|
1254
|
-
error: options.soft
|
|
1255
|
-
? 'V3 health probe timed out; continuing with agent stream.'
|
|
1256
|
-
: 'V3 agent health check timed out. The service may be busy with another run or temporarily overloaded.',
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
return {
|
|
1260
|
-
healthy: false,
|
|
1261
|
-
endpoint: runUrl,
|
|
1262
|
-
error: reason,
|
|
1263
|
-
};
|
|
1146
|
+
catch {
|
|
1147
|
+
continue;
|
|
1264
1148
|
}
|
|
1265
|
-
};
|
|
1266
|
-
const results = await Promise.all(endpoints.map((baseUrl) => probe(baseUrl)));
|
|
1267
|
-
const healthy = results.find((entry) => entry.healthy);
|
|
1268
|
-
if (healthy) {
|
|
1269
|
-
return healthy;
|
|
1270
1149
|
}
|
|
1271
|
-
|
|
1272
|
-
if (timedOut) {
|
|
1273
|
-
return timedOut;
|
|
1274
|
-
}
|
|
1275
|
-
return results[0] || {
|
|
1150
|
+
return {
|
|
1276
1151
|
healthy: false,
|
|
1277
|
-
endpoint: this.getV3AgentRunUrl(endpoints[0]
|
|
1278
|
-
error: 'V3
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
resolveChatConnectTimeoutMs(options = {}) {
|
|
1282
|
-
if (options.connectTimeoutMs && options.connectTimeoutMs > 0) {
|
|
1283
|
-
return options.connectTimeoutMs;
|
|
1284
|
-
}
|
|
1285
|
-
if (options.timeoutMs && options.timeoutMs > 0 && options.timeoutMs <= 60000) {
|
|
1286
|
-
return options.timeoutMs;
|
|
1287
|
-
}
|
|
1288
|
-
return DEFAULT_CHAT_CONNECT_TIMEOUT_MS;
|
|
1289
|
-
}
|
|
1290
|
-
resolveChatIdleTimeoutMs(options = {}) {
|
|
1291
|
-
if (options.idleTimeoutMs && options.idleTimeoutMs > 0) {
|
|
1292
|
-
return options.idleTimeoutMs;
|
|
1293
|
-
}
|
|
1294
|
-
if (options.timeoutMs && options.timeoutMs > 60000) {
|
|
1295
|
-
return options.timeoutMs;
|
|
1296
|
-
}
|
|
1297
|
-
return DEFAULT_CHAT_IDLE_TIMEOUT_MS;
|
|
1298
|
-
}
|
|
1299
|
-
mapPreflightEndpointToRoute(endpoint) {
|
|
1300
|
-
const normalized = String(endpoint || '').toLowerCase();
|
|
1301
|
-
if (normalized.includes('coder'))
|
|
1302
|
-
return 'coder';
|
|
1303
|
-
if (normalized.includes('models'))
|
|
1304
|
-
return 'models';
|
|
1305
|
-
if (normalized.includes('dedicated') || normalized.includes('self-hosted') || normalized.includes('selfhosted')) {
|
|
1306
|
-
return 'selfhosted';
|
|
1307
|
-
}
|
|
1308
|
-
return null;
|
|
1309
|
-
}
|
|
1310
|
-
isCanonicalCoderDuplicate() {
|
|
1311
|
-
const apiUrl = String(this.config.get('apiUrl') || '').replace(/\/$/, '').toLowerCase();
|
|
1312
|
-
return apiUrl === 'https://coder.vigthoria.io';
|
|
1313
|
-
}
|
|
1314
|
-
async consumeOpenAIStreamResponse(response, idleTimeoutMs, onDelta) {
|
|
1315
|
-
if (!response.body) {
|
|
1316
|
-
throw new Error('Streaming response had no body');
|
|
1317
|
-
}
|
|
1318
|
-
const reader = response.body.getReader();
|
|
1319
|
-
const decoder = new TextDecoder();
|
|
1320
|
-
let buffer = '';
|
|
1321
|
-
let content = '';
|
|
1322
|
-
let lastActivity = Date.now();
|
|
1323
|
-
const waitForChunk = async () => {
|
|
1324
|
-
const remainingIdle = idleTimeoutMs - (Date.now() - lastActivity);
|
|
1325
|
-
if (idleTimeoutMs > 0 && remainingIdle <= 0) {
|
|
1326
|
-
throw new Error('Model stream stalled (no output)');
|
|
1327
|
-
}
|
|
1328
|
-
const readPromise = reader.read();
|
|
1329
|
-
if (idleTimeoutMs <= 0) {
|
|
1330
|
-
return readPromise;
|
|
1331
|
-
}
|
|
1332
|
-
return Promise.race([
|
|
1333
|
-
readPromise,
|
|
1334
|
-
new Promise((_, reject) => {
|
|
1335
|
-
setTimeout(() => reject(new Error('Model stream stalled (no output)')), Math.max(1000, remainingIdle));
|
|
1336
|
-
}),
|
|
1337
|
-
]);
|
|
1152
|
+
endpoint: endpoints[0] ? this.getV3AgentRunUrl(endpoints[0]) : 'unknown',
|
|
1153
|
+
error: 'V3 service is not reachable during startup preflight. Check if V3 Code Agent is online and reachable.',
|
|
1338
1154
|
};
|
|
1339
|
-
while (true) {
|
|
1340
|
-
const { done, value } = await waitForChunk();
|
|
1341
|
-
if (done) {
|
|
1342
|
-
break;
|
|
1343
|
-
}
|
|
1344
|
-
lastActivity = Date.now();
|
|
1345
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1346
|
-
const frames = buffer.split(/\r?\n\r?\n/);
|
|
1347
|
-
buffer = frames.pop() || '';
|
|
1348
|
-
for (const frame of frames) {
|
|
1349
|
-
const dataLines = frame
|
|
1350
|
-
.split(/\r?\n/)
|
|
1351
|
-
.filter((line) => line.startsWith('data:'))
|
|
1352
|
-
.map((line) => line.slice(5).trimStart());
|
|
1353
|
-
const payload = dataLines.join('\n').trim();
|
|
1354
|
-
if (!payload || payload === '[DONE]') {
|
|
1355
|
-
continue;
|
|
1356
|
-
}
|
|
1357
|
-
try {
|
|
1358
|
-
const parsed = JSON.parse(payload);
|
|
1359
|
-
const delta = parsed?.choices?.[0]?.delta?.content
|
|
1360
|
-
|| parsed?.choices?.[0]?.message?.content
|
|
1361
|
-
|| parsed?.choices?.[0]?.text
|
|
1362
|
-
|| '';
|
|
1363
|
-
if (typeof delta === 'string' && delta) {
|
|
1364
|
-
content += delta;
|
|
1365
|
-
onDelta?.(delta);
|
|
1366
|
-
lastActivity = Date.now();
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
catch {
|
|
1370
|
-
// Ignore malformed stream frames.
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
return content.trim();
|
|
1375
|
-
}
|
|
1376
|
-
async tryStreamingModelRouterChat(baseURL, messages, resolvedModel, requestedModel, headers, options) {
|
|
1377
|
-
const connectTimeoutMs = this.resolveChatConnectTimeoutMs(options);
|
|
1378
|
-
const idleTimeoutMs = this.resolveChatIdleTimeoutMs(options);
|
|
1379
|
-
const controller = new AbortController();
|
|
1380
|
-
const connectTimer = setTimeout(() => controller.abort(), connectTimeoutMs);
|
|
1381
|
-
try {
|
|
1382
|
-
const response = await fetch(`${baseURL.replace(/\/$/, '')}/v1/chat/completions`, {
|
|
1383
|
-
method: 'POST',
|
|
1384
|
-
headers: {
|
|
1385
|
-
'Content-Type': 'application/json',
|
|
1386
|
-
...headers,
|
|
1387
|
-
},
|
|
1388
|
-
body: JSON.stringify({
|
|
1389
|
-
model: resolvedModel,
|
|
1390
|
-
messages,
|
|
1391
|
-
max_tokens: this.config.get('preferences').maxTokens,
|
|
1392
|
-
temperature: 0.7,
|
|
1393
|
-
stream: true,
|
|
1394
|
-
}),
|
|
1395
|
-
signal: controller.signal,
|
|
1396
|
-
});
|
|
1397
|
-
if (!response.ok) {
|
|
1398
|
-
return null;
|
|
1399
|
-
}
|
|
1400
|
-
clearTimeout(connectTimer);
|
|
1401
|
-
const content = await this.consumeOpenAIStreamResponse(response, idleTimeoutMs, options.onStreamDelta);
|
|
1402
|
-
if (!content) {
|
|
1403
|
-
return null;
|
|
1404
|
-
}
|
|
1405
|
-
return {
|
|
1406
|
-
id: `vigthoria-stream-${Date.now()}`,
|
|
1407
|
-
message: content,
|
|
1408
|
-
model: resolvedModel || requestedModel,
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
catch (error) {
|
|
1412
|
-
clearTimeout(connectTimer);
|
|
1413
|
-
throw error;
|
|
1414
|
-
}
|
|
1415
1155
|
}
|
|
1416
1156
|
async runV3AgentAuthPreflight(baseUrl, body, executionContext) {
|
|
1417
1157
|
const endpoint = this.getV3AgentRunUrl(baseUrl);
|
|
@@ -1670,11 +1410,10 @@ export class APIClient {
|
|
|
1670
1410
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
1671
1411
|
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
1672
1412
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1673
|
-
const
|
|
1674
|
-
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, { prompt });
|
|
1413
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
1675
1414
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
1676
1415
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
1677
|
-
const localWorkspaceName =
|
|
1416
|
+
const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
|
|
1678
1417
|
const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
|
|
1679
1418
|
const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
|
|
1680
1419
|
localWorkspacePath,
|
|
@@ -1723,50 +1462,6 @@ export class APIClient {
|
|
|
1723
1462
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
1724
1463
|
subscriptionPlan: this.config.getNormalizedPlan() || null,
|
|
1725
1464
|
email: this.config.get('email') || null,
|
|
1726
|
-
rawPrompt: prompt || null,
|
|
1727
|
-
promptFocusTerms: prompt ? this.extractPromptFocusTerms(prompt) : [],
|
|
1728
|
-
vigthoriaBrain: resolvedContext.vigthoriaBrain
|
|
1729
|
-
? this.trimVigthoriaBrainForTransport(resolvedContext.vigthoriaBrain)
|
|
1730
|
-
: null,
|
|
1731
|
-
clientToolExecution: resolvedContext.clientToolExecution === true
|
|
1732
|
-
|| (resolvedContext.localMachineCapable !== false
|
|
1733
|
-
&& (resolvedContext.executionSurface === 'cli' || resolvedContext.clientSurface === 'cli')
|
|
1734
|
-
&& !!localWorkspacePath
|
|
1735
|
-
&& !serverWorkspacePath),
|
|
1736
|
-
approvalLevel: resolvedContext.autoApprove === true ? 'auto' : (resolvedContext.approvalLevel || 'confirm'),
|
|
1737
|
-
allowedCommands: Array.isArray(resolvedContext.allowedCommands) ? resolvedContext.allowedCommands : [],
|
|
1738
|
-
clientToolPathRules: (resolvedContext.clientToolExecution === true || (resolvedContext.localMachineCapable !== false
|
|
1739
|
-
&& (resolvedContext.executionSurface === 'cli' || resolvedContext.clientSurface === 'cli')
|
|
1740
|
-
&& !!localWorkspacePath
|
|
1741
|
-
&& !serverWorkspacePath))
|
|
1742
|
-
? [
|
|
1743
|
-
'Client-side read tools execute on the user real machine via the CLI.',
|
|
1744
|
-
'Use paths relative to the workspace root only.',
|
|
1745
|
-
'Never prefix tool paths with workspace/ or vigthoria://workspace/.',
|
|
1746
|
-
`The workspace root folder name is "${localWorkspaceName || 'project'}". Do not repeat it in paths.`,
|
|
1747
|
-
'Examples: ".", "Vigthoria-dominion/README.md", "vigthoria-dominion-win/".',
|
|
1748
|
-
].join(' ')
|
|
1749
|
-
: null,
|
|
1750
|
-
executionHints: {
|
|
1751
|
-
...(resolvedContext.agentTaskType === 'analysis'
|
|
1752
|
-
? {
|
|
1753
|
-
max_iterations: Number.parseInt(process.env.VIGTHORIA_ANALYSIS_MAX_ITERATIONS || '15', 10) || 15,
|
|
1754
|
-
task_kind: 'analysis',
|
|
1755
|
-
requires_file_changes: false,
|
|
1756
|
-
analysis_guidance: this.buildAnalysisGuidance(Array.isArray(localWorkspaceSummary?.promptFocusDirectories)
|
|
1757
|
-
? localWorkspaceSummary.promptFocusDirectories
|
|
1758
|
-
: [], Array.isArray(localWorkspaceSummary?.mandatoryReadPaths)
|
|
1759
|
-
? localWorkspaceSummary.mandatoryReadPaths
|
|
1760
|
-
: []),
|
|
1761
|
-
}
|
|
1762
|
-
: {}),
|
|
1763
|
-
...(['implementation', 'game-build', 'web-build', 'build', 'fix'].includes(String(resolvedContext.agentTaskType || '').toLowerCase())
|
|
1764
|
-
? {
|
|
1765
|
-
requires_file_changes: true,
|
|
1766
|
-
task_kind: String(resolvedContext.agentTaskType || 'implementation').toLowerCase(),
|
|
1767
|
-
}
|
|
1768
|
-
: {}),
|
|
1769
|
-
},
|
|
1770
1465
|
};
|
|
1771
1466
|
return this.compactV3Context(payload);
|
|
1772
1467
|
}
|
|
@@ -1794,15 +1489,9 @@ export class APIClient {
|
|
|
1794
1489
|
const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
|
|
1795
1490
|
const budget = LIMIT - overhead - 512; // reserve a little headroom
|
|
1796
1491
|
if (budget > 0) {
|
|
1797
|
-
const focusTerms = Array.isArray(payload.promptFocusTerms) ? payload.promptFocusTerms : [];
|
|
1798
|
-
const focusDirectories = Array.isArray(payload.localWorkspaceSummary?.promptFocusDirectories)
|
|
1799
|
-
? payload.localWorkspaceSummary.promptFocusDirectories
|
|
1800
|
-
: [];
|
|
1801
|
-
const sortedEntries = [...fileEntries].sort((a, b) => this.scoreWorkspacePathForHydration(b[0], focusTerms, focusDirectories)
|
|
1802
|
-
- this.scoreWorkspacePathForHydration(a[0], focusTerms, focusDirectories));
|
|
1803
1492
|
const trimmed = {};
|
|
1804
1493
|
let used = 2; // {}
|
|
1805
|
-
for (const [k, v] of
|
|
1494
|
+
for (const [k, v] of fileEntries) {
|
|
1806
1495
|
const entryLen = JSON.stringify(k).length + 1 + JSON.stringify(v).length + 1;
|
|
1807
1496
|
if (used + entryLen > budget)
|
|
1808
1497
|
break;
|
|
@@ -1869,7 +1558,7 @@ export class APIClient {
|
|
|
1869
1558
|
|| resolvedContext.projectRoot
|
|
1870
1559
|
|| process.cwd();
|
|
1871
1560
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1872
|
-
const localWorkspaceName =
|
|
1561
|
+
const localWorkspaceName = this.getDisplayWorkspaceName(targetPath);
|
|
1873
1562
|
const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
|
|
1874
1563
|
return JSON.stringify({
|
|
1875
1564
|
workspace: this.buildPublicWorkspaceDescriptor(resolvedContext.workspace, {
|
|
@@ -2368,8 +2057,7 @@ menu {
|
|
|
2368
2057
|
const headers = await this.getMcpHeaders();
|
|
2369
2058
|
const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
|
|
2370
2059
|
const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
|
|
2371
|
-
const
|
|
2372
|
-
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, { prompt });
|
|
2060
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
2373
2061
|
const metadata = {
|
|
2374
2062
|
source: 'vigthoria-cli',
|
|
2375
2063
|
sharedContextId: executionContext.contextId,
|
|
@@ -2405,7 +2093,6 @@ menu {
|
|
|
2405
2093
|
method: 'PUT',
|
|
2406
2094
|
headers,
|
|
2407
2095
|
body: JSON.stringify({ data }),
|
|
2408
|
-
signal: AbortSignal.timeout(DEFAULT_MCP_BIND_TIMEOUT_MS),
|
|
2409
2096
|
});
|
|
2410
2097
|
if (!response.ok) {
|
|
2411
2098
|
const errorText = await response.text().catch(() => '');
|
|
@@ -2437,7 +2124,6 @@ menu {
|
|
|
2437
2124
|
userId: this.config.get('userId') || this.config.get('email') || 'vigthoria-cli',
|
|
2438
2125
|
metadata,
|
|
2439
2126
|
}),
|
|
2440
|
-
signal: AbortSignal.timeout(DEFAULT_MCP_BIND_TIMEOUT_MS),
|
|
2441
2127
|
});
|
|
2442
2128
|
if (!createResponse.ok) {
|
|
2443
2129
|
const errorText = await createResponse.text().catch(() => '');
|
|
@@ -2467,7 +2153,7 @@ menu {
|
|
|
2467
2153
|
if (!candidate || !path.isAbsolute(candidate) || !fs.existsSync(candidate)) {
|
|
2468
2154
|
return '';
|
|
2469
2155
|
}
|
|
2470
|
-
const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www
|
|
2156
|
+
const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
|
|
2471
2157
|
.split(',')
|
|
2472
2158
|
.map((entry) => entry.trim())
|
|
2473
2159
|
.filter(Boolean);
|
|
@@ -2488,8 +2174,16 @@ menu {
|
|
|
2488
2174
|
}
|
|
2489
2175
|
return '';
|
|
2490
2176
|
}
|
|
2177
|
+
getDisplayWorkspaceName(workspacePath) {
|
|
2178
|
+
const cleaned = String(workspacePath || '').trim().replace(/[\\/]+$/g, '');
|
|
2179
|
+
if (!cleaned) {
|
|
2180
|
+
return null;
|
|
2181
|
+
}
|
|
2182
|
+
const parts = cleaned.split(/[\\/]+/).filter(Boolean);
|
|
2183
|
+
return parts.length > 0 ? parts[parts.length - 1] : cleaned;
|
|
2184
|
+
}
|
|
2491
2185
|
buildPublicWorkspaceDescriptor(workspace, paths = {}) {
|
|
2492
|
-
const localName =
|
|
2186
|
+
const localName = this.getDisplayWorkspaceName(paths.localWorkspacePath || '');
|
|
2493
2187
|
const isServerBindable = !!paths.serverWorkspacePath;
|
|
2494
2188
|
const descriptor = {
|
|
2495
2189
|
name: localName,
|
|
@@ -2510,9 +2204,8 @@ menu {
|
|
|
2510
2204
|
if (!runtime || typeof runtime !== 'object') {
|
|
2511
2205
|
return null;
|
|
2512
2206
|
}
|
|
2513
|
-
const localName =
|
|
2207
|
+
const localName = this.getDisplayWorkspaceName(paths.localWorkspacePath || '');
|
|
2514
2208
|
const serverBindableWorkspace = !!paths.serverWorkspacePath || runtime.serverBindableWorkspace === true;
|
|
2515
|
-
const isLocalMachine = !serverBindableWorkspace;
|
|
2516
2209
|
return {
|
|
2517
2210
|
osPlatform: runtime.osPlatform || null,
|
|
2518
2211
|
platform: runtime.platform || null,
|
|
@@ -2523,273 +2216,17 @@ menu {
|
|
|
2523
2216
|
workspaceName: localName,
|
|
2524
2217
|
workspacePath: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
|
|
2525
2218
|
cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
|
|
2526
|
-
localExecutionRequired: isLocalMachine,
|
|
2527
|
-
toolExecutionSurface: isLocalMachine ? 'cli-local' : 'server-workspace',
|
|
2528
|
-
executionGuidance: isLocalMachine
|
|
2529
|
-
? 'The user runs Vigthoria CLI on their local machine. Real files and folders live on the client filesystem. Server workspace tools only see a partial hydrated copy — read `.vigthoria-workspace-index.md` and `promptFocusDirectories` before guessing root-level README.md. Prefer localWorkspaceSummary for structure and tell the user when live local tool execution is required.'
|
|
2530
|
-
: null,
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2533
|
-
extractPromptFocusTerms(prompt) {
|
|
2534
|
-
const terms = new Set();
|
|
2535
|
-
const normalized = String(prompt || '').trim();
|
|
2536
|
-
if (!normalized) {
|
|
2537
|
-
return [];
|
|
2538
|
-
}
|
|
2539
|
-
for (const match of normalized.matchAll(/["'`]([^"'`]{2,120})["'`]/g)) {
|
|
2540
|
-
terms.add(match[1].trim());
|
|
2541
|
-
}
|
|
2542
|
-
for (const match of normalized.matchAll(/(?:project|game|folder|directory|path|repo(?:sitory)?)\s+([A-Za-z0-9][\w ._-]{2,80})/gi)) {
|
|
2543
|
-
terms.add(match[1].trim());
|
|
2544
|
-
}
|
|
2545
|
-
for (const match of normalized.matchAll(/(?:investigate|find|explore|look at|check|inspect|analyse|analyze)\s+(?:the\s+)?([A-Za-z0-9][\w ._-]{2,80}?)(?:\s+(?:folder|directory|foulder|dir|project|game))?\b/gi)) {
|
|
2546
|
-
terms.add(match[1].trim());
|
|
2547
|
-
}
|
|
2548
|
-
for (const match of normalized.matchAll(/\b([A-Z][a-z0-9]+(?:\s+[A-Z][a-z0-9]+)+)\b/g)) {
|
|
2549
|
-
const phrase = match[1].trim();
|
|
2550
|
-
if (phrase.length <= 48) {
|
|
2551
|
-
terms.add(phrase);
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
for (const match of normalized.matchAll(/\/([\w .-]{2,80})/g)) {
|
|
2555
|
-
const segment = match[1].trim();
|
|
2556
|
-
if (!/^(users|home|desktop|documents|var|www|c)$/i.test(segment)) {
|
|
2557
|
-
terms.add(segment);
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
return [...terms].filter((term) => term.length >= 3 && term.length <= 48).slice(0, 8);
|
|
2561
|
-
}
|
|
2562
|
-
buildTopLevelWorkspaceLayout(rootPath) {
|
|
2563
|
-
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2564
|
-
const layout = { directories: [], files: [] };
|
|
2565
|
-
try {
|
|
2566
|
-
for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) {
|
|
2567
|
-
if (entry.name.startsWith('.')) {
|
|
2568
|
-
continue;
|
|
2569
|
-
}
|
|
2570
|
-
if (entry.isDirectory()) {
|
|
2571
|
-
if (!skipDirs.has(entry.name)) {
|
|
2572
|
-
layout.directories.push(entry.name);
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
else if (entry.isFile()) {
|
|
2576
|
-
layout.files.push(entry.name);
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
catch {
|
|
2581
|
-
return layout;
|
|
2582
|
-
}
|
|
2583
|
-
layout.directories.sort((a, b) => a.localeCompare(b));
|
|
2584
|
-
layout.files.sort((a, b) => a.localeCompare(b));
|
|
2585
|
-
return layout;
|
|
2586
|
-
}
|
|
2587
|
-
normalizeFocusPathKey(value) {
|
|
2588
|
-
return String(value || '').toLowerCase().replace(/[\s_./\\-]+/g, '');
|
|
2589
|
-
}
|
|
2590
|
-
resolvePromptFocusDirectories(rootPath, focusTerms) {
|
|
2591
|
-
const layout = this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2592
|
-
const matches = new Set();
|
|
2593
|
-
const normalizedTerms = focusTerms.map((term) => term.toLowerCase());
|
|
2594
|
-
const normalizedTermKeys = focusTerms.map((term) => this.normalizeFocusPathKey(term));
|
|
2595
|
-
for (const dir of layout.directories) {
|
|
2596
|
-
const dirLower = dir.toLowerCase();
|
|
2597
|
-
const dirKey = this.normalizeFocusPathKey(dir);
|
|
2598
|
-
for (let index = 0; index < normalizedTerms.length; index += 1) {
|
|
2599
|
-
const term = normalizedTerms[index];
|
|
2600
|
-
const termKey = normalizedTermKeys[index];
|
|
2601
|
-
if (dirLower === term
|
|
2602
|
-
|| dirLower.includes(term)
|
|
2603
|
-
|| term.includes(dirLower)
|
|
2604
|
-
|| (termKey && (dirKey === termKey || dirKey.includes(termKey) || termKey.includes(dirKey)))) {
|
|
2605
|
-
matches.add(`${dir.replace(/\\/g, '/')}/`);
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
for (const term of focusTerms) {
|
|
2610
|
-
const candidate = path.join(rootPath, term);
|
|
2611
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
2612
|
-
matches.add(`${term.replace(/\\/g, '/')}/`);
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
return [...matches];
|
|
2616
|
-
}
|
|
2617
|
-
buildMandatoryFocusReadPaths(rootPath, focusDirectories) {
|
|
2618
|
-
const candidates = [];
|
|
2619
|
-
const pushIfExists = (relativePath) => {
|
|
2620
|
-
const normalized = relativePath.replace(/\\/g, '/');
|
|
2621
|
-
const absolutePath = path.join(rootPath, normalized);
|
|
2622
|
-
if (fs.existsSync(absolutePath)) {
|
|
2623
|
-
candidates.push(normalized);
|
|
2624
|
-
}
|
|
2625
2219
|
};
|
|
2626
|
-
for (const focusDir of focusDirectories) {
|
|
2627
|
-
const base = focusDir.replace(/\\/g, '/').replace(/\/$/, '');
|
|
2628
|
-
pushIfExists(`${base}/package.json`);
|
|
2629
|
-
pushIfExists(`${base}/game.js`);
|
|
2630
|
-
pushIfExists(`${base}/src/Game.js`);
|
|
2631
|
-
pushIfExists(`${base}/src/factions/FactionModels.js`);
|
|
2632
|
-
pushIfExists(`${base}/GAME_DESIGN_DOCUMENT.md`);
|
|
2633
|
-
}
|
|
2634
|
-
const layout = this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2635
|
-
for (const dir of layout.directories) {
|
|
2636
|
-
if (/dominion/i.test(dir) && /-win|desktop|electron/i.test(dir)) {
|
|
2637
|
-
pushIfExists(`${dir.replace(/\\/g, '/')}/`);
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
return [...new Set(candidates)];
|
|
2641
|
-
}
|
|
2642
|
-
buildAnalysisGuidance(focusDirectories, mandatoryReadPaths) {
|
|
2643
|
-
const focusLabel = focusDirectories.length > 0
|
|
2644
|
-
? focusDirectories.map((entry) => entry.replace(/\/$/, '')).join(', ')
|
|
2645
|
-
: 'the requested project folder';
|
|
2646
|
-
const reads = mandatoryReadPaths.length > 0
|
|
2647
|
-
? mandatoryReadPaths.slice(0, 12).map((entry) => `- \`${entry}\``).join('\n')
|
|
2648
|
-
: '- package.json\n- game.js\n- src/Game.js\n- src/factions/\n- GAME_DESIGN_DOCUMENT.md';
|
|
2649
|
-
return [
|
|
2650
|
-
`The user asked for a grounded overview of ${focusLabel}.`,
|
|
2651
|
-
'Do NOT summarize from root README.md, PRODUCTION_ROADMAP.md, or PROJECT_STATUS_REPORT.md alone — they may be stale or AI-generated.',
|
|
2652
|
-
'Read these code-first paths before writing your final answer:',
|
|
2653
|
-
reads,
|
|
2654
|
-
'MANDATORY: Compare README claims against package.json, game.js, and src/. If they disagree, your final answer MUST:',
|
|
2655
|
-
'1) State what the game actually is based on code,',
|
|
2656
|
-
'2) Quote what README incorrectly claims,',
|
|
2657
|
-
'3) Explicitly say "README is inconsistent with the codebase" and recommend fixing it.',
|
|
2658
|
-
'List `vigthoria-dominion-win/` (or similar) if present to confirm desktop/Electron distribution.',
|
|
2659
|
-
].join('\n');
|
|
2660
|
-
}
|
|
2661
|
-
collectFocusDirectoryFilePaths(rootPath, focusDirRelative, maxFiles = 250) {
|
|
2662
|
-
const focusRoot = path.join(rootPath, focusDirRelative);
|
|
2663
|
-
if (!fs.existsSync(focusRoot)) {
|
|
2664
|
-
return [];
|
|
2665
|
-
}
|
|
2666
|
-
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2667
|
-
const results = [];
|
|
2668
|
-
const stack = [{ abs: focusRoot, rel: focusDirRelative.replace(/\\/g, '/') }];
|
|
2669
|
-
while (stack.length > 0 && results.length < maxFiles) {
|
|
2670
|
-
const current = stack.pop();
|
|
2671
|
-
if (!current) {
|
|
2672
|
-
continue;
|
|
2673
|
-
}
|
|
2674
|
-
let entries;
|
|
2675
|
-
try {
|
|
2676
|
-
entries = fs.readdirSync(current.abs, { withFileTypes: true });
|
|
2677
|
-
}
|
|
2678
|
-
catch {
|
|
2679
|
-
continue;
|
|
2680
|
-
}
|
|
2681
|
-
for (const entry of entries) {
|
|
2682
|
-
if (results.length >= maxFiles) {
|
|
2683
|
-
break;
|
|
2684
|
-
}
|
|
2685
|
-
const rel = `${current.rel}${current.rel.endsWith('/') ? '' : '/'}${entry.name}`.replace(/\\/g, '/');
|
|
2686
|
-
const abs = path.join(current.abs, entry.name);
|
|
2687
|
-
if (entry.isDirectory()) {
|
|
2688
|
-
if (!skipDirs.has(entry.name) && !entry.name.startsWith('.')) {
|
|
2689
|
-
stack.push({ abs, rel: `${rel}/` });
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
else if (entry.isFile()) {
|
|
2693
|
-
results.push(rel);
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
return results;
|
|
2698
|
-
}
|
|
2699
|
-
mergeWorkspacePathCandidates(primary, extras) {
|
|
2700
|
-
const seen = new Set();
|
|
2701
|
-
const merged = [];
|
|
2702
|
-
for (const candidate of [...extras, ...primary]) {
|
|
2703
|
-
const normalized = candidate.replace(/\\/g, '/');
|
|
2704
|
-
if (!normalized || seen.has(normalized)) {
|
|
2705
|
-
continue;
|
|
2706
|
-
}
|
|
2707
|
-
seen.add(normalized);
|
|
2708
|
-
merged.push(normalized);
|
|
2709
|
-
}
|
|
2710
|
-
return merged;
|
|
2711
|
-
}
|
|
2712
|
-
scoreWorkspacePathForHydration(filePath, focusTerms, focusDirectories = []) {
|
|
2713
|
-
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
2714
|
-
let score = 0;
|
|
2715
|
-
if (normalized === '.vigthoria-workspace-index.md') {
|
|
2716
|
-
score += 1000;
|
|
2717
|
-
}
|
|
2718
|
-
if (normalized.endsWith('package.json')) {
|
|
2719
|
-
score += 360;
|
|
2720
|
-
}
|
|
2721
|
-
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized)) {
|
|
2722
|
-
score += 350;
|
|
2723
|
-
}
|
|
2724
|
-
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized)) {
|
|
2725
|
-
score += 340;
|
|
2726
|
-
}
|
|
2727
|
-
if (/game_design_document\.md/i.test(normalized)) {
|
|
2728
|
-
score += 320;
|
|
2729
|
-
}
|
|
2730
|
-
if (normalized.endsWith('readme.md')) {
|
|
2731
|
-
score += focusDirectories.some((dir) => normalized.startsWith(dir.toLowerCase())) ? 120 : 40;
|
|
2732
|
-
}
|
|
2733
|
-
if (/production_roadmap|project_status_report|status_report|roadmap\.md/i.test(normalized)) {
|
|
2734
|
-
score += 20;
|
|
2735
|
-
}
|
|
2736
|
-
if (/\.(js|ts|tsx|jsx|mjs|cjs)$/i.test(normalized)) {
|
|
2737
|
-
score += 100;
|
|
2738
|
-
}
|
|
2739
|
-
if (/\.(md|txt|json|yaml|yml|toml)$/i.test(normalized)) {
|
|
2740
|
-
score += 40;
|
|
2741
|
-
}
|
|
2742
|
-
for (const dir of focusDirectories.map((entry) => entry.toLowerCase())) {
|
|
2743
|
-
if (normalized.startsWith(dir)) {
|
|
2744
|
-
score += 250;
|
|
2745
|
-
}
|
|
2746
|
-
}
|
|
2747
|
-
for (const term of focusTerms) {
|
|
2748
|
-
const termLower = term.toLowerCase();
|
|
2749
|
-
const termKey = this.normalizeFocusPathKey(term);
|
|
2750
|
-
if (normalized.includes(termLower) || (termKey && normalized.replace(/[\s_./\\-]+/g, '').includes(termKey))) {
|
|
2751
|
-
score += 80;
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
return score;
|
|
2755
2220
|
}
|
|
2756
|
-
|
|
2757
|
-
return [...filePaths].sort((a, b) => this.scoreWorkspacePathForHydration(b, focusTerms, focusDirectories)
|
|
2758
|
-
- this.scoreWorkspacePathForHydration(a, focusTerms, focusDirectories));
|
|
2759
|
-
}
|
|
2760
|
-
trimVigthoriaBrainForTransport(brain, maxChars = 12000) {
|
|
2761
|
-
try {
|
|
2762
|
-
const serialized = typeof brain === 'string' ? brain : JSON.stringify(brain);
|
|
2763
|
-
if (serialized.length <= maxChars) {
|
|
2764
|
-
return typeof brain === 'string' ? brain : JSON.parse(serialized);
|
|
2765
|
-
}
|
|
2766
|
-
return {
|
|
2767
|
-
truncated: true,
|
|
2768
|
-
excerpt: serialized.slice(0, maxChars),
|
|
2769
|
-
};
|
|
2770
|
-
}
|
|
2771
|
-
catch {
|
|
2772
|
-
return null;
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
buildLocalWorkspaceSummary(rootPath, options = {}) {
|
|
2221
|
+
buildLocalWorkspaceSummary(rootPath) {
|
|
2776
2222
|
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
2777
2223
|
return null;
|
|
2778
2224
|
}
|
|
2779
2225
|
try {
|
|
2780
|
-
const prompt = String(options.prompt || '').trim();
|
|
2781
|
-
const focusTerms = prompt ? this.extractPromptFocusTerms(prompt) : [];
|
|
2782
|
-
const topLevelLayout = this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2783
|
-
const focusDirectories = this.resolvePromptFocusDirectories(rootPath, focusTerms);
|
|
2784
|
-
const mandatoryReadPaths = this.buildMandatoryFocusReadPaths(rootPath, focusDirectories);
|
|
2785
2226
|
const summary = {
|
|
2786
2227
|
path: 'vigthoria://workspace/',
|
|
2787
|
-
name: path.basename(rootPath),
|
|
2228
|
+
name: this.getDisplayWorkspaceName(rootPath) || path.basename(rootPath),
|
|
2788
2229
|
files: [],
|
|
2789
|
-
topLevelLayout,
|
|
2790
|
-
promptFocusTerms: focusTerms,
|
|
2791
|
-
promptFocusDirectories: focusDirectories,
|
|
2792
|
-
mandatoryReadPaths,
|
|
2793
2230
|
};
|
|
2794
2231
|
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2795
2232
|
summary.fileCount = snapshot.fileCount;
|
|
@@ -2806,34 +2243,12 @@ menu {
|
|
|
2806
2243
|
};
|
|
2807
2244
|
}
|
|
2808
2245
|
const readmePath = path.join(rootPath, 'README.md');
|
|
2809
|
-
if (
|
|
2246
|
+
if (fs.existsSync(readmePath)) {
|
|
2810
2247
|
summary.readmeExcerpt = fs.readFileSync(readmePath, 'utf8').slice(0, 2500);
|
|
2811
2248
|
}
|
|
2812
|
-
else if (focusDirectories.length > 0) {
|
|
2813
|
-
summary.readmeWarning = 'Root README and planning docs are often stale or agent-generated. Prefer package.json, game.js, src/, and GAME_DESIGN_DOCUMENT.md inside the focus folder.';
|
|
2814
|
-
}
|
|
2815
|
-
for (const focusDir of focusDirectories) {
|
|
2816
|
-
const focusReadme = path.join(rootPath, focusDir, 'README.md');
|
|
2817
|
-
if (fs.existsSync(focusReadme)) {
|
|
2818
|
-
summary.focusReadmeWarning = summary.focusReadmeWarning || {};
|
|
2819
|
-
summary.focusReadmeWarning[`${focusDir}README.md`.replace(/\\/g, '/')] =
|
|
2820
|
-
'Subfolder README may be stale or wrong. Prefer package.json, game.js, src/, and GAME_DESIGN_DOCUMENT.md.';
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
const focusPaths = [
|
|
2824
|
-
...mandatoryReadPaths,
|
|
2825
|
-
...focusDirectories.flatMap((focusDir) => this.collectFocusDirectoryFilePaths(rootPath, focusDir)),
|
|
2826
|
-
];
|
|
2827
|
-
const hydrationPaths = this.sortWorkspacePathsForHydration(this.mergeWorkspacePathCandidates(snapshot.paths, focusPaths), focusTerms, focusDirectories);
|
|
2828
2249
|
// Hydrate workspace: include actual file contents so the V3 server
|
|
2829
2250
|
// can populate the remote workspace before the agent starts.
|
|
2830
|
-
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath,
|
|
2831
|
-
summary.workspaceFiles['.vigthoria-workspace-index.md'] = this.buildWorkspaceIndexFile(rootPath, snapshot, {
|
|
2832
|
-
focusTerms,
|
|
2833
|
-
focusDirectories,
|
|
2834
|
-
topLevelLayout,
|
|
2835
|
-
mandatoryReadPaths,
|
|
2836
|
-
});
|
|
2251
|
+
summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, snapshot.paths);
|
|
2837
2252
|
return summary;
|
|
2838
2253
|
}
|
|
2839
2254
|
catch (error) {
|
|
@@ -2841,68 +2256,13 @@ menu {
|
|
|
2841
2256
|
return null;
|
|
2842
2257
|
}
|
|
2843
2258
|
}
|
|
2844
|
-
buildWorkspaceIndexFile(rootPath, snapshot, options = {}) {
|
|
2845
|
-
const workspaceName = path.basename(rootPath);
|
|
2846
|
-
const listedPaths = snapshot.paths.slice(0, 1800);
|
|
2847
|
-
const focusTerms = options.focusTerms || [];
|
|
2848
|
-
const focusDirectories = options.focusDirectories || [];
|
|
2849
|
-
const mandatoryReadPaths = options.mandatoryReadPaths || [];
|
|
2850
|
-
const topLevelLayout = options.topLevelLayout || this.buildTopLevelWorkspaceLayout(rootPath);
|
|
2851
|
-
const lines = [
|
|
2852
|
-
'# Vigthoria Local Workspace Index',
|
|
2853
|
-
'',
|
|
2854
|
-
'This file is generated by the Vigthoria CLI so the remote V3 agent can search and understand the local workspace layout without seeing private machine paths.',
|
|
2855
|
-
'Read this file before guessing paths like README.md at the workspace root.',
|
|
2856
|
-
'README.md, PRODUCTION_ROADMAP.md, and PROJECT_STATUS_REPORT.md may be stale or AI-generated. Trust package.json, game.js, src/, and GAME_DESIGN_DOCUMENT.md instead.',
|
|
2857
|
-
'',
|
|
2858
|
-
'## Path rules for client-side tools',
|
|
2859
|
-
'- The workspace root is already bound to the user folder below.',
|
|
2860
|
-
'- Use relative paths from that root: `.`, `Vigthoria-dominion/`, `README.md`.',
|
|
2861
|
-
'- NEVER prefix paths with `workspace/` — that alias does not exist on the user machine.',
|
|
2862
|
-
'- Do NOT repeat the workspace folder name in paths.',
|
|
2863
|
-
'',
|
|
2864
|
-
`Workspace name: ${workspaceName}`,
|
|
2865
|
-
`Indexed files: ${snapshot.fileCount}`,
|
|
2866
|
-
'',
|
|
2867
|
-
'## Top-level layout',
|
|
2868
|
-
...(topLevelLayout.directories.length > 0
|
|
2869
|
-
? topLevelLayout.directories.map((dir) => `- ${dir}/`)
|
|
2870
|
-
: ['- (no subdirectories listed)']),
|
|
2871
|
-
...(topLevelLayout.files.length > 0
|
|
2872
|
-
? ['', '## Top-level files', ...topLevelLayout.files.map((file) => `- ${file}`)]
|
|
2873
|
-
: []),
|
|
2874
|
-
];
|
|
2875
|
-
if (focusTerms.length > 0 || focusDirectories.length > 0) {
|
|
2876
|
-
lines.push('', '## Prompt focus');
|
|
2877
|
-
if (focusTerms.length > 0) {
|
|
2878
|
-
lines.push(`User focus terms: ${focusTerms.join(', ')}`);
|
|
2879
|
-
}
|
|
2880
|
-
if (focusDirectories.length > 0) {
|
|
2881
|
-
lines.push('Likely project folders:');
|
|
2882
|
-
for (const focusDir of focusDirectories) {
|
|
2883
|
-
lines.push(`- ${focusDir}`);
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
2886
|
-
if (mandatoryReadPaths.length > 0) {
|
|
2887
|
-
lines.push('', '## Mandatory code-first reads (before summarizing)');
|
|
2888
|
-
for (const readPath of mandatoryReadPaths.slice(0, 12)) {
|
|
2889
|
-
lines.push(`- ${readPath}`);
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
}
|
|
2893
|
-
lines.push('', '## Files', ...listedPaths.map((filePath) => `- ${filePath.replace(/\\/g, '/')}`));
|
|
2894
|
-
if (snapshot.fileCount > listedPaths.length) {
|
|
2895
|
-
lines.push('', `Index truncated: ${snapshot.fileCount - listedPaths.length} additional files were not listed.`);
|
|
2896
|
-
}
|
|
2897
|
-
return `${lines.join('\n')}\n`;
|
|
2898
|
-
}
|
|
2899
2259
|
/**
|
|
2900
2260
|
* Collect text file contents from the workspace for V3 agent hydration.
|
|
2901
2261
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
2902
2262
|
*/
|
|
2903
2263
|
collectWorkspaceFileContents(rootPath, filePaths) {
|
|
2904
|
-
const MAX_TOTAL_BYTES =
|
|
2905
|
-
const MAX_FILE_BYTES =
|
|
2264
|
+
const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
2265
|
+
const MAX_FILE_BYTES = 200 * 1024;
|
|
2906
2266
|
const BINARY_EXTENSIONS = new Set([
|
|
2907
2267
|
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
|
|
2908
2268
|
'.mp3', '.mp4', '.wav', '.ogg', '.webm', '.flac', '.aac',
|
|
@@ -2948,24 +2308,14 @@ menu {
|
|
|
2948
2308
|
if (!root || !fs.existsSync(root)) {
|
|
2949
2309
|
return false;
|
|
2950
2310
|
}
|
|
2951
|
-
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2952
|
-
const startedAt = Date.now();
|
|
2953
2311
|
const stack = [root];
|
|
2954
|
-
let scannedDirs = 0;
|
|
2955
2312
|
while (stack.length > 0) {
|
|
2956
|
-
if (Date.now() - startedAt > DEFAULT_WORKSPACE_SCAN_TIMEOUT_MS) {
|
|
2957
|
-
return false;
|
|
2958
|
-
}
|
|
2959
2313
|
const current = stack.pop();
|
|
2960
2314
|
if (!current)
|
|
2961
2315
|
continue;
|
|
2962
|
-
scannedDirs += 1;
|
|
2963
|
-
if (scannedDirs > DEFAULT_WORKSPACE_SCAN_MAX_FILES) {
|
|
2964
|
-
return false;
|
|
2965
|
-
}
|
|
2966
2316
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
2967
2317
|
for (const entry of entries) {
|
|
2968
|
-
if (
|
|
2318
|
+
if (entry.name === '.git' || entry.name === 'node_modules') {
|
|
2969
2319
|
continue;
|
|
2970
2320
|
}
|
|
2971
2321
|
const fullPath = path.join(current, entry.name);
|
|
@@ -2984,24 +2334,16 @@ menu {
|
|
|
2984
2334
|
return false;
|
|
2985
2335
|
}
|
|
2986
2336
|
getAgentWorkspaceSnapshot(rootPath) {
|
|
2987
|
-
const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cache', '__pycache__', '.venv', 'venv']);
|
|
2988
2337
|
const stack = [rootPath];
|
|
2989
|
-
const startedAt = Date.now();
|
|
2990
2338
|
let fileCount = 0;
|
|
2991
2339
|
const entries = [];
|
|
2992
2340
|
while (stack.length > 0) {
|
|
2993
|
-
if (Date.now() - startedAt > DEFAULT_WORKSPACE_SCAN_TIMEOUT_MS) {
|
|
2994
|
-
break;
|
|
2995
|
-
}
|
|
2996
|
-
if (entries.length >= DEFAULT_WORKSPACE_SCAN_MAX_FILES) {
|
|
2997
|
-
break;
|
|
2998
|
-
}
|
|
2999
2341
|
const current = stack.pop();
|
|
3000
2342
|
if (!current) {
|
|
3001
2343
|
continue;
|
|
3002
2344
|
}
|
|
3003
2345
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
3004
|
-
if (
|
|
2346
|
+
if (entry.name === '.git' || entry.name === 'node_modules') {
|
|
3005
2347
|
continue;
|
|
3006
2348
|
}
|
|
3007
2349
|
const fullPath = path.join(current, entry.name);
|
|
@@ -3015,9 +2357,6 @@ menu {
|
|
|
3015
2357
|
fileCount += 1;
|
|
3016
2358
|
const stat = fs.statSync(fullPath);
|
|
3017
2359
|
entries.push(`${path.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
|
|
3018
|
-
if (entries.length >= DEFAULT_WORKSPACE_SCAN_MAX_FILES) {
|
|
3019
|
-
break;
|
|
3020
|
-
}
|
|
3021
2360
|
}
|
|
3022
2361
|
}
|
|
3023
2362
|
entries.sort();
|
|
@@ -3136,16 +2475,11 @@ menu {
|
|
|
3136
2475
|
streamedFiles[filePath] = args.content;
|
|
3137
2476
|
return;
|
|
3138
2477
|
}
|
|
3139
|
-
if (event.name === 'edit_file') {
|
|
3140
|
-
const
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
const existing = streamedFiles[filePath];
|
|
3144
|
-
if (typeof existing === 'string' && existing.includes(oldString)) {
|
|
3145
|
-
streamedFiles[filePath] = existing.replace(oldString, newString);
|
|
3146
|
-
}
|
|
2478
|
+
if (event.name === 'edit_file' && typeof args.old_string === 'string' && typeof args.new_string === 'string') {
|
|
2479
|
+
const existing = streamedFiles[filePath];
|
|
2480
|
+
if (typeof existing === 'string' && existing.includes(args.old_string)) {
|
|
2481
|
+
streamedFiles[filePath] = existing.replace(args.old_string, args.new_string);
|
|
3147
2482
|
}
|
|
3148
|
-
return;
|
|
3149
2483
|
}
|
|
3150
2484
|
}
|
|
3151
2485
|
}
|
|
@@ -3663,32 +2997,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3663
2997
|
formatV3AgentResponse(data) {
|
|
3664
2998
|
const result = data?.result || {};
|
|
3665
2999
|
if (typeof result === 'string') {
|
|
3666
|
-
return
|
|
3000
|
+
return this.sanitizeV3AgentResponseText(result);
|
|
3667
3001
|
}
|
|
3668
|
-
if (typeof result?.summary === 'string' && result.summary.trim()
|
|
3669
|
-
return
|
|
3002
|
+
if (typeof result?.summary === 'string' && result.summary.trim()) {
|
|
3003
|
+
return this.sanitizeV3AgentResponseText(result.summary);
|
|
3670
3004
|
}
|
|
3671
|
-
if (typeof result?.message === 'string' && result.message.trim()
|
|
3672
|
-
return
|
|
3005
|
+
if (typeof result?.message === 'string' && result.message.trim()) {
|
|
3006
|
+
return this.sanitizeV3AgentResponseText(result.message);
|
|
3673
3007
|
}
|
|
3674
3008
|
if (Array.isArray(data?.events)) {
|
|
3675
|
-
const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim()
|
|
3009
|
+
const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
|
|
3676
3010
|
if (completionEvent) {
|
|
3677
3011
|
return sanitizeUserFacingPathText(completionEvent.summary);
|
|
3678
3012
|
}
|
|
3679
3013
|
const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string' && event.content.trim());
|
|
3680
3014
|
if (messageEvent) {
|
|
3681
|
-
return
|
|
3015
|
+
return this.sanitizeV3AgentResponseText(messageEvent.content);
|
|
3682
3016
|
}
|
|
3683
3017
|
// Synthesize a grounded answer from the tool-call evidence the
|
|
3684
3018
|
// agent produced, rather than dumping the raw event trace.
|
|
3685
|
-
const answer = this.synthesizeAnswerFromV3Events(data.events
|
|
3686
|
-
if (answer) {
|
|
3687
|
-
return answer;
|
|
3688
|
-
}
|
|
3689
|
-
}
|
|
3690
|
-
if (Array.isArray(data?.liveToolEvidence) && data.liveToolEvidence.length > 0) {
|
|
3691
|
-
const answer = this.synthesizeAnswerFromV3Events([], data.liveToolEvidence);
|
|
3019
|
+
const answer = this.synthesizeAnswerFromV3Events(data.events);
|
|
3692
3020
|
if (answer) {
|
|
3693
3021
|
return answer;
|
|
3694
3022
|
}
|
|
@@ -3701,444 +3029,85 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3701
3029
|
const text = sanitizeUserFacingPathText(JSON.stringify(data, null, 2));
|
|
3702
3030
|
return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
|
|
3703
3031
|
}
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3719
|
-
let score = 0;
|
|
3720
|
-
if (normalized.endsWith('package.json'))
|
|
3721
|
-
score += 100;
|
|
3722
|
-
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized))
|
|
3723
|
-
score += 95;
|
|
3724
|
-
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized))
|
|
3725
|
-
score += 90;
|
|
3726
|
-
if (/game_design_document\.md/i.test(normalized))
|
|
3727
|
-
score += 85;
|
|
3728
|
-
if (/\.(js|ts|tsx|jsx|mjs|cjs)$/i.test(normalized))
|
|
3729
|
-
score += 70;
|
|
3730
|
-
if (normalized.endsWith('index.html'))
|
|
3731
|
-
score += 50;
|
|
3732
|
-
if (/production_roadmap|project_status_report|status_report/i.test(normalized))
|
|
3733
|
-
score += 5;
|
|
3734
|
-
if (normalized.endsWith('readme.md'))
|
|
3735
|
-
score += normalized.includes('vigthoria-dominion') ? 25 : 10;
|
|
3736
|
-
return score;
|
|
3737
|
-
}
|
|
3738
|
-
isLowTrustDocPath(filePath) {
|
|
3739
|
-
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3740
|
-
return normalized.endsWith('readme.md')
|
|
3741
|
-
|| /production_roadmap|project_status_report|status_report|roadmap\.md/i.test(normalized);
|
|
3742
|
-
}
|
|
3743
|
-
extractCodeSignalsFromEvidence(filesRead, liveToolEvidence = []) {
|
|
3744
|
-
const signals = new Set();
|
|
3745
|
-
const ingest = (filePath, excerpt) => {
|
|
3746
|
-
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3747
|
-
const text = String(excerpt || '');
|
|
3748
|
-
const lower = text.toLowerCase();
|
|
3749
|
-
if (normalized.endsWith('package.json')) {
|
|
3750
|
-
const match = text.match(/"description"\s*:\s*"([^"]+)"/i);
|
|
3751
|
-
if (match?.[1])
|
|
3752
|
-
signals.add(`package.json: ${match[1]}`);
|
|
3753
|
-
if (/rts|real-time strategy|command & conquer/i.test(text)) {
|
|
3754
|
-
signals.add('package.json identifies a real-time strategy game project.');
|
|
3755
|
-
}
|
|
3756
|
-
}
|
|
3757
|
-
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized)) {
|
|
3758
|
-
if (/rts|real-time strategy|command & conquer|dune 2000|3d rts/i.test(lower)) {
|
|
3759
|
-
signals.add('game.js entry identifies a 3D RTS engine (Command & Conquer / Dune 2000 style).');
|
|
3760
|
-
}
|
|
3761
|
-
}
|
|
3762
|
-
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized)) {
|
|
3763
|
-
const factions = ['Iron Dominion', 'Nova Collective', 'Aqua Tide', 'Swarm Hive']
|
|
3764
|
-
.filter((name) => lower.includes(name.toLowerCase()));
|
|
3765
|
-
if (factions.length >= 2) {
|
|
3766
|
-
signals.add(`Faction code references: ${factions.join(', ')} (4-faction RTS).`);
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
if (/vigthoria-dominion-win/i.test(normalized) || /dist-desktop|electron/i.test(lower)) {
|
|
3770
|
-
signals.add('Desktop/Electron build artifacts are present (full desktop RTS distribution).');
|
|
3771
|
-
}
|
|
3772
|
-
};
|
|
3773
|
-
for (const entry of filesRead)
|
|
3774
|
-
ingest(entry.path, entry.excerpt);
|
|
3775
|
-
for (const entry of liveToolEvidence) {
|
|
3776
|
-
if (entry?.success === false)
|
|
3777
|
-
continue;
|
|
3778
|
-
ingest(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3779
|
-
}
|
|
3780
|
-
return [...signals];
|
|
3781
|
-
}
|
|
3782
|
-
classifyGameGenreFromText(text) {
|
|
3783
|
-
const lower = String(text || '').toLowerCase();
|
|
3784
|
-
if (/card game|deck-building|dominion is a deck|treasure cards?|victory points?|buy phase|action phase/i.test(lower)) {
|
|
3785
|
-
return 'card/deck-building game';
|
|
3786
|
-
}
|
|
3787
|
-
if (/rts|real-time strategy|command & conquer|dune 2000|3d rts|build bases|gather resources|train units/i.test(lower)) {
|
|
3788
|
-
return '3D real-time strategy (RTS) game';
|
|
3789
|
-
}
|
|
3790
|
-
if (/turn-based strategy|4x strategy/i.test(lower)) {
|
|
3791
|
-
return 'turn-based strategy game';
|
|
3792
|
-
}
|
|
3793
|
-
if (/platformer|roguelike|puzzle game/i.test(lower)) {
|
|
3794
|
-
const match = lower.match(/(platformer|roguelike|puzzle game)/i);
|
|
3795
|
-
return match?.[1] || null;
|
|
3796
|
-
}
|
|
3797
|
-
return null;
|
|
3798
|
-
}
|
|
3799
|
-
extractReadmeGenreClaims(filesRead, liveToolEvidence = []) {
|
|
3800
|
-
const claims = [];
|
|
3801
|
-
const ingest = (filePath, excerpt) => {
|
|
3802
|
-
const normalized = String(filePath || '').replace(/\\/g, '/');
|
|
3803
|
-
if (!/readme\.md$/i.test(normalized))
|
|
3804
|
-
return;
|
|
3805
|
-
const genre = this.classifyGameGenreFromText(excerpt);
|
|
3806
|
-
if (!genre)
|
|
3807
|
-
return;
|
|
3808
|
-
const lines = String(excerpt || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
3809
|
-
const quoteLine = lines.find((line) => /card game|deck|rts|real-time strategy|strategy game|browser game|3d/i.test(line))
|
|
3810
|
-
|| lines.slice(0, 6).join(' ');
|
|
3811
|
-
claims.push({
|
|
3812
|
-
path: normalized,
|
|
3813
|
-
genre,
|
|
3814
|
-
quote: quoteLine.slice(0, 220),
|
|
3815
|
-
});
|
|
3816
|
-
};
|
|
3817
|
-
for (const entry of filesRead)
|
|
3818
|
-
ingest(entry.path, entry.excerpt);
|
|
3819
|
-
for (const entry of liveToolEvidence) {
|
|
3820
|
-
if (entry?.success === false)
|
|
3821
|
-
continue;
|
|
3822
|
-
ingest(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3823
|
-
}
|
|
3824
|
-
return claims;
|
|
3825
|
-
}
|
|
3826
|
-
extractCodeGenreClaims(filesRead, liveToolEvidence = []) {
|
|
3827
|
-
const sources = [];
|
|
3828
|
-
const factions = new Set();
|
|
3829
|
-
let genre = null;
|
|
3830
|
-
const ingest = (filePath, excerpt) => {
|
|
3831
|
-
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3832
|
-
const text = String(excerpt || '');
|
|
3833
|
-
const detected = this.classifyGameGenreFromText(text);
|
|
3834
|
-
const isCodeFile = normalized.endsWith('package.json')
|
|
3835
|
-
|| /(^|\/)game\.js$/i.test(normalized)
|
|
3836
|
-
|| /\/src\//i.test(normalized);
|
|
3837
|
-
if (detected && isCodeFile) {
|
|
3838
|
-
genre = genre || detected;
|
|
3839
|
-
sources.push(normalized);
|
|
3840
|
-
}
|
|
3841
|
-
if (/\/src\/factions\//i.test(normalized) || /factionmodels\.js/i.test(normalized)) {
|
|
3842
|
-
for (const name of ['Iron Dominion', 'Nova Collective', 'Aqua Tide', 'Swarm Hive']) {
|
|
3843
|
-
if (text.toLowerCase().includes(name.toLowerCase()))
|
|
3844
|
-
factions.add(name);
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
};
|
|
3848
|
-
for (const entry of filesRead)
|
|
3849
|
-
ingest(entry.path, entry.excerpt);
|
|
3850
|
-
for (const entry of liveToolEvidence) {
|
|
3851
|
-
if (entry?.success === false)
|
|
3852
|
-
continue;
|
|
3853
|
-
ingest(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3854
|
-
}
|
|
3855
|
-
return { genre, sources: [...new Set(sources)], factions: [...factions] };
|
|
3856
|
-
}
|
|
3857
|
-
buildReadmeCodeConsistencySection(filesRead, liveToolEvidence = []) {
|
|
3858
|
-
const readmeClaims = this.extractReadmeGenreClaims(filesRead, liveToolEvidence);
|
|
3859
|
-
const codeClaims = this.extractCodeGenreClaims(filesRead, liveToolEvidence);
|
|
3860
|
-
if (readmeClaims.length === 0 && !codeClaims.genre) {
|
|
3861
|
-
return null;
|
|
3862
|
-
}
|
|
3863
|
-
const lines = ['## README vs code — consistency check'];
|
|
3864
|
-
if (codeClaims.genre) {
|
|
3865
|
-
lines.push('', '**What the game actually is (from code):**');
|
|
3866
|
-
lines.push(`- Genre: **${codeClaims.genre}**`);
|
|
3867
|
-
if (codeClaims.factions.length >= 2) {
|
|
3868
|
-
lines.push(`- Factions in source: ${codeClaims.factions.join(', ')}`);
|
|
3869
|
-
}
|
|
3870
|
-
if (codeClaims.sources.length > 0) {
|
|
3871
|
-
lines.push(`- Evidence: ${codeClaims.sources.slice(0, 4).map((entry) => `\`${entry}\``).join(', ')}`);
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
else {
|
|
3875
|
-
lines.push('', '**What the game actually is (from code):** _Not determined — core code files were not read yet._');
|
|
3876
|
-
}
|
|
3877
|
-
if (readmeClaims.length > 0) {
|
|
3878
|
-
lines.push('', '**What README says:**');
|
|
3879
|
-
for (const claim of readmeClaims.slice(0, 4)) {
|
|
3880
|
-
lines.push(`- \`${claim.path}\` → **${claim.genre}**`);
|
|
3881
|
-
if (claim.quote)
|
|
3882
|
-
lines.push(` > "${claim.quote}"`);
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
3885
|
-
const readmeGenres = new Set(readmeClaims.map((entry) => entry.genre));
|
|
3886
|
-
const hasConflict = codeClaims.genre
|
|
3887
|
-
&& readmeClaims.some((entry) => entry.genre !== codeClaims.genre);
|
|
3888
|
-
if (hasConflict && codeClaims.genre) {
|
|
3889
|
-
lines.push('', '**Status: INCONSISTENT** — README documentation contradicts the codebase.', '', `The project code identifies this as a **${codeClaims.genre}**, but README describes it differently (${[...readmeGenres].join('; ')}).`, 'When reporting to the user, state the **code truth first**, then explicitly call out that README is wrong or stale and should be corrected.');
|
|
3890
|
-
}
|
|
3891
|
-
else if (codeClaims.genre && readmeClaims.length > 0) {
|
|
3892
|
-
lines.push('', '**Status: CONSISTENT** — README and code agree on the project type.');
|
|
3893
|
-
}
|
|
3894
|
-
else if (readmeClaims.length > 0 && !codeClaims.genre) {
|
|
3895
|
-
lines.push('', '**Status: UNVERIFIED** — README was read but core code was not inspected.', 'Do not present README claims as fact until package.json, game.js, and src/ are read.');
|
|
3896
|
-
}
|
|
3897
|
-
return lines.join('\n');
|
|
3898
|
-
}
|
|
3899
|
-
detectDocReliabilityWarnings(filesRead, liveToolEvidence = []) {
|
|
3900
|
-
const warnings = [];
|
|
3901
|
-
let subfolderCardGameReadme = false;
|
|
3902
|
-
let codeSaysRts = false;
|
|
3903
|
-
let readRootPlanningDocs = false;
|
|
3904
|
-
let readCoreCode = false;
|
|
3905
|
-
const inspect = (filePath, excerpt) => {
|
|
3906
|
-
const normalized = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
3907
|
-
const lower = String(excerpt || '').toLowerCase();
|
|
3908
|
-
if (/production_roadmap|project_status_report|status_report/i.test(normalized)) {
|
|
3909
|
-
readRootPlanningDocs = true;
|
|
3910
|
-
}
|
|
3911
|
-
if (/vigthoria-dominion\/readme\.md$/i.test(normalized) && /card game|deck-building|dominion is a deck/i.test(lower)) {
|
|
3912
|
-
subfolderCardGameReadme = true;
|
|
3913
|
-
}
|
|
3914
|
-
if (/readme\.md$/i.test(normalized) && /card game|deck-building|dominion is a deck/i.test(lower)) {
|
|
3915
|
-
subfolderCardGameReadme = true;
|
|
3916
|
-
}
|
|
3917
|
-
if (/(^|\/)game\.js$/i.test(normalized) || /\/src\/game\.js$/i.test(normalized) || normalized.endsWith('package.json')) {
|
|
3918
|
-
if (/rts|real-time strategy|command & conquer|3d rts/i.test(lower)) {
|
|
3919
|
-
codeSaysRts = true;
|
|
3920
|
-
readCoreCode = true;
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
if (/\/src\//i.test(normalized) && /\.(js|ts)$/i.test(normalized)) {
|
|
3924
|
-
readCoreCode = true;
|
|
3925
|
-
}
|
|
3926
|
-
};
|
|
3927
|
-
for (const entry of filesRead)
|
|
3928
|
-
inspect(entry.path, entry.excerpt);
|
|
3929
|
-
for (const entry of liveToolEvidence) {
|
|
3930
|
-
if (entry?.success === false)
|
|
3931
|
-
continue;
|
|
3932
|
-
inspect(String(entry.target || entry.arguments?.path || ''), String(entry.output || ''));
|
|
3933
|
-
}
|
|
3934
|
-
if (subfolderCardGameReadme && codeSaysRts) {
|
|
3935
|
-
warnings.push('README describes a card/deck-building game, but code (`game.js` / `package.json`) describes a 3D RTS. Report the RTS truth first, then tell the user README is inconsistent and should be fixed.');
|
|
3936
|
-
}
|
|
3937
|
-
if (readRootPlanningDocs && !readCoreCode) {
|
|
3938
|
-
warnings.push('The agent read planning/status markdown at the workspace root but did not read core game code under `Vigthoria-dominion/src/` or `package.json`. Any genre/feature summary from those docs may be wrong.');
|
|
3939
|
-
}
|
|
3940
|
-
if (!readCoreCode) {
|
|
3941
|
-
warnings.push('Core source files (`package.json`, entry `game.js`, and key files under `src/`) were not read. The overview may be incomplete until those files are inspected.');
|
|
3942
|
-
}
|
|
3943
|
-
return warnings;
|
|
3032
|
+
sanitizeV3AgentResponseText(input) {
|
|
3033
|
+
let text = sanitizeUserFacingPathText(String(input || ''));
|
|
3034
|
+
text = text
|
|
3035
|
+
.replace(/<think>/gi, '<thinking>')
|
|
3036
|
+
.replace(/<\/think>/gi, '</thinking>')
|
|
3037
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
3038
|
+
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
3039
|
+
.replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
|
|
3040
|
+
.replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
|
|
3041
|
+
.replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
|
|
3042
|
+
.replace(/^\s*(?:list_dir|read_file|write_file|edit_file|glob|grep|bash)\s*$/gim, '')
|
|
3043
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
3044
|
+
.trim();
|
|
3045
|
+
return text;
|
|
3944
3046
|
}
|
|
3945
3047
|
/**
|
|
3946
3048
|
* Build a human-readable answer from the tool results in a V3 event
|
|
3947
3049
|
* stream when the server didn't emit a proper complete/message event.
|
|
3948
3050
|
*/
|
|
3949
|
-
synthesizeAnswerFromV3Events(events
|
|
3051
|
+
synthesizeAnswerFromV3Events(events) {
|
|
3950
3052
|
const toolResults = [];
|
|
3951
|
-
const failedPaths = [];
|
|
3952
3053
|
const filesRead = [];
|
|
3953
3054
|
const filesWritten = [];
|
|
3954
|
-
const directoriesListed = [];
|
|
3955
|
-
const searches = [];
|
|
3956
3055
|
const assistantFragments = [];
|
|
3957
|
-
const pendingToolCalls = [];
|
|
3958
|
-
const ingestToolEvidence = (name, args, output, success) => {
|
|
3959
|
-
const target = String(args.path || args.file_path || args.file || args.target || '').trim();
|
|
3960
|
-
const cleanOutput = sanitizeUserFacingPathText(String(output || '').trim());
|
|
3961
|
-
if (!success || !cleanOutput) {
|
|
3962
|
-
if (!success && cleanOutput) {
|
|
3963
|
-
const failPath = target || name;
|
|
3964
|
-
failedPaths.push({
|
|
3965
|
-
path: sanitizeUserFacingPathText(failPath),
|
|
3966
|
-
detail: cleanOutput.split('\n').find(Boolean)?.slice(0, 160) || cleanOutput.slice(0, 160),
|
|
3967
|
-
});
|
|
3968
|
-
toolResults.push(`[${name}] ${cleanOutput.slice(0, 400)}`);
|
|
3969
|
-
}
|
|
3970
|
-
return;
|
|
3971
|
-
}
|
|
3972
|
-
if (name === 'read_file') {
|
|
3973
|
-
const filePath = target || 'unknown file';
|
|
3974
|
-
const excerpt = cleanOutput.length > 4000 ? `${cleanOutput.slice(0, 4000)}\n\n[...truncated...]` : cleanOutput;
|
|
3975
|
-
filesRead.push({ path: sanitizeUserFacingPathText(filePath), excerpt });
|
|
3976
|
-
return;
|
|
3977
|
-
}
|
|
3978
|
-
if (name === 'list_directory') {
|
|
3979
|
-
const excerpt = cleanOutput.length > 1200 ? `${cleanOutput.slice(0, 1200)}\n[...truncated...]` : cleanOutput;
|
|
3980
|
-
directoriesListed.push({ path: sanitizeUserFacingPathText(target || '.'), excerpt });
|
|
3981
|
-
return;
|
|
3982
|
-
}
|
|
3983
|
-
if (name === 'search_files') {
|
|
3984
|
-
const pattern = String(args.pattern || args.query || '').trim();
|
|
3985
|
-
const searchPath = String(args.path || '.').trim();
|
|
3986
|
-
searches.push(`${pattern || 'search'} in ${sanitizeUserFacingPathText(searchPath)}`);
|
|
3987
|
-
if (cleanOutput.length > 0) {
|
|
3988
|
-
toolResults.push(`[search_files] ${cleanOutput.slice(0, 600)}`);
|
|
3989
|
-
}
|
|
3990
|
-
return;
|
|
3991
|
-
}
|
|
3992
|
-
if (name === 'write_file' || name === 'create_file' || name === 'edit_file') {
|
|
3993
|
-
if (target)
|
|
3994
|
-
filesWritten.push(sanitizeUserFacingPathText(target));
|
|
3995
|
-
return;
|
|
3996
|
-
}
|
|
3997
|
-
toolResults.push(`[${name}] ${cleanOutput.slice(0, 600)}`);
|
|
3998
|
-
};
|
|
3999
3056
|
for (const event of events) {
|
|
4000
3057
|
if (!event)
|
|
4001
3058
|
continue;
|
|
4002
|
-
if (event.type === '
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
? event.output
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
: '';
|
|
4016
|
-
const name = String(event.name || event.tool || 'unknown_tool');
|
|
4017
|
-
const callIndex = pendingToolCalls.findIndex((call) => call.name === name);
|
|
4018
|
-
const call = callIndex >= 0 ? pendingToolCalls.splice(callIndex, 1)[0] : pendingToolCalls.shift();
|
|
4019
|
-
ingestToolEvidence(name, call?.args || {}, output, success);
|
|
3059
|
+
if (event.type === 'tool_result' && event.success && typeof event.output === 'string') {
|
|
3060
|
+
const name = event.name || 'unknown_tool';
|
|
3061
|
+
if (name === 'read_file' && typeof event.target === 'string') {
|
|
3062
|
+
filesRead.push(sanitizeUserFacingPathText(event.target));
|
|
3063
|
+
}
|
|
3064
|
+
else if ((name === 'write_file' || name === 'create_file') && typeof event.target === 'string') {
|
|
3065
|
+
filesWritten.push(sanitizeUserFacingPathText(event.target));
|
|
3066
|
+
}
|
|
3067
|
+
else {
|
|
3068
|
+
// Keep last ~300 chars of output for context
|
|
3069
|
+
const excerpt = event.output.length > 300 ? event.output.slice(-300) : event.output;
|
|
3070
|
+
toolResults.push(`[${name}] ${sanitizeUserFacingPathText(excerpt)}`);
|
|
3071
|
+
}
|
|
4020
3072
|
}
|
|
4021
3073
|
if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
|
|
4022
|
-
const
|
|
4023
|
-
if (
|
|
4024
|
-
assistantFragments.push(
|
|
4025
|
-
}
|
|
3074
|
+
const cleaned = this.sanitizeV3AgentResponseText(event.content.trim());
|
|
3075
|
+
if (cleaned)
|
|
3076
|
+
assistantFragments.push(cleaned);
|
|
4026
3077
|
}
|
|
4027
3078
|
// Some servers emit 'text' events for incremental assistant text
|
|
4028
3079
|
if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
|
|
4029
|
-
const
|
|
4030
|
-
if (
|
|
4031
|
-
assistantFragments.push(
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
if (event.type === 'message' && typeof event.content === 'string' && event.content.trim()) {
|
|
4035
|
-
const fragment = sanitizeUserFacingPathText(event.content.trim());
|
|
4036
|
-
if (!this.isV3StreamStatusMessage(fragment)) {
|
|
4037
|
-
assistantFragments.push(fragment);
|
|
4038
|
-
}
|
|
3080
|
+
const cleaned = this.sanitizeV3AgentResponseText(event.content.trim());
|
|
3081
|
+
if (cleaned)
|
|
3082
|
+
assistantFragments.push(cleaned);
|
|
4039
3083
|
}
|
|
4040
3084
|
// Some servers emit content_block_delta for streamed text
|
|
4041
3085
|
if (event.type === 'content_block_delta' && typeof event.delta?.text === 'string' && event.delta.text.trim()) {
|
|
4042
|
-
|
|
3086
|
+
const cleaned = this.sanitizeV3AgentResponseText(event.delta.text.trim());
|
|
3087
|
+
if (cleaned)
|
|
3088
|
+
assistantFragments.push(cleaned);
|
|
4043
3089
|
}
|
|
4044
3090
|
}
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
continue;
|
|
4048
|
-
ingestToolEvidence(String(entry.name || entry.tool || 'unknown_tool'), entry.arguments && typeof entry.arguments === 'object' ? entry.arguments : { path: entry.target || entry.path || '' }, String(entry.output || ''), entry.success !== false);
|
|
4049
|
-
}
|
|
4050
|
-
// Prefer grounded tool evidence over streamed status chatter.
|
|
4051
|
-
const hasEvidence = directoriesListed.length > 0
|
|
4052
|
-
|| filesRead.length > 0
|
|
4053
|
-
|| searches.length > 0
|
|
4054
|
-
|| filesWritten.length > 0
|
|
4055
|
-
|| toolResults.length > 0
|
|
4056
|
-
|| failedPaths.length > 0;
|
|
4057
|
-
if (hasEvidence) {
|
|
4058
|
-
const sections = [];
|
|
4059
|
-
sections.push('## Workspace analysis (from local file inspection)');
|
|
4060
|
-
sections.push('This report is rebuilt from files the agent actually read. **Code and package manifests outrank README/planning docs**, which may be stale or AI-generated.');
|
|
4061
|
-
const codeSignals = this.extractCodeSignalsFromEvidence(filesRead, liveToolEvidence);
|
|
4062
|
-
const consistencySection = this.buildReadmeCodeConsistencySection(filesRead, liveToolEvidence);
|
|
4063
|
-
if (consistencySection) {
|
|
4064
|
-
sections.unshift(consistencySection);
|
|
4065
|
-
}
|
|
4066
|
-
if (codeSignals.length > 0) {
|
|
4067
|
-
sections.push(['## What the code says (trusted)', ...codeSignals.map((line) => `- ${line}`)].join('\n'));
|
|
4068
|
-
if (!consistencySection && codeSignals.some((line) => /rts|real-time strategy|3d rts|4-faction|desktop\/electron/i.test(line))) {
|
|
4069
|
-
sections.unshift([
|
|
4070
|
-
'## Executive summary',
|
|
4071
|
-
'Vigthoria Dominion is a **3D real-time strategy game** (Command & Conquer / Dune 2000 style) with **four factions**, based on code evidence from your local workspace — not from README/planning docs, which may be wrong.',
|
|
4072
|
-
'The project includes browser/Three.js source (`Vigthoria-dominion/`) and may include a desktop Electron build (`vigthoria-dominion-win/`).',
|
|
4073
|
-
].join('\n'));
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
else if (consistencySection && /INCONSISTENT/i.test(consistencySection)) {
|
|
4077
|
-
sections.unshift([
|
|
4078
|
-
'## Executive summary',
|
|
4079
|
-
'The agent found that **README documentation does not match the codebase**. See the consistency check below — the code defines what this game actually is.',
|
|
4080
|
-
].join('\n'));
|
|
4081
|
-
}
|
|
4082
|
-
const docWarnings = this.detectDocReliabilityWarnings(filesRead, liveToolEvidence);
|
|
4083
|
-
if (docWarnings.length > 0) {
|
|
4084
|
-
sections.push(['## Documentation reliability warnings', ...docWarnings.map((line) => `- ${line}`)].join('\n'));
|
|
4085
|
-
}
|
|
4086
|
-
if (directoriesListed.length > 0) {
|
|
4087
|
-
const uniqueDirs = new Map();
|
|
4088
|
-
for (const entry of directoriesListed) {
|
|
4089
|
-
if (!uniqueDirs.has(entry.path))
|
|
4090
|
-
uniqueDirs.set(entry.path, entry.excerpt);
|
|
4091
|
-
}
|
|
4092
|
-
const dirLines = [...uniqueDirs.entries()].slice(0, 8).map(([dirPath, excerpt]) => {
|
|
4093
|
-
const preview = excerpt.split('\n').filter(Boolean).slice(0, 12).join('\n');
|
|
4094
|
-
return `### ${dirPath}\n${preview}`;
|
|
4095
|
-
});
|
|
4096
|
-
sections.push(['## Directories inspected', ...dirLines].join('\n\n'));
|
|
4097
|
-
}
|
|
4098
|
-
if (filesRead.length > 0) {
|
|
4099
|
-
const uniqueFiles = new Map();
|
|
4100
|
-
for (const entry of filesRead) {
|
|
4101
|
-
if (!uniqueFiles.has(entry.path))
|
|
4102
|
-
uniqueFiles.set(entry.path, entry.excerpt);
|
|
4103
|
-
}
|
|
4104
|
-
const sortedFiles = [...uniqueFiles.entries()].sort((a, b) => this.scoreEvidenceFilePath(b[0]) - this.scoreEvidenceFilePath(a[0]));
|
|
4105
|
-
const fileLines = sortedFiles.slice(0, 8).map(([filePath, excerpt]) => {
|
|
4106
|
-
const tag = this.isLowTrustDocPath(filePath) ? ' _(low-trust doc)_' : '';
|
|
4107
|
-
return `### ${filePath}${tag}\n${excerpt}`;
|
|
4108
|
-
});
|
|
4109
|
-
sections.push(['## Files read (code-first order)', ...fileLines].join('\n\n'));
|
|
4110
|
-
}
|
|
4111
|
-
if (searches.length > 0) {
|
|
4112
|
-
sections.push(`## Searches run\n${Array.from(new Set(searches)).slice(0, 8).map((entry) => `- ${entry}`).join('\n')}`);
|
|
4113
|
-
}
|
|
4114
|
-
if (filesWritten.length > 0) {
|
|
4115
|
-
sections.push(`## Files written\n${Array.from(new Set(filesWritten)).slice(0, 12).map((entry) => `- ${entry}`).join('\n')}`);
|
|
4116
|
-
}
|
|
4117
|
-
if (failedPaths.length > 0) {
|
|
4118
|
-
const uniqueFails = new Map();
|
|
4119
|
-
for (const entry of failedPaths) {
|
|
4120
|
-
if (!uniqueFails.has(entry.path))
|
|
4121
|
-
uniqueFails.set(entry.path, entry.detail);
|
|
4122
|
-
}
|
|
4123
|
-
const failLines = [...uniqueFails.entries()].slice(0, 12).map(([failPath, detail]) => `- \`${failPath}\`${detail ? ` — ${detail}` : ''}`);
|
|
4124
|
-
sections.push([
|
|
4125
|
-
'## Paths not found (exploration misses)',
|
|
4126
|
-
...failLines,
|
|
4127
|
-
'',
|
|
4128
|
-
'_These paths were guessed by the agent and do not exist at that location. Check the directories inspected above for the correct layout (often under `src/`)._',
|
|
4129
|
-
].join('\n'));
|
|
4130
|
-
}
|
|
4131
|
-
if (toolResults.length > 0) {
|
|
4132
|
-
sections.push(`## Additional tool evidence\n${toolResults.slice(-5).join('\n\n')}`);
|
|
4133
|
-
}
|
|
4134
|
-
return sections.join('\n\n');
|
|
4135
|
-
}
|
|
4136
|
-
// Concatenate substantive assistant text fragments in order.
|
|
3091
|
+
// Concatenate ALL assistant text fragments in order — keeps full
|
|
3092
|
+
// multi-turn reasoning instead of only the last fragment.
|
|
4137
3093
|
const fullAssistantText = assistantFragments.join('\n\n').trim();
|
|
4138
|
-
if (fullAssistantText.length >
|
|
3094
|
+
if (fullAssistantText.length > 20) {
|
|
4139
3095
|
return fullAssistantText;
|
|
4140
3096
|
}
|
|
4141
|
-
|
|
3097
|
+
// Otherwise build a summary from tool evidence
|
|
3098
|
+
const sections = [];
|
|
3099
|
+
if (filesRead.length > 0) {
|
|
3100
|
+
sections.push(`Files inspected: ${filesRead.join(', ')}`);
|
|
3101
|
+
}
|
|
3102
|
+
if (filesWritten.length > 0) {
|
|
3103
|
+
sections.push(`Files written: ${filesWritten.join(', ')}`);
|
|
3104
|
+
}
|
|
3105
|
+
if (toolResults.length > 0) {
|
|
3106
|
+
sections.push(toolResults.slice(-5).join('\n'));
|
|
3107
|
+
}
|
|
3108
|
+
return sections.length > 0
|
|
3109
|
+
? sections.join('\n\n')
|
|
3110
|
+
: '';
|
|
4142
3111
|
}
|
|
4143
3112
|
sanitizeV3AgentEventForUser(event) {
|
|
4144
3113
|
const sanitizeValue = (value) => {
|
|
@@ -4288,36 +3257,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4288
3257
|
}
|
|
4289
3258
|
}
|
|
4290
3259
|
}
|
|
4291
|
-
if (event.type === 'approval_required') {
|
|
4292
|
-
const activeContextId = String(event.context_id || contextId || context.contextId || '').trim();
|
|
4293
|
-
const approvalId = String(event.approval_id || '').trim();
|
|
4294
|
-
const command = String(event.command || '').trim();
|
|
4295
|
-
let approved = context.autoApprove === true;
|
|
4296
|
-
let allowMode = 'once';
|
|
4297
|
-
if (!approved) {
|
|
4298
|
-
const handler = context.onCommandApproval;
|
|
4299
|
-
if (typeof handler === 'function') {
|
|
4300
|
-
try {
|
|
4301
|
-
const decision = await handler(event);
|
|
4302
|
-
if (decision === true) {
|
|
4303
|
-
approved = true;
|
|
4304
|
-
}
|
|
4305
|
-
else if (decision && typeof decision === 'object') {
|
|
4306
|
-
approved = decision.approved === true;
|
|
4307
|
-
allowMode = String(decision.allowMode || decision.allow_mode || 'once');
|
|
4308
|
-
}
|
|
4309
|
-
}
|
|
4310
|
-
catch (error) {
|
|
4311
|
-
approved = false;
|
|
4312
|
-
this.logger?.warn?.(`Command approval prompt failed: ${error?.message || error}`);
|
|
4313
|
-
}
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
|
-
if (activeContextId && approvalId) {
|
|
4317
|
-
await this.submitCommandApproval(activeContextId, approvalId, approved, command, allowMode, context.v3BackendUrl);
|
|
4318
|
-
}
|
|
4319
|
-
continue;
|
|
4320
|
-
}
|
|
4321
3260
|
if (typeof context.onStreamEvent === 'function') {
|
|
4322
3261
|
try {
|
|
4323
3262
|
context.onStreamEvent(userEvent);
|
|
@@ -4326,43 +3265,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4326
3265
|
// Ignore UI callback failures; never break the agent stream for them.
|
|
4327
3266
|
}
|
|
4328
3267
|
}
|
|
4329
|
-
if (event.type === 'client_tool_request') {
|
|
4330
|
-
const activeContextId = String(event.context_id || contextId || context.contextId || '').trim();
|
|
4331
|
-
const callId = String(event.call_id || '').trim();
|
|
4332
|
-
const handler = context.onClientToolExecute;
|
|
4333
|
-
let toolResult = {
|
|
4334
|
-
success: false,
|
|
4335
|
-
output: '',
|
|
4336
|
-
error: 'No local tool handler configured',
|
|
4337
|
-
};
|
|
4338
|
-
if (typeof handler === 'function' && activeContextId && callId) {
|
|
4339
|
-
try {
|
|
4340
|
-
toolResult = await handler(event);
|
|
4341
|
-
}
|
|
4342
|
-
catch (error) {
|
|
4343
|
-
toolResult = {
|
|
4344
|
-
success: false,
|
|
4345
|
-
output: '',
|
|
4346
|
-
error: sanitizeUserFacingErrorText(error?.message || 'Local tool execution failed'),
|
|
4347
|
-
};
|
|
4348
|
-
}
|
|
4349
|
-
}
|
|
4350
|
-
if (activeContextId && callId) {
|
|
4351
|
-
await this.submitClientToolResult(activeContextId, callId, toolResult, context.v3BackendUrl);
|
|
4352
|
-
}
|
|
4353
|
-
continue;
|
|
4354
|
-
}
|
|
4355
|
-
if (event.type === 'context' && typeof context.onWorkspaceContext === 'function') {
|
|
4356
|
-
try {
|
|
4357
|
-
await context.onWorkspaceContext({
|
|
4358
|
-
contextId: String(event.context_id || contextId || '').trim(),
|
|
4359
|
-
serverWorkspaceRoot: String(event.workspace_root || serverWorkspaceRoot || '').trim(),
|
|
4360
|
-
});
|
|
4361
|
-
}
|
|
4362
|
-
catch {
|
|
4363
|
-
// Ignore workspace bind callback failures.
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
3268
|
if (event.type === 'error') {
|
|
4367
3269
|
if (event.checkpointed && event.task_id) {
|
|
4368
3270
|
// Agent checkpointed — return data so caller can auto-continue
|
|
@@ -4416,75 +3318,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4416
3318
|
serverWorkspaceRoot: serverWorkspaceRoot || null,
|
|
4417
3319
|
};
|
|
4418
3320
|
}
|
|
4419
|
-
async submitClientToolResult(contextId, callId, result, backendUrl) {
|
|
4420
|
-
const trimmedContextId = String(contextId || '').trim();
|
|
4421
|
-
const trimmedCallId = String(callId || '').trim();
|
|
4422
|
-
if (!trimmedContextId || !trimmedCallId) {
|
|
4423
|
-
return;
|
|
4424
|
-
}
|
|
4425
|
-
const baseUrl = String(backendUrl || this.getV3AgentBaseUrls()[0] || '').replace(/\/$/, '');
|
|
4426
|
-
if (!baseUrl) {
|
|
4427
|
-
return;
|
|
4428
|
-
}
|
|
4429
|
-
const headers = await this.getV3AgentHeaders();
|
|
4430
|
-
const endpoint = /127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)
|
|
4431
|
-
? `${baseUrl}/api/agent/client-tool-result`
|
|
4432
|
-
: `${baseUrl}/api/v3-agent/client-tool-result`;
|
|
4433
|
-
const body = JSON.stringify({
|
|
4434
|
-
context_id: trimmedContextId,
|
|
4435
|
-
call_id: trimmedCallId,
|
|
4436
|
-
success: result.success === true,
|
|
4437
|
-
output: String(result.output || ''),
|
|
4438
|
-
error: String(result.error || ''),
|
|
4439
|
-
});
|
|
4440
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
4441
|
-
const response = await fetch(endpoint, {
|
|
4442
|
-
method: 'POST',
|
|
4443
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
4444
|
-
body,
|
|
4445
|
-
});
|
|
4446
|
-
if (response.ok) {
|
|
4447
|
-
return;
|
|
4448
|
-
}
|
|
4449
|
-
const errorText = await response.text().catch(() => '');
|
|
4450
|
-
const isPendingRace = response.status === 404 && /no pending client tool call/i.test(errorText);
|
|
4451
|
-
if (isPendingRace && attempt < 2) {
|
|
4452
|
-
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
4453
|
-
continue;
|
|
4454
|
-
}
|
|
4455
|
-
throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
4456
|
-
}
|
|
4457
|
-
}
|
|
4458
|
-
async submitCommandApproval(contextId, approvalId, approved, command = '', allowMode = 'once', backendUrl) {
|
|
4459
|
-
const trimmedContextId = String(contextId || '').trim();
|
|
4460
|
-
const trimmedApprovalId = String(approvalId || '').trim();
|
|
4461
|
-
if (!trimmedContextId || !trimmedApprovalId) {
|
|
4462
|
-
return;
|
|
4463
|
-
}
|
|
4464
|
-
const baseUrl = String(backendUrl || this.getV3AgentBaseUrls()[0] || '').replace(/\/$/, '');
|
|
4465
|
-
if (!baseUrl) {
|
|
4466
|
-
return;
|
|
4467
|
-
}
|
|
4468
|
-
const headers = await this.getV3AgentHeaders();
|
|
4469
|
-
const endpoint = /127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)
|
|
4470
|
-
? `${baseUrl}/api/agent/approval`
|
|
4471
|
-
: `${baseUrl}/api/v3-agent/approval`;
|
|
4472
|
-
const response = await fetch(endpoint, {
|
|
4473
|
-
method: 'POST',
|
|
4474
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
4475
|
-
body: JSON.stringify({
|
|
4476
|
-
context_id: trimmedContextId,
|
|
4477
|
-
approval_id: trimmedApprovalId,
|
|
4478
|
-
approved: approved === true,
|
|
4479
|
-
command: String(command || ''),
|
|
4480
|
-
allow_mode: String(allowMode || 'once'),
|
|
4481
|
-
}),
|
|
4482
|
-
});
|
|
4483
|
-
if (!response.ok) {
|
|
4484
|
-
const errorText = await response.text().catch(() => '');
|
|
4485
|
-
throw new Error(`Command approval rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
3321
|
async runV3AgentWorkflow(message, context = {}) {
|
|
4489
3322
|
const executionContext = await this.bindExecutionContext(context);
|
|
4490
3323
|
const requestedTimeoutMs = Number(executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS);
|
|
@@ -4553,10 +3386,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4553
3386
|
}
|
|
4554
3387
|
throw new Error(`V3 agent ${response.status}: ${sanitized}`);
|
|
4555
3388
|
}
|
|
4556
|
-
const data = await this.collectV3AgentStream(response,
|
|
4557
|
-
...requestExecutionContext,
|
|
4558
|
-
v3BackendUrl: baseUrl,
|
|
4559
|
-
});
|
|
3389
|
+
const data = await this.collectV3AgentStream(response, requestExecutionContext);
|
|
4560
3390
|
// Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
|
|
4561
3391
|
if (data.checkpointed && data.checkpointed_task_id) {
|
|
4562
3392
|
const maxContinuations = 10;
|
|
@@ -4593,10 +3423,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4593
3423
|
if (!continueResponse.ok) {
|
|
4594
3424
|
break; // Fall through to normal completion with partial data
|
|
4595
3425
|
}
|
|
4596
|
-
continuationData = await this.collectV3AgentStream(continueResponse,
|
|
4597
|
-
...requestExecutionContext,
|
|
4598
|
-
v3BackendUrl: baseUrl,
|
|
4599
|
-
});
|
|
3426
|
+
continuationData = await this.collectV3AgentStream(continueResponse, requestExecutionContext);
|
|
4600
3427
|
}
|
|
4601
3428
|
catch {
|
|
4602
3429
|
break; // Fall through to normal completion with partial data
|
|
@@ -4613,13 +3440,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4613
3440
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
4614
3441
|
const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
4615
3442
|
return {
|
|
4616
|
-
content: this.formatV3AgentResponse(
|
|
4617
|
-
...continuationData,
|
|
4618
|
-
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4619
|
-
}) || this.formatV3AgentResponse({
|
|
4620
|
-
...data,
|
|
4621
|
-
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4622
|
-
}),
|
|
3443
|
+
content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
|
|
4623
3444
|
taskId: continuationData.task_id || data.task_id || null,
|
|
4624
3445
|
contextId: finalContextId,
|
|
4625
3446
|
backendUrl: baseUrl,
|
|
@@ -4638,10 +3459,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4638
3459
|
continue;
|
|
4639
3460
|
}
|
|
4640
3461
|
return {
|
|
4641
|
-
content: this.formatV3AgentResponse(
|
|
4642
|
-
...data,
|
|
4643
|
-
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4644
|
-
}),
|
|
3462
|
+
content: this.formatV3AgentResponse(data),
|
|
4645
3463
|
taskId: data.task_id || null,
|
|
4646
3464
|
contextId,
|
|
4647
3465
|
backendUrl: baseUrl,
|
|
@@ -4655,10 +3473,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4655
3473
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
4656
3474
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
4657
3475
|
return {
|
|
4658
|
-
content: this.formatV3AgentResponse(
|
|
4659
|
-
...error.partialData,
|
|
4660
|
-
liveToolEvidence: requestExecutionContext.liveToolEvidence,
|
|
4661
|
-
}) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
3476
|
+
content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
|
|
4662
3477
|
taskId: error.partialData.task_id || null,
|
|
4663
3478
|
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
4664
3479
|
backendUrl: baseUrl,
|
|
@@ -4929,7 +3744,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4929
3744
|
*
|
|
4930
3745
|
* NO localhost fallbacks - CLI is for external users, not server-side!
|
|
4931
3746
|
*/
|
|
4932
|
-
async chat(messages, model, useLocal = false
|
|
3747
|
+
async chat(messages, model, useLocal = false) {
|
|
4933
3748
|
this.lastChatTransportErrors = [];
|
|
4934
3749
|
const resolvedModel = this.resolveModelId(model);
|
|
4935
3750
|
const candidateModels = this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()
|
|
@@ -4940,7 +3755,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4940
3755
|
candidateModels.push(fallbackModel);
|
|
4941
3756
|
}
|
|
4942
3757
|
for (const candidateModel of candidateModels) {
|
|
4943
|
-
const response = await this.tryChatWithModel(messages, candidateModel, model
|
|
3758
|
+
const response = await this.tryChatWithModel(messages, candidateModel, model);
|
|
4944
3759
|
if (response) {
|
|
4945
3760
|
if (candidateModel !== resolvedModel) {
|
|
4946
3761
|
this.logger.debug(`Recovered chat request using fallback model: ${candidateModel}`);
|
|
@@ -4954,94 +3769,79 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4954
3769
|
}
|
|
4955
3770
|
throw new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend');
|
|
4956
3771
|
}
|
|
4957
|
-
getLastChatTransportErrors() {
|
|
4958
|
-
return [...this.lastChatTransportErrors];
|
|
4959
|
-
}
|
|
4960
3772
|
shouldSkipCloudRoutes(resolvedModel) {
|
|
4961
3773
|
return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
|
|
4962
3774
|
}
|
|
4963
|
-
async tryChatWithModel(messages, resolvedModel, requestedModel
|
|
3775
|
+
async tryChatWithModel(messages, resolvedModel, requestedModel) {
|
|
4964
3776
|
const routeFailures = [];
|
|
4965
|
-
const
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
options.onRouteAttempt?.(label);
|
|
4971
|
-
}
|
|
4972
|
-
catch {
|
|
4973
|
-
// Spinner callbacks must not break chat routing.
|
|
4974
|
-
}
|
|
4975
|
-
};
|
|
4976
|
-
const tryModelsRoute = async () => {
|
|
4977
|
-
if (this.shouldSkipCloudRoutes(resolvedModel) || this.isCloudModelId(resolvedModel)) {
|
|
4978
|
-
return null;
|
|
4979
|
-
}
|
|
4980
|
-
const token = this.getAccessToken();
|
|
4981
|
-
const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
|
|
4982
|
-
if (useStream) {
|
|
4983
|
-
try {
|
|
4984
|
-
notifyRoute('Streaming from Vigthoria Models API...');
|
|
4985
|
-
return await this.tryStreamingModelRouterChat(this.modelRouterClient.defaults.baseURL || 'https://api.vigthoria.io', messages, resolvedModel, requestedModel, authHeaders, options);
|
|
4986
|
-
}
|
|
4987
|
-
catch (error) {
|
|
4988
|
-
const errMsg = error?.message || 'Unknown error';
|
|
4989
|
-
routeFailures.push(`models-stream:${String(errMsg).slice(0, 120)}`);
|
|
4990
|
-
}
|
|
3777
|
+
const preferSelfHostedFirst = this.isSelfHostedPreferredModel(resolvedModel, requestedModel);
|
|
3778
|
+
if (preferSelfHostedFirst) {
|
|
3779
|
+
const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
|
|
3780
|
+
if (selfHostedResponse) {
|
|
3781
|
+
return selfHostedResponse;
|
|
4991
3782
|
}
|
|
3783
|
+
}
|
|
3784
|
+
// STRATEGY 1: Direct Vigthoria Models API (api.vigthoria.io)
|
|
3785
|
+
// Cloud aliases must be charged through Coder wallet gates first.
|
|
3786
|
+
if (!this.shouldSkipCloudRoutes(resolvedModel) && !this.isCloudModelId(resolvedModel)) {
|
|
4992
3787
|
try {
|
|
4993
|
-
|
|
3788
|
+
this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
|
|
3789
|
+
const token = this.getAccessToken();
|
|
4994
3790
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
4995
3791
|
model: resolvedModel,
|
|
4996
3792
|
messages,
|
|
4997
3793
|
max_tokens: this.config.get('preferences').maxTokens,
|
|
4998
3794
|
temperature: 0.7,
|
|
4999
3795
|
stream: false,
|
|
5000
|
-
cloudAccessApproved: this.isCloudModelId(resolvedModel),
|
|
5001
|
-
routeClass: 'direct-chat',
|
|
5002
3796
|
}, {
|
|
5003
|
-
|
|
5004
|
-
headers: {
|
|
5005
|
-
...authHeaders,
|
|
5006
|
-
...this.buildDirectChatHeaders(this.isCloudModelId(resolvedModel)),
|
|
5007
|
-
},
|
|
3797
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
5008
3798
|
});
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
3799
|
+
if (response.data.choices && response.data.choices.length > 0) {
|
|
3800
|
+
const content = response.data.choices[0].message?.content || response.data.choices[0].text;
|
|
3801
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
3802
|
+
this.logger.debug(`Direct API returned empty message content for ${resolvedModel}`);
|
|
3803
|
+
}
|
|
3804
|
+
else {
|
|
3805
|
+
return {
|
|
3806
|
+
id: response.data.id || `vigthoria-${Date.now()}`,
|
|
3807
|
+
message: content,
|
|
3808
|
+
model: response.data.model || resolvedModel || requestedModel,
|
|
3809
|
+
usage: response.data.usage,
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
5017
3812
|
}
|
|
3813
|
+
this.logger.debug(`Direct API returned no choices for ${resolvedModel}`);
|
|
5018
3814
|
}
|
|
5019
3815
|
catch (error) {
|
|
5020
3816
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3817
|
+
this.logger.debug(`Direct Vigthoria Models API failed for ${resolvedModel}: ${errMsg}`);
|
|
5021
3818
|
routeFailures.push(`models:${String(errMsg).slice(0, 120)}`);
|
|
5022
3819
|
}
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
3820
|
+
}
|
|
3821
|
+
else {
|
|
3822
|
+
if (this.isCloudModelId(resolvedModel)) {
|
|
3823
|
+
this.logger.debug(`Cloud model ${resolvedModel} routed through Coder wallet gate first; skipping direct models API route.`);
|
|
3824
|
+
}
|
|
3825
|
+
else {
|
|
3826
|
+
this.logger.debug(`Simulating cloud failure for ${resolvedModel}; skipping cloud-backed public API route.`);
|
|
5028
3827
|
}
|
|
3828
|
+
}
|
|
3829
|
+
// STRATEGY 2: Vigthoria Cloud API via Coder (authenticated users only)
|
|
3830
|
+
if (this.config.isAuthenticated() && !this.shouldSkipCloudRoutes(resolvedModel)) {
|
|
5029
3831
|
try {
|
|
5030
|
-
|
|
3832
|
+
this.logger.debug(`Vigthoria Cloud API fallback: ${resolvedModel}`);
|
|
5031
3833
|
const response = await this.client.post('/api/ai/chat', {
|
|
5032
3834
|
messages,
|
|
5033
3835
|
model: resolvedModel,
|
|
5034
3836
|
maxTokens: this.config.get('preferences').maxTokens,
|
|
5035
3837
|
temperature: 0.7,
|
|
5036
|
-
routeClass: 'direct-chat',
|
|
5037
|
-
cloudAccessApproved: this.isCloudModelId(resolvedModel),
|
|
5038
|
-
}, {
|
|
5039
|
-
timeout: idleTimeoutMs,
|
|
5040
|
-
headers: this.buildDirectChatHeaders(this.isCloudModelId(resolvedModel)),
|
|
5041
3838
|
});
|
|
5042
3839
|
if (response.data.success !== false) {
|
|
5043
3840
|
const content = response.data.response || response.data.message || response.data.content;
|
|
5044
|
-
if (typeof content
|
|
3841
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
3842
|
+
this.logger.debug(`Cloud API returned empty message content for ${resolvedModel}`);
|
|
3843
|
+
}
|
|
3844
|
+
else {
|
|
5045
3845
|
return {
|
|
5046
3846
|
id: response.data.id || `vigthoria-coder-${Date.now()}`,
|
|
5047
3847
|
message: content,
|
|
@@ -5050,81 +3850,48 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5050
3850
|
};
|
|
5051
3851
|
}
|
|
5052
3852
|
}
|
|
3853
|
+
this.logger.debug(`Cloud API returned success=false for ${resolvedModel}`);
|
|
5053
3854
|
}
|
|
5054
3855
|
catch (error) {
|
|
5055
3856
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3857
|
+
this.logger.debug(`Vigthoria Cloud API failed for ${resolvedModel}: ${errMsg}`);
|
|
5056
3858
|
routeFailures.push(`coder:${String(errMsg).slice(0, 120)}`);
|
|
5057
3859
|
}
|
|
5058
|
-
return null;
|
|
5059
|
-
};
|
|
5060
|
-
const tryCanonicalRoute = async () => {
|
|
5061
|
-
if (options.fastFail || this.isCanonicalCoderDuplicate() || !this.config.isAuthenticated()) {
|
|
5062
|
-
return null;
|
|
5063
|
-
}
|
|
5064
3860
|
try {
|
|
5065
|
-
|
|
3861
|
+
this.logger.debug(`Canonical Vigthoria Cloud fallback: ${resolvedModel}`);
|
|
5066
3862
|
const token = this.getAccessToken();
|
|
5067
3863
|
const response = await axios.post('https://coder.vigthoria.io/api/ai/chat', {
|
|
5068
3864
|
messages,
|
|
5069
3865
|
model: resolvedModel,
|
|
5070
3866
|
maxTokens: this.config.get('preferences').maxTokens,
|
|
5071
3867
|
temperature: 0.7,
|
|
5072
|
-
routeClass: 'direct-chat',
|
|
5073
|
-
cloudAccessApproved: this.isCloudModelId(resolvedModel),
|
|
5074
3868
|
}, {
|
|
5075
|
-
timeout:
|
|
3869
|
+
timeout: 180000,
|
|
5076
3870
|
httpsAgent: this._httpsAgent ?? undefined,
|
|
5077
|
-
headers: {
|
|
5078
|
-
...(token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {}),
|
|
5079
|
-
...this.buildDirectChatHeaders(this.isCloudModelId(resolvedModel)),
|
|
5080
|
-
},
|
|
3871
|
+
headers: token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {},
|
|
5081
3872
|
});
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
3873
|
+
if (response.data?.success !== false) {
|
|
3874
|
+
const content = response.data.response || response.data.message || response.data.content;
|
|
3875
|
+
if (typeof content === 'string' && content.trim()) {
|
|
3876
|
+
return {
|
|
3877
|
+
id: response.data.id || `vigthoria-coder-canonical-${Date.now()}`,
|
|
3878
|
+
message: content,
|
|
3879
|
+
model: response.data.model || resolvedModel || requestedModel,
|
|
3880
|
+
usage: response.data.usage,
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
5090
3883
|
}
|
|
5091
3884
|
}
|
|
5092
3885
|
catch (error) {
|
|
5093
3886
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3887
|
+
this.logger.debug(`Canonical Vigthoria Cloud fallback failed for ${resolvedModel}: ${errMsg}`);
|
|
5094
3888
|
routeFailures.push(`coder-canonical:${String(errMsg).slice(0, 120)}`);
|
|
5095
3889
|
}
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
};
|
|
5102
|
-
const preferred = options.preferredRoute
|
|
5103
|
-
|| (this.config.isAuthenticated() ? 'coder' : 'models');
|
|
5104
|
-
const singleRouteMode = options.fastFail === true && (options.singleRoute === true || !!options.preferredRoute);
|
|
5105
|
-
const routeOrder = singleRouteMode && options.preferredRoute
|
|
5106
|
-
? [options.preferredRoute]
|
|
5107
|
-
: preferred === 'coder'
|
|
5108
|
-
? ['coder', 'models', 'selfhosted']
|
|
5109
|
-
: preferred === 'selfhosted'
|
|
5110
|
-
? ['selfhosted', 'coder', 'models']
|
|
5111
|
-
: ['models', 'coder', 'selfhosted'];
|
|
5112
|
-
for (const route of routeOrder) {
|
|
5113
|
-
let result = null;
|
|
5114
|
-
if (route === 'coder') {
|
|
5115
|
-
result = await tryCoderRoute();
|
|
5116
|
-
if (!result) {
|
|
5117
|
-
result = await tryCanonicalRoute();
|
|
5118
|
-
}
|
|
5119
|
-
}
|
|
5120
|
-
else if (route === 'models') {
|
|
5121
|
-
result = await tryModelsRoute();
|
|
5122
|
-
}
|
|
5123
|
-
else {
|
|
5124
|
-
result = await trySelfHostedRoute();
|
|
5125
|
-
}
|
|
5126
|
-
if (result) {
|
|
5127
|
-
return result;
|
|
3890
|
+
}
|
|
3891
|
+
if (!preferSelfHostedFirst) {
|
|
3892
|
+
const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
|
|
3893
|
+
if (selfHostedResponse) {
|
|
3894
|
+
return selfHostedResponse;
|
|
5128
3895
|
}
|
|
5129
3896
|
}
|
|
5130
3897
|
if (routeFailures.length > 0) {
|
|
@@ -5132,7 +3899,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5132
3899
|
}
|
|
5133
3900
|
return null;
|
|
5134
3901
|
}
|
|
5135
|
-
async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel
|
|
3902
|
+
async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel) {
|
|
5136
3903
|
if (!this.selfHostedModelRouterClient || !this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
|
|
5137
3904
|
return null;
|
|
5138
3905
|
}
|
|
@@ -5146,7 +3913,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5146
3913
|
temperature: 0.7,
|
|
5147
3914
|
stream: false,
|
|
5148
3915
|
}, {
|
|
5149
|
-
timeout: requestTimeoutMs,
|
|
5150
3916
|
headers: {
|
|
5151
3917
|
'x-agent-mode': 'true',
|
|
5152
3918
|
},
|
|
@@ -5186,37 +3952,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5186
3952
|
return null;
|
|
5187
3953
|
}
|
|
5188
3954
|
isCloudModelId(resolvedModel) {
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
'
|
|
5192
|
-
'
|
|
5193
|
-
'vigthoria-cloud-
|
|
5194
|
-
'vigthoria-cloud-k2',
|
|
5195
|
-
'vigthoria-cloud-ultra',
|
|
5196
|
-
'vigthoria-cloud-fast',
|
|
5197
|
-
'vigthoria-cloud-balanced',
|
|
5198
|
-
'vigthoria-cloud-code',
|
|
5199
|
-
'vigthoria-cloud-power',
|
|
5200
|
-
'vigthoria-cloud-maximum',
|
|
5201
|
-
'cloud-fast',
|
|
5202
|
-
'cloud-balanced',
|
|
5203
|
-
'cloud-code',
|
|
5204
|
-
'cloud-power',
|
|
5205
|
-
'cloud-maximum',
|
|
5206
|
-
'cloud',
|
|
5207
|
-
'cloud-reason',
|
|
5208
|
-
'ultra',
|
|
5209
|
-
]);
|
|
5210
|
-
return cloudIds.has(normalized) || normalized.includes('vigthoria-cloud-');
|
|
5211
|
-
}
|
|
5212
|
-
buildDirectChatHeaders(isCloud) {
|
|
5213
|
-
const headers = {
|
|
5214
|
-
'x-vigthoria-route-class': 'direct-chat',
|
|
5215
|
-
};
|
|
5216
|
-
if (isCloud) {
|
|
5217
|
-
headers['x-vigthoria-cloud-approved'] = 'true';
|
|
5218
|
-
}
|
|
5219
|
-
return headers;
|
|
3955
|
+
return resolvedModel === 'deepseek-v3.1:671b-cloud'
|
|
3956
|
+
|| resolvedModel === 'moonshotai/kimi-k2.5'
|
|
3957
|
+
|| resolvedModel === 'vigthoria-cloud-pro'
|
|
3958
|
+
|| resolvedModel === 'vigthoria-cloud-k2'
|
|
3959
|
+
|| resolvedModel === 'vigthoria-cloud-ultra';
|
|
5220
3960
|
}
|
|
5221
3961
|
canUseCloudModel() {
|
|
5222
3962
|
return this.config.hasCloudAccess();
|
|
@@ -6258,6 +4998,123 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6258
4998
|
};
|
|
6259
4999
|
}
|
|
6260
5000
|
}
|
|
5001
|
+
/**
|
|
5002
|
+
* Fast preflight for local agent loop — probes model backends in parallel
|
|
5003
|
+
* so users see a clear pass/fail before "Planning..." hangs on slow routes.
|
|
5004
|
+
*/
|
|
5005
|
+
async runChatModelPreflight(requestedModel = 'agent') {
|
|
5006
|
+
const routes = [];
|
|
5007
|
+
const probes = [
|
|
5008
|
+
{
|
|
5009
|
+
name: 'Vigthoria Coder API',
|
|
5010
|
+
run: async () => {
|
|
5011
|
+
const health = await this.getCoderHealth();
|
|
5012
|
+
return { ok: health.ok, error: health.error };
|
|
5013
|
+
},
|
|
5014
|
+
},
|
|
5015
|
+
{
|
|
5016
|
+
name: 'Vigthoria Models API',
|
|
5017
|
+
run: async () => {
|
|
5018
|
+
const health = await this.getModelsHealth();
|
|
5019
|
+
return { ok: health.ok, error: health.error };
|
|
5020
|
+
},
|
|
5021
|
+
},
|
|
5022
|
+
];
|
|
5023
|
+
const selfHostedHealth = await this.getSelfHostedHealth();
|
|
5024
|
+
if (selfHostedHealth) {
|
|
5025
|
+
const sh = selfHostedHealth;
|
|
5026
|
+
probes.unshift({
|
|
5027
|
+
name: sh.name || 'Dedicated Models API',
|
|
5028
|
+
run: async () => ({ ok: sh.ok, error: sh.error }),
|
|
5029
|
+
});
|
|
5030
|
+
}
|
|
5031
|
+
if (this.isSelfHostedPreferredModel(this.resolveModelId(requestedModel), requestedModel)) {
|
|
5032
|
+
// Agent/code models should also accept V3 health as a usable inference path.
|
|
5033
|
+
probes.push({
|
|
5034
|
+
name: 'V3 Agent API',
|
|
5035
|
+
run: async () => {
|
|
5036
|
+
const v3 = await this.runV3HealthCheck();
|
|
5037
|
+
return { ok: v3.healthy, error: v3.error };
|
|
5038
|
+
},
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
5041
|
+
const results = await Promise.all(probes.map(async (probe) => {
|
|
5042
|
+
try {
|
|
5043
|
+
const result = await probe.run();
|
|
5044
|
+
routes.push({ name: probe.name, ok: result.ok, error: result.error });
|
|
5045
|
+
return result.ok;
|
|
5046
|
+
}
|
|
5047
|
+
catch (error) {
|
|
5048
|
+
const message = error?.message || String(error);
|
|
5049
|
+
routes.push({ name: probe.name, ok: false, error: message });
|
|
5050
|
+
return false;
|
|
5051
|
+
}
|
|
5052
|
+
}));
|
|
5053
|
+
const healthy = results.some(Boolean);
|
|
5054
|
+
const inferenceRoutes = routes.filter((route) => route.name !== 'V3 Agent API');
|
|
5055
|
+
const firstHealthy = inferenceRoutes.find((route) => route.ok) || routes.find((route) => route.ok);
|
|
5056
|
+
return {
|
|
5057
|
+
healthy,
|
|
5058
|
+
endpoint: firstHealthy?.name || 'api.vigthoria.io',
|
|
5059
|
+
error: healthy
|
|
5060
|
+
? undefined
|
|
5061
|
+
: 'No Vigthoria model backend responded during preflight. Run `vigthoria login` and check your internet connection.',
|
|
5062
|
+
routes,
|
|
5063
|
+
};
|
|
5064
|
+
}
|
|
5065
|
+
mapPreflightEndpointToRoute(endpoint) {
|
|
5066
|
+
const normalized = String(endpoint || '').toLowerCase();
|
|
5067
|
+
if (normalized.includes('coder'))
|
|
5068
|
+
return 'coder';
|
|
5069
|
+
if (normalized.includes('models'))
|
|
5070
|
+
return 'models';
|
|
5071
|
+
if (normalized.includes('dedicated') || normalized.includes('self-hosted') || normalized.includes('selfhosted')) {
|
|
5072
|
+
return 'selfhosted';
|
|
5073
|
+
}
|
|
5074
|
+
return null;
|
|
5075
|
+
}
|
|
5076
|
+
getLastChatTransportErrors() {
|
|
5077
|
+
return this.lastChatTransportErrors;
|
|
5078
|
+
}
|
|
5079
|
+
async submitClientToolResult(contextId, callId, result, backendUrl) {
|
|
5080
|
+
const trimmedContextId = String(contextId || '').trim();
|
|
5081
|
+
const trimmedCallId = String(callId || '').trim();
|
|
5082
|
+
if (!trimmedContextId || !trimmedCallId) {
|
|
5083
|
+
return;
|
|
5084
|
+
}
|
|
5085
|
+
const baseUrl = String(backendUrl || this.getV3AgentBaseUrls()[0] || '').replace(/\/$/, '');
|
|
5086
|
+
if (!baseUrl) {
|
|
5087
|
+
return;
|
|
5088
|
+
}
|
|
5089
|
+
const headers = await this.getV3AgentHeaders();
|
|
5090
|
+
const endpoint = /127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)
|
|
5091
|
+
? `${baseUrl}/api/agent/client-tool-result`
|
|
5092
|
+
: `${baseUrl}/api/v3-agent/client-tool-result`;
|
|
5093
|
+
const body = JSON.stringify({
|
|
5094
|
+
context_id: trimmedContextId,
|
|
5095
|
+
call_id: trimmedCallId,
|
|
5096
|
+
success: result.success === true,
|
|
5097
|
+
output: String(result.output || ''),
|
|
5098
|
+
error: String(result.error || ''),
|
|
5099
|
+
});
|
|
5100
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
5101
|
+
const response = await fetch(endpoint, {
|
|
5102
|
+
method: 'POST',
|
|
5103
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
5104
|
+
body,
|
|
5105
|
+
});
|
|
5106
|
+
if (response.ok) {
|
|
5107
|
+
return;
|
|
5108
|
+
}
|
|
5109
|
+
const errorText = await response.text().catch(() => '');
|
|
5110
|
+
const isPendingRace = response.status === 404 && /no pending client tool call/i.test(errorText);
|
|
5111
|
+
if (isPendingRace && attempt < 2) {
|
|
5112
|
+
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
5113
|
+
continue;
|
|
5114
|
+
}
|
|
5115
|
+
throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
6261
5118
|
isHealthyServicePayload(payload) {
|
|
6262
5119
|
const status = String(payload?.status || '').toLowerCase();
|
|
6263
5120
|
return payload?.healthy === true
|