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