@warmio/mcp 6.0.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -25
- package/dist/config-paths.d.ts +2 -1
- package/dist/config-paths.js +6 -4
- package/dist/http.d.ts +6 -0
- package/dist/http.js +36 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +18 -6
- package/dist/install.d.ts +2 -0
- package/dist/install.js +209 -94
- package/dist/schemas.d.ts +46 -6
- package/dist/schemas.js +24 -0
- package/dist/server.d.ts +5 -3
- package/dist/server.js +76 -12
- package/dist/types.d.ts +5 -2
- package/dist/types.js +7 -1
- package/dist/warm-api-client.d.ts +54 -3
- package/dist/warm-api-client.js +99 -41
- package/package.json +3 -2
package/dist/install.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createInterface } from 'readline';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { dirname, join, resolve } from 'path';
|
|
4
4
|
import { homedir, platform } from 'os';
|
|
5
|
-
import { verifyWarmApiKey } from './server.js';
|
|
5
|
+
import { verifyWarmApiKey, WarmApiError } from './server.js';
|
|
6
6
|
import { getWarmApiKeyPath, readConfigFile } from './config-paths.js';
|
|
7
7
|
const HOME = homedir();
|
|
8
8
|
const CWD = process.cwd();
|
|
@@ -47,12 +47,15 @@ const GLOBAL_CLIENTS = [
|
|
|
47
47
|
];
|
|
48
48
|
const PROJECT_CONFIGS = ['.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json'];
|
|
49
49
|
const MCP_PACKAGE_SPEC = '@warmio/mcp@latest';
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
50
|
+
function getServerName(mode) {
|
|
51
|
+
return mode === 'automation' ? 'warm-automation' : 'warm';
|
|
52
|
+
}
|
|
53
|
+
function getMcpConfig(mode) {
|
|
54
|
+
const args = ['-y', MCP_PACKAGE_SPEC, 'mcp', '--mode', mode];
|
|
55
|
+
return platform() === 'win32'
|
|
56
|
+
? { command: 'cmd', args: ['/c', 'npx', ...args] }
|
|
57
|
+
: { command: 'npx', args };
|
|
58
|
+
}
|
|
56
59
|
function detectProjectClients() {
|
|
57
60
|
const found = [];
|
|
58
61
|
for (const name of PROJECT_CONFIGS) {
|
|
@@ -83,49 +86,81 @@ function getStringArray(value) {
|
|
|
83
86
|
function isWarmPackageSpecifier(value) {
|
|
84
87
|
return /^@warmio\/mcp(?:@.+)?$/.test(value);
|
|
85
88
|
}
|
|
86
|
-
function isSupportedWarmInvocation(command, args) {
|
|
89
|
+
function isSupportedWarmInvocation(command, args, mode) {
|
|
87
90
|
if (command !== 'npx' && command !== 'cmd') {
|
|
88
91
|
return false;
|
|
89
92
|
}
|
|
90
|
-
return args.some((value) => isWarmPackageSpecifier(value)) &&
|
|
93
|
+
return (args.some((value) => isWarmPackageSpecifier(value)) &&
|
|
94
|
+
args.includes('mcp') &&
|
|
95
|
+
args.includes('--mode') &&
|
|
96
|
+
args.includes(mode));
|
|
91
97
|
}
|
|
92
|
-
function getJsonEnvStatus(server) {
|
|
98
|
+
function getJsonEnvStatus(server, mode) {
|
|
93
99
|
const env = isRecord(server?.env) ? server.env : undefined;
|
|
94
|
-
const
|
|
95
|
-
|
|
100
|
+
const apiKeyName = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
|
|
101
|
+
const apiKeyFileName = `${apiKeyName}_FILE`;
|
|
102
|
+
const oppositeApiKeyName = mode === 'automation' ? 'WARM_CONTEXT_API_KEY' : 'WARM_AUTOMATION_API_KEY';
|
|
103
|
+
const inlineApiKey = typeof env?.[apiKeyName] === 'string' && env[apiKeyName].trim()
|
|
104
|
+
? env[apiKeyName].trim()
|
|
96
105
|
: null;
|
|
97
106
|
return {
|
|
98
|
-
|
|
107
|
+
apiKeyFileOverride: typeof env?.[apiKeyFileName] === 'string' && env[apiKeyFileName].trim().length > 0
|
|
108
|
+
? env[apiKeyFileName].trim()
|
|
109
|
+
: null,
|
|
110
|
+
hasIrrelevantCredential: ['WARM_API_KEY', 'WARM_API_KEY_FILE', oppositeApiKeyName, `${oppositeApiKeyName}_FILE`]
|
|
111
|
+
.some((name) => typeof env?.[name] === 'string' && env[name].trim().length > 0),
|
|
99
112
|
inlineApiKey,
|
|
100
113
|
};
|
|
101
114
|
}
|
|
102
|
-
function
|
|
115
|
+
function resolveApiKeyFileOverride(filePath) {
|
|
116
|
+
if (filePath === '~') {
|
|
117
|
+
return HOME;
|
|
118
|
+
}
|
|
119
|
+
if (filePath.startsWith('~/')) {
|
|
120
|
+
return join(HOME, filePath.slice(2));
|
|
121
|
+
}
|
|
122
|
+
return resolve(filePath);
|
|
123
|
+
}
|
|
124
|
+
function createClientStatus(client, invocationState, envStatus, mode) {
|
|
125
|
+
const credentialApiKey = envStatus.inlineApiKey ||
|
|
126
|
+
(envStatus.apiKeyFileOverride
|
|
127
|
+
? readConfigFile(resolveApiKeyFileOverride(envStatus.apiKeyFileOverride))
|
|
128
|
+
: readConfigFile(getWarmApiKeyPath(mode)));
|
|
129
|
+
const state = invocationState === 'configured' && credentialApiKey && !envStatus.hasIrrelevantCredential
|
|
130
|
+
? 'configured'
|
|
131
|
+
: invocationState === 'missing'
|
|
132
|
+
? 'missing'
|
|
133
|
+
: 'needs_setup';
|
|
134
|
+
return { client, credentialApiKey, state, ...envStatus };
|
|
135
|
+
}
|
|
136
|
+
function getJsonStatus(client, content, mode) {
|
|
103
137
|
try {
|
|
104
138
|
const parsed = JSON.parse(content);
|
|
105
139
|
const servers = isRecord(parsed.mcpServers) ? parsed.mcpServers : undefined;
|
|
106
|
-
const
|
|
107
|
-
const
|
|
140
|
+
const candidate = servers?.[getServerName(mode)];
|
|
141
|
+
const server = isRecord(candidate) ? candidate : undefined;
|
|
142
|
+
const envStatus = getJsonEnvStatus(server, mode);
|
|
108
143
|
if (!server) {
|
|
109
|
-
return
|
|
144
|
+
return createClientStatus(client, 'missing', envStatus, mode);
|
|
110
145
|
}
|
|
111
146
|
const command = typeof server.command === 'string' ? server.command : undefined;
|
|
112
147
|
const args = getStringArray(server.args);
|
|
113
|
-
if (isSupportedWarmInvocation(command, args)) {
|
|
114
|
-
return
|
|
148
|
+
if (isSupportedWarmInvocation(command, args, mode)) {
|
|
149
|
+
return createClientStatus(client, 'configured', envStatus, mode);
|
|
115
150
|
}
|
|
116
|
-
return
|
|
151
|
+
return createClientStatus(client, 'needs_setup', envStatus, mode);
|
|
117
152
|
}
|
|
118
153
|
catch {
|
|
119
|
-
return
|
|
154
|
+
return createClientStatus(client, 'missing', { apiKeyFileOverride: null, hasIrrelevantCredential: false, inlineApiKey: null }, mode);
|
|
120
155
|
}
|
|
121
156
|
}
|
|
122
157
|
function escapeRegExp(value) {
|
|
123
158
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
124
159
|
}
|
|
125
160
|
function getTomlSection(content, sectionName) {
|
|
126
|
-
const pattern = new RegExp(`(?:^|\\n)\\[${escapeRegExp(sectionName)}\\]\\n([\\s\\S]*?)(?=\\n\\[[^\\n]+\\]|\\s*$)`);
|
|
161
|
+
const pattern = new RegExp(`(?:^|\\r?\\n)\\[${escapeRegExp(sectionName)}\\]\\r?\\n([\\s\\S]*?)(?=\\r?\\n\\[[^\\r\\n]+\\]|\\s*$)`);
|
|
127
162
|
const match = content.match(pattern);
|
|
128
|
-
return match ? match[0].replace(/^\n/, '') : null;
|
|
163
|
+
return match ? match[0].replace(/^\r?\n/, '') : null;
|
|
129
164
|
}
|
|
130
165
|
function getTomlStringValue(block, key) {
|
|
131
166
|
if (!block) {
|
|
@@ -151,116 +186,171 @@ function getTomlStringArrayValue(block, key) {
|
|
|
151
186
|
}
|
|
152
187
|
return values;
|
|
153
188
|
}
|
|
154
|
-
function getTomlEnvStatus(content) {
|
|
155
|
-
const envBlock = getTomlSection(content,
|
|
189
|
+
function getTomlEnvStatus(content, mode) {
|
|
190
|
+
const envBlock = getTomlSection(content, `mcp_servers.${getServerName(mode)}.env`);
|
|
156
191
|
if (!envBlock) {
|
|
157
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
apiKeyFileOverride: null,
|
|
194
|
+
envLines: [],
|
|
195
|
+
hasIrrelevantCredential: false,
|
|
196
|
+
inlineApiKey: null,
|
|
197
|
+
};
|
|
158
198
|
}
|
|
159
|
-
const envLines = envBlock.split(
|
|
160
|
-
const
|
|
161
|
-
const
|
|
199
|
+
const envLines = envBlock.split(/\r?\n/).slice(1).filter((line) => line.trim().length > 0);
|
|
200
|
+
const apiKeyName = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY' : 'WARM_CONTEXT_API_KEY';
|
|
201
|
+
const oppositeApiKeyName = mode === 'automation' ? 'WARM_CONTEXT_API_KEY' : 'WARM_AUTOMATION_API_KEY';
|
|
202
|
+
const inlineApiKey = getTomlStringValue(envBlock, apiKeyName)?.trim() || null;
|
|
203
|
+
const apiKeyFile = getTomlStringValue(envBlock, `${apiKeyName}_FILE`);
|
|
162
204
|
return {
|
|
205
|
+
apiKeyFileOverride: typeof apiKeyFile === 'string' && apiKeyFile.trim() ? apiKeyFile.trim() : null,
|
|
163
206
|
envLines,
|
|
164
|
-
|
|
207
|
+
hasIrrelevantCredential: [
|
|
208
|
+
'WARM_API_KEY',
|
|
209
|
+
'WARM_API_KEY_FILE',
|
|
210
|
+
oppositeApiKeyName,
|
|
211
|
+
`${oppositeApiKeyName}_FILE`,
|
|
212
|
+
].some((name) => Boolean(getTomlStringValue(envBlock, name)?.trim())),
|
|
165
213
|
inlineApiKey,
|
|
166
214
|
};
|
|
167
215
|
}
|
|
168
|
-
function getTomlStatus(client, content) {
|
|
169
|
-
const warmBlock = getTomlSection(content,
|
|
170
|
-
const envStatus = getTomlEnvStatus(content);
|
|
216
|
+
function getTomlStatus(client, content, mode) {
|
|
217
|
+
const warmBlock = getTomlSection(content, `mcp_servers.${getServerName(mode)}`);
|
|
218
|
+
const envStatus = getTomlEnvStatus(content, mode);
|
|
171
219
|
if (!warmBlock) {
|
|
172
|
-
return
|
|
220
|
+
return createClientStatus(client, 'missing', envStatus, mode);
|
|
173
221
|
}
|
|
174
222
|
const command = getTomlStringValue(warmBlock, 'command');
|
|
175
223
|
const args = getTomlStringArrayValue(warmBlock, 'args');
|
|
176
|
-
if (isSupportedWarmInvocation(command, args)) {
|
|
177
|
-
return
|
|
224
|
+
if (isSupportedWarmInvocation(command, args, mode)) {
|
|
225
|
+
return createClientStatus(client, 'configured', envStatus, mode);
|
|
178
226
|
}
|
|
179
|
-
return
|
|
227
|
+
return createClientStatus(client, 'needs_setup', envStatus, mode);
|
|
180
228
|
}
|
|
181
|
-
function getClientStatus(client) {
|
|
229
|
+
function getClientStatus(client, mode) {
|
|
182
230
|
if (!existsSync(client.configPath)) {
|
|
183
|
-
return
|
|
231
|
+
return createClientStatus(client, 'missing', { apiKeyFileOverride: null, hasIrrelevantCredential: false, inlineApiKey: null }, mode);
|
|
184
232
|
}
|
|
185
233
|
const content = readFileSync(client.configPath, 'utf-8');
|
|
186
234
|
if (client.format === 'toml') {
|
|
187
|
-
return getTomlStatus(client, content);
|
|
235
|
+
return getTomlStatus(client, content, mode);
|
|
188
236
|
}
|
|
189
|
-
return getJsonStatus(client, content);
|
|
237
|
+
return getJsonStatus(client, content, mode);
|
|
190
238
|
}
|
|
191
|
-
function configureJson(client) {
|
|
239
|
+
function configureJson(client, mode, preserveSelectedKeyFile) {
|
|
192
240
|
let config = {};
|
|
193
241
|
if (existsSync(client.configPath)) {
|
|
242
|
+
const content = readFileSync(client.configPath, 'utf-8');
|
|
243
|
+
let parsed;
|
|
194
244
|
try {
|
|
195
|
-
|
|
245
|
+
parsed = JSON.parse(content);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
throw new Error(`Refusing to overwrite unparsable JSON config ${client.configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
196
249
|
}
|
|
197
|
-
|
|
198
|
-
|
|
250
|
+
if (!isRecord(parsed)) {
|
|
251
|
+
throw new Error(`Refusing to overwrite JSON config ${client.configPath}: root must be an object.`);
|
|
199
252
|
}
|
|
253
|
+
config = parsed;
|
|
200
254
|
}
|
|
201
|
-
if (
|
|
255
|
+
if (config.mcpServers === undefined) {
|
|
202
256
|
config.mcpServers = {};
|
|
203
257
|
}
|
|
204
258
|
const servers = config.mcpServers;
|
|
205
|
-
|
|
259
|
+
if (!isRecord(servers)) {
|
|
260
|
+
throw new Error(`Refusing to overwrite JSON config ${client.configPath}: mcpServers must be an object.`);
|
|
261
|
+
}
|
|
262
|
+
const serverName = getServerName(mode);
|
|
263
|
+
const existing = isRecord(servers[serverName]) ? servers[serverName] : undefined;
|
|
206
264
|
const existingEnv = isRecord(existing?.env) ? existing.env : {};
|
|
207
265
|
const nextEnv = { ...existingEnv };
|
|
208
266
|
const existingCommand = typeof existing?.command === 'string' ? existing.command : undefined;
|
|
209
267
|
const existingArgs = getStringArray(existing?.args);
|
|
210
268
|
const preserveProjectCommand = client.isProjectLevel &&
|
|
211
269
|
!!existingCommand &&
|
|
212
|
-
isSupportedWarmInvocation(existingCommand, existingArgs);
|
|
213
|
-
|
|
270
|
+
isSupportedWarmInvocation(existingCommand, existingArgs, mode);
|
|
271
|
+
const selectedKeyFile = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
|
|
272
|
+
const oppositeKeyFile = mode === 'automation' ? 'WARM_CONTEXT_API_KEY_FILE' : 'WARM_AUTOMATION_API_KEY_FILE';
|
|
273
|
+
for (const name of [
|
|
274
|
+
'WARM_API_KEY',
|
|
275
|
+
'WARM_API_KEY_FILE',
|
|
276
|
+
'WARM_CONTEXT_API_KEY',
|
|
277
|
+
'WARM_AUTOMATION_API_KEY',
|
|
278
|
+
oppositeKeyFile,
|
|
279
|
+
]) {
|
|
280
|
+
delete nextEnv[name];
|
|
281
|
+
}
|
|
282
|
+
if (!preserveSelectedKeyFile) {
|
|
283
|
+
delete nextEnv[selectedKeyFile];
|
|
284
|
+
}
|
|
214
285
|
if (preserveProjectCommand) {
|
|
215
|
-
servers
|
|
286
|
+
servers[serverName] = {
|
|
216
287
|
...existing,
|
|
217
|
-
|
|
288
|
+
env: Object.keys(nextEnv).length > 0 ? nextEnv : undefined,
|
|
218
289
|
};
|
|
219
290
|
}
|
|
220
291
|
else {
|
|
221
|
-
servers
|
|
292
|
+
servers[serverName] = {
|
|
222
293
|
...existing,
|
|
223
|
-
...
|
|
224
|
-
|
|
294
|
+
...getMcpConfig(mode),
|
|
295
|
+
env: Object.keys(nextEnv).length > 0 ? nextEnv : undefined,
|
|
225
296
|
};
|
|
226
297
|
}
|
|
227
298
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
228
299
|
writeFileSync(client.configPath, JSON.stringify(config, null, 2) + '\n');
|
|
229
300
|
}
|
|
230
|
-
function configureToml(client) {
|
|
301
|
+
function configureToml(client, mode, preserveSelectedKeyFile) {
|
|
231
302
|
let content = '';
|
|
303
|
+
let lineEnding = '\n';
|
|
232
304
|
if (existsSync(client.configPath)) {
|
|
233
305
|
content = readFileSync(client.configPath, 'utf-8');
|
|
306
|
+
lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
234
307
|
if (!content.endsWith('\n')) {
|
|
235
|
-
content +=
|
|
308
|
+
content += lineEnding;
|
|
236
309
|
}
|
|
237
310
|
}
|
|
311
|
+
const serverName = getServerName(mode);
|
|
238
312
|
const tomlCommand = platform() === 'win32' ? 'cmd' : 'npx';
|
|
239
313
|
const tomlArgs = platform() === 'win32'
|
|
240
|
-
? `["/c", "npx", "-y", "${MCP_PACKAGE_SPEC}", "mcp"]`
|
|
241
|
-
: `["-y", "${MCP_PACKAGE_SPEC}", "mcp"]`;
|
|
242
|
-
const warmBlock = `[mcp_servers
|
|
243
|
-
const
|
|
314
|
+
? `["/c", "npx", "-y", "${MCP_PACKAGE_SPEC}", "mcp", "--mode", "${mode}"]`
|
|
315
|
+
: `["-y", "${MCP_PACKAGE_SPEC}", "mcp", "--mode", "${mode}"]`;
|
|
316
|
+
const warmBlock = `[mcp_servers.${serverName}]${lineEnding}command = "${tomlCommand}"${lineEnding}args = ${tomlArgs}${lineEnding}`;
|
|
317
|
+
const selectedKeyFile = mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE';
|
|
318
|
+
const oppositeKeyFile = mode === 'automation' ? 'WARM_CONTEXT_API_KEY_FILE' : 'WARM_AUTOMATION_API_KEY_FILE';
|
|
319
|
+
const removedEnvNames = new Set([
|
|
320
|
+
'WARM_API_KEY',
|
|
321
|
+
'WARM_API_KEY_FILE',
|
|
322
|
+
'WARM_CONTEXT_API_KEY',
|
|
323
|
+
'WARM_AUTOMATION_API_KEY',
|
|
324
|
+
oppositeKeyFile,
|
|
325
|
+
...(!preserveSelectedKeyFile ? [selectedKeyFile] : []),
|
|
326
|
+
]);
|
|
327
|
+
const preservedEnvLines = getTomlEnvStatus(content, mode).envLines.filter((line) => {
|
|
328
|
+
const name = line.match(/^\s*([A-Z0-9_]+)\s*=/)?.[1];
|
|
329
|
+
return !name || !removedEnvNames.has(name);
|
|
330
|
+
});
|
|
331
|
+
const escapedServerName = escapeRegExp(serverName);
|
|
332
|
+
const tomlBlockPattern = new RegExp(`(?:\\r?\\n)?\\[mcp_servers\\.${escapedServerName}\\]\\r?\\n[\\s\\S]*?(?=\\r?\\n\\[[^\\r\\n]+\\]|\\s*$)`, 'g');
|
|
333
|
+
const tomlEnvBlockPattern = new RegExp(`(?:\\r?\\n)?\\[mcp_servers\\.${escapedServerName}\\.env\\]\\r?\\n[\\s\\S]*?(?=\\r?\\n\\[[^\\r\\n]+\\]|\\s*$)`, 'g');
|
|
244
334
|
let nextContent = content
|
|
245
|
-
.replace(
|
|
246
|
-
.replace(
|
|
335
|
+
.replace(tomlEnvBlockPattern, '')
|
|
336
|
+
.replace(tomlBlockPattern, '')
|
|
247
337
|
.trimEnd();
|
|
248
338
|
if (nextContent.length > 0) {
|
|
249
|
-
nextContent +=
|
|
339
|
+
nextContent += `${lineEnding}${lineEnding}`;
|
|
250
340
|
}
|
|
251
341
|
nextContent += warmBlock;
|
|
252
342
|
if (preservedEnvLines.length > 0) {
|
|
253
|
-
nextContent +=
|
|
343
|
+
nextContent += `${lineEnding}[mcp_servers.${serverName}.env]${lineEnding}${preservedEnvLines.join(lineEnding)}${lineEnding}`;
|
|
254
344
|
}
|
|
255
345
|
mkdirSync(dirname(client.configPath), { recursive: true });
|
|
256
|
-
writeFileSync(client.configPath, nextContent.endsWith('\n') ? nextContent : `${nextContent}
|
|
346
|
+
writeFileSync(client.configPath, nextContent.endsWith('\n') ? nextContent : `${nextContent}${lineEnding}`);
|
|
257
347
|
}
|
|
258
|
-
function configure(client) {
|
|
348
|
+
function configure(client, mode, preserveSelectedKeyFile) {
|
|
259
349
|
if (client.format === 'json') {
|
|
260
|
-
configureJson(client);
|
|
350
|
+
configureJson(client, mode, preserveSelectedKeyFile);
|
|
261
351
|
return;
|
|
262
352
|
}
|
|
263
|
-
configureToml(client);
|
|
353
|
+
configureToml(client, mode, preserveSelectedKeyFile);
|
|
264
354
|
}
|
|
265
355
|
function shortPath(filePath) {
|
|
266
356
|
return filePath.replace(HOME, '~').replace(CWD, '.');
|
|
@@ -274,28 +364,29 @@ function prompt(question) {
|
|
|
274
364
|
});
|
|
275
365
|
});
|
|
276
366
|
}
|
|
277
|
-
function
|
|
278
|
-
const normalized = message.toLowerCase();
|
|
279
|
-
return (normalized.includes('invalid or expired api key') ||
|
|
280
|
-
normalized.includes('pro subscription required'));
|
|
281
|
-
}
|
|
282
|
-
async function validateApiKey(apiKey) {
|
|
367
|
+
async function validateApiKey(apiKey, mode) {
|
|
283
368
|
console.log(' Validating API key...');
|
|
284
369
|
try {
|
|
285
|
-
const result = await verifyWarmApiKey(apiKey);
|
|
370
|
+
const result = await verifyWarmApiKey(apiKey, mode);
|
|
286
371
|
if (!result.valid) {
|
|
287
372
|
console.log(` Validation failed: ${result.status}`);
|
|
288
373
|
console.log(' Check https://warm.io/settings and try again.');
|
|
289
374
|
console.log('');
|
|
290
375
|
return false;
|
|
291
376
|
}
|
|
377
|
+
if (result.audience !== mode) {
|
|
378
|
+
console.log(` Validation failed: this installer mode requires a ${mode} API key.`);
|
|
379
|
+
console.log(` Create a ${mode} key in https://warm.io/settings and try again.`);
|
|
380
|
+
console.log('');
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
292
383
|
console.log(' API key verified.');
|
|
293
384
|
console.log('');
|
|
294
385
|
return true;
|
|
295
386
|
}
|
|
296
387
|
catch (error) {
|
|
297
388
|
const message = error instanceof Error ? error.message : String(error);
|
|
298
|
-
if (
|
|
389
|
+
if (error instanceof WarmApiError && (error.status === 401 || error.status === 403)) {
|
|
299
390
|
console.log(` Validation failed: ${message}`);
|
|
300
391
|
console.log('');
|
|
301
392
|
return false;
|
|
@@ -306,9 +397,19 @@ async function validateApiKey(apiKey) {
|
|
|
306
397
|
return true;
|
|
307
398
|
}
|
|
308
399
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
400
|
+
async function validateEffectiveApiKeys(apiKeys, mode) {
|
|
401
|
+
const uniqueApiKeys = [...new Set(apiKeys.filter((apiKey) => Boolean(apiKey)))];
|
|
402
|
+
for (const apiKey of uniqueApiKeys) {
|
|
403
|
+
if (!(await validateApiKey(apiKey, mode))) {
|
|
404
|
+
throw new Error(`The ${mode} API key could not be validated.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function storeApiKey(apiKey, mode) {
|
|
409
|
+
const apiKeyPath = getWarmApiKeyPath(mode);
|
|
410
|
+
mkdirSync(dirname(apiKeyPath), { recursive: true });
|
|
411
|
+
writeFileSync(apiKeyPath, `${apiKey}\n`, { mode: 0o600 });
|
|
412
|
+
chmodSync(apiKeyPath, 0o600);
|
|
312
413
|
}
|
|
313
414
|
function getStatusLabel(status, force) {
|
|
314
415
|
if (force) {
|
|
@@ -324,6 +425,7 @@ function getStatusLabel(status, force) {
|
|
|
324
425
|
}
|
|
325
426
|
export async function install(options = {}) {
|
|
326
427
|
const force = options.force ?? false;
|
|
428
|
+
const mode = options.mode ?? 'context';
|
|
327
429
|
const shouldValidateApiKey = options.validateApiKey ?? true;
|
|
328
430
|
console.log('');
|
|
329
431
|
console.log(' Warmio Installer');
|
|
@@ -332,7 +434,7 @@ export async function install(options = {}) {
|
|
|
332
434
|
const globalClients = GLOBAL_CLIENTS.filter(isDetected);
|
|
333
435
|
const projectClients = detectProjectClients();
|
|
334
436
|
const allClients = [...globalClients, ...projectClients];
|
|
335
|
-
const clientStatuses = allClients.map(getClientStatus);
|
|
437
|
+
const clientStatuses = allClients.map((client) => getClientStatus(client, mode));
|
|
336
438
|
const needsSetup = clientStatuses.filter((status) => force || status.state !== 'configured');
|
|
337
439
|
console.log(' MCP clients found:');
|
|
338
440
|
clientStatuses.forEach((status) => {
|
|
@@ -340,15 +442,20 @@ export async function install(options = {}) {
|
|
|
340
442
|
});
|
|
341
443
|
console.log('');
|
|
342
444
|
if (needsSetup.length === 0) {
|
|
445
|
+
if (shouldValidateApiKey) {
|
|
446
|
+
await validateEffectiveApiKeys(clientStatuses.map((status) => status.credentialApiKey), mode);
|
|
447
|
+
}
|
|
343
448
|
console.log(' All clients already configured!');
|
|
344
449
|
console.log(' Run with --force to update the API key.');
|
|
345
450
|
console.log('');
|
|
346
451
|
return;
|
|
347
452
|
}
|
|
348
|
-
const
|
|
453
|
+
const apiKeyPath = getWarmApiKeyPath(mode);
|
|
454
|
+
const storedApiKey = readConfigFile(apiKeyPath);
|
|
349
455
|
const migratedApiKey = needsSetup.find((status) => typeof status.inlineApiKey === 'string' && status.inlineApiKey.length > 0)
|
|
350
456
|
?.inlineApiKey ?? null;
|
|
351
|
-
const
|
|
457
|
+
const usableApiKeyFileOverride = needsSetup.find((status) => status.apiKeyFileOverride && !status.inlineApiKey && status.credentialApiKey);
|
|
458
|
+
const targetMissingCredential = needsSetup.some((status) => !status.credentialApiKey);
|
|
352
459
|
let apiKeyToStore = null;
|
|
353
460
|
let shouldStoreApiKey = false;
|
|
354
461
|
let shouldPromptForApiKey = force;
|
|
@@ -364,8 +471,8 @@ export async function install(options = {}) {
|
|
|
364
471
|
apiKeyToStore = migratedApiKey;
|
|
365
472
|
shouldStoreApiKey = true;
|
|
366
473
|
}
|
|
367
|
-
else if (
|
|
368
|
-
console.log(
|
|
474
|
+
else if (usableApiKeyFileOverride && !targetMissingCredential) {
|
|
475
|
+
console.log(` Reusing existing ${mode === 'automation' ? 'WARM_AUTOMATION_API_KEY_FILE' : 'WARM_CONTEXT_API_KEY_FILE'} override.`);
|
|
369
476
|
console.log('');
|
|
370
477
|
}
|
|
371
478
|
else {
|
|
@@ -378,35 +485,43 @@ export async function install(options = {}) {
|
|
|
378
485
|
console.log('');
|
|
379
486
|
console.log(' No key provided. Get one at https://warm.io/settings');
|
|
380
487
|
console.log('');
|
|
381
|
-
|
|
488
|
+
throw new Error(`A ${mode} API key is required to complete setup.`);
|
|
382
489
|
}
|
|
383
490
|
apiKeyToStore = apiKey;
|
|
384
491
|
shouldStoreApiKey = true;
|
|
385
492
|
}
|
|
386
|
-
if (
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
493
|
+
if (shouldValidateApiKey) {
|
|
494
|
+
const retainedClientApiKeys = force
|
|
495
|
+
? []
|
|
496
|
+
: clientStatuses
|
|
497
|
+
.filter((status) => status.state === 'configured' ||
|
|
498
|
+
Boolean(status.apiKeyFileOverride && status.credentialApiKey))
|
|
499
|
+
.map((status) => status.credentialApiKey);
|
|
500
|
+
await validateEffectiveApiKeys([apiKeyToStore, ...retainedClientApiKeys], mode);
|
|
391
501
|
}
|
|
392
502
|
if (apiKeyToStore && shouldStoreApiKey) {
|
|
393
|
-
storeApiKey(apiKeyToStore);
|
|
503
|
+
storeApiKey(apiKeyToStore, mode);
|
|
394
504
|
}
|
|
395
505
|
console.log(' Configuring...');
|
|
396
506
|
console.log('');
|
|
507
|
+
const configurationFailures = [];
|
|
397
508
|
needsSetup.forEach((status) => {
|
|
398
509
|
try {
|
|
399
|
-
configure(status.client);
|
|
510
|
+
configure(status.client, mode, !force && Boolean(status.apiKeyFileOverride && status.credentialApiKey));
|
|
400
511
|
console.log(` ${status.client.name.padEnd(22)} done`);
|
|
401
512
|
}
|
|
402
513
|
catch (error) {
|
|
403
514
|
const message = error instanceof Error ? error.message : String(error);
|
|
404
515
|
console.log(` ${status.client.name.padEnd(22)} failed: ${message}`);
|
|
516
|
+
configurationFailures.push(`${status.client.name}: ${message}`);
|
|
405
517
|
}
|
|
406
518
|
});
|
|
407
519
|
console.log('');
|
|
520
|
+
if (configurationFailures.length > 0) {
|
|
521
|
+
throw new Error(`Failed to configure MCP clients: ${configurationFailures.join('; ')}`);
|
|
522
|
+
}
|
|
408
523
|
if (apiKeyToStore) {
|
|
409
|
-
console.log(` Stored API key at ${shortPath(
|
|
524
|
+
console.log(` Stored ${mode} API key at ${shortPath(apiKeyPath)}`);
|
|
410
525
|
}
|
|
411
526
|
console.log(' All set! Restart your MCP clients and try:');
|
|
412
527
|
console.log(' "What\'s my net worth?"');
|
package/dist/schemas.d.ts
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const emptyInputSchema: {};
|
|
3
|
+
export declare const automationInputSchema: z.ZodObject<{
|
|
4
|
+
body: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
5
|
+
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
6
|
+
}, "strict", z.ZodTypeAny, {
|
|
7
|
+
body?: Record<string, unknown> | undefined;
|
|
8
|
+
params?: Record<string, string> | undefined;
|
|
9
|
+
}, {
|
|
10
|
+
body?: Record<string, unknown> | undefined;
|
|
11
|
+
params?: Record<string, string> | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const searchOperationsInputSchema: {
|
|
14
|
+
query: z.ZodOptional<z.ZodString>;
|
|
15
|
+
};
|
|
16
|
+
export declare const describeOperationInputSchema: {
|
|
17
|
+
operation_id: z.ZodString;
|
|
18
|
+
input: z.ZodOptional<z.ZodObject<{
|
|
19
|
+
body: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
20
|
+
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
21
|
+
}, "strict", z.ZodTypeAny, {
|
|
22
|
+
body?: Record<string, unknown> | undefined;
|
|
23
|
+
params?: Record<string, string> | undefined;
|
|
24
|
+
}, {
|
|
25
|
+
body?: Record<string, unknown> | undefined;
|
|
26
|
+
params?: Record<string, string> | undefined;
|
|
27
|
+
}>>;
|
|
28
|
+
};
|
|
29
|
+
export declare const invokeOperationInputSchema: {
|
|
30
|
+
operation_id: z.ZodString;
|
|
31
|
+
input: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
body: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
33
|
+
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
34
|
+
}, "strict", z.ZodTypeAny, {
|
|
35
|
+
body?: Record<string, unknown> | undefined;
|
|
36
|
+
params?: Record<string, string> | undefined;
|
|
37
|
+
}, {
|
|
38
|
+
body?: Record<string, unknown> | undefined;
|
|
39
|
+
params?: Record<string, string> | undefined;
|
|
40
|
+
}>>;
|
|
41
|
+
approval_id: z.ZodOptional<z.ZodString>;
|
|
42
|
+
};
|
|
3
43
|
export declare const transactionIndexSchema: z.ZodObject<{
|
|
4
44
|
total: z.ZodNumber;
|
|
5
45
|
months: z.ZodArray<z.ZodObject<{
|
|
@@ -397,10 +437,10 @@ export declare const healthSchema: z.ZodObject<{
|
|
|
397
437
|
build: number | null;
|
|
398
438
|
}>>;
|
|
399
439
|
}, "strict", z.ZodTypeAny, {
|
|
440
|
+
summary: string;
|
|
400
441
|
score: number;
|
|
401
442
|
label: "Needs Attention" | "Good" | "Strong";
|
|
402
443
|
level: "critical" | "poor" | "fair" | "good" | "strong";
|
|
403
|
-
summary: string;
|
|
404
444
|
data_completeness: number;
|
|
405
445
|
pillars: {
|
|
406
446
|
spend: number | null;
|
|
@@ -409,10 +449,10 @@ export declare const healthSchema: z.ZodObject<{
|
|
|
409
449
|
build: number | null;
|
|
410
450
|
} | null;
|
|
411
451
|
}, {
|
|
452
|
+
summary: string;
|
|
412
453
|
score: number;
|
|
413
454
|
label: "Needs Attention" | "Good" | "Strong";
|
|
414
455
|
level: "critical" | "poor" | "fair" | "good" | "strong";
|
|
415
|
-
summary: string;
|
|
416
456
|
data_completeness: number;
|
|
417
457
|
pillars: {
|
|
418
458
|
spend: number | null;
|
|
@@ -760,10 +800,10 @@ export declare const financialContextSchema: z.ZodObject<{
|
|
|
760
800
|
build: number | null;
|
|
761
801
|
}>>;
|
|
762
802
|
}, "strict", z.ZodTypeAny, {
|
|
803
|
+
summary: string;
|
|
763
804
|
score: number;
|
|
764
805
|
label: "Needs Attention" | "Good" | "Strong";
|
|
765
806
|
level: "critical" | "poor" | "fair" | "good" | "strong";
|
|
766
|
-
summary: string;
|
|
767
807
|
data_completeness: number;
|
|
768
808
|
pillars: {
|
|
769
809
|
spend: number | null;
|
|
@@ -772,10 +812,10 @@ export declare const financialContextSchema: z.ZodObject<{
|
|
|
772
812
|
build: number | null;
|
|
773
813
|
} | null;
|
|
774
814
|
}, {
|
|
815
|
+
summary: string;
|
|
775
816
|
score: number;
|
|
776
817
|
label: "Needs Attention" | "Good" | "Strong";
|
|
777
818
|
level: "critical" | "poor" | "fair" | "good" | "strong";
|
|
778
|
-
summary: string;
|
|
779
819
|
data_completeness: number;
|
|
780
820
|
pillars: {
|
|
781
821
|
spend: number | null;
|
|
@@ -883,10 +923,10 @@ export declare const financialContextSchema: z.ZodObject<{
|
|
|
883
923
|
cost_basis: number | null;
|
|
884
924
|
}[];
|
|
885
925
|
health: {
|
|
926
|
+
summary: string;
|
|
886
927
|
score: number;
|
|
887
928
|
label: "Needs Attention" | "Good" | "Strong";
|
|
888
929
|
level: "critical" | "poor" | "fair" | "good" | "strong";
|
|
889
|
-
summary: string;
|
|
890
930
|
data_completeness: number;
|
|
891
931
|
pillars: {
|
|
892
932
|
spend: number | null;
|
|
@@ -994,10 +1034,10 @@ export declare const financialContextSchema: z.ZodObject<{
|
|
|
994
1034
|
cost_basis: number | null;
|
|
995
1035
|
}[];
|
|
996
1036
|
health: {
|
|
1037
|
+
summary: string;
|
|
997
1038
|
score: number;
|
|
998
1039
|
label: "Needs Attention" | "Good" | "Strong";
|
|
999
1040
|
level: "critical" | "poor" | "fair" | "good" | "strong";
|
|
1000
|
-
summary: string;
|
|
1001
1041
|
data_completeness: number;
|
|
1002
1042
|
pillars: {
|
|
1003
1043
|
spend: number | null;
|
package/dist/schemas.js
CHANGED
|
@@ -3,6 +3,30 @@ const monthSchema = z.string().regex(/^\d{4}-\d{2}$/, 'Expected YYYY-MM month.')
|
|
|
3
3
|
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected YYYY-MM-DD date.');
|
|
4
4
|
const dateTimeSchema = z.string().datetime({ offset: true });
|
|
5
5
|
export const emptyInputSchema = {};
|
|
6
|
+
export const automationInputSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
body: z.record(z.unknown()).optional(),
|
|
9
|
+
params: z.record(z.string()).optional(),
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
12
|
+
export const searchOperationsInputSchema = {
|
|
13
|
+
query: z.string().max(200).optional().describe('Words describing the action or data you need.'),
|
|
14
|
+
};
|
|
15
|
+
export const describeOperationInputSchema = {
|
|
16
|
+
operation_id: z.string().min(1),
|
|
17
|
+
input: automationInputSchema
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Exact intended input. Required to obtain approval for a write operation.'),
|
|
20
|
+
};
|
|
21
|
+
export const invokeOperationInputSchema = {
|
|
22
|
+
operation_id: z.string().min(1),
|
|
23
|
+
input: automationInputSchema.optional(),
|
|
24
|
+
approval_id: z
|
|
25
|
+
.string()
|
|
26
|
+
.uuid()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Approval ID returned by describe_operation for the exact write input.'),
|
|
29
|
+
};
|
|
6
30
|
export const transactionIndexSchema = z
|
|
7
31
|
.object({
|
|
8
32
|
total: z.number().int().nonnegative(),
|