mdinterface 0.1.1 → 0.2.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/package.json +4 -2
- package/server.js +81 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdinterface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A live markdown canvas wired to Claude Code. Highlight a passage to set both the context and the scope, ask, and Claude edits exactly that and leaves the rest alone.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kevin Sundstrom",
|
|
@@ -51,9 +51,11 @@
|
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"express": "^4.19.0",
|
|
54
|
-
"node-pty": "^1.0.0",
|
|
55
54
|
"ws": "^8.17.0"
|
|
56
55
|
},
|
|
56
|
+
"optionalDependencies": {
|
|
57
|
+
"node-pty": "^1.0.0"
|
|
58
|
+
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@biomejs/biome": "^2.0.0",
|
|
59
61
|
"@types/express": "^4.17.21",
|
package/server.js
CHANGED
|
@@ -16,7 +16,15 @@ const path = require("node:path");
|
|
|
16
16
|
const os = require("node:os");
|
|
17
17
|
const crypto = require("node:crypto");
|
|
18
18
|
const { WebSocketServer } = require("ws");
|
|
19
|
-
|
|
19
|
+
// node-pty powers the embedded terminal pane. It's an OPTIONAL native dependency (see
|
|
20
|
+
// optionalDependencies in package.json): if it didn't install or build, the rendered canvas,
|
|
21
|
+
// the file watcher, and the selection bridge all still come up — only the in-window terminal
|
|
22
|
+
// is disabled, and you run `claude` in your own terminal instead (the hooks still feed it the
|
|
23
|
+
// selection off disk). The specific reason is surfaced to the user at spawn time.
|
|
24
|
+
let pty = null;
|
|
25
|
+
try {
|
|
26
|
+
pty = require("node-pty");
|
|
27
|
+
} catch {}
|
|
20
28
|
|
|
21
29
|
// ---------- args ----------
|
|
22
30
|
const args = process.argv.slice(2);
|
|
@@ -405,13 +413,65 @@ function onPtyData(d) {
|
|
|
405
413
|
flushTerm();
|
|
406
414
|
}, TERM_FLUSH_MS);
|
|
407
415
|
}
|
|
416
|
+
// pnpm, Yarn PnP, CI caches, and Docker COPY frequently strip the +x bit from node-pty's
|
|
417
|
+
// prebuilt spawn-helper, which makes pty.spawn die — the single most common runtime failure.
|
|
418
|
+
// Restore it in-process, since a postinstall script can't help the people who hit this most
|
|
419
|
+
// (--ignore-scripts / pnpm users disable postinstall). Returns true if it actually fixed a bit.
|
|
420
|
+
function ensureSpawnHelperExecutable() {
|
|
421
|
+
let fixed = false;
|
|
422
|
+
try {
|
|
423
|
+
const root = path.dirname(require.resolve("node-pty/package.json")); // works wherever it's hoisted
|
|
424
|
+
for (const base of [path.join(root, "prebuilds"), path.join(root, "build", "Release")]) {
|
|
425
|
+
let ents;
|
|
426
|
+
try {
|
|
427
|
+
ents = fs.readdirSync(base, { withFileTypes: true });
|
|
428
|
+
} catch {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const files = [];
|
|
432
|
+
for (const ent of ents) {
|
|
433
|
+
if (ent.isDirectory()) files.push(path.join(base, ent.name, "spawn-helper"));
|
|
434
|
+
else if (ent.name === "spawn-helper") files.push(path.join(base, ent.name));
|
|
435
|
+
}
|
|
436
|
+
for (const f of files) {
|
|
437
|
+
try {
|
|
438
|
+
if (!(fs.statSync(f).mode & 0o111)) {
|
|
439
|
+
fs.chmodSync(f, 0o755);
|
|
440
|
+
fixed = true;
|
|
441
|
+
}
|
|
442
|
+
} catch {}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch {}
|
|
446
|
+
return fixed;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Turn a spawn failure into a specific, actionable message instead of a generic one.
|
|
450
|
+
function diagnosePtyFailure(e) {
|
|
451
|
+
const msg = (e && e.message) || String(e);
|
|
452
|
+
let hint;
|
|
453
|
+
if (/NODE_MODULE_VERSION|compiled against|different Node/i.test(msg))
|
|
454
|
+
hint = "node-pty was built for a different Node version — rebuild it:\n npm rebuild node-pty";
|
|
455
|
+
else if (/spawn-helper|EACCES|ENOENT|permission/i.test(msg))
|
|
456
|
+
hint =
|
|
457
|
+
"node-pty's spawn-helper is missing or not executable:\n" +
|
|
458
|
+
" chmod +x node_modules/node-pty/prebuilds/*/spawn-helper\n npm rebuild node-pty";
|
|
459
|
+
else hint = "Reinstall the native module:\n npm rebuild node-pty";
|
|
460
|
+
return (
|
|
461
|
+
`Embedded terminal could not start (${msg}).\n${hint}\n` +
|
|
462
|
+
`The canvas and selection bridge still work — run \`${CLAUDE_CMD}\` in your own terminal beside this window.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
408
466
|
function spawnPty(cols = 100, rows = 32) {
|
|
467
|
+
if (!pty) return null; // optional dependency absent — startShell shows the fallback message
|
|
468
|
+
ensureSpawnHelperExecutable(); // proactively fix a stripped +x bit before we spawn
|
|
409
469
|
const env = { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor" };
|
|
410
470
|
const cwd = path.dirname(DOC);
|
|
411
471
|
const sh = process.env.SHELL || (os.platform() === "win32" ? "powershell.exe" : "/bin/bash");
|
|
412
472
|
const opts = { name: "xterm-256color", cols, rows, cwd, env };
|
|
413
|
-
// Launch through the user's interactive login shell so PATH, rc files,
|
|
414
|
-
//
|
|
473
|
+
// Launch through the user's interactive login shell so PATH, rc files, and aliases apply —
|
|
474
|
+
// this is how `claude` is normally found.
|
|
415
475
|
try {
|
|
416
476
|
return pty.spawn(sh, ["-ilc", CLAUDE_CMD], opts);
|
|
417
477
|
} catch (e) {
|
|
@@ -419,18 +479,18 @@ function spawnPty(cols = 100, rows = 32) {
|
|
|
419
479
|
`Could not start "${CLAUDE_CMD}" via ${sh} (${e.message}); opening a plain shell.`
|
|
420
480
|
);
|
|
421
481
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return null;
|
|
482
|
+
// Plain-shell fallback. If even this fails, a stripped +x bit on spawn-helper is the usual
|
|
483
|
+
// cause: self-heal it and retry once before giving up with a diagnosis.
|
|
484
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
485
|
+
try {
|
|
486
|
+
return pty.spawn(sh, [], opts);
|
|
487
|
+
} catch (e) {
|
|
488
|
+
if (attempt === 0 && ensureSpawnHelperExecutable()) continue; // fixed a bit → retry
|
|
489
|
+
console.error(diagnosePtyFailure(e));
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
433
492
|
}
|
|
493
|
+
return null;
|
|
434
494
|
}
|
|
435
495
|
|
|
436
496
|
// Start the PTY and wire BOTH its data and exit handlers (the earlier version reattached
|
|
@@ -445,7 +505,13 @@ let rapidExits = 0;
|
|
|
445
505
|
function startShell() {
|
|
446
506
|
shell = spawnPty(ptyCols, ptyRows); // respawn at the current size, not the 100x32 default
|
|
447
507
|
if (!shell) {
|
|
448
|
-
|
|
508
|
+
// Two cases, both non-fatal: node-pty isn't installed at all, or it is but the spawn
|
|
509
|
+
// failed (details already in the server console via diagnosePtyFailure). Either way the
|
|
510
|
+
// canvas + selection bridge work, so point the user at running claude in their own terminal.
|
|
511
|
+
const data = pty
|
|
512
|
+
? "\r\nEmbedded terminal could not start (see the server console). The canvas and selection still work — run claude in your own terminal beside this window and it gets the same context.\r\n"
|
|
513
|
+
: "\r\nEmbedded terminal disabled: node-pty isn't installed. The canvas and selection still work — run claude in your own terminal beside this window (it gets the same context via the hooks). To enable the in-window terminal: npm i node-pty\r\n";
|
|
514
|
+
broadcast({ type: "term", data });
|
|
449
515
|
return;
|
|
450
516
|
}
|
|
451
517
|
shellSpawnAt = Date.now();
|