@supanadit/morse-web 0.13.0 → 0.14.1

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/server.mjs CHANGED
@@ -33,6 +33,7 @@ var CLIENT_MESSAGE_TYPES = [
33
33
  "history/load",
34
34
  "session/list",
35
35
  "commands/refresh",
36
+ "models/refresh",
36
37
  "project/list",
37
38
  "project/open",
38
39
  "model/set",
@@ -79,7 +80,7 @@ function hasKnownType(value, allowed) {
79
80
  }
80
81
 
81
82
  // ../protocol/dist/version.js
82
- var PROTOCOL_VERSION = 21;
83
+ var PROTOCOL_VERSION = 22;
83
84
  var FRONTEND_MANIFEST_FILE = "webview.manifest.json";
84
85
  var DEFAULT_WS_PATH = "/ws";
85
86
 
@@ -169,9 +170,9 @@ var SessionRegistry = class {
169
170
  hotLimit;
170
171
  activeKey;
171
172
  syntheticKeys = 0;
172
- /** Cached snapshot of a never-recording spawn (see `draftDefaults`). */
173
- draftCache;
174
- draftProbe;
173
+ /** Cached snapshots of never-recording spawns, keyed by model (`''` = the default). */
174
+ draftCache = /* @__PURE__ */ new Map();
175
+ draftProbes = /* @__PURE__ */ new Map();
175
176
  constructor(deps) {
176
177
  this.deps = deps;
177
178
  this.hotLimit = Math.max(1, deps.hotLimit ?? DEFAULT_HOT_LIMIT);
@@ -204,24 +205,43 @@ var SessionRegistry = class {
204
205
  hotKeys() {
205
206
  return [...this.recency];
206
207
  }
208
+ /**
209
+ * Forgets the session-less probe caches, so the next `draftDefaults()` runs pi
210
+ * again. The model picker calls it while the panel is still on a draft, where
211
+ * spawning nothing would otherwise freeze the catalog for the host's lifetime.
212
+ */
213
+ refreshDraftDefaults() {
214
+ this.draftCache.clear();
215
+ this.draftProbes.clear();
216
+ }
207
217
  /**
208
218
  * What the backend offers before any session exists: the model catalog,
209
- * thinking levels and commands of an untouched spawn. Probed once per
210
- * registry — never per client — from a gateway that never records itself
211
- * (pi `--no-session`), so the empty state can ship a real model picker
212
- * without creating a session.
219
+ * thinking levels and commands of an untouched spawn. Probed once per model —
220
+ * never per client — from a gateway that never records itself (pi
221
+ * `--no-session`), so the empty state can ship a real model picker without
222
+ * creating a session. Passing `model` asks for that model's thinking levels,
223
+ * which pi reports per current model.
213
224
  */
214
- async draftDefaults() {
215
- if (this.draftCache) {
216
- return this.draftCache;
217
- }
218
- if (!this.draftProbe) {
219
- this.draftProbe = this.probeDraft().catch((error) => {
220
- this.draftProbe = void 0;
225
+ async draftDefaults(model) {
226
+ const key = model ? `${model.provider}/${model.id}` : "";
227
+ const cached = this.draftCache.get(key);
228
+ if (cached) {
229
+ return cached;
230
+ }
231
+ let probe = this.draftProbes.get(key);
232
+ if (!probe) {
233
+ probe = this.probeDraft(model).then((state) => {
234
+ if (state) {
235
+ this.draftCache.set(key, state);
236
+ }
237
+ return state;
238
+ }).catch((error) => {
239
+ this.draftProbes.delete(key);
221
240
  throw error;
222
241
  });
242
+ this.draftProbes.set(key, probe);
223
243
  }
224
- return this.draftProbe;
244
+ return probe;
225
245
  }
226
246
  /** Opens a new (or resumes an existing) session and makes it active. */
227
247
  async open(options = {}) {
@@ -372,8 +392,8 @@ var SessionRegistry = class {
372
392
  this.recency.length = 0;
373
393
  this.listeners.clear();
374
394
  this.activeKey = void 0;
375
- this.draftCache = void 0;
376
- this.draftProbe = void 0;
395
+ this.draftCache.clear();
396
+ this.draftProbes.clear();
377
397
  await Promise.all(sessions.map((session) => {
378
398
  session.detach();
379
399
  return session.gateway.dispose().catch((error) => {
@@ -439,13 +459,16 @@ var SessionRegistry = class {
439
459
  return `${workspace.cwd}#ephemeral-${this.syntheticKeys}`;
440
460
  }
441
461
  /** Asks the factory for a never-recording gateway and snapshots its state. */
442
- async probeDraft() {
462
+ async probeDraft(model) {
443
463
  const factory = this.deps.factory.probeDefaults;
444
464
  if (!factory) {
445
465
  return void 0;
446
466
  }
447
467
  try {
448
- const gateway = await factory.call(this.deps.factory, { workspace: this.deps.defaultWorkspace });
468
+ const gateway = await factory.call(this.deps.factory, {
469
+ workspace: this.deps.defaultWorkspace,
470
+ ...model ? { model } : {}
471
+ });
449
472
  const state = await gateway.state();
450
473
  await gateway.dispose();
451
474
  return state;
@@ -1036,6 +1059,14 @@ function isDefined(value) {
1036
1059
  // ../adapter-pi-rpc/dist/internal/rpc-client.js
1037
1060
  import { spawn } from "node:child_process";
1038
1061
 
1062
+ // ../adapter-pi-rpc/dist/internal/spawn-env.js
1063
+ function cleanSpawnEnv(env = process.env) {
1064
+ const copy = { ...env };
1065
+ delete copy.NODE_CHANNEL_FD;
1066
+ delete copy.NODE_CHANNEL_SERIALIZATION_MODE;
1067
+ return copy;
1068
+ }
1069
+
1039
1070
  // ../adapter-pi-rpc/dist/internal/jsonl-framer.js
1040
1071
  import { StringDecoder } from "node:string_decoder";
1041
1072
  var JsonlFramer = class {
@@ -1089,7 +1120,7 @@ var PiRpcClient = class {
1089
1120
  this.failed = void 0;
1090
1121
  const child = spawn(this.options.command, this.options.args, {
1091
1122
  cwd: this.options.cwd,
1092
- env: this.options.env ?? process.env,
1123
+ env: cleanSpawnEnv(this.options.env),
1093
1124
  stdio: ["pipe", "pipe", "pipe"],
1094
1125
  windowsHide: true
1095
1126
  });
@@ -1378,7 +1409,29 @@ var PiRpcAgent = class {
1378
1409
  provider: model.provider,
1379
1410
  modelId: model.id
1380
1411
  });
1381
- this.patchState({ model: data ? toModelRef(data) : model });
1412
+ const thinking = await this.readThinkingLevels();
1413
+ this.patchState({
1414
+ model: data ? toModelRef(data) : model,
1415
+ ...thinking
1416
+ });
1417
+ }
1418
+ /**
1419
+ * The levels the current model supports, and the level pi settled on (a model
1420
+ * without reasoning resets it to `off`). Returns a patch with nothing to say
1421
+ * when pi did not answer, so the caller keeps the last known levels instead of
1422
+ * inventing the full list — a failed refresh must not make the picker wrong.
1423
+ */
1424
+ async readThinkingLevels() {
1425
+ const [levels, state] = await Promise.all([
1426
+ this.client.request({ type: "get_available_thinking_levels" }).catch(() => void 0),
1427
+ this.client.request({ type: "get_state" }).catch(() => void 0)
1428
+ ]);
1429
+ const available = dedupeThinkingLevels((levels?.levels ?? []).map(toThinkingLevel).filter(isDefined2));
1430
+ const thinkingLevel = state ? toThinkingLevel(state.thinkingLevel) : void 0;
1431
+ return {
1432
+ ...available.length > 0 ? { availableThinkingLevels: available } : {},
1433
+ ...thinkingLevel !== void 0 ? { thinkingLevel } : {}
1434
+ };
1382
1435
  }
1383
1436
  async setThinkingLevel(level) {
1384
1437
  await this.client.request({ type: "set_thinking_level", level });
@@ -1446,6 +1499,22 @@ var PiRpcAgent = class {
1446
1499
  commandContext() {
1447
1500
  return { cwd: this.options.workspace.cwd, env: this.options.env };
1448
1501
  }
1502
+ /**
1503
+ * Re-reads `get_available_models` and reports it the same way a fresh spawn
1504
+ * would. The model picker calls it the moment it opens, so a model added to
1505
+ * `models.json` while this session is warm is there without reopening the
1506
+ * session or restarting the host.
1507
+ */
1508
+ async refreshModels() {
1509
+ if (this.disposed || !this.ready) {
1510
+ return;
1511
+ }
1512
+ const models = await this.client.request({ type: "get_available_models" }).catch(() => void 0);
1513
+ if (models === void 0) {
1514
+ return;
1515
+ }
1516
+ this.patchState({ availableModels: (models.models ?? []).map(toModelRef) });
1517
+ }
1449
1518
  /**
1450
1519
  * Pulls the cumulative token/cost/context totals pi keeps for the session, so
1451
1520
  * the footer can show the same numbers the TUI does. Best-effort: an older pi
@@ -1577,7 +1646,7 @@ function toThinkingLevel(value) {
1577
1646
  if (!value) {
1578
1647
  return void 0;
1579
1648
  }
1580
- return THINKING_LEVELS.find((level) => level === value);
1649
+ return THINKING_LEVELS.find((level) => level === value) ?? value.trim();
1581
1650
  }
1582
1651
  function dedupeThinkingLevels(levels) {
1583
1652
  return [...new Set(levels)];
@@ -1756,7 +1825,7 @@ ${mentions}` : mentions;
1756
1825
  // ../adapter-pi-rpc/dist/internal/resolve-pi.js
1757
1826
  import { accessSync, constants } from "node:fs";
1758
1827
  import { homedir as homedir2 } from "node:os";
1759
- import { delimiter, isAbsolute, join as join2 } from "node:path";
1828
+ import { delimiter, dirname, isAbsolute, join as join2 } from "node:path";
1760
1829
  var PI_INSTALL_COMMAND = "npm install -g @earendil-works/pi-coding-agent";
1761
1830
  function buildRpcArgs(options) {
1762
1831
  const args = ["--mode", "rpc"];
@@ -1794,6 +1863,33 @@ function resolvePi(options = {}) {
1794
1863
  "- NestJS host: env MORSE_PI_PATH (or MORSE_PI_ENTRY for a bundled rpc-entry.js)"
1795
1864
  ].join("\n"), { remedy: { install: PI_INSTALL_COMMAND } });
1796
1865
  }
1866
+ function resolvePiCli(options = {}) {
1867
+ const env = options.env ?? process.env;
1868
+ const configured = options.piPath ?? env.MORSE_PI_PATH;
1869
+ if (configured) {
1870
+ return { command: configured, baseArgs: [], source: "configured" };
1871
+ }
1872
+ const onPath = findOnPath("pi", env);
1873
+ if (onPath) {
1874
+ return { command: onPath, baseArgs: [], source: "path" };
1875
+ }
1876
+ const entry = options.nodeEntryPath ?? env.MORSE_PI_ENTRY;
1877
+ const cli = entry ? siblingCliEntry(entry) : void 0;
1878
+ if (cli) {
1879
+ return { command: process.execPath, baseArgs: [cli], source: "node-entry" };
1880
+ }
1881
+ throw new AgentUnavailableError([
1882
+ "The pi CLI was not found, so Morse cannot run `pi mcp`.",
1883
+ `Install it (\`${PI_INSTALL_COMMAND}\`) or point Morse at it:`,
1884
+ '- VS Code: setting "morse.pi.path"',
1885
+ "- NestJS host: env MORSE_PI_PATH"
1886
+ ].join("\n"), { remedy: { install: PI_INSTALL_COMMAND } });
1887
+ }
1888
+ function siblingCliEntry(entry) {
1889
+ const directory = dirname(entry);
1890
+ const cli = join2(directory, "cli.js");
1891
+ return entry.endsWith("rpc-entry.js") && isExecutableEntry(cli) ? cli : void 0;
1892
+ }
1797
1893
  function findOnPath(binary, env = process.env) {
1798
1894
  const pathValue = env.PATH ?? env.Path ?? "";
1799
1895
  if (pathValue.length === 0) {
@@ -1865,7 +1961,13 @@ var PiRpcAgentFactory = class {
1865
1961
  extraArgs: this.options.extraArgs,
1866
1962
  env: this.options.env
1867
1963
  };
1868
- return this.spawnAgent(resolveOptions, input.workspace);
1964
+ const agent = await this.spawnAgent(resolveOptions, input.workspace);
1965
+ if (input.model) {
1966
+ await agent.setModel(input.model).catch((error) => {
1967
+ this.logger.warn("Could not select the model on the draft probe", error);
1968
+ });
1969
+ }
1970
+ return agent;
1869
1971
  }
1870
1972
  async spawnAgent(resolveOptions, workspace, sessionPath) {
1871
1973
  const spawn2 = resolvePi(resolveOptions);
@@ -1883,9 +1985,425 @@ var PiRpcAgentFactory = class {
1883
1985
  }
1884
1986
  };
1885
1987
 
1988
+ // ../adapter-pi-rpc/dist/pi-mcp.js
1989
+ import { execFile } from "node:child_process";
1990
+ import { homedir as homedir3 } from "node:os";
1991
+ import { join as join3 } from "node:path";
1992
+
1993
+ // ../adapter-pi-rpc/dist/internal/mcp-config.js
1994
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1995
+ import { dirname as dirname2 } from "node:path";
1996
+ var SERVER_NAME = /^[A-Za-z0-9_-]+$/;
1997
+ function isValidMcpServerName(name) {
1998
+ return SERVER_NAME.test(name);
1999
+ }
2000
+ function readMcpFile(path) {
2001
+ if (!existsSync(path)) {
2002
+ return { mcpServers: {} };
2003
+ }
2004
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2005
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2006
+ throw new Error(`${path} is not a JSON object.`);
2007
+ }
2008
+ const servers = parsed.mcpServers;
2009
+ if (servers !== void 0 && (typeof servers !== "object" || servers === null || Array.isArray(servers))) {
2010
+ throw new Error(`${path}: "mcpServers" must be an object.`);
2011
+ }
2012
+ return {
2013
+ ...parsed,
2014
+ mcpServers: servers ?? {}
2015
+ };
2016
+ }
2017
+ function writeMcpFile(path, file) {
2018
+ mkdirSync(dirname2(path), { recursive: true });
2019
+ writeFileSync(path, `${JSON.stringify(file, null, 2)}
2020
+ `, "utf8");
2021
+ }
2022
+ function findMcpServer(path, name) {
2023
+ return readMcpFile(path).mcpServers[name];
2024
+ }
2025
+ function definesMcpServer(entry) {
2026
+ return entry !== void 0 && (entry.command !== void 0 || entry.url !== void 0 || entry.type !== void 0);
2027
+ }
2028
+ function setMcpServerOverrideEnabled(path, name, enabled) {
2029
+ const file = readMcpFile(path);
2030
+ const existing = file.mcpServers[name];
2031
+ if (enabled) {
2032
+ if (!existing) {
2033
+ return;
2034
+ }
2035
+ delete existing.enabled;
2036
+ if (Object.keys(existing).length === 0) {
2037
+ delete file.mcpServers[name];
2038
+ }
2039
+ } else {
2040
+ const entry = definesMcpServer(existing) ? {} : existing ?? {};
2041
+ entry.enabled = false;
2042
+ file.mcpServers[name] = entry;
2043
+ }
2044
+ writeMcpFile(path, file);
2045
+ }
2046
+ function upsertMcpServer(path, name, config) {
2047
+ if (!isValidMcpServerName(name)) {
2048
+ throw new Error(`"${name}" is not a valid MCP server name (letters, digits, "_" and "-").`);
2049
+ }
2050
+ const file = readMcpFile(path);
2051
+ file.mcpServers[name] = config;
2052
+ writeMcpFile(path, file);
2053
+ }
2054
+ function removeMcpServer(path, name) {
2055
+ const file = readMcpFile(path);
2056
+ if (!(name in file.mcpServers)) {
2057
+ return false;
2058
+ }
2059
+ delete file.mcpServers[name];
2060
+ writeMcpFile(path, file);
2061
+ return true;
2062
+ }
2063
+ function setMcpServerEnabled(path, name, enabled) {
2064
+ const file = readMcpFile(path);
2065
+ const server = file.mcpServers[name];
2066
+ if (!server) {
2067
+ return false;
2068
+ }
2069
+ if (enabled) {
2070
+ delete server.enabled;
2071
+ } else {
2072
+ server.enabled = false;
2073
+ }
2074
+ writeMcpFile(path, file);
2075
+ return true;
2076
+ }
2077
+
2078
+ // ../adapter-pi-rpc/dist/pi-mcp.js
2079
+ function parseMcpServerInput(raw) {
2080
+ const args = isRecord(raw) ? raw : {};
2081
+ const input = { name: asString2(args.name) };
2082
+ if (args.scope === "global" || args.scope === "project") {
2083
+ input.scope = args.scope;
2084
+ }
2085
+ if (args.type === "stdio" || args.type === "http") {
2086
+ input.type = args.type;
2087
+ }
2088
+ for (const key of ["command", "cwd", "url", "description"]) {
2089
+ const value = asString2(args[key]);
2090
+ if (value.length > 0) {
2091
+ input[key] = value;
2092
+ }
2093
+ }
2094
+ if (args.exposure === "codemode" || args.exposure === "deferred" || args.exposure === "direct" || args.exposure === "hidden") {
2095
+ input.exposure = args.exposure;
2096
+ }
2097
+ if (Array.isArray(args.args)) {
2098
+ input.args = args.args.filter(isString);
2099
+ }
2100
+ input.env = stringRecord(args.env);
2101
+ input.headers = stringRecord(args.headers);
2102
+ return input;
2103
+ }
2104
+ var DEFAULT_TIMEOUT_MS = 2e4;
2105
+ var PiMcp = class {
2106
+ options;
2107
+ constructor(options = {}) {
2108
+ this.options = options;
2109
+ }
2110
+ /** Connect every enabled server and report what happened. */
2111
+ async status(cwd) {
2112
+ const spawn2 = resolvePiCli(this.options);
2113
+ const { stdout, stderr, failed, timedOut } = await run(spawn2.command, [...spawn2.baseArgs, "mcp", "list", "--json"], cwd, cleanSpawnEnv(this.env()), this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
2114
+ const parsed = parseStatus(stdout);
2115
+ if (!parsed) {
2116
+ const seconds = Math.round((this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 1e3);
2117
+ const detail = timedOut ? `\`pi mcp list\` did not finish within ${seconds}s \u2014 a server is likely hanging (try \`pi mcp list\` in a terminal)` : stderr.trim() || (failed ? `\`pi mcp list\` exited with an error` : "no output");
2118
+ throw new Error(`Could not read the MCP server list: ${detail}`);
2119
+ }
2120
+ return parsed;
2121
+ }
2122
+ async add(input, cwd) {
2123
+ if (!isValidMcpServerName(input.name)) {
2124
+ return {
2125
+ ok: false,
2126
+ message: `"${input.name}" is not a valid server name (letters, digits, "_" and "-").`
2127
+ };
2128
+ }
2129
+ const isHttp = input.type === "http" || input.url !== void 0 && input.command === void 0;
2130
+ const config = this.toConfig(input, isHttp);
2131
+ if (typeof config === "string") {
2132
+ return { ok: false, message: config };
2133
+ }
2134
+ const path = this.configPath(cwd, input.scope);
2135
+ try {
2136
+ upsertMcpServer(path, input.name, config);
2137
+ } catch (error) {
2138
+ return { ok: false, message: describe(error) };
2139
+ }
2140
+ return { ok: true, path };
2141
+ }
2142
+ async remove(name, cwd, scope) {
2143
+ if (scope === "global") {
2144
+ return this.removeFrom(this.globalPath(), name, "global");
2145
+ }
2146
+ if (scope === "project") {
2147
+ return this.removeFrom(this.projectPath(cwd), name, "project");
2148
+ }
2149
+ for (const path of this.configPaths(cwd)) {
2150
+ try {
2151
+ if (removeMcpServer(path, name)) {
2152
+ return { ok: true, path };
2153
+ }
2154
+ } catch (error) {
2155
+ return { ok: false, message: describe(error) };
2156
+ }
2157
+ }
2158
+ return { ok: false, message: `No server named "${name}" is configured.` };
2159
+ }
2160
+ removeFrom(path, name, scope) {
2161
+ try {
2162
+ if (!removeMcpServer(path, name)) {
2163
+ return {
2164
+ ok: false,
2165
+ message: scope === "project" ? `"${name}" is not defined in this project's .pi/mcp.json.` : `"${name}" is not defined in the user mcp.json.`
2166
+ };
2167
+ }
2168
+ } catch (error) {
2169
+ return { ok: false, message: describe(error) };
2170
+ }
2171
+ return { ok: true, path, scope };
2172
+ }
2173
+ /**
2174
+ * Enable or disable a server in one scope.
2175
+ *
2176
+ * Global edits the user file. Project edits the project entry when the project
2177
+ * defines the server, and otherwise writes a project **override** — the way
2178
+ * pi's `/mcp` does "Disable in this project" — so a user-level server can be
2179
+ * turned off in one directory without touching every other project.
2180
+ */
2181
+ async setEnabled(name, enabled, cwd, scope) {
2182
+ if (scope === "global") {
2183
+ return this.setEnabledIn(this.globalPath(), name, enabled, "global");
2184
+ }
2185
+ if (scope === "project") {
2186
+ const projectPath = this.projectPath(cwd);
2187
+ if (definesMcpServer(findMcpServer(projectPath, name))) {
2188
+ return this.setEnabledIn(projectPath, name, enabled, "project");
2189
+ }
2190
+ const known = findMcpServer(projectPath, name) !== void 0 || findMcpServer(this.globalPath(), name) !== void 0;
2191
+ if (!known) {
2192
+ return {
2193
+ ok: false,
2194
+ message: `No server named "${name}" is configured.`
2195
+ };
2196
+ }
2197
+ try {
2198
+ setMcpServerOverrideEnabled(projectPath, name, enabled);
2199
+ } catch (error) {
2200
+ return { ok: false, message: describe(error) };
2201
+ }
2202
+ return { ok: true, path: projectPath, scope: "project", override: true };
2203
+ }
2204
+ for (const path of this.configPaths(cwd)) {
2205
+ try {
2206
+ if (findMcpServer(path, name) === void 0) {
2207
+ continue;
2208
+ }
2209
+ setMcpServerEnabled(path, name, enabled);
2210
+ return { ok: true, path };
2211
+ } catch (error) {
2212
+ return { ok: false, message: describe(error) };
2213
+ }
2214
+ }
2215
+ return { ok: false, message: `No server named "${name}" is configured.` };
2216
+ }
2217
+ setEnabledIn(path, name, enabled, scope) {
2218
+ try {
2219
+ if (findMcpServer(path, name) === void 0) {
2220
+ return {
2221
+ ok: false,
2222
+ message: scope === "project" ? `"${name}" is not defined in this project's .pi/mcp.json.` : `"${name}" is not defined in the user mcp.json.`
2223
+ };
2224
+ }
2225
+ setMcpServerEnabled(path, name, enabled);
2226
+ } catch (error) {
2227
+ return { ok: false, message: describe(error) };
2228
+ }
2229
+ return { ok: true, path, scope };
2230
+ }
2231
+ /** The user-level file first, then the project's — so a project definition wins. */
2232
+ configPaths(cwd) {
2233
+ return [this.projectPath(cwd), this.globalPath()];
2234
+ }
2235
+ configPath(cwd, scope) {
2236
+ return scope === "project" ? this.projectPath(cwd) : this.globalPath();
2237
+ }
2238
+ globalPath() {
2239
+ return join3(this.agentDir(), "mcp.json");
2240
+ }
2241
+ projectPath(cwd) {
2242
+ return join3(cwd, ".pi", "mcp.json");
2243
+ }
2244
+ agentDir() {
2245
+ if (this.options.agentDir) {
2246
+ return this.options.agentDir;
2247
+ }
2248
+ const fromEnv = this.env().PI_CODING_AGENT_DIR;
2249
+ return fromEnv && fromEnv.length > 0 ? fromEnv : join3(homedir3(), ".pi", "agent");
2250
+ }
2251
+ env() {
2252
+ return this.options.env ?? process.env;
2253
+ }
2254
+ /** The stored entry, or an error message when the input is incomplete. */
2255
+ toConfig(input, isHttp) {
2256
+ const config = {};
2257
+ if (isHttp) {
2258
+ const url = input.url?.trim();
2259
+ if (!url) {
2260
+ return 'An HTTP server needs a "url".';
2261
+ }
2262
+ config.type = "http";
2263
+ config.url = url;
2264
+ if (input.headers && Object.keys(input.headers).length > 0) {
2265
+ config.headers = input.headers;
2266
+ }
2267
+ } else {
2268
+ const command = input.command?.trim();
2269
+ if (!command) {
2270
+ return 'A stdio server needs a "command".';
2271
+ }
2272
+ config.type = "stdio";
2273
+ config.command = command;
2274
+ if (input.args && input.args.length > 0) {
2275
+ config.args = input.args;
2276
+ }
2277
+ if (input.cwd?.trim()) {
2278
+ config.cwd = input.cwd.trim();
2279
+ }
2280
+ if (input.env && Object.keys(input.env).length > 0) {
2281
+ config.env = input.env;
2282
+ }
2283
+ }
2284
+ if (input.exposure && input.exposure !== "codemode") {
2285
+ config.exposure = input.exposure;
2286
+ }
2287
+ if (input.description?.trim()) {
2288
+ config.description = input.description.trim();
2289
+ }
2290
+ if (input.enabled === false) {
2291
+ config.enabled = false;
2292
+ }
2293
+ return config;
2294
+ }
2295
+ };
2296
+ function run(command, args, cwd, env, timeoutMs) {
2297
+ return new Promise((resolve6, reject) => {
2298
+ execFile(command, args, { cwd, env, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
2299
+ const err = error;
2300
+ const code = err?.code;
2301
+ if (typeof code === "string") {
2302
+ reject(new Error(`Could not run "${command}": ${stderr.trim() || code}`));
2303
+ return;
2304
+ }
2305
+ resolve6({ stdout, stderr, failed: error !== null, timedOut: err?.killed === true });
2306
+ });
2307
+ });
2308
+ }
2309
+ function parseStatus(stdout) {
2310
+ const trimmed = stdout.trim();
2311
+ if (trimmed.length === 0) {
2312
+ return void 0;
2313
+ }
2314
+ try {
2315
+ const parsed = JSON.parse(trimmed);
2316
+ if (!Array.isArray(parsed.servers)) {
2317
+ return void 0;
2318
+ }
2319
+ return {
2320
+ servers: parsed.servers.map(toServerStatus),
2321
+ errors: Array.isArray(parsed.errors) ? parsed.errors.filter(isString) : [],
2322
+ ...typeof parsed.note === "string" ? { note: parsed.note } : {}
2323
+ };
2324
+ } catch {
2325
+ return void 0;
2326
+ }
2327
+ }
2328
+ function toServerStatus(raw) {
2329
+ const row = typeof raw === "object" && raw !== null ? raw : {};
2330
+ const scope = row.scope;
2331
+ return {
2332
+ name: isString(row.name) ? row.name : "unknown",
2333
+ scope: scope === "project" || scope === "extension" ? scope : "global",
2334
+ source: isString(row.source) ? row.source : "",
2335
+ ...isString(row.override) ? { override: row.override } : {},
2336
+ enabled: row.enabled !== false,
2337
+ exposure: toExposure(row.exposure),
2338
+ transport: isString(row.transport) ? row.transport : "",
2339
+ state: toState(row.state),
2340
+ tools: Array.isArray(row.tools) ? row.tools.filter(isString) : [],
2341
+ ...isRecord(row.toolExposure) ? { toolExposure: toExposureMap(row.toolExposure) } : {},
2342
+ ...typeof row.resources === "number" ? { resources: row.resources } : {},
2343
+ ...typeof row.resourceTemplates === "number" ? { resourceTemplates: row.resourceTemplates } : {},
2344
+ ...isString(row.error) ? { error: row.error } : {}
2345
+ };
2346
+ }
2347
+ function toState(value) {
2348
+ switch (value) {
2349
+ case "connecting":
2350
+ case "connected":
2351
+ case "disconnected":
2352
+ case "needs-auth":
2353
+ case "failed":
2354
+ case "closed":
2355
+ case "disabled":
2356
+ return value;
2357
+ default:
2358
+ return "failed";
2359
+ }
2360
+ }
2361
+ function toExposure(value) {
2362
+ switch (value) {
2363
+ case "codemode":
2364
+ case "deferred":
2365
+ case "direct":
2366
+ case "hidden":
2367
+ return value;
2368
+ default:
2369
+ return "codemode";
2370
+ }
2371
+ }
2372
+ function toExposureMap(value) {
2373
+ const result = {};
2374
+ for (const [key, entry] of Object.entries(value)) {
2375
+ result[key] = toExposure(entry);
2376
+ }
2377
+ return result;
2378
+ }
2379
+ function isString(value) {
2380
+ return typeof value === "string";
2381
+ }
2382
+ function asString2(value) {
2383
+ return typeof value === "string" ? value : "";
2384
+ }
2385
+ function stringRecord(value) {
2386
+ if (!isRecord(value)) {
2387
+ return void 0;
2388
+ }
2389
+ const result = {};
2390
+ for (const [key, entry] of Object.entries(value)) {
2391
+ if (typeof entry === "string") {
2392
+ result[key] = entry;
2393
+ }
2394
+ }
2395
+ return Object.keys(result).length > 0 ? result : void 0;
2396
+ }
2397
+ function isRecord(value) {
2398
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2399
+ }
2400
+ function describe(error) {
2401
+ return error instanceof Error ? error.message : String(error);
2402
+ }
2403
+
1886
2404
  // ../adapter-pi-rpc/dist/pi-rpc-session-catalog.js
1887
2405
  import { open, readdir as readdir2, readFile as readFile2, stat as stat2, unlink } from "node:fs/promises";
1888
- import { join as join3, resolve as resolve2, sep } from "node:path";
2406
+ import { join as join4, resolve as resolve2, sep } from "node:path";
1889
2407
  var TYPE_PREFIX = '{"type":"';
1890
2408
  function typeAt(raw, start, end) {
1891
2409
  if (!raw.startsWith(TYPE_PREFIX, start)) {
@@ -1974,12 +2492,12 @@ var PiRpcSessionCatalog = class {
1974
2492
  if (!bucket.isDirectory()) {
1975
2493
  continue;
1976
2494
  }
1977
- const bucketPath = join3(this.directory, bucket.name);
2495
+ const bucketPath = join4(this.directory, bucket.name);
1978
2496
  for (const entry of await safeReadDir(bucketPath)) {
1979
2497
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
1980
2498
  continue;
1981
2499
  }
1982
- const path = join3(bucketPath, entry.name);
2500
+ const path = join4(bucketPath, entry.name);
1983
2501
  const info = await stat2(path).catch(() => void 0);
1984
2502
  files.push({ path, size: info?.size ?? 0, updatedAt: info?.mtimeMs ?? 0 });
1985
2503
  }
@@ -2131,9 +2649,16 @@ function createPiRpcAdapter(config, logger) {
2131
2649
  env: config.env,
2132
2650
  removeFile: config.removeSessionFile
2133
2651
  });
2652
+ const cliOptions = {
2653
+ piPath: config.piPath,
2654
+ nodeEntryPath: config.nodeEntryPath,
2655
+ env: config.env
2656
+ };
2657
+ const mcp = new PiMcp(cliOptions);
2134
2658
  return {
2135
2659
  factory,
2136
2660
  catalog,
2661
+ mcp,
2137
2662
  describe: () => resolvePi({
2138
2663
  piPath: config.piPath,
2139
2664
  nodeEntryPath: config.nodeEntryPath,
@@ -2141,7 +2666,14 @@ function createPiRpcAdapter(config, logger) {
2141
2666
  noSession: config.noSession,
2142
2667
  extraArgs: config.extraArgs,
2143
2668
  env: config.env
2144
- })
2669
+ }),
2670
+ describeCli: () => {
2671
+ try {
2672
+ return resolvePiCli(cliOptions);
2673
+ } catch {
2674
+ return void 0;
2675
+ }
2676
+ }
2145
2677
  };
2146
2678
  }
2147
2679
 
@@ -2722,6 +3254,7 @@ var HostSessionController = class {
2722
3254
  void this.warmDraft();
2723
3255
  } else {
2724
3256
  await this.runSessionChange(() => this.options.services.registry.activate(existingKey));
3257
+ void this.refreshModels();
2725
3258
  }
2726
3259
  }
2727
3260
  void this.publishLists();
@@ -2814,6 +3347,9 @@ var HostSessionController = class {
2814
3347
  case "commands/refresh":
2815
3348
  await this.refreshCommands();
2816
3349
  return;
3350
+ case "models/refresh":
3351
+ await this.refreshModels();
3352
+ return;
2817
3353
  case "project/list":
2818
3354
  await this.publishProjects();
2819
3355
  return;
@@ -2978,7 +3514,7 @@ var HostSessionController = class {
2978
3514
  /** Fills the draft pickers from the registry's session-less probe. */
2979
3515
  async warmDraft() {
2980
3516
  try {
2981
- this.draftCatalog = await this.options.services.registry.draftDefaults();
3517
+ this.draftCatalog = await this.options.services.registry.draftDefaults(this.draftModel);
2982
3518
  } catch (error) {
2983
3519
  this.agentError = describeError(error);
2984
3520
  this.agentFailure = describeAgentFailure(error, this.options.agentHint);
@@ -3311,6 +3847,7 @@ var HostSessionController = class {
3311
3847
  const match = this.draftCatalog?.availableModels.find((model) => model.provider === provider && model.id === id);
3312
3848
  this.draftModel = match ?? { provider, id, name: id };
3313
3849
  this.emitState();
3850
+ void this.refreshDraftCatalog(this.draftModel);
3314
3851
  return;
3315
3852
  }
3316
3853
  const result = await this.guard(() => this.options.services.chat.setModel({ provider, id, name: id }));
@@ -3318,6 +3855,23 @@ var HostSessionController = class {
3318
3855
  this.emitState();
3319
3856
  }
3320
3857
  }
3858
+ /**
3859
+ * Re-reads the draft catalog for a model picked before any session exists, so
3860
+ * the thinking picker mirrors that model's levels instead of the default
3861
+ * model's. The request is dropped when the reader picks again or a session
3862
+ * opens while the probe is in flight.
3863
+ */
3864
+ async refreshDraftCatalog(model) {
3865
+ try {
3866
+ const catalog = await this.options.services.registry.draftDefaults(model);
3867
+ if (catalog && this.activeKey === void 0 && !this.disposed && this.draftModel === model) {
3868
+ this.draftCatalog = catalog;
3869
+ this.emitState();
3870
+ }
3871
+ } catch (error) {
3872
+ this.options.logger.warn("Could not probe the draft catalog for the picked model", error);
3873
+ }
3874
+ }
3321
3875
  async setThinkingLevel(level) {
3322
3876
  if (this.activeKey === void 0) {
3323
3877
  this.draftThinking = level;
@@ -3350,6 +3904,24 @@ var HostSessionController = class {
3350
3904
  }
3351
3905
  await this.guard(() => gateway.refreshCommands());
3352
3906
  }
3907
+ /**
3908
+ * Re-reads the model catalog on request. A warm session re-asks pi over RPC
3909
+ * (cheap); a draft re-runs the session-less probe, since that is where its
3910
+ * picker's catalog came from and it would otherwise be frozen for the host's
3911
+ * lifetime.
3912
+ */
3913
+ async refreshModels() {
3914
+ if (this.activeKey === void 0) {
3915
+ this.options.services.registry.refreshDraftDefaults();
3916
+ await this.warmDraft();
3917
+ return;
3918
+ }
3919
+ const gateway = this.options.services.registry.active();
3920
+ if (gateway?.refreshModels === void 0) {
3921
+ return;
3922
+ }
3923
+ await this.guard(() => gateway.refreshModels());
3924
+ }
3353
3925
  async runHostCommand(command, args, requestId) {
3354
3926
  const handler = this.options.onHostCommand;
3355
3927
  if (!handler) {
@@ -3705,7 +4277,7 @@ function hintOnly(hint) {
3705
4277
  // ../server/src/internal/http/health.controller.ts
3706
4278
  import { Controller, Get } from "@nestjs/common";
3707
4279
  import { readFile as readFile3 } from "node:fs/promises";
3708
- import { join as join4 } from "node:path";
4280
+ import { join as join5 } from "node:path";
3709
4281
  import { Inject } from "@nestjs/common";
3710
4282
 
3711
4283
  // ../server/src/app/tokens.ts
@@ -3733,7 +4305,7 @@ var HealthController = class {
3733
4305
  };
3734
4306
  }
3735
4307
  async manifest() {
3736
- const raw = await readFile3(join4(this.config.uiDir, "webview.manifest.json"), "utf8").catch(
4308
+ const raw = await readFile3(join5(this.config.uiDir, "webview.manifest.json"), "utf8").catch(
3737
4309
  () => void 0
3738
4310
  );
3739
4311
  return parseFrontendManifest(raw) ?? { name: "unknown", protocolVersion: PROTOCOL_VERSION };
@@ -3811,13 +4383,13 @@ import { spawn as spawnPty } from "node-pty";
3811
4383
  import { Inject as Inject2, Injectable } from "@nestjs/common";
3812
4384
 
3813
4385
  // ../server/src/internal/terminal/terminal-log.ts
3814
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3815
- import { join as join5 } from "node:path";
4386
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
4387
+ import { join as join6 } from "node:path";
3816
4388
  var LOG_DIR = "terminals";
3817
4389
  var LOG_MAX_CHARS = TERMINAL_BUFFER_CHARS * 4;
3818
4390
  function readTerminalLog(dataDir, terminalId) {
3819
4391
  try {
3820
- const raw = readFileSync(logPath(dataDir, terminalId), "utf8");
4392
+ const raw = readFileSync2(logPath(dataDir, terminalId), "utf8");
3821
4393
  return trimTail(raw, LOG_MAX_CHARS);
3822
4394
  } catch {
3823
4395
  return "";
@@ -3828,12 +4400,12 @@ function writeTerminalLog(dataDir, terminalId, text) {
3828
4400
  const target = logPath(dataDir, terminalId);
3829
4401
  const temp = `${target}.${process.pid}.tmp`;
3830
4402
  try {
3831
- mkdirSync(join5(dataDir, LOG_DIR), { recursive: true });
3832
- writeFileSync(temp, trimmed);
4403
+ mkdirSync2(join6(dataDir, LOG_DIR), { recursive: true });
4404
+ writeFileSync2(temp, trimmed);
3833
4405
  renameSync(temp, target);
3834
4406
  } catch {
3835
4407
  try {
3836
- writeFileSync(target, trimmed);
4408
+ writeFileSync2(target, trimmed);
3837
4409
  } catch {
3838
4410
  rmSync(temp, { force: true });
3839
4411
  }
@@ -3847,7 +4419,7 @@ function safeTerminalName(terminalId) {
3847
4419
  return safe.length > 0 ? safe : "terminal";
3848
4420
  }
3849
4421
  function logPath(dataDir, terminalId) {
3850
- return join5(dataDir, LOG_DIR, `${safeTerminalName(terminalId)}.log`);
4422
+ return join6(dataDir, LOG_DIR, `${safeTerminalName(terminalId)}.log`);
3851
4423
  }
3852
4424
 
3853
4425
  // ../server/src/internal/terminal/terminal.service.ts
@@ -4095,9 +4667,9 @@ import { Inject as Inject5 } from "@nestjs/common";
4095
4667
  import { Inject as Inject4, Injectable as Injectable3 } from "@nestjs/common";
4096
4668
 
4097
4669
  // ../server/src/internal/uploads/upload-store.ts
4098
- import { existsSync } from "node:fs";
4670
+ import { existsSync as existsSync2 } from "node:fs";
4099
4671
  import { mkdir, writeFile } from "node:fs/promises";
4100
- import { basename as basename2, isAbsolute as isAbsolute3, join as join6, relative, sep as sep3 } from "node:path";
4672
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join7, relative, sep as sep3 } from "node:path";
4101
4673
  var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
4102
4674
  var DEFAULT_UPLOAD_DIR = ".morse/uploads";
4103
4675
  var UploadError = class extends Error {
@@ -4117,7 +4689,7 @@ async function saveUpload(cwd, request, uploadDir = DEFAULT_UPLOAD_DIR) {
4117
4689
  `That file is ${formatMb(bytes.length)} MB; the limit is ${formatMb(MAX_UPLOAD_BYTES)} MB.`
4118
4690
  );
4119
4691
  }
4120
- const directory = isAbsolute3(uploadDir) ? uploadDir : join6(cwd, uploadDir);
4692
+ const directory = isAbsolute3(uploadDir) ? uploadDir : join7(cwd, uploadDir);
4121
4693
  await mkdir(directory, { recursive: true });
4122
4694
  const target = uniquePath(directory, name);
4123
4695
  await writeFile(target, bytes);
@@ -4136,10 +4708,10 @@ function uniquePath(directory, name) {
4136
4708
  const dot = name.lastIndexOf(".");
4137
4709
  const stem = dot > 0 ? name.slice(0, dot) : name;
4138
4710
  const extension = dot > 0 ? name.slice(dot) : "";
4139
- let candidate = join6(directory, name);
4711
+ let candidate = join7(directory, name);
4140
4712
  let suffix = 1;
4141
- while (existsSync(candidate)) {
4142
- candidate = join6(directory, `${stem}-${suffix}${extension}`);
4713
+ while (existsSync2(candidate)) {
4714
+ candidate = join7(directory, `${stem}-${suffix}${extension}`);
4143
4715
  suffix += 1;
4144
4716
  }
4145
4717
  return candidate;
@@ -4155,8 +4727,8 @@ function formatMb(bytes) {
4155
4727
 
4156
4728
  // ../server/src/internal/workspace/directory-browser.ts
4157
4729
  import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
4158
- import { homedir as homedir3 } from "node:os";
4159
- import { basename as basename3, dirname, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "node:path";
4730
+ import { homedir as homedir4 } from "node:os";
4731
+ import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute4, join as join8, resolve as resolve4 } from "node:path";
4160
4732
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
4161
4733
  "node_modules",
4162
4734
  "dist",
@@ -4180,8 +4752,8 @@ async function browseDirectory(path, options) {
4180
4752
  throw new Error(`A folder path must be absolute: ${requested}`);
4181
4753
  }
4182
4754
  const entries = await readdir3(start, { withFileTypes: true });
4183
- const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path: join7(start, entry.name) })).sort((left, right) => left.name.localeCompare(right.name));
4184
- const parent = dirname(start);
4755
+ const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path: join8(start, entry.name) })).sort((left, right) => left.name.localeCompare(right.name));
4756
+ const parent = dirname3(start);
4185
4757
  return {
4186
4758
  path: start,
4187
4759
  parent: parent === start ? void 0 : parent,
@@ -4195,7 +4767,7 @@ function shouldSkip(name) {
4195
4767
  }
4196
4768
  async function isGitRepo(path) {
4197
4769
  try {
4198
- await stat3(join7(path, ".git"));
4770
+ await stat3(join8(path, ".git"));
4199
4771
  return true;
4200
4772
  } catch {
4201
4773
  return false;
@@ -4204,7 +4776,7 @@ async function isGitRepo(path) {
4204
4776
  function shortcutRoots(options) {
4205
4777
  const seen = /* @__PURE__ */ new Set();
4206
4778
  const roots = [];
4207
- for (const candidate of [...options.roots ?? [], homedir3(), options.defaultPath]) {
4779
+ for (const candidate of [...options.roots ?? [], homedir4(), options.defaultPath]) {
4208
4780
  if (!candidate || !isAbsolute4(candidate)) {
4209
4781
  continue;
4210
4782
  }
@@ -4266,7 +4838,7 @@ function toPosix(path) {
4266
4838
  }
4267
4839
 
4268
4840
  // ../server/src/internal/workspace/git-log.ts
4269
- import { execFile } from "node:child_process";
4841
+ import { execFile as execFile2 } from "node:child_process";
4270
4842
  import { relative as relative3 } from "node:path";
4271
4843
  var DEFAULT_MAX_COMMITS = 250;
4272
4844
  var HARD_MAX_COMMITS = 500;
@@ -4317,9 +4889,9 @@ async function readGitStatus(cwd) {
4317
4889
  gitText(["rev-parse", "--show-prefix"], cwd),
4318
4890
  gitText(["status", "--porcelain=v1", "-z", "--untracked-files=all"], cwd)
4319
4891
  ]);
4320
- return { isRepo: true, files: parseStatus(raw ?? "", prefix?.trim() ?? "") };
4892
+ return { isRepo: true, files: parseStatus2(raw ?? "", prefix?.trim() ?? "") };
4321
4893
  }
4322
- function parseStatus(raw, root) {
4894
+ function parseStatus2(raw, root) {
4323
4895
  const records = raw.split("\0");
4324
4896
  const files = [];
4325
4897
  for (let index = 0; index < records.length; index += 1) {
@@ -4563,7 +5135,7 @@ function parseRefs(raw) {
4563
5135
  }
4564
5136
  function gitText(args, cwd) {
4565
5137
  return new Promise((resolve6) => {
4566
- execFile(
5138
+ execFile2(
4567
5139
  "git",
4568
5140
  args,
4569
5141
  { cwd, timeout: 5e3, maxBuffer: 16 * 1024 * 1024, windowsHide: true },
@@ -4579,7 +5151,7 @@ async function gitRun(args, cwd, timeout = 5e3) {
4579
5151
  }
4580
5152
  function gitExec(args, cwd, timeout = 5e3) {
4581
5153
  return new Promise((resolve6) => {
4582
- execFile(
5154
+ execFile2(
4583
5155
  "git",
4584
5156
  args,
4585
5157
  {
@@ -4607,9 +5179,9 @@ function gitExec(args, cwd, timeout = 5e3) {
4607
5179
  }
4608
5180
 
4609
5181
  // ../server/src/internal/workspace/workspace-index.ts
4610
- import { execFile as execFile2 } from "node:child_process";
5182
+ import { execFile as execFile3 } from "node:child_process";
4611
5183
  import { readdir as readdir4 } from "node:fs/promises";
4612
- import { join as join8, relative as relative4 } from "node:path";
5184
+ import { join as join9, relative as relative4 } from "node:path";
4613
5185
  var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set([
4614
5186
  "node_modules",
4615
5187
  "dist",
@@ -4694,7 +5266,7 @@ async function gitFileList(cwd) {
4694
5266
  }
4695
5267
  function runGit(args, cwd) {
4696
5268
  return new Promise((resolve6) => {
4697
- execFile2(
5269
+ execFile3(
4698
5270
  "git",
4699
5271
  args,
4700
5272
  { cwd, timeout: 5e3, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
@@ -4720,7 +5292,7 @@ async function walk(root, directory, files) {
4720
5292
  if (files.length >= MAX_INDEXED_FILES) {
4721
5293
  return;
4722
5294
  }
4723
- const full = join8(directory, entry.name);
5295
+ const full = join9(directory, entry.name);
4724
5296
  if (entry.isDirectory()) {
4725
5297
  if (SKIPPED_DIRECTORIES2.has(entry.name) || entry.name.startsWith(".")) {
4726
5298
  continue;
@@ -4738,20 +5310,20 @@ function toPosix3(path) {
4738
5310
  }
4739
5311
 
4740
5312
  // ../server/src/internal/workspace/workbench-store.ts
4741
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
4742
- import { join as join9 } from "node:path";
5313
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
5314
+ import { join as join10 } from "node:path";
4743
5315
  var LAYOUT_FILE = "workbench.json";
4744
5316
  var DRAFTS_FILE = "drafts.json";
4745
5317
  var LAYOUT_MAX_BYTES = 256 * 1024;
4746
5318
  var DRAFTS_MAX_BYTES = 32 * 1024 * 1024;
4747
5319
  function readWorkbench(dataDir) {
4748
- return readSlot(join9(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
5320
+ return readSlot(join10(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
4749
5321
  }
4750
5322
  function saveWorkbench(dataDir, snapshot) {
4751
5323
  return saveSlot(dataDir, LAYOUT_FILE, snapshot, LAYOUT_MAX_BYTES);
4752
5324
  }
4753
5325
  function readDrafts(dataDir) {
4754
- return readSlot(join9(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
5326
+ return readSlot(join10(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
4755
5327
  }
4756
5328
  function saveDrafts(dataDir, snapshot) {
4757
5329
  return saveSlot(dataDir, DRAFTS_FILE, snapshot, DRAFTS_MAX_BYTES);
@@ -4759,7 +5331,7 @@ function saveDrafts(dataDir, snapshot) {
4759
5331
  function readSlot(path, maxBytes) {
4760
5332
  let raw;
4761
5333
  try {
4762
- raw = readFileSync2(path, "utf8");
5334
+ raw = readFileSync3(path, "utf8");
4763
5335
  } catch {
4764
5336
  return void 0;
4765
5337
  }
@@ -4783,16 +5355,16 @@ function saveSlot(dataDir, file, snapshot, maxBytes) {
4783
5355
  if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
4784
5356
  throw new UnsupportedByHostError(`That ${file} snapshot is too large to save.`);
4785
5357
  }
4786
- const target = join9(dataDir, file);
5358
+ const target = join10(dataDir, file);
4787
5359
  const temp = `${target}.${process.pid}.tmp`;
4788
5360
  try {
4789
- mkdirSync2(dataDir, { recursive: true });
4790
- writeFileSync2(temp, serialized);
5361
+ mkdirSync3(dataDir, { recursive: true });
5362
+ writeFileSync3(temp, serialized);
4791
5363
  renameSync2(temp, target);
4792
5364
  return true;
4793
5365
  } catch {
4794
5366
  try {
4795
- writeFileSync2(target, serialized);
5367
+ writeFileSync3(target, serialized);
4796
5368
  return true;
4797
5369
  } catch {
4798
5370
  rmSync2(temp, { force: true });
@@ -4814,13 +5386,14 @@ function asSnapshot(value) {
4814
5386
 
4815
5387
  // ../server/src/internal/ws/session-factory.service.ts
4816
5388
  var MorseSessionFactory = class {
4817
- constructor(registry, transcripts, policy, logger, config, terminal) {
5389
+ constructor(registry, transcripts, policy, logger, config, terminal, pi) {
4818
5390
  this.registry = registry;
4819
5391
  this.transcripts = transcripts;
4820
5392
  this.policy = policy;
4821
5393
  this.logger = logger;
4822
5394
  this.config = config;
4823
5395
  this.terminal = terminal;
5396
+ this.pi = pi;
4824
5397
  }
4825
5398
  capabilities() {
4826
5399
  return {
@@ -4865,7 +5438,10 @@ var MorseSessionFactory = class {
4865
5438
  // A page served by this host may ask the registry for the latest release, so
4866
5439
  // the sidebar can say when a newer Morse is out. A host without it stays
4867
5440
  // quiet; this one is on unless `MORSE_UPDATE_CHECK=0` says otherwise.
4868
- updateCheck: this.config.updateCheck
5441
+ updateCheck: this.config.updateCheck,
5442
+ // Managing MCP servers runs the `pi` CLI, so this host offers it only when
5443
+ // it found one. A host without it hides the affordance.
5444
+ mcp: this.pi.describeCli() !== void 0
4869
5445
  };
4870
5446
  }
4871
5447
  createFor(emit) {
@@ -5010,6 +5586,31 @@ var MorseSessionFactory = class {
5010
5586
  const branch = typeof args?.branch === "string" ? args.branch : "";
5011
5587
  return checkoutGit(cwd, branch, args?.create === true);
5012
5588
  }
5589
+ case "mcpStatus": {
5590
+ const cwd = this.requireWritableCwd(context);
5591
+ try {
5592
+ return await this.pi.mcp.status(cwd);
5593
+ } catch (error) {
5594
+ return { servers: [], errors: [describeError2(error)] };
5595
+ }
5596
+ }
5597
+ case "mcpAdd": {
5598
+ const cwd = this.requireWritableCwd(context);
5599
+ return this.pi.mcp.add(parseMcpServerInput(args), cwd);
5600
+ }
5601
+ case "mcpRemove": {
5602
+ const cwd = this.requireWritableCwd(context);
5603
+ return this.pi.mcp.remove(stringArg(args, "name"), cwd, mcpScope(args));
5604
+ }
5605
+ case "mcpSetEnabled": {
5606
+ const cwd = this.requireWritableCwd(context);
5607
+ return this.pi.mcp.setEnabled(
5608
+ stringArg(args, "name"),
5609
+ args?.enabled === true,
5610
+ cwd,
5611
+ mcpScope(args)
5612
+ );
5613
+ }
5013
5614
  default:
5014
5615
  throw new UnsupportedByHostError(`This host does not support "${command}".`);
5015
5616
  }
@@ -5059,7 +5660,8 @@ MorseSessionFactory = __decorateClass([
5059
5660
  __decorateParam(2, Inject4(MORSE_PROJECT_POLICY)),
5060
5661
  __decorateParam(3, Inject4(MORSE_LOGGER)),
5061
5662
  __decorateParam(4, Inject4(MORSE_CONFIG)),
5062
- __decorateParam(5, Inject4(ServerTerminalBackend))
5663
+ __decorateParam(5, Inject4(ServerTerminalBackend)),
5664
+ __decorateParam(6, Inject4(MORSE_PI_ADAPTER))
5063
5665
  ], MorseSessionFactory);
5064
5666
  function gitPaths(args) {
5065
5667
  const paths = args?.paths;
@@ -5068,6 +5670,16 @@ function gitPaths(args) {
5068
5670
  }
5069
5671
  return paths.filter((path) => typeof path === "string" && path.length > 0);
5070
5672
  }
5673
+ function stringArg(args, key) {
5674
+ const value = args?.[key];
5675
+ return typeof value === "string" ? value : "";
5676
+ }
5677
+ function mcpScope(args) {
5678
+ return args?.scope === "global" || args?.scope === "project" ? args.scope : void 0;
5679
+ }
5680
+ function describeError2(error) {
5681
+ return error instanceof Error ? error.message : String(error);
5682
+ }
5071
5683
 
5072
5684
  // ../server/src/internal/ws/morse.gateway.ts
5073
5685
  var MorseGateway = class {
@@ -5117,19 +5729,19 @@ MorseGateway = __decorateClass([
5117
5729
  ], MorseGateway);
5118
5730
 
5119
5731
  // ../server/src/app/config.ts
5120
- import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute6, join as join10 } from "node:path";
5121
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
5122
- import { homedir as homedir4 } from "node:os";
5732
+ import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute6, join as join11 } from "node:path";
5733
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
5734
+ import { homedir as homedir5 } from "node:os";
5123
5735
  import { fileURLToPath } from "node:url";
5124
5736
  function loadConfig(env = process.env) {
5125
5737
  const workspacePath = resolveWorkspacePath(env);
5126
5738
  const configuredUiDir = env.MORSE_UI_DIR;
5127
- const moduleDir = dirname2(fileURLToPath(import.meta.url));
5128
- const bundledUiDir = join10(moduleDir, "ui");
5129
- const monorepoUiDir = join10(moduleDir, "..", "..", "..", "ui-angular", "dist");
5130
- const defaultUiDir = existsSync2(join10(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
5739
+ const moduleDir = dirname4(fileURLToPath(import.meta.url));
5740
+ const bundledUiDir = join11(moduleDir, "ui");
5741
+ const monorepoUiDir = join11(moduleDir, "..", "..", "..", "ui-angular", "dist");
5742
+ const defaultUiDir = existsSync3(join11(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
5131
5743
  const defaultHost = "127.0.0.1";
5132
- const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join10(process.cwd(), configuredUiDir) : defaultUiDir;
5744
+ const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join11(process.cwd(), configuredUiDir) : defaultUiDir;
5133
5745
  return {
5134
5746
  host: env.MORSE_HOST ?? defaultHost,
5135
5747
  port: parsePositiveInt(env.MORSE_PORT, 4399),
@@ -5153,7 +5765,7 @@ function loadConfig(env = process.env) {
5153
5765
  function readFrontendIdentity(uiDir) {
5154
5766
  try {
5155
5767
  return frontendIdentity(
5156
- parseFrontendManifest(readFileSync3(join10(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
5768
+ parseFrontendManifest(readFileSync4(join11(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
5157
5769
  );
5158
5770
  } catch {
5159
5771
  return void 0;
@@ -5161,7 +5773,7 @@ function readFrontendIdentity(uiDir) {
5161
5773
  }
5162
5774
  function resolveDataDir(env) {
5163
5775
  const explicit = env.MORSE_HOME?.trim();
5164
- return explicit && explicit.length > 0 ? explicit : join10(homedir4(), ".morse");
5776
+ return explicit && explicit.length > 0 ? explicit : join11(homedir5(), ".morse");
5165
5777
  }
5166
5778
  function parsePositiveInt(value, fallback) {
5167
5779
  if (!value) {
@@ -5188,11 +5800,11 @@ function resolveWorkspacePath(env) {
5188
5800
  const start = process.cwd();
5189
5801
  let directory = start;
5190
5802
  for (; ; ) {
5191
- const manifest = join10(directory, "package.json");
5192
- if (existsSync2(manifest) && declaresWorkspaces(manifest)) {
5803
+ const manifest = join11(directory, "package.json");
5804
+ if (existsSync3(manifest) && declaresWorkspaces(manifest)) {
5193
5805
  return directory;
5194
5806
  }
5195
- const parent = dirname2(directory);
5807
+ const parent = dirname4(directory);
5196
5808
  if (parent === directory) {
5197
5809
  return start;
5198
5810
  }
@@ -5201,7 +5813,7 @@ function resolveWorkspacePath(env) {
5201
5813
  }
5202
5814
  function declaresWorkspaces(manifestPath) {
5203
5815
  try {
5204
- const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
5816
+ const manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
5205
5817
  return manifest.workspaces !== void 0;
5206
5818
  } catch {
5207
5819
  return false;