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/README.md +82 -12
- package/README.zh-CN.md +69 -10
- package/dist/hook-session-start.mjs +69 -13
- package/dist/native/LICENSES.txt +443 -0
- package/dist/native/darwin-arm64/fs-native-extensions.node +0 -0
- package/dist/native/darwin-x64/fs-native-extensions.node +0 -0
- package/dist/native/linux-arm64/fs-native-extensions.node +0 -0
- package/dist/native/linux-x64/fs-native-extensions.node +0 -0
- package/dist/native/win32-arm64/fs-native-extensions.node +0 -0
- package/dist/native/win32-x64/fs-native-extensions.node +0 -0
- package/dist/native-lock.cjs +346 -0
- package/dist/omp-extension.mjs +359 -0
- package/dist/server.mjs +199 -139
- package/dist/terminal-worker.mjs +116 -74
- package/dist/watch.mjs +125 -83
- package/dist/web.mjs +170 -113
- package/docs/codex.md +4 -1
- package/docs/locking.md +115 -0
- package/docs/map-api.md +29 -13
- package/lib/store/migration.js +19 -12
- package/lib/store/native-lock.d.ts +5 -0
- package/lib/store/native-lock.js +27 -0
- package/lib/store/transaction.d.ts +9 -2
- package/lib/store/transaction.js +79 -50
- package/package.json +11 -3
- package/scripts/install-mmap-command.mjs +179 -13
package/dist/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from 'node:module'; const require =
|
|
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
|
|
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
|
|
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
|
|
23253
|
-
import { dirname as
|
|
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/
|
|
23273
|
-
import {
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
|
|
23278
|
-
|
|
23279
|
-
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
}
|
|
23287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
23314
|
-
|
|
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
|
-
|
|
23319
|
-
|
|
23320
|
-
|
|
23321
|
-
|
|
23322
|
-
|
|
23323
|
-
|
|
23324
|
-
|
|
23325
|
-
|
|
23326
|
-
|
|
23327
|
-
|
|
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
|
-
|
|
23333
|
-
const lock =
|
|
23334
|
-
|
|
23335
|
-
|
|
23329
|
+
mkdirSync2(dir, { recursive: true });
|
|
23330
|
+
const lock = join4(dir, ".write-lock");
|
|
23331
|
+
checkLockPath(lock);
|
|
23332
|
+
let fd;
|
|
23336
23333
|
try {
|
|
23337
|
-
|
|
23334
|
+
fd = openSync(lock, "a+", 384);
|
|
23338
23335
|
} catch (error2) {
|
|
23339
|
-
|
|
23340
|
-
|
|
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
|
|
23339
|
+
let acquired = false;
|
|
23358
23340
|
try {
|
|
23359
|
-
|
|
23360
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
-
|
|
23913
|
-
const expected = join7(
|
|
23914
|
-
const actual =
|
|
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
|
-
|
|
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 = () =>
|
|
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
|
|
23948
|
-
if (!
|
|
23949
|
-
pages.push({ page: slug, map:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
24070
|
+
async function runningWebRuntime(defaultFile) {
|
|
24023
24071
|
try {
|
|
24024
|
-
const info = JSON.parse(
|
|
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
|
|
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
|
|
24036
|
-
if (
|
|
24037
|
-
const
|
|
24038
|
-
|
|
24039
|
-
|
|
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
|
-
|
|
24099
|
+
runtime = void 0;
|
|
24047
24100
|
}
|
|
24048
24101
|
}
|
|
24049
|
-
if (!
|
|
24050
|
-
if (!
|
|
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 (!
|
|
24111
|
+
while (!runtime && Date.now() < deadline) {
|
|
24059
24112
|
if (failure) throw failure;
|
|
24060
24113
|
await new Promise((resolve4) => setTimeout(resolve4, 100));
|
|
24061
|
-
|
|
24114
|
+
runtime = await runningWebRuntime(defaultFile);
|
|
24062
24115
|
}
|
|
24063
|
-
if (!
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
24547
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
24488
24548
|
function loadOrEmpty(file) {
|
|
24489
|
-
const
|
|
24490
|
-
if (
|
|
24491
|
-
return
|
|
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(
|
|
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
|
|
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(
|
|
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 (!
|
|
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.
|
|
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
|
-
${
|
|
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 (!
|
|
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.` :
|
|
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,
|
|
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,
|
|
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
|
|
24900
|
+
return realpathSync5(argv1) === realpathSync5(fileURLToPath3(moduleUrl));
|
|
24841
24901
|
} catch {
|
|
24842
24902
|
return pathToFileURL(argv1).href === moduleUrl;
|
|
24843
24903
|
}
|