aiterm-mcp 0.41.3 → 0.42.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,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.42.1] - 2026-09-28
11
+
12
+ ### 修正
13
+
14
+ - Claude Codeの利用上限の知らせを、pane logの末尾16KBから探していた。上限が明けても古い知らせがlogに残るため、次のturnの最初の見回りで`wait_process`が`rate_limited`を返し、実際には動いているturnやAPIエラーで終わったturnを取り落としていた。入力欄の下に出る知らせを今の画面から探す。この知らせは次のturnが始まると消える。会話欄や依頼文に残る上限の文は数えない。
15
+
16
+ ## [0.42.0] - 2026-09-28
17
+
18
+ ### 追加
19
+
20
+ - BellTeamとの連携用に、`pty_observe`がBellTeamからの要求にだけ、実行中のturnで書かれた作業の合間の言葉を返せるようにした。ほかの呼び手に対するツールの説明、引数、結果は変わらない。判断はrepositoryの`docs/adr/0071-bellteam-interim-words.md`。
21
+
22
+ ### 修正
23
+
24
+ - Cursorは記録にturnの終了を書いたあとも、しばらく画面に実行中の表示を残す。完了通知の直後に`pty_send`すると差し込みと判定し、`STEER_NOT_QUEUED`で失敗していた(手元で3回中2回)。記録の末尾がturnの終了なら新しいturnとして送る。
25
+
10
26
  ## [0.41.3] - 2026-09-28
11
27
 
12
28
  ### 修正
@@ -1828,7 +1844,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1828
1844
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1829
1845
  provenance.
1830
1846
 
