@s-berdia/opencode-router 0.0.0-stage → 1.0.1

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/dist/bin.js ADDED
@@ -0,0 +1,2951 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "util";
5
+
6
+ // src/types.ts
7
+ var PLAN_PROFILES = ["go", "go-plus", "free"];
8
+ function isPlan(value) {
9
+ return typeof value === "string" && PLAN_PROFILES.includes(value);
10
+ }
11
+ var AGENT_ROLES = ["plan", "build", "general", "explore"];
12
+ var INTERNAL_ROLES = ["small_model", "title", "summary", "compaction"];
13
+ var ROLE_NAMES = [...AGENT_ROLES, ...INTERNAL_ROLES];
14
+ function isAgentRole(value) {
15
+ return value !== void 0 && AGENT_ROLES.includes(value);
16
+ }
17
+ function isRoleName(value) {
18
+ return ROLE_NAMES.includes(value);
19
+ }
20
+
21
+ // src/catalog.ts
22
+ import fs2 from "fs";
23
+
24
+ // src/paths.ts
25
+ import os from "os";
26
+ import path from "path";
27
+ import fs from "fs";
28
+
29
+ // node_modules/jsonc-parser/lib/esm/impl/scanner.js
30
+ function createScanner(text, ignoreTrivia = false) {
31
+ const len = text.length;
32
+ let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
33
+ function scanHexDigits(count, exact) {
34
+ let digits = 0;
35
+ let value2 = 0;
36
+ while (digits < count || !exact) {
37
+ let ch = text.charCodeAt(pos);
38
+ if (ch >= 48 && ch <= 57) {
39
+ value2 = value2 * 16 + ch - 48;
40
+ } else if (ch >= 65 && ch <= 70) {
41
+ value2 = value2 * 16 + ch - 65 + 10;
42
+ } else if (ch >= 97 && ch <= 102) {
43
+ value2 = value2 * 16 + ch - 97 + 10;
44
+ } else {
45
+ break;
46
+ }
47
+ pos++;
48
+ digits++;
49
+ }
50
+ if (digits < count) {
51
+ value2 = -1;
52
+ }
53
+ return value2;
54
+ }
55
+ function setPosition(newPosition) {
56
+ pos = newPosition;
57
+ value = "";
58
+ tokenOffset = 0;
59
+ token = 16;
60
+ scanError = 0;
61
+ }
62
+ function scanNumber() {
63
+ let start = pos;
64
+ if (text.charCodeAt(pos) === 48) {
65
+ pos++;
66
+ } else {
67
+ pos++;
68
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
69
+ pos++;
70
+ }
71
+ }
72
+ if (pos < text.length && text.charCodeAt(pos) === 46) {
73
+ pos++;
74
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
75
+ pos++;
76
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
77
+ pos++;
78
+ }
79
+ } else {
80
+ scanError = 3;
81
+ return text.substring(start, pos);
82
+ }
83
+ }
84
+ let end = pos;
85
+ if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
86
+ pos++;
87
+ if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
88
+ pos++;
89
+ }
90
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
91
+ pos++;
92
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
93
+ pos++;
94
+ }
95
+ end = pos;
96
+ } else {
97
+ scanError = 3;
98
+ }
99
+ }
100
+ return text.substring(start, end);
101
+ }
102
+ function scanString() {
103
+ let result = "", start = pos;
104
+ while (true) {
105
+ if (pos >= len) {
106
+ result += text.substring(start, pos);
107
+ scanError = 2;
108
+ break;
109
+ }
110
+ const ch = text.charCodeAt(pos);
111
+ if (ch === 34) {
112
+ result += text.substring(start, pos);
113
+ pos++;
114
+ break;
115
+ }
116
+ if (ch === 92) {
117
+ result += text.substring(start, pos);
118
+ pos++;
119
+ if (pos >= len) {
120
+ scanError = 2;
121
+ break;
122
+ }
123
+ const ch2 = text.charCodeAt(pos++);
124
+ switch (ch2) {
125
+ case 34:
126
+ result += '"';
127
+ break;
128
+ case 92:
129
+ result += "\\";
130
+ break;
131
+ case 47:
132
+ result += "/";
133
+ break;
134
+ case 98:
135
+ result += "\b";
136
+ break;
137
+ case 102:
138
+ result += "\f";
139
+ break;
140
+ case 110:
141
+ result += "\n";
142
+ break;
143
+ case 114:
144
+ result += "\r";
145
+ break;
146
+ case 116:
147
+ result += " ";
148
+ break;
149
+ case 117:
150
+ const ch3 = scanHexDigits(4, true);
151
+ if (ch3 >= 0) {
152
+ result += String.fromCharCode(ch3);
153
+ } else {
154
+ scanError = 4;
155
+ }
156
+ break;
157
+ default:
158
+ scanError = 5;
159
+ }
160
+ start = pos;
161
+ continue;
162
+ }
163
+ if (ch >= 0 && ch <= 31) {
164
+ if (isLineBreak(ch)) {
165
+ result += text.substring(start, pos);
166
+ scanError = 2;
167
+ break;
168
+ } else {
169
+ scanError = 6;
170
+ }
171
+ }
172
+ pos++;
173
+ }
174
+ return result;
175
+ }
176
+ function scanNext() {
177
+ value = "";
178
+ scanError = 0;
179
+ tokenOffset = pos;
180
+ lineStartOffset = lineNumber;
181
+ prevTokenLineStartOffset = tokenLineStartOffset;
182
+ if (pos >= len) {
183
+ tokenOffset = len;
184
+ return token = 17;
185
+ }
186
+ let code = text.charCodeAt(pos);
187
+ if (isWhiteSpace(code)) {
188
+ do {
189
+ pos++;
190
+ value += String.fromCharCode(code);
191
+ code = text.charCodeAt(pos);
192
+ } while (isWhiteSpace(code));
193
+ return token = 15;
194
+ }
195
+ if (isLineBreak(code)) {
196
+ pos++;
197
+ value += String.fromCharCode(code);
198
+ if (code === 13 && text.charCodeAt(pos) === 10) {
199
+ pos++;
200
+ value += "\n";
201
+ }
202
+ lineNumber++;
203
+ tokenLineStartOffset = pos;
204
+ return token = 14;
205
+ }
206
+ switch (code) {
207
+ // tokens: []{}:,
208
+ case 123:
209
+ pos++;
210
+ return token = 1;
211
+ case 125:
212
+ pos++;
213
+ return token = 2;
214
+ case 91:
215
+ pos++;
216
+ return token = 3;
217
+ case 93:
218
+ pos++;
219
+ return token = 4;
220
+ case 58:
221
+ pos++;
222
+ return token = 6;
223
+ case 44:
224
+ pos++;
225
+ return token = 5;
226
+ // strings
227
+ case 34:
228
+ pos++;
229
+ value = scanString();
230
+ return token = 10;
231
+ // comments
232
+ case 47:
233
+ const start = pos - 1;
234
+ if (text.charCodeAt(pos + 1) === 47) {
235
+ pos += 2;
236
+ while (pos < len) {
237
+ if (isLineBreak(text.charCodeAt(pos))) {
238
+ break;
239
+ }
240
+ pos++;
241
+ }
242
+ value = text.substring(start, pos);
243
+ return token = 12;
244
+ }
245
+ if (text.charCodeAt(pos + 1) === 42) {
246
+ pos += 2;
247
+ const safeLength = len - 1;
248
+ let commentClosed = false;
249
+ while (pos < safeLength) {
250
+ const ch = text.charCodeAt(pos);
251
+ if (ch === 42 && text.charCodeAt(pos + 1) === 47) {
252
+ pos += 2;
253
+ commentClosed = true;
254
+ break;
255
+ }
256
+ pos++;
257
+ if (isLineBreak(ch)) {
258
+ if (ch === 13 && text.charCodeAt(pos) === 10) {
259
+ pos++;
260
+ }
261
+ lineNumber++;
262
+ tokenLineStartOffset = pos;
263
+ }
264
+ }
265
+ if (!commentClosed) {
266
+ pos++;
267
+ scanError = 1;
268
+ }
269
+ value = text.substring(start, pos);
270
+ return token = 13;
271
+ }
272
+ value += String.fromCharCode(code);
273
+ pos++;
274
+ return token = 16;
275
+ // numbers
276
+ case 45:
277
+ value += String.fromCharCode(code);
278
+ pos++;
279
+ if (pos === len || !isDigit(text.charCodeAt(pos))) {
280
+ return token = 16;
281
+ }
282
+ // found a minus, followed by a number so
283
+ // we fall through to proceed with scanning
284
+ // numbers
285
+ case 48:
286
+ case 49:
287
+ case 50:
288
+ case 51:
289
+ case 52:
290
+ case 53:
291
+ case 54:
292
+ case 55:
293
+ case 56:
294
+ case 57:
295
+ value += scanNumber();
296
+ return token = 11;
297
+ // literals and unknown symbols
298
+ default:
299
+ while (pos < len && isUnknownContentCharacter(code)) {
300
+ pos++;
301
+ code = text.charCodeAt(pos);
302
+ }
303
+ if (tokenOffset !== pos) {
304
+ value = text.substring(tokenOffset, pos);
305
+ switch (value) {
306
+ case "true":
307
+ return token = 8;
308
+ case "false":
309
+ return token = 9;
310
+ case "null":
311
+ return token = 7;
312
+ }
313
+ return token = 16;
314
+ }
315
+ value += String.fromCharCode(code);
316
+ pos++;
317
+ return token = 16;
318
+ }
319
+ }
320
+ function isUnknownContentCharacter(code) {
321
+ if (isWhiteSpace(code) || isLineBreak(code)) {
322
+ return false;
323
+ }
324
+ switch (code) {
325
+ case 125:
326
+ case 93:
327
+ case 123:
328
+ case 91:
329
+ case 34:
330
+ case 58:
331
+ case 44:
332
+ case 47:
333
+ return false;
334
+ }
335
+ return true;
336
+ }
337
+ function scanNextNonTrivia() {
338
+ let result;
339
+ do {
340
+ result = scanNext();
341
+ } while (result >= 12 && result <= 15);
342
+ return result;
343
+ }
344
+ return {
345
+ setPosition,
346
+ getPosition: () => pos,
347
+ scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
348
+ getToken: () => token,
349
+ getTokenValue: () => value,
350
+ getTokenOffset: () => tokenOffset,
351
+ getTokenLength: () => pos - tokenOffset,
352
+ getTokenStartLine: () => lineStartOffset,
353
+ getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
354
+ getTokenError: () => scanError
355
+ };
356
+ }
357
+ function isWhiteSpace(ch) {
358
+ return ch === 32 || ch === 9;
359
+ }
360
+ function isLineBreak(ch) {
361
+ return ch === 10 || ch === 13;
362
+ }
363
+ function isDigit(ch) {
364
+ return ch >= 48 && ch <= 57;
365
+ }
366
+ var CharacterCodes;
367
+ (function(CharacterCodes2) {
368
+ CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed";
369
+ CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn";
370
+ CharacterCodes2[CharacterCodes2["space"] = 32] = "space";
371
+ CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0";
372
+ CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1";
373
+ CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2";
374
+ CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3";
375
+ CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4";
376
+ CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5";
377
+ CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6";
378
+ CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7";
379
+ CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8";
380
+ CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9";
381
+ CharacterCodes2[CharacterCodes2["a"] = 97] = "a";
382
+ CharacterCodes2[CharacterCodes2["b"] = 98] = "b";
383
+ CharacterCodes2[CharacterCodes2["c"] = 99] = "c";
384
+ CharacterCodes2[CharacterCodes2["d"] = 100] = "d";
385
+ CharacterCodes2[CharacterCodes2["e"] = 101] = "e";
386
+ CharacterCodes2[CharacterCodes2["f"] = 102] = "f";
387
+ CharacterCodes2[CharacterCodes2["g"] = 103] = "g";
388
+ CharacterCodes2[CharacterCodes2["h"] = 104] = "h";
389
+ CharacterCodes2[CharacterCodes2["i"] = 105] = "i";
390
+ CharacterCodes2[CharacterCodes2["j"] = 106] = "j";
391
+ CharacterCodes2[CharacterCodes2["k"] = 107] = "k";
392
+ CharacterCodes2[CharacterCodes2["l"] = 108] = "l";
393
+ CharacterCodes2[CharacterCodes2["m"] = 109] = "m";
394
+ CharacterCodes2[CharacterCodes2["n"] = 110] = "n";
395
+ CharacterCodes2[CharacterCodes2["o"] = 111] = "o";
396
+ CharacterCodes2[CharacterCodes2["p"] = 112] = "p";
397
+ CharacterCodes2[CharacterCodes2["q"] = 113] = "q";
398
+ CharacterCodes2[CharacterCodes2["r"] = 114] = "r";
399
+ CharacterCodes2[CharacterCodes2["s"] = 115] = "s";
400
+ CharacterCodes2[CharacterCodes2["t"] = 116] = "t";
401
+ CharacterCodes2[CharacterCodes2["u"] = 117] = "u";
402
+ CharacterCodes2[CharacterCodes2["v"] = 118] = "v";
403
+ CharacterCodes2[CharacterCodes2["w"] = 119] = "w";
404
+ CharacterCodes2[CharacterCodes2["x"] = 120] = "x";
405
+ CharacterCodes2[CharacterCodes2["y"] = 121] = "y";
406
+ CharacterCodes2[CharacterCodes2["z"] = 122] = "z";
407
+ CharacterCodes2[CharacterCodes2["A"] = 65] = "A";
408
+ CharacterCodes2[CharacterCodes2["B"] = 66] = "B";
409
+ CharacterCodes2[CharacterCodes2["C"] = 67] = "C";
410
+ CharacterCodes2[CharacterCodes2["D"] = 68] = "D";
411
+ CharacterCodes2[CharacterCodes2["E"] = 69] = "E";
412
+ CharacterCodes2[CharacterCodes2["F"] = 70] = "F";
413
+ CharacterCodes2[CharacterCodes2["G"] = 71] = "G";
414
+ CharacterCodes2[CharacterCodes2["H"] = 72] = "H";
415
+ CharacterCodes2[CharacterCodes2["I"] = 73] = "I";
416
+ CharacterCodes2[CharacterCodes2["J"] = 74] = "J";
417
+ CharacterCodes2[CharacterCodes2["K"] = 75] = "K";
418
+ CharacterCodes2[CharacterCodes2["L"] = 76] = "L";
419
+ CharacterCodes2[CharacterCodes2["M"] = 77] = "M";
420
+ CharacterCodes2[CharacterCodes2["N"] = 78] = "N";
421
+ CharacterCodes2[CharacterCodes2["O"] = 79] = "O";
422
+ CharacterCodes2[CharacterCodes2["P"] = 80] = "P";
423
+ CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q";
424
+ CharacterCodes2[CharacterCodes2["R"] = 82] = "R";
425
+ CharacterCodes2[CharacterCodes2["S"] = 83] = "S";
426
+ CharacterCodes2[CharacterCodes2["T"] = 84] = "T";
427
+ CharacterCodes2[CharacterCodes2["U"] = 85] = "U";
428
+ CharacterCodes2[CharacterCodes2["V"] = 86] = "V";
429
+ CharacterCodes2[CharacterCodes2["W"] = 87] = "W";
430
+ CharacterCodes2[CharacterCodes2["X"] = 88] = "X";
431
+ CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y";
432
+ CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z";
433
+ CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk";
434
+ CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash";
435
+ CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace";
436
+ CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket";
437
+ CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon";
438
+ CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma";
439
+ CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot";
440
+ CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote";
441
+ CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus";
442
+ CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace";
443
+ CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket";
444
+ CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus";
445
+ CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash";
446
+ CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed";
447
+ CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab";
448
+ })(CharacterCodes || (CharacterCodes = {}));
449
+
450
+ // node_modules/jsonc-parser/lib/esm/impl/string-intern.js
451
+ var cachedSpaces = new Array(20).fill(0).map((_, index) => {
452
+ return " ".repeat(index);
453
+ });
454
+ var maxCachedValues = 200;
455
+ var cachedBreakLinesWithSpaces = {
456
+ " ": {
457
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
458
+ return "\n" + " ".repeat(index);
459
+ }),
460
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
461
+ return "\r" + " ".repeat(index);
462
+ }),
463
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
464
+ return "\r\n" + " ".repeat(index);
465
+ })
466
+ },
467
+ " ": {
468
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
469
+ return "\n" + " ".repeat(index);
470
+ }),
471
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
472
+ return "\r" + " ".repeat(index);
473
+ }),
474
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
475
+ return "\r\n" + " ".repeat(index);
476
+ })
477
+ }
478
+ };
479
+ var supportedEols = ["\n", "\r", "\r\n"];
480
+
481
+ // node_modules/jsonc-parser/lib/esm/impl/format.js
482
+ function format(documentText, range, options) {
483
+ let initialIndentLevel;
484
+ let formatText;
485
+ let formatTextStart;
486
+ let rangeStart;
487
+ let rangeEnd;
488
+ if (range) {
489
+ rangeStart = range.offset;
490
+ rangeEnd = rangeStart + range.length;
491
+ formatTextStart = rangeStart;
492
+ while (formatTextStart > 0 && !isEOL(documentText, formatTextStart - 1)) {
493
+ formatTextStart--;
494
+ }
495
+ let endOffset = rangeEnd;
496
+ while (endOffset < documentText.length && !isEOL(documentText, endOffset)) {
497
+ endOffset++;
498
+ }
499
+ formatText = documentText.substring(formatTextStart, endOffset);
500
+ initialIndentLevel = computeIndentLevel(formatText, options);
501
+ } else {
502
+ formatText = documentText;
503
+ initialIndentLevel = 0;
504
+ formatTextStart = 0;
505
+ rangeStart = 0;
506
+ rangeEnd = documentText.length;
507
+ }
508
+ const eol = getEOL(options, documentText);
509
+ const eolFastPathSupported = supportedEols.includes(eol);
510
+ let numberLineBreaks = 0;
511
+ let indentLevel = 0;
512
+ let indentValue;
513
+ if (options.insertSpaces) {
514
+ indentValue = cachedSpaces[options.tabSize || 4] ?? repeat(cachedSpaces[1], options.tabSize || 4);
515
+ } else {
516
+ indentValue = " ";
517
+ }
518
+ const indentType = indentValue === " " ? " " : " ";
519
+ let scanner = createScanner(formatText, false);
520
+ let hasError = false;
521
+ function newLinesAndIndent() {
522
+ if (numberLineBreaks > 1) {
523
+ return repeat(eol, numberLineBreaks) + repeat(indentValue, initialIndentLevel + indentLevel);
524
+ }
525
+ const amountOfSpaces = indentValue.length * (initialIndentLevel + indentLevel);
526
+ if (!eolFastPathSupported || amountOfSpaces > cachedBreakLinesWithSpaces[indentType][eol].length) {
527
+ return eol + repeat(indentValue, initialIndentLevel + indentLevel);
528
+ }
529
+ if (amountOfSpaces <= 0) {
530
+ return eol;
531
+ }
532
+ return cachedBreakLinesWithSpaces[indentType][eol][amountOfSpaces];
533
+ }
534
+ function scanNext() {
535
+ let token = scanner.scan();
536
+ numberLineBreaks = 0;
537
+ while (token === 15 || token === 14) {
538
+ if (token === 14 && options.keepLines) {
539
+ numberLineBreaks += 1;
540
+ } else if (token === 14) {
541
+ numberLineBreaks = 1;
542
+ }
543
+ token = scanner.scan();
544
+ }
545
+ hasError = token === 16 || scanner.getTokenError() !== 0;
546
+ return token;
547
+ }
548
+ const editOperations = [];
549
+ function addEdit(text, startOffset, endOffset) {
550
+ if (!hasError && (!range || startOffset < rangeEnd && endOffset > rangeStart) && documentText.substring(startOffset, endOffset) !== text) {
551
+ editOperations.push({ offset: startOffset, length: endOffset - startOffset, content: text });
552
+ }
553
+ }
554
+ let firstToken = scanNext();
555
+ if (options.keepLines && numberLineBreaks > 0) {
556
+ addEdit(repeat(eol, numberLineBreaks), 0, 0);
557
+ }
558
+ if (firstToken !== 17) {
559
+ let firstTokenStart = scanner.getTokenOffset() + formatTextStart;
560
+ let initialIndent = indentValue.length * initialIndentLevel < 20 && options.insertSpaces ? cachedSpaces[indentValue.length * initialIndentLevel] : repeat(indentValue, initialIndentLevel);
561
+ addEdit(initialIndent, formatTextStart, firstTokenStart);
562
+ }
563
+ while (firstToken !== 17) {
564
+ let firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart;
565
+ let secondToken = scanNext();
566
+ let replaceContent = "";
567
+ let needsLineBreak = false;
568
+ while (numberLineBreaks === 0 && (secondToken === 12 || secondToken === 13)) {
569
+ let commentTokenStart = scanner.getTokenOffset() + formatTextStart;
570
+ addEdit(cachedSpaces[1], firstTokenEnd, commentTokenStart);
571
+ firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart;
572
+ needsLineBreak = secondToken === 12;
573
+ replaceContent = needsLineBreak ? newLinesAndIndent() : "";
574
+ secondToken = scanNext();
575
+ }
576
+ if (secondToken === 2) {
577
+ if (firstToken !== 1) {
578
+ indentLevel--;
579
+ }
580
+ ;
581
+ if (options.keepLines && numberLineBreaks > 0 || !options.keepLines && firstToken !== 1) {
582
+ replaceContent = newLinesAndIndent();
583
+ } else if (options.keepLines) {
584
+ replaceContent = cachedSpaces[1];
585
+ }
586
+ } else if (secondToken === 4) {
587
+ if (firstToken !== 3) {
588
+ indentLevel--;
589
+ }
590
+ ;
591
+ if (options.keepLines && numberLineBreaks > 0 || !options.keepLines && firstToken !== 3) {
592
+ replaceContent = newLinesAndIndent();
593
+ } else if (options.keepLines) {
594
+ replaceContent = cachedSpaces[1];
595
+ }
596
+ } else {
597
+ switch (firstToken) {
598
+ case 3:
599
+ case 1:
600
+ indentLevel++;
601
+ if (options.keepLines && numberLineBreaks > 0 || !options.keepLines) {
602
+ replaceContent = newLinesAndIndent();
603
+ } else {
604
+ replaceContent = cachedSpaces[1];
605
+ }
606
+ break;
607
+ case 5:
608
+ if (options.keepLines && numberLineBreaks > 0 || !options.keepLines) {
609
+ replaceContent = newLinesAndIndent();
610
+ } else {
611
+ replaceContent = cachedSpaces[1];
612
+ }
613
+ break;
614
+ case 12:
615
+ replaceContent = newLinesAndIndent();
616
+ break;
617
+ case 13:
618
+ if (numberLineBreaks > 0) {
619
+ replaceContent = newLinesAndIndent();
620
+ } else if (!needsLineBreak) {
621
+ replaceContent = cachedSpaces[1];
622
+ }
623
+ break;
624
+ case 6:
625
+ if (options.keepLines && numberLineBreaks > 0) {
626
+ replaceContent = newLinesAndIndent();
627
+ } else if (!needsLineBreak) {
628
+ replaceContent = cachedSpaces[1];
629
+ }
630
+ break;
631
+ case 10:
632
+ if (options.keepLines && numberLineBreaks > 0) {
633
+ replaceContent = newLinesAndIndent();
634
+ } else if (secondToken === 6 && !needsLineBreak) {
635
+ replaceContent = "";
636
+ }
637
+ break;
638
+ case 7:
639
+ case 8:
640
+ case 9:
641
+ case 11:
642
+ case 2:
643
+ case 4:
644
+ if (options.keepLines && numberLineBreaks > 0) {
645
+ replaceContent = newLinesAndIndent();
646
+ } else {
647
+ if ((secondToken === 12 || secondToken === 13) && !needsLineBreak) {
648
+ replaceContent = cachedSpaces[1];
649
+ } else if (secondToken !== 5 && secondToken !== 17) {
650
+ hasError = true;
651
+ }
652
+ }
653
+ break;
654
+ case 16:
655
+ hasError = true;
656
+ break;
657
+ }
658
+ if (numberLineBreaks > 0 && (secondToken === 12 || secondToken === 13)) {
659
+ replaceContent = newLinesAndIndent();
660
+ }
661
+ }
662
+ if (secondToken === 17) {
663
+ if (options.keepLines && numberLineBreaks > 0) {
664
+ replaceContent = newLinesAndIndent();
665
+ } else {
666
+ replaceContent = options.insertFinalNewline ? eol : "";
667
+ }
668
+ }
669
+ const secondTokenStart = scanner.getTokenOffset() + formatTextStart;
670
+ addEdit(replaceContent, firstTokenEnd, secondTokenStart);
671
+ firstToken = secondToken;
672
+ }
673
+ return editOperations;
674
+ }
675
+ function repeat(s, count) {
676
+ let result = "";
677
+ for (let i = 0; i < count; i++) {
678
+ result += s;
679
+ }
680
+ return result;
681
+ }
682
+ function computeIndentLevel(content, options) {
683
+ let i = 0;
684
+ let nChars = 0;
685
+ const tabSize = options.tabSize || 4;
686
+ while (i < content.length) {
687
+ let ch = content.charAt(i);
688
+ if (ch === cachedSpaces[1]) {
689
+ nChars++;
690
+ } else if (ch === " ") {
691
+ nChars += tabSize;
692
+ } else {
693
+ break;
694
+ }
695
+ i++;
696
+ }
697
+ return Math.floor(nChars / tabSize);
698
+ }
699
+ function getEOL(options, text) {
700
+ for (let i = 0; i < text.length; i++) {
701
+ const ch = text.charAt(i);
702
+ if (ch === "\r") {
703
+ if (i + 1 < text.length && text.charAt(i + 1) === "\n") {
704
+ return "\r\n";
705
+ }
706
+ return "\r";
707
+ } else if (ch === "\n") {
708
+ return "\n";
709
+ }
710
+ }
711
+ return options && options.eol || "\n";
712
+ }
713
+ function isEOL(text, offset) {
714
+ return "\r\n".indexOf(text.charAt(offset)) !== -1;
715
+ }
716
+
717
+ // node_modules/jsonc-parser/lib/esm/impl/parser.js
718
+ var ParseOptions;
719
+ (function(ParseOptions2) {
720
+ ParseOptions2.DEFAULT = {
721
+ allowTrailingComma: false
722
+ };
723
+ })(ParseOptions || (ParseOptions = {}));
724
+ function parse(text, errors = [], options = ParseOptions.DEFAULT) {
725
+ let currentProperty = null;
726
+ let currentParent = [];
727
+ const previousParents = [];
728
+ function onValue(value) {
729
+ if (Array.isArray(currentParent)) {
730
+ currentParent.push(value);
731
+ } else if (currentProperty !== null) {
732
+ currentParent[currentProperty] = value;
733
+ }
734
+ }
735
+ const visitor = {
736
+ onObjectBegin: () => {
737
+ const object = {};
738
+ onValue(object);
739
+ previousParents.push(currentParent);
740
+ currentParent = object;
741
+ currentProperty = null;
742
+ },
743
+ onObjectProperty: (name) => {
744
+ currentProperty = name;
745
+ },
746
+ onObjectEnd: () => {
747
+ currentParent = previousParents.pop();
748
+ },
749
+ onArrayBegin: () => {
750
+ const array = [];
751
+ onValue(array);
752
+ previousParents.push(currentParent);
753
+ currentParent = array;
754
+ currentProperty = null;
755
+ },
756
+ onArrayEnd: () => {
757
+ currentParent = previousParents.pop();
758
+ },
759
+ onLiteralValue: onValue,
760
+ onError: (error, offset, length) => {
761
+ errors.push({ error, offset, length });
762
+ }
763
+ };
764
+ visit(text, visitor, options);
765
+ return currentParent[0];
766
+ }
767
+ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
768
+ let currentParent = { type: "array", offset: -1, length: -1, children: [], parent: void 0 };
769
+ function ensurePropertyComplete(endOffset) {
770
+ if (currentParent.type === "property") {
771
+ currentParent.length = endOffset - currentParent.offset;
772
+ currentParent = currentParent.parent;
773
+ }
774
+ }
775
+ function onValue(valueNode) {
776
+ currentParent.children.push(valueNode);
777
+ return valueNode;
778
+ }
779
+ const visitor = {
780
+ onObjectBegin: (offset) => {
781
+ currentParent = onValue({ type: "object", offset, length: -1, parent: currentParent, children: [] });
782
+ },
783
+ onObjectProperty: (name, offset, length) => {
784
+ currentParent = onValue({ type: "property", offset, length: -1, parent: currentParent, children: [] });
785
+ currentParent.children.push({ type: "string", value: name, offset, length, parent: currentParent });
786
+ },
787
+ onObjectEnd: (offset, length) => {
788
+ ensurePropertyComplete(offset + length);
789
+ currentParent.length = offset + length - currentParent.offset;
790
+ currentParent = currentParent.parent;
791
+ ensurePropertyComplete(offset + length);
792
+ },
793
+ onArrayBegin: (offset, length) => {
794
+ currentParent = onValue({ type: "array", offset, length: -1, parent: currentParent, children: [] });
795
+ },
796
+ onArrayEnd: (offset, length) => {
797
+ currentParent.length = offset + length - currentParent.offset;
798
+ currentParent = currentParent.parent;
799
+ ensurePropertyComplete(offset + length);
800
+ },
801
+ onLiteralValue: (value, offset, length) => {
802
+ onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
803
+ ensurePropertyComplete(offset + length);
804
+ },
805
+ onSeparator: (sep, offset, length) => {
806
+ if (currentParent.type === "property") {
807
+ if (sep === ":") {
808
+ currentParent.colonOffset = offset;
809
+ } else if (sep === ",") {
810
+ ensurePropertyComplete(offset);
811
+ }
812
+ }
813
+ },
814
+ onError: (error, offset, length) => {
815
+ errors.push({ error, offset, length });
816
+ }
817
+ };
818
+ visit(text, visitor, options);
819
+ const result = currentParent.children[0];
820
+ if (result) {
821
+ delete result.parent;
822
+ }
823
+ return result;
824
+ }
825
+ function findNodeAtLocation(root, path5) {
826
+ if (!root) {
827
+ return void 0;
828
+ }
829
+ let node = root;
830
+ for (let segment of path5) {
831
+ if (typeof segment === "string") {
832
+ if (node.type !== "object" || !Array.isArray(node.children)) {
833
+ return void 0;
834
+ }
835
+ let found = false;
836
+ for (const propertyNode of node.children) {
837
+ if (Array.isArray(propertyNode.children) && propertyNode.children[0].value === segment && propertyNode.children.length === 2) {
838
+ node = propertyNode.children[1];
839
+ found = true;
840
+ break;
841
+ }
842
+ }
843
+ if (!found) {
844
+ return void 0;
845
+ }
846
+ } else {
847
+ const index = segment;
848
+ if (node.type !== "array" || index < 0 || !Array.isArray(node.children) || index >= node.children.length) {
849
+ return void 0;
850
+ }
851
+ node = node.children[index];
852
+ }
853
+ }
854
+ return node;
855
+ }
856
+ function visit(text, visitor, options = ParseOptions.DEFAULT) {
857
+ const _scanner = createScanner(text, false);
858
+ const _jsonPath = [];
859
+ let suppressedCallbacks = 0;
860
+ function toNoArgVisit(visitFunction) {
861
+ return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
862
+ }
863
+ function toOneArgVisit(visitFunction) {
864
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
865
+ }
866
+ function toOneArgVisitWithPath(visitFunction) {
867
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
868
+ }
869
+ function toBeginVisit(visitFunction) {
870
+ return visitFunction ? () => {
871
+ if (suppressedCallbacks > 0) {
872
+ suppressedCallbacks++;
873
+ } else {
874
+ let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
875
+ if (cbReturn === false) {
876
+ suppressedCallbacks = 1;
877
+ }
878
+ }
879
+ } : () => true;
880
+ }
881
+ function toEndVisit(visitFunction) {
882
+ return visitFunction ? () => {
883
+ if (suppressedCallbacks > 0) {
884
+ suppressedCallbacks--;
885
+ }
886
+ if (suppressedCallbacks === 0) {
887
+ visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
888
+ }
889
+ } : () => true;
890
+ }
891
+ const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
892
+ const disallowComments = options && options.disallowComments;
893
+ const allowTrailingComma = options && options.allowTrailingComma;
894
+ function scanNext() {
895
+ while (true) {
896
+ const token = _scanner.scan();
897
+ switch (_scanner.getTokenError()) {
898
+ case 4:
899
+ handleError(
900
+ 14
901
+ /* ParseErrorCode.InvalidUnicode */
902
+ );
903
+ break;
904
+ case 5:
905
+ handleError(
906
+ 15
907
+ /* ParseErrorCode.InvalidEscapeCharacter */
908
+ );
909
+ break;
910
+ case 3:
911
+ handleError(
912
+ 13
913
+ /* ParseErrorCode.UnexpectedEndOfNumber */
914
+ );
915
+ break;
916
+ case 1:
917
+ if (!disallowComments) {
918
+ handleError(
919
+ 11
920
+ /* ParseErrorCode.UnexpectedEndOfComment */
921
+ );
922
+ }
923
+ break;
924
+ case 2:
925
+ handleError(
926
+ 12
927
+ /* ParseErrorCode.UnexpectedEndOfString */
928
+ );
929
+ break;
930
+ case 6:
931
+ handleError(
932
+ 16
933
+ /* ParseErrorCode.InvalidCharacter */
934
+ );
935
+ break;
936
+ }
937
+ switch (token) {
938
+ case 12:
939
+ case 13:
940
+ if (disallowComments) {
941
+ handleError(
942
+ 10
943
+ /* ParseErrorCode.InvalidCommentToken */
944
+ );
945
+ } else {
946
+ onComment();
947
+ }
948
+ break;
949
+ case 16:
950
+ handleError(
951
+ 1
952
+ /* ParseErrorCode.InvalidSymbol */
953
+ );
954
+ break;
955
+ case 15:
956
+ case 14:
957
+ break;
958
+ default:
959
+ return token;
960
+ }
961
+ }
962
+ }
963
+ function handleError(error, skipUntilAfter = [], skipUntil = []) {
964
+ onError(error);
965
+ if (skipUntilAfter.length + skipUntil.length > 0) {
966
+ let token = _scanner.getToken();
967
+ while (token !== 17) {
968
+ if (skipUntilAfter.indexOf(token) !== -1) {
969
+ scanNext();
970
+ break;
971
+ } else if (skipUntil.indexOf(token) !== -1) {
972
+ break;
973
+ }
974
+ token = scanNext();
975
+ }
976
+ }
977
+ }
978
+ function parseString(isValue) {
979
+ const value = _scanner.getTokenValue();
980
+ if (isValue) {
981
+ onLiteralValue(value);
982
+ } else {
983
+ onObjectProperty(value);
984
+ _jsonPath.push(value);
985
+ }
986
+ scanNext();
987
+ return true;
988
+ }
989
+ function parseLiteral() {
990
+ switch (_scanner.getToken()) {
991
+ case 11:
992
+ const tokenValue = _scanner.getTokenValue();
993
+ let value = Number(tokenValue);
994
+ if (isNaN(value)) {
995
+ handleError(
996
+ 2
997
+ /* ParseErrorCode.InvalidNumberFormat */
998
+ );
999
+ value = 0;
1000
+ }
1001
+ onLiteralValue(value);
1002
+ break;
1003
+ case 7:
1004
+ onLiteralValue(null);
1005
+ break;
1006
+ case 8:
1007
+ onLiteralValue(true);
1008
+ break;
1009
+ case 9:
1010
+ onLiteralValue(false);
1011
+ break;
1012
+ default:
1013
+ return false;
1014
+ }
1015
+ scanNext();
1016
+ return true;
1017
+ }
1018
+ function parseProperty() {
1019
+ if (_scanner.getToken() !== 10) {
1020
+ handleError(3, [], [
1021
+ 2,
1022
+ 5
1023
+ /* SyntaxKind.CommaToken */
1024
+ ]);
1025
+ return false;
1026
+ }
1027
+ parseString(false);
1028
+ if (_scanner.getToken() === 6) {
1029
+ onSeparator(":");
1030
+ scanNext();
1031
+ if (!parseValue()) {
1032
+ handleError(4, [], [
1033
+ 2,
1034
+ 5
1035
+ /* SyntaxKind.CommaToken */
1036
+ ]);
1037
+ }
1038
+ } else {
1039
+ handleError(5, [], [
1040
+ 2,
1041
+ 5
1042
+ /* SyntaxKind.CommaToken */
1043
+ ]);
1044
+ }
1045
+ _jsonPath.pop();
1046
+ return true;
1047
+ }
1048
+ function parseObject() {
1049
+ onObjectBegin();
1050
+ scanNext();
1051
+ let needsComma = false;
1052
+ while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
1053
+ if (_scanner.getToken() === 5) {
1054
+ if (!needsComma) {
1055
+ handleError(4, [], []);
1056
+ }
1057
+ onSeparator(",");
1058
+ scanNext();
1059
+ if (_scanner.getToken() === 2 && allowTrailingComma) {
1060
+ break;
1061
+ }
1062
+ } else if (needsComma) {
1063
+ handleError(6, [], []);
1064
+ }
1065
+ if (!parseProperty()) {
1066
+ handleError(4, [], [
1067
+ 2,
1068
+ 5
1069
+ /* SyntaxKind.CommaToken */
1070
+ ]);
1071
+ }
1072
+ needsComma = true;
1073
+ }
1074
+ onObjectEnd();
1075
+ if (_scanner.getToken() !== 2) {
1076
+ handleError(7, [
1077
+ 2
1078
+ /* SyntaxKind.CloseBraceToken */
1079
+ ], []);
1080
+ } else {
1081
+ scanNext();
1082
+ }
1083
+ return true;
1084
+ }
1085
+ function parseArray() {
1086
+ onArrayBegin();
1087
+ scanNext();
1088
+ let isFirstElement = true;
1089
+ let needsComma = false;
1090
+ while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
1091
+ if (_scanner.getToken() === 5) {
1092
+ if (!needsComma) {
1093
+ handleError(4, [], []);
1094
+ }
1095
+ onSeparator(",");
1096
+ scanNext();
1097
+ if (_scanner.getToken() === 4 && allowTrailingComma) {
1098
+ break;
1099
+ }
1100
+ } else if (needsComma) {
1101
+ handleError(6, [], []);
1102
+ }
1103
+ if (isFirstElement) {
1104
+ _jsonPath.push(0);
1105
+ isFirstElement = false;
1106
+ } else {
1107
+ _jsonPath[_jsonPath.length - 1]++;
1108
+ }
1109
+ if (!parseValue()) {
1110
+ handleError(4, [], [
1111
+ 4,
1112
+ 5
1113
+ /* SyntaxKind.CommaToken */
1114
+ ]);
1115
+ }
1116
+ needsComma = true;
1117
+ }
1118
+ onArrayEnd();
1119
+ if (!isFirstElement) {
1120
+ _jsonPath.pop();
1121
+ }
1122
+ if (_scanner.getToken() !== 4) {
1123
+ handleError(8, [
1124
+ 4
1125
+ /* SyntaxKind.CloseBracketToken */
1126
+ ], []);
1127
+ } else {
1128
+ scanNext();
1129
+ }
1130
+ return true;
1131
+ }
1132
+ function parseValue() {
1133
+ switch (_scanner.getToken()) {
1134
+ case 3:
1135
+ return parseArray();
1136
+ case 1:
1137
+ return parseObject();
1138
+ case 10:
1139
+ return parseString(true);
1140
+ default:
1141
+ return parseLiteral();
1142
+ }
1143
+ }
1144
+ scanNext();
1145
+ if (_scanner.getToken() === 17) {
1146
+ if (options.allowEmptyContent) {
1147
+ return true;
1148
+ }
1149
+ handleError(4, [], []);
1150
+ return false;
1151
+ }
1152
+ if (!parseValue()) {
1153
+ handleError(4, [], []);
1154
+ return false;
1155
+ }
1156
+ if (_scanner.getToken() !== 17) {
1157
+ handleError(9, [], []);
1158
+ }
1159
+ return true;
1160
+ }
1161
+ function getNodeType(value) {
1162
+ switch (typeof value) {
1163
+ case "boolean":
1164
+ return "boolean";
1165
+ case "number":
1166
+ return "number";
1167
+ case "string":
1168
+ return "string";
1169
+ case "object": {
1170
+ if (!value) {
1171
+ return "null";
1172
+ } else if (Array.isArray(value)) {
1173
+ return "array";
1174
+ }
1175
+ return "object";
1176
+ }
1177
+ default:
1178
+ return "null";
1179
+ }
1180
+ }
1181
+
1182
+ // node_modules/jsonc-parser/lib/esm/impl/edit.js
1183
+ function setProperty(text, originalPath, value, options) {
1184
+ const path5 = originalPath.slice();
1185
+ const errors = [];
1186
+ const root = parseTree(text, errors);
1187
+ let parent = void 0;
1188
+ let lastSegment = void 0;
1189
+ while (path5.length > 0) {
1190
+ lastSegment = path5.pop();
1191
+ parent = findNodeAtLocation(root, path5);
1192
+ if (parent === void 0 && value !== void 0) {
1193
+ if (typeof lastSegment === "string") {
1194
+ value = { [lastSegment]: value };
1195
+ } else {
1196
+ value = [value];
1197
+ }
1198
+ } else {
1199
+ break;
1200
+ }
1201
+ }
1202
+ if (!parent) {
1203
+ if (value === void 0) {
1204
+ throw new Error("Can not delete in empty document");
1205
+ }
1206
+ return withFormatting(text, { offset: root ? root.offset : 0, length: root ? root.length : 0, content: JSON.stringify(value) }, options);
1207
+ } else if (parent.type === "object" && typeof lastSegment === "string" && Array.isArray(parent.children)) {
1208
+ const existing = findNodeAtLocation(parent, [lastSegment]);
1209
+ if (existing !== void 0) {
1210
+ if (value === void 0) {
1211
+ if (!existing.parent) {
1212
+ throw new Error("Malformed AST");
1213
+ }
1214
+ const propertyIndex = parent.children.indexOf(existing.parent);
1215
+ let removeBegin;
1216
+ let removeEnd = existing.parent.offset + existing.parent.length;
1217
+ if (propertyIndex > 0) {
1218
+ let previous = parent.children[propertyIndex - 1];
1219
+ removeBegin = previous.offset + previous.length;
1220
+ } else {
1221
+ removeBegin = parent.offset + 1;
1222
+ if (parent.children.length > 1) {
1223
+ let next = parent.children[1];
1224
+ removeEnd = next.offset;
1225
+ }
1226
+ }
1227
+ return withFormatting(text, { offset: removeBegin, length: removeEnd - removeBegin, content: "" }, options);
1228
+ } else {
1229
+ return withFormatting(text, { offset: existing.offset, length: existing.length, content: JSON.stringify(value) }, options);
1230
+ }
1231
+ } else {
1232
+ if (value === void 0) {
1233
+ return [];
1234
+ }
1235
+ const newProperty = `${JSON.stringify(lastSegment)}: ${JSON.stringify(value)}`;
1236
+ const index = options.getInsertionIndex ? options.getInsertionIndex(parent.children.map((p) => p.children[0].value)) : parent.children.length;
1237
+ let edit;
1238
+ if (index > 0) {
1239
+ let previous = parent.children[index - 1];
1240
+ edit = { offset: previous.offset + previous.length, length: 0, content: "," + newProperty };
1241
+ } else if (parent.children.length === 0) {
1242
+ edit = { offset: parent.offset + 1, length: 0, content: newProperty };
1243
+ } else {
1244
+ edit = { offset: parent.offset + 1, length: 0, content: newProperty + "," };
1245
+ }
1246
+ return withFormatting(text, edit, options);
1247
+ }
1248
+ } else if (parent.type === "array" && typeof lastSegment === "number" && Array.isArray(parent.children)) {
1249
+ const insertIndex = lastSegment;
1250
+ if (insertIndex === -1) {
1251
+ const newProperty = `${JSON.stringify(value)}`;
1252
+ let edit;
1253
+ if (parent.children.length === 0) {
1254
+ edit = { offset: parent.offset + 1, length: 0, content: newProperty };
1255
+ } else {
1256
+ const previous = parent.children[parent.children.length - 1];
1257
+ edit = { offset: previous.offset + previous.length, length: 0, content: "," + newProperty };
1258
+ }
1259
+ return withFormatting(text, edit, options);
1260
+ } else if (value === void 0 && parent.children.length >= 0) {
1261
+ const removalIndex = lastSegment;
1262
+ const toRemove = parent.children[removalIndex];
1263
+ let edit;
1264
+ if (parent.children.length === 1) {
1265
+ edit = { offset: parent.offset + 1, length: parent.length - 2, content: "" };
1266
+ } else if (parent.children.length - 1 === removalIndex) {
1267
+ let previous = parent.children[removalIndex - 1];
1268
+ let offset = previous.offset + previous.length;
1269
+ let parentEndOffset = parent.offset + parent.length;
1270
+ edit = { offset, length: parentEndOffset - 2 - offset, content: "" };
1271
+ } else {
1272
+ edit = { offset: toRemove.offset, length: parent.children[removalIndex + 1].offset - toRemove.offset, content: "" };
1273
+ }
1274
+ return withFormatting(text, edit, options);
1275
+ } else if (value !== void 0) {
1276
+ let edit;
1277
+ const newProperty = `${JSON.stringify(value)}`;
1278
+ if (!options.isArrayInsertion && parent.children.length > lastSegment) {
1279
+ const toModify = parent.children[lastSegment];
1280
+ edit = { offset: toModify.offset, length: toModify.length, content: newProperty };
1281
+ } else if (parent.children.length === 0 || lastSegment === 0) {
1282
+ edit = { offset: parent.offset + 1, length: 0, content: parent.children.length === 0 ? newProperty : newProperty + "," };
1283
+ } else {
1284
+ const index = lastSegment > parent.children.length ? parent.children.length : lastSegment;
1285
+ const previous = parent.children[index - 1];
1286
+ edit = { offset: previous.offset + previous.length, length: 0, content: "," + newProperty };
1287
+ }
1288
+ return withFormatting(text, edit, options);
1289
+ } else {
1290
+ throw new Error(`Can not ${value === void 0 ? "remove" : options.isArrayInsertion ? "insert" : "modify"} Array index ${insertIndex} as length is not sufficient`);
1291
+ }
1292
+ } else {
1293
+ throw new Error(`Can not add ${typeof lastSegment !== "number" ? "index" : "property"} to parent of type ${parent.type}`);
1294
+ }
1295
+ }
1296
+ function withFormatting(text, edit, options) {
1297
+ if (!options.formattingOptions) {
1298
+ return [edit];
1299
+ }
1300
+ let newText = applyEdit(text, edit);
1301
+ let begin = edit.offset;
1302
+ let end = edit.offset + edit.content.length;
1303
+ if (edit.length === 0 || edit.content.length === 0) {
1304
+ while (begin > 0 && !isEOL(newText, begin - 1)) {
1305
+ begin--;
1306
+ }
1307
+ while (end < newText.length && !isEOL(newText, end)) {
1308
+ end++;
1309
+ }
1310
+ }
1311
+ const edits = format(newText, { offset: begin, length: end - begin }, { ...options.formattingOptions, keepLines: false });
1312
+ for (let i = edits.length - 1; i >= 0; i--) {
1313
+ const edit2 = edits[i];
1314
+ newText = applyEdit(newText, edit2);
1315
+ begin = Math.min(begin, edit2.offset);
1316
+ end = Math.max(end, edit2.offset + edit2.length);
1317
+ end += edit2.content.length - edit2.length;
1318
+ }
1319
+ const editLength = text.length - (newText.length - end) - begin;
1320
+ return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }];
1321
+ }
1322
+ function applyEdit(text, edit) {
1323
+ return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length);
1324
+ }
1325
+
1326
+ // node_modules/jsonc-parser/lib/esm/main.js
1327
+ var ScanError;
1328
+ (function(ScanError2) {
1329
+ ScanError2[ScanError2["None"] = 0] = "None";
1330
+ ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
1331
+ ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString";
1332
+ ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber";
1333
+ ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode";
1334
+ ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter";
1335
+ ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter";
1336
+ })(ScanError || (ScanError = {}));
1337
+ var SyntaxKind;
1338
+ (function(SyntaxKind2) {
1339
+ SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken";
1340
+ SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken";
1341
+ SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken";
1342
+ SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken";
1343
+ SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken";
1344
+ SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken";
1345
+ SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword";
1346
+ SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword";
1347
+ SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword";
1348
+ SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral";
1349
+ SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral";
1350
+ SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia";
1351
+ SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia";
1352
+ SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia";
1353
+ SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia";
1354
+ SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown";
1355
+ SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
1356
+ })(SyntaxKind || (SyntaxKind = {}));
1357
+ var parse2 = parse;
1358
+ var parseTree2 = parseTree;
1359
+ var findNodeAtLocation2 = findNodeAtLocation;
1360
+ var ParseErrorCode;
1361
+ (function(ParseErrorCode2) {
1362
+ ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
1363
+ ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
1364
+ ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected";
1365
+ ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected";
1366
+ ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected";
1367
+ ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected";
1368
+ ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected";
1369
+ ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected";
1370
+ ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected";
1371
+ ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken";
1372
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment";
1373
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString";
1374
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber";
1375
+ ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode";
1376
+ ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
1377
+ ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
1378
+ })(ParseErrorCode || (ParseErrorCode = {}));
1379
+ function printParseErrorCode(code) {
1380
+ switch (code) {
1381
+ case 1:
1382
+ return "InvalidSymbol";
1383
+ case 2:
1384
+ return "InvalidNumberFormat";
1385
+ case 3:
1386
+ return "PropertyNameExpected";
1387
+ case 4:
1388
+ return "ValueExpected";
1389
+ case 5:
1390
+ return "ColonExpected";
1391
+ case 6:
1392
+ return "CommaExpected";
1393
+ case 7:
1394
+ return "CloseBraceExpected";
1395
+ case 8:
1396
+ return "CloseBracketExpected";
1397
+ case 9:
1398
+ return "EndOfFileExpected";
1399
+ case 10:
1400
+ return "InvalidCommentToken";
1401
+ case 11:
1402
+ return "UnexpectedEndOfComment";
1403
+ case 12:
1404
+ return "UnexpectedEndOfString";
1405
+ case 13:
1406
+ return "UnexpectedEndOfNumber";
1407
+ case 14:
1408
+ return "InvalidUnicode";
1409
+ case 15:
1410
+ return "InvalidEscapeCharacter";
1411
+ case 16:
1412
+ return "InvalidCharacter";
1413
+ }
1414
+ return "<unknown ParseErrorCode>";
1415
+ }
1416
+ function modify(text, path5, value, options) {
1417
+ return setProperty(text, path5, value, options);
1418
+ }
1419
+ function applyEdits(text, edits) {
1420
+ let sortedEdits = edits.slice(0).sort((a, b) => {
1421
+ const diff = a.offset - b.offset;
1422
+ if (diff === 0) {
1423
+ return a.length - b.length;
1424
+ }
1425
+ return diff;
1426
+ });
1427
+ let lastModifiedOffset = text.length;
1428
+ for (let i = sortedEdits.length - 1; i >= 0; i--) {
1429
+ let e = sortedEdits[i];
1430
+ if (e.offset + e.length <= lastModifiedOffset) {
1431
+ text = applyEdit(text, e);
1432
+ } else {
1433
+ throw new Error("Overlapping edit");
1434
+ }
1435
+ lastModifiedOffset = e.offset;
1436
+ }
1437
+ return text;
1438
+ }
1439
+
1440
+ // src/paths.ts
1441
+ function globalConfigDir() {
1442
+ const xdg = process.env.XDG_CONFIG_HOME;
1443
+ const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
1444
+ return path.join(base, "opencode");
1445
+ }
1446
+ function catalogCacheFile() {
1447
+ const xdg = process.env.XDG_CACHE_HOME;
1448
+ const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".cache");
1449
+ return path.join(base, "opencode", "models.json");
1450
+ }
1451
+ function globalRouterConfigFile() {
1452
+ return path.join(globalConfigDir(), "router.config.json");
1453
+ }
1454
+ function projectRouterConfigFile(dir) {
1455
+ return path.join(dir, ".opencode", "router.config.json");
1456
+ }
1457
+ function opencodeConfigCandidates(dir) {
1458
+ return [path.join(dir, ".opencode", "opencode.json"), path.join(dir, "opencode.jsonc"), path.join(dir, "opencode.json")];
1459
+ }
1460
+ function globalOpencodeConfigCandidates() {
1461
+ return [path.join(globalConfigDir(), "opencode.jsonc"), path.join(globalConfigDir(), "opencode.json")];
1462
+ }
1463
+ function ensureDir(dir) {
1464
+ fs.mkdirSync(dir, { recursive: true });
1465
+ }
1466
+ function fileExists(file) {
1467
+ try {
1468
+ return fs.statSync(file).isFile();
1469
+ } catch {
1470
+ return false;
1471
+ }
1472
+ }
1473
+ function pathExists(file) {
1474
+ try {
1475
+ fs.statSync(file);
1476
+ return true;
1477
+ } catch {
1478
+ return false;
1479
+ }
1480
+ }
1481
+ var PROJECT_MARKERS = [
1482
+ ".git",
1483
+ "package.json",
1484
+ "opencode.json",
1485
+ "opencode.jsonc",
1486
+ "pyproject.toml",
1487
+ "Cargo.toml",
1488
+ "go.mod"
1489
+ ];
1490
+ function isProjectDir(dir) {
1491
+ return PROJECT_MARKERS.some((marker) => pathExists(path.join(dir, marker)));
1492
+ }
1493
+ function parseJsonc(text) {
1494
+ const errors = [];
1495
+ const value = parse2(text, errors, { allowTrailingComma: true });
1496
+ const first = errors[0];
1497
+ if (first) return { error: `${printParseErrorCode(first.error)} at line ${lineAt(text, first.offset)}` };
1498
+ return { value };
1499
+ }
1500
+ function lineAt(text, offset) {
1501
+ return text.slice(0, offset).split("\n").length;
1502
+ }
1503
+ function readText(file) {
1504
+ try {
1505
+ return fs.readFileSync(file, "utf8").replace(/^/, "");
1506
+ } catch {
1507
+ return void 0;
1508
+ }
1509
+ }
1510
+ function readJsonFile(file) {
1511
+ const text = readText(file);
1512
+ if (text === void 0 || text.trim() === "") return { status: "absent" };
1513
+ const parsed = parseJsonc(text);
1514
+ if (parsed.error !== void 0) return { status: "invalid", error: parsed.error };
1515
+ return { status: "ok", value: parsed.value };
1516
+ }
1517
+ function readJson(file) {
1518
+ const result = readJsonFile(file);
1519
+ return result.status === "ok" ? result.value : void 0;
1520
+ }
1521
+ function isPlainObject(value) {
1522
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1523
+ }
1524
+ function writeTextAtomic(file, text) {
1525
+ ensureDir(path.dirname(file));
1526
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
1527
+ fs.writeFileSync(temp, text, "utf8");
1528
+ try {
1529
+ fs.renameSync(temp, file);
1530
+ } catch {
1531
+ fs.writeFileSync(file, text, "utf8");
1532
+ fs.rmSync(temp, { force: true });
1533
+ }
1534
+ }
1535
+ function writeJson(file, value) {
1536
+ writeTextAtomic(file, JSON.stringify(value, null, 2) + "\n");
1537
+ }
1538
+ function hasPath(text, keyPath) {
1539
+ const tree = parseTree2(text);
1540
+ return tree !== void 0 && findNodeAtLocation2(tree, keyPath) !== void 0;
1541
+ }
1542
+ function editJsonFile(file, edits, initialText = "{}\n") {
1543
+ const current = readJsonFile(file);
1544
+ if (current.status === "invalid") {
1545
+ throw new Error(`${file} has a JSON error (${current.error}). Fix the file and run the command again. The file was not changed.`);
1546
+ }
1547
+ if (current.status === "ok" && !isPlainObject(current.value)) {
1548
+ throw new Error(`${file} does not contain a JSON object. The file was not changed.`);
1549
+ }
1550
+ const created = !fileExists(file);
1551
+ let text = current.status === "ok" ? readText(file) ?? initialText : initialText;
1552
+ const formattingOptions = {
1553
+ insertSpaces: !/^\t/m.test(text),
1554
+ tabSize: 2,
1555
+ eol: text.includes("\r\n") ? "\r\n" : "\n"
1556
+ };
1557
+ for (const edit of edits) {
1558
+ if (edit.value === void 0 && !hasPath(text, edit.path)) continue;
1559
+ text = applyEdits(text, modify(text, edit.path, edit.value, { formattingOptions }));
1560
+ }
1561
+ writeTextAtomic(file, text);
1562
+ return { created };
1563
+ }
1564
+
1565
+ // src/catalog.ts
1566
+ var ROUTED_PROVIDERS = ["opencode-go", "opencode"];
1567
+ function readCatalogFile(file = catalogCacheFile()) {
1568
+ try {
1569
+ const raw = fs2.readFileSync(file, "utf8");
1570
+ return JSON.parse(raw);
1571
+ } catch {
1572
+ return void 0;
1573
+ }
1574
+ }
1575
+ function toNumber(value, fallback = 0) {
1576
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1577
+ }
1578
+ function mapTiers(raw) {
1579
+ const tiers = raw.cost?.tiers;
1580
+ if (!Array.isArray(tiers)) return [];
1581
+ const out = [];
1582
+ for (const t of tiers) {
1583
+ const size = t?.tier?.size;
1584
+ if (typeof size !== "number") continue;
1585
+ out.push({
1586
+ size,
1587
+ input: toNumber(t.input),
1588
+ output: toNumber(t.output),
1589
+ cacheRead: t.cache_read !== void 0 ? toNumber(t.cache_read) : void 0,
1590
+ cacheWrite: t.cache_write !== void 0 ? toNumber(t.cache_write) : void 0
1591
+ });
1592
+ }
1593
+ return out.sort((a, b) => a.size - b.size);
1594
+ }
1595
+ function isFreeModel(cost, tiers) {
1596
+ const baseFree = cost.input === 0 && cost.output === 0;
1597
+ const tiersFree = tiers.every((t) => t.input === 0 && t.output === 0);
1598
+ return baseFree && tiersFree;
1599
+ }
1600
+ function mapModel(providerID, raw) {
1601
+ const cost = {
1602
+ input: toNumber(raw.cost?.input),
1603
+ output: toNumber(raw.cost?.output),
1604
+ cacheRead: raw.cost?.cache_read !== void 0 ? toNumber(raw.cost.cache_read) : void 0,
1605
+ cacheWrite: raw.cost?.cache_write !== void 0 ? toNumber(raw.cost.cache_write) : void 0
1606
+ };
1607
+ const tiers = mapTiers(raw);
1608
+ const rawInputs = raw.modalities?.input;
1609
+ const inputModalities = Array.isArray(rawInputs) ? rawInputs.map(String) : ["text"];
1610
+ return {
1611
+ providerID,
1612
+ modelID: raw.id,
1613
+ id: `${providerID}/${raw.id}`,
1614
+ name: raw.name ?? raw.id,
1615
+ family: raw.family,
1616
+ releaseDate: raw.release_date ?? raw.last_updated,
1617
+ reasoning: raw.reasoning === true,
1618
+ toolCall: raw.tool_call === true,
1619
+ attachment: raw.attachment === true,
1620
+ openWeights: raw.open_weights === true,
1621
+ limit: {
1622
+ context: toNumber(raw.limit?.context),
1623
+ input: raw.limit?.input !== void 0 ? toNumber(raw.limit.input) : void 0,
1624
+ output: toNumber(raw.limit?.output)
1625
+ },
1626
+ cost,
1627
+ tiers,
1628
+ inputModalities,
1629
+ free: isFreeModel(cost, tiers)
1630
+ };
1631
+ }
1632
+ function loadCatalog(file = catalogCacheFile()) {
1633
+ const raw = readCatalogFile(file);
1634
+ if (!raw) return [];
1635
+ const out = [];
1636
+ for (const providerID of ROUTED_PROVIDERS) {
1637
+ const provider = raw[providerID];
1638
+ if (!provider?.models) continue;
1639
+ for (const rawModel of Object.values(provider.models)) {
1640
+ if (rawModel && typeof rawModel.id === "string") out.push(mapModel(providerID, rawModel));
1641
+ }
1642
+ }
1643
+ return out;
1644
+ }
1645
+ function catalogMeta(file = catalogCacheFile()) {
1646
+ let mtime = null;
1647
+ try {
1648
+ mtime = fs2.statSync(file).mtime.toISOString();
1649
+ } catch {
1650
+ mtime = null;
1651
+ }
1652
+ return { path: file, mtime, total: loadCatalog(file).length };
1653
+ }
1654
+
1655
+ // src/scoring.ts
1656
+ var FIVE_HOUR_SHARE = 0.2;
1657
+ var FREE_ROLE_BONUS = 6;
1658
+ var DEFAULT_UNVETTED_PENALTY = 8;
1659
+ function modelClass(model) {
1660
+ if (model.free) return "free";
1661
+ if (model.providerID === "opencode-go") return "subscription";
1662
+ return "paid";
1663
+ }
1664
+ function emptyFeedScores() {
1665
+ return { byFamily: /* @__PURE__ */ new Map(), byId: /* @__PURE__ */ new Map() };
1666
+ }
1667
+ function clamp(value, min = 0, max = 100) {
1668
+ return Math.max(min, Math.min(max, value));
1669
+ }
1670
+ function resolveMetric(model, table, feeds, metric) {
1671
+ const override = table.models[model.id]?.[metric];
1672
+ if (override !== void 0) return { value: clamp(override), source: "override" };
1673
+ const fromId = feeds.byId.get(model.id.toLowerCase())?.[metric];
1674
+ if (fromId !== void 0) return { value: clamp(fromId), source: "feed" };
1675
+ const fromFamily = model.family ? feeds.byFamily.get(model.family.toLowerCase())?.[metric] : void 0;
1676
+ if (fromFamily !== void 0) return { value: clamp(fromFamily), source: "feed" };
1677
+ const id = model.modelID.toLowerCase();
1678
+ const pattern = table.heuristics.patterns.find((p) => id.includes(p.match.toLowerCase()));
1679
+ if (pattern) return { value: clamp(pattern[metric]), source: "heuristic" };
1680
+ const fallback = metric === "intelligence" ? table.heuristics.defaultIntelligence : table.heuristics.defaultSpeed;
1681
+ return { value: clamp(fallback), source: "default" };
1682
+ }
1683
+ function resolveIntelligence(model, table, feeds) {
1684
+ return resolveMetric(model, table, feeds, "intelligence");
1685
+ }
1686
+ function resolveSpeed(model, table, feeds) {
1687
+ return resolveMetric(model, table, feeds, "speed");
1688
+ }
1689
+ function blendedCost(model, cfg) {
1690
+ const { input, output, cacheRead } = cfg.blend;
1691
+ const c = model.cost;
1692
+ return c.input * input + c.output * output + (c.cacheRead ?? 0) * cacheRead;
1693
+ }
1694
+ function monthlyCapFor(modelId, table, plan) {
1695
+ if (plan === "free") return null;
1696
+ const entry = table.monthlyCaps[modelId];
1697
+ if (!entry) return null;
1698
+ return plan === "go-plus" ? entry.goPlus : entry.go;
1699
+ }
1700
+ function logScale(value, min, max) {
1701
+ const lo = Math.log10(Math.max(min, 1e-6));
1702
+ const hi = Math.log10(Math.max(max, min * 1.001));
1703
+ const v = Math.log10(Math.max(value, 1e-6));
1704
+ return clamp((v - lo) / (hi - lo) * 100);
1705
+ }
1706
+ function resolveEndurance(model, table, cfg) {
1707
+ const bc = blendedCost(model, cfg);
1708
+ if (model.free) {
1709
+ return { score: 100, source: "cap", tokensPer5h: null, monthlyCapUSD: null, blendedCost: bc };
1710
+ }
1711
+ const cap = monthlyCapFor(model.id, table, cfg.plan);
1712
+ if (cap === null) {
1713
+ return { score: 100, source: "cap", tokensPer5h: null, monthlyCapUSD: null, blendedCost: bc };
1714
+ }
1715
+ if (bc <= 0) {
1716
+ return { score: 100, source: "cap", tokensPer5h: null, monthlyCapUSD: cap, blendedCost: bc };
1717
+ }
1718
+ const tokensPer5h = cap * FIVE_HOUR_SHARE / bc;
1719
+ const score = logScale(tokensPer5h, cfg.enduranceScale.minTokensPer5h, cfg.enduranceScale.maxTokensPer5h);
1720
+ return { score, source: "cap", tokensPer5h, monthlyCapUSD: cap, blendedCost: bc };
1721
+ }
1722
+ function scoreModel(model, role, table, cfg, feeds) {
1723
+ const intel = resolveIntelligence(model, table, feeds);
1724
+ const speed = resolveSpeed(model, table, feeds);
1725
+ const end = resolveEndurance(model, table, cfg);
1726
+ const w = role.weights;
1727
+ let score = w.intelligence * intel.value + w.speed * speed.value + w.endurance * end.score;
1728
+ const klass = modelClass(model);
1729
+ if (role.preferFree) {
1730
+ if (klass === "free") score += FREE_ROLE_BONUS;
1731
+ } else {
1732
+ score += table.classPreference?.[cfg.plan]?.[klass] ?? 0;
1733
+ }
1734
+ if (table.models[model.id] === void 0) score -= table.heuristics.unvettedPenalty ?? DEFAULT_UNVETTED_PENALTY;
1735
+ score = clamp(score);
1736
+ return {
1737
+ model,
1738
+ intelligence: intel.value,
1739
+ speed: speed.value,
1740
+ endurance: end.score,
1741
+ score,
1742
+ intelligenceSource: intel.source,
1743
+ speedSource: speed.source,
1744
+ enduranceSource: end.source,
1745
+ blendedCost: end.blendedCost,
1746
+ tokensPer5h: end.tokensPer5h,
1747
+ monthlyCapUSD: end.monthlyCapUSD
1748
+ };
1749
+ }
1750
+
1751
+ // src/feeds.ts
1752
+ async function loadFeedScores(cfg) {
1753
+ const feeds = emptyFeedScores();
1754
+ if (!cfg.feeds?.enabled || !cfg.feeds.url) return { feeds, note: null };
1755
+ const controller = new AbortController();
1756
+ const timeout = setTimeout(() => controller.abort(), cfg.feeds.timeoutMs ?? 5e3);
1757
+ try {
1758
+ const res = await fetch(cfg.feeds.url, { signal: controller.signal });
1759
+ if (!res.ok) return { feeds, note: `feed ${cfg.feeds.url} returned ${res.status}; using table + heuristics` };
1760
+ const payload = await res.json();
1761
+ const records = Array.isArray(payload.models) ? payload.models : [];
1762
+ for (const rec of records) {
1763
+ if (!rec) continue;
1764
+ const value = {};
1765
+ if (typeof rec.intelligence === "number") value.intelligence = rec.intelligence;
1766
+ if (typeof rec.speed === "number") value.speed = rec.speed;
1767
+ if (value.intelligence === void 0 && value.speed === void 0) continue;
1768
+ if (rec.id) feeds.byId.set(rec.id.toLowerCase(), value);
1769
+ if (rec.family) feeds.byFamily.set(rec.family.toLowerCase(), value);
1770
+ }
1771
+ return { feeds, note: `feed ${cfg.feeds.url}: ${records.length} records` };
1772
+ } catch (error) {
1773
+ const message = error instanceof Error ? error.message : String(error);
1774
+ return { feeds, note: `feed unavailable (${message}); using table + heuristics` };
1775
+ } finally {
1776
+ clearTimeout(timeout);
1777
+ }
1778
+ }
1779
+
1780
+ // src/config.ts
1781
+ var DEFAULT_SOFT_THRESHOLD = 0.85;
1782
+ var DEFAULT_CHAIN_DEPTH = 4;
1783
+ function loadUserConfig(projectDir) {
1784
+ const notes = [];
1785
+ const global = readUserConfigFile(globalRouterConfigFile(), notes);
1786
+ const project = projectDir ? readUserConfigFile(projectRouterConfigFile(projectDir), notes) : {};
1787
+ return { config: mergeUserConfig(global, project), notes };
1788
+ }
1789
+ function readUserConfigFile(file, notes) {
1790
+ const result = readJsonFile(file);
1791
+ if (result.status === "absent") return {};
1792
+ if (result.status === "invalid") {
1793
+ notes.push(`${file} has a JSON error (${result.error}); it was ignored.`);
1794
+ return {};
1795
+ }
1796
+ if (!isPlainObject(result.value)) {
1797
+ notes.push(`${file} does not contain a JSON object; it was ignored.`);
1798
+ return {};
1799
+ }
1800
+ return sanitizeUserConfig(result.value, file, notes);
1801
+ }
1802
+ function sanitizeUserConfig(raw, file, notes) {
1803
+ const config = { ...raw };
1804
+ const drop = (key, rule) => {
1805
+ notes.push(`${file}: "${key}" ${rule}, not ${JSON.stringify(config[key])}; it was ignored.`);
1806
+ delete config[key];
1807
+ };
1808
+ if (config.plan !== void 0 && !isPlan(config.plan)) drop("plan", `must be one of ${PLAN_PROFILES.join(" | ")}`);
1809
+ const depth = config.chainDepth;
1810
+ if (depth !== void 0 && !(typeof depth === "number" && Number.isInteger(depth) && depth >= 1)) {
1811
+ drop("chainDepth", "must be a whole number of 1 or more");
1812
+ }
1813
+ const threshold = config.softThreshold;
1814
+ if (threshold !== void 0 && !(typeof threshold === "number" && threshold > 0 && threshold <= 1)) {
1815
+ drop("softThreshold", "must be a number above 0 and at most 1");
1816
+ }
1817
+ for (const key of ["denyModels", "allowModels"]) {
1818
+ const list = config[key];
1819
+ if (list !== void 0 && !(Array.isArray(list) && list.every((item) => typeof item === "string"))) {
1820
+ drop(key, "must be a list of model IDs");
1821
+ }
1822
+ }
1823
+ for (const key of ["roles", "weights"]) {
1824
+ const perRole = config[key];
1825
+ if (perRole === void 0) continue;
1826
+ if (!isPlainObject(perRole)) {
1827
+ drop(key, "must be an object keyed by role");
1828
+ continue;
1829
+ }
1830
+ const known = {};
1831
+ for (const [role, value] of Object.entries(perRole)) {
1832
+ if (isRoleName(role)) known[role] = value;
1833
+ else notes.push(`${file}: unknown role "${role}" in "${key}"; it was ignored.`);
1834
+ }
1835
+ config[key] = known;
1836
+ }
1837
+ return config;
1838
+ }
1839
+ function mergeByKey(base = {}, override = {}) {
1840
+ const out = { ...base };
1841
+ for (const key of Object.keys(override)) {
1842
+ out[key] = { ...base[key], ...override[key] };
1843
+ }
1844
+ return out;
1845
+ }
1846
+ function mergeUserConfig(base, override) {
1847
+ return {
1848
+ ...base,
1849
+ ...override,
1850
+ blend: { ...base.blend, ...override.blend },
1851
+ enduranceScale: { ...base.enduranceScale, ...override.enduranceScale },
1852
+ weights: mergeByKey(base.weights, override.weights),
1853
+ roles: mergeByKey(base.roles, override.roles),
1854
+ scores: mergeByKey(base.scores, override.scores),
1855
+ privacy: { ...base.privacy, ...override.privacy },
1856
+ feeds: { ...base.feeds, ...override.feeds },
1857
+ denyModels: override.denyModels ?? base.denyModels,
1858
+ allowModels: override.allowModels ?? base.allowModels
1859
+ };
1860
+ }
1861
+ function detectPlan(catalog) {
1862
+ const hasGo = catalog.some((m) => m.providerID === "opencode-go" && !m.free);
1863
+ if (hasGo) return "go";
1864
+ const hasFree = catalog.some((m) => m.free);
1865
+ if (hasFree) return "free";
1866
+ return "go";
1867
+ }
1868
+ function resolveConfig(user, catalog, cliPlan) {
1869
+ const plan = cliPlan ?? user.plan ?? detectPlan(catalog);
1870
+ return {
1871
+ ...user,
1872
+ plan,
1873
+ chainDepth: user.chainDepth ?? DEFAULT_CHAIN_DEPTH,
1874
+ softThreshold: user.softThreshold ?? DEFAULT_SOFT_THRESHOLD,
1875
+ blend: {
1876
+ input: user.blend?.input ?? 0.75,
1877
+ output: user.blend?.output ?? 0.25,
1878
+ cacheRead: user.blend?.cacheRead ?? 0.1
1879
+ },
1880
+ enduranceScale: {
1881
+ minTokensPer5h: user.enduranceScale?.minTokensPer5h ?? 0.25,
1882
+ maxTokensPer5h: user.enduranceScale?.maxTokensPer5h ?? 40
1883
+ },
1884
+ denyModels: user.denyModels ?? [],
1885
+ allowModels: user.allowModels ?? [],
1886
+ privacy: { freeModelsOk: user.privacy?.freeModelsOk ?? true }
1887
+ };
1888
+ }
1889
+
1890
+ // model-scores.json
1891
+ var model_scores_default = {
1892
+ $comment: "Committed default score table for opencode-router. Safe to edit; user config (router.config.json) overrides these values.",
1893
+ version: 1,
1894
+ updated: "2026-10-18",
1895
+ roles: {
1896
+ plan: {
1897
+ weights: { intelligence: 0.65, speed: 0.1, endurance: 0.25 },
1898
+ variant: "high",
1899
+ chainDepth: 4,
1900
+ requires: { toolCall: true, minContext: 2e5 },
1901
+ diversifyFamily: true
1902
+ },
1903
+ build: {
1904
+ weights: { intelligence: 0.4, speed: 0.25, endurance: 0.35 },
1905
+ variant: "medium",
1906
+ chainDepth: 4,
1907
+ requires: { toolCall: true, minContext: 2e5 },
1908
+ diversifyFamily: true
1909
+ },
1910
+ general: {
1911
+ weights: { intelligence: 0.35, speed: 0.25, endurance: 0.4 },
1912
+ variant: "medium",
1913
+ chainDepth: 4,
1914
+ requires: { toolCall: true, minContext: 2e5 },
1915
+ diversifyFamily: true
1916
+ },
1917
+ explore: {
1918
+ weights: { intelligence: 0.15, speed: 0.45, endurance: 0.4 },
1919
+ variant: "low",
1920
+ chainDepth: 4,
1921
+ requires: { toolCall: true, minContext: 128e3 },
1922
+ preferFree: true
1923
+ },
1924
+ small_model: {
1925
+ weights: { intelligence: 0.05, speed: 0.55, endurance: 0.4 },
1926
+ variant: "low",
1927
+ chainDepth: 3,
1928
+ requires: { toolCall: true, minContext: 1e5 },
1929
+ preferFree: true
1930
+ },
1931
+ title: {
1932
+ weights: { intelligence: 0.05, speed: 0.55, endurance: 0.4 },
1933
+ variant: "low",
1934
+ chainDepth: 3,
1935
+ requires: { toolCall: true, minContext: 1e5 },
1936
+ preferFree: true
1937
+ },
1938
+ summary: {
1939
+ weights: { intelligence: 0.3, speed: 0.3, endurance: 0.4 },
1940
+ variant: "low",
1941
+ chainDepth: 3,
1942
+ requires: { toolCall: true, minContext: 2e5 },
1943
+ preferFree: true
1944
+ },
1945
+ compaction: {
1946
+ weights: { intelligence: 0.3, speed: 0.2, endurance: 0.5 },
1947
+ variant: "none",
1948
+ chainDepth: 3,
1949
+ requires: { toolCall: true, minContext: 1e6 },
1950
+ preferFree: true
1951
+ }
1952
+ },
1953
+ models: {
1954
+ "opencode-go/gemini-4-argon-contributor": { intelligence: 95, speed: 25 },
1955
+ "opencode-go/kimi-k3": { intelligence: 93, speed: 20 },
1956
+ "opencode-go/qwen3.8-max": { intelligence: 92, speed: 35 },
1957
+ "opencode-go/grok-4.7": { intelligence: 91, speed: 40 },
1958
+ "opencode-go/deepseek-v4-pro": { intelligence: 90, speed: 30 },
1959
+ "opencode-go/glm-5.3": { intelligence: 90, speed: 45 },
1960
+ "opencode-go/glm-5.2": { intelligence: 88, speed: 45 },
1961
+ "opencode-go/step-5-preview": { intelligence: 87, speed: 40 },
1962
+ "opencode-go/qwen3.7-plus": { intelligence: 86, speed: 45 },
1963
+ "opencode-go/kimi-k2.7-code": { intelligence: 85, speed: 50 },
1964
+ "opencode-go/gpt-6-luna": { intelligence: 85, speed: 55 },
1965
+ "opencode-go/gpt-5.6-luna": { intelligence: 84, speed: 55 },
1966
+ "opencode-go/minimax-m3": { intelligence: 84, speed: 55 },
1967
+ "opencode-go/hy4-preview": { intelligence: 82, speed: 40 },
1968
+ "opencode-go/qwen3.8-flash": { intelligence: 80, speed: 70 },
1969
+ "opencode-go/deepseek-v4.1-flash": { intelligence: 80, speed: 65 },
1970
+ "opencode-go/space-bunny": { intelligence: 79, speed: 60 },
1971
+ "opencode-go/glm-5.3-flash": { intelligence: 78, speed: 72 },
1972
+ "opencode-go/mimo-v2.6-pro": { intelligence: 78, speed: 58 },
1973
+ "opencode-go/muse-spark-1.3-contributor": { intelligence: 77, speed: 62 },
1974
+ "opencode-go/mimo-v2.5-pro": { intelligence: 76, speed: 58 },
1975
+ "opencode-go/longcat-2.0": { intelligence: 76, speed: 55 },
1976
+ "opencode-go/muse-spark-1.2-contributor": { intelligence: 75, speed: 62 },
1977
+ "opencode-go/longcat-2.5-preview-free": { intelligence: 75, speed: 60 },
1978
+ "opencode-go/hy3": { intelligence: 74, speed: 60 },
1979
+ "opencode-go/claude-haiku-5-5": { intelligence: 74, speed: 78 },
1980
+ "opencode-go/mimo-v2.6-flash": { intelligence: 73, speed: 75 },
1981
+ "opencode-go/mimo-v2.5": { intelligence: 72, speed: 72 },
1982
+ "opencode-go/beam-free": { intelligence: 72, speed: 80 },
1983
+ "opencode/deepseek-v4.1-flash-free": { intelligence: 80, speed: 65 },
1984
+ "opencode/space-bunny-free": { intelligence: 79, speed: 60 },
1985
+ "opencode/glm-5.3-flash-free": { intelligence: 78, speed: 72 },
1986
+ "opencode/longcat-2.5-preview-free": { intelligence: 75, speed: 60 },
1987
+ "opencode/nemotron-3-ultra-free": { intelligence: 74, speed: 62 },
1988
+ "opencode/mimo-v2.6-flash-free": { intelligence: 73, speed: 75 },
1989
+ "opencode/beam-free": { intelligence: 72, speed: 80 },
1990
+ "opencode/nemotron-3.5-super-vl-free": { intelligence: 72, speed: 70 },
1991
+ "opencode/nemotron-3.5-lightning-free": { intelligence: 68, speed: 88 },
1992
+ "opencode/big-pickle": { intelligence: 66, speed: 70 }
1993
+ },
1994
+ heuristics: {
1995
+ patterns: [
1996
+ { match: "gemini-4-argon", intelligence: 95, speed: 25 },
1997
+ { match: "kimi-k3", intelligence: 93, speed: 20 },
1998
+ { match: "qwen3.8-max", intelligence: 92, speed: 35 },
1999
+ { match: "opus", intelligence: 94, speed: 30 },
2000
+ { match: "grok", intelligence: 91, speed: 40 },
2001
+ { match: "deepseek-v4-pro", intelligence: 90, speed: 30 },
2002
+ { match: "sonnet", intelligence: 88, speed: 50 },
2003
+ { match: "step-5", intelligence: 87, speed: 40 },
2004
+ { match: "gpt-6", intelligence: 85, speed: 55 },
2005
+ { match: "kimi", intelligence: 85, speed: 45 },
2006
+ { match: "gpt-5", intelligence: 84, speed: 55 },
2007
+ { match: "minimax", intelligence: 84, speed: 55 },
2008
+ { match: "hy4", intelligence: 82, speed: 40 },
2009
+ { match: "claude", intelligence: 82, speed: 60 },
2010
+ { match: "gpt", intelligence: 82, speed: 55 },
2011
+ { match: "qwen", intelligence: 80, speed: 55 },
2012
+ { match: "deepseek", intelligence: 80, speed: 65 },
2013
+ { match: "glm", intelligence: 80, speed: 55 },
2014
+ { match: "space-bunny", intelligence: 79, speed: 60 },
2015
+ { match: "muse", intelligence: 77, speed: 62 },
2016
+ { match: "longcat", intelligence: 76, speed: 58 },
2017
+ { match: "hy3", intelligence: 74, speed: 60 },
2018
+ { match: "haiku", intelligence: 74, speed: 78 },
2019
+ { match: "mimo", intelligence: 74, speed: 72 },
2020
+ { match: "nemotron", intelligence: 72, speed: 75 },
2021
+ { match: "big-pickle", intelligence: 66, speed: 70 },
2022
+ { match: "-max", intelligence: 90, speed: 35 },
2023
+ { match: "-pro", intelligence: 88, speed: 42 },
2024
+ { match: "-flash", intelligence: 78, speed: 74 },
2025
+ { match: "-mini", intelligence: 74, speed: 80 },
2026
+ { match: "-lite", intelligence: 70, speed: 84 },
2027
+ { match: "-free", intelligence: 72, speed: 70 },
2028
+ { match: "contributor", intelligence: 74, speed: 62 }
2029
+ ],
2030
+ defaultIntelligence: 60,
2031
+ defaultSpeed: 60,
2032
+ unvettedPenalty: 8
2033
+ },
2034
+ classPreference: {
2035
+ go: { subscription: 0, paid: -6, free: -12 },
2036
+ "go-plus": { subscription: 0, paid: -6, free: -12 },
2037
+ free: { subscription: 0, paid: 0, free: 0 }
2038
+ },
2039
+ monthlyCaps: {
2040
+ "opencode-go/longcat-2.5-preview-free": { go: null, goPlus: null },
2041
+ "opencode-go/beam-free": { go: null, goPlus: null },
2042
+ "opencode-go/deepseek-v4.1-flash": { go: 80, goPlus: 160 },
2043
+ "opencode-go/longcat-2.0": { go: 60, goPlus: 240 },
2044
+ "opencode-go/hy3": { go: 60, goPlus: 240 },
2045
+ "opencode-go/step-5-preview": { go: 60, goPlus: 240 },
2046
+ "opencode-go/gemini-4-argon-contributor": { go: 60, goPlus: 240 },
2047
+ "opencode-go/glm-5.3-flash": { go: 60, goPlus: 180 },
2048
+ "opencode-go/glm-5.2": { go: 60, goPlus: 180 },
2049
+ "opencode-go/kimi-k2.7-code": { go: 60, goPlus: 180 },
2050
+ "opencode-go/minimax-m3": { go: 60, goPlus: 180 },
2051
+ "opencode-go/qwen3.7-plus": { go: 60, goPlus: 180 },
2052
+ "opencode-go/mimo-v2.6-flash": { go: 60, goPlus: 120 },
2053
+ "opencode-go/mimo-v2.5": { go: 60, goPlus: 120 },
2054
+ "opencode-go/muse-spark-1.3-contributor": { go: 60, goPlus: 120 },
2055
+ "opencode-go/muse-spark-1.2-contributor": { go: 60, goPlus: 120 },
2056
+ "opencode-go/hy4-preview": { go: 30, goPlus: 120 },
2057
+ "opencode-go/space-bunny": { go: 30, goPlus: 120 },
2058
+ "opencode-go/qwen3.8-flash": { go: 30, goPlus: 90 },
2059
+ "opencode-go/glm-5.3": { go: 15, goPlus: 120 },
2060
+ "opencode-go/kimi-k3": { go: 15, goPlus: 60 },
2061
+ "opencode-go/mimo-v2.6-pro": { go: 15, goPlus: 60 },
2062
+ "opencode-go/mimo-v2.5-pro": { go: 15, goPlus: 60 },
2063
+ "opencode-go/qwen3.8-max": { go: 15, goPlus: 60 },
2064
+ "opencode-go/deepseek-v4-pro": { go: 15, goPlus: 60 },
2065
+ "opencode-go/grok-4.7": { go: 15, goPlus: 60 },
2066
+ "opencode-go/gpt-6-luna": { go: 15, goPlus: 60 },
2067
+ "opencode-go/gpt-5.6-luna": { go: 15, goPlus: 60 },
2068
+ "opencode-go/claude-haiku-5-5": { go: 15, goPlus: 60 }
2069
+ }
2070
+ };
2071
+
2072
+ // src/score-table.ts
2073
+ var DEFAULT_TABLE = model_scores_default;
2074
+ function loadDefaultTable() {
2075
+ return structuredClone(DEFAULT_TABLE);
2076
+ }
2077
+ function mergeRole(base, override) {
2078
+ return {
2079
+ ...base,
2080
+ ...override,
2081
+ weights: { ...base.weights, ...override.weights },
2082
+ requires: { ...base.requires, ...override.requires }
2083
+ };
2084
+ }
2085
+ function resolveTable(user) {
2086
+ const table = loadDefaultTable();
2087
+ for (const role of ROLE_NAMES) {
2088
+ let spec = table.roles[role];
2089
+ if (!spec) continue;
2090
+ if (user.chainDepth !== void 0) spec = { ...spec, chainDepth: user.chainDepth };
2091
+ const override = user.roles?.[role];
2092
+ if (override) spec = mergeRole(spec, override);
2093
+ const weights = user.weights?.[role];
2094
+ if (weights) spec = { ...spec, weights: { ...spec.weights, ...weights } };
2095
+ table.roles[role] = spec;
2096
+ }
2097
+ for (const [id, score] of Object.entries(user.scores ?? {})) {
2098
+ table.models[id] = { ...table.models[id], ...score };
2099
+ }
2100
+ return table;
2101
+ }
2102
+
2103
+ // src/rank.ts
2104
+ function globToRegex(glob) {
2105
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
2106
+ return new RegExp(`^${escaped}$`, "i");
2107
+ }
2108
+ function matchesAny(id, globs) {
2109
+ return globs.some((g) => globToRegex(g).test(id));
2110
+ }
2111
+ function providerAllowed(model, plan) {
2112
+ if (plan === "free") {
2113
+ return model.providerID === "opencode" && model.free;
2114
+ }
2115
+ if (plan === "go" || plan === "go-plus") {
2116
+ if (model.providerID === "opencode-go") return true;
2117
+ if (model.providerID === "opencode") return model.free;
2118
+ return false;
2119
+ }
2120
+ return false;
2121
+ }
2122
+ function requirementOk(model, role) {
2123
+ const req = role.requires;
2124
+ if (!req) return true;
2125
+ if (req.toolCall && !model.toolCall) return false;
2126
+ if (req.reasoning && !model.reasoning) return false;
2127
+ if (req.minContext !== void 0 && model.limit.context < req.minContext) return false;
2128
+ if (req.minOutput !== void 0 && model.limit.output < req.minOutput) return false;
2129
+ if (req.inputModalities && req.inputModalities.length > 0) {
2130
+ const ok = req.inputModalities.every((m) => model.inputModalities.includes(m));
2131
+ if (!ok) return false;
2132
+ }
2133
+ return true;
2134
+ }
2135
+ function eligibleModels(catalog, role, table, cfg) {
2136
+ return catalog.filter((model) => {
2137
+ if (!providerAllowed(model, cfg.plan)) return false;
2138
+ if (!requirementOk(model, role)) return false;
2139
+ if ((cfg.plan === "go" || cfg.plan === "go-plus") && !model.free && !table.monthlyCaps[model.id]) return false;
2140
+ if (cfg.denyModels.length > 0 && matchesAny(model.id, cfg.denyModels)) return false;
2141
+ if (cfg.allowModels.length > 0 && !matchesAny(model.id, cfg.allowModels)) return false;
2142
+ if (!cfg.privacy.freeModelsOk && model.free && !role.preferFree) return false;
2143
+ return true;
2144
+ });
2145
+ }
2146
+ function rankRole(catalog, roleName, table, cfg, feeds) {
2147
+ const role = table.roles[roleName];
2148
+ if (!role) return [];
2149
+ const scored = eligibleModels(catalog, role, table, cfg).map((model) => scoreModel(model, role, table, cfg, feeds)).sort((a, b) => b.score - a.score || a.blendedCost - b.blendedCost);
2150
+ const depth = role.chainDepth ?? cfg.chainDepth;
2151
+ const diversify = role.diversifyFamily ?? false;
2152
+ const chosen = [];
2153
+ const seenFamilies = /* @__PURE__ */ new Set();
2154
+ for (const entry of scored) {
2155
+ if (chosen.length >= depth) break;
2156
+ const family = (entry.model.family ?? entry.model.modelID).toLowerCase();
2157
+ if (diversify && seenFamilies.has(family) && chosen.length < depth) continue;
2158
+ chosen.push(entry);
2159
+ seenFamilies.add(family);
2160
+ }
2161
+ if (chosen.length < depth) {
2162
+ for (const entry of scored) {
2163
+ if (chosen.length >= depth) break;
2164
+ if (chosen.includes(entry)) continue;
2165
+ chosen.push(entry);
2166
+ }
2167
+ }
2168
+ return chosen.map((entry, index) => ({
2169
+ rank: index + 1,
2170
+ providerID: entry.model.providerID,
2171
+ modelID: entry.model.modelID,
2172
+ id: entry.model.id,
2173
+ variant: role.variant,
2174
+ score: round(entry.score),
2175
+ intelligence: round(entry.intelligence),
2176
+ speed: round(entry.speed),
2177
+ endurance: round(entry.endurance),
2178
+ tokensPer5h: entry.tokensPer5h === null ? null : round(entry.tokensPer5h, 3),
2179
+ monthlyCapUSD: entry.monthlyCapUSD,
2180
+ sources: {
2181
+ intelligence: entry.intelligenceSource,
2182
+ speed: entry.speedSource,
2183
+ endurance: entry.enduranceSource
2184
+ },
2185
+ class: modelClass(entry.model)
2186
+ }));
2187
+ }
2188
+ function round(value, digits = 1) {
2189
+ const f = Math.pow(10, digits);
2190
+ return Math.round(value * f) / f;
2191
+ }
2192
+
2193
+ // src/engine.ts
2194
+ async function buildRoutingTable(opts = {}) {
2195
+ const catalogFile = opts.catalogFile ?? catalogCacheFile();
2196
+ const catalog = opts.catalog ?? loadCatalog(catalogFile);
2197
+ const user = loadUserConfig(opts.projectDir);
2198
+ const notes = [...opts.notes ?? [], ...user.notes];
2199
+ if (catalog.length === 0) {
2200
+ notes.push(`No models found in ${catalogFile}. Start OpenCode one time and sign in, then run this command again.`);
2201
+ }
2202
+ const cfg = resolveConfig(user.config, catalog, opts.plan);
2203
+ const scoreTable = resolveTable(user.config);
2204
+ let feeds = opts.feeds;
2205
+ if (!feeds) {
2206
+ const loaded = await loadFeedScores(cfg);
2207
+ feeds = loaded.feeds;
2208
+ if (loaded.note) notes.push(loaded.note);
2209
+ }
2210
+ const profiles = {};
2211
+ for (const plan of PLAN_PROFILES) {
2212
+ const planCfg = { ...cfg, plan };
2213
+ const chains = {};
2214
+ for (const role of ROLE_NAMES) {
2215
+ chains[role] = rankRole(catalog, role, scoreTable, planCfg, feeds);
2216
+ }
2217
+ profiles[plan] = chains;
2218
+ }
2219
+ const table = {
2220
+ version: 1,
2221
+ createdAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString(),
2222
+ plan: cfg.plan,
2223
+ active: profiles[cfg.plan] ?? {},
2224
+ profiles,
2225
+ notes
2226
+ };
2227
+ return { table, cfg, scoreTable, catalog };
2228
+ }
2229
+ function activeEntries(table) {
2230
+ return ROLE_NAMES.flatMap((role) => table.active[role] ?? []);
2231
+ }
2232
+ function pickForRole(table, role, isHealthy) {
2233
+ const chain = table.active[role];
2234
+ if (!chain || chain.length === 0) return void 0;
2235
+ return chain.find((entry) => isHealthy(entry.id)) ?? chain[0];
2236
+ }
2237
+ function healthCheck(result, ledger) {
2238
+ return (modelId) => ledger.isHealthy(modelId, monthlyCapFor(modelId, result.scoreTable, result.cfg.plan), result.cfg.softThreshold);
2239
+ }
2240
+ function usagePercent(result, ledger, modelId) {
2241
+ return ledger.usage(modelId, monthlyCapFor(modelId, result.scoreTable, result.cfg.plan)).pct5h * 100;
2242
+ }
2243
+ function statusRows(result, ledger) {
2244
+ const isHealthy = healthCheck(result, ledger);
2245
+ return ROLE_NAMES.map((role) => {
2246
+ const entry = isAgentRole(role) ? pickForRole(result.table, role, isHealthy) : result.table.active[role]?.[0];
2247
+ if (!entry) return { role, pct5h: 0, state: "none" };
2248
+ const state = !isHealthy(entry.id) ? "limited" : entry.rank > 1 ? "failover" : "healthy";
2249
+ return { role, entry, pct5h: usagePercent(result, ledger, entry.id), state };
2250
+ });
2251
+ }
2252
+
2253
+ // src/format.ts
2254
+ function fmtTokens(value) {
2255
+ if (value === null) return "unlimited";
2256
+ if (value >= 1) return `${value.toFixed(1)}M`;
2257
+ return `${(value * 1e3).toFixed(0)}K`;
2258
+ }
2259
+ function fmtCap(value) {
2260
+ return value === null ? "inf" : `$${value}`;
2261
+ }
2262
+ function classTag(entry) {
2263
+ if (entry.class === "free") return "free*";
2264
+ if (entry.class === "subscription") return "go";
2265
+ return "paid";
2266
+ }
2267
+ function formatScorecard(table, opts = {}) {
2268
+ const lines = [];
2269
+ const when = table.createdAt.replace("T", " ").replace(/\..*$/, "");
2270
+ lines.push(`## opencode-router scorecard`);
2271
+ lines.push("");
2272
+ lines.push(`- Plan: **${table.plan}**`);
2273
+ lines.push(`- Updated: ${when} (UTC)`);
2274
+ lines.push(`- Legend: go = Go subscription, free* = $0 (data may be collected), paid = Zen pay-as-you-go`);
2275
+ lines.push("");
2276
+ lines.push("| Role | # | Model | Var | Score | Intel | Speed | Endur | Tokens/5h | Cap/mo | Class |");
2277
+ lines.push("| :-- | --: | :-- | :-- | --: | --: | --: | --: | --: | --: | :-- |");
2278
+ for (const role of ROLE_NAMES) {
2279
+ const chain = table.active[role];
2280
+ if (!chain) continue;
2281
+ if (chain.length === 0) {
2282
+ lines.push(`| ${role} | - | _no eligible model_ | | | | | | | | |`);
2283
+ continue;
2284
+ }
2285
+ chain.forEach((entry, index) => {
2286
+ const roleCol = index === 0 ? role : "";
2287
+ const sources = opts.verbose ? ` (${entry.sources.intelligence}/${entry.sources.speed}/${entry.sources.endurance})` : "";
2288
+ lines.push(
2289
+ `| ${roleCol} | ${entry.rank} | \`${entry.id}\`${sources} | ${entry.variant ?? "-"} | ${entry.score} | ${entry.intelligence} | ${entry.speed} | ${entry.endurance} | ${fmtTokens(entry.tokensPer5h)} | ${fmtCap(entry.monthlyCapUSD)} | ${classTag(entry)} |`
2290
+ );
2291
+ });
2292
+ }
2293
+ if (table.notes.length > 0) {
2294
+ lines.push("");
2295
+ lines.push("**Notes**");
2296
+ for (const note of table.notes) lines.push(`- ${note}`);
2297
+ }
2298
+ lines.push("");
2299
+ lines.push(
2300
+ opts.verbose ? `_Sources (intel/speed/endur): override = committed/user table, feed = external benchmark, cap = computed from quota, heuristic = name pattern, default = no match._` : `_Run \`opencode-router refresh --dry-run --verbose\` in a terminal to see the source of each score._`
2301
+ );
2302
+ return lines.join("\n");
2303
+ }
2304
+ function formatStatus(table, rows) {
2305
+ const lines = [];
2306
+ lines.push(`## opencode-router status`);
2307
+ lines.push("");
2308
+ lines.push(`- Plan: **${table.plan}**`);
2309
+ lines.push(
2310
+ `- State: healthy = first model; failover = a later model, because the first is limited; limited = the model in use is near its limit or in cooldown`
2311
+ );
2312
+ lines.push("");
2313
+ lines.push("| Role | Model in use | Rank | Usage (5h) | State |");
2314
+ lines.push("| :-- | :-- | --: | --: | :-- |");
2315
+ for (const row of rows) {
2316
+ if (!row.entry) {
2317
+ lines.push(`| ${row.role} | _none_ | - | - | - |`);
2318
+ continue;
2319
+ }
2320
+ lines.push(`| ${row.role} | \`${row.entry.id}\` | ${row.entry.rank} | ${row.pct5h.toFixed(0)}% | ${row.state} |`);
2321
+ }
2322
+ return lines.join("\n");
2323
+ }
2324
+
2325
+ // src/apply.ts
2326
+ import path2 from "path";
2327
+ var INTERNAL_AGENTS = ["compaction", "title", "summary"];
2328
+ var SCHEMA_URL = "https://opencode.ai/config.json";
2329
+ function routingConfigBody(table) {
2330
+ const agent = {};
2331
+ for (const role of AGENT_ROLES) {
2332
+ const entry = table.active[role]?.[0];
2333
+ if (!entry) continue;
2334
+ agent[role] = entry.variant && entry.variant !== "none" ? { model: entry.id, variant: entry.variant } : { model: entry.id };
2335
+ }
2336
+ for (const role of INTERNAL_AGENTS) {
2337
+ const entry = table.active[role]?.[0];
2338
+ if (!entry) continue;
2339
+ agent[role] = { model: entry.id };
2340
+ }
2341
+ const body = { agent };
2342
+ const build2 = table.active.build?.[0];
2343
+ const small = table.active.small_model?.[0];
2344
+ if (build2) body.model = build2.id;
2345
+ if (small) body.small_model = small.id;
2346
+ return body;
2347
+ }
2348
+ function pickConfigPath(scope, dir) {
2349
+ const candidates = scope === "global" ? globalOpencodeConfigCandidates() : opencodeConfigCandidates(dir);
2350
+ for (const candidate of candidates) {
2351
+ if (fileExists(candidate)) return candidate;
2352
+ }
2353
+ return scope === "global" ? path2.join(globalConfigDir(), "opencode.json") : path2.join(dir, "opencode.json");
2354
+ }
2355
+ function writeRoutingConfig(table, scope, dir) {
2356
+ const target = pickConfigPath(scope, dir);
2357
+ const body = routingConfigBody(table);
2358
+ const edits = [];
2359
+ if (body.model) edits.push({ path: ["model"], value: body.model });
2360
+ if (body.small_model) edits.push({ path: ["small_model"], value: body.small_model });
2361
+ for (const [role, spec] of Object.entries(body.agent)) {
2362
+ edits.push({ path: ["agent", role, "model"], value: spec.model });
2363
+ if (isAgentRole(role)) edits.push({ path: ["agent", role, "variant"], value: spec.variant });
2364
+ }
2365
+ const { created } = editJsonFile(target, edits, `{
2366
+ "$schema": "${SCHEMA_URL}"
2367
+ }
2368
+ `);
2369
+ return { path: target, created };
2370
+ }
2371
+ async function patchServerConfig(table) {
2372
+ const base = process.env.OPENCODE_SERVER_URL ?? process.env.OPENCODE_SERVER;
2373
+ if (!base) {
2374
+ return { applied: false, detail: "live PATCH not attempted (no OPENCODE_SERVER_URL set)" };
2375
+ }
2376
+ const url = base.replace(/\/$/, "") + "/config";
2377
+ const headers = { "content-type": "application/json" };
2378
+ const user = process.env.OPENCODE_SERVER_USERNAME;
2379
+ const pass = process.env.OPENCODE_SERVER_PASSWORD;
2380
+ if (user && pass) headers.authorization = "Basic " + Buffer.from(`${user}:${pass}`).toString("base64");
2381
+ try {
2382
+ const res = await fetch(url, { method: "PATCH", headers, body: JSON.stringify(routingConfigBody(table)) });
2383
+ if (!res.ok) return { applied: false, detail: `PATCH ${url} -> ${res.status}` };
2384
+ return { applied: true, detail: `PATCH ${url}` };
2385
+ } catch (error) {
2386
+ const message = error instanceof Error ? error.message : String(error);
2387
+ return { applied: false, detail: `PATCH failed: ${message}` };
2388
+ }
2389
+ }
2390
+
2391
+ // src/ledger.ts
2392
+ import fs3 from "fs";
2393
+ var HOUR = 36e5;
2394
+ var DAY = 24 * HOUR;
2395
+ var WINDOW_5H = 5 * HOUR;
2396
+ var WINDOW_WEEK = 7 * DAY;
2397
+ var WINDOW_MONTH = 30 * DAY;
2398
+ var MONTHLY_LIMIT = 0.95;
2399
+ function emptyState(now) {
2400
+ return {
2401
+ cost5h: 0,
2402
+ costWeek: 0,
2403
+ costMonth: 0,
2404
+ tokens5h: 0,
2405
+ tokensWeek: 0,
2406
+ tokensMonth: 0,
2407
+ window5hStart: now,
2408
+ windowWeekStart: now,
2409
+ windowMonthStart: now
2410
+ };
2411
+ }
2412
+ function emptyLedger(now) {
2413
+ return { version: 1, updatedAt: new Date(now).toISOString(), models: {} };
2414
+ }
2415
+ function isLedger(value) {
2416
+ return isPlainObject(value) && value.version === 1 && isPlainObject(value.models);
2417
+ }
2418
+ function mtimeOf(file) {
2419
+ try {
2420
+ return fs3.statSync(file).mtimeMs;
2421
+ } catch {
2422
+ return -1;
2423
+ }
2424
+ }
2425
+ var LedgerStore = class {
2426
+ constructor(file, now = Date.now) {
2427
+ this.file = file;
2428
+ this.now = now;
2429
+ this.data = this.load();
2430
+ }
2431
+ file;
2432
+ now;
2433
+ data;
2434
+ loadedMtime = -1;
2435
+ /** Read the file. A corrupt file is kept as `<file>.corrupt-<time>` and a new ledger starts. */
2436
+ load() {
2437
+ this.loadedMtime = mtimeOf(this.file);
2438
+ const result = readJsonFile(this.file);
2439
+ if (result.status === "ok" && isLedger(result.value)) return result.value;
2440
+ if (result.status !== "absent") {
2441
+ try {
2442
+ fs3.renameSync(this.file, `${this.file}.corrupt-${this.now()}`);
2443
+ } catch {
2444
+ }
2445
+ this.loadedMtime = -1;
2446
+ }
2447
+ return emptyLedger(this.now());
2448
+ }
2449
+ sync() {
2450
+ if (mtimeOf(this.file) !== this.loadedMtime) this.data = this.load();
2451
+ }
2452
+ /** Reset each window that has ended. */
2453
+ roll(state, now) {
2454
+ if (now - state.window5hStart >= WINDOW_5H) {
2455
+ state.cost5h = 0;
2456
+ state.tokens5h = 0;
2457
+ state.window5hStart = now;
2458
+ }
2459
+ if (now - state.windowWeekStart >= WINDOW_WEEK) {
2460
+ state.costWeek = 0;
2461
+ state.tokensWeek = 0;
2462
+ state.windowWeekStart = now;
2463
+ }
2464
+ if (now - state.windowMonthStart >= WINDOW_MONTH) {
2465
+ state.costMonth = 0;
2466
+ state.tokensMonth = 0;
2467
+ state.windowMonthStart = now;
2468
+ }
2469
+ return state;
2470
+ }
2471
+ state(modelId) {
2472
+ this.sync();
2473
+ const now = this.now();
2474
+ let entry = this.data.models[modelId];
2475
+ if (!entry) {
2476
+ entry = emptyState(now);
2477
+ this.data.models[modelId] = entry;
2478
+ }
2479
+ return this.roll(entry, now);
2480
+ }
2481
+ record(modelId, costUSD, tokens) {
2482
+ const state = this.state(modelId);
2483
+ state.cost5h += costUSD;
2484
+ state.costWeek += costUSD;
2485
+ state.costMonth += costUSD;
2486
+ state.tokens5h += tokens;
2487
+ state.tokensWeek += tokens;
2488
+ state.tokensMonth += tokens;
2489
+ state.lastUsedAt = this.now();
2490
+ this.save();
2491
+ }
2492
+ cooldown(modelId, durationMs, reason) {
2493
+ const state = this.state(modelId);
2494
+ state.cooldownUntil = this.now() + durationMs;
2495
+ if (reason) state.lastError = reason;
2496
+ this.save();
2497
+ }
2498
+ clearCooldown(modelId) {
2499
+ const state = this.state(modelId);
2500
+ state.cooldownUntil = void 0;
2501
+ this.save();
2502
+ }
2503
+ usage(modelId, monthlyCapUSD) {
2504
+ return usageOf(this.state(modelId), monthlyCapUSD);
2505
+ }
2506
+ /** Healthy = not cooling down, under the soft threshold for 5 hours, and under the monthly limit. */
2507
+ isHealthy(modelId, monthlyCapUSD, softThreshold) {
2508
+ const state = this.state(modelId);
2509
+ if (state.cooldownUntil && this.now() < state.cooldownUntil) return false;
2510
+ const u = usageOf(state, monthlyCapUSD);
2511
+ return u.pct5h < softThreshold && u.pctMonth < MONTHLY_LIMIT;
2512
+ }
2513
+ snapshot() {
2514
+ return structuredClone(this.data);
2515
+ }
2516
+ save() {
2517
+ this.data.updatedAt = new Date(this.now()).toISOString();
2518
+ writeJson(this.file, this.data);
2519
+ this.loadedMtime = mtimeOf(this.file);
2520
+ }
2521
+ };
2522
+ function usageOf(state, monthlyCapUSD) {
2523
+ const cap5h = monthlyCapUSD === null ? null : monthlyCapUSD * FIVE_HOUR_SHARE;
2524
+ return {
2525
+ cost5h: state.cost5h,
2526
+ costMonth: state.costMonth,
2527
+ tokens5h: state.tokens5h,
2528
+ tokensMonth: state.tokensMonth,
2529
+ pct5h: cap5h === null || cap5h <= 0 ? 0 : state.cost5h / cap5h,
2530
+ pctMonth: monthlyCapUSD === null || monthlyCapUSD <= 0 ? 0 : state.costMonth / monthlyCapUSD,
2531
+ cooldownUntil: state.cooldownUntil ?? null
2532
+ };
2533
+ }
2534
+
2535
+ // src/install.ts
2536
+ import fs4 from "fs";
2537
+ import path3 from "path";
2538
+
2539
+ // src/commands.ts
2540
+ import { fileURLToPath } from "url";
2541
+ var SLASH_COMMANDS = [
2542
+ { name: "refresh-routing", description: "Recompute and apply OpenCode model routing", args: "refresh" },
2543
+ { name: "routing-status", description: "Show the model in use per role and its 5-hour quota use", args: "status" },
2544
+ { name: "routing-plan", description: "Switch the routing plan (go | go-plus | free) and rebuild", args: "plan $1" },
2545
+ { name: "routing-eval", description: "Run the router scoring stress test", args: "eval" }
2546
+ ];
2547
+ var OUTPUT_BEGIN = "---BEGIN ROUTER OUTPUT---";
2548
+ function cliScriptPath() {
2549
+ return fileURLToPath(new URL("./bin.js", import.meta.url)).replace(/\\/g, "/");
2550
+ }
2551
+ function commandTemplate(args) {
2552
+ return [
2553
+ "opencode-router already ran. Its output is between the markers below.",
2554
+ "",
2555
+ "Rules:",
2556
+ "1. Do not run any command, script, or tool. Do not search for opencode-router on PATH.",
2557
+ "2. Present the text between the markers to the user exactly as written.",
2558
+ "3. Do not summarize, reformat, shorten, or reorganize it.",
2559
+ "",
2560
+ OUTPUT_BEGIN,
2561
+ `!\`node "${cliScriptPath()}" ${args} --scope global\``,
2562
+ "---END ROUTER OUTPUT---"
2563
+ ].join("\n");
2564
+ }
2565
+
2566
+ // src/install.ts
2567
+ function commandFile(command) {
2568
+ return ["---", `description: ${command.description}`, "agent: build", "---", "", commandTemplate(command.args), ""].join("\n");
2569
+ }
2570
+ function installCommands(scope, dir) {
2571
+ const base = scope === "global" ? globalConfigDir() : path3.join(dir, ".opencode");
2572
+ const commandDir = path3.join(base, "command");
2573
+ ensureDir(commandDir);
2574
+ const files = SLASH_COMMANDS.map((command) => {
2575
+ const file = path3.join(commandDir, `${command.name}.md`);
2576
+ fs4.writeFileSync(file, commandFile(command), "utf8");
2577
+ return file;
2578
+ });
2579
+ const configPath = path3.join(base, "router.config.json");
2580
+ const configCreated = !fileExists(configPath);
2581
+ if (configCreated) writeJson(configPath, {});
2582
+ return { commandDir, configPath, configCreated, files };
2583
+ }
2584
+
2585
+ // src/state.ts
2586
+ import fs5 from "fs";
2587
+ import path4 from "path";
2588
+ function stateDir() {
2589
+ return path4.join(globalConfigDir(), "router");
2590
+ }
2591
+ function ledgerFile() {
2592
+ return path4.join(stateDir(), "ledger.json");
2593
+ }
2594
+ function evalReportFile() {
2595
+ return path4.join(stateDir(), "eval-report.md");
2596
+ }
2597
+ function applyMarkerFile() {
2598
+ return path4.join(stateDir(), "last-apply.json");
2599
+ }
2600
+ var MARKER_MAX_AGE_MS = 1e4;
2601
+ function readApplyMarker(maxAgeMs = MARKER_MAX_AGE_MS) {
2602
+ const marker = readJson(applyMarkerFile());
2603
+ if (!marker?.at || !marker.detail) return null;
2604
+ const at = Date.parse(marker.at);
2605
+ if (!Number.isFinite(at) || Date.now() - at > maxAgeMs) return null;
2606
+ return { ok: marker.ok === true, detail: marker.detail };
2607
+ }
2608
+
2609
+ // src/eval.ts
2610
+ var ROLE = "build";
2611
+ var CHAIN_LIMIT = 4;
2612
+ var PROMPT = `In a fixture repo, build a CLI named \`todo\` with \`add\`, \`list\`, \`done <id>\`, and \`rm <id>\` subcommands backed by a JSON file. Include input validation, a \`--json\` output flag, and a test suite. Then fix any failing tests.`;
2613
+ var GO_URL = "https://opencode.ai/inference/go/openai/v1";
2614
+ var ZEN_URL = "https://opencode.ai/inference/openai/v1";
2615
+ function staticGrade(text) {
2616
+ const lower = text.toLowerCase();
2617
+ const checks = {
2618
+ add: /\badd\b/.test(lower),
2619
+ list: /\blist\b/.test(lower),
2620
+ done: /\bdone\b/.test(lower),
2621
+ rm: /\brm\b|\bremove\b|\bdelete\b/.test(lower),
2622
+ jsonFlag: /--json/.test(text),
2623
+ validation: /invalid|validat|error/.test(lower),
2624
+ tests: /\btest\b|\bdescribe\(|\bit\(|assert/.test(lower)
2625
+ };
2626
+ const passed = Object.values(checks).filter(Boolean).length;
2627
+ return { passed, checks };
2628
+ }
2629
+ async function callModel(entry, apiKey) {
2630
+ const base = entry.providerID === "opencode-go" ? GO_URL : ZEN_URL;
2631
+ const started = Date.now();
2632
+ try {
2633
+ const res = await fetch(`${base}/chat/completions`, {
2634
+ method: "POST",
2635
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
2636
+ body: JSON.stringify({
2637
+ model: entry.modelID,
2638
+ messages: [{ role: "user", content: PROMPT }],
2639
+ max_tokens: 2e3
2640
+ })
2641
+ });
2642
+ const latencyMs = Date.now() - started;
2643
+ if (!res.ok) {
2644
+ return { ok: false, latencyMs, inputTokens: 0, outputTokens: 0, costUSD: 0, text: "", error: `HTTP ${res.status}` };
2645
+ }
2646
+ const json = await res.json();
2647
+ const text = json.choices?.[0]?.message?.content ?? "";
2648
+ return {
2649
+ ok: true,
2650
+ latencyMs,
2651
+ inputTokens: json.usage?.prompt_tokens ?? 0,
2652
+ outputTokens: json.usage?.completion_tokens ?? 0,
2653
+ costUSD: estimateCost(entry, json.usage?.prompt_tokens ?? 0, json.usage?.completion_tokens ?? 0),
2654
+ text
2655
+ };
2656
+ } catch (error) {
2657
+ return {
2658
+ ok: false,
2659
+ latencyMs: Date.now() - started,
2660
+ inputTokens: 0,
2661
+ outputTokens: 0,
2662
+ costUSD: 0,
2663
+ text: "",
2664
+ error: error instanceof Error ? error.message : String(error)
2665
+ };
2666
+ }
2667
+ }
2668
+ function estimateCost(entry, inputTokens, outputTokens) {
2669
+ const per1M = entry.tokensPer5h && entry.monthlyCapUSD ? entry.monthlyCapUSD * FIVE_HOUR_SHARE / entry.tokensPer5h : 1;
2670
+ return (inputTokens + outputTokens) / 1e6 * per1M;
2671
+ }
2672
+ async function runEval(opts = {}) {
2673
+ const { table } = await buildRoutingTable({ plan: opts.plan, catalogFile: opts.catalog, projectDir: opts.projectDir });
2674
+ const chain = (table.active[ROLE] ?? []).slice(0, CHAIN_LIMIT);
2675
+ if (chain.length === 0) {
2676
+ process.stderr.write(`error: no candidate models for role "${ROLE}"
2677
+ `);
2678
+ return 1;
2679
+ }
2680
+ const apiKey = process.env.OPENCODE_API_KEY;
2681
+ const reportPath = evalReportFile();
2682
+ const lines = [];
2683
+ lines.push(`# opencode-router eval report`);
2684
+ lines.push("");
2685
+ lines.push(`- Role: **${ROLE}**`);
2686
+ lines.push(`- Plan: **${table.plan}**`);
2687
+ lines.push(`- Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
2688
+ lines.push("");
2689
+ lines.push(`## Prompt`);
2690
+ lines.push("");
2691
+ lines.push("> " + PROMPT.replace(/\r?\n/g, " "));
2692
+ lines.push("");
2693
+ if (!apiKey) {
2694
+ lines.push("## Result");
2695
+ lines.push("");
2696
+ lines.push("`OPENCODE_API_KEY` is not set. Nothing was called. The predicted ranking is below.");
2697
+ lines.push("");
2698
+ lines.push("| Rank | Model | Score | Intel | Speed | Endur | Tokens/5h | Class |");
2699
+ lines.push("| --: | :-- | --: | --: | --: | --: | --: | :-- |");
2700
+ for (const e of chain) {
2701
+ lines.push(`| ${e.rank} | \`${e.id}\` | ${e.score} | ${e.intelligence} | ${e.speed} | ${e.endurance} | ${e.tokensPer5h ?? "inf"} | ${e.class} |`);
2702
+ }
2703
+ lines.push("");
2704
+ lines.push("Set `OPENCODE_API_KEY` and re-run `opencode-router eval` to measure real latency, tokens, and pass rate.");
2705
+ writeTextAtomic(reportPath, lines.join("\n") + "\n");
2706
+ if (opts.json) process.stdout.write(JSON.stringify({ role: ROLE, predicted: chain }, null, 2) + "\n");
2707
+ else process.stdout.write(`Wrote ${reportPath} (predicted only; set OPENCODE_API_KEY for live results)
2708
+ `);
2709
+ return 0;
2710
+ }
2711
+ lines.push("## Result");
2712
+ lines.push("");
2713
+ lines.push("| Predicted rank | Model | Latency (ms) | Tokens | Cost (USD) | Checks | Static pass | Error |");
2714
+ lines.push("| --: | :-- | --: | --: | --: | --: | --: | :-- |");
2715
+ const results = [];
2716
+ for (const entry of chain) {
2717
+ const result = await callModel(entry, apiKey);
2718
+ const grade = staticGrade(result.text);
2719
+ results.push({ entry, result, pass: grade.passed });
2720
+ lines.push(
2721
+ `| ${entry.rank} | \`${entry.id}\` | ${result.latencyMs} | ${result.inputTokens + result.outputTokens} | ${result.costUSD.toFixed(5)} | ${grade.passed}/7 | ${result.ok ? grade.passed >= 5 ? "PASS" : "WEAK" : "FAIL"} | ${result.error ?? ""} |`
2722
+ );
2723
+ }
2724
+ const measured = [...results].sort((a, b) => b.pass - a.pass || a.result.latencyMs - b.result.latencyMs);
2725
+ lines.push("");
2726
+ lines.push("## Predicted vs measured");
2727
+ lines.push("");
2728
+ lines.push("| Predicted rank | Measured rank | Model |");
2729
+ lines.push("| --: | --: | :-- |");
2730
+ for (const { entry } of results) {
2731
+ const measuredRank = measured.findIndex((m) => m.entry.id === entry.id) + 1;
2732
+ lines.push(`| ${entry.rank} | ${measuredRank} | \`${entry.id}\` |`);
2733
+ }
2734
+ lines.push("");
2735
+ lines.push("Feed the measured ranks back into `model-scores.json` (or `router.config.json` -> `scores`) to tune accuracy.");
2736
+ writeTextAtomic(reportPath, lines.join("\n") + "\n");
2737
+ if (opts.json) process.stdout.write(JSON.stringify({ role: ROLE, results }, null, 2) + "\n");
2738
+ else process.stdout.write(`Wrote ${reportPath}
2739
+ `);
2740
+ return 0;
2741
+ }
2742
+
2743
+ // src/cli.ts
2744
+ var HELP = `opencode-router - dynamic model routing for OpenCode Go / Go Plus / Free
2745
+
2746
+ Usage:
2747
+ opencode-router refresh [options] Recompute the routing table and apply it
2748
+ opencode-router status [options] Show the model in use per role and its 5-hour quota use
2749
+ opencode-router plan <profile> Save the plan profile (go | go-plus | free) and rebuild
2750
+ opencode-router install [--scope] Write slash-command files and a starter config
2751
+ opencode-router cat Show catalog metadata (path, date, model count)
2752
+ opencode-router eval Run the scoring stress test
2753
+
2754
+ Options:
2755
+ --plan <go|go-plus|free> Plan profile for this run
2756
+ --scope <project|global> Which config files to write (default: project in a project folder, else global)
2757
+ --catalog <file> Path to an OpenCode models.json cache
2758
+ --dry-run Do not write opencode.json
2759
+ --no-apply Write opencode.json but do not send it to a running server
2760
+ --json Print machine-readable JSON
2761
+ --verbose Include score source tags
2762
+ -h, --help Show this help
2763
+ `;
2764
+ var PLAN_CHOICES = PLAN_PROFILES.join(" | ");
2765
+ var UsageError = class extends Error {
2766
+ };
2767
+ function resolveScope(explicit, value, dir) {
2768
+ if (explicit) return value;
2769
+ return isProjectDir(dir) ? "project" : "global";
2770
+ }
2771
+ function parsePlan(value) {
2772
+ if (value === void 0) return void 0;
2773
+ if (!isPlan(value)) throw new UsageError(`--plan must be one of ${PLAN_CHOICES}, not "${value}"`);
2774
+ return value;
2775
+ }
2776
+ function parseScope(value) {
2777
+ if (value === void 0 || value === "project" || value === "global") return value;
2778
+ throw new UsageError(`--scope must be project or global, not "${value}"`);
2779
+ }
2780
+ function parseCommandLine(argv, cwd) {
2781
+ let parsed;
2782
+ try {
2783
+ parsed = parseArgs({
2784
+ args: argv,
2785
+ options: {
2786
+ plan: { type: "string" },
2787
+ scope: { type: "string" },
2788
+ catalog: { type: "string" },
2789
+ "dry-run": { type: "boolean" },
2790
+ "no-apply": { type: "boolean" },
2791
+ json: { type: "boolean" },
2792
+ verbose: { type: "boolean" },
2793
+ help: { type: "boolean", short: "h" }
2794
+ },
2795
+ allowPositionals: true
2796
+ });
2797
+ } catch (error) {
2798
+ throw new UsageError(error instanceof Error ? error.message : String(error));
2799
+ }
2800
+ const { values, positionals } = parsed;
2801
+ const scope = parseScope(values.scope);
2802
+ const opts = {
2803
+ plan: parsePlan(values.plan),
2804
+ scope: resolveScope(scope !== void 0, scope ?? "project", cwd),
2805
+ catalog: values.catalog,
2806
+ dryRun: values["dry-run"] === true,
2807
+ apply: values["no-apply"] !== true,
2808
+ json: values.json === true,
2809
+ verbose: values.verbose === true,
2810
+ cwd
2811
+ };
2812
+ return { command: positionals[0], args: positionals.slice(1), help: values.help === true, opts };
2813
+ }
2814
+ function build(opts) {
2815
+ return buildRoutingTable({ plan: opts.plan, catalogFile: opts.catalog, projectDir: opts.cwd });
2816
+ }
2817
+ async function cmdRefresh(opts) {
2818
+ const { table } = await build(opts);
2819
+ if (opts.json) {
2820
+ process.stdout.write(JSON.stringify({ table, routing: routingConfigBody(table) }, null, 2) + "\n");
2821
+ } else {
2822
+ process.stdout.write(formatScorecard(table, { verbose: opts.verbose }) + "\n");
2823
+ }
2824
+ let detail = "dry-run: nothing written";
2825
+ let applied = false;
2826
+ if (!opts.dryRun) {
2827
+ const written = writeRoutingConfig(table, opts.scope, opts.cwd);
2828
+ detail = `${written.created ? "created" : "updated"} ${written.path}`;
2829
+ const marker = readApplyMarker();
2830
+ if (marker) {
2831
+ detail += ` | ${marker.detail}`;
2832
+ applied = marker.ok;
2833
+ } else if (opts.apply) {
2834
+ const patch = await patchServerConfig(table);
2835
+ detail += patch.applied ? ` | hot-applied (${patch.detail})` : ` | ${patch.detail}`;
2836
+ applied = patch.applied;
2837
+ }
2838
+ }
2839
+ if (!opts.json) {
2840
+ process.stdout.write(`
2841
+ Applied: ${detail}
2842
+ `);
2843
+ if (!opts.dryRun && !applied) {
2844
+ process.stdout.write(
2845
+ "A running OpenCode session recomputes routing on its next message (within 15 s). Restart to reload the UI display now.\n"
2846
+ );
2847
+ }
2848
+ }
2849
+ return 0;
2850
+ }
2851
+ async function cmdStatus(opts) {
2852
+ const result = await build(opts);
2853
+ const ledger = new LedgerStore(ledgerFile());
2854
+ const rows = statusRows(result, ledger);
2855
+ if (opts.json) {
2856
+ const usage = {};
2857
+ for (const entry of activeEntries(result.table)) usage[entry.id] = { pct: usagePercent(result, ledger, entry.id) };
2858
+ process.stdout.write(JSON.stringify({ table: result.table, rows, usage }, null, 2) + "\n");
2859
+ } else {
2860
+ process.stdout.write(formatStatus(result.table, rows) + "\n");
2861
+ }
2862
+ return 0;
2863
+ }
2864
+ async function cmdPlan(profile, opts) {
2865
+ if (!isPlan(profile)) throw new UsageError(`plan must be one of ${PLAN_CHOICES}`);
2866
+ const file = opts.scope === "global" ? globalRouterConfigFile() : projectRouterConfigFile(opts.cwd);
2867
+ editJsonFile(file, [{ path: ["plan"], value: profile }]);
2868
+ process.stdout.write(`Plan set to "${profile}" in ${file}
2869
+ `);
2870
+ if (opts.scope === "global") {
2871
+ const projectFile = projectRouterConfigFile(opts.cwd);
2872
+ const projectPlan = readJson(projectFile)?.plan;
2873
+ if (projectPlan !== void 0 && projectPlan !== profile) {
2874
+ process.stdout.write(
2875
+ `Note: ${projectFile} sets "plan": ${JSON.stringify(projectPlan)}, which overrides the global plan in this project.
2876
+ `
2877
+ );
2878
+ }
2879
+ }
2880
+ process.stdout.write("\n");
2881
+ return cmdRefresh({ ...opts, plan: void 0 });
2882
+ }
2883
+ function cmdCat(catalog) {
2884
+ const meta = catalogMeta(catalog);
2885
+ process.stdout.write(`Catalog: ${meta.path}
2886
+ `);
2887
+ process.stdout.write(`Modified: ${meta.mtime ?? "missing"}
2888
+ `);
2889
+ process.stdout.write(`Routed models: ${meta.total}
2890
+ `);
2891
+ return 0;
2892
+ }
2893
+ function cmdInstall(opts) {
2894
+ const result = installCommands(opts.scope, opts.cwd);
2895
+ process.stdout.write(`Wrote ${result.files.length} command files to ${result.commandDir}
2896
+ `);
2897
+ process.stdout.write(
2898
+ result.configCreated ? `Wrote starter config: ${result.configPath}
2899
+ ` : `Kept existing config: ${result.configPath}
2900
+ `
2901
+ );
2902
+ return 0;
2903
+ }
2904
+ function usageError(error) {
2905
+ process.stderr.write(`error: ${error.message}
2906
+
2907
+ Run "opencode-router --help" for usage.
2908
+ `);
2909
+ return 2;
2910
+ }
2911
+ async function main(argv, cwd = process.cwd()) {
2912
+ try {
2913
+ const { command, args, help, opts } = parseCommandLine(argv, cwd);
2914
+ if (!command || command === "help" || help) {
2915
+ process.stdout.write(HELP);
2916
+ return 0;
2917
+ }
2918
+ switch (command) {
2919
+ case "refresh":
2920
+ return await cmdRefresh(opts);
2921
+ case "status":
2922
+ return await cmdStatus(opts);
2923
+ case "plan":
2924
+ return await cmdPlan(args[0], opts);
2925
+ case "cat":
2926
+ return cmdCat(opts.catalog);
2927
+ case "install":
2928
+ return cmdInstall(opts);
2929
+ case "eval":
2930
+ return await runEval({ plan: opts.plan, catalog: opts.catalog, json: opts.json, projectDir: opts.cwd });
2931
+ default:
2932
+ throw new UsageError(`unknown command "${command}"`);
2933
+ }
2934
+ } catch (error) {
2935
+ if (error instanceof UsageError) return usageError(error);
2936
+ throw error;
2937
+ }
2938
+ }
2939
+
2940
+ // src/bin.ts
2941
+ main(process.argv.slice(2)).then(
2942
+ (code) => {
2943
+ process.exitCode = code;
2944
+ },
2945
+ (error) => {
2946
+ process.stderr.write(`opencode-router failed: ${error instanceof Error ? error.message : String(error)}
2947
+ `);
2948
+ process.exitCode = 1;
2949
+ }
2950
+ );
2951
+ //# sourceMappingURL=bin.js.map