aiterm-mcp 0.42.2 → 0.43.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,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.43.1] - 2026-09-29
11
+
12
+ ### 修正
13
+
14
+ - Codexを親にすると、子のClaude Codeの完了が親へ届かないことがあった(rabbitで再現)。子へ渡すStop hookが`node`をPATHから探す形で、Codexが起動したAitermのPATHにnodeの場所が入っていない端末では、hookが動かず完了の記録が書かれていなかった。hookのnodeは絶対pathで呼び、Homebrewの版付きの実体は更新でも残るopt側へ置き換える。
15
+ - Windowsで、PATHを固定したMCP登録(Cursor等)から起動されたAitermがpsmuxを見つけられなかった(foxで再現)。利用者用に加えて、PC全体用のWinGet Links(`C:\Program Files\WinGet\Links`)も探す。
16
+
17
+ ## [0.43.0] - 2026-09-29
18
+
19
+ ### 変更
20
+
21
+ - 親への配送(Codexの公式キュー+同期hook、Claude CodeのasyncRewake hook、Cursorのhook+背景受信)の本体を、共通パッケージ[aiterm-steer-delivery](https://github.com/kitepon/aiterm-steer-delivery)へ移した。保存場所、hook command、entrypointのファイル名、案内文、配送の挙動は変わらない。旧中継(relay)はAitermに残す。他の製品(Peertable、gpt-connector)も同じパッケージで配送する。判断はrepositoryの`docs/adr/0073-parent-delivery-module.md`。
22
+ - Codex Steerの導入で、導入前から動いているCodexを再起動待ちとして見分ける時、引用符付きのコマンド行も照合する(Windowsで見落としていた)。
23
+
10
24
  ## [0.42.2] - 2026-09-28
11
25
 
12
26
  ### 修正
@@ -1854,7 +1868,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1854
1868
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1855
1869
  provenance.
1856
1870
 
1857
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.42.2...HEAD
1871
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.1...HEAD
1872
+ [0.43.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.0...v0.43.1
1873
+ [0.43.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.42.2...v0.43.0
1858
1874
  [0.42.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.42.1...v0.42.2
1859
1875
  [0.42.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.42.0...v0.42.1
1860
1876
  [0.42.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.3...v0.42.0
package/README.ja.md CHANGED
@@ -203,7 +203,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
203
203
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
204
204
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
205
205
 
206
- **状態:** 開発継続中 · 現行公開版 **v0.42.2** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
206
+ **状態:** 開発継続中 · 現行公開版 **v0.43.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
207
207
 
208
208
  ### 更新と巻き戻し
209
209
 
package/README.md CHANGED
@@ -217,7 +217,7 @@ collection is off by default and performs no network I/O. It ships via
217
217
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
218
218
  Release re-registers the Official MCP Registry entry.
219
219
 
220
- **Status:** actively maintained · current public release **v0.42.2** · 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
+ **Status:** actively maintained · current public release **v0.43.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).
221
221
 
222
222
  ### Update and rollback
223
223
 
@@ -1,28 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Claude Codeが直接起動する公式hook。MCP stdioとは別processで本文をstderrへ返す。
3
- import { prepareClaudeHookRequest, runClaudeResultHook, closeClaudeParentSession } from "./claude-parent-receiver.js";
4
- async function main() {
5
- let input = "";
6
- process.stdin.setEncoding("utf8");
7
- for await (const chunk of process.stdin)
8
- input += chunk;
9
- const event = JSON.parse(input);
10
- switch (event.hook_event_name) {
11
- case "PreToolUse":
12
- prepareClaudeHookRequest(event);
13
- break;
14
- case "PostToolUse":
15
- process.exitCode = await runClaudeResultHook(event, text => new Promise((resolve, reject) => {
16
- process.stderr.write(text, error => error ? reject(error) : resolve());
17
- }));
18
- break;
19
- case "SessionEnd":
20
- closeClaudeParentSession(event);
21
- break;
22
- default: throw new Error("CLAUDE_PARENT_HOOK_EVENT_INVALID: 未対応のhookです");
23
- }
24
- }
25
- main().catch(error => {
26
- process.stderr.write(`${error instanceof Error ? error.message : "CLAUDE_PARENT_HOOK_FAILED"}\n`);
27
- process.exitCode = 2;
28
- });
2
+ // Claude Codeが直接起動する公式hook。MCP stdioとは別processで本文をstderrへ返す。本体はaiterm-steer-delivery。
3
+ import { runClaudeHookMain } from "aiterm-steer-delivery";
4
+ import { AITERM_PROFILE } from "./steer-profile.js";
5
+ await runClaudeHookMain(AITERM_PROFILE);
@@ -1,179 +1,27 @@
1
- // Claude Codeの公式hookを受信口にする。待機processはharnessが所有し、親のturnを止めない。
2
- import * as fs from "node:fs";
1
+ // Claude Codeの公式hookを受信口にする。待機processはharnessが所有し、親のturnを止めない。本体はaiterm-steer-delivery。
3
2
  import * as path from "node:path";
4
3
  import { z } from "zod";
5
- import { ensureStateRoot, waitForFileState, writeJson0600 } from "./agent-shared.js";
6
- import { readRuntimeProcesses } from "./process-runtime.js";
7
- import { AitermError } from "./errors.js";
8
- const requestId = z.string().regex(/^[A-Za-z0-9_-]{1,160}$/);
9
- const invocationSchema = z.object({
10
- request_id: requestId, session_id: z.uuid(), agent_id: z.string().nullable(),
11
- parent_pid: z.number().int().positive(), parent_started_identity: z.string(),
12
- }).strict();
4
+ import * as steer from "aiterm-steer-delivery";
5
+ import { ensureStateRoot } from "./agent-shared.js";
6
+ import { AITERM_PROFILE } from "./steer-profile.js";
7
+ // 配送記録の検証はAitermのzodで行う(パッケージのschemaと同じ形)。
13
8
  export const claudeParentSchema = z.object({
14
- kind: z.literal("claude"), request_id: requestId, session_id: z.uuid(), hook_root: z.string(),
9
+ kind: z.literal("claude"), request_id: z.string().regex(/^[A-Za-z0-9_-]{1,160}$/), session_id: z.uuid(), hook_root: z.string(),
15
10
  }).strict();
16
- export class ClaudeDeliveryError 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
- }
11
+ export const ClaudeDeliveryError = steer.ClaudeDeliveryError;
25
12
  function defaultRoot() { return path.join(ensureStateRoot(), "claude-parent-hooks"); }
26
- function processIdentity(pid) { return readRuntimeProcesses().find(entry => entry.pid === pid)?.started_identity; }
27
- function directory(parent) { return path.join(parent.hook_root, parent.request_id); }
28
- function readInvocation(parent) {
29
- let value;
30
- try {
31
- value = invocationSchema.parse(JSON.parse(fs.readFileSync(path.join(directory(parent), "request.json"), "utf8")));
32
- }
33
- catch {
34
- throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_UNAVAILABLE", "親のhook記録がありません。aiterm-setupを実行し、Claude Codeのhookを有効にしてください");
35
- }
36
- if (value.request_id !== parent.request_id || value.session_id !== parent.session_id) {
37
- throw new ClaudeDeliveryError("CLAUDE_PARENT_SESSION_MISMATCH", "要求とhookの会話が一致しません");
38
- }
39
- return value;
40
- }
41
- function assertOpen(parent) {
42
- if (fs.existsSync(path.join(directory(parent), "closed.json"))) {
43
- throw new ClaudeDeliveryError("CLAUDE_PARENT_SESSION_CLOSED", "依頼元の会話は終了しました。回答を別の会話へ送っていません");
44
- }
45
- }
46
- export function prepareClaudeHookRequest(input, root = defaultRoot()) {
47
- const event = z.object({ session_id: z.uuid(), tool_use_id: requestId, agent_id: z.string().optional() }).parse(input);
48
- const identity = processIdentity(process.ppid);
49
- if (!identity)
50
- throw new ClaudeDeliveryError("CLAUDE_PARENT_PROCESS_UNAVAILABLE", "hookを起動した親processを確認できません");
51
- const dir = path.join(root, event.tool_use_id);
52
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
53
- writeJson0600(path.join(dir, "request.json"), {
54
- request_id: event.tool_use_id, session_id: event.session_id, agent_id: event.agent_id ?? null,
55
- parent_pid: process.ppid, parent_started_identity: identity,
56
- });
57
- }
13
+ export function prepareClaudeHookRequest(input, root = defaultRoot()) { steer.prepareClaudeHookRequest(input, root); }
58
14
  /** 起動時envのsession IDは/clearで古くなるため、実際のPreToolUseとの相関だけを使う。 */
59
15
  export function claudeParentFromRequest(clientName, metadata, root = defaultRoot()) {
60
- if (clientName !== "claude-code")
61
- return null;
62
- const parsed = requestId.safeParse(metadata?.["claudecode/toolUseId"]);
63
- if (!parsed.success)
64
- throw new ClaudeDeliveryError("CLAUDE_PARENT_ID_UNAVAILABLE", "MCP要求にtoolUseIdがありません。対応するClaude Codeへ更新してください");
65
- let invocation;
66
- try {
67
- invocation = invocationSchema.parse(JSON.parse(fs.readFileSync(path.join(root, parsed.data, "request.json"), "utf8")));
68
- }
69
- catch {
70
- throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_UNAVAILABLE", "親のPreToolUse hookを確認できません。aiterm-setupを実行し、hookを有効にしてください");
71
- }
72
- const parent = { kind: "claude", request_id: parsed.data, session_id: invocation.session_id, hook_root: root };
73
- verifyClaudeParent(parent);
74
- return parent;
75
- }
76
- export function verifyClaudeParent(parent) {
77
- const invocation = readInvocation(parent);
78
- if (invocation.agent_id)
79
- throw new ClaudeDeliveryError("CLAUDE_PARENT_SUBAGENT_UNSUPPORTED", "agent_id付きのClaude会話(--agent起動またはnative subagent)への自動配送には対応していません");
80
- assertOpen(parent);
81
- }
82
- export function bindClaudeParentDelivery(parent, deliveryId) {
83
- verifyClaudeParent(parent);
84
- writeJson0600(path.join(directory(parent), "delivery.json"), { delivery_id: z.uuid().parse(deliveryId) });
16
+ return steer.claudeParentFromRequest(AITERM_PROFILE, clientName, metadata, root);
85
17
  }
18
+ export function verifyClaudeParent(parent) { steer.verifyClaudeParent(AITERM_PROFILE, parent); }
19
+ export function bindClaudeParentDelivery(parent, deliveryId) { steer.bindClaudeParentDelivery(AITERM_PROFILE, parent, deliveryId); }
86
20
  /** SessionEndはその時点の依頼だけを終了する。同じ会話をresumeした新規依頼は別requestになる。 */
87
- export function closeClaudeParentSession(input, root = defaultRoot()) {
88
- const { session_id } = z.object({ session_id: z.uuid() }).parse(input);
89
- if (!fs.existsSync(root))
90
- return;
91
- for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
92
- if (!entry.isDirectory())
93
- continue;
94
- const file = path.join(root, entry.name, "request.json");
95
- if (!fs.existsSync(file))
96
- continue;
97
- const invocation = invocationSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
98
- if (invocation.session_id === session_id)
99
- writeJson0600(path.join(root, entry.name, "closed.json"), { session_id });
100
- }
101
- }
102
- function assertParentAlive(invocation) {
103
- if (processIdentity(invocation.parent_pid) !== invocation.parent_started_identity) {
104
- throw new ClaudeDeliveryError("CLAUDE_PARENT_PROCESS_CLOSED", "依頼元のClaude processは終了しました。回答は保存したままです");
105
- }
106
- }
107
- export async function submitClaudeParentAnswer(parent, deliveryId, text) {
108
- const invocation = readInvocation(parent);
109
- const dir = directory(parent);
110
- // 会話終了でも確定本文を失わない。終了判定より先に保存する。
111
- writeJson0600(path.join(dir, "answer.json"), { delivery_id: deliveryId, text });
112
- await waitForFileState(dir, () => {
113
- const emitted = path.join(dir, "emitted.json");
114
- if (fs.existsSync(emitted)) {
115
- const value = JSON.parse(fs.readFileSync(emitted, "utf8"));
116
- if (value.delivery_id !== deliveryId)
117
- throw new ClaudeDeliveryError("CLAUDE_PARENT_DELIVERY_MISMATCH", "hookの配送IDが一致しません", true);
118
- return true;
119
- }
120
- assertOpen(parent);
121
- const failed = path.join(dir, "failed.json");
122
- if (fs.existsSync(failed)) {
123
- const value = JSON.parse(fs.readFileSync(failed, "utf8"));
124
- throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_FAILED", "親へのhook出力が失敗しました。回答は保存したままです", value.outcome_unknown === true);
125
- }
126
- const hookFile = path.join(dir, "hook.json");
127
- if (fs.existsSync(hookFile)) {
128
- const hook = z.object({ pid: z.number().int().positive(), started_identity: z.string() }).parse(JSON.parse(fs.readFileSync(hookFile, "utf8")));
129
- if (processIdentity(hook.pid) !== hook.started_identity) {
130
- throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_CLOSED", "親の受信hookが終了しました。自動再送はしていません", fs.existsSync(path.join(dir, "sending.json")));
131
- }
132
- }
133
- assertParentAlive(invocation);
134
- return undefined;
135
- });
136
- return { queued_submission_id: null };
21
+ export function closeClaudeParentSession(input, root = defaultRoot()) { steer.closeClaudeParentSession(input, root); }
22
+ export function submitClaudeParentAnswer(parent, deliveryId, text) {
23
+ return steer.submitClaudeParentAnswer(AITERM_PROFILE, parent, deliveryId, text);
137
24
  }
138
- export async function runClaudeResultHook(input, emit, root = defaultRoot()) {
139
- const event = z.object({ session_id: z.uuid(), tool_use_id: requestId }).parse(input);
140
- const parent = { kind: "claude", request_id: event.tool_use_id, session_id: event.session_id, hook_root: root };
141
- const invocation = readInvocation(parent);
142
- const dir = directory(parent);
143
- const binding = path.join(dir, "delivery.json");
144
- if (!fs.existsSync(binding)) {
145
- fs.rmSync(dir, { recursive: true });
146
- return 0;
147
- }
148
- const deliveryId = z.uuid().parse(JSON.parse(fs.readFileSync(binding, "utf8")).delivery_id);
149
- const identity = processIdentity(process.pid);
150
- if (!identity)
151
- throw new ClaudeDeliveryError("CLAUDE_PARENT_HOOK_UNAVAILABLE", "受信hookのprocessを確認できません");
152
- writeJson0600(path.join(dir, "hook.json"), { pid: process.pid, started_identity: identity });
153
- try {
154
- const answer = await waitForFileState(dir, () => {
155
- if (fs.existsSync(path.join(dir, "closed.json")))
156
- return null;
157
- assertParentAlive(invocation);
158
- const file = path.join(dir, "answer.json");
159
- if (!fs.existsSync(file))
160
- return undefined;
161
- const value = z.object({ delivery_id: z.uuid(), text: z.string() }).strict().parse(JSON.parse(fs.readFileSync(file, "utf8")));
162
- if (value.delivery_id !== deliveryId)
163
- throw new ClaudeDeliveryError("CLAUDE_PARENT_DELIVERY_MISMATCH", "保存された回答の配送IDが一致しません");
164
- return value;
165
- });
166
- if (!answer)
167
- return 0;
168
- assertOpen(parent);
169
- writeJson0600(path.join(dir, "sending.json"), { delivery_id: deliveryId });
170
- await emit(answer.text);
171
- writeJson0600(path.join(dir, "emitted.json"), { delivery_id: deliveryId });
172
- // Claudeが定めるasyncRewakeの再開信号。子の成功/失敗は本文のoutcomeで区別する。
173
- return 2;
174
- }
175
- catch (error) {
176
- writeJson0600(path.join(dir, "failed.json"), { outcome_unknown: fs.existsSync(path.join(dir, "sending.json")) });
177
- throw error;
178
- }
25
+ export function runClaudeResultHook(input, emit, root = defaultRoot()) {
26
+ return steer.runClaudeResultHook(AITERM_PROFILE, input, emit, root);
179
27
  }
@@ -1,10 +1 @@
1
- import { AitermError } from "./errors.js";
2
- export class CodexDeliveryError extends AitermError {
3
- delivery_code;
4
- outcome_unknown;
5
- constructor(delivery_code, message, outcome_unknown = false) {
6
- super(`${delivery_code}: ${message}`, 2);
7
- this.delivery_code = delivery_code;
8
- this.outcome_unknown = outcome_unknown;
9
- }
10
- }
1
+ export { CodexDeliveryError } from "aiterm-steer-delivery";
@@ -1,27 +1,6 @@
1
- // Codex DesktopのSteerで使う同梱Codex CLIの場所。Desktopは更新のたびに同梱物の場所を変えることがある
2
- // (Windowsは版ごとのcache directory、macOSはbundle内の配置)。setup時に保存した場所が消えていたら、
3
- // 使う時点で公式Desktopから探し直して設定を更新する。探せなければ理由付きで失敗し、別のCodexへは切り替えない。
4
- import * as fs from "node:fs";
5
- import * as path from "node:path";
6
- import { CodexDeliveryError } from "./codex-delivery-error.js";
7
- import { codexHookDirectory, writeHookJson } from "./codex-hook-state.js";
8
- async function platformFinder() {
9
- if (process.platform === "win32")
10
- return (await import("./windows-codex-setup.js")).findWindowsCodexBinary;
11
- return (await import("./setup-codex-relay.js")).findDesktopBinary;
12
- }
1
+ // Codex DesktopのSteerで使う同梱Codex CLIの場所。消えていたら使う時点で探し直す。本体はaiterm-steer-delivery。
2
+ import * as steer from "aiterm-steer-delivery";
3
+ import { AITERM_PROFILE } from "./steer-profile.js";
13
4
  export async function currentCodexDesktopBinary(config, options = {}) {
14
- const exists = options.exists ?? fs.existsSync;
15
- if (exists(config.binary))
16
- return config.binary;
17
- let binary;
18
- try {
19
- binary = (options.find ?? await platformFinder())();
20
- }
21
- catch (error) {
22
- throw new CodexDeliveryError("CODEX_DESKTOP_BINARY_MOVED", `Codex Desktopの更新で ${config.binary} が無くなり、新しい場所も特定できません(${error instanceof Error ? error.message : String(error)})。` +
23
- "Codex Desktopを起動してから aiterm-setup --codex-steer enable を実行してください");
24
- }
25
- writeHookJson(path.join(options.directory ?? codexHookDirectory(), "config.json"), { ...config, binary });
26
- return binary;
5
+ return steer.currentCodexDesktopBinary(AITERM_PROFILE, config, options);
27
6
  }
@@ -1,126 +1,19 @@
1
- // Codexの設定領域にはhook登録だけを置き、配送の所有情報はAitermが保持する。
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { createHash, randomUUID } from "node:crypto";
5
- import { z } from "zod";
6
- import { relayConfigDirectory } from "./codex-relay-config.js";
7
- import { CodexDeliveryError } from "./codex-delivery-error.js";
8
- import { readRuntimeProcesses } from "./process-runtime.js";
9
- const processSchema = z.object({ pid: z.number().int(), started_identity: z.string() });
10
- export const codexHookConfigSchema = z.object({
11
- schema: z.literal("aiterm.codex-parent-hooks.v1"), enabled: z.boolean(),
12
- codex_home: z.string(), binary: z.string(), command: z.string(), node: z.string(), hook: z.string(),
13
- stale_processes: z.array(processSchema),
14
- }).strict();
15
- export function codexHookDirectory() { return path.join(path.dirname(relayConfigDirectory()), "codex-parent-hooks"); }
16
- export function writeHookJson(file, value) {
17
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
18
- const temporary = `${file}.${randomUUID()}.tmp`;
19
- try {
20
- fs.writeFileSync(temporary, JSON.stringify(value) + "\n", { flag: "wx", mode: 0o600 });
21
- fs.renameSync(temporary, file);
22
- }
23
- finally {
24
- if (fs.existsSync(temporary))
25
- fs.unlinkSync(temporary);
26
- }
27
- }
28
- export function readCodexHookConfig(directory = codexHookDirectory()) {
29
- try {
30
- return codexHookConfigSchema.parse(JSON.parse(fs.readFileSync(path.join(directory, "config.json"), "utf8")));
31
- }
32
- catch (error) {
33
- if (error.code === "ENOENT")
34
- return null;
35
- throw new CodexDeliveryError("CODEX_HOOK_CONFIG_INVALID", "AitermのCodex hook設定を読めません");
36
- }
37
- }
38
- export function codexInputDirectory(root, home, thread) {
39
- z.uuid().parse(thread);
40
- const hash = createHash("sha256").update(fs.realpathSync(home)).digest("hex");
41
- return path.join(root, "inputs", hash, thread);
42
- }
43
- export const hookInputSchema = z.object({
44
- delivery_id: z.uuid(), thread_id: z.uuid(), text_sha256: z.string(),
45
- }).strict();
46
- export function answerDigest(text) { return createHash("sha256").update(text).digest("hex"); }
1
+ // Codexの設定領域にはhook登録だけを置き、配送の所有情報はAitermが保持する。本体はaiterm-steer-delivery。
2
+ import * as steer from "aiterm-steer-delivery";
3
+ import { AITERM_PROFILE } from "./steer-profile.js";
4
+ export const codexHookConfigSchema = steer.codexHookConfigSchema(AITERM_PROFILE);
5
+ export const { writeHookJson, codexInputDirectory, hookInputSchema, answerDigest } = steer;
6
+ export function codexHookDirectory() { return steer.codexHookDirectory(AITERM_PROFILE); }
7
+ export function readCodexHookConfig(directory = codexHookDirectory()) { return steer.readCodexHookConfig(AITERM_PROFILE, directory); }
47
8
  export function registerCodexHookInput(home, thread, id, text, root = codexHookDirectory()) {
48
- const input = hookInputSchema.parse({ delivery_id: id, thread_id: thread, text_sha256: answerDigest(text) });
49
- const directory = codexInputDirectory(root, home, thread);
50
- fs.mkdirSync(path.join(directory, "pending"), { recursive: true, mode: 0o700 });
51
- // 一つの配送IDを別の本文で再使用しない。送信前の所有記録は再送の指示にはしない。
52
- fs.writeFileSync(path.join(directory, "pending", `${id}.json`), JSON.stringify(input) + "\n", { flag: "wx", mode: 0o600 });
53
- }
54
- export function finishCodexHookSubmission(home, thread, id, root) {
55
- const directory = codexInputDirectory(root, home, thread);
56
- const file = path.join(directory, "settled", `${id}.json`);
57
- writeHookJson(file, { delivery_id: id });
58
- if (!fs.existsSync(path.join(directory, "pending", `${id}.json`)))
59
- fs.rmSync(file, { force: true });
9
+ steer.registerCodexHookInput(home, thread, id, text, root);
60
10
  }
11
+ export function finishCodexHookSubmission(home, thread, id, root) { steer.finishCodexHookSubmission(home, thread, id, root); }
61
12
  export function codexHookDeliveryState(home, thread, id, root = codexHookDirectory()) {
62
- if (!fs.existsSync(path.join(root, "inputs")))
63
- return null;
64
- let value;
65
- try {
66
- value = JSON.parse(fs.readFileSync(path.join(codexInputDirectory(root, home, thread), "claims", `${z.uuid().parse(id)}.json`), "utf8"));
67
- }
68
- catch (error) {
69
- if (error.code === "ENOENT")
70
- return null;
71
- throw error;
72
- }
73
- // 所有権のlinkだけを作った段階では、公式キューからまだ削除していない。
74
- if (hookInputSchema.safeParse(value).success)
75
- return null;
76
- if (value.state === "unknown")
77
- return "unknown";
78
- if (value.state === "deleting")
79
- return readRuntimeProcesses().some(row => row.pid === value.pid && row.started_identity === value.started_identity) ? "sending" : "unknown";
80
- if (["emitted", "not_in_queue"].includes(value.state))
81
- return null;
82
- throw new CodexDeliveryError("CODEX_HOOK_STATE_INVALID", "hookの配送状態を読めません");
83
- }
84
- export function ownedCodexHooks(response, command, file) {
85
- const rows = response?.data;
86
- if (!Array.isArray(rows) || rows.length !== 1 || !Array.isArray(rows[0]?.hooks) || rows[0]?.errors?.length) {
87
- throw new CodexDeliveryError("CODEX_HOOK_UNAVAILABLE", "公式APIでCodexのhook設定を読めません");
88
- }
89
- const source = fs.realpathSync(file);
90
- const owned = rows[0].hooks.filter((hook) => hook.command === command && hook.sourcePath === source);
91
- for (const event of ["postToolUse", "stop"]) {
92
- const matches = owned.filter((hook) => hook.eventName === event);
93
- if (matches.length !== 1 || matches[0].async || matches[0].handlerType !== "command") {
94
- throw new CodexDeliveryError("CODEX_HOOK_UNAVAILABLE", "Aitermの同期hook登録を確認できません");
95
- }
96
- }
97
- if (owned.length !== 2)
98
- throw new CodexDeliveryError("CODEX_HOOK_UNAVAILABLE", "Aitermのhook登録が重複しています");
99
- return owned;
100
- }
101
- export function assertCodexHooksReady(response, command, file) {
102
- if (ownedCodexHooks(response, command, file).some(hook => !hook.enabled || !["trusted", "managed"].includes(hook.trustStatus))) {
103
- throw new CodexDeliveryError("CODEX_HOOK_UNTRUSTED", "AitermのCodex hookが無効または未承認です。aiterm-setupを実行してください");
104
- }
13
+ return steer.codexHookDeliveryState(home, thread, id, root);
105
14
  }
15
+ export function ownedCodexHooks(response, command, file) { return steer.ownedCodexHooks(AITERM_PROFILE, response, command, file); }
16
+ export function assertCodexHooksReady(response, command, file) { steer.assertCodexHooksReady(AITERM_PROFILE, response, command, file); }
106
17
  export function assertCodexHookParentCurrent(config, rows, pid = process.pid) {
107
- try {
108
- fs.accessSync(config.node, fs.constants.X_OK);
109
- fs.accessSync(config.hook, fs.constants.R_OK);
110
- }
111
- catch {
112
- throw new CodexDeliveryError("CODEX_HOOK_RUNTIME_UNAVAILABLE", "Codex親hookの実行ファイルを確認できません。aiterm-setupを実行してください");
113
- }
114
- if (!config.stale_processes.length)
115
- return;
116
- const processes = new Map((rows ?? readRuntimeProcesses()).map(row => [row.pid, row]));
117
- const seen = new Set();
118
- let current = processes.get(pid);
119
- while (current && !seen.has(current.pid)) {
120
- seen.add(current.pid);
121
- if (config.stale_processes.some(stale => stale.pid === current.pid && stale.started_identity === current.started_identity)) {
122
- throw new CodexDeliveryError("CODEX_STEER_RESTART_REQUIRED", "親のCodexはhookの導入前から動いています。完全終了して再起動してください");
123
- }
124
- current = processes.get(current.parent_pid);
125
- }
18
+ steer.assertCodexHookParentCurrent(AITERM_PROFILE, config, rows, pid);
126
19
  }
@@ -1,15 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // stdoutはCodexの公式hook出力だけに使う。失敗をStop継続のexit 2に変換しない。
3
- import { runCodexResultHook } from "./codex-parent-hooks.js";
4
- let input = "";
5
- try {
6
- for await (const chunk of process.stdin)
7
- input += chunk;
8
- await runCodexResultHook(JSON.parse(input), value => new Promise((resolve, reject) => {
9
- process.stdout.write(JSON.stringify(value) + "\n", error => error ? reject(error) : resolve());
10
- }), { directory: process.argv[2] });
11
- }
12
- catch (error) {
13
- process.stderr.write(`CODEX_PARENT_HOOK_FAILED: ${error instanceof Error ? error.message : String(error)}\n`);
14
- process.exitCode = 1;
15
- }
2
+ // Codexが起動する同期hook(PostToolUse/Stop)。stdoutはCodexの公式hook出力だけに使う。本体はaiterm-steer-delivery。
3
+ import { runCodexHookMain } from "aiterm-steer-delivery";
4
+ import { AITERM_PROFILE } from "./steer-profile.js";
5
+ await runCodexHookMain(AITERM_PROFILE, process.argv[2]);
@@ -1,113 +1,6 @@
1
- // 親の同期hookだけが、公式キューのAiterm回答を同じターンの入力へ移す。
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { z } from "zod";
5
- import { realCodexHome } from "./harnesses/codex.js";
6
- import { withCodexReceiver } from "./codex-parent-receiver.js";
7
- import { CodexDeliveryError } from "./codex-delivery-error.js";
8
- import { readRuntimeProcesses } from "./process-runtime.js";
9
- import { currentCodexDesktopBinary } from "./codex-desktop-binary.js";
10
- import { answerDigest, codexHookDirectory, codexInputDirectory, hookInputSchema, readCodexHookConfig, writeHookJson } from "./codex-hook-state.js";
1
+ // 親の同期hookだけが、公式キューのAiterm回答を同じターンの入力へ移す。本体はaiterm-steer-delivery。
2
+ import * as steer from "aiterm-steer-delivery";
3
+ import { AITERM_PROFILE } from "./steer-profile.js";
11
4
  export async function runCodexResultHook(input, emit, options = {}) {
12
- const event = z.object({ session_id: z.uuid(), turn_id: z.string().min(1),
13
- hook_event_name: z.enum(["PostToolUse", "Stop"]) }).parse(input);
14
- const root = options.directory ?? codexHookDirectory();
15
- const config = readCodexHookConfig(root);
16
- if (!config?.enabled) {
17
- await emit({});
18
- return;
19
- }
20
- const home = options.codex_home ?? realCodexHome();
21
- if (fs.realpathSync(home) !== fs.realpathSync(config.codex_home)) {
22
- throw new CodexDeliveryError("CODEX_HOOK_HOME_MISMATCH", "hookと配送設定のCodex環境が一致しません");
23
- }
24
- const directory = codexInputDirectory(root, home, event.session_id);
25
- const pending = path.join(directory, "pending");
26
- if (!fs.existsSync(pending) || !fs.readdirSync(pending).length) {
27
- await emit({});
28
- return;
29
- }
30
- const claims = path.join(directory, "claims");
31
- fs.mkdirSync(claims, { recursive: true, mode: 0o700 });
32
- const taken = [];
33
- const identity = readRuntimeProcesses().find(row => row.pid === process.pid);
34
- if (!identity)
35
- throw new CodexDeliveryError("CODEX_HOOK_PROCESS_UNAVAILABLE", "受信hookのprocessを確認できません");
36
- try {
37
- await withCodexReceiver({ thread_id: event.session_id, codex_home: home }, async (request) => {
38
- // 削除でoffsetが動くため、全ページを読んでから取り出す。
39
- const entries = [];
40
- let cursor = null;
41
- do {
42
- const page = await request("thread/queue/list", { threadId: event.session_id, cursor, limit: 100 });
43
- if (!Array.isArray(page?.data) || !(page.nextCursor === null || typeof page.nextCursor === "string")) {
44
- throw new CodexDeliveryError("CODEX_HOOK_QUEUE_INVALID", "公式キューの一覧応答を認識できません");
45
- }
46
- entries.push(...page.data);
47
- cursor = page.nextCursor;
48
- } while (cursor);
49
- const queued = new Set(entries.map(entry => entry.clientUserMessageId));
50
- // 送信processが終了し、キューに残っていない所有記録を消す。投入中は触らない。
51
- for (const name of fs.readdirSync(pending)) {
52
- const ack = path.join(directory, "settled", name);
53
- if (name.endsWith(".json") && !queued.has(name.slice(0, -5)) && fs.existsSync(ack)) {
54
- fs.rmSync(path.join(pending, name), { force: true });
55
- fs.rmSync(ack, { force: true });
56
- }
57
- }
58
- for (const entry of entries) {
59
- const id = z.uuid().safeParse(entry.clientUserMessageId);
60
- if (!id.success)
61
- continue;
62
- const source = path.join(pending, `${id.data}.json`);
63
- let owner;
64
- try {
65
- owner = hookInputSchema.parse(JSON.parse(fs.readFileSync(source, "utf8")));
66
- }
67
- catch (error) {
68
- if (error.code === "ENOENT")
69
- continue;
70
- throw error;
71
- }
72
- const text = entry.input?.length === 1 && entry.input[0].type === "text" ? entry.input[0].text : undefined;
73
- if (owner.delivery_id !== id.data || owner.thread_id !== event.session_id || typeof text !== "string" || answerDigest(text) !== owner.text_sha256) {
74
- throw new CodexDeliveryError("CODEX_HOOK_INPUT_CHANGED", "Aitermの配送記録とキュー本文が一致しません。取り出していません");
75
- }
76
- const file = path.join(claims, `${id.data}.json`);
77
- // 同時hookが同じ回答を取り出すことを防ぎ、中断したclaimは自動再送しない。
78
- try {
79
- fs.linkSync(source, file);
80
- fs.unlinkSync(source);
81
- }
82
- catch (error) {
83
- if (["ENOENT", "EEXIST"].includes(error.code ?? ""))
84
- continue;
85
- throw error;
86
- }
87
- fs.rmSync(path.join(directory, "settled", `${id.data}.json`), { force: true });
88
- const item = { file, delivery_id: id.data, text };
89
- taken.push(item);
90
- writeHookJson(file, { ...owner, text, state: "deleting", turn_id: event.turn_id, queued_submission_id: entry.id,
91
- pid: identity.pid, started_identity: identity.started_identity });
92
- const result = await request("thread/queue/delete", { threadId: event.session_id, queuedSubmissionId: entry.id });
93
- if (typeof result?.deleted !== "boolean")
94
- throw new CodexDeliveryError("CODEX_HOOK_QUEUE_INVALID", "キュー削除の結果を確認できません", true);
95
- if (!result.deleted) {
96
- writeHookJson(file, { ...owner, state: "not_in_queue", turn_id: event.turn_id });
97
- taken.pop();
98
- }
99
- }
100
- }, options.runtime ?? { executable: await currentCodexDesktopBinary(config), timeout_ms: 5_000 });
101
- const text = taken.map(item => item.text).join("\n\n");
102
- await emit(!taken.length ? {} : event.hook_event_name === "Stop"
103
- ? { decision: "block", reason: text }
104
- : { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } });
105
- for (const item of taken)
106
- writeHookJson(item.file, { delivery_id: item.delivery_id, state: "emitted", turn_id: event.turn_id, text: item.text });
107
- }
108
- catch (error) {
109
- for (const item of taken)
110
- writeHookJson(item.file, { delivery_id: item.delivery_id, state: "unknown", turn_id: event.turn_id, text: item.text });
111
- throw error;
112
- }
5
+ await steer.runCodexResultHook(AITERM_PROFILE, input, emit, options);
113
6
  }