aiterm-mcp 0.42.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,154 +1,22 @@
1
- // Steerの正規導入。公式hookを登録・承認してから、旧中継の起動差し替えを解除する。
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { isDeepStrictEqual } from "node:util";
1
+ // Steerの正規導入。公式hookを登録・承認してから、旧中継の起動差し替えを解除する。本体はaiterm-steer-delivery。
5
2
  import { fileURLToPath } from "node:url";
6
- import { currentCodexDesktopBinary } from "./codex-desktop-binary.js";
7
- import { withCodexReceiver } from "./codex-parent-receiver.js";
8
- import { realCodexHome } from "./harnesses/codex.js";
9
- import { SetupError } from "./setup-platform.js";
10
- import { setupNodeExecutable } from "./setup-node.js";
3
+ import * as steer from "aiterm-steer-delivery";
11
4
  import { readRelayConfig } from "./codex-relay-config.js";
12
- import { configureCodexSteer as configureLegacyRelay, findDesktopBinary } from "./setup-codex-relay.js";
13
- import { findWindowsCodexBinary } from "./windows-codex-setup.js";
14
- import { readRuntimeProcesses } from "./process-runtime.js";
15
- import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
16
- import { quotePowerShell } from "./windows-codex-state.js";
17
- import { assertCodexHooksReady, codexHookDirectory, ownedCodexHooks, readCodexHookConfig, writeHookJson } from "./codex-hook-state.js";
5
+ import { configureCodexSteer as configureLegacyRelay } from "./setup-codex-relay.js";
6
+ import { readCodexHookConfig } from "./codex-hook-state.js";
7
+ import { AITERM_PROFILE } from "./steer-profile.js";
18
8
  export function codexSteerSelected() { return readCodexHookConfig()?.enabled === true || readRelayConfig()?.enabled === true; }
