@bytesbrains/opencode-cruise 0.1.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/LICENSE +15 -0
- package/README.md +189 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +44 -0
- package/dist/auth.js.map +1 -0
- package/dist/base-url.d.ts +8 -0
- package/dist/base-url.d.ts.map +1 -0
- package/dist/base-url.js +30 -0
- package/dist/base-url.js.map +1 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +20 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetch-models.d.ts +23 -0
- package/dist/fetch-models.d.ts.map +1 -0
- package/dist/fetch-models.js +61 -0
- package/dist/fetch-models.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +27 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +95 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-url.d.ts +6 -0
- package/dist/mcp-url.d.ts.map +1 -0
- package/dist/mcp-url.js +22 -0
- package/dist/mcp-url.js.map +1 -0
- package/dist/models.d.ts +42 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +132 -0
- package/dist/models.js.map +1 -0
- package/dist/provider.d.ts +22 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +68 -0
- package/dist/provider.js.map +1 -0
- package/dist/resolve-api-key.d.ts +15 -0
- package/dist/resolve-api-key.d.ts.map +1 -0
- package/dist/resolve-api-key.js +43 -0
- package/dist/resolve-api-key.js.map +1 -0
- package/dist/tools.d.ts +124 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +231 -0
- package/dist/tools.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
BytesBrains Cruise for OpenCode
|
|
2
|
+
© 2026 BYTESBRAINS PTE. LTD. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This material is provided for use with the BytesBrains Cruise service and OpenCode.
|
|
5
|
+
|
|
6
|
+
You may use the recipes and examples on any machine you control to point OpenCode at Cruise.
|
|
7
|
+
|
|
8
|
+
You may not present a copy of this work as your own, or redistribute a modified copy under the
|
|
9
|
+
BytesBrains or Cruise name. Nothing here grants any right to the Cruise service itself, whose
|
|
10
|
+
use is governed by the terms of service of BytesBrains Pte. Ltd.
|
|
11
|
+
|
|
12
|
+
THE MATERIAL IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
13
|
+
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
14
|
+
NON-INFRINGEMENT. IN NO EVENT SHALL BYTESBRAINS PTE. LTD. BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
15
|
+
OTHER LIABILITY ARISING FROM OR IN CONNECTION WITH THIS MATERIAL OR ITS USE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://bytesbrains.com/brand/cruise-logo-480.png" alt="BytesBrains Cruise" width="280" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">BytesBrains Cruise for OpenCode</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Every model your Cruise key can reach — as an OpenCode provider / plugin —<br />
|
|
9
|
+
with budgets, per-project keys, and one cost ledger that stay on the gateway.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://bytesbrains.com/cruise"><img src="https://img.shields.io/badge/Product-bytesbrains.com%2Fcruise-111111" alt="Product" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@bytesbrains/opencode-cruise"><img src="https://img.shields.io/npm/v/@bytesbrains/opencode-cruise?label=npm" alt="npm" /></a>
|
|
15
|
+
<a href="https://opencode.ai/docs/providers/"><img src="https://img.shields.io/badge/Host-OpenCode-555" alt="OpenCode docs" /></a>
|
|
16
|
+
<a href="https://github.com/bytesbrains/cruise-opencode"><img src="https://img.shields.io/badge/Source-bytesbrains%2Fcruise--opencode-111111" alt="Source" /></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What this is
|
|
22
|
+
|
|
23
|
+
[BytesBrains Cruise](https://bytesbrains.com/cruise) is one OpenAI-compatible endpoint in front of
|
|
24
|
+
every model provider. This repository is the **[OpenCode](https://opencode.ai)** client: a
|
|
25
|
+
published plugin (and the verified recipe behind it) so sessions route through the gateway.
|
|
26
|
+
|
|
27
|
+
Your keys, budgets and ledger stay on the gateway. OpenCode only holds a `cru_` key and talks to
|
|
28
|
+
the base URL you configure.
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| **Product** | [bytesbrains.com/cruise](https://bytesbrains.com/cruise) |
|
|
33
|
+
| **Source** | [bytesbrains/cruise-opencode](https://github.com/bytesbrains/cruise-opencode) |
|
|
34
|
+
| **Host** | [OpenCode](https://opencode.ai) — [providers](https://opencode.ai/docs/providers/), [plugins](https://opencode.ai/docs/plugins/) |
|
|
35
|
+
| **Production API** | `https://cruise.bytesbrains.net/v1` |
|
|
36
|
+
| **Demo API** | `https://cruise-demo.bytesbrains.net/v1` |
|
|
37
|
+
|
|
38
|
+
**Status:** demo-verified; npm package `@bytesbrains/opencode-cruise` `0.1.0` — publish by
|
|
39
|
+
tagging `v0.1.0` after Trusted Publisher is configured (see
|
|
40
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md)). Track work in
|
|
41
|
+
[GitHub issues](https://github.com/bytesbrains/cruise-opencode/issues). Sister clients that
|
|
42
|
+
already ship: [cruise-vscode](https://github.com/bytesbrains/cruise-vscode),
|
|
43
|
+
[cruise-hermes](https://github.com/bytesbrains/cruise-hermes),
|
|
44
|
+
[openclaw-cruise](https://github.com/bytesbrains/openclaw-cruise),
|
|
45
|
+
[cruise-cursor-plugin](https://github.com/bytesbrains/cruise-cursor-plugin),
|
|
46
|
+
[cruise-claude-plugin](https://github.com/bytesbrains/cruise-claude-plugin).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Add the plugin and set a Cruise key. Once `v0.1.0` is tagged, OpenCode installs from npm via
|
|
53
|
+
the `plugin` array. The plugin registers provider `cruise`
|
|
54
|
+
(`npm: @ai-sdk/openai-compatible`, base URL with `/v1`) and fills models from live
|
|
55
|
+
`GET /v1/models` for that key — never a frozen catalogue.
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
// opencode.json
|
|
59
|
+
{
|
|
60
|
+
"$schema": "https://opencode.ai/config.json",
|
|
61
|
+
"plugin": ["@bytesbrains/opencode-cruise"],
|
|
62
|
+
// optional — prefer a lane once models are discovered:
|
|
63
|
+
// "model": "cruise/bb/agentic-coding"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
export CRUISE_API_KEY=cru_demo_… # or cru_live_…
|
|
69
|
+
# optional — defaults to production:
|
|
70
|
+
# export CRUISE_BASE_URL=https://cruise.bytesbrains.net/v1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or copy [`.env.example`](./.env.example) to `.env` for local rehearsal. You can also run
|
|
74
|
+
`/connect` in OpenCode and choose **Cruise API Key** (same provider id: `cruise`).
|
|
75
|
+
|
|
76
|
+
Manual custom provider (no plugin) if you only need two strings + pasted model ids:
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
// manual fallback (bucket A — two strings + models you paste from GET /v1/models)
|
|
80
|
+
{
|
|
81
|
+
"$schema": "https://opencode.ai/config.json",
|
|
82
|
+
"provider": {
|
|
83
|
+
"cruise": {
|
|
84
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
85
|
+
"name": "BytesBrains Cruise",
|
|
86
|
+
"env": ["CRUISE_API_KEY"],
|
|
87
|
+
"options": {
|
|
88
|
+
"baseURL": "https://cruise-demo.bytesbrains.net/v1",
|
|
89
|
+
"apiKey": "{env:CRUISE_API_KEY}"
|
|
90
|
+
},
|
|
91
|
+
"models": {
|
|
92
|
+
"bb/agentic-coding": { "name": "Agentic Coding (lane)" }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Prefer rehearsing on the demo host first.
|
|
100
|
+
|
|
101
|
+
### Budget tools (Cruise MCP)
|
|
102
|
+
|
|
103
|
+
The plugin registers read-only tools that call Cruise’s remote MCP with the same `cru_` key:
|
|
104
|
+
|
|
105
|
+
| Tool | Answers |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `cruise_list_models` | models/lanes this key can reach (optional `kind`, `modality`) |
|
|
108
|
+
| `cruise_get_budget` | project budget period, caps, `action`, and wallet |
|
|
109
|
+
| `cruise_get_spend` | month’s charges by model or lane |
|
|
110
|
+
| `cruise_setup` | check key + probe `get_budget`; with consent, merge `mcp.cruise` into `opencode.json` |
|
|
111
|
+
|
|
112
|
+
Production MCP: `https://cruise.bytesbrains.net/mcp` · Demo:
|
|
113
|
+
`https://cruise-demo.bytesbrains.net/mcp`. The key stays in `CRUISE_API_KEY` (or `/connect`) —
|
|
114
|
+
never in config. Optional host-managed MCP (same server) after `cruise_setup` with
|
|
115
|
+
`write_config=true`:
|
|
116
|
+
|
|
117
|
+
```jsonc
|
|
118
|
+
"mcp": {
|
|
119
|
+
"cruise": {
|
|
120
|
+
"type": "remote",
|
|
121
|
+
"url": "https://cruise.bytesbrains.net/mcp",
|
|
122
|
+
"oauth": false,
|
|
123
|
+
"headers": { "Authorization": "Bearer {env:CRUISE_API_KEY}" }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Develop from this repo
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
npm install
|
|
132
|
+
npm run typecheck
|
|
133
|
+
npm run build
|
|
134
|
+
npm test
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Demo rehearsal
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
cp .env.example .env # set CRUISE_API_KEY=cru_demo_… — never commit .env
|
|
141
|
+
npm run build
|
|
142
|
+
npm run rehearse:demo
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for what the script checks.
|
|
146
|
+
|
|
147
|
+
### Verified against demo
|
|
148
|
+
|
|
149
|
+
**2026-09-19 UTC** — `@bytesbrains/opencode-cruise` `0.1.0` (built locally), env:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
CRUISE_API_KEY=cru_demo_… # never committed
|
|
153
|
+
CRUISE_BASE_URL=https://cruise-demo.bytesbrains.net/v1
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Check | Result |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `npm run rehearse:demo` | passed |
|
|
159
|
+
| Live catalogue | plugin projection **63** ids from `GET /v1/models`, incl. `bb/agentic-coding` |
|
|
160
|
+
| Streamed chat | `POST /v1/chat/completions` SSE completed (`bb/agentic-coding` → `x-cruise-model: anthropic/claude-fable-5-1`, budget ok) |
|
|
161
|
+
| Tools | tool-bearing chat request accepted (demo fabricates the body) |
|
|
162
|
+
| Cruise MCP | `get_budget` + `list_models` over `/mcp` (see latest `rehearse:demo` run) |
|
|
163
|
+
|
|
164
|
+
Demo holds no provider credential — answers may be fabricated; the point is the wire.
|
|
165
|
+
|
|
166
|
+
### Refusals
|
|
167
|
+
|
|
168
|
+
Cruise declines with `error.code` (`budget_exhausted`, `wallet_exhausted`,
|
|
169
|
+
`measurement_stale`, …). Branch on that code — not HTTP status alone. The plugin logs
|
|
170
|
+
known refusal codes on `session.error` when the body is present.
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Conventions
|
|
174
|
+
|
|
175
|
+
- **Key in the environment** (`CRUISE_API_KEY`), never in a committed `opencode.json`.
|
|
176
|
+
- **Model ids are Cruise ids** from `GET /v1/models` for the presented key — lanes (`bb/…`)
|
|
177
|
+
preferred over pinned upstream ids.
|
|
178
|
+
- **No traffic anywhere but the configured Cruise base URL.** No telemetry, no second host.
|
|
179
|
+
- **Branch refusals on `error.code`**, not HTTP status alone (`budget_exhausted`,
|
|
180
|
+
`wallet_exhausted`, `measurement_stale`, …).
|
|
181
|
+
- **Rehearse on the demo** (`cruise-demo.bytesbrains.net` + `cru_demo_` key) before production.
|
|
182
|
+
|
|
183
|
+
See [AGENT.md](./AGENT.md) for agent working notes and [SECURITY.md](./SECURITY.md) for disclosure.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
© 2026 BYTESBRAINS PTE. LTD. See [LICENSE](./LICENSE).
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuthHook } from "@opencode-ai/plugin";
|
|
2
|
+
/**
|
|
3
|
+
* `/connect` entry for Cruise — stores a `cru_` key in OpenCode's auth store.
|
|
4
|
+
* Recipes should still prefer `CRUISE_API_KEY` in the environment.
|
|
5
|
+
*/
|
|
6
|
+
export declare const cruiseAuthHook: AuthHook;
|
|
7
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,QAqC5B,CAAC"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PROVIDER_ID } from "./constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* `/connect` entry for Cruise — stores a `cru_` key in OpenCode's auth store.
|
|
4
|
+
* Recipes should still prefer `CRUISE_API_KEY` in the environment.
|
|
5
|
+
*/
|
|
6
|
+
export const cruiseAuthHook = {
|
|
7
|
+
provider: PROVIDER_ID,
|
|
8
|
+
methods: [
|
|
9
|
+
{
|
|
10
|
+
type: "api",
|
|
11
|
+
label: "Cruise API Key",
|
|
12
|
+
prompts: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
key: "apiKey",
|
|
16
|
+
message: "Enter your BytesBrains Cruise API key",
|
|
17
|
+
placeholder: "cru_demo_… or cru_live_…",
|
|
18
|
+
validate: (value) => {
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
if (!trimmed.startsWith("cru_")) {
|
|
21
|
+
return "Expected a Cruise key starting with cru_";
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
async authorize(inputs) {
|
|
28
|
+
const key = inputs?.apiKey?.trim();
|
|
29
|
+
if (!key?.startsWith("cru_")) {
|
|
30
|
+
return { type: "failed" };
|
|
31
|
+
}
|
|
32
|
+
return { type: "success", key, provider: PROVIDER_ID };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
async loader(getAuth) {
|
|
37
|
+
const auth = await getAuth();
|
|
38
|
+
if (auth.type === "api" && typeof auth.key === "string" && auth.key) {
|
|
39
|
+
return { apiKey: auth.key };
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAa;IACtC,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE;QACP;YACE,IAAI,EAAE,KAAK;YACX,KAAK,EAAE,gBAAgB;YACvB,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,GAAG,EAAE,QAAQ;oBACb,OAAO,EAAE,uCAAuC;oBAChD,WAAW,EAAE,0BAA0B;oBACvC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;wBAClB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;wBAC7B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;4BAChC,OAAO,0CAA0C,CAAC;wBACpD,CAAC;wBACD,OAAO,SAAS,CAAC;oBACnB,CAAC;iBACF;aACF;YACD,KAAK,CAAC,SAAS,CAAC,MAAM;gBACpB,MAAM,GAAG,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;gBACnC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC7B,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;gBAC5B,CAAC;gBACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;YACzD,CAAC;SACF;KACF;IACD,KAAK,CAAC,MAAM,CAAC,OAAO;QAClB,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACpE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9B,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Only the documented production and demo Cruise HTTPS hosts.
|
|
3
|
+
* Rejects other hosts (including other `*.bytesbrains.net` names) so a
|
|
4
|
+
* tampered config cannot SSRF with the presented key.
|
|
5
|
+
* Invalid or disallowed values fall back to production.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveAllowedCruiseBaseUrl(baseUrl?: string): string;
|
|
8
|
+
//# sourceMappingURL=base-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-url.d.ts","sourceRoot":"","sources":["../src/base-url.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAepE"}
|
package/dist/base-url.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CRUISE_BASE_URL, CRUISE_DEMO_BASE_URL } from "./constants.js";
|
|
2
|
+
/** Hosts the plugin will send traffic to (SSRF-safe explicit allowlist). */
|
|
3
|
+
const ALLOWED_HOSTS = new Set([
|
|
4
|
+
new URL(CRUISE_BASE_URL).hostname,
|
|
5
|
+
new URL(CRUISE_DEMO_BASE_URL).hostname,
|
|
6
|
+
]);
|
|
7
|
+
/**
|
|
8
|
+
* Only the documented production and demo Cruise HTTPS hosts.
|
|
9
|
+
* Rejects other hosts (including other `*.bytesbrains.net` names) so a
|
|
10
|
+
* tampered config cannot SSRF with the presented key.
|
|
11
|
+
* Invalid or disallowed values fall back to production.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveAllowedCruiseBaseUrl(baseUrl) {
|
|
14
|
+
const candidate = (baseUrl ?? "").trim() || CRUISE_BASE_URL;
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(candidate);
|
|
17
|
+
if (url.protocol !== "https:") {
|
|
18
|
+
return CRUISE_BASE_URL;
|
|
19
|
+
}
|
|
20
|
+
const host = url.hostname.toLowerCase();
|
|
21
|
+
if (!ALLOWED_HOSTS.has(host)) {
|
|
22
|
+
return CRUISE_BASE_URL;
|
|
23
|
+
}
|
|
24
|
+
return candidate.replace(/\/+$/, "");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return CRUISE_BASE_URL;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=base-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-url.js","sourceRoot":"","sources":["../src/base-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEvE,4EAA4E;AAC5E,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC,QAAQ;IACjC,IAAI,GAAG,CAAC,oBAAoB,CAAC,CAAC,QAAQ;CACvC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CAAC,OAAgB;IAC1D,MAAM,SAAS,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,eAAe,CAAC;IAC5D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;QAC/B,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QACxC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,OAAO,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,eAAe,CAAC;IACzB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Public Cruise endpoints, env names, and OpenCode provider id. */
|
|
2
|
+
/** OpenCode provider id (`opencode.json` / `/connect`). */
|
|
3
|
+
export declare const PROVIDER_ID = "cruise";
|
|
4
|
+
/** Stable plugin id for diagnostics. */
|
|
5
|
+
export declare const PLUGIN_ID = "bytesbrains-cruise";
|
|
6
|
+
/** Production Cruise OpenAI-compatible base URL (includes `/v1`). */
|
|
7
|
+
export declare const CRUISE_BASE_URL = "https://cruise.bytesbrains.net/v1";
|
|
8
|
+
/** Demo Cruise base URL for rehearsal with `cru_demo_` keys. */
|
|
9
|
+
export declare const CRUISE_DEMO_BASE_URL = "https://cruise-demo.bytesbrains.net/v1";
|
|
10
|
+
/** Environment variable for the Cruise API key. */
|
|
11
|
+
export declare const CRUISE_API_KEY_ENV = "CRUISE_API_KEY";
|
|
12
|
+
/** Optional override for the Cruise base URL. */
|
|
13
|
+
export declare const CRUISE_BASE_URL_ENV = "CRUISE_BASE_URL";
|
|
14
|
+
/** Default model id recommended in docs (a lane, not a pinned upstream id). */
|
|
15
|
+
export declare const CRUISE_DEFAULT_MODEL_ID = "bb/agentic-coding";
|
|
16
|
+
/** npm package OpenCode uses for `/v1/chat/completions`. */
|
|
17
|
+
export declare const OPENAI_COMPATIBLE_NPM = "@ai-sdk/openai-compatible";
|
|
18
|
+
/** How long to wait for `GET /v1/models` during plugin config. */
|
|
19
|
+
export declare const MODELS_FETCH_TIMEOUT_MS = 10000;
|
|
20
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,2DAA2D;AAC3D,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,wCAAwC;AACxC,eAAO,MAAM,SAAS,uBAAuB,CAAC;AAE9C,qEAAqE;AACrE,eAAO,MAAM,eAAe,sCAAsC,CAAC;AAEnE,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,2CAA2C,CAAC;AAE7E,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAEnD,iDAAiD;AACjD,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAE3D,4DAA4D;AAC5D,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AAEjE,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,QAAS,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Public Cruise endpoints, env names, and OpenCode provider id. */
|
|
2
|
+
/** OpenCode provider id (`opencode.json` / `/connect`). */
|
|
3
|
+
export const PROVIDER_ID = "cruise";
|
|
4
|
+
/** Stable plugin id for diagnostics. */
|
|
5
|
+
export const PLUGIN_ID = "bytesbrains-cruise";
|
|
6
|
+
/** Production Cruise OpenAI-compatible base URL (includes `/v1`). */
|
|
7
|
+
export const CRUISE_BASE_URL = "https://cruise.bytesbrains.net/v1";
|
|
8
|
+
/** Demo Cruise base URL for rehearsal with `cru_demo_` keys. */
|
|
9
|
+
export const CRUISE_DEMO_BASE_URL = "https://cruise-demo.bytesbrains.net/v1";
|
|
10
|
+
/** Environment variable for the Cruise API key. */
|
|
11
|
+
export const CRUISE_API_KEY_ENV = "CRUISE_API_KEY";
|
|
12
|
+
/** Optional override for the Cruise base URL. */
|
|
13
|
+
export const CRUISE_BASE_URL_ENV = "CRUISE_BASE_URL";
|
|
14
|
+
/** Default model id recommended in docs (a lane, not a pinned upstream id). */
|
|
15
|
+
export const CRUISE_DEFAULT_MODEL_ID = "bb/agentic-coding";
|
|
16
|
+
/** npm package OpenCode uses for `/v1/chat/completions`. */
|
|
17
|
+
export const OPENAI_COMPATIBLE_NPM = "@ai-sdk/openai-compatible";
|
|
18
|
+
/** How long to wait for `GET /v1/models` during plugin config. */
|
|
19
|
+
export const MODELS_FETCH_TIMEOUT_MS = 10_000;
|
|
20
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,2DAA2D;AAC3D,MAAM,CAAC,MAAM,WAAW,GAAG,QAAQ,CAAC;AAEpC,wCAAwC;AACxC,MAAM,CAAC,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAE9C,qEAAqE;AACrE,MAAM,CAAC,MAAM,eAAe,GAAG,mCAAmC,CAAC;AAEnE,gEAAgE;AAChE,MAAM,CAAC,MAAM,oBAAoB,GAAG,wCAAwC,CAAC;AAE7E,mDAAmD;AACnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AAEnD,iDAAiD;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAErD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE3D,4DAA4D;AAC5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,2BAA2B,CAAC;AAEjE,kEAAkE;AAClE,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise refusal helpers — branch on `error.code`, not HTTP status alone.
|
|
3
|
+
*/
|
|
4
|
+
/** Known Cruise gateway refusal codes (public product behaviour). */
|
|
5
|
+
export declare const CRUISE_REFUSAL_CODES: readonly ["budget_exhausted", "wallet_exhausted", "measurement_stale", "model_not_found", "permission_error"];
|
|
6
|
+
export type CruiseRefusalCode = (typeof CRUISE_REFUSAL_CODES)[number];
|
|
7
|
+
/**
|
|
8
|
+
* Reads `error.code` from a Cruise-shaped error payload (nested under `error`
|
|
9
|
+
* or at the top level). Returns undefined when absent or not a string.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readCruiseErrorCode(payload: unknown): string | undefined;
|
|
12
|
+
/** True when the payload carries a known Cruise refusal `error.code`. */
|
|
13
|
+
export declare function isCruiseRefusal(payload: unknown): boolean;
|
|
14
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,+GAMvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAItE;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAWxE;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAGzD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise refusal helpers — branch on `error.code`, not HTTP status alone.
|
|
3
|
+
*/
|
|
4
|
+
/** Known Cruise gateway refusal codes (public product behaviour). */
|
|
5
|
+
export const CRUISE_REFUSAL_CODES = [
|
|
6
|
+
"budget_exhausted",
|
|
7
|
+
"wallet_exhausted",
|
|
8
|
+
"measurement_stale",
|
|
9
|
+
"model_not_found",
|
|
10
|
+
"permission_error",
|
|
11
|
+
];
|
|
12
|
+
const REFUSAL_SET = new Set(CRUISE_REFUSAL_CODES);
|
|
13
|
+
/**
|
|
14
|
+
* Reads `error.code` from a Cruise-shaped error payload (nested under `error`
|
|
15
|
+
* or at the top level). Returns undefined when absent or not a string.
|
|
16
|
+
*/
|
|
17
|
+
export function readCruiseErrorCode(payload) {
|
|
18
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const root = payload;
|
|
22
|
+
const nested = root.error && typeof root.error === "object" && !Array.isArray(root.error)
|
|
23
|
+
? root.error
|
|
24
|
+
: undefined;
|
|
25
|
+
const code = nested?.code ?? root.code;
|
|
26
|
+
return typeof code === "string" && code.trim() ? code.trim() : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** True when the payload carries a known Cruise refusal `error.code`. */
|
|
29
|
+
export function isCruiseRefusal(payload) {
|
|
30
|
+
const code = readCruiseErrorCode(payload);
|
|
31
|
+
return code !== undefined && REFUSAL_SET.has(code);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qEAAqE;AACrE,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,kBAAkB;IAClB,kBAAkB;IAClB,mBAAmB;IACnB,iBAAiB;IACjB,kBAAkB;CACV,CAAC;AAIX,MAAM,WAAW,GAAG,IAAI,GAAG,CAAS,oBAAoB,CAAC,CAAC;AAE1D;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAgB;IAClD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACtE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,OAAkC,CAAC;IAChD,MAAM,MAAM,GACV,IAAI,CAAC,KAAK,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QACxE,CAAC,CAAE,IAAI,CAAC,KAAiC;QACzC,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;IACvC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3E,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,MAAM,IAAI,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC1C,OAAO,IAAI,KAAK,SAAS,IAAI,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type OpenCodeModelConfig } from "./models.js";
|
|
2
|
+
export type FetchCruiseModelsOptions = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** Injectable for tests. Defaults to global `fetch`. */
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export type FetchCruiseModelsResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
models: Record<string, OpenCodeModelConfig>;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: string;
|
|
15
|
+
code?: string;
|
|
16
|
+
status?: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `GET {baseUrl}/models` with the presented Cruise key.
|
|
20
|
+
* `baseUrl` must already include `/v1` (production/demo defaults do).
|
|
21
|
+
*/
|
|
22
|
+
export declare function fetchCruiseModels(options: FetchCruiseModelsOptions): Promise<FetchCruiseModelsResult>;
|
|
23
|
+
//# sourceMappingURL=fetch-models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-models.d.ts","sourceRoot":"","sources":["../src/fetch-models.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGhF,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;CAAE,GACzD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAwDlC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { MODELS_FETCH_TIMEOUT_MS } from "./constants.js";
|
|
2
|
+
import { projectCruiseLiveModels } from "./models.js";
|
|
3
|
+
import { readCruiseErrorCode } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* `GET {baseUrl}/models` with the presented Cruise key.
|
|
6
|
+
* `baseUrl` must already include `/v1` (production/demo defaults do).
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchCruiseModels(options) {
|
|
9
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
10
|
+
const timeoutMs = options.timeoutMs ?? MODELS_FETCH_TIMEOUT_MS;
|
|
11
|
+
const url = `${options.baseUrl.replace(/\/+$/, "")}/models`;
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetchImpl(url, {
|
|
16
|
+
method: "GET",
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
19
|
+
Accept: "application/json",
|
|
20
|
+
},
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
let body;
|
|
24
|
+
try {
|
|
25
|
+
body = await response.json();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
body = undefined;
|
|
29
|
+
}
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const code = readCruiseErrorCode(body);
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: code
|
|
35
|
+
? `Cruise refused models list (${code})`
|
|
36
|
+
: `Cruise models list failed with HTTP ${response.status}`,
|
|
37
|
+
code,
|
|
38
|
+
status: response.status,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const data = body && typeof body === "object" && !Array.isArray(body)
|
|
42
|
+
? body.data
|
|
43
|
+
: undefined;
|
|
44
|
+
if (!Array.isArray(data)) {
|
|
45
|
+
return { ok: false, reason: "Cruise models response missing data array", status: response.status };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, models: projectCruiseLiveModels(data) };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof Error
|
|
51
|
+
? error.name === "AbortError"
|
|
52
|
+
? `Cruise models list timed out after ${timeoutMs}ms`
|
|
53
|
+
: error.message
|
|
54
|
+
: String(error);
|
|
55
|
+
return { ok: false, reason: message };
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=fetch-models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-models.js","sourceRoot":"","sources":["../src/fetch-models.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAA4B,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAclD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAiC;IAEjC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC7C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,uBAAuB,CAAC;IAC/D,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC;IAE5D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;YACpC,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,OAAO,CAAC,MAAM,EAAE;gBACzC,MAAM,EAAE,kBAAkB;aAC3B;YACD,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QAEH,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;YACvC,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,IAAI;oBACV,CAAC,CAAC,+BAA+B,IAAI,GAAG;oBACxC,CAAC,CAAC,uCAAuC,QAAQ,CAAC,MAAM,EAAE;gBAC5D,IAAI;gBACJ,MAAM,EAAE,QAAQ,CAAC,MAAM;aACxB,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GACR,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YACtD,CAAC,CAAE,IAA2B,CAAC,IAAI;YACnC,CAAC,CAAC,SAAS,CAAC;QAChB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,2CAA2C,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrG,CAAC;QAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,uBAAuB,CAAC,IAAI,CAAC,EAAE,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GACX,KAAK,YAAY,KAAK;YACpB,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,YAAY;gBAC3B,CAAC,CAAC,sCAAsC,SAAS,IAAI;gBACrD,CAAC,CAAC,KAAK,CAAC,OAAO;YACjB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IACxC,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BytesBrains Cruise — OpenCode plugin entry.
|
|
3
|
+
*
|
|
4
|
+
* Registers Cruise as an OpenAI-compatible provider (`@ai-sdk/openai-compatible`)
|
|
5
|
+
* and fills models from live `GET /v1/models` when a Cruise key is available
|
|
6
|
+
* (env or OpenCode `/connect` auth store).
|
|
7
|
+
*/
|
|
8
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
9
|
+
export { PLUGIN_ID, PROVIDER_ID, CRUISE_BASE_URL, CRUISE_DEMO_BASE_URL, CRUISE_API_KEY_ENV, CRUISE_BASE_URL_ENV, CRUISE_DEFAULT_MODEL_ID, OPENAI_COMPATIBLE_NPM, } from "./constants.js";
|
|
10
|
+
export { resolveAllowedCruiseBaseUrl } from "./base-url.js";
|
|
11
|
+
export { microsToDollarsPerMtok, projectCruiseLiveModels, type OpenCodeModelConfig, } from "./models.js";
|
|
12
|
+
export { fetchCruiseModels } from "./fetch-models.js";
|
|
13
|
+
export { applyCruiseProvider } from "./provider.js";
|
|
14
|
+
export { resolveCruiseApiKey } from "./resolve-api-key.js";
|
|
15
|
+
export { CRUISE_REFUSAL_CODES, isCruiseRefusal, readCruiseErrorCode, type CruiseRefusalCode, } from "./errors.js";
|
|
16
|
+
export { cruiseAuthHook } from "./auth.js";
|
|
17
|
+
export { resolveCruiseMcpUrl } from "./mcp-url.js";
|
|
18
|
+
export { callCruiseMcpTool } from "./mcp-client.js";
|
|
19
|
+
export { applyCruiseMcp, cruiseTools, createCruiseTools } from "./tools.js";
|
|
20
|
+
export declare const CruisePlugin: Plugin;
|
|
21
|
+
export default CruisePlugin;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAU,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAQvE,OAAO,EACL,SAAS,EACT,WAAW,EACX,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAmF5E,eAAO,MAAM,YAAY,EAAE,MAW1B,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { cruiseAuthHook } from "./auth.js";
|
|
2
|
+
import { applyCruiseProvider } from "./provider.js";
|
|
3
|
+
import { isCruiseRefusal, readCruiseErrorCode } from "./errors.js";
|
|
4
|
+
import { resolveCruiseApiKey } from "./resolve-api-key.js";
|
|
5
|
+
import { PROVIDER_ID } from "./constants.js";
|
|
6
|
+
import { cruiseTools } from "./tools.js";
|
|
7
|
+
export { PLUGIN_ID, PROVIDER_ID, CRUISE_BASE_URL, CRUISE_DEMO_BASE_URL, CRUISE_API_KEY_ENV, CRUISE_BASE_URL_ENV, CRUISE_DEFAULT_MODEL_ID, OPENAI_COMPATIBLE_NPM, } from "./constants.js";
|
|
8
|
+
export { resolveAllowedCruiseBaseUrl } from "./base-url.js";
|
|
9
|
+
export { microsToDollarsPerMtok, projectCruiseLiveModels, } from "./models.js";
|
|
10
|
+
export { fetchCruiseModels } from "./fetch-models.js";
|
|
11
|
+
export { applyCruiseProvider } from "./provider.js";
|
|
12
|
+
export { resolveCruiseApiKey } from "./resolve-api-key.js";
|
|
13
|
+
export { CRUISE_REFUSAL_CODES, isCruiseRefusal, readCruiseErrorCode, } from "./errors.js";
|
|
14
|
+
export { cruiseAuthHook } from "./auth.js";
|
|
15
|
+
export { resolveCruiseMcpUrl } from "./mcp-url.js";
|
|
16
|
+
export { callCruiseMcpTool } from "./mcp-client.js";
|
|
17
|
+
export { applyCruiseMcp, cruiseTools, createCruiseTools } from "./tools.js";
|
|
18
|
+
async function resolveStateDir(client) {
|
|
19
|
+
try {
|
|
20
|
+
const result = await client.path.get();
|
|
21
|
+
const data = "data" in result ? result.data : undefined;
|
|
22
|
+
if (data && typeof data === "object" && "state" in data) {
|
|
23
|
+
const state = data.state;
|
|
24
|
+
return typeof state === "string" && state.trim() ? state.trim() : undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
async function logWarn(client, message) {
|
|
33
|
+
await client.app
|
|
34
|
+
.log({
|
|
35
|
+
body: {
|
|
36
|
+
service: PROVIDER_ID,
|
|
37
|
+
level: "warn",
|
|
38
|
+
message,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {
|
|
42
|
+
/* logging is best-effort during config */
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function cruisePayloadFromSessionError(raw) {
|
|
46
|
+
if (!raw || typeof raw !== "object" || !("data" in raw)) {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
const data = raw.data;
|
|
50
|
+
if (typeof data?.responseBody !== "string") {
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(data.responseBody);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return data.responseBody;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function handleConfigHook(client, config) {
|
|
61
|
+
const stateDir = await resolveStateDir(client);
|
|
62
|
+
const apiKey = await resolveCruiseApiKey({ stateDir });
|
|
63
|
+
const result = await applyCruiseProvider(config, { apiKey });
|
|
64
|
+
if (result.fetchError) {
|
|
65
|
+
await logWarn(client, result.fetchError);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function handleSessionError(client, event) {
|
|
69
|
+
if (event.type !== "session.error") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const payload = cruisePayloadFromSessionError(event.properties?.error);
|
|
73
|
+
if (!isCruiseRefusal(payload)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const code = readCruiseErrorCode(payload);
|
|
77
|
+
await client.app
|
|
78
|
+
.log({
|
|
79
|
+
body: {
|
|
80
|
+
service: PROVIDER_ID,
|
|
81
|
+
level: "error",
|
|
82
|
+
message: `Cruise refused the request (${code}) — branch on error.code, not HTTP status alone`,
|
|
83
|
+
extra: { code },
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
.catch(() => {
|
|
87
|
+
/* best-effort */
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export const CruisePlugin = async ({ client }) => {
|
|
91
|
+
return {
|
|
92
|
+
auth: cruiseAuthHook,
|
|
93
|
+
tool: cruiseTools,
|
|
94
|
+
config: async (config) => {
|
|
95
|
+
await handleConfigHook(client, config);
|
|
96
|
+
},
|
|
97
|
+
event: async ({ event }) => {
|
|
98
|
+
await handleSessionError(client, event);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
export default CruisePlugin;
|
|
103
|
+
//# sourceMappingURL=index.js.map
|