@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/README.md +53 -10
- package/dist/server.mjs +692 -85
- package/dist/server.mjs.map +4 -4
- package/dist/ui/3rdpartylicenses.txt +4 -4
- package/dist/ui/{chunk-D1etuzzZ.js → chunk-CwVAC7xx.js} +1 -1
- package/dist/ui/{chunk-CVTlMVVu.js → chunk-Dk_j780x.js} +1 -1
- package/dist/ui/{chunk-ourdPesa.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-TLDRX4Y6.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,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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
3832
|
-
|
|
4398
|
+
mkdirSync2(join6(dataDir, LOG_DIR), { recursive: true });
|
|
4399
|
+
writeFileSync2(temp, trimmed);
|
|
3833
4400
|
renameSync(temp, target);
|
|
3834
4401
|
} catch {
|
|
3835
4402
|
try {
|
|
3836
|
-
|
|
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
|
|
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
|
|
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 :
|
|
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 =
|
|
4706
|
+
let candidate = join7(directory, name);
|
|
4140
4707
|
let suffix = 1;
|
|
4141
|
-
while (
|
|
4142
|
-
candidate =
|
|
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
|
|
4159
|
-
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";
|
|
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:
|
|
4184
|
-
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);
|
|
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(
|
|
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 ?? [],
|
|
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:
|
|
4887
|
+
return { isRepo: true, files: parseStatus2(raw ?? "", prefix?.trim() ?? "") };
|
|
4321
4888
|
}
|
|
4322
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5177
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
4611
5178
|
import { readdir as readdir4 } from "node:fs/promises";
|
|
4612
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
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
|
|
4742
|
-
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";
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
5353
|
+
const target = join10(dataDir, file);
|
|
4787
5354
|
const temp = `${target}.${process.pid}.tmp`;
|
|
4788
5355
|
try {
|
|
4789
|
-
|
|
4790
|
-
|
|
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
|
-
|
|
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
|
|
5121
|
-
import { existsSync as
|
|
5122
|
-
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";
|
|
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 =
|
|
5128
|
-
const bundledUiDir =
|
|
5129
|
-
const monorepoUiDir =
|
|
5130
|
-
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;
|
|
5131
5738
|
const defaultHost = "127.0.0.1";
|
|
5132
|
-
const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir :
|
|
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(
|
|
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 :
|
|
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 =
|
|
5192
|
-
if (
|
|
5798
|
+
const manifest = join11(directory, "package.json");
|
|
5799
|
+
if (existsSync3(manifest) && declaresWorkspaces(manifest)) {
|
|
5193
5800
|
return directory;
|
|
5194
5801
|
}
|
|
5195
|
-
const parent =
|
|
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(
|
|
5811
|
+
const manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
5205
5812
|
return manifest.workspaces !== void 0;
|
|
5206
5813
|
} catch {
|
|
5207
5814
|
return false;
|