aiterm-mcp 0.29.31 → 0.31.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 CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.31.0] - 2026-09-04
11
+
12
+ ### Fixed
13
+
14
+ - Claude Codeのturnが529 Overloaded等のAPIエラーで打ち切られるとStop hookが走らず、`aiterm-wait`が永久に未完了を返し続けていた(実被弾 2026-09-03: BellTeamのClaude席で70分の待機)。会話記録(`<config dir>/projects/<cwd slug>/<session-id>.jsonl`)に観測開始後に増えた`isApiErrorMessage:true`の行を読み、新しいoutcome `error`(exit 7、`error`にエラー本文)で返す。
15
+ - Grokの`turn_ended outcome=error`を完了境界として扱わず待ち続けていた。同じくoutcome `error`で返す。Cursorは`turn_ended`のstatusを問わず既に終了を返していたため変更なし。Codexはエラー終了の記録形が実測できておらず未対応。
16
+
17
+ ## [0.30.0] - 2026-09-04
18
+
19
+ ### Added
20
+
21
+ - `pty_send`(agent dispatch)、`agent_steer`、`agent_launch`に`image`(画像ファイルの絶対パスの配列)を追加した。aitermが本文末尾へ添付行を付け、Claude Code/Codex/Grok/Cursorはいずれも自分のfile読取toolでそのpathを画像として開く(実測 2026-09-04)。呼出し側が入力欄へパスを先打鍵する等のharness別手順を持つ必要を無くす。相対パス・未対応拡張子・不在fileは送信前にtyped errorで拒否し、通常PTY送信とforce送信では指定できない。
22
+
10
23
  ## [0.29.31] - 2026-09-04
11
24
 
12
25
  ### Fixed
@@ -1540,7 +1553,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1540
1553
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1541
1554
  provenance.
1542
1555
 