19
- export function codexHookCommand(node, hook, directory, platform = process.platform) {
20
- if (platform === "win32") {
21
- const script = `& ${[node, hook, directory].map(quotePowerShell).join(" ")}; exit $LASTEXITCODE`;
22
- // Codexは利用中のPowerShellでhookを評価する。引用した実行パスには呼出し演算子が必要。
23
- return `& ${quotePowerShell(resolveWindowsPowerShell7())} -NoLogo -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
24
- }
25
- return [node, hook, directory].map(value => "'" + value.replace(/'/g, "'\"'\"'") + "'").join(" ");
26
- }
9
+ export const codexHookCommand = steer.codexHookCommand;
27
10
  export function mergeCodexParentHooks(file, command, previousCommand) {
28
- if (!fs.existsSync(file)) {
29
- try {
30
- if (fs.lstatSync(file).isSymbolicLink())
31
- throw new SetupError("codex_hook_config_invalid", "hook設定symlinkの参照先がありません");
32
- }
33
- catch (error) {
34
- if (error.code !== "ENOENT")
35
- throw error;
36
- }
37
- }
38
- const target = fs.existsSync(file) ? fs.realpathSync(file) : file;
39
- let current = {};
40
- if (fs.existsSync(target)) {
41
- try {
42
- current = JSON.parse(fs.readFileSync(target, "utf8"));
43
- }
44
- catch {
45
- throw new SetupError("codex_hook_config_invalid", "Codexのhook設定JSONを読めません");
46
- }
47
- }
48
- const object = (value) => value && typeof value === "object" && !Array.isArray(value);
49
- if (!object(current) || (current.hooks !== undefined && !object(current.hooks)))
50
- throw new SetupError("codex_hook_config_invalid", "Codexのhooks設定を読めません");
51
- const hooks = { ...current.hooks };
52
- for (const event of ["PostToolUse", "Stop"]) {
53
- const groups = hooks[event] ?? [];
54
- if (!Array.isArray(groups) || groups.some(group => !object(group) || !Array.isArray(group.hooks)))
55
- throw new SetupError("codex_hook_config_invalid", `${event}のhook設定を読めません`);
56
- // 既存の位置で置換する。末尾へ移すと同じ設定の再導入でも変更扱いになり、
57
- // 稼働中のCodexへ不要な再起動を要求してしまう。
58
- hooks[event] = [];
59
- let insertionIndex = null;
60
- for (const group of groups) {
61
- const remaining = group.hooks.filter((hook) => !(hook?.type === "command" && ((command !== null && hook.command === command) || (previousCommand && hook.command === previousCommand))));
62
- if (remaining.length !== group.hooks.length && insertionIndex === null)
63
- insertionIndex = hooks[event].length;
64
- if (remaining.length)
65
- hooks[event].push({ ...group, hooks: remaining });
66
- }
67
- if (command !== null)
68
- hooks[event].splice(insertionIndex ?? hooks[event].length, 0, { ...(event === "PostToolUse" ? { matcher: ".*" } : {}),
69
- hooks: [{ type: "command", command, timeout: 20, ...(event === "PostToolUse" ? { additionalContextLimit: 0 } : {}) }] });
70
- if (!hooks[event].length)
71
- delete hooks[event];
72
- }
73
- const next = { ...current, hooks };
74
- if (isDeepStrictEqual(current, next))
75
- return false;
76
- if (fs.existsSync(target))
77
- fs.copyFileSync(target, `${target}.aiterm-backup`);
78
- writeHookJson(target, next);
79
- if (!isDeepStrictEqual(JSON.parse(fs.readFileSync(target, "utf8")), next))
80
- throw new SetupError("codex_hook_readback_failed", "Codex hookの読戻しが一致しません");
81
- return true;
11
+ return steer.mergeCodexParentHooks(AITERM_PROFILE, file, command, previousCommand);
82
12
  }
83
13
  export async function verifyCodexHookRegistration(config, approve) {
84
- fs.accessSync(config.node, fs.constants.X_OK);
85
- fs.accessSync(config.hook, fs.constants.R_OK);
86
- const file = path.join(config.codex_home, "hooks.json");
87
- await withCodexReceiver({ thread_id: "00000000-0000-4000-8000-000000000000", codex_home: config.codex_home }, async (request) => {
88
- const list = () => request("hooks/list", { cwds: [config.codex_home] });
89
- const hooks = ownedCodexHooks(await list(), config.command, file);
90
- if (approve) {
91
- const edits = hooks.filter(hook => !hook.enabled || hook.trustStatus !== "trusted").flatMap(hook => {
92
- if (typeof hook.key !== "string" || typeof hook.currentHash !== "string")
93
- throw new SetupError("codex_hook_schema_unknown", "公式hookの承認情報を認識できません");
94
- const key = `hooks.state.${JSON.stringify(hook.key)}`;
95
- return [
96
- { keyPath: `${key}.trusted_hash`, value: hook.currentHash, mergeStrategy: "replace" },
97
- { keyPath: `${key}.enabled`, value: true, mergeStrategy: "replace" },
98
- ];
99
- });
100
- if (edits.length)
101
- await request("config/batchWrite", { edits, filePath: path.join(config.codex_home, "config.toml") });
102
- }
103
- assertCodexHooksReady(await list(), config.command, file);
104
- }, { executable: await currentCodexDesktopBinary(config, { directory: codexHookDirectory() }) });
14
+ await steer.verifyCodexHookRegistration(AITERM_PROFILE, config, approve);
105
15
  }
106
16
  export async function configureCodexSteer(action = "status", overrides = {}) {
107
- const runtime = { platform: process.platform, directory: codexHookDirectory(), codex_home: realCodexHome(),
108
- node: process.execPath, hook: fileURLToPath(new URL("./codex-parent-hook.js", import.meta.url)),
109
- findBinary: process.platform === "win32" ? findWindowsCodexBinary : findDesktopBinary,
110
- processes: readRuntimeProcesses, legacy: readRelayConfig,
111
- disableLegacy: () => configureLegacyRelay("disable"), verify: verifyCodexHookRegistration, ...overrides };
112
- const previous = readCodexHookConfig(runtime.directory);
113
- const legacy = runtime.legacy();
114
- const file = path.join(action === "disable" && previous ? previous.codex_home : runtime.codex_home, "hooks.json");
115
- const save = (config) => writeHookJson(path.join(runtime.directory, "config.json"), config);
116
- const needsRestart = (config) => runtime.processes().some(process => config.stale_processes.some(stale => stale.pid === process.pid && stale.started_identity === process.started_identity));
117
- if (action === "status") {
118
- if (!previous?.enabled)
119
- return legacy?.enabled ? { status: "failed", reason_code: "codex_steer_migration_required" } : { status: "disabled" };
120
- await runtime.verify(previous, false);
121
- return legacy?.enabled || needsRestart(previous) ? { status: "restart_required", reason_code: "codex_restart_required" } : { status: "ready" };
122
- }
123
- if (action === "disable") {
124
- if (previous?.enabled) {
125
- mergeCodexParentHooks(file, null, previous.command);
126
- save({ ...previous, enabled: false });
127
- }
128
- if (legacy?.enabled)
129
- return runtime.disableLegacy();
130
- return previous?.enabled ? { status: "restart_required", reason_code: "codex_restart_required" } : { status: "disabled" };
131
- }
132
- if (!["darwin", "win32"].includes(runtime.platform))
133
- return { status: "unsupported", reason_code: "codex_steer_platform_unsupported" };
134
- if (previous?.enabled && fs.realpathSync(previous.codex_home) !== fs.realpathSync(runtime.codex_home))
135
- throw new SetupError("codex_steer_configuration_conflict", "別のCODEX_HOMEでSteerが有効です。先に既存の選択を解除してください");
136
- const node = setupNodeExecutable(runtime.node);
137
- fs.accessSync(node, fs.constants.X_OK);
138
- fs.accessSync(runtime.hook, fs.constants.R_OK);
139
- const binary = runtime.findBinary();
140
- const command = codexHookCommand(node, runtime.hook, runtime.directory, runtime.platform);
141
- const changed = mergeCodexParentHooks(file, command, previous?.command);
142
- const stale = !previous?.enabled || changed || legacy?.enabled
143
- ? runtime.processes().filter(row => row.executable === binary || row.command.startsWith(binary + " "))
144
- .map(({ pid, started_identity }) => ({ pid, started_identity }))
145
- : previous.stale_processes;
146
- const config = { schema: "aiterm.codex-parent-hooks.v1", enabled: true,
147
- codex_home: runtime.codex_home, binary, command, node, hook: runtime.hook, stale_processes: stale };
148
- await runtime.verify(config, true);
149
- // 解除が中断しても、次のenableで移行を続行できるよう所有情報を先に保存する。
150
- save(config);
151
- if (legacy?.enabled)
152
- await runtime.disableLegacy();
153
- return needsRestart(config) ? { status: "restart_required", reason_code: "codex_restart_required" } : { status: "ready" };
17
+ return steer.configureCodexSteer(AITERM_PROFILE, action, {
18
+ hook: fileURLToPath(new URL("./codex-parent-hook.js", import.meta.url)),
19
+ legacy: readRelayConfig, disableLegacy: () => configureLegacyRelay("disable"), verify: verifyCodexHookRegistration,
20
+ ...overrides,
21
+ });
154
22
  }
@@ -3,8 +3,10 @@ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathS
3
3
  import { dirname, join } from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { isDeepStrictEqual } from "node:util";
6
+ import * as steer from "aiterm-steer-delivery";
6
7
  import { resolveAgentBin } from "./agent-resolver.js";
7
8
  import { SetupError, runSetupCommand } from "./setup-platform.js";
9
+ import { AITERM_PROFILE } from "./steer-profile.js";
8
10
  export { powershellInvocation } from "./setup-platform.js";
9
11
  const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
12
  export function mergeJsonMcp(file, registration) {
@@ -56,215 +58,28 @@ export function mergeJsonMcp(file, registration) {
56
58
  }
57
59
  return "configured";
58
60
  }
61
+ // 配送hookの登録・解除はaiterm-steer-delivery。hook入口はMCP登録と同じdist directoryに置く。
62
+ function hookRuntime(registration, file) {
63
+ return { command: registration.command, script: join(dirname(registration.args[0]), file) };
64
+ }
59
65
  export function claudeParentHookEntries(registration) {
60
- const command = { type: "command", command: registration.command, args: [join(dirname(registration.args[0]), "claude-parent-hook.js")] };
61
- const matcher = "^mcp__aiterm__(agent_launch|claude_agent|codex_agent|grok_agent|pty_send|claude_turn)$";
62
- return {
63
- PreToolUse: [{ matcher, hooks: [{ ...command, timeout: 15 }] }],
64
- PostToolUse: [{ matcher, hooks: [{ ...command, asyncRewake: true, timeout: 86400 }] }],
65
- SessionEnd: [{ hooks: [{ ...command, timeout: 15 }] }],
66
- };
66
+ return steer.claudeParentHookEntries(AITERM_PROFILE, hookRuntime(registration, AITERM_PROFILE.hooks.claude));
67
67
  }
68
68
  export function mergeClaudeParentHooks(file, registration) {
69
- const target = existsSync(file) ? realpathSync(file) : file;
70
- let current = {};
71
- if (existsSync(target)) {
72
- try {
73
- current = JSON.parse(readFileSync(target, "utf8"));
74
- }
75
- catch {
76
- throw new SetupError("config_invalid", "Claudeのhook設定JSONを読めません");
77
- }
78
- }
79
- if (!record(current) || (current.hooks !== undefined && !record(current.hooks))) {
80
- throw new SetupError("config_invalid", "Claudeのhooks設定はobjectである必要があります");
81
- }
82
- if (current.disableAllHooks === true)
83
- throw new SetupError("claude_parent_hooks_disabled", "Claudeのhookが無効です。disableAllHooksをfalseに変更してからsetupしてください");
84
- const hooks = { ...current.hooks };
85
- for (const [event, additions] of Object.entries(claudeParentHookEntries(registration))) {
86
- const previous = hooks[event] ?? [];
87
- if (!Array.isArray(previous) || previous.some(group => !record(group) || !Array.isArray(group.hooks))) {
88
- throw new SetupError("config_invalid", `Claudeの${event} hook形式を読めません`);
89
- }
90
- // 他製品のhookとmatcherはそのまま保ち、当製品の専用entryだけを更新する。
91
- const retained = previous.map(group => ({ ...group, hooks: group.hooks.filter(hook => !isClaudeParentHook(hook))
92
- })).filter(group => group.hooks.length > 0);
93
- hooks[event] = [...retained, ...additions];
94
- }
95
- const next = { ...current, hooks };
96
- if (isDeepStrictEqual(current, next))
97
- return "unchanged";
98
- mkdirSync(dirname(target), { recursive: true });
99
- const temporary = `${target}.aiterm-${randomUUID()}`;
100
- try {
101
- writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600, flag: "wx" });
102
- if (existsSync(target))
103
- copyFileSync(target, `${target}.aiterm-backup`);
104
- renameSync(temporary, target);
105
- }
106
- finally {
107
- if (existsSync(temporary))
108
- unlinkSync(temporary);
109
- }
110
- if (!isDeepStrictEqual(JSON.parse(readFileSync(target, "utf8")), next)) {
111
- throw new SetupError("config_readback_failed", "Claudeのhook登録の読戻しが一致しません");
112
- }
113
- return "configured";
114
- }
115
- function isClaudeParentHook(hook) {
116
- return record(hook) && hook.type === "command" && Array.isArray(hook.args)
117
- && typeof hook.args[0] === "string" && /[/\\]claude-parent-hook\.js$/.test(hook.args[0]);
69
+ return steer.mergeClaudeParentHooks(AITERM_PROFILE, file, hookRuntime(registration, AITERM_PROFILE.hooks.claude));
118
70
  }
119
71
  /** hookを持たない旧版へ戻す前に、当製品の登録だけを除く。 */
120
72
  export function removeClaudeParentHooks(file) {
121
- if (!existsSync(file))
122
- return "unchanged";
123
- const target = realpathSync(file);
124
- const current = JSON.parse(readFileSync(target, "utf8"));
125
- if (!record(current) || (current.hooks !== undefined && !record(current.hooks))) {
126
- throw new SetupError("config_invalid", "Claudeのhook設定を読めません");
127
- }
128
- if (current.hooks === undefined)
129
- return "unchanged";
130
- const hooks = { ...current.hooks };
131
- for (const event of ["PreToolUse", "PostToolUse", "SessionEnd"]) {
132
- if (hooks[event] === undefined)
133
- continue;
134
- if (!Array.isArray(hooks[event]))
135
- throw new SetupError("config_invalid", "Claudeのhook設定を読めません");
136
- hooks[event] = hooks[event].map(group => {
137
- if (!record(group) || !Array.isArray(group.hooks))
138
- throw new SetupError("config_invalid", "Claudeのhook設定を読めません");
139
- return { ...group, hooks: group.hooks.filter(hook => !isClaudeParentHook(hook)) };
140
- }).filter(group => group.hooks.length > 0);
141
- }
142
- const next = { ...current, hooks };
143
- if (isDeepStrictEqual(current, next))
144
- return "unchanged";
145
- const temporary = `${target}.aiterm-${randomUUID()}`;
146
- try {
147
- writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600, flag: "wx" });
148
- copyFileSync(target, `${target}.aiterm-backup`);
149
- renameSync(temporary, target);
150
- }
151
- finally {
152
- if (existsSync(temporary))
153
- unlinkSync(temporary);
154
- }
155
- if (!isDeepStrictEqual(JSON.parse(readFileSync(target, "utf8")), next))
156
- throw new SetupError("config_readback_failed", "Claude hook解除の読戻しが一致しません");
157
- return "removed";
158
- }
159
- function shellQuote(value) {
160
- if (process.platform === "win32")
161
- return `'${value.replace(/'/g, "''")}'`;
162
- return `'${value.replace(/'/g, `'"'"'`)}'`;
73
+ return steer.removeClaudeParentHooks(AITERM_PROFILE, file);
163
74
  }
