@warmio/mcp 11.0.7 → 11.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,100 @@
1
1
  # @warmio/mcp
2
2
 
3
- `@warmio/mcp` is Warm's local stdio MCP server. Run the interactive installer to validate and store one full-access `WARM_API_KEY`, then configure detected supported clients:
3
+ `@warmio/mcp` is Warm's local stdio MCP server. It also documents the hosted HTTP endpoint at `https://app.warm.io/mcp`. Run the interactive installer to validate and store one full-access `WARM_API_KEY`, then configure detected supported clients:
4
4
 
5
5
  ```sh
6
- npx -y @warmio/mcp@11.0.7 install
6
+ npx -y @warmio/mcp@latest install
7
7
  ```
8
8
 
9
- Each client starts the server with the exact pinned package spec from `@warmio/contracts/mcp` (currently `@warmio/mcp@11.0.7`):
9
+ Each client starts the server with the canonical latest launcher spec from `@warmio/contracts/mcp`:
10
10
 
11
11
  ```sh
12
- npx -y @warmio/mcp@11.0.7 mcp
12
+ npx -y @warmio/mcp@latest mcp
13
13
  ```
14
14
 
15
15
  The server exposes financial-context read tools plus the automation operation catalog. The installer stores the local credential with owner-only file permissions. Importing `@warmio/mcp` exposes the library API without starting the CLI.
16
16
 
