omnilane 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ semantic version tags.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.3] - 2026-07-18
10
+
11
+ ### Added
12
+
13
+ - Dependency-free MCP stdio server (`omnilane mcp`) exposing routing, lane
14
+ discovery, and background-job status/results to any MCP-capable host.
15
+
9
16
  ## [0.8.2] - 2026-07-18
10
17
 
11
18
  ### Added
@@ -252,7 +259,8 @@ semantic version tags.
252
259
  - Initial shared routing table, cross-vendor dispatcher, runners, installer,
253
260
  and baseline lint fixes.
254
261
 
255
- [Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.8.2...HEAD
262
+ [Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.8.3...HEAD
263
+ [0.8.3]: https://github.com/Seraphim0916/omnilane/compare/v0.8.2...v0.8.3
256
264
  [0.8.2]: https://github.com/Seraphim0916/omnilane/compare/v0.8.1...v0.8.2
257
265
 
258
266
  [0.8.1]: https://github.com/Seraphim0916/omnilane/compare/v0.8.0...v0.8.1
package/README.ja.md CHANGED
@@ -5,8 +5,9 @@
5
5
  ### ルーティングテーブルは一枚、ハーネスは全部。
6
6
 
7
7
  *メインループはもう、どのモデルを使うか迷わない。*<br/>
8
- すべてのサブタスクを、その作業が本当に得意なモデルへ——<br/>
9
- **Claude Code · Codex · Grok Build · Antigravity** を横断、いまのサブスクリプションのままで。
8
+ **Claude Code · Codex · Grok Build · Antigravity** のどれから操縦しても、すべてのサブタスクを<br/>
9
+ その作業が本当に得意なモデルへ——Codex、ClaudeGrok、Gemini、Kimi、Qwen、OpenCode、<br/>
10
+ さらに OpenRouter 経由の任意のホスト型モデル——いまのサブスクリプションのまま、または API キー一つで。
10
11
 
11
12
  <img src="docs/hero.ja.png" alt="omnilane が各サブタスクを Claude Code・Codex・Grok・Antigravity の最適なモデルへ振り分ける" width="820"/>
12
13
 
@@ -20,6 +21,14 @@
20
21
 
21
22
  ---
22
23
 
24
+ ## v0.8.3 の新機能
25
+
26
+ - **MCP サーバー** — `omnilane mcp` は依存ゼロの stdio MCP サーバーを起動し、
27
+ MCP 対応ホスト(Claude Code、Codex、Gemini CLI、Cursor、OpenCode など)が
28
+ スキルなしで omnilane を発見・呼び出しできます:ツールは `route`、
29
+ `jobs_status`、`jobs_result`、`list_lanes`。`route` は読み取り専用の
30
+ advise がデフォルトで、work モードには明示的な workdir が必須です。
31
+
23
32
  ## v0.8.2 の新機能
24
33
 
25
34
  - **`openrouter` vendor** — `curl` と `OPENROUTER_API_KEY` だけで
@@ -114,8 +123,9 @@ omnilane ui start # 任意:ブラウザでジョブをライブ表示
114
123
  ## 🧭 仕組み
115
124
 
116
125
  omnilane は、**どの** agentic CLI のメインループでも、サブタスクをレーンに
117
- 分類し、各レーンをその作業が最も得意なベンダー CLI へヘッドレスで
118
- ディスパッチさせる仕組みです。既存のサブスクリプションログインをそのまま使います:
126
+ 分類し、各レーンをその作業が最も得意なベンダーへヘッドレスで
127
+ ディスパッチさせる仕組みです。既存のサブスクリプションログインをそのまま
128
+ 使います(`openrouter` vendor は例外:CLI 不要、API キー一つで直接接続):
119
129
 
120
130
  ```mermaid
121
131
  flowchart LR
@@ -243,9 +253,11 @@ omnilane ui stop # 正常に停止
243
253
 
244
254
  ## 📦 インストール
245
255
 
246
- 前提:ルーティングしたいベンダー CLI(`codex`、`claude`、`grok`、`agy`)が
247
- ログイン済みで `PATH` 上にあること——**持っている分だけで OK**、
248
- 足りないレーンは自動的に降格します。
256
+ 前提:ルーティングしたいベンダー CLI(`codex`、`claude`、`grok`、`agy`、
257
+ 任意で `kimi`、`qwen`、`opencode`)がログイン済みで `PATH` 上にあること——
258
+ **持っている分だけで OK**、足りないレーンは自動的に降格します。
259
+ `openrouter` vendor は例外で、CLI は不要——`curl` と環境変数の
260
+ `OPENROUTER_API_KEY` だけで動きます。
249
261
 
250
262
  最速:`./install.sh` — 本機の CLI を検出してスキルを接続し、残りのプラグイン
251
263
  コマンドを表示、実効ルーティングを出力し、最後に対話式設定メニューを
@@ -271,6 +283,28 @@ CLI バージョンにより異なる場合あり)へマーカー付きの可逆
271
283
  - **Antigravity**:`agy plugin install <このリポジトリ>`(先に
272
284
  `agy plugin validate` で確認)
273
285
 
286
+ ### MCP サーバー
287
+
288
+ `omnilane mcp` は依存ゼロでローカル実行される MCP stdio サーバーを起動し、
289
+ MCP 対応ホストがスキルの導入やルーティングリマインダーなしで omnilane を
290
+ 発見・呼び出しできるようにします。ホスト側の設定でインストール済み CLI を
291
+ 指定してください:
292
+
293
+ ```json
294
+ {
295
+ "mcpServers": {
296
+ "omnilane": {
297
+ "command": "omnilane",
298
+ "args": ["mcp"]
299
+ }
300
+ }
301
+ }
302
+ ```
303
+
304
+ サーバーは `route`、`jobs_status`、`jobs_result`、`list_lanes` を公開します。
305
+ `route` のデフォルトは読み取り専用の `advise` モードで、`work` を選ぶ呼び
306
+ 出しには明示的な `workdir` が必須です。
307
+
274
308
  ## ⚙️ カスタマイズ
275
309
 
276
310
  三層、すべて任意:
@@ -325,9 +359,13 @@ configure.sh # 対話式レーンメニ
325
359
  ## 🎭 モード
326
360
 
327
361
  - **advise(デフォルト)** — 読み取り専用ワーカー。Codex は read-only
328
- サンドボックス、Claude は Read/Glob/Grep のみ、Grok は plan モード。
362
+ サンドボックス、Claude は Read/Glob/Grep のみ、Grok は plan モード、
363
+ Kimi と OpenCode はそれぞれの読み取り専用 plan モードに固定、
364
+ OpenRouter は設計上 advise 専用(純推論)。
329
365
  - **work** — 指定した `--workdir` 内でのみファイル編集可。Codex は
330
366
  workspace-write、Claude は編集自動承認、Gemini は accept-edits モード。
367
+ `openrouter` vendor は work モードを明確に拒否します——編集はエージェント型
368
+ CLI ベンダーへ。
331
369
 
332
370
  ## 🔒 安全機構
333
371
 
@@ -381,11 +419,14 @@ configure.sh # 対話式レーンメニ
381
419
 
382
420
  ## 🌱 ステータス
383
421
 
384
- v0.5.1Git 管理外でも Codex `work` を利用可能に保ち、process group の
385
- クリーンアップで停止を制限し、公開バージョン情報を同期します。v0.5.0
386
- インストーラー、ライフサイクル、ジョブストア、期限、診断、リリース CI
387
- 強化も維持します。Grok/Antigravity のコマンドシェル挙動は CLI バージョンで
388
- 変わる可能性があります。issue PR を歓迎します。
422
+ v0.8.38 つのディスパッチベンダーを備えます——4 つのハーネスネイティブ
423
+ (codex、claude、grok、gemini)、3 つの集約/オーバーフロー CLI(kimi、qwen、
424
+ opencode)、そして CLI 不要の `openrouter` 直接 API ベンダー——すべて統一
425
+ runner 契約と契約テスト付き。Claude Code の `SessionStart` 自動リマインダー
426
+ も同梱。kimi、qwen、opencode、openrouter runner はフェイクバイナリで
427
+ 契約テスト済みです。実モデルでの報告を歓迎します。Grok/Antigravity の
428
+ コマンドシェル挙動は CLI バージョンで変わる可能性があります。issue と PR を
429
+ 歓迎します。
389
430
 
390
431
  プロジェクト文書:[コントリビューション](CONTRIBUTING.md) ·
391
432
  [セキュリティ](SECURITY.md) · [変更履歴](CHANGELOG.md)
package/README.ko.md CHANGED
@@ -5,8 +5,9 @@
5
5
  ### 라우팅 테이블 하나로, 모든 하네스를.
6
6
 
7
7
  *메인 루프가 더 이상 어떤 모델을 쓸지 고민하지 않습니다.*<br/>
8
- 모든 서브태스크를 일을 정말 잘하는 모델에게——<br/>
9
- **Claude Code · Codex · Grok Build · Antigravity** 를 가로질러, 이미 내고 있는 구독 그대로.
8
+ **Claude Code · Codex · Grok Build · Antigravity** 어디서 운전하든, 모든 서브태스크를<br/>
9
+ 일을 정말 잘하는 모델에게——Codex, Claude, Grok, Gemini, Kimi, Qwen, OpenCode,<br/>
10
+ 그리고 OpenRouter 를 통한 모든 호스팅 모델까지——이미 내고 있는 구독 그대로, 또는 API 키 하나로.
10
11
 
11
12
  <img src="docs/hero.ko.png" alt="omnilane 이 각 서브태스크를 Claude Code·Codex·Grok·Antigravity 중 가장 잘하는 모델로 보냅니다" width="820"/>
12
13
 
@@ -20,6 +21,14 @@
20
21
 
21
22
  ---
22
23
 
24
+ ## v0.8.3 새 기능
25
+
26
+ - **MCP 서버** — `omnilane mcp` 는 의존성 없는 stdio MCP 서버를 시작하여,
27
+ MCP 지원 호스트(Claude Code, Codex, Gemini CLI, Cursor, OpenCode 등)가
28
+ 스킬 설치 없이 omnilane 을 발견하고 호출할 수 있습니다: 도구는 `route`,
29
+ `jobs_status`, `jobs_result`, `list_lanes`. `route` 는 읽기 전용 advise 가
30
+ 기본값이며, work 모드는 명시적 workdir 이 필요합니다.
31
+
23
32
  ## v0.8.2 새 기능
24
33
 
25
34
  - **`openrouter` vendor** — `curl`과 `OPENROUTER_API_KEY`만으로
@@ -114,8 +123,9 @@ omnilane ui start # 선택: 브라우저에서 잡을 실시간 확인
114
123
  ## 🧭 동작 방식
115
124
 
116
125
  omnilane 은 **어떤** agentic CLI 든 메인 루프가 서브태스크를 레인으로 분류하고,
117
- 각 레인을 그 작업에 가장 강한 벤더 CLI 로 헤드리스 디스패치하게 해 줍니다.
118
- 기존 구독 로그인을 그대로 사용합니다:
126
+ 각 레인을 그 작업에 가장 강한 벤더로 헤드리스 디스패치하게 해 줍니다.
127
+ 기존 구독 로그인을 그대로 사용합니다(`openrouter` vendor 는 예외: CLI 없이
128
+ API 키 하나로 직접 연결):
119
129
 
120
130
  ```mermaid
121
131
  flowchart LR
@@ -240,8 +250,10 @@ omnilane ui stop # 정상 중지
240
250
 
241
251
  ## 📦 설치
242
252
 
243
- 전제: 라우팅할 벤더 CLI(`codex`, `claude`, `grok`, `agy`)가 로그인된 채
244
- `PATH` 에 있을 것——**가진 것만 있으면 됩니다**, 없는 레인은 자동 강등.
253
+ 전제: 라우팅할 벤더 CLI(`codex`, `claude`, `grok`, `agy`, 선택적으로
254
+ `kimi`, `qwen`, `opencode`)가 로그인된 채 `PATH` 에 있을 것——**가진 것만
255
+ 있으면 됩니다**, 없는 레인은 자동 강등. `openrouter` vendor 는 예외로 CLI 가
256
+ 필요 없습니다——`curl` 과 환경 변수의 `OPENROUTER_API_KEY` 만 있으면 됩니다.
245
257
 
246
258
  가장 빠른 방법: `./install.sh` — 로컬 CLI 를 감지해 스킬을 연결하고, 나머지
247
259
  플러그인 명령을 안내하며, 실효 라우팅을 출력한 뒤 대화형 설정 메뉴를
@@ -265,6 +277,27 @@ omnilane ui stop # 정상 중지
265
277
  - **Antigravity**: `agy plugin install <이 저장소>`(먼저
266
278
  `agy plugin validate` 로 확인)
267
279
 
280
+ ### MCP 서버
281
+
282
+ `omnilane mcp` 는 의존성 없이 로컬에서 실행되는 MCP stdio 서버를 시작하여,
283
+ MCP 지원 호스트가 스킬 설치나 라우팅 리마인더 없이 omnilane 을 발견하고
284
+ 호출할 수 있게 합니다. 호스트 설정에서 설치된 CLI 를 지정하면 됩니다:
285
+
286
+ ```json
287
+ {
288
+ "mcpServers": {
289
+ "omnilane": {
290
+ "command": "omnilane",
291
+ "args": ["mcp"]
292
+ }
293
+ }
294
+ }
295
+ ```
296
+
297
+ 서버는 `route`, `jobs_status`, `jobs_result`, `list_lanes` 를 제공합니다.
298
+ `route` 의 기본값은 읽기 전용 `advise` 모드이며, `work` 를 선택한 호출은
299
+ 명시적 `workdir` 을 함께 제공해야 합니다.
300
+
268
301
  ## ⚙️ 사용자 설정
269
302
 
270
303
  세 계층, 모두 선택 사항:
@@ -318,9 +351,12 @@ CLI 를 사용할 수 없음, `5` 1라운드 성공 투표자 부족, `6` 2라
318
351
  ## 🎭 모드
319
352
 
320
353
  - **advise(기본)** — 읽기 전용 워커. Codex 는 read-only 샌드박스,
321
- Claude 는 Read/Glob/Grep 만, Grok 은 plan 모드.
354
+ Claude 는 Read/Glob/Grep 만, Grok 은 plan 모드, Kimi 와 OpenCode 는 각자의
355
+ 읽기 전용 plan 모드 고정, OpenRouter 는 설계상 advise 전용(순수 추론).
322
356
  - **work** — 지정한 `--workdir` 안에서만 파일 수정 허용. Codex 는
323
357
  workspace-write, Claude 는 편집 자동 승인, Gemini 는 accept-edits 모드.
358
+ `openrouter` vendor 는 work 모드를 명확한 오류로 거부합니다——파일 편집은
359
+ 에이전트형 CLI 벤더로 보내세요.
324
360
 
325
361
  ## 🔒 안전 장치
326
362
 
@@ -371,11 +407,13 @@ CLI 를 사용할 수 없음, `5` 1라운드 성공 투표자 부족, `6` 2라
371
407
 
372
408
  ## 🌱 상태
373
409
 
374
- v0.5.1 Git 저장소 밖에서도 Codex `work` 를 사용할 수 있게 유지하고 process
375
- group 정리로 멈춤을 제한하며 모든 공개 버전 정보를 동기화합니다. v0.5.0 의 설치
376
- 프로그램, 디스패치 수명주기, 저장소, 전체 기한, 진단, 릴리스 CI 강화도 그대로
377
- 유지합니다. Grok/Antigravity 커맨드 동작은 CLI 버전에 따라 달라질 수 있습니다.
378
- issue PR 환영합니다.
410
+ v0.8.3 8개의 디스패치 벤더를 갖춥니다——4개의 하네스 네이티브(codex,
411
+ claude, grok, gemini), 3개의 집합/오버플로 CLI(kimi, qwen, opencode), 그리고
412
+ CLI 필요 없는 `openrouter` 직접 API 벤더——모두 통일 runner 계약과 계약
413
+ 테스트를 갖추었고, Claude Code `SessionStart` 자동 리마인더도 포함합니다.
414
+ kimi, qwen, opencode, openrouter 의 runner 는 페이크 바이너리로 계약 테스트를
415
+ 마쳤습니다. 실제 모델 사용 보고를 환영합니다. Grok/Antigravity 커맨드 셸
416
+ 동작은 CLI 버전에 따라 달라질 수 있습니다. issue 와 PR 환영합니다.
379
417
 
380
418
  프로젝트 문서: [기여 가이드](CONTRIBUTING.md) · [보안 정책](SECURITY.md) ·
381
419
  [변경 기록](CHANGELOG.md)
package/README.md CHANGED
@@ -5,8 +5,9 @@
5
5
  ### One routing table, every harness.
6
6
 
7
7
  *Your main loop stops guessing which model to use.*<br/>
8
- Every subtask goes to the model that is actually best at it across<br/>
9
- **Claude Code · Codex · Grok Build · Antigravity**, on the subscriptions you already pay for.
8
+ Drive it from **Claude Code · Codex · Grok Build · Antigravity**, and every subtask goes<br/>
9
+ to the model that is actually best at it Codex, Claude, Grok, Gemini, Kimi, Qwen, OpenCode,<br/>
10
+ or any hosted model via OpenRouter — on the subscriptions you already pay for, or a single API key.
10
11
 
11
12
  <img src="docs/hero.png" alt="omnilane routes each subtask to the best model across Claude Code, Codex, Grok and Antigravity" width="820"/>
12
13
 
@@ -20,6 +21,14 @@ Every subtask goes to the model that is actually best at it — across<br/>
20
21
 
21
22
  ---
22
23
 
24
+ ## What's new in v0.8.3
25
+
26
+ - **MCP server** — `omnilane mcp` starts a zero-dependency stdio MCP server,
27
+ so any MCP-capable host (Claude Code, Codex, Gemini CLI, Cursor, OpenCode…)
28
+ can discover and call omnilane without installing the skill: tools `route`,
29
+ `jobs_status`, `jobs_result`, and `list_lanes`. `route` defaults to
30
+ read-only advise mode; work mode requires an explicit workdir.
31
+
23
32
  ## What's new in v0.8.2
24
33
 
25
34
  - **`openrouter` vendor** — dispatch straight to the OpenRouter API with
@@ -115,8 +124,9 @@ omnilane ui start # optional: watch jobs live in your browser
115
124
  ## 🧭 How it works
116
125
 
117
126
  omnilane lets the main loop of **any** agentic CLI classify subtasks into
118
- lanes and dispatch each lane to the best vendor's CLI — headlessly, using
119
- your existing subscription logins:
127
+ lanes and dispatch each lane to the best vendor — headlessly, using your
128
+ existing subscription logins (or, for the `openrouter` vendor, a direct API
129
+ key with no extra CLI at all):
120
130
 
121
131
  ```mermaid
122
132
  flowchart LR
@@ -244,8 +254,10 @@ Core routing does not need Python; only this UI requires Python 3.9 or newer.
244
254
  ## 📦 Install
245
255
 
246
256
  Requirements: the vendor CLIs you want to route to, logged in (`codex`,
247
- `claude`, `grok`, `agy`) and on `PATH` install only the ones you have; the
248
- rest of the table degrades automatically.
257
+ `claude`, `grok`, `agy`, and optionally `kimi`, `qwen`, `opencode`) and on
258
+ `PATH` — install only the ones you have; the rest of the table degrades
259
+ automatically. The `openrouter` vendor is the exception: it needs no CLI,
260
+ only `curl` and an `OPENROUTER_API_KEY` in your environment.
249
261
 
250
262
  Quickest: `./install.sh` — symlinks the skill for the CLIs it finds, prints
251
263
  the plugin commands for the rest, shows your effective routing, and offers the
@@ -272,6 +284,27 @@ Rollback the installer-owned links and marked reminders with
272
284
  - **Antigravity**: `agy plugin install <this repo>` (check first with
273
285
  `agy plugin validate <this repo>`)
274
286
 
287
+ ### MCP server
288
+
289
+ `omnilane mcp` starts a zero-dependency, local MCP stdio server so any
290
+ MCP-capable host can discover and call omnilane without installing the skill or
291
+ adding a routing reminder. Configure the host to launch the installed CLI:
292
+
293
+ ```json
294
+ {
295
+ "mcpServers": {
296
+ "omnilane": {
297
+ "command": "omnilane",
298
+ "args": ["mcp"]
299
+ }
300
+ }
301
+ }
302
+ ```
303
+
304
+ The server exposes `route`, `jobs_status`, `jobs_result`, and `list_lanes`.
305
+ `route` defaults to read-only `advise` mode. Calls that select `work` must also
306
+ provide an explicit `workdir`.
307
+
275
308
  ## ⚙️ Configure
276
309
 
277
310
  Three layers, all optional:
@@ -343,11 +376,13 @@ code passes through.
343
376
  ## 🎭 Modes
344
377
 
345
378
  - **advise** (default) — read-only worker. Codex runs in a read-only sandbox;
346
- Claude gets only Read/Glob/Grep; Grok runs in plan mode. Use for reviews,
347
- questions, second opinions.
379
+ Claude gets only Read/Glob/Grep; Grok runs in plan mode; Kimi and OpenCode
380
+ pin their read-only plan modes; OpenRouter is advise-only by design (pure
381
+ inference). Use for reviews, questions, second opinions.
348
382
  - **work** — the worker may edit files, only inside the `--workdir` you name.
349
383
  Codex gets a workspace-write sandbox; Claude auto-accepts edits; Gemini runs
350
- in accept-edits mode.
384
+ in accept-edits mode. The `openrouter` vendor refuses work mode with a clear
385
+ error — route edits to an agentic CLI vendor instead.
351
386
 
352
387
  ## 🔒 Safety rails
353
388
 
@@ -404,11 +439,13 @@ configurator and `routing.local.yaml` exist so you can disagree.
404
439
 
405
440
  ## 🌱 Status
406
441
 
407
- v0.5.1 keeps Codex `work` usable outside Git while bounding stalls with
408
- process-group cleanup, and synchronizes every public version surface. It builds
409
- on v0.5.0's installer, lifecycle, job-store, deadline, diagnostics, and release
410
- CI hardening. Grok/Antigravity command-shell behavior may still vary across CLI
411
- versions. Issues and PRs welcome.
442
+ v0.8.3 spans eight dispatch vendors four harness natives (codex, claude,
443
+ grok, gemini), three aggregator/overflow CLIs (kimi, qwen, opencode), and the
444
+ CLI-free `openrouter` direct-API vendor on the uniform runner contract with
445
+ contract tests, plus the Claude Code `SessionStart` auto-reminder. kimi, qwen,
446
+ opencode, and openrouter runners are contract-tested against fake binaries;
447
+ real-model reports welcome. Grok/Antigravity command-shell behavior may still
448
+ vary across CLI versions. Issues and PRs welcome.
412
449
 
413
450
  Project policies: [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) ·
414
451
  [Changelog](CHANGELOG.md)
package/README.zh-CN.md CHANGED
@@ -5,8 +5,9 @@
5
5
  ### 一张路由表,四个执行框架通用。
6
6
 
7
7
  *让主循环不再猜要用哪个模型。*<br/>
8
- 每个子任务都派给真正最擅长它的模型——横跨<br/>
9
- **Claude Code · Codex · Grok Build · Antigravity**,直接用你已经在付的订阅。
8
+ 从 **Claude Code · Codex · Grok Build · Antigravity** 任一框架驾驶,每个子任务都派给<br/>
9
+ 真正最擅长它的模型——Codex、ClaudeGrok、Gemini、Kimi、Qwen、OpenCode,<br/>
10
+ 或经 OpenRouter 直达任何托管模型——用你已经在付的订阅,或一把 API 密钥。
10
11
 
11
12
  <img src="docs/hero.zh-CN.png" alt="omnilane 把每个子任务派给 Claude Code、Codex、Grok、Antigravity 中最擅长的模型" width="820"/>
12
13
 
@@ -20,6 +21,14 @@
20
21
 
21
22
  ---
22
23
 
24
+ ## v0.8.3 新功能
25
+
26
+ - **MCP server** — `omnilane mcp` 启动零依赖的 stdio MCP server,任何支持
27
+ MCP 的宿主(Claude Code、Codex、Gemini CLI、Cursor、OpenCode……)无需安装
28
+ skill 即可发现并调用 omnilane:提供 `route`、`jobs_status`、`jobs_result`、
29
+ `list_lanes` 四个工具。`route` 默认只读 advise 模式;work 模式必须明确
30
+ 指定 workdir。
31
+
23
32
  ## v0.8.2 新功能
24
33
 
25
34
  - **`openrouter` vendor** — 只需 `curl` 加一个 `OPENROUTER_API_KEY`,
@@ -101,7 +110,8 @@ omnilane ui start # 可选:在浏览器实时查看派发
101
110
  ## 🧭 工作原理
102
111
 
103
112
  omnilane 让**任何**一个 agentic CLI 的主循环把子任务分类到通道(lane),
104
- 再以无头方式把每条通道派发给该项工作最强的厂商 CLI,直接沿用你已有的订阅登录:
113
+ 再以无头方式把每条通道派发给该项工作最强的厂商——直接沿用你已有的订阅登录
114
+ (`openrouter` vendor 例外:免装任何 CLI,一把 API 密钥直连):
105
115
 
106
116
  ```mermaid
107
117
  flowchart LR
@@ -215,8 +225,10 @@ Esc。服务器发送事件(SSE)会实时更新,又不会重建当前聚焦的
215
225
 
216
226
  ## 📦 安装
217
227
 
218
- 前置需求:想路由到的厂商 CLI(`codex`、`claude`、`grok`、`agy`)已登录且在
219
- `PATH` 上——**有几家装几家就好**,缺的通道会自动降级。
228
+ 前置需求:想路由到的厂商 CLI(`codex`、`claude`、`grok`、`agy`,另可选
229
+ `kimi`、`qwen`、`opencode`)已登录且在 `PATH` 上——**有几家装几家就好**,
230
+ 缺的通道会自动降级。`openrouter` vendor 是例外:不需要任何 CLI,只要
231
+ `curl` 和环境变量里的 `OPENROUTER_API_KEY`。
220
232
 
221
233
  最快:`./install.sh` — 自动检测本机的 CLI、接好技能、列出其余的插件安装命令、
222
234
  打印这台机器的生效路由表,最后询问是否进入交互设置菜单(`--uninstall` 可逆)。
@@ -238,6 +250,27 @@ Esc。服务器发送事件(SSE)会实时更新,又不会重建当前聚焦的
238
250
  - **Antigravity**:`agy plugin install <本仓库路径>`(先用
239
251
  `agy plugin validate` 检查)
240
252
 
253
+ ### MCP server
254
+
255
+ `omnilane mcp` 会启动零依赖、跑在本机的 MCP stdio server,让任何支持 MCP
256
+ 的宿主无需安装 skill、也不用加路由提醒,即可发现并调用 omnilane。在宿主
257
+ 配置里指向已安装的 CLI 即可:
258
+
259
+ ```json
260
+ {
261
+ "mcpServers": {
262
+ "omnilane": {
263
+ "command": "omnilane",
264
+ "args": ["mcp"]
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ Server 提供 `route`、`jobs_status`、`jobs_result`、`list_lanes` 四个工具。
271
+ `route` 默认只读 `advise` 模式;选择 `work` 的调用必须同时提供明确的
272
+ `workdir`。
273
+
241
274
  ## ⚙️ 自定义设置
242
275
 
243
276
  三层,全部可选:
@@ -291,9 +324,11 @@ configure.sh # 交互通道菜单
291
324
  ## 🎭 模式
292
325
 
293
326
  - **advise(默认)** — 只读工作端。Codex 跑只读沙箱;Claude 只给
294
- Read/Glob/Grep;Grok 跑 plan 模式。适合审查、提问、第二意见。
327
+ Read/Glob/Grep;Grok 跑 plan 模式;Kimi 与 OpenCode 锁各自的只读
328
+ plan 模式;OpenRouter 天生只做 advise(纯推理)。适合审查、提问、第二意见。
295
329
  - **work** — 允许改文件,仅限你指定的 `--workdir`。Codex 给
296
330
  workspace-write 沙箱;Claude 自动接受编辑;Gemini 跑 accept-edits 模式。
331
+ `openrouter` vendor 会明确拒绝 work 模式——改文件请走代理式 CLI vendor。
297
332
 
298
333
  ## 🔒 内置安全机制
299
334
 
@@ -345,10 +380,12 @@ configure.sh # 交互通道菜单
345
380
 
346
381
  ## 🌱 状态
347
382
 
348
- v0.5.1 Codex `work` 在非 Git 目录仍可使用,同时以进程组清理限制卡死,
349
- 并同步所有公开版本来源。它延续 v0.5.0 对安装器、派发生命周期、作业存储、
350
- 整体截止时间、诊断和发布 CI 的强化。Grok/Antigravity 命令壳行为仍可能随
351
- CLI 版本变动。欢迎提交 issue PR。
383
+ v0.8.3 共有八个派发 vendor——四个框架原生(codex、claude、grok、gemini)、
384
+ 三个聚合/溢流 CLI(kimi、qwen、opencode),加上免 CLI 的 `openrouter` 直连
385
+ API vendor——全部走统一 runner 契约并附 contract 测试,另有 Claude Code
386
+ `SessionStart` 自动提醒。kimi、qwen、opencode、openrouter runner 以假
387
+ 可执行文件做过契约测试;欢迎反馈真实模型使用经验。Grok/Antigravity 命令壳
388
+ 行为仍可能随 CLI 版本变动。欢迎提交 issue 与 PR。
352
389
 
353
390
  项目文档:[贡献指南](CONTRIBUTING.md) · [安全政策](SECURITY.md) ·
354
391
  [变更记录](CHANGELOG.md)
package/README.zh-TW.md CHANGED
@@ -5,8 +5,9 @@
5
5
  ### 一張路由表,四個執行框架通用。
6
6
 
7
7
  *讓主迴圈不再猜要用哪個模型。*<br/>
8
- 每個子任務都派給真正最擅長它的模型——橫跨<br/>
9
- **Claude Code · Codex · Grok Build · Antigravity**,直接用你已經在付的訂閱。
8
+ 從 **Claude Code · Codex · Grok Build · Antigravity** 任一框架開車,每個子任務都派給<br/>
9
+ 真正最擅長它的模型——Codex、ClaudeGrok、Gemini、Kimi、Qwen、OpenCode,<br/>
10
+ 或經 OpenRouter 直達任何託管模型——用你已經在付的訂閱,或一把 API 金鑰。
10
11
 
11
12
  <img src="docs/hero.zh-TW.png" alt="omnilane 把每個子任務派給 Claude Code、Codex、Grok、Antigravity 中最擅長的模型" width="820"/>
12
13
 
@@ -20,6 +21,14 @@
20
21
 
21
22
  ---
22
23
 
24
+ ## v0.8.3 新功能
25
+
26
+ - **MCP server** — `omnilane mcp` 啟動零依賴的 stdio MCP server,任何支援
27
+ MCP 的宿主(Claude Code、Codex、Gemini CLI、Cursor、OpenCode……)不必安裝
28
+ skill 就能發現並呼叫 omnilane:提供 `route`、`jobs_status`、`jobs_result`、
29
+ `list_lanes` 四個工具。`route` 預設唯讀 advise 模式;work 模式必須明確
30
+ 指定 workdir。
31
+
23
32
  ## v0.8.2 新功能
24
33
 
25
34
  - **`openrouter` vendor** — 只要 `curl` 加一把 `OPENROUTER_API_KEY`,
@@ -101,7 +110,8 @@ omnilane ui start # 選配:在瀏覽器即時看派工
101
110
  ## 🧭 運作方式
102
111
 
103
112
  omnilane 讓**任何**一個 agentic CLI 的主迴圈把子任務分類到通道(lane),
104
- 再以無頭方式把每條通道派工給該項工作最強的廠商 CLI,直接沿用你既有的訂閱登入:
113
+ 再以無頭方式把每條通道派工給該項工作最強的廠商——直接沿用你既有的訂閱登入
114
+ (`openrouter` vendor 例外:免裝任何 CLI,一把 API 金鑰直連):
105
115
 
106
116
  ```mermaid
107
117
  flowchart LR
@@ -215,8 +225,10 @@ omnilane ui stop # 正常停止
215
225
 
216
226
  ## 📦 安裝
217
227
 
218
- 前置需求:想路由到的廠商 CLI(`codex`、`claude`、`grok`、`agy`)已登入且在
219
- `PATH` 上——**有幾家裝幾家就好**,缺的通道會自動降級。
228
+ 前置需求:想路由到的廠商 CLI(`codex`、`claude`、`grok`、`agy`,另可選
229
+ `kimi`、`qwen`、`opencode`)已登入且在 `PATH` 上——**有幾家裝幾家就好**,
230
+ 缺的通道會自動降級。`openrouter` vendor 是例外:不需要任何 CLI,只要
231
+ `curl` 和環境變數裡的 `OPENROUTER_API_KEY`。
220
232
 
221
233
  最快:`./install.sh` — 自動偵測本機的 CLI、接好技能、列出其餘的外掛安裝指令、
222
234
  印出這台機器的生效路由表,最後問你要不要進入互動設定選單(`--uninstall` 可逆)。
@@ -239,6 +251,27 @@ omnilane ui stop # 正常停止
239
251
  - **Antigravity**:`agy plugin install <本 repo 路徑>`(先用
240
252
  `agy plugin validate` 檢查)
241
253
 
254
+ ### MCP server
255
+
256
+ `omnilane mcp` 會啟動零依賴、跑在本機的 MCP stdio server,讓任何支援 MCP
257
+ 的宿主不必安裝 skill、也不用加路由提醒,就能發現並呼叫 omnilane。在宿主
258
+ 設定裡指向已安裝的 CLI 即可:
259
+
260
+ ```json
261
+ {
262
+ "mcpServers": {
263
+ "omnilane": {
264
+ "command": "omnilane",
265
+ "args": ["mcp"]
266
+ }
267
+ }
268
+ }
269
+ ```
270
+
271
+ Server 提供 `route`、`jobs_status`、`jobs_result`、`list_lanes` 四個工具。
272
+ `route` 預設唯讀 `advise` 模式;選 `work` 的呼叫必須同時提供明確的
273
+ `workdir`。
274
+
242
275
  ## ⚙️ 自訂設定
243
276
 
244
277
  三層,全部選用:
@@ -304,9 +337,11 @@ codex/claude/grok/gemini 自選 1-4 個評審。開了之後,同一個問題丟
304
337
  ## 🎭 模式
305
338
 
306
339
  - **advise(預設)** — 唯讀工作端。Codex 跑唯讀沙箱;Claude 只給
307
- Read/Glob/Grep;Grok 跑 plan 模式。適合審查、提問、第二意見。
340
+ Read/Glob/Grep;Grok 跑 plan 模式;Kimi 與 OpenCode 鎖各自的唯讀
341
+ plan 模式;OpenRouter 天生只做 advise(純推論)。適合審查、提問、第二意見。
308
342
  - **work** — 允許改檔案,僅限你指定的 `--workdir`。Codex 給
309
343
  workspace-write 沙箱;Claude 自動接受編輯;Gemini 跑 accept-edits 模式。
344
+ `openrouter` vendor 會明確拒絕 work 模式——改檔請走代理式 CLI vendor。
310
345
 
311
346
  ## 🔒 內建安全機制
312
347
 
@@ -355,10 +390,12 @@ codex/claude/grok/gemini 自選 1-4 個評審。開了之後,同一個問題丟
355
390
 
356
391
  ## 🌱 狀態
357
392
 
358
- v0.5.1 Codex `work` 在非 Git 目錄仍可使用,同時以程序群組清理限制卡死,
359
- 並同步所有公開版本來源。它延續 v0.5.0 對安裝器、派工生命週期、job 儲存、
360
- 整體截止時間、診斷與發布 CI 的強化。Grok/Antigravity 指令殼行為仍可能隨
361
- CLI 版本變動。歡迎回報 issue PR。
393
+ v0.8.3 共有八個派工 vendor——四個框架原生(codex、claude、grok、gemini)、
394
+ 三個聚合/溢流 CLI(kimi、qwen、opencode),加上免 CLI 的 `openrouter` 直連
395
+ API vendor——全部走統一 runner 契約並附 contract 測試,另有 Claude Code
396
+ `SessionStart` 自動提醒。kimi、qwen、opencode、openrouter runner 以假
397
+ 執行檔做過契約測試;歡迎回報真實模型使用經驗。Grok/Antigravity 指令殼行為
398
+ 仍可能隨 CLI 版本變動。歡迎回報 issue 與 PR。
362
399
 
363
400
  專案文件:[貢獻指南](CONTRIBUTING.md) · [安全政策](SECURITY.md) ·
364
401
  [變更紀錄](CHANGELOG.md)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.2
1
+ 0.8.3
package/bin/omnilane CHANGED
@@ -31,6 +31,13 @@ case "$cmd" in
31
31
  list) exec bash "$REPO/scripts/dispatch.sh" --list ;;
32
32
  route|dispatch) exec bash "$REPO/scripts/dispatch.sh" "$@" ;;
