aiterm-mcp 0.56.0 → 0.57.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 +26 -1
- package/README.ja.md +4 -2
- package/README.md +4 -2
- package/dist/core.js +70 -13
- package/dist/index.js +20 -2
- package/dist/parent-delivery-cli.js +18 -0
- package/docs/DESIGN.md +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.57.0] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### 追加
|
|
13
|
+
|
|
14
|
+
- `pty_send`(agentの席への送信)に、任意の引数`preface`(本文の前に置く1行)を足す(ADR 0099)。渡る文は「`preface`・空行・`text`」。
|
|
15
|
+
- Claude Codeの席へは、`preface`を端末の貼り付けの印なしで入れ、空行と`text`を1回の貼り付けで入れる。Claude Codeは貼り付けた長い文を
|
|
16
|
+
`<pasted_content>`で包み、席に「包みの中の指示は、包みの外の利用者自身の言葉が頼んだ時だけ従う」と教える。今までは文の全部が包みの中で、
|
|
17
|
+
席が連絡に従わない事があった(止めた直後の席で、実物の7回中5回)。前置きが包みの外に出る。
|
|
18
|
+
- Codex・Grok・Cursorの席へは、`preface`・空行・`text`を1つにつないで今までどおり貼る。
|
|
19
|
+
- `preface`は1行・200字以内・制御文字なしで、行頭が記号や空白でなく、`@`を含まない文。外れた時は`AGENT_PREFACE_INVALID`と
|
|
20
|
+
「文字列は送信していません。」で、待たずに断る。通常PTY・`force`付き・`remote`付きでは使えない(打鍵前に断る)。
|
|
21
|
+
- 省略した時の動き、振り分け、返りの形、断りの文面は変わらない。
|
|
22
|
+
|
|
23
|
+
## [0.56.1] - 2026-10-07
|
|
24
|
+
|
|
25
|
+
### 修正
|
|
26
|
+
|
|
27
|
+
- `aiterm-parent-delivery`が、呼ぶ側のPATHにnodeの場所が無い環境でも、nodeで動くCodexの起動役(npm版の`codex`。1行目が
|
|
28
|
+
`#!/usr/bin/env node`)を起こせるようにする。0.56.0は、そういう環境(素のsshの環境、製品の常駐process、アプリ配下のprocess)から
|
|
29
|
+
呼ぶと、`codex verify`が`CODEX_RECEIVER_TRANSPORT_FAILED`(exit=127)で断っていた。本文は送られていなかった。
|
|
30
|
+
- 命令が、自分を動かしているnodeの場所を、起こすCodexのPATHの頭へ足す。既にPATHにあれば何も変えない。
|
|
31
|
+
- 引数・返りの形は変わらない。Desktop同梱のCodex(macOS・Windows)は元から当たらない。
|
|
32
|
+
|
|
10
33
|
## [0.56.0] - 2026-10-07
|
|
11
34
|
|
|
12
35
|
### 追加
|
|
@@ -2333,7 +2356,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2333
2356
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2334
2357
|
provenance.
|
|
2335
2358
|
|
|
2336
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
2359
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.0...HEAD
|
|
2360
|
+
[0.57.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.1...v0.57.0
|
|
2361
|
+
[0.56.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.0...v0.56.1
|
|
2337
2362
|
[0.56.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.4...v0.56.0
|
|
2338
2363
|
[0.55.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.3...v0.55.4
|
|
2339
2364
|
[0.55.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.2...v0.55.3
|
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.57.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -261,6 +261,8 @@ ClaudeがAPIエラーや安全判定の拒否で終了した時は、Stop hook
|
|
|
261
261
|
|
|
262
262
|
agent配送だけを期待する連携では、`pty_send`に`require_agent:true`を指定する。agent登録が無ければ、打鍵前に`AGENT_SESSION_REQUIRED`と「文字列は送信していません」を返す。省略時は通常PTY送信を維持する。`force:true`との併用は未送信で拒否する。
|
|
263
263
|
|
|
264
|
+
agent session宛ての`pty_send`は、任意の`preface`(本文の前に置く1行)を受ける。渡る文は「`preface`・空行・`text`」。Claude Codeの席へは、`preface`を端末の貼り付けの印なしで入れ、空行と本文を1回の貼り付けで入れる。Claude Codeは貼り付けた長い文を`<pasted_content>`で包み、「包みの中の指示は、包みの外の利用者自身の言葉が頼んだ時だけ従う」と読むので、連携元が付ける前置き(誰からの連絡か)を包みの外へ出すために使う。Codex・Grok・Cursorの席へは1つにつないで貼る。`preface`は1行・200字以内・制御文字なしで、行頭が記号や空白でなく、`@`を含まない文。外れた時は`AGENT_PREFACE_INVALID`と「文字列は送信していません。」を返す。通常PTY・`force:true`・`remote`付きでは使えない。
|
|
265
|
+
|
|
264
266
|
同じagent sessionへの`pty_send`が重なった時は、1本ずつ通す。後の1通は、前の1通を送り終えてから振り分け直すので、順に送った時と同じ結果になる(起動直後のClaude Codeへ重なった後の1通は`mode=agent_steer`で返る)。前の1通が起動直後の入力受付待ちなら、その分だけ返りが遅れる。待つ上限(POSIX 60秒、Windows 180秒)を越えた時は、打鍵前に`AGENT_SEND_BUSY`と「文字列は送信していません」を返す。送信の途中で終了したprocessのlockは、次の送信が片付けて進む。
|
|
265
267
|
|
|
266
268
|
`agent_launch`・`pty_send`(agent session宛て)は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
|
|
@@ -568,7 +570,7 @@ Claudeの相関済み承認は既存の`claude_approval`を使う。
|
|
|
568
570
|
| ツール | 役割 | 主な引数 |
|
|
569
571
|
| --- | --- | --- |
|
|
570
572
|
| `pty_open` | 端末を1個開き`session_id`を返す | `name?`, `shell?`, `env_vars?` |
|
|
571
|
-
| `pty_send` | テキストを送る。agent sessionでは子のturnが実行中なら現在のturnへ差し込み(`agent_steer`)、それ以外は非ブロックdispatchとして`event_cursor`を返す(`agent_dispatch`)。差し込みでGrokが待ち行列へ入れない時とCursorの入力欄に残った時は失敗する | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `rtk`, `raw` |
|
|
573
|
+
| `pty_send` | テキストを送る。agent sessionでは子のturnが実行中なら現在のturnへ差し込み(`agent_steer`)、それ以外は非ブロックdispatchとして`event_cursor`を返す(`agent_dispatch`)。差し込みでGrokが待ち行列へ入れない時とCursorの入力欄に残った時は失敗する | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `preface`, `rtk`, `raw` |
|
|
572
574
|
| `pty_read` | 出力を削減して読む(既定は増分) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
|
|
573
575
|
| `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
|
|
574
576
|
| `pty_close` | 冪等に閉じ、`closed` / `already_closed`を返す | `session_id` |
|
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.57.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
|
|
|
@@ -282,6 +282,8 @@ The same primitive hosts another agent's TUI. `agent_launch` starts a selected e
|
|
|
282
282
|
|
|
283
283
|
Set `require_agent:true` on `pty_send` when the integration requires agent delivery. If the agent registration is missing, Aiterm refuses before sending any text and returns `AGENT_SESSION_REQUIRED` with an explicit unsent message. The default preserves ordinary PTY sends; combining it with `force:true` is rejected before sending.
|
|
284
284
|
|
|
285
|
+
`pty_send` to an agent session accepts an optional `preface`: one line placed before the text, so the message reads "`preface`, blank line, `text`". For a Claude Code session Aiterm enters the `preface` without the terminal's paste markers and pastes the blank line and the text in one paste. Claude Code wraps long pasted text in `<pasted_content>` and follows instructions inside it only where the user's own words outside the tags ask it to, so this keeps an integration's header (who the message is from) outside the wrapper. For Codex, Grok and Cursor sessions the three parts are joined and pasted as before. The `preface` must be a single line of at most 200 characters with no control characters, must not start with a symbol or whitespace, and must not contain `@`; otherwise Aiterm returns `AGENT_PREFACE_INVALID` and "文字列は送信していません。" before sending anything. It cannot be used with ordinary PTY sends, `force:true`, or `remote`.
|
|
286
|
+
|
|
285
287
|
Overlapping `pty_send` calls to the same agent session are handled one at a time. A later call picks its route only after the earlier one has been sent, so the result matches calls made in sequence (a call that overlaps the first send to a freshly started Claude Code session returns `mode=agent_steer`). It returns later by however long the earlier call waits for the session to accept input. If its turn does not come within the limit (60 seconds on POSIX, 180 seconds on Windows), Aiterm refuses before sending any text and returns `AGENT_SEND_BUSY` with an explicit unsent message. A lock left by a process that exited mid-send is cleared by the next send.
|
|
286
288
|
|
|
287
289
|
`agent_launch` and `pty_send` (to an agent session) accept an optional `image`: an array of absolute paths to image files (png/jpg/jpeg/gif/webp). Aiterm appends an attachment block to the prompt, and every harness opens the path with its own file-reading tool and sees the image; the caller never learns harness-specific attachment tricks. Invalid paths are rejected before anything is sent.
|
|
@@ -606,7 +608,7 @@ continue to use `claude_approval`.
|
|
|
606
608
|
| Tool | Role | Key args |
|
|
607
609
|
| --- | --- | --- |
|
|
608
610
|
| `pty_open` | Open one terminal and return a `session_id` | `name?`, `shell?`, `env_vars?` |
|
|
609
|
-
| `pty_send` | Send text. On an agent session Aiterm picks the route when it sends: if the child's turn is running, it steers the text into that turn (`agent_steer`); otherwise it is a non-blocking **dispatch** returning an `event_cursor` (`agent_dispatch`). Steering fails when Grok does not queue the text or Cursor leaves it in the composer | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `rtk`, `raw` |
|
|
611
|
+
| `pty_send` | Send text. On an agent session Aiterm picks the route when it sends: if the child's turn is running, it steers the text into that turn (`agent_steer`); otherwise it is a non-blocking **dispatch** returning an `event_cursor` (`agent_dispatch`). Steering fails when Grok does not queue the text or Cursor leaves it in the composer | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `preface`, `rtk`, `raw` |
|
|
610
612
|
| `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
|
|
611
613
|
| `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
|
|
612
614
|
| `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
|
package/dist/core.js
CHANGED
|
@@ -762,6 +762,38 @@ export function openSession(name, shell = "bash", envVars = []) {
|
|
|
762
762
|
}
|
|
763
763
|
return [nm, attachHint(nm)];
|
|
764
764
|
}
|
|
765
|
+
export const AGENT_PREFACE_MAX_CHARS = 200;
|
|
766
|
+
// 行頭のASCII記号と空白は、入力欄が命令として読む(Claude Codeの`/`・`!`・`#`・`?`など)。`@`は文中でも候補の枠を開く。
|
|
767
|
+
const AGENT_PREFACE_LEADING_SYMBOL_RE = /^[\s!-\/:-@\[-`{-~]/u;
|
|
768
|
+
const AGENT_PREFACE_CONTROL_RE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/u;
|
|
769
|
+
/**
|
|
770
|
+
* `pty_send`の`preface`(本文の前に置く1行)を、打鍵の前に確かめる。印なしで入れる文は入力欄がそのまま読むので、
|
|
771
|
+
* 入力欄が命令として読む形を通さない。外れた時は何も送らずに断る。
|
|
772
|
+
*/
|
|
773
|
+
export function assertAgentPreface(preface) {
|
|
774
|
+
const reason = typeof preface !== "string" || preface.length === 0 ? "空です"
|
|
775
|
+
: AGENT_PREFACE_CONTROL_RE.test(preface) ? "改行か制御文字を含んでいます"
|
|
776
|
+
: [...preface].length > AGENT_PREFACE_MAX_CHARS ? `${AGENT_PREFACE_MAX_CHARS}字を越えています`
|
|
777
|
+
: AGENT_PREFACE_LEADING_SYMBOL_RE.test(preface) ? "行頭が記号か空白です"
|
|
778
|
+
: /\s$/u.test(preface) ? "行末が空白です"
|
|
779
|
+
: preface.includes("@") ? "@ を含んでいます"
|
|
780
|
+
: null;
|
|
781
|
+
if (reason) {
|
|
782
|
+
throw new AitermError(`AGENT_PREFACE_INVALID: prefaceが${reason}。1行・${AGENT_PREFACE_MAX_CHARS}字以内・制御文字なしで、` +
|
|
783
|
+
"行頭が記号や空白でなく、@ を含まない文にしてください。文字列は送信していません。", 2);
|
|
784
|
+
}
|
|
785
|
+
return preface;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* 前置きつきの送信文を、harnessごとの形へ分ける。Claude Codeは前置きを印なしで入れ、空行と本文を貼る。
|
|
789
|
+
* Codex・Grok・Cursorは貼り付けを包まないので、前置き・空行・本文を1つにつないで貼る(渡る文は前置きなしの時と同じ並び)。
|
|
790
|
+
*/
|
|
791
|
+
function splitAgentPreface(kind, preface, text) {
|
|
792
|
+
if (preface == null)
|
|
793
|
+
return { body: text };
|
|
794
|
+
assertAgentPreface(preface);
|
|
795
|
+
return kind === "claude" ? { typedPreface: preface, body: `\n\n${text}` } : { body: `${preface}\n\n${text}` };
|
|
796
|
+
}
|
|
765
797
|
function prepareSendText(text, o) {
|
|
766
798
|
if (!o.raw)
|
|
767
799
|
text = text.replace(PASTE_MARKERS_RE, "").replace(ANSI_RE, "").replace(CTRL_RE, "");
|
|
@@ -823,17 +855,18 @@ export function send(name, text, o = {}) {
|
|
|
823
855
|
}
|
|
824
856
|
// POSIX tmuxはUTF-8安全な256byte pasteへ分割する。Windows psmuxは認証済み
|
|
825
857
|
// server commandでpayload全体を単一SendBytesへ変換し、別processとの交差を構造的に防ぐ。
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
858
|
+
const writePty = (payload, bracketedPaste, what) => {
|
|
859
|
+
if (isWin) {
|
|
860
|
+
const sent = sendPsmuxPayload(true, name, payload, bracketedPaste);
|
|
861
|
+
if (sent.code !== 0) {
|
|
862
|
+
throw new AitermError(`psmuxのPTY送信に失敗しました${what}: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
830
865
|
}
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
866
|
// tmuxの`paste-buffer -p`はchunkごとに包むため使わない。Claude Codeは貼り付け1回ごとに
|
|
834
867
|
// 画像pathを探すので、chunk境界でpathが切れて画像を落とし、行もずれた(2026-10-01、
|
|
835
868
|
// 画像4枚付きの約500byte)。全体を1回だけ包み、中身はchunkのまま流す。
|
|
836
|
-
const chunks = splitPtyText(
|
|
869
|
+
const chunks = splitPtyText(bracketedPaste ? `\x1b[200~${payload}\x1b[201~` : payload);
|
|
837
870
|
const pasteSupportsNoSanitize = pasteBufferSupportsNoSanitizeFlag();
|
|
838
871
|
const bufferBase = `aiterm-${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
839
872
|
for (let i = 0; i < chunks.length; i += 1) {
|
|
@@ -844,7 +877,7 @@ export function send(name, text, o = {}) {
|
|
|
844
877
|
const loaded = loadPtyBufferChunk(true, bufferName, chunks[i]);
|
|
845
878
|
if (loaded.code !== 0) {
|
|
846
879
|
tmuxCleanup("delete-buffer", "-b", bufferName);
|
|
847
|
-
throw new AitermError(`tmux buffer
|
|
880
|
+
throw new AitermError(`tmux bufferへの送信準備に失敗しました${what}` +
|
|
848
881
|
`(chunk ${i + 1}/${chunks.length}): ${loaded.stderr.trim() || `code=${loaded.code}`}.${partial}`, 2);
|
|
849
882
|
}
|
|
850
883
|
const pasteArgs = pasteBufferBaseArgs();
|
|
@@ -854,14 +887,30 @@ export function send(name, text, o = {}) {
|
|
|
854
887
|
const pasted = tmux(...pasteArgs);
|
|
855
888
|
if (pasted.code !== 0) {
|
|
856
889
|
tmuxCleanup("delete-buffer", "-b", bufferName);
|
|
857
|
-
throw new AitermError(`tmux bufferのPTY
|
|
890
|
+
throw new AitermError(`tmux bufferのPTY送信に失敗しました${what}` +
|
|
858
891
|
`(chunk ${i + 1}/${chunks.length}): ${pasted.stderr.trim() || `code=${pasted.code}`}.${partial}`, 2);
|
|
859
892
|
}
|
|
860
893
|
if (i + 1 < chunks.length) {
|
|
861
894
|
Atomics.wait(PTY_PASTE_PAUSE_BUFFER, 0, 0, PTY_PASTE_CHUNK_PAUSE_MS);
|
|
862
895
|
}
|
|
863
896
|
}
|
|
897
|
+
};
|
|
898
|
+
if (o.typedPreface !== undefined) {
|
|
899
|
+
// 前置きは貼り付けの印なしで入れる。本文は続けて貼る。前置きだけが入って本文が失敗した時は、入力欄に前置きが残る。
|
|
900
|
+
writePty(o.typedPreface, false, "(前置き)");
|
|
901
|
+
Atomics.wait(PTY_PASTE_PAUSE_BUFFER, 0, 0, PTY_PASTE_CHUNK_PAUSE_MS);
|
|
902
|
+
try {
|
|
903
|
+
writePty(text, !!o.bracketedPaste, "");
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
if (error instanceof AitermError) {
|
|
907
|
+
throw new AitermError(`${error.message} 前置きはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。`, error.code);
|
|
908
|
+
}
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
864
911
|
}
|
|
912
|
+
else
|
|
913
|
+
writePty(text, !!o.bracketedPaste, "");
|
|
865
914
|
if (enter) {
|
|
866
915
|
const entered = tmux("send-keys", "-t", name, "Enter");
|
|
867
916
|
if (entered.code !== 0) {
|
|
@@ -4259,9 +4308,10 @@ async function dispatchAgentTurnInLock(name, text, o) {
|
|
|
4259
4308
|
const startOffset = agentCompletionCursor(meta);
|
|
4260
4309
|
// promptなしで起動したCursorは、最初のdispatch時点ではtranscriptとの相関markerをまだ持たない。
|
|
4261
4310
|
// その1回だけlaunch contextを加え、以後はbind済みconversationへ通常textだけを送る。
|
|
4311
|
+
const prefaced = splitAgentPreface(meta.kind, o.preface, text);
|
|
4262
4312
|
const dispatchText = meta.kind === "cursor" && !meta.vendor_session_id
|
|
4263
|
-
? `${subagentInstruction(meta)}\n\n${
|
|
4264
|
-
:
|
|
4313
|
+
? `${subagentInstruction(meta)}\n\n${prefaced.body}`
|
|
4314
|
+
: prefaced.body;
|
|
4265
4315
|
prepareSendText(dispatchText, { raw: o.raw });
|
|
4266
4316
|
await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
|
|
4267
4317
|
harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
|
|
@@ -4284,6 +4334,7 @@ async function dispatchAgentTurnInLock(name, text, o) {
|
|
|
4284
4334
|
rtk: false,
|
|
4285
4335
|
preserveAgentOperation: meta.kind === "claude",
|
|
4286
4336
|
bracketedPaste: true,
|
|
4337
|
+
typedPreface: prefaced.typedPreface,
|
|
4287
4338
|
});
|
|
4288
4339
|
// Cursorの冷間起動ではbracketed pasteの反映が250msを超えることがある。本文がcomposerへ
|
|
4289
4340
|
// 現れた実測をsubmit条件にし、未反映のままEnterだけを失う競合を作らない。
|
|
@@ -4354,6 +4405,9 @@ export function __testSteerQueued(kind, screen) {
|
|
|
4354
4405
|
export async function sendAgentMessage(name, text, o = {}) {
|
|
4355
4406
|
assertSessionName(name);
|
|
4356
4407
|
loadAgentMetadata(name);
|
|
4408
|
+
// 前置きは、順番待ちにも入力受付の待ちにも入る前に確かめる(外れた時に待たせない)。
|
|
4409
|
+
if (o.preface != null)
|
|
4410
|
+
assertAgentPreface(o.preface);
|
|
4357
4411
|
return withAgentSendLock(name, () => sendAgentMessageInLock(name, text, o));
|
|
4358
4412
|
}
|
|
4359
4413
|
async function sendAgentMessageInLock(name, text, o) {
|
|
@@ -4374,9 +4428,9 @@ async function sendAgentMessageInLock(name, text, o) {
|
|
|
4374
4428
|
if (running && meta.kind === "cursor" && latestCursorCompletion(meta, readTranscriptLines) !== null)
|
|
4375
4429
|
running = false;
|
|
4376
4430
|
if (running) {
|
|
4377
|
-
return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery });
|
|
4431
|
+
return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery, preface: o.preface });
|
|
4378
4432
|
}
|
|
4379
|
-
return dispatchAgentTurnInLock(name, text, { raw: o.raw, before_send: o.before_send, pane_input_recovery: paneInputRecovery });
|
|
4433
|
+
return dispatchAgentTurnInLock(name, text, { raw: o.raw, before_send: o.before_send, pane_input_recovery: paneInputRecovery, preface: o.preface });
|
|
4380
4434
|
}
|
|
4381
4435
|
/**
|
|
4382
4436
|
* 実行中turnへ追加の指示を渡す。各harnessの標準操作だけを使い、完了境界は1つに保つ。
|
|
@@ -4395,6 +4449,8 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
4395
4449
|
harness: agentHarness(meta.kind),
|
|
4396
4450
|
pane_input_recovery: o.pane_input_recovery,
|
|
4397
4451
|
};
|
|
4452
|
+
const prefaced = splitAgentPreface(meta.kind, o.preface, text);
|
|
4453
|
+
text = prefaced.body;
|
|
4398
4454
|
prepareSendText(text, { raw: o.raw });
|
|
4399
4455
|
// Claudeのoperation相関は変えない。差し込みは実行中turnの一部であり、新しいturnを予約しない。
|
|
4400
4456
|
const preserveAgentOperation = meta.kind === "claude";
|
|
@@ -4406,6 +4462,7 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
4406
4462
|
rtk: false,
|
|
4407
4463
|
preserveAgentOperation,
|
|
4408
4464
|
bracketedPaste: true,
|
|
4465
|
+
typedPreface: prefaced.typedPreface,
|
|
4409
4466
|
});
|
|
4410
4467
|
if (meta.kind === "cursor") {
|
|
4411
4468
|
const visible = await waitCursorPromptVisible(name, text);
|
package/dist/index.js
CHANGED
|
@@ -209,6 +209,9 @@ function remoteToolHandler(name) {
|
|
|
209
209
|
async function sendRemote(target, args, extra) {
|
|
210
210
|
if (args.image && args.image.length > 0)
|
|
211
211
|
throw new core.AitermError("REMOTE_IMAGE_UNSUPPORTED: 別端末への画像添付には未対応です", 2);
|
|
212
|
+
// 現地が古い版だと、知らない引数として前置きだけが黙って落ちる。届く形を確かめられるまで通さない。
|
|
213
|
+
if (args.preface !== undefined)
|
|
214
|
+
throw new core.AitermError("REMOTE_PREFACE_UNSUPPORTED: 別端末へのprefaceには未対応です。文字列は送信していません。", 2);
|
|
212
215
|
const key = deliveryKey(args.session_id, target);
|
|
213
216
|
const delivery = args.force ? null : await deliveryForRequest(extra, true);
|
|
214
217
|
// 終わった依頼の回答は送る前に保存する。実行中の依頼は残る。現地が差し込みを選べばその依頼が回答を受け取る。
|
|
@@ -289,6 +292,8 @@ registerRemoteAwareTool("pty_send", {
|
|
|
289
292
|
NON_BLOCKING_RULE +
|
|
290
293
|
"自動配送以外の結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
|
|
291
294
|
"require_agent:true はagent登録が無いsessionへ打鍵せず、AGENT_SESSION_REQUIREDで未送信を返す。" +
|
|
295
|
+
"preface はagent sessionへの送信で本文の前に置く1行。Claude Codeの席では貼り付けの印なしで入れ、本文だけを貼る" +
|
|
296
|
+
"(前置きが貼り付けの包みの外へ出る)。他のharnessでは前置き・空行・本文を1つで貼る。" +
|
|
292
297
|
"force:true は非Claude agent sessionへの手動介入用の素送信。aiterm相関付きClaudeの承認UIはclaude_approvalを使う。",
|
|
293
298
|
inputSchema: {
|
|
294
299
|
session_id: z.string(),
|
|
@@ -308,6 +313,13 @@ registerRemoteAwareTool("pty_send", {
|
|
|
308
313
|
.default(false)
|
|
309
314
|
.describe("非Claude agent sessionでは自動dispatchせず素送信する。aiterm相関付きClaudeのactive turnには使えない"),
|
|
310
315
|
require_agent: z.boolean().default(false).describe("agent登録が無いsessionは打鍵前に拒否する。force:trueとは併用できない"),
|
|
316
|
+
preface: z
|
|
317
|
+
.string()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe("agent session への送信(forceなし)で、本文の前に置く1行。送る文は「preface・空行・text」の並びになる。" +
|
|
320
|
+
"Claude Codeの席ではprefaceを貼り付けの印なしで入れ、textだけを貼る。Codex/Grok/Cursorの席では1つで貼る。" +
|
|
321
|
+
"1行・200字以内・制御文字なしで、行頭が記号や空白でなく、@ を含まない文。外れた時と通常PTY送信・force送信・remote付きでは、" +
|
|
322
|
+
"打鍵前に拒否する"),
|
|
311
323
|
rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
|
|
312
324
|
raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
|
|
313
325
|
image: z
|
|
@@ -332,7 +344,7 @@ registerRemoteAwareTool("pty_send", {
|
|
|
332
344
|
// dispatch前に行った pane 入力の回復("fg" / "fg_stopped" / "stty_raw")。通常送信では省略(additive)。
|
|
333
345
|
pane_input_recovery: z.array(z.string()).optional(),
|
|
334
346
|
},
|
|
335
|
-
}, async ({ session_id, text, enter, mark, force, require_agent, rtk, raw, image }, extra) => {
|
|
347
|
+
}, async ({ session_id, text, enter, mark, force, require_agent, rtk, raw, image, preface }, extra) => {
|
|
336
348
|
let delivery = null;
|
|
337
349
|
try {
|
|
338
350
|
if (require_agent && force) {
|
|
@@ -342,6 +354,12 @@ registerRemoteAwareTool("pty_send", {
|
|
|
342
354
|
if (require_agent && !agentSession) {
|
|
343
355
|
throw new Error(`AGENT_SESSION_REQUIRED: session '${session_id}' のagent登録がありません。文字列は送信していません。`);
|
|
344
356
|
}
|
|
357
|
+
if (preface !== undefined) {
|
|
358
|
+
if (!agentSession) {
|
|
359
|
+
throw new Error("preface は agent session への送信(forceなし)だけで使えます。文字列は送信していません。");
|
|
360
|
+
}
|
|
361
|
+
core.assertAgentPreface(preface);
|
|
362
|
+
}
|
|
345
363
|
if (agentSession) {
|
|
346
364
|
if (enter === false)
|
|
347
365
|
throw new Error("agent session への dispatch は enter:false と併用できません(手動介入は force:true)");
|
|
@@ -350,7 +368,7 @@ registerRemoteAwareTool("pty_send", {
|
|
|
350
368
|
if (rtk)
|
|
351
369
|
throw new Error("agent session への dispatch は rtk:true と併用できません");
|
|
352
370
|
delivery = await deliveryForRequest(extra);
|
|
353
|
-
const receipt = await core.sendAgentMessage(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send });
|
|
371
|
+
const receipt = await core.sendAgentMessage(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send, preface });
|
|
354
372
|
if (receipt.schema === "aiterm.agent-steer.v1") {
|
|
355
373
|
return {
|
|
356
374
|
content: [
|
|
@@ -119,6 +119,22 @@ async function deliveryState(target, delivery) {
|
|
|
119
119
|
return { state, hook, turn_id: turn, queued: null, queue_error: typeof code === "string" ? code : "CODEX_QUEUE_UNREADABLE" };
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* 起こすCodexが、この命令を動かしているnodeを見つけられるPATHを返す。
|
|
124
|
+
* npm版のCodexは`#!/usr/bin/env node`の起動役で、呼ぶ側のPATHにnodeの場所が無いと起きない(exit 127)。製品の常駐processや
|
|
125
|
+
* アプリ配下のprocessは、PATHが細い事がある(2026-10-07、素のsshの環境からの呼び出しで起きた)。
|
|
126
|
+
* nodeの場所がPATHに無い時だけ頭へ足す。既にあれば、並びも中身も変えない。
|
|
127
|
+
*/
|
|
128
|
+
export function pathWithNode(env, node = process.execPath, platform = process.platform) {
|
|
129
|
+
const windows = platform === "win32";
|
|
130
|
+
// Windowsの環境変数名は大文字小文字を区別しない(`Path`で入っている事が多い)。
|
|
131
|
+
const key = Object.keys(env).find(name => windows ? name.toLowerCase() === "path" : name === "PATH") ?? "PATH";
|
|
132
|
+
const delimiter = windows ? ";" : ":";
|
|
133
|
+
const directory = (windows ? path.win32 : path.posix).dirname(node);
|
|
134
|
+
const current = env[key] ?? "";
|
|
135
|
+
const listed = current.split(delimiter).some(part => windows ? part.toLowerCase() === directory.toLowerCase() : part === directory);
|
|
136
|
+
return { key, value: listed ? current : current ? `${directory}${delimiter}${current}` : directory };
|
|
137
|
+
}
|
|
122
138
|
/** この命令の場所と版。製品側の入口(aiterm-steer-delivery)が、呼べる相手かどうかを確かめるのに使う。 */
|
|
123
139
|
export function providerDescription() {
|
|
124
140
|
return { version: pkg.version, node: process.execPath, cli: fileURLToPath(import.meta.url) };
|
|
@@ -170,6 +186,8 @@ function isDirectExecution() {
|
|
|
170
186
|
}
|
|
171
187
|
if (isDirectExecution()) {
|
|
172
188
|
try {
|
|
189
|
+
const found = pathWithNode(process.env);
|
|
190
|
+
process.env[found.key] = found.value;
|
|
173
191
|
process.stdout.write(JSON.stringify({ ok: true, schema: PARENT_DELIVERY_SCHEMA, ...(await runParentDelivery(process.argv.slice(2))) }) + "\n");
|
|
174
192
|
}
|
|
175
193
|
catch (error) {
|
package/docs/DESIGN.md
CHANGED
|
@@ -57,6 +57,11 @@ agent配送だけを期待する連携元は`pty_send(require_agent:true)`を指
|
|
|
57
57
|
`AGENT_SESSION_REQUIRED`と「文字列は送信していません」を返す。未送信を確定してから対象sessionを復旧できる。
|
|
58
58
|
省略時の通常PTY送信は維持し、`force:true`との併用は打鍵前に拒否する。
|
|
59
59
|
|
|
60
|
+
agentの席への`pty_send`は、任意の`preface`(本文の前に置く1行)を受ける(ADR 0099)。Claude Codeの席へは、`preface`を貼り付けの印なしで入れ、
|
|
61
|
+
空行と本文を1回の貼り付けで入れる。貼り付けた長い文をClaude Codeが`<pasted_content>`で包んでも、前置きは包みの外に出る。
|
|
62
|
+
ほかのharnessの席へは1つにつないで貼る。`preface`は入力欄がそのまま読むので、1行・200字以内・制御文字なし・行頭が記号や空白でない・
|
|
63
|
+
`@`を含まない文だけを通し、外れた時は打鍵前に断る。
|
|
64
|
+
|
|
60
65
|
`pty_observe`は存在、pane/harnessの生存、画面状態と理由、native process identityを分ける。
|
|
61
66
|
PIDは開始識別子・argv digestと組にし、paneとharnessを同一視しない。特定できないidentityはnull。
|
|
62
67
|
同じlaunchに属するnpm shimとnative本体は、中間の非候補processも含めた祖先関係から一つの起動として扱う。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.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": [
|