contextwise 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/bin/contextwise.js +17 -0
- package/dist/chunk-P7JW7EPW.js +4383 -0
- package/dist/chunk-P7JW7EPW.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +1116 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1435 -0
- package/dist/index.js +171 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigLoader,
|
|
3
|
+
ContextWiseConfigSchema,
|
|
4
|
+
ContextWiseProxy,
|
|
5
|
+
KnownServerRegistry,
|
|
6
|
+
LLM_PRICING,
|
|
7
|
+
ServerManager,
|
|
8
|
+
SmitheryClient,
|
|
9
|
+
UnifiedRegistryClient,
|
|
10
|
+
UpstreamMultiplexer,
|
|
11
|
+
VaultAuditor,
|
|
12
|
+
cloudClient,
|
|
13
|
+
logger,
|
|
14
|
+
metricsCollector,
|
|
15
|
+
secretVault,
|
|
16
|
+
syncManager
|
|
17
|
+
} from "./chunk-P7JW7EPW.js";
|
|
18
|
+
|
|
19
|
+
// src/cli/index.ts
|
|
20
|
+
import { resolve as resolve2 } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { Command } from "commander";
|
|
23
|
+
|
|
24
|
+
// src/cli/commands/add.ts
|
|
25
|
+
import chalk from "chalk";
|
|
26
|
+
async function addCommand(serverName, extraArg, options) {
|
|
27
|
+
const rawName = serverName.trim();
|
|
28
|
+
const name = rawName.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
29
|
+
console.log(chalk.bold.cyan(`
|
|
30
|
+
\u26A1 ContextWise Server Configuration: "${name}"
|
|
31
|
+
`));
|
|
32
|
+
let serverConfig;
|
|
33
|
+
const envMap = {};
|
|
34
|
+
if (options.env) {
|
|
35
|
+
for (const item of options.env) {
|
|
36
|
+
const idx = item.indexOf("=");
|
|
37
|
+
if (idx > 0) {
|
|
38
|
+
const k = item.slice(0, idx).trim();
|
|
39
|
+
let v = item.slice(idx + 1).trim();
|
|
40
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
41
|
+
v = v.slice(1, -1);
|
|
42
|
+
}
|
|
43
|
+
envMap[k] = v;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (options.url) {
|
|
48
|
+
serverConfig = {
|
|
49
|
+
url: options.url,
|
|
50
|
+
headers: {},
|
|
51
|
+
transport: "auto",
|
|
52
|
+
autoReconnect: true
|
|
53
|
+
};
|
|
54
|
+
} else if (options.smithery || name.startsWith("smithery:") || name.startsWith("ai.smithery/")) {
|
|
55
|
+
const pkg = typeof options.smithery === "string" ? options.smithery : name.replace(/^smithery:/, "").replace(/^ai\.smithery\//, "");
|
|
56
|
+
console.log(chalk.magenta(`\u2713 Configuring Smithery package: ${chalk.bold(pkg)}`));
|
|
57
|
+
serverConfig = SmitheryClient.buildServerConfig(pkg, { env: envMap });
|
|
58
|
+
} else {
|
|
59
|
+
const knownPreset = KnownServerRegistry.find(name);
|
|
60
|
+
if (knownPreset) {
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.green(`\u2713 Recognized preset for ${chalk.bold(knownPreset.displayName)}`)
|
|
63
|
+
);
|
|
64
|
+
const passedArgs = options.args ?? [];
|
|
65
|
+
if (extraArg && !passedArgs.includes(extraArg)) {
|
|
66
|
+
passedArgs.push(extraArg);
|
|
67
|
+
}
|
|
68
|
+
for (const param of knownPreset.requiredParams) {
|
|
69
|
+
if (param.type === "env" && !envMap[param.name] && !process.env[param.name]) {
|
|
70
|
+
console.log(
|
|
71
|
+
chalk.yellow(
|
|
72
|
+
`\u26A0 Note: ${param.name} not set. Specify with --env ${param.name}=<value>`
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
serverConfig = KnownServerRegistry.buildConfig(knownPreset, {
|
|
78
|
+
args: passedArgs,
|
|
79
|
+
env: envMap
|
|
80
|
+
});
|
|
81
|
+
} else if (options.command) {
|
|
82
|
+
serverConfig = {
|
|
83
|
+
command: options.command,
|
|
84
|
+
args: options.args ?? (extraArg ? [extraArg] : []),
|
|
85
|
+
env: envMap,
|
|
86
|
+
autoRestart: true
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.red(`Error: "${name}" is not a recognized preset.`)
|
|
91
|
+
);
|
|
92
|
+
console.log(
|
|
93
|
+
`Please specify --command <cmd>, use --smithery, or check available presets with ${chalk.cyan("contextwise browse")}.
|
|
94
|
+
`
|
|
95
|
+
);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (options.test !== false) {
|
|
100
|
+
console.log(chalk.gray(`Testing connection to "${name}"...`));
|
|
101
|
+
const multiplexer = new UpstreamMultiplexer();
|
|
102
|
+
try {
|
|
103
|
+
await multiplexer.connectServer(name, serverConfig);
|
|
104
|
+
const status = multiplexer.getStatus().find((s) => s.name === name);
|
|
105
|
+
if (status && status.status === "connected") {
|
|
106
|
+
const tools = multiplexer.getAllTools();
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.green(
|
|
109
|
+
`\u2713 Connected successfully! Discovered ${tools.length} tools.`
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
for (const t of tools.slice(0, 5)) {
|
|
113
|
+
console.log(` \u2022 ${chalk.gray(t.namespacedName)}`);
|
|
114
|
+
}
|
|
115
|
+
if (tools.length > 5) {
|
|
116
|
+
console.log(` ... and ${tools.length - 5} more`);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
console.log(
|
|
120
|
+
chalk.yellow(
|
|
121
|
+
`\u26A0 Warning: Server could not connect (${status?.error || "unknown error"}). Saving configuration anyway.`
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
} catch (testErr) {
|
|
126
|
+
console.log(
|
|
127
|
+
chalk.yellow(
|
|
128
|
+
`\u26A0 Warning: Connection test encountered error: ${testErr}. Saving configuration anyway.`
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
await multiplexer.closeAll();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const savedPath = ServerManager.saveUpstream(name, serverConfig, options.config);
|
|
136
|
+
console.log(chalk.green(`
|
|
137
|
+
\u2713 Server "${name}" successfully saved to ${savedPath}!`));
|
|
138
|
+
console.log(
|
|
139
|
+
chalk.gray(
|
|
140
|
+
`ContextWise will now automatically index "${name}" on startup and make its tools available.
|
|
141
|
+
`
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/cli/commands/browse.ts
|
|
147
|
+
import chalk3 from "chalk";
|
|
148
|
+
|
|
149
|
+
// src/cli/ui/navigator.ts
|
|
150
|
+
import readline from "readline";
|
|
151
|
+
import chalk2 from "chalk";
|
|
152
|
+
async function launchNavigator(options = {}) {
|
|
153
|
+
if (!process.stdin.isTTY) {
|
|
154
|
+
console.log(chalk2.yellow("Interactive navigator requires an interactive TTY terminal."));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let searchQuery = options.query || options.category || "";
|
|
158
|
+
let selectedIndex = 0;
|
|
159
|
+
let allServers = [];
|
|
160
|
+
let isFetching = true;
|
|
161
|
+
allServers = await UnifiedRegistryClient.search({
|
|
162
|
+
query: searchQuery || void 0,
|
|
163
|
+
category: options.category,
|
|
164
|
+
source: options.source || "all",
|
|
165
|
+
limit: 40
|
|
166
|
+
});
|
|
167
|
+
isFetching = false;
|
|
168
|
+
readline.emitKeypressEvents(process.stdin);
|
|
169
|
+
process.stdin.setRawMode(true);
|
|
170
|
+
process.stdin.resume();
|
|
171
|
+
process.stdout.write("\x1B[?25l");
|
|
172
|
+
const cleanup = () => {
|
|
173
|
+
process.stdout.write("\x1B[?25h");
|
|
174
|
+
if (process.stdin.isTTY) {
|
|
175
|
+
process.stdin.setRawMode(false);
|
|
176
|
+
}
|
|
177
|
+
process.stdin.pause();
|
|
178
|
+
console.clear();
|
|
179
|
+
};
|
|
180
|
+
const getSourceBadge = (src) => {
|
|
181
|
+
switch (src) {
|
|
182
|
+
case "curated":
|
|
183
|
+
return chalk2.bgGreen.black(" CURATED ");
|
|
184
|
+
case "official":
|
|
185
|
+
return chalk2.bgCyan.black(" OFFICIAL ");
|
|
186
|
+
case "smithery":
|
|
187
|
+
return chalk2.bgMagenta.black(" SMITHERY ");
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const render = () => {
|
|
191
|
+
const q = searchQuery.toLowerCase().trim();
|
|
192
|
+
const filtered = allServers.filter(
|
|
193
|
+
(s) => !q || s.displayName.toLowerCase().includes(q) || s.id.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.tags && s.tags.some((t) => t.toLowerCase().includes(q))
|
|
194
|
+
);
|
|
195
|
+
if (selectedIndex >= filtered.length) {
|
|
196
|
+
selectedIndex = Math.max(0, filtered.length - 1);
|
|
197
|
+
}
|
|
198
|
+
const termHeight = process.stdout.rows || 25;
|
|
199
|
+
const termWidth = process.stdout.columns || 80;
|
|
200
|
+
const pageSize = Math.max(5, Math.min(10, termHeight - 14));
|
|
201
|
+
const startIdx = Math.max(
|
|
202
|
+
0,
|
|
203
|
+
Math.min(
|
|
204
|
+
selectedIndex - Math.floor(pageSize / 2),
|
|
205
|
+
Math.max(0, filtered.length - pageSize)
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
const visibleServers = filtered.slice(startIdx, startIdx + pageSize);
|
|
209
|
+
console.clear();
|
|
210
|
+
console.log(
|
|
211
|
+
chalk2.bold.cyan("\u{1F310} ContextWise Interactive MCP Navigator") + chalk2.gray(" | ") + chalk2.white("Search, browse, and hot-load MCP servers")
|
|
212
|
+
);
|
|
213
|
+
console.log(
|
|
214
|
+
chalk2.gray("Controls: ") + chalk2.yellow("\u2191/\u2193") + chalk2.gray(" Navigate ") + chalk2.yellow("[a] / [Enter]") + chalk2.gray(" Add Server ") + chalk2.yellow("[Esc] / [q]") + chalk2.gray(" Exit ") + chalk2.yellow("Type") + chalk2.gray(" to filter\n")
|
|
215
|
+
);
|
|
216
|
+
console.log(
|
|
217
|
+
chalk2.bold("Filter: ") + chalk2.cyan(searchQuery ? searchQuery : chalk2.gray("(Type to filter...)")) + chalk2.gray(` [${filtered.length} matching / ${allServers.length} total]`)
|
|
218
|
+
);
|
|
219
|
+
console.log(chalk2.gray("\u2500".repeat(Math.min(termWidth, 76))));
|
|
220
|
+
if (isFetching) {
|
|
221
|
+
console.log(chalk2.yellow("\n Loading servers from registries...\n"));
|
|
222
|
+
} else if (filtered.length === 0) {
|
|
223
|
+
console.log(chalk2.yellow(`
|
|
224
|
+
No servers matching "${searchQuery}".`));
|
|
225
|
+
console.log(chalk2.gray(" Press Backspace to broaden search or Esc to exit.\n"));
|
|
226
|
+
} else {
|
|
227
|
+
for (let i = 0; i < visibleServers.length; i++) {
|
|
228
|
+
const item = visibleServers[i];
|
|
229
|
+
const actualIndex = startIdx + i;
|
|
230
|
+
const isSelected = actualIndex === selectedIndex;
|
|
231
|
+
const cursor = isSelected ? chalk2.bold.green("\u25B8 ") : " ";
|
|
232
|
+
const badge = getSourceBadge(item.source);
|
|
233
|
+
const nameStr = isSelected ? chalk2.bold.underline.white(item.displayName) : chalk2.white(item.displayName);
|
|
234
|
+
const idStr = chalk2.gray(`(${item.id})`);
|
|
235
|
+
console.log(`${cursor}${badge} ${nameStr} ${idStr}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
console.log(chalk2.gray("\u2500".repeat(Math.min(termWidth, 76))));
|
|
239
|
+
const selectedItem = filtered[selectedIndex];
|
|
240
|
+
if (selectedItem) {
|
|
241
|
+
console.log(
|
|
242
|
+
chalk2.bold("Selected: ") + chalk2.green(selectedItem.displayName) + chalk2.gray(` [${selectedItem.sourceLabel}]`)
|
|
243
|
+
);
|
|
244
|
+
console.log(chalk2.white(`Description: ${selectedItem.description}`));
|
|
245
|
+
if (selectedItem.requiredParams && selectedItem.requiredParams.length > 0) {
|
|
246
|
+
const params = selectedItem.requiredParams.map((p) => `${p.name} (${p.type}: ${p.description})`).join(", ");
|
|
247
|
+
console.log(chalk2.yellow(`Required Params: ${params}`));
|
|
248
|
+
}
|
|
249
|
+
const docsUrl = selectedItem.homepage || (selectedItem.source === "smithery" ? `https://smithery.ai/servers/${selectedItem.id}` : selectedItem.source === "curated" ? `https://github.com/modelcontextprotocol/servers/tree/main/src/${selectedItem.id}` : `https://registry.modelcontextprotocol.io`);
|
|
250
|
+
console.log(chalk2.cyan("Docs: ") + chalk2.underline.white(docsUrl));
|
|
251
|
+
console.log(
|
|
252
|
+
chalk2.cyan(`Quick Action: `) + chalk2.white(`Press `) + chalk2.bold.yellow(`[a]`) + chalk2.white(` or `) + chalk2.bold.yellow(`[Enter]`) + chalk2.white(` to install `) + chalk2.bold(selectedItem.id)
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
console.log(chalk2.gray("Select a server above to view configuration and details."));
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
render();
|
|
259
|
+
return new Promise((resolve3) => {
|
|
260
|
+
process.stdin.on("keypress", async (str, key) => {
|
|
261
|
+
if (!key) {
|
|
262
|
+
if (str && str.length === 1 && str >= " ") {
|
|
263
|
+
searchQuery += str;
|
|
264
|
+
selectedIndex = 0;
|
|
265
|
+
render();
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (key.name === "escape" || key.ctrl && key.name === "c" || key.name === "q" && searchQuery === "") {
|
|
270
|
+
cleanup();
|
|
271
|
+
console.log(chalk2.gray("Exited ContextWise Navigator.\n"));
|
|
272
|
+
resolve3();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (key.name === "up") {
|
|
276
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
277
|
+
render();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (key.name === "down") {
|
|
281
|
+
const q = searchQuery.toLowerCase().trim();
|
|
282
|
+
const filteredCount = allServers.filter(
|
|
283
|
+
(s) => !q || s.displayName.toLowerCase().includes(q) || s.id.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)
|
|
284
|
+
).length;
|
|
285
|
+
selectedIndex = Math.min(Math.max(0, filteredCount - 1), selectedIndex + 1);
|
|
286
|
+
render();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (key.name === "backspace") {
|
|
290
|
+
if (searchQuery.length > 0) {
|
|
291
|
+
searchQuery = searchQuery.slice(0, -1);
|
|
292
|
+
selectedIndex = 0;
|
|
293
|
+
render();
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (key.name === "return" || key.name === "a") {
|
|
298
|
+
const q = searchQuery.toLowerCase().trim();
|
|
299
|
+
const filtered = allServers.filter(
|
|
300
|
+
(s) => !q || s.displayName.toLowerCase().includes(q) || s.id.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)
|
|
301
|
+
);
|
|
302
|
+
const target = filtered[selectedIndex];
|
|
303
|
+
if (target) {
|
|
304
|
+
cleanup();
|
|
305
|
+
console.log(chalk2.bold.green(`
|
|
306
|
+
\u2713 Selected MCP Server: "${target.displayName}" (${target.id})
|
|
307
|
+
`));
|
|
308
|
+
try {
|
|
309
|
+
let config = target.suggestedConfig;
|
|
310
|
+
if (!config) {
|
|
311
|
+
if (target.source === "smithery") {
|
|
312
|
+
config = SmitheryClient.buildServerConfig(target.id);
|
|
313
|
+
} else {
|
|
314
|
+
const preset = KnownServerRegistry.find(target.id);
|
|
315
|
+
if (preset) {
|
|
316
|
+
config = KnownServerRegistry.buildConfig(preset);
|
|
317
|
+
} else {
|
|
318
|
+
config = {
|
|
319
|
+
command: "npx",
|
|
320
|
+
args: ["-y", target.id],
|
|
321
|
+
env: {},
|
|
322
|
+
autoRestart: true
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const savedPath = ServerManager.saveUpstream(target.id, config);
|
|
328
|
+
console.log(
|
|
329
|
+
chalk2.green(
|
|
330
|
+
`\u2713 Successfully added "${target.id}" into ${chalk2.bold(savedPath)}!`
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
console.log(
|
|
334
|
+
chalk2.cyan(
|
|
335
|
+
`The server is now configured and will be loaded automatically on next session or when ContextWise starts.
|
|
336
|
+
`
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.log(chalk2.red(`Failed to add server: ${err}
|
|
341
|
+
`));
|
|
342
|
+
}
|
|
343
|
+
resolve3();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta && str >= " ") {
|
|
348
|
+
searchQuery += str;
|
|
349
|
+
selectedIndex = 0;
|
|
350
|
+
render();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/cli/commands/browse.ts
|
|
357
|
+
async function browseCommand(options) {
|
|
358
|
+
if (options.interactive) {
|
|
359
|
+
await launchNavigator({
|
|
360
|
+
query: options.query,
|
|
361
|
+
category: options.category,
|
|
362
|
+
source: options.source
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const source = (options.offline ? "curated" : options.source) || "all";
|
|
367
|
+
console.log(chalk3.bold.cyan("\n\u{1F310} ContextWise Multi-Registry MCP Browser"));
|
|
368
|
+
console.log(
|
|
369
|
+
chalk3.gray(
|
|
370
|
+
`Searching sources: ${source === "all" ? "Official MCP Registry + Smithery + Curated Presets" : source}
|
|
371
|
+
`
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
const servers = await UnifiedRegistryClient.search({
|
|
375
|
+
query: options.query,
|
|
376
|
+
category: options.category,
|
|
377
|
+
source,
|
|
378
|
+
limit: 20
|
|
379
|
+
});
|
|
380
|
+
if (servers.length === 0) {
|
|
381
|
+
console.log(chalk3.yellow("No matching MCP servers found across registries."));
|
|
382
|
+
console.log(
|
|
383
|
+
chalk3.gray(
|
|
384
|
+
'Try broadening your query, or install any custom server using "contextwise add <name> --command <cmd>"\n'
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const getSourceBadge = (src) => {
|
|
390
|
+
switch (src) {
|
|
391
|
+
case "curated":
|
|
392
|
+
return chalk3.bgGreen.black(" CURATED / VERIFIED ");
|
|
393
|
+
case "official":
|
|
394
|
+
return chalk3.bgCyan.black(" OFFICIAL MCP REGISTRY ");
|
|
395
|
+
case "smithery":
|
|
396
|
+
return chalk3.bgMagenta.black(" SMITHERY ");
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
console.log(chalk3.bold(`Found ${servers.length} MCP Servers:
|
|
400
|
+
`));
|
|
401
|
+
for (const s of servers) {
|
|
402
|
+
const verifiedIcon = s.verified ? chalk3.green(" \u2713") : "";
|
|
403
|
+
console.log(
|
|
404
|
+
`${getSourceBadge(s.source)} ${chalk3.bold.white(s.displayName)} (${chalk3.gray(s.id)})${verifiedIcon}`
|
|
405
|
+
);
|
|
406
|
+
console.log(` ${chalk3.white(s.description)}`);
|
|
407
|
+
if (s.requiredParams && s.requiredParams.length > 0) {
|
|
408
|
+
const paramStr = s.requiredParams.map((p) => `${p.name} (${p.type})`).join(", ");
|
|
409
|
+
console.log(` ${chalk3.yellow("Required parameters:")} ${chalk3.gray(paramStr)}`);
|
|
410
|
+
}
|
|
411
|
+
const docsUrl = s.homepage || (s.source === "smithery" ? `https://smithery.ai/servers/${s.id}` : s.source === "curated" ? `https://github.com/modelcontextprotocol/servers/tree/main/src/${s.id}` : `https://registry.modelcontextprotocol.io`);
|
|
412
|
+
console.log(` ${chalk3.cyan("Docs:")} ${chalk3.underline.white(docsUrl)}`);
|
|
413
|
+
let installCmd = `contextwise add ${s.id}`;
|
|
414
|
+
if (s.source === "smithery") {
|
|
415
|
+
installCmd = `contextwise add ${s.id} --smithery`;
|
|
416
|
+
}
|
|
417
|
+
console.log(` ${chalk3.cyan("Install:")} ${chalk3.bold(installCmd)}
|
|
418
|
+
`);
|
|
419
|
+
}
|
|
420
|
+
console.log(
|
|
421
|
+
chalk3.gray(
|
|
422
|
+
"To install any server: " + chalk3.white("contextwise add <id>") + " or hot-load in chat with " + chalk3.white("contextwise_add_server\n")
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/cli/commands/init.ts
|
|
428
|
+
import { existsSync, writeFileSync } from "fs";
|
|
429
|
+
import { resolve } from "path";
|
|
430
|
+
import chalk4 from "chalk";
|
|
431
|
+
async function initCommand(options) {
|
|
432
|
+
const cwd = options.cwd ?? process.cwd();
|
|
433
|
+
const targetPath = resolve(cwd, "contextwise.json");
|
|
434
|
+
console.log(chalk4.bold.cyan("\n\u{1F680} ContextWise Setup Wizard\n"));
|
|
435
|
+
if (existsSync(targetPath) && !options.force) {
|
|
436
|
+
console.log(
|
|
437
|
+
chalk4.yellow(`Configuration file already exists at ${targetPath}.`)
|
|
438
|
+
);
|
|
439
|
+
console.log("Use --force to overwrite.\n");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const imported = ConfigLoader.autoImportClientConfigs(cwd);
|
|
443
|
+
let initialConfig;
|
|
444
|
+
if (imported && Object.keys(imported.upstreams).length > 0) {
|
|
445
|
+
console.log(
|
|
446
|
+
chalk4.green(
|
|
447
|
+
`\u2713 Found existing MCP servers from AI client configuration:`
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
for (const name of Object.keys(imported.upstreams)) {
|
|
451
|
+
console.log(` - ${chalk4.bold(name)}`);
|
|
452
|
+
}
|
|
453
|
+
initialConfig = imported;
|
|
454
|
+
} else {
|
|
455
|
+
console.log(chalk4.blue("\u2139 No existing MCP clients detected. Generating template configuration."));
|
|
456
|
+
initialConfig = ContextWiseConfigSchema.parse({
|
|
457
|
+
version: "1.0.0",
|
|
458
|
+
proxy: {
|
|
459
|
+
transport: "stdio",
|
|
460
|
+
port: 3456,
|
|
461
|
+
logLevel: "info"
|
|
462
|
+
},
|
|
463
|
+
routing: {
|
|
464
|
+
strategy: "hybrid",
|
|
465
|
+
topK: 5,
|
|
466
|
+
similarityThreshold: 0.45,
|
|
467
|
+
pinnedTools: []
|
|
468
|
+
},
|
|
469
|
+
guardrails: {
|
|
470
|
+
enableCache: true,
|
|
471
|
+
cacheTtlSeconds: 120,
|
|
472
|
+
maxCallsPerMinute: 60,
|
|
473
|
+
loopBreakerThreshold: 3
|
|
474
|
+
},
|
|
475
|
+
upstreams: {}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
writeFileSync(targetPath, JSON.stringify(initialConfig, null, 2), "utf-8");
|
|
479
|
+
console.log(chalk4.green(`
|
|
480
|
+
\u2713 Successfully created ${targetPath}`));
|
|
481
|
+
console.log(
|
|
482
|
+
chalk4.gray(
|
|
483
|
+
"\nNext step: Connect your AI client (Cursor, Claude Desktop, Windsurf) by adding ContextWise to its MCP configuration:\n"
|
|
484
|
+
)
|
|
485
|
+
);
|
|
486
|
+
console.log(chalk4.white('{\n "mcpServers": {\n "contextwise": {\n "command": "npx",\n "args": ["-y", "contextwise", "start"]\n }\n }\n}\n'));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/cli/commands/list.ts
|
|
490
|
+
import chalk5 from "chalk";
|
|
491
|
+
async function listCommand(options) {
|
|
492
|
+
const config = ConfigLoader.load({ configPath: options.config });
|
|
493
|
+
const upstreamCount = Object.keys(config.upstreams).length;
|
|
494
|
+
console.log(chalk5.bold.cyan("\n\u{1F50D} ContextWise Catalog Overview\n"));
|
|
495
|
+
console.log(`Configured Upstreams: ${chalk5.bold(upstreamCount)}`);
|
|
496
|
+
if (upstreamCount === 0) {
|
|
497
|
+
console.log(
|
|
498
|
+
chalk5.yellow('No upstream MCP servers defined. Run "contextwise init" to add servers.\n')
|
|
499
|
+
);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const multiplexer = new UpstreamMultiplexer();
|
|
503
|
+
try {
|
|
504
|
+
await multiplexer.connectAll(config.upstreams);
|
|
505
|
+
const statuses = multiplexer.getStatus();
|
|
506
|
+
const tools = multiplexer.getAllTools();
|
|
507
|
+
console.log(chalk5.bold("\n--- Upstream Servers ---"));
|
|
508
|
+
for (const s of statuses) {
|
|
509
|
+
const statusColor = s.status === "connected" ? chalk5.green : chalk5.red;
|
|
510
|
+
console.log(
|
|
511
|
+
`\u2022 ${chalk5.bold(s.name)} [${s.transportType}] - ${statusColor(s.status.toUpperCase())} (${s.toolsCount} tools)`
|
|
512
|
+
);
|
|
513
|
+
if (s.error) {
|
|
514
|
+
console.log(` ${chalk5.red("Error:")} ${s.error}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
console.log(chalk5.bold(`
|
|
518
|
+
--- Aggregated Tools (${tools.length} total) ---`));
|
|
519
|
+
for (const tool of tools) {
|
|
520
|
+
const hint = tool.readOnlyHint ? chalk5.gray(" [read-only]") : "";
|
|
521
|
+
console.log(`\u2022 ${chalk5.green(tool.namespacedName)}${hint}`);
|
|
522
|
+
if (tool.description) {
|
|
523
|
+
console.log(` ${chalk5.gray(tool.description.slice(0, 100))}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
console.log("");
|
|
527
|
+
} finally {
|
|
528
|
+
await multiplexer.closeAll();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/cli/commands/cloud.ts
|
|
533
|
+
import { exec } from "child_process";
|
|
534
|
+
import chalk6 from "chalk";
|
|
535
|
+
function openBrowser(url) {
|
|
536
|
+
const startCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
537
|
+
exec(`${startCmd} "${url}"`, (err) => {
|
|
538
|
+
if (err) {
|
|
539
|
+
console.log(chalk6.gray(`Could not open browser automatically. Please visit:
|
|
540
|
+
${url}`));
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
async function loginCommand(apiKey) {
|
|
545
|
+
const key = apiKey?.trim();
|
|
546
|
+
if (key) {
|
|
547
|
+
try {
|
|
548
|
+
const token = await cloudClient.loginWithKey(key);
|
|
549
|
+
console.log(chalk6.green(`
|
|
550
|
+
\u2714 Logged in successfully as ${chalk6.bold(token.email)}
|
|
551
|
+
`));
|
|
552
|
+
console.log(chalk6.gray(`User ID: ${token.userId}`));
|
|
553
|
+
console.log(chalk6.gray(`Session valid until: ${new Date(token.expiresAt).toLocaleDateString()}
|
|
554
|
+
`));
|
|
555
|
+
return;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.error(chalk6.red(`
|
|
558
|
+
\u2716 Login failed: ${err instanceof Error ? err.message : err}
|
|
559
|
+
`));
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
console.log(chalk6.cyan("\nInitiating ContextWise Cloud authorization..."));
|
|
565
|
+
const flow = await cloudClient.startDeviceFlow();
|
|
566
|
+
console.log(chalk6.bold("\nTo authorize this device, follow these steps:"));
|
|
567
|
+
console.log(`1. Open this URL in your browser:`);
|
|
568
|
+
console.log(` ${chalk6.bold.underline.blue(flow.verification_uri)}`);
|
|
569
|
+
console.log(`2. Verify the confirmation code:`);
|
|
570
|
+
console.log(` ${chalk6.bold.yellow(flow.user_code)}
|
|
571
|
+
`);
|
|
572
|
+
console.log(chalk6.gray("Waiting for browser approval... (press Ctrl+C to cancel)"));
|
|
573
|
+
const token = await cloudClient.pollDeviceToken(flow.device_code, flow.interval);
|
|
574
|
+
console.log(chalk6.green(`
|
|
575
|
+
\u2714 Logged in successfully as ${chalk6.bold(token.email)}
|
|
576
|
+
`));
|
|
577
|
+
console.log(chalk6.gray(`User ID: ${token.userId}`));
|
|
578
|
+
console.log(chalk6.gray(`Session valid until: ${new Date(token.expiresAt).toLocaleDateString()}
|
|
579
|
+
`));
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.error(chalk6.red(`
|
|
582
|
+
\u2716 Login failed: ${err instanceof Error ? err.message : err}
|
|
583
|
+
`));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async function logoutCommand() {
|
|
588
|
+
cloudClient.clearToken();
|
|
589
|
+
console.log(chalk6.green("\n\u2714 Logged out of ContextWise Cloud.\n"));
|
|
590
|
+
}
|
|
591
|
+
async function whoamiCommand() {
|
|
592
|
+
try {
|
|
593
|
+
const info = await cloudClient.whoami();
|
|
594
|
+
console.log(chalk6.bold.cyan("\n\u{1F464} ContextWise Cloud Identity\n"));
|
|
595
|
+
console.log(`User Email: ${chalk6.bold(info.email)}`);
|
|
596
|
+
console.log(`User ID: ${chalk6.gray(info.userId)}`);
|
|
597
|
+
const planDisplay = info.plan === "pro" ? chalk6.bold.green("PRO") : info.plan === "team" ? chalk6.bold.blue("TEAM") : info.plan === "enterprise" ? chalk6.bold.magenta("ENTERPRISE") : chalk6.bold.gray("COMMUNITY (Free)");
|
|
598
|
+
const statusDisplay = info.subscriptionStatus === "active" ? chalk6.green("Active") : chalk6.yellow(info.subscriptionStatus || "Inactive");
|
|
599
|
+
console.log(`Plan: ${planDisplay} (${statusDisplay})`);
|
|
600
|
+
if (info.currentPeriodEnd) {
|
|
601
|
+
console.log(
|
|
602
|
+
`Next Renewal: ${chalk6.gray(
|
|
603
|
+
new Date(info.currentPeriodEnd).toISOString().replace("T", " ").slice(0, 10)
|
|
604
|
+
)}`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
console.log("");
|
|
608
|
+
if (info.workspaces && info.workspaces.length > 0) {
|
|
609
|
+
console.log(chalk6.bold("Workspaces:"));
|
|
610
|
+
for (const w of info.workspaces) {
|
|
611
|
+
console.log(`\u2022 ${chalk6.green(w.name)} (${w.id}) - Role: ${w.role}`);
|
|
612
|
+
}
|
|
613
|
+
console.log("");
|
|
614
|
+
}
|
|
615
|
+
} catch (err) {
|
|
616
|
+
console.error(chalk6.red(`
|
|
617
|
+
\u2716 Failed to retrieve identity: ${err instanceof Error ? err.message : err}
|
|
618
|
+
`));
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function upgradeCommand(options) {
|
|
623
|
+
if (!cloudClient.isAuthenticated()) {
|
|
624
|
+
console.log(chalk6.yellow("\nYou must be logged in to upgrade your subscription."));
|
|
625
|
+
console.log(
|
|
626
|
+
chalk6.cyan(
|
|
627
|
+
'Run "contextwise login" first, or visit https://contextwise.dev/#pricing in your browser.\n'
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const plan = options.team ? "team" : "pro";
|
|
633
|
+
const interval = options.annual ? "year" : "month";
|
|
634
|
+
console.log(
|
|
635
|
+
chalk6.cyan(
|
|
636
|
+
`
|
|
637
|
+
Initiating Stripe Checkout for ContextWise ${chalk6.bold(plan.toUpperCase())} (${interval}ly)...`
|
|
638
|
+
)
|
|
639
|
+
);
|
|
640
|
+
try {
|
|
641
|
+
const session = await cloudClient.createCheckoutSession(plan, interval);
|
|
642
|
+
console.log(chalk6.green("\n\u2714 Checkout session created."));
|
|
643
|
+
console.log(`Opening your browser to complete payment via Stripe:
|
|
644
|
+
`);
|
|
645
|
+
console.log(` ${chalk6.bold.underline.blue(session.checkoutUrl)}
|
|
646
|
+
`);
|
|
647
|
+
openBrowser(session.checkoutUrl);
|
|
648
|
+
console.log(chalk6.gray("Once payment is confirmed, your subscription will be instantly unlocked."));
|
|
649
|
+
console.log(chalk6.gray('Run "contextwise whoami" to confirm your subscription status.\n'));
|
|
650
|
+
} catch (err) {
|
|
651
|
+
console.error(
|
|
652
|
+
chalk6.red(`
|
|
653
|
+
\u2716 Unable to initiate checkout: ${err instanceof Error ? err.message : err}
|
|
654
|
+
`)
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function billingCommand() {
|
|
659
|
+
if (!cloudClient.isAuthenticated()) {
|
|
660
|
+
console.log(chalk6.yellow("\nYou must be logged in to access the billing portal."));
|
|
661
|
+
console.log(chalk6.cyan('Run "contextwise login" first.\n'));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
console.log(chalk6.cyan("\nOpening Stripe Customer Billing Portal..."));
|
|
665
|
+
try {
|
|
666
|
+
const session = await cloudClient.createPortalSession();
|
|
667
|
+
console.log(`Opening your browser to manage payment methods, invoices, and subscriptions:
|
|
668
|
+
`);
|
|
669
|
+
console.log(` ${chalk6.bold.underline.blue(session.portalUrl)}
|
|
670
|
+
`);
|
|
671
|
+
openBrowser(session.portalUrl);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
console.error(
|
|
674
|
+
chalk6.red(`
|
|
675
|
+
\u2716 Unable to open billing portal: ${err instanceof Error ? err.message : err}
|
|
676
|
+
`)
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function pushCommand(workspaceId) {
|
|
681
|
+
if (!cloudClient.isAuthenticated()) {
|
|
682
|
+
console.log(chalk6.yellow("\nNot logged in to ContextWise Cloud."));
|
|
683
|
+
console.log(chalk6.cyan('Run "contextwise login" first.\n'));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
console.log(chalk6.cyan("\n\u2601 Pushing local workspace to ContextWise Cloud..."));
|
|
687
|
+
try {
|
|
688
|
+
const res = await syncManager.push(workspaceId);
|
|
689
|
+
if (res.status === "committed") {
|
|
690
|
+
console.log(chalk6.green(`\u2714 Successfully pushed revision #${chalk6.bold(res.revision)} to Cloud.
|
|
691
|
+
`));
|
|
692
|
+
} else {
|
|
693
|
+
console.log(chalk6.yellow(`\u26A0 Push conflict: Server has revision #${res.serverRevision}. Pull changes first.
|
|
694
|
+
`));
|
|
695
|
+
}
|
|
696
|
+
} catch (err) {
|
|
697
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
698
|
+
if (msg.includes("requires an upgraded subscription") || msg.includes("Cloud Sync requires")) {
|
|
699
|
+
console.log(
|
|
700
|
+
chalk6.yellow("\n\u26A0 Multi-device Cloud Sync requires a ContextWise Pro or Team subscription.")
|
|
701
|
+
);
|
|
702
|
+
console.log(
|
|
703
|
+
chalk6.cyan(
|
|
704
|
+
'Run "contextwise upgrade" to subscribe via Stripe, or visit https://contextwise.dev/#pricing\n'
|
|
705
|
+
)
|
|
706
|
+
);
|
|
707
|
+
} else {
|
|
708
|
+
console.error(chalk6.red(`
|
|
709
|
+
\u2716 Cloud push failed: ${msg}
|
|
710
|
+
`));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async function pullCommand(workspaceId) {
|
|
715
|
+
if (!cloudClient.isAuthenticated()) {
|
|
716
|
+
console.log(chalk6.yellow("\nNot logged in to ContextWise Cloud."));
|
|
717
|
+
console.log(chalk6.cyan('Run "contextwise login" first.\n'));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
console.log(chalk6.cyan("\n\u2601 Pulling latest workspace snapshot from ContextWise Cloud..."));
|
|
721
|
+
try {
|
|
722
|
+
const res = await syncManager.pull(workspaceId);
|
|
723
|
+
if (res) {
|
|
724
|
+
console.log(chalk6.green(`\u2714 Applied cloud revision #${chalk6.bold(res.revision)} to local workspace.
|
|
725
|
+
`));
|
|
726
|
+
} else {
|
|
727
|
+
console.log(chalk6.gray("\u2714 Local workspace is already up to date.\n"));
|
|
728
|
+
}
|
|
729
|
+
} catch (err) {
|
|
730
|
+
console.error(chalk6.red(`
|
|
731
|
+
\u2716 Cloud pull failed: ${err instanceof Error ? err.message : err}
|
|
732
|
+
`));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async function syncStatusCommand() {
|
|
736
|
+
const status = syncManager.getStatus();
|
|
737
|
+
console.log(chalk6.bold.cyan("\n\u{1F504} ContextWise Cloud Sync Status\n"));
|
|
738
|
+
console.log(`Logged In: ${status.isLoggedIn ? chalk6.green("YES") : chalk6.gray("NO")}`);
|
|
739
|
+
if (status.userEmail) {
|
|
740
|
+
console.log(`Account: ${chalk6.bold(status.userEmail)}`);
|
|
741
|
+
}
|
|
742
|
+
console.log(`Device ID: ${chalk6.gray(status.deviceId)}`);
|
|
743
|
+
console.log(`Workspace ID: ${status.workspaceId}`);
|
|
744
|
+
console.log(`Local Revision: ${chalk6.bold(status.revision)}`);
|
|
745
|
+
console.log(
|
|
746
|
+
`Last Synced: ${status.lastSyncedAt > 0 ? new Date(status.lastSyncedAt).toISOString().replace("T", " ").slice(0, 19) : chalk6.gray("Never")}
|
|
747
|
+
`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/cli/commands/secret.ts
|
|
752
|
+
import { createInterface } from "readline/promises";
|
|
753
|
+
import chalk7 from "chalk";
|
|
754
|
+
async function secretSetCommand(key, value, options = {}) {
|
|
755
|
+
if (!key || typeof key !== "string") {
|
|
756
|
+
console.error(chalk7.red("Error: Secret key must be specified."));
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
let secretValue = value;
|
|
760
|
+
if (!secretValue) {
|
|
761
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
762
|
+
try {
|
|
763
|
+
secretValue = await rl.question(chalk7.cyan(`Enter secret value for "${key}": `));
|
|
764
|
+
} finally {
|
|
765
|
+
rl.close();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
secretValue = secretValue?.trim();
|
|
769
|
+
if (!secretValue) {
|
|
770
|
+
console.error(chalk7.red("Error: Secret value cannot be empty."));
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
const scope = options.scope || "personal";
|
|
774
|
+
await secretVault.set(key, secretValue, scope);
|
|
775
|
+
console.log(
|
|
776
|
+
chalk7.green(
|
|
777
|
+
`
|
|
778
|
+
\u2714 Secret "${chalk7.bold(key)}" stored successfully in vault [${secretVault.getActiveDriverName()}] (scope: ${scope})
|
|
779
|
+
`
|
|
780
|
+
)
|
|
781
|
+
);
|
|
782
|
+
console.log(
|
|
783
|
+
chalk7.gray(
|
|
784
|
+
`Tip: Reference this secret in contextwise.json with: "vault://${key}"
|
|
785
|
+
`
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
async function secretGetCommand(key, options = {}) {
|
|
790
|
+
if (!key) {
|
|
791
|
+
console.error(chalk7.red("Error: Secret key must be specified."));
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
const value = await secretVault.get(key);
|
|
795
|
+
if (value === null || value === void 0) {
|
|
796
|
+
console.log(chalk7.red(`
|
|
797
|
+
\u2716 Secret "${key}" not found in ContextWise vault.
|
|
798
|
+
`));
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
if (options.reveal) {
|
|
802
|
+
console.log(`
|
|
803
|
+
${chalk7.bold(key)}: ${chalk7.yellow(value)}
|
|
804
|
+
`);
|
|
805
|
+
} else {
|
|
806
|
+
const masked = value.length > 8 ? `${value.slice(0, 4)}\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022${value.slice(-4)}` : "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
807
|
+
console.log(
|
|
808
|
+
`
|
|
809
|
+
${chalk7.bold(key)}: ${chalk7.yellow(masked)} ${chalk7.gray(
|
|
810
|
+
`(${value.length} chars, pass --reveal to view plaintext)`
|
|
811
|
+
)}
|
|
812
|
+
`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function secretListCommand() {
|
|
817
|
+
const secrets = await secretVault.list();
|
|
818
|
+
console.log(chalk7.bold.cyan("\n\u{1F510} ContextWise Secret Vault\n"));
|
|
819
|
+
console.log(`Active Storage Driver: ${chalk7.bold(secretVault.getActiveDriverName())}`);
|
|
820
|
+
console.log(`Total Stored Secrets: ${chalk7.bold(secrets.length)}
|
|
821
|
+
`);
|
|
822
|
+
if (secrets.length === 0) {
|
|
823
|
+
console.log(chalk7.yellow("No secrets currently stored in vault."));
|
|
824
|
+
console.log(
|
|
825
|
+
chalk7.gray("Store a secret with: contextwise secret set <key> <value>\n")
|
|
826
|
+
);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
console.log(
|
|
830
|
+
chalk7.bold(
|
|
831
|
+
`${"KEY".padEnd(30)} ${"SCOPE".padEnd(14)} ${"BACKEND".padEnd(18)} ${"UPDATED"}`
|
|
832
|
+
)
|
|
833
|
+
);
|
|
834
|
+
console.log(chalk7.gray("\u2500".repeat(75)));
|
|
835
|
+
for (const s of secrets) {
|
|
836
|
+
const dateStr = new Date(s.updatedAt).toISOString().replace("T", " ").slice(0, 19);
|
|
837
|
+
console.log(
|
|
838
|
+
`${chalk7.cyan(s.key.padEnd(30))} ${s.scope.padEnd(14)} ${s.backend.padEnd(18)} ${chalk7.gray(dateStr)}`
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
console.log("");
|
|
842
|
+
}
|
|
843
|
+
async function secretDeleteCommand(key) {
|
|
844
|
+
if (!key) {
|
|
845
|
+
console.error(chalk7.red("Error: Secret key must be specified."));
|
|
846
|
+
process.exit(1);
|
|
847
|
+
}
|
|
848
|
+
const deleted = await secretVault.delete(key);
|
|
849
|
+
if (deleted) {
|
|
850
|
+
console.log(chalk7.green(`
|
|
851
|
+
\u2714 Secret "${chalk7.bold(key)}" was successfully deleted from vault.
|
|
852
|
+
`));
|
|
853
|
+
} else {
|
|
854
|
+
console.log(chalk7.yellow(`
|
|
855
|
+
\u2716 Secret "${key}" was not found in vault.
|
|
856
|
+
`));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function secretAuditCommand() {
|
|
860
|
+
const config = ConfigLoader.load();
|
|
861
|
+
const report = await VaultAuditor.audit(config);
|
|
862
|
+
console.log(chalk7.bold.cyan("\n\u{1F6E1}\uFE0F ContextWise Secret Security Audit\n"));
|
|
863
|
+
console.log(`Upstream Servers Inspected: ${chalk7.bold(report.totalUpstreams)}`);
|
|
864
|
+
console.log(`Vault References in Config: ${chalk7.bold(report.vaultReferencedCount)}`);
|
|
865
|
+
console.log(`Active Vault Driver: ${chalk7.bold(report.activeDriver)}`);
|
|
866
|
+
console.log(`Redacted Secrets in Memory: ${chalk7.bold(report.registeredSecretCount)}
|
|
867
|
+
`);
|
|
868
|
+
if (report.plaintextWarnings.length === 0) {
|
|
869
|
+
console.log(
|
|
870
|
+
chalk7.green(
|
|
871
|
+
"\u2714 Vault Audit Passed: No exposed plaintext credentials detected in configuration.\n"
|
|
872
|
+
)
|
|
873
|
+
);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
console.log(
|
|
877
|
+
chalk7.red(
|
|
878
|
+
`\u26A0 Found ${report.plaintextWarnings.length} potential credential risk(s) in configuration:
|
|
879
|
+
`
|
|
880
|
+
)
|
|
881
|
+
);
|
|
882
|
+
for (const warning of report.plaintextWarnings) {
|
|
883
|
+
const badge = warning.severity === "critical" ? chalk7.bgRed.bold(" CRITICAL ") : chalk7.bgYellow.black.bold(" WARNING ");
|
|
884
|
+
console.log(`${badge} [${warning.serverName}] ${chalk7.bold(warning.envVar)}`);
|
|
885
|
+
console.log(` ${chalk7.gray("Issue:")} ${warning.reason}`);
|
|
886
|
+
console.log(` ${chalk7.cyan("Action:")} ${warning.suggestion}
|
|
887
|
+
`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/cli/commands/start.ts
|
|
892
|
+
async function startCommand(options) {
|
|
893
|
+
try {
|
|
894
|
+
const config = ConfigLoader.load({
|
|
895
|
+
configPath: options.config
|
|
896
|
+
});
|
|
897
|
+
if (options.logLevel) {
|
|
898
|
+
config.proxy.logLevel = options.logLevel;
|
|
899
|
+
}
|
|
900
|
+
if (options.passthrough) {
|
|
901
|
+
config.routing.strategy = "passthrough";
|
|
902
|
+
}
|
|
903
|
+
const proxy = new ContextWiseProxy();
|
|
904
|
+
let isShuttingDown = false;
|
|
905
|
+
const cleanup = async () => {
|
|
906
|
+
if (isShuttingDown) return;
|
|
907
|
+
isShuttingDown = true;
|
|
908
|
+
setTimeout(() => process.exit(0), 3e3).unref();
|
|
909
|
+
try {
|
|
910
|
+
await proxy.stop();
|
|
911
|
+
} catch (err) {
|
|
912
|
+
logger.error(`Error stopping ContextWise proxy: ${err}`);
|
|
913
|
+
} finally {
|
|
914
|
+
process.exit(0);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
process.on("SIGINT", cleanup);
|
|
918
|
+
process.on("SIGTERM", cleanup);
|
|
919
|
+
await proxy.start(config);
|
|
920
|
+
process.stdin.resume();
|
|
921
|
+
} catch (err) {
|
|
922
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
923
|
+
logger.error(`ContextWise proxy failed to start: ${msg}`);
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/cli/commands/stats.ts
|
|
929
|
+
import chalk8 from "chalk";
|
|
930
|
+
function col(text, width, colorFn) {
|
|
931
|
+
const truncated = text.length > width ? text.slice(0, width) : text;
|
|
932
|
+
const padded = truncated.padEnd(width);
|
|
933
|
+
return colorFn ? colorFn(padded) : padded;
|
|
934
|
+
}
|
|
935
|
+
function makeSeparator(cLeft, cMid, cRight, widths) {
|
|
936
|
+
const parts = widths.map((w) => "\u2500".repeat(w + 2));
|
|
937
|
+
return ` ${cLeft}${parts.join(cMid)}${cRight}`;
|
|
938
|
+
}
|
|
939
|
+
function makeHeader(title, widths) {
|
|
940
|
+
const totalWidth = widths.reduce((acc, w) => acc + w + 2, 0) + widths.length - 1;
|
|
941
|
+
const banner = ` \u250C\u2500\u2500 ${title} `;
|
|
942
|
+
const remaining = Math.max(0, totalWidth + 3 - banner.length);
|
|
943
|
+
return `${banner}${"\u2500".repeat(remaining)}\u2510`;
|
|
944
|
+
}
|
|
945
|
+
async function statsCommand(options = {}) {
|
|
946
|
+
if (options.reset) {
|
|
947
|
+
metricsCollector.reset();
|
|
948
|
+
console.log(
|
|
949
|
+
chalk8.green.bold("\n\u2714 All ContextWise lifetime metrics and token analytics have been reset to zero.\n")
|
|
950
|
+
);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
metricsCollector.reload();
|
|
954
|
+
const summary = metricsCollector.getSummary();
|
|
955
|
+
if (options.json) {
|
|
956
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const doubleDivider = chalk8.bold.cyan("\u2550".repeat(74));
|
|
960
|
+
console.log(`
|
|
961
|
+
${doubleDivider}`);
|
|
962
|
+
console.log(
|
|
963
|
+
chalk8.bold.cyan(" \u{1F4B0} ContextWise Performance, Token & Cost Analytics")
|
|
964
|
+
);
|
|
965
|
+
console.log(`${doubleDivider}
|
|
966
|
+
`);
|
|
967
|
+
const baselineSavings = summary.dollarSavings.claudeSonnet.toFixed(2);
|
|
968
|
+
const totalTokens = summary.estimatedTotalTokensSaved.toLocaleString();
|
|
969
|
+
if (summary.estimatedTotalTokensSaved > 0) {
|
|
970
|
+
console.log(
|
|
971
|
+
chalk8.bgGreen.black.bold(` SAVINGS `) + chalk8.green.bold(
|
|
972
|
+
` ContextWise has saved you ~$${baselineSavings} in LLM API costs!`
|
|
973
|
+
)
|
|
974
|
+
);
|
|
975
|
+
console.log(
|
|
976
|
+
chalk8.dim(` ${totalTokens} total tokens avoided across all sessions
|
|
977
|
+
`)
|
|
978
|
+
);
|
|
979
|
+
} else {
|
|
980
|
+
console.log(
|
|
981
|
+
chalk8.bgBlue.black.bold(` STANDBY `) + chalk8.cyan.bold(
|
|
982
|
+
` Ready to route! Connect Claude Code, Cursor, or Antigravity to track savings.`
|
|
983
|
+
)
|
|
984
|
+
);
|
|
985
|
+
console.log(
|
|
986
|
+
chalk8.dim(` 0 tokens saved so far (start a session to begin tracking)
|
|
987
|
+
`)
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
const modelCols = [30, 18, 16];
|
|
991
|
+
console.log(chalk8.bold.yellow(makeHeader("\u{1F4A1} ESTIMATED SAVINGS BY MODEL", modelCols)));
|
|
992
|
+
console.log(
|
|
993
|
+
` \u2502 ${col("Model Family", 30, chalk8.bold)} \u2502 ${col("Benchmark Rate", 18, chalk8.bold)} \u2502 ${col("Dollar Saved", 16, chalk8.bold)} \u2502`
|
|
994
|
+
);
|
|
995
|
+
console.log(makeSeparator("\u251C", "\u253C", "\u2524", modelCols));
|
|
996
|
+
console.log(
|
|
997
|
+
` \u2502 ${col("Claude 3.5 / 3.7 Sonnet (def)", 30, chalk8.cyan)} \u2502 ${col(`$${LLM_PRICING.CLAUDE_SONNET.toFixed(2)} / 1M`, 18)} \u2502 ${col(`$${summary.dollarSavings.claudeSonnet.toFixed(2)}`, 16, chalk8.green.bold)} \u2502`
|
|
998
|
+
);
|
|
999
|
+
console.log(
|
|
1000
|
+
` \u2502 ${col("OpenAI GPT-4o", 30)} \u2502 ${col(`$${LLM_PRICING.GPT_4O.toFixed(2)} / 1M`, 18)} \u2502 ${col(`$${summary.dollarSavings.gpt4o.toFixed(2)}`, 16, chalk8.green)} \u2502`
|
|
1001
|
+
);
|
|
1002
|
+
console.log(
|
|
1003
|
+
` \u2502 ${col("Claude 3 Opus", 30)} \u2502 ${col(`$${LLM_PRICING.CLAUDE_OPUS.toFixed(2)} / 1M`, 18)} \u2502 ${col(`$${summary.dollarSavings.claudeOpus.toFixed(2)}`, 16, chalk8.green)} \u2502`
|
|
1004
|
+
);
|
|
1005
|
+
console.log(
|
|
1006
|
+
` \u2502 ${col("Haiku / GPT-4o mini", 30)} \u2502 ${col(`$${LLM_PRICING.HAIKU_OR_MINI.toFixed(2)} / 1M`, 18)} \u2502 ${col(`$${summary.dollarSavings.haikuOrMini.toFixed(2)}`, 16, chalk8.green)} \u2502`
|
|
1007
|
+
);
|
|
1008
|
+
console.log(makeSeparator("\u2514", "\u2534", "\u2518", modelCols) + "\n");
|
|
1009
|
+
const tokenCols = [24, 18, 24];
|
|
1010
|
+
console.log(chalk8.bold.yellow(makeHeader("\u{1F4CA} TOKEN REDUCTION BREAKDOWN", tokenCols)));
|
|
1011
|
+
console.log(
|
|
1012
|
+
` \u2502 ${col("Source", 24, chalk8.bold)} \u2502 ${col("Tokens Saved", 18, chalk8.bold)} \u2502 ${col("Details", 24, chalk8.bold)} \u2502`
|
|
1013
|
+
);
|
|
1014
|
+
console.log(makeSeparator("\u251C", "\u253C", "\u2524", tokenCols));
|
|
1015
|
+
console.log(
|
|
1016
|
+
` \u2502 ${col("Schema Pruning", 24)} \u2502 ${col(summary.schemaPruningTokensSaved.toLocaleString(), 18, chalk8.green)} \u2502 ${col(`${Math.max(0, summary.totalCatalogToolsCount - summary.exposedToolsCount)} tools hidden/turn`, 24, chalk8.dim)} \u2502`
|
|
1017
|
+
);
|
|
1018
|
+
console.log(
|
|
1019
|
+
` \u2502 ${col("Response Cache", 24)} \u2502 ${col(summary.cacheTokensSaved.toLocaleString(), 18, chalk8.green)} \u2502 ${col(`${summary.cachedCalls} read calls cached`, 24, chalk8.dim)} \u2502`
|
|
1020
|
+
);
|
|
1021
|
+
console.log(
|
|
1022
|
+
` \u2502 ${col("Loop Prevention", 24)} \u2502 ${col(summary.loopPreventionTokensSaved.toLocaleString(), 18, chalk8.green)} \u2502 ${col(`${summary.loopsPrevented} runaway loops stopped`, 24, chalk8.dim)} \u2502`
|
|
1023
|
+
);
|
|
1024
|
+
console.log(makeSeparator("\u2514", "\u2534", "\u2518", tokenCols) + "\n");
|
|
1025
|
+
const trackingSince = summary.firstRecordedAt > 0 ? new Date(summary.firstRecordedAt).toLocaleDateString(void 0, {
|
|
1026
|
+
month: "short",
|
|
1027
|
+
day: "numeric",
|
|
1028
|
+
year: "numeric"
|
|
1029
|
+
}) : "Session start";
|
|
1030
|
+
const perfCols = [26, 43];
|
|
1031
|
+
console.log(chalk8.bold.yellow(makeHeader("\u26A1 PROXY PERFORMANCE & RELIABILITY", perfCols)));
|
|
1032
|
+
console.log(` \u2502 ${col("Total Proxied Calls:", 26)} \u2502 ${col(String(summary.totalCalls), 43, chalk8.bold)} \u2502`);
|
|
1033
|
+
console.log(
|
|
1034
|
+
` \u2502 ${col("Cache Hit Efficiency:", 26)} \u2502 ${col(`${summary.cacheHitRatePct}% (${summary.cachedCalls} cached / ${summary.totalCalls} calls)`, 43, chalk8.bold)} \u2502`
|
|
1035
|
+
);
|
|
1036
|
+
console.log(` \u2502 ${col("Avg Execution Latency:", 26)} \u2502 ${col(`${summary.averageLatencyMs} ms`, 43, chalk8.bold)} \u2502`);
|
|
1037
|
+
console.log(
|
|
1038
|
+
` \u2502 ${col("Tripped Loops / Errors:", 26)} \u2502 ${col(
|
|
1039
|
+
summary.loopsPrevented > 0 || summary.errorCalls > 0 ? `${summary.loopsPrevented} loops / ${summary.errorCalls} errors` : "0 loops / 0 errors",
|
|
1040
|
+
43,
|
|
1041
|
+
summary.loopsPrevented > 0 || summary.errorCalls > 0 ? chalk8.red : chalk8.green
|
|
1042
|
+
)} \u2502`
|
|
1043
|
+
);
|
|
1044
|
+
console.log(` \u2502 ${col("Metrics Tracking Since:", 26)} \u2502 ${col(trackingSince, 43, chalk8.dim)} \u2502`);
|
|
1045
|
+
console.log(makeSeparator("\u2514", "\u2534", "\u2518", perfCols) + "\n");
|
|
1046
|
+
console.log(
|
|
1047
|
+
chalk8.dim(
|
|
1048
|
+
' Tip: Run "contextwise stats --reset" to clear counters or "--json" for raw exports.\n'
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/cli/index.ts
|
|
1054
|
+
function createCli() {
|
|
1055
|
+
const program = new Command();
|
|
1056
|
+
program.name("contextwise").description(
|
|
1057
|
+
"ContextWise: Dynamic MCP Tool Routing, Schema Compression & Execution Gateway"
|
|
1058
|
+
).version("0.1.0");
|
|
1059
|
+
program.command("start").description("Start the ContextWise MCP proxy gateway").option("-c, --config <path>", "Path to contextwise.json config file").option(
|
|
1060
|
+
"-l, --log-level <level>",
|
|
1061
|
+
"Logging level (debug, info, warn, error, silent)",
|
|
1062
|
+
"info"
|
|
1063
|
+
).option(
|
|
1064
|
+
"--passthrough",
|
|
1065
|
+
"Run in raw passthrough mode without dynamic tool routing"
|
|
1066
|
+
).action(startCommand);
|
|
1067
|
+
program.command("init").description("Initialize contextwise.json by scanning existing MCP configurations").option("-f, --force", "Overwrite existing configuration file").action(initCommand);
|
|
1068
|
+
program.command("browse [category]").description("Browse MCP servers across Official Registry, Smithery, and Curated Presets").option("-q, --query <query>", "Filter by search keyword").option("-s, --source <source>", "Filter by registry source (all, official, smithery, curated)", "all").option("--offline", "Only show local curated presets without querying remote registries").option("-i, --interactive", "Launch interactive keyboard-navigable terminal UI").action(
|
|
1069
|
+
(category, opts) => browseCommand({
|
|
1070
|
+
category,
|
|
1071
|
+
query: opts.query,
|
|
1072
|
+
source: opts.source,
|
|
1073
|
+
offline: opts.offline,
|
|
1074
|
+
interactive: opts.interactive
|
|
1075
|
+
})
|
|
1076
|
+
);
|
|
1077
|
+
program.command("add <name> [extraArg]").description(
|
|
1078
|
+
"Add and configure an upstream MCP server in contextwise.json (e.g. contextwise add postgres postgresql://...)"
|
|
1079
|
+
).option("-s, --smithery [package]", "Install and run via Smithery CLI (@smithery/cli)").option("-c, --command <cmd>", "Executable command for local stdio server").option("-a, --args <args...>", "Command-line arguments").option("-e, --env <env...>", "Environment variables in KEY=VALUE format").option("-u, --url <url>", "Remote SSE endpoint URL").option("--no-test", "Skip testing connection before saving").action(addCommand);
|
|
1080
|
+
program.command("list").description("Inspect configured upstreams and list all aggregated tools").option("-c, --config <path>", "Path to contextwise.json config file").action(listCommand);
|
|
1081
|
+
const secret = program.command("secret").description("Manage encrypted secrets, credentials, and run security audits");
|
|
1082
|
+
secret.command("set <key> [value]").description("Store an encrypted secret in the vault (prompts securely if value omitted)").option("-s, --scope <scope>", "Secret scope (personal, workspace, team)", "personal").action((key, value, opts) => secretSetCommand(key, value, { scope: opts.scope }));
|
|
1083
|
+
secret.command("get <key>").description("Retrieve secret metadata and preview value").option("--reveal", "Print full unmasked secret value in plaintext").action((key, opts) => secretGetCommand(key, { reveal: opts.reveal }));
|
|
1084
|
+
secret.command("list").description("List all secrets stored in the ContextWise vault").action(secretListCommand);
|
|
1085
|
+
secret.command("delete <key>").alias("rm").description("Delete a secret from the vault").action(secretDeleteCommand);
|
|
1086
|
+
secret.command("audit").description("Scan configuration for exposed plaintext API keys and credentials").action(secretAuditCommand);
|
|
1087
|
+
program.command("login [apiKey]").description("Log in to ContextWise Cloud for end-to-end encrypted synchronization").action(loginCommand);
|
|
1088
|
+
program.command("logout").description("Log out of ContextWise Cloud on this device").action(logoutCommand);
|
|
1089
|
+
program.command("whoami").description("Display currently authenticated ContextWise Cloud account and workspaces").action(whoamiCommand);
|
|
1090
|
+
program.command("push [workspaceId]").description("Push local workspace configuration and encrypted secrets to ContextWise Cloud").action(pushCommand);
|
|
1091
|
+
program.command("pull [workspaceId]").description("Pull latest workspace configuration and secrets from ContextWise Cloud").action(pullCommand);
|
|
1092
|
+
program.command("sync").description("Display status of ContextWise Cloud synchronization").action(syncStatusCommand);
|
|
1093
|
+
program.command("upgrade").description("Upgrade your subscription to ContextWise Pro or Team via Stripe").option("--team", "Upgrade to Team subscription").option("--annual", "Select annual billing cycle for discounted pricing").action((opts) => upgradeCommand({ team: opts.team, annual: opts.annual }));
|
|
1094
|
+
program.command("billing").description("Manage your Stripe subscription, invoices, and payment methods in Customer Portal").action(billingCommand);
|
|
1095
|
+
program.command("stats").description("Display metrics on token savings, cost ROI, and cache efficiency").option("--reset", "Reset all recorded lifetime metrics").option("--json", "Output stats in raw JSON format").action((opts) => statsCommand({ reset: opts.reset, json: opts.json }));
|
|
1096
|
+
return program;
|
|
1097
|
+
}
|
|
1098
|
+
async function runCli() {
|
|
1099
|
+
const program = createCli();
|
|
1100
|
+
await program.parseAsync(process.argv);
|
|
1101
|
+
}
|
|
1102
|
+
if (process.argv[1]) {
|
|
1103
|
+
try {
|
|
1104
|
+
const executed = resolve2(process.argv[1]);
|
|
1105
|
+
const current = fileURLToPath(import.meta.url);
|
|
1106
|
+
if (executed === current) {
|
|
1107
|
+
runCli();
|
|
1108
|
+
}
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
export {
|
|
1113
|
+
createCli,
|
|
1114
|
+
runCli
|
|
1115
|
+
};
|
|
1116
|
+
//# sourceMappingURL=cli.js.map
|