@toolplex/client 0.1.18 ā 0.1.19
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/mcp-server/index.js +10 -0
- package/dist/mcp-server/registry.d.ts +17 -0
- package/dist/mcp-server/registry.js +23 -0
- package/dist/mcp-server/toolHandlers/initHandler.d.ts +9 -0
- package/dist/mcp-server/toolHandlers/initHandler.js +46 -1
- package/dist/mcp-server/toolplexServer.js +5 -0
- package/dist/mcp-server/utils/runtimeCheck.d.ts +18 -0
- package/dist/mcp-server/utils/runtimeCheck.js +70 -6
- package/dist/server-manager/serverManager.js +3 -9
- package/dist/shared/enhancedPath.js +3 -3
- package/dist/shared/mcpServerTypes.d.ts +12 -0
- package/dist/src/mcp-server/clientContext.js +118 -0
- package/dist/src/mcp-server/logging/telemetryLogger.js +54 -0
- package/dist/src/mcp-server/policy/callToolObserver.js +27 -0
- package/dist/src/mcp-server/policy/feedbackPolicy.js +39 -0
- package/dist/src/mcp-server/policy/installObserver.js +35 -0
- package/dist/src/mcp-server/policy/playbookPolicy.js +81 -0
- package/dist/src/mcp-server/policy/policyEnforcer.js +105 -0
- package/dist/src/mcp-server/policy/serverPolicy.js +63 -0
- package/dist/src/mcp-server/promptsCache.js +51 -0
- package/dist/src/mcp-server/registry.js +134 -0
- package/dist/src/mcp-server/serversCache.js +100 -0
- package/dist/src/mcp-server/toolDefinitionsCache.js +67 -0
- package/dist/src/mcp-server/toolHandlers/initHandler.js +185 -0
- package/dist/src/mcp-server/toolplexApi/service.js +221 -0
- package/dist/src/mcp-server/toolplexApi/types.js +1 -0
- package/dist/src/mcp-server/utils/initServerManagers.js +31 -0
- package/dist/src/mcp-server/utils/runtimeCheck.js +94 -0
- package/dist/src/shared/enhancedPath.js +52 -0
- package/dist/src/shared/fileLogger.js +66 -0
- package/dist/src/shared/mcpServerTypes.js +158 -0
- package/dist/src/shared/serverManagerTypes.js +73 -0
- package/dist/src/shared/stdioServerManagerClient.js +98 -0
- package/dist/tests/unit/bundledDependencies.test.js +152 -0
- package/dist/tests/unit/registry.test.js +216 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -3
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { FileLogger } from "../../shared/fileLogger.js";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { initServerManagersOnly } from "../utils/initServerManagers.js";
|
|
4
|
+
import Registry from "../registry.js";
|
|
5
|
+
import envPaths from "env-paths";
|
|
6
|
+
import which from "which";
|
|
7
|
+
import { getEnhancedPath } from "../../shared/enhancedPath.js";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
const logger = FileLogger;
|
|
10
|
+
/**
|
|
11
|
+
* Helper to resolve dependency path with fallback:
|
|
12
|
+
* 1. Bundled dependency (if available)
|
|
13
|
+
* 2. System PATH (fallback)
|
|
14
|
+
* 3. "not available" if neither exists
|
|
15
|
+
*
|
|
16
|
+
* Exported for testing purposes.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveDependencyForInit(bundledPath, commandName) {
|
|
19
|
+
// Check bundled first
|
|
20
|
+
if (bundledPath && fs.existsSync(bundledPath)) {
|
|
21
|
+
return bundledPath;
|
|
22
|
+
}
|
|
23
|
+
// Fall back to system PATH
|
|
24
|
+
try {
|
|
25
|
+
const enhancedPath = getEnhancedPath();
|
|
26
|
+
const systemPath = which.sync(commandName, {
|
|
27
|
+
path: enhancedPath,
|
|
28
|
+
nothrow: true,
|
|
29
|
+
});
|
|
30
|
+
if (systemPath) {
|
|
31
|
+
return `${systemPath} (system)`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore errors, will return "not available"
|
|
36
|
+
}
|
|
37
|
+
return "not available";
|
|
38
|
+
}
|
|
39
|
+
export async function handleInitialize(params) {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
await logger.info("Initializing ToolPlex");
|
|
42
|
+
await logger.debug(`Initialization params: ${JSON.stringify(params)}`);
|
|
43
|
+
const clientContext = Registry.getClientContext();
|
|
44
|
+
const apiService = Registry.getToolplexApiService();
|
|
45
|
+
const serverManagerClients = Registry.getServerManagerClients();
|
|
46
|
+
const telemetryLogger = Registry.getTelemetryLogger();
|
|
47
|
+
const promptsCache = Registry.getPromptsCache();
|
|
48
|
+
const serversCache = Registry.getServersCache();
|
|
49
|
+
const policyEnforcer = Registry.getPolicyEnforcer();
|
|
50
|
+
await logger.debug(`Server manager clients: ${Object.keys(serverManagerClients).join(", ")}`);
|
|
51
|
+
const platform = os.platform();
|
|
52
|
+
const osName = platform === "darwin"
|
|
53
|
+
? "macOS"
|
|
54
|
+
: platform === "win32"
|
|
55
|
+
? "Windows"
|
|
56
|
+
: platform.charAt(0).toUpperCase() + platform.slice(1);
|
|
57
|
+
const paths = envPaths("ToolPlex", { suffix: "" });
|
|
58
|
+
// Get bundled dependency information
|
|
59
|
+
const bundledDeps = Registry.getBundledDependencies();
|
|
60
|
+
const systemInfo = {
|
|
61
|
+
os: `${osName} ${os.release()}`,
|
|
62
|
+
arch: os.arch(),
|
|
63
|
+
memory: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))}GB`,
|
|
64
|
+
cpuCores: os.cpus().length,
|
|
65
|
+
workDir: paths.data,
|
|
66
|
+
date: new Date().toLocaleDateString("en-US", {
|
|
67
|
+
weekday: "long",
|
|
68
|
+
year: "numeric",
|
|
69
|
+
month: "long",
|
|
70
|
+
day: "numeric",
|
|
71
|
+
}),
|
|
72
|
+
// Resolve dependency paths with bundled > system > not available priority
|
|
73
|
+
nodePath: resolveDependencyForInit(bundledDeps.node, "node"),
|
|
74
|
+
pythonPath: resolveDependencyForInit(bundledDeps.python, platform === "win32" ? "python" : "python3"),
|
|
75
|
+
gitPath: resolveDependencyForInit(bundledDeps.git, "git"),
|
|
76
|
+
uvxPath: resolveDependencyForInit(bundledDeps.uvx, "uvx"),
|
|
77
|
+
npxPath: resolveDependencyForInit(bundledDeps.npx, "npx"),
|
|
78
|
+
};
|
|
79
|
+
await logger.debug("Initializing server managers and API service");
|
|
80
|
+
const [serverManagerInitResults, toolplexApiInitResponse] = await Promise.all([
|
|
81
|
+
initServerManagersOnly(serverManagerClients).catch((err) => {
|
|
82
|
+
logger.warn(`Server manager init failed: ${err}`);
|
|
83
|
+
return { succeeded: [], failures: {} };
|
|
84
|
+
}),
|
|
85
|
+
apiService.init(),
|
|
86
|
+
]);
|
|
87
|
+
clientContext.isOrgUser = toolplexApiInitResponse.is_org_user;
|
|
88
|
+
clientContext.sessionId = toolplexApiInitResponse.session_id;
|
|
89
|
+
clientContext.permissions = toolplexApiInitResponse.permissions;
|
|
90
|
+
clientContext.flags = toolplexApiInitResponse.flags;
|
|
91
|
+
// Replace {ARGS.workDir} in all prompts before initializing the cache
|
|
92
|
+
const processedPrompts = { ...toolplexApiInitResponse.prompts };
|
|
93
|
+
for (const [key, prompt] of Object.entries(processedPrompts)) {
|
|
94
|
+
if (typeof prompt === "string") {
|
|
95
|
+
processedPrompts[key] = prompt.replace(/\{ARGS\.workDir\}/g, paths.data);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
promptsCache.init(processedPrompts);
|
|
99
|
+
// Init PolicyEnforce after setting permissions and flags
|
|
100
|
+
policyEnforcer.init(clientContext);
|
|
101
|
+
const allSucceeded = serverManagerInitResults.succeeded;
|
|
102
|
+
const allFailures = serverManagerInitResults.failures;
|
|
103
|
+
// Initialize the serversCache with the succeeded servers
|
|
104
|
+
serversCache.init(allSucceeded);
|
|
105
|
+
await logger.debug(`Total successes: ${allSucceeded.length}, Total failures: ${Object.keys(allFailures).length}`);
|
|
106
|
+
await logger.debug("Building initialization response");
|
|
107
|
+
// Safe to use prompts after init.
|
|
108
|
+
const result = {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: promptsCache
|
|
113
|
+
.getPrompt("initialization")
|
|
114
|
+
.replace("{ARGS.os}", systemInfo.os)
|
|
115
|
+
.replace("{ARGS.arch}", systemInfo.arch)
|
|
116
|
+
.replace("{ARGS.memory}", systemInfo.memory)
|
|
117
|
+
.replace("{ARGS.cpuCores}", systemInfo.cpuCores.toString())
|
|
118
|
+
.replace("{ARGS.workDir}", systemInfo.workDir)
|
|
119
|
+
.replace("{ARGS.date}", systemInfo.date)
|
|
120
|
+
.replace("{ARGS.nodePath}", systemInfo.nodePath)
|
|
121
|
+
.replace("{ARGS.pythonPath}", systemInfo.pythonPath)
|
|
122
|
+
.replace("{ARGS.gitPath}", systemInfo.gitPath)
|
|
123
|
+
.replace("{ARGS.uvxPath}", systemInfo.uvxPath)
|
|
124
|
+
.replace("{ARGS.npxPath}", systemInfo.npxPath),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
result.content.push({
|
|
129
|
+
type: "text",
|
|
130
|
+
text: promptsCache
|
|
131
|
+
.getPrompt("initialization_results")
|
|
132
|
+
.replace("{SUCCEEDED}", allSucceeded
|
|
133
|
+
.map((s) => `${s.server_id} (${s.server_name})`)
|
|
134
|
+
.join(", ") || "none")
|
|
135
|
+
.replace("{FAILURES}", Object.entries(allFailures)
|
|
136
|
+
.map(([serverId, failure]) => `${serverId} (${failure.server_name}): ${failure.error}`)
|
|
137
|
+
.join(", ") || "none")
|
|
138
|
+
.replace("{FAILURE_NOTE}", Object.keys(allFailures).length > 0
|
|
139
|
+
? "Please note there were failures installing some servers. Inform the user."
|
|
140
|
+
: ""),
|
|
141
|
+
});
|
|
142
|
+
if (clientContext.permissions.allowed_mcp_servers &&
|
|
143
|
+
clientContext.permissions.allowed_mcp_servers.length > 0) {
|
|
144
|
+
result.content.push({
|
|
145
|
+
type: "text",
|
|
146
|
+
text: promptsCache
|
|
147
|
+
.getPrompt("allowed_mcp_servers")
|
|
148
|
+
.replace("{ALLOWED_MCP_SERVERS}", clientContext.permissions.allowed_mcp_servers.join(", ")),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
result.content.push({
|
|
152
|
+
type: "text",
|
|
153
|
+
text: "Your Most Recently Used Playbooks:\n" +
|
|
154
|
+
toolplexApiInitResponse.playbooks.playbooks
|
|
155
|
+
.map((p) => `- ${p.id}: ${p.description}\n` +
|
|
156
|
+
` Used ${p.times_used} times` +
|
|
157
|
+
(p.days_since_last_used !== null
|
|
158
|
+
? `, last use: ${p.days_since_last_used} ${p.days_since_last_used === 1 ? "day" : "days"} ago`
|
|
159
|
+
: ""))
|
|
160
|
+
.join("\n") +
|
|
161
|
+
"\n\nMore playbooks are available through the search tool.",
|
|
162
|
+
});
|
|
163
|
+
if (toolplexApiInitResponse.announcement) {
|
|
164
|
+
result.content.push({
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `\nToolPlex Platform Announcements: ${toolplexApiInitResponse.announcement}`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
await telemetryLogger.log("client_initialize_toolplex", {
|
|
170
|
+
session_id: toolplexApiInitResponse.session_id,
|
|
171
|
+
success: Object.keys(allFailures).length === 0,
|
|
172
|
+
log_context: {
|
|
173
|
+
os_platform: platform,
|
|
174
|
+
os_arch: systemInfo.arch,
|
|
175
|
+
cpu_cores: systemInfo.cpuCores,
|
|
176
|
+
total_memory_gb: Math.round(os.totalmem() / (1024 * 1024 * 1024)),
|
|
177
|
+
num_succeeded_servers: allSucceeded.length,
|
|
178
|
+
num_failed_servers: Object.keys(allFailures).length,
|
|
179
|
+
num_recent_playbooks: toolplexApiInitResponse.playbooks.playbooks.length,
|
|
180
|
+
},
|
|
181
|
+
latency_ms: Date.now() - startTime,
|
|
182
|
+
});
|
|
183
|
+
await logger.info("ToolPlex initialization completed");
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import { FileLogger } from "../../shared/fileLogger.js";
|
|
3
|
+
import os from "os";
|
|
4
|
+
const logger = FileLogger;
|
|
5
|
+
export class ToolplexApiService {
|
|
6
|
+
constructor(clientContext) {
|
|
7
|
+
if (!clientContext.apiKey) {
|
|
8
|
+
throw new Error("API key not set in client context");
|
|
9
|
+
}
|
|
10
|
+
if (!clientContext.clientVersion) {
|
|
11
|
+
throw new Error("Client version not set in client context");
|
|
12
|
+
}
|
|
13
|
+
this.clientContext = clientContext;
|
|
14
|
+
this.baseUrl = this.getBaseUrl(clientContext.dev);
|
|
15
|
+
this.machineContext = {
|
|
16
|
+
os: `${os.platform()} ${os.release()}`,
|
|
17
|
+
arch: os.arch(),
|
|
18
|
+
memory_gb: Math.round(os.totalmem() / (1024 * 1024 * 1024)),
|
|
19
|
+
cpu_cores: os.cpus().length.toString(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
getBaseUrl(dev) {
|
|
23
|
+
return dev ? "http://localhost:8080" : "https://api.toolplex.ai";
|
|
24
|
+
}
|
|
25
|
+
async handleFetchResponse(response) {
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.text();
|
|
28
|
+
throw new Error(`HTTP ${response.status}: ${error}`);
|
|
29
|
+
}
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
getBaseHeaders() {
|
|
33
|
+
return {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
Accept: "application/json",
|
|
36
|
+
"x-api-key": this.clientContext.apiKey,
|
|
37
|
+
"x-client-mode": this.clientContext.clientMode,
|
|
38
|
+
"x-client-name": this.clientContext.clientName,
|
|
39
|
+
"x-client-version": this.clientContext.clientVersion,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
getHeadersWithSession() {
|
|
43
|
+
return {
|
|
44
|
+
...this.getBaseHeaders(),
|
|
45
|
+
"x-session-id": this.clientContext.sessionId,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async init() {
|
|
49
|
+
try {
|
|
50
|
+
const initRequest = {
|
|
51
|
+
llm_context: this.clientContext.llmContext,
|
|
52
|
+
};
|
|
53
|
+
const response = await fetch(`${this.baseUrl}/init`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: this.getBaseHeaders(),
|
|
56
|
+
body: JSON.stringify(initRequest),
|
|
57
|
+
});
|
|
58
|
+
return this.handleFetchResponse(response);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
await logger.error(`Error initializing session: ${err}`);
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async getTools() {
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`${this.baseUrl}/tools`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: this.getBaseHeaders(),
|
|
70
|
+
body: JSON.stringify({}),
|
|
71
|
+
});
|
|
72
|
+
return this.handleFetchResponse(response);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
await logger.error(`Error fetching tool definitions: ${err}`);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async logTelemetryEvents(events) {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(`${this.baseUrl}/telemetry/log/batch`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: this.getHeadersWithSession(),
|
|
84
|
+
body: JSON.stringify(events.map((event) => ({
|
|
85
|
+
event_type: event.eventType,
|
|
86
|
+
...event.data,
|
|
87
|
+
}))),
|
|
88
|
+
});
|
|
89
|
+
return this.handleFetchResponse(response);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
await logger.error(`Error batch logging telemetry events: ${err}`);
|
|
93
|
+
return { success: false };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async lookupEntity(entityType, entityId) {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(`${this.baseUrl}/lookup-entity`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: this.getHeadersWithSession(),
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
entity_type: entityType,
|
|
103
|
+
entity_id: entityId,
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
return this.handleFetchResponse(response);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
await logger.error(`Error looking up entity: ${err}`);
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async search(query, expandedKeywords = [], filter = "all", size = 10, scope = "all") {
|
|
114
|
+
const requestBody = {
|
|
115
|
+
query,
|
|
116
|
+
expanded_keywords: expandedKeywords,
|
|
117
|
+
filter,
|
|
118
|
+
size,
|
|
119
|
+
scope,
|
|
120
|
+
};
|
|
121
|
+
await logger.debug(`Searching API at ${this.baseUrl} with query: ${query}`);
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(`${this.baseUrl}/search`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: this.getHeadersWithSession(),
|
|
126
|
+
body: JSON.stringify(requestBody),
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const errorText = await response.text();
|
|
130
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
131
|
+
}
|
|
132
|
+
return (await response.json());
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
await logger.error(`Error during search request: ${err}`);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async createPlaybook(playbook_name, description, actions, domain, keywords, requirements, privacy, sourcePlaybookId, forkReason) {
|
|
140
|
+
const requestBody = {
|
|
141
|
+
playbook_name,
|
|
142
|
+
description,
|
|
143
|
+
actions,
|
|
144
|
+
llm_context: this.clientContext.llmContext,
|
|
145
|
+
domain,
|
|
146
|
+
keywords,
|
|
147
|
+
requirements,
|
|
148
|
+
privacy,
|
|
149
|
+
source_playbook_id: sourcePlaybookId,
|
|
150
|
+
fork_reason: forkReason,
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(`${this.baseUrl}/playbooks/create`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: this.getHeadersWithSession(),
|
|
156
|
+
body: JSON.stringify(requestBody),
|
|
157
|
+
});
|
|
158
|
+
return this.handleFetchResponse(response);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
await logger.error(`Error creating playbook: ${err}`);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async logPlaybookUsage(playbookId, success, errorMessage) {
|
|
166
|
+
const requestBody = {
|
|
167
|
+
playbook_id: playbookId,
|
|
168
|
+
success,
|
|
169
|
+
llm_context: this.clientContext.llmContext,
|
|
170
|
+
error_message: errorMessage,
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(`${this.baseUrl}/playbooks/log-usage`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: this.getHeadersWithSession(),
|
|
176
|
+
body: JSON.stringify(requestBody),
|
|
177
|
+
});
|
|
178
|
+
return this.handleFetchResponse(response);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
await logger.error(`Error logging playbook usage: ${err}`);
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async submitFeedback(targetType, targetId, vote, message, securityAssessment) {
|
|
186
|
+
const requestBody = {
|
|
187
|
+
target_type: targetType,
|
|
188
|
+
target_id: targetId,
|
|
189
|
+
vote,
|
|
190
|
+
message,
|
|
191
|
+
llm_context: this.clientContext.llmContext,
|
|
192
|
+
machine_context: this.machineContext,
|
|
193
|
+
security_assessment: securityAssessment,
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(`${this.baseUrl}/feedback/submit`, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: this.getHeadersWithSession(),
|
|
199
|
+
body: JSON.stringify(requestBody),
|
|
200
|
+
});
|
|
201
|
+
return this.handleFetchResponse(response);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
await logger.error(`Error submitting feedback: ${err}`);
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async getFeedbackSummary() {
|
|
209
|
+
try {
|
|
210
|
+
const response = await fetch(`${this.baseUrl}/feedback/summarize`, {
|
|
211
|
+
method: "GET",
|
|
212
|
+
headers: this.getHeadersWithSession(),
|
|
213
|
+
});
|
|
214
|
+
return this.handleFetchResponse(response);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
await logger.error(`Error getting feedback summary: ${err}`);
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { InitializeResultSchema, } from "../../shared/serverManagerTypes.js";
|
|
2
|
+
import { FileLogger } from "../../shared/fileLogger.js";
|
|
3
|
+
const logger = FileLogger;
|
|
4
|
+
export async function initServerManagersOnly(serverManagerClients) {
|
|
5
|
+
await logger.info("Pre-warming server manager clients");
|
|
6
|
+
const initPromises = Object.entries(serverManagerClients).map(async ([runtime, client]) => {
|
|
7
|
+
try {
|
|
8
|
+
const response = await client.sendRequest("initialize", {});
|
|
9
|
+
if ("error" in response)
|
|
10
|
+
throw new Error(response.error.message);
|
|
11
|
+
const parsed = InitializeResultSchema.safeParse(response);
|
|
12
|
+
if (!parsed.success)
|
|
13
|
+
throw new Error(parsed.error.message);
|
|
14
|
+
return { runtime, result: parsed.data };
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
await logger.error(`Warmup error for ${runtime}: ${err}`);
|
|
18
|
+
return { runtime, result: { succeeded: [], failures: {} } };
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const results = await Promise.all(initPromises);
|
|
22
|
+
const allSucceeded = [];
|
|
23
|
+
const allFailures = {};
|
|
24
|
+
for (const { runtime, result } of results) {
|
|
25
|
+
allSucceeded.push(...(result.succeeded || []));
|
|
26
|
+
Object.assign(allFailures, result.failures || {});
|
|
27
|
+
await logger.debug(`Warmup result for ${runtime}: ${JSON.stringify(result)}`);
|
|
28
|
+
}
|
|
29
|
+
await logger.debug(`Warmup completed: ${allSucceeded.length} successes, ${Object.keys(allFailures).length} failures`);
|
|
30
|
+
return { succeeded: allSucceeded, failures: allFailures };
|
|
31
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getEnhancedPath } from "../../shared/enhancedPath.js";
|
|
2
|
+
import which from "which";
|
|
3
|
+
import Registry from "../registry.js";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
const INSTALL_HINTS = {
|
|
6
|
+
uvx: "Install uvx: https://docs.astral.sh/uv/getting-started/installation/",
|
|
7
|
+
uv: "Install uv: https://docs.astral.sh/uv/getting-started/installation/",
|
|
8
|
+
python: "Install Python: https://www.python.org/downloads/. Or check if you have `python3` installed.",
|
|
9
|
+
python3: "Install Python: https://www.python.org/downloads/. Or check if you have `python` installed.",
|
|
10
|
+
node: "Install Node.js: https://nodejs.org/en/download/",
|
|
11
|
+
npx: "Install npx (comes with Node.js): https://nodejs.org/en/download/",
|
|
12
|
+
git: "Install Git: https://git-scm.com/downloads",
|
|
13
|
+
};
|
|
14
|
+
// Commands that should use bundled dependencies (required)
|
|
15
|
+
const BUNDLED_DEPENDENCY_COMMANDS = [
|
|
16
|
+
"node",
|
|
17
|
+
"python",
|
|
18
|
+
"python3",
|
|
19
|
+
"git",
|
|
20
|
+
"npx",
|
|
21
|
+
"uvx",
|
|
22
|
+
];
|
|
23
|
+
export class RuntimeCheck {
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a dependency path with priority order:
|
|
26
|
+
* 1. Bundled dependencies (if provided by host application like ToolPlex Desktop)
|
|
27
|
+
* 2. System PATH (fallback for standalone @client usage)
|
|
28
|
+
* 3. Error if neither available
|
|
29
|
+
*
|
|
30
|
+
* This allows ToolPlex Desktop to provide reliable bundled dependencies while
|
|
31
|
+
* still supporting standalone users who have system dependencies installed.
|
|
32
|
+
*
|
|
33
|
+
* @param commandName - The command to resolve
|
|
34
|
+
* @returns The full path to the command executable
|
|
35
|
+
* @throws Error if the command is not available in bundled deps or system PATH
|
|
36
|
+
*/
|
|
37
|
+
static resolveDependency(commandName) {
|
|
38
|
+
// Check if this is a known bundled dependency type
|
|
39
|
+
const isBundledDep = BUNDLED_DEPENDENCY_COMMANDS.includes(commandName);
|
|
40
|
+
if (isBundledDep) {
|
|
41
|
+
// Priority 1: Try bundled dependency first (preferred for ToolPlex Desktop)
|
|
42
|
+
const bundledPath = Registry.getBundledDependencyPath(commandName);
|
|
43
|
+
if (bundledPath && fs.existsSync(bundledPath)) {
|
|
44
|
+
return bundledPath;
|
|
45
|
+
}
|
|
46
|
+
// Handle python3 -> python mapping for bundled deps
|
|
47
|
+
if (commandName === "python3") {
|
|
48
|
+
const pythonPath = Registry.getBundledDependencyPath("python");
|
|
49
|
+
if (pythonPath && fs.existsSync(pythonPath)) {
|
|
50
|
+
return pythonPath;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Priority 2: Fall back to system PATH (for standalone @client usage)
|
|
54
|
+
const enhancedPath = getEnhancedPath();
|
|
55
|
+
const resolved = which.sync(commandName, {
|
|
56
|
+
path: enhancedPath,
|
|
57
|
+
nothrow: true,
|
|
58
|
+
});
|
|
59
|
+
if (resolved) {
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
// Priority 3: Neither bundled nor system available - error
|
|
63
|
+
const hint = INSTALL_HINTS[commandName];
|
|
64
|
+
throw new Error(`Missing required command: '${commandName}'.\n` +
|
|
65
|
+
`This command is not available in bundled dependencies or system PATH.\n` +
|
|
66
|
+
(hint ? `š ${hint}` : ""));
|
|
67
|
+
}
|
|
68
|
+
// For non-bundled dependencies, only check system PATH
|
|
69
|
+
const enhancedPath = getEnhancedPath();
|
|
70
|
+
const resolved = which.sync(commandName, {
|
|
71
|
+
path: enhancedPath,
|
|
72
|
+
nothrow: true,
|
|
73
|
+
});
|
|
74
|
+
if (!resolved) {
|
|
75
|
+
const hint = INSTALL_HINTS[commandName];
|
|
76
|
+
if (hint) {
|
|
77
|
+
throw new Error(`Missing required command: '${commandName}'.\nš ${hint}`);
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Command '${commandName}' not found in enhanced PATH. Please install it manually or check your config.`);
|
|
80
|
+
}
|
|
81
|
+
return resolved;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate that a command is available (either bundled or in system PATH).
|
|
85
|
+
* Throws an error if the command is not found.
|
|
86
|
+
*/
|
|
87
|
+
static validateCommandOrThrow(rawCommand) {
|
|
88
|
+
const command = this.extractCommandName(rawCommand);
|
|
89
|
+
this.resolveDependency(command);
|
|
90
|
+
}
|
|
91
|
+
static extractCommandName(command) {
|
|
92
|
+
return command.trim().split(/\s+/)[0];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
/**
|
|
6
|
+
* Returns an enhanced PATH string by prepending common binary directories
|
|
7
|
+
* across platforms (Linux, macOS, Windows).
|
|
8
|
+
*
|
|
9
|
+
* Ensures no duplicates and checks for existence.
|
|
10
|
+
*/
|
|
11
|
+
export function getEnhancedPath() {
|
|
12
|
+
const home = homedir();
|
|
13
|
+
const basePaths = (process.env.PATH || "").split(path.delimiter);
|
|
14
|
+
const extraPaths = getDefaultExtraPaths(home);
|
|
15
|
+
const seen = new Set(basePaths);
|
|
16
|
+
const allPaths = [...basePaths];
|
|
17
|
+
for (const extraPath of extraPaths) {
|
|
18
|
+
if (extraPath.includes("*")) {
|
|
19
|
+
const matches = glob.sync(extraPath);
|
|
20
|
+
for (const match of matches) {
|
|
21
|
+
if (fs.existsSync(match) && !seen.has(match)) {
|
|
22
|
+
seen.add(match);
|
|
23
|
+
allPaths.unshift(match);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (fs.existsSync(extraPath) && !seen.has(extraPath)) {
|
|
29
|
+
seen.add(extraPath);
|
|
30
|
+
allPaths.unshift(extraPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return allPaths.join(path.delimiter);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns platform-specific extra binary paths.
|
|
38
|
+
*/
|
|
39
|
+
function getDefaultExtraPaths(home) {
|
|
40
|
+
const isWindows = process.platform === "win32";
|
|
41
|
+
return isWindows
|
|
42
|
+
? [
|
|
43
|
+
path.join(home, "AppData/Local/Programs/Python/Python3*/Scripts"),
|
|
44
|
+
path.join(home, "AppData/Roaming/npm"),
|
|
45
|
+
]
|
|
46
|
+
: [
|
|
47
|
+
path.join(home, ".local/bin"),
|
|
48
|
+
path.join(home, ".cargo/bin"),
|
|
49
|
+
"/usr/local/bin",
|
|
50
|
+
"/opt/homebrew/bin",
|
|
51
|
+
];
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import envPaths from "env-paths";
|
|
3
|
+
import winston from "winston";
|
|
4
|
+
import callsite from "callsite";
|
|
5
|
+
import DailyRotateFile from "winston-daily-rotate-file";
|
|
6
|
+
const paths = envPaths("ToolPlex", { suffix: "" });
|
|
7
|
+
export const logDir = path.join(paths.log);
|
|
8
|
+
function getCallingModule() {
|
|
9
|
+
const stack = callsite();
|
|
10
|
+
for (let i = 2; i < stack.length; i++) {
|
|
11
|
+
const fileName = stack[i].getFileName();
|
|
12
|
+
if (fileName &&
|
|
13
|
+
!fileName.includes("node_modules") &&
|
|
14
|
+
!fileName.includes("fileLogger") &&
|
|
15
|
+
!fileName.includes("callsite")) {
|
|
16
|
+
return path.basename(fileName, path.extname(fileName));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
export class FileLogger {
|
|
22
|
+
static initialize(processName) {
|
|
23
|
+
if (this.logger)
|
|
24
|
+
return;
|
|
25
|
+
this.processName = processName;
|
|
26
|
+
const logLevel = process.env.LOG_LEVEL || "info";
|
|
27
|
+
this.transport = new DailyRotateFile({
|
|
28
|
+
dirname: logDir,
|
|
29
|
+
filename: `ToolPlex-${processName}-%DATE%.log`,
|
|
30
|
+
datePattern: "YYYY-MM-DD",
|
|
31
|
+
maxFiles: "7d",
|
|
32
|
+
zippedArchive: false,
|
|
33
|
+
level: logLevel,
|
|
34
|
+
});
|
|
35
|
+
this.logger = winston.createLogger({
|
|
36
|
+
level: logLevel,
|
|
37
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.printf(({ level, message, timestamp, module }) => {
|
|
38
|
+
return `${timestamp} [${level.toUpperCase()}] ${module || "unknown"} - ${message}`;
|
|
39
|
+
})),
|
|
40
|
+
defaultMeta: {},
|
|
41
|
+
transports: [this.transport],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
static log(level, message) {
|
|
45
|
+
const module = getCallingModule();
|
|
46
|
+
this.logger.log({ level, message, module });
|
|
47
|
+
}
|
|
48
|
+
static info(message) {
|
|
49
|
+
this.log("info", message);
|
|
50
|
+
}
|
|
51
|
+
static warn(message) {
|
|
52
|
+
this.log("warn", message);
|
|
53
|
+
}
|
|
54
|
+
static error(message) {
|
|
55
|
+
this.log("error", message);
|
|
56
|
+
}
|
|
57
|
+
static debug(message) {
|
|
58
|
+
this.log("debug", message);
|
|
59
|
+
}
|
|
60
|
+
static async flush() {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
this.transport.on("finish", resolve);
|
|
63
|
+
this.logger.end();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|