@quolu/aishell 0.4.12 → 0.6.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/README.ja.md CHANGED
@@ -21,19 +21,17 @@ state/schema migration、診断、復旧、更新、releaseを単独で所有し
21
21
  [dotagents](https://github.com/kitepon/dotagents)は公開contractを使って製品横断wireと
22
22
  互換性を統合しますが、AIShellの内部運用を制御しません。
23
23
 
24
- AIShellは許可root、file identity、filesystem照合state、直接起動したprocess、完全log、artifactを所有する。reasoning、thread、compaction、sub-agent、汎用terminalはAI hostの責務として残す。
24
+ AIShellfile identity、filesystem照合state、直接起動したprocess、完全log、artifactを所有する。reasoning、thread、compaction、sub-agent、汎用terminalはAI hostの責務として残す。
25
25
 
26
26
  ## 30秒で試す
27
27
 
28
28
  Apple Silicon Mac、macOS 15以降が必要。
29
29
 
30
30
  ```sh
31
- npm install -g @quolu/aishell
32
- aishell-open
33
- codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
31
+ npm install -g @quolu/aishell && aishell-setup
34
32
  ```
35
33
 
36
- 管理アプリでAIに許可するfolderを追加し、新しいCodex taskで次のように頼む。
34
+ フォルダの事前登録は不要。新しいCodex taskで対象フォルダを指定して実行する。
37
35
 
38
36
  ```text
39
37
  初回workspace contextはworkspace_snapshotで取得して。focused testはrun_checkで実行し、
@@ -49,8 +47,8 @@ summaryから省略された証拠だけartifact_readで読んで。
49
47
  | `search_context` | 直接起動した`rg` workerによるbudget付き検索context |
50
48
  | `run_check` | 直接process実行、主要diagnostic、完全stdout/stderr artifact |
51
49
  | `artifact_read` | 保持artifactのrange、tail、pattern周辺read |
52
- | `runtime_status` | 未設定・停止中も含む許可root、停止、worktree、次操作の状態取得 |
53
- | `runtime_open_manager` | root追加またはAI操作再開のため管理アプリを開く |
50
+ | `runtime_status` | 停止状態、相対パスの基準、次操作の状態取得 |
51
+ | `runtime_open_manager` | AI操作の停止・再開のため管理アプリを開く |
54
52
 
55
53
  MCP serverへ`AISHELL_CAPABILITY_SET=expanded-v1`を設定すると、candidate surfaceへ明示opt-inできる。
56
54
  高密度development 9本と復旧control 2本を公開し、`run_observe`、`workspace_wait`、
@@ -60,7 +58,7 @@ semantic search、project profile、Git branch/worktree modeが加わる。
60
58
  Codexでは次のように登録する。
61
59
 
62
60
  ```sh
63
- codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
61
+ aishell-setup --ai codex
64
62
  ```
65
63
 
66
64
  未知値または空の`AISHELL_CAPABILITY_SET`と`AISHELL_TOOL_PROFILE`はtyped errorでstartup停止し、
@@ -79,7 +77,7 @@ statelessな連携では、モデルがworkspaceを何度もscanし、command出
79
77
  | Context | budget・cursor付きstructured result | stdoutを手動または暗黙に切り詰める |
80
78
  | Execution | executable URL、引数、cwd、lifecycleを分離 | shellが1本のcommand文字列を評価 |
81
79
  | Evidence | 完全stdout/stderrを期限付きhandleで保持 | response truncation時に証拠が失われやすい |
82
- | Scope | 人が管理する許可rootと明示的stop状態 | 周囲のshellとhost policyに依存 |
80
+ | Scope | macOSのアクセス権と明示的stop状態 | 周囲のshellとhost policyに依存 |
83
81
 
84
82
  AIShellはsandboxではなく、任意code実行を安全化しない。process railの目的はtyped executionと観測可能なlifecycleを維持することであり、改名binaryや許可workerが起動する子processを阻止することではない。
85
83
 
@@ -89,7 +87,7 @@ AIShellはsandboxではなく、任意code実行を安全化しない。process
89
87
  flowchart LR
90
88
  Host[AI host<br/>reasoning · threads · compaction] --> MCP[AIShellMCP<br/>MCP 2025-11-25]
91
89
  MCP --> Core[AIShellCore]
92
- Core --> State[Allowed roots · file identity<br/>FSEvents + reconciliation]
90
+ Core --> State[File identity<br/>FSEvents + reconciliation]
93
91
  Core --> Process[Direct process lifecycle<br/>stdout · stderr · timeout]
94
92
  Core --> Evidence[Retained evidence<br/>artifacts · freshness]
95
93
  Process --> Workers[git · rg · compiler · tests]
@@ -101,16 +99,16 @@ flowchart LR
101
99
 
102
100
  ## npmからinstall
103
101
 
104
- global packageは`aishell-mcp`と`aishell-open`を`PATH`へ追加する。`aishell-open`は同梱された管理アプリをLaunchServicesで開く。install scriptは実行しない。
102
+ global packageは`aishell-mcp`、`aishell-open`、`aishell-setup`を`PATH`へ追加する。npm install自体ではスクリプトも管理アプリも起動しない。
105
103
 
106
- upgradeは開いたままの管理アプリの足元でapp bundleを置き換えるため、その窓は壊れる。削除済みbundleを
107
- 掴み続け、ファイル選択を伴う操作を無言で失う。窓自身がこれを検知してbannerで示す。新版が同じpathに
108
- 在る場合はbannerの再起動1回で新版の窓へ移り、pathごと消えている場合は終了と`aishell-open`での開き直しを
109
- 促す。実際の`npm install -g` upgradeで検証済みである。
104
+ 対象AIのCLI(`claude`、`codex`、`grok`、Cursorの`agent`)を先に導入する。setupは各CLIからの読戻しも確認する。
105
+
106
+ `aishell-setup`は導入済みのClaude Code・Codex・Grok Build・Cursorを検出し、管理アプリ準備、MCP登録、設定の読戻し、実際のMCP操作まで確認する。登録はbare `aishell-mcp`+`AISHELL_CAPABILITY_SET=expanded-v1`。利用者のenv、PATH、他の設定を保持する。`--ai`で対象を指定でき、`--check`は設定やアプリ起動を変更せず診断する。Windows/LinuxとIntel Macは対象外。詳細は[製品単体の導入契約](https://github.com/kitepon/aishell/blob/main/docs/setup.md)を参照。
107
+
108
+ 更新後も同じ`aishell-setup`を実行する。旧管理アプリを正常終了して導入済みのアプリを開き、登録保持・読戻し・MCP実操作まで確認する。接続済みのMCPは、hostで再接続すると新版へ切り替わる。
110
109
 
111
110
  ```sh
112
- npm install -g @quolu/aishell
113
- aishell-open
111
+ npm install -g @quolu/aishell && aishell-setup
114
112
  ```
115
113
 
116
114
  現在の実験版はDeveloper ID署名・notarization前である。
@@ -127,16 +125,15 @@ open build/AIShell.app
127
125
 
128
126
  MCP実行ファイルは`build/AIShell.app/Contents/Helpers/aishell-mcp`へ同梱される。
129
127
 
130
- 管理アプリの「許可rootを追加」でAIに操作させるfolderを選ぶ。許可済みGit repositoryの`.git/worktrees`へ正式登録され、双方の管理情報が一致するworktreeは自動的に実効rootへ加わる。
128
+ フォルダ登録は不要。絶対パスは指定した場所を、相対パスと省略時はMCP起動ディレクトリを基準にする。Git worktreeも直接指定でき、旧設定の許可フォルダ一覧は無視される。
131
129
 
132
130
  ## 別のAI hostへ接続
133
131
 
134
132
  global npm install後は`PATH`上のcommand名とexpanded development surfaceを登録する。
135
133
 
136
134
  ```sh
137
- codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
138
- claude mcp add --scope user aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
139
- codex mcp get aishell
135
+ aishell-setup --ai claude,codex,grok,cursor
136
+ aishell-setup --check
140
137
  ```
141
138
 
142
139
  解除:
@@ -166,8 +163,8 @@ full profileにはfile一覧・read、SHA-256競合検出付きatomic update、c
166
163
 
167
164
  ## 現在の制限
168
165
 
169
- - stdio serverは1 requestずつ処理する。
170
- - MCP cancellationと並列run pollingは未実装。
166
+ - stdio requestは復旧操作・読み取り・実行の3系統で処理する。読み取りと復旧操作は長時間の実行中も応答するが、実行系requestは直列化する。
167
+ - MCPの`notifications/cancelled`を受け付ける。管理対象processの明示的な停止は`run_observe`の`cancel`で行う。
171
168
  - timeout時は直接所有するprocess treeを終了するが、終了までに許可workerがopen-worldな副作用を起こし得る。
172
169
  - 初回workspace entryはbounded previewで、後続deltaはcursor pageになる。
173
170
  - Developer ID署名とnotarizationは未設定。
@@ -177,11 +174,10 @@ full profileにはfile一覧・read、SHA-256競合検出付きatomic update、c
177
174
  単独installの更新は初回と同じ公式npm経路を使い、新版の管理アプリを開く。
178
175
 
179
176
  ```sh
180
- npm install -g @quolu/aishell@latest
181
- aishell-open
177
+ npm install -g @quolu/aishell@latest && aishell-setup
182
178
  ```
183
179
 
184
- 未設定・停止中の復旧入口は`runtime_status`と`runtime_open_manager`である。
180
+ 停止中の復旧入口は`runtime_status`と`runtime_open_manager`である。
185
181
  工場consumerは専用`AISHELL_TOOL_PROFILE=factory` MCP surfaceから
186
182
  `factory_diagnostics`を呼ぶ。schemaとprivacy境界は
187
183
  [製品側diagnostics contract](https://github.com/kitepon/aishell/blob/main/docs/factory-diagnostics.md)が正である。
@@ -194,13 +190,28 @@ npm test
194
190
  npm run test:package
195
191
  git fetch origin
196
192
  npm run verify:release-commit
197
- npm publish --access public
193
+ npm whoami
194
+ npm publish --access public --browser=false
198
195
  ```
199
196
 
200
197
  release gateはdirty treeと既定branchへ未着地のcommitを拒否する。publish後は対応する
201
198
  GitHub Releaseを作り、`@quolu/aishell@latest`を再installしてMCP initializeと
202
199
  `factory_diagnostics`をsmokeする。公開済みversionの正本はGitHub Releasesである。
203
200
 
201
+ ### 公開認証と導入確認
202
+
203
+ `npm whoami`が認証エラーを返した場合は、対話端末で`npm login --registry=https://registry.npmjs.org/ --browser=false`を実行し、表示された新しいURLを利用するブラウザで開いて認証する。公開コマンドも対話端末で実行し、出力をファイルへリダイレクトしない。公開用の認証URLが表示された場合は、ログインとは別に認証する。URLが失効した場合はコマンドを再実行して新しいURLを使う。
204
+
205
+ npmのログインsessionは2時間で失効し、公開時には二要素認証が適用される([npm公式説明](https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/))。公開の成功後に、registryとグローバルインストールを確認する。
206
+
207
+ ```sh
208
+ npm view @quolu/aishell dist-tags.latest
209
+ npm install -g @quolu/aishell@latest
210
+ npm ls -g @quolu/aishell --depth=0
211
+ ```
212
+
213
+ MCPを再接続し、`initialize`のversion、`runtime_status`、事前登録のない対象フォルダの検索と実行を確認する。工場診断は別processを`AISHELL_TOOL_PROFILE=factory`で起動し、`AISHELL_CAPABILITY_SET`を設定せずに確認する。
214
+
204
215
  ## 開発検証
205
216
 
206
217
  ```sh
package/README.md CHANGED
@@ -22,19 +22,17 @@ diagnostics, recovery, updates, and releases.
22
22
  for cross-product wiring and compatibility; it does not control AIShell's
23
23
  internal operation.
24
24
 
25
- AIShell owns the OS-facing state below the model: allowed roots, file identity, filesystem reconciliation, directly launched processes, complete logs, and retained artifacts. The AI host remains responsible for reasoning, threads, compaction, sub-agents, and general-purpose terminal work.
25
+ AIShell owns the OS-facing state below the model: file identity, filesystem reconciliation, directly launched processes, complete logs, and retained artifacts. The AI host remains responsible for reasoning, threads, compaction, sub-agents, and general-purpose terminal work.
26
26
 
27
27
  ## Try it in 30 seconds
28
28
 
29
29
  Requires an Apple Silicon Mac running macOS 15 or later.
30
30
 
31
31
  ```sh
32
- npm install -g @quolu/aishell
33
- aishell-open
34
- codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
32
+ npm install -g @quolu/aishell && aishell-setup
35
33
  ```
36
34
 
37
- In the manager app, add the folders the AI may access. Start a new Codex task and try:
35
+ フォルダの事前登録は不要です。新しいCodex taskで対象フォルダを指定して実行します。
38
36
 
39
37
  ```text
40
38
  Use workspace_snapshot for the initial workspace context. Run the focused tests with
@@ -50,15 +48,15 @@ The default profile exposes five high-density development tools plus two always-
50
48
  | `search_context` | Budgeted lexical context from a directly launched `rg` worker, scoped to a directory or one regular file; the expanded capability also provides cursor-bound semantic definition/reference/symbol queries without lexical fallback |
51
49
  | `run_check` | Direct process execution, primary diagnostics, and complete stdout/stderr artifacts |
52
50
  | `artifact_read` | Range, tail, and pattern-centered reads from retained artifacts; the expanded capability also searches and compares finalized managed-run artifacts |
53
- | `runtime_status` | Allowed-root, pause, worktree, and next-action state, including while paused or unconfigured |
54
- | `runtime_open_manager` | Open the manager app to add roots or resume AI operations |
51
+ | `runtime_status` | Pause, relative-path base, and next-action state |
52
+ | `runtime_open_manager` | Open the manager app to pause or resume AI operations |
55
53
 
56
54
  Set `AISHELL_CAPABILITY_SET=expanded-v1` on the MCP server process to opt in to the candidate surface. It exposes nine high-density development tools plus the two recovery controls. The added tools are `run_observe`, `workspace_wait`, `change_impact`, and `apply_change_set`; existing tools gain closed managed-run, artifact query, semantic search, project-profile, and Git branch/worktree modes. Cross-run artifact operations require an explicit project path and reject live, expired, legacy-unbound, or different-project evidence instead of silently falling back to partial logs.
57
55
 
58
56
  For Codex, register the expanded surface explicitly:
59
57
 
60
58
  ```sh
61
- codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
59
+ aishell-setup --ai codex
62
60
  ```
63
61
 
64
62
  Unknown or empty `AISHELL_CAPABILITY_SET` and `AISHELL_TOOL_PROFILE` values fail startup with typed errors; they never fall back to another profile.
@@ -77,7 +75,7 @@ Typical stateless integrations repeatedly ask the model to rediscover workspace
77
75
  | Context | Bounded, cursor-based structured results | Unbounded or manually truncated stdout |
78
76
  | Execution | Executable URL, arguments, working directory, and lifecycle remain separate | A shell evaluates one command string |
79
77
  | Evidence | Complete stdout/stderr retained behind expiring handles | Evidence often disappears when the response is truncated |
80
- | Scope | Human-managed allowed roots and explicit stop state | Depends on the surrounding shell and host policy |
78
+ | Scope | macOS access permissions and explicit stop state | Depends on the surrounding shell and host policy |
81
79
 
82
80
  AIShell is not a sandbox and does not make arbitrary code execution safe. Its process rails exist to preserve typed execution and observable lifecycle—not to stop renamed binaries or child processes launched by an allowed worker.
83
81
 
@@ -87,7 +85,7 @@ AIShell is not a sandbox and does not make arbitrary code execution safe. Its pr
87
85
  flowchart LR
88
86
  Host[AI host<br/>reasoning · threads · compaction] --> MCP[AIShellMCP<br/>MCP 2025-11-25]
89
87
  MCP --> Core[AIShellCore]
90
- Core --> State[Allowed roots · file identity<br/>FSEvents + reconciliation]
88
+ Core --> State[File identity<br/>FSEvents + reconciliation]
91
89
  Core --> Process[Direct process lifecycle<br/>stdout · stderr · timeout]
92
90
  Core --> Evidence[Retained evidence<br/>artifacts · freshness]
93
91
  Process --> Workers[git · rg · compiler · tests]
@@ -99,17 +97,16 @@ flowchart LR
99
97
 
100
98
  ## Install from npm
101
99
 
102
- The global package adds `aishell-mcp` and `aishell-open` to `PATH`. `aishell-open` opens the bundled manager app through LaunchServices. The package runs no install script.
100
+ global packageは`aishell-mcp`、`aishell-open`、`aishell-setup`を`PATH`へ追加する。npm install自体ではスクリプトも管理アプリも起動しない。
103
101
 
104
- Upgrading replaces the app bundle underneath any manager window left open, which breaks that window —
105
- it keeps holding a deleted bundle and silently loses every operation that opens a file panel. The
106
- window detects this itself and says so with a banner. When the new version sits at the same path, the
107
- banner restarts the window into it in one click; otherwise it asks for a quit and a fresh
108
- `aishell-open`. Verified against real `npm install -g` upgrades, not only simulated ones.
102
+ 対象AIのCLI(`claude`、`codex`、`grok`、Cursorの`agent`)を先に導入する。setupは各CLIからの読戻しも確認する。
103
+
104
+ `aishell-setup`は導入済みのClaude Code・Codex・Grok Build・Cursorを検出し、管理アプリ準備、MCP登録、設定の読戻し、実際のMCP操作まで確認する。登録はbare `aishell-mcp`+`AISHELL_CAPABILITY_SET=expanded-v1`。利用者のenv、PATH、他の設定を保持する。`--ai`で対象を指定でき、`--check`は設定やアプリ起動を変更せず診断する。Windows/LinuxとIntel Macは対象外。詳細は[製品単体の導入契約](https://github.com/kitepon/aishell/blob/main/docs/setup.md)を参照。
105
+
106
+ 更新後も同じ`aishell-setup`を実行する。旧管理アプリを正常終了して導入済みのアプリを開き、登録保持・読戻し・MCP実操作まで確認する。接続済みのMCPは、hostで再接続すると新版へ切り替わる。
109
107
 
110
108
  ```sh
111
- npm install -g @quolu/aishell
112
- aishell-open
109
+ npm install -g @quolu/aishell && aishell-setup
113
110
  ```
114
111
 
115
112
  The current experimental build is not yet Developer ID signed or notarized.
@@ -126,16 +123,15 @@ open build/AIShell.app
126
123
 
127
124
  The MCP executable is bundled at `build/AIShell.app/Contents/Helpers/aishell-mcp`.
128
125
 
129
- After opening the app, use **Add Allowed Root** to select the folders AIShell may access. A Git worktree registered under an allowed repository is recognized automatically when both sides of the worktree metadata agree.
126
+ フォルダ登録は不要です。絶対パスは指定した場所を、相対パスと省略時はMCP起動ディレクトリを基準にします。Git worktreeも直接指定できます。旧設定の許可フォルダ一覧は無視されます。
130
127
 
131
128
  ## Connect another AI host
132
129
 
133
130
  For a global npm installation, register the executable name from `PATH` and the expanded development surface:
134
131
 
135
132
  ```sh
136
- codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
137
- claude mcp add --scope user aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
138
- codex mcp get aishell
133
+ aishell-setup --ai claude,codex,grok,cursor
134
+ aishell-setup --check
139
135
  ```
140
136
 
141
137
  Remove the registration with:
@@ -167,8 +163,8 @@ The full profile includes file listing and reads, atomic SHA-256-guarded updates
167
163
 
168
164
  ## Current limitations
169
165
 
170
- - The stdio server handles one request at a time.
171
- - MCP cancellation and concurrent run polling are not implemented.
166
+ - stdio requestは復旧操作・読み取り・実行の3系統で処理する。読み取りと復旧操作は長時間の実行中も応答するが、実行系requestは直列化する。
167
+ - MCPの`notifications/cancelled`を受け付ける。管理対象processの明示的な停止は`run_observe`の`cancel`で行う。
172
168
  - A timeout terminates the directly owned process tree, but an allowed worker remains capable of open-world side effects before termination.
173
169
  - Initial workspace entries are a bounded preview; later deltas are cursor-paged.
174
170
  - Developer ID signing and notarization are not yet configured.
@@ -176,15 +172,14 @@ The full profile includes file listing and reads, atomic SHA-256-guarded updates
176
172
  ## Operations, updates, and releases
177
173
 
178
174
  Upgrade a standalone installation through the same official npm path used for
179
- initial installation, then open the newly installed manager:
175
+ initial installation, then run the explicit setup:
180
176
 
181
177
  ```sh
182
- npm install -g @quolu/aishell@latest
183
- aishell-open
178
+ npm install -g @quolu/aishell@latest && aishell-setup
184
179
  ```
185
180
 
186
- `runtime_status` and `runtime_open_manager` are the recovery entrypoints for an
187
- unconfigured or paused runtime. Factory consumers call `factory_diagnostics`
181
+ `runtime_status` and `runtime_open_manager` are the recovery entrypoints for a
182
+ paused runtime. Factory consumers call `factory_diagnostics`
188
183
  through the dedicated `AISHELL_TOOL_PROFILE=factory` MCP surface; its schema and
189
184
  privacy boundary are owned by [the product contract](https://github.com/kitepon/aishell/blob/main/docs/factory-diagnostics.md).
190
185
 
@@ -196,14 +191,29 @@ npm test
196
191
  npm run test:package
197
192
  git fetch origin
198
193
  npm run verify:release-commit
199
- npm publish --access public
194
+ npm whoami
195
+ npm publish --access public --browser=false
200
196
  ```
201
197
 
202
198
  The release gate rejects a dirty tree or a commit that has not landed on the
203
199
  default branch. After publishing, create the matching GitHub Release, reinstall
204
- `@quolu/aishell@latest`, and smoke MCP initialize plus `factory_diagnostics`.
200
+ `@quolu/aishell@latest`, run `aishell-setup` and `aishell-setup --check`, and separately smoke `factory_diagnostics`.
205
201
  GitHub Releases are the public record of shipped versions.
206
202
 
203
+ ### 公開認証と導入確認
204
+
205
+ `npm whoami`が認証エラーを返した場合は、対話端末で`npm login --registry=https://registry.npmjs.org/ --browser=false`を実行し、表示された新しいURLを利用するブラウザで開いて認証する。公開コマンドも対話端末で実行し、出力をファイルへリダイレクトしない。公開用の認証URLが表示された場合は、ログインとは別に認証する。URLが失効した場合はコマンドを再実行して新しいURLを使う。
206
+
207
+ npmのログインsessionは2時間で失効し、公開時には二要素認証が適用される([npm公式説明](https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/))。公開の成功後に、registryとグローバルインストールを確認する。
208
+
209
+ ```sh
210
+ npm view @quolu/aishell dist-tags.latest
211
+ npm install -g @quolu/aishell@latest
212
+ npm ls -g @quolu/aishell --depth=0
213
+ ```
214
+
215
+ MCPを再接続し、`initialize`のversion、`runtime_status`、事前登録のない対象フォルダの検索と実行を確認する。工場診断は別processを`AISHELL_TOOL_PROFILE=factory`で起動し、`AISHELL_CAPABILITY_SET`を設定せずに確認する。
216
+
207
217
  ## Development
208
218
 
209
219
  ```sh
@@ -17,9 +17,9 @@
17
17
  <key>CFBundlePackageType</key>
18
18
  <string>APPL</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>0.4.12</string>
20
+ <string>0.6.0</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>19</string>
22
+ <string>21</string>
23
23
  <key>LSApplicationCategoryType</key>
24
24
  <string>public.app-category.utilities</string>
25
25
  <key>LSMinimumSystemVersion</key>
@@ -10,19 +10,19 @@
10
10
  <dict>
11
11
  <key>cdhash</key>
12
12
  <data>
13
- uzkv8b/DrKQPJzoVL8NtTyIqqek=
13
+ UTk6ryOelBpibaQkQByLjOitg6Q=
14
14
  </data>
15
15
  <key>requirement</key>
16
- <string>cdhash H"bb392ff1bfc3aca40f273a152fc36d4f222aa9e9"</string>
16
+ <string>cdhash H"51393aaf239e941a626da424401c8b8ce8ad83a4"</string>
17
17
  </dict>
18
18
  <key>Helpers/aishell-run-supervisor</key>
19
19
  <dict>
20
20
  <key>cdhash</key>
21
21
  <data>
22
- 0V5WoY6tV3Zqijm+8J8Zv0B6/Pw=
22
+ JDIaGpdJLctZbPoDtu5xZihSiRM=
23
23
  </data>
24
24
  <key>requirement</key>
25
- <string>cdhash H"d15e56a18ead57766a8a39bef09f19bf407afcfc"</string>
25
+ <string>cdhash H"24321a1a97492dcb596cfa03b6ee716628528913"</string>
26
26
  </dict>
27
27
  </dict>
28
28
  <key>rules</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/aishell",
3
- "version": "0.4.12",
3
+ "version": "0.6.0",
4
4
  "description": "A native macOS state runtime with high-density MCP tools for AI development",
5
5
  "license": "Apache-2.0",
6
6
  "author": {
@@ -30,17 +30,25 @@
30
30
  ],
31
31
  "bin": {
32
32
  "aishell-mcp": "dist/AIShell.app/Contents/Helpers/aishell-mcp",
33
- "aishell-open": "scripts/aishell-open.mjs"
33
+ "aishell-open": "scripts/aishell-open.mjs",
34
+ "aishell-setup": "scripts/aishell-setup.mjs"
34
35
  },
35
36
  "files": [
36
37
  "dist/AIShell.app",
37
38
  "scripts/aishell-open.mjs",
39
+ "scripts/aishell-setup.mjs",
40
+ "scripts/setup/hosts.mjs",
41
+ "scripts/setup/host-cli.mjs",
42
+ "scripts/setup/setup.mjs",
43
+ "scripts/setup/mcp-client.mjs",
44
+ "scripts/setup/toml-registration.mjs",
38
45
  "README.md"
39
46
  ],
40
47
  "scripts": {
41
48
  "build:npm": "node scripts/prepare-npm-release.mjs",
42
49
  "prepack": "npm run build:npm",
43
- "test": "swift test && npm run test:release-gate && npm run test:repository-contract",
50
+ "test": "swift test && npm run test:setup && npm run test:release-gate && npm run test:repository-contract",
51
+ "test:setup": "node --test scripts/setup/*.test.mjs",
44
52
  "test:release-gate": "node --test scripts/verify-release-commit.test.mjs",
45
53
  "test:repository-contract": "node --test scripts/repository-contract.test.mjs",
46
54
  "test:package": "npm run build:npm && node scripts/verify-npm-package.mjs",
@@ -59,5 +67,9 @@
59
67
  },
60
68
  "publishConfig": {
61
69
  "access": "public"
70
+ },
71
+ "dependencies": {
72
+ "smol-toml": "1.8.0",
73
+ "toml-eslint-parser": "0.10.0"
62
74
  }
63
75
  }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { setup } from './setup/setup.mjs';
3
+ import { SetupError } from './setup/mcp-client.mjs';
4
+ import { aiNames } from './setup/hosts.mjs';
5
+
6
+ try {
7
+ const args = process.argv.slice(2);
8
+ if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
9
+ console.log('aishell-setup [--ai claude,codex,grok,cursor] [--check]\n初回・再実行・更新後に管理アプリ準備、MCP登録、読戻し、実操作を確認します。\n省略時は導入済みAIを検出。--checkは設定変更と管理アプリ起動を行いません。');
10
+ } else {
11
+ const options = {};
12
+ for (let index = 0; index < args.length; index++) {
13
+ if (args[index] === '--check' && options.check === undefined) options.check = true;
14
+ else if (args[index] === '--ai' && options.ais === undefined) {
15
+ options.ais = (args[++index] ?? '').split(',');
16
+ if (options.ais.some(ai => !aiNames.includes(ai)) || new Set(options.ais).size !== options.ais.length) throw new SetupError('AI_UNSUPPORTED', '--aiにはclaude,codex,grok,cursorを重複なしで指定してください。');
17
+ } else throw new SetupError('ARGUMENT_INVALID', '引数を確認してください。aishell-setup --helpで使い方を表示できます。');
18
+ }
19
+ console.log(JSON.stringify(await setup(options)));
20
+ }
21
+ } catch (error) {
22
+ console.error(JSON.stringify({ ...(error.report ?? { schemaVersion: 'aishell.setup.v1', status: 'failed' }), error: { code: error.code ?? 'SETUP_FAILED', message: error instanceof SetupError ? error.message : 'setup中の入出力処理に失敗しました。設定fileの権限と導入状態を確認してください。' } }));
23
+ process.exitCode = 1;
24
+ }
@@ -0,0 +1,63 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { lstat, readdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { backupFiles } from './hosts.mjs';
5
+ import { SetupError } from './mcp-client.mjs';
6
+
7
+ export function runHostCLI(command, args, options) {
8
+ try { return execFileSync(command, args, { ...options, encoding: 'utf8', timeout: 45000, stdio: ['ignore', 'pipe', 'pipe'] }); }
9
+ catch (error) {
10
+ throw new SetupError(error.code === 'ENOENT' ? 'AI_CLI_NOT_FOUND' : 'AI_VERIFICATION_FAILED', `${command}によるMCP確認に失敗しました。AI本体の導入・接続・承認状態を確認してください。`);
11
+ }
12
+ }
13
+
14
+ async function present(file) {
15
+ try {
16
+ const info = await lstat(file);
17
+ if (!info.isFile()) throw new SetupError('CONFIG_PATH_UNSUPPORTED', 'Cursorの設定は通常fileである必要があります。');
18
+ return true;
19
+ } catch (error) { if (error.code === 'ENOENT') return false; throw error; }
20
+ }
21
+
22
+ async function backupCursor(spec, backupDirectory) {
23
+ const files = [];
24
+ const config = path.join(spec.base, 'cli-config.json');
25
+ if (await present(config)) files.push(config);
26
+ const directory = path.join(spec.base, 'projects');
27
+ let entries;
28
+ try { entries = await readdir(directory, { withFileTypes: true }); }
29
+ catch (error) { if (error.code !== 'ENOENT') throw error; entries = []; }
30
+ for (const entry of entries) if (entry.isDirectory()) {
31
+ const file = path.join(directory, entry.name, 'mcp-approvals.json');
32
+ if (await present(file)) files.push(file);
33
+ }
34
+ await backupFiles(files, backupDirectory, 'cursor-approval');
35
+ }
36
+
37
+ export async function verifyHostCLI(spec, registration, { check, env, cwd, backupDirectory, run = runHostCLI }) {
38
+ const options = { env: { ...env, NO_COLOR: '1', TERM: 'dumb' }, cwd };
39
+ let actual;
40
+ if (spec.ai === 'codex') {
41
+ const value = JSON.parse(run('codex', ['mcp', 'get', 'aishell', '--json'], options));
42
+ if (value.enabled !== true || value.transport?.type !== 'stdio') throw new SetupError('AI_REGISTRATION_MISMATCH', 'Codexの実効登録が有効なstdioではありません。');
43
+ actual = value.transport;
44
+ } else if (spec.ai === 'grok') {
45
+ const values = JSON.parse(run('grok', ['mcp', 'list', '--json'], options)).filter(item => item.name === 'aishell');
46
+ if (values.length !== 1 || values[0].enabled !== true) throw new SetupError('AI_REGISTRATION_MISMATCH', 'Grokの実効登録を一意に確認できません。');
47
+ actual = values[0];
48
+ } else if (spec.ai === 'claude') {
49
+ const output = run('claude', ['mcp', 'get', 'aishell'], options);
50
+ if (!/^\s*Command: aishell-mcp\s*$/m.test(output) || !output.includes('expanded-v1') || !/Status:.*Connected/.test(output)) throw new SetupError('AI_REGISTRATION_MISMATCH', 'Claudeの実効登録またはMCP接続が一致しません。');
51
+ } else {
52
+ if (!check) {
53
+ await backupCursor(spec, backupDirectory);
54
+ run('agent', ['mcp', 'enable', 'aishell'], options);
55
+ } else if (!await present(path.join(spec.base, 'cli-config.json'))) {
56
+ throw new SetupError('AI_VERIFICATION_FAILED', 'Cursor CLIが未準備です。aishell-setupを実行してください。');
57
+ }
58
+ const output = run('agent', ['mcp', 'list-tools', 'aishell'], options);
59
+ if (!['runtime_status', 'workspace_snapshot', 'apply_change_set'].every(name => output.includes(name))) throw new SetupError('AI_REGISTRATION_MISMATCH', 'Cursorからexpanded-v1のtoolを確認できません。');
60
+ }
61
+ if (actual && (actual.command !== registration.command || (actual.args ?? []).length !== 0 || actual.env?.AISHELL_CAPABILITY_SET !== 'expanded-v1')) throw new SetupError('AI_REGISTRATION_MISMATCH', `${spec.ai}の実効登録が製品登録と一致しません。project設定の上書きも確認してください。`);
62
+ return { hostVerified: true };
63
+ }
@@ -0,0 +1,115 @@
1
+ import { readFile, lstat, mkdir, writeFile, rename, rm } from 'node:fs/promises';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import path from 'node:path';
5
+ import { isDeepStrictEqual } from 'node:util';
6
+ import { parse } from 'smol-toml';
7
+ import { replaceTOMLRegistration, replaceTopLevelValue } from './toml-registration.mjs';
8
+ import { SetupError } from './mcp-client.mjs';
9
+
10
+ export const aiNames = ['claude', 'codex', 'grok', 'cursor'];
11
+ export function hostSpec(ai, home, env) {
12
+ if (ai === 'cursor' && env.CURSOR_HOME && path.resolve(env.CURSOR_HOME) !== path.join(home, '.cursor')) {
13
+ throw new SetupError('AI_CONFIG_LOCATION_UNSUPPORTED', 'CursorはCURSOR_HOMEを読みません。通常の~/.cursorを使う環境でsetupを実行してください。');
14
+ }
15
+ const bases = {
16
+ claude: env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'),
17
+ codex: env.CODEX_HOME || path.join(home, '.codex'),
18
+ grok: env.GROK_HOME || path.join(home, '.grok'),
19
+ cursor: path.join(home, '.cursor'),
20
+ };
21
+ if (!aiNames.includes(ai)) throw new SetupError('AI_UNSUPPORTED', `未対応のAIです: ${ai}`);
22
+ const base = path.resolve(bases[ai]);
23
+ return {
24
+ ai, base,
25
+ file: ai === 'claude' ? (env.CLAUDE_CONFIG_DIR ? path.join(base, '.claude.json') : path.join(home, '.claude.json'))
26
+ : path.join(base, ai === 'cursor' ? 'mcp.json' : 'config.toml'),
27
+ format: ['codex', 'grok'].includes(ai) ? 'toml' : 'json',
28
+ key: ['codex', 'grok'].includes(ai) ? 'mcp_servers' : 'mcpServers',
29
+ };
30
+ }
31
+
32
+ function object(value, label) {
33
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new SetupError('CONFIG_INVALID', `${label}はobjectである必要があります。`);
34
+ return value;
35
+ }
36
+
37
+ export function canonicalRegistration(current = {}, ai) {
38
+ object(current, 'AIShell設定');
39
+ const next = { ...current, command: 'aishell-mcp', args: [], env: { ...object(current.env ?? {}, 'AIShell env'), AISHELL_CAPABILITY_SET: 'expanded-v1' } };
40
+ if (Object.values(next.env).some(value => typeof value !== 'string')) throw new SetupError('CONFIG_INVALID', 'AIShell envの値は文字列である必要があります。');
41
+ // transportの旧値はAIShellが所有する。利用者のtimeout・許可tool等は保持する。
42
+ for (const key of ['url', 'http_headers', 'env_http_headers', 'bearer_token_env_var', 'headers']) delete next[key];
43
+ if (ai === 'claude' || ai === 'cursor') next.type = 'stdio';
44
+ else { delete next.type; next.enabled = true; }
45
+ if ('enabled' in next) next.enabled = true;
46
+ if ('disabled' in next) next.disabled = false;
47
+ return next;
48
+ }
49
+
50
+ export function decode(text, spec) {
51
+ try { return object(text.trim() ? (spec.format === 'toml' ? parse(text) : JSON.parse(text)) : {}, 'AI設定'); }
52
+ catch (error) {
53
+ if (error instanceof SetupError) throw error;
54
+ throw new SetupError('CONFIG_INVALID', `${spec.ai}の設定を解析できません。`);
55
+ }
56
+ }
57
+
58
+ export async function readHost(spec) {
59
+ let info;
60
+ try { info = await lstat(spec.file); }
61
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
62
+ if (info && !info.isFile()) throw new SetupError('CONFIG_PATH_UNSUPPORTED', `${spec.ai}の設定は通常fileである必要があります。`);
63
+ const text = info ? await readFile(spec.file, 'utf8') : '';
64
+ const data = decode(text, spec);
65
+ const servers = object(data[spec.key] ?? {}, `${spec.ai} MCP設定`);
66
+ return { text, data, info, registration: servers.aishell };
67
+ }
68
+
69
+ export async function planHost(spec) {
70
+ const before = await readHost(spec);
71
+ const registration = canonicalRegistration(before.registration, spec.ai);
72
+ const data = { ...before.data, [spec.key]: { ...before.data[spec.key], aishell: registration } };
73
+ if (spec.ai === 'grok' && data.disabled_mcp_servers !== undefined) {
74
+ if (!Array.isArray(data.disabled_mcp_servers) || data.disabled_mcp_servers.some(name => typeof name !== 'string')) throw new SetupError('CONFIG_INVALID', 'Grokのdisabled_mcp_serversは文字列配列である必要があります。');
75
+ data.disabled_mcp_servers = data.disabled_mcp_servers.filter(name => name !== 'aishell');
76
+ }
77
+ const changed = !isDeepStrictEqual(before.data, data);
78
+ let text;
79
+ try { text = changed ? (spec.format === 'toml' ? replaceTOMLRegistration(before.text, registration) : JSON.stringify(data, null, 2) + '\n') : before.text; }
80
+ catch { throw new SetupError('CONFIG_INVALID', `${spec.ai}の設定形式を保持して更新できません。`); }
81
+ if (!isDeepStrictEqual(before.data.disabled_mcp_servers, data.disabled_mcp_servers)) text = replaceTopLevelValue(text, 'disabled_mcp_servers', data.disabled_mcp_servers);
82
+ // serializeで他の設定値が変わる形式は、書込み前に明示拒否する。
83
+ if (!isDeepStrictEqual(decode(text, spec), data)) throw new SetupError('CONFIG_SERIALIZATION_LOSS', `${spec.ai}の設定値を保持できません。`);
84
+ return { spec, before, text, data, registration, changed };
85
+ }
86
+
87
+ export async function backupFiles(files, backupDirectory, label) {
88
+ if (!files.length) return;
89
+ await mkdir(backupDirectory, { recursive: true, mode: 0o700 });
90
+ // 設定の原本をtarへ保存してから更新する。秘密を含み得るため作成時から0600に限定する。
91
+ const archive = path.join(backupDirectory, `${label}-${randomUUID()}.tar`);
92
+ await writeFile(archive, '', { flag: 'wx', mode: 0o600 });
93
+ execFileSync('/usr/bin/tar', ['-cf', archive, '-C', '/', ...files.map(file => file.slice(1))], { stdio: 'pipe' });
94
+ }
95
+
96
+ export async function writeHost(plan, backupDirectory) {
97
+ if (!plan.changed) return;
98
+ const { spec, before, text } = plan;
99
+ if (before.info) await backupFiles([spec.file], backupDirectory, spec.ai);
100
+ await mkdir(path.dirname(spec.file), { recursive: true, mode: 0o700 });
101
+ const temporary = path.join(path.dirname(spec.file), `.aishell-${randomUUID()}.tmp`);
102
+ try {
103
+ await writeFile(temporary, text, { flag: 'wx', mode: before.info ? before.info.mode & 0o777 : 0o600 });
104
+ const current = await readHost(spec);
105
+ if (current.text !== before.text || Boolean(current.info) !== Boolean(before.info)) throw new SetupError('CONFIG_CHANGED', `${spec.ai}の設定が同時に更新されました。再実行してください。`);
106
+ await rename(temporary, spec.file);
107
+ } finally { await rm(temporary, { force: true }); }
108
+ }
109
+
110
+ export async function verifyHost(plan) {
111
+ const actual = await readHost(plan.spec);
112
+ if (!isDeepStrictEqual(actual.registration, plan.registration)) throw new SetupError('REGISTRATION_MISMATCH', `${plan.spec.ai}の登録読戻しが一致しません。`);
113
+ if (!isDeepStrictEqual(actual.data, plan.data)) throw new SetupError('CONFIG_CHANGED', `${plan.spec.ai}の他設定が変更されました。`);
114
+ return actual.registration;
115
+ }
@@ -0,0 +1,64 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+
4
+ export class SetupError extends Error {
5
+ constructor(code, message) { super(message); this.code = code; }
6
+ }
7
+
8
+ // 読戻した登録をそのまま起動する。env値と子process出力は診断へ転載しない。
9
+ export async function withMCP(registration, operation, { cwd, env = process.env, timeout = 30000 } = {}) {
10
+ const child = spawn(registration.command, registration.args ?? [], {
11
+ cwd, env: { ...env, ...registration.env }, stdio: ['pipe', 'pipe', 'pipe'],
12
+ });
13
+ const pending = new Map();
14
+ let sequence = 0;
15
+ let failure;
16
+ const fail = (error) => {
17
+ failure = error;
18
+ for (const item of pending.values()) item.reject(error);
19
+ pending.clear();
20
+ };
21
+ child.on('error', () => fail(new SetupError('MCP_START_FAILED', '登録されたMCPを起動できません。PATHと導入状態を確認してください。')));
22
+ child.stdin.on('error', () => fail(new SetupError('MCP_CLOSED', 'MCPの入力が閉じました。')));
23
+ child.stderr.resume();
24
+ const closed = new Promise(resolve => child.once('close', resolve));
25
+ child.once('close', () => fail(new SetupError('MCP_CLOSED', 'MCPが応答完了前に終了しました。')));
26
+ const lines = createInterface({ input: child.stdout });
27
+ lines.on('line', line => {
28
+ let response;
29
+ try { response = JSON.parse(line); }
30
+ catch { fail(new SetupError('MCP_PROTOCOL_INVALID', 'MCPがJSON以外を返しました。')); return; }
31
+ const item = pending.get(response.id);
32
+ if (!item) return;
33
+ pending.delete(response.id);
34
+ if (response.error || response.result?.isError) item.reject(new SetupError('MCP_OPERATION_FAILED', `${item.method}が失敗しました。`));
35
+ else item.resolve(response.result);
36
+ });
37
+ const timer = setTimeout(() => {
38
+ fail(new SetupError('MCP_TIMEOUT', 'MCP応答が制限時間を超えました。'));
39
+ child.kill('SIGTERM');
40
+ }, timeout);
41
+ const request = (method, params) => new Promise((resolve, reject) => {
42
+ if (failure) { reject(failure); return; }
43
+ const id = ++sequence;
44
+ pending.set(id, { resolve, reject, method });
45
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
46
+ });
47
+ try {
48
+ const initialized = await request('initialize', {
49
+ protocolVersion: '2025-11-25', capabilities: {},
50
+ clientInfo: { name: 'aishell-setup', version: '1' },
51
+ });
52
+ if (initialized?.protocolVersion !== '2025-11-25') throw new SetupError('MCP_PROTOCOL_INVALID', 'MCP protocol versionが一致しません。');
53
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
54
+ return await operation({ initialized, request, call: (name, args = {}) => request('tools/call', { name, arguments: args }) });
55
+ } finally {
56
+ clearTimeout(timer);
57
+ child.stdin.end();
58
+ const killTimer = setTimeout(() => child.kill('SIGKILL'), 1000);
59
+ child.kill('SIGTERM');
60
+ await closed;
61
+ clearTimeout(killTimer);
62
+ lines.close();
63
+ }
64
+ }
@@ -0,0 +1,104 @@
1
+ import { access, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { aiNames, hostSpec, planHost, verifyHost, writeHost } from './hosts.mjs';
8
+ import { SetupError, withMCP } from './mcp-client.mjs';
9
+ import { verifyHostCLI } from './host-cli.mjs';
10
+
11
+ export const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
12
+
13
+ export function supportedPlatform(platform, arch, macVersion) {
14
+ if (platform !== 'darwin' || arch !== 'arm64' || !/^\d+(\.\d+)*$/.test(macVersion ?? '') || Number(macVersion.split('.')[0]) < 15) {
15
+ throw new SetupError('PLATFORM_UNSUPPORTED', '対応環境はmacOS 15以降のApple Silicon(arm64)だけです。設定・processは作成しません。');
16
+ }
17
+ }
18
+
19
+ async function exists(file, mode) {
20
+ try { await access(file, mode); return true; }
21
+ catch (error) { if (['ENOENT', 'EACCES'].includes(error.code)) return false; throw error; }
22
+ }
23
+
24
+ async function installed(spec, env) {
25
+ if (await exists(spec.base) || await exists(spec.file)) return true;
26
+ const commands = spec.ai === 'cursor' ? ['cursor', 'agent'] : [spec.ai];
27
+ for (const directory of (env.PATH ?? '').split(path.delimiter)) {
28
+ if (!directory) continue;
29
+ for (const command of commands) if (await exists(path.join(directory, command), constants.X_OK)) return true;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ export function prepareManager() {
35
+ const binary = path.join(packageDirectory, 'dist/AIShell.app/Contents/Helpers/aishell-mcp');
36
+ let value;
37
+ try { value = JSON.parse(execFileSync(binary, ['--prepare-manager'], { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] })); }
38
+ catch { throw new SetupError('MANAGER_PREPARATION_FAILED', '管理アプリの準備に失敗しました。aishell-openで状態を確認してください。'); }
39
+ if (!Number.isInteger(value.processIdentifier) || value.processIdentifier <= 0) throw new SetupError('MANAGER_PREPARATION_FAILED', '管理アプリの実行確認が返りませんでした。');
40
+ return { ready: true, processIdentifier: value.processIdentifier };
41
+ }
42
+
43
+ export async function smoke(registration, expectedVersion, { env = process.env } = {}) {
44
+ const directory = await mkdtemp(path.join(tmpdir(), 'aishell-setup-smoke-'));
45
+ try {
46
+ await writeFile(path.join(directory, 'probe.txt'), 'AIShell setup smoke\n');
47
+ return await withMCP(registration, async ({ initialized, request, call }) => {
48
+ if (initialized?.serverInfo?.version !== expectedVersion) throw new SetupError('MCP_VERSION_MISMATCH', '登録先のMCP版が導入済みpackageと一致しません。PATHを確認してください。');
49
+ const list = await request('tools/list', {});
50
+ if (!list?.tools?.some(tool => tool.name === 'workspace_snapshot') || !list.tools.some(tool => tool.name === 'apply_change_set')) throw new SetupError('MCP_CAPABILITY_MISMATCH', 'expanded-v1の開発toolを利用できません。AISHELL_TOOL_PROFILEなどのenvを確認してください。');
51
+ const status = (await call('runtime_status')).structuredContent;
52
+ if (status?.isPaused !== false) throw new SetupError('RUNTIME_NOT_READY', 'AIShellが停止中か、状態を確認できません。管理アプリで再開してから再実行してください。');
53
+ const snapshot = (await call('workspace_snapshot', { path: directory, context_budget: 0, entry_limit: 10, project_profile: { mode: 'none' } })).structuredContent;
54
+ if (snapshot?.freshness !== 'fresh' || !snapshot.entries?.some(entry => entry.path === 'probe.txt' || entry.relativePath === 'probe.txt' || entry.path === path.join(directory, 'probe.txt'))) throw new SetupError('MCP_SMOKE_FAILED', '未登録フォルダの実fileをMCPで確認できません。');
55
+ return { version: initialized.serverInfo.version, toolCount: list.tools.length, operation: 'workspace_snapshot', ready: true };
56
+ }, { cwd: directory, env });
57
+ } finally { await rm(directory, { recursive: true, force: true }); }
58
+ }
59
+
60
+ export async function setup(options = {}, dependencies = {}) {
61
+ const env = dependencies.env ?? process.env;
62
+ const platform = dependencies.platform ?? process.platform;
63
+ const arch = dependencies.arch ?? process.arch;
64
+ // 対応外OSではmacOS commandを含めて一切の準備処理を実行しない。
65
+ const macVersion = platform === 'darwin' ? (dependencies.macVersion ?? execFileSync('/usr/bin/sw_vers', ['-productVersion'], { encoding: 'utf8' }).trim()) : null;
66
+ supportedPlatform(platform, arch, macVersion);
67
+ const home = dependencies.home ?? homedir();
68
+ const chosen = options.ais ? options.ais.map(ai => hostSpec(ai, home, env)) : [];
69
+ if (!options.ais) for (const ai of aiNames) {
70
+ const spec = hostSpec(ai, home, { ...env, CURSOR_HOME: undefined });
71
+ if (await installed(spec, env)) chosen.push(hostSpec(ai, home, env));
72
+ }
73
+ if (!chosen.length) throw new SetupError('AI_NOT_FOUND', '対応AIを検出できません。対象を--ai claude,codex,grok,cursorで指定してください。');
74
+ const report = { schemaVersion: 'aishell.setup.v1', mode: options.check ? 'diagnose' : 'setup', platform: { os: platform, arch, version: macVersion }, manager: null, hosts: [], skipped: aiNames.filter(ai => !chosen.some(spec => spec.ai === ai)) };
75
+ const version = dependencies.version ?? JSON.parse(await (await import('node:fs/promises')).readFile(path.join(packageDirectory, 'package.json'), 'utf8')).version;
76
+ const backupDirectory = path.join(home, 'Library/Application Support/AIShell/setup-backups');
77
+ let stage = 'preflight';
78
+ try {
79
+ // 全対象を先に解析する。壊れた設定があるままappや別AIを更新しない。
80
+ const plans = [];
81
+ for (const spec of chosen) plans.push(await planHost(spec));
82
+ if (!options.check) {
83
+ stage = 'prepare';
84
+ report.manager = await (dependencies.prepare ?? prepareManager)();
85
+ }
86
+ for (const plan of plans) {
87
+ stage = `register:${plan.spec.ai}`;
88
+ if (!options.check) await (dependencies.write ?? writeHost)(plan, backupDirectory);
89
+ else if (plan.changed) throw new SetupError('REGISTRATION_MISMATCH', `${plan.spec.ai}のAIShell登録が正規契約と一致しません。setupを実行してください。`);
90
+ const registration = await verifyHost(plan);
91
+ const host = { ai: plan.spec.ai, registration: options.check ? 'verified' : plan.changed ? 'updated' : 'unchanged', ready: false };
92
+ report.hosts.push(host);
93
+ stage = `host:${plan.spec.ai}`;
94
+ Object.assign(host, await (dependencies.verifyCLI ?? verifyHostCLI)(plan.spec, registration, { check: options.check, env, cwd: dependencies.cwd ?? process.cwd(), backupDirectory }));
95
+ stage = `smoke:${plan.spec.ai}`;
96
+ const result = await (dependencies.smoke ?? smoke)(registration, version, { env });
97
+ Object.assign(host, result);
98
+ }
99
+ return { ...report, status: 'ready' };
100
+ } catch (error) {
101
+ error.report = { ...report, status: 'failed', stage };
102
+ throw error;
103
+ }
104
+ }
@@ -0,0 +1,53 @@
1
+ import { parseForESLint } from 'toml-eslint-parser';
2
+ import { stringify } from 'smol-toml';
3
+
4
+ const target = ['mcp_servers', 'aishell'];
5
+ const prefix = (left, right) => left.every((key, index) => right[index] === key);
6
+ const keyPath = node => node.keys.map(key => key.type === 'TOMLBare' ? key.name : key.value);
7
+
8
+ export function replaceTopLevelValue(text, key, value) {
9
+ const { ast } = parseForESLint(text);
10
+ const node = ast.body[0].body.find(node => node.type === 'TOMLKeyValue' && keyPath(node.key).length === 1 && keyPath(node.key)[0] === key);
11
+ if (!node) throw new Error('トップレベルの設定値を特定できません。');
12
+ return text.slice(0, node.value.range[0]) + inline(value) + text.slice(node.value.range[1]);
13
+ }
14
+
15
+ function inline(value) {
16
+ if (Array.isArray(value)) return `[${value.map(inline).join(', ')}]`;
17
+ if (value && typeof value === 'object' && !(value instanceof Date)) return `{ ${Object.entries(value).map(([key, item]) => `${JSON.stringify(key)} = ${inline(item)}`).join(', ')} }`;
18
+ return stringify({ value }).slice('value = '.length).trim();
19
+ }
20
+
21
+ // 構文木の範囲だけを変更し、別server、コメント、float/date表記をそのまま残す。
22
+ // 複数行文字列にある偽のtable headerを正規表現で誤認しない。
23
+ export function replaceTOMLRegistration(text, registration) {
24
+ const { ast } = parseForESLint(text);
25
+ const edits = [];
26
+ let replaced = false;
27
+ function visitKeyValue(node, base) {
28
+ const keys = [...base, ...keyPath(node.key)];
29
+ if (keys.length === target.length && prefix(target, keys)) {
30
+ edits.push({ range: node.value.range, text: inline(registration) });
31
+ replaced = true;
32
+ } else if (prefix(target, keys)) {
33
+ edits.push({ range: node.range, text: '' });
34
+ } else if (prefix(keys, target) && node.value.type === 'TOMLInlineTable') {
35
+ for (const child of node.value.body) visitKeyValue(child, keys);
36
+ if (!replaced) {
37
+ const at = node.value.range[1] - 1;
38
+ edits.push({ range: [at, at], text: `${node.value.body.length ? ', ' : ''}${JSON.stringify(target.at(-1))} = ${inline(registration)}` });
39
+ replaced = true;
40
+ }
41
+ }
42
+ }
43
+ for (const node of ast.body[0].body) {
44
+ if (node.type === 'TOMLTable') {
45
+ if (prefix(target, node.resolvedKey)) edits.push({ range: node.range, text: '' });
46
+ else for (const child of node.body) visitKeyValue(child, node.resolvedKey);
47
+ } else visitKeyValue(node, []);
48
+ }
49
+ let result = text;
50
+ for (const edit of edits.sort((a, b) => b.range[0] - a.range[0])) result = result.slice(0, edit.range[0]) + edit.text + result.slice(edit.range[1]);
51
+ if (!replaced) result += `\n${stringify({ mcp_servers: { aishell: registration } })}`;
52
+ return result;
53
+ }