omnilane 0.8.1 → 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/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,25 @@
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
+
32
+ ## v0.8.2 新功能
33
+
34
+ - **`openrouter` vendor** — 只要 `curl` 加一把 `OPENROUTER_API_KEY`,
35
+ 就能直連 OpenRouter API 派工:任何 omnilane 安裝都摸得到數百個
36
+ 託管模型,不必再裝任何代理 CLI。僅限 advise/consult(不能改檔,
37
+ work 模式會明確報錯指路),模型 slug 必填,例如
38
+ `dispatch.sh --vendor openrouter --model anthropic/claude-sonnet-5 consult "..."`。
39
+ - **`opencode` vendor** — 透過 OpenCode 多供應商聚合 CLI 無頭派工
40
+ (`opencode run`)。advise 模式鎖定內建唯讀 `plan` agent;work 模式
41
+ 用 `--auto`。加入預設 `coding-overflow` 鏈作為最後備援。
42
+
23
43
  ## v0.8.1 新功能
24
44
 
25
45
  - **Claude Code 外掛開場自動載入路由提醒** — 外掛新增 `SessionStart`
@@ -90,7 +110,8 @@ omnilane ui start # 選配:在瀏覽器即時看派工
90
110
  ## 🧭 運作方式
91
111
 
92
112
  omnilane 讓**任何**一個 agentic CLI 的主迴圈把子任務分類到通道(lane),
93
- 再以無頭方式把每條通道派工給該項工作最強的廠商 CLI,直接沿用你既有的訂閱登入:
113
+ 再以無頭方式把每條通道派工給該項工作最強的廠商——直接沿用你既有的訂閱登入
114
+ (`openrouter` vendor 例外:免裝任何 CLI,一把 API 金鑰直連):
94
115
 
