aiterm-mcp 0.40.0 → 0.40.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.40.1] - 2026-09-27
11
+
12
+ ### 修正
13
+
14
+ - Cursorの送信前hookがpromptを拒否した時に、`pty_send`が成功receiptを返していた。Cursorはpromptを捨てて入力欄を空に戻すため、入力欄の残留検査では見分けられず、親は来ない完了を待ち続けた。起動時promptは理由の無い`submitted_unconfirmed`になっていた。拒否の表示(`Hook blocked with message:`)を見分け、起動時promptは`initial_prompt=failed`、`pty_send`はエラーとし、どちらも`USER_HOOK_BLOCKED`とhookの出力を返す。確認時間より後の拒否は、完了待ちが`outcome=error`で返す。`pty_observe`は`blocked`/`user_hook_blocked`を返す。
15
+ - Cursorの送信前hookが3秒の確認時間より長く動くと、拒否されても起動時promptは理由の無い`submitted_unconfirmed`で返っていた。Windowsではhookごとの起動が遅く、数本続くと3秒を超える。hookの実行中の画面(「Working」だけで`ctrl+c to stop`が無い)が続く間は、最長60秒まで結果を待つ。
16
+
10
17
  ## [0.40.0] - 2026-09-27
11
18
 
12
19
  ### 変更(互換性なし)
@@ -1785,7 +1792,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1785
1792
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1786
1793
  provenance.
1787
1794
 
