aiterm-mcp 0.35.1 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/LICENSE +1 -0
- package/README.ja.md +30 -6
- package/README.md +32 -6
- package/dist/codex-delivery-error.js +10 -0
- package/dist/codex-parent-receiver.js +29 -11
- package/dist/codex-relay-client.js +117 -0
- package/dist/codex-relay-config.js +70 -0
- package/dist/codex-relay-launcher.js +63 -0
- package/dist/codex-relay-login.js +47 -0
- package/dist/codex-stdio-relay.js +120 -0
- package/dist/parent-delivery.js +2 -2
- package/dist/process-runtime.js +2 -1
- package/dist/runtime-error-store.js +11 -2
- package/dist/setup-cli.js +39 -8
- package/dist/setup-codex-relay.js +187 -0
- package/dist/setup.js +14 -0
- package/dist/windows-codex-connection.js +111 -0
- package/dist/windows-codex-relay.js +154 -0
- package/dist/windows-codex-setup.js +201 -0
- package/dist/windows-codex-state.js +44 -0
- package/docs/DESIGN.md +35 -9
- package/docs/RELEASE.md +15 -0
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.37.0] - 2026-09-13
|
|
11
|
+
|
|
12
|
+
### 追加
|
|
13
|
+
|
|
14
|
+
- WindowsのCodex Desktop親へのSteerと終了後再開に対応。公式CLI、認証付きloopback接続、native launcher、設定・解除をAitermだけで提供する。
|
|
15
|
+
- 稼働中の互換launcherは親process・ACL・公式RPCを確認した時だけ共有し、既存の起動設定を保持する。
|
|
16
|
+
|
|
17
|
+
## [0.36.0] - 2026-09-13
|
|
18
|
+
|
|
19
|
+
### 追加
|
|
20
|
+
|
|
21
|
+
- macOSのCodex Desktop向けにSteerの選択導入を追加。`aiterm-setup`の対話選択または`--codex-steer enable`で、同じ公式App Serverへ回答を送り、実行中は同じターンへ反映、終了後は同じタスクを再開する。
|
|
22
|
+
- 中継・POSIX起動処理をnpm packageへ同梱。公式App Serverの再ビルド、別配布、Pythonの追加導入は不要。公式バイナリのPID・Desktopとの親子関係・署名・通常環境を維持する。
|
|
23
|
+
- `--codex-steer status|disable`で実効状態の確認と元の設定への復元を行う。ログイン時の設定は専用LaunchAgentで維持する。初回は`restart_required`と終了コード3を返し、Codexの完全再起動を必要とする。
|
|
24
|
+
- Steerを選択した環境で接続できない時は明示エラーとし、queueへ自動退避しない。送信結果不明の回答は従来どおり保存し、自動再送しない。配送record schemaは維持し、Steer受付済みの`queued_submission_id`はnullとなる。
|
|
25
|
+
- Steerを持たない旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行する。Windows・LinuxのSteer選択は未対応を明示し、単品導入と通常のqueue配送は維持する。
|
|
26
|
+
|
|
27
|
+
### 修正
|
|
28
|
+
|
|
29
|
+
- 公開コマンドが配布物の検査前にbuildを行い、clean cloneからの公開で未生成の`dist/`を参照して失敗する問題を修正した。
|
|
30
|
+
- 同一エラーの再発時に`product_version`を更新し、snapshotに最終実発生時の版を公開する。
|
|
31
|
+
- runtime storeをv2へ移行する。旧v1の単発記録は版を保持し、複数回の旧集約は最終発生版を復元できないため`unknown`にする。読取りで状態JSONを書き戻さず、次のロック内更新でv2を保存する。
|
|
32
|
+
- v2を保存したstoreは旧writerで開かない。consumerを先に更新し、旧プロセスを終了してから新writerへ切り替える。旧版へ戻す場合は製品のバックアップを使い、v2をv1に偽装しない。
|
|
33
|
+
|
|
10
34
|
## [0.35.1] - 2026-09-10
|
|
11
35
|
|
|
12
36
|
### Fixed
|
|
@@ -1640,7 +1664,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1640
1664
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1641
1665
|
provenance.
|
|
1642
1666
|
|
|
1643
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
1667
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.0...HEAD
|
|
1668
|
+
[0.37.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...v0.37.0
|
|
1669
|
+
[0.36.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...v0.36.0
|
|
1644
1670
|
[0.35.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.0...v0.35.1
|
|
1645
1671
|
[0.35.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.34.0...v0.35.0
|
|
1646
1672
|
[0.34.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.1...v0.34.0
|
package/LICENSE
CHANGED
package/README.ja.md
CHANGED
|
@@ -40,11 +40,35 @@ WindowsはwingetでPowerShell 7・Git for Windows・psmux、macOSはHomebrewでt
|
|
|
40
40
|
Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package managerと実行権限は事前に必要。
|
|
41
41
|
他のLinuxでも既存tmuxを利用できるが、自動導入は`unsupported`で停止する。
|
|
42
42
|
既存設定の他サーバーを保持し、JSON設定は変更前の`.aiterm-backup`を残す。
|
|
43
|
-
結果の`status`は`ready`/`unsupported`/`failed`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
|
|
43
|
+
結果の`status`は`ready`/`unsupported`/`failed`/`restart_required`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
|
|
44
44
|
登録先はglobal packageのNodeとMCP入口の絶対パスで、npm一時cacheやsource checkoutは登録しない。
|
|
45
45
|
更新後も同じ入口を実行し、MCP clientを再起動する。npm install自体はユーザー設定を変更しない。
|
|
46
46
|
公開JSONは`schema: "aiterm.setup-result.v1"`、全体の`status`、端末の`backend`、
|
|
47
|
-
AI別の`integrations`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、それ以外は2となる。
|
|
47
|
+
AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
### Codex DesktopへSteerを有効にする(macOS・Windows)
|
|
51
|
+
|
|
52
|
+
対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
aiterm-setup --json --codex-steer enable
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
|
|
59
|
+
終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
|
|
60
|
+
Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
|
|
61
|
+
`aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
|
|
62
|
+
|
|
63
|
+
選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
|
|
64
|
+
Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
|
|
65
|
+
解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
66
|
+
SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
|
|
67
|
+
Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
|
|
68
|
+
Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
|
|
69
|
+
既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
|
|
70
|
+
互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
|
|
71
|
+
LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
|
|
48
72
|
|
|
49
73
|
cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
|
|
50
74
|
|
|
@@ -171,7 +195,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
171
195
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
172
196
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
173
197
|
|
|
174
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
198
|
+
**状態:** 開発継続中 · 現行公開版 **v0.37.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
175
199
|
|
|
176
200
|
### 更新と巻き戻し
|
|
177
201
|
|
|
@@ -205,7 +229,7 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
|
|
|
205
229
|
|
|
206
230
|
同じprimitiveが別エージェントのTUIを宿す。`agent_launch`の`harness`はagent loop・認証・hook・session・transcriptを所有する実行基盤、`model`は独立した選択。起動processは直接CLIと同じproject/user環境を使い、通常config、MCP、plugin、skill、permission、trust、memory、historyをcopy・filter・置換しない。
|
|
207
231
|
|
|
208
|
-
`aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返す。Codex
|
|
232
|
+
`aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返す。Codex親は選択に応じて公式Steerまたは受信キュー、Claude Code親は公式非同期hookで回答本文を自動受信する。それ以外の親は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
|
|
209
233
|
|
|
210
234
|
`agent_launch`・`pty_send`(agent dispatch)・`agent_steer`は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
|
|
211
235
|
|
|
@@ -518,9 +542,9 @@ SSH先がPowerShellの場合、`mark:true`は現在の標準`PS ...>`プロン
|
|
|
518
542
|
|
|
519
543
|
Codex/Claude Codeから子を起動・通常dispatchした後は、別作業へ進むか親のturnを終了するだけでよい。Aitermが完了を観測し、加工前の回答を保存して親へ届ける。waiter、`pty_read`による回答回収、子への送信指示は不要。子は全対応harnessから選べる。
|
|
520
544
|
|
|
521
|
-
自動配送時はreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodex
|
|
545
|
+
自動配送時は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`とする。自動再送はしない。
|
|
522
546
|
|
|
523
|
-
CodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
|
|
547
|
+
単品導入のCodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
|
|
524
548
|
|
|
525
549
|
Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が専用の`PreToolUse`、`PostToolUse`、`SessionEnd`を登録するため、Channelsの起動flagは不要。公式`asyncRewake` hookだけが裏で待ち、親はその間も次のturnへ進める。回答は`Stop hook feedback`として届く。hookのexit 2は親の再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
|
|
526
550
|
|
package/README.md
CHANGED
|
@@ -40,11 +40,35 @@ WindowsはwingetでPowerShell 7・Git for Windows・psmux、macOSはHomebrewでt
|
|
|
40
40
|
Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package managerと実行権限は事前に必要。
|
|
41
41
|
他のLinuxでも既存tmuxを利用できるが、自動導入は`unsupported`で停止する。
|
|
42
42
|
既存設定の他サーバーを保持し、JSON設定は変更前の`.aiterm-backup`を残す。
|
|
43
|
-
結果の`status`は`ready`/`unsupported`/`failed`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
|
|
43
|
+
結果の`status`は`ready`/`unsupported`/`failed`/`restart_required`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
|
|
44
44
|
登録先はglobal packageのNodeとMCP入口の絶対パスで、npm一時cacheやsource checkoutは登録しない。
|
|
45
45
|
更新後も同じ入口を実行し、MCP clientを再起動する。npm install自体はユーザー設定を変更しない。
|
|
46
46
|
公開JSONは`schema: "aiterm.setup-result.v1"`、全体の`status`、端末の`backend`、
|
|
47
|
-
AI別の`integrations`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、それ以外は2となる。
|
|
47
|
+
AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
### Codex DesktopへSteerを有効にする(macOS・Windows)
|
|
51
|
+
|
|
52
|
+
対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
aiterm-setup --json --codex-steer enable
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
|
|
59
|
+
終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
|
|
60
|
+
Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
|
|
61
|
+
`aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
|
|
62
|
+
|
|
63
|
+
選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
|
|
64
|
+
Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
|
|
65
|
+
解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
|
|
66
|
+
SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
|
|
67
|
+
Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
|
|
68
|
+
Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
|
|
69
|
+
既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
|
|
70
|
+
互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
|
|
71
|
+
LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
|
|
48
72
|
|
|
49
73
|
No clone or build is required. Each client launches the published package with:
|
|
50
74
|
|
|
@@ -185,7 +209,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
185
209
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
186
210
|
Release re-registers the Official MCP Registry entry.
|
|
187
211
|
|
|
188
|
-
**Status:** actively maintained · current public release **v0.
|
|
212
|
+
**Status:** actively maintained · current public release **v0.37.0** · 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).
|
|
189
213
|
|
|
190
214
|
### Update and rollback
|
|
191
215
|
|
|
@@ -225,7 +249,7 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
|
|
|
225
249
|
|
|
226
250
|
The same primitive hosts another agent's TUI. `agent_launch` starts a selected execution harness inside a fresh persistent terminal and returns a `session_id`. `harness` names the component that owns the agent loop, authentication, hooks, session, and transcript; `model` remains an independent choice. The launched process sees the same project and user environment as a direct CLI invocation: normal configuration, MCPs, plugins, skills, permissions, trust decisions, memory, and history are not copied, filtered, or replaced. Aiterm adds only completion correlation and a non-user sub-agent context containing `role=subagent`, the parent session, delegation depth, lineage, and `delegation_allowed=true`.
|
|
227
251
|
|
|
228
|
-
起動結果には正規`harness`を含む`aiterm.agent-launch-result.v1`が付き、旧`provider`は互換fieldとして残る。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`にも載る。Codexは通常rollout、Grokは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcriptの`turn_ended`を完了正本に使う。agentへの送信は非ブロックdispatchで、harnessごとの完了境界を表す整数`event_cursor`を返す。Codex
|
|
252
|
+
起動結果には正規`harness`を含む`aiterm.agent-launch-result.v1`が付き、旧`provider`は互換fieldとして残る。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`にも載る。Codexは通常rollout、Grokは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcriptの`turn_ended`を完了正本に使う。agentへの送信は非ブロックdispatchで、harnessごとの完了境界を表す整数`event_cursor`を返す。Codex親は選択に応じて公式Steerまたはqueue、Claude Code親は公式非同期hookで本文を自動受信する。他の親は[`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait)を使う。CursorのsubmitはadapterがCLIのextended keyboard protocolへ変換し、送信本文がcomposerへ残る場合は明示errorにする。
|
|
229
253
|
|
|
230
254
|
`agent_launch`, `pty_send` (agent dispatch), and `agent_steer` accept an optional `image`: an array of absolute paths to image files (png/jpg/jpeg/gif/webp). Aiterm appends an attachment block to the prompt, and every harness opens the path with its own file-reading tool and sees the image; the caller never learns harness-specific attachment tricks. Invalid paths are rejected before anything is sent.
|
|
231
255
|
|
|
@@ -513,6 +537,8 @@ continue to use `claude_approval`.
|
|
|
513
537
|
|
|
514
538
|
### Local runtime error snapshot
|
|
515
539
|
|
|
540
|
+
snapshotの`product_version`は各recordの最終実発生時の版を表す。store v2は旧v1を読み取り、単発記録の版を保持し、複数回の旧集約の版は`unknown`にする。読取りでは状態JSONを書き戻さず、次のロック内更新でv2を保存する。consumerを先に更新し、旧writerの終了後に新writerを使う。v2保存後の旧版への切替は、製品のバックアップ復元を伴う。
|
|
541
|
+
|
|
516
542
|
`aiterm-runtime-errors snapshot` exposes a machine-readable, product-owned local snapshot for the dotagents factory adapter. Collection is fail-closed unless the canonical dotagents factory-reporter config is schema-exact, its host profile matches the executing OS, and it contains the JSON boolean `collection.enabled: true`; reporting fields are schema-validated but endpoints and credential files are never contacted, and the store performs no network I/O. The only accepted observations are three fixed codes owned by the core boundary (PTY dependency, persistence, and optional vendor launcher). Stored data is limited to fixed templates and aggregate metadata (SHA-256 fingerprint, count, first/last seen, status, and monotonic sequence); exceptions, stderr/stdout, stacks, prompts, terminal/transcript/event bodies, paths, and arbitrary context cannot enter the API. Persisted JSON is revalidated with exact top/record fields and a recomputed fingerprint before explicit DTO projection.
|
|
517
543
|
|
|
518
544
|
Consumer flow is `aiterm-runtime-errors snapshot`, then `aiterm-runtime-errors ack --cursor N` after durable ingestion. Operators can use `resolve|reopen --fingerprint SHA256`. MCP collection and diagnostic reads run in timeout-bounded child processes, so a FIFO or stalled filesystem cannot block terminal work; child failure emits only the fixed store diagnostic. Store mutation uses a bounded bakery ticket queue: every waiter owns a never-reused ticket containing PID, process-start identity, and an owner token, so dead owners are removed by unique filename without fixed-path reclaim ABA. The queue deadline measures lack of progress by the same head owner, not total wait behind healthy predecessors; normal polling uses the native process-liveness check and validates process-start identity only when a blocker stalls. Worker deadlines use forced termination so a SIGTERM-ignoring child cannot mutate state after timeout. POSIX state is atomically replaced under `$XDG_STATE_HOME/aiterm-mcp/` (default `~/.local/state/aiterm-mcp/`) with owner/mode rechecked on every read. Windows native uses `%LOCALAPPDATA%\aiterm-mcp\`; each DACL is rebuilt and read back as one non-inherited FullControl ACE for the current SID. Windows path/DACL/timeout behavior is covered by pure tests in this change; no new Windows integration success is claimed.
|
|
@@ -551,9 +577,9 @@ For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` p
|
|
|
551
577
|
|
|
552
578
|
**Codex/Claude Code親には子の回答本文が自動で届く。** 子を起動・dispatchした後は、別作業へ進むか親のturnを終える。Aitermが完了を観測し、加工前の本文を保存して親へ渡す。waiter、`pty_read`による回答回収、子への返送指示は不要。子は全対応harnessから選べる。
|
|
553
579
|
|
|
554
|
-
自動配送のreceiptには`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodex
|
|
580
|
+
自動配送の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`とする。自動再送はしない。
|
|
555
581
|
|
|
556
|
-
|
|
582
|
+
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. This queue route applies when Desktop Steer is not enabled.
|
|
557
583
|
|
|
558
584
|
Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が専用の`PreToolUse`、`PostToolUse`、`SessionEnd`を登録するため、Channelsの起動flagは不要。公式`asyncRewake` hookだけが裏で待ち、親はその間も次のturnへ進める。回答は`Stop hook feedback`として届く。hookのexit 2は親の再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
|
|
559
585
|
|
|
@@ -0,0 +1,10 @@
|
|
|
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,19 +1,13 @@
|
|
|
1
|
-
// Codex
|
|
1
|
+
// Codex親の配送。Steerを選択した環境は同じ公式App Server、それ以外は公式queueを使う。
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { resolveAgentBin } from "./agent-resolver.js";
|
|
6
6
|
import { realCodexHome } from "./harnesses/codex.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
constructor(delivery_code, message, outcome_unknown = false) {
|
|
12
|
-
super(`${delivery_code}: ${message}`, 2);
|
|
13
|
-
this.delivery_code = delivery_code;
|
|
14
|
-
this.outcome_unknown = outcome_unknown;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
7
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
8
|
+
import { readRelayConfig, parentRelaySocket } from "./codex-relay-config.js";
|
|
9
|
+
import { withCodexRelay, verifyLoadedParent } from "./codex-relay-client.js";
|
|
10
|
+
export { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
17
11
|
/** modelの引数ではなく、CodexがMCP要求へ付けるmetadataだけを宛先にする。 */
|
|
18
12
|
export function codexParentFromRequest(clientName, metadata) {
|
|
19
13
|
if (clientName !== "codex-mcp-client")
|
|
@@ -24,6 +18,12 @@ export function codexParentFromRequest(clientName, metadata) {
|
|
|
24
18
|
}
|
|
25
19
|
return { thread_id: threadId, codex_home: path.resolve(realCodexHome()) };
|
|
26
20
|
}
|
|
21
|
+
function relaySocket(runtime) {
|
|
22
|
+
if (runtime)
|
|
23
|
+
return runtime.socket_path ?? null;
|
|
24
|
+
const config = readRelayConfig();
|
|
25
|
+
return config?.enabled ? parentRelaySocket(config) : null;
|
|
26
|
+
}
|
|
27
27
|
async function withCodexReceiver(parent, action, runtime = {}) {
|
|
28
28
|
const executable = runtime.executable ?? resolveAgentBin("codex");
|
|
29
29
|
if (!executable)
|
|
@@ -108,6 +108,11 @@ async function withCodexReceiver(parent, action, runtime = {}) {
|
|
|
108
108
|
}
|
|
109
109
|
/** 子へ送る前に、同じstoreの宛先と公式キューの対応を確認する。本文は保存・表示しない。 */
|
|
110
110
|
export async function verifyCodexParent(parent, runtime) {
|
|
111
|
+
const socket = relaySocket(runtime);
|
|
112
|
+
if (socket) {
|
|
113
|
+
await withCodexRelay(socket, request => verifyLoadedParent(request, parent.thread_id), runtime?.timeout_ms);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
111
116
|
await withCodexReceiver(parent, async (request) => {
|
|
112
117
|
const response = await request("thread/read", { threadId: parent.thread_id, includeTurns: false });
|
|
113
118
|
if (response?.thread?.id !== parent.thread_id) {
|
|
@@ -121,6 +126,19 @@ export async function verifyCodexParent(parent, runtime) {
|
|
|
121
126
|
}, runtime);
|
|
122
127
|
}
|
|
123
128
|
export async function submitCodexParentAnswer(parent, deliveryId, text, runtime) {
|
|
129
|
+
const socket = relaySocket(runtime);
|
|
130
|
+
if (socket) {
|
|
131
|
+
return withCodexRelay(socket, async (request) => {
|
|
132
|
+
await verifyLoadedParent(request, parent.thread_id);
|
|
133
|
+
// 公式の同一処理内で、実行中はSteer、終了済みなら開始する。本文は一度だけ送る。
|
|
134
|
+
const result = await request("turn/start", { threadId: parent.thread_id,
|
|
135
|
+
input: [{ type: "text", text, text_elements: [] }], clientUserMessageId: deliveryId });
|
|
136
|
+
if (typeof result?.turn?.id !== "string" || !result.turn.id) {
|
|
137
|
+
throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "配送先turnの受付IDを確認できません", true);
|
|
138
|
+
}
|
|
139
|
+
return { queued_submission_id: null };
|
|
140
|
+
}, runtime?.timeout_ms);
|
|
141
|
+
}
|
|
124
142
|
return withCodexReceiver(parent, async (request) => {
|
|
125
143
|
const result = await request("thread/queue/add", {
|
|
126
144
|
threadId: parent.thread_id,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
4
|
+
import { verifyRelaySocket } from "./codex-relay-config.js";
|
|
5
|
+
import { windowsSocketConnection } from "./windows-codex-connection.js";
|
|
6
|
+
/** 公式受付へ追加接続する。Desktopへの承認要求や通知には応答しない。 */
|
|
7
|
+
export async function withCodexRelay(socketPath, action, timeout = 15_000) {
|
|
8
|
+
let connection;
|
|
9
|
+
try {
|
|
10
|
+
if (process.platform === "win32")
|
|
11
|
+
connection = windowsSocketConnection(socketPath);
|
|
12
|
+
else {
|
|
13
|
+
verifyRelaySocket(socketPath);
|
|
14
|
+
connection = { url: "ws://localhost/rpc", options: { createConnection: () => createConnection(socketPath) } };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof CodexDeliveryError)
|
|
19
|
+
throw error;
|
|
20
|
+
throw new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverのsocketがありません。Codexを再起動してください");
|
|
21
|
+
}
|
|
22
|
+
const socket = new WebSocket(connection.url, {
|
|
23
|
+
...connection.options, handshakeTimeout: timeout, perMessageDeflate: false,
|
|
24
|
+
});
|
|
25
|
+
let sequence = 0;
|
|
26
|
+
let failure = null;
|
|
27
|
+
const pending = new Map();
|
|
28
|
+
const fail = (message) => {
|
|
29
|
+
failure = message;
|
|
30
|
+
for (const item of pending.values()) {
|
|
31
|
+
clearTimeout(item.timer);
|
|
32
|
+
item.reject(new CodexDeliveryError("CODEX_RELAY_TRANSPORT_FAILED", message, item.writing));
|
|
33
|
+
}
|
|
34
|
+
pending.clear();
|
|
35
|
+
};
|
|
36
|
+
socket.on("error", () => fail("公式App Serverとの通信が失敗しました"));
|
|
37
|
+
socket.on("close", () => fail("公式App Serverとの接続が終了しました"));
|
|
38
|
+
socket.on("message", (data, binary) => {
|
|
39
|
+
let value;
|
|
40
|
+
try {
|
|
41
|
+
if (binary)
|
|
42
|
+
throw new Error();
|
|
43
|
+
value = JSON.parse(data.toString());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
fail("公式App ServerのJSON応答を読めません");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (value?.method !== undefined)
|
|
50
|
+
return;
|
|
51
|
+
const item = pending.get(value?.id);
|
|
52
|
+
if (!item)
|
|
53
|
+
return;
|
|
54
|
+
clearTimeout(item.timer);
|
|
55
|
+
pending.delete(value.id);
|
|
56
|
+
if (value.error)
|
|
57
|
+
item.reject(new CodexDeliveryError("CODEX_RECEIVER_REJECTED", typeof value.error.message === "string" ? value.error.message : "公式受信口が要求を拒否しました"));
|
|
58
|
+
else if ("result" in value)
|
|
59
|
+
item.resolve(value.result);
|
|
60
|
+
else
|
|
61
|
+
item.reject(new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "公式受信口の応答にresultがありません", item.writing));
|
|
62
|
+
});
|
|
63
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
64
|
+
if (failure || socket.readyState !== WebSocket.OPEN) {
|
|
65
|
+
reject(new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", failure ?? "接続が開いていません"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const id = ++sequence;
|
|
69
|
+
const writing = method === "turn/start";
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
pending.delete(id);
|
|
72
|
+
reject(new CodexDeliveryError("CODEX_RECEIVER_TIMEOUT", `${method}の応答を確認できません`, writing));
|
|
73
|
+
}, timeout);
|
|
74
|
+
pending.set(id, { resolve, reject, timer, writing });
|
|
75
|
+
socket.send(JSON.stringify({ id, method, params }), error => { if (error)
|
|
76
|
+
fail("公式App Serverへの送信に失敗しました"); });
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
socket.once("open", resolve);
|
|
81
|
+
socket.once("error", () => reject(new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverへ接続できません")));
|
|
82
|
+
socket.once("close", () => reject(new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverが接続を閉じました")));
|
|
83
|
+
});
|
|
84
|
+
await request("initialize", { clientInfo: { name: "aiterm_parent_delivery", version: "1" } });
|
|
85
|
+
socket.send(JSON.stringify({ method: "initialized" }));
|
|
86
|
+
return await action(request);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
for (const item of pending.values())
|
|
90
|
+
clearTimeout(item.timer);
|
|
91
|
+
pending.clear();
|
|
92
|
+
socket.terminate();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function verifyLoadedParent(request, threadId) {
|
|
96
|
+
let cursor = null;
|
|
97
|
+
do {
|
|
98
|
+
const result = await request("thread/loaded/list", { limit: 100, ...(cursor ? { cursor } : {}) });
|
|
99
|
+
if (!Array.isArray(result?.data))
|
|
100
|
+
throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "実行中taskの一覧を確認できません");
|
|
101
|
+
if (result.data.includes(threadId)) {
|
|
102
|
+
const read = await request("thread/read", { threadId, includeTurns: false });
|
|
103
|
+
if (read?.thread?.id !== threadId)
|
|
104
|
+
throw new CodexDeliveryError("CODEX_PARENT_UNAVAILABLE", "親taskの識別が一致しません");
|
|
105
|
+
const subagent = read.thread.source?.subAgent;
|
|
106
|
+
if (subagent && typeof subagent === "object" && "thread_spawn" in subagent) {
|
|
107
|
+
throw new CodexDeliveryError("CODEX_PARENT_UNSUPPORTED", "Codexのnative sub-agentは外部processからの直接入力を受け付けません");
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const next = result.nextCursor ?? null;
|
|
112
|
+
if (next !== null && (typeof next !== "string" || next === cursor))
|
|
113
|
+
throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "task一覧の続きが不正です");
|
|
114
|
+
cursor = next;
|
|
115
|
+
} while (cursor);
|
|
116
|
+
throw new CodexDeliveryError("CODEX_PARENT_UNAVAILABLE", "同じ公式App Serverに親taskがありません。別processでのresumeやqueueへの退避は行いません");
|
|
117
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Aitermの選択設定と、同じ親processが所有する公式socketの識別。
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
7
|
+
import { CodexDeliveryError } from "./codex-delivery-error.js";
|
|
8
|
+
import { findWindowsParentSocket } from "./windows-codex-connection.js";
|
|
9
|
+
export const relayConfigSchema = z.object({
|
|
10
|
+
schema: z.literal("aiterm.codex-relay.v1"),
|
|
11
|
+
enabled: z.boolean(),
|
|
12
|
+
binary: z.string(), node: z.string(), launcher: z.string(), socket_root: z.string(),
|
|
13
|
+
previous_cli_path: z.string().nullable(),
|
|
14
|
+
}).strict();
|
|
15
|
+
export function relayConfigDirectory(home = process.env.HOME ?? homedir()) {
|
|
16
|
+
return path.join(home, ".config", "aiterm-mcp", "codex-relay");
|
|
17
|
+
}
|
|
18
|
+
export function readRelayConfig(directory = relayConfigDirectory()) {
|
|
19
|
+
const file = path.join(directory, "config.json");
|
|
20
|
+
let text;
|
|
21
|
+
try {
|
|
22
|
+
text = fs.readFileSync(file, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code === "ENOENT")
|
|
26
|
+
return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return relayConfigSchema.parse(JSON.parse(text));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new CodexDeliveryError("CODEX_RELAY_CONFIG_INVALID", "Steerの設定を読めません。aiterm-setup --codex-steer statusで確認してください");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function prepareRelayDirectory(directory) {
|
|
37
|
+
if (!path.isAbsolute(directory))
|
|
38
|
+
throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "socketディレクトリが絶対pathではありません");
|
|
39
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
40
|
+
const stat = fs.lstatSync(directory);
|
|
41
|
+
if (!stat.isDirectory() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o700) {
|
|
42
|
+
throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "socketディレクトリは本人所有の0700である必要があります");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function verifyRelaySocket(socket) {
|
|
46
|
+
const directory = fs.lstatSync(path.dirname(socket));
|
|
47
|
+
const stat = fs.lstatSync(socket);
|
|
48
|
+
if (!directory.isDirectory() || directory.uid !== process.getuid?.() || (directory.mode & 0o777) !== 0o700
|
|
49
|
+
|| !stat.isSocket() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o600) {
|
|
50
|
+
throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "本人所有の公式socketを確認できません");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function parentRelaySocket(config, processes, pid = process.pid) {
|
|
54
|
+
if (process.platform === "win32")
|
|
55
|
+
return findWindowsParentSocket(undefined, pid, config.launcher === config.previous_cli_path ? undefined : config.socket_root);
|
|
56
|
+
processes ??= readRuntimeProcesses();
|
|
57
|
+
const rows = new Map(processes.map(row => [row.pid, row]));
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
let current = rows.get(pid)?.parent_pid;
|
|
60
|
+
while (current && !seen.has(current)) {
|
|
61
|
+
seen.add(current);
|
|
62
|
+
const socket = path.join(config.socket_root, `${current}.sock`);
|
|
63
|
+
if (fs.existsSync(socket)) {
|
|
64
|
+
verifyRelaySocket(socket);
|
|
65
|
+
return socket;
|
|
66
|
+
}
|
|
67
|
+
current = rows.get(current)?.parent_pid;
|
|
68
|
+
}
|
|
69
|
+
throw new CodexDeliveryError("CODEX_STEER_RESTART_REQUIRED", "Steerを有効にしたCodex Desktopの親接続がありません。aiterm-setupの結果を確認し、Codexを再起動してください");
|
|
70
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// POSIXのexecで公式CLIのPIDとDesktopからの親子関係を維持する。
|
|
2
|
+
const quote = (text) => `'${text.replace(/'/g, `'"'"'`)}'`;
|
|
3
|
+
export function codexRelayLauncher(options) {
|
|
4
|
+
return `#!/bin/sh
|
|
5
|
+
binary=${quote(options.binary)}
|
|
6
|
+
node=${quote(options.node)}
|
|
7
|
+
relay=${quote(options.relay)}
|
|
8
|
+
root=${quote(options.socket_root)}
|
|
9
|
+
mode=root
|
|
10
|
+
value=no
|
|
11
|
+
for arg do
|
|
12
|
+
if [ "$value" = yes ]; then value=no; continue; fi
|
|
13
|
+
case "$arg" in
|
|
14
|
+
-c|--config|--enable|--disable|--listen) value=yes; continue ;;
|
|
15
|
+
esac
|
|
16
|
+
if [ "$mode" = root ]; then
|
|
17
|
+
case "$arg" in
|
|
18
|
+
--config=*|--enable=*|--disable=*|-c?*) ;;
|
|
19
|
+
app-server) mode=server ;;
|
|
20
|
+
*) mode=other; break ;;
|
|
21
|
+
esac
|
|
22
|
+
else
|
|
23
|
+
case "$arg" in
|
|
24
|
+
proxy|start|stop|status|generate-ts|generate-json-schema|--help|-h|--version|-V) mode=other; break ;;
|
|
25
|
+
esac
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
if [ "$mode" != server ]; then exec "$binary" "$@"; fi
|
|
29
|
+
|
|
30
|
+
# 引数を一巡させ、stdio指定だけを公式Unix受付へ変更する。
|
|
31
|
+
remaining=$#
|
|
32
|
+
value=no
|
|
33
|
+
while [ "$remaining" -gt 0 ]; do
|
|
34
|
+
arg=$1
|
|
35
|
+
shift
|
|
36
|
+
remaining=$((remaining - 1))
|
|
37
|
+
if [ "$value" = yes ]; then
|
|
38
|
+
set -- "$@" "$arg"
|
|
39
|
+
value=no
|
|
40
|
+
continue
|
|
41
|
+
fi
|
|
42
|
+
case "$arg" in
|
|
43
|
+
-c|--config|--enable|--disable) value=yes; set -- "$@" "$arg" ;;
|
|
44
|
+
--stdio) ;;
|
|
45
|
+
--listen)
|
|
46
|
+
if [ "$remaining" -eq 0 ] || [ "$1" != stdio:// ]; then
|
|
47
|
+
echo 'aiterm-relay: stdio以外の接続指定は変更できません' >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
shift
|
|
51
|
+
remaining=$((remaining - 1)) ;;
|
|
52
|
+
--listen=stdio://) ;;
|
|
53
|
+
--listen=*) echo 'aiterm-relay: stdio以外の接続指定は変更できません' >&2; exit 2 ;;
|
|
54
|
+
*) set -- "$@" "$arg" ;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
"$node" "$relay" --prepare "$root" || exit $?
|
|
58
|
+
socket="$root/$$.sock"
|
|
59
|
+
exec 3<&0
|
|
60
|
+
"$node" "$relay" "$socket" "$$" <&3 3<&- &
|
|
61
|
+
exec "$binary" "$@" --listen "unix://$socket" </dev/null >/dev/null 3<&-
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ユーザーのログイン時にもCodexの起動設定を適用する、Aiterm専用LaunchAgent。
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { SetupError } from "./setup-platform.js";
|
|
7
|
+
const label = "dev.kitepon.aiterm-codex-relay";
|
|
8
|
+
const xml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9
|
+
export function relayLoginPlist(launcher) {
|
|
10
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>/bin/launchctl</string><string>setenv</string><string>CODEX_CLI_PATH</string><string>${xml(launcher)}</string></array><key>RunAtLoad</key><true/></dict></plist>\n`;
|
|
11
|
+
}
|
|
12
|
+
function context(options) {
|
|
13
|
+
const directory = options.directory ?? path.join(process.env.HOME ?? homedir(), "Library", "LaunchAgents");
|
|
14
|
+
const uid = options.uid ?? process.getuid?.();
|
|
15
|
+
if (uid === undefined)
|
|
16
|
+
throw new SetupError("codex_steer_platform_unsupported", "LaunchAgentはmacOSだけで使用します");
|
|
17
|
+
const run = options.run ?? ((args) => {
|
|
18
|
+
const result = spawnSync("/bin/launchctl", args, { encoding: "utf8", timeout: 10_000 });
|
|
19
|
+
if (result.error)
|
|
20
|
+
throw new SetupError("codex_steer_login_failed", "ログイン時の起動設定を実行できません");
|
|
21
|
+
return result.status ?? -1;
|
|
22
|
+
});
|
|
23
|
+
return { directory, file: path.join(directory, `${label}.plist`), domain: `gui/${uid}`, service: `gui/${uid}/${label}`, run };
|
|
24
|
+
}
|
|
25
|
+
export function installRelayLogin(launcher, options = {}) {
|
|
26
|
+
const { directory, file, domain, service, run } = context(options);
|
|
27
|
+
const contents = relayLoginPlist(launcher);
|
|
28
|
+
if (fs.existsSync(file) && (fs.lstatSync(file).isSymbolicLink() || fs.readFileSync(file, "utf8") !== contents)) {
|
|
29
|
+
throw new SetupError("codex_steer_login_conflict", "同名のLaunchAgentが変更されています。所有外の設定は上書きしません");
|
|
30
|
+
}
|
|
31
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
32
|
+
if (!fs.existsSync(file))
|
|
33
|
+
fs.writeFileSync(file, contents, { mode: 0o600, flag: "wx" });
|
|
34
|
+
if (run(["print", service]) !== 0 && run(["bootstrap", domain, file]) !== 0)
|
|
35
|
+
throw new SetupError("codex_steer_login_failed", "ログイン時のSteer設定を登録できません");
|
|
36
|
+
}
|
|
37
|
+
export function removeRelayLogin(launcher, options = {}) {
|
|
38
|
+
const { file, service, run } = context(options);
|
|
39
|
+
if (!fs.existsSync(file))
|
|
40
|
+
return;
|
|
41
|
+
if (fs.lstatSync(file).isSymbolicLink() || fs.readFileSync(file, "utf8") !== relayLoginPlist(launcher)) {
|
|
42
|
+
throw new SetupError("codex_steer_login_conflict", "変更されたLaunchAgentは削除しません");
|
|
43
|
+
}
|
|
44
|
+
if (run(["print", service]) === 0 && run(["bootout", service]) !== 0)
|
|
45
|
+
throw new SetupError("codex_steer_login_failed", "ログイン時のSteer設定を解除できません");
|
|
46
|
+
fs.unlinkSync(file);
|
|
47
|
+
}
|