95
116
  ```mermaid
96
117
  flowchart LR
@@ -134,7 +155,7 @@ flowchart LR
134
155
  | 📚 long-context | Gemini 3.1 Pro (High) | Claude Opus 4.8 (high) | 百萬 token 長文整合——僅限分析,不派 agentic 長鏈 |
135
156
  | ⚡ fast-agentic | Gemini 3.5 Flash (High) | GPT-5.6 Luna (high) | 快速多步驟 agentic 迴圈、多模態檢查 |
136
157
  | 📡 live-search | Grok 4.5 | —(off) | 即時 X/網路搜尋與社群脈絡 |
137
- | 🚰 coding-overflow | Grok 4.5 | Kimi K3 → Qwen3 Coder Plus | Codex 額度吃緊時的中量級編碼溢流道;事實性宣稱須另行查證 |
158
+ | 🚰 coding-overflow | Grok 4.5 | Kimi K3 → Qwen3 Coder Plus → OpenCode | Codex 額度吃緊時的中量級編碼溢流道;事實性宣稱須另行查證 |
138
159
  | 🗳️ arbitrate | off(選配評審團) | — | 內建意見評審團,重大決定用——預設關閉,要用在 `routing.local.yaml` 開;每評審每輪燒一次額度 |
139
160
 
140
161
  **備選模型**是候選鏈的下一位——首選那家的廠商 CLI 沒裝時,派工就降到它。
@@ -204,8 +225,10 @@ omnilane ui stop # 正常停止
204
225
 
205
226
  ## 📦 安裝
206
227
 
207
- 前置需求:想路由到的廠商 CLI(`codex`、`claude`、`grok`、`agy`)已登入且在
208
- `PATH` 上——**有幾家裝幾家就好**,缺的通道會自動降級。
228
+ 前置需求:想路由到的廠商 CLI(`codex`、`claude`、`grok`、`agy`,另可選
229
+ `kimi`、`qwen`、`opencode`)已登入且在 `PATH` 上——**有幾家裝幾家就好**,
230
+ 缺的通道會自動降級。`openrouter` vendor 是例外:不需要任何 CLI,只要
231
+ `curl` 和環境變數裡的 `OPENROUTER_API_KEY`。
209
232
 
210
233
  最快:`./install.sh` — 自動偵測本機的 CLI、接好技能、列出其餘的外掛安裝指令、
211
234
  印出這台機器的生效路由表,最後問你要不要進入互動設定選單(`--uninstall` 可逆)。
@@ -228,6 +251,27 @@ omnilane ui stop # 正常停止
228
251
  - **Antigravity**:`agy plugin install <本 repo 路徑>`(先用
229
252
  `agy plugin validate` 檢查)
230
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
+
231
275
  ## ⚙️ 自訂設定
232
276
 
233
277
  三層,全部選用:
@@ -293,9 +337,11 @@ codex/claude/grok/gemini 自選 1-4 個評審。開了之後,同一個問題丟
293
337
  ## 🎭 模式
294
338
 
295
339
  - **advise(預設)** — 唯讀工作端。Codex 跑唯讀沙箱;Claude 只給
296
- Read/Glob/Grep;Grok 跑 plan 模式。適合審查、提問、第二意見。
340
+ Read/Glob/Grep;Grok 跑 plan 模式;Kimi 與 OpenCode 鎖各自的唯讀
341
+ plan 模式;OpenRouter 天生只做 advise(純推論)。適合審查、提問、第二意見。
297
342
  - **work** — 允許改檔案,僅限你指定的 `--workdir`。Codex 給
298
343
  workspace-write 沙箱;Claude 自動接受編輯;Gemini 跑 accept-edits 模式。
344
+ `openrouter` vendor 會明確拒絕 work 模式——改檔請走代理式 CLI vendor。
299
345
 
300
346
  ## 🔒 內建安全機制
301
347
 
@@ -344,10 +390,12 @@ codex/claude/grok/gemini 自選 1-4 個評審。開了之後,同一個問題丟
344
390
 
345
391
  ## 🌱 狀態
346
392
 
347
- v0.5.1 Codex `work` 在非 Git 目錄仍可使用,同時以程序群組清理限制卡死,
348
- 並同步所有公開版本來源。它延續 v0.5.0 對安裝器、派工生命週期、job 儲存、
349
- 整體截止時間、診斷與發布 CI 的強化。Grok/Antigravity 指令殼行為仍可能隨
350
- 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。
351
399
 
352
400
  專案文件:[貢獻指南](CONTRIBUTING.md) · [安全政策](SECURITY.md) ·
353
401
  [變更紀錄](CHANGELOG.md)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.1
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
+ });
@@ -64,7 +64,7 @@ _omnilane() {
64
64
  '--dry-run[resolve and print the plan without side effects]' \
65
65
  '--mode[execution mode]:mode:(advise work)' \
66
66
  '--workdir[target directory]:directory:_directories' \
67
- '--vendor[vendor override]:vendor:(codex claude grok gemini exec vote)' \
67
+ '--vendor[vendor override]:vendor:(codex claude grok gemini kimi qwen opencode openrouter exec vote)' \
68
68
  '--model[model override]:model:' \
69
69
  '--effort[reasoning effort]:effort:(low medium high xhigh max)' \
70
70
  '--timeout[per-call seconds]:seconds:' \
@@ -64,7 +64,7 @@ _omnilane() {
64
64
  route|dispatch)
65
65
  case "$prev" in
66
66
  --mode) words="advise work" ;;
67
- --vendor) words="codex claude grok gemini kimi qwen exec vote" ;;
67
+ --vendor) words="codex claude grok gemini kimi qwen opencode openrouter exec vote" ;;
68
68
  --effort) words="low medium high xhigh max" ;;
69
69
  --workdir)
70
70
  COMPREPLY=()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.8.1",
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"
package/routing.yaml CHANGED
@@ -1,6 +1,8 @@
1
1
  # omnilane — single routing table shared by every harness (CC / Codex / Grok / Antigravity)
2
2
  # Format: one lane per line -> lane: vendor model effort [| vendor model effort | off]
3
- # vendor = codex | claude | grok | gemini
3
+ # vendor = codex | claude | grok | gemini | kimi | qwen | opencode | openrouter
4
+ # openrouter is direct-API (needs only OPENROUTER_API_KEY + curl) and advise/consult
5
+ # only — it cannot edit files, so it is not in the default work-capable chains.
4
6
  # effort = codex reasoning effort / claude --effort / agy thinking suffix / (grok: ignored)
5
7
  # Fallback chain: the first candidate whose vendor CLI is installed wins, so the
6
8
  # same table degrades gracefully when you only subscribe to one or two vendors.
@@ -22,7 +24,7 @@ ui-draft: codex gpt-5.6-sol xhigh | claude claude-opus-4-8 high # only w
22
24
  long-context: gemini "Gemini 3.1 Pro (High)" - | claude claude-opus-4-8 high | codex gpt-5.6-sol high # 1M-token synthesis ONLY — never agentic loops (AA agentic 21.4)
23
25
  fast-agentic: gemini "Gemini 3.5 Flash (High)" - | codex gpt-5.6-luna high # speed + agentic tool loops
24
26
  live-search: grok grok-4.5 - | off # native X/web search lane; no real substitute
25
- coding-overflow: grok grok-4.5 - | kimi kimi-k3 - | qwen qwen3-coder-plus - | off # codex-quota relief valve: mid-tier coding; verify factual claims (grok hallucination-prone). kimi/qwen model fields are CLI aliases — adjust to your login.
27
+ coding-overflow: grok grok-4.5 - | kimi kimi-k3 - | qwen qwen3-coder-plus - | opencode - - | off # codex-quota relief valve: mid-tier coding; verify factual claims (grok hallucination-prone). kimi/qwen model fields are CLI aliases — adjust to your login. opencode "-" model = its own configured default.
26
28
  arbitrate: off - - # opinion panel is OPT-IN: it costs one call per voter per round.
27
29
  # Enable: `arbitrate: vote codex,claude,grok -` (any 1-4 of codex/claude/grok/gemini)
28
30
  # Debate round (each voter rebuts the others): set the effort field to 2.