aiterm-mcp 0.12.1 → 0.12.2

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 CHANGED
@@ -22,7 +22,12 @@
22
22
 
23
23
  **言葉でなく実測で:** このリポジトリ自身の 203 テストで、`pty_read` はコンテキストに載るトークンを生ログの **約 7.1 分の 1** に減らす。しかも pass/fail の判定は畳んでも残る。→ [組み込みシェルツールとの使い分け](#組み込みシェルツールとの使い分け)
24
24
 
25
- 9 ツール: 6 つの **PTY ツール**(`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list`)で 1 本の永続端末を開き・操作し・読む。加えて 3 つの **エージェント起動ツール**(`codex_agent` / `grok_agent` / `composer_agent`)が、別のコーディングエージェントの TUI を新しい端末の中に起動する。バックエンドは **tmux** なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
25
+ 10 ツール: 6 つの **PTY ツール**(`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list`)で 1 本の永続端末を開き・操作し・読む。加えて 3 つの **エージェント起動ツール**(`codex_agent` / `grok_agent` / `composer_agent`)が、別のコーディングエージェントの TUI を新しい端末の中に起動し、`diagnostics` が安全な factory readiness を返す。バックエンドは **tmux** なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
26
+
27
+ **v0.12.2 は release candidate(公開待ち)です。** factory diagnostics と local
28
+ runtime-error store は canonical dotagents config の `collection.enabled: true` が明示された
29
+ 場合だけ収集し、既定OFF、network送信は行いません。npm公開、tag、CI、registry登録、registry由来install
30
+ の確認は未実施です。
26
31
 
27
32
  **状態:** 開発継続中 · この分野では新参で、別の形に賭けている([既存手段との比較](#既存手段との比較)参照)· 動作対象は Linux · WSL2 · macOS · Windows ネイティブ(core PTY ツール。`agent_done` は現時点では POSIX/WSL/macOS のみ)· MIT · [変更履歴](CHANGELOG.md)。
28
33
 
@@ -129,7 +134,7 @@ claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
129
134
  Claude Code を再起動して、接続を確認:
130
135
 
131
136
  ```bash
132
- /mcp # aiterm が connected・9 ツール公開、と出る
137
+ /mcp # aiterm が connected・10 ツール公開、と出る
133
138
  ```
134
139
 
135
140
  最初のセッション——4 回の呼び出しで、1 個の永続端末:
@@ -167,7 +172,7 @@ MCP クライアントが aiterm を stdio 越しにプログラムから駆動
167
172
 
168
173
  ```mermaid
169
174
  flowchart LR
170
- AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · codex_agent<br/>grok_agent · composer_agent"| S["aiterm-mcp<br/>stdio MCP · 9 tools"]
175
+ AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · codex_agent<br/>grok_agent · composer_agent · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 10 tools"]
171
176
  S -->|"pty_read<br/>token-reduced"| AI
172
177
  S -->|"tmux send-keys<br/>capture-pane"| P["persistent PTYs<br/>tmux · survive restarts"]
173
178
  P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
@@ -248,6 +253,15 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
248
253
  | `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
249
254
  | `pty_close` | セッションを閉じる | `session_id` |
250
255
  | `pty_list` | セッション一覧 | (なし) |
256
+ | `diagnostics` | 機械可読 JSON による read-only factory readiness | (なし) |
257
+
258
+ `diagnostics` は PTY やエージェントを起動しない。パッケージ版、MCP 呼出 readiness、read-only な PTY 一覧要約、bounded runtime-error-store status、任意 vendor launcher の可用性だけを返す。path・環境値・認証情報・コマンド本文・PTY 出力・raw log は意図的に返さない。通常未設定の任意依存は `not_applicable`、安全に確定できない状態は `unverified` と表す。
259
+
260
+ ### ローカル runtime error snapshot
261
+
262
+ `aiterm-runtime-errors snapshot` は dotagents factory adapter 向けに、製品所有のローカル snapshot を機械可読 JSON で返す。canonical dotagents factory-reporter config が schema-exact、host profile が実行 OS と一致し、`collection.enabled` が JSON boolean `true` の時だけ収集する。reporting field は schema 検証するが endpoint/credential file へ接続・読取せず network I/O も行わない。観測 API は core owner layer の固定3 code(PTY dependency・persistence・任意 vendor launcher)だけを受け、保存するのも固定 template と aggregate metadata(SHA-256 fingerprint、count、first/last、status、monotonic sequence)だけ。exception、stderr/stdout、stack、prompt、PTY/transcript/event body、path、任意 context は受け付けない。保存済み JSON も top/record exact・固定定義一致・fingerprint 再計算を通し、明示 DTO だけを返す。
263
+
264
+ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後に `aiterm-runtime-errors ack --cursor N` を呼ぶ。運用上の明示操作は `resolve|reopen --fingerprint SHA256`。MCP からの収集・diagnostic read は timeout 付き child process に隔離し、FIFOや停止 filesystem が端末本体を止めない。store mutation は期限付き bakery ticket queue で直列化する。各waiterは PID+process start identity+owner token を持つ再利用されない固有ticketを所有するため、死んだownerだけを固有名で除去でき、固定path回収のABAを作らない。POSIX state は `$XDG_STATE_HOME/aiterm-mcp/`(既定 `~/.local/state/aiterm-mcp/`)へ atomic replacement で置き、every read で owner/mode を再検証する。Windows native は `%LOCALAPPDATA%\aiterm-mcp\` で current SID の非継承 FullControl ACE 1件だけへ DACL を再構築し readback する。今回 Windows は path/DACL/timeout の純粋テストだけであり、新しい実機統合成功は主張しない。
251
265
 
252
266
  ### 対話エージェント起動ツール
253
267
 
package/README.md CHANGED
@@ -22,7 +22,13 @@
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
- Nine tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal, plus three **agent launchers** — `codex_agent` / `grok_agent` / `composer_agent` — that each start another coding agent's TUI inside a fresh one. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
25
+ Ten tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal, three **agent launchers** — `codex_agent` / `grok_agent` / `composer_agent` — that each start another coding agent's TUI inside a fresh one, and `diagnostics` for safe factory readiness. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
26
+
27
+ **v0.12.2 is a release candidate (pending).** Factory diagnostics and the local
28
+ runtime-error store collect only when canonical dotagents config explicitly sets
29
+ `collection.enabled: true`; collection is off by default and performs no network
30
+ I/O. Publication, tag, CI, registry registration, and registry-install verification
31
+ are pending.
26
32
 
27
33
  **Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows for the core PTY tools (`agent_done` is POSIX/WSL/macOS only for now) · MIT · see the [CHANGELOG](CHANGELOG.md).
28
34
 
@@ -129,7 +135,7 @@ claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
129
135
  Restart Claude Code, then verify the connection:
130
136
 
131
137
  ```bash
132
- /mcp # aiterm should show as connected, exposing 9 tools
138
+ /mcp # aiterm should show as connected, exposing 10 tools
133
139
  ```
134
140
 
135
141
  Your first session — four calls, one persistent terminal:
@@ -167,7 +173,7 @@ The terminal is real and shared, so a human *can* jump in ([A human can watch](#
167
173
 
168
174
  ```mermaid
169
175
  flowchart LR
170
- AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · codex_agent<br/>grok_agent · composer_agent"| S["aiterm-mcp<br/>stdio MCP · 9 tools"]
176
+ AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · codex_agent<br/>grok_agent · composer_agent · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 10 tools"]
171
177
  S -->|"pty_read<br/>token-reduced"| AI
172
178
  S -->|"tmux send-keys<br/>capture-pane"| P["persistent PTYs<br/>tmux · survive restarts"]
173
179
  P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
@@ -250,6 +256,15 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
250
256
  | `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
251
257
  | `pty_close` | Close a session | `session_id` |
252
258
  | `pty_list` | List sessions (agent rows carry `agent=<kind>` metadata) | (none) |
259
+ | `diagnostics` | Read-only factory readiness as machine-readable JSON | (none) |
260
+
261
+ `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`.
262
+
263
+ ### Local runtime error snapshot
264
+
265
+ `aiterm-runtime-errors snapshot` exposes a machine-readable, product-owned local snapshot for the dotagents factory adapter. Collection is fail-closed unless the canonical dotagents factory-reporter config is schema-exact, its host profile matches the executing OS, and it contains the JSON boolean `collection.enabled: true`; reporting fields are schema-validated but endpoints and credential files are never contacted, and the store performs no network I/O. The only accepted observations are three fixed codes owned by the core boundary (PTY dependency, persistence, and optional vendor launcher). Stored data is limited to fixed templates and aggregate metadata (SHA-256 fingerprint, count, first/last seen, status, and monotonic sequence); exceptions, stderr/stdout, stacks, prompts, terminal/transcript/event bodies, paths, and arbitrary context cannot enter the API. Persisted JSON is revalidated with exact top/record fields and a recomputed fingerprint before explicit DTO projection.
266
+
267
+ Consumer flow is `aiterm-runtime-errors snapshot`, then `aiterm-runtime-errors ack --cursor N` after durable ingestion. Operators can use `resolve|reopen --fingerprint SHA256`. MCP collection and diagnostic reads run in timeout-bounded child processes, so a FIFO or stalled filesystem cannot block terminal work; child failure emits only the fixed store diagnostic. Store mutation uses a bounded bakery ticket queue: every waiter owns a never-reused ticket containing PID, process-start identity, and an owner token, so dead owners are removed by unique filename without fixed-path reclaim ABA. Worker deadlines use forced termination so a SIGTERM-ignoring child cannot mutate state after timeout. POSIX state is atomically replaced under `$XDG_STATE_HOME/aiterm-mcp/` (default `~/.local/state/aiterm-mcp/`) with owner/mode rechecked on every read. Windows native uses `%LOCALAPPDATA%\aiterm-mcp\`; each DACL is rebuilt and read back as one non-inherited FullControl ACE for the current SID. Windows path/DACL/timeout behavior is covered by pure tests in this change; no new Windows integration success is claimed.
253
268
 
254
269
  ### Interactive agent launchers
255
270
 
package/dist/core.js CHANGED
@@ -14,6 +14,7 @@ import * as os from "node:os";
14
14
  import { createHash, randomBytes } from "node:crypto";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import * as rtk from "./rtk.js";
17
+ import { recordRuntimeError } from "./runtime-error-store.js";
17
18
  // Windows ネイティブには tmux が無い。その場合だけ全 tmux 呼び出しを WSL 経由へ橋渡しする
18
19
  // (POSIX = Linux/WSL2/macOS は従来どおり tmux を直接叩く)。
19
20
  const isWin = process.platform === "win32";
@@ -98,6 +99,33 @@ export class AitermError extends Error {
98
99
  this.code = code;
99
100
  }
100
101
  }
102
+ class TelemetryOwnedError extends AitermError {
103
+ telemetryCode;
104
+ constructor(message, code, telemetryCode, cause) {
105
+ super(message, code);
106
+ this.name = "TelemetryOwnedError";
107
+ this.telemetryCode = telemetryCode;
108
+ if (cause !== undefined)
109
+ this.cause = cause;
110
+ }
111
+ }
112
+ function telemetryOwnedFailure(telemetryCode, error, fallbackCode = 1) {
113
+ if (error instanceof TelemetryOwnedError)
114
+ return error;
115
+ recordRuntimeError(telemetryCode);
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ const code = error instanceof AitermError ? error.code : fallbackCode;
118
+ return new TelemetryOwnedError(message, code, telemetryCode, error);
119
+ }
120
+ function ownTelemetryFailure(telemetryCode, error, fallbackCode = 1) {
121
+ throw telemetryOwnedFailure(telemetryCode, error, fallbackCode);
122
+ }
123
+ function ptyDependencyError(message, observe = true) {
124
+ const error = new AitermError(message, 2);
125
+ if (observe)
126
+ ownTelemetryFailure("AITERM.PTY_DEPENDENCY_UNAVAILABLE", error, 2);
127
+ throw error;
128
+ }
101
129
  // Windows のドライブパス (C:\a\b) を WSL から見える /mnt/c/a/b へ変換する。
102
130
  // 一時領域は常にドライブ直下なので UNC は想定外=弾く(黙って壊れた //server パスを作らない)。
103
131
  export function toWslPath(p) {
@@ -109,22 +137,22 @@ export function toWslPath(p) {
109
137
  // Windows で最初の tmux 呼び出し前に一度だけ WSL+tmux の可用性を確かめ、失敗は原因別に投げる。
110
138
  // -e(ログインシェル非経由)+短い timeout で、初回セットアップ未完了の wsl によるハングも防ぐ。
111
139
  let winBridgeOk = false;
112
- function ensureWinBridge() {
140
+ function ensureWinBridge(observe = true) {
113
141
  if (winBridgeOk)
114
142
  return;
115
143
  const r = spawnSync("wsl.exe", ["-e", "tmux", "-V"], { encoding: "utf8", timeout: 10000 });
116
144
  if (r.error) {
117
145
  const code = r.error.code;
118
146
  if (code === "ETIMEDOUT")
119
- throw new AitermError("WSL が応答しません(初回セットアップ未完了の可能性)。一度 `wsl` を起動してから再実行してください。", 2);
147
+ ptyDependencyError("WSL が応答しません(初回セットアップ未完了の可能性)。一度 `wsl` を起動してから再実行してください。", observe);
120
148
  if (code === "ENOENT")
121
- throw new AitermError("wsl.exe が見つかりません。Windows では WSL 上の tmux 経由で動作します。WSL と tmux を導入してください。", 2);
122
- throw new AitermError(`wsl.exe を起動できませんでした(${code ?? "unknown"})。`, 2);
149
+ ptyDependencyError("wsl.exe が見つかりません。Windows では WSL 上の tmux 経由で動作します。WSL と tmux を導入してください。", observe);
150
+ ptyDependencyError(`wsl.exe を起動できませんでした(${code ?? "unknown"})。`, observe);
123
151
  }
124
152
  // wsl.exe は System32 にあるので「起動」は成功するが、ディストリ未導入や distro 内に tmux が無いと
125
153
  // 非ゼロで終わる。両方を区別せず(wsl の出力は UTF-16 で文字化けし得るため)正直に表す。
126
154
  if (r.status !== 0)
127
- throw new AitermError("WSL 経由で tmux を起動できませんでした。WSL のディストリ未導入、または distro 内に tmux が無い可能性があります。`wsl tmux -V` が通るか確認してください(tmux 導入例: sudo apt install tmux)。", 2);
155
+ ptyDependencyError("WSL 経由で tmux を起動できませんでした。WSL のディストリ未導入、または distro 内に tmux が無い可能性があります。`wsl tmux -V` が通るか確認してください(tmux 導入例: sudo apt install tmux)。", observe);
128
156
  winBridgeOk = true;
129
157
  }
130
158
  // tmux が見つからないときの説明。macOS は tmux を同梱せず、Homebrew の bin は GUI 起動時の PATH に
@@ -143,7 +171,7 @@ function tmuxMissingMessage() {
143
171
  // 解決順: AITERM_TMUX(明示指定)→ PATH 上の tmux → Homebrew 既定パス。一度だけ実行しキャッシュする。
144
172
  // 見つからなければ tmuxMissingMessage で投げる(黙ってフォールバックせず、原因が見えるようにする)。
145
173
  let tmuxBin = null;
146
- function resolveTmux() {
174
+ function resolveTmux(observe = true) {
147
175
  if (tmuxBin)
148
176
  return tmuxBin;
149
177
  const override = process.env.AITERM_TMUX;
@@ -151,7 +179,7 @@ function resolveTmux() {
151
179
  const r = spawnSync(override, ["-V"], { encoding: "utf8", timeout: 5000 });
152
180
  if (!r.error && r.status === 0)
153
181
  return (tmuxBin = override);
154
- throw new AitermError(`AITERM_TMUX に指定された tmux を起動できません: ${override}(\`${override} -V\` が通りません)`, 2);
182
+ ptyDependencyError(`AITERM_TMUX に指定された tmux を起動できません: ${override}(\`${override} -V\` が通りません)`, observe);
155
183
  }
156
184
  // CLI/開発時は PATH 上の tmux をそのまま使う(最優先)。
157
185
  const onPath = spawnSync("tmux", ["-V"], { encoding: "utf8", timeout: 5000 });
@@ -169,20 +197,20 @@ function resolveTmux() {
169
197
  /* 次の候補へ */
170
198
  }
171
199
  }
172
- throw new AitermError(tmuxMissingMessage(), 2);
200
+ ptyDependencyError(tmuxMissingMessage(), observe);
173
201
  }
