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 +17 -3
- package/README.md +18 -3
- package/dist/core.js +136 -27
- package/dist/index.js +36 -0
- package/dist/runtime-error-store.js +836 -0
- package/dist/runtime-error-worker.js +24 -0
- package/dist/runtime-errors-cli.js +66 -0
- package/package.json +3 -2
package/README.ja.md
CHANGED
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
**言葉でなく実測で:** このリポジトリ自身の 203 テストで、`pty_read` はコンテキストに載るトークンを生ログの **約 7.1 分の 1** に減らす。しかも pass/fail の判定は畳んでも残る。→ [組み込みシェルツールとの使い分け](#組み込みシェルツールとの使い分け)
|
|
24
24
|
|
|
25
|
-
|
|
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・
|
|
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 ·
|
|
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
|
-
|
|
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
|
|
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 ·
|
|
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
|
-
|
|
147
|
+
ptyDependencyError("WSL が応答しません(初回セットアップ未完了の可能性)。一度 `wsl` を起動してから再実行してください。", observe);
|
|
120
148
|
if (code === "ENOENT")
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
ptyDependencyError(tmuxMissingMessage(), observe);
|
|
173
201
|
}
|
|
174
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2595
|
+
closeSessionInternal(sid, false);
|
|
2487
2596
|
}
|
|
2488
2597
|
catch {
|
|
2489
2598
|
/* 片付け失敗より元エラーの伝達を優先 */
|
|
2490
2599
|
}
|
|
2491
|
-
throw
|
|
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
|
"クライアントが再起動してもセッションは生存する。リモート操作は専用ツールにせず、開いた端末の中で " +
|