@supanadit/morse-web 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -32,6 +32,7 @@ var CLIENT_MESSAGE_TYPES = [
32
32
  "session/compact",
33
33
  "history/load",
34
34
  "session/list",
35
+ "commands/refresh",
35
36
  "project/list",
36
37
  "project/open",
37
38
  "model/set",
@@ -104,6 +105,11 @@ function frontendIdentity(manifest) {
104
105
  // ../server/src/app.module.ts
105
106
  import { Module } from "@nestjs/common";
106
107
 
108
+ // ../adapter-pi-rpc/dist/pi-rpc-agent.js
109
+ import { readdir, readFile, stat } from "node:fs/promises";
110
+ import { homedir } from "node:os";
111
+ import { basename, join, resolve } from "node:path";
112
+
107
113
  // ../core/dist/domain.js
108
114
  var THINKING_LEVELS = [
109
115
  "off",
@@ -205,7 +211,12 @@ var SessionRegistry = class {
205
211
  if (this.draftCache) {
206
212
  return this.draftCache;
207
213
  }
208
- this.draftProbe ??= this.probeDraft();
214
+ if (!this.draftProbe) {
215
+ this.draftProbe = this.probeDraft().catch((error) => {
216
+ this.draftProbe = void 0;
217
+ throw error;
218
+ });
219
+ }
209
220
  return this.draftProbe;
210
221
  }
211
222
  /** Opens a new (or resumes an existing) session and makes it active. */
@@ -435,6 +446,9 @@ var SessionRegistry = class {
435
446
  await gateway.dispose();
436
447
  return state;
437
448
  } catch (error) {
449
+ if (error instanceof AgentUnavailableError) {
450
+ throw error;
451
+ }
438
452
  this.deps.logger.warn("Could not probe the draft model catalog", error);
439
453
  return void 0;
440
454
  }
@@ -1094,14 +1108,14 @@ var PiRpcClient = class {
1094
1108
  throw this.failed ?? new AgentUnavailableError("The pi agent is not running.");
1095
1109
  }
1096
1110
  const id = `morse-${this.nextId++}`;
1097
- return new Promise((resolve4, reject) => {
1111
+ return new Promise((resolve5, reject) => {
1098
1112
  const timer = setTimeout(() => {
1099
1113
  this.pending.delete(id);
1100
1114
  reject(new AgentProtocolError(`pi did not answer "${command.type}" within ${timeoutMs} ms.`));
1101
1115
  }, timeoutMs);
1102
1116
  this.pending.set(id, {
1103
1117
  command: command.type,
1104
- resolve: (data) => resolve4(data),
1118
+ resolve: (data) => resolve5(data),
1105
1119
  reject,
1106
1120
  timer
1107
1121
  });
@@ -1120,14 +1134,14 @@ var PiRpcClient = class {
1120
1134
  return;
1121
1135
  }
1122
1136
  let exited = false;
1123
- await new Promise((resolve4) => {
1137
+ await new Promise((resolve5) => {
1124
1138
  const finish = () => {
1125
1139
  exited = true;
1126
- resolve4();
1140
+ resolve5();
1127
1141
  };
1128
1142
  child.once("exit", finish);
1129
1143
  child.stdin.end();
1130
- setTimeout(resolve4, SHUTDOWN_GRACE_MS).unref?.();
1144
+ setTimeout(resolve5, SHUTDOWN_GRACE_MS).unref?.();
1131
1145
  });
1132
1146
  if (!exited) {
1133
1147
  child.kill();
@@ -1401,13 +1415,33 @@ var PiRpcAgent = class {
1401
1415
  thinkingLevel: thinkingLevel ?? this.sessionState.thinkingLevel,
1402
1416
  availableModels,
1403
1417
  availableThinkingLevels: availableThinkingLevels.length > 0 ? availableThinkingLevels : [...THINKING_LEVELS],
1404
- availableCommands: toCommands(commands?.commands),
1418
+ availableCommands: await buildCommandList(commands?.commands, this.commandContext()),
1405
1419
  streaming: state.isStreaming === true
1406
1420
  };
1407
1421
  this.ready = true;
1408
1422
  await this.refreshStats();
1409
1423
  this.emit({ type: "agent/ready", at: this.now(), state: this.snapshotState() });
1410
1424
  }
1425
+ /**
1426
+ * Re-reads the resources behind `get_commands` and reports the result the same
1427
+ * way a fresh spawn would. The palette calls it the moment it opens, so a new
1428
+ * or edited template is there by the time the reader finishes typing.
1429
+ */
1430
+ async refreshCommands() {
1431
+ if (this.disposed || !this.ready) {
1432
+ return;
1433
+ }
1434
+ const commands = await this.client.request({ type: "get_commands" }).catch(() => void 0);
1435
+ this.sessionState = {
1436
+ ...this.sessionState,
1437
+ availableCommands: await buildCommandList(commands?.commands, this.commandContext())
1438
+ };
1439
+ this.emit({ type: "agent/state", at: this.now(), state: this.snapshotState() });
1440
+ }
1441
+ /** Where a command list is rebuilt from: the session's workspace and env. */
1442
+ commandContext() {
1443
+ return { cwd: this.options.workspace.cwd, env: this.options.env };
1444
+ }
1411
1445
  /**
1412
1446
  * Pulls the cumulative token/cost/context totals pi keeps for the session, so
1413
1447
  * the footer can show the same numbers the TUI does. Best-effort: an older pi
@@ -1547,14 +1581,141 @@ function dedupeThinkingLevels(levels) {
1547
1581
  function toCommandSource(value) {
1548
1582
  return value === "prompt" || value === "skill" ? value : "extension";
1549
1583
  }
1550
- function toCommands(items) {
1551
- return (items ?? []).filter((item) => {
1552
- return typeof item.name === "string" && item.name.trim().length > 0;
1553
- }).map((item) => ({
1554
- name: item.name,
1555
- description: item.description,
1556
- source: toCommandSource(item.source)
1557
- }));
1584
+ async function readTemplateFile(path) {
1585
+ if (path === void 0 || path.length === 0) {
1586
+ return void 0;
1587
+ }
1588
+ try {
1589
+ return await readFile(path, "utf8");
1590
+ } catch {
1591
+ return void 0;
1592
+ }
1593
+ }
1594
+ async function buildCommandList(items, context) {
1595
+ const commands = [];
1596
+ const seen = /* @__PURE__ */ new Set();
1597
+ let projectTrusted = false;
1598
+ for (const item of items ?? []) {
1599
+ if (typeof item.name !== "string" || item.name.trim().length === 0) {
1600
+ continue;
1601
+ }
1602
+ const source = toCommandSource(item.source);
1603
+ if (source !== "prompt") {
1604
+ commands.push({ name: item.name, description: item.description, source });
1605
+ seen.add(item.name);
1606
+ continue;
1607
+ }
1608
+ if (item.sourceInfo?.scope === "project") {
1609
+ projectTrusted = true;
1610
+ }
1611
+ const template = await readTemplateFile(item.sourceInfo?.path);
1612
+ if (template === void 0) {
1613
+ continue;
1614
+ }
1615
+ commands.push({
1616
+ name: item.name,
1617
+ // Prefer the freshly parsed description so an edited frontmatter reflects
1618
+ // too; pi's cached description is the fallback.
1619
+ description: promptDescription(template) ?? item.description,
1620
+ source,
1621
+ template
1622
+ });
1623
+ seen.add(item.name);
1624
+ }
1625
+ for (const dir of await promptDirs(projectTrusted, context)) {
1626
+ for (const file of await listPromptFiles(dir)) {
1627
+ if (seen.has(file.name)) {
1628
+ continue;
1629
+ }
1630
+ const template = await readTemplateFile(file.path);
1631
+ if (template === void 0) {
1632
+ continue;
1633
+ }
1634
+ commands.push({
1635
+ name: file.name,
1636
+ description: promptDescription(template),
1637
+ source: "prompt",
1638
+ template
1639
+ });
1640
+ seen.add(file.name);
1641
+ }
1642
+ }
1643
+ return commands;
1644
+ }
1645
+ async function promptDirs(projectTrusted, context) {
1646
+ const dirs = [join(agentDir(context.env), "prompts")];
1647
+ if (projectTrusted || await isProjectTrusted(context.cwd, context.env)) {
1648
+ dirs.push(resolve(context.cwd, ".pi", "prompts"));
1649
+ }
1650
+ return dirs;
1651
+ }
1652
+ function agentDir(env) {
1653
+ const configured = env?.PI_CODING_AGENT_DIR?.trim();
1654
+ return configured && configured.length > 0 ? expandHome(configured) : join(homedir(), ".pi", "agent");
1655
+ }
1656
+ function expandHome(path) {
1657
+ if (path === "~") {
1658
+ return homedir();
1659
+ }
1660
+ if (path.startsWith("~/") || path.startsWith("~\\")) {
1661
+ return join(homedir(), path.slice(2));
1662
+ }
1663
+ return path;
1664
+ }
1665
+ async function isProjectTrusted(cwd, env) {
1666
+ try {
1667
+ const raw = await readFile(join(agentDir(env), "trust.json"), "utf8");
1668
+ const decisions = JSON.parse(raw);
1669
+ return decisions[cwd] === true || decisions[resolve(cwd)] === true;
1670
+ } catch {
1671
+ return false;
1672
+ }
1673
+ }
1674
+ async function listPromptFiles(dir) {
1675
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
1676
+ const files = [];
1677
+ for (const entry of entries) {
1678
+ if (!entry.name.endsWith(".md")) {
1679
+ continue;
1680
+ }
1681
+ const path = join(dir, entry.name);
1682
+ let isFile = entry.isFile();
1683
+ if (entry.isSymbolicLink()) {
1684
+ try {
1685
+ isFile = (await stat(path)).isFile();
1686
+ } catch {
1687
+ continue;
1688
+ }
1689
+ }
1690
+ if (isFile) {
1691
+ files.push({ name: basename(entry.name, ".md"), path });
1692
+ }
1693
+ }
1694
+ return files;
1695
+ }
1696
+ function promptDescription(raw) {
1697
+ let body = raw;
1698
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
1699
+ if (frontmatter !== null) {
1700
+ const described = /^description:[ \t]*(.*)$/m.exec(frontmatter[1]);
1701
+ if (described !== null) {
1702
+ const value = unquote(described[1].trim());
1703
+ if (value.length > 0) {
1704
+ return value;
1705
+ }
1706
+ }
1707
+ body = raw.slice(frontmatter[0].length);
1708
+ }
1709
+ const firstLine = body.split("\n").find((line) => line.trim().length > 0);
1710
+ if (firstLine === void 0) {
1711
+ return void 0;
1712
+ }
1713
+ const trimmed = firstLine.trim();
1714
+ return trimmed.length > 60 ? `${trimmed.slice(0, 60)}...` : trimmed;
1715
+ }
1716
+ function unquote(value) {
1717
+ const match = /^(['"])([\s\S]*)\1$/.exec(value);
1718
+ return match === null ? value : match[2];
1558
1719
  }
1559
1720
  function toNoticeLevel(value) {
1560
1721
  if (value === "error") {
@@ -1590,8 +1751,8 @@ ${mentions}` : mentions;
1590
1751
 
1591
1752
  // ../adapter-pi-rpc/dist/internal/resolve-pi.js
1592
1753
  import { accessSync, constants } from "node:fs";
1593
- import { homedir } from "node:os";
1594
- import { delimiter, isAbsolute, join } from "node:path";
1754
+ import { homedir as homedir2 } from "node:os";
1755
+ import { delimiter, isAbsolute, join as join2 } from "node:path";
1595
1756
  var PI_INSTALL_COMMAND = "npm install -g @earendil-works/pi-coding-agent";
1596
1757
  function buildRpcArgs(options) {
1597
1758
  const args = ["--mode", "rpc"];
@@ -1640,7 +1801,7 @@ function findOnPath(binary, env = process.env) {
1640
1801
  continue;
1641
1802
  }
1642
1803
  for (const extension of extensions) {
1643
- const candidate = join(directory, `${binary}${extension}`);
1804
+ const candidate = join2(directory, `${binary}${extension}`);
1644
1805
  if (isExecutableEntry(candidate)) {
1645
1806
  return candidate;
1646
1807
  }
@@ -1651,8 +1812,8 @@ function findOnPath(binary, env = process.env) {
1651
1812
  function resolveSessionDir(configured, env = process.env) {
1652
1813
  const fromEnv = env.PI_CODING_AGENT_SESSION_DIR;
1653
1814
  const value = configured ?? (fromEnv && fromEnv.length > 0 ? fromEnv : void 0);
1654
- const base = value ?? join(homedir(), ".pi", "agent", "sessions");
1655
- return isAbsolute(base) ? base : join(process.cwd(), base);
1815
+ const base = value ?? join2(homedir2(), ".pi", "agent", "sessions");
1816
+ return isAbsolute(base) ? base : join2(process.cwd(), base);
1656
1817
  }
1657
1818
  function isExecutableEntry(path) {
1658
1819
  try {
@@ -1719,8 +1880,8 @@ var PiRpcAgentFactory = class {
1719
1880
  };
1720
1881
 
1721
1882
  // ../adapter-pi-rpc/dist/pi-rpc-session-catalog.js
1722
- import { open, readdir, readFile, stat, unlink } from "node:fs/promises";
1723
- import { join as join2, resolve, sep } from "node:path";
1883
+ import { open, readdir as readdir2, readFile as readFile2, stat as stat2, unlink } from "node:fs/promises";
1884
+ import { join as join3, resolve as resolve2, sep } from "node:path";
1724
1885
  var TYPE_PREFIX = '{"type":"';
1725
1886
  function typeAt(raw, start, end) {
1726
1887
  if (!raw.startsWith(TYPE_PREFIX, start)) {
@@ -1795,8 +1956,8 @@ var PiRpcSessionCatalog = class {
1795
1956
  * a delete of an arbitrary file.
1796
1957
  */
1797
1958
  async remove(id) {
1798
- const path = resolve(id);
1799
- const directory = resolve(this.directory);
1959
+ const path = resolve2(id);
1960
+ const directory = resolve2(this.directory);
1800
1961
  if (!path.startsWith(`${directory}${sep}`) || !path.endsWith(".jsonl")) {
1801
1962
  throw new Error(`Refusing to delete a session outside ${directory}`);
1802
1963
  }
@@ -1809,13 +1970,13 @@ var PiRpcSessionCatalog = class {
1809
1970
  if (!bucket.isDirectory()) {
1810
1971
  continue;
1811
1972
  }
1812
- const bucketPath = join2(this.directory, bucket.name);
1973
+ const bucketPath = join3(this.directory, bucket.name);
1813
1974
  for (const entry of await safeReadDir(bucketPath)) {
1814
1975
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
1815
1976
  continue;
1816
1977
  }
1817
- const path = join2(bucketPath, entry.name);
1818
- const info = await stat(path).catch(() => void 0);
1978
+ const path = join3(bucketPath, entry.name);
1979
+ const info = await stat2(path).catch(() => void 0);
1819
1980
  files.push({ path, size: info?.size ?? 0, updatedAt: info?.mtimeMs ?? 0 });
1820
1981
  }
1821
1982
  }
@@ -1826,11 +1987,11 @@ var PiRpcSessionCatalog = class {
1826
1987
  async function readSessionSummary(path, updatedAt, previous) {
1827
1988
  let resumable = previous !== void 0 && previous.scanned > 0 && updatedAt >= previous.updatedAt;
1828
1989
  if (resumable && previous !== void 0) {
1829
- const info = await stat(path).catch(() => void 0);
1990
+ const info = await stat2(path).catch(() => void 0);
1830
1991
  resumable = info !== void 0 && info.size > previous.scanned;
1831
1992
  }
1832
1993
  const from = resumable && previous !== void 0 ? previous.scanned : 0;
1833
- const raw = resumable ? await readFrom(path, from).catch(() => void 0) : await readFile(path, "utf8").catch(() => void 0);
1994
+ const raw = resumable ? await readFrom(path, from).catch(() => void 0) : await readFile2(path, "utf8").catch(() => void 0);
1834
1995
  if (raw === void 0) {
1835
1996
  return void 0;
1836
1997
  }
@@ -1951,7 +2112,7 @@ function titleFromText(text) {
1951
2112
  }
1952
2113
  async function safeReadDir(path) {
1953
2114
  try {
1954
- return await readdir(path, { withFileTypes: true });
2115
+ return await readdir2(path, { withFileTypes: true });
1955
2116
  } catch {
1956
2117
  return [];
1957
2118
  }
@@ -2401,7 +2562,8 @@ function toCommandOption(command) {
2401
2562
  return {
2402
2563
  name: command.name,
2403
2564
  description: command.description,
2404
- source: command.source
2565
+ source: command.source,
2566
+ template: command.template
2405
2567
  };
2406
2568
  }
2407
2569
  function toThinkingLevel2(level) {
@@ -2591,6 +2753,9 @@ var HostSessionController = class {
2591
2753
  case "session/list":
2592
2754
  await this.publishSessions();
2593
2755
  return;
2756
+ case "commands/refresh":
2757
+ await this.refreshCommands();
2758
+ return;
2594
2759
  case "project/list":
2595
2760
  await this.publishProjects();
2596
2761
  return;
@@ -2660,7 +2825,15 @@ var HostSessionController = class {
2660
2825
  }
2661
2826
  /** Fills the draft pickers from the registry's session-less probe. */
2662
2827
  async warmDraft() {
2663
- this.draftCatalog = await this.options.services.registry.draftDefaults();
2828
+ try {
2829
+ this.draftCatalog = await this.options.services.registry.draftDefaults();
2830
+ } catch (error) {
2831
+ this.agentError = describeError(error);
2832
+ this.agentFailure = describeAgentFailure(error, this.options.agentHint);
2833
+ this.options.logger.warn("Could not probe the draft model catalog", error);
2834
+ this.emitState();
2835
+ return;
2836
+ }
2664
2837
  if (this.activeKey === void 0 && !this.disposed) {
2665
2838
  this.emitState();
2666
2839
  }
@@ -3013,6 +3186,18 @@ var HostSessionController = class {
3013
3186
  }
3014
3187
  await this.guard(() => gateway.respondToInteraction(response));
3015
3188
  }
3189
+ /**
3190
+ * The palette asked for a fresh command list. The agent re-reads the template
3191
+ * files and reports back through `agent/state`, so the composer sees a template
3192
+ * added, changed or deleted since the session started — without a pi reload.
3193
+ */
3194
+ async refreshCommands() {
3195
+ const gateway = this.options.services.registry.active();
3196
+ if (gateway?.refreshCommands === void 0) {
3197
+ return;
3198
+ }
3199
+ await this.guard(() => gateway.refreshCommands());
3200
+ }
3016
3201
  async runHostCommand(command, args, requestId) {
3017
3202
  const handler = this.options.onHostCommand;
3018
3203
  if (!handler) {
@@ -3353,8 +3538,8 @@ function hintOnly(hint) {
3353
3538
 
3354
3539
  // ../server/src/internal/http/health.controller.ts
3355
3540
  import { Controller, Get } from "@nestjs/common";
3356
- import { readFile as readFile2 } from "node:fs/promises";
3357
- import { join as join3 } from "node:path";
3541
+ import { readFile as readFile3 } from "node:fs/promises";
3542
+ import { join as join4 } from "node:path";
3358
3543
  import { Inject } from "@nestjs/common";
3359
3544
 
3360
3545
  // ../server/src/app/tokens.ts
@@ -3375,11 +3560,14 @@ var HealthController = class {
3375
3560
  status: "ok",
3376
3561
  protocolVersion: PROTOCOL_VERSION,
3377
3562
  workspace: this.config.workspace.cwd,
3378
- frontend: this.config.uiDir
3563
+ frontend: this.config.uiDir,
3564
+ // Echoed so the CLI that spawned this daemon can tell it apart from an
3565
+ // unrelated process that later reused the same pid.
3566
+ ...this.config.instance ? { instance: this.config.instance } : {}
3379
3567
  };
3380
3568
  }
3381
3569
  async manifest() {
3382
- const raw = await readFile2(join3(this.config.uiDir, "webview.manifest.json"), "utf8").catch(
3570
+ const raw = await readFile3(join4(this.config.uiDir, "webview.manifest.json"), "utf8").catch(
3383
3571
  () => void 0
3384
3572
  );
3385
3573
  return parseFrontendManifest(raw) ?? { name: "unknown", protocolVersion: PROTOCOL_VERSION };
@@ -3430,10 +3618,10 @@ function withDetail(message, detail) {
3430
3618
  }
3431
3619
 
3432
3620
  // ../server/src/internal/projects/project-policy.ts
3433
- import { isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
3621
+ import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep2 } from "node:path";
3434
3622
  var ServerProjectPolicy = class {
3435
3623
  constructor(roots) {
3436
- this.roots = roots.filter((root) => root.trim().length > 0).map((root) => resolve2(root));
3624
+ this.roots = roots.filter((root) => root.trim().length > 0).map((root) => resolve3(root));
3437
3625
  }
3438
3626
  get allowList() {
3439
3627
  return [...this.roots];
@@ -3445,7 +3633,7 @@ var ServerProjectPolicy = class {
3445
3633
  if (this.roots.length === 0) {
3446
3634
  return true;
3447
3635
  }
3448
- const target = resolve2(path);
3636
+ const target = resolve3(path);
3449
3637
  return this.roots.some(
3450
3638
  (root) => target === root || target.startsWith(`${root}${sep2}`)
3451
3639
  );
@@ -3491,7 +3679,7 @@ import { Inject as Inject3, Injectable as Injectable2 } from "@nestjs/common";
3491
3679
  // ../server/src/internal/uploads/upload-store.ts
3492
3680
  import { existsSync } from "node:fs";
3493
3681
  import { mkdir, writeFile } from "node:fs/promises";
3494
- import { basename, isAbsolute as isAbsolute3, join as join4, relative, sep as sep3 } from "node:path";
3682
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join5, relative, sep as sep3 } from "node:path";
3495
3683
  var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
3496
3684
  var DEFAULT_UPLOAD_DIR = ".morse/uploads";
3497
3685
  var UploadError = class extends Error {
@@ -3511,15 +3699,15 @@ async function saveUpload(cwd, request, uploadDir = DEFAULT_UPLOAD_DIR) {
3511
3699
  `That file is ${formatMb(bytes.length)} MB; the limit is ${formatMb(MAX_UPLOAD_BYTES)} MB.`
3512
3700
  );
3513
3701
  }
3514
- const directory = isAbsolute3(uploadDir) ? uploadDir : join4(cwd, uploadDir);
3702
+ const directory = isAbsolute3(uploadDir) ? uploadDir : join5(cwd, uploadDir);
3515
3703
  await mkdir(directory, { recursive: true });
3516
3704
  const target = uniquePath(directory, name);
3517
3705
  await writeFile(target, bytes);
3518
- return { path: mentionPath(cwd, target), name: basename(target), bytes: bytes.length };
3706
+ return { path: mentionPath(cwd, target), name: basename2(target), bytes: bytes.length };
3519
3707
  }
3520
3708
  function safeFileName(raw) {
3521
3709
  const value = typeof raw === "string" ? raw : "";
3522
- const base = basename(value.replace(/\\/g, "/")).trim();
3710
+ const base = basename2(value.replace(/\\/g, "/")).trim();
3523
3711
  const cleaned = base.replace(/[\u0000-\u001f<>:"|?*]/g, "_").replace(/^\.+/, "").trim();
3524
3712
  if (cleaned.length === 0) {
3525
3713
  throw new UploadError("That upload had no usable file name.");
@@ -3530,10 +3718,10 @@ function uniquePath(directory, name) {
3530
3718
  const dot = name.lastIndexOf(".");
3531
3719
  const stem = dot > 0 ? name.slice(0, dot) : name;
3532
3720
  const extension = dot > 0 ? name.slice(dot) : "";
3533
- let candidate = join4(directory, name);
3721
+ let candidate = join5(directory, name);
3534
3722
  let suffix = 1;
3535
3723
  while (existsSync(candidate)) {
3536
- candidate = join4(directory, `${stem}-${suffix}${extension}`);
3724
+ candidate = join5(directory, `${stem}-${suffix}${extension}`);
3537
3725
  suffix += 1;
3538
3726
  }
3539
3727
  return candidate;
@@ -3548,9 +3736,9 @@ function formatMb(bytes) {
3548
3736
  }
3549
3737
 
3550
3738
  // ../server/src/internal/workspace/directory-browser.ts
3551
- import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
3552
- import { homedir as homedir2 } from "node:os";
3553
- import { basename as basename2, dirname, isAbsolute as isAbsolute4, join as join5, resolve as resolve3 } from "node:path";
3739
+ import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
3740
+ import { homedir as homedir3 } from "node:os";
3741
+ import { basename as basename3, dirname, isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "node:path";
3554
3742
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
3555
3743
  "node_modules",
3556
3744
  "dist",
@@ -3569,12 +3757,12 @@ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
3569
3757
  ]);
3570
3758
  async function browseDirectory(path, options) {
3571
3759
  const requested = path?.trim();
3572
- const start = resolve3(requested && requested.length > 0 ? requested : options.defaultPath);
3760
+ const start = resolve4(requested && requested.length > 0 ? requested : options.defaultPath);
3573
3761
  if (requested && !isAbsolute4(requested)) {
3574
3762
  throw new Error(`A folder path must be absolute: ${requested}`);
3575
3763
  }
3576
- const entries = await readdir2(start, { withFileTypes: true });
3577
- const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path: join5(start, entry.name) })).sort((left, right) => left.name.localeCompare(right.name));
3764
+ const entries = await readdir3(start, { withFileTypes: true });
3765
+ const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path: join6(start, entry.name) })).sort((left, right) => left.name.localeCompare(right.name));
3578
3766
  const parent = dirname(start);
3579
3767
  return {
3580
3768
  path: start,
@@ -3589,7 +3777,7 @@ function shouldSkip(name) {
3589
3777
  }
3590
3778
  async function isGitRepo(path) {
3591
3779
  try {
3592
- await stat2(join5(path, ".git"));
3780
+ await stat3(join6(path, ".git"));
3593
3781
  return true;
3594
3782
  } catch {
3595
3783
  return false;
@@ -3598,24 +3786,24 @@ async function isGitRepo(path) {
3598
3786
  function shortcutRoots(options) {
3599
3787
  const seen = /* @__PURE__ */ new Set();
3600
3788
  const roots = [];
3601
- for (const candidate of [...options.roots ?? [], homedir2(), options.defaultPath]) {
3789
+ for (const candidate of [...options.roots ?? [], homedir3(), options.defaultPath]) {
3602
3790
  if (!candidate || !isAbsolute4(candidate)) {
3603
3791
  continue;
3604
3792
  }
3605
- const path = resolve3(candidate);
3793
+ const path = resolve4(candidate);
3606
3794
  if (seen.has(path)) {
3607
3795
  continue;
3608
3796
  }
3609
3797
  seen.add(path);
3610
- roots.push({ name: basename2(path) || path, path });
3798
+ roots.push({ name: basename3(path) || path, path });
3611
3799
  }
3612
3800
  return roots;
3613
3801
  }
3614
3802
 
3615
3803
  // ../server/src/internal/workspace/workspace-index.ts
3616
3804
  import { execFile } from "node:child_process";
3617
- import { readdir as readdir3 } from "node:fs/promises";
3618
- import { join as join6, relative as relative2 } from "node:path";
3805
+ import { readdir as readdir4 } from "node:fs/promises";
3806
+ import { join as join7, relative as relative2 } from "node:path";
3619
3807
  var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set([
3620
3808
  "node_modules",
3621
3809
  "dist",
@@ -3699,12 +3887,12 @@ async function gitFileList(cwd) {
3699
3887
  }
3700
3888
  }
3701
3889
  function runGit(args, cwd) {
3702
- return new Promise((resolve4) => {
3890
+ return new Promise((resolve5) => {
3703
3891
  execFile(
3704
3892
  "git",
3705
3893
  args,
3706
3894
  { cwd, timeout: 5e3, maxBuffer: 8 * 1024 * 1024, windowsHide: true },
3707
- (error, stdout) => resolve4(error ? void 0 : stdout)
3895
+ (error, stdout) => resolve5(error ? void 0 : stdout)
3708
3896
  );
3709
3897
  });
3710
3898
  }
@@ -3718,7 +3906,7 @@ async function walk(root, directory, files) {
3718
3906
  }
3719
3907
  let entries;
3720
3908
  try {
3721
- entries = await readdir3(directory, { withFileTypes: true });
3909
+ entries = await readdir4(directory, { withFileTypes: true });
3722
3910
  } catch {
3723
3911
  return;
3724
3912
  }
@@ -3726,7 +3914,7 @@ async function walk(root, directory, files) {
3726
3914
  if (files.length >= MAX_INDEXED_FILES) {
3727
3915
  return;
3728
3916
  }
3729
- const full = join6(directory, entry.name);
3917
+ const full = join7(directory, entry.name);
3730
3918
  if (entry.isDirectory()) {
3731
3919
  if (SKIPPED_DIRECTORIES2.has(entry.name) || entry.name.startsWith(".")) {
3732
3920
  continue;
@@ -3921,24 +4109,24 @@ MorseGateway = __decorateClass([
3921
4109
  ], MorseGateway);
3922
4110
 
3923
4111
  // ../server/src/app/config.ts
3924
- import { basename as basename3, dirname as dirname2, isAbsolute as isAbsolute5, join as join7 } from "node:path";
4112
+ import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute5, join as join8 } from "node:path";
3925
4113
  import { existsSync as existsSync2, readFileSync } from "node:fs";
3926
4114
  import { fileURLToPath } from "node:url";
3927
4115
  function loadConfig(env = process.env) {
3928
4116
  const workspacePath = resolveWorkspacePath(env);
3929
4117
  const configuredUiDir = env.MORSE_UI_DIR;
3930
4118
  const moduleDir = dirname2(fileURLToPath(import.meta.url));
3931
- const bundledUiDir = join7(moduleDir, "ui");
3932
- const monorepoUiDir = join7(moduleDir, "..", "..", "..", "ui-angular", "dist");
3933
- const defaultUiDir = existsSync2(join7(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
4119
+ const bundledUiDir = join8(moduleDir, "ui");
4120
+ const monorepoUiDir = join8(moduleDir, "..", "..", "..", "ui-angular", "dist");
4121
+ const defaultUiDir = existsSync2(join8(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
3934
4122
  const defaultHost = "127.0.0.1";
3935
- const uiDir = configuredUiDir ? isAbsolute5(configuredUiDir) ? configuredUiDir : join7(process.cwd(), configuredUiDir) : defaultUiDir;
4123
+ const uiDir = configuredUiDir ? isAbsolute5(configuredUiDir) ? configuredUiDir : join8(process.cwd(), configuredUiDir) : defaultUiDir;
3936
4124
  return {
3937
4125
  host: env.MORSE_HOST ?? defaultHost,
3938
4126
  port: parsePositiveInt(env.MORSE_PORT, 4399),
3939
4127
  uiDir,
3940
4128
  frontend: readFrontendIdentity(uiDir),
3941
- workspace: { cwd: workspacePath, name: basename3(workspacePath) || workspacePath },
4129
+ workspace: { cwd: workspacePath, name: basename4(workspacePath) || workspacePath },
3942
4130
  piPath: env.MORSE_PI_PATH,
3943
4131
  nodeEntryPath: env.MORSE_PI_ENTRY,
3944
4132
  sessionDir: env.MORSE_SESSION_DIR,
@@ -3947,13 +4135,14 @@ function loadConfig(env = process.env) {
3947
4135
  hotSessions: parsePositiveInt(env.MORSE_HOT_SESSIONS, 4),
3948
4136
  projects: parseList(env.MORSE_PROJECTS),
3949
4137
  uploadDir: env.MORSE_UPLOAD_DIR?.trim() || DEFAULT_UPLOAD_DIR,
3950
- updateCheck: !isOff(env.MORSE_UPDATE_CHECK)
4138
+ updateCheck: !isOff(env.MORSE_UPDATE_CHECK),
4139
+ instance: env.MORSE_INSTANCE?.trim() || void 0
3951
4140
  };
3952
4141
  }
3953
4142
  function readFrontendIdentity(uiDir) {
3954
4143
  try {
3955
4144
  return frontendIdentity(
3956
- parseFrontendManifest(readFileSync(join7(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
4145
+ parseFrontendManifest(readFileSync(join8(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
3957
4146
  );
3958
4147
  } catch {
3959
4148
  return void 0;
@@ -3984,7 +4173,7 @@ function resolveWorkspacePath(env) {
3984
4173
  const start = process.cwd();
3985
4174
  let directory = start;
3986
4175
  for (; ; ) {
3987
- const manifest = join7(directory, "package.json");
4176
+ const manifest = join8(directory, "package.json");
3988
4177
  if (existsSync2(manifest) && declaresWorkspaces(manifest)) {
3989
4178
  return directory;
3990
4179
  }