164
75
  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");
76
+ return steer.cursorParentHookCommand(hookRuntime(registration, AITERM_PROFILE.hooks.cursor));
171
77
  }
172
78
  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";
79
+ return steer.mergeCursorParentHooks(AITERM_PROFILE, file, hookRuntime(registration, AITERM_PROFILE.hooks.cursor));
228
80
  }
229
81
  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";
82
+ return steer.removeCursorParentHooks(AITERM_PROFILE, file);
268
83
  }
269
84
  export function configureIntegrations(home, registration, run = runSetupCommand, resolveClient = resolveAgentBin) {
270
85
  const results = {};
@@ -4,13 +4,9 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
5
5
  import { resolveWinPaneShell } from "./agent-resolver.js";
6
6
  import { ensureWinPsmux, resolveTmux } from "./tmux-runtime.js";
7
- export class SetupError extends Error {
8
- code;
9
- constructor(code, message) {
10
- super(message);
11
- this.code = code;
12
- }
13
- }
7
+ // setupの失敗は配送パッケージと同じclassにする。パッケージのsetupが投げたものも同じreason_codeで返す。
8
+ import { SetupError } from "aiterm-steer-delivery";
9
+ export { SetupError };
14
10
  export function powershellInvocation(command, args) {
15
11
  const literal = (value) => `'${value.replaceAll("'", "''")}'`;
16
12
  const script = `& ${[command, ...args].map(literal).join(" ")}; if ($null -eq $LASTEXITCODE) { exit 1 }; exit $LASTEXITCODE`;
@@ -0,0 +1,18 @@
1
+ // Aitermがaiterm-steer-deliveryへ渡す製品の識別情報。保存場所・hook入口の名前・案内文は従来のまま。
2
+ import * as path from "node:path";
3
+ import { ensureStateRoot } from "./agent-shared.js";
4
+ import { relayConfigDirectory } from "./codex-relay-config.js";
5
+ export const AITERM_PROFILE = {
6
+ id: "aiterm",
7
+ display_name: "Aiterm",
8
+ setup_command: "aiterm-setup",
9
+ codex_steer_command: "aiterm-setup --codex-steer enable",
10
+ mcp_server: "aiterm",
11
+ dispatch_tools: ["agent_launch", "claude_agent", "codex_agent", "grok_agent", "pty_send", "claude_turn"],
12
+ state_root: ensureStateRoot,
13
+ config_root: () => path.dirname(relayConfigDirectory()),
14
+ hooks: { codex: "codex-parent-hook.js", claude: "claude-parent-hook.js", cursor: "cursor-parent-hook.js" },
15
+ codex_client_name: "aiterm_parent_delivery",
16
+ codex_hook_schema: "aiterm.codex-parent-hooks.v1",
17
+ backup_suffix: ".aiterm-backup",
18
+ };
@@ -19,6 +19,8 @@ Grok/Composerのsandbox起動拒否については、[DESIGNの失敗と復旧
19
19
 
20
20
  Codex親への回答の自動配送は[DESIGN](DESIGN.md#codex親への自動配送)に、対応範囲と失敗時の状態を置く。
21
21
  Claude Code親の非同期hookによる受信は[DESIGN](DESIGN.md#claude-code親への自動配送)を参照する。
22
+ 親配送の共通モジュール化は[DESIGNの共通化設計](DESIGN.md#親配送の共通モジュール化設計)を参照する。
23
+ 共通化は設計段階であり、稼働済みAitermを正本とする。
22
24
 
23
25
  ## 履歴と証拠
24
26
 
package/docs/DESIGN.md CHANGED
@@ -75,6 +75,14 @@ Grokは待ち行列へ入れた後に「send now」を押す。旧turnは`cancel
75
75
  Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼び出し側は通常のdispatchだけを使う。
76
76
  起動直後のClaude sessionへの初回dispatchは、他harnessと同じくTUIの入力受付を確認してから貼付とEnterを送る。
77
77
 
78
+ ### 親配送の共通モジュール化(設計)
79
+
80
+ 共通化の正本は、正常稼働しているAitermの親配送実装と挙動である。他製品との差分はAitermへ揃える。
81
+ 親の識別、配送、hook、配送固有state、導入・診断を独立packageへ抽出し、各製品が通常の依存として使う。
82
+ 各製品の完了観測・本文作成・job台帳は各製品に残し、hook登録と保存場所も製品ごとに維持する。
83
+ 実装・移行は未実施であり、以下に記す現行契約はそのまま有効である。
84
+ 公開API、抽出元、移行と受入条件は[ADR 0073](https://github.com/kitepon/aiterm-mcp/blob/main/docs/adr/0073-parent-delivery-module.md)に定める。
85
+
78
86
  ### Codex親への自動配送
79
87
 
80
88
  `src/parent-delivery.ts`が依頼の送信前に宛先と完了境界を保存し、完了観測、加工前の回答保存、配送を所有する。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.42.2",
3
+ "version": "0.43.0",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [
@@ -70,6 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@modelcontextprotocol/sdk": "^1.29.0",
73
+ "aiterm-steer-delivery": "^0.1.0",
73
74
  "ws": "8.21.3",
74
75
  "zod": "^4.4.3"
75
76
  },