33
33
  jobs) exec bash "$REPO/scripts/jobs.sh" "$@" ;;
34
+ mcp)
35
+ command -v node >/dev/null 2>&1 || {
36
+ echo "omnilane: mcp requires Node.js on PATH" >&2
37
+ exit 1
38
+ }
39
+ exec node "$REPO/bin/omnilane-mcp" "$@"
40
+ ;;
34
41
  completion)
35
42
  [[ $# -eq 1 ]] || {
36
43
  echo "usage: omnilane completion bash|zsh" >&2
@@ -61,6 +68,7 @@ omnilane — one routing table, every harness
61
68
  omnilane route [--vendor V] [flags] LANE "TASK"
62
69
  dispatch or consult a model
63
70
  omnilane jobs [--json] COMMAND [ARGS] list, status, result, stats, wait, or audit
71
+ omnilane mcp start the MCP stdio server
64
72
  omnilane completion bash|zsh print a shell completion script
65
73
  omnilane release-audit [flags] offline, read-only release gate
66
74
  omnilane doctor [--json] read-only local health report
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const readline = require('readline');
7
+ const { spawn } = require('child_process');
8
+
9
+ const ROOT = path.resolve(__dirname, '..');
10
+ const DISPATCH_SCRIPT = path.join(ROOT, 'scripts', 'dispatch.sh');
11
+ const JOBS_SCRIPT = path.join(ROOT, 'scripts', 'jobs.sh');
12
+ const LATEST_PROTOCOL_VERSION = '2025-11-25';
13
+ const SUPPORTED_PROTOCOL_VERSIONS = new Set([
14
+ '2025-11-25',
15
+ '2025-06-18',
16
+ '2025-03-26',
17
+ '2024-11-05',
18
+ ]);
19
+ const MAX_COLLECTED_BYTES = 1024 * 1024;
20
+ const TRUNCATION_MARKER = `[omnilane-mcp: output truncated after ${MAX_COLLECTED_BYTES} bytes]`;
21
+ const LANE_PATTERN = /^[a-z][a-z0-9-]*$/;
22
+ const JOB_ID_PATTERN = /^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$/;
23
+ const OWN = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
24
+
25
+ let serverVersion;
26
+ try {
27
+ serverVersion = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf8').trim();
28
+ if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(serverVersion)) {
29
+ throw new Error('invalid VERSION file');
30
+ }
31
+ } catch (error) {
32
+ process.stderr.write(`omnilane-mcp: ${error.message}\n`);
33
+ process.exit(1);
34
+ }
35
+
36
+ const tools = [
37
+ {
38
+ name: 'route',
39
+ description: 'Route a task through omnilane. Defaults to read-only advise mode; work mode requires an explicit workdir.',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ lane: {
44
+ type: 'string',
45
+ pattern: '^[a-z][a-z0-9-]*$',
46
+ description: 'Configured omnilane lane name.',
47
+ },
48
+ task: { type: 'string', description: 'Task text sent to the selected lane.' },
49
+ mode: {
50
+ type: 'string',
51
+ enum: ['advise', 'work'],
52
+ default: 'advise',
53
+ description: 'Read-only advice or write-enabled work.',
54
+ },
55
+ workdir: {
56
+ type: 'string',
57
+ minLength: 1,
58
+ description: 'Working directory. Required when mode is work.',
59
+ },
60
+ vendor: { type: 'string', description: 'Optional configured vendor override.' },
61
+ model: { type: 'string', description: 'Optional routed model override.' },
62
+ effort: { type: 'string', description: 'Optional routed effort override.' },
63
+ background: {
64
+ type: 'boolean',
65
+ default: false,
66
+ description: 'Run as an omnilane background job and return its job ID.',
67
+ },
68
+ timeout: {
69
+ type: 'integer',
70
+ minimum: 1,
71
+ description: 'Per-call timeout in seconds forwarded to dispatch.sh.',
72
+ },
73
+ },
74
+ required: ['lane', 'task'],
75
+ additionalProperties: false,
76
+ allOf: [
77
+ {
78
+ if: { properties: { mode: { const: 'work' } }, required: ['mode'] },
79
+ then: { required: ['workdir'] },
80
+ },
81
+ ],
82
+ },
83
+ annotations: {
84
+ readOnlyHint: false,
85
+ destructiveHint: true,
86
+ idempotentHint: false,
87
+ openWorldHint: true,
88
+ },
89
+ },
90
+ {
91
+ name: 'jobs_status',
92
+ description: 'Read the current status of one omnilane background job.',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ id: {
97
+ type: 'string',
98
+ pattern: '^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$',
99
+ description: 'Omnilane background job ID.',
100
+ },
101
+ },
102
+ required: ['id'],
103
+ additionalProperties: false,
104
+ },
105
+ annotations: {
106
+ readOnlyHint: true,
107
+ destructiveHint: false,
108
+ idempotentHint: true,
109
+ openWorldHint: false,
110
+ },
111
+ },
112
+ {
113
+ name: 'jobs_result',
114
+ description: 'Read the result of one completed omnilane background job.',
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ id: {
119
+ type: 'string',
120
+ pattern: '^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$',
121
+ description: 'Omnilane background job ID.',
122
+ },
123
+ },
124
+ required: ['id'],
125
+ additionalProperties: false,
126
+ },
127
+ annotations: {
128
+ readOnlyHint: true,
129
+ destructiveHint: false,
130
+ idempotentHint: true,
131
+ openWorldHint: false,
132
+ },
133
+ },
134
+ {
135
+ name: 'list_lanes',
136
+ description: 'List the effective omnilane routing table and available fallback selection.',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {},
140
+ additionalProperties: false,
141
+ },
142
+ annotations: {
143
+ readOnlyHint: true,
144
+ destructiveHint: false,
145
+ idempotentHint: true,
146
+ openWorldHint: false,
147
+ },
148
+ },
149
+ ];
150
+
151
+ class RpcError extends Error {
152
+ constructor(code, message, data) {
153
+ super(message);
154
+ this.code = code;
155
+ this.data = data;
156
+ }
157
+ }
158
+
159
+ const activeChildren = new Set();
160
+ let shuttingDown = false;
161
+ let outputClosed = false;
162
+
163
+ function isObject(value) {
164
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
165
+ }
166
+
167
+ function textResult(text) {
168
+ return { content: [{ type: 'text', text }] };
169
+ }
170
+
171
+ function toolError(message) {
172
+ return {
173
+ content: [{ type: 'text', text: `Error: ${message}` }],
174
+ isError: true,
175
+ };
176
+ }
177
+
178
+ function writeMessage(message) {
179
+ if (outputClosed) return;
180
+ process.stdout.write(`${JSON.stringify(message)}\n`);
181
+ }
182
+
183
+ function writeResult(id, result) {
184
+ writeMessage({ jsonrpc: '2.0', id, result });
185
+ }
186
+
187
+ function writeError(id, code, message, data) {
188
+ const error = { code, message };
189
+ if (data !== undefined) error.data = data;
190
+ writeMessage({ jsonrpc: '2.0', id, error });
191
+ }
192
+
193
+ function validationError(args, allowedKeys) {
194
+ if (!isObject(args)) return 'arguments must be an object';
195
+ const unexpected = Object.keys(args).filter((key) => !allowedKeys.includes(key));
196
+ if (unexpected.length > 0) return `unexpected argument: ${unexpected[0]}`;
197
+ return null;
198
+ }
199
+
200
+ function validateRouteArguments(args) {
201
+ let error = validationError(args, [
202
+ 'lane', 'task', 'mode', 'workdir', 'vendor', 'model', 'effort',
203
+ 'background', 'timeout',
204
+ ]);
205
+ if (error) return error;
206
+ if (typeof args.lane !== 'string' || !LANE_PATTERN.test(args.lane)) {
207
+ return 'lane must match ^[a-z][a-z0-9-]*$';
208
+ }
209
+ if (typeof args.task !== 'string') return 'task must be a string';
210
+ if (OWN(args, 'mode') && args.mode !== 'advise' && args.mode !== 'work') {
211
+ return 'mode must be advise or work';
212
+ }
213
+ if (OWN(args, 'workdir') && (typeof args.workdir !== 'string' || args.workdir.length === 0)) {
214
+ return 'workdir must be a non-empty string';
215
+ }
216
+ for (const key of ['vendor', 'model', 'effort']) {
217
+ if (OWN(args, key) && typeof args[key] !== 'string') return `${key} must be a string`;
218
+ }
219
+ if (OWN(args, 'background') && typeof args.background !== 'boolean') {
220
+ return 'background must be a boolean';
221
+ }
222
+ if (OWN(args, 'timeout') && (!Number.isInteger(args.timeout) || args.timeout < 1)) {
223
+ return 'timeout must be a positive integer';
224
+ }
225
+ if (args.mode === 'work' && !OWN(args, 'workdir')) {
226
+ return 'work mode requires an explicit workdir';
227
+ }
228
+ return null;
229
+ }
230
+
231
+ function validateJobArguments(args) {
232
+ const error = validationError(args, ['id']);
233
+ if (error) return error;
234
+ if (typeof args.id !== 'string' || !JOB_ID_PATTERN.test(args.id)) {
235
+ return 'id must match ^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$';
236
+ }
237
+ return null;
238
+ }
239
+
240
+ function createCapture() {
241
+ const chunks = { stdout: [], stderr: [] };
242
+ let collected = 0;
243
+ let truncated = false;
244
+
245
+ function add(stream, chunk) {
246
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
247
+ const remaining = MAX_COLLECTED_BYTES - collected;
248
+ if (remaining > 0) {
249
+ chunks[stream].push(buffer.subarray(0, remaining));
250
+ collected += Math.min(buffer.length, remaining);
251
+ }
252
+ if (buffer.length > remaining) truncated = true;
253
+ }
254
+
255
+ function read(stream) {
256
+ return Buffer.concat(chunks[stream]).toString('utf8');
257
+ }
258
+
259
+ return {
260
+ add,
261
+ stdout: () => read('stdout'),
262
+ stderr: () => read('stderr'),
263
+ wasTruncated: () => truncated,
264
+ };
265
+ }
266
+
267
+ function appendTruncationMarker(text, truncated) {
268
+ if (!truncated) return text;
269
+ const separator = text.length === 0 || text.endsWith('\n') ? '' : '\n';
270
+ return `${text}${separator}${TRUNCATION_MARKER}`;
271
+ }
272
+
273
+ function runScript(script, args) {
274
+ return new Promise((resolve) => {
275
+ const capture = createCapture();
276
+ let child;
277
+ let settled = false;
278
+
279
+ try {
280
+ child = spawn('bash', [script].concat(args), {
281
+ stdio: ['ignore', 'pipe', 'pipe'],
282
+ });
283
+ } catch (error) {
284
+ resolve({ spawnError: error });
285
+ return;
286
+ }
287
+
288
+ activeChildren.add(child);
289
+ child.stdout.on('data', (chunk) => capture.add('stdout', chunk));
290
+ child.stderr.on('data', (chunk) => capture.add('stderr', chunk));
291
+
292
+ function finish(result) {
293
+ if (settled) return;
294
+ settled = true;
295
+ activeChildren.delete(child);
296
+ resolve(Object.assign(result, {
297
+ stdout: capture.stdout(),
298
+ stderr: capture.stderr(),
299
+ truncated: capture.wasTruncated(),
300
+ }));
301
+ if (shuttingDown && activeChildren.size === 0) {
302
+ setImmediate(() => process.exit(0));
303
+ }
304
+ }
305
+
306
+ child.once('error', (error) => finish({ spawnError: error }));
307
+ child.once('close', (code, signal) => finish({ code, signal }));
308
+ });
309
+ }
310
+
311
+ function processFailure(label, result) {
312
+ if (result.spawnError) {
313
+ return toolError(`${label} could not start: ${result.spawnError.message}`);
314
+ }
315
+ const parts = [];
316
+ if (result.stderr) parts.push(result.stderr);
317
+ if (result.stdout) parts.push(result.stdout);
318
+ let detail = parts.join(parts.length > 1 ? '\n' : '');
319
+ detail = appendTruncationMarker(detail, result.truncated);
320
+ const status = result.signal ? `signal ${result.signal}` : `exit ${result.code}`;
321
+ return toolError(`${label} failed with ${status}${detail ? `:\n${detail}` : ''}`);
322
+ }
323
+
324
+ async function runToolScript(label, script, args) {
325
+ const result = await runScript(script, args);
326
+ if (result.spawnError || result.code !== 0) return processFailure(label, result);
327
+ return textResult(appendTruncationMarker(result.stdout, result.truncated));
328
+ }
329
+
330
+ async function callRoute(args) {
331
+ const error = validateRouteArguments(args);
332
+ if (error) return toolError(error);
333
+
334
+ const mode = OWN(args, 'mode') ? args.mode : 'advise';
335
+ const argv = ['--mode', mode];
336
+ for (const key of ['workdir', 'vendor', 'model', 'effort', 'timeout']) {
337
+ if (OWN(args, key)) argv.push(`--${key}`, String(args[key]));
338
+ }
339
+ if (args.background === true) argv.push('--background');
340
+ argv.push(args.lane, args.task);
341
+
342
+ const result = await runScript(DISPATCH_SCRIPT, argv);
343
+ if (result.spawnError || result.code !== 0) return processFailure('route', result);
344
+ const stdout = appendTruncationMarker(result.stdout, result.truncated);
345
+ if (args.background === true) {
346
+ const jobId = stdout.split(/\r?\n/).find((line) => JOB_ID_PATTERN.test(line));
347
+ if (!jobId) return toolError('background route did not return a valid job ID');
348
+ return textResult(jobId);
349
+ }
350
+ return textResult(stdout);
351
+ }
352
+
353
+ async function callTool(name, args) {
354
+ if (name === 'route') return callRoute(args);
355
+ if (name === 'list_lanes') {
356
+ const error = validationError(args, []);
357
+ if (error) return toolError(error);
358
+ return runToolScript('list_lanes', DISPATCH_SCRIPT, ['--list']);
359
+ }
360
+ if (name === 'jobs_status' || name === 'jobs_result') {
361
+ const error = validateJobArguments(args);
362
+ if (error) return toolError(error);
363
+ const command = name === 'jobs_status' ? 'status' : 'result';
364
+ return runToolScript(name, JOBS_SCRIPT, [command, args.id]);
365
+ }
366
+ throw new RpcError(-32602, `Unknown tool: ${name}`);
367
+ }
368
+
369
+ function initialize(params) {
370
+ if (!isObject(params) || typeof params.protocolVersion !== 'string') {
371
+ throw new RpcError(-32602, 'initialize requires a protocolVersion string');
372
+ }
373
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(params.protocolVersion)
374
+ ? params.protocolVersion
375
+ : LATEST_PROTOCOL_VERSION;
376
+ return {
377
+ protocolVersion,
378
+ capabilities: { tools: {} },
379
+ serverInfo: { name: 'omnilane', version: serverVersion },
380
+ };
381
+ }
382
+
383
+ async function handleRequest(message) {
384
+ switch (message.method) {
385
+ case 'initialize':
386
+ return initialize(message.params);
387
+ case 'tools/list':
388
+ if (message.params !== undefined && !isObject(message.params)) {
389
+ throw new RpcError(-32602, 'tools/list params must be an object');
390
+ }
391
+ return { tools };
392
+ case 'tools/call': {
393
+ if (!isObject(message.params) || typeof message.params.name !== 'string') {
394
+ throw new RpcError(-32602, 'tools/call requires a tool name');
395
+ }
396
+ const args = message.params.arguments === undefined ? {} : message.params.arguments;
397
+ return callTool(message.params.name, args);
398
+ }
399
+ default:
400
+ throw new RpcError(-32601, `Method not found: ${message.method}`);
401
+ }
402
+ }
403
+
404
+ async function processLine(line) {
405
+ if (line.length === 0) return;
406
+
407
+ let message;
408
+ try {
409
+ message = JSON.parse(line);
410
+ } catch (error) {
411
+ writeError(null, -32700, 'Parse error');
412
+ return;
413
+ }
414
+
415
+ const hasId = isObject(message) && OWN(message, 'id');
416
+ if (!isObject(message) || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
417
+ if (hasId) writeError(message.id, -32600, 'Invalid Request');
418
+ return;
419
+ }
420
+ if (!hasId) return;
421
+
422
+ try {
423
+ writeResult(message.id, await handleRequest(message));
424
+ } catch (error) {
425
+ if (error instanceof RpcError) {
426
+ writeError(message.id, error.code, error.message, error.data);
427
+ } else {
428
+ process.stderr.write(`omnilane-mcp: internal error: ${error.message}\n`);
429
+ writeError(message.id, -32603, 'Internal error');
430
+ }
431
+ }
432
+ }
433
+
434
+ const input = readline.createInterface({
435
+ input: process.stdin,
436
+ crlfDelay: Infinity,
437
+ terminal: false,
438
+ });
439
+
440
+ input.on('line', (line) => {
441
+ processLine(line).catch((error) => {
442
+ process.stderr.write(`omnilane-mcp: internal error: ${error.message}\n`);
443
+ });
444
+ });
445
+
446
+ function shutdown() {
447
+ if (shuttingDown) return;
448
+ shuttingDown = true;
449
+ input.close();
450
+ for (const child of activeChildren) child.kill('SIGTERM');
451
+ if (activeChildren.size === 0) process.exit(0);
452
+ setTimeout(() => process.exit(0), 1000);
453
+ }
454
+
455
+ process.on('SIGINT', shutdown);
456
+ process.on('SIGTERM', shutdown);
457
+ process.stdout.on('error', (error) => {
458
+ if (error.code === 'EPIPE') {
459
+ outputClosed = true;
460
+ shutdown();
461
+ return;
462
+ }
463
+ process.stderr.write(`omnilane-mcp: stdout error: ${error.message}\n`);
464
+ process.exit(1);
465
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "One routing table, every harness — classify subtasks into lanes and dispatch each lane to the best vendor's agentic CLI (Codex, Claude, Gemini, Grok) using your existing subscription logins.",
5
5
  "bin": {
6
6
  "omnilane": "bin/omnilane"