aiterm-mcp 0.37.10 → 0.38.1

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,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.38.1] - 2026-09-22
11
+
12
+ ### 修正
13
+
14
+ - Cursor親のhookへ渡るtool結果は本文だけで、`structuredContent`の`delivery_id`が落ちる。dispatchの本文へ`delivery_id`行を出し、hookがその行から会話へ束縛する。
15
+
16
+ ## [0.38.0] - 2026-09-22
17
+
18
+ ### 追加
19
+
20
+ - Cursor親(`cursor-vscode`)へ子の回答を自動配送する。`aiterm-setup`が`~/.cursor/hooks.json`へ`afterMCPExecution`と`postToolUse`を追加し、作業中は次のツール返りへ`additional_context`で本文を差し込む。ターンを終える前にreceiptの`wait_process`を背景で起動すると、idle中の完了でも受け口が本文を出して起きる。hook未登録は送信前に`CURSOR_PARENT_HOOK_UNAVAILABLE`で止める。解除は`aiterm-setup --remove-cursor-parent-hooks`。Codex/Claude Codeの配送契約は変えない。
21
+
10
22
  ## [0.37.10] - 2026-09-21
11
23
 
12
24
  ### 修正
@@ -1732,7 +1744,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1732
1744
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1733
1745
  provenance.
1734
1746
 
1735
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.10...HEAD
1747
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.38.1...HEAD
1748
+ [0.38.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.38.0...v0.38.1
1749
+ [0.38.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.10...v0.38.0
1736
1750
  [0.37.10]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.9...v0.37.10
1737
1751
  [0.37.9]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.8...v0.37.9
1738
1752
  [0.37.8]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.7...v0.37.8
package/README.ja.md CHANGED
@@ -202,7 +202,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
202
202
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
203
203
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
204
204
 
205
- **状態:** 開発継続中 · 現行公開版 **v0.37.10** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
205
+ **状態:** 開発継続中 · 現行公開版 **v0.38.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
206
206
 
207
207
  ### 更新と巻き戻し
208
208
 
@@ -550,11 +550,11 @@ SSH先がPowerShellの場合、`mark:true`は現在の標準`PS ...>`プロン
550
550
 
551
551
  `pty_read({ wait: true })`は通常PTYを、process終了/`mark:true` sentinel/`until`一致/shell復帰を伴う出力静止/timeoutの5層で判定する。agent sessionは第6の正確な層を使い、Codexは通常rollout、Grokは通常session event、Claudeはlaunch相関Stop event、Cursorは通常agent transcriptの`turn_ended`を`aiterm-wait --cursor`が観測する。親はブロックもポーリングもしない。
552
552
 
553
- ### Codex/Claude Code親への回答自動配送
553
+ ### Codex/Claude Code/Cursor親への回答自動配送
554
554
 
555
555
  Codex/Claude Codeから子を起動・通常dispatchした後は、別作業へ進むか親のturnを終了するだけでよい。Aitermが完了を観測し、加工前の回答を保存して親へ届ける。waiter、`pty_read`による回答回収、子への送信指示は不要。子は全対応harnessから選べる。
556
556
 
557
- 自動配送時はreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexの公式受信口での受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、送信中に接続が切れて結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
557
+ Codex/Claude Codeの自動配送ではreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexの公式受信口での受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、送信中に接続が切れて結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
558
558
 
559
559
  単品導入のCodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
560
560
 
@@ -564,6 +564,8 @@ Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が
564
564
 
565
565
  hookを持たない旧版へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
566
566
 
567
+ Cursor親(`clientInfo.name`が`cursor-vscode`)も同じ完了観測と本文保存を使う。`aiterm-setup`が`~/.cursor/hooks.json`へ`afterMCPExecution`と`postToolUse`を追加し、他製品のhookと順序は保持する。hookが無い場合は子への送信前に`CURSOR_PARENT_HOOK_UNAVAILABLE`で止める。作業を続けていれば次のツール返りへ`additional_context`で本文が差し込まれ、ターンを終える前にreceiptの`wait_process`を背景で起動しておけばidle中の完了でも起きられる。`wait_command`はnull。`submitted`はhookまたは受け口が本文を受け取った状態であり、modelの読了ではない。24時間以内に受け取りが無ければ`failed`とし、本文は残して自動再送しない。解除は`aiterm-setup --remove-cursor-parent-hooks`。Cursor Cloud Agent/Background Agentはこの受信契約に含めない。
568
+
567
569
  Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
568
570
 
569
571
 
package/README.md CHANGED
@@ -216,7 +216,7 @@ collection is off by default and performs no network I/O. It ships via
216
216
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
217
217
  Release re-registers the Official MCP Registry entry.
218
218
 
219
- **Status:** actively maintained · current public release **v0.37.10** · 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).
219
+ **Status:** actively maintained · current public release **v0.38.1** · 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).
220
220
 
221
221
  ### Update and rollback
222
222
 
@@ -587,9 +587,9 @@ For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` p
587
587
 
588
588
  ### Completion push for parent agents (`aiterm-wait`)
589
589
 
590
- **Codex/Claude Code親には子の回答本文が自動で届く。** 子を起動・dispatchした後は、別作業へ進むか親のturnを終える。Aitermが完了を観測し、加工前の本文を保存して親へ渡す。waiter、`pty_read`による回答回収、子への返送指示は不要。子は全対応harnessから選べる。
590
+ **Codex/Claude Code/Cursor親には子の回答本文が自動で届く。** Codex/Claude Codeでは、子を起動・dispatchした後は別作業へ進むか親のturnを終える。Aitermが完了を観測し、加工前の本文を保存して親へ渡す。waiter、`pty_read`による回答回収、子への返送指示は不要。子は全対応harnessから選べる。
591
591
 
592
- 自動配送のreceiptには`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexの公式受信口での受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、出力中断で結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
592
+ Codex/Claude Codeの自動配送ではreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexの公式受信口での受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、出力中断で結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
593
593
 
594
594
  For queue delivery, use a Codex runtime that supplies MCP `_meta.threadId` and the official `thread/queue` API (verified with Codex CLI 0.154.0). `aiterm-setup` checks the installed queue entry point; Aiterm verifies the requesting thread before each dispatch. Codex native sub-agents reject external queue input and cannot be automatic-delivery parents. Steer相当の選択時も公式キューへ投入し、専用hookが同一ターンへ取り込みます。
595
595
 
@@ -599,6 +599,8 @@ Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が
599
599
 
600
600
  hookを持たない旧版へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
601
601
 
602
+ Cursor parents (`clientInfo.name` of `cursor-vscode`) use the same completion capture. `aiterm-setup` adds `afterMCPExecution` and `postToolUse` to `~/.cursor/hooks.json` and keeps every other hook and its position. A missing registration fails the dispatch with `CURSOR_PARENT_HOOK_UNAVAILABLE` before the child is sent. While the parent keeps calling tools, the answer is injected through `additional_context` on the next tool result. If the parent ends the turn, start the receipt `wait_process` in the background first; that receiver exits when the answer arrives. `wait_command` is null. `submitted` means the hook or the receiver claimed the text, not that the model has read it. No claim within 24 hours is `failed`, the text is kept, and nothing is resent. Remove only Aiterm's entries with `aiterm-setup --remove-cursor-parent-hooks`. Cursor Cloud Agents and Background Agents are outside this contract.
603
+
602
604
  Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
