@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.
Files changed (51) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +189 -0
  3. package/dist/auth.d.ts +7 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +44 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/base-url.d.ts +8 -0
  8. package/dist/base-url.d.ts.map +1 -0
  9. package/dist/base-url.js +30 -0
  10. package/dist/base-url.js.map +1 -0
  11. package/dist/constants.d.ts +20 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +20 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/errors.d.ts +14 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +33 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/fetch-models.d.ts +23 -0
  20. package/dist/fetch-models.d.ts.map +1 -0
  21. package/dist/fetch-models.js +61 -0
  22. package/dist/fetch-models.js.map +1 -0
  23. package/dist/index.d.ts +22 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +103 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp-client.d.ts +27 -0
  28. package/dist/mcp-client.d.ts.map +1 -0
  29. package/dist/mcp-client.js +95 -0
  30. package/dist/mcp-client.js.map +1 -0
  31. package/dist/mcp-url.d.ts +6 -0
  32. package/dist/mcp-url.d.ts.map +1 -0
  33. package/dist/mcp-url.js +22 -0
  34. package/dist/mcp-url.js.map +1 -0
  35. package/dist/models.d.ts +42 -0
  36. package/dist/models.d.ts.map +1 -0
  37. package/dist/models.js +132 -0
  38. package/dist/models.js.map +1 -0
  39. package/dist/provider.d.ts +22 -0
  40. package/dist/provider.d.ts.map +1 -0
  41. package/dist/provider.js +68 -0
  42. package/dist/provider.js.map +1 -0
  43. package/dist/resolve-api-key.d.ts +15 -0
  44. package/dist/resolve-api-key.d.ts.map +1 -0
  45. package/dist/resolve-api-key.js +43 -0
  46. package/dist/resolve-api-key.js.map +1 -0
  47. package/dist/tools.d.ts +124 -0
  48. package/dist/tools.d.ts.map +1 -0
  49. package/dist/tools.js +231 -0
  50. package/dist/tools.js.map +1 -0
  51. 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
@@ -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"}
@@ -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"}
@@ -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"}
@@ -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