ms365-mcp-server 1.0.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 +497 -0
- package/bin/cli.js +247 -0
- package/dist/index.js +1251 -0
- package/dist/utils/api.js +43 -0
- package/dist/utils/credential-store.js +258 -0
- package/dist/utils/ms365-auth-enhanced.js +639 -0
- package/dist/utils/ms365-auth.js +363 -0
- package/dist/utils/ms365-operations.js +644 -0
- package/dist/utils/multi-user-auth.js +359 -0
- package/install.js +41 -0
- package/package.json +59 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { ConfidentialClientApplication } from '@azure/msal-node';
|
|
2
|
+
import { Client } from '@microsoft/microsoft-graph-client';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import { URL } from 'url';
|
|
8
|
+
import { logger } from './api.js';
|
|
9
|
+
import { createHash, randomBytes } from 'crypto';
|
|
10
|
+
// Scopes required for Microsoft 365 operations
|
|
11
|
+
const SCOPES = [
|
|
12
|
+
'https://graph.microsoft.com/Mail.ReadWrite',
|
|
13
|
+
'https://graph.microsoft.com/Mail.Send',
|
|
14
|
+
'https://graph.microsoft.com/MailboxSettings.Read',
|
|
15
|
+
'https://graph.microsoft.com/Contacts.Read',
|
|
16
|
+
'https://graph.microsoft.com/User.Read',
|
|
17
|
+
'offline_access'
|
|
18
|
+
];
|
|
19
|
+
// Configuration directory
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
|
|
21
|
+
/**
|
|
22
|
+
* Multi-user Microsoft 365 authentication manager
|
|
23
|
+
*/
|
|
24
|
+
export class MultiUserMS365Auth {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.sessions = new Map();
|
|
27
|
+
this.credentials = null;
|
|
28
|
+
this.authServers = new Map();
|
|
29
|
+
this.ensureConfigDir();
|
|
30
|
+
this.loadCredentials();
|
|
31
|
+
this.loadExistingSessions();
|
|
32
|
+
}
|
|
33
|
+
ensureConfigDir() {
|
|
34
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
35
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load credentials from environment or file
|
|
40
|
+
*/
|
|
41
|
+
loadCredentials() {
|
|
42
|
+
try {
|
|
43
|
+
// Try environment variables first
|
|
44
|
+
if (process.env.MS365_CLIENT_ID && process.env.MS365_CLIENT_SECRET && process.env.MS365_TENANT_ID) {
|
|
45
|
+
this.credentials = {
|
|
46
|
+
clientId: process.env.MS365_CLIENT_ID,
|
|
47
|
+
clientSecret: process.env.MS365_CLIENT_SECRET,
|
|
48
|
+
tenantId: process.env.MS365_TENANT_ID,
|
|
49
|
+
redirectUri: process.env.MS365_REDIRECT_URI || 'http://localhost:44001/oauth2callback'
|
|
50
|
+
};
|
|
51
|
+
logger.log('Loaded MS365 credentials from environment variables');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Try credentials file
|
|
55
|
+
const credentialsFile = path.join(CONFIG_DIR, 'credentials.json');
|
|
56
|
+
if (fs.existsSync(credentialsFile)) {
|
|
57
|
+
const credentialsData = fs.readFileSync(credentialsFile, 'utf8');
|
|
58
|
+
this.credentials = JSON.parse(credentialsData);
|
|
59
|
+
logger.log('Loaded MS365 credentials from file');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.error('Error loading MS365 credentials:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load existing user sessions
|
|
68
|
+
*/
|
|
69
|
+
loadExistingSessions() {
|
|
70
|
+
try {
|
|
71
|
+
const sessionsFile = path.join(CONFIG_DIR, 'user-sessions.json');
|
|
72
|
+
if (fs.existsSync(sessionsFile)) {
|
|
73
|
+
const sessionsData = fs.readFileSync(sessionsFile, 'utf8');
|
|
74
|
+
const sessions = JSON.parse(sessionsData);
|
|
75
|
+
for (const [userId, session] of Object.entries(sessions)) {
|
|
76
|
+
this.sessions.set(userId, session);
|
|
77
|
+
}
|
|
78
|
+
logger.log(`Loaded ${this.sessions.size} existing user sessions`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
logger.error('Error loading existing sessions:', error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Save user sessions to file
|
|
87
|
+
*/
|
|
88
|
+
saveUserSessions() {
|
|
89
|
+
try {
|
|
90
|
+
const sessionsFile = path.join(CONFIG_DIR, 'user-sessions.json');
|
|
91
|
+
const sessionsObj = Object.fromEntries(this.sessions);
|
|
92
|
+
fs.writeFileSync(sessionsFile, JSON.stringify(sessionsObj, null, 2));
|
|
93
|
+
logger.log('Saved user sessions');
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
logger.error('Error saving user sessions:', error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generate a unique user ID
|
|
101
|
+
*/
|
|
102
|
+
generateUserId(userEmail) {
|
|
103
|
+
const timestamp = Date.now().toString();
|
|
104
|
+
const random = randomBytes(8).toString('hex');
|
|
105
|
+
if (userEmail) {
|
|
106
|
+
const emailHash = createHash('sha256').update(userEmail).digest('hex').substring(0, 8);
|
|
107
|
+
return `user_${emailHash}_${timestamp.substring(-6)}_${random.substring(0, 4)}`;
|
|
108
|
+
}
|
|
109
|
+
return `user_${timestamp}_${random}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create MSAL client for a user
|
|
113
|
+
*/
|
|
114
|
+
createMsalClient(redirectUri) {
|
|
115
|
+
if (!this.credentials) {
|
|
116
|
+
throw new Error('MS365 credentials not configured');
|
|
117
|
+
}
|
|
118
|
+
const config = {
|
|
119
|
+
auth: {
|
|
120
|
+
clientId: this.credentials.clientId,
|
|
121
|
+
clientSecret: this.credentials.clientSecret,
|
|
122
|
+
authority: `https://login.microsoftonline.com/${this.credentials.tenantId}`
|
|
123
|
+
},
|
|
124
|
+
system: {
|
|
125
|
+
loggerOptions: {
|
|
126
|
+
loggerCallback: (level, message, containsPii) => {
|
|
127
|
+
if (!containsPii) {
|
|
128
|
+
logger.log(`MSAL: ${message}`);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
piiLoggingEnabled: false,
|
|
132
|
+
logLevel: 3 // Error level
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
return new ConfidentialClientApplication(config);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Start authentication flow for a new user
|
|
140
|
+
*/
|
|
141
|
+
async authenticateNewUser(userEmail) {
|
|
142
|
+
if (!this.credentials) {
|
|
143
|
+
throw new Error('MS365 credentials not configured. Please set MS365_CLIENT_ID, MS365_CLIENT_SECRET, and MS365_TENANT_ID environment variables.');
|
|
144
|
+
}
|
|
145
|
+
const userId = this.generateUserId(userEmail);
|
|
146
|
+
// Find available port
|
|
147
|
+
const port = await this.findAvailablePort();
|
|
148
|
+
const redirectUri = `http://localhost:${port}/oauth2callback`;
|
|
149
|
+
// Create MSAL client with specific redirect URI
|
|
150
|
+
const msalClient = this.createMsalClient(redirectUri);
|
|
151
|
+
// Generate authentication URL
|
|
152
|
+
const authUrl = await msalClient.getAuthCodeUrl({
|
|
153
|
+
scopes: SCOPES,
|
|
154
|
+
redirectUri: redirectUri,
|
|
155
|
+
prompt: 'consent',
|
|
156
|
+
state: userId // Include user ID in state for identification
|
|
157
|
+
});
|
|
158
|
+
// Start callback server for this user
|
|
159
|
+
await this.startCallbackServer(userId, msalClient, port, redirectUri);
|
|
160
|
+
logger.log(`Started authentication flow for user ${userId} on port ${port}`);
|
|
161
|
+
return { userId, authUrl, port };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Find an available port for OAuth callback
|
|
165
|
+
*/
|
|
166
|
+
async findAvailablePort(startPort = 44001) {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const server = createServer();
|
|
169
|
+
server.listen(startPort, () => {
|
|
170
|
+
const port = server.address()?.port || startPort;
|
|
171
|
+
server.close();
|
|
172
|
+
resolve(port);
|
|
173
|
+
});
|
|
174
|
+
server.on('error', () => {
|
|
175
|
+
resolve(this.findAvailablePort(startPort + 1));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Start callback server for OAuth2 authentication
|
|
181
|
+
*/
|
|
182
|
+
async startCallbackServer(userId, msalClient, port, redirectUri) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const server = createServer(async (req, res) => {
|
|
185
|
+
if (req.url?.startsWith('/oauth2callback')) {
|
|
186
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
187
|
+
const code = url.searchParams.get('code');
|
|
188
|
+
const state = url.searchParams.get('state');
|
|
189
|
+
const error = url.searchParams.get('error');
|
|
190
|
+
if (error) {
|
|
191
|
+
res.end(`<html><body><h1>Authentication Error</h1><p>${error}</p></body></html>`);
|
|
192
|
+
server.close();
|
|
193
|
+
reject(new Error(`OAuth2 error: ${error}`));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (code && state === userId) {
|
|
197
|
+
try {
|
|
198
|
+
// Exchange code for token
|
|
199
|
+
const tokenResponse = await msalClient.acquireTokenByCode({
|
|
200
|
+
code: code,
|
|
201
|
+
scopes: SCOPES,
|
|
202
|
+
redirectUri: redirectUri
|
|
203
|
+
});
|
|
204
|
+
if (tokenResponse) {
|
|
205
|
+
// Save user session
|
|
206
|
+
const userSession = {
|
|
207
|
+
userId: userId,
|
|
208
|
+
userEmail: tokenResponse.account?.username,
|
|
209
|
+
accessToken: tokenResponse.accessToken,
|
|
210
|
+
refreshToken: '', // MSAL handles refresh tokens internally
|
|
211
|
+
expiresOn: tokenResponse.expiresOn?.getTime() || 0,
|
|
212
|
+
authenticated: true,
|
|
213
|
+
account: tokenResponse.account
|
|
214
|
+
};
|
|
215
|
+
this.sessions.set(userId, userSession);
|
|
216
|
+
this.saveUserSessions();
|
|
217
|
+
res.end(`<html><body><h1>Authentication Successful!</h1><p>User ID: ${userId}<br/>You can close this window and return to the application.</p></body></html>`);
|
|
218
|
+
server.close();
|
|
219
|
+
resolve();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (tokenError) {
|
|
224
|
+
logger.error(`Token exchange failed for user ${userId}:`, tokenError);
|
|
225
|
+
res.end(`<html><body><h1>Token Exchange Failed</h1><p>Please try again.</p></body></html>`);
|
|
226
|
+
server.close();
|
|
227
|
+
reject(tokenError);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
res.end('<html><body><h1>Invalid Request</h1></body></html>');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
res.end('<html><body><h1>MS365 MCP Server OAuth2</h1><p>Waiting for authentication...</p></body></html>');
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
this.authServers.set(userId, server);
|
|
238
|
+
server.listen(port, () => {
|
|
239
|
+
logger.log(`OAuth2 callback server started for user ${userId} on port ${port}`);
|
|
240
|
+
// Set timeout for authentication
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
if (this.authServers.has(userId)) {
|
|
243
|
+
server.close();
|
|
244
|
+
this.authServers.delete(userId);
|
|
245
|
+
logger.log(`Authentication timeout for user ${userId}`);
|
|
246
|
+
reject(new Error('Authentication timeout'));
|
|
247
|
+
}
|
|
248
|
+
}, 600000); // 10 minutes timeout
|
|
249
|
+
});
|
|
250
|
+
server.on('error', (err) => {
|
|
251
|
+
this.authServers.delete(userId);
|
|
252
|
+
reject(err);
|
|
253
|
+
});
|
|
254
|
+
server.on('close', () => {
|
|
255
|
+
this.authServers.delete(userId);
|
|
256
|
+
resolve();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get Microsoft Graph client for a specific user
|
|
262
|
+
*/
|
|
263
|
+
async getGraphClientForUser(userId) {
|
|
264
|
+
const session = this.sessions.get(userId);
|
|
265
|
+
if (!session) {
|
|
266
|
+
throw new Error(`User session not found: ${userId}`);
|
|
267
|
+
}
|
|
268
|
+
// Check if token is expired
|
|
269
|
+
if (session.expiresOn < Date.now()) {
|
|
270
|
+
await this.refreshUserToken(userId);
|
|
271
|
+
}
|
|
272
|
+
const client = Client.init({
|
|
273
|
+
authProvider: (done) => {
|
|
274
|
+
const updatedSession = this.sessions.get(userId);
|
|
275
|
+
done(null, updatedSession?.accessToken || '');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return client;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Refresh user token
|
|
282
|
+
*/
|
|
283
|
+
async refreshUserToken(userId) {
|
|
284
|
+
const session = this.sessions.get(userId);
|
|
285
|
+
if (!session?.account) {
|
|
286
|
+
throw new Error(`No account information available for user: ${userId}`);
|
|
287
|
+
}
|
|
288
|
+
if (!this.credentials) {
|
|
289
|
+
throw new Error('MS365 credentials not configured');
|
|
290
|
+
}
|
|
291
|
+
const msalClient = this.createMsalClient();
|
|
292
|
+
try {
|
|
293
|
+
const tokenResponse = await msalClient.acquireTokenSilent({
|
|
294
|
+
scopes: SCOPES,
|
|
295
|
+
account: session.account
|
|
296
|
+
});
|
|
297
|
+
if (!tokenResponse) {
|
|
298
|
+
throw new Error('Failed to refresh token');
|
|
299
|
+
}
|
|
300
|
+
// Update session
|
|
301
|
+
session.accessToken = tokenResponse.accessToken;
|
|
302
|
+
session.refreshToken = ''; // MSAL handles refresh tokens internally
|
|
303
|
+
session.expiresOn = tokenResponse.expiresOn?.getTime() || 0;
|
|
304
|
+
session.account = tokenResponse.account || session.account;
|
|
305
|
+
this.sessions.set(userId, session);
|
|
306
|
+
this.saveUserSessions();
|
|
307
|
+
logger.log(`Token refreshed for user ${userId}`);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
logger.error(`Token refresh failed for user ${userId}:`, error);
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get user session information
|
|
316
|
+
*/
|
|
317
|
+
getUserSession(userId) {
|
|
318
|
+
return this.sessions.get(userId) || null;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get all authenticated users
|
|
322
|
+
*/
|
|
323
|
+
getAuthenticatedUsers() {
|
|
324
|
+
return Array.from(this.sessions.values()).filter(session => session.authenticated);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Remove user session
|
|
328
|
+
*/
|
|
329
|
+
removeUser(userId) {
|
|
330
|
+
const success = this.sessions.delete(userId);
|
|
331
|
+
if (success) {
|
|
332
|
+
this.saveUserSessions();
|
|
333
|
+
logger.log(`Removed user session: ${userId}`);
|
|
334
|
+
}
|
|
335
|
+
return success;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Clear all user sessions
|
|
339
|
+
*/
|
|
340
|
+
clearAllSessions() {
|
|
341
|
+
this.sessions.clear();
|
|
342
|
+
this.saveUserSessions();
|
|
343
|
+
logger.log('Cleared all user sessions');
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Check if user is authenticated
|
|
347
|
+
*/
|
|
348
|
+
isUserAuthenticated(userId) {
|
|
349
|
+
const session = this.sessions.get(userId);
|
|
350
|
+
return session?.authenticated || false;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get user count
|
|
354
|
+
*/
|
|
355
|
+
getUserCount() {
|
|
356
|
+
return this.sessions.size;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
export const multiUserMS365Auth = new MultiUserMS365Auth();
|
package/install.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
console.log('Setting up MS365 MCP Server...');
|
|
11
|
+
|
|
12
|
+
// Make CLI executable
|
|
13
|
+
const binPath = path.join(__dirname, 'bin', 'cli.js');
|
|
14
|
+
if (fs.existsSync(binPath)) {
|
|
15
|
+
try {
|
|
16
|
+
fs.chmodSync(binPath, '755');
|
|
17
|
+
console.log('✓ Made CLI executable');
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.warn('Warning: Could not make CLI executable:', error.message);
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
console.warn('Warning: CLI file not found at', binPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Display setup message
|
|
26
|
+
console.log(`
|
|
27
|
+
✓ MS365 MCP Server installed successfully!
|
|
28
|
+
|
|
29
|
+
Next steps:
|
|
30
|
+
1. Set up authentication: npx ms365-mcp-server --setup-auth
|
|
31
|
+
2. Start the server: npx ms365-mcp-server
|
|
32
|
+
|
|
33
|
+
For help: npx ms365-mcp-server --help
|
|
34
|
+
|
|
35
|
+
Authentication Setup:
|
|
36
|
+
- Register app in Azure Portal: https://portal.azure.com
|
|
37
|
+
- Set environment variables: MS365_CLIENT_ID, MS365_CLIENT_SECRET, MS365_TENANT_ID
|
|
38
|
+
- Or use interactive setup with --setup-auth
|
|
39
|
+
|
|
40
|
+
Documentation: https://github.com/yourusername/ms365-mcp-server
|
|
41
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ms365-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ms365-mcp-server": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"postinstall": "node install.js",
|
|
16
|
+
"prepare": "chmod +x bin/cli.js && npm run build",
|
|
17
|
+
"install-global": "npm install -g .",
|
|
18
|
+
"postpublish": "echo 'Package published! Note: Users can test with: npx ms365-mcp-server --help'",
|
|
19
|
+
"test-npx": "cd /tmp && npx -y ms365-mcp-server@latest --help",
|
|
20
|
+
"version-bump": "node scripts/version-bump.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"microsoft365",
|
|
25
|
+
"outlook",
|
|
26
|
+
"email",
|
|
27
|
+
"office365",
|
|
28
|
+
"graph-api",
|
|
29
|
+
"oauth2",
|
|
30
|
+
"claude"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.10.1",
|
|
35
|
+
"@azure/msal-node": "^2.6.6",
|
|
36
|
+
"@microsoft/microsoft-graph-client": "^3.0.7",
|
|
37
|
+
"isomorphic-fetch": "^3.0.0",
|
|
38
|
+
"open": "^10.1.0",
|
|
39
|
+
"mime-types": "^2.1.35",
|
|
40
|
+
"keytar": "^7.9.0",
|
|
41
|
+
"typescript": "^5.0.4"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.2.3",
|
|
45
|
+
"@types/mime-types": "^2.1.4",
|
|
46
|
+
"@types/isomorphic-fetch": "^0.0.36",
|
|
47
|
+
"ts-node": "^10.9.1",
|
|
48
|
+
"tsx": "^4.19.4"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"bin/",
|
|
52
|
+
"dist/",
|
|
53
|
+
"README.md",
|
|
54
|
+
"install.js"
|
|
55
|
+
],
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|