@supanadit/morse-web 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
  });
@@ -1379,6 +1410,23 @@ var PiRpcAgent = class {
1379
1410
  modelId: model.id
1380
1411
  });
1381
1412
  this.patchState({ model: data ? toModelRef(data) : model });
1413
+ await this.refreshThinkingLevels();
1414
+ }
1415
+ /**
1416
+ * Re-read the levels the current model supports, and the level pi actually
1417
+ * settled the session on (a model without reasoning resets it to `off`).
1418
+ */
1419
+ async refreshThinkingLevels() {
1420
+ const [levels, state] = await Promise.all([
1421
+ this.client.request({ type: "get_available_thinking_levels" }).catch(() => void 0),
1422
+ this.client.request({ type: "get_state" }).catch(() => void 0)
1423
+ ]);
1424
+ const available = dedupeThinkingLevels((levels?.levels ?? []).map(toThinkingLevel).filter(isDefined2));
1425
+ const thinkingLevel = state ? toThinkingLevel(state.thinkingLevel) : void 0;
1426
+ this.patchState({
1427
+ availableThinkingLevels: available.length > 0 ? available : [...THINKING_LEVELS],
1428
+ ...thinkingLevel !== void 0 ? { thinkingLevel } : {}
1429
+ });
1382
1430
  }
