auto-model-router 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.omp-plugin/marketplace.json +2 -2
- package/README.md +117 -17
- package/package.json +1 -1
- package/src/cli/connect.ts +131 -2
- package/src/cli/harnesses.ts +277 -0
- package/src/cost/ledger.ts +9 -2
- package/src/cost/types.ts +7 -0
- package/src/cost/views.ts +27 -10
- package/src/index.ts +1 -0
- package/src/server/http.ts +3 -1
- package/src/server/turn.ts +4 -0
- package/src/util/sqlite.ts +12 -1
- package/test/connect-harnesses.test.ts +299 -0
- package/test/fixtures/connect/cline-providers.json +16 -0
- package/test/fixtures/connect/continue-config.yaml +14 -0
- package/test/fixtures/connect/opencode.json +15 -0
- package/test/fixtures/harness/cline-cli-connected.json +817 -0
- package/test/harness-requests.test.ts +13 -2
- package/test/migrations.test.ts +10 -2
- package/test/trust-attribution.test.ts +2 -2
- package/test/turn.test.ts +23 -0
- package/test/views.test.ts +39 -13
- package/tools/capture-proxy.ts +1 -1
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "auto-model-router: a local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.20.0",
|
|
11
11
|
"pluginRoot": "."
|
|
12
12
|
},
|
|
13
13
|
"plugins": [
|
|
14
14
|
{
|
|
15
15
|
"name": "auto-model-router",
|
|
16
16
|
"description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter. Runs in-process, routes per turn by price and task complexity, with budget caps, mid-stream escalation, and cache-aware hysteresis.",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.20.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/README.md
CHANGED
|
@@ -472,8 +472,17 @@ cline -P openai -m auto "your task"
|
|
|
472
472
|
Verified live with cline 3.0 (captured request:
|
|
473
473
|
`test/fixtures/harness/cline-cli.json`). The CLI sends native tool calls
|
|
474
474
|
(`read_files`, `search_codebase`, `run_commands`, `fetch_web_content`, …),
|
|
475
|
-
all in `digest.toolAliases
|
|
476
|
-
|
|
475
|
+
all in `digest.toolAliases`. No session id or hooks.
|
|
476
|
+
|
|
477
|
+
`auto-model-router connect` writes this itself, and writes one thing the
|
|
478
|
+
`auth` command cannot: Cline's provider store
|
|
479
|
+
(`~/.cline/data/settings/providers.json`) has a `settings.headers` map with no
|
|
480
|
+
CLI flag behind it, so a file written by hand is what finally gives Cline a
|
|
481
|
+
harness id. Verified live on cline 3.0.61 by pointing it at a recording
|
|
482
|
+
server — `test/fixtures/harness/cline-cli-connected.json` is that capture, with
|
|
483
|
+
`X-Omp-Harness: cline` on it. That one file also serves the **VS Code
|
|
484
|
+
extension**, which reads the same store since its settings migration, so the
|
|
485
|
+
extension needs no separate recipe (and neither does any editor that hosts it).
|
|
477
486
|
|
|
478
487
|
### Kilo Code CLI
|
|
479
488
|
|
|
@@ -534,10 +543,11 @@ id or hooks: the digest applies only through summarising compaction.
|
|
|
534
543
|
|
|
535
544
|
### Cline (VS Code)
|
|
536
545
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
546
|
+
Nothing extra: the extension reads the same provider store as the CLI
|
|
547
|
+
(`~/.cline/data/settings/providers.json`), so `connect` has already configured
|
|
548
|
+
it — pick the *OpenAI Compatible* provider and the `auto` model. By hand, set
|
|
549
|
+
the base URL to `http://127.0.0.1:8788/v1`, any API key, and the model id
|
|
550
|
+
`auto` (or `auto-cheap` / `auto-max`). Its tool names are in
|
|
541
551
|
`digest.toolAliases`, and the digest applies only through summarising
|
|
542
552
|
compaction.
|
|
543
553
|
|
|
@@ -564,9 +574,16 @@ SSE frame, which is why the router's final summary frame is shaped as a
|
|
|
564
574
|
chunk with no choices. Its tool names (`read`, `grep`, `glob`, `bash`,
|
|
565
575
|
`webfetch`) match the router's canonical list.
|
|
566
576
|
|
|
577
|
+
`auto-model-router connect` writes both halves of this: the provider block
|
|
578
|
+
(other providers, MCP servers and `$schema` untouched) and the plugin below.
|
|
579
|
+
The config directory is `~/.config/opencode` on **every** platform, Windows
|
|
580
|
+
included — OpenCode uses the XDG layout there rather than `%APPDATA%`.
|
|
581
|
+
|
|
567
582
|
**Native features (OpenCode plugin API).** Copy
|
|
568
583
|
`opencode-plugin/auto-model-router.ts` to `~/.config/opencode/plugin/` (or a
|
|
569
|
-
project's `.opencode/plugin/`); OpenCode loads it on start
|
|
584
|
+
project's `.opencode/plugin/`); OpenCode loads it on start (`plugins/`, the
|
|
585
|
+
name current docs use, is loaded too — both were confirmed on opencode 1.18).
|
|
586
|
+
It adds:
|
|
570
587
|
|
|
571
588
|
- **Session identity** — `X-Omp-Session`, `X-Omp-Harness` (`opencode`, or
|
|
572
589
|
`OMP_HARNESS_ID`) and `X-Omp-Subagent` for sessions with a parent, through
|
|
@@ -581,6 +598,57 @@ No `/router` command (OpenCode commands are markdown files, not plugin
|
|
|
581
598
|
hooks): use `auto-model-router report` on the terminal, or the router's
|
|
582
599
|
HTTP endpoints.
|
|
583
600
|
|
|
601
|
+
### Continue
|
|
602
|
+
|
|
603
|
+
```yaml
|
|
604
|
+
# ~/.continue/config.yaml
|
|
605
|
+
name: auto-model-router
|
|
606
|
+
version: 0.0.1
|
|
607
|
+
schema: v1
|
|
608
|
+
models:
|
|
609
|
+
- name: auto-model-router
|
|
610
|
+
provider: openai
|
|
611
|
+
model: auto
|
|
612
|
+
apiBase: http://127.0.0.1:8788/v1
|
|
613
|
+
apiKey: local
|
|
614
|
+
roles: [chat, edit, apply, summarize]
|
|
615
|
+
capabilities: [tool_use, image_input]
|
|
616
|
+
requestOptions:
|
|
617
|
+
headers:
|
|
618
|
+
X-Omp-Harness: continue
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`connect` writes all three profiles as entries like this one, editing the YAML
|
|
622
|
+
*document* rather than re-serialising it, so a hand-written config keeps its
|
|
623
|
+
comments, key order and other models; entries are matched by `name`, so a later
|
|
624
|
+
connect replaces them in place. Pick `auto-model-router` in the model dropdown.
|
|
625
|
+
This is Continue's documented assistant schema rather than a shape captured
|
|
626
|
+
here — Continue was not run against the router — and `config.yaml` takes
|
|
627
|
+
precedence over the older `config.json` if you still have one.
|
|
628
|
+
|
|
629
|
+
### Cursor
|
|
630
|
+
|
|
631
|
+
Cursor has no provider file to write: the OpenAI override is an application
|
|
632
|
+
setting in the editor's own state. `connect` prints the values instead —
|
|
633
|
+
Cursor Settings → Models → OpenAI API Key, enable the base-URL override, then
|
|
634
|
+
the base URL, the key and `auto` as a custom model.
|
|
635
|
+
|
|
636
|
+
Two limits worth knowing before you try. Cursor proxies chat through **its own
|
|
637
|
+
servers** with your key attached, so the base URL has to be reachable from the
|
|
638
|
+
internet — a `127.0.0.1` router or a LAN team edition will never be called. And
|
|
639
|
+
the override carries no custom header, so those turns arrive with no
|
|
640
|
+
`X-Omp-Harness` id and share the unnamed budget.
|
|
641
|
+
|
|
642
|
+
### Windsurf
|
|
643
|
+
|
|
644
|
+
Windsurf has no custom base-URL field at all: its bring-your-own-key page takes
|
|
645
|
+
first-party provider keys, and the files under `~/.codeium/<channel>` are MCP
|
|
646
|
+
servers, rules and skills. So the route in is an extension. Windsurf is a VS
|
|
647
|
+
Code fork, and **Cline** reads the provider store `connect` already wrote
|
|
648
|
+
(`~/.cline/data/settings/providers.json`) — installing the extension is the
|
|
649
|
+
whole configuration. Continue works the same way against `~/.continue/config.yaml`.
|
|
650
|
+
`connect` says exactly this when it finds a Windsurf install.
|
|
651
|
+
|
|
584
652
|
### The OpenRouter key
|
|
585
653
|
|
|
586
654
|
**omp does not need to be authenticated to OpenRouter.** On a routed turn omp
|
|
@@ -694,7 +762,9 @@ virtual profile it picked. Every routed response carries
|
|
|
694
762
|
|
|
695
763
|
The ledger records every dispatch: model decided and served, tier, provider,
|
|
696
764
|
tokens (including cached), reported cost, time to first token, total latency,
|
|
697
|
-
escalation signal, error
|
|
765
|
+
escalation signal, error, and the agentdox context scope the turn carried (the
|
|
766
|
+
project it belongs to; NULL for a turn that carried none, and for every row
|
|
767
|
+
written before v0.19.0). Three views aggregate it, all from the same
|
|
698
768
|
`buildUsageReport` in `src/cost/report.ts`:
|
|
699
769
|
|
|
700
770
|
- `/router report` in omp — a fullscreen hub with the `/models` look: views
|
|
@@ -706,10 +776,12 @@ escalation signal, error. Three views aggregate it, all from the same
|
|
|
706
776
|
transcript. Falls back to reading the ledger directly if the router is
|
|
707
777
|
unreachable.
|
|
708
778
|
- `auto-model-router report --days 7 [--harness <id>] [--json]` on the terminal.
|
|
709
|
-
- `auto-model-router export --days 30 [--harness a,b] [--json]`: one row per day, harness
|
|
710
|
-
and
|
|
711
|
-
`
|
|
712
|
-
|
|
779
|
+
- `auto-model-router export --days 30 [--harness a,b] [--json]`: one row per day, harness,
|
|
780
|
+
model and context scope (dispatches, tokens, spend, escalations, errors) as CSV — the
|
|
781
|
+
`scope` column is last, and is empty for a turn that carried none. Also
|
|
782
|
+
`GET /v1/router/export?days=&harness=[&format=json]`; `GET /v1/router/spend?sinceMs=&harness=[&scope=]`
|
|
783
|
+
gives spend over a harness set since an instant, narrowed to one context scope when `scope`
|
|
784
|
+
is given (a project's own bill), and `GET /v1/router/feedback?days=&harness=`
|
|
713
785
|
lists verdicts by model and the recent ones with the harness that gave them, and
|
|
714
786
|
`GET /v1/router/decisions?harness=&days=|since=&slug=&tier=&limit=` is the decision trail
|
|
715
787
|
itself, newest first, each turn with its reasons, the classifier's view, forecast against
|
|
@@ -1309,9 +1381,21 @@ embed extension registers the remote router as omp's provider with that key inst
|
|
|
1309
1381
|
binding a local one, and the toast, `/router` hub and digest extensions talk to it. Nothing
|
|
1310
1382
|
is classified or selected locally; the remote router is the router. The same command adds
|
|
1311
1383
|
the extensions to omp's config, installs the Hermes plugins and points them at the remote,
|
|
1312
|
-
adds the Codex provider and the Aider settings,
|
|
1313
|
-
the
|
|
1314
|
-
|
|
1384
|
+
adds the Codex provider and the Aider settings, writes Claude Code's settings file, merges
|
|
1385
|
+
the provider into OpenCode's `opencode.json` (and copies its plugin), Cline's
|
|
1386
|
+
`providers.json` and Continue's `config.yaml`, and prints what to set by hand for Cursor
|
|
1387
|
+
and Windsurf. `--harness omp,hermes` restricts it (`omp`, `hermes`, `codex`, `aider`,
|
|
1388
|
+
`claude`, `opencode`, `cline`, `continue`, `cursor`, `windsurf`); `--dry-run` shows the
|
|
1389
|
+
changes. Delete `remote.json` to go back to a local router. (`join` is an alias.)
|
|
1390
|
+
|
|
1391
|
+
**What gets written and what gets printed.** A harness is configured only where its file
|
|
1392
|
+
format is documented and, wherever the harness could be run here, seen to be read: omp,
|
|
1393
|
+
Hermes, Codex, Aider, Claude Code, OpenCode, Cline and Continue. Cursor and Windsurf keep
|
|
1394
|
+
their provider settings in application state — an editor's own database, a vendor's
|
|
1395
|
+
account page — so `connect` reports them as `manual` and prints the base URL, key and
|
|
1396
|
+
model to paste. That is deliberate: a config key invented for them would write a file that
|
|
1397
|
+
silently does nothing while the run reported success. Every write is idempotent, backs the
|
|
1398
|
+
previous file up, and touches only its own keys.
|
|
1315
1399
|
|
|
1316
1400
|
`connect` also writes omp's `models.yml` (a managed block, other providers untouched, the
|
|
1317
1401
|
previous file backed up). That entry is what makes `auto-model-router/auto` resolvable at
|
|
@@ -1470,10 +1554,12 @@ plus a harness header; the rest needs the harness's own hook API.
|
|
|
1470
1554
|
| Hermes | provider plugin | yes | yes (native plugin) | yes (native plugin) | no | text | yes (native plugin) | on demand | no |
|
|
1471
1555
|
| Codex CLI | Responses API wire | yes | yes (from body) | yes (from body) | no | no | compaction only | no | no |
|
|
1472
1556
|
| Aider | config only | via model settings | no | no | no | no | no tools | no | no |
|
|
1473
|
-
| Cline CLI | config only |
|
|
1557
|
+
| Cline (CLI + VS Code) | config only | yes | no | no | no | no | compaction only | no | no |
|
|
1474
1558
|
| Kilo Code CLI | config only | yes | no | no | no | no | compaction only | no | no |
|
|
1475
1559
|
| Roo Code (VS Code) | config only | yes | no | no | no | no | compaction only | no | no |
|
|
1476
|
-
|
|
|
1560
|
+
| Continue | config only | yes | no | no | no | no | compaction only | no | no |
|
|
1561
|
+
| Cursor | manual (no config file; needs a public URL) | no | no | no | no | no | compaction only | no | no |
|
|
1562
|
+
| Windsurf | via the Cline or Continue extension | as that extension | no | no | no | no | compaction only | no | no |
|
|
1477
1563
|
| OpenCode | config + plugin | yes | yes (plugin) | yes (plugin) | yes (plugin) | no | yes (plugin) | no | no |
|
|
1478
1564
|
| Claude Code | needs an Anthropic Messages wire module | — | — | — | — | — | — | — | — |
|
|
1479
1565
|
|
|
@@ -1555,6 +1641,20 @@ a team posts exactly what it always did, so its block is byte-identical; an olde
|
|
|
1555
1641
|
agentdox ignores the keys it does not know. `context.layers: false` is the kill
|
|
1556
1642
|
switch — the headers are still parsed but nothing new goes to agentdox.
|
|
1557
1643
|
|
|
1644
|
+
### Charging spend back to a project
|
|
1645
|
+
|
|
1646
|
+
Every ledger row records the scope the bridge resolved for that turn — the
|
|
1647
|
+
request's `X-Agentdox-Scope`, or `context.defaultScope` behind it — so the money
|
|
1648
|
+
and the project are the same row. That is what a front door bills from: it names
|
|
1649
|
+
a project's scope on the turns it forwards, then reads its share back with
|
|
1650
|
+
`GET /v1/router/spend?sinceMs=&scope=<scope>` (composable with `harness=`, so one
|
|
1651
|
+
member's spend on one project is one call), or takes the whole split from
|
|
1652
|
+
`GET /v1/router/export`, whose rows now group by day, harness, model **and**
|
|
1653
|
+
scope and carry the scope as their last CSV column. `decisionEntries` (and
|
|
1654
|
+
`GET /v1/router/decisions`) carry it on each turn too. Rows written before
|
|
1655
|
+
v0.19.0, and turns that carried no scope at all, store NULL and export as `""` —
|
|
1656
|
+
old ledgers open and gain the column, they just have nothing to charge.
|
|
1657
|
+
|
|
1558
1658
|
### The origin fingerprint
|
|
1559
1659
|
|
|
1560
1660
|
The scope is the folder's name, and folder names collide: two unrelated repositories
|
package/package.json
CHANGED
package/src/cli/connect.ts
CHANGED
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
* Claude Code ~/.claude/settings.json gains the base URL (its `env` block) and
|
|
18
18
|
* `apiKeyHelper` running `auto-model-router token`, so no key
|
|
19
19
|
* sits in its environment or on disk for it
|
|
20
|
+
* OpenCode ~/.config/opencode/opencode.json gains the provider block, and
|
|
21
|
+
* plugin/ the native plugin
|
|
22
|
+
* Cline ~/.cline/data/settings/providers.json gains the
|
|
23
|
+
* openai-compatible provider — the CLI and the extension share it
|
|
24
|
+
* Continue ~/.continue/config.yaml gains the three profiles as models
|
|
25
|
+
*
|
|
26
|
+
* Cursor and Windsurf keep their provider settings in application state rather
|
|
27
|
+
* than in a file, so they are PRINTED, not written; see configureExtraHarnesses
|
|
28
|
+
* for where that line is drawn and why.
|
|
20
29
|
*
|
|
21
30
|
* Every write is idempotent and announced. `--profile` persists the
|
|
22
31
|
* environment lines (shell rc on POSIX, user environment on Windows).
|
|
@@ -34,6 +43,7 @@ import { executablePath, materializePackage, readEmbeddedPackage } from "./embed
|
|
|
34
43
|
import { fetchSkills, installSkills, type SkillsBundle, type SkillsInstallReport, type SkillsTarget } from "./skills.ts";
|
|
35
44
|
import { pickStore, saveRefreshToken, type StoreDeps, type StoreKind } from "./credential-store.ts";
|
|
36
45
|
import { flagString, type CliArgs } from "./args.ts";
|
|
46
|
+
import { cursorSnippet, mergeClineProviders, mergeContinueConfig, mergeOpenCodeConfig, windsurfSnippet, type ManualSnippet } from "./harnesses.ts";
|
|
37
47
|
|
|
38
48
|
export interface ConnectOptions {
|
|
39
49
|
url: string;
|
|
@@ -42,7 +52,7 @@ export interface ConnectOptions {
|
|
|
42
52
|
name: string;
|
|
43
53
|
profile: boolean;
|
|
44
54
|
dryRun: boolean;
|
|
45
|
-
/** Restrict to these harnesses (omp, hermes, codex, aider, claude); empty ⇒ every one detected. */
|
|
55
|
+
/** Restrict to these harnesses (omp, hermes, codex, aider, claude, opencode, cline, continue, cursor, windsurf); empty ⇒ every one detected. */
|
|
46
56
|
only: string[];
|
|
47
57
|
env: Record<string, string | undefined>;
|
|
48
58
|
home: string;
|
|
@@ -84,6 +94,12 @@ export interface ConnectReport {
|
|
|
84
94
|
skipped: string[];
|
|
85
95
|
envLines: string[];
|
|
86
96
|
notes: string[];
|
|
97
|
+
/**
|
|
98
|
+
* Harnesses whose provider settings live in application state rather than a
|
|
99
|
+
* documented file: what to paste into their settings UI. Never a guess at a
|
|
100
|
+
* file — see harnesses.ts for why the line is drawn on confidence.
|
|
101
|
+
*/
|
|
102
|
+
manual: ManualSnippet[];
|
|
87
103
|
/** What the remote's skills bundle did, when there was one. */
|
|
88
104
|
skills?: SkillsInstallReport;
|
|
89
105
|
/** Files that gained (or lost) the team-context MCP server. */
|
|
@@ -278,7 +294,7 @@ export function hasForeignRouterProvider(text: string): boolean {
|
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
export function connectRemote(o: ConnectOptions): ConnectReport {
|
|
281
|
-
const report: ConnectReport = { remoteFile: "", configured: [], skipped: [], envLines: [], notes: [] };
|
|
297
|
+
const report: ConnectReport = { remoteFile: "", configured: [], skipped: [], envLines: [], notes: [], manual: [] };
|
|
282
298
|
const write = (path: string, content: string): void => {
|
|
283
299
|
if (o.dryRun) return;
|
|
284
300
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -405,6 +421,8 @@ export function connectRemote(o: ConnectOptions): ConnectReport {
|
|
|
405
421
|
}
|
|
406
422
|
report.configured.push(`Claude Code (${settingsPath}: env.ANTHROPIC_BASE_URL + apiKeyHelper; open a new session)`);
|
|
407
423
|
} else report.skipped.push("Claude Code (not on PATH and no ~/.claude)");
|
|
424
|
+
|
|
425
|
+
configureExtraHarnesses(o, report, write);
|
|
408
426
|
report.envLines.unshift(`AUTO_MODEL_ROUTER_URL=${o.url}`, `AUTO_MODEL_ROUTER_API_KEY=${o.key}`);
|
|
409
427
|
report.envLines = [...new Set(report.envLines)];
|
|
410
428
|
|
|
@@ -463,6 +481,112 @@ export function connectRemote(o: ConnectOptions): ConnectReport {
|
|
|
463
481
|
return report;
|
|
464
482
|
}
|
|
465
483
|
|
|
484
|
+
/**
|
|
485
|
+
* The harnesses added in 0.20.0, kept out of `connectRemote` only for its
|
|
486
|
+
* length: same contract, same report, same idempotence.
|
|
487
|
+
*
|
|
488
|
+
* Two shapes appear here. The automated ones own a documented config file, and
|
|
489
|
+
* `connect` merges its own keys into it (harnesses.ts holds those merges, and
|
|
490
|
+
* says why each format is trusted). The manual ones keep their provider
|
|
491
|
+
* settings in application state — a VS Code `globalState` blob, an IDE's own
|
|
492
|
+
* database — where there is no file to edit, so `connect` prints the values to
|
|
493
|
+
* paste and says so in the report rather than writing something that would look
|
|
494
|
+
* like success and do nothing.
|
|
495
|
+
*
|
|
496
|
+
* A manual harness is announced when it is INSTALLED, or when `--harness` named
|
|
497
|
+
* it outright; a user without Cursor should not be read a Cursor recipe.
|
|
498
|
+
*/
|
|
499
|
+
function configureExtraHarnesses(o: ConnectOptions, report: ConnectReport, write: (path: string, content: string) => void): void {
|
|
500
|
+
const scope = o.agentdoxScope ?? "";
|
|
501
|
+
const named = (h: string): boolean => o.only.includes(h);
|
|
502
|
+
|
|
503
|
+
// OpenCode: the provider block the README documents, plus the plugin that
|
|
504
|
+
// gives it session identity, the toast and the digest — the same two-part
|
|
505
|
+
// install Hermes gets, since OpenCode is the other harness with a real hook API.
|
|
506
|
+
const ocDir = openCodeDir(o);
|
|
507
|
+
if (wants(o, "opencode") && (existsSync(ocDir) || o.pathHas("opencode"))) {
|
|
508
|
+
const p = join(ocDir, "opencode.json");
|
|
509
|
+
const before = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
510
|
+
const after = mergeOpenCodeConfig(before, o.url, o.key, scope);
|
|
511
|
+
if (after === null && before.trim() !== "" && !isJsonObject(before)) {
|
|
512
|
+
report.notes.push(`${p} is not a JSON object; left alone — add the auto-model-router provider by hand`);
|
|
513
|
+
} else if (after !== null) {
|
|
514
|
+
if (before !== "" && !o.dryRun) writeFileSync(`${p}.${backupStamp()}.bak`, before, "utf8");
|
|
515
|
+
write(p, after);
|
|
516
|
+
}
|
|
517
|
+
if (!o.dryRun) cpSync(join(o.packageDir, "opencode-plugin", "auto-model-router.ts"), join(ocDir, "plugin", "auto-model-router.ts"));
|
|
518
|
+
report.configured.push(`OpenCode (${p} + ${join(ocDir, "plugin")}; model auto-model-router/auto)`);
|
|
519
|
+
} else report.skipped.push("OpenCode (not on PATH and no ~/.config/opencode)");
|
|
520
|
+
|
|
521
|
+
// Cline: one provider store for the CLI and, since its settings migration, the
|
|
522
|
+
// VS Code extension — so this single write serves both, and every editor that
|
|
523
|
+
// hosts the extension (see the Windsurf snippet below).
|
|
524
|
+
const clineDataDir = clineDir(o);
|
|
525
|
+
let clineConfigured = false;
|
|
526
|
+
if (wants(o, "cline") && (existsSync(clineDataDir) || o.pathHas("cline"))) {
|
|
527
|
+
const p = join(clineDataDir, "settings", "providers.json");
|
|
528
|
+
const before = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
529
|
+
const after = mergeClineProviders(before, o.url, o.key, new Date().toISOString(), scope);
|
|
530
|
+
if (after === null && before.trim() !== "" && !isJsonObject(before)) {
|
|
531
|
+
report.notes.push(`${p} is not a JSON object; left alone — run \`cline auth -p openai -b ${o.url}/v1 -k <key> -m auto\` instead`);
|
|
532
|
+
} else if (after !== null) {
|
|
533
|
+
if (before !== "" && !o.dryRun) writeFileSync(`${p}.${backupStamp()}.bak`, before, "utf8");
|
|
534
|
+
write(p, after);
|
|
535
|
+
}
|
|
536
|
+
clineConfigured = true;
|
|
537
|
+
report.configured.push(`Cline (${p}; \`cline -m auto\`, or the VS Code extension)`);
|
|
538
|
+
} else report.skipped.push("Cline (not on PATH and no ~/.cline)");
|
|
539
|
+
|
|
540
|
+
// Continue: one assistant file holds the models, so the three profiles go in as
|
|
541
|
+
// three entries and the user picks between them in the model dropdown.
|
|
542
|
+
const continueDir = join(o.home, ".continue");
|
|
543
|
+
if (wants(o, "continue") && (existsSync(continueDir) || o.pathHas("cn"))) {
|
|
544
|
+
const p = join(continueDir, "config.yaml");
|
|
545
|
+
const before = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
546
|
+
const after = mergeContinueConfig(before, o.url, o.key, scope);
|
|
547
|
+
if (after === null && before.trim() !== "") {
|
|
548
|
+
report.notes.push(`${p} is not an assistant file we can edit; left alone — add the models entry by hand`);
|
|
549
|
+
} else if (after !== null) {
|
|
550
|
+
if (before !== "" && !o.dryRun) writeFileSync(`${p}.${backupStamp()}.bak`, before, "utf8");
|
|
551
|
+
write(p, after);
|
|
552
|
+
// config.yaml wins over the older config.json, so say so rather than let a
|
|
553
|
+
// user wonder why the settings they had stopped applying.
|
|
554
|
+
if (before === "" && existsSync(join(continueDir, "config.json"))) report.notes.push(`${join(continueDir, "config.json")} is Continue's older format and config.yaml now takes precedence over it`);
|
|
555
|
+
}
|
|
556
|
+
report.configured.push(`Continue (${p}; pick auto-model-router in the model dropdown)`);
|
|
557
|
+
} else report.skipped.push("Continue (no ~/.continue)");
|
|
558
|
+
|
|
559
|
+
// Cursor and Windsurf keep provider settings where no file can reach them.
|
|
560
|
+
if (wants(o, "cursor") && (named("cursor") || existsSync(join(o.home, ".cursor")) || o.pathHas("cursor"))) report.manual.push(cursorSnippet(o.url, o.key));
|
|
561
|
+
else report.skipped.push("Cursor (no ~/.cursor)");
|
|
562
|
+
if (wants(o, "windsurf") && (named("windsurf") || existsSync(join(o.home, ".codeium")) || o.pathHas("windsurf"))) report.manual.push(windsurfSnippet(clineConfigured));
|
|
563
|
+
else report.skipped.push("Windsurf (no ~/.codeium)");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Cline's state directory: `--data-dir`'s default, `~/.cline/data`, or `CLINE_DATA_DIR` when the user moved it. */
|
|
567
|
+
function clineDir(o: ConnectOptions): string {
|
|
568
|
+
const d = o.env.CLINE_DATA_DIR;
|
|
569
|
+
return d !== undefined && d !== "" ? expand(d, o.home) : join(o.home, ".cline", "data");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** OpenCode's config home: `XDG_CONFIG_HOME`, else `~/.config` — the same on Windows, where it does not use APPDATA. */
|
|
573
|
+
function openCodeDir(o: ConnectOptions): string {
|
|
574
|
+
const xdg = o.env.XDG_CONFIG_HOME;
|
|
575
|
+
return join(xdg !== undefined && xdg !== "" ? expand(xdg, o.home) : join(o.home, ".config"), "opencode");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** A filename-safe timestamp for the `.bak` beside a file we are about to replace. */
|
|
579
|
+
const backupStamp = (): string => new Date().toISOString().replaceAll(":", "-");
|
|
580
|
+
|
|
581
|
+
const isJsonObject = (text: string): boolean => {
|
|
582
|
+
try {
|
|
583
|
+
const v = JSON.parse(text) as unknown;
|
|
584
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
466
590
|
/** What a team's one-time setup token is traded for. */
|
|
467
591
|
export interface IssuedCredential {
|
|
468
592
|
key: string;
|
|
@@ -597,6 +721,11 @@ export async function connectCommand(args: CliArgs): Promise<void> {
|
|
|
597
721
|
console.log(`${args.flags.has("dry-run") ? "would write" : "wrote"} ${report.remoteFile}${name === "" ? "" : ` for ${name}`}`);
|
|
598
722
|
for (const c of report.configured) console.log(` configured ${c}`);
|
|
599
723
|
for (const s of report.skipped) console.log(` skipped ${s}`);
|
|
724
|
+
// A manual harness gets the values printed rather than a file written; see configureExtraHarnesses.
|
|
725
|
+
for (const m of report.manual) {
|
|
726
|
+
console.log(` manual ${m.harness} — ${m.reason}`);
|
|
727
|
+
for (const l of m.lines) console.log(` ${l}`);
|
|
728
|
+
}
|
|
600
729
|
console.log("environment:");
|
|
601
730
|
for (const l of report.envLines) console.log(` ${process.platform === "win32" ? "$env:" : "export "}${process.platform === "win32" ? l.replace("=", '="') + '"' : l}`);
|
|
602
731
|
for (const n of report.notes) console.log(`note: ${n}`);
|