mellos-mapping 0.20.3 → 0.23.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 +130 -69
- package/README.zh-CN.md +126 -65
- package/dist/hook-session-start.mjs +28 -20
- package/dist/mmap.mjs +303 -171
- package/dist/preview.mjs +1457 -0
- package/dist/server.mjs +1984 -741
- package/dist/store-paths.mjs +69 -22
- package/dist/terminal-worker.mjs +3331 -0
- package/dist/watch.mjs +957 -571
- package/dist/web/TERMINAL-LICENSES.txt +70 -0
- package/dist/web/app.css +967 -0
- package/dist/web/app.js +1356 -0
- package/dist/web/index.html +9 -0
- package/dist/web/terminal.css +9 -0
- package/dist/web/terminal.html +7 -0
- package/dist/web/terminal.js +9293 -0
- package/dist/web/xterm.css +285 -0
- package/dist/web.mjs +5646 -0
- package/docs/codex.md +189 -0
- 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.d.ts +9 -0
- package/lib/domain/text.js +54 -0
- package/lib/domain/types.d.ts +3 -0
- package/lib/preview/index.d.ts +3 -0
- package/lib/preview/index.js +3 -0
- package/lib/preview/markdown.d.ts +8 -0
- package/lib/preview/markdown.js +54 -0
- package/lib/preview/presentation.d.ts +6 -0
- package/lib/preview/presentation.js +14 -0
- package/lib/preview/publisher.d.ts +23 -0
- package/lib/preview/publisher.js +143 -0
- package/lib/preview/svg.d.ts +3 -0
- package/lib/preview/svg.js +74 -0
- package/lib/preview/text.d.ts +4 -0
- package/lib/preview/text.js +13 -0
- package/lib/render/canvas.d.ts +1 -1
- package/lib/render/canvas.js +4 -2
- package/lib/render/draw.js +9 -6
- package/lib/render/render.d.ts +6 -0
- package/lib/render/render.js +50 -18
- package/lib/render/width.js +3 -1
- package/lib/store/atomic.d.ts +18 -0
- package/lib/store/atomic.js +86 -0
- package/lib/store/channels.d.ts +40 -0
- package/lib/store/channels.js +135 -0
- package/lib/store/format.js +21 -4
- package/lib/store/json-text.d.ts +9 -0
- package/lib/store/json-text.js +16 -0
- package/lib/store/maps.d.ts +12 -0
- package/lib/store/maps.js +42 -0
- package/lib/store/migration.d.ts +12 -0
- package/lib/store/migration.js +46 -0
- package/lib/store/pages.d.ts +46 -0
- package/lib/store/pages.js +89 -0
- package/lib/store/policy.d.ts +74 -0
- package/lib/store/policy.js +144 -0
- package/lib/store/project.d.ts +2 -0
- package/lib/store/project.js +29 -0
- package/lib/store/store.d.ts +15 -257
- package/lib/store/store.js +16 -695
- package/lib/store/transaction.d.ts +12 -0
- package/lib/store/transaction.js +91 -0
- package/lib/store/viewers.d.ts +81 -0
- package/lib/store/viewers.js +186 -0
- package/package.json +27 -5
- package/scripts/codex-cli.mjs +42 -0
- package/scripts/codex-register.mjs +27 -94
- package/scripts/mmap.mjs +27 -17
- package/scripts/open-pane.mjs +37 -18
- package/scripts/pane-core.mjs +57 -220
- package/scripts/terminal-session.mjs +137 -0
- package/scripts/tmux-session.mjs +90 -0
- package/scripts/watcher-command.mjs +16 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** tmux adapter: discover an attached session and address it without shell eval. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { watcherArgs } from './watcher-command.mjs';
|
|
5
|
+
|
|
6
|
+
const SESSION_FORMAT = '#{session_id}\t#{session_attached}';
|
|
7
|
+
const TARGET_FORMAT = '#{socket_path}\t#{session_id}\t#{session_created}\t#{session_attached}\t#{pane_id}\t#{pane_pid}';
|
|
8
|
+
const PANE_FORMAT = '#{pane_id}\t#{pane_pid}\t#{window_id}\t#{window_active}\t#{window_zoomed_flag}\t#{pane_active}';
|
|
9
|
+
|
|
10
|
+
export function createTmuxAdapter({ env = process.env, spawn = spawnSync, nodePath = process.execPath } = {}) {
|
|
11
|
+
// Socket paths may contain commas. Only the final two fields are numeric.
|
|
12
|
+
const inherited = /^(.*),\d+,(\d+)$/.exec(env.TMUX ?? '');
|
|
13
|
+
const socket = env.MELLOS_MAPPING_TMUX_SOCKET || inherited?.[1];
|
|
14
|
+
|
|
15
|
+
function run(args, socketPath = socket) {
|
|
16
|
+
const result = spawn('tmux', [...(socketPath ? ['-S', socketPath] : []), ...args], {
|
|
17
|
+
env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5_000, windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
return result.status === 0
|
|
20
|
+
? { ok: true, value: result.stdout.trim() }
|
|
21
|
+
: { ok: false, error: `tmux ${args[0]} failed: ${result.error?.message || result.stderr?.trim() || `exit ${result.status}`}` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function inspectSession(cfg, viewers = []) {
|
|
25
|
+
// An explicitly selected socket must not inherit a pane id from another server.
|
|
26
|
+
const sameServer = !env.MELLOS_MAPPING_TMUX_SOCKET || socket === inherited?.[1];
|
|
27
|
+
let selected = env.MELLOS_MAPPING_TMUX_TARGET || (sameServer && inherited
|
|
28
|
+
? (/^%\d+$/.test(env.TMUX_PANE ?? '') ? env.TMUX_PANE : `$${inherited[2]}:`)
|
|
29
|
+
: undefined);
|
|
30
|
+
if (!selected) {
|
|
31
|
+
const listed = run(['list-sessions', '-F', SESSION_FORMAT]);
|
|
32
|
+
if (!listed.ok) return listed;
|
|
33
|
+
const attached = listed.value.split('\n').map(line => line.split('\t'))
|
|
34
|
+
.filter(([id, count]) => /^\$\d+$/.test(id) && Number(count) > 0).map(([id]) => id);
|
|
35
|
+
if (attached.length !== 1) return { ok: false, error: attached.length === 0
|
|
36
|
+
? 'No attached tmux session was found. Attach a session before retrying.'
|
|
37
|
+
: `Multiple attached tmux sessions (${attached.join(', ')}). Set MELLOS_MAPPING_TMUX_TARGET to the intended session or pane in the MCP server environment before retrying.` };
|
|
38
|
+
selected = `${attached[0]}:`;
|
|
39
|
+
}
|
|
40
|
+
const inspected = run(['display-message', '-p', '-t', selected, TARGET_FORMAT]);
|
|
41
|
+
if (!inspected.ok) return inspected;
|
|
42
|
+
const [socketPath, session, created, attached, pane, panePid] = inspected.value.split('\t');
|
|
43
|
+
if (!socketPath || !/^\$\d+$/.test(session) || !/^\d+$/.test(created) || !/^%\d+$/.test(pane)) {
|
|
44
|
+
return { ok: false, error: 'tmux returned an invalid session target; no pane was opened.' };
|
|
45
|
+
}
|
|
46
|
+
if (!(Number(attached) > 0)) return { ok: false, error: `tmux session ${session} has no attached client. Attach it before retrying.` };
|
|
47
|
+
// Window ownership survives selecting the newly created window; splits
|
|
48
|
+
// belong to the source pane, so conversations in one session stay separate.
|
|
49
|
+
const identity = [socketPath, session, created, cfg.mode, cfg.mode === 'window' ? '' : pane];
|
|
50
|
+
// When a stripped environment resolves to the map itself, its live process
|
|
51
|
+
// report carries the original conversation identity. Do not invent a new owner.
|
|
52
|
+
const selectedViewer = cfg.mode === 'split' ? viewers.find(viewer =>
|
|
53
|
+
viewer.pid === Number(panePid) && /^tmux-[a-f0-9]{24}$/.test(viewer.owner ?? '')) : undefined;
|
|
54
|
+
const owner = selectedViewer?.owner ?? `tmux-${createHash('sha256').update(JSON.stringify(identity)).digest('hex').slice(0, 24)}`;
|
|
55
|
+
return { ok: true, value: { backend: 'tmux', socket: socketPath, session, pane, owner, owners: [owner] } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function revealPane(target, viewer) {
|
|
59
|
+
const listed = run(['list-panes', '-s', '-t', target.session, '-F', PANE_FORMAT], target.socket);
|
|
60
|
+
if (!listed.ok) return listed;
|
|
61
|
+
const pane = listed.value.split('\n').map(line => line.split('\t')).find(fields => Number(fields[1]) === viewer.pid);
|
|
62
|
+
if (!pane || !/^%\d+$/.test(pane[0]) || !/^@\d+$/.test(pane[2])) {
|
|
63
|
+
return { ok: false, error: 'The live watcher could not be located in this tmux session; visibility is unverified.' };
|
|
64
|
+
}
|
|
65
|
+
const selected = run(['select-window', '-t', `${target.session}:${pane[2]}`], target.socket);
|
|
66
|
+
if (!selected.ok) return selected;
|
|
67
|
+
if (pane[4] === '1' && pane[5] !== '1') {
|
|
68
|
+
const unzoomed = run(['resize-pane', '-Z', '-t', pane[0]], target.socket);
|
|
69
|
+
if (!unzoomed.ok) return unzoomed;
|
|
70
|
+
}
|
|
71
|
+
const visible = run(['display-message', '-p', '-t', `${target.session}:${pane[2]}.${pane[0]}`,
|
|
72
|
+
'#{session_attached}\t#{window_active}\t#{window_zoomed_flag}\t#{pane_active}'], target.socket);
|
|
73
|
+
if (!visible.ok) return visible;
|
|
74
|
+
const [attached, active, zoomed, focused] = visible.value.split('\t');
|
|
75
|
+
return Number(attached) > 0 && active === '1' && (zoomed !== '1' || focused === '1')
|
|
76
|
+
? { ok: true, value: 'visible' }
|
|
77
|
+
: { ok: false, error: 'The watcher is running, but its tmux pane is not visible in the attached session.' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openPane(cfg, watchPath, mapFile, target) {
|
|
81
|
+
const placement = target.mode === 'window'
|
|
82
|
+
? ['new-window', '-n', 'mellos-mapping', '-t', `${target.session}:`]
|
|
83
|
+
: ['split-window', '-h', '-d', '-l', '42%', '-t', target.pane];
|
|
84
|
+
const result = run([...placement, '-c', cfg.projectDir,
|
|
85
|
+
nodePath, ...watcherArgs(cfg, watchPath, mapFile), '--owner', target.owner], target.socket);
|
|
86
|
+
return result.ok ? { ok: true, value: { ...target, reason: 'requested' } } : result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { kind: 'tmux', inspectSession, openPane, revealPane };
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Watcher arguments shared by terminal adapters and the copyable fallback. */
|
|
2
|
+
export function watcherArgs(cfg, watchPath, mapFile) {
|
|
3
|
+
const args = [watchPath, '--file', mapFile, ...cfg.watcherFlags];
|
|
4
|
+
if (cfg.pageSlug !== undefined) args.push('--page', cfg.pageSlug);
|
|
5
|
+
return args;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** POSIX shell quoting is only for the human fallback; adapters use argv. */
|
|
9
|
+
export function manualWatcherCommand(cfg, watchPath, mapFile, nodePath = process.execPath) {
|
|
10
|
+
return [nodePath, ...watcherArgs(cfg, watchPath, mapFile)]
|
|
11
|
+
.map(arg => "'" + arg.replaceAll("'", "'\"'\"'") + "'").join(' ');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function paneFailureMessage(error, cfg, watchPath, mapFile, platform = process.platform) {
|
|
15
|
+
return platform === 'win32' ? error : `${error}\nAutomatic opening failed. Do not retry until the terminal environment changes. Run this command in a visible terminal:\n${manualWatcherCommand(cfg, watchPath, mapFile)}`;
|
|
16
|
+
}
|