1383
1431
  async setThinkingLevel(level) {
1384
1432
  await this.client.request({ type: "set_thinking_level", level });
@@ -1446,6 +1494,22 @@ var PiRpcAgent = class {
1446
1494
  commandContext() {
1447
1495
  return { cwd: this.options.workspace.cwd, env: this.options.env };
1448
1496
  }
1497
+ /**
1498
+ * Re-reads `get_available_models` and reports it the same way a fresh spawn
1499
+ * would. The model picker calls it the moment it opens, so a model added to
1500
+ * `models.json` while this session is warm is there without reopening the
1501
+ * session or restarting the host.
1502
+ */
1503
+ async refreshModels() {
1504
+ if (this.disposed || !this.ready) {
1505
+ return;
1506
+ }
1507
+ const models = await this.client.request({ type: "get_available_models" }).catch(() => void 0);
1508
+ if (models === void 0) {
1509
+ return;
1510
+ }
1511
+ this.patchState({ availableModels: (models.models ?? []).map(toModelRef) });
1512
+ }
1449
1513
  /**
1450
1514
  * Pulls the cumulative token/cost/context totals pi keeps for the session, so
1451
1515
  * the footer can show the same numbers the TUI does. Best-effort: an older pi
@@ -1577,7 +1641,7 @@ function toThinkingLevel(value) {
1577
1641
  if (!value) {
1578
1642
  return void 0;
1579
1643
  }
1580
- return THINKING_LEVELS.find((level) => level === value);
1644
+ return THINKING_LEVELS.find((level) => level === value) ?? value.trim();
1581
1645
  }
1582
1646
  function dedupeThinkingLevels(levels) {
1583
1647
  return [...new Set(levels)];
@@ -1756,7 +1820,7 @@ ${mentions}` : mentions;
1756
1820
  // ../adapter-pi-rpc/dist/internal/resolve-pi.js
1757
1821
  import { accessSync, constants } from "node:fs";
1758
1822
  import { homedir as homedir2 } from "node:os";
1759
- import { delimiter, isAbsolute, join as join2 } from "node:path";
1823
+ import { delimiter, dirname, isAbsolute, join as join2 } from "node:path";
1760
1824
  var PI_INSTALL_COMMAND = "npm install -g @earendil-works/pi-coding-agent";
1761
1825
  function buildRpcArgs(options) {
1762
1826
  const args = ["--mode", "rpc"];
@@ -1794,6 +1858,33 @@ function resolvePi(options = {}) {
1794
1858
  "- NestJS host: env MORSE_PI_PATH (or MORSE_PI_ENTRY for a bundled rpc-entry.js)"
1795
1859
  ].join("\n"), { remedy: { install: PI_INSTALL_COMMAND } });
1796
1860
  }
1861
+ function resolvePiCli(options = {}) {
1862
+ const env = options.env ?? process.env;
1863
+ const configured = options.piPath ?? env.MORSE_PI_PATH;
1864
+ if (configured) {
1865
+ return { command: configured, baseArgs: [], source: "configured" };
1866
+ }
1867
+ const onPath = findOnPath("pi", env);
1868
+ if (onPath) {
1869
+ return { command: onPath, baseArgs: [], source: "path" };
1870
+ }
1871
+ const entry = options.nodeEntryPath ?? env.MORSE_PI_ENTRY;
1872
+ const cli = entry ? siblingCliEntry(entry) : void 0;
1873
+ if (cli) {
1874
+ return { command: process.execPath, baseArgs: [cli], source: "node-entry" };
1875
+ }
1876
+ throw new AgentUnavailableError([
1877
+ "The pi CLI was not found, so Morse cannot run `pi mcp`.",
1878
+ `Install it (\`${PI_INSTALL_COMMAND}\`) or point Morse at it:`,
1879
+ '- VS Code: setting "morse.pi.path"',
1880
+ "- NestJS host: env MORSE_PI_PATH"
1881
+ ].join("\n"), { remedy: { install: PI_INSTALL_COMMAND } });
1882
+ }
1883
+ function siblingCliEntry(entry) {
1884
+ const directory = dirname(entry);
1885
+ const cli = join2(directory, "cli.js");
1886
+ return entry.endsWith("rpc-entry.js") && isExecutableEntry(cli) ? cli : void 0;
1887
+ }
1797
1888
  function findOnPath(binary, env = process.env) {
1798
1889
  const pathValue = env.PATH ?? env.Path ?? "";
1799
1890
  if (pathValue.length === 0) {
@@ -1865,7 +1956,13 @@ var PiRpcAgentFactory = class {
1865
1956
  extraArgs: this.options.extraArgs,
1866
1957
  env: this.options.env
1867
1958
  };
1868
- return this.spawnAgent(resolveOptions, input.workspace);
1959
+ const agent = await this.spawnAgent(resolveOptions, input.workspace);
1960
+ if (input.model) {
1961
+ await agent.setModel(input.model).catch((error) => {
1962
+ this.logger.warn("Could not select the model on the draft probe", error);
1963
+ });
1964
+ }
1965
+ return agent;
1869
1966
  }
1870
1967
  async spawnAgent(resolveOptions, workspace, sessionPath) {
1871
1968
  const spawn2 = resolvePi(resolveOptions);
@@ -1883,9 +1980,425 @@ var PiRpcAgentFactory = class {
1883
1980
  }
1884
1981
  };
1885
1982
 
1983
+ // ../adapter-pi-rpc/dist/pi-mcp.js
1984
+ import { execFile } from "node:child_process";
1985
+ import { homedir as homedir3 } from "node:os";
1986
+ import { join as join3 } from "node:path";
1987
+
1988
+ // ../adapter-pi-rpc/dist/internal/mcp-config.js
1989
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1990
+ import { dirname as dirname2 } from "node:path";
1991
+ var SERVER_NAME = /^[A-Za-z0-9_-]+$/;
1992
+ function isValidMcpServerName(name) {
1993
+ return SERVER_NAME.test(name);
1994
+ }
1995
+ function readMcpFile(path) {
1996
+ if (!existsSync(path)) {
1997
+ return { mcpServers: {} };
1998
+ }
1999
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2000
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2001
+ throw new Error(`${path} is not a JSON object.`);
2002
+ }
2003
+ const servers = parsed.mcpServers;
2004
+ if (servers !== void 0 && (typeof servers !== "object" || servers === null || Array.isArray(servers))) {
2005
+ throw new Error(`${path}: "mcpServers" must be an object.`);
2006
+ }
2007
+ return {
2008
+ ...parsed,
2009
+ mcpServers: servers ?? {}
2010
+ };
2011
+ }
2012
+ function writeMcpFile(path, file) {
2013
+ mkdirSync(dirname2(path), { recursive: true });
2014
+ writeFileSync(path, `${JSON.stringify(file, null, 2)}
2015
+ `, "utf8");
2016
+ }
2017
+ function findMcpServer(path, name) {
2018
+ return readMcpFile(path).mcpServers[name];
2019
+ }
2020
+ function definesMcpServer(entry) {
2021
+ return entry !== void 0 && (entry.command !== void 0 || entry.url !== void 0 || entry.type !== void 0);
2022
+ }
2023
+ function setMcpServerOverrideEnabled(path, name, enabled) {
2024
+ const file = readMcpFile(path);
2025
+ const existing = file.mcpServers[name];
2026
+ if (enabled) {
2027
+ if (!existing) {
2028
+ return;
2029
+ }
2030
+ delete existing.enabled;
2031
+ if (Object.keys(existing).length === 0) {
2032
+ delete file.mcpServers[name];
2033
+ }
2034
+ } else {
2035
+ const entry = definesMcpServer(existing) ? {} : existing ?? {};
2036
+ entry.enabled = false;
2037
+ file.mcpServers[name] = entry;
2038
+ }
2039
+ writeMcpFile(path, file);
2040
+ }
2041
+ function upsertMcpServer(path, name, config) {
2042
+ if (!isValidMcpServerName(name)) {
2043
+ throw new Error(`"${name}" is not a valid MCP server name (letters, digits, "_" and "-").`);
2044
+ }
2045
+ const file = readMcpFile(path);
2046
+ file.mcpServers[name] = config;
2047
+ writeMcpFile(path, file);
2048
+ }
2049
+ function removeMcpServer(path, name) {
2050
+ const file = readMcpFile(path);
2051
+ if (!(name in file.mcpServers)) {
2052
+ return false;
2053
+ }
2054
+ delete file.mcpServers[name];
2055
+ writeMcpFile(path, file);
2056
+ return true;
2057
+ }
2058
+ function setMcpServerEnabled(path, name, enabled) {
2059
+ const file = readMcpFile(path);
2060
+ const server = file.mcpServers[name];
2061
+ if (!server) {
2062
+ return false;
2063
+ }
2064
+ if (enabled) {
2065
+ delete server.enabled;
2066
+ } else {
2067
+ server.enabled = false;
2068
+ }
2069
+ writeMcpFile(path, file);
2070
+ return true;
2071
+ }
2072
+
2073
+ // ../adapter-pi-rpc/dist/pi-mcp.js
2074
+ function parseMcpServerInput(raw) {
2075
+ const args = isRecord(raw) ? raw : {};
2076
+ const input = { name: asString2(args.name) };
2077
+ if (args.scope === "global" || args.scope === "project") {
2078
+ input.scope = args.scope;
2079
+ }
2080
+ if (args.type === "stdio" || args.type === "http") {
2081
+ input.type = args.type;
2082
+ }
2083
+ for (const key of ["command", "cwd", "url", "description"]) {
2084
+ const value = asString2(args[key]);
2085
+ if (value.length > 0) {
2086
+ input[key] = value;
2087
+ }
2088
+ }
2089
+ if (args.exposure === "codemode" || args.exposure === "deferred" || args.exposure === "direct" || args.exposure === "hidden") {
2090
+ input.exposure = args.exposure;
2091
+ }
2092
+ if (Array.isArray(args.args)) {
2093
+ input.args = args.args.filter(isString);
2094
+ }
2095
+ input.env = stringRecord(args.env);
2096
+ input.headers = stringRecord(args.headers);
2097
+ return input;
2098
+ }
2099
+ var DEFAULT_TIMEOUT_MS = 2e4;
2100
+ var PiMcp = class {
2101
+ options;
2102
+ constructor(options = {}) {
2103
+ this.options = options;
2104
+ }
2105
+ /** Connect every enabled server and report what happened. */
2106
+ async status(cwd) {
2107
+ const spawn2 = resolvePiCli(this.options);
2108
+ const { stdout, stderr, failed, timedOut } = await run(spawn2.command, [...spawn2.baseArgs, "mcp", "list", "--json"], cwd, cleanSpawnEnv(this.env()), this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
2109
+ const parsed = parseStatus(stdout);
2110
+ if (!parsed) {
2111
+ const seconds = Math.round((this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS) / 1e3);
2112
+ 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");
2113
+ throw new Error(`Could not read the MCP server list: ${detail}`);
2114
+ }
2115
+ return parsed;
2116
+ }
2117
+ async add(input, cwd) {
2118
+ if (!isValidMcpServerName(input.name)) {
2119
+ return {
2120
+ ok: false,
2121
+ message: `"${input.name}" is not a valid server name (letters, digits, "_" and "-").`
2122
+ };
2123
+ }
2124
+ const isHttp = input.type === "http" || input.url !== void 0 && input.command === void 0;
2125
+ const config = this.toConfig(input, isHttp);
2126
+ if (typeof config === "string") {
2127
+ return { ok: false, message: config };
2128
+ }
2129
+ const path = this.configPath(cwd, input.scope);
2130
+ try {
2131
+ upsertMcpServer(path, input.name, config);
2132
+ } catch (error) {
2133
+ return { ok: false, message: describe(error) };
2134
+ }
2135
+ return { ok: true, path };
2136
+ }
2137
+ async remove(name, cwd, scope) {
2138
+ if (scope === "global") {
2139
+ return this.removeFrom(this.globalPath(), name, "global");
2140
+ }
2141
+ if (scope === "project") {
2142
+ return this.removeFrom(this.projectPath(cwd), name, "project");
2143
+ }
2144
+ for (const path of this.configPaths(cwd)) {
2145
+ try {
2146
+ if (removeMcpServer(path, name)) {
2147
+ return { ok: true, path };
2148
+ }
2149
+ } catch (error) {
2150
+ return { ok: false, message: describe(error) };
2151
+ }
2152
+ }
2153
+ return { ok: false, message: `No server named "${name}" is configured.` };
2154
+ }
2155
+ removeFrom(path, name, scope) {
2156
+ try {
2157
+ if (!removeMcpServer(path, name)) {
2158
+ return {
2159
+ ok: false,
2160
+ message: scope === "project" ? `"${name}" is not defined in this project's .pi/mcp.json.` : `"${name}" is not defined in the user mcp.json.`
2161
+ };
2162
+ }
2163
+ } catch (error) {
2164
+ return { ok: false, message: describe(error) };
2165
+ }
2166
+ return { ok: true, path, scope };
2167
+ }
2168
+ /**
2169
+ * Enable or disable a server in one scope.
2170
+ *
2171
+ * Global edits the user file. Project edits the project entry when the project
2172
+ * defines the server, and otherwise writes a project **override** — the way
2173
+ * pi's `/mcp` does "Disable in this project" — so a user-level server can be
2174
+ * turned off in one directory without touching every other project.
2175
+ */
2176
+ async setEnabled(name, enabled, cwd, scope) {
2177
+ if (scope === "global") {
2178
+ return this.setEnabledIn(this.globalPath(), name, enabled, "global");
2179
+ }
2180
+ if (scope === "project") {
2181
+ const projectPath = this.projectPath(cwd);
2182
+ if (definesMcpServer(findMcpServer(projectPath, name))) {
2183
+ return this.setEnabledIn(projectPath, name, enabled, "project");
2184
+ }
2185
+ const known = findMcpServer(projectPath, name) !== void 0 || findMcpServer(this.globalPath(), name) !== void 0;
2186
+ if (!known) {
2187
+ return {
2188
+ ok: false,
2189
+ message: `No server named "${name}" is configured.`
2190
+ };
2191
+ }
2192
+ try {
2193
+ setMcpServerOverrideEnabled(projectPath, name, enabled);
2194
+ } catch (error) {
2195
+ return { ok: false, message: describe(error) };
2196
+ }
2197
+ return { ok: true, path: projectPath, scope: "project", override: true };
2198
+ }
2199
+ for (const path of this.configPaths(cwd)) {
2200
+ try {
2201
+ if (findMcpServer(path, name) === void 0) {
2202
+ continue;
2203
+ }
2204
+ setMcpServerEnabled(path, name, enabled);
2205
+ return { ok: true, path };
2206
+ } catch (error) {
2207
+ return { ok: false, message: describe(error) };
2208
+ }
2209
+ }
2210
+ return { ok: false, message: `No server named "${name}" is configured.` };
2211
+ }
2212
+ setEnabledIn(path, name, enabled, scope) {
2213
+ try {
2214
+ if (findMcpServer(path, name) === void 0) {
2215
+ return {
2216
+ ok: false,
2217
+ message: scope === "project" ? `"${name}" is not defined in this project's .pi/mcp.json.` : `"${name}" is not defined in the user mcp.json.`
2218
+ };
2219
+ }
2220
+ setMcpServerEnabled(path, name, enabled);
2221
+ } catch (error) {
2222
+ return { ok: false, message: describe(error) };
2223
+ }
2224
+ return { ok: true, path, scope };
2225
+ }
2226
+ /** The user-level file first, then the project's — so a project definition wins. */
2227
+ configPaths(cwd) {
2228
+ return [this.projectPath(cwd), this.globalPath()];
2229
+ }
2230
+ configPath(cwd, scope) {
2231
+ return scope === "project" ? this.projectPath(cwd) : this.globalPath();
2232
+ }
2233
+ globalPath() {
2234
+ return join3(this.agentDir(), "mcp.json");
2235
+ }
2236
+ projectPath(cwd) {
2237
+ return join3(cwd, ".pi", "mcp.json");
2238
+ }
2239
+ agentDir() {
2240
+ if (this.options.agentDir) {
2241
+ return this.options.agentDir;
2242
+ }
2243
+ const fromEnv = this.env().PI_CODING_AGENT_DIR;
2244
+ return fromEnv && fromEnv.length > 0 ? fromEnv : join3(homedir3(), ".pi", "agent");
2245
+ }
2246
+ env() {
2247
+ return this.options.env ?? process.env;
2248
+ }
2249
+ /** The stored entry, or an error message when the input is incomplete. */
2250
+ toConfig(input, isHttp) {
2251
+ const config = {};
2252
+ if (isHttp) {
2253
+ const url = input.url?.trim();
2254
+ if (!url) {
2255
+ return 'An HTTP server needs a "url".';
2256
+ }
2257
+ config.type = "http";
2258
+ config.url = url;
2259
+ if (input.headers && Object.keys(input.headers).length > 0) {
2260
+ config.headers = input.headers;
2261
+ }
2262
+ } else {
2263
+ const command = input.command?.trim();
2264
+ if (!command) {
2265
+ return 'A stdio server needs a "command".';
2266
+ }
2267
+ config.type = "stdio";
2268
+ config.command = command;
2269
+ if (input.args && input.args.length > 0) {
2270
+ config.args = input.args;
2271
+ }
2272
+ if (input.cwd?.trim()) {
2273
+ config.cwd = input.cwd.trim();
2274
+ }
2275
+ if (input.env && Object.keys(input.env).length > 0) {
2276
+ config.env = input.env;
2277
+ }
2278
+ }
2279
+ if (input.exposure && input.exposure !== "codemode") {
2280
+ config.exposure = input.exposure;
2281
+ }
2282
+ if (input.description?.trim()) {
2283
+ config.description = input.description.trim();
2284
+ }
2285
+ if (input.enabled === false) {
2286
+ config.enabled = false;
2287
+ }
2288
+ return config;
2289
+ }
2290
+ };
2291
+ function run(command, args, cwd, env, timeoutMs) {
2292
+ return new Promise((resolve6, reject) => {
2293
+ execFile(command, args, { cwd, env, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
2294
+ const err = error;
2295
+ const code = err?.code;
2296
+ if (typeof code === "string") {
2297
+ reject(new Error(`Could not run "${command}": ${stderr.trim() || code}`));
2298
+ return;
2299
+ }
2300
+ resolve6({ stdout, stderr, failed: error !== null, timedOut: err?.killed === true });
2301
+ });
2302
+ });
2303
+ }
2304
+ function parseStatus(stdout) {
2305
+ const trimmed = stdout.trim();
2306
+ if (trimmed.length === 0) {
2307
+ return void 0;
2308
+ }
2309
+ try {
2310
+ const parsed = JSON.parse(trimmed);
2311
+ if (!Array.isArray(parsed.servers)) {
2312
+ return void 0;
2313
+ }
2314
+ return {
2315
+ servers: parsed.servers.map(toServerStatus),
2316
+ errors: Array.isArray(parsed.errors) ? parsed.errors.filter(isString) : [],
2317
+ ...typeof parsed.note === "string" ? { note: parsed.note } : {}
2318
+ };
2319
+ } catch {
2320
+ return void 0;
2321
+ }
2322
+ }
2323
+ function toServerStatus(raw) {
2324
+ const row = typeof raw === "object" && raw !== null ? raw : {};
2325
+ const scope = row.scope;
2326
+ return {
2327
+ name: isString(row.name) ? row.name : "unknown",
2328
+ scope: scope === "project" || scope === "extension" ? scope : "global",
2329
+ source: isString(row.source) ? row.source : "",
2330
+ ...isString(row.override) ? { override: row.override } : {},
2331
+ enabled: row.enabled !== false,
2332
+ exposure: toExposure(row.exposure),
2333
+ transport: isString(row.transport) ? row.transport : "",
2334
+ state: toState(row.state),
2335
+ tools: Array.isArray(row.tools) ? row.tools.filter(isString) : [],
2336
+ ...isRecord(row.toolExposure) ? { toolExposure: toExposureMap(row.toolExposure) } : {},
2337
+ ...typeof row.resources === "number" ? { resources: row.resources } : {},
2338
+ ...typeof row.resourceTemplates === "number" ? { resourceTemplates: row.resourceTemplates } : {},
2339
+ ...isString(row.error) ? { error: row.error } : {}
2340
+ };
2341
+ }
2342
+ function toState(value) {
2343
+ switch (value) {
2344
+ case "connecting":
2345
+ case "connected":
2346
+ case "disconnected":
2347
+ case "needs-auth":
2348
+ case "failed":
2349
+ case "closed":
2350
+ case "disabled":
2351
+ return value;
2352
+ default:
2353
+ return "failed";
2354
+ }
2355
+ }
2356
+ function toExposure(value) {
2357
+ switch (value) {
2358
+ case "codemode":
2359
+ case "deferred":
2360
+ case "direct":
2361
+ case "hidden":
2362
+ return value;
2363
+ default:
2364
+ return "codemode";
2365
+ }
2366
+ }
2367
+ function toExposureMap(value) {
2368
+ const result = {};
2369
+ for (const [key, entry] of Object.entries(value)) {
2370
+ result[key] = toExposure(entry);
2371
+ }
2372
+ return result;
2373
+ }
2374
+ function isString(value) {
2375
+ return typeof value === "string";
2376
+ }
2377
+ function asString2(value) {
2378
+ return typeof value === "string" ? value : "";
2379
+ }
2380
+ function stringRecord(value) {
2381
+ if (!isRecord(value)) {
2382
+ return void 0;
2383
+ }
2384
+ const result = {};
2385
+ for (const [key, entry] of Object.entries(value)) {
2386
+ if (typeof entry === "string") {
2387
+ result[key] = entry;
2388
+ }
2389
+ }
2390
+ return Object.keys(result).length > 0 ? result : void 0;
2391
+ }
2392
+ function isRecord(value) {
2393
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2394
+ }
2395
+ function describe(error) {
2396
+ return error instanceof Error ? error.message : String(error);
2397
+ }
2398
+
1886
2399
  // ../adapter-pi-rpc/dist/pi-rpc-session-catalog.js
1887
2400
  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";
2401
+ import { join as join4, resolve as resolve2, sep } from "node:path";
1889
2402
  var TYPE_PREFIX = '{"type":"';
1890
2403
  function typeAt(raw, start, end) {
1891
2404
  if (!raw.startsWith(TYPE_PREFIX, start)) {
@@ -1974,12 +2487,12 @@ var PiRpcSessionCatalog = class {
1974
2487
  if (!bucket.isDirectory()) {
1975
2488
  continue;
1976
2489
  }
1977
- const bucketPath = join3(this.directory, bucket.name);
2490
+ const bucketPath = join4(this.directory, bucket.name);
1978
2491
  for (const entry of await safeReadDir(bucketPath)) {
1979
2492
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
1980
2493
  continue;
1981
2494
  }
1982
- const path = join3(bucketPath, entry.name);
2495
+ const path = join4(bucketPath, entry.name);
1983
2496
  const info = await stat2(path).catch(() => void 0);
1984
2497
  files.push({ path, size: info?.size ?? 0, updatedAt: info?.mtimeMs ?? 0 });
1985
2498
  }
@@ -2131,9 +2644,16 @@ function createPiRpcAdapter(config, logger) {
2131
2644
  env: config.env,
2132
2645
  removeFile: config.removeSessionFile
2133
2646
  });
2647
+ const cliOptions = {
2648
+ piPath: config.piPath,
2649
+ nodeEntryPath: config.nodeEntryPath,
2650
+ env: config.env
2651
+ };
2652
+ const mcp = new PiMcp(cliOptions);
2134
2653
  return {
2135
2654
  factory,
2136
2655
  catalog,
2656
+ mcp,
2137
2657
  describe: () => resolvePi({
2138
2658
  piPath: config.piPath,
2139
2659
  nodeEntryPath: config.nodeEntryPath,
@@ -2141,7 +2661,14 @@ function createPiRpcAdapter(config, logger) {
2141
2661
  noSession: config.noSession,
2142
2662
  extraArgs: config.extraArgs,
2143
2663
  env: config.env
2144
- })
2664
+ }),
2665
+ describeCli: () => {
2666
+ try {
2667
+ return resolvePiCli(cliOptions);
2668
+ } catch {
2669
+ return void 0;
2670
+ }
2671
+ }
2145
2672
  };
2146
2673
  }
2147
2674
 
@@ -2722,6 +3249,7 @@ var HostSessionController = class {
2722
3249
  void this.warmDraft();
2723
3250
  } else {
2724
3251
  await this.runSessionChange(() => this.options.services.registry.activate(existingKey));
3252
+ void this.refreshModels();
2725
3253
  }
2726
3254
  }