1788
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.40.0...HEAD
1795
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.40.1...HEAD
1796
+ [0.40.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.40.0...v0.40.1
1789
1797
  [0.40.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.39.1...v0.40.0
1790
1798
  [0.39.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.39.0...v0.39.1
1791
1799
  [0.39.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.38.2...v0.39.0
package/README.ja.md CHANGED
@@ -202,7 +202,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
202
202
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
203
203
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
204
204
 
205
- **状態:** 開発継続中 · 現行公開版 **v0.40.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
205
+ **状態:** 開発継続中 · 現行公開版 **v0.40.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
206
206
 
207
207
  ### 更新と巻き戻し
208
208
 
@@ -258,6 +258,8 @@ Grok/Composerの無人起動は公式`--trust`で指定された作業フォ
258
258
 
259
259
  Grok/Composerで終了済みターンのweekly-limitパネルが残っている場合、次の通常`pty_send`が`Shift+X`で一度閉じ、入力受付を確認して今回の本文を送る。同じsessionと会話を保ち、receiptの`pane_input_recovery`に`grok_rate_limit_dialog_dismissed`を記録する。ターン未終了・harness不在は`GROK_RATE_LIMIT_RECOVERY_BLOCKED`、解除後の入力受付失敗は`GROK_RATE_LIMIT_RECOVERY_FAILED`となり、本文は未送信。上限の継続は`rate_limited`として返し、過去promptは再送しない。Grokの上限観測には現在の画面だけを使う。
260
260
 
261
+ Cursorの送信前hook(`beforeSubmitPrompt`と、互換読込するClaude Codeの`UserPromptSubmit`)がpromptを拒否すると、Cursorはpromptを捨て、turnも完了も起きない。Aitermはこの拒否の表示を見分け、起動時promptは`initial_prompt=failed`、`pty_send`は成功receiptを返さず、どちらも`USER_HOOK_BLOCKED`とhookの出力を返す。確認時間(3秒)より後の拒否は、完了待ちが`outcome=error`(`aiterm-wait`はexit 7)で返す。
262
+
261
263
  Grok/Composerがread-only sandboxの適用を拒否した場合、prompt送信時に`GROK_SANDBOX_STARTUP_FAILED`とCLIの原因を返す。例えばhookのパスにシンボリックリンクがあるとGrok CLIは起動を拒否する。設定の管理元で原因を修正し、対象sessionを`pty_close`して起動し直す。Aitermはsandboxを解除したりhookをコピーしたりしない。
262
264
 
263
265
  この判定はGrok専用アダプターが所有し、同じCLIを使うComposerにも適用する。初回prompt付きの`agent_launch`と通常の`pty_send`で、入力受付待ち中に拒否を検出すると未送信のエラーを返す。promptなし・`trust_project`指定なしの起動応答は入力受付を保証しない。`trust_project:true`では入力受付まで確認し、`startup.status`を返す。Grokのprivacy notice起動設定も同アダプターが所有する。実装の責務分担は[DESIGN](docs/DESIGN.md#failure-and-recovery)を参照。
package/README.md CHANGED
@@ -216,7 +216,7 @@ collection is off by default and performs no network I/O. It ships via
216
216
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
217
217
  Release re-registers the Official MCP Registry entry.
218
218
 
219
- **Status:** actively maintained · current public release **v0.40.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
219
+ **Status:** actively maintained · current public release **v0.40.1** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
220
220
 
221
221
  ### Update and rollback
222
222
 
@@ -279,6 +279,8 @@ Grok/Composerの無人起動は公式`--trust`で指定された作業フォ
279
279
 
280
280
  Grok/Composerで終了済みターンのweekly-limitパネルが残っている場合、次の通常`pty_send`が`Shift+X`で一度閉じ、入力受付を確認して今回の本文を送る。同じsessionと会話を保ち、receiptの`pane_input_recovery`に`grok_rate_limit_dialog_dismissed`を記録する。ターン未終了・harness不在は`GROK_RATE_LIMIT_RECOVERY_BLOCKED`、解除後の入力受付失敗は`GROK_RATE_LIMIT_RECOVERY_FAILED`となり、本文は未送信。上限の継続は`rate_limited`として返し、過去promptは再送しない。Grokの上限観測には現在の画面だけを使う。
281
281
 
282
+ When a Cursor pre-submit hook (`beforeSubmitPrompt`, or a Claude Code `UserPromptSubmit` hook that Cursor loads for compatibility) rejects the prompt, Cursor drops it and no turn or completion follows. Aiterm recognizes the rejection: an initial prompt returns `initial_prompt=failed`, and `pty_send` returns an error instead of a success receipt, both with `USER_HOOK_BLOCKED` and the hook's output. A rejection that comes after the 3-second start check is reported by the completion wait as `outcome=error` (`aiterm-wait` exit 7).
283
+
282
284
  Grok/Composerがread-only sandboxの適用を拒否した場合、prompt送信時に`GROK_SANDBOX_STARTUP_FAILED`とCLIの原因を返す。hookパスのシンボリックリンクなど、CLIが示した原因を設定の管理元で修正し、対象sessionを`pty_close`して起動し直す。Aitermはsandboxを解除したりhookをコピーしたりしない。
283
285
 
284
286
  この判定はGrok専用アダプターが所有し、同じCLIを使うComposerにも適用する。初回prompt付きの`agent_launch`と通常の`pty_send`で、入力受付待ち中に拒否を検出すると未送信のエラーを返す。promptなし・`trust_project`指定なしの起動応答は入力受付を保証しない。`trust_project:true`では入力受付まで確認し、`startup.status`を返す。Grokのprivacy notice起動設定も同アダプターが所有する。実装の責務分担は[DESIGN](docs/DESIGN.md#failure-and-recovery)を参照。
package/dist/core.js CHANGED
@@ -21,7 +21,7 @@ import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, write
21
21
  import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
22
22
  import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
23
23
  import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
24
- import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
24
+ import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
25
25
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
26
26
  export { AitermError } from "./errors.js";
27
27
  export { tmuxSpawnEnv } from "./tmux-runtime.js";
@@ -2669,7 +2669,7 @@ export async function observeAgentDone(name, o = {}) {
2669
2669
  return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
2670
2670
  }
2671
2671
  if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
2672
- return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
2672
+ return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit, (session) => captureScreen(session, 0), o.signal);
2673
2673
  }
2674
2674
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2675
2675
  return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
@@ -3213,8 +3213,10 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
3213
3213
  }
3214
3214
  let delivery = { status: "submitted_unconfirmed", reason: "start_unconfirmed", turn_started: null };
3215
3215
  const deadline = performance.now() + 3000;
3216
+ const hookDeadline = performance.now() + CURSOR_PROMPT_HOOK_WAIT_MS;
3217
+ let screen = "";
3216
3218
  do {
3217
- const screen = captureScreen(name, AGENT_TUI_READY_LINES);
3219
+ screen = captureScreen(name, AGENT_TUI_READY_LINES);
3218
3220
  const state = meta.kind === "grok" || meta.kind === "composer" ? grokPaneObservation(screen)
3219
3221
  : meta.kind === "codex" ? codexPaneObservation(screen)
3220
3222
  : meta.kind === "claude" ? claudePaneObservation(screen) : cursorPaneObservation(screen);
@@ -3226,6 +3228,11 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
3226
3228
  delivery = { status: "started", reason: "approval_required", turn_started: true };
3227
3229
  break;
3228
3230
  }
3231
+ if (state.state === "blocked" && state.reason === "user_hook_blocked") {
3232
+ setInitialDelivery(meta, { status: "submitted_unconfirmed", reason: "user_hook_blocked", turn_started: false }, startOffset);
3233
+ setInitialPromptState(meta, "failed");
3234
+ throw new AitermError(userHookBlockedMessage(`initial_prompt=failed vendor=${meta.kind} harness=${agentHarness(meta.kind)}`, "起動時prompt", state.detail), 2);
3235
+ }
3229
3236
  const completed = await observeAgentDone(name, { cursor: startOffset, timeout: 0 });
3230
3237
  if (completed.outcome === "done") {
3231
3238
  delivery = { status: "started", reason: "turn_completed", turn_started: true };
@@ -3236,7 +3243,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
3236
3243
  break;
3237
3244
  }
3238
3245
  await sleep(100);
3239
- } while (performance.now() < deadline);
3246
+ } while (performance.now() < deadline || (meta.kind === "cursor" && performance.now() < hookDeadline && cursorPromptHooksRunning(screen)));
3240
3247
  setInitialDelivery(meta, delivery, startOffset);
3241
3248
  return {
3242
3249
  text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset} harness=${agentHarness(meta.kind)}\n` +
@@ -3510,6 +3517,32 @@ export function attachImages(text, images) {
3510
3517
  const body = text.trim().length > 0 ? text : "添付画像を確認してください。";
3511
3518
  return `${body}\n\n${lines.join("\n")}\n添付画像は上のファイルを読んで確認する。`;
3512
3519
  }
3520
+ function userHookBlockedMessage(head, what, detail) {
3521
+ return `${head}\nUSER_HOOK_BLOCKED: 利用者のhookが${what}を拒否したため、turnは始まっていません。完了通知も来ません。` +
3522
+ `hookを直すか外してから送り直してください。hookの出力: ${detail ?? "(不明)"}`;
3523
+ }
3524
+ const CURSOR_START_CONFIRM_MS = 3000;
3525
+ // 送信前hookが動いている間は、確認時間を過ぎても結果を待つ。Windowsではhookごとの起動が遅く、数本続くと3秒を超える
3526
+ // (fox実測 2026-09-27)。上限は利用者のhookに付く最長のtimeout(60秒)に合わせる。
3527
+ const CURSOR_PROMPT_HOOK_WAIT_MS = 60_000;
3528
+ // Cursorは送信前hookの実行中も「Working」だけを出し、拒否されると入力欄を空に戻す。入力欄の残留検査では
3529
+ // 拒否を見分けられず、成功receiptを返すと親は来ない完了を待ち続ける。turnの開始か拒否の表示を確かめる。
3530
+ // hookが上限を過ぎてから拒否した時は、完了待ち(observeCursorDone)がoutcome=errorで返す。
3531
+ async function assertCursorPromptNotHookBlocked(name) {
3532
+ const deadline = performance.now() + CURSOR_START_CONFIRM_MS;
3533
+ const hookDeadline = performance.now() + CURSOR_PROMPT_HOOK_WAIT_MS;
3534
+ let screen = "";
3535
+ do {
3536
+ screen = captureScreen(name, AGENT_TUI_READY_LINES);
3537
+ const state = cursorPaneObservation(screen);
3538
+ if (state.state === "busy")
3539
+ return;
3540
+ if (state.state === "blocked" && state.reason === "user_hook_blocked") {
3541
+ throw new AitermError(userHookBlockedMessage(`vendor=cursor session=${name}`, "送信した文", state.detail), 2);
3542
+ }
3543
+ await sleep(100);
3544
+ } while (performance.now() < deadline || (performance.now() < hookDeadline && cursorPromptHooksRunning(screen)));
3545
+ }
3513
3546
  export async function dispatchAgentTurn(name, text, o = {}) {
3514
3547
  assertSessionName(name);
3515
3548
  const meta = loadAgentMetadata(name);
@@ -3608,6 +3641,8 @@ export async function dispatchAgentTurn(name, text, o = {}) {
3608
3641
  // 陽性観測した場合だけ同じEnterを一度再送し、再検査後も残る時は失敗として返す。
3609
3642
  residue = await retryCursorSubmitIfResidue(name, meta.kind, dispatchText, residue);
3610
3643
  assertAgentSubmitDelivered(name, meta.kind, residue);
3644
+ if (meta.kind === "cursor")
3645
+ await assertCursorPromptNotHookBlocked(name);
3611
3646
  return {
3612
3647
  schema: "aiterm.agent-dispatch.v1",
3613
3648
  session_id: meta.aiterm_session,
@@ -183,13 +183,13 @@ export function latestCursorCompletion(meta, readTranscriptLines) {
183
183
  ? cursorCompletionEvent(meta, harnessSessionId, state.terminalRecord, `cursor:${state.userTurns}`)
184
184
  : null;
185
185
  }
186
- export async function observeCursorDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
186
+ export async function observeCursorDone(meta, timeout, requestedCursor, detectRateLimit, readScreen, signal) {
187
187
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
188
188
  let transcript = cursorTranscript(meta);
189
189
  const startBoundary = requestedCursor ?? (transcript ? cursorTranscriptState(transcript).userTurns : 0);
190
190
  let malformedEvents = 0;
191
191
  const deadline = performance.now() + timeout * 1000;
192
- const observation = (outcome, ev = null, rateLimit = null) => ({
192
+ const observation = (outcome, ev = null, rateLimit = null, error = null) => ({
193
193
  schema: "aiterm.agent-wait-result.v1",
194
194
  session_id: meta.aiterm_session,
195
195
  launch_id: meta.launch_id,
@@ -202,7 +202,7 @@ export async function observeCursorDone(meta, timeout, requestedCursor, detectRa
202
202
  malformed_events: malformedEvents,
203
203
  at: ev?.at ?? null,
204
204
  rate_limit: rateLimit,
205
- error: null,
205
+ error,
206
206
  });
207
207
  for (;;) {
208
208
  signal?.throwIfAborted();
@@ -222,6 +222,10 @@ export async function observeCursorDone(meta, timeout, requestedCursor, detectRa
222
222
  const limited = detectRateLimit(meta.kind, meta.aiterm_session);
223
223
  if (limited)
224
224
  return observation("rate_limited", null, limited);
225
+ // 拒否されたpromptのturnは始まらず、完了も来ない。拒否の表示は数秒で消えるので、見えている間に終わらせる。
226
+ const hookBlocked = cursorHookBlocked(readScreen(meta.aiterm_session));
227
+ if (hookBlocked)
228
+ return observation("error", null, null, `USER_HOOK_BLOCKED: ${hookBlocked.message}`);
225
229
  if (performance.now() >= deadline)
226
230
  return observation(timeout === 0 ? "running" : "timeout");
227
231
  await sleep(AGENT_DONE_POLL_MS);
@@ -452,10 +456,44 @@ export function cursorUsageLimit(screen) {
452
456
  }
453
457
  return { message: [`${heading[1]}.`, ...detail].join(" ") };
454
458
  }
459
+ // Cursor Agentはpromptを送る前のhook(beforeSubmitPromptと、互換読込したClaude CodeのUserPromptSubmit)が
460
+ // 拒否すると、promptを捨てて入力欄を空に戻し、その下に「Hook blocked with message:」を数秒だけ出す。turnは始まらず、
461
+ // transcriptにもuser turnは残らない(v2026.09.26、Windows実機採取 2026-09-27)。
462
+ const CURSOR_HOOK_BLOCKED_RE = /^[ \t]*Hook blocked with message:[ \t]*(.*)$/gim;
463
+ const CURSOR_HOOK_BLOCKED_MESSAGE_LIMIT = 600;
464
+ export function cursorHookBlocked(screen) {
465
+ const clean = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
466
+ const hit = [...clean.matchAll(CURSOR_HOOK_BLOCKED_RE)].at(-1);
467
+ if (!hit)
468
+ return null;
469
+ const after = clean.slice(hit.index + hit[0].length);
470
+ // 拒否の後に新しいturnが動いている画面は、古い表示の名残として数えない。
471
+ if (/ctrl\+c to stop/i.test(after) || CURSOR_FOLLOWUP_MARKER_RE.test(after))
472
+ return null;
473
+ const lines = [hit[1].trim()];
474
+ for (const line of after.split("\n").slice(1)) {
475
+ if (!line.trim())
476
+ break;
477
+ lines.push(line.trim());
478
+ }
479
+ // hookを動かしたNode自身の警告は拒否の理由ではない。
480
+ const message = lines.filter(line => line && !/ExperimentalWarning|--trace-warnings|^any time$/.test(line)).join(" ");
481
+ return { message: (message || "(hookは理由を出していません)").slice(0, CURSOR_HOOK_BLOCKED_MESSAGE_LIMIT) };
482
+ }
483
+ // 送信前hookの実行中、Cursorは入力欄の上に「Working」の回転表示だけを出し、入力欄はまだ「Plan, search, build anything」の
484
+ // ままで「ctrl+c to stop」も無い(v2026.09.26、Windows実機採取 2026-09-27)。turnはまだ始まっておらず、hookが拒否すればここで終わる。
485
+ const CURSOR_SPINNER_WORKING_RE = /^[ \t]*[⠀-⣿]+[ \t]+Working\b/m;
486
+ export function cursorPromptHooksRunning(screen) {
487
+ const tail = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split("\n").slice(-32).join("\n");
488
+ return CURSOR_SPINNER_WORKING_RE.test(tail) && CURSOR_START_PROMPT_MARKER_RE.test(tail) && !/ctrl\+c to stop/i.test(tail);
489
+ }
455
490
  export function cursorPaneObservation(screen) {
456
491
  const tail = screen.split("\n").slice(-32).join("\n");
457
492
  if (cursorUsageLimit(tail))
458
493
  return { state: "blocked", reason: "rate_limited" };
494
+ const hookBlocked = cursorHookBlocked(tail);
495
+ if (hookBlocked)
496
+ return { state: "blocked", reason: "user_hook_blocked", detail: hookBlocked.message };
459
497
  if (/ctrl\+c to stop/i.test(tail))
460
498
  return { state: "busy", reason: "turn_running" };
461
499
  if (cursorTuiReady(tail))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.40.0",
3
+ "version": "0.40.1",
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": [