my-pi 0.0.8 → 0.0.10

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.
@@ -1,11 +1,13 @@
1
1
  import { InteractiveMode as InteractiveMode$1, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, defineTool, getAgentDir, parseFrontmatter, runPrintMode as runPrintMode$1 } from "@mariozechner/pi-coding-agent";
2
2
  import { cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, join, relative, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { Type } from "@sinclair/typebox";
6
- import { execFileSync, spawn } from "node:child_process";
6
+ import { spawn } from "node:child_process";
7
7
  import { homedir } from "node:os";
8
8
  import { Container, SettingsList, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
+ import { readFile } from "node:fs/promises";
10
+ import { EventEmitter } from "node:events";
9
11
  import { createHash } from "node:crypto";
10
12
  //#region src/extensions/chain.ts
11
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -399,6 +401,13 @@ const BUILTIN_EXTENSIONS = [
399
401
  "preset",
400
402
  "presets"
401
403
  ]
404
+ },
405
+ {
406
+ key: "lsp",
407
+ label: "LSP",
408
+ description: "Language Server Protocol tools (diagnostics, hover, definition, references)",
409
+ cli_flag: "--no-lsp",
410
+ aliases: ["lsp", "language-server"]
402
411
  }
403
412
  ];
404
413
  function get_builtin_extensions_config_path() {
@@ -814,6 +823,1013 @@ ${task || "Continue from where the previous session left off."}
814
823
  });
815
824
  }
816
825
  //#endregion
