mellos-mapping 0.22.1 → 0.24.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 +31 -4
- package/README.zh-CN.md +26 -4
- package/dist/hook-session-start.mjs +3 -1
- package/dist/mmap.mjs +1 -1
- package/dist/preview.mjs +41 -2
- package/dist/server.mjs +857 -412
- package/dist/store-paths.mjs +29 -1
- package/dist/terminal-worker.mjs +444 -300
- package/dist/watch.mjs +563 -339
- package/dist/web/app.css +106 -2
- package/dist/web/app.js +78 -0
- package/dist/web/index.html +1 -1
- package/dist/web/terminal.css +103 -0
- package/dist/web/terminal.html +1 -1
- package/dist/web/terminal.js +78 -0
- package/dist/web.mjs +314 -67
- package/docs/codex.md +7 -1
- package/docs/map-api.md +148 -0
- package/lib/domain/context.d.ts +11 -0
- package/lib/domain/context.js +27 -0
- package/lib/domain/text.js +11 -0
- package/lib/domain/types.d.ts +3 -0
- package/lib/store/format.js +18 -3
- package/lib/store/project.d.ts +2 -0
- package/lib/store/project.js +29 -0
- package/lib/store/store.d.ts +6 -1
- package/lib/store/store.js +6 -1
- package/lib/store/transaction.d.ts +12 -0
- package/lib/store/transaction.js +91 -0
- package/package.json +4 -2
- package/scripts/codex-register.mjs +1 -1
- package/scripts/mmap.mjs +1 -1
package/dist/web.mjs
CHANGED
|
@@ -2271,7 +2271,7 @@ var require_websocket = __commonJS({
|
|
|
2271
2271
|
var http = __require("http");
|
|
2272
2272
|
var net = __require("net");
|
|
2273
2273
|
var tls = __require("tls");
|
|
2274
|
-
var { randomBytes: randomBytes2, createHash:
|
|
2274
|
+
var { randomBytes: randomBytes2, createHash: createHash4 } = __require("crypto");
|
|
2275
2275
|
var { Duplex, Readable } = __require("stream");
|
|
2276
2276
|
var { URL: URL2 } = __require("url");
|
|
2277
2277
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2939,7 +2939,7 @@ var require_websocket = __commonJS({
|
|
|
2939
2939
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2940
2940
|
return;
|
|
2941
2941
|
}
|
|
2942
|
-
const digest =
|
|
2942
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
2943
2943
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2944
2944
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2945
2945
|
return;
|
|
@@ -3308,7 +3308,7 @@ var require_websocket_server = __commonJS({
|
|
|
3308
3308
|
var EventEmitter = __require("events");
|
|
3309
3309
|
var http = __require("http");
|
|
3310
3310
|
var { Duplex } = __require("stream");
|
|
3311
|
-
var { createHash:
|
|
3311
|
+
var { createHash: createHash4 } = __require("crypto");
|
|
3312
3312
|
var extension2 = require_extension();
|
|
3313
3313
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3314
3314
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3615,7 +3615,7 @@ var require_websocket_server = __commonJS({
|
|
|
3615
3615
|
);
|
|
3616
3616
|
}
|
|
3617
3617
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3618
|
-
const digest =
|
|
3618
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
3619
3619
|
const headers = [
|
|
3620
3620
|
"HTTP/1.1 101 Switching Protocols",
|
|
3621
3621
|
"Upgrade: websocket",
|
|
@@ -3703,9 +3703,10 @@ var require_websocket_server = __commonJS({
|
|
|
3703
3703
|
});
|
|
3704
3704
|
|
|
3705
3705
|
// src/web/cli.ts
|
|
3706
|
-
import { existsSync as
|
|
3707
|
-
import { dirname as
|
|
3706
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync3, rmSync as rmSync4, statSync as statSync2 } from "node:fs";
|
|
3707
|
+
import { dirname as dirname9, join as join7, resolve as resolve2 } from "node:path";
|
|
3708
3708
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3709
|
+
import { homedir } from "node:os";
|
|
3709
3710
|
|
|
3710
3711
|
// src/domain/types.ts
|
|
3711
3712
|
var ok = (value) => ({ ok: true, value });
|
|
@@ -3924,12 +3925,37 @@ function updateNode(map, input) {
|
|
|
3924
3925
|
return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
|
|
3925
3926
|
}
|
|
3926
3927
|
|
|
3928
|
+
// src/domain/context.ts
|
|
3929
|
+
function sourceError(raw) {
|
|
3930
|
+
if (!Array.isArray(raw) || raw.length > 100) return "sources must be an array of at most 100 file references";
|
|
3931
|
+
for (const item of raw) {
|
|
3932
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return "source must be an object";
|
|
3933
|
+
const s = item;
|
|
3934
|
+
if (Object.keys(s).some((k) => k !== "path" && k !== "sha256")) return "unknown source field";
|
|
3935
|
+
if (typeof s.path !== "string" || s.path.length > 1024 || !s.path || /[\u0000-\u001f\u007f-\u009f\\:]/.test(s.path) || s.path.startsWith("/") || s.path.split("/").some((p) => !p || p === "." || p === "..")) return "source path must be relative to the project, with forward slashes and no traversal";
|
|
3936
|
+
if (s.sha256 !== void 0 && (typeof s.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(s.sha256))) return "source sha256 must be a lowercase SHA256 hash";
|
|
3937
|
+
}
|
|
3938
|
+
return void 0;
|
|
3939
|
+
}
|
|
3940
|
+
function contextError(raw) {
|
|
3941
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "context must be an object";
|
|
3942
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
3943
|
+
if (key !== "summary" && key !== "next") return "unknown context field";
|
|
3944
|
+
if (typeof value !== "string" || value.length > 2e3 || /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/.test(value)) return "context fields must be text of at most 2000 characters";
|
|
3945
|
+
}
|
|
3946
|
+
return void 0;
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3927
3949
|
// src/domain/text.ts
|
|
3928
3950
|
var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
|
|
3929
3951
|
var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
|
|
3930
3952
|
var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
|
|
3931
3953
|
var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
|
|
3932
3954
|
function mapTextError(map) {
|
|
3955
|
+
if (map.context !== void 0) {
|
|
3956
|
+
const error2 = contextError(map.context);
|
|
3957
|
+
if (error2) return error2;
|
|
3958
|
+
}
|
|
3933
3959
|
const check = (field, value, multiline = false) => value === void 0 || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value) ? void 0 : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
|
|
3934
3960
|
let error = check("title", map.title);
|
|
3935
3961
|
if (error) return error;
|
|
@@ -3944,6 +3970,10 @@ function mapTextError(map) {
|
|
|
3944
3970
|
}
|
|
3945
3971
|
}
|
|
3946
3972
|
for (const [i, node] of map.nodes.entries()) {
|
|
3973
|
+
if (node.sources !== void 0) {
|
|
3974
|
+
const error2 = sourceError(node.sources);
|
|
3975
|
+
if (error2) return `nodes[${i}]: ${error2}`;
|
|
3976
|
+
}
|
|
3947
3977
|
for (const name of ["label", "evidence", "detail"]) {
|
|
3948
3978
|
error = check(`nodes[${i}].${name}`, node[name], name !== "label");
|
|
3949
3979
|
if (error) return error;
|
|
@@ -4006,8 +4036,8 @@ function optionalString(rec, key, where, path) {
|
|
|
4006
4036
|
}
|
|
4007
4037
|
function parseMap(raw, path) {
|
|
4008
4038
|
if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
|
|
4009
|
-
if (raw["version"] !== STATE_FILE_VERSION) {
|
|
4010
|
-
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
|
|
4039
|
+
if (raw["version"] !== STATE_FILE_VERSION && raw["version"] !== 2) {
|
|
4040
|
+
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION} or 2` });
|
|
4011
4041
|
}
|
|
4012
4042
|
const layers = arrayField(raw, "layers", path, "required");
|
|
4013
4043
|
if (!layers.ok) return layers;
|
|
@@ -4020,6 +4050,11 @@ function parseMap(raw, path) {
|
|
|
4020
4050
|
const groups = arrayField(raw, "groups", path, "optional");
|
|
4021
4051
|
if (!groups.ok) return groups;
|
|
4022
4052
|
let map = EMPTY_MAP;
|
|
4053
|
+
if (raw["context"] !== void 0) {
|
|
4054
|
+
const error = contextError(raw["context"]);
|
|
4055
|
+
if (error) return err({ kind: "bad-shape", path, detail: error });
|
|
4056
|
+
map = { ...map, context: raw["context"] };
|
|
4057
|
+
}
|
|
4023
4058
|
const title = optionalString(raw, "title", "map", path);
|
|
4024
4059
|
if (!title.ok) return title;
|
|
4025
4060
|
if (title.value !== void 0) map = setTitle(map, title.value);
|
|
@@ -4148,6 +4183,11 @@ function parseMap(raw, path) {
|
|
|
4148
4183
|
if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
|
|
4149
4184
|
map = updated.value;
|
|
4150
4185
|
}
|
|
4186
|
+
if (rawNode["sources"] !== void 0) {
|
|
4187
|
+
const error = sourceError(rawNode["sources"]);
|
|
4188
|
+
if (error) return err({ kind: "bad-shape", path, detail: `${where}: ${error}` });
|
|
4189
|
+
map = { ...map, nodes: map.nodes.map((n) => n.id === id.value ? { ...n, sources: rawNode["sources"] } : n) };
|
|
4190
|
+
}
|
|
4151
4191
|
}
|
|
4152
4192
|
for (const [i, rawEdge] of edges.value.entries()) {
|
|
4153
4193
|
const where = `edges[${i}]`;
|
|
@@ -4282,15 +4322,84 @@ function loadMapFile(path) {
|
|
|
4282
4322
|
return parseMap(raw, path);
|
|
4283
4323
|
}
|
|
4284
4324
|
|
|
4325
|
+
// src/store/transaction.ts
|
|
4326
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4327
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
4328
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
4329
|
+
var LedgerError = class extends Error {
|
|
4330
|
+
constructor(code, message, details = {}) {
|
|
4331
|
+
super(message);
|
|
4332
|
+
this.code = code;
|
|
4333
|
+
this.details = details;
|
|
4334
|
+
}
|
|
4335
|
+
};
|
|
4336
|
+
function storeDirectory(file) {
|
|
4337
|
+
const dir = dirname4(file);
|
|
4338
|
+
return dir.endsWith("/pages") || dir.endsWith("\\pages") ? dirname4(dir) : dir;
|
|
4339
|
+
}
|
|
4340
|
+
function deadOwner(lock) {
|
|
4341
|
+
try {
|
|
4342
|
+
const owner = JSON.parse(readFileSync2(join3(lock, "owner.json"), "utf8"));
|
|
4343
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
|
|
4344
|
+
try {
|
|
4345
|
+
process.kill(owner.pid, 0);
|
|
4346
|
+
return false;
|
|
4347
|
+
} catch (e) {
|
|
4348
|
+
return e.code === "ESRCH";
|
|
4349
|
+
}
|
|
4350
|
+
} catch {
|
|
4351
|
+
return false;
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
function withStoreLock(file, action) {
|
|
4355
|
+
const dir = storeDirectory(file);
|
|
4356
|
+
mkdirSync2(dir, { recursive: true });
|
|
4357
|
+
const lock = join3(dir, ".write-lock");
|
|
4358
|
+
const owner = join3(lock, "owner.json");
|
|
4359
|
+
const token = randomUUID();
|
|
4360
|
+
try {
|
|
4361
|
+
mkdirSync2(lock);
|
|
4362
|
+
} catch (error) {
|
|
4363
|
+
if (error.code !== "EEXIST") throw error;
|
|
4364
|
+
if (!deadOwner(lock)) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes. An orphan without owner metadata needs manual inspection.`);
|
|
4365
|
+
try {
|
|
4366
|
+
mkdirSync2(join3(lock, ".reap"));
|
|
4367
|
+
} catch {
|
|
4368
|
+
throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
|
|
4369
|
+
}
|
|
4370
|
+
if (!deadOwner(lock)) {
|
|
4371
|
+
rmSync3(join3(lock, ".reap"), { recursive: true, force: true });
|
|
4372
|
+
throw new LedgerError("BUSY", "Writer ownership changed.");
|
|
4373
|
+
}
|
|
4374
|
+
rmSync3(lock, { recursive: true });
|
|
4375
|
+
try {
|
|
4376
|
+
mkdirSync2(lock);
|
|
4377
|
+
} catch {
|
|
4378
|
+
throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
let initialized = false;
|
|
4382
|
+
try {
|
|
4383
|
+
writeFileSync2(owner, JSON.stringify({ pid: process.pid, token }), { flag: "wx" });
|
|
4384
|
+
initialized = true;
|
|
4385
|
+
return action();
|
|
4386
|
+
} finally {
|
|
4387
|
+
try {
|
|
4388
|
+
if (!initialized || JSON.parse(readFileSync2(owner, "utf8")).token === token) rmSync3(lock, { recursive: true });
|
|
4389
|
+
} catch {
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4285
4394
|
// src/web/launcher.ts
|
|
4286
4395
|
import { spawn } from "node:child_process";
|
|
4287
|
-
import { existsSync as existsSync2, readFileSync as
|
|
4288
|
-
import { dirname as
|
|
4396
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
4397
|
+
import { dirname as dirname6, join as join4 } from "node:path";
|
|
4289
4398
|
|
|
4290
4399
|
// src/web/source.ts
|
|
4291
|
-
import { createHash } from "node:crypto";
|
|
4400
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4292
4401
|
import { statSync } from "node:fs";
|
|
4293
|
-
import { basename as basename2, dirname as
|
|
4402
|
+
import { basename as basename2, dirname as dirname5 } from "node:path";
|
|
4294
4403
|
function readWebSnapshot(defaultFile) {
|
|
4295
4404
|
const pages = listPageFiles(defaultFile).map((file) => {
|
|
4296
4405
|
const id = pageIdOfFile(defaultFile, file) ?? "";
|
|
@@ -4305,15 +4414,15 @@ function readWebSnapshot(defaultFile) {
|
|
|
4305
4414
|
}
|
|
4306
4415
|
});
|
|
4307
4416
|
if (pages.length === 0) pages.push({ id: "", title: "\u7B49\u5F85\u7B2C\u4E00\u5F20\u5730\u56FE", modified: 0, map: EMPTY_MAP });
|
|
4308
|
-
const value = { project: basename2(
|
|
4309
|
-
return { revision:
|
|
4417
|
+
const value = { project: basename2(dirname5(dirname5(defaultFile))), pages };
|
|
4418
|
+
return { revision: createHash2("sha256").update(JSON.stringify(value)).digest("hex"), value };
|
|
4310
4419
|
}
|
|
4311
4420
|
|
|
4312
4421
|
// src/web/launcher.ts
|
|
4313
|
-
var webRuntimeFile = (defaultFile) =>
|
|
4422
|
+
var webRuntimeFile = (defaultFile) => join4(dirname6(defaultFile), "web", "server.json");
|
|
4314
4423
|
async function runningWebUrl(defaultFile) {
|
|
4315
4424
|
try {
|
|
4316
|
-
const info = JSON.parse(
|
|
4425
|
+
const info = JSON.parse(readFileSync3(webRuntimeFile(defaultFile), "utf8"));
|
|
4317
4426
|
if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535 || !/^[a-f0-9]{48}$/.test(info.token)) return void 0;
|
|
4318
4427
|
const url = `http://127.0.0.1:${info.port}/${info.token}/`;
|
|
4319
4428
|
const response = await fetch(`${url}api/health`, { signal: AbortSignal.timeout(700) });
|
|
@@ -4325,14 +4434,14 @@ async function runningWebUrl(defaultFile) {
|
|
|
4325
4434
|
async function openWebPreview(defaultFile, entry, page, terminal = false) {
|
|
4326
4435
|
if (page !== void 0 && (!ID_RULE.test(page) || !readWebSnapshot(defaultFile).value.pages.some((p) => p.id === page))) throw new Error(`No map page named "${page}".`);
|
|
4327
4436
|
let url = await runningWebUrl(defaultFile);
|
|
4328
|
-
if (url
|
|
4437
|
+
if (url) {
|
|
4329
4438
|
const health = await fetch(`${url}api/health`, { signal: AbortSignal.timeout(2e3) });
|
|
4330
4439
|
const info = await health.json();
|
|
4331
|
-
if (!info.surfaces?.includes("web-terminal")) {
|
|
4440
|
+
if (!info.formats?.includes(2) || terminal && !info.surfaces?.includes("web-terminal")) {
|
|
4332
4441
|
await fetch(`${url}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
|
|
4333
4442
|
const deadline = Date.now() + 3e3;
|
|
4334
4443
|
while (await runningWebUrl(defaultFile)) {
|
|
4335
|
-
if (Date.now() > deadline) throw new Error("Old web viewer is still stopping. Retry opening the
|
|
4444
|
+
if (Date.now() > deadline) throw new Error("Old web viewer is still stopping. Retry opening the map.");
|
|
4336
4445
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4337
4446
|
}
|
|
4338
4447
|
url = void 0;
|
|
@@ -4365,9 +4474,9 @@ import { randomBytes } from "node:crypto";
|
|
|
4365
4474
|
import { createServer } from "node:http";
|
|
4366
4475
|
|
|
4367
4476
|
// src/preview/publisher.ts
|
|
4368
|
-
import { createHash as
|
|
4369
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
4370
|
-
import { dirname as
|
|
4477
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4478
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync, rmdirSync } from "node:fs";
|
|
4479
|
+
import { dirname as dirname7, join as join5, resolve } from "node:path";
|
|
4371
4480
|
|
|
4372
4481
|
// src/semantics/vocabulary.ts
|
|
4373
4482
|
var STATUS_GLYPHS = {
|
|
@@ -5084,15 +5193,15 @@ var PREVIEW_DIR_NAME = "previews";
|
|
|
5084
5193
|
var ENABLED = ".enabled";
|
|
5085
5194
|
var PUBLISH_LOCK = ".publish-lock";
|
|
5086
5195
|
function previewDirectory(defaultFile) {
|
|
5087
|
-
return
|
|
5196
|
+
return join5(dirname7(defaultFile), PREVIEW_DIR_NAME);
|
|
5088
5197
|
}
|
|
5089
5198
|
function previewFile(defaultFile, page) {
|
|
5090
5199
|
if (page !== void 0 && !ID_RULE.test(page)) throw new Error("Invalid preview page id.");
|
|
5091
|
-
return
|
|
5200
|
+
return join5(previewDirectory(defaultFile), documentName(page));
|
|
5092
5201
|
}
|
|
5093
5202
|
function save(path, contents) {
|
|
5094
5203
|
try {
|
|
5095
|
-
if (
|
|
5204
|
+
if (readFileSync4(path, "utf8") === contents) return;
|
|
5096
5205
|
} catch (error) {
|
|
5097
5206
|
if (error.code !== "ENOENT") throw error;
|
|
5098
5207
|
}
|
|
@@ -5100,19 +5209,19 @@ function save(path, contents) {
|
|
|
5100
5209
|
if (!result.ok) throw new Error(describeStoreError(result.error));
|
|
5101
5210
|
}
|
|
5102
5211
|
function ownedDirectory(path) {
|
|
5103
|
-
|
|
5104
|
-
const expected =
|
|
5212
|
+
mkdirSync3(path, { recursive: true });
|
|
5213
|
+
const expected = join5(realpathSync(dirname7(path)), path.slice(dirname7(path).length + 1));
|
|
5105
5214
|
const actual = realpathSync(path);
|
|
5106
5215
|
if (process.platform === "win32" ? actual.toLowerCase() !== expected.toLowerCase() : actual !== expected) {
|
|
5107
5216
|
throw new Error(`Preview directory redirects outside its parent: ${path}`);
|
|
5108
5217
|
}
|
|
5109
5218
|
}
|
|
5110
5219
|
function acquireLock(directory) {
|
|
5111
|
-
const path =
|
|
5220
|
+
const path = join5(directory, PUBLISH_LOCK);
|
|
5112
5221
|
const deadline = Date.now() + 2e3;
|
|
5113
5222
|
while (true) {
|
|
5114
5223
|
try {
|
|
5115
|
-
|
|
5224
|
+
mkdirSync3(path);
|
|
5116
5225
|
return () => rmdirSync(path);
|
|
5117
5226
|
} catch (error) {
|
|
5118
5227
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -5123,7 +5232,7 @@ function acquireLock(directory) {
|
|
|
5123
5232
|
}
|
|
5124
5233
|
function createPreviewPublisher(defaultFile) {
|
|
5125
5234
|
const directory = previewDirectory(defaultFile);
|
|
5126
|
-
const enabledFile =
|
|
5235
|
+
const enabledFile = join5(directory, ENABLED);
|
|
5127
5236
|
const enabled = () => existsSync3(enabledFile);
|
|
5128
5237
|
const refresh = (page) => {
|
|
5129
5238
|
try {
|
|
@@ -5141,24 +5250,24 @@ function createPreviewPublisher(defaultFile) {
|
|
|
5141
5250
|
}
|
|
5142
5251
|
if (page !== void 0 && !pages.some((p) => p.page === page)) return err(`No map page named "${page}".`);
|
|
5143
5252
|
if (page === void 0 && !pages.some((p) => p.page === void 0)) pages.unshift({ page: void 0, map: EMPTY_MAP });
|
|
5144
|
-
const images =
|
|
5253
|
+
const images = join5(directory, "images");
|
|
5145
5254
|
ownedDirectory(images);
|
|
5146
5255
|
const present = /* @__PURE__ */ new Set();
|
|
5147
5256
|
for (const item of pages) {
|
|
5148
5257
|
const svg = renderMapSvg(item.map);
|
|
5149
|
-
const digest =
|
|
5258
|
+
const digest = createHash3("sha256").update(svg).digest("hex");
|
|
5150
5259
|
const image = `images/${digest}.svg`;
|
|
5151
|
-
save(
|
|
5260
|
+
save(join5(images, `${digest}.svg`), svg);
|
|
5152
5261
|
const filename = documentName(item.page);
|
|
5153
|
-
save(
|
|
5262
|
+
save(join5(directory, filename), renderMapMarkdown(item.map, image, pages));
|
|
5154
5263
|
present.add(filename);
|
|
5155
5264
|
}
|
|
5156
5265
|
for (const filename of readdirSync2(directory)) {
|
|
5157
5266
|
if (/^(map|page-[a-z0-9][a-z0-9-]{0,63})\.md$/.test(filename) && !present.has(filename)) {
|
|
5158
|
-
save(
|
|
5267
|
+
save(join5(directory, filename), "# \u5730\u56FE\u5DF2\u5220\u9664\n\n\u6B64\u9875\u9762\u5DF2\u4E0D\u5728\u9879\u76EE\u5730\u56FE\u4E2D\u3002\n\n[\u8FD4\u56DE\u5730\u56FE\u76EE\u5F55](index.md)\n");
|
|
5159
5268
|
}
|
|
5160
5269
|
}
|
|
5161
|
-
const index =
|
|
5270
|
+
const index = join5(directory, "index.md");
|
|
5162
5271
|
save(index, renderPreviewIndex(pages));
|
|
5163
5272
|
return ok({ path: resolve(path), index: resolve(index), pages: pages.length });
|
|
5164
5273
|
} finally {
|
|
@@ -5327,12 +5436,117 @@ function attachTerminalService(server, options) {
|
|
|
5327
5436
|
};
|
|
5328
5437
|
}
|
|
5329
5438
|
|
|
5439
|
+
// src/support/open-star.ts
|
|
5440
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5441
|
+
import { win32 } from "node:path";
|
|
5442
|
+
|
|
5443
|
+
// src/support/star-reminder.ts
|
|
5444
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
5445
|
+
import { basename as basename3, dirname as dirname8, join as join6 } from "node:path";
|
|
5446
|
+
var STAR_URL = "https://github.com/GuangminJu/mellos-mapping";
|
|
5447
|
+
var DAY_MS = 864e5;
|
|
5448
|
+
function isNpmInstallation(entry) {
|
|
5449
|
+
try {
|
|
5450
|
+
const root = dirname8(dirname8(realpathSync2(entry)));
|
|
5451
|
+
if (basename3(dirname8(root)).toLowerCase() !== "node_modules" || existsSync4(join6(root, ".git")) || existsSync4(join6(root, ".codex-plugin")) || existsSync4(join6(root, ".claude-plugin"))) return false;
|
|
5452
|
+
const pkg = JSON.parse(readFileSync5(join6(root, "package.json"), "utf8"));
|
|
5453
|
+
return pkg.name === "mellos-mapping" && pkg.bin?.mmap === "dist/mmap.mjs";
|
|
5454
|
+
} catch {
|
|
5455
|
+
return false;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
function starReminderFile(userBase) {
|
|
5459
|
+
return join6(userBase, ".mellos", "support", "star-reminder.json");
|
|
5460
|
+
}
|
|
5461
|
+
function enabledFlag(value) {
|
|
5462
|
+
return !!value && value !== "0" && value.toLowerCase() !== "false";
|
|
5463
|
+
}
|
|
5464
|
+
function validState(value) {
|
|
5465
|
+
if (!value || typeof value !== "object") return false;
|
|
5466
|
+
const s = value;
|
|
5467
|
+
return s.version === 1 && Number.isFinite(s.firstSeen) && s.firstSeen >= 0 && Number.isSafeInteger(s.lastDay) && s.lastDay >= Math.floor(s.firstSeen / DAY_MS) && Number.isSafeInteger(s.days) && s.days >= 1 && s.days <= 3 && typeof s.notified === "boolean";
|
|
5468
|
+
}
|
|
5469
|
+
function createStarReminder(options) {
|
|
5470
|
+
const eligible = isNpmInstallation(options.entry) && !enabledFlag(options.env["CI"]) && !enabledFlag(options.env["MELLOS_MAPPING_NO_STAR"]);
|
|
5471
|
+
const file = starReminderFile(options.userBase);
|
|
5472
|
+
return {
|
|
5473
|
+
visit() {
|
|
5474
|
+
if (!eligible) return false;
|
|
5475
|
+
try {
|
|
5476
|
+
return withStoreLock(file, () => {
|
|
5477
|
+
const now = (options.now ?? Date.now)();
|
|
5478
|
+
if (!Number.isFinite(now) || now < 0) return false;
|
|
5479
|
+
const day = Math.floor(now / DAY_MS);
|
|
5480
|
+
let state;
|
|
5481
|
+
try {
|
|
5482
|
+
const saved2 = JSON.parse(readFileSync5(file, "utf8"));
|
|
5483
|
+
if (!validState(saved2)) return false;
|
|
5484
|
+
state = saved2;
|
|
5485
|
+
} catch (error) {
|
|
5486
|
+
if (error.code !== "ENOENT") return false;
|
|
5487
|
+
state = { version: 1, firstSeen: now, lastDay: day, days: 1, notified: false };
|
|
5488
|
+
}
|
|
5489
|
+
if (state.notified || now < state.firstSeen || day < state.lastDay) return false;
|
|
5490
|
+
if (day > state.lastDay) {
|
|
5491
|
+
state.days = Math.min(3, state.days + 1);
|
|
5492
|
+
state.lastDay = day;
|
|
5493
|
+
}
|
|
5494
|
+
const show = state.days >= 3 && now - state.firstSeen >= 7 * DAY_MS;
|
|
5495
|
+
if (show) state.notified = true;
|
|
5496
|
+
const saved = writeAtomic(file, JSON.stringify(state, null, 2) + "\n", 1);
|
|
5497
|
+
return saved.ok && show;
|
|
5498
|
+
});
|
|
5499
|
+
} catch {
|
|
5500
|
+
return false;
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
};
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
// src/support/open-star.ts
|
|
5507
|
+
function starBrowserCommand(platform, windowsRoot = "C:\\Windows") {
|
|
5508
|
+
if (platform === "win32") return {
|
|
5509
|
+
file: win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
5510
|
+
args: [
|
|
5511
|
+
"-NoLogo",
|
|
5512
|
+
"-NoProfile",
|
|
5513
|
+
"-NonInteractive",
|
|
5514
|
+
"-WindowStyle",
|
|
5515
|
+
"Hidden",
|
|
5516
|
+
"-Command",
|
|
5517
|
+
`$ErrorActionPreference = 'Stop'; Start-Process -FilePath '${STAR_URL}'`
|
|
5518
|
+
]
|
|
5519
|
+
};
|
|
5520
|
+
if (platform === "darwin") return { file: "/usr/bin/open", args: [STAR_URL] };
|
|
5521
|
+
if (platform === "linux") return { file: "xdg-open", args: [STAR_URL] };
|
|
5522
|
+
throw new Error("No supported desktop browser launcher");
|
|
5523
|
+
}
|
|
5524
|
+
function openStarInDefaultBrowser() {
|
|
5525
|
+
const command = starBrowserCommand(process.platform, process.env["SystemRoot"]);
|
|
5526
|
+
return new Promise((resolve3, reject) => {
|
|
5527
|
+
const child = spawn3(command.file, command.args, { windowsHide: true, stdio: "ignore" });
|
|
5528
|
+
let finished = false;
|
|
5529
|
+
const finish = (error) => {
|
|
5530
|
+
if (finished) return;
|
|
5531
|
+
finished = true;
|
|
5532
|
+
clearTimeout(deadline);
|
|
5533
|
+
child.unref();
|
|
5534
|
+
if (error) reject(error);
|
|
5535
|
+
else resolve3();
|
|
5536
|
+
};
|
|
5537
|
+
const deadline = setTimeout(() => finish(), 5e3);
|
|
5538
|
+
child.once("error", (error) => finish(error));
|
|
5539
|
+
child.once("exit", (code) => finish(code === 0 ? void 0 : new Error("Default browser launch failed")));
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5330
5543
|
// src/web/service.ts
|
|
5331
5544
|
async function startWebService(defaultFile, assets, options = {}) {
|
|
5332
5545
|
const token = options.token ?? randomBytes(24).toString("hex");
|
|
5333
5546
|
if (!/^[a-f0-9]{48}$/.test(token)) throw new Error("Invalid web token");
|
|
5334
5547
|
const prefix = `/${token}/`;
|
|
5335
5548
|
let origin = "", lastRequest = Date.now(), closed = false;
|
|
5549
|
+
let openingStar = false;
|
|
5336
5550
|
const send = (res, code, body, type = "application/json; charset=utf-8") => {
|
|
5337
5551
|
res.writeHead(code, {
|
|
5338
5552
|
"Content-Type": type,
|
|
@@ -5381,7 +5595,37 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5381
5595
|
}
|
|
5382
5596
|
}
|
|
5383
5597
|
if (req.method === "GET" && route === "api/health") {
|
|
5384
|
-
send(res, 200, JSON.stringify({ file: defaultFile, pid: process.pid, surfaces: terminalService ? ["web", "web-terminal"] : ["web"] }));
|
|
5598
|
+
send(res, 200, JSON.stringify({ file: defaultFile, pid: process.pid, formats: [1, 2], surfaces: terminalService ? ["web", "web-terminal"] : ["web"] }));
|
|
5599
|
+
return;
|
|
5600
|
+
}
|
|
5601
|
+
if (req.method === "POST" && route === "api/star-reminder/open") {
|
|
5602
|
+
if (req.headers.origin !== origin) {
|
|
5603
|
+
send(res, 403, '{"error":"Origin refused"}');
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
if (req.url !== `${prefix}api/star-reminder/open` || req.headers["transfer-encoding"] || Number(req.headers["content-length"] ?? 0) !== 0) {
|
|
5607
|
+
send(res, 400, '{"error":"No parameters accepted"}');
|
|
5608
|
+
return;
|
|
5609
|
+
}
|
|
5610
|
+
if (openingStar) {
|
|
5611
|
+
send(res, 409, '{"error":"Browser is already opening"}');
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
openingStar = true;
|
|
5615
|
+
void Promise.resolve().then(options.openStar ?? openStarInDefaultBrowser).then(() => {
|
|
5616
|
+
send(res, 200, '{"opened":true}');
|
|
5617
|
+
}, () => {
|
|
5618
|
+
send(res, 503, '{"error":"\u65E0\u6CD5\u6253\u5F00\u9ED8\u8BA4\u6D4F\u89C8\u5668"}');
|
|
5619
|
+
}).finally(() => {
|
|
5620
|
+
openingStar = false;
|
|
5621
|
+
});
|
|
5622
|
+
return;
|
|
5623
|
+
}
|
|
5624
|
+
if (req.method === "POST" && route === "api/star-reminder/visit") {
|
|
5625
|
+
const requested = new URL(req.url, origin).searchParams.get("page");
|
|
5626
|
+
const id = !requested || requested === "_default" ? "" : requested;
|
|
5627
|
+
const hasMap = options.starReminder && readWebSnapshot(defaultFile).value.pages.some((page) => page.id === id && !page.error && (page.map?.nodes.length ?? 0) > 0);
|
|
5628
|
+
send(res, 200, JSON.stringify({ show: !!hasMap && options.starReminder.visit() }));
|
|
5385
5629
|
return;
|
|
5386
5630
|
}
|
|
5387
5631
|
if (req.method === "GET" && route === "api/state") {
|
|
@@ -5396,23 +5640,26 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5396
5640
|
return;
|
|
5397
5641
|
}
|
|
5398
5642
|
if (req.method === "DELETE" && route.startsWith("api/pages/")) {
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5643
|
+
withStoreLock(defaultFile, () => {
|
|
5644
|
+
const id = route.slice("api/pages/".length);
|
|
5645
|
+
if (id !== "_default" && !ID_RULE.test(id)) {
|
|
5646
|
+
send(res, 400, '{"error":"Invalid page"}');
|
|
5647
|
+
return;
|
|
5648
|
+
}
|
|
5649
|
+
if (req.headers["if-match"] !== `"${readWebSnapshot(defaultFile).revision}"`) {
|
|
5650
|
+
send(res, 409, '{"error":"\u5730\u56FE\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u786E\u8BA4\u5220\u9664\u3002"}');
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5653
|
+
const result = deletePageFile(pageFilePath(defaultFile, id === "_default" ? void 0 : id));
|
|
5654
|
+
if (!result.ok) {
|
|
5655
|
+
send(res, 500, JSON.stringify({ error: describeStoreError(result.error) }));
|
|
5656
|
+
return;
|
|
5657
|
+
}
|
|
5658
|
+
const previews = createPreviewPublisher(defaultFile);
|
|
5659
|
+
const refreshed = previews.enabled() ? previews.refresh() : void 0;
|
|
5660
|
+
send(res, 200, JSON.stringify({ deleted: true, ...refreshed && !refreshed.ok ? { warning: `\u5730\u56FE\u5DF2\u5220\u9664\uFF0CMarkdown \u9884\u89C8\u5F85\u91CD\u65B0\u751F\u6210\uFF1A${refreshed.error}` } : {} }));
|
|
5411
5661
|
return;
|
|
5412
|
-
}
|
|
5413
|
-
const previews = createPreviewPublisher(defaultFile);
|
|
5414
|
-
const refreshed = previews.enabled() ? previews.refresh() : void 0;
|
|
5415
|
-
send(res, 200, JSON.stringify({ deleted: true, ...refreshed && !refreshed.ok ? { warning: `\u5730\u56FE\u5DF2\u5220\u9664\uFF0CMarkdown \u9884\u89C8\u5F85\u91CD\u65B0\u751F\u6210\uFF1A${refreshed.error}` } : {} }));
|
|
5662
|
+
});
|
|
5416
5663
|
return;
|
|
5417
5664
|
}
|
|
5418
5665
|
if (req.method === "POST" && route === "api/stop") {
|
|
@@ -5422,7 +5669,7 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5422
5669
|
}
|
|
5423
5670
|
send(res, 404, '{"error":"Not found"}');
|
|
5424
5671
|
} catch (error) {
|
|
5425
|
-
send(res, 500, JSON.stringify({ error: String(error) }));
|
|
5672
|
+
send(res, error instanceof LedgerError && error.code === "BUSY" ? 409 : 500, JSON.stringify({ error: String(error) }));
|
|
5426
5673
|
}
|
|
5427
5674
|
});
|
|
5428
5675
|
const terminalService = assets.terminal && options.terminalWorker ? attachTerminalService(server, {
|
|
@@ -5471,19 +5718,19 @@ async function runWeb(args) {
|
|
|
5471
5718
|
if (args[0] === "--serve" && args.length === 2) {
|
|
5472
5719
|
const file2 = resolve2(args[1]);
|
|
5473
5720
|
if (await runningWebUrl(file2)) return;
|
|
5474
|
-
const directory =
|
|
5475
|
-
|
|
5476
|
-
if (
|
|
5477
|
-
const assets =
|
|
5721
|
+
const directory = dirname9(webRuntimeFile(file2));
|
|
5722
|
+
mkdirSync4(directory, { recursive: true });
|
|
5723
|
+
if (realpathSync3(directory).toLowerCase() !== join7(realpathSync3(dirname9(directory)), "web").toLowerCase()) throw new Error("Redirected web runtime directory");
|
|
5724
|
+
const assets = join7(dirname9(entry), "web");
|
|
5478
5725
|
let service;
|
|
5479
5726
|
service = await startWebService(file2, {
|
|
5480
|
-
html:
|
|
5481
|
-
javascript:
|
|
5482
|
-
css:
|
|
5483
|
-
terminal: { html:
|
|
5484
|
-
}, { terminalWorker:
|
|
5727
|
+
html: readFileSync6(join7(assets, "index.html"), "utf8"),
|
|
5728
|
+
javascript: readFileSync6(join7(assets, "app.js"), "utf8"),
|
|
5729
|
+
css: readFileSync6(join7(assets, "app.css"), "utf8"),
|
|
5730
|
+
terminal: { html: readFileSync6(join7(assets, "terminal.html"), "utf8"), javascript: readFileSync6(join7(assets, "terminal.js"), "utf8"), css: readFileSync6(join7(assets, "terminal.css"), "utf8"), xtermCss: readFileSync6(join7(assets, "xterm.css"), "utf8") }
|
|
5731
|
+
}, { starReminder: createStarReminder({ entry, userBase: homedir(), env: process.env }), terminalWorker: join7(dirname9(entry), "terminal-worker.mjs"), onClose: () => {
|
|
5485
5732
|
try {
|
|
5486
|
-
if (JSON.parse(
|
|
5733
|
+
if (JSON.parse(readFileSync6(webRuntimeFile(file2), "utf8")).token === service.token) rmSync4(webRuntimeFile(file2));
|
|
5487
5734
|
} catch {
|
|
5488
5735
|
}
|
|
5489
5736
|
} });
|
|
@@ -5514,8 +5761,8 @@ async function runWeb(args) {
|
|
|
5514
5761
|
else throw new Error(usage);
|
|
5515
5762
|
}
|
|
5516
5763
|
const project = resolve2(args[0]);
|
|
5517
|
-
if (!
|
|
5518
|
-
const file =
|
|
5764
|
+
if (!existsSync5(project) || !statSync2(project).isDirectory()) throw new Error(`Project directory does not exist: ${project}`);
|
|
5765
|
+
const file = join7(project, STATE_FILE_RELATIVE_PATH);
|
|
5519
5766
|
if (stop) {
|
|
5520
5767
|
const url = await runningWebUrl(file);
|
|
5521
5768
|
if (url) await fetch(`${url}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
|
|
@@ -5524,7 +5771,7 @@ async function runWeb(args) {
|
|
|
5524
5771
|
}
|
|
5525
5772
|
console.log(JSON.stringify({ surface: terminal ? "web-terminal" : "web", url: await openWebPreview(file, entry, page, terminal), visibility: "unconfirmed" }));
|
|
5526
5773
|
}
|
|
5527
|
-
if (process.argv[1] &&
|
|
5774
|
+
if (process.argv[1] && existsSync5(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync3(process.argv[1])).href) {
|
|
5528
5775
|
runWeb(process.argv.slice(2)).catch((error) => {
|
|
5529
5776
|
console.error(String(error));
|
|
5530
5777
|
process.exitCode = 1;
|
package/docs/codex.md
CHANGED
|
@@ -12,7 +12,7 @@ From the `chatgpt-app` branch run `node install.mjs`; from `main` run
|
|
|
12
12
|
|
|
13
13
|
The installer checks file hashes and a real MCP handshake, copies the runtime to
|
|
14
14
|
`~/.mellos/installations/chatgpt-app/`, registers the `mellos-mapping-codex`
|
|
15
|
-
marketplace, installs the skill, and registers the
|
|
15
|
+
marketplace, installs the skill, and registers the eight MCP tools at user scope.
|
|
16
16
|
The runtime uses an absolute Node executable and leaves its working directory
|
|
17
17
|
unset so each conversation writes to its own project. The clone can be deleted
|
|
18
18
|
or moved after installation. Other plugins, maps and mapping policies are preserved.
|
|
@@ -41,6 +41,12 @@ publish the package into OpenAI's public plugin directory.
|
|
|
41
41
|
|
|
42
42
|
## Automatic web terminal
|
|
43
43
|
|
|
44
|
+
For saved-map discovery, structured CRUD, checkpoints and concurrent updates,
|
|
45
|
+
see [the persistent-map API guide](map-api.md). Start by reading existing pages;
|
|
46
|
+
a new conversation does not require a new map. Restart older MCP/native watchers
|
|
47
|
+
before writing maps with format-2 context or source references. Reopening a Web
|
|
48
|
+
surface replaces services without format-2 support and returns a new URL.
|
|
49
|
+
|
|
44
50
|
Call `mmap_open {surface: "web-terminal", page: "<slug>"}` and pass the returned
|
|
45
51
|
`hostOpen` object to `open_in_codex`. It uses `placement: "right"` and a browser
|
|
46
52
|
target in the current conversation. The local service starts mmap when the
|