@apify/mcpc 0.3.1-beta.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -3
- package/README.md +1 -1
- package/dist/bridge/index.js +2 -2
- package/dist/bridge/index.js.map +1 -1
- package/dist/cli/commands/connect.d.ts +63 -0
- package/dist/cli/commands/connect.d.ts.map +1 -0
- package/dist/cli/commands/connect.js +655 -0
- package/dist/cli/commands/connect.js.map +1 -0
- package/dist/cli/commands/sessions.d.ts +3 -58
- package/dist/cli/commands/sessions.d.ts.map +1 -1
- package/dist/cli/commands/sessions.js +24 -733
- package/dist/cli/commands/sessions.js.map +1 -1
- package/dist/cli/index.js +8 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +4 -5
- package/dist/cli/output.js.map +1 -1
- package/dist/core/mcp-client.d.ts +1 -1
- package/dist/core/mcp-client.d.ts.map +1 -1
- package/dist/core/mcp-client.js +5 -3
- package/dist/core/mcp-client.js.map +1 -1
- package/dist/lib/bridge-manager.d.ts +1 -1
- package/dist/lib/bridge-manager.d.ts.map +1 -1
- package/dist/lib/bridge-manager.js +5 -5
- package/dist/lib/bridge-manager.js.map +1 -1
- package/dist/lib/session-client.d.ts +1 -1
- package/dist/lib/session-client.d.ts.map +1 -1
- package/dist/lib/session-client.js +3 -3
- package/dist/lib/session-client.js.map +1 -1
- package/dist/lib/types.d.ts +3 -3
- package/dist/lib/types.d.ts.map +1 -1
- package/docs/claude-skill/SKILL.md +0 -1
- package/package.json +1 -1
|
@@ -1,352 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { stat } from 'fs/promises';
|
|
3
|
-
import { isValidSessionName, generateSessionName, normalizeServerUrl, validateProfileName, isProcessAlive, getServerHost, redactHeaders, } from '../../lib/index.js';
|
|
1
|
+
import { isProcessAlive, getServerHost, redactHeaders, ClientError, } from '../../lib/index.js';
|
|
4
2
|
import { DISCONNECTED_THRESHOLD_MS } from '../../lib/types.js';
|
|
5
|
-
import { formatOutput, formatSuccess, formatWarning, formatError, formatSessionLine, formatServerDetails,
|
|
6
|
-
import { withMcpClient,
|
|
3
|
+
import { formatOutput, formatSuccess, formatWarning, formatError, formatSessionLine, formatServerDetails, theme, } from '../output.js';
|
|
4
|
+
import { withMcpClient, resolveAuthProfile } from '../helpers.js';
|
|
7
5
|
import { listAuthProfiles } from '../../lib/auth/profiles.js';
|
|
8
|
-
import { sessionExists, deleteSession,
|
|
6
|
+
import { sessionExists, deleteSession, updateSession, consolidateSessions, getSession, } from '../../lib/sessions.js';
|
|
9
7
|
import { startBridge, stopBridge, reconnectCrashedSessions, } from '../../lib/bridge-manager.js';
|
|
10
|
-
import { storeKeychainSessionHeaders, storeKeychainProxyBearerToken, } from '../../lib/auth/keychain.js';
|
|
11
|
-
import { AuthError, ClientError, isAuthenticationError, createServerAuthError, } from '../../lib/index.js';
|
|
12
|
-
import { getWallet } from '../../lib/wallets.js';
|
|
13
8
|
import chalk from 'chalk';
|
|
14
9
|
import { createLogger } from '../../lib/logger.js';
|
|
15
|
-
import { parseProxyArg } from '../parser.js';
|
|
16
10
|
import { getBridgeLogPath } from '../../lib/log-reader.js';
|
|
17
|
-
import { loadConfig, listServers, isStdioEntry, scanMcpConfigFiles, getStandardMcpConfigPaths, } from '../../lib/config.js';
|
|
18
11
|
const logger = createLogger('sessions');
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
resolve(false);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
server.once('listening', () => {
|
|
31
|
-
server.close(() => {
|
|
32
|
-
resolve(true);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
server.listen(port, host);
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
async function findMatchingSession(parsed, options) {
|
|
39
|
-
const storage = await loadSessions();
|
|
40
|
-
const sessions = Object.values(storage.sessions);
|
|
41
|
-
if (sessions.length === 0)
|
|
42
|
-
return undefined;
|
|
43
|
-
const effectiveProfile = options.noProfile ? undefined : (options.profile ?? 'default');
|
|
44
|
-
for (const session of sessions) {
|
|
45
|
-
if (!session.server)
|
|
46
|
-
continue;
|
|
47
|
-
if (parsed.type === 'url') {
|
|
48
|
-
if (!session.server.url)
|
|
49
|
-
continue;
|
|
50
|
-
try {
|
|
51
|
-
const existingUrl = normalizeServerUrl(session.server.url);
|
|
52
|
-
const newUrl = normalizeServerUrl(parsed.url);
|
|
53
|
-
if (existingUrl !== newUrl)
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const sessionProfile = session.profileName ?? 'default';
|
|
64
|
-
if (effectiveProfile !== sessionProfile)
|
|
65
|
-
continue;
|
|
66
|
-
const existingHeaderKeys = Object.keys(session.server.headers || {}).sort();
|
|
67
|
-
const newHeaderKeys = (options.headers || [])
|
|
68
|
-
.map((h) => h.split(':')[0]?.trim() || '')
|
|
69
|
-
.filter(Boolean)
|
|
70
|
-
.sort();
|
|
71
|
-
if (existingHeaderKeys.join(',') !== newHeaderKeys.join(','))
|
|
72
|
-
continue;
|
|
73
|
-
return session.name;
|
|
74
|
-
}
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
77
|
-
export async function resolveSessionName(parsed, options) {
|
|
78
|
-
const existingName = await findMatchingSession(parsed, options);
|
|
79
|
-
if (existingName) {
|
|
80
|
-
return existingName;
|
|
81
|
-
}
|
|
82
|
-
const candidateName = generateSessionName(parsed);
|
|
83
|
-
const storage = await loadSessions();
|
|
84
|
-
if (!(candidateName in storage.sessions)) {
|
|
85
|
-
if (options.outputMode === 'human') {
|
|
86
|
-
console.log(theme.cyan(`Using session name: ${candidateName}`));
|
|
87
|
-
}
|
|
88
|
-
return candidateName;
|
|
89
|
-
}
|
|
90
|
-
for (let i = 2; i <= 99; i++) {
|
|
91
|
-
const suffixed = `${candidateName}-${i}`;
|
|
92
|
-
if (isValidSessionName(suffixed) && !(suffixed in storage.sessions)) {
|
|
93
|
-
if (options.outputMode === 'human') {
|
|
94
|
-
console.log(theme.cyan(`Using session name: ${suffixed}`));
|
|
95
|
-
}
|
|
96
|
-
return suffixed;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
throw new ClientError(`Cannot auto-generate session name: too many sessions for this server.\n` +
|
|
100
|
-
`Specify a name explicitly: mcpc connect ${parsed.type === 'url' ? parsed.url : `${parsed.file}:${parsed.entry}`} @my-session`);
|
|
101
|
-
}
|
|
102
|
-
async function buildConnectResultEntry(sessionName, status, options) {
|
|
103
|
-
return await withMcpClient(sessionName, {
|
|
104
|
-
outputMode: 'json',
|
|
105
|
-
hideTarget: true,
|
|
106
|
-
...(options.verbose && { verbose: options.verbose }),
|
|
107
|
-
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
108
|
-
}, async (client, context) => {
|
|
109
|
-
const serverDetails = await client.getServerDetails();
|
|
110
|
-
const tools = (await client.listAllTools()).tools;
|
|
111
|
-
const server = context.serverConfig
|
|
112
|
-
? {
|
|
113
|
-
...context.serverConfig,
|
|
114
|
-
...(context.serverConfig.headers && {
|
|
115
|
-
headers: redactHeaders(context.serverConfig.headers),
|
|
116
|
-
}),
|
|
117
|
-
}
|
|
118
|
-
: undefined;
|
|
119
|
-
return {
|
|
120
|
-
_mcpc: {
|
|
121
|
-
sessionName: context.sessionName ?? sessionName,
|
|
122
|
-
...(context.profileName && { profileName: context.profileName }),
|
|
123
|
-
...(server && { server }),
|
|
124
|
-
...(options.configFile && { configFile: options.configFile }),
|
|
125
|
-
...(options.entry && { entry: options.entry }),
|
|
126
|
-
status,
|
|
127
|
-
...(serverDetails.statefulness &&
|
|
128
|
-
serverDetails.statefulness !== 'unknown' && {
|
|
129
|
-
statefulness: serverDetails.statefulness,
|
|
130
|
-
}),
|
|
131
|
-
},
|
|
132
|
-
...(serverDetails.protocolVersion && { protocolVersion: serverDetails.protocolVersion }),
|
|
133
|
-
...(serverDetails.capabilities && { capabilities: serverDetails.capabilities }),
|
|
134
|
-
...(serverDetails.serverInfo && { serverInfo: serverDetails.serverInfo }),
|
|
135
|
-
...(serverDetails.instructions && { instructions: serverDetails.instructions }),
|
|
136
|
-
...(tools.length > 0 && { toolNames: tools.map((t) => t.name) }),
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
export async function connectSession(target, name, options) {
|
|
141
|
-
if (!isValidSessionName(name)) {
|
|
142
|
-
throw new ClientError(`Invalid session name: ${name}\n` +
|
|
143
|
-
`Session names must start with @ and be followed by 1-64 characters, alphanumeric with hyphens or underscores only (e.g., @my-session).`);
|
|
144
|
-
}
|
|
145
|
-
if (options.profile) {
|
|
146
|
-
validateProfileName(options.profile);
|
|
147
|
-
}
|
|
148
|
-
let proxyConfig;
|
|
149
|
-
if (options.proxy) {
|
|
150
|
-
proxyConfig = parseProxyArg(options.proxy);
|
|
151
|
-
logger.debug(`Proxy config: ${proxyConfig.host}:${proxyConfig.port}`);
|
|
152
|
-
const portAvailable = await checkPortAvailable(proxyConfig.host, proxyConfig.port);
|
|
153
|
-
if (!portAvailable) {
|
|
154
|
-
throw new ClientError(`Port ${proxyConfig.port} is already in use on ${proxyConfig.host}. ` +
|
|
155
|
-
`Choose a different port with --proxy [host:]port`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (options.proxyBearerToken && !options.proxy) {
|
|
159
|
-
throw new ClientError('--proxy-bearer-token requires --proxy to be specified');
|
|
160
|
-
}
|
|
161
|
-
const existingSession = await getSession(name);
|
|
162
|
-
if (existingSession) {
|
|
163
|
-
const bridgeStatus = getBridgeStatus(existingSession);
|
|
164
|
-
if (bridgeStatus === 'live') {
|
|
165
|
-
if (options.outputMode === 'human' && !options.quiet) {
|
|
166
|
-
console.log(formatSuccess(`Session ${name} is already active`));
|
|
167
|
-
}
|
|
168
|
-
if (!options.skipDetails) {
|
|
169
|
-
if (options.outputMode === 'json') {
|
|
170
|
-
const entry = await buildConnectResultEntry(name, 'active', {
|
|
171
|
-
...(options.verbose && { verbose: options.verbose }),
|
|
172
|
-
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
173
|
-
});
|
|
174
|
-
console.log(formatOutput([entry], 'json'));
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
await showServerDetails(name, { ...options, hideTarget: false });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (options.outputMode === 'human' && !options.quiet) {
|
|
183
|
-
console.log(theme.yellow(`Session ${name} exists but bridge is ${bridgeStatus}, reconnecting...`));
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
await stopBridge(name);
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
const serverConfig = await resolveTarget(target, options);
|
|
192
|
-
const hasExplicitAuthHeader = serverConfig.headers?.Authorization !== undefined;
|
|
193
|
-
const hasExplicitProfile = options.profile !== undefined;
|
|
194
|
-
if (hasExplicitAuthHeader && hasExplicitProfile) {
|
|
195
|
-
throw new ClientError(`Cannot combine --profile with --header "Authorization: ...".\n\n` +
|
|
196
|
-
`Use either:\n` +
|
|
197
|
-
` --profile ${options.profile} (OAuth authentication via saved profile)\n` +
|
|
198
|
-
` --header "Authorization: Bearer <token>" (static bearer token)`);
|
|
199
|
-
}
|
|
200
|
-
let profileName;
|
|
201
|
-
if (serverConfig.url) {
|
|
202
|
-
if (options.noProfile) {
|
|
203
|
-
logger.debug('Skipping OAuth profile: --no-profile specified');
|
|
204
|
-
}
|
|
205
|
-
else if (hasExplicitAuthHeader) {
|
|
206
|
-
logger.debug('Skipping OAuth profile auto-detection: explicit Authorization header provided via --header');
|
|
207
|
-
}
|
|
208
|
-
else if (options.x402 && !options.profile) {
|
|
209
|
-
logger.debug('Skipping OAuth profile auto-detection: --x402 specified');
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
profileName = await resolveAuthProfile(serverConfig.url, target, options.profile, {
|
|
213
|
-
sessionName: name,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
let headers;
|
|
218
|
-
if (Object.keys(serverConfig.headers || {}).length > 0) {
|
|
219
|
-
headers = { ...serverConfig.headers };
|
|
220
|
-
if (Object.keys(headers).length > 0) {
|
|
221
|
-
logger.debug(`Storing ${Object.keys(headers).length} headers for session ${name} in keychain`);
|
|
222
|
-
await storeKeychainSessionHeaders(name, headers);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
headers = undefined;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (options.proxyBearerToken) {
|
|
229
|
-
logger.debug(`Storing proxy bearer token for session ${name} in keychain`);
|
|
230
|
-
await storeKeychainProxyBearerToken(name, options.proxyBearerToken);
|
|
231
|
-
}
|
|
232
|
-
if (options.x402) {
|
|
233
|
-
const wallet = await getWallet();
|
|
234
|
-
if (!wallet) {
|
|
235
|
-
throw new ClientError('x402 wallet not found. Create one with: mcpc x402 init');
|
|
236
|
-
}
|
|
237
|
-
logger.debug(`Using x402 wallet: ${wallet.address}`);
|
|
238
|
-
}
|
|
239
|
-
const isReconnect = !!existingSession;
|
|
240
|
-
const { headers: _originalHeaders, ...baseTransportConfig } = serverConfig;
|
|
241
|
-
const sessionTransportConfig = {
|
|
242
|
-
...baseTransportConfig,
|
|
243
|
-
...(headers && { headers: redactHeaders(headers) }),
|
|
244
|
-
};
|
|
245
|
-
const sessionUpdate = {
|
|
246
|
-
server: sessionTransportConfig,
|
|
247
|
-
...(profileName && { profileName }),
|
|
248
|
-
...(proxyConfig && { proxy: proxyConfig }),
|
|
249
|
-
...(options.x402 && { x402: options.x402 }),
|
|
250
|
-
...(options.insecure && { insecure: true }),
|
|
251
|
-
...(isReconnect && { status: 'active' }),
|
|
252
|
-
};
|
|
253
|
-
if (isReconnect) {
|
|
254
|
-
await updateSession(name, sessionUpdate);
|
|
255
|
-
logger.debug(`Session record updated for reconnect: ${name}`);
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
await saveSession(name, {
|
|
259
|
-
server: sessionTransportConfig,
|
|
260
|
-
createdAt: new Date().toISOString(),
|
|
261
|
-
status: 'connecting',
|
|
262
|
-
lastConnectionAttemptAt: new Date().toISOString(),
|
|
263
|
-
...sessionUpdate,
|
|
264
|
-
});
|
|
265
|
-
logger.debug(`Initial session record created for: ${name}`);
|
|
266
|
-
}
|
|
267
|
-
try {
|
|
268
|
-
const bridgeOptions = {
|
|
269
|
-
sessionName: name,
|
|
270
|
-
serverConfig: serverConfig,
|
|
271
|
-
verbose: options.verbose || false,
|
|
272
|
-
};
|
|
273
|
-
if (headers) {
|
|
274
|
-
bridgeOptions.headers = headers;
|
|
275
|
-
}
|
|
276
|
-
if (profileName) {
|
|
277
|
-
bridgeOptions.profileName = profileName;
|
|
278
|
-
}
|
|
279
|
-
if (proxyConfig) {
|
|
280
|
-
bridgeOptions.proxyConfig = proxyConfig;
|
|
281
|
-
}
|
|
282
|
-
if (options.x402) {
|
|
283
|
-
bridgeOptions.x402 = options.x402;
|
|
284
|
-
}
|
|
285
|
-
if (options.insecure) {
|
|
286
|
-
bridgeOptions.insecure = true;
|
|
287
|
-
}
|
|
288
|
-
const { pid } = await startBridge(bridgeOptions);
|
|
289
|
-
await updateSession(name, { pid, status: 'active' });
|
|
290
|
-
logger.debug(`Session ${name} updated with bridge PID: ${pid}`);
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
logger.debug(`Bridge start failed, cleaning up session ${name}`);
|
|
294
|
-
if (!isReconnect) {
|
|
295
|
-
try {
|
|
296
|
-
await deleteSession(name);
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
throw error;
|
|
302
|
-
}
|
|
303
|
-
if (options.skipDetails) {
|
|
304
|
-
if (options.outputMode === 'human' && !options.quiet) {
|
|
305
|
-
console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
if (options.outputMode === 'json') {
|
|
311
|
-
const entry = await buildConnectResultEntry(name, 'created', {
|
|
312
|
-
...(options.verbose && { verbose: options.verbose }),
|
|
313
|
-
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
314
|
-
});
|
|
315
|
-
console.log(formatOutput([entry], 'json'));
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
await showServerDetails(name, {
|
|
319
|
-
...options,
|
|
320
|
-
hideTarget: false,
|
|
321
|
-
});
|
|
322
|
-
console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
catch (detailsError) {
|
|
326
|
-
if (detailsError instanceof AuthError) {
|
|
327
|
-
throw detailsError;
|
|
328
|
-
}
|
|
329
|
-
if (detailsError instanceof Error && isAuthenticationError(detailsError.message)) {
|
|
330
|
-
throw createServerAuthError(serverConfig.url || target, { sessionName: name });
|
|
331
|
-
}
|
|
332
|
-
const errorMsg = detailsError instanceof Error ? detailsError.message : String(detailsError);
|
|
333
|
-
if (options.outputMode === 'json') {
|
|
334
|
-
const failed = {
|
|
335
|
-
_mcpc: {
|
|
336
|
-
sessionName: name,
|
|
337
|
-
status: 'failed',
|
|
338
|
-
error: errorMsg,
|
|
339
|
-
},
|
|
340
|
-
};
|
|
341
|
-
console.log(formatOutput([failed], 'json'));
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
console.log(formatWarning(`Session ${name} created but server is not responding: ${errorMsg}\n` +
|
|
345
|
-
` The session will auto-recover when the server becomes available.\n` +
|
|
346
|
-
` Check status with: mcpc ${name}`));
|
|
347
|
-
}
|
|
348
|
-
logger.debug(`showServerDetails failed for new session ${name}: ${errorMsg}`);
|
|
349
|
-
}
|
|
12
|
+
export function statelessField(connectionMode) {
|
|
13
|
+
if (connectionMode === 'stateless')
|
|
14
|
+
return { stateless: true };
|
|
15
|
+
if (connectionMode === 'stateful')
|
|
16
|
+
return { stateless: false };
|
|
17
|
+
return { stateless: null };
|
|
350
18
|
}
|
|
351
19
|
export function getBridgeStatus(session) {
|
|
352
20
|
if (session.status === 'unauthorized') {
|
|
@@ -420,9 +88,10 @@ export async function listSessionsAndAuthProfiles(options) {
|
|
|
420
88
|
reconnectCrashedSessions(consolidateResult.sessionsToRestart);
|
|
421
89
|
const profiles = await listAuthProfiles();
|
|
422
90
|
if (options.outputMode === 'json') {
|
|
423
|
-
const sessionsWithStatus = sessions.map((session) => ({
|
|
91
|
+
const sessionsWithStatus = sessions.map(({ connectionMode, ...session }) => ({
|
|
424
92
|
...session,
|
|
425
93
|
status: getBridgeStatus(session),
|
|
94
|
+
...statelessField(connectionMode),
|
|
426
95
|
}));
|
|
427
96
|
console.log(formatOutput({
|
|
428
97
|
sessions: sessionsWithStatus,
|
|
@@ -522,7 +191,7 @@ export async function closeSession(name, options) {
|
|
|
522
191
|
export async function showServerDetails(target, options) {
|
|
523
192
|
await withMcpClient(target, options, async (client, context) => {
|
|
524
193
|
const serverDetails = await client.getServerDetails();
|
|
525
|
-
const { serverInfo, capabilities, instructions, protocolVersion,
|
|
194
|
+
const { serverInfo, capabilities, instructions, protocolVersion, connectionMode } = serverDetails;
|
|
526
195
|
const cachedToolsResult = await client.listAllTools();
|
|
527
196
|
const tools = cachedToolsResult.tools;
|
|
528
197
|
if (options.outputMode === 'human') {
|
|
@@ -536,24 +205,16 @@ export async function showServerDetails(target, options) {
|
|
|
536
205
|
}),
|
|
537
206
|
};
|
|
538
207
|
let logPath;
|
|
539
|
-
let logSize;
|
|
540
208
|
if (target.startsWith('@')) {
|
|
541
209
|
logPath = getBridgeLogPath(target);
|
|
542
|
-
try {
|
|
543
|
-
const st = await stat(logPath);
|
|
544
|
-
logSize = st.size;
|
|
545
|
-
}
|
|
546
|
-
catch {
|
|
547
|
-
}
|
|
548
210
|
}
|
|
549
211
|
console.log(formatOutput({
|
|
550
212
|
_mcpc: {
|
|
551
213
|
sessionName: context.sessionName,
|
|
552
214
|
profileName: context.profileName,
|
|
553
215
|
server,
|
|
554
|
-
...(
|
|
216
|
+
...statelessField(connectionMode),
|
|
555
217
|
...(logPath && { logPath }),
|
|
556
|
-
...(logSize !== undefined && { logSize }),
|
|
557
218
|
},
|
|
558
219
|
protocolVersion,
|
|
559
220
|
capabilities,
|
|
@@ -584,14 +245,6 @@ export async function restartSession(name, options) {
|
|
|
584
245
|
}
|
|
585
246
|
const { readKeychainSessionHeaders } = await import('../../lib/auth/keychain.js');
|
|
586
247
|
const headers = await readKeychainSessionHeaders(name);
|
|
587
|
-
const bridgeOptions = {
|
|
588
|
-
sessionName: name,
|
|
589
|
-
serverConfig: { ...serverConfig, ...(headers && { headers }) },
|
|
590
|
-
verbose: options.verbose || false,
|
|
591
|
-
};
|
|
592
|
-
if (headers) {
|
|
593
|
-
bridgeOptions.headers = headers;
|
|
594
|
-
}
|
|
595
248
|
const hasExplicitAuthHeader = headers?.Authorization !== undefined;
|
|
596
249
|
let profileName = session.profileName;
|
|
597
250
|
if (!profileName && serverConfig.url && !hasExplicitAuthHeader && !session.x402) {
|
|
@@ -603,18 +256,16 @@ export async function restartSession(name, options) {
|
|
|
603
256
|
await updateSession(name, { profileName });
|
|
604
257
|
}
|
|
605
258
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
bridgeOptions.insecure = session.insecure;
|
|
617
|
-
}
|
|
259
|
+
const bridgeOptions = {
|
|
260
|
+
sessionName: name,
|
|
261
|
+
serverConfig: { ...serverConfig, ...(headers && { headers }) },
|
|
262
|
+
verbose: options.verbose || false,
|
|
263
|
+
...(headers && { headers }),
|
|
264
|
+
...(profileName && { profileName }),
|
|
265
|
+
...(session.proxy && { proxyConfig: session.proxy }),
|
|
266
|
+
...(session.x402 && { x402: session.x402 }),
|
|
267
|
+
...(session.insecure && { insecure: session.insecure }),
|
|
268
|
+
};
|
|
618
269
|
const { pid } = await startBridge(bridgeOptions);
|
|
619
270
|
await updateSession(name, { pid, status: 'active' });
|
|
620
271
|
logger.debug(`Session ${name} restarted with bridge PID: ${pid}`);
|
|
@@ -641,366 +292,6 @@ export async function restartSession(name, options) {
|
|
|
641
292
|
throw error;
|
|
642
293
|
}
|
|
643
294
|
}
|
|
644
|
-
async function bulkConnectEntries(entries, options, { printBadges = true } = {}) {
|
|
645
|
-
const liveSet = new Set();
|
|
646
|
-
for (const { sessionName } of entries) {
|
|
647
|
-
const session = await getSession(sessionName);
|
|
648
|
-
if (session && getBridgeStatus(session) === 'live') {
|
|
649
|
-
liveSet.add(sessionName);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
const settled = await Promise.allSettled(entries.map(async ({ entry, sessionName, configFile }) => connectSession(entry, sessionName, {
|
|
653
|
-
...options,
|
|
654
|
-
config: configFile,
|
|
655
|
-
skipDetails: true,
|
|
656
|
-
quiet: true,
|
|
657
|
-
})));
|
|
658
|
-
const results = settled.map((outcome, i) => {
|
|
659
|
-
const base = entries[i];
|
|
660
|
-
if (outcome.status === 'fulfilled') {
|
|
661
|
-
return { ...base, status: liveSet.has(base.sessionName) ? 'active' : 'created' };
|
|
662
|
-
}
|
|
663
|
-
const error = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
664
|
-
return { ...base, status: 'failed', error };
|
|
665
|
-
});
|
|
666
|
-
if (options.outputMode === 'human' && printBadges) {
|
|
667
|
-
for (const r of results) {
|
|
668
|
-
const name = theme.cyan(r.sessionName);
|
|
669
|
-
switch (r.status) {
|
|
670
|
-
case 'created':
|
|
671
|
-
console.log(` ${theme.yellow('●')} ${name} ${theme.yellow('connecting')}`);
|
|
672
|
-
break;
|
|
673
|
-
case 'active':
|
|
674
|
-
console.log(` ${theme.green('●')} ${name} ${chalk.dim('already active')}`);
|
|
675
|
-
break;
|
|
676
|
-
case 'failed':
|
|
677
|
-
console.log(` ${theme.red('●')} ${name} ${theme.red('failed')}${r.error ? chalk.dim(` — ${r.error}`) : ''}`);
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
return results;
|
|
683
|
-
}
|
|
684
|
-
function printBulkConnectSummary(results, options) {
|
|
685
|
-
const active = results.filter((r) => r.status === 'active').length;
|
|
686
|
-
const connecting = results.filter((r) => r.status === 'created').length;
|
|
687
|
-
const failed = results.filter((r) => r.status === 'failed').length;
|
|
688
|
-
if (options.outputMode === 'human' && results.length > 1) {
|
|
689
|
-
const parts = [];
|
|
690
|
-
if (active > 0)
|
|
691
|
-
parts.push(`${active} already active`);
|
|
692
|
-
if (connecting > 0)
|
|
693
|
-
parts.push(`${connecting} connecting`);
|
|
694
|
-
if (failed > 0)
|
|
695
|
-
parts.push(`${failed} failed`);
|
|
696
|
-
const summary = parts.join(', ');
|
|
697
|
-
if (failed === 0) {
|
|
698
|
-
console.log(formatSuccess(summary));
|
|
699
|
-
}
|
|
700
|
-
else if (active + connecting > 0) {
|
|
701
|
-
console.log(formatWarning(summary));
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
return { active, connecting, failed };
|
|
705
|
-
}
|
|
706
|
-
export async function connectAllFromConfig(configFile, options) {
|
|
707
|
-
const config = loadConfig(configFile);
|
|
708
|
-
const allNames = listServers(config);
|
|
709
|
-
if (allNames.length === 0) {
|
|
710
|
-
throw new ClientError(`No servers found in config file: ${configFile}`);
|
|
711
|
-
}
|
|
712
|
-
const stdioSkipped = [];
|
|
713
|
-
const serverNames = allNames.filter((name) => {
|
|
714
|
-
if (!options.stdio && isStdioEntry(config, name)) {
|
|
715
|
-
stdioSkipped.push(name);
|
|
716
|
-
return false;
|
|
717
|
-
}
|
|
718
|
-
return true;
|
|
719
|
-
});
|
|
720
|
-
if (serverNames.length === 0) {
|
|
721
|
-
if (options.outputMode === 'json') {
|
|
722
|
-
const skippedEntries = stdioSkipped.map((entry) => ({
|
|
723
|
-
_mcpc: {
|
|
724
|
-
sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
|
|
725
|
-
configFile,
|
|
726
|
-
entry,
|
|
727
|
-
status: 'skipped',
|
|
728
|
-
skipReason: 'stdio',
|
|
729
|
-
},
|
|
730
|
-
}));
|
|
731
|
-
console.log(formatOutput(skippedEntries, 'json'));
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
throw new ClientError(`All ${allNames.length} server${allNames.length === 1 ? '' : 's'} in ${configFile} use stdio transport.\n` +
|
|
735
|
-
`Pass --stdio to include them: mcpc connect ${configFile} --stdio`);
|
|
736
|
-
}
|
|
737
|
-
if (options.outputMode === 'human') {
|
|
738
|
-
console.log(theme.cyan(`Connecting ${serverNames.length} server${serverNames.length === 1 ? '' : 's'} from ${configFile}...`));
|
|
739
|
-
if (stdioSkipped.length > 0) {
|
|
740
|
-
console.log(chalk.dim(` skipping ${stdioSkipped.length} stdio server${stdioSkipped.length === 1 ? '' : 's'} ` +
|
|
741
|
-
`(${stdioSkipped.join(', ')}), pass --stdio to include`));
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
const entries = serverNames.map((entry) => ({
|
|
745
|
-
configFile,
|
|
746
|
-
entry,
|
|
747
|
-
sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
|
|
748
|
-
}));
|
|
749
|
-
const results = await bulkConnectEntries(entries, options);
|
|
750
|
-
if (options.outputMode === 'json') {
|
|
751
|
-
const resultEntries = await buildBulkConnectEntries(results, options);
|
|
752
|
-
const skippedEntries = stdioSkipped.map((entry) => ({
|
|
753
|
-
_mcpc: {
|
|
754
|
-
sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
|
|
755
|
-
configFile,
|
|
756
|
-
entry,
|
|
757
|
-
status: 'skipped',
|
|
758
|
-
skipReason: 'stdio',
|
|
759
|
-
},
|
|
760
|
-
}));
|
|
761
|
-
console.log(formatOutput([...resultEntries, ...skippedEntries], 'json'));
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
const { active, connecting, failed } = printBulkConnectSummary(results, options);
|
|
765
|
-
if (active + connecting === 0 && failed > 0) {
|
|
766
|
-
throw new ClientError(`Failed to connect any servers from ${configFile}`);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
async function buildBulkConnectEntries(results, options) {
|
|
770
|
-
return await Promise.all(results.map(async (r) => {
|
|
771
|
-
if (r.status === 'failed') {
|
|
772
|
-
return {
|
|
773
|
-
_mcpc: {
|
|
774
|
-
sessionName: r.sessionName,
|
|
775
|
-
configFile: r.configFile,
|
|
776
|
-
entry: r.entry,
|
|
777
|
-
status: 'failed',
|
|
778
|
-
...(r.error && { error: r.error }),
|
|
779
|
-
},
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
try {
|
|
783
|
-
return await buildConnectResultEntry(r.sessionName, r.status, {
|
|
784
|
-
...(options.verbose && { verbose: options.verbose }),
|
|
785
|
-
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
786
|
-
configFile: r.configFile,
|
|
787
|
-
entry: r.entry,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
catch (err) {
|
|
791
|
-
return {
|
|
792
|
-
_mcpc: {
|
|
793
|
-
sessionName: r.sessionName,
|
|
794
|
-
configFile: r.configFile,
|
|
795
|
-
entry: r.entry,
|
|
796
|
-
status: 'failed',
|
|
797
|
-
error: err instanceof Error ? err.message : String(err),
|
|
798
|
-
},
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
}));
|
|
802
|
-
}
|
|
803
|
-
function aggregateDiscoveredEntries(discovered, options) {
|
|
804
|
-
const entries = [];
|
|
805
|
-
const skippedDuplicates = [];
|
|
806
|
-
const skippedStdio = [];
|
|
807
|
-
const seenNames = new Set();
|
|
808
|
-
for (const d of discovered) {
|
|
809
|
-
for (const entry of Object.keys(d.config.mcpServers)) {
|
|
810
|
-
const sessionName = generateSessionName({ type: 'config', file: d.path, entry });
|
|
811
|
-
if (!options.stdio && isStdioEntry(d.config, entry)) {
|
|
812
|
-
skippedStdio.push({ configFile: d.path, entry, sessionName });
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
if (seenNames.has(sessionName)) {
|
|
816
|
-
skippedDuplicates.push({ configFile: d.path, entry, sessionName });
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
seenNames.add(sessionName);
|
|
820
|
-
entries.push({
|
|
821
|
-
configFile: d.path,
|
|
822
|
-
entry,
|
|
823
|
-
sessionName,
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return { entries, skippedDuplicates, skippedStdio };
|
|
828
|
-
}
|
|
829
|
-
function buildNoServersError(scan) {
|
|
830
|
-
if (scan.empty.length > 0 || scan.errors.length > 0) {
|
|
831
|
-
const lines = [];
|
|
832
|
-
if (scan.empty.length > 0) {
|
|
833
|
-
lines.push(scan.empty.length === 1
|
|
834
|
-
? `Found a config file, but it defines no servers:`
|
|
835
|
-
: `Found config files, but they define no servers:`);
|
|
836
|
-
for (const c of scan.empty)
|
|
837
|
-
lines.push(` ${formatPath(c.path)}`);
|
|
838
|
-
}
|
|
839
|
-
if (scan.errors.length > 0) {
|
|
840
|
-
if (lines.length > 0)
|
|
841
|
-
lines.push('');
|
|
842
|
-
lines.push(scan.errors.length === 1
|
|
843
|
-
? `Found a config file, but it couldn't be used:`
|
|
844
|
-
: `Found config files, but they couldn't be used:`);
|
|
845
|
-
for (const c of scan.errors)
|
|
846
|
-
lines.push(` ${formatPath(c.path)} — ${c.error}`);
|
|
847
|
-
}
|
|
848
|
-
return (`No MCP servers to connect.\n\n` +
|
|
849
|
-
`${lines.join('\n')}\n\n` +
|
|
850
|
-
`Add a server under "mcpServers" and re-run mcpc connect, or connect one now:\n` +
|
|
851
|
-
` mcpc connect mcp.example.com @myserver`);
|
|
852
|
-
}
|
|
853
|
-
const searchPaths = getStandardMcpConfigPaths()
|
|
854
|
-
.map((c) => ` ${formatPath(c.path)}`)
|
|
855
|
-
.join('\n');
|
|
856
|
-
return (`No MCP config files found in standard locations.\n\n` +
|
|
857
|
-
`Searched:\n${searchPaths}\n\n` +
|
|
858
|
-
`Connect a specific server: mcpc connect mcp.example.com\n` +
|
|
859
|
-
`Connect from a specific file: mcpc connect /path/to/mcp.json`);
|
|
860
|
-
}
|
|
861
|
-
export async function connectAllFromStandardConfigs(options) {
|
|
862
|
-
const scan = scanMcpConfigFiles();
|
|
863
|
-
const { discovered } = scan;
|
|
864
|
-
const hasApifyToken = !!process.env.APIFY_API_TOKEN;
|
|
865
|
-
if (discovered.length === 0 && !hasApifyToken) {
|
|
866
|
-
if (options.outputMode === 'json') {
|
|
867
|
-
console.log(formatOutput([], 'json'));
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
throw new ClientError(buildNoServersError(scan));
|
|
871
|
-
}
|
|
872
|
-
if (discovered.length === 0) {
|
|
873
|
-
await maybeConnectApify([], [], options);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
const { entries, skippedDuplicates, skippedStdio } = aggregateDiscoveredEntries(discovered, {
|
|
877
|
-
...(options.stdio && { stdio: true }),
|
|
878
|
-
});
|
|
879
|
-
const results = entries.length > 0 ? await bulkConnectEntries(entries, options, { printBadges: false }) : [];
|
|
880
|
-
if (options.outputMode === 'json') {
|
|
881
|
-
const toSkipped = (s, skipReason) => ({
|
|
882
|
-
_mcpc: {
|
|
883
|
-
sessionName: s.sessionName,
|
|
884
|
-
configFile: s.configFile,
|
|
885
|
-
entry: s.entry,
|
|
886
|
-
status: 'skipped',
|
|
887
|
-
skipReason,
|
|
888
|
-
},
|
|
889
|
-
});
|
|
890
|
-
const skippedJsonEntries = [
|
|
891
|
-
...skippedStdio.map((s) => toSkipped(s, 'stdio')),
|
|
892
|
-
...skippedDuplicates.map((s) => toSkipped(s, 'duplicate')),
|
|
893
|
-
];
|
|
894
|
-
if (entries.length === 0) {
|
|
895
|
-
if (!hasApifyToken) {
|
|
896
|
-
console.log(formatOutput(skippedJsonEntries, 'json'));
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
await maybeConnectApify([], [], options);
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
const resultEntries = await buildBulkConnectEntries(results, options);
|
|
903
|
-
console.log(formatOutput([...resultEntries, ...skippedJsonEntries], 'json'));
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
const totalEntries = entries.length + skippedDuplicates.length + skippedStdio.length;
|
|
907
|
-
const fileCount = discovered.length + scan.empty.length + scan.errors.length;
|
|
908
|
-
console.log(theme.cyan(`Found ${fileCount} MCP config file${fileCount === 1 ? '' : 's'} ` +
|
|
909
|
-
`with ${totalEntries} server${totalEntries === 1 ? '' : 's'}:`));
|
|
910
|
-
const statusByName = new Map(results.map((r) => [r.sessionName, r]));
|
|
911
|
-
const liveSkippedStdio = new Set();
|
|
912
|
-
for (const s of skippedStdio) {
|
|
913
|
-
const session = await getSession(s.sessionName);
|
|
914
|
-
if (session && getBridgeStatus(session) === 'live') {
|
|
915
|
-
liveSkippedStdio.add(s.sessionName);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
const unconnectedStdio = skippedStdio.length - liveSkippedStdio.size;
|
|
919
|
-
for (const d of discovered) {
|
|
920
|
-
console.log(` ${formatPath(d.path)} ${chalk.dim(`(${d.serverCount} server${d.serverCount === 1 ? '' : 's'})`)}`);
|
|
921
|
-
for (const entryName of Object.keys(d.config.mcpServers)) {
|
|
922
|
-
const sessionName = generateSessionName({ type: 'config', file: d.path, entry: entryName });
|
|
923
|
-
const serverCfg = d.config.mcpServers[entryName];
|
|
924
|
-
const target = serverCfg?.url ?? [serverCfg?.command, ...(serverCfg?.args ?? [])].join(' ');
|
|
925
|
-
const truncated = target && target.length > 72 ? target.slice(0, 72) + '…' : target;
|
|
926
|
-
let marker;
|
|
927
|
-
if (skippedStdio.some((s) => s.configFile === d.path && s.entry === entryName)) {
|
|
928
|
-
marker = liveSkippedStdio.has(sessionName)
|
|
929
|
-
? formatConnectStatusBadge('active')
|
|
930
|
-
: theme.yellow('○ skipped (stdio)');
|
|
931
|
-
}
|
|
932
|
-
else if (skippedDuplicates.some((s) => s.configFile === d.path && s.entry === entryName)) {
|
|
933
|
-
marker = chalk.dim('○ skipped (duplicate)');
|
|
934
|
-
}
|
|
935
|
-
else {
|
|
936
|
-
const r = statusByName.get(sessionName);
|
|
937
|
-
marker = r ? formatConnectStatusBadge(r.status, r.error) : '';
|
|
938
|
-
}
|
|
939
|
-
console.log(` ${theme.cyan(sessionName)} → ${chalk.dim(truncated ?? entryName)}${marker ? ` ${marker}` : ''}`);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
for (const c of scan.empty) {
|
|
943
|
-
console.log(` ${formatPath(c.path)} ${chalk.dim('(0 servers)')}`);
|
|
944
|
-
}
|
|
945
|
-
for (const c of scan.errors) {
|
|
946
|
-
console.log(` ${formatPath(c.path)} ${chalk.dim('(invalid)')}`);
|
|
947
|
-
console.log(` ${chalk.dim(c.error)}`);
|
|
948
|
-
}
|
|
949
|
-
if (entries.length === 0 && !hasApifyToken && liveSkippedStdio.size === 0) {
|
|
950
|
-
throw new ClientError(`All servers in discovered config files use stdio transport.\n` +
|
|
951
|
-
`Pass --stdio to include them: mcpc connect --stdio`);
|
|
952
|
-
}
|
|
953
|
-
if (unconnectedStdio > 0) {
|
|
954
|
-
console.log('\nTo include stdio servers, run: mcpc connect --stdio');
|
|
955
|
-
}
|
|
956
|
-
await maybeConnectApify(entries, results, options);
|
|
957
|
-
const failed = results.filter((r) => r.status === 'failed').length;
|
|
958
|
-
const succeeded = results.filter((r) => r.status === 'active' || r.status === 'created').length;
|
|
959
|
-
if (entries.length > 0 && succeeded === 0 && failed > 0) {
|
|
960
|
-
throw new ClientError(`Failed to connect any servers from discovered config files`);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
const APIFY_MCP_URL = 'https://mcp.apify.com';
|
|
964
|
-
const APIFY_SESSION_NAME = '@apify';
|
|
965
|
-
async function maybeConnectApify(configEntries, configResults, options) {
|
|
966
|
-
const token = process.env.APIFY_API_TOKEN;
|
|
967
|
-
if (!token)
|
|
968
|
-
return;
|
|
969
|
-
if (configEntries.some((e) => e.sessionName === APIFY_SESSION_NAME))
|
|
970
|
-
return;
|
|
971
|
-
if (configResults.some((r) => r.sessionName === APIFY_SESSION_NAME))
|
|
972
|
-
return;
|
|
973
|
-
const existing = await getSession(APIFY_SESSION_NAME);
|
|
974
|
-
const isLive = existing && getBridgeStatus(existing) === 'live';
|
|
975
|
-
if (options.outputMode === 'human') {
|
|
976
|
-
console.log(theme.cyan(`\nAPIFY_API_TOKEN detected, connecting to ${APIFY_MCP_URL}...`));
|
|
977
|
-
}
|
|
978
|
-
if (isLive) {
|
|
979
|
-
if (options.outputMode === 'human') {
|
|
980
|
-
console.log(` ${theme.green('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.green('live')}`);
|
|
981
|
-
}
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
try {
|
|
985
|
-
await connectSession(APIFY_MCP_URL, APIFY_SESSION_NAME, {
|
|
986
|
-
outputMode: options.outputMode,
|
|
987
|
-
...(options.verbose && { verbose: true }),
|
|
988
|
-
headers: [`Authorization: Bearer ${token}`],
|
|
989
|
-
skipDetails: true,
|
|
990
|
-
quiet: true,
|
|
991
|
-
noProfile: true,
|
|
992
|
-
});
|
|
993
|
-
if (options.outputMode === 'human') {
|
|
994
|
-
console.log(` ${theme.yellow('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.yellow('connecting')}`);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
catch (error) {
|
|
998
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
999
|
-
if (options.outputMode === 'human') {
|
|
1000
|
-
console.log(` ${theme.red('●')} ${theme.cyan(APIFY_SESSION_NAME)} ${theme.red('failed')}${chalk.dim(` — ${msg}`)}`);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
295
|
export async function openShell(target) {
|
|
1005
296
|
console.error(formatWarning('The "shell" command is deprecated and will be removed in a future release.'));
|
|
1006
297
|
const { startShell } = await import('../shell.js');
|