aiterm-mcp 0.53.2 → 0.55.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/CHANGELOG.md +44 -1
- package/README.ja.md +6 -3
- package/README.md +6 -3
- package/dist/core.js +29 -5
- package/dist/harnesses/claude.js +30 -1
- package/dist/harnesses/codex.js +11 -3
- package/dist/harnesses/cursor.js +22 -6
- package/dist/harnesses/grok.js +6 -3
- package/dist/index.js +17 -2
- package/dist/parent-delivery.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.55.0] - 2026-10-06
|
|
11
|
+
|
|
12
|
+
### 変更
|
|
13
|
+
|
|
14
|
+
- `agent_launch`は、起動時promptの開始を確認できなかった返り(`initial_prompt.status`が`submitted_unconfirmed`)に
|
|
15
|
+
`isError`を付けない(ADR 0093)。sessionは立ち、promptは入力欄を離れ、親への配送も登録済みなので、状態は
|
|
16
|
+
`initial_prompt`と本文で伝える。本文には「turnの開始は未確認(reason=…)。再送もagentの起動し直しもしない。
|
|
17
|
+
確かめる時は pty_observe」の案内が付く。誤りの印でこの状態を見分けていた呼び出し側は、`initial_prompt.status`を見る。
|
|
18
|
+
送信の途中で失敗した起動は、今までどおり誤りで返る。`remote`付きの起動は、現地が旧版で誤りを返しても同じ形で返す。
|
|
19
|
+
|
|
20
|
+
### 修正
|
|
21
|
+
|
|
22
|
+
- Claude Code親が起こした子の回答が、親へ届かない事があった。開始未確認の返りを誤りで返していて、Claude Codeは
|
|
23
|
+
誤りの返りでは受け口のhook(`PostToolUse`)を走らせない。配送は`sending`のまま残り、返りの本文は「回答は自動配送する。
|
|
24
|
+
回収は不要」と案内していた。上の変更で、受け口が出来て回答が届く。
|
|
25
|
+
- 動いているClaude Codeを、入力待ち(`idle`/`composer_ready`)と読む事があった。Claude Code(2.1.289〜2.1.291で確認)は、
|
|
26
|
+
複数行のprompt(貼り付けの形で入る)を送った後の約8秒、足元の行を`paste again to expand`へ置き換える。動作中の印に
|
|
27
|
+
していた`esc to interrupt`がその間は画面に無い。複数行のpromptを付けたClaudeの子の起動は毎回、開始未確認になり、
|
|
28
|
+
`pty_observe`もその間`idle`を返していた。動いている間は入力欄の上に進行行(`✶ Slithering… (3s · ↓ 225 tokens)`)が
|
|
29
|
+
出続けるので、それも動作中の印にする。Linux・Mac・Windowsの実画面で確かめた。Codex・Grok・Cursorの読み方は変えていない。
|
|
30
|
+
|
|
31
|
+
## [0.54.0] - 2026-10-06
|
|
32
|
+
|
|
33
|
+
### 変更
|
|
34
|
+
|
|
35
|
+
- `pty_read(agent_transcript:true)`は、回答が空で終わったturnを誤りにせず、`text`が空文字列、`answer_empty`が`true`の
|
|
36
|
+
結果で返す(ADR 0092)。`aiterm.pty-read-result.v1`へ`answer_empty`を足した(回答がある時は`false`、terminalの読み取りは`null`)。
|
|
37
|
+
今までは、回答を空で終えただけのturnと、記録の形が変わって回答を読めなくなった時が、同じ
|
|
38
|
+
「最終 assistant メッセージを特定できませんでした」の誤りになり、呼び出し側が見分けられなかった。
|
|
39
|
+
Codex(0.160.1)は、道具で用を済ませて回答を空で終えたturnを、`text`が空の`final_answer`として記録に残す。
|
|
40
|
+
誤りにするのは、回答の場所を記録から見つけられない時だけにした(文面は同じ)。
|
|
41
|
+
|
|
42
|
+
### 修正
|
|
43
|
+
|
|
44
|
+
- 親への自動配送で、Codexの子が回答を空で終えると、親へ`AGENT_RESULT_UNAVAILABLE: transcript がまだありません。
|
|
45
|
+
ターン完了後に再取得してください`が`outcome=error`で届いていた。turnは終わっているので、
|
|
46
|
+
「子のターンは完了しましたが、回答の本文は空でした。」を`outcome=done`で届ける。Claude Code・Grok・Cursorの子も同じ。
|
|
47
|
+
- Codexの回答の読み取りは、turn ID付きの本文が空の時に、turn IDの無い旧形式の本文(前のturnの物かもしれない)へ
|
|
48
|
+
落とす作りだった。turn ID付きの本文が記録にあれば、空でもそれを回答とする。今のCodexは旧形式を書かず、
|
|
49
|
+
実記録で前のturnの回答が返った例は確認していない。
|
|
50
|
+
|
|
10
51
|
## [0.53.2] - 2026-10-05
|
|
11
52
|
|
|
12
53
|
### 修正
|
|
@@ -2215,7 +2256,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2215
2256
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2216
2257
|
provenance.
|
|
2217
2258
|
|
|
2218
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
2259
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.0...HEAD
|
|
2260
|
+
[0.55.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.54.0...v0.55.0
|
|
2261
|
+
[0.54.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.53.2...v0.54.0
|
|
2219
2262
|
[0.53.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.53.1...v0.53.2
|
|
2220
2263
|
[0.53.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.53.0...v0.53.1
|
|
2221
2264
|
[0.53.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.3...v0.53.0
|
package/README.ja.md
CHANGED
|
@@ -209,7 +209,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
209
209
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
210
210
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
211
211
|
|
|
212
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.55.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -546,7 +546,10 @@ Claude Code初回起動の文字表示テーマ選択では、画面で選択済
|
|
|
546
546
|
それでも描かれなければ、今までと同じく未送信(`initial_prompt=not_sent`)で返り、sessionは残る。
|
|
547
547
|
WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
|
|
548
548
|
初手の`initial_prompt.status`は`not_requested`/`not_sent`/`submitted_unconfirmed`/`started`を区別する。
|
|
549
|
-
|
|
549
|
+
未送信の失敗もsession付きstructuredContentを保持する。
|
|
550
|
+
`submitted_unconfirmed`(promptは入力欄を離れたが、確認時間の内に開始を見られなかった)は誤りにしない。`isError`は付かず、
|
|
551
|
+
本文に未確認の案内が付く。sessionは立っていて、親への配送も登録済み。未確認のpromptを再送せず、agentも起動し直さず、
|
|
552
|
+
返ったcursorか`pty_observe`で観測する。
|
|
550
553
|
|
|
551
554
|
Codexの実行中承認は`agent_approval({ action: "inspect", session_id })`の`prompt`と`choices`を確認し、
|
|
552
555
|
`respond`へ`observed_prompt_digest`と`approval_choice`(`approve_once`/`deny`)を渡す。
|
|
@@ -648,7 +651,7 @@ handoff contextを前置きできる。この任意経路は`throughline >= 0.9.
|
|
|
648
651
|
`THROUGHLINE_BIN`、次に`PATH`から解決し、不在・不正・空のexportはPTY作成前に明示失敗する。
|
|
649
652
|
任意の`throughline_supplement_file`はThroughline 0.10.8以降と`throughline_source_session`を必要とし、Aitermは内容を解釈せずThroughlineへ渡す。
|
|
650
653
|
|
|
651
|
-
エージェントの回答が画面tailより長ければ、`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。既存の人間向けcontentは診断suffixを維持し、機械呼出し側は`aiterm.pty-read-result.v1`の`structuredContent.text`から回答本文だけを取得する。Claudeはlaunch相関Stop hook、Codexは通常rollout、Grokは最後の実user行以後の最後の空でないassistantメッセージだけ、Cursorはlaunch IDでbindした通常agent transcriptから同じturn
|
|
654
|
+
エージェントの回答が画面tailより長ければ、`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。既存の人間向けcontentは診断suffixを維持し、機械呼出し側は`aiterm.pty-read-result.v1`の`structuredContent.text`から回答本文だけを取得する。Claudeはlaunch相関Stop hook、Codexは通常rollout、Grokは最後の実user行以後の最後の空でないassistantメッセージだけ、Cursorはlaunch IDでbindした通常agent transcriptから同じturnを回収する。回答を空で終えたturnは誤りにせず、`text`が空文字列、`answer_empty`が`true`で返すので、回答を読めなかった時と見分けられる。回答の場所を記録から見つけられない時だけ誤りにする。
|
|
652
655
|
|
|
653
656
|
### 完了検出(5 層)
|
|
654
657
|
|
package/README.md
CHANGED
|
@@ -223,7 +223,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
223
223
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
224
224
|
Release re-registers the Official MCP Registry entry.
|
|
225
225
|
|
|
226
|
-
**Status:** actively maintained · current public release **v0.
|
|
226
|
+
**Status:** actively maintained · current public release **v0.55.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).
|
|
227
227
|
|
|
228
228
|
### Update and rollback
|
|
229
229
|
|
|
@@ -583,7 +583,10 @@ waits up to 50 seconds from startup. If nothing is drawn by then, it still retur
|
|
|
583
583
|
A prompt-free launch without this option retains `startup.status="not_checked"`. `initial_prompt.status` distinguishes
|
|
584
584
|
`not_requested`, `not_sent`, `submitted_unconfirmed`, and `started`. Failure responses retain structured session information.
|
|
585
585
|
WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
|
|
586
|
-
|
|
586
|
+
`submitted_unconfirmed` (the prompt left the composer, but the turn start was not observed within the confirmation window) is
|
|
587
|
+
not a tool error: `isError` is not set, the text carries an unconfirmed-start note, the session is alive and the parent
|
|
588
|
+
delivery stays registered. Do not resend an unconfirmed prompt or relaunch the agent; observe with `pty_observe` or wait
|
|
589
|
+
using its returned cursor.
|
|
587
590
|
|
|
588
591
|
For a live Codex approval, inspect with `agent_approval({ action: "inspect", session_id })`, review `prompt` and `choices`,
|
|
589
592
|
then respond with `observed_prompt_digest` and `approval_choice` (`approve_once` or `deny`). Unknown or changed dialogs return
|
|
@@ -689,7 +692,7 @@ unchanged. Optional `throughline_supplement_file` is passed unchanged to Through
|
|
|
689
692
|
`throughline_source_session` and Throughline 0.10.8 or later; Aiterm does not read or classify the supplement. Throughline is resolved through `THROUGHLINE_BIN` and then `PATH`; a missing or invalid
|
|
690
693
|
export fails before the PTY exists instead of silently launching clean.
|
|
691
694
|
|
|
692
|
-
When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. The existing human-readable content keeps its diagnostic suffix; machine callers read the answer alone from `structuredContent.text` in `aiterm.pty-read-result.v1`. Claude reads the bounded owner-only result captured by the launch-correlated Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. Codex uses the normal rollout transcript's `task_complete.turn_id`; Grok returns the last non-empty assistant message after the last real user row, excluding tool-use preambles; Cursor uses the normal agent transcript bound to the launch ID and current turn. Missing or ambiguous attribution remains an explicit error.
|
|
695
|
+
When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. The existing human-readable content keeps its diagnostic suffix; machine callers read the answer alone from `structuredContent.text` in `aiterm.pty-read-result.v1`. Claude reads the bounded owner-only result captured by the launch-correlated Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. Codex uses the normal rollout transcript's `task_complete.turn_id`; Grok returns the last non-empty assistant message after the last real user row, excluding tool-use preambles; Cursor uses the normal agent transcript bound to the launch ID and current turn. A turn that completed with an empty answer is not an error: `text` is the empty string and `answer_empty` is `true`, so callers can tell it apart from an answer that could not be read. Missing or ambiguous attribution, including an answer that cannot be located in the record, remains an explicit error.
|
|
693
696
|
|
|
694
697
|
### Completion detection (5 layers)
|
|
695
698
|
|
package/dist/core.js
CHANGED
|
@@ -2681,6 +2681,8 @@ function agentCompletionCursor(meta) {
|
|
|
2681
2681
|
function transcriptUnavailable() {
|
|
2682
2682
|
throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${agentWaitGuide()}`, 2);
|
|
2683
2683
|
}
|
|
2684
|
+
/** 完了したturnの回答が空だった時に、人とAIが読む表示へ出す文。機械はstructuredContentのanswer_emptyを見る。 */
|
|
2685
|
+
const AGENT_ANSWER_EMPTY_NOTICE = "このターンは完了しましたが、回答の本文は空でした。";
|
|
2684
2686
|
function transcriptNotFound(vendor) {
|
|
2685
2687
|
throw new AitermError(`最終 assistant メッセージを特定できませんでした(harness=${agentHarness(vendor)}, vendor=${vendor})。screen で確認してください。`, 2);
|
|
2686
2688
|
}
|
|
@@ -2763,7 +2765,8 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2763
2765
|
throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${agentWaitGuide(name)}`, 2);
|
|
2764
2766
|
}
|
|
2765
2767
|
const turnId = o.completion?.turn_id ?? done?.turn_id ?? null;
|
|
2766
|
-
|
|
2768
|
+
// 各harnessの読み取りは、回答の場所を見つけたら文字列(空でもよい)、見つからなければnullを返す。
|
|
2769
|
+
let text;
|
|
2767
2770
|
if (meta.kind === "claude") {
|
|
2768
2771
|
if (!done)
|
|
2769
2772
|
transcriptUnavailable();
|
|
@@ -2780,12 +2783,16 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2780
2783
|
transcriptUnavailable();
|
|
2781
2784
|
text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable, o.completion?.turn_id);
|
|
2782
2785
|
}
|
|
2783
|
-
|
|
2786
|
+
// 回答の場所が見つからない時だけ誤りにする。場所があって中身が空白だけのturnは「回答が空」として返す。
|
|
2787
|
+
if (text === null)
|
|
2784
2788
|
transcriptNotFound(meta.kind);
|
|
2785
|
-
|
|
2789
|
+
const answerEmpty = !text.trim();
|
|
2790
|
+
if (answerEmpty)
|
|
2791
|
+
text = "";
|
|
2792
|
+
else if (o.lines != null)
|
|
2786
2793
|
text = text.split("\n").slice(-o.lines).join("\n");
|
|
2787
2794
|
const rawChars = text.length;
|
|
2788
|
-
const [body, outputMeta] = o.raw ? [text, ""] : reduceOutput(text, name, true);
|
|
2795
|
+
const [body, outputMeta] = answerEmpty || o.raw ? [text, ""] : reduceOutput(text, name, true);
|
|
2789
2796
|
const transcriptMeta = [
|
|
2790
2797
|
"agent_transcript",
|
|
2791
2798
|
`vendor=${meta.kind}`,
|
|
@@ -2793,14 +2800,16 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2793
2800
|
`harness=${agentHarness(meta.kind)}`,
|
|
2794
2801
|
done?.operation_id ? `operation_id=${done.operation_id}` : null,
|
|
2795
2802
|
`raw_chars=${rawChars}`,
|
|
2803
|
+
answerEmpty ? "answer_empty=true" : null,
|
|
2796
2804
|
].filter(Boolean).join(" ");
|
|
2797
2805
|
return {
|
|
2798
2806
|
text: body,
|
|
2799
|
-
display: `${body}\n${outputMeta} [${transcriptMeta}]`,
|
|
2807
|
+
display: `${answerEmpty ? AGENT_ANSWER_EMPTY_NOTICE : body}\n${outputMeta} [${transcriptMeta}]`,
|
|
2800
2808
|
vendor: meta.kind,
|
|
2801
2809
|
turn_id: turnId,
|
|
2802
2810
|
harness: agentHarness(meta.kind),
|
|
2803
2811
|
raw_chars: rawChars,
|
|
2812
|
+
answer_empty: answerEmpty,
|
|
2804
2813
|
};
|
|
2805
2814
|
}
|
|
2806
2815
|
/** 人間向け互換表示を維持する。機械利用は readAgentTranscriptResult の text を使う。 */
|
|
@@ -3314,6 +3323,20 @@ async function waitCursorPromptVisibleImpl(text, sample, sleepFn, opts = {}) {
|
|
|
3314
3323
|
async function waitCursorPromptVisible(name, text) {
|
|
3315
3324
|
return waitCursorPromptVisibleImpl(text, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep);
|
|
3316
3325
|
}
|
|
3326
|
+
// 起動時promptは入力欄を離れたが、確認時間の内にturnの開始を見られなかった時の案内。この返りは誤り(isError)にしない。
|
|
3327
|
+
// 誤りで返すと、Claude Code親は受け口のhook(PostToolUse)を走らせず、登録済みの配送が届かない(ADR 0093)。
|
|
3328
|
+
// 親が同じ依頼で起動し直すと子が2つ動くので、再送も再起動もしない事を先に書く。
|
|
3329
|
+
export const INITIAL_PROMPT_UNCONFIRMED_MARK = "turnの開始は未確認";
|
|
3330
|
+
export function initialPromptUnconfirmedNote(name, reason) {
|
|
3331
|
+
return (`注意: ${INITIAL_PROMPT_UNCONFIRMED_MARK}(reason=${reason})。promptは入力欄を離れているので、再送もagentの起動し直しもしない。` +
|
|
3332
|
+
`開始していれば結果は上の方法で届く。確かめる時は pty_observe(${name}) で状態を見る。`);
|
|
3333
|
+
}
|
|
3334
|
+
// 起動そのものは普通に返り、初手の開始だけが未確認の返りか。送信の途中で失敗した起動も同じstatusを持つ
|
|
3335
|
+
// (その時のstartup.reasonは失敗の理由になる)ので、起動が普通に返った形(ready/composer_ready)と組で見る。
|
|
3336
|
+
export function launchStartUnconfirmed(structured) {
|
|
3337
|
+
return structured.initial_prompt?.status === "submitted_unconfirmed"
|
|
3338
|
+
&& structured.startup?.status === "ready" && structured.startup?.reason === "composer_ready";
|
|
3339
|
+
}
|
|
3317
3340
|
export function agentSubmitResidueWarning(name, residue) {
|
|
3318
3341
|
if (residue !== true)
|
|
3319
3342
|
return "";
|
|
@@ -3613,6 +3636,7 @@ async function sendInitialAgentPromptInLock(name, text, o) {
|
|
|
3613
3636
|
return {
|
|
3614
3637
|
text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset} harness=${agentHarness(meta.kind)}\n` +
|
|
3615
3638
|
`起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
|
|
3639
|
+
(delivery.status === "submitted_unconfirmed" ? `\n${initialPromptUnconfirmedNote(name, delivery.reason)}` : "") +
|
|
3616
3640
|
agentSubmitResidueWarning(name, residue.residue),
|
|
3617
3641
|
event_cursor: startOffset,
|
|
3618
3642
|
submit_residue: residue.residue,
|
package/dist/harnesses/claude.js
CHANGED
|
@@ -281,13 +281,42 @@ export function claudeTuiReady(screen) {
|
|
|
281
281
|
// その下に描画された現在のcomposerを優先する。
|
|
282
282
|
return !/^❯\s*(?:\d+\.|\[|Enable selected\b|No,\s*exit\b|Yes,\s*I trust this folder\b)/iu.test(lastMarker);
|
|
283
283
|
}
|
|
284
|
+
// Claude Codeは動いている間、入力欄の上へ進行行を出し続ける(2.1.291の実画面 2026-10-06)。
|
|
285
|
+
// ✻ Swirling… (running UserPromptSubmit hook · 0s)
|
|
286
|
+
// * Swirling…
|
|
287
|
+
// ✶ Slithering… (3s · ↓ 225 tokens · thought for 2s)
|
|
288
|
+
// ✢ コマンド実行中… (12s · ↓ 1.0k tokens · thinking) ← 作業の一覧を使う時は、進行中の項目の文になる
|
|
289
|
+
// ⎿ ◼ 下の命令を1回だけ動かす ← 一覧は進行行の下へ字下げで並ぶ
|
|
290
|
+
// 終わると同じ位置が「✻ Cogitated for 11s · done 4:45 AM」へ変わる(「…」が無く、「·」で区切る)。
|
|
291
|
+
const CLAUDE_PROGRESS_LINE_RE = /^[·✢✳✶✻✽*∗] [^·…\s][^·…]*…(?: \(.*\))?\s*$/u;
|
|
292
|
+
// 入力欄の上の罫線から上へ、空行と字下げの行を飛ばし、行頭から始まる最初の行が進行行かを見る。
|
|
293
|
+
// 会話欄の行(「●」の回答、「❯」の依頼文、字下げの道具の出力)は進行行の形にならない。
|
|
294
|
+
function claudeTurnProgress(screen) {
|
|
295
|
+
const lines = screen.split(/\r?\n/u);
|
|
296
|
+
let marker = lines.length - 1;
|
|
297
|
+
while (marker >= 0 && !CLAUDE_COMPOSER_MARKER_RE.test(lines[marker]))
|
|
298
|
+
marker--;
|
|
299
|
+
if (marker <= 0 || !/^\s*─{8,}\s*$/u.test(lines[marker - 1]))
|
|
300
|
+
return false;
|
|
301
|
+
for (let i = marker - 2; i >= 0; i--) {
|
|
302
|
+
if (lines[i].trim() === "" || /^\s/u.test(lines[i]))
|
|
303
|
+
continue;
|
|
304
|
+
return CLAUDE_PROGRESS_LINE_RE.test(lines[i]);
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
// 足元の「esc to interrupt」は、貼り付けの知らせ(paste again to expand)へ置き換わる間は出ない。複数行のpromptを
|
|
309
|
+
// 送った後の約8秒がそうで、その間は動いているのに入力待ちと読んでいた(2.1.291、2026-10-06)。進行行も動作中の印にする。
|
|
310
|
+
export function claudeTuiBusy(screen) {
|
|
311
|
+
return /esc to interrupt/i.test(screen.split("\n").slice(-32).join("\n")) || claudeTurnProgress(screen);
|
|
312
|
+
}
|
|
284
313
|
export function claudePaneObservation(screen) {
|
|
285
314
|
const tail = screen.split("\n").slice(-32).join("\n");
|
|
286
315
|
if (claudeLoginMethodMenu(screen))
|
|
287
316
|
return { state: "blocked", reason: "vendor_onboarding_required" };
|
|
288
317
|
if (/Do you want to proceed\?/.test(tail) && /(?:^|\n)\s*[❯>]?\s*\d+\.\s+(?:Yes|No)\s*$/m.test(tail))
|
|
289
318
|
return { state: "blocked", reason: "tool_approval" };
|
|
290
|
-
if (
|
|
319
|
+
if (claudeTuiBusy(screen))
|
|
291
320
|
return { state: "busy", reason: "turn_running" };
|
|
292
321
|
if (claudeTuiReady(screen))
|
|
293
322
|
return { state: "idle", reason: "composer_ready" };
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -618,6 +618,9 @@ export function codexMoreReasoningChoice(screen) {
|
|
|
618
618
|
return null;
|
|
619
619
|
}
|
|
620
620
|
// 回収対象turnの最終assistantメッセージをroot rollout transcriptから抽出する。
|
|
621
|
+
// 回答の場所が記録にあれば、中身が空でも文字列を返す。場所が見つからない時(記録の形が変わった時を含む)はnull。
|
|
622
|
+
// Codexは回答を空で終えたturnも、textが空のfinal_answerをturn ID付きで残す(0.160.1の実記録 2026-10-06。
|
|
623
|
+
// task_completeのlast_agent_messageはnull)。
|
|
621
624
|
export function codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, exactCompletion = false) {
|
|
622
625
|
if (!meta.codex_home || !meta.vendor_session_id)
|
|
623
626
|
transcriptUnavailable();
|
|
@@ -642,7 +645,7 @@ export function codexTranscriptText(meta, turnId, readTranscriptLines, transcrip
|
|
|
642
645
|
if (typeof payload.last_agent_message === "string")
|
|
643
646
|
return payload.last_agent_message;
|
|
644
647
|
const text = matching.text();
|
|
645
|
-
if (text)
|
|
648
|
+
if (text !== null)
|
|
646
649
|
return text;
|
|
647
650
|
transcriptUnavailable();
|
|
648
651
|
}
|
|
@@ -650,7 +653,7 @@ export function codexTranscriptText(meta, turnId, readTranscriptLines, transcrip
|
|
|
650
653
|
transcriptUnavailable();
|
|
651
654
|
}
|
|
652
655
|
const matching = codexTurnAnswer();
|
|
653
|
-
let finalAnswer =
|
|
656
|
+
let finalAnswer = null;
|
|
654
657
|
for (const line of lines) {
|
|
655
658
|
if (!line.trim())
|
|
656
659
|
continue;
|
|
@@ -670,7 +673,9 @@ export function codexTranscriptText(meta, turnId, readTranscriptLines, transcrip
|
|
|
670
673
|
finalAnswer = payload.message;
|
|
671
674
|
}
|
|
672
675
|
}
|
|
673
|
-
|
|
676
|
+
// turnの本文が記録にあれば、空でもそれが回答。agent_message(turn IDの無い旧形式)は別turnの物かもしれないので、
|
|
677
|
+
// turnの本文が見つからず、かつ空でない時だけ使う。
|
|
678
|
+
return matching.text() ?? (finalAnswer?.trim() ? finalAnswer : null);
|
|
674
679
|
}
|
|
675
680
|
// turnのassistant本文を集める。Codexは作業途中の報告をphase=commentary、回答をphase=final_answerで残すので、
|
|
676
681
|
// final_answerがあるturnは途中の報告を回答に含めない。phaseの無い旧形式はturnの本文を全部つなぐ。
|
|
@@ -691,7 +696,10 @@ function codexTurnAnswer() {
|
|
|
691
696
|
finals.push(item.text);
|
|
692
697
|
}
|
|
693
698
|
},
|
|
699
|
+
// turnのassistant本文が1つも無ければnull(回答の場所が見つからない)。
|
|
694
700
|
text() {
|
|
701
|
+
if (all.length === 0)
|
|
702
|
+
return null;
|
|
695
703
|
return (finals.length > 0 ? finals : all).join("\n");
|
|
696
704
|
},
|
|
697
705
|
};
|
package/dist/harnesses/cursor.js
CHANGED
|
@@ -245,6 +245,10 @@ export function cursorTranscriptText(meta, readTranscriptLines, transcriptUnavai
|
|
|
245
245
|
transcriptUnavailable();
|
|
246
246
|
// tool_useで区切った本文の塊を持つ。道具を呼ぶ前の文は作業途中の報告なので、回答は最後の塊だけにする。
|
|
247
247
|
let current = [[]];
|
|
248
|
+
// 回答の場所が分かるturnか。assistant行があり、そのpartがtextとtool_useだけの時に限る(実記録にあるのはこの2種類)。
|
|
249
|
+
// 知らないpartがあるturnは、本文がそこへ移ったのかもしれないので「回答が空」とは言わない。
|
|
250
|
+
let assistantRows = 0;
|
|
251
|
+
let unknownParts = false;
|
|
248
252
|
let completed = null;
|
|
249
253
|
for (const line of readTranscriptLines(transcript)) {
|
|
250
254
|
if (!line.trim())
|
|
@@ -256,22 +260,34 @@ export function cursorTranscriptText(meta, readTranscriptLines, transcriptUnavai
|
|
|
256
260
|
catch {
|
|
257
261
|
continue;
|
|
258
262
|
}
|
|
259
|
-
if (record?.role === "user")
|
|
263
|
+
if (record?.role === "user") {
|
|
260
264
|
current = [[]];
|
|
265
|
+
assistantRows = 0;
|
|
266
|
+
unknownParts = false;
|
|
267
|
+
}
|
|
261
268
|
if (record?.role === "assistant" && Array.isArray(record?.message?.content)) {
|
|
269
|
+
assistantRows += 1;
|
|
262
270
|
for (const part of record.message.content) {
|
|
263
271
|
if (part?.type === "text" && typeof part?.text === "string")
|
|
264
272
|
current.at(-1).push(part.text);
|
|
265
|
-
else if (part?.type === "tool_use"
|
|
266
|
-
current.
|
|
273
|
+
else if (part?.type === "tool_use") {
|
|
274
|
+
if (current.at(-1).length > 0)
|
|
275
|
+
current.push([]);
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
unknownParts = true;
|
|
267
279
|
}
|
|
268
280
|
}
|
|
269
|
-
if (record?.type === "turn_ended")
|
|
270
|
-
completed = current.map((chunk) => [...chunk]);
|
|
281
|
+
if (record?.type === "turn_ended") {
|
|
282
|
+
completed = { chunks: current.map((chunk) => [...chunk]), located: assistantRows > 0 && !unknownParts };
|
|
283
|
+
}
|
|
271
284
|
}
|
|
272
285
|
if (completed === null)
|
|
273
286
|
transcriptUnavailable();
|
|
274
|
-
|
|
287
|
+
const text = (completed.chunks.filter((chunk) => chunk.length > 0).at(-1) ?? []).join("\n");
|
|
288
|
+
if (text.trim())
|
|
289
|
+
return text;
|
|
290
|
+
return completed.located ? "" : null;
|
|
275
291
|
}
|
|
276
292
|
export function createCursorAgentMetadata(name, cwd, initialPrompt, writeScope, lineage) {
|
|
277
293
|
const launchId = randomBytes(16).toString("hex");
|
package/dist/harnesses/grok.js
CHANGED
|
@@ -500,9 +500,12 @@ export function grokTranscriptText(meta, readTranscriptLines, transcriptUnavaila
|
|
|
500
500
|
const replies = records
|
|
501
501
|
.slice(lastUser + 1, end)
|
|
502
502
|
.filter((record) => record?.type === "assistant" && typeof record?.content === "string")
|
|
503
|
-
.map((record) => record.content.trim())
|
|
504
|
-
|
|
505
|
-
|
|
503
|
+
.map((record) => record.content.trim());
|
|
504
|
+
// contentが文字列のassistant行が1つも無ければnull(回答の場所が見つからない)。
|
|
505
|
+
// 道具を呼ぶだけの行はcontentが空文字列で残るので、そういう行だけのturnは「回答が空」になる。
|
|
506
|
+
if (replies.length === 0)
|
|
507
|
+
return null;
|
|
508
|
+
return replies.filter(Boolean).at(-1) ?? "";
|
|
506
509
|
}
|
|
507
510
|
export function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath, writeScope, lineageContext) {
|
|
508
511
|
const launchId = randomBytes(16).toString("hex");
|
package/dist/index.js
CHANGED
|
@@ -413,6 +413,7 @@ registerRemoteAwareTool("pty_read", {
|
|
|
413
413
|
description: "セッションの出力をトークン削減して読む(既定は前回読取位置からの増分)。" +
|
|
414
414
|
"削減: 制御文字除去 / 反復圧縮 / head+tail 折りたたみ+復元ヒント+メタ併記。" +
|
|
415
415
|
"agent_transcript:true は agent session の直近完了ターンの最終 assistant メッセージを公開されたharness記録から平文で返す。" +
|
|
416
|
+
"回答が空で終わったターンは誤りにせず、空のtextとanswer_empty:trueを返す。" +
|
|
416
417
|
"長い回答が screen tail で切れた時の回収用。",
|
|
417
418
|
inputSchema: {
|
|
418
419
|
session_id: z.string(),
|
|
@@ -454,6 +455,12 @@ registerRemoteAwareTool("pty_read", {
|
|
|
454
455
|
turn_id: z.string().nullable(),
|
|
455
456
|
harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).nullable(),
|
|
456
457
|
raw_chars: z.number().int().nonnegative().nullable(),
|
|
458
|
+
// remote付きは現地の結果をそのまま返す。この項目が無い旧版(0.53.2以前)の結果も通すのでoptional。
|
|
459
|
+
answer_empty: z
|
|
460
|
+
.boolean()
|
|
461
|
+
.nullable()
|
|
462
|
+
.optional()
|
|
463
|
+
.describe("agent_transcriptで、完了したturnの回答が空だった時だけtrue(textは空文字列)。回答を読めなかった時は誤りを返す。terminalではnull"),
|
|
457
464
|
},
|
|
458
465
|
}, async ({ session_id, wait, until, until_regex, timeout, screen, full, lines, line_range, raw, rtk, agent_transcript, operation_id }) => {
|
|
459
466
|
try {
|
|
@@ -478,6 +485,7 @@ registerRemoteAwareTool("pty_read", {
|
|
|
478
485
|
turn_id: result.turn_id,
|
|
479
486
|
harness: result.harness,
|
|
480
487
|
raw_chars: result.raw_chars,
|
|
488
|
+
answer_empty: result.answer_empty,
|
|
481
489
|
},
|
|
482
490
|
};
|
|
483
491
|
}
|
|
@@ -521,6 +529,7 @@ registerRemoteAwareTool("pty_read", {
|
|
|
521
529
|
turn_id: null,
|
|
522
530
|
harness: null,
|
|
523
531
|
raw_chars: null,
|
|
532
|
+
answer_empty: null,
|
|
524
533
|
},
|
|
525
534
|
};
|
|
526
535
|
}
|
|
@@ -904,12 +913,17 @@ async function launchRemoteAgent(target, harness, args, extra) {
|
|
|
904
913
|
return fail(new Error(`別端末でagentは起動しましたが、回答配送を登録できませんでした(session_id=${structured.session_id}): ${e instanceof Error ? e.message : String(e)}`));
|
|
905
914
|
}
|
|
906
915
|
const waited = await remoteCompletionWait(delivery, target, structured.session_id, structured.event_cursor);
|
|
916
|
+
// 現地が開始未確認を誤りで返す版(0.54.0以前)でも、ここでは誤りにしない(ADR 0093)。案内が無ければ足す。
|
|
917
|
+
const unconfirmed = core.launchStartUnconfirmed(structured);
|
|
918
|
+
const noted = result.content.some(item => item.type === "text" && item.text.includes(core.INITIAL_PROMPT_UNCONFIRMED_MARK));
|
|
907
919
|
return {
|
|
908
920
|
content: [...result.content, remoteNote(target, result),
|
|
921
|
+
...(unconfirmed && !noted
|
|
922
|
+
? [{ type: "text", text: core.initialPromptUnconfirmedNote(structured.session_id, structured.initial_prompt.reason) }] : []),
|
|
909
923
|
{ type: "text", text: `以後このsessionを操作する時は、同じremoteを付けて呼ぶ。${deliveryIdLine(delivery)}` }],
|
|
910
924
|
structuredContent: { ...structured, ...waited, ...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
|
|
911
925
|
remote_host: remoteLabel(target), remote_version: result.remote_version },
|
|
912
|
-
...(result.isError ? { isError: true } : {}),
|
|
926
|
+
...(result.isError && !unconfirmed ? { isError: true } : {}),
|
|
913
927
|
};
|
|
914
928
|
}
|
|
915
929
|
async function launchAgent(kind, args, extra) {
|
|
@@ -958,10 +972,11 @@ async function launchAgent(kind, args, extra) {
|
|
|
958
972
|
}
|
|
959
973
|
: {}),
|
|
960
974
|
};
|
|
975
|
+
// 開始未確認(submitted_unconfirmed)は誤りにしない。sessionは立ち、promptは入力欄を離れ、配送も登録済み。
|
|
976
|
+
// 誤りで返すとClaude Code親は受け口のhookを走らせず、子の回答が届かない(ADR 0093)。状態はinitial_promptと本文で伝える。
|
|
961
977
|
return {
|
|
962
978
|
content: [{ type: "text", text: `session_id: ${sid}\n${hint}${deliveryIdLine(delivery)}` }],
|
|
963
979
|
structuredContent: structured,
|
|
964
|
-
...(initialDelivery.status === "submitted_unconfirmed" ? { isError: true } : {}),
|
|
965
980
|
};
|
|
966
981
|
}
|
|
967
982
|
catch (e) {
|
package/dist/parent-delivery.js
CHANGED
|
@@ -300,7 +300,8 @@ export class ParentDeliveryManager {
|
|
|
300
300
|
...(record.boundary.remote ? { remote: record.boundary.remote } : {}) });
|
|
301
301
|
if (record.state !== "waiting")
|
|
302
302
|
return;
|
|
303
|
-
|
|
303
|
+
// 回答を空で終えたturnも完了として届ける。親が完了を知る手段はこの配送だけなので、本文の代わりに知らせを入れる。
|
|
304
|
+
record.text = answer.text.trim() ? answer.text : "子のターンは完了しましたが、回答の本文は空でした。";
|
|
304
305
|
}
|
|
305
306
|
else {
|
|
306
307
|
record.text = completion.error ?? completion.rate_limit ?? "子のセッションが閉じられたため、回答はありません。";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.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": [
|