174
- function tmux(...args) {
202
+ function tmuxCommand(observe, ...args) {
175
203
  // maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
176
204
  // 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
177
205
  // Windows は同じ tmux を WSL 経由(-e でログインシェル非経由=$ 展開やクオート崩れを防ぐ)で叩く。
178
206
  let r;
179
207
  if (isWin) {
180
- ensureWinBridge();
208
+ ensureWinBridge(observe);
181
209
  r = spawnSync("wsl.exe", ["-e", "tmux", "-S", SOCK, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
182
210
  }
183
211
  else {
184
212
  // resolveTmux() は tmux を解決できなければ明確な AitermError を投げる(POSIX 版の事前確認)。
185
- r = spawnSync(resolveTmux(), ["-S", SOCK, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
213
+ r = spawnSync(resolveTmux(observe), ["-S", SOCK, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
186
214
  }
187
215
  // ENOBUFS(出力が 64MiB 超)を「code=1 の失敗」へ握り潰すと部分/空 stdout を正常扱いしてしまう。区別して投げる。
188
216
  // EXPECTED-FAILURE: 外部システム境界(tmux 出力過大)
@@ -193,10 +221,16 @@ function tmux(...args) {
193
221
  // 通常は resolveTmux() が事前に弾くため発火しないが、mid-run 消滅に対する正直な防御。
194
222
  if (!isWin && r.error && r.error.code === "ENOENT") {
195
223
  tmuxBin = null; // 次回 resolveTmux で再解決を許す
196
- throw new AitermError(tmuxMissingMessage(), 2);
224
+ ptyDependencyError(tmuxMissingMessage(), observe);
197
225
  }
198
226
  return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
199
227
  }
228
+ function tmux(...args) {
229
+ return tmuxCommand(true, ...args);
230
+ }
231
+ function tmuxCleanup(...args) {
232
+ return tmuxCommand(false, ...args);
233
+ }
200
234
  function sessionExists(name) {
201
235
  return tmux("has-session", "-t", name).code === 0;
202
236
  }
@@ -685,7 +719,12 @@ function assertNotDestructive(text, code, context = "") {
685
719
  }
686
720
  // ---------------------------------------------------------------- 操作(return で返す / 失敗は AitermError)
687
721
  export function openSession(name, shell = "bash") {
688
- fs.mkdirSync(SOCKDIR, { recursive: true });
722
+ try {
723
+ fs.mkdirSync(SOCKDIR, { recursive: true });
724
+ }
725
+ catch (error) {
726
+ ownTelemetryFailure("AITERM.PERSISTENCE_WRITE_FAILED", error);
727
+ }
689
728
  // macOS の /bin/bash は 3.2 で、起動時に zsh 移行バナーを出して最初の read を汚す。darwin かつ bash の
690
729
  // ときだけ -e で環境変数を渡して抑止する。-e は tmux>=3.2 が必要だが macOS の Homebrew tmux は常に該当。
691
730
  // (古い tmux<3.2 が残る Linux/WSL で -e を渡すと new-session が落ちるため、darwin 限定にする。)
@@ -720,7 +759,12 @@ export function openSession(name, shell = "bash") {
720
759
  // 新規セッションの .log は必ず truncate する。"a"(追記)だと外部 kill / killAll / クラッシュで
721
760
  // 同名 session だけ消えて .log が残った場合、offset=0 と相まって旧出力を新規として返す(B5)。
722
761
  // break は new-session 成功後にのみ到達=作りたての空 session ゆえ切り詰めは安全。lastcmd/mark 残骸も掃除。
723
- fs.writeFileSync(logpath(nm), "");
762
+ try {
763
+ fs.writeFileSync(logpath(nm), "");
764
+ }
765
+ catch (error) {
766
+ ownTelemetryFailure("AITERM.PERSISTENCE_WRITE_FAILED", error);
767
+ }
724
768
  for (const p of [lastcmdpath(nm), markpath(nm)]) {
725
769
  try {
726
770
  fs.unlinkSync(p);
@@ -738,16 +782,24 @@ export function openSession(name, shell = "bash") {
738
782
  const pr = tmux("pipe-pane", "-t", nm, "-o", `cat >> ${quoted}`);
739
783
  if (pr.code !== 0) {
740
784
  // 配管に失敗した session は pty_read が永遠に空を返す=成功を装わない。作った session を片付けて明示エラー。
741
- tmux("kill-session", "-t", nm);
785
+ try {
786
+ tmuxCleanup("kill-session", "-t", nm);
787
+ }
788
+ catch { /* cleanup failure is not a second observation */ }
742
789
  try {
743
790
  fs.unlinkSync(logpath(nm)); // B14: 直前に作った空 .log も残さない
744
791
  }
745
792
  catch {
746
793
  /* noop */
747
794
  }
748
- throw new AitermError("tmux pipe-pane 失敗(出力ログを配管できないため session を破棄): " + pr.stderr.trim(), 2);
795
+ ownTelemetryFailure("AITERM.PERSISTENCE_WRITE_FAILED", new AitermError("tmux pipe-pane 失敗(出力ログを配管できないため session を破棄): " + pr.stderr.trim(), 2), 2);
796
+ }
797
+ try {
798
+ writeOffset(nm, 0);
799
+ }
800
+ catch (error) {
801
+ ownTelemetryFailure("AITERM.PERSISTENCE_WRITE_FAILED", error);
749
802
  }
750
- writeOffset(nm, 0);
751
803
  return [nm, attachHint(nm)];
752
804
  }
753
805
  export function send(name, text, o = {}) {
@@ -979,7 +1031,29 @@ export function listSessions() {
979
1031
  }
980
1032
  return "(セッション無し)";
981
1033
  }
982
- export function closeSession(name) {
1034
+ /**
1035
+ * `pty_list` 相当を read-only に照会し、内容を返さず session 件数だけを返す。
1036
+ * session 名・前面コマンド・PTY 出力を診断応答へ持ち出さないため、factory が安全に readiness を
1037
+ * 見られる。socket 不在は「セッション未設定」であって障害ではない。
1038
+ */
1039
+ export function readOnlyPtyListDiagnostic() {
1040
+ try {
1041
+ const r = tmux("list-sessions", "-F", "#{session_name}");
1042
+ if (r.code === 0) {
1043
+ return { status: "ready", session_count: r.stdout.split(/\r?\n/).filter(Boolean).length };
1044
+ }
1045
+ // tmux は専用 socket に server がいない通常状態を exit 1 で返す。その他の失敗を「空」と
1046
+ // 偽装しないため、メッセージを公開せず unverified に留める。
1047
+ if (/no server running|failed to connect/i.test(r.stderr)) {
1048
+ return { status: "not_applicable", session_count: 0 };
1049
+ }
1050
+ }
1051
+ catch {
1052
+ // tmux 未導入・WSL bridge 不全等。絶対 path や生 stderr を診断 JSON に出さない。
1053
+ }
1054
+ return { status: "unverified", session_count: null };
1055
+ }
1056
+ function closeSessionInternal(name, observeDependency = true) {
983
1057
  assertSessionName(name);
984
1058
  if (agentWaitLocks.has(name)) {
985
1059
  throw new AitermError(`agent session '${name}' は agent_done 待機中のため close できません`, 2);
@@ -992,7 +1066,7 @@ export function closeSession(name) {
992
1066
  throw new AitermError(`agent session '${name}' は別プロセス${d.pid != null ? `(pid ${d.pid})` : ""}の agent_done 待機中のため close できません`, 2);
993
1067
  }
994
1068
  }
995
- tmux("kill-session", "-t", name);
1069
+ (observeDependency ? tmux : tmuxCleanup)("kill-session", "-t", name);
996
1070
  for (const p of [logpath(name), offsetpath(name), lastcmdpath(name), markpath(name)]) {
997
1071
  try {
998
1072
  fs.unlinkSync(p);
@@ -1004,6 +1078,9 @@ export function closeSession(name) {
1004
1078
  cleanupAgentState(name);
1005
1079
  return `closed ${name}`;
1006
1080
  }
1081
+ export function closeSession(name) {
1082
+ return closeSessionInternal(name, true);
1083
+ }
1007
1084
  export function killAll() {
1008
1085
  if (agentWaitLocks.size > 0) {
1009
1086
  throw new AitermError(`agent_done 待機中の session があるため killAll できません: ${Array.from(agentWaitLocks).join(",")}`, 2);
@@ -2288,21 +2365,46 @@ function resolveAgentBin(kind) {
2288
2365
  // 明示指定 env は実在を検証する。存在しないパスを黙って返すと、session を作って
2289
2366
  // `'/typo' ...` を送信し bash が command not found を出すだけで openAgent は「起動した」と
2290
2367
  // 偽成功を返す(既定パス/PATH 経路は検証するのに env だけ無検証だった非対称の解消・A3)。
2291
- if (fs.existsSync(fromEnv))
2368
+ if (isUsableExecutableFile(fromEnv))
2292
2369
  return fromEnv;
2293
2370
  throw new AitermError(`${envVar} に指定された ${name} が存在しません: ${fromEnv}`, 2);
2294
2371
  }
2295
2372
  const cand = path.join(home, ...rel);
2296
- if (fs.existsSync(cand))
2373
+ if (isUsableExecutableFile(cand))
2297
2374
  return cand;
2298
2375
  const w = spawnSync(isWin ? "where" : "which", [name], {
2299
2376
  encoding: "utf8",
2300
2377
  timeout: 5000,
2301
2378
  });
2302
- if (w.status === 0 && (w.stdout ?? "").trim())
2303
- return w.stdout.trim().split(/\r?\n/)[0];
2379
+ if (w.status === 0 && (w.stdout ?? "").trim()) {
2380
+ const resolved = w.stdout.trim().split(/\r?\n/)[0];
2381
+ if (isUsableExecutableFile(resolved))
2382
+ return resolved;
2383
+ }
2304
2384
  return null;
2305
2385
  }
2386
+ function isUsableExecutableFile(candidate) {
2387
+ try {
2388
+ if (!fs.statSync(candidate).isFile())
2389
+ return false;
2390
+ if (isWin)
2391
+ return /\.(?:exe|cmd|bat|com)$/i.test(candidate);
2392
+ fs.accessSync(candidate, fs.constants.X_OK);
2393
+ return true;
2394
+ }
2395
+ catch {
2396
+ return false;
2397
+ }
2398
+ }
2399
+ /** vendor CLI の存在だけを安全に要約する。認証状態・実行出力・解決先 path は返さない。 */
2400
+ export function vendorLauncherDiagnostic(kind) {
2401
+ try {
2402
+ return resolveAgentBin(kind) ? "ready" : "not_applicable";
2403
+ }
2404
+ catch {
2405
+ return "unverified";
2406
+ }
2407
+ }
2306
2408
  // 単一引用符で安全に包む(' は '\'' で脱出)。send は raw:true で送るため自前で quote する。
2307
2409
  function shq(s) {
2308
2410
  return `'${s.replace(/'/g, "'\\''")}'`;
@@ -2427,10 +2529,16 @@ export function openAgent(kind, opts = {}) {
2427
2529
  "(モデルカタログ supports_reasoning_effort=false)", 2);
2428
2530
  }
2429
2531
  const agentDone = !!opts.agent_done;
2430
- const bin = resolveAgentBin(kind);
2532
+ let bin;
2533
+ try {
2534
+ bin = resolveAgentBin(kind);
2535
+ }
2536
+ catch (error) {
2537
+ ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", error, 2);
2538
+ }
2431
2539
  if (!bin) {
2432
2540
  const where = kind === "codex" ? "~/.local/bin/codex" : "~/.grok/bin/grok";
2433
- throw new AitermError(`${label} の CLI が見つかりません(${where} か PATH が必要)`, 2);
2541
+ ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", new AitermError(`${label} の CLI が見つかりません(${where} か PATH が必要)`, 2), 2);
2434
2542
  }
2435
2543
  // cwd 検証(session を作る前に。cd 失敗はシェル内で静かに死に「起動した」と偽成功を返すため)。
2436
2544
  let cwd = null;
@@ -2481,14 +2589,15 @@ export function openAgent(kind, opts = {}) {
2481
2589
  send(sid, full, { enter: true, mark: false, force: true, rtk: false, raw: true });
2482
2590
  }
2483
2591
  catch (e) {
2592
+ const failure = telemetryOwnedFailure("AITERM.VENDOR_LAUNCHER_FAILED", e);
2484
2593
  // 起動コマンドを投入できなかった session は空のまま残る=残骸を作らない。片付けてから元エラーを伝える。
2485
2594
  try {
2486
- closeSession(sid);
2595
+ closeSessionInternal(sid, false);
2487
2596
  }
2488
2597
  catch {
2489
2598
  /* 片付け失敗より元エラーの伝達を優先 */
2490
2599
  }
2491
- throw e;
2600
+ throw failure;
2492
2601
  }
2493
2602
  return [
2494
2603
  sid,
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import { z } from "zod";
14
14
  import * as core from "./core.js";
15
+ import { runtimeErrorStoreDiagnostic } from "./runtime-error-store.js";
15
16
  import { createRequire } from "node:module";
16
17
  // package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
17
18
  // createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
@@ -26,6 +27,41 @@ function fail(e) {
26
27
  const msg = e instanceof Error ? e.message : String(e);
27
28
  return { content: [{ type: "text", text: "aiterm: " + msg }], isError: true };
28
29
  }
30
+ /**
31
+ * Factory 向けの read-only 診断。JSON の field は意図的に allowlist 制で、絶対 path、環境変数、
32
+ * token、コマンド本文、PTY 出力、raw log、認証状態を公開しない。
33
+ */
34
+ async function factoryDiagnostics() {
35
+ const ptyList = core.readOnlyPtyListDiagnostic();
36
+ const codex = core.vendorLauncherDiagnostic("codex");
37
+ const grok = core.vendorLauncherDiagnostic("grok");
38
+ const runtimeErrors = await runtimeErrorStoreDiagnostic();
39
+ const overall = ptyList.status === "unverified" || runtimeErrors.status === "unverified" ? "unverified" : "ready";
40
+ return JSON.stringify({
41
+ diagnostic_schema: "aiterm-mcp.factory-diagnostics.v1",
42
+ version: pkg.version,
43
+ overall,
44
+ mcp: { transport: "stdio", initialize: "ready", tool_call: "ready" },
45
+ pty_list: { access: "read_only", ...ptyList },
46
+ runtime_error_store: runtimeErrors,
47
+ vendor_dependencies: {
48
+ codex: {
49
+ status: codex,
50
+ optional: true,
51
+ required_for: ["codex_agent"],
52
+ },
53
+ grok: {
54
+ status: grok,
55
+ optional: true,
56
+ required_for: ["grok_agent", "composer_agent"],
57
+ },
58
+ },
59
+ });
60
+ }
61
+ server.registerTool("diagnostics", {
62
+ description: "Factory 向け read-only 診断。安全な状態語彙だけを機械可読 JSON で返す(PTY 内容・認証情報・path・環境値は返さない)。",
63
+ inputSchema: {},
64
+ }, async () => ok(await factoryDiagnostics()));
29
65
  server.registerTool("pty_open", {
30
66
  description: "ローカル永続端末(tmux セッション)を1個開き、session_id を返す。tmux サーバ常駐ゆえ本サーバや " +
31
67
  "クライアントが再起動してもセッションは生存する。リモート操作は専用ツールにせず、開いた端末の中で " +