17
+ ## Hosted HTTP
18
+
19
+ Claude Code, Cursor, and VS Code can use the hosted endpoint instead of the local process. The URL is `https://app.warm.io/mcp`. Send the existing Warm API key in the `Authorization` header as `Bearer` plus the key. Do not put the key in the URL.
20
+
21
+ The hosted server lists only `get_financial_context` and `get_transactions`. The local installer and its five tools stay the same. Claude Desktop, claude.ai, and ChatGPT are not supported on this endpoint. A missing, invalid, or revoked key returns HTTP 401 with `WWW-Authenticate: Bearer realm="Warm MCP"`.
22
+
23
+ Claude Code:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "warm": {
29
+ "type": "http",
30
+ "url": "https://app.warm.io/mcp",
31
+ "headers": {
32
+ "Authorization": "Bearer ${WARM_API_KEY}"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ Cursor:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "warm": {
45
+ "url": "https://app.warm.io/mcp",
46
+ "headers": {
47
+ "Authorization": "Bearer ${env:WARM_API_KEY}"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ VS Code (`.vscode/mcp.json`):
55
+
56
+ ```json
57
+ {
58
+ "servers": {
59
+ "warm": {
60
+ "type": "http",
61
+ "url": "https://app.warm.io/mcp",
62
+ "headers": {
63
+ "Authorization": "Bearer ${input:warm-api-key}"
64
+ }
65
+ }
66
+ },
67
+ "inputs": [
68
+ {
69
+ "id": "warm-api-key",
70
+ "type": "promptString",
71
+ "description": "Warm API key",
72
+ "password": true
73
+ }
74
+ ]
75
+ }
76
+ ```
77
+
78
+ ## OpenCode 2
79
+
80
+ The installer configures OpenCode 2 using `mcp.servers.warm`. It uses an existing
81
+ `~/.config/opencode/opencode.jsonc` file when present, otherwise `opencode.json`.
82
+ Comments and unrelated settings remain in place.
83
+
84
+ When reinstalling, the installer moves existing OpenCode 1 MCP server entries under
85
+ `mcp.servers`. It converts `enabled`, numeric timeouts, and OAuth field names to their
86
+ OpenCode 2 equivalents. Conflicting old and new entries for another server stop the
87
+ installation without writing files. The installer replaces Warm's own entry after
88
+ the existing reinstall confirmation, or when `--force` is supplied.
89
+
90
+ The installer migrates MCP settings only. Migrate the rest of an OpenCode 1 configuration
91
+ using OpenCode's [migration guide](https://opencode.ai/v2/docs/migrate-v1/) before using
92
+ OpenCode 2. New installer output targets OpenCode 2 rather than OpenCode 1.
93
+
94
+ No plugin is required. OpenCode 2 discovers Warm's tools through MCP and enables Code
95
+ Mode by default. Warm still requires Plus or Pro and staying within the plan's
96
+ bank-login limit.
97
+
17
98
  ## Environment precedence
18
99
 
19
100
  Credential and config resolution is fixed and never loaded from ancestor or project `.env` files by default:
@@ -28,22 +109,24 @@ Optional local development: set `WARM_LOAD_PROJECT_ENV=1` before starting the CL
28
109
 
29
110
  ## Launcher integrity contract
30
111
 
31
- Generated launcher configs pin an exact package version (`@warmio/mcp@<version>`), never `@latest`. The release manifest records a `packageIntegrity` contract whose `registryIntegrity` value is populated only after the trusted publish workflow (`.github/workflows/publish-mcp.yml`) completes npm registry readback for the pinned version. Until that gate passes, treat `registryIntegrity: null` as an external deployment requirement rather than a local hash to copy by hand.
112
+ Setup commands and generated launcher configs use the approved `@warmio/mcp@latest` tag. The manifest separately retains the exact release `packageSpec` and `packageVersion` for provenance. Its `packageIntegrity` contract applies to that exact release. The `.github/workflows/publish-mcp.yml` workflow requires an exact merged source SHA and npm registry readback for that version. A registry integrity value does not attest which release the floating `latest` tag resolves to. Until that gate passes, treat `registryIntegrity: null` as an external deployment requirement rather than a local hash to copy by hand.
32
113
 
33
114
  Set `WARM_MCP_REQUIRE_REGISTRY_INTEGRITY=1` to require an exact registry readback. When the manifest still has `registryIntegrity: null`, set `WARM_MCP_REGISTRY_INTEGRITY` to the trusted workflow's `dist.integrity` value. The gate rejects missing or malformed values. Default installs and launcher generation remain allowed until this opt-in gate is enabled.
34
115
 
35
116
  ## Claude Desktop Extension
36
117
 
37
- For Claude Desktop users, a packaged `.mcpb` extension is available in `mcpb/`. To build it:
118
+ For Claude Desktop users, the fixed-tree extension is produced only by the guarded, context-aware flow:
38
119
 
39
120
  ```sh
40
- cd mcpb
41
- pnpm install
42
- pnpm pack
121
+ /absolute/worktree/tools/tasks/run-plan-gate.mjs \
122
+ --target-id P0_MCPB_FIXED_TREE \
123
+ --argv-json '[\"/usr/bin/node\",\"/absolute/worktree/libraries/mcp/mcpb/build.js\",\"--contracts-tar\",\"/absolute/worktree/libraries/mcp/mcpb/.inputs/contracts.tgz\",\"--mcp-tar\",\"/absolute/worktree/libraries/mcp/mcpb/.inputs/mcp.tgz\"]' \
124
+ --context /absolute/context.json
43
125
  ```
44
126
 
45
- This produces `warm.mcpb` which can be installed via double-click or drag-and-drop into Claude Desktop.
46
- The extension uses the same API key authentication as the CLI but provides a settings UI for key entry.
127
+ The flow consumes only the exact local closure tarballs, materializes and packs offline, then
128
+ securely extracts and smokes the unpacked MCPB. It produces `warm.mcpb`; registry and network
129
+ fallbacks are forbidden.
47
130
 
48
131
  ## Cursor Plugin
49
132
 
@@ -0,0 +1,9 @@
1
+ export declare const HOSTED_MCP_MAX_REQUEST_BYTES = 65536;
2
+ type HostedDispatch = (request: Request) => Promise<Response>;
3
+ export interface HostedReadOnlyMcpDependencies {
4
+ concurrencyKey: string;
5
+ dispatchApi: HostedDispatch;
6
+ }
7
+ export declare function handleHostedReadOnlyMcpRequest(request: Request, dependencies: HostedReadOnlyMcpDependencies): Promise<Response>;
8
+ export {};
9
+ //# sourceMappingURL=hosted-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-http.d.ts","sourceRoot":"","sources":["../src/hosted-http.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAWnD,KAAK,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE9D,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,cAAc,CAAC;CAC7B;AAkMD,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,6BAA6B,GAC1C,OAAO,CAAC,QAAQ,CAAC,CA2CnB"}
@@ -0,0 +1,238 @@
1
+ import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker';
2
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
3
+ import { apiEndpointManifest } from '@warmio/contracts/api';
4
+ import { transactionPeriodsMcpOutputSchema, WARM_MCP_PACKAGE_VERSION, } from '@warmio/contracts/mcp';
5
+ import { financialContextSchema, latestTransactionsSchema, transactionMonthSchema, } from '@warmio/contracts/schemas';
6
+ import { z } from 'zod';
7
+ import { createPublicWarmServer, HostedApiResultError, HostedMcpLimitError, } from './public-server.js';
8
+ const HOSTED_MCP_MAX_IN_FLIGHT = 2;
9
+ export const HOSTED_MCP_MAX_REQUEST_BYTES = 65_536;
10
+ const HOSTED_MCP_MAX_RESPONSE_BYTES = 16_777_216;
11
+ const HOSTED_MCP_LIMIT_MESSAGE = 'The response is larger than 16777216 bytes.';
12
+ const HOSTED_MCP_MAX_ERROR_MESSAGE_LENGTH = 500;
13
+ const apiErrorSchema = z.object({ error: z.string().min(1) }).passthrough();
14
+ const rpcIdSchema = z
15
+ .object({ id: z.union([z.string(), z.number(), z.null()]).optional() })
16
+ .passthrough();
17
+ const inFlightToolCalls = new Map();
18
+ function authorizeHostedTool(permission) {
19
+ switch (permission) {
20
+ case 'financial_context:read':
21
+ case 'transactions:read':
22
+ return true;
23
+ default:
24
+ return false;
25
+ }
26
+ }
27
+ function apiPath(operationId) {
28
+ const endpoint = apiEndpointManifest.find((item) => item.operation_id === operationId);
29
+ if (!endpoint)
30
+ throw new Error(`Unknown Warm API operation: ${operationId}`);
31
+ return endpoint.path;
32
+ }
33
+ function forwardedHeaders(request) {
34
+ const headers = new Headers();
35
+ const authorization = request.headers.get('authorization');
36
+ const accept = request.headers.get('accept');
37
+ if (authorization !== null)
38
+ headers.set('authorization', authorization);
39
+ if (accept !== null)
40
+ headers.set('accept', accept);
41
+ return headers;
42
+ }
43
+ function financialContextRequest(origin, headers) {
44
+ return new Request(new URL(apiPath('getFinancialContext'), origin), {
45
+ headers,
46
+ method: 'GET',
47
+ });
48
+ }
49
+ function transactionsRequest(origin, headers, input) {
50
+ const url = new URL(apiPath('getFinancialContextTransactions'), origin);
51
+ if ('month' in input)
52
+ url.searchParams.set('month', input.month);
53
+ else
54
+ url.searchParams.set('latest', '1');
55
+ return new Request(url, { headers, method: 'GET' });
56
+ }
57
+ async function readResponseText(response) {
58
+ const reader = response.body?.getReader();
59
+ if (!reader)
60
+ return '';
61
+ const chunks = [];
62
+ let total = 0;
63
+ try {
64
+ while (true) {
65
+ const next = await reader.read();
66
+ if (next.done)
67
+ break;
68
+ chunks.push(next.value);
69
+ total += next.value.byteLength;
70
+ }
71
+ }
72
+ finally {
73
+ await reader.cancel().catch(() => undefined);
74
+ }
75
+ const bytes = new Uint8Array(total);
76
+ let offset = 0;
77
+ for (const chunk of chunks) {
78
+ bytes.set(chunk, offset);
79
+ offset += chunk.byteLength;
80
+ }
81
+ return new TextDecoder().decode(bytes);
82
+ }
83
+ async function readRpcId(request) {
84
+ try {
85
+ const parsed = rpcIdSchema.safeParse(JSON.parse(await request.text()));
86
+ if (!parsed.success || parsed.data.id === undefined)
87
+ return null;
88
+ return parsed.data.id;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ function oversizedToolResponse(id) {
95
+ const message = {
96
+ result: {
97
+ content: [
98
+ {
99
+ type: 'text',
100
+ text: JSON.stringify({ error: HOSTED_MCP_LIMIT_MESSAGE }),
101
+ },
102
+ ],
103
+ isError: true,
104
+ },
105
+ jsonrpc: '2.0',
106
+ id,
107
+ };
108
+ return new Response(JSON.stringify(message), {
109
+ status: 200,
110
+ headers: { 'Content-Type': 'application/json' },
111
+ });
112
+ }
113
+ async function enforceSerializedResponseCap(response, id) {
114
+ const reader = response.body?.getReader();
115
+ if (!reader)
116
+ return response;
117
+ const chunks = [];
118
+ let total = 0;
119
+ let overflow = false;
120
+ try {
121
+ while (true) {
122
+ const next = await reader.read();
123
+ if (next.done)
124
+ break;
125
+ total += next.value.byteLength;
126
+ if (total > HOSTED_MCP_MAX_RESPONSE_BYTES) {
127
+ overflow = true;
128
+ break;
129
+ }
130
+ chunks.push(next.value);
131
+ }
132
+ }
133
+ finally {
134
+ await reader.cancel().catch(() => undefined);
135
+ }
136
+ if (overflow)
137
+ return oversizedToolResponse(id);
138
+ const body = new ArrayBuffer(total);
139
+ const bytes = new Uint8Array(body);
140
+ let offset = 0;
141
+ for (const chunk of chunks) {
142
+ bytes.set(chunk, offset);
143
+ offset += chunk.byteLength;
144
+ }
145
+ const headers = new Headers(response.headers);
146
+ headers.delete('content-length');
147
+ return new Response(body, {
148
+ status: response.status,
149
+ statusText: response.statusText,
150
+ headers,
151
+ });
152
+ }
153
+ function parseFinancialContext(text) {
154
+ return financialContextSchema.parse(JSON.parse(text));
155
+ }
156
+ function parseTransactions(text) {
157
+ const value = JSON.parse(text);
158
+ const periods = transactionPeriodsMcpOutputSchema.safeParse(value);
159
+ if (periods.success)
160
+ return periods.data;
161
+ const month = transactionMonthSchema.safeParse(value);
162
+ if (month.success)
163
+ return month.data;
164
+ return latestTransactionsSchema.parse(value);
165
+ }
166
+ function parseTransactionMonth(text) {
167
+ return transactionMonthSchema.parse(JSON.parse(text));
168
+ }
169
+ async function readApiErrorMessage(response) {
170
+ if (response.status >= 500) {
171
+ await response.body?.cancel();
172
+ return null;
173
+ }
174
+ try {
175
+ const parsed = apiErrorSchema.safeParse(JSON.parse(await readResponseText(response)));
176
+ if (!parsed.success)
177
+ return null;
178
+ return parsed.data.error.slice(0, HOSTED_MCP_MAX_ERROR_MESSAGE_LENGTH);
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ }
184
+ async function readDispatch(dependencies, request, parse) {
185
+ const key = dependencies.concurrencyKey;
186
+ const inFlight = inFlightToolCalls.get(key) ?? 0;
187
+ if (inFlight >= HOSTED_MCP_MAX_IN_FLIGHT) {
188
+ throw new HostedMcpLimitError('Too many Warm MCP calls are in progress. Retry.');
189
+ }
190
+ inFlightToolCalls.set(key, inFlight + 1);
191
+ try {
192
+ const response = await dependencies.dispatchApi(request);
193
+ if (!response.ok) {
194
+ throw new HostedApiResultError(response.status, await readApiErrorMessage(response));
195
+ }
196
+ return parse(await readResponseText(response));
197
+ }
198
+ finally {
199
+ const remaining = (inFlightToolCalls.get(key) ?? 1) - 1;
200
+ if (remaining > 0)
201
+ inFlightToolCalls.set(key, remaining);
202
+ else
203
+ inFlightToolCalls.delete(key);
204
+ }
205
+ }
206
+ export async function handleHostedReadOnlyMcpRequest(request, dependencies) {
207
+ const headers = forwardedHeaders(request);
208
+ const origin = new URL(request.url).origin;
209
+ const server = createPublicWarmServer({
210
+ authorize: authorizeHostedTool,
211
+ jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),
212
+ readClient: {
213
+ getFinancialContext: () => readDispatch(dependencies, financialContextRequest(origin, headers), parseFinancialContext),
214
+ getTransactions: async (input) => {
215
+ if ('periods' in input) {
216
+ const periods = await Promise.all(input.periods.map((month) => readDispatch(dependencies, transactionsRequest(origin, headers, { month }), parseTransactionMonth)));
217
+ return transactionPeriodsMcpOutputSchema.parse({ periods });
218
+ }
219
+ return readDispatch(dependencies, transactionsRequest(origin, headers, input), parseTransactions);
220
+ },
221
+ },
222
+ serverInfo: { name: 'warm', version: WARM_MCP_PACKAGE_VERSION },
223
+ });
224
+ const transport = new WebStandardStreamableHTTPServerTransport({
225
+ enableJsonResponse: true,
226
+ maxRequestBodySize: HOSTED_MCP_MAX_REQUEST_BYTES,
227
+ });
228
+ const rpcId = await readRpcId(request.clone());
229
+ try {
230
+ await server.connect(transport);
231
+ return await enforceSerializedResponseCap(await transport.handleRequest(request), rpcId);
232
+ }
233
+ finally {
234
+ await server.close();
235
+ await transport.close();
236
+ }
237
+ }
238
+ //# sourceMappingURL=hosted-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-http.js","sourceRoot":"","sources":["../src/hosted-http.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAyCA,KAAK,aAAa,GAAG;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAcF,MAAM,WAAW,cAAc;IAC7B,gGAAgG;IAChG,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,OAAO,KAAK,CAAC;IACnC,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvE,sEAAsE;IACtE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,0EAA0E;IAC1E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7E;AAgXD,2EAA2E;AAC3E,wBAAsB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCzE"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AA4CA,KAAK,aAAa,GAAG;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAcF,MAAM,WAAW,cAAc;IAC7B,gGAAgG;IAChG,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,OAAO,KAAK,CAAC;IACnC,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvE,sEAAsE;IACtE,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,0EAA0E;IAC1E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7E;AAscD,2EAA2E;AAC3E,wBAAsB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCzE"}
package/dist/install.js CHANGED
@@ -3,9 +3,12 @@ import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
- import { API_ORIGIN, WARM_API_KEY_PREFIX } from '@warmio/contracts/api';
6
+ import { applyEdits, modify, parse } from 'jsonc-parser';
7
+ import { z } from 'zod';
8
+ import { API_ORIGIN, isWarmApiKey } from '@warmio/contracts/api';
7
9
  import { assertWarmMcpLauncherContract, maybeRequireWarmMcpRegistryIntegrity, WARM_MCP_CLIENTS, WARM_MCP_MANIFEST, WARM_MCP_PROJECT_CONFIGS, WARM_MCP_SERVER_CONFIG, } from '@warmio/contracts/mcp';
8
10
  import { getWarmApiKeyPath } from './config-paths.js';
11
+ import { BANK_LOGIN_CAPACITY_EXCEEDED_CODE } from './warm-api-client.js';
9
12
  function hasReadlineWriter(readline) {
10
13
  return '_writeToOutput' in readline && readline._writeToOutput instanceof Function;
11
14
  }
@@ -15,7 +18,6 @@ function isRecord(value) {
15
18
  // oxlint-disable-next-line anti-slop/no-runtime-typeof -- object detection is the filesystem JSON boundary check.
16
19
  return typeof value === 'object' && value !== null && !Array.isArray(value);
17
20
  }
18
- const API_KEY_PATTERN = new RegExp(`^${WARM_API_KEY_PREFIX}[A-Za-z0-9_-]{43}$`);
19
21
  function configPath(config, homeDir) {
20
22
  const root = config.root === 'appData'
21
23
  ? process.env['APPDATA']?.trim() || path.join(homeDir, 'AppData', 'Roaming')
@@ -50,7 +52,14 @@ function supportedClients(homeDir, workingDirectory) {
50
52
  return clients;
51
53
  }, []);
52
54
  const configPaths = new Set();
53
- return [...globalClients, ...projectClients].filter((client) => {
55
+ return [...globalClients, ...projectClients]
56
+ .map((client) => {
57
+ if (client.configDialect !== 'openCode')
58
+ return client;
59
+ const jsoncPath = `${client.configPath}c`;
60
+ return fs.existsSync(jsoncPath) ? { ...client, configPath: jsoncPath } : client;
61
+ })
62
+ .filter((client) => {
54
63
  if (configPaths.has(client.configPath))
55
64
  return false;
56
65
  configPaths.add(client.configPath);
@@ -79,7 +88,12 @@ function isCurrentWarmConfig(client, server) {
79
88
  function parseJsonConfig(client, content) {
80
89
  if (!content)
81
90
  return {};
82
- const parsed = JSON.parse(content);
91
+ const errors = [];
92
+ const parsed = client.configDialect === 'openCode'
93
+ ? parse(content, errors, { allowTrailingComma: true })
94
+ : JSON.parse(content);
95
+ if (errors.length > 0)
96
+ throw new Error(`${client.name} config is not valid JSONC.`);
83
97
  if (!isRecord(parsed))
84
98
  throw new Error(`${client.name} config is not a JSON object.`);
85
99
  return parsed;
@@ -87,6 +101,17 @@ function parseJsonConfig(client, content) {
87
101
  function readJsonConfig(client) {
88
102
  return parseJsonConfig(client, fs.existsSync(client.configPath) ? fs.readFileSync(client.configPath, 'utf8') : '');
89
103
  }
104
+ function openCodeMcpConfig(config) {
105
+ if (config['mcp'] === undefined)
106
+ return {};
107
+ if (!isRecord(config['mcp']))
108
+ throw new Error('OpenCode mcp config must be an object.');
109
+ const mcp = config['mcp'];
110
+ if (mcp['servers'] !== undefined && !isRecord(mcp['servers'])) {
111
+ throw new Error('OpenCode mcp.servers config must be an object.');
112
+ }
113
+ return mcp;
114
+ }
90
115
  const WARM_TOML_BLOCK = /\n?\[mcp_servers\.warm\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
91
116
  const WARM_TOML_ENV_BLOCK = /\n?\[mcp_servers\.warm\.env\][\s\S]*?(?=\n\[[^\n]+\]|\s*$)/g;
92
117
  function existingWarmConfig(client) {
@@ -109,14 +134,16 @@ function existingWarmConfig(client) {
109
134
  }
110
135
  const config = readJsonConfig(client);
111
136
  if (client.configDialect === 'openCode') {
112
- const servers = isRecord(config['mcp']) ? config['mcp'] : {};
137
+ const mcp = openCodeMcpConfig(config);
138
+ const servers = isRecord(mcp['servers']) ? mcp['servers'] : {};
113
139
  const legacyServers = isRecord(config['mcpServers']) ? config['mcpServers'] : {};
114
140
  const hasLegacyWarmConfig = 'warm' in legacyServers;
115
141
  return {
116
142
  current: isRecord(servers['warm']) &&
117
143
  isCurrentWarmConfig(client, servers['warm']) &&
144
+ !('warm' in mcp) &&
118
145
  !hasLegacyWarmConfig,
119
- exists: 'warm' in servers || hasLegacyWarmConfig,
146
+ exists: 'warm' in servers || 'warm' in mcp || hasLegacyWarmConfig,
120
147
  };
121
148
  }
122
149
  const servers = isRecord(config['mcpServers']) ? config['mcpServers'] : {};
@@ -125,21 +152,69 @@ function existingWarmConfig(client) {
125
152
  exists: 'warm' in servers,
126
153
  };
127
154
  }
155
+ function openCodeServerConfig(server) {
156
+ const migrated = { ...server };
157
+ if (server['enabled'] === true || server['enabled'] === false) {
158
+ migrated['disabled'] = !server['enabled'];
159
+ delete migrated['enabled'];
160
+ }
161
+ const timeout = z.number().safeParse(server['timeout']);
162
+ if (timeout.success)
163
+ migrated['timeout'] = { catalog: timeout.data, execution: timeout.data };
164
+ if (isRecord(server['oauth'])) {
165
+ const oauth = { ...server['oauth'] };
166
+ for (const [oldName, newName] of [
167
+ ['clientId', 'client_id'],
168
+ ['clientSecret', 'client_secret'],
169
+ ['callbackPort', 'callback_port'],
170
+ ['redirectUri', 'redirect_uri'],
171
+ ]) {
172
+ if (!(oldName in oauth))
173
+ continue;
174
+ if (!(newName in oauth))
175
+ oauth[newName] = oauth[oldName];
176
+ delete oauth[oldName];
177
+ }
178
+ migrated['oauth'] = oauth;
179
+ }
180
+ return migrated;
181
+ }
182
+ function prepareOpenCodeConfig(config, current) {
183
+ const mcp = openCodeMcpConfig(config);
184
+ const servers = isRecord(mcp['servers']) ? mcp['servers'] : {};
185
+ let content = current || '{}';
186
+ const edit = (location, value) => {
187
+ content = applyEdits(content, modify(content, location, value, {
188
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' },
189
+ }));
190
+ };
191
+ for (const [name, server] of Object.entries(mcp)) {
192
+ if (name === 'warm' ||
193
+ !isRecord(server) ||
194
+ (server['type'] !== 'local' && server['type'] !== 'remote'))
195
+ continue;
196
+ if (name in servers)
197
+ throw new Error(`OpenCode config has conflicting MCP entries for ${name}.`);
198
+ edit(['mcp', 'servers', name], openCodeServerConfig(server));
199
+ edit(['mcp', name], undefined);
200
+ }
201
+ edit(['mcp', 'servers', 'warm'], {
202
+ type: 'local',
203
+ command: [WARM_MCP_SERVER_CONFIG.command, ...WARM_MCP_SERVER_CONFIG.args],
204
+ });
205
+ if ('warm' in mcp)
206
+ edit(['mcp', 'warm'], undefined);
207
+ if (isRecord(config['mcpServers']) && 'warm' in config['mcpServers']) {
208
+ edit(['mcpServers', 'warm'], undefined);
209
+ if (Object.keys(config['mcpServers']).length === 1)
210
+ edit(['mcpServers'], undefined);
211
+ }
212
+ return `${content.trimEnd()}\n`;
213
+ }
128
214
  function prepareJsonConfig(client, current) {
129
215
  const config = parseJsonConfig(client, current);
130
216
  if (client.configDialect === 'openCode') {
131
- const servers = isRecord(config['mcp']) ? config['mcp'] : {};
132
- servers['warm'] = {
133
- type: 'local',
134
- command: [WARM_MCP_SERVER_CONFIG.command, ...WARM_MCP_SERVER_CONFIG.args],
135
- };
136
- config['mcp'] = servers;
137
- if (isRecord(config['mcpServers']) && 'warm' in config['mcpServers']) {
138
- delete config['mcpServers']['warm'];
139
- if (Object.keys(config['mcpServers']).length === 0)
140
- delete config['mcpServers'];
141
- }
142
- return `${JSON.stringify(config, null, 2)}\n`;
217
+ return prepareOpenCodeConfig(config, current);
143
218
  }
144
219
  const servers = isRecord(config['mcpServers']) ? config['mcpServers'] : {};
145
220
  servers['warm'] = {
@@ -171,6 +246,7 @@ function snapshotFile(target) {
171
246
  };
172
247
  }
173
248
  catch (error) {
249
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- [typescript/consistent-type-assertions] Pre-existing production TypeScript assertion surfaced by the corrected assertion-expression family classifier.
174
250
  if (error.code !== 'ENOENT')
175
251
  throw error;
176
252
  return { content: '', exists: false, mode: 0o666 };
@@ -281,9 +357,13 @@ function readlinePrompt(question, options) {
281
357
  });
282
358
  });
283
359
  }
360
+ const API_KEY_VALIDATION_FAILED_MESSAGE = 'Warm API key validation failed. Check Warm Settings and try again.';
361
+ const BANK_LOGIN_CAPACITY_RESOLUTION_MESSAGE = 'This Warm API key is valid, but your account holds more bank logins than your plan includes. Remove a bank login or change your plan in Warm, then run the installer again.';
362
+ class WarmApiCapacityError extends Error {
363
+ }
284
364
  async function validateApiKey(apiKey, options) {
285
- if (!API_KEY_PATTERN.test(apiKey)) {
286
- throw new Error('Warm API key validation failed. Check Warm Settings and try again.');
365
+ if (!isWarmApiKey(apiKey)) {
366
+ throw new Error(API_KEY_VALIDATION_FAILED_MESSAGE);
287
367
  }
288
368
  const response = await (options.fetchImplementation || fetch)(new URL('/api/verify', options.apiUrl || API_ORIGIN), {
289
369
  headers: {
@@ -293,9 +373,13 @@ async function validateApiKey(apiKey, options) {
293
373
  });
294
374
  const text = await response.text();
295
375
  const body = parseJsonResponse(text);
296
- if (!response.ok || !isRecord(body) || body['valid'] !== true || Object.keys(body).length !== 1) {
297
- throw new Error('Warm API key validation failed. Check Warm Settings and try again.');
376
+ if (response.ok && isRecord(body) && body['valid'] === true && Object.keys(body).length === 1) {
377
+ return;
378
+ }
379
+ if (!response.ok && isRecord(body) && body['code'] === BANK_LOGIN_CAPACITY_EXCEEDED_CODE) {
380
+ throw new WarmApiCapacityError(BANK_LOGIN_CAPACITY_RESOLUTION_MESSAGE);
298
381
  }
382
+ throw new Error(API_KEY_VALIDATION_FAILED_MESSAGE);
299
383
  }
300
384
  function parseJsonResponse(text) {
301
385
  try {
@@ -327,7 +411,9 @@ async function resolveApiKey(target, ask, options) {
327
411
  await validateApiKey(storedApiKey, options);
328
412
  return { apiKey: storedApiKey, replaceStoredKey: false };
329
413
  }
330
- catch {
414
+ catch (error) {
415
+ if (error instanceof WarmApiCapacityError)
416
+ throw error;
331
417
  return {
332
418
  apiKey: await requestedApiKey(ask, options, INVALID_STORED_API_KEY_PASTE_PROMPT),
333
419
  replaceStoredKey: true,