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 +9 -1
- package/README.ja.md +54 -13
- package/README.ko.md +50 -12
- package/README.md +51 -14
- package/README.zh-CN.md +47 -10
- package/README.zh-TW.md +47 -10
- package/VERSION +1 -1
- package/bin/omnilane +8 -0
- package/bin/omnilane-mcp +465 -0
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
9
|
-
|
|
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.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
|
-
|
|
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
|
-
|
|
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.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
422
|
+
v0.8.3 は 8 つのディスパッチベンダーを備えます——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
|
-
|
|
9
|
-
|
|
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
|
-
각 레인을 그 작업에 가장 강한
|
|
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.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
119
|
-
|
|
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
|
|
248
|
-
rest of the table degrades
|
|
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
|
|
347
|
-
|
|
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.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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.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
|
-
|
|
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.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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.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
|
-
|
|
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.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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.
|
|
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
|
package/bin/omnilane-mcp
ADDED
|
@@ -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.
|
|
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"
|