@townco/cli 0.1.85 → 0.1.88
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/dist/commands/deploy.js +2 -25
- package/package.json +9 -9
- package/dist/commands/delete.d.ts +0 -1
- package/dist/commands/delete.js +0 -60
- package/dist/commands/edit.d.ts +0 -1
- package/dist/commands/edit.js +0 -92
- package/dist/commands/list.d.ts +0 -1
- package/dist/commands/list.js +0 -55
- package/dist/commands/mcp-add.d.ts +0 -14
- package/dist/commands/mcp-add.js +0 -494
- package/dist/commands/mcp-list.d.ts +0 -3
- package/dist/commands/mcp-list.js +0 -63
- package/dist/commands/mcp-remove.d.ts +0 -3
- package/dist/commands/mcp-remove.js +0 -120
- package/dist/commands/tool-add.d.ts +0 -6
- package/dist/commands/tool-add.js +0 -349
- package/dist/commands/tool-list.d.ts +0 -3
- package/dist/commands/tool-list.js +0 -61
- package/dist/commands/tool-register.d.ts +0 -7
- package/dist/commands/tool-register.js +0 -291
- package/dist/commands/tool-remove.d.ts +0 -3
- package/dist/commands/tool-remove.js +0 -202
- package/dist/components/MergedLogsPane.d.ts +0 -11
- package/dist/components/MergedLogsPane.js +0 -205
- package/dist/lib/auth-storage.d.ts +0 -38
- package/dist/lib/auth-storage.js +0 -89
- package/dist/lib/mcp-storage.d.ts +0 -32
- package/dist/lib/mcp-storage.js +0 -111
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import TextInput from "ink-text-input";
|
|
4
|
-
import { useEffect, useMemo, useState } from "react";
|
|
5
|
-
const SERVICE_COLORS = ["cyan", "magenta", "blue", "green", "yellow"];
|
|
6
|
-
const LOG_LEVELS = {
|
|
7
|
-
trace: 0,
|
|
8
|
-
debug: 1,
|
|
9
|
-
info: 2,
|
|
10
|
-
warn: 3,
|
|
11
|
-
error: 4,
|
|
12
|
-
fatal: 5,
|
|
13
|
-
};
|
|
14
|
-
// Parse log level from a line by looking for [LEVEL] markers
|
|
15
|
-
function parseLogLevel(line) {
|
|
16
|
-
const match = line.match(/\[(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\]/i);
|
|
17
|
-
if (match?.[1]) {
|
|
18
|
-
return match[1].toLowerCase();
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
// Parse timestamp from a JSON log line
|
|
23
|
-
function parseTimestamp(line) {
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(line);
|
|
26
|
-
if (parsed.timestamp && typeof parsed.timestamp === "string") {
|
|
27
|
-
return parsed.timestamp;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
// Not JSON, use current time
|
|
32
|
-
}
|
|
33
|
-
return new Date().toISOString();
|
|
34
|
-
}
|
|
35
|
-
// Fuzzy search: checks if query characters appear in order in the target string
|
|
36
|
-
function fuzzyMatch(query, target) {
|
|
37
|
-
if (!query)
|
|
38
|
-
return true;
|
|
39
|
-
const lowerQuery = query.toLowerCase();
|
|
40
|
-
const lowerTarget = target.toLowerCase();
|
|
41
|
-
let queryIndex = 0;
|
|
42
|
-
for (let i = 0; i < lowerTarget.length && queryIndex < lowerQuery.length; i++) {
|
|
43
|
-
if (lowerTarget[i] === lowerQuery[queryIndex]) {
|
|
44
|
-
queryIndex++;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return queryIndex === lowerQuery.length;
|
|
48
|
-
}
|
|
49
|
-
export function MergedLogsPane({ services, onClear }) {
|
|
50
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
51
|
-
const [searchMode, setSearchMode] = useState(false);
|
|
52
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
53
|
-
const [serviceFilter, setServiceFilter] = useState(null);
|
|
54
|
-
const [levelFilter, setLevelFilter] = useState(null);
|
|
55
|
-
// Only include services that actually have output
|
|
56
|
-
const availableServices = useMemo(() => services.filter((s) => s.output.length > 0).map((s) => s.service), [services]);
|
|
57
|
-
// Clear service filter if the selected service is no longer available
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (serviceFilter && !availableServices.includes(serviceFilter)) {
|
|
60
|
-
setServiceFilter(null);
|
|
61
|
-
}
|
|
62
|
-
}, [availableServices, serviceFilter]);
|
|
63
|
-
// Merge all outputs into a single array with service tags and parsed levels
|
|
64
|
-
// Sort by timestamp to show chronological order
|
|
65
|
-
const mergedLines = useMemo(() => {
|
|
66
|
-
const lines = [];
|
|
67
|
-
for (const [serviceIndex, service] of services.entries()) {
|
|
68
|
-
for (const [index, line] of service.output.entries()) {
|
|
69
|
-
lines.push({
|
|
70
|
-
service: service.service,
|
|
71
|
-
line,
|
|
72
|
-
index,
|
|
73
|
-
serviceIndex,
|
|
74
|
-
level: parseLogLevel(line),
|
|
75
|
-
timestamp: parseTimestamp(line),
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Sort by timestamp chronologically
|
|
80
|
-
return lines.sort((a, b) => {
|
|
81
|
-
return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
|
|
82
|
-
});
|
|
83
|
-
}, [services]);
|
|
84
|
-
// Handle keyboard input
|
|
85
|
-
useInput((input, key) => {
|
|
86
|
-
// '/' to enter search mode
|
|
87
|
-
if (input === "/" && !searchMode) {
|
|
88
|
-
setSearchMode(true);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
// Escape to exit search mode or jump to bottom
|
|
92
|
-
if (key.escape) {
|
|
93
|
-
if (searchMode) {
|
|
94
|
-
setSearchMode(false);
|
|
95
|
-
setSearchQuery("");
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
setScrollOffset(0);
|
|
99
|
-
}
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
// Don't handle other inputs in search mode
|
|
103
|
-
if (searchMode) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
// 's' to cycle through service filters
|
|
107
|
-
if (input === "s") {
|
|
108
|
-
if (serviceFilter === null) {
|
|
109
|
-
setServiceFilter(availableServices[0] ?? null);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
const currentIndex = availableServices.indexOf(serviceFilter);
|
|
113
|
-
const nextIndex = (currentIndex + 1) % (availableServices.length + 1);
|
|
114
|
-
setServiceFilter(nextIndex === availableServices.length
|
|
115
|
-
? null
|
|
116
|
-
: (availableServices[nextIndex] ?? null));
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
// 'l' to cycle through level filters
|
|
121
|
-
if (input === "l") {
|
|
122
|
-
const levels = [
|
|
123
|
-
null,
|
|
124
|
-
"debug",
|
|
125
|
-
"info",
|
|
126
|
-
"warn",
|
|
127
|
-
"error",
|
|
128
|
-
"fatal",
|
|
129
|
-
];
|
|
130
|
-
const currentIndex = levelFilter === null ? 0 : levels.indexOf(levelFilter);
|
|
131
|
-
const nextIndex = (currentIndex + 1) % levels.length;
|
|
132
|
-
const nextLevel = levels[nextIndex];
|
|
133
|
-
setLevelFilter(nextLevel ?? null);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
// 'c' to clear all outputs
|
|
137
|
-
if (input === "c") {
|
|
138
|
-
if (onClear) {
|
|
139
|
-
// Clear all services
|
|
140
|
-
for (const [index] of services.entries()) {
|
|
141
|
-
onClear(index);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
setScrollOffset(0);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
// Apply service filter
|
|
149
|
-
const serviceFilteredLines = useMemo(() => {
|
|
150
|
-
if (!serviceFilter)
|
|
151
|
-
return mergedLines;
|
|
152
|
-
return mergedLines.filter((log) => log.service === serviceFilter);
|
|
153
|
-
}, [mergedLines, serviceFilter]);
|
|
154
|
-
// Apply level filter
|
|
155
|
-
const levelFilteredLines = useMemo(() => {
|
|
156
|
-
if (!levelFilter)
|
|
157
|
-
return serviceFilteredLines;
|
|
158
|
-
return serviceFilteredLines.filter((log) => {
|
|
159
|
-
if (!log.level)
|
|
160
|
-
return true; // Show lines without level info
|
|
161
|
-
const logLevelIndex = LOG_LEVELS[log.level];
|
|
162
|
-
const filterLevelIndex = LOG_LEVELS[levelFilter];
|
|
163
|
-
return logLevelIndex >= filterLevelIndex;
|
|
164
|
-
});
|
|
165
|
-
}, [serviceFilteredLines, levelFilter]);
|
|
166
|
-
// Apply fuzzy search
|
|
167
|
-
const searchedLines = useMemo(() => {
|
|
168
|
-
if (!searchQuery)
|
|
169
|
-
return levelFilteredLines;
|
|
170
|
-
return levelFilteredLines.filter((log) => fuzzyMatch(searchQuery, log.line));
|
|
171
|
-
}, [levelFilteredLines, searchQuery]);
|
|
172
|
-
const isAtBottom = scrollOffset === 0;
|
|
173
|
-
// Show all lines - no truncation
|
|
174
|
-
const displayLines = useMemo(() => {
|
|
175
|
-
if (isAtBottom) {
|
|
176
|
-
return searchedLines; // Show all logs when at bottom
|
|
177
|
-
}
|
|
178
|
-
return searchedLines.slice(0, searchedLines.length - scrollOffset);
|
|
179
|
-
}, [searchedLines, scrollOffset, isAtBottom]);
|
|
180
|
-
// Get overall status (error if any error, stopped if all stopped, etc.)
|
|
181
|
-
const overallStatus = useMemo(() => {
|
|
182
|
-
if (services.some((s) => s.status === "error"))
|
|
183
|
-
return "error";
|
|
184
|
-
if (services.every((s) => s.status === "stopped"))
|
|
185
|
-
return "stopped";
|
|
186
|
-
if (services.some((s) => s.status === "running"))
|
|
187
|
-
return "running";
|
|
188
|
-
return "starting";
|
|
189
|
-
}, [services]);
|
|
190
|
-
const statusColor = overallStatus === "running"
|
|
191
|
-
? "green"
|
|
192
|
-
: overallStatus === "error"
|
|
193
|
-
? "red"
|
|
194
|
-
: overallStatus === "starting"
|
|
195
|
-
? "yellow"
|
|
196
|
-
: "gray";
|
|
197
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, marginBottom: 1, flexShrink: 0, children: [_jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })] })), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, paddingX: 1, children: displayLines.length === 0 ? (_jsx(Text, { dimColor: true, children: searchQuery || serviceFilter || levelFilter
|
|
198
|
-
? "No logs match the current filters. Press 'c' to clear, 's' for service, or 'l' for level."
|
|
199
|
-
: "Waiting for output..." })) : (displayLines.map((logLine, idx) => {
|
|
200
|
-
const serviceColor = SERVICE_COLORS[availableServices.indexOf(logLine.service) %
|
|
201
|
-
SERVICE_COLORS.length] || "white";
|
|
202
|
-
const keyStr = `${logLine.service}-${logLine.serviceIndex}-${logLine.index}-${idx}`;
|
|
203
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: serviceColor, children: ["[", logLine.service, "]"] }), _jsxs(Text, { children: [" ", logLine.line] })] }, keyStr));
|
|
204
|
-
})) }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: statusColor, children: "\u25CF" }), _jsxs(Text, { children: [" ", overallStatus] }), serviceFilter && _jsxs(Text, { children: [" | [", serviceFilter, "]"] }), levelFilter && (_jsxs(Text, { children: [" ", "| [", ">=", levelFilter, "]"] })), searchQuery && _jsxs(Text, { color: "cyan", children: [" | [SEARCH: ", searchQuery, "]"] }), !isAtBottom && _jsx(Text, { color: "yellow", children: " | [SCROLLED]" }), _jsxs(Text, { dimColor: true, children: [" ", "| ", displayLines.length, "/", searchedLines.length, " lines"] }), scrollOffset > 0 && (_jsxs(Text, { dimColor: true, children: [" (", scrollOffset, " from bottom)"] }))] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "(/)search | (s)ervice | (l)evel | (c)lear | ESC" }) })] })] }));
|
|
205
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface AuthCredentials {
|
|
2
|
-
access_token: string;
|
|
3
|
-
refresh_token: string;
|
|
4
|
-
expires_at: number;
|
|
5
|
-
user: {
|
|
6
|
-
id: string;
|
|
7
|
-
email: string;
|
|
8
|
-
};
|
|
9
|
-
shed_url: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Get the shed URL from environment or default
|
|
13
|
-
*/
|
|
14
|
-
export declare function getShedUrl(): string;
|
|
15
|
-
/**
|
|
16
|
-
* Save auth credentials to disk
|
|
17
|
-
*/
|
|
18
|
-
export declare function saveAuthCredentials(credentials: AuthCredentials): void;
|
|
19
|
-
/**
|
|
20
|
-
* Load auth credentials from disk
|
|
21
|
-
*/
|
|
22
|
-
export declare function loadAuthCredentials(): AuthCredentials | null;
|
|
23
|
-
/**
|
|
24
|
-
* Delete auth credentials (logout)
|
|
25
|
-
*/
|
|
26
|
-
export declare function clearAuthCredentials(): boolean;
|
|
27
|
-
/**
|
|
28
|
-
* Check if user is logged in (has valid credentials file)
|
|
29
|
-
*/
|
|
30
|
-
export declare function isLoggedIn(): boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Check if access token is expired or about to expire (within 5 minutes)
|
|
33
|
-
*/
|
|
34
|
-
export declare function isTokenExpired(credentials: AuthCredentials): boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Get the auth file path (for display purposes)
|
|
37
|
-
*/
|
|
38
|
-
export declare function getAuthFilePath(): string;
|
package/dist/lib/auth-storage.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
// ============================================================================
|
|
5
|
-
// Constants
|
|
6
|
-
// ============================================================================
|
|
7
|
-
const TOWN_CONFIG_DIR = join(homedir(), ".config", "town");
|
|
8
|
-
const AUTH_FILE = join(TOWN_CONFIG_DIR, "auth.json");
|
|
9
|
-
const DEFAULT_SHED_URL = "http://localhost:3000";
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Helper Functions
|
|
12
|
-
// ============================================================================
|
|
13
|
-
function ensureConfigDir() {
|
|
14
|
-
if (!existsSync(TOWN_CONFIG_DIR)) {
|
|
15
|
-
mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
// ============================================================================
|
|
19
|
-
// Public API
|
|
20
|
-
// ============================================================================
|
|
21
|
-
/**
|
|
22
|
-
* Get the shed URL from environment or default
|
|
23
|
-
*/
|
|
24
|
-
export function getShedUrl() {
|
|
25
|
-
return process.env.TOWN_SHED_URL || DEFAULT_SHED_URL;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Save auth credentials to disk
|
|
29
|
-
*/
|
|
30
|
-
export function saveAuthCredentials(credentials) {
|
|
31
|
-
ensureConfigDir();
|
|
32
|
-
try {
|
|
33
|
-
const content = JSON.stringify(credentials, null, 2);
|
|
34
|
-
writeFileSync(AUTH_FILE, content, { mode: 0o600 });
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
throw new Error(`Failed to save auth credentials: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Load auth credentials from disk
|
|
42
|
-
*/
|
|
43
|
-
export function loadAuthCredentials() {
|
|
44
|
-
if (!existsSync(AUTH_FILE)) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
const content = readFileSync(AUTH_FILE, "utf-8");
|
|
49
|
-
return JSON.parse(content);
|
|
50
|
-
}
|
|
51
|
-
catch (_error) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Delete auth credentials (logout)
|
|
57
|
-
*/
|
|
58
|
-
export function clearAuthCredentials() {
|
|
59
|
-
if (!existsSync(AUTH_FILE)) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
try {
|
|
63
|
-
unlinkSync(AUTH_FILE);
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
throw new Error(`Failed to clear auth credentials: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Check if user is logged in (has valid credentials file)
|
|
72
|
-
*/
|
|
73
|
-
export function isLoggedIn() {
|
|
74
|
-
return loadAuthCredentials() !== null;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Check if access token is expired or about to expire (within 5 minutes)
|
|
78
|
-
*/
|
|
79
|
-
export function isTokenExpired(credentials) {
|
|
80
|
-
const now = Math.floor(Date.now() / 1000);
|
|
81
|
-
const buffer = 5 * 60; // 5 minutes buffer
|
|
82
|
-
return credentials.expires_at <= now + buffer;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Get the auth file path (for display purposes)
|
|
86
|
-
*/
|
|
87
|
-
export function getAuthFilePath() {
|
|
88
|
-
return AUTH_FILE;
|
|
89
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export type MCPConfig = {
|
|
2
|
-
name: string;
|
|
3
|
-
transport: "stdio" | "http";
|
|
4
|
-
command?: string;
|
|
5
|
-
args?: string[];
|
|
6
|
-
url?: string;
|
|
7
|
-
headers?: Record<string, string>;
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* Save an MCP config to the store
|
|
11
|
-
*/
|
|
12
|
-
export declare function saveMCPConfig(config: MCPConfig): void;
|
|
13
|
-
/**
|
|
14
|
-
* Load an MCP config by name
|
|
15
|
-
*/
|
|
16
|
-
export declare function loadMCPConfig(name: string): MCPConfig | null;
|
|
17
|
-
/**
|
|
18
|
-
* Delete an MCP config by name
|
|
19
|
-
*/
|
|
20
|
-
export declare function deleteMCPConfig(name: string): boolean;
|
|
21
|
-
/**
|
|
22
|
-
* List all MCP configs
|
|
23
|
-
*/
|
|
24
|
-
export declare function listMCPConfigs(): MCPConfig[];
|
|
25
|
-
/**
|
|
26
|
-
* Check if an MCP config exists
|
|
27
|
-
*/
|
|
28
|
-
export declare function mcpConfigExists(name: string): boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Get a summary of an MCP config for display
|
|
31
|
-
*/
|
|
32
|
-
export declare function getMCPSummary(config: MCPConfig): string;
|
package/dist/lib/mcp-storage.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
// ============================================================================
|
|
5
|
-
// Constants
|
|
6
|
-
// ============================================================================
|
|
7
|
-
const TOWN_CONFIG_DIR = join(homedir(), ".config", "town");
|
|
8
|
-
const MCPS_FILE = join(TOWN_CONFIG_DIR, "mcps.json");
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// Helper Functions
|
|
11
|
-
// ============================================================================
|
|
12
|
-
/**
|
|
13
|
-
* Ensure the config directory exists
|
|
14
|
-
*/
|
|
15
|
-
function ensureConfigDir() {
|
|
16
|
-
if (!existsSync(TOWN_CONFIG_DIR)) {
|
|
17
|
-
mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Load all MCP configs from the JSON file
|
|
22
|
-
*/
|
|
23
|
-
function loadStore() {
|
|
24
|
-
ensureConfigDir();
|
|
25
|
-
if (!existsSync(MCPS_FILE)) {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const content = readFileSync(MCPS_FILE, "utf-8");
|
|
30
|
-
return JSON.parse(content);
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
throw new Error(`Failed to load MCP configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Save all MCP configs to the JSON file
|
|
38
|
-
*/
|
|
39
|
-
function saveStore(store) {
|
|
40
|
-
ensureConfigDir();
|
|
41
|
-
try {
|
|
42
|
-
const content = JSON.stringify(store, null, 2);
|
|
43
|
-
writeFileSync(MCPS_FILE, content, "utf-8");
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
throw new Error(`Failed to save MCP configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// ============================================================================
|
|
50
|
-
// Public API
|
|
51
|
-
// ============================================================================
|
|
52
|
-
/**
|
|
53
|
-
* Save an MCP config to the store
|
|
54
|
-
*/
|
|
55
|
-
export function saveMCPConfig(config) {
|
|
56
|
-
const store = loadStore();
|
|
57
|
-
store[config.name] = config;
|
|
58
|
-
saveStore(store);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Load an MCP config by name
|
|
62
|
-
*/
|
|
63
|
-
export function loadMCPConfig(name) {
|
|
64
|
-
const store = loadStore();
|
|
65
|
-
return store[name] || null;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Delete an MCP config by name
|
|
69
|
-
*/
|
|
70
|
-
export function deleteMCPConfig(name) {
|
|
71
|
-
const store = loadStore();
|
|
72
|
-
if (store[name]) {
|
|
73
|
-
delete store[name];
|
|
74
|
-
saveStore(store);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* List all MCP configs
|
|
81
|
-
*/
|
|
82
|
-
export function listMCPConfigs() {
|
|
83
|
-
const store = loadStore();
|
|
84
|
-
return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Check if an MCP config exists
|
|
88
|
-
*/
|
|
89
|
-
export function mcpConfigExists(name) {
|
|
90
|
-
const store = loadStore();
|
|
91
|
-
return name in store;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Get a summary of an MCP config for display
|
|
95
|
-
*/
|
|
96
|
-
export function getMCPSummary(config) {
|
|
97
|
-
if (config.transport === "http") {
|
|
98
|
-
const parts = [`HTTP: ${config.url}`];
|
|
99
|
-
if (config.headers && Object.keys(config.headers).length > 0) {
|
|
100
|
-
parts.push(`(${Object.keys(config.headers).length} header${Object.keys(config.headers).length === 1 ? "" : "s"})`);
|
|
101
|
-
}
|
|
102
|
-
return parts.join(" ");
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
const parts = [`Stdio: ${config.command}`];
|
|
106
|
-
if (config.args && config.args.length > 0) {
|
|
107
|
-
parts.push(`[${config.args.join(" ")}]`);
|
|
108
|
-
}
|
|
109
|
-
return parts.join(" ");
|
|
110
|
-
}
|
|
111
|
-
}
|