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 +15 -1
- package/README.ja.md +5 -3
- package/README.md +5 -3
- package/dist/agent-shared.js +40 -0
- package/dist/claude-parent-receiver.js +1 -36
- package/dist/core.js +14 -1
- package/dist/cursor-parent-hook.js +35 -0
- package/dist/cursor-parent-receive.js +56 -0
- package/dist/cursor-parent-receiver.js +255 -0
- package/dist/index.js +32 -12
- package/dist/parent-delivery.js +36 -7
- package/dist/setup-cli.js +12 -2
- package/dist/setup-integrations.js +112 -0
- package/docs/DESIGN.md +19 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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親には子の回答本文が自動で届く。**
|
|
590
|
+
**Codex/Claude Code/Cursor親には子の回答本文が自動で届く。** Codex/Claude Codeでは、子を起動・dispatchした後は別作業へ進むか親のturnを終える。Aitermが完了を観測し、加工前の本文を保存して親へ渡す。waiter、`pty_read`による回答回収、子への返送指示は不要。子は全対応harnessから選べる。
|
|
591
591
|
|
|
592
|
-
|
|
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:
|
package/dist/agent-shared.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"));
|
package/dist/parent-delivery.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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": [
|