1543
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.31...HEAD
1556
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.31.0...HEAD
1557
+ [0.31.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.30.0...v0.31.0
1558
+ [0.30.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.31...v0.30.0
1544
1559
  [0.29.31]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.30...v0.29.31
1545
1560
  [0.29.30]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.29...v0.29.30
1546
1561
  [0.29.29]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.28...v0.29.29
package/README.ja.md CHANGED
@@ -153,7 +153,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
153
153
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
154
154
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
155
155
 
156
- **状態:** 開発継続中 · 現行公開版 **v0.29.31** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
156
+ **状態:** 開発継続中 · 現行公開版 **v0.31.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
157
157
 
158
158
  ### 更新と巻き戻し
159
159
 
@@ -189,6 +189,8 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
189
189
 
190
190
  `aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返し、完了通知は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
191
191
 
192
+ `agent_launch`・`pty_send`(agent dispatch)・`agent_steer`は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
193
+
192
194
  `agent_launch`は任意の`write_scope`も受ける。Codex/Grokのread-onlyは`--sandbox read-only`、Cursorは公式`--mode ask`で実効化する。path説明は同等CLI引数がないためdeclaration-only。
193
195
 
194
196
  ```text
@@ -199,7 +201,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
199
201
  → { session_id: "codex1", … } # Codex が永続端末で稼働開始
200
202
  pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
201
203
  pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
202
- $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed。回収は pty_read(agent_transcript:true)
204
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed / 7=error(APIエラー等でturn打ち切り)。回収は pty_read(agent_transcript:true)
203
205
  → 操舵し、Codex の次の入力境界で返る
204
206
  ```
205
207
 
package/README.md CHANGED
@@ -169,7 +169,7 @@ collection is off by default and performs no network I/O. It ships via
169
169
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
170
170
  Release re-registers the Official MCP Registry entry.
171
171
 
172
- **Status:** actively maintained · current public release **v0.29.31** · 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).
172
+ **Status:** actively maintained · current public release **v0.31.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).
173
173
 
174
174
  ### Update and rollback
175
175
 
@@ -211,6 +211,8 @@ The same primitive hosts another agent's TUI. `agent_launch` starts a selected e
211
211
 
212
212
  The human-readable launch text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt containing the canonical `harness`; the old `provider` field remains for compatibility. The same `harness` is carried by agent dispatch, `aiterm-wait`, `agent_configure`, and agent rows in `pty_list`, while their old vendor/provider/agent fields remain compatibility fields. Codex completion comes from its normal durable rollout transcript, Grok CLI from its normal session events, Claude Code from a launch-specific Stop hook settings addition, and Cursor from its normal agent transcript's terminal `turn_ended` record. Sending to any agent session is a non-blocking **dispatch** — the call returns immediately with an opaque, harness-specific integer `event_cursor`, and completion arrives via [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). The Cursor adapter translates submit into the current CLI's extended keyboard protocol. If submitted text remains in Cursor's composer, its dispatch fails instead of returning a successful receipt.
213
213
 
214
+ `agent_launch`, `pty_send` (agent dispatch), and `agent_steer` 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.
215
+
214
216
  `agent_launch` accepts an optional `write_scope`: either `"read-only"` or a human-readable description of writable paths. Codex/Grok use `--sandbox read-only`; Cursor uses its official read-only `--mode ask`. A path description remains declaration-only because these CLI launch surfaces provide no equivalent path allowlist flag.
215
217
 
216
218
  For a correlated 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.
@@ -224,7 +226,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
224
226
  pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
225
227
  pty_send("codex1", "also fix the imports it broke")
226
228
  → non-blocking dispatch; receipt carries event_cursor
227
- $ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed
229
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed, 7=error (turn aborted by an API error)
228
230
  pty_read("codex1", { agent_transcript: true }) → collect the full answer
229
231
  ```
230
232
 
@@ -2,7 +2,7 @@
2
2
  // aiterm-wait — agent turn 完了eventの純リーダー観測CLI。
3
3
  // 完了/timeout/close を1行のJSON receiptで返してexitする。lock・PTY・dispatch状態には一切触れない。
4
4
  // 親AIホストのバックグラウンドタスクとして起動し、exitを「観測終了の通知」として使う。
5
- // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=harness利用上限 / 1=エラー)。
5
+ // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=harness利用上限 / 7=error=turnがエラー終了 / 1=waiter自身のエラー)。
6
6
  // --timeout 0 は待たずに一度だけ観測する照会で、未完了は running(timeout と混同させない)。
7
7
  // receipt の outcome が正で、done 以外は未完了。timeout の既定は core の DEFAULT_AGENT_DONE_TIMEOUT(600秒)。
8
8
  import { fileURLToPath } from "node:url";
@@ -75,6 +75,7 @@ const OUTCOME_EXIT_CODES = {
75
75
  timeout: 3,
76
76
  closed: 4,
77
77
  rate_limited: 6,
78
+ error: 7,
78
79
  };
79
80
  export async function main(argv) {
80
81
  const cmd = parseArgs(argv);
package/dist/core.js CHANGED
@@ -18,7 +18,7 @@ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, past
18
18
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
19
19
  import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
20
20
  import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexLaunchBlockingDialog, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
21
- import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, } from "./harnesses/claude.js";
21
+ import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
22
22
  import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
23
23
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
24
24
  export { AitermError } from "./errors.js";
@@ -2251,7 +2251,7 @@ function assertInitialPromptNotPendingForSend(name, force) {
2251
2251
  `${agentWaitGuide(name)}完了後に再度 pty_send するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2252
2252
  }
2253
2253
  // aiterm-wait の exit 契約(CLI と各所の案内文で共有する正)。exit≠完了: outcome が done の時だけ完了。
2254
- export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed。receiptのoutcomeが正で、done以外は未完了`;
2254
+ export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed / 7=error(harnessの記録でturnがAPIエラー等で打ち切られた。結果は無い)。receiptのoutcomeが正で、done以外は未完了`;
2255
2255
  function quoteWindowsProcessArgument(value) {
2256
2256
  if (value !== "" && !/[\s"]/u.test(value))
2257
2257
  return value;
@@ -2393,7 +2393,12 @@ export async function observeAgentDone(name, o = {}) {
2393
2393
  let cursor = startOffset;
2394
2394
  let carry = "";
2395
2395
  let malformedEvents = 0;
2396
- const observation = (outcome, ev = null, rateLimit = null) => ({
2396
+ // Claudeの会話記録はこの観測開始時点の末尾から先だけを読む。過去turnのAPIエラー行を今回の
2397
+ // 終了と誤認しないため。dispatch直後に待機を始める親(receiptのwait_process)を前提にする。
2398
+ const transcriptFile = claudeSessionTranscriptPath(meta);
2399
+ let transcriptCursor = transcriptFile ? safeStatSize(transcriptFile) : 0;
2400
+ let transcriptCarry = "";
2401
+ const observation = (outcome, ev = null, rateLimit = null, apiError = null) => ({
2397
2402
  schema: "aiterm.agent-wait-result.v1",
2398
2403
  session_id: meta.aiterm_session,
2399
2404
  launch_id: meta.launch_id,
@@ -2404,8 +2409,9 @@ export async function observeAgentDone(name, o = {}) {
2404
2409
  vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
2405
2410
  turn_id: ev?.turn_id ?? null,
2406
2411
  malformed_events: malformedEvents,
2407
- at: ev?.at ?? null,
2412
+ at: ev?.at ?? apiError?.at ?? null,
2408
2413
  rate_limit: rateLimit,
2414
+ error: apiError?.text ?? null,
2409
2415
  });
2410
2416
  for (;;) {
2411
2417
  if (!fs.existsSync(metadataFile))
@@ -2431,6 +2437,24 @@ export async function observeAgentDone(name, o = {}) {
2431
2437
  if (scanned.event)
2432
2438
  return observation("done", scanned.event);
2433
2439
  }
2440
+ if (transcriptFile && fs.existsSync(transcriptFile)) {
2441
+ const transcriptSize = safeStatSize(transcriptFile);
2442
+ if (transcriptSize < transcriptCursor) {
2443
+ transcriptCursor = 0;
2444
+ transcriptCarry = "";
2445
+ }
2446
+ if (transcriptSize > transcriptCursor) {
2447
+ transcriptCarry += readFileRange(transcriptFile, transcriptCursor, transcriptSize).toString("utf8");
2448
+ transcriptCursor = transcriptSize;
2449
+ const lines = transcriptCarry.split("\n");
2450
+ transcriptCarry = lines.pop() ?? "";
2451
+ for (const line of lines) {
2452
+ const apiError = claudeApiErrorFromLine(line);
2453
+ if (apiError)
2454
+ return observation("error", null, null, apiError);
2455
+ }
2456
+ }
2457
+ }
2434
2458
  // timeout=0 は「待たずに一度だけ見る」照会=未完了は失敗ではなく running。
2435
2459
  // 1秒以上を指定した待機の未完了は従来どおり timeout で、待ち方の意味は変えない。
2436
2460
  {
@@ -3049,6 +3073,37 @@ export function __testCodexConfigureChoices(screen, model, effort) {
3049
3073
  // v0.16.0: 親をブロックする wait 経路は廃止した。send は ready gate と submit 分離を内蔵した
3050
3074
  // dispatch として即返り、event_cursor(送信直前のharness完了正本境界)を receipt で返す。
3051
3075
  // 完了通知は aiterm-wait(--cursor で境界を渡す)、回収は pty_read / claude_turn recover が担う。
3076
+ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
3077
+ /**
3078
+ * 画像添付。Claude Code/Codex/Grok/Cursorの4 harnessは、本文に書かれた画像ファイルの絶対パスを
3079
+ * 自分のfile読取toolで開いて画像として見る(実測 2026-09-04: 赤青の試験画像を全harnessが正しく答えた)。
3080
+ * 入力欄へパスを先打鍵する等のharness別操作は不要であり、呼出し側にharnessの癖を覚えさせない。
3081
+ * 添付の表現とpath検査はこの1箇所だけが所有する。検査は外部入力(呼出し側が渡すpath)の境界。
3082
+ */
3083
+ export function attachImages(text, images) {
3084
+ if (!images || images.length === 0)
3085
+ return text;
3086
+ const lines = images.map((image, index) => {
3087
+ if (typeof image !== "string" || !path.isAbsolute(image)) {
3088
+ throw new AitermError(`image[${index}] は画像ファイルの絶対パスで指定してください: ${String(image)}`, 2);
3089
+ }
3090
+ if (!IMAGE_EXTENSIONS.has(path.extname(image).toLowerCase())) {
3091
+ throw new AitermError(`image[${index}] の拡張子に対応していません(png/jpg/jpeg/gif/webp): ${image}`, 2);
3092
+ }
3093
+ let st;
3094
+ try {
3095
+ st = fs.statSync(image);
3096
+ }
3097
+ catch {
3098
+ throw new AitermError(`image[${index}] が読めません: ${image}`, 2);
3099
+ }
3100
+ if (!st.isFile())
3101
+ throw new AitermError(`image[${index}] はfileではありません: ${image}`, 2);
3102
+ return `[aiterm 添付画像 ${index + 1}/${images.length}] ${image}`;
3103
+ });
3104
+ const body = text.trim().length > 0 ? text : "添付画像を確認してください。";
3105
+ return `${body}\n\n${lines.join("\n")}\n添付画像は上のファイルを読んで確認する。`;
3106
+ }
3052
3107
  export async function dispatchAgentTurn(name, text, o = {}) {
3053
3108
  assertSessionName(name);
3054
3109
  const meta = loadAgentMetadata(name);
@@ -2,6 +2,7 @@
2
2
  // core 所有のサービス(transcript 不在エラー)は引数で注入し、
3
3
  // 依存方向を core → harnesses → agent-shared の一方向に保つ。
4
4
  import * as fs from "node:fs";
5
+ import * as os from "node:os";
5
6
  import * as path from "node:path";
6
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
8
  import { fileURLToPath } from "node:url";
@@ -216,3 +217,45 @@ export function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperat
216
217
  writeAgentMetadata(meta);
217
218
  return meta;
218
219
  }
220
+ // ---------------------------------------------------------------- APIエラー終了の検知(会話記録)
221
+ // Claude CodeはAPIエラー(529 Overloaded等)でturnを打ち切る時、Stop hookを走らせない。
222
+ // 代わりに会話記録(<config dir>/projects/<cwd slug>/<session-id>.jsonl)へ
223
+ // `type:"assistant", isApiErrorMessage:true, apiErrorStatus:<code>` の1行を書く(実測 2026-09-03、
224
+ // BellTeamのチャイム席で70分の待機を生んだ529)。完了eventだけを待つと永久に running になるため、
225
+ // dispatch後に増えた記録行からこの印を読み、outcome=error として親へ返す。
226
+ export function claudeConfigDir() {
227
+ return process.env.CLAUDE_CONFIG_DIR ?? path.join(process.env.HOME ?? os.homedir(), ".claude");
228
+ }
229
+ // Claude Codeのproject slug: cwdの英数字以外を1文字ずつ "-" にする(実測: "/Users/kite/.throughline-x" → "-Users-kite--throughline-x")。
230
+ export function claudeProjectSlug(cwd) {
231
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
232
+ }
233
+ export function claudeSessionTranscriptPath(meta) {
234
+ if (meta.kind !== "claude" || !meta.vendor_session_id)
235
+ return null;
236
+ return path.join(claudeConfigDir(), "projects", claudeProjectSlug(meta.cwd ?? process.cwd()), `${meta.vendor_session_id}.jsonl`);
237
+ }
238
+ export function claudeApiErrorFromLine(line) {
239
+ if (!line.trim())
240
+ return null;
241
+ let record;
242
+ try {
243
+ record = JSON.parse(line);
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ if (record?.type !== "assistant" || record?.isApiErrorMessage !== true)
249
+ return null;
250
+ const content = record?.message?.content;
251
+ const text = typeof content === "string"
252
+ ? content
253
+ : Array.isArray(content)
254
+ ? content.map((part) => (typeof part?.text === "string" ? part.text : "")).join("")
255
+ : "";
256
+ const status = record?.apiErrorStatus;
257
+ return {
258
+ text: text.trim() || (status != null ? `API Error: ${String(status)}` : "API Error"),
259
+ at: typeof record?.timestamp === "string" ? record.timestamp : null,
260
+ };
261
+ }
@@ -272,6 +272,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, detectRat
272
272
  malformed_events: malformedEvents,
273
273
  at: ev?.at ?? null,
274
274
  rate_limit: rateLimit,
275
+ error: null,
275
276
  });
276
277
  for (;;) {
277
278
  if (!fs.existsSync(metadataFile))
@@ -202,6 +202,7 @@ export async function observeCursorDone(meta, timeout, requestedCursor, detectRa
202
202
  malformed_events: malformedEvents,
203
203
  at: ev?.at ?? null,
204
204
  rate_limit: rateLimit,
205
+ error: null,
205
206
  });
206
207
  for (;;) {
207
208
  if (!fs.existsSync(metadataFile))
@@ -81,7 +81,7 @@ export function grokEventsTranscript(meta) {
81
81
  export function grokCompletionEvent(meta, record) {
82
82
  if ((meta.kind !== "grok" && meta.kind !== "composer") ||
83
83
  record?.type !== "turn_ended" ||
84
- (record?.outcome !== "completed" && record?.outcome !== "cancelled"))
84
+ (record?.outcome !== "completed" && record?.outcome !== "cancelled" && record?.outcome !== "error"))
85
85
  return null;
86
86
  const turnId = typeof record?.ts === "string" || typeof record?.ts === "number" ? String(record.ts) : null;
87
87
  return {
@@ -93,7 +93,9 @@ export function grokCompletionEvent(meta, record) {
93
93
  turn_id: turnId,
94
94
  operation_id: null,
95
95
  reason: `Grok transcript turn_ended:${record.outcome}`,
96
- done_status: "turn_done",
96
+ // Grok CLIはAPIエラー等でturnを打ち切る時も turn_ended を書き、outcome だけが error になる
97
+ // (実測: 手元の events.jsonl に outcome=error が17件)。完了と同じ境界だが結果は無い。
98
+ done_status: record.outcome === "error" ? "turn_error" : "turn_done",
97
99
  stop_hook_active: false,
98
100
  at: typeof record?.ts === "string" ? record.ts : new Date().toISOString(),
99
101
  };
@@ -166,6 +168,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
166
168
  malformed_events: malformedEvents,
167
169
  at: ev?.at ?? null,
168
170
  rate_limit: rateLimit,
171
+ error: ev?.done_status === "turn_error" ? ev.reason : null,
169
172
  });
170
173
  for (;;) {
171
174
  if (!fs.existsSync(metadataFile))
@@ -204,7 +207,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
204
207
  try {
205
208
  const done = grokCompletionEvent(meta, JSON.parse(line));
206
209
  if (done)
207
- return observation("done", done);
210
+ return observation(done.done_status === "turn_error" ? "error" : "done", done);
208
211
  }
209
212
  catch {
210
213
  malformedEvents++;
package/dist/index.js CHANGED
@@ -136,6 +136,11 @@ server.registerTool("pty_send", {
136
136
  .describe("非Claude agent sessionでは自動dispatchせず素送信する。aiterm相関付きClaudeのactive turnには使えない"),
137
137
  rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
138
138
  raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
139
+ image: z
140
+ .array(z.string())
141
+ .optional()
142
+ .describe("添付する画像ファイルの絶対パス(png/jpg/jpeg/gif/webp)。agent session への dispatch だけで使え、" +
143
+ "harness別の添付手順はaitermが吸収する。通常PTY送信やforce送信では指定できない"),
139
144
  },
140
145
  outputSchema: {
141
146
  schema: z.literal("aiterm.pty-send-result.v1"),
@@ -152,7 +157,7 @@ server.registerTool("pty_send", {
152
157
  // dispatch前に行った pane 入力の回復("fg" / "fg_stopped" / "stty_raw")。通常送信では省略(additive)。
153
158
  pane_input_recovery: z.array(z.string()).optional(),
154
159
  },
155
- }, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
160
+ }, async ({ session_id, text, enter, mark, force, rtk, raw, image }) => {
156
161
  try {
157
162
  if (!force && core.isAgentSession(session_id)) {
158
163
  if (enter === false)
@@ -161,7 +166,7 @@ server.registerTool("pty_send", {
161
166
  throw new Error("agent session への dispatch は mark:true と併用できません");
162
167
  if (rtk)
163
168
  throw new Error("agent session への dispatch は rtk:true と併用できません");
164
- const receipt = await core.dispatchAgentTurn(session_id, text, { raw });
169
+ const receipt = await core.dispatchAgentTurn(session_id, core.attachImages(text, image), { raw });
165
170
  const waitProcess = core.agentWaitProcess(receipt.session_id, receipt.event_cursor);
166
171
  return {
167
172
  content: [
@@ -186,6 +191,9 @@ server.registerTool("pty_send", {
186
191
  },
187
192
  };
188
193
  }
194
+ if (image && image.length > 0) {
195
+ throw new Error("image は agent session への dispatch(forceなし)だけで使えます。通常PTY送信では本文にpathを書いてください");
196
+ }
189
197
  const out = core.send(session_id, text, { enter, mark, force, rtk, raw });
190
198
  return {
191
199
  content: [{ type: "text", text: out }],
@@ -212,6 +220,7 @@ server.registerTool("agent_steer", {
212
220
  inputSchema: {
213
221
  session_id: z.string(),
214
222
  text: z.string().describe("現在のターンへ追加する文字列。UTF-8で最大64KiB"),
223
+ image: z.array(z.string()).optional().describe("添付する画像ファイルの絶対パス(png/jpg/jpeg/gif/webp)"),
215
224
  },
216
225
  outputSchema: {
217
226
  schema: z.literal("aiterm.agent-steer.v1"),
@@ -221,9 +230,9 @@ server.registerTool("agent_steer", {
221
230
  harness: z.enum(["codex-cli", "grok-cli"]),
222
231
  delivery: z.enum(["steered", "idle"]),
223
232
  },
224
- }, async ({ session_id, text }) => {
233
+ }, async ({ session_id, text, image }) => {
225
234
  try {
226
- const receipt = await core.steerAgentTurn(session_id, text);
235
+ const receipt = await core.steerAgentTurn(session_id, core.attachImages(text, image));
227
236
  return {
228
237
  content: [{ type: "text", text: `${receipt.delivery} ${receipt.session_id}` }],
229
238
  structuredContent: receipt,
@@ -547,13 +556,14 @@ const agentEnvironmentDesc = `通常CLIと同じHOME・cwd・project/user/local
547
556
  `delegation depth/lineage、delegation_allowed=trueを注入し、必要な追加委譲は許可する。`;
548
557
  async function launchAgent(kind, args) {
549
558
  const supportsWriteScope = kind !== "claude";
550
- const { prompt, throughline_source_session, throughline_supplement_file, model, reasoning_effort, env_vars, cwd, session_name, launch_operation_id, write_scope } = args;
559
+ const { prompt, image, throughline_source_session, throughline_supplement_file, model, reasoning_effort, env_vars, cwd, session_name, launch_operation_id, write_scope } = args;
551
560
  try {
552
561
  if (!supportsWriteScope && write_scope !== undefined) {
553
562
  throw new core.AitermError("claude-code harnessはwrite_scopeに対応していません。指定を外してください", 2);
554
563
  }
564
+ const initialPrompt = image && image.length > 0 ? core.attachImages(prompt ?? "", image) : prompt ?? undefined;
555
565
  const [sid, hint, eventCursor, submitResidue] = await core.openAgentWithInitialPrompt(kind, {
556
- prompt: prompt ?? undefined,
566
+ prompt: initialPrompt,
557
567
  throughline_source_session,
558
568
  throughline_supplement_file,
559
569
  model: model ?? undefined,
@@ -663,6 +673,7 @@ server.registerTool("agent_launch", {
663
673
  inputSchema: {
664
674
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).describe("agent loop・session・hook・transcript・認証を所有する実行基盤"),
665
675
  prompt: z.string().nullish().describe("起動時に渡す初手プロンプト(任意)。送信後は待たずに即返る"),
676
+ image: z.array(z.string()).optional().describe("初手プロンプトへ添付する画像ファイルの絶対パス(png/jpg/jpeg/gif/webp)"),
666
677
  throughline_source_session: z.string().min(1).optional().describe("同一端末のThroughline sessionから読み取り専用contextを初手へ注入する"),
667
678
  throughline_supplement_file: z.string().min(1).optional().describe("Throughline 0.10.8以降へそのまま渡すproject束縛済み長期記憶・知識の補足JSON path"),
668
679
  model: z.string().nullish().describe("harnessが選ぶモデル。provider名ではなくlive catalog上のmodel ID"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.29.31",
3
+ "version": "0.31.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": [