2727
3255
  void this.publishLists();
@@ -2814,6 +3342,9 @@ var HostSessionController = class {
2814
3342
  case "commands/refresh":
2815
3343
  await this.refreshCommands();
2816
3344
  return;
3345
+ case "models/refresh":
3346
+ await this.refreshModels();
3347
+ return;
2817
3348
  case "project/list":
2818
3349
  await this.publishProjects();
2819
3350
  return;
@@ -2978,7 +3509,7 @@ var HostSessionController = class {
2978
3509
  /** Fills the draft pickers from the registry's session-less probe. */
2979
3510
  async warmDraft() {
2980
3511
  try {
2981
- this.draftCatalog = await this.options.services.registry.draftDefaults();
3512
+ this.draftCatalog = await this.options.services.registry.draftDefaults(this.draftModel);
2982
3513
  } catch (error) {
2983
3514
  this.agentError = describeError(error);
2984
3515
  this.agentFailure = describeAgentFailure(error, this.options.agentHint);
@@ -3311,6 +3842,7 @@ var HostSessionController = class {
3311
3842
  const match = this.draftCatalog?.availableModels.find((model) => model.provider === provider && model.id === id);
3312
3843
  this.draftModel = match ?? { provider, id, name: id };
3313
3844
  this.emitState();
3845
+ void this.refreshDraftCatalog(this.draftModel);
3314
3846
  return;
3315
3847
  }
3316
3848
  const result = await this.guard(() => this.options.services.chat.setModel({ provider, id, name: id }));
@@ -3318,6 +3850,23 @@ var HostSessionController = class {
3318
3850
  this.emitState();
3319
3851
  }
3320
3852
  }
3853
+ /**
3854
+ * Re-reads the draft catalog for a model picked before any session exists, so
3855
+ * the thinking picker mirrors that model's levels instead of the default
3856
+ * model's. The request is dropped when the reader picks again or a session
3857
+ * opens while the probe is in flight.
3858
+ */
3859
+ async refreshDraftCatalog(model) {
3860
+ try {
3861
+ const catalog = await this.options.services.registry.draftDefaults(model);
3862
+ if (catalog && this.activeKey === void 0 && !this.disposed && this.draftModel === model) {
3863
+ this.draftCatalog = catalog;
3864
+ this.emitState();
3865
+ }
3866
+ } catch (error) {
3867
+ this.options.logger.warn("Could not probe the draft catalog for the picked model", error);
3868
+ }
3869
+ }
3321
3870
  async setThinkingLevel(level) {
3322
3871
  if (this.activeKey === void 0) {
3323
3872
  this.draftThinking = level;
@@ -3350,6 +3899,24 @@ var HostSessionController = class {
3350
3899
  }
3351
3900
  await this.guard(() => gateway.refreshCommands());
3352
3901
  }
3902
+ /**
3903
+ * Re-reads the model catalog on request. A warm session re-asks pi over RPC
3904
+ * (cheap); a draft re-runs the session-less probe, since that is where its
3905
+ * picker's catalog came from and it would otherwise be frozen for the host's
3906
+ * lifetime.
3907
+ */
3908
+ async refreshModels() {
3909
+ if (this.activeKey === void 0) {
3910
+ this.options.services.registry.refreshDraftDefaults();
3911
+ await this.warmDraft();
3912
+ return;
3913
+ }
3914
+ const gateway = this.options.services.registry.active();
3915
+ if (gateway?.refreshModels === void 0) {
3916
+ return;
3917
+ }
3918
+ await this.guard(() => gateway.refreshModels());
3919
+ }
3353
3920
  async runHostCommand(command, args, requestId) {
3354
3921
  const handler = this.options.onHostCommand;
3355
3922
  if (!handler) {
@@ -3705,7 +4272,7 @@ function hintOnly(hint) {
3705
4272
  // ../server/src/internal/http/health.controller.ts
3706
4273
  import { Controller, Get } from "@nestjs/common";
3707
4274
  import { readFile as readFile3 } from "node:fs/promises";
3708
- import { join as join4 } from "node:path";
4275
+ import { join as join5 } from "node:path";
3709
4276
  import { Inject } from "@nestjs/common";
3710
4277
 
3711
4278
  // ../server/src/app/tokens.ts
@@ -3733,7 +4300,7 @@ var HealthController = class {
3733
4300
  };
3734
4301
  }
3735
4302
  async manifest() {
3736
- const raw = await readFile3(join4(this.config.uiDir, "webview.manifest.json"), "utf8").catch(
4303
+ const raw = await readFile3(join5(this.config.uiDir, "webview.manifest.json"), "utf8").catch(
3737
4304
  () => void 0
3738
4305
  );
3739
4306
  return parseFrontendManifest(raw) ?? { name: "unknown", protocolVersion: PROTOCOL_VERSION };
@@ -3811,13 +4378,13 @@ import { spawn as spawnPty } from "node-pty";
3811
4378
  import { Inject as Inject2, Injectable } from "@nestjs/common";
3812
4379
 
3813
4380
  // ../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";
4381
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
4382
+ import { join as join6 } from "node:path";
3816
4383
  var LOG_DIR = "terminals";
3817
4384
  var LOG_MAX_CHARS = TERMINAL_BUFFER_CHARS * 4;
3818
4385
  function readTerminalLog(dataDir, terminalId) {
3819
4386
  try {
3820
- const raw = readFileSync(logPath(dataDir, terminalId), "utf8");
4387
+ const raw = readFileSync2(logPath(dataDir, terminalId), "utf8");
3821
4388
  return trimTail(raw, LOG_MAX_CHARS);
3822
4389
  } catch {
3823
4390
  return "";
@@ -3828,12 +4395,12 @@ function writeTerminalLog(dataDir, terminalId, text) {
3828
4395
  const target = logPath(dataDir, terminalId);
3829
4396
  const temp = `${target}.${process.pid}.tmp`;
3830
4397
  try {
3831
- mkdirSync(join5(dataDir, LOG_DIR), { recursive: true });
3832
- writeFileSync(temp, trimmed);
4398
+ mkdirSync2(join6(dataDir, LOG_DIR), { recursive: true });
4399
+ writeFileSync2(temp, trimmed);
3833
4400
  renameSync(temp, target);
3834
4401
  } catch {
3835
4402
  try {
3836
- writeFileSync(target, trimmed);
4403
+ writeFileSync2(target, trimmed);
3837
4404
  } catch {
3838
4405
  rmSync(temp, { force: true });
3839
4406
  }
@@ -3847,7 +4414,7 @@ function safeTerminalName(terminalId) {
3847
4414
  return safe.length > 0 ? safe : "terminal";
3848
4415
  }
3849
4416
  function logPath(dataDir, terminalId) {
3850
- return join5(dataDir, LOG_DIR, `${safeTerminalName(terminalId)}.log`);
4417
+ return join6(dataDir, LOG_DIR, `${safeTerminalName(terminalId)}.log`);
3851
4418
  }
3852
4419
 
3853
4420
  // ../server/src/internal/terminal/terminal.service.ts
@@ -4095,9 +4662,9 @@ import { Inject as Inject5 } from "@nestjs/common";
4095
4662
  import { Inject as Inject4, Injectable as Injectable3 } from "@nestjs/common";
4096
4663
 
4097
4664
  // ../server/src/internal/uploads/upload-store.ts
4098
- import { existsSync } from "node:fs";
4665
+ import { existsSync as existsSync2 } from "node:fs";
4099
4666
  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";
4667
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join7, relative, sep as sep3 } from "node:path";
4101
4668
  var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
4102
4669
  var DEFAULT_UPLOAD_DIR = ".morse/uploads";
4103
4670
  var UploadError = class extends Error {
@@ -4117,7 +4684,7 @@ async function saveUpload(cwd, request, uploadDir = DEFAULT_UPLOAD_DIR) {
4117
4684
  `That file is ${formatMb(bytes.length)} MB; the limit is ${formatMb(MAX_UPLOAD_BYTES)} MB.`
4118
4685
  );
4119
4686
  }
4120
- const directory = isAbsolute3(uploadDir) ? uploadDir : join6(cwd, uploadDir);
4687
+ const directory = isAbsolute3(uploadDir) ? uploadDir : join7(cwd, uploadDir);
4121
4688
  await mkdir(directory, { recursive: true });
4122
4689
  const target = uniquePath(directory, name);
4123
4690
  await writeFile(target, bytes);
@@ -4136,10 +4703,10 @@ function uniquePath(directory, name) {
4136
4703
  const dot = name.lastIndexOf(".");
4137
4704
  const stem = dot > 0 ? name.slice(0, dot) : name;
4138
4705
  const extension = dot > 0 ? name.slice(dot) : "";
4139
- let candidate = join6(directory, name);
4706
+ let candidate = join7(directory, name);
4140
4707
  let suffix = 1;
4141
- while (existsSync(candidate)) {
4142
- candidate = join6(directory, `${stem}-${suffix}${extension}`);
4708
+ while (existsSync2(candidate)) {
4709
+ candidate = join7(directory, `${stem}-${suffix}${extension}`);
4143
4710
  suffix += 1;
4144
4711
  }
4145
4712
  return candidate;
@@ -4155,8 +4722,8 @@ function formatMb(bytes) {
4155
4722
 
4156
4723
  // ../server/src/internal/workspace/directory-browser.ts
4157
4724
  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";
4725
+ import { homedir as homedir4 } from "node:os";
4726
+ import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute4, join as join8, resolve as resolve4 } from "node:path";
4160
4727
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
4161
4728
  "node_modules",
4162
4729
  "dist",
@@ -4180,8 +4747,8 @@ async function browseDirectory(path, options) {
4180
4747
  throw new Error(`A folder path must be absolute: ${requested}`);
4181
4748
  }
4182
4749
  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);
4750
+ 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));
4751
+ const parent = dirname3(start);
4185
4752
  return {
4186
4753
  path: start,
4187
4754
  parent: parent === start ? void 0 : parent,
@@ -4195,7 +4762,7 @@ function shouldSkip(name) {
4195
4762
  }
4196
4763
  async function isGitRepo(path) {
4197
4764
  try {
4198
- await stat3(join7(path, ".git"));
4765
+ await stat3(join8(path, ".git"));
4199
4766
  return true;
4200
4767
  } catch {
4201
4768
  return false;
@@ -4204,7 +4771,7 @@ async function isGitRepo(path) {
4204
4771
  function shortcutRoots(options) {
4205
4772
  const seen = /* @__PURE__ */ new Set();
4206
4773
  const roots = [];
4207
- for (const candidate of [...options.roots ?? [], homedir3(), options.defaultPath]) {
4774
+ for (const candidate of [...options.roots ?? [], homedir4(), options.defaultPath]) {
4208
4775
  if (!candidate || !isAbsolute4(candidate)) {
4209
4776
  continue;
4210
4777
  }
@@ -4266,7 +4833,7 @@ function toPosix(path) {
4266
4833
  }
4267
4834
 
4268
4835
  // ../server/src/internal/workspace/git-log.ts
4269
- import { execFile } from "node:child_process";
4836
+ import { execFile as execFile2 } from "node:child_process";
4270
4837
  import { relative as relative3 } from "node:path";
4271
4838
  var DEFAULT_MAX_COMMITS = 250;
4272
4839
  var HARD_MAX_COMMITS = 500;
@@ -4317,9 +4884,9 @@ async function readGitStatus(cwd) {
4317
4884
  gitText(["rev-parse", "--show-prefix"], cwd),
4318
4885
  gitText(["status", "--porcelain=v1", "-z", "--untracked-files=all"], cwd)
4319
4886
  ]);
4320
- return { isRepo: true, files: parseStatus(raw ?? "", prefix?.trim() ?? "") };
4887
+ return { isRepo: true, files: parseStatus2(raw ?? "", prefix?.trim() ?? "") };
4321
4888
  }
4322
- function parseStatus(raw, root) {
4889
+ function parseStatus2(raw, root) {
4323
4890
  const records = raw.split("\0");
4324
4891
  const files = [];
4325
4892
  for (let index = 0; index < records.length; index += 1) {
@@ -4563,7 +5130,7 @@ function parseRefs(raw) {
4563
5130
  }
4564
5131
  function gitText(args, cwd) {
4565
5132
  return new Promise((resolve6) => {
4566
- execFile(
5133
+ execFile2(
4567
5134
  "git",
4568
5135
  args,
4569
5136
  { cwd, timeout: 5e3, maxBuffer: 16 * 1024 * 1024, windowsHide: true },
@@ -4579,7 +5146,7 @@ async function gitRun(args, cwd, timeout = 5e3) {
4579
5146
  }
4580
5147
  function gitExec(args, cwd, timeout = 5e3) {
4581
5148
  return new Promise((resolve6) => {
4582
- execFile(
5149
+ execFile2(
4583
5150
  "git",
4584
5151
  args,
4585
5152
  {
@@ -4607,9 +5174,9 @@ function gitExec(args, cwd, timeout = 5e3) {
4607
5174
  }
4608
5175
 
4609
5176
  // ../server/src/internal/workspace/workspace-index.ts
4610
- import { execFile as execFile2 } from "node:child_process";
5177
+ import { execFile as execFile3 } from "node:child_process";
4611
5178
  import { readdir as readdir4 } from "node:fs/promises";
4612
- import { join as join8, relative as relative4 } from "node:path";
5179
+ import { join as join9, relative as relative4 } from "node:path";
4613
5180
  var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set([
4614
5181
  "node_modules",
4615
5182
  "dist",
@@ -4694,7 +5261,7 @@ async function gitFileList(cwd) {
4694
5261
  }
4695
5262
  function runGit(args, cwd) {
4696
5263
  return new Promise((resolve6) => {
4697
- execFile2(
5264
+ execFile3(
4698
5265
  "git",
4699
5266
  args,
4700
5267
  { cwd, timeout: 5e3, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
@@ -4720,7 +5287,7 @@ async function walk(root, directory, files) {
4720
5287
  if (files.length >= MAX_INDEXED_FILES) {
4721
5288
  return;
4722
5289
  }
4723
- const full = join8(directory, entry.name);
5290
+ const full = join9(directory, entry.name);
4724
5291
  if (entry.isDirectory()) {
4725
5292
  if (SKIPPED_DIRECTORIES2.has(entry.name) || entry.name.startsWith(".")) {
4726
5293
  continue;
@@ -4738,20 +5305,20 @@ function toPosix3(path) {
4738
5305
  }
4739
5306
 
4740
5307
  // ../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";
5308
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
5309
+ import { join as join10 } from "node:path";
4743
5310
  var LAYOUT_FILE = "workbench.json";
4744
5311
  var DRAFTS_FILE = "drafts.json";
4745
5312
  var LAYOUT_MAX_BYTES = 256 * 1024;
4746
5313
  var DRAFTS_MAX_BYTES = 32 * 1024 * 1024;
4747
5314
  function readWorkbench(dataDir) {
4748
- return readSlot(join9(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
5315
+ return readSlot(join10(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
4749
5316
  }
4750
5317
  function saveWorkbench(dataDir, snapshot) {
4751
5318
  return saveSlot(dataDir, LAYOUT_FILE, snapshot, LAYOUT_MAX_BYTES);
4752
5319
  }
4753
5320
  function readDrafts(dataDir) {
4754
- return readSlot(join9(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
5321
+ return readSlot(join10(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
4755
5322
  }
4756
5323
  function saveDrafts(dataDir, snapshot) {
4757
5324
  return saveSlot(dataDir, DRAFTS_FILE, snapshot, DRAFTS_MAX_BYTES);
@@ -4759,7 +5326,7 @@ function saveDrafts(dataDir, snapshot) {
4759
5326
  function readSlot(path, maxBytes) {
4760
5327
  let raw;
4761
5328
  try {
4762
- raw = readFileSync2(path, "utf8");
5329
+ raw = readFileSync3(path, "utf8");
4763
5330
  } catch {
4764
5331
  return void 0;
4765
5332
  }
@@ -4783,16 +5350,16 @@ function saveSlot(dataDir, file, snapshot, maxBytes) {
4783
5350
  if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
4784
5351
  throw new UnsupportedByHostError(`That ${file} snapshot is too large to save.`);
4785
5352
  }
4786
- const target = join9(dataDir, file);
5353
+ const target = join10(dataDir, file);
4787
5354
  const temp = `${target}.${process.pid}.tmp`;
4788
5355
  try {
4789
- mkdirSync2(dataDir, { recursive: true });
4790
- writeFileSync2(temp, serialized);
5356
+ mkdirSync3(dataDir, { recursive: true });
5357
+ writeFileSync3(temp, serialized);
4791
5358
  renameSync2(temp, target);
4792
5359
  return true;
4793
5360
  } catch {
4794
5361
  try {
4795
- writeFileSync2(target, serialized);
5362
+ writeFileSync3(target, serialized);
4796
5363
  return true;
4797
5364
  } catch {
4798
5365
  rmSync2(temp, { force: true });
@@ -4814,13 +5381,14 @@ function asSnapshot(value) {
4814
5381
 
4815
5382
  // ../server/src/internal/ws/session-factory.service.ts
4816
5383
  var MorseSessionFactory = class {
4817
- constructor(registry, transcripts, policy, logger, config, terminal) {
5384
+ constructor(registry, transcripts, policy, logger, config, terminal, pi) {
4818
5385
  this.registry = registry;
4819
5386
  this.transcripts = transcripts;
4820
5387
  this.policy = policy;
4821
5388
  this.logger = logger;
4822
5389
  this.config = config;
4823
5390
  this.terminal = terminal;
5391
+ this.pi = pi;
4824
5392
  }
4825
5393
  capabilities() {
4826
5394
  return {
@@ -4865,7 +5433,10 @@ var MorseSessionFactory = class {
4865
5433
  // A page served by this host may ask the registry for the latest release, so
4866
5434
  // the sidebar can say when a newer Morse is out. A host without it stays
4867
5435
  // quiet; this one is on unless `MORSE_UPDATE_CHECK=0` says otherwise.
4868
- updateCheck: this.config.updateCheck
5436
+ updateCheck: this.config.updateCheck,
5437
+ // Managing MCP servers runs the `pi` CLI, so this host offers it only when
5438
+ // it found one. A host without it hides the affordance.
5439
+ mcp: this.pi.describeCli() !== void 0
4869
5440
  };
4870
5441
  }
4871
5442
  createFor(emit) {
@@ -5010,6 +5581,31 @@ var MorseSessionFactory = class {
5010
5581
  const branch = typeof args?.branch === "string" ? args.branch : "";
5011
5582
  return checkoutGit(cwd, branch, args?.create === true);
5012
5583
  }
5584
+ case "mcpStatus": {
5585
+ const cwd = this.requireWritableCwd(context);
5586
+ try {
5587
+ return await this.pi.mcp.status(cwd);
5588
+ } catch (error) {
5589
+ return { servers: [], errors: [describeError2(error)] };
5590
+ }
5591
+ }
5592
+ case "mcpAdd": {
5593
+ const cwd = this.requireWritableCwd(context);
5594
+ return this.pi.mcp.add(parseMcpServerInput(args), cwd);
5595
+ }
5596
+ case "mcpRemove": {
5597
+ const cwd = this.requireWritableCwd(context);
5598
+ return this.pi.mcp.remove(stringArg(args, "name"), cwd, mcpScope(args));
5599
+ }
5600
+ case "mcpSetEnabled": {
5601
+ const cwd = this.requireWritableCwd(context);
5602
+ return this.pi.mcp.setEnabled(
5603
+ stringArg(args, "name"),
5604
+ args?.enabled === true,
5605
+ cwd,
5606
+ mcpScope(args)
5607
+ );
5608
+ }
5013
5609
  default:
5014
5610
  throw new UnsupportedByHostError(`This host does not support "${command}".`);
5015
5611
  }
@@ -5059,7 +5655,8 @@ MorseSessionFactory = __decorateClass([
5059
5655
  __decorateParam(2, Inject4(MORSE_PROJECT_POLICY)),
5060
5656
  __decorateParam(3, Inject4(MORSE_LOGGER)),
5061
5657
  __decorateParam(4, Inject4(MORSE_CONFIG)),
5062
- __decorateParam(5, Inject4(ServerTerminalBackend))
5658
+ __decorateParam(5, Inject4(ServerTerminalBackend)),
5659
+ __decorateParam(6, Inject4(MORSE_PI_ADAPTER))
5063
5660
  ], MorseSessionFactory);
5064
5661
  function gitPaths(args) {
5065
5662
  const paths = args?.paths;
@@ -5068,6 +5665,16 @@ function gitPaths(args) {
5068
5665
  }
5069
5666
  return paths.filter((path) => typeof path === "string" && path.length > 0);
5070
5667
  }
5668
+ function stringArg(args, key) {
5669
+ const value = args?.[key];
5670
+ return typeof value === "string" ? value : "";
5671
+ }
5672
+ function mcpScope(args) {
5673
+ return args?.scope === "global" || args?.scope === "project" ? args.scope : void 0;
5674
+ }
5675
+ function describeError2(error) {
5676
+ return error instanceof Error ? error.message : String(error);
5677
+ }
5071
5678
 
5072
5679
  // ../server/src/internal/ws/morse.gateway.ts
5073
5680
  var MorseGateway = class {
@@ -5117,19 +5724,19 @@ MorseGateway = __decorateClass([
5117
5724
  ], MorseGateway);
5118
5725
 
5119
5726
  // ../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";
5727
+ import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute6, join as join11 } from "node:path";
5728
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
5729
+ import { homedir as homedir5 } from "node:os";
5123
5730
  import { fileURLToPath } from "node:url";
5124
5731
  function loadConfig(env = process.env) {
5125
5732
  const workspacePath = resolveWorkspacePath(env);
5126
5733
  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;
5734
+ const moduleDir = dirname4(fileURLToPath(import.meta.url));
5735
+ const bundledUiDir = join11(moduleDir, "ui");
5736
+ const monorepoUiDir = join11(moduleDir, "..", "..", "..", "ui-angular", "dist");
5737
+ const defaultUiDir = existsSync3(join11(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
5131
5738
  const defaultHost = "127.0.0.1";
5132
- const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join10(process.cwd(), configuredUiDir) : defaultUiDir;
5739
+ const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join11(process.cwd(), configuredUiDir) : defaultUiDir;
5133
5740
  return {
5134
5741
  host: env.MORSE_HOST ?? defaultHost,
5135
5742
  port: parsePositiveInt(env.MORSE_PORT, 4399),
@@ -5153,7 +5760,7 @@ function loadConfig(env = process.env) {
5153
5760
  function readFrontendIdentity(uiDir) {
5154
5761
  try {
5155
5762
  return frontendIdentity(
5156
- parseFrontendManifest(readFileSync3(join10(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
5763
+ parseFrontendManifest(readFileSync4(join11(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
5157
5764
  );
5158
5765
  } catch {
5159
5766
  return void 0;
@@ -5161,7 +5768,7 @@ function readFrontendIdentity(uiDir) {
5161
5768
  }
5162
5769
  function resolveDataDir(env) {
5163
5770
  const explicit = env.MORSE_HOME?.trim();
5164
- return explicit && explicit.length > 0 ? explicit : join10(homedir4(), ".morse");
5771
+ return explicit && explicit.length > 0 ? explicit : join11(homedir5(), ".morse");
5165
5772
  }
5166
5773
  function parsePositiveInt(value, fallback) {
5167
5774
  if (!value) {
@@ -5188,11 +5795,11 @@ function resolveWorkspacePath(env) {
5188
5795
  const start = process.cwd();
5189
5796
  let directory = start;
5190
5797
  for (; ; ) {
5191
- const manifest = join10(directory, "package.json");
5192
- if (existsSync2(manifest) && declaresWorkspaces(manifest)) {
5798
+ const manifest = join11(directory, "package.json");
5799
+ if (existsSync3(manifest) && declaresWorkspaces(manifest)) {
5193
5800
  return directory;
5194
5801
  }
5195
- const parent = dirname2(directory);
5802
+ const parent = dirname4(directory);
5196
5803
  if (parent === directory) {
5197
5804
  return start;
5198
5805
  }
@@ -5201,7 +5808,7 @@ function resolveWorkspacePath(env) {
5201
5808
  }
5202
5809
  function declaresWorkspaces(manifestPath) {
5203
5810
  try {
5204
- const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
5811
+ const manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
5205
5812
  return manifest.workspaces !== void 0;
5206
5813
  } catch {
5207
5814
  return false;