826
+ //#region src/lsp/client.ts
827
+ var LspClientStartError = class extends Error {
828
+ command;
829
+ args;
830
+ code;
831
+ constructor(message, options) {
832
+ super(message, options.cause ? { cause: options.cause } : void 0);
833
+ this.name = "LspClientStartError";
834
+ this.command = options.command;
835
+ this.args = options.args;
836
+ this.code = options.code;
837
+ }
838
+ };
839
+ var LspClient = class extends EventEmitter {
840
+ #proc = null;
841
+ #options;
842
+ #next_id = 1;
843
+ #pending = /* @__PURE__ */ new Map();
844
+ #buffer = Buffer.alloc(0);
845
+ #initialized = false;
846
+ #open_docs = /* @__PURE__ */ new Map();
847
+ #diagnostics_by_uri = /* @__PURE__ */ new Map();
848
+ #diagnostic_waiters = /* @__PURE__ */ new Set();
849
+ constructor(options) {
850
+ super();
851
+ this.#options = options;
852
+ }
853
+ async start() {
854
+ this.#proc = spawn(this.#options.command, this.#options.args, {
855
+ stdio: [
856
+ "pipe",
857
+ "pipe",
858
+ "pipe"
859
+ ],
860
+ env: process.env
861
+ });
862
+ let start_reject = null;
863
+ const start_failure = new Promise((_, reject) => {
864
+ start_reject = reject;
865
+ });
866
+ const reject_start = (error) => {
867
+ if (!start_reject) return false;
868
+ const reject = start_reject;
869
+ start_reject = null;
870
+ reject(error);
871
+ return true;
872
+ };
873
+ const start_error = (message, cause, code) => new LspClientStartError(message, {
874
+ command: this.#options.command,
875
+ args: this.#options.args,
876
+ cause,
877
+ code
878
+ });
879
+ this.#proc.on("error", (err) => {
880
+ const wrapped = start_error(`Failed to spawn ${this.#options.command}`, err, error_code(err));
881
+ if (!reject_start(wrapped)) this.#emit_error(wrapped);
882
+ });
883
+ this.#proc.on("close", () => {
884
+ if (!this.#initialized) reject_start(start_error(`LSP server ${this.#options.command} closed before initialization`));
885
+ for (const pending of this.#pending.values()) {
886
+ clearTimeout(pending.timer);
887
+ pending.reject(/* @__PURE__ */ new Error("LSP server closed"));
888
+ }
889
+ this.#pending.clear();
890
+ this.#initialized = false;
891
+ this.#proc = null;
892
+ });
893
+ this.#proc.stderr?.on("data", () => {});
894
+ this.#proc.stdout.on("data", (chunk) => {
895
+ this.#buffer = Buffer.concat([this.#buffer, chunk]);
896
+ this.#drain_buffer();
897
+ });
898
+ try {
899
+ await Promise.race([this.#request("initialize", {
900
+ processId: process.pid,
901
+ rootUri: this.#options.root_uri,
902
+ capabilities: {
903
+ textDocument: {
904
+ publishDiagnostics: { relatedInformation: true },
905
+ hover: { contentFormat: ["markdown", "plaintext"] },
906
+ definition: { linkSupport: false },
907
+ references: {},
908
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true }
909
+ },
910
+ workspace: {
911
+ workspaceFolders: true,
912
+ symbol: {}
913
+ }
914
+ },
915
+ workspaceFolders: [{
916
+ uri: this.#options.root_uri,
917
+ name: "workspace"
918
+ }]
919
+ }), start_failure]);
920
+ this.#notify("initialized", {});
921
+ this.#initialized = true;
922
+ start_reject = null;
923
+ } catch (error) {
924
+ await this.stop();
925
+ throw error;
926
+ }
927
+ }
928
+ is_ready() {
929
+ return this.#initialized;
930
+ }
931
+ async ensure_document_open(uri, text) {
932
+ const existing = this.#open_docs.get(uri);
933
+ if (existing) {
934
+ if (existing.text === text) return;
935
+ const next_version = existing.version + 1;
936
+ this.#open_docs.set(uri, {
937
+ version: next_version,
938
+ text
939
+ });
940
+ this.#notify("textDocument/didChange", {
941
+ textDocument: {
942
+ uri,
943
+ version: next_version
944
+ },
945
+ contentChanges: [{ text }]
946
+ });
947
+ return;
948
+ }
949
+ const language_id = this.#options.language_id_for_uri(uri) ?? "plaintext";
950
+ this.#open_docs.set(uri, {
951
+ version: 1,
952
+ text
953
+ });
954
+ this.#notify("textDocument/didOpen", { textDocument: {
955
+ uri,
956
+ languageId: language_id,
957
+ version: 1,
958
+ text
959
+ } });
960
+ }
961
+ async hover(uri, position) {
962
+ return await this.#request("textDocument/hover", {
963
+ textDocument: { uri },
964
+ position
965
+ }) ?? null;
966
+ }
967
+ async definition(uri, position) {
968
+ return normalize_location_result(await this.#request("textDocument/definition", {
969
+ textDocument: { uri },
970
+ position
971
+ }));
972
+ }
973
+ async references(uri, position, include_declaration) {
974
+ return await this.#request("textDocument/references", {
975
+ textDocument: { uri },
976
+ position,
977
+ context: { includeDeclaration: include_declaration }
978
+ }) ?? [];
979
+ }
980
+ async document_symbols(uri) {
981
+ return normalize_document_symbol_result(await this.#request("textDocument/documentSymbol", { textDocument: { uri } }));
982
+ }
983
+ get_diagnostics(uri) {
984
+ return this.#diagnostics_by_uri.get(uri) ?? [];
985
+ }
986
+ async wait_for_diagnostics(uri, timeout_ms = 1500) {
987
+ if (this.#diagnostics_by_uri.has(uri)) return this.get_diagnostics(uri);
988
+ return new Promise((resolve) => {
989
+ let active = true;
990
+ const cleanup = () => {
991
+ if (!active) return;
992
+ active = false;
993
+ this.off("diagnostics", handler);
994
+ clearTimeout(timer);
995
+ this.#diagnostic_waiters.delete(cleanup);
996
+ resolve(this.get_diagnostics(uri));
997
+ };
998
+ const handler = (event_uri) => {
999
+ if (event_uri !== uri) return;
1000
+ cleanup();
1001
+ };
1002
+ const timer = setTimeout(cleanup, timeout_ms);
1003
+ this.on("diagnostics", handler);
1004
+ this.#diagnostic_waiters.add(cleanup);
1005
+ });
1006
+ }
1007
+ async stop() {
1008
+ if (this.#initialized) try {
1009
+ await this.#request("shutdown", null, 1e3);
1010
+ this.#notify("exit", null);
1011
+ } catch {}
1012
+ for (const pending of this.#pending.values()) {
1013
+ clearTimeout(pending.timer);
1014
+ pending.reject(/* @__PURE__ */ new Error("LSP client stopped"));
1015
+ }
1016
+ this.#pending.clear();
1017
+ for (const cleanup of Array.from(this.#diagnostic_waiters)) cleanup();
1018
+ if (this.#proc) {
1019
+ this.#proc.kill();
1020
+ this.#proc = null;
1021
+ }
1022
+ this.#initialized = false;
1023
+ }
1024
+ #request(method, params, timeout_override) {
1025
+ return new Promise((resolve, reject) => {
1026
+ const id = this.#next_id++;
1027
+ const timeout_ms = timeout_override ?? this.#options.request_timeout_ms ?? 3e4;
1028
+ const timer = setTimeout(() => {
1029
+ if (this.#pending.has(id)) {
1030
+ this.#pending.delete(id);
1031
+ reject(/* @__PURE__ */ new Error(`LSP request ${method} timed out`));
1032
+ }
1033
+ }, timeout_ms);
1034
+ this.#pending.set(id, {
1035
+ resolve,
1036
+ reject,
1037
+ timer
1038
+ });
1039
+ try {
1040
+ this.#send({
1041
+ jsonrpc: "2.0",
1042
+ id,
1043
+ method,
1044
+ params
1045
+ });
1046
+ } catch (error) {
1047
+ clearTimeout(timer);
1048
+ this.#pending.delete(id);
1049
+ reject(error);
1050
+ }
1051
+ });
1052
+ }
1053
+ #notify(method, params) {
1054
+ this.#send({
1055
+ jsonrpc: "2.0",
1056
+ method,
1057
+ params
1058
+ });
1059
+ }
1060
+ #send(message) {
1061
+ if (!this.#proc?.stdin?.writable) throw new Error("LSP server not connected");
1062
+ const body = Buffer.from(JSON.stringify(message), "utf8");
1063
+ const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii");
1064
+ this.#proc.stdin.write(Buffer.concat([header, body]));
1065
+ }
1066
+ #emit_error(error) {
1067
+ if (this.listenerCount("error") > 0) this.emit("error", error);
1068
+ }
1069
+ #drain_buffer() {
1070
+ while (true) {
1071
+ const header_end = this.#buffer.indexOf("\r\n\r\n");
1072
+ if (header_end === -1) return;
1073
+ const match = this.#buffer.subarray(0, header_end).toString("ascii").match(/Content-Length:\s*(\d+)/i);
1074
+ if (!match) {
1075
+ this.#buffer = this.#buffer.subarray(header_end + 4);
1076
+ continue;
1077
+ }
1078
+ const length = Number(match[1]);
1079
+ const body_start = header_end + 4;
1080
+ if (this.#buffer.length < body_start + length) return;
1081
+ const body = this.#buffer.subarray(body_start, body_start + length);
1082
+ this.#buffer = this.#buffer.subarray(body_start + length);
1083
+ try {
1084
+ this.#handle_message(JSON.parse(body.toString("utf8")));
1085
+ } catch (error) {
1086
+ this.#emit_error(error);
1087
+ }
1088
+ }
1089
+ }
1090
+ #handle_message(message) {
1091
+ const numeric_id = typeof message.id === "number" ? message.id : typeof message.id === "string" && /^-?\d+$/.test(message.id) ? Number(message.id) : null;
1092
+ if (numeric_id != null && this.#pending.has(numeric_id)) {
1093
+ const pending = this.#pending.get(numeric_id);
1094
+ this.#pending.delete(numeric_id);
1095
+ clearTimeout(pending.timer);
1096
+ if (message.error) pending.reject(/* @__PURE__ */ new Error(`LSP error ${message.error.code}: ${message.error.message}`));
1097
+ else pending.resolve(message.result);
1098
+ return;
1099
+ }
1100
+ if (message.method === "textDocument/publishDiagnostics" && message.params) {
1101
+ const params = message.params;
1102
+ this.#diagnostics_by_uri.set(params.uri, params.diagnostics);
1103
+ this.emit("diagnostics", params.uri);
1104
+ return;
1105
+ }
1106
+ if (message.method && message.id != null) this.#send({
1107
+ jsonrpc: "2.0",
1108
+ id: message.id,
1109
+ result: null
1110
+ });
1111
+ }
1112
+ };
1113
+ function normalize_location_result(result) {
1114
+ if (!result) return [];
1115
+ return (Array.isArray(result) ? result : [result]).map((entry) => {
1116
+ if ("uri" in entry) return entry;
1117
+ return {
1118
+ uri: entry.targetUri,
1119
+ range: entry.targetSelectionRange ?? entry.targetRange
1120
+ };
1121
+ });
1122
+ }
1123
+ function normalize_document_symbol_result(result) {
1124
+ if (!result) return [];
1125
+ if (result.length === 0 || "range" in result[0] && "selectionRange" in result[0]) return result;
1126
+ return result.map((entry) => ({
1127
+ name: entry.name,
1128
+ kind: entry.kind,
1129
+ range: entry.location.range,
1130
+ selectionRange: entry.location.range,
1131
+ containerName: entry.containerName,
1132
+ uri: entry.location.uri
1133
+ }));
1134
+ }
1135
+ function file_path_to_uri(file_path) {
1136
+ return pathToFileURL(file_path).href;
1137
+ }
1138
+ function error_code(error) {
1139
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1140
+ }
1141
+ //#endregion
1142
+ //#region src/lsp/servers.ts
1143
+ const EXTENSION_LANGUAGES = {
1144
+ ".ts": "typescript",
1145
+ ".tsx": "typescript",
1146
+ ".mts": "typescript",
1147
+ ".cts": "typescript",
1148
+ ".js": "typescript",
1149
+ ".jsx": "typescript",
1150
+ ".mjs": "typescript",
1151
+ ".cjs": "typescript",
1152
+ ".py": "python",
1153
+ ".rs": "rust",
1154
+ ".go": "go",
1155
+ ".rb": "ruby",
1156
+ ".java": "java",
1157
+ ".lua": "lua",
1158
+ ".svelte": "svelte"
1159
+ };
1160
+ const LANGUAGE_SERVERS = {
1161
+ typescript: {
1162
+ language: "typescript",
1163
+ command: "typescript-language-server",
1164
+ args: ["--stdio"],
1165
+ install_hint: "Install TypeScript LSP with: pnpm add -D typescript typescript-language-server"
1166
+ },
1167
+ python: {
1168
+ language: "python",
1169
+ command: "pylsp",
1170
+ args: [],
1171
+ install_hint: "Install Python LSP with: pip install python-lsp-server"
1172
+ },
1173
+ rust: {
1174
+ language: "rust",
1175
+ command: "rust-analyzer",
1176
+ args: [],
1177
+ install_hint: "Install Rust Analyzer and ensure the rust-analyzer binary is on PATH."
1178
+ },
1179
+ go: {
1180
+ language: "go",
1181
+ command: "gopls",
1182
+ args: ["serve"],
1183
+ install_hint: "Install Go LSP with: go install golang.org/x/tools/gopls@latest"
1184
+ },
1185
+ ruby: {
1186
+ language: "ruby",
1187
+ command: "solargraph",
1188
+ args: ["stdio"],
1189
+ install_hint: "Install Ruby LSP with: gem install solargraph"
1190
+ },
1191
+ java: {
1192
+ language: "java",
1193
+ command: "jdtls",
1194
+ args: [],
1195
+ install_hint: "Install Eclipse JDT Language Server and ensure the jdtls binary is on PATH."
1196
+ },
1197
+ lua: {
1198
+ language: "lua",
1199
+ command: "lua-language-server",
1200
+ args: [],
1201
+ install_hint: "Install Lua LSP and ensure the lua-language-server binary is on PATH."
1202
+ },
1203
+ svelte: {
1204
+ language: "svelte",
1205
+ command: "svelteserver",
1206
+ args: ["--stdio"],
1207
+ install_hint: "Install Svelte LSP with: pnpm add -D svelte-language-server (or volta install svelte-language-server)"
1208
+ }
1209
+ };
1210
+ const WORKSPACE_MARKERS = [
1211
+ "svelte.config.js",
1212
+ "svelte.config.ts",
1213
+ "tsconfig.json",
1214
+ "jsconfig.json",
1215
+ "package.json",
1216
+ "pyproject.toml",
1217
+ "Cargo.toml",
1218
+ "go.mod",
1219
+ "Gemfile",
1220
+ "pom.xml",
1221
+ "build.gradle",
1222
+ "build.gradle.kts"
1223
+ ];
1224
+ const REPOSITORY_MARKERS = [
1225
+ "pnpm-workspace.yaml",
1226
+ "package-lock.json",
1227
+ "yarn.lock",
1228
+ "bun.lockb",
1229
+ "bun.lock",
1230
+ ".git"
1231
+ ];
1232
+ function detect_language(file_path) {
1233
+ return EXTENSION_LANGUAGES[extname(file_path).toLowerCase()];
1234
+ }
1235
+ function list_supported_languages() {
1236
+ return Object.keys(LANGUAGE_SERVERS).sort();
1237
+ }
1238
+ function resolve_server_command(command, cwd = process.cwd()) {
1239
+ if (!command) return command;
1240
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) return command;
1241
+ for (const dir of ancestor_directories(cwd)) {
1242
+ const local_bin = resolve_local_binary(dir, command);
1243
+ if (local_bin) return local_bin;
1244
+ }
1245
+ return command;
1246
+ }
1247
+ function get_server_config(language, cwd = process.cwd()) {
1248
+ const base = LANGUAGE_SERVERS[language];
1249
+ if (!base) return void 0;
1250
+ return {
1251
+ ...base,
1252
+ command: resolve_server_command(base.command, cwd)
1253
+ };
1254
+ }
1255
+ function language_id_for_file(file_path) {
1256
+ return detect_language(file_path);
1257
+ }
1258
+ function find_workspace_root(file_path, fallback = process.cwd()) {
1259
+ const start = resolve(dirname(file_path));
1260
+ const project_root = find_nearest_marker_directory(start, WORKSPACE_MARKERS);
1261
+ if (project_root) return project_root;
1262
+ const repo_root = find_nearest_marker_directory(start, REPOSITORY_MARKERS);
1263
+ if (repo_root) return repo_root;
1264
+ return resolve(fallback);
1265
+ }
1266
+ function find_nearest_marker_directory(start, markers) {
1267
+ for (const dir of ancestor_directories(start)) if (markers.some((marker) => existsSync(join(dir, marker)))) return dir;
1268
+ }
1269
+ function ancestor_directories(start) {
1270
+ const dirs = [];
1271
+ let current = resolve(start);
1272
+ while (true) {
1273
+ dirs.push(current);
1274
+ const parent = dirname(current);
1275
+ if (parent === current) break;
1276
+ current = parent;
1277
+ }
1278
+ return dirs;
1279
+ }
1280
+ function resolve_local_binary(directory, command) {
1281
+ return [join(directory, "node_modules", ".bin", command), join(directory, "node_modules", ".bin", `${command}.cmd`)].find((candidate) => existsSync(candidate));
1282
+ }
1283
+ //#endregion
1284
+ //#region src/extensions/lsp.ts
1285
+ var LspToolError = class extends Error {
1286
+ details;
1287
+ constructor(details) {
1288
+ super(details.message);
1289
+ this.name = "LspToolError";
1290
+ this.details = details;
1291
+ }
1292
+ };
1293
+ var LspStartupCancelledError = class extends Error {
1294
+ constructor(language, workspace_root) {
1295
+ super(`Startup cancelled for ${language} LSP in ${workspace_root}`);
1296
+ this.name = "LspStartupCancelledError";
1297
+ }
1298
+ };
1299
+ const SYMBOL_KIND_LABELS = {
1300
+ 2: "module",
1301
+ 3: "namespace",
1302
+ 5: "class",
1303
+ 6: "method",
1304
+ 7: "property",
1305
+ 8: "field",
1306
+ 9: "constructor",
1307
+ 11: "interface",
1308
+ 12: "function",
1309
+ 13: "variable",
1310
+ 14: "constant",
1311
+ 23: "struct",
1312
+ 24: "event"
1313
+ };
1314
+ const SYMBOL_KIND_NAMES = Object.values(SYMBOL_KIND_LABELS);
1315
+ const SYMBOL_KIND_SCHEMA = Type.Union(SYMBOL_KIND_NAMES.map((name) => Type.Literal(name)));
1316
+ function create_lsp_extension(options = {}) {
1317
+ const create_client = options.create_client ?? ((client_options) => new LspClient(client_options));
1318
+ const read_file = options.read_file ?? ((path) => readFile(path, "utf-8"));
1319
+ return async function lsp(pi) {
1320
+ const cwd = options.cwd?.() ?? process.cwd();
1321
+ const clients_by_server = /* @__PURE__ */ new Map();
1322
+ const failed_servers = /* @__PURE__ */ new Map();
1323
+ const starting_servers = /* @__PURE__ */ new Map();
1324
+ const resolve_abs = (file) => isAbsolute(file) ? file : resolve(cwd, file);
1325
+ const server_key = (language, workspace_root) => `${language}\u0000${workspace_root}`;
1326
+ const make_tool_result = (text, details = {}) => ({
1327
+ content: [{
1328
+ type: "text",
1329
+ text
1330
+ }],
1331
+ details
1332
+ });
1333
+ const make_tool_error = (details) => make_tool_result(format_tool_error(details), {
1334
+ ok: false,
1335
+ error: details
1336
+ });
1337
+ const clear_language_state = async (language) => {
1338
+ const states = language ? Array.from(clients_by_server.entries()).filter(([, state]) => state.language === language) : Array.from(clients_by_server.entries());
1339
+ const starting = language ? Array.from(starting_servers.entries()).filter(([key]) => key.startsWith(`${language}\u0000`)) : Array.from(starting_servers.entries());
1340
+ for (const [key, startup] of starting) {
1341
+ startup.cancelled = true;
1342
+ starting_servers.delete(key);
1343
+ }
1344
+ await Promise.allSettled(states.map(([, state]) => state.client.stop()));
1345
+ for (const [key] of states) clients_by_server.delete(key);
1346
+ if (!language) {
1347
+ failed_servers.clear();
1348
+ return;
1349
+ }
1350
+ for (const [key, failure] of failed_servers.entries()) if (failure.language === language) failed_servers.delete(key);
1351
+ };
1352
+ const get_or_start_client = async (file_path) => {
1353
+ const language = detect_language(file_path);
1354
+ if (!language) return void 0;
1355
+ const workspace_root = find_workspace_root(file_path, cwd);
1356
+ const key = server_key(language, workspace_root);
1357
+ const existing = clients_by_server.get(key);
1358
+ if (existing) return existing;
1359
+ const failed = failed_servers.get(key);
1360
+ if (failed) throw new LspToolError(failed);
1361
+ const in_flight = starting_servers.get(key);
1362
+ if (in_flight) return in_flight.promise;
1363
+ const server_config = get_server_config(language, workspace_root);
1364
+ if (!server_config) return void 0;
1365
+ const root_uri = file_path_to_uri(workspace_root);
1366
+ const startup = {
1367
+ cancelled: false,
1368
+ promise: Promise.resolve(void 0)
1369
+ };
1370
+ const start_promise = (async () => {
1371
+ const client = create_client({
1372
+ command: server_config.command,
1373
+ args: server_config.args,
1374
+ root_uri,
1375
+ language_id_for_uri: (uri) => language_id_for_file(uri)
1376
+ });
1377
+ try {
1378
+ await client.start();
1379
+ } catch (error) {
1380
+ if (startup.cancelled) throw new LspStartupCancelledError(language, workspace_root);
1381
+ const failure = to_lsp_tool_error(file_path, language, workspace_root, server_config.command, server_config.install_hint, error);
1382
+ failed_servers.set(key, failure);
1383
+ throw new LspToolError(failure);
1384
+ }
1385
+ if (startup.cancelled) {
1386
+ await Promise.allSettled([client.stop()]);
1387
+ throw new LspStartupCancelledError(language, workspace_root);
1388
+ }
1389
+ const state = {
1390
+ client,
1391
+ language,
1392
+ workspace_root,
1393
+ root_uri,
1394
+ command: server_config.command,
1395
+ install_hint: server_config.install_hint
1396
+ };
1397
+ clients_by_server.set(key, state);
1398
+ failed_servers.delete(key);
1399
+ return state;
1400
+ })();
1401
+ startup.promise = start_promise;
1402
+ starting_servers.set(key, startup);
1403
+ try {
1404
+ return await start_promise;
1405
+ } finally {
1406
+ if (starting_servers.get(key) === startup) starting_servers.delete(key);
1407
+ }
1408
+ };
1409
+ const open_file = async (state, abs_path) => {
1410
+ const text = await read_file(abs_path);
1411
+ const uri = file_path_to_uri(abs_path);
1412
+ await state.client.ensure_document_open(uri, text);
1413
+ return uri;
1414
+ };
1415
+ const get_file_state = async (file) => {
1416
+ const abs = resolve_abs(file);
1417
+ const state = await get_or_start_client(abs);
1418
+ if (!state) return void 0;
1419
+ return {
1420
+ abs,
1421
+ uri: await open_file(state, abs),
1422
+ state
1423
+ };
1424
+ };
1425
+ const resolve_file_state = async (file) => {
1426
+ const abs = resolve_abs(file);
1427
+ try {
1428
+ const result = await get_file_state(abs);
1429
+ if (!result) return {
1430
+ ok: false,
1431
+ error: {
1432
+ kind: "unsupported_language",
1433
+ file: abs,
1434
+ message: `No language server configured for ${abs}`
1435
+ }
1436
+ };
1437
+ return {
1438
+ ok: true,
1439
+ result
1440
+ };
1441
+ } catch (error) {
1442
+ if (error instanceof LspToolError) return {
1443
+ ok: false,
1444
+ error: error.details
1445
+ };
1446
+ return {
1447
+ ok: false,
1448
+ error: {
1449
+ kind: "tool_execution_failed",
1450
+ file: abs,
1451
+ message: error instanceof Error ? error.message : String(error)
1452
+ }
1453
+ };
1454
+ }
1455
+ };
1456
+ const with_file_state = async (file, run) => {
1457
+ const resolved = await resolve_file_state(file);
1458
+ if (!resolved.ok) return make_tool_error(resolved.error);
1459
+ const { result } = resolved;
1460
+ try {
1461
+ return make_tool_result(await run(result), {
1462
+ ok: true,
1463
+ language: result.state.language,
1464
+ command: result.state.command,
1465
+ workspace_root: result.state.workspace_root
1466
+ });
1467
+ } catch (error) {
1468
+ return make_tool_error(to_lsp_tool_error(result.abs, result.state.language, result.state.workspace_root, result.state.command, result.state.install_hint, error));
1469
+ }
1470
+ };
1471
+ pi.registerTool(defineTool({
1472
+ name: "lsp_diagnostics",
1473
+ label: "LSP: diagnostics",
1474
+ description: "Get language server diagnostics (errors, warnings, hints) for a file. Uses the project language server and returns empty output if the file is clean.",
1475
+ parameters: Type.Object({
1476
+ file: Type.String({ description: "Path to the file to check (relative to cwd or absolute)." }),
1477
+ wait_ms: Type.Optional(Type.Number({ description: "Max ms to wait for diagnostics after opening the file. Default 1500." }))
1478
+ }),
1479
+ execute: async (_id, params) => with_file_state(params.file, async (result) => {
1480
+ const diagnostics = await result.state.client.wait_for_diagnostics(result.uri, params.wait_ms ?? 1500);
1481
+ return format_diagnostics(result.abs, diagnostics);
1482
+ })
1483
+ }));
1484
+ pi.registerTool(defineTool({
1485
+ name: "lsp_diagnostics_many",
1486
+ label: "LSP: diagnostics many",
1487
+ description: "Get language server diagnostics for multiple files in one call. Useful for package-level sweeps and summarization.",
1488
+ parameters: Type.Object({
1489
+ files: Type.Array(Type.String(), {
1490
+ minItems: 1,
1491
+ maxItems: 100,
1492
+ description: "Files to check (relative to cwd or absolute)."
1493
+ }),
1494
+ wait_ms: Type.Optional(Type.Number({ description: "Max ms to wait for diagnostics after opening each file. Default 1500." }))
1495
+ }),
1496
+ execute: async (_id, params) => {
1497
+ const results = await Promise.all(params.files.map((file) => resolve_file_state(file)));
1498
+ const wait_ms = params.wait_ms ?? 1500;
1499
+ const lines_with_stats = await Promise.all(results.map(async (resolved) => {
1500
+ if (!resolved.ok) return {
1501
+ line: format_tool_error(resolved.error),
1502
+ diagnostics: 0,
1503
+ error: true
1504
+ };
1505
+ const diagnostics = await resolved.result.state.client.wait_for_diagnostics(resolved.result.uri, wait_ms);
1506
+ return {
1507
+ line: format_diagnostics(resolved.result.abs, diagnostics),
1508
+ diagnostics: diagnostics.length,
1509
+ error: false
1510
+ };
1511
+ }));
1512
+ let diagnostic_count = 0;
1513
+ let clean_count = 0;
1514
+ let error_count = 0;
1515
+ const lines = [];
1516
+ for (const entry of lines_with_stats) {
1517
+ lines.push(entry.line);
1518
+ if (entry.error) error_count += 1;
1519
+ else {
1520
+ diagnostic_count += entry.diagnostics;
1521
+ if (entry.diagnostics === 0) clean_count += 1;
1522
+ }
1523
+ }
1524
+ return make_tool_result([`Checked ${params.files.length} file(s): ${diagnostic_count} diagnostic(s), ${clean_count} clean, ${error_count} error(s)`, ...lines].join("\n\n"), {
1525
+ ok: error_count === 0,
1526
+ checked: params.files.length,
1527
+ diagnostic_count,
1528
+ clean_count,
1529
+ error_count
1530
+ });
1531
+ }
1532
+ }));
1533
+ pi.registerTool(defineTool({
1534
+ name: "lsp_find_symbol",
1535
+ label: "LSP: find symbol",
1536
+ description: "Find symbols in a file by name or detail text using document symbols. Supports exact matching, kind filters, and top-level-only mode.",
1537
+ parameters: Type.Object({
1538
+ file: Type.String(),
1539
+ query: Type.String({ description: "Substring to match against symbol names/details." }),
1540
+ max_results: Type.Optional(Type.Number({ description: "Max number of matches to return. Default 20." })),
1541
+ top_level_only: Type.Optional(Type.Boolean({ description: "Only match top-level symbols. Default false." })),
1542
+ exact_match: Type.Optional(Type.Boolean({ description: "Match whole symbol names/details exactly instead of substring matching. Default false." })),
1543
+ kinds: Type.Optional(Type.Array(SYMBOL_KIND_SCHEMA, {
1544
+ minItems: 1,
1545
+ maxItems: SYMBOL_KIND_NAMES.length,
1546
+ description: "Restrict matches to these symbol kinds."
1547
+ }))
1548
+ }),
1549
+ execute: async (_id, params) => with_file_state(params.file, async (result) => {
1550
+ const matches = find_symbol_matches(await result.state.client.document_symbols(result.uri), params.query, {
1551
+ max_results: params.max_results ?? 20,
1552
+ top_level_only: params.top_level_only ?? false,
1553
+ exact_match: params.exact_match ?? false,
1554
+ kinds: new Set(params.kinds ?? [])
1555
+ });
1556
+ return format_symbol_matches(result.abs, params.query, matches);
1557
+ })
1558
+ }));
1559
+ pi.registerTool(defineTool({
1560
+ name: "lsp_hover",
1561
+ label: "LSP: hover",
1562
+ description: "Get hover info (types, docs) at a position in a file. Positions are zero-based.",
1563
+ parameters: Type.Object({
1564
+ file: Type.String(),
1565
+ line: Type.Number({ description: "Zero-based line number." }),
1566
+ character: Type.Number({ description: "Zero-based character offset." })
1567
+ }),
1568
+ execute: async (_id, params) => with_file_state(params.file, async (result) => {
1569
+ return format_hover(await result.state.client.hover(result.uri, {
1570
+ line: params.line,
1571
+ character: params.character
1572
+ }));
1573
+ })
1574
+ }));
1575
+ pi.registerTool(defineTool({
1576
+ name: "lsp_definition",
1577
+ label: "LSP: go to definition",
1578
+ description: "Find definition locations for the symbol at a position. Positions are zero-based.",
1579
+ parameters: Type.Object({
1580
+ file: Type.String(),
1581
+ line: Type.Number(),
1582
+ character: Type.Number()
1583
+ }),
1584
+ execute: async (_id, params) => with_file_state(params.file, async (result) => {
1585
+ return format_locations(await result.state.client.definition(result.uri, {
1586
+ line: params.line,
1587
+ character: params.character
1588
+ }), "No definition found.");
1589
+ })
1590
+ }));
1591
+ pi.registerTool(defineTool({
1592
+ name: "lsp_references",
1593
+ label: "LSP: find references",
1594
+ description: "Find references to the symbol at a position. Positions are zero-based.",
1595
+ parameters: Type.Object({
1596
+ file: Type.String(),
1597
+ line: Type.Number(),
1598
+ character: Type.Number(),
1599
+ include_declaration: Type.Optional(Type.Boolean())
1600
+ }),
1601
+ execute: async (_id, params) => with_file_state(params.file, async (result) => {
1602
+ return format_locations(await result.state.client.references(result.uri, {
1603
+ line: params.line,
1604
+ character: params.character
1605
+ }, params.include_declaration ?? true), "No references found.");
1606
+ })
1607
+ }));
1608
+ pi.registerTool(defineTool({
1609
+ name: "lsp_document_symbols",
1610
+ label: "LSP: document symbols",
1611
+ description: "List symbols in a file (functions, classes, variables) using the language server.",
1612
+ parameters: Type.Object({ file: Type.String() }),
1613
+ execute: async (_id, params) => with_file_state(params.file, async (result) => {
1614
+ const symbols = await result.state.client.document_symbols(result.uri);
1615
+ return format_document_symbols(result.abs, symbols);
1616
+ })
1617
+ }));
1618
+ pi.registerCommand("lsp", {
1619
+ description: "Show or manage language server state",
1620
+ getArgumentCompletions: (prefix) => {
1621
+ const parts = prefix.trim().split(/\s+/);
1622
+ const subcommands = [
1623
+ "status",
1624
+ "list",
1625
+ "restart"
1626
+ ];
1627
+ if (!prefix.trim()) return subcommands.map((value) => ({
1628
+ value,
1629
+ label: value
1630
+ }));
1631
+ if (parts.length <= 1) return subcommands.filter((value) => value.startsWith(parts[0])).map((value) => ({
1632
+ value,
1633
+ label: value
1634
+ }));
1635
+ if (parts[0] === "restart") {
1636
+ const candidate = parts[1] ?? "";
1637
+ return ["all", ...list_supported_languages()].filter((value) => value.startsWith(candidate)).map((value) => ({
1638
+ value: `restart ${value}`,
1639
+ label: value
1640
+ }));
1641
+ }
1642
+ return null;
1643
+ },
1644
+ handler: async (args, ctx) => {
1645
+ await handle_lsp_command(args, ctx, cwd, clients_by_server, failed_servers, clear_language_state);
1646
+ }
1647
+ });
1648
+ pi.on("session_shutdown", async () => {
1649
+ await clear_language_state();
1650
+ });
1651
+ };
1652
+ }
1653
+ var lsp_default = create_lsp_extension();
1654
+ async function handle_lsp_command(args, ctx, cwd, clients_by_server, failed_servers, clear_language_state) {
1655
+ const [subcommand = "status", target] = args.trim() ? args.trim().split(/\s+/, 2) : [];
1656
+ switch (subcommand) {
1657
+ case "status":
1658
+ case "list":
1659
+ ctx.ui.notify(format_status_lines(cwd, clients_by_server, failed_servers).join("\n"));
1660
+ return;
1661
+ case "restart":
1662
+ if (!target || target === "all") {
1663
+ await clear_language_state();
1664
+ ctx.ui.notify("Restarted all language server state.");
1665
+ return;
1666
+ }
1667
+ if (!list_supported_languages().includes(target)) {
1668
+ ctx.ui.notify(`Unknown language: ${target}. Use one of: ${list_supported_languages().join(", ")}`, "warning");
1669
+ return;
1670
+ }
1671
+ await clear_language_state(target);
1672
+ ctx.ui.notify(`Restarted ${target} language server state.`);
1673
+ return;
1674
+ default: ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: status, list, restart`, "warning");
1675
+ }
1676
+ }
1677
+ function format_status_lines(cwd, clients_by_server, failed_servers) {
1678
+ const lines = [];
1679
+ const active_languages = /* @__PURE__ */ new Set();
1680
+ const running_states = Array.from(clients_by_server.values()).sort((a, b) => a.language.localeCompare(b.language) || a.workspace_root.localeCompare(b.workspace_root));
1681
+ for (const running of running_states) {
1682
+ active_languages.add(running.language);
1683
+ lines.push(`${running.language}: running (ready=${running.client.is_ready()}) — ${running.command} [workspace ${running.workspace_root}]`);
1684
+ }
1685
+ const failures = Array.from(failed_servers.values()).sort((a, b) => (a.language ?? "").localeCompare(b.language ?? "") || (a.workspace_root ?? "").localeCompare(b.workspace_root ?? ""));
1686
+ for (const failure of failures) {
1687
+ if (failure.language) active_languages.add(failure.language);
1688
+ const workspace = failure.workspace_root ? ` [workspace ${failure.workspace_root}]` : "";
1689
+ const language = failure.language ?? "unknown";
1690
+ lines.push(`${language}: failed — ${failure.message}${workspace}`);
1691
+ }
1692
+ for (const language of list_supported_languages()) {
1693
+ if (active_languages.has(language)) continue;
1694
+ const config = get_server_config(language, cwd);
1695
+ if (config) lines.push(`${language}: idle — ${config.command}`);
1696
+ }
1697
+ return lines.length > 0 ? lines : ["No language servers configured for this project."];
1698
+ }
1699
+ function to_lsp_tool_error(file, language, workspace_root, command, install_hint, error) {
1700
+ if (error instanceof LspToolError) return error.details;
1701
+ if (error instanceof LspClientStartError) {
1702
+ const missing_binary = error.code === "ENOENT";
1703
+ return {
1704
+ kind: "server_start_failed",
1705
+ file,
1706
+ language,
1707
+ workspace_root,
1708
+ command,
1709
+ install_hint,
1710
+ code: error.code,
1711
+ message: missing_binary ? `command "${command}" not found` : error.message
1712
+ };
1713
+ }
1714
+ return {
1715
+ kind: "tool_execution_failed",
1716
+ file,
1717
+ language,
1718
+ workspace_root,
1719
+ command,
1720
+ install_hint,
1721
+ message: error instanceof Error ? error.message : String(error),
1722
+ code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0
1723
+ };
1724
+ }
1725
+ function format_tool_error(details) {
1726
+ if (details.kind === "unsupported_language") return details.message;
1727
+ const lines = [details.language ? `${details.language} LSP unavailable for ${details.file}` : `LSP request failed for ${details.file}`, `Reason: ${details.message}`];
1728
+ if (details.command) lines.push(`Command: ${details.command}`);
1729
+ if (details.workspace_root) lines.push(`Workspace: ${details.workspace_root}`);
1730
+ if (details.install_hint) lines.push(`Hint: ${details.install_hint}`);
1731
+ return lines.join("\n");
1732
+ }
1733
+ function severity_label(severity) {
1734
+ switch (severity) {
1735
+ case 1: return "error";
1736
+ case 2: return "warning";
1737
+ case 3: return "info";
1738
+ case 4: return "hint";
1739
+ default: return "info";
1740
+ }
1741
+ }
1742
+ function format_diagnostics(file, diagnostics) {
1743
+ if (diagnostics.length === 0) return `${file}: no diagnostics`;
1744
+ const lines = [`${file}: ${diagnostics.length} diagnostic(s)`];
1745
+ for (const d of diagnostics) {
1746
+ const position = `${d.range.start.line + 1}:${d.range.start.character + 1}`;
1747
+ const source = d.source ? ` [${d.source}]` : "";
1748
+ const code = d.code != null ? ` (${d.code})` : "";
1749
+ lines.push(` ${position} ${severity_label(d.severity)}${source}${code}: ${d.message}`);
1750
+ }
1751
+ return lines.join("\n");
1752
+ }
1753
+ function format_hover(hover) {
1754
+ if (!hover) return "No hover info.";
1755
+ const contents = hover.contents;
1756
+ const extract = (item) => typeof item === "string" ? item : item.value ?? "";
1757
+ if (Array.isArray(contents)) return contents.map(extract).join("\n\n").trim() || "No hover info.";
1758
+ return extract(contents).trim() || "No hover info.";
1759
+ }
1760
+ function format_locations(locations, empty_message) {
1761
+ if (locations.length === 0) return empty_message;
1762
+ return locations.map((loc) => {
1763
+ return `${file_url_to_path_or_value(loc.uri)}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
1764
+ }).join("\n");
1765
+ }
1766
+ function format_document_symbols(file, symbols) {
1767
+ if (symbols.length === 0) return `${file}: no symbols`;
1768
+ const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
1769
+ append_symbol_lines(lines, symbols, 1);
1770
+ return lines.join("\n");
1771
+ }
1772
+ function find_symbol_matches(symbols, query, options) {
1773
+ const normalized = query.trim().toLowerCase();
1774
+ if (!normalized) return [];
1775
+ const matches = [];
1776
+ const matches_query = (symbol) => {
1777
+ const values = [symbol.name, symbol.detail ?? ""].map((value) => value.trim().toLowerCase());
1778
+ return options.exact_match ? values.some((value) => value === normalized) : values.some((value) => value.includes(normalized));
1779
+ };
1780
+ const matches_kind = (symbol) => {
1781
+ if (options.kinds.size === 0) return true;
1782
+ return options.kinds.has(symbol_kind_label(symbol.kind));
1783
+ };
1784
+ const visit = (entries, depth) => {
1785
+ for (const symbol of entries) {
1786
+ if (matches_kind(symbol) && matches_query(symbol)) {
1787
+ matches.push({
1788
+ symbol,
1789
+ depth
1790
+ });
1791
+ if (matches.length >= options.max_results) return;
1792
+ }
1793
+ if (!options.top_level_only && symbol.children?.length) {
1794
+ visit(symbol.children, depth + 1);
1795
+ if (matches.length >= options.max_results) return;
1796
+ }
1797
+ }
1798
+ };
1799
+ visit(symbols, 1);
1800
+ return matches;
1801
+ }
1802
+ function format_symbol_matches(file, query, matches) {
1803
+ if (matches.length === 0) return `${file}: no symbols matching "${query}"`;
1804
+ const lines = [`${file}: ${matches.length} symbol match(es) for "${query}"`];
1805
+ for (const { symbol, depth } of matches) {
1806
+ const indent = " ".repeat(depth);
1807
+ const detail = symbol.detail ? ` — ${symbol.detail}` : "";
1808
+ const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
1809
+ lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
1810
+ }
1811
+ return lines.join("\n");
1812
+ }
1813
+ function append_symbol_lines(lines, symbols, depth) {
1814
+ for (const symbol of symbols) {
1815
+ const indent = " ".repeat(depth);
1816
+ const detail = symbol.detail ? ` — ${symbol.detail}` : "";
1817
+ const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
1818
+ lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
1819
+ if (symbol.children?.length) append_symbol_lines(lines, symbol.children, depth + 1);
1820
+ }
1821
+ }
1822
+ function symbol_kind_label(kind) {
1823
+ return SYMBOL_KIND_LABELS[kind] ?? "symbol";
1824
+ }
1825
+ function file_url_to_path_or_value(uri) {
1826
+ try {
1827
+ return uri.startsWith("file:") ? fileURLToPath(uri) : uri;
1828
+ } catch {
1829
+ return uri;
1830
+ }
1831
+ }
1832
+ //#endregion
817
1833
  //#region src/mcp/client.ts
