mellos-mapping 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
2
+ import { createRequire as __mellosCreateRequire } from 'node:module'; const require = __mellosCreateRequire(import.meta.url);
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -7196,10 +7196,10 @@ var require_dist = __commonJS({
7196
7196
  });
7197
7197
 
7198
7198
  // src/server/server.ts
7199
- import { existsSync as existsSync8, realpathSync as realpathSync4 } from "node:fs";
7199
+ import { existsSync as existsSync9, realpathSync as realpathSync5 } from "node:fs";
7200
7200
  import { homedir as homedir2 } from "node:os";
7201
7201
  import { join as join10 } from "node:path";
7202
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
7202
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
7203
7203
 
7204
7204
  // node_modules/zod/v3/external.js
7205
7205
  var external_exports = {};
@@ -23249,55 +23249,40 @@ function effectiveMappingPolicy(projectConfigFile, userConfigFile) {
23249
23249
  }
23250
23250
 
23251
23251
  // src/store/migration.ts
23252
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync as renameSync2 } from "node:fs";
23253
- import { dirname as dirname5, join as join4 } from "node:path";
23254
- var LEGACY_STATE_FILE_RELATIVE_PATH = join4(".claude", "mellos-mapping.json");
23255
- var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
23256
- var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
23257
- function migrateLegacyStore(defaultFile) {
23258
- const projectRoot = dirname5(dirname5(defaultFile));
23259
- const legacyDefault = join4(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
23260
- const legacyPages = join4(dirname5(legacyDefault), LEGACY_PAGES_DIR_NAME);
23261
- const legacyConfig = join4(dirname5(legacyDefault), LEGACY_CONFIG_FILE_NAME);
23262
- const hasLegacy = existsSync2(legacyDefault) || existsSync2(legacyPages) || existsSync2(legacyConfig);
23263
- const hasCurrent = existsSync2(defaultFile) || existsSync2(join4(dirname5(defaultFile), PAGES_DIR_NAME)) || existsSync2(configFilePath(defaultFile));
23264
- if (!hasLegacy || hasCurrent) return false;
23265
- mkdirSync2(dirname5(defaultFile), { recursive: true });
23266
- if (existsSync2(legacyDefault)) renameSync2(legacyDefault, defaultFile);
23267
- if (existsSync2(legacyPages)) renameSync2(legacyPages, join4(dirname5(defaultFile), PAGES_DIR_NAME));
23268
- if (existsSync2(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
23269
- return true;
23270
- }
23252
+ import { existsSync as existsSync3, renameSync as renameSync2 } from "node:fs";
23253
+ import { dirname as dirname6, join as join5 } from "node:path";
23271
23254
 
23272
- // src/store/maps.ts
23273
- import { readFileSync as readFileSync3 } from "node:fs";
23274
- function loadMapFile(path) {
23275
- let text2;
23276
- try {
23277
- text2 = readFileSync3(path, "utf8");
23278
- } catch (e) {
23279
- const code = e.code;
23280
- if (code === "ENOENT") return err({ kind: "not-found", path });
23281
- throw e;
23282
- }
23283
- let raw;
23284
- try {
23285
- raw = JSON.parse(stripBom(text2));
23286
- } catch (e) {
23287
- return err({ kind: "malformed-json", path, detail: e.message });
23255
+ // src/store/transaction.ts
23256
+ import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync2, openSync, realpathSync } from "node:fs";
23257
+ import { basename as basename2, dirname as dirname5, join as join4 } from "node:path";
23258
+ import { createHash } from "node:crypto";
23259
+
23260
+ // src/store/native-lock.ts
23261
+ import { existsSync as existsSync2 } from "node:fs";
23262
+ import { createRequire } from "node:module";
23263
+ import { fileURLToPath } from "node:url";
23264
+ var loaded;
23265
+ function nativeLock() {
23266
+ if (loaded) return loaded;
23267
+ if (Number(process.versions.napi ?? 0) < 9) {
23268
+ throw new Error("Project locks require Node-API 9 (Node 18.17+ or 20.3+).");
23269
+ }
23270
+ const candidates = [
23271
+ new URL("./native-lock.cjs", import.meta.url),
23272
+ new URL("../../dist/native-lock.cjs", import.meta.url)
23273
+ ];
23274
+ const entry = candidates.find((candidate) => existsSync2(candidate));
23275
+ if (!entry) throw new Error("The installed package is missing dist/native-lock.cjs; reinstall the complete package.");
23276
+ const backend = createRequire(import.meta.url)(fileURLToPath(entry));
23277
+ if (typeof backend.tryLock !== "function" || typeof backend.unlock !== "function") {
23278
+ throw new Error("The installed native lock backend is invalid.");
23288
23279
  }
23289
- return parseMap(raw, path);
23290
- }
23291
- function saveMapFile(path, map) {
23292
- const textError = mapTextError(map);
23293
- if (textError) return err({ kind: "save-failed", path, detail: textError });
23294
- return writeFileAtomic(path, serializeMap(map));
23280
+ loaded = backend;
23281
+ return backend;
23295
23282
  }
23296
23283
 
23297
23284
  // src/store/transaction.ts
23298
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync2 } from "node:fs";
23299
- import { dirname as dirname6, join as join5 } from "node:path";
23300
- import { randomUUID, createHash } from "node:crypto";
23285
+ var STORE_LOCK_PROTOCOL = "os-file-v1";
23301
23286
  var LedgerError = class extends Error {
23302
23287
  constructor(code, message, details = {}) {
23303
23288
  super(message);
@@ -23310,84 +23295,147 @@ function assertRevision(actual, expected) {
23310
23295
  if (expected !== void 0 && expected !== actual) throw new LedgerError("CONFLICT", "Map changed; read the current revision before retrying.", { expectedRevision: expected, actualRevision: actual });
23311
23296
  }
23312
23297
  function storeDirectory(file) {
23313
- const dir = dirname6(file);
23314
- return dir.endsWith("/pages") || dir.endsWith("\\pages") ? dirname6(dir) : dir;
23315
- }
23316
- function deadOwner(lock) {
23298
+ const dir = dirname5(file);
23299
+ if (basename2(dir).toLowerCase() === "pages") return dirname5(dir);
23300
+ let canonical = dir;
23317
23301
  try {
23318
- const owner = JSON.parse(readFileSync4(join5(lock, "owner.json"), "utf8"));
23319
- if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
23320
- try {
23321
- process.kill(owner.pid, 0);
23322
- return false;
23323
- } catch (e) {
23324
- return e.code === "ESRCH";
23325
- }
23326
- } catch {
23327
- return false;
23302
+ canonical = realpathSync.native(dir);
23303
+ } catch (error2) {
23304
+ if (error2.code !== "ENOENT") throw error2;
23305
+ }
23306
+ return basename2(canonical).toLowerCase() === "pages" ? dirname5(canonical) : canonical;
23307
+ }
23308
+ function checkLockPath(lock) {
23309
+ const entry = lstatSync(lock, { throwIfNoEntry: false });
23310
+ if (entry?.isDirectory()) {
23311
+ throw new LedgerError(
23312
+ "LOCK_MIGRATION_REQUIRED",
23313
+ `Legacy directory lock at ${lock}. Stop all old MCP servers, viewers and watchers for this project, then move that directory aside for inspection and retry. Never remove the new regular lock file.`,
23314
+ { path: lock }
23315
+ );
23316
+ }
23317
+ if (entry && !entry.isFile()) {
23318
+ throw new LedgerError("LOCK_UNAVAILABLE", `Project lock must be a regular file, not a symlink or special file: ${lock}`, { path: lock });
23328
23319
  }
23329
23320
  }
23330
23321
  function withStoreLock(file, action) {
23322
+ let backend;
23323
+ try {
23324
+ backend = nativeLock();
23325
+ } catch (error2) {
23326
+ throw new LedgerError("LOCK_UNAVAILABLE", `Cannot load the operating-system lock backend: ${error2 instanceof Error ? error2.message : String(error2)}`);
23327
+ }
23331
23328
  const dir = storeDirectory(file);
23332
- mkdirSync3(dir, { recursive: true });
23333
- const lock = join5(dir, ".write-lock");
23334
- const owner = join5(lock, "owner.json");
23335
- const token = randomUUID();
23329
+ mkdirSync2(dir, { recursive: true });
23330
+ const lock = join4(dir, ".write-lock");
23331
+ checkLockPath(lock);
23332
+ let fd;
23336
23333
  try {
23337
- mkdirSync3(lock);
23334
+ fd = openSync(lock, "a+", 384);
23338
23335
  } catch (error2) {
23339
- if (error2.code !== "EEXIST") throw error2;
23340
- if (!deadOwner(lock)) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes. An orphan without owner metadata needs manual inspection.`);
23341
- try {
23342
- mkdirSync3(join5(lock, ".reap"));
23343
- } catch {
23344
- throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
23345
- }
23346
- if (!deadOwner(lock)) {
23347
- rmSync4(join5(lock, ".reap"), { recursive: true, force: true });
23348
- throw new LedgerError("BUSY", "Writer ownership changed.");
23349
- }
23350
- rmSync4(lock, { recursive: true });
23351
- try {
23352
- mkdirSync3(lock);
23353
- } catch {
23354
- throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
23355
- }
23336
+ checkLockPath(lock);
23337
+ throw new LedgerError("LOCK_UNAVAILABLE", `Cannot open project lock ${lock}: ${error2 instanceof Error ? error2.message : String(error2)}`, { path: lock });
23356
23338
  }
23357
- let initialized = false;
23339
+ let acquired = false;
23358
23340
  try {
23359
- writeFileSync2(owner, JSON.stringify({ pid: process.pid, token }), { flag: "wx" });
23360
- initialized = true;
23341
+ checkLockPath(lock);
23342
+ if (!fstatSync(fd).isFile()) throw new LedgerError("LOCK_UNAVAILABLE", `Project lock is not a regular file: ${lock}`);
23343
+ try {
23344
+ acquired = backend.tryLock(fd);
23345
+ } catch (error2) {
23346
+ throw new LedgerError("LOCK_UNAVAILABLE", `Cannot acquire project lock ${lock}: ${error2 instanceof Error ? error2.message : String(error2)}`, { path: lock });
23347
+ }
23348
+ if (!acquired) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes.`);
23361
23349
  return action();
23362
23350
  } finally {
23363
23351
  try {
23364
- if (!initialized || JSON.parse(readFileSync4(owner, "utf8")).token === token) rmSync4(lock, { recursive: true });
23352
+ if (acquired) backend.unlock(fd);
23365
23353
  } catch {
23354
+ warnLockCleanup(`Could not explicitly unlock ${lock}; closing its file handle.`);
23355
+ } finally {
23356
+ try {
23357
+ closeSync(fd);
23358
+ } catch {
23359
+ warnLockCleanup(`Could not close the project lock handle for ${lock}; restart this process before retrying writes.`);
23360
+ }
23366
23361
  }
23367
23362
  }
23368
23363
  }
23364
+ function warnLockCleanup(message) {
23365
+ try {
23366
+ process.stderr.write(`mellos-mapping: ${message}
23367
+ `);
23368
+ } catch {
23369
+ }
23370
+ }
23371
+
23372
+ // src/store/migration.ts
23373
+ var LEGACY_STATE_FILE_RELATIVE_PATH = join5(".claude", "mellos-mapping.json");
23374
+ var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
23375
+ var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
23376
+ function migrateLegacyStore(defaultFile) {
23377
+ const projectRoot = dirname6(dirname6(defaultFile));
23378
+ const legacyDefault = join5(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
23379
+ const legacyPages = join5(dirname6(legacyDefault), LEGACY_PAGES_DIR_NAME);
23380
+ const legacyConfig = join5(dirname6(legacyDefault), LEGACY_CONFIG_FILE_NAME);
23381
+ const hasLegacy = () => existsSync3(legacyDefault) || existsSync3(legacyPages) || existsSync3(legacyConfig);
23382
+ const hasCurrent = () => existsSync3(defaultFile) || existsSync3(join5(dirname6(defaultFile), PAGES_DIR_NAME)) || existsSync3(configFilePath(defaultFile));
23383
+ if (!hasLegacy() || hasCurrent()) return false;
23384
+ return withStoreLock(defaultFile, () => {
23385
+ if (!hasLegacy() || hasCurrent()) return false;
23386
+ if (existsSync3(legacyDefault)) renameSync2(legacyDefault, defaultFile);
23387
+ if (existsSync3(legacyPages)) renameSync2(legacyPages, join5(dirname6(defaultFile), PAGES_DIR_NAME));
23388
+ if (existsSync3(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
23389
+ return true;
23390
+ });
23391
+ }
23392
+
23393
+ // src/store/maps.ts
23394
+ import { readFileSync as readFileSync3 } from "node:fs";
23395
+ function loadMapFile(path) {
23396
+ let text2;
23397
+ try {
23398
+ text2 = readFileSync3(path, "utf8");
23399
+ } catch (e) {
23400
+ const code = e.code;
23401
+ if (code === "ENOENT") return err({ kind: "not-found", path });
23402
+ throw e;
23403
+ }
23404
+ let raw;
23405
+ try {
23406
+ raw = JSON.parse(stripBom(text2));
23407
+ } catch (e) {
23408
+ return err({ kind: "malformed-json", path, detail: e.message });
23409
+ }
23410
+ return parseMap(raw, path);
23411
+ }
23412
+ function saveMapFile(path, map) {
23413
+ const textError = mapTextError(map);
23414
+ if (textError) return err({ kind: "save-failed", path, detail: textError });
23415
+ return writeFileAtomic(path, serializeMap(map));
23416
+ }
23369
23417
 
23370
23418
  // src/store/project.ts
23371
- import { existsSync as existsSync3, realpathSync } from "node:fs";
23419
+ import { existsSync as existsSync4, realpathSync as realpathSync2 } from "node:fs";
23372
23420
  import { dirname as dirname7, join as join6, resolve } from "node:path";
23373
23421
  import { homedir, tmpdir } from "node:os";
23374
23422
  function resolveProjectDirectory(cwd, stopAt = [homedir(), tmpdir()]) {
23375
23423
  let start = resolve(cwd);
23376
23424
  try {
23377
- start = realpathSync(start);
23425
+ start = realpathSync2(start);
23378
23426
  } catch {
23379
23427
  }
23380
23428
  let dir = start;
23381
23429
  const boundaries = new Set(stopAt.map((path) => {
23382
23430
  try {
23383
- return realpathSync(path);
23431
+ return realpathSync2(path);
23384
23432
  } catch {
23385
23433
  return resolve(path);
23386
23434
  }
23387
23435
  }));
23388
23436
  while (true) {
23389
23437
  if (dir !== start && boundaries.has(dir)) return start;
23390
- if (existsSync3(join6(dir, ".git")) || existsSync3(join6(dir, ".mellos", "map.json")) || existsSync3(join6(dir, ".mellos", "pages"))) return dir;
23438
+ if (existsSync4(join6(dir, ".git")) || existsSync4(join6(dir, ".mellos", "map.json")) || existsSync4(join6(dir, ".mellos", "pages"))) return dir;
23391
23439
  const parent = dirname7(dir);
23392
23440
  if (parent === dir) return start;
23393
23441
  dir = parent;
@@ -23734,7 +23782,7 @@ function summarize(map) {
23734
23782
 
23735
23783
  // src/preview/publisher.ts
23736
23784
  import { createHash as createHash2 } from "node:crypto";
23737
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, realpathSync as realpathSync2, rmdirSync } from "node:fs";
23785
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync3, realpathSync as realpathSync3, rmdirSync } from "node:fs";
23738
23786
  import { dirname as dirname8, join as join7, resolve as resolve2 } from "node:path";
23739
23787
 
23740
23788
  // src/preview/presentation.ts
@@ -23901,7 +23949,7 @@ function previewFile(defaultFile, page2) {
23901
23949
  }
23902
23950
  function save(path, contents) {
23903
23951
  try {
23904
- if (readFileSync5(path, "utf8") === contents) return;
23952
+ if (readFileSync4(path, "utf8") === contents) return;
23905
23953
  } catch (error2) {
23906
23954
  if (error2.code !== "ENOENT") throw error2;
23907
23955
  }
@@ -23909,9 +23957,9 @@ function save(path, contents) {
23909
23957
  if (!result.ok) throw new Error(describeStoreError(result.error));
23910
23958
  }
23911
23959
  function ownedDirectory(path) {
23912
- mkdirSync4(path, { recursive: true });
23913
- const expected = join7(realpathSync2(dirname8(path)), path.slice(dirname8(path).length + 1));
23914
- const actual = realpathSync2(path);
23960
+ mkdirSync3(path, { recursive: true });
23961
+ const expected = join7(realpathSync3(dirname8(path)), path.slice(dirname8(path).length + 1));
23962
+ const actual = realpathSync3(path);
23915
23963
  if (process.platform === "win32" ? actual.toLowerCase() !== expected.toLowerCase() : actual !== expected) {
23916
23964
  throw new Error(`Preview directory redirects outside its parent: ${path}`);
23917
23965
  }
@@ -23921,7 +23969,7 @@ function acquireLock(directory) {
23921
23969
  const deadline = Date.now() + 2e3;
23922
23970
  while (true) {
23923
23971
  try {
23924
- mkdirSync4(path);
23972
+ mkdirSync3(path);
23925
23973
  return () => rmdirSync(path);
23926
23974
  } catch (error2) {
23927
23975
  if (error2.code !== "EEXIST") throw error2;
@@ -23933,7 +23981,7 @@ function acquireLock(directory) {
23933
23981
  function createPreviewPublisher(defaultFile) {
23934
23982
  const directory = previewDirectory(defaultFile);
23935
23983
  const enabledFile = join7(directory, ENABLED);
23936
- const enabled = () => existsSync4(enabledFile);
23984
+ const enabled = () => existsSync5(enabledFile);
23937
23985
  const refresh = (page2) => {
23938
23986
  try {
23939
23987
  const path = previewFile(defaultFile, page2);
@@ -23944,9 +23992,9 @@ function createPreviewPublisher(defaultFile) {
23944
23992
  for (const source of listPageFiles(defaultFile)) {
23945
23993
  const slug = pageIdOfFile(defaultFile, source);
23946
23994
  if (slug !== void 0 && !ID_RULE.test(slug)) throw new Error(`Invalid map page filename: ${source}`);
23947
- const loaded = loadMapFile(source);
23948
- if (!loaded.ok) throw new Error(describeStoreError(loaded.error));
23949
- pages.push({ page: slug, map: loaded.value });
23995
+ const loaded2 = loadMapFile(source);
23996
+ if (!loaded2.ok) throw new Error(describeStoreError(loaded2.error));
23997
+ pages.push({ page: slug, map: loaded2.value });
23950
23998
  }
23951
23999
  if (page2 !== void 0 && !pages.some((p) => p.page === page2)) return err(`No map page named "${page2}".`);
23952
24000
  if (page2 === void 0 && !pages.some((p) => p.page === void 0)) pages.unshift({ page: void 0, map: EMPTY_MAP });
@@ -23992,13 +24040,13 @@ function createPreviewPublisher(defaultFile) {
23992
24040
 
23993
24041
  // src/web/launcher.ts
23994
24042
  import { spawn } from "node:child_process";
23995
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
24043
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
23996
24044
  import { dirname as dirname10, join as join8 } from "node:path";
23997
24045
 
23998
24046
  // src/web/source.ts
23999
24047
  import { createHash as createHash3 } from "node:crypto";
24000
24048
  import { statSync as statSync2 } from "node:fs";
24001
- import { basename as basename2, dirname as dirname9 } from "node:path";
24049
+ import { basename as basename3, dirname as dirname9 } from "node:path";
24002
24050
  function readWebSnapshot(defaultFile) {
24003
24051
  const pages = listPageFiles(defaultFile).map((file) => {
24004
24052
  const id2 = pageIdOfFile(defaultFile, file) ?? "";
@@ -24013,41 +24061,46 @@ function readWebSnapshot(defaultFile) {
24013
24061
  }
24014
24062
  });
24015
24063
  if (pages.length === 0) pages.push({ id: "", title: "\u7B49\u5F85\u7B2C\u4E00\u5F20\u5730\u56FE", modified: 0, map: EMPTY_MAP });
24016
- const value = { project: basename2(dirname9(dirname9(defaultFile))), pages };
24064
+ const value = { project: basename3(dirname9(dirname9(defaultFile))), pages };
24017
24065
  return { revision: createHash3("sha256").update(JSON.stringify(value)).digest("hex"), value };
24018
24066
  }
24019
24067
 
24020
24068
  // src/web/launcher.ts
24021
24069
  var webRuntimeFile = (defaultFile) => join8(dirname10(defaultFile), "web", "server.json");
24022
- async function runningWebUrl(defaultFile) {
24070
+ async function runningWebRuntime(defaultFile) {
24023
24071
  try {
24024
- const info = JSON.parse(readFileSync6(webRuntimeFile(defaultFile), "utf8"));
24072
+ const info = JSON.parse(readFileSync5(webRuntimeFile(defaultFile), "utf8"));
24025
24073
  if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535 || !/^[a-f0-9]{48}$/.test(info.token)) return void 0;
24026
24074
  const url = `http://127.0.0.1:${info.port}/${info.token}/`;
24027
24075
  const response = await fetch(`${url}api/health`, { signal: AbortSignal.timeout(700) });
24028
- if (response.ok && (await response.json()).file === defaultFile) return url;
24076
+ if (response.ok) {
24077
+ const health = await response.json();
24078
+ if (health.file === defaultFile) return { url, health };
24079
+ }
24029
24080
  } catch {
24030
24081
  }
24031
24082
  return void 0;
24032
24083
  }
24084
+ async function runningWebUrl(defaultFile) {
24085
+ return (await runningWebRuntime(defaultFile))?.url;
24086
+ }
24033
24087
  async function openWebPreview(defaultFile, entry, page2, terminal = false) {
24034
24088
  if (page2 !== void 0 && (!ID_RULE.test(page2) || !readWebSnapshot(defaultFile).value.pages.some((p) => p.id === page2))) throw new Error(`No map page named "${page2}".`);
24035
- let url = await runningWebUrl(defaultFile);
24036
- if (url) {
24037
- const health = await fetch(`${url}api/health`, { signal: AbortSignal.timeout(2e3) });
24038
- const info = await health.json();
24039
- if (!info.formats?.includes(2) || terminal && !info.surfaces?.includes("web-terminal")) {
24040
- await fetch(`${url}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
24089
+ let runtime = await runningWebRuntime(defaultFile);
24090
+ if (runtime) {
24091
+ const { url: url2, health } = runtime;
24092
+ if (health.lockProtocol === STORE_LOCK_PROTOCOL && (!health.formats?.includes(2) || terminal && !health.surfaces?.includes("web-terminal"))) {
24093
+ await fetch(`${url2}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
24041
24094
  const deadline = Date.now() + 3e3;
24042
24095
  while (await runningWebUrl(defaultFile)) {
24043
24096
  if (Date.now() > deadline) throw new Error("Old web viewer is still stopping. Retry opening the map.");
24044
24097
  await new Promise((resolve4) => setTimeout(resolve4, 100));
24045
24098
  }
24046
- url = void 0;
24099
+ runtime = void 0;
24047
24100
  }
24048
24101
  }
24049
- if (!url) {
24050
- if (!existsSync5(entry)) throw new Error(`Web runtime missing: ${entry}. Run npm run build or reinstall the plugin.`);
24102
+ if (!runtime) {
24103
+ if (!existsSync6(entry)) throw new Error(`Web runtime missing: ${entry}. Run npm run build or reinstall the plugin.`);
24051
24104
  const child = spawn(process.execPath, [entry, "--serve", defaultFile], { detached: true, windowsHide: true, stdio: "ignore" });
24052
24105
  let failure;
24053
24106
  child.on("error", (error2) => {
@@ -24055,13 +24108,20 @@ async function openWebPreview(defaultFile, entry, page2, terminal = false) {
24055
24108
  });
24056
24109
  child.unref();
24057
24110
  const deadline = Date.now() + 8e3;
24058
- while (!url && Date.now() < deadline) {
24111
+ while (!runtime && Date.now() < deadline) {
24059
24112
  if (failure) throw failure;
24060
24113
  await new Promise((resolve4) => setTimeout(resolve4, 100));
24061
- url = await runningWebUrl(defaultFile);
24114
+ runtime = await runningWebRuntime(defaultFile);
24062
24115
  }
24063
- if (!url) throw new Error("Web preview did not start. Run the web CLI directly to inspect the error.");
24116
+ if (!runtime) throw new Error("Web preview did not start. Run the web CLI directly to inspect the error.");
24117
+ }
24118
+ if (runtime.health.lockProtocol !== STORE_LOCK_PROTOCOL) {
24119
+ throw new LedgerError(
24120
+ "LOCK_MIGRATION_REQUIRED",
24121
+ "The running web viewer uses an older storage-lock protocol. Stop the viewer with mellos-mapping-web <project-directory> --stop, then reopen it using the updated runtime."
24122
+ );
24064
24123
  }
24124
+ const { url } = runtime;
24065
24125
  const query = new URLSearchParams();
24066
24126
  if (terminal) query.set("view", "terminal");
24067
24127
  if (page2 !== void 0) query.set("page", page2);
@@ -24323,7 +24383,7 @@ function viewTool() {
24323
24383
  function openTool() {
24324
24384
  return {
24325
24385
  title: "Open the map pane",
24326
- description: 'For automatic display beside the current ChatGPT desktop conversation in Codex mode, use surface: "web-terminal", then call open_in_codex with the returned browser hostOpen object. No paste or Computer Use is needed. For the native host terminal, use surface: "codex-terminal": prepare absolute watcher commands for the current project, then ask the host to open its right terminal. This does not launch the watcher or type into that terminal. Agent exec PTYs cannot be attached using their numeric session ids. For a document panel, use surface: "markdown": generate MD + SVG files, enable automatic preview updates after successful map writes, then use the HOST file-opening tool to display the returned absolute Markdown path on the right of the current conversation. Generated does not mean visible: this server cannot open or observe the desktop side panel. For interactive maps, choose surface: "web": start or reuse a project-local web viewer and pass the returned URL to the host browser-opening tool. Markdown and terminal remain available. The default surface is "terminal", preserving the terminal workflow. Put the live map on the user\'s screen: a terminal pane beside this conversation that redraws on every write. Call it whenever a result says `pane: CLOSED` \u2014 and do NOT ask permission first, because a user who has set a mapping policy has already said they want to see the map. With a pane already open this RETARGETS it to `page` instead of opening a second one, so it is also how you show the user a particular page when they ask for one. It never closes a pane: taking the map off the screen belongs to the user (the `q` key in the pane, or typing `mmap` in a terminal). The reply says whether a pane actually reported itself in afterwards, not merely that a command was run. Automatic terminal opening supports Windows Terminal and tmux on Linux/macOS. If opening fails, relay the reason and copyable command; retry only after the environment changes or the user asks.',
24386
+ description: 'For automatic display beside the current ChatGPT desktop conversation in Codex mode, use surface: "web-terminal", then call open_in_codex with the returned browser hostOpen object. No paste or Computer Use is needed. For the native host terminal, use surface: "codex-terminal": prepare absolute watcher commands for the current project, then ask the host to open its right terminal. This does not launch the watcher or type into that terminal. Agent exec PTYs cannot be attached using their numeric session ids. For a document panel, use surface: "markdown": generate MD + SVG files, enable automatic preview updates after successful map writes, then use the HOST file-opening tool to display the returned absolute Markdown path on the right of the current conversation. Generated does not mean visible: this server cannot open or observe the desktop side panel. For interactive maps, choose surface: "web": start or reuse a project-local web viewer and pass the returned URL to the host browser-opening tool. Markdown and terminal remain available. The default surface is "terminal", preserving the terminal workflow. Put the live map on the user\'s screen: a terminal pane beside this conversation that redraws on every write. Call it whenever a result says `pane: CLOSED` \u2014 and do NOT ask permission first, because a user who has set a mapping policy has already said they want to see the map. With a pane already open this RETARGETS it to `page` instead of opening a second one, so it is also how you show the user a particular page when they ask for one. It never closes a pane: taking the map off the screen belongs to the user (the `q` key in the pane, or typing `mmap` in a terminal). The reply says whether a pane actually reported itself in afterwards, not merely that a command was run. Automatic terminal opening supports Windows Terminal and tmux on Linux/macOS. omp (Oh My Pi) has no side panel of its own: there the routes are this same terminal pane (a Windows Terminal split, or a tmux split on Linux/macOS) and the web viewer, whose URL the session opens with its own browser tool. If opening fails, relay the reason and copyable command; retry only after the environment changes or the user asks.',
24327
24387
  inputSchema: closed({
24328
24388
  surface: external_exports.enum(["terminal", "codex-terminal", "markdown", "web", "web-terminal"]).optional().describe("web-terminal = automatically started mmap terminal in a local browser page; codex-terminal = prepare a command for the desktop host terminal; web = local browser viewer; markdown = MD/SVG; terminal = Windows Terminal or tmux launcher (default)"),
24329
24389
  page: id(
@@ -24338,7 +24398,7 @@ function openTool() {
24338
24398
 
24339
24399
  // src/server/read.ts
24340
24400
  import { createHash as createHash4 } from "node:crypto";
24341
- import { readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
24401
+ import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync as statSync3 } from "node:fs";
24342
24402
  import { dirname as dirname11, isAbsolute, relative, resolve as resolve3 } from "node:path";
24343
24403
  var hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
24344
24404
  function requireMap(file) {
@@ -24374,12 +24434,12 @@ function related(map, seeds, direction, depth) {
24374
24434
  function changes(node, project, map) {
24375
24435
  const sources2 = (node.sources ?? []).map((source) => {
24376
24436
  try {
24377
- const root = realpathSync3(project), target = realpathSync3(resolve3(root, source.path));
24437
+ const root = realpathSync4(project), target = realpathSync4(resolve3(root, source.path));
24378
24438
  const rel = relative(root, target);
24379
24439
  if (isAbsolute(rel) || rel === ".." || rel.startsWith("..\\") || rel.startsWith("../")) return { ...source, state: "outside-project" };
24380
24440
  const stat = statSync3(target);
24381
24441
  if (!stat.isFile() || stat.size > 8 * 1024 * 1024) return { ...source, state: "unreadable" };
24382
- const currentSha256 = createHash4("sha256").update(readFileSync7(target)).digest("hex");
24442
+ const currentSha256 = createHash4("sha256").update(readFileSync6(target)).digest("hex");
24383
24443
  return { ...source, currentSha256, state: source.sha256 === void 0 ? "unverified" : source.sha256 === currentSha256 ? "unchanged" : "changed" };
24384
24444
  } catch (e) {
24385
24445
  return { ...source, state: e.code === "ENOENT" ? "missing" : "unreadable" };
@@ -24484,16 +24544,16 @@ function paneLine(stateFile, touched, openFailure) {
24484
24544
  }
24485
24545
 
24486
24546
  // src/server/map-service.ts
24487
- import { existsSync as existsSync6 } from "node:fs";
24547
+ import { existsSync as existsSync7 } from "node:fs";
24488
24548
  function loadOrEmpty(file) {
24489
- const loaded = loadMapFile(file);
24490
- if (loaded.ok) return loaded;
24491
- return loaded.error.kind === "not-found" ? { ok: true, value: EMPTY_MAP } : { ok: false, error: describeStoreError(loaded.error) };
24549
+ const loaded2 = loadMapFile(file);
24550
+ if (loaded2.ok) return loaded2;
24551
+ return loaded2.error.kind === "not-found" ? { ok: true, value: EMPTY_MAP } : { ok: false, error: describeStoreError(loaded2.error) };
24492
24552
  }
24493
24553
  function mutateMap(file, apply, expectedRevision2) {
24494
24554
  const current = loadOrEmpty(file);
24495
24555
  if (!current.ok) return { ok: false, error: { kind: "load", detail: current.error } };
24496
- assertRevision(existsSync6(file) ? revisionOf(current.value) : "absent", expectedRevision2);
24556
+ assertRevision(existsSync7(file) ? revisionOf(current.value) : "absent", expectedRevision2);
24497
24557
  const applied = apply(current.value);
24498
24558
  if (!applied.ok) return { ok: false, error: { kind: "refused", detail: applied.error } };
24499
24559
  const saved = saveMapFile(file, applied.value);
@@ -24502,15 +24562,15 @@ function mutateMap(file, apply, expectedRevision2) {
24502
24562
 
24503
24563
  // src/server/pane-launcher.ts
24504
24564
  import { spawn as spawn2 } from "node:child_process";
24505
- import { existsSync as existsSync7 } from "node:fs";
24565
+ import { existsSync as existsSync8 } from "node:fs";
24506
24566
  import { dirname as dirname12, join as join9 } from "node:path";
24507
- import { fileURLToPath } from "node:url";
24567
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
24508
24568
  var pageName2 = (page2) => page2 ?? "(default)";
24509
24569
  var LAUNCH_TIMEOUT_MS = 6e4;
24510
24570
  var PANE_REPORT_TIMEOUT_MS = 8e3;
24511
24571
  var PANE_REPORT_POLL_MS = 250;
24512
24572
  function launcherPath(moduleUrl) {
24513
- return join9(dirname12(dirname12(fileURLToPath(moduleUrl))), "scripts", "open-pane.mjs");
24573
+ return join9(dirname12(dirname12(fileURLToPath2(moduleUrl))), "scripts", "open-pane.mjs");
24514
24574
  }
24515
24575
  function projectDirOf(stateFile) {
24516
24576
  return dirname12(dirname12(stateFile));
@@ -24546,7 +24606,7 @@ function runLauncher(script, args) {
24546
24606
  }
24547
24607
  function launchPane(args) {
24548
24608
  const script = launcherPath(import.meta.url);
24549
- if (!existsSync7(script)) return Promise.resolve({
24609
+ if (!existsSync8(script)) return Promise.resolve({
24550
24610
  ok: false,
24551
24611
  output: `the launcher is missing at ${script}. This install is incomplete \u2014 reinstall the plugin (a source checkout needs "npm run build").`
24552
24612
  });
@@ -24590,7 +24650,7 @@ function launcherViewerPid(run) {
24590
24650
 
24591
24651
  // src/server/server.ts
24592
24652
  var SERVER_NAME = "mellos-mapping";
24593
- var SERVER_VERSION = "0.24.0";
24653
+ var SERVER_VERSION = "0.26.0";
24594
24654
  function text(s, isError = false) {
24595
24655
  return { content: [{ type: "text", text: s }], ...isError ? { isError: true } : {} };
24596
24656
  }
@@ -24637,7 +24697,7 @@ revision: ${revision}` + refreshPreview(page2)), structuredContent: { page: page
24637
24697
  };
24638
24698
  const mutate = (page2, apply, expectedRevision2) => guard(() => withStoreLock(stateFile, () => mutateUnlocked(page2, apply, expectedRevision2)));
24639
24699
  const withPane = (result, page2) => result.isError === true || previews.enabled() ? result : { ...result, ...text(`${result.content[0]?.text ?? ""}
24640
- ${existsSync8(webRuntimeFile(stateFile)) ? 'web: configured \u2014 the browser reads project map updates. Use mmap_open {surface: "web", page} to open or reconnect; desktop visibility is not tracked.' : currentPaneLine(page2)}`) };
24700
+ ${existsSync9(webRuntimeFile(stateFile)) ? 'web: configured \u2014 the browser reads project map updates. Use mmap_open {surface: "web", page} to open or reconnect; desktop visibility is not tracked.' : currentPaneLine(page2)}`) };
24641
24701
  const knownPages = () => listPageFiles(stateFile).map((f) => pageIdOfFile(stateFile, f)).filter((p) => p !== void 0);
24642
24702
  const refusePageDeletion = (pages, target) => {
24643
24703
  if (target !== void 0 && pages.includes(target)) {
@@ -24698,7 +24758,7 @@ note: ${describeStoreError(scopes.error)} \u2014 fix it or rerun setup (mmap_set
24698
24758
  );
24699
24759
  server.registerTool("mmap_read", readTool(), (input) => guard(() => structured(readMaps(stateFile, input))));
24700
24760
  server.registerTool("mmap_batch", batchTool(), (input) => withPane(mutate(input.page, (map) => {
24701
- if (!existsSync8(fileOf(input.page)) && !input.operations.some((op) => op.op === "declare")) throw new LedgerError("NOT_FOUND", "Create the page before updating it.");
24761
+ if (!existsSync9(fileOf(input.page)) && !input.operations.some((op) => op.op === "declare")) throw new LedgerError("NOT_FOUND", "Create the page before updating it.");
24702
24762
  return applyBatch(map, input.operations, input.page);
24703
24763
  }, input.expectedRevision), input.page));
24704
24764
  server.registerTool(
@@ -24770,7 +24830,7 @@ note: ${describeStoreError(scopes.error)} \u2014 fix it or rerun setup (mmap_set
24770
24830
  const zoom = clampZoom(input.zoom ?? 0);
24771
24831
  const picture = renderMap(current.value, { color: false, unicode: true, spinnerFrame: 0, zoom }).join("\n");
24772
24832
  const surface = previews.enabled() ? `markdown: ${previewFile(stateFile, input.page)}
24773
- Use mmap_open {surface: "markdown", page} to regenerate. Desktop visibility is not tracked.` : existsSync8(webRuntimeFile(stateFile)) ? 'web: configured \u2014 use mmap_open {surface: "web", page} to open or reconnect. Desktop visibility is not tracked.' : currentPaneLine(input.page);
24833
+ Use mmap_open {surface: "markdown", page} to regenerate. Desktop visibility is not tracked.` : existsSync9(webRuntimeFile(stateFile)) ? 'web: configured \u2014 use mmap_open {surface: "web", page} to open or reconnect. Desktop visibility is not tracked.' : currentPaneLine(input.page);
24774
24834
  return text(`${picture}
24775
24835
  ${pagesLine(stateFile, input.page)}
24776
24836
  ${surface}`);
@@ -24785,13 +24845,13 @@ ${surface}`);
24785
24845
  if (input.page && !listPageFiles(stateFile).some((file) => pageIdOfFile(stateFile, file) === input.page)) {
24786
24846
  return text(`Unknown page: ${input.page}. ${pagesLine(stateFile, input.page)}`, true);
24787
24847
  }
24788
- const handoff = terminalHandoff(process.execPath, fileURLToPath2(new URL("./watch.mjs", import.meta.url)), stateFile, input.page);
24848
+ const handoff = terminalHandoff(process.execPath, fileURLToPath3(new URL("./watch.mjs", import.meta.url)), stateFile, input.page);
24789
24849
  return text("terminal: ready-to-start\n" + JSON.stringify(handoff, null, 2) + "\nUse open_in_codex with hostOpen in the CURRENT conversation, without a threadId. If a supported host tool can run commands in that user terminal, use it. Otherwise give the user the command for their shell to paste once. An exec_command session_id belongs to the agent PTY, not this terminal. queued is not visible, and opened is not running. Use read_thread_terminal to confirm the map title and controls after startup. Preserve a page the user pinned.");
24790
24850
  }
24791
24851
  if (input.surface === "web" || input.surface === "web-terminal") {
24792
24852
  if (input.window === true) return text('surface: "web" cannot be combined with window: true. Open the returned URL using the desktop host.', true);
24793
24853
  try {
24794
- const url = await openWebPreview(stateFile, fileURLToPath2(new URL("./web.mjs", import.meta.url)), input.page, input.surface === "web-terminal");
24854
+ const url = await openWebPreview(stateFile, fileURLToPath3(new URL("./web.mjs", import.meta.url)), input.page, input.surface === "web-terminal");
24795
24855
  return text(`surface: ${input.surface}
24796
24856
  hostOpen: ${JSON.stringify({ placement: "right", target: { type: "browser", url } })}
24797
24857
  preview: ready
@@ -24837,7 +24897,7 @@ async function main() {
24837
24897
  function launchedAsEntry(argv1, moduleUrl) {
24838
24898
  if (argv1 === void 0) return false;
24839
24899
  try {
24840
- return realpathSync4(argv1) === realpathSync4(fileURLToPath2(moduleUrl));
24900
+ return realpathSync5(argv1) === realpathSync5(fileURLToPath3(moduleUrl));
24841
24901
  } catch {
24842
24902
  return pathToFileURL(argv1).href === moduleUrl;
24843
24903
  }