@supanadit/morse-web 0.12.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/README.md +53 -10
- package/dist/server.mjs +962 -129
- package/dist/server.mjs.map +4 -4
- package/dist/ui/3rdpartylicenses.txt +4 -4
- package/dist/ui/{chunk-C_meY2xd.js → chunk-CwVAC7xx.js} +1 -1
- package/dist/ui/{chunk-BJlUk5wu.js → chunk-Dk_j780x.js} +1 -1
- package/dist/ui/{chunk-DxDqTq2a.js → chunk-veepV9dF.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/ui/main-L3KG4R7V.js +120 -0
- package/dist/ui/webview.manifest.json +3 -3
- package/package.json +12 -1
- package/dist/ui/main-33GJA27U.js +0 -120
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 =
|
|
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
|
|
173
|
-
draftCache;
|
|
174
|
-
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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
|
|
376
|
-
this.
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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,10 +2661,28 @@ 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
|
|
|
2675
|
+
// ../host-runtime/dist/terminal-buffer.js
|
|
2676
|
+
var TERMINAL_BUFFER_CHARS = 256 * 1024;
|
|
2677
|
+
function trimTail(text, maxChars = TERMINAL_BUFFER_CHARS) {
|
|
2678
|
+
if (text.length <= maxChars) {
|
|
2679
|
+
return text;
|
|
2680
|
+
}
|
|
2681
|
+
const tail = text.slice(-maxChars);
|
|
2682
|
+
const newline = tail.indexOf("\n");
|
|
2683
|
+
return newline === -1 ? tail : tail.slice(newline + 1);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2148
2686
|
// ../host-runtime/dist/transcript-projector.js
|
|
2149
2687
|
var TranscriptProjector = class {
|
|
2150
2688
|
options;
|
|
@@ -2671,8 +3209,15 @@ var HostSessionController = class {
|
|
|
2671
3209
|
loadingOlderHistory = false;
|
|
2672
3210
|
/** Discriminates history item ids across pages (see `historyItems`). */
|
|
2673
3211
|
historyPageSeq = 0;
|
|
2674
|
-
/**
|
|
3212
|
+
/** This client's live attachments to shells, keyed by the terminal id it named. */
|
|
2675
3213
|
terminals = /* @__PURE__ */ new Map();
|
|
3214
|
+
/**
|
|
3215
|
+
* The ids this connection has already opened. It separates a *first* open — a
|
|
3216
|
+
* page reattaching to a shell the host still runs — from a repeat open (the
|
|
3217
|
+
* Restart button), which must replace the shell and clear its scrollback
|
|
3218
|
+
* instead of replaying it into the emulator that already shows it.
|
|
3219
|
+
*/
|
|
3220
|
+
openedTerminals = /* @__PURE__ */ new Set();
|
|
2676
3221
|
/**
|
|
2677
3222
|
* Bumped whenever a terminal id is opened or closed. A shell's late `exit`
|
|
2678
3223
|
* (a replaced one, a killed one) compares its generation and stays silent, so
|
|
@@ -2704,6 +3249,7 @@ var HostSessionController = class {
|
|
|
2704
3249
|
void this.warmDraft();
|
|
2705
3250
|
} else {
|
|
2706
3251
|
await this.runSessionChange(() => this.options.services.registry.activate(existingKey));
|
|
3252
|
+
void this.refreshModels();
|
|
2707
3253
|
}
|
|
2708
3254
|
}
|
|
2709
3255
|
void this.publishLists();
|
|
@@ -2721,11 +3267,12 @@ var HostSessionController = class {
|
|
|
2721
3267
|
return;
|
|
2722
3268
|
}
|
|
2723
3269
|
this.disposed = true;
|
|
2724
|
-
for (const
|
|
2725
|
-
|
|
3270
|
+
for (const session of this.terminals.values()) {
|
|
3271
|
+
session.detach();
|
|
2726
3272
|
}
|
|
2727
3273
|
this.terminals.clear();
|
|
2728
3274
|
this.terminalGenerations.clear();
|
|
3275
|
+
this.openedTerminals.clear();
|
|
2729
3276
|
this.detachRegistry?.();
|
|
2730
3277
|
this.detachRegistry = void 0;
|
|
2731
3278
|
this.detachTranscripts?.();
|
|
@@ -2795,6 +3342,9 @@ var HostSessionController = class {
|
|
|
2795
3342
|
case "commands/refresh":
|
|
2796
3343
|
await this.refreshCommands();
|
|
2797
3344
|
return;
|
|
3345
|
+
case "models/refresh":
|
|
3346
|
+
await this.refreshModels();
|
|
3347
|
+
return;
|
|
2798
3348
|
case "project/list":
|
|
2799
3349
|
await this.publishProjects();
|
|
2800
3350
|
return;
|
|
@@ -2830,23 +3380,38 @@ var HostSessionController = class {
|
|
|
2830
3380
|
}
|
|
2831
3381
|
}
|
|
2832
3382
|
/**
|
|
2833
|
-
*
|
|
2834
|
-
* (or the draft's), never an arbitrary path the client
|
|
2835
|
-
* still re-checks it against the host policy before
|
|
3383
|
+
* Attaches this client to a shell for the bottom panel. The directory is the
|
|
3384
|
+
* viewing session's (or the draft's), never an arbitrary path the client
|
|
3385
|
+
* names — the backend still re-checks it against the host policy before
|
|
3386
|
+
* spawning.
|
|
3387
|
+
*
|
|
3388
|
+
* A *first* open on this connection may land on a shell that is already running
|
|
3389
|
+
* (a reloaded page reattaching); a repeat open (the Restart button) replaces
|
|
3390
|
+
* it, scrollback and all, so the fresh shell does not replay the old one.
|
|
2836
3391
|
*/
|
|
2837
3392
|
async openTerminal(payload) {
|
|
2838
|
-
|
|
3393
|
+
const backend = this.options.terminal;
|
|
3394
|
+
if (backend === void 0) {
|
|
2839
3395
|
this.emitTerminalExit(payload.terminalId, void 0, "This host has no terminal.");
|
|
2840
3396
|
return;
|
|
2841
3397
|
}
|
|
2842
|
-
this.
|
|
2843
|
-
|
|
3398
|
+
const previous = this.terminals.get(payload.terminalId);
|
|
3399
|
+
if (previous !== void 0) {
|
|
3400
|
+
previous.detach();
|
|
3401
|
+
this.terminals.delete(payload.terminalId);
|
|
3402
|
+
backend.close(payload.terminalId);
|
|
3403
|
+
} else if (this.openedTerminals.has(payload.terminalId)) {
|
|
3404
|
+
backend.close(payload.terminalId);
|
|
3405
|
+
}
|
|
3406
|
+
this.openedTerminals.add(payload.terminalId);
|
|
3407
|
+
const generation = (this.terminalGenerations.get(payload.terminalId) ?? 0) + 1;
|
|
3408
|
+
this.terminalGenerations.set(payload.terminalId, generation);
|
|
2844
3409
|
const cwd = payload.cwd ?? this.currentState().workspace.cwd;
|
|
2845
3410
|
const cols = clampGeometry(payload.cols, 80);
|
|
2846
3411
|
const rows = clampGeometry(payload.rows, 24);
|
|
2847
|
-
let
|
|
3412
|
+
let session;
|
|
2848
3413
|
try {
|
|
2849
|
-
|
|
3414
|
+
session = await backend.attach(payload.terminalId, { cwd, cols, rows }, {
|
|
2850
3415
|
output: (data) => {
|
|
2851
3416
|
if (this.terminalGenerations.get(payload.terminalId) === generation) {
|
|
2852
3417
|
this.emitTerminalOutput(payload.terminalId, data);
|
|
@@ -2866,19 +3431,25 @@ var HostSessionController = class {
|
|
|
2866
3431
|
return;
|
|
2867
3432
|
}
|
|
2868
3433
|
if (!this.disposed && this.terminalGenerations.get(payload.terminalId) === generation) {
|
|
2869
|
-
this.terminals.set(payload.terminalId,
|
|
3434
|
+
this.terminals.set(payload.terminalId, session);
|
|
3435
|
+
if (session.replay.length > 0) {
|
|
3436
|
+
this.emitTerminalOutput(payload.terminalId, session.replay);
|
|
3437
|
+
}
|
|
2870
3438
|
} else {
|
|
2871
|
-
|
|
3439
|
+
session.detach();
|
|
2872
3440
|
}
|
|
2873
3441
|
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Ends a shell for good: the reader closed the pane. Bumps the generation first,
|
|
3444
|
+
* so a late `exit` for the shell being replaced is not reported as news.
|
|
3445
|
+
*/
|
|
2874
3446
|
closeTerminal(terminalId) {
|
|
2875
3447
|
this.terminalGenerations.set(terminalId, (this.terminalGenerations.get(terminalId) ?? 0) + 1);
|
|
2876
|
-
const
|
|
2877
|
-
if (process2 === void 0) {
|
|
2878
|
-
return;
|
|
2879
|
-
}
|
|
3448
|
+
const session = this.terminals.get(terminalId);
|
|
2880
3449
|
this.terminals.delete(terminalId);
|
|
2881
|
-
|
|
3450
|
+
this.openedTerminals.delete(terminalId);
|
|
3451
|
+
session?.detach();
|
|
3452
|
+
this.options.terminal?.close(terminalId);
|
|
2882
3453
|
}
|
|
2883
3454
|
emitTerminalOutput(terminalId, data) {
|
|
2884
3455
|
if (data.length > 0 && !this.disposed) {
|
|
@@ -2938,7 +3509,7 @@ var HostSessionController = class {
|
|
|
2938
3509
|
/** Fills the draft pickers from the registry's session-less probe. */
|
|
2939
3510
|
async warmDraft() {
|
|
2940
3511
|
try {
|
|
2941
|
-
this.draftCatalog = await this.options.services.registry.draftDefaults();
|
|
3512
|
+
this.draftCatalog = await this.options.services.registry.draftDefaults(this.draftModel);
|
|
2942
3513
|
} catch (error) {
|
|
2943
3514
|
this.agentError = describeError(error);
|
|
2944
3515
|
this.agentFailure = describeAgentFailure(error, this.options.agentHint);
|
|
@@ -3271,6 +3842,7 @@ var HostSessionController = class {
|
|
|
3271
3842
|
const match = this.draftCatalog?.availableModels.find((model) => model.provider === provider && model.id === id);
|
|
3272
3843
|
this.draftModel = match ?? { provider, id, name: id };
|
|
3273
3844
|
this.emitState();
|
|
3845
|
+
void this.refreshDraftCatalog(this.draftModel);
|
|
3274
3846
|
return;
|
|
3275
3847
|
}
|
|
3276
3848
|
const result = await this.guard(() => this.options.services.chat.setModel({ provider, id, name: id }));
|
|
@@ -3278,6 +3850,23 @@ var HostSessionController = class {
|
|
|
3278
3850
|
this.emitState();
|
|
3279
3851
|
}
|
|
3280
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
|
+
}
|
|
3281
3870
|
async setThinkingLevel(level) {
|
|
3282
3871
|
if (this.activeKey === void 0) {
|
|
3283
3872
|
this.draftThinking = level;
|
|
@@ -3310,6 +3899,24 @@ var HostSessionController = class {
|
|
|
3310
3899
|
}
|
|
3311
3900
|
await this.guard(() => gateway.refreshCommands());
|
|
3312
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
|
+
}
|
|
3313
3920
|
async runHostCommand(command, args, requestId) {
|
|
3314
3921
|
const handler = this.options.onHostCommand;
|
|
3315
3922
|
if (!handler) {
|
|
@@ -3665,7 +4272,7 @@ function hintOnly(hint) {
|
|
|
3665
4272
|
// ../server/src/internal/http/health.controller.ts
|
|
3666
4273
|
import { Controller, Get } from "@nestjs/common";
|
|
3667
4274
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
3668
|
-
import { join as
|
|
4275
|
+
import { join as join5 } from "node:path";
|
|
3669
4276
|
import { Inject } from "@nestjs/common";
|
|
3670
4277
|
|
|
3671
4278
|
// ../server/src/app/tokens.ts
|
|
@@ -3693,7 +4300,7 @@ var HealthController = class {
|
|
|
3693
4300
|
};
|
|
3694
4301
|
}
|
|
3695
4302
|
async manifest() {
|
|
3696
|
-
const raw = await readFile3(
|
|
4303
|
+
const raw = await readFile3(join5(this.config.uiDir, "webview.manifest.json"), "utf8").catch(
|
|
3697
4304
|
() => void 0
|
|
3698
4305
|
);
|
|
3699
4306
|
return parseFrontendManifest(raw) ?? { name: "unknown", protocolVersion: PROTOCOL_VERSION };
|
|
@@ -3769,7 +4376,50 @@ var ServerProjectPolicy = class {
|
|
|
3769
4376
|
// ../server/src/internal/terminal/terminal.service.ts
|
|
3770
4377
|
import { spawn as spawnPty } from "node-pty";
|
|
3771
4378
|
import { Inject as Inject2, Injectable } from "@nestjs/common";
|
|
4379
|
+
|
|
4380
|
+
// ../server/src/internal/terminal/terminal-log.ts
|
|
4381
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4382
|
+
import { join as join6 } from "node:path";
|
|
4383
|
+
var LOG_DIR = "terminals";
|
|
4384
|
+
var LOG_MAX_CHARS = TERMINAL_BUFFER_CHARS * 4;
|
|
4385
|
+
function readTerminalLog(dataDir, terminalId) {
|
|
4386
|
+
try {
|
|
4387
|
+
const raw = readFileSync2(logPath(dataDir, terminalId), "utf8");
|
|
4388
|
+
return trimTail(raw, LOG_MAX_CHARS);
|
|
4389
|
+
} catch {
|
|
4390
|
+
return "";
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
function writeTerminalLog(dataDir, terminalId, text) {
|
|
4394
|
+
const trimmed = trimTail(text, LOG_MAX_CHARS);
|
|
4395
|
+
const target = logPath(dataDir, terminalId);
|
|
4396
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
4397
|
+
try {
|
|
4398
|
+
mkdirSync2(join6(dataDir, LOG_DIR), { recursive: true });
|
|
4399
|
+
writeFileSync2(temp, trimmed);
|
|
4400
|
+
renameSync(temp, target);
|
|
4401
|
+
} catch {
|
|
4402
|
+
try {
|
|
4403
|
+
writeFileSync2(target, trimmed);
|
|
4404
|
+
} catch {
|
|
4405
|
+
rmSync(temp, { force: true });
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
function removeTerminalLog(dataDir, terminalId) {
|
|
4410
|
+
rmSync(logPath(dataDir, terminalId), { force: true });
|
|
4411
|
+
}
|
|
4412
|
+
function safeTerminalName(terminalId) {
|
|
4413
|
+
const safe = terminalId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 100);
|
|
4414
|
+
return safe.length > 0 ? safe : "terminal";
|
|
4415
|
+
}
|
|
4416
|
+
function logPath(dataDir, terminalId) {
|
|
4417
|
+
return join6(dataDir, LOG_DIR, `${safeTerminalName(terminalId)}.log`);
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
// ../server/src/internal/terminal/terminal.service.ts
|
|
3772
4421
|
var SIGKILL_AFTER_MS = 1500;
|
|
4422
|
+
var FLUSH_DEBOUNCE_MS = 2e3;
|
|
3773
4423
|
function defaultShell() {
|
|
3774
4424
|
const configured = process.env.SHELL?.trim();
|
|
3775
4425
|
if (configured !== void 0 && configured.length > 0) {
|
|
@@ -3778,17 +4428,95 @@ function defaultShell() {
|
|
|
3778
4428
|
return process.platform === "win32" ? "powershell.exe" : "/bin/bash";
|
|
3779
4429
|
}
|
|
3780
4430
|
var ServerTerminalBackend = class {
|
|
3781
|
-
constructor(policy, logger) {
|
|
4431
|
+
constructor(policy, logger, config) {
|
|
3782
4432
|
this.policy = policy;
|
|
3783
4433
|
this.logger = logger;
|
|
4434
|
+
this.config = config;
|
|
4435
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
4436
|
+
}
|
|
4437
|
+
attach(terminalId, options, sink) {
|
|
4438
|
+
const entry = this.entryFor(terminalId);
|
|
4439
|
+
if (entry.pty === void 0) {
|
|
4440
|
+
this.spawn(terminalId, entry, options);
|
|
4441
|
+
} else {
|
|
4442
|
+
this.resize(entry, options.cols, options.rows);
|
|
4443
|
+
}
|
|
4444
|
+
entry.sinks.add(sink);
|
|
4445
|
+
this.cancelIdle(entry);
|
|
4446
|
+
return {
|
|
4447
|
+
replay: entry.output,
|
|
4448
|
+
write: (data) => entry.pty?.write(data),
|
|
4449
|
+
resize: (cols, rows) => this.resize(entry, cols, rows),
|
|
4450
|
+
detach: () => this.detach(terminalId, entry, sink)
|
|
4451
|
+
};
|
|
4452
|
+
}
|
|
4453
|
+
/** Ends a shell for good, drops its scrollback, and tells any other viewer. */
|
|
4454
|
+
close(terminalId) {
|
|
4455
|
+
const entry = this.entries.get(terminalId);
|
|
4456
|
+
this.entries.delete(terminalId);
|
|
4457
|
+
if (entry !== void 0) {
|
|
4458
|
+
this.stopFlush(entry);
|
|
4459
|
+
this.cancelIdle(entry);
|
|
4460
|
+
entry.generation += 1;
|
|
4461
|
+
const sinks = [...entry.sinks];
|
|
4462
|
+
entry.sinks.clear();
|
|
4463
|
+
this.killPty(entry.pty);
|
|
4464
|
+
entry.pty = void 0;
|
|
4465
|
+
for (const sink of sinks) {
|
|
4466
|
+
sink.exit();
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
removeTerminalLog(this.config.dataDir, terminalId);
|
|
4470
|
+
}
|
|
4471
|
+
onModuleDestroy() {
|
|
4472
|
+
for (const [terminalId, entry] of this.entries) {
|
|
4473
|
+
entry.generation += 1;
|
|
4474
|
+
this.persist(terminalId, entry);
|
|
4475
|
+
this.killPty(entry.pty);
|
|
4476
|
+
entry.pty = void 0;
|
|
4477
|
+
this.stopFlush(entry);
|
|
4478
|
+
this.cancelIdle(entry);
|
|
4479
|
+
}
|
|
4480
|
+
this.entries.clear();
|
|
3784
4481
|
}
|
|
3785
|
-
|
|
4482
|
+
/** SIGTERM, then SIGKILL for a shell that ignores it (a hung process group). */
|
|
4483
|
+
killPty(pty) {
|
|
4484
|
+
if (pty === void 0) {
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
4487
|
+
try {
|
|
4488
|
+
pty.kill();
|
|
4489
|
+
} catch {
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
const timer = setTimeout(() => {
|
|
4493
|
+
try {
|
|
4494
|
+
pty.kill("SIGKILL");
|
|
4495
|
+
} catch {
|
|
4496
|
+
}
|
|
4497
|
+
}, SIGKILL_AFTER_MS);
|
|
4498
|
+
timer.unref?.();
|
|
4499
|
+
}
|
|
4500
|
+
entryFor(terminalId) {
|
|
4501
|
+
const existing = this.entries.get(terminalId);
|
|
4502
|
+
if (existing !== void 0) {
|
|
4503
|
+
return existing;
|
|
4504
|
+
}
|
|
4505
|
+
const entry = {
|
|
4506
|
+
sinks: /* @__PURE__ */ new Set(),
|
|
4507
|
+
output: readTerminalLog(this.config.dataDir, terminalId),
|
|
4508
|
+
dirty: false,
|
|
4509
|
+
generation: 0
|
|
4510
|
+
};
|
|
4511
|
+
this.entries.set(terminalId, entry);
|
|
4512
|
+
return entry;
|
|
4513
|
+
}
|
|
4514
|
+
spawn(terminalId, entry, options) {
|
|
3786
4515
|
if (!this.policy.canOpen(options.cwd)) {
|
|
3787
|
-
throw new UnsupportedByHostError(
|
|
3788
|
-
`Morse is not allowed to run a terminal in ${options.cwd}.`
|
|
3789
|
-
);
|
|
4516
|
+
throw new UnsupportedByHostError(`Morse is not allowed to run a terminal in ${options.cwd}.`);
|
|
3790
4517
|
}
|
|
3791
4518
|
const shell = defaultShell();
|
|
4519
|
+
const generation = ++entry.generation;
|
|
3792
4520
|
const pty = spawnPty(shell, [], {
|
|
3793
4521
|
name: "xterm-256color",
|
|
3794
4522
|
cols: options.cols,
|
|
@@ -3796,41 +4524,105 @@ var ServerTerminalBackend = class {
|
|
|
3796
4524
|
cwd: options.cwd,
|
|
3797
4525
|
env: { ...process.env, TERM: "xterm-256color", MORSE_TERMINAL: "1" }
|
|
3798
4526
|
});
|
|
4527
|
+
entry.pty = pty;
|
|
3799
4528
|
this.logger.info(`Terminal opened in ${options.cwd} (${shell}, pid ${pty.pid})`);
|
|
3800
|
-
|
|
3801
|
-
|
|
4529
|
+
pty.onData((chunk) => {
|
|
4530
|
+
if (entry.generation !== generation) {
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
entry.output += chunk;
|
|
4534
|
+
if (entry.output.length > TERMINAL_BUFFER_CHARS * 1.5) {
|
|
4535
|
+
entry.output = trimTail(entry.output, TERMINAL_BUFFER_CHARS);
|
|
4536
|
+
}
|
|
4537
|
+
entry.dirty = true;
|
|
4538
|
+
this.scheduleFlush(terminalId, entry);
|
|
4539
|
+
for (const sink of entry.sinks) {
|
|
4540
|
+
sink.output(chunk);
|
|
4541
|
+
}
|
|
4542
|
+
});
|
|
3802
4543
|
pty.onExit(({ exitCode }) => {
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
4544
|
+
if (entry.generation !== generation) {
|
|
4545
|
+
return;
|
|
4546
|
+
}
|
|
4547
|
+
entry.pty = void 0;
|
|
4548
|
+
this.persist(terminalId, entry);
|
|
4549
|
+
for (const sink of [...entry.sinks]) {
|
|
4550
|
+
sink.exit(exitCode);
|
|
4551
|
+
}
|
|
3806
4552
|
});
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
4553
|
+
}
|
|
4554
|
+
resize(entry, cols, rows) {
|
|
4555
|
+
try {
|
|
4556
|
+
entry.pty?.resize(cols, rows);
|
|
4557
|
+
} catch {
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
detach(terminalId, entry, sink) {
|
|
4561
|
+
entry.sinks.delete(sink);
|
|
4562
|
+
if (entry.sinks.size > 0) {
|
|
4563
|
+
return;
|
|
4564
|
+
}
|
|
4565
|
+
this.persist(terminalId, entry);
|
|
4566
|
+
this.scheduleIdle(terminalId, entry);
|
|
4567
|
+
}
|
|
4568
|
+
/**
|
|
4569
|
+
* Reclaims a shell nobody has reattached to. The log stays (the output should
|
|
4570
|
+
* still be there next time); only the process goes.
|
|
4571
|
+
*/
|
|
4572
|
+
scheduleIdle(terminalId, entry) {
|
|
4573
|
+
if (this.config.terminalIdleMs <= 0) {
|
|
4574
|
+
return;
|
|
4575
|
+
}
|
|
4576
|
+
this.cancelIdle(entry);
|
|
4577
|
+
entry.idleTimer = setTimeout(() => {
|
|
4578
|
+
entry.idleTimer = void 0;
|
|
4579
|
+
if (entry.sinks.size > 0) {
|
|
4580
|
+
return;
|
|
3826
4581
|
}
|
|
3827
|
-
|
|
4582
|
+
entry.generation += 1;
|
|
4583
|
+
this.killPty(entry.pty);
|
|
4584
|
+
entry.pty = void 0;
|
|
4585
|
+
this.persist(terminalId, entry);
|
|
4586
|
+
this.entries.delete(terminalId);
|
|
4587
|
+
}, this.config.terminalIdleMs);
|
|
4588
|
+
entry.idleTimer.unref?.();
|
|
4589
|
+
}
|
|
4590
|
+
cancelIdle(entry) {
|
|
4591
|
+
if (entry.idleTimer !== void 0) {
|
|
4592
|
+
clearTimeout(entry.idleTimer);
|
|
4593
|
+
entry.idleTimer = void 0;
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
scheduleFlush(terminalId, entry) {
|
|
4597
|
+
if (entry.flushTimer !== void 0) {
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
entry.flushTimer = setTimeout(() => {
|
|
4601
|
+
entry.flushTimer = void 0;
|
|
4602
|
+
this.persist(terminalId, entry);
|
|
4603
|
+
}, FLUSH_DEBOUNCE_MS);
|
|
4604
|
+
entry.flushTimer.unref?.();
|
|
4605
|
+
}
|
|
4606
|
+
stopFlush(entry) {
|
|
4607
|
+
if (entry.flushTimer !== void 0) {
|
|
4608
|
+
clearTimeout(entry.flushTimer);
|
|
4609
|
+
entry.flushTimer = void 0;
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
persist(terminalId, entry) {
|
|
4613
|
+
this.stopFlush(entry);
|
|
4614
|
+
if (!entry.dirty) {
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
entry.dirty = false;
|
|
4618
|
+
writeTerminalLog(this.config.dataDir, terminalId, entry.output);
|
|
3828
4619
|
}
|
|
3829
4620
|
};
|
|
3830
4621
|
ServerTerminalBackend = __decorateClass([
|
|
3831
4622
|
Injectable(),
|
|
3832
4623
|
__decorateParam(0, Inject2(MORSE_PROJECT_POLICY)),
|
|
3833
|
-
__decorateParam(1, Inject2(MORSE_LOGGER))
|
|
4624
|
+
__decorateParam(1, Inject2(MORSE_LOGGER)),
|
|
4625
|
+
__decorateParam(2, Inject2(MORSE_CONFIG))
|
|
3834
4626
|
], ServerTerminalBackend);
|
|
3835
4627
|
|
|
3836
4628
|
// ../server/src/internal/registry.ts
|
|
@@ -3870,9 +4662,9 @@ import { Inject as Inject5 } from "@nestjs/common";
|
|
|
3870
4662
|
import { Inject as Inject4, Injectable as Injectable3 } from "@nestjs/common";
|
|
3871
4663
|
|
|
3872
4664
|
// ../server/src/internal/uploads/upload-store.ts
|
|
3873
|
-
import { existsSync } from "node:fs";
|
|
4665
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
3874
4666
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3875
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
4667
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join7, relative, sep as sep3 } from "node:path";
|
|
3876
4668
|
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
3877
4669
|
var DEFAULT_UPLOAD_DIR = ".morse/uploads";
|
|
3878
4670
|
var UploadError = class extends Error {
|
|
@@ -3892,7 +4684,7 @@ async function saveUpload(cwd, request, uploadDir = DEFAULT_UPLOAD_DIR) {
|
|
|
3892
4684
|
`That file is ${formatMb(bytes.length)} MB; the limit is ${formatMb(MAX_UPLOAD_BYTES)} MB.`
|
|
3893
4685
|
);
|
|
3894
4686
|
}
|
|
3895
|
-
const directory = isAbsolute3(uploadDir) ? uploadDir :
|
|
4687
|
+
const directory = isAbsolute3(uploadDir) ? uploadDir : join7(cwd, uploadDir);
|
|
3896
4688
|
await mkdir(directory, { recursive: true });
|
|
3897
4689
|
const target = uniquePath(directory, name);
|
|
3898
4690
|
await writeFile(target, bytes);
|
|
@@ -3911,10 +4703,10 @@ function uniquePath(directory, name) {
|
|
|
3911
4703
|
const dot = name.lastIndexOf(".");
|
|
3912
4704
|
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
3913
4705
|
const extension = dot > 0 ? name.slice(dot) : "";
|
|
3914
|
-
let candidate =
|
|
4706
|
+
let candidate = join7(directory, name);
|
|
3915
4707
|
let suffix = 1;
|
|
3916
|
-
while (
|
|
3917
|
-
candidate =
|
|
4708
|
+
while (existsSync2(candidate)) {
|
|
4709
|
+
candidate = join7(directory, `${stem}-${suffix}${extension}`);
|
|
3918
4710
|
suffix += 1;
|
|
3919
4711
|
}
|
|
3920
4712
|
return candidate;
|
|
@@ -3930,8 +4722,8 @@ function formatMb(bytes) {
|
|
|
3930
4722
|
|
|
3931
4723
|
// ../server/src/internal/workspace/directory-browser.ts
|
|
3932
4724
|
import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
|
|
3933
|
-
import { homedir as
|
|
3934
|
-
import { basename as basename3, dirname, isAbsolute as isAbsolute4, join as
|
|
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";
|
|
3935
4727
|
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
3936
4728
|
"node_modules",
|
|
3937
4729
|
"dist",
|
|
@@ -3955,8 +4747,8 @@ async function browseDirectory(path, options) {
|
|
|
3955
4747
|
throw new Error(`A folder path must be absolute: ${requested}`);
|
|
3956
4748
|
}
|
|
3957
4749
|
const entries = await readdir3(start, { withFileTypes: true });
|
|
3958
|
-
const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path:
|
|
3959
|
-
const parent =
|
|
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);
|
|
3960
4752
|
return {
|
|
3961
4753
|
path: start,
|
|
3962
4754
|
parent: parent === start ? void 0 : parent,
|
|
@@ -3970,7 +4762,7 @@ function shouldSkip(name) {
|
|
|
3970
4762
|
}
|
|
3971
4763
|
async function isGitRepo(path) {
|
|
3972
4764
|
try {
|
|
3973
|
-
await stat3(
|
|
4765
|
+
await stat3(join8(path, ".git"));
|
|
3974
4766
|
return true;
|
|
3975
4767
|
} catch {
|
|
3976
4768
|
return false;
|
|
@@ -3979,7 +4771,7 @@ async function isGitRepo(path) {
|
|
|
3979
4771
|
function shortcutRoots(options) {
|
|
3980
4772
|
const seen = /* @__PURE__ */ new Set();
|
|
3981
4773
|
const roots = [];
|
|
3982
|
-
for (const candidate of [...options.roots ?? [],
|
|
4774
|
+
for (const candidate of [...options.roots ?? [], homedir4(), options.defaultPath]) {
|
|
3983
4775
|
if (!candidate || !isAbsolute4(candidate)) {
|
|
3984
4776
|
continue;
|
|
3985
4777
|
}
|
|
@@ -4041,7 +4833,7 @@ function toPosix(path) {
|
|
|
4041
4833
|
}
|
|
4042
4834
|
|
|
4043
4835
|
// ../server/src/internal/workspace/git-log.ts
|
|
4044
|
-
import { execFile } from "node:child_process";
|
|
4836
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
4045
4837
|
import { relative as relative3 } from "node:path";
|
|
4046
4838
|
var DEFAULT_MAX_COMMITS = 250;
|
|
4047
4839
|
var HARD_MAX_COMMITS = 500;
|
|
@@ -4092,9 +4884,9 @@ async function readGitStatus(cwd) {
|
|
|
4092
4884
|
gitText(["rev-parse", "--show-prefix"], cwd),
|
|
4093
4885
|
gitText(["status", "--porcelain=v1", "-z", "--untracked-files=all"], cwd)
|
|
4094
4886
|
]);
|
|
4095
|
-
return { isRepo: true, files:
|
|
4887
|
+
return { isRepo: true, files: parseStatus2(raw ?? "", prefix?.trim() ?? "") };
|
|
4096
4888
|
}
|
|
4097
|
-
function
|
|
4889
|
+
function parseStatus2(raw, root) {
|
|
4098
4890
|
const records = raw.split("\0");
|
|
4099
4891
|
const files = [];
|
|
4100
4892
|
for (let index = 0; index < records.length; index += 1) {
|
|
@@ -4338,7 +5130,7 @@ function parseRefs(raw) {
|
|
|
4338
5130
|
}
|
|
4339
5131
|
function gitText(args, cwd) {
|
|
4340
5132
|
return new Promise((resolve6) => {
|
|
4341
|
-
|
|
5133
|
+
execFile2(
|
|
4342
5134
|
"git",
|
|
4343
5135
|
args,
|
|
4344
5136
|
{ cwd, timeout: 5e3, maxBuffer: 16 * 1024 * 1024, windowsHide: true },
|
|
@@ -4354,7 +5146,7 @@ async function gitRun(args, cwd, timeout = 5e3) {
|
|
|
4354
5146
|
}
|
|
4355
5147
|
function gitExec(args, cwd, timeout = 5e3) {
|
|
4356
5148
|
return new Promise((resolve6) => {
|
|
4357
|
-
|
|
5149
|
+
execFile2(
|
|
4358
5150
|
"git",
|
|
4359
5151
|
args,
|
|
4360
5152
|
{
|
|
@@ -4382,9 +5174,9 @@ function gitExec(args, cwd, timeout = 5e3) {
|
|
|
4382
5174
|
}
|
|
4383
5175
|
|
|
4384
5176
|
// ../server/src/internal/workspace/workspace-index.ts
|
|
4385
|
-
import { execFile as
|
|
5177
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
4386
5178
|
import { readdir as readdir4 } from "node:fs/promises";
|
|
4387
|
-
import { join as
|
|
5179
|
+
import { join as join9, relative as relative4 } from "node:path";
|
|
4388
5180
|
var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set([
|
|
4389
5181
|
"node_modules",
|
|
4390
5182
|
"dist",
|
|
@@ -4469,7 +5261,7 @@ async function gitFileList(cwd) {
|
|
|
4469
5261
|
}
|
|
4470
5262
|
function runGit(args, cwd) {
|
|
4471
5263
|
return new Promise((resolve6) => {
|
|
4472
|
-
|
|
5264
|
+
execFile3(
|
|
4473
5265
|
"git",
|
|
4474
5266
|
args,
|
|
4475
5267
|
{ cwd, timeout: 5e3, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
|
|
@@ -4495,7 +5287,7 @@ async function walk(root, directory, files) {
|
|
|
4495
5287
|
if (files.length >= MAX_INDEXED_FILES) {
|
|
4496
5288
|
return;
|
|
4497
5289
|
}
|
|
4498
|
-
const full =
|
|
5290
|
+
const full = join9(directory, entry.name);
|
|
4499
5291
|
if (entry.isDirectory()) {
|
|
4500
5292
|
if (SKIPPED_DIRECTORIES2.has(entry.name) || entry.name.startsWith(".")) {
|
|
4501
5293
|
continue;
|
|
@@ -4513,20 +5305,20 @@ function toPosix3(path) {
|
|
|
4513
5305
|
}
|
|
4514
5306
|
|
|
4515
5307
|
// ../server/src/internal/workspace/workbench-store.ts
|
|
4516
|
-
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4517
|
-
import { join as
|
|
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";
|
|
4518
5310
|
var LAYOUT_FILE = "workbench.json";
|
|
4519
5311
|
var DRAFTS_FILE = "drafts.json";
|
|
4520
5312
|
var LAYOUT_MAX_BYTES = 256 * 1024;
|
|
4521
5313
|
var DRAFTS_MAX_BYTES = 32 * 1024 * 1024;
|
|
4522
5314
|
function readWorkbench(dataDir) {
|
|
4523
|
-
return readSlot(
|
|
5315
|
+
return readSlot(join10(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
|
|
4524
5316
|
}
|
|
4525
5317
|
function saveWorkbench(dataDir, snapshot) {
|
|
4526
5318
|
return saveSlot(dataDir, LAYOUT_FILE, snapshot, LAYOUT_MAX_BYTES);
|
|
4527
5319
|
}
|
|
4528
5320
|
function readDrafts(dataDir) {
|
|
4529
|
-
return readSlot(
|
|
5321
|
+
return readSlot(join10(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
|
|
4530
5322
|
}
|
|
4531
5323
|
function saveDrafts(dataDir, snapshot) {
|
|
4532
5324
|
return saveSlot(dataDir, DRAFTS_FILE, snapshot, DRAFTS_MAX_BYTES);
|
|
@@ -4534,7 +5326,7 @@ function saveDrafts(dataDir, snapshot) {
|
|
|
4534
5326
|
function readSlot(path, maxBytes) {
|
|
4535
5327
|
let raw;
|
|
4536
5328
|
try {
|
|
4537
|
-
raw =
|
|
5329
|
+
raw = readFileSync3(path, "utf8");
|
|
4538
5330
|
} catch {
|
|
4539
5331
|
return void 0;
|
|
4540
5332
|
}
|
|
@@ -4558,19 +5350,19 @@ function saveSlot(dataDir, file, snapshot, maxBytes) {
|
|
|
4558
5350
|
if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
|
|
4559
5351
|
throw new UnsupportedByHostError(`That ${file} snapshot is too large to save.`);
|
|
4560
5352
|
}
|
|
4561
|
-
const target =
|
|
5353
|
+
const target = join10(dataDir, file);
|
|
4562
5354
|
const temp = `${target}.${process.pid}.tmp`;
|
|
4563
5355
|
try {
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
5356
|
+
mkdirSync3(dataDir, { recursive: true });
|
|
5357
|
+
writeFileSync3(temp, serialized);
|
|
5358
|
+
renameSync2(temp, target);
|
|
4567
5359
|
return true;
|
|
4568
5360
|
} catch {
|
|
4569
5361
|
try {
|
|
4570
|
-
|
|
5362
|
+
writeFileSync3(target, serialized);
|
|
4571
5363
|
return true;
|
|
4572
5364
|
} catch {
|
|
4573
|
-
|
|
5365
|
+
rmSync2(temp, { force: true });
|
|
4574
5366
|
return false;
|
|
4575
5367
|
}
|
|
4576
5368
|
}
|
|
@@ -4589,13 +5381,14 @@ function asSnapshot(value) {
|
|
|
4589
5381
|
|
|
4590
5382
|
// ../server/src/internal/ws/session-factory.service.ts
|
|
4591
5383
|
var MorseSessionFactory = class {
|
|
4592
|
-
constructor(registry, transcripts, policy, logger, config, terminal) {
|
|
5384
|
+
constructor(registry, transcripts, policy, logger, config, terminal, pi) {
|
|
4593
5385
|
this.registry = registry;
|
|
4594
5386
|
this.transcripts = transcripts;
|
|
4595
5387
|
this.policy = policy;
|
|
4596
5388
|
this.logger = logger;
|
|
4597
5389
|
this.config = config;
|
|
4598
5390
|
this.terminal = terminal;
|
|
5391
|
+
this.pi = pi;
|
|
4599
5392
|
}
|
|
4600
5393
|
capabilities() {
|
|
4601
5394
|
return {
|
|
@@ -4640,7 +5433,10 @@ var MorseSessionFactory = class {
|
|
|
4640
5433
|
// A page served by this host may ask the registry for the latest release, so
|
|
4641
5434
|
// the sidebar can say when a newer Morse is out. A host without it stays
|
|
4642
5435
|
// quiet; this one is on unless `MORSE_UPDATE_CHECK=0` says otherwise.
|
|
4643
|
-
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
|
|
4644
5440
|
};
|
|
4645
5441
|
}
|
|
4646
5442
|
createFor(emit) {
|
|
@@ -4785,6 +5581,31 @@ var MorseSessionFactory = class {
|
|
|
4785
5581
|
const branch = typeof args?.branch === "string" ? args.branch : "";
|
|
4786
5582
|
return checkoutGit(cwd, branch, args?.create === true);
|
|
4787
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
|
+
}
|
|
4788
5609
|
default:
|
|
4789
5610
|
throw new UnsupportedByHostError(`This host does not support "${command}".`);
|
|
4790
5611
|
}
|
|
@@ -4834,7 +5655,8 @@ MorseSessionFactory = __decorateClass([
|
|
|
4834
5655
|
__decorateParam(2, Inject4(MORSE_PROJECT_POLICY)),
|
|
4835
5656
|
__decorateParam(3, Inject4(MORSE_LOGGER)),
|
|
4836
5657
|
__decorateParam(4, Inject4(MORSE_CONFIG)),
|
|
4837
|
-
__decorateParam(5, Inject4(ServerTerminalBackend))
|
|
5658
|
+
__decorateParam(5, Inject4(ServerTerminalBackend)),
|
|
5659
|
+
__decorateParam(6, Inject4(MORSE_PI_ADAPTER))
|
|
4838
5660
|
], MorseSessionFactory);
|
|
4839
5661
|
function gitPaths(args) {
|
|
4840
5662
|
const paths = args?.paths;
|
|
@@ -4843,6 +5665,16 @@ function gitPaths(args) {
|
|
|
4843
5665
|
}
|
|
4844
5666
|
return paths.filter((path) => typeof path === "string" && path.length > 0);
|
|
4845
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
|
+
}
|
|
4846
5678
|
|
|
4847
5679
|
// ../server/src/internal/ws/morse.gateway.ts
|
|
4848
5680
|
var MorseGateway = class {
|
|
@@ -4892,19 +5724,19 @@ MorseGateway = __decorateClass([
|
|
|
4892
5724
|
], MorseGateway);
|
|
4893
5725
|
|
|
4894
5726
|
// ../server/src/app/config.ts
|
|
4895
|
-
import { basename as basename4, dirname as
|
|
4896
|
-
import { existsSync as
|
|
4897
|
-
import { homedir as
|
|
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";
|
|
4898
5730
|
import { fileURLToPath } from "node:url";
|
|
4899
5731
|
function loadConfig(env = process.env) {
|
|
4900
5732
|
const workspacePath = resolveWorkspacePath(env);
|
|
4901
5733
|
const configuredUiDir = env.MORSE_UI_DIR;
|
|
4902
|
-
const moduleDir =
|
|
4903
|
-
const bundledUiDir =
|
|
4904
|
-
const monorepoUiDir =
|
|
4905
|
-
const defaultUiDir =
|
|
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;
|
|
4906
5738
|
const defaultHost = "127.0.0.1";
|
|
4907
|
-
const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir :
|
|
5739
|
+
const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join11(process.cwd(), configuredUiDir) : defaultUiDir;
|
|
4908
5740
|
return {
|
|
4909
5741
|
host: env.MORSE_HOST ?? defaultHost,
|
|
4910
5742
|
port: parsePositiveInt(env.MORSE_PORT, 4399),
|
|
@@ -4920,6 +5752,7 @@ function loadConfig(env = process.env) {
|
|
|
4920
5752
|
projects: parseList(env.MORSE_PROJECTS),
|
|
4921
5753
|
uploadDir: env.MORSE_UPLOAD_DIR?.trim() || DEFAULT_UPLOAD_DIR,
|
|
4922
5754
|
dataDir: resolveDataDir(env),
|
|
5755
|
+
terminalIdleMs: parsePositiveInt(env.MORSE_TERMINAL_IDLE_MS, 30 * 6e4),
|
|
4923
5756
|
updateCheck: !isOff(env.MORSE_UPDATE_CHECK),
|
|
4924
5757
|
instance: env.MORSE_INSTANCE?.trim() || void 0
|
|
4925
5758
|
};
|
|
@@ -4927,7 +5760,7 @@ function loadConfig(env = process.env) {
|
|
|
4927
5760
|
function readFrontendIdentity(uiDir) {
|
|
4928
5761
|
try {
|
|
4929
5762
|
return frontendIdentity(
|
|
4930
|
-
parseFrontendManifest(
|
|
5763
|
+
parseFrontendManifest(readFileSync4(join11(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
|
|
4931
5764
|
);
|
|
4932
5765
|
} catch {
|
|
4933
5766
|
return void 0;
|
|
@@ -4935,7 +5768,7 @@ function readFrontendIdentity(uiDir) {
|
|
|
4935
5768
|
}
|
|
4936
5769
|
function resolveDataDir(env) {
|
|
4937
5770
|
const explicit = env.MORSE_HOME?.trim();
|
|
4938
|
-
return explicit && explicit.length > 0 ? explicit :
|
|
5771
|
+
return explicit && explicit.length > 0 ? explicit : join11(homedir5(), ".morse");
|
|
4939
5772
|
}
|
|
4940
5773
|
function parsePositiveInt(value, fallback) {
|
|
4941
5774
|
if (!value) {
|
|
@@ -4962,11 +5795,11 @@ function resolveWorkspacePath(env) {
|
|
|
4962
5795
|
const start = process.cwd();
|
|
4963
5796
|
let directory = start;
|
|
4964
5797
|
for (; ; ) {
|
|
4965
|
-
const manifest =
|
|
4966
|
-
if (
|
|
5798
|
+
const manifest = join11(directory, "package.json");
|
|
5799
|
+
if (existsSync3(manifest) && declaresWorkspaces(manifest)) {
|
|
4967
5800
|
return directory;
|
|
4968
5801
|
}
|
|
4969
|
-
const parent =
|
|
5802
|
+
const parent = dirname4(directory);
|
|
4970
5803
|
if (parent === directory) {
|
|
4971
5804
|
return start;
|
|
4972
5805
|
}
|
|
@@ -4975,7 +5808,7 @@ function resolveWorkspacePath(env) {
|
|
|
4975
5808
|
}
|
|
4976
5809
|
function declaresWorkspaces(manifestPath) {
|
|
4977
5810
|
try {
|
|
4978
|
-
const manifest = JSON.parse(
|
|
5811
|
+
const manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
4979
5812
|
return manifest.workspaces !== void 0;
|
|
4980
5813
|
} catch {
|
|
4981
5814
|
return false;
|