aiterm-mcp 0.12.2 → 0.15.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/dist/index.js CHANGED
@@ -33,6 +33,7 @@ function fail(e) {
33
33
  */
34
34
  async function factoryDiagnostics() {
35
35
  const ptyList = core.readOnlyPtyListDiagnostic();
36
+ const claude = core.vendorLauncherDiagnostic("claude");
36
37
  const codex = core.vendorLauncherDiagnostic("codex");
37
38
  const grok = core.vendorLauncherDiagnostic("grok");
38
39
  const runtimeErrors = await runtimeErrorStoreDiagnostic();
@@ -45,6 +46,11 @@ async function factoryDiagnostics() {
45
46
  pty_list: { access: "read_only", ...ptyList },
46
47
  runtime_error_store: runtimeErrors,
47
48
  vendor_dependencies: {
49
+ claude: {
50
+ status: claude,
51
+ optional: true,
52
+ required_for: ["claude_agent"],
53
+ },
48
54
  codex: {
49
55
  status: codex,
50
56
  optional: true,
@@ -81,18 +87,26 @@ server.registerTool("pty_open", {
81
87
  });
82
88
  server.registerTool("pty_send", {
83
89
  description: "セッションへテキスト(コマンド)を送る。通常は送信だけ行い、出力は pty_read で取得する。" +
84
- "agent_done:true で起動した Codex/Grok/Composer セッションは wait:'agent_done' で Stop hook まで待てる。",
90
+ "agent_done:true で起動した Claude/Codex/Grok/Composer セッションは wait:'agent_done' で Stop hook まで待てる。" +
91
+ "managed Claude sessionはturn相関のためwait:'agent_done'を必須とし、通常送信を拒否する。",
85
92
  inputSchema: {
86
93
  session_id: z.string(),
87
- text: z.string().describe("送る文字列(コマンド)"),
94
+ text: z
95
+ .string()
96
+ .describe("送る文字列(コマンド)。UTF-8で最大64KiB"),
88
97
  enter: z.boolean().default(true).describe("末尾で Enter を送る"),
89
98
  wait: z
90
99
  .enum(["none", "agent_done"])
91
100
  .default("none")
92
- .describe("none=従来通り送信のみ。agent_done=agent Stop hook まで待って最終画面を返す"),
101
+ .describe("none=従来通り送信のみ(managed Claudeでは拒否)。agent_done=agent Stop hook まで待って最終画面を返す"),
93
102
  timeout: z.number().default(600).describe("wait:'agent_done' の最大待ち秒数"),
94
103
  screen: z.boolean().default(true).describe("wait:'agent_done' の返り値を描画済みスクリーンにする"),
95
104
  lines: z.number().int().nullish().describe("wait:'agent_done' で返す末尾 N 行"),
105
+ operation_id: z
106
+ .string()
107
+ .regex(/^sha256:[0-9a-f]{64}$/)
108
+ .nullish()
109
+ .describe("Claudeのdurable caller operation ID。wait:'agent_done'時だけ指定し、timeout後の同一結果回収へ使う"),
96
110
  mark: z
97
111
  .boolean()
98
112
  .default(false)
@@ -106,7 +120,7 @@ server.registerTool("pty_send", {
106
120
  rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
107
121
  raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
108
122
  },
109
- }, async ({ session_id, text, enter, wait, timeout, screen, lines, mark, force, rtk, raw }) => {
123
+ }, async ({ session_id, text, enter, wait, timeout, screen, lines, operation_id, mark, force, rtk, raw }) => {
110
124
  try {
111
125
  if (wait === "agent_done") {
112
126
  return ok(await core.sendAndWaitAgentDone(session_id, text, {
@@ -118,8 +132,11 @@ server.registerTool("pty_send", {
118
132
  timeout,
119
133
  screen,
120
134
  lines: lines ?? null,
135
+ operation_id: operation_id ?? null,
121
136
  }));
122
137
  }
138
+ if (operation_id != null)
139
+ throw new Error("operation_idはwait:'agent_done'時だけ指定できます");
123
140
  return ok(core.send(session_id, text, { enter, mark, force, rtk, raw }));
124
141
  }
125
142
  catch (e) {
@@ -129,7 +146,7 @@ server.registerTool("pty_send", {
129
146
  server.registerTool("pty_read", {
130
147
  description: "セッションの出力をトークン削減して読む(既定は前回読取位置からの増分)。" +
131
148
  "削減: 制御文字除去 / 反復圧縮 / head+tail 折りたたみ+復元ヒント+メタ併記。" +
132
- "agent_transcript:true は agent session の直近完了ターンの最終 assistant メッセージを vendor transcript から平文で返す。" +
149
+ "agent_transcript:true は agent session の直近完了ターンの最終 assistant メッセージを公開されたvendor記録から平文で返す。" +
133
150
  "長い回答が screen tail で切れた時の回収用。",
134
151
  inputSchema: {
135
152
  session_id: z.string(),
@@ -155,16 +172,26 @@ server.registerTool("pty_read", {
155
172
  agent_transcript: z
156
173
  .boolean()
157
174
  .default(false)
158
- .describe("agent session の直近完了ターンの最終 assistant メッセージを vendor transcript から平文で返す。長い回答が screen tail で切れた時の回収用"),
175
+ .describe("agent session の直近完了ターンの最終 assistant メッセージを返す。Claudeはmanaged Stop hook result、他vendorはtranscriptを使う。長い回答がscreen tailで切れた時の回収用"),
176
+ operation_id: z
177
+ .string()
178
+ .regex(/^sha256:[0-9a-f]{64}$/)
179
+ .nullish()
180
+ .describe("Claude operationの期待ID。agent_transcript:true時だけ指定し、古い別operationの結果を拒否する"),
159
181
  },
160
- }, async ({ session_id, wait, until, until_regex, timeout, screen, full, lines, line_range, raw, rtk, agent_transcript }) => {
182
+ }, async ({ session_id, wait, until, until_regex, timeout, screen, full, lines, line_range, raw, rtk, agent_transcript, operation_id }) => {
161
183
  try {
162
184
  if (agent_transcript) {
163
185
  if (screen || full || rtk || wait || line_range != null) {
164
186
  throw new Error("agent_transcript:true は screen / full / rtk / line_range / wait と併用できません。lines のみ指定できます。");
165
187
  }
166
- return ok(await core.readAgentTranscript(session_id, { lines: lines ?? null }));
188
+ return ok(await core.readAgentTranscript(session_id, {
189
+ lines: lines ?? null,
190
+ operation_id: operation_id ?? null,
191
+ }));
167
192
  }
193
+ if (operation_id != null)
194
+ throw new Error("operation_idはagent_transcript:true時だけ指定できます");
168
195
  let range = null;
169
196
  if (line_range) {
170
197
  const idx = line_range.indexOf(":");
@@ -199,7 +226,7 @@ server.registerTool("pty_read", {
199
226
  }
200
227
  });
201
228
  server.registerTool("pty_key", {
202
- description: "制御キーを送る(C-c, C-d, Enter, Tab, Up, Down... の別名に対応)。",
229
+ description: "制御キーを送る(C-c, C-d, Enter, Tab, Up, Down... の別名に対応)。managed Claude sessionではturn相関を守るためC-cだけを許可する。",
203
230
  inputSchema: {
204
231
  session_id: z.string(),
205
232
  key: z.string().describe('キー名(例 "C-c", "Enter", "Up")'),
@@ -213,11 +240,21 @@ server.registerTool("pty_key", {
213
240
  }
214
241
  });
215
242
  server.registerTool("pty_close", {
216
- description: "セッションを閉じ、ログ/読取位置を破棄する。",
243
+ description: "セッションを閉じ、ログ/読取位置を破棄する。同じsession_idへの再試行は安全で、" +
244
+ "closed/already_closedのstructured receiptを返す。",
217
245
  inputSchema: { session_id: z.string() },
246
+ outputSchema: {
247
+ schema: z.literal("aiterm.pty-close-result.v1"),
248
+ session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
249
+ outcome: z.enum(["closed", "already_closed"]),
250
+ },
218
251
  }, async ({ session_id }) => {
219
252
  try {
220
- return ok(core.closeSession(session_id));
253
+ const result = core.closeSessionResult(session_id);
254
+ return {
255
+ content: [{ type: "text", text: `closed ${session_id}` }],
256
+ structuredContent: { ...result },
257
+ };
221
258
  }
222
259
  catch (e) {
223
260
  return fail(e);
@@ -234,20 +271,61 @@ server.registerTool("pty_list", {
234
271
  return fail(e);
235
272
  }
236
273
  });
274
+ server.registerTool("claude_turn", {
275
+ description: "managed Claude sessionのdurable operationを構造化issue/recoverするmachine-caller専用面。" +
276
+ "pending/unknown/completedを人間向けerror文字列の解析なしで返し、Observer固有ロジックは持たない。",
277
+ inputSchema: {
278
+ action: z.enum(["issue", "recover"]),
279
+ session_id: z.string(),
280
+ operation_id: z.string().regex(/^sha256:[0-9a-f]{64}$/),
281
+ text: z.string().optional().describe("issueだけに指定するbounded turn本文"),
282
+ timeout: z.number().min(0).max(3600).optional().describe("issueだけに指定するStop待ち秒数"),
283
+ },
284
+ outputSchema: {
285
+ schema: z.literal("aiterm.claude-operation-result.v1"),
286
+ action: z.enum(["issue", "recover"]),
287
+ status: z.enum(["accepted", "pending", "completed", "unknown"]),
288
+ session_id: z.string(),
289
+ operation_id: z.string().regex(/^sha256:[0-9a-f]{64}$/),
290
+ raw_output: z.string().nullable(),
291
+ reason: z.enum(["operation_not_found", "result_unknown"]).nullable(),
292
+ },
293
+ }, async ({ action, session_id, operation_id, text, timeout }) => {
294
+ try {
295
+ const result = await core.runClaudeOperation({
296
+ action,
297
+ session_id,
298
+ operation_id,
299
+ text: text ?? undefined,
300
+ timeout,
301
+ });
302
+ return {
303
+ content: [{ type: "text", text: JSON.stringify(result) }],
304
+ structuredContent: { ...result },
305
+ };
306
+ }
307
+ catch (e) {
308
+ return fail(e);
309
+ }
310
+ });
237
311
  // 対話型エージェント起動ツール(モデルごとに1つ=ツール名/説明でどのモデルか一目で分かる)。
238
312
  // いずれも永続端末に TUI を起動し session_id を返す。以後 pty_read/pty_send で対話操作する。
239
- const agentModelDesc = (kind) => kind === "codex"
240
- ? "起動モデル(例: gpt-5.6-sol / gpt-5.6-terra / gpt-5.6-luna)。省略時は端末 config/CLI 既定を継承" +
241
- "(端末側のピンがそのまま効く。実効値は起動応答に明示される)"
242
- : `起動モデル。省略時は ${kind === "grok" ? "grok-4.5" : "grok-composer-2.5-fast"}`;
243
- const agentEffortDesc = (grokLike) => grokLike
244
- ? "指定不可(grok CLI の --effort は headless 専用で、対話 TUI では警告の上無視される。" +
245
- "composer は effort 自体非対応)。指定すると起動前にエラーを返す"
246
- : "reasoning effort(思考レベル)。low/medium/high/xhigh/max/ultra(CLI 版依存)。" +
247
- "ultra は max 推論+proactive 自動委譲 ON=使用量急増注意(明示要求時のみ)。省略時は端末 config/CLI 既定。";
248
- function registerAgentTool(toolName, kind, desc, grokLike) {
313
+ const agentModelDesc = (kind) => kind === "claude"
314
+ ? "起動モデル(例: claude-sonnet-4-6)。省略時はClaude CLI既定"
315
+ : kind === "codex"
316
+ ? "起動モデル(例: gpt-5.6-sol / gpt-5.6-terra / gpt-5.6-luna)。省略時は端末 config/CLI 既定を継承" +
317
+ "(端末側のピンがそのまま効く。実効値は起動応答に明示される)"
318
+ : `起動モデル。省略時は ${kind === "grok" ? "grok-4.5" : "grok-composer-2.5-fast"}`;
319
+ const agentEffortDesc = (kind) => kind === "claude"
320
+ ? "Claude Code reasoning effort。low/medium/high/xhigh/max。省略時はCLI既定"
321
+ : kind === "grok" || kind === "composer"
322
+ ? "指定不可(grok CLI の --effort は headless 専用で、対話 TUI では警告の上無視される。" +
323
+ "composer は effort 自体非対応)。指定すると起動前にエラーを返す"
324
+ : "reasoning effort(思考レベル)。low/medium/high/xhigh/max/ultra(CLI 版依存)。" +
325
+ "ultra は max 推論+proactive 自動委譲 ON=使用量急増注意(明示要求時のみ)。省略時は端末 config/CLI 既定。";
326
+ function registerAgentTool(toolName, kind, desc) {
249
327
  const initialPromptWaitSchema = {};
250
- if (kind === "codex") {
328
+ if (kind === "codex" || kind === "claude") {
251
329
  initialPromptWaitSchema.wait = z
252
330
  .enum(["none", "agent_done"])
253
331
  .default("none")
@@ -256,6 +334,14 @@ function registerAgentTool(toolName, kind, desc, grokLike) {
256
334
  initialPromptWaitSchema.screen = z.boolean().default(true).describe("wait:'agent_done' の返り値を描画済みスクリーンにする");
257
335
  initialPromptWaitSchema.lines = z.number().int().nullish().describe("wait:'agent_done' で返す末尾 N 行");
258
336
  }
337
+ const correlatedLaunchSchema = {};
338
+ if (kind === "claude") {
339
+ correlatedLaunchSchema.launch_operation_id = z
340
+ .string()
341
+ .regex(/^sha256:[0-9a-f]{64}$/)
342
+ .optional()
343
+ .describe("promptless managed launchのexact replay相関ID。session_nameとagent_done:trueが必須");
344
+ }
259
345
  server.registerTool(toolName, {
260
346
  description: desc,
261
347
  inputSchema: {
@@ -263,16 +349,23 @@ function registerAgentTool(toolName, kind, desc, grokLike) {
263
349
  model: z.string().nullish().describe(agentModelDesc(kind)),
264
350
  // grok/composer の effort は対話 TUI で無効(headless 専用)=core 側が起動前に明示エラーで拒否。
265
351
  // codex は CLI 側の値集合が版で変わるため縛らない(core 側も同方針)。
266
- reasoning_effort: z.string().nullish().describe(agentEffortDesc(grokLike)),
352
+ reasoning_effort: z.string().nullish().describe(agentEffortDesc(kind)),
267
353
  cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
268
354
  session_name: z.string().nullish().describe("セッション名(省略で自動採番)"),
269
355
  agent_done: z
270
356
  .boolean()
271
357
  .default(false)
272
358
  .describe("managed Stop hook を有効化し、pty_send(wait:'agent_done') を使えるようにする"),
359
+ ...correlatedLaunchSchema,
273
360
  ...initialPromptWaitSchema,
274
361
  },
275
- }, async ({ prompt, model, reasoning_effort, cwd, session_name, agent_done, wait, timeout, screen, lines }) => {
362
+ outputSchema: {
363
+ schema: z.literal("aiterm.agent-launch-result.v1"),
364
+ provider: z.literal(kind),
365
+ session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
366
+ managed_completion: z.boolean(),
367
+ },
368
+ }, async ({ prompt, model, reasoning_effort, cwd, session_name, agent_done, launch_operation_id, wait, timeout, screen, lines }) => {
276
369
  try {
277
370
  const [sid, hint] = await core.openAgentWithInitialPrompt(kind, {
278
371
  prompt: prompt ?? undefined,
@@ -281,25 +374,37 @@ function registerAgentTool(toolName, kind, desc, grokLike) {
281
374
  cwd: cwd ?? undefined,
282
375
  session_name: session_name ?? undefined,
283
376
  agent_done: agent_done ?? false,
377
+ launch_operation_id: launch_operation_id ?? undefined,
284
378
  wait: wait ?? "none",
285
379
  timeout,
286
380
  screen,
287
381
  lines,
288
382
  });
289
- return ok(`session_id: ${sid}\n${hint}`);
383
+ const structured = {
384
+ schema: "aiterm.agent-launch-result.v1",
385
+ provider: kind,
386
+ session_id: sid,
387
+ managed_completion: agent_done ?? false,
388
+ };
389
+ return {
390
+ content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
391
+ structuredContent: structured,
392
+ };
290
393
  }
291
394
  catch (e) {
292
395
  return fail(e);
293
396
  }
294
397
  });
295
398
  }
399
+ registerAgentTool("claude_agent", "claude", "【Claude Code (Anthropic)】の対話エージェントTUIを永続端末に起動する。`claude -p`ではなく、" +
400
+ "同じ利用者可視sessionへpty_sendで継続入力する。agent_done:trueではisolated settingsのmanaged Stop hookで完了と結果を回収する。");
296
401
  registerAgentTool("codex_agent", "codex", "【Codex (OpenAI)】の対話エージェント TUI を永続端末に起動する。実装・レビュー・調査を対話で回す。" +
297
402
  "起動後は pty_read で画面を読み pty_send で操作する。model / reasoning_effort を引数で指定可" +
298
- "(省略時は端末 config/CLI 既定を継承。実効値は起動応答に明示)。", false);
403
+ "(省略時は端末 config/CLI 既定を継承。実効値は起動応答に明示)。");
299
404
  registerAgentTool("grok_agent", "grok", "【Grok Build の Grok モデル (既定 grok-4.5)】の対話エージェント TUI を永続端末に起動する。" +
300
- "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は対話 TUI 非対応(指定はエラー)。", true);
405
+ "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は対話 TUI 非対応(指定はエラー)。");
301
406
  registerAgentTool("composer_agent", "composer", "【Grok Build の Composer モデル (既定 grok-composer-2.5-fast)】の対話エージェント TUI を永続端末に起動する。" +
302
- "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は非対応(指定はエラー)。", true);
407
+ "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は非対応(指定はエラー)。");
303
408
  async function main() {
304
409
  const transport = new StdioServerTransport();
305
410
  await server.connect(transport);
@@ -31,6 +31,11 @@ const RECORD_KEYS = [
31
31
  const DIAGNOSTIC_KEYS = ["status", "collection", "record_count", "unacknowledged_count"];
32
32
  const ARCHES = new Set(["x64", "arm64", "arm", "ia32"]);
33
33
  const EMPTY_STATE = () => ({ schema_version: STORE_SCHEMA, cursor: 0, acknowledged_cursor: 0, records: [] });
34
+ // readLock と queue scan の内部だけで使う。entry 本文など外部入力の Error message で
35
+ // disappearance を判定しないため、公開せず型そのものを識別子にする。
36
+ class LockDisappearedOrReplacedError extends Error {
37
+ constructor() { super("runtime error store lock が消滅または置換されました"); }
38
+ }
34
39
  function isObject(value) {
35
40
  return value !== null && typeof value === "object" && !Array.isArray(value);
36
41
  }
@@ -426,6 +431,10 @@ export class RuntimeErrorStore {
426
431
  }
427
432
  else {
428
433
  const before = fs.lstatSync(lock);
434
+ // APFS の高競合では readdir snapshot 後、正当に unlink 済みの entry が pre-open
435
+ // lstat で nlink=0 と観測される。これは置換/消滅として caller の既存 skip 経路へ渡す。
436
+ if (before.nlink === 0)
437
+ throw new LockDisappearedOrReplacedError();
429
438
  if (!before.isFile() || before.isSymbolicLink() || before.nlink < 1 || before.nlink > 2
430
439
  || before.uid !== process.getuid() || (before.mode & 0o777) !== 0o600 || before.size > 4096) {
431
440
  throw new Error("runtime error store lock が不正です");
@@ -433,9 +442,10 @@ export class RuntimeErrorStore {
433
442
  const fd = fs.openSync(lock, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
434
443
  try {
435
444
  const after = fs.fstatSync(fd);
436
- if (before.dev !== after.dev || before.ino !== after.ino || after.nlink < 1 || after.nlink > 2) {
437
- throw new Error("runtime error store lock が置換されました");
438
- }
445
+ if (before.dev !== after.dev || before.ino !== after.ino || after.nlink === 0)
446
+ throw new LockDisappearedOrReplacedError();
447
+ if (after.nlink < 1 || after.nlink > 2)
448
+ throw new Error("runtime error store lock が不正です");
439
449
  text = fs.readFileSync(fd, "utf8");
440
450
  }
441
451
  finally {
@@ -532,8 +542,7 @@ export class RuntimeErrorStore {
532
542
  current = this.readLock(currentPath);
533
543
  }
534
544
  catch (error) {
535
- if (error.code === "ENOENT"
536
- || (error instanceof Error && error.message.includes("置換されました")))
545
+ if (error.code === "ENOENT" || error instanceof LockDisappearedOrReplacedError)
537
546
  continue;
538
547
  throw error;
539
548
  }
@@ -568,8 +577,7 @@ export class RuntimeErrorStore {
568
577
  current = this.readLock(currentPath);
569
578
  }
570
579
  catch (error) {
571
- if (error.code === "ENOENT"
572
- || (error instanceof Error && error.message.includes("置換されました")))
580
+ if (error.code === "ENOENT" || error instanceof LockDisappearedOrReplacedError)
573
581
  continue;
574
582
  throw error;
575
583
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.12.2",
3
+ "version": "0.15.0",
4
4
  "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
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 Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
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": [
7
7
  "mcp",
8
8
  "mcp-server",
@@ -33,7 +33,8 @@
33
33
  "type": "module",
34
34
  "bin": {
35
35
  "aiterm-mcp": "dist/index.js",
36
- "aiterm-runtime-errors": "dist/runtime-errors-cli.js"
36
+ "aiterm-runtime-errors": "dist/runtime-errors-cli.js",
37
+ "aiterm-wait": "dist/aiterm-wait-cli.js"
37
38
  },
38
39
  "files": [
39
40
  "dist",