aiterm-mcp 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +6 -5
- package/README.md +9 -4
- package/dist/core.js +171 -0
- package/dist/index.js +54 -3
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
**言葉でなく実測で:** このリポジトリ自身の 203 テストで、`pty_read` はコンテキストに載るトークンを生ログの **約 7.1 分の 1** に減らす。しかも pass/fail の判定は畳んでも残る。→ [組み込みシェルツールとの使い分け](#組み込みシェルツールとの使い分け)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
13 ツール: 6 つの **PTY ツール**(`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list`)で 1 本の永続端末を開き・操作し・読む。加えて 4 つの **エージェント起動ツール**(`claude_agent` / `codex_agent` / `grok_agent` / `composer_agent`)が別のコーディングエージェントの TUI を新しい端末の中に起動し、`claude_turn`がdurable caller向けの構造化issue/recoveryを、`claude_approval`がmanaged Claudeの相関済み承認UI中継を、`diagnostics`が安全なfactory readinessを返す。バックエンドは **tmux** なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
|
|
26
26
|
|
|
27
27
|
**v0.18.1 を 2026-07-18 に公開**(0.18.0+stale案内文言の修理1件)。実運用障害の還流による agent dispatch の hardening:
|
|
28
28
|
全 dispatch/launch receipt に **submit 座礁観測** `submit_residue` を追加
|
|
@@ -82,7 +82,7 @@ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeo
|
|
|
82
82
|
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
83
83
|
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
84
84
|
|
|
85
|
-
各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claudeは通常settingsを継承しないlaunch専用settingsとStop hookを使い、本文なしeventとowner-only bounded resultを分離する。`pty_read({ agent_transcript:true })`はdigestとbyte数を検証したresultだけを返し、Claude private transcriptを読まない。後着resultは同じsessionからprompt再送なしで回収できる。managed Claude
|
|
85
|
+
各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claudeは通常settingsを継承しないlaunch専用settingsとStop hookを使い、本文なしeventとowner-only bounded resultを分離する。`pty_read({ agent_transcript:true })`はdigestとbyte数を検証したresultだけを返し、Claude private transcriptを読まない。後着resultは同じsessionからprompt再送なしで回収できる。managed Claudeのactive turn中はC-c以外の`pty_key`と素送信を拒否する。Claudeが`Do you want to proceed?`を表示したら、`claude_approval(action:"inspect", ...)`で画面digestを取得し、表示内容を判断してから、そのdigestと`approve_once`または`deny`を`respond`へ渡す。同じoperation・同じ画面が維持されている時だけ入力し、任意文字列や恒久許可選択肢は中継しない。中断は`C-c`、解除は`pty_close`。
|
|
86
86
|
|
|
87
87
|
エージェント間の隠れたプロトコルは無い。起動したClaude/Codex/Grok/Composerは利用者がattachできるもう1本の永続sessionであり、MCPクライアントが通常のPTY操作で駆動する。
|
|
88
88
|
|
|
@@ -145,7 +145,7 @@ claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
|
145
145
|
Claude Code を再起動して、接続を確認:
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
|
-
/mcp # aiterm が connected・
|
|
148
|
+
/mcp # aiterm が connected・13 ツール公開、と出る
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
最初のセッション——4 回の呼び出しで、1 個の永続端末:
|
|
@@ -186,7 +186,7 @@ MCP クライアントが aiterm を stdio 越しにプログラムから駆動
|
|
|
186
186
|
|
|
187
187
|
```mermaid
|
|
188
188
|
flowchart LR
|
|
189
|
-
AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · claude_agent · claude_turn · codex_agent<br/>grok_agent · composer_agent · diagnostics"| S["aiterm-mcp<br/>stdio MCP ·
|
|
189
|
+
AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · claude_agent · claude_turn · claude_approval · codex_agent<br/>grok_agent · composer_agent · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 13 tools"]
|
|
190
190
|
S -->|"pty_read<br/>token-reduced"| AI
|
|
191
191
|
S -->|"tmux send-keys<br/>capture-pane"| P["persistent PTYs<br/>tmux · survive restarts"]
|
|
192
192
|
P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
|
|
@@ -262,12 +262,13 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
262
262
|
| ツール | 役割 | 主な引数 |
|
|
263
263
|
| --- | --- | --- |
|
|
264
264
|
| `pty_open` | 端末を 1 個握り `session_id` を返す | `name?`, `shell="bash"` |
|
|
265
|
-
| `pty_send` |
|
|
265
|
+
| `pty_send` | テキストを送る。agent sessionでは非ブロックdispatchとして`event_cursor`を返す | `session_id`, `text`, `enter=true`, `mark`, `force`, `rtk`, `raw` |
|
|
266
266
|
| `pty_read` | 出力を削減して読む(既定は増分) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
|
|
267
267
|
| `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
|
|
268
268
|
| `pty_close` | 冪等に閉じ、`closed` / `already_closed`を返す | `session_id` |
|
|
269
269
|
| `pty_list` | セッション一覧 | (なし) |
|
|
270
270
|
| `claude_turn` | 相関済みmanaged Claude operationをdispatch(issue)または回収(recover) | `action`, `session_id`, `operation_id`, `text?` |
|
|
271
|
+
| `claude_approval` | 現在表示中の相関済みmanaged Claude承認UIを検査または応答 | `action`, `session_id`, `operation_id?`, `approval_choice?`, `observed_prompt_digest?` |
|
|
271
272
|
| `diagnostics` | 機械可読 JSON による read-only factory readiness | (なし) |
|
|
272
273
|
|
|
273
274
|
`diagnostics` は PTY やエージェントを起動しない。パッケージ版、MCP 呼出 readiness、read-only な PTY 一覧要約、bounded runtime-error-store status、任意 vendor launcher の可用性だけを返す。path・環境値・認証情報・コマンド本文・PTY 出力・raw log は意図的に返さない。通常未設定の任意依存は `not_applicable`、安全に確定できない状態は `unverified` と表す。
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
**Measured, not claimed:** on this repo's own 203-test suite, a `pty_read` puts **~7.1× fewer tokens** in your context than the raw log — and the pass/fail verdict survives the fold. → [When to reach for it vs. the built-in shell](#when-to-reach-for-it-vs-the-built-in-shell)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Thirteen tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal, four **agent launchers** — `claude_agent` / `codex_agent` / `grok_agent` / `composer_agent` — that each start another coding agent's TUI inside a fresh one, `claude_turn` for durable structured issue/recovery, `claude_approval` for correlated managed-Claude approval prompts, and `diagnostics` for safe factory readiness. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
|
|
26
26
|
|
|
27
27
|
**v0.18.1 was published on 2026-07-18** (v0.18.0 plus one stale-guidance
|
|
28
28
|
message fix). Field-failure hardening for agent
|
|
@@ -75,6 +75,8 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
|
|
|
75
75
|
|
|
76
76
|
The same primitive hosts another agent's TUI. Four launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. Their existing human-readable text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt, so durable callers never parse display text for the session handle; when the launch carries an initial `prompt`, the receipt also includes the `event_cursor`, a ready-made `wait_command` for the completion waiter, and a `submit_residue` observation (`true` = the prompt is likely still sitting unsubmitted in the composer — the hint explains recovery; `false` = no residue observed, not a proof of submission; `null` = not applicable). From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) Every launch is **managed**: aiterm installs its own Stop hook, so turn completion is a first-class event. Sending to an agent session is a non-blocking **dispatch** — the call returns immediately with an `event_cursor`, and completion arrives via [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). Durable machine callers use `claude_turn({ action: "issue" | "recover", session_id, operation_id, ... })`: it returns fixed `accepted` / `pending` / `completed` / `unknown` states without parsing human-facing errors, never resends during recovery, and includes exact `raw_output` only for a verified completion. The same operation ID is carried through the dispatch receipt, active marker, Stop event, and result. The ordinary `pty_send` / `pty_read` surface remains available for interactive callers and humans. `C-c` keeps the marker for a delayed Stop; if no Stop arrives, close the session. An initial `prompt` on `claude_agent`/`codex_agent` is submitted through the same ready gate and the launcher returns without waiting; on Grok/Composer it is passed on the CLI's argv. This needs the vendor's own CLI installed and authenticated — see [Requirements](#requirements).
|
|
77
77
|
|
|
78
|
+
For a managed Claude turn stopped at `Do you want to proceed?`, use `claude_approval(action: "inspect", ...)` to capture the active operation and SHA-256 screen digest, review the displayed command, then call `respond` with that exact digest and either `approve_once` or `deny`. The relay rechecks the operation and screen under the send lock, never exposes arbitrary input or permanent approval, keeps the active marker intact, and records a prompt-free owner-only receipt. `pty_send(force: true)` does not bypass this boundary.
|
|
79
|
+
|
|
78
80
|
```text
|
|
79
81
|
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
80
82
|
prompt: "port test/legacy.py to vitest" })
|
|
@@ -95,7 +97,9 @@ One call per model, so the tool name itself tells you which model you get:
|
|
|
95
97
|
| `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?` |
|
|
96
98
|
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?` |
|
|
97
99
|
|
|
98
|
-
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/claude` / `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: empty `model` values and unsupported effort values are rejected up front; a missing CLI binary or a nonexistent `cwd` fails for all four. A rejected launch leaves **zero leftover session** behind. Claude and Codex launchers forward `model` and `reasoning_effort` through their vendor CLI's public flags; Grok/Composer reject `reasoning_effort` because it is headless-only. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless Claude launch exactly replayable by passing an explicit `session_name` and a `launch_operation_id` formatted as `sha256:<64 lowercase hex>`. Repeating the identical launch returns the same structured session receipt without starting the CLI twice; a different correlation ID or launch argument for that session fails explicitly. Claude uses launch-local managed settings containing only aiterm's Stop hook: normal user/project/local hooks are not inherited, the hook event contains no answer body, and the bounded owner-only result is returned by `pty_read({ agent_transcript:true })` without reading Claude's private transcript. A late result remains recoverable from the same session without re-sending the prompt.
|
|
100
|
+
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/claude` / `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: empty `model` values and unsupported effort values are rejected up front; a missing CLI binary or a nonexistent `cwd` fails for all four. A rejected launch leaves **zero leftover session** behind. Claude and Codex launchers forward `model` and `reasoning_effort` through their vendor CLI's public flags; Grok/Composer reject `reasoning_effort` because it is headless-only. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless Claude launch exactly replayable by passing an explicit `session_name` and a `launch_operation_id` formatted as `sha256:<64 lowercase hex>`. Repeating the identical launch returns the same structured session receipt without starting the CLI twice; a different correlation ID or launch argument for that session fails explicitly. Claude uses launch-local managed settings containing only aiterm's Stop hook: normal user/project/local hooks are not inherited, the hook event contains no answer body, and the bounded owner-only result is returned by `pty_read({ agent_transcript:true })` without reading Claude's private transcript. A late result remains recoverable from the same session without re-sending the prompt. While a managed Claude turn is active, raw sends and non-interrupt keys are rejected. If Claude displays `Do you want to proceed?`, call `claude_approval(action:"inspect", ...)`, decide from the visible prompt, then call `respond` with the returned digest and either `approve_once` or `deny`. The response is accepted only while the same operation and screen digest remain current; arbitrary text and persistent-allow choices are never relayed. Use `pty_key("C-c")` to interrupt and `pty_close` to abandon the session. For unconstrained manual key-by-key driving, open a plain `pty_open` session and start the vendor CLI yourself. Codex uses a managed `CODEX_HOME`; Grok/Composer isolate their managed homes and pass validated OAuth state through `GROK_AUTH_PATH`. Before the first unbound dispatch, aiterm waits for the vendor TUI's input prompt and fails before sending if it is not ready. Managed completion requires POSIX filesystem semantics (Linux, WSL2, macOS).
|
|
101
|
+
|
|
102
|
+
The managed Codex home links authentication, privately snapshots `config.toml` and `agents/*.toml` custom-role definitions, and keeps sessions/caches isolated. A symlinked role definition is resolved into a regular-file snapshot rather than shared with the source home.
|
|
99
103
|
|
|
100
104
|
There is no hidden protocol between agents: a launched Claude, Codex, Grok, or Composer is another user-visible persistent terminal session. The MCP client drives that TUI with ordinary PTY operations, and a human can attach to watch or take over.
|
|
101
105
|
|
|
@@ -158,7 +162,7 @@ claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
|
158
162
|
Restart Claude Code, then verify the connection:
|
|
159
163
|
|
|
160
164
|
```bash
|
|
161
|
-
/mcp # aiterm should show as connected, exposing
|
|
165
|
+
/mcp # aiterm should show as connected, exposing 13 tools
|
|
162
166
|
```
|
|
163
167
|
|
|
164
168
|
Your first session — four calls, one persistent terminal:
|
|
@@ -199,7 +203,7 @@ The terminal is real and shared, so a human *can* jump in ([A human can watch](#
|
|
|
199
203
|
|
|
200
204
|
```mermaid
|
|
201
205
|
flowchart LR
|
|
202
|
-
AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · claude_agent · claude_turn · codex_agent<br/>grok_agent · composer_agent · diagnostics"| S["aiterm-mcp<br/>stdio MCP ·
|
|
206
|
+
AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · claude_agent · claude_turn · claude_approval · codex_agent<br/>grok_agent · composer_agent · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 13 tools"]
|
|
203
207
|
S -->|"pty_read<br/>token-reduced"| AI
|
|
204
208
|
S -->|"tmux send-keys<br/>capture-pane"| P["persistent PTYs<br/>tmux · survive restarts"]
|
|
205
209
|
P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
|
|
@@ -283,6 +287,7 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
|
|
|
283
287
|
| `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
|
|
284
288
|
| `pty_list` | List sessions (agent rows carry `agent=<kind>` metadata) | (none) |
|
|
285
289
|
| `claude_turn` | Issue (dispatch-only) or recover one correlated managed-Claude operation | `action`, `session_id`, `operation_id`, `text?` |
|
|
290
|
+
| `claude_approval` | Inspect or answer the current correlated managed-Claude approval prompt | `action`, `session_id`, `operation_id?`, `approval_choice?`, `observed_prompt_digest?` |
|
|
286
291
|
| `diagnostics` | Read-only factory readiness as machine-readable JSON | (none) |
|
|
287
292
|
|
|
288
293
|
`diagnostics` never starts a PTY or agent. It reports package version, MCP call readiness, a read-only PTY-list summary, bounded runtime-error-store status, and optional vendor-launcher availability. It deliberately excludes paths, environment values, credentials, command text, PTY output, and raw logs; normal unset optional dependencies are `not_applicable`, while an indeterminate probe is `unverified`.
|
package/dist/core.js
CHANGED
|
@@ -56,6 +56,7 @@ const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
|
|
|
56
56
|
const AGENT_TUI_READY_POLL_MS = 500;
|
|
57
57
|
const AGENT_TUI_READY_STABLE_SAMPLES = 11;
|
|
58
58
|
const AGENT_TUI_READY_LINES = 45;
|
|
59
|
+
const CLAUDE_APPROVAL_SCREEN_LINES = 80;
|
|
59
60
|
// submit座礁観測(dispatch後にcomposerへ送信textが残存していないかの有界チェック)
|
|
60
61
|
const AGENT_SUBMIT_RESIDUE_DELAY_MS = 250;
|
|
61
62
|
const AGENT_SUBMIT_RESIDUE_POLL_MS = 300;
|
|
@@ -404,6 +405,12 @@ function agentClaudeOperationPath(name, launchId) {
|
|
|
404
405
|
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
405
406
|
return path.join(agentsDir(), `${name}.${launchId}.claude-operation.json`);
|
|
406
407
|
}
|
|
408
|
+
function agentClaudeApprovalReceiptPath(name, launchId) {
|
|
409
|
+
assertSessionName(name);
|
|
410
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
411
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
412
|
+
return path.join(agentsDir(), `${name}.${launchId}.claude-approval.json`);
|
|
413
|
+
}
|
|
407
414
|
function agentClaudeDispatchReceiptPath(name, launchId, operationId) {
|
|
408
415
|
assertSessionName(name);
|
|
409
416
|
if (!LAUNCH_ID_RE.test(launchId))
|
|
@@ -470,6 +477,7 @@ function cleanupAgentState(name) {
|
|
|
470
477
|
f.endsWith(".claude-settings.json") ||
|
|
471
478
|
f.endsWith(".claude-result.json") ||
|
|
472
479
|
f.endsWith(".claude-operation.json") ||
|
|
480
|
+
f.endsWith(".claude-approval.json") ||
|
|
473
481
|
f.endsWith(".claude-dispatch"))
|
|
474
482
|
fs.unlinkSync(p);
|
|
475
483
|
else if (f.endsWith(".codex-home") || f.endsWith(".grok-home") || f.endsWith(".home")) {
|
|
@@ -1505,6 +1513,40 @@ function managedCodexConfigSummary(configPath, hookTrustBypass) {
|
|
|
1505
1513
|
bits.push("hook trust bypass 有効");
|
|
1506
1514
|
return `managed config: ${bits.join(" / ")}`;
|
|
1507
1515
|
}
|
|
1516
|
+
// Custom agent definitions are configuration, not mutable Codex state. Copy only direct
|
|
1517
|
+
// agents/*.toml entries into the per-launch home so role discovery matches the source home while
|
|
1518
|
+
// sessions/cache remain isolated. Source symlinks are resolved and their contents are snapshotted;
|
|
1519
|
+
// the managed home never points back to the source definition.
|
|
1520
|
+
function snapshotCodexAgentDefinitions(srcHome, managedHome) {
|
|
1521
|
+
const srcDir = path.join(srcHome, "agents");
|
|
1522
|
+
let srcDirSt;
|
|
1523
|
+
try {
|
|
1524
|
+
srcDirSt = fs.statSync(srcDir);
|
|
1525
|
+
}
|
|
1526
|
+
catch (e) {
|
|
1527
|
+
if (e.code === "ENOENT")
|
|
1528
|
+
return;
|
|
1529
|
+
throw e;
|
|
1530
|
+
}
|
|
1531
|
+
if (!srcDirSt.isDirectory()) {
|
|
1532
|
+
throw new AitermError(`Codex agents が directory ではありません: ${srcDir}`, 2);
|
|
1533
|
+
}
|
|
1534
|
+
const names = fs.readdirSync(srcDir).filter((name) => name.endsWith(".toml")).sort();
|
|
1535
|
+
if (!names.length)
|
|
1536
|
+
return;
|
|
1537
|
+
const dstDir = path.join(managedHome, "agents");
|
|
1538
|
+
fs.mkdirSync(dstDir, { mode: 0o700 });
|
|
1539
|
+
fs.chmodSync(dstDir, 0o700);
|
|
1540
|
+
for (const name of names) {
|
|
1541
|
+
const src = path.join(srcDir, name);
|
|
1542
|
+
const resolved = fs.realpathSync(src);
|
|
1543
|
+
const st = fs.statSync(resolved);
|
|
1544
|
+
if (!st.isFile()) {
|
|
1545
|
+
throw new AitermError(`Codex agent definition が通常ファイルではありません: ${src}`, 2);
|
|
1546
|
+
}
|
|
1547
|
+
writeText0600(path.join(dstDir, name), fs.readFileSync(resolved, "utf8"));
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1508
1550
|
function createManagedCodexHome(name, launchId, overrides = {}) {
|
|
1509
1551
|
const srcHome = realCodexHome();
|
|
1510
1552
|
let srcSt;
|
|
@@ -1549,6 +1591,7 @@ function createManagedCodexHome(name, launchId, overrides = {}) {
|
|
|
1549
1591
|
fs.writeFileSync(configDst, configOut, { mode: 0o600 });
|
|
1550
1592
|
fs.chmodSync(configDst, 0o600);
|
|
1551
1593
|
}
|
|
1594
|
+
snapshotCodexAgentDefinitions(srcHome, managedHome);
|
|
1552
1595
|
const hookScript = codexHookScriptPath();
|
|
1553
1596
|
if (!fs.existsSync(hookScript)) {
|
|
1554
1597
|
throw new AitermError(`Codex Stop hook wrapper が見つかりません。npm run build を実行してください: ${hookScript}`, 2);
|
|
@@ -1828,6 +1871,134 @@ function managedClaudeOperation(name) {
|
|
|
1828
1871
|
return undefined;
|
|
1829
1872
|
return readClaudeOperationMarker(meta);
|
|
1830
1873
|
}
|
|
1874
|
+
function canonicalClaudeApprovalScreen(screen) {
|
|
1875
|
+
return stripControl(screen)
|
|
1876
|
+
.split("\n")
|
|
1877
|
+
.map((line) => line.replace(/^\s*[❯>]\s*/, "").replace(/\s+$/, ""))
|
|
1878
|
+
.join("\n")
|
|
1879
|
+
.trim();
|
|
1880
|
+
}
|
|
1881
|
+
function parseClaudeApprovalScreen(screen) {
|
|
1882
|
+
const canonical = canonicalClaudeApprovalScreen(screen);
|
|
1883
|
+
const lines = canonical.split("\n");
|
|
1884
|
+
let question = -1;
|
|
1885
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1886
|
+
if (lines[i].trim() === "Do you want to proceed?")
|
|
1887
|
+
question = i;
|
|
1888
|
+
}
|
|
1889
|
+
if (question < 0) {
|
|
1890
|
+
throw new AitermError("managed Claudeの承認UIを現在画面で確認できません(Do you want to proceed? がありません)", 2);
|
|
1891
|
+
}
|
|
1892
|
+
const choices = [];
|
|
1893
|
+
const seen = new Set();
|
|
1894
|
+
for (const line of lines.slice(question + 1)) {
|
|
1895
|
+
const match = line.trim().match(/^(\d+)\.\s+(.+?)\s*$/);
|
|
1896
|
+
if (!match)
|
|
1897
|
+
continue;
|
|
1898
|
+
const index = Number(match[1]);
|
|
1899
|
+
const label = match[2];
|
|
1900
|
+
const decision = /^yes$/i.test(label)
|
|
1901
|
+
? "approve_once"
|
|
1902
|
+
: /^no$/i.test(label)
|
|
1903
|
+
? "deny"
|
|
1904
|
+
: null;
|
|
1905
|
+
// 「常に許可」等は意図的に公開しない。単発Yes/No以外を自動操作できる契約にしない。
|
|
1906
|
+
if (!decision)
|
|
1907
|
+
continue;
|
|
1908
|
+
if (!Number.isSafeInteger(index) || index < 1 || seen.has(decision)) {
|
|
1909
|
+
throw new AitermError("managed Claudeの承認UI選択肢が一意に解釈できません", 2);
|
|
1910
|
+
}
|
|
1911
|
+
seen.add(decision);
|
|
1912
|
+
choices.push({ decision, index, label });
|
|
1913
|
+
}
|
|
1914
|
+
if (!seen.has("approve_once") || !seen.has("deny")) {
|
|
1915
|
+
throw new AitermError("managed Claudeの承認UIに安全な単発Yes/No選択肢を確認できません", 2);
|
|
1916
|
+
}
|
|
1917
|
+
return {
|
|
1918
|
+
promptDigest: `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`,
|
|
1919
|
+
choices,
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
function assertExpectedClaudeOperation(meta, expectedOperationId) {
|
|
1923
|
+
const active = readClaudeOperationMarker(meta);
|
|
1924
|
+
if (!active)
|
|
1925
|
+
throw new AitermError("managed Claudeに未解決のactive operationがありません", 2);
|
|
1926
|
+
if (active.operationId !== expectedOperationId) {
|
|
1927
|
+
const actual = active.operationId ?? "operation_idなし";
|
|
1928
|
+
const expected = expectedOperationId ?? "operation_idなし";
|
|
1929
|
+
throw new AitermError(`active operationが一致しません(expected=${expected}, actual=${actual})`, 2);
|
|
1930
|
+
}
|
|
1931
|
+
return active;
|
|
1932
|
+
}
|
|
1933
|
+
export function runClaudeApproval({ action, session_id: name, operation_id: operationIdInput, approval_choice: approvalChoice, observed_prompt_digest: observedPromptDigest, }) {
|
|
1934
|
+
assertSessionName(name);
|
|
1935
|
+
if (!sessionExists(name))
|
|
1936
|
+
throw new AitermError(`session '${name}' が無い`, 2);
|
|
1937
|
+
const meta = loadAgentMetadata(name);
|
|
1938
|
+
if (meta.kind !== "claude")
|
|
1939
|
+
throw new AitermError("claude_approvalはmanaged Claude agent sessionだけで使用できます", 2);
|
|
1940
|
+
const operationId = operationIdInput == null ? null : validateOperationId(operationIdInput);
|
|
1941
|
+
if (action === "inspect") {
|
|
1942
|
+
if (approvalChoice != null || observedPromptDigest != null) {
|
|
1943
|
+
throw new AitermError("claude_approval inspectにapproval_choice/observed_prompt_digestは指定できません", 2);
|
|
1944
|
+
}
|
|
1945
|
+
assertExpectedClaudeOperation(meta, operationId);
|
|
1946
|
+
const observed = parseClaudeApprovalScreen(captureScreen(name, CLAUDE_APPROVAL_SCREEN_LINES));
|
|
1947
|
+
return {
|
|
1948
|
+
schema: "aiterm.claude-approval-result.v1",
|
|
1949
|
+
action,
|
|
1950
|
+
status: "approval_required",
|
|
1951
|
+
session_id: name,
|
|
1952
|
+
operation_id: operationId,
|
|
1953
|
+
prompt_digest: observed.promptDigest,
|
|
1954
|
+
choices: observed.choices,
|
|
1955
|
+
selected_choice: null,
|
|
1956
|
+
at: new Date().toISOString(),
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
if (action !== "respond")
|
|
1960
|
+
throw new AitermError(`claude_approval actionが不正です: ${action}`, 2);
|
|
1961
|
+
if (approvalChoice == null || observedPromptDigest == null) {
|
|
1962
|
+
throw new AitermError("claude_approval respondにはapproval_choiceとobserved_prompt_digestが必要です", 2);
|
|
1963
|
+
}
|
|
1964
|
+
if (!OPERATION_ID_RE.test(observedPromptDigest)) {
|
|
1965
|
+
throw new AitermError("observed_prompt_digestはsha256:<64 lowercase hex>で指定してください", 2);
|
|
1966
|
+
}
|
|
1967
|
+
const releaseSendLock = acquireSessionSendFileLock(name);
|
|
1968
|
+
try {
|
|
1969
|
+
// inspect後にoperationまたは画面が変わっていないことを、入力と同じsend lock内で再検証する。
|
|
1970
|
+
assertExpectedClaudeOperation(meta, operationId);
|
|
1971
|
+
const observed = parseClaudeApprovalScreen(captureScreen(name, CLAUDE_APPROVAL_SCREEN_LINES));
|
|
1972
|
+
if (observed.promptDigest !== observedPromptDigest) {
|
|
1973
|
+
throw new AitermError("承認UIがinspect後に変化しました。再度inspectしてから判断してください", 2);
|
|
1974
|
+
}
|
|
1975
|
+
const choice = observed.choices.find((entry) => entry.decision === approvalChoice);
|
|
1976
|
+
if (!choice)
|
|
1977
|
+
throw new AitermError(`承認UIに${approvalChoice}の安全な選択肢がありません`, 2);
|
|
1978
|
+
const sent = tmux("send-keys", "-t", name, String(choice.index), "Enter");
|
|
1979
|
+
if (sent.code !== 0) {
|
|
1980
|
+
throw new AitermError(`Claude承認入力を送れませんでした: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
|
|
1981
|
+
}
|
|
1982
|
+
const at = new Date().toISOString();
|
|
1983
|
+
const result = {
|
|
1984
|
+
schema: "aiterm.claude-approval-result.v1",
|
|
1985
|
+
action,
|
|
1986
|
+
status: "submitted",
|
|
1987
|
+
session_id: name,
|
|
1988
|
+
operation_id: operationId,
|
|
1989
|
+
prompt_digest: observed.promptDigest,
|
|
1990
|
+
choices: observed.choices,
|
|
1991
|
+
selected_choice: approvalChoice,
|
|
1992
|
+
at,
|
|
1993
|
+
};
|
|
1994
|
+
// prompt本文は保存せず、相関ID・digest・選択だけをowner-only receiptへ残す。
|
|
1995
|
+
writeJson0600(agentClaudeApprovalReceiptPath(name, meta.launch_id), result);
|
|
1996
|
+
return result;
|
|
1997
|
+
}
|
|
1998
|
+
finally {
|
|
1999
|
+
releaseSendLock();
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1831
2002
|
function hasClaudeDispatchReceipt(meta, operationId) {
|
|
1832
2003
|
const file = agentClaudeDispatchReceiptPath(meta.aiterm_session, meta.launch_id, operationId);
|
|
1833
2004
|
let st;
|
package/dist/index.js
CHANGED
|
@@ -92,7 +92,7 @@ server.registerTool("pty_send", {
|
|
|
92
92
|
"ホストのバックグラウンドタスクとして実行し、exit時にreceiptのoutcomeで判定する" +
|
|
93
93
|
`(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
|
|
94
94
|
"結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
|
|
95
|
-
"force:true
|
|
95
|
+
"force:true は非Claude agent sessionへの手動介入用の素送信。managed Claudeの承認UIはclaude_approvalを使う。",
|
|
96
96
|
inputSchema: {
|
|
97
97
|
session_id: z.string(),
|
|
98
98
|
text: z
|
|
@@ -108,7 +108,7 @@ server.registerTool("pty_send", {
|
|
|
108
108
|
force: z
|
|
109
109
|
.boolean()
|
|
110
110
|
.default(false)
|
|
111
|
-
.describe("
|
|
111
|
+
.describe("破壊的コマンドゲートを越える。非Claude agent sessionではdispatchせず素送信する。managed Claudeのactive turnには使えない"),
|
|
112
112
|
rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
|
|
113
113
|
raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
|
|
114
114
|
},
|
|
@@ -255,7 +255,7 @@ server.registerTool("pty_read", {
|
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
server.registerTool("pty_key", {
|
|
258
|
-
description: "制御キーを送る(C-c, C-d, Enter, Tab, Up, Down... の別名に対応)。managed Claude sessionではturn相関を守るためC-c
|
|
258
|
+
description: "制御キーを送る(C-c, C-d, Enter, Tab, Up, Down... の別名に対応)。managed Claude sessionではturn相関を守るためC-cだけを許可し、承認UIはclaude_approvalで操作する。",
|
|
259
259
|
inputSchema: {
|
|
260
260
|
session_id: z.string(),
|
|
261
261
|
key: z.string().describe('キー名(例 "C-c", "Enter", "Up")'),
|
|
@@ -338,6 +338,57 @@ server.registerTool("claude_turn", {
|
|
|
338
338
|
return fail(e);
|
|
339
339
|
}
|
|
340
340
|
});
|
|
341
|
+
server.registerTool("claude_approval", {
|
|
342
|
+
description: "managed Claudeのactive turn中に表示された権限確認UIを、turn相関を保ったまま検査・応答する専用面。" +
|
|
343
|
+
"inspectで画面digestと安全な単発Yes/Noだけを取得し、respondは同じoperation・同じdigestが現在も表示中の場合だけ送信する。",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
action: z.enum(["inspect", "respond"]),
|
|
346
|
+
session_id: z.string(),
|
|
347
|
+
operation_id: z
|
|
348
|
+
.string()
|
|
349
|
+
.regex(/^sha256:[0-9a-f]{64}$/)
|
|
350
|
+
.nullish()
|
|
351
|
+
.describe("durable operationのID。通常pty_send由来の匿名turnでは省略する"),
|
|
352
|
+
approval_choice: z.enum(["approve_once", "deny"]).optional().describe("respondだけに指定する"),
|
|
353
|
+
observed_prompt_digest: z
|
|
354
|
+
.string()
|
|
355
|
+
.regex(/^sha256:[0-9a-f]{64}$/)
|
|
356
|
+
.optional()
|
|
357
|
+
.describe("直前のinspectが返したdigest。respondだけに指定する"),
|
|
358
|
+
},
|
|
359
|
+
outputSchema: {
|
|
360
|
+
schema: z.literal("aiterm.claude-approval-result.v1"),
|
|
361
|
+
action: z.enum(["inspect", "respond"]),
|
|
362
|
+
status: z.enum(["approval_required", "submitted"]),
|
|
363
|
+
session_id: z.string(),
|
|
364
|
+
operation_id: z.string().regex(/^sha256:[0-9a-f]{64}$/).nullable(),
|
|
365
|
+
prompt_digest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
366
|
+
choices: z.array(z.object({
|
|
367
|
+
decision: z.enum(["approve_once", "deny"]),
|
|
368
|
+
index: z.number().int().positive(),
|
|
369
|
+
label: z.string(),
|
|
370
|
+
})),
|
|
371
|
+
selected_choice: z.enum(["approve_once", "deny"]).nullable(),
|
|
372
|
+
at: z.string(),
|
|
373
|
+
},
|
|
374
|
+
}, async ({ action, session_id, operation_id, approval_choice, observed_prompt_digest }) => {
|
|
375
|
+
try {
|
|
376
|
+
const result = core.runClaudeApproval({
|
|
377
|
+
action,
|
|
378
|
+
session_id,
|
|
379
|
+
operation_id: operation_id ?? null,
|
|
380
|
+
approval_choice,
|
|
381
|
+
observed_prompt_digest,
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
385
|
+
structuredContent: { ...result },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
return fail(e);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
341
392
|
// 対話型エージェント起動ツール(モデルごとに1つ=ツール名/説明でどのモデルか一目で分かる)。
|
|
342
393
|
// いずれも永続端末に TUI を起動し session_id を返す。以後 pty_read/pty_send で対話操作する。
|
|
343
394
|
const agentModelDesc = (kind) => kind === "claude"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"mcpName": "io.github.kitepon-rgb/aiterm-mcp",
|
|
5
5
|
"description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Also launches interactive Claude/Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
|
|
6
6
|
"keywords": [
|