trantor 0.18.95 → 0.18.97
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/.claude-plugin/plugin.json +1 -1
- package/bin/crew/open.mjs +37 -7
- package/bin/crew-runner.mjs +2 -1
- package/bin/seat-signal.mjs +97 -0
- package/hooks/sessionstart.mjs +12 -2
- package/lib/project.mjs +11 -5
- package/lib/seat-signal-guard.mjs +30 -0
- package/mcp.mjs +2 -40
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.97",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
package/bin/crew/open.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, openSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, openSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { spawn, execSync } from "node:child_process";
|
|
@@ -43,19 +43,44 @@ function sessionId(ctx) {
|
|
|
43
43
|
return id;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
// #11641: a handoff is stale once its own session kept working after writing it (auto handoffs fire
|
|
47
|
+
// at a context threshold, not at the end). Slack so the write itself, and the hook flushing the
|
|
48
|
+
// transcript around it, never read as "later work".
|
|
49
|
+
export const HANDOFF_SUPERSEDE_GRACE_SECS = 120;
|
|
50
|
+
|
|
51
|
+
function unconsumedHandoffs(ctx) {
|
|
47
52
|
const dir = join(ctx.env.AGENT_BUS_DIR || ctx.env.RELAY_DATA_DIR || join(ctx.home, ".agent-bus"), "handoffs");
|
|
48
|
-
if (!existsSync(dir)) return
|
|
53
|
+
if (!existsSync(dir)) return [];
|
|
49
54
|
const prefix = `${ctx.project}-`;
|
|
55
|
+
const out = [];
|
|
50
56
|
for (const file of readdirSync(dir).filter(name => name.startsWith(prefix) && name.endsWith(".json")).sort().reverse()) {
|
|
51
|
-
try {
|
|
57
|
+
try { const rec = JSON.parse(readFileSync(join(dir, file), "utf8")); if (!rec.consumed) out.push({ file, rec }); }
|
|
52
58
|
catch {}
|
|
53
59
|
}
|
|
54
|
-
return
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The handoff written BY the recorded session `id` whose transcript has since grown past it, or null.
|
|
64
|
+
export function supersededHandoff(ctx, id) {
|
|
65
|
+
for (const { file, rec } of unconsumedHandoffs(ctx)) {
|
|
66
|
+
if (!id || rec.session_id !== id || !(Number(rec.stamp) > 0)) continue;
|
|
67
|
+
const slug = String(rec.project || ctx.dir || "").replace(/[/.]/g, "-");
|
|
68
|
+
const transcript = rec.transcript_path || join(ctx.home, ".claude", "projects", slug, `${id}.jsonl`);
|
|
69
|
+
try {
|
|
70
|
+
const workedAt = statSync(transcript).mtimeMs / 1000;
|
|
71
|
+
if (workedAt > Number(rec.stamp) + HANDOFF_SUPERSEDE_GRACE_SECS) return { file, handoffAt: Number(rec.stamp), workedAt };
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function hasPendingHandoff(ctx, id) {
|
|
78
|
+
const sup = id ? supersededHandoff(ctx, id) : null;
|
|
79
|
+
return unconsumedHandoffs(ctx).some(({ file }) => !sup || file !== sup.file);
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
export function takeoverSessionId(ctx, id) {
|
|
58
|
-
return hasPendingHandoff(ctx) ? randomUUID() : id;
|
|
83
|
+
return hasPendingHandoff(ctx, id) ? randomUUID() : id;
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
function harnessFlag(ctx) {
|
|
@@ -201,7 +226,12 @@ export function openOrchestrator(ctx, args) {
|
|
|
201
226
|
if (!ctx.dry) maybeBuildGraft(ctx.dir);
|
|
202
227
|
if (!ctx.have.herdr) throw new Error("trantor open needs herdr (the pane host) — install: curl -fsSL https://herdr.dev/install.sh | sh");
|
|
203
228
|
let id = sessionId(ctx);
|
|
204
|
-
|
|
229
|
+
const stale = supersededHandoff(ctx, id);
|
|
230
|
+
if (stale) {
|
|
231
|
+
const at = secs => new Date(secs * 1000).toISOString();
|
|
232
|
+
console.error(`— handoff ${stale.file} was superseded by later work in session ${id} (handoff ${at(stale.handoffAt)}, transcript ${at(stale.workedAt)}): resuming it, leaving the handoff unclaimed —`);
|
|
233
|
+
}
|
|
234
|
+
if (hasPendingHandoff(ctx, id)) {
|
|
205
235
|
const fresh = takeoverSessionId(ctx, id);
|
|
206
236
|
console.error(`— recorded session ${id} handed off: starting fresh as ${fresh} to claim it —`);
|
|
207
237
|
id = fresh;
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { snapshotBeforeCut } from "./cut-snapshot.mjs";
|
|
31
31
|
import { ocTurnUsage, dshTurnUsage, usageTotal } from "../lib/turn-usage.mjs";
|
|
32
32
|
import { ocCompactionContent, ocCompactionLogLine } from "../lib/seat-compaction.mjs";
|
|
33
|
+
import { seatSignalPrefix } from "../lib/seat-signal-guard.mjs";
|
|
33
34
|
import { listContainers, newContainers, recordContainers, statePathFor as dockerStatePathFor, sweep as dockerSweep } from "../lib/docker-janitor.mjs";
|
|
34
35
|
import {
|
|
35
36
|
auditDutyNudges, claimDutyNudges, claudeTranscriptDir, dutyEscalations, dutyNudgeDirective,
|
|
@@ -834,7 +835,7 @@ async function runTurn(prompt, isFirst, trigger = "kickoff", opts = {}) {
|
|
|
834
835
|
// #6393: the store's keys ride the spawn env under shadow names and the shell re-exports them
|
|
835
836
|
// AFTER the sources ran, so the keychain wins over a stale .env line and no file is ever copied.
|
|
836
837
|
const secrets = resolveSecrets(process.env);
|
|
837
|
-
cmd = withEnvFiles(withSecretExports(cmd, Object.keys(secrets)), envs);
|
|
838
|
+
cmd = withEnvFiles(withSecretExports(seatSignalPrefix(TURN_DIR) + cmd, Object.keys(secrets)), envs);
|
|
838
839
|
log(`turn starting (${isFirst ? "fresh session" : "resume"})${MODEL ? ` · model=${MODEL}` : ""}`);
|
|
839
840
|
// #7777: the one effort line — which effort parameters were set, or that the model is uncatalogued.
|
|
840
841
|
if (EFFORT && EFFORT_FLAG.text) log(EFFORT_FLAG.text);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { constants } from "node:os";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
|
|
6
|
+
const [command, ...args] = process.argv.slice(2);
|
|
7
|
+
const refuse = reason => { throw new Error(reason); };
|
|
8
|
+
|
|
9
|
+
function signalValue(value) {
|
|
10
|
+
if (/^\d+$/.test(value)) {
|
|
11
|
+
const n = Number(value);
|
|
12
|
+
if (n === 0 || Object.values(constants.signals).includes(n)) return n;
|
|
13
|
+
}
|
|
14
|
+
const name = value.toUpperCase().replace(/^SIG/, "");
|
|
15
|
+
if (constants.signals[`SIG${name}`]) return constants.signals[`SIG${name}`];
|
|
16
|
+
return refuse("unknown signal");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function processTable() {
|
|
20
|
+
const ps = spawnSync("/bin/ps", ["-axo", "pid=,ppid=,comm="], { encoding: "utf8", timeout: 5000 });
|
|
21
|
+
if (ps.error || ps.status !== 0) refuse("cannot inspect process ancestry");
|
|
22
|
+
const rows = new Map();
|
|
23
|
+
for (const line of ps.stdout.split("\n")) {
|
|
24
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
25
|
+
if (match) rows.set(Number(match[1]), { parent: Number(match[2]), name: basename(match[3].trim()) });
|
|
26
|
+
}
|
|
27
|
+
return rows;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function descendant(pid, root, rows) {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
while (pid !== root && rows.has(pid) && !seen.has(pid)) {
|
|
33
|
+
seen.add(pid);
|
|
34
|
+
pid = rows.get(pid).parent;
|
|
35
|
+
}
|
|
36
|
+
return seen.size > 0 && pid === root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function targets(rows) {
|
|
40
|
+
let signal = constants.signals.SIGTERM;
|
|
41
|
+
let at = 0;
|
|
42
|
+
if (["-s", "--signal"].includes(args[at])) {
|
|
43
|
+
if (!args[at + 1]) refuse("missing signal");
|
|
44
|
+
signal = signalValue(args[at + 1]); at += 2;
|
|
45
|
+
} else if (/^-(?:\d+|(?:SIG)?[A-Z]+)$/i.test(args[at] || "") && !/^-[fix]+$/.test(args[at])) {
|
|
46
|
+
signal = signalValue(args[at].slice(1)); at++;
|
|
47
|
+
}
|
|
48
|
+
if (command === "kill") {
|
|
49
|
+
if (args[at] === "--") at++;
|
|
50
|
+
const pids = args.slice(at);
|
|
51
|
+
if (!pids.length || pids.some(p => !/^[1-9]\d*$/.test(p) || !Number.isSafeInteger(Number(p)))) {
|
|
52
|
+
refuse("kill requires positive PIDs; process groups and job specs are refused");
|
|
53
|
+
}
|
|
54
|
+
return { signal, pids: pids.map(Number) };
|
|
55
|
+
}
|
|
56
|
+
if (command === "pkill") {
|
|
57
|
+
const flags = [];
|
|
58
|
+
while (/^-[fix]+$/.test(args[at] || "")) flags.push(args[at++]);
|
|
59
|
+
if (args.length - at !== 1 || args[at].startsWith("-")) {
|
|
60
|
+
refuse("pkill requires options before one pattern; ambiguous or unsupported arguments");
|
|
61
|
+
}
|
|
62
|
+
// pgrep resolves native regex semantics without ever invoking the signalling binary.
|
|
63
|
+
const result = spawnSync("/usr/bin/pgrep", [...flags, args[at]], { encoding: "utf8", timeout: 5000 });
|
|
64
|
+
if (result.error || ![0, 1].includes(result.status)) refuse("cannot resolve pkill pattern");
|
|
65
|
+
return { signal, pids: result.stdout.trim().split(/\s+/).filter(Boolean).map(Number) };
|
|
66
|
+
}
|
|
67
|
+
if (command === "killall") {
|
|
68
|
+
const names = args.slice(at);
|
|
69
|
+
if (!names.length || names.some(name => name.startsWith("-") || name.includes("/"))) {
|
|
70
|
+
refuse("killall requires exact process names; unsupported arguments");
|
|
71
|
+
}
|
|
72
|
+
return { signal, pids: [...rows].filter(([, row]) => names.includes(row.name)).map(([pid]) => pid) };
|
|
73
|
+
}
|
|
74
|
+
return refuse("unknown signal command");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const root = Number(process.env.TRANTOR_SIGNAL_ROOT);
|
|
79
|
+
if (!Number.isSafeInteger(root) || root <= 1) refuse("missing seat process root");
|
|
80
|
+
const rows = processTable();
|
|
81
|
+
if (!descendant(process.pid, root, rows)) refuse("guard is outside the seat process tree");
|
|
82
|
+
const selected = targets(rows);
|
|
83
|
+
const pids = [...new Set(selected.pids)].filter(pid => pid !== process.pid);
|
|
84
|
+
if (!pids.length) process.exit(1);
|
|
85
|
+
for (const pid of pids) {
|
|
86
|
+
if (!descendant(pid, root, rows)) refuse(`PID ${pid} is outside the seat process tree`);
|
|
87
|
+
}
|
|
88
|
+
// Recheck the whole set before sending anything; never partially apply a broad match.
|
|
89
|
+
const current = processTable();
|
|
90
|
+
for (const pid of pids) {
|
|
91
|
+
if (!descendant(pid, root, current)) refuse(`PID ${pid} left the seat process tree`);
|
|
92
|
+
}
|
|
93
|
+
for (const pid of pids) process.kill(pid, selected.signal);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`seat signal guard: refused ${command}: ${error.message}`);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -405,6 +405,11 @@ try {
|
|
|
405
405
|
// A RESUMED session must never claim a handoff: it already carries its history, so the takeover banner
|
|
406
406
|
// would land in the same maxed-out context and look right while the window never reset.
|
|
407
407
|
const isResume = source === "resume";
|
|
408
|
+
// A print-mode one-shot (`claude -p`) is never the successor either (#11577): SessionStart fires
|
|
409
|
+
// for it with source "startup" — the hook INPUT is indistinguishable from a human start — but
|
|
410
|
+
// the CLI marks the hook ENV (CLAUDE_CODE_ENTRYPOINT=sdk-cli; interactive "cli", SDKs sdk-*).
|
|
411
|
+
// Unguarded, it claimed the waiting handoff and superseded the live orchestrator.
|
|
412
|
+
const isPrintRun = String(process.env.CLAUDE_CODE_ENTRYPOINT || "cli").startsWith("sdk-");
|
|
408
413
|
const orchEnv = process.env.TRANTOR_ORCH || "";
|
|
409
414
|
const isOrchPane = !!orchEnv && (orchEnv === "1" || orchEnv === project); // project-matched: a child claude in another dir must not inherit the badge
|
|
410
415
|
const orchSid = readOrchSession(project);
|
|
@@ -413,19 +418,24 @@ try {
|
|
|
413
418
|
const orchOrigin = !!(peek && orchSid && peek.session_id && peek.session_id === orchSid);
|
|
414
419
|
const ageMs = peek ? Date.now() - (Number(peek.stamp) || 0) * 1000 : 0;
|
|
415
420
|
const held = orchOrigin && !isOrchPane && !isCompact && ageMs < holdMs;
|
|
416
|
-
const handoff = !peek ? null
|
|
421
|
+
const handoff = !peek || isPrintRun ? null
|
|
417
422
|
: (isCompact || held || isResume) ? peek
|
|
418
423
|
: loadPendingHandoff(basename(projectDir), {
|
|
419
424
|
claim: true,
|
|
420
425
|
freshSession: { session_id: stdinObj.session_id || "", transcript_path: stdinObj.transcript_path || "" },
|
|
421
426
|
});
|
|
422
427
|
const claimed = !!handoff && !isCompact && !held && !isResume;
|
|
428
|
+
// The handoff banner never rides a one-shot either: it is not the successor and nobody asked it
|
|
429
|
+
// to recap, so a print start with a pending handoff gets one quiet stderr line and nothing else.
|
|
430
|
+
if (isPrintRun && peek && !isCompact) {
|
|
431
|
+
process.stderr.write(`[trantor] pending handoff ${peek.id} NOT claimed — print-mode one-shot (claude -p) is never the successor\n`);
|
|
432
|
+
}
|
|
423
433
|
// Follow the thread: claiming the orchestrator's baton makes THIS session the orchestrator
|
|
424
434
|
// thread, so the map `trantor open` resumes and the app's chat reads moves with it. The pane
|
|
425
435
|
// also records itself on every fresh start — that keeps the map honest even if a future
|
|
426
436
|
// claude forks the session id on resume.
|
|
427
437
|
if (claimed && orchOrigin && stdinObj.session_id) writeOrchSession(project, String(stdinObj.session_id), "sessionstart-claim");
|
|
428
|
-
if (isOrchPane && !isCompact && stdinObj.session_id) writeOrchSession(project, String(stdinObj.session_id), "orch-pane-start");
|
|
438
|
+
if (isOrchPane && !isCompact && !isPrintRun && stdinObj.session_id) writeOrchSession(project, String(stdinObj.session_id), "orch-pane-start");
|
|
429
439
|
if (handoff && held) {
|
|
430
440
|
process.stderr.write(`[trantor] pending handoff ${handoff.id} HELD for the orch pane (${Math.round(ageMs / 60000)}m old)\n`);
|
|
431
441
|
const mins = Math.max(1, Math.round((holdMs - ageMs) / 60000));
|
package/lib/project.mjs
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
// trantor — canonical project identity (client side): one repo = one lane, keyed by the id recorded
|
|
2
2
|
// in the checkout (.trantor/project.json) or, unmarked, by the git repo root basename; an explicit
|
|
3
3
|
// RELAY_PROJECT always wins and the hub folds aliases on top.
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
4
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
5
5
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync, realpathSync } from "node:fs";
|
|
6
6
|
import { basename, join, dirname, resolve, sep } from "node:path";
|
|
7
7
|
import { homedir, hostname } from "node:os";
|
|
8
8
|
import { asRecord, asString } from "./decode.mjs";
|
|
9
9
|
|
|
10
10
|
export function gitRoot(dir) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// A loaded runner can miss the short deadline; retry timeouts, not non-repository errors (#11580).
|
|
12
|
+
for (const timeout of [2000, 10000]) {
|
|
13
|
+
try {
|
|
14
|
+
return execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"],
|
|
15
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.code !== "ETIMEDOUT") return "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
// A BUS SEAT WORKTREE is named for its project BY CONSTRUCTION — the bus creates
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const quote = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
6
|
+
|
|
7
|
+
// Install after env files are sourced so a provider's PATH cannot hide the guard.
|
|
8
|
+
export function seatSignalPrefix(workdir, env = process.env, root = process.pid) {
|
|
9
|
+
const parent = join(workdir, ".agent-bus-out", "signal-guard");
|
|
10
|
+
const dir = join(parent, String(root));
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
// Runtime shims must not count as seat work or enter a cut snapshot in arbitrary repositories.
|
|
13
|
+
writeFileSync(join(parent, ".gitignore"), "*\n");
|
|
14
|
+
const guard = fileURLToPath(new URL("../bin/seat-signal.mjs", import.meta.url));
|
|
15
|
+
for (const name of ["kill", "pkill", "killall"]) {
|
|
16
|
+
writeFileSync(join(dir, name), `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(guard)} ${name} "$@"\n`, { mode: 0o755 });
|
|
17
|
+
}
|
|
18
|
+
const path = `export PATH=${quote(dir)}:"$PATH"\n`;
|
|
19
|
+
const bash = join(dir, "bash-env");
|
|
20
|
+
const previousBash = env.BASH_ENV && env.BASH_ENV !== bash
|
|
21
|
+
? `[ ! -f ${quote(env.BASH_ENV)} ] || . ${quote(env.BASH_ENV)}\n` : "";
|
|
22
|
+
writeFileSync(bash, `${previousBash}${path}enable -n kill\n`);
|
|
23
|
+
const previousZdot = env.ZDOTDIR || env.HOME;
|
|
24
|
+
for (const name of [".zshenv", ".zprofile", ".zshrc", ".zlogin"]) {
|
|
25
|
+
const original = previousZdot && previousZdot !== dir ? join(previousZdot, name) : "";
|
|
26
|
+
const source = original ? `[ ! -f ${quote(original)} ] || . ${quote(original)}\n` : "";
|
|
27
|
+
writeFileSync(join(dir, name), `${source}${path}disable kill\n`);
|
|
28
|
+
}
|
|
29
|
+
return `export TRANTOR_SIGNAL_ROOT=${quote(root)} BASH_ENV=${quote(bash)} ZDOTDIR=${quote(dir)}\n${path}enable -n kill\n`;
|
|
30
|
+
}
|
package/mcp.mjs
CHANGED
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
// via the agent's MCP config. Identity + hub URL come from env (RELAY_SESSION, RELAY_URL).
|
|
5
5
|
// Loading this server AUTO-REGISTERS the session — so presence works on every agent.
|
|
6
6
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
7
|
-
import { join, basename
|
|
7
|
+
import { join, basename } from "node:path";
|
|
8
8
|
import { homedir, hostname } from "node:os";
|
|
9
9
|
import { execSync, spawnSync } from "node:child_process";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { createRequire } from "node:module";
|
|
12
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
11
|
import { advise, loadWorld } from "./bin/advise.mjs";
|
|
14
12
|
import { loadSeatRecord } from "./lib/seat-record.mjs";
|
|
15
13
|
import { resolveProject, hostId, resolveHubInfo, nonSeatReason, handoffDir, orchWriterSid } from "./lib/project.mjs";
|
|
@@ -20,43 +18,7 @@ import { assertNoSecrets } from "./lib/scrub.mjs";
|
|
|
20
18
|
import { isMessageCardTitle, OPEN_CARD_STATUSES } from "./lib/turn-policy.mjs";
|
|
21
19
|
import { markDoing, hollowVerdict, blastRadius, blastLine } from "./hooks/lib/hollow-move.mjs";
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
// GitHub-sourced plugin ships no node_modules and a static SDK import dies with
|
|
25
|
-
// ERR_MODULE_NOT_FOUND before any line runs — silently: every relay tool just disappears.
|
|
26
|
-
// Resolve ourselves: plain import(spec) first; on NOT_FOUND borrow the global trantor's tree.
|
|
27
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
const req = createRequire(import.meta.url);
|
|
29
|
-
|
|
30
|
-
let fallbackRoots = null;
|
|
31
|
-
function borrowRoots() {
|
|
32
|
-
if (fallbackRoots) return fallbackRoots;
|
|
33
|
-
const roots = [];
|
|
34
|
-
const add = (p) => { if (p && !roots.includes(p)) roots.push(p); };
|
|
35
|
-
// Cheap guesses first — every one of these is a string join, no process spawn.
|
|
36
|
-
if (process.env.npm_config_prefix) add(join(process.env.npm_config_prefix, "lib", "node_modules"));
|
|
37
|
-
add(join(dirname(process.execPath), "..", "lib", "node_modules")); // homebrew, nvm, volta, asdf
|
|
38
|
-
// Only shell out if the guesses missed — `npm root -g` costs ~0.5s of MCP startup.
|
|
39
|
-
if (!roots.some((r) => existsSync(join(r, "trantor")))) {
|
|
40
|
-
try { add(execSync("npm root -g", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()); } catch {}
|
|
41
|
-
}
|
|
42
|
-
fallbackRoots = roots.flatMap((r) => [join(r, "trantor"), r]);
|
|
43
|
-
return fallbackRoots;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function dep(spec) {
|
|
47
|
-
try {
|
|
48
|
-
return await import(spec);
|
|
49
|
-
} catch (err) {
|
|
50
|
-
if (err?.code !== "ERR_MODULE_NOT_FOUND" && err?.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") throw err;
|
|
51
|
-
}
|
|
52
|
-
for (const path of [HERE, ...borrowRoots()]) {
|
|
53
|
-
try { return await import(pathToFileURL(req.resolve(spec, { paths: [path] })).href); } catch {}
|
|
54
|
-
}
|
|
55
|
-
throw new Error(
|
|
56
|
-
`[trantor-mcp] cannot resolve '${spec}'. This plugin snapshot has no node_modules and no global ` +
|
|
57
|
-
`trantor install was found to borrow from. Fix: npm i -g trantor (or: cd ${HERE} && npm install --omit=dev)`,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
21
|
+
import { dep } from "./lib/dep.mjs";
|
|
60
22
|
|
|
61
23
|
const { McpServer } = await dep("@modelcontextprotocol/sdk/server/mcp.js");
|
|
62
24
|
const { StdioServerTransport } = await dep("@modelcontextprotocol/sdk/server/stdio.js");
|