1831
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.3...HEAD
1847
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.42.1...HEAD
1848
+ [0.42.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.42.0...v0.42.1
1849
+ [0.42.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.3...v0.42.0
1832
1850
  [0.41.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.2...v0.41.3
1833
1851
  [0.41.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.1...v0.41.2
1834
1852
  [0.41.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.0...v0.41.1
package/README.ja.md CHANGED
@@ -203,7 +203,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
203
203
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
204
204
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
205
205
 
206
- **状態:** 開発継続中 · 現行公開版 **v0.41.3** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
206
+ **状態:** 開発継続中 · 現行公開版 **v0.42.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
207
207
 
208
208
  ### 更新と巻き戻し
209
209
 
package/README.md CHANGED
@@ -217,7 +217,7 @@ collection is off by default and performs no network I/O. It ships via
217
217
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
218
218
  Release re-registers the Official MCP Registry entry.
219
219
 
220
- **Status:** actively maintained · current public release **v0.41.3** · 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
+ **Status:** actively maintained · current public release **v0.42.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).
221
221
 
222
222
  ### Update and rollback
223
223
 
package/dist/core.js CHANGED
@@ -20,8 +20,9 @@ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, past
20
20
  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";
21
21
  import { 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
- 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, claudeApiErrorAfter, } from "./harnesses/claude.js";
23
+ import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
24
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
+ import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
25
26
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
26
27
  export { AitermError } from "./errors.js";
27
28
  export { tmuxSpawnEnv } from "./tmux-runtime.js";
@@ -306,7 +307,8 @@ function cleanupAgentState(name) {
306
307
  f.endsWith(".cursor-result.json") ||
307
308
  f.endsWith(".claude-operation.json") ||
308
309
  f.endsWith(".claude-approval.json") ||
309
- f.endsWith(".claude-dispatch"))
310
+ f.endsWith(".claude-dispatch") ||
311
+ f.endsWith(".interim.json"))
310
312
  fs.unlinkSync(p);
311
313
  else if (f.endsWith(".codex-home") || f.endsWith(".grok-home") || f.endsWith(".cursor-plugin") || f.endsWith(".home")) {
312
314
  fs.rmSync(p, { recursive: true, force: true });
@@ -1333,6 +1335,7 @@ export function killAll() {
1333
1335
  f.endsWith(".claude-result.json") ||
1334
1336
  f.endsWith(".claude-operation.json") ||
1335
1337
  f.endsWith(".claude-dispatch") ||
1338
+ f.endsWith(".interim.json") ||
1336
1339
  f.endsWith(".codex-home") ||
1337
1340
  f.endsWith(".grok-home") ||
1338
1341
  f.endsWith(".home")) {
@@ -2406,6 +2409,14 @@ async function settlePublishedClaudeCompletionMarker(meta, marker) {
2406
2409
  }
2407
2410
  return active;
2408
2411
  }
2412
+ /** BellTeamのため: agent sessionの現在(または直前)のturnの合間の言葉。agent sessionでなければnull。 */
2413
+ export function readSessionInterimWords(name, after) {
2414
+ assertSessionName(name);
2415
+ const dir = existingAgentsDir();
2416
+ if (!dir || !fs.readdirSync(dir).some((f) => f.startsWith(`${name}.`) && f.endsWith(".agent.json")))
2417
+ return null;
2418
+ return readInterimWords(loadAgentMetadata(name), after);
2419
+ }
2409
2420
  /** agent harness の構造化 transcript から直近完了ターンの最終回答と相関情報を読む。 */
2410
2421
  export async function readAgentTranscriptResult(name, o = {}) {
2411
2422
  const meta = loadAgentMetadata(name);
@@ -2630,14 +2641,13 @@ export function agentWaitGuide(session) {
2630
2641
  const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
2631
2642
  return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
2632
2643
  }
2633
- // harness別の利用上限観測。Grokは現在の質問カード、Cursorは現在の画面、他harnessは既存logを使う。
2644
+ // harness別の利用上限観測。Grokは現在の質問カード、CursorとClaudeは現在の画面、Codexは既存logを使う。
2634
2645
  // 出典(2026-08-22): grok は live 実バナーで検証、codex/claude はインストール済み実バイナリの
2635
2646
  // 埋込文字列から抽出(codex: "You've hit your usage limit for" / claude: "Usage limit reached ·
2636
2647
  // continuing automatically when it resets"。Claude Code はリセット時に自動継続する設計なので、
2637
2648
  // この報告は「今は上限で止まっている」の観測であり恒久停止を意味しない)。
2638
2649
  const AGENT_RATE_LIMIT_PATTERNS = {
2639
2650
  codex: [/You'?ve hit your usage limit/i],
2640
- claude: [/Usage limit reached/i],
2641
2651
  };
2642
2652
  const AGENT_RATE_LIMIT_SCAN_BYTES = 16 * 1024;
2643
2653
  // pane log の末尾から上限バナーを探す。読めない・無い・対象 harness でないは全て null(誤検知より取りこぼし側へ倒す)。
@@ -2647,6 +2657,10 @@ export function detectAgentRateLimit(kind, aitermSession) {
2647
2657
  }
2648
2658
  if (kind === "cursor")
2649
2659
  return cursorUsageLimit(captureScreen(aitermSession, 0))?.message ?? null;
2660
+ // Claudeの知らせはpane logに上限が明けた後も残り、次のturnの最初の見回りで誤って拾っていた(2026-09-28)。
2661
+ // 足元の知らせは次のturnで消えるので、今の画面だけを見る。
2662
+ if (kind === "claude")
2663
+ return claudeUsageLimit(captureScreen(aitermSession, 0))?.message ?? null;
2650
2664
  const patterns = AGENT_RATE_LIMIT_PATTERNS[kind];
2651
2665
  if (!patterns)
2652
2666
  return null;
@@ -3626,6 +3640,7 @@ export async function dispatchAgentTurn(name, text, o = {}) {
3626
3640
  prepareSendText(dispatchText, { raw: o.raw });
3627
3641
  await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
3628
3642
  harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
3643
+ recordInterimBoundary(meta, startOffset, operationId);
3629
3644
  if (meta.kind === "claude") {
3630
3645
  // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
3631
3646
  // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
@@ -3716,6 +3731,11 @@ export async function sendAgentMessage(name, text, o = {}) {
3716
3731
  clearClaudeApiErrorOperation(meta);
3717
3732
  running = readClaudeOperationMarker(meta) !== null;
3718
3733
  }
3734
+ // Cursorはturn_endedを記録へ書いたあとも、しばらく画面にbusy表示を残す。その間に差し込むと待ち行列へ入らず
3735
+ // STEER_NOT_QUEUEDで失敗していた(実測 2026-09-28、完了通知の直後の送信で3回中2回)。記録の末尾がturn_endedなら
3736
+ // turnは終わっているので新しいturnとして送る。dispatchは入力を受け付けるまで待ってから送る。
3737
+ if (running && meta.kind === "cursor" && latestCursorCompletion(meta, readTranscriptLines) !== null)
3738
+ running = false;
3719
3739
  if (running) {
3720
3740
  return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery });
3721
3741
  }
@@ -206,6 +206,30 @@ export function claudePaneObservation(screen) {
206
206
  return { state: "blocked", reason: "startup_dialog" };
207
207
  return { state: "unknown", reason: "unrecognized_screen" };
208
208
  }
209
+ // 利用上限で止まっているClaude Codeは、入力欄の下の枠線と権限表示の間へ知らせを出す(BellTeamコンテナの実画面 2026-09-28)。
210
+ // ❯
211
+ // ────────
212
+ // ⚠ Usage limit reached · continuing automatically at 4:10pm · esc to cancel
213
+ // ⏵⏵ bypass permissions on (shift+tab to cycle)
214
+ // 次のturnが始まると足元は描き直されて知らせは消える。会話欄の「● Usage limit reached」や依頼文の引用は
215
+ // 上限が明けても画面やpane logに残るので、今の状態として数えない。
216
+ export function claudeUsageLimit(screen) {
217
+ const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split("\n");
218
+ let composer = lines.length - 1;
219
+ while (composer >= 0 && !CLAUDE_COMPOSER_MARKER_RE.test(lines[composer]))
220
+ composer--;
221
+ if (composer < 0)
222
+ return null;
223
+ const rule = lines.findIndex((line, i) => i > composer && /^\s*─{8,}\s*$/u.test(line));
224
+ if (rule < 0)
225
+ return null;
226
+ for (const line of lines.slice(rule + 1)) {
227
+ const hit = /^\s*(?:⚠\s*)?(Usage limit reached\b.*?)\s*(?:·\s*esc to cancel\s*)?$/iu.exec(line);
228
+ if (hit)
229
+ return { message: hit[1] };
230
+ }
231
+ return null;
232
+ }
209
233
  // Claude Code 2.1.282 は選択肢へ「1.」などの番号を付ける。番号の有無のどちらも受ける。
210
234
  export function claudeLoginMethodMenu(screen) {
211
235
  const tail = screen.split("\n").slice(-32).join("\n");
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { acceptRemote, callRemoteTool, observeRemoteAgentDone, remoteInputDescri
20
20
  import { codexParentFromRequest } from "./codex-parent-receiver.js";
21
21
  import { claudeParentFromRequest } from "./claude-parent-receiver.js";
22
22
  import { cursorParentFromRequest, isCursorMcpClient } from "./cursor-parent-receiver.js";
23
+ import { INTERIM_RESULT_META_KEY, interimRequestFromMeta } from "./interim-words.js";
23
24
  // package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
24
25
  // createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
25
26
  // engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
@@ -587,16 +588,30 @@ registerRemoteAwareTool("pty_observe", {
587
588
  cpu_seconds: z.number().nullable(), cpu_delta_seconds: z.number().nullable(), cpu_delta_complete: z.boolean().nullable(),
588
589
  background_cpu_seconds: z.number().nullable(), background_cpu_delta_seconds: z.number().nullable(), background_cpu_delta_complete: z.boolean().nullable() }),
589
590
  },
590
- }, async ({ session_id, cursor }) => {
591
+ }, async ({ session_id, cursor }, extra) => {
591
592
  try {
592
593
  const observation = core.observeSession(session_id, cursor);
593
594
  const result = { ...observation, ...(parentDelivery ? { parent_deliveries: parentDelivery.status(session_id) } : {}) };
594
- return { ...ok(JSON.stringify(result)), structuredContent: { ...result } };
595
+ return { ...ok(JSON.stringify(result)), structuredContent: { ...result }, ...interimWordsMeta(session_id, extra._meta) };
595
596
  }
596
597
  catch (e) {
597
598
  return fail(e);
598
599
  }
599
600
  });
601
+ // BellTeamのため: 要求の_metaに aiterm/caller="BellTeam" がある時だけ、結果の_metaへ合間の言葉を載せる。
602
+ // 説明と引数には出さない(interim-words.ts の冒頭)。それ以外の呼び手には何も足さない。
603
+ function interimWordsMeta(sessionId, requestMeta) {
604
+ const request = interimRequestFromMeta(requestMeta);
605
+ if (!request)
606
+ return {};
607
+ try {
608
+ const words = core.readSessionInterimWords(sessionId, request.after);
609
+ return words ? { _meta: { [INTERIM_RESULT_META_KEY]: words } } : {};
610
+ }
611
+ catch (e) {
612
+ return { _meta: { [INTERIM_RESULT_META_KEY]: { schema: "aiterm.interim-words.v1", error: e instanceof Error ? e.message : String(e) } } };
613
+ }
614
+ }
600
615
  registerRemoteAwareTool("agent_approval", {
601
616
  description: "Codexの現在の承認をinspectし、digestへ束縛した単発許可または拒否をrespondする。恒久許可は選ばない。Claudeは既存claude_approvalを使う。未知dialogはblockedのtyped errorで返す。",
602
617
  inputSchema: {
@@ -0,0 +1,256 @@
1
+ // 合間の言葉: agent sessionがturnの途中で書き、そのあと道具を使った言葉。
2
+ // BellTeamのためにある。BellTeamは実行中のBotが何をしているかを会話画面へ出すため、
3
+ // pty_observeのMCP要求の_metaに caller="BellTeam" を入れて呼び、結果の_metaでこれを受け取る。
4
+ // 利用者向けのツール説明と引数には出さない(BellTeam以外のAIに指定させないため)。要らないものと見て消さないこと。
5
+ //
6
+ // 最後の回答と思考は含めない。APIエラーやsession limitの知らせは、記録に書かれていれば含める。
7
+ // ハーネスごとの見分け方(2026-09-28 記録で確認):
8
+ // Claude Code: text block で message.stop_reason="tool_use"。isApiErrorMessage の行は知らせ。
9
+ // Codex: assistant message の phase="commentary"。
10
+ // Grok: content が空でなく tool_calls を持つ assistant。
11
+ // Cursor: assistant の text のあとに tool_use が続くもの(Cursorは道具が終わってから行を書くので遅れて届く)。
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { AitermError } from "./errors.js";
15
+ import { agentHarness, agentsDir, assertSessionName, LAUNCH_ID_RE, readFileRange, safeStatSize, writeJson0600, } from "./agent-shared.js";
16
+ import { claudeSessionTranscriptPath } from "./harnesses/claude.js";
17
+ import { codexRootTranscript } from "./harnesses/codex.js";
18
+ import { grokSessionDirectory } from "./harnesses/grok.js";
19
+ import { cursorTranscript } from "./harnesses/cursor.js";
20
+ /** BellTeamが要求の_metaへ入れる呼び手の印。値が "BellTeam" の時だけ合間の言葉を返す。 */
21
+ export const INTERIM_CALLER_META_KEY = "aiterm/caller";
22
+ export const INTERIM_CALLER = "BellTeam";
23
+ /** 前回受け取った最後のseq。これより後の言葉だけを返す。 */
24
+ export const INTERIM_AFTER_META_KEY = "aiterm/interim_after";
25
+ /** 結果の_metaで返すキー。 */
26
+ export const INTERIM_RESULT_META_KEY = "aiterm/interim_words";
27
+ export function agentInterimBoundaryPath(name, launchId) {
28
+ assertSessionName(name);
29
+ if (!LAUNCH_ID_RE.test(launchId))
30
+ throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
31
+ return path.join(agentsDir(), `${name}.${launchId}.interim.json`);
32
+ }
33
+ /** 要求の_metaがBellTeamの印を持つ時だけ、前回のseqを返す。それ以外はnull(何も足さない)。 */
34
+ export function interimRequestFromMeta(meta) {
35
+ if (!meta || typeof meta !== "object")
36
+ return null;
37
+ const record = meta;
38
+ if (record[INTERIM_CALLER_META_KEY] !== INTERIM_CALLER)
39
+ return null;
40
+ const after = record[INTERIM_AFTER_META_KEY];
41
+ return { after: typeof after === "number" && Number.isSafeInteger(after) && after > 0 ? after : 0 };
42
+ }
43
+ function parse(line) {
44
+ if (!line.trim())
45
+ return null;
46
+ try {
47
+ return JSON.parse(line);
48
+ }
49
+ catch {
50
+ // 書込み途中の末尾行と、byte境界で切れた先頭行は読まない。
51
+ return null;
52
+ }
53
+ }
54
+ // ---------------------------------------------------------------- Claude Code
55
+ function claudeText(content) {
56
+ if (typeof content === "string")
57
+ return content;
58
+ if (!Array.isArray(content))
59
+ return "";
60
+ return content.map((part) => (part?.type === "text" && typeof part.text === "string" ? part.text : "")).join("");
61
+ }
62
+ export function claudeInterimWords(lines) {
63
+ const words = [];
64
+ for (const line of lines) {
65
+ const record = parse(line);
66
+ if (record?.type !== "assistant" || record.isSidechain === true)
67
+ continue;
68
+ const at = typeof record.timestamp === "string" ? record.timestamp : null;
69
+ if (record.isApiErrorMessage === true) {
70
+ const text = claudeText(record.message?.content).trim();
71
+ if (text)
72
+ words.push({ text, kind: "error", at, turn_id: null });
73
+ continue;
74
+ }
75
+ if (record.message?.stop_reason !== "tool_use")
76
+ continue;
77
+ const text = claudeText(record.message?.content).trim();
78
+ if (text)
79
+ words.push({ text, kind: "interim", at, turn_id: null });
80
+ }
81
+ return words;
82
+ }
83
+ // ---------------------------------------------------------------- Codex
84
+ export function codexInterimWords(lines) {
85
+ const words = [];
86
+ for (const line of lines) {
87
+ const record = parse(line);
88
+ const payload = record?.payload;
89
+ if (record?.type !== "response_item" || payload?.type !== "message" || payload?.role !== "assistant"
90
+ || payload?.phase !== "commentary" || !Array.isArray(payload?.content))
91
+ continue;
92
+ const text = payload.content
93
+ .map((item) => (item?.type === "output_text" && typeof item.text === "string" ? item.text : ""))
94
+ .join("")
95
+ .trim();
96
+ if (!text)
97
+ continue;
98
+ const turnId = payload.internal_chat_message_metadata_passthrough?.turn_id;
99
+ words.push({
100
+ text,
101
+ kind: "interim",
102
+ at: typeof record.timestamp === "string" ? record.timestamp : null,
103
+ turn_id: typeof turnId === "string" && turnId ? turnId : null,
104
+ });
105
+ }
106
+ return words;
107
+ }
108
+ // ---------------------------------------------------------------- Grok
109
+ function grokRealUser(record) {
110
+ return record?.type === "user" && !("synthetic_reason" in record);
111
+ }
112
+ export function grokUserCount(lines) {
113
+ let count = 0;
114
+ for (const line of lines)
115
+ if (grokRealUser(parse(line)))
116
+ count++;
117
+ return count;
118
+ }
119
+ /** boundary+1番目のuser発話(このturnの依頼)以後のassistantのうち、言葉と道具の呼び出しを両方持つもの。 */
120
+ export function grokInterimWords(lines, boundary) {
121
+ const words = [];
122
+ let users = 0;
123
+ for (const line of lines) {
124
+ const record = parse(line);
125
+ if (grokRealUser(record))
126
+ users++;
127
+ if (users <= boundary || record?.type !== "assistant")
128
+ continue;
129
+ if (!Array.isArray(record.tool_calls) || record.tool_calls.length === 0)
130
+ continue;
131
+ const text = typeof record.content === "string" ? record.content.trim() : "";
132
+ if (text)
133
+ words.push({ text, kind: "interim", at: null, turn_id: null });
134
+ }
135
+ return words;
136
+ }
137
+ // ---------------------------------------------------------------- Cursor
138
+ /**
139
+ * boundary+1番目のuser発話(このturnの依頼)以後で、あとにtool_useが続いたtextの塊。
140
+ * 次にuser発話かturn_endedが来た塊はそのturnの最後の回答なので含めない。末尾で行き先が決まっていない塊も待つ。
141
+ */
142
+ export function cursorInterimWords(lines, boundary) {
143
+ const words = [];
144
+ let users = 0;
145
+ let pending = [];
146
+ for (const line of lines) {
147
+ const record = parse(line);
148
+ if (!record)
149
+ continue;
150
+ if (record.role === "user" || record.type === "turn_ended") {
151
+ if (record.role === "user")
152
+ users++;
153
+ pending = [];
154
+ continue;
155
+ }
156
+ if (record.role !== "assistant" || !Array.isArray(record.message?.content))
157
+ continue;
158
+ for (const part of record.message.content) {
159
+ if (part?.type === "text" && typeof part.text === "string")
160
+ pending.push(part.text);
161
+ else if (part?.type === "tool_use") {
162
+ const text = pending.join("\n").trim();
163
+ if (text && users > boundary)
164
+ words.push({ text, kind: "interim", at: null, turn_id: null });
165
+ pending = [];
166
+ }
167
+ }
168
+ }
169
+ return words;
170
+ }
171
+ // ---------------------------------------------------------------- 境界と読み取り
172
+ function claudeTranscriptSize(meta) {
173
+ const file = claudeSessionTranscriptPath(meta);
174
+ return file ? safeStatSize(file) : 0;
175
+ }
176
+ function grokChatHistory(meta) {
177
+ const dir = grokSessionDirectory(meta);
178
+ return dir ? path.join(dir, "chat_history.jsonl") : null;
179
+ }
180
+ function readLines(file, from = 0) {
181
+ if (!file)
182
+ return [];
183
+ const size = safeStatSize(file);
184
+ if (size <= from)
185
+ return [];
186
+ return readFileRange(file, from, size).toString("utf8").split("\n");
187
+ }
188
+ /**
189
+ * dispatchの送信直前に、このturnの言葉が記録のどこから始まるかを残す。
190
+ * eventCursorはdispatch receiptのevent_cursor(Codex・Cursorでは境界そのもの)。
191
+ * 失敗してもdispatchは止めない。境界が古いままでも、呼び手はevent_cursorの不一致で気づける。
192
+ */
193
+ export function recordInterimBoundary(meta, eventCursor, operationId) {
194
+ try {
195
+ let boundary;
196
+ if (meta.kind === "claude")
197
+ boundary = claudeTranscriptSize(meta);
198
+ else if (meta.kind === "grok")
199
+ boundary = grokUserCount(readLines(grokChatHistory(meta)));
200
+ else
201
+ boundary = eventCursor;
202
+ const value = {
203
+ schema: "aiterm.interim-boundary.v1",
204
+ event_cursor: eventCursor,
205
+ operation_id: operationId,
206
+ boundary,
207
+ dispatched_at: new Date().toISOString(),
208
+ };
209
+ writeJson0600(agentInterimBoundaryPath(meta.aiterm_session, meta.launch_id), value);
210
+ }
211
+ catch {
212
+ /* 合間の言葉は表示のためだけにある。送信を妨げない。 */
213
+ }
214
+ }
215
+ function readInterimBoundary(meta) {
216
+ let value;
217
+ try {
218
+ value = JSON.parse(fs.readFileSync(agentInterimBoundaryPath(meta.aiterm_session, meta.launch_id), "utf8"));
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ if (value?.schema !== "aiterm.interim-boundary.v1" || !Number.isSafeInteger(value.boundary) || value.boundary < 0
224
+ || !Number.isSafeInteger(value.event_cursor))
225
+ return null;
226
+ return value;
227
+ }
228
+ /** 現在(または直前)のturnで書かれた合間の言葉のうち、seqがafterより後のもの。 */
229
+ export function readInterimWords(meta, after) {
230
+ const recorded = readInterimBoundary(meta);
231
+ // dispatchの記録が無ければ、起動時promptのturnだけを記録の先頭から読む。promptなしで起動してまだ送っていない時は無い。
232
+ const launchTurn = !recorded && meta.initial_prompt !== "none";
233
+ let found = [];
234
+ if (recorded || launchTurn) {
235
+ const boundary = recorded?.boundary ?? 0;
236
+ if (meta.kind === "claude")
237
+ found = claudeInterimWords(readLines(claudeSessionTranscriptPath(meta), boundary));
238
+ else if (meta.kind === "codex")
239
+ found = codexInterimWords(readLines(codexRootTranscript(meta), boundary));
240
+ else if (meta.kind === "grok")
241
+ found = grokInterimWords(readLines(grokChatHistory(meta)), boundary);
242
+ else
243
+ found = cursorInterimWords(readLines(cursorTranscript(meta)), boundary);
244
+ }
245
+ const words = found.map((word, index) => ({ seq: index + 1, ...word }));
246
+ return {
247
+ schema: "aiterm.interim-words.v1",
248
+ session_id: meta.aiterm_session,
249
+ launch_id: meta.launch_id,
250
+ harness: agentHarness(meta.kind),
251
+ event_cursor: recorded?.event_cursor ?? null,
252
+ operation_id: recorded?.operation_id ?? null,
253
+ last_seq: words.length,
254
+ words: words.filter((word) => word.seq > after),
255
+ };
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.41.3",
3
+ "version": "0.42.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": [