603
605
 
604
606
  **For other parent hosts**, dispatch and start the receipt's waiter in a separate process:
@@ -113,6 +113,46 @@ export function ensureStateRoot() {
113
113
  export function agentsDir() {
114
114
  return path.join(ensureStateRoot(), "agents");
115
115
  }
116
+ /** directory内の状態変化を待つ。inspectが値を返した時点でresolveする。timeoutMsを省くと期限なし。 */
117
+ export function waitForFileState(dir, inspect, timeoutMs) {
118
+ return new Promise((resolve, reject) => {
119
+ let finished = false;
120
+ let timer;
121
+ let timeout;
122
+ const watcher = fs.watch(dir, () => check());
123
+ const finish = (error, value) => {
124
+ if (finished)
125
+ return;
126
+ finished = true;
127
+ watcher.close();
128
+ if (timer)
129
+ clearInterval(timer);
130
+ if (timeout)
131
+ clearTimeout(timeout);
132
+ if (error)
133
+ reject(error);
134
+ else
135
+ resolve(value);
136
+ };
137
+ const check = () => {
138
+ if (finished)
139
+ return;
140
+ try {
141
+ const value = inspect();
142
+ if (value !== undefined)
143
+ finish(null, value);
144
+ }
145
+ catch (error) {
146
+ finish(error);
147
+ }
148
+ };
149
+ watcher.on("error", error => finish(error));
150
+ timer = setInterval(check, 5000);
151
+ if (timeoutMs !== undefined)
152
+ timeout = setTimeout(() => finish(new Error("WAIT_FOR_FILE_TIMEOUT")), timeoutMs);
153
+ check();
154
+ });
155
+ }
116
156
  export function agentEventPath(name, launchId) {
117
157
  assertSessionName(name);
118
158
  if (!LAUNCH_ID_RE.test(launchId))
@@ -2,7 +2,7 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { z } from "zod";
5
- import { ensureStateRoot, writeJson0600 } from "./agent-shared.js";
5
+ import { ensureStateRoot, waitForFileState, writeJson0600 } from "./agent-shared.js";
6
6
  import { readRuntimeProcesses } from "./process-runtime.js";
7
7
  import { AitermError } from "./errors.js";
8
8
  const requestId = z.string().regex(/^[A-Za-z0-9_-]{1,160}$/);
@@ -99,41 +99,6 @@ export function closeClaudeParentSession(input, root = defaultRoot()) {
99
99
  writeJson0600(path.join(root, entry.name, "closed.json"), { session_id });
100
100
  }
101
101
  }
102
- // filesystemの通知を先に登録してから状態を読む。producerの終了は低頻度のprocess照合でも検出する。
103
- function waitForFileState(dir, inspect) {
104
- return new Promise((resolve, reject) => {
105
- let finished = false;
106
- let timer;
107
- const watcher = fs.watch(dir, () => check());
108
- const finish = (error, value) => {
109
- if (finished)
110
- return;
111
- finished = true;
112
- watcher.close();
113
- if (timer)
114
- clearInterval(timer);
115
- if (error)
116
- reject(error);
117
- else
118
- resolve(value);
119
- };
120
- const check = () => {
121
- if (finished)
122
- return;
123
- try {
124
- const value = inspect();
125
- if (value !== undefined)
126
- finish(null, value);
127
- }
128
- catch (error) {
129
- finish(error);
130
- }
131
- };
132
- watcher.on("error", error => finish(error));
133
- timer = setInterval(check, 5000);
134
- check();
135
- });
136
- }
137
102
  function assertParentAlive(invocation) {
138
103
  if (processIdentity(invocation.parent_pid) !== invocation.parent_started_identity) {
139
104
  throw new ClaudeDeliveryError("CLAUDE_PARENT_PROCESS_CLOSED", "依頼元のClaude processは終了しました。回答は保存したままです");
package/dist/core.js CHANGED
@@ -2556,7 +2556,13 @@ export function agentWaitProcess(session, cursor, runtime = {}) {
2556
2556
  // 親ホストの識別(MCP initialize の clientInfo.name)。配送の可否はMCP入口が検証し、ここは案内だけを作る。
2557
2557
  let parentClientName = null;
2558
2558
  function autoDeliveryParent() {
2559
- return parentClientName === "codex-mcp-client" ? "Codex" : parentClientName === "claude-code" ? "Claude Code" : null;
2559
+ if (parentClientName === "codex-mcp-client")
2560
+ return "Codex";
2561
+ if (parentClientName === "claude-code")
2562
+ return "Claude Code";
2563
+ if (parentClientName === "cursor-vscode" || parentClientName?.startsWith("cursor-vscode "))
2564
+ return "Cursor";
2565
+ return null;
2560
2566
  }
2561
2567
  export function setParentClient(name) {
2562
2568
  const trimmed = typeof name === "string" ? name.trim() : "";
@@ -2573,6 +2579,11 @@ export function agentWaitLaunchForm(command) {
2573
2579
  // dispatch / 起動時 prompt 送信後の共通案内。第一文で「待たない」を宣言し、待ち方は後段に置く。
2574
2580
  export function agentDispatchGuide(session, cursor) {
2575
2581
  const parent = autoDeliveryParent();
2582
+ if (parent === "Cursor") {
2583
+ return "回答はこの会話へ自動で届く。作業を続ければ次のツール返りに差し込まれる。" +
2584
+ "ターンを終える前にreceiptのwait_processを背景(block_until_ms: 0)で起動しておけば、idle中に完了しても起きられる。" +
2585
+ "ポーリング・pty_read(agent_transcript:true)は不要。";
2586
+ }
2576
2587
  if (parent) {
2577
2588
  return `回答本文はAitermがこの${parent}親へ自動配送する。wait起動・ポーリング・通常の回答回収は不要。` +
2578
2589
  "親は作業を続けるかターンを終え、順番待ちから届く子の回答で続行する。";
@@ -2585,6 +2596,8 @@ export function agentDispatchGuide(session, cursor) {
2585
2596
  // 未完了 session へ触った時の共通案内。ここでも待つのは waiter プロセスであって親ではない。
2586
2597
  export function agentWaitGuide(session) {
2587
2598
  const parent = autoDeliveryParent();
2599
+ if (parent === "Cursor")
2600
+ return "回答はこの会話へ届く。完了通知はreceiptのwait_processを背景で起動して受ける。ポーリングは不要。";
2588
2601
  if (parent)
2589
2602
  return `回答本文はこの${parent}親へ自動配送される。親は作業を続けるかターンを終える。`;
2590
2603
  const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // Cursorが起動する公式hook。MCP stdioとは別processで、差し込み文をstdoutのJSONへ返す。
3
+ import { fileURLToPath } from "node:url";
4
+ import * as fs from "node:fs";
5
+ import { handleCursorHook } from "./cursor-parent-receiver.js";
6
+ async function main() {
7
+ let input = "";
8
+ process.stdin.setEncoding("utf8");
9
+ for await (const chunk of process.stdin)
10
+ input += chunk;
11
+ const result = await handleCursorHook(input.length > 0 ? input : "{}");
12
+ process.stdout.write(`${JSON.stringify(result)}\n`);
13
+ }
14
+ function isDirectExecution() {
15
+ const entry = process.argv[1];
16
+ if (!entry)
17
+ return false;
18
+ try {
19
+ const self = fileURLToPath(import.meta.url);
20
+ const a = fs.realpathSync(entry);
21
+ const b = fs.realpathSync(self);
22
+ if (a === b)
23
+ return true;
24
+ return process.platform === "win32" && a.toLowerCase() === b.toLowerCase();
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ if (isDirectExecution()) {
31
+ main().catch(error => {
32
+ process.stderr.write(`${error instanceof Error ? error.message : "CURSOR_PARENT_HOOK_FAILED"}\n`);
33
+ process.exitCode = 2;
34
+ });
35
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // Cursor親がidleのとき、背景で起動して回答本文の到着を待つ受け口。
3
+ import { fileURLToPath } from "node:url";
4
+ import * as fs from "node:fs";
5
+ import { cursorHookRoot, receiveCursorAnswer } from "./cursor-parent-receiver.js";
6
+ import { windowsStartProcessArgumentList } from "./core.js";
7
+ export function cursorReceiveProcess(deliveryId, executable = process.execPath) {
8
+ const args = [fileURLToPath(import.meta.url), "--delivery", deliveryId];
9
+ return {
10
+ executable,
11
+ args,
12
+ windows_start_process_argument_list: process.platform === "win32" ? windowsStartProcessArgumentList(args) : null,
13
+ };
14
+ }
15
+ function emit(value) {
16
+ process.stdout.write(JSON.stringify(value) + "\n");
17
+ }
18
+ export async function main(argv) {
19
+ if (argv.length !== 2 || argv[0] !== "--delivery" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(argv[1])) {
20
+ emit({ ok: false, code: "CURSOR_PARENT_RECEIVE_USAGE", message: "usage: cursor-parent-receive --delivery <uuid>" });
21
+ return 1;
22
+ }
23
+ const result = await receiveCursorAnswer(cursorHookRoot(), argv[1]);
24
+ if (result.outcome === "timeout") {
25
+ emit({ delivery_id: argv[1], outcome: "timeout" });
26
+ return 3;
27
+ }
28
+ if (result.outcome === "delivered_by_hook") {
29
+ emit({ delivery_id: argv[1], outcome: "delivered_by_hook" });
30
+ return 0;
31
+ }
32
+ emit({ delivery_id: argv[1], outcome: "delivered", text: result.text });
33
+ return 0;
34
+ }
35
+ function isDirectExecution() {
36
+ const entry = process.argv[1];
37
+ if (!entry)
38
+ return false;
39
+ try {
40
+ const self = fileURLToPath(import.meta.url);
41
+ const a = fs.realpathSync(entry);
42
+ const b = fs.realpathSync(self);
43
+ if (a === b)
44
+ return true;
45
+ return process.platform === "win32" && a.toLowerCase() === b.toLowerCase();
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ if (isDirectExecution()) {
52
+ main(process.argv.slice(2)).then(code => { process.exitCode = code; }).catch(error => {
53
+ emit({ ok: false, code: "CURSOR_PARENT_RECEIVE_FAILED", message: error instanceof Error ? error.message : "cursor-parent-receive: operation failed" });
54
+ process.exitCode = 1;
55
+ });
56
+ }
@@ -0,0 +1,255 @@
1
+ // Cursor親の受信口。会話への差し込みはCursor公式hookのadditional_context、idle時の起床は受け口processが担う。
2
+ import * as fs from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import * as path from "node:path";
5
+ import { z } from "zod";
6
+ import { ensureStateRoot, waitForFileState, writeJson0600 } from "./agent-shared.js";
7
+ import { AitermError } from "./errors.js";
8
+ export const cursorParentSchema = z.object({
9
+ kind: z.literal("cursor"),
10
+ hook_root: z.string(),
11
+ }).strict();
12
+ const deliveryIdSchema = z.string().uuid();
13
+ const dispatchTools = new Set([
14
+ "agent_launch", "claude_agent", "codex_agent", "grok_agent", "composer_agent", "pty_send", "claude_turn",
15
+ ]);
16
+ export class CursorDeliveryError extends AitermError {
17
+ delivery_code;
18
+ outcome_unknown;
19
+ constructor(delivery_code, message, outcome_unknown = false) {
20
+ super(`${delivery_code}: ${message}`, 2);
21
+ this.delivery_code = delivery_code;
22
+ this.outcome_unknown = outcome_unknown;
23
+ }
24
+ }
25
+ export function isCursorMcpClient(clientName) {
26
+ if (clientName === undefined)
27
+ return false;
28
+ return clientName === "cursor-vscode" || clientName.startsWith("cursor-vscode ");
29
+ }
30
+ export function cursorHookRoot(state = ensureStateRoot()) {
31
+ return path.join(state, "cursor-parent-hooks");
32
+ }
33
+ export function cursorHooksFile(home = process.env.HOME ?? homedir()) {
34
+ return path.join(process.env.CURSOR_HOME ?? path.join(home, ".cursor"), "hooks.json");
35
+ }
36
+ export function cursorParentHooksRegistered(document) {
37
+ if (document === null || typeof document !== "object" || Array.isArray(document))
38
+ return false;
39
+ const hooks = document.hooks;
40
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks))
41
+ return false;
42
+ const owns = (event) => {
43
+ const list = hooks[event];
44
+ return Array.isArray(list) && list.some(entry => {
45
+ if (entry === null || typeof entry !== "object")
46
+ return false;
47
+ const command = entry.command;
48
+ return typeof command === "string" && command.includes("cursor-parent-hook.js");
49
+ });
50
+ };
51
+ return owns("postToolUse") && owns("afterMCPExecution");
52
+ }
53
+ export function verifyCursorParent(parent, hooksFile = cursorHooksFile()) {
54
+ cursorParentSchema.parse(parent);
55
+ let document;
56
+ try {
57
+ document = JSON.parse(fs.readFileSync(hooksFile, "utf8"));
58
+ }
59
+ catch {
60
+ throw new CursorDeliveryError("CURSOR_PARENT_HOOK_UNAVAILABLE", "Cursorのhookが登録されていません。aiterm-setupを実行してください");
61
+ }
62
+ if (!cursorParentHooksRegistered(document)) {
63
+ throw new CursorDeliveryError("CURSOR_PARENT_HOOK_UNAVAILABLE", "Cursorのhookが登録されていません。aiterm-setupを実行してください");
64
+ }
65
+ }
66
+ export function cursorParentFromRequest(clientName, options = {}) {
67
+ if (!isCursorMcpClient(clientName))
68
+ return null;
69
+ const parent = { kind: "cursor", hook_root: options.hookRoot ?? cursorHookRoot() };
70
+ verifyCursorParent(parent, options.hooksFile);
71
+ return parent;
72
+ }
73
+ function deliveryDir(parent, deliveryId) {
74
+ return path.join(parent.hook_root, "deliveries", deliveryIdSchema.parse(deliveryId));
75
+ }
76
+ export function prepareCursorDelivery(parent, deliveryId) {
77
+ cursorParentSchema.parse(parent);
78
+ fs.mkdirSync(deliveryDir(parent, deliveryId), { recursive: true, mode: 0o700 });
79
+ }
80
+ export async function submitCursorParentAnswer(parent, deliveryId, text, timeoutMs = 86_400_000) {
81
+ const dir = deliveryDir(parent, deliveryId);
82
+ writeJson0600(path.join(dir, "answer.json"), { delivery_id: deliveryId, text });
83
+ try {
84
+ await waitForFileState(dir, () => fs.existsSync(path.join(dir, "claim.json")) ? true : undefined, timeoutMs);
85
+ }
86
+ catch (error) {
87
+ if (error instanceof Error && error.message === "WAIT_FOR_FILE_TIMEOUT") {
88
+ throw new CursorDeliveryError("CURSOR_PARENT_DELIVERY_UNCLAIMED", "Cursor親が時間内に回答を受け取りませんでした。回答は保存したままです");
89
+ }
90
+ throw error;
91
+ }
92
+ return { queued_submission_id: null };
93
+ }
94
+ function conversationId(value) {
95
+ if (typeof value !== "string" || value.length === 0 || value.length > 200)
96
+ return null;
97
+ if (value.includes("/") || value.includes("\\") || value.includes(".."))
98
+ return null;
99
+ return value;
100
+ }
101
+ function parseJson(value) {
102
+ try {
103
+ return JSON.parse(value);
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ function deliveryIdFromResult(value, depth = 0) {
110
+ if (depth > 3 || value === null || value === undefined)
111
+ return null;
112
+ const parsed = typeof value === "string" ? parseJson(value) : value;
113
+ if (parsed === null || typeof parsed !== "object")
114
+ return null;
115
+ const record = parsed;
116
+ const structured = record.structuredContent;
117
+ const carrier = structured !== null && typeof structured === "object" ? structured : record;
118
+ const parentDelivery = carrier.parent_delivery;
119
+ if (parentDelivery !== null && typeof parentDelivery === "object") {
120
+ const id = parentDelivery.delivery_id;
121
+ if (typeof id === "string" && deliveryIdSchema.safeParse(id).success)
122
+ return id;
123
+ }
124
+ if (depth === 0 && Array.isArray(record.content)) {
125
+ for (const entry of record.content) {
126
+ if (entry !== null && typeof entry === "object" && entry.type === "text") {
127
+ const text = entry.text;
128
+ if (typeof text === "string") {
129
+ const id = deliveryIdFromResult(text, depth + 1) ?? deliveryIdInText(text);
130
+ if (id)
131
+ return id;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return null;
137
+ }
138
+ function deliveryIdInText(text) {
139
+ for (const line of text.split("\n")) {
140
+ const match = /^delivery_id=([0-9a-f-]{36})$/i.exec(line.trim());
141
+ if (match && deliveryIdSchema.safeParse(match[1]).success)
142
+ return match[1];
143
+ }
144
+ return null;
145
+ }
146
+ function toolBaseName(name) {
147
+ const tail = name.split(/[:.]/).pop() ?? name;
148
+ return tail.replace(/^mcp__aiterm__/, "");
149
+ }
150
+ function claim(dir, channel) {
151
+ try {
152
+ fs.writeFileSync(path.join(dir, "claim.json"), JSON.stringify({ channel, at: new Date().toISOString() }), { flag: "wx", mode: 0o600 });
153
+ return true;
154
+ }
155
+ catch (error) {
156
+ if (error.code === "EEXIST")
157
+ return false;
158
+ throw error;
159
+ }
160
+ }
161
+ function bind(hookRoot, deliveryId, conv) {
162
+ const dir = path.join(hookRoot, "deliveries", deliveryId);
163
+ if (!fs.existsSync(dir))
164
+ return;
165
+ const bindFile = path.join(dir, "bind.json");
166
+ if (!fs.existsSync(bindFile)) {
167
+ writeJson0600(bindFile, { conversation_id: conv, created_at: new Date().toISOString() });
168
+ }
169
+ const indexDir = path.join(hookRoot, "conversations", conv);
170
+ fs.mkdirSync(indexDir, { recursive: true, mode: 0o700 });
171
+ const index = path.join(indexDir, deliveryId);
172
+ if (!fs.existsSync(index))
173
+ fs.writeFileSync(index, "", { mode: 0o600 });
174
+ }
175
+ function inject(hookRoot, conv) {
176
+ const indexDir = path.join(hookRoot, "conversations", conv);
177
+ if (!fs.existsSync(indexDir))
178
+ return [];
179
+ const pending = fs.readdirSync(indexDir).flatMap(deliveryId => {
180
+ const dir = path.join(hookRoot, "deliveries", deliveryId);
181
+ const answerFile = path.join(dir, "answer.json");
182
+ const bindFile = path.join(dir, "bind.json");
183
+ if (!fs.existsSync(answerFile) || !fs.existsSync(bindFile))
184
+ return [];
185
+ const answer = z.object({ delivery_id: z.string(), text: z.string() }).parse(JSON.parse(fs.readFileSync(answerFile, "utf8")));
186
+ const bound = z.object({ conversation_id: z.string(), created_at: z.string() }).parse(JSON.parse(fs.readFileSync(bindFile, "utf8")));
187
+ if (answer.delivery_id !== deliveryId || bound.conversation_id !== conv)
188
+ return [];
189
+ return [{ deliveryId, dir, created_at: bound.created_at, text: answer.text }];
190
+ }).sort((a, b) => a.created_at.localeCompare(b.created_at));
191
+ const texts = [];
192
+ for (const item of pending) {
193
+ const index = path.join(indexDir, item.deliveryId);
194
+ if (!claim(item.dir, "hook")) {
195
+ if (fs.existsSync(path.join(item.dir, "claim.json")))
196
+ fs.rmSync(index, { force: true });
197
+ continue;
198
+ }
199
+ texts.push(item.text);
200
+ fs.rmSync(index, { force: true });
201
+ }
202
+ return texts;
203
+ }
204
+ export async function handleCursorHook(raw, hookRoot = cursorHookRoot()) {
205
+ let parsed;
206
+ try {
207
+ parsed = JSON.parse(raw);
208
+ }
209
+ catch {
210
+ return {};
211
+ }
212
+ if (parsed === null || typeof parsed !== "object")
213
+ return {};
214
+ const event = parsed;
215
+ const name = event.hook_event_name;
216
+ if (name !== "afterMCPExecution" && name !== "postToolUse" && name !== "postToolUseFailure")
217
+ return {};
218
+ const conv = conversationId(event.conversation_id);
219
+ if (!conv)
220
+ return {};
221
+ const toolName = typeof event.tool_name === "string" ? event.tool_name : "";
222
+ if (dispatchTools.has(toolBaseName(toolName))) {
223
+ const payload = name === "afterMCPExecution" ? event.result_json : event.tool_output;
224
+ const deliveryId = deliveryIdFromResult(payload);
225
+ if (deliveryId)
226
+ bind(hookRoot, deliveryId, conv);
227
+ }
228
+ if (name === "postToolUse" || name === "postToolUseFailure") {
229
+ const texts = inject(hookRoot, conv);
230
+ if (texts.length > 0)
231
+ return { additional_context: texts.join("\n\n") };
232
+ }
233
+ return {};
234
+ }
235
+ export async function receiveCursorAnswer(hookRoot, deliveryId, timeoutMs = 86_400_000) {
236
+ const id = deliveryIdSchema.parse(deliveryId);
237
+ const dir = path.join(hookRoot, "deliveries", id);
238
+ if (!fs.existsSync(dir))
239
+ throw new CursorDeliveryError("CURSOR_PARENT_DELIVERY_UNKNOWN", "Cursor配送の記録がありません");
240
+ const answerFile = path.join(dir, "answer.json");
241
+ try {
242
+ await waitForFileState(dir, () => fs.existsSync(answerFile) ? true : undefined, timeoutMs);
243
+ }
244
+ catch (error) {
245
+ if (error instanceof Error && error.message === "WAIT_FOR_FILE_TIMEOUT")
246
+ return { outcome: "timeout" };
247
+ throw error;
248
+ }
249
+ const answer = z.object({ delivery_id: z.string(), text: z.string() }).parse(JSON.parse(fs.readFileSync(answerFile, "utf8")));
250
+ if (answer.delivery_id !== id)
251
+ throw new CursorDeliveryError("CURSOR_PARENT_DELIVERY_MISMATCH", "保存された回答の配送IDが一致しません");
252
+ if (!claim(dir, "receiver"))
253
+ return { outcome: "delivered_by_hook" };
254
+ return { outcome: "delivered", text: answer.text };
255
+ }
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { createRequire } from "node:module";
18
18
  import { ParentDeliveryManager } from "./parent-delivery.js";
19
19
  import { codexParentFromRequest } from "./codex-parent-receiver.js";
20
20
  import { claudeParentFromRequest } from "./claude-parent-receiver.js";
21
+ import { cursorParentFromRequest, isCursorMcpClient } from "./cursor-parent-receiver.js";
21
22
  // package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
22
23
  // createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
23
24
  // engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
@@ -25,15 +26,33 @@ import { claudeParentFromRequest } from "./claude-parent-receiver.js";
25
26
  const pkg = createRequire(import.meta.url)("../package.json");
26
27
  const server = new McpServer({ name: "aiterm", version: pkg.version });
27
28
  let parentDelivery = null;
29
+ function deliveryParentKind(clientName) {
30
+ if (clientName === "claude-code")
31
+ return "claude";
32
+ if (isCursorMcpClient(clientName))
33
+ return "cursor";
34
+ return undefined;
35
+ }
28
36
  async function deliveryForRequest(extra) {
29
37
  const clientName = server.server.getClientVersion()?.name;
30
- const parent = codexParentFromRequest(clientName, extra._meta) ?? claudeParentFromRequest(clientName, extra._meta);
38
+ const parent = codexParentFromRequest(clientName, extra._meta) ?? claudeParentFromRequest(clientName, extra._meta) ?? cursorParentFromRequest(clientName);
31
39
  if (!parent)
32
40
  return null;
33
- parentDelivery ??= new ParentDeliveryManager({ parent_kind: clientName === "claude-code" ? "claude" : undefined });
41
+ parentDelivery ??= new ParentDeliveryManager({ parent_kind: deliveryParentKind(clientName) });
34
42
  await parentDelivery.prepare(parent);
35
43
  return parentDelivery.request(parent);
36
44
  }
45
+ function deliveryIdLine(delivery) {
46
+ const id = delivery?.result()?.delivery_id;
47
+ return id ? `\ndelivery_id=${id}` : "";
48
+ }
49
+ function completionWait(delivery, session, eventCursor) {
50
+ if (eventCursor === null)
51
+ return { wait_process: null, wait_command: null };
52
+ if (delivery)
53
+ return { wait_process: delivery.wait_process(), wait_command: null };
54
+ return { wait_process: core.agentWaitProcess(session, eventCursor), wait_command: `aiterm-wait --session ${session} --cursor ${eventCursor}` };
55
+ }
37
56
  /**
38
57
  * dispatch 系の説明で共有する非ブロック規範。tool description は registerTool 時=initialize 前に
39
58
  * 固定されるため親ホストを名指しできない(ホスト別の具体形は receipt 側が core.agentWaitLaunchForm で出す)。
@@ -41,6 +60,7 @@ async function deliveryForRequest(extra) {
41
60
  */
42
61
  const NON_BLOCKING_RULE = "dispatch した子は投げっぱなしでよい=親はここで待たない。" +
43
62
  "Codex親とClaude Code親にはAitermが回答本文を自動配送する。parent_deliveryがある場合はwait起動も通常の回答回収も不要。親は作業を続けるかターンを終える。" +
63
+ "Cursor親にはparent_deliveryとwait_processが付く。作業を続ければ次のツール返りに回答が差し込まれ、ターンを終える前にwait_processを背景で起動するとidle中の完了でも起きられる。ポーリングとpty_read(agent_transcript:true)は不要。" +
44
64
  "その他の親では、完了通知をreceiptの `wait_process.executable` と `wait_process.args` をそのまま親のターンを塞がない別プロセスAPIへ渡して受け、" +
45
65
  "PowerShell 7のStart-Processだけは `windows_start_process_argument_list` を単一文字列として渡す。" +
46
66
  `exit を完了通知として扱う(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
@@ -190,14 +210,15 @@ server.registerTool("pty_send", {
190
210
  throw new Error("agent session への dispatch は rtk:true と併用できません");
191
211
  delivery = await deliveryForRequest(extra);
192
212
  const receipt = await core.dispatchAgentTurn(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send });
193
- const waitProcess = delivery ? null : core.agentWaitProcess(receipt.session_id, receipt.event_cursor);
213
+ const waited = completionWait(delivery, receipt.session_id, receipt.event_cursor);
194
214
  return {
195
215
  content: [
196
216
  {
197
217
  type: "text",
198
218
  text: `dispatchした(harness=${receipt.harness}, vendor=${receipt.vendor})。\n` +
199
219
  core.agentDispatchGuide(receipt.session_id, receipt.event_cursor) +
200
- core.agentSubmitResidueWarning(receipt.session_id, receipt.submit_residue),
220
+ core.agentSubmitResidueWarning(receipt.session_id, receipt.submit_residue) +
221
+ deliveryIdLine(delivery),
201
222
  },
202
223
  ],
203
224
  structuredContent: {
@@ -205,7 +226,7 @@ server.registerTool("pty_send", {
205
226
  mode: "agent_dispatch",
206
227
  session_id: receipt.session_id,
207
228
  event_cursor: receipt.event_cursor,
208
- wait_process: waitProcess,
229
+ wait_process: waited.wait_process,
209
230
  ...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
210
231
  launch_id: receipt.launch_id,
211
232
  vendor: receipt.vendor,
@@ -687,8 +708,7 @@ async function launchAgent(kind, args, extra) {
687
708
  session_id: sid,
688
709
  managed_completion: true,
689
710
  event_cursor: eventCursor,
690
- wait_process: eventCursor === null || delivery ? null : core.agentWaitProcess(sid, eventCursor),
691
- wait_command: eventCursor === null || delivery ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
711
+ ...completionWait(delivery, sid, eventCursor),
692
712
  ...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
693
713
  submit_residue: submitResidue,
694
714
  initial_prompt: initialDelivery,
@@ -703,7 +723,7 @@ async function launchAgent(kind, args, extra) {
703
723
  : {}),
704
724
  };
705
725
  return {
706
- content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
726
+ content: [{ type: "text", text: `session_id: ${sid}\n${hint}${deliveryIdLine(delivery)}` }],
707
727
  structuredContent: structured,
708
728
  ...(initialDelivery.status === "submitted_unconfirmed" ? { isError: true } : {}),
709
729
  };
@@ -716,8 +736,7 @@ async function launchAgent(kind, args, extra) {
716
736
  structuredContent: {
717
737
  schema: "aiterm.agent-launch-result.v1", provider: kind, harness: core.agentHarness(kind),
718
738
  session_id: e.session_id, managed_completion: true, event_cursor: e.event_cursor,
719
- wait_process: e.event_cursor === null || delivery ? null : core.agentWaitProcess(e.session_id, e.event_cursor),
720
- wait_command: e.event_cursor === null || delivery ? null : `aiterm-wait --session ${e.session_id} --cursor ${e.event_cursor}`,
739
+ ...completionWait(delivery, e.session_id, e.event_cursor),
721
740
  ...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
722
741
  submit_residue: null, initial_prompt: e.initial_prompt, startup: e.startup,
723
742
  },
@@ -863,8 +882,9 @@ async function main() {
863
882
  server.server.oninitialized = () => {
864
883
  const name = server.server.getClientVersion()?.name;
865
884
  core.setParentClient(name ?? null);
866
- if (name === "codex-mcp-client" || name === "claude-code")
867
- parentDelivery ??= new ParentDeliveryManager({ parent_kind: name === "claude-code" ? "claude" : undefined });
885
+ if (name === "codex-mcp-client" || name === "claude-code" || isCursorMcpClient(name)) {
886
+ parentDelivery ??= new ParentDeliveryManager({ parent_kind: deliveryParentKind(name) });
887
+ }
868
888
  };
869
889
  server.server.onclose = () => {
870
890
  void parentDelivery?.close().catch(() => process.stderr.write("aiterm: PARENT_DELIVERY_CLOSE_FAILED\n"));
@@ -8,6 +8,8 @@ import { ensureStateRoot, writeJson0600 } from "./agent-shared.js";
8
8
  import { readRuntimeProcesses } from "./process-runtime.js";
9
9
  import { CodexDeliveryError, submitCodexParentAnswer, verifyCodexParent } from "./codex-parent-receiver.js";
10
10
  import { claudeParentSchema, ClaudeDeliveryError, bindClaudeParentDelivery, submitClaudeParentAnswer, verifyClaudeParent } from "./claude-parent-receiver.js";
11
+ import { cursorParentSchema, CursorDeliveryError, prepareCursorDelivery, submitCursorParentAnswer, verifyCursorParent } from "./cursor-parent-receiver.js";
12
+ import { cursorReceiveProcess } from "./cursor-parent-receive.js";
11
13
  import { AitermError } from "./errors.js";
12
14
  import { codexHookDeliveryState } from "./codex-hook-state.js";
13
15
  const recordSchema = z.object({
@@ -15,7 +17,7 @@ const recordSchema = z.object({
15
17
  delivery_id: z.uuid(),
16
18
  created_at: z.string(),
17
19
  updated_at: z.string(),
18
- parent: z.union([z.object({ thread_id: z.uuid(), codex_home: z.string() }).strict(), claudeParentSchema]),
20
+ parent: z.union([z.object({ thread_id: z.uuid(), codex_home: z.string() }).strict(), claudeParentSchema, cursorParentSchema]),
19
21
  boundary: z.object({
20
22
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
21
23
  launch_id: z.string().regex(/^[0-9a-f]{32}$/),
@@ -31,14 +33,22 @@ const recordSchema = z.object({
31
33
  queued_submission_id: z.string().nullable(),
32
34
  }).strict();
33
35
  const isClaude = (parent) => "kind" in parent && parent.kind === "claude";
36
+ const isCursor = (parent) => "kind" in parent && parent.kind === "cursor";
37
+ const isCodex = (parent) => !isClaude(parent) && !isCursor(parent);
34
38
  async function verifyParent(parent) {
35
39
  if (isClaude(parent))
36
40
  verifyClaudeParent(parent);
41
+ else if (isCursor(parent))
42
+ verifyCursorParent(parent);
37
43
  else
38
44
  await verifyCodexParent(parent);
39
45
  }
40
46
  async function submitParentAnswer(parent, deliveryId, text) {
41
- return isClaude(parent) ? submitClaudeParentAnswer(parent, deliveryId, text) : submitCodexParentAnswer(parent, deliveryId, text);
47
+ if (isClaude(parent))
48
+ return submitClaudeParentAnswer(parent, deliveryId, text);
49
+ if (isCursor(parent))
50
+ return submitCursorParentAnswer(parent, deliveryId, text);
51
+ return submitCodexParentAnswer(parent, deliveryId, text);
42
52
  }
43
53
  function readRecord(file) {
44
54
  try {
@@ -54,7 +64,7 @@ function readRecord(file) {
54
64
  }
55
65
  }
56
66
  function receipt(record) {
57
- const hookState = !isClaude(record.parent) && record.state === "submitted"
67
+ const hookState = isCodex(record.parent) && record.state === "submitted"
58
68
  ? codexHookDeliveryState(record.parent.codex_home, record.parent.thread_id, record.delivery_id) : null;
59
69
  return { delivery_id: record.delivery_id, state: hookState ?? record.state, child_outcome: record.child_outcome,
60
70
  child_turn_id: record.child_turn_id, queued_submission_id: record.queued_submission_id,
@@ -87,9 +97,15 @@ export class ParentDeliveryManager {
87
97
  this.deps = { observe: observeAgentDone, answer: readAgentTranscriptResult, submit: submitParentAnswer,
88
98
  verify: verifyParent, processes: readRuntimeProcesses, ...options.dependencies };
89
99
  const stateRoot = ensureStateRoot();
90
- this.root = options.root ?? path.join(stateRoot, options.parent_kind === "claude" ? "claude-parent-deliveries" : "parent-deliveries");
91
- // 旧版のCodex readerへ未知のparentを渡さない。公開照会と子の予約だけは両方で共有する。
92
- this.recordRoots = options.root ? [options.root] : [path.join(stateRoot, "parent-deliveries"), path.join(stateRoot, "claude-parent-deliveries")];
100
+ const directory = options.parent_kind === "claude" ? "claude-parent-deliveries"
101
+ : options.parent_kind === "cursor" ? "cursor-parent-deliveries" : "parent-deliveries";
102
+ this.root = options.root ?? path.join(stateRoot, directory);
103
+ // 旧版のCodex/Claude readerへ未知のparentを渡さない。公開照会と子の予約だけは保存場所を横断する。
104
+ this.recordRoots = options.root ? [options.root] : [
105
+ path.join(stateRoot, "parent-deliveries"),
106
+ path.join(stateRoot, "claude-parent-deliveries"),
107
+ path.join(stateRoot, "cursor-parent-deliveries"),
108
+ ];
93
109
  this.active = path.join(this.root, "active");
94
110
  this.results = path.join(this.root, "results");
95
111
  this.claims = path.join(options.root ?? path.join(stateRoot, "parent-deliveries"), "claims");
@@ -148,6 +164,17 @@ export class ParentDeliveryManager {
148
164
  }
149
165
  if (isClaude(parent))
150
166
  bindClaudeParentDelivery(parent, record.delivery_id);
167
+ if (isCursor(parent)) {
168
+ try {
169
+ prepareCursorDelivery(parent, record.delivery_id);
170
+ }
171
+ catch (error) {
172
+ this.releaseClaim(job);
173
+ fs.unlinkSync(job.file);
174
+ job = null;
175
+ throw error;
176
+ }
177
+ }
151
178
  this.jobs.set(record.delivery_id, job);
152
179
  this.watch(job);
153
180
  });
@@ -161,6 +188,7 @@ export class ParentDeliveryManager {
161
188
  }
162
189
  },
163
190
  result: () => job ? receipt(job.record) : null,
191
+ wait_process: () => job && isCursor(parent) ? cursorReceiveProcess(job.record.delivery_id) : null,
164
192
  failed: (error) => {
165
193
  if (!job || job.record.state !== "waiting")
166
194
  return;
@@ -264,7 +292,8 @@ export class ParentDeliveryManager {
264
292
  job.record.state = "submitted";
265
293
  }
266
294
  catch (error) {
267
- job.record.state = (error instanceof CodexDeliveryError || error instanceof ClaudeDeliveryError) && !error.outcome_unknown ? "failed" : "unknown";
295
+ const known = error instanceof CodexDeliveryError || error instanceof ClaudeDeliveryError || error instanceof CursorDeliveryError;
296
+ job.record.state = known && !error.outcome_unknown ? "failed" : "unknown";
268
297
  job.record.error = error instanceof Error ? error.message : String(error);
269
298
  }
270
299
  this.finish(job);
package/dist/setup-cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { runSetup } from "./setup.js";
3
- import { removeClaudeParentHooks } from "./setup-integrations.js";
3
+ import { removeClaudeParentHooks, removeCursorParentHooks } from "./setup-integrations.js";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
@@ -16,8 +16,18 @@ if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
16
16
  process.exitCode = 2;
17
17
  }
18
18
  }
19
+ else if (args.length === 1 && args[0] === "--remove-cursor-parent-hooks") {
20
+ try {
21
+ const status = removeCursorParentHooks(join(process.env.CURSOR_HOME ?? join(process.env.HOME ?? homedir(), ".cursor"), "hooks.json"));
22
+ process.stdout.write(`${JSON.stringify({ schema: "aiterm.cursor-parent-hooks-remove-result.v1", status })}\n`);
23
+ }
24
+ catch (error) {
25
+ process.stderr.write(`aiterm-setup: ${error instanceof Error ? error.message : String(error)}\n`);
26
+ process.exitCode = 2;
27
+ }
28
+ }
19
29
  else if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
20
- process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS・Windows対応で、初回はCodexの再起動が必要です。disableは専用hookを解除し、statusは公式hookの登録・承認と再起動の必要性を確認します。旧中継はhook導入後に解除します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n");
30
+ process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS・Windows対応で、初回はCodexの再起動が必要です。disableは専用hookを解除し、statusは公式hookの登録・承認と再起動の必要性を確認します。旧中継はhook導入後に解除します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n旧版へ戻す前のCursor専用hook解除: --remove-cursor-parent-hooks\n");
21
31
  }
22
32
  else {
23
33
  try {
@@ -156,6 +156,116 @@ export function removeClaudeParentHooks(file) {
156
156
  throw new SetupError("config_readback_failed", "Claude hook解除の読戻しが一致しません");
157
157
  return "removed";
158
158
  }
159
+ function shellQuote(value) {
160
+ if (process.platform === "win32")
161
+ return `'${value.replace(/'/g, "''")}'`;
162
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
163
+ }
164
+ export function cursorParentHookCommand(registration) {
165
+ const script = join(dirname(registration.args[0]), "cursor-parent-hook.js");
166
+ const command = `${shellQuote(registration.command)} ${shellQuote(script)}`;
167
+ return process.platform === "win32" ? `& ${command}` : command;
168
+ }
169
+ function ownsCursorParentHook(hook) {
170
+ return record(hook) && typeof hook.command === "string" && hook.command.includes("cursor-parent-hook.js");
171
+ }
172
+ export function mergeCursorParentHooks(file, registration) {
173
+ const target = existsSync(file) ? realpathSync(file) : file;
174
+ let current = { version: 1, hooks: {} };
175
+ if (existsSync(target)) {
176
+ try {
177
+ current = JSON.parse(readFileSync(target, "utf8"));
178
+ }
179
+ catch {
180
+ throw new SetupError("config_invalid", "Cursorのhook設定JSONを読めません");
181
+ }
182
+ }
183
+ else {
184
+ try {
185
+ if (lstatSync(file).isSymbolicLink())
186
+ throw new SetupError("config_invalid", "Cursorのhooks設定symlinkの参照先がありません");
187
+ }
188
+ catch (error) {
189
+ if (error.code !== "ENOENT")
190
+ throw error;
191
+ }
192
+ }
193
+ if (!record(current) || (current.hooks !== undefined && !record(current.hooks))) {
194
+ throw new SetupError("config_invalid", "Cursorのhooks設定はobjectである必要があります");
195
+ }
196
+ const hooks = { ...current.hooks };
197
+ const entry = { command: cursorParentHookCommand(registration), timeout: 15 };
198
+ for (const event of ["afterMCPExecution", "postToolUse"]) {
199
+ const previous = hooks[event] ?? [];
200
+ if (!Array.isArray(previous))
201
+ throw new SetupError("config_invalid", `Cursorの${event} hook形式を読めません`);
202
+ const retained = previous.filter(hook => !ownsCursorParentHook(hook));
203
+ const owned = previous.filter(hook => ownsCursorParentHook(hook));
204
+ if (owned.length === 1 && isDeepStrictEqual(owned[0], entry) && retained.length + 1 === previous.length)
205
+ continue;
206
+ const index = previous.findIndex(hook => ownsCursorParentHook(hook));
207
+ hooks[event] = index < 0 ? [...retained, entry] : previous.map((hook, i) => i === index ? entry : hook).filter((hook, i) => i === index || !ownsCursorParentHook(hook));
208
+ }
209
+ const next = { ...current, version: current.version ?? 1, hooks };
210
+ if (isDeepStrictEqual(current, next))
211
+ return "unchanged";
212
+ mkdirSync(dirname(target), { recursive: true });
213
+ const temporary = `${target}.aiterm-${randomUUID()}`;
214
+ try {
215
+ writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600, flag: "wx" });
216
+ if (existsSync(target))
217
+ copyFileSync(target, `${target}.aiterm-backup`);
218
+ renameSync(temporary, target);
219
+ }
220
+ finally {
221
+ if (existsSync(temporary))
222
+ unlinkSync(temporary);
223
+ }
224
+ if (!isDeepStrictEqual(JSON.parse(readFileSync(target, "utf8")), next)) {
225
+ throw new SetupError("config_readback_failed", "Cursorのhook登録の読戻しが一致しません");
226
+ }
227
+ return "configured";
228
+ }
229
+ export function removeCursorParentHooks(file) {
230
+ if (!existsSync(file))
231
+ return "unchanged";
232
+ const target = realpathSync(file);
233
+ const current = JSON.parse(readFileSync(target, "utf8"));
234
+ if (!record(current) || (current.hooks !== undefined && !record(current.hooks))) {
235
+ throw new SetupError("config_invalid", "Cursorのhook設定を読めません");
236
+ }
237
+ if (current.hooks === undefined)
238
+ return "unchanged";
239
+ const hooks = { ...current.hooks };
240
+ for (const event of ["afterMCPExecution", "postToolUse"]) {
241
+ if (hooks[event] === undefined)
242
+ continue;
243
+ if (!Array.isArray(hooks[event]))
244
+ throw new SetupError("config_invalid", "Cursorのhook設定を読めません");
245
+ const retained = hooks[event].filter(hook => !ownsCursorParentHook(hook));
246
+ if (retained.length)
247
+ hooks[event] = retained;
248
+ else
249
+ delete hooks[event];
250
+ }
251
+ const next = { ...current, hooks };
252
+ if (isDeepStrictEqual(current, next))
253
+ return "unchanged";
254
+ const temporary = `${target}.aiterm-${randomUUID()}`;
255
+ try {
256
+ writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600, flag: "wx" });
257
+ copyFileSync(target, `${target}.aiterm-backup`);
258
+ renameSync(temporary, target);
259
+ }
260
+ finally {
261
+ if (existsSync(temporary))
262
+ unlinkSync(temporary);
263
+ }
264
+ if (!isDeepStrictEqual(JSON.parse(readFileSync(target, "utf8")), next)) {
265
+ throw new SetupError("config_readback_failed", "Cursor hook解除の読戻しが一致しません");
266
+ }
267
+ return "removed";
268
+ }
159
269
  export function configureIntegrations(home, registration, run = runSetupCommand, resolveClient = resolveAgentBin) {
160
270
  const results = {};
161
271
  for (const client of ["claude", "codex", "grok", "cursor"]) {
@@ -169,6 +279,8 @@ export function configureIntegrations(home, registration, run = runSetupCommand,
169
279
  const file = client === "cursor" ? join(home, ".cursor", "mcp.json")
170
280
  : process.env.CLAUDE_CONFIG_DIR ? join(process.env.CLAUDE_CONFIG_DIR, ".claude.json") : join(home, ".claude.json");
171
281
  mergeJsonMcp(file, client === "claude" ? { type: "stdio", ...registration } : registration);
282
+ if (client === "cursor")
283
+ mergeCursorParentHooks(join(home, ".cursor", "hooks.json"), registration);
172
284
  if (client === "claude") {
173
285
  const version = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(run(executable, ["--version"]));
174
286
  if (!version || Number(version[1]) < 2 || (Number(version[1]) === 2 && (Number(version[2]) < 1 || (Number(version[2]) === 1 && Number(version[3]) < 259)))) {
package/docs/DESIGN.md CHANGED
@@ -151,6 +151,25 @@ Claude Desktopのチャット、Web、`agent_id`付きの会話(`--agent`で
151
151
  Claudeの起動metadataには指定cwdの実体パスを保存する。Claude Codeが実体パスから作るproject slugと
152
152
  APIエラー監視の参照先を一致させ、監視中のリンク変更で保存場所を取り違えない。
153
153
 
154
+ ### Cursor親への自動配送
155
+
156
+ `aiterm-setup`は`~/.cursor/hooks.json`へAiterm専用の`afterMCPExecution`と`postToolUse`を追加する。
157
+ 他製品のhookと順序は保持し、`command`に`cursor-parent-hook.js`を含むentryだけを更新する。
158
+ 親の識別はMCP `initialize`の`clientInfo.name`が`cursor-vscode`(またはその後ろに空白を挟む派生名)であることだけで、
159
+ 会話IDはMCPの`_meta`に来ない。hook未登録は子への送信前に`CURSOR_PARENT_HOOK_UNAVAILABLE`で止め、waiterへ切り替えない。
160
+
161
+ `afterMCPExecution`またはdispatch toolの`postToolUse`が、tool返りの`parent_delivery.delivery_id`とhook入力の
162
+ `conversation_id`を結ぶ。`delivery_id`は`structuredContent`か、`content`のtextをJSONとして読んだ中から取る。
163
+ 完了観測と本文保存は`parent-delivery.ts`が所有し、記録は`cursor-parent-deliveries`へ分ける。子の予約`claims`は共有する。
164
+
165
+ 親が次のツールを呼ぶと`postToolUse`が未受領の本文を`additional_context`で会話へ差し込む。
166
+ 親がターンを終えている場合は、receiptの`wait_process`で起動した`cursor-parent-receive`が本文をstdoutへ出して終了する。
167
+ `wait_command`はnull。受け取りは`claim.json`の排他作成で一つに決め、hookと受け口の両方へ本文を出さない。
168
+ `submitted`はどちらかがclaimした状態であり、modelの読了ではない。24時間以内にclaimが無ければ`failed`とし、
169
+ 本文は残して自動再送しない。Cursor Cloud Agent/Background Agentはこの受信契約に含めない。
170
+
171
+ hookを外す時は`aiterm-setup --remove-cursor-parent-hooks`で専用entryだけを解除する。
172
+
154
173
  `trust_project:true`は対象projectの既知のworkspace、hooks、MCP初期同意を起動準備として進める意図である。
155
174
  promptなしでも入力受付とharness生存を確認して`startup.ready`を返す。指定なしのpromptなし起動は
156
175
  従来どおり`startup.not_checked`で返す。初手receiptは未要求・未送信・送信済み未確認・開始確認を分ける。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.37.10",
3
+ "version": "0.38.1",
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": [