aiterm-mcp 0.29.3 → 0.29.6

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/dist/core.js CHANGED
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import { createHash, randomBytes } from "node:crypto";
14
14
  import * as rtk from "./rtk.js";
15
15
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
16
- import { isWin, SOCKDIR, tmuxCommand, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
16
+ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
17
17
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
18
18
  import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
19
19
  import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexLaunchBlockingDialog, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
@@ -75,7 +75,7 @@ const MAX_SEND_BYTES = 64 * 1024;
75
75
  const PTY_PASTE_CHUNK_BYTES = 256;
76
76
  const PTY_PASTE_CHUNK_PAUSE_MS = 10;
77
77
  const PTY_PASTE_PAUSE_BUFFER = new Int32Array(new SharedArrayBuffer(4));
78
- const SESSION_SEND_LOCK_WAIT_MS = 10_000;
78
+ const SESSION_SEND_LOCK_WAIT_MS = isWin ? 130_000 : 10_000;
79
79
  const SESSION_SEND_LOCK_POLL_MS = 25;
80
80
  // 安全: send 前に弾く破壊的コマンド(外部システム境界の防御)
81
81
  const DESTRUCTIVE = [
@@ -121,6 +121,15 @@ function paneCurrentCommand(name) {
121
121
  return "";
122
122
  return normalizePaneCommand(r.stdout.trim());
123
123
  }
124
+ function paneCurrentCommandForMark(name) {
125
+ const foreground = paneCurrentCommand(name);
126
+ if (!isWin || foreground === "powershell" || foreground === "pwsh")
127
+ return foreground;
128
+ // psmuxはnew-session直後だけpane_current_commandへ起動元shellを返すことがある。
129
+ // 現在の末尾画面にPowerShell promptが描画済みなら、mark構文はその実効shellへ合わせる。
130
+ const screen = captureScreen(name, 4).replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "");
131
+ return /(?:^|\n)PS [^\r\n]*>\s*$/m.test(screen) ? "pwsh" : foreground;
132
+ }
124
133
  function pasteBufferSupportsNoSanitizeFlag() {
125
134
  const listed = tmux("list-commands");
126
135
  if (listed.code !== 0) {
@@ -658,7 +667,7 @@ export function send(name, text, o = {}) {
658
667
  if (o.mark) {
659
668
  // mark の sentinel は POSIX シェル構文。前面が fish/csh/tcsh 等の非 POSIX 対話シェルだと "$?" が
660
669
  // 壊れて sentinel が成立しない。黙って壊れた完了検出を作らず、明示エラーで until を促す(B8)。
661
- fg = paneCurrentCommand(name);
670
+ fg = paneCurrentCommandForMark(name);
662
671
  if (NON_POSIX_MARK_SHELLS.has(fg)) {
663
672
  throw new AitermError(`mark は POSIX シェル(bash/sh/zsh/dash)前提です。前面が ${fg} のため sentinel の "$?" が` +
664
673
  `正しく展開されません。until で完了パターンを指定してください。`, 2);
@@ -702,42 +711,44 @@ export function send(name, text, o = {}) {
702
711
  /* noop */
703
712
  }
704
713
  }
705
- // `send-keys -l`と単発`paste-buffer`は、長文をPTY入力queueへ一度に流すと、OSを問わず
706
- // 途中以降が欠落しても tmux 自体は code=0 を返す。UTF-8を壊さない256byte以下に分け、
707
- // 全chunk+Enterをsession単位のcross-process lock内で直列化する。
708
- const pasteSupportsNoSanitize = pasteBufferSupportsNoSanitizeFlag();
709
- const chunks = splitPtyText(text);
710
- const bufferBase = `aiterm-${process.pid}-${randomBytes(8).toString("hex")}`;
711
- for (let i = 0; i < chunks.length; i += 1) {
712
- const bufferName = `${bufferBase}-${i}`;
713
- const partial = i > 0
714
- ? " 先行chunkはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。"
715
- : "";
716
- const loaded = loadPtyBufferChunk(true, bufferName, chunks[i]);
717
- if (loaded.code !== 0) {
718
- tmuxCleanup("delete-buffer", "-b", bufferName);
719
- throw new AitermError(`tmux bufferへの送信準備に失敗しました` +
720
- `(chunk ${i + 1}/${chunks.length}): ${loaded.stderr.trim() || `code=${loaded.code}`}.${partial}`, 2);
721
- }
722
- // -Sは対応新版だけでvis(3)制御文字変換を無効化する。
723
- // send 自身の raw/sanitize 契約だけを真実とし、tmux 側で黙って再変換させない。
724
- const pasteArgs = pasteBufferBaseArgs();
725
- if (o.bracketedPaste)
726
- pasteArgs.push("-p");
727
- if (pasteSupportsNoSanitize)
728
- pasteArgs.push("-S");
729
- pasteArgs.push("-b", bufferName, "-t", name);
730
- const pasted = tmux(...pasteArgs);
731
- if (pasted.code !== 0) {
732
- // paste 失敗時は -d で消えないbufferを明示的に掃除する。元エラーを優先する。
733
- tmuxCleanup("delete-buffer", "-b", bufferName);
734
- throw new AitermError(`tmux bufferのPTY送信に失敗しました` +
735
- `(chunk ${i + 1}/${chunks.length}): ${pasted.stderr.trim() || `code=${pasted.code}`}.${partial}`, 2);
714
+ // POSIX tmuxはUTF-8安全な256byte pasteへ分割する。Windows psmuxは認証済み
715
+ // server commandでpayload全体を単一SendBytesへ変換し、別processとの交差を構造的に防ぐ。
716
+ if (isWin) {
717
+ const sent = sendPsmuxPayload(true, name, text, !!o.bracketedPaste);
718
+ if (sent.code !== 0) {
719
+ throw new AitermError(`psmuxのPTY送信に失敗しました: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
736
720
  }
737
- if (i + 1 < chunks.length) {
738
- // tmux clientの終了だけでは、serverが前chunkをPTYへdrainしたことを保証しない。
739
- // 次chunkまで短く間を空け、外部PTY入力queueの飽和による黙った末尾欠落を防ぐ。
740
- Atomics.wait(PTY_PASTE_PAUSE_BUFFER, 0, 0, PTY_PASTE_CHUNK_PAUSE_MS);
721
+ }
722
+ else {
723
+ const chunks = splitPtyText(text);
724
+ const pasteSupportsNoSanitize = pasteBufferSupportsNoSanitizeFlag();
725
+ const bufferBase = `aiterm-${process.pid}-${randomBytes(8).toString("hex")}`;
726
+ for (let i = 0; i < chunks.length; i += 1) {
727
+ const bufferName = `${bufferBase}-${i}`;
728
+ const partial = i > 0
729
+ ? " 先行chunkはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。"
730
+ : "";
731
+ const loaded = loadPtyBufferChunk(true, bufferName, chunks[i]);
732
+ if (loaded.code !== 0) {
733
+ tmuxCleanup("delete-buffer", "-b", bufferName);
734
+ throw new AitermError(`tmux bufferへの送信準備に失敗しました` +
735
+ `(chunk ${i + 1}/${chunks.length}): ${loaded.stderr.trim() || `code=${loaded.code}`}.${partial}`, 2);
736
+ }
737
+ const pasteArgs = pasteBufferBaseArgs();
738
+ if (o.bracketedPaste)
739
+ pasteArgs.push("-p");
740
+ if (pasteSupportsNoSanitize)
741
+ pasteArgs.push("-S");
742
+ pasteArgs.push("-b", bufferName, "-t", name);
743
+ const pasted = tmux(...pasteArgs);
744
+ if (pasted.code !== 0) {
745
+ tmuxCleanup("delete-buffer", "-b", bufferName);
746
+ throw new AitermError(`tmux bufferのPTY送信に失敗しました` +
747
+ `(chunk ${i + 1}/${chunks.length}): ${pasted.stderr.trim() || `code=${pasted.code}`}.${partial}`, 2);
748
+ }
749
+ if (i + 1 < chunks.length) {
750
+ Atomics.wait(PTY_PASTE_PAUSE_BUFFER, 0, 0, PTY_PASTE_CHUNK_PAUSE_MS);
751
+ }
741
752
  }
742
753
  }
743
754
  if (enter) {
@@ -0,0 +1,99 @@
1
+ import * as fs from "node:fs";
2
+ import * as net from "node:net";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ const MAX_PAYLOAD_BYTES = 64 * 1024 + 12;
6
+ const PTY_CHUNK_BYTES = 256;
7
+ const MAX_FINAL_DRAIN_MS = 3_000;
8
+ const SESSION_BASE_RE = /^[A-Za-z0-9_-]{1,160}$/;
9
+ const KEY_RE = /^[0-9a-f]{16}$/i;
10
+ async function readPayload() {
11
+ const chunks = [];
12
+ let total = 0;
13
+ for await (const chunk of process.stdin) {
14
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
15
+ total += bytes.length;
16
+ if (total > MAX_PAYLOAD_BYTES)
17
+ throw new Error(`payload exceeds ${MAX_PAYLOAD_BYTES} bytes`);
18
+ chunks.push(bytes);
19
+ }
20
+ return Buffer.concat(chunks);
21
+ }
22
+ function psmuxDataDir() {
23
+ const override = process.env.PSMUX_DATA_DIR;
24
+ if (override) {
25
+ if (!path.isAbsolute(override))
26
+ throw new Error("PSMUX_DATA_DIR must be absolute");
27
+ return override;
28
+ }
29
+ return path.join(process.env.USERPROFILE || os.homedir(), ".psmux");
30
+ }
31
+ async function sendCommand(port, key, command, expectedResponse) {
32
+ await new Promise((resolve, reject) => {
33
+ const socket = net.createConnection({ host: "127.0.0.1", port });
34
+ let response = "";
35
+ let settled = false;
36
+ const finish = (error) => {
37
+ if (settled)
38
+ return;
39
+ settled = true;
40
+ socket.destroy();
41
+ if (error)
42
+ reject(error);
43
+ else
44
+ resolve();
45
+ };
46
+ socket.setTimeout(2_000, () => finish(new Error("psmux command timed out")));
47
+ socket.on("connect", () => socket.end(`AUTH ${key}\n${command}\n`));
48
+ socket.on("data", (chunk) => {
49
+ response += chunk.toString("utf8");
50
+ });
51
+ socket.on("end", () => {
52
+ if (response.split(/\r?\n/).includes("OK") &&
53
+ (expectedResponse == null || response.includes(expectedResponse)))
54
+ finish();
55
+ else
56
+ finish(new Error(`psmux authentication failed: ${response.trim() || "empty response"}`));
57
+ });
58
+ socket.on("error", (error) => finish(error));
59
+ });
60
+ }
61
+ function delay(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+ async function send() {
65
+ const sessionBase = process.argv[2] ?? "";
66
+ if (!SESSION_BASE_RE.test(sessionBase))
67
+ throw new Error("invalid psmux session base");
68
+ const payload = await readPayload();
69
+ const dir = psmuxDataDir();
70
+ const portText = fs.readFileSync(path.join(dir, `${sessionBase}.port`), "utf8").trim();
71
+ const key = fs.readFileSync(path.join(dir, `${sessionBase}.key`), "utf8").trim();
72
+ const port = Number(portText);
73
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
74
+ throw new Error("invalid psmux port");
75
+ if (!KEY_RE.test(key))
76
+ throw new Error("invalid psmux auth key");
77
+ const subcommands = [];
78
+ for (let offset = 0; offset < payload.length; offset += PTY_CHUNK_BYTES) {
79
+ const chunk = payload.subarray(offset, offset + PTY_CHUNK_BYTES);
80
+ const hex = Array.from(chunk, (byte) => byte.toString(16).padStart(2, "0")).join(" ");
81
+ subcommands.push(`send -H ${hex}`);
82
+ }
83
+ const chunkDrainMs = payload.length > 1024 ? 300 : 100;
84
+ for (const command of subcommands) {
85
+ await sendCommand(port, key, command);
86
+ await delay(chunkDrainMs);
87
+ }
88
+ const completionMarker = `AITERM_PSMUX_SEND_DONE_${process.pid}`;
89
+ await sendCommand(port, key, `display-message -p ${completionMarker}`, completionMarker);
90
+ const finalDrainMs = Math.min(MAX_FINAL_DRAIN_MS, Math.max(200, Math.ceil(payload.length / 1024) * 500));
91
+ await delay(finalDrainMs);
92
+ if (process.env.AITERM_PSMUX_DEBUG === "1") {
93
+ process.stderr.write(`psmux-send-worker debug: chunks=${subcommands.length} sent=${subcommands.length}\n`);
94
+ }
95
+ }
96
+ send().catch((error) => {
97
+ process.stderr.write(`psmux-send-worker: ${error instanceof Error ? error.message : String(error)}\n`);
98
+ process.exitCode = 1;
99
+ });
@@ -11,7 +11,12 @@ const STORE_SCHEMA = "aiterm-mcp.runtime-errors.v1";
11
11
  const STATE_SCHEMA = "1.0";
12
12
  const MAX_CONFIG_BYTES = 16 * 1024;
13
13
  const MAX_STORE_BYTES = 1024 * 1024;
14
- const WORKER_TIMEOUT_MS = 2_000;
14
+ const POSIX_WORKER_TIMEOUT_MS = 2_000;
15
+ // Windows はprivate DACLの適用とreadbackをPowerShell 7境界で行う。診断は最大2回、
16
+ // 記録はlock queueとstate更新で複数回この境界を通るため、process起動だけを想定した
17
+ // POSIXの2秒上限では正常処理まで強制終了する。各境界自身の5秒上限は維持し、worker全体だけ分ける。
18
+ const WINDOWS_DIAGNOSTIC_WORKER_TIMEOUT_MS = 12_000;
19
+ const WINDOWS_RECORD_WORKER_TIMEOUT_MS = 30_000;
15
20
  export const RUNTIME_ERROR_DEFINITIONS = Object.freeze({
16
21
  "AITERM.PTY_DEPENDENCY_UNAVAILABLE": Object.freeze({
17
22
  component: "pty-dependency", message_template: "PTY dependency is unavailable", severity: "high",
@@ -635,6 +640,11 @@ export class RuntimeErrorStore {
635
640
  }
636
641
  const WORKER = fileURLToPath(new URL("./runtime-error-worker.js", import.meta.url));
637
642
  function fixedStoreFailure(stderr) { stderr("aiterm: runtime error store unavailable\n"); }
643
+ function defaultWorkerTimeoutMs(action) {
644
+ if (process.platform !== "win32")
645
+ return POSIX_WORKER_TIMEOUT_MS;
646
+ return action === "record" ? WINDOWS_RECORD_WORKER_TIMEOUT_MS : WINDOWS_DIAGNOSTIC_WORKER_TIMEOUT_MS;
647
+ }
638
648
  export function recordRuntimeError(code, options = {}) {
639
649
  validateRuntimeObservation({ code });
640
650
  const stderr = options.stderr ?? ((line) => process.stderr.write(line));
@@ -647,7 +657,7 @@ export function recordRuntimeError(code, options = {}) {
647
657
  const child = spawn(process.execPath, [options.workerPath ?? WORKER, "record", code], {
648
658
  stdio: "ignore", windowsHide: true, env: process.env,
649
659
  });
650
- const timer = setTimeout(() => { report(); forceKill(child); }, options.timeoutMs ?? WORKER_TIMEOUT_MS);
660
+ const timer = setTimeout(() => { report(); forceKill(child); }, options.timeoutMs ?? defaultWorkerTimeoutMs("record"));
651
661
  timer.unref();
652
662
  child.once("error", report);
653
663
  child.once("exit", (exitCode, signal) => { clearTimeout(timer); if (exitCode !== 0 || signal)
@@ -686,7 +696,7 @@ export async function runtimeErrorStoreDiagnostic(options = {}) {
686
696
  finish(fallback);
687
697
  return;
688
698
  }
689
- const timer = setTimeout(() => { forceKill(child); finish(fallback); }, options.timeoutMs ?? WORKER_TIMEOUT_MS);
699
+ const timer = setTimeout(() => { forceKill(child); finish(fallback); }, options.timeoutMs ?? defaultWorkerTimeoutMs("diagnostic"));
690
700
  child.stdout.setEncoding("utf8");
691
701
  child.stdout.on("data", (chunk) => { if (stdout.length <= 4096)
692
702
  stdout += chunk; });
@@ -6,6 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import * as os from "node:os";
8
8
  import { createHash } from "node:crypto";
9
+ import { fileURLToPath } from "node:url";
9
10
  import { AitermError, ptyDependencyError } from "./errors.js";
10
11
  // Windows ネイティブには tmux が無いため、tmux CLI 互換の native psmux を叩く
11
12
  // (POSIX = Linux/WSL2/macOS は従来どおり tmux を直接叩く。WSL 橋は e3f5fc8 で全廃)。
@@ -129,6 +130,28 @@ export function tmuxCommandWithInput(observe, input, ...args) {
129
130
  export function tmuxCommand(observe, ...args) {
130
131
  return tmuxCommandWithInput(observe, undefined, ...args);
131
132
  }
133
+ export function sendPsmuxPayload(observe, sessionName, text, bracketedPaste) {
134
+ if (!isWin)
135
+ return { code: 1, stdout: "", stderr: "psmux direct send is Windows-only" };
136
+ ensureWinPsmux(observe);
137
+ const worker = fileURLToPath(new URL("./psmux-send-worker.js", import.meta.url));
138
+ const body = bracketedPaste ? `\x1b[200~${text}\x1b[201~` : text;
139
+ const result = spawnSync(process.execPath, [worker, `${WIN_NS}__${sessionName}`], {
140
+ encoding: "utf8",
141
+ maxBuffer: 1024 * 1024,
142
+ input: Buffer.from(body, "utf8"),
143
+ env: tmuxSpawnEnv(),
144
+ timeout: 120_000,
145
+ });
146
+ if (result.error) {
147
+ return {
148
+ code: 1,
149
+ stdout: result.stdout ?? "",
150
+ stderr: `${result.error.code ?? "worker error"}: ${result.error.message}`,
151
+ };
152
+ }
153
+ return { code: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
154
+ }
132
155
  // psmux の load-buffer は stdin (`-`) 非対応で path 位置引数だけを取る。
133
156
  // Windows は owner-only の SOCKDIR 内へ一時ファイル経由で渡し、POSIX は stdin で渡す。
134
157
  export function loadPtyBufferChunk(observe, bufferName, chunk) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.29.3",
3
+ "version": "0.29.6",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [