aiterm-mcp 0.28.3 → 0.29.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.ja.md CHANGED
@@ -459,7 +459,7 @@ handoff contextを前置きできる。この任意経路は`throughline >= 0.9.
459
459
  - **Node.js >= 18**
460
460
  - **tmux または psmux**(実行時の前提)
461
461
  - **macOS / Linux / WSL2** は tmux を直接使う。macOS は同梱されないので `brew install tmux` で導入する。MCP クライアントがターミナルでなく **GUI から起動**された場合、Homebrew の bin(Apple Silicon: `/opt/homebrew/bin`、Intel: `/usr/local/bin`)が `PATH` に入らないことがある。その場合 aiterm が自動で探索するか、**`AITERM_TMUX=/path/to/tmux`** で明示指定する。
462
- - **Windows ネイティブ**は WSL を使わず、tmux CLI互換の [psmux](https://github.com/psmux/psmux) **3.3.8以上**を直接使う(`winget install marlocarlo.psmux`)。pane shell用に Git for Windows も必要。解決先は **`AITERM_PSMUX`**/**`AITERM_BASH`** で上書きできる。Windows toolはSSHと同じく入れ子で握れ、`pty_send "powershell.exe"`でPowerShellへ入れる。
462
+ - **Windows ネイティブ**は WSL を使わず、tmux CLI互換の [psmux](https://github.com/psmux/psmux) **3.3.8以上**をAitermのterminal/session multiplexer backendとして使う(`winget install marlocarlo.psmux`)。**psmuxはshellではない。** `pty_open`の既定shellはPowerShell 7(`pwsh.exe`)で、Windows PowerShell 5.1・PowerShell 6・`cmd.exe`へfallbackしない。5.1しかなければMicrosoft公式installer/package managerで7を導入してから使う。Git for Windowsはharness launcherが内部で明示するBash shell用に引き続き必要。解決先は **`AITERM_PSMUX`**/**`AITERM_BASH`** で上書きできる。他製品はpsmuxへ直接依存せず、永続端末をAiterm公開APIから利用する。
463
463
  - **agent harness**を使う場合: 対応CLIを製品所有者の公式経路で導入・認証する。CursorはmacOS/Linux/WSLで`curl https://cursor.com/install -fsS | bash`、Windows nativeで`irm 'https://cursor.com/install?win32=true' | iex`を使い、`agent login`で認証、`agent update`で更新する。Aitermは`cursor-agent`を起動する。portable forkだけは追加で`throughline >= 0.9.0`が必要。
464
464
  - 任意: [`rtk`](https://github.com/rtk-ai/rtk) バイナリ(`pty_send` の `rtk: true` 委譲で使う。無くても動く)
465
465
 
package/README.md CHANGED
@@ -501,7 +501,7 @@ Sessions live on a shared tmux socket on POSIX or a shared psmux namespace on na
501
501
  - **Node.js >= 18**
502
502
  - **tmux or psmux** (platform runtime prerequisite)
503
503
  - **macOS / Linux / WSL2** run tmux directly. On macOS install it with `brew install tmux` (stock macOS ships none). If your MCP client is launched from the **GUI** rather than a terminal, Homebrew's bin (`/opt/homebrew/bin` on Apple Silicon, `/usr/local/bin` on Intel) may be off its `PATH`; aiterm auto-searches those locations, or set **`AITERM_TMUX=/path/to/tmux`** to point at it explicitly.
504
- - **Native Windows** has no tmux, so aiterm drives [psmux](https://github.com/psmux/psmux) — a tmux-CLI-compatible native multiplexer — with a per-install `-L` namespace. **No WSL is required.** Install psmux **3.3.8 or newer** (`winget install marlocarlo.psmux`; 3.3.8 is the first release whose `pipe-pane` file sink, byte-exact `paste-buffer` wire, and foreground `#{pane_current_command}` behave the way aiterm's capture/dispatch paths rely on), plus [Git for Windows](https://gitforwindows.org/) whose `bash.exe` becomes the pane shell (System32's `bash.exe` is the WSL launcher and is deliberately not used). Override resolution with **`AITERM_PSMUX`** / **`AITERM_BASH`** when the binaries live elsewhere. You reach Windows tools the same way you reach SSH: `pty_send "powershell.exe …"` nests into PowerShell. The `grok-cli` harness and its deprecated aliases launch the **Windows-native** Grok CLI (`%USERPROFILE%\.grok\bin\grok.exe`, or `GROK_BIN` pointing at a `.exe`) as a Windows process, and a WSL-side grok is rejected before a session is created so product auth and session records never split across an OS boundary.
504
+ - **Native Windows** has no tmux, so aiterm drives [psmux](https://github.com/psmux/psmux) — a tmux-CLI-compatible native terminal/session multiplexer — with a per-install `-L` namespace. **psmux is not a shell.** `pty_open` defaults to PowerShell 7 (`pwsh.exe`) and never falls back to Windows PowerShell 5.1, PowerShell 6, or `cmd.exe`; if only 5.1 is installed, use Microsoft's official installer or package manager first. Install psmux **3.3.8 or newer** (`winget install marlocarlo.psmux`; 3.3.8 is the first release whose `pipe-pane` file sink, byte-exact `paste-buffer` wire, and foreground `#{pane_current_command}` behave the way aiterm's capture/dispatch paths rely on). [Git for Windows](https://gitforwindows.org/) remains required for the explicit Bash shell used internally by harness launchers; System32's `bash.exe` is the WSL launcher and is deliberately not used. Override multiplexer/Bash resolution with **`AITERM_PSMUX`** / **`AITERM_BASH`**. Other products consume persistent terminals through Aiterm's public API instead of depending on psmux directly.
505
505
  - For **agent harnesses**: the selected CLI, installed and authenticated through its product owner's official path — `claude`, `codex`, `grok`, or Cursor's `cursor-agent`. Portable fork additionally needs `throughline >= 0.9.0`; ordinary clean launch does not. (Not needed if you only use the PTY tools.)
506
506
  - Optional: the [`rtk`](https://github.com/rtk-ai/rtk) binary (used by `pty_send`'s `rtk: true` delegation; works fine without it)
507
507
 
@@ -5,6 +5,7 @@ import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import * as os from "node:os";
7
7
  import { AitermError } from "./errors.js";
8
+ import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
8
9
  import { isWin } from "./tmux-runtime.js";
9
10
  export function isUsableExecutableFile(candidate) {
10
11
  try {
@@ -84,7 +85,12 @@ export function spawnAgentControlCommand(bin, args, _cwd, options) {
84
85
  return spawnSync(bin, args, options);
85
86
  }
86
87
  export function resolveWinPaneShell(shell) {
87
- if (!isWin || shell !== "bash")
88
+ if (!isWin)
89
+ return shell;
90
+ if (["powershell", "powershell.exe", "pwsh", "pwsh.exe"].includes(path.basename(shell).toLowerCase())) {
91
+ return resolveWindowsPowerShell7();
92
+ }
93
+ if (shell !== "bash")
88
94
  return shell;
89
95
  const fromEnv = process.env.AITERM_BASH;
90
96
  if (fromEnv) {
@@ -166,7 +172,7 @@ export function runThroughlineHandoffContext(bin, sessionId) {
166
172
  if (isWin && /\.(?:cmd|bat)$/i.test(bin)) {
167
173
  const ps1 = path.join(path.dirname(bin), `${path.basename(bin, path.extname(bin))}.ps1`);
168
174
  if (fs.existsSync(ps1)) {
169
- return spawnSync("powershell.exe", [
175
+ return spawnSync(resolveWindowsPowerShell7(), [
170
176
  "-NoLogo",
171
177
  "-NoProfile",
172
178
  "-NonInteractive",
package/dist/core.js CHANGED
@@ -16,7 +16,7 @@ import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./error
16
16
  import { isWin, SOCKDIR, tmuxCommand, 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, grokInitializationComplete, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
19
- import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
19
+ import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexLaunchBlockingDialog, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
20
20
  import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, } from "./harnesses/claude.js";
21
21
  import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_COMPOSER_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
22
22
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
@@ -2508,12 +2508,14 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2508
2508
  setInitialPromptState(meta, "not_sent");
2509
2509
  const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2510
2510
  if (!ready.ready) {
2511
- return {
2512
- text: `initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples} harness=${agentHarness(meta.kind)}\n` +
2513
- `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`,
2514
- event_cursor: null,
2515
- submit_residue: null,
2516
- };
2511
+ // ready失敗は成功形で返さず明示エラーにする(実被弾 2026-08-25: Codexのupdate確認ダイアログで
2512
+ // 未送信のまま成功形receiptが返り、呼び出し側が40分気づけなかった)。sessionは調査/復旧用に残る。
2513
+ const dialog = meta.kind === "codex" ? codexLaunchBlockingDialog(ready.lastScreen) : null;
2514
+ const causeNote = dialog
2515
+ ? `${dialog}が入力を塞いでいます。pty_read(screen:true)で画面を確認し、pty_keyでダイアログに応答してから、pty_sendでpromptを送ってください。`
2516
+ : `pty_read(screen:true)で画面を確認し、入力受付になってからpty_sendでpromptを送ってください。`;
2517
+ throw new AitermError(`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples} harness=${agentHarness(meta.kind)}\n` +
2518
+ `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態にならず、prompt は送信していません。${causeNote}`, 2);
2517
2519
  }
2518
2520
  const startOffset = agentCompletionCursor(meta);
2519
2521
  try {
@@ -381,6 +381,20 @@ export function codexTuiReady(screen) {
381
381
  }
382
382
  // submit座礁観測のcomposer領域マーカー(ready判定と同じ記号を行頭基準で探す)。
383
383
  export const CODEX_COMPOSER_MARKER_RE = /^\s*[›>]/;
384
+ // Codexの起動前modalダイアログ検知。表示中はheader/footerが描かれず codexTuiReady が
385
+ // 恒久falseになるため、ready gate失敗の原因究明用に画面から種別を特定する
386
+ // (実被弾 2026-08-25: update確認で初手prompt未送信のまま30秒timeout→40分停滞)。
387
+ // 文字列は実機captureの逐語。既知2種に一致しない「Press enter to continue」も
388
+ // 同型のmodalとして拾う(種別不明のまま握りつぶさない)。
389
+ export function codexLaunchBlockingDialog(screen) {
390
+ if (screen.includes("Update available!"))
391
+ return "update確認ダイアログ";
392
+ if (screen.includes("Do you trust the contents of this directory"))
393
+ return "directory trust確認ダイアログ";
394
+ if (screen.includes("Press enter to continue"))
395
+ return "起動時ダイアログ(種別未特定)";
396
+ return null;
397
+ }
384
398
  export function codexModelChoice(screen, model) {
385
399
  for (const line of screen.slice(screen.lastIndexOf("Select Model and Effort")).split("\n")) {
386
400
  const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(\S+)/);
package/dist/index.js CHANGED
@@ -84,13 +84,14 @@ server.registerTool("diagnostics", {
84
84
  description: "Factory 向け read-only 診断。安全な状態語彙だけを機械可読 JSON で返す(PTY 内容・認証情報・path・環境値は返さない)。",
85
85
  inputSchema: {},
86
86
  }, async () => ok(await factoryDiagnostics()));
87
+ const DEFAULT_PTY_SHELL = process.platform === "win32" ? "pwsh" : "bash";
87
88
  server.registerTool("pty_open", {
88
89
  description: "ローカル永続端末(tmux セッション)を1個開き、session_id を返す。tmux サーバ常駐ゆえ本サーバや " +
89
90
  "クライアントが再起動してもセッションは生存する。リモート操作は専用ツールにせず、開いた端末の中で " +
90
91
  'pty_send(session_id, "ssh host") と打って入る。',
91
92
  inputSchema: {
92
93
  name: z.string().nullish().describe("セッション名(省略時は t1, t2... を自動採番)"),
93
- shell: z.string().default("bash").describe("起動シェル(既定 bash)"),
94
+ shell: z.string().default(DEFAULT_PTY_SHELL).describe(`起動シェル(既定 ${DEFAULT_PTY_SHELL})`),
94
95
  },
95
96
  }, async ({ name, shell }) => {
96
97
  try {
@@ -5,6 +5,7 @@ import { spawn, spawnSync } from "node:child_process";
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
+ import { resolveWindowsPowerShell7, windowsPowerShell7Command } from "./windows-powershell.js";
8
9
  export function defaultRuntimeErrorPaths(options = {}) {
9
10
  const platform = options.platform ?? process.platform;
10
11
  const home = options.home ?? os.homedir();
@@ -26,7 +27,9 @@ const WINDOWS_DACL_VERIFY_SCRIPT = String.raw `
26
27
  $ErrorActionPreference='Stop'
27
28
  $target=$env:AITERMMCP_ACL_PATH; $kind=$env:AITERMMCP_ACL_KIND
28
29
  $sid=[Security.Principal.WindowsIdentity]::GetCurrent().User
29
- $check=if($kind -eq 'directory'){[IO.Directory]::GetAccessControl($target)}else{[IO.File]::GetAccessControl($target)}
30
+ $item=Get-Item -LiteralPath $target
31
+ $sections=[Security.AccessControl.AccessControlSections]::Access -bor [Security.AccessControl.AccessControlSections]::Owner
32
+ $check=[IO.FileSystemAclExtensions]::GetAccessControl($item,$sections)
30
33
  $ownerSid=$check.GetOwner([Security.Principal.SecurityIdentifier]).Value
31
34
  $rules=@($check.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]))
32
35
  if($ownerSid -ne $sid.Value -or $rules.Count -ne 1 -or $rules[0].IdentityReference.Value -ne $sid.Value -or $rules[0].AccessControlType -ne 'Allow' -or $rules[0].IsInherited -or (($rules[0].FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -ne [Security.AccessControl.FileSystemRights]::FullControl)){exit 9}
@@ -38,18 +41,18 @@ $sid=[Security.Principal.WindowsIdentity]::GetCurrent().User
38
41
  if($kind -eq 'directory'){$acl=New-Object Security.AccessControl.DirectorySecurity;$inherit=[Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'}else{$acl=New-Object Security.AccessControl.FileSecurity;$inherit=[Security.AccessControl.InheritanceFlags]::None}
39
42
  $acl.SetOwner($sid); $acl.SetAccessRuleProtection($true,$false)
40
43
  $rule=New-Object Security.AccessControl.FileSystemAccessRule($sid,[Security.AccessControl.FileSystemRights]::FullControl,$inherit,[Security.AccessControl.PropagationFlags]::None,[Security.AccessControl.AccessControlType]::Allow)
41
- $acl.AddAccessRule($rule); if($kind -eq 'directory'){[IO.Directory]::SetAccessControl($target,$acl)}else{[IO.File]::SetAccessControl($target,$acl)}
44
+ $acl.AddAccessRule($rule); $item=Get-Item -LiteralPath $target; [IO.FileSystemAclExtensions]::SetAccessControl($item,$acl)
42
45
  ` + WINDOWS_DACL_VERIFY_SCRIPT;
43
46
  export function windowsPrivateDaclCommand(target, kind = "directory") {
44
47
  return {
45
- command: "powershell.exe",
48
+ command: windowsPowerShell7Command(),
46
49
  args: ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", WINDOWS_DACL_SCRIPT],
47
50
  env: { ...process.env, AITERMMCP_ACL_PATH: target, AITERMMCP_ACL_KIND: kind },
48
51
  };
49
52
  }
50
53
  export function windowsPrivateDaclVerifyCommand(target, kind = "file") {
51
54
  return {
52
- command: "powershell.exe",
55
+ command: windowsPowerShell7Command(),
53
56
  args: ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", WINDOWS_DACL_VERIFY_SCRIPT],
54
57
  env: { ...process.env, AITERMMCP_ACL_PATH: target, AITERMMCP_ACL_KIND: kind },
55
58
  };
@@ -128,7 +131,7 @@ export function processStartIdentity(pid, platform, timeoutMs = 1000) {
128
131
  }
129
132
  if (platform === "win32") {
130
133
  const script = "$p=Get-Process -Id $env:AITERMMCP_PROCESS_ID -ErrorAction Stop; $p.StartTime.ToUniversalTime().Ticks";
131
- const result = spawnSync("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
134
+ const result = spawnSync(resolveWindowsPowerShell7(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
132
135
  encoding: "utf8", timeout: timeoutMs, maxBuffer: 4096, windowsHide: true,
133
136
  env: { ...process.env, AITERMMCP_PROCESS_ID: String(pid) },
134
137
  });
@@ -0,0 +1,42 @@
1
+ // Windows PowerShell実体の唯一の解決境界。5.1/cmd fallbackを持たず、検出したpwsh自身の
2
+ // edition/majorを実行時に検証してから絶対pathを返す。
3
+ import { spawnSync } from "node:child_process";
4
+ import * as path from "node:path";
5
+ import { AitermError } from "./errors.js";
6
+ export const WINDOWS_POWERSHELL_7_COMMAND = "pwsh.exe";
7
+ export const WINDOWS_POWERSHELL_7_INSTALL = "winget install --id Microsoft.PowerShell --source winget";
8
+ export const WINDOWS_POWERSHELL_7_FIXTURE = "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
9
+ const defaultProbe = (command, args) => spawnSync(command, [...args], {
10
+ encoding: "utf8", timeout: 5000, maxBuffer: 16 * 1024, windowsHide: true,
11
+ });
12
+ let resolvedDefault = null;
13
+ export function resolveWindowsPowerShell7(probe = defaultProbe) {
14
+ if (probe === defaultProbe && resolvedDefault !== null)
15
+ return resolvedDefault;
16
+ const located = probe("where.exe", [WINDOWS_POWERSHELL_7_COMMAND]);
17
+ const resolved = located.stdout?.split(/\r?\n/u)
18
+ .find(candidate => path.win32.isAbsolute(candidate)
19
+ && path.win32.basename(candidate).toLowerCase() === WINDOWS_POWERSHELL_7_COMMAND);
20
+ const fail = () => {
21
+ throw new AitermError(`PowerShell 7が必要です。Microsoft公式経路で導入してください: ${WINDOWS_POWERSHELL_7_INSTALL}`, 2);
22
+ };
23
+ if (located.status !== 0 || typeof resolved !== "string")
24
+ fail();
25
+ const executable = resolved;
26
+ const version = probe(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command",
27
+ '[ordered]@{ edition = $PSVersionTable.PSEdition; major = $PSVersionTable.PSVersion.Major } | ConvertTo-Json -Compress']);
28
+ let identity = null;
29
+ try {
30
+ identity = JSON.parse(version.stdout?.trim() ?? "");
31
+ }
32
+ catch { /* typed failure below */ }
33
+ if (version.status !== 0 || identity?.edition !== "Core"
34
+ || !Number.isInteger(identity.major) || Number(identity.major) < 7)
35
+ fail();
36
+ if (probe === defaultProbe)
37
+ resolvedDefault = executable;
38
+ return executable;
39
+ }
40
+ export function windowsPowerShell7Command(platform = process.platform) {
41
+ return platform === "win32" ? resolveWindowsPowerShell7() : WINDOWS_POWERSHELL_7_FIXTURE;
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.28.3",
3
+ "version": "0.29.0",
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": [