@wrongstack/webui 0.265.1 → 0.267.0
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/assets/index-UZtAQH-v.css +2 -0
- package/dist/assets/index-q6W9UOEF.js +121 -0
- package/dist/assets/{vendor-BX-wfDR9.js → vendor-HxGAEBey.js} +230 -230
- package/dist/index.html +3 -3
- package/dist/index.js +3331 -2562
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +493 -365
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.d.ts +50 -24
- package/dist/server/index.js +512 -382
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/package.json +6 -5
- package/dist/assets/index-CC3KdGXG.js +0 -118
- package/dist/assets/index-CFHDQU3T.css +0 -2
package/dist/server/index.js
CHANGED
|
@@ -181,8 +181,8 @@ import {
|
|
|
181
181
|
createAutonomyBrain,
|
|
182
182
|
createTieredBrainArbiter
|
|
183
183
|
} from "@wrongstack/core";
|
|
184
|
-
import * as
|
|
185
|
-
import * as
|
|
184
|
+
import * as fs9 from "fs/promises";
|
|
185
|
+
import * as path9 from "path";
|
|
186
186
|
|
|
187
187
|
// src/server/http-server.ts
|
|
188
188
|
import * as fs from "fs/promises";
|
|
@@ -224,8 +224,8 @@ function extractTokenFromCookie(cookieHeader) {
|
|
|
224
224
|
for (const part of raw.split(";")) {
|
|
225
225
|
const eq = part.indexOf("=");
|
|
226
226
|
if (eq < 0) continue;
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
227
|
+
const name2 = part.slice(0, eq).trim();
|
|
228
|
+
if (name2 === "ws_token") {
|
|
229
229
|
try {
|
|
230
230
|
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
231
231
|
} catch {
|
|
@@ -930,8 +930,8 @@ var KEEP_DOTFILES = /* @__PURE__ */ new Set([
|
|
|
930
930
|
".eslintrc",
|
|
931
931
|
".prettierrc"
|
|
932
932
|
]);
|
|
933
|
-
function isHiddenEntry(
|
|
934
|
-
return
|
|
933
|
+
function isHiddenEntry(name2) {
|
|
934
|
+
return name2.startsWith(".") && !KEEP_DOTFILES.has(name2);
|
|
935
935
|
}
|
|
936
936
|
function rankFiles(paths, query, limit) {
|
|
937
937
|
const q = query.toLowerCase();
|
|
@@ -1143,245 +1143,218 @@ async function handleMemoryForget(ws, msg, memoryStore) {
|
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
1145
|
// src/server/mcp-handlers.ts
|
|
1146
|
-
import
|
|
1147
|
-
import
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1146
|
+
import { allServers } from "@wrongstack/core";
|
|
1147
|
+
import {
|
|
1148
|
+
addMcp,
|
|
1149
|
+
disableMcp,
|
|
1150
|
+
discoverMcp,
|
|
1151
|
+
enableMcp,
|
|
1152
|
+
listMcp,
|
|
1153
|
+
removeMcp,
|
|
1154
|
+
restartMcp,
|
|
1155
|
+
updateMcp
|
|
1156
|
+
} from "@wrongstack/mcp";
|
|
1157
|
+
function mapStatus(raw) {
|
|
1158
|
+
switch (raw) {
|
|
1159
|
+
case "connected":
|
|
1160
|
+
return "connected";
|
|
1161
|
+
case "connecting":
|
|
1162
|
+
case "reconnecting":
|
|
1163
|
+
return "connecting";
|
|
1164
|
+
case "failed":
|
|
1165
|
+
return "error";
|
|
1166
|
+
case "dormant":
|
|
1167
|
+
return "sleeping";
|
|
1168
|
+
default:
|
|
1169
|
+
return "stopped";
|
|
1170
|
+
}
|
|
1151
1171
|
}
|
|
1152
|
-
function
|
|
1153
|
-
|
|
1154
|
-
name,
|
|
1155
|
-
transport:
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1172
|
+
function toView(info) {
|
|
1173
|
+
const view = {
|
|
1174
|
+
name: info.name,
|
|
1175
|
+
transport: info.transport,
|
|
1176
|
+
// A dormant lazy server is "asleep", not stopped — preserve that even when
|
|
1177
|
+
// it's enabled in config.
|
|
1178
|
+
status: info.status === "dormant" ? "sleeping" : info.enabled === false ? "stopped" : mapStatus(info.status),
|
|
1179
|
+
enabled: info.enabled,
|
|
1180
|
+
tools: info.tools
|
|
1160
1181
|
};
|
|
1182
|
+
if (info.description !== void 0) view.description = info.description;
|
|
1183
|
+
if (info.lazy !== void 0) view.lazy = info.lazy;
|
|
1184
|
+
return view;
|
|
1161
1185
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1186
|
+
function deps(ws, globalConfigPath, registry) {
|
|
1187
|
+
if (!registry || !globalConfigPath) {
|
|
1188
|
+
send(ws, {
|
|
1189
|
+
type: "mcp.operation_result",
|
|
1190
|
+
payload: { success: false, message: "MCP registry is not available in this session." }
|
|
1191
|
+
});
|
|
1192
|
+
return null;
|
|
1168
1193
|
}
|
|
1194
|
+
return { configPath: globalConfigPath, registry, presets: allServers() };
|
|
1169
1195
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
await fs3.mkdir(dir, { recursive: true });
|
|
1173
|
-
await fs3.writeFile(configPath, JSON.stringify(cfg, null, 2), "utf-8");
|
|
1196
|
+
function name(msg) {
|
|
1197
|
+
return msg.payload?.name ?? "";
|
|
1174
1198
|
}
|
|
1175
|
-
async function
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
servers.push(projectServer(name, cfg));
|
|
1199
|
+
async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
1200
|
+
if (!mcpRegistry || !globalConfigPath) {
|
|
1201
|
+
send(ws, { type: "mcp.list", payload: { servers: [] } });
|
|
1202
|
+
return;
|
|
1180
1203
|
}
|
|
1181
|
-
|
|
1204
|
+
const servers = await listMcp({
|
|
1205
|
+
configPath: globalConfigPath,
|
|
1206
|
+
registry: mcpRegistry,
|
|
1207
|
+
presets: allServers()
|
|
1208
|
+
});
|
|
1209
|
+
send(ws, { type: "mcp.list", payload: { servers: servers.map(toView) } });
|
|
1182
1210
|
}
|
|
1183
|
-
function
|
|
1184
|
-
const
|
|
1185
|
-
if (!
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1211
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1212
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1213
|
+
if (!d) return;
|
|
1214
|
+
const result = await addMcp(msg.payload, d);
|
|
1215
|
+
if (result.ok && result.server) {
|
|
1216
|
+
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
1217
|
+
if (result.registryError) {
|
|
1218
|
+
send(ws, {
|
|
1219
|
+
type: "mcp.server.error",
|
|
1220
|
+
payload: { name: result.server.name, error: result.registryError }
|
|
1221
|
+
});
|
|
1222
|
+
} else if (result.server.enabled) {
|
|
1223
|
+
send(ws, { type: "mcp.server.connected", payload: { name: result.server.name } });
|
|
1190
1224
|
}
|
|
1191
|
-
} catch {
|
|
1192
1225
|
}
|
|
1193
|
-
|
|
1226
|
+
send(ws, {
|
|
1227
|
+
type: "mcp.operation_result",
|
|
1228
|
+
payload: { success: result.ok, message: result.message }
|
|
1229
|
+
});
|
|
1194
1230
|
}
|
|
1195
|
-
async function
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
server.status = registryState.state;
|
|
1202
|
-
server.tools = Array.from({ length: registryState.toolCount }, (_, i) => `tool-${i + 1}`);
|
|
1203
|
-
}
|
|
1231
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1232
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1233
|
+
if (!d) return;
|
|
1234
|
+
const result = await updateMcp(msg.payload, d);
|
|
1235
|
+
if (result.ok && result.server) {
|
|
1236
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1204
1237
|
}
|
|
1205
|
-
send(ws, {
|
|
1238
|
+
send(ws, {
|
|
1239
|
+
type: "mcp.operation_result",
|
|
1240
|
+
payload: { success: result.ok, message: result.message }
|
|
1241
|
+
});
|
|
1206
1242
|
}
|
|
1207
|
-
async function
|
|
1208
|
-
const
|
|
1209
|
-
if (!
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
try {
|
|
1214
|
-
const diskConfig = await readConfig(globalConfigPath);
|
|
1215
|
-
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1216
|
-
if (mcpServers[payload.name]) {
|
|
1217
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" already exists` } });
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
mcpServers[payload.name] = {
|
|
1221
|
-
transport: payload.transport,
|
|
1222
|
-
description: payload.description,
|
|
1223
|
-
enabled: payload.enabled ?? true,
|
|
1224
|
-
command: payload.command,
|
|
1225
|
-
args: payload.args,
|
|
1226
|
-
env: payload.env,
|
|
1227
|
-
allowedTools: payload.allowedTools
|
|
1228
|
-
};
|
|
1229
|
-
diskConfig.mcpServers = mcpServers;
|
|
1230
|
-
await writeConfig(globalConfigPath, diskConfig);
|
|
1231
|
-
const newServer = projectServer(payload.name, mcpServers[payload.name]);
|
|
1232
|
-
send(ws, { type: "mcp.server.added", payload: { server: newServer } });
|
|
1233
|
-
if (mcpRegistry && (payload.enabled ?? true)) {
|
|
1234
|
-
const serverConfig = mcpServers[payload.name];
|
|
1235
|
-
try {
|
|
1236
|
-
await mcpRegistry.start({
|
|
1237
|
-
name: payload.name,
|
|
1238
|
-
transport: payload.transport,
|
|
1239
|
-
command: payload.command,
|
|
1240
|
-
args: payload.args,
|
|
1241
|
-
env: payload.env,
|
|
1242
|
-
allowedTools: payload.allowedTools,
|
|
1243
|
-
enabled: true
|
|
1244
|
-
});
|
|
1245
|
-
} catch (err) {
|
|
1246
|
-
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" added` } });
|
|
1250
|
-
} catch (err) {
|
|
1251
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to add server: ${err}` } });
|
|
1243
|
+
async function handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1244
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1245
|
+
if (!d) return;
|
|
1246
|
+
const result = await removeMcp(name(msg), d);
|
|
1247
|
+
if (result.ok) {
|
|
1248
|
+
send(ws, { type: "mcp.server.removed", payload: { name: name(msg) } });
|
|
1252
1249
|
}
|
|
1250
|
+
send(ws, {
|
|
1251
|
+
type: "mcp.operation_result",
|
|
1252
|
+
payload: { success: result.ok, message: result.message }
|
|
1253
|
+
});
|
|
1253
1254
|
}
|
|
1254
|
-
async function
|
|
1255
|
-
const
|
|
1256
|
-
if (!
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const diskConfig = await readConfig(globalConfigPath);
|
|
1268
|
-
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1269
|
-
if (!mcpServers[payload.name]) {
|
|
1270
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" not found` } });
|
|
1271
|
-
return;
|
|
1255
|
+
async function handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1256
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1257
|
+
if (!d) return;
|
|
1258
|
+
const result = await enableMcp(name(msg), d);
|
|
1259
|
+
if (result.ok && result.server) {
|
|
1260
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1261
|
+
if (result.registryError) {
|
|
1262
|
+
send(ws, {
|
|
1263
|
+
type: "mcp.server.error",
|
|
1264
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1265
|
+
});
|
|
1266
|
+
} else {
|
|
1267
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1272
1268
|
}
|
|
1273
|
-
delete mcpServers[payload.name];
|
|
1274
|
-
diskConfig.mcpServers = mcpServers;
|
|
1275
|
-
await writeConfig(globalConfigPath, diskConfig);
|
|
1276
|
-
send(ws, { type: "mcp.server.removed", payload: { name: payload.name } });
|
|
1277
|
-
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" removed` } });
|
|
1278
|
-
} catch (err) {
|
|
1279
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to remove server: ${err}` } });
|
|
1280
1269
|
}
|
|
1270
|
+
send(ws, {
|
|
1271
|
+
type: "mcp.operation_result",
|
|
1272
|
+
payload: { success: result.ok, message: result.message }
|
|
1273
|
+
});
|
|
1281
1274
|
}
|
|
1282
|
-
async function
|
|
1283
|
-
const
|
|
1284
|
-
if (!
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1291
|
-
if (!mcpServers[payload.name]) {
|
|
1292
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" not found` } });
|
|
1293
|
-
return;
|
|
1275
|
+
async function handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1276
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1277
|
+
if (!d) return;
|
|
1278
|
+
const result = await disableMcp(name(msg), d);
|
|
1279
|
+
if (result.ok) {
|
|
1280
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: name(msg) } });
|
|
1281
|
+
if (result.server) {
|
|
1282
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1294
1283
|
}
|
|
1295
|
-
const existing = mcpServers[payload.name];
|
|
1296
|
-
mcpServers[payload.name] = {
|
|
1297
|
-
transport: payload.transport ?? existing.transport,
|
|
1298
|
-
description: payload.description ?? existing.description,
|
|
1299
|
-
enabled: payload.enabled ?? existing.enabled,
|
|
1300
|
-
command: payload.command ?? existing.command,
|
|
1301
|
-
args: payload.args ?? existing.args,
|
|
1302
|
-
env: payload.env ?? existing.env,
|
|
1303
|
-
allowedTools: payload.allowedTools ?? existing.allowedTools
|
|
1304
|
-
};
|
|
1305
|
-
diskConfig.mcpServers = mcpServers;
|
|
1306
|
-
await writeConfig(globalConfigPath, diskConfig);
|
|
1307
|
-
const updatedServer = projectServer(payload.name, mcpServers[payload.name]);
|
|
1308
|
-
send(ws, { type: "mcp.server.updated", payload: { server: updatedServer } });
|
|
1309
|
-
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" updated` } });
|
|
1310
|
-
} catch (err) {
|
|
1311
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to update server: ${err}` } });
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
async function handleMcpWake(ws, msg, _config, _globalConfigPath, mcpRegistry) {
|
|
1315
|
-
const payload = msg.payload;
|
|
1316
|
-
if (!payload.name) {
|
|
1317
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
if (!mcpRegistry) {
|
|
1321
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "MCP registry not available" } });
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
try {
|
|
1325
|
-
send(ws, { type: "mcp.server.waking", payload: { name: payload.name } });
|
|
1326
|
-
await mcpRegistry.restart(payload.name);
|
|
1327
|
-
send(ws, { type: "mcp.server.connected", payload: { name: payload.name } });
|
|
1328
|
-
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" restarted` } });
|
|
1329
|
-
} catch (err) {
|
|
1330
|
-
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1331
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to restart "${payload.name}": ${err}` } });
|
|
1332
1284
|
}
|
|
1285
|
+
send(ws, {
|
|
1286
|
+
type: "mcp.operation_result",
|
|
1287
|
+
payload: { success: result.ok, message: result.message }
|
|
1288
|
+
});
|
|
1333
1289
|
}
|
|
1334
|
-
async function handleMcpSleep(ws, msg,
|
|
1335
|
-
const
|
|
1336
|
-
if (!
|
|
1337
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
if (!mcpRegistry) {
|
|
1341
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "MCP registry not available" } });
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1290
|
+
async function handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1291
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1292
|
+
if (!d) return;
|
|
1344
1293
|
try {
|
|
1345
|
-
await
|
|
1346
|
-
send(ws, { type: "mcp.server.sleeping", payload: { name:
|
|
1347
|
-
send(ws, {
|
|
1294
|
+
await d.registry.stop(name(msg));
|
|
1295
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: name(msg) } });
|
|
1296
|
+
send(ws, {
|
|
1297
|
+
type: "mcp.operation_result",
|
|
1298
|
+
payload: { success: true, message: `Server "${name(msg)}" stopped` }
|
|
1299
|
+
});
|
|
1348
1300
|
} catch (err) {
|
|
1349
|
-
|
|
1350
|
-
send(ws, { type: "mcp.
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
if (!payload.name) {
|
|
1356
|
-
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1357
|
-
return;
|
|
1301
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1302
|
+
send(ws, { type: "mcp.server.error", payload: { name: name(msg), error } });
|
|
1303
|
+
send(ws, {
|
|
1304
|
+
type: "mcp.operation_result",
|
|
1305
|
+
payload: { success: false, message: `Failed to stop "${name(msg)}": ${error}` }
|
|
1306
|
+
});
|
|
1358
1307
|
}
|
|
1359
|
-
send(ws, { type: "mcp.server.discovered", payload: { name: payload.name, tools: [] } });
|
|
1360
|
-
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" tools were discovered on connect` } });
|
|
1361
1308
|
}
|
|
1362
|
-
async function
|
|
1363
|
-
const
|
|
1364
|
-
if (!
|
|
1365
|
-
|
|
1366
|
-
|
|
1309
|
+
async function handleMcpWake(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1310
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1311
|
+
if (!d) return;
|
|
1312
|
+
send(ws, { type: "mcp.server.waking", payload: { name: name(msg) } });
|
|
1313
|
+
const result = await restartMcp(name(msg), d);
|
|
1314
|
+
if (result.ok && !result.registryError) {
|
|
1315
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1316
|
+
} else if (result.registryError) {
|
|
1317
|
+
send(ws, {
|
|
1318
|
+
type: "mcp.server.error",
|
|
1319
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1320
|
+
});
|
|
1367
1321
|
}
|
|
1368
|
-
send(ws, {
|
|
1322
|
+
send(ws, {
|
|
1323
|
+
type: "mcp.operation_result",
|
|
1324
|
+
payload: { success: result.ok, message: result.message }
|
|
1325
|
+
});
|
|
1369
1326
|
}
|
|
1370
|
-
async function
|
|
1371
|
-
const
|
|
1372
|
-
if (!
|
|
1373
|
-
|
|
1374
|
-
|
|
1327
|
+
async function handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1328
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1329
|
+
if (!d) return;
|
|
1330
|
+
const result = await restartMcp(name(msg), d);
|
|
1331
|
+
if (result.ok && !result.registryError) {
|
|
1332
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1333
|
+
} else if (result.registryError) {
|
|
1334
|
+
send(ws, {
|
|
1335
|
+
type: "mcp.server.error",
|
|
1336
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1337
|
+
});
|
|
1375
1338
|
}
|
|
1376
|
-
send(ws, {
|
|
1339
|
+
send(ws, {
|
|
1340
|
+
type: "mcp.operation_result",
|
|
1341
|
+
payload: { success: result.ok, message: result.message }
|
|
1342
|
+
});
|
|
1377
1343
|
}
|
|
1378
|
-
async function
|
|
1379
|
-
const
|
|
1380
|
-
if (!
|
|
1381
|
-
|
|
1382
|
-
|
|
1344
|
+
async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1345
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1346
|
+
if (!d) return;
|
|
1347
|
+
const result = await discoverMcp(name(msg), d);
|
|
1348
|
+
if (result.ok) {
|
|
1349
|
+
send(ws, {
|
|
1350
|
+
type: "mcp.server.discovered",
|
|
1351
|
+
payload: { name: name(msg), tools: result.tools ?? [] }
|
|
1352
|
+
});
|
|
1383
1353
|
}
|
|
1384
|
-
send(ws, {
|
|
1354
|
+
send(ws, {
|
|
1355
|
+
type: "mcp.operation_result",
|
|
1356
|
+
payload: { success: result.ok, message: result.message }
|
|
1357
|
+
});
|
|
1385
1358
|
}
|
|
1386
1359
|
|
|
1387
1360
|
// src/server/index.ts
|
|
@@ -1417,12 +1390,14 @@ import {
|
|
|
1417
1390
|
repairToolUseAdjacency,
|
|
1418
1391
|
resolveContextWindowPolicy,
|
|
1419
1392
|
enhanceUserPrompt,
|
|
1420
|
-
recentTextTurns
|
|
1393
|
+
recentTextTurns,
|
|
1394
|
+
resolveProviderModelList
|
|
1421
1395
|
} from "@wrongstack/core";
|
|
1422
1396
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
1423
1397
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
1424
1398
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
1425
1399
|
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
1400
|
+
import { MCPRegistry } from "@wrongstack/mcp";
|
|
1426
1401
|
import { WebSocketServer } from "ws";
|
|
1427
1402
|
|
|
1428
1403
|
// ../runtime/src/container.ts
|
|
@@ -2669,9 +2644,9 @@ var WorktreeWebSocketHandler = class {
|
|
|
2669
2644
|
|
|
2670
2645
|
// src/server/mailbox-handlers.ts
|
|
2671
2646
|
import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
|
|
2672
|
-
async function handleMailboxMessages(ws,
|
|
2647
|
+
async function handleMailboxMessages(ws, deps2, payload) {
|
|
2673
2648
|
try {
|
|
2674
|
-
const dir = resolveProjectDir(
|
|
2649
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
2675
2650
|
const mb = new GlobalMailbox(dir);
|
|
2676
2651
|
const messages = await mb.query({
|
|
2677
2652
|
limit: payload?.limit ?? 30,
|
|
@@ -2705,9 +2680,9 @@ async function handleMailboxMessages(ws, deps, payload) {
|
|
|
2705
2680
|
send(ws, { type: "mailbox.messages", payload: { messages: [], error: errMessage(err) } });
|
|
2706
2681
|
}
|
|
2707
2682
|
}
|
|
2708
|
-
async function handleMailboxAgents(ws,
|
|
2683
|
+
async function handleMailboxAgents(ws, deps2, payload) {
|
|
2709
2684
|
try {
|
|
2710
|
-
const dir = resolveProjectDir(
|
|
2685
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
2711
2686
|
const mb = new GlobalMailbox(dir);
|
|
2712
2687
|
const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
|
|
2713
2688
|
send(ws, {
|
|
@@ -2734,9 +2709,9 @@ async function handleMailboxAgents(ws, deps, payload) {
|
|
|
2734
2709
|
send(ws, { type: "mailbox.agents", payload: { agents: [], error: errMessage(err) } });
|
|
2735
2710
|
}
|
|
2736
2711
|
}
|
|
2737
|
-
async function handleMailboxClear(ws,
|
|
2712
|
+
async function handleMailboxClear(ws, deps2) {
|
|
2738
2713
|
try {
|
|
2739
|
-
const dir = resolveProjectDir(
|
|
2714
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
2740
2715
|
const mb = new GlobalMailbox(dir);
|
|
2741
2716
|
await mb.clearAll();
|
|
2742
2717
|
send(ws, { type: "mailbox.cleared", payload: {} });
|
|
@@ -2744,9 +2719,9 @@ async function handleMailboxClear(ws, deps) {
|
|
|
2744
2719
|
send(ws, { type: "mailbox.cleared", payload: { error: errMessage(err) } });
|
|
2745
2720
|
}
|
|
2746
2721
|
}
|
|
2747
|
-
async function handleMailboxPurge(ws,
|
|
2722
|
+
async function handleMailboxPurge(ws, deps2, opts) {
|
|
2748
2723
|
try {
|
|
2749
|
-
const dir = resolveProjectDir(
|
|
2724
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
2750
2725
|
const mb = new GlobalMailbox(dir);
|
|
2751
2726
|
const result = await mb.purgeStale(opts);
|
|
2752
2727
|
send(ws, { type: "mailbox.purged", payload: result });
|
|
@@ -2793,14 +2768,14 @@ function registerShutdownHandlers(res) {
|
|
|
2793
2768
|
|
|
2794
2769
|
// src/server/instance-registry.ts
|
|
2795
2770
|
import * as os from "os";
|
|
2796
|
-
import * as
|
|
2797
|
-
import * as
|
|
2771
|
+
import * as path3 from "path";
|
|
2772
|
+
import * as fs3 from "fs/promises";
|
|
2798
2773
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
2799
2774
|
function defaultBaseDir() {
|
|
2800
|
-
return
|
|
2775
|
+
return path3.join(os.homedir(), ".wrongstack");
|
|
2801
2776
|
}
|
|
2802
2777
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
2803
|
-
return
|
|
2778
|
+
return path3.join(baseDir, "webui-instances.json");
|
|
2804
2779
|
}
|
|
2805
2780
|
function isPidAlive(pid) {
|
|
2806
2781
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -2813,7 +2788,7 @@ function isPidAlive(pid) {
|
|
|
2813
2788
|
}
|
|
2814
2789
|
async function load(file) {
|
|
2815
2790
|
try {
|
|
2816
|
-
const raw = await
|
|
2791
|
+
const raw = await fs3.readFile(file, "utf8");
|
|
2817
2792
|
const parsed = JSON.parse(raw);
|
|
2818
2793
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
2819
2794
|
return parsed;
|
|
@@ -2961,15 +2936,15 @@ import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/cor
|
|
|
2961
2936
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
2962
2937
|
|
|
2963
2938
|
// src/server/provider-config-io.ts
|
|
2964
|
-
import * as
|
|
2965
|
-
import * as
|
|
2939
|
+
import * as fs4 from "fs/promises";
|
|
2940
|
+
import * as path4 from "path";
|
|
2966
2941
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
2967
2942
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
2968
2943
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
2969
2944
|
async function loadSavedProviders(configPath, vault) {
|
|
2970
2945
|
let raw;
|
|
2971
2946
|
try {
|
|
2972
|
-
raw = await
|
|
2947
|
+
raw = await fs4.readFile(configPath, "utf8");
|
|
2973
2948
|
} catch {
|
|
2974
2949
|
return {};
|
|
2975
2950
|
}
|
|
@@ -2986,7 +2961,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
2986
2961
|
let raw;
|
|
2987
2962
|
let fileExists = true;
|
|
2988
2963
|
try {
|
|
2989
|
-
raw = await
|
|
2964
|
+
raw = await fs4.readFile(configPath, "utf8");
|
|
2990
2965
|
} catch (err) {
|
|
2991
2966
|
if (err.code !== "ENOENT") {
|
|
2992
2967
|
throw new Error(
|
|
@@ -3014,7 +2989,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
3014
2989
|
await atomicWrite3(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
3015
2990
|
}
|
|
3016
2991
|
function createProviderConfigIO(configPath) {
|
|
3017
|
-
const keyFile =
|
|
2992
|
+
const keyFile = path4.join(path4.dirname(configPath), ".key");
|
|
3018
2993
|
const vault = new DefaultSecretVault({ keyFile });
|
|
3019
2994
|
return {
|
|
3020
2995
|
load: () => loadSavedProviders(configPath, vault),
|
|
@@ -3144,9 +3119,9 @@ function projectSavedProviders(providers) {
|
|
|
3144
3119
|
});
|
|
3145
3120
|
}
|
|
3146
3121
|
var probeScrubber = new DefaultSecretScrubber2();
|
|
3147
|
-
function createProviderHandlers(
|
|
3148
|
-
const { globalConfigPath, vault, broadcast: broadcast2, clients } =
|
|
3149
|
-
let configWriteLock =
|
|
3122
|
+
function createProviderHandlers(deps2) {
|
|
3123
|
+
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
|
|
3124
|
+
let configWriteLock = deps2.getConfigWriteLock();
|
|
3150
3125
|
async function loadConfigProviders() {
|
|
3151
3126
|
return loadSavedProviders(globalConfigPath, vault);
|
|
3152
3127
|
}
|
|
@@ -3161,7 +3136,7 @@ function createProviderHandlers(deps) {
|
|
|
3161
3136
|
}));
|
|
3162
3137
|
});
|
|
3163
3138
|
configWriteLock = next;
|
|
3164
|
-
|
|
3139
|
+
deps2.setConfigWriteLock(next);
|
|
3165
3140
|
await next;
|
|
3166
3141
|
}
|
|
3167
3142
|
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
@@ -3317,11 +3292,11 @@ function createProviderHandlers(deps) {
|
|
|
3317
3292
|
}
|
|
3318
3293
|
|
|
3319
3294
|
// src/server/setup-events.ts
|
|
3320
|
-
import * as
|
|
3295
|
+
import * as fs5 from "fs/promises";
|
|
3321
3296
|
import { watch as fsWatch } from "fs";
|
|
3322
|
-
import * as
|
|
3323
|
-
function setupEvents(
|
|
3324
|
-
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } =
|
|
3297
|
+
import * as path5 from "path";
|
|
3298
|
+
function setupEvents(deps2) {
|
|
3299
|
+
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
|
|
3325
3300
|
const disposers = [];
|
|
3326
3301
|
events.on("iteration.started", (e) => {
|
|
3327
3302
|
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
|
|
@@ -3528,16 +3503,16 @@ function setupEvents(deps) {
|
|
|
3528
3503
|
if (wpaths?.projectStatus) {
|
|
3529
3504
|
try {
|
|
3530
3505
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
3531
|
-
const dir =
|
|
3532
|
-
await
|
|
3533
|
-
await
|
|
3506
|
+
const dir = path5.dirname(statusFile);
|
|
3507
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
3508
|
+
await fs5.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
3534
3509
|
} catch (err) {
|
|
3535
3510
|
console.error("[setup-events] Failed to write status.json:", err);
|
|
3536
3511
|
}
|
|
3537
3512
|
}
|
|
3538
3513
|
});
|
|
3539
3514
|
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
3540
|
-
const projectsDir =
|
|
3515
|
+
const projectsDir = path5.join(wpaths.configDir, "projects");
|
|
3541
3516
|
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
3542
3517
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
3543
3518
|
const DEBOUNCE_MS = 150;
|
|
@@ -3600,20 +3575,20 @@ function setupEvents(deps) {
|
|
|
3600
3575
|
let watcher;
|
|
3601
3576
|
const startWatcher = async () => {
|
|
3602
3577
|
try {
|
|
3603
|
-
await
|
|
3578
|
+
await fs5.mkdir(projectsDir, { recursive: true });
|
|
3604
3579
|
watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
|
|
3605
3580
|
if (eventType === "change") {
|
|
3606
3581
|
if (filename == null) return;
|
|
3607
3582
|
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
3608
|
-
const targetFile =
|
|
3583
|
+
const targetFile = path5.join(projectsDir, String(filename));
|
|
3609
3584
|
if (targetFile.endsWith("status.json")) {
|
|
3610
|
-
const projectHash2 =
|
|
3585
|
+
const projectHash2 = path5.basename(path5.dirname(targetFile));
|
|
3611
3586
|
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
3612
3587
|
return;
|
|
3613
3588
|
}
|
|
3614
3589
|
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
3615
3590
|
try {
|
|
3616
|
-
const content = await
|
|
3591
|
+
const content = await fs5.readFile(targetFile, "utf-8");
|
|
3617
3592
|
const statusData = JSON.parse(content);
|
|
3618
3593
|
if (statusData.projectHash) {
|
|
3619
3594
|
const hash = String(statusData.projectHash);
|
|
@@ -3665,7 +3640,7 @@ function setupEvents(deps) {
|
|
|
3665
3640
|
}
|
|
3666
3641
|
});
|
|
3667
3642
|
}
|
|
3668
|
-
const globalRoot = globalConfigPath ?
|
|
3643
|
+
const globalRoot = globalConfigPath ? path5.dirname(globalConfigPath) : void 0;
|
|
3669
3644
|
if (globalRoot) {
|
|
3670
3645
|
const broadcastSessions = async () => {
|
|
3671
3646
|
try {
|
|
@@ -3713,8 +3688,8 @@ function setupEvents(deps) {
|
|
|
3713
3688
|
let regDebounce;
|
|
3714
3689
|
try {
|
|
3715
3690
|
const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
|
|
3716
|
-
const
|
|
3717
|
-
if (!
|
|
3691
|
+
const name2 = filename ? String(filename) : "";
|
|
3692
|
+
if (!name2.startsWith("session-registry.json") || name2.endsWith(".lock")) return;
|
|
3718
3693
|
if (regDebounce) clearTimeout(regDebounce);
|
|
3719
3694
|
regDebounce = setTimeout(() => void broadcastSessions(), 150);
|
|
3720
3695
|
});
|
|
@@ -3738,11 +3713,11 @@ function setupEvents(deps) {
|
|
|
3738
3713
|
|
|
3739
3714
|
// src/server/custom-context-modes.ts
|
|
3740
3715
|
import { listContextWindowModes, atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
3741
|
-
import * as
|
|
3742
|
-
import * as
|
|
3716
|
+
import * as fs6 from "fs/promises";
|
|
3717
|
+
import * as path6 from "path";
|
|
3743
3718
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
3744
3719
|
function storePath(wrongstackDir) {
|
|
3745
|
-
return
|
|
3720
|
+
return path6.join(wrongstackDir, STORE_FILENAME);
|
|
3746
3721
|
}
|
|
3747
3722
|
var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
|
|
3748
3723
|
function createCustomModeStore(wrongstackDir) {
|
|
@@ -3750,7 +3725,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
3750
3725
|
const load2 = async () => {
|
|
3751
3726
|
modes.clear();
|
|
3752
3727
|
try {
|
|
3753
|
-
const raw = await
|
|
3728
|
+
const raw = await fs6.readFile(storePath(wrongstackDir), "utf8");
|
|
3754
3729
|
const parsed = JSON.parse(raw);
|
|
3755
3730
|
if (Array.isArray(parsed.modes)) {
|
|
3756
3731
|
for (const m of parsed.modes) {
|
|
@@ -3930,14 +3905,14 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
3930
3905
|
}
|
|
3931
3906
|
|
|
3932
3907
|
// src/server/shell-open.ts
|
|
3933
|
-
import * as
|
|
3934
|
-
import * as
|
|
3908
|
+
import * as fs7 from "fs/promises";
|
|
3909
|
+
import * as path7 from "path";
|
|
3935
3910
|
import { spawn as spawn2 } from "child_process";
|
|
3936
3911
|
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
3937
3912
|
async function handleShellOpen(req, logger) {
|
|
3938
3913
|
try {
|
|
3939
|
-
const resolved =
|
|
3940
|
-
await
|
|
3914
|
+
const resolved = path7.resolve(req.path);
|
|
3915
|
+
await fs7.access(resolved);
|
|
3941
3916
|
if (METACHAR_REGEX.test(resolved)) {
|
|
3942
3917
|
return { success: false, message: "Path contains unsupported characters." };
|
|
3943
3918
|
}
|
|
@@ -4013,10 +3988,148 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
4013
3988
|
send(ws, { type: "git.info", payload: { branch: "", added: 0, deleted: 0, untracked: 0, ahead: 0, behind: 0 } });
|
|
4014
3989
|
}
|
|
4015
3990
|
}
|
|
3991
|
+
function makeGit(cwd) {
|
|
3992
|
+
return async (args) => {
|
|
3993
|
+
const { execFile: ef } = await import("child_process");
|
|
3994
|
+
return new Promise((resolve5) => {
|
|
3995
|
+
ef(
|
|
3996
|
+
"git",
|
|
3997
|
+
args,
|
|
3998
|
+
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
3999
|
+
(err, stdout) => resolve5(err ? "" : stdout)
|
|
4000
|
+
);
|
|
4001
|
+
});
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
async function handleGitChanges(ws, projectRoot) {
|
|
4005
|
+
const cwd = projectRoot || void 0;
|
|
4006
|
+
try {
|
|
4007
|
+
const git = makeGit(cwd);
|
|
4008
|
+
const [statusRaw, unstagedNumstat, stagedNumstat] = await Promise.all([
|
|
4009
|
+
git(["status", "--porcelain", "-z"]),
|
|
4010
|
+
git(["diff", "--numstat", "-z"]),
|
|
4011
|
+
git(["diff", "--cached", "--numstat", "-z"])
|
|
4012
|
+
]);
|
|
4013
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4014
|
+
const parseNumstat = (raw) => {
|
|
4015
|
+
const parts = raw.split("\0");
|
|
4016
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4017
|
+
const entry = parts[i];
|
|
4018
|
+
if (!entry) continue;
|
|
4019
|
+
const m = /^(\d+|-)\t(\d+|-)\t(.*)$/.exec(entry);
|
|
4020
|
+
if (!m) continue;
|
|
4021
|
+
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
4022
|
+
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
4023
|
+
let path10 = m[3] ?? "";
|
|
4024
|
+
if (path10 === "") {
|
|
4025
|
+
i += 1;
|
|
4026
|
+
path10 = parts[i + 1] ?? parts[i] ?? "";
|
|
4027
|
+
i += 1;
|
|
4028
|
+
}
|
|
4029
|
+
if (!path10) continue;
|
|
4030
|
+
const prev = counts.get(path10) ?? { added: 0, deleted: 0 };
|
|
4031
|
+
counts.set(path10, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
4032
|
+
}
|
|
4033
|
+
};
|
|
4034
|
+
parseNumstat(unstagedNumstat);
|
|
4035
|
+
parseNumstat(stagedNumstat);
|
|
4036
|
+
const records = statusRaw.split("\0").filter((r) => r.length > 0);
|
|
4037
|
+
const files = [];
|
|
4038
|
+
for (let i = 0; i < records.length; i++) {
|
|
4039
|
+
const rec = records[i];
|
|
4040
|
+
if (!rec || rec.length < 3) continue;
|
|
4041
|
+
const x = rec[0] ?? " ";
|
|
4042
|
+
const y = rec[1] ?? " ";
|
|
4043
|
+
const path10 = rec.slice(3);
|
|
4044
|
+
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
4045
|
+
if (isRename) i += 1;
|
|
4046
|
+
let status;
|
|
4047
|
+
if (x === "?" && y === "?") status = "?";
|
|
4048
|
+
else if (x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D") status = "U";
|
|
4049
|
+
else if (x === "R" || y === "R") status = "R";
|
|
4050
|
+
else if (x === "C" || y === "C") status = "C";
|
|
4051
|
+
else if (x === "A" || y === "A") status = "A";
|
|
4052
|
+
else if (x === "D" || y === "D") status = "D";
|
|
4053
|
+
else status = "M";
|
|
4054
|
+
const staged = x !== " " && x !== "?";
|
|
4055
|
+
let added = counts.get(path10)?.added ?? 0;
|
|
4056
|
+
let deleted = counts.get(path10)?.deleted ?? 0;
|
|
4057
|
+
if (status === "?") {
|
|
4058
|
+
added = await countUntrackedLines(cwd, path10);
|
|
4059
|
+
deleted = 0;
|
|
4060
|
+
}
|
|
4061
|
+
files.push({ path: path10, status, added, deleted, staged });
|
|
4062
|
+
}
|
|
4063
|
+
send(ws, { type: "git.changes", payload: { files } });
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
send(ws, {
|
|
4066
|
+
type: "git.changes",
|
|
4067
|
+
payload: { files: [], error: err instanceof Error ? err.message : String(err) }
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
async function countUntrackedLines(cwd, relPath) {
|
|
4072
|
+
try {
|
|
4073
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
4074
|
+
const { join: join8 } = await import("path");
|
|
4075
|
+
const abs = cwd ? join8(cwd, relPath) : relPath;
|
|
4076
|
+
const buf = await readFile8(abs);
|
|
4077
|
+
if (buf.includes(0)) return 0;
|
|
4078
|
+
if (buf.length === 0) return 0;
|
|
4079
|
+
let lines = 0;
|
|
4080
|
+
for (let i = 0; i < buf.length; i++) if (buf[i] === 10) lines++;
|
|
4081
|
+
if (buf[buf.length - 1] !== 10) lines++;
|
|
4082
|
+
return lines;
|
|
4083
|
+
} catch {
|
|
4084
|
+
return 0;
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
4088
|
+
async function handleGitDiff(ws, projectRoot, path10) {
|
|
4089
|
+
const cwd = projectRoot || void 0;
|
|
4090
|
+
const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path10, ...extra } });
|
|
4091
|
+
if (!path10 || path10.includes("\0") || path10.includes("..")) {
|
|
4092
|
+
reply({ oldText: "", newText: "", error: "invalid path" });
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
try {
|
|
4096
|
+
const git = makeGit(cwd);
|
|
4097
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
4098
|
+
const { join: join8 } = await import("path");
|
|
4099
|
+
const oldText = await git(["show", `HEAD:${path10}`]);
|
|
4100
|
+
let newText = "";
|
|
4101
|
+
try {
|
|
4102
|
+
const abs = cwd ? join8(cwd, path10) : path10;
|
|
4103
|
+
const buf = await readFile8(abs);
|
|
4104
|
+
if (buf.includes(0)) {
|
|
4105
|
+
reply({ oldText: "", newText: "", binary: true });
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
if (buf.length > MAX_DIFF_BYTES) {
|
|
4109
|
+
reply({ oldText: "", newText: "", tooLarge: true });
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
newText = buf.toString("utf8");
|
|
4113
|
+
} catch {
|
|
4114
|
+
newText = "";
|
|
4115
|
+
}
|
|
4116
|
+
if ((oldText.length || 0) > MAX_DIFF_BYTES) {
|
|
4117
|
+
reply({ oldText: "", newText: "", tooLarge: true });
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
if (oldText.includes("\0")) {
|
|
4121
|
+
reply({ oldText: "", newText: "", binary: true });
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
reply({ oldText, newText });
|
|
4125
|
+
} catch (err) {
|
|
4126
|
+
reply({ oldText: "", newText: "", error: err instanceof Error ? err.message : String(err) });
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4016
4129
|
|
|
4017
4130
|
// src/server/skills-handlers.ts
|
|
4018
|
-
import { promises as
|
|
4019
|
-
import
|
|
4131
|
+
import { promises as fs8 } from "fs";
|
|
4132
|
+
import path8 from "path";
|
|
4020
4133
|
import JSZip from "jszip";
|
|
4021
4134
|
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
4022
4135
|
async function handleSkillsContent(ws, ctx, msg) {
|
|
@@ -4030,26 +4143,26 @@ async function handleSkillsContent(ws, ctx, msg) {
|
|
|
4030
4143
|
return;
|
|
4031
4144
|
}
|
|
4032
4145
|
try {
|
|
4033
|
-
const { name, source } = contentPayload;
|
|
4146
|
+
const { name: name2, source } = contentPayload;
|
|
4034
4147
|
const entries = await ctx.skillLoader.listEntries();
|
|
4035
|
-
const entry = entries.find((e) => e.name.toLowerCase() ===
|
|
4148
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name2.toLowerCase());
|
|
4036
4149
|
if (!entry) {
|
|
4037
|
-
send(ws, { type: "skills.content", payload: { name, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${
|
|
4150
|
+
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
4038
4151
|
return;
|
|
4039
4152
|
}
|
|
4040
|
-
const body = await
|
|
4041
|
-
const skillDir =
|
|
4153
|
+
const body = await fs8.readFile(entry.path, "utf8");
|
|
4154
|
+
const skillDir = path8.dirname(entry.path);
|
|
4042
4155
|
let relatedFiles = [];
|
|
4043
4156
|
try {
|
|
4044
|
-
const files = await
|
|
4045
|
-
relatedFiles = files.filter((f) => f !==
|
|
4157
|
+
const files = await fs8.readdir(skillDir);
|
|
4158
|
+
relatedFiles = files.filter((f) => f !== path8.basename(entry.path)).map((f) => path8.join(skillDir, f));
|
|
4046
4159
|
} catch {
|
|
4047
4160
|
}
|
|
4048
|
-
const nameLower =
|
|
4161
|
+
const nameLower = name2.toLowerCase();
|
|
4049
4162
|
const refResults = await Promise.all(
|
|
4050
4163
|
entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
|
|
4051
4164
|
try {
|
|
4052
|
-
const content = await
|
|
4165
|
+
const content = await fs8.readFile(e.path, "utf8");
|
|
4053
4166
|
return [e.name, content.toLowerCase().includes(nameLower)];
|
|
4054
4167
|
} catch {
|
|
4055
4168
|
return [e.name, false];
|
|
@@ -4057,7 +4170,7 @@ async function handleSkillsContent(ws, ctx, msg) {
|
|
|
4057
4170
|
})
|
|
4058
4171
|
);
|
|
4059
4172
|
const refs = refResults.filter(([, hasRef]) => hasRef).map(([n]) => n);
|
|
4060
|
-
send(ws, { type: "skills.content", payload: { name, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
4173
|
+
send(ws, { type: "skills.content", payload: { name: name2, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
4061
4174
|
} catch (err) {
|
|
4062
4175
|
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
4063
4176
|
}
|
|
@@ -4146,14 +4259,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
|
|
|
4146
4259
|
return;
|
|
4147
4260
|
}
|
|
4148
4261
|
try {
|
|
4149
|
-
const targetDir = createPayload.scope === "global" ?
|
|
4262
|
+
const targetDir = createPayload.scope === "global" ? path8.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path8.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
4150
4263
|
try {
|
|
4151
|
-
await
|
|
4264
|
+
await fs8.access(targetDir);
|
|
4152
4265
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
4153
4266
|
return;
|
|
4154
4267
|
} catch {
|
|
4155
4268
|
}
|
|
4156
|
-
await
|
|
4269
|
+
await fs8.mkdir(targetDir, { recursive: true });
|
|
4157
4270
|
const lines = createPayload.description.trim().split("\n");
|
|
4158
4271
|
const firstLine = lines[0].trim();
|
|
4159
4272
|
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
@@ -4201,13 +4314,13 @@ ${trigger}
|
|
|
4201
4314
|
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
4202
4315
|
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
4203
4316
|
].join("\n");
|
|
4204
|
-
await
|
|
4317
|
+
await fs8.writeFile(path8.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
4205
4318
|
send(ws, {
|
|
4206
4319
|
type: "skills.created",
|
|
4207
4320
|
payload: {
|
|
4208
4321
|
success: true,
|
|
4209
4322
|
error: null,
|
|
4210
|
-
skill: { name: createPayload.name.trim(), path:
|
|
4323
|
+
skill: { name: createPayload.name.trim(), path: path8.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
4211
4324
|
}
|
|
4212
4325
|
});
|
|
4213
4326
|
} catch (err) {
|
|
@@ -4239,7 +4352,7 @@ async function handleSkillsEdit(ws, ctx, msg) {
|
|
|
4239
4352
|
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
4240
4353
|
return;
|
|
4241
4354
|
}
|
|
4242
|
-
await
|
|
4355
|
+
await fs8.writeFile(entry.path, editPayload.body, "utf-8");
|
|
4243
4356
|
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
4244
4357
|
} catch (err) {
|
|
4245
4358
|
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
@@ -4356,6 +4469,21 @@ async function startWebUI(opts = {}) {
|
|
|
4356
4469
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
4357
4470
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
4358
4471
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
4472
|
+
const mcpRegistry = new MCPRegistry({
|
|
4473
|
+
toolRegistry,
|
|
4474
|
+
events,
|
|
4475
|
+
log: logger,
|
|
4476
|
+
// Lazy-connect (per-server `lazy`) manifest cache + default idle auto-sleep.
|
|
4477
|
+
cacheDir: wpaths.cacheDir
|
|
4478
|
+
});
|
|
4479
|
+
if (config.features.mcp && config.mcpServers) {
|
|
4480
|
+
for (const [name2, cfg] of Object.entries(config.mcpServers)) {
|
|
4481
|
+
if (cfg.enabled === false) continue;
|
|
4482
|
+
void mcpRegistry.start({ ...cfg, name: name2 }).catch((err) => {
|
|
4483
|
+
logger.warn(`MCP server "${name2}" failed to start at boot`, err);
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4359
4487
|
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
4360
4488
|
if (!opts.services?.session) {
|
|
4361
4489
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
@@ -4383,7 +4511,7 @@ async function startWebUI(opts = {}) {
|
|
|
4383
4511
|
sessionId: session.id,
|
|
4384
4512
|
projectSlug: wpaths.projectSlug,
|
|
4385
4513
|
projectRoot,
|
|
4386
|
-
projectName:
|
|
4514
|
+
projectName: path9.basename(projectRoot),
|
|
4387
4515
|
workingDir,
|
|
4388
4516
|
clientType: "webui",
|
|
4389
4517
|
pid: process.pid,
|
|
@@ -4439,9 +4567,9 @@ async function startWebUI(opts = {}) {
|
|
|
4439
4567
|
} : void 0;
|
|
4440
4568
|
const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
|
|
4441
4569
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
4442
|
-
manifestPath:
|
|
4443
|
-
projectSkillsDir:
|
|
4444
|
-
globalSkillsDir:
|
|
4570
|
+
manifestPath: path9.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
4571
|
+
projectSkillsDir: path9.join(projectRoot, ".wrongstack", "skills"),
|
|
4572
|
+
globalSkillsDir: path9.join(wstackGlobalRoot2(), "skills"),
|
|
4445
4573
|
projectHash: projectHash(projectRoot),
|
|
4446
4574
|
skillLoader
|
|
4447
4575
|
}) : void 0;
|
|
@@ -4602,7 +4730,7 @@ async function startWebUI(opts = {}) {
|
|
|
4602
4730
|
const write = async () => {
|
|
4603
4731
|
let raw;
|
|
4604
4732
|
try {
|
|
4605
|
-
raw = await
|
|
4733
|
+
raw = await fs9.readFile(globalConfigPath, "utf8");
|
|
4606
4734
|
} catch {
|
|
4607
4735
|
raw = "{}";
|
|
4608
4736
|
}
|
|
@@ -4911,7 +5039,7 @@ async function startWebUI(opts = {}) {
|
|
|
4911
5039
|
inputCost,
|
|
4912
5040
|
outputCost,
|
|
4913
5041
|
cacheReadCost,
|
|
4914
|
-
projectName:
|
|
5042
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
4915
5043
|
projectRoot,
|
|
4916
5044
|
cwd: workingDir,
|
|
4917
5045
|
mode: modeId,
|
|
@@ -5126,33 +5254,33 @@ async function startWebUI(opts = {}) {
|
|
|
5126
5254
|
});
|
|
5127
5255
|
}
|
|
5128
5256
|
async function touchProjectEntry(root, workDir) {
|
|
5129
|
-
const resolved =
|
|
5257
|
+
const resolved = path9.resolve(root);
|
|
5130
5258
|
const manifest = await loadManifest(globalConfigPath);
|
|
5131
5259
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5132
|
-
const existing = manifest.projects.find((p) =>
|
|
5260
|
+
const existing = manifest.projects.find((p) => path9.resolve(p.root) === resolved);
|
|
5133
5261
|
if (existing) {
|
|
5134
5262
|
existing.lastSeen = now;
|
|
5135
|
-
if (workDir) existing.lastWorkingDir =
|
|
5263
|
+
if (workDir) existing.lastWorkingDir = path9.resolve(workDir);
|
|
5136
5264
|
} else {
|
|
5137
5265
|
manifest.projects.push({
|
|
5138
|
-
name:
|
|
5266
|
+
name: path9.basename(resolved),
|
|
5139
5267
|
root: resolved,
|
|
5140
5268
|
slug: generateProjectSlug(resolved),
|
|
5141
5269
|
createdAt: now,
|
|
5142
5270
|
lastSeen: now,
|
|
5143
|
-
lastWorkingDir: workDir ?
|
|
5271
|
+
lastWorkingDir: workDir ? path9.resolve(workDir) : void 0
|
|
5144
5272
|
});
|
|
5145
5273
|
}
|
|
5146
5274
|
await saveManifest(manifest, globalConfigPath);
|
|
5147
5275
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
5148
5276
|
}
|
|
5149
5277
|
function projectsJsonPath(globalConfigPath2) {
|
|
5150
|
-
const base =
|
|
5151
|
-
return
|
|
5278
|
+
const base = path9.dirname(globalConfigPath2);
|
|
5279
|
+
return path9.join(base, "projects.json");
|
|
5152
5280
|
}
|
|
5153
5281
|
async function loadManifest(globalConfigPath2) {
|
|
5154
5282
|
try {
|
|
5155
|
-
const raw = await
|
|
5283
|
+
const raw = await fs9.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
5156
5284
|
const parsed = JSON.parse(raw);
|
|
5157
5285
|
return { projects: parsed.projects ?? [] };
|
|
5158
5286
|
} catch {
|
|
@@ -5161,16 +5289,16 @@ async function startWebUI(opts = {}) {
|
|
|
5161
5289
|
}
|
|
5162
5290
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
5163
5291
|
const file = projectsJsonPath(globalConfigPath2);
|
|
5164
|
-
await
|
|
5165
|
-
await
|
|
5292
|
+
await fs9.mkdir(path9.dirname(file), { recursive: true });
|
|
5293
|
+
await fs9.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
5166
5294
|
}
|
|
5167
5295
|
function generateProjectSlug(rootPath) {
|
|
5168
5296
|
return projectSlug(rootPath);
|
|
5169
5297
|
}
|
|
5170
5298
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
5171
|
-
const base =
|
|
5172
|
-
const dir =
|
|
5173
|
-
await
|
|
5299
|
+
const base = path9.dirname(globalConfigPath2);
|
|
5300
|
+
const dir = path9.join(base, "projects", slug);
|
|
5301
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
5174
5302
|
return dir;
|
|
5175
5303
|
}
|
|
5176
5304
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -5476,27 +5604,17 @@ async function startWebUI(opts = {}) {
|
|
|
5476
5604
|
}
|
|
5477
5605
|
case "provider.models": {
|
|
5478
5606
|
const providerId = msg.payload.providerId;
|
|
5479
|
-
const
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
inputCost: m.cost?.input,
|
|
5491
|
-
outputCost: m.cost?.output,
|
|
5492
|
-
capabilities: [
|
|
5493
|
-
...m.tool_call ? ["tools"] : [],
|
|
5494
|
-
...m.reasoning ? ["reasoning"] : []
|
|
5495
|
-
]
|
|
5496
|
-
}))
|
|
5497
|
-
}
|
|
5498
|
-
});
|
|
5499
|
-
}
|
|
5607
|
+
const saved = await providerHandlers.loadConfigProviders();
|
|
5608
|
+
const cfg = saved[providerId];
|
|
5609
|
+
const catalogId = cfg?.type && cfg.type !== providerId ? cfg.type : providerId;
|
|
5610
|
+
const provider2 = await modelsRegistry.getProvider(catalogId);
|
|
5611
|
+
send(ws, {
|
|
5612
|
+
type: "provider.models",
|
|
5613
|
+
payload: {
|
|
5614
|
+
provider: providerId,
|
|
5615
|
+
models: resolveProviderModelList(cfg?.models, provider2)
|
|
5616
|
+
}
|
|
5617
|
+
});
|
|
5500
5618
|
break;
|
|
5501
5619
|
}
|
|
5502
5620
|
case "model.switch": {
|
|
@@ -5511,7 +5629,7 @@ async function startWebUI(opts = {}) {
|
|
|
5511
5629
|
updateAutoCompactionMaxContext?.(newProv);
|
|
5512
5630
|
try {
|
|
5513
5631
|
const next = configWriteLock.then(async () => {
|
|
5514
|
-
const raw = await
|
|
5632
|
+
const raw = await fs9.readFile(globalConfigPath, "utf8");
|
|
5515
5633
|
const parsed = JSON.parse(raw);
|
|
5516
5634
|
parsed.provider = newProvider;
|
|
5517
5635
|
parsed.model = newModel;
|
|
@@ -5745,27 +5863,28 @@ async function startWebUI(opts = {}) {
|
|
|
5745
5863
|
return handleMemoryRemember(ws, msg, memoryStore);
|
|
5746
5864
|
case "memory.forget":
|
|
5747
5865
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
5748
|
-
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts)
|
|
5866
|
+
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
|
|
5867
|
+
// backed by the live MCPRegistry constructed above. ──
|
|
5749
5868
|
case "mcp.list":
|
|
5750
|
-
return handleMcpList(ws, msg,
|
|
5869
|
+
return handleMcpList(ws, msg, globalConfigPath, mcpRegistry);
|
|
5751
5870
|
case "mcp.add":
|
|
5752
|
-
return handleMcpAdd(ws, msg,
|
|
5871
|
+
return handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry);
|
|
5753
5872
|
case "mcp.remove":
|
|
5754
|
-
return handleMcpRemove(ws, msg,
|
|
5873
|
+
return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
|
|
5755
5874
|
case "mcp.update":
|
|
5756
|
-
return handleMcpUpdate(ws, msg,
|
|
5875
|
+
return handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry);
|
|
5757
5876
|
case "mcp.wake":
|
|
5758
|
-
return handleMcpWake(ws, msg,
|
|
5877
|
+
return handleMcpWake(ws, msg, globalConfigPath, mcpRegistry);
|
|
5759
5878
|
case "mcp.sleep":
|
|
5760
|
-
return handleMcpSleep(ws, msg,
|
|
5879
|
+
return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
|
|
5761
5880
|
case "mcp.discover":
|
|
5762
|
-
return handleMcpDiscover(ws, msg,
|
|
5881
|
+
return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
|
|
5763
5882
|
case "mcp.enable":
|
|
5764
|
-
return handleMcpEnable(ws, msg,
|
|
5883
|
+
return handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry);
|
|
5765
5884
|
case "mcp.disable":
|
|
5766
|
-
return handleMcpDisable(ws, msg,
|
|
5885
|
+
return handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry);
|
|
5767
5886
|
case "mcp.restart":
|
|
5768
|
-
return handleMcpRestart(ws, msg,
|
|
5887
|
+
return handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry);
|
|
5769
5888
|
case "skills.list": {
|
|
5770
5889
|
if (!skillLoader) {
|
|
5771
5890
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -5827,33 +5946,33 @@ async function startWebUI(opts = {}) {
|
|
|
5827
5946
|
break;
|
|
5828
5947
|
}
|
|
5829
5948
|
try {
|
|
5830
|
-
const { name, source } = contentPayload;
|
|
5949
|
+
const { name: name2, source } = contentPayload;
|
|
5831
5950
|
const entries = await skillLoader.listEntries();
|
|
5832
|
-
const entry = entries.find((e) => e.name.toLowerCase() ===
|
|
5951
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name2.toLowerCase());
|
|
5833
5952
|
if (!entry) {
|
|
5834
|
-
send(ws, { type: "skills.content", payload: { name, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${
|
|
5953
|
+
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
5835
5954
|
break;
|
|
5836
5955
|
}
|
|
5837
|
-
const body = await skillLoader.readBody(
|
|
5838
|
-
const skillDir =
|
|
5956
|
+
const body = await skillLoader.readBody(name2);
|
|
5957
|
+
const skillDir = path9.dirname(entry.path);
|
|
5839
5958
|
let relatedFiles = [];
|
|
5840
5959
|
try {
|
|
5841
|
-
const files = await
|
|
5842
|
-
relatedFiles = files.filter((f) => f !==
|
|
5960
|
+
const files = await fs9.readdir(skillDir);
|
|
5961
|
+
relatedFiles = files.filter((f) => f !== path9.basename(entry.path)).map((f) => path9.join(skillDir, f));
|
|
5843
5962
|
} catch {
|
|
5844
5963
|
}
|
|
5845
5964
|
const refs = [];
|
|
5846
5965
|
for (const e of entries) {
|
|
5847
|
-
if (e.name.toLowerCase() ===
|
|
5966
|
+
if (e.name.toLowerCase() === name2.toLowerCase()) continue;
|
|
5848
5967
|
try {
|
|
5849
5968
|
const content = await skillLoader.readBody(e.name);
|
|
5850
|
-
if (content.toLowerCase().includes(
|
|
5969
|
+
if (content.toLowerCase().includes(name2.toLowerCase())) {
|
|
5851
5970
|
refs.push(e.name);
|
|
5852
5971
|
}
|
|
5853
5972
|
} catch {
|
|
5854
5973
|
}
|
|
5855
5974
|
}
|
|
5856
|
-
send(ws, { type: "skills.content", payload: { name, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
5975
|
+
send(ws, { type: "skills.content", payload: { name: name2, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
5857
5976
|
} catch (err) {
|
|
5858
5977
|
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
5859
5978
|
}
|
|
@@ -5946,14 +6065,14 @@ async function startWebUI(opts = {}) {
|
|
|
5946
6065
|
break;
|
|
5947
6066
|
}
|
|
5948
6067
|
try {
|
|
5949
|
-
const targetDir = createPayload.scope === "global" ?
|
|
6068
|
+
const targetDir = createPayload.scope === "global" ? path9.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path9.join(projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
5950
6069
|
try {
|
|
5951
|
-
await
|
|
6070
|
+
await fs9.access(targetDir);
|
|
5952
6071
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
5953
6072
|
break;
|
|
5954
6073
|
} catch {
|
|
5955
6074
|
}
|
|
5956
|
-
await
|
|
6075
|
+
await fs9.mkdir(targetDir, { recursive: true });
|
|
5957
6076
|
const lines = createPayload.description.trim().split("\n");
|
|
5958
6077
|
const firstLine = lines[0].trim();
|
|
5959
6078
|
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
@@ -6001,13 +6120,13 @@ ${trigger}
|
|
|
6001
6120
|
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
6002
6121
|
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
6003
6122
|
].join("\n");
|
|
6004
|
-
await
|
|
6123
|
+
await fs9.writeFile(path9.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
6005
6124
|
send(ws, {
|
|
6006
6125
|
type: "skills.created",
|
|
6007
6126
|
payload: {
|
|
6008
6127
|
success: true,
|
|
6009
6128
|
error: null,
|
|
6010
|
-
skill: { name: createPayload.name.trim(), path:
|
|
6129
|
+
skill: { name: createPayload.name.trim(), path: path9.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
6011
6130
|
}
|
|
6012
6131
|
});
|
|
6013
6132
|
} catch (err) {
|
|
@@ -6040,7 +6159,7 @@ ${trigger}
|
|
|
6040
6159
|
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
6041
6160
|
break;
|
|
6042
6161
|
}
|
|
6043
|
-
await
|
|
6162
|
+
await fs9.writeFile(entry.path, editPayload.body, "utf-8");
|
|
6044
6163
|
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
6045
6164
|
} catch (err) {
|
|
6046
6165
|
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
@@ -6329,6 +6448,14 @@ ${trigger}
|
|
|
6329
6448
|
await handleGitInfo(ws, projectRoot);
|
|
6330
6449
|
break;
|
|
6331
6450
|
}
|
|
6451
|
+
case "git.changes": {
|
|
6452
|
+
await handleGitChanges(ws, projectRoot);
|
|
6453
|
+
break;
|
|
6454
|
+
}
|
|
6455
|
+
case "git.diff": {
|
|
6456
|
+
await handleGitDiff(ws, projectRoot, String(msg.payload?.path ?? ""));
|
|
6457
|
+
break;
|
|
6458
|
+
}
|
|
6332
6459
|
case "webui.shutdown": {
|
|
6333
6460
|
console.log("[WebUI] Shutdown requested from client");
|
|
6334
6461
|
process.kill(process.pid, "SIGINT");
|
|
@@ -6337,7 +6464,7 @@ ${trigger}
|
|
|
6337
6464
|
case "goal.get": {
|
|
6338
6465
|
try {
|
|
6339
6466
|
const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
|
|
6340
|
-
const raw = await
|
|
6467
|
+
const raw = await fs9.readFile(goalPath, "utf8");
|
|
6341
6468
|
const goal = JSON.parse(raw);
|
|
6342
6469
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
6343
6470
|
} catch {
|
|
@@ -6397,7 +6524,7 @@ ${trigger}
|
|
|
6397
6524
|
try {
|
|
6398
6525
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
6399
6526
|
const rewinder = new DefaultSessionRewinder(
|
|
6400
|
-
|
|
6527
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
6401
6528
|
projectRoot
|
|
6402
6529
|
);
|
|
6403
6530
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -6418,7 +6545,7 @@ ${trigger}
|
|
|
6418
6545
|
try {
|
|
6419
6546
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
6420
6547
|
const rewinder = new DefaultSessionRewinder(
|
|
6421
|
-
|
|
6548
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
6422
6549
|
projectRoot
|
|
6423
6550
|
);
|
|
6424
6551
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
@@ -6452,9 +6579,9 @@ ${trigger}
|
|
|
6452
6579
|
case "projects.add": {
|
|
6453
6580
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
6454
6581
|
try {
|
|
6455
|
-
const resolved =
|
|
6456
|
-
await
|
|
6457
|
-
const stat2 = await
|
|
6582
|
+
const resolved = path9.resolve(addRoot);
|
|
6583
|
+
await fs9.access(resolved);
|
|
6584
|
+
const stat2 = await fs9.stat(resolved);
|
|
6458
6585
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
6459
6586
|
const manifest = await loadManifest(globalConfigPath);
|
|
6460
6587
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -6470,26 +6597,26 @@ ${trigger}
|
|
|
6470
6597
|
});
|
|
6471
6598
|
break;
|
|
6472
6599
|
}
|
|
6473
|
-
const
|
|
6600
|
+
const name2 = displayName?.trim() || path9.basename(resolved);
|
|
6474
6601
|
const slug = generateProjectSlug(resolved);
|
|
6475
6602
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
6476
6603
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6477
|
-
manifest.projects.push({ name, root: resolved, slug, lastSeen: now, createdAt: now });
|
|
6604
|
+
manifest.projects.push({ name: name2, root: resolved, slug, lastSeen: now, createdAt: now });
|
|
6478
6605
|
await saveManifest(manifest, globalConfigPath);
|
|
6479
6606
|
send(ws, {
|
|
6480
6607
|
type: "projects.added",
|
|
6481
6608
|
payload: {
|
|
6482
|
-
name,
|
|
6609
|
+
name: name2,
|
|
6483
6610
|
root: resolved,
|
|
6484
6611
|
slug,
|
|
6485
|
-
message: `Registered project "${
|
|
6612
|
+
message: `Registered project "${name2}"`
|
|
6486
6613
|
}
|
|
6487
6614
|
});
|
|
6488
6615
|
} catch (err) {
|
|
6489
6616
|
send(ws, {
|
|
6490
6617
|
type: "projects.added",
|
|
6491
6618
|
payload: {
|
|
6492
|
-
name:
|
|
6619
|
+
name: path9.basename(addRoot),
|
|
6493
6620
|
root: addRoot,
|
|
6494
6621
|
slug: "",
|
|
6495
6622
|
message: errMessage(err)
|
|
@@ -6501,17 +6628,17 @@ ${trigger}
|
|
|
6501
6628
|
case "projects.select": {
|
|
6502
6629
|
const { root: selRoot, name: selName } = msg.payload;
|
|
6503
6630
|
try {
|
|
6504
|
-
const resolved =
|
|
6631
|
+
const resolved = path9.resolve(selRoot);
|
|
6505
6632
|
try {
|
|
6506
|
-
await
|
|
6507
|
-
const stat2 = await
|
|
6633
|
+
await fs9.access(resolved);
|
|
6634
|
+
const stat2 = await fs9.stat(resolved);
|
|
6508
6635
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
6509
6636
|
} catch (err) {
|
|
6510
6637
|
send(ws, {
|
|
6511
6638
|
type: "projects.selected",
|
|
6512
6639
|
payload: {
|
|
6513
6640
|
root: selRoot,
|
|
6514
|
-
name: selName ||
|
|
6641
|
+
name: selName || path9.basename(selRoot),
|
|
6515
6642
|
message: `Cannot switch: ${errMessage(err)}`
|
|
6516
6643
|
}
|
|
6517
6644
|
});
|
|
@@ -6523,10 +6650,10 @@ ${trigger}
|
|
|
6523
6650
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
6524
6651
|
entry.lastWorkingDir = resolved;
|
|
6525
6652
|
} else {
|
|
6526
|
-
const
|
|
6653
|
+
const name2 = selName?.trim() || path9.basename(resolved);
|
|
6527
6654
|
const slug = generateProjectSlug(resolved);
|
|
6528
6655
|
manifest.projects.push({
|
|
6529
|
-
name,
|
|
6656
|
+
name: name2,
|
|
6530
6657
|
root: resolved,
|
|
6531
6658
|
slug,
|
|
6532
6659
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6564,13 +6691,13 @@ ${trigger}
|
|
|
6564
6691
|
});
|
|
6565
6692
|
} catch {
|
|
6566
6693
|
}
|
|
6567
|
-
const newSessionsDir =
|
|
6568
|
-
|
|
6694
|
+
const newSessionsDir = path9.join(
|
|
6695
|
+
path9.dirname(globalConfigPath),
|
|
6569
6696
|
"projects",
|
|
6570
6697
|
switchSlug,
|
|
6571
6698
|
"sessions"
|
|
6572
6699
|
);
|
|
6573
|
-
await
|
|
6700
|
+
await fs9.mkdir(newSessionsDir, { recursive: true });
|
|
6574
6701
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
6575
6702
|
const oldSessionId = session.id;
|
|
6576
6703
|
try {
|
|
@@ -6602,7 +6729,7 @@ ${trigger}
|
|
|
6602
6729
|
sessionId: session.id,
|
|
6603
6730
|
projectSlug: switchSlug,
|
|
6604
6731
|
projectRoot,
|
|
6605
|
-
projectName:
|
|
6732
|
+
projectName: path9.basename(projectRoot),
|
|
6606
6733
|
workingDir,
|
|
6607
6734
|
clientType: "webui",
|
|
6608
6735
|
pid: process.pid,
|
|
@@ -6614,8 +6741,8 @@ ${trigger}
|
|
|
6614
6741
|
type: "projects.selected",
|
|
6615
6742
|
payload: {
|
|
6616
6743
|
root: resolved,
|
|
6617
|
-
name: selName ||
|
|
6618
|
-
message: `Switched to ${selName ||
|
|
6744
|
+
name: selName || path9.basename(resolved),
|
|
6745
|
+
message: `Switched to ${selName || path9.basename(resolved)}`
|
|
6619
6746
|
}
|
|
6620
6747
|
});
|
|
6621
6748
|
broadcast(clients, {
|
|
@@ -6638,7 +6765,7 @@ ${trigger}
|
|
|
6638
6765
|
type: "projects.selected",
|
|
6639
6766
|
payload: {
|
|
6640
6767
|
root: selRoot,
|
|
6641
|
-
name: selName ||
|
|
6768
|
+
name: selName || path9.basename(selRoot),
|
|
6642
6769
|
message: errMessage(err)
|
|
6643
6770
|
}
|
|
6644
6771
|
});
|
|
@@ -6649,14 +6776,14 @@ ${trigger}
|
|
|
6649
6776
|
case "working_dir.set": {
|
|
6650
6777
|
const { path: newPath } = msg.payload;
|
|
6651
6778
|
try {
|
|
6652
|
-
const resolved =
|
|
6653
|
-
if (!resolved.startsWith(projectRoot +
|
|
6779
|
+
const resolved = path9.resolve(projectRoot, newPath);
|
|
6780
|
+
if (!resolved.startsWith(projectRoot + path9.sep) && resolved !== projectRoot) {
|
|
6654
6781
|
sendResult2(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
6655
6782
|
break;
|
|
6656
6783
|
}
|
|
6657
6784
|
try {
|
|
6658
|
-
await
|
|
6659
|
-
const stat2 = await
|
|
6785
|
+
await fs9.access(resolved);
|
|
6786
|
+
const stat2 = await fs9.stat(resolved);
|
|
6660
6787
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
6661
6788
|
} catch {
|
|
6662
6789
|
sendResult2(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
@@ -6687,24 +6814,24 @@ ${trigger}
|
|
|
6687
6814
|
case "mailbox.messages":
|
|
6688
6815
|
return handleMailboxMessages(
|
|
6689
6816
|
ws,
|
|
6690
|
-
{ projectRoot, globalRoot:
|
|
6817
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
6691
6818
|
msg.payload
|
|
6692
6819
|
);
|
|
6693
6820
|
case "mailbox.agents":
|
|
6694
6821
|
return handleMailboxAgents(
|
|
6695
6822
|
ws,
|
|
6696
|
-
{ projectRoot, globalRoot:
|
|
6823
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
6697
6824
|
msg.payload
|
|
6698
6825
|
);
|
|
6699
6826
|
case "mailbox.clear":
|
|
6700
6827
|
return handleMailboxClear(
|
|
6701
6828
|
ws,
|
|
6702
|
-
{ projectRoot, globalRoot:
|
|
6829
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) }
|
|
6703
6830
|
);
|
|
6704
6831
|
case "mailbox.purge":
|
|
6705
6832
|
return handleMailboxPurge(
|
|
6706
6833
|
ws,
|
|
6707
|
-
{ projectRoot, globalRoot:
|
|
6834
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
6708
6835
|
msg.payload
|
|
6709
6836
|
);
|
|
6710
6837
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
@@ -6783,7 +6910,7 @@ ${trigger}
|
|
|
6783
6910
|
};
|
|
6784
6911
|
const httpServer = createHttpServer({
|
|
6785
6912
|
host: wsHost,
|
|
6786
|
-
distDir:
|
|
6913
|
+
distDir: path9.resolve(import.meta.dirname, "../../dist"),
|
|
6787
6914
|
wsPort,
|
|
6788
6915
|
globalRoot: wpaths.globalRoot,
|
|
6789
6916
|
apiToken: wsToken,
|
|
@@ -6792,7 +6919,7 @@ ${trigger}
|
|
|
6792
6919
|
void fleetBroadcast?.();
|
|
6793
6920
|
}
|
|
6794
6921
|
});
|
|
6795
|
-
const registryBaseDir =
|
|
6922
|
+
const registryBaseDir = path9.dirname(globalConfigPath);
|
|
6796
6923
|
httpServer.listen(httpPort, wsHost, () => {
|
|
6797
6924
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
6798
6925
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -6804,7 +6931,7 @@ ${trigger}
|
|
|
6804
6931
|
wsPort,
|
|
6805
6932
|
host: wsHost,
|
|
6806
6933
|
projectRoot,
|
|
6807
|
-
projectName:
|
|
6934
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
6808
6935
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6809
6936
|
url: `http://${wsHost}:${httpPort}`
|
|
6810
6937
|
},
|
|
@@ -6831,6 +6958,7 @@ ${trigger}
|
|
|
6831
6958
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
6832
6959
|
onShutdown: () => {
|
|
6833
6960
|
brainMonitor.stop();
|
|
6961
|
+
void mcpRegistry.stopAll().catch(() => void 0);
|
|
6834
6962
|
if (disposeEvents) {
|
|
6835
6963
|
disposeEvents();
|
|
6836
6964
|
disposeEvents = null;
|
|
@@ -6865,6 +6993,8 @@ export {
|
|
|
6865
6993
|
handleFilesRead,
|
|
6866
6994
|
handleFilesTree,
|
|
6867
6995
|
handleFilesWrite,
|
|
6996
|
+
handleGitChanges,
|
|
6997
|
+
handleGitDiff,
|
|
6868
6998
|
handleGitInfo,
|
|
6869
6999
|
handleMcpAdd,
|
|
6870
7000
|
handleMcpDisable,
|