818
1834
  var McpClient = class {
819
1835
  #proc = null;
@@ -1075,56 +2091,93 @@ function load_mcp_config(cwd) {
1075
2091
  }
1076
2092
  //#endregion
1077
2093
  //#region src/extensions/mcp.ts
2094
+ function remove_server_tools_from_active(pi, tool_names) {
2095
+ const tool_set = new Set(tool_names);
2096
+ pi.setActiveTools(pi.getActiveTools().filter((tool) => !tool_set.has(tool)));
2097
+ }
2098
+ function format_server_status(state) {
2099
+ switch (state.status) {
2100
+ case "connected": return state.enabled ? "enabled" : "disabled";
2101
+ case "connecting": return state.enabled ? "connecting" : "connecting, disabled";
2102
+ case "failed": return state.enabled ? "failed" : "failed, disabled";
2103
+ default: return state.enabled ? "not connected yet" : "disabled";
2104
+ }
2105
+ }
1078
2106
  async function mcp(pi) {
1079
- const cwd = process.cwd();
1080
- const servers = /* @__PURE__ */ new Map();
1081
- const configs = load_mcp_config(cwd);
1082
- const results = await Promise.allSettled(configs.map(async (config) => {
1083
- const client = new McpClient(config);
1084
- await client.connect();
1085
- return {
1086
- config,
1087
- client,
1088
- mcp_tools: await client.listTools()
1089
- };
1090
- }));
1091
- for (const result of results) {
1092
- if (result.status === "rejected") {
1093
- console.error(`MCP server failed: ${result.reason}`);
1094
- continue;
2107
+ const configs = load_mcp_config(process.cwd());
2108
+ const servers = new Map(configs.map((config) => [config.name, {
2109
+ config,
2110
+ tool_names: [],
2111
+ enabled: true,
2112
+ status: "disconnected"
2113
+ }]));
2114
+ const registered_tool_names = /* @__PURE__ */ new Set();
2115
+ const connect_server = async (state) => {
2116
+ if (state.status === "connected") return;
2117
+ if (state.connect_promise) {
2118
+ await state.connect_promise;
2119
+ return;
1095
2120
  }
1096
- const { config, client, mcp_tools } = result.value;
1097
- const tool_names = [];
1098
- for (const mcp_tool of mcp_tools) {
1099
- const tool_name = `mcp__${config.name}__${mcp_tool.name}`;
1100
- tool_names.push(tool_name);
1101
- pi.registerTool(defineTool({
1102
- name: tool_name,
1103
- label: `${config.name}: ${mcp_tool.name}`,
1104
- description: mcp_tool.description || mcp_tool.name,
1105
- parameters: mcp_tool.inputSchema || {
1106
- type: "object",
1107
- properties: {}
1108
- },
1109
- execute: async (_id, params) => {
1110
- const result = await client.callTool(mcp_tool.name, params);
1111
- return {
1112
- content: [{
1113
- type: "text",
1114
- text: result?.content?.map((c) => c.text || "").join("\n") || JSON.stringify(result)
1115
- }],
1116
- details: {}
1117
- };
2121
+ state.connect_promise = (async () => {
2122
+ state.status = "connecting";
2123
+ state.error = void 0;
2124
+ const client = new McpClient(state.config);
2125
+ try {
2126
+ await client.connect();
2127
+ state.client = client;
2128
+ const mcp_tools = await client.listTools();
2129
+ const tool_names = [];
2130
+ for (const mcp_tool of mcp_tools) {
2131
+ const tool_name = `mcp__${state.config.name}__${mcp_tool.name}`;
2132
+ tool_names.push(tool_name);
2133
+ if (registered_tool_names.has(tool_name)) continue;
2134
+ registered_tool_names.add(tool_name);
2135
+ pi.registerTool(defineTool({
2136
+ name: tool_name,
2137
+ label: `${state.config.name}: ${mcp_tool.name}`,
2138
+ description: mcp_tool.description || mcp_tool.name,
2139
+ parameters: mcp_tool.inputSchema || {
2140
+ type: "object",
2141
+ properties: {}
2142
+ },
2143
+ execute: async (_id, params) => {
2144
+ const result = await state.client.callTool(mcp_tool.name, params);
2145
+ return {
2146
+ content: [{
2147
+ type: "text",
2148
+ text: result?.content?.map((c) => c.text || "").join("\n") || JSON.stringify(result)
2149
+ }],
2150
+ details: {}
2151
+ };
2152
+ }
2153
+ }));
1118
2154
  }
1119
- }));
1120
- }
1121
- servers.set(config.name, {
1122
- config,
1123
- client,
1124
- tool_names,
1125
- enabled: true
1126
- });
1127
- }
2155
+ state.tool_names = tool_names;
2156
+ state.status = "connected";
2157
+ if (!state.enabled) remove_server_tools_from_active(pi, state.tool_names);
2158
+ } catch (error) {
2159
+ state.status = "failed";
2160
+ state.error = error instanceof Error ? error.message : String(error);
2161
+ state.client = void 0;
2162
+ await client.disconnect().catch(() => {});
2163
+ console.error(`MCP server failed (${state.config.name}): ${state.error}`);
2164
+ throw error;
2165
+ } finally {
2166
+ state.connect_promise = void 0;
2167
+ }
2168
+ })();
2169
+ await state.connect_promise;
2170
+ };
2171
+ const connect_all_servers = async (options = {}) => {
2172
+ await Promise.allSettled(Array.from(servers.values()).filter((state) => state.enabled).filter((state) => options.include_failed || state.status !== "failed").map((state) => connect_server(state)));
2173
+ };
2174
+ pi.on("session_start", async () => {
2175
+ connect_all_servers();
2176
+ });
2177
+ pi.on("before_agent_start", async (event) => {
2178
+ await connect_all_servers();
2179
+ return event;
2180
+ });
1128
2181
  pi.registerCommand("mcp", {
1129
2182
  description: "Manage MCP servers (list, enable, disable)",
1130
2183
  getArgumentCompletions: (prefix) => {
@@ -1156,10 +2209,7 @@ async function mcp(pi) {
1156
2209
  return;
1157
2210
  }
1158
2211
  const lines = [];
1159
- for (const [sname, state] of servers.entries()) {
1160
- const status = state.enabled ? "enabled" : "disabled";
1161
- lines.push(`${sname} (${status}) — ${state.tool_names.length} tools`);
1162
- }
2212
+ for (const [sname, state] of servers.entries()) lines.push(`${sname} (${format_server_status(state)}) — ${state.tool_names.length} tools${state.error ? ` — ${state.error}` : ""}`);
1163
2213
  ctx.ui.notify(lines.join("\n"));
1164
2214
  break;
1165
2215
  }
@@ -1169,14 +2219,23 @@ async function mcp(pi) {
1169
2219
  ctx.ui.notify(`Unknown server: ${name}`, "warning");
1170
2220
  return;
1171
2221
  }
1172
- if (server.enabled) {
2222
+ if (server.enabled && server.status !== "failed") {
1173
2223
  ctx.ui.notify(`${name} already enabled`);
1174
2224
  return;
1175
2225
  }
1176
2226
  server.enabled = true;
1177
- const active = pi.getActiveTools();
1178
- pi.setActiveTools([...active, ...server.tool_names]);
1179
- ctx.ui.notify(`Enabled ${name}`);
2227
+ if (server.status === "connected") {
2228
+ const active = pi.getActiveTools();
2229
+ pi.setActiveTools([...active, ...server.tool_names]);
2230
+ ctx.ui.notify(`Enabled ${name}`);
2231
+ return;
2232
+ }
2233
+ if (server.status === "failed") {
2234
+ server.status = "disconnected";
2235
+ server.error = void 0;
2236
+ }
2237
+ connect_server(server);
2238
+ ctx.ui.notify(`Enabling ${name} and connecting in background`);
1180
2239
  break;
1181
2240
  }
1182
2241
  case "disable": {
@@ -1190,8 +2249,7 @@ async function mcp(pi) {
1190
2249
  return;
1191
2250
  }
1192
2251
  server.enabled = false;
1193
- const tool_set = new Set(server.tool_names);
1194
- pi.setActiveTools(pi.getActiveTools().filter((t) => !tool_set.has(t)));
2252
+ remove_server_tools_from_active(pi, server.tool_names);
1195
2253
  ctx.ui.notify(`Disabled ${name}`);
1196
2254
  break;
1197
2255
  }
@@ -1200,7 +2258,12 @@ async function mcp(pi) {
1200
2258
  }
1201
2259
  });
1202
2260
  pi.on("session_shutdown", async () => {
1203
- for (const server of servers.values()) await server.client.disconnect();
2261
+ await Promise.allSettled(Array.from(servers.values()).map(async (server) => {
2262
+ await server.connect_promise?.catch(() => {});
2263
+ await server.client?.disconnect();
2264
+ server.client = void 0;
2265
+ if (server.status !== "failed") server.status = "disconnected";
2266
+ }));
1204
2267
  });
1205
2268
  }
1206
2269
  //#endregion
@@ -2021,22 +3084,20 @@ async function prompt_presets(pi) {
2021
3084
  //#endregion
2022
3085
  //#region src/extensions/recall.ts
2023
3086
  const DEFAULT_DB_PATH = join(process.env.HOME, ".pi", "pirecall.db");
2024
- async function recall(pi) {
2025
- if (existsSync(DEFAULT_DB_PATH)) try {
2026
- execFileSync("npx", [
3087
+ function sync_recall_db_in_background() {
3088
+ if (!existsSync(DEFAULT_DB_PATH)) return;
3089
+ try {
3090
+ spawn("npx", [
2027
3091
  "pirecall",
2028
3092
  "sync",
2029
3093
  "--json"
2030
- ], {
2031
- encoding: "utf-8",
2032
- timeout: 3e4,
2033
- stdio: [
2034
- "pipe",
2035
- "pipe",
2036
- "pipe"
2037
- ]
2038
- });
3094
+ ], { stdio: "ignore" }).unref();
2039
3095
  } catch {}
3096
+ }
3097
+ async function recall(pi) {
3098
+ pi.on("session_start", async () => {
3099
+ sync_recall_db_in_background();
3100
+ });
2040
3101
  pi.on("before_agent_start", async (event) => {
2041
3102
  return { systemPrompt: event.systemPrompt + `
2042
3103
 
@@ -2814,7 +3875,8 @@ const BUILTIN_EXTENSION_FACTORIES = {
2814
3875
  "filter-output": filter_output,
2815
3876
  handoff,
2816
3877
  recall,
2817
- "prompt-presets": prompt_presets
3878
+ "prompt-presets": prompt_presets,
3879
+ lsp: lsp_default
2818
3880
  };
2819
3881
  const PACKAGE_THEME_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "themes");
2820
3882
  function get_force_disabled_builtins(options) {
@@ -2826,6 +3888,7 @@ function get_force_disabled_builtins(options) {
2826
3888
  if (!options.handoff) force_disabled.add("handoff");
2827
3889
  if (!options.recall) force_disabled.add("recall");
2828
3890
  if (!options.prompt_presets) force_disabled.add("prompt-presets");
3891
+ if (!options.lsp) force_disabled.add("lsp");
2829
3892
  return force_disabled;
2830
3893
  }
2831
3894
  function create_builtin_extension_factory(key, extension, force_disabled) {
@@ -2847,7 +3910,7 @@ function create_extensions_override(managed_inline_paths) {
2847
3910
  };
2848
3911
  }
2849
3912
  async function create_my_pi(options = {}) {
2850
- const { cwd = process.cwd(), extensions = [], extensionFactories: user_factories = [], mcp = true, skills = true, chain = true, filter_output = true, handoff = true, recall = true, prompt_presets = true, model, system_prompt, append_system_prompt } = options;
3913
+ const { cwd = process.cwd(), extensions = [], extensionFactories: user_factories = [], mcp = true, skills = true, chain = true, filter_output = true, handoff = true, recall = true, prompt_presets = true, lsp = true, model, system_prompt, append_system_prompt } = options;
2851
3914
  const resolved_extensions = extensions.map((p) => resolve(cwd, p));
2852
3915
  const force_disabled = get_force_disabled_builtins({
2853
3916
  mcp,
@@ -2856,7 +3919,8 @@ async function create_my_pi(options = {}) {
2856
3919
  filter_output,
2857
3920
  handoff,
2858
3921
  recall,
2859
- prompt_presets
3922
+ prompt_presets,
3923
+ lsp
2860
3924
  });
2861
3925
  const managed_extension_factories = [create_extensions_extension({ force_disabled }), ...BUILTIN_EXTENSIONS.map((extension) => create_builtin_extension_factory(extension.key, BUILTIN_EXTENSION_FACTORIES[extension.key], force_disabled))];
2862
3926
  const managed_inline_paths = managed_extension_factories.map((_, index) => `<inline:${index + 1}>`);
@@ -2905,4 +3969,4 @@ async function create_my_pi(options = {}) {
2905
3969
  //#endregion
2906
3970
  export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
2907
3971
 
2908
- //# sourceMappingURL=api-BA6XuDOE.js.map
3972
+ //# sourceMappingURL=api-